Passed
Pull Request — master (#185)
by
unknown
02:58
created

AbstractLoader::prepareOptions()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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