1
|
|
|
<?php namespace Limoncello\Flute\Adapters; |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* Copyright 2015-2018 [email protected] |
5
|
|
|
* |
6
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
7
|
|
|
* you may not use this file except in compliance with the License. |
8
|
|
|
* You may obtain a copy of the License at |
9
|
|
|
* |
10
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0 |
11
|
|
|
* |
12
|
|
|
* Unless required by applicable law or agreed to in writing, software |
13
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS, |
14
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
15
|
|
|
* See the License for the specific language governing permissions and |
16
|
|
|
* limitations under the License. |
17
|
|
|
*/ |
18
|
|
|
|
19
|
|
|
use Closure; |
20
|
|
|
use DateTimeInterface; |
21
|
|
|
use Doctrine\DBAL\Connection; |
22
|
|
|
use Doctrine\DBAL\DBALException; |
23
|
|
|
use Doctrine\DBAL\Query\Expression\CompositeExpression; |
24
|
|
|
use Doctrine\DBAL\Query\QueryBuilder; |
25
|
|
|
use Doctrine\DBAL\Types\DateTimeType; |
26
|
|
|
use Doctrine\DBAL\Types\Type; |
27
|
|
|
use Limoncello\Contracts\Data\ModelSchemaInfoInterface; |
28
|
|
|
use Limoncello\Contracts\Data\RelationshipTypes; |
29
|
|
|
use Limoncello\Flute\Contracts\Http\Query\FilterParameterInterface; |
30
|
|
|
use Limoncello\Flute\Exceptions\InvalidArgumentException; |
31
|
|
|
use PDO; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @package Limoncello\Flute |
35
|
|
|
* |
36
|
|
|
* @SuppressWarnings(PHPMD.TooManyMethods) |
37
|
|
|
* @SuppressWarnings(PHPMD.TooManyPublicMethods) |
38
|
|
|
* @SuppressWarnings(PHPMD.CouplingBetweenObjects) |
39
|
|
|
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity) |
40
|
|
|
*/ |
41
|
|
|
class ModelQueryBuilder extends QueryBuilder |
42
|
|
|
{ |
43
|
|
|
/** |
44
|
|
|
* Condition joining method. |
45
|
|
|
*/ |
46
|
|
|
public const AND = 0; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* Condition joining method. |
50
|
|
|
*/ |
51
|
|
|
public const OR = self::AND + 1; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* @var string |
55
|
|
|
*/ |
56
|
|
|
private $modelClass; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* @var string |
60
|
|
|
*/ |
61
|
|
|
private $mainTableName; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* @var string |
65
|
|
|
*/ |
66
|
|
|
private $mainAlias; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* @var Closure |
70
|
|
|
*/ |
71
|
|
|
private $columnMapper; |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* @var ModelSchemaInfoInterface |
75
|
|
|
*/ |
76
|
|
|
private $modelSchemas; |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* @var int |
80
|
|
|
*/ |
81
|
|
|
private $aliasIdCounter = 0; |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* @var array |
85
|
|
|
*/ |
86
|
|
|
private $knownAliases = []; |
87
|
|
|
|
88
|
|
|
/** |
89
|
|
|
* @var Type|null |
90
|
|
|
*/ |
91
|
|
|
private $dateTimeType; |
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* @param Connection $connection |
95
|
|
|
* @param string $modelClass |
96
|
|
|
* @param ModelSchemaInfoInterface $modelSchemas |
97
|
|
|
* |
98
|
|
|
* @SuppressWarnings(PHPMD.StaticAccess) |
99
|
|
|
*/ |
100
|
66 |
|
public function __construct(Connection $connection, string $modelClass, ModelSchemaInfoInterface $modelSchemas) |
101
|
|
|
{ |
102
|
66 |
|
assert(!empty($modelClass)); |
103
|
|
|
|
104
|
66 |
|
parent::__construct($connection); |
105
|
|
|
|
106
|
66 |
|
$this->modelSchemas = $modelSchemas; |
107
|
66 |
|
$this->modelClass = $modelClass; |
108
|
|
|
|
109
|
66 |
|
$this->mainTableName = $this->getModelSchemas()->getTable($this->getModelClass()); |
110
|
66 |
|
$this->mainAlias = $this->createAlias($this->getTableName()); |
111
|
|
|
|
112
|
66 |
|
$this->setColumnToDatabaseMapper(Closure::fromCallable([$this, 'buildColumnName'])); |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
/** |
116
|
|
|
* @return string |
117
|
|
|
*/ |
118
|
66 |
|
public function getModelClass(): string |
119
|
|
|
{ |
120
|
66 |
|
return $this->modelClass; |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* @param string|null $tableAlias |
125
|
|
|
* @param string|null $modelClass |
126
|
|
|
* |
127
|
|
|
* @return array |
128
|
|
|
*/ |
129
|
|
|
public function getModelColumns(string $tableAlias = null, string $modelClass = null): array |
130
|
|
|
{ |
131
|
|
|
$modelClass = $modelClass ?? $this->getModelClass(); |
132
|
57 |
|
$tableAlias = $tableAlias ?? $this->getAlias(); |
133
|
|
|
|
134
|
57 |
|
$quotedColumns = []; |
135
|
5 |
|
|
136
|
5 |
|
$columnMapper = $this->getColumnToDatabaseMapper(); |
137
|
|
|
$selectedColumns = $this->getModelSchemas()->getAttributes($modelClass); |
138
|
52 |
|
foreach ($selectedColumns as $column) { |
139
|
52 |
|
$quotedColumns[] = call_user_func($columnMapper, $tableAlias, $column, $this); |
140
|
|
|
} |
141
|
|
|
|
142
|
57 |
|
$rawColumns = $this->getModelSchemas()->getRawAttributes($modelClass); |
143
|
57 |
|
foreach ($rawColumns as $columnOrCallable) { |
144
|
57 |
|
$quotedColumns[] = is_callable($columnOrCallable) === true ? |
145
|
57 |
|
call_user_func($columnOrCallable, $this) : $columnOrCallable; |
146
|
|
|
} |
147
|
57 |
|
|
148
|
1 |
|
return $quotedColumns; |
149
|
1 |
|
} |
150
|
|
|
|
151
|
|
|
/** |
152
|
57 |
|
* Select all fields associated with model. |
153
|
|
|
* |
154
|
57 |
|
* @param iterable|null $columns |
155
|
|
|
* |
156
|
|
|
* @return self |
157
|
|
|
* |
158
|
|
|
* @SuppressWarnings(PHPMD.ElseExpression) |
159
|
|
|
*/ |
160
|
14 |
|
public function selectModelColumns(iterable $columns = null): self |
161
|
|
|
{ |
162
|
|
|
if ($columns !== null) { |
163
|
14 |
|
$quotedColumns = []; |
164
|
14 |
|
foreach ($columns as $column) { |
165
|
|
|
$quotedColumns[] = $this->buildColumnName($this->getAlias(), $column); |
166
|
14 |
|
} |
167
|
|
|
} else { |
168
|
|
|
$quotedColumns = $this->getModelColumns(); |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
$this->select($quotedColumns); |
172
|
|
|
|
173
|
|
|
return $this; |
174
|
66 |
|
} |
175
|
|
|
|
176
|
66 |
|
/** |
177
|
|
|
* @return self |
178
|
66 |
|
*/ |
179
|
|
|
public function distinct(): self |
180
|
|
|
{ |
181
|
|
|
// emulate SELECT DISTINCT with grouping by primary key |
182
|
|
|
$primaryColumn = $this->getModelSchemas()->getPrimaryKey($this->getModelClass()); |
183
|
|
|
$this->addGroupBy($this->getQuotedMainAliasColumn($primaryColumn)); |
184
|
57 |
|
|
185
|
|
|
return $this; |
186
|
57 |
|
} |
187
|
57 |
|
|
188
|
57 |
|
/** |
189
|
|
|
* @param Closure $columnMapper |
190
|
|
|
* |
191
|
57 |
|
* @return self |
192
|
|
|
*/ |
193
|
|
|
public function setColumnToDatabaseMapper(Closure $columnMapper): self |
194
|
|
|
{ |
195
|
|
|
$this->columnMapper = $columnMapper; |
196
|
|
|
|
197
|
|
|
return $this; |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
/** |
201
|
|
|
* @return self |
202
|
|
|
*/ |
203
|
5 |
|
public function fromModelTable(): self |
204
|
|
|
{ |
205
|
5 |
|
$this->from( |
206
|
|
|
$this->quoteTableName($this->getTableName()), |
207
|
5 |
|
$this->quoteTableName($this->getAlias()) |
208
|
5 |
|
); |
209
|
5 |
|
|
210
|
|
|
return $this; |
211
|
5 |
|
} |
212
|
|
|
|
213
|
5 |
|
/** |
214
|
|
|
* @param iterable $attributes |
215
|
|
|
* |
216
|
|
|
* @return self |
217
|
|
|
* |
218
|
|
|
* @throws DBALException |
219
|
|
|
* |
220
|
|
|
* @SuppressWarnings(PHPMD.StaticAccess) |
221
|
|
|
*/ |
222
|
|
|
public function createModel(iterable $attributes): self |
223
|
|
|
{ |
224
|
|
|
$this->insert($this->quoteTableName($this->getTableName())); |
225
|
6 |
|
|
226
|
|
|
$valuesAsParams = []; |
227
|
6 |
|
foreach ($this->bindAttributes($this->getModelClass(), $attributes) as $quotedColumn => $parameterName) { |
228
|
|
|
$valuesAsParams[$quotedColumn] = $parameterName; |
229
|
6 |
|
} |
230
|
6 |
|
$this->values($valuesAsParams); |
231
|
|
|
|
232
|
|
|
return $this; |
233
|
6 |
|
} |
234
|
|
|
|
235
|
|
|
/** |
236
|
|
|
* @param iterable $attributes |
237
|
|
|
* |
238
|
|
|
* @return self |
239
|
|
|
* |
240
|
|
|
* @throws DBALException |
241
|
|
|
* |
242
|
|
|
* @SuppressWarnings(PHPMD.StaticAccess) |
243
|
|
|
*/ |
244
|
|
|
public function updateModels(iterable $attributes): self |
245
|
|
|
{ |
246
|
11 |
|
$this->update($this->quoteTableName($this->getTableName())); |
247
|
|
|
|
248
|
11 |
|
foreach ($this->bindAttributes($this->getModelClass(), $attributes) as $quotedColumn => $parameterName) { |
249
|
11 |
|
$this->set($quotedColumn, $parameterName); |
250
|
|
|
} |
251
|
11 |
|
|
252
|
11 |
|
return $this; |
253
|
|
|
} |
254
|
11 |
|
|
255
|
11 |
|
/** |
256
|
11 |
|
* @param string $modelClass |
257
|
11 |
|
* @param iterable $attributes |
258
|
|
|
* |
259
|
11 |
|
* @return iterable |
|
|
|
|
260
|
|
|
* |
261
|
|
|
* @SuppressWarnings(PHPMD.StaticAccess) |
262
|
|
|
* |
263
|
|
|
* @throws DBALException |
264
|
|
|
*/ |
265
|
|
|
public function bindAttributes(string $modelClass, iterable $attributes): iterable |
266
|
4 |
|
{ |
267
|
|
|
$dbPlatform = $this->getConnection()->getDatabasePlatform(); |
268
|
4 |
|
$types = $this->getModelSchemas()->getAttributeTypes($modelClass); |
269
|
|
|
|
270
|
4 |
|
foreach ($attributes as $column => $value) { |
271
|
|
|
assert(is_string($column) && $this->getModelSchemas()->hasAttributeType($this->getModelClass(), $column)); |
272
|
|
|
|
273
|
|
|
$quotedColumn = $this->quoteColumnName($column); |
274
|
|
|
$type = $this->getDbalType($types[$column]); |
275
|
|
|
$pdoValue = $type->convertToDatabaseValue($value, $dbPlatform); |
276
|
|
|
$parameterName = $this->createNamedParameter($pdoValue, $type->getBindingType()); |
277
|
|
|
|
278
|
|
|
yield $quotedColumn => $parameterName; |
279
|
|
|
} |
280
|
5 |
|
} |
281
|
|
|
|
282
|
|
|
/** |
283
|
|
|
* @return self |
284
|
|
|
*/ |
285
|
|
|
public function deleteModels(): self |
286
|
5 |
|
{ |
287
|
|
|
$this->delete($this->quoteTableName($this->getTableName())); |
288
|
|
|
|
289
|
5 |
|
return $this; |
290
|
5 |
|
} |
291
|
5 |
|
|
292
|
5 |
|
/** |
293
|
|
|
* @param string $relationshipName |
294
|
|
|
* @param string $identity |
295
|
5 |
|
* @param string $secondaryIdBindName |
296
|
|
|
* |
297
|
|
|
* @return self |
298
|
|
|
*/ |
299
|
|
|
public function prepareCreateInToManyRelationship( |
300
|
|
|
string $relationshipName, |
301
|
|
|
string $identity, |
302
|
|
|
string $secondaryIdBindName |
303
|
|
|
): self { |
304
|
|
|
list ($intermediateTable, $primaryKey, $secondaryKey) = |
305
|
|
|
$this->getModelSchemas()->getBelongsToManyRelationship($this->getModelClass(), $relationshipName); |
306
|
|
|
|
307
|
1 |
|
$this |
308
|
|
|
->insert($this->quoteTableName($intermediateTable)) |
309
|
|
|
->values([ |
310
|
|
|
$this->quoteColumnName($primaryKey) => $this->createNamedParameter($identity), |
311
|
|
|
$this->quoteColumnName($secondaryKey) => $secondaryIdBindName, |
312
|
|
|
]); |
313
|
1 |
|
|
314
|
|
|
return $this; |
315
|
|
|
} |
316
|
1 |
|
|
317
|
1 |
|
/** |
318
|
|
|
* @param string $relationshipName |
319
|
|
|
* @param string $identity |
320
|
1 |
|
* @param iterable $secondaryIds |
321
|
|
|
* |
322
|
1 |
|
* @return ModelQueryBuilder |
323
|
1 |
|
* |
324
|
|
|
* @throws DBALException |
325
|
1 |
|
*/ |
326
|
|
|
public function prepareDeleteInToManyRelationship( |
327
|
1 |
|
string $relationshipName, |
328
|
|
|
string $identity, |
329
|
|
|
iterable $secondaryIds |
330
|
|
|
): self { |
331
|
|
|
list ($intermediateTable, $primaryKey, $secondaryKey) = |
332
|
|
|
$this->getModelSchemas()->getBelongsToManyRelationship($this->getModelClass(), $relationshipName); |
333
|
|
|
|
334
|
|
|
$filters = [ |
335
|
|
|
$primaryKey => [FilterParameterInterface::OPERATION_EQUALS => [$identity]], |
336
|
|
|
$secondaryKey => [FilterParameterInterface::OPERATION_IN => $secondaryIds], |
337
|
|
|
]; |
338
|
2 |
|
|
339
|
|
|
$addWith = $this->expr()->andX(); |
340
|
|
|
$this |
341
|
2 |
|
->delete($this->quoteTableName($intermediateTable)) |
342
|
|
|
->applyFilters($addWith, $intermediateTable, $filters); |
|
|
|
|
343
|
2 |
|
|
344
|
2 |
|
$addWith->count() <= 0 ?: $this->andWhere($addWith); |
345
|
|
|
|
346
|
2 |
|
return $this; |
347
|
2 |
|
} |
348
|
|
|
|
349
|
2 |
|
/** |
350
|
|
|
* @param string $relationshipName |
351
|
2 |
|
* @param string $identity |
352
|
|
|
* |
353
|
|
|
* @return self |
354
|
|
|
* |
355
|
|
|
* @throws DBALException |
356
|
|
|
*/ |
357
|
|
|
public function clearToManyRelationship(string $relationshipName, string $identity): self |
358
|
|
|
{ |
359
|
|
|
list ($intermediateTable, $primaryKey) = |
360
|
|
|
$this->getModelSchemas()->getBelongsToManyRelationship($this->getModelClass(), $relationshipName); |
361
|
9 |
|
|
362
|
|
|
$filters = [$primaryKey => [FilterParameterInterface::OPERATION_EQUALS => [$identity]]]; |
363
|
9 |
|
$addWith = $this->expr()->andX(); |
364
|
9 |
|
$this |
365
|
9 |
|
->delete($this->quoteTableName($intermediateTable)) |
366
|
|
|
->applyFilters($addWith, $intermediateTable, $filters); |
|
|
|
|
367
|
9 |
|
|
368
|
|
|
$addWith->count() <= 0 ?: $this->andWhere($addWith); |
369
|
|
|
|
370
|
|
|
return $this; |
371
|
|
|
} |
372
|
|
|
|
373
|
|
|
/** |
374
|
|
|
* @param iterable $filters |
375
|
|
|
* |
376
|
|
|
* @return self |
377
|
1 |
|
* |
378
|
|
|
* @throws DBALException |
379
|
1 |
|
*/ |
380
|
1 |
|
public function addFiltersWithAndToTable(iterable $filters): self |
381
|
1 |
|
{ |
382
|
|
|
$addWith = $this->expr()->andX(); |
383
|
1 |
|
$this->applyFilters($addWith, $this->getTableName(), $filters); |
384
|
|
|
$addWith->count() <= 0 ?: $this->andWhere($addWith); |
385
|
|
|
|
386
|
|
|
return $this; |
387
|
|
|
} |
388
|
|
|
|
389
|
|
|
/** |
390
|
|
|
* @param iterable $filters |
391
|
|
|
* |
392
|
|
|
* @return self |
393
|
39 |
|
* |
394
|
|
|
* @throws DBALException |
395
|
39 |
|
*/ |
396
|
39 |
|
public function addFiltersWithOrToTable(iterable $filters): self |
397
|
38 |
|
{ |
398
|
|
|
$addWith = $this->expr()->orX(); |
399
|
38 |
|
$this->applyFilters($addWith, $this->getTableName(), $filters); |
400
|
|
|
$addWith->count() <= 0 ?: $this->andWhere($addWith); |
401
|
|
|
|
402
|
|
|
return $this; |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
/** |
406
|
|
|
* @param iterable $filters |
407
|
|
|
* |
408
|
|
|
* @return self |
409
|
2 |
|
* |
410
|
|
|
* @throws DBALException |
411
|
2 |
|
*/ |
412
|
2 |
|
public function addFiltersWithAndToAlias(iterable $filters): self |
413
|
2 |
|
{ |
414
|
|
|
$addWith = $this->expr()->andX(); |
415
|
2 |
|
$this->applyFilters($addWith, $this->getAlias(), $filters); |
416
|
|
|
$addWith->count() <= 0 ?: $this->andWhere($addWith); |
417
|
|
|
|
418
|
|
|
return $this; |
419
|
|
|
} |
420
|
|
|
|
421
|
|
|
/** |
422
|
|
|
* @param iterable $filters |
423
|
|
|
* |
424
|
|
|
* @return self |
425
|
|
|
* |
426
|
|
|
* @throws DBALException |
427
|
|
|
*/ |
428
|
|
|
public function addFiltersWithOrToAlias(iterable $filters): self |
429
|
|
|
{ |
430
|
|
|
$addWith = $this->expr()->orX(); |
431
|
|
|
$this->applyFilters($addWith, $this->getAlias(), $filters); |
432
|
|
|
$addWith->count() <= 0 ?: $this->andWhere($addWith); |
433
|
29 |
|
|
434
|
|
|
return $this; |
435
|
|
|
} |
436
|
|
|
|
437
|
|
|
/** |
438
|
|
|
* @param string $relationshipName |
439
|
|
|
* @param iterable|null $relationshipFilters |
440
|
29 |
|
* @param iterable|null $relationshipSorts |
441
|
|
|
* @param int $joinIndividuals |
442
|
29 |
|
* @param int $joinRelationship |
443
|
29 |
|
* |
444
|
29 |
|
* @return self |
445
|
|
|
* |
446
|
|
|
* @throws DBALException |
447
|
29 |
|
* |
448
|
29 |
|
* @SuppressWarnings(PHPMD.ElseExpression) |
449
|
|
|
* @SuppressWarnings(PHPMD.NPathComplexity) |
450
|
29 |
|
* @SuppressWarnings(PHPMD.CyclomaticComplexity) |
451
|
|
|
*/ |
452
|
29 |
|
public function addRelationshipFiltersAndSorts( |
453
|
28 |
|
string $relationshipName, |
454
|
|
|
?iterable $relationshipFilters, |
455
|
|
|
?iterable $relationshipSorts, |
456
|
|
|
int $joinIndividuals = self::AND, |
457
|
|
|
int $joinRelationship = self::AND |
458
|
|
|
): self { |
459
|
15 |
|
$targetAlias = null; |
460
|
15 |
|
|
461
|
|
|
if ($relationshipFilters !== null) { |
462
|
|
|
$isBelongsTo = $this->getModelSchemas() |
463
|
19 |
|
->getRelationshipType($this->getModelClass(), $relationshipName) === RelationshipTypes::BELONGS_TO; |
464
|
19 |
|
|
465
|
|
|
// it will have non-null value only in a `belongsTo` relationship |
466
|
|
|
$reversePk = $isBelongsTo === true ? |
467
|
28 |
|
$this->getModelSchemas()->getReversePrimaryKey($this->getModelClass(), $relationshipName)[0] : null; |
468
|
28 |
|
|
469
|
28 |
|
$addWith = $joinIndividuals === self::AND ? $this->expr()->andX() : $this->expr()->orX(); |
470
|
28 |
|
|
471
|
28 |
|
foreach ($relationshipFilters as $columnName => $operationsWithArgs) { |
472
|
|
|
if ($columnName === $reversePk) { |
473
|
28 |
|
// We are applying a filter to a primary key in `belongsTo` relationship |
474
|
|
|
// It could be replaced with a filter to a value in main table. Why might we need it? |
475
|
|
|
// Filter could be 'IS NULL' so joining a table will not work because there are no |
476
|
28 |
|
// related records with 'NULL` key. For plain values it will produce shorter SQL. |
477
|
28 |
|
$fkName = |
478
|
|
|
$this->getModelSchemas()->getForeignKey($this->getModelClass(), $relationshipName); |
479
|
|
|
$fullColumnName = $this->getQuotedMainAliasColumn($fkName); |
480
|
|
|
} else { |
481
|
|
|
// Will apply filters to a joined table. |
482
|
|
|
$targetAlias = $targetAlias ?: $this->createRelationshipAlias($relationshipName); |
483
|
29 |
|
$fullColumnName = $this->buildColumnName($targetAlias, $columnName); |
484
|
23 |
|
} |
485
|
|
|
|
486
|
8 |
|
foreach ($operationsWithArgs as $operation => $arguments) { |
487
|
|
|
assert( |
488
|
8 |
|
is_iterable($arguments) === true || is_array($arguments) === true, |
489
|
8 |
|
"Operation arguments are missing for `$columnName` column. " . |
490
|
8 |
|
'Use an empty array as an empty argument list.' |
491
|
|
|
); |
492
|
|
|
$addWith->add($this->createFilterExpression($fullColumnName, $operation, $arguments)); |
493
|
|
|
} |
494
|
29 |
|
|
495
|
|
|
if ($addWith->count() > 0) { |
496
|
|
|
$joinRelationship === self::AND ? $this->andWhere($addWith) : $this->orWhere($addWith); |
497
|
|
|
} |
498
|
|
|
} |
499
|
|
|
} |
500
|
|
|
|
501
|
|
|
if ($relationshipSorts !== null) { |
502
|
8 |
|
foreach ($relationshipSorts as $columnName => $isAsc) { |
503
|
|
|
// we join the table only once and only if we have at least one 'sort' or non-belongsToPK filter. |
504
|
8 |
|
$targetAlias = $targetAlias ?: $this->createRelationshipAlias($relationshipName); |
505
|
|
|
|
506
|
|
|
assert(is_string($columnName) === true && is_bool($isAsc) === true); |
507
|
|
|
$fullColumnName = $this->buildColumnName($targetAlias, $columnName); |
508
|
|
|
$this->addOrderBy($fullColumnName, $isAsc === true ? 'ASC' : 'DESC'); |
509
|
|
|
} |
510
|
|
|
} |
511
|
|
|
|
512
|
1 |
|
return $this; |
513
|
|
|
} |
514
|
1 |
|
|
515
|
|
|
/** |
516
|
|
|
* @param iterable $sortParameters |
517
|
|
|
* |
518
|
|
|
* @return self |
519
|
|
|
*/ |
520
|
|
|
public function addSorts(iterable $sortParameters): self |
521
|
|
|
{ |
522
|
58 |
|
return $this->applySorts($this->getAlias(), $sortParameters); |
523
|
|
|
} |
524
|
58 |
|
|
525
|
|
|
/** |
526
|
|
|
* @param string $column |
527
|
|
|
* |
528
|
|
|
* @return string |
529
|
|
|
*/ |
530
|
|
|
public function getQuotedMainTableColumn(string $column): string |
531
|
|
|
{ |
532
|
21 |
|
return $this->buildColumnName($this->getTableName(), $column); |
533
|
|
|
} |
534
|
21 |
|
|
535
|
|
|
/** |
536
|
21 |
|
* @param string $column |
537
|
|
|
* |
538
|
4 |
|
* @return string |
539
|
4 |
|
*/ |
540
|
4 |
|
public function getQuotedMainAliasColumn(string $column): string |
541
|
4 |
|
{ |
542
|
4 |
|
return $this->buildColumnName($this->getAlias(), $column); |
543
|
4 |
|
} |
544
|
|
|
|
545
|
4 |
|
/** |
546
|
|
|
* @param string $name |
547
|
18 |
|
* |
548
|
|
|
* @return string Table alias. |
549
|
13 |
|
*/ |
550
|
13 |
|
public function createRelationshipAlias(string $name): string |
551
|
13 |
|
{ |
552
|
13 |
|
$relationshipType = $this->getModelSchemas()->getRelationshipType($this->getModelClass(), $name); |
553
|
13 |
|
switch ($relationshipType) { |
554
|
13 |
|
case RelationshipTypes::BELONGS_TO: |
555
|
|
|
list($targetColumn, $targetTable) = |
556
|
13 |
|
$this->getModelSchemas()->getReversePrimaryKey($this->getModelClass(), $name); |
557
|
|
|
$targetAlias = $this->innerJoinOneTable( |
558
|
10 |
|
$this->getAlias(), |
559
|
|
|
$this->getModelSchemas()->getForeignKey($this->getModelClass(), $name), |
560
|
10 |
|
$targetTable, |
561
|
10 |
|
$targetColumn |
562
|
|
|
); |
563
|
10 |
|
break; |
564
|
|
|
|
565
|
10 |
|
case RelationshipTypes::HAS_MANY: |
566
|
|
|
list($targetColumn, $targetTable) = |
567
|
10 |
|
$this->getModelSchemas()->getReverseForeignKey($this->getModelClass(), $name); |
568
|
10 |
|
$targetAlias = $this->innerJoinOneTable( |
569
|
10 |
|
$this->getAlias(), |
570
|
10 |
|
$this->getModelSchemas()->getPrimaryKey($this->getModelClass()), |
571
|
10 |
|
$targetTable, |
572
|
10 |
|
$targetColumn |
573
|
10 |
|
); |
574
|
10 |
|
break; |
575
|
|
|
|
576
|
10 |
|
case RelationshipTypes::BELONGS_TO_MANY: |
577
|
|
|
default: |
578
|
|
|
assert($relationshipType === RelationshipTypes::BELONGS_TO_MANY); |
579
|
21 |
|
$primaryKey = $this->getModelSchemas()->getPrimaryKey($this->getModelClass()); |
580
|
|
|
list ($intermediateTable, $intermediatePk, $intermediateFk) = |
581
|
|
|
$this->getModelSchemas()->getBelongsToManyRelationship($this->getModelClass(), $name); |
582
|
|
|
list($targetPrimaryKey, $targetTable) = |
583
|
|
|
$this->getModelSchemas()->getReversePrimaryKey($this->getModelClass(), $name); |
584
|
|
|
|
585
|
58 |
|
$targetAlias = $this->innerJoinTwoSequentialTables( |
586
|
|
|
$this->getAlias(), |
587
|
58 |
|
$primaryKey, |
588
|
|
|
$intermediateTable, |
589
|
|
|
$intermediatePk, |
590
|
|
|
$intermediateFk, |
591
|
|
|
$targetTable, |
592
|
|
|
$targetPrimaryKey |
593
|
|
|
); |
594
|
|
|
break; |
595
|
|
|
} |
596
|
|
|
|
597
|
|
|
return $targetAlias; |
598
|
|
|
} |
599
|
|
|
|
600
|
46 |
|
/** |
601
|
|
|
* @return string |
602
|
46 |
|
*/ |
603
|
46 |
|
public function getAlias(): string |
604
|
46 |
|
{ |
605
|
46 |
|
return $this->mainAlias; |
606
|
|
|
} |
607
|
46 |
|
|
608
|
46 |
|
/** |
609
|
46 |
|
* @param CompositeExpression $expression |
610
|
46 |
|
* @param string $tableOrAlias |
611
|
46 |
|
* @param iterable $filters |
612
|
46 |
|
* |
613
|
|
|
* @return self |
614
|
46 |
|
* |
615
|
|
|
* @throws DBALException |
616
|
|
|
* @throws InvalidArgumentException |
617
|
|
|
*/ |
618
|
45 |
|
public function applyFilters(CompositeExpression $expression, string $tableOrAlias, iterable $filters): self |
619
|
|
|
{ |
620
|
|
|
foreach ($filters as $columnName => $operationsWithArgs) { |
621
|
|
|
assert( |
622
|
|
|
is_string($columnName) === true && empty($columnName) === false, |
623
|
|
|
"Haven't you forgotten to specify a column name in a relationship that joins `$tableOrAlias` table?" |
624
|
|
|
); |
625
|
|
|
$fullColumnName = $this->buildColumnName($tableOrAlias, $columnName); |
626
|
|
|
foreach ($operationsWithArgs as $operation => $arguments) { |
627
|
8 |
|
assert( |
628
|
|
|
is_iterable($arguments) === true || is_array($arguments) === true, |
629
|
8 |
|
"Operation arguments are missing for `$columnName` column. " . |
630
|
8 |
|
'Use an empty array as an empty argument list.' |
631
|
8 |
|
); |
632
|
8 |
|
$expression->add($this->createFilterExpression($fullColumnName, $operation, $arguments)); |
633
|
|
|
} |
634
|
|
|
} |
635
|
8 |
|
|
636
|
|
|
return $this; |
637
|
|
|
} |
638
|
|
|
|
639
|
|
|
/** |
640
|
|
|
* @param string $tableOrAlias |
641
|
|
|
* @param iterable $sorts |
642
|
|
|
* |
643
|
|
|
* @return self |
644
|
64 |
|
*/ |
645
|
|
|
public function applySorts(string $tableOrAlias, iterable $sorts): self |
646
|
64 |
|
{ |
647
|
|
|
foreach ($sorts as $columnName => $isAsc) { |
648
|
|
|
assert(is_string($columnName) === true && is_bool($isAsc) === true); |
649
|
|
|
$fullColumnName = $this->buildColumnName($tableOrAlias, $columnName); |
650
|
|
|
$this->addOrderBy($fullColumnName, $isAsc === true ? 'ASC' : 'DESC'); |
651
|
|
|
} |
652
|
|
|
|
653
|
|
|
return $this; |
654
|
|
|
} |
655
|
|
|
|
656
|
58 |
|
/** |
657
|
|
|
* @param string $table |
658
|
58 |
|
* @param string $column |
659
|
|
|
* |
660
|
58 |
|
* @return string |
661
|
|
|
*/ |
662
|
|
|
public function buildColumnName(string $table, string $column): string |
663
|
|
|
{ |
664
|
|
|
return $this->quoteTableName($table) . '.' . $this->quoteColumnName($column); |
665
|
|
|
} |
666
|
|
|
|
667
|
|
|
/** |
668
|
|
|
* @param $value |
669
|
|
|
* |
670
|
18 |
|
* @return string |
671
|
|
|
* |
672
|
18 |
|
* @throws DBALException |
673
|
|
|
*/ |
674
|
18 |
|
public function createSingleValueNamedParameter($value): string |
675
|
18 |
|
{ |
676
|
|
|
$paramName = $this->createNamedParameter($this->getPdoValue($value), $this->getPdoType($value)); |
677
|
|
|
|
678
|
18 |
|
return $paramName; |
679
|
|
|
} |
680
|
|
|
|
681
|
|
|
/** |
682
|
|
|
* @param iterable $values |
683
|
|
|
* |
684
|
|
|
* @return array |
685
|
|
|
* |
686
|
66 |
|
* @throws DBALException |
687
|
|
|
*/ |
688
|
66 |
|
public function createArrayValuesNamedParameter(iterable $values): array |
689
|
66 |
|
{ |
690
|
|
|
$names = []; |
691
|
66 |
|
|
692
|
|
|
foreach ($values as $value) { |
693
|
|
|
$names[] = $this->createSingleValueNamedParameter($value); |
694
|
|
|
} |
695
|
|
|
|
696
|
|
|
return $names; |
697
|
|
|
} |
698
|
|
|
|
699
|
|
|
/** |
700
|
|
|
* @param string $tableName |
701
|
|
|
* |
702
|
21 |
|
* @return string |
703
|
|
|
*/ |
704
|
|
|
public function createAlias(string $tableName): string |
705
|
|
|
{ |
706
|
|
|
$alias = $tableName . (++$this->aliasIdCounter); |
707
|
|
|
$this->knownAliases[$tableName] = $alias; |
708
|
21 |
|
|
709
|
21 |
|
return $alias; |
710
|
21 |
|
} |
711
|
|
|
|
712
|
21 |
|
/** |
713
|
21 |
|
* @param string $fromAlias |
714
|
21 |
|
* @param string $fromColumn |
715
|
21 |
|
* @param string $targetTable |
716
|
21 |
|
* @param string $targetColumn |
717
|
|
|
* |
718
|
|
|
* @return string |
719
|
21 |
|
*/ |
720
|
|
|
public function innerJoinOneTable( |
721
|
|
|
string $fromAlias, |
722
|
|
|
string $fromColumn, |
723
|
|
|
string $targetTable, |
724
|
|
|
string $targetColumn |
725
|
|
|
): string { |
726
|
|
|
$targetAlias = $this->createAlias($targetTable); |
727
|
|
|
$joinCondition = $this->buildColumnName($fromAlias, $fromColumn) . '=' . |
728
|
|
|
$this->buildColumnName($targetAlias, $targetColumn); |
729
|
|
|
|
730
|
|
|
$this->innerJoin( |
731
|
|
|
$this->quoteTableName($fromAlias), |
732
|
|
|
$this->quoteTableName($targetTable), |
733
|
10 |
|
$this->quoteTableName($targetAlias), |
734
|
|
|
$joinCondition |
735
|
|
|
); |
736
|
|
|
|
737
|
|
|
return $targetAlias; |
738
|
|
|
} |
739
|
|
|
|
740
|
|
|
/** |
741
|
|
|
* @param string $fromAlias |
742
|
10 |
|
* @param string $fromColumn |
743
|
10 |
|
* @param string $intTable |
744
|
|
|
* @param string $intToFromColumn |
745
|
10 |
|
* @param string $intToTargetColumn |
746
|
|
|
* @param string $targetTable |
747
|
|
|
* @param string $targetColumn |
748
|
|
|
* |
749
|
|
|
* @return string |
750
|
|
|
*/ |
751
|
|
|
public function innerJoinTwoSequentialTables( |
752
|
|
|
string $fromAlias, |
753
|
|
|
string $fromColumn, |
754
|
|
|
string $intTable, |
755
|
|
|
string $intToFromColumn, |
756
|
|
|
string $intToTargetColumn, |
757
|
11 |
|
string $targetTable, |
758
|
|
|
string $targetColumn |
759
|
11 |
|
): string { |
760
|
11 |
|
$intAlias = $this->innerJoinOneTable($fromAlias, $fromColumn, $intTable, $intToFromColumn); |
761
|
|
|
$targetAlias = $this->innerJoinOneTable($intAlias, $intToTargetColumn, $targetTable, $targetColumn); |
762
|
11 |
|
|
763
|
|
|
return $targetAlias; |
764
|
|
|
} |
765
|
|
|
|
766
|
|
|
/** |
767
|
|
|
* @param string $name |
768
|
66 |
|
* |
769
|
|
|
* @return Type |
770
|
66 |
|
* |
771
|
|
|
* @throws DBALException |
772
|
|
|
* |
773
|
|
|
* @SuppressWarnings(PHPMD.StaticAccess) |
774
|
|
|
*/ |
775
|
|
|
protected function getDbalType(string $name): Type |
776
|
66 |
|
{ |
777
|
|
|
assert(Type::hasType($name), "Type `$name` either do not exist or registered."); |
778
|
66 |
|
$type = Type::getType($name); |
779
|
|
|
|
780
|
|
|
return $type; |
781
|
|
|
} |
782
|
|
|
|
783
|
|
|
/** |
784
|
|
|
* @return string |
785
|
|
|
*/ |
786
|
66 |
|
private function getTableName(): string |
787
|
|
|
{ |
788
|
66 |
|
return $this->mainTableName; |
789
|
|
|
} |
790
|
|
|
|
791
|
|
|
/** |
792
|
|
|
* @return ModelSchemaInfoInterface |
793
|
|
|
*/ |
794
|
|
|
private function getModelSchemas(): ModelSchemaInfoInterface |
795
|
|
|
{ |
796
|
66 |
|
return $this->modelSchemas; |
797
|
|
|
} |
798
|
66 |
|
|
799
|
|
|
/** |
800
|
|
|
* @param string $tableName |
801
|
|
|
* |
802
|
|
|
* @return string |
803
|
|
|
*/ |
804
|
|
|
private function quoteTableName(string $tableName): string |
805
|
|
|
{ |
806
|
|
|
return $this->getConnection()->quoteIdentifier($tableName); |
807
|
|
|
} |
808
|
|
|
|
809
|
|
|
/** |
810
|
|
|
* @param string $columnName |
811
|
|
|
* |
812
|
|
|
* @return string |
813
|
|
|
*/ |
814
|
59 |
|
private function quoteColumnName(string $columnName): string |
815
|
|
|
{ |
816
|
|
|
return $this->getConnection()->quoteIdentifier($columnName); |
817
|
59 |
|
} |
818
|
48 |
|
|
819
|
47 |
|
/** |
820
|
47 |
|
* @param string $fullColumnName |
821
|
30 |
|
* @param int $operation |
822
|
1 |
|
* @param iterable $arguments |
823
|
1 |
|
* |
824
|
1 |
|
* @return string |
825
|
30 |
|
* |
826
|
6 |
|
* @throws DBALException |
827
|
6 |
|
* @throws InvalidArgumentException |
828
|
6 |
|
* |
829
|
30 |
|
* @SuppressWarnings(PHPMD.StaticAccess) |
830
|
7 |
|
* @SuppressWarnings(PHPMD.CyclomaticComplexity) |
831
|
7 |
|
*/ |
832
|
7 |
|
private function createFilterExpression(string $fullColumnName, int $operation, iterable $arguments): string |
833
|
29 |
|
{ |
834
|
2 |
|
switch ($operation) { |
835
|
2 |
|
case FilterParameterInterface::OPERATION_EQUALS: |
836
|
2 |
|
$parameter = $this->createSingleValueNamedParameter($this->firstValue($arguments)); |
837
|
28 |
|
$expression = $this->expr()->eq($fullColumnName, $parameter); |
838
|
6 |
|
break; |
839
|
6 |
|
case FilterParameterInterface::OPERATION_NOT_EQUALS: |
840
|
6 |
|
$parameter = $this->createSingleValueNamedParameter($this->firstValue($arguments)); |
841
|
23 |
|
$expression = $this->expr()->neq($fullColumnName, $parameter); |
842
|
9 |
|
break; |
843
|
9 |
|
case FilterParameterInterface::OPERATION_LESS_THAN: |
844
|
9 |
|
$parameter = $this->createSingleValueNamedParameter($this->firstValue($arguments)); |
845
|
18 |
|
$expression = $this->expr()->lt($fullColumnName, $parameter); |
846
|
1 |
|
break; |
847
|
1 |
|
case FilterParameterInterface::OPERATION_LESS_OR_EQUALS: |
848
|
1 |
|
$parameter = $this->createSingleValueNamedParameter($this->firstValue($arguments)); |
849
|
18 |
|
$expression = $this->expr()->lte($fullColumnName, $parameter); |
850
|
18 |
|
break; |
851
|
18 |
|
case FilterParameterInterface::OPERATION_GREATER_THAN: |
852
|
18 |
|
$parameter = $this->createSingleValueNamedParameter($this->firstValue($arguments)); |
853
|
1 |
|
$expression = $this->expr()->gt($fullColumnName, $parameter); |
854
|
1 |
|
break; |
855
|
1 |
|
case FilterParameterInterface::OPERATION_GREATER_OR_EQUALS: |
856
|
1 |
|
$parameter = $this->createSingleValueNamedParameter($this->firstValue($arguments)); |
857
|
1 |
|
$expression = $this->expr()->gte($fullColumnName, $parameter); |
858
|
1 |
|
break; |
859
|
1 |
|
case FilterParameterInterface::OPERATION_LIKE: |
860
|
1 |
|
$parameter = $this->createSingleValueNamedParameter($this->firstValue($arguments)); |
861
|
|
|
$expression = $this->expr()->like($fullColumnName, $parameter); |
862
|
1 |
|
break; |
863
|
1 |
|
case FilterParameterInterface::OPERATION_NOT_LIKE: |
864
|
1 |
|
$parameter = $this->createSingleValueNamedParameter($this->firstValue($arguments)); |
865
|
|
|
$expression = $this->expr()->notLike($fullColumnName, $parameter); |
866
|
|
|
break; |
867
|
58 |
|
case FilterParameterInterface::OPERATION_IN: |
868
|
|
|
$parameters = $this->createArrayValuesNamedParameter($arguments); |
869
|
|
|
$expression = $this->expr()->in($fullColumnName, $parameters); |
870
|
|
|
break; |
871
|
|
|
case FilterParameterInterface::OPERATION_NOT_IN: |
872
|
|
|
$parameters = $this->createArrayValuesNamedParameter($arguments); |
873
|
|
|
$expression = $this->expr()->notIn($fullColumnName, $parameters); |
874
|
|
|
break; |
875
|
|
|
case FilterParameterInterface::OPERATION_IS_NULL: |
876
|
|
|
$expression = $this->expr()->isNull($fullColumnName); |
877
|
55 |
|
break; |
878
|
|
|
case FilterParameterInterface::OPERATION_IS_NOT_NULL: |
879
|
55 |
|
default: |
880
|
54 |
|
assert($operation === FilterParameterInterface::OPERATION_IS_NOT_NULL); |
881
|
|
|
$expression = $this->expr()->isNotNull($fullColumnName); |
882
|
|
|
break; |
883
|
|
|
} |
884
|
1 |
|
|
885
|
|
|
return $expression; |
886
|
|
|
} |
887
|
|
|
|
888
|
|
|
/** |
889
|
|
|
* @param iterable $arguments |
890
|
57 |
|
* |
891
|
|
|
* @return mixed |
892
|
57 |
|
* |
893
|
|
|
* @throws InvalidArgumentException |
894
|
|
|
*/ |
895
|
|
|
private function firstValue(iterable $arguments) |
896
|
|
|
{ |
897
|
|
|
foreach ($arguments as $argument) { |
898
|
|
|
return $argument; |
899
|
|
|
} |
900
|
|
|
|
901
|
|
|
// arguments are empty |
902
|
58 |
|
throw new InvalidArgumentException(); |
903
|
|
|
} |
904
|
58 |
|
|
905
|
|
|
/** |
906
|
|
|
* @return Closure |
907
|
|
|
*/ |
908
|
|
|
private function getColumnToDatabaseMapper(): Closure |
909
|
|
|
{ |
910
|
|
|
return $this->columnMapper; |
911
|
|
|
} |
912
|
|
|
|
913
|
|
|
/** |
914
|
1 |
|
* @param mixed $value |
915
|
|
|
* |
916
|
1 |
|
* @return mixed |
917
|
1 |
|
* |
918
|
1 |
|
* @throws DBALException |
919
|
|
|
*/ |
920
|
|
|
private function getPdoValue($value) |
921
|
|
|
{ |
922
|
|
|
return $value instanceof DateTimeInterface ? $this->convertDataTimeToDatabaseFormat($value) : $value; |
923
|
|
|
} |
924
|
|
|
|
925
|
|
|
/** |
926
|
|
|
* @param DateTimeInterface $dateTime |
927
|
|
|
* |
928
|
|
|
* @return string |
929
|
58 |
|
* |
930
|
|
|
* @throws DBALException |
931
|
58 |
|
*/ |
932
|
46 |
|
private function convertDataTimeToDatabaseFormat(DateTimeInterface $dateTime): string |
933
|
27 |
|
{ |
934
|
1 |
|
return $this->getDateTimeType()->convertToDatabaseValue( |
935
|
26 |
|
$dateTime, |
936
|
1 |
|
$this->getConnection()->getDatabasePlatform() |
937
|
|
|
); |
938
|
25 |
|
} |
939
|
25 |
|
|
940
|
|
|
/** |
941
|
25 |
|
* @param mixed $value |
942
|
|
|
* |
943
|
25 |
|
* @return int |
944
|
25 |
|
* |
945
|
|
|
* @SuppressWarnings(PHPMD.ElseExpression) |
946
|
|
|
*/ |
947
|
58 |
|
private function getPdoType($value): int |
948
|
|
|
{ |
949
|
|
|
if (is_int($value) === true) { |
950
|
|
|
$type = PDO::PARAM_INT; |
951
|
|
|
} elseif (is_bool($value)) { |
952
|
|
|
$type = PDO::PARAM_BOOL; |
953
|
|
|
} elseif ($value instanceof DateTimeInterface) { |
954
|
|
|
$type = PDO::PARAM_STR; |
955
|
|
|
} else { |
956
|
|
|
assert( |
957
|
1 |
|
$value !== null, |
958
|
|
|
'It seems you are trying to use `null` with =, >, <, or etc operator. ' . |
959
|
1 |
|
'Use `is null` or `not null` instead.' |
960
|
1 |
|
); |
961
|
|
|
assert(is_string($value), "Only strings, booleans and integers are supported."); |
962
|
|
|
$type = PDO::PARAM_STR; |
963
|
1 |
|
} |
964
|
|
|
|
965
|
|
|
return $type; |
966
|
|
|
} |
967
|
|
|
|
968
|
|
|
/** |
969
|
|
|
* @return Type |
970
|
|
|
* |
971
|
|
|
* @throws DBALException |
972
|
|
|
* |
973
|
|
|
* @SuppressWarnings(PHPMD.StaticAccess) |
974
|
|
|
*/ |
975
|
|
|
private function getDateTimeType(): Type |
976
|
|
|
{ |
977
|
|
|
if ($this->dateTimeType === null) { |
978
|
|
|
$this->dateTimeType = Type::getType(DateTimeType::DATETIME); |
979
|
|
|
} |
980
|
|
|
|
981
|
|
|
return $this->dateTimeType; |
982
|
|
|
} |
983
|
|
|
} |
984
|
|
|
|
This check compares the return type specified in the
@return
annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.