diff --git a/lib/Doctrine/Common/Collections/Collection.php b/lib/Doctrine/Common/Collections/Collection.php index 5a399df03..b9c4d0eb8 100644 --- a/lib/Doctrine/Common/Collections/Collection.php +++ b/lib/Doctrine/Common/Collections/Collection.php @@ -33,7 +33,6 @@ use \ArrayIterator; * * @author Roman S. Borschel * @since 2.0 - * @todo Consider extending ArrayObject */ class Collection implements Countable, IteratorAggregate, ArrayAccess { @@ -310,6 +309,7 @@ class Collection implements Countable, IteratorAggregate, ArrayAccess /** * Checks whether the collection is empty. + * * Note: This is preferrable over count() == 0. * * @return boolean TRUE if the collection is empty, FALSE otherwise. diff --git a/lib/Doctrine/DBAL/Connection.php b/lib/Doctrine/DBAL/Connection.php index dd7f0bd0e..86760a281 100644 --- a/lib/Doctrine/DBAL/Connection.php +++ b/lib/Doctrine/DBAL/Connection.php @@ -601,16 +601,16 @@ class Connection if ($this->_config->getSqlLogger()) { $this->_config->getSqlLogger()->logSql($query, $params); } - + if ( ! empty($params)) { - $stmt = $this->prepare($query); + $stmt = $this->_conn->prepare($query); $stmt->execute($params); - return $stmt; } else { $stmt = $this->_conn->query($query); - $this->_queryCount++; - return $stmt; } + $this->_queryCount++; + + return $stmt; } /** @@ -630,14 +630,15 @@ class Connection } if ( ! empty($params)) { - $stmt = $this->prepare($query); + $stmt = $this->_conn->prepare($query); $stmt->execute($params); - return $stmt->rowCount(); + $result = $stmt->rowCount(); } else { - $count = $this->_conn->exec($query); - $this->_queryCount++; - return $count; + $result = $this->_conn->exec($query); } + $this->_queryCount++; + + return $result; } /** @@ -769,23 +770,6 @@ class Connection return true; } - /** - * Quotes pattern (% and _) characters in a string) - * - * EXPERIMENTAL - * - * WARNING: this function is experimental and may change signature at - * any time until labelled as non-experimental - * - * @param string the input string to quote - * - * @return string quoted string - */ - protected function _escapePattern($text) - { - return $text; - } - /** * Gets the wrapped driver connection. * diff --git a/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php b/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php index 6b8d33c41..230d99095 100644 --- a/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php +++ b/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php @@ -1219,10 +1219,8 @@ abstract class AbstractPlatform $item[$k] = (int) $value; } } - } else { - if (is_bool($item)) { - $item = (int) $item; - } + } else if (is_bool($item)) { + $item = (int) $item; } return $item; } diff --git a/lib/Doctrine/DBAL/Types/BooleanType.php b/lib/Doctrine/DBAL/Types/BooleanType.php index dc51ae591..79df0dae4 100644 --- a/lib/Doctrine/DBAL/Types/BooleanType.php +++ b/lib/Doctrine/DBAL/Types/BooleanType.php @@ -14,7 +14,7 @@ class BooleanType extends Type * * @override */ - public function convertToDatabaseValue($value, Doctrine_DatabasePlatform $platform) + public function convertToDatabaseValue($value, \Doctrine\DBAL\Platforms\AbstractPlatform $platform) { return $platform->convertBooleans($value); } @@ -24,7 +24,7 @@ class BooleanType extends Type * * @override */ - public function convertToObjectValue($value) + public function convertToPHPValue($value) { return (bool) $value; } diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index 866a28b33..6568d8a62 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -178,19 +178,23 @@ abstract class AbstractQuery * * @return array Defined parameters */ - public function getParams($params = array()) + public function getParameters($params = array()) { - return array_merge($this->_params, $params); + if ($params) { + return array_merge($this->_params, $params); + } + return $this->_params; } - + /** - * setParams - * - * @param array $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 setParams(array $params = array()) + public function getParameter($key) { - $this->_params = $params; + return isset($this->_params[$key]) ? $this->_params[$key] : null; } /** @@ -469,8 +473,8 @@ abstract class AbstractQuery if ($hydrationMode !== null) { $this->_hydrationMode = $hydrationMode; } - - $params = $this->getParams($params); + + $params = $this->getParameters($params); // Check result cache if ($cacheDriver = $this->getResultCacheDriver()) { @@ -503,12 +507,15 @@ abstract class AbstractQuery } /** - * @nodoc + * Prepares the given parameters for execution in an SQL statement. + * + * Note to inheritors: This method must return a numerically, continously indexed array, + * starting with index 0 where the values (the parameter values) are in the order + * in which the parameters appear in the SQL query. + * + * @return array The SQL parameter array. */ - protected function _prepareParams(array $params) - { - return $this->_em->getConnection()->getDatabasePlatform()->convertBooleans($params); - } + abstract protected function _prepareParams(array $params); /** * Executes the query and returns a reference to the resulting Statement object. diff --git a/lib/Doctrine/ORM/DynamicProxyGenerator.php b/lib/Doctrine/ORM/DynamicProxyGenerator.php index 9187024a3..121e880b6 100644 --- a/lib/Doctrine/ORM/DynamicProxyGenerator.php +++ b/lib/Doctrine/ORM/DynamicProxyGenerator.php @@ -71,6 +71,7 @@ class DynamicProxyGenerator require $fileName; } $proxyClassName = '\\' . self::$_ns . $proxyClassName; + return new $proxyClassName($this->_em, $class, $identifier); } @@ -89,6 +90,7 @@ class DynamicProxyGenerator require $fileName; } $proxyClassName = '\\' . self::$_ns . $proxyClassName; + return new $proxyClassName($this->_em, $assoc, $owner); } diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadata.php b/lib/Doctrine/ORM/Mapping/ClassMetadata.php index d56af872c..6c0ce51b4 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadata.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadata.php @@ -858,6 +858,9 @@ final class ClassMetadata /** * Extracts the identifier values of an entity of this class. + * + * For composite identifiers, the identifier values are returned as an array + * with the same order as the field order in {@link identifier}. * * @param object $entity * @return mixed diff --git a/lib/Doctrine/ORM/Mapping/ManyToManyMapping.php b/lib/Doctrine/ORM/Mapping/ManyToManyMapping.php index 83e030ba7..c29c6ad60 100644 --- a/lib/Doctrine/ORM/Mapping/ManyToManyMapping.php +++ b/lib/Doctrine/ORM/Mapping/ManyToManyMapping.php @@ -63,6 +63,9 @@ class ManyToManyMapping extends AssociationMapping */ public $joinTableColumns = array(); + /** FUTURE: The key column mapping, if any. The key column holds the keys of the Collection. */ + //public $keyColumn; + /** * Initializes a new ManyToManyMapping. * diff --git a/lib/Doctrine/ORM/Mapping/OneToManyMapping.php b/lib/Doctrine/ORM/Mapping/OneToManyMapping.php index 47a960db0..6cd1fbc2c 100644 --- a/lib/Doctrine/ORM/Mapping/OneToManyMapping.php +++ b/lib/Doctrine/ORM/Mapping/OneToManyMapping.php @@ -42,24 +42,10 @@ namespace Doctrine\ORM\Mapping; */ class OneToManyMapping extends AssociationMapping { - /** The target foreign key columns that reference the sourceKeyColumns. */ - /* NOTE: Currently not used because uni-directional one-many not supported atm. */ - //protected $_targetForeignKeyColumns; - - /** The (typically primary) source key columns that are referenced by the targetForeignKeyColumns. */ - /* NOTE: Currently not used because uni-directional one-many not supported atm. */ - //protected $_sourceKeyColumns; - - /** This maps the target foreign key columns to the corresponding (primary) source key columns. */ - /* NOTE: Currently not used because uni-directional one-many not supported atm. */ - //protected $_targetForeignKeysToSourceKeys; - - /** This maps the (primary) source key columns to the corresponding target foreign key columns. */ - /* NOTE: Currently not used because uni-directional one-many not supported atm. */ - //protected $_sourceKeysToTargetForeignKeys; - /** Whether to delete orphaned elements (removed from the collection) */ public $deleteOrphans = false; + /** FUTURE: The key column mapping, if any. The key column holds the keys of the Collection. */ + //public $keyColumn; /** * Initializes a new OneToManyMapping. diff --git a/lib/Doctrine/ORM/NativeQuery.php b/lib/Doctrine/ORM/NativeQuery.php index d2262369d..85ba00905 100644 --- a/lib/Doctrine/ORM/NativeQuery.php +++ b/lib/Doctrine/ORM/NativeQuery.php @@ -74,4 +74,21 @@ final class NativeQuery extends AbstractQuery { return $this->_em->getConnection()->execute($this->_sql, $this->_prepareParams($params)); } + + /** + * {@inheritdoc} + * + * @override + */ + protected function _prepareParams(array $params) + { + $sqlParams = array(); + + foreach ($params as $key => $value) { + $sqlParams[$key] = $value; + } + ksort($sqlParams); + + return array_values($sqlParams); + } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/PersistentCollection.php b/lib/Doctrine/ORM/PersistentCollection.php index 76f7c718d..b1c1fc2e8 100644 --- a/lib/Doctrine/ORM/PersistentCollection.php +++ b/lib/Doctrine/ORM/PersistentCollection.php @@ -26,6 +26,7 @@ use Doctrine\ORM\Mapping\AssociationMapping; /** * A PersistentCollection represents a collection of elements that have persistent state. + * * Collections of entities represent only the associations (links) to those entities. * That means, if the collection is part of a many-many mapping and you remove * entities from the collection, only the links in the relation table are removed (on flush). diff --git a/lib/Doctrine/ORM/Query.php b/lib/Doctrine/ORM/Query.php index 676293c1f..f0e978fbc 100644 --- a/lib/Doctrine/ORM/Query.php +++ b/lib/Doctrine/ORM/Query.php @@ -48,7 +48,7 @@ final class Query extends AbstractQuery * parsed/processed. This is automatically defined as DIRTY when addDqlQueryPart * is called. */ - const STATE_DIRTY = 2; + const STATE_DIRTY = 2; /** * @var integer $_state The current state of this query. @@ -64,6 +64,16 @@ final class Query extends AbstractQuery * @var Doctrine\ORM\Query\ParserResult The parser result that holds DQL => SQL information. */ private $_parserResult; + + /** + * @var integer The first result to return (the "offset"). + */ + private $_firstResult = null; + + /** + * @var integer The maximum number of results to return (the "limit"). + */ + private $_maxResults = null; /** * @var CacheDriver The cache driver used for caching queries. @@ -100,7 +110,7 @@ final class Query extends AbstractQuery */ public function getSql() { - return $this->parse()->getSqlExecutor()->getSqlStatements(); + return $this->_parse()->getSqlExecutor()->getSqlStatements(); } /** @@ -110,7 +120,7 @@ final class Query extends AbstractQuery * * @return Doctrine\ORM\Query\ParserResult */ - public function parse() + private function _parse() { if ($this->_state === self::STATE_DIRTY) { $parser = new Parser($this); @@ -121,15 +131,15 @@ final class Query extends AbstractQuery } /** - * _execute + * {@inheritdoc} * * @param array $params - * @return PDOStatement The executed PDOStatement. + * @return Statement The resulting Statement. * @override */ protected function _doExecute(array $params) { - // If there is a CacheDriver associated to cache queries... + // Check query cache if ($queryCache = $this->getQueryCacheDriver()) { // Calculate hash for dql query. $hash = md5($this->getDql() . 'DOCTRINE_QUERY_CACHE_SALT'); @@ -137,7 +147,7 @@ final class Query extends AbstractQuery if ($cached === false) { // Cache miss. - $executor = $this->parse()->getSqlExecutor(); + $executor = $this->_parse()->getSqlExecutor(); $queryCache->save($hash, serialize($this->_parserResult), null); } else { // Cache hit. @@ -145,19 +155,48 @@ final class Query extends AbstractQuery $executor = $this->_parserResult->getSqlExecutor(); } } else { - $executor = $this->parse()->getSqlExecutor(); + $executor = $this->_parse()->getSqlExecutor(); } - // Converting parameters $params = $this->_prepareParams($params); if ( ! $this->_resultSetMapping) { $this->_resultSetMapping = $this->_parserResult->getResultSetMapping(); } - // Executing the query and returning statement return $executor->execute($this->_em->getConnection(), $params); } + + /** + * {@inheritdoc} + * + * @override + */ + protected function _prepareParams(array $params) + { + $sqlParams = array(); + + $paramMappings = $this->_parserResult->getParameterMappings(); + foreach ($params as $key => $value) { + if (is_object($value)) { + $values = $this->_em->getClassMetadata(get_class($value))->getIdentifierValues($value); + $sqlPositions = $paramMappings[$key]; + $sqlParams = array_merge($sqlParams, array_combine((array)$sqlPositions, (array)$values)); + } else if (is_bool($value)) { + $boolValue = $this->_em->getConnection()->getDatabasePlatform()->convertBooleans($value); + foreach ($paramMappings[$key] as $position) { + $sqlParams[$position] = $boolValue; + } + } else { + foreach ($paramMappings[$key] as $position) { + $sqlParams[$position] = $value; + } + } + } + ksort($sqlParams); + + return array_values($sqlParams); + } /** * Defines a cache driver to be used for caching queries. @@ -292,6 +331,52 @@ final class Query extends AbstractQuery */ public function contains($dql) { - return stripos($this->getDql(), $dql) === false ? false : true; + return stripos($this->getDql(), $dql) === false ? false : true; + } + + /** + * Sets the position of the first result to retrieve (the "offset"). + * + * @param integer $firstResult The first result to return. + * @return Query This query object. + */ + public function setFirstResult($firstResult) + { + $this->_firstResult = $firstResult; + return $this; + } + + /** + * Gets the position of the first result the query object was set to retrieve (the "offset"). + * Returns NULL if {@link setFirstResult} was not applied to this query. + * + * @return integer The position of the first result. + */ + public function getFirstResult() + { + return $this->_firstResult; + } + + /** + * Sets the maximum number of results to retrieve (the "limit"). + * + * @param integer $maxResults + * @return Query This query object. + */ + public function setMaxResults($maxResults) + { + $this->_maxResults = $maxResults; + return $this; + } + + /** + * Gets the maximum number of results the query object was set to retrieve (the "limit"). + * Returns NULL if {@link setMaxResults} was not applied to this query. + * + * @return integer Maximum number of results. + */ + public function getMaxResults() + { + return $this->_maxResults; } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Query/AST/CollectionMemberExpression.php b/lib/Doctrine/ORM/Query/AST/CollectionMemberExpression.php new file mode 100644 index 000000000..49bef8a79 --- /dev/null +++ b/lib/Doctrine/ORM/Query/AST/CollectionMemberExpression.php @@ -0,0 +1,28 @@ + + */ +class CollectionMemberExpression extends Node +{ + public $entityExpression; + public $collectionValuedPathExpression; + public $isNot; + + public function __construct($entityExpr, $collValuedPathExpr, $isNot) + { + $this->entityExpression = $entityExpr; + $this->collectionValuedPathExpression = $collValuedPathExpr; + $this->isNot = $isNot; + } + + public function dispatch($walker) + { + return $walker->walkCollectionMemberExpression($this); + } +} + diff --git a/lib/Doctrine/ORM/Query/AST/Functions/AbsFunction.php b/lib/Doctrine/ORM/Query/AST/Functions/AbsFunction.php index bc1863fa9..7b36b0b2e 100644 --- a/lib/Doctrine/ORM/Query/AST/Functions/AbsFunction.php +++ b/lib/Doctrine/ORM/Query/AST/Functions/AbsFunction.php @@ -9,7 +9,7 @@ namespace Doctrine\ORM\Query\AST\Functions; /** * "ABS" "(" SimpleArithmeticExpression ")" * - * @author robo + * @author Roman Borschel */ class AbsFunction extends FunctionNode { @@ -37,7 +37,7 @@ class AbsFunction extends FunctionNode $lexer = $parser->getLexer(); $parser->match($lexer->lookahead['value']); $parser->match('('); - $this->_simpleArithmeticExpression = $parser->_SimpleArithmeticExpression(); + $this->_simpleArithmeticExpression = $parser->SimpleArithmeticExpression(); $parser->match(')'); } } diff --git a/lib/Doctrine/ORM/Query/AST/Functions/ConcatFunction.php b/lib/Doctrine/ORM/Query/AST/Functions/ConcatFunction.php index 070b222fb..a6918cddb 100644 --- a/lib/Doctrine/ORM/Query/AST/Functions/ConcatFunction.php +++ b/lib/Doctrine/ORM/Query/AST/Functions/ConcatFunction.php @@ -9,7 +9,7 @@ namespace Doctrine\ORM\Query\AST\Functions; /** * "CONCAT" "(" StringPrimary "," StringPrimary ")" * - * @author robo + * @author Roman Borschel */ class ConcatFunction extends FunctionNode { @@ -36,13 +36,6 @@ class ConcatFunction extends FunctionNode $sqlWalker->walkStringPrimary($this->_firstStringPrimary), $sqlWalker->walkStringPrimary($this->_secondStringPrimary) ); - /* - $sql = 'CONCAT(' . - $sqlWalker->walkStringPrimary($this->_firstStringPrimary) - . ', ' . - $sqlWalker->walkStringPrimary($this->_secondStringPrimary) - . ')'; - return $sql;*/ } /** @@ -54,9 +47,9 @@ class ConcatFunction extends FunctionNode $parser->match($lexer->lookahead['value']); $parser->match('('); - $this->_firstStringPrimary = $parser->_StringPrimary(); + $this->_firstStringPrimary = $parser->StringPrimary(); $parser->match(','); - $this->_secondStringPrimary = $parser->_StringPrimary(); + $this->_secondStringPrimary = $parser->StringPrimary(); $parser->match(')'); } diff --git a/lib/Doctrine/ORM/Query/AST/Functions/FunctionNode.php b/lib/Doctrine/ORM/Query/AST/Functions/FunctionNode.php index faa1e8df6..26801a368 100644 --- a/lib/Doctrine/ORM/Query/AST/Functions/FunctionNode.php +++ b/lib/Doctrine/ORM/Query/AST/Functions/FunctionNode.php @@ -31,7 +31,6 @@ use Doctrine\ORM\Query\AST\Node; abstract class FunctionNode extends Node { private $_name; - //private $_expressions = array(); public function __construct($name) { diff --git a/lib/Doctrine/ORM/Query/AST/Functions/LengthFunction.php b/lib/Doctrine/ORM/Query/AST/Functions/LengthFunction.php index 142b296ba..f6c806de5 100644 --- a/lib/Doctrine/ORM/Query/AST/Functions/LengthFunction.php +++ b/lib/Doctrine/ORM/Query/AST/Functions/LengthFunction.php @@ -9,7 +9,7 @@ namespace Doctrine\ORM\Query\AST\Functions; /** * "LENGTH" "(" StringPrimary ")" * - * @author robo + * @author Roman Borschel */ class LengthFunction extends FunctionNode { @@ -37,7 +37,7 @@ class LengthFunction extends FunctionNode $lexer = $parser->getLexer(); $parser->match($lexer->lookahead['value']); $parser->match('('); - $this->_stringPrimary = $parser->_StringPrimary(); + $this->_stringPrimary = $parser->StringPrimary(); $parser->match(')'); } } diff --git a/lib/Doctrine/ORM/Query/AST/Functions/LocateFunction.php b/lib/Doctrine/ORM/Query/AST/Functions/LocateFunction.php index b7ee43b5f..1b53acfef 100644 --- a/lib/Doctrine/ORM/Query/AST/Functions/LocateFunction.php +++ b/lib/Doctrine/ORM/Query/AST/Functions/LocateFunction.php @@ -57,12 +57,12 @@ class LocateFunction extends FunctionNode $lexer = $parser->getLexer(); $parser->match($lexer->lookahead['value']); $parser->match('('); - $this->_firstStringPrimary = $parser->_StringPrimary(); + $this->_firstStringPrimary = $parser->StringPrimary(); $parser->match(','); - $this->_secondStringPrimary = $parser->_StringPrimary(); + $this->_secondStringPrimary = $parser->StringPrimary(); if ($lexer->isNextToken(',')) { $parser->match(','); - $this->_simpleArithmeticExpression = $parser->_SimpleArithmeticExpression(); + $this->_simpleArithmeticExpression = $parser->SimpleArithmeticExpression(); } $parser->match(')'); } diff --git a/lib/Doctrine/ORM/Query/AST/Functions/LowerFunction.php b/lib/Doctrine/ORM/Query/AST/Functions/LowerFunction.php index 8577c7115..6b47704db 100644 --- a/lib/Doctrine/ORM/Query/AST/Functions/LowerFunction.php +++ b/lib/Doctrine/ORM/Query/AST/Functions/LowerFunction.php @@ -37,7 +37,7 @@ class LowerFunction extends FunctionNode $lexer = $parser->getLexer(); $parser->match($lexer->lookahead['value']); $parser->match('('); - $this->_stringPrimary = $parser->_StringPrimary(); + $this->_stringPrimary = $parser->StringPrimary(); $parser->match(')'); } } diff --git a/lib/Doctrine/ORM/Query/AST/Functions/ModFunction.php b/lib/Doctrine/ORM/Query/AST/Functions/ModFunction.php index 9d176888c..8a9f565fa 100644 --- a/lib/Doctrine/ORM/Query/AST/Functions/ModFunction.php +++ b/lib/Doctrine/ORM/Query/AST/Functions/ModFunction.php @@ -47,9 +47,9 @@ class ModFunction extends FunctionNode $lexer = $parser->getLexer(); $parser->match($lexer->lookahead['value']); $parser->match('('); - $this->_firstSimpleArithmeticExpression = $parser->_SimpleArithmeticExpression(); + $this->_firstSimpleArithmeticExpression = $parser->SimpleArithmeticExpression(); $parser->match(','); - $this->_secondSimpleArithmeticExpression = $parser->_SimpleArithmeticExpression(); + $this->_secondSimpleArithmeticExpression = $parser->SimpleArithmeticExpression(); $parser->match(')'); } } diff --git a/lib/Doctrine/ORM/Query/AST/Functions/SizeFunction.php b/lib/Doctrine/ORM/Query/AST/Functions/SizeFunction.php index ff612ce63..00d9fde15 100644 --- a/lib/Doctrine/ORM/Query/AST/Functions/SizeFunction.php +++ b/lib/Doctrine/ORM/Query/AST/Functions/SizeFunction.php @@ -37,7 +37,7 @@ class SizeFunction extends FunctionNode $lexer = $parser->getLexer(); $parser->match($lexer->lookahead['value']); $parser->match('('); - $this->_collectionPathExpression = $parser->_CollectionValuedPathExpression(); + $this->_collectionPathExpression = $parser->CollectionValuedPathExpression(); $parser->match(')'); } } diff --git a/lib/Doctrine/ORM/Query/AST/Functions/SqrtFunction.php b/lib/Doctrine/ORM/Query/AST/Functions/SqrtFunction.php index cf97dc64d..a2614fab4 100644 --- a/lib/Doctrine/ORM/Query/AST/Functions/SqrtFunction.php +++ b/lib/Doctrine/ORM/Query/AST/Functions/SqrtFunction.php @@ -9,7 +9,7 @@ namespace Doctrine\ORM\Query\AST\Functions; /** * "SQRT" "(" SimpleArithmeticExpression ")" * - * @author robo + * @author Roman Borschel */ class SqrtFunction extends FunctionNode { @@ -37,7 +37,7 @@ class SqrtFunction extends FunctionNode $lexer = $parser->getLexer(); $parser->match($lexer->lookahead['value']); $parser->match('('); - $this->_simpleArithmeticExpression = $parser->_SimpleArithmeticExpression(); + $this->_simpleArithmeticExpression = $parser->SimpleArithmeticExpression(); $parser->match(')'); } } diff --git a/lib/Doctrine/ORM/Query/AST/Functions/SubstringFunction.php b/lib/Doctrine/ORM/Query/AST/Functions/SubstringFunction.php index 528b8ba45..97d5d7312 100644 --- a/lib/Doctrine/ORM/Query/AST/Functions/SubstringFunction.php +++ b/lib/Doctrine/ORM/Query/AST/Functions/SubstringFunction.php @@ -57,11 +57,11 @@ class SubstringFunction extends FunctionNode $parser->match($lexer->lookahead['value']); $parser->match('('); - $this->_stringPrimary = $parser->_StringPrimary(); + $this->_stringPrimary = $parser->StringPrimary(); $parser->match(','); - $this->_firstSimpleArithmeticExpression = $parser->_SimpleArithmeticExpression(); + $this->_firstSimpleArithmeticExpression = $parser->SimpleArithmeticExpression(); $parser->match(','); - $this->_secondSimpleArithmeticExpression = $parser->_SimpleArithmeticExpression(); + $this->_secondSimpleArithmeticExpression = $parser->SimpleArithmeticExpression(); $parser->match(')'); } diff --git a/lib/Doctrine/ORM/Query/AST/Functions/TrimFunction.php b/lib/Doctrine/ORM/Query/AST/Functions/TrimFunction.php index eed6a7764..e003a74fa 100644 --- a/lib/Doctrine/ORM/Query/AST/Functions/TrimFunction.php +++ b/lib/Doctrine/ORM/Query/AST/Functions/TrimFunction.php @@ -75,7 +75,7 @@ class TrimFunction extends FunctionNode if ($this->_leading) $sql .= 'LEADING '; else if ($this->_trailing) $sql .= 'TRAILING '; else if ($this->_both) $sql .= 'BOTH '; - if ($this->_trimChar) $sql .= $this->_trimChar . ' '; //TODO: quote() + if ($this->_trimChar) $sql .= $sqlWalker->getConnection()->quote($this->_trimChar) . ' '; $sql .= 'FROM ' . $sqlWalker->walkStringPrimary($this->_stringPrimary); $sql .= ')'; return $sql; @@ -110,7 +110,7 @@ class TrimFunction extends FunctionNode $parser->match(Lexer::T_FROM); } - $this->_stringPrimary = $parser->_StringPrimary(); + $this->_stringPrimary = $parser->StringPrimary(); $parser->match(')'); } diff --git a/lib/Doctrine/ORM/Query/AST/Functions/UpperFunction.php b/lib/Doctrine/ORM/Query/AST/Functions/UpperFunction.php index 6ff8bc1af..8cf66b6a5 100644 --- a/lib/Doctrine/ORM/Query/AST/Functions/UpperFunction.php +++ b/lib/Doctrine/ORM/Query/AST/Functions/UpperFunction.php @@ -37,7 +37,7 @@ class UpperFunction extends FunctionNode $lexer = $parser->getLexer(); $parser->match($lexer->lookahead['value']); $parser->match('('); - $this->_stringPrimary = $parser->_StringPrimary(); + $this->_stringPrimary = $parser->StringPrimary(); $parser->match(')'); } } diff --git a/lib/Doctrine/ORM/Query/AST/PathExpression.php b/lib/Doctrine/ORM/Query/AST/PathExpression.php new file mode 100644 index 000000000..d5ef759ef --- /dev/null +++ b/lib/Doctrine/ORM/Query/AST/PathExpression.php @@ -0,0 +1,72 @@ + + * @since 2.0 + */ +class PathExpression extends Node +{ + const TYPE_COLLECTION_VALUED_ASSOCIATION = 1; + const TYPE_SINGLE_VALUED_ASSOCIATION = 2; + const TYPE_STATE_FIELD = 4; + + private $_type; + private $_identificationVariable; + private $_parts; + + public function __construct($type, $identificationVariable, array $parts) + { + $this->_type = $type; + $this->_identificationVariable = $identificationVariable; + $this->_parts = $parts; + } + + public function getIdentificationVariable() + { + return $this->_identificationVariable; + } + + public function getParts() + { + return $this->_parts; + } + + public function getType() + { + return $this->_type; + } + + public function dispatch($walker) + { + switch ($this->_type) { + case self::TYPE_STATE_FIELD: + return $walker->walkStateFieldPathExpression($this); + case self::TYPE_SINGLE_VALUED_ASSOCIATION: + return $walker->walkSingleValuedAssociationPathExpression($this); + case self::TYPE_COLLECTION_VALUED_ASSOCIATION: + return $walker->walkCollectionValuedAssociationPathExpression($this); + default: + throw new \Exception("Unexhaustive match."); + } + } +} diff --git a/lib/Doctrine/ORM/Query/AST/SimpleStateFieldPathExpression.php b/lib/Doctrine/ORM/Query/AST/SimpleStateFieldPathExpression.php deleted file mode 100644 index b962af9da..000000000 --- a/lib/Doctrine/ORM/Query/AST/SimpleStateFieldPathExpression.php +++ /dev/null @@ -1,59 +0,0 @@ -. - */ - -namespace Doctrine\ORM\Query\AST; - -/** - * SimpleStateFieldPathExpression ::= IdentificationVariable "." SimpleStateField - * - * @author Guilherme Blanco - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL - * @link http://www.doctrine-project.org - * @since 2.0 - * @version $Revision$ - */ -class SimpleStateFieldPathExpression extends Node -{ - private $_identificationVariable = null; - private $_simpleStateField = null; - - public function __construct($identificationVariable, $simpleStateField) - { - $this->_identificationVariable = $identificationVariable; - $this->_simpleStateField = $simpleStateField; - } - - /* Getters */ - public function getIdentificationVariable() - { - return $this->_identificationVariable; - } - - public function getSimpleStateField() - { - return $this->_simpleStateField; - } - - public function dispatch($sqlWalker) - { - return $sqlWalker->walkSimpleStateFieldPathExpression($this); - } -} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Query/AST/StateFieldPathExpression.php b/lib/Doctrine/ORM/Query/AST/StateFieldPathExpression.php deleted file mode 100644 index c72f51717..000000000 --- a/lib/Doctrine/ORM/Query/AST/StateFieldPathExpression.php +++ /dev/null @@ -1,124 +0,0 @@ -. - */ - -namespace Doctrine\ORM\Query\AST; - -/** - * StateFieldPathExpression ::= SimpleStateFieldPathExpression | SimpleStateFieldAssociationPathExpression - * - * @author robo - */ -class StateFieldPathExpression extends Node -{ - //const TYPE_COLLECTION_VALUED_ASSOCIATION = 1; - //const TYPE_SINGLE_VALUED_ASSOCIATION = 2; - //const TYPE_STATE_FIELD = 3; - //private $_type; - - - private $_parts; - // Information that is attached during semantical analysis. - private $_isSimpleStateFieldPathExpression = false; - private $_isSimpleStateFieldAssociationPathExpression = false; - private $_embeddedClassFields = array(); - private $_singleValuedAssociationFields = array(); - private $_collectionValuedAssociationFields = array(); - - public function __construct(array $parts) - { - $this->_parts = $parts; - } - - public function getParts() { - return $this->_parts; - } - - /** - * Gets whether the path expression represents a state field that is reached - * either directly (u.name) or by navigating over optionally many embedded class instances - * (u.address.zip). - * - * @return boolean - */ - public function isSimpleStateFieldPathExpression() - { - return $this->_isSimpleStateFieldPathExpression; - } - - /** - * Gets whether the path expression represents a state field that is reached - * by navigating over at least one single-valued association and optionally - * many embedded class instances. (u.Group.address.zip, u.Group.address, ...) - * - * @return boolean - */ - public function isSimpleStateFieldAssociationPathExpression() - { - return $this->_isSimpleStateFieldAssociationPathExpression; - } - - public function isPartEmbeddedClassField($part) - { - return isset($this->_embeddedClassFields[$part]); - } - - public function isPartSingleValuedAssociationField($part) - { - return isset($this->_singleValuedAssociationFields[$part]); - } - - public function isPartCollectionValuedAssociationField($part) - { - return isset($this->_collectionValuedAssociationFields[$part]); - } - - /* Setters to attach semantical information during semantical analysis. */ - - public function setIsSimpleStateFieldPathExpression($bool) - { - $this->_isSimpleStateFieldPathExpression = $bool; - } - - public function setIsSimpleStateFieldAssociationPathExpression($bool) - { - $this->_isSimpleStateFieldAssociationPathExpression = $bool; - } - - public function setIsEmbeddedClassPart($part) - { - $this->_embeddedClassFields[$part] = true; - } - - public function setIsSingleValuedAssociationPart($part) - { - $this->_singleValuedAssociationFields[$part] = true; - } - - public function setIsCollectionValuedAssociationPart($part) - { - $this->_collectionValuedAssociationFields[$part] = true; - } - - public function dispatch($sqlWalker) - { - return $sqlWalker->walkStateFieldPathExpression($this); - } -} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Query/Lexer.php b/lib/Doctrine/ORM/Query/Lexer.php index d853b862a..5dcf9ca2c 100644 --- a/lib/Doctrine/ORM/Query/Lexer.php +++ b/lib/Doctrine/ORM/Query/Lexer.php @@ -88,6 +88,8 @@ class Lexer const T_WITH = 145; const T_TRUE = 146; const T_FALSE = 147; + const T_MEMBER = 148; + const T_OF = 149; private $_keywordsTable; @@ -131,8 +133,8 @@ class Lexer /** * Checks whether a given token matches the current lookahead. * - * @param $token - * @return + * @param integer|string $token + * @return boolean */ public function isNextToken($token) { @@ -170,7 +172,7 @@ class Lexer * @param string $identifier identifier name * @return int token type */ - public function _checkLiteral($identifier) + private function _checkLiteral($identifier) { $name = 'Doctrine\ORM\Query\Lexer::T_' . strtoupper($identifier); @@ -237,6 +239,7 @@ class Lexer } if ($value[0] === "'" && $value[strlen($value) - 1] === "'") { $type = self::T_STRING; + $value = str_replace("''", "'", substr($value, 1, strlen($value) - 2)); } else if (ctype_alpha($value[0]) || $value[0] === '_') { $type = $this->_checkLiteral($value); } else if ($value[0] === '?' || $value[0] === ':') { diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index 66a3d3f4e..751fb9f74 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -47,7 +47,7 @@ class Parser 'trim' => 'Doctrine\ORM\Query\AST\Functions\TrimFunction', 'lower' => 'Doctrine\ORM\Query\AST\Functions\LowerFunction', 'upper' => 'Doctrine\ORM\Query\AST\Functions\UpperFunction' - ); + ); /** Maps registered numeric function names to class names. */ private static $_NUMERIC_FUNCTIONS = array( @@ -83,19 +83,19 @@ class Parser private $_deferredPathExpressionStacks = array(); /** - * A scanner object. + * The lexer. * * @var Doctrine\ORM\Query\Lexer */ private $_lexer; /** - * The Parser Result object. + * The parser result. * * @var Doctrine\ORM\Query\ParserResult */ private $_parserResult; - + /** * The EntityManager. * @@ -111,12 +111,13 @@ class Parser private $_query; /** - * Map of declared classes in the parsed query. - * Maps the declared DQL alias (key) to the class name (value). + * Map of declared query components in the parsed query. * * @var array */ private $_queryComponents = array(); + + private $_sqlTreeWalker; /** * Creates a new query parser object. @@ -129,7 +130,6 @@ class Parser $this->_em = $query->getEntityManager(); $this->_lexer = new Lexer($query->getDql()); $this->_parserResult = new ParserResult; - //$this->_parserResult->setEntityManager($this->_em); } /** @@ -157,10 +157,10 @@ class Parser } /** - * Free this parser enabling it to be reused - * - * @param boolean $deep Whether to clean peek and reset errors - * @param integer $position Position to reset + * Free this parser enabling it to be reused + * + * @param boolean $deep Whether to clean peek and reset errors + * @param integer $position Position to reset */ public function free($deep = false, $position = 0) { @@ -180,14 +180,14 @@ class Parser /** * Parses a query string. - * + * * @return ParserResult */ public function parse() { // Parse & build AST - $AST = $this->_QueryLanguage(); - + $AST = $this->QueryLanguage(); + // Check for end of string if ($this->_lexer->lookahead !== null) { //var_dump($this->_lexer->lookahead); @@ -195,7 +195,7 @@ class Parser } // Create SqlWalker who creates the SQL from the AST - $sqlWalker = new SqlWalker($this->_query, $this->_parserResult, $this->_queryComponents); + $sqlWalker = $this->_sqlTreeWalker ?: new SqlWalker($this->_query, $this->_parserResult, $this->_queryComponents); // Assign an SQL executor to the parser result $this->_parserResult->setSqlExecutor(Exec\AbstractExecutor::create($AST, $sqlWalker)); @@ -274,15 +274,15 @@ class Parser * @param array $token Token that it was processing. */ /*protected function _logError($message = '', $token) - { - if ($this->_errorDistance >= self::MIN_ERROR_DISTANCE) { - $message = 'line 0, col ' . $token['position'] . ': ' . $message; - $this->_errors[] = $message; - } + { + if ($this->_errorDistance >= self::MIN_ERROR_DISTANCE) { + $message = 'line 0, col ' . $token['position'] . ': ' . $message; + $this->_errors[] = $message; + } + + $this->_errorDistance = 0; + }*/ - $this->_errorDistance = 0; - }*/ - /** * Gets the EntityManager used by the parser. * @@ -319,16 +319,16 @@ class Parser /** * QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement */ - public function _QueryLanguage() + public function QueryLanguage() { $this->_lexer->moveNext(); switch ($this->_lexer->lookahead['type']) { case Lexer::T_SELECT: - return $this->_SelectStatement(); + return $this->SelectStatement(); case Lexer::T_UPDATE: - return $this->_UpdateStatement(); + return $this->UpdateStatement(); case Lexer::T_DELETE: - return $this->_DeleteStatement(); + return $this->DeleteStatement(); default: $this->syntaxError('SELECT, UPDATE or DELETE'); break; @@ -338,27 +338,27 @@ class Parser /** * SelectStatement ::= SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] */ - public function _SelectStatement() + public function SelectStatement() { $this->_beginDeferredPathExpressionStack(); - $selectClause = $this->_SelectClause(); - $fromClause = $this->_FromClause(); + $selectClause = $this->SelectClause(); + $fromClause = $this->FromClause(); $this->_processDeferredPathExpressionStack(); $whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) ? - $this->_WhereClause() : null; + $this->WhereClause() : null; $groupByClause = $this->_lexer->isNextToken(Lexer::T_GROUP) ? - $this->_GroupByClause() : null; + $this->GroupByClause() : null; $havingClause = $this->_lexer->isNextToken(Lexer::T_HAVING) ? - $this->_HavingClause() : null; + $this->HavingClause() : null; $orderByClause = $this->_lexer->isNextToken(Lexer::T_ORDER) ? - $this->_OrderByClause() : null; + $this->OrderByClause() : null; return new AST\SelectStatement( - $selectClause, $fromClause, $whereClause, $groupByClause, $havingClause, $orderByClause + $selectClause, $fromClause, $whereClause, $groupByClause, $havingClause, $orderByClause ); } @@ -381,49 +381,19 @@ class Parser $exprStack = array_pop($this->_deferredPathExpressionStacks); $qComps = $this->_queryComponents; foreach ($exprStack as $expr) { - $parts = $expr->getParts(); - $numParts = count($parts); - $dqlAlias = $parts[0]; - if (count($parts) == 2) { - $expr->setIsSimpleStateFieldPathExpression(true); - if ( ! $qComps[$dqlAlias]['metadata']->hasField($parts[1])) { - $this->semanticalError('The class ' . $qComps[$dqlAlias]['metadata']->name - . ' has no simple state field named ' . $parts[1]); - } - } else { - $embeddedClassFieldSeen = false; - $assocSeen = false; - for ($i = 1; $i < $numParts - 1; ++$i) { - if ($qComps[$dqlAlias]['metadata']->hasAssociation($parts[$i])) { - if ($embeddedClassFieldSeen) { - $this->semanticalError('Invalid navigation path.'); - } - // Indirect join - $assoc = $qComps[$dqlAlias]['metadata']->getAssociationMapping($parts[$i]); - if ( ! $assoc->isOneToOne()) { - $this->semanticalError('Single-valued association expected.'); - } - $expr->setIsSingleValuedAssociationPart($parts[$i]); - //TODO... - $assocSeen = true; - } else if ($qComps[$dqlAlias]['metadata']->hasEmbeddedClassField($parts[$i])) { - //TODO... - $expr->setIsEmbeddedClassPart($parts[$i]); - $this->syntaxError(); - } else { - $this->syntaxError(); - } - } - if ( ! $assocSeen) { - $expr->setIsSimpleStateFieldPathExpression(true); - } else { - $expr->setIsSimpleStateFieldAssociationPathExpression(true); - } - // Last part MUST be a simple state field - if ( ! $qComps[$dqlAlias]['metadata']->hasField($parts[$numParts-1])) { - $this->semanticalError('The class ' . $qComps[$dqlAlias]['metadata']->name - . ' has no simple state field named ' . $parts[$numParts-1]); - } + switch ($expr->getType()) { + case AST\PathExpression::TYPE_STATE_FIELD: + $this->_validateStateFieldPathExpression($expr); + break; + case AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION: + $this->_validateSingleValuedAssociationPathExpression($expr); + break; + case AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION: + $this->_validateCollectionValuedAssociationPathExpression($expr); + break; + default: + $this->semanticalError('Encountered invalid PathExpression.'); + break; } } } @@ -431,22 +401,22 @@ class Parser /** * UpdateStatement ::= UpdateClause [WhereClause] */ - public function _UpdateStatement() + public function UpdateStatement() { - $updateStatement = new AST\UpdateStatement($this->_UpdateClause()); + $updateStatement = new AST\UpdateStatement($this->UpdateClause()); $updateStatement->setWhereClause( - $this->_lexer->isNextToken(Lexer::T_WHERE) ? $this->_WhereClause() : null - ); + $this->_lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null + ); return $updateStatement; } /** * UpdateClause ::= "UPDATE" AbstractSchemaName [["AS"] AliasIdentificationVariable] "SET" UpdateItem {"," UpdateItem}* */ - public function _UpdateClause() + public function UpdateClause() { $this->match(Lexer::T_UPDATE); - $abstractSchemaName = $this->_AbstractSchemaName(); + $abstractSchemaName = $this->AbstractSchemaName(); $aliasIdentificationVariable = null; if ($this->_lexer->isNextToken(Lexer::T_AS)) { $this->match(Lexer::T_AS); @@ -459,19 +429,19 @@ class Parser } $this->match(Lexer::T_SET); $updateItems = array(); - $updateItems[] = $this->_UpdateItem(); + $updateItems[] = $this->UpdateItem(); while ($this->_lexer->isNextToken(',')) { $this->match(','); - $updateItems[] = $this->_UpdateItem(); + $updateItems[] = $this->UpdateItem(); } $classMetadata = $this->_em->getClassMetadata($abstractSchemaName); // Building queryComponent $queryComponent = array( - 'metadata' => $classMetadata, - 'parent' => null, - 'relation' => null, - 'map' => null + 'metadata' => $classMetadata, + 'parent' => null, + 'relation' => null, + 'map' => null ); $this->_queryComponents[$aliasIdentificationVariable] = $queryComponent; @@ -484,7 +454,7 @@ class Parser /** * UpdateItem ::= [IdentificationVariable "."] {StateField | SingleValuedAssociationField} "=" NewValue */ - public function _UpdateItem() + public function UpdateItem() { $peek = $this->_lexer->glimpse(); $identVariable = null; @@ -498,7 +468,7 @@ class Parser $this->match(Lexer::T_IDENTIFIER); $field = $this->_lexer->token['value']; $this->match('='); - $newValue = $this->_NewValue(); + $newValue = $this->NewValue(); $updateItem = new AST\UpdateItem($field, $newValue); $updateItem->setIdentificationVariable($identVariable); @@ -511,7 +481,7 @@ class Parser * EnumPrimary | SimpleEntityExpression | "NULL" * @todo Implementation still incomplete. */ - public function _NewValue() + public function NewValue() { if ($this->_lexer->isNextToken(Lexer::T_NULL)) { $this->match(Lexer::T_NULL); @@ -521,7 +491,7 @@ class Parser return new AST\InputParameter($this->_lexer->token['value']); } else if ($this->_lexer->isNextToken(Lexer::T_STRING)) { //TODO: Can be StringPrimary or EnumPrimary - return $this->_StringPrimary(); + return $this->StringPrimary(); } else { $this->syntaxError('Not yet implemented-1.'); } @@ -530,25 +500,25 @@ class Parser /** * DeleteStatement ::= DeleteClause [WhereClause] */ - public function _DeleteStatement() + public function DeleteStatement() { - $deleteStatement = new AST\DeleteStatement($this->_DeleteClause()); + $deleteStatement = new AST\DeleteStatement($this->DeleteClause()); $deleteStatement->setWhereClause( - $this->_lexer->isNextToken(Lexer::T_WHERE) ? $this->_WhereClause() : null - ); + $this->_lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null + ); return $deleteStatement; } /** * DeleteClause ::= "DELETE" ["FROM"] AbstractSchemaName [["AS"] AliasIdentificationVariable] */ - public function _DeleteClause() + public function DeleteClause() { $this->match(Lexer::T_DELETE); if ($this->_lexer->isNextToken(Lexer::T_FROM)) { $this->match(Lexer::T_FROM); } - $deleteClause = new AST\DeleteClause($this->_AbstractSchemaName()); + $deleteClause = new AST\DeleteClause($this->AbstractSchemaName()); if ($this->_lexer->isNextToken(Lexer::T_AS)) { $this->match(Lexer::T_AS); } @@ -561,7 +531,7 @@ class Parser $classMetadata = $this->_em->getClassMetadata($deleteClause->getAbstractSchemaName()); $queryComponent = array( - 'metadata' => $classMetadata + 'metadata' => $classMetadata ); $this->_queryComponents[$deleteClause->getAliasIdentificationVariable()] = $queryComponent; @@ -571,7 +541,7 @@ class Parser /** * SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression} */ - public function _SelectClause() + public function SelectClause() { $isDistinct = false; $this->match(Lexer::T_SELECT); @@ -584,10 +554,10 @@ class Parser // Process SelectExpressions (1..N) $selectExpressions = array(); - $selectExpressions[] = $this->_SelectExpression(); + $selectExpressions[] = $this->SelectExpression(); while ($this->_lexer->isNextToken(',')) { $this->match(','); - $selectExpressions[] = $this->_SelectExpression(); + $selectExpressions[] = $this->SelectExpression(); } return new AST\SelectClause($selectExpressions, $isDistinct); @@ -596,15 +566,15 @@ class Parser /** * FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration}* */ - public function _FromClause() + public function FromClause() { $this->match(Lexer::T_FROM); $identificationVariableDeclarations = array(); - $identificationVariableDeclarations[] = $this->_IdentificationVariableDeclaration(); + $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration(); while ($this->_lexer->isNextToken(',')) { $this->match(','); - $identificationVariableDeclarations[] = $this->_IdentificationVariableDeclaration(); + $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration(); } return new AST\FromClause($identificationVariableDeclarations); @@ -616,24 +586,24 @@ class Parser * (AggregateExpression | "(" Subselect ")") [["AS"] FieldAliasIdentificationVariable] | * Function */ - public function _SelectExpression() + public function SelectExpression() { $expression = null; $fieldIdentificationVariable = null; $peek = $this->_lexer->glimpse(); // First we recognize for an IdentificationVariable (DQL class alias) if ($peek['value'] != '.' && $peek['value'] != '(' && $this->_lexer->lookahead['type'] === Lexer::T_IDENTIFIER) { - $expression = $this->_IdentificationVariable(); + $expression = $this->IdentificationVariable(); } else if (($isFunction = $this->_isFunction()) !== false || $this->_isSubselect()) { if ($isFunction) { - if ($this->_isAggregateFunction($this->_lexer->lookahead['type'])) { - $expression = $this->_AggregateExpression(); + if ($this->isAggregateFunction($this->_lexer->lookahead['type'])) { + $expression = $this->AggregateExpression(); } else { $expression = $this->_Function(); } } else { $this->match('('); - $expression = $this->_Subselect(); + $expression = $this->Subselect(); $this->match(')'); } if ($this->_lexer->isNextToken(Lexer::T_AS)) { @@ -646,7 +616,7 @@ class Parser } else { //TODO: If hydration mode is OBJECT throw an exception ("partial object dangerous...") // unless the doctrine.forcePartialLoad query hint is set - $expression = $this->_StateFieldPathExpression(); + $expression = $this->StateFieldPathExpression(); } return new AST\SelectExpression($expression, $fieldIdentificationVariable); } @@ -654,7 +624,7 @@ class Parser /** * IdentificationVariable ::= identifier */ - public function _IdentificationVariable() + public function IdentificationVariable() { $this->match(Lexer::T_IDENTIFIER); return $this->_lexer->token['value']; @@ -663,55 +633,55 @@ class Parser /** * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {JoinVariableDeclaration}* */ - public function _IdentificationVariableDeclaration() + public function IdentificationVariableDeclaration() { - $rangeVariableDeclaration = $this->_RangeVariableDeclaration(); - $indexBy = $this->_lexer->isNextToken(Lexer::T_INDEX) ? $this->_IndexBy() : null; + $rangeVariableDeclaration = $this->RangeVariableDeclaration(); + $indexBy = $this->_lexer->isNextToken(Lexer::T_INDEX) ? $this->IndexBy() : null; $joinVariableDeclarations = array(); while ( - $this->_lexer->isNextToken(Lexer::T_LEFT) || - $this->_lexer->isNextToken(Lexer::T_INNER) || - $this->_lexer->isNextToken(Lexer::T_JOIN) + $this->_lexer->isNextToken(Lexer::T_LEFT) || + $this->_lexer->isNextToken(Lexer::T_INNER) || + $this->_lexer->isNextToken(Lexer::T_JOIN) ) { - $joinVariableDeclarations[] = $this->_JoinVariableDeclaration(); + $joinVariableDeclarations[] = $this->JoinVariableDeclaration(); } return new AST\IdentificationVariableDeclaration( - $rangeVariableDeclaration, $indexBy, $joinVariableDeclarations + $rangeVariableDeclaration, $indexBy, $joinVariableDeclarations ); } /** * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable */ - public function _RangeVariableDeclaration() + public function RangeVariableDeclaration() { - $abstractSchemaName = $this->_AbstractSchemaName(); + $abstractSchemaName = $this->AbstractSchemaName(); if ($this->_lexer->isNextToken(Lexer::T_AS)) { $this->match(Lexer::T_AS); } - $aliasIdentificationVariable = $this->_AliasIdentificationVariable(); + $aliasIdentificationVariable = $this->AliasIdentificationVariable(); $classMetadata = $this->_em->getClassMetadata($abstractSchemaName); // Building queryComponent $queryComponent = array( - 'metadata' => $classMetadata, - 'parent' => null, - 'relation' => null, - 'map' => null + 'metadata' => $classMetadata, + 'parent' => null, + 'relation' => null, + 'map' => null ); $this->_queryComponents[$aliasIdentificationVariable] = $queryComponent; return new AST\RangeVariableDeclaration( - $classMetadata, $aliasIdentificationVariable + $classMetadata, $aliasIdentificationVariable ); } /** * AbstractSchemaName ::= identifier */ - public function _AbstractSchemaName() + public function AbstractSchemaName() { $this->match(Lexer::T_IDENTIFIER); return $this->_lexer->token['value']; @@ -720,7 +690,7 @@ class Parser /** * AliasIdentificationVariable = identifier */ - public function _AliasIdentificationVariable() + public function AliasIdentificationVariable() { $this->match(Lexer::T_IDENTIFIER); return $this->_lexer->token['value']; @@ -729,11 +699,11 @@ class Parser /** * JoinVariableDeclaration ::= Join [IndexBy] */ - public function _JoinVariableDeclaration() + public function JoinVariableDeclaration() { - $join = $this->_Join(); + $join = $this->Join(); $indexBy = $this->_lexer->isNextToken(Lexer::T_INDEX) ? - $this->_IndexBy() : null; + $this->IndexBy() : null; return new AST\JoinVariableDeclaration($join, $indexBy); } @@ -741,7 +711,7 @@ class Parser * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN" JoinAssociationPathExpression * ["AS"] AliasIdentificationVariable [("ON" | "WITH") ConditionalExpression] */ - public function _Join() + public function Join() { // Check Join type $joinType = AST\Join::JOIN_TYPE_INNER; @@ -760,28 +730,28 @@ class Parser $this->match(Lexer::T_JOIN); - $joinPathExpression = $this->_JoinPathExpression(); + $joinPathExpression = $this->JoinPathExpression(); if ($this->_lexer->isNextToken(Lexer::T_AS)) { $this->match(Lexer::T_AS); } - $aliasIdentificationVariable = $this->_AliasIdentificationVariable(); + $aliasIdentificationVariable = $this->AliasIdentificationVariable(); // Verify that the association exists. $parentClass = $this->_queryComponents[$joinPathExpression->getIdentificationVariable()]['metadata']; $assocField = $joinPathExpression->getAssociationField(); if ( ! $parentClass->hasAssociation($assocField)) { $this->semanticalError("Class " . $parentClass->name . - " has no association named '$assocField'."); + " has no association named '$assocField'."); } $targetClassName = $parentClass->getAssociationMapping($assocField)->getTargetEntityName(); // Building queryComponent $joinQueryComponent = array( - 'metadata' => $this->_em->getClassMetadata($targetClassName), - 'parent' => $joinPathExpression->getIdentificationVariable(), - 'relation' => $parentClass->getAssociationMapping($assocField), - 'map' => null + 'metadata' => $this->_em->getClassMetadata($targetClassName), + 'parent' => $joinPathExpression->getIdentificationVariable(), + 'relation' => $parentClass->getAssociationMapping($assocField), + 'map' => null ); $this->_queryComponents[$aliasIdentificationVariable] = $joinQueryComponent; @@ -796,7 +766,7 @@ class Parser } else { $this->match(Lexer::T_WITH); } - $join->setConditionalExpression($this->_ConditionalExpression()); + $join->setConditionalExpression($this->ConditionalExpression()); } return $join; @@ -805,125 +775,238 @@ class Parser /** * JoinPathExpression ::= IdentificationVariable "." (CollectionValuedAssociationField | SingleValuedAssociationField) */ - public function _JoinPathExpression() + public function JoinPathExpression() { - $identificationVariable = $this->_IdentificationVariable(); + $identificationVariable = $this->IdentificationVariable(); $this->match('.'); $this->match(Lexer::T_IDENTIFIER); return new AST\JoinPathExpression( - $identificationVariable, $this->_lexer->token['value'] + $identificationVariable, $this->_lexer->token['value'] ); } /** * IndexBy ::= "INDEX" "BY" SimpleStateFieldPathExpression */ - public function _IndexBy() + public function IndexBy() { $this->match(Lexer::T_INDEX); $this->match(Lexer::T_BY); - $pathExp = $this->_SimpleStateFieldPathExpression(); + $pathExp = $this->SimpleStateFieldPathExpression(); // Add the INDEX BY info to the query component - $this->_queryComponents[$pathExp->getIdentificationVariable()]['map'] = $pathExp->getSimpleStateField(); + $parts = $pathExp->getParts(); + $this->_queryComponents[$pathExp->getIdentificationVariable()]['map'] = $parts[0]; return $pathExp; } - /** - * SimpleStateFieldPathExpression ::= IdentificationVariable "." StateField - * @todo Implementation incomplete. Recognize StateField properly (see EBNF). - */ - public function _SimpleStateFieldPathExpression() - { - $identificationVariable = $this->_IdentificationVariable(); - $this->match('.'); - $this->match(Lexer::T_IDENTIFIER); - $simpleStateField = $this->_lexer->token['value']; - return new AST\SimpleStateFieldPathExpression($identificationVariable, $simpleStateField); - } - /** * StateFieldPathExpression ::= SimpleStateFieldPathExpression | SimpleStateFieldAssociationPathExpression */ - public function _StateFieldPathExpression() + public function StateFieldPathExpression() { + $pathExpr = $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD); + if ( ! empty($this->_deferredPathExpressionStacks)) { $exprStack = array_pop($this->_deferredPathExpressionStacks); - $this->match(Lexer::T_IDENTIFIER); - $parts = array($this->_lexer->token['value']); - while ($this->_lexer->isNextToken('.')) { - $this->match('.'); - $this->match(Lexer::T_IDENTIFIER); - $parts[] = $this->_lexer->token['value']; - } - $expr = new AST\StateFieldPathExpression($parts); - $exprStack[] = $expr; + $exprStack[] = $pathExpr; array_push($this->_deferredPathExpressionStacks, $exprStack); - return $expr; // EARLY EXIT! + + return $pathExpr; } - $parts = array(); - $stateFieldSeen = false; - $assocSeen = false; - - $identificationVariable = $this->_IdentificationVariable(); - if ( ! isset($this->_queryComponents[$identificationVariable])) { - $this->syntaxError("IdentificationVariable '$identificationVariable' was not declared."); - } - - $qComp = $this->_queryComponents[$identificationVariable]; - $parts[] = $identificationVariable; - - $class = $qComp['metadata']; - - if ( ! $this->_lexer->isNextToken('.')) { - if ($class->isIdentifierComposite) { - $this->syntaxError(); - } - $parts[] = $class->identifier[0]; - } - - while ($this->_lexer->isNextToken('.')) { - if ($stateFieldSeen) { - $this->syntaxError(); - } - $this->match('.'); - $part = $this->_IdentificationVariable(); - if ($class->hasField($part)) { - $stateFieldSeen = true; - } else if ($class->hasAssociation($part)) { - $assoc = $class->getAssociationMapping($part); - $class = $this->_em->getClassMetadata($assoc->getTargetEntityName()); - $assocSeen = true; - } else { - $this->semanticalError('The class ' . $class->name . - ' has no field or association named ' . $part); - } - $parts[] = $part; - } - - $pathExpr = new AST\StateFieldPathExpression($parts); - - if ($assocSeen) { - $pathExpr->setIsSimpleStateFieldAssociationPathExpression(true); - } else { - $pathExpr->setIsSimpleStateFieldPathExpression(true); - } + $this->_validateStateFieldPathExpression($pathExpr); return $pathExpr; } + /** + * SimpleStateFieldPathExpression ::= IdentificationVariable "." StateField + */ + public function SimpleStateFieldPathExpression() + { + $pathExpr = $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD); + + if (count($pathExpr->getParts()) > 1) { + $this->syntaxError('SimpleStateFieldPathExpression'); + } + + if ( ! empty($this->_deferredPathExpressionStacks)) { + $exprStack = array_pop($this->_deferredPathExpressionStacks); + $exprStack[] = $pathExpr; + array_push($this->_deferredPathExpressionStacks, $exprStack); + + return $pathExpr; + } + + $this->_validateStateFieldPathExpression($pathExpr); + + return $pathExpr; + } + + /** + * SingleValuedAssociationPathExpression ::= IdentificationVariable "." {SingleValuedAssociationField "."}* SingleValuedAssociationField + */ + public function SingleValuedAssociationPathExpression() + { + $pathExpr = $this->PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION); + if ( ! empty($this->_deferredPathExpressionStacks)) { + $exprStack = array_pop($this->_deferredPathExpressionStacks); + $exprStack[] = $pathExpr; + array_push($this->_deferredPathExpressionStacks, $exprStack); + + return $pathExpr; + } + + $this->_validateSingleValuedAssociationPathExpression($pathExpr); + + return $pathExpr; + } + + /** + * CollectionValuedPathExpression ::= IdentificationVariable "." {SingleValuedAssociationField "."}* CollectionValuedAssociationField + */ + public function CollectionValuedPathExpression() + { + $pathExpr = $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION); + if ( ! empty($this->_deferredPathExpressionStacks)) { + $exprStack = array_pop($this->_deferredPathExpressionStacks); + $exprStack[] = $pathExpr; + array_push($this->_deferredPathExpressionStacks, $exprStack); + + return $pathExpr; + } + + $this->_validateCollectionValuedPathExpression($pathExpr); + + return $pathExpr; + } + + /** + * Validates that the given PathExpression is a semantically correct + * CollectionValuedPathExpression as specified in the grammar. + * + * @param PathExpression $pathExpression + */ + private function _validateCollectionValuedPathExpression(AST\PathExpression $pathExpression) + { + $class = $this->_queryComponents[$pathExpression->getIdentificationVariable()]['metadata']; + $collectionValuedField = null; + foreach ($pathExpression->getParts() as $field) { + if ($collectionValuedField !== null) { + $this->semanticalError('Can not navigate through collection-valued field named ' . $collectionValuedField); + } + if ( ! isset($class->associationMappings[$field])) { + $this->semanticalError('Class ' . $class->name . ' has no association field named ' . $lastItem); + } + if ($class->associationMappings[$field]->isOneToOne()) { + $class = $this->_em->getClassMetadata($class->associationMappings[$field]->targetEntityName); + } else { + $collectionValuedField = $field; + } + } + + if ($collectionValuedField === null) { + $this->syntaxError('SingleValuedAssociationField or CollectionValuedAssociationField'); + } + } + + /** + * Validates that the given PathExpression is a semantically correct + * SingleValuedPathExpression as specified in the grammar. + * + * @param PathExpression $pathExpression + */ + private function _validateSingleValuedAssociationPathExpression(AST\PathExpression $pathExpression) + { + $class = $this->_queryComponents[$pathExpression->getIdentificationVariable()]['metadata']; + foreach ($pathExpression->getParts() as $field) { + if ( ! isset($class->associationMappings[$field])) { + $this->semanticalError('Class ' . $class->name . ' has no association field named ' . $field); + } + if ($class->associationMappings[$field]->isOneToOne()) { + $class = $this->_em->getClassMetadata($class->associationMappings[$field]->targetEntityName); + } else { + $this->syntaxError('SingleValuedAssociationField'); + } + } + } + + /** + * Validates that the given PathExpression is a semantically correct + * StateFieldPathExpression as specified in the grammar. + * + * @param PathExpression $pathExpression + */ + private function _validateStateFieldPathExpression(AST\PathExpression $pathExpression) + { + $class = $this->_queryComponents[$pathExpression->getIdentificationVariable()]['metadata']; + $stateFieldSeen = false; + foreach ($pathExpression->getParts() as $field) { + if ($stateFieldSeen) { + $this->semanticalError('Cannot navigate through state field.'); + } + if (isset($class->associationMappings[$field]) && $class->associationMappings[$field]->isOneToOne()) { + $class = $this->_em->getClassMetadata($class->associationMappings[$field]->targetEntityName); + } else if (isset($class->fieldMappings[$field])) { + $stateFieldSeen = true; + } else { + $this->semanticalError('SingleValuedAssociationField or StateField expected.'); + } + } + + if ( ! $stateFieldSeen) { + $this->semanticalError('Invalid StateFieldPathExpression. Must end in a state field.'); + } + } + + /** + * Parses an arbitrary path expression without any semantic validation. + * + * PathExpression ::= IdentificationVariable "." {identifier "."}* identifier + * + * @return PathExpression + */ + public function PathExpression($type) + { + $this->match(Lexer::T_IDENTIFIER); + $identificationVariable = $this->_lexer->token['value']; + $this->match('.'); + + $parts = array(); + while ($this->_lexer->isNextToken(Lexer::T_IDENTIFIER)) { + $this->match(Lexer::T_IDENTIFIER); + $parts[] = $this->_lexer->token['value']; + if ($this->_lexer->isNextToken('.')) { + $this->match('.'); + } else { + break; + } + } + + return new AST\PathExpression($type, $identificationVariable, $parts); + } + + /** + * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression + * SimpleEntityExpression ::= IdentificationVariable | InputParameter + */ + public function EntityExpression() + { + + } + /** * NullComparisonExpression ::= (SingleValuedPathExpression | InputParameter) "IS" ["NOT"] "NULL" * @todo Implementation incomplete for SingleValuedPathExpression. */ - public function _NullComparisonExpression() + public function NullComparisonExpression() { if ($this->_lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) { $this->match(Lexer::T_INPUT_PARAMETER); $expr = new AST\InputParameter($this->_lexer->token['value']); } else { //TODO: Support SingleValuedAssociationPathExpression - $expr = $this->_StateFieldPathExpression(); + $expr = $this->StateFieldPathExpression(); } $nullCompExpr = new AST\NullComparisonExpression($expr); $this->match(Lexer::T_IS); @@ -932,7 +1015,7 @@ class Parser $nullCompExpr->setNot(true); } $this->match(Lexer::T_NULL); - + return $nullCompExpr; } @@ -942,7 +1025,7 @@ class Parser * "COUNT" "(" ["DISTINCT"] (IdentificationVariable | SingleValuedAssociationPathExpression | StateFieldPathExpression) ")" * @todo Implementation incomplete. Support for SingleValuedAssociationPathExpression. */ - public function _AggregateExpression() + public function AggregateExpression() { $isDistinct = false; $functionName = ''; @@ -954,8 +1037,8 @@ class Parser $this->match(Lexer::T_DISTINCT); $isDistinct = true; } - // For now we only support a PathExpression here... - $pathExp = $this->_StateFieldPathExpression(); + //TODO: Support SingleValuedAssociationPathExpression + $pathExp = $this->StateFieldPathExpression(); $this->match(')'); } else { if ($this->_lexer->isNextToken(Lexer::T_AVG)) { @@ -971,10 +1054,10 @@ class Parser } $functionName = $this->_lexer->token['value']; $this->match('('); - $pathExp = $this->_StateFieldPathExpression(); + $pathExp = $this->StateFieldPathExpression(); $this->match(')'); } - + return new AST\AggregateExpression($functionName, $pathExp, $isDistinct); } @@ -983,15 +1066,15 @@ class Parser * GroupByItem ::= IdentificationVariable | SingleValuedPathExpression * @todo Implementation incomplete for GroupByItem. */ - public function _GroupByClause() + public function GroupByClause() { $this->match(Lexer::T_GROUP); $this->match(Lexer::T_BY); $groupByItems = array(); - $groupByItems[] = $this->_StateFieldPathExpression(); + $groupByItems[] = $this->StateFieldPathExpression(); while ($this->_lexer->isNextToken(',')) { $this->match(','); - $groupByItems[] = $this->_StateFieldPathExpression(); + $groupByItems[] = $this->StateFieldPathExpression(); } return new AST\GroupByClause($groupByItems); } @@ -999,24 +1082,24 @@ class Parser /** * HavingClause ::= "HAVING" ConditionalExpression */ - public function _HavingClause() + public function HavingClause() { $this->match(Lexer::T_HAVING); - return new AST\HavingClause($this->_ConditionalExpression()); + return new AST\HavingClause($this->ConditionalExpression()); } /** * OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}* */ - public function _OrderByClause() + public function OrderByClause() { $this->match(Lexer::T_ORDER); $this->match(Lexer::T_BY); $orderByItems = array(); - $orderByItems[] = $this->_OrderByItem(); + $orderByItems[] = $this->OrderByItem(); while ($this->_lexer->isNextToken(',')) { $this->match(','); - $orderByItems[] = $this->_OrderByItem(); + $orderByItems[] = $this->OrderByItem(); } return new AST\OrderByClause($orderByItems); } @@ -1025,9 +1108,9 @@ class Parser * OrderByItem ::= ResultVariable | StateFieldPathExpression ["ASC" | "DESC"] * @todo Implementation incomplete for OrderByItem. */ - public function _OrderByItem() + public function OrderByItem() { - $item = new AST\OrderByItem($this->_StateFieldPathExpression()); + $item = new AST\OrderByItem($this->StateFieldPathExpression()); if ($this->_lexer->isNextToken(Lexer::T_ASC)) { $this->match(Lexer::T_ASC); $item->setAsc(true); @@ -1043,22 +1126,22 @@ class Parser /** * WhereClause ::= "WHERE" ConditionalExpression */ - public function _WhereClause() + public function WhereClause() { $this->match(Lexer::T_WHERE); - return new AST\WhereClause($this->_ConditionalExpression()); + return new AST\WhereClause($this->ConditionalExpression()); } /** * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}* */ - public function _ConditionalExpression() + public function ConditionalExpression() { $conditionalTerms = array(); - $conditionalTerms[] = $this->_ConditionalTerm(); + $conditionalTerms[] = $this->ConditionalTerm(); while ($this->_lexer->isNextToken(Lexer::T_OR)) { $this->match(Lexer::T_OR); - $conditionalTerms[] = $this->_ConditionalTerm(); + $conditionalTerms[] = $this->ConditionalTerm(); } return new AST\ConditionalExpression($conditionalTerms); } @@ -1066,13 +1149,13 @@ class Parser /** * ConditionalTerm ::= ConditionalFactor {"AND" ConditionalFactor}* */ - public function _ConditionalTerm() + public function ConditionalTerm() { $conditionalFactors = array(); - $conditionalFactors[] = $this->_ConditionalFactor(); + $conditionalFactors[] = $this->ConditionalFactor(); while ($this->_lexer->isNextToken(Lexer::T_AND)) { $this->match(Lexer::T_AND); - $conditionalFactors[] = $this->_ConditionalFactor(); + $conditionalFactors[] = $this->ConditionalFactor(); } return new AST\ConditionalTerm($conditionalFactors); } @@ -1080,21 +1163,21 @@ class Parser /** * ConditionalFactor ::= ["NOT"] ConditionalPrimary */ - public function _ConditionalFactor() + public function ConditionalFactor() { $not = false; if ($this->_lexer->isNextToken(Lexer::T_NOT)) { $this->match(Lexer::T_NOT); $not = true; } - return new AST\ConditionalFactor($this->_ConditionalPrimary(), $not); + return new AST\ConditionalFactor($this->ConditionalPrimary(), $not); } /** * ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")" * @todo Implementation incomplete: Recognition of SimpleConditionalExpression is incomplete. */ - public function _ConditionalPrimary() + public function ConditionalPrimary() { $condPrimary = new AST\ConditionalPrimary; if ($this->_lexer->isNextToken('(')) { @@ -1115,17 +1198,17 @@ class Parser $comparisonOps = array("=", "<", "<=", "<>", ">", ">=", "!="); if (in_array($peek['value'], $comparisonOps)) { - $condPrimary->setSimpleConditionalExpression($this->_SimpleConditionalExpression()); + $condPrimary->setSimpleConditionalExpression($this->SimpleConditionalExpression()); } else { $this->match('('); - $conditionalExpression = $this->_ConditionalExpression(); + $conditionalExpression = $this->ConditionalExpression(); $this->match(')'); $condPrimary->setConditionalExpression($conditionalExpression); } } else { - $condPrimary->setSimpleConditionalExpression($this->_SimpleConditionalExpression()); + $condPrimary->setSimpleConditionalExpression($this->SimpleConditionalExpression()); } - + return $condPrimary; } @@ -1134,11 +1217,8 @@ class Parser * ComparisonExpression | BetweenExpression | LikeExpression | * InExpression | NullComparisonExpression | ExistsExpression | * EmptyCollectionComparisonExpression | CollectionMemberExpression - * - * @todo Implementation incomplete. This is difficult and a strict recognition may - * even require backtracking. */ - public function _SimpleConditionalExpression() + public function SimpleConditionalExpression() { if ($this->_lexer->isNextToken(Lexer::T_NOT)) { $token = $this->_lexer->glimpse(); @@ -1146,13 +1226,13 @@ class Parser $token = $this->_lexer->lookahead; } if ($token['type'] === Lexer::T_EXISTS) { - return $this->_ExistsExpression(); + return $this->ExistsExpression(); } - $stateFieldPathExpr = false; - if ($token['type'] === Lexer::T_IDENTIFIER) { + $pathExprOrInputParam = false; + if ($token['type'] === Lexer::T_IDENTIFIER || $token['type'] === Lexer::T_INPUT_PARAMETER) { // Peek beyond the PathExpression - $stateFieldPathExpr = true; + $pathExprOrInputParam = true; $peek = $this->_lexer->peek(); while ($peek['value'] === '.') { $this->_lexer->peek(); @@ -1168,117 +1248,92 @@ class Parser $token = $peek; } - if ($stateFieldPathExpr) { + if ($pathExprOrInputParam) { switch ($token['type']) { - case Lexer::T_BETWEEN: - return $this->_BetweenExpression(); - case Lexer::T_LIKE: - return $this->_LikeExpression(); - case Lexer::T_IN: - return $this->_InExpression(); - case Lexer::T_IS: - return $this->_NullComparisonExpression(); case Lexer::T_NONE: - return $this->_ComparisonExpression(); + return $this->ComparisonExpression(); + case Lexer::T_BETWEEN: + return $this->BetweenExpression(); + case Lexer::T_LIKE: + return $this->LikeExpression(); + case Lexer::T_IN: + return $this->InExpression(); + case Lexer::T_IS: + return $this->NullComparisonExpression(); + case Lexer::T_MEMBER: + return $this->CollectionMemberExpression(); default: $this->syntaxError(); } } else { - return $this->_ComparisonExpression(); + return $this->ComparisonExpression(); } } + + /** + * CollectionMemberExpression ::= EntityExpression ["NOT"] "MEMBER" ["OF"] CollectionValuedPathExpression + * + * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression + * SimpleEntityExpression ::= IdentificationVariable | InputParameter + * + * @return AST\CollectionMemberExpression + * @todo Support SingleValuedAssociationPathExpression and IdentificationVariable + */ + public function CollectionMemberExpression() + { + $isNot = false; + if ($this->_lexer->lookahead['type'] == Lexer::T_INPUT_PARAMETER) { + $this->match($this->_lexer->lookahead['value']); + $entityExpr = new AST\InputParameter($this->_lexer->token['value']); + if ($this->_lexer->isNextToken(Lexer::T_NOT)) { + $isNot = true; + $this->match(Lexer::T_NOT); + } + $this->match(Lexer::T_MEMBER); + if ($this->_lexer->isNextToken(Lexer::T_OF)) { + $this->match(Lexer::T_OF); + } + $collValuedPathExpr = $this->CollectionValuedPathExpression(); + } else { + throw QueryException::notImplemented(); + } + + return new AST\CollectionMemberExpression($entityExpr, $collValuedPathExpr, $isNot); + } /** - * ComparisonExpression ::= - * ArithmeticExpression ComparisonOperator (QuantifiedExpression | ArithmeticExpression) | - * StringExpression ComparisonOperator (StringExpression | QuantifiedExpression) | - * BooleanExpression ("=" | "<>" | "!=") (BooleanExpression | QuantifiedExpression) | - * EnumExpression ("=" | "<>" | "!=") (EnumExpression | QuantifiedExpression) | - * DatetimeExpression ComparisonOperator (DatetimeExpression | QuantifiedExpression) | - * EntityExpression ("=" | "<>") (EntityExpression | QuantifiedExpression) - * - * @todo Implementation incomplete. Seems difficult. + * ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression ) + * + * @return AST\ComparisonExpression + * @todo Semantical checks whether $leftExpr $operator and $rightExpr are compatible. */ - public function _ComparisonExpression() + public function ComparisonExpression() { $peek = $this->_lexer->glimpse(); - if ($this->_lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) { - if ($this->_isComparisonOperator($peek)) { - $this->match(Lexer::T_INPUT_PARAMETER); - $leftExpr = new AST\InputParameter($this->_lexer->token['value']); - } else { - $leftExpr = $this->_ArithmeticExpression(); - } - $operator = $this->_ComparisonOperator(); - $rightExpr = $this->_ArithmeticExpression(); - //... - } - else if ($this->_lexer->isNextToken('(') && $peek['type'] == Lexer::T_SELECT) { - $leftExpr = $this->_Subselect(); - //... - } - else if ($this->_lexer->isNextToken(Lexer::T_IDENTIFIER) && $peek['value'] == '(') { - $peek2 = $this->_peekBeyond(')'); - if ($this->_isComparisonOperator($peek2)) { - if ($this->_isStringFunction($this->_lexer->lookahead['value'])) { - $leftExpr = $this->_FunctionsReturningStrings(); - $operator = $this->_ComparisonOperator(); - if ($this->_isNextAllAnySome()) { - $rightExpr = $this->_QuantifiedExpression(); - } else { - $rightExpr = $this->_StringPrimary(); - } - } else if ($this->_isNumericFunction($this->_lexer->lookahead['value'])) { - $leftExpr = $this->_FunctionsReturningNumerics(); - $operator = $this->_ComparisonOperator(); - if ($this->_isNextAllAnySome()) { - $rightExpr = $this->_QuantifiedExpression(); - } else { - $rightExpr = $this->_ArithmeticExpression(); - } - } else { - $leftExpr = $this->_FunctionsReturningDatetime(); - $operator = $this->_ComparisonOperator(); - if ($this->_isNextAllAnySome()) { - $rightExpr = $this->_QuantifiedExpression(); - } else { - $rightExpr = $this->_DatetimePrimary(); - } - } - } else { - $leftExpr = $this->_ArithmeticExpression(); - $operator = $this->_ComparisonOperator(); - if ($this->_isNextAllAnySome()) { - $rightExpr = $this->_QuantifiedExpression(); - } else { - $rightExpr = $this->_StringExpression(); - } - } + $leftExpr = $this->ArithmeticExpression(); + $operator = $this->ComparisonOperator(); + + if ($this->_isNextAllAnySome()) { + $rightExpr = $this->QuantifiedExpression(); } else { - $leftExpr = $this->_ArithmeticExpression(); - $operator = $this->_ComparisonOperator(); - if ($this->_isNextAllAnySome()) { - $rightExpr = $this->_QuantifiedExpression(); - } else { - $rightExpr = $this->_ArithmeticExpression(); - } + $rightExpr = $this->ArithmeticExpression(); } return new AST\ComparisonExpression($leftExpr, $operator, $rightExpr); } - + /** * Checks whether the current lookahead token of the lexer has the type * T_ALL, T_ANY or T_SOME. - * + * * @return boolean */ private function _isNextAllAnySome() { return $this->_lexer->lookahead['type'] === Lexer::T_ALL || - $this->_lexer->lookahead['type'] === Lexer::T_ANY || - $this->_lexer->lookahead['type'] === Lexer::T_SOME; + $this->_lexer->lookahead['type'] === Lexer::T_ANY || + $this->_lexer->lookahead['type'] === Lexer::T_SOME; } /** @@ -1287,12 +1342,12 @@ class Parser public function _Function() { $funcName = $this->_lexer->lookahead['value']; - if ($this->_isStringFunction($funcName)) { - return $this->_FunctionsReturningStrings(); - } else if ($this->_isNumericFunction($funcName)) { - return $this->_FunctionsReturningNumerics(); - } else if ($this->_isDatetimeFunction($funcName)) { - return $this->_FunctionsReturningDatetime(); + if ($this->isStringFunction($funcName)) { + return $this->FunctionsReturningStrings(); + } else if ($this->isNumericFunction($funcName)) { + return $this->FunctionsReturningNumerics(); + } else if ($this->isDatetimeFunction($funcName)) { + return $this->FunctionsReturningDatetime(); } else { $this->syntaxError('Known function.'); } @@ -1302,7 +1357,7 @@ class Parser * Checks whether the function with the given name is a string function * (a function that returns strings). */ - public function _isStringFunction($funcName) + public function isStringFunction($funcName) { return isset(self::$_STRING_FUNCTIONS[strtolower($funcName)]); } @@ -1311,7 +1366,7 @@ class Parser * Checks whether the function with the given name is a numeric function * (a function that returns numerics). */ - public function _isNumericFunction($funcName) + public function isNumericFunction($funcName) { return isset(self::$_NUMERIC_FUNCTIONS[strtolower($funcName)]); } @@ -1320,7 +1375,7 @@ class Parser * Checks whether the function with the given name is a datetime function * (a function that returns date/time values). */ - public function _isDatetimeFunction($funcName) + public function isDatetimeFunction($funcName) { return isset(self::$_DATETIME_FUNCTIONS[strtolower($funcName)]); } @@ -1336,46 +1391,46 @@ class Parser } $peek = $this->_lexer->peek(); $this->_lexer->resetPeek(); - + return $peek; } /** * Checks whether the given token is a comparison operator. */ - public function _isComparisonOperator($token) + public function isComparisonOperator($token) { $value = $token['value']; return $value == '=' || $value == '<' || $value == '<=' || $value == '<>' || - $value == '>' || $value == '>=' || $value == '!='; + $value == '>' || $value == '>=' || $value == '!='; } /** * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")" */ - public function _ArithmeticExpression() + public function ArithmeticExpression() { $expr = new AST\ArithmeticExpression; if ($this->_lexer->lookahead['value'] === '(') { $peek = $this->_lexer->glimpse(); if ($peek['type'] === Lexer::T_SELECT) { $this->match('('); - $expr->setSubselect($this->_Subselect()); + $expr->setSubselect($this->Subselect()); $this->match(')'); return $expr; } } - $expr->setSimpleArithmeticExpression($this->_SimpleArithmeticExpression()); + $expr->setSimpleArithmeticExpression($this->SimpleArithmeticExpression()); return $expr; } /** * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}* */ - public function _SimpleArithmeticExpression() + public function SimpleArithmeticExpression() { $terms = array(); - $terms[] = $this->_ArithmeticTerm(); + $terms[] = $this->ArithmeticTerm(); while ($this->_lexer->lookahead['value'] == '+' || $this->_lexer->lookahead['value'] == '-') { if ($this->_lexer->lookahead['value'] == '+') { $this->match('+'); @@ -1383,7 +1438,7 @@ class Parser $this->match('-'); } $terms[] = $this->_lexer->token['value']; - $terms[] = $this->_ArithmeticTerm(); + $terms[] = $this->ArithmeticTerm(); } return new AST\SimpleArithmeticExpression($terms); } @@ -1391,10 +1446,10 @@ class Parser /** * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}* */ - public function _ArithmeticTerm() + public function ArithmeticTerm() { $factors = array(); - $factors[] = $this->_ArithmeticFactor(); + $factors[] = $this->ArithmeticFactor(); while ($this->_lexer->lookahead['value'] == '*' || $this->_lexer->lookahead['value'] == '/') { if ($this->_lexer->lookahead['value'] == '*') { $this->match('*'); @@ -1402,7 +1457,7 @@ class Parser $this->match('/'); } $factors[] = $this->_lexer->token['value']; - $factors[] = $this->_ArithmeticFactor(); + $factors[] = $this->ArithmeticFactor(); } return new AST\ArithmeticTerm($factors); } @@ -1410,7 +1465,7 @@ class Parser /** * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary */ - public function _ArithmeticFactor() + public function ArithmeticFactor() { $pSign = $nSign = false; if ($this->_lexer->lookahead['value'] == '+') { @@ -1420,15 +1475,15 @@ class Parser $this->match('-'); $nSign = true; } - return new AST\ArithmeticFactor($this->_ArithmeticPrimary(), $pSign, $nSign); + return new AST\ArithmeticFactor($this->ArithmeticPrimary(), $pSign, $nSign); } /** * InExpression ::= StateFieldPathExpression ["NOT"] "IN" "(" (Literal {"," Literal}* | Subselect) ")" */ - public function _InExpression() + public function InExpression() { - $inExpression = new AST\InExpression($this->_StateFieldPathExpression()); + $inExpression = new AST\InExpression($this->StateFieldPathExpression()); if ($this->_lexer->isNextToken(Lexer::T_NOT)) { $this->match(Lexer::T_NOT); $inExpression->setNot(true); @@ -1436,13 +1491,13 @@ class Parser $this->match(Lexer::T_IN); $this->match('('); if ($this->_lexer->isNextToken(Lexer::T_SELECT)) { - $inExpression->setSubselect($this->_Subselect()); + $inExpression->setSubselect($this->Subselect()); } else { $literals = array(); - $literals[] = $this->_Literal(); + $literals[] = $this->Literal(); while ($this->_lexer->isNextToken(',')) { $this->match(','); - $literals[] = $this->_Literal(); + $literals[] = $this->Literal(); } $inExpression->setLiterals($literals); } @@ -1454,7 +1509,7 @@ class Parser /** * ExistsExpression ::= ["NOT"] "EXISTS" "(" Subselect ")" */ - public function _ExistsExpression() + public function ExistsExpression() { $not = false; if ($this->_lexer->isNextToken(Lexer::T_NOT)) { @@ -1463,17 +1518,17 @@ class Parser } $this->match(Lexer::T_EXISTS); $this->match('('); - $existsExpression = new AST\ExistsExpression($this->_Subselect()); + $existsExpression = new AST\ExistsExpression($this->Subselect()); $this->match(')'); $existsExpression->setNot($not); - + return $existsExpression; } /** * QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")" */ - public function _QuantifiedExpression() + public function QuantifiedExpression() { $all = $any = $some = false; if ($this->_lexer->isNextToken(Lexer::T_ALL)) { @@ -1489,39 +1544,39 @@ class Parser $this->syntaxError('ALL, ANY or SOME'); } $this->match('('); - $qExpr = new AST\QuantifiedExpression($this->_Subselect()); + $qExpr = new AST\QuantifiedExpression($this->Subselect()); $this->match(')'); $qExpr->setAll($all); $qExpr->setAny($any); $qExpr->setSome($some); - + return $qExpr; } /** * Subselect ::= SimpleSelectClause SubselectFromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] */ - public function _Subselect() + public function Subselect() { $this->_beginDeferredPathExpressionStack(); - $subselect = new AST\Subselect($this->_SimpleSelectClause(), $this->_SubselectFromClause()); + $subselect = new AST\Subselect($this->SimpleSelectClause(), $this->SubselectFromClause()); $this->_processDeferredPathExpressionStack(); $subselect->setWhereClause( - $this->_lexer->isNextToken(Lexer::T_WHERE) ? $this->_WhereClause() : null - ); + $this->_lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null + ); $subselect->setGroupByClause( - $this->_lexer->isNextToken(Lexer::T_GROUP) ? $this->_GroupByClause() : null - ); + $this->_lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null + ); $subselect->setHavingClause( - $this->_lexer->isNextToken(Lexer::T_HAVING) ? $this->_HavingClause() : null - ); + $this->_lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null + ); $subselect->setOrderByClause( - $this->_lexer->isNextToken(Lexer::T_ORDER) ? $this->_OrderByClause() : null - ); + $this->_lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null + ); return $subselect; } @@ -1529,7 +1584,7 @@ class Parser /** * SimpleSelectClause ::= "SELECT" ["DISTINCT"] SimpleSelectExpression */ - public function _SimpleSelectClause() + public function SimpleSelectClause() { $distinct = false; $this->match(Lexer::T_SELECT); @@ -1537,62 +1592,62 @@ class Parser $this->match(Lexer::T_DISTINCT); $distinct = true; } - $simpleSelectClause = new AST\SimpleSelectClause($this->_SimpleSelectExpression()); + $simpleSelectClause = new AST\SimpleSelectClause($this->SimpleSelectExpression()); $simpleSelectClause->setDistinct($distinct); - + return $simpleSelectClause; } /** * SubselectFromClause ::= "FROM" SubselectIdentificationVariableDeclaration {"," SubselectIdentificationVariableDeclaration}* */ - public function _SubselectFromClause() + public function SubselectFromClause() { $this->match(Lexer::T_FROM); $identificationVariables = array(); - $identificationVariables[] = $this->_SubselectIdentificationVariableDeclaration(); + $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration(); while ($this->_lexer->isNextToken(',')) { $this->match(','); - $identificationVariables[] = $this->_SubselectIdentificationVariableDeclaration(); + $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration(); } - + return new AST\SubselectFromClause($identificationVariables); } /** * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration | (AssociationPathExpression ["AS"] AliasIdentificationVariable) */ - public function _SubselectIdentificationVariableDeclaration() + public function SubselectIdentificationVariableDeclaration() { $peek = $this->_lexer->glimpse(); if ($peek['value'] == '.') { $subselectIdentificationVarDecl = new AST\SubselectIdentificationVariableDeclaration; - $subselectIdentificationVarDecl->setAssociationPathExpression($this->_AssociationPathExpression()); + $subselectIdentificationVarDecl->setAssociationPathExpression($this->AssociationPathExpression()); $this->match(Lexer::T_AS); $this->match(Lexer::T_IDENTIFIER); $subselectIdentificationVarDecl->setAliasIdentificationVariable($this->_lexer->token['value']); return $subselectIdentificationVarDecl; } else { - return $this->_IdentificationVariableDeclaration(); + return $this->IdentificationVariableDeclaration(); } } /** * SimpleSelectExpression ::= StateFieldPathExpression | IdentificationVariable | (AggregateExpression [["AS"] FieldAliasIdentificationVariable]) */ - public function _SimpleSelectExpression() + public function SimpleSelectExpression() { if ($this->_lexer->isNextToken(Lexer::T_IDENTIFIER)) { // SingleValuedPathExpression | IdentificationVariable $peek = $this->_lexer->glimpse(); if ($peek['value'] == '.') { - return new AST\SimpleSelectExpression($this->_StateFieldPathExpression()); + return new AST\SimpleSelectExpression($this->StateFieldPathExpression()); } else { $this->match($this->_lexer->lookahead['value']); return new AST\SimpleSelectExpression($this->_lexer->token['value']); } } else { - $expr = new AST\SimpleSelectExpression($this->_AggregateExpression()); + $expr = new AST\SimpleSelectExpression($this->AggregateExpression()); if ($this->_lexer->isNextToken(Lexer::T_AS)) { $this->match(Lexer::T_AS); } @@ -1606,8 +1661,10 @@ class Parser /** * Literal ::= string | char | integer | float | boolean | InputParameter + * + * @todo Rip out InputParameter. Thats not a literal. */ - public function _Literal() + public function Literal() { switch ($this->_lexer->lookahead['type']) { case Lexer::T_INPUT_PARAMETER: @@ -1626,18 +1683,18 @@ class Parser /** * BetweenExpression ::= ArithmeticExpression ["NOT"] "BETWEEN" ArithmeticExpression "AND" ArithmeticExpression */ - public function _BetweenExpression() + public function BetweenExpression() { $not = false; - $arithExpr1 = $this->_ArithmeticExpression(); + $arithExpr1 = $this->ArithmeticExpression(); if ($this->_lexer->isNextToken(Lexer::T_NOT)) { $this->match(Lexer::T_NOT); $not = true; } $this->match(Lexer::T_BETWEEN); - $arithExpr2 = $this->_ArithmeticExpression(); + $arithExpr2 = $this->ArithmeticExpression(); $this->match(Lexer::T_AND); - $arithExpr3 = $this->_ArithmeticExpression(); + $arithExpr3 = $this->ArithmeticExpression(); $betweenExpr = new AST\BetweenExpression($arithExpr1, $arithExpr2, $arithExpr3); $betweenExpr->setNot($not); @@ -1646,14 +1703,17 @@ class Parser } /** - * ArithmeticPrimary ::= StateFieldPathExpression | Literal | "(" SimpleArithmeticExpression ")" | Function | AggregateExpression - * @todo Implementation incomplete. + * ArithmeticPrimary ::= StateFieldPathExpression | Literal | "(" SimpleArithmeticExpression ")" + * | FunctionsReturningNumerics | AggregateExpression | FunctionsReturningStrings + * | FunctionsReturningDatetime | IdentificationVariable | SingleValuedAssociationPathExpression + * + * @todo IdentificationVariable | SingleValuedAssociationPathExpression */ - public function _ArithmeticPrimary() + public function ArithmeticPrimary() { if ($this->_lexer->lookahead['value'] === '(') { $this->match('('); - $expr = $this->_SimpleArithmeticExpression(); + $expr = $this->SimpleArithmeticExpression(); $this->match(')'); return $expr; } @@ -1662,9 +1722,15 @@ class Parser case Lexer::T_IDENTIFIER: $peek = $this->_lexer->glimpse(); if ($peek['value'] == '(') { - return $this->_FunctionsReturningNumerics(); + return $this->_Function(); } - return $this->_StateFieldPathExpression(); + if ($peek['value'] == '.') { + //TODO: SingleValuedAssociationPathExpression + return $this->StateFieldPathExpression(); + } + $identificationVariable = $this->_lexer->lookahead['value']; + $this->match($identificationVariable); + return $identificationVariable; case Lexer::T_INPUT_PARAMETER: $this->match($this->_lexer->lookahead['value']); return new AST\InputParameter($this->_lexer->token['value']); @@ -1676,16 +1742,14 @@ class Parser default: $peek = $this->_lexer->glimpse(); if ($peek['value'] == '(') { - if ($this->_isAggregateFunction($this->_lexer->lookahead['type'])) { - return $this->_AggregateExpression(); + if ($this->isAggregateFunction($this->_lexer->lookahead['type'])) { + return $this->AggregateExpression(); } - return $this->_FunctionsReturningStrings(); + return $this->FunctionsReturningStrings(); } else { $this->syntaxError(); } } - throw DoctrineException::updateMe("Not yet implemented2."); - //TODO... } /** @@ -1696,7 +1760,7 @@ class Parser * "LOWER" "(" StringPrimary ")" | * "UPPER" "(" StringPrimary ")" */ - public function _FunctionsReturningStrings() + public function FunctionsReturningStrings() { $funcNameLower = strtolower($this->_lexer->lookahead['value']); $funcClass = self::$_STRING_FUNCTIONS[$funcNameLower]; @@ -1714,7 +1778,7 @@ class Parser * "MOD" "(" SimpleArithmeticExpression "," SimpleArithmeticExpression ")" | * "SIZE" "(" CollectionValuedPathExpression ")" */ - public function _FunctionsReturningNumerics() + public function FunctionsReturningNumerics() { $funcNameLower = strtolower($this->_lexer->lookahead['value']); $funcClass = self::$_NUMERIC_FUNCTIONS[$funcNameLower]; @@ -1726,7 +1790,7 @@ class Parser /** * FunctionsReturningDateTime ::= "CURRENT_DATE" | "CURRENT_TIME" | "CURRENT_TIMESTAMP" */ - public function _FunctionsReturningDatetime() + public function FunctionsReturningDatetime() { $funcNameLower = strtolower($this->_lexer->lookahead['value']); $funcClass = self::$_DATETIME_FUNCTIONS[$funcNameLower]; @@ -1738,17 +1802,17 @@ class Parser /** * Checks whether the given token type indicates an aggregate function. */ - public function _isAggregateFunction($tokenType) + public function isAggregateFunction($tokenType) { return $tokenType == Lexer::T_AVG || $tokenType == Lexer::T_MIN || - $tokenType == Lexer::T_MAX || $tokenType == Lexer::T_SUM || - $tokenType == Lexer::T_COUNT; + $tokenType == Lexer::T_MAX || $tokenType == Lexer::T_SUM || + $tokenType == Lexer::T_COUNT; } /** * ComparisonOperator ::= "=" | "<" | "<=" | "<>" | ">" | ">=" | "!=" */ - public function _ComparisonOperator() + public function ComparisonOperator() { switch ($this->_lexer->lookahead['value']) { case '=': @@ -1786,9 +1850,9 @@ class Parser /** * LikeExpression ::= StringExpression ["NOT"] "LIKE" (string | input_parameter) ["ESCAPE" char] */ - public function _LikeExpression() + public function LikeExpression() { - $stringExpr = $this->_StringExpression(); + $stringExpr = $this->StringExpression(); $isNot = false; if ($this->_lexer->lookahead['type'] === Lexer::T_NOT) { $this->match(Lexer::T_NOT); @@ -1808,38 +1872,38 @@ class Parser $this->match(Lexer::T_STRING); $escapeChar = $this->_lexer->token['value']; } - + return new AST\LikeExpression($stringExpr, $stringPattern, $isNot, $escapeChar); } /** * StringExpression ::= StringPrimary | "(" Subselect ")" */ - public function _StringExpression() + public function StringExpression() { if ($this->_lexer->lookahead['value'] === '(') { $peek = $this->_lexer->glimpse(); if ($peek['type'] === Lexer::T_SELECT) { $this->match('('); - $expr = $this->_Subselect(); + $expr = $this->Subselect(); $this->match(')'); return $expr; } } - return $this->_StringPrimary(); + return $this->StringPrimary(); } /** * StringPrimary ::= StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression */ - public function _StringPrimary() + public function StringPrimary() { if ($this->_lexer->lookahead['type'] === Lexer::T_IDENTIFIER) { $peek = $this->_lexer->glimpse(); if ($peek['value'] == '.') { - return $this->_StateFieldPathExpression(); + return $this->StateFieldPathExpression(); } else if ($peek['value'] == '(') { - return $this->_FunctionsReturningStrings(); + return $this->FunctionsReturningStrings(); } else { $this->syntaxError("'.' or '('"); } @@ -1849,12 +1913,22 @@ class Parser } else if ($this->_lexer->lookahead['type'] === Lexer::T_INPUT_PARAMETER) { $this->match(Lexer::T_INPUT_PARAMETER); return new AST\InputParameter($this->_lexer->token['value']); - } else if ($this->_isAggregateFunction($this->_lexer->lookahead['type'])) { - return $this->_AggregateExpression(); + } else if ($this->isAggregateFunction($this->_lexer->lookahead['type'])) { + return $this->AggregateExpression(); } else { $this->syntaxError('StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression'); } } + + /** + * Sets the list of custom tree walkers. + * + * @param array $treeWalkers + */ + public function setSqlTreeWalker($treeWalker) + { + $this->_treeWalker = $treeWalker; + } /** * Registers a custom function that returns strings. diff --git a/lib/Doctrine/ORM/Query/ParserResult.php b/lib/Doctrine/ORM/Query/ParserResult.php index 223181a4e..22ac0d5ca 100644 --- a/lib/Doctrine/ORM/Query/ParserResult.php +++ b/lib/Doctrine/ORM/Query/ParserResult.php @@ -35,8 +35,12 @@ namespace Doctrine\ORM\Query; */ class ParserResult { + /** The SQL executor used for executing the SQL. */ private $_sqlExecutor; + /** The ResultSetMapping that describes how to map the SQL result set. */ private $_resultSetMapping; + /** The mappings of DQL parameter names/positions to SQL parameter positions. */ + private $_parameterMappings; /** * Initializes a new instance of the ParserResult class. @@ -87,4 +91,37 @@ class ParserResult { return $this->_sqlExecutor; } + + /** + * Adds a DQL to SQL parameter mapping. One DQL parameter name/position can map to + * several SQL parameter positions. + * + * @param string|integer $dqlPosition + * @param integer $sqlPosition + */ + public function addParameterMapping($dqlPosition, $sqlPosition) + { + $this->_parameterMappings[$dqlPosition][] = $sqlPosition; + } + + /** + * Gets all DQL to SQL parameter mappings. + * + * @return array The parameter mappings. + */ + public function getParameterMappings() + { + return $this->_parameterMappings; + } + + /** + * Gets the SQL parameter positions for a DQL parameter name/position. + * + * @param string|integer $dqlPosition The name or position of the DQL parameter. + * @return array The positions of the corresponding SQL parameters. + */ + public function getSqlParameterPositions($dqlPosition) + { + return $this->_parameterMappings[$dqlPosition]; + } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index e2bc3e737..e736b4caf 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -26,23 +26,31 @@ use Doctrine\ORM\Query\AST; use Doctrine\Common\DoctrineException; /** - * The SqlWalker walks over an AST that represents a DQL query and constructs - * the corresponding SQL. The walking can start at any node, not only at some root - * node. Therefore it is possible to only generate SQL parts by simply walking over - * certain subtrees of the AST. + * The SqlWalker is a TreeWalker that walks over a DQL AST and constructs + * the corresponding SQL. * * @author Roman Borschel * @since 2.0 + * @todo Code review for identifier quoting. */ -class SqlWalker +class SqlWalker implements TreeWalker { - private $_resultSetMapping; + /** The ResultSetMapping. */ + private $_rsm; + /** Counter for generating unique column aliases. */ private $_aliasCounter = 0; + /** Counter for generating unique table aliases. */ private $_tableAliasCounter = 0; private $_scalarResultCounter = 0; + /** Counter for SQL parameter positions. */ + private $_sqlParamIndex = 1; + /** The ParserResult. */ private $_parserResult; + /** The EntityManager. */ private $_em; + /** The Connection of the EntityManager. */ private $_conn; + /** The parsed Query instance. */ private $_query; private $_dqlToSqlAliasMap = array(); /** Map of all components/classes that appear in the DQL query. */ @@ -54,7 +62,10 @@ class SqlWalker * TODO: May need to be turned into a stack for usage in subqueries */ private $_currentRootAlias; - /** Flag that indicates whether to generate SQL table aliases in the SQL. */ + /** + * Flag that indicates whether to generate SQL table aliases in the SQL. + * These should only be generated for SELECT queries. + */ private $_useSqlTableAliases = true; /** @@ -65,7 +76,7 @@ class SqlWalker */ public function __construct($query, $parserResult, array $queryComponents) { - $this->_resultSetMapping = $parserResult->getResultSetMapping(); + $this->_rsm = $parserResult->getResultSetMapping(); $this->_query = $query; $this->_em = $query->getEntityManager(); $this->_conn = $this->_em->getConnection(); @@ -116,9 +127,9 @@ class SqlWalker foreach ($this->_selectedClasses as $dqlAlias => $class) { if ($this->_queryComponents[$dqlAlias]['relation'] === null) { - $this->_resultSetMapping->addEntityResult($class->name, $dqlAlias); + $this->_rsm->addEntityResult($class->name, $dqlAlias); } else { - $this->_resultSetMapping->addJoinedEntityResult( + $this->_rsm->addJoinedEntityResult( $class->name, $dqlAlias, $this->_queryComponents[$dqlAlias]['parent'], $this->_queryComponents[$dqlAlias]['relation'] @@ -132,7 +143,7 @@ class SqlWalker $discrColumn = $rootClass->discriminatorColumn; $columnAlias = $this->getSqlColumnAlias($discrColumn['name']); $sql .= ", $tblAlias." . $discrColumn['name'] . ' AS ' . $columnAlias; - $this->_resultSetMapping->setDiscriminatorColumn($dqlAlias, $columnAlias); + $this->_rsm->setDiscriminatorColumn($dqlAlias, $columnAlias); } //} } @@ -204,9 +215,10 @@ class SqlWalker //TODO: support general SingleValuedPathExpression, not just state field $pathExpr = $orderByItem->getStateFieldPathExpression(); $parts = $pathExpr->getParts(); - $qComp = $this->_queryComponents[$parts[0]]; - $columnName = $qComp['metadata']->getColumnName($parts[1]); - $sql = $this->getSqlTableAlias($qComp['metadata']->getTableName() . $parts[0]) . '.' . $columnName; + $dqlAlias = $pathExpr->getIdentificationVariable(); + $qComp = $this->_queryComponents[$dqlAlias]; + $columnName = $qComp['metadata']->getColumnName($parts[0]); + $sql = $this->getSqlTableAlias($qComp['metadata']->getTableName() . $dqlAlias) . '.' . $columnName; $sql .= $orderByItem->isAsc() ? ' ASC' : ' DESC'; return $sql; } @@ -219,7 +231,6 @@ class SqlWalker */ public function walkHavingClause($havingClause) { - // HavingClause ::= "HAVING" ConditionalExpression return ' HAVING ' . implode(' OR ', array_map(array($this, 'walkConditionalTerm'), $havingClause->getConditionalExpression()->getConditionalTerms())); } @@ -228,7 +239,7 @@ class SqlWalker * Walks down a JoinVariableDeclaration AST node and creates the corresponding SQL. * * @param JoinVariableDeclaration $joinVarDecl - * @return string + * @return string The SQL. */ public function walkJoinVariableDeclaration($joinVarDecl) { @@ -333,11 +344,11 @@ class SqlWalker { $sql = ''; $expr = $selectExpression->getExpression(); - if ($expr instanceof AST\StateFieldPathExpression) { - if ($expr->isSimpleStateFieldPathExpression()) { + if ($expr instanceof AST\PathExpression) { + if ($expr->getType() == AST\PathExpression::TYPE_STATE_FIELD) { $parts = $expr->getParts(); $numParts = count($parts); - $dqlAlias = $parts[0]; + $dqlAlias = $expr->getIdentificationVariable(); $fieldName = $parts[$numParts-1]; $qComp = $this->_queryComponents[$dqlAlias]; $class = $qComp['metadata']; @@ -346,21 +357,13 @@ class SqlWalker $this->_selectedClasses[$dqlAlias] = $class; } - if ($numParts > 2) { - for ($i = 1; $i < $numParts-1; ++$i) { - //TODO - } - } - $sqlTableAlias = $this->getSqlTableAlias($class->getTableName() . $dqlAlias); $columnName = $class->getColumnName($fieldName); $columnAlias = $this->getSqlColumnAlias($columnName); $sql .= $sqlTableAlias . '.' . $columnName . ' AS ' . $columnAlias; - $this->_resultSetMapping->addFieldResult($dqlAlias, $columnAlias, $fieldName); + $this->_rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName); - } else if ($expr->isSimpleStateFieldAssociationPathExpression()) { - throw DoctrineException::updateMe("Not yet implemented."); } else { throw DoctrineException::updateMe("Encountered invalid PathExpression during SQL construction."); } @@ -372,7 +375,7 @@ class SqlWalker } $columnAlias = 'sclr' . $this->_aliasCounter++; $sql .= $this->walkAggregateExpression($expr) . ' AS ' . $columnAlias; - $this->_resultSetMapping->addScalarResult($columnAlias, $resultAlias); + $this->_rsm->addScalarResult($columnAlias, $resultAlias); } else if ($expr instanceof AST\Subselect) { $sql .= $this->walkSubselect($expr); } else if ($expr instanceof AST\Functions\FunctionNode) { @@ -383,7 +386,7 @@ class SqlWalker } $columnAlias = 'sclr' . $this->_aliasCounter++; $sql .= $this->walkFunction($expr) . ' AS ' . $columnAlias; - $this->_resultSetMapping->addScalarResult($columnAlias, $resultAlias); + $this->_rsm->addScalarResult($columnAlias, $resultAlias); } else { // IdentificationVariable @@ -408,7 +411,7 @@ class SqlWalker $sqlTableAlias = $this->getSqlTableAlias($tableName . $dqlAlias); $columnAlias = $this->getSqlColumnAlias($mapping['columnName']); $sql .= $sqlTableAlias . '.' . $this->_conn->quoteIdentifier($mapping['columnName']) . ' AS ' . $columnAlias; - $this->_resultSetMapping->addFieldResult($dqlAlias, $columnAlias, $fieldName); + $this->_rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName); } // Add any additional fields of subclasses (not inherited fields) @@ -422,7 +425,7 @@ class SqlWalker $sqlTableAlias = $this->getSqlTableAlias($subClass->primaryTable['name'] . $dqlAlias); $columnAlias = $this->getSqlColumnAlias($mapping['columnName']); $sql .= $sqlTableAlias . '.' . $this->_conn->quoteIdentifier($mapping['columnName']) . ' AS ' . $columnAlias; - $this->_resultSetMapping->addFieldResult($dqlAlias, $columnAlias, $fieldName); + $this->_rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName); } } } else { @@ -438,7 +441,7 @@ class SqlWalker if ($beginning) $beginning = false; else $sql .= ', '; $columnAlias = $this->getSqlColumnAlias($mapping['columnName']); $sql .= $sqlTableAlias . '.' . $this->_conn->quoteIdentifier($mapping['columnName']) . ' AS ' . $columnAlias; - $this->_resultSetMapping->addFieldResult($dqlAlias, $columnAlias, $fieldName); + $this->_rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName); } } } @@ -530,7 +533,7 @@ class SqlWalker { $sql = ''; $expr = $simpleSelectExpression->getExpression(); - if ($expr instanceof AST\StateFieldPathExpression) { + if ($expr instanceof AST\PathExpression) { $sql .= ' ' . $this->walkPathExpression($expr); //... } else if ($expr instanceof AST\AggregateExpression) { @@ -561,8 +564,8 @@ class SqlWalker { $sql = ''; $parts = $aggExpression->getPathExpression()->getParts(); - $dqlAlias = $parts[0]; - $fieldName = $parts[1]; + $dqlAlias = $aggExpression->getPathExpression()->getIdentificationVariable(); + $fieldName = $parts[0]; $qComp = $this->_queryComponents[$dqlAlias]; $columnName = $qComp['metadata']->getColumnName($fieldName); @@ -593,13 +596,14 @@ class SqlWalker * @param GroupByItem * @return string The SQL. */ - public function walkGroupByItem($pathExpr) + public function walkGroupByItem(AST\PathExpression $pathExpr) { //TODO: support general SingleValuedPathExpression, not just state field $parts = $pathExpr->getParts(); - $qComp = $this->_queryComponents[$parts[0]]; - $columnName = $qComp['metadata']->getColumnName($parts[1]); - return $this->getSqlTableAlias($qComp['metadata']->getTableName() . $parts[0]) . '.' . $columnName; + $dqlAlias = $pathExpr->getIdentificationVariable(); + $qComp = $this->_queryComponents[$dqlAlias]; + $columnName = $qComp['metadata']->getColumnName($parts[0]); + return $this->getSqlTableAlias($qComp['metadata']->getTableName() . $dqlAlias) . '.' . $columnName; } /** @@ -704,7 +708,7 @@ class SqlWalker if (strcasecmp($newValue, 'NULL') === 0) { $sql .= 'NULL'; } else { - $sql .= $newValue; //TODO: quote() + $sql .= $this->_conn->quote($newValue); } } @@ -756,8 +760,6 @@ class SqlWalker $sql .= $this->getSqlTableAlias($class->getTableName() . $dqlAlias) . '.'; } $sql .= $discrColumn['name'] . ' IN (' . implode(', ', $values) . ')'; - } else if ($class->isInheritanceTypeJoined()) { - //TODO } } return $sql; @@ -808,6 +810,97 @@ class SqlWalker $sql .= 'EXISTS (' . $this->walkSubselect($existsExpr->getSubselect()) . ')'; return $sql; } + + /** + * Walks down a CollectionMemberExpression AST node, thereby generating the appropriate SQL. + * + * @param CollectionMemberExpression + * @return string The SQL. + */ + public function walkCollectionMemberExpression($collMemberExpr) + { + $sql = $collMemberExpr->isNot ? 'NOT ' : ''; + $sql .= 'EXISTS (SELECT 1 FROM '; + $entityExpr = $collMemberExpr->entityExpression; + $collPathExpr = $collMemberExpr->collectionValuedPathExpression; + $parts = $collPathExpr->getParts(); + $dqlAlias = $collPathExpr->getIdentificationVariable(); + + $class = $this->_queryComponents[$dqlAlias]['metadata']; + + if ($entityExpr instanceof AST\InputParameter) { + $dqlParamKey = $entityExpr->isNamed() ? $entityExpr->getName() : $entityExpr->getPosition(); + $entity = $this->_query->getParameter($dqlParamKey); + + } else { + throw DoctrineException::notImplemented(); + } + + $assoc = $class->associationMappings[$parts[0]]; + if ($assoc->isOneToMany()) { + $targetClass = $this->_em->getClassMetadata($assoc->targetEntityName); + $targetTableAlias = $this->getSqlTableAlias($targetClass->primaryTable['name']); + $sourceTableAlias = $this->getSqlTableAlias($class->primaryTable['name'] . $dqlAlias); + + $sql .= $this->_conn->quoteIdentifier($targetClass->primaryTable['name']) + . ' ' . $targetTableAlias . ' WHERE '; + + $owningAssoc = $targetClass->associationMappings[$assoc->mappedByFieldName]; + + $first = true; + foreach ($owningAssoc->targetToSourceKeyColumns as $targetColumn => $sourceColumn) { + if ($first) $first = false; else $sql .= ' AND '; + $sql .= $sourceTableAlias . '.' . $targetColumn . ' = ' . $targetTableAlias . '.' . $sourceColumn; + } + + $sql .= ' AND '; + $first = true; + foreach ($targetClass->identifier as $idField) { + if ($first) $first = false; else $sql .= ' AND '; + $this->_parserResult->addParameterMapping($dqlParamKey, $this->_sqlParamIndex++); + $sql .= $targetTableAlias . '.' . $targetClass->columnNames[$idField] . ' = ?'; + } + } else { // many-to-many + $targetClass = $this->_em->getClassMetadata($assoc->targetEntityName); + $sourceTableAlias = $this->getSqlTableAlias($class->primaryTable['name'] . $dqlAlias); + $joinTable = $assoc->isOwningSide ? $assoc->joinTable : $targetClass->associationMappings[$assoc->mappedByFieldName]->joinTable; + $joinTableAlias = $this->getSqlTableAlias($joinTable['name']); + $targetTableAlias = $this->getSqlTableAlias($targetClass->primaryTable['name']); + + // join to target table + $sql .= $this->_conn->quoteIdentifier($joinTable['name']) + . ' ' . $joinTableAlias . ' INNER JOIN ' + . $this->_conn->quoteIdentifier($targetClass->primaryTable['name']) + . ' ' . $targetTableAlias . ' ON '; + + // join conditions + $joinColumns = $assoc->isOwningSide ? $joinTable['joinColumns'] : $joinTable['inverseJoinColumns']; + $first = true; + foreach ($joinColumns as $joinColumn) { + if ($first) $first = false; else $sql .= ' AND '; + $sql .= $joinTableAlias . '.' . $joinColumn['name'] . ' = ' . $sourceTableAlias . '.' . $joinColumn['referencedColumnName']; + } + + $sql .= ' WHERE '; + + $joinColumns = $assoc->isOwningSide ? $joinTable['inverseJoinColumns'] : $joinTable['joinColumns']; + $first = true; + foreach ($joinColumns as $joinColumn) { + if ($first) $first = false; else $sql .= ' AND '; + $sql .= $joinTableAlias . '.' . $joinColumn['name'] . ' = ' . $targetTableAlias . '.' . $joinColumn['referencedColumnName']; + } + + $sql .= ' AND '; + $first = true; + foreach ($targetClass->identifier as $idField) { + if ($first) $first = false; else $sql .= ' AND '; + $this->_parserResult->addParameterMapping($dqlParamKey, $this->_sqlParamIndex++); + $sql .= $targetTableAlias . '.' . $targetClass->columnNames[$idField] . ' = ?'; + } + } + + return $sql . ')'; + } /** * Walks down a NullComparisonExpression AST node, thereby generating the appropriate SQL. @@ -820,7 +913,9 @@ class SqlWalker $sql = ''; $innerExpr = $nullCompExpr->getExpression(); if ($innerExpr instanceof AST\InputParameter) { - $sql .= ' ' . ($innerExpr->isNamed() ? ':' . $innerExpr->getName() : '?'); + $dqlParamKey = $innerExpr->isNamed() ? $innerExpr->getName() : $innerExpr->getPosition(); + $this->_parserResult->addParameterMapping($dqlParamKey, $this->_sqlParamIndex++); + $sql .= ' ?';// . ($innerExpr->isNamed() ? ':' . $innerExpr->getName() : '?'); } else { $sql .= $this->walkPathExpression($innerExpr); } @@ -857,7 +952,10 @@ class SqlWalker public function walkLiteral($literal) { if ($literal instanceof AST\InputParameter) { - return ($literal->isNamed() ? ':' . $literal->getName() : '?'); + $dqlParamKey = $literal->isNamed() ? $literal->getName() : $literal->getPosition(); + $this->_parserResult->addParameterMapping($dqlParamKey, $this->_sqlParamIndex++); + //return ($literal->isNamed() ? ':' . $literal->getName() : '?'); + return '?'; } else { return $literal; //TODO: quote() ? } @@ -896,12 +994,15 @@ class SqlWalker if ($likeExpr->getStringPattern() instanceof AST\InputParameter) { $inputParam = $likeExpr->getStringPattern(); - $sql .= $inputParam->isNamed() ? ':' . $inputParam->getName() : '?'; + $dqlParamKey = $inputParam->isNamed() ? $inputParam->getName() : $inputParam->getPosition(); + $this->_parserResult->addParameterMapping($dqlParamKey, $this->_sqlParamIndex++); + //$sql .= $inputParam->isNamed() ? ':' . $inputParam->getName() : '?'; + $sql .= '?'; } else { - $sql .= $likeExpr->getStringPattern(); + $sql .= $this->_conn->quote($likeExpr->getStringPattern()); } if ($likeExpr->getEscapeChar()) { - $sql .= ' ESCAPE ' . $likeExpr->getEscapeChar(); + $sql .= ' ESCAPE ' . $this->_conn->quote($likeExpr->getEscapeChar()); } return $sql; } @@ -954,7 +1055,9 @@ class SqlWalker */ public function walkInputParameter($inputParam) { - return $inputParam->isNamed() ? ':' . $inputParam->getName() : '?'; + $dqlParamKey = $inputParam->isNamed() ? $inputParam->getName() : $inputParam->getPosition(); + $this->_parserResult->addParameterMapping($dqlParamKey, $this->_sqlParamIndex++); + return '?'; } /** @@ -999,7 +1102,7 @@ class SqlWalker public function walkStringPrimary($stringPrimary) { if (is_string($stringPrimary)) { - return $stringPrimary; + return $this->_conn->quote($stringPrimary); } else { return $stringPrimary->dispatch($this); } @@ -1021,7 +1124,7 @@ class SqlWalker $sql .= $primary; //TODO: quote() ? } else if (is_string($primary)) { //TODO: quote string according to platform - $sql .= $primary; + $sql .= $this->_conn->quote($primary); } else if ($primary instanceof AST\SimpleArithmeticExpression) { $sql .= '(' . $this->walkSimpleArithmeticExpression($primary) . ')'; } else if ($primary instanceof AST\Node) { @@ -1052,10 +1155,10 @@ class SqlWalker public function walkPathExpression($pathExpr) { $sql = ''; - if ($pathExpr->isSimpleStateFieldPathExpression()) { + if ($pathExpr->getType() == AST\PathExpression::TYPE_STATE_FIELD) { $parts = $pathExpr->getParts(); $numParts = count($parts); - $dqlAlias = $parts[0]; + $dqlAlias = $pathExpr->getIdentificationVariable(); $fieldName = $parts[$numParts-1]; $qComp = $this->_queryComponents[$dqlAlias]; $class = $qComp['metadata']; diff --git a/lib/Doctrine/ORM/Query/TreeWalker.php b/lib/Doctrine/ORM/Query/TreeWalker.php new file mode 100644 index 000000000..3d2149c17 --- /dev/null +++ b/lib/Doctrine/ORM/Query/TreeWalker.php @@ -0,0 +1,336 @@ + + * @since 2.0 + */ +interface TreeWalker +{ + /** + * Walks down a SelectStatement AST node, thereby generating the appropriate SQL. + * + * @return string The SQL. + */ + function walkSelectStatement(AST\SelectStatement $AST); + + /** + * Walks down a SelectClause AST node, thereby generating the appropriate SQL. + * + * @return string The SQL. + */ + function walkSelectClause($selectClause); + + /** + * Walks down a FromClause AST node, thereby generating the appropriate SQL. + * + * @return string The SQL. + */ + function walkFromClause($fromClause); + + /** + * Walks down a FunctionNode AST node, thereby generating the appropriate SQL. + * + * @return string The SQL. + */ + function walkFunction($function); + + /** + * Walks down an OrderByClause AST node, thereby generating the appropriate SQL. + * + * @param OrderByClause + * @return string The SQL. + */ + function walkOrderByClause($orderByClause); + + /** + * Walks down an OrderByItem AST node, thereby generating the appropriate SQL. + * + * @param OrderByItem + * @return string The SQL. + */ + function walkOrderByItem($orderByItem); + + /** + * Walks down a HavingClause AST node, thereby generating the appropriate SQL. + * + * @param HavingClause + * @return string The SQL. + */ + function walkHavingClause($havingClause); + + /** + * Walks down a JoinVariableDeclaration AST node and creates the corresponding SQL. + * + * @param JoinVariableDeclaration $joinVarDecl + * @return string The SQL. + */ + function walkJoinVariableDeclaration($joinVarDecl); + + /** + * Walks down a SelectExpression AST node and generates the corresponding SQL. + * + * @param SelectExpression $selectExpression + * @return string The SQL. + */ + function walkSelectExpression($selectExpression); + + /** + * Walks down a QuantifiedExpression AST node, thereby generating the appropriate SQL. + * + * @param QuantifiedExpression + * @return string The SQL. + */ + function walkQuantifiedExpression($qExpr); + + /** + * Walks down a Subselect AST node, thereby generating the appropriate SQL. + * + * @param Subselect + * @return string The SQL. + */ + function walkSubselect($subselect); + + /** + * Walks down a SubselectFromClause AST node, thereby generating the appropriate SQL. + * + * @param SubselectFromClause + * @return string The SQL. + */ + function walkSubselectFromClause($subselectFromClause); + + /** + * Walks down a SimpleSelectClause AST node, thereby generating the appropriate SQL. + * + * @param SimpleSelectClause + * @return string The SQL. + */ + function walkSimpleSelectClause($simpleSelectClause); + + /** + * Walks down a SimpleSelectExpression AST node, thereby generating the appropriate SQL. + * + * @param SimpleSelectExpression + * @return string The SQL. + */ + function walkSimpleSelectExpression($simpleSelectExpression); + + /** + * Walks down an AggregateExpression AST node, thereby generating the appropriate SQL. + * + * @param AggregateExpression + * @return string The SQL. + */ + function walkAggregateExpression($aggExpression); + + /** + * Walks down a GroupByClause AST node, thereby generating the appropriate SQL. + * + * @param GroupByClause + * @return string The SQL. + */ + function walkGroupByClause($groupByClause); + + /** + * Walks down a GroupByItem AST node, thereby generating the appropriate SQL. + * + * @param GroupByItem + * @return string The SQL. + */ + function walkGroupByItem(AST\PathExpression $pathExpr); + + /** + * Walks down an UpdateStatement AST node, thereby generating the appropriate SQL. + * + * @param UpdateStatement + * @return string The SQL. + */ + function walkUpdateStatement(AST\UpdateStatement $AST); + + /** + * Walks down a DeleteStatement AST node, thereby generating the appropriate SQL. + * + * @param DeleteStatement + * @return string The SQL. + */ + function walkDeleteStatement(AST\DeleteStatement $AST); + + /** + * Walks down a DeleteClause AST node, thereby generating the appropriate SQL. + * + * @param DeleteClause + * @return string The SQL. + */ + function walkDeleteClause(AST\DeleteClause $deleteClause); + + /** + * Walks down an UpdateClause AST node, thereby generating the appropriate SQL. + * + * @param UpdateClause + * @return string The SQL. + */ + function walkUpdateClause($updateClause); + + /** + * Walks down an UpdateItem AST node, thereby generating the appropriate SQL. + * + * @param UpdateItem + * @return string The SQL. + */ + function walkUpdateItem($updateItem); + + /** + * Walks down a WhereClause AST node, thereby generating the appropriate SQL. + * + * @param WhereClause + * @return string The SQL. + */ + function walkWhereClause($whereClause); + + /** + * Walks down a ConditionalTerm AST node, thereby generating the appropriate SQL. + * + * @param ConditionalTerm + * @return string The SQL. + */ + function walkConditionalTerm($condTerm); + + /** + * Walks down a ConditionalFactor AST node, thereby generating the appropriate SQL. + * + * @param ConditionalFactor + * @return string The SQL. + */ + function walkConditionalFactor($factor); + + /** + * Walks down an ExistsExpression AST node, thereby generating the appropriate SQL. + * + * @param ExistsExpression + * @return string The SQL. + */ + function walkExistsExpression($existsExpr); + + /** + * Walks down a CollectionMemberExpression AST node, thereby generating the appropriate SQL. + * + * @param CollectionMemberExpression + * @return string The SQL. + */ + function walkCollectionMemberExpression($collMemberExpr); + + /** + * Walks down a NullComparisonExpression AST node, thereby generating the appropriate SQL. + * + * @param NullComparisonExpression + * @return string The SQL. + */ + function walkNullComparisonExpression($nullCompExpr); + + /** + * Walks down an InExpression AST node, thereby generating the appropriate SQL. + * + * @param InExpression + * @return string The SQL. + */ + function walkInExpression($inExpr); + + /** + * Walks down a literal that represents an AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + function walkLiteral($literal); + + /** + * Walks down a BetweenExpression AST node, thereby generating the appropriate SQL. + * + * @param BetweenExpression + * @return string The SQL. + */ + function walkBetweenExpression($betweenExpr); + + /** + * Walks down a LikeExpression AST node, thereby generating the appropriate SQL. + * + * @param LikeExpression + * @return string The SQL. + */ + function walkLikeExpression($likeExpr); + + /** + * Walks down a StateFieldPathExpression AST node, thereby generating the appropriate SQL. + * + * @param StateFieldPathExpression + * @return string The SQL. + */ + function walkStateFieldPathExpression($stateFieldPathExpression); + + /** + * Walks down a ComparisonExpression AST node, thereby generating the appropriate SQL. + * + * @param ComparisonExpression + * @return string The SQL. + */ + function walkComparisonExpression($compExpr); + + /** + * Walks down an InputParameter AST node, thereby generating the appropriate SQL. + * + * @param InputParameter + * @return string The SQL. + */ + function walkInputParameter($inputParam); + + /** + * Walks down an ArithmeticExpression AST node, thereby generating the appropriate SQL. + * + * @param ArithmeticExpression + * @return string The SQL. + */ + function walkArithmeticExpression($arithmeticExpr); + + /** + * Walks down an ArithmeticTerm AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + function walkArithmeticTerm($term); + + /** + * Walks down a StringPrimary that represents an AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + function walkStringPrimary($stringPrimary); + + /** + * Walks down an ArithmeticFactor that represents an AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + function walkArithmeticFactor($factor); + + /** + * Walks down an SimpleArithmeticExpression AST node, thereby generating the appropriate SQL. + * + * @param SimpleArithmeticExpression + * @return string The SQL. + */ + function walkSimpleArithmeticExpression($simpleArithmeticExpr); + + /** + * Walks down an PathExpression AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + function walkPathExpression($pathExpr); +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Query/TreeWalkerAdapter.php b/lib/Doctrine/ORM/Query/TreeWalkerAdapter.php new file mode 100644 index 000000000..2f0df4af7 --- /dev/null +++ b/lib/Doctrine/ORM/Query/TreeWalkerAdapter.php @@ -0,0 +1,337 @@ + + * @since 2.0 + */ +abstract class TreeWalkerAdapter implements TreeWalker +{ + /** + * Walks down a SelectStatement AST node, thereby generating the appropriate SQL. + * + * @return string The SQL. + */ + public function walkSelectStatement(AST\SelectStatement $AST) {} + + /** + * Walks down a SelectClause AST node, thereby generating the appropriate SQL. + * + * @return string The SQL. + */ + public function walkSelectClause($selectClause) {} + + /** + * Walks down a FromClause AST node, thereby generating the appropriate SQL. + * + * @return string The SQL. + */ + public function walkFromClause($fromClause) {} + + /** + * Walks down a FunctionNode AST node, thereby generating the appropriate SQL. + * + * @return string The SQL. + */ + public function walkFunction($function) {} + + /** + * Walks down an OrderByClause AST node, thereby generating the appropriate SQL. + * + * @param OrderByClause + * @return string The SQL. + */ + public function walkOrderByClause($orderByClause) {} + + /** + * Walks down an OrderByItem AST node, thereby generating the appropriate SQL. + * + * @param OrderByItem + * @return string The SQL. + */ + public function walkOrderByItem($orderByItem) {} + + /** + * Walks down a HavingClause AST node, thereby generating the appropriate SQL. + * + * @param HavingClause + * @return string The SQL. + */ + public function walkHavingClause($havingClause) {} + + /** + * Walks down a JoinVariableDeclaration AST node and creates the corresponding SQL. + * + * @param JoinVariableDeclaration $joinVarDecl + * @return string The SQL. + */ + public function walkJoinVariableDeclaration($joinVarDecl) {} + + /** + * Walks down a SelectExpression AST node and generates the corresponding SQL. + * + * @param SelectExpression $selectExpression + * @return string The SQL. + */ + public function walkSelectExpression($selectExpression) {} + + /** + * Walks down a QuantifiedExpression AST node, thereby generating the appropriate SQL. + * + * @param QuantifiedExpression + * @return string The SQL. + */ + public function walkQuantifiedExpression($qExpr) {} + + /** + * Walks down a Subselect AST node, thereby generating the appropriate SQL. + * + * @param Subselect + * @return string The SQL. + */ + public function walkSubselect($subselect) {} + + /** + * Walks down a SubselectFromClause AST node, thereby generating the appropriate SQL. + * + * @param SubselectFromClause + * @return string The SQL. + */ + public function walkSubselectFromClause($subselectFromClause) {} + + /** + * Walks down a SimpleSelectClause AST node, thereby generating the appropriate SQL. + * + * @param SimpleSelectClause + * @return string The SQL. + */ + public function walkSimpleSelectClause($simpleSelectClause) {} + + /** + * Walks down a SimpleSelectExpression AST node, thereby generating the appropriate SQL. + * + * @param SimpleSelectExpression + * @return string The SQL. + */ + public function walkSimpleSelectExpression($simpleSelectExpression) {} + + /** + * Walks down an AggregateExpression AST node, thereby generating the appropriate SQL. + * + * @param AggregateExpression + * @return string The SQL. + */ + public function walkAggregateExpression($aggExpression) {} + + /** + * Walks down a GroupByClause AST node, thereby generating the appropriate SQL. + * + * @param GroupByClause + * @return string The SQL. + */ + public function walkGroupByClause($groupByClause) {} + + /** + * Walks down a GroupByItem AST node, thereby generating the appropriate SQL. + * + * @param GroupByItem + * @return string The SQL. + */ + public function walkGroupByItem(AST\PathExpression $pathExpr) {} + + /** + * Walks down an UpdateStatement AST node, thereby generating the appropriate SQL. + * + * @param UpdateStatement + * @return string The SQL. + */ + public function walkUpdateStatement(AST\UpdateStatement $AST) {} + + /** + * Walks down a DeleteStatement AST node, thereby generating the appropriate SQL. + * + * @param DeleteStatement + * @return string The SQL. + */ + public function walkDeleteStatement(AST\DeleteStatement $AST) {} + + /** + * Walks down a DeleteClause AST node, thereby generating the appropriate SQL. + * + * @param DeleteClause + * @return string The SQL. + */ + public function walkDeleteClause(AST\DeleteClause $deleteClause) {} + + /** + * Walks down an UpdateClause AST node, thereby generating the appropriate SQL. + * + * @param UpdateClause + * @return string The SQL. + */ + public function walkUpdateClause($updateClause) {} + + /** + * Walks down an UpdateItem AST node, thereby generating the appropriate SQL. + * + * @param UpdateItem + * @return string The SQL. + */ + public function walkUpdateItem($updateItem) {} + + /** + * Walks down a WhereClause AST node, thereby generating the appropriate SQL. + * + * @param WhereClause + * @return string The SQL. + */ + public function walkWhereClause($whereClause) {} + + /** + * Walks down a ConditionalTerm AST node, thereby generating the appropriate SQL. + * + * @param ConditionalTerm + * @return string The SQL. + */ + public function walkConditionalTerm($condTerm) {} + + /** + * Walks down a ConditionalFactor AST node, thereby generating the appropriate SQL. + * + * @param ConditionalFactor + * @return string The SQL. + */ + public function walkConditionalFactor($factor) {} + + /** + * Walks down an ExistsExpression AST node, thereby generating the appropriate SQL. + * + * @param ExistsExpression + * @return string The SQL. + */ + public function walkExistsExpression($existsExpr) {} + + /** + * Walks down a CollectionMemberExpression AST node, thereby generating the appropriate SQL. + * + * @param CollectionMemberExpression + * @return string The SQL. + */ + public function walkCollectionMemberExpression($collMemberExpr) {} + + /** + * Walks down a NullComparisonExpression AST node, thereby generating the appropriate SQL. + * + * @param NullComparisonExpression + * @return string The SQL. + */ + public function walkNullComparisonExpression($nullCompExpr) {} + + /** + * Walks down an InExpression AST node, thereby generating the appropriate SQL. + * + * @param InExpression + * @return string The SQL. + */ + public function walkInExpression($inExpr) {} + + /** + * Walks down a literal that represents an AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + public function walkLiteral($literal) {} + + /** + * Walks down a BetweenExpression AST node, thereby generating the appropriate SQL. + * + * @param BetweenExpression + * @return string The SQL. + */ + public function walkBetweenExpression($betweenExpr) {} + + /** + * Walks down a LikeExpression AST node, thereby generating the appropriate SQL. + * + * @param LikeExpression + * @return string The SQL. + */ + public function walkLikeExpression($likeExpr) {} + + /** + * Walks down a StateFieldPathExpression AST node, thereby generating the appropriate SQL. + * + * @param StateFieldPathExpression + * @return string The SQL. + */ + public function walkStateFieldPathExpression($stateFieldPathExpression) {} + + /** + * Walks down a ComparisonExpression AST node, thereby generating the appropriate SQL. + * + * @param ComparisonExpression + * @return string The SQL. + */ + public function walkComparisonExpression($compExpr) {} + + /** + * Walks down an InputParameter AST node, thereby generating the appropriate SQL. + * + * @param InputParameter + * @return string The SQL. + */ + public function walkInputParameter($inputParam) {} + + /** + * Walks down an ArithmeticExpression AST node, thereby generating the appropriate SQL. + * + * @param ArithmeticExpression + * @return string The SQL. + */ + public function walkArithmeticExpression($arithmeticExpr) {} + + /** + * Walks down an ArithmeticTerm AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + public function walkArithmeticTerm($term) {} + + /** + * Walks down a StringPrimary that represents an AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + public function walkStringPrimary($stringPrimary) {} + + /** + * Walks down an ArithmeticFactor that represents an AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + public function walkArithmeticFactor($factor) {} + + /** + * Walks down an SimpleArithmeticExpression AST node, thereby generating the appropriate SQL. + * + * @param SimpleArithmeticExpression + * @return string The SQL. + */ + public function walkSimpleArithmeticExpression($simpleArithmeticExpr) {} + + /** + * Walks down an PathExpression AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + public function walkPathExpression($pathExpr) {} +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Tools/SchemaTool.php b/lib/Doctrine/ORM/Tools/SchemaTool.php index 9eb69ae66..510e8459a 100644 --- a/lib/Doctrine/ORM/Tools/SchemaTool.php +++ b/lib/Doctrine/ORM/Tools/SchemaTool.php @@ -224,6 +224,7 @@ class SchemaTool if (isset($class->inheritedAssociationFields[$fieldName])) { continue; } + $foreignClass = $this->_em->getClassMetadata($mapping->targetEntityName); if ($mapping->isOneToOne() && $mapping->isOwningSide) { $constraint = array(); diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index d56850dd2..c41f928a5 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -403,12 +403,14 @@ class UnitOfWork implements PropertyChangedListener && $actualData[$name] !== null && ! ($actualData[$name] instanceof PersistentCollection) ) { - //TODO: If $actualData[$name] is Collection then unwrap the array + // If $actualData[$name] is Collection then unwrap the array + if ($actualData[$name] instanceof Collection) { + $actualData[$name] = $actualData[$name]->unwrap(); + } $assoc = $class->associationMappings[$name]; - //echo PHP_EOL . "INJECTING PCOLL into $name" . PHP_EOL; // Inject PersistentCollection $coll = new PersistentCollection($this->_em, $this->_em->getClassMetadata($assoc->targetEntityName), - $actualData[$name] ? $actualData[$name] : array()); + $actualData[$name] ? $actualData[$name] : array()); $coll->setOwner($entity, $assoc); if ( ! $coll->isEmpty()) { $coll->setDirty(true); diff --git a/tests/Doctrine/Tests/Common/Collections/CollectionTest.php b/tests/Doctrine/Tests/Common/Collections/CollectionTest.php index 6f4fd8694..0af97e964 100644 --- a/tests/Doctrine/Tests/Common/Collections/CollectionTest.php +++ b/tests/Doctrine/Tests/Common/Collections/CollectionTest.php @@ -19,9 +19,9 @@ class CollectionTest extends \Doctrine\Tests\DoctrineTestCase { $this->_coll->add("one"); $this->_coll->add("two"); - $exists = $this->_coll->exists(function($key, $element) { return $element == "one"; }); + $exists = $this->_coll->exists(function($k, $e) { return $e == "one"; }); $this->assertTrue($exists); - $exists = $this->_coll->exists(function($key, $element) { return $element == "other"; }); + $exists = $this->_coll->exists(function($k, $e) { return $e == "other"; }); $this->assertFalse($exists); } @@ -29,7 +29,7 @@ class CollectionTest extends \Doctrine\Tests\DoctrineTestCase { $this->_coll->add(1); $this->_coll->add(2); - $res = $this->_coll->map(function ($e) { return $e * 2; }); + $res = $this->_coll->map(function($e) { return $e * 2; }); $this->assertEquals(array(2, 4), $res->unwrap()); } @@ -38,7 +38,7 @@ class CollectionTest extends \Doctrine\Tests\DoctrineTestCase $this->_coll->add(1); $this->_coll->add("foo"); $this->_coll->add(3); - $res = $this->_coll->filter(function ($e) { return is_numeric($e); }); + $res = $this->_coll->filter(function($e) { return is_numeric($e); }); $this->assertEquals(array(0 => 1, 2 => 3), $res->unwrap()); } @@ -66,39 +66,39 @@ class CollectionTest extends \Doctrine\Tests\DoctrineTestCase public function testContainsKey() { $this->_coll[5] = 'five'; - $this->assertEquals($this->_coll->containsKey(5), true); + $this->assertTrue($this->_coll->containsKey(5)); } public function testContains() { $this->_coll[0] = 'test'; - $this->assertEquals($this->_coll->contains('test'), true); + $this->assertTrue($this->_coll->contains('test')); } public function testSearch() { $this->_coll[0] = 'test'; - $this->assertEquals($this->_coll->search('test'), 0); + $this->assertEquals(0, $this->_coll->search('test')); } public function testGet() { $this->_coll[0] = 'test'; - $this->assertEquals($this->_coll->get(0), 'test'); + $this->assertEquals('test', $this->_coll->get(0)); } public function testGetKeys() { $this->_coll[] = 'one'; $this->_coll[] = 'two'; - $this->assertEquals($this->_coll->getKeys(), array(0, 1)); + $this->assertEquals(array(0, 1), $this->_coll->getKeys()); } public function testGetElements() { $this->_coll[] = 'one'; $this->_coll[] = 'two'; - $this->assertEquals($this->_coll->getElements(), array('one', 'two')); + $this->assertEquals(array('one', 'two'), $this->_coll->getElements()); } public function testCount() @@ -113,15 +113,15 @@ class CollectionTest extends \Doctrine\Tests\DoctrineTestCase { $this->_coll[] = 'one'; $this->_coll[] = 'two'; - $this->assertEquals($this->_coll->forAll(function($key, $element) { return is_string($element); }), true); - $this->assertEquals($this->_coll->forAll(function($key, $element) { return is_array($element); }), false); + $this->assertEquals($this->_coll->forAll(function($k, $e) { return is_string($e); }), true); + $this->assertEquals($this->_coll->forAll(function($k, $e) { return is_array($e); }), false); } public function testPartition() { $this->_coll[] = true; $this->_coll[] = false; - $partition = $this->_coll->partition(function($key, $element) { return $element == true; }); + $partition = $this->_coll->partition(function($k, $e) { return $e == true; }); $this->assertEquals($partition[0][0], true); $this->assertEquals($partition[1][0], false); } diff --git a/tests/Doctrine/Tests/Mocks/ConnectionMock.php b/tests/Doctrine/Tests/Mocks/ConnectionMock.php index 58f5f7cd0..87e3d7b7d 100644 --- a/tests/Doctrine/Tests/Mocks/ConnectionMock.php +++ b/tests/Doctrine/Tests/Mocks/ConnectionMock.php @@ -53,7 +53,7 @@ class ConnectionMock extends \Doctrine\DBAL\Connection */ public function quote($input, $type = null) { - if ($type === 'string') { + if (is_string($input)) { return "'" . $input . "'"; } return $input; diff --git a/tests/Doctrine/Tests/Mocks/MockTreeWalker.php b/tests/Doctrine/Tests/Mocks/MockTreeWalker.php new file mode 100644 index 000000000..ee2f532d6 --- /dev/null +++ b/tests/Doctrine/Tests/Mocks/MockTreeWalker.php @@ -0,0 +1,9 @@ +id; @@ -47,6 +55,20 @@ class CompanyPerson public function getSpouse() { return $this->spouse; } + + public function getFriends() { + return $this->friends; + } + + public function addFriend(CompanyPerson $friend) { + if ( ! $this->friends) { + $this->friends = new \Doctrine\Common\Collections\Collection; + } + if ( ! $this->friends->contains($friend)) { + $this->friends->add($friend); + $friend->addFriend($this); + } + } public function setSpouse(CompanyPerson $spouse) { if ($spouse !== $this->spouse) { diff --git a/tests/Doctrine/Tests/ORM/Functional/ClassTableInheritanceTest.php b/tests/Doctrine/Tests/ORM/Functional/ClassTableInheritanceTest.php index 2bd573b98..77aaf34a1 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ClassTableInheritanceTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ClassTableInheritanceTest.php @@ -65,28 +65,6 @@ class ClassTableInheritanceTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals(100000, $entities[0]->getSalary()); $this->_em->clear(); - /* - $query = $this->_em->createQuery("select r,o from Doctrine\Tests\ORM\Functional\RelatedEntity r join r.owner o"); - - $entities = $query->getResultList(); - $this->assertEquals(1, count($entities)); - $this->assertTrue($entities[0] instanceof RelatedEntity); - $this->assertTrue(is_numeric($entities[0]->getId())); - $this->assertEquals('theRelatedOne', $entities[0]->getName()); - $this->assertTrue($entities[0]->getOwner() instanceof ChildEntity); - $this->assertEquals('thedata', $entities[0]->getOwner()->getData()); - $this->assertSame($entities[0], $entities[0]->getOwner()->getRelatedEntity()); - - $query = $this->_em->createQuery("update Doctrine\Tests\ORM\Functional\ChildEntity e set e.data = 'newdata'"); - - $affected = $query->execute(); - $this->assertEquals(1, $affected); - - $query = $this->_em->createQuery("delete Doctrine\Tests\ORM\Functional\ParentEntity e"); - - $affected = $query->execute(); - $this->assertEquals(2, $affected); - */ } public function testMultiLevelUpdateAndFind() { @@ -149,4 +127,37 @@ class ClassTableInheritanceTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals('John Smith', $result[0]->getSpouse()->getName()); $this->assertSame($result[0], $result[0]->getSpouse()->getSpouse()); } + + public function testSelfReferencingManyToMany() + { + $person1 = new CompanyPerson; + $person1->setName('Roman'); + + $person2 = new CompanyPerson; + $person2->setName('Jonathan'); + + $person1->addFriend($person2); + + $this->assertEquals(1, count($person1->getFriends())); + $this->assertEquals(1, count($person2->getFriends())); + + + $this->_em->save($person1); + $this->_em->save($person2); + + $this->_em->flush(); + + $this->_em->clear(); + + $query = $this->_em->createQuery('select p, f from Doctrine\Tests\Models\Company\CompanyPerson p join p.friends f where p.name=?1'); + $query->setParameter(1, 'Roman'); + + $result = $query->getResultList(); + $this->assertEquals(1, count($result)); + $this->assertEquals(1, count($result[0]->getFriends())); + $this->assertEquals('Roman', $result[0]->getName()); + + $friends = $result[0]->getFriends(); + $this->assertEquals('Jonathan', $friends[0]->getName()); + } } diff --git a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php index 8f0ff7c9d..bdfa99171 100644 --- a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php +++ b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php @@ -16,7 +16,9 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase { try { $query = $this->_em->createQuery($dql); - $parserResult = $query->parse(); + $parser = new \Doctrine\ORM\Query\Parser($query); + $parser->setSqlTreeWalker(new \Doctrine\Tests\Mocks\MockTreeWalker); + $parserResult = $parser->parse(); } catch (\Exception $e) { if ($debug) { echo $e->getTraceAsString() . PHP_EOL; @@ -30,15 +32,15 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase try { $query = $this->_em->createQuery($dql); $query->setDql($dql); - $parserResult = $query->parse(); + $parser = new \Doctrine\ORM\Query\Parser($query); + $parser->setSqlTreeWalker(new \Doctrine\Tests\Mocks\MockTreeWalker); + $parserResult = $parser->parse(); $this->fail('No syntax errors were detected, when syntax errors were expected'); } catch (\Exception $e) { - //echo $e->getMessage() . PHP_EOL; if ($debug) { echo $e->getMessage() . PHP_EOL; echo $e->getTraceAsString() . PHP_EOL; } - // It was expected! } } @@ -319,4 +321,9 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase { $this->assertValidDql('SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id > SOME (SELECT u2.id FROM Doctrine\Tests\Models\CMS\CmsUser u2 WHERE u2.name = u.name)'); } + + public function testMemberOfExpression() + { + $this->assertValidDql('SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE :param MEMBER OF u.phonenumbers'); + } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Query/LexerTest.php b/tests/Doctrine/Tests/ORM/Query/LexerTest.php index f2ef583b4..073124c1e 100644 --- a/tests/Doctrine/Tests/ORM/Query/LexerTest.php +++ b/tests/Doctrine/Tests/ORM/Query/LexerTest.php @@ -182,7 +182,7 @@ class LexerTest extends \Doctrine\Tests\OrmTestCase $lexer->moveNext(); $token = $lexer->lookahead; $this->assertEquals(Lexer::T_STRING, $token['type']); - $this->assertEquals("'This is a string.'", $token['value']); + $this->assertEquals("This is a string.", $token['value']); } public function testScannerRecognizesStringContainingSingleQuotes() @@ -191,7 +191,7 @@ class LexerTest extends \Doctrine\Tests\OrmTestCase $lexer->moveNext(); $token = $lexer->lookahead; $this->assertEquals(Lexer::T_STRING, $token['type']); - $this->assertEquals("'abc''defg'''", $token['value']); + $this->assertEquals("abc'defg'", $token['value']); } public function testScannerRecognizesInputParameter() @@ -269,7 +269,7 @@ class LexerTest extends \Doctrine\Tests\OrmTestCase 'position' => 47 ), array( - 'value' => "'Jack O''Neil'", + 'value' => "Jack O'Neil", 'type' => Lexer::T_STRING, 'position' => 49 ) diff --git a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php index cdc5415da..3cc4815a0 100644 --- a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php +++ b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php @@ -21,7 +21,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase $query->free(); } catch (Doctrine_Exception $e) { echo $e->getMessage(); - echo $e->getTraceAsString(); die(); + echo $e->getTraceAsString(); $this->fail($e->getMessage()); } } @@ -91,7 +91,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase { $this->assertSqlGeneration( 'select u from Doctrine\Tests\Models\Forum\ForumUser u where u.username = :name', - 'SELECT f0_.id AS id0, f0_.username AS username1 FROM forum_users f0_ WHERE f0_.username = :name' + 'SELECT f0_.id AS id0, f0_.username AS username1 FROM forum_users f0_ WHERE f0_.username = ?' ); } @@ -99,7 +99,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase { $this->assertSqlGeneration( 'select u from Doctrine\Tests\Models\Forum\ForumUser u where u.username = :name and u.username = :name2', - 'SELECT f0_.id AS id0, f0_.username AS username1 FROM forum_users f0_ WHERE f0_.username = :name AND f0_.username = :name2' + 'SELECT f0_.id AS id0, f0_.username AS username1 FROM forum_users f0_ WHERE f0_.username = ? AND f0_.username = ?' ); } @@ -107,7 +107,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase { $this->assertSqlGeneration( 'select u from Doctrine\Tests\Models\Forum\ForumUser u where (u.username = :name OR u.username = :name2) AND u.id = :id', - 'SELECT f0_.id AS id0, f0_.username AS username1 FROM forum_users f0_ WHERE (f0_.username = :name OR f0_.username = :name2) AND f0_.id = :id' + 'SELECT f0_.id AS id0, f0_.username AS username1 FROM forum_users f0_ WHERE (f0_.username = ? OR f0_.username = ?) AND f0_.id = ?' ); } @@ -243,8 +243,45 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase 'SELECT c0_.id AS id0 FROM cms_users c0_ WHERE EXISTS (SELECT c1_.phonenumber FROM cms_phonenumbers c1_ WHERE c1_.phonenumber = c0_.id)' ); } + + public function testMemberOfExpression() + { + // "Get all users who have $phone as a phonenumber." (*cough* doesnt really make sense...) + $q1 = $this->_em->createQuery('SELECT u.id FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE :param MEMBER OF u.phonenumbers'); + $phone = new \Doctrine\Tests\Models\CMS\CmsPhonenumber; + $phone->phonenumber = 101; + $q1->setParameter('param', $phone); + + $this->assertEquals( + 'SELECT c0_.id AS id0 FROM cms_users c0_ WHERE EXISTS (SELECT 1 FROM cms_phonenumbers c1_ WHERE c0_.id = c1_.user_id AND c1_.phonenumber = ?)', + $q1->getSql() + ); + + // "Get all users who are members of $group." + $q2 = $this->_em->createQuery('SELECT u.id FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE :param MEMBER OF u.groups'); + $group = new \Doctrine\Tests\Models\CMS\CmsGroup; + $group->id = 101; + $q2->setParameter('param', $group); + + $this->assertEquals( + 'SELECT c0_.id AS id0 FROM cms_users c0_ WHERE EXISTS (SELECT 1 FROM cms_users_groups c1_ INNER JOIN cms_groups c2_ ON c1_.user_id = c0_.id WHERE c1_.group_id = c2_.id AND c2_.id = ?)', + $q2->getSql() + ); + + // "Get all persons who have $person as a friend." + // Tough one: Many-many self-referencing ("friends") with class table inheritance + $q3 = $this->_em->createQuery('SELECT p.id FROM Doctrine\Tests\Models\Company\CompanyPerson p WHERE :param MEMBER OF p.friends'); + $person = new \Doctrine\Tests\Models\Company\CompanyPerson; + $this->_em->getClassMetadata(get_class($person))->setIdentifierValues($person, 101); + $q3->setParameter('param', $person); + + $this->assertEquals( + 'SELECT c0_.id AS id0, c0_.discr AS discr1 FROM company_persons c0_ LEFT JOIN company_employees c1_ ON c0_.id = c1_.id LEFT JOIN company_managers c2_ ON c0_.id = c2_.id WHERE EXISTS (SELECT 1 FROM company_persons_friends c3_ INNER JOIN company_persons c4_ ON c3_.person_id = c0_.id WHERE c3_.friend_id = c4_.id AND c4_.id = ?)', + $q3->getSql() + ); + } - public function testExistsExpressionInWhereCorrelatedSubqueryAssocCondition() + /*public function testExistsExpressionInWhereCorrelatedSubqueryAssocCondition() { $this->assertSqlGeneration( // DQL @@ -261,5 +298,5 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase . ')' ); - } + }*/ } \ No newline at end of file diff --git a/tests/Doctrine/Tests/OrmFunctionalTestCase.php b/tests/Doctrine/Tests/OrmFunctionalTestCase.php index a53335fb6..b1c1706b6 100644 --- a/tests/Doctrine/Tests/OrmFunctionalTestCase.php +++ b/tests/Doctrine/Tests/OrmFunctionalTestCase.php @@ -67,6 +67,7 @@ class OrmFunctionalTestCase extends OrmTestCase $conn->exec('DELETE FROM cms_users'); } if (isset($this->_usedModelSets['company'])) { + $conn->exec('DELETE FROM company_persons_friends'); $conn->exec('DELETE FROM company_managers'); $conn->exec('DELETE FROM company_employees'); $conn->exec('DELETE FROM company_persons'); @@ -99,7 +100,7 @@ class OrmFunctionalTestCase extends OrmTestCase $classes = array(); foreach ($this->_usedModelSets as $setName => $bool) { - if ( ! isset(self::$_tablesCreated[$setName]) || $forceCreateTables) { + if ( ! isset(self::$_tablesCreated[$setName])/* || $forceCreateTables*/) { foreach (self::$_modelSets[$setName] as $className) { $classes[] = $this->_em->getClassMetadata($className); }