Completed
Pull Request — master (#6194)
by Marco
27:35 queued 21:56
created

Paginator::getQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
ccs 0
cts 2
cp 0
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 2
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ORM\Tools\Pagination;
21
22
use Doctrine\ORM\Query\Parser;
23
use Doctrine\ORM\QueryBuilder;
24
use Doctrine\ORM\Query;
25
use Doctrine\ORM\Query\ResultSetMapping;
26
use Doctrine\ORM\NoResultException;
27
28
/**
29
 * The paginator can handle various complex scenarios with DQL.
30
 *
31
 * @author Pablo Díez <[email protected]>
32
 * @author Benjamin Eberlei <[email protected]>
33
 * @license New BSD
34
 */
35
class Paginator implements \Countable, \IteratorAggregate
36
{
37
    /**
38
     * @var Query
39
     */
40
    private $query;
41
42
    /**
43
     * @var bool
44
     */
45
    private $fetchJoinCollection;
46
47
    /**
48
     * @var bool|null
49
     */
50
    private $useOutputWalkers;
51
52
    /**
53
     * @var int
54
     */
55
    private $count;
56
57
    /**
58
     * Constructor.
59
     *
60
     * @param Query|QueryBuilder $query               A Doctrine ORM query or query builder.
61
     * @param boolean            $fetchJoinCollection Whether the query joins a collection (true by default).
62
     */
63 106
    public function __construct($query, $fetchJoinCollection = true)
64
    {
65 106
        if ($query instanceof QueryBuilder) {
66 1
            $query = $query->getQuery();
67
        }
68
69 106
        $this->query = $query;
70 106
        $this->fetchJoinCollection = (bool) $fetchJoinCollection;
71 106
    }
72
73
    /**
74
     * Returns the query.
75
     *
76
     * @return Query
77
     */
78
    public function getQuery()
79
    {
80
        return $this->query;
81
    }
82
83
    /**
84
     * Returns whether the query joins a collection.
85
     *
86
     * @return boolean Whether the query joins a collection.
87
     */
88
    public function getFetchJoinCollection()
89
    {
90
        return $this->fetchJoinCollection;
91
    }
92
93
    /**
94
     * Returns whether the paginator will use an output walker.
95
     *
96
     * @return bool|null
97
     */
98
    public function getUseOutputWalkers()
99
    {
100
        return $this->useOutputWalkers;
101
    }
102
103
    /**
104
     * Sets whether the paginator will use an output walker.
105
     *
106
     * @param bool|null $useOutputWalkers
107
     *
108
     * @return $this
109
     */
110 94
    public function setUseOutputWalkers($useOutputWalkers)
111
    {
112 94
        $this->useOutputWalkers = $useOutputWalkers;
113
114 94
        return $this;
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120 17
    public function count()
121
    {
122 17
        if ($this->count === null) {
123
            try {
124 17
                $this->count = array_sum(array_map('current', $this->getCountQuery()->getScalarResult()));
0 ignored issues
show
Documentation Bug introduced by
It seems like array_sum(array_map('cur...()->getScalarResult())) can also be of type double. However, the property $count is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
125 2
            } catch (NoResultException $e) {
126
                $this->count = 0;
127
            }
128
        }
129
130 15
        return $this->count;
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136 93
    public function getIterator()
137
    {
138 93
        $offset = $this->query->getFirstResult();
139 93
        $length = $this->query->getMaxResults();
140
141 93
        if ($this->fetchJoinCollection) {
142 66
            $subQuery = $this->cloneQuery($this->query);
143
144 66
            if ($this->useOutputWalker($subQuery)) {
145 42
                $subQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
146
            } else {
147 24
                $this->appendTreeWalker($subQuery, LimitSubqueryWalker::class);
148
            }
149
150 66
            $subQuery->setFirstResult($offset)->setMaxResults($length);
151
152 66
            $ids = array_map('current', $subQuery->getScalarResult());
153
154 63
            $whereInQuery = $this->cloneQuery($this->query);
155
            // don't do this for an empty id array
156 63
            if (count($ids) === 0) {
157 1
                return new \ArrayIterator([]);
158
            }
159
160 63
            $this->appendTreeWalker($whereInQuery, WhereInWalker::class);
161 63
            $whereInQuery->setHint(WhereInWalker::HINT_PAGINATOR_ID_COUNT, count($ids));
162 63
            $whereInQuery->setFirstResult(null)->setMaxResults(null);
163 63
            $whereInQuery->setParameter(WhereInWalker::PAGINATOR_ID_ALIAS, $ids);
164 63
            $whereInQuery->setCacheable($this->query->isCacheable());
165
166 63
            $result = $whereInQuery->getResult($this->query->getHydrationMode());
167
        } else {
168 27
            $result = $this->cloneQuery($this->query)
169 27
                ->setMaxResults($length)
170 27
                ->setFirstResult($offset)
171 27
                ->setCacheable($this->query->isCacheable())
172 27
                ->getResult($this->query->getHydrationMode())
173
            ;
174
        }
175
176 90
        return new \ArrayIterator($result);
177
    }
178
179
    /**
180
     * Clones a query.
181
     *
182
     * @param Query $query The query.
183
     *
184
     * @return Query The cloned query.
185
     */
186 106
    private function cloneQuery(Query $query)
187
    {
188
        /* @var $cloneQuery Query */
189 106
        $cloneQuery = clone $query;
190
191 106
        $cloneQuery->setParameters(clone $query->getParameters());
192 106
        $cloneQuery->setCacheable(false);
193
194 106
        foreach ($query->getHints() as $name => $value) {
195 3
            $cloneQuery->setHint($name, $value);
196
        }
197
198 106
        return $cloneQuery;
199
    }
200
201
    /**
202
     * Determines whether to use an output walker for the query.
203
     *
204
     * @param Query $query The query.
205
     *
206
     * @return bool
207
     */
208 80
    private function useOutputWalker(Query $query)
209
    {
210 80
        if ($this->useOutputWalkers === null) {
211 12
            return (bool) $query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER) === false;
212
        }
213
214 68
        return $this->useOutputWalkers;
215
    }
216
217
    /**
218
     * Appends a custom tree walker to the tree walkers hint.
219
     *
220
     * @param Query  $query
221
     * @param string $walkerClass
222
     */
223 73
    private function appendTreeWalker(Query $query, $walkerClass)
224
    {
225 73
        $hints = $query->getHint(Query::HINT_CUSTOM_TREE_WALKERS);
226
227 73
        if ($hints === false) {
228 72
            $hints = [];
229
        }
230
231 73
        $hints[] = $walkerClass;
232 73
        $query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, $hints);
233 73
    }
234
235
    /**
236
     * Returns Query prepared to count.
237
     *
238
     * @return Query
239
     */
240 17
    private function getCountQuery()
241
    {
242
        /* @var $countQuery Query */
243 17
        $countQuery = $this->cloneQuery($this->query);
244
245 17
        if ( ! $countQuery->hasHint(CountWalker::HINT_DISTINCT)) {
246 17
            $countQuery->setHint(CountWalker::HINT_DISTINCT, true);
247
        }
248
249 17
        if ($this->useOutputWalker($countQuery)) {
250 10
            $platform = $countQuery->getEntityManager()->getConnection()->getDatabasePlatform(); // law of demeter win
251
252 10
            $rsm = $countQuery->getResultSetMapping();
253
254 10
            $rsm->addScalarResult($platform->getSQLResultCasing('dctrn_count'), 'count');
255
256 10
            $countQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, CountOutputWalker::class);
257 10
            $countQuery->setResultSetMapping($rsm);
0 ignored issues
show
Bug introduced by
It seems like $rsm defined by $countQuery->getResultSetMapping() on line 252 can be null; however, Doctrine\ORM\AbstractQuery::setResultSetMapping() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
258
        } else {
259 8
            $this->appendTreeWalker($countQuery, CountWalker::class);
260
        }
261
262 17
        $countQuery->setFirstResult(null)->setMaxResults(null);
263
264 17
        $parser            = new Parser($countQuery);
265 17
        $parameterMappings = $parser->parse()->getParameterMappings();
266
        /* @var $parameters \Doctrine\Common\Collections\Collection|\Doctrine\ORM\Query\Parameter[] */
267 15
        $parameters        = $countQuery->getParameters();
268
269 15
        foreach ($parameters as $key => $parameter) {
270 2
            $parameterName = $parameter->getName();
271
272 2
            if ( ! (isset($parameterMappings[$parameterName]) || array_key_exists($parameterName, $parameterMappings))) {
273 2
                unset($parameters[$key]);
274
            }
275
        }
276
277 15
        $countQuery->setParameters($parameters);
0 ignored issues
show
Bug introduced by
It seems like $parameters can also be of type object<Doctrine\Common\Collections\Collection>; however, Doctrine\ORM\AbstractQuery::setParameters() does only seem to accept object<Doctrine\Common\C...\ArrayCollection>|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
278
279 15
        return $countQuery;
280
    }
281
}
282