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