Passed
Pull Request — master (#239)
by
unknown
03:30
created

AbstractLoader::loadData()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
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 AliasTrait;
47
    use ChainTrait;
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 @internal */
56
    protected $orm;
57
58
    /** @var string */
59
    protected $target;
60
61
    /** @var array */
62
    protected $options = [
63
        'load' => false,
64
        'scope' => 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
        $options = $this->prepareOptions($options);
136
137
        // check that given options are known
138
        if (!empty($wrong = array_diff(array_keys($options), array_keys($this->options)))) {
139
            throw new LoaderException(
140
                sprintf(
141
                    'Relation %s does not support option: %s',
142
                    static::class,
143
                    implode(',', $wrong)
144
                )
145
            );
146
        }
147
148
        $loader = clone $this;
149
        $loader->parent = $parent;
150
        $loader->options = $options + $this->options;
151
152
        return $loader;
153
    }
154
155
    /**
156
     * Load the relation.
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
     * @param bool   $load     Load relation data.
162
     *
163
     * @throws LoaderException
164
     *
165
     * @return LoaderInterface Must return loader for a requested relation.
166
     */
167
    public function loadRelation(
168
        string $relation,
169
        array $options,
170
        bool $join = false,
171
        bool $load = false
172
    ): LoaderInterface {
173
        $relation = $this->resolvePath($relation);
174
        if (!empty($options['as'])) {
175
            $this->registerPath($options['as'], $relation);
176
        }
177
178
        //Check if relation contain dot, i.e. relation chain
179
        if ($this->isChain($relation)) {
180
            return $this->loadChain($relation, $options, $join, $load);
181
        }
182
183
        /*
184
         * Joined loaders must be isolated from normal loaders due they would not load any data
185
         * and will only modify SelectQuery.
186
         */
187
        if (!$join || $load) {
188
            $loaders = &$this->load;
189
        } else {
190
            $loaders = &$this->join;
191
        }
192
193
        if ($load) {
194
            $options['load'] = $options['load'] ?? true;
195
        }
196
197
        if (isset($loaders[$relation])) {
198
            // overwrite existing loader options
199
            return $loaders[$relation] = $loaders[$relation]->withContext($this, $options);
200
        }
201
202
        if ($join) {
203
            if (empty($options['method']) || !in_array($options['method'], [self::JOIN, self::LEFT_JOIN], true)) {
204
                // let's tell our loaded that it's method is JOIN (forced)
205
                $options['method'] = self::JOIN;
206
            }
207
        }
208
209
        try {
210
            //Creating new loader.
211
            $loader = $this->orm->getFactory()->loader(
212
                $this->orm,
213
                $this->orm->getSchema(),
214
                $this->target,
215
                $relation
216
            );
217
        } catch (SchemaException | FactoryException $e) {
218
            throw new LoaderException(
219
                sprintf('Unable to create loader: %s', $e->getMessage()),
220
                $e->getCode(),
221
                $e
222
            );
223
        }
224
225
        return $loaders[$relation] = $loader->withContext($this, $options);
226
    }
227
228
    /**
229
     * {@inheritdoc}
230
     */
231
    public function createNode(): AbstractNode
232
    {
233
        $node = $this->initNode();
234
235
        foreach ($this->load as $relation => $loader) {
236
            if ($loader instanceof JoinableInterface && $loader->isJoined()) {
237
                $node->joinNode($relation, $loader->createNode());
238
                continue;
239
            }
240
241
            $node->linkNode($relation, $loader->createNode());
242
        }
243
244
        return $node;
245
    }
246
247
    /**
248
     * @param AbstractNode $node
249
     */
250
    public function loadData(AbstractNode $node): void
251
    {
252
        $this->loadChild($node);
253
    }
254
255
    /**
256
     * Indicates that loader loads data.
257
     *
258
     * @return bool
259
     */
260
    abstract public function isLoaded(): bool;
261
262
    /**
263
     * Indicates that query can load multiple joined rows,
264
     * which possibly can lead to data duplication.
265
     *
266
     * @return bool
267
     */
268
    public function isDataDuplicationPossible(): bool
269
    {
270
        foreach ($this->join as $loader) {
271
            if ($loader->isDataDuplicationPossible()) {
272
                return true;
273
            }
274
        }
275
276
        return false;
277
    }
278
279
    /**
280
     * @param AbstractNode $node
281
     */
282
    protected function loadChild(AbstractNode $node): void
283
    {
284
        foreach ($this->load as $relation => $loader) {
285
            $loader->loadData($node->getNode($relation));
286
        }
287
    }
288
289
    /**
290
     * Create input node for the loader.
291
     *
292
     * @return AbstractNode
293
     */
294
    abstract protected function initNode(): AbstractNode;
295
296
    /**
297
     * @param SelectQuery $query
298
     *
299
     * @return SelectQuery
300
     */
301
    protected function configureQuery(SelectQuery $query): SelectQuery
302
    {
303
        $query = $this->applyConstrain($query);
304
305
        foreach ($this->join as $loader) {
306
            $query = $loader->configureQuery($query);
307
        }
308
309
        foreach ($this->load as $loader) {
310
            if ($loader instanceof JoinableInterface && $loader->isJoined()) {
311
                $query = $loader->configureQuery($query);
312
            }
313
        }
314
315
        return $query;
316
    }
317
318
    /**
319
     * @param SelectQuery $query
320
     *
321
     * @return SelectQuery
322
     */
323
    abstract protected function applyConstrain(SelectQuery $query): SelectQuery;
324
325
    /**
326
     * Define schema option associated with the entity.
327
     *
328
     * @param int $property
329
     *
330
     * @return mixed
331
     */
332
    protected function define(int $property)
333
    {
334
        return $this->orm->getSchema()->define($this->target, $property);
335
    }
336
337
    /**
338
     * Returns list of relations to be automatically joined with parent object.
339
     *
340
     * @return \Generator
341
     */
342
    protected function getEagerRelations(): \Generator
343
    {
344
        $relations = $this->orm->getSchema()->define($this->target, Schema::RELATIONS) ?? [];
345
        foreach ($relations as $relation => $schema) {
346
            if (($schema[Relation::LOAD] ?? null) === Relation::LOAD_EAGER) {
347
                yield $relation;
348
            }
349
        }
350
    }
351
352
    protected function prepareOptions(array $options): array
353
    {
354
        if (array_key_exists('constrain', $options) && !array_key_exists('scope', $options)) {
355
            $options['scope'] = $options['constrain'];
356
        }
357
        unset($options['constrain']);
358
359
        return $options;
360
    }
361
}
362