|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace ORM; |
|
4
|
|
|
|
|
5
|
|
|
use ORM\Exception\NotJoined; |
|
6
|
|
|
use ORM\QueryBuilder\ParenthesisInterface; |
|
7
|
|
|
use ORM\QueryBuilder\QueryBuilder; |
|
8
|
|
|
use ORM\QueryBuilder\QueryBuilderInterface; |
|
9
|
|
|
|
|
10
|
|
|
/** |
|
11
|
|
|
* Fetch entities from database |
|
12
|
|
|
* |
|
13
|
|
|
* If you need more specific queries you write them yourself. If you need just more specific where clause you can pass |
|
14
|
|
|
* them to the *where() methods. |
|
15
|
|
|
* |
|
16
|
|
|
* Supported: |
|
17
|
|
|
* - joins with on clause (and alias) |
|
18
|
|
|
* - joins with using (and alias) |
|
19
|
|
|
* - where conditions |
|
20
|
|
|
* - parenthesis |
|
21
|
|
|
* - order by one or more columns / expressions |
|
22
|
|
|
* - group by one or more columns / expressions |
|
23
|
|
|
* - limit and offset |
|
24
|
|
|
* - modifiers |
|
25
|
|
|
* |
|
26
|
|
|
* @package ORM |
|
27
|
|
|
* @author Thomas Flori <[email protected]> |
|
28
|
|
|
*/ |
|
29
|
|
|
class EntityFetcher extends QueryBuilder |
|
30
|
|
|
{ |
|
31
|
|
|
/** The entity class that we want to fetch |
|
32
|
|
|
* @var string|Entity */ |
|
33
|
|
|
protected $class; |
|
34
|
|
|
|
|
35
|
|
|
/** The result object from PDO |
|
36
|
|
|
* @var \PDOStatement */ |
|
37
|
|
|
protected $result; |
|
38
|
|
|
|
|
39
|
|
|
/** The query to execute (overwrites other settings) |
|
40
|
|
|
* @var string|QueryBuilderInterface */ |
|
41
|
|
|
protected $query; |
|
42
|
|
|
|
|
43
|
|
|
/** The class to alias mapping and vise versa |
|
44
|
|
|
* @var string[][] */ |
|
45
|
|
|
protected $classMapping = [ |
|
46
|
|
|
'byClass' => [], |
|
47
|
|
|
'byAlias' => [], |
|
48
|
|
|
]; |
|
49
|
|
|
|
|
50
|
|
|
/** @noinspection PhpMissingParentConstructorInspection */ |
|
51
|
|
|
/** |
|
52
|
|
|
* Constructor |
|
53
|
|
|
* |
|
54
|
|
|
* @param EntityManager $entityManager EntityManager where to store the fetched entities |
|
55
|
|
|
* @param Entity|string $class Class to fetch |
|
56
|
|
|
*/ |
|
57
|
|
|
public function __construct(EntityManager $entityManager, $class) |
|
58
|
|
|
{ |
|
59
|
67 |
|
$this->entityManager = $entityManager; |
|
60
|
|
|
$this->class = $class; |
|
61
|
67 |
|
|
|
62
|
67 |
|
$this->tableName = $entityManager->escapeIdentifier($class::getTableName()); |
|
63
|
|
|
$this->alias = 't0'; |
|
64
|
67 |
|
$this->columns = [ 't0.*' ]; |
|
65
|
67 |
|
$this->modifier = [ 'DISTINCT' ]; |
|
66
|
67 |
|
|
|
67
|
67 |
|
$this->classMapping['byClass'][$class] = 't0'; |
|
68
|
|
|
$this->classMapping['byAlias']['t0'] = $class; |
|
69
|
67 |
|
} |
|
70
|
67 |
|
|
|
71
|
67 |
|
/** @return self |
|
72
|
|
|
* @internal |
|
73
|
|
|
*/ |
|
74
|
|
|
public function columns(array $columns = null) |
|
75
|
|
|
{ |
|
76
|
1 |
|
return $this; |
|
77
|
|
|
} |
|
78
|
1 |
|
|
|
79
|
|
|
/** @return self |
|
80
|
|
|
* @internal |
|
81
|
|
|
*/ |
|
82
|
|
|
public function column($column, $args = [], $alias = '') |
|
83
|
|
|
{ |
|
84
|
1 |
|
return $this; |
|
85
|
|
|
} |
|
86
|
1 |
|
|
|
87
|
|
|
/** |
|
88
|
|
|
* Replaces questionmarks in $expression with $args |
|
89
|
|
|
* |
|
90
|
|
|
* Additionally this method replaces "ClassName::var" with "alias.col" and "alias.var" with "alias.col" if |
|
91
|
|
|
* $translateCols is true (default). |
|
92
|
|
|
* |
|
93
|
|
|
* @param string $expression Expression with placeholders |
|
94
|
|
|
* @param array|mixed $args Argument(s) to insert |
|
95
|
|
|
* @param bool $translateCols Whether or not column names should be translated |
|
96
|
|
|
* @return string |
|
97
|
|
|
*/ |
|
98
|
|
|
protected function convertPlaceholders($expression, $args, $translateCols = true) |
|
99
|
|
|
{ |
|
100
|
|
|
if ($translateCols) { |
|
101
|
|
|
$expression = preg_replace_callback( |
|
102
|
41 |
|
'/(?<b>^| |\()' . |
|
103
|
|
|
'((?<class>[A-Za-z_][A-Za-z0-9_\\\\]*)::|(?<alias>[A-Za-z_][A-Za-z0-9_]+)\.)?' . |
|
104
|
41 |
|
'(?<column>[A-Za-z_][A-Za-z0-9_]*)' . |
|
105
|
38 |
|
'(?<a>$| |,|\))/', |
|
106
|
|
|
function ($match) { |
|
107
|
|
|
if ($match['class']) { |
|
108
|
|
|
if (!isset($this->classMapping['byClass'][$match['class']])) { |
|
109
|
38 |
|
throw new NotJoined("Class " . $match['class'] . " not joined"); |
|
110
|
38 |
|
} |
|
111
|
38 |
|
$class = $match['class']; |
|
112
|
2 |
|
$alias = $this->classMapping['byClass'][$match['class']]; |
|
113
|
1 |
|
} elseif ($match['alias']) { |
|
114
|
|
|
if (!isset($this->classMapping['byAlias'][$match['alias']])) { |
|
115
|
1 |
|
return $match[0]; |
|
116
|
1 |
|
} |
|
117
|
36 |
|
$alias = $match['alias']; |
|
118
|
21 |
|
$class = $this->classMapping['byAlias'][$match['alias']]; |
|
119
|
2 |
|
} else { |
|
120
|
|
|
if ($match['column'] === strtoupper($match['column'])) { |
|
121
|
20 |
|
return $match['b'] . $match['column'] . $match['a']; |
|
122
|
20 |
|
} |
|
123
|
|
|
$class = $this->class; |
|
124
|
15 |
|
$alias = $this->alias; |
|
125
|
1 |
|
} |
|
126
|
|
|
|
|
127
|
15 |
|
/** @var Entity|string $class */ |
|
128
|
15 |
|
return $match['b'] . $this->entityManager->escapeIdentifier( |
|
129
|
|
|
$alias . '.' . $class::getColumnName($match['column']) |
|
130
|
|
|
) . $match['a']; |
|
131
|
|
|
}, |
|
132
|
36 |
|
$expression |
|
133
|
36 |
|
); |
|
134
|
36 |
|
} |
|
135
|
38 |
|
|
|
136
|
|
|
return parent::convertPlaceholders($expression, $args); |
|
137
|
|
|
} |
|
138
|
|
|
|
|
139
|
|
|
/** |
|
140
|
40 |
|
* Common implementation for *Join methods |
|
141
|
|
|
* |
|
142
|
|
|
* Additionally this method replaces class name with table name and forces an alias. |
|
143
|
|
|
* |
|
144
|
|
|
* @param string $join The join type (e. g. `LEFT JOIN`) |
|
145
|
|
|
* @param string $class Class to join |
|
146
|
|
|
* @param string $expression Expression to use in on clause or single column for USING |
|
147
|
|
|
* @param string $alias Alias for the table |
|
148
|
|
|
* @param array|mixed $args Arguments to use in $expression |
|
149
|
|
|
* @param bool $empty Create an empty join (without USING and ON) |
|
150
|
|
|
* @return EntityFetcher|ParenthesisInterface |
|
151
|
|
|
* @internal |
|
152
|
|
|
*/ |
|
153
|
|
|
protected function createJoin($join, $class, $expression, $alias, $args, $empty) |
|
154
|
|
|
{ |
|
155
|
|
|
if (class_exists($class)) { |
|
156
|
|
|
/** @var Entity|string $class */ |
|
157
|
|
|
$tableName = $this->entityManager->escapeIdentifier($class::getTableName()); |
|
158
|
|
|
$alias = $alias ?: 't' . count($this->classMapping['byAlias']); |
|
159
|
|
|
|
|
160
|
|
|
$this->classMapping['byClass'][$class] = $alias; |
|
161
|
20 |
|
$this->classMapping['byAlias'][$alias] = $class; |
|
162
|
|
|
} else { |
|
163
|
20 |
|
$tableName = $class; |
|
164
|
|
|
} |
|
165
|
14 |
|
|
|
166
|
14 |
|
return parent::createJoin($join, $tableName, $expression, $alias, $args, $empty); |
|
167
|
|
|
} |
|
168
|
14 |
|
|
|
169
|
14 |
|
/** |
|
170
|
|
|
* Create the join with $join type |
|
171
|
8 |
|
* |
|
172
|
|
|
* @param $join |
|
173
|
|
|
* @param $relation |
|
174
|
20 |
|
* @return $this |
|
175
|
|
|
*/ |
|
176
|
|
|
public function createRelatedJoin($join, $relation) |
|
177
|
|
|
{ |
|
178
|
|
|
if (strpos($relation, '.') !== false) { |
|
179
|
|
|
list($alias, $relation) = explode('.', $relation); |
|
180
|
|
|
$class = $this->classMapping['byAlias'][$alias]; |
|
181
|
|
|
} else { |
|
182
|
|
|
$class = $this->class; |
|
183
|
|
|
$alias = $this->alias; |
|
184
|
10 |
|
} |
|
185
|
|
|
|
|
186
|
10 |
|
call_user_func([ $class, 'getRelation' ], $relation)->addJoin($this, $join, $alias); |
|
187
|
1 |
|
return $this; |
|
188
|
1 |
|
} |
|
189
|
|
|
|
|
190
|
10 |
|
/** |
|
191
|
10 |
|
* Join $relation |
|
192
|
|
|
* |
|
193
|
|
|
* @param $relation |
|
194
|
10 |
|
* @return $this |
|
195
|
10 |
|
*/ |
|
196
|
|
|
public function joinRelated($relation) |
|
197
|
|
|
{ |
|
198
|
|
|
return $this->createRelatedJoin('join', $relation); |
|
199
|
|
|
} |
|
200
|
|
|
|
|
201
|
|
|
/** |
|
202
|
|
|
* Left outer join $relation |
|
203
|
|
|
* |
|
204
|
6 |
|
* @param $relation |
|
205
|
|
|
* @return $this |
|
206
|
6 |
|
*/ |
|
207
|
|
|
public function leftJoinRelated($relation) |
|
208
|
|
|
{ |
|
209
|
|
|
return $this->createRelatedJoin('leftJoin', $relation); |
|
210
|
|
|
} |
|
211
|
|
|
|
|
212
|
|
|
|
|
213
|
|
|
/** |
|
214
|
|
|
* Fetch one entity |
|
215
|
4 |
|
* |
|
216
|
|
|
* If there is no more entity in the result set it returns null. |
|
217
|
4 |
|
* |
|
218
|
|
|
* @return Entity |
|
219
|
|
|
*/ |
|
220
|
|
|
public function one() |
|
221
|
|
|
{ |
|
222
|
|
|
$result = $this->getStatement(); |
|
223
|
|
|
if (!$result) { |
|
224
|
|
|
return null; |
|
225
|
|
|
} |
|
226
|
|
|
|
|
227
|
|
|
$data = $result->fetch(\PDO::FETCH_ASSOC); |
|
228
|
|
|
|
|
229
|
|
|
if (!$data) { |
|
230
|
|
|
return null; |
|
231
|
15 |
|
} |
|
232
|
|
|
|
|
233
|
15 |
|
$c = $this->class; |
|
234
|
14 |
|
$newEntity = new $c($data, $this->entityManager, true); |
|
235
|
7 |
|
$entity = $this->entityManager->map($newEntity); |
|
236
|
|
|
|
|
237
|
|
|
if ($newEntity !== $entity) { |
|
238
|
7 |
|
$dirty = $entity->isDirty(); |
|
239
|
|
|
$entity->setOriginalData($data); |
|
240
|
7 |
|
if (!$dirty && $entity->isDirty()) { |
|
241
|
1 |
|
$entity->reset(); |
|
242
|
|
|
} |
|
243
|
|
|
} |
|
244
|
6 |
|
|
|
245
|
6 |
|
return $entity; |
|
246
|
6 |
|
} |
|
247
|
|
|
|
|
248
|
6 |
|
/** |
|
249
|
4 |
|
* Fetch an array of entities |
|
250
|
4 |
|
* |
|
251
|
4 |
|
* When no $limit is set it fetches all entities in result set. |
|
252
|
2 |
|
* |
|
253
|
|
|
* @param int $limit Maximum number of entities to fetch |
|
254
|
|
|
* @return Entity[] |
|
255
|
|
|
*/ |
|
256
|
6 |
|
public function all($limit = 0) |
|
257
|
|
|
{ |
|
258
|
|
|
$result = []; |
|
259
|
|
|
|
|
260
|
|
|
while ($entity = $this->one()) { |
|
261
|
|
|
$result[] = $entity; |
|
262
|
|
|
if ($limit && count($result) >= $limit) { |
|
263
|
|
|
break; |
|
264
|
|
|
} |
|
265
|
|
|
} |
|
266
|
|
|
|
|
267
|
|
|
return $result; |
|
268
|
|
|
} |
|
269
|
|
|
|
|
270
|
6 |
|
/** |
|
271
|
|
|
* Get the count of the resulting items |
|
272
|
6 |
|
* |
|
273
|
|
|
* @return int |
|
274
|
6 |
|
*/ |
|
275
|
5 |
|
public function count() |
|
276
|
5 |
|
{ |
|
277
|
1 |
|
// set the columns and reset after get query |
|
278
|
|
|
$this->columns = [ 'COUNT(DISTINCT t0.*)' ]; |
|
279
|
|
|
$this->modifier = []; |
|
280
|
|
|
$query = $this->getQuery(); |
|
281
|
6 |
|
$this->columns = [ 't0.*' ]; |
|
282
|
|
|
$this->modifier = [ 'DISTINCT' ]; |
|
283
|
|
|
|
|
284
|
|
|
return (int) $this->entityManager->getConnection()->query($query)->fetchColumn(); |
|
285
|
|
|
} |
|
286
|
|
|
|
|
287
|
|
|
/** |
|
288
|
|
|
* Query database and return result |
|
289
|
4 |
|
* |
|
290
|
|
|
* Queries the database with current query and returns the resulted PDOStatement. |
|
291
|
|
|
* |
|
292
|
4 |
|
* If query failed it returns false. It also stores this failed result and to change the query afterwards will not |
|
293
|
4 |
|
* change the result. |
|
294
|
4 |
|
* |
|
295
|
4 |
|
* @return \PDOStatement|bool |
|
296
|
4 |
|
*/ |
|
297
|
|
|
private function getStatement() |
|
298
|
4 |
|
{ |
|
299
|
|
|
if ($this->result === null) { |
|
300
|
|
|
$this->result = $this->entityManager->getConnection()->query($this->getQuery()); |
|
|
|
|
|
|
301
|
|
|
} |
|
302
|
|
|
return $this->result; |
|
303
|
|
|
} |
|
304
|
|
|
|
|
305
|
|
|
/** {@inheritdoc} */ |
|
306
|
|
|
public function getQuery() |
|
307
|
|
|
{ |
|
308
|
|
|
if ($this->query) { |
|
309
|
|
|
return $this->query instanceof QueryBuilderInterface ? $this->query->getQuery() : $this->query; |
|
310
|
|
|
} |
|
311
|
|
|
return parent::getQuery(); |
|
312
|
15 |
|
} |
|
313
|
|
|
|
|
314
|
15 |
|
/** |
|
315
|
15 |
|
* Set a raw query or use different QueryBuilder |
|
316
|
|
|
* |
|
317
|
14 |
|
* For easier use and against sql injection it allows question mark placeholders. |
|
318
|
|
|
* |
|
319
|
|
|
* @param string|QueryBuilderInterface $query Raw query string or a QueryBuilderInterface |
|
320
|
|
|
* @param array $args The arguments for placeholders |
|
321
|
48 |
|
* @return $this |
|
322
|
|
|
*/ |
|
323
|
48 |
|
public function setQuery($query, $args = null) |
|
324
|
4 |
|
{ |
|
325
|
|
|
if (!$query instanceof QueryBuilderInterface) { |
|
326
|
44 |
|
$query = $this->convertPlaceholders($query, $args, false); |
|
327
|
|
|
} |
|
328
|
|
|
|
|
329
|
|
|
$this->query = $query; |
|
330
|
|
|
return $this; |
|
331
|
|
|
} |
|
332
|
|
|
} |
|
333
|
|
|
|
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
$accountIdthat can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theidproperty of an instance of theAccountclass. 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.