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

JoinableLoader::calculateAlias()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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

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

135
            $loader->setScope(/** @scrutinizer ignore-deprecated */ $this->getSource()->getConstrain());

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
136
        }
137
138
        if ($loader->isLoaded()) {
139
            foreach ($loader->getEagerRelations() as $relation) {
140
                $loader->loadRelation($relation, [], false, true);
141
            }
142
        }
143
144
        return $loader;
145
    }
146
147
    /**
148
     * {@inheritdoc}
149
     */
150
    public function loadData(AbstractNode $node): void
151
    {
152
        if ($this->isJoined() || !$this->isLoaded()) {
153
            // load data for all nested relations
154
            parent::loadData($node);
155
156
            return;
157
        }
158
159
        $references = $node->getReferences();
160
        if ($references === []) {
161
            // nothing found at parent level, unable to create sub query
162
            return;
163
        }
164
165
        //Ensure all nested relations
166
        $statement = $this->configureQuery($this->initQuery(), $references)->run();
167
168
        foreach ($statement->fetchAll(StatementInterface::FETCH_NUM) as $row) {
169
            try {
170
                $node->parseRow(0, $row);
171
            } catch (\Throwable $e) {
172
                throw $e;
173
            }
174
        }
175
176
        $statement->close();
177
178
        // load data for all nested relations
179
        parent::loadData($node);
180
    }
181
182
    /**
183
     * Indicated that loaded must generate JOIN statement.
184
     *
185
     * @return bool
186
     */
187
    public function isJoined(): bool
188
    {
189
        if (!empty($this->options['using'])) {
190
            return true;
191
        }
192
193
        return in_array($this->getMethod(), [self::INLOAD, self::JOIN, self::LEFT_JOIN], true);
194
    }
195
196
    /**
197
     * Indication that loader want to load data.
198
     *
199
     * @return bool
200
     */
201
    public function isLoaded(): bool
202
    {
203
        return $this->options['load'] || in_array($this->getMethod(), [self::INLOAD, self::POSTLOAD]);
204
    }
205
206
    /**
207
     * Configure query with conditions, joins and columns.
208
     *
209
     * @param SelectQuery $query
210
     * @param array       $outerKeys Set of OUTER_KEY values collected by parent loader.
211
     *
212
     * @return SelectQuery
213
     */
214
    public function configureQuery(SelectQuery $query, array $outerKeys = []): SelectQuery
215
    {
216
        if ($this->isLoaded()) {
217
            if ($this->isJoined()) {
218
                // mounting the columns to parent query
219
                $this->mountColumns($query, $this->options['minify']);
220
            } else {
221
                // this is initial set of columns (remove all existed)
222
                $this->mountColumns($query, $this->options['minify'], '', true);
223
            }
224
225
            if ($this->options['load'] instanceof ScopeInterface) {
226
                $this->options['load']->apply($this->makeQueryBuilder($query));
227
            }
228
229
            if (is_callable($this->options['load'], true)) {
230
                ($this->options['load'])($this->makeQueryBuilder($query));
231
            }
232
        }
233
234
        return parent::configureQuery($query);
235
    }
236
237
    public function isDataDuplicationPossible(): bool
238
    {
239
        $outerKey = $this->schema[Relation::OUTER_KEY];
240
        $indexes = $this->orm->getIndexes($this->target);
0 ignored issues
show
Bug introduced by
The method getIndexes() does not exist on Cycle\ORM\ORMInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Cycle\ORM\ORMInterface. ( Ignorable by Annotation )

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

240
        /** @scrutinizer ignore-call */ 
241
        $indexes = $this->orm->getIndexes($this->target);
Loading history...
241
242
        if (!\in_array($outerKey, $indexes, true)) {
243
            return true;
244
        }
245
246
        return parent::isDataDuplicationPossible();
247
    }
248
249
    /**
250
     * @param SelectQuery $query
251
     *
252
     * @return SelectQuery
253
     */
254
    protected function applyConstrain(SelectQuery $query): SelectQuery
255
    {
256
        if ($this->constrain !== null) {
257
            $this->constrain->apply($this->makeQueryBuilder($query));
258
        }
259
260
        return $query;
261
    }
262
263
    /**
264
     * Get load method.
265
     *
266
     * @return int
267
     */
268
    protected function getMethod(): int
269
    {
270
        return $this->options['method'];
271
    }
272
273
    /**
274
     * Create relation specific select query.
275
     *
276
     * @return SelectQuery
277
     */
278
    protected function initQuery(): SelectQuery
279
    {
280
        return $this->getSource()->getDatabase()->select()->from($this->getJoinTable());
281
    }
282
283
    /**
284
     * Calculate table alias.
285
     *
286
     * @param AbstractLoader $parent
287
     *
288
     * @return string
289
     */
290
    protected function calculateAlias(AbstractLoader $parent): string
291
    {
292
        if (!empty($this->options['as'])) {
293
            return $this->options['as'];
294
        }
295
296
        $alias = $parent->getAlias() . '_' . $this->name;
297
298
        if ($this->isLoaded() && $this->isJoined()) {
299
            // to avoid collisions
300
            return 'l_' . $alias;
301
        }
302
303
        return $alias;
304
    }
305
306
    /**
307
     * Generate sql identifier using loader alias and value from relation definition. Key name to be
308
     * fetched from schema.
309
     *
310
     * Example:
311
     * $this->getKey(Relation::OUTER_KEY);
312
     *
313
     * @param mixed $key
314
     *
315
     * @return string|null
316
     */
317
    protected function localKey($key): ?string
318
    {
319
        if (empty($this->schema[$key])) {
320
            return null;
321
        }
322
323
        return $this->getAlias() . '.' . $this->fieldAlias($this->schema[$key]);
324
    }
325
326
    /**
327
     * Get parent identifier based on relation configuration key.
328
     *
329
     * @param mixed $key
330
     *
331
     * @return string
332
     */
333
    protected function parentKey($key): string
334
    {
335
        return $this->parent->getAlias() . '.' . $this->parent->fieldAlias($this->schema[$key]);
336
    }
337
338
    /**
339
     * @return string
340
     */
341
    protected function getJoinMethod(): string
342
    {
343
        return $this->getMethod() == self::JOIN ? 'INNER' : 'LEFT';
344
    }
345
346
    /**
347
     * Joined table name and alias.
348
     *
349
     * @return string
350
     */
351
    protected function getJoinTable(): string
352
    {
353
        return "{$this->define(Schema::TABLE)} AS {$this->getAlias()}";
354
    }
355
356
    /**
357
     * Relation columns.
358
     *
359
     * @return array
360
     */
361
    protected function getColumns(): array
362
    {
363
        return $this->define(Schema::COLUMNS);
364
    }
365
366
    /**
367
     * @param SelectQuery $query
368
     *
369
     * @return QueryBuilder
370
     */
371
    private function makeQueryBuilder(SelectQuery $query): QueryBuilder
372
    {
373
        $builder = new QueryBuilder($query, $this);
374
        if ($this->isJoined()) {
375
            return $builder->withForward('onWhere');
376
        }
377
378
        return $builder;
379
    }
380
}
381