Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
354 / 354 |
|
100.00% |
63 / 63 |
CRAP | |
100.00% |
1 / 1 |
Model | |
100.00% |
354 / 354 |
|
100.00% |
63 / 63 |
141 | |
100.00% |
1 / 1 |
__call | |
100.00% |
26 / 26 |
|
100.00% |
1 / 1 |
8 | |||
convertCase | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
5 | |||
getConnectionRead | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getConnectionWrite | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTable | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
makeTableName | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
getPrimaryKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isProtectPrimaryKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getReturnType | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getAllowedFields | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
isAutoTimestamps | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFieldCreated | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFieldUpdated | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTimestampFormat | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLanguageInstance | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLanguage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
checkPrimaryKey | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
filterAllowedFields | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
getDatabaseToRead | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDatabaseToWrite | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
count | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
2 | |||
makePageLimitAndOffset | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
5 | |||
sanitizePageNumber | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
paginate | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
7 | |||
setPager | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
5 | |||
getPagerUrl | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPagerView | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPagerQuery | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPagerAllowedQueries | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPager | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
readBy | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
findBy | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
read | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
find | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
readRow | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
findRow | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
findWithCache | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
readWithCache | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
5 | |||
findAll | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
list | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
makeEntity | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
makeArray | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getTimestamp | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
timezone | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
create | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
7 | |||
createBy | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
6 | |||
checkMysqliException | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
setDuplicateEntryError | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
updateCachedRow | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
save | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
update | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
updateBy | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
5 | |||
replace | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
replaceBy | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
4 | |||
delete | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
deleteBy | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
getValidation | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
3 | |||
getValidationLabels | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getValidationMessages | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getValidationRules | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getValidationValidators | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getErrors | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isCacheActive | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCacheInstance | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCacheTtl | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCacheDataNotFound | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCache | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCacheKey | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 |
1 | <?php declare(strict_types=1); |
2 | /* |
3 | * This file is part of Aplus Framework MVC Library. |
4 | * |
5 | * (c) Natan Felles <natanfelles@gmail.com> |
6 | * |
7 | * For the full copyright and license information, please view the LICENSE |
8 | * file that was distributed with this source code. |
9 | */ |
10 | namespace Framework\MVC; |
11 | |
12 | use BadMethodCallException; |
13 | use DateTime; |
14 | use DateTimeZone; |
15 | use Exception; |
16 | use Framework\Cache\Cache; |
17 | use Framework\Database\Database; |
18 | use Framework\Database\Manipulation\Traits\Where; |
19 | use Framework\Language\Language; |
20 | use Framework\Pagination\Pager; |
21 | use Framework\Validation\Debug\ValidationCollector; |
22 | use Framework\Validation\FilesValidator; |
23 | use Framework\Validation\Validation; |
24 | use InvalidArgumentException; |
25 | use JetBrains\PhpStorm\ArrayShape; |
26 | use JetBrains\PhpStorm\Deprecated; |
27 | use JetBrains\PhpStorm\Pure; |
28 | use LogicException; |
29 | use mysqli_sql_exception; |
30 | use RuntimeException; |
31 | use stdClass; |
32 | |
33 | /** |
34 | * Class Model. |
35 | * |
36 | * @package mvc |
37 | * |
38 | * @method false|int|string createById(array|Entity|stdClass $data) Create a new row and return the id. |
39 | * @method array|Entity|stdClass|null readById(int|string $id) Read a row by id. |
40 | * @method false|int|string updateById(int|string $id, array|Entity|stdClass $data) Update rows by id. |
41 | * @method false|int|string deleteById(int|string $id) Delete rows by id. |
42 | * @method false|int|string replaceById(int|string $id, array|Entity|stdClass $data) Replace rows by id. |
43 | */ |
44 | abstract class Model implements ModelInterface |
45 | { |
46 | /** |
47 | * Database connection instance name for read operations. |
48 | * |
49 | * @var string |
50 | */ |
51 | protected string $connectionRead = 'default'; |
52 | /** |
53 | * Database connection instance name for write operations. |
54 | * |
55 | * @var string |
56 | */ |
57 | protected string $connectionWrite = 'default'; |
58 | /** |
59 | * Table name. |
60 | * |
61 | * @var string |
62 | */ |
63 | protected string $table; |
64 | /** |
65 | * Table Primary Key. |
66 | * |
67 | * @var string |
68 | */ |
69 | protected string $primaryKey = 'id'; |
70 | /** |
71 | * Prevents Primary Key changes on INSERT and UPDATE. |
72 | * |
73 | * @var bool |
74 | */ |
75 | protected bool $protectPrimaryKey = true; |
76 | /** |
77 | * Fetched item return type. |
78 | * |
79 | * Array, object or the classname of an Entity instance. |
80 | * |
81 | * @see Entity |
82 | * |
83 | * @var string |
84 | */ |
85 | protected string $returnType = stdClass::class; |
86 | /** |
87 | * Allowed columns for INSERT and UPDATE. |
88 | * |
89 | * @var array<int,string> |
90 | */ |
91 | protected array $allowedFields; |
92 | /** |
93 | * Auto set timestamp fields. |
94 | * |
95 | * @var bool |
96 | */ |
97 | protected bool $autoTimestamps = false; |
98 | /** |
99 | * The timestamp field for 'created at' time when $autoTimestamps is true. |
100 | * |
101 | * @var string |
102 | */ |
103 | protected string $fieldCreated = 'createdAt'; |
104 | /** |
105 | * The timestamp field for 'updated at' time when $autoTimestamps is true. |
106 | * |
107 | * @var string |
108 | */ |
109 | protected string $fieldUpdated = 'updatedAt'; |
110 | /** |
111 | * The timestamp format used on database write operations. |
112 | * |
113 | * @var string |
114 | */ |
115 | protected string $timestampFormat = 'Y-m-d H:i:s'; |
116 | /** |
117 | * The Model Validation instance. |
118 | */ |
119 | protected Validation $validation; |
120 | /** |
121 | * Validation field labels. |
122 | * |
123 | * @var array<string,string> |
124 | */ |
125 | protected array $validationLabels; |
126 | /** |
127 | * Validation error messages. |
128 | * |
129 | * @var array<string,array<string,string>> |
130 | */ |
131 | protected array $validationMessages; |
132 | /** |
133 | * Validation rules. |
134 | * |
135 | * @see Validation::setRules |
136 | * |
137 | * @var array<string,array<string>|string> |
138 | */ |
139 | protected array $validationRules; |
140 | /** |
141 | * Validation Validators. |
142 | * |
143 | * @var array<int,string> |
144 | */ |
145 | protected array $validationValidators = [ |
146 | Validator::class, |
147 | FilesValidator::class, |
148 | ]; |
149 | /** |
150 | * The Pager instance. |
151 | * |
152 | * Instantiated when calling the paginate method. |
153 | * |
154 | * @see Model::paginate |
155 | * |
156 | * @var Pager |
157 | */ |
158 | protected Pager $pager; |
159 | /** |
160 | * Default pager view. |
161 | * |
162 | * @var string |
163 | */ |
164 | protected string $pagerView; |
165 | /** |
166 | * @var string |
167 | */ |
168 | protected string $pagerQuery; |
169 | /** |
170 | * @var array<string> |
171 | */ |
172 | protected array $pagerAllowedQueries; |
173 | /** |
174 | * Pager URL. |
175 | * |
176 | * @var string |
177 | */ |
178 | protected string $pagerUrl; |
179 | protected bool $cacheActive = false; |
180 | protected string $cacheInstance = 'default'; |
181 | protected int $cacheTtl = 60; |
182 | protected int | string $cacheDataNotFound = 0; |
183 | protected string $languageInstance = 'default'; |
184 | protected string $columnCase = 'camel'; |
185 | |
186 | /** |
187 | * @param string $method |
188 | * @param array<string,mixed> $arguments |
189 | * |
190 | * @return mixed |
191 | */ |
192 | public function __call(string $method, array $arguments) : mixed |
193 | { |
194 | if (\str_starts_with($method, 'createBy')) { |
195 | $method = \substr($method, 8); |
196 | $method = $this->convertCase($method, $this->columnCase); |
197 | return $this->createBy($method, $arguments[0]); // @phpstan-ignore-line |
198 | } |
199 | if (\str_starts_with($method, 'readBy')) { |
200 | $method = \substr($method, 6); |
201 | $method = $this->convertCase($method, $this->columnCase); |
202 | return $this->readBy($method, $arguments[0]); // @phpstan-ignore-line |
203 | } |
204 | if (\str_starts_with($method, 'updateBy')) { |
205 | $method = \substr($method, 8); |
206 | $method = $this->convertCase($method, $this->columnCase); |
207 | return $this->updateBy($method, $arguments[0], $arguments[1]); // @phpstan-ignore-line |
208 | } |
209 | if (\str_starts_with($method, 'deleteBy')) { |
210 | $method = \substr($method, 8); |
211 | $method = $this->convertCase($method, $this->columnCase); |
212 | return $this->deleteBy($method, $arguments[0]); // @phpstan-ignore-line |
213 | } |
214 | if (\str_starts_with($method, 'replaceBy')) { |
215 | $method = \substr($method, 9); |
216 | $method = $this->convertCase($method, $this->columnCase); |
217 | return $this->replaceBy($method, $arguments[0], $arguments[1]); // @phpstan-ignore-line |
218 | } |
219 | // @codeCoverageIgnoreStart |
220 | if (\str_starts_with($method, 'findBy')) { |
221 | $method = \substr($method, 6); |
222 | $method = $this->convertCase($method, $this->columnCase); |
223 | return $this->findBy($method, $arguments[0]); // @phpstan-ignore-line |
224 | } |
225 | // @codeCoverageIgnoreEnd |
226 | $class = static::class; |
227 | if (\method_exists($this, $method)) { |
228 | throw new BadMethodCallException( |
229 | "Method not allowed: {$class}::{$method}" |
230 | ); |
231 | } |
232 | throw new BadMethodCallException("Method not found: {$class}::{$method}"); |
233 | } |
234 | |
235 | /** |
236 | * Convert a value to specific case. |
237 | * |
238 | * @param string $value |
239 | * @param string $case camel, pascal or snake |
240 | * |
241 | * @return string The converted value |
242 | */ |
243 | protected function convertCase(string $value, string $case) : string |
244 | { |
245 | if ($case === 'camel' || $case === 'pascal') { |
246 | $value = \preg_replace('/([a-z])([A-Z])/', '\\1 \\2', $value); |
247 | $value = \preg_replace('@[^a-zA-Z0-9\-_ ]+@', '', $value); |
248 | $value = \str_replace(['-', '_'], ' ', $value); |
249 | $value = \str_replace(' ', '', \ucwords(\strtolower($value))); |
250 | $value = \strtolower($value[0]) . \substr($value, 1); |
251 | return $case === 'camel' ? \lcfirst($value) : \ucfirst($value); |
252 | } |
253 | if ($case === 'snake') { |
254 | $value = \preg_replace('/([a-z])([A-Z])/', '\\1_\\2', $value); |
255 | return \strtolower($value); |
256 | } |
257 | throw new InvalidArgumentException('Invalid case: ' . $case); |
258 | } |
259 | |
260 | #[Pure] |
261 | protected function getConnectionRead() : string |
262 | { |
263 | return $this->connectionRead; |
264 | } |
265 | |
266 | #[Pure] |
267 | protected function getConnectionWrite() : string |
268 | { |
269 | return $this->connectionWrite; |
270 | } |
271 | |
272 | protected function getTable() : string |
273 | { |
274 | return $this->table ??= $this->makeTableName(); |
275 | } |
276 | |
277 | protected function makeTableName() : string |
278 | { |
279 | $name = static::class; |
280 | $pos = \strrpos($name, '\\'); |
281 | if ($pos) { |
282 | $name = \substr($name, $pos + 1); |
283 | } |
284 | if (\str_ends_with($name, 'Model')) { |
285 | $name = \substr($name, 0, -5); |
286 | } |
287 | return $name; |
288 | } |
289 | |
290 | #[Pure] |
291 | protected function getPrimaryKey() : string |
292 | { |
293 | return $this->primaryKey; |
294 | } |
295 | |
296 | #[Pure] |
297 | protected function isProtectPrimaryKey() : bool |
298 | { |
299 | return $this->protectPrimaryKey; |
300 | } |
301 | |
302 | #[Pure] |
303 | protected function getReturnType() : string |
304 | { |
305 | return $this->returnType; |
306 | } |
307 | |
308 | /** |
309 | * @return array<int,string> |
310 | */ |
311 | protected function getAllowedFields() : array |
312 | { |
313 | if (empty($this->allowedFields)) { |
314 | throw new LogicException( |
315 | 'Allowed fields not defined for database writes' |
316 | ); |
317 | } |
318 | return $this->allowedFields; |
319 | } |
320 | |
321 | #[Pure] |
322 | protected function isAutoTimestamps() : bool |
323 | { |
324 | return $this->autoTimestamps; |
325 | } |
326 | |
327 | #[Pure] |
328 | protected function getFieldCreated() : string |
329 | { |
330 | return $this->fieldCreated; |
331 | } |
332 | |
333 | #[Pure] |
334 | protected function getFieldUpdated() : string |
335 | { |
336 | return $this->fieldUpdated; |
337 | } |
338 | |
339 | #[Pure] |
340 | protected function getTimestampFormat() : string |
341 | { |
342 | return $this->timestampFormat; |
343 | } |
344 | |
345 | protected function getLanguageInstance() : string |
346 | { |
347 | return $this->languageInstance; |
348 | } |
349 | |
350 | protected function getLanguage() : Language |
351 | { |
352 | return App::language($this->getLanguageInstance()); |
353 | } |
354 | |
355 | protected function checkPrimaryKey(int | string $id) : void |
356 | { |
357 | if (empty($id)) { |
358 | throw new InvalidArgumentException( |
359 | 'Primary Key can not be empty' |
360 | ); |
361 | } |
362 | } |
363 | |
364 | /** |
365 | * @template T |
366 | * |
367 | * @param array<string,T> $data |
368 | * |
369 | * @return array<string,T> |
370 | */ |
371 | protected function filterAllowedFields(array $data) : array |
372 | { |
373 | $fields = \array_intersect_key($data, \array_flip($this->getAllowedFields())); |
374 | if ($this->isProtectPrimaryKey() !== false |
375 | && \array_key_exists($this->getPrimaryKey(), $fields) |
376 | ) { |
377 | throw new LogicException( |
378 | 'Protected Primary Key field can not be SET' |
379 | ); |
380 | } |
381 | return $fields; |
382 | } |
383 | |
384 | /** |
385 | * @see Model::$connectionRead |
386 | * |
387 | * @return Database |
388 | */ |
389 | protected function getDatabaseToRead() : Database |
390 | { |
391 | return App::database($this->getConnectionRead()); |
392 | } |
393 | |
394 | /** |
395 | * @see Model::$connectionWrite |
396 | * |
397 | * @return Database |
398 | */ |
399 | protected function getDatabaseToWrite() : Database |
400 | { |
401 | return App::database($this->getConnectionWrite()); |
402 | } |
403 | |
404 | /** |
405 | * A basic function to count rows in the table. |
406 | * |
407 | * @param array<array<mixed>> $where Array in this format: `[['id', '=', 25]]` |
408 | * |
409 | * @see Where |
410 | * |
411 | * @return int |
412 | */ |
413 | public function count(array $where = []) : int |
414 | { |
415 | $select = $this->getDatabaseToRead() |
416 | ->select() |
417 | ->expressions([ |
418 | 'count' => static function () : string { |
419 | return 'COUNT(*)'; |
420 | }, |
421 | ]) |
422 | ->from($this->getTable()); |
423 | foreach ($where as $args) { |
424 | $select->where(...$args); |
425 | } |
426 | return $select->run()->fetch()->count; // @phpstan-ignore-line |
427 | } |
428 | |
429 | /** |
430 | * @param int $page |
431 | * @param int $perPage |
432 | * |
433 | * @see Model::paginate |
434 | * |
435 | * @return array<int,int|null> |
436 | */ |
437 | #[ArrayShape([0 => 'int', 1 => 'int|null'])] |
438 | #[Pure] |
439 | protected function makePageLimitAndOffset(int $page, int $perPage = 10) : array |
440 | { |
441 | $page = $this->sanitizePageNumber($page); |
442 | $perPage = $this->sanitizePageNumber($perPage); |
443 | $page = $page <= 1 ? null : $page * $perPage - $perPage; |
444 | if ($page > \PHP_INT_MAX) { |
445 | $page = \PHP_INT_MAX; |
446 | } |
447 | if ($perPage === \PHP_INT_MAX && $page !== null) { |
448 | $page = \PHP_INT_MAX; |
449 | } |
450 | return [ |
451 | $perPage, |
452 | $page, |
453 | ]; |
454 | } |
455 | |
456 | protected function sanitizePageNumber(int $number) : int |
457 | { |
458 | if ($number < 0) { |
459 | if ($number === \PHP_INT_MIN) { |
460 | $number++; |
461 | } |
462 | $number *= -1; |
463 | } |
464 | return $number; |
465 | } |
466 | |
467 | /** |
468 | * A basic function to paginate all rows of the table. |
469 | * |
470 | * @param mixed $page The current page |
471 | * @param mixed $perPage Items per page |
472 | * @param array<string>|string $orderBy Order by columns |
473 | * @param string $orderByDirection asc or desc |
474 | * @param array<array<mixed>> $where Array in this format: `[['id', '=', 25]]` |
475 | * |
476 | * @see Where |
477 | * |
478 | * @return array<int,array<mixed>|Entity|stdClass> |
479 | */ |
480 | public function paginate( |
481 | mixed $page, |
482 | mixed $perPage = 10, |
483 | array $where = [], |
484 | array | string $orderBy = null, |
485 | string $orderByDirection = 'asc', |
486 | ) : array { |
487 | $page = Pager::sanitize($page); |
488 | $perPage = Pager::sanitize($perPage); |
489 | $select = $this->getDatabaseToRead() |
490 | ->select() |
491 | ->from($this->getTable()) |
492 | ->limit(...$this->makePageLimitAndOffset($page, $perPage)); |
493 | if ($where) { |
494 | foreach ($where as $args) { |
495 | $select->where(...$args); |
496 | } |
497 | } |
498 | if ($orderBy !== null) { |
499 | $orderBy = (array) $orderBy; |
500 | $orderByDir = \strtolower($orderByDirection); |
501 | if ( ! \in_array($orderByDir, [ |
502 | 'asc', |
503 | 'desc', |
504 | ])) { |
505 | throw new InvalidArgumentException( |
506 | 'Invalid ORDER BY direction: ' . $orderByDirection |
507 | ); |
508 | } |
509 | $orderByDir === 'asc' |
510 | ? $select->orderByAsc(...$orderBy) |
511 | : $select->orderByDesc(...$orderBy); |
512 | } |
513 | $data = $select->run()->fetchArrayAll(); |
514 | foreach ($data as &$row) { |
515 | $row = $this->makeEntity($row); |
516 | } |
517 | unset($row); |
518 | $this->setPager(new Pager($page, $perPage, $this->count($where))); |
519 | return $data; |
520 | } |
521 | |
522 | /** |
523 | * Set the Pager. |
524 | * |
525 | * @param Pager $pager |
526 | * |
527 | * @return static |
528 | */ |
529 | protected function setPager(Pager $pager) : static |
530 | { |
531 | $pager->setLanguage($this->getLanguage()); |
532 | $temp = $this->getPagerQuery(); |
533 | if (isset($temp)) { |
534 | $pager->setQuery($temp); |
535 | } |
536 | $temp = $this->getPagerUrl(); |
537 | if (isset($temp)) { |
538 | $pager->setUrl($temp); |
539 | } |
540 | $temp = $this->getPagerAllowedQueries(); |
541 | if (isset($temp)) { |
542 | $pager->setAllowedQueries($temp); |
543 | } |
544 | $temp = $this->getPagerView(); |
545 | if (isset($temp)) { |
546 | $pager->setDefaultView($temp); |
547 | } |
548 | $this->pager = $pager; |
549 | return $this; |
550 | } |
551 | |
552 | /** |
553 | * Get the custom URL to be used in the Pager. |
554 | * |
555 | * @return string|null |
556 | */ |
557 | protected function getPagerUrl() : ?string |
558 | { |
559 | return $this->pagerUrl ?? null; |
560 | } |
561 | |
562 | /** |
563 | * Get the custom view to be used in the Pager. |
564 | * |
565 | * @return string|null |
566 | */ |
567 | protected function getPagerView() : ?string |
568 | { |
569 | return $this->pagerView ?? null; |
570 | } |
571 | |
572 | /** |
573 | * Get the custom query to be used in the Pager. |
574 | * |
575 | * @return string|null |
576 | */ |
577 | protected function getPagerQuery() : ?string |
578 | { |
579 | return $this->pagerQuery ?? null; |
580 | } |
581 | |
582 | /** |
583 | * Get allowed queries to be used in the Pager. |
584 | * |
585 | * @return array<string>|null |
586 | */ |
587 | protected function getPagerAllowedQueries() : ?array |
588 | { |
589 | return $this->pagerAllowedQueries ?? null; |
590 | } |
591 | |
592 | /** |
593 | * Get the Pager. |
594 | * |
595 | * Allowed only after calling a method that sets the Pager. |
596 | * |
597 | * @see Model::paginate() |
598 | * |
599 | * @return Pager |
600 | */ |
601 | public function getPager() : Pager |
602 | { |
603 | return $this->pager; |
604 | } |
605 | |
606 | /** |
607 | * Read a row by column name and value. |
608 | * |
609 | * @param string $column |
610 | * @param int|string $value |
611 | * |
612 | * @since 3.6 |
613 | * |
614 | * @return array<string,float|int|string|null>|Entity|stdClass|null |
615 | */ |
616 | public function readBy( |
617 | string $column, |
618 | int | string $value |
619 | ) : array | Entity | stdClass | null { |
620 | if ($this->isCacheActive()) { |
621 | return $this->readWithCache($column, $value); |
622 | } |
623 | $data = $this->readRow($column, $value); |
624 | return $data ? $this->makeEntity($data) : null; |
625 | } |
626 | |
627 | /** |
628 | * Find a row by column name and value. |
629 | * |
630 | * @param string $column |
631 | * @param int|string $value |
632 | * |
633 | * @deprecated |
634 | * |
635 | * @codeCoverageIgnore |
636 | * |
637 | * @return array<string,float|int|string|null>|Entity|stdClass|null |
638 | */ |
639 | #[Deprecated( |
640 | reason: 'since MVC Library version 3.6, use readBy() instead', |
641 | replacement: '%class%->readBy(%parameter0%, %parameter1%)' |
642 | )] |
643 | public function findBy( |
644 | string $column, |
645 | int | string $value |
646 | ) : array | Entity | stdClass | null { |
647 | \trigger_error( |
648 | 'Method ' . __METHOD__ . ' is deprecated', |
649 | \E_USER_DEPRECATED |
650 | ); |
651 | return $this->readBy($column, $value); |
652 | } |
653 | |
654 | /** |
655 | * Read a row based on Primary Key. |
656 | * |
657 | * @param int|string $id |
658 | * |
659 | * @since 3.6 |
660 | * |
661 | * @return array<string,float|int|string|null>|Entity|stdClass|null The |
662 | * selected row as configured on $returnType property or null if row was |
663 | * not found |
664 | */ |
665 | public function read(int | string $id) : array | Entity | stdClass | null |
666 | { |
667 | $this->checkPrimaryKey($id); |
668 | return $this->readBy($this->getPrimaryKey(), $id); |
669 | } |
670 | |
671 | /** |
672 | * @param int|string $id |
673 | * |
674 | * @return array|Entity|float[]|int[]|null[]|stdClass|string[]|null |
675 | * |
676 | * @deprecated |
677 | * |
678 | * @codeCoverageIgnore |
679 | */ |
680 | #[Deprecated( |
681 | reason: 'since MVC Library version 3.6, use read() instead', |
682 | replacement: '%class%->read(%parameter0%)' |
683 | )] |
684 | public function find(int | string $id) : array | Entity | stdClass | null |
685 | { |
686 | \trigger_error( |
687 | 'Method ' . __METHOD__ . ' is deprecated', |
688 | \E_USER_DEPRECATED |
689 | ); |
690 | return $this->read($id); |
691 | } |
692 | |
693 | /** |
694 | * @param string $column |
695 | * @param int|string $value |
696 | * |
697 | * @since 3.6 |
698 | * |
699 | * @return array<string,float|int|string|null>|null |
700 | */ |
701 | protected function readRow(string $column, int | string $value) : array | null |
702 | { |
703 | return $this->getDatabaseToRead() |
704 | ->select() |
705 | ->from($this->getTable()) |
706 | ->whereEqual($column, $value) |
707 | ->limit(1) |
708 | ->run() |
709 | ->fetchArray(); |
710 | } |
711 | |
712 | /** |
713 | * @param string $column |
714 | * @param int|string $value |
715 | * |
716 | * @return array<string,float|int|string|null>|null |
717 | * |
718 | * @deprecated |
719 | * |
720 | * @codeCoverageIgnore |
721 | */ |
722 | #[Deprecated( |
723 | reason: 'since MVC Library version 3.6, use readRow() instead', |
724 | replacement: '%class%->readRow(%parameter0%, %parameter1%)' |
725 | )] |
726 | protected function findRow(string $column, int | string $value) : array | null |
727 | { |
728 | \trigger_error( |
729 | 'Method ' . __METHOD__ . ' is deprecated', |
730 | \E_USER_DEPRECATED |
731 | ); |
732 | return $this->readRow($column, $value); |
733 | } |
734 | |
735 | /** |
736 | * @param string $column |
737 | * @param int|string $value |
738 | * |
739 | * @return array<string,float|int|string|null>|Entity|stdClass|null |
740 | * |
741 | * @deprecated |
742 | * |
743 | * @codeCoverageIgnore |
744 | */ |
745 | #[Deprecated( |
746 | reason: 'since MVC Library version 3.6, use readWithCache() instead', |
747 | replacement: '%class%->readWithCache(%parameter0%, %parameter1%)' |
748 | )] |
749 | protected function findWithCache(string $column, int | string $value) : array | Entity | stdClass | null |
750 | { |
751 | \trigger_error( |
752 | 'Method ' . __METHOD__ . ' is deprecated', |
753 | \E_USER_DEPRECATED |
754 | ); |
755 | return $this->readWithCache($column, $value); |
756 | } |
757 | |
758 | /** |
759 | * @param string $column |
760 | * @param int|string $value |
761 | * |
762 | * @since 3.6 |
763 | * |
764 | * @return array<string,float|int|string|null>|Entity|stdClass|null |
765 | */ |
766 | protected function readWithCache(string $column, int | string $value) : array | Entity | stdClass | null |
767 | { |
768 | $cacheKey = $this->getCacheKey([ |
769 | $column => $value, |
770 | ]); |
771 | $data = $this->getCache()->get($cacheKey); |
772 | if ($data === $this->getCacheDataNotFound()) { |
773 | return null; |
774 | } |
775 | if (\is_array($data)) { |
776 | return $this->makeEntity($data); |
777 | } |
778 | $data = $this->readRow($column, $value); |
779 | if ($data === null) { |
780 | $data = $this->getCacheDataNotFound(); |
781 | } |
782 | $this->getCache()->set($cacheKey, $data, $this->getCacheTtl()); |
783 | return \is_array($data) ? $this->makeEntity($data) : null; |
784 | } |
785 | |
786 | /** |
787 | * Find all rows with limit and offset. |
788 | * |
789 | * @param int|null $limit |
790 | * @param int|null $offset |
791 | * |
792 | * @return array<int,array<mixed>|Entity|stdClass> |
793 | * |
794 | * @deprecated |
795 | * |
796 | * @codeCoverageIgnore |
797 | */ |
798 | #[Deprecated( |
799 | reason: 'since MVC Library version 3.6, use list() instead', |
800 | replacement: '%class%->readAll(%parameter0%, %parameter1%)' |
801 | )] |
802 | public function findAll(int $limit = null, int $offset = null) : array |
803 | { |
804 | \trigger_error( |
805 | 'Method ' . __METHOD__ . ' is deprecated', |
806 | \E_USER_DEPRECATED |
807 | ); |
808 | return $this->list($limit, $offset); |
809 | } |
810 | |
811 | /** |
812 | * List rows, optionally with limit and offset. |
813 | * |
814 | * @param int|null $limit |
815 | * @param int|null $offset |
816 | * |
817 | * @since 3.6 |
818 | * |
819 | * @return array<int,array<mixed>|Entity|stdClass> |
820 | */ |
821 | public function list(int $limit = null, int $offset = null) : array |
822 | { |
823 | $data = $this->getDatabaseToRead() |
824 | ->select() |
825 | ->from($this->getTable()); |
826 | if ($limit !== null) { |
827 | $data->limit($limit, $offset); |
828 | } |
829 | $data = $data->run()->fetchArrayAll(); |
830 | foreach ($data as &$row) { |
831 | $row = $this->makeEntity($row); |
832 | } |
833 | unset($row); |
834 | return $data; |
835 | } |
836 | |
837 | /** |
838 | * @param array<string,float|int|string|null> $data |
839 | * |
840 | * @return array<string,float|int|string|null>|Entity|stdClass |
841 | */ |
842 | protected function makeEntity(array $data) : array | Entity | stdClass |
843 | { |
844 | $returnType = $this->getReturnType(); |
845 | if ($returnType === 'array') { |
846 | return $data; |
847 | } |
848 | if ($returnType === 'object' || $returnType === stdClass::class) { |
849 | return (object) $data; |
850 | } |
851 | return new $returnType($data); // @phpstan-ignore-line |
852 | } |
853 | |
854 | /** |
855 | * @param array<string,mixed>|Entity|stdClass $data |
856 | * |
857 | * @return array<string,mixed> |
858 | */ |
859 | protected function makeArray(array | Entity | stdClass $data) : array |
860 | { |
861 | return $data instanceof Entity |
862 | ? $data->toModel() |
863 | : (array) $data; |
864 | } |
865 | |
866 | /** |
867 | * Used to auto set the timestamp fields. |
868 | * |
869 | * @throws Exception if a DateTime error occur |
870 | * |
871 | * @return string The timestamp in the $timestampFormat property format |
872 | */ |
873 | protected function getTimestamp() : string |
874 | { |
875 | return (new DateTime('now', $this->timezone()))->format( |
876 | $this->getTimestampFormat() |
877 | ); |
878 | } |
879 | |
880 | /** |
881 | * Get the timezone from database write connection config. As fallback, uses |
882 | * the UTC timezone. |
883 | * |
884 | * @throws Exception if database config has a bad timezone |
885 | * |
886 | * @return DateTimeZone |
887 | */ |
888 | protected function timezone() : DateTimeZone |
889 | { |
890 | $timezone = $this->getDatabaseToWrite()->getConfig()['timezone'] ?? '+00:00'; |
891 | return new DateTimeZone($timezone); |
892 | } |
893 | |
894 | /** |
895 | * Insert a new row. |
896 | * |
897 | * @param array<string,float|int|string|null>|Entity|stdClass $data |
898 | * |
899 | * @return false|int|string The LAST_INSERT_ID() on success or false if |
900 | * validation fail |
901 | */ |
902 | public function create(array | Entity | stdClass $data) : false | int | string |
903 | { |
904 | $data = $this->makeArray($data); |
905 | if ($this->getValidation()->validate($data) === false) { |
906 | return false; |
907 | } |
908 | $data = $this->filterAllowedFields($data); |
909 | if ($this->isAutoTimestamps()) { |
910 | $timestamp = $this->getTimestamp(); |
911 | $data[$this->getFieldCreated()] ??= $timestamp; |
912 | $data[$this->getFieldUpdated()] ??= $timestamp; |
913 | } |
914 | $database = $this->getDatabaseToWrite(); |
915 | try { |
916 | $affectedRows = $database->insert() |
917 | ->into($this->getTable()) |
918 | ->set($data) |
919 | ->run(); |
920 | } catch (mysqli_sql_exception $exception) { |
921 | $this->checkMysqliException($exception); |
922 | return false; |
923 | } |
924 | $insertId = $affectedRows > 0 // $affectedRows is -1 if fail with MYSQLI_REPORT_OFF |
925 | ? $database->insertId() |
926 | : false; |
927 | if ($insertId && $this->isCacheActive()) { |
928 | $this->updateCachedRow($this->getPrimaryKey(), $insertId); |
929 | } |
930 | return $insertId; |
931 | } |
932 | |
933 | /** |
934 | * Insert a new row and return the inserted column value. |
935 | * |
936 | * @param string $column Column name |
937 | * @param array<string,float|int|string|null>|Entity|stdClass $data |
938 | * |
939 | * @return false|int|string The value from the column data or false if |
940 | * validation fail |
941 | */ |
942 | public function createBy(string $column, array | Entity | stdClass $data) : false | int | string |
943 | { |
944 | $data = $this->makeArray($data); |
945 | if ($this->getValidation()->validate($data) === false) { |
946 | return false; |
947 | } |
948 | $data = $this->filterAllowedFields($data); |
949 | if ( ! isset($data[$column])) { |
950 | throw new LogicException('Value of column ' . $column . ' is not set'); |
951 | } |
952 | if ($this->isAutoTimestamps()) { |
953 | $timestamp = $this->getTimestamp(); |
954 | $data[$this->getFieldCreated()] ??= $timestamp; |
955 | $data[$this->getFieldUpdated()] ??= $timestamp; |
956 | } |
957 | try { |
958 | $this->getDatabaseToWrite()->insert() |
959 | ->into($this->getTable()) |
960 | ->set($data) |
961 | ->run(); |
962 | } catch (mysqli_sql_exception $exception) { |
963 | $this->checkMysqliException($exception); |
964 | return false; |
965 | } |
966 | if ($this->isCacheActive()) { |
967 | $this->updateCachedRow($column, $data[$column]); |
968 | } |
969 | return $data[$column]; |
970 | } |
971 | |
972 | /** |
973 | * @param mysqli_sql_exception $exception |
974 | * |
975 | * @throws mysqli_sql_exception if message is not for duplicate entry |
976 | */ |
977 | protected function checkMysqliException(mysqli_sql_exception $exception) : void |
978 | { |
979 | $message = $exception->getMessage(); |
980 | if (\str_starts_with($message, 'Duplicate entry')) { |
981 | $this->setDuplicateEntryError($message); |
982 | return; |
983 | } |
984 | throw $exception; |
985 | } |
986 | |
987 | /** |
988 | * Set "Duplicate entry" as 'unique' error in the Validation. |
989 | * |
990 | * NOTE: We will get the index key name and not the column name. Usually the |
991 | * names are the same. If table have different column and index names, |
992 | * override this method and get the column name from the information_schema |
993 | * table. |
994 | * |
995 | * @param string $message The "Duplicate entry" message from the mysqli_sql_exception |
996 | */ |
997 | protected function setDuplicateEntryError(string $message) : void |
998 | { |
999 | $field = \rtrim($message, "'"); |
1000 | $field = \substr($field, \strrpos($field, "'") + 1); |
1001 | if ($field === 'PRIMARY') { |
1002 | $field = $this->getPrimaryKey(); |
1003 | } |
1004 | $validation = $this->getValidation(); |
1005 | $validation->setError($field, 'unique'); |
1006 | $validation->getDebugCollector() |
1007 | ?->setErrorInDebugData($field, $validation->getError($field)); |
1008 | } |
1009 | |
1010 | /** |
1011 | * @param string $column |
1012 | * @param int|string $value |
1013 | */ |
1014 | protected function updateCachedRow(string $column, int | string $value) : void |
1015 | { |
1016 | $data = $this->readRow($column, $value); |
1017 | if ($data === null) { |
1018 | $data = $this->getCacheDataNotFound(); |
1019 | } |
1020 | $this->getCache()->set( |
1021 | $this->getCacheKey([$column => $value]), |
1022 | $data, |
1023 | $this->getCacheTtl() |
1024 | ); |
1025 | } |
1026 | |
1027 | /** |
1028 | * Save a row. Update if the Primary Key is present, otherwise |
1029 | * insert a new row. |
1030 | * |
1031 | * @param array<string,float|int|string|null>|Entity|stdClass $data |
1032 | * |
1033 | * @return false|int|string The number of affected rows on updates as int, the |
1034 | * LAST_INSERT_ID() as int on inserts or false if validation fails |
1035 | */ |
1036 | public function save(array | Entity | stdClass $data) : false | int | string |
1037 | { |
1038 | $data = $this->makeArray($data); |
1039 | $id = $data[$this->getPrimaryKey()] ?? null; |
1040 | $data = $this->filterAllowedFields($data); |
1041 | if ($id !== null) { |
1042 | return $this->update($id, $data); |
1043 | } |
1044 | return $this->create($data); |
1045 | } |
1046 | |
1047 | /** |
1048 | * Update based on Primary Key and return the number of affected rows. |
1049 | * |
1050 | * @param int|string $id |
1051 | * @param array<string,float|int|string|null>|Entity|stdClass $data |
1052 | * |
1053 | * @return false|int|string The number of affected rows or false if |
1054 | * validation fails |
1055 | */ |
1056 | public function update(int | string $id, array | Entity | stdClass $data) : false | int | string |
1057 | { |
1058 | $this->checkPrimaryKey($id); |
1059 | return $this->updateBy($this->getPrimaryKey(), $id, $data); |
1060 | } |
1061 | |
1062 | /** |
1063 | * Update based on column value and return the number of affected rows. |
1064 | * |
1065 | * @param string $column |
1066 | * @param int|string $value |
1067 | * @param array<string,float|int|string|null>|Entity|stdClass $data |
1068 | * |
1069 | * @return false|int|string The number of affected rows or false if |
1070 | * validation fails |
1071 | */ |
1072 | public function updateBy( |
1073 | string $column, |
1074 | int | string $value, |
1075 | array | Entity | stdClass $data |
1076 | ) : false | int | string { |
1077 | $data = $this->makeArray($data); |
1078 | $data[$column] ??= $value; |
1079 | if ($this->getValidation()->validateOnly($data) === false) { |
1080 | return false; |
1081 | } |
1082 | $data = $this->filterAllowedFields($data); |
1083 | if ($this->isAutoTimestamps()) { |
1084 | $data[$this->getFieldUpdated()] ??= $this->getTimestamp(); |
1085 | } |
1086 | try { |
1087 | $affectedRows = $this->getDatabaseToWrite() |
1088 | ->update() |
1089 | ->table($this->getTable()) |
1090 | ->set($data) |
1091 | ->whereEqual($column, $value) |
1092 | ->run(); |
1093 | } catch (mysqli_sql_exception $exception) { |
1094 | $this->checkMysqliException($exception); |
1095 | return false; |
1096 | } |
1097 | if ($this->isCacheActive()) { |
1098 | $this->updateCachedRow($column, $value); |
1099 | } |
1100 | return $affectedRows; |
1101 | } |
1102 | |
1103 | /** |
1104 | * Replace based on Primary Key and return the number of affected rows. |
1105 | * |
1106 | * Most used with HTTP PUT method. |
1107 | * |
1108 | * @param int|string $id |
1109 | * @param array<string,float|int|string|null>|Entity|stdClass $data |
1110 | * |
1111 | * @return false|int|string The number of affected rows or false if |
1112 | * validation fails |
1113 | */ |
1114 | public function replace(int | string $id, array | Entity | stdClass $data) : false | int | string |
1115 | { |
1116 | $this->checkPrimaryKey($id); |
1117 | return $this->replaceBy($this->getPrimaryKey(), $id, $data); |
1118 | } |
1119 | |
1120 | /** |
1121 | * Replace based on column value and return the number of affected rows. |
1122 | * |
1123 | * @param string $column |
1124 | * @param int|string $value |
1125 | * @param array<string,float|int|string|null>|Entity|stdClass $data |
1126 | * |
1127 | * @return false|int|string The number of affected rows or false if |
1128 | * validation fails |
1129 | */ |
1130 | public function replaceBy( |
1131 | string $column, |
1132 | int | string $value, |
1133 | array | Entity | stdClass $data |
1134 | ) : false | int | string { |
1135 | $data = $this->makeArray($data); |
1136 | $data[$column] ??= $value; |
1137 | if ($this->getValidation()->validate($data) === false) { |
1138 | return false; |
1139 | } |
1140 | $data = $this->filterAllowedFields($data); |
1141 | $data[$column] = $value; |
1142 | if ($this->isAutoTimestamps()) { |
1143 | $timestamp = $this->getTimestamp(); |
1144 | $data[$this->getFieldCreated()] ??= $timestamp; |
1145 | $data[$this->getFieldUpdated()] ??= $timestamp; |
1146 | } |
1147 | $affectedRows = $this->getDatabaseToWrite() |
1148 | ->replace() |
1149 | ->into($this->getTable()) |
1150 | ->set($data) |
1151 | ->run(); |
1152 | if ($this->isCacheActive()) { |
1153 | $this->updateCachedRow($column, $value); |
1154 | } |
1155 | return $affectedRows; |
1156 | } |
1157 | |
1158 | /** |
1159 | * Delete based on Primary Key. |
1160 | * |
1161 | * @param int|string $id |
1162 | * |
1163 | * @return false|int|string The number of affected rows |
1164 | */ |
1165 | public function delete(int | string $id) : false | int | string |
1166 | { |
1167 | $this->checkPrimaryKey($id); |
1168 | return $this->deleteBy($this->getPrimaryKey(), $id); |
1169 | } |
1170 | |
1171 | /** |
1172 | * Delete based on column value. |
1173 | * |
1174 | * @param string $column |
1175 | * @param int|string $value |
1176 | * |
1177 | * @return false|int|string The number of affected rows |
1178 | */ |
1179 | public function deleteBy(string $column, int | string $value) : false | int | string |
1180 | { |
1181 | $affectedRows = $this->getDatabaseToWrite() |
1182 | ->delete() |
1183 | ->from($this->getTable()) |
1184 | ->whereEqual($column, $value) |
1185 | ->run(); |
1186 | if ($this->isCacheActive()) { |
1187 | $this->getCache()->delete( |
1188 | $this->getCacheKey([$column => $value]) |
1189 | ); |
1190 | } |
1191 | return $affectedRows; |
1192 | } |
1193 | |
1194 | protected function getValidation() : Validation |
1195 | { |
1196 | if (isset($this->validation)) { |
1197 | return $this->validation; |
1198 | } |
1199 | $this->validation = new Validation( |
1200 | $this->getValidationValidators(), |
1201 | $this->getLanguage() |
1202 | ); |
1203 | $this->validation->setRules($this->getValidationRules()) |
1204 | ->setLabels($this->getValidationLabels()) |
1205 | ->setMessages($this->getValidationMessages()); |
1206 | if (App::isDebugging()) { |
1207 | $name = 'model ' . static::class; |
1208 | $collector = new ValidationCollector($name); |
1209 | App::debugger()->addCollector($collector, 'Validation'); |
1210 | $this->validation->setDebugCollector($collector); |
1211 | } |
1212 | return $this->validation; |
1213 | } |
1214 | |
1215 | /** |
1216 | * @return array<string,string> |
1217 | */ |
1218 | protected function getValidationLabels() : array |
1219 | { |
1220 | return $this->validationLabels ?? []; |
1221 | } |
1222 | |
1223 | /** |
1224 | * @return array<string,array<string,string>> |
1225 | */ |
1226 | public function getValidationMessages() : array |
1227 | { |
1228 | return $this->validationMessages ?? []; |
1229 | } |
1230 | |
1231 | /** |
1232 | * @return array<string,array<string>|string> |
1233 | */ |
1234 | protected function getValidationRules() : array |
1235 | { |
1236 | if ( ! isset($this->validationRules)) { |
1237 | throw new RuntimeException('Validation rules are not set'); |
1238 | } |
1239 | return $this->validationRules; |
1240 | } |
1241 | |
1242 | /** |
1243 | * @return array<int,string> |
1244 | */ |
1245 | public function getValidationValidators() : array |
1246 | { |
1247 | return $this->validationValidators; |
1248 | } |
1249 | |
1250 | /** |
1251 | * Get Validation errors. |
1252 | * |
1253 | * @return array<string,string> |
1254 | */ |
1255 | public function getErrors() : array |
1256 | { |
1257 | return $this->getValidation()->getErrors(); |
1258 | } |
1259 | |
1260 | #[Pure] |
1261 | protected function isCacheActive() : bool |
1262 | { |
1263 | return $this->cacheActive; |
1264 | } |
1265 | |
1266 | #[Pure] |
1267 | protected function getCacheInstance() : string |
1268 | { |
1269 | return $this->cacheInstance; |
1270 | } |
1271 | |
1272 | #[Pure] |
1273 | protected function getCacheTtl() : int |
1274 | { |
1275 | return $this->cacheTtl; |
1276 | } |
1277 | |
1278 | #[Pure] |
1279 | protected function getCacheDataNotFound() : int | string |
1280 | { |
1281 | return $this->cacheDataNotFound; |
1282 | } |
1283 | |
1284 | protected function getCache() : Cache |
1285 | { |
1286 | return App::cache($this->getCacheInstance()); |
1287 | } |
1288 | |
1289 | /** |
1290 | * @param array<string,float|int|string> $fields |
1291 | * |
1292 | * @return string |
1293 | */ |
1294 | protected function getCacheKey(array $fields) : string |
1295 | { |
1296 | \ksort($fields); |
1297 | $suffix = []; |
1298 | foreach ($fields as $field => $value) { |
1299 | $suffix[] = $field . '=' . $value; |
1300 | } |
1301 | $suffix = \implode(';', $suffix); |
1302 | return 'Model:' . static::class . '::' . $suffix; |
1303 | } |
1304 | } |