PluginProbe
The Innovative Form Builder – IvyForms / 0.4
The Innovative Form Builder – IvyForms v0.4
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / backend / src / Repository / Entry / EntryRepository.php

EntryRepository.php in The Innovative Form Builder – IvyForms 0.4, at backend/src/Repository/Entry/EntryRepository.php

460 lines 14.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * @copyright © TMS-Plugins. All rights reserved.
5 * @licence See LICENCE.md for license details.
6 */
7
8 namespace IvyForms\Repository\Entry;
9
10 // phpcs:disable PSR1.Files.SideEffects
11 if (!defined('ABSPATH')) {
12 exit; // Exit if accessed directly
13 }
14
15 use IvyForms\Common\Exceptions\InvalidArgumentException;
16 use IvyForms\Common\Exceptions\QueryExecutionException;
17 use IvyForms\Entity\Entry\Entry;
18 use IvyForms\Factory\Entry\EntryFactory;
19 use IvyForms\Repository\AbstractRepository;
20 use IvyForms\Services\InstallActions\DB\Entry\EntriesTable;
21 use IvyForms\Services\InstallActions\DB\EntryField\EntryFieldsTable as EntryFieldsTable;
22 use IvyForms\Services\InstallActions\DB\Form\FormsTable;
23 use IvyForms\Services\Translations\BackendStrings;
24
25 class EntryRepository extends AbstractRepository implements EntryRepositoryInterface
26 {
27 public const FACTORY = EntryFactory::class;
28
29 /**
30 * Add entry to the database
31 *
32 * @param Entry $entity
33 *
34 * @return int
35 *
36 * @throws QueryExecutionException
37 */
38 public function add($entity): int
39 {
40 $data = $entity->toArray(true);
41
42 $result = $this->wpdb->insert(
43 $this->table,
44 [
45 'formId' => $data['formId'],
46 'userId' => $data['userId'],
47 'dateCreated' => current_time('mysql'),
48 'dateEdited' => current_time('mysql'),
49 'status' => $data['status'],
50 'ipAddress' => $data['ipAddress'],
51 'userAgent' => $data['userAgent'],
52 'sourceURL' => $data['sourceURL'],
53 'starred' => $data['starred'] ? 1 : 0,
54 ],
55 ['%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%d']
56 );
57
58 if ($result === false) {
59 throw new QueryExecutionException(
60 BackendStrings::getExceptionStrings()['unable_to_add_data'] . __CLASS__
61 );
62 }
63
64 return $this->wpdb->insert_id;
65 }
66
67 /**
68 * Update entry in the database
69 *
70 * @param int $id
71 * @param Entry $entity
72 *
73 * @return bool
74 *
75 * @throws QueryExecutionException
76 */
77 public function update(int $id, $entity): bool
78 {
79 $result = $this->wpdb->update(
80 $this->table,
81 [
82 'dateEdited' => current_time('mysql'),
83 ],
84 ['id' => $id],
85 ['%s'],
86 ['%d']
87 );
88
89 if ($result === false) {
90 throw new QueryExecutionException(
91 BackendStrings::getExceptionStrings()['unable_update_data'] . __CLASS__
92 );
93 }
94
95 return (bool)$result;
96 }
97
98 /**
99 * Update the starred status of an entry.
100 *
101 * @param int $entryId
102 * @param bool $value
103 *
104 * @return void
105 * @throws QueryExecutionException
106 */
107 public function updateEntryStarred(int $entryId, bool $value): void
108 {
109 $result = $this->wpdb->update(
110 $this->table,
111 [
112 'starred' => $value ? 1 : 0,
113 'dateEdited' => current_time('mysql'),
114 ],
115 ['id' => $entryId],
116 ['%d', '%s'],
117 ['%d']
118 );
119
120 if ($result === false) {
121 throw new QueryExecutionException(
122 BackendStrings::getExceptionStrings()['unable_update_starred'] . __CLASS__
123 );
124 }
125 }
126
127 /**
128 * Update the status of an entry.
129 *
130 * @param int $entryId
131 * @param string $status
132 *
133 * @return void
134 * @throws QueryExecutionException
135 */
136 public function updateEntryStatus(int $entryId, string $status): void
137 {
138 $result = $this->wpdb->update(
139 $this->table,
140 [
141 'status' => $status,
142 'dateEdited' => current_time('mysql'),
143 ],
144 ['id' => $entryId],
145 ['%s', '%s'],
146 ['%d']
147 );
148
149 if ($result === false) {
150 throw new QueryExecutionException(
151 BackendStrings::getExceptionStrings()['unable_update_status'] . __CLASS__
152 );
153 }
154 }
155
156 /**
157 * Search for entries with extended logic (field value, form name) and return paginated results with meta.
158 *
159 * @param array<string, mixed>|null $params
160 * @return array<mixed>
161 * @throws InvalidArgumentException|QueryExecutionException
162 */
163 public function search(?array $params): array
164 {
165 $queryParams = [];
166 $whereClauses = $this->buildWhereClauses($params, $queryParams);
167
168 // Always join entry fields and forms for search/filter/sort
169 $entriesTable = EntriesTable::getTableName();
170 $entryFieldsTable = EntryFieldsTable::getTableName();
171 $formsTable = FormsTable::getTableName();
172 $join = " LEFT JOIN {$entryFieldsTable} eft ON e.id = eft.entryId ";
173 $join .= " LEFT JOIN {$formsTable} f ON e.formId = f.id ";
174
175 // Sorting
176 $sortableColumns = $this->getSortableColumns();
177 $sortBy = $params['orderBy'] ?? 'id';
178 $order = strtolower($params['order'] ?? 'asc') === 'asc' ? 'ASC' : 'DESC';
179 // Map 'formName' to 'f.name' for sorting
180 $sortColumn = 'e.id';
181 if ($sortBy === 'formName') {
182 $sortColumn = 'f.name';
183 } elseif (in_array($sortBy, $sortableColumns, true)) {
184 $sortColumn = 'e.' . $sortBy;
185 }
186
187 // Pagination
188 $page = max(($params['page'] ?? 1), 1);
189 $perPage = max(($params['perPage'] ?? 10), 1);
190 $offset = ($page - 1) * $perPage;
191 $queryParams[] = $perPage;
192 $queryParams[] = $offset;
193
194 // Main query
195 $sql = "SELECT DISTINCT e.* FROM {$entriesTable} e $join
196 WHERE {$whereClauses}
197 ORDER BY {$sortColumn} {$order} LIMIT %d OFFSET %d";
198 $results = $this->wpdb->get_results($this->wpdb->prepare($sql, $queryParams), ARRAY_A);
199 if ($results === false) {
200 throw new QueryExecutionException(
201 BackendStrings::getExceptionStrings()['unable_search_entries'] . __CLASS__
202 );
203 }
204
205 // Total count for pagination meta (exclude LIMIT/OFFSET)
206 $countParams = $queryParams;
207 array_pop($countParams); // offset
208 array_pop($countParams); // perPage
209 $countSql = "SELECT COUNT(DISTINCT e.id) FROM {$entriesTable} e $join WHERE {$whereClauses}";
210 $total = (int)$this->wpdb->get_var($this->wpdb->prepare($countSql, $countParams));
211
212 // Fetch form names for all unique formIds in the results
213 $formIdToName = [];
214 if (!empty($results)) {
215 $formIds = array_unique(array_column($results, 'formId'));
216 if (!empty($formIds)) {
217 $placeholders = implode(',', array_fill(0, count($formIds), '%d'));
218 $formSql = "SELECT id, name FROM {$formsTable} WHERE id IN ($placeholders)";
219 $formRows = $this->wpdb->get_results($this->wpdb->prepare($formSql, $formIds), ARRAY_A);
220 if ($formRows === false) {
221 throw new QueryExecutionException(
222 BackendStrings::getExceptionStrings()['unable_get_form_names'] . __CLASS__
223 );
224 }
225 foreach ($formRows as $row) {
226 $formIdToName[$row['id']] = $row['name'];
227 }
228 }
229 }
230
231 return [
232 'data' => $results,
233 'meta' => [
234 'page' => $page,
235 'perPage' => $perPage,
236 'total' => $total,
237 ],
238 'formIdToName' => $formIdToName,
239 ];
240 }
241
242 /**
243 * Override addSearchClauses to support searching fieldValue and form name.
244 * Extracted search conditions to a helper for complexity reduction.
245 *
246 * @param array<string, mixed>|null $params
247 * @param array<string> $whereClauses
248 * @param array<int|string, mixed> $queryParams
249 *
250 * @return void
251 */
252 protected function addSearchClauses(?array $params, array &$whereClauses, array &$queryParams): void
253 {
254 if (!empty($params['search'])) {
255 $searchEscaped = '%' . $this->wpdb->esc_like($params['search']) . '%';
256 $searchConditions = EntryQueryHelper::getEntrySearchConditions($params);
257 $whereClauses[] = '(' . implode(' OR ', $searchConditions) . ')';
258 $queryParams = array_merge($queryParams, array_fill(0, count($searchConditions), $searchEscaped));
259 }
260 }
261
262 /**
263 * Override addFilterClauses to prefix columns with the correct alias (e.g., e.)
264 * so that dynamic filtering works with JOINs and aliases.
265 */
266 protected function addFilterClauses(?array $params, array &$whereClauses, array &$queryParams): void
267 {
268 if (!empty($params['filters'])) {
269 $filterableColumns = $this->getFilterableColumns();
270 foreach ($params['filters'] as $key => $value) {
271 if (in_array($key, $filterableColumns, true) && $value !== null && $value !== '') {
272 // Prefix with e. for the entries table
273 $whereClauses[] = "e.{$key} = %s";
274 $queryParams[] = sanitize_text_field($value);
275 }
276 }
277 }
278 }
279
280 /**
281 * Get the columns that can be searched in the database (including virtual columns for search).
282 *
283 * @return array<string>
284 */
285 protected function getSearchableColumns(): array
286 {
287 return [
288 'id',
289 'formId',
290 'fieldValue', // virtual, from EntryFields table
291 'name', // virtual, from Forms table
292 ];
293 }
294
295 /**
296 * Get filterable columns
297 *
298 * @return array<string>
299 */
300 protected function getFilterableColumns(): array
301 {
302 return ['starred', 'status', 'formId'];
303 }
304
305 /**
306 * Get sortable columns
307 *
308 * @return array<string>
309 */
310 protected function getSortableColumns(): array
311 {
312 return ['id', 'dateCreated', 'formName'];
313 }
314
315 /**
316 * Get the date column
317 *
318 * @return string
319 */
320 protected function getDateColumn(): string
321 {
322 return 'e.dateCreated';
323 }
324
325 /**
326 * @param int $entryId
327 *
328 * @return bool
329 * @throws QueryExecutionException
330 */
331 public function exists(int $entryId): bool
332 {
333 $query = $this->wpdb->prepare(
334 "SELECT COUNT(*) FROM {$this->table} WHERE id = %d",
335 $entryId
336 );
337
338 $count = $this->wpdb->get_var($query);
339 if ($count === false) {
340 throw new QueryExecutionException(
341 BackendStrings::getExceptionStrings()['unable_find_by_id'] . __CLASS__
342 );
343 }
344 return (int)$count > 0;
345 }
346
347 /**
348 * Get the count of entries for a specific form.
349 *
350 * @param int $formId
351 * @return int
352 * @throws QueryExecutionException
353 */
354 public function getCountByFormId(int $formId): int
355 {
356 $queryData = EntryQueryHelper::getCountByFormIdQuery($this->table, $formId);
357 $query = $this->wpdb->prepare($queryData['sql'], $queryData['params']);
358 $count = $this->wpdb->get_var($query);
359 if ($count === null) {
360 throw new QueryExecutionException(
361 BackendStrings::getExceptionStrings()['unable_find_by_id'] . __CLASS__
362 );
363 }
364 return (int)$count;
365 }
366
367 /**
368 * Get the count of entries for the filter dropdown.
369 *
370 * @param array<string, mixed>|null $params
371 * @return array<string, int>
372 * @throws QueryExecutionException
373 */
374 public function getFilterCount(?array $params = null): array
375 {
376 $queryData = EntryQueryHelper::getFilterCountQuery($this->table, $params);
377 $result = null;
378 if (!empty($queryData['params'])) {
379 $result = $this->wpdb->get_row(
380 $this->wpdb->prepare($queryData['sql'], ...$queryData['params']),
381 ARRAY_A
382 );
383 }
384 if ($result === null) {
385 $result = $this->wpdb->get_row($queryData['sql'], ARRAY_A);
386 }
387 if ($result === null) {
388 throw new QueryExecutionException(
389 BackendStrings::getExceptionStrings()['unable_search_entries'] . __CLASS__
390 );
391 }
392 return array_map('intval', $result ?: [
393 'readTrueCount' => 0,
394 'readFalseCount' => 0,
395 'starredTrueCount' => 0,
396 'starredFalseCount' => 0,
397 ]);
398 }
399
400 /**
401 * Get entry fields for the given entries.
402 *
403 * @param array<int, array<string, mixed>> $entries
404 * @return array<mixed>
405 *
406 * @throws QueryExecutionException|InvalidArgumentException
407 */
408 public function getEntryFields(array $entries): array
409 {
410 $queryData = EntryQueryHelper::getEntryFieldsQuery($entries);
411 if (empty($queryData['sql'])) {
412 return [];
413 }
414 $results = $this->wpdb->get_results(
415 $this->wpdb->prepare($queryData['sql'], $queryData['params']),
416 ARRAY_A
417 );
418 if ($results === null) {
419 throw new QueryExecutionException(
420 BackendStrings::getExceptionStrings()['unable_get_entry_fields'] . __CLASS__
421 );
422 }
423 return $results;
424 }
425
426 /**
427 * Get the count of entries for multiple form IDs.
428 *
429 * @param int[] $formIds
430 * @return array<int, int> formId => count
431 * @throws QueryExecutionException
432 */
433 public function getEntryCountByFormIds(array $formIds): array
434 {
435 $queryData = EntryQueryHelper::getEntryCountByFormIdsQuery($formIds);
436 if (empty($queryData['sql'])) {
437 return [];
438 }
439 $results = $this->wpdb->get_results(
440 $this->wpdb->prepare($queryData['sql'], $queryData['params']),
441 ARRAY_A
442 );
443 if ($results === null) {
444 throw new QueryExecutionException(
445 BackendStrings::getExceptionStrings()['unable_search_entries'] . __CLASS__
446 );
447 }
448 $counts = [];
449 foreach ($results as $row) {
450 $counts[(int)$row['formId']] = (int)$row['count'];
451 }
452 foreach ($queryData['formIds'] as $id) {
453 if (!isset($counts[$id])) {
454 $counts[$id] = 0;
455 }
456 }
457 return $counts;
458 }
459 }
460