1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Bdf\Prime\Query\Compiler\AliasResolver; |
4
|
|
|
|
5
|
|
|
use Bdf\Prime\Mapper\Metadata; |
6
|
|
|
use Bdf\Prime\Query\Contract\EntityJoinable; |
7
|
|
|
use Bdf\Prime\Query\QueryInterface; |
8
|
|
|
use Bdf\Prime\Relations\Exceptions\RelationNotFoundException; |
9
|
|
|
use Bdf\Prime\Repository\RepositoryInterface; |
10
|
|
|
use Bdf\Prime\Types\TypesRegistryInterface; |
11
|
|
|
|
12
|
|
|
/** |
13
|
|
|
* Create and resolve query alias and relation paths |
14
|
|
|
* |
15
|
|
|
* @internal |
16
|
|
|
*/ |
17
|
|
|
class AliasResolver |
18
|
|
|
{ |
19
|
|
|
/** |
20
|
|
|
* @var Metadata |
21
|
|
|
*/ |
22
|
|
|
protected $metadata; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* @var RepositoryInterface |
26
|
|
|
*/ |
27
|
|
|
protected $repository; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* @var TypesRegistryInterface |
31
|
|
|
*/ |
32
|
|
|
protected $types; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* @var QueryInterface&EntityJoinable |
|
|
|
|
36
|
|
|
*/ |
37
|
|
|
protected $query; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* The counter of alias |
41
|
|
|
* |
42
|
|
|
* @var integer |
|
|
|
|
43
|
|
|
* @internal |
|
|
|
|
44
|
|
|
*/ |
45
|
|
|
private $counter = 0; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* Array of alias for relations |
49
|
|
|
* The key is the relation path (ex: customer.user) |
50
|
|
|
* The value is the generated alias (ex: t1, t2...) |
51
|
|
|
* |
52
|
|
|
* @var string[] |
53
|
|
|
*/ |
54
|
|
|
protected $relationAlias = []; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* Array of path, indexed by alias |
58
|
|
|
* |
59
|
|
|
* ex: [ |
|
|
|
|
60
|
|
|
* t0 => user |
61
|
|
|
* t1 => customer |
62
|
|
|
* t2 => customer.driver |
63
|
|
|
* ] |
64
|
|
|
* |
65
|
|
|
* @var array<string, string> |
|
|
|
|
66
|
|
|
*/ |
67
|
|
|
protected $aliasToPath = []; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* Array of metadata by alias |
71
|
|
|
* Used by the select compilation to map attribute by its field map name. |
72
|
|
|
* |
73
|
|
|
* @var array |
74
|
|
|
*/ |
75
|
|
|
protected $metadataByAlias = []; |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* Does the root repository (i.e. $this->repository) is already registered (i.e. has an alias) |
79
|
|
|
* The root alias must be defined before resolving any fields, so use this field to auto register the repository if not yet done |
80
|
|
|
* |
81
|
|
|
* @var bool |
|
|
|
|
82
|
|
|
*/ |
83
|
|
|
private $rootRepositoryRegistered = false; |
84
|
|
|
|
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* AliasResolver constructor. |
88
|
|
|
* |
89
|
|
|
* @param RepositoryInterface $repository |
90
|
|
|
* @param TypesRegistryInterface $types |
91
|
|
|
*/ |
92
|
404 |
|
public function __construct(RepositoryInterface $repository, TypesRegistryInterface $types) |
|
|
|
|
93
|
|
|
{ |
94
|
404 |
|
$this->repository = $repository; |
95
|
404 |
|
$this->metadata = $this->repository->metadata(); |
96
|
404 |
|
$this->types = $types; |
97
|
404 |
|
} |
98
|
|
|
|
99
|
|
|
/** |
|
|
|
|
100
|
|
|
* Set the query instance |
101
|
|
|
* |
102
|
|
|
* @param QueryInterface&EntityJoinable|null $query |
|
|
|
|
103
|
|
|
*/ |
104
|
404 |
|
public function setQuery(?QueryInterface $query = null): void |
105
|
|
|
{ |
106
|
404 |
|
$this->query = $query; |
107
|
404 |
|
} |
108
|
|
|
|
109
|
|
|
/** |
110
|
|
|
* Reset all registered aliases |
111
|
|
|
*/ |
112
|
3 |
|
public function reset() |
113
|
|
|
{ |
114
|
3 |
|
$this->aliasToPath = []; |
115
|
3 |
|
$this->relationAlias = []; |
116
|
3 |
|
$this->counter = 0; |
117
|
3 |
|
$this->metadataByAlias = []; |
118
|
3 |
|
$this->rootRepositoryRegistered = false; |
119
|
3 |
|
} |
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* Resolve the attribute path (i.e. user.customer.name) and get aliases and SQL valid expression. |
123
|
|
|
* |
124
|
|
|
* /!\ Don't forget to {@link AliasResolver::registerMetadata()} the root tables before resolve any attributes |
|
|
|
|
125
|
|
|
* |
126
|
|
|
* ex: |
127
|
|
|
* |
128
|
|
|
* resolve('user.customer.name', $type) => 't1.name_', $type : StringType() |
129
|
|
|
* resolve('123', $type) => Do not modify : 123 is a DBAL expression |
130
|
|
|
* |
131
|
|
|
* @param string $attribute The attribute path |
132
|
|
|
* @param mixed $type in-out : If set to true, this reference would be filled with the mapped type |
|
|
|
|
133
|
|
|
* |
134
|
|
|
* @return string The SQL valid expression, {table alias}.{table attribute} |
135
|
|
|
*/ |
136
|
302 |
|
public function resolve($attribute, &$type = null) |
137
|
|
|
{ |
138
|
|
|
// The root repository is not registered |
139
|
302 |
|
if (!$this->rootRepositoryRegistered) { |
140
|
12 |
|
$this->registerMetadata($this->repository, null); |
141
|
|
|
} |
142
|
|
|
|
143
|
302 |
|
$metadata = $this->metadata; |
144
|
|
|
|
145
|
302 |
|
if (!isset($metadata->attributes[$attribute])) { |
146
|
84 |
|
list($alias, $attribute, $metadata) = $this->exploreExpression($attribute); |
147
|
|
|
|
148
|
|
|
//No metadata found => DBAL expression. |
|
|
|
|
149
|
84 |
|
if ($metadata === null) { |
150
|
4 |
|
return $attribute; |
151
|
|
|
} |
152
|
|
|
} |
153
|
|
|
|
154
|
298 |
|
if (empty($alias)) { |
155
|
282 |
|
$alias = $this->getPathAlias($metadata->table); |
156
|
|
|
} |
157
|
|
|
|
158
|
298 |
|
if ($type === true) { |
159
|
262 |
|
$type = $this->types->get($metadata->attributes[$attribute]['type']); |
160
|
|
|
} |
161
|
|
|
|
162
|
298 |
|
return $alias.'.'.$metadata->attributes[$attribute]['field']; |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* Register metadata |
167
|
|
|
* |
168
|
|
|
* Used only for select query |
169
|
|
|
* If the alias is null, the method will create one |
|
|
|
|
170
|
|
|
* |
171
|
|
|
* @param string|Metadata|RepositoryInterface $repository |
172
|
|
|
* @param string|null $alias |
173
|
|
|
* |
174
|
|
|
* @return string|null Returns the metadata alias, or null is the first parameter is a DBAL value |
175
|
|
|
*/ |
176
|
403 |
|
public function registerMetadata($repository, ?string $alias): ?string |
177
|
|
|
{ |
178
|
403 |
|
if (!$repository instanceof RepositoryInterface) { |
179
|
385 |
|
$repository = $this->findRepository($repository); |
180
|
|
|
|
181
|
385 |
|
if ($repository === null) { |
182
|
|
|
// No repository found. The given repository will be considered as a dbal value |
183
|
3 |
|
return $alias; |
184
|
|
|
} |
185
|
|
|
} |
186
|
|
|
|
187
|
402 |
|
$metadata = $repository->metadata(); |
188
|
|
|
|
189
|
402 |
|
if (empty($alias)) { |
190
|
393 |
|
$alias = $this->getPathAlias($metadata->table); |
191
|
|
|
} |
192
|
|
|
|
193
|
402 |
|
if (!isset($this->aliasToPath[$alias])) { |
194
|
28 |
|
$this->aliasToPath[$alias] = $metadata->table; |
195
|
|
|
} |
196
|
|
|
|
197
|
402 |
|
if (!isset($this->relationAlias[$metadata->table])) { |
198
|
72 |
|
$this->relationAlias[$metadata->table] = $alias; |
199
|
|
|
} |
200
|
|
|
|
201
|
402 |
|
if ($metadata->useQuoteIdentifier) { |
202
|
3 |
|
$this->query->useQuoteIdentifier(true); |
203
|
|
|
} |
204
|
|
|
|
205
|
402 |
|
$this->query->where($repository->constraints('$'.$alias)); |
206
|
402 |
|
$this->metadataByAlias[$alias] = $metadata; |
207
|
|
|
|
208
|
402 |
|
if ($repository === $this->repository) { |
209
|
396 |
|
$this->rootRepositoryRegistered = true; |
210
|
|
|
} |
211
|
|
|
|
212
|
402 |
|
return $alias; |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
/** |
216
|
|
|
* Find the associated repository |
217
|
|
|
* |
218
|
|
|
* @param mixed $search |
219
|
|
|
* |
220
|
|
|
* @return RepositoryInterface|null |
221
|
|
|
* |
222
|
|
|
* @todo find repository from table name |
|
|
|
|
223
|
|
|
*/ |
224
|
385 |
|
protected function findRepository($search): ?RepositoryInterface |
225
|
|
|
{ |
226
|
385 |
|
if ($this->metadata->table === $search) { |
227
|
381 |
|
return $this->repository; |
228
|
|
|
} |
229
|
|
|
|
230
|
21 |
|
return $this->repository->repository($search); |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* Extract from expression the attribute name and its metadata |
235
|
|
|
* |
236
|
|
|
* @param string $expression |
237
|
|
|
* |
238
|
|
|
* @return array The attribute name and the owner metadata |
239
|
|
|
*/ |
240
|
84 |
|
protected function exploreExpression($expression) |
241
|
|
|
{ |
242
|
84 |
|
$tokens = ExpressionCompiler::instance()->compile($expression); |
243
|
|
|
|
244
|
84 |
|
$state = new ExpressionExplorationState(); |
245
|
84 |
|
$state->metadata = $this->metadata; |
246
|
|
|
|
247
|
84 |
|
foreach ($tokens as $token) { |
248
|
|
|
try { |
249
|
84 |
|
switch ($token->type) { |
250
|
84 |
|
case ExpressionToken::TYPE_ALIAS: |
251
|
40 |
|
$this->resolveAlias($token->value, $state); |
252
|
40 |
|
break; |
|
|
|
|
253
|
|
|
|
254
|
84 |
|
case ExpressionToken::TYPE_ATTR: |
255
|
52 |
|
$state->attribute = $token->value; |
256
|
52 |
|
break; |
|
|
|
|
257
|
|
|
|
258
|
84 |
|
case ExpressionToken::TYPE_STA: |
259
|
2 |
|
$this->resolveStatic($token->value, $state); |
260
|
2 |
|
break; |
|
|
|
|
261
|
|
|
|
262
|
83 |
|
case ExpressionToken::TYPE_DYN: |
263
|
83 |
|
$this->resolveDynamic($token->value, $state); |
264
|
80 |
|
break; |
|
|
|
|
265
|
|
|
} |
266
|
4 |
|
} catch (RelationNotFoundException $exception) { |
267
|
|
|
// SQL expression |
268
|
84 |
|
return [null, $expression, null]; |
269
|
|
|
} |
|
|
|
|
270
|
|
|
} |
|
|
|
|
271
|
|
|
|
272
|
|
|
// SQL expression |
273
|
80 |
|
if ($state->attribute === null) { |
274
|
|
|
return [null, $expression, null]; |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
// If no alias was given => create an alias from the path |
278
|
80 |
|
if ($state->alias === null) { |
279
|
|
|
$state->alias = $this->getPathAlias($state->path); |
280
|
|
|
} |
281
|
|
|
|
282
|
80 |
|
return [$state->alias, $state->attribute, $state->metadata]; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
/** |
286
|
|
|
* Resolve from an $ALIAS expression token |
287
|
|
|
* @see ExpressionCompiler |
|
|
|
|
288
|
|
|
* |
289
|
|
|
* @param string $alias The alias name |
|
|
|
|
290
|
|
|
* @param ExpressionExplorationState $state |
291
|
|
|
*/ |
292
|
40 |
|
protected function resolveAlias($alias, ExpressionExplorationState $state) |
293
|
|
|
{ |
294
|
40 |
|
$state->alias = $alias; |
295
|
40 |
|
$state->path = $this->getRealPath($alias); |
296
|
40 |
|
$state->metadata = $this->metadataByAlias[$alias]; |
297
|
40 |
|
} |
298
|
|
|
|
299
|
|
|
/** |
300
|
|
|
* Revolve from a $STA expression token |
301
|
|
|
* @see ExpressionCompiler |
|
|
|
|
302
|
|
|
* |
303
|
|
|
* @param string $expression The static expression |
|
|
|
|
304
|
|
|
* @param ExpressionExplorationState $state |
305
|
|
|
*/ |
306
|
2 |
|
protected function resolveStatic($expression, ExpressionExplorationState $state) |
307
|
|
|
{ |
308
|
|
|
// Static expression not resolved yet |
309
|
2 |
|
if (!isset($this->relationAlias[$expression])) { |
310
|
2 |
|
$this->resolveDynamic(explode('.', $expression), $state); |
311
|
|
|
} else { |
312
|
2 |
|
$state->path = $expression; |
313
|
2 |
|
$state->alias = $this->relationAlias[$state->path]; |
314
|
2 |
|
$state->metadata = $this->metadataByAlias[$state->alias]; |
315
|
|
|
} |
316
|
2 |
|
} |
317
|
|
|
|
318
|
|
|
/** |
319
|
|
|
* Resolve from a $DYN expression |
320
|
|
|
* @see ExpressionCompiler |
|
|
|
|
321
|
|
|
* |
322
|
|
|
* @param array $expression Array of names |
|
|
|
|
323
|
|
|
* @param ExpressionExplorationState $state |
324
|
|
|
*/ |
325
|
84 |
|
protected function resolveDynamic(array $expression, ExpressionExplorationState $state) |
326
|
|
|
{ |
327
|
84 |
|
$attribute = implode('.', $expression); |
328
|
|
|
|
329
|
|
|
//Expression is the attribute |
|
|
|
|
330
|
84 |
|
if (isset($state->metadata->attributes[$attribute])) { |
331
|
33 |
|
$state->attribute = $attribute; |
332
|
33 |
|
return; |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
/* |
336
|
|
|
* Attribute is the last part of the expression |
337
|
|
|
* OR the expression is a path |
338
|
|
|
* |
339
|
|
|
* i.e. $expression = $path . '.' . $attribute |
340
|
|
|
* i.e. $expression = $path |
341
|
|
|
*/ |
342
|
|
|
|
343
|
66 |
|
for ($i = 0, $count = count($expression); $i < $count; ++$i) { |
344
|
66 |
|
$part = $expression[$i]; |
345
|
|
|
|
346
|
66 |
|
if ($state->path) { |
347
|
15 |
|
$state->path .= '.'; |
348
|
|
|
} |
349
|
|
|
|
350
|
66 |
|
$state->path .= $part; |
351
|
|
|
|
352
|
66 |
|
$this->declareRelation($part, $state); |
353
|
|
|
|
354
|
62 |
|
$attribute = substr($attribute, strlen($part) + 1); |
|
|
|
|
355
|
|
|
|
356
|
|
|
//Attribute find in attributes |
|
|
|
|
357
|
62 |
|
if (isset($state->metadata->attributes[$attribute])) { |
358
|
59 |
|
$state->attribute = $attribute; |
359
|
59 |
|
return; |
360
|
|
|
} |
361
|
|
|
} |
362
|
52 |
|
} |
363
|
|
|
|
364
|
|
|
/** |
365
|
|
|
* Declare all relation in the tokens |
366
|
|
|
* |
367
|
|
|
* Manage relation like "customer.documents.contact.name" |
368
|
|
|
* |
369
|
|
|
* Use cases : |
370
|
|
|
* - The path is already defined as an alias |
371
|
|
|
* - Expend the alias |
372
|
|
|
* - Get the metadata |
373
|
|
|
* - Use the alias |
374
|
|
|
* - Metadata not loaded |
375
|
|
|
* - Load the relation |
376
|
|
|
* - Create an alias |
377
|
|
|
* - Join entity and apply constrains |
378
|
|
|
* - Metadata loaded |
379
|
|
|
* - Retrieve the alias |
380
|
|
|
* - Use metadata and alias |
381
|
|
|
* |
382
|
|
|
* @param string $relationName The current relation name |
383
|
|
|
* @param ExpressionExplorationState $state |
384
|
|
|
*/ |
385
|
66 |
|
protected function declareRelation($relationName, ExpressionExplorationState $state) |
386
|
|
|
{ |
387
|
|
|
// The path is an alias |
388
|
|
|
// - Save the alias |
|
|
|
|
389
|
|
|
// - Get the metadata from the alias |
|
|
|
|
390
|
|
|
// - Expend the path |
|
|
|
|
391
|
66 |
|
if (isset($this->metadataByAlias[$state->path])) { |
392
|
53 |
|
$state->alias = $state->path; |
393
|
53 |
|
$state->metadata = $this->metadataByAlias[$state->path]; |
394
|
53 |
|
$state->path = $this->getRealPath($state->path); |
395
|
53 |
|
return; |
396
|
|
|
} |
397
|
|
|
|
398
|
52 |
|
$alias = $this->getPathAlias($state->path); |
399
|
|
|
|
400
|
|
|
// If no metadata has been registered the alias could be: |
401
|
|
|
// 1. A relation and declare the relationship. |
|
|
|
|
402
|
|
|
// 2. A table alias of another metadata added by DBAL methods (@see Query::from, @see Query::join) |
|
|
|
|
403
|
52 |
|
if (!isset($this->metadataByAlias[$alias])) { |
404
|
|
|
// Get the relation name. '#' is for polymorphic relation |
405
|
51 |
|
$relationName = explode('#', $relationName); |
406
|
|
|
|
407
|
51 |
|
$relation = $this->repository->repository($state->metadata->entityName)->relation($relationName[0]); |
408
|
47 |
|
$relation->setLocalAlias($this->getParentAlias($state->path)); |
409
|
|
|
|
410
|
47 |
|
foreach ($relation->joinRepositories($this->query, $alias, isset($relationName[1]) ? $relationName[1] : null) as $alias => $repository) { |
411
|
47 |
|
$this->registerMetadata($repository, $alias); |
412
|
|
|
} |
413
|
|
|
|
414
|
|
|
// If we have polymophism, add to alias |
415
|
47 |
|
$relation->join($this->query, $alias . (isset($relationName[1]) ? '#' . $relationName[1] : '')); |
416
|
47 |
|
$relation->setLocalAlias(null); |
417
|
|
|
} |
418
|
|
|
|
419
|
48 |
|
$state->alias = $alias; |
420
|
48 |
|
$state->metadata = $this->metadataByAlias[$alias]; |
421
|
48 |
|
} |
422
|
|
|
|
423
|
|
|
/** |
424
|
|
|
* Get the alias of a path. |
425
|
|
|
* |
426
|
|
|
* If the path is not found, will create a new alias (t0, t1...) |
427
|
|
|
* |
428
|
|
|
* @param string|null $path The relation path, or null to use the root table |
429
|
|
|
* |
430
|
|
|
* @return string |
431
|
|
|
*/ |
432
|
398 |
|
public function getPathAlias($path = null) |
433
|
|
|
{ |
434
|
398 |
|
if (empty($path)) { |
435
|
304 |
|
$path = $this->metadata->table; |
436
|
|
|
} |
437
|
|
|
|
438
|
398 |
|
if (!isset($this->relationAlias[$path])) { |
439
|
394 |
|
$alias = 't'.$this->counter++; |
440
|
394 |
|
$this->relationAlias[$path] = $alias; |
441
|
394 |
|
$this->aliasToPath[$alias] = $path; |
442
|
|
|
} |
443
|
|
|
|
444
|
398 |
|
return $this->relationAlias[$path]; |
445
|
|
|
} |
446
|
|
|
|
447
|
|
|
/** |
448
|
|
|
* Get the real attribute path |
449
|
|
|
* |
450
|
|
|
* If the arguments is not found in alias table, concider the argument as path |
|
|
|
|
451
|
|
|
* |
452
|
|
|
* @param string $path The alias, or path |
453
|
|
|
* |
454
|
|
|
* @return string |
455
|
|
|
*/ |
456
|
80 |
|
protected function getRealPath($path) |
457
|
|
|
{ |
458
|
80 |
|
return isset($this->aliasToPath[$path]) ? $this->aliasToPath[$path] : $path; |
459
|
|
|
} |
460
|
|
|
|
461
|
|
|
/** |
462
|
|
|
* Get the alias of the parent relation |
463
|
|
|
* |
464
|
|
|
* @param string $path |
465
|
|
|
* |
466
|
|
|
* @return string |
467
|
|
|
*/ |
468
|
47 |
|
protected function getParentAlias($path) |
469
|
|
|
{ |
470
|
47 |
|
$path = $this->getRealPath($path); |
471
|
|
|
|
472
|
47 |
|
$pos = strrpos($path, '.'); |
473
|
|
|
|
474
|
47 |
|
if ($pos === false) { |
475
|
47 |
|
return $this->getPathAlias($this->metadata->table); |
476
|
|
|
} |
477
|
|
|
|
478
|
15 |
|
$path = substr($path, 0, $pos); |
479
|
|
|
|
480
|
15 |
|
return $this->getPathAlias($path); |
481
|
|
|
} |
482
|
|
|
|
483
|
|
|
/** |
484
|
|
|
* Check if the alias is registered |
485
|
|
|
* |
486
|
|
|
* @param string $alias |
487
|
|
|
* |
488
|
|
|
* @return bool |
|
|
|
|
489
|
|
|
*/ |
490
|
384 |
|
public function hasAlias($alias) |
491
|
|
|
{ |
492
|
384 |
|
return isset($this->metadataByAlias[$alias]); |
493
|
|
|
} |
494
|
|
|
|
495
|
|
|
/** |
496
|
|
|
* Get the metadata from the alias (or entity name) |
497
|
|
|
* |
498
|
|
|
* @param string $alias |
499
|
|
|
* |
500
|
|
|
* @return Metadata |
501
|
|
|
*/ |
502
|
394 |
|
public function getMetadata($alias) |
503
|
|
|
{ |
504
|
394 |
|
return $this->metadataByAlias[$alias]; |
505
|
|
|
} |
506
|
|
|
} |
507
|
|
|
|