Passed
Pull Request — master (#29)
by Sébastien
08:57
created

AliasResolver::exploreExpression()   B

Complexity

Conditions 9
Paths 21

Size

Total Lines 43
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 9.0414

Importance

Changes 0
Metric Value
eloc 25
dl 0
loc 43
ccs 23
cts 25
cp 0.92
rs 8.0555
c 0
b 0
f 0
cc 9
nc 21
nop 1
crap 9.0414
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
0 ignored issues
show
introduced by
Expected "QueryInterfaceEntityJoinable" but found "QueryInterface&EntityJoinable" for @var tag in member variable comment
Loading history...
36
     */
37
    protected $query;
38
39
    /**
40
     * The counter of alias
41
     *
42
     * @var integer
0 ignored issues
show
introduced by
Expected "int" but found "integer" for @var tag in member variable comment
Loading history...
43
     * @internal
0 ignored issues
show
Coding Style Documentation introduced by
@internal tag is not allowed in member variable comment
Loading history...
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: [
0 ignored issues
show
Coding Style introduced by
Doc comment long description must start with a capital letter
Loading history...
60
     *  t0 => user
61
     *  t1 => customer
62
     *  t2 => customer.driver
63
     * ]
64
     *
65
     * @var array<string, string>
0 ignored issues
show
introduced by
Expected "arraystringstring" but found "array<string, string>" for @var tag in member variable comment
Loading history...
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
0 ignored issues
show
Bug introduced by
Expected "boolean" but found "bool" for @var tag in member variable comment
Loading history...
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)
0 ignored issues
show
Coding Style introduced by
Expected 1 blank line before function; 2 found
Loading history...
93
    {
94 404
        $this->repository = $repository;
95 404
        $this->metadata = $this->repository->metadata();
96 404
        $this->types = $types;
97 404
    }
98
99
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $query should have a doc-comment as per coding-style.
Loading history...
introduced by
Parameter $query is not described in comment
Loading history...
100
     * Set the query instance
101
     *
102
     * @param QueryInterface&EntityJoinable|null $query
0 ignored issues
show
introduced by
Parameter comment must start with a capital letter
Loading history...
Coding Style introduced by
Doc comment for parameter &EntityJoinable|null does not match actual variable name $query
Loading history...
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
0 ignored issues
show
introduced by
Doc comment long description must end with a full stop
Loading history...
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
0 ignored issues
show
introduced by
Parameter comment must start with a capital letter
Loading history...
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.
0 ignored issues
show
Coding Style introduced by
No space found before comment text; expected "// No metadata found => DBAL expression." but found "//No metadata found => DBAL expression."
Loading history...
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
0 ignored issues
show
introduced by
Doc comment long description must end with a full stop
Loading history...
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
0 ignored issues
show
Coding Style introduced by
Comment refers to a TODO task

This check looks TODO comments that have been left in the code.

``TODO``s show that something is left unfinished and should be attended to.

Loading history...
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;
0 ignored issues
show
introduced by
Case breaking statement indented incorrectly; expected 22 spaces, found 24
Loading history...
253
254 84
                    case ExpressionToken::TYPE_ATTR:
255 52
                        $state->attribute = $token->value;
256 52
                        break;
0 ignored issues
show
introduced by
Case breaking statement indented incorrectly; expected 22 spaces, found 24
Loading history...
257
258 84
                    case ExpressionToken::TYPE_STA:
259 2
                        $this->resolveStatic($token->value, $state);
260 2
                        break;
0 ignored issues
show
introduced by
Case breaking statement indented incorrectly; expected 22 spaces, found 24
Loading history...
261
262 83
                    case ExpressionToken::TYPE_DYN:
263 83
                        $this->resolveDynamic($token->value, $state);
264 80
                        break;
0 ignored issues
show
introduced by
Case breaking statement indented incorrectly; expected 22 spaces, found 24
Loading history...
265
                }
266 4
            } catch (RelationNotFoundException $exception) {
267
                // SQL expression
268 84
                return [null, $expression, null];
269
            }
0 ignored issues
show
Coding Style introduced by
End comment for long condition not found; expected "//end try"
Loading history...
270
        }
0 ignored issues
show
Coding Style introduced by
End comment for long condition not found; expected "//end foreach"
Loading history...
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
0 ignored issues
show
Coding Style introduced by
There must be exactly one blank line before the tags in a doc comment
Loading history...
288
     *
289
     * @param string $alias The alias name
0 ignored issues
show
Coding Style introduced by
Parameter tags must be defined first in a doc comment
Loading history...
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
0 ignored issues
show
Coding Style introduced by
There must be exactly one blank line before the tags in a doc comment
Loading history...
302
     *
303
     * @param string $expression The static expression
0 ignored issues
show
Coding Style introduced by
Parameter tags must be defined first in a doc comment
Loading history...
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
0 ignored issues
show
Coding Style introduced by
There must be exactly one blank line before the tags in a doc comment
Loading history...
321
     *
322
     * @param array $expression Array of names
0 ignored issues
show
Coding Style introduced by
Parameter tags must be defined first in a doc comment
Loading history...
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
0 ignored issues
show
Coding Style introduced by
No space found before comment text; expected "// Expression is the attribute" but found "//Expression is the attribute"
Loading history...
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);
0 ignored issues
show
Coding Style introduced by
Operation must be bracketed
Loading history...
355
356
            //Attribute find in attributes
0 ignored issues
show
Coding Style introduced by
No space found before comment text; expected "// Attribute find in attributes" but found "//Attribute find in attributes"
Loading history...
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
0 ignored issues
show
Coding Style introduced by
Expected 1 space before comment text but found 2; use block comment if you need indentation
Loading history...
introduced by
Comment indentation error, expected only 1 spaces
Loading history...
389
        //  - Get the metadata from the alias
0 ignored issues
show
Coding Style introduced by
Expected 1 space before comment text but found 2; use block comment if you need indentation
Loading history...
390
        //  - Expend the path
0 ignored issues
show
Coding Style introduced by
Expected 1 space before comment text but found 2; use block comment if you need indentation
Loading history...
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.
0 ignored issues
show
Coding Style introduced by
Expected 1 space before comment text but found 3; use block comment if you need indentation
Loading history...
introduced by
Comment indentation error, expected only 1 spaces
Loading history...
402
        //   2. A table alias of another metadata added by DBAL methods (@see Query::from, @see Query::join)
0 ignored issues
show
Coding Style introduced by
Expected 1 space before comment text but found 3; use block comment if you need indentation
Loading history...
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
0 ignored issues
show
introduced by
Doc comment long description must end with a full stop
Loading history...
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
0 ignored issues
show
Coding Style introduced by
Expected "boolean" but found "bool" for function return type
Loading history...
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