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
← All changes | backend/src/Repository/AbstractRepository.php +391 -0 0.6.1 → 1.5.1 View file →
@@ -1,0 +1,391 @@
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 +}