Passed
Pull Request — master (#16)
by Vincent
07:14
created

AliasResolver   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 488
Duplicated Lines 0 %

Test Coverage

Coverage 99.3%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 51
eloc 132
c 1
b 0
f 0
dl 0
loc 488
ccs 142
cts 143
cp 0.993
rs 7.92

16 Methods

Rating   Name   Duplication   Size   Complexity  
A getRealPath() 0 3 2
A setQuery() 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 __construct() 0 5 1
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
0 ignored issues
show
Coding Style Documentation introduced by
@internal tag is not allowed in class comment
Loading history...
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
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...)
0 ignored issues
show
introduced by
Doc comment short description must be on a single line, further text should be a separate paragraph
Loading history...
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 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.
0 ignored issues
show
introduced by
Doc comment short description must be on a single line, further text should be a separate paragraph
Loading history...
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
0 ignored issues
show
introduced by
Doc comment short description must be on a single line, further text should be a separate paragraph
Loading history...
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 385
    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 385
        $this->repository = $repository;
95 385
        $this->metadata = $this->repository->metadata();
96 385
        $this->types = $types;
97 385
    }
98
99
    /**
100
     * Set the query instance
101
     *
102
     * @param QueryInterface|null $query
103
     */
104 385
    public function setQuery(?QueryInterface $query = null)
105
    {
106 385
        $this->query = $query;
107 385
    }
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 289
    public function resolve($attribute, &$type = null)
137
    {
138
        // The root repository is not registered
139 289
        if (!$this->rootRepositoryRegistered) {
140 12
            $this->registerMetadata($this->repository, null);
141
        }
142
143 289
        $metadata = $this->metadata;
144
145 289
        if (!isset($metadata->attributes[$attribute])) {
146 79
            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 79
            if ($metadata === null) {
150 4
                return $attribute;
151
            }
152
        }
153
154 285
        if (empty($alias)) {
155 269
            $alias = $this->getPathAlias($metadata->table);
156
        }
157
158 285
        if ($type === true) {
159 255
            $type = $this->types->get($metadata->attributes[$attribute]['type']);
160
        }
161
162 285
        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                              $alias
173
     *
174
     * @return string  Returns the metadata alias
175
     */
176 384
    public function registerMetadata($repository, $alias)
177
    {
178 384
        if (!$repository instanceof RepositoryInterface) {
179 366
            $repository = $this->findRepository($repository);
180
181 366
            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 383
        $metadata = $repository->metadata();
188
189 383
        if (empty($alias)) {
190 376
            $alias = $this->getPathAlias($metadata->table);
191
        }
192
193 383
        if (!isset($this->aliasToPath[$alias])) {
194 26
            $this->aliasToPath[$alias] = $metadata->table;
195
        }
196
197 383
        if (!isset($this->relationAlias[$metadata->table])) {
198 70
            $this->relationAlias[$metadata->table] = $alias;
199
        }
200
201 383
        if ($metadata->useQuoteIdentifier) {
202 3
            $this->query->useQuoteIdentifier(true);
0 ignored issues
show
Bug introduced by
The method useQuoteIdentifier() does not exist on Bdf\Prime\Query\Contract\EntityJoinable. Since it exists in all sub-types, consider adding an abstract or default implementation to Bdf\Prime\Query\Contract\EntityJoinable. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

202
            $this->query->/** @scrutinizer ignore-call */ 
203
                          useQuoteIdentifier(true);
Loading history...
203
        }
204
205 383
        $this->query->where($repository->constraints('$'.$alias));
206 383
        $this->metadataByAlias[$alias] = $metadata;
207
208 383
        if ($repository === $this->repository) {
209 377
            $this->rootRepositoryRegistered = true;
210
        }
211
212 383
        return $alias;
213
    }
214
215
    /**
216
     * Find the associated repository
217
     *
218
     * @param mixed $search
219
     *
220
     * @return RepositoryInterface
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 366
    protected function findRepository($search)
225
    {
226 366
        if ($this->metadata->table === $search) {
227 362
            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 79
    protected function exploreExpression($expression)
241
    {
242 79
        $tokens = ExpressionCompiler::instance()->compile($expression);
243
244 79
        $state = new ExpressionExplorationState();
245 79
        $state->metadata = $this->metadata;
246
247 79
        foreach ($tokens as $token) {
248
            try {
249 79
                switch ($token->type) {
250 79
                    case ExpressionToken::TYPE_ALIAS:
251 35
                        $this->resolveAlias($token->value, $state);
252 35
                        break;
0 ignored issues
show
introduced by
Case breaking statement indented incorrectly; expected 22 spaces, found 24
Loading history...
253
254 79
                    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 79
                    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 78
                    case ExpressionToken::TYPE_DYN:
263 78
                        $this->resolveDynamic($token->value, $state);
264 76
                        break;
0 ignored issues
show
introduced by
Case breaking statement indented incorrectly; expected 22 spaces, found 24
Loading history...
265
                }
266 3
            } catch (RelationNotFoundException $exception) {
267
                // SQL expression
268 79
                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 76
        if ($state->attribute === null) {
274 1
            return [null, $expression, null];
275
        }
276
277
        // If no alias was given => create an alias from the path
278 75
        if ($state->alias === null) {
279
            $state->alias = $this->getPathAlias($state->path);
280
        }
281
282 75
        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 35
    protected function resolveAlias($alias, ExpressionExplorationState $state)
293
    {
294 35
        $state->alias = $alias;
295 35
        $state->path = $this->getRealPath($alias);
296 35
        $state->metadata = $this->metadataByAlias[$alias];
297 35
    }
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 79
    protected function resolveDynamic(array $expression, ExpressionExplorationState $state)
326
    {
327 79
        $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 79
        if (isset($state->metadata->attributes[$attribute])) {
331 28
            $state->attribute = $attribute;
332 28
            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 63
            $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 63
            if (isset($state->metadata->attributes[$attribute])) {
358 59
                $state->attribute = $attribute;
359 59
                return;
360
            }
361
        }
362 53
    }
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 50
            $relationName = explode('#', $relationName);
406
407 50
            $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 49
        $state->alias = $alias;
420 49
        $state->metadata = $this->metadataByAlias[$alias];
421 49
    }
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 379
    public function getPathAlias($path = null)
433
    {
434 379
        if (empty($path)) {
435 296
            $path = $this->metadata->table;
436
        }
437
438 379
        if (!isset($this->relationAlias[$path])) {
439 377
            $alias = 't'.$this->counter++;
440 377
            $this->relationAlias[$path] = $alias;
441 377
            $this->aliasToPath[$alias] = $path;
442
        }
443
444 379
        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 75
    protected function getRealPath($path)
457
    {
458 75
        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 365
    public function hasAlias($alias)
491
    {
492 365
        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 375
    public function getMetadata($alias)
503
    {
504 375
        return $this->metadataByAlias[$alias];
505
    }
506
}
507