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