1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Doctrine\ORM\Persisters\Entity; |
6
|
|
|
|
7
|
|
|
use Doctrine\Common\Collections\Criteria; |
8
|
|
|
use Doctrine\Common\Collections\Expr\Comparison; |
9
|
|
|
use Doctrine\DBAL\Connection; |
10
|
|
|
use Doctrine\DBAL\LockMode; |
11
|
|
|
use Doctrine\DBAL\Types\Type; |
12
|
|
|
use Doctrine\ORM\EntityManagerInterface; |
13
|
|
|
use Doctrine\ORM\Mapping\AssociationMetadata; |
14
|
|
|
use Doctrine\ORM\Mapping\ClassMetadata; |
15
|
|
|
use Doctrine\ORM\Mapping\FetchMode; |
16
|
|
|
use Doctrine\ORM\Mapping\FieldMetadata; |
17
|
|
|
use Doctrine\ORM\Mapping\GeneratorType; |
18
|
|
|
use Doctrine\ORM\Mapping\InheritanceType; |
19
|
|
|
use Doctrine\ORM\Mapping\JoinColumnMetadata; |
20
|
|
|
use Doctrine\ORM\Mapping\LocalColumnMetadata; |
21
|
|
|
use Doctrine\ORM\Mapping\ManyToManyAssociationMetadata; |
22
|
|
|
use Doctrine\ORM\Mapping\MappingException; |
23
|
|
|
use Doctrine\ORM\Mapping\OneToManyAssociationMetadata; |
24
|
|
|
use Doctrine\ORM\Mapping\ToManyAssociationMetadata; |
25
|
|
|
use Doctrine\ORM\Mapping\ToOneAssociationMetadata; |
26
|
|
|
use Doctrine\ORM\Mapping\VersionFieldMetadata; |
27
|
|
|
use Doctrine\ORM\OptimisticLockException; |
28
|
|
|
use Doctrine\ORM\ORMException; |
29
|
|
|
use Doctrine\ORM\PersistentCollection; |
30
|
|
|
use Doctrine\ORM\Persisters\SqlExpressionVisitor; |
31
|
|
|
use Doctrine\ORM\Persisters\SqlValueVisitor; |
32
|
|
|
use Doctrine\ORM\Query; |
33
|
|
|
use Doctrine\ORM\UnitOfWork; |
34
|
|
|
use Doctrine\ORM\Utility\PersisterHelper; |
35
|
|
|
use Doctrine\ORM\Utility\StaticClassNameConverter; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* A BasicEntityPersister maps an entity to a single table in a relational database. |
39
|
|
|
* |
40
|
|
|
* A persister is always responsible for a single entity type. |
41
|
|
|
* |
42
|
|
|
* EntityPersisters are used during a UnitOfWork to apply any changes to the persistent |
43
|
|
|
* state of entities onto a relational database when the UnitOfWork is committed, |
44
|
|
|
* as well as for basic querying of entities and their associations (not DQL). |
45
|
|
|
* |
46
|
|
|
* The persisting operations that are invoked during a commit of a UnitOfWork to |
47
|
|
|
* persist the persistent entity state are: |
48
|
|
|
* |
49
|
|
|
* - {@link insert} : To insert the persistent state of an entity. |
50
|
|
|
* - {@link update} : To update the persistent state of an entity. |
51
|
|
|
* - {@link delete} : To delete the persistent state of an entity. |
52
|
|
|
* |
53
|
|
|
* As can be seen from the above list, insertions are batched and executed all at once |
54
|
|
|
* for increased efficiency. |
55
|
|
|
* |
56
|
|
|
* The querying operations invoked during a UnitOfWork, either through direct find |
57
|
|
|
* requests or lazy-loading, are the following: |
58
|
|
|
* |
59
|
|
|
* - {@link load} : Loads (the state of) a single, managed entity. |
60
|
|
|
* - {@link loadAll} : Loads multiple, managed entities. |
61
|
|
|
* - {@link loadToOneEntity} : Loads a one/many-to-one entity association (lazy-loading). |
62
|
|
|
* - {@link loadOneToManyCollection} : Loads a one-to-many entity association (lazy-loading). |
63
|
|
|
* - {@link loadManyToManyCollection} : Loads a many-to-many entity association (lazy-loading). |
64
|
|
|
* |
65
|
|
|
* The BasicEntityPersister implementation provides the default behavior for |
66
|
|
|
* persisting and querying entities that are mapped to a single database table. |
67
|
|
|
* |
68
|
|
|
* Subclasses can be created to provide custom persisting and querying strategies, |
69
|
|
|
* i.e. spanning multiple tables. |
70
|
|
|
*/ |
71
|
|
|
class BasicEntityPersister implements EntityPersister |
72
|
|
|
{ |
73
|
|
|
/** |
74
|
|
|
* @var string[] |
75
|
|
|
*/ |
76
|
|
|
private static $comparisonMap = [ |
77
|
|
|
Comparison::EQ => '= %s', |
78
|
|
|
Comparison::IS => '= %s', |
79
|
|
|
Comparison::NEQ => '!= %s', |
80
|
|
|
Comparison::GT => '> %s', |
81
|
|
|
Comparison::GTE => '>= %s', |
82
|
|
|
Comparison::LT => '< %s', |
83
|
|
|
Comparison::LTE => '<= %s', |
84
|
|
|
Comparison::IN => 'IN (%s)', |
85
|
|
|
Comparison::NIN => 'NOT IN (%s)', |
86
|
|
|
Comparison::CONTAINS => 'LIKE %s', |
87
|
|
|
Comparison::STARTS_WITH => 'LIKE %s', |
88
|
|
|
Comparison::ENDS_WITH => 'LIKE %s', |
89
|
|
|
]; |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* Metadata object that describes the mapping of the mapped entity class. |
93
|
|
|
* |
94
|
|
|
* @var \Doctrine\ORM\Mapping\ClassMetadata |
95
|
|
|
*/ |
96
|
|
|
protected $class; |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* The underlying DBAL Connection of the used EntityManager. |
100
|
|
|
* |
101
|
|
|
* @var \Doctrine\DBAL\Connection $conn |
102
|
|
|
*/ |
103
|
|
|
protected $conn; |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* The database platform. |
107
|
|
|
* |
108
|
|
|
* @var \Doctrine\DBAL\Platforms\AbstractPlatform |
109
|
|
|
*/ |
110
|
|
|
protected $platform; |
111
|
|
|
|
112
|
|
|
/** |
113
|
|
|
* The EntityManager instance. |
114
|
|
|
* |
115
|
|
|
* @var EntityManagerInterface |
116
|
|
|
*/ |
117
|
|
|
protected $em; |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* The map of column names to DBAL columns used when INSERTing or UPDATEing an entity. |
121
|
|
|
* |
122
|
|
|
* @var array<ColumnMetadata> |
123
|
|
|
* |
124
|
|
|
* @see prepareInsertData($entity) |
125
|
|
|
* @see prepareUpdateData($entity) |
126
|
|
|
*/ |
127
|
|
|
protected $columns = []; |
128
|
|
|
|
129
|
|
|
/** |
130
|
|
|
* The INSERT SQL statement used for entities handled by this persister. |
131
|
|
|
* This SQL is only generated once per request, if at all. |
132
|
|
|
* |
133
|
|
|
* @var string |
134
|
|
|
*/ |
135
|
|
|
private $insertSql; |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* @var CachedPersisterContext |
139
|
|
|
*/ |
140
|
|
|
protected $currentPersisterContext; |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* @var CachedPersisterContext |
144
|
|
|
*/ |
145
|
|
|
private $limitsHandlingContext; |
146
|
|
|
|
147
|
|
|
/** |
148
|
|
|
* @var CachedPersisterContext |
149
|
|
|
*/ |
150
|
|
|
private $noLimitsContext; |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* Initializes a new <tt>BasicEntityPersister</tt> that uses the given EntityManager |
154
|
|
|
* and persists instances of the class described by the given ClassMetadata descriptor. |
155
|
|
|
*/ |
156
|
1040 |
|
public function __construct(EntityManagerInterface $em, ClassMetadata $class) |
157
|
|
|
{ |
158
|
1040 |
|
$this->em = $em; |
159
|
1040 |
|
$this->class = $class; |
160
|
1040 |
|
$this->conn = $em->getConnection(); |
161
|
1040 |
|
$this->platform = $this->conn->getDatabasePlatform(); |
162
|
1040 |
|
$this->noLimitsContext = $this->currentPersisterContext = new CachedPersisterContext( |
163
|
1040 |
|
$class, |
164
|
1040 |
|
new Query\ResultSetMapping(), |
165
|
1040 |
|
false |
166
|
|
|
); |
167
|
1040 |
|
$this->limitsHandlingContext = new CachedPersisterContext( |
168
|
1040 |
|
$class, |
169
|
1040 |
|
new Query\ResultSetMapping(), |
170
|
1040 |
|
true |
171
|
|
|
); |
172
|
1040 |
|
} |
173
|
|
|
|
174
|
|
|
/** |
175
|
|
|
* {@inheritdoc} |
176
|
|
|
*/ |
177
|
15 |
|
public function getClassMetadata() |
178
|
|
|
{ |
179
|
15 |
|
return $this->class; |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* {@inheritdoc} |
184
|
|
|
*/ |
185
|
11 |
|
public function getResultSetMapping() |
186
|
|
|
{ |
187
|
11 |
|
return $this->currentPersisterContext->rsm; |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* {@inheritdoc} |
192
|
|
|
*/ |
193
|
972 |
|
public function getIdentifier($entity) : array |
194
|
|
|
{ |
195
|
972 |
|
$id = []; |
196
|
|
|
|
197
|
972 |
|
foreach ($this->class->getIdentifier() as $fieldName) { |
198
|
972 |
|
$property = $this->class->getProperty($fieldName); |
199
|
972 |
|
$value = $property->getValue($entity); |
200
|
|
|
|
201
|
972 |
|
if ($value !== null) { |
202
|
972 |
|
$id[$fieldName] = $value; |
203
|
|
|
} |
204
|
|
|
} |
205
|
|
|
|
206
|
972 |
|
return $id; |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
/** |
210
|
|
|
* Populates the entity identifier of an entity. |
211
|
|
|
* |
212
|
|
|
* @param object $entity |
213
|
|
|
* @param mixed[] $id |
214
|
|
|
*/ |
215
|
198 |
|
public function setIdentifier($entity, array $id) : void |
216
|
|
|
{ |
217
|
198 |
|
foreach ($id as $idField => $idValue) { |
218
|
198 |
|
$property = $this->class->getProperty($idField); |
219
|
|
|
|
220
|
198 |
|
$property->setValue($entity, $idValue); |
221
|
|
|
} |
222
|
198 |
|
} |
223
|
|
|
|
224
|
|
|
/** |
225
|
|
|
* {@inheritdoc} |
226
|
|
|
*/ |
227
|
846 |
|
public function insert($entity) |
228
|
|
|
{ |
229
|
846 |
|
$stmt = $this->conn->prepare($this->getInsertSQL()); |
230
|
846 |
|
$tableName = $this->class->getTableName(); |
231
|
846 |
|
$insertData = $this->prepareInsertData($entity); |
232
|
846 |
|
$generationPlan = $this->class->getValueGenerationPlan(); |
233
|
|
|
|
234
|
846 |
|
if (isset($insertData[$tableName])) { |
235
|
826 |
|
$paramIndex = 1; |
236
|
|
|
|
237
|
826 |
|
foreach ($insertData[$tableName] as $columnName => $value) { |
238
|
826 |
|
$type = $this->columns[$columnName]->getType(); |
239
|
|
|
|
240
|
826 |
|
$stmt->bindValue($paramIndex++, $value, $type); |
241
|
|
|
} |
242
|
|
|
} |
243
|
|
|
|
244
|
846 |
|
$stmt->execute(); |
245
|
|
|
|
246
|
845 |
|
if ($generationPlan->containsDeferred()) { |
247
|
760 |
|
$generationPlan->executeDeferred($this->em, $entity); |
248
|
|
|
} |
249
|
|
|
|
250
|
845 |
|
if ($this->class->isVersioned()) { |
251
|
188 |
|
$this->assignDefaultVersionValue($entity, $this->getIdentifier($entity)); |
252
|
|
|
} |
253
|
|
|
|
254
|
845 |
|
$stmt->closeCursor(); |
255
|
845 |
|
} |
256
|
|
|
|
257
|
|
|
/** |
258
|
|
|
* Retrieves the default version value which was created |
259
|
|
|
* by the preceding INSERT statement and assigns it back in to the |
260
|
|
|
* entities version field. |
261
|
|
|
* |
262
|
|
|
* @param object $entity |
263
|
|
|
* @param mixed[] $id |
264
|
|
|
*/ |
265
|
196 |
|
protected function assignDefaultVersionValue($entity, array $id) |
266
|
|
|
{ |
267
|
196 |
|
$versionProperty = $this->class->versionProperty; |
268
|
196 |
|
$versionValue = $this->fetchVersionValue($versionProperty, $id); |
269
|
|
|
|
270
|
196 |
|
$versionProperty->setValue($entity, $versionValue); |
271
|
196 |
|
} |
272
|
|
|
|
273
|
|
|
/** |
274
|
|
|
* Fetches the current version value of a versioned entity. |
275
|
|
|
* |
276
|
|
|
* @param mixed[] $id |
277
|
|
|
* |
278
|
|
|
* @return mixed |
279
|
|
|
*/ |
280
|
196 |
|
protected function fetchVersionValue(VersionFieldMetadata $versionProperty, array $id) |
281
|
|
|
{ |
282
|
196 |
|
$versionedClass = $versionProperty->getDeclaringClass(); |
283
|
196 |
|
$tableName = $versionedClass->table->getQuotedQualifiedName($this->platform); |
284
|
196 |
|
$columnName = $this->platform->quoteIdentifier($versionProperty->getColumnName()); |
285
|
196 |
|
$identifier = array_map( |
286
|
196 |
|
function ($columnName) { |
287
|
196 |
|
return $this->platform->quoteIdentifier($columnName); |
288
|
196 |
|
}, |
289
|
196 |
|
array_keys($versionedClass->getIdentifierColumns($this->em)) |
290
|
|
|
); |
291
|
|
|
|
292
|
|
|
// FIXME: Order with composite keys might not be correct |
293
|
196 |
|
$sql = 'SELECT ' . $columnName |
294
|
196 |
|
. ' FROM ' . $tableName |
295
|
196 |
|
. ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?'; |
296
|
|
|
|
297
|
196 |
|
$flattenedId = $this->em->getIdentifierFlattener()->flattenIdentifier($versionedClass, $id); |
298
|
196 |
|
$versionType = $versionProperty->getType(); |
299
|
|
|
|
300
|
196 |
|
$value = $this->conn->fetchColumn( |
301
|
196 |
|
$sql, |
302
|
196 |
|
array_values($flattenedId), |
303
|
196 |
|
0, |
304
|
196 |
|
$this->extractIdentifierTypes($id, $versionedClass) |
305
|
|
|
); |
306
|
|
|
|
307
|
196 |
|
return $versionType->convertToPHPValue($value, $this->platform); |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
/** |
311
|
|
|
* @param mixed[] $id |
312
|
|
|
* |
313
|
|
|
* @return mixed[] |
314
|
|
|
*/ |
315
|
196 |
|
private function extractIdentifierTypes(array $id, ClassMetadata $versionedClass) : array |
316
|
|
|
{ |
317
|
196 |
|
$types = []; |
318
|
|
|
|
319
|
196 |
|
foreach ($id as $field => $value) { |
320
|
196 |
|
$types = array_merge($types, $this->getTypes($field, $value, $versionedClass)); |
321
|
|
|
} |
322
|
|
|
|
323
|
196 |
|
return $types; |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
/** |
327
|
|
|
* {@inheritdoc} |
328
|
|
|
*/ |
329
|
70 |
|
public function update($entity) |
330
|
|
|
{ |
331
|
70 |
|
$tableName = $this->class->getTableName(); |
332
|
70 |
|
$updateData = $this->prepareUpdateData($entity); |
333
|
|
|
|
334
|
70 |
|
if (! isset($updateData[$tableName])) { |
335
|
8 |
|
return; |
336
|
|
|
} |
337
|
|
|
|
338
|
62 |
|
$data = $updateData[$tableName]; |
339
|
|
|
|
340
|
62 |
|
if (! $data) { |
341
|
|
|
return; |
342
|
|
|
} |
343
|
|
|
|
344
|
62 |
|
$isVersioned = $this->class->isVersioned(); |
345
|
62 |
|
$quotedTableName = $this->class->table->getQuotedQualifiedName($this->platform); |
346
|
|
|
|
347
|
62 |
|
$this->updateTable($entity, $quotedTableName, $data, $isVersioned); |
348
|
|
|
|
349
|
62 |
|
if ($isVersioned) { |
350
|
10 |
|
$id = $this->em->getUnitOfWork()->getEntityIdentifier($entity); |
351
|
|
|
|
352
|
10 |
|
$this->assignDefaultVersionValue($entity, $id); |
353
|
|
|
} |
354
|
62 |
|
} |
355
|
|
|
|
356
|
|
|
/** |
357
|
|
|
* {@inheritdoc} |
358
|
|
|
*/ |
359
|
53 |
|
public function delete($entity) |
360
|
|
|
{ |
361
|
53 |
|
$class = $this->class; |
362
|
53 |
|
$unitOfWork = $this->em->getUnitOfWork(); |
363
|
53 |
|
$identifier = $unitOfWork->getEntityIdentifier($entity); |
364
|
53 |
|
$tableName = $class->table->getQuotedQualifiedName($this->platform); |
365
|
|
|
|
366
|
53 |
|
$types = []; |
367
|
53 |
|
$id = []; |
368
|
|
|
|
369
|
53 |
|
foreach ($class->identifier as $field) { |
370
|
53 |
|
$property = $class->getProperty($field); |
371
|
|
|
|
372
|
53 |
|
if ($property instanceof FieldMetadata) { |
373
|
51 |
|
$columnName = $property->getColumnName(); |
374
|
51 |
|
$quotedColumnName = $this->platform->quoteIdentifier($columnName); |
375
|
|
|
|
376
|
51 |
|
$id[$quotedColumnName] = $identifier[$field]; |
377
|
51 |
|
$types[] = $property->getType(); |
378
|
|
|
|
379
|
51 |
|
continue; |
380
|
|
|
} |
381
|
|
|
|
382
|
5 |
|
$targetClass = $this->em->getClassMetadata($property->getTargetEntity()); |
383
|
5 |
|
$joinColumns = $property instanceof ManyToManyAssociationMetadata |
384
|
|
|
? $property->getTable()->getJoinColumns() |
385
|
5 |
|
: $property->getJoinColumns() |
386
|
|
|
; |
387
|
|
|
|
388
|
5 |
|
$associationValue = null; |
389
|
5 |
|
$value = $identifier[$field]; |
390
|
|
|
|
391
|
5 |
|
if ($value !== null) { |
392
|
|
|
// @todo guilhermeblanco Make sure we do not have flat association values. |
393
|
5 |
|
if (! is_array($value)) { |
394
|
5 |
|
$value = [$targetClass->identifier[0] => $value]; |
395
|
|
|
} |
396
|
|
|
|
397
|
5 |
|
$associationValue = $value; |
398
|
|
|
} |
399
|
|
|
|
400
|
5 |
|
foreach ($joinColumns as $joinColumn) { |
401
|
|
|
/** @var JoinColumnMetadata $joinColumn */ |
402
|
5 |
|
$quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
403
|
5 |
|
$referencedColumnName = $joinColumn->getReferencedColumnName(); |
404
|
5 |
|
$targetField = $targetClass->fieldNames[$referencedColumnName]; |
405
|
|
|
|
406
|
5 |
|
if (! $joinColumn->getType()) { |
407
|
|
|
$joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em)); |
408
|
|
|
} |
409
|
|
|
|
410
|
5 |
|
$id[$quotedColumnName] = $associationValue ? $associationValue[$targetField] : null; |
411
|
5 |
|
$types[] = $joinColumn->getType(); |
412
|
|
|
} |
413
|
|
|
} |
414
|
|
|
|
415
|
53 |
|
$this->deleteJoinTableRecords($identifier); |
416
|
|
|
|
417
|
53 |
|
return (bool) $this->conn->delete($tableName, $id, $types); |
418
|
|
|
} |
419
|
|
|
|
420
|
|
|
/** |
421
|
|
|
* Performs an UPDATE statement for an entity on a specific table. |
422
|
|
|
* The UPDATE can optionally be versioned, which requires the entity to have a version field. |
423
|
|
|
* |
424
|
|
|
* @param object $entity The entity object being updated. |
425
|
|
|
* @param string $quotedTableName The quoted name of the table to apply the UPDATE on. |
426
|
|
|
* @param mixed[] $updateData The map of columns to update (column => value). |
427
|
|
|
* @param bool $versioned Whether the UPDATE should be versioned. |
428
|
|
|
* |
429
|
|
|
* @throws \Doctrine\ORM\ORMException |
430
|
|
|
* @throws \Doctrine\ORM\OptimisticLockException |
431
|
|
|
*/ |
432
|
89 |
|
final protected function updateTable($entity, $quotedTableName, array $updateData, $versioned = false) |
433
|
|
|
{ |
434
|
89 |
|
$set = []; |
435
|
89 |
|
$types = []; |
436
|
89 |
|
$params = []; |
437
|
|
|
|
438
|
89 |
|
foreach ($updateData as $columnName => $value) { |
439
|
89 |
|
$column = $this->columns[$columnName]; |
440
|
89 |
|
$quotedColumnName = $this->platform->quoteIdentifier($column->getColumnName()); |
441
|
89 |
|
$type = $column->getType(); |
442
|
89 |
|
$placeholder = $type->convertToDatabaseValueSQL('?', $this->platform); |
443
|
|
|
|
444
|
89 |
|
$set[] = sprintf('%s = %s', $quotedColumnName, $placeholder); |
445
|
89 |
|
$params[] = $value; |
446
|
89 |
|
$types[] = $column->getType(); |
447
|
|
|
} |
448
|
|
|
|
449
|
|
|
// @todo guilhermeblanco Bring this back: $this->em->getUnitOfWork()->getEntityIdentifier($entity); |
|
|
|
|
450
|
89 |
|
$identifier = $this->getIdentifier($entity); |
451
|
89 |
|
$where = []; |
452
|
|
|
|
453
|
89 |
|
foreach ($this->class->identifier as $idField) { |
454
|
89 |
|
$property = $this->class->getProperty($idField); |
455
|
|
|
|
456
|
|
|
switch (true) { |
457
|
89 |
|
case ($property instanceof FieldMetadata): |
458
|
86 |
|
$where[] = $this->platform->quoteIdentifier($property->getColumnName()); |
459
|
86 |
|
$params[] = $identifier[$idField]; |
460
|
86 |
|
$types[] = $property->getType(); |
461
|
86 |
|
break; |
462
|
|
|
|
463
|
4 |
|
case ($property instanceof ToOneAssociationMetadata): |
464
|
4 |
|
$targetClass = $this->em->getClassMetadata($property->getTargetEntity()); |
465
|
4 |
|
$targetPersister = $this->em->getUnitOfWork()->getEntityPersister($property->getTargetEntity()); |
466
|
|
|
|
467
|
4 |
|
foreach ($property->getJoinColumns() as $joinColumn) { |
468
|
|
|
/** @var JoinColumnMetadata $joinColumn */ |
469
|
4 |
|
$quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
470
|
4 |
|
$referencedColumnName = $joinColumn->getReferencedColumnName(); |
471
|
|
|
|
472
|
4 |
|
if (! $joinColumn->getType()) { |
473
|
|
|
$joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em)); |
474
|
|
|
} |
475
|
|
|
|
476
|
4 |
|
$value = $targetPersister->getColumnValue($identifier[$idField], $referencedColumnName); |
477
|
|
|
|
478
|
4 |
|
$where[] = $quotedColumnName; |
479
|
4 |
|
$params[] = $value; |
480
|
4 |
|
$types[] = $joinColumn->getType(); |
481
|
|
|
} |
482
|
89 |
|
break; |
483
|
|
|
} |
484
|
|
|
} |
485
|
|
|
|
486
|
89 |
|
if ($versioned) { |
487
|
14 |
|
$versionProperty = $this->class->versionProperty; |
488
|
14 |
|
$versionColumnType = $versionProperty->getType(); |
489
|
14 |
|
$versionColumnName = $this->platform->quoteIdentifier($versionProperty->getColumnName()); |
490
|
|
|
|
491
|
14 |
|
$where[] = $versionColumnName; |
492
|
14 |
|
$types[] = $versionColumnType; |
493
|
14 |
|
$params[] = $versionProperty->getValue($entity); |
494
|
|
|
|
495
|
14 |
|
switch ($versionColumnType->getName()) { |
496
|
|
|
case Type::SMALLINT: |
497
|
|
|
case Type::INTEGER: |
498
|
|
|
case Type::BIGINT: |
499
|
14 |
|
$set[] = $versionColumnName . ' = ' . $versionColumnName . ' + 1'; |
500
|
14 |
|
break; |
501
|
|
|
|
502
|
|
|
case Type::DATETIME: |
503
|
|
|
$set[] = $versionColumnName . ' = CURRENT_TIMESTAMP'; |
504
|
|
|
break; |
505
|
|
|
} |
506
|
|
|
} |
507
|
|
|
|
508
|
89 |
|
$sql = 'UPDATE ' . $quotedTableName |
509
|
89 |
|
. ' SET ' . implode(', ', $set) |
510
|
89 |
|
. ' WHERE ' . implode(' = ? AND ', $where) . ' = ?'; |
511
|
|
|
|
512
|
89 |
|
$result = $this->conn->executeUpdate($sql, $params, $types); |
513
|
|
|
|
514
|
89 |
|
if ($versioned && ! $result) { |
515
|
|
|
throw OptimisticLockException::lockFailed($entity); |
516
|
|
|
} |
517
|
89 |
|
} |
518
|
|
|
|
519
|
|
|
/** |
520
|
|
|
* @todo Add check for platform if it supports foreign keys/cascading. |
521
|
|
|
* |
522
|
|
|
* @param mixed[] $identifier |
523
|
|
|
*/ |
524
|
57 |
|
protected function deleteJoinTableRecords($identifier) |
525
|
|
|
{ |
526
|
57 |
|
foreach ($this->class->getDeclaredPropertiesIterator() as $association) { |
527
|
57 |
|
if (! ($association instanceof ManyToManyAssociationMetadata)) { |
528
|
57 |
|
continue; |
529
|
|
|
} |
530
|
|
|
|
531
|
|
|
// @Todo this only covers scenarios with no inheritance or of the same level. Is there something |
532
|
|
|
// like self-referential relationship between different levels of an inheritance hierarchy? I hope not! |
533
|
20 |
|
$selfReferential = $association->getTargetEntity() === $association->getSourceEntity(); |
534
|
20 |
|
$owningAssociation = $association; |
535
|
20 |
|
$otherColumns = []; |
536
|
20 |
|
$otherKeys = []; |
537
|
20 |
|
$keys = []; |
538
|
|
|
|
539
|
20 |
|
if (! $owningAssociation->isOwningSide()) { |
540
|
6 |
|
$class = $this->em->getClassMetadata($association->getTargetEntity()); |
541
|
6 |
|
$owningAssociation = $class->getProperty($association->getMappedBy()); |
542
|
|
|
} |
543
|
|
|
|
544
|
20 |
|
$joinTable = $owningAssociation->getJoinTable(); |
545
|
20 |
|
$joinTableName = $joinTable->getQuotedQualifiedName($this->platform); |
546
|
20 |
|
$joinColumns = $association->isOwningSide() |
547
|
16 |
|
? $joinTable->getJoinColumns() |
548
|
20 |
|
: $joinTable->getInverseJoinColumns() |
549
|
|
|
; |
550
|
|
|
|
551
|
20 |
|
if ($selfReferential) { |
552
|
1 |
|
$otherColumns = ! $association->isOwningSide() |
553
|
|
|
? $joinTable->getJoinColumns() |
554
|
1 |
|
: $joinTable->getInverseJoinColumns() |
555
|
|
|
; |
556
|
|
|
} |
557
|
|
|
|
558
|
20 |
|
$isOnDeleteCascade = false; |
559
|
|
|
|
560
|
20 |
|
foreach ($joinColumns as $joinColumn) { |
561
|
20 |
|
$keys[] = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
562
|
|
|
|
563
|
20 |
|
if ($joinColumn->isOnDeleteCascade()) { |
564
|
20 |
|
$isOnDeleteCascade = true; |
565
|
|
|
} |
566
|
|
|
} |
567
|
|
|
|
568
|
20 |
|
foreach ($otherColumns as $joinColumn) { |
569
|
1 |
|
$otherKeys[] = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
570
|
|
|
|
571
|
1 |
|
if ($joinColumn->isOnDeleteCascade()) { |
572
|
1 |
|
$isOnDeleteCascade = true; |
573
|
|
|
} |
574
|
|
|
} |
575
|
|
|
|
576
|
20 |
|
if ($isOnDeleteCascade) { |
577
|
5 |
|
continue; |
578
|
|
|
} |
579
|
|
|
|
580
|
16 |
|
$this->conn->delete($joinTableName, array_combine($keys, $identifier)); |
581
|
|
|
|
582
|
16 |
|
if ($selfReferential) { |
583
|
16 |
|
$this->conn->delete($joinTableName, array_combine($otherKeys, $identifier)); |
584
|
|
|
} |
585
|
|
|
} |
586
|
57 |
|
} |
587
|
|
|
|
588
|
|
|
/** |
589
|
|
|
* Prepares the data changeset of a managed entity for database insertion (initial INSERT). |
590
|
|
|
* The changeset of the entity is obtained from the currently running UnitOfWork. |
591
|
|
|
* |
592
|
|
|
* The default insert data preparation is the same as for updates. |
593
|
|
|
* |
594
|
|
|
* @param object $entity The entity for which to prepare the data. |
595
|
|
|
* |
596
|
|
|
* @return mixed[] The prepared data for the tables to update. |
597
|
|
|
*/ |
598
|
926 |
|
protected function prepareInsertData($entity) : array |
599
|
|
|
{ |
600
|
926 |
|
return $this->prepareUpdateData($entity); |
601
|
|
|
} |
602
|
|
|
|
603
|
|
|
/** |
604
|
|
|
* Prepares the changeset of an entity for database insertion (UPDATE). |
605
|
|
|
* |
606
|
|
|
* The changeset is obtained from the currently running UnitOfWork. |
607
|
|
|
* |
608
|
|
|
* During this preparation the array that is passed as the second parameter is filled with |
609
|
|
|
* <columnName> => <value> pairs, grouped by table name. |
610
|
|
|
* |
611
|
|
|
* Example: |
612
|
|
|
* <code> |
613
|
|
|
* array( |
614
|
|
|
* 'foo_table' => array('column1' => 'value1', 'column2' => 'value2', ...), |
615
|
|
|
* 'bar_table' => array('columnX' => 'valueX', 'columnY' => 'valueY', ...), |
616
|
|
|
* ... |
617
|
|
|
* ) |
618
|
|
|
* </code> |
619
|
|
|
* |
620
|
|
|
* @param object $entity The entity for which to prepare the data. |
621
|
|
|
* |
622
|
|
|
* @return mixed[] The prepared data. |
623
|
|
|
*/ |
624
|
927 |
|
protected function prepareUpdateData($entity) |
625
|
|
|
{ |
626
|
927 |
|
$uow = $this->em->getUnitOfWork(); |
627
|
927 |
|
$result = []; |
628
|
927 |
|
$versionPropertyName = $this->class->isVersioned() |
629
|
196 |
|
? $this->class->versionProperty->getName() |
630
|
927 |
|
: null |
631
|
|
|
; |
632
|
|
|
|
633
|
|
|
// @todo guilhermeblanco This should check column insertability/updateability instead of field changeset |
634
|
927 |
|
foreach ($uow->getEntityChangeSet($entity) as $propertyName => $propertyChangeSet) { |
635
|
899 |
|
if ($versionPropertyName === $propertyName) { |
636
|
|
|
continue; |
637
|
|
|
} |
638
|
|
|
|
639
|
899 |
|
$property = $this->class->getProperty($propertyName); |
640
|
899 |
|
$newValue = $propertyChangeSet[1]; |
641
|
|
|
|
642
|
899 |
|
if ($property instanceof FieldMetadata) { |
643
|
|
|
// @todo guilhermeblanco Please remove this in the future for good... |
644
|
870 |
|
$this->columns[$property->getColumnName()] = $property; |
645
|
|
|
|
646
|
870 |
|
$result[$property->getTableName()][$property->getColumnName()] = $newValue; |
647
|
|
|
|
648
|
870 |
|
continue; |
649
|
|
|
} |
650
|
|
|
|
651
|
|
|
// Only owning side of x-1 associations can have a FK column. |
652
|
782 |
|
if (! $property instanceof ToOneAssociationMetadata || ! $property->isOwningSide()) { |
653
|
8 |
|
continue; |
654
|
|
|
} |
655
|
|
|
|
656
|
|
|
// The associated entity $newVal is not yet persisted, so we must |
657
|
|
|
// set $newVal = null, in order to insert a null value and schedule an |
658
|
|
|
// extra update on the UnitOfWork. |
659
|
782 |
|
if ($newValue !== null && $uow->isScheduledForInsert($newValue)) { |
660
|
30 |
|
$uow->scheduleExtraUpdate($entity, [$propertyName => [null, $newValue]]); |
661
|
|
|
|
662
|
30 |
|
$newValue = null; |
663
|
|
|
} |
664
|
|
|
|
665
|
782 |
|
$targetClass = $this->em->getClassMetadata($property->getTargetEntity()); |
666
|
782 |
|
$targetPersister = $uow->getEntityPersister($targetClass->getClassName()); |
667
|
|
|
|
668
|
782 |
|
foreach ($property->getJoinColumns() as $joinColumn) { |
669
|
|
|
/** @var JoinColumnMetadata $joinColumn */ |
670
|
782 |
|
$referencedColumnName = $joinColumn->getReferencedColumnName(); |
671
|
|
|
|
672
|
782 |
|
if (! $joinColumn->getType()) { |
673
|
8 |
|
$joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em)); |
674
|
|
|
} |
675
|
|
|
|
676
|
|
|
// @todo guilhermeblanco Please remove this in the future for good... |
677
|
782 |
|
$this->columns[$joinColumn->getColumnName()] = $joinColumn; |
678
|
|
|
|
679
|
782 |
|
$result[$joinColumn->getTableName()][$joinColumn->getColumnName()] = $newValue !== null |
680
|
583 |
|
? $targetPersister->getColumnValue($newValue, $referencedColumnName) |
681
|
782 |
|
: null |
682
|
|
|
; |
683
|
|
|
} |
684
|
|
|
} |
685
|
|
|
|
686
|
927 |
|
return $result; |
687
|
|
|
} |
688
|
|
|
|
689
|
|
|
/** |
690
|
|
|
* @param object $entity |
691
|
|
|
* |
692
|
|
|
* @return mixed|null |
693
|
|
|
*/ |
694
|
583 |
|
public function getColumnValue($entity, string $columnName) |
695
|
|
|
{ |
696
|
|
|
// Looking for fields by column is the easiest way to look at local columns or x-1 owning side associations |
697
|
583 |
|
$propertyName = $this->class->fieldNames[$columnName]; |
698
|
583 |
|
$property = $this->class->getProperty($propertyName); |
699
|
|
|
|
700
|
583 |
|
if (! $property) { |
701
|
|
|
return null; |
702
|
|
|
} |
703
|
|
|
|
704
|
583 |
|
$propertyValue = $property->getValue($entity); |
705
|
|
|
|
706
|
583 |
|
if ($property instanceof LocalColumnMetadata) { |
707
|
583 |
|
return $propertyValue; |
708
|
|
|
} |
709
|
|
|
|
710
|
|
|
/* @var ToOneAssociationMetadata $property */ |
711
|
19 |
|
$unitOfWork = $this->em->getUnitOfWork(); |
712
|
19 |
|
$targetClass = $this->em->getClassMetadata($property->getTargetEntity()); |
713
|
19 |
|
$targetPersister = $unitOfWork->getEntityPersister($property->getTargetEntity()); |
714
|
|
|
|
715
|
19 |
|
foreach ($property->getJoinColumns() as $joinColumn) { |
716
|
|
|
/** @var JoinColumnMetadata $joinColumn */ |
717
|
19 |
|
$referencedColumnName = $joinColumn->getReferencedColumnName(); |
718
|
|
|
|
719
|
19 |
|
if (! $joinColumn->getType()) { |
720
|
|
|
$joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em)); |
721
|
|
|
} |
722
|
|
|
|
723
|
19 |
|
if ($joinColumn->getColumnName() !== $columnName) { |
724
|
|
|
continue; |
725
|
|
|
} |
726
|
|
|
|
727
|
19 |
|
return $targetPersister->getColumnValue($propertyValue, $referencedColumnName); |
728
|
|
|
} |
729
|
|
|
|
730
|
|
|
return null; |
731
|
|
|
} |
732
|
|
|
|
733
|
|
|
/** |
734
|
|
|
* {@inheritdoc} |
735
|
|
|
*/ |
736
|
438 |
|
public function load( |
737
|
|
|
array $criteria, |
738
|
|
|
$entity = null, |
739
|
|
|
?AssociationMetadata $association = null, |
740
|
|
|
array $hints = [], |
741
|
|
|
$lockMode = null, |
742
|
|
|
$limit = null, |
743
|
|
|
array $orderBy = [] |
744
|
|
|
) { |
745
|
438 |
|
$this->switchPersisterContext(null, $limit); |
746
|
|
|
|
747
|
438 |
|
$sql = $this->getSelectSQL($criteria, $association, $lockMode, $limit, null, $orderBy); |
748
|
|
|
|
749
|
438 |
|
list($params, $types) = $this->expandParameters($criteria); |
750
|
|
|
|
751
|
438 |
|
$stmt = $this->conn->executeQuery($sql, $params, $types); |
752
|
|
|
|
753
|
438 |
|
if ($entity !== null) { |
754
|
55 |
|
$hints[Query::HINT_REFRESH] = true; |
755
|
55 |
|
$hints[Query::HINT_REFRESH_ENTITY] = $entity; |
756
|
|
|
} |
757
|
|
|
|
758
|
438 |
|
$hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT); |
759
|
438 |
|
$entities = $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, $hints); |
760
|
|
|
|
761
|
438 |
|
return $entities ? $entities[0] : null; |
762
|
|
|
} |
763
|
|
|
|
764
|
|
|
/** |
765
|
|
|
* {@inheritdoc} |
766
|
|
|
*/ |
767
|
364 |
|
public function loadById(array $identifier, $entity = null) |
768
|
|
|
{ |
769
|
364 |
|
return $this->load($identifier, $entity); |
770
|
|
|
} |
771
|
|
|
|
772
|
|
|
/** |
773
|
|
|
* {@inheritdoc} |
774
|
|
|
*/ |
775
|
86 |
|
public function loadToOneEntity(ToOneAssociationMetadata $association, $sourceEntity, array $identifier = []) |
776
|
|
|
{ |
777
|
86 |
|
$unitOfWork = $this->em->getUnitOfWork(); |
778
|
86 |
|
$targetEntity = $association->getTargetEntity(); |
779
|
86 |
|
$foundEntity = $unitOfWork->tryGetById($identifier, $targetEntity); |
780
|
|
|
|
781
|
86 |
|
if ($foundEntity !== false) { |
782
|
|
|
return $foundEntity; |
783
|
|
|
} |
784
|
|
|
|
785
|
86 |
|
$targetClass = $this->em->getClassMetadata($targetEntity); |
786
|
|
|
|
787
|
86 |
|
if ($association->isOwningSide()) { |
788
|
29 |
|
$inversedBy = $association->getInversedBy(); |
789
|
29 |
|
$targetProperty = $inversedBy ? $targetClass->getProperty($inversedBy) : null; |
790
|
29 |
|
$isInverseSingleValued = $targetProperty && $targetProperty instanceof ToOneAssociationMetadata; |
791
|
|
|
|
792
|
|
|
// Mark inverse side as fetched in the hints, otherwise the UoW would |
793
|
|
|
// try to load it in a separate query (remember: to-one inverse sides can not be lazy). |
794
|
29 |
|
$hints = []; |
795
|
|
|
|
796
|
29 |
|
if ($isInverseSingleValued) { |
797
|
|
|
$hints['fetched']['r'][$inversedBy] = true; |
798
|
|
|
} |
799
|
|
|
|
800
|
|
|
/* cascade read-only status |
|
|
|
|
801
|
|
|
if ($this->em->getUnitOfWork()->isReadOnly($sourceEntity)) { |
802
|
|
|
$hints[Query::HINT_READ_ONLY] = true; |
803
|
|
|
} |
804
|
|
|
*/ |
805
|
|
|
|
806
|
29 |
|
$entity = $this->load($identifier, null, $association, $hints); |
807
|
|
|
|
808
|
|
|
// Complete bidirectional association, if necessary |
809
|
29 |
|
if ($entity !== null && $isInverseSingleValued) { |
810
|
|
|
$targetProperty->setValue($entity, $sourceEntity); |
811
|
|
|
} |
812
|
|
|
|
813
|
29 |
|
return $entity; |
814
|
|
|
} |
815
|
|
|
|
816
|
57 |
|
$sourceClass = $association->getDeclaringClass(); |
817
|
57 |
|
$owningAssociation = $targetClass->getProperty($association->getMappedBy()); |
818
|
57 |
|
$targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName()); |
819
|
|
|
|
820
|
57 |
|
foreach ($owningAssociation->getJoinColumns() as $joinColumn) { |
821
|
57 |
|
$sourceKeyColumn = $joinColumn->getReferencedColumnName(); |
822
|
57 |
|
$targetKeyColumn = $joinColumn->getColumnName(); |
823
|
|
|
|
824
|
57 |
|
if (! isset($sourceClass->fieldNames[$sourceKeyColumn])) { |
825
|
|
|
throw MappingException::joinColumnMustPointToMappedField( |
826
|
|
|
$sourceClass->getClassName(), |
827
|
|
|
$sourceKeyColumn |
828
|
|
|
); |
829
|
|
|
} |
830
|
|
|
|
831
|
57 |
|
$property = $sourceClass->getProperty($sourceClass->fieldNames[$sourceKeyColumn]); |
832
|
57 |
|
$value = $property->getValue($sourceEntity); |
833
|
|
|
|
834
|
|
|
// unset the old value and set the new sql aliased value here. By definition |
835
|
|
|
// unset($identifier[$targetKeyColumn] works here with how UnitOfWork::createEntity() calls this method. |
836
|
|
|
// @todo guilhermeblanco In master we have: $identifier[$targetClass->getFieldForColumn($targetKeyColumn)] = |
837
|
57 |
|
unset($identifier[$targetKeyColumn]); |
838
|
|
|
|
839
|
57 |
|
$identifier[$targetClass->fieldNames[$targetKeyColumn]] = $value; |
840
|
|
|
} |
841
|
|
|
|
842
|
57 |
|
$entity = $this->load($identifier, null, $association); |
843
|
|
|
|
844
|
57 |
|
if ($entity !== null) { |
845
|
16 |
|
$owningAssociation->setValue($entity, $sourceEntity); |
846
|
|
|
} |
847
|
|
|
|
848
|
57 |
|
return $entity; |
849
|
|
|
} |
850
|
|
|
|
851
|
|
|
/** |
852
|
|
|
* {@inheritdoc} |
853
|
|
|
*/ |
854
|
11 |
|
public function refresh(array $id, $entity, $lockMode = null) |
855
|
|
|
{ |
856
|
11 |
|
$sql = $this->getSelectSQL($id, null, $lockMode); |
857
|
11 |
|
list($params, $types) = $this->expandParameters($id); |
858
|
11 |
|
$stmt = $this->conn->executeQuery($sql, $params, $types); |
859
|
|
|
|
860
|
11 |
|
$hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT); |
861
|
11 |
|
$hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [Query::HINT_REFRESH => true]); |
862
|
11 |
|
} |
863
|
|
|
|
864
|
|
|
/** |
865
|
|
|
* {@inheritDoc} |
866
|
|
|
*/ |
867
|
46 |
|
public function count($criteria = []) |
868
|
|
|
{ |
869
|
46 |
|
$sql = $this->getCountSQL($criteria); |
870
|
|
|
|
871
|
46 |
|
list($params, $types) = ($criteria instanceof Criteria) |
872
|
25 |
|
? $this->expandCriteriaParameters($criteria) |
873
|
46 |
|
: $this->expandParameters($criteria); |
874
|
|
|
|
875
|
46 |
|
return (int) $this->conn->executeQuery($sql, $params, $types)->fetchColumn(); |
876
|
|
|
} |
877
|
|
|
|
878
|
|
|
/** |
879
|
|
|
* {@inheritdoc} |
880
|
|
|
*/ |
881
|
6 |
|
public function loadCriteria(Criteria $criteria) |
882
|
|
|
{ |
883
|
6 |
|
$orderBy = $criteria->getOrderings(); |
884
|
6 |
|
$limit = $criteria->getMaxResults(); |
885
|
6 |
|
$offset = $criteria->getFirstResult(); |
886
|
6 |
|
$query = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy); |
887
|
|
|
|
888
|
6 |
|
list($params, $types) = $this->expandCriteriaParameters($criteria); |
889
|
|
|
|
890
|
6 |
|
$stmt = $this->conn->executeQuery($query, $params, $types); |
891
|
6 |
|
$rsm = $this->currentPersisterContext->rsm; |
892
|
6 |
|
$hints = [UnitOfWork::HINT_DEFEREAGERLOAD => true]; |
893
|
6 |
|
$hydratorType = $this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT; |
894
|
6 |
|
$hydrator = $this->em->newHydrator($hydratorType); |
895
|
|
|
|
896
|
6 |
|
return $hydrator->hydrateAll($stmt, $rsm, $hints); |
897
|
|
|
} |
898
|
|
|
|
899
|
|
|
/** |
900
|
|
|
* {@inheritdoc} |
901
|
|
|
*/ |
902
|
37 |
|
public function expandCriteriaParameters(Criteria $criteria) |
903
|
|
|
{ |
904
|
37 |
|
$expression = $criteria->getWhereExpression(); |
905
|
37 |
|
$sqlParams = []; |
906
|
37 |
|
$sqlTypes = []; |
907
|
|
|
|
908
|
37 |
|
if ($expression === null) { |
909
|
2 |
|
return [$sqlParams, $sqlTypes]; |
910
|
|
|
} |
911
|
|
|
|
912
|
36 |
|
$valueVisitor = new SqlValueVisitor(); |
913
|
|
|
|
914
|
36 |
|
$valueVisitor->dispatch($expression); |
915
|
|
|
|
916
|
36 |
|
list($params, $types) = $valueVisitor->getParamsAndTypes(); |
917
|
|
|
|
918
|
36 |
|
foreach ($params as $param) { |
919
|
32 |
|
$sqlParams = array_merge($sqlParams, $this->getValues($param)); |
920
|
|
|
} |
921
|
|
|
|
922
|
36 |
|
foreach ($types as $type) { |
923
|
32 |
|
list ($field, $value) = $type; |
924
|
32 |
|
$sqlTypes = array_merge($sqlTypes, $this->getTypes($field, $value, $this->class)); |
925
|
|
|
} |
926
|
|
|
|
927
|
36 |
|
return [$sqlParams, $sqlTypes]; |
928
|
|
|
} |
929
|
|
|
|
930
|
|
|
/** |
931
|
|
|
* {@inheritdoc} |
932
|
|
|
*/ |
933
|
62 |
|
public function loadAll(array $criteria = [], array $orderBy = [], $limit = null, $offset = null) |
934
|
|
|
{ |
935
|
62 |
|
$this->switchPersisterContext($offset, $limit); |
936
|
|
|
|
937
|
62 |
|
$sql = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy); |
938
|
|
|
|
939
|
62 |
|
list($params, $types) = $this->expandParameters($criteria); |
940
|
|
|
|
941
|
62 |
|
$stmt = $this->conn->executeQuery($sql, $params, $types); |
942
|
62 |
|
$rsm = $this->currentPersisterContext->rsm; |
943
|
62 |
|
$hints = [UnitOfWork::HINT_DEFEREAGERLOAD => true]; |
944
|
62 |
|
$hydratorType = $this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT; |
945
|
62 |
|
$hydrator = $this->em->newHydrator($hydratorType); |
946
|
|
|
|
947
|
62 |
|
return $hydrator->hydrateAll($stmt, $rsm, $hints); |
948
|
|
|
} |
949
|
|
|
|
950
|
|
|
/** |
951
|
|
|
* {@inheritdoc} |
952
|
|
|
*/ |
953
|
8 |
|
public function getManyToManyCollection( |
954
|
|
|
ManyToManyAssociationMetadata $association, |
955
|
|
|
$sourceEntity, |
956
|
|
|
$offset = null, |
957
|
|
|
$limit = null |
958
|
|
|
) { |
959
|
8 |
|
$this->switchPersisterContext($offset, $limit); |
960
|
|
|
|
961
|
8 |
|
$stmt = $this->getManyToManyStatement($association, $sourceEntity, $offset, $limit); |
962
|
|
|
|
963
|
8 |
|
return $this->loadArrayFromStatement($association, $stmt); |
964
|
|
|
} |
965
|
|
|
|
966
|
|
|
/** |
967
|
|
|
* {@inheritdoc} |
968
|
|
|
*/ |
969
|
70 |
|
public function loadManyToManyCollection( |
970
|
|
|
ManyToManyAssociationMetadata $association, |
971
|
|
|
$sourceEntity, |
972
|
|
|
PersistentCollection $collection |
973
|
|
|
) { |
974
|
70 |
|
$stmt = $this->getManyToManyStatement($association, $sourceEntity); |
975
|
|
|
|
976
|
70 |
|
return $this->loadCollectionFromStatement($association, $stmt, $collection); |
977
|
|
|
} |
978
|
|
|
|
979
|
|
|
/** |
980
|
|
|
* Loads an array of entities from a given DBAL statement. |
981
|
|
|
* |
982
|
|
|
* @param \Doctrine\DBAL\Statement $stmt |
983
|
|
|
* |
984
|
|
|
* @return mixed[] |
985
|
|
|
*/ |
986
|
13 |
|
private function loadArrayFromStatement(ToManyAssociationMetadata $association, $stmt) |
987
|
|
|
{ |
988
|
13 |
|
$rsm = $this->currentPersisterContext->rsm; |
989
|
|
|
|
990
|
13 |
|
if ($association->getIndexedBy()) { |
991
|
7 |
|
$rsm = clone ($this->currentPersisterContext->rsm); // this is necessary because the "default rsm" should be changed. |
992
|
7 |
|
$rsm->addIndexBy('r', $association->getIndexedBy()); |
993
|
|
|
} |
994
|
|
|
|
995
|
13 |
|
$hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT); |
996
|
13 |
|
$hints = [UnitOfWork::HINT_DEFEREAGERLOAD => true]; |
997
|
|
|
|
998
|
13 |
|
return $hydrator->hydrateAll($stmt, $rsm, $hints); |
999
|
|
|
} |
1000
|
|
|
|
1001
|
|
|
/** |
1002
|
|
|
* Hydrates a collection from a given DBAL statement. |
1003
|
|
|
* |
1004
|
|
|
* @param \Doctrine\DBAL\Statement $stmt |
1005
|
|
|
* @param PersistentCollection $collection |
1006
|
|
|
* |
1007
|
|
|
* @return mixed[] |
1008
|
|
|
*/ |
1009
|
129 |
|
private function loadCollectionFromStatement(ToManyAssociationMetadata $association, $stmt, $collection) |
1010
|
|
|
{ |
1011
|
129 |
|
$rsm = $this->currentPersisterContext->rsm; |
1012
|
|
|
|
1013
|
129 |
|
if ($association->getIndexedBy()) { |
1014
|
10 |
|
$rsm = clone ($this->currentPersisterContext->rsm); // this is necessary because the "default rsm" should be changed. |
1015
|
10 |
|
$rsm->addIndexBy('r', $association->getIndexedBy()); |
1016
|
|
|
} |
1017
|
|
|
|
1018
|
129 |
|
$hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT); |
1019
|
|
|
$hints = [ |
1020
|
129 |
|
UnitOfWork::HINT_DEFEREAGERLOAD => true, |
1021
|
129 |
|
'collection' => $collection, |
1022
|
|
|
]; |
1023
|
|
|
|
1024
|
129 |
|
return $hydrator->hydrateAll($stmt, $rsm, $hints); |
1025
|
|
|
} |
1026
|
|
|
|
1027
|
|
|
/** |
1028
|
|
|
* @param object $sourceEntity |
1029
|
|
|
* @param int|null $offset |
1030
|
|
|
* @param int|null $limit |
1031
|
|
|
* |
1032
|
|
|
* @return \Doctrine\DBAL\Driver\Statement |
1033
|
|
|
* |
1034
|
|
|
* @throws \Doctrine\ORM\Mapping\MappingException |
1035
|
|
|
*/ |
1036
|
77 |
|
private function getManyToManyStatement( |
1037
|
|
|
ManyToManyAssociationMetadata $association, |
1038
|
|
|
$sourceEntity, |
1039
|
|
|
$offset = null, |
1040
|
|
|
$limit = null |
1041
|
|
|
) { |
1042
|
77 |
|
$this->switchPersisterContext($offset, $limit); |
1043
|
|
|
|
1044
|
|
|
/** @var ClassMetadata $sourceClass */ |
1045
|
77 |
|
$sourceClass = $this->em->getClassMetadata($association->getSourceEntity()); |
1046
|
77 |
|
$class = $sourceClass; |
1047
|
77 |
|
$owningAssoc = $association; |
1048
|
77 |
|
$criteria = []; |
1049
|
77 |
|
$parameters = []; |
1050
|
|
|
|
1051
|
77 |
|
if (! $association->isOwningSide()) { |
1052
|
12 |
|
$class = $this->em->getClassMetadata($association->getTargetEntity()); |
1053
|
12 |
|
$owningAssoc = $class->getProperty($association->getMappedBy()); |
1054
|
|
|
} |
1055
|
|
|
|
1056
|
77 |
|
$joinTable = $owningAssoc->getJoinTable(); |
1057
|
77 |
|
$joinTableName = $joinTable->getQuotedQualifiedName($this->platform); |
1058
|
77 |
|
$joinColumns = $association->isOwningSide() |
1059
|
70 |
|
? $joinTable->getJoinColumns() |
1060
|
77 |
|
: $joinTable->getInverseJoinColumns() |
1061
|
|
|
; |
1062
|
|
|
|
1063
|
77 |
|
foreach ($joinColumns as $joinColumn) { |
1064
|
77 |
|
$quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
1065
|
77 |
|
$fieldName = $sourceClass->fieldNames[$joinColumn->getReferencedColumnName()]; |
1066
|
77 |
|
$property = $sourceClass->getProperty($fieldName); |
1067
|
|
|
|
1068
|
77 |
|
if ($property instanceof FieldMetadata) { |
1069
|
76 |
|
$value = $property->getValue($sourceEntity); |
1070
|
4 |
|
} elseif ($property instanceof AssociationMetadata) { |
1071
|
4 |
|
$property = $sourceClass->getProperty($fieldName); |
1072
|
4 |
|
$targetClass = $this->em->getClassMetadata($property->getTargetEntity()); |
1073
|
4 |
|
$value = $property->getValue($sourceEntity); |
1074
|
|
|
|
1075
|
4 |
|
$value = $this->em->getUnitOfWork()->getEntityIdentifier($value); |
1076
|
4 |
|
$value = $value[$targetClass->identifier[0]]; |
1077
|
|
|
} |
1078
|
|
|
|
1079
|
77 |
|
$criteria[$joinTableName . '.' . $quotedColumnName] = $value; |
1080
|
77 |
|
$parameters[] = [ |
1081
|
77 |
|
'value' => $value, |
1082
|
77 |
|
'field' => $fieldName, |
1083
|
77 |
|
'class' => $sourceClass, |
1084
|
|
|
]; |
1085
|
|
|
} |
1086
|
|
|
|
1087
|
77 |
|
$sql = $this->getSelectSQL($criteria, $association, null, $limit, $offset); |
1088
|
|
|
|
1089
|
77 |
|
list($params, $types) = $this->expandToManyParameters($parameters); |
1090
|
|
|
|
1091
|
77 |
|
return $this->conn->executeQuery($sql, $params, $types); |
1092
|
|
|
} |
1093
|
|
|
|
1094
|
|
|
/** |
1095
|
|
|
* {@inheritdoc} |
1096
|
|
|
*/ |
1097
|
480 |
|
public function getSelectSQL( |
1098
|
|
|
$criteria, |
1099
|
|
|
?AssociationMetadata $association = null, |
1100
|
|
|
$lockMode = null, |
1101
|
|
|
$limit = null, |
1102
|
|
|
$offset = null, |
1103
|
|
|
array $orderBy = [] |
1104
|
|
|
) { |
1105
|
480 |
|
$this->switchPersisterContext($offset, $limit); |
1106
|
|
|
|
1107
|
480 |
|
$lockSql = ''; |
1108
|
480 |
|
$joinSql = ''; |
1109
|
480 |
|
$orderBySql = ''; |
1110
|
|
|
|
1111
|
480 |
|
if ($association instanceof ManyToManyAssociationMetadata) { |
1112
|
78 |
|
$joinSql = $this->getSelectManyToManyJoinSQL($association); |
1113
|
|
|
} |
1114
|
|
|
|
1115
|
480 |
|
if ($association instanceof ToManyAssociationMetadata && $association->getOrderBy()) { |
1116
|
5 |
|
$orderBy = $association->getOrderBy(); |
1117
|
|
|
} |
1118
|
|
|
|
1119
|
480 |
|
if ($orderBy) { |
1120
|
9 |
|
$orderBySql = $this->getOrderBySQL($orderBy, $this->getSQLTableAlias($this->class->getTableName())); |
1121
|
|
|
} |
1122
|
|
|
|
1123
|
480 |
|
$conditionSql = ($criteria instanceof Criteria) |
1124
|
6 |
|
? $this->getSelectConditionCriteriaSQL($criteria) |
1125
|
480 |
|
: $this->getSelectConditionSQL($criteria, $association); |
1126
|
|
|
|
1127
|
|
|
switch ($lockMode) { |
1128
|
480 |
|
case LockMode::PESSIMISTIC_READ: |
|
|
|
|
1129
|
|
|
$lockSql = ' ' . $this->platform->getReadLockSQL(); |
1130
|
|
|
break; |
1131
|
|
|
|
1132
|
480 |
|
case LockMode::PESSIMISTIC_WRITE: |
|
|
|
|
1133
|
|
|
$lockSql = ' ' . $this->platform->getWriteLockSQL(); |
1134
|
|
|
break; |
1135
|
|
|
} |
1136
|
|
|
|
1137
|
480 |
|
$columnList = $this->getSelectColumnsSQL(); |
1138
|
480 |
|
$tableAlias = $this->getSQLTableAlias($this->class->getTableName()); |
1139
|
480 |
|
$filterSql = $this->generateFilterConditionSQL($this->class, $tableAlias); |
1140
|
480 |
|
$tableName = $this->class->table->getQuotedQualifiedName($this->platform); |
1141
|
|
|
|
1142
|
480 |
|
if ($filterSql !== '') { |
1143
|
12 |
|
$conditionSql = $conditionSql |
1144
|
11 |
|
? $conditionSql . ' AND ' . $filterSql |
1145
|
12 |
|
: $filterSql; |
1146
|
|
|
} |
1147
|
|
|
|
1148
|
480 |
|
$select = 'SELECT ' . $columnList; |
1149
|
480 |
|
$from = ' FROM ' . $tableName . ' ' . $tableAlias; |
1150
|
480 |
|
$join = $this->currentPersisterContext->selectJoinSql . $joinSql; |
1151
|
480 |
|
$where = ($conditionSql ? ' WHERE ' . $conditionSql : ''); |
1152
|
480 |
|
$lock = $this->platform->appendLockHint($from, $lockMode); |
1153
|
|
|
$query = $select |
1154
|
480 |
|
. $lock |
1155
|
480 |
|
. $join |
1156
|
480 |
|
. $where |
1157
|
480 |
|
. $orderBySql; |
1158
|
|
|
|
1159
|
480 |
|
return $this->platform->modifyLimitQuery($query, $limit, $offset) . $lockSql; |
1160
|
|
|
} |
1161
|
|
|
|
1162
|
|
|
/** |
1163
|
|
|
* {@inheritDoc} |
1164
|
|
|
*/ |
1165
|
41 |
|
public function getCountSQL($criteria = []) |
1166
|
|
|
{ |
1167
|
41 |
|
$tableName = $this->class->table->getQuotedQualifiedName($this->platform); |
1168
|
41 |
|
$tableAlias = $this->getSQLTableAlias($this->class->getTableName()); |
1169
|
|
|
|
1170
|
41 |
|
$conditionSql = ($criteria instanceof Criteria) |
1171
|
25 |
|
? $this->getSelectConditionCriteriaSQL($criteria) |
1172
|
41 |
|
: $this->getSelectConditionSQL($criteria); |
1173
|
|
|
|
1174
|
41 |
|
$filterSql = $this->generateFilterConditionSQL($this->class, $tableAlias); |
1175
|
|
|
|
1176
|
41 |
|
if ($filterSql !== '') { |
1177
|
2 |
|
$conditionSql = $conditionSql |
1178
|
2 |
|
? $conditionSql . ' AND ' . $filterSql |
1179
|
2 |
|
: $filterSql; |
1180
|
|
|
} |
1181
|
|
|
|
1182
|
|
|
$sql = 'SELECT COUNT(*) ' |
1183
|
41 |
|
. 'FROM ' . $tableName . ' ' . $tableAlias |
1184
|
41 |
|
. (empty($conditionSql) ? '' : ' WHERE ' . $conditionSql); |
1185
|
|
|
|
1186
|
41 |
|
return $sql; |
1187
|
|
|
} |
1188
|
|
|
|
1189
|
|
|
/** |
1190
|
|
|
* Gets the ORDER BY SQL snippet for ordered collections. |
1191
|
|
|
* |
1192
|
|
|
* @param mixed[] $orderBy |
1193
|
|
|
* @param string $baseTableAlias |
1194
|
|
|
* |
1195
|
|
|
* @return string |
1196
|
|
|
* |
1197
|
|
|
* @throws \Doctrine\ORM\ORMException |
1198
|
|
|
*/ |
1199
|
76 |
|
final protected function getOrderBySQL(array $orderBy, $baseTableAlias) |
1200
|
|
|
{ |
1201
|
76 |
|
if (! $orderBy) { |
1202
|
66 |
|
return ''; |
1203
|
|
|
} |
1204
|
|
|
|
1205
|
10 |
|
$orderByList = []; |
1206
|
|
|
|
1207
|
10 |
|
foreach ($orderBy as $fieldName => $orientation) { |
1208
|
10 |
|
$orientation = strtoupper(trim($orientation)); |
1209
|
|
|
|
1210
|
10 |
|
if (! in_array($orientation, ['ASC', 'DESC'], true)) { |
1211
|
|
|
throw ORMException::invalidOrientation($this->class->getClassName(), $fieldName); |
1212
|
|
|
} |
1213
|
|
|
|
1214
|
10 |
|
$property = $this->class->getProperty($fieldName); |
1215
|
|
|
|
1216
|
10 |
|
if ($property instanceof FieldMetadata) { |
1217
|
9 |
|
$tableAlias = $this->getSQLTableAlias($property->getTableName()); |
1218
|
9 |
|
$columnName = $this->platform->quoteIdentifier($property->getColumnName()); |
1219
|
|
|
|
1220
|
9 |
|
$orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation; |
1221
|
|
|
|
1222
|
9 |
|
continue; |
1223
|
1 |
|
} elseif ($property instanceof AssociationMetadata) { |
1224
|
1 |
|
if (! $property->isOwningSide()) { |
1225
|
|
|
throw ORMException::invalidFindByInverseAssociation($this->class->getClassName(), $fieldName); |
1226
|
|
|
} |
1227
|
|
|
|
1228
|
1 |
|
$class = $this->class->isInheritedProperty($fieldName) |
1229
|
|
|
? $property->getDeclaringClass() |
1230
|
1 |
|
: $this->class; |
1231
|
1 |
|
$tableAlias = $this->getSQLTableAlias($class->getTableName()); |
1232
|
|
|
|
1233
|
1 |
|
foreach ($property->getJoinColumns() as $joinColumn) { |
1234
|
|
|
/* @var JoinColumnMetadata $joinColumn */ |
1235
|
1 |
|
$quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
1236
|
|
|
|
1237
|
1 |
|
$orderByList[] = $tableAlias . '.' . $quotedColumnName . ' ' . $orientation; |
1238
|
|
|
} |
1239
|
|
|
|
1240
|
1 |
|
continue; |
1241
|
|
|
} |
1242
|
|
|
|
1243
|
|
|
throw ORMException::unrecognizedField($fieldName); |
1244
|
|
|
} |
1245
|
|
|
|
1246
|
10 |
|
return ' ORDER BY ' . implode(', ', $orderByList); |
1247
|
|
|
} |
1248
|
|
|
|
1249
|
|
|
/** |
1250
|
|
|
* Gets the SQL fragment with the list of columns to select when querying for |
1251
|
|
|
* an entity in this persister. |
1252
|
|
|
* |
1253
|
|
|
* Subclasses should override this method to alter or change the select column |
1254
|
|
|
* list SQL fragment. Note that in the implementation of BasicEntityPersister |
1255
|
|
|
* the resulting SQL fragment is generated only once and cached in {@link selectColumnListSql}. |
1256
|
|
|
* Subclasses may or may not do the same. |
1257
|
|
|
* |
1258
|
|
|
* @return string The SQL fragment. |
1259
|
|
|
*/ |
1260
|
481 |
|
protected function getSelectColumnsSQL() |
1261
|
|
|
{ |
1262
|
481 |
|
if ($this->currentPersisterContext->selectColumnListSql !== null) { |
1263
|
95 |
|
return $this->currentPersisterContext->selectColumnListSql; |
1264
|
|
|
} |
1265
|
|
|
|
1266
|
481 |
|
$this->currentPersisterContext->rsm->addEntityResult($this->class->getClassName(), 'r'); // r for root |
1267
|
481 |
|
$this->currentPersisterContext->selectJoinSql = ''; |
1268
|
|
|
|
1269
|
481 |
|
$eagerAliasCounter = 0; |
1270
|
481 |
|
$columnList = []; |
1271
|
|
|
|
1272
|
481 |
|
foreach ($this->class->getDeclaredPropertiesIterator() as $fieldName => $property) { |
1273
|
|
|
switch (true) { |
1274
|
481 |
|
case ($property instanceof FieldMetadata): |
1275
|
479 |
|
$columnList[] = $this->getSelectColumnSQL($fieldName, $this->class); |
1276
|
479 |
|
break; |
1277
|
|
|
|
1278
|
431 |
|
case ($property instanceof AssociationMetadata): |
1279
|
429 |
|
$assocColumnSQL = $this->getSelectColumnAssociationSQL($fieldName, $property, $this->class); |
1280
|
|
|
|
1281
|
429 |
|
if ($assocColumnSQL) { |
1282
|
366 |
|
$columnList[] = $assocColumnSQL; |
1283
|
|
|
} |
1284
|
|
|
|
1285
|
429 |
|
$isAssocToOneInverseSide = $property instanceof ToOneAssociationMetadata && ! $property->isOwningSide(); |
1286
|
429 |
|
$isAssocFromOneEager = ! $property instanceof ManyToManyAssociationMetadata && $property->getFetchMode() === FetchMode::EAGER; |
1287
|
|
|
|
1288
|
429 |
|
if (! ($isAssocFromOneEager || $isAssocToOneInverseSide)) { |
1289
|
415 |
|
break; |
1290
|
|
|
} |
1291
|
|
|
|
1292
|
163 |
|
if ($property instanceof ToManyAssociationMetadata && $this->currentPersisterContext->handlesLimits) { |
1293
|
3 |
|
break; |
1294
|
|
|
} |
1295
|
|
|
|
1296
|
160 |
|
$targetEntity = $property->getTargetEntity(); |
1297
|
160 |
|
$eagerEntity = $this->em->getClassMetadata($targetEntity); |
1298
|
|
|
|
1299
|
160 |
|
if ($eagerEntity->inheritanceType !== InheritanceType::NONE) { |
1300
|
5 |
|
break; // now this is why you shouldn't use inheritance |
1301
|
|
|
} |
1302
|
|
|
|
1303
|
155 |
|
$assocAlias = 'e' . ($eagerAliasCounter++); |
1304
|
|
|
|
1305
|
155 |
|
$this->currentPersisterContext->rsm->addJoinedEntityResult($targetEntity, $assocAlias, 'r', $fieldName); |
1306
|
|
|
|
1307
|
155 |
|
foreach ($eagerEntity->getDeclaredPropertiesIterator() as $eagerProperty) { |
1308
|
|
|
switch (true) { |
1309
|
155 |
|
case ($eagerProperty instanceof FieldMetadata): |
1310
|
153 |
|
$columnList[] = $this->getSelectColumnSQL($eagerProperty->getName(), $eagerEntity, $assocAlias); |
1311
|
153 |
|
break; |
1312
|
|
|
|
1313
|
152 |
|
case ($eagerProperty instanceof ToOneAssociationMetadata && $eagerProperty->isOwningSide()): |
1314
|
150 |
|
$columnList[] = $this->getSelectColumnAssociationSQL( |
1315
|
150 |
|
$eagerProperty->getName(), |
1316
|
150 |
|
$eagerProperty, |
1317
|
150 |
|
$eagerEntity, |
1318
|
150 |
|
$assocAlias |
1319
|
|
|
); |
1320
|
155 |
|
break; |
1321
|
|
|
} |
1322
|
|
|
} |
1323
|
|
|
|
1324
|
155 |
|
$owningAssociation = $property; |
1325
|
155 |
|
$joinCondition = []; |
1326
|
|
|
|
1327
|
155 |
|
if ($property instanceof ToManyAssociationMetadata && $property->getIndexedBy()) { |
1328
|
1 |
|
$this->currentPersisterContext->rsm->addIndexBy($assocAlias, $property->getIndexedBy()); |
1329
|
|
|
} |
1330
|
|
|
|
1331
|
155 |
|
if (! $property->isOwningSide()) { |
1332
|
150 |
|
$owningAssociation = $eagerEntity->getProperty($property->getMappedBy()); |
1333
|
|
|
} |
1334
|
|
|
|
1335
|
155 |
|
$joinTableAlias = $this->getSQLTableAlias($eagerEntity->getTableName(), $assocAlias); |
1336
|
155 |
|
$joinTableName = $eagerEntity->table->getQuotedQualifiedName($this->platform); |
1337
|
|
|
|
1338
|
155 |
|
$this->currentPersisterContext->selectJoinSql .= ' ' . $this->getJoinSQLForAssociation($property); |
1339
|
|
|
|
1340
|
155 |
|
$sourceClass = $this->em->getClassMetadata($owningAssociation->getSourceEntity()); |
1341
|
155 |
|
$targetClass = $this->em->getClassMetadata($owningAssociation->getTargetEntity()); |
1342
|
155 |
|
$targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $property->isOwningSide() ? $assocAlias : ''); |
1343
|
155 |
|
$sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $property->isOwningSide() ? '' : $assocAlias); |
1344
|
|
|
|
1345
|
155 |
|
foreach ($owningAssociation->getJoinColumns() as $joinColumn) { |
1346
|
155 |
|
$joinCondition[] = sprintf( |
1347
|
155 |
|
'%s.%s = %s.%s', |
1348
|
155 |
|
$sourceTableAlias, |
1349
|
155 |
|
$this->platform->quoteIdentifier($joinColumn->getColumnName()), |
1350
|
155 |
|
$targetTableAlias, |
1351
|
155 |
|
$this->platform->quoteIdentifier($joinColumn->getReferencedColumnName()) |
1352
|
|
|
); |
1353
|
|
|
} |
1354
|
|
|
|
1355
|
155 |
|
$filterSql = $this->generateFilterConditionSQL($eagerEntity, $targetTableAlias); |
1356
|
|
|
|
1357
|
|
|
// Add filter SQL |
1358
|
155 |
|
if ($filterSql) { |
1359
|
|
|
$joinCondition[] = $filterSql; |
1360
|
|
|
} |
1361
|
|
|
|
1362
|
155 |
|
$this->currentPersisterContext->selectJoinSql .= ' ' . $joinTableName . ' ' . $joinTableAlias . ' ON '; |
1363
|
155 |
|
$this->currentPersisterContext->selectJoinSql .= implode(' AND ', $joinCondition); |
1364
|
|
|
|
1365
|
481 |
|
break; |
1366
|
|
|
} |
1367
|
|
|
} |
1368
|
|
|
|
1369
|
481 |
|
$this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList); |
1370
|
|
|
|
1371
|
481 |
|
return $this->currentPersisterContext->selectColumnListSql; |
1372
|
|
|
} |
1373
|
|
|
|
1374
|
|
|
/** |
1375
|
|
|
* Gets the SQL join fragment used when selecting entities from an association. |
1376
|
|
|
* |
1377
|
|
|
* @param string $field |
1378
|
|
|
* @param string $alias |
1379
|
|
|
* |
1380
|
|
|
* @return string |
1381
|
|
|
*/ |
1382
|
429 |
|
protected function getSelectColumnAssociationSQL($field, AssociationMetadata $association, ClassMetadata $class, $alias = 'r') |
1383
|
|
|
{ |
1384
|
429 |
|
if (! ($association->isOwningSide() && $association instanceof ToOneAssociationMetadata)) { |
1385
|
352 |
|
return ''; |
1386
|
|
|
} |
1387
|
|
|
|
1388
|
378 |
|
$columnList = []; |
1389
|
378 |
|
$targetClass = $this->em->getClassMetadata($association->getTargetEntity()); |
1390
|
378 |
|
$sqlTableAlias = $this->getSQLTableAlias($class->getTableName(), ($alias === 'r' ? '' : $alias)); |
1391
|
|
|
|
1392
|
378 |
|
foreach ($association->getJoinColumns() as $joinColumn) { |
1393
|
|
|
/** @var JoinColumnMetadata $joinColumn */ |
1394
|
378 |
|
$columnName = $joinColumn->getColumnName(); |
1395
|
378 |
|
$quotedColumnName = $this->platform->quoteIdentifier($columnName); |
1396
|
378 |
|
$referencedColumnName = $joinColumn->getReferencedColumnName(); |
1397
|
378 |
|
$resultColumnName = $this->getSQLColumnAlias(); |
1398
|
|
|
|
1399
|
378 |
|
if (! $joinColumn->getType()) { |
1400
|
9 |
|
$joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em)); |
1401
|
|
|
} |
1402
|
|
|
|
1403
|
378 |
|
$this->currentPersisterContext->rsm->addMetaResult( |
1404
|
378 |
|
$alias, |
1405
|
378 |
|
$resultColumnName, |
1406
|
378 |
|
$columnName, |
1407
|
378 |
|
$association->isPrimaryKey(), |
1408
|
378 |
|
$joinColumn->getType() |
1409
|
|
|
); |
1410
|
|
|
|
1411
|
378 |
|
$columnList[] = sprintf('%s.%s AS %s', $sqlTableAlias, $quotedColumnName, $resultColumnName); |
1412
|
|
|
} |
1413
|
|
|
|
1414
|
378 |
|
return implode(', ', $columnList); |
1415
|
|
|
} |
1416
|
|
|
|
1417
|
|
|
/** |
1418
|
|
|
* Gets the SQL join fragment used when selecting entities from a |
1419
|
|
|
* many-to-many association. |
1420
|
|
|
* |
1421
|
|
|
* @param ManyToManyAssociationMetadata $manyToMany |
1422
|
|
|
* |
1423
|
|
|
* @return string |
1424
|
|
|
*/ |
1425
|
80 |
|
protected function getSelectManyToManyJoinSQL(ManyToManyAssociationMetadata $association) |
1426
|
|
|
{ |
1427
|
80 |
|
$conditions = []; |
1428
|
80 |
|
$owningAssociation = $association; |
1429
|
80 |
|
$sourceTableAlias = $this->getSQLTableAlias($this->class->getTableName()); |
1430
|
|
|
|
1431
|
80 |
|
if (! $association->isOwningSide()) { |
1432
|
13 |
|
$targetEntity = $this->em->getClassMetadata($association->getTargetEntity()); |
1433
|
13 |
|
$owningAssociation = $targetEntity->getProperty($association->getMappedBy()); |
1434
|
|
|
} |
1435
|
|
|
|
1436
|
80 |
|
$joinTable = $owningAssociation->getJoinTable(); |
1437
|
80 |
|
$joinTableName = $joinTable->getQuotedQualifiedName($this->platform); |
1438
|
80 |
|
$joinColumns = $association->isOwningSide() |
1439
|
72 |
|
? $joinTable->getInverseJoinColumns() |
1440
|
80 |
|
: $joinTable->getJoinColumns() |
1441
|
|
|
; |
1442
|
|
|
|
1443
|
80 |
|
foreach ($joinColumns as $joinColumn) { |
1444
|
80 |
|
$conditions[] = sprintf( |
1445
|
80 |
|
'%s.%s = %s.%s', |
1446
|
80 |
|
$sourceTableAlias, |
1447
|
80 |
|
$this->platform->quoteIdentifier($joinColumn->getReferencedColumnName()), |
1448
|
80 |
|
$joinTableName, |
1449
|
80 |
|
$this->platform->quoteIdentifier($joinColumn->getColumnName()) |
1450
|
|
|
); |
1451
|
|
|
} |
1452
|
|
|
|
1453
|
80 |
|
return ' INNER JOIN ' . $joinTableName . ' ON ' . implode(' AND ', $conditions); |
1454
|
|
|
} |
1455
|
|
|
|
1456
|
|
|
/** |
1457
|
|
|
* {@inheritdoc} |
1458
|
|
|
*/ |
1459
|
927 |
|
public function getInsertSQL() |
1460
|
|
|
{ |
1461
|
927 |
|
if ($this->insertSql !== null) { |
1462
|
639 |
|
return $this->insertSql; |
1463
|
|
|
} |
1464
|
|
|
|
1465
|
927 |
|
$columns = $this->getInsertColumnList(); |
1466
|
927 |
|
$tableName = $this->class->table->getQuotedQualifiedName($this->platform); |
1467
|
|
|
|
1468
|
927 |
|
if (empty($columns)) { |
1469
|
93 |
|
$property = $this->class->getProperty($this->class->identifier[0]); |
1470
|
93 |
|
$identityColumn = $this->platform->quoteIdentifier($property->getColumnName()); |
1471
|
|
|
|
1472
|
93 |
|
$this->insertSql = $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn); |
1473
|
|
|
|
1474
|
93 |
|
return $this->insertSql; |
1475
|
|
|
} |
1476
|
|
|
|
1477
|
909 |
|
$quotedColumns = []; |
1478
|
909 |
|
$values = []; |
1479
|
|
|
|
1480
|
909 |
|
foreach ($columns as $columnName) { |
1481
|
909 |
|
$column = $this->columns[$columnName]; |
1482
|
|
|
|
1483
|
909 |
|
$quotedColumns[] = $this->platform->quoteIdentifier($column->getColumnName()); |
1484
|
909 |
|
$values[] = $column->getType()->convertToDatabaseValueSQL('?', $this->platform); |
1485
|
|
|
} |
1486
|
|
|
|
1487
|
909 |
|
$quotedColumns = implode(', ', $quotedColumns); |
1488
|
909 |
|
$values = implode(', ', $values); |
1489
|
|
|
|
1490
|
909 |
|
$this->insertSql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $quotedColumns, $values); |
1491
|
|
|
|
1492
|
909 |
|
return $this->insertSql; |
1493
|
|
|
} |
1494
|
|
|
|
1495
|
|
|
/** |
1496
|
|
|
* Gets the list of columns to put in the INSERT SQL statement. |
1497
|
|
|
* |
1498
|
|
|
* Subclasses should override this method to alter or change the list of |
1499
|
|
|
* columns placed in the INSERT statements used by the persister. |
1500
|
|
|
* |
1501
|
|
|
* @return string[] The list of columns. |
1502
|
|
|
*/ |
1503
|
847 |
|
protected function getInsertColumnList() |
1504
|
|
|
{ |
1505
|
847 |
|
$columns = []; |
1506
|
847 |
|
$versionPropertyName = $this->class->isVersioned() |
1507
|
188 |
|
? $this->class->versionProperty->getName() |
1508
|
847 |
|
: null |
1509
|
|
|
; |
1510
|
|
|
|
1511
|
847 |
|
foreach ($this->class->getDeclaredPropertiesIterator() as $name => $property) { |
1512
|
|
|
/*if (isset($this->class->embeddedClasses[$name])) { |
|
|
|
|
1513
|
|
|
continue; |
1514
|
|
|
}*/ |
1515
|
|
|
|
1516
|
|
|
switch (true) { |
1517
|
847 |
|
case ($property instanceof VersionFieldMetadata): |
1518
|
|
|
// Do nothing |
1519
|
188 |
|
break; |
1520
|
|
|
|
1521
|
847 |
|
case ($property instanceof LocalColumnMetadata): |
1522
|
847 |
|
if (($property instanceof FieldMetadata |
1523
|
|
|
&& ( |
1524
|
847 |
|
! $property->hasValueGenerator() |
1525
|
847 |
|
|| $property->getValueGenerator()->getType() !== GeneratorType::IDENTITY |
1526
|
|
|
) |
1527
|
|
|
) |
1528
|
847 |
|
|| $this->class->identifier[0] !== $name |
1529
|
|
|
) { |
1530
|
791 |
|
$columnName = $property->getColumnName(); |
1531
|
|
|
|
1532
|
791 |
|
$columns[] = $columnName; |
1533
|
|
|
|
1534
|
791 |
|
$this->columns[$columnName] = $property; |
1535
|
|
|
} |
1536
|
|
|
|
1537
|
847 |
|
break; |
1538
|
|
|
|
1539
|
752 |
|
case ($property instanceof AssociationMetadata): |
1540
|
750 |
|
if ($property->isOwningSide() && $property instanceof ToOneAssociationMetadata) { |
1541
|
712 |
|
$targetClass = $this->em->getClassMetadata($property->getTargetEntity()); |
1542
|
|
|
|
1543
|
712 |
|
foreach ($property->getJoinColumns() as $joinColumn) { |
1544
|
|
|
/** @var JoinColumnMetadata $joinColumn */ |
1545
|
712 |
|
$columnName = $joinColumn->getColumnName(); |
1546
|
712 |
|
$referencedColumnName = $joinColumn->getReferencedColumnName(); |
1547
|
|
|
|
1548
|
712 |
|
if (! $joinColumn->getType()) { |
1549
|
112 |
|
$joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em)); |
1550
|
|
|
} |
1551
|
|
|
|
1552
|
712 |
|
$columns[] = $columnName; |
1553
|
|
|
|
1554
|
712 |
|
$this->columns[$columnName] = $joinColumn; |
1555
|
|
|
} |
1556
|
|
|
} |
1557
|
|
|
|
1558
|
847 |
|
break; |
1559
|
|
|
} |
1560
|
|
|
} |
1561
|
|
|
|
1562
|
847 |
|
return $columns; |
1563
|
|
|
} |
1564
|
|
|
|
1565
|
|
|
/** |
1566
|
|
|
* Gets the SQL snippet of a qualified column name for the given field name. |
1567
|
|
|
* |
1568
|
|
|
* @param string $field The field name. |
1569
|
|
|
* @param ClassMetadata $class The class that declares this field. The table this class is |
1570
|
|
|
* mapped to must own the column for the given field. |
1571
|
|
|
* @param string $alias |
1572
|
|
|
* |
1573
|
|
|
* @return string |
1574
|
|
|
*/ |
1575
|
514 |
|
protected function getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r') |
1576
|
|
|
{ |
1577
|
514 |
|
$property = $class->getProperty($field); |
1578
|
514 |
|
$columnAlias = $this->getSQLColumnAlias(); |
1579
|
514 |
|
$sql = sprintf( |
1580
|
514 |
|
'%s.%s', |
1581
|
514 |
|
$this->getSQLTableAlias($property->getTableName(), ($alias === 'r' ? '' : $alias)), |
1582
|
514 |
|
$this->platform->quoteIdentifier($property->getColumnName()) |
1583
|
|
|
); |
1584
|
|
|
|
1585
|
514 |
|
$this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field, $class->getClassName()); |
1586
|
|
|
|
1587
|
514 |
|
return $property->getType()->convertToPHPValueSQL($sql, $this->platform) . ' AS ' . $columnAlias; |
1588
|
|
|
} |
1589
|
|
|
|
1590
|
|
|
/** |
1591
|
|
|
* Gets the SQL table alias for the given class name. |
1592
|
|
|
* |
1593
|
|
|
* @param string $tableName |
1594
|
|
|
* @param string $assocName |
1595
|
|
|
* |
1596
|
|
|
* @return string The SQL table alias. |
1597
|
|
|
*/ |
1598
|
545 |
|
protected function getSQLTableAlias($tableName, $assocName = '') |
1599
|
|
|
{ |
1600
|
545 |
|
if ($tableName) { |
1601
|
545 |
|
$tableName .= '#' . $assocName; |
1602
|
|
|
} |
1603
|
|
|
|
1604
|
545 |
|
if (isset($this->currentPersisterContext->sqlTableAliases[$tableName])) { |
1605
|
540 |
|
return $this->currentPersisterContext->sqlTableAliases[$tableName]; |
1606
|
|
|
} |
1607
|
|
|
|
1608
|
545 |
|
$tableAlias = 't' . $this->currentPersisterContext->sqlAliasCounter++; |
1609
|
|
|
|
1610
|
545 |
|
$this->currentPersisterContext->sqlTableAliases[$tableName] = $tableAlias; |
1611
|
|
|
|
1612
|
545 |
|
return $tableAlias; |
1613
|
|
|
} |
1614
|
|
|
|
1615
|
|
|
/** |
1616
|
|
|
* {@inheritdoc} |
1617
|
|
|
*/ |
1618
|
|
|
public function lock(array $criteria, $lockMode) |
1619
|
|
|
{ |
1620
|
|
|
$lockSql = ''; |
1621
|
|
|
$conditionSql = $this->getSelectConditionSQL($criteria); |
1622
|
|
|
|
1623
|
|
|
switch ($lockMode) { |
1624
|
|
|
case LockMode::PESSIMISTIC_READ: |
1625
|
|
|
$lockSql = $this->platform->getReadLockSQL(); |
1626
|
|
|
|
1627
|
|
|
break; |
1628
|
|
|
case LockMode::PESSIMISTIC_WRITE: |
1629
|
|
|
$lockSql = $this->platform->getWriteLockSQL(); |
1630
|
|
|
break; |
1631
|
|
|
} |
1632
|
|
|
|
1633
|
|
|
$lock = $this->getLockTablesSql($lockMode); |
1634
|
|
|
$where = ($conditionSql ? ' WHERE ' . $conditionSql : '') . ' '; |
1635
|
|
|
$sql = 'SELECT 1 ' |
1636
|
|
|
. $lock |
1637
|
|
|
. $where |
1638
|
|
|
. $lockSql; |
1639
|
|
|
|
1640
|
|
|
list($params, $types) = $this->expandParameters($criteria); |
1641
|
|
|
|
1642
|
|
|
$this->conn->executeQuery($sql, $params, $types); |
1643
|
|
|
} |
1644
|
|
|
|
1645
|
|
|
/** |
1646
|
|
|
* Gets the FROM and optionally JOIN conditions to lock the entity managed by this persister. |
1647
|
|
|
* |
1648
|
|
|
* @param int $lockMode One of the Doctrine\DBAL\LockMode::* constants. |
1649
|
|
|
* |
1650
|
|
|
* @return string |
1651
|
|
|
*/ |
1652
|
13 |
|
protected function getLockTablesSql($lockMode) |
1653
|
|
|
{ |
1654
|
13 |
|
$tableName = $this->class->table->getQuotedQualifiedName($this->platform); |
1655
|
|
|
|
1656
|
13 |
|
return $this->platform->appendLockHint( |
1657
|
13 |
|
'FROM ' . $tableName . ' ' . $this->getSQLTableAlias($this->class->getTableName()), |
1658
|
13 |
|
$lockMode |
1659
|
|
|
); |
1660
|
|
|
} |
1661
|
|
|
|
1662
|
|
|
/** |
1663
|
|
|
* Gets the Select Where Condition from a Criteria object. |
1664
|
|
|
* |
1665
|
|
|
* @return string |
1666
|
|
|
*/ |
1667
|
37 |
|
protected function getSelectConditionCriteriaSQL(Criteria $criteria) |
1668
|
|
|
{ |
1669
|
37 |
|
$expression = $criteria->getWhereExpression(); |
1670
|
|
|
|
1671
|
37 |
|
if ($expression === null) { |
1672
|
2 |
|
return ''; |
1673
|
|
|
} |
1674
|
|
|
|
1675
|
36 |
|
$visitor = new SqlExpressionVisitor($this, $this->class); |
1676
|
|
|
|
1677
|
36 |
|
return $visitor->dispatch($expression); |
1678
|
|
|
} |
1679
|
|
|
|
1680
|
|
|
/** |
1681
|
|
|
* {@inheritdoc} |
1682
|
|
|
*/ |
1683
|
523 |
|
public function getSelectConditionStatementSQL( |
1684
|
|
|
$field, |
1685
|
|
|
$value, |
1686
|
|
|
?AssociationMetadata $association = null, |
1687
|
|
|
$comparison = null |
1688
|
|
|
) { |
1689
|
523 |
|
$selectedColumns = []; |
1690
|
523 |
|
$columns = $this->getSelectConditionStatementColumnSQL($field, $association); |
1691
|
|
|
|
1692
|
523 |
|
if (in_array($comparison, [Comparison::IN, Comparison::NIN], true) && isset($columns[1])) { |
1693
|
|
|
// @todo try to support multi-column IN expressions. Example: (col1, col2) IN (('val1A', 'val2A'), ...) |
1694
|
|
|
throw ORMException::cantUseInOperatorOnCompositeKeys(); |
1695
|
|
|
} |
1696
|
|
|
|
1697
|
523 |
|
foreach ($columns as $column) { |
1698
|
523 |
|
$property = $this->class->getProperty($field); |
1699
|
523 |
|
$placeholder = '?'; |
1700
|
|
|
|
1701
|
523 |
|
if ($property instanceof FieldMetadata) { |
1702
|
440 |
|
$placeholder = $property->getType()->convertToDatabaseValueSQL($placeholder, $this->platform); |
1703
|
|
|
} |
1704
|
|
|
|
1705
|
523 |
|
if ($comparison !== null) { |
1706
|
|
|
// special case null value handling |
1707
|
42 |
|
if (($comparison === Comparison::EQ || $comparison === Comparison::IS) && $value ===null) { |
1708
|
6 |
|
$selectedColumns[] = $column . ' IS NULL'; |
1709
|
|
|
|
1710
|
6 |
|
continue; |
1711
|
|
|
} |
1712
|
|
|
|
1713
|
36 |
|
if ($comparison === Comparison::NEQ && $value === null) { |
1714
|
3 |
|
$selectedColumns[] = $column . ' IS NOT NULL'; |
1715
|
|
|
|
1716
|
3 |
|
continue; |
1717
|
|
|
} |
1718
|
|
|
|
1719
|
33 |
|
$selectedColumns[] = $column . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder); |
1720
|
|
|
|
1721
|
33 |
|
continue; |
1722
|
|
|
} |
1723
|
|
|
|
1724
|
496 |
|
if (is_array($value)) { |
1725
|
10 |
|
$in = sprintf('%s IN (%s)', $column, $placeholder); |
1726
|
|
|
|
1727
|
10 |
|
if (array_search(null, $value, true) !== false) { |
1728
|
4 |
|
$selectedColumns[] = sprintf('(%s OR %s IS NULL)', $in, $column); |
1729
|
|
|
|
1730
|
4 |
|
continue; |
1731
|
|
|
} |
1732
|
|
|
|
1733
|
6 |
|
$selectedColumns[] = $in; |
1734
|
|
|
|
1735
|
6 |
|
continue; |
1736
|
|
|
} |
1737
|
|
|
|
1738
|
486 |
|
if ($value === null) { |
1739
|
9 |
|
$selectedColumns[] = sprintf('%s IS NULL', $column); |
1740
|
|
|
|
1741
|
9 |
|
continue; |
1742
|
|
|
} |
1743
|
|
|
|
1744
|
478 |
|
$selectedColumns[] = sprintf('%s = %s', $column, $placeholder); |
1745
|
|
|
} |
1746
|
|
|
|
1747
|
523 |
|
return implode(' AND ', $selectedColumns); |
1748
|
|
|
} |
1749
|
|
|
|
1750
|
|
|
/** |
1751
|
|
|
* Builds the left-hand-side of a where condition statement. |
1752
|
|
|
* |
1753
|
|
|
* @param string $field |
1754
|
|
|
* |
1755
|
|
|
* @return string[] |
1756
|
|
|
* |
1757
|
|
|
* @throws \Doctrine\ORM\ORMException |
1758
|
|
|
*/ |
1759
|
523 |
|
private function getSelectConditionStatementColumnSQL($field, ?AssociationMetadata $association = null) |
1760
|
|
|
{ |
1761
|
523 |
|
$property = $this->class->getProperty($field); |
1762
|
|
|
|
1763
|
523 |
|
if ($property instanceof FieldMetadata) { |
1764
|
440 |
|
$tableAlias = $this->getSQLTableAlias($property->getTableName()); |
1765
|
440 |
|
$columnName = $this->platform->quoteIdentifier($property->getColumnName()); |
1766
|
|
|
|
1767
|
440 |
|
return [$tableAlias . '.' . $columnName]; |
1768
|
|
|
} |
1769
|
|
|
|
1770
|
266 |
|
if ($property instanceof AssociationMetadata) { |
1771
|
135 |
|
$owningAssociation = $property; |
1772
|
135 |
|
$columns = []; |
1773
|
|
|
|
1774
|
|
|
// Many-To-Many requires join table check for joinColumn |
1775
|
135 |
|
if ($owningAssociation instanceof ManyToManyAssociationMetadata) { |
1776
|
3 |
|
if (! $owningAssociation->isOwningSide()) { |
1777
|
2 |
|
$owningAssociation = $association; |
1778
|
|
|
} |
1779
|
|
|
|
1780
|
3 |
|
$joinTable = $owningAssociation->getJoinTable(); |
1781
|
3 |
|
$joinTableName = $joinTable->getQuotedQualifiedName($this->platform); |
1782
|
3 |
|
$joinColumns = $association->isOwningSide() |
1783
|
2 |
|
? $joinTable->getJoinColumns() |
1784
|
3 |
|
: $joinTable->getInverseJoinColumns() |
1785
|
|
|
; |
1786
|
|
|
|
1787
|
3 |
|
foreach ($joinColumns as $joinColumn) { |
1788
|
3 |
|
$quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
1789
|
|
|
|
1790
|
3 |
|
$columns[] = $joinTableName . '.' . $quotedColumnName; |
1791
|
|
|
} |
1792
|
|
|
} else { |
1793
|
133 |
|
if (! $owningAssociation->isOwningSide()) { |
1794
|
|
|
throw ORMException::invalidFindByInverseAssociation($this->class->getClassName(), $field); |
1795
|
|
|
} |
1796
|
|
|
|
1797
|
133 |
|
$class = $this->class->isInheritedProperty($field) |
1798
|
11 |
|
? $owningAssociation->getDeclaringClass() |
1799
|
133 |
|
: $this->class |
1800
|
|
|
; |
1801
|
133 |
|
$tableAlias = $this->getSQLTableAlias($class->getTableName()); |
1802
|
|
|
|
1803
|
133 |
|
foreach ($owningAssociation->getJoinColumns() as $joinColumn) { |
1804
|
133 |
|
$quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
1805
|
|
|
|
1806
|
133 |
|
$columns[] = $tableAlias . '.' . $quotedColumnName; |
1807
|
|
|
} |
1808
|
|
|
} |
1809
|
|
|
|
1810
|
135 |
|
return $columns; |
1811
|
|
|
} |
1812
|
|
|
|
1813
|
145 |
|
if ($association !== null && strpos($field, ' ') === false && strpos($field, '(') === false) { |
1814
|
|
|
// very careless developers could potentially open up this normally hidden api for userland attacks, |
1815
|
|
|
// therefore checking for spaces and function calls which are not allowed. |
1816
|
|
|
|
1817
|
|
|
// found a join column condition, not really a "field" |
1818
|
145 |
|
return [$field]; |
1819
|
|
|
} |
1820
|
|
|
|
1821
|
|
|
throw ORMException::unrecognizedField($field); |
1822
|
|
|
} |
1823
|
|
|
|
1824
|
|
|
/** |
1825
|
|
|
* Gets the conditional SQL fragment used in the WHERE clause when selecting |
1826
|
|
|
* entities in this persister. |
1827
|
|
|
* |
1828
|
|
|
* Subclasses are supposed to override this method if they intend to change |
1829
|
|
|
* or alter the criteria by which entities are selected. |
1830
|
|
|
* |
1831
|
|
|
* @param mixed[] $criteria |
1832
|
|
|
* |
1833
|
|
|
* @return string |
1834
|
|
|
*/ |
1835
|
518 |
|
protected function getSelectConditionSQL(array $criteria, ?AssociationMetadata $association = null) |
1836
|
|
|
{ |
1837
|
518 |
|
$conditions = []; |
1838
|
|
|
|
1839
|
518 |
|
foreach ($criteria as $field => $value) { |
1840
|
495 |
|
$conditions[] = $this->getSelectConditionStatementSQL($field, $value, $association); |
1841
|
|
|
} |
1842
|
|
|
|
1843
|
518 |
|
return implode(' AND ', $conditions); |
1844
|
|
|
} |
1845
|
|
|
|
1846
|
|
|
/** |
1847
|
|
|
* {@inheritdoc} |
1848
|
|
|
*/ |
1849
|
5 |
|
public function getOneToManyCollection( |
1850
|
|
|
OneToManyAssociationMetadata $association, |
1851
|
|
|
$sourceEntity, |
1852
|
|
|
$offset = null, |
1853
|
|
|
$limit = null |
1854
|
|
|
) { |
1855
|
5 |
|
$this->switchPersisterContext($offset, $limit); |
1856
|
|
|
|
1857
|
5 |
|
$stmt = $this->getOneToManyStatement($association, $sourceEntity, $offset, $limit); |
1858
|
|
|
|
1859
|
5 |
|
return $this->loadArrayFromStatement($association, $stmt); |
1860
|
|
|
} |
1861
|
|
|
|
1862
|
|
|
/** |
1863
|
|
|
* {@inheritdoc} |
1864
|
|
|
*/ |
1865
|
67 |
|
public function loadOneToManyCollection( |
1866
|
|
|
OneToManyAssociationMetadata $association, |
1867
|
|
|
$sourceEntity, |
1868
|
|
|
PersistentCollection $collection |
1869
|
|
|
) { |
1870
|
67 |
|
$stmt = $this->getOneToManyStatement($association, $sourceEntity); |
1871
|
|
|
|
1872
|
67 |
|
return $this->loadCollectionFromStatement($association, $stmt, $collection); |
1873
|
|
|
} |
1874
|
|
|
|
1875
|
|
|
/** |
1876
|
|
|
* Builds criteria and execute SQL statement to fetch the one to many entities from. |
1877
|
|
|
* |
1878
|
|
|
* @param object $sourceEntity |
1879
|
|
|
* @param int|null $offset |
1880
|
|
|
* @param int|null $limit |
1881
|
|
|
* |
1882
|
|
|
* @return \Doctrine\DBAL\Statement |
1883
|
|
|
*/ |
1884
|
72 |
|
private function getOneToManyStatement( |
1885
|
|
|
OneToManyAssociationMetadata $association, |
1886
|
|
|
$sourceEntity, |
1887
|
|
|
$offset = null, |
1888
|
|
|
$limit = null |
1889
|
|
|
) { |
1890
|
72 |
|
$this->switchPersisterContext($offset, $limit); |
1891
|
|
|
|
1892
|
72 |
|
$criteria = []; |
1893
|
72 |
|
$parameters = []; |
1894
|
72 |
|
$owningAssoc = $this->class->getProperty($association->getMappedBy()); |
1895
|
72 |
|
$sourceClass = $this->em->getClassMetadata($association->getSourceEntity()); |
1896
|
72 |
|
$class = $owningAssoc->getDeclaringClass(); |
1897
|
72 |
|
$tableAlias = $this->getSQLTableAlias($class->getTableName()); |
1898
|
|
|
|
1899
|
72 |
|
foreach ($owningAssoc->getJoinColumns() as $joinColumn) { |
1900
|
72 |
|
$quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
1901
|
72 |
|
$fieldName = $sourceClass->fieldNames[$joinColumn->getReferencedColumnName()]; |
1902
|
72 |
|
$property = $sourceClass->getProperty($fieldName); |
1903
|
|
|
|
1904
|
72 |
|
if ($property instanceof FieldMetadata) { |
1905
|
72 |
|
$value = $property->getValue($sourceEntity); |
1906
|
3 |
|
} elseif ($property instanceof AssociationMetadata) { |
1907
|
3 |
|
$targetClass = $this->em->getClassMetadata($property->getTargetEntity()); |
1908
|
3 |
|
$value = $property->getValue($sourceEntity); |
1909
|
|
|
|
1910
|
3 |
|
$value = $this->em->getUnitOfWork()->getEntityIdentifier($value); |
1911
|
3 |
|
$value = $value[$targetClass->identifier[0]]; |
1912
|
|
|
} |
1913
|
|
|
|
1914
|
72 |
|
$criteria[$tableAlias . '.' . $quotedColumnName] = $value; |
1915
|
72 |
|
$parameters[] = [ |
1916
|
72 |
|
'value' => $value, |
1917
|
72 |
|
'field' => $fieldName, |
1918
|
72 |
|
'class' => $sourceClass, |
1919
|
|
|
]; |
1920
|
|
|
} |
1921
|
|
|
|
1922
|
72 |
|
$sql = $this->getSelectSQL($criteria, $association, null, $limit, $offset); |
1923
|
72 |
|
list($params, $types) = $this->expandToManyParameters($parameters); |
1924
|
|
|
|
1925
|
72 |
|
return $this->conn->executeQuery($sql, $params, $types); |
1926
|
|
|
} |
1927
|
|
|
|
1928
|
|
|
/** |
1929
|
|
|
* {@inheritdoc} |
1930
|
|
|
*/ |
1931
|
500 |
|
public function expandParameters($criteria) |
1932
|
|
|
{ |
1933
|
500 |
|
$params = []; |
1934
|
500 |
|
$types = []; |
1935
|
|
|
|
1936
|
500 |
|
foreach ($criteria as $field => $value) { |
1937
|
477 |
|
if ($value === null) { |
1938
|
3 |
|
continue; // skip null values. |
1939
|
|
|
} |
1940
|
|
|
|
1941
|
475 |
|
$types = array_merge($types, $this->getTypes($field, $value, $this->class)); |
1942
|
475 |
|
$params = array_merge($params, $this->getValues($value)); |
1943
|
|
|
} |
1944
|
|
|
|
1945
|
500 |
|
return [$params, $types]; |
1946
|
|
|
} |
1947
|
|
|
|
1948
|
|
|
/** |
1949
|
|
|
* Expands the parameters from the given criteria and use the correct binding types if found, |
1950
|
|
|
* specialized for OneToMany or ManyToMany associations. |
1951
|
|
|
* |
1952
|
|
|
* @param mixed[][] $criteria an array of arrays containing following: |
1953
|
|
|
* - field to which each criterion will be bound |
1954
|
|
|
* - value to be bound |
1955
|
|
|
* - class to which the field belongs to |
1956
|
|
|
* |
1957
|
|
|
* |
1958
|
|
|
* @return mixed[][] |
1959
|
|
|
*/ |
1960
|
141 |
|
private function expandToManyParameters($criteria) |
1961
|
|
|
{ |
1962
|
141 |
|
$params = []; |
1963
|
141 |
|
$types = []; |
1964
|
|
|
|
1965
|
141 |
|
foreach ($criteria as $criterion) { |
1966
|
141 |
|
if ($criterion['value'] === null) { |
1967
|
6 |
|
continue; // skip null values. |
1968
|
|
|
} |
1969
|
|
|
|
1970
|
135 |
|
$types = array_merge($types, $this->getTypes($criterion['field'], $criterion['value'], $criterion['class'])); |
1971
|
135 |
|
$params = array_merge($params, $this->getValues($criterion['value'])); |
1972
|
|
|
} |
1973
|
|
|
|
1974
|
141 |
|
return [$params, $types]; |
1975
|
|
|
} |
1976
|
|
|
|
1977
|
|
|
/** |
1978
|
|
|
* Infers field types to be used by parameter type casting. |
1979
|
|
|
* |
1980
|
|
|
* @param string $field |
1981
|
|
|
* @param mixed $value |
1982
|
|
|
* |
1983
|
|
|
* @return mixed[] |
1984
|
|
|
* |
1985
|
|
|
* @throws \Doctrine\ORM\Query\QueryException |
1986
|
|
|
*/ |
1987
|
622 |
|
private function getTypes($field, $value, ClassMetadata $class) |
1988
|
|
|
{ |
1989
|
622 |
|
$property = $class->getProperty($field); |
1990
|
622 |
|
$types = []; |
1991
|
|
|
|
1992
|
|
|
switch (true) { |
1993
|
622 |
|
case ($property instanceof FieldMetadata): |
|
|
|
|
1994
|
568 |
|
$types = array_merge($types, [$property->getType()]); |
1995
|
568 |
|
break; |
1996
|
|
|
|
1997
|
136 |
|
case ($property instanceof AssociationMetadata): |
|
|
|
|
1998
|
135 |
|
$class = $this->em->getClassMetadata($property->getTargetEntity()); |
1999
|
|
|
|
2000
|
135 |
|
if (! $property->isOwningSide()) { |
2001
|
2 |
|
$property = $class->getProperty($property->getMappedBy()); |
2002
|
2 |
|
$class = $this->em->getClassMetadata($property->getTargetEntity()); |
2003
|
|
|
} |
2004
|
|
|
|
2005
|
135 |
|
$joinColumns = $property instanceof ManyToManyAssociationMetadata |
2006
|
3 |
|
? $property->getJoinTable()->getInverseJoinColumns() |
2007
|
135 |
|
: $property->getJoinColumns() |
2008
|
|
|
; |
2009
|
|
|
|
2010
|
135 |
|
foreach ($joinColumns as $joinColumn) { |
2011
|
|
|
/** @var JoinColumnMetadata $joinColumn */ |
2012
|
135 |
|
$referencedColumnName = $joinColumn->getReferencedColumnName(); |
2013
|
|
|
|
2014
|
135 |
|
if (! $joinColumn->getType()) { |
2015
|
1 |
|
$joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $class, $this->em)); |
2016
|
|
|
} |
2017
|
|
|
|
2018
|
135 |
|
$types[] = $joinColumn->getType(); |
2019
|
|
|
} |
2020
|
|
|
|
2021
|
135 |
|
break; |
2022
|
|
|
|
2023
|
|
|
default: |
|
|
|
|
2024
|
1 |
|
$types[] = null; |
2025
|
1 |
|
break; |
2026
|
|
|
} |
2027
|
|
|
|
2028
|
622 |
|
if (is_array($value)) { |
2029
|
12 |
|
return array_map(function ($type) { |
2030
|
12 |
|
return $type->getBindingType() + Connection::ARRAY_PARAM_OFFSET; |
2031
|
12 |
|
}, $types); |
2032
|
|
|
} |
2033
|
|
|
|
2034
|
613 |
|
return $types; |
2035
|
|
|
} |
2036
|
|
|
|
2037
|
|
|
/** |
2038
|
|
|
* Retrieves the parameters that identifies a value. |
2039
|
|
|
* |
2040
|
|
|
* @param mixed $value |
2041
|
|
|
* |
2042
|
|
|
* @return mixed[] |
2043
|
|
|
*/ |
2044
|
505 |
|
private function getValues($value) |
2045
|
|
|
{ |
2046
|
505 |
|
if (is_array($value)) { |
2047
|
12 |
|
$newValue = []; |
2048
|
|
|
|
2049
|
12 |
|
foreach ($value as $itemValue) { |
2050
|
12 |
|
$newValue = array_merge($newValue, $this->getValues($itemValue)); |
2051
|
|
|
} |
2052
|
|
|
|
2053
|
12 |
|
return [$newValue]; |
2054
|
|
|
} |
2055
|
|
|
|
2056
|
505 |
|
$metadataFactory = $this->em->getMetadataFactory(); |
2057
|
505 |
|
$unitOfWork = $this->em->getUnitOfWork(); |
2058
|
|
|
|
2059
|
505 |
|
if (is_object($value) && $metadataFactory->hasMetadataFor(StaticClassNameConverter::getClass($value))) { |
2060
|
45 |
|
$class = $metadataFactory->getMetadataFor(get_class($value)); |
2061
|
45 |
|
$persister = $unitOfWork->getEntityPersister($class->getClassName()); |
2062
|
|
|
|
2063
|
45 |
|
if ($class->isIdentifierComposite()) { |
2064
|
3 |
|
$newValue = []; |
2065
|
|
|
|
2066
|
3 |
|
foreach ($persister->getIdentifier($value) as $innerValue) { |
2067
|
3 |
|
$newValue = array_merge($newValue, $this->getValues($innerValue)); |
2068
|
|
|
} |
2069
|
|
|
|
2070
|
3 |
|
return $newValue; |
2071
|
|
|
} |
2072
|
|
|
} |
2073
|
|
|
|
2074
|
505 |
|
return [$this->getIndividualValue($value)]; |
2075
|
|
|
} |
2076
|
|
|
|
2077
|
|
|
/** |
2078
|
|
|
* Retrieves an individual parameter value. |
2079
|
|
|
* |
2080
|
|
|
* @param mixed $value |
2081
|
|
|
* |
2082
|
|
|
* @return mixed |
2083
|
|
|
*/ |
2084
|
505 |
|
private function getIndividualValue($value) |
2085
|
|
|
{ |
2086
|
505 |
|
if (! is_object($value) || ! $this->em->getMetadataFactory()->hasMetadataFor(StaticClassNameConverter::getClass($value))) { |
2087
|
503 |
|
return $value; |
2088
|
|
|
} |
2089
|
|
|
|
2090
|
45 |
|
return $this->em->getUnitOfWork()->getSingleIdentifierValue($value); |
2091
|
|
|
} |
2092
|
|
|
|
2093
|
|
|
/** |
2094
|
|
|
* {@inheritdoc} |
2095
|
|
|
*/ |
2096
|
14 |
|
public function exists($entity, ?Criteria $extraConditions = null) |
2097
|
|
|
{ |
2098
|
14 |
|
$criteria = $this->getIdentifier($entity); |
2099
|
|
|
|
2100
|
14 |
|
if (! $criteria) { |
2101
|
2 |
|
return false; |
2102
|
|
|
} |
2103
|
|
|
|
2104
|
13 |
|
$alias = $this->getSQLTableAlias($this->class->getTableName()); |
2105
|
|
|
|
2106
|
|
|
$sql = 'SELECT 1 ' |
2107
|
13 |
|
. $this->getLockTablesSql(null) |
2108
|
13 |
|
. ' WHERE ' . $this->getSelectConditionSQL($criteria); |
2109
|
|
|
|
2110
|
13 |
|
list($params, $types) = $this->expandParameters($criteria); |
2111
|
|
|
|
2112
|
13 |
|
if ($extraConditions !== null) { |
2113
|
9 |
|
$sql .= ' AND ' . $this->getSelectConditionCriteriaSQL($extraConditions); |
2114
|
9 |
|
list($criteriaParams, $criteriaTypes) = $this->expandCriteriaParameters($extraConditions); |
2115
|
|
|
|
2116
|
9 |
|
$params = array_merge($params, $criteriaParams); |
2117
|
9 |
|
$types = array_merge($types, $criteriaTypes); |
2118
|
|
|
} |
2119
|
|
|
|
2120
|
13 |
|
$filterSql = $this->generateFilterConditionSQL($this->class, $alias); |
2121
|
|
|
|
2122
|
13 |
|
if ($filterSql) { |
2123
|
3 |
|
$sql .= ' AND ' . $filterSql; |
2124
|
|
|
} |
2125
|
|
|
|
2126
|
13 |
|
return (bool) $this->conn->fetchColumn($sql, $params, 0, $types); |
2127
|
|
|
} |
2128
|
|
|
|
2129
|
|
|
/** |
2130
|
|
|
* Generates the appropriate join SQL for the given association. |
2131
|
|
|
* |
2132
|
|
|
* @return string LEFT JOIN if one of the columns is nullable, INNER JOIN otherwise. |
2133
|
|
|
*/ |
2134
|
155 |
|
protected function getJoinSQLForAssociation(AssociationMetadata $association) |
2135
|
|
|
{ |
2136
|
155 |
|
if (! $association->isOwningSide()) { |
2137
|
150 |
|
return 'LEFT JOIN'; |
2138
|
|
|
} |
2139
|
|
|
|
2140
|
|
|
// if one of the join columns is nullable, return left join |
2141
|
8 |
|
foreach ($association->getJoinColumns() as $joinColumn) { |
2142
|
8 |
|
if (! $joinColumn->isNullable()) { |
2143
|
1 |
|
continue; |
2144
|
|
|
} |
2145
|
|
|
|
2146
|
8 |
|
return 'LEFT JOIN'; |
2147
|
|
|
} |
2148
|
|
|
|
2149
|
1 |
|
return 'INNER JOIN'; |
2150
|
|
|
} |
2151
|
|
|
|
2152
|
|
|
/** |
2153
|
|
|
* Gets an SQL column alias for a column name. |
2154
|
|
|
* |
2155
|
|
|
* @return string |
2156
|
|
|
*/ |
2157
|
515 |
|
public function getSQLColumnAlias() |
2158
|
|
|
{ |
2159
|
515 |
|
return $this->platform->getSQLResultCasing('c' . $this->currentPersisterContext->sqlAliasCounter++); |
2160
|
|
|
} |
2161
|
|
|
|
2162
|
|
|
/** |
2163
|
|
|
* Generates the filter SQL for a given entity and table alias. |
2164
|
|
|
* |
2165
|
|
|
* @param ClassMetadata $targetEntity Metadata of the target entity. |
2166
|
|
|
* @param string $targetTableAlias The table alias of the joined/selected table. |
2167
|
|
|
* |
2168
|
|
|
* @return string The SQL query part to add to a query. |
2169
|
|
|
*/ |
2170
|
539 |
|
protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias) |
2171
|
|
|
{ |
2172
|
539 |
|
$filterClauses = []; |
2173
|
|
|
|
2174
|
539 |
|
foreach ($this->em->getFilters()->getEnabledFilters() as $filter) { |
2175
|
22 |
|
$filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias); |
2176
|
|
|
|
2177
|
22 |
|
if ($filterExpr !== '') { |
2178
|
22 |
|
$filterClauses[] = '(' . $filterExpr . ')'; |
2179
|
|
|
} |
2180
|
|
|
} |
2181
|
|
|
|
2182
|
539 |
|
$sql = implode(' AND ', $filterClauses); |
2183
|
|
|
|
2184
|
539 |
|
return $sql ? '(' . $sql . ')' : ''; // Wrap again to avoid "X or Y and FilterConditionSQL" |
2185
|
|
|
} |
2186
|
|
|
|
2187
|
|
|
/** |
2188
|
|
|
* Switches persister context according to current query offset/limits |
2189
|
|
|
* |
2190
|
|
|
* This is due to the fact that to-many associations cannot be fetch-joined when a limit is involved |
2191
|
|
|
* |
2192
|
|
|
* @param int|null $offset |
2193
|
|
|
* @param int|null $limit |
2194
|
|
|
*/ |
2195
|
514 |
|
protected function switchPersisterContext($offset, $limit) |
2196
|
|
|
{ |
2197
|
514 |
|
if ($offset === null && $limit === null) { |
2198
|
502 |
|
$this->currentPersisterContext = $this->noLimitsContext; |
2199
|
|
|
|
2200
|
502 |
|
return; |
2201
|
|
|
} |
2202
|
|
|
|
2203
|
38 |
|
$this->currentPersisterContext = $this->limitsHandlingContext; |
2204
|
38 |
|
} |
2205
|
|
|
} |
2206
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.