Completed
Push — master ( eacc73...69d932 )
by Adrian
01:58
created

Mapper::addQueryScope()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
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 aliases (table column => entity attribute)
68
     * @var array
69
     */
70
    protected $columnAttributeMap = [];
71
72
    /**
73
     * @var HydratorInterface
74
     */
75
    protected $entityHydrator;
76
77
    /**
78
     * Default attributes
79
     * @var array
80
     */
81
    protected $entityDefaultAttributes = [];
82
83
    /**
84
     * List of behaviours to be attached to the mapper
85
     * @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...
86
     */
87
    protected $behaviours = [];
88
89
    /**
90
     * @var array
91
     */
92
    protected $relations = [];
93
94
    /**
95
     * @var array
96
     */
97
    protected $scopes = [];
98
99
    /**
100
     * @var array
101
     */
102
    protected $guards = [];
103
104
    /**
105
     * @var QueryBuilder
106
     */
107
    protected $queryBuilder;
108
109
    /**
110
     * @var Orm
111
     */
112
    protected $orm;
113
114
    /**
115
     * @var Query
116
     */
117
    private $queryPrototype;
0 ignored issues
show
introduced by
The private property $queryPrototype is not used, and could be removed.
Loading history...
118
119
    public static function make(Orm $orm, MapperConfig $mapperConfig)
120
    {
121
        $mapper                          = new static($orm, $mapperConfig->entityHydrator);
122
        $mapper->table                   = $mapperConfig->table;
123
        $mapper->tableAlias              = $mapperConfig->tableAlias;
124
        $mapper->primaryKey              = $mapperConfig->primaryKey;
125
        $mapper->columns                 = $mapperConfig->columns;
126
        $mapper->entityDefaultAttributes = $mapperConfig->entityDefaultAttributes;
127
        $mapper->columnAttributeMap      = $mapperConfig->columnAttributeMap;
128
        $mapper->scopes                  = $mapperConfig->scopes;
129
        $mapper->guards                  = $mapperConfig->guards;
130
        $mapper->tableReference          = QueryHelper::reference($mapper->table, $mapper->tableAlias);
131
132
        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...
133
            $mapper->relations = array_merge($mapper->relations, $mapperConfig->relations);
134
        }
135
136
        if ($mapperConfig->entityClass) {
137
            $mapper->entityClass = $mapperConfig->entityClass;
138
        }
139
140
        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...
141
            $mapper->use(...$mapperConfig->behaviours);
142
        }
143
144
        return $mapper;
145
    }
146
147
    public function __construct(Orm $orm, HydratorInterface $entityHydrator = null, QueryBuilder $queryBuilder = null)
148
    {
149
        $this->orm = $orm;
150
        if (! $entityHydrator) {
151
            $entityHydrator = new GenericEntityHydrator($orm, $this);
152
        }
153
        if (! $queryBuilder) {
154
            $this->queryBuilder = QueryBuilder::getInstance();
155
        }
156
        $this->entityHydrator = $entityHydrator;
157
        $this->tableReference = QueryHelper::reference($this->table, $this->tableAlias);
158
    }
159
160
    public function __call(string $method, array $params)
161
    {
162
        switch ($method) {
163
            case 'where':
164
            case 'columns':
165
            case 'orderBy':
166
                $query = $this->newQuery();
167
168
                return $query->{$method}(...$params);
169
        }
170
171
172
        throw new \BadMethodCallException('Unknown method {$method} for class ' . get_class($this));
173
    }
174
175
    /**
176
     * Add behaviours to the mapper
177
     *
178
     * @param mixed ...$behaviours
179
     */
180
    public function use(...$behaviours)
181
    {
182
        if (empty($behaviours)) {
183
            return;
184
        }
185
        foreach ($behaviours as $behaviour) {
186
            /** @var $behaviour BehaviourInterface */
187
            if (isset($this->behaviours[$behaviour->getName()])) {
188
                throw new \BadMethodCallException(
189
                    sprintf('Behaviour "%s" is already registered', $behaviour->getName())
190
                );
191
            }
192
            $this->behaviours[$behaviour->getName()] = $behaviour;
193
        }
194
    }
195
196
    public function without(...$behaviours)
197
    {
198
        if (empty($behaviours)) {
199
            return $this;
200
        }
201
        $mapper = clone $this;
202
        foreach ($behaviours as $behaviour) {
203
            unset($mapper->behaviours[$behaviour]);
204
        }
205
206
        return $mapper;
207
    }
208
209
    public function addQueryScope($scope, callable $callback)
210
    {
211
        $this->scopes[$scope] = $callback;
212
    }
213
214
    public function getQueryScope($scope)
215
    {
216
        return $this->scopes[$scope] ?? null;
217
    }
218
219
    public function registerCasts(CastingManager $castingManager)
220
    {
221
        $mapper = $this;
222
223
        $singular = Inflector::singularize($this->getTableAlias(true));
224
        $castingManager->register($singular, function ($value) use ($mapper, $castingManager) {
225
            if ($value instanceof $this->entityClass) {
226
                return $value;
227
            }
228
229
            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

229
            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...
230
        });
231
232
        $plural = $this->getTableAlias(true);
233
        $castingManager->register($plural, function ($values) use ($mapper, $castingManager) {
234
            if ($values instanceof Collection) {
235
                return $values;
236
            }
237
            $collection = new Collection();
238
            foreach ($values as $value) {
239
                $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

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

402
                $relation->attachMatchesToEntity($entity, /** @scrutinizer ignore-type */ $tracker->getRelationResults($name));
Loading history...
403
            } elseif ($relation->isLazyLoad()) {
404
                $trackerIdDisposable = false;
405
                $relation->attachLazyValueToEntity($entity, $tracker);
406
            }
407
        }
408
409
        $tracker->setDisposable($trackerIdDisposable);
410
    }
411
412
    protected function getEntityDefaults()
413
    {
414
        return $this->entityDefaultAttributes;
415
    }
416
417
    public function setEntityAttribute(EntityInterface $entity, $attribute, $value)
418
    {
419
        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

419
        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...
420
    }
421
422
    public function getEntityAttribute(EntityInterface $entity, $attribute)
423
    {
424
        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

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