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

JoinableLoader::applyScope()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 7
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\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 Spiral\Database\Query\SelectQuery;
20
use Spiral\Database\StatementInterface;
21
22
/**
23
 * Provides ability to load relation data in a form of JOIN or external query.
24
 */
25
abstract class JoinableLoader extends AbstractLoader implements JoinableInterface
26
{
27
    use ColumnsTrait;
28
29
    /**
30
     * Default set of relation options. Child implementation might defined their of default options.
31
     *
32
     * @var array
33
     */
34
    protected $options = [
35
        // load relation data
36
        'load' => false,
37
38
        // true or instance to enable, false or null to disable
39
        'scope' => true,
40
41
        // scope to be used for the relation
42
        'method' => null,
43
44
        // load method, see AbstractLoader constants
45
        'minify' => true,
46
47
        // when true all loader columns will be minified (only for loading)
48
        'as' => null,
49
50
        // table alias
51
        'using' => null,
52
53
        // alias used by another relation
54
        'where' => null,
55
56
        // where conditions (if any)
57
    ];
58
59
    /** @var string */
60
    protected $name;
61
62
    /** @var array */
63
    protected $schema;
64
65
    /**
66
     * @param ORMInterface $orm
67
     * @param string $name
68
     * @param string $target
69
     * @param array $schema
70
     */
71
    public function __construct(ORMInterface $orm, string $name, string $target, array $schema)
72
    {
73
        parent::__construct($orm, $target);
74
75
        $this->name = $name;
76
        $this->schema = $schema;
77
    }
78
79
    /**
80
     * Relation table alias.
81
     *
82
     * @return string
83
     */
84
    public function getAlias(): string
85
    {
86
        if ($this->options['using'] !== null) {
87
            //We are using another relation (presumably defined by with() to load data).
88
            return $this->options['using'];
89
        }
90
91
        if ($this->options['as'] !== null) {
92
            return $this->options['as'];
93
        }
94
95
        throw new LoaderException('Unable to resolve loader alias');
96
    }
97
98
    /**
99
     * {@inheritdoc}
100
     */
101
    public function withContext(LoaderInterface $parent, array $options = []): LoaderInterface
102
    {
103
        $options = $this->prepareOptions($options);
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('scope', $options)) {
126
            if ($loader->options['scope'] instanceof ScopeInterface) {
127
                $loader->setScope($loader->options['scope']);
128
            } elseif (is_string($loader->options['scope'])) {
129
                $loader->setScope($this->orm->getFactory()->make($loader->options['scope']));
130
            }
131
        }
132
133
        if ($loader->isLoaded()) {
134
            foreach ($loader->getEagerRelations() as $relation) {
135
                $loader->loadRelation($relation, [], false, true);
136
            }
137
        }
138
139
        return $loader;
140
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145
    public function loadData(AbstractNode $node): void
146
    {
147
        if ($this->isJoined() || !$this->isLoaded()) {
148
            // load data for all nested relations
149
            parent::loadData($node);
150
151
            return;
152
        }
153
154
        $references = $node->getReferences();
155
        if ($references === []) {
156
            // nothing found at parent level, unable to create sub query
157
            return;
158
        }
159
160
        //Ensure all nested relations
161
        $statement = $this->configureQuery($this->initQuery(), $references)->run();
162
163
        foreach ($statement->fetchAll(StatementInterface::FETCH_NUM) as $row) {
164
            try {
165
                $node->parseRow(0, $row);
166
            } catch (\Throwable $e) {
167
                throw $e;
168
            }
169
        }
170
171
        $statement->close();
172
173
        // load data for all nested relations
174
        parent::loadData($node);
175
    }
176
177
    /**
178
     * Indicated that loaded must generate JOIN statement.
179
     *
180
     * @return bool
181
     */
182
    public function isJoined(): bool
183
    {
184
        if (!empty($this->options['using'])) {
185
            return true;
186
        }
187
188
        return in_array($this->getMethod(), [self::INLOAD, self::JOIN, self::LEFT_JOIN], true);
189
    }
190
191
    /**
192
     * Indication that loader want to load data.
193
     *
194
     * @return bool
195
     */
196
    public function isLoaded(): bool
197
    {
198
        return $this->options['load'] || in_array($this->getMethod(), [self::INLOAD, self::POSTLOAD]);
199
    }
200
201
    /**
202
     * Configure query with conditions, joins and columns.
203
     *
204
     * @param SelectQuery $query
205
     * @param array $outerKeys Set of OUTER_KEY values collected by parent loader.
206
     * @return SelectQuery
207
     */
208
    public function configureQuery(SelectQuery $query, array $outerKeys = []): SelectQuery
209
    {
210
        if ($this->isLoaded()) {
211
            if ($this->isJoined()) {
212
                // mounting the columns to parent query
213
                $this->mountColumns($query, $this->options['minify']);
214
            } else {
215
                // this is initial set of columns (remove all existed)
216
                $this->mountColumns($query, $this->options['minify'], '', true);
217
            }
218
219
            if ($this->options['load'] instanceof ScopeInterface) {
220
                $this->options['load']->apply($this->makeQueryBuilder($query));
221
            }
222
223
            if (is_callable($this->options['load'], true)) {
224
                ($this->options['load'])($this->makeQueryBuilder($query));
225
            }
226
        }
227
228
        return parent::configureQuery($query);
229
    }
230
231
    /**
232
     * @param SelectQuery $query
233
     * @return SelectQuery
234
     */
235
    protected function applyScope(SelectQuery $query): SelectQuery
236
    {
237
        if ($this->scope !== null) {
238
            $this->scope->apply($this->makeQueryBuilder($query));
239
        }
240
241
        return $query;
242
    }
243
244
    /**
245
     * Get load method.
246
     *
247
     * @return int
248
     */
249
    protected function getMethod(): int
250
    {
251
        return $this->options['method'];
252
    }
253
254
    /**
255
     * Create relation specific select query.
256
     *
257
     * @return SelectQuery
258
     */
259
    protected function initQuery(): SelectQuery
260
    {
261
        return $this->getSource()->getDatabase()->select()->from($this->getJoinTable());
262
    }
263
264
    /**
265
     * Calculate table alias.
266
     *
267
     * @param AbstractLoader $parent
268
     * @return string
269
     */
270
    protected function calculateAlias(AbstractLoader $parent): string
271
    {
272
        if (!empty($this->options['as'])) {
273
            return $this->options['as'];
274
        }
275
276
        $alias = $parent->getAlias() . '_' . $this->name;
277
278
        if ($this->isLoaded() && $this->isJoined()) {
279
            // to avoid collisions
280
            return 'l_' . $alias;
281
        }
282
283
        return $alias;
284
    }
285
286
    /**
287
     * Generate sql identifier using loader alias and value from relation definition. Key name to be
288
     * fetched from schema.
289
     *
290
     * Example:
291
     * $this->getKey(Relation::OUTER_KEY);
292
     *
293
     * @param mixed $key
294
     * @return string|null
295
     */
296
    protected function localKey($key): ?string
297
    {
298
        if (empty($this->schema[$key])) {
299
            return null;
300
        }
301
302
        return $this->getAlias() . '.' . $this->fieldAlias($this->schema[$key]);
303
    }
304
305
    /**
306
     * Get parent identifier based on relation configuration key.
307
     *
308
     * @param mixed $key
309
     * @return string
310
     */
311
    protected function parentKey($key): string
312
    {
313
        return $this->parent->getAlias() . '.' . $this->parent->fieldAlias($this->schema[$key]);
314
    }
315
316
    /**
317
     * @return string
318
     */
319
    protected function getJoinMethod(): string
320
    {
321
        return $this->getMethod() == self::JOIN ? 'INNER' : 'LEFT';
322
    }
323
324
    /**
325
     * Joined table name and alias.
326
     *
327
     * @return string
328
     */
329
    protected function getJoinTable(): string
330
    {
331
        return "{$this->define(Schema::TABLE)} AS {$this->getAlias()}";
332
    }
333
334
    /**
335
     * Relation columns.
336
     *
337
     * @return array
338
     */
339
    protected function getColumns(): array
340
    {
341
        return $this->define(Schema::COLUMNS);
342
    }
343
344
    /**
345
     * @param SelectQuery $query
346
     * @return QueryBuilder
347
     */
348
    private function makeQueryBuilder(SelectQuery $query): QueryBuilder
349
    {
350
        $builder = new QueryBuilder($query, $this);
351
        if ($this->isJoined()) {
352
            return $builder->withForward('onWhere');
353
        }
354
355
        return $builder;
356
    }
357
}
358