Completed
Push — master ( 575bb9...ca6359 )
by Anton
02:20
created

AbstractLoader::getEagerRelations()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 3
nc 3
nop 0
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Cycle DataMapper ORM
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
declare(strict_types=1);
9
10
namespace Cycle\ORM\Select;
11
12
use Cycle\ORM\Exception\FactoryException;
13
use Cycle\ORM\Exception\LoaderException;
14
use Cycle\ORM\Exception\SchemaException;
15
use Cycle\ORM\ORMInterface;
16
use Cycle\ORM\Parser\AbstractNode;
17
use Cycle\ORM\Relation;
18
use Cycle\ORM\Schema;
19
use Cycle\ORM\Select\Traits\AliasTrait;
20
use Cycle\ORM\Select\Traits\ChainTrait;
21
use Spiral\Database\Query\SelectQuery;
22
23
/**
24
 * ORM Loaders used to load an compile data tree based on results fetched from SQL databases,
25
 * loaders can communicate with SelectQuery by providing it's own set of conditions, columns
26
 * joins and etc. In some cases loader may create additional selector to load data using information
27
 * fetched from previous query.
28
 *
29
 * Attention, AbstractLoader can only work with ORM Records, you must implement LoaderInterface
30
 * in order to support external references (MongoDB and etc).
31
 *
32
 * Loaders can be used for both - loading and filtering of record data.
33
 *
34
 * Reference tree generation logic example:
35
 *   User has many Posts (relation "posts"), user primary is ID, post inner key pointing to user
36
 *   is USER_ID. Post loader must request User data loader to create references based on ID field
37
 *   values. Once Post data were parsed we can mount it under parent user using mount method:
38
 *
39
 * @see Select::load()
40
 * @see Select::with()
41
 */
42
abstract class AbstractLoader implements LoaderInterface
43
{
44
    use ChainTrait, AliasTrait;
45
46
    // Loading methods for data loaders.
47
    public const INLOAD    = 1;
48
    public const POSTLOAD  = 2;
49
    public const JOIN      = 3;
50
    public const LEFT_JOIN = 4;
51
52
    /** @var ORMInterface|SourceProviderInterface @internal */
53
    protected $orm;
54
55
    /** @var string */
56
    protected $target;
57
58
    /** @var array */
59
    protected $options = [
60
        'load'      => false,
61
        'constrain' => true,
62
    ];
63
64
    /** @var LoaderInterface[] */
65
    protected $load = [];
66
67
    /** @var AbstractLoader[] */
68
    protected $join = [];
69
70
    /** @var LoaderInterface @internal */
71
    protected $parent;
72
73
    /**
74
     * @param ORMInterface $orm
75
     * @param string       $target
76
     */
77
    public function __construct(ORMInterface $orm, string $target)
78
    {
79
        $this->orm = $orm;
80
        $this->target = $target;
81
    }
82
83
    /**
84
     * @return string
85
     */
86
    public function getTarget(): string
87
    {
88
        return $this->target;
89
    }
90
91
    /**
92
     * Data source associated with the loader.
93
     *
94
     * @return SourceInterface
95
     */
96
    public function getSource(): SourceInterface
97
    {
98
        return $this->orm->getSource($this->target);
99
    }
100
101
    /**
102
     * {@inheritdoc}
103
     */
104
    public function withContext(LoaderInterface $parent, array $options = []): LoaderInterface
105
    {
106
        // check that given options are known
107
        if (!empty($wrong = array_diff(array_keys($options), array_keys($this->options)))) {
108
            throw new LoaderException(sprintf(
109
                "Relation %s does not support option: %s",
110
                get_class($this),
111
                join(',', $wrong)
112
            ));
113
        }
114
115
        $loader = clone $this;
116
        $loader->parent = $parent;
117
        $loader->options = $options + $this->options;
118
119
        return $loader;
120
    }
121
122
    /**
123
     * Load the relation.
124
     *
125
     * @param string $relation Relation name, or chain of relations separated by.
126
     * @param array  $options  Loader options (to be applied to last chain element only).
127
     * @param bool   $join     When set to true loaders will be forced into JOIN mode.
128
     * @param bool   $load     Load relation data.
129
     * @return LoaderInterface Must return loader for a requested relation.
130
     *
131
     * @throws LoaderException
132
     */
133
    public function loadRelation(
134
        string $relation,
135
        array $options,
136
        bool $join = false,
137
        bool $load = false
138
    ): LoaderInterface {
139
        $relation = $this->resolvePath($relation);
140
        if (!empty($options['as'])) {
141
            $this->registerPath($options['as'], $relation);
142
        }
143
144
        //Check if relation contain dot, i.e. relation chain
145
        if ($this->isChain($relation)) {
146
            return $this->loadChain($relation, $options, $join, $load);
147
        }
148
149
        /*
150
         * Joined loaders must be isolated from normal loaders due they would not load any data
151
         * and will only modify SelectQuery.
152
         */
153
        if (!$join || $load) {
154
            $loaders = &$this->load;
155
        } else {
156
            $loaders = &$this->join;
157
        }
158
159
        if ($load) {
160
            $options['load'] = true;
161
        }
162
163
        if ($join) {
164
            if (empty($options['method']) || !in_array($options['method'], [self::JOIN, self::LEFT_JOIN])) {
165
                // let's tell our loaded that it's method is JOIN (forced)
166
                $options['method'] = self::JOIN;
167
            }
168
        }
169
170
        if (isset($loaders[$relation])) {
171
            // overwrite existing loader options
172
            return $loaders[$relation] = $loaders[$relation]->withContext($this, $options);
173
        }
174
175
        try {
176
            //Creating new loader.
177
            $loader = $this->orm->getFactory()->loader(
0 ignored issues
show
Bug introduced by
The method getFactory() does not exist on Cycle\ORM\Select\SourceProviderInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Cycle\ORM\Select\SourceProviderInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

177
            $loader = $this->orm->/** @scrutinizer ignore-call */ getFactory()->loader(
Loading history...
178
                $this->orm,
179
                $this->orm->getSchema(),
0 ignored issues
show
Bug introduced by
The method getSchema() does not exist on Cycle\ORM\Select\SourceProviderInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Cycle\ORM\Select\SourceProviderInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

179
                $this->orm->/** @scrutinizer ignore-call */ 
180
                            getSchema(),
Loading history...
180
                $this->target,
181
                $relation
182
            );
183
        } catch (SchemaException|FactoryException $e) {
184
            throw new LoaderException(
185
                sprintf("Unable to create loader: %s", $e->getMessage()),
186
                $e->getCode(),
187
                $e
188
            );
189
        }
190
191
        return $loaders[$relation] = $loader->withContext($this, $options);
192
    }
193
194
    /**
195
     * {@inheritdoc}
196
     */
197
    public function createNode(): AbstractNode
198
    {
199
        $node = $this->initNode();
200
201
        foreach ($this->load as $relation => $loader) {
202
            if ($loader instanceof JoinableLoader && $loader->isJoined()) {
203
                $node->joinNode($relation, $loader->createNode());
204
                continue;
205
            }
206
207
            $node->linkNode($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->load as $relation => $loader) {
220
            $loader->loadData($node->getNode($relation));
221
        }
222
    }
223
224
    /**
225
     * Ensure state of every nested loader.
226
     */
227
    public function __clone()
228
    {
229
        $this->parent = null;
230
231
        foreach ($this->load as $name => $loader) {
232
            $this->load[$name] = $loader->withContext($this);
233
        }
234
235
        foreach ($this->join as $name => $loader) {
236
            $this->join[$name] = $loader->withContext($this);
237
        }
238
    }
239
240
    /**
241
     * Destruct loader.
242
     */
243
    final public function __destruct()
244
    {
245
        $this->parent = null;
246
        $this->load = [];
247
        $this->join = [];
248
    }
249
250
    /**
251
     * Create input node for the loader.
252
     *
253
     * @return AbstractNode
254
     */
255
    abstract protected function initNode(): AbstractNode;
256
257
    /**
258
     * @param SelectQuery $query
259
     * @return SelectQuery
260
     */
261
    protected function configureQuery(SelectQuery $query): SelectQuery
262
    {
263
        $query = $this->applyConstrain(clone $query);
264
        foreach ($this->load as $loader) {
265
            if ($loader instanceof JoinableLoader && $loader->isJoined()) {
266
                $query = $loader->configureQuery($query);
267
            }
268
        }
269
270
        foreach ($this->join as $loader) {
271
            $query = $loader->configureQuery($query);
272
        }
273
274
        return $query;
275
    }
276
277
    /**
278
     * @param SelectQuery $query
279
     * @return SelectQuery
280
     */
281
    abstract protected function applyConstrain(SelectQuery $query): SelectQuery;
282
283
    /**
284
     * Define schema option associated with the entity.
285
     *
286
     * @param int $property
287
     * @return mixed
288
     */
289
    protected function define(int $property)
290
    {
291
        return $this->orm->getSchema()->define($this->target, $property);
292
    }
293
294
    /**
295
     * Returns list of relations to be automatically joined with parent object.
296
     *
297
     * @return \Generator
298
     */
299
    protected function getEagerRelations(): \Generator
300
    {
301
        foreach ($this->orm->getSchema()->define($this->target, Schema::RELATIONS) as $relation => $schema) {
302
            if (($schema[Relation::FETCH] ?? null) == Relation::FETCH_EAGER) {
303
                yield $relation;
304
            }
305
        }
306
    }
307
}