Completed
Push — master ( 89a77f...e0a427 )
by Adrian
01:46
created

Relation   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 328
Duplicated Lines 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
wmc 53
eloc 111
c 3
b 1
f 0
dl 0
loc 328
rs 6.96

26 Methods

Rating   Name   Duplication   Size   Complexity  
A getOption() 0 7 2
A setOptionIfMissing() 0 4 2
A applyDefaults() 0 4 1
A __construct() 0 8 1
A addActions() 0 10 5
A isEagerLoad() 0 3 1
A entitiesBelongTogether() 0 16 5
A getKeyColumn() 0 12 3
A isCascade() 0 3 1
A attachLazyRelationToEntity() 0 4 1
A getQuery() 0 14 1
A getKeyPairs() 0 3 1
A isLazyLoad() 0 3 1
A applyForeignGuards() 0 8 2
A getEntityId() 0 7 2
A getRemainingRelations() 0 9 2
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 cascadeIsAllowedForAction() 0 8 3
A indexQueryResults() 0 13 3
A relationWasChanged() 0 5 2
A getNativeMapper() 0 3 1
A getForeignMapper() 0 3 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\LazyLoader;
0 ignored issues
show
Bug introduced by
The type Sirius\Orm\LazyLoader 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...
17
use Sirius\Orm\Mapper;
18
use Sirius\Orm\Query;
19
use Sirius\Sql\Select;
20
21
abstract class Relation
22
{
23
    /**
24
     * Name of the relation (used to infer defaults)
25
     * @var
26
     */
27
    protected $name;
28
29
    /**
30
     * @var Mapper
31
     */
32
    protected $nativeMapper;
33
34
    /**
35
     * @var Mapper
36
     */
37
    protected $foreignMapper;
38
39
    /**
40
     * @var array
41
     */
42
    protected $options = [];
43
44
    /**
45
     * @var array
46
     */
47
    protected $keyPairs;
48
49
    public function __construct($name, Mapper $nativeMapper, Mapper $foreignMapper, array $options = [])
50
    {
51
        $this->name          = $name;
52
        $this->nativeMapper  = $nativeMapper;
53
        $this->foreignMapper = $foreignMapper;
54
        $this->options       = $options;
55
        $this->applyDefaults();
56
        $this->keyPairs = $this->computeKeyPairs();
57
    }
58
59
    protected function applyDefaults(): void
60
    {
61
        $this->setOptionIfMissing(RelationConfig::LOAD_STRATEGY, RelationConfig::LOAD_LAZY);
62
        $this->setOptionIfMissing(RelationConfig::CASCADE, false);
63
    }
64
65
    protected function setOptionIfMissing($name, $value)
66
    {
67
        if (! isset($this->options[$name])) {
68
            $this->options[$name] = $value;
69
        }
70
    }
71
72
    public function getOption($name)
73
    {
74
        if ($name == 'name') {
75
            return $this->name;
76
        }
77
78
        return $this->options[$name] ?? null;
79
    }
80
81
    /**
82
     * @return array
83
     */
84
    public function getKeyPairs(): array
85
    {
86
        return $this->keyPairs;
87
    }
88
89
    /**
90
     * Checks if a native entity belongs and a foreign entity belong together according to this relation
91
     * It verifies if the attributes are properly linked
92
     *
93
     * @param EntityInterface $nativeEntity
94
     * @param EntityInterface $foreignEntity
95
     *
96
     * @return mixed
97
     */
98
    public function entitiesBelongTogether(EntityInterface $nativeEntity, EntityInterface $foreignEntity)
99
    {
100
        /**
101
         * @todo make this method protected
102
         */
103
        foreach ($this->keyPairs as $nativeCol => $foreignCol) {
104
            $nativeKeyValue  = $this->nativeMapper->getEntityAttribute($nativeEntity, $nativeCol);
105
            $foreignKeyValue = $this->foreignMapper->getEntityAttribute($foreignEntity, $foreignCol);
106
            // if both native and foreign key values are present (not unlinked entities) they must be the same
107
            // otherwise we assume that the entities can be linked together
108
            if ($nativeKeyValue && $foreignKeyValue && $nativeKeyValue != $foreignKeyValue) {
109
                return false;
110
            }
111
        }
112
113
        return true;
114
    }
115
116
    public function isEagerLoad()
117
    {
118
        return $this->options[RelationConfig::LOAD_STRATEGY] == RelationConfig::LOAD_EAGER;
119
    }
120
121
    public function isLazyLoad()
122
    {
123
        return $this->options[RelationConfig::LOAD_STRATEGY] == RelationConfig::LOAD_LAZY;
124
    }
125
126
    public function isCascade()
127
    {
128
        return $this->options[RelationConfig::CASCADE] === true;
129
    }
130
131
    protected function getKeyColumn($name, $column)
132
    {
133
        if (is_array($column)) {
134
            $keyColumn = [];
135
            foreach ($column as $col) {
136
                $keyColumn[] = $name . '_' . $col;
137
            }
138
139
            return $keyColumn;
140
        }
141
142
        return $name . '_' . $column;
143
    }
144
145
    public function addActions(BaseAction $action)
146
    {
147
        if (! $this->cascadeIsAllowedForAction($action)) {
148
            return;
149
        }
150
151
        if ($action instanceof Delete) {
152
            $this->addActionOnDelete($action);
0 ignored issues
show
Bug introduced by
The method addActionOnDelete() does not exist on Sirius\Orm\Relation\Relation. Did you maybe mean addActions()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

152
            $this->/** @scrutinizer ignore-call */ 
153
                   addActionOnDelete($action);

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...
153
        } 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...
154
            $this->addActionOnSave($action);
0 ignored issues
show
Bug introduced by
The method addActionOnSave() does not exist on Sirius\Orm\Relation\Relation. Did you maybe mean addActions()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

154
            $this->/** @scrutinizer ignore-call */ 
155
                   addActionOnSave($action);

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...
155
        }
156
    }
157
158
    abstract public function attachMatchesToEntity(EntityInterface $nativeEntity, array $queryResult);
159
160
    abstract public function detachEntities(EntityInterface $nativeEntity, EntityInterface $foreignEntity);
161
162
    abstract public function joinSubselect(Query $query, string $reference);
163
164
    public function attachLazyRelationToEntity(EntityInterface $entity, Tracker $tracker)
165
    {
166
        $valueLoader = new LazyRelation($entity, $tracker, $this);
167
        $this->nativeMapper->setEntityAttribute($entity, $this->name, $valueLoader);
168
    }
169
170
    public function getQuery(Tracker $tracker)
171
    {
172
        $nativeKey = $this->options[RelationConfig::NATIVE_KEY];
173
        $nativePks = $tracker->pluck($nativeKey);
174
175
        $query = $this->foreignMapper
176
            ->newQuery()
177
            ->where($this->foreignMapper->getPrimaryKey(), $nativePks);
178
179
        $query = $this->applyQueryCallback($query);
180
181
        $query = $this->applyForeignGuards($query);
182
183
        return $query;
184
    }
185
186
    public function indexQueryResults(array $entities)
187
    {
188
        $result = [];
189
190
        foreach ($entities as $entity) {
191
            $entityId = $this->getEntityId($this->foreignMapper, $entity, array_values($this->keyPairs));
192
            if (!isset($result[$entityId])) {
193
                $result[$entityId] = [];
194
            }
195
            $result[$entityId][] = $entity;
196
        }
197
198
        return $result;
199
    }
200
201
    protected function getEntityId(Mapper $mapper, EntityInterface $entity, array $keyColumns)
202
    {
203
        $entityKeys = [];
204
        foreach ($keyColumns as $col) {
205
            $entityKeys[] = $mapper->getEntityAttribute($entity, $col);
206
        }
207
        return implode('-', $entityKeys);
208
    }
209
210
    /**
211
     * Method used by `entitiesBelongTogether` to check
212
     * if a foreign entity belongs to the native entity
213
     * @return array
214
     */
215
    protected function computeKeyPairs()
216
    {
217
        $pairs      = [];
218
        $nativeKey  = (array)$this->options[RelationConfig::NATIVE_KEY];
219
        $foreignKey = (array)$this->options[RelationConfig::FOREIGN_KEY];
220
        foreach ($nativeKey as $k => $v) {
221
            $pairs[$v] = $foreignKey[$k];
222
        }
223
224
        return $pairs;
225
    }
226
227
    /**
228
     * @param BaseAction $action
229
     *
230
     * @return bool|mixed|null
231
     * @see BaseAction::$options
232
     */
233
    protected function cascadeIsAllowedForAction(BaseAction $action)
234
    {
235
        $relations = $action->getOption('relations');
236
        if (is_array($relations) && ! in_array($this->name, $relations)) {
237
            return false;
238
        }
239
240
        return $relations;
241
    }
242
243
    /**
244
     * Computes the $withRelations value to be passed on to the next related entities
245
     * If an entity receives on delete/save $withRelations = ['category', 'category.images']
246
     * the related 'category' is saved with $withRelations = ['images']
247
     *
248
     * @param $relations
249
     *
250
     * @return array
251
     */
252
    protected function getRemainingRelations($relations)
253
    {
254
        if (! is_array($relations)) {
255
            return $relations;
256
        }
257
258
        $children = Arr::getChildren(array_combine($relations, $relations), $this->name);
0 ignored issues
show
Bug introduced by
It seems like array_combine($relations, $relations) can also be of type false; however, parameter $arr of Sirius\Orm\Helpers\Arr::getChildren() 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

258
        $children = Arr::getChildren(/** @scrutinizer ignore-type */ array_combine($relations, $relations), $this->name);
Loading history...
259
260
        return array_keys($children);
261
    }
262
263
    protected function getExtraColumnsForAction()
264
    {
265
        $cols   = [];
266
        $guards = $this->getOption(RelationConfig::FOREIGN_GUARDS);
267
        if (is_array($guards)) {
268
            foreach ($guards as $col => $val) {
269
                // guards that are strings (eg: 'deleted_at is null') can't be used as extra columns
270
                if (! is_int($col)) {
271
                    $cols[$col] = $val;
272
                }
273
            }
274
        }
275
276
        return $cols;
277
    }
278
279
    protected function newSyncAction(EntityInterface $nativeEntity, EntityInterface $foreignEntity, string $actionType)
280
    {
281
        if ($actionType == 'delete') {
282
            return new DetachEntities(
283
                $nativeEntity,
284
                $foreignEntity,
285
                $this,
286
                'save'
287
            );
288
        }
289
290
        return new AttachEntities(
291
            $nativeEntity,
292
            $foreignEntity,
293
            $this,
294
            'save'
295
        );
296
    }
297
298
    protected function relationWasChanged(EntityInterface $entity)
299
    {
300
        $changes = $entity->getChanges();
301
302
        return isset($changes[$this->name]) && $changes[$this->name];
303
    }
304
305
    protected function applyQueryCallback(Select $query)
306
    {
307
        $queryCallback = $this->getOption(RelationConfig::QUERY_CALLBACK);
308
        if ($queryCallback && is_callable($queryCallback)) {
309
            $query = $queryCallback($query);
310
        }
311
312
        return $query;
313
    }
314
315
    protected function applyForeignGuards(Select $query)
316
    {
317
        $guards = $this->getOption(RelationConfig::FOREIGN_GUARDS);
318
        if ($guards) {
319
            $query->setGuards($guards);
320
        }
321
322
        return $query;
323
    }
324
325
    protected function getJoinOnForSubselect()
326
    {
327
        return QueryHelper::joinCondition(
328
            $this->nativeMapper->getTableAlias(true),
329
            $this->getOption(RelationConfig::NATIVE_KEY),
330
            $this->name,
331
            $this->getOption(RelationConfig::FOREIGN_KEY)
332
        );
333
    }
334
335
    /**
336
     * @return Mapper
337
     */
338
    public function getNativeMapper(): Mapper
339
    {
340
        return $this->nativeMapper;
341
    }
342
343
    /**
344
     * @return Mapper
345
     */
346
    public function getForeignMapper(): Mapper
347
    {
348
        return $this->foreignMapper;
349
    }
350
}
351