Passed
Push — master ( c5cb83...e03db5 )
by Anton
02:08
created

JoinableLoader::configureQuery()   A

Complexity

Conditions 5
Paths 9

Size

Total Lines 21
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 5
eloc 10
nc 9
nop 2
dl 0
loc 21
rs 9.6111
c 2
b 0
f 1
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\LoaderException;
15
use Cycle\ORM\ORMInterface;
16
use Cycle\ORM\Parser\AbstractNode;
17
use Cycle\ORM\Schema;
18
use Cycle\ORM\Select\Traits\ColumnsTrait;
19
use Cycle\ORM\Select\Traits\ConstrainTrait;
20
use Spiral\Database\Query\SelectQuery;
21
use Spiral\Database\StatementInterface;
22
23
/**
24
 * Provides ability to load relation data in a form of JOIN or external query.
25
 */
26
abstract class JoinableLoader extends AbstractLoader implements JoinableInterface
27
{
28
    use ColumnsTrait;
29
    use ConstrainTrait;
30
31
    /**
32
     * Default set of relation options. Child implementation might defined their of default options.
33
     *
34
     * @var array
35
     */
36
    protected $options = [
37
        // load relation data
38
        'load'      => false,
39
40
        // true or instance to enable, false or null to disable
41
        'constrain' => true,
42
43
        // scope to be used for the relation
44
        'method'    => null,
45
46
        // load method, see AbstractLoader constants
47
        'minify'    => true,
48
49
        // when true all loader columns will be minified (only for loading)
50
        'as'        => null,
51
52
        // table alias
53
        'using'     => null,
54
55
        // alias used by another relation
56
        'where'     => null,
57
58
        // where conditions (if any)
59
    ];
60
61
    /** @var string */
62
    protected $name;
63
64
    /** @var array */
65
    protected $schema;
66
67
    /**
68
     * @param ORMInterface $orm
69
     * @param string       $name
70
     * @param string       $target
71
     * @param array        $schema
72
     */
73
    public function __construct(ORMInterface $orm, string $name, string $target, array $schema)
74
    {
75
        parent::__construct($orm, $target);
76
77
        $this->name = $name;
78
        $this->schema = $schema;
79
    }
80
81
    /**
82
     * Relation table alias.
83
     *
84
     * @return string
85
     */
86
    public function getAlias(): string
87
    {
88
        if ($this->options['using'] !== null) {
89
            //We are using another relation (presumably defined by with() to load data).
90
            return $this->options['using'];
91
        }
92
93
        if ($this->options['as'] !== null) {
94
            return $this->options['as'];
95
        }
96
97
        throw new LoaderException('Unable to resolve loader alias');
98
    }
99
100
    /**
101
     * {@inheritdoc}
102
     */
103
    public function withContext(LoaderInterface $parent, array $options = []): LoaderInterface
104
    {
105
        /**
106
         * @var AbstractLoader $parent
107
         * @var self           $loader
108
         */
109
        $loader = parent::withContext($parent, $options);
110
111
        if ($loader->getSource()->getDatabase() !== $parent->getSource()->getDatabase()) {
112
            if ($loader->isJoined()) {
113
                throw new LoaderException('Unable to join tables located in different databases');
114
            }
115
116
            // loader is not joined, let's make sure that POSTLOAD is used
117
            if ($this->isLoaded()) {
118
                $loader->options['method'] = self::POSTLOAD;
119
            }
120
        }
121
122
        //Calculate table alias
123
        $loader->options['as'] = $loader->calculateAlias($parent);
124
125
        if (array_key_exists('constrain', $options)) {
126
            if ($loader->options['constrain'] instanceof ConstrainInterface) {
127
                $loader->setConstrain($loader->options['constrain']);
128
            } elseif (is_string($loader->options['constrain'])) {
129
                $loader->setConstrain($this->orm->getFactory()->make($loader->options['constrain']));
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

129
                $loader->setConstrain($this->orm->/** @scrutinizer ignore-call */ getFactory()->make($loader->options['constrain']));
Loading history...
130
            }
131
        } else {
132
            $loader->setConstrain($this->getSource()->getConstrain());
133
        }
134
135
        if ($this->isLoaded()) {
136
            foreach ($loader->getEagerRelations() as $relation) {
137
                $loader->loadRelation($relation, [], false, true);
138
            }
139
        }
140
141
        return $loader;
142
    }
143
144
    /**
145
     * {@inheritdoc}
146
     */
147
    public function loadData(AbstractNode $node): void
148
    {
149
        if ($this->isJoined() || !$this->isLoaded()) {
150
            // load data for all nested relations
151
            parent::loadData($node);
152
153
            return;
154
        }
155
156
        $references = $node->getReferences();
157
        if ($references === []) {
158
            // nothing found at parent level, unable to create sub query
159
            return;
160
        }
161
162
        //Ensure all nested relations
163
        $statement = $this->configureQuery($this->initQuery(), $references)->run();
164
165
        foreach ($statement->fetchAll(StatementInterface::FETCH_NUM) as $row) {
166
            try {
167
                $node->parseRow(0, $row);
168
            } catch (\Throwable $e) {
169
                echo 'x';
170
                throw $e;
171
            }
172
        }
173
174
        $statement->close();
175
176
        // load data for all nested relations
177
        parent::loadData($node);
178
    }
179
180
    /**
181
     * Indicated that loaded must generate JOIN statement.
182
     *
183
     * @return bool
184
     */
185
    public function isJoined(): bool
186
    {
187
        if (!empty($this->options['using'])) {
188
            return true;
189
        }
190
191
        return in_array($this->getMethod(), [self::INLOAD, self::JOIN, self::LEFT_JOIN], true);
192
    }
193
194
    /**
195
     * Indication that loader want to load data.
196
     *
197
     * @return bool
198
     */
199
    public function isLoaded(): bool
200
    {
201
        return $this->options['load'] || in_array($this->getMethod(), [self::INLOAD, self::POSTLOAD]);
202
    }
203
204
    /**
205
     * Configure query with conditions, joins and columns.
206
     *
207
     * @param SelectQuery $query
208
     * @param array       $outerKeys Set of OUTER_KEY values collected by parent loader.
209
     * @return SelectQuery
210
     */
211
    public function configureQuery(SelectQuery $query, array $outerKeys = []): SelectQuery
212
    {
213
        if ($this->isLoaded()) {
214
            if ($this->isJoined()) {
215
                // mounting the columns to parent query
216
                $this->mountColumns($query, $this->options['minify']);
217
            } else {
218
                // this is initial set of columns (remove all existed)
219
                $this->mountColumns($query, $this->options['minify'], '', true);
220
            }
221
222
            if ($this->options['load'] instanceof ConstrainInterface) {
223
                $this->options['load']->apply($this->makeQueryBuilder($query));
224
            }
225
226
            if (is_callable($this->options['load'], true)) {
227
                ($this->options['load'])($this->makeQueryBuilder($query));
228
            }
229
        }
230
231
        return parent::configureQuery($query);
232
    }
233
234
    /**
235
     * @param SelectQuery $query
236
     * @return SelectQuery
237
     */
238
    protected function applyConstrain(SelectQuery $query): SelectQuery
239
    {
240
        if ($this->constrain !== null) {
241
            $this->constrain->apply($this->makeQueryBuilder($query));
242
        }
243
244
        return $query;
245
    }
246
247
    /**
248
     * Get load method.
249
     *
250
     * @return int
251
     */
252
    protected function getMethod(): int
253
    {
254
        return $this->options['method'];
255
    }
256
257
    /**
258
     * Create relation specific select query.
259
     *
260
     * @return SelectQuery
261
     */
262
    protected function initQuery(): SelectQuery
263
    {
264
        return $this->getSource()->getDatabase()->select()->from($this->getJoinTable());
265
    }
266
267
    /**
268
     * Calculate table alias.
269
     *
270
     * @param AbstractLoader $parent
271
     * @return string
272
     */
273
    protected function calculateAlias(AbstractLoader $parent): string
274
    {
275
        if (!empty($this->options['as'])) {
276
            return $this->options['as'];
277
        }
278
279
        $alias = $parent->getAlias() . '_' . $this->name;
280
281
        if ($this->isLoaded() && $this->isJoined()) {
282
            // to avoid collisions
283
            return 'l_' . $alias;
284
        }
285
286
        return $alias;
287
    }
288
289
    /**
290
     * Generate sql identifier using loader alias and value from relation definition. Key name to be
291
     * fetched from schema.
292
     *
293
     * Example:
294
     * $this->getKey(Relation::OUTER_KEY);
295
     *
296
     * @param mixed $key
297
     * @return string|null
298
     */
299
    protected function localKey($key): ?string
300
    {
301
        if (empty($this->schema[$key])) {
302
            return null;
303
        }
304
305
        return $this->getAlias() . '.' . $this->fieldAlias($this->schema[$key]);
306
    }
307
308
    /**
309
     * Get parent identifier based on relation configuration key.
310
     *
311
     * @param mixed $key
312
     * @return string
313
     */
314
    protected function parentKey($key): string
315
    {
316
        return $this->parent->getAlias() . '.' . $this->parent->fieldAlias($this->schema[$key]);
317
    }
318
319
    /**
320
     * @return string
321
     */
322
    protected function getJoinMethod(): string
323
    {
324
        return $this->getMethod() == self::JOIN ? 'INNER' : 'LEFT';
325
    }
326
327
    /**
328
     * Joined table name and alias.
329
     *
330
     * @return string
331
     */
332
    protected function getJoinTable(): string
333
    {
334
        return "{$this->define(Schema::TABLE)} AS {$this->getAlias()}";
335
    }
336
337
    /**
338
     * Relation columns.
339
     *
340
     * @return array
341
     */
342
    protected function getColumns(): array
343
    {
344
        return $this->define(Schema::COLUMNS);
345
    }
346
347
    /**
348
     * @param SelectQuery $query
349
     * @return QueryBuilder
350
     */
351
    private function makeQueryBuilder(SelectQuery $query): QueryBuilder
352
    {
353
        $builder = new QueryBuilder($query, $this);
354
        if ($this->isJoined()) {
355
            return $builder->withForward('onWhere');
356
        }
357
358
        return $builder;
359
    }
360
}
361