Completed
Push — master ( e55059...ea0818 )
by Adrian
02:34
created

Mapper::addRelation()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 8
rs 10
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\Relation;
23
24
/**
25
 * @method array where($column, $value, $condition)
26
 * @method array columns(string $expr, string ...$exprs)
27
 * @method array orderBy(string $expr, string ...$exprs)
28
 */
29
class Mapper
30
{
31
    /**
32
     * Name of the class/interface to be used to determine
33
     * if this mapper can persist a specific entity
34
     * @var string
35
     */
36
    protected $entityClass = GenericEntity::class;
37
38
    /**
39
     * @var string|array
40
     */
41
    protected $primaryKey = 'id';
42
43
    /**
44
     * @var string
45
     */
46
    protected $table;
47
48
    /**
49
     * Used in queries like so: FROM table as tableAlias
50
     * This is especially useful if you are using prefixed tables
51
     * @var string
52
     */
53
    protected $tableAlias = '';
54
55
    /**
56
     * @var string
57
     */
58
    protected $tableReference;
59
60
    /**
61
     * Table columns
62
     * @var array
63
     */
64
    protected $columns = [];
65
66
    /**
67
     * Column casts
68
     * @var array
69
     */
70
    protected $casts = ['id' => 'int'];
71
72
    /**
73
     * Column aliases (table column => entity attribute)
74
     * @var array
75
     */
76
    protected $columnAttributeMap = [];
77
78
    /**
79
     * @var HydratorInterface
80
     */
81
    protected $entityHydrator;
82
83
    /**
84
     * Default attributes
85
     * @var array
86
     */
87
    protected $entityDefaultAttributes = [];
88
89
    /**
90
     * List of behaviours to be attached to the mapper
91
     * @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...
92
     */
93
    protected $behaviours = [];
94
95
    /**
96
     * @var array
97
     */
98
    protected $relations = [];
99
100
    /**
101
     * @var array
102
     */
103
    protected $scopes = [];
104
105
    /**
106
     * @var array
107
     */
108
    protected $guards = [];
109
110
    /**
111
     * @var QueryBuilder
112
     */
113
    protected $queryBuilder;
114
115
    /**
116
     * @var Orm
117
     */
118
    protected $orm;
119
120
    public static function make(Orm $orm, MapperConfig $mapperConfig)
121
    {
122
        $mapper                          = new static($orm, $mapperConfig->entityHydrator);
123
        $mapper->table                   = $mapperConfig->table;
124
        $mapper->tableAlias              = $mapperConfig->tableAlias;
125
        $mapper->primaryKey              = $mapperConfig->primaryKey;
126
        $mapper->columns                 = $mapperConfig->columns;
127
        $mapper->entityDefaultAttributes = $mapperConfig->entityDefaultAttributes;
128
        $mapper->columnAttributeMap      = $mapperConfig->columnAttributeMap;
129
        $mapper->scopes                  = $mapperConfig->scopes;
130
        $mapper->guards                  = $mapperConfig->guards;
131
        $mapper->tableReference          = QueryHelper::reference($mapper->table, $mapper->tableAlias);
132
133
        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...
134
            $mapper->relations = array_merge($mapper->relations, $mapperConfig->relations);
135
        }
136
137
        if ($mapperConfig->entityClass) {
138
            $mapper->entityClass = $mapperConfig->entityClass;
139
        }
140
141
        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...
142
            $mapper->use(...$mapperConfig->behaviours);
143
        }
144
145
        return $mapper;
146
    }
147
148
    public function __construct(Orm $orm, HydratorInterface $entityHydrator = null, QueryBuilder $queryBuilder = null)
149
    {
150
        $this->orm = $orm;
151
152
        if (! $entityHydrator) {
153
            $entityHydrator = new GenericEntityHydrator();
154
            $entityHydrator->setMapper($this);
155
            $entityHydrator->setCastingManager($orm->getCastingManager());
156
        }
157
        $this->entityHydrator = $entityHydrator;
158
159
        if (! $queryBuilder) {
160
            $queryBuilder = QueryBuilder::getInstance();
161
        }
162
        $this->queryBuilder = $queryBuilder;
163
    }
164
165
    public function __call(string $method, array $params)
166
    {
167
        switch ($method) {
168
            case 'where':
169
            case 'where':
170
            case 'columns':
171
            case 'orderBy':
172
                $query = $this->newQuery();
173
174
                return $query->{$method}(...$params);
175
        }
176
177
178
        throw new \BadMethodCallException('Unknown method {$method} for class ' . get_class($this));
179
    }
180
181
    /**
182
     * Add behaviours to the mapper
183
     *
184
     * @param mixed ...$behaviours
185
     */
186
    public function use(...$behaviours)
187
    {
188
        if (empty($behaviours)) {
189
            return;
190
        }
191
        foreach ($behaviours as $behaviour) {
192
            /** @var $behaviour BehaviourInterface */
193
            if (isset($this->behaviours[$behaviour->getName()])) {
194
                throw new \BadMethodCallException(
195
                    sprintf('Behaviour "%s" is already registered', $behaviour->getName())
196
                );
197
            }
198
            $this->behaviours[$behaviour->getName()] = $behaviour;
199
        }
200
    }
201
202
    public function without(...$behaviours)
203
    {
204
        if (empty($behaviours)) {
205
            return $this;
206
        }
207
        $mapper = clone $this;
208
        foreach ($behaviours as $behaviour) {
209
            unset($mapper->behaviours[$behaviour]);
210
        }
211
212
        return $mapper;
213
    }
214
215
    public function addQueryScope($scope, callable $callback)
216
    {
217
        $this->scopes[$scope] = $callback;
218
    }
219
220
    public function getQueryScope($scope)
221
    {
222
        return $this->scopes[$scope] ?? null;
223
    }
224
225
    public function registerCasts(CastingManager $castingManager)
226
    {
227
        $mapper = $this;
228
229
        $singular = Inflector::singularize($this->getTableAlias(true));
230
        $castingManager->register($singular, function ($value) use ($mapper, $castingManager) {
231
            if ($value instanceof $this->entityClass) {
232
                return $value;
233
            }
234
235
            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

235
            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...
236
        });
237
238
        $plural = $this->getTableAlias(true);
239
        $castingManager->register($plural, function ($values) use ($mapper, $castingManager) {
240
            if ($values instanceof Collection) {
241
                return $values;
242
            }
243
            $collection = new Collection();
244
            if (is_array($values)) {
245
                foreach ($values as $value) {
246
                    $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

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

414
                $relation->attachMatchesToEntity($entity, /** @scrutinizer ignore-type */ $tracker->getRelationResults($name));
Loading history...
415
            } elseif ($relation->isLazyLoad()) {
416
                $trackerIdDisposable = false;
417
                $relation->attachLazyValueToEntity($entity, $tracker);
418
            }
419
        }
420
421
        $tracker->setDisposable($trackerIdDisposable);
422
    }
423
424
    protected function getEntityDefaults()
425
    {
426
        return $this->entityDefaultAttributes;
427
    }
428
429
    public function setEntityAttribute(EntityInterface $entity, $attribute, $value)
430
    {
431
        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

431
        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...
432
    }
433
434
    public function getEntityAttribute(EntityInterface $entity, $attribute)
435
    {
436
        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

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