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

DatabaseProcessor::getProcessor()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 0
cts 5
cp 0
rs 9.9332
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;
11
12
use Doctrine\ORM\Query as DoctrineQuery;
13
use Doctrine\ORM\QueryBuilder;
14
use RDS\Hydrogen\Criteria;
15
use RDS\Hydrogen\Query;
16
17
/**
18
 * Class DatabaseProcessor
19
 */
20
class DatabaseProcessor extends Processor
21
{
22
    /**
23
     * @var string[]|BuilderInterface[]
24
     */
25
    protected const CRITERIA_MAPPINGS = [
26
        Criteria\Group::class     => DatabaseProcessor\GroupBuilder::class,
27
        Criteria\GroupBy::class   => DatabaseProcessor\GroupByBuilder::class,
28
        Criteria\Having::class    => DatabaseProcessor\HavingBuilder::class,
29
        Criteria\Join::class      => DatabaseProcessor\JoinBuilder::class,
30
        Criteria\Limit::class     => DatabaseProcessor\LimitBuilder::class,
31
        Criteria\Offset::class    => DatabaseProcessor\OffsetBuilder::class,
32
        Criteria\OrderBy::class   => DatabaseProcessor\OrderByBuilder::class,
33
        Criteria\Relation::class  => DatabaseProcessor\RelationBuilder::class,
34
        Criteria\Selection::class => DatabaseProcessor\SelectBuilder::class,
35
        Criteria\Where::class     => DatabaseProcessor\WhereBuilder::class,
36
    ];
37
38
    /**
39
     * @param string $entity
40
     * @return ProcessorInterface
41
     */
42
    public function getProcessor(string $entity): ProcessorInterface
43
    {
44
        $repository = $this->em->getRepository($entity);
45
46
        if (\method_exists($repository, 'getProcessor')) {
47
            return $repository->getProcessor();
48
        }
49
50
        return new static($repository, $this->em);
51
    }
52
53
    /**
54
     * @param Query $query
55
     * @return mixed
56
     */
57
    public function getScalarResult(Query $query)
58
    {
59
        return $this->exec($query, function (DoctrineQuery $query) {
60
            return $query->getSingleScalarResult();
61
        });
62
    }
63
64
    /**
65
     * @param Query $query
66
     * @param \Closure $execute
67
     * @return mixed
68
     */
69 30
    private function exec(Query $query, \Closure $execute)
70
    {
71 30
        $query->from($this->repository);
72 30
        $queue = new Queue();
73
74
        /** @var QueryBuilder $builder */
75 30
        $builder = $this->fillQueueThrough($queue, $this->toBuilder($query));
76
77 30
        $result = $execute($builder->getQuery());
78
79 30
        foreach ($queue->reduce($result) as $out) {
80
            $children = $this->bypass($out, $query, $this->applicator($builder));
81
82
            $this->fillQueueThrough($queue, $children);
83
        }
84
85 30
        return $result;
86
    }
87
88
    /**
89
     * @param Queue $queue
90
     * @param \Generator $generator
91
     * @return QueryBuilder|mixed
92
     */
93 30
    private function fillQueueThrough(Queue $queue, \Generator $generator)
94
    {
95 30
        foreach ($generator as $result) {
96
            if ($result instanceof \Closure) {
97
                $queue->push($result);
98
            }
99
        }
100
101 30
        return $generator->getReturn();
102
    }
103
104
    /**
105
     * Creates a new QueryBuilder and applies all necessary operations.
106
     * Returns a set of pending operations (Deferred) and QueryBuilder.
107
     *
108
     * @param Query $query
109
     * @return \Generator|\Closure[]|QueryBuilder
110
     */
111 30
    protected function toBuilder(Query $query): \Generator
112
    {
113 30
        $builder = $this->em->createQueryBuilder();
114 30
        $builder->setCacheable(false);
115
116 30
        yield from $generator = $this->apply($builder, $query);
117
118 30
        return $generator->getReturn();
119
    }
120
121
    /**
122
     * Applies all necessary operations to the QueryBuilder.
123
     * Returns a set of pending operations (Deferred) and QueryBuilder.
124
     *
125
     * @param QueryBuilder $builder
126
     * @param Query $query
127
     * @return \Generator
128
     */
129 30
    protected function apply(QueryBuilder $builder, Query $query): \Generator
130
    {
131
        // Add an alias and indicate that this
132
        // alias is relevant to the entity of a repository.
133 30
        $builder->addSelect($query->getAlias());
134 30
        $builder->from($query->getRepository()->getClassName(), $query->getAlias());
0 ignored issues
show
Bug introduced by
The method getClassName does only exist in Doctrine\Common\Persistence\ObjectRepository, but not in RDS\Hydrogen\Hydrogen.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
135
136
        //
137
        //
138 30
        $response = $this->bypass($this->forEach($builder, $query), $query, $this->applicator($builder));
139
140 30
        foreach ($response as $field => $value) {
141 8
            if ($value instanceof \Closure) {
142
                yield $value;
143
                continue;
144
            }
145
146 8
            $builder->setParameter($field, $value);
147
        }
148
149 30
        return $builder;
150
    }
151
152
    /**
153
     * A set of coroutine operations valid for the current DatabaseProcessor.
154
     *
155
     * @param QueryBuilder $builder
156
     * @return \Closure
157
     */
158
    private function applicator(QueryBuilder $builder): \Closure
159
    {
160 30
        return function ($response) use ($builder) {
161
            // Send the context (the builder) in case the
162
            // answer contains an empty value.
163
            if ($response === null) {
164
                return $builder;
165
            }
166
167
            // In the case that the response is returned to the Query
168
            // instance - we need to fulfill this query and return a response.
169
            if ($response instanceof Query) {
170
                /** @var DatabaseProcessor $processor */
171
                $processor = $response->getRepository()->getProcessor();
0 ignored issues
show
Bug introduced by
The method getProcessor does only exist in RDS\Hydrogen\Hydrogen, but not in Doctrine\Common\Persistence\ObjectRepository.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
172
173
                return $processor->getResult($response);
174
            }
175
176
            return $response;
177 30
        };
178
    }
179
180
    /**
181
     * @param Query $query
182
     * @return iterable
183
     */
184
    public function getResult(Query $query): iterable
185
    {
186 20
        $result = $this->exec($query, function (DoctrineQuery $query) {
187 20
            return $query->getResult();
188 20
        });
189
190 20
        $fn = function (array $result) {
191 20
            foreach ($result as $item) {
192 19
                yield \is_array($item) ? \reset($item) : $item;
193
            }
194 20
        };
195
196 20
        return \iterator_to_array($fn($result));
0 ignored issues
show
Bug Best Practice introduced by
The return type of return \iterator_to_array($fn($result)); (array) is incompatible with the return type declared by the interface RDS\Hydrogen\Processor\P...sorInterface::getResult of type RDS\Hydrogen\Processor\iterable.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
197
    }
198
199
    /**
200
     * @param Query $query
201
     * @return array
202
     */
203
    public function getArrayResult(Query $query): array
204
    {
205 12
        return $this->exec($query, function (DoctrineQuery $query) {
206 12
            return $query->getArrayResult();
207 12
        });
208
    }
209
}
210