DataMapper   F
last analyzed

Complexity

Total Complexity 100

Size/Duplication

Total Lines 678
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 252
dl 0
loc 678
rs 2
c 2
b 0
f 0
wmc 100

30 Methods

Rating   Name   Duplication   Size   Complexity  
A getRawColumns() 0 3 1
A hasRelation() 0 5 1
A link() 0 3 1
A setRelated() 0 23 6
A isNew() 0 3 1
A isReadOnly() 0 3 1
B getColumn() 0 40 8
A setRawColumn() 0 5 1
A isDeleted() 0 3 1
A getEntityMapper() 0 3 1
A clearRelated() 0 13 3
A wasModified() 0 3 2
A refresh() 0 3 1
A unlink() 0 3 1
A setColumn() 0 29 6
A getEntityManager() 0 3 1
A getRelated() 0 37 6
A __construct() 0 18 3
A getModifiedColumns() 0 3 1
A clearColumn() 0 6 2
A hasColumn() 0 4 2
A markAsDeleted() 0 3 1
C castSet() 0 42 13
A markAsUpdated() 0 19 3
A executePendingLinkage() 0 16 4
C castGet() 0 44 13
A setLink() 0 21 3
A markAsSaved() 0 21 4
A hydrate() 0 27 5
A fill() 0 13 4

How to fix   Complexity   

Complex Class

Complex classes like DataMapper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DataMapper, and based on these observations, apply Extract Interface, too.

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 (count($fillable) > 0) {
487
            $columns = array_intersect_key($columns, array_flip($fillable));
488
        } elseif (count($guarded) > 0) {
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() === false) {
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 (count($this->pendingLinks) > 0) {
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
        // some relation already loaded still in the cache
541
        // so force reload it
542
        $this->relations = [];
543
544
        if (count($this->pendingLinks) > 0) {
545
            $this->executePendingLinkage();
546
        }
547
548
        return true;
549
    }
550
551
    /**
552
     * Mark the entity as deleted
553
     * @return bool
554
     */
555
    public function markAsDeleted(): bool
556
    {
557
        return $this->deleted = true;
558
    }
559
560
    /**
561
     * Execute the pending links
562
     * @return void
563
     */
564
    public function executePendingLinkage(): void
565
    {
566
        foreach ($this->pendingLinks as $item) {
567
            /** @var \Platine\Orm\Relation\ShareOne<TEntity>|\Platine\Orm\Relation\ShareMany<TEntity> $rel */
568
            $rel = $item['relation'];
569
570
            if (isset($item['link'])) {
571
                if ($item['link'] === true) {
572
                    $rel->link($this, $item['entity']);
573
                } else {
574
                    $rel->unlink($this, $item['entity']);
575
                }
576
            }
577
        }
578
579
        $this->pendingLinks = [];
580
    }
581
582
    /**
583
     * Get fresh data from data store
584
     * @return void
585
     */
586
    protected function hydrate(): void
587
    {
588
        if ($this->refresh === false) {
589
            return;
590
        }
591
592
        $select = new Select($this->manager->getConnection(), $this->mapper->getTable());
593
594
        $primaryKeys = $this->mapper->getPrimaryKey()->getValue($this->rawColumns, true);
595
        if (is_array($primaryKeys)) {
596
            foreach ($primaryKeys as $pkColumn => $pkValue) {
597
                $select->where($pkColumn)->is($pkValue);
598
            }
599
        }
600
601
        $columns = $select->select()->fetchAssoc()->get();
602
603
        if ($columns === false) {
604
            $this->deleted = true;
605
            return;
606
        }
607
608
        $this->rawColumns = $columns;
609
        $this->columns = [];
610
        $this->relations = [];
611
        $this->loaders = [];
612
        $this->refresh = false;
613
    }
614
615
    /**
616
     * Cast the value to the given type for get
617
     * @param mixed $value
618
     * @param string $type
619
     *
620
     * @return mixed
621
     */
622
    protected function castGet(mixed $value, string $type): mixed
623
    {
624
        $original = $type;
625
626
        if ($type[0] === '?') {
627
            if ($value === null) {
628
                return null;
629
            }
630
            $type = substr($type, 1);
631
        }
632
633
        switch ($type) {
634
            case 'int':
635
            case 'integer':
636
                $value = (int) $value;
637
                break;
638
            case 'float':
639
            case 'double':
640
                $value = (float) $value;
641
                break;
642
            case 'bool':
643
            case 'boolean':
644
                $value = (bool) $value;
645
                break;
646
            case 'string':
647
                $value = (string) $value;
648
                break;
649
            case 'date':
650
                $value = DateTime::createFromFormat($this->manager->getDateFormat(), $value);
651
                break;
652
            case 'json':
653
                $value = json_decode($value);
654
                break;
655
            case 'json-assoc':
656
                $value = json_decode($value, true);
657
                break;
658
            default:
659
                throw new RuntimeException(sprintf(
660
                    'Invalid cast type [%s]',
661
                    $original
662
                ));
663
        }
664
665
        return $value;
666
    }
667
668
    /**
669
     * Cast the value to the given type for set
670
     * @param mixed $value
671
     * @param string $type
672
     *
673
     * @return mixed
674
     */
675
    protected function castSet(mixed $value, string $type): mixed
676
    {
677
        $original = $type;
678
679
        if ($type[0] === '?') {
680
            if ($value === null) {
681
                return null;
682
            }
683
            $type = substr($type, 1);
684
        }
685
686
        switch ($type) {
687
            case 'int':
688
            case 'integer':
689
                $value = (int) $value;
690
                break;
691
            case 'float':
692
            case 'double':
693
                $value = (float) $value;
694
                break;
695
            case 'bool':
696
            case 'boolean':
697
                $value = (bool) $value;
698
                break;
699
            case 'string':
700
                $value = (string) $value;
701
                break;
702
            case 'date':
703
                $value = /** @var DateTime $value */ $value->format($this->manager->getDateFormat());
704
                break;
705
            case 'json':
706
            case 'json-assoc':
707
                $value = json_encode($value);
708
                break;
709
            default:
710
                throw new RuntimeException(sprintf(
711
                    'Invalid cast type [%s]',
712
                    $original
713
                ));
714
        }
715
716
        return $value;
717
    }
718
719
    /**
720
     * Set
721
     * @param string $relation
722
     * @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...
723
     * @param bool $link
724
     * @return void
725
     */
726
    private function setLink(string $relation, Entity $entity, bool $link): void
727
    {
728
        $relations = $this->mapper->getRelations();
729
730
        if (!isset($relations[$relation])) {
731
            throw new RelationNotFoundException(sprintf(
732
                'Unknown relation [%s]',
733
                $relation
734
            ));
735
        }
736
737
        /** @var \Platine\Orm\Relation\Relation<TEntity> $rel  */
738
        $rel = $relations[$relation];
739
        if (!($rel instanceof ShareRelation)) {
740
            throw new RuntimeException('Unsupported relation type');
741
        }
742
743
        $this->pendingLinks[] = [
744
            'relation' => $rel,
745
            'entity' => $entity,
746
            'link' => $link
747
        ];
748
    }
749
}
750