AbstractLoader   F
last analyzed

Complexity

Total Complexity 66

Size/Duplication

Total Lines 384
Duplicated Lines 0 %

Test Coverage

Coverage 96.27%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 145
c 2
b 1
f 0
dl 0
loc 384
ccs 129
cts 134
cp 0.9627
rs 3.12
wmc 66

21 Methods

Rating   Name   Duplication   Size   Complexity  
B configureQuery() 0 27 9
A loadHierarchy() 0 22 6
A generateSublassLoaders() 0 6 3
A generateEagerRelationLoaders() 0 6 3
A getEagerLoaders() 0 9 2
A loadIerarchy() 0 4 1
A loadChild() 0 6 2
A define() 0 3 1
A generateParentLoader() 0 6 2
A getTarget() 0 3 1
A __clone() 0 16 4
A getSource() 0 3 1
A __destruct() 0 3 1
A __construct() 0 8 1
A loadData() 0 3 1
A getParentLoader() 0 3 1
C loadRelation() 0 75 14
A setSubclassesLoading() 0 3 1
B createNode() 0 24 7
A isHierarchical() 0 3 3
A withContext() 0 18 2

How to fix   Complexity   

Complex Class

Complex classes like AbstractLoader 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 AbstractLoader, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Cycle\ORM\Select;
6
7
use Cycle\Database\Query\SelectQuery;
8
use Cycle\ORM\Exception\FactoryException;
9
use Cycle\ORM\Exception\LoaderException;
10
use Cycle\ORM\Exception\SchemaException;
11
use Cycle\ORM\FactoryInterface;
12
use Cycle\ORM\Parser\AbstractNode;
13
use Cycle\ORM\Service\SourceProviderInterface;
14
use Cycle\ORM\Relation;
15
use Cycle\ORM\SchemaInterface;
16
use Cycle\ORM\Select\Loader\ParentLoader;
17
use Cycle\ORM\Select\Loader\SubclassLoader;
18
use Cycle\ORM\Select\Traits\AliasTrait;
19
use Cycle\ORM\Select\Traits\ChainTrait;
20
use JetBrains\PhpStorm\Deprecated;
21
22
/**
23
 * ORM Loaders used to load an compile data tree based on results fetched from SQL databases,
24
 * loaders can communicate with SelectQuery by providing it's own set of conditions, columns
25
 * joins and etc. In some cases loader may create additional selector to load data using information
26
 * fetched from previous query.
27
 *
28
 * Attention, AbstractLoader can only work with ORM Records, you must implement LoaderInterface
29
 * in order to support external references (MongoDB and etc).
30
 *
31
 * Loaders can be used for both - loading and filtering of record data.
32
 *
33
 * Reference tree generation logic example:
34
 *   User has many Posts (relation "posts"), user primary is ID, post inner key pointing to user
35
 *   is USER_ID. Post loader must request User data loader to create references based on ID field
36
 *   values. Once Post data were parsed we can mount it under parent user using mount method:
37
 *
38
 * @see Select::load()
39
 * @see Select::with()
40
 *
41
 * @internal
42
 */
43
abstract class AbstractLoader implements LoaderInterface
44
{
45
    use AliasTrait;
46
    use ChainTrait;
47
48
    // Loading methods for data loaders.
49
    public const INLOAD = 1;
50
    public const POSTLOAD = 2;
51
    public const JOIN = 3;
52
    public const LEFT_JOIN = 4;
53
    protected const SUBQUERY = 5;
54
55
    protected array $options = [
56
        'load' => false,
57
        'scope' => true,
58
    ];
59
60
    /** @var LoaderInterface[] */
61
    protected array $load = [];
62
63
    /** @var AbstractLoader[] */
64
    protected array $join = [];
65
66
    /**
67
     * Parent in class inheritance hierarchy
68
     */
69
    protected ?AbstractLoader $inherit = null;
70
71
    /** @var SubclassLoader[] */
72
    protected array $subclasses = [];
73
74
    protected bool $loadSubclasses = true;
75
76
    /**
77
     * Loader that contains current loader
78
     */
79
    protected ?LoaderInterface $parent = null;
80
81
    /**
82
     * Children roles for Joined Table Inheritance
83
     *
84
     * @var array<string, array>
85
     */
86
    protected array $children;
87
88 6834
    protected SourceInterface $source;
89
90
    public function __construct(
91
        protected SchemaInterface $ormSchema,
92
        protected SourceProviderInterface $sourceProvider,
93
        protected FactoryInterface $factory,
94 6834
        protected string $target,
95 6834
    ) {
96
        $this->children = $this->ormSchema->getInheritedRoles($target);
97
        $this->source = $this->sourceProvider->getSource($target);
98 6834
    }
99
100 6834
    public function isHierarchical(): bool
101
    {
102
        return $this->inherit !== null || ($this->loadSubclasses && $this->children !== []);
103
    }
104
105
    public function setSubclassesLoading(bool $enabled): void
106 6064
    {
107
        $this->loadSubclasses = $enabled;
108 6064
    }
109
110 6064
    public function getTarget(): string
111 1386
    {
112
        return $this->target;
113
    }
114 6064
115 72
    public function withContext(LoaderInterface $parent, array $options = []): static
116
    {
117
        // check that given options are known
118 6064
        if (!empty($wrong = \array_diff(\array_keys($options), \array_keys($this->options)))) {
119
            throw new LoaderException(
120 6064
                \sprintf(
121 176
                    'Relation %s does not support option: %s',
122
                    $this::class,
123
                    \implode(',', $wrong),
124
                ),
125 1146
            );
126
        }
127 1146
128
        $loader = clone $this;
129
        $loader->parent = $parent;
130 264
        $loader->options = $options + $this->options;
131
132 264
        return $loader;
133
    }
134
135 6358
    /**
136
     * Load the relation.
137 6358
     *
138
     * @param LoaderInterface|string $relation Relation name, or chain of relations separated by. If you need to set
139
     * inheritance then pass LoaderInterface object
140 4458
     * @param array  $options  Loader options (to be applied to last chain element only).
141
     * @param bool   $join     When set to true loaders will be forced into JOIN mode.
142
     * @param bool   $load     Load relation data.
143 4458
     *
144
     * @return LoaderInterface Must return loader for a requested relation.
145
     * @throws LoaderException
146
     */
147
    public function loadRelation(
148
        string|LoaderInterface $relation,
149
        array $options,
150
        bool $join = false,
151
        bool $load = false,
152
    ): LoaderInterface {
153 4458
        if ($relation instanceof ParentLoader) {
154 4458
            return $this->inherit = $relation->withContext($this);
155 4458
        }
156
157 4458
        if ($relation instanceof SubclassLoader) {
158
            $loader = $relation->withContext($this);
159
            $this->subclasses[] = $loader;
160
            return $loader;
161
        }
162
163
        $relation = $this->resolvePath($relation);
0 ignored issues
show
Bug introduced by
It seems like $relation can also be of type Cycle\ORM\Select\LoaderInterface; however, parameter $relation of Cycle\ORM\Select\AbstractLoader::resolvePath() does only seem to accept string, 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

163
        $relation = $this->resolvePath(/** @scrutinizer ignore-type */ $relation);
Loading history...
164
        $alias ??= $options['alias'] ?? $relation;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $alias seems to be never defined.
Loading history...
165
        unset($options['alias']);
166
        if (!empty($options['as'])) {
167
            // ??
168
            $this->registerPath($options['as'], $alias);
169
        }
170
171
        // Check if relation contain dot, i.e. relation chain
172
        if ($this->isChain($relation)) {
173 4554
            return $this->loadChain($relation, $options, $join, $load, $alias);
0 ignored issues
show
Unused Code introduced by
The call to Cycle\ORM\Select\AbstractLoader::loadChain() has too many arguments starting with $alias. ( Ignorable by Annotation )

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

173
            return $this->/** @scrutinizer ignore-call */ loadChain($relation, $options, $join, $load, $alias);

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...
174
        }
175
176
        /*
177
         * Joined loaders must be isolated from normal loaders due they would not load any data
178
         * and will only modify SelectQuery.
179 4554
         */
180 744
        if (!$join || $load) {
181
            $loaders = &$this->load;
182 4394
        } else {
183 616
            $loaders = &$this->join;
184 616
        }
185 616
186
        if ($load) {
187 4082
            $options['load'] ??= true;
188 4082
        }
189 160
190
        if (isset($loaders[$relation])) {
191
            // Overwrite existing loader options
192
            return $loaders[$alias] = $loaders[$alias]->withContext($this, $options);
193 4082
        }
194 362
195
        if ($join) {
196
            if (empty($options['method']) || !\in_array($options['method'], [self::JOIN, self::LEFT_JOIN], true)) {
197
                // let's tell our loaded that it's method is JOIN (forced)
198
                $options['method'] = self::JOIN;
199
            }
200
        }
201 4082
202 3642
        try {
203
            // Creating new loader.
204 552
            $loader = $this->factory->loader(
205
                $this->ormSchema,
206
                $this->sourceProvider,
207 4082
                $this->target,
208 3642
                $relation,
209
            );
210
        } catch (SchemaException | FactoryException $e) {
211 4082
            if ($this->inherit instanceof self) {
212
                return $this->inherit->loadRelation($relation, $options, $join, $load, $alias);
0 ignored issues
show
Unused Code introduced by
The call to Cycle\ORM\Select\AbstractLoader::loadRelation() has too many arguments starting with $alias. ( Ignorable by Annotation )

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

212
                return $this->inherit->/** @scrutinizer ignore-call */ loadRelation($relation, $options, $join, $load, $alias);

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...
213 416
            }
214
            throw new LoaderException(
215
                \sprintf('Unable to create loader: %s', $e->getMessage()),
216 4082
                $e->getCode(),
217 552
                $e,
218
            );
219 544
        }
220
221
        return $loaders[$alias] = $loader->withContext($this, $options);
222
    }
223
224
    public function createNode(): AbstractNode
225 4082
    {
226 4082
        $node = $this->initNode();
227 4082
228 4082
        if ($this->inherit !== null) {
229
            $node->joinNode(null, $this->inherit->createNode());
230
        }
231 64
232 64
        foreach ($this->load as $relation => $loader) {
233 56
            if ($loader instanceof JoinableInterface && $loader->isJoined()) {
234
                $node->joinNode($relation, $loader->createNode());
235 8
                continue;
236 8
            }
237 8
238
            $node->linkNode($relation, $loader->createNode());
239
        }
240
241
        if ($this->loadSubclasses) {
242 4082
            foreach ($this->subclasses as $loader) {
243
                $node->joinNode(null, $loader->createNode());
244
            }
245 6382
        }
246
247 6382
        return $node;
248
    }
249 6382
250 744
    public function loadData(AbstractNode $node, bool $includeRole = false): void
251
    {
252
        $this->loadChild($node, $includeRole);
253 6382
    }
254 3554
255 1370
    public function getSource(): SourceInterface
256 1370
    {
257
        return $this->source;
258
    }
259 2378
260
    /**
261
     * Returns inheritance parent loader.
262 6382
     */
263 6334
    public function getParentLoader(): ?LoaderInterface
264 336
    {
265
        return $this->inherit;
266
    }
267
268 6382
    /**
269
     * Indicates that loader loads data.
270
     */
271 3842
    abstract public function isLoaded(): bool;
272
273 3842
    /**
274
     * Ensure state of every nested loader.
275
     */
276
    public function __clone()
277
    {
278
        $this->parent = null;
279
280
        foreach ($this->load as $name => $loader) {
281
            $this->load[$name] = $loader->withContext($this);
282
        }
283
284
        foreach ($this->join as $name => $loader) {
285
            $this->join[$name] = $loader->withContext($this);
286 3842
        }
287
288 3842
        $this->inherit = $this->inherit?->withContext($this);
289 730
290
        foreach ($this->subclasses as $i => $loader) {
291 3842
            $this->subclasses[$i] = $loader->withContext($this);
292
        }
293
    }
294 6334
295
    final public function __destruct()
296 6334
    {
297 168
        unset($this->parent, $this->inherit, $this->subclasses, $this->load, $this->join);
298
    }
299
300
    protected function loadChild(AbstractNode $node, bool $includeRole = false): void
301 6286
    {
302 744
        foreach ($this->load as $relation => $loader) {
303 744
            $loader->loadData($node->getNode($relation), $includeRole);
304
        }
305
        $this->loadHierarchy($node, $includeRole);
306
    }
307 6286
308 6286
    /**
309 6286
     * @deprecated
310 328
     *
311 328
     * @codeCoverageIgnore
312
     */
313
    #[Deprecated('2.3', '$this->loadHierarchy(%parameter0%, %parameter1%)')]
314
    protected function loadIerarchy(AbstractNode $node, bool $includeRole = false): void
315 6286
    {
316
        $this->loadHierarchy($node, $includeRole);
317
    }
318
319
    protected function loadHierarchy(AbstractNode $node, bool $includeRole = false): void
320
    {
321
        if ($this->inherit === null && !$this->loadSubclasses) {
322
            return;
323 6494
        }
324
325 6494
        // Merge parent nodes
326
        if ($this->inherit !== null) {
327 6494
            $inheritNode = $node->getParentMergeNode();
328 744
            $this->inherit->loadData($inheritNode, $includeRole);
329
        }
330
331 6494
        // Merge subclass nodes
332 552
        if ($this->loadSubclasses) {
333
            $subclassNodes = $node->getSubclassMergeNodes();
334
            foreach ($this->subclasses as $i => $loader) {
335 6494
                $inheritNode = $subclassNodes[$i];
336 3562
                $loader->loadData($inheritNode, $includeRole);
337 1378
            }
338 24
        }
339 1354
340
        $node->mergeInheritanceNodes($includeRole);
341
    }
342
343 6470
    /**
344 6422
     * Create input node for the loader.
345 328
     */
346
    abstract protected function initNode(): AbstractNode;
347
348
    protected function configureQuery(SelectQuery $query): SelectQuery
349 6470
    {
350
        $query = $this->applyScope($query);
351
352
        if ($this->inherit !== null) {
353
            $query = $this->inherit->configureQuery($query);
354
        }
355
356
        foreach ($this->join as $loader) {
357
            $query = $loader->configureQuery($query);
358
        }
359 6834
360
        foreach ($this->load as $loader) {
361 6834
            if ($loader instanceof JoinableInterface && $loader->isJoined()) {
362
                $query = $loader->isHierarchical()
363
                    ? $loader->configureSubQuery($query)
0 ignored issues
show
Bug introduced by
The method configureSubQuery() does not exist on Cycle\ORM\Select\JoinableInterface. Did you maybe mean configureQuery()? ( Ignorable by Annotation )

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

363
                    ? $loader->/** @scrutinizer ignore-call */ configureSubQuery($query)

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...
364
                    : $loader->configureQuery($query);
365
            }
366
        }
367 6834
368
        if ($this->loadSubclasses) {
369 6834
            foreach ($this->subclasses as $loader) {
370 6834
                $query = $loader->configureQuery($query);
371 6834
            }
372 744
        }
373
374 6834
        return $query;
375 6834
    }
376
377
    abstract protected function applyScope(SelectQuery $query): SelectQuery;
378 6834
379
    /**
380 6834
     * Define schema option associated with the entity.
381 6834
     *
382 6834
     * @return mixed
383 6834
     */
384
    protected function define(int $property)
385
    {
386 6834
        return $this->ormSchema->define($this->target, $property);
387
    }
388 6834
389 616
    /**
390 616
     * Returns list of relations to be automatically joined with parent object.
391 616
     */
392
    protected function getEagerLoaders(?string $role = null): \Generator
393
    {
394
        $role ??= $this->target;
395
        $parentLoader = $this->generateParentLoader($role);
396 6834
        if ($parentLoader !== null) {
397
            yield $parentLoader;
398 6834
        }
399 6834
        yield from $this->generateSublassLoaders();
400 5278
        yield from $this->generateEagerRelationLoaders($role);
401 840
    }
402
403
    protected function generateParentLoader(string $role): ?LoaderInterface
404
    {
405
        $parent = $this->ormSchema->define($role, SchemaInterface::PARENT);
406
        return $parent === null
407
            ? null
408
            : $this->factory->loader($this->ormSchema, $this->sourceProvider, $role, FactoryInterface::PARENT_LOADER);
409
    }
410
411
    protected function generateSublassLoaders(): iterable
412
    {
413
        if ($this->children !== []) {
414
            foreach ($this->children as $subRole => $children) {
415
                yield $this->factory
416
                    ->loader($this->ormSchema, $this->sourceProvider, $subRole, FactoryInterface::CHILD_LOADER);
417
            }
418
        }
419
    }
420
421
    protected function generateEagerRelationLoaders(string $target): \Generator
422
    {
423
        $relations = $this->ormSchema->define($target, SchemaInterface::RELATIONS) ?? [];
424
        foreach ($relations as $relation => $schema) {
425
            if (($schema[Relation::LOAD] ?? null) === Relation::LOAD_EAGER) {
426
                yield $relation;
427
            }
428
        }
429
    }
430
}
431