Rajout de doctrine/orm
[zf2.biz/galerie.git] / vendor / doctrine / orm / lib / Doctrine / ORM / Persisters / ManyToManyPersister.php
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;
21
22 use Doctrine\ORM\Mapping\ClassMetadata,
23     Doctrine\ORM\PersistentCollection,
24     Doctrine\ORM\UnitOfWork;
25
26 /**
27  * Persister for many-to-many collections.
28  *
29  * @author  Roman Borschel <roman@code-factory.org>
30  * @author  Guilherme Blanco <guilhermeblanco@hotmail.com>
31  * @author  Alexander <iam.asm89@gmail.com>
32  * @since   2.0
33  */
34 class ManyToManyPersister extends AbstractCollectionPersister
35 {
36     /**
37      * {@inheritdoc}
38      *
39      * @override
40      */
41     protected function _getDeleteRowSQL(PersistentCollection $coll)
42     {
43         $columns = array();
44         $mapping = $coll->getMapping();
45         $class   = $this->_em->getClassMetadata(get_class($coll->getOwner()));
46
47         foreach ($mapping['joinTable']['joinColumns'] as $joinColumn) {
48             $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
49         }
50
51         foreach ($mapping['joinTable']['inverseJoinColumns'] as $joinColumn) {
52             $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
53         }
54
55         return 'DELETE FROM ' . $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform)
56              . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
57     }
58
59     /**
60      * {@inheritdoc}
61      *
62      * @override
63      * @internal Order of the parameters must be the same as the order of the columns in
64      *           _getDeleteRowSql.
65      */
66     protected function _getDeleteRowSQLParameters(PersistentCollection $coll, $element)
67     {
68         return $this->_collectJoinTableColumnParameters($coll, $element);
69     }
70
71     /**
72      * {@inheritdoc}
73      *
74      * @override
75      */
76     protected function _getUpdateRowSQL(PersistentCollection $coll)
77     {}
78
79     /**
80      * {@inheritdoc}
81      *
82      * @override
83      * @internal Order of the parameters must be the same as the order of the columns in
84      *           _getInsertRowSql.
85      */
86     protected function _getInsertRowSQL(PersistentCollection $coll)
87     {
88         $columns    = array();
89         $mapping    = $coll->getMapping();
90         $class      = $this->_em->getClassMetadata(get_class($coll->getOwner()));
91         $joinTable  = $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform);
92
93         foreach ($mapping['joinTable']['joinColumns'] as $joinColumn) {
94             $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
95         }
96
97         foreach ($mapping['joinTable']['inverseJoinColumns'] as $joinColumn) {
98             $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
99         }
100
101         return 'INSERT INTO ' . $joinTable . ' (' . implode(', ', $columns) . ')'
102              . ' VALUES (' . implode(', ', array_fill(0, count($columns), '?')) . ')';
103     }
104
105     /**
106      * {@inheritdoc}
107      *
108      * @override
109      * @internal Order of the parameters must be the same as the order of the columns in
110      *           _getInsertRowSql.
111      */
112     protected function _getInsertRowSQLParameters(PersistentCollection $coll, $element)
113     {
114         return $this->_collectJoinTableColumnParameters($coll, $element);
115     }
116
117     /**
118      * Collects the parameters for inserting/deleting on the join table in the order
119      * of the join table columns as specified in ManyToManyMapping#joinTableColumns.
120      *
121      * @param $coll
122      * @param $element
123      * @return array
124      */
125     private function _collectJoinTableColumnParameters(PersistentCollection $coll, $element)
126     {
127         $params      = array();
128         $mapping     = $coll->getMapping();
129         $isComposite = count($mapping['joinTableColumns']) > 2;
130
131         $identifier1 = $this->_uow->getEntityIdentifier($coll->getOwner());
132         $identifier2 = $this->_uow->getEntityIdentifier($element);
133
134         if ($isComposite) {
135             $class1 = $this->_em->getClassMetadata(get_class($coll->getOwner()));
136             $class2 = $coll->getTypeClass();
137         }
138
139         foreach ($mapping['joinTableColumns'] as $joinTableColumn) {
140             $isRelationToSource = isset($mapping['relationToSourceKeyColumns'][$joinTableColumn]);
141
142             if ( ! $isComposite) {
143                 $params[] = $isRelationToSource ? array_pop($identifier1) : array_pop($identifier2);
144
145                 continue;
146             }
147
148             if ($isRelationToSource) {
149                 $params[] = $identifier1[$class1->getFieldForColumn($mapping['relationToSourceKeyColumns'][$joinTableColumn])];
150
151                 continue;
152             }
153
154             $params[] = $identifier2[$class2->getFieldForColumn($mapping['relationToTargetKeyColumns'][$joinTableColumn])];
155         }
156
157         return $params;
158     }
159
160     /**
161      * {@inheritdoc}
162      *
163      * @override
164      */
165     protected function _getDeleteSQL(PersistentCollection $coll)
166     {
167         $columns    = array();
168         $mapping    = $coll->getMapping();
169         $class      = $this->_em->getClassMetadata(get_class($coll->getOwner()));
170         $joinTable  = $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform);
171
172         foreach ($mapping['joinTable']['joinColumns'] as $joinColumn) {
173             $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
174         }
175
176         return 'DELETE FROM ' . $joinTable
177              . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
178     }
179
180     /**
181      * {@inheritdoc}
182      *
183      * @override
184      * @internal Order of the parameters must be the same as the order of the columns in
185      *           _getDeleteSql.
186      */
187     protected function _getDeleteSQLParameters(PersistentCollection $coll)
188     {
189         $identifier = $this->_uow->getEntityIdentifier($coll->getOwner());
190         $mapping    = $coll->getMapping();
191         $params     = array();
192
193         // Optimization for single column identifier
194         if (count($mapping['relationToSourceKeyColumns']) === 1) {
195             $params[] = array_pop($identifier);
196
197             return $params;
198         }
199
200         // Composite identifier
201         $sourceClass = $this->_em->getClassMetadata(get_class($coll->getOwner()));
202
203         foreach ($mapping['relationToSourceKeyColumns'] as $srcColumn) {
204             $params[] = $identifier[$sourceClass->fieldNames[$srcColumn]];
205         }
206
207         return $params;
208     }
209
210     /**
211      * {@inheritdoc}
212      */
213     public function count(PersistentCollection $coll)
214     {
215         $conditions     = array();
216         $params         = array();
217         $mapping        = $coll->getMapping();
218         $association    = $mapping;
219         $class          = $this->_em->getClassMetadata($mapping['sourceEntity']);
220         $id             = $this->_em->getUnitOfWork()->getEntityIdentifier($coll->getOwner());
221
222         if ( ! $mapping['isOwningSide']) {
223             $targetEntity   = $this->_em->getClassMetadata($mapping['targetEntity']);
224             $association    = $targetEntity->associationMappings[$mapping['mappedBy']];
225         }
226
227         $joinColumns = ( ! $mapping['isOwningSide'])
228             ? $association['joinTable']['inverseJoinColumns']
229             : $association['joinTable']['joinColumns'];
230
231         foreach ($joinColumns as $joinColumn) {
232             $columnName     = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
233             $referencedName = $joinColumn['referencedColumnName'];
234             $conditions[]   = $columnName . ' = ?';
235             $params[]       = ($class->containsForeignIdentifier)
236                 ? $id[$class->getFieldForColumn($referencedName)]
237                 : $id[$class->fieldNames[$referencedName]];
238         }
239
240         $joinTableName = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform);
241         list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($mapping);
242
243         if ($filterSql) {
244             $conditions[] = $filterSql;
245         }
246
247         $sql = 'SELECT COUNT(*)'
248             . ' FROM ' . $joinTableName . ' t'
249             . $joinTargetEntitySQL
250             . ' WHERE ' . implode(' AND ', $conditions);
251
252         return $this->_conn->fetchColumn($sql, $params);
253     }
254
255     /**
256      * @param PersistentCollection $coll
257      * @param int $offset
258      * @param int $length
259      * @return array
260      */
261     public function slice(PersistentCollection $coll, $offset, $length = null)
262     {
263         $mapping = $coll->getMapping();
264
265         return $this->_em->getUnitOfWork()->getEntityPersister($mapping['targetEntity'])->getManyToManyCollection($mapping, $coll->getOwner(), $offset, $length);
266     }
267
268     /**
269      * @param PersistentCollection $coll
270      * @param object $element
271      * @return boolean
272      */
273     public function contains(PersistentCollection $coll, $element)
274     {
275         $uow = $this->_em->getUnitOfWork();
276
277         // Shortcut for new entities
278         $entityState = $uow->getEntityState($element, UnitOfWork::STATE_NEW);
279
280         if ($entityState === UnitOfWork::STATE_NEW) {
281             return false;
282         }
283
284         // Entity is scheduled for inclusion
285         if ($entityState === UnitOfWork::STATE_MANAGED && $uow->isScheduledForInsert($element)) {
286             return false;
287         }
288
289         list($quotedJoinTable, $whereClauses, $params) = $this->getJoinTableRestrictions($coll, $element, true);
290
291         $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses);
292
293         return (bool) $this->_conn->fetchColumn($sql, $params);
294     }
295
296     /**
297      * @param PersistentCollection $coll
298      * @param object $element
299      * @return boolean
300      */
301     public function removeElement(PersistentCollection $coll, $element)
302     {
303         $uow = $this->_em->getUnitOfWork();
304
305         // shortcut for new entities
306         $entityState = $uow->getEntityState($element, UnitOfWork::STATE_NEW);
307
308         if ($entityState === UnitOfWork::STATE_NEW) {
309             return false;
310         }
311
312         // If Entity is scheduled for inclusion, it is not in this collection.
313         // We can assure that because it would have return true before on array check
314         if ($entityState === UnitOfWork::STATE_MANAGED && $uow->isScheduledForInsert($element)) {
315             return false;
316         }
317
318         list($quotedJoinTable, $whereClauses, $params) = $this->getJoinTableRestrictions($coll, $element, false);
319
320         $sql = 'DELETE FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses);
321
322         return (bool) $this->_conn->executeUpdate($sql, $params);
323     }
324
325     /**
326      * @param \Doctrine\ORM\PersistentCollection $coll
327      * @param object $element
328      * @param boolean $addFilters Whether the filter SQL should be included or not.
329      * @return array
330      */
331     private function getJoinTableRestrictions(PersistentCollection $coll, $element, $addFilters)
332     {
333         $uow     = $this->_em->getUnitOfWork();
334         $mapping = $filterMapping = $coll->getMapping();
335
336         if ( ! $mapping['isOwningSide']) {
337             $sourceClass = $this->_em->getClassMetadata($mapping['targetEntity']);
338             $targetClass = $this->_em->getClassMetadata($mapping['sourceEntity']);
339             $sourceId = $uow->getEntityIdentifier($element);
340             $targetId = $uow->getEntityIdentifier($coll->getOwner());
341
342             $mapping = $sourceClass->associationMappings[$mapping['mappedBy']];
343         } else {
344             $sourceClass = $this->_em->getClassMetadata($mapping['sourceEntity']);
345             $targetClass = $this->_em->getClassMetadata($mapping['targetEntity']);
346             $sourceId = $uow->getEntityIdentifier($coll->getOwner());
347             $targetId = $uow->getEntityIdentifier($element);
348         }
349
350         $quotedJoinTable = $this->quoteStrategy->getJoinTableName($mapping, $sourceClass, $this->platform);
351         $whereClauses    = array();
352         $params          = array();
353
354         foreach ($mapping['joinTableColumns'] as $joinTableColumn) {
355             $whereClauses[] = $joinTableColumn . ' = ?';
356
357             if (isset($mapping['relationToTargetKeyColumns'][$joinTableColumn])) {
358                 $params[] = ($targetClass->containsForeignIdentifier)
359                     ? $targetId[$targetClass->getFieldForColumn($mapping['relationToTargetKeyColumns'][$joinTableColumn])]
360                     : $targetId[$targetClass->fieldNames[$mapping['relationToTargetKeyColumns'][$joinTableColumn]]];
361                 continue;
362             }
363
364             // relationToSourceKeyColumns
365             $params[] = ($sourceClass->containsForeignIdentifier)
366                 ? $sourceId[$sourceClass->getFieldForColumn($mapping['relationToSourceKeyColumns'][$joinTableColumn])]
367                 : $sourceId[$sourceClass->fieldNames[$mapping['relationToSourceKeyColumns'][$joinTableColumn]]];
368         }
369
370         if ($addFilters) {
371             list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($filterMapping);
372             if ($filterSql) {
373                 $quotedJoinTable .= ' t ' . $joinTargetEntitySQL;
374                 $whereClauses[] = $filterSql;
375             }
376         }
377
378         return array($quotedJoinTable, $whereClauses, $params);
379     }
380
381     /**
382      * Generates the filter SQL for a given mapping.
383      *
384      * This method is not used for actually grabbing the related entities
385      * but when the extra-lazy collection methods are called on a filtered
386      * association. This is why besides the many to many table we also
387      * have to join in the actual entities table leading to additional
388      * JOIN.
389      *
390      * @param array $mapping Array containing mapping information.
391      *
392      * @return string The SQL query part to add to a query.
393      */
394     public function getFilterSql($mapping)
395     {
396         $targetClass = $this->_em->getClassMetadata($mapping['targetEntity']);
397
398         if ($mapping['isOwningSide']) {
399             $joinColumns = $mapping['relationToTargetKeyColumns'];
400         } else {
401             $mapping = $targetClass->associationMappings[$mapping['mappedBy']];
402             $joinColumns = $mapping['relationToSourceKeyColumns'];
403         }
404
405         $targetClass = $this->_em->getClassMetadata($targetClass->rootEntityName);
406
407         // A join is needed if there is filtering on the target entity
408         $joinTargetEntitySQL = '';
409         if ($filterSql = $this->generateFilterConditionSQL($targetClass, 'te')) {
410             $joinTargetEntitySQL = ' JOIN '
411                 . $this->quoteStrategy->getTableName($targetClass, $this->platform) . ' te'
412                 . ' ON';
413
414             $joinTargetEntitySQLClauses = array();
415             foreach ($joinColumns as $joinTableColumn => $targetTableColumn) {
416                 $joinTargetEntitySQLClauses[] = ' t.' . $joinTableColumn . ' = ' . 'te.' . $targetTableColumn;
417             }
418
419             $joinTargetEntitySQL .= implode(' AND ', $joinTargetEntitySQLClauses);
420         }
421
422         return array($joinTargetEntitySQL, $filterSql);
423     }
424
425     /**
426      * Generates the filter SQL for a given entity and table alias.
427      *
428      * @param ClassMetadata $targetEntity Metadata of the target entity.
429      * @param string $targetTableAlias The table alias of the joined/selected table.
430      *
431      * @return string The SQL query part to add to a query.
432      */
433     protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias)
434     {
435         $filterClauses = array();
436
437         foreach ($this->_em->getFilters()->getEnabledFilters() as $filter) {
438             if ($filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias)) {
439                 $filterClauses[] = '(' . $filterExpr . ')';
440             }
441         }
442
443         $sql = implode(' AND ', $filterClauses);
444         return $sql ? "(" . $sql . ")" : "";
445     }
446 }