diff --git a/lib/Doctrine/ORM/PersistentCollection.php b/lib/Doctrine/ORM/PersistentCollection.php index 9eee0c06c..90890e906 100644 --- a/lib/Doctrine/ORM/PersistentCollection.php +++ b/lib/Doctrine/ORM/PersistentCollection.php @@ -869,8 +869,14 @@ final class PersistentCollection implements Collection, Selectable return $this->coll->matching($criteria); } - if ($this->association['type'] !== ClassMetadata::ONE_TO_MANY) { - throw new \RuntimeException("Matching Criteria on PersistentCollection only works on OneToMany associations at the moment."); + if ($this->association['type'] !== ClassMetadata::ONE_TO_MANY + && $this->association['type'] !== ClassMetadata::MANY_TO_MANY) { + throw new \RuntimeException("Matching Criteria on PersistentCollection only works on OneToMany and ManyToMany associations at the moment."); + } + + if ($this->association['type'] === ClassMetadata::MANY_TO_MANY) { + $persister = $this->em->getUnitOfWork()->getCollectionPersister($this->association); + return new ArrayCollection($persister->loadCriteria($this, $this->owner, $criteria)); } $builder = Criteria::expr(); diff --git a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php index a032982da..6d818f6bd 100644 --- a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php @@ -19,8 +19,10 @@ namespace Doctrine\ORM\Persisters; +use Doctrine\Common\Collections\Criteria; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\PersistentCollection; +use Doctrine\ORM\Query; use Doctrine\ORM\UnitOfWork; /** @@ -54,7 +56,7 @@ class ManyToManyPersister extends AbstractCollectionPersister } return 'DELETE FROM ' . $tableName - . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?'; + . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?'; } /** @@ -102,7 +104,7 @@ class ManyToManyPersister extends AbstractCollectionPersister } return 'INSERT INTO ' . $joinTable . ' (' . implode(', ', $columns) . ')' - . ' VALUES (' . implode(', ', array_fill(0, count($columns), '?')) . ')'; + . ' VALUES (' . implode(', ', array_fill(0, count($columns), '?')) . ')'; } /** @@ -178,7 +180,7 @@ class ManyToManyPersister extends AbstractCollectionPersister } return 'DELETE FROM ' . $joinTable - . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?'; + . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?'; } /** @@ -257,7 +259,11 @@ class ManyToManyPersister extends AbstractCollectionPersister } /** - * {@inheritdoc} + * @param \Doctrine\ORM\PersistentCollection $coll + * @param int $offset + * @param int|null $length + * + * @return array */ public function slice(PersistentCollection $coll, $offset, $length = null) { @@ -276,7 +282,10 @@ class ManyToManyPersister extends AbstractCollectionPersister } /** - * {@inheritdoc} + * @param \Doctrine\ORM\PersistentCollection $coll + * @param object $element + * + * @return boolean */ public function contains(PersistentCollection $coll, $element) { @@ -302,7 +311,10 @@ class ManyToManyPersister extends AbstractCollectionPersister } /** - * {@inheritdoc} + * @param \Doctrine\ORM\PersistentCollection $coll + * @param object $element + * + * @return boolean */ public function removeElement(PersistentCollection $coll, $element) { @@ -485,29 +497,12 @@ class ManyToManyPersister extends AbstractCollectionPersister return array('', ''); } - $conditions = array(); - $association = $mapping; - - if ( ! $mapping['isOwningSide']) { - $class = $this->em->getClassMetadata($mapping['targetEntity']); - $association = $class->associationMappings[$mapping['mappedBy']]; - } - // A join is needed if there is filtering on the target entity - $tableName = $this->quoteStrategy->getTableName($rootClass, $this->platform); - $joinSql = ' JOIN ' . $tableName . ' te' . ' ON'; - $joinColumns = $mapping['isOwningSide'] - ? $association['joinTable']['inverseJoinColumns'] - : $association['joinTable']['joinColumns']; + $tableName = $this->quoteStrategy->getTableName($rootClass, $this->platform); + $joinSql = ' JOIN ' . $tableName . ' te' . ' ON'; + $onConditions = $this->getOnConditionSQL($mapping); - foreach ($joinColumns as $joinColumn) { - $joinColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); - $refColumnName = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); - - $conditions[] = ' t.' . $joinColumnName . ' = ' . 'te.' . $refColumnName; - } - - $joinSql .= implode(' AND ', $conditions); + $joinSql .= implode(' AND ', $onConditions); return array($joinSql, $filterSql); } @@ -533,4 +528,101 @@ class ManyToManyPersister extends AbstractCollectionPersister $sql = implode(' AND ', $filterClauses); return $sql ? "(" . $sql . ")" : ""; } + + /** + * Generate ON condition + * + * @param array $mapping + * @return array + */ + protected function getOnConditionSQL($mapping) + { + $association = $mapping; + + if ( ! $mapping['isOwningSide']) { + $class = $this->em->getClassMetadata($mapping['targetEntity']); + $association = $class->associationMappings[$mapping['mappedBy']]; + } + + $targetClass = $this->em->getClassMetadata($mapping['targetEntity']); + + $joinColumns = $mapping['isOwningSide'] + ? $association['joinTable']['inverseJoinColumns'] + : $association['joinTable']['joinColumns']; + + $conditions = array(); + + foreach ($joinColumns as $joinColumn) { + $joinColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); + $refColumnName = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); + + $conditions[] = ' t.' . $joinColumnName . ' = ' . 'te.' . $refColumnName; + } + + return $conditions; + } + + /** + * Loads Entities matching the given Criteria object. + * + * @param PersistentCollection $coll + * @param object $owner + * @param \Doctrine\Common\Collections\Criteria $criteria + * @return array + */ + public function loadCriteria(PersistentCollection $coll, $owner, Criteria $criteria) + { + list($quotedJoinTable, $whereClauses, $params) = $this->getJoinTableRestrictions($coll, $owner, true); + + $parameters = $this->expandCriteriaParameters($criteria); + + foreach ($parameters as $parameter) { + list($name, $value) = $parameter; + $whereClauses[] = sprintf("te.%s = ?", $name); + $params[] = $value; + } + + $mapping = $coll->getMapping(); + $targetClass = $this->em->getClassMetadata($mapping['targetEntity']); + $tableName = $this->quoteStrategy->getTableName($targetClass, $this->platform); + $onConditions = $this->getOnConditionSQL($mapping); + + $sql = 'SELECT * FROM ' . $tableName . ' te' + . ' JOIN ' . $quotedJoinTable . ' ON' + . implode(' AND ', $onConditions) + . ' WHERE ' . implode(' AND ', $whereClauses); + + $stmt = $this->conn->executeQuery($sql, $params); + $hydrator = $this->em->newHydrator(Query::HYDRATE_ARRAY); + + $rsm = new Query\ResultSetMapping(); + $rsm->addEntityResult($mapping['targetEntity'], 'r'); + + return $hydrator->hydrateAll($stmt, $rsm); + } + + /** + * Expands Criteria Parameters by walking the expressions and grabbing all + * parameters and types from it. + * + * @param \Doctrine\Common\Collections\Criteria $criteria + * + * @return array(array(), array()) + */ + private function expandCriteriaParameters(Criteria $criteria) + { + $expression = $criteria->getWhereExpression(); + + if ($expression === null) { + return array(array(), array()); + } + + $valueVisitor = new SqlValueVisitor(); + + $valueVisitor->dispatch($expression); + + list($values, $types) = $valueVisitor->getParamsAndTypes(); + + return $types; + } }