1
0
mirror of synced 2024-12-13 14:56:01 +03:00
doctrine2/lib/Doctrine/Query/Parser.php
guilhermeblanco ad4db34a87 Fixes in unit tests.
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.
2008-09-12 06:09:16 +00:00

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);
}
}