Completed
Push — master ( a7b476...58257b )
by Adrian
01:51
created

Mapper::getEntityAttribute()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 3
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
    /**
121
     * @var Query
122
     */
123
    private $queryPrototype;
0 ignored issues
show
introduced by
The private property $queryPrototype is not used, and could be removed.
Loading history...
124
125
    public static function make(Orm $orm, MapperConfig $mapperConfig)
126
    {
127
        $mapper                          = new static($orm, $mapperConfig->entityHydrator);
128
        $mapper->table                   = $mapperConfig->table;
129
        $mapper->tableAlias              = $mapperConfig->tableAlias;
130
        $mapper->primaryKey              = $mapperConfig->primaryKey;
131
        $mapper->columns                 = $mapperConfig->columns;
132
        $mapper->entityDefaultAttributes = $mapperConfig->entityDefaultAttributes;
133
        $mapper->columnAttributeMap      = $mapperConfig->columnAttributeMap;
134
        $mapper->scopes                  = $mapperConfig->scopes;
135
        $mapper->guards                  = $mapperConfig->guards;
136
        $mapper->tableReference          = QueryHelper::reference($mapper->table, $mapper->tableAlias);
137
138
        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...
139
            $mapper->relations = array_merge($mapper->relations, $mapperConfig->relations);
140
        }
141
142
        if ($mapperConfig->entityClass) {
143
            $mapper->entityClass = $mapperConfig->entityClass;
144
        }
145
146
        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...
147
            $mapper->use(...$mapperConfig->behaviours);
148
        }
149
150
        return $mapper;
151
    }
152
153
    public function __construct(Orm $orm, HydratorInterface $entityHydrator = null, QueryBuilder $queryBuilder = null)
154
    {
155
        $this->orm = $orm;
156
        if (! $entityHydrator) {
157
            $entityHydrator = new GenericEntityHydrator($orm, $this);
158
        }
159
        if (! $queryBuilder) {
160
            $this->queryBuilder = QueryBuilder::getInstance();
161
        }
162
        $this->entityHydrator = $entityHydrator;
163
        $this->tableReference = QueryHelper::reference($this->table, $this->tableAlias);
164
    }
165
166
    public function __call(string $method, array $params)
167
    {
168
        switch ($method) {
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
        return $this->tableReference;
281
    }
282
283
    /**
284
     * @return array
285
     */
286
    public function getColumns(): array
287
    {
288
        return $this->columns;
289
    }
290
291
    /**
292
     * @return array
293
     */
294
    public function getColumnAttributeMap(): array
295
    {
296
        return $this->columnAttributeMap;
297
    }
298
299
    /**
300
     * @return string
301
     */
302
    public function getEntityClass(): string
303
    {
304
        return $this->entityClass;
305
    }
306
307
    /**
308
     * @return array
309
     */
310
    public function getGuards(): array
311
    {
312
        return $this->guards;
313
    }
314
315
    /**
316
     * @param $data
317
     *
318
     * @return EntityInterface
319
     */
320
    public function newEntity(array $data): EntityInterface
321
    {
322
        $entity = $this->entityHydrator->hydrate(array_merge($this->getEntityDefaults(), $data));
323
324
        return $this->applyBehaviours(__FUNCTION__, $entity);
325
    }
326
327
    public function extractFromEntity(EntityInterface $entity): array
328
    {
329
        $data = $this->entityHydrator->extract($entity);
330
331
        return $this->applyBehaviours(__FUNCTION__, $data);
332
    }
333
334
    public function newEntityFromRow(array $data = null, array $load = [], Tracker $tracker = null)
335
    {
336
        if ($data == null) {
337
            return null;
338
        }
339
340
        $receivedTracker = ! ! $tracker;
341
        if (! $tracker) {
342
            $receivedTracker = false;
343
            $tracker         = new Tracker($this, [$data]);
344
        }
345
346
        $entity = $this->newEntity($data);
347
        $this->injectRelations($entity, $tracker, $load);
348
        $entity->setPersistenceState(StateEnum::SYNCHRONIZED);
349
350
        if (! $receivedTracker) {
351
            $tracker->replaceRows([$entity]);
352
            if ($tracker->isDisposable()) {
353
                unset($tracker);
354
            }
355
        }
356
357
        return $entity;
358
    }
359
360
    public function newCollectionFromRows(array $rows, array $load = []): Collection
361
    {
362
        $entities = [];
363
        $tracker  = new Tracker($this, $rows);
364
        foreach ($rows as $row) {
365
            $entity     = $this->newEntityFromRow($row, $load, $tracker);
366
            $entities[] = $entity;
367
        }
368
        $tracker->replaceRows($entities);
369
        if ($tracker->isDisposable()) {
370
            unset($tracker);
371
        }
372
373
        return new Collection($entities);
374
    }
375
376
    public function newPaginatedCollectionFromRows(
377
        array $rows,
378
        int $totalCount,
379
        int $perPage,
380
        int $currentPage,
381
        array $load = []
382
    ): PaginatedCollection {
383
        $entities = [];
384
        $tracker  = new Tracker($this, $rows);
385
        foreach ($rows as $row) {
386
            $entity     = $this->newEntityFromRow($row, $load, $tracker);
387
            $entities[] = $entity;
388
        }
389
        $tracker->replaceRows($entities);
390
        if ($tracker->isDisposable()) {
391
            unset($tracker);
392
        }
393
394
        return new PaginatedCollection($entities, $totalCount, $perPage, $currentPage);
395
    }
396
397
    protected function injectRelations(EntityInterface $entity, Tracker $tracker, array $eagerLoad = [])
398
    {
399
        $trackerIdDisposable = true;
400
        foreach (array_keys($this->relations) as $name) {
401
            $relation      = $this->getRelation($name);
402
            $queryCallback = $eagerLoad[$name] ?? null;
403
            $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...
404
405
            if (! $tracker->hasRelation($name)) {
406
                $tracker->setRelation($name, $relation, $queryCallback);
407
            }
408
409
            if (array_key_exists($name, $eagerLoad) || $relation->isEagerLoad()) {
410
                $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

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

427
        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...
428
    }
429
430
    public function getEntityAttribute(EntityInterface $entity, $attribute)
431
    {
432
        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

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