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) { |
|
|
|
|
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; |
|
|
|
|
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); |
|
|
|
|
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
$this->frozen = TRUE; |
336
|
|
|
return $this->iterator = $iterator; |
|
|
|
|
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
|
|
|
|
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.