PluginProbe
The Innovative Form Builder – IvyForms / 0.8
The Innovative Form Builder – IvyForms v0.8
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 1.2 1.3
ivyforms / backend / src / Repository / Entry / EntryRepository.php

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

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