ResultSet::count()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
/**
4
 * This file is part of the Kdyby (http://www.kdyby.org)
5
 *
6
 * Copyright (c) 2008 Filip Procházka ([email protected])
7
 *
8
 * For the full copyright and license information, please view the file license.txt that was distributed with this source code.
9
 */
10
11
namespace Kdyby\Doctrine;
12
13
use Doctrine\ORM;
14
use Doctrine\ORM\ORMException;
15
use Doctrine\ORM\Tools\Pagination\Paginator as ResultPaginator;
16
use Kdyby;
17
use Kdyby\Persistence\Queryable;
18
use Nette;
19
use Nette\Utils\Strings;
20
use Nette\Utils\Paginator as UIPaginator;
21
22
23
24
/**
25
 * ResultSet accepts a Query that it can then paginate and count the results for you
26
 *
27
 * <code>
28
 * public function renderDefault()
29
 * {
30
 *    $articles = $this->articlesRepository->fetch(new ArticlesQuery());
31
 *    $articles->applyPaginator($this['vp']->paginator);
32
 *    $this->template->articles = $articles;
33
 * }
34
 *
35
 * protected function createComponentVp()
36
 * {
37
 *    return new VisualPaginator;
38
 * }
39
 * </code>.
40
 *
41
 * It automatically counts the query, passes the count of results to paginator
42
 * and then reads the offset from paginator and applies it to the query so you get the correct results.
43
 *
44
 * @author Filip Procházka <[email protected]>
45
 */
46
class ResultSet implements \Countable, \IteratorAggregate
47
{
48
49
	use \Kdyby\StrictObjects\Scream;
50
51
	/**
52
	 * @var int|NULL
53
	 */
54
	private $totalCount;
55
56
	/**
57
	 * @var \Doctrine\ORM\AbstractQuery|\Doctrine\ORM\Query|\Doctrine\ORM\NativeQuery
58
	 */
59
	private $query;
60
61
	/**
62
	 * @var \Kdyby\Doctrine\QueryObject|NULL
63
	 */
64
	private $queryObject;
65
66
	/**
67
	 * @var \Kdyby\Persistence\Queryable|NULL
68
	 */
69
	private $repository;
70
71
	/**
72
	 * @var bool
73
	 */
74
	private $fetchJoinCollection = TRUE;
75
76
	/**
77
	 * @var bool|NULL
78
	 */
79
	private $useOutputWalkers;
80
81
	/**
82
	 * @var \ArrayIterator|NULL
83
	 */
84
	private $iterator;
85
86
	/**
87
	 * @var bool
88
	 */
89
	private $frozen = FALSE;
90
91
92
93
	/**
94
	 * @param ORM\AbstractQuery $query
95
	 * @param \Kdyby\Doctrine\QueryObject $queryObject
96
	 * @param \Kdyby\Persistence\Queryable $repository
97
	 */
98
	public function __construct(ORM\AbstractQuery $query, QueryObject $queryObject = NULL, Queryable $repository = NULL)
99
	{
100
		$this->query = $query;
101
		$this->queryObject = $queryObject;
102
		$this->repository = $repository;
103
104
		if ($this->query instanceof NativeQueryWrapper || $this->query instanceof ORM\NativeQuery) {
105
			$this->fetchJoinCollection = FALSE;
106
		}
107
	}
108
109
110
111
	/**
112
	 * @param bool $fetchJoinCollection
113
	 * @throws InvalidStateException
114
	 * @return ResultSet
115
	 */
116
	public function setFetchJoinCollection($fetchJoinCollection)
117
	{
118
		$this->updating();
119
120
		$this->fetchJoinCollection = !is_bool($fetchJoinCollection) ? (bool) $fetchJoinCollection : $fetchJoinCollection;
121
		$this->iterator = NULL;
122
123
		return $this;
124
	}
125
126
127
128
	/**
129
	 * @param bool|null $useOutputWalkers
130
	 * @throws InvalidStateException
131
	 * @return ResultSet
132
	 */
133
	public function setUseOutputWalkers($useOutputWalkers)
134
	{
135
		$this->updating();
136
137
		$this->useOutputWalkers = $useOutputWalkers;
138
		$this->iterator = NULL;
139
140
		return $this;
141
	}
142
143
144
145
	/**
146
	 * @return bool|null
147
	 */
148
	public function getUseOutputWalkers()
149
	{
150
		return $this->useOutputWalkers;
151
	}
152
153
154
155
	/**
156
	 * @return boolean
157
	 */
158
	public function getFetchJoinCollection()
159
	{
160
		return $this->fetchJoinCollection;
161
	}
162
163
164
165
	/**
166
	 * Removes ORDER BY clause that is not inside subquery.
167
	 *
168
	 * @throws InvalidStateException
169
	 * @return ResultSet
170
	 */
171
	public function clearSorting()
172
	{
173
		$this->updating();
174
175
		if ($this->query instanceof ORM\Query) {
176
			$dql = Strings::normalize((string) $this->query->getDQL());
177
			if (preg_match('~^(.+)\\s+(ORDER BY\\s+((?!FROM|WHERE|ORDER\\s+BY|GROUP\\sBY|JOIN).)*)\\z~si', $dql, $m)) {
178
				$dql = $m[1];
179
			}
180
			$this->query->setDQL(trim($dql));
181
		}
182
183
		return $this;
184
	}
185
186
187
188
	/**
189
	 * @param string|array $columns
190
	 * @throws InvalidStateException
191
	 * @return ResultSet
192
	 */
193
	public function applySorting($columns)
194
	{
195
		$this->updating();
196
197
		$sorting = [];
198
		foreach (is_array($columns) ? $columns : func_get_args() as $name => $column) {
199
			if (!is_numeric($name)) {
200
				$column = $name . ' ' . $column;
201
			}
202
203
			if (!preg_match('~\s+(DESC|ASC)\s*\z~i', $column = trim($column))) {
204
				$column .= ' ASC';
205
			}
206
			$sorting[] = $column;
207
		}
208
209
		if ($sorting && $this->query instanceof ORM\Query) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $sorting of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
210
			$dql = Strings::normalize((string) $this->query->getDQL());
211
212
			if (!preg_match('~^(.+)\\s+(ORDER BY\\s+((?!FROM|WHERE|ORDER\\s+BY|GROUP\\sBY|JOIN).)*)\\z~si', $dql, $m)) {
213
				$dql .= ' ORDER BY ';
214
215
			} else {
216
				$dql .= ', ';
217
			}
218
219
			$this->query->setDQL($dql . implode(', ', $sorting));
220
		}
221
		$this->iterator = NULL;
222
223
		return $this;
224
	}
225
226
227
228
	/**
229
	 * @param int|NULL $offset
230
	 * @param int|NULL $limit
231
	 *
232
	 * @throws InvalidStateException
233
	 * @return ResultSet
234
	 */
235
	public function applyPaging($offset, $limit)
236
	{
237
		if ($this->query instanceof ORM\Query && ($this->query->getFirstResult() != $offset || $this->query->getMaxResults() != $limit)) {
238
			$this->query->setFirstResult($offset);
239
			$this->query->setMaxResults($limit);
240
			$this->iterator = NULL;
241
		}
242
243
		return $this;
244
	}
245
246
247
248
	/**
249
	 * @param \Nette\Utils\Paginator $paginator
250
	 * @param int $itemsPerPage
251
	 * @return ResultSet
252
	 */
253
	public function applyPaginator(UIPaginator $paginator, $itemsPerPage = NULL)
254
	{
255
		if ($itemsPerPage !== NULL) {
256
			$paginator->setItemsPerPage($itemsPerPage);
257
		}
258
259
		$paginator->setItemCount($this->getTotalCount());
260
		$this->applyPaging($paginator->getOffset(), $paginator->getLength());
261
262
		return $this;
263
	}
264
265
266
267
	/**
268
	 * @return bool
269
	 */
270
	public function isEmpty()
271
	{
272
		$count = $this->getTotalCount();
273
		$offset = $this->query instanceof ORM\Query ? $this->query->getFirstResult() : 0;
274
275
		return $count <= $offset;
276
	}
277
278
279
280
	/**
281
	 * @throws \Kdyby\Doctrine\QueryException
282
	 * @return int
283
	 */
284
	public function getTotalCount()
285
	{
286
		if ($this->totalCount !== NULL) {
287
			return $this->totalCount;
288
		}
289
290
		try {
291
			$paginatedQuery = $this->createPaginatedQuery($this->query);
292
293
			if ($this->queryObject !== NULL && $this->repository !== NULL) {
294
				$totalCount = $this->queryObject->count($this->repository, $this, $paginatedQuery);
295
296
			} else {
297
				$totalCount = $paginatedQuery->count();
298
			}
299
300
			$this->frozen = TRUE;
301
			return $this->totalCount = $totalCount;
0 ignored issues
show
Documentation Bug introduced by
It seems like $totalCount can also be of type double. However, the property $totalCount is declared as type integer|null. 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...
302
303
		} catch (ORMException $e) {
304
			throw new QueryException($e, $this->query, $e->getMessage());
305
		}
306
	}
307
308
309
310
	/**
311
	 * @param int $hydrationMode
312
	 * @throws QueryException
313
	 * @return \ArrayIterator
314
	 */
315
	public function getIterator($hydrationMode = ORM\AbstractQuery::HYDRATE_OBJECT)
316
	{
317
		if ($this->iterator !== NULL) {
318
			return $this->iterator;
319
		}
320
321
		$this->query->setHydrationMode($hydrationMode);
322
323
		try {
324
			if ($this->fetchJoinCollection && $this->query instanceof ORM\Query && ($this->query->getMaxResults() > 0 || $this->query->getFirstResult() > 0)) {
325
				$iterator = $this->createPaginatedQuery($this->query)->getIterator();
326
327
			} else {
328
				$iterator = new \ArrayIterator($this->query->getResult());
329
			}
330
331
			if ($this->queryObject !== NULL && $this->repository !== NULL) {
332
				$this->queryObject->postFetch($this->repository, $iterator);
0 ignored issues
show
Compatibility introduced by
$iterator of type object<Traversable> is not a sub-type of object<Iterator>. It seems like you assume a child interface of the interface Traversable 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...
333
			}
334
335
			$this->frozen = TRUE;
336
			return $this->iterator = $iterator;
0 ignored issues
show
Documentation Bug introduced by
It seems like $iterator of type object<Traversable> is incompatible with the declared type object<ArrayIterator>|null of property $iterator.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
337
338
		} catch (ORMException $e) {
339
			throw new QueryException($e, $this->query, $e->getMessage());
340
		}
341
	}
342
343
344
345
	/**
346
	 * @param int $hydrationMode
347
	 * @return array
348
	 */
349
	public function toArray($hydrationMode = ORM\AbstractQuery::HYDRATE_OBJECT)
350
	{
351
		return iterator_to_array(clone $this->getIterator($hydrationMode), TRUE);
352
	}
353
354
355
356
	/**
357
	 * @return int
358
	 */
359
	public function count()
360
	{
361
		return $this->getIterator()->count();
362
	}
363
364
365
366
	/**
367
	 * @param \Doctrine\ORM\AbstractQuery|\Doctrine\ORM\Query|\Doctrine\ORM\NativeQuery $query
368
	 * @return \Doctrine\ORM\Tools\Pagination\Paginator
369
	 */
370
	private function createPaginatedQuery(ORM\AbstractQuery $query)
371
	{
372
		if (!$query instanceof ORM\Query) {
373
			throw new InvalidArgumentException(sprintf('QueryObject pagination only works with %s', \Doctrine\ORM\Query::class));
374
		}
375
376
		$paginated = new ResultPaginator($query, $this->fetchJoinCollection);
377
		$paginated->setUseOutputWalkers($this->useOutputWalkers);
378
379
		return $paginated;
380
	}
381
382
383
384
	private function updating()
385
	{
386
		if ($this->frozen !== FALSE) {
387
			throw new InvalidStateException("Cannot modify result set, that was already fetched from storage.");
388
		}
389
	}
390
391
}
392