Completed
Branch feature/pre-split (823f56)
by Anton
03:27
created

AbstractLoader::getTable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * 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
            //Let's tell our loaded that it's method is JOIN (forced)
188
            $options['method'] = self::JOIN;
189
        }
190
191
        if (isset($loaders[$relation])) {
192
            //Overwriting existed loader options
193
            return $loaders[$relation] = $loaders[$relation]->withContext($this, $options);
194
        }
195
196
        try {
197
            //Creating new loader.
198
            $loader = $this->orm->makeLoader($this->class, $relation);
199
        } catch (ORMException $e) {
200
            throw new LoaderException("Unable to create loader", $e->getCode(), $e);
201
        }
202
203
        //Configuring loader scope
204
        return $loaders[$relation] = $loader->withContext($this, $options);
205
    }
206
207
    /**
208
     * {@inheritdoc}
209
     */
210
    final public function createNode(): AbstractNode
211
    {
212
        $node = $this->initNode();
213
214
        //Working with nested relation loaders
215
        foreach ($this->loaders as $relation => $loader) {
216
            $node->registerNode($relation, $loader->createNode());
217
        }
218
219
        return $node;
220
    }
221
222
    /**
223
     * @param AbstractNode $node
224
     */
225
    public function loadData(AbstractNode $node)
226
    {
227
        //Loading data thought child loaders
228
        foreach ($this->loaders as $relation => $loader) {
229
            $loader->loadData($node->fetchNode($relation));
230
        }
231
    }
232
233
    /**
234
     * Ensure state of every nested loader.
235
     */
236
    public function __clone()
237
    {
238
        foreach ($this->loaders as $name => $loader) {
239
            //Will automatically ensure nested change parents
240
            $this->loaders[$name] = $loader->withContext($this);
241
        }
242
243
        foreach ($this->joiners as $name => $loader) {
244
            //Will automatically ensure nested change parents
245
            $this->joiners[$name] = $loader->withContext($this);
246
        }
247
    }
248
249
    /**
250
     * Destruct loader.
251
     */
252
    final public function __destruct()
253
    {
254
        $this->loaders = [];
255
        $this->joiners = [];
256
    }
257
258
    /**
259
     * @param SelectQuery $query
260
     *
261
     * @return SelectQuery
262
     */
263
    protected function configureQuery(SelectQuery $query): SelectQuery
264
    {
265
        foreach ($this->loaders as $loader) {
266
            if ($loader instanceof RelationLoader && $loader->isJoined()) {
267
                $query = $loader->configureQuery(clone $query);
268
            }
269
        }
270
271
        foreach ($this->joiners as $loader) {
272
            $query = $loader->configureQuery(clone $query);
273
        }
274
275
        return $query;
276
    }
277
278
    /**
279
     * Get database name associated with relation
280
     *
281
     * @return string
282
     */
283
    protected function getDatabase(): string
284
    {
285
        return $this->orm->define($this->class, ORMInterface::R_DATABASE);
286
    }
287
288
    /**
289
     * Get table name associated with relation
290
     *
291
     * @return string
292
     */
293
    protected function getTable(): string
294
    {
295
        return $this->orm->define($this->class, ORMInterface::R_TABLE);
296
    }
297
298
    /**
299
     * @return AbstractNode
300
     */
301
    abstract protected function initNode(): AbstractNode;
302
303
    /**
304
     * Joined table alias.
305
     *
306
     * @return string
307
     */
308
    abstract protected function getAlias(): string;
309
310
    /**
311
     * list of columns to be loaded.
312
     *
313
     * @return array
314
     */
315
    abstract protected function getColumns(): array;
316
317
    /**
318
     * Check if given relation is actually chain of relations.
319
     *
320
     * @param string $relation
321
     *
322
     * @return bool
323
     */
324
    private function isChain(string $relation): bool
325
    {
326
        return strpos($relation, '.') !== false;
327
    }
328
329
    /**
330
     * @see loadRelation()
331
     * @see joinRelation()
332
     *
333
     * @param string $chain
334
     * @param array  $options Final loader options.
335
     * @param bool   $join    See loadRelation().
336
     *
337
     * @return LoaderInterface
338
     *
339
     * @throws LoaderException When one of chain elements is not actually chainable (let's say ODM
340
     *                         loader).
341
     */
342
    private function loadChain(string $chain, array $options, bool $join): LoaderInterface
343
    {
344
        $position = strpos($chain, '.');
345
346
        //Chain of relations provided (relation.nestedRelation)
347
        $child = $this->loadRelation(
348
            substr($chain, 0, $position),
349
            [],
350
            $join
351
        );
352
353
        if (!$child instanceof AbstractLoader) {
354
            throw new LoaderException(sprintf(
355
                "Loader '%s' does not support chain relation loading",
356
                get_class($child)
357
            ));
358
        }
359
360
        //Loading nested relation thought chain (chainOptions prior to user options)
361
        return $child->loadRelation(
362
            substr($chain, $position + 1),
363
            $options,
364
            $join
365
        );
366
    }
367
}