Completed
Branch feature/pre-split (669609)
by Anton
03:30
created

AbstractLoader::withContext()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 15
nc 3
nop 2
dl 0
loc 28
rs 8.8571
c 0
b 0
f 0
1
<?php
2
/**
3
 * components
4
 *
5
 * @author    Wolfy-J
6
 */
7
namespace Spiral\ORM\Entities\Loaders;
8
9
use Spiral\Database\Builders\SelectQuery;
10
use Spiral\ORM\Entities\Loaders\Traits\ColumnsTrait;
11
use Spiral\ORM\Entities\Nodes\AbstractNode;
12
use Spiral\ORM\Entities\RecordSelector;
13
use Spiral\ORM\Exceptions\LoaderException;
14
use Spiral\ORM\Exceptions\ORMException;
15
use Spiral\ORM\LoaderInterface;
16
use Spiral\ORM\ORMInterface;
17
18
/**
19
 * ORM Loaders used to load an compile data tree based on results fetched from SQL databases,
20
 * loaders can communicate with parent selector by providing it's own set of conditions, columns
21
 * joins and etc. In some cases loader may create additional selector to load data using information
22
 * fetched from previous query.
23
 *
24
 * Attention, AbstractLoader can only work with ORM Records, you must implement LoaderInterface
25
 * in order to support external references (MongoDB and etc).
26
 *
27
 * Loaders can be used for both - loading and filtering of record data.
28
 *
29
 * Reference tree generation logic example:
30
 * User has many Posts (relation "posts"), user primary is ID, post inner key pointing to user
31
 * is USER_ID. Post loader must request User data loader to create references based on ID field
32
 * values. Once Post data were parsed we can mount it under parent user using mount method:
33
 *
34
 * @see RecordSelector::load()
35
 * @see RecordSelector::with()
36
 */
37
abstract class AbstractLoader implements LoaderInterface
38
{
39
    use ColumnsTrait;
40
41
    /**
42
     * Loading methods for data loaders.
43
     */
44
    const INLOAD    = 1;
45
    const POSTLOAD  = 2;
46
    const JOIN      = 3;
47
    const LEFT_JOIN = 4;
48
49
    /**
50
     * Nested loaders.
51
     *
52
     * @var LoaderInterface[]
53
     */
54
    protected $loaders = [];
55
56
    /**
57
     * Set of loaders with ability to JOIN it's data into parent SelectQuery.
58
     *
59
     * @var AbstractLoader[]
60
     */
61
    protected $joiners = [];
62
63
    /**
64
     * @var string
65
     */
66
    protected $class;
67
68
    /**
69
     * @invisible
70
     * @var ORMInterface
71
     */
72
    protected $orm;
73
74
    /**
75
     * Parent loader if any.
76
     *
77
     * @invisible
78
     * @var AbstractLoader
79
     */
80
    protected $parent;
81
82
    /**
83
     * Loader options, can be altered on RecordSelector level.
84
     *
85
     * @var array
86
     */
87
    protected $options = [];
88
89
    /**
90
     * Relation schema.
91
     *
92
     * @var array
93
     */
94
    protected $schema = [];
95
96
    /**
97
     * @param string       $class
98
     * @param array        $schema Relation schema.
99
     * @param ORMInterface $orm
100
     */
101
    public function __construct(string $class, array $schema, ORMInterface $orm)
102
    {
103
        $this->class = $class;
104
        $this->schema = $schema;
105
        $this->orm = $orm;
106
    }
107
108
    /**
109
     * {@inheritdoc}
110
     */
111
    public function withContext(LoaderInterface $parent, array $options = []): LoaderInterface
112
    {
113
        if (!$parent instanceof AbstractLoader) {
114
            throw new LoaderException(sprintf(
115
                "Loader of type '%s' can not accept parent '%s'",
116
                get_class($this),
117
                get_class($parent)
118
            ));
119
        }
120
121
        /*
122
         * This scary construction simply checks if input array has keys which do not present in a
123
         * current set of options (i.e. default options, i.e. current options).
124
         */
125
        if (!empty($wrong = array_diff(array_keys($options), array_keys($this->options)))) {
126
            throw new LoaderException(sprintf(
127
                "Relation %s does not support options: %s",
128
                get_class($this),
129
                join(',', $wrong)
130
            ));
131
        }
132
133
        $loader = clone $this;
134
        $loader->parent = $parent;
135
        $loader->options = $options + $this->options;
136
137
        return $loader;
138
    }
139
140
    /**
141
     * Pre-load data on inner relation or relation chain. Method automatically called by Selector,
142
     * see load() method.
143
     *
144
     * Method support chain initiation via dot notation. Method will return already exists loader if
145
     * such presented.
146
     *
147
     * @see RecordSelector::load()
148
     *
149
     * @param string $relation Relation name, or chain of relations separated by.
150
     * @param array  $options  Loader options (to be applied to last chain element only).
151
     * @param bool   $join     When set to true loaders will be forced into JOIN mode.
152
     *
153
     * @return LoaderInterface Must return loader for a requested relation.
154
     *
155
     * @throws LoaderException
156
     */
157
    final public function loadRelation(
158
        string $relation,
159
        array $options,
160
        bool $join = false
161
    ): LoaderInterface {
162
        //Check if relation contain dot, i.e. relation chain
163
        if ($this->isChain($relation)) {
164
            return $this->loadChain($relation, $options, $join);
165
        }
166
167
        /*
168
         * Joined loaders must be isolated from normal loaders due they would not load any data
169
         * and will only modify SelectQuery.
170
         */
171
        if (!$join) {
172
            $loaders = &$this->loaders;
173
        } else {
174
            $loaders = &$this->joiners;
175
        }
176
177
        if ($join) {
178
            //Let's tell our loaded that it's method is JOIN (forced)
179
            $options['method'] = self::JOIN;
180
        }
181
182
        if (isset($loaders[$relation])) {
183
            //Overwriting existed loader options
184
            return $loaders[$relation] = $loaders[$relation]->withContext($this, $options);
185
        }
186
187
        try {
188
            //Creating new loader.
189
            $loader = $this->orm->makeLoader($this->class, $relation);
190
        } catch (ORMException $e) {
191
            throw new LoaderException("Unable to create loader", $e->getCode(), $e);
192
        }
193
194
        //Configuring loader scope
195
        return $loaders[$relation] = $loader->withContext($this, $options);
196
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201
    final public function createNode(): AbstractNode
202
    {
203
        $node = $this->initNode();
204
205
        //Working with nested relation loaders
206
        foreach ($this->loaders as $relation => $loader) {
207
            $node->registerNode($relation, $loader->createNode());
208
        }
209
210
        return $node;
211
    }
212
213
    /**
214
     * @param AbstractNode $node
215
     */
216
    public function loadData(AbstractNode $node)
217
    {
218
        //Loading data thought child loaders
219
        foreach ($this->loaders as $relation => $loader) {
220
            $loader->loadData($node->fetchNode($relation));
221
        }
222
    }
223
224
    /**
225
     * Ensure state of every nested loader.
226
     */
227
    final public function __clone()
228
    {
229
        foreach ($this->loaders as $name => $loader) {
230
            //Will automatically ensure nested change parents
231
            $this->loaders[$name] = $loader->withContext($this);
232
        }
233
234
        foreach ($this->joiners as $name => $loader) {
235
            //Will automatically ensure nested change parents
236
            $this->joiners[$name] = $loader->withContext($this);
237
        }
238
    }
239
240
    /**
241
     * Destruct loader.
242
     */
243
    final public function __destruct()
244
    {
245
        $this->loaders = [];
246
        $this->joiners = [];
247
    }
248
249
    /**
250
     * @param SelectQuery $query
251
     *
252
     * @return SelectQuery
253
     */
254
    protected function configureQuery(SelectQuery $query): SelectQuery
255
    {
256
        foreach ($this->loaders as $loader) {
257
            if ($loader instanceof RelationLoader && $loader->isJoined()) {
258
                $query = $loader->configureQuery(clone $query);
1 ignored issue
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...
259
            }
260
        }
261
262
        foreach ($this->joiners as $loader) {
263
            $query = $loader->configureQuery(clone $query);
264
        }
265
266
        return $query;
267
    }
268
269
    /**
270
     * Get database name associated with relation
271
     *
272
     * @return string
273
     */
274
    protected function getDatabase(): string
275
    {
276
        return $this->orm->define($this->class, ORMInterface::R_DATABASE);
277
    }
278
279
    /**
280
     * Get table name associated with relation
281
     *
282
     * @return string
283
     */
284
    protected function getTable(): string
285
    {
286
        return $this->orm->define($this->class, ORMInterface::R_TABLE);
287
    }
288
289
    /**
290
     * @return AbstractNode
291
     */
292
    abstract protected function initNode(): AbstractNode;
293
294
    /**
295
     * Joined table alias.
296
     *
297
     * @return string
298
     */
299
    abstract protected function getAlias(): string;
300
301
    /**
302
     * list of columns to be loaded.
303
     *
304
     * @return array
305
     */
306
    abstract protected function getColumns(): array;
307
308
    /**
309
     * Check if given relation is actually chain of relations.
310
     *
311
     * @param string $relation
312
     *
313
     * @return bool
314
     */
315
    private function isChain(string $relation): bool
316
    {
317
        return strpos($relation, '.') !== false;
318
    }
319
320
    /**
321
     * @see loadRelation()
322
     * @see joinRelation()
323
     *
324
     * @param string $chain
325
     * @param array  $options Final loader options.
326
     * @param bool   $join    See loadRelation().
327
     *
328
     * @return LoaderInterface
329
     *
330
     * @throws LoaderException When one of chain elements is not actually chainable (let's say ODM
331
     *                         loader).
332
     */
333
    private function loadChain(string $chain, array $options, bool $join): LoaderInterface
334
    {
335
        $position = strpos($chain, '.');
336
337
        //Chain of relations provided (relation.nestedRelation)
338
        $child = $this->loadRelation(
339
            substr($chain, 0, $position),
340
            [],
341
            $join
342
        );
343
344
        if (!$child instanceof self) {
345
            throw new LoaderException(sprintf(
346
                "Loader '%s' does not support chain relation loading",
347
                get_class($child)
348
            ));
349
        }
350
351
        //Loading nested relation thought chain (chainOptions prior to user options)
352
        return $child->loadRelation(
353
            substr($chain, $position + 1),
354
            $options,
355
            $join
356
        );
357
    }
358
}