AbstractLoader   F
last analyzed

Complexity

Total Complexity 67

Size/Duplication

Total Lines 406
Duplicated Lines 0 %

Test Coverage

Coverage 96.27%

Importance

Changes 3
Bugs 1 Features 1
Metric Value
eloc 146
dl 0
loc 406
ccs 129
cts 134
cp 0.9627
rs 3.04
c 3
b 1
f 1
wmc 67

22 Methods

Rating   Name   Duplication   Size   Complexity  
A getSource() 0 3 1
A loadData() 0 3 1
A getParentLoader() 0 3 1
C loadRelation() 0 75 14
B createNode() 0 24 7
B configureQuery() 0 27 9
A getTarget() 0 3 1
A __clone() 0 16 4
A getJoinedLoaders() 0 3 1
A __destruct() 0 3 1
A __construct() 0 12 1
A setSubclassesLoading() 0 3 1
A loadHierarchy() 0 22 6
A isHierarchical() 0 3 3
A loadIerarchy() 0 4 1
A withContext() 0 18 2
A loadChild() 0 6 2
A define() 0 3 1
A generateSubclassLoaders() 0 6 3
A generateEagerRelationLoaders() 0 6 3
A getEagerLoaders() 0 9 2
A generateParentLoader() 0 6 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
95 6834
        /**
96
         * @var non-empty-string Target role
97
         */
98 6834
        protected string $target,
99
    ) {
100 6834
        $this->children = $this->ormSchema->getInheritedRoles($target);
101
        $this->source = $this->sourceProvider->getSource($target);
102
    }
103
104
    public function isHierarchical(): bool
105
    {
106 6064
        return $this->inherit !== null || ($this->loadSubclasses && $this->children !== []);
107
    }
108 6064
109
    public function setSubclassesLoading(bool $enabled): void
110 6064
    {
111 1386
        $this->loadSubclasses = $enabled;
112
    }
113
114 6064
    public function getTarget(): string
115 72
    {
116
        return $this->target;
117
    }
118 6064
119
    public function withContext(LoaderInterface $parent, array $options = []): static
120 6064
    {
121 176
        // check that given options are known
122
        if (!empty($wrong = \array_diff(\array_keys($options), \array_keys($this->options)))) {
123
            throw new LoaderException(
124
                \sprintf(
125 1146
                    'Relation %s does not support option: %s',
126
                    $this::class,
127 1146
                    \implode(',', $wrong),
128
                ),
129
            );
130 264
        }
131
132 264
        $loader = clone $this;
133
        $loader->parent = $parent;
134
        $loader->options = $options + $this->options;
135 6358
136
        return $loader;
137 6358
    }
138
139
    /**
140 4458
     * Load the relation.
141
     *
142
     * @param LoaderInterface|string $relation Relation name, or chain of relations separated by. If you need to set
143 4458
     * inheritance then pass LoaderInterface object
144
     * @param array  $options  Loader options (to be applied to last chain element only).
145
     * @param bool   $join     When set to true loaders will be forced into JOIN mode.
146
     * @param bool   $load     Load relation data.
147
     *
148
     * @return LoaderInterface Must return loader for a requested relation.
149
     * @throws LoaderException
150
     */
151
    public function loadRelation(
152
        string|LoaderInterface $relation,
153 4458
        array $options,
154 4458
        bool $join = false,
155 4458
        bool $load = false,
156
    ): LoaderInterface {
157 4458
        if ($relation instanceof ParentLoader) {
158
            return $this->inherit = $relation->withContext($this);
159
        }
160
161
        if ($relation instanceof SubclassLoader) {
162
            $loader = $relation->withContext($this);
163
            $this->subclasses[] = $loader;
164
            return $loader;
165
        }
166
167
        $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

167
        $relation = $this->resolvePath(/** @scrutinizer ignore-type */ $relation);
Loading history...
168
        $alias ??= $options['alias'] ?? $relation;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $alias seems to be never defined.
Loading history...
169
        unset($options['alias']);
170
        if (!empty($options['as'])) {
171
            // ??
172
            $this->registerPath($options['as'], $alias);
173 4554
        }
174
175
        // Check if relation contain dot, i.e. relation chain
176
        if ($this->isChain($relation)) {
177
            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

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

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

377
                    ? $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...
378 6834
                    : $loader->configureQuery($query);
379
            }
380 6834
        }
381 6834
382 6834
        if ($this->loadSubclasses) {
383 6834
            foreach ($this->subclasses as $loader) {
384
                $query = $loader->configureQuery($query);
385
            }
386 6834
        }
387
388 6834
        return $query;
389 616
    }
390 616
391 616
    abstract protected function applyScope(SelectQuery $query): SelectQuery;
392
393
    /**
394
     * Define schema option associated with the entity.
395
     *
396 6834
     * @return mixed
397
     */
398 6834
    protected function define(int $property)
399 6834
    {
400 5278
        return $this->ormSchema->define($this->target, $property);
401 840
    }
402
403
    /**
404
     * Returns list of relations to be automatically joined with parent object.
405
     *
406
     * @return \Generator<int, LoaderInterface|non-empty-string>
407
     */
408
    protected function getEagerLoaders(?string $role = null): \Generator
409
    {
410
        $role ??= $this->target;
411
        $parentLoader = $this->generateParentLoader($role);
412
        if ($parentLoader !== null) {
413
            yield $parentLoader;
414
        }
415
        yield from $this->generateSubclassLoaders();
416
        yield from $this->generateEagerRelationLoaders($role);
417
    }
418
419
    protected function generateParentLoader(string $role): ?LoaderInterface
420
    {
421
        $parent = $this->ormSchema->define($role, SchemaInterface::PARENT);
422
        return $parent === null
423
            ? null
424
            : $this->factory->loader($this->ormSchema, $this->sourceProvider, $role, FactoryInterface::PARENT_LOADER);
425
    }
426
427
    /**
428
     * @return iterable<LoaderInterface>
429
     */
430
    protected function generateSubclassLoaders(): iterable
431
    {
432
        if ($this->children !== []) {
433
            foreach ($this->children as $subRole => $_) {
434
                yield $this->factory
435
                    ->loader($this->ormSchema, $this->sourceProvider, $subRole, FactoryInterface::CHILD_LOADER);
436
            }
437
        }
438
    }
439
440
    /**
441
     * @return iterable<non-empty-string>
442
     */
443
    protected function generateEagerRelationLoaders(string $target): iterable
444
    {
445
        $relations = $this->ormSchema->define($target, SchemaInterface::RELATIONS) ?? [];
446
        foreach ($relations as $relation => $schema) {
447
            if (($schema[Relation::LOAD] ?? null) === Relation::LOAD_EAGER) {
448
                yield $relation;
449
            }
450
        }
451
    }
452
}
453