diff --git a/lib/Doctrine/ORM/Query/QueryExpressionVisitor.php b/lib/Doctrine/ORM/Query/QueryExpressionVisitor.php new file mode 100644 index 000000000..6f16a3515 --- /dev/null +++ b/lib/Doctrine/ORM/Query/QueryExpressionVisitor.php @@ -0,0 +1,168 @@ +. + */ + +namespace Doctrine\ORM\Query; + +use Doctrine\Common\Collections\ArrayCollection; + +use Doctrine\Common\Collections\Expr\ExpressionVisitor; +use Doctrine\Common\Collections\Expr\Comparison; +use Doctrine\Common\Collections\Expr\CompositeExpression; +use Doctrine\Common\Collections\Expr\Value; + +use Doctrine\ORM\QueryBuilder; +use Doctrine\ORM\Query\Parameter; + +/** + * Convert Collection expressions to Query expressions + * + * @author Kirill chEbba Chebunin + * @since 2.4 + */ +class QueryExpressionVisitor extends ExpressionVisitor +{ + private static $operatorMap = array( + Comparison::GT => Expr\Comparison::GT, + Comparison::GTE => Expr\Comparison::GTE, + Comparison::LT => Expr\Comparison::LT, + Comparison::LTE => Expr\Comparison::LTE + ); + + private $expr; + private $parameters = array(); + + /** + * Constructor with internal initialization + */ + public function __construct() + { + $this->expr = new Expr(); + } + + /** + * Get bound parameters. + * Filled after {@link dispach()}. + * + * @return \Doctrine\Common\Collections\Collection + */ + public function getParameters() + { + return new ArrayCollection($this->parameters); + } + + /** + * Clear parameters + */ + public function clearParameters() + { + $this->parameters = array(); + } + + /** + * Convert Criteria expression to Query one based on static map. + * + * @param string $criteriaOperator + * + * @return string|null + */ + private static function convertComparisonOperator($criteriaOperator) + { + return isset(self::$operatorMap[$criteriaOperator]) ? self::$operatorMap[$criteriaOperator] : null; + } + + /** + * {@inheritDoc} + */ + public function walkCompositeExpression(CompositeExpression $expr) + { + $expressionList = array(); + + foreach ($expr->getExpressionList() as $child) { + $expressionList[] = $this->dispatch($child); + } + + switch($expr->getType()) { + case CompositeExpression::TYPE_AND: + return new Expr\Andx($expressionList); + + case CompositeExpression::TYPE_OR: + return new Expr\Orx($expressionList); + + default: + throw new \RuntimeException("Unknown composite " . $expr->getType()); + } + } + + /** + * {@inheritDoc} + */ + public function walkComparison(Comparison $comparison) + { + $parameterName = str_replace('.', '_', $comparison->getField()); + $parameter = new Parameter($parameterName, $this->walkValue($comparison->getValue())); + $placeholder = ':' . $parameterName; + + switch ($comparison->getOperator()) { + case Comparison::IN: + $this->parameters[] = $parameter; + return $this->expr->in($comparison->getField(), $placeholder); + + case Comparison::NIN: + $this->parameters[] = $parameter; + return $this->expr->notIn($comparison->getField(), $placeholder); + + case Comparison::EQ: + case Comparison::IS: + if ($this->walkValue($comparison->getValue()) === null) { + return $this->expr->isNull($comparison->getField()); + } + $this->parameters[] = $parameter; + return $this->expr->eq($comparison->getField(), $placeholder); + + case Comparison::NEQ: + if ($this->walkValue($comparison->getValue()) === null) { + return $this->expr->isNotNull($comparison->getField()); + } + $this->parameters[] = $parameter; + return $this->expr->neq($comparison->getField(), $placeholder); + + default: + $operator = self::convertComparisonOperator($comparison->getOperator()); + if ($operator) { + $this->parameters[] = $parameter; + return new Expr\Comparison( + $comparison->getField(), + $operator, + $placeholder + ); + } + + throw new \RuntimeException("Unknown comparison operator: " . $comparison->getOperator()); + } + + } + + /** + * {@inheritDoc} + */ + public function walkValue(Value $value) + { + return $value->getValue(); + } +} diff --git a/lib/Doctrine/ORM/QueryBuilder.php b/lib/Doctrine/ORM/QueryBuilder.php index 51ef27706..f29b7ced4 100644 --- a/lib/Doctrine/ORM/QueryBuilder.php +++ b/lib/Doctrine/ORM/QueryBuilder.php @@ -20,8 +20,10 @@ namespace Doctrine\ORM; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Criteria; use Doctrine\ORM\Query\Expr; +use Doctrine\ORM\Query\QueryExpressionVisitor; /** * This class is responsible for building DQL query strings via an object oriented @@ -1018,6 +1020,43 @@ class QueryBuilder return $this->add('orderBy', new Expr\OrderBy($sort, $order), true); } + /** + * Add criteria to query. + * Add where expressions with AND operator. + * Add orderings. + * Override firstResult and maxResults if they set. + * + * @param Criteria $criteria + * @return QueryBuilder + */ + public function addCriteria(Criteria $criteria) + { + $visitor = new QueryExpressionVisitor(); + + if ($whereExpression = $criteria->getWhereExpression()) { + $this->andWhere($visitor->dispatch($whereExpression)); + foreach ($visitor->getParameters() as $parameter) { + $this->parameters->add($parameter); + } + } + + if ($criteria->getOrderings()) { + foreach ($criteria->getOrderings() as $sort => $order) { + $this->addOrderBy($sort, $order); + } + } + + // Overwrite limits only if they was set in criteria + if (($firstResult = $criteria->getFirstResult()) !== null) { + $this->setFirstResult($firstResult); + } + if (($maxResults = $criteria->getMaxResults()) !== null) { + $this->setMaxResults($maxResults); + } + + return $this; + } + /** * Get a query part by its name. * diff --git a/tests/Doctrine/Tests/ORM/Query/QueryExpressionVisitorTest.php b/tests/Doctrine/Tests/ORM/Query/QueryExpressionVisitorTest.php new file mode 100644 index 000000000..5eaeb7646 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Query/QueryExpressionVisitorTest.php @@ -0,0 +1,134 @@ +. + */ + +namespace Doctrine\Tests\ORM\Query; + +use Doctrine\Common\Collections\ArrayCollection; + +use Doctrine\Common\Collections\Expr\Value; +use Doctrine\Common\Collections\Expr\Comparison as CriteriaComparison; +use Doctrine\ORM\Query\Expr\Comparison as QueryComparison; +use Doctrine\Common\Collections\ExpressionBuilder as CriteriaBuilder; +use Doctrine\ORM\Query\Expr as QueryBuilder; + +use Doctrine\ORM\Query\Parameter; +use Doctrine\ORM\Query\QueryExpressionVisitor; + +/** + * Test for QueryExpressionVisitor + * + * @author Kirill chEbba Chebunin + */ +class QueryExpressionVisitorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var QueryExpressionVisitor + */ + private $visitor; + + /** + * {@inheritDoc} + */ + protected function setUp() + { + $this->visitor = new QueryExpressionVisitor(); + } + + /** + * @param CriteriaComparison $criteriaExpr + * @param QueryComparison|string $queryExpr + * @param Parameter $parameter + * + * @dataProvider comparisonData + */ + public function testWalkComparison(CriteriaComparison $criteriaExpr, $queryExpr, Parameter $parameter = null) + { + $this->assertEquals($queryExpr, $this->visitor->walkComparison($criteriaExpr)); + if ($parameter) { + $this->assertEquals(new ArrayCollection(array($parameter)), $this->visitor->getParameters()); + } + } + + public function comparisonData() + { + $cb = new CriteriaBuilder(); + $qb = new QueryBuilder(); + + return array( + array($cb->eq('field', 'value'), $qb->eq('field', ':field'), new Parameter('field', 'value')), + array($cb->neq('field', 'value'), $qb->neq('field', ':field'), new Parameter('field', 'value')), + array($cb->eq('field', null), $qb->isNull('field')), + array($cb->neq('field', null), $qb->isNotNull('field')), + array($cb->isNull('field'), $qb->isNull('field')), + + array($cb->gt('field', 'value'), $qb->gt('field', ':field'), new Parameter('field', 'value')), + array($cb->gte('field', 'value'), $qb->gte('field', ':field'), new Parameter('field', 'value')), + array($cb->lt('field', 'value'), $qb->lt('field', ':field'), new Parameter('field', 'value')), + array($cb->lte('field', 'value'), $qb->lte('field', ':field'), new Parameter('field', 'value')), + + array($cb->in('field', array('value')), $qb->in('field', ':field'), new Parameter('field', array('value'))), + array($cb->notIn('field', array('value')), $qb->notIn('field', ':field'), new Parameter('field', array('value'))), + + // Test parameter conversion + array($cb->eq('object.field', 'value'), $qb->eq('object.field', ':object_field'), new Parameter('object_field', 'value')), + ); + } + + public function testWalkAndCompositeExpression() + { + $cb = new CriteriaBuilder(); + $expr = $this->visitor->walkCompositeExpression( + $cb->andX( + $cb->eq("foo", 1), + $cb->eq("bar", 1) + ) + ); + + $this->assertInstanceOf('Doctrine\ORM\Query\Expr\Andx', $expr); + $this->assertCount(2, $expr->getParts()); + } + + public function testWalkOrCompositeExpression() + { + $cb = new CriteriaBuilder(); + $expr = $this->visitor->walkCompositeExpression( + $cb->orX( + $cb->eq("foo", 1), + $cb->eq("bar", 1) + ) + ); + + $this->assertInstanceOf('Doctrine\ORM\Query\Expr\Orx', $expr); + $this->assertCount(2, $expr->getParts()); + } + + public function testWalkValue() + { + $this->assertEquals('value', $this->visitor->walkValue(new Value('value'))); + } + + public function testClearParameters() + { + $this->visitor->getParameters()->add(new Parameter('field', 'value')); + + $this->visitor->clearParameters(); + + $this->assertCount(0, $this->visitor->getParameters()); + } +} diff --git a/tests/Doctrine/Tests/ORM/QueryBuilderTest.php b/tests/Doctrine/Tests/ORM/QueryBuilderTest.php index 82ee08ba4..8f52fb8bf 100644 --- a/tests/Doctrine/Tests/ORM/QueryBuilderTest.php +++ b/tests/Doctrine/Tests/ORM/QueryBuilderTest.php @@ -19,7 +19,8 @@ namespace Doctrine\Tests\ORM; -use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\ArrayCollection, + Doctrine\Common\Collections\Criteria; use Doctrine\ORM\QueryBuilder, Doctrine\ORM\Query\Expr, @@ -38,6 +39,9 @@ require_once __DIR__ . '/../TestInit.php'; */ class QueryBuilderTest extends \Doctrine\Tests\OrmTestCase { + /** + * @var \Doctrine\ORM\EntityManager + */ private $_em; protected function setUp() @@ -371,6 +375,55 @@ class QueryBuilderTest extends \Doctrine\Tests\OrmTestCase $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u ORDER BY u.username ASC, u.username DESC'); } + public function testAddCriteriaWhere() + { + $qb = $this->_em->createQueryBuilder(); + $criteria = new Criteria(); + $criteria->where($criteria->expr()->eq('field', 'value')); + + $qb->addCriteria($criteria); + + $this->assertEquals('field = :field', (string) $qb->getDQLPart('where')); + $this->assertNotNull($qb->getParameter('field')); + } + + public function testAddCriteriaOrder() + { + $qb = $this->_em->createQueryBuilder(); + $criteria = new Criteria(); + $criteria->orderBy(array('field' => Criteria::DESC)); + + $qb->addCriteria($criteria); + + $this->assertCount(1, $orderBy = $qb->getDQLPart('orderBy')); + $this->assertEquals('field DESC', (string) $orderBy[0]); + } + + public function testAddCriteriaLimit() + { + $qb = $this->_em->createQueryBuilder(); + $criteria = new Criteria(); + $criteria->setFirstResult(2); + $criteria->setMaxResults(10); + + $qb->addCriteria($criteria); + + $this->assertEquals(2, $qb->getFirstResult()); + $this->assertEquals(10, $qb->getMaxResults()); + } + + public function testAddCriteriaUndefinedLimit() + { + $qb = $this->_em->createQueryBuilder(); + $qb->setFirstResult(2)->setMaxResults(10); + $criteria = new Criteria(); + + $qb->addCriteria($criteria); + + $this->assertEquals(2, $qb->getFirstResult()); + $this->assertEquals(10, $qb->getMaxResults()); + } + public function testGetQuery() { $qb = $this->_em->createQueryBuilder()