Passed
Push — master ( ea0818...92b4a6 )
by Adrian
01:28
created

Mapper::make()   B

Complexity

Conditions 7
Paths 16

Size

Total Lines 30
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 19
c 2
b 0
f 0
dl 0
loc 30
rs 8.8333
cc 7
nc 16
nop 2
1
<?php
2
declare(strict_types=1);
3
4
namespace Sirius\Orm;
5
6
use Sirius\Orm\Action\BaseAction;
7
use Sirius\Orm\Action\Delete;
8
use Sirius\Orm\Action\Insert;
9
use Sirius\Orm\Action\Update;
10
use Sirius\Orm\Behaviours\BehaviourInterface;
0 ignored issues
show
Bug introduced by
The type Sirius\Orm\Behaviours\BehaviourInterface 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...
11
use Sirius\Orm\Collection\Collection;
12
use Sirius\Orm\Collection\PaginatedCollection;
13
use Sirius\Orm\Entity\EntityInterface;
14
use Sirius\Orm\Entity\GenericEntity;
15
use Sirius\Orm\Entity\GenericEntityHydrator;
16
use Sirius\Orm\Entity\HydratorInterface;
17
use Sirius\Orm\Entity\StateEnum;
18
use Sirius\Orm\Entity\Tracker;
19
use Sirius\Orm\Helpers\Arr;
20
use Sirius\Orm\Helpers\Inflector;
21
use Sirius\Orm\Helpers\QueryHelper;
22
use Sirius\Orm\Relation\Aggregate;
23
use Sirius\Orm\Relation\Relation;
24
use Sirius\Orm\Relation\RelationConfig;
25
26
/**
27
 * @method array where($column, $value, $condition)
28
 * @method array columns(string $expr, string ...$exprs)
29
 * @method array orderBy(string $expr, string ...$exprs)
30
 */
31
class Mapper
32
{
33
    /**
34
     * Name of the class/interface to be used to determine
35
     * if this mapper can persist a specific entity
36
     * @var string
37
     */
38
    protected $entityClass = GenericEntity::class;
39
40
    /**
41
     * @var string|array
42
     */
43
    protected $primaryKey = 'id';
44
45
    /**
46
     * @var string
47
     */
48
    protected $table;
49
50
    /**
51
     * Used in queries like so: FROM table as tableAlias
52
     * This is especially useful if you are using prefixed tables
53
     * @var string
54
     */
55
    protected $tableAlias = '';
56
57
    /**
58
     * @var string
59
     */
60
    protected $tableReference;
61
62
    /**
63
     * Table columns
64
     * @var array
65
     */
66
    protected $columns = [];
67
68
    /**
69
     * Column casts
70
     * @var array
71
     */
72
    protected $casts = ['id' => 'int'];
73
74
    /**
75
     * Column aliases (table column => entity attribute)
76
     * @var array
77
     */
78
    protected $columnAttributeMap = [];
79
80
    /**
81
     * @var HydratorInterface
82
     */
83
    protected $entityHydrator;
84
85
    /**
86
     * Default attributes
87
     * @var array
88
     */
89
    protected $entityDefaultAttributes = [];
90
91
    /**
92
     * List of behaviours to be attached to the mapper
93
     * @var array[BehaviourInterface]
0 ignored issues
show
Documentation Bug introduced by
The doc comment array[BehaviourInterface] at position 1 could not be parsed: Expected ']' at position 1, but found '['.
Loading history...
94
     */
95
    protected $behaviours = [];
96
97
    /**
98
     * @var array
99
     */
100
    protected $relations = [];
101
102
    /**
103
     * @var array
104
     */
105
    protected $scopes = [];
106
107
    /**
108
     * @var array
109
     */
110
    protected $guards = [];
111
112
    /**
113
     * @var QueryBuilder
114
     */
115
    protected $queryBuilder;
116
117
    /**
118
     * @var Orm
119
     */
120
    protected $orm;
121
122
    public static function make(Orm $orm, MapperConfig $mapperConfig)
123
    {
124
        $mapper                          = new static($orm, $mapperConfig->entityHydrator);
125
        $mapper->table                   = $mapperConfig->table;
126
        $mapper->tableAlias              = $mapperConfig->tableAlias;
127
        $mapper->primaryKey              = $mapperConfig->primaryKey;
128
        $mapper->columns                 = $mapperConfig->columns;
129
        $mapper->entityDefaultAttributes = $mapperConfig->entityDefaultAttributes;
130
        $mapper->columnAttributeMap      = $mapperConfig->columnAttributeMap;
131
        $mapper->scopes                  = $mapperConfig->scopes;
132
        $mapper->guards                  = $mapperConfig->guards;
133
        $mapper->tableReference          = QueryHelper::reference($mapper->table, $mapper->tableAlias);
134
135
        if (isset($mapperConfig->casts) && !empty($mapperConfig->casts)) {
136
            $mapper->casts = $mapperConfig->casts;
137
        }
138
139
        if ($mapperConfig->relations) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $mapperConfig->relations of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
140
            $mapper->relations = array_merge($mapper->relations, $mapperConfig->relations);
141
        }
142
143
        if ($mapperConfig->entityClass) {
144
            $mapper->entityClass = $mapperConfig->entityClass;
145
        }
146
147
        if ($mapperConfig->behaviours && ! empty($mapperConfig->behaviours)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $mapperConfig->behaviours of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
148
            $mapper->use(...$mapperConfig->behaviours);
149
        }
150
151
        return $mapper;
152
    }
153
154
    public function __construct(Orm $orm, HydratorInterface $entityHydrator = null, QueryBuilder $queryBuilder = null)
155
    {
156
        $this->orm = $orm;
157
158
        if (! $entityHydrator) {
159
            $entityHydrator = new GenericEntityHydrator();
160
            $entityHydrator->setMapper($this);
161
            $entityHydrator->setCastingManager($orm->getCastingManager());
162
        }
163
        $this->entityHydrator = $entityHydrator;
164
165
        if (! $queryBuilder) {
166
            $queryBuilder = QueryBuilder::getInstance();
167
        }
168
        $this->queryBuilder = $queryBuilder;
169
    }
170
171
    public function __call(string $method, array $params)
172
    {
173
        switch ($method) {
174
            case 'where':
175
            case 'where':
176
            case 'columns':
177
            case 'orderBy':
178
                $query = $this->newQuery();
179
180
                return $query->{$method}(...$params);
181
        }
182
183
184
        throw new \BadMethodCallException('Unknown method {$method} for class ' . get_class($this));
185
    }
186
187
    /**
188
     * Add behaviours to the mapper
189
     *
190
     * @param mixed ...$behaviours
191
     */
192
    public function use(...$behaviours)
193
    {
194
        if (empty($behaviours)) {
195
            return;
196
        }
197
        foreach ($behaviours as $behaviour) {
198
            /** @var $behaviour BehaviourInterface */
199
            if (isset($this->behaviours[$behaviour->getName()])) {
200
                throw new \BadMethodCallException(
201
                    sprintf('Behaviour "%s" is already registered', $behaviour->getName())
202
                );
203
            }
204
            $this->behaviours[$behaviour->getName()] = $behaviour;
205
        }
206
    }
207
208
    public function without(...$behaviours)
209
    {
210
        if (empty($behaviours)) {
211
            return $this;
212
        }
213
        $mapper = clone $this;
214
        foreach ($behaviours as $behaviour) {
215
            unset($mapper->behaviours[$behaviour]);
216
        }
217
218
        return $mapper;
219
    }
220
221
    public function addQueryScope($scope, callable $callback)
222
    {
223
        $this->scopes[$scope] = $callback;
224
    }
225
226
    public function getQueryScope($scope)
227
    {
228
        return $this->scopes[$scope] ?? null;
229
    }
230
231
    public function registerCasts(CastingManager $castingManager)
232
    {
233
        $mapper = $this;
234
235
        $singular = Inflector::singularize($this->getTableAlias(true));
236
        $castingManager->register($singular, function ($value) use ($mapper, $castingManager) {
237
            if ($value instanceof $this->entityClass) {
238
                return $value;
239
            }
240
241
            return $value !== null ? $mapper->newEntity($value, $castingManager) : null;
0 ignored issues
show
Unused Code introduced by
The call to Sirius\Orm\Mapper::newEntity() has too many arguments starting with $castingManager. ( Ignorable by Annotation )

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

241
            return $value !== null ? $mapper->/** @scrutinizer ignore-call */ newEntity($value, $castingManager) : null;

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
242
        });
243
244
        $plural = $this->getTableAlias(true);
245
        $castingManager->register($plural, function ($values) use ($mapper, $castingManager) {
246
            if ($values instanceof Collection) {
247
                return $values;
248
            }
249
            $collection = new Collection();
250
            if (is_array($values)) {
251
                foreach ($values as $value) {
252
                    $collection->add($mapper->newEntity($value, $castingManager));
0 ignored issues
show
Unused Code introduced by
The call to Sirius\Orm\Mapper::newEntity() has too many arguments starting with $castingManager. ( Ignorable by Annotation )

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

252
                    $collection->add($mapper->/** @scrutinizer ignore-call */ newEntity($value, $castingManager));

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
253
                }
254
            }
255
256
            return $collection;
257
        });
258
    }
259
260
    /**
261
     * @return array|string
262
     */
263
    public function getPrimaryKey()
264
    {
265
        return $this->primaryKey;
266
    }
267
268
    /**
269
     * @return string
270
     */
271
    public function getTable(): string
272
    {
273
        return $this->table;
274
    }
275
276
    /**
277
     * @return string
278
     */
279
    public function getTableAlias($returnTableIfNull = false)
280
    {
281
        return (! $this->tableAlias && $returnTableIfNull) ? $this->table : $this->tableAlias;
282
    }
283
284
    public function getTableReference()
285
    {
286
        if (!$this->tableReference) {
287
            $this->tableReference = QueryHelper::reference($this->table, $this->tableAlias);
288
        }
289
290
        return $this->tableReference;
291
    }
292
293
    /**
294
     * @return array
295
     */
296
    public function getColumns(): array
297
    {
298
        return $this->columns;
299
    }
300
301
    /**
302
     * @return array
303
     */
304
    public function getColumnAttributeMap(): array
305
    {
306
        return $this->columnAttributeMap;
307
    }
308
309
    /**
310
     * @return string
311
     */
312
    public function getEntityClass(): string
313
    {
314
        return $this->entityClass;
315
    }
316
317
    /**
318
     * @return array
319
     */
320
    public function getGuards(): array
321
    {
322
        return $this->guards;
323
    }
324
325
    /**
326
     * @param $data
327
     *
328
     * @return EntityInterface
329
     */
330
    public function newEntity(array $data): EntityInterface
331
    {
332
        $entity = $this->entityHydrator->hydrate(array_merge($this->getEntityDefaults(), $data));
333
334
        return $this->applyBehaviours(__FUNCTION__, $entity);
335
    }
336
337
    public function extractFromEntity(EntityInterface $entity): array
338
    {
339
        $data = $this->entityHydrator->extract($entity);
340
341
        return $this->applyBehaviours(__FUNCTION__, $data);
342
    }
343
344
    public function newEntityFromRow(array $data = null, array $load = [], Tracker $tracker = null)
345
    {
346
        if ($data == null) {
347
            return null;
348
        }
349
350
        $receivedTracker = ! ! $tracker;
351
        if (! $tracker) {
352
            $receivedTracker = false;
353
            $tracker         = new Tracker([$data]);
354
        }
355
356
        $entity = $this->newEntity($data);
357
        $this->injectRelations($entity, $tracker, $load);
358
        $this->injectAggregates($entity, $tracker, $load);
359
        $entity->setPersistenceState(StateEnum::SYNCHRONIZED);
360
361
        if (! $receivedTracker) {
362
            $tracker->replaceRows([$entity]);
363
        }
364
365
        return $entity;
366
    }
367
368
    public function newCollectionFromRows(array $rows, array $load = []): Collection
369
    {
370
        $entities = [];
371
        $tracker  = new Tracker($rows);
372
        foreach ($rows as $row) {
373
            $entity     = $this->newEntityFromRow($row, $load, $tracker);
374
            $entities[] = $entity;
375
        }
376
        $tracker->replaceRows($entities);
377
378
        return new Collection($entities);
379
    }
380
381
    public function newPaginatedCollectionFromRows(
382
        array $rows,
383
        int $totalCount,
384
        int $perPage,
385
        int $currentPage,
386
        array $load = []
387
    ): PaginatedCollection {
388
        $entities = [];
389
        $tracker  = new Tracker($rows);
390
        foreach ($rows as $row) {
391
            $entity     = $this->newEntityFromRow($row, $load, $tracker);
392
            $entities[] = $entity;
393
        }
394
        $tracker->replaceRows($entities);
395
396
        return new PaginatedCollection($entities, $totalCount, $perPage, $currentPage);
397
    }
398
399
    protected function injectRelations(EntityInterface $entity, Tracker $tracker, array $eagerLoad = [])
400
    {
401
        foreach (array_keys($this->relations) as $name) {
402
            $relation      = $this->getRelation($name);
403
            $queryCallback = $eagerLoad[$name] ?? null;
404
            $nextLoad      = Arr::getChildren($eagerLoad, $name);
405
406
            if (! $tracker->hasRelation($name)) {
407
                $tracker->setRelation($name, $relation, $queryCallback, $nextLoad);
408
            }
409
410
            if (array_key_exists($name, $eagerLoad) || in_array($name, $eagerLoad) || $relation->isEagerLoad()) {
411
                #$relation->attachLazyRelationToEntity($entity, $tracker);
412
                #$this->getEntityAttribute($entity, $name);
413
                $relation->attachMatchesToEntity($entity, $tracker->getResultsForRelation($name));
0 ignored issues
show
Bug introduced by
It seems like $tracker->getResultsForRelation($name) can also be of type null; however, parameter $queryResult of Sirius\Orm\Relation\Rela...attachMatchesToEntity() does only seem to accept array, 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

413
                $relation->attachMatchesToEntity($entity, /** @scrutinizer ignore-type */ $tracker->getResultsForRelation($name));
Loading history...
414
            } elseif ($relation->isLazyLoad()) {
415
                $relation->attachLazyRelationToEntity($entity, $tracker);
416
            }
417
        }
418
    }
419
420
    protected function injectAggregates(EntityInterface $entity, Tracker $tracker, array $eagerLoad = [])
421
    {
422
        foreach (array_keys($this->relations) as $name) {
423
            $relation      = $this->getRelation($name);
424
            if (!method_exists($relation, 'getAggregates')) {
425
                continue;
426
            }
427
            $aggregates = $relation->getAggregates();
428
            foreach ($aggregates as $aggName => $aggregate) {
429
                /** @var $aggregate Aggregate */
430
                if (array_key_exists($aggName, $eagerLoad) || $aggregate->isEagerLoad()) {
431
                    $aggregate->attachAggregateToEntity($entity, $tracker->getAggregateResults($aggregate));
432
                } elseif ($aggregate->isLazyLoad()) {
433
                    $aggregate->attachLazyAggregateToEntity($entity, $tracker);
434
                }
435
            }
436
        }
437
    }
438
439
    protected function getEntityDefaults()
440
    {
441
        return $this->entityDefaultAttributes;
442
    }
443
444
    public function setEntityAttribute(EntityInterface $entity, $attribute, $value)
445
    {
446
        return $entity->set($attribute, $value);
0 ignored issues
show
Bug introduced by
The method set() does not exist on Sirius\Orm\Entity\EntityInterface. Did you maybe mean setPk()? ( Ignorable by Annotation )

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

446
        return $entity->/** @scrutinizer ignore-call */ set($attribute, $value);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
447
    }
448
449
    public function getEntityAttribute(EntityInterface $entity, $attribute)
450
    {
451
        return $entity->get($attribute);
0 ignored issues
show
Bug introduced by
The method get() does not exist on Sirius\Orm\Entity\EntityInterface. Did you maybe mean getPk()? ( Ignorable by Annotation )

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

451
        return $entity->/** @scrutinizer ignore-call */ get($attribute);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
452
    }
453
454
    public function addRelation($name, $relation)
455
    {
456
        if (is_array($relation) || $relation instanceof Relation) {
457
            $this->relations[$name] = $relation;
458
            return;
459
        }
460
        throw new \InvalidArgumentException(
461
            sprintf('The relation has to be an Relation instance or an array of configuration options')
462
        );
463
    }
464
465
    public function hasRelation($name): bool
466
    {
467
        return isset($this->relations[$name]);
468
    }
469
470
    public function getRelation($name): Relation
471
    {
472
        if (! $this->hasRelation($name)) {
473
            throw new \InvalidArgumentException("Relation named {$name} is not registered for this mapper");
474
        }
475
476
        if (is_array($this->relations[$name])) {
477
            $this->relations[$name] = $this->orm->createRelation($this, $name, $this->relations[$name]);
478
        }
479
        $relation = $this->relations[$name];
480
        if (! $relation instanceof Relation) {
481
            throw new \InvalidArgumentException("Relation named {$name} is not a proper Relation instance");
482
        }
483
484
        return $relation;
485
    }
486
487
    public function getRelations(): array
488
    {
489
        return array_keys($this->relations);
490
    }
491
492
    public function newQuery(): Query
493
    {
494
        $query = $this->queryBuilder->newQuery($this);
495
496
        return $this->applyBehaviours(__FUNCTION__, $query);
497
    }
498
499
    public function find($pk, array $load = [])
500
    {
501
        return $this->newQuery()
502
                    ->where($this->getPrimaryKey(), $pk)
503
                    ->load(...$load)
504
                    ->first();
505
    }
506
507
    /**
508
     * @param EntityInterface $entity
509
     *
510
     * @return bool
511
     * @throws \Exception
512
     */
513
    public function save(EntityInterface $entity, $withRelations = true)
514
    {
515
        $this->assertCanPersistEntity($entity);
516
        $action = $this->newSaveAction($entity, ['relations' => $withRelations]);
517
518
        $this->orm->getConnectionLocator()->lockToWrite(true);
519
        $this->getWriteConnection()->beginTransaction();
520
        try {
521
            $action->run();
522
            $this->getWriteConnection()->commit();
523
            $this->orm->getConnectionLocator()->lockToWrite(false);
524
            return true;
525
        } catch (\Exception $e) {
526
            $this->getWriteConnection()->rollBack();
527
            $this->orm->getConnectionLocator()->lockToWrite(false);
528
            throw $e;
529
        }
530
    }
531
532
    public function newSaveAction(EntityInterface $entity, $options): BaseAction
533
    {
534
        if (! $this->getEntityAttribute($entity, $this->primaryKey)) {
535
            $action = new Insert($this, $entity, $options);
536
        } else {
537
            $action = new Update($this, $entity, $options);
538
        }
539
540
        return $this->applyBehaviours('save', $action);
541
    }
542
543
    public function delete(EntityInterface $entity, $withRelations = true)
544
    {
545
        $this->assertCanPersistEntity($entity);
546
547
        $action = $this->newDeleteAction($entity, ['relations' => $withRelations]);
548
549
        $this->orm->getConnectionLocator()->lockToWrite(true);
550
        $this->getWriteConnection()->beginTransaction();
551
        try {
552
            $action->run();
553
            $this->getWriteConnection()->commit();
554
555
            return true;
556
        } catch (\Exception $e) {
557
            $this->getWriteConnection()->rollBack();
558
            throw $e;
559
        }
560
    }
561
562
    public function newDeleteAction(EntityInterface $entity, $options)
563
    {
564
        $action = new Delete($this, $entity, $options);
565
566
        return $this->applyBehaviours('delete', $action);
567
    }
568
569
    protected function assertCanPersistEntity($entity)
570
    {
571
        if (! $entity || ! $entity instanceof $this->entityClass) {
572
            throw new \InvalidArgumentException(sprintf(
573
                'Mapper %s can only persist entity of class %s. %s class provided',
574
                __CLASS__,
575
                $this->entityClass,
576
                get_class($entity)
577
            ));
578
        }
579
    }
580
581
    protected function applyBehaviours($target, $result, ...$args)
582
    {
583
        foreach ($this->behaviours as $behaviour) {
584
            $method = 'on' . Helpers\Str::className($target);
585
            if (method_exists($behaviour, $method)) {
586
                $result = $behaviour->{$method}($this, $result, ...$args);
587
            }
588
        }
589
590
        return $result;
591
    }
592
593
    public function getReadConnection()
594
    {
595
        return $this->orm->getConnectionLocator()->getRead();
596
    }
597
598
    public function getWriteConnection()
599
    {
600
        return $this->orm->getConnectionLocator()->getWrite();
601
    }
602
603
    public function getCasts()
604
    {
605
        return $this->casts;
606
    }
607
}
608