PluginProbe
The Innovative Form Builder – IvyForms / 1.5.1
The Innovative Form Builder – IvyForms v1.5.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 1.5.1, at backend/src/Repository/AbstractRepository.php

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