Completed
Push — master ( b7ef6a...58a002 )
by Kirill
03:12
created

JoinBuilder::apply()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.686

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 4
cts 9
cp 0.4444
rs 9.7998
c 0
b 0
f 0
cc 2
nc 2
nop 2
crap 2.686
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
            // Resolve relation alias
58 2
            $relationAlias = $isLast && $join->hasJoinQuery()
59
                ? $this->getAlias($relation)
60 2
                : $this->getCachedAlias($relation);
61
62
            // Create join
63 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...
64 2
            $this->join($builder, $join, $relationField, $relationAlias);
65
66
            // Add join to selection statement
67 2
            $builder->addSelect($relationAlias);
68
69
            // Shift parent
70 2
            $alias = $relationAlias;
71
        }
72
73 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...
74
    }
75
76
    /**
77
     * @param QueryBuilder $builder
78
     * @param Join $join
79
     * @param string $field
80
     * @param string $relationAlias
81
     * @return void
82
     */
83 2
    private function join(QueryBuilder $builder, Join $join, string $field, string $relationAlias): void
84
    {
85 2
        switch ($join->getType()) {
86 2
            case Join::TYPE_JOIN:
87
                $builder->join($field, $relationAlias);
88
                break;
89
90 2
            case Join::TYPE_INNER_JOIN:
91
                $builder->innerJoin($field, $relationAlias);
92
                break;
93
94 2
            case Join::TYPE_LEFT_JOIN:
95 2
                $builder->leftJoin($field, $relationAlias);
96 2
                break;
97
        }
98 2
    }
99
100
    /**
101
     * @param array $relation
102
     * @return string
103
     */
104 2
    private function getKey(array $relation): string
105
    {
106 2
        return $relation['sourceEntity'] . '_' . $relation['targetEntity'];
107
    }
108
109
    /**
110
     * @param array $relation
111
     * @return string
112
     */
113 2
    private function getCachedAlias(array $relation): string
114
    {
115 2
        $key = $this->getKey($relation);
116
117 2
        if (! isset($this->relations[$key])) {
118 2
            return $this->relations[$key] =
119 2
                $this->query->createAlias(
120 2
                    $relation['sourceEntity'],
121 2
                    $relation['targetEntity']
122
                );
123
        }
124
125
        return $this->relations[$key];
126
    }
127
128
    /**
129
     * @param array $relation
130
     * @return string
131
     */
132
    private function getAlias(array $relation): string
133
    {
134
        return $this->query->createAlias(
135
            $relation['sourceEntity'],
136
            $relation['targetEntity']
137
        );
138
    }
139
}
140