From a9ed0085d2b646036b1c7d5a1b1e66b1334396ba Mon Sep 17 00:00:00 2001 From: jwage Date: Thu, 9 Jul 2009 21:56:34 +0000 Subject: [PATCH] [2.0] More work on the QueryBuilder and Expr classes --- lib/Doctrine/ORM/Query/Expr.php | 59 ++++- lib/Doctrine/ORM/QueryBuilder.php | 212 +++++++++++++----- tests/Doctrine/Tests/ORM/Query/ExprTest.php | 40 ++++ tests/Doctrine/Tests/ORM/QueryBuilderTest.php | 200 ++++++++--------- 4 files changed, 342 insertions(+), 169 deletions(-) diff --git a/lib/Doctrine/ORM/Query/Expr.php b/lib/Doctrine/ORM/Query/Expr.php index e88b481be..e6923d180 100644 --- a/lib/Doctrine/ORM/Query/Expr.php +++ b/lib/Doctrine/ORM/Query/Expr.php @@ -69,7 +69,12 @@ class Expr 'gtoet' => '_greaterThanOrEqualToExpr', 'ltoet' => '_lessThanOrEqualToExpr', 'between' => '_betweenExpr', - 'trim' => '_trimExpr' + 'trim' => '_trimExpr', + 'on' => '_onExpr', + 'with' => '_withExpr', + 'from' => '_fromExpr', + 'innerJoin' => '_innerJoinExpr', + 'leftJoin' => '_leftJoinExpr' ); private $_type; @@ -281,6 +286,33 @@ class Expr return 'TRIM(' . $this->_parts[0] . ')'; } + private function _onExpr() + { + return 'ON ' . $this->_parts[0]; + } + + private function _withExpr() + { + return 'WITH ' . $this->_parts[0]; + } + + private function _fromExpr() + { + return $this->_parts[0] . ' ' . $this->_parts[1]; + } + + private function _leftJoinExpr() + { + return 'LEFT JOIN ' . $this->_parts[0] . '.' . $this->_parts[1] . ' ' + . $this->_parts[2] . (isset($this->_parts[3]) ? ' ' . $this->_parts[3] : null); + } + + private function _innerJoinExpr() + { + return 'INNER JOIN ' . $this->_parts[0] . '.' . $this->_parts[1] . ' ' + . $this->_parts[2] . (isset($this->_parts[3]) ? ' ' . $this->_parts[3] : null); + } + public static function avg($x) { return new self('avg', array($x)); @@ -480,4 +512,29 @@ class Expr { return new self('trim', array($val, $spec, $char)); } + + public static function on($x) + { + return new self('on', array($x)); + } + + public static function with($x) + { + return new self('with', array($x)); + } + + public static function from($from, $alias) + { + return new self('from', array($from, $alias)); + } + + public static function leftJoin($parentAlias, $join, $alias, $condition = null) + { + return new self('leftJoin', array($parentAlias, $join, $alias, $condition)); + } + + public static function innerJoin($parentAlias, $join, $alias, $condition = null) + { + return new self('innerJoin', array($parentAlias, $join, $alias, $condition)); + } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/QueryBuilder.php b/lib/Doctrine/ORM/QueryBuilder.php index d4bb89ca3..4e790360a 100644 --- a/lib/Doctrine/ORM/QueryBuilder.php +++ b/lib/Doctrine/ORM/QueryBuilder.php @@ -46,7 +46,14 @@ class QueryBuilder const STATE_DIRTY = 0; const STATE_CLEAN = 1; - protected $_entityManager; + /** + * @var EntityManager $em Instance of an EntityManager to use for query + */ + protected $_em; + + /** + * @var array $dqlParts The array of DQL parts collected + */ protected $_dqlParts = array( 'select' => array(), 'from' => array(), @@ -57,13 +64,30 @@ class QueryBuilder 'limit' => array(), 'offset' => array() ); + + /** + * @var integer $type The type of query this is. Can be select, update or delete + */ protected $_type = self::SELECT; + + /** + * @var integer $state The state of the query object. Can be dirty or clean. + */ protected $_state = self::STATE_CLEAN; + + /** + * @var string $dql The complete DQL string for this query + */ protected $_dql; + /** + * @var array $params Parameters of this query. + */ + protected $_params = array(); + public function __construct(EntityManager $entityManager) { - $this->_entityManager = $entityManager; + $this->_em = $entityManager; } public static function create(EntityManager $entityManager) @@ -76,6 +100,11 @@ class QueryBuilder return $this->_type; } + public function getEntityManager() + { + return $this->_em; + } + public function getState() { return $this->_state; @@ -111,21 +140,100 @@ class QueryBuilder public function getQuery() { - $q = new Query($this->_entityManager); + $q = new Query($this->_em); $q->setDql($this->getDql()); + $q->setParameters($this->getParameters()); return $q; } - public function select($select = null) + public function execute($params = array(), $hydrationMode = null) { + return $this->getQuery()->execute($params, $hydrationMode); + } + + /** + * Sets a query parameter. + * + * @param string|integer $key The parameter position or name. + * @param mixed $value The parameter value. + */ + public function setParameter($key, $value) + { + $this->_params[$key] = $value; + + return $this; + } + + /** + * Sets a collection of query parameters. + * + * @param array $params + */ + public function setParameters(array $params) + { + foreach ($params as $key => $value) { + $this->setParameter($key, $value); + } + + return $this; + } + + /** + * Get all defined parameters + * + * @return array Defined parameters + */ + public function getParameters($params = array()) + { + if ($params) { + return array_merge($this->_params, $params); + } + return $this->_params; + } + + /** + * Gets a query parameter. + * + * @param mixed $key The key (index or name) of the bound parameter. + * @return mixed The value of the bound parameter. + */ + public function getParameter($key) + { + return isset($this->_params[$key]) ? $this->_params[$key] : null; + } + + /** + * Add a single DQL query part to the array of parts + * + * @param string $dqlPartName + * @param string $dqlPart + * @param string $append + * @return QueryBuilder $this + */ + public function add($dqlPartName, $dqlPart, $append = false) + { + if ($append) { + $this->_dqlParts[$dqlPartName][] = $dqlPart; + } else { + $this->_dqlParts[$dqlPartName] = array($dqlPart); + } + + $this->_state = self::STATE_DIRTY; + + return $this; + } + + public function select() + { + $selects = func_get_args(); $this->_type = self::SELECT; - if ( ! $select) { + if (empty($selects)) { return $this; } - return $this->_addDqlQueryPart('select', $select, true); + return $this->add('select', implode(', ', $selects), true); } public function delete($delete = null, $alias = null) @@ -136,7 +244,7 @@ class QueryBuilder return $this; } - return $this->_addDqlQueryPart('from', $delete . ' ' . $alias); + return $this->add('from', Expr::from($delete, $alias)); } public function update($update = null, $alias = null) @@ -147,103 +255,80 @@ class QueryBuilder return $this; } - return $this->_addDqlQueryPart('from', $update . ' ' . $alias); + return $this->add('from', Expr::from($update, $alias)); } - public function set($key, $value = null) + public function set($key, $value) { - return $this->_addDqlQueryPart('set', $key . ' = ' . $value, true); + return $this->add('set', Expr::eq($key, $value), true); } public function from($from, $alias) { - return $this->_addDqlQueryPart('from', $from . ' ' . $alias, true); + return $this->add('from', Expr::from($from, $alias), true); } - public function join($join, $alias) + public function innerJoin($parentAlias, $join, $alias, $condition = null) { - return $this->_addDqlQueryPart('from', 'INNER JOIN ' . $join . ' ' . $alias, true); + return $this->add('from', Expr::innerJoin($parentAlias, $join, $alias, $condition), true); } - public function innerJoin($join, $alias) + public function leftJoin($parentAlias, $join, $alias, $condition = null) { - return $this->join($join, $alias); - } - - public function leftJoin($join, $alias) - { - return $this->_addDqlQueryPart('from', 'LEFT JOIN ' . $join . ' ' . $alias, true); + return $this->add('from', Expr::leftJoin($parentAlias, $join, $alias, $condition), true); } public function where($where) { - return $this->_addDqlQueryPart('where', $where, false); - } - - public function andWhere($where) - { - if (count($this->getDqlQueryPart('where')) > 0) { - $this->_addDqlQueryPart('where', 'AND', true); - } - - return $this->_addDqlQueryPart('where', $where, true); - } - - public function orWhere($where) - { - if (count($this->getDqlQueryPart('where')) > 0) { - $this->_addDqlQueryPart('where', 'OR', true); - } - - return $this->_addDqlQueryPart('where', $where, true); + return $this->add('where', $where, false); } public function groupBy($groupBy) { - return $this->_addDqlQueryPart('groupBy', $groupBy, false); + return $this->add('groupBy', $groupBy, false); } public function having($having) { - return $this->_addDqlQueryPart('having', $having, false); + return $this->add('having', $having, false); } public function andHaving($having) { - if (count($this->getDqlQueryPart('having')) > 0) { - $this->_addDqlQueryPart('having', 'AND', true); + if (count($this->_getDqlQueryPart('having')) > 0) { + $this->add('having', 'AND', true); } - return $this->_addDqlQueryPart('having', $having, true); + return $this->add('having', $having, true); } public function orHaving($having) { - if (count($this->getDqlQueryPart('having')) > 0) { - $this->_addDqlQueryPart('having', 'OR', true); + if (count($this->_getDqlQueryPart('having')) > 0) { + $this->add('having', 'OR', true); } - return $this->_addDqlQueryPart('having', $having, true); + return $this->add('having', $having, true); } public function orderBy($sort, $order) { - return $this->_addDqlQueryPart('orderBy', $sort . ' ' . $order, false); + return $this->add('orderBy', $sort . ' ' . $order, false); } public function addOrderBy($sort, $order) { - return $this->_addDqlQueryPart('orderBy', $sort . ' ' . $order, true); + return $this->add('orderBy', $sort . ' ' . $order, true); } public function limit($limit) { - return $this->_addDqlQueryPart('limit', $limit); + return $this->add('limit', $limit); } public function offset($offset) { - return $this->_addDqlQueryPart('offset', $offset); + return $this->add('offset', $offset); } /** @@ -332,26 +417,31 @@ class QueryBuilder } $str = (isset($options['pre']) ? $options['pre'] : ''); - $str .= implode($options['separator'], $this->getDqlQueryPart($queryPartName)); + $str .= implode($options['separator'], $this->_getDqlQueryPart($queryPartName)); $str .= (isset($options['post']) ? $options['post'] : ''); return $str; } - private function getDqlQueryPart($queryPartName) + private function _getDqlQueryPart($queryPartName) { return $this->_dqlParts[$queryPartName]; } - private function _addDqlQueryPart($queryPartName, $queryPart, $append = false) + /** + * Proxy method calls to the Expr class to ease the syntax of the query builder + * + * @param string $method The method name called + * @param string $arguments The arguments passed to the method + * @return void + * @throws \Doctrine\Common\DoctrineException Throws exception if method could not be proxied + */ + public function __call($method, $arguments) { - if ($append) { - $this->_dqlParts[$queryPartName][] = $queryPart; - } else { - $this->_dqlParts[$queryPartName] = array($queryPart); + $class = 'Doctrine\ORM\Query\Expr'; + if (method_exists($class, $method)) { + return call_user_func_array(array('Doctrine\ORM\Query\Expr', $method), $arguments); } - - $this->_state = self::STATE_DIRTY; - return $this; + throw \Doctrine\Common\DoctrineException::notImplemented($method, get_class($this)); } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Query/ExprTest.php b/tests/Doctrine/Tests/ORM/Query/ExprTest.php index 9634b98b4..c04ad7158 100644 --- a/tests/Doctrine/Tests/ORM/Query/ExprTest.php +++ b/tests/Doctrine/Tests/ORM/Query/ExprTest.php @@ -228,4 +228,44 @@ class ExprTest extends \Doctrine\Tests\OrmTestCase { $this->assertEquals('u.id IN(1, 2, 3)', (string) Expr::in('u.id', array(1, 2, 3))); } + + public function testOnExpr() + { + $this->assertEquals('ON 1 = 1', (string) Expr::on(Expr::eq(1, 1))); + } + + public function testWithExpr() + { + $this->assertEquals('WITH 1 = 1', (string) Expr::with(Expr::eq(1, 1))); + } + + public function testLeftJoinExpr() + { + $this->assertEquals('LEFT JOIN u.Profile p', (string) Expr::leftJoin('u', 'Profile', 'p')); + } + + public function testLeftJoinOnConditionExpr() + { + $this->assertEquals('LEFT JOIN u.Profile p ON p.user_id = u.id', (string) Expr::leftJoin('u', 'Profile', 'p', Expr::on(Expr::eq('p.user_id', 'u.id')))); + } + + public function testLeftJoinWithConditionExpr() + { + $this->assertEquals('LEFT JOIN u.Profile p WITH p.user_id = u.id', (string) Expr::leftJoin('u', 'Profile', 'p', Expr::with(Expr::eq('p.user_id', 'u.id')))); + } + + public function testInnerJoinExpr() + { + $this->assertEquals('INNER JOIN u.Profile p', (string) Expr::innerJoin('u', 'Profile', 'p')); + } + + public function testInnerJoinOnConditionExpr() + { + $this->assertEquals('INNER JOIN u.Profile p ON p.user_id = u.id', (string) Expr::innerJoin('u', 'Profile', 'p', Expr::on(Expr::eq('p.user_id', 'u.id')))); + } + + public function testInnerJoinWithConditionExpr() + { + $this->assertEquals('INNER JOIN u.Profile p WITH p.user_id = u.id', (string) Expr::innerJoin('u', 'Profile', 'p', Expr::with(Expr::eq('p.user_id', 'u.id')))); + } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/QueryBuilderTest.php b/tests/Doctrine/Tests/ORM/QueryBuilderTest.php index d50401c06..96f69e914 100644 --- a/tests/Doctrine/Tests/ORM/QueryBuilderTest.php +++ b/tests/Doctrine/Tests/ORM/QueryBuilderTest.php @@ -62,11 +62,38 @@ class QueryBuilderTest extends \Doctrine\Tests\OrmTestCase $this->assertEquals($expectedDql, $dql); } + public function testSelectSetsType() + { + $qb = QueryBuilder::create($this->_em) + ->delete('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->select('u.id', 'u.username'); + + $this->assertEquals($qb->getType(), QueryBuilder::SELECT); + } + + public function testDeleteSetsType() + { + $qb = QueryBuilder::create($this->_em) + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->delete(); + + $this->assertEquals($qb->getType(), QueryBuilder::DELETE); + } + + public function testUpdateSetsType() + { + $qb = QueryBuilder::create($this->_em) + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->update(); + + $this->assertEquals($qb->getType(), QueryBuilder::UPDATE); + } + public function testSimpleSelect() { $qb = QueryBuilder::create($this->_em) ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') - ->select('u.id, u.username'); + ->select('u.id', 'u.username'); $this->assertValidQueryBuilder($qb, 'SELECT u.id, u.username FROM Doctrine\Tests\Models\CMS\CmsUser u'); } @@ -83,27 +110,17 @@ class QueryBuilderTest extends \Doctrine\Tests\OrmTestCase { $qb = QueryBuilder::create($this->_em) ->update('Doctrine\Tests\Models\CMS\CmsUser', 'u') - ->set('u.username', ':username', 'jonwage'); + ->set('u.username', ':username'); $this->assertValidQueryBuilder($qb, 'UPDATE Doctrine\Tests\Models\CMS\CmsUser u SET u.username = :username'); } - public function testJoin() - { - $qb = QueryBuilder::create($this->_em) - ->select('u, a') - ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') - ->join('u.articles', 'a'); - - $this->assertValidQueryBuilder($qb, 'SELECT u, a FROM Doctrine\Tests\Models\CMS\CmsUser u INNER JOIN u.articles a'); - } - public function testInnerJoin() { $qb = QueryBuilder::create($this->_em) - ->select('u, a') + ->select('u', 'a') ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') - ->innerJoin('u.articles', 'a'); + ->innerJoin('u', 'articles', 'a'); $this->assertValidQueryBuilder($qb, 'SELECT u, a FROM Doctrine\Tests\Models\CMS\CmsUser u INNER JOIN u.articles a'); } @@ -111,9 +128,9 @@ class QueryBuilderTest extends \Doctrine\Tests\OrmTestCase public function testLeftJoin() { $qb = QueryBuilder::create($this->_em) - ->select('u, a') + ->select('u', 'a') ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') - ->leftJoin('u.articles', 'a'); + ->leftJoin('u', 'articles', 'a'); $this->assertValidQueryBuilder($qb, 'SELECT u, a FROM Doctrine\Tests\Models\CMS\CmsUser u LEFT JOIN u.articles a'); } @@ -123,100 +140,11 @@ class QueryBuilderTest extends \Doctrine\Tests\OrmTestCase $qb = QueryBuilder::create($this->_em) ->select('u') ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') - ->where('u.id = :uid') - ->where('u.id = :id'); + ->where('u.id = :uid'); - $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id = :id'); + $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id = :uid'); } - public function testAndWhere() - { - $qb = QueryBuilder::create($this->_em) - ->select('u') - ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') - ->where('u.id = :id') - ->andWhere('u.username = :username'); - - $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id = :id AND u.username = :username'); - } - - public function testOrWhere() - { - $qb = QueryBuilder::create($this->_em) - ->select('u') - ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') - ->where('u.id = :id') - ->orWhere('u.username = :username'); - - $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id = :id OR u.username = :username'); - } - - /* - public function testWhereIn() - { - $qb = QueryBuilder::create($this->_em) - ->select('u') - ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') - ->whereIn('u.id', array(1)); - - $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id IN(1)'); - } - - public function testWhereNotIn() - { - $qb = QueryBuilder::create($this->_em) - ->select('u') - ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') - ->whereNotIn('u.id', array(1)); - - $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id NOT IN(1)'); - } - - public function testAndWhereIn() - { - $qb = QueryBuilder::create($this->_em) - ->select('u') - ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') - ->where('u.id = :id') - ->andWhereIn('u.id', array(1, 2, 3)); - - $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id = :id AND u.id IN(1, 2, 3)'); - } - - public function testAndWhereNotIn() - { - $qb = QueryBuilder::create($this->_em) - ->select('u') - ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') - ->where('u.id = :id') - ->andWhereNotIn('u.id', array(1, 2, 3)); - - $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id = :id AND u.id NOT IN(1, 2, 3)'); - } - - public function testOrWhereIn() - { - $qb = QueryBuilder::create($this->_em) - ->select('u') - ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') - ->where('u.id = :id') - ->orWhereIn('u.id', array(1, 2, 3)); - - $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id = :id OR u.id IN(1, 2, 3)'); - } - - public function testOrWhereNotIn() - { - $qb = QueryBuilder::create($this->_em) - ->select('u') - ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') - ->where('u.id = :id') - ->orWhereNotIn('u.id', array(1, 2, 3)); - - $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id = :id OR u.id NOT IN(1, 2, 3)'); - } - */ - public function testGroupBy() { $qb = QueryBuilder::create($this->_em) @@ -284,6 +212,64 @@ 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 testGetQuery() + { + $qb = QueryBuilder::create($this->_em) + ->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $q = $qb->getQuery(); + + $this->assertEquals(get_class($q), 'Doctrine\ORM\Query'); + } + + public function testSetParameter() + { + $qb = QueryBuilder::create($this->_em) + ->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->where('u.id = :id') + ->setParameter('id', 1); + + $q = $qb->getQuery(); + + $this->assertEquals($q->getParameters(), array('id' => 1)); + } + + public function testSetParameters() + { + $qb = QueryBuilder::create($this->_em) + ->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->where('u.username = :username OR u.username = :username2'); + + $qb->setParameters(array('username' => 'jwage', 'username2' => 'jonwage')); + + $q = $qb->getQuery(); + + $this->assertEquals($q->getParameters(), array('username' => 'jwage', 'username2' => 'jonwage')); + } + + public function testExprProxyWithMagicCall() + { + $qb = QueryBuilder::create($this->_em) + ->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $qb->where($qb->eq('u.id', 1)); + + $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id = 1'); + } + + /** + * @expectedException \Doctrine\Common\DoctrineException + */ + public function testInvalidQueryBuilderMethodThrowsException() + { + $qb = QueryBuilder::create($this->_em) + ->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $qb->where($qb->blah('u.id', 1)); + } + public function testLimit() { /*