Completed
Push — master ( 7c9133...99ee33 )
by Adrian
02:03
created

Relation   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 319
Duplicated Lines 0 %

Importance

Changes 6
Bugs 1 Features 0
Metric Value
wmc 50
eloc 108
c 6
b 1
f 0
dl 0
loc 319
rs 8.4

25 Methods

Rating   Name   Duplication   Size   Complexity  
A getOption() 0 7 2
A isEagerLoad() 0 3 1
A entitiesBelongTogether() 0 16 5
A setOptionIfMissing() 0 4 2
A __construct() 0 8 1
A applyDefaults() 0 4 1
A isCascade() 0 3 1
A getKeyPairs() 0 3 1
A isLazyLoad() 0 3 1
A addActions() 0 6 4
A applyForeignGuards() 0 8 2
A getEntityId() 0 7 2
A getRemainingRelations() 0 14 3
A computeKeyPairs() 0 10 2
A applyQueryCallback() 0 8 3
A newSyncAction() 0 16 2
A getExtraColumnsForAction() 0 14 4
A getJoinOnForSubselect() 0 7 1
A getKeyColumn() 0 12 3
A indexQueryResults() 0 13 3
A relationWasChanged() 0 5 2
A attachLazyRelationToEntity() 0 4 1
A getNativeMapper() 0 3 1
A getForeignMapper() 0 3 1
A getQuery() 0 14 1

How to fix   Complexity   

Complex Class

Complex classes like Relation often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Relation, and based on these observations, apply Extract Interface, too.

1
<?php
2
declare(strict_types=1);
3
4
namespace Sirius\Orm\Relation;
5
6
use Sirius\Orm\Action\AttachEntities;
7
use Sirius\Orm\Action\BaseAction;
8
use Sirius\Orm\Action\Delete;
9
use Sirius\Orm\Action\DetachEntities;
10
use Sirius\Orm\Action\Update;
11
use Sirius\Orm\Entity\EntityInterface;
12
use Sirius\Orm\Entity\LazyRelation;
13
use Sirius\Orm\Entity\Tracker;
14
use Sirius\Orm\Helpers\Arr;
15
use Sirius\Orm\Helpers\QueryHelper;
16
use Sirius\Orm\Mapper;
17
use Sirius\Orm\Query;
18
use Sirius\Sql\Select;
19
20
abstract class Relation
21
{
22
    /**
23
     * Name of the relation (used to infer defaults)
24
     * @var
25
     */
26
    protected $name;
27
28
    /**
29
     * @var Mapper
30
     */
31
    protected $nativeMapper;
32
33
    /**
34
     * @var Mapper
35
     */
36
    protected $foreignMapper;
37
38
    /**
39
     * @var array
40
     */
41
    protected $options = [];
42
43
    /**
44
     * @var array
45
     */
46
    protected $keyPairs;
47
48
    public function __construct($name, Mapper $nativeMapper, Mapper $foreignMapper, array $options = [])
49
    {
50
        $this->name          = $name;
51
        $this->nativeMapper  = $nativeMapper;
52
        $this->foreignMapper = $foreignMapper;
53
        $this->options       = $options;
54
        $this->applyDefaults();
55
        $this->keyPairs = $this->computeKeyPairs();
56
    }
57
58
    protected function applyDefaults(): void
59
    {
60
        $this->setOptionIfMissing(RelationConfig::LOAD_STRATEGY, RelationConfig::LOAD_LAZY);
61
        $this->setOptionIfMissing(RelationConfig::CASCADE, false);
62
    }
63
64
    protected function setOptionIfMissing($name, $value)
65
    {
66
        if (! isset($this->options[$name])) {
67
            $this->options[$name] = $value;
68
        }
69
    }
70
71
    public function getOption($name)
72
    {
73
        if ($name == 'name') {
74
            return $this->name;
75
        }
76
77
        return $this->options[$name] ?? null;
78
    }
79
80
    /**
81
     * @return array
82
     */
83
    public function getKeyPairs(): array
84
    {
85
        return $this->keyPairs;
86
    }
87
88
    /**
89
     * Checks if a native entity belongs and a foreign entity belong together according to this relation
90
     * It verifies if the attributes are properly linked
91
     *
92
     * @param EntityInterface $nativeEntity
93
     * @param EntityInterface $foreignEntity
94
     *
95
     * @return mixed
96
     */
97
    public function entitiesBelongTogether(EntityInterface $nativeEntity, EntityInterface $foreignEntity)
98
    {
99
        /**
100
         * @todo make this method protected
101
         */
102
        foreach ($this->keyPairs as $nativeCol => $foreignCol) {
103
            $nativeKeyValue  = $this->nativeMapper->getEntityAttribute($nativeEntity, $nativeCol);
104
            $foreignKeyValue = $this->foreignMapper->getEntityAttribute($foreignEntity, $foreignCol);
105
            // if both native and foreign key values are present (not unlinked entities) they must be the same
106
            // otherwise we assume that the entities can be linked together
107
            if ($nativeKeyValue && $foreignKeyValue && $nativeKeyValue != $foreignKeyValue) {
108
                return false;
109
            }
110
        }
111
112
        return true;
113
    }
114
115
    public function isEagerLoad()
116
    {
117
        return $this->options[RelationConfig::LOAD_STRATEGY] == RelationConfig::LOAD_EAGER;
118
    }
119
120
    public function isLazyLoad()
121
    {
122
        return $this->options[RelationConfig::LOAD_STRATEGY] == RelationConfig::LOAD_LAZY;
123
    }
124
125
    public function isCascade()
126
    {
127
        return $this->options[RelationConfig::CASCADE] === true;
128
    }
129
130
    protected function getKeyColumn($name, $column)
131
    {
132
        if (!is_array($column)) {
133
            return $name . '_' . $column;
134
        }
135
136
        $keyColumn = [];
137
        foreach ($column as $col) {
138
            $keyColumn[] = $name . '_' . $col;
139
        }
140
141
        return $keyColumn;
142
    }
143
144
    public function addActions(BaseAction $action)
145
    {
146
        if ($action instanceof Delete) {
147
            $this->addActionOnDelete($action);
148
        } elseif ($action instanceof Insert || $action instanceof Update) {
0 ignored issues
show
Bug introduced by
The type Sirius\Orm\Relation\Insert 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...
149
            $this->addActionOnSave($action);
150
        }
151
    }
152
153
    abstract protected function addActionOnSave(BaseAction $action);
154
155
    abstract protected function addActionOnDelete(BaseAction $action);
156
157
    abstract public function attachMatchesToEntity(EntityInterface $nativeEntity, array $queryResult);
158
159
    abstract public function attachEntities(EntityInterface $nativeEntity, EntityInterface $foreignEntity);
160
161
    abstract public function detachEntities(EntityInterface $nativeEntity, EntityInterface $foreignEntity);
162
163
    abstract public function joinSubselect(Query $query, string $reference);
164
165
    public function attachLazyRelationToEntity(EntityInterface $entity, Tracker $tracker)
166
    {
167
        $valueLoader = new LazyRelation($entity, $tracker, $this);
168
        $this->nativeMapper->setEntityAttribute($entity, $this->name, $valueLoader);
169
    }
170
171
    public function getQuery(Tracker $tracker)
172
    {
173
        $nativeKey = $this->options[RelationConfig::NATIVE_KEY];
174
        $nativePks = $tracker->pluck($nativeKey);
175
176
        $query = $this->foreignMapper
177
            ->newQuery()
178
            ->where($this->foreignMapper->getPrimaryKey(), $nativePks);
179
180
        $query = $this->applyQueryCallback($query);
181
182
        $query = $this->applyForeignGuards($query);
183
184
        return $query;
185
    }
186
187
    public function indexQueryResults(array $entities)
188
    {
189
        $result = [];
190
191
        foreach ($entities as $entity) {
192
            $entityId = $this->getEntityId($this->foreignMapper, $entity, array_values($this->keyPairs));
193
            if (!isset($result[$entityId])) {
194
                $result[$entityId] = [];
195
            }
196
            $result[$entityId][] = $entity;
197
        }
198
199
        return $result;
200
    }
201
202
    protected function getEntityId(Mapper $mapper, EntityInterface $entity, array $keyColumns)
203
    {
204
        $entityKeys = [];
205
        foreach ($keyColumns as $col) {
206
            $entityKeys[] = $mapper->getEntityAttribute($entity, $col);
207
        }
208
        return implode('-', $entityKeys);
209
    }
210
211
    /**
212
     * Method used by `entitiesBelongTogether` to check
213
     * if a foreign entity belongs to the native entity
214
     * @return array
215
     */
216
    protected function computeKeyPairs()
217
    {
218
        $pairs      = [];
219
        $nativeKey  = (array)$this->options[RelationConfig::NATIVE_KEY];
220
        $foreignKey = (array)$this->options[RelationConfig::FOREIGN_KEY];
221
        foreach ($nativeKey as $k => $v) {
222
            $pairs[$v] = $foreignKey[$k];
223
        }
224
225
        return $pairs;
226
    }
227
228
    /**
229
     * Computes the $withRelations value to be passed on to the next related entities
230
     * If an entity receives on delete/save $withRelations = ['category', 'category.images']
231
     * the related 'category' is saved with $withRelations = ['images']
232
     *
233
     * @param $relations
234
     *
235
     * @return array
236
     */
237
    protected function getRemainingRelations($relations)
238
    {
239
        if (! is_array($relations)) {
240
            return $relations;
241
        }
242
243
        $arr = array_combine($relations, $relations);
244
        if (is_array($arr)) {
245
            $children = Arr::getChildren($arr, $this->name);
246
247
            return array_keys($children);
248
        }
249
250
        return [];
251
    }
252
253
    protected function getExtraColumnsForAction()
254
    {
255
        $cols   = [];
256
        $guards = $this->getOption(RelationConfig::FOREIGN_GUARDS);
257
        if (is_array($guards)) {
258
            foreach ($guards as $col => $val) {
259
                // guards that are strings (eg: 'deleted_at is null') can't be used as extra columns
260
                if (! is_int($col)) {
261
                    $cols[$col] = $val;
262
                }
263
            }
264
        }
265
266
        return $cols;
267
    }
268
269
    protected function newSyncAction(EntityInterface $nativeEntity, EntityInterface $foreignEntity, string $actionType)
270
    {
271
        if ($actionType == 'delete') {
272
            return new DetachEntities(
273
                $nativeEntity,
274
                $foreignEntity,
275
                $this,
276
                'save'
277
            );
278
        }
279
280
        return new AttachEntities(
281
            $nativeEntity,
282
            $foreignEntity,
283
            $this,
284
            'save'
285
        );
286
    }
287
288
    protected function relationWasChanged(EntityInterface $entity)
289
    {
290
        $changes = $entity->getChanges();
291
292
        return isset($changes[$this->name]) && $changes[$this->name];
293
    }
294
295
    protected function applyQueryCallback(Select $query)
296
    {
297
        $queryCallback = $this->getOption(RelationConfig::QUERY_CALLBACK);
298
        if ($queryCallback && is_callable($queryCallback)) {
299
            $query = $queryCallback($query);
300
        }
301
302
        return $query;
303
    }
304
305
    protected function applyForeignGuards(Select $query)
306
    {
307
        $guards = $this->getOption(RelationConfig::FOREIGN_GUARDS);
308
        if ($guards) {
309
            $query->setGuards($guards);
310
        }
311
312
        return $query;
313
    }
314
315
    protected function getJoinOnForSubselect()
316
    {
317
        return QueryHelper::joinCondition(
318
            $this->nativeMapper->getTableAlias(true),
319
            $this->getOption(RelationConfig::NATIVE_KEY),
320
            $this->name,
321
            $this->getOption(RelationConfig::FOREIGN_KEY)
322
        );
323
    }
324
325
    /**
326
     * @return Mapper
327
     */
328
    public function getNativeMapper(): Mapper
329
    {
330
        return $this->nativeMapper;
331
    }
332
333
    /**
334
     * @return Mapper
335
     */
336
    public function getForeignMapper(): Mapper
337
    {
338
        return $this->foreignMapper;
339
    }
340
}
341