From b2385e0afa5e36adcdfbdc8d58e0fc963e45fdc7 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Tue, 19 Jun 2012 00:01:03 +0200 Subject: [PATCH] [DDC-1637] Implementation of Criteria Collections API for PersistentCollection (OneToMany only) and EntityRepository. --- lib/Doctrine/ORM/EntityRepository.php | 40 ++- lib/Doctrine/ORM/PersistentCollection.php | 67 ++++- .../ORM/Persisters/BasicEntityPersister.php | 228 ++++++++++++++---- .../Persisters/JoinedSubclassPersister.php | 19 +- .../ORM/Persisters/SingleTablePersister.php | 29 ++- .../ORM/Persisters/SqlExpressionVisitor.php | 102 ++++++++ .../ORM/Persisters/SqlValueVisitor.php | 126 ++++++++++ .../Functional/ClassTableInheritanceTest.php | 31 ++- .../ORM/Functional/EntityRepositoryTest.php | 134 ++++++++++ .../OneToManyBidirectionalAssociationTest.php | 23 ++ .../Functional/SingleTableInheritanceTest.php | 23 +- 11 files changed, 746 insertions(+), 76 deletions(-) create mode 100644 lib/Doctrine/ORM/Persisters/SqlExpressionVisitor.php create mode 100644 lib/Doctrine/ORM/Persisters/SqlValueVisitor.php diff --git a/lib/Doctrine/ORM/EntityRepository.php b/lib/Doctrine/ORM/EntityRepository.php index 7656d3eda..72ae8c589 100644 --- a/lib/Doctrine/ORM/EntityRepository.php +++ b/lib/Doctrine/ORM/EntityRepository.php @@ -22,6 +22,11 @@ namespace Doctrine\ORM; use Doctrine\DBAL\LockMode; use Doctrine\Common\Persistence\ObjectRepository; +use Doctrine\Common\Collections\Selectable; +use Doctrine\Common\Collections\Criteria; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\ExpressionBuilder; + /** * An EntityRepository serves as a repository for entities with generic as well as * business specific methods for retrieving entities. @@ -35,8 +40,13 @@ use Doctrine\Common\Persistence\ObjectRepository; * @author Jonathan Wage * @author Roman Borschel */ -class EntityRepository implements ObjectRepository +class EntityRepository implements ObjectRepository, Selectable { + /** + * @var Doctrine\Common\Collections\ExpressionBuilder + */ + private static $expressionBuilder; + /** * @var string */ @@ -308,4 +318,32 @@ class EntityRepository implements ObjectRepository { return $this->_class; } + + /** + * Select all elements from a selectable that match the expression and + * return a new collection containing these elements. + * + * @param \Doctrine\Common\Collections\Criteria $criteria + * + * @return \Doctrine\Common\Collections\Collection + */ + public function matching(Criteria $criteria) + { + $persister = $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName); + + return new ArrayCollection($persister->loadCriteria($criteria)); + } + + /** + * Return Builder object that helps with building criteria expressions. + * + * @return \Doctrine\Common\Collections\ExpressionBuilder + */ + public function expr() + { + if (self::$expressionBuilder === null) { + self::$expressionBuilder = new ExpressionBuilder(); + } + return self::$expressionBuilder; + } } diff --git a/lib/Doctrine/ORM/PersistentCollection.php b/lib/Doctrine/ORM/PersistentCollection.php index bb8bffd95..4a8ca0610 100644 --- a/lib/Doctrine/ORM/PersistentCollection.php +++ b/lib/Doctrine/ORM/PersistentCollection.php @@ -19,10 +19,15 @@ namespace Doctrine\ORM; -use Doctrine\ORM\Mapping\ClassMetadata, - Doctrine\Common\Collections\Collection, - Doctrine\Common\Collections\ArrayCollection, - Closure; +use Doctrine\ORM\Mapping\ClassMetadata; + +use Doctrine\Common\Collections\Collection; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Selectable; +use Doctrine\Common\Collections\Criteria; +use Doctrine\Common\Collections\ExpressionBuilder; + +use Closure; /** * A PersistentCollection represents a collection of elements that have persistent state. @@ -39,8 +44,13 @@ use Doctrine\ORM\Mapping\ClassMetadata, * @author Giorgio Sironi * @todo Design for inheritance to allow custom implementations? */ -final class PersistentCollection implements Collection +final class PersistentCollection implements Collection, Selectable { + /** + * @var Doctrine\Common\Collections\ExpressionBuilder + */ + static private $expressionBuilder; + /** * A snapshot of the collection at the moment it was fetched from the database. * This is used to create a diff of the collection at commit time. @@ -789,4 +799,51 @@ final class PersistentCollection implements Collection $this->changed(); } + + /** + * Select all elements from a selectable that match the expression and + * return a new collection containing these elements. + * + * @param \Doctrine\Common\Collections\Criteria $criteria + * @return Collection + */ + public function matching(Criteria $criteria) + { + if ($this->initialized) { + return $this->coll->matching($criteria); + } + + if ($this->association['type'] !== ClassMetadata::ONE_TO_MANY) { + throw new \RuntimeException("Matching Criteria on PersistentCollection only works on OneToMany assocations at the moment."); + } + + $targetClass = $this->em->getClassMetadata(get_class($this->owner)); + + $id = $targetClass->getSingleIdReflectionProperty()->getValue($this->owner); + $builder = $this->expr(); + $ownerExpression = $builder->eq($this->backRefFieldName, $id); + $expression = $criteria->getWhereExpression(); + $expression = $expression ? $builder->andX($expression, $ownerExpression) : $ownerExpression; + + $criteria->where($expression); + + $persister = $this->em->getUnitOfWork()->getEntityPersister($this->association['targetEntity']); + + return new ArrayCollection($persister->loadCriteria($criteria)); + } + + /** + * Return Builder object that helps with building criteria expressions. + * + * @return \Doctrine\Common\Collections\ExpressionBuilder + */ + public function expr() + { + if (self::$expressionBuilder === null) { + self::$expressionBuilder = new ExpressionBuilder(); + } + + return self::$expressionBuilder; + } } + diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index f6a2b1b55..ed3485386 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -19,21 +19,26 @@ namespace Doctrine\ORM\Persisters; -use PDO, - Doctrine\DBAL\LockMode, - Doctrine\DBAL\Types\Type, - Doctrine\DBAL\Connection, - Doctrine\ORM\ORMException, - Doctrine\ORM\OptimisticLockException, - Doctrine\ORM\EntityManager, - Doctrine\ORM\UnitOfWork, - Doctrine\ORM\Query, - Doctrine\ORM\PersistentCollection, - Doctrine\ORM\Mapping\MappingException, - Doctrine\ORM\Mapping\ClassMetadata, - Doctrine\ORM\Events, - Doctrine\ORM\Event\LifecycleEventArgs, - Doctrine\Common\Util\ClassUtils; +use PDO; + +use Doctrine\DBAL\LockMode; +use Doctrine\DBAL\Types\Type; +use Doctrine\DBAL\Connection; + +use Doctrine\ORM\ORMException; +use Doctrine\ORM\OptimisticLockException; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\UnitOfWork; +use Doctrine\ORM\Query; +use Doctrine\ORM\PersistentCollection; +use Doctrine\ORM\Mapping\MappingException; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Events; +use Doctrine\ORM\Event\LifecycleEventArgs; + +use Doctrine\Common\Util\ClassUtils; +use Doctrine\Common\Collections\Criteria; +use Doctrine\Common\Collections\Expr\Comparison; /** * A BasicEntityPersiter maps an entity to a single table in a relational database. @@ -78,6 +83,21 @@ use PDO, */ class BasicEntityPersister { + /** + * @var array + */ + static private $comparisonMap = array( + Comparison::EQ => '= %s', + Comparison::IS => '= %s', + Comparison::NEQ => '!= %s', + Comparison::GT => '> %s', + Comparison::GTE => '>= %s', + Comparison::LT => '< %s', + Comparison::LTE => '<= %s', + Comparison::IN => 'IN (%s)', + Comparison::NIN => 'NOT IN (%s)', + ); + /** * Metadata object that describes the mapping of the mapped entity class. * @@ -748,6 +768,52 @@ class BasicEntityPersister $hydrator->hydrateAll($stmt, $this->_rsm, array(Query::HINT_REFRESH => true)); } + /** + * Load Entities matching the given Criteria object + * + * @param \Doctrine\Common\Collections\Criteria $criteria + * + * @return array + */ + public function loadCriteria(Criteria $criteria) + { + $orderBy = $criteria->getOrderings(); + $limit = $criteria->getMaxResults(); + $offset = $criteria->getFirstResult(); + + $sql = $this->_getSelectEntitiesSQL($criteria, null, 0, $limit, $offset, $orderBy); + + list($params, $types) = $this->expandCriteriaParameters($criteria); + + $stmt = $this->_conn->executeQuery($sql, $params, $types); + + $hydrator = $this->_em->newHydrator(($this->_selectJoinSql) ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT); + + return $hydrator->hydrateAll($stmt, $this->_rsm, array('deferEagerLoads' => true)); + } + + /** + * Expand 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($this->_class); + $valueVisitor->dispatch($expression); + + return $valueVisitor->getParamsAndTypes(); + } + /** * Loads a list of entities by a list of field criteria. * @@ -920,7 +986,7 @@ class BasicEntityPersister /** * Gets the SELECT SQL to select one or more entities by a set of field criteria. * - * @param array $criteria + * @param array|\Doctrine\Common\Collections\Criteria $criteria * @param AssociationMapping $assoc * @param string $orderBy * @param int $lockMode @@ -930,10 +996,12 @@ class BasicEntityPersister * @return string * @todo Refactor: _getSelectSQL(...) */ - protected function _getSelectEntitiesSQL(array $criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null) + protected function _getSelectEntitiesSQL($criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null) { $joinSql = ($assoc != null && $assoc['type'] == ClassMetadata::MANY_TO_MANY) ? $this->_getSelectManyToManyJoinSQL($assoc) : ''; - $conditionSql = $this->_getSelectConditionSQL($criteria, $assoc); + $conditionSql = ($criteria instanceof Criteria) + ? $this->_getSelectConditionCriteriaSQL($criteria) + : $this->_getSelectConditionSQL($criteria, $assoc); $orderBy = ($assoc !== null && isset($assoc['orderBy'])) ? $assoc['orderBy'] : $orderBy; $orderBySql = $orderBy ? $this->_getOrderBySQL($orderBy, $this->_getSQLTableAlias($this->_class->name)) : ''; @@ -1336,6 +1404,93 @@ class BasicEntityPersister . $this->_getSQLTableAlias($this->_class->name); } + /** + * Get the Select Where Condition from a Criteria object. + * + * @param \Doctrine\Common\Collections\Criteria $criteria + * @return string + */ + protected function _getSelectConditionCriteriaSQL(Criteria $criteria) + { + $expression = $criteria->getWhereExpression(); + + if ($expression === null) { + return ''; + } + + $visitor = new SqlExpressionVisitor($this); + + return $visitor->dispatch($expression); + } + + /** + * Get the SQL WHERE condition for matching a field with a given value. + * + * @param string $field + * @param mixed $value + * @param array|null $assoc + * @param string $comparison + * + * @return string + */ + public function getSelectConditionStatementSQL($field, $value, $assoc = null, $comparison = null) + { + $conditionSql = $this->getSelectConditionStatementColumnSQL($field, $assoc); + $placeholder = '?'; + + if (isset($this->_class->fieldMappings[$field]['requireSQLConversion'])) { + $type = Type::getType($this->_class->getTypeOfField($field)); + $placeholder = $type->convertToDatabaseValueSQL($placeholder, $this->_platform); + } + + $conditionSql .= ($comparison === null) + ? ((is_array($value)) ? ' IN (?)' : (($value === null) ? ' IS NULL' : ' = ' . $placeholder)) + : ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder); + + + return $conditionSql; + } + + /** + * Build the left-hand-side of a where condition statement. + * + * @param string $field + * @param array $assoc + * + * @return string + */ + protected function getSelectConditionStatementColumnSQL($field, $assoc = null) + { + switch (true) { + case (isset($this->_class->columnNames[$field])): + $className = (isset($this->_class->fieldMappings[$field]['inherited'])) + ? $this->_class->fieldMappings[$field]['inherited'] + : $this->_class->name; + + return $this->_getSQLTableAlias($className) . '.' . $this->quoteStrategy->getColumnName($field, $this->_class, $this->_platform); + + case (isset($this->_class->associationMappings[$field])): + if ( ! $this->_class->associationMappings[$field]['isOwningSide']) { + throw ORMException::invalidFindByInverseAssociation($this->_class->name, $field); + } + + $className = (isset($this->_class->associationMappings[$field]['inherited'])) + ? $this->_class->associationMappings[$field]['inherited'] + : $this->_class->name; + + return $this->_getSQLTableAlias($className) . '.' . $this->_class->associationMappings[$field]['joinColumns'][0]['name']; + + case ($assoc !== null && strpos($field, " ") === false && strpos($field, "(") === false): + // very careless developers could potentially open up this normally hidden api for userland attacks, + // therefore checking for spaces and function calls which are not allowed. + + // found a join column condition, not really a "field" + return $field; + } + + throw ORMException::unrecognizedField($field); + } + /** * Gets the conditional SQL fragment used in the WHERE clause when selecting * entities in this persister. @@ -1353,42 +1508,9 @@ class BasicEntityPersister foreach ($criteria as $field => $value) { $conditionSql .= $conditionSql ? ' AND ' : ''; - - $placeholder = '?'; - - if (isset($this->_class->columnNames[$field])) { - $className = (isset($this->_class->fieldMappings[$field]['inherited'])) - ? $this->_class->fieldMappings[$field]['inherited'] - : $this->_class->name; - - $conditionSql .= $this->_getSQLTableAlias($className) . '.' . $this->quoteStrategy->getColumnName($field, $this->_class, $this->_platform); - - if (isset($this->_class->fieldMappings[$field]['requireSQLConversion'])) { - $type = Type::getType($this->_class->getTypeOfField($field)); - $placeholder = $type->convertToDatabaseValueSQL($placeholder, $this->_platform); - } - } else if (isset($this->_class->associationMappings[$field])) { - if ( ! $this->_class->associationMappings[$field]['isOwningSide']) { - throw ORMException::invalidFindByInverseAssociation($this->_class->name, $field); - } - - $className = (isset($this->_class->associationMappings[$field]['inherited'])) - ? $this->_class->associationMappings[$field]['inherited'] - : $this->_class->name; - - $conditionSql .= $this->_getSQLTableAlias($className) . '.' . $this->_class->associationMappings[$field]['joinColumns'][0]['name']; - } else if ($assoc !== null && strpos($field, " ") === false && strpos($field, "(") === false) { - // very careless developers could potentially open up this normally hidden api for userland attacks, - // therefore checking for spaces and function calls which are not allowed. - - // found a join column condition, not really a "field" - $conditionSql .= $field; - } else { - throw ORMException::unrecognizedField($field); - } - - $conditionSql .= (is_array($value)) ? ' IN (?)' : (($value === null) ? ' IS NULL' : ' = ' . $placeholder); + $conditionSql .= $this->getSelectConditionStatementSQL($field, $value, $assoc); } + return $conditionSql; } diff --git a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php index b41fbaede..431b237a6 100644 --- a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php +++ b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php @@ -19,11 +19,14 @@ namespace Doctrine\ORM\Persisters; -use Doctrine\ORM\ORMException, - Doctrine\ORM\Mapping\ClassMetadata, - Doctrine\DBAL\LockMode, - Doctrine\DBAL\Types\Type, - Doctrine\ORM\Query\ResultSetMapping; +use Doctrine\ORM\ORMException; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Query\ResultSetMapping; + +use Doctrine\DBAL\LockMode; +use Doctrine\DBAL\Types\Type; + +use Doctrine\Common\Collections\Criteria; /** * The joined subclass persister maps a single entity instance to several tables in the @@ -264,7 +267,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister /** * {@inheritdoc} */ - protected function _getSelectEntitiesSQL(array $criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null) + protected function _getSelectEntitiesSQL($criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null) { $idColumns = $this->_class->getIdentifierColumnNames(); $baseTableAlias = $this->_getSQLTableAlias($this->_class->name); @@ -373,7 +376,9 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister $joinSql .= ($assoc != null && $assoc['type'] == ClassMetadata::MANY_TO_MANY) ? $this->_getSelectManyToManyJoinSQL($assoc) : ''; - $conditionSql = $this->_getSelectConditionSQL($criteria, $assoc); + $conditionSql = ($criteria instanceof Criteria) + ? $this->_getSelectConditionCriteriaSQL($criteria) + : $this->_getSelectConditionSQL($criteria, $assoc); // If the current class in the root entity, add the filters if ($filterSql = $this->generateFilterConditionSQL($this->_em->getClassMetadata($this->_class->rootEntityName), $this->_getSQLTableAlias($this->_class->rootEntityName))) { diff --git a/lib/Doctrine/ORM/Persisters/SingleTablePersister.php b/lib/Doctrine/ORM/Persisters/SingleTablePersister.php index 5b2ce205b..9fedcaa00 100644 --- a/lib/Doctrine/ORM/Persisters/SingleTablePersister.php +++ b/lib/Doctrine/ORM/Persisters/SingleTablePersister.php @@ -20,6 +20,7 @@ namespace Doctrine\ORM\Persisters; use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\Common\Collections\Criteria; /** * Persister for entities that participate in a hierarchy mapped with the @@ -113,9 +114,27 @@ class SingleTablePersister extends AbstractEntityInheritancePersister { $conditionSql = parent::_getSelectConditionSQL($criteria, $assoc); - // Append discriminator condition - if ($conditionSql) $conditionSql .= ' AND '; + if ($conditionSql) { + $conditionSql .= ' AND '; + } + return $conditionSql . $this->_getSelectConditionDiscriminatorValueSQL(); + } + + /** {@inheritdoc} */ + protected function _getSelectConditionCriteriaSQL(Criteria $criteria) + { + $conditionSql = parent::_getSelectConditionCriteriaSQL($criteria); + + if ($conditionSql) { + $conditionSql .= ' AND '; + } + + return $conditionSql . $this->_getSelectConditionDiscriminatorValueSQL(); + } + + protected function _getSelectConditionDiscriminatorValueSQL() + { $values = array(); if ($this->_class->discriminatorValue !== null) { // discriminators can be 0 @@ -128,10 +147,8 @@ class SingleTablePersister extends AbstractEntityInheritancePersister $values[] = $this->_conn->quote($discrValues[$subclassName]); } - $conditionSql .= $this->_getSQLTableAlias($this->_class->name) . '.' . $this->_class->discriminatorColumn['name'] - . ' IN (' . implode(', ', $values) . ')'; - - return $conditionSql; + return $this->_getSQLTableAlias($this->_class->name) . '.' . $this->_class->discriminatorColumn['name'] + . ' IN (' . implode(', ', $values) . ')'; } /** {@inheritdoc} */ diff --git a/lib/Doctrine/ORM/Persisters/SqlExpressionVisitor.php b/lib/Doctrine/ORM/Persisters/SqlExpressionVisitor.php new file mode 100644 index 000000000..2fb685f98 --- /dev/null +++ b/lib/Doctrine/ORM/Persisters/SqlExpressionVisitor.php @@ -0,0 +1,102 @@ +. + */ + +namespace Doctrine\ORM\Persisters; + +use Doctrine\Common\Collections\Expr\ExpressionVisitor; +use Doctrine\Common\Collections\Expr\Comparison; +use Doctrine\Common\Collections\Expr\Value; +use Doctrine\Common\Collections\Expr\CompositeExpression; + +/** + * Visit Expressions and generate SQL WHERE conditions from them. + * + * @author Benjamin Eberlei + * @since 2.3 + */ +class SqlExpressionVisitor extends ExpressionVisitor +{ + /** + * @var \Doctrine\ORM\Persisters\BasicEntityPersister + */ + private $persister; + + /** + * @param \Doctrine\ORM\Persisters\BasicEntityPersister $persister + */ + public function __construct(BasicEntityPersister $persister) + { + $this->persister = $persister; + } + + /** + * Convert a comparison expression into the target query language output + * + * @param \Doctrine\Common\Collections\Expr\Comparison $comparison + * + * @return mixed + */ + public function walkComparison(Comparison $comparison) + { + $field = $comparison->getField(); + $value = $comparison->getValue()->getValue(); // shortcut for walkValue() + + return $this->persister->getSelectConditionStatementSQL($field, $value, null, $comparison->getOperator()); + } + + /** + * Convert a composite expression into the target query language output + * + * @param \Doctrine\Common\Collections\Expr\CompositeExpression $expr + * + * @return mixed + */ + public function walkCompositeExpression(CompositeExpression $expr) + { + $expressionList = array(); + + foreach ($expr->getExpressionList() as $child) { + $expressionList[] = $this->dispatch($child); + } + + switch($expr->getType()) { + case CompositeExpression::TYPE_AND: + return '(' . implode(' AND ', $expressionList) . ')'; + + case CompositeExpression::TYPE_OR: + return '(' . implode(' OR ', $expressionList) . ')'; + + default: + throw new \RuntimeException("Unknown composite " . $expr->getType()); + } + } + + /** + * Convert a value expression into the target query language part. + * + * @param \Doctrine\Common\Collections\Expr\Value $value + * + * @return mixed + */ + public function walkValue(Value $value) + { + return '?'; + } +} + diff --git a/lib/Doctrine/ORM/Persisters/SqlValueVisitor.php b/lib/Doctrine/ORM/Persisters/SqlValueVisitor.php new file mode 100644 index 000000000..043be12b8 --- /dev/null +++ b/lib/Doctrine/ORM/Persisters/SqlValueVisitor.php @@ -0,0 +1,126 @@ +. + */ + +namespace Doctrine\ORM\Persisters; + +use Doctrine\Common\Collections\Expr\ExpressionVisitor; +use Doctrine\Common\Collections\Expr\Comparison; +use Doctrine\Common\Collections\Expr\Value; +use Doctrine\Common\Collections\Expr\CompositeExpression; + +use Doctrine\ORM\Mapping\ClassMetadata; + +use Doctrine\DBAL\Types\Type; +use Doctrine\DBAL\Connection; + +/** + * Extract the values from a criteria/expression + * + * @author Benjamin Eberlei + */ +class SqlValueVisitor extends ExpressionVisitor +{ + /** + * @var array + */ + private $values = array(); + + /** + * @var array + */ + private $types = array(); + + /** + * @var \Doctrine\ORM\Mapping\ClassMetadata + */ + private $class; + + /** + * @param \Doctrine\ORM\Mapping\ClassMetadata + */ + public function __construct(ClassMetadata $class) + { + $this->class = $class; + } + + /** + * Convert a comparison expression into the target query language output + * + * @param \Doctrine\Common\Collections\Expr\Comparison $comparison + * + * @return mixed + */ + public function walkComparison(Comparison $comparison) + { + $value = $comparison->getValue()->getValue(); + $field = $comparison->getField(); + + $this->values[] = $value; + $this->types[] = $this->getType($field, $value); + } + + /** + * Convert a composite expression into the target query language output + * + * @param \Doctrine\Common\Collections\Expr\CompositeExpression $expr + * + * @return mixed + */ + public function walkCompositeExpression(CompositeExpression $expr) + { + foreach ($expr->getExpressionList() as $child) { + $this->dispatch($child); + } + } + + /** + * Convert a value expression into the target query language part. + * + * @param \Doctrine\Common\Collections\Expr\Value $value + * + * @return mixed + */ + public function walkValue(Value $value) + { + return; + } + + /** + * Return the Parameters and Types necessary for matching the last visited expression. + * + * @return array + */ + public function getParamsAndTypes() + { + return array($this->values, $this->types); + } + + private function getType($field, $value) + { + $type = isset($this->class->fieldMappings[$field]) + ? Type::getType($this->class->fieldMappings[$field]['type'])->getBindingType() + : \PDO::PARAM_STR; + + if (is_array($value)) { + $type += Connection::ARRAY_PARAM_OFFSET; + } + + return $type; + } +} diff --git a/tests/Doctrine/Tests/ORM/Functional/ClassTableInheritanceTest.php b/tests/Doctrine/Tests/ORM/Functional/ClassTableInheritanceTest.php index bd6c7943f..f7a6e93d7 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ClassTableInheritanceTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ClassTableInheritanceTest.php @@ -2,8 +2,6 @@ namespace Doctrine\Tests\ORM\Functional; -require_once __DIR__ . '/../../TestInit.php'; - use Doctrine\Tests\Models\Company\CompanyPerson, Doctrine\Tests\Models\Company\CompanyEmployee, Doctrine\Tests\Models\Company\CompanyManager, @@ -13,6 +11,8 @@ use Doctrine\Tests\Models\Company\CompanyPerson, Doctrine\Tests\Models\Company\CompanyRaffle, Doctrine\Tests\Models\Company\CompanyCar; +use Doctrine\Common\Collections\Criteria; + /** * Functional tests for the Class Table Inheritance mapping strategy. * @@ -467,4 +467,31 @@ class ClassTableInheritanceTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertTrue($this->_em->getUnitOfWork()->getEntityPersister(get_class($manager))->exists($manager)); } + + /** + * @group DDC-1637 + */ + public function testMatching() + { + $manager = new CompanyManager(); + $manager->setName('gblanco'); + $manager->setSalary(1234); + $manager->setTitle('Awesome!'); + $manager->setDepartment('IT'); + + $this->_em->persist($manager); + $this->_em->flush(); + + $repository = $this->_em->getRepository("Doctrine\Tests\Models\Company\CompanyEmployee"); + $users = $repository->matching(new Criteria( + $repository->expr()->eq('department', 'IT') + )); + $this->assertEquals(1, count($users)); + + $repository = $this->_em->getRepository("Doctrine\Tests\Models\Company\CompanyManager"); + $users = $repository->matching(new Criteria( + $repository->expr()->eq('department', 'IT') + )); + $this->assertEquals(1, count($users)); + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php index ef754ffb0..4fadeb637 100644 --- a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php @@ -5,6 +5,7 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\Tests\Models\CMS\CmsUser; use Doctrine\Tests\Models\CMS\CmsAddress; use Doctrine\Tests\Models\CMS\CmsPhonenumber; +use Doctrine\Common\Collections\Criteria; require_once __DIR__ . '/../../TestInit.php'; @@ -558,5 +559,138 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals(array(1,2,3), $query['params'][0]); $this->assertEquals(\Doctrine\DBAL\Connection::PARAM_INT_ARRAY, $query['types'][0]); } + + /** + * @group DDC-1637 + */ + public function testMatchingEmptyCriteria() + { + $this->loadFixture(); + + $repository = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); + $users = $repository->matching(new Criteria()); + + $this->assertEquals(4, count($users)); + } + + /** + * @group DDC-1637 + */ + public function testMatchingCriteriaEqComparison() + { + $this->loadFixture(); + + $repository = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); + $users = $repository->matching(new Criteria( + $repository->expr()->eq('username', 'beberlei') + )); + + $this->assertEquals(1, count($users)); + } + + /** + * @group DDC-1637 + */ + public function testMatchingCriteriaNeqComparison() + { + $this->loadFixture(); + + $repository = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); + $users = $repository->matching(new Criteria( + $repository->expr()->neq('username', 'beberlei') + )); + + $this->assertEquals(3, count($users)); + } + + /** + * @group DDC-1637 + */ + public function testMatchingCriteriaInComparison() + { + $this->loadFixture(); + + $repository = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); + $users = $repository->matching(new Criteria( + $repository->expr()->in('username', array('beberlei', 'gblanco')) + )); + + $this->assertEquals(2, count($users)); + } + + /** + * @group DDC-1637 + */ + public function testMatchingCriteriaNotInComparison() + { + $this->loadFixture(); + + $repository = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); + $users = $repository->matching(new Criteria( + $repository->expr()->notIn('username', array('beberlei', 'gblanco', 'asm89')) + )); + + $this->assertEquals(1, count($users)); + } + + /** + * @group DDC-1637 + */ + public function testMatchingCriteriaLtComparison() + { + $firstUserId = $this->loadFixture(); + + $repository = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); + $users = $repository->matching(new Criteria( + $repository->expr()->lt('id', $firstUserId + 1) + )); + + $this->assertEquals(1, count($users)); + } + + /** + * @group DDC-1637 + */ + public function testMatchingCriteriaLeComparison() + { + $firstUserId = $this->loadFixture(); + + $repository = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); + $users = $repository->matching(new Criteria( + $repository->expr()->lte('id', $firstUserId + 1) + )); + + $this->assertEquals(2, count($users)); + } + + /** + * @group DDC-1637 + */ + public function testMatchingCriteriaGtComparison() + { + $firstUserId = $this->loadFixture(); + + $repository = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); + $users = $repository->matching(new Criteria( + $repository->expr()->gt('id', $firstUserId) + )); + + $this->assertEquals(3, count($users)); + } + + /** + * @group DDC-1637 + */ + public function testMatchingCriteriaGteComparison() + { + $firstUserId = $this->loadFixture(); + + $repository = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); + $users = $repository->matching(new Criteria( + $repository->expr()->gte('id', $firstUserId) + )); + + $this->assertEquals(4, count($users)); + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/OneToManyBidirectionalAssociationTest.php b/tests/Doctrine/Tests/ORM/Functional/OneToManyBidirectionalAssociationTest.php index a9258dacf..18928f1d8 100644 --- a/tests/Doctrine/Tests/ORM/Functional/OneToManyBidirectionalAssociationTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/OneToManyBidirectionalAssociationTest.php @@ -151,6 +151,29 @@ class OneToManyBidirectionalAssociationTest extends \Doctrine\Tests\OrmFunctiona $this->assertEquals(0, count($features)); } + /** + * @group DDC-1637 + */ + public function testMatching() + { + $this->_createFixture(); + + $product = $this->_em->find('Doctrine\Tests\Models\ECommerce\ECommerceProduct', $this->product->getId()); + $features = $product->getFeatures(); + + $results = $features->matching(new \Doctrine\Common\Collections\Criteria( + $features->expr()->eq('description', 'Model writing tutorial') + )); + + $this->assertInstanceOf('Doctrine\Common\Collections\Collection', $results); + $this->assertEquals(1, count($results)); + + $results = $features->matching(new \Doctrine\Common\Collections\Criteria()); + + $this->assertInstanceOf('Doctrine\Common\Collections\Collection', $results); + $this->assertEquals(2, count($results)); + } + private function _createFixture() { $this->product->addFeature($this->firstFeature); diff --git a/tests/Doctrine/Tests/ORM/Functional/SingleTableInheritanceTest.php b/tests/Doctrine/Tests/ORM/Functional/SingleTableInheritanceTest.php index 7271f42c3..47d772542 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SingleTableInheritanceTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SingleTableInheritanceTest.php @@ -3,8 +3,7 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\ORM\Mapping\ClassMetadata; - -require_once __DIR__ . '/../../TestInit.php'; +use Doctrine\Common\Collections\Criteria; class SingleTableInheritanceTest extends \Doctrine\Tests\OrmFunctionalTestCase { @@ -335,6 +334,26 @@ class SingleTableInheritanceTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals(1, count($contracts), "There should be 1 entities related to " . $this->salesPerson->getId() . " for 'Doctrine\Tests\Models\Company\CompanyFlexUltraContract'"); } + /** + * @group DDC-1637 + */ + public function testInheritanceMatching() + { + $this->loadFullFixture(); + + $repository = $this->_em->getRepository("Doctrine\Tests\Models\Company\CompanyContract"); + $contracts = $repository->matching(new Criteria( + $repository->expr()->eq('salesPerson', $this->salesPerson->getId()) + )); + $this->assertEquals(3, count($contracts)); + + $repository = $this->_em->getRepository("Doctrine\Tests\Models\Company\CompanyFixContract"); + $contracts = $repository->matching(new Criteria( + $repository->expr()->eq('salesPerson', $this->salesPerson->getId()) + )); + $this->assertEquals(1, count($contracts)); + } + /** * @group DDC-834 */