Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
59.53% |
228 / 383 |
|
17.65% |
3 / 17 |
CRAP | |
0.00% |
0 / 1 |
ReadMapper | |
59.53% |
228 / 383 |
|
17.65% |
3 / 17 |
2038.76 | |
0.00% |
0 / 1 |
get | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getRaw | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getAll | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
count | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getRandom | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
columns | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
42.86% |
3 / 7 |
|
0.00% |
0 / 1 |
16.14 | |||
executeGet | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
6 | |||
executeGetRaw | |
43.75% |
7 / 16 |
|
0.00% |
0 / 1 |
6.85 | |||
executeGetAll | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
5.20 | |||
executeCount | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
executeRandom | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getQuery | |
56.35% |
71 / 126 |
|
0.00% |
0 / 1 |
239.63 | |||
populateAbstract | |
59.63% |
65 / 109 |
|
0.00% |
0 / 1 |
253.98 | |||
populateOwnsOne | |
71.43% |
10 / 14 |
|
0.00% |
0 / 1 |
6.84 | |||
populateBelongsTo | |
47.83% |
11 / 23 |
|
0.00% |
0 / 1 |
13.96 | |||
loadHasManyRelations | |
72.92% |
35 / 48 |
|
0.00% |
0 / 1 |
31.62 |
1 | <?php |
2 | /** |
3 | * Jingga |
4 | * |
5 | * PHP Version 8.1 |
6 | * |
7 | * @package phpOMS\DataStorage\Database\Mapper |
8 | * @copyright Dennis Eichhorn |
9 | * @license OMS License 2.0 |
10 | * @version 1.0.0 |
11 | * @link https://jingga.app |
12 | */ |
13 | declare(strict_types=1); |
14 | |
15 | namespace phpOMS\DataStorage\Database\Mapper; |
16 | |
17 | use phpOMS\DataStorage\Database\Query\Builder; |
18 | use phpOMS\DataStorage\Database\Query\Where; |
19 | use phpOMS\Utils\ArrayUtils; |
20 | |
21 | /** |
22 | * Read mapper (SELECTS). |
23 | * |
24 | * @package phpOMS\DataStorage\Database\Mapper |
25 | * @license OMS License 2.0 |
26 | * @link https://jingga.app |
27 | * @since 1.0.0 |
28 | * |
29 | * @todo Add memory cache per read mapper parent call (These should be cached: attribute types, file types, etc.) |
30 | * @todo Add getArray functions to get array instead of object |
31 | * @todo Allow to define columns in all functions instead of members? |
32 | * |
33 | * @template R |
34 | */ |
35 | final class ReadMapper extends DataMapperAbstract |
36 | { |
37 | /** |
38 | * Columns to load |
39 | * |
40 | * @var array |
41 | * @since 1.0.0 |
42 | */ |
43 | private array $columns = []; |
44 | |
45 | /** |
46 | * Create get mapper |
47 | * |
48 | * This makes execute() return a single object or an array of object depending the result size |
49 | * |
50 | * @return self |
51 | * |
52 | * @since 1.0.0 |
53 | */ |
54 | public function get() : self |
55 | { |
56 | $this->type = MapperType::GET; |
57 | |
58 | return $this; |
59 | } |
60 | |
61 | /** |
62 | * Get raw result set |
63 | * |
64 | * @return self |
65 | * |
66 | * @since 1.0.0 |
67 | */ |
68 | public function getRaw() : self |
69 | { |
70 | $this->type = MapperType::GET_RAW; |
71 | |
72 | return $this; |
73 | } |
74 | |
75 | /** |
76 | * Create get mapper |
77 | * |
78 | * This makes execute() always return an array of objects (or an empty array) |
79 | * |
80 | * @return self |
81 | * |
82 | * @since 1.0.0 |
83 | */ |
84 | public function getAll() : self |
85 | { |
86 | $this->type = MapperType::GET_ALL; |
87 | |
88 | return $this; |
89 | } |
90 | |
91 | /** |
92 | * Create count mapper |
93 | * |
94 | * @return self |
95 | * |
96 | * @since 1.0.0 |
97 | */ |
98 | public function count() : self |
99 | { |
100 | $this->type = MapperType::COUNT_MODELS; |
101 | |
102 | return $this; |
103 | } |
104 | |
105 | /** |
106 | * Create random mapper |
107 | * |
108 | * @return self |
109 | * |
110 | * @since 1.0.0 |
111 | */ |
112 | public function getRandom() : self |
113 | { |
114 | $this->type = MapperType::GET_RANDOM; |
115 | |
116 | return $this; |
117 | } |
118 | |
119 | /** |
120 | * Define the columns to load |
121 | * |
122 | * @param array $columns Columns to load |
123 | * |
124 | * @return self |
125 | * |
126 | * @since 1.0.0 |
127 | * @todo: consider to accept properties instead and then check ::COLUMNS which contian the property and ADD that array into $this->columns. Maybe also consider a rename from columns() to property() |
128 | */ |
129 | public function columns(array $columns) : self |
130 | { |
131 | $this->columns = $columns; |
132 | |
133 | return $this; |
134 | } |
135 | |
136 | /** |
137 | * Execute mapper |
138 | * |
139 | * @param mixed ...$options Options to pass to read mapper |
140 | * |
141 | * @return R |
142 | * |
143 | * @since 1.0.0 |
144 | */ |
145 | public function execute(mixed ...$options) : mixed |
146 | { |
147 | switch($this->type) { |
148 | case MapperType::GET: |
149 | /** @var null|Builder ...$options */ |
150 | return $this->executeGet(...$options); |
151 | case MapperType::GET_RAW: |
152 | /** @var null|Builder ...$options */ |
153 | return $this->executeGetRaw(...$options); |
154 | case MapperType::GET_ALL: |
155 | /** @var null|Builder ...$options */ |
156 | return $this->executeGetAll(...$options); |
157 | case MapperType::GET_RANDOM: |
158 | return $this->executeGetRaw(); |
159 | case MapperType::COUNT_MODELS: |
160 | return $this->executeCount(); |
161 | default: |
162 | return null; |
163 | } |
164 | } |
165 | |
166 | /** |
167 | * Execute mapper |
168 | * |
169 | * @param null|Builder $query Query to use instead of the internally generated query |
170 | * Careful, this doesn't merge with the internal query. |
171 | * If you want to merge it use ->query() instead |
172 | * |
173 | * @return R |
174 | * |
175 | * @since 1.0.0 |
176 | */ |
177 | public function executeGet(Builder $query = null) : mixed |
178 | { |
179 | $primaryKeys = []; |
180 | $memberOfPrimaryField = $this->mapper::COLUMNS[$this->mapper::PRIMARYFIELD]['internal']; |
181 | |
182 | if (isset($this->where[$memberOfPrimaryField])) { |
183 | $keys = $this->where[$memberOfPrimaryField][0]['value']; |
184 | $primaryKeys = \array_merge(\is_array($keys) ? $keys : [$keys], $primaryKeys); |
185 | } |
186 | |
187 | // Get initialized objects from memory cache. |
188 | $obj = []; |
189 | |
190 | // Get remaining objects (not available in memory cache) or remaining where clauses. |
191 | $dbData = $this->executeGetRaw($query); |
192 | |
193 | foreach ($dbData as $row) { |
194 | $value = $row[$this->mapper::PRIMARYFIELD . '_d' . $this->depth]; |
195 | $obj[$value] = $this->mapper::createBaseModel($row); |
196 | |
197 | $obj[$value] = $this->populateAbstract($row, $obj[$value]); |
198 | $this->loadHasManyRelations($obj[$value]); |
199 | } |
200 | |
201 | $countResulsts = \count($obj); |
202 | |
203 | if ($countResulsts === 0) { |
204 | return $this->mapper::createNullModel(); |
205 | } elseif ($countResulsts === 1) { |
206 | return \reset($obj); |
207 | } |
208 | |
209 | return $obj; |
210 | } |
211 | |
212 | /** |
213 | * Execute mapper |
214 | * |
215 | * @param null|Builder $query Query to use instead of the internally generated query |
216 | * Careful, this doesn't merge with the internal query. |
217 | * If you want to merge it use ->query() instead |
218 | * |
219 | * @return array |
220 | * |
221 | * @since 1.0.0 |
222 | */ |
223 | public function executeGetRaw(Builder $query = null) : array |
224 | { |
225 | $query ??= $this->getQuery(); |
226 | |
227 | try { |
228 | $results = false; |
229 | |
230 | $sth = $this->db->con->prepare($a = $query->toSql()); |
231 | if ($sth !== false) { |
232 | $sth->execute(); |
233 | $results = $sth->fetchAll(\PDO::FETCH_ASSOC); |
234 | } |
235 | } catch (\Throwable $t) { |
236 | $results = false; |
237 | |
238 | \phpOMS\Log\FileLogger::getInstance()->error( |
239 | \phpOMS\Log\FileLogger::MSG_FULL, [ |
240 | 'message' => $t->getMessage() . ':' . $query->toSql(), |
241 | 'line' => __LINE__, |
242 | 'file' => self::class, |
243 | ] |
244 | ); |
245 | } |
246 | |
247 | return $results === false ? [] : $results; |
248 | } |
249 | |
250 | /** |
251 | * Execute mapper |
252 | * |
253 | * @param null|Builder $query Query to use instead of the internally generated query |
254 | * Careful, this doesn't merge with the internal query. |
255 | * If you want to merge it use ->query() instead |
256 | * |
257 | * @return array |
258 | * |
259 | * @since 1.0.0 |
260 | */ |
261 | public function executeGetAll(Builder $query = null) : array |
262 | { |
263 | $result = $this->executeGet($query); |
264 | |
265 | if (\is_object($result) |
266 | && (\str_starts_with($class = \get_class($result), 'Null') || \stripos($class, '\Null') !== false) |
267 | ) { |
268 | return []; |
269 | } |
270 | |
271 | return \is_array($result) ? $result : [$result]; |
272 | } |
273 | |
274 | /** |
275 | * Count the number of elements |
276 | * |
277 | * @return int |
278 | * |
279 | * @since 1.0.0 |
280 | */ |
281 | public function executeCount() : int |
282 | { |
283 | $query = $this->getQuery(null, ['COUNT(*)' => 'count']); |
284 | |
285 | return (int) $query->execute()?->fetchColumn(); |
286 | } |
287 | |
288 | /** |
289 | * Get random object |
290 | * |
291 | * @return mixed |
292 | * |
293 | * @since 1.0.0 |
294 | */ |
295 | public function executeRandom() : mixed |
296 | { |
297 | $query = $this->getQuery(); |
298 | $query->random($this->mapper::PRIMARYFIELD); |
299 | |
300 | return $this->executeGet($query); |
301 | } |
302 | |
303 | /** |
304 | * Get mapper specific builder |
305 | * |
306 | * @param Builder $query Query to fill |
307 | * @param array $columns Columns to use |
308 | * |
309 | * @return Builder |
310 | * |
311 | * @since 1.0.0 |
312 | */ |
313 | public function getQuery(Builder $query = null, array $columns = []) : Builder |
314 | { |
315 | $query ??= $this->query ?? new Builder($this->db, true); |
316 | $columns = empty($columns) |
317 | ? (empty($this->columns) ? $this->mapper::COLUMNS : $this->columns) |
318 | : $columns; |
319 | |
320 | foreach ($columns as $key => $values) { |
321 | if (\is_string($values)) { |
322 | $query->selectAs($key, $values); |
323 | } elseif (($values['writeonly'] ?? false) === false || isset($this->with[$values['internal']])) { |
324 | $query->selectAs($this->mapper::TABLE . '_d' . $this->depth . '.' . $key, $key . '_d' . $this->depth); |
325 | } |
326 | } |
327 | |
328 | if (empty($query->from)) { |
329 | $query->fromAs($this->mapper::TABLE, $this->mapper::TABLE . '_d' . $this->depth); |
330 | } |
331 | |
332 | // Join tables manually without using "with()" (NOT has many/owns one etc.) |
333 | // This is necessary for special cases, e.g. when joining in the other direction |
334 | // Example: Show all profiles who have written a news article. |
335 | // "with()" only allows to go from articles to accounts but we want to go the other way |
336 | foreach ($this->join as $member => $values) { |
337 | if (($col = $this->mapper::getColumnByMember($member)) === null) { |
338 | continue; |
339 | } |
340 | |
341 | /* variable in model */ |
342 | // @todo: join handling is extremely ugly, needs to be refactored |
343 | foreach ($values as $join) { |
344 | // @todo: the has many, etc. if checks only work if it is a relation on the first level, if we have a deeper where condition nesting this fails |
345 | if ($join['child'] !== '') { |
346 | continue; |
347 | } |
348 | |
349 | if (isset($join['mapper']::HAS_MANY[$join['value']])) { |
350 | if (isset($join['mapper']::HAS_MANY[$join['value']]['external'])) { |
351 | // join with relation table |
352 | $query->join($join['mapper']::HAS_MANY[$join['value']]['table'], $join['type'], $join['mapper']::HAS_MANY[$join['value']]['table'] . '_d' . ($this->depth + 1)) |
353 | ->on( |
354 | $this->mapper::TABLE . '_d' . $this->depth . '.' . $col, |
355 | '=', |
356 | $join['mapper']::HAS_MANY[$join['value']]['table'] . '_d' . ($this->depth + 1) . '.' . $join['mapper']::HAS_MANY[$join['value']]['external'], |
357 | 'AND', |
358 | $join['mapper']::HAS_MANY[$join['value']]['table'] . '_d' . ($this->depth + 1) |
359 | ); |
360 | |
361 | // join with model table |
362 | $query->join($join['mapper']::TABLE, $join['type'], $join['mapper']::TABLE . '_d' . ($this->depth + 1)) |
363 | ->on( |
364 | $join['mapper']::HAS_MANY[$join['value']]['table'] . '_d' . ($this->depth + 1) . '.' . $join['mapper']::HAS_MANY[$join['value']]['self'], |
365 | '=', |
366 | $join['mapper']::TABLE . '_d' . ($this->depth + 1) . '.' . $join['mapper']::PRIMARYFIELD, |
367 | 'AND', |
368 | $join['mapper']::TABLE . '_d' . ($this->depth + 1) |
369 | ); |
370 | |
371 | if (isset($this->on[$join['value']])) { |
372 | foreach ($this->on[$join['value']] as $on) { |
373 | $query->where( |
374 | $join['mapper']::TABLE . '_d' . ($this->depth + 1) . '.' . $join['mapper']::getColumnByMember($on['member']), |
375 | '=', |
376 | $on['value'], |
377 | 'AND' |
378 | ); |
379 | } |
380 | } |
381 | } |
382 | } else { |
383 | $query->join($join['mapper']::TABLE, $join['type'], $join['mapper']::TABLE . '_d' . ($this->depth + 1)) |
384 | ->on( |
385 | $this->mapper::TABLE . '_d' . $this->depth . '.' . $col, |
386 | '=', |
387 | $join['mapper']::TABLE . '_d' . ($this->depth + 1) . '.' . $join['mapper']::getColumnByMember($join['value']), |
388 | 'AND', |
389 | $join['mapper']::TABLE . '_d' . ($this->depth + 1) |
390 | ); |
391 | } |
392 | } |
393 | } |
394 | |
395 | // where |
396 | foreach ($this->where as $member => $values) { |
397 | // handle where query |
398 | if ($member === '' && $values[0]['value'] instanceof Where) { |
399 | $query->where($values[0]['value'], boolean: $values[0]['comparison']); |
400 | |
401 | continue; |
402 | } |
403 | |
404 | if (($col = $this->mapper::getColumnByMember($member)) === null) { |
405 | continue; |
406 | } |
407 | |
408 | // In case alternative where values are allowed |
409 | // This is different from normal or conditions as these are exclusive or conditions |
410 | // This means they are only selected IFF the previous where clause fails |
411 | $alt = []; |
412 | |
413 | /* variable in model */ |
414 | $previous = null; |
415 | foreach ($values as $where) { |
416 | // @todo: the has many, etc. if checks only work if it is a relation on the first level, if we have a deeper where condition nesting this fails |
417 | if ($where['child'] !== '') { |
418 | continue; |
419 | } |
420 | |
421 | $comparison = \is_array($where['value']) && \count($where['value']) > 1 ? 'in' : $where['logic']; |
422 | if ($where['comparison'] === 'ALT') { |
423 | // This uses an alternative value if the previous value(s) in the where clause don't exist (e.g. for localized results where you allow a user language, alternatively a primary language, and then alternatively any language if the first two don't exist). |
424 | |
425 | // is first value |
426 | if (empty($alt)) { |
427 | $alt[] = $previous['value']; |
428 | } |
429 | |
430 | /* |
431 | select * from table_name |
432 | where // where starts here |
433 | field1 = 'value1' // comes from normal where |
434 | or ( // where1 starts here |
435 | field1 = 'default' |
436 | and NOT EXISTS ( // where2 starts here |
437 | select 1 from table_name where field1 = 'value1' |
438 | ) |
439 | ) |
440 | */ |
441 | $where1 = new Where($this->db); |
442 | $where1->where($this->mapper::TABLE . '_d' . $this->depth . '.' . $col, $comparison, $where['value'], 'and'); |
443 | |
444 | $where2 = new Builder($this->db); |
445 | $where2->select('1') |
446 | ->from($this->mapper::TABLE . '_d' . $this->depth) |
447 | ->where($this->mapper::TABLE . '_d' . $this->depth . '.' . $col, 'in', $alt); |
448 | |
449 | $where1->where($this->mapper::TABLE . '_d' . $this->depth . '.' . $col, 'not exists', $where2, 'and'); |
450 | |
451 | $query->where($this->mapper::TABLE . '_d' . $this->depth . '.' . $col, $comparison, $where1, 'or'); |
452 | |
453 | $alt[] = $where['value']; |
454 | } else { |
455 | $previous = $where; |
456 | $query->where($this->mapper::TABLE . '_d' . $this->depth . '.' . $col, $comparison, $where['value'], $where['comparison']); |
457 | } |
458 | } |
459 | } |
460 | |
461 | // load relations |
462 | foreach ($this->with as $member => $data) { |
463 | $rel = null; |
464 | if ((isset($this->mapper::OWNS_ONE[$member]) || isset($this->mapper::BELONGS_TO[$member])) |
465 | || (!isset($this->mapper::HAS_MANY[$member]['external']) && isset($this->mapper::HAS_MANY[$member]['column'])) |
466 | ) { |
467 | $rel = $this->mapper::OWNS_ONE[$member] ?? ($this->mapper::BELONGS_TO[$member] ?? ($this->mapper::HAS_MANY[$member] ?? null)); |
468 | } else { |
469 | continue; |
470 | } |
471 | |
472 | foreach ($data as $with) { |
473 | if ($with['child'] !== '') { |
474 | continue; |
475 | } |
476 | |
477 | if (isset($this->mapper::OWNS_ONE[$member]) || isset($this->mapper::BELONGS_TO[$member])) { |
478 | $query->leftJoin($rel['mapper']::TABLE, $rel['mapper']::TABLE . '_d' . ($this->depth + 1)) |
479 | ->on( |
480 | $this->mapper::TABLE . '_d' . $this->depth . '.' . $rel['external'], '=', |
481 | $rel['mapper']::TABLE . '_d' . ($this->depth + 1) . '.' . ( |
482 | isset($rel['by']) ? $rel['mapper']::getColumnByMember($rel['by']) : $rel['mapper']::PRIMARYFIELD |
483 | ), 'and', |
484 | $rel['mapper']::TABLE . '_d' . ($this->depth + 1) |
485 | ); |
486 | } elseif (!isset($this->mapper::HAS_MANY[$member]['external']) && isset($this->mapper::HAS_MANY[$member]['column'])) { |
487 | // get HasManyQuery (but only for elements which have a 'column' defined) |
488 | |
489 | // @todo: handle self and self === null |
490 | $query->leftJoin($rel['mapper']::TABLE, $rel['mapper']::TABLE . '_d' . ($this->depth + 1)) |
491 | ->on( |
492 | $this->mapper::TABLE . '_d' . $this->depth . '.' . ($rel['external'] ?? $this->mapper::PRIMARYFIELD), '=', |
493 | $rel['mapper']::TABLE . '_d' . ($this->depth + 1) . '.' . ( |
494 | isset($rel['by']) ? $rel['mapper']::getColumnByMember($rel['by']) : $rel['self'] |
495 | ), 'and', |
496 | $rel['mapper']::TABLE . '_d' . ($this->depth + 1) |
497 | ); |
498 | } |
499 | |
500 | /** @var self $relMapper */ |
501 | $relMapper = $this->createRelationMapper($rel['mapper']::reader(db: $this->db), $member); |
502 | $relMapper->depth = $this->depth + 1; |
503 | |
504 | $query = $relMapper->getQuery( |
505 | $query, |
506 | isset($rel['column']) ? [$rel['mapper']::getColumnByMember($rel['column']) => []] : [] |
507 | ); |
508 | |
509 | break; // there is only one root element (one element with child === '') |
510 | } |
511 | } |
512 | |
513 | // handle sort, the column name order is very important. Therefore it cannot be done in the foreach loop above! |
514 | foreach ($this->sort as $member => $data) { |
515 | foreach ($data as $sort) { |
516 | if (($column = $this->mapper::getColumnByMember($member)) === null |
517 | || ($sort['child'] !== '') |
518 | ) { |
519 | continue; |
520 | } |
521 | |
522 | $query->orderBy($this->mapper::TABLE . '_d' . $this->depth . '.' . $column, $sort['order']); |
523 | |
524 | break; // there is only one root element (one element with child === '') |
525 | } |
526 | } |
527 | |
528 | // handle limit |
529 | foreach ($this->limit as $member => $data) { |
530 | if ($member !== '') { |
531 | continue; |
532 | } |
533 | |
534 | foreach ($data as $limit) { |
535 | if ($limit['child'] === '') { |
536 | $query->limit($limit['limit']); |
537 | |
538 | break 2; // there is only one root element (one element with child === '') |
539 | } |
540 | } |
541 | } |
542 | |
543 | return $query; |
544 | } |
545 | |
546 | /** |
547 | * Populate data. |
548 | * |
549 | * @param array $result Query result set |
550 | * @param object $obj Object to populate |
551 | * |
552 | * @return object |
553 | * |
554 | * @since 1.0.0 |
555 | */ |
556 | public function populateAbstract(array $result, object $obj) : object |
557 | { |
558 | $refClass = new \ReflectionClass($obj); |
559 | |
560 | foreach ($this->mapper::COLUMNS as $column => $def) { |
561 | $alias = $column . '_d' . $this->depth; |
562 | |
563 | if (!\array_key_exists($alias, $result)) { |
564 | continue; |
565 | } |
566 | |
567 | $value = $result[$alias]; |
568 | |
569 | $hasPath = false; |
570 | $aValue = []; |
571 | $arrayPath = ''; |
572 | |
573 | if (\stripos($def['internal'], '/') !== false) { |
574 | $hasPath = true; |
575 | $path = \explode('/', \ltrim($def['internal'], '/')); |
576 | $member = $path[0]; |
577 | |
578 | $refProp = $refClass->getProperty($path[0]); |
579 | $isPublic = $refProp->isPublic(); |
580 | $aValue = $isPublic ? $obj->{$path[0]} : $refProp->getValue($obj); |
581 | |
582 | \array_shift($path); |
583 | $arrayPath = \implode('/', $path); |
584 | } else { |
585 | $refProp = $refClass->getProperty($def['internal']); |
586 | $isPublic = $refProp->isPublic(); |
587 | $member = $def['internal']; |
588 | } |
589 | |
590 | if (isset($this->mapper::OWNS_ONE[$def['internal']])) { |
591 | $default = null; |
592 | if (!isset($this->with[$member]) && $refProp->isInitialized($obj)) { |
593 | $default = $isPublic ? $obj->{$def['internal']} : $refProp->getValue($obj); |
594 | } |
595 | |
596 | $value = $this->populateOwnsOne($def['internal'], $result, $default); |
597 | |
598 | // loads has many relations. other relations are loaded in the populateOwnsOne |
599 | if (\is_object($value) && isset($this->mapper::OWNS_ONE[$def['internal']]['mapper'])) { |
600 | $this->mapper::OWNS_ONE[$def['internal']]['mapper']::reader(db: $this->db)->loadHasManyRelations($value); |
601 | } |
602 | |
603 | if (!empty($value)) { |
604 | // @todo: find better solution. this was because of a bug with the sales billing list query depth = 4. The address was set (from the client, referral or creator) but then somehow there was a second address element which was all null and null cannot be asigned to a string variable (e.g. country). The problem with this solution is that if the model expects an initialization (e.g. at lest set the elements to null, '', 0 etc.) this is now not done. |
605 | $refProp->setValue($obj, $value); |
606 | } |
607 | } elseif (isset($this->mapper::BELONGS_TO[$def['internal']])) { |
608 | $default = null; |
609 | if (!isset($this->with[$member]) && $refProp->isInitialized($obj)) { |
610 | $default = $isPublic ? $obj->{$def['internal']} : $refProp->getValue($obj); |
611 | } |
612 | |
613 | $value = $this->populateBelongsTo($def['internal'], $result, $default); |
614 | |
615 | // loads has many relations. other relations are loaded in the populateBelongsTo |
616 | if (\is_object($value) && isset($this->mapper::BELONGS_TO[$def['internal']]['mapper'])) { |
617 | $this->mapper::BELONGS_TO[$def['internal']]['mapper']::reader(db: $this->db)->loadHasManyRelations($value); |
618 | } |
619 | |
620 | $refProp->setValue($obj, $value); |
621 | } elseif (\in_array($def['type'], ['string', 'compress', 'int', 'float', 'bool'])) { |
622 | if ($value !== null && $def['type'] === 'compress') { |
623 | $def['type'] = 'string'; |
624 | |
625 | $value = \gzinflate($value); |
626 | } |
627 | |
628 | if ($value !== null || $refProp->getValue($obj) !== null) { |
629 | \settype($value, $def['type']); |
630 | } |
631 | |
632 | if ($hasPath) { |
633 | $value = ArrayUtils::setArray($arrayPath, $aValue, $value, '/', true); |
634 | } |
635 | |
636 | $refProp->setValue($obj, $value); |
637 | } elseif ($def['type'] === 'DateTime') { |
638 | $value = $value === null ? null : new \DateTime($value); |
639 | if ($hasPath) { |
640 | $value = ArrayUtils::setArray($arrayPath, $aValue, $value, '/', true); |
641 | } |
642 | |
643 | $refProp->setValue($obj, $value); |
644 | } elseif ($def['type'] === 'DateTimeImmutable') { |
645 | $value = $value === null ? null : new \DateTimeImmutable($value); |
646 | if ($hasPath) { |
647 | $value = ArrayUtils::setArray($arrayPath, $aValue, $value, '/', true); |
648 | } |
649 | |
650 | $refProp->setValue($obj, $value); |
651 | } elseif ($def['type'] === 'Json') { |
652 | if ($hasPath) { |
653 | $value = ArrayUtils::setArray($arrayPath, $aValue, $value, '/', true); |
654 | } |
655 | |
656 | $refProp->setValue($obj, \json_decode($value, true)); |
657 | } elseif ($def['type'] === 'Serializable') { |
658 | $member = $isPublic ? $obj->{$def['internal']} : $refProp->getValue($obj); |
659 | |
660 | if ($member === null || $value === null) { |
661 | $obj->{$def['internal']} = $value; |
662 | } else { |
663 | $member->unserialize($value); |
664 | } |
665 | } |
666 | } |
667 | |
668 | foreach ($this->mapper::HAS_MANY as $member => $def) { |
669 | $column = $def['mapper']::getColumnByMember($def['column'] ?? $member); |
670 | $alias = $column . '_d' . ($this->depth + 1); |
671 | |
672 | if (!\array_key_exists($alias, $result) || !isset($def['column'])) { |
673 | continue; |
674 | } |
675 | |
676 | $value = $result[$alias]; |
677 | $hasPath = false; |
678 | $aValue = null; |
679 | $arrayPath = '/'; |
680 | |
681 | if (\stripos($member, '/') !== false) { |
682 | $hasPath = true; |
683 | $path = \explode('/', $member); |
684 | $refProp = $refClass->getProperty($path[0]); |
685 | $isPublic = $refProp->isPublic(); |
686 | |
687 | \array_shift($path); |
688 | $arrayPath = \implode('/', $path); |
689 | $aValue = $isPublic ? $obj->{$path[0]} : $refProp->getValue($obj); |
690 | } else { |
691 | $refProp = $refClass->getProperty($member); |
692 | $isPublic = $refProp->isPublic(); |
693 | } |
694 | |
695 | if (\in_array($def['mapper']::COLUMNS[$column]['type'], ['string', 'int', 'float', 'bool'])) { |
696 | if ($value !== null || $refProp->getValue($obj) !== null) { |
697 | \settype($value, $def['mapper']::COLUMNS[$column]['type']); |
698 | } |
699 | |
700 | if ($hasPath) { |
701 | $value = ArrayUtils::setArray($arrayPath, $aValue, $value, '/', true); |
702 | } |
703 | |
704 | $refProp->setValue($obj, $value); |
705 | } elseif ($def['mapper']::COLUMNS[$column]['type'] === 'DateTime') { |
706 | $value = $value === null ? null : new \DateTime($value); |
707 | if ($hasPath) { |
708 | $value = ArrayUtils::setArray($arrayPath, $aValue, $value, '/', true); |
709 | } |
710 | |
711 | $refProp->setValue($obj, $value); |
712 | } elseif ($def['mapper']::COLUMNS[$column]['type'] === 'DateTimeImmutable') { |
713 | $value = $value === null ? null : new \DateTimeImmutable($value); |
714 | if ($hasPath) { |
715 | $value = ArrayUtils::setArray($arrayPath, $aValue, $value, '/', true); |
716 | } |
717 | |
718 | $refProp->setValue($obj, $value); |
719 | } elseif ($def['mapper']::COLUMNS[$column]['type'] === 'Json') { |
720 | if ($hasPath) { |
721 | $value = ArrayUtils::setArray($arrayPath, $aValue, $value, '/', true); |
722 | } |
723 | |
724 | $refProp->setValue($obj, \json_decode($value, true)); |
725 | } elseif ($def['mapper']::COLUMNS[$column]['type'] === 'Serializable') { |
726 | $member = $isPublic ? $obj->{$member} : $refProp->getValue($obj); |
727 | $member->unserialize($value); |
728 | } |
729 | } |
730 | |
731 | return $obj; |
732 | } |
733 | |
734 | /** |
735 | * Populate data. |
736 | * |
737 | * @param string $member Member name |
738 | * @param array $result Result data |
739 | * @param mixed $default Default value |
740 | * |
741 | * @return mixed |
742 | * |
743 | * @todo: in the future we could pass not only the $id ref but all of the data as a join!!! and save an additional select!!! |
744 | * @todo: parent and child elements however must be loaded because they are not loaded |
745 | * |
746 | * @since 1.0.0 |
747 | */ |
748 | public function populateOwnsOne(string $member, array $result, mixed $default = null) : mixed |
749 | { |
750 | /** @var class-string<DataMapperFactory> $mapper */ |
751 | $mapper = $this->mapper::OWNS_ONE[$member]['mapper']; |
752 | |
753 | if (!isset($this->with[$member])) { |
754 | if (\array_key_exists($this->mapper::OWNS_ONE[$member]['external'] . '_d' . ($this->depth), $result)) { |
755 | return isset($this->mapper::OWNS_ONE[$member]['column']) |
756 | ? $result[$this->mapper::OWNS_ONE[$member]['external'] . '_d' . ($this->depth)] |
757 | : $mapper::createNullModel($result[$this->mapper::OWNS_ONE[$member]['external'] . '_d' . ($this->depth)]); |
758 | } else { |
759 | return $default; |
760 | } |
761 | } |
762 | |
763 | if (isset($this->mapper::OWNS_ONE[$member]['column'])) { |
764 | return $result[$mapper::getColumnByMember($this->mapper::OWNS_ONE[$member]['column']) . '_d' . $this->depth]; |
765 | } |
766 | |
767 | if (!isset($result[$mapper::PRIMARYFIELD . '_d' . ($this->depth + 1)])) { |
768 | return $mapper::createNullModel(); |
769 | } |
770 | |
771 | /** @var self $ownsOneMapper */ |
772 | $ownsOneMapper = $this->createRelationMapper($mapper::get($this->db), $member); |
773 | $ownsOneMapper->depth = $this->depth + 1; |
774 | |
775 | return $ownsOneMapper->populateAbstract($result, $mapper::createBaseModel($result)); |
776 | } |
777 | |
778 | /** |
779 | * Populate data. |
780 | * |
781 | * @param string $member Member name |
782 | * @param array $result Result data |
783 | * @param mixed $default Default value |
784 | * |
785 | * @return mixed |
786 | * |
787 | * @todo: in the future we could pass not only the $id ref but all of the data as a join!!! and save an additional select!!! |
788 | * @todo: only the belongs to model gets populated the children of the belongsto model are always null models. either this function needs to call the get for the children, it should call get for the belongs to right away like the has many, or i find a way to recursevily load the data for all sub models and then populate that somehow recursively, probably too complex. |
789 | * |
790 | * @since 1.0.0 |
791 | */ |
792 | public function populateBelongsTo(string $member, array $result, mixed $default = null) : mixed |
793 | { |
794 | /** @var class-string<DataMapperFactory> $mapper */ |
795 | $mapper = $this->mapper::BELONGS_TO[$member]['mapper']; |
796 | |
797 | if (!isset($this->with[$member])) { |
798 | if (\array_key_exists($this->mapper::BELONGS_TO[$member]['external'] . '_d' . ($this->depth), $result)) { |
799 | return isset($this->mapper::BELONGS_TO[$member]['column']) |
800 | ? $result[$this->mapper::BELONGS_TO[$member]['external'] . '_d' . ($this->depth)] |
801 | : $mapper::createNullModel($result[$this->mapper::BELONGS_TO[$member]['external'] . '_d' . ($this->depth)]); |
802 | } else { |
803 | return $default; |
804 | } |
805 | } |
806 | |
807 | if (isset($this->mapper::BELONGS_TO[$member]['column'])) { |
808 | return $result[$mapper::getColumnByMember($this->mapper::BELONGS_TO[$member]['column']) . '_d' . $this->depth]; |
809 | } |
810 | |
811 | if (!isset($result[$mapper::PRIMARYFIELD . '_d' . ($this->depth + 1)])) { |
812 | return $mapper::createNullModel(); |
813 | } |
814 | |
815 | // get the belongs to based on a different column (not primary key) |
816 | // this is often used if the value is actually a different model: |
817 | // you want the profile but the account id is referenced |
818 | // in this case you can get the profile by loading the profile based on the account reference column |
819 | if (isset($this->mapper::BELONGS_TO[$member]['by'])) { |
820 | /** @var self $belongsToMapper */ |
821 | $belongsToMapper = $this->createRelationMapper($mapper::get($this->db), $member); |
822 | $belongsToMapper->depth = $this->depth + 1; |
823 | $belongsToMapper->where( |
824 | $this->mapper::BELONGS_TO[$member]['by'], |
825 | $result[$mapper::getColumnByMember($this->mapper::BELONGS_TO[$member]['by']) . '_d' . ($this->depth + 1)], |
826 | '=' |
827 | ); |
828 | |
829 | return $belongsToMapper->execute(); |
830 | } |
831 | |
832 | /** @var self $belongsToMapper */ |
833 | $belongsToMapper = $this->createRelationMapper($mapper::get($this->db), $member); |
834 | $belongsToMapper->depth = $this->depth + 1; |
835 | |
836 | return $belongsToMapper->populateAbstract($result, $mapper::createBaseModel($result)); |
837 | } |
838 | |
839 | /** |
840 | * Fill object with relations |
841 | * |
842 | * @param object $obj Object to fill |
843 | * |
844 | * @return void |
845 | * |
846 | * @since 1.0.0 |
847 | */ |
848 | public function loadHasManyRelations(object $obj) : void |
849 | { |
850 | if (empty($this->with)) { |
851 | return; |
852 | } |
853 | |
854 | $primaryKey = $this->mapper::getObjectId($obj); |
855 | if (empty($primaryKey)) { |
856 | return; |
857 | } |
858 | |
859 | $refClass = null; |
860 | |
861 | // @todo: check if there are more cases where the relation is already loaded with joins etc. |
862 | // there can be pseudo has many elements like localizations. They are has manies but these are already loaded with joins! |
863 | foreach ($this->with as $member => $withData) { |
864 | if (isset($this->mapper::HAS_MANY[$member])) { |
865 | $many = $this->mapper::HAS_MANY[$member]; |
866 | if (isset($many['column'])) { |
867 | continue; |
868 | } |
869 | |
870 | $objectMapper = $this->createRelationMapper($many['mapper']::get(db: $this->db), $member); |
871 | if ($many['external'] === null/* same as $many['table'] !== $many['mapper']::TABLE */) { |
872 | $objectMapper->where($many['mapper']::COLUMNS[$many['self']]['internal'], $primaryKey); |
873 | } else { |
874 | $query = new Builder($this->db, true); |
875 | $query->leftJoin($many['table']) |
876 | ->on($many['mapper']::TABLE . '_d1.' . $many['mapper']::PRIMARYFIELD, '=', $many['table'] . '.' . $many['external']) |
877 | ->where($many['table'] . '.' . $many['self'], '=', $primaryKey); |
878 | |
879 | // Cannot use join, because join only works on members and we don't have members for a relation table |
880 | // This is why we need to create a "base" query which contians the join on table columns |
881 | $objectMapper->query($query); |
882 | } |
883 | |
884 | $objects = $objectMapper->execute(); |
885 | if (empty($objects) || (!\is_array($objects) && $objects->id === 0)) { |
886 | continue; |
887 | } |
888 | |
889 | if ($refClass === null) { |
890 | $refClass = new \ReflectionClass($obj); |
891 | } |
892 | |
893 | $refProp = $refClass->getProperty($member); |
894 | if (!$refProp->isPublic()) { |
895 | $refProp->setValue($obj, !\is_array($objects) && ($many['conditional'] ?? false) === false |
896 | ? [$many['mapper']::getObjectId($objects) => $objects] |
897 | : $objects // if conditional === true the obj will be asigned (e.g. has many localizations but only one is loaded for the model) |
898 | ); |
899 | } else { |
900 | $obj->{$member} = !\is_array($objects) && ($many['conditional'] ?? false) === false |
901 | ? [$many['mapper']::getObjectId($objects) => $objects] |
902 | : $objects; // if conditional === true the obj will be asigned (e.g. has many localizations but only one is loaded for the model) |
903 | } |
904 | |
905 | continue; |
906 | } elseif (isset($this->mapper::OWNS_ONE[$member]) |
907 | || isset($this->mapper::BELONGS_TO[$member]) |
908 | ) { |
909 | $relation = isset($this->mapper::OWNS_ONE[$member]) |
910 | ? $this->mapper::OWNS_ONE[$member] |
911 | : $this->mapper::BELONGS_TO[$member]; |
912 | |
913 | if (\count($withData) < 2) { |
914 | continue; |
915 | } |
916 | |
917 | if ($refClass === null) { |
918 | $refClass = new \ReflectionClass($obj); |
919 | } |
920 | |
921 | /** @var ReadMapper $relMapper */ |
922 | $relMapper = $this->createRelationMapper($relation['mapper']::reader($this->db), $member); |
923 | |
924 | $refProp = $refClass->getProperty($member); |
925 | if (!$refProp->isPublic()) { |
926 | $relMapper->loadHasManyRelations($refProp->getValue($obj)); |
927 | } else { |
928 | $relMapper->loadHasManyRelations($obj->{$member}); |
929 | } |
930 | } |
931 | } |
932 | } |
933 | } |