AbstractLoader   A
last analyzed

Complexity

Total Complexity 31

Size/Duplication

Total Lines 333
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 6

Importance

Changes 0
Metric Value
wmc 31
lcom 2
cbo 6
dl 0
loc 333
rs 9.92
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
initNode() 0 1 ?
A __construct() 0 6 1
A getClass() 0 4 1
A withContext() 0 28 3
B loadRelation() 0 42 8
A createNode() 0 11 2
A loadData() 0 7 2
A __clone() 0 12 3
A __destruct() 0 5 1
A configureQuery() 0 14 5
A getDatabase() 0 4 1
A getTable() 0 4 1
A isChain() 0 4 1
A loadChain() 0 25 2
1
<?php
2
/**
3
 * Spiral, Core Components
4
 *
5
 * @author Wolfy-J
6
 */
7
8
namespace Spiral\ORM\Entities\Loaders;
9
10
use Spiral\Database\Builders\SelectQuery;
11
use Spiral\ORM\Entities\Loaders\Traits\ColumnsTrait;
12
use Spiral\ORM\Entities\Nodes\AbstractNode;
13
use Spiral\ORM\Entities\RecordSelector;
14
use Spiral\ORM\Exceptions\LoaderException;
15
use Spiral\ORM\Exceptions\ORMException;
16
use Spiral\ORM\LoaderInterface;
17
use Spiral\ORM\ORMInterface;
18
19
/**
20
 * ORM Loaders used to load an compile data tree based on results fetched from SQL databases,
21
 * loaders can communicate with parent selector by providing it's own set of conditions, columns
22
 * joins and etc. In some cases loader may create additional selector to load data using information
23
 * fetched from previous query.
24
 *
25
 * Attention, AbstractLoader can only work with ORM Records, you must implement LoaderInterface
26
 * in order to support external references (MongoDB and etc).
27
 *
28
 * Loaders can be used for both - loading and filtering of record data.
29
 *
30
 * Reference tree generation logic example:
31
 * User has many Posts (relation "posts"), user primary is ID, post inner key pointing to user
32
 * is USER_ID. Post loader must request User data loader to create references based on ID field
33
 * values. Once Post data were parsed we can mount it under parent user using mount method:
34
 *
35
 * @see RecordSelector::load()
36
 * @see RecordSelector::with()
37
 */
38
abstract class AbstractLoader implements LoaderInterface
39
{
40
    use ColumnsTrait;
41
42
    /**
43
     * Loading methods for data loaders.
44
     */
45
    const INLOAD    = 1;
46
    const POSTLOAD  = 2;
47
    const JOIN      = 3;
48
    const LEFT_JOIN = 4;
49
50
    /**
51
     * Nested loaders.
52
     *
53
     * @var LoaderInterface[]
54
     */
55
    protected $loaders = [];
56
57
    /**
58
     * Set of loaders with ability to JOIN it's data into parent SelectQuery.
59
     *
60
     * @var AbstractLoader[]
61
     */
62
    protected $joiners = [];
63
64
    /**
65
     * @var string
66
     */
67
    protected $class;
68
69
    /**
70
     * @invisible
71
     * @var ORMInterface
72
     */
73
    protected $orm;
74
75
    /**
76
     * Parent loader if any.
77
     *
78
     * @invisible
79
     * @var AbstractLoader
80
     */
81
    protected $parent;
82
83
    /**
84
     * Loader options, can be altered on RecordSelector level.
85
     *
86
     * @var array
87
     */
88
    protected $options = [];
89
90
    /**
91
     * Relation schema.
92
     *
93
     * @var array
94
     */
95
    protected $schema = [];
96
97
    /**
98
     * @param string       $class
99
     * @param array        $schema Relation schema.
100
     * @param ORMInterface $orm
101
     */
102
    public function __construct(string $class, array $schema, ORMInterface $orm)
103
    {
104
        $this->class = $class;
105
        $this->schema = $schema;
106
        $this->orm = $orm;
107
    }
108
109
    /**
110
     * @return string
111
     */
112
    public function getClass(): string
113
    {
114
        return $this->class;
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120
    public function withContext(LoaderInterface $parent, array $options = []): LoaderInterface
121
    {
122
        if (!$parent instanceof AbstractLoader) {
123
            throw new LoaderException(sprintf(
124
                "Loader of type '%s' can not accept parent '%s'",
125
                get_class($this),
126
                get_class($parent)
127
            ));
128
        }
129
130
        /*
131
         * This scary construction simply checks if input array has keys which do not present in a
132
         * current set of options (i.e. default options, i.e. current options).
133
         */
134
        if (!empty($wrong = array_diff(array_keys($options), array_keys($this->options)))) {
135
            throw new LoaderException(sprintf(
136
                "Relation %s does not support option: %s",
137
                get_class($this),
138
                join(',', $wrong)
139
            ));
140
        }
141
142
        $loader = clone $this;
143
        $loader->parent = $parent;
144
        $loader->options = $options + $this->options;
145
146
        return $loader;
147
    }
148
149
    /**
150
     * Pre-load data on inner relation or relation chain. Method automatically called by Selector,
151
     * see load() method.
152
     *
153
     * Method support chain initiation via dot notation. Method will return already exists loader if
154
     * such presented.
155
     *
156
     * @see RecordSelector::load()
157
     *
158
     * @param string $relation Relation name, or chain of relations separated by.
159
     * @param array  $options  Loader options (to be applied to last chain element only).
160
     * @param bool   $join     When set to true loaders will be forced into JOIN mode.
161
     *
162
     * @return LoaderInterface Must return loader for a requested relation.
163
     *
164
     * @throws LoaderException
165
     */
166
    final public function loadRelation(
167
        string $relation,
168
        array $options,
169
        bool $join = false
170
    ): LoaderInterface {
171
        //Check if relation contain dot, i.e. relation chain
172
        if ($this->isChain($relation)) {
173
            return $this->loadChain($relation, $options, $join);
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
         */
180
        if (!$join) {
181
            $loaders = &$this->loaders;
182
        } else {
183
            $loaders = &$this->joiners;
184
        }
185
186
        if ($join) {
187
            if (empty($options['method']) || !in_array($options['method'], [self::JOIN, self::LEFT_JOIN])) {
188
                //Let's tell our loaded that it's method is JOIN (forced)
189
                $options['method'] = self::JOIN;
190
            }
191
        }
192
193
        if (isset($loaders[$relation])) {
194
            //Overwriting existed loader options
195
            return $loaders[$relation] = $loaders[$relation]->withContext($this, $options);
196
        }
197
198
        try {
199
            //Creating new loader.
200
            $loader = $this->orm->makeLoader($this->class, $relation);
201
        } catch (ORMException $e) {
202
            throw new LoaderException("Unable to create loader", $e->getCode(), $e);
203
        }
204
205
        //Configuring loader scope
206
        return $loaders[$relation] = $loader->withContext($this, $options);
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     */
212
    final public function createNode(): AbstractNode
213
    {
214
        $node = $this->initNode();
215
216
        //Working with nested relation loaders
217
        foreach ($this->loaders as $relation => $loader) {
218
            $node->registerNode($relation, $loader->createNode());
219
        }
220
221
        return $node;
222
    }
223
224
    /**
225
     * @param AbstractNode $node
226
     */
227
    public function loadData(AbstractNode $node)
228
    {
229
        //Loading data thought child loaders
230
        foreach ($this->loaders as $relation => $loader) {
231
            $loader->loadData($node->fetchNode($relation));
232
        }
233
    }
234
235
    /**
236
     * Ensure state of every nested loader.
237
     */
238
    public function __clone()
239
    {
240
        foreach ($this->loaders as $name => $loader) {
241
            //Will automatically ensure nested change parents
242
            $this->loaders[$name] = $loader->withContext($this);
243
        }
244
245
        foreach ($this->joiners as $name => $loader) {
246
            //Will automatically ensure nested change parents
247
            $this->joiners[$name] = $loader->withContext($this);
248
        }
249
    }
250
251
    /**
252
     * Destruct loader.
253
     */
254
    final public function __destruct()
255
    {
256
        $this->loaders = [];
257
        $this->joiners = [];
258
    }
259
260
    /**
261
     * @param SelectQuery $query
262
     * @param bool        $loadColumns
263
     *
264
     * @return SelectQuery
265
     */
266
    protected function configureQuery(SelectQuery $query, bool $loadColumns = true): SelectQuery
267
    {
268
        foreach ($this->loaders as $loader) {
269
            if ($loader instanceof RelationLoader && $loader->isJoined()) {
270
                $query = $loader->configureQuery(clone $query, $loadColumns);
0 ignored issues
show
Bug introduced by
The method configureQuery() cannot be called from this context as it is declared protected in class Spiral\ORM\Entities\Loaders\RelationLoader.

This check looks for access to methods that are not accessible from the current context.

If you need to make a method accessible to another context you can raise its visibility level in the defining class.

Loading history...
271
            }
272
        }
273
274
        foreach ($this->joiners as $loader) {
275
            $query = $loader->configureQuery(clone $query, false);
276
        }
277
278
        return $query;
279
    }
280
281
    /**
282
     * Get database name associated with relation
283
     *
284
     * @return string
285
     */
286
    protected function getDatabase(): string
287
    {
288
        return $this->orm->define($this->class, ORMInterface::R_DATABASE);
289
    }
290
291
    /**
292
     * Get table name associated with relation
293
     *
294
     * @return string
295
     */
296
    protected function getTable(): string
297
    {
298
        return $this->orm->define($this->class, ORMInterface::R_TABLE);
299
    }
300
301
    /**
302
     * @return AbstractNode
303
     */
304
    abstract protected function initNode(): AbstractNode;
305
306
    /**
307
     * Joined table alias.
308
     *
309
     * @return string
310
     */
311
    abstract protected function getAlias(): string;
312
313
    /**
314
     * list of columns to be loaded.
315
     *
316
     * @return array
317
     */
318
    abstract protected function getColumns(): array;
319
320
    /**
321
     * Check if given relation is actually chain of relations.
322
     *
323
     * @param string $relation
324
     *
325
     * @return bool
326
     */
327
    private function isChain(string $relation): bool
328
    {
329
        return strpos($relation, '.') !== false;
330
    }
331
332
    /**
333
     * @see loadRelation()
334
     * @see joinRelation()
335
     *
336
     * @param string $chain
337
     * @param array  $options Final loader options.
338
     * @param bool   $join    See loadRelation().
339
     *
340
     * @return LoaderInterface
341
     *
342
     * @throws LoaderException When one of chain elements is not actually chainable (let's say ODM
343
     *                         loader).
344
     */
345
    private function loadChain(string $chain, array $options, bool $join): LoaderInterface
346
    {
347
        $position = strpos($chain, '.');
348
349
        //Chain of relations provided (relation.nestedRelation)
350
        $child = $this->loadRelation(
351
            substr($chain, 0, $position),
352
            [],
353
            $join
354
        );
355
356
        if (!$child instanceof AbstractLoader) {
357
            throw new LoaderException(sprintf(
358
                "Loader '%s' does not support chain relation loading",
359
                get_class($child)
360
            ));
361
        }
362
363
        //Loading nested relation thought chain (chainOptions prior to user options)
364
        return $child->loadRelation(
365
            substr($chain, $position + 1),
366
            $options,
367
            $join
368
        );
369
    }
370
}
371