Passed
Push — master ( e19e50...961f6c )
by Anton
02:47
created

AbstractLoader   A

Complexity

Total Complexity 36

Size/Duplication

Total Lines 281
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 87
dl 0
loc 281
rs 9.52
c 1
b 0
f 0
wmc 36

13 Methods

Rating   Name   Duplication   Size   Complexity  
A getTarget() 0 3 1
A __clone() 0 10 3
A getSource() 0 3 1
A __construct() 0 4 1
A __destruct() 0 5 1
B loadRelation() 0 59 11
A createNode() 0 14 4
A withContext() 0 18 2
A configureQuery() 0 15 5
A loadData() 0 3 1
A getEagerRelations() 0 6 3
A loadChild() 0 4 2
A define() 0 3 1
1
<?php
2
3
/**
4
 * Cycle DataMapper ORM
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\ORM\Select;
13
14
use Cycle\ORM\Exception\FactoryException;
15
use Cycle\ORM\Exception\LoaderException;
16
use Cycle\ORM\Exception\SchemaException;
17
use Cycle\ORM\ORMInterface;
18
use Cycle\ORM\Parser\AbstractNode;
19
use Cycle\ORM\Relation;
20
use Cycle\ORM\Schema;
21
use Cycle\ORM\Select\Traits\AliasTrait;
22
use Cycle\ORM\Select\Traits\ChainTrait;
23
use Spiral\Database\Query\SelectQuery;
24
25
/**
26
 * ORM Loaders used to load an compile data tree based on results fetched from SQL databases,
27
 * loaders can communicate with SelectQuery by providing it's own set of conditions, columns
28
 * joins and etc. In some cases loader may create additional selector to load data using information
29
 * fetched from previous query.
30
 *
31
 * Attention, AbstractLoader can only work with ORM Records, you must implement LoaderInterface
32
 * in order to support external references (MongoDB and etc).
33
 *
34
 * Loaders can be used for both - loading and filtering of record data.
35
 *
36
 * Reference tree generation logic example:
37
 *   User has many Posts (relation "posts"), user primary is ID, post inner key pointing to user
38
 *   is USER_ID. Post loader must request User data loader to create references based on ID field
39
 *   values. Once Post data were parsed we can mount it under parent user using mount method:
40
 *
41
 * @see Select::load()
42
 * @see Select::with()
43
 */
44
abstract class AbstractLoader implements LoaderInterface
45
{
46
    use ChainTrait;
47
    use AliasTrait;
48
49
    // Loading methods for data loaders.
50
    public const INLOAD    = 1;
51
    public const POSTLOAD  = 2;
52
    public const JOIN      = 3;
53
    public const LEFT_JOIN = 4;
54
55
    /** @var ORMInterface|SourceProviderInterface @internal */
56
    protected $orm;
57
58
    /** @var string */
59
    protected $target;
60
61
    /** @var array */
62
    protected $options = [
63
        'load'      => false,
64
        'constrain' => true,
65
    ];
66
67
    /** @var LoaderInterface[] */
68
    protected $load = [];
69
70
    /** @var AbstractLoader[] */
71
    protected $join = [];
72
73
    /** @var LoaderInterface @internal */
74
    protected $parent;
75
76
    /**
77
     * @param ORMInterface $orm
78
     * @param string       $target
79
     */
80
    public function __construct(ORMInterface $orm, string $target)
81
    {
82
        $this->orm = $orm;
83
        $this->target = $target;
84
    }
85
86
    /**
87
     * Destruct loader.
88
     */
89
    final public function __destruct()
90
    {
91
        $this->parent = null;
92
        $this->load = [];
93
        $this->join = [];
94
    }
95
96
    /**
97
     * Ensure state of every nested loader.
98
     */
99
    public function __clone()
100
    {
101
        $this->parent = null;
102
103
        foreach ($this->load as $name => $loader) {
104
            $this->load[$name] = $loader->withContext($this);
105
        }
106
107
        foreach ($this->join as $name => $loader) {
108
            $this->join[$name] = $loader->withContext($this);
109
        }
110
    }
111
112
    /**
113
     * @return string
114
     */
115
    public function getTarget(): string
116
    {
117
        return $this->target;
118
    }
119
120
    /**
121
     * Data source associated with the loader.
122
     *
123
     * @return SourceInterface
124
     */
125
    public function getSource(): SourceInterface
126
    {
127
        return $this->orm->getSource($this->target);
128
    }
129
130
    /**
131
     * {@inheritdoc}
132
     */
133
    public function withContext(LoaderInterface $parent, array $options = []): LoaderInterface
134
    {
135
        // check that given options are known
136
        if (!empty($wrong = array_diff(array_keys($options), array_keys($this->options)))) {
137
            throw new LoaderException(
138
                sprintf(
139
                    'Relation %s does not support option: %s',
140
                    get_class($this),
141
                    join(',', $wrong)
142
                )
143
            );
144
        }
145
146
        $loader = clone $this;
147
        $loader->parent = $parent;
148
        $loader->options = $options + $this->options;
149
150
        return $loader;
151
    }
152
153
    /**
154
     * Load the relation.
155
     *
156
     * @param string $relation Relation name, or chain of relations separated by.
157
     * @param array  $options  Loader options (to be applied to last chain element only).
158
     * @param bool   $join     When set to true loaders will be forced into JOIN mode.
159
     * @param bool   $load     Load relation data.
160
     * @return LoaderInterface Must return loader for a requested relation.
161
     *
162
     * @throws LoaderException
163
     */
164
    public function loadRelation(
165
        string $relation,
166
        array $options,
167
        bool $join = false,
168
        bool $load = false
169
    ): LoaderInterface {
170
        $relation = $this->resolvePath($relation);
171
        if (!empty($options['as'])) {
172
            $this->registerPath($options['as'], $relation);
173
        }
174
175
        //Check if relation contain dot, i.e. relation chain
176
        if ($this->isChain($relation)) {
177
            return $this->loadChain($relation, $options, $join, $load);
178
        }
179
180
        /*
181
         * Joined loaders must be isolated from normal loaders due they would not load any data
182
         * and will only modify SelectQuery.
183
         */
184
        if (!$join || $load) {
185
            $loaders = &$this->load;
186
        } else {
187
            $loaders = &$this->join;
188
        }
189
190
        if ($load) {
191
            $options['load'] = $options['load'] ?? true;
192
        }
193
194
        if (isset($loaders[$relation])) {
195
            // overwrite existing loader options
196
            return $loaders[$relation] = $loaders[$relation]->withContext($this, $options);
197
        }
198
199
        if ($join) {
200
            if (empty($options['method']) || !in_array($options['method'], [self::JOIN, self::LEFT_JOIN], true)) {
201
                // let's tell our loaded that it's method is JOIN (forced)
202
                $options['method'] = self::JOIN;
203
            }
204
        }
205
206
        try {
207
            //Creating new loader.
208
            $loader = $this->orm->getFactory()->loader(
0 ignored issues
show
Bug introduced by
The method getFactory() does not exist on Cycle\ORM\Select\SourceProviderInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Cycle\ORM\Select\SourceProviderInterface. ( Ignorable by Annotation )

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

208
            $loader = $this->orm->/** @scrutinizer ignore-call */ getFactory()->loader(
Loading history...
209
                $this->orm,
210
                $this->orm->getSchema(),
0 ignored issues
show
Bug introduced by
The method getSchema() does not exist on Cycle\ORM\Select\SourceProviderInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Cycle\ORM\Select\SourceProviderInterface. ( Ignorable by Annotation )

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

210
                $this->orm->/** @scrutinizer ignore-call */ 
211
                            getSchema(),
Loading history...
211
                $this->target,
212
                $relation
213
            );
214
        } catch (SchemaException | FactoryException $e) {
215
            throw new LoaderException(
216
                sprintf('Unable to create loader: %s', $e->getMessage()),
217
                $e->getCode(),
218
                $e
219
            );
220
        }
221
222
        return $loaders[$relation] = $loader->withContext($this, $options);
223
    }
224
225
    /**
226
     * {@inheritdoc}
227
     */
228
    public function createNode(): AbstractNode
229
    {
230
        $node = $this->initNode();
231
232
        foreach ($this->load as $relation => $loader) {
233
            if ($loader instanceof JoinableInterface && $loader->isJoined()) {
234
                $node->joinNode($relation, $loader->createNode());
235
                continue;
236
            }
237
238
            $node->linkNode($relation, $loader->createNode());
239
        }
240
241
        return $node;
242
    }
243
244
    /**
245
     * @param AbstractNode $node
246
     */
247
    public function loadData(AbstractNode $node): void
248
    {
249
        $this->loadChild($node);
250
    }
251
252
    /**
253
     * Indicates that loader loads data.
254
     *
255
     * @return bool
256
     */
257
    abstract public function isLoaded(): bool;
258
259
    /**
260
     * @param AbstractNode $node
261
     */
262
    protected function loadChild(AbstractNode $node): void
263
    {
264
        foreach ($this->load as $relation => $loader) {
265
            $loader->loadData($node->getNode($relation));
266
        }
267
    }
268
269
    /**
270
     * Create input node for the loader.
271
     *
272
     * @return AbstractNode
273
     */
274
    abstract protected function initNode(): AbstractNode;
275
276
    /**
277
     * @param SelectQuery $query
278
     * @return SelectQuery
279
     */
280
    protected function configureQuery(SelectQuery $query): SelectQuery
281
    {
282
        $query = $this->applyConstrain($query);
283
284
        foreach ($this->join as $loader) {
285
            $query = $loader->configureQuery($query);
286
        }
287
288
        foreach ($this->load as $loader) {
289
            if ($loader instanceof JoinableInterface && $loader->isJoined()) {
290
                $query = $loader->configureQuery($query);
291
            }
292
        }
293
294
        return $query;
295
    }
296
297
    /**
298
     * @param SelectQuery $query
299
     * @return SelectQuery
300
     */
301
    abstract protected function applyConstrain(SelectQuery $query): SelectQuery;
302
303
    /**
304
     * Define schema option associated with the entity.
305
     *
306
     * @param int $property
307
     * @return mixed
308
     */
309
    protected function define(int $property)
310
    {
311
        return $this->orm->getSchema()->define($this->target, $property);
312
    }
313
314
    /**
315
     * Returns list of relations to be automatically joined with parent object.
316
     *
317
     * @return \Generator
318
     */
319
    protected function getEagerRelations(): \Generator
320
    {
321
        $relations = $this->orm->getSchema()->define($this->target, Schema::RELATIONS) ?? [];
322
        foreach ($relations as $relation => $schema) {
323
            if (($schema[Relation::LOAD] ?? null) === Relation::LOAD_EAGER) {
324
                yield $relation;
325
            }
326
        }
327
    }
328
}
329