ad4db34a87
Started refactoring in DQL parser to separate Production into Parser and AST classes. Finished first tests. Currently only 4 are active in IdentifierRecognitionTest, and only 2 are passing.
381 lines
10 KiB
PHP
381 lines
10 KiB
PHP
<?php
|
|
|
|
/*
|
|
* $Id$
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*
|
|
* This software consists of voluntary contributions made by many individuals
|
|
* and is licensed under the LGPL. For more information, see
|
|
* <http://www.phpdoctrine.org>.
|
|
*/
|
|
|
|
/**
|
|
* An LL(k) parser for the context-free grammar of Doctrine Query Language.
|
|
* Parses a DQL query, reports any errors in it, and generates the corresponding
|
|
* SQL.
|
|
*
|
|
* @package Doctrine
|
|
* @subpackage Query
|
|
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
|
|
* @author Janne Vanhala <jpvanhal@cc.hut.fi>
|
|
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
|
|
* @link http://www.phpdoctrine.org
|
|
* @since 2.0
|
|
* @version $Revision$
|
|
*/
|
|
class Doctrine_Query_Parser
|
|
{
|
|
/**
|
|
* The minimum number of tokens read after last detected error before
|
|
* another error can be reported.
|
|
*
|
|
* @var int
|
|
*/
|
|
const MIN_ERROR_DISTANCE = 2;
|
|
|
|
|
|
/**
|
|
* The Sql Builder object.
|
|
*
|
|
* @var Doctrine_Query_SqlBuilder
|
|
*/
|
|
protected $_sqlbuilder;
|
|
|
|
/**
|
|
* DQL string.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $_input;
|
|
|
|
/**
|
|
* A scanner object.
|
|
*
|
|
* @var Doctrine_Query_Scanner
|
|
*/
|
|
protected $_scanner;
|
|
|
|
/**
|
|
* The Parser Result object.
|
|
*
|
|
* @var Doctrine_Query_ParserResult
|
|
*/
|
|
protected $_parserResult;
|
|
|
|
/**
|
|
* Keyword symbol table
|
|
*
|
|
* @var Doctrine_Query_Token
|
|
*/
|
|
protected $_keywordTable;
|
|
|
|
// Scanner Stuff
|
|
|
|
/**
|
|
* @var array The next token in the query string.
|
|
*/
|
|
public $lookahead;
|
|
|
|
/**
|
|
* @var array The last matched token.
|
|
*/
|
|
public $token;
|
|
|
|
// End of Scanner Stuff
|
|
|
|
|
|
// Error management stuff
|
|
|
|
/**
|
|
* Array containing errors detected in the query string during parsing process.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_errors;
|
|
|
|
/**
|
|
* @var int The number of tokens read since last error in the input string.
|
|
*/
|
|
protected $_errorDistance;
|
|
|
|
/**
|
|
* The EntityManager.
|
|
*
|
|
* @var EnityManager
|
|
*/
|
|
protected $_em;
|
|
|
|
// End of Error management stuff
|
|
|
|
|
|
/**
|
|
* Creates a new query parser object.
|
|
*
|
|
* @param string $dql DQL to be parsed.
|
|
* @param Doctrine_Connection $connection The connection to use
|
|
*/
|
|
public function __construct(Doctrine_Query $query)
|
|
{
|
|
$this->_em = $query->getEntityManager();
|
|
$this->_input = $query->getDql();
|
|
$this->_scanner = new Doctrine_Query_Scanner($this->_input);
|
|
$this->_sqlBuilder = Doctrine_Query_SqlBuilder::fromConnection($this->_em);
|
|
$this->_keywordTable = new Doctrine_Query_Token();
|
|
|
|
$this->_parserResult = new Doctrine_Query_ParserResult(
|
|
'',
|
|
array( // queryComponent
|
|
'dctrn' => array(
|
|
'metadata' => null,
|
|
'parent' => null,
|
|
'relation' => null,
|
|
'map' => null,
|
|
'scalar' => null,
|
|
),
|
|
),
|
|
array( // tableAliasMap
|
|
'dctrn' => 'dctrn',
|
|
)
|
|
);
|
|
|
|
$this->_parserResult->setEntityManager($this->_em);
|
|
|
|
$this->free(true);
|
|
}
|
|
|
|
|
|
/**
|
|
* Attempts to match the given token with the current lookahead token.
|
|
*
|
|
* If they match, updates the lookahead token; otherwise raises a syntax
|
|
* error.
|
|
*
|
|
* @param int|string token type or value
|
|
* @return bool True, if tokens match; false otherwise.
|
|
*/
|
|
public function match($token)
|
|
{
|
|
if (is_string($token)) {
|
|
$isMatch = ($this->lookahead['value'] === $token);
|
|
} else {
|
|
$isMatch = ($this->lookahead['type'] === $token);
|
|
}
|
|
|
|
if ( ! $isMatch) {
|
|
// No definition for value checking.
|
|
$this->syntaxError($this->_keywordTable->getLiteral($token));
|
|
}
|
|
|
|
$this->next();
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Moves the parser scanner to next token
|
|
*
|
|
* @return void
|
|
*/
|
|
public function next()
|
|
{
|
|
$this->token = $this->lookahead;
|
|
$this->lookahead = $this->_scanner->next();
|
|
$this->_errorDistance++;
|
|
}
|
|
|
|
|
|
public function isA($value, $token)
|
|
{
|
|
return $this->_scanner->isA($value, $token);
|
|
}
|
|
|
|
|
|
/**
|
|
* Free this parser enabling it to be reused
|
|
*
|
|
* @param boolean $deep Whether to clean peek and reset errors
|
|
* @param integer $position Position to reset
|
|
* @return void
|
|
*/
|
|
public function free($deep = false, $position = 0)
|
|
{
|
|
// WARNING! Use this method with care. It resets the scanner!
|
|
$this->_scanner->resetPosition($position);
|
|
|
|
// Deep = true cleans peek and also any previously defined errors
|
|
if ($deep) {
|
|
$this->_scanner->resetPeek();
|
|
$this->_errors = array();
|
|
}
|
|
|
|
$this->token = null;
|
|
$this->lookahead = null;
|
|
|
|
$this->_errorDistance = self::MIN_ERROR_DISTANCE;
|
|
}
|
|
|
|
|
|
/**
|
|
* Parses a query string.
|
|
*/
|
|
public function parse()
|
|
{
|
|
$this->lookahead = $this->_scanner->next();
|
|
|
|
// Building the Abstract Syntax Tree
|
|
// We have to double the call of QueryLanguage to allow it to work correctly... =\
|
|
$DQL = new Doctrine_Query_Parser_QueryLanguage($this);
|
|
$AST = $DQL->parse('QueryLanguage', Doctrine_Query_ParserParamHolder::create());
|
|
|
|
// Check for end of string
|
|
if ($this->lookahead !== null) {
|
|
$this->syntaxError('end of string');
|
|
}
|
|
|
|
// Check for semantical errors
|
|
if (count($this->_errors) > 0) {
|
|
throw new Doctrine_Query_Parser_Exception(implode("\r\n", $this->_errors));
|
|
}
|
|
|
|
// Assign the executor in parser result
|
|
$this->_parserResult->setSqlExecutor(Doctrine_Query_SqlExecutor_Abstract::create($AST));
|
|
|
|
return $this->_parserResult;
|
|
}
|
|
|
|
|
|
/**
|
|
* Retrieves the assocated Doctrine_Query_SqlBuilder to this object.
|
|
*
|
|
* @return Doctrine_Query_SqlBuilder
|
|
*/
|
|
public function getSqlBuilder()
|
|
{
|
|
return $this->_sqlBuilder;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the scanner object associated with this object.
|
|
*
|
|
* @return Doctrine_Query_Scanner
|
|
*/
|
|
public function getScanner()
|
|
{
|
|
return $this->_scanner;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the parser result associated with this object.
|
|
*
|
|
* @return Doctrine_Query_ParserResult
|
|
*/
|
|
public function getParserResult()
|
|
{
|
|
return $this->_parserResult;
|
|
}
|
|
|
|
|
|
/**
|
|
* Generates a new syntax error.
|
|
*
|
|
* @param string $expected Optional expected string.
|
|
* @param array $token Optional token.
|
|
*/
|
|
public function syntaxError($expected = '', $token = null)
|
|
{
|
|
if ($token === null) {
|
|
$token = $this->lookahead;
|
|
}
|
|
|
|
// Formatting message
|
|
$message = 'line 0, col ' . (isset($token['position']) ? $token['position'] : '-1') . ': Error: ';
|
|
|
|
if ($expected !== '') {
|
|
$message .= "Expected '$expected', got ";
|
|
} else {
|
|
$message .= 'Unexpected ';
|
|
}
|
|
|
|
if ($this->lookahead === null) {
|
|
$message .= 'end of string.';
|
|
} else {
|
|
$message .= "'{$this->lookahead['value']}'";
|
|
}
|
|
|
|
throw new Doctrine_Query_Parser_Exception($message);
|
|
}
|
|
|
|
|
|
/**
|
|
* Generates a new semantical error.
|
|
*
|
|
* @param string $message Optional message.
|
|
* @param array $token Optional token.
|
|
*/
|
|
public function semanticalError($message = '', $token = null)
|
|
{
|
|
$this->_semanticalErrorCount++;
|
|
|
|
if ($token === null) {
|
|
$token = $this->token;
|
|
}
|
|
|
|
$this->_logError('Warning: ' . $message, $token);
|
|
}
|
|
|
|
|
|
/**
|
|
* Logs new error entry.
|
|
*
|
|
* @param string $message Message to log.
|
|
* @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;
|
|
}
|
|
|
|
$this->_errorDistance = 0;
|
|
}
|
|
|
|
/**
|
|
* Gets the EntityManager used by the parser.
|
|
*
|
|
* @return EntityManager
|
|
*/
|
|
public function getEntityManager()
|
|
{
|
|
return $this->_em;
|
|
}
|
|
|
|
|
|
/**
|
|
* Retrieve the piece of DQL string given the token position
|
|
*
|
|
* @param array $token Token that it was processing.
|
|
* @return string Piece of DQL string.
|
|
*/
|
|
public function getQueryPiece($token, $previousChars = 10, $nextChars = 10)
|
|
{
|
|
$start = max(0, $token['position'] - $previousChars);
|
|
$end = max($token['position'] + $nextChars, strlen($this->_input));
|
|
|
|
return substr($this->_input, $start, $end);
|
|
}
|
|
}
|