Completed
Push — master ( 16f241...ce3748 )
by Kirill
02:52
created

JoinBuilder::hasAlias()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
/**
3
 * This file is part of Hydrogen package.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
declare(strict_types=1);
9
10
namespace RDS\Hydrogen\Processor\DatabaseProcessor;
11
12
use Doctrine\ORM\QueryBuilder;
13
use RDS\Hydrogen\Criteria\Common\Field;
14
use RDS\Hydrogen\Criteria\CriterionInterface;
15
use RDS\Hydrogen\Criteria\Join;
16
17
/**
18
 * Class JoinBuilder
19
 */
20
class JoinBuilder extends WhereBuilder
21
{
22
    /**
23
     * @var array
24
     */
25
    private $relations = [];
26
27
    /**
28
     * @param QueryBuilder $builder
29
     * @param CriterionInterface|Join $join
30
     * @return iterable|null
31
     */
32 2
    public function apply($builder, CriterionInterface $join): ?iterable
33
    {
34 2
        [$entity, $alias] = $this->joinAll($builder, $join);
0 ignored issues
show
Bug introduced by
The variable $entity does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $alias does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Compatibility introduced by
$join of type object<RDS\Hydrogen\Criteria\CriterionInterface> is not a sub-type of object<RDS\Hydrogen\Criteria\Join>. It seems like you assume a concrete implementation of the interface RDS\Hydrogen\Criteria\CriterionInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
35
36 2
        if ($join->hasJoinQuery()) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface RDS\Hydrogen\Criteria\CriterionInterface as the method hasJoinQuery() does only exist in the following implementations of said interface: RDS\Hydrogen\Criteria\Join.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
37
            $repository = $this->processor->getProcessor($entity)->getRepository();
38
39
            $query = $this->query->create()
40
                ->from($repository)
41
                ->withAlias($alias);
42
43
            yield $join->getJoinQuery($query);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface RDS\Hydrogen\Criteria\CriterionInterface as the method getJoinQuery() does only exist in the following implementations of said interface: RDS\Hydrogen\Criteria\Join.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
44
        }
45 2
    }
46
47
    /**
48
     * @param QueryBuilder $builder
49
     * @param Join $join
50
     * @return array
51
     */
52 2
    private function joinAll(QueryBuilder $builder, Join $join): array
53
    {
54 2
        [$alias, $relation] = [$join->getQueryAlias(), []];
0 ignored issues
show
Bug introduced by
The variable $alias seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
Bug introduced by
The variable $relation seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
55
56 2
        foreach ($join->getRelations($this->processor) as $isLast => $relation) {
57
            // Is the relation already loaded in current query execution
58 2
            $exists = $this->hasAlias($relation);
59
60
            // Resolve relation alias
61 2
            $relationAlias = $isLast && $join->hasJoinQuery()
62
                ? $this->getAlias($relation)
63 2
                : $this->getCachedAlias($relation);
64
65 2
            if (! $exists) {
66
                // Create join
67 2
                $relationField = Field::new($relation['fieldName'])->toString($alias);
0 ignored issues
show
Bug introduced by
The variable $alias does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
68 2
                $this->join($builder, $join, $relationField, $relationAlias);
69
70
                // Add join to selection statement
71 2
                $builder->addSelect($relationAlias);
72
            }
73
74
            // Shift parent
75 2
            $alias = $relationAlias;
76
        }
77
78 2
        return [$relation['targetEntity'], $alias];
0 ignored issues
show
Bug introduced by
The variable $relation seems to be defined by a foreach iteration on line 56. Are you sure the iterator is never empty, otherwise this variable is not defined?

It seems like you are relying on a variable being defined by an iteration:

foreach ($a as $b) {
}

// $b is defined here only if $a has elements, for example if $a is array()
// then $b would not be defined here. To avoid that, we recommend to set a
// default value for $b.


// Better
$b = 0; // or whatever default makes sense in your context
foreach ($a as $b) {
}

// $b is now guaranteed to be defined here.
Loading history...
79
    }
80
81
    /**
82
     * @param QueryBuilder $builder
83
     * @param Join $join
84
     * @param string $field
85
     * @param string $relationAlias
86
     * @return void
87
     */
88 2
    private function join(QueryBuilder $builder, Join $join, string $field, string $relationAlias): void
89
    {
90 2
        switch ($join->getType()) {
91 2
            case Join::TYPE_JOIN:
92
                $builder->join($field, $relationAlias);
93
                break;
94
95 2
            case Join::TYPE_INNER_JOIN:
96
                $builder->innerJoin($field, $relationAlias);
97
                break;
98
99 2
            case Join::TYPE_LEFT_JOIN:
100 2
                $builder->leftJoin($field, $relationAlias);
101 2
                break;
102
        }
103 2
    }
104
105
    /**
106
     * @param array $relation
107
     * @return string
108
     */
109 2
    private function getKey(array $relation): string
110
    {
111 2
        return $relation['sourceEntity'] . '_' . $relation['targetEntity'];
112
    }
113
114
    /**
115
     * @param array $relation
116
     * @return bool
117
     */
118 2
    private function hasAlias(array $relation): bool
119
    {
120 2
        $key = $this->getKey($relation);
121
122 2
        return isset($this->relations[$key]);
123
    }
124
125
    /**
126
     * @param array $relation
127
     * @return string
128
     */
129 2
    private function getCachedAlias(array $relation): string
130
    {
131 2
        $key = $this->getKey($relation);
132
133 2
        if (! isset($this->relations[$key])) {
134 2
            return $this->relations[$key] =
135 2
                $this->query->createAlias(
136 2
                    $relation['sourceEntity'],
137 2
                    $relation['targetEntity']
138
                );
139
        }
140
141
        return $this->relations[$key];
142
    }
143
144
    /**
145
     * @param array $relation
146
     * @return string
147
     */
148
    private function getAlias(array $relation): string
149
    {
150
        return $this->query->createAlias(
151
            $relation['sourceEntity'],
152
            $relation['targetEntity']
153
        );
154
    }
155
}
156