Passed
Pull Request — master (#185)
by
unknown
03:13
created

JoinableLoader::applyConstrain()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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