DataMapper::hydrate()   A
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 27
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 16
c 1
b 0
f 0
nc 5
nop 0
dl 0
loc 27
rs 9.4222
1
<?php
2
3
/**
4
 * Platine ORM
5
 *
6
 * Platine ORM provides a flexible and powerful ORM implementing a data-mapper pattern.
7
 *
8
 * This content is released under the MIT License (MIT)
9
 *
10
 * Copyright (c) 2020 Platine ORM
11
 *
12
 * Permission is hereby granted, free of charge, to any person obtaining a copy
13
 * of this software and associated documentation files (the "Software"), to deal
14
 * in the Software without restriction, including without limitation the rights
15
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
 * copies of the Software, and to permit persons to whom the Software is
17
 * furnished to do so, subject to the following conditions:
18
 *
19
 * The above copyright notice and this permission notice shall be included in all
20
 * copies or substantial portions of the Software.
21
 *
22
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
 * SOFTWARE.
29
 */
30
31
/**
32
 *  @file DataMapper.php
33
 *
34
 *  The Data Mapper class
35
 *
36
 *  @package    Platine\Orm\Mapper
37
 *  @author Platine Developers Team
38
 *  @copyright  Copyright (c) 2020
39
 *  @license    http://opensource.org/licenses/MIT  MIT License
40
 *  @link   https://www.platine-php.com
41
 *  @version 1.0.0
42
 *  @filesource
43
 */
44
45
declare(strict_types=1);
46
47
namespace Platine\Orm\Mapper;
48
49
use DateTime;
50
use Platine\Database\Query\Select;
51
use Platine\Orm\Entity;
52
use Platine\Orm\EntityManager;
53
use Platine\Orm\Exception\EntityStateException;
54
use Platine\Orm\Exception\PropertyNotFoundException;
55
use Platine\Orm\Exception\RelationNotFoundException;
56
use Platine\Orm\Mapper\DataMapperInterface;
57
use Platine\Orm\Mapper\EntityMapper;
58
use Platine\Orm\Relation\BelongsTo;
59
use Platine\Orm\Relation\HasRelation;
60
use Platine\Orm\Relation\ShareRelation;
61
use RuntimeException;
62
63
/**
64
 * @class DataMapper
65
 * @package Platine\Orm\Mapper
66
 * @template TEntity as Entity
67
 * @implements DataMapperInterface<TEntity>
68
 */
69
class DataMapper implements DataMapperInterface
70
{
71
    /**
72
     * The raw columns data
73
     * @var array<string, mixed>
74
     */
75
    protected array $rawColumns = [];
76
77
    /**
78
     * The current columns data. After refresh, etc.
79
     * @var array<string, mixed>
80
     */
81
    protected array $columns = [];
82
83
    /**
84
     * The list of relation loaders
85
     * @var array<string, \Platine\Orm\Relation\RelationLoader<TEntity>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string, \Platine\O...elationLoader<TEntity>> at position 4 could not be parsed: Expected '>' at position 4, but found '\Platine\Orm\Relation\RelationLoader'.
Loading history...
86
     */
87
    protected array $loaders = [];
88
89
    /**
90
     * The Entity manager instance
91
     * @var EntityManager<TEntity>
92
     */
93
    protected EntityManager $manager;
94
95
    /**
96
     *
97
     * @var EntityMapper<TEntity>
98
     */
99
    protected EntityMapper $mapper;
100
101
    /**
102
     * Whether the data is read only
103
     * @var bool
104
     */
105
    protected bool $isReadOnly = false;
106
107
    /**
108
     * Whether the data is new
109
     * @var bool
110
     */
111
    protected bool $isNew = false;
112
113
    /**
114
     * Whether the data is deleted
115
     * @var bool
116
     */
117
    protected bool $deleted = false;
118
119
    /**
120
     * Whether the data need to refreshed from data store
121
     * @var bool
122
     */
123
    protected bool $refresh = false;
124
125
    /**
126
     * The name of the sequence to use
127
     * @var string|null
128
     */
129
    protected ?string $sequence = null;
130
131
    /**
132
     * The list of modified data
133
     * @var array<string, mixed>
134
     */
135
    protected array $modified = [];
136
137
    /**
138
     * The list of relations loaded data (cached)
139
     * @var array<string, mixed>
140
     */
141
    protected array $relations = [];
142
143
    /**
144
     * The list of pending links
145
     * @var array<int, array<string, mixed>>
146
     */
147
    protected array $pendingLinks = [];
148
149
    /**
150
     * Create new instance
151
     * @param EntityManager<TEntity> $manager
152
     * @param EntityMapper<TEntity> $mapper
153
     * @param array<string, mixed> $columns
154
     * @param array<string, mixed> $loaders
155
     * @param bool $isReadOnly
156
     * @param bool $isNew
157
     */
158
    public function __construct(
159
        EntityManager $manager,
160
        EntityMapper $mapper,
161
        array $columns,
162
        array $loaders,
163
        bool $isReadOnly,
164
        bool $isNew
165
    ) {
166
        $this->manager = $manager;
167
        $this->mapper = $mapper;
168
        $this->loaders = $loaders;
169
        $this->isReadOnly = $isReadOnly;
170
        $this->isNew = $isNew;
171
        $this->rawColumns = $columns;
172
173
        if ($isNew && count($columns) > 0) {
174
            $this->rawColumns = [];
175
            $this->fill($columns);
176
        }
177
    }
178
179
    /**
180
     *
181
     * @return EntityManager<TEntity>
182
     */
183
    public function getEntityManager(): EntityManager
184
    {
185
        return $this->manager;
186
    }
187
188
    /**
189
     *
190
     * @return EntityMapper<TEntity>
191
     */
192
    public function getEntityMapper(): EntityMapper
193
    {
194
        return $this->mapper;
195
    }
196
197
    /**
198
     * {@inheritedoc}
199
     */
200
    public function isDeleted(): bool
201
    {
202
        return $this->deleted;
203
    }
204
205
    /**
206
     * {@inheritedoc}
207
     */
208
    public function isNew(): bool
209
    {
210
        return $this->isNew;
211
    }
212
213
    /**
214
     * {@inheritedoc}
215
     */
216
    public function isReadOnly(): bool
217
    {
218
        return $this->isReadOnly;
219
    }
220
221
    /**
222
     * {@inheritedoc}
223
     */
224
    public function wasModified(): bool
225
    {
226
        return count($this->modified) > 0 || count($this->pendingLinks) > 0;
227
    }
228
229
    /**
230
     * {@inheritedoc}
231
     */
232
    public function getColumn(string $name): mixed
233
    {
234
        if ($this->refresh) {
235
            $this->hydrate();
236
        }
237
238
        if ($this->deleted) {
239
            throw new EntityStateException('The record was deleted');
240
        }
241
242
        if (array_key_exists($name, $this->columns)) {
243
            return $this->columns[$name];
244
        }
245
246
        if (!array_key_exists($name, $this->rawColumns)) {
247
            throw new PropertyNotFoundException(sprintf(
248
                'Unknown column [%s]',
249
                $name
250
            ));
251
        }
252
253
        $value = $this->rawColumns[$name];
254
        $casts = $this->mapper->getCasts();
255
256
        if (isset($casts[$name])) {
257
            $value = $this->castGet($value, $casts[$name]);
258
        }
259
260
        $primaryKey = $this->mapper->getPrimaryKey();
261
        if ($name === (string)$primaryKey) {
262
            return $this->columns[$name] = $value;
263
        }
264
265
        $getters = $this->mapper->getGetters();
266
        if (isset($getters[$name])) {
267
            $callback = $getters[$name];
268
            $value = ($callback)($value, $this);
269
        }
270
271
        return $this->columns[$name] = $value;
272
    }
273
274
    /**
275
     * {@inheritedoc}
276
     */
277
    public function setColumn(string $name, mixed $value): void
278
    {
279
        if ($this->isReadOnly) {
280
            throw new EntityStateException('The record is readonly');
281
        }
282
283
        if ($this->deleted) {
284
            throw new EntityStateException('The record was deleted');
285
        }
286
287
        if ($this->refresh) {
288
            $this->hydrate();
289
        }
290
291
        $casts = $this->mapper->getCasts();
292
        $setters = $this->mapper->getSetters();
293
294
        if (isset($setters[$name])) {
295
            $callback = $setters[$name];
296
            $value = ($callback)($value, $this);
297
        }
298
299
        if (isset($casts[$name])) {
300
            $value = $this->castSet($value, $casts[$name]);
301
        }
302
303
        $this->modified[$name] = true;
304
        unset($this->columns[$name]);
305
        $this->rawColumns[$name] = $value;
306
    }
307
308
    /**
309
     * {@inheritedoc}
310
     */
311
    public function clearColumn(string $name, bool $raw = false): void
312
    {
313
        unset($this->columns[$name]);
314
315
        if ($raw) {
316
            unset($this->rawColumns[$name]);
317
        }
318
    }
319
320
     /**
321
     * {@inheritedoc}
322
     */
323
    public function hasColumn(string $column): bool
324
    {
325
        return array_key_exists($column, $this->columns)
326
                || array_key_exists($column, $this->rawColumns);
327
    }
328
329
    /**
330
     * {@inheritedoc}
331
     */
332
    public function getModifiedColumns(): array
333
    {
334
        return array_keys($this->modified);
335
    }
336
337
    /**
338
     * {@inheritedoc}
339
     */
340
    public function getRawColumns(): array
341
    {
342
        return $this->rawColumns;
343
    }
344
345
    /**
346
     * {@inheritedoc}
347
     */
348
    public function setRawColumn(string $name, mixed $value): void
349
    {
350
        $this->modified[$name] = true;
351
        unset($this->columns[$name]);
352
        $this->rawColumns[$name] = $value;
353
    }
354
355
     /**
356
     * {@inheritedoc}
357
     */
358
    public function getRelated(string $name, callable $callback = null): mixed
359
    {
360
        if (array_key_exists($name, $this->relations)) {
361
            return $this->relations[$name];
362
        }
363
364
        /** @var array<string, \Platine\Orm\Relation\Relation<TEntity>> $relations */
365
        $relations = $this->mapper->getRelations();
366
367
        $cacheKey = $name;
368
369
        $index = strpos($name, ':');
370
        if ($index !== false) {
371
            $name = substr($name, $index + 1);
372
        }
373
374
        if (!isset($relations[$name])) {
375
            throw new RelationNotFoundException(sprintf(
376
                'Unknown relation [%s]',
377
                $name
378
            ));
379
        }
380
381
        $this->hydrate();
382
383
        //Race condition
384
        //@codeCoverageIgnoreStart
385
        if (isset($this->relations[$cacheKey])) {
386
            return $this->relations[$cacheKey];
387
        }
388
        //@codeCoverageIgnoreEnd
389
390
        if (isset($this->loaders[$cacheKey])) {
391
            return $this->relations[$cacheKey] = $this->loaders[$name]->getResult($this);
392
        }
393
394
        return $this->relations[$cacheKey] = $relations[$name]->getResult($this, $callback);
395
    }
396
397
    /**
398
     * {@inheritedoc}
399
     */
400
    public function setRelated(string $name, ?Entity $entity = null): void
401
    {
402
        $relations = $this->mapper->getRelations();
403
404
        if (!isset($relations[$name])) {
405
            throw new RelationNotFoundException(sprintf(
406
                'Unknown relation [%s]',
407
                $name
408
            ));
409
        }
410
411
        /** @var \Platine\Orm\Relation\Relation<TEntity> $rel */
412
        $rel = $relations[$name];
413
414
        if (!($rel instanceof BelongsTo) && !($rel instanceof HasRelation)) {
415
            throw new RuntimeException('Unsupported relation type');
416
        }
417
418
        if ($entity === null && !($rel instanceof BelongsTo)) {
419
            throw new RuntimeException('Unsupported relation type');
420
        }
421
422
        $rel->addRelatedEntity($this, $entity);
0 ignored issues
show
Bug introduced by
It seems like $entity can also be of type null; however, parameter $entity of Platine\Orm\Relation\Has...ion::addRelatedEntity() does only seem to accept Platine\Orm\Entity, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

422
        $rel->addRelatedEntity($this, /** @scrutinizer ignore-type */ $entity);
Loading history...
423
    }
424
425
    /**
426
     * {@inheritedoc}
427
     */
428
    public function clearRelated(string $name, bool $loaders = false): void
429
    {
430
        $cacheKey = $name;
431
432
        $index = strpos($name, ':');
433
        if ($index !== false) {
434
            $name = substr($name, $index + 1);
435
        }
436
437
        unset($this->relations[$cacheKey]);
438
439
        if ($loaders) {
440
            unset($this->loaders[$name]);
441
        }
442
    }
443
444
    /**
445
     * {@inheritedoc}
446
     */
447
    public function hasRelation(string $relation): bool
448
    {
449
        $relations = $this->mapper->getRelations();
450
451
        return isset($relations[$relation]);
452
    }
453
454
    /**
455
     * {@inheritedoc}
456
     */
457
    public function link(string $relation, Entity $entity): void
458
    {
459
        $this->setLink($relation, $entity, true);
460
    }
461
462
    /**
463
     * {@inheritedoc}
464
     */
465
    public function unlink(string $relation, Entity $entity): void
466
    {
467
        $this->setLink($relation, $entity, false);
468
    }
469
470
    /**
471
     * {@inheritedoc}
472
     */
473
    public function refresh(): void
474
    {
475
        $this->refresh = true;
476
    }
477
478
    /**
479
     * {@inheritedoc}
480
     */
481
    public function fill(array $columns): void
482
    {
483
        $fillable = $this->mapper->getFillable();
484
        $guarded = $this->mapper->getGuarded();
485
486
        if (!empty($fillable)) {
487
            $columns = array_intersect_key($columns, array_flip($fillable));
488
        } elseif (!empty($guarded)) {
489
            $columns = array_diff_key($columns, array_flip($guarded));
490
        }
491
492
        foreach ($columns as $name => $value) {
493
            $this->setColumn($name, $value);
494
        }
495
    }
496
497
    /**
498
     * Mark the entity as saved
499
     * @param mixed $id
500
     * @return bool
501
     */
502
    public function markAsSaved(mixed $id): bool
503
    {
504
        $primaryKey = $this->mapper->getPrimaryKey();
505
        if (!$primaryKey->isComposite()) {
506
            $columns = $primaryKey->columns();
507
            $this->rawColumns[$columns[0]] = $id;
508
        } else {
509
            foreach ($primaryKey->columns() as $pkColumn) {
510
                $this->rawColumns[$pkColumn] = $id[$pkColumn];
511
            }
512
        }
513
514
        $this->refresh = true;
515
        $this->isNew = false;
516
        $this->modified = [];
517
518
        if (!empty($this->pendingLinks)) {
519
            $this->executePendingLinkage();
520
        }
521
522
        return true;
523
    }
524
525
    /**
526
     * Mark the entity as updated
527
     * @param string|null $updatedAt
528
     * @return bool
529
     */
530
    public function markAsUpdated(?string $updatedAt = null): bool
531
    {
532
        if ($updatedAt !== null) {
533
            list(, $column) = $this->mapper->getTimestampColumns();
534
            unset($this->columns[$column]);
535
            $this->rawColumns[$column] = $updatedAt;
536
        }
537
538
        $this->modified = [];
539
540
        if (!empty($this->pendingLinks)) {
541
            $this->executePendingLinkage();
542
        }
543
544
        return true;
545
    }
546
547
    /**
548
     * Mark the entity as deleted
549
     * @return bool
550
     */
551
    public function markAsDeleted(): bool
552
    {
553
        return $this->deleted = true;
554
    }
555
556
    /**
557
     * Execute the pending links
558
     * @return void
559
     */
560
    public function executePendingLinkage(): void
561
    {
562
        foreach ($this->pendingLinks as $item) {
563
            /** @var \Platine\Orm\Relation\ShareOne<TEntity>|\Platine\Orm\Relation\ShareMany<TEntity> $rel */
564
            $rel = $item['relation'];
565
566
            if (isset($item['link'])) {
567
                if ($item['link'] === true) {
568
                    $rel->link($this, $item['entity']);
569
                } else {
570
                    $rel->unlink($this, $item['entity']);
571
                }
572
            }
573
        }
574
575
        $this->pendingLinks = [];
576
    }
577
578
    /**
579
     * Get fresh data from data store
580
     * @return void
581
     */
582
    protected function hydrate(): void
583
    {
584
        if ($this->refresh === false) {
585
            return;
586
        }
587
588
        $select = new Select($this->manager->getConnection(), $this->mapper->getTable());
589
590
        $primaryKeys = $this->mapper->getPrimaryKey()->getValue($this->rawColumns, true);
591
        if (is_array($primaryKeys)) {
592
            foreach ($primaryKeys as $pkColumn => $pkValue) {
593
                $select->where($pkColumn)->is($pkValue);
594
            }
595
        }
596
597
        $columns = $select->select()->fetchAssoc()->get();
598
599
        if ($columns === false) {
600
            $this->deleted = true;
601
            return;
602
        }
603
604
        $this->rawColumns = $columns;
605
        $this->columns = [];
606
        $this->relations = [];
607
        $this->loaders = [];
608
        $this->refresh = false;
609
    }
610
611
    /**
612
     * Cast the value to the given type for get
613
     * @param mixed $value
614
     * @param string $type
615
     *
616
     * @return mixed
617
     */
618
    protected function castGet(mixed $value, string $type): mixed
619
    {
620
        $original = $type;
621
622
        if ($type[0] === '?') {
623
            if ($value === null) {
624
                return null;
625
            }
626
            $type = substr($type, 1);
627
        }
628
629
        switch ($type) {
630
            case 'int':
631
            case 'integer':
632
                $value = (int) $value;
633
                break;
634
            case 'float':
635
            case 'double':
636
                $value = (float) $value;
637
                break;
638
            case 'bool':
639
            case 'boolean':
640
                $value = (bool) $value;
641
                break;
642
            case 'string':
643
                $value = (string) $value;
644
                break;
645
            case 'date':
646
                $value = DateTime::createFromFormat($this->manager->getDateFormat(), $value);
647
                break;
648
            case 'json':
649
                $value = json_decode($value);
650
                break;
651
            case 'json-assoc':
652
                $value = json_decode($value, true);
653
                break;
654
            default:
655
                throw new RuntimeException(sprintf(
656
                    'Invalid cast type [%s]',
657
                    $original
658
                ));
659
        }
660
661
        return $value;
662
    }
663
664
    /**
665
     * Cast the value to the given type for set
666
     * @param mixed $value
667
     * @param string $type
668
     *
669
     * @return mixed
670
     */
671
    protected function castSet(mixed $value, string $type): mixed
672
    {
673
        $original = $type;
674
675
        if ($type[0] === '?') {
676
            if ($value === null) {
677
                return null;
678
            }
679
            $type = substr($type, 1);
680
        }
681
682
        switch ($type) {
683
            case 'int':
684
            case 'integer':
685
                $value = (int) $value;
686
                break;
687
            case 'float':
688
            case 'double':
689
                $value = (float) $value;
690
                break;
691
            case 'bool':
692
            case 'boolean':
693
                $value = (bool) $value;
694
                break;
695
            case 'string':
696
                $value = (string) $value;
697
                break;
698
            case 'date':
699
                $value = /** @var DateTime $value */ $value->format($this->manager->getDateFormat());
700
                break;
701
            case 'json':
702
            case 'json-assoc':
703
                $value = json_encode($value);
704
                break;
705
            default:
706
                throw new RuntimeException(sprintf(
707
                    'Invalid cast type [%s]',
708
                    $original
709
                ));
710
        }
711
712
        return $value;
713
    }
714
715
    /**
716
     * Set
717
     * @param string $relation
718
     * @param TEntity $entity
0 ignored issues
show
Bug introduced by
The type Platine\Orm\Mapper\TEntity was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
719
     * @param bool $link
720
     * @return void
721
     */
722
    private function setLink(string $relation, Entity $entity, bool $link): void
723
    {
724
        $relations = $this->mapper->getRelations();
725
726
        if (!isset($relations[$relation])) {
727
            throw new RelationNotFoundException(sprintf(
728
                'Unknown relation [%s]',
729
                $relation
730
            ));
731
        }
732
733
        /** @var \Platine\Orm\Relation\Relation<TEntity> $rel  */
734
        $rel = $relations[$relation];
735
        if (!($rel instanceof ShareRelation)) {
736
            throw new RuntimeException('Unsupported relation type');
737
        }
738
739
        $this->pendingLinks[] = [
740
            'relation' => $rel,
741
            'entity' => $entity,
742
            'link' => $link
743
        ];
744
    }
745
}
746