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()); |
|
|
|
|
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); |
|
|
|
|
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
|
|
|
|
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.