Passed
Pull Request — master (#185)
by
unknown
02:16
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 Cycle\ORM\Select\Traits\ScopeTrait;
24
use Spiral\Database\Query\SelectQuery;
25
26
/**
27
 * ORM Loaders used to load an compile data tree based on results fetched from SQL databases,
28
 * loaders can communicate with SelectQuery by providing it's own set of conditions, columns
29
 * joins and etc. In some cases loader may create additional selector to load data using information
30
 * fetched from previous query.
31
 *
32
 * Attention, AbstractLoader can only work with ORM Records, you must implement LoaderInterface
33
 * in order to support external references (MongoDB and etc).
34
 *
35
 * Loaders can be used for both - loading and filtering of record data.
36
 *
37
 * Reference tree generation logic example:
38
 *   User has many Posts (relation "posts"), user primary is ID, post inner key pointing to user
39
 *   is USER_ID. Post loader must request User data loader to create references based on ID field
40
 *   values. Once Post data were parsed we can mount it under parent user using mount method:
41
 *
42
 * @see Select::load()
43
 * @see Select::with()
44
 */
45
abstract class AbstractLoader implements LoaderInterface
46
{
47
    use ChainTrait;
48
    use AliasTrait;
49
    use ScopeTrait;
50
51
    // Loading methods for data loaders.
52
    public const INLOAD = 1;
53
    public const POSTLOAD = 2;
54
    public const JOIN = 3;
55
    public const LEFT_JOIN = 4;
56
57
    /** @var ORMInterface @internal */
58
    protected $orm;
59
60
    /** @var string */
61
    protected $target;
62
63
    /** @var array */
64
    protected $options = [
65
        'load' => false,
66
        'scope' => true,
67
    ];
68
69
    /** @var LoaderInterface[] */
70
    protected $load = [];
71
72
    /** @var AbstractLoader[] */
73
    protected $join = [];
74
75
    /** @var LoaderInterface @internal */
76
    protected $parent;
77
78
    /**
79
     * @param ORMInterface $orm
80
     * @param string $target
81
     */
82
    public function __construct(ORMInterface $orm, string $target)
83
    {
84
        $this->orm = $orm;
85
        $this->target = $target;
86
87
        $this->setScope($this->getSource()->getScope());
88
    }
89
90
    /**
91
     * Destruct loader.
92
     */
93
    final public function __destruct()
94
    {
95
        $this->parent = null;
96
        $this->load = [];
97
        $this->join = [];
98
    }
99
100
    /**
101
     * Ensure state of every nested loader.
102
     */
103
    public function __clone()
104
    {
105
        $this->parent = null;
106
107
        foreach ($this->load as $name => $loader) {
108
            $this->load[$name] = $loader->withContext($this);
109
        }
110
111
        foreach ($this->join as $name => $loader) {
112
            $this->join[$name] = $loader->withContext($this);
113
        }
114
    }
115
116
    /**
117
     * @return string
118
     */
119
    public function getTarget(): string
120
    {
121
        return $this->target;
122
    }
123
124
    /**
125
     * Data source associated with the loader.
126
     *
127
     * @return SourceInterface
128
     */
129
    public function getSource(): SourceInterface
130
    {
131
        return $this->orm->getSource($this->target);
132
    }
133
134
    /**
135
     * {@inheritdoc}
136
     */
137
    public function withContext(LoaderInterface $parent, array $options = []): LoaderInterface
138
    {
139
        $options = $this->prepareOptions($options);
140
141
        // check that given options are known
142
        if (!empty($wrong = array_diff(array_keys($options), array_keys($this->options)))) {
143
            throw new LoaderException(
144
                sprintf(
145
                    'Relation %s does not support option: %s',
146
                    get_class($this),
147
                    implode(',', $wrong)
148
                )
149
            );
150
        }
151
152
        $loader = clone $this;
153
        $loader->parent = $parent;
154
        $loader->options = $options + $this->options;
155
156
        return $loader;
157
    }
158
159
    /**
160
     * Load the relation.
161
     *
162
     * @param string $relation Relation name, or chain of relations separated by.
163
     * @param array $options Loader options (to be applied to last chain element only).
164
     * @param bool $join When set to true loaders will be forced into JOIN mode.
165
     * @param bool $load Load relation data.
166
     * @return LoaderInterface Must return loader for a requested relation.
167
     *
168
     * @throws LoaderException
169
     */
170
    public function loadRelation(
171
        string $relation,
172
        array $options,
173
        bool $join = false,
174
        bool $load = false
175
    ): LoaderInterface {
176
        $relation = $this->resolvePath($relation);
177
        if (!empty($options['as'])) {
178
            $this->registerPath($options['as'], $relation);
179
        }
180
181
        //Check if relation contain dot, i.e. relation chain
182
        if ($this->isChain($relation)) {
183
            return $this->loadChain($relation, $options, $join, $load);
184
        }
185
186
        /*
187
         * Joined loaders must be isolated from normal loaders due they would not load any data
188
         * and will only modify SelectQuery.
189
         */
190
        if (!$join || $load) {
191
            $loaders = &$this->load;
192
        } else {
193
            $loaders = &$this->join;
194
        }
195
196
        if ($load) {
197
            $options['load'] = $options['load'] ?? true;
198
        }
199
200
        if (isset($loaders[$relation])) {
201
            // overwrite existing loader options
202
            return $loaders[$relation] = $loaders[$relation]->withContext($this, $options);
203
        }
204
205
        if ($join) {
206
            if (empty($options['method']) || !in_array($options['method'], [self::JOIN, self::LEFT_JOIN], true)) {
207
                // let's tell our loaded that it's method is JOIN (forced)
208
                $options['method'] = self::JOIN;
209
            }
210
        }
211
212
        try {
213
            //Creating new loader.
214
            $loader = $this->orm->getFactory()->loader(
215
                $this->orm,
216
                $this->orm->getSchema(),
217
                $this->target,
218
                $relation
219
            );
220
        } catch (SchemaException | FactoryException $e) {
221
            throw new LoaderException(
222
                sprintf('Unable to create loader: %s', $e->getMessage()),
223
                $e->getCode(),
224
                $e
225
            );
226
        }
227
228
        return $loaders[$relation] = $loader->withContext($this, $options);
229
    }
230
231
    /**
232
     * {@inheritdoc}
233
     */
234
    public function createNode(): AbstractNode
235
    {
236
        $node = $this->initNode();
237
238
        foreach ($this->load as $relation => $loader) {
239
            if ($loader instanceof JoinableInterface && $loader->isJoined()) {
240
                $node->joinNode($relation, $loader->createNode());
241
                continue;
242
            }
243
244
            $node->linkNode($relation, $loader->createNode());
245
        }
246
247
        return $node;
248
    }
249
250
    /**
251
     * @param AbstractNode $node
252
     */
253
    public function loadData(AbstractNode $node): void
254
    {
255
        $this->loadChild($node);
256
    }
257
258
    /**
259
     * Indicates that loader loads data.
260
     *
261
     * @return bool
262
     */
263
    abstract public function isLoaded(): bool;
264
265
    /**
266
     * @param AbstractNode $node
267
     */
268
    protected function loadChild(AbstractNode $node): void
269
    {
270
        foreach ($this->load as $relation => $loader) {
271
            $loader->loadData($node->getNode($relation));
272
        }
273
    }
274
275
    /**
276
     * Create input node for the loader.
277
     *
278
     * @return AbstractNode
279
     */
280
    abstract protected function initNode(): AbstractNode;
281
282
    /**
283
     * @param SelectQuery $query
284
     * @return SelectQuery
285
     */
286
    protected function configureQuery(SelectQuery $query): SelectQuery
287
    {
288
        $query = $this->applyScope($query);
289
290
        foreach ($this->join as $loader) {
291
            $query = $loader->configureQuery($query);
292
        }
293
294
        foreach ($this->load as $loader) {
295
            if ($loader instanceof JoinableInterface && $loader->isJoined()) {
296
                $query = $loader->configureQuery($query);
297
            }
298
        }
299
300
        return $query;
301
    }
302
303
    /**
304
     * Define schema option associated with the entity.
305
     *
306
     * @param int $property
307
     * @return mixed
308
     */
309
    protected function define(int $property)
310
    {
311
        return $this->orm->getSchema()->define($this->target, $property);
312
    }
313
314
    /**
315
     * Returns list of relations to be automatically joined with parent object.
316
     *
317
     * @return \Generator
318
     */
319
    protected function getEagerRelations(): \Generator
320
    {
321
        $relations = $this->orm->getSchema()->define($this->target, Schema::RELATIONS) ?? [];
322
        foreach ($relations as $relation => $schema) {
323
            if (($schema[Relation::LOAD] ?? null) === Relation::LOAD_EAGER) {
324
                yield $relation;
325
            }
326
        }
327
    }
328
329
    protected function prepareOptions(array $options): array
330
    {
331
        if (array_key_exists('constrain', $options) && !array_key_exists('scope', $options)) {
332
            $options['scope'] = $options['constrain'];
333
        }
334
        unset($options['constrain']);
335
336
        return $options;
337
    }
338
}
339