PluginProbe
The Innovative Form Builder – IvyForms / 0.9.1
The Innovative Form Builder – IvyForms v0.9.1
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.9.1, at backend/src/Repository/Entry/EntryRepository.php

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