Mapper::injectRelations()   B
last analyzed

Complexity

Conditions 7
Paths 7

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 10
c 2
b 0
f 0
dl 0
loc 15
rs 8.8333
cc 7
nc 7
nop 3
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\Behaviour\BehaviourInterface;
11
use Sirius\Orm\Collection\Collection;
12
use Sirius\Orm\Collection\PaginatedCollection;
13
use Sirius\Orm\Entity\Behaviours;
14
use Sirius\Orm\Entity\EntityInterface;
15
use Sirius\Orm\Entity\GenericEntity;
16
use Sirius\Orm\Entity\GenericEntityHydrator;
17
use Sirius\Orm\Entity\HydratorInterface;
18
use Sirius\Orm\Entity\StateEnum;
19
use Sirius\Orm\Entity\Tracker;
20
use Sirius\Orm\Helpers\Arr;
21
use Sirius\Orm\Helpers\Inflector;
22
use Sirius\Orm\Helpers\QueryHelper;
23
use Sirius\Orm\Relation\Aggregate;
24
use Sirius\Orm\Relation\Relation;
25
use Sirius\Orm\Relation\RelationConfig;
26
27
/**
28
 * @method array where($column, $value, $condition)
29
 * @method array columns(string $expr, string ...$exprs)
30
 * @method array orderBy(string $expr, string ...$exprs)
31
 */
32
class Mapper
33
{
34
    /**
35
     * Name of the class/interface to be used to determine
36
     * if this mapper can persist a specific entity
37
     * @var string
38
     */
39
    protected $entityClass = GenericEntity::class;
40
41
    /**
42
     * @var string|array
43
     */
44
    protected $primaryKey = 'id';
45
46
    /**
47
     * @var string
48
     */
49
    protected $table;
50
51
    /**
52
     * Used in queries like so: FROM table as tableAlias
53
     * This is especially useful if you are using prefixed tables
54
     * @var string
55
     */
56
    protected $tableAlias = '';
57
58
    /**
59
     * @var string
60
     */
61
    protected $tableReference;
62
63
    /**
64
     * Table columns
65
     * @var array
66
     */
67
    protected $columns = [];
68
69
    /**
70
     * Column casts
71
     * @var array
72
     */
73
    protected $casts = ['id' => 'int'];
74
75
    /**
76
     * Column aliases (table column => entity attribute)
77
     * @var array
78
     */
79
    protected $columnAttributeMap = [];
80
81
    /**
82
     * @var HydratorInterface
83
     */
84
    protected $entityHydrator;
85
86
    /**
87
     * Default attributes
88
     * @var array
89
     */
90
    protected $entityDefaultAttributes = [];
91
92
    /**
93
     * @var Behaviours
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 (!empty($mapperConfig->relations)) {
140
            $mapper->relations = array_merge($mapper->relations, $mapperConfig->relations);
141
        }
142
143
        if ($mapperConfig->entityClass) {
144
            $mapper->entityClass = $mapperConfig->entityClass;
145
        }
146
147
        if (isset($mapperConfig->behaviours) && ! empty($mapperConfig->behaviours)) {
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
        $this->behaviours = new Behaviours();
170
    }
171
172
    public function __call(string $method, array $params)
173
    {
174
        switch ($method) {
175
            case 'where':
176
            case 'where':
177
            case 'columns':
178
            case 'orderBy':
179
                $query = $this->newQuery();
180
181
                return $query->{$method}(...$params);
182
        }
183
184
185
        throw new \BadMethodCallException('Unknown method {$method} for class ' . get_class($this));
186
    }
187
188
    /**
189
     * Add behaviours to the mapper
190
     *
191
     * @param mixed ...$behaviours
192
     */
193
    public function use(...$behaviours)
194
    {
195
        foreach ($behaviours as $behaviour) {
196
            $this->behaviours->add($behaviour);
197
        }
198
    }
199
200
    public function without(...$behaviours)
201
    {
202
        $mapper = clone $this;
203
        $mapper->behaviours = $this->behaviours->without(...$behaviours);
204
205
        return $mapper;
206
    }
207
208
    public function addQueryScope($scope, callable $callback)
209
    {
210
        $this->scopes[$scope] = $callback;
211
    }
212
213
    public function getQueryScope($scope)
214
    {
215
        return $this->scopes[$scope] ?? null;
216
    }
217
218
    public function registerCasts(CastingManager $castingManager)
219
    {
220
        $mapper = $this;
221
222
        $singular = Inflector::singularize($this->getTableAlias(true));
223
        $castingManager->register($singular, function ($value) use ($mapper) {
224
            if ($value instanceof $this->entityClass) {
225
                return $value;
226
            }
227
228
            return $value !== null ? $mapper->newEntity($value) : null;
229
        });
230
231
        $plural = $this->getTableAlias(true);
232
        $castingManager->register($plural, function ($values) use ($mapper) {
233
            if ($values instanceof Collection) {
234
                return $values;
235
            }
236
            $collection = new Collection();
237
            if (is_array($values)) {
238
                foreach ($values as $value) {
239
                    $collection->add($mapper->newEntity($value));
240
                }
241
            }
242
243
            return $collection;
244
        });
245
    }
246
247
    /**
248
     * @return array|string
249
     */
250
    public function getPrimaryKey()
251
    {
252
        return $this->primaryKey;
253
    }
254
255
    /**
256
     * @return string
257
     */
258
    public function getTable(): string
259
    {
260
        return $this->table;
261
    }
262
263
    /**
264
     * @return string
265
     */
266
    public function getTableAlias($returnTableIfNull = false)
267
    {
268
        return (! $this->tableAlias && $returnTableIfNull) ? $this->table : $this->tableAlias;
269
    }
270
271
    public function getTableReference()
272
    {
273
        if (!$this->tableReference) {
274
            $this->tableReference = QueryHelper::reference($this->table, $this->tableAlias);
275
        }
276
277
        return $this->tableReference;
278
    }
279
280
    /**
281
     * @return array
282
     */
283
    public function getColumns(): array
284
    {
285
        return $this->columns;
286
    }
287
288
    /**
289
     * @return array
290
     */
291
    public function getColumnAttributeMap(): array
292
    {
293
        return $this->columnAttributeMap;
294
    }
295
296
    /**
297
     * @return string
298
     */
299
    public function getEntityClass(): string
300
    {
301
        return $this->entityClass;
302
    }
303
304
    /**
305
     * @return array
306
     */
307
    public function getGuards(): array
308
    {
309
        return $this->guards;
310
    }
311
312
    /**
313
     * @param $data
314
     *
315
     * @return EntityInterface
316
     */
317
    public function newEntity(array $data): EntityInterface
318
    {
319
        $entity = $this->entityHydrator->hydrate(array_merge($this->getEntityDefaults(), $data));
320
321
        return $this->behaviours->apply($this, __FUNCTION__, $entity);
322
    }
323
324
    public function extractFromEntity(EntityInterface $entity): array
325
    {
326
        $data = $this->entityHydrator->extract($entity);
327
328
        return $this->behaviours->apply($this, __FUNCTION__, $data);
329
    }
330
331
    public function newEntityFromRow(array $data = null, array $load = [], Tracker $tracker = null)
332
    {
333
        if ($data == null) {
334
            return null;
335
        }
336
337
        $receivedTracker = ! ! $tracker;
338
        if (! $tracker) {
339
            $receivedTracker = false;
340
            $tracker         = new Tracker([$data]);
341
        }
342
343
        $entity = $this->newEntity($data);
344
        $this->injectRelations($entity, $tracker, $load);
345
        $this->injectAggregates($entity, $tracker, $load);
346
        $entity->setPersistenceState(StateEnum::SYNCHRONIZED);
347
348
        if (! $receivedTracker) {
349
            $tracker->replaceRows([$entity]);
350
        }
351
352
        return $entity;
353
    }
354
355
    public function newCollectionFromRows(array $rows, array $load = []): Collection
356
    {
357
        $entities = [];
358
        $tracker  = new Tracker($rows);
359
        foreach ($rows as $row) {
360
            $entity     = $this->newEntityFromRow($row, $load, $tracker);
361
            $entities[] = $entity;
362
        }
363
        $tracker->replaceRows($entities);
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($rows);
377
        foreach ($rows as $row) {
378
            $entity     = $this->newEntityFromRow($row, $load, $tracker);
379
            $entities[] = $entity;
380
        }
381
        $tracker->replaceRows($entities);
382
383
        return new PaginatedCollection($entities, $totalCount, $perPage, $currentPage);
384
    }
385
386
    protected function injectRelations(EntityInterface $entity, Tracker $tracker, array $eagerLoad = [])
387
    {
388
        foreach (array_keys($this->relations) as $name) {
389
            $relation      = $this->getRelation($name);
390
            $queryCallback = $eagerLoad[$name] ?? null;
391
            $nextLoad      = Arr::getChildren($eagerLoad, $name);
392
393
            if (! $tracker->hasRelation($name)) {
394
                $tracker->setRelation($name, $relation, $queryCallback, $nextLoad);
395
            }
396
397
            if (array_key_exists($name, $eagerLoad) || in_array($name, $eagerLoad) || $relation->isEagerLoad()) {
398
                $relation->attachMatchesToEntity($entity, $tracker->getResultsForRelation($name));
399
            } elseif ($relation->isLazyLoad()) {
400
                $relation->attachLazyRelationToEntity($entity, $tracker);
401
            }
402
        }
403
    }
404
405
    protected function injectAggregates(EntityInterface $entity, Tracker $tracker, array $eagerLoad = [])
406
    {
407
        foreach (array_keys($this->relations) as $name) {
408
            $relation      = $this->getRelation($name);
409
            if (!method_exists($relation, 'getAggregates')) {
410
                continue;
411
            }
412
            $aggregates = $relation->getAggregates();
413
            foreach ($aggregates as $aggName => $aggregate) {
414
                /** @var $aggregate Aggregate */
415
                if (array_key_exists($aggName, $eagerLoad) || $aggregate->isEagerLoad()) {
416
                    $aggregate->attachAggregateToEntity($entity, $tracker->getAggregateResults($aggregate));
417
                } elseif ($aggregate->isLazyLoad()) {
418
                    $aggregate->attachLazyAggregateToEntity($entity, $tracker);
419
                }
420
            }
421
        }
422
    }
423
424
    protected function getEntityDefaults()
425
    {
426
        return $this->entityDefaultAttributes;
427
    }
428
429
    public function setEntityAttribute(EntityInterface $entity, $attribute, $value)
430
    {
431
        return $this->entityHydrator->set($entity, $attribute, $value);
432
    }
433
434
    public function getEntityAttribute(EntityInterface $entity, $attribute)
435
    {
436
        return $this->entityHydrator->get($entity, $attribute);
437
    }
438
439
    public function setEntityPk(EntityInterface $entity, $value)
440
    {
441
        if (is_array($this->primaryKey)) {
442
            foreach ($this->primaryKey as $k => $col) {
443
                $this->entityHydrator->set($entity, $col, $value[$k]);
444
            }
445
        }
446
        return $this->entityHydrator->set($entity, $this->primaryKey, $value);
447
    }
448
449
    public function getEntityPk(EntityInterface $entity)
450
    {
451
        if (is_array($this->primaryKey)) {
452
            $result = [];
453
            foreach ($this->primaryKey as $col) {
454
                $result[] = $this->entityHydrator->get($entity, $col);
455
            }
456
457
            return $result;
458
        }
459
        return $this->entityHydrator->get($entity, $this->primaryKey);
460
    }
461
462
    public function addRelation($name, $relation)
463
    {
464
        if (is_array($relation) || $relation instanceof Relation) {
465
            $this->relations[$name] = $relation;
466
            return;
467
        }
468
        throw new \InvalidArgumentException(
469
            sprintf('The relation has to be an Relation instance or an array of configuration options')
470
        );
471
    }
472
473
    public function hasRelation($name): bool
474
    {
475
        return isset($this->relations[$name]);
476
    }
477
478
    public function getRelation($name): Relation
479
    {
480
        if (! $this->hasRelation($name)) {
481
            throw new \InvalidArgumentException("Relation named {$name} is not registered for this mapper");
482
        }
483
484
        if (is_array($this->relations[$name])) {
485
            $this->relations[$name] = $this->orm->createRelation($this, $name, $this->relations[$name]);
486
        }
487
        $relation = $this->relations[$name];
488
        if (! $relation instanceof Relation) {
489
            throw new \InvalidArgumentException("Relation named {$name} is not a proper Relation instance");
490
        }
491
492
        return $relation;
493
    }
494
495
    public function getRelations(): array
496
    {
497
        return array_keys($this->relations);
498
    }
499
500
    public function newQuery(): Query
501
    {
502
        $query = $this->queryBuilder->newQuery($this);
503
504
        return $this->behaviours->apply($this, __FUNCTION__, $query);
505
    }
506
507
    public function find($pk, array $load = [])
508
    {
509
        return $this->newQuery()
510
                    ->where($this->getPrimaryKey(), $pk)
511
                    ->load(...$load)
512
                    ->first();
513
    }
514
515
    /**
516
     * @param EntityInterface $entity
517
     *
518
     * @return bool
519
     * @throws \Exception
520
     */
521
    public function save(EntityInterface $entity, $withRelations = true)
522
    {
523
        $this->assertCanPersistEntity($entity);
524
        $action = $this->newSaveAction($entity, ['relations' => $withRelations]);
525
526
        $this->orm->getConnectionLocator()->lockToWrite(true);
527
        $this->getWriteConnection()->beginTransaction();
528
        try {
529
            $action->run();
530
            $this->getWriteConnection()->commit();
531
            $this->orm->getConnectionLocator()->lockToWrite(false);
532
            return true;
533
        } catch (\Exception $e) {
534
            $this->getWriteConnection()->rollBack();
535
            $this->orm->getConnectionLocator()->lockToWrite(false);
536
            throw $e;
537
        }
538
    }
539
540
    public function newSaveAction(EntityInterface $entity, $options): Update
541
    {
542
        if (! $this->getEntityAttribute($entity, $this->primaryKey)) {
543
            $action = new Insert($this, $entity, $options);
544
        } else {
545
            $action = new Update($this, $entity, $options);
546
        }
547
548
        return $this->behaviours->apply($this, 'save', $action);
549
    }
550
551
    public function delete(EntityInterface $entity, $withRelations = true)
552
    {
553
        $this->assertCanPersistEntity($entity);
554
555
        $action = $this->newDeleteAction($entity, ['relations' => $withRelations]);
556
557
        $this->orm->getConnectionLocator()->lockToWrite(true);
558
        $this->getWriteConnection()->beginTransaction();
559
        try {
560
            $action->run();
561
            $this->getWriteConnection()->commit();
562
563
            return true;
564
        } catch (\Exception $e) {
565
            $this->getWriteConnection()->rollBack();
566
            throw $e;
567
        }
568
    }
569
570
    public function newDeleteAction(EntityInterface $entity, $options)
571
    {
572
        $action = new Delete($this, $entity, $options);
573
574
        return $this->behaviours->apply($this, 'delete', $action);
575
    }
576
577
    protected function assertCanPersistEntity($entity)
578
    {
579
        if (! $entity || ! $entity instanceof $this->entityClass) {
580
            throw new \InvalidArgumentException(sprintf(
581
                'Mapper %s can only persist entity of class %s. %s class provided',
582
                __CLASS__,
583
                $this->entityClass,
584
                get_class($entity)
585
            ));
586
        }
587
    }
588
589
    public function getReadConnection()
590
    {
591
        return $this->orm->getConnectionLocator()->getRead();
592
    }
593
594
    public function getWriteConnection()
595
    {
596
        return $this->orm->getConnectionLocator()->getWrite();
597
    }
598
599
    public function getCasts()
600
    {
601
        return $this->casts;
602
    }
603
}
604