1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* This file is part of the Grido (https://github.com/o5/grido) |
5
|
|
|
* |
6
|
|
|
* Copyright (c) 2011 Petr Bugyík (http://petr.bugyik.cz) |
7
|
|
|
* |
8
|
|
|
* For the full copyright and license information, please view |
9
|
|
|
* the file LICENSE.md that was distributed with this source code. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace Grido\DataSources; |
13
|
|
|
|
14
|
|
|
use Grido\Exception; |
15
|
|
|
use Grido\Components\Filters\Condition; |
16
|
|
|
|
17
|
|
|
use Nette\Utils\Strings; |
18
|
|
|
use Nette\Utils\Random; |
19
|
|
|
use Doctrine\ORM\Tools\Pagination\Paginator; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* Doctrine data source. |
23
|
|
|
* |
24
|
|
|
* @package Grido |
25
|
|
|
* @subpackage DataSources |
26
|
|
|
* @author Martin Jantosovic <[email protected]> |
27
|
|
|
* @author Petr Bugyík |
28
|
|
|
* |
29
|
|
|
* @property-read \Doctrine\ORM\QueryBuilder $qb |
30
|
|
|
* @property-read array $filterMapping |
31
|
|
|
* @property-read array $sortMapping |
32
|
|
|
* @property-read int $count |
33
|
|
|
* @property-read array $data |
34
|
|
|
*/ |
35
|
1 |
|
class Doctrine implements IDataSource |
36
|
|
|
{ |
37
|
|
|
use \Nette\SmartObject; |
38
|
|
|
|
39
|
|
|
/** @var \Doctrine\ORM\QueryBuilder */ |
40
|
|
|
protected $qb; |
41
|
|
|
|
42
|
|
|
/** @var array Map column to the query builder */ |
43
|
|
|
protected $filterMapping; |
44
|
|
|
|
45
|
|
|
/** @var array Map column to the query builder */ |
46
|
|
|
protected $sortMapping; |
47
|
|
|
|
48
|
|
|
/** @var bool use OutputWalker in Doctrine Paginator */ |
49
|
|
|
protected $useOutputWalkers; |
50
|
|
|
|
51
|
|
|
/** @var bool fetch join collection in Doctrine Paginator */ |
52
|
|
|
protected $fetchJoinCollection = TRUE; |
53
|
|
|
|
54
|
|
|
/** @var array */ |
55
|
|
|
protected $rand; |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* If $sortMapping is not set and $filterMapping is set, |
59
|
|
|
* $filterMapping will be used also as $sortMapping. |
60
|
|
|
* @param \Doctrine\ORM\QueryBuilder $qb |
61
|
|
|
* @param array $filterMapping Maps columns to the DQL columns |
62
|
|
|
* @param array $sortMapping Maps columns to the DQL columns |
63
|
|
|
*/ |
64
|
1 |
|
public function __construct(\Doctrine\ORM\QueryBuilder $qb, array $filterMapping = NULL, array $sortMapping = NULL) |
65
|
1 |
|
{ |
66
|
1 |
|
$this->qb = $qb; |
67
|
|
|
$this->filterMapping = $filterMapping; |
|
|
|
|
68
|
1 |
|
$this->sortMapping = $sortMapping; |
|
|
|
|
69
|
1 |
|
|
70
|
1 |
|
if (!$this->sortMapping && $this->filterMapping) { |
71
|
1 |
|
$this->sortMapping = $this->filterMapping; |
72
|
|
|
} |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* @param bool $useOutputWalkers |
77
|
|
|
* @return \Grido\DataSources\Doctrine |
78
|
|
|
*/ |
79
|
|
|
public function setUseOutputWalkers($useOutputWalkers) |
80
|
|
|
{ |
81
|
|
|
$this->useOutputWalkers = $useOutputWalkers; |
82
|
|
|
return $this; |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* @param bool $fetchJoinCollection |
87
|
|
|
* @return \Grido\DataSources\Doctrine |
88
|
|
|
*/ |
89
|
|
|
public function setFetchJoinCollection($fetchJoinCollection) |
90
|
|
|
{ |
91
|
|
|
$this->fetchJoinCollection = $fetchJoinCollection; |
92
|
|
|
return $this; |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* @return \Doctrine\ORM\Query |
97
|
|
|
*/ |
98
|
1 |
|
public function getQuery() |
99
|
|
|
{ |
100
|
|
|
return $this->_getQuery($this->qb); |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* Workaround for https://github.com/Kdyby/DoctrineCache/issues/23 |
105
|
|
|
* |
106
|
|
|
* @param \Doctrine\ORM\QueryBuilder $qb |
107
|
|
|
* @return \Doctrine\ORM\Query |
108
|
|
|
*/ |
109
|
|
|
private function _getQuery(\Doctrine\ORM\QueryBuilder $qb) |
110
|
|
|
{ |
111
|
|
|
$query = $qb->getQuery(); |
112
|
|
|
$ttl = $query->getQueryCacheLifetime(); |
113
|
|
|
if (!\is_int($ttl)) { |
114
|
|
|
$query->setQueryCacheLifetime(0); |
115
|
|
|
} |
116
|
|
|
return $query; |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* @return \Doctrine\ORM\QueryBuilder |
121
|
|
|
*/ |
122
|
|
|
public function getQb() |
123
|
|
|
{ |
124
|
|
|
return $this->qb; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* @return array|NULL |
129
|
|
|
*/ |
130
|
|
|
public function getFilterMapping() |
131
|
|
|
{ |
132
|
1 |
|
return $this->filterMapping; |
133
|
1 |
|
} |
134
|
|
|
|
135
|
1 |
|
/** |
136
|
1 |
|
* @return array|NULL |
137
|
|
|
*/ |
138
|
|
|
public function getSortMapping() |
139
|
1 |
|
{ |
140
|
1 |
|
return $this->sortMapping; |
141
|
1 |
|
} |
142
|
1 |
|
|
143
|
1 |
|
/** |
144
|
1 |
|
* @param Condition $condition |
145
|
1 |
|
* @param \Doctrine\ORM\QueryBuilder $qb |
146
|
1 |
|
*/ |
147
|
1 |
|
protected function makeWhere(Condition $condition, \Doctrine\ORM\QueryBuilder $qb = NULL) |
148
|
1 |
|
{ |
149
|
|
|
$qb = $qb === NULL |
150
|
1 |
|
? $this->qb |
151
|
1 |
|
: $qb; |
152
|
|
|
|
153
|
1 |
|
if ($condition->callback) { |
154
|
1 |
|
return call_user_func_array($condition->callback, [$condition->value, $qb]); |
155
|
1 |
|
} |
156
|
1 |
|
|
157
|
1 |
|
$columns = $condition->column; |
158
|
1 |
|
foreach ($columns as $key => $column) { |
159
|
|
|
if (!Condition::isOperator($column)) { |
160
|
1 |
|
$columns[$key] = (isset($this->filterMapping[$column]) |
161
|
|
|
? $this->filterMapping[$column] |
162
|
1 |
|
: (Strings::contains($column, ".") |
163
|
1 |
|
? $column |
164
|
1 |
|
: current($this->qb->getRootAliases()) . '.' . $column)); |
165
|
1 |
|
} |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
$condition->setColumn($columns); |
169
|
|
|
list($where) = $condition->__toArray(NULL, NULL, FALSE); |
170
|
|
|
|
171
|
|
|
$rand = $this->getRand(); |
172
|
|
|
$where = preg_replace_callback('/\?/', function() use ($rand) { |
173
|
1 |
|
static $i = -1; |
174
|
1 |
|
$i++; |
175
|
|
|
return ":$rand{$i}"; |
176
|
1 |
|
}, $where); |
177
|
1 |
|
|
178
|
|
|
$qb->andWhere($where); |
179
|
|
|
|
180
|
|
|
foreach ($condition->getValueForColumn() as $i => $val) { |
181
|
|
|
$qb->setParameter("$rand{$i}", $val); |
182
|
|
|
} |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* @return string |
187
|
1 |
|
*/ |
188
|
1 |
|
protected function getRand() |
189
|
|
|
{ |
190
|
1 |
|
do { |
191
|
|
|
$rand = Random::generate(4, 'a-z'); |
192
|
|
|
} while (isset($this->rand[$rand])); |
193
|
|
|
|
194
|
|
|
$this->rand[$rand] = $rand; |
195
|
|
|
return $rand; |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
/*********************************** interface IDataSource ************************************/ |
199
|
|
|
|
200
|
|
|
/** |
201
|
1 |
|
* @return int |
202
|
|
|
*/ |
203
|
|
|
public function getCount() |
204
|
1 |
|
{ |
205
|
1 |
|
$paginator = new Paginator($this->getQuery(), $this->fetchJoinCollection); |
206
|
1 |
|
$paginator->setUseOutputWalkers($this->useOutputWalkers); |
207
|
|
|
|
208
|
1 |
|
return $paginator->count(); |
209
|
|
|
} |
210
|
1 |
|
|
211
|
1 |
|
/** |
212
|
|
|
* It is possible to use query builder with additional columns. |
213
|
1 |
|
* In this case, only item at index [0] is returned, because |
214
|
|
|
* it should be an entity object. |
215
|
1 |
|
* @return array |
216
|
|
|
*/ |
217
|
|
|
public function getData() |
218
|
|
|
{ |
219
|
|
|
$data = []; |
220
|
|
|
|
221
|
|
|
// Paginator is better if the query uses ManyToMany associations |
222
|
|
|
$result = $this->qb->getMaxResults() !== NULL || $this->qb->getFirstResult() !== NULL |
223
|
|
|
? new Paginator($this->getQuery()) |
224
|
1 |
|
: $this->getQuery()->getResult(); |
225
|
1 |
|
|
226
|
1 |
|
foreach ($result as $item) { |
227
|
1 |
|
// Return only entity itself |
228
|
|
|
$data[] = is_array($item) |
229
|
|
|
? $item[0] |
230
|
|
|
: $item; |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
return $data; |
234
|
|
|
} |
235
|
|
|
|
236
|
1 |
|
/** |
237
|
1 |
|
* Sets filter. |
238
|
1 |
|
* @param array $conditions |
239
|
|
|
*/ |
240
|
|
|
public function filter(array $conditions) |
241
|
|
|
{ |
242
|
|
|
foreach ($conditions as $condition) { |
243
|
|
|
$this->makeWhere($condition); |
244
|
|
|
} |
245
|
|
|
} |
246
|
1 |
|
|
247
|
1 |
|
/** |
248
|
1 |
|
* Sets offset and limit. |
249
|
1 |
|
* @param int $offset |
250
|
|
|
* @param int $limit |
251
|
1 |
|
*/ |
252
|
1 |
|
public function limit($offset, $limit) |
253
|
1 |
|
{ |
254
|
|
|
$this->qb->setFirstResult($offset) |
255
|
|
|
->setMaxResults($limit); |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
/** |
259
|
|
|
* Sets sorting. |
260
|
|
|
* @param array $sorting |
261
|
|
|
*/ |
262
|
|
|
public function sort(array $sorting) |
263
|
|
|
{ |
264
|
1 |
|
foreach ($sorting as $key => $value) { |
265
|
1 |
|
$column = isset($this->sortMapping[$key]) |
266
|
|
|
? $this->sortMapping[$key] |
267
|
1 |
|
: current($this->qb->getRootAliases()) . '.' . $key; |
268
|
1 |
|
|
269
|
1 |
|
$this->qb->addOrderBy($column, $value); |
270
|
1 |
|
} |
271
|
|
|
} |
272
|
1 |
|
|
273
|
1 |
|
/** |
274
|
|
|
* @param mixed $column |
275
|
1 |
|
* @param array $conditions |
276
|
1 |
|
* @param int $limit |
277
|
1 |
|
* @return array |
278
|
|
|
* @throws Exception |
279
|
1 |
|
*/ |
280
|
1 |
|
public function suggest($column, array $conditions, $limit) |
281
|
1 |
|
{ |
282
|
1 |
|
$qb = clone $this->qb; |
283
|
1 |
|
$qb->setMaxResults($limit); |
284
|
1 |
|
|
285
|
1 |
|
if (is_string($column)) { |
286
|
1 |
|
$mapping = isset($this->filterMapping[$column]) |
287
|
|
|
? $this->filterMapping[$column] |
288
|
|
|
: current($qb->getRootAliases()) . '.' . $column; |
289
|
|
|
|
290
|
|
|
$qb->select($mapping)->distinct()->orderBy($mapping); |
291
|
1 |
|
} |
292
|
1 |
|
|
293
|
|
|
foreach ($conditions as $condition) { |
294
|
1 |
|
$this->makeWhere($condition, $qb); |
295
|
1 |
|
} |
296
|
|
|
|
297
|
|
|
$items = []; |
298
|
1 |
|
$data = $this->_getQuery($qb)->getScalarResult(); |
299
|
|
|
foreach ($data as $row) { |
300
|
|
|
if (is_string($column)) { |
301
|
|
|
$value = (string) current($row); |
302
|
|
|
} elseif (is_callable($column)) { |
303
|
|
|
$value = (string) $column($row); |
304
|
|
|
} else { |
305
|
|
|
$type = gettype($column); |
306
|
|
|
throw new Exception("Column of suggestion must be string or callback, $type given."); |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
$items[$value] = \Latte\Runtime\Filters::escapeHtml($value); |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
is_callable($column) && sort($items); |
313
|
|
|
return array_values($items); |
314
|
|
|
} |
315
|
|
|
} |
316
|
|
|
|
Our type inference engine has found an assignment of a scalar value (like a string, an integer or null) to a property which is an array.
Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property.
To type hint that a parameter can be either an array or null, you can set a type hint of array and a default value of null. The PHP interpreter will then accept both an array or null for that parameter.
The function can be called with either null or an array for the parameter
$needle
but will only accept an array as$haystack
.