Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
50.00% covered (danger)
50.00%
73 / 146
28.57% covered (danger)
28.57%
4 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
DataMapperAbstract
50.00% covered (danger)
50.00%
73 / 146
28.57% covered (danger)
28.57%
4 / 14
510.00
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 query
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 with
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 sort
68.75% covered (warning)
68.75%
11 / 16
0.00% covered (danger)
0.00%
0 / 1
5.76
 offset
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 limit
68.75% covered (warning)
68.75%
11 / 16
0.00% covered (danger)
0.00%
0 / 1
5.76
 where
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 join
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 on
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 leftJoin
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 rightJoin
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 innerJoin
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 createRelationMapper
40.54% covered (danger)
40.54%
15 / 37
0.00% covered (danger)
0.00%
0 / 1
123.74
 parseValue
80.95% covered (warning)
80.95%
17 / 21
0.00% covered (danger)
0.00%
0 / 1
15.35
 execute
n/a
0 / 0
n/a
0 / 0
0
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 */
13declare(strict_types=1);
14
15namespace phpOMS\DataStorage\Database\Mapper;
16
17use phpOMS\DataStorage\Database\Connection\ConnectionAbstract;
18use phpOMS\DataStorage\Database\Query\Builder;
19use phpOMS\DataStorage\Database\Query\JoinType;
20use phpOMS\DataStorage\Database\Query\OrderType;
21
22/**
23 * Mapper abstract.
24 *
25 * @package phpOMS\DataStorage\Database\Mapper
26 * @license OMS License 2.0
27 * @link    https://jingga.app
28 * @since   1.0.0
29 */
30abstract class DataMapperAbstract
31{
32    /**
33     * Base mapper
34     *
35     * @var DataMapperFactory
36     * @since 1.0.0
37     */
38    protected DataMapperFactory $mapper;
39
40    /**
41     * Mapper type (e.g. writer, reader, ...)
42     *
43     * @var int
44     * @since 1.0.0
45     */
46    protected int $type = 0;
47
48    /**
49     * Mapper depths.
50     *
51     * Mappers may have relations to other models (e.g. hasMany) which can have other relations, ...
52     * The depths indicates how deep in the relation tree we are
53     *
54     * @var int
55     * @since 1.0.0
56     */
57    protected int $depth = 1;
58
59    /**
60     * Relations which should be loaded
61     *
62     * @var array
63     * @since 1.0.0
64     */
65    protected array $with = [];
66
67    /**
68     * Sort order
69     *
70     * @var array
71     * @since 1.0.0
72     */
73    protected array $sort = [];
74
75    /**
76     * Offset
77     *
78     * @var array
79     * @since 1.0.0
80     */
81    protected array $offset = [];
82
83    /**
84     * Limit
85     *
86     * @var array
87     * @since 1.0.0
88     */
89    protected array $limit = [];
90
91    /**
92     * Where conditions
93     *
94     * @var array
95     * @since 1.0.0
96     */
97    protected array $where = [];
98
99    /**
100     * Join conditions
101     *
102     * @var array
103     * @since 1.0.0
104     */
105    protected array $join = [];
106
107    /**
108     * Join conditions
109     *
110     * @var array
111     * @since 1.0.0
112     */
113    protected array $on = [];
114
115    /**
116     * Base query which is merged with the query in the mapper
117     *
118     * Sometimes you want to merge two queries together.
119     *
120     * @var null|Builder
121     * @since 1.0.0
122     */
123    protected ?Builder $query = null;
124
125    /**
126     * Database connection.
127     *
128     * @var ConnectionAbstract
129     * @since 1.0.0
130     */
131    protected ConnectionAbstract $db;
132
133    /** Constructor.
134     *
135     * @param DataMapperFactory  $mapper Base mapper
136     * @param ConnectionAbstract $db     Database connection
137     *
138     * @since 1.0.0
139     */
140    public function __construct(DataMapperFactory $mapper, ConnectionAbstract $db)
141    {
142        $this->mapper = $mapper;
143        $this->db     = $db;
144    }
145
146    /**
147     * Define a query which is merged with the internal query generation.
148     *
149     * @param Builder $query Query
150     *
151     * @return static
152     *
153     * @since 1.0.0
154     */
155    public function query(Builder $query = null) : self
156    {
157        $this->query = $query;
158
159        return $this;
160    }
161
162    /**
163     * Define model relations which should be loaded
164     *
165     * @param string $member Property name of the relation (e.g. hasMany, belongsTo, ...)
166     *
167     * @return static
168     *
169     * @since 1.0.0
170     */
171    public function with(string $member) : self
172    {
173        $split       = \explode('/', $member);
174        $memberSplit = \array_shift($split);
175
176        $this->with[$memberSplit][] = [
177            'child' => \implode('/', $split),
178        ];
179
180        return $this;
181    }
182
183    /**
184     * Sort order
185     *
186     * @param string $member Property name to sort by
187     * @param string $order  Order type (DESC/ASC)
188     *
189     * @return static
190     *
191     * @since 1.0.0
192     */
193    public function sort(string $member, string $order = OrderType::DESC) : self
194    {
195        $split       = \explode('/', $member);
196        $memberSplit = \array_shift($split);
197
198        $child       = \implode('/', $split);
199        $overwritten = false;
200
201        if (isset($this->sort[$memberSplit])) {
202            foreach ($this->sort[$memberSplit] as $key => $element) {
203                if ($element['child'] === $child) {
204                    $this->sort[$memberSplit][$key]['order'] = $order;
205                    $overwritten                             = true;
206
207                    break;
208                }
209            }
210        }
211
212        if (!$overwritten) {
213            $this->sort[$memberSplit][] = [
214                'child' => \implode('/', $split),
215                'order' => $order,
216            ];
217        }
218
219        return $this;
220    }
221
222    /**
223     * Define the result offset
224     *
225     * @param int    $offset Offset
226     * @param string $member Property name to offset by ('' = base model, anything else for relations such as hasMany relations)
227     *
228     * @return static
229     *
230     * @since 1.0.0
231     */
232    public function offset(int $offset = 0, string $member = '') : self
233    {
234        $split       = \explode('/', $member);
235        $memberSplit = \array_shift($split);
236
237        $child       = \implode('/', $split);
238        $overwritten = false;
239
240        if (isset($this->offset[$memberSplit])) {
241            foreach ($this->offset[$memberSplit] as $key => $element) {
242                if ($element['child'] === $child) {
243                    $this->offset[$memberSplit][$key]['offset'] = $offset;
244                    $overwritten                                = true;
245
246                    break;
247                }
248            }
249        }
250
251        if (!$overwritten) {
252            $this->offset[$memberSplit][] = [
253                'child'  => \implode('/', $split),
254                'offset' => $offset,
255            ];
256        }
257
258        return $this;
259    }
260
261    /**
262     * Define the result limit
263     *
264     * @param int    $limit  Limit
265     * @param string $member Property name to limit by ('' = base model, anything else for relations such as hasMany relations)
266     *
267     * @return static
268     *
269     * @since 1.0.0
270     */
271    public function limit(int $limit = 0, string $member = '') : self
272    {
273        $split       = \explode('/', $member);
274        $memberSplit = \array_shift($split);
275
276        $child       = \implode('/', $split);
277        $overwritten = false;
278
279        if (isset($this->limit[$memberSplit])) {
280            foreach ($this->limit[$memberSplit] as $key => $element) {
281                if ($element['child'] === $child) {
282                    $this->limit[$memberSplit][$key]['limit'] = $limit;
283                    $overwritten                              = true;
284
285                    break;
286                }
287            }
288        }
289
290        if (!$overwritten) {
291            $this->limit[$memberSplit][] = [
292                'child' => \implode('/', $split),
293                'limit' => $limit,
294            ];
295        }
296
297        return $this;
298    }
299
300    /**
301     * Define the result filtering
302     *
303     * @param string $member    Property name to filter by
304     * @param mixed  $value     Filter value
305     * @param string $logic     Comparison logic (e.g. =, in, ...)
306     * @param string $connector Filter connector (e.g. AND, OR, ...)
307     *
308     * @return static
309     *
310     * @since 1.0.0
311     */
312    public function where(string $member, mixed $value, string $logic = '=', string $connector = 'AND') : self
313    {
314        $split       = \explode('/', $member);
315        $memberSplit = \array_shift($split);
316
317        $this->where[$memberSplit][] = [
318            'child'      => \implode('/', $split),
319            'value'      => $value,
320            'logic'      => $logic,
321            'comparison' => $connector,
322        ];
323
324        return $this;
325    }
326
327    /**
328     * Define the joining data
329     *
330     * @param string $member Property name to filter by
331     * @param string $mapper Mapper
332     * @param mixed  $value  Filter value
333     * @param string $logic  Comparison logic (e.g. =, in, ...)
334     * @param string $type   Join type (e.g. left, right, inner)
335     *
336     * @return static
337     *
338     * @since 1.0.0
339     */
340    public function join(string $member, string $mapper, mixed $value, string $logic = '=', string $type = JoinType::LEFT_JOIN) : self
341    {
342        $split       = \explode('/', $member);
343        $memberSplit = \array_shift($split);
344
345        $this->join[$memberSplit][] = [
346            'child'  => \implode('/', $split),
347            'mapper' => $mapper,
348            'value'  => $value,
349            'logic'  => $logic,
350            'type'   => $type,
351        ];
352
353        return $this;
354    }
355
356    /**
357     * Define the joining data
358     *
359     * @param string $member    Property name to filter by
360     * @param mixed  $value     Filter value
361     * @param string $logic     Comparison logic (e.g. =, in, ...)
362     * @param string $connector Filter connector (e.g. AND, OR, ...)
363     *
364     * @return self
365     *
366     * @since 1.0.0
367     */
368    public function on(string $member, mixed $value, string $logic = '=', string $connector = 'AND', string $relation = '') : self
369    {
370        $this->on[$relation][] = [
371            'child'      => '',
372            'member'     => $member,
373            'value'      => $value,
374            'logic'      => $logic,
375            'comparison' => $connector,
376        ];
377
378        return $this;
379    }
380
381    /**
382     * Define the joining data
383     *
384     * @param string $member Property name to filter by
385     * @param string $mapper Mapper
386     * @param mixed  $value  Filter value
387     * @param string $logic  Comparison logic (e.g. =, in, ...)
388     *
389     * @return static
390     *
391     * @since 1.0.0
392     */
393    public function leftJoin(string $member, string $mapper, mixed $value, string $logic = '=') : self
394    {
395        return $this->join($member, $mapper, $value, $logic, JoinType::LEFT_JOIN);
396    }
397
398    /**
399     * Define the joining data
400     *
401     * @param string $member Property name to filter by
402     * @param string $mapper Mapper
403     * @param mixed  $value  Filter value
404     * @param string $logic  Comparison logic (e.g. =, in, ...)
405     *
406     * @return static
407     *
408     * @since 1.0.0
409     */
410    public function rightJoin(string $member, string $mapper, mixed $value, string $logic = '=') : self
411    {
412        return $this->join($member, $mapper, $value, $logic, JoinType::RIGHT_JOIN);
413    }
414
415    /**
416     * Define the joining data
417     *
418     * @param string $member Property name to filter by
419     * @param string $mapper Mapper
420     * @param mixed  $value  Filter value
421     * @param string $logic  Comparison logic (e.g. =, in, ...)
422     *
423     * @return static
424     *
425     * @since 1.0.0
426     */
427    public function innerJoin(string $member, string $mapper, mixed $value, string $logic = '=') : self
428    {
429        return $this->join($member, $mapper, $value, $logic, JoinType::INNER_JOIN);
430    }
431
432    /**
433     * Populate a mapper (e.g. child mapper, relation mapper) based on the current mapper information.
434     *
435     * @param DataMapperAbstract $mapper Relation mapper to populate
436     * @param string             $member Relation property (e.g. ownsOne, hasMany, ... property name)
437     *
438     * @return self
439     *
440     * @since 1.0.0
441     */
442    public function createRelationMapper(self $mapper, string $member) : self
443    {
444        $relMapper = $mapper;
445
446        if (isset($this->with[$member])) {
447            foreach ($this->with[$member] as $with) {
448                if ($with['child'] === '') {
449                    continue;
450                }
451
452                $relMapper->with($with['child']);
453            }
454        }
455
456        if (isset($this->sort[$member])) {
457            foreach ($this->sort[$member] as $sort) {
458                // member = child element in this case
459                if ($member === '') {
460                    continue;
461                }
462
463                $relMapper->sort($sort['child'], $sort['order']);
464            }
465        }
466
467        if (isset($this->offset[$member])) {
468            foreach ($this->offset[$member] as $offset) {
469                if ($offset['child'] === '') {
470                    continue;
471                }
472
473                $relMapper->offset($offset['offset'], $offset['child']);
474            }
475        }
476
477        if (isset($this->limit[$member])) {
478            foreach ($this->limit[$member] as $limit) {
479                // member = child element in this case
480                if ($member === '') {
481                    continue;
482                }
483
484                $relMapper->limit($limit['limit'], $limit['child']);
485            }
486        }
487
488        if (isset($this->where[$member])) {
489            foreach ($this->where[$member] as $where) {
490                if ($where['child'] === '') {
491                    continue;
492                }
493
494                $relMapper->where($where['child'], $where['value'], $where['logic'], $where['comparison']);
495            }
496        }
497
498        if (isset($this->join[$member])) {
499            foreach ($this->join[$member] as $join) {
500                if ($join['child'] === '') {
501                    continue;
502                }
503
504                $relMapper->join($join['child'], $join['mapper'], $join['value'], $join['logic'], $join['type']);
505            }
506        }
507
508        if (isset($this->on[$member])) {
509            foreach ($this->on[$member] as $on) {
510                if ($on['child'] === '') {
511                    continue;
512                }
513
514                $relMapper->on($on['child'], $on['value'], $on['logic'], $on['comparison'], $on['field']);
515            }
516        }
517
518        return $relMapper;
519    }
520
521    /**
522     * Parse value
523     *
524     * @param string $type  Value type
525     * @param mixed  $value Value to parse
526     *
527     * @return mixed
528     *
529     * @since 1.0.0
530     */
531    public function parseValue(string $type, mixed $value = null) : mixed
532    {
533        if ($value === null) {
534            return null;
535        } elseif ($type === 'int') {
536            return (int) $value;
537        } elseif ($type === 'string') {
538            return (string) $value;
539        } elseif ($type === 'float') {
540            return (float) $value;
541        } elseif ($type === 'bool') {
542            return (bool) $value;
543        } elseif ($type === 'DateTime' || $type === 'DateTimeImmutable') {
544            return $value === null ? null : $value->format($this->mapper::$datetimeFormat);
545        } elseif ($type === 'Json') {
546            return (string) \json_encode($value);
547        } elseif ($type === 'compress') {
548            return (string) \gzdeflate($value);
549        } elseif ($type === 'Serializable') {
550            return $value->serialize();
551        } elseif (\is_object($value) && \method_exists($value, 'getId')) {
552            return $value->getId();
553        }
554
555        return $value;
556    }
557
558    /**
559     * Execute mapper
560     *
561     * @param mixed ...$options Data for the mapper
562     *
563     * @return mixed
564     *
565     * @since 1.0.0
566     */
567    abstract public function execute(mixed ...$options) : mixed;
568}