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 | 1129 | public function __construct(EntityManagerInterface $em, ClassMetadata $class) |
|
157 | { |
||
158 | 1129 | $this->em = $em; |
|
159 | 1129 | $this->class = $class; |
|
160 | 1129 | $this->conn = $em->getConnection(); |
|
161 | 1129 | $this->platform = $this->conn->getDatabasePlatform(); |
|
162 | 1129 | $this->noLimitsContext = $this->currentPersisterContext = new CachedPersisterContext( |
|
163 | 1129 | $class, |
|
164 | 1129 | new Query\ResultSetMapping(), |
|
165 | 1129 | false |
|
166 | ); |
||
167 | 1129 | $this->limitsHandlingContext = new CachedPersisterContext( |
|
168 | 1129 | $class, |
|
169 | 1129 | new Query\ResultSetMapping(), |
|
170 | 1129 | true |
|
171 | ); |
||
172 | 1129 | } |
|
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 | 1049 | public function getIdentifier($entity) : array |
|
194 | { |
||
195 | 1049 | $id = []; |
|
196 | |||
197 | 1049 | foreach ($this->class->getIdentifier() as $fieldName) { |
|
198 | 1049 | $property = $this->class->getProperty($fieldName); |
|
199 | 1049 | $value = $property->getValue($entity); |
|
200 | |||
201 | 1049 | if ($value !== null) { |
|
202 | 1049 | $id[$fieldName] = $value; |
|
203 | } |
||
204 | } |
||
205 | |||
206 | 1049 | return $id; |
|
207 | } |
||
208 | |||
209 | /** |
||
210 | * Populates the entity identifier of an entity. |
||
211 | * |
||
212 | * @param object $entity |
||
213 | * @param mixed[] $id |
||
214 | */ |
||
215 | 219 | public function setIdentifier($entity, array $id) : void |
|
216 | { |
||
217 | 219 | foreach ($id as $idField => $idValue) { |
|
218 | 219 | $property = $this->class->getProperty($idField); |
|
219 | |||
220 | 219 | $property->setValue($entity, $idValue); |
|
221 | } |
||
222 | 219 | } |
|
223 | |||
224 | /** |
||
225 | * {@inheritdoc} |
||
226 | */ |
||
227 | 912 | public function insert($entity) |
|
228 | { |
||
229 | 912 | $stmt = $this->conn->prepare($this->getInsertSQL()); |
|
230 | 912 | $tableName = $this->class->getTableName(); |
|
231 | 912 | $insertData = $this->prepareInsertData($entity); |
|
232 | 912 | $generationPlan = $this->class->getValueGenerationPlan(); |
|
233 | |||
234 | 912 | if (isset($insertData[$tableName])) { |
|
235 | 888 | $paramIndex = 1; |
|
236 | |||
237 | 888 | foreach ($insertData[$tableName] as $columnName => $value) { |
|
238 | 888 | $type = $this->columns[$columnName]->getType(); |
|
239 | |||
240 | 888 | $stmt->bindValue($paramIndex++, $value, $type); |
|
241 | } |
||
242 | } |
||
243 | |||
244 | 912 | $stmt->execute(); |
|
245 | |||
246 | 911 | if ($generationPlan->containsDeferred()) { |
|
247 | 826 | $generationPlan->executeDeferred($this->em, $entity); |
|
248 | } |
||
249 | |||
250 | 911 | if ($this->class->isVersioned()) { |
|
251 | 196 | $this->assignDefaultVersionValue($entity, $this->getIdentifier($entity)); |
|
252 | } |
||
253 | |||
254 | 911 | $stmt->closeCursor(); |
|
255 | 911 | } |
|
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 | 205 | protected function assignDefaultVersionValue($entity, array $id) |
|
266 | { |
||
267 | 205 | $versionProperty = $this->class->versionProperty; |
|
268 | 205 | $versionValue = $this->fetchVersionValue($versionProperty, $id); |
|
269 | |||
270 | 205 | $versionProperty->setValue($entity, $versionValue); |
|
271 | 205 | } |
|
272 | |||
273 | /** |
||
274 | * Fetches the current version value of a versioned entity. |
||
275 | * |
||
276 | * @param mixed[] $id |
||
277 | * |
||
278 | * @return mixed |
||
279 | */ |
||
280 | 205 | protected function fetchVersionValue(VersionFieldMetadata $versionProperty, array $id) |
|
281 | { |
||
282 | 205 | $versionedClass = $versionProperty->getDeclaringClass(); |
|
283 | 205 | $tableName = $versionedClass->table->getQuotedQualifiedName($this->platform); |
|
284 | 205 | $columnName = $this->platform->quoteIdentifier($versionProperty->getColumnName()); |
|
285 | 205 | $identifier = array_map( |
|
286 | 205 | function ($columnName) { |
|
287 | 205 | return $this->platform->quoteIdentifier($columnName); |
|
288 | 205 | }, |
|
289 | 205 | array_keys($versionedClass->getIdentifierColumns($this->em)) |
|
290 | ); |
||
291 | |||
292 | // FIXME: Order with composite keys might not be correct |
||
293 | 205 | $sql = 'SELECT ' . $columnName |
|
294 | 205 | . ' FROM ' . $tableName |
|
295 | 205 | . ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?'; |
|
296 | |||
297 | 205 | $flattenedId = $this->em->getIdentifierFlattener()->flattenIdentifier($versionedClass, $id); |
|
298 | 205 | $versionType = $versionProperty->getType(); |
|
299 | |||
300 | 205 | $value = $this->conn->fetchColumn( |
|
301 | 205 | $sql, |
|
302 | 205 | array_values($flattenedId), |
|
303 | 205 | 0, |
|
304 | 205 | $this->extractIdentifierTypes($id, $versionedClass) |
|
305 | ); |
||
306 | |||
307 | 205 | return $versionType->convertToPHPValue($value, $this->platform); |
|
308 | } |
||
309 | |||
310 | /** |
||
311 | * @param mixed[] $id |
||
312 | * |
||
313 | * @return mixed[] |
||
314 | */ |
||
315 | 205 | private function extractIdentifierTypes(array $id, ClassMetadata $versionedClass) : array |
|
316 | { |
||
317 | 205 | $types = []; |
|
318 | |||
319 | 205 | foreach ($id as $field => $value) { |
|
320 | 205 | $types = array_merge($types, $this->getTypes($field, $value, $versionedClass)); |
|
321 | } |
||
322 | |||
323 | 205 | return $types; |
|
324 | } |
||
325 | |||
326 | /** |
||
327 | * {@inheritdoc} |
||
328 | */ |
||
329 | 80 | public function update($entity) |
|
330 | { |
||
331 | 80 | $tableName = $this->class->getTableName(); |
|
332 | 80 | $updateData = $this->prepareUpdateData($entity); |
|
333 | |||
334 | 80 | if (! isset($updateData[$tableName])) { |
|
335 | 8 | return; |
|
336 | } |
||
337 | |||
338 | 72 | $data = $updateData[$tableName]; |
|
339 | |||
340 | 72 | if (! $data) { |
|
341 | return; |
||
342 | } |
||
343 | |||
344 | 72 | $isVersioned = $this->class->isVersioned(); |
|
345 | 72 | $quotedTableName = $this->class->table->getQuotedQualifiedName($this->platform); |
|
346 | |||
347 | 72 | $this->updateTable($entity, $quotedTableName, $data, $isVersioned); |
|
348 | |||
349 | 70 | if ($isVersioned) { |
|
350 | 11 | $id = $this->em->getUnitOfWork()->getEntityIdentifier($entity); |
|
351 | |||
352 | 11 | $this->assignDefaultVersionValue($entity, $id); |
|
353 | } |
||
354 | 70 | } |
|
355 | |||
356 | /** |
||
357 | * {@inheritdoc} |
||
358 | */ |
||
359 | 58 | public function delete($entity) |
|
360 | { |
||
361 | 58 | $class = $this->class; |
|
362 | 58 | $unitOfWork = $this->em->getUnitOfWork(); |
|
363 | 58 | $identifier = $unitOfWork->getEntityIdentifier($entity); |
|
364 | 58 | $tableName = $class->table->getQuotedQualifiedName($this->platform); |
|
365 | |||
366 | 58 | $types = []; |
|
367 | 58 | $id = []; |
|
368 | |||
369 | 58 | foreach ($class->identifier as $field) { |
|
370 | 58 | $property = $class->getProperty($field); |
|
371 | |||
372 | 58 | if ($property instanceof FieldMetadata) { |
|
373 | 56 | $columnName = $property->getColumnName(); |
|
374 | 56 | $quotedColumnName = $this->platform->quoteIdentifier($columnName); |
|
375 | |||
376 | 56 | $id[$quotedColumnName] = $identifier[$field]; |
|
377 | 56 | $types[] = $property->getType(); |
|
378 | |||
379 | 56 | 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 | 58 | $this->deleteJoinTableRecords($identifier); |
|
416 | |||
417 | 58 | 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 | 102 | final protected function updateTable($entity, $quotedTableName, array $updateData, $versioned = false) |
|
433 | { |
||
434 | 102 | $set = []; |
|
435 | 102 | $types = []; |
|
436 | 102 | $params = []; |
|
437 | |||
438 | 102 | foreach ($updateData as $columnName => $value) { |
|
439 | 102 | $column = $this->columns[$columnName]; |
|
440 | 102 | $quotedColumnName = $this->platform->quoteIdentifier($column->getColumnName()); |
|
441 | 102 | $type = $column->getType(); |
|
442 | 102 | $placeholder = $type->convertToDatabaseValueSQL('?', $this->platform); |
|
443 | |||
444 | 102 | $set[] = sprintf('%s = %s', $quotedColumnName, $placeholder); |
|
445 | 102 | $params[] = $value; |
|
446 | 102 | $types[] = $column->getType(); |
|
447 | } |
||
448 | |||
449 | // @todo guilhermeblanco Bring this back: $this->em->getUnitOfWork()->getEntityIdentifier($entity); |
||
450 | 102 | $identifier = $this->getIdentifier($entity); |
|
451 | 102 | $where = []; |
|
452 | |||
453 | 102 | foreach ($this->class->identifier as $idField) { |
|
454 | 102 | $property = $this->class->getProperty($idField); |
|
455 | |||
456 | switch (true) { |
||
457 | 102 | case ($property instanceof FieldMetadata): |
|
458 | 99 | $where[] = $this->platform->quoteIdentifier($property->getColumnName()); |
|
459 | 99 | $params[] = $identifier[$idField]; |
|
460 | 99 | $types[] = $property->getType(); |
|
461 | 99 | 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 | 102 | break; |
|
483 | } |
||
484 | } |
||
485 | |||
486 | 102 | if ($versioned) { |
|
487 | 19 | $versionProperty = $this->class->versionProperty; |
|
488 | 19 | $versionColumnType = $versionProperty->getType(); |
|
489 | 19 | $versionColumnName = $this->platform->quoteIdentifier($versionProperty->getColumnName()); |
|
490 | |||
491 | 19 | $where[] = $versionColumnName; |
|
492 | 19 | $types[] = $versionColumnType; |
|
493 | 19 | $params[] = $versionProperty->getValue($entity); |
|
494 | |||
495 | 19 | switch ($versionColumnType->getName()) { |
|
496 | case Type::SMALLINT: |
||
497 | case Type::INTEGER: |
||
498 | case Type::BIGINT: |
||
499 | 18 | $set[] = $versionColumnName . ' = ' . $versionColumnName . ' + 1'; |
|
500 | 18 | break; |
|
501 | |||
502 | case Type::DATETIME: |
||
503 | 1 | $set[] = $versionColumnName . ' = CURRENT_TIMESTAMP'; |
|
504 | 1 | break; |
|
505 | } |
||
506 | } |
||
507 | |||
508 | 102 | $sql = 'UPDATE ' . $quotedTableName |
|
509 | 102 | . ' SET ' . implode(', ', $set) |
|
510 | 102 | . ' WHERE ' . implode(' = ? AND ', $where) . ' = ?'; |
|
511 | |||
512 | 102 | $result = $this->conn->executeUpdate($sql, $params, $types); |
|
513 | |||
514 | 102 | if ($versioned && ! $result) { |
|
515 | 4 | throw OptimisticLockException::lockFailed($entity); |
|
516 | } |
||
517 | 99 | } |
|
518 | |||
519 | /** |
||
520 | * @todo Add check for platform if it supports foreign keys/cascading. |
||
521 | * |
||
522 | * @param mixed[] $identifier |
||
523 | */ |
||
524 | 62 | protected function deleteJoinTableRecords($identifier) |
|
525 | { |
||
526 | 62 | foreach ($this->class->getDeclaredPropertiesIterator() as $association) { |
|
527 | 62 | if (! ($association instanceof ManyToManyAssociationMetadata)) { |
|
528 | 62 | 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 | 23 | $selfReferential = $association->getTargetEntity() === $association->getSourceEntity(); |
|
534 | 23 | $owningAssociation = $association; |
|
535 | 23 | $otherColumns = []; |
|
536 | 23 | $otherKeys = []; |
|
537 | 23 | $keys = []; |
|
538 | |||
539 | 23 | if (! $owningAssociation->isOwningSide()) { |
|
540 | 6 | $class = $this->em->getClassMetadata($association->getTargetEntity()); |
|
541 | 6 | $owningAssociation = $class->getProperty($association->getMappedBy()); |
|
542 | } |
||
543 | |||
544 | 23 | $joinTable = $owningAssociation->getJoinTable(); |
|
545 | 23 | $joinTableName = $joinTable->getQuotedQualifiedName($this->platform); |
|
546 | 23 | $joinColumns = $association->isOwningSide() |
|
547 | 19 | ? $joinTable->getJoinColumns() |
|
548 | 23 | : $joinTable->getInverseJoinColumns() |
|
549 | ; |
||
550 | |||
551 | 23 | if ($selfReferential) { |
|
552 | 1 | $otherColumns = ! $association->isOwningSide() |
|
553 | ? $joinTable->getJoinColumns() |
||
554 | 1 | : $joinTable->getInverseJoinColumns() |
|
555 | ; |
||
556 | } |
||
557 | |||
558 | 23 | $isOnDeleteCascade = false; |
|
559 | |||
560 | 23 | foreach ($joinColumns as $joinColumn) { |
|
561 | 23 | $keys[] = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
|
562 | |||
563 | 23 | if ($joinColumn->isOnDeleteCascade()) { |
|
564 | 23 | $isOnDeleteCascade = true; |
|
565 | } |
||
566 | } |
||
567 | |||
568 | 23 | 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 | 23 | if ($isOnDeleteCascade) { |
|
577 | 5 | continue; |
|
578 | } |
||
579 | |||
580 | 19 | $this->conn->delete($joinTableName, array_combine($keys, $identifier)); |
|
581 | |||
582 | 19 | if ($selfReferential) { |
|
583 | 19 | $this->conn->delete($joinTableName, array_combine($otherKeys, $identifier)); |
|
584 | } |
||
585 | } |
||
586 | 62 | } |
|
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 | 996 | protected function prepareInsertData($entity) : array |
|
599 | { |
||
600 | 996 | 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 | 1001 | protected function prepareUpdateData($entity) |
|
625 | { |
||
626 | 1001 | $uow = $this->em->getUnitOfWork(); |
|
627 | 1001 | $result = []; |
|
628 | 1001 | $versionPropertyName = $this->class->isVersioned() |
|
629 | 209 | ? $this->class->versionProperty->getName() |
|
630 | 1001 | : null |
|
631 | ; |
||
632 | |||
633 | // @todo guilhermeblanco This should check column insertability/updateability instead of field changeset |
||
634 | 1001 | foreach ($uow->getEntityChangeSet($entity) as $propertyName => $propertyChangeSet) { |
|
635 | 968 | if ($versionPropertyName === $propertyName) { |
|
636 | continue; |
||
637 | } |
||
638 | |||
639 | 968 | $property = $this->class->getProperty($propertyName); |
|
640 | 968 | $newValue = $propertyChangeSet[1]; |
|
641 | |||
642 | 968 | if ($property instanceof FieldMetadata) { |
|
643 | // @todo guilhermeblanco Please remove this in the future for good... |
||
644 | 935 | $this->columns[$property->getColumnName()] = $property; |
|
645 | |||
646 | 935 | $result[$property->getTableName()][$property->getColumnName()] = $newValue; |
|
647 | |||
648 | 935 | continue; |
|
649 | } |
||
650 | |||
651 | // Only owning side of x-1 associations can have a FK column. |
||
652 | 832 | 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 | 832 | if ($newValue !== null && $uow->isScheduledForInsert($newValue)) { |
|
660 | 32 | $uow->scheduleExtraUpdate($entity, [$propertyName => [null, $newValue]]); |
|
661 | |||
662 | 32 | $newValue = null; |
|
663 | } |
||
664 | |||
665 | 832 | $targetClass = $this->em->getClassMetadata($property->getTargetEntity()); |
|
666 | 832 | $targetPersister = $uow->getEntityPersister($targetClass->getClassName()); |
|
667 | |||
668 | 832 | foreach ($property->getJoinColumns() as $joinColumn) { |
|
669 | /** @var JoinColumnMetadata $joinColumn */ |
||
670 | 832 | $referencedColumnName = $joinColumn->getReferencedColumnName(); |
|
671 | |||
672 | 832 | if (! $joinColumn->getType()) { |
|
673 | 9 | $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em)); |
|
674 | } |
||
675 | |||
676 | // @todo guilhermeblanco Please remove this in the future for good... |
||
677 | 832 | $this->columns[$joinColumn->getColumnName()] = $joinColumn; |
|
678 | |||
679 | 832 | $result[$joinColumn->getTableName()][$joinColumn->getColumnName()] = $newValue !== null |
|
680 | 614 | ? $targetPersister->getColumnValue($newValue, $referencedColumnName) |
|
681 | 832 | : null |
|
682 | ; |
||
683 | } |
||
684 | } |
||
685 | |||
686 | 1001 | return $result; |
|
687 | } |
||
688 | |||
689 | /** |
||
690 | * @param object $entity |
||
691 | * |
||
692 | * @return mixed|null |
||
693 | */ |
||
694 | 614 | 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 | 614 | $propertyName = $this->class->fieldNames[$columnName]; |
|
698 | 614 | $property = $this->class->getProperty($propertyName); |
|
699 | |||
700 | 614 | if (! $property) { |
|
701 | return null; |
||
702 | } |
||
703 | |||
704 | 614 | $propertyValue = $property->getValue($entity); |
|
705 | |||
706 | 614 | if ($property instanceof LocalColumnMetadata) { |
|
707 | 614 | 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 | 471 | 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 | 471 | $this->switchPersisterContext(null, $limit); |
|
746 | |||
747 | 471 | $sql = $this->getSelectSQL($criteria, $association, $lockMode, $limit, null, $orderBy); |
|
748 | |||
749 | 470 | list($params, $types) = $this->expandParameters($criteria); |
|
750 | |||
751 | 470 | $stmt = $this->conn->executeQuery($sql, $params, $types); |
|
752 | |||
753 | 470 | if ($entity !== null) { |
|
754 | 63 | $hints[Query::HINT_REFRESH] = true; |
|
755 | 63 | $hints[Query::HINT_REFRESH_ENTITY] = $entity; |
|
756 | } |
||
757 | |||
758 | 470 | $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT); |
|
759 | 470 | $entities = $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, $hints); |
|
760 | |||
761 | 470 | return $entities ? $entities[0] : null; |
|
762 | } |
||
763 | |||
764 | /** |
||
765 | * {@inheritdoc} |
||
766 | */ |
||
767 | 395 | public function loadById(array $identifier, $entity = null) |
|
768 | { |
||
769 | 395 | return $this->load($identifier, $entity); |
|
770 | } |
||
771 | |||
772 | /** |
||
773 | * {@inheritdoc} |
||
774 | */ |
||
775 | 92 | public function loadToOneEntity(ToOneAssociationMetadata $association, $sourceEntity, array $identifier = []) |
|
776 | { |
||
777 | 92 | $unitOfWork = $this->em->getUnitOfWork(); |
|
778 | 92 | $targetEntity = $association->getTargetEntity(); |
|
779 | 92 | $foundEntity = $unitOfWork->tryGetById($identifier, $targetEntity); |
|
780 | |||
781 | 92 | if ($foundEntity !== false) { |
|
782 | return $foundEntity; |
||
783 | } |
||
784 | |||
785 | 92 | $targetClass = $this->em->getClassMetadata($targetEntity); |
|
786 | |||
787 | 92 | if ($association->isOwningSide()) { |
|
788 | 30 | $inversedBy = $association->getInversedBy(); |
|
789 | 30 | $targetProperty = $inversedBy ? $targetClass->getProperty($inversedBy) : null; |
|
790 | 30 | $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 | 30 | $hints = []; |
|
795 | |||
796 | 30 | 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 | 30 | $entity = $this->load($identifier, null, $association, $hints); |
|
807 | |||
808 | // Complete bidirectional association, if necessary |
||
809 | 30 | if ($entity !== null && $isInverseSingleValued) { |
|
810 | $targetProperty->setValue($entity, $sourceEntity); |
||
811 | } |
||
812 | |||
813 | 30 | return $entity; |
|
814 | } |
||
815 | |||
816 | 62 | $sourceClass = $association->getDeclaringClass(); |
|
817 | 62 | $owningAssociation = $targetClass->getProperty($association->getMappedBy()); |
|
818 | 62 | $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName()); |
|
819 | |||
820 | 62 | foreach ($owningAssociation->getJoinColumns() as $joinColumn) { |
|
821 | 62 | $sourceKeyColumn = $joinColumn->getReferencedColumnName(); |
|
822 | 62 | $targetKeyColumn = $joinColumn->getColumnName(); |
|
823 | |||
824 | 62 | if (! isset($sourceClass->fieldNames[$sourceKeyColumn])) { |
|
825 | throw MappingException::joinColumnMustPointToMappedField( |
||
826 | $sourceClass->getClassName(), |
||
827 | $sourceKeyColumn |
||
828 | ); |
||
829 | } |
||
830 | |||
831 | 62 | $property = $sourceClass->getProperty($sourceClass->fieldNames[$sourceKeyColumn]); |
|
832 | 62 | $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 | 62 | unset($identifier[$targetKeyColumn]); |
|
838 | |||
839 | 62 | $identifier[$targetClass->fieldNames[$targetKeyColumn]] = $value; |
|
840 | } |
||
841 | |||
842 | 62 | $entity = $this->load($identifier, null, $association); |
|
843 | |||
844 | 62 | if ($entity !== null) { |
|
845 | 16 | $owningAssociation->setValue($entity, $sourceEntity); |
|
846 | } |
||
847 | |||
848 | 62 | return $entity; |
|
849 | } |
||
850 | |||
851 | /** |
||
852 | * {@inheritdoc} |
||
853 | */ |
||
854 | 15 | public function refresh(array $id, $entity, $lockMode = null) |
|
855 | { |
||
856 | 15 | $sql = $this->getSelectSQL($id, null, $lockMode); |
|
857 | 15 | list($params, $types) = $this->expandParameters($id); |
|
858 | 15 | $stmt = $this->conn->executeQuery($sql, $params, $types); |
|
859 | |||
860 | 15 | $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT); |
|
861 | 15 | $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [Query::HINT_REFRESH => true]); |
|
862 | 15 | } |
|
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 | 8 | public function loadCriteria(Criteria $criteria) |
|
882 | { |
||
883 | 8 | $orderBy = $criteria->getOrderings(); |
|
884 | 8 | $limit = $criteria->getMaxResults(); |
|
885 | 8 | $offset = $criteria->getFirstResult(); |
|
886 | 8 | $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 | 71 | public function loadAll(array $criteria = [], array $orderBy = [], $limit = null, $offset = null) |
|
934 | { |
||
935 | 71 | $this->switchPersisterContext($offset, $limit); |
|
936 | |||
937 | 71 | $sql = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy); |
|
938 | |||
939 | 67 | list($params, $types) = $this->expandParameters($criteria); |
|
940 | |||
941 | 67 | $stmt = $this->conn->executeQuery($sql, $params, $types); |
|
942 | 67 | $rsm = $this->currentPersisterContext->rsm; |
|
943 | 67 | $hints = [UnitOfWork::HINT_DEFEREAGERLOAD => true]; |
|
944 | 67 | $hydratorType = $this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT; |
|
945 | 67 | $hydrator = $this->em->newHydrator($hydratorType); |
|
946 | |||
947 | 67 | 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 | 73 | public function loadManyToManyCollection( |
|
970 | ManyToManyAssociationMetadata $association, |
||
971 | $sourceEntity, |
||
972 | PersistentCollection $collection |
||
973 | ) { |
||
974 | 73 | $stmt = $this->getManyToManyStatement($association, $sourceEntity); |
|
975 | |||
976 | 73 | 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 | 135 | private function loadCollectionFromStatement(ToManyAssociationMetadata $association, $stmt, $collection) |
|
1010 | { |
||
1011 | 135 | $rsm = $this->currentPersisterContext->rsm; |
|
1012 | |||
1013 | 135 | 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 | 135 | $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT); |
|
1019 | $hints = [ |
||
1020 | 135 | UnitOfWork::HINT_DEFEREAGERLOAD => true, |
|
1021 | 135 | 'collection' => $collection, |
|
1022 | ]; |
||
1023 | |||
1024 | 135 | 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 | 80 | private function getManyToManyStatement( |
|
1037 | ManyToManyAssociationMetadata $association, |
||
1038 | $sourceEntity, |
||
1039 | $offset = null, |
||
1040 | $limit = null |
||
1041 | ) { |
||
1042 | 80 | $this->switchPersisterContext($offset, $limit); |
|
1043 | |||
1044 | /** @var ClassMetadata $sourceClass */ |
||
1045 | 80 | $sourceClass = $this->em->getClassMetadata($association->getSourceEntity()); |
|
1046 | 80 | $class = $sourceClass; |
|
1047 | 80 | $owningAssoc = $association; |
|
1048 | 80 | $criteria = []; |
|
1049 | 80 | $parameters = []; |
|
1050 | |||
1051 | 80 | if (! $association->isOwningSide()) { |
|
1052 | 12 | $class = $this->em->getClassMetadata($association->getTargetEntity()); |
|
1053 | 12 | $owningAssoc = $class->getProperty($association->getMappedBy()); |
|
1054 | } |
||
1055 | |||
1056 | 80 | $joinTable = $owningAssoc->getJoinTable(); |
|
1057 | 80 | $joinTableName = $joinTable->getQuotedQualifiedName($this->platform); |
|
1058 | 80 | $joinColumns = $association->isOwningSide() |
|
1059 | 73 | ? $joinTable->getJoinColumns() |
|
1060 | 80 | : $joinTable->getInverseJoinColumns() |
|
1061 | ; |
||
1062 | |||
1063 | 80 | foreach ($joinColumns as $joinColumn) { |
|
1064 | 80 | $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
|
1065 | 80 | $fieldName = $sourceClass->fieldNames[$joinColumn->getReferencedColumnName()]; |
|
1066 | 80 | $property = $sourceClass->getProperty($fieldName); |
|
1067 | |||
1068 | 80 | if ($property instanceof FieldMetadata) { |
|
1069 | 79 | $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 | 80 | $criteria[$joinTableName . '.' . $quotedColumnName] = $value; |
|
1080 | 80 | $parameters[] = [ |
|
1081 | 80 | 'value' => $value, |
|
1082 | 80 | 'field' => $fieldName, |
|
1083 | 80 | 'class' => $sourceClass, |
|
1084 | ]; |
||
1085 | } |
||
1086 | |||
1087 | 80 | $sql = $this->getSelectSQL($criteria, $association, null, $limit, $offset); |
|
1088 | |||
1089 | 80 | list($params, $types) = $this->expandToManyParameters($parameters); |
|
1090 | |||
1091 | 80 | return $this->conn->executeQuery($sql, $params, $types); |
|
1092 | } |
||
1093 | |||
1094 | /** |
||
1095 | * {@inheritdoc} |
||
1096 | */ |
||
1097 | 524 | public function getSelectSQL( |
|
1098 | $criteria, |
||
1099 | ?AssociationMetadata $association = null, |
||
1100 | $lockMode = null, |
||
1101 | $limit = null, |
||
1102 | $offset = null, |
||
1103 | array $orderBy = [] |
||
1104 | ) { |
||
1105 | 524 | $this->switchPersisterContext($offset, $limit); |
|
1106 | |||
1107 | 524 | $lockSql = ''; |
|
1108 | 524 | $joinSql = ''; |
|
1109 | 524 | $orderBySql = ''; |
|
1110 | |||
1111 | 524 | if ($association instanceof ManyToManyAssociationMetadata) { |
|
1112 | 81 | $joinSql = $this->getSelectManyToManyJoinSQL($association); |
|
1113 | } |
||
1114 | |||
1115 | 524 | if ($association instanceof ToManyAssociationMetadata && $association->getOrderBy()) { |
|
1116 | 5 | $orderBy = $association->getOrderBy(); |
|
1117 | } |
||
1118 | |||
1119 | 524 | if ($orderBy) { |
|
1120 | 11 | $orderBySql = $this->getOrderBySQL($orderBy, $this->getSQLTableAlias($this->class->getTableName())); |
|
1121 | } |
||
1122 | |||
1123 | 522 | $conditionSql = ($criteria instanceof Criteria) |
|
1124 | 8 | ? $this->getSelectConditionCriteriaSQL($criteria) |
|
1125 | 520 | : $this->getSelectConditionSQL($criteria, $association); |
|
1126 | |||
1127 | switch ($lockMode) { |
||
1128 | 517 | case LockMode::PESSIMISTIC_READ: |
|
1129 | $lockSql = ' ' . $this->platform->getReadLockSQL(); |
||
1130 | break; |
||
1131 | |||
1132 | 517 | case LockMode::PESSIMISTIC_WRITE: |
|
1133 | $lockSql = ' ' . $this->platform->getWriteLockSQL(); |
||
1134 | break; |
||
1135 | } |
||
1136 | |||
1137 | 517 | $columnList = $this->getSelectColumnsSQL(); |
|
1138 | 517 | $tableAlias = $this->getSQLTableAlias($this->class->getTableName()); |
|
1139 | 517 | $filterSql = $this->generateFilterConditionSQL($this->class, $tableAlias); |
|
1140 | 517 | $tableName = $this->class->table->getQuotedQualifiedName($this->platform); |
|
1141 | |||
1142 | 517 | if ($filterSql !== '') { |
|
1143 | 12 | $conditionSql = $conditionSql |
|
1144 | 11 | ? $conditionSql . ' AND ' . $filterSql |
|
1145 | 12 | : $filterSql; |
|
1146 | } |
||
1147 | |||
1148 | 517 | $select = 'SELECT ' . $columnList; |
|
1149 | 517 | $from = ' FROM ' . $tableName . ' ' . $tableAlias; |
|
1150 | 517 | $join = $this->currentPersisterContext->selectJoinSql . $joinSql; |
|
1151 | 517 | $where = ($conditionSql ? ' WHERE ' . $conditionSql : ''); |
|
1152 | 517 | $lock = $this->platform->appendLockHint($from, $lockMode); |
|
1153 | $query = $select |
||
1154 | 517 | . $lock |
|
1155 | 517 | . $join |
|
1156 | 517 | . $where |
|
1157 | 517 | . $orderBySql; |
|
1158 | |||
1159 | 517 | 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 | 79 | final protected function getOrderBySQL(array $orderBy, $baseTableAlias) |
|
1200 | { |
||
1201 | 79 | if (! $orderBy) { |
|
1202 | 67 | return ''; |
|
1203 | } |
||
1204 | |||
1205 | 12 | $orderByList = []; |
|
1206 | |||
1207 | 12 | foreach ($orderBy as $fieldName => $orientation) { |
|
1208 | 12 | $orientation = strtoupper(trim($orientation)); |
|
1209 | |||
1210 | 12 | if (! in_array($orientation, ['ASC', 'DESC'])) { |
|
1211 | 1 | throw ORMException::invalidOrientation($this->class->getClassName(), $fieldName); |
|
1212 | } |
||
1213 | |||
1214 | 11 | $property = $this->class->getProperty($fieldName); |
|
1215 | |||
1216 | 11 | 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 | 2 | } elseif ($property instanceof AssociationMetadata) { |
|
1224 | 2 | if (! $property->isOwningSide()) { |
|
1225 | 1 | 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 | 518 | protected function getSelectColumnsSQL() |
|
1261 | { |
||
1262 | 518 | if ($this->currentPersisterContext->selectColumnListSql !== null) { |
|
1263 | 100 | return $this->currentPersisterContext->selectColumnListSql; |
|
1264 | } |
||
1265 | |||
1266 | 518 | $this->currentPersisterContext->rsm->addEntityResult($this->class->getClassName(), 'r'); // r for root |
|
1267 | 518 | $this->currentPersisterContext->selectJoinSql = ''; |
|
1268 | |||
1269 | 518 | $eagerAliasCounter = 0; |
|
1270 | 518 | $columnList = []; |
|
1271 | |||
1272 | 518 | foreach ($this->class->getDeclaredPropertiesIterator() as $fieldName => $property) { |
|
1273 | switch (true) { |
||
1274 | 518 | case ($property instanceof FieldMetadata): |
|
1275 | 516 | $columnList[] = $this->getSelectColumnSQL($fieldName, $this->class); |
|
1276 | 516 | break; |
|
1277 | |||
1278 | 465 | case ($property instanceof AssociationMetadata): |
|
1279 | 461 | $assocColumnSQL = $this->getSelectColumnAssociationSQL($fieldName, $property, $this->class); |
|
1280 | |||
1281 | 461 | if ($assocColumnSQL) { |
|
1282 | 390 | $columnList[] = $assocColumnSQL; |
|
1283 | } |
||
1284 | |||
1285 | 461 | $isAssocToOneInverseSide = $property instanceof ToOneAssociationMetadata && ! $property->isOwningSide(); |
|
1286 | 461 | $isAssocFromOneEager = ! $property instanceof ManyToManyAssociationMetadata && $property->getFetchMode() === FetchMode::EAGER; |
|
1287 | |||
1288 | 461 | if (! ($isAssocFromOneEager || $isAssocToOneInverseSide)) { |
|
1289 | 439 | break; |
|
1290 | } |
||
1291 | |||
1292 | 178 | if ($property instanceof ToManyAssociationMetadata && $this->currentPersisterContext->handlesLimits) { |
|
1293 | 3 | break; |
|
1294 | } |
||
1295 | |||
1296 | 175 | $targetEntity = $property->getTargetEntity(); |
|
1297 | 175 | $eagerEntity = $this->em->getClassMetadata($targetEntity); |
|
1298 | |||
1299 | 175 | if ($eagerEntity->inheritanceType !== InheritanceType::NONE) { |
|
1300 | 5 | break; // now this is why you shouldn't use inheritance |
|
1301 | } |
||
1302 | |||
1303 | 170 | $assocAlias = 'e' . ($eagerAliasCounter++); |
|
1304 | |||
1305 | 170 | $this->currentPersisterContext->rsm->addJoinedEntityResult($targetEntity, $assocAlias, 'r', $fieldName); |
|
1306 | |||
1307 | 170 | foreach ($eagerEntity->getDeclaredPropertiesIterator() as $eagerProperty) { |
|
1308 | switch (true) { |
||
1309 | 170 | case ($eagerProperty instanceof FieldMetadata): |
|
1310 | 168 | $columnList[] = $this->getSelectColumnSQL($eagerProperty->getName(), $eagerEntity, $assocAlias); |
|
1311 | 168 | break; |
|
1312 | |||
1313 | 167 | case ($eagerProperty instanceof ToOneAssociationMetadata && $eagerProperty->isOwningSide()): |
|
1314 | 164 | $columnList[] = $this->getSelectColumnAssociationSQL( |
|
1315 | 164 | $eagerProperty->getName(), |
|
1316 | 164 | $eagerProperty, |
|
1317 | 164 | $eagerEntity, |
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
1318 | 164 | $assocAlias |
|
1319 | ); |
||
1320 | 170 | break; |
|
1321 | } |
||
1322 | } |
||
1323 | |||
1324 | 170 | $owningAssociation = $property; |
|
1325 | 170 | $joinCondition = []; |
|
1326 | |||
1327 | 170 | if ($property instanceof ToManyAssociationMetadata && $property->getIndexedBy()) { |
|
1328 | 1 | $this->currentPersisterContext->rsm->addIndexBy($assocAlias, $property->getIndexedBy()); |
|
1329 | } |
||
1330 | |||
1331 | 170 | if (! $property->isOwningSide()) { |
|
1332 | 163 | $owningAssociation = $eagerEntity->getProperty($property->getMappedBy()); |
|
1333 | } |
||
1334 | |||
1335 | 170 | $joinTableAlias = $this->getSQLTableAlias($eagerEntity->getTableName(), $assocAlias); |
|
1336 | 170 | $joinTableName = $eagerEntity->table->getQuotedQualifiedName($this->platform); |
|
1337 | |||
1338 | 170 | $this->currentPersisterContext->selectJoinSql .= ' ' . $this->getJoinSQLForAssociation($property); |
|
1339 | |||
1340 | 170 | $sourceClass = $this->em->getClassMetadata($owningAssociation->getSourceEntity()); |
|
1341 | 170 | $targetClass = $this->em->getClassMetadata($owningAssociation->getTargetEntity()); |
|
1342 | 170 | $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $property->isOwningSide() ? $assocAlias : ''); |
|
1343 | 170 | $sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $property->isOwningSide() ? '' : $assocAlias); |
|
1344 | |||
1345 | 170 | foreach ($owningAssociation->getJoinColumns() as $joinColumn) { |
|
1346 | 170 | $joinCondition[] = sprintf( |
|
1347 | 170 | '%s.%s = %s.%s', |
|
1348 | 170 | $sourceTableAlias, |
|
1349 | 170 | $this->platform->quoteIdentifier($joinColumn->getColumnName()), |
|
1350 | 170 | $targetTableAlias, |
|
1351 | 170 | $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName()) |
|
1352 | ); |
||
1353 | } |
||
1354 | |||
1355 | 170 | $filterSql = $this->generateFilterConditionSQL($eagerEntity, $targetTableAlias); |
|
1356 | |||
1357 | // Add filter SQL |
||
1358 | 170 | if ($filterSql) { |
|
1359 | $joinCondition[] = $filterSql; |
||
1360 | } |
||
1361 | |||
1362 | 170 | $this->currentPersisterContext->selectJoinSql .= ' ' . $joinTableName . ' ' . $joinTableAlias . ' ON '; |
|
1363 | 170 | $this->currentPersisterContext->selectJoinSql .= implode(' AND ', $joinCondition); |
|
1364 | |||
1365 | 518 | break; |
|
1366 | } |
||
1367 | } |
||
1368 | |||
1369 | 518 | $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList); |
|
1370 | |||
1371 | 518 | 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 | 461 | protected function getSelectColumnAssociationSQL($field, AssociationMetadata $association, ClassMetadata $class, $alias = 'r') |
|
1383 | { |
||
1384 | 461 | if (! ($association->isOwningSide() && $association instanceof ToOneAssociationMetadata)) { |
|
1385 | 370 | return ''; |
|
1386 | } |
||
1387 | |||
1388 | 407 | $columnList = []; |
|
1389 | 407 | $targetClass = $this->em->getClassMetadata($association->getTargetEntity()); |
|
1390 | 407 | $sqlTableAlias = $this->getSQLTableAlias($class->getTableName(), ($alias === 'r' ? '' : $alias)); |
|
1391 | |||
1392 | 407 | foreach ($association->getJoinColumns() as $joinColumn) { |
|
1393 | /** @var JoinColumnMetadata $joinColumn */ |
||
1394 | 407 | $columnName = $joinColumn->getColumnName(); |
|
1395 | 407 | $quotedColumnName = $this->platform->quoteIdentifier($columnName); |
|
1396 | 407 | $referencedColumnName = $joinColumn->getReferencedColumnName(); |
|
1397 | 407 | $resultColumnName = $this->getSQLColumnAlias(); |
|
1398 | |||
1399 | 407 | if (! $joinColumn->getType()) { |
|
1400 | 9 | $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em)); |
|
1401 | } |
||
1402 | |||
1403 | 407 | $this->currentPersisterContext->rsm->addMetaResult( |
|
1404 | 407 | $alias, |
|
1405 | 407 | $resultColumnName, |
|
1406 | 407 | $columnName, |
|
1407 | 407 | $association->isPrimaryKey(), |
|
1408 | 407 | $joinColumn->getType() |
|
1409 | ); |
||
1410 | |||
1411 | 407 | $columnList[] = sprintf('%s.%s AS %s', $sqlTableAlias, $quotedColumnName, $resultColumnName); |
|
1412 | } |
||
1413 | |||
1414 | 407 | 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 | 83 | protected function getSelectManyToManyJoinSQL(ManyToManyAssociationMetadata $association) |
|
1426 | { |
||
1427 | 83 | $conditions = []; |
|
1428 | 83 | $owningAssociation = $association; |
|
1429 | 83 | $sourceTableAlias = $this->getSQLTableAlias($this->class->getTableName()); |
|
1430 | |||
1431 | 83 | if (! $association->isOwningSide()) { |
|
1432 | 13 | $targetEntity = $this->em->getClassMetadata($association->getTargetEntity()); |
|
1433 | 13 | $owningAssociation = $targetEntity->getProperty($association->getMappedBy()); |
|
1434 | } |
||
1435 | |||
1436 | 83 | $joinTable = $owningAssociation->getJoinTable(); |
|
1437 | 83 | $joinTableName = $joinTable->getQuotedQualifiedName($this->platform); |
|
1438 | 83 | $joinColumns = $association->isOwningSide() |
|
1439 | 75 | ? $joinTable->getInverseJoinColumns() |
|
1440 | 83 | : $joinTable->getJoinColumns() |
|
1441 | ; |
||
1442 | |||
1443 | 83 | foreach ($joinColumns as $joinColumn) { |
|
1444 | 83 | $conditions[] = sprintf( |
|
1445 | 83 | '%s.%s = %s.%s', |
|
1446 | 83 | $sourceTableAlias, |
|
1447 | 83 | $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName()), |
|
1448 | 83 | $joinTableName, |
|
1449 | 83 | $this->platform->quoteIdentifier($joinColumn->getColumnName()) |
|
1450 | ); |
||
1451 | } |
||
1452 | |||
1453 | 83 | return ' INNER JOIN ' . $joinTableName . ' ON ' . implode(' AND ', $conditions); |
|
1454 | } |
||
1455 | |||
1456 | /** |
||
1457 | * {@inheritdoc} |
||
1458 | */ |
||
1459 | 997 | public function getInsertSQL() |
|
1460 | { |
||
1461 | 997 | if ($this->insertSql !== null) { |
|
1462 | 660 | return $this->insertSql; |
|
1463 | } |
||
1464 | |||
1465 | 997 | $columns = $this->getInsertColumnList(); |
|
1466 | 997 | $tableName = $this->class->table->getQuotedQualifiedName($this->platform); |
|
1467 | |||
1468 | 997 | if (empty($columns)) { |
|
1469 | 104 | $property = $this->class->getProperty($this->class->identifier[0]); |
|
1470 | 104 | $identityColumn = $this->platform->quoteIdentifier($property->getColumnName()); |
|
1471 | |||
1472 | 104 | $this->insertSql = $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn); |
|
1473 | |||
1474 | 104 | return $this->insertSql; |
|
1475 | } |
||
1476 | |||
1477 | 975 | $quotedColumns = []; |
|
1478 | 975 | $values = []; |
|
1479 | |||
1480 | 975 | foreach ($columns as $columnName) { |
|
1481 | 975 | $column = $this->columns[$columnName]; |
|
1482 | |||
1483 | 975 | $quotedColumns[] = $this->platform->quoteIdentifier($column->getColumnName()); |
|
1484 | 975 | $values[] = $column->getType()->convertToDatabaseValueSQL('?', $this->platform); |
|
1485 | } |
||
1486 | |||
1487 | 975 | $quotedColumns = implode(', ', $quotedColumns); |
|
1488 | 975 | $values = implode(', ', $values); |
|
1489 | |||
1490 | 975 | $this->insertSql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $quotedColumns, $values); |
|
1491 | |||
1492 | 975 | 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 | 913 | protected function getInsertColumnList() |
|
1504 | { |
||
1505 | 913 | $columns = []; |
|
1506 | 913 | $versionPropertyName = $this->class->isVersioned() |
|
1507 | 196 | ? $this->class->versionProperty->getName() |
|
1508 | 913 | : null |
|
1509 | ; |
||
1510 | |||
1511 | 913 | foreach ($this->class->getDeclaredPropertiesIterator() as $name => $property) { |
|
1512 | /*if (isset($this->class->embeddedClasses[$name])) { |
||
1513 | continue; |
||
1514 | }*/ |
||
1515 | |||
1516 | switch (true) { |
||
1517 | 913 | case ($property instanceof VersionFieldMetadata): |
|
1518 | // Do nothing |
||
1519 | 196 | break; |
|
1520 | |||
1521 | 913 | case ($property instanceof LocalColumnMetadata): |
|
1522 | 913 | if (($property instanceof FieldMetadata |
|
1523 | && ( |
||
1524 | 913 | ! $property->hasValueGenerator() |
|
1525 | 913 | || $property->getValueGenerator()->getType() !== GeneratorType::IDENTITY |
|
1526 | ) |
||
1527 | ) |
||
1528 | 913 | || $this->class->identifier[0] !== $name |
|
1529 | ) { |
||
1530 | 850 | $columnName = $property->getColumnName(); |
|
1531 | |||
1532 | 850 | $columns[] = $columnName; |
|
1533 | |||
1534 | 850 | $this->columns[$columnName] = $property; |
|
1535 | } |
||
1536 | |||
1537 | 913 | break; |
|
1538 | |||
1539 | 807 | case ($property instanceof AssociationMetadata): |
|
1540 | 803 | if ($property->isOwningSide() && $property instanceof ToOneAssociationMetadata) { |
|
1541 | 761 | $targetClass = $this->em->getClassMetadata($property->getTargetEntity()); |
|
1542 | |||
1543 | 761 | foreach ($property->getJoinColumns() as $joinColumn) { |
|
1544 | /** @var JoinColumnMetadata $joinColumn */ |
||
1545 | 761 | $columnName = $joinColumn->getColumnName(); |
|
1546 | 761 | $referencedColumnName = $joinColumn->getReferencedColumnName(); |
|
1547 | |||
1548 | 761 | if (! $joinColumn->getType()) { |
|
1549 | 116 | $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em)); |
|
1550 | } |
||
1551 | |||
1552 | 761 | $columns[] = $columnName; |
|
1553 | |||
1554 | 761 | $this->columns[$columnName] = $joinColumn; |
|
1555 | } |
||
1556 | } |
||
1557 | |||
1558 | 913 | break; |
|
1559 | } |
||
1560 | } |
||
1561 | |||
1562 | 913 | 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 | 552 | protected function getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r') |
|
1576 | { |
||
1577 | 552 | $property = $class->getProperty($field); |
|
1578 | 552 | $columnAlias = $this->getSQLColumnAlias(); |
|
1579 | 552 | $sql = sprintf( |
|
1580 | 552 | '%s.%s', |
|
1581 | 552 | $this->getSQLTableAlias($property->getTableName(), ($alias === 'r' ? '' : $alias)), |
|
1582 | 552 | $this->platform->quoteIdentifier($property->getColumnName()) |
|
1583 | ); |
||
1584 | |||
1585 | 552 | $this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field, $class->getClassName()); |
|
1586 | |||
1587 | 552 | 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 | 586 | protected function getSQLTableAlias($tableName, $assocName = '') |
|
1599 | { |
||
1600 | 586 | if ($tableName) { |
|
1601 | 586 | $tableName .= '#' . $assocName; |
|
1602 | } |
||
1603 | |||
1604 | 586 | if (isset($this->currentPersisterContext->sqlTableAliases[$tableName])) { |
|
1605 | 578 | return $this->currentPersisterContext->sqlTableAliases[$tableName]; |
|
1606 | } |
||
1607 | |||
1608 | 586 | $tableAlias = 't' . $this->currentPersisterContext->sqlAliasCounter++; |
|
1609 | |||
1610 | 586 | $this->currentPersisterContext->sqlTableAliases[$tableName] = $tableAlias; |
|
1611 | |||
1612 | 586 | 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 | 39 | protected function getSelectConditionCriteriaSQL(Criteria $criteria) |
|
1668 | { |
||
1669 | 39 | $expression = $criteria->getWhereExpression(); |
|
1670 | |||
1671 | 39 | if ($expression === null) { |
|
1672 | 2 | return ''; |
|
1673 | } |
||
1674 | |||
1675 | 38 | $visitor = new SqlExpressionVisitor($this, $this->class); |
|
1676 | |||
1677 | 38 | return $visitor->dispatch($expression); |
|
1678 | } |
||
1679 | |||
1680 | /** |
||
1681 | * {@inheritdoc} |
||
1682 | */ |
||
1683 | 565 | public function getSelectConditionStatementSQL( |
|
1684 | $field, |
||
1685 | $value, |
||
1686 | ?AssociationMetadata $association = null, |
||
1687 | $comparison = null |
||
1688 | ) { |
||
1689 | 565 | $selectedColumns = []; |
|
1690 | 565 | $columns = $this->getSelectConditionStatementColumnSQL($field, $association); |
|
1691 | |||
1692 | 561 | if (in_array($comparison, [Comparison::IN, Comparison::NIN]) && isset($columns[1])) { |
|
1693 | // @todo try to support multi-column IN expressions. Example: (col1, col2) IN (('val1A', 'val2A'), ...) |
||
1694 | 1 | throw ORMException::cantUseInOperatorOnCompositeKeys(); |
|
1695 | } |
||
1696 | |||
1697 | 560 | foreach ($columns as $column) { |
|
1698 | 560 | $property = $this->class->getProperty($field); |
|
1699 | 560 | $placeholder = '?'; |
|
1700 | |||
1701 | 560 | if ($property instanceof FieldMetadata) { |
|
1702 | 472 | $placeholder = $property->getType()->convertToDatabaseValueSQL($placeholder, $this->platform); |
|
1703 | } |
||
1704 | |||
1705 | 560 | 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 | 533 | if (is_array($value)) { |
|
1725 | 14 | $in = sprintf('%s IN (%s)', $column, $placeholder); |
|
1726 | |||
1727 | 14 | 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 | 10 | $selectedColumns[] = $in; |
|
1734 | |||
1735 | 10 | continue; |
|
1736 | } |
||
1737 | |||
1738 | 522 | if ($value === null) { |
|
1739 | 9 | $selectedColumns[] = sprintf('%s IS NULL', $column); |
|
1740 | |||
1741 | 9 | continue; |
|
1742 | } |
||
1743 | |||
1744 | 514 | $selectedColumns[] = sprintf('%s = %s', $column, $placeholder); |
|
1745 | } |
||
1746 | |||
1747 | 560 | 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 | 565 | private function getSelectConditionStatementColumnSQL($field, ?AssociationMetadata $association = null) |
|
1760 | { |
||
1761 | 565 | $property = $this->class->getProperty($field); |
|
1762 | |||
1763 | 565 | if ($property instanceof FieldMetadata) { |
|
1764 | 472 | $tableAlias = $this->getSQLTableAlias($property->getTableName()); |
|
1765 | 472 | $columnName = $this->platform->quoteIdentifier($property->getColumnName()); |
|
1766 | |||
1767 | 472 | return [$tableAlias . '.' . $columnName]; |
|
1768 | } |
||
1769 | |||
1770 | 281 | if ($property instanceof AssociationMetadata) { |
|
1771 | 142 | $owningAssociation = $property; |
|
1772 | 142 | $columns = []; |
|
1773 | |||
1774 | // Many-To-Many requires join table check for joinColumn |
||
1775 | 142 | 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 | 140 | if (! $owningAssociation->isOwningSide()) { |
|
1794 | 1 | throw ORMException::invalidFindByInverseAssociation($this->class->getClassName(), $field); |
|
1795 | } |
||
1796 | |||
1797 | 139 | $class = $this->class->isInheritedProperty($field) |
|
1798 | 11 | ? $owningAssociation->getDeclaringClass() |
|
1799 | 139 | : $this->class |
|
1800 | ; |
||
1801 | 139 | $tableAlias = $this->getSQLTableAlias($class->getTableName()); |
|
1802 | |||
1803 | 139 | foreach ($owningAssociation->getJoinColumns() as $joinColumn) { |
|
1804 | 139 | $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
|
1805 | |||
1806 | 139 | $columns[] = $tableAlias . '.' . $quotedColumnName; |
|
1807 | } |
||
1808 | } |
||
1809 | |||
1810 | 141 | return $columns; |
|
1811 | } |
||
1812 | |||
1813 | 154 | 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 | 151 | return [$field]; |
|
1819 | } |
||
1820 | |||
1821 | 3 | 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 | 559 | protected function getSelectConditionSQL(array $criteria, ?AssociationMetadata $association = null) |
|
1836 | { |
||
1837 | 559 | $conditions = []; |
|
1838 | |||
1839 | 559 | foreach ($criteria as $field => $value) { |
|
1840 | 535 | $conditions[] = $this->getSelectConditionStatementSQL($field, $value, $association); |
|
1841 | } |
||
1842 | |||
1843 | 556 | 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 | 72 | public function loadOneToManyCollection( |
|
1866 | OneToManyAssociationMetadata $association, |
||
1867 | $sourceEntity, |
||
1868 | PersistentCollection $collection |
||
1869 | ) { |
||
1870 | 72 | $stmt = $this->getOneToManyStatement($association, $sourceEntity); |
|
1871 | |||
1872 | 72 | 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 | 77 | private function getOneToManyStatement( |
|
1885 | OneToManyAssociationMetadata $association, |
||
1886 | $sourceEntity, |
||
1887 | $offset = null, |
||
1888 | $limit = null |
||
1889 | ) { |
||
1890 | 77 | $this->switchPersisterContext($offset, $limit); |
|
1891 | |||
1892 | 77 | $criteria = []; |
|
1893 | 77 | $parameters = []; |
|
1894 | 77 | $owningAssoc = $this->class->getProperty($association->getMappedBy()); |
|
1895 | 77 | $sourceClass = $this->em->getClassMetadata($association->getSourceEntity()); |
|
1896 | 77 | $class = $owningAssoc->getDeclaringClass(); |
|
1897 | 77 | $tableAlias = $this->getSQLTableAlias($class->getTableName()); |
|
1898 | |||
1899 | 77 | foreach ($owningAssoc->getJoinColumns() as $joinColumn) { |
|
1900 | 77 | $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
|
1901 | 77 | $fieldName = $sourceClass->fieldNames[$joinColumn->getReferencedColumnName()]; |
|
1902 | 77 | $property = $sourceClass->getProperty($fieldName); |
|
1903 | |||
1904 | 77 | if ($property instanceof FieldMetadata) { |
|
1905 | 77 | $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 | 77 | $criteria[$tableAlias . '.' . $quotedColumnName] = $value; |
|
1915 | 77 | $parameters[] = [ |
|
1916 | 77 | 'value' => $value, |
|
1917 | 77 | 'field' => $fieldName, |
|
1918 | 77 | 'class' => $sourceClass, |
|
1919 | ]; |
||
1920 | } |
||
1921 | |||
1922 | 77 | $sql = $this->getSelectSQL($criteria, $association, null, $limit, $offset); |
|
1923 | 77 | list($params, $types) = $this->expandToManyParameters($parameters); |
|
1924 | |||
1925 | 77 | return $this->conn->executeQuery($sql, $params, $types); |
|
1926 | } |
||
1927 | |||
1928 | /** |
||
1929 | * {@inheritdoc} |
||
1930 | */ |
||
1931 | 536 | public function expandParameters($criteria) |
|
1932 | { |
||
1933 | 536 | $params = []; |
|
1934 | 536 | $types = []; |
|
1935 | |||
1936 | 536 | foreach ($criteria as $field => $value) { |
|
1937 | 512 | if ($value === null) { |
|
1938 | 3 | continue; // skip null values. |
|
1939 | } |
||
1940 | |||
1941 | 510 | $types = array_merge($types, $this->getTypes($field, $value, $this->class)); |
|
1942 | 510 | $params = array_merge($params, $this->getValues($value)); |
|
1943 | } |
||
1944 | |||
1945 | 536 | 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 | 147 | private function expandToManyParameters($criteria) |
|
1961 | { |
||
1962 | 147 | $params = []; |
|
1963 | 147 | $types = []; |
|
1964 | |||
1965 | 147 | foreach ($criteria as $criterion) { |
|
1966 | 147 | if ($criterion['value'] === null) { |
|
1967 | 6 | continue; // skip null values. |
|
1968 | } |
||
1969 | |||
1970 | 141 | $types = array_merge($types, $this->getTypes($criterion['field'], $criterion['value'], $criterion['class'])); |
|
1971 | 141 | $params = array_merge($params, $this->getValues($criterion['value'])); |
|
1972 | } |
||
1973 | |||
1974 | 147 | 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 | 667 | private function getTypes($field, $value, ClassMetadata $class) |
|
1988 | { |
||
1989 | 667 | $property = $class->getProperty($field); |
|
1990 | 667 | $types = []; |
|
1991 | |||
1992 | switch (true) { |
||
1993 | 667 | case ($property instanceof FieldMetadata): |
|
1994 | 610 | $types = array_merge($types, [$property->getType()]); |
|
1995 | 610 | break; |
|
1996 | |||
1997 | 141 | case ($property instanceof AssociationMetadata): |
|
1998 | 140 | $class = $this->em->getClassMetadata($property->getTargetEntity()); |
|
1999 | |||
2000 | 140 | if (! $property->isOwningSide()) { |
|
2001 | 2 | $property = $class->getProperty($property->getMappedBy()); |
|
2002 | 2 | $class = $this->em->getClassMetadata($property->getTargetEntity()); |
|
2003 | } |
||
2004 | |||
2005 | 140 | $joinColumns = $property instanceof ManyToManyAssociationMetadata |
|
2006 | 3 | ? $property->getJoinTable()->getInverseJoinColumns() |
|
2007 | 140 | : $property->getJoinColumns() |
|
2008 | ; |
||
2009 | |||
2010 | 140 | foreach ($joinColumns as $joinColumn) { |
|
2011 | /** @var JoinColumnMetadata $joinColumn */ |
||
2012 | 140 | $referencedColumnName = $joinColumn->getReferencedColumnName(); |
|
2013 | |||
2014 | 140 | if (! $joinColumn->getType()) { |
|
2015 | 1 | $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $class, $this->em)); |
|
2016 | } |
||
2017 | |||
2018 | 140 | $types[] = $joinColumn->getType(); |
|
2019 | } |
||
2020 | |||
2021 | 140 | break; |
|
2022 | |||
2023 | default: |
||
2024 | 1 | $types[] = null; |
|
2025 | 1 | break; |
|
2026 | } |
||
2027 | |||
2028 | 667 | if (is_array($value)) { |
|
2029 | 16 | return array_map(function ($type) { |
|
2030 | 16 | return $type->getBindingType() + Connection::ARRAY_PARAM_OFFSET; |
|
2031 | 16 | }, $types); |
|
2032 | } |
||
2033 | |||
2034 | 657 | return $types; |
|
2035 | } |
||
2036 | |||
2037 | /** |
||
2038 | * Retrieves the parameters that identifies a value. |
||
2039 | * |
||
2040 | * @param mixed $value |
||
2041 | * |
||
2042 | * @return mixed[] |
||
2043 | */ |
||
2044 | 542 | private function getValues($value) |
|
2045 | { |
||
2046 | 542 | if (is_array($value)) { |
|
2047 | 16 | $newValue = []; |
|
2048 | |||
2049 | 16 | foreach ($value as $itemValue) { |
|
2050 | 16 | $newValue = array_merge($newValue, $this->getValues($itemValue)); |
|
2051 | } |
||
2052 | |||
2053 | 16 | return [$newValue]; |
|
2054 | } |
||
2055 | |||
2056 | 542 | $metadataFactory = $this->em->getMetadataFactory(); |
|
2057 | 542 | $unitOfWork = $this->em->getUnitOfWork(); |
|
2058 | |||
2059 | 542 | 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 | 542 | return [$this->getIndividualValue($value)]; |
|
2075 | } |
||
2076 | |||
2077 | /** |
||
2078 | * Retrieves an individual parameter value. |
||
2079 | * |
||
2080 | * @param mixed $value |
||
2081 | * |
||
2082 | * @return mixed |
||
2083 | */ |
||
2084 | 542 | private function getIndividualValue($value) |
|
2085 | { |
||
2086 | 542 | if (! is_object($value) || ! $this->em->getMetadataFactory()->hasMetadataFor(StaticClassNameConverter::getClass($value))) { |
|
2087 | 540 | 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 | 170 | protected function getJoinSQLForAssociation(AssociationMetadata $association) |
|
2135 | { |
||
2136 | 170 | if (! $association->isOwningSide()) { |
|
2137 | 163 | return 'LEFT JOIN'; |
|
2138 | } |
||
2139 | |||
2140 | // if one of the join columns is nullable, return left join |
||
2141 | 13 | foreach ($association->getJoinColumns() as $joinColumn) { |
|
2142 | 13 | if (! $joinColumn->isNullable()) { |
|
2143 | 5 | continue; |
|
2144 | } |
||
2145 | |||
2146 | 11 | return 'LEFT JOIN'; |
|
2147 | } |
||
2148 | |||
2149 | 5 | return 'INNER JOIN'; |
|
2150 | } |
||
2151 | |||
2152 | /** |
||
2153 | * Gets an SQL column alias for a column name. |
||
2154 | * |
||
2155 | * @return string |
||
2156 | */ |
||
2157 | 553 | public function getSQLColumnAlias() |
|
2158 | { |
||
2159 | 553 | 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 | 577 | protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias) |
|
2171 | { |
||
2172 | 577 | $filterClauses = []; |
|
2173 | |||
2174 | 577 | 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 | 577 | $sql = implode(' AND ', $filterClauses); |
|
2183 | |||
2184 | 577 | 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 | 559 | protected function switchPersisterContext($offset, $limit) |
|
2196 | { |
||
2197 | 559 | if ($offset === null && $limit === null) { |
|
2198 | 546 | $this->currentPersisterContext = $this->noLimitsContext; |
|
2199 | |||
2200 | 546 | return; |
|
2201 | } |
||
2202 | |||
2203 | 41 | $this->currentPersisterContext = $this->limitsHandlingContext; |
|
2204 | 41 | } |
|
2205 | } |
||
2206 |