Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
40.00% covered (danger)
40.00%
66 / 165
61.90% covered (warning)
61.90%
13 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
DataMapperFactory
40.00% covered (danger)
40.00%
66 / 165
61.90% covered (warning)
61.90%
13 / 21
1006.90
0.00% covered (danger)
0.00%
0 / 1
 __construct
n/a
0 / 0
n/a
0 / 0
1
 __clone
n/a
0 / 0
n/a
0 / 0
1
 db
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 reader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getRaw
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getRandom
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 count
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getQuery
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAll
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 writer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 updater
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 update
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 remover
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 delete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isNullModel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 createNullModel
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 createBaseModel
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 getObjectId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setObjectId
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 getColumnByMember
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 find
27.42% covered (danger)
27.42%
34 / 124
0.00% covered (danger)
0.00%
0 / 1
531.53
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\OrderType;
20use phpOMS\DataStorage\Database\Query\Where;
21
22/**
23 * Mapper factory.
24 *
25 * @package phpOMS\DataStorage\Database\Mapper
26 * @license OMS License 2.0
27 * @link    https://jingga.app
28 * @since   1.0.0
29 *
30 * @template T
31 */
32class DataMapperFactory
33{
34    /**
35     * Datetime format of the database datetime
36     *
37     * This is only for the datetime stored in the database not the generated query.
38     * For the query check the datetime in Grammar:$datetimeFormat
39     *
40     * @var string
41     * @since 1.0.0
42     */
43    public static string $datetimeFormat = 'Y-m-d H:i:s';
44
45    /**
46     * Primary field name.
47     *
48     * @var string
49     * @since 1.0.0
50     */
51    public const PRIMARYFIELD = '';
52
53    /**
54     * Autoincrement primary field.
55     *
56     * @var bool
57     * @since 1.0.0
58     */
59    public const AUTOINCREMENT = true;
60
61    /**
62     * Primary field name.
63     *
64     * @var string
65     * @since 1.0.0
66     */
67    public const CREATED_AT = '';
68
69    /**
70     * Columns.
71     *
72     * @var array<string, array{name:string, type:string, internal:string, autocomplete?:bool, readonly?:bool, writeonly?:bool, annotations?:array}>
73     * @since 1.0.0
74     */
75    public const COLUMNS = [];
76
77    /**
78     * Has many relation.
79     *
80     * @var array<string, array>
81     * @since 1.0.0
82     */
83    public const HAS_MANY = [];
84
85    /**
86     * Relations.
87     *
88     * Relation is defined in current mapper
89     *
90     * @var array<string, array{mapper:class-string, external:string, by?:string, column?:string, conditional?:bool}>
91     * @since 1.0.0
92     */
93    public const OWNS_ONE = [];
94
95    /**
96     * Belongs to.
97     *
98     * @var array<string, array{mapper:class-string, external:string, column?:string, by?:string}>
99     * @since 1.0.0
100     */
101    public const BELONGS_TO = [];
102
103    /**
104     * Table.
105     *
106     * @var string
107     * @since 1.0.0
108     */
109    public const TABLE = '';
110
111    /**
112     * Parent column.
113     *
114     * @var class-string
115     * @since 1.0.0
116     */
117    public const PARENT = '';
118
119    /**
120     * Model to use by the mapper.
121     *
122     * @var class-string<T>
123     * @since 1.0.0
124     */
125    public const MODEL = '';
126
127    /**
128     * Model factory to use by the mapper.
129     *
130     * @var class-string
131     * @since 1.0.0
132     */
133    public const FACTORY = '';
134
135    /**
136     * Database connection.
137     *
138     * @var ConnectionAbstract
139     * @since 1.0.0
140     */
141    protected static ConnectionAbstract $db;
142
143    /**
144     * Constructor.
145     *
146     * @since 1.0.0
147     * @codeCoverageIgnore
148     */
149    final private function __construct()
150    {
151    }
152
153    /**
154     * Clone.
155     *
156     * @return void
157     *
158     * @since 1.0.0
159     * @codeCoverageIgnore
160     */
161    private function __clone()
162    {
163    }
164
165    /**
166     * Set default database connection
167     *
168     * @param ConnectionAbstract $db Database connection
169     *
170     * @return class-string<self>
171     *
172     * @since 1.0.0
173     */
174    public static function db(ConnectionAbstract $db) : string
175    {
176        self::$db = $db;
177
178        return static::class;
179    }
180
181    /**
182     * Create read mapper
183     *
184     * @param ConnectionAbstract $db Database connection
185     *
186     * @return ReadMapper
187     *
188     * @since 1.0.0
189     */
190    public static function reader(ConnectionAbstract $db = null) : ReadMapper
191    {
192        return new ReadMapper(new static(), $db ?? self::$db);
193    }
194
195    /**
196     * Create read mapper
197     *
198     * @param ConnectionAbstract $db Database connection
199     *
200     * @return ReadMapper<T>
201     *
202     * @since 1.0.0
203     */
204    public static function get(ConnectionAbstract $db = null) : ReadMapper
205    {
206        /** @var ReadMapper<T> $reader */
207        $reader = new ReadMapper(new static(), $db ?? self::$db);
208
209        return $reader->get();
210    }
211
212    /**
213     * Create read mapper
214     *
215     * @param ConnectionAbstract $db Database connection
216     *
217     * @return ReadMapper
218     *
219     * @since 1.0.0
220     */
221    public static function getRaw(ConnectionAbstract $db = null) : ReadMapper
222    {
223        /** @var ReadMapper<T> $reader */
224        $reader = new ReadMapper(new static(), $db ?? self::$db);
225
226        return $reader->getRaw();
227    }
228
229    /**
230     * Create read mapper
231     *
232     * @param ConnectionAbstract $db Database connection
233     *
234     * @return ReadMapper
235     *
236     * @since 1.0.0
237     */
238    public static function getRandom(ConnectionAbstract $db = null) : ReadMapper
239    {
240        return (new ReadMapper(new static(), $db ?? self::$db))->getRandom();
241    }
242
243    /**
244     * Create read mapper
245     *
246     * @param ConnectionAbstract $db Database connection
247     *
248     * @return ReadMapper
249     *
250     * @since 1.0.0
251     */
252    public static function count(ConnectionAbstract $db = null) : ReadMapper
253    {
254        return (new ReadMapper(new static(), $db ?? self::$db))->count();
255    }
256
257    /**
258     * Create read mapper
259     *
260     * @param ConnectionAbstract $db Database connection
261     *
262     * @return Builder
263     *
264     * @since 1.0.0
265     */
266    public static function getQuery(ConnectionAbstract $db = null) : Builder
267    {
268        return (new ReadMapper(new static(), $db ?? self::$db))->getQuery();
269    }
270
271    /**
272     * Create read mapper
273     *
274     * @param ConnectionAbstract $db Database connection
275     *
276     * @return ReadMapper
277     *
278     * @since 1.0.0
279     */
280    public static function getAll(ConnectionAbstract $db = null) : ReadMapper
281    {
282        /** @var ReadMapper<T> $reader */
283        $reader = new ReadMapper(new static(), $db ?? self::$db);
284
285        return $reader->getAll();
286    }
287
288    /**
289     * Create write mapper
290     *
291     * @param ConnectionAbstract $db Database connection
292     *
293     * @return WriteMapper
294     *
295     * @since 1.0.0
296     */
297    public static function writer(ConnectionAbstract $db = null) : WriteMapper
298    {
299        return new WriteMapper(new static(), $db ?? self::$db);
300    }
301
302    /**
303     * Create write mapper
304     *
305     * @param ConnectionAbstract $db Database connection
306     *
307     * @return WriteMapper
308     *
309     * @since 1.0.0
310     */
311    public static function create(ConnectionAbstract $db = null) : WriteMapper
312    {
313        return (new WriteMapper(new static(), $db ?? self::$db))->create();
314    }
315
316    /**
317     * Create update mapper
318     *
319     * @param ConnectionAbstract $db Database connection
320     *
321     * @return UpdateMapper
322     *
323     * @since 1.0.0
324     */
325    public static function updater(ConnectionAbstract $db = null) : UpdateMapper
326    {
327        return new UpdateMapper(new static(), $db ?? self::$db);
328    }
329
330    /**
331     * Create update mapper
332     *
333     * @param ConnectionAbstract $db Database connection
334     *
335     * @return UpdateMapper
336     *
337     * @since 1.0.0
338     */
339    public static function update(ConnectionAbstract $db = null) : UpdateMapper
340    {
341        return (new UpdateMapper(new static(), $db ?? self::$db))->update();
342    }
343
344    /**
345     * Create delete mapper
346     *
347     * @param ConnectionAbstract $db Database connection
348     *
349     * @return DeleteMapper
350     *
351     * @since 1.0.0
352     */
353    public static function remover(ConnectionAbstract $db = null) : DeleteMapper
354    {
355        return new DeleteMapper(new static(), $db ?? self::$db);
356    }
357
358    /**
359     * Create delete mapper
360     *
361     * @param ConnectionAbstract $db Database connection
362     *
363     * @return DeleteMapper
364     *
365     * @since 1.0.0
366     */
367    public static function delete(ConnectionAbstract $db = null) : DeleteMapper
368    {
369        return (new DeleteMapper(new static(), $db ?? self::$db))->delete();
370    }
371
372    /**
373     * Test if object is null object
374     *
375     * @param mixed $obj Object to check
376     *
377     * @return bool
378     *
379     * @since 1.0.0
380     */
381    public static function isNullModel(mixed $obj) : bool
382    {
383        return \is_object($obj) && \strpos(\get_class($obj), '\Null') !== false;
384    }
385
386    /**
387     * Creates the current null object
388     *
389     * @param mixed $id Model id
390     *
391     * @return mixed
392     *
393     * @since 1.0.0
394     */
395    public static function createNullModel(mixed $id = null) : mixed
396    {
397        $class     = empty(static::MODEL) ? \substr(static::class, 0, -6) : static::MODEL;
398        $parts     = \explode('\\', $class);
399        $name      = $parts[$c = (\count($parts) - 1)];
400        $parts[$c] = 'Null' . $name;
401        $class     = \implode('\\', $parts);
402
403        return $id !== null ? new $class($id) : new $class();
404    }
405
406    /**
407     * Create the empty base model
408     *
409     * @param null|array $data Data to use for initialization
410     *
411     * @return object
412     *
413     * @since 1.0.0
414     */
415    public static function createBaseModel(array $data = null) : object
416    {
417        if (empty(static::FACTORY)) {
418            $class = empty(static::MODEL) ? \substr(static::class, 0, -6) : static::MODEL;
419
420            return new $class();
421        }
422
423        return static::FACTORY::createWith($data);
424    }
425
426    /**
427     * Get id of object
428     *
429     * @param object $obj    Model to create
430     * @param string $member Member name for the id, if it is not the primary key
431     *
432     * @return mixed
433     *
434     * @since 1.0.0
435     */
436    public static function getObjectId(object $obj, string $member = null) : mixed
437    {
438        $propertyName = $member ?? static::COLUMNS[static::PRIMARYFIELD]['internal'];
439
440        return $obj->{$propertyName};
441    }
442
443    /**
444     * Set id to model
445     *
446     * @param \ReflectionClass $refClass Reflection class
447     * @param object           $obj      Object to create
448     * @param mixed            $objId    Id to set
449     *
450     * @return void
451     *
452     * @since 1.0.0
453     */
454    public static function setObjectId(\ReflectionClass $refClass, object $obj, mixed $objId) : void
455    {
456        $propertyName = static::COLUMNS[static::PRIMARYFIELD]['internal'];
457        $refProp      = $refClass->getProperty($propertyName);
458
459        \settype($objId, static::COLUMNS[static::PRIMARYFIELD]['type']);
460        if (!$refProp->isPublic()) {
461            $refProp->setValue($obj, $objId);
462        } else {
463            $obj->{$propertyName} = $objId;
464        }
465    }
466
467    /**
468     * Find database column name by member name
469     *
470     * @param string $name member name
471     *
472     * @return null|string
473     *
474     * @since 1.0.0
475     */
476    public static function getColumnByMember(string $name) : ?string
477    {
478        foreach (static::COLUMNS as $cName => $column) {
479            if ($column['internal'] === $name) {
480                return $cName;
481            }
482        }
483
484        return null;
485    }
486
487    /**
488     * Find data.
489     *
490     * @param string             $search       Search string
491     * @param DataMapperAbstract $mapper       Mapper to populate
492     * @param int                $id           Pivot element id
493     * @param string             $secondaryId  secondary id which becomes necessary for sorted results
494     * @param string             $type         Page type (p = get previous elements, n = get next elements)
495     * @param int                $pageLimit    Limit result set
496     * @param string             $sortBy       Model member name to sort by
497     * @param string             $sortOrder    Sort order
498     * @param array              $searchFields Fields to search in. ([] = all) @todo: maybe change to all which have autocomplete = true defined?
499     * @param array              $filters      Additional search filters applied ['type', 'value1', 'logic1', 'value2', 'logic2']
500     *
501     * @return array{hasPrevious:bool, hasNext:bool, data:object[]}
502     *
503     * @since 1.0.0
504     */
505    public static function find(
506        string $search = null,
507        DataMapperAbstract $mapper = null,
508        int $id = 0,
509        string $secondaryId = '',
510        string $type = null,
511        int $pageLimit = 25,
512        string $sortBy = null,
513        string $sortOrder = OrderType::DESC,
514        array $searchFields = [],
515        array $filters = []
516    ) : array {
517        $mapper  ??= static::getAll();
518        $sortOrder = \strtoupper($sortOrder);
519
520        $data = [];
521
522        $type        = $id === 0 ? null : $type;
523        $hasPrevious = false;
524        $hasNext     = false;
525
526        $primarySortField = static::COLUMNS[static::PRIMARYFIELD]['internal'];
527
528        $sortBy = empty($sortBy) || static::getColumnByMember($sortBy) === null ? $primarySortField : $sortBy;
529
530        $sortById    = $sortBy === $primarySortField;
531        $secondaryId = $sortById ? $id : $secondaryId;
532
533        foreach ($filters as $key => $filter) {
534            $mapper->where($key, '%' . $filter['value1'] . '%', $filter['logic1'] ?? 'like');
535
536            if (!empty($filter['value2'])) {
537                $mapper->where($key, '%' . $filter['value2'] . '%', $filter['logic2'] ?? 'like');
538            }
539        }
540
541        if (!empty($search)) {
542            $where   = new Where(static::$db);
543            $counter = 0;
544
545            if (empty($searchFields)) {
546                foreach (static::COLUMNS as $column) {
547                    $searchFields[] = $column['internal'];
548                }
549            }
550
551            foreach ($searchFields as $searchField) {
552                if (($column = static::getColumnByMember($searchField)) === null) {
553                    continue;
554                }
555
556                $where->where($column, 'like', '%' . $search . '%', 'OR');
557                ++$counter;
558            }
559
560            if ($counter > 0) {
561                $mapper->where('', $where);
562            }
563        }
564
565        // @todo: how to handle columns which are NOT members (columns which are manipulated)
566        //          Maybe pass callback array which can handle these cases?
567
568        if ($type === 'p') {
569            $cloned = clone $mapper;
570            $mapper->sort(
571                    $sortBy,
572                    $sortOrder === OrderType::DESC ? OrderType::ASC : OrderType::DESC
573                )
574                ->where($sortBy, $secondaryId, $sortOrder === OrderType::DESC ? '>=' : '<=')
575                ->limit($pageLimit + 2);
576
577            if (!$sortById) {
578                $where = new Where(static::$db);
579                $where->where(static::PRIMARYFIELD, '>=', $id)
580                    ->orWhere(
581                        static::getColumnByMember($sortBy),
582                        $sortOrder === OrderType::DESC ? '>' : '<',
583                        $secondaryId
584                    );
585
586                $mapper->where('', $where)
587                    ->sort($primarySortField, OrderType::ASC);
588            }
589
590            $data = $mapper->execute();
591
592            if (($count = \count($data)) < 2) {
593                $cloned->sort($sortBy, $sortOrder)
594                    ->limit($pageLimit + 1);
595
596                if (!$sortById) {
597                    $where = new Where(static::$db);
598                    $where->where(static::PRIMARYFIELD, '<=', $id)
599                        ->orWhere(
600                            static::getColumnByMember($sortBy),
601                            $sortOrder === OrderType::DESC ? '<' : '>',
602                            $secondaryId
603                        );
604
605                    $cloned->where('', $where)
606                        ->sort($primarySortField, OrderType::DESC);
607                }
608
609                $data = $cloned->execute();
610
611                $hasNext = $count > $pageLimit;
612                if ($hasNext) {
613                    \array_pop($data);
614                    --$count;
615                }
616            } else {
617                if (\reset($data)->getId() === $id) {
618                    \array_shift($data);
619                    $hasNext = true;
620                    --$count;
621                }
622
623                if ($count > $pageLimit) {
624                    if (!$hasNext) { // @todo: can be maybe removed?
625                        \array_pop($data);
626                        $hasNext = true;
627                        --$count;
628                    }
629
630                    if ($count > $pageLimit) {
631                        $hasPrevious = true;
632                        \array_pop($data);
633                    }
634                }
635
636                $data = \array_reverse($data);
637            }
638        } elseif ($type === 'n') {
639            $mapper = $mapper->sort($sortBy, $sortOrder)
640                ->where($sortBy, $secondaryId, $sortOrder === OrderType::DESC ? '<=' : '>=')
641                ->limit($pageLimit + 2);
642
643            if (!$sortById) {
644                $where = new Where(static::$db);
645                $where->where(static::PRIMARYFIELD, '<=', $id)
646                    ->orWhere(
647                        static::getColumnByMember($sortBy),
648                        $sortOrder === OrderType::DESC ? '<' : '>',
649                        $secondaryId
650                    );
651
652                $mapper = $mapper
653                    ->where('', $where)
654                    ->sort($primarySortField, OrderType::DESC);
655            }
656
657            $data  = $mapper->execute();
658            $count = \count($data);
659
660            if ($count < 1) {
661                return [
662                    'hasPrevious' => false,
663                    'hasNext'     => false,
664                    'data'        => [],
665                ];
666            }
667
668            if (\reset($data)->getId() === $id) {
669                \array_shift($data);
670                $hasPrevious = true;
671                --$count;
672            }
673
674            if ($count > $pageLimit) {
675                \array_pop($data);
676                $hasNext = true;
677                --$count;
678            }
679
680            if ($count > $pageLimit) {
681                \array_pop($data);
682                --$count;
683            }
684        } else {
685            $mapper->sort($sortBy, $sortOrder)
686                ->limit($pageLimit + 1);
687
688            if (!$sortById) {
689                $mapper->sort($primarySortField, OrderType::DESC);
690            }
691
692            $data = $mapper->execute();
693
694            $hasNext = ($count = \count($data)) > $pageLimit;
695            if ($hasNext) {
696                \array_pop($data);
697            }
698        }
699
700        return [
701            'hasPrevious' => $hasPrevious,
702            'hasNext'     => $hasNext,
703            'data'        => $data,
704        ];
705    }
706}