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

460 lines 13.9 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->getData();
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 if (!in_array($sortBy, $sortableColumns, true)) {
180 $sortBy = 'id';
181 }
182
183 // Pagination
184 $page = max(($params['page'] ?? 1), 1);
185 $perPage = max(($params['perPage'] ?? 10), 1);
186 $offset = ($page - 1) * $perPage;
187 $queryParams[] = $perPage;
188 $queryParams[] = $offset;
189
190 // Main query
191 $sql = "SELECT DISTINCT e.* FROM {$entriesTable} e $join
192 WHERE {$whereClauses}
193 ORDER BY e.{$sortBy} {$order} LIMIT %d OFFSET %d";
194 $results = $this->wpdb->get_results($this->wpdb->prepare($sql, $queryParams), ARRAY_A);
195 if ($results === false) {
196 throw new QueryExecutionException(
197 BackendStrings::getExceptionStrings()['unable_search_entries'] . __CLASS__
198 );
199 }
200
201 // Total count for pagination meta (exclude LIMIT/OFFSET)
202 $countParams = $queryParams;
203 array_pop($countParams); // offset
204 array_pop($countParams); // perPage
205 $countSql = "SELECT COUNT(DISTINCT e.id) FROM {$entriesTable} e $join WHERE {$whereClauses}";
206 $total = (int)$this->wpdb->get_var($this->wpdb->prepare($countSql, $countParams));
207
208 return [
209 'data' => $results,
210 'meta' => [
211 'page' => $page,
212 'perPage' => $perPage,
213 'total' => $total,
214 ],
215 ];
216 }
217
218 /**
219 * Override addSearchClauses to support searching fieldValue and form name.
220 * Extracted search conditions to a helper for complexity reduction.
221 *
222 * @param array<string, mixed>|null $params
223 * @param array<string> $whereClauses
224 * @param array<int|string, mixed> $queryParams
225 *
226 * @return void
227 */
228 protected function addSearchClauses(?array $params, array &$whereClauses, array &$queryParams): void
229 {
230 if (!empty($params['search'])) {
231 $searchEscaped = '%' . $this->wpdb->esc_like($params['search']) . '%';
232 $searchConditions = $this->getEntrySearchConditions($params);
233 $whereClauses[] = '(' . implode(' OR ', $searchConditions) . ')';
234 // Add the same number of params as conditions, no unused variable
235 $queryParams = array_merge($queryParams, array_fill(0, count($searchConditions), $searchEscaped));
236 }
237 }
238
239 /**
240 * Helper to return search conditions for entries (for complexity reduction)
241 *
242 * @param array<string, mixed>|null $params
243 * @return array<string>
244 */
245 protected function getEntrySearchConditions(?array $params = null): array
246 {
247 $conditions = [
248 'e.id LIKE %s',
249 'e.formId LIKE %s',
250 'f.name LIKE %s',
251 ];
252 // Only include fieldValue if searchFieldValue is true (default true for BC)
253 if (!isset($params['searchFieldValue']) || $params['searchFieldValue']) {
254 $conditions[] = 'eft.fieldValue LIKE %s';
255 }
256 return $conditions;
257 }
258
259 /**
260 * Override addFilterClauses to prefix columns with the correct alias (e.g., e.)
261 * so that dynamic filtering works with JOINs and aliases.
262 */
263 protected function addFilterClauses(?array $params, array &$whereClauses, array &$queryParams): void
264 {
265 if (!empty($params['filters'])) {
266 $filterableColumns = $this->getFilterableColumns();
267 foreach ($params['filters'] as $key => $value) {
268 if (in_array($key, $filterableColumns, true) && $value !== null && $value !== '') {
269 // Prefix with e. for the entries table
270 $whereClauses[] = "e.{$key} = %s";
271 $queryParams[] = sanitize_text_field($value);
272 }
273 }
274 }
275 }
276
277 /**
278 * Get the columns that can be searched in the database (including virtual columns for search).
279 *
280 * @return array<string>
281 */
282 protected function getSearchableColumns(): array
283 {
284 return [
285 'id',
286 'formId',
287 'fieldValue', // virtual, from EntryFields table
288 'name', // virtual, from Forms table
289 ];
290 }
291
292 /**
293 * Get filterable columns
294 *
295 * @return array<string>
296 */
297 protected function getFilterableColumns(): array
298 {
299 return ['starred', 'status', 'formId'];
300 }
301
302 /**
303 * Get sortable columns
304 *
305 * @return array<string>
306 */
307 protected function getSortableColumns(): array
308 {
309 return ['id', 'dateCreated'];
310 }
311
312 /**
313 * Get the date column
314 *
315 * @return string
316 */
317 protected function getDateColumn(): string
318 {
319 return 'e.dateCreated';
320 }
321
322 /**
323 * @param int $entryId
324 *
325 * @return bool
326 */
327 public function exists(int $entryId): bool
328 {
329 $query = $this->wpdb->prepare(
330 "SELECT COUNT(*) FROM {$this->table} WHERE id = %d",
331 $entryId
332 );
333
334 $count = (int) $this->wpdb->get_var($query);
335
336 return $count > 0;
337 }
338
339 /**
340 * Get the count of entries for a specific form.
341 *
342 * @param int $formId
343 * @return int
344 */
345 public function getCountByFormId(int $formId): int
346 {
347 $query = $this->wpdb->prepare(
348 "SELECT COUNT(*) FROM {$this->table} WHERE formId = %d",
349 $formId
350 );
351
352 $count = (int) $this->wpdb->get_var($query);
353
354 return $count;
355 }
356
357 /**
358 * Get the count of entries for the filter dropdown.
359 *
360 * @param array<string, mixed>|null $params
361 * @return array<string, int>
362 */
363 public function getFilterCount(?array $params = null): array
364 {
365 $where = '';
366 $queryParams = [];
367 if (!empty($params['filters']['formId'])) {
368 $where = 'WHERE formId = %d';
369 $queryParams[] = (int)$params['filters']['formId'];
370 }
371 $sql = "SELECT
372 SUM(status = 'read') as readTrueCount,
373 SUM(status = 'unread') as readFalseCount,
374 SUM(starred = 1) as starredTrueCount,
375 SUM(starred = 0) as starredFalseCount
376 FROM {$this->table} $where";
377 $result = $this->wpdb->get_row(
378 $queryParams ? $this->wpdb->prepare($sql, ...$queryParams) : $sql,
379 ARRAY_A
380 );
381 // Convert all values to integers and provide defaults if query returns null
382 return array_map('intval', $result ?: [
383 'readTrueCount' => 0,
384 'readFalseCount' => 0,
385 'starredTrueCount' => 0,
386 'starredFalseCount' => 0,
387 ]);
388 }
389
390 /**
391 * Get entry fields for the given entries.
392 *
393 * @param array<int, array<string, mixed>> $entries
394 * @return array<mixed>
395 *
396 * @throws QueryExecutionException|InvalidArgumentException
397 */
398 public function getEntryFields(array $entries): array
399 {
400 if (empty($entries)) {
401 return [];
402 }
403
404 // Extract entry IDs from the entries array
405 $entryIds = array_column($entries, 'id');
406
407 // Prepare placeholders for the query
408 $placeholders = implode(',', array_fill(0, count($entryIds), '%d'));
409 $entryFieldsTable = EntryFieldsTable::getTableName();
410
411 // Query to fetch fields for the given entry IDs
412 $query = "SELECT * FROM {$entryFieldsTable} WHERE entryId IN ($placeholders)";
413
414 // Execute the query with the entry IDs as parameters
415 $results = $this->wpdb->get_results(
416 $this->wpdb->prepare($query, $entryIds),
417 ARRAY_A
418 );
419
420 if ($results === false) {
421 throw new QueryExecutionException(
422 BackendStrings::getExceptionStrings()['unable_get_entry_fields'] . __CLASS__
423 );
424 }
425
426 return $results;
427 }
428
429 /**
430 * Get the count of entries for multiple form IDs.
431 *
432 * @param int[] $formIds
433 * @return array<int, int> formId => count
434 * @throws InvalidArgumentException
435 */
436 public function getEntryCountByFormIds(array $formIds): array
437 {
438 if (empty($formIds)) {
439 return [];
440 }
441 $formIds = array_map('intval', $formIds);
442 $placeholders = implode(',', array_fill(0, count($formIds), '%d'));
443 $tableName = EntriesTable::getTableName();
444 $query = "SELECT formId, COUNT(*) as count FROM {$tableName} WHERE formId IN ($placeholders) GROUP BY formId";
445 $results = $this->wpdb->get_results($this->wpdb->prepare($query, $formIds), ARRAY_A);
446 $counts = [];
447 foreach ($results as $row) {
448 $counts[(int)$row['formId']] = (int)$row['count'];
449 }
450
451 // Ensure all requested IDs are present in the result (default to 0)
452 foreach ($formIds as $id) {
453 if (!isset($counts[$id])) {
454 $counts[$id] = 0;
455 }
456 }
457 return $counts;
458 }
459 }
460