Test Failed
Push — master ( 1c5165...b7ef6a )
by Kirill
03:37
created

RelationBuilder::getInverseRelationMappings()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 0
cts 7
cp 0
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 6
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\Mapping\ClassMetadata;
13
use Doctrine\ORM\QueryBuilder;
14
use RDS\Hydrogen\Criteria\CriterionInterface;
15
use RDS\Hydrogen\Criteria\Relation;
16
use RDS\Hydrogen\Query;
17
18
/**
19
 * Class RelationBuilder
20
 */
21
class RelationBuilder extends WhereBuilder
22
{
23
    /**
24
     * @var int
25
     */
26
    private static $relationId = 0;
27
28
    /**
29
     * @param QueryBuilder $builder
30
     * @param CriterionInterface|Relation $relation
31
     * @return \Generator
32
     */
33
    public function apply($builder, CriterionInterface $relation): \Generator
34
    {
35
        [$selections, $table, $alias] = [[], $this->meta->getTableName(), $relation->getAlias()];
0 ignored issues
show
Bug introduced by
The variable $selections 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 $table 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...
Bug introduced by
It seems like you code against a concrete implementation and not the interface RDS\Hydrogen\Criteria\CriterionInterface as the method getAlias() does only exist in the following implementations of said interface: RDS\Hydrogen\Criteria\Join, RDS\Hydrogen\Criteria\Relation, RDS\Hydrogen\Criteria\Selection.

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...
36
37
        $association = $this->getAssociation($this->meta, $relation->getRelation()->getName());
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 getRelation() does only exist in the following implementations of said interface: RDS\Hydrogen\Criteria\Join, RDS\Hydrogen\Criteria\Relation.

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...
38
39
        foreach ($this->getRelationsMappings($association) as $parent => $child) {
40
            $selections[$parent] = ['relatedTo' => $child, 'values' => []];
0 ignored issues
show
Coding Style Comprehensibility introduced by
$selections was never initialized. Although not strictly required by PHP, it is generally a good practice to add $selections = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
41
42
            $builder->addSelect("RAW('$table', '$alias', '$parent') AS $parent");
43
        }
44
45
        yield function (iterable $result) use ($association, $selections, $relation) {
0 ignored issues
show
Bug introduced by
The variable $selections 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...
46
            $query = $relation->getQuery();
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 getQuery() does only exist in the following implementations of said interface: RDS\Hydrogen\Criteria\Group, RDS\Hydrogen\Criteria\Join, RDS\Hydrogen\Criteria\Relation.

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...
47
            [$table, $alias] = [$this->meta($association['targetEntity'])->getTableName(), $query->getAlias()];
0 ignored issues
show
Bug introduced by
The variable $table 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...
48
49
            foreach ($result as $entity => $mappings) {
50
                foreach ($selections as $parent => $data) {
51
                    $selections[$parent]['values'][] = $mappings[$parent];
52
                }
53
            }
54
55
            foreach ($selections as $parent => ['relatedTo' => $child, 'values' => $values]) {
56
                $query->select(["RAW('$table', '$alias', '$child')" => $child]);
0 ignored issues
show
Bug introduced by
The variable $child 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...
57
                $query = $query->whereIn(':' . $child, $values);
0 ignored issues
show
Bug introduced by
The variable $values 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...
58
            }
59
60
            $this->execute($association['targetEntity'], $query);
61
        };
62
    }
63
64
    /**
65
     * @param ClassMetadata $metadata
66
     * @param string $relation
67
     * @return array
68
     */
69
    private function getAssociation(ClassMetadata $metadata, string $relation): array
70
    {
71
        $association = $metadata->associationMappings[$relation] ?? null;
72
73
        if (! $association) {
74
            $error = 'Invalid relation name "%s" of entity %s';
75
            throw new \LogicException(\sprintf($error, $relation, $metadata->name));
76
        }
77
78
        return $association;
79
    }
80
81
    /**
82
     * @param array $association
83
     * @return \Generator
84
     */
85
    private function getRelationsMappings(array $association): \Generator
86
    {
87
        yield from $association['isOwningSide']
88
            ? $this->getDirectRelationMappings($association)
89
            : $this->getInverseRelationMappings($association);
90
    }
91
92
    /**
93
     * @param array $association
94
     * @return \Generator
95
     */
96
    private function getDirectRelationMappings(array $association): \Generator
97
    {
98
        $joins = $association['joinColumns'] ?? null;
99
100
        if (! $joins) {
101
            $error = 'Relation "%s" of entity %s should provide one or more join columns.';
102
            throw new \LogicException(\sprintf($error, $association['fieldName'], $association['sourceEntity']));
103
        }
104
105
        foreach ($joins as $join) {
106
            yield $join['name'] => $join['referencedColumnName'];
107
        }
108
    }
109
110
    /**
111
     * @param array $association
112
     * @return \Generator
113
     */
114
    private function getInverseRelationMappings(array $association): \Generator
115
    {
116
        $association = $this->getAssociation($this->meta($association['targetEntity']), $association['mappedBy']);
117
118
        foreach ($this->getDirectRelationMappings($association) as $parent => $child) {
119
            yield $child => $parent;
120
        }
121
    }
122
123
    /**
124
     * @param array $association
125
     * @return array
126
     */
127
    private function getJoinContext(array $association): array
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
128
    {
129
        if ($association['isOwningSide']) {
130
            return [
131
                $this->getDirectRelationMappings($association),
132
                $this->meta,
133
                $this->meta($association['targetEntity']),
134
            ];
135
        }
136
137
        return [
138
            $this->getInverseRelationMappings($association),
139
            $this->meta($association['targetEntity']),
140
            $this->meta,
141
        ];
142
    }
143
}
144