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 / AbstractRepository.php

AbstractRepository.php in The Innovative Form Builder – IvyForms 0.9.1, at backend/src/Repository/AbstractRepository.php

397 lines 11.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 © Melograno Venture Studio. All rights reserved.
5 * @licence See LICENCE.md for license details.
6 */
7
8 namespace IvyForms\Repository;
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\NotFoundException;
17 use IvyForms\Common\Exceptions\QueryExecutionException;
18 use IvyForms\Services\Translations\BackendStrings;
19 use IvyForms\Repository\ColumnRepositoryTrait;
20
21 /**
22 * Class AbstractRepository
23 *
24 * @package IvyForms\Repository
25 */
26 class AbstractRepository
27 {
28 use ColumnRepositoryTrait;
29
30 public const FACTORY = '';
31
32 /** @var string */
33 protected string $table;
34
35 /** @var mixed */
36 protected $wpdb;
37
38 public function __construct(string $table)
39 {
40 global $wpdb;
41 $this->wpdb = $wpdb;
42 $this->table = $table;
43 }
44
45 /**
46 * @param int $id
47 * @return object|null
48 *
49 */
50 public function getById(int $id): ?object
51 {
52 $row = $this->wpdb->get_row(
53 $this->wpdb->prepare(
54 $this->selectQuery() . " WHERE $this->table.id = %d",
55 $id
56 ),
57 ARRAY_A
58 );
59
60 if (!$row) {
61 return null;
62 }
63
64 return call_user_func([static::FACTORY, 'create'], $row);
65 }
66
67 /**
68 * @return mixed[]
69 * @throws InvalidArgumentException
70 */
71 public function getAll(): array
72 {
73 $rows = $this->wpdb->get_results(
74 $this->selectQuery(),
75 ARRAY_A
76 );
77
78 $result = [];
79
80 foreach ($rows as $row) {
81 $result[] = call_user_func([static::FACTORY, 'create'], $row);
82 }
83
84 return $result;
85 }
86
87 /**
88 * @param int $id
89 * @return int
90 * @throws QueryExecutionException
91 */
92 public function delete(int $id): int
93 {
94 $result = $this->wpdb->query(
95 $this->wpdb->prepare(
96 "DELETE FROM $this->table WHERE id = %d",
97 $id
98 )
99 );
100
101 if ($result === false) {
102 throw new QueryExecutionException(
103 sprintf(BackendStrings::getExceptionStrings()['failed_delete_by_id'], $id, $this->table)
104 );
105 }
106
107 return $result;
108 }
109
110 /**
111 * Delete multiple entities by their IDs in bulk.
112 *
113 * @param array<int> $ids
114 * @return int Number of deleted entities
115 * @throws QueryExecutionException
116 */
117 public function deleteMany(array $ids): int
118 {
119 if (empty($ids)) {
120 return 0;
121 }
122 $placeholders = implode(',', array_fill(0, count($ids), '%d'));
123 $query = "DELETE FROM $this->table WHERE id IN ($placeholders)";
124 $result = $this->wpdb->query($this->wpdb->prepare($query, ...$ids));
125
126 if ($result === false) {
127 throw new QueryExecutionException(
128 sprintf(
129 BackendStrings::getExceptionStrings()['failed_delete_by_ids'],
130 implode(', ', $ids),
131 $this->table
132 )
133 );
134 }
135
136 return $result;
137 }
138
139 /**
140 * Delete multiple entities by foreign key values (e.g., form IDs).
141 *
142 * @param array<int> $foreignIds Array of foreign key values (e.g., form IDs)
143 * @param string $foreignColumn Foreign key column name (default: 'formId')
144 * @return int Number of deleted entities
145 * @throws InvalidArgumentException If no foreign IDs are provided
146 * @throws QueryExecutionException
147 */
148 public function deleteManyByForeignKeyValues(array $foreignIds, string $foreignColumn = 'formId'): int
149 {
150 if (empty($foreignIds)) {
151 throw new InvalidArgumentException(
152 BackendStrings::getExceptionStrings()['no_form_ids_provided']
153 );
154 }
155
156 // Validate foreign column to prevent SQL injection
157 if (!in_array($foreignColumn, $this->getAllowedForeignColumns(), true)) {
158 throw new InvalidArgumentException(
159 BackendStrings::getExceptionStrings()['invalid_foreign_column']
160 );
161 }
162
163 $foreignIds = array_map('intval', $foreignIds);
164 $placeholders = implode(',', array_fill(0, count($foreignIds), '%d'));
165 $query = "DELETE FROM {$this->table} WHERE {$foreignColumn} IN ($placeholders)";
166 $result = $this->wpdb->query($this->wpdb->prepare($query, ...$foreignIds));
167
168 if ($result === false) {
169 throw new QueryExecutionException(
170 sprintf(
171 BackendStrings::getExceptionStrings()['failed_delete_by_foreign_keys'],
172 implode(', ', $foreignIds),
173 $this->table
174 )
175 );
176 }
177
178 return $result;
179 }
180
181 /**
182 * Delete entities by a single foreign key value (e.g., form ID).
183 *
184 * @param int $foreignId Foreign key value (e.g., form ID)
185 * @param string $foreignColumn Foreign key column name (default: 'formId')
186 * @return int Number of deleted entities
187 * @throws InvalidArgumentException If invalid foreign column is provided
188 * @throws QueryExecutionException
189 */
190 public function deleteOneByForeignKeyValue(int $foreignId, string $foreignColumn = 'formId'): int
191 {
192 // Validate foreign column to prevent SQL injection
193 if (!in_array($foreignColumn, $this->getAllowedForeignColumns(), true)) {
194 throw new InvalidArgumentException(
195 BackendStrings::getExceptionStrings()['invalid_foreign_column']
196 );
197 }
198
199 $query = "DELETE FROM {$this->table} WHERE {$foreignColumn} = %d";
200 $result = $this->wpdb->query($this->wpdb->prepare($query, $foreignId));
201
202 if ($result === false) {
203 throw new QueryExecutionException(
204 sprintf(BackendStrings::getExceptionStrings()['failed_delete_by_foreign_key'], $foreignId, $this->table)
205 );
206 }
207
208 return $result;
209 }
210
211 /**
212 * @param array<string, mixed>|null $params
213 * @return array<string, mixed>
214 */
215 public function search(?array $params): array
216 {
217 $queryParams = [];
218 $whereClauses = $this->buildWhereClauses($params, $queryParams);
219
220 $query = $this->buildSearchQuery($whereClauses, $params, $queryParams);
221 $results = $this->wpdb->get_results($this->wpdb->prepare($query, $queryParams));
222
223 $total = $this->getTotalCount($whereClauses, $queryParams);
224
225 return [
226 'data' => $results,
227 'meta' => [
228 'page' => $params['page'],
229 'perPage' => $params['perPage'],
230 'total' => $total,
231 ],
232 ];
233 }
234
235 /**
236 * @param array<string, mixed>|null $params
237 * @param array<int|string, mixed> $queryParams
238 * @return string
239 */
240 protected function buildWhereClauses(?array $params, array &$queryParams): string
241 {
242 $whereClauses = ['1=1'];
243
244 $this->addSearchClauses($params, $whereClauses, $queryParams);
245 $this->addFilterClauses($params, $whereClauses, $queryParams);
246 $this->addDateRangeClauses($params['dateRange'] ?? [], $whereClauses, $queryParams);
247
248 return implode(' AND ', $whereClauses);
249 }
250
251 /**
252 * @param array<string, mixed>|null $params
253 * @param array<int, string> $whereClauses
254 * @param array<int|string, mixed> $queryParams
255 */
256 protected function addSearchClauses(?array $params, array &$whereClauses, array &$queryParams): void
257 {
258 if (!empty($params['search'])) {
259 $searchEscaped = '%' . $this->wpdb->esc_like($params['search']) . '%';
260 $searchableColumns = $this->getSearchableColumns();
261 $searchConditions = array_map(fn($col) => "$col LIKE %s", $searchableColumns);
262 $whereClauses[] = '(' . implode(' OR ', $searchConditions) . ')';
263 foreach ($searchableColumns as $col) {
264 $queryParams[] = $searchEscaped;
265 }
266 }
267 }
268
269 /**
270 * @param array<string, mixed>|null $params
271 * @param array<int, string> $whereClauses
272 * @param array<int|string, mixed> $queryParams
273 */
274 protected function addFilterClauses(?array $params, array &$whereClauses, array &$queryParams): void
275 {
276 if (!empty($params['filters'])) {
277 $filterableColumns = $this->getFilterableColumns();
278 foreach ($params['filters'] as $key => $value) {
279 if (in_array($key, $filterableColumns, true) && $value !== null && $value !== '') {
280 $whereClauses[] = "{$key} = %s";
281 $queryParams[] = sanitize_text_field($value);
282 }
283 }
284 }
285 }
286
287 /**
288 * @param array<int, string> $dateRange
289 * @param array<int, string> $whereClauses
290 * @param array<int|string, mixed> $queryParams
291 */
292 protected function addDateRangeClauses(array $dateRange, array &$whereClauses, array &$queryParams): void
293 {
294
295 if (empty($dateRange) || empty($dateRange[0])) {
296 return;
297 }
298
299 $dateColumn = $this->getDateColumn();
300
301 if (empty($dateRange[1])) {
302 $dateRange[1] = $dateRange[0];
303 }
304
305 $normalizeStartOfDay = function ($isoDate) {
306 $day = substr($isoDate, 0, 10);
307 return $day . ' 00:00:00';
308 };
309 $normalizeEndOfDay = function ($isoDate) {
310 $day = substr($isoDate, 0, 10);
311 return $day . ' 23:59:59';
312 };
313
314 $start = $normalizeStartOfDay($dateRange[0]);
315 $end = $normalizeEndOfDay($dateRange[1]);
316
317 $whereClauses[] = "$dateColumn >= %s";
318 $queryParams[] = $start;
319
320 $whereClauses[] = "$dateColumn <= %s";
321 $queryParams[] = $end;
322 }
323
324 /**
325 * @param string $whereClauses
326 * @param array<string, mixed>|null $params
327 * @param array<int|string, mixed> $queryParams
328 * @return string
329 */
330 protected function buildSearchQuery(string $whereClauses, ?array $params, array &$queryParams): string
331 {
332 $sortableColumns = $this->getSortableColumns();
333 $sortBy = in_array($params['orderBy'] ?? 'id', $sortableColumns, true) ? $params['orderBy'] : 'id';
334 $order = strtolower($params['order'] ?? 'asc') === 'asc' ? 'ASC' : 'DESC';
335 $page = max(($params['page'] ?? 1), 1);
336 $perPage = max(($params['perPage'] ?? 10), 1);
337 $offset = ($page - 1) * $perPage;
338
339 $queryParams[] = $perPage;
340 $queryParams[] = $offset;
341
342 return $this->selectQuery() . " WHERE {$whereClauses} ORDER BY {$sortBy} {$order} LIMIT %d OFFSET %d";
343 }
344
345 /**
346 * @param string $whereClauses
347 * @param array<int|string, mixed> $queryParams
348 * @return int
349 */
350 protected function getTotalCount(string $whereClauses, array $queryParams): int
351 {
352 // Exclude only LIMIT and OFFSET, keeping filter parameters intact
353 $countParams = $queryParams;
354 if (isset($queryParams[count($queryParams) - 2]) && isset($queryParams[count($queryParams) - 1])) {
355 $countParams = array_slice($queryParams, 0, -2);
356 }
357
358 $countQuery = "SELECT COUNT(*) FROM $this->table WHERE {$whereClauses}";
359 return (int)$this->wpdb->get_var($this->wpdb->prepare($countQuery, $countParams));
360 }
361
362 public function selectQuery(): string
363 {
364 return "SELECT * FROM " . $this->table;
365 }
366
367 /**
368 * Start a database transaction.
369 *
370 * @return bool
371 */
372 public function beginTransaction(): bool
373 {
374 return (bool) $this->wpdb->query('START TRANSACTION');
375 }
376
377 /**
378 * Commit a database transaction.
379 *
380 * @return bool
381 */
382 public function commit(): bool
383 {
384 return (bool) $this->wpdb->query('COMMIT');
385 }
386
387 /**
388 * Rollback a database transaction.
389 *
390 * @return bool
391 */
392 public function rollback(): bool
393 {
394 return (bool) $this->wpdb->query('ROLLBACK');
395 }
396 }
397