Passed
Pull Request — 2.x (#405)
by Aleksei
17:51
created

AbstractLoader::configureQuery()   B

Complexity

Conditions 9
Paths 32

Size

Total Lines 27
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 9

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 14
c 1
b 0
f 0
nc 32
nop 1
dl 0
loc 27
ccs 8
cts 8
cp 1
crap 9
rs 8.0555
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
    protected SourceInterface $source;
88 6834
89
    public function __construct(
90
        protected SchemaInterface $ormSchema,
91
        protected SourceProviderInterface $sourceProvider,
92
        protected FactoryInterface $factory,
93
        protected string $target
94 6834
    ) {
95 6834
        $this->children = $this->ormSchema->getInheritedRoles($target);
96
        $this->source = $this->sourceProvider->getSource($target);
97
    }
98 6834
99
    final public function __destruct()
100 6834
    {
101
        unset($this->parent, $this->inherit, $this->subclasses, $this->load, $this->join);
102
    }
103
104
    /**
105
     * Ensure state of every nested loader.
106 6064
     */
107
    public function __clone()
108 6064
    {
109
        $this->parent = null;
110 6064
111 1386
        foreach ($this->load as $name => $loader) {
112
            $this->load[$name] = $loader->withContext($this);
113
        }
114 6064
115 72
        foreach ($this->join as $name => $loader) {
116
            $this->join[$name] = $loader->withContext($this);
117
        }
118 6064
119
        $this->inherit = $this->inherit?->withContext($this);
120 6064
121 176
        foreach ($this->subclasses as $i => $loader) {
122
            $this->subclasses[$i] = $loader->withContext($this);
123
        }
124
    }
125 1146
126
    public function isHierarchical(): bool
127 1146
    {
128
        return $this->inherit !== null || ($this->loadSubclasses && $this->children !== []);
129
    }
130 264
131
    public function setSubclassesLoading(bool $enabled): void
132 264
    {
133
        $this->loadSubclasses = $enabled;
134
    }
135 6358
136
    public function getTarget(): string
137 6358
    {
138
        return $this->target;
139
    }
140 4458
141
    public function withContext(LoaderInterface $parent, array $options = []): static
142
    {
143 4458
        // check that given options are known
144
        if (!empty($wrong = array_diff(array_keys($options), array_keys($this->options)))) {
145
            throw new LoaderException(
146
                sprintf(
147
                    'Relation %s does not support option: %s',
148
                    $this::class,
149
                    implode(',', $wrong)
150
                )
151
            );
152
        }
153 4458
154 4458
        $loader = clone $this;
155 4458
        $loader->parent = $parent;
156
        $loader->options = $options + $this->options;
157 4458
158
        return $loader;
159
    }
160
161
    /**
162
     * Load the relation.
163
     *
164
     * @param LoaderInterface|string $relation Relation name, or chain of relations separated by. If you need to set
165
     * inheritance then pass LoaderInterface object
166
     * @param array  $options  Loader options (to be applied to last chain element only).
167
     * @param bool   $join     When set to true loaders will be forced into JOIN mode.
168
     * @param bool   $load     Load relation data.
169
     *
170
     * @throws LoaderException
171
     *
172
     * @return LoaderInterface Must return loader for a requested relation.
173 4554
     */
174
    public function loadRelation(
175
        string|LoaderInterface $relation,
176
        array $options,
177
        bool $join = false,
178
        bool $load = false
179 4554
    ): LoaderInterface {
180 744
        if ($relation instanceof ParentLoader) {
181
            return $this->inherit = $relation->withContext($this);
182 4394
        }
183 616
        if ($relation instanceof SubclassLoader) {
184 616
            $loader = $relation->withContext($this);
185 616
            $this->subclasses[] = $loader;
186
            return $loader;
187 4082
        }
188 4082
        $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

188
        $relation = $this->resolvePath(/** @scrutinizer ignore-type */ $relation);
Loading history...
189 160
        if (!empty($options['as'])) {
190
            $this->registerPath($options['as'], $relation);
191
        }
192
193 4082
        //Check if relation contain dot, i.e. relation chain
194 362
        if ($this->isChain($relation)) {
195
            return $this->loadChain($relation, $options, $join, $load);
196
        }
197
198
        /*
199
         * Joined loaders must be isolated from normal loaders due they would not load any data
200
         * and will only modify SelectQuery.
201 4082
         */
202 3642
        if (!$join || $load) {
203
            $loaders = &$this->load;
204 552
        } else {
205
            $loaders = &$this->join;
206
        }
207 4082
208 3642
        if ($load) {
209
            $options['load'] ??= true;
210
        }
211 4082
212
        if (isset($loaders[$relation])) {
213 416
            // overwrite existing loader options
214
            return $loaders[$relation] = $loaders[$relation]->withContext($this, $options);
215
        }
216 4082
217 552
        if ($join) {
218
            if (empty($options['method']) || !in_array($options['method'], [self::JOIN, self::LEFT_JOIN], true)) {
219 544
                // let's tell our loaded that it's method is JOIN (forced)
220
                $options['method'] = self::JOIN;
221
            }
222
        }
223
224
        try {
225 4082
            //Creating new loader.
226 4082
            $loader = $this->factory->loader(
227 4082
                $this->ormSchema,
228 4082
                $this->sourceProvider,
229
                $this->target,
230
                $relation
231 64
            );
232 64
        } catch (SchemaException | FactoryException $e) {
233 56
            if ($this->inherit instanceof self) {
234
                return $this->inherit->loadRelation($relation, $options, $join, $load);
235 8
            }
236 8
            throw new LoaderException(
237 8
                sprintf('Unable to create loader: %s', $e->getMessage()),
238
                $e->getCode(),
239
                $e
240
            );
241
        }
242 4082
243
        return $loaders[$relation] = $loader->withContext($this, $options);
244
    }
245 6382
246
    public function createNode(): AbstractNode
247 6382
    {
248
        $node = $this->initNode();
249 6382
250 744
        if ($this->inherit !== null) {
251
            $node->joinNode(null, $this->inherit->createNode());
252
        }
253 6382
254 3554
        foreach ($this->load as $relation => $loader) {
255 1370
            if ($loader instanceof JoinableInterface && $loader->isJoined()) {
256 1370
                $node->joinNode($relation, $loader->createNode());
257
                continue;
258
            }
259 2378
260
            $node->linkNode($relation, $loader->createNode());
261
        }
262 6382
263 6334
        if ($this->loadSubclasses) {
264 336
            foreach ($this->subclasses as $loader) {
265
                $node->joinNode(null, $loader->createNode());
266
            }
267
        }
268 6382
269
        return $node;
270
    }
271 3842
272
    public function loadData(AbstractNode $node, bool $includeRole = false): void
273 3842
    {
274
        $this->loadChild($node, $includeRole);
275
    }
276
277
    public function getSource(): SourceInterface
278
    {
279
        return $this->source;
280
    }
281
282
    /**
283
     * Returns inheritance parent loader.
284
     */
285
    public function getParentLoader(): ?LoaderInterface
286 3842
    {
287
        return $this->inherit;
288 3842
    }
289 730
290
    /**
291 3842
     * Indicates that loader loads data.
292
     */
293
    abstract public function isLoaded(): bool;
294 6334
295
    protected function loadChild(AbstractNode $node, bool $includeRole = false): void
296 6334
    {
297 168
        foreach ($this->load as $relation => $loader) {
298
            $loader->loadData($node->getNode($relation), $includeRole);
299
        }
300
        $this->loadHierarchy($node, $includeRole);
301 6286
    }
302 744
303 744
    /**
304
     * @deprecated
305
     *
306
     * @codeCoverageIgnore
307 6286
     */
308 6286
    #[Deprecated('2.3', '$this->loadHierarchy(%parameter0%, %parameter1%)')]
309 6286
    protected function loadIerarchy(AbstractNode $node, bool $includeRole = false): void
310 328
    {
311 328
        $this->loadHierarchy($node, $includeRole);
312
    }
313
314
    protected function loadHierarchy(AbstractNode $node, bool $includeRole = false): void
315 6286
    {
316
        if ($this->inherit === null && !$this->loadSubclasses) {
317
            return;
318
        }
319
320
        // Merge parent nodes
321
        if ($this->inherit !== null) {
322
            $inheritNode = $node->getParentMergeNode();
323 6494
            $this->inherit->loadData($inheritNode, $includeRole);
324
        }
325 6494
326
        // Merge subclass nodes
327 6494
        if ($this->loadSubclasses) {
328 744
            $subclassNodes = $node->getSubclassMergeNodes();
329
            foreach ($this->subclasses as $i => $loader) {
330
                $inheritNode = $subclassNodes[$i];
331 6494
                $loader->loadData($inheritNode, $includeRole);
332 552
            }
333
        }
334
335 6494
        $node->mergeInheritanceNodes($includeRole);
336 3562
    }
337 1378
338 24
    /**
339 1354
     * Create input node for the loader.
340
     */
341
    abstract protected function initNode(): AbstractNode;
342
343 6470
    protected function configureQuery(SelectQuery $query): SelectQuery
344 6422
    {
345 328
        $query = $this->applyScope($query);
346
347
        if ($this->inherit !== null) {
348
            $query = $this->inherit->configureQuery($query);
349 6470
        }
350
351
        foreach ($this->join as $loader) {
352
            $query = $loader->configureQuery($query);
353
        }
354
355
        foreach ($this->load as $loader) {
356
            if ($loader instanceof JoinableInterface && $loader->isJoined()) {
357
                $query = $loader->isHierarchical()
358
                    ? $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

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