AliasResolver   B
last analyzed

Complexity

Total Complexity 50

Size/Duplication

Total Lines 501
Duplicated Lines 0 %

Test Coverage

Coverage 98.53%

Importance

Changes 0
Metric Value
wmc 50
eloc 132
dl 0
loc 501
ccs 134
cts 136
cp 0.9853
rs 8.4
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A setQuery() 0 3 1
A __construct() 0 5 1
A getRealPath() 0 3 1
A findRepository() 0 7 2
A getPathAlias() 0 13 3
B registerMetadata() 0 37 8
B exploreExpression() 0 43 9
A resolveStatic() 0 9 2
A reset() 0 7 1
A hasAlias() 0 3 1
A resolveDynamic() 0 35 5
A resolveAlias() 0 5 1
A getParentAlias() 0 13 2
A resolve() 0 27 6
A declareRelation() 0 36 6
A getMetadata() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like AliasResolver often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AliasResolver, and based on these observations, apply Extract Interface, too.

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