PluginProbe
The Innovative Form Builder – IvyForms / 1.5
The Innovative Form Builder – IvyForms v1.5
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
← All changes | backend/src/Repository/Entry/EntryRepository.php +147 -127 0.3 → 1.5 View file →
@@ -1,23 +1,21 @@
1 1 <?php
2 2
3 3 /**
4 - * @copyright © TMS-Plugins. All rights reserved.
4 + * @copyright © Melograno Venture Studio. All rights reserved.
5 5 * @licence See LICENCE.md for license details.
6 6 */
7 7
8 8 namespace IvyForms\Repository\Entry;
9 9
10 -// phpcs:disable PSR1.Files.SideEffects
11 -if (!defined('ABSPATH')) {
12 - exit; // Exit if accessed directly
13 -}
14 -
15 10 use IvyForms\Common\Exceptions\InvalidArgumentException;
16 11 use IvyForms\Common\Exceptions\QueryExecutionException;
12 +use IvyForms\Common\Helpers\EntryBatchUpdater;
13 +use IvyForms\Common\Helpers\EntryQueryHelper;
17 14 use IvyForms\Entity\Entry\Entry;
18 15 use IvyForms\Factory\Entry\EntryFactory;
19 16 use IvyForms\Repository\AbstractRepository;
17 +use IvyForms\Repository\VisibleFormIdsQueryFilter;
20 18 use IvyForms\Services\InstallActions\DB\Entry\EntriesTable;
21 19 use IvyForms\Services\InstallActions\DB\EntryField\EntryFieldsTable as EntryFieldsTable;
22 20 use IvyForms\Services\InstallActions\DB\Form\FormsTable;
23 21 use IvyForms\Services\Translations\BackendStrings;
@@ -25,8 +23,12 @@
25 23 class EntryRepository extends AbstractRepository implements EntryRepositoryInterface
26 24 {
27 25 public const FACTORY = EntryFactory::class;
28 26
27 + // Column constants
28 + public const COLUMN_STATUS = 'status';
29 + public const COLUMN_STARRED = 'starred';
30 +
29 31 /**
30 32 * Add entry to the database
31 33 *
32 34 * @param Entry $entity
@@ -36,17 +38,20 @@
36 38 * @throws QueryExecutionException
37 39 */
38 40 public function add($entity): int
39 41 {
40 - $data = $entity->getData();
42 + $data = $entity->toArray(true);
41 43
44 + $dateCreated = !empty($data['dateCreated']) ? (string)$data['dateCreated'] : current_time('mysql');
45 + $dateEdited = !empty($data['dateEdited']) ? (string)$data['dateEdited'] : $dateCreated;
46 +
42 47 $result = $this->wpdb->insert(
43 48 $this->table,
44 49 [
45 50 'formId' => $data['formId'],
46 51 'userId' => $data['userId'],
47 - 'dateCreated' => current_time('mysql'),
48 - 'dateEdited' => current_time('mysql'),
52 + 'dateCreated' => $dateCreated,
53 + 'dateEdited' => $dateEdited,
49 54 'status' => $data['status'],
50 55 'ipAddress' => $data['ipAddress'],
51 56 'userAgent' => $data['userAgent'],
52 57 'sourceURL' => $data['sourceURL'],
@@ -166,33 +171,39 @@
166 171 $whereClauses = $this->buildWhereClauses($params, $queryParams);
167 172
168 173 // Always join entry fields and forms for search/filter/sort
169 174 $entriesTable = EntriesTable::getTableName();
170 - $entryFieldsTable = EntryFieldsTable::getTableName();
175 + $join = EntryQueryHelper::getEntryJoinClause();
171 176 $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 177
175 178 // Sorting
176 179 $sortableColumns = $this->getSortableColumns();
177 180 $sortBy = $params['orderBy'] ?? 'id';
178 181 $order = strtolower($params['order'] ?? 'asc') === 'asc' ? 'ASC' : 'DESC';
179 - if (!in_array($sortBy, $sortableColumns, true)) {
180 - $sortBy = 'id';
182 + // Map 'formName' to 'f.name' for sorting
183 + $sortColumn = 'e.id';
184 + if ($sortBy === 'formName') {
185 + $sortColumn = 'f.name';
186 + } elseif (in_array($sortBy, $sortableColumns, true)) {
187 + $sortColumn = 'e.' . $sortBy;
181 188 }
182 189
183 190 // Pagination
184 191 $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);
192 + $perPage = $params['perPage'] ?? 10;
193 + $queryBuild = EntryQueryHelper::buildEntrySelectQuery(
194 + $entriesTable,
195 + $join,
196 + $whereClauses,
197 + $sortColumn,
198 + $order,
199 + $queryParams,
200 + $perPage,
201 + $page
202 + );
203 + $sql = $queryBuild['sql'];
204 + $mainParams = $queryBuild['params'];
205 + $results = $this->wpdb->get_results($this->wpdb->prepare($sql, $mainParams), ARRAY_A);
195 206 if ($results === false) {
196 207 throw new QueryExecutionException(
197 208 BackendStrings::getExceptionStrings()['unable_search_entries'] . __CLASS__
198 209 );
@@ -198,14 +209,29 @@
198 209 );
199 210 }
200 211
201 212 // Total count for pagination meta (exclude LIMIT/OFFSET)
202 - $countParams = $queryParams;
203 - array_pop($countParams); // offset
204 - array_pop($countParams); // perPage
205 213 $countSql = "SELECT COUNT(DISTINCT e.id) FROM {$entriesTable} e $join WHERE {$whereClauses}";
206 - $total = (int)$this->wpdb->get_var($this->wpdb->prepare($countSql, $countParams));
214 + $total = (int)$this->wpdb->get_var($this->wpdb->prepare($countSql, $queryParams));
207 215
216 + // Fetch form names for all unique formIds in the results
217 + $formIdToName = [];
218 + $formNamesQuery = EntryQueryHelper::getFormNamesByIdsQuery($results, $formsTable);
219 + if (!empty($formNamesQuery['sql'])) {
220 + $formRows = $this->wpdb->get_results(
221 + $this->wpdb->prepare($formNamesQuery['sql'], $formNamesQuery['params']),
222 + ARRAY_A
223 + );
224 + if ($formRows === null) {
225 + throw new QueryExecutionException(
226 + BackendStrings::getExceptionStrings()['unable_get_form_names'] . __CLASS__
227 + );
228 + }
229 + foreach ($formRows as $row) {
230 + $formIdToName[$row['id']] = $row['name'];
231 + }
232 + }
233 +
208 234 return [
209 235 'data' => $results,
210 236 'meta' => [
211 237 'page' => $page,
@@ -211,68 +237,39 @@
211 237 'page' => $page,
212 238 'perPage' => $perPage,
213 239 'total' => $total,
214 240 ],
241 + 'formIdToName' => $formIdToName,
215 242 ];
216 243 }
217 244
218 245 /**
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
246 + * Override addSearchClauses to use EntryQueryHelper for search WHERE clause construction.
227 247 */
228 248 protected function addSearchClauses(?array $params, array &$whereClauses, array &$queryParams): void
229 249 {
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));
250 + $searchResult = EntryQueryHelper::buildSearchWhereClause($params);
251 + if ($searchResult['where']) {
252 + $whereClauses[] = $searchResult['where'];
253 + $queryParams = array_merge($queryParams, $searchResult['params']);
236 254 }
237 255 }
238 256
239 257 /**
240 - * Helper to return search conditions for entries (for complexity reduction)
241 - *
258 + * Override addFilterClauses to use EntryQueryHelper for filter WHERE clause construction.
242 259 * @param array<string, mixed>|null $params
243 - * @return array<string>
244 260 */
245 - protected function getEntrySearchConditions(?array $params = null): array
261 + protected function addFilterClauses(?array $params, array &$whereClauses, array &$queryParams): void
246 262 {
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';
263 + $filterableColumns = $this->getFilterableColumns();
264 + $filterResult = EntryQueryHelper::buildFilterWhereClause($params, $filterableColumns);
265 + if (!empty($filterResult['where'])) {
266 + $whereClauses = array_merge($whereClauses, $filterResult['where']);
267 + // Ensure all values are strings
268 + $queryParams = array_merge($queryParams, array_map('strval', $filterResult['params']));
255 269 }
256 - return $conditions;
257 - }
258 270
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 - }
271 + VisibleFormIdsQueryFilter::apply($params, $whereClauses, $queryParams, 'e.formId');
275 272 }
276 273
277 274 /**
278 275 * Get the columns that can be searched in the database (including virtual columns for search).
@@ -295,9 +292,9 @@
295 292 * @return array<string>
296 293 */
297 294 protected function getFilterableColumns(): array
298 295 {
299 - return ['starred', 'status', 'formId'];
296 + return ['starred', 'status', 'formId', 'userId'];
300 297 }
301 298
302 299 /**
303 300 * Get sortable columns
@@ -305,9 +302,9 @@
305 302 * @return array<string>
306 303 */
307 304 protected function getSortableColumns(): array
308 305 {
309 - return ['id', 'dateCreated'];
306 + return ['id', 'dateCreated', 'formName'];
310 307 }
311 308
312 309 /**
313 310 * Get the date column
@@ -322,8 +319,9 @@
322 319 /**
323 320 * @param int $entryId
324 321 *
325 322 * @return bool
323 + * @throws QueryExecutionException
326 324 */
327 325 public function exists(int $entryId): bool
328 326 {
329 327 $query = $this->wpdb->prepare(
@@ -330,11 +328,15 @@
330 328 "SELECT COUNT(*) FROM {$this->table} WHERE id = %d",
331 329 $entryId
332 330 );
333 331
334 - $count = (int) $this->wpdb->get_var($query);
335 -
336 - return $count > 0;
332 + $count = $this->wpdb->get_var($query);
333 + if ($count === false) {
334 + throw new QueryExecutionException(
335 + BackendStrings::getExceptionStrings()['unable_find_by_id'] . __CLASS__
336 + );
337 + }
338 + return (int)$count > 0;
337 339 }
338 340
339 341 /**
340 342 * Get the count of entries for a specific form.
@@ -340,19 +342,21 @@
340 342 * Get the count of entries for a specific form.
341 343 *
342 344 * @param int $formId
343 345 * @return int
346 + * @throws QueryExecutionException
344 347 */
345 348 public function getCountByFormId(int $formId): int
346 349 {
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;
350 + $queryData = EntryQueryHelper::getCountByFormIdQuery($this->table, $formId);
351 + $query = $this->wpdb->prepare($queryData['sql'], $queryData['params']);
352 + $count = $this->wpdb->get_var($query);
353 + if ($count === null) {
354 + throw new QueryExecutionException(
355 + BackendStrings::getExceptionStrings()['unable_find_by_id'] . __CLASS__
356 + );
357 + }
358 + return (int)$count;
355 359 }
356 360
357 361 /**
358 362 * Get the count of entries for the filter dropdown.
@@ -358,28 +362,28 @@
358 362 * Get the count of entries for the filter dropdown.
359 363 *
360 364 * @param array<string, mixed>|null $params
361 365 * @return array<string, int>
366 + * @throws QueryExecutionException
362 367 */
363 368 public function getFilterCount(?array $params = null): array
364 369 {
365 - $where = '';
366 - $queryParams = [];
367 - if (!empty($params['filters']['formId'])) {
368 - $where = 'WHERE formId = %d';
369 - $queryParams[] = (int)$params['filters']['formId'];
370 + $queryData = EntryQueryHelper::getFilterCountQuery($this->table, $params);
371 + $result = null;
372 + if (!empty($queryData['params'])) {
373 + $result = $this->wpdb->get_row(
374 + $this->wpdb->prepare($queryData['sql'], ...$queryData['params']),
375 + ARRAY_A
376 + );
370 377 }
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
378 + if ($result === null) {
379 + $result = $this->wpdb->get_row($queryData['sql'], ARRAY_A);
380 + }
381 + if ($result === null) {
382 + throw new QueryExecutionException(
383 + BackendStrings::getExceptionStrings()['unable_search_entries'] . __CLASS__
384 + );
385 + }
382 386 return array_map('intval', $result ?: [
383 387 'readTrueCount' => 0,
384 388 'readFalseCount' => 0,
385 389 'starredTrueCount' => 0,
@@ -396,34 +400,21 @@
396 400 * @throws QueryExecutionException|InvalidArgumentException
397 401 */
398 402 public function getEntryFields(array $entries): array
399 403 {
400 - if (empty($entries)) {
404 + $queryData = EntryQueryHelper::getEntryFieldsQuery($entries);
405 + if (empty($queryData['sql'])) {
401 406 return [];
402 407 }
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 408 $results = $this->wpdb->get_results(
416 - $this->wpdb->prepare($query, $entryIds),
409 + $this->wpdb->prepare($queryData['sql'], $queryData['params']),
417 410 ARRAY_A
418 411 );
419 -
420 - if ($results === false) {
412 + if ($results === null) {
421 413 throw new QueryExecutionException(
422 414 BackendStrings::getExceptionStrings()['unable_get_entry_fields'] . __CLASS__
423 415 );
424 416 }
425 -
426 417 return $results;
427 418 }
428 419
429 420 /**
@@ -430,30 +421,59 @@
430 421 * Get the count of entries for multiple form IDs.
431 422 *
432 423 * @param int[] $formIds
433 424 * @return array<int, int> formId => count
425 + * @throws QueryExecutionException
434 426 * @throws InvalidArgumentException
435 427 */
436 428 public function getEntryCountByFormIds(array $formIds): array
437 429 {
438 - if (empty($formIds)) {
430 + $queryData = EntryQueryHelper::getEntryCountByFormIdsQuery($formIds);
431 + if (empty($queryData['sql'])) {
439 432 return [];
440 433 }
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);
434 + $results = $this->wpdb->get_results(
435 + $this->wpdb->prepare($queryData['sql'], $queryData['params']),
436 + ARRAY_A
437 + );
438 + if ($results === null) {
439 + throw new QueryExecutionException(
440 + BackendStrings::getExceptionStrings()['unable_search_entries'] . __CLASS__
441 + );
442 + }
446 443 $counts = [];
447 444 foreach ($results as $row) {
448 445 $counts[(int)$row['formId']] = (int)$row['count'];
449 446 }
450 -
451 - // Ensure all requested IDs are present in the result (default to 0)
452 - foreach ($formIds as $id) {
447 + foreach ($queryData['formIds'] as $id) {
453 448 if (!isset($counts[$id])) {
454 449 $counts[$id] = 0;
455 450 }
456 451 }
457 452 return $counts;
453 + }
454 +
455 + /**
456 + * Resolve owning form IDs for the given entry IDs in a single query.
457 + *
458 + * @param int[] $entryIds
459 + * @return array<int, int> entryId => formId
460 + * @throws QueryExecutionException
461 + */
462 + public function getFormIdsByEntryIds(array $entryIds): array
463 + {
464 + return EntryQueryHelper::fetchFormIdsByEntryIds($this->wpdb, $entryIds);
465 + }
466 +
467 + /**
468 + * Get allowed columns for bulk update operations
469 + *
470 + * @return string[]
471 + */
472 + protected function getAllowedBulkUpdateColumns(): array
473 + {
474 + return [
475 + self::COLUMN_STARRED,
476 + self::COLUMN_STATUS,
477 + ];
458 478 }
459 479 }