Location: PHPKode > projects > MongoDB Object Document Mapper > lib/Doctrine/ODM/MongoDB/Query/Parser.php
<?php
/*
 * 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.doctrine-project.org>.
 */

namespace Doctrine\ODM\MongoDB\Query;

use Doctrine\ODM\MongoDB\Query,
    Doctrine\ODM\MongoDB\DocumentManager;

/**
 * A simple parser for MongoDB Document Query Language
 *
 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @since       1.0
 * @author      Jonathan H. Wage <hide@address.com>
 */
class Parser
{
    /**
     * The DocumentManager instance for this query
     *
     * @var Doctrine\ODM\MongoDB\DocumentManager
     */
    private $dm;

    /**
     * The lexer.
     *
     * @var Doctrine\ODM\MongoDB\Query\Lexer
     */
    private $lexer;

    public function __construct(DocumentManager $dm)
    {
        $this->dm = $dm;
        $this->lexer = new Lexer;
    }

    public function parse($query, $parameters = array())
    {
        if (strpos($query, '?') !== false && strpos($query, ':') !== false) {
            throw new \InvalidArgumentException('Cannot mixed named and regular placeholders.');
        }

        $this->lexer->reset();
        $this->lexer->setInput($query);

        $query = $this->QueryLanguage($parameters);

        return $query;
    }

    /**
     * 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 ( ! ($this->lexer->lookahead['type'] === $token)) {
            $this->syntaxError($this->lexer->getLiteral($token));
        }
        $this->lexer->moveNext();
    }

    /**
     * Generates a new syntax error.
     *
     * @param string $expected Expected string.
     * @param array $token Optional token.
     * @throws AnnotationException
     */
    public function syntaxError($expected, $token = null)
    {
        if ($token === null) {
            $token = $this->lexer->lookahead;
        }

        $message =  "Expected {$expected}, got ";

        if ($this->lexer->lookahead === null) {
            $message .= 'end of string';
        } else {
            $message .= "'{$token['value']}' at position {$token['position']}";
        }

        $message .= '.';

        throw new \Doctrine\ODM\MongoDB\MongoDBException($message);
    }

    /**
     * QueryLanguage ::= FindQuery | InsertQuery | UpdateQuery | RemoveQuery
     */
    public function QueryLanguage(array &$parameters)
    {
        $this->lexer->moveNext();

        $query = new Query($this->dm);

        switch ($this->lexer->lookahead['type']) {
            case Lexer::T_FIND:
                $this->FindQuery($query, $parameters);
                break;
            case Lexer::T_INSERT:
                $this->InsertQuery($query, $parameters);
                break;
            case Lexer::T_UPDATE:
                $this->UpdateQuery($query, $parameters);
                break;
            case Lexer::T_REMOVE:
                $this->RemoveQuery($query, $parameters);
                break;
            default:
                $this->syntaxError('FIND, INSERT, UPDATE or REMOVE');
                break;
        }
        return $query;
    }

    /**
     * FindQuery ::= FindClause [WhereClause] [MapClause] [ReduceClause] [SortClause] [LimitClause] [SkipClause]
     */
    public function FindQuery(Query $query, array &$parameters)
    {
        $this->FindClause($query);

        if ($this->lexer->isNextToken(Lexer::T_WHERE)) {
            $this->WhereClause($query, $parameters);
        }

        if ($this->lexer->isNextToken(Lexer::T_MAP)) {
            $this->MapClause($query, $parameters);
        }

        if ($this->lexer->isNextToken(Lexer::T_REDUCE)) {
            $this->ReduceClause($query, $parameters);
        }

        while ($this->lexer->isNextToken($this->lexer->lookahead['type'])) {
            $this->match($this->lexer->lookahead['type']);
            switch ($this->lexer->token['type']) {
                case Lexer::T_SORT:
                    $this->SortClause($query, $parameters);
                    break;
                case Lexer::T_SKIP;
                    $this->SkipClause($query, $parameters);
                    break;
                case Lexer::T_LIMIT:
                    $this->LimitClause($query, $parameters);
                    break;
                default:
                    break(2);
            }
        }
    }

    /**
     * FindClause ::= "FIND" all | SelectField {"," SelectField}
     */
    public function FindClause(Query $query)
    {
        $this->match(Lexer::T_FIND);

        if ($this->lexer->isNextToken(Lexer::T_ALL)) {
            $this->match(Lexer::T_ALL);
        } else {
            $this->SelectField($query);
            while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
                $this->match(Lexer::T_COMMA);
                $this->SelectField($query);
            }
        }

        if ($this->lexer->isNextToken(Lexer::T_FROM)) {
            $this->match(Lexer::T_FROM);
        }

        $this->match(Lexer::T_IDENTIFIER);
        $query->find($this->lexer->token['value']);
    }

    /**
     * SelectField ::= DocumentFieldName
     */
    public function SelectField(Query $query)
    {
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
            $this->match(Lexer::T_DISTINCT);
            $fieldName = $this->DocumentFieldName();
            $query->distinct($fieldName);
            if ( ! $this->lexer->isNextToken(Lexer::T_IDENTIFIER) && ! $this->lexer->isNextToken(Lexer::T_FROM)) {
                $this->syntaxError($this->lexer->getLiteral(Lexer::T_IDENTIFIER));
            }
            return;
        } else {
            $fieldName = $this->DocumentFieldName();
        }

        $limit = null;
        $skip = null;
        while ($this->lexer->isNextToken($this->lexer->lookahead['type'])) {
            switch ($this->lexer->lookahead['type']) {
                case Lexer::T_SKIP;
                    $this->match(Lexer::T_SKIP);
                    $this->match(Lexer::T_INTEGER);
                    $skip = $this->lexer->token['value'];
                    break;
                case Lexer::T_LIMIT:
                    $this->match(Lexer::T_LIMIT);
                    $this->match(Lexer::T_INTEGER);
                    $limit = $this->lexer->token['value'];
                    break;
                default:
                    break(2);
            }
        }

        if ($skip || $limit) {
            $skip = $skip !== null ? $skip : 0;
            $query->selectSlice($fieldName, $skip, $limit);
        } else {
            $query->select($fieldName);
        }
    }

    /**
     * UpdateQuery ::= UpdateClause [WhereClause]
     */
    public function UpdateQuery(Query $query, array &$parameters)
    {
        $this->match(Lexer::T_UPDATE);
        $this->match(Lexer::T_IDENTIFIER);
        $query->update($this->lexer->token['value']);

        $this->UpdateClause($query, $parameters);
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
            $this->match(Lexer::T_COMMA);
            $this->UpdateClause($query, $parameters);
        }

        if ($this->lexer->isNextToken(Lexer::T_WHERE)) {
            $this->WhereClause($query, $parameters);
        }
    }

    /**
     * UpdateClause ::= [SetExpression], [UnsetExpression], [IncrementExpression],
     *                  [PushExpression], [PushAllExpression], [PullExpression],
     *                  [PullAllExpression], [AddToSetExpression], [AddManyToSetExpression],
     *                  [PopFirstExpression], [PopLastExpression]
     */
    public function UpdateClause(Query $query, array &$parameters)
    {
        $this->match($this->lexer->lookahead['type']);
        switch ($this->lexer->token['type']) {
            case Lexer::T_SET:
                $this->SetExpression($query, $parameters);
                break;
            case Lexer::T_UNSET:
                $this->UnsetExpression($query, $parameters);
                break;
            case Lexer::T_INC:
                $this->IncrementExpression($query, $parameters);
                break;
            case Lexer::T_PUSH:
                $this->PushExpression($query, $parameters);
                break;
            case Lexer::T_PUSHALL:
                $this->PushAllExpression($query, $parameters);
                break;
            case Lexer::T_PULL:
                $this->PullExpression($query, $parameters);
                break;
            case Lexer::T_PULLALL:
                $this->PullAllExpression($query, $parameters);
                break;
            case Lexer::T_ADDTOSET:
                $this->AddToSetExpression($query, $parameters);
                break;
            case Lexer::T_ADDMANYTOSET:
                $this->AddManyToSetExpression($query, $parameters);
                break;
            case Lexer::T_POPFIRST:
                $this->PopFirstExpression($query, $parameters);
                break;
            case Lexer::T_POPLAST:
                $this->PopLastExpression($query, $parameters);
                break;
        }
    }

    /**
     * InsertQuery ::= InsertClause InsertSetClause {"," InsertSetClause}
     */
    public function InsertQuery(Query $query, array &$parameters)
    {
        $this->match(Lexer::T_INSERT);
        $this->match(Lexer::T_IDENTIFIER);
        $query->insert($this->lexer->token['value']);

        $this->match(Lexer::T_SET);
        $this->InsertSetClause($query, $parameters);
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
            $this->match(Lexer::T_COMMA);
            $this->InsertSetClause($query, $parameters);
        }
    }

    /**
     * InsertSetClause ::= DocumentFieldName "=" Value
     */
    public function InsertSetClause(Query $query, array &$parameters)
    {
        $fieldName = $this->DocumentFieldName();
        $value = $this->Value($parameters);
        $query->field($fieldName)->set($value, false);
    }

    /**
     * RemoveQuery ::= RemoveClause [WhereClause]
     * RemoveClause ::= "REMOVE" DocumentClassName
     */
    public function RemoveQuery(Query $query, array &$parameters)
    {
        $this->match(Lexer::T_REMOVE);
        $this->match(Lexer::T_IDENTIFIER);
        $query->remove($this->lexer->token['value']);

        if ($this->lexer->isNextToken(Lexer::T_WHERE)) {
            $this->WhereClause($query, $parameters);
        }
    }

    /**
     * SortClause ::= SortClauseField {"," SortClauseField}
     */
    public function SortClause(Query $query)
    {
        $this->SortClauseField($query);
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
            $this->match(Lexer::T_COMMA);
            $this->SortClauseField($query);
        }
    }

    /**
     * SortClauseField ::= DocumentFieldName "ASC | DESC"
     */
    public function SortClauseField(Query $query)
    {
        $fieldName = $this->DocumentFieldName();
        $this->match(Lexer::T_IDENTIFIER);
        $order = $this->lexer->token['value'];
        $query->sort($fieldName, $order);
    }

    /**
     * LimitClause ::= "LIMIT" LimitInteger
     */
    public function LimitClause(Query $query)
    {
        $this->match($this->lexer->lookahead['type']);
        $query->limit($this->lexer->token['value']);
    }

    /**
     * SkipClause ::= "SKIP" SkipInteger
     */
    public function SkipClause(Query $query)
    {
        $this->match($this->lexer->lookahead['type']);
        $query->skip($this->lexer->token['value']);
    }

    /**
     * MapClause ::= "MAP" MapFunction
     */
    public function MapClause(Query $query, array &$parameters)
    {
        $this->match(Lexer::T_MAP);
        $this->match(Lexer::T_STRING);
        $query->map($this->lexer->token['value']);
    }

    /**
     * ReduceClause ::= "REDUCE" ReduceFunction
     */
    public function ReduceClause(Query $query, array &$parameters)
    {
        $this->match(Lexer::T_REDUCE);
        $this->match(Lexer::T_STRING);
        $query->reduce($this->lexer->token['value']);
    }

    /**
     * DocumentFieldName ::= DocumentFieldName | EmbeddedDocument "." {"." DocumentFieldName}
     */
    public function DocumentFieldName()
    {
        $this->match(Lexer::T_IDENTIFIER);
        $fieldName = $this->lexer->token['value'];
        while ($this->lexer->isNextToken(Lexer::T_DOT)) {
            $this->match(Lexer::T_DOT);
            $this->match(Lexer::T_IDENTIFIER);
            $fieldName .= '.' . $this->lexer->token['value'];
        }
        return $fieldName;
    }

    /**
     * WhereClause ::= "WHERE" WhereClausePart {"AND" WhereClausePart}
     */
    public function WhereClause(Query $query, array &$parameters)
    {
        $this->match(Lexer::T_WHERE);
        $this->WhereClauseExpression($query, $parameters);
        while ($this->lexer->isNextToken(Lexer::T_AND)) {
            $this->match(Lexer::T_AND);
            $this->WhereClauseExpression($query, $parameters);
        }
    }

    /**
     * WhereClausePart ::= ["all", "not"] DocumentFieldName WhereClauseExpression Value
     * WhereClauseExpression ::= "=" | "!=" | ">=" | "<=" | ">" | "<" | "in"
     *                         "notIn" | "all" | "size" | "exists" | "type"
     */
    public function WhereClauseExpression(Query $query, array &$parameters)
    {
        $options = array();
        switch ($this->lexer->lookahead['type']) {
            case Lexer::T_ALL:
                $this->match(Lexer::T_ALL);
                $options['elemMatch'] = true;
                break;
            case Lexer::T_NOT:
                $this->match(Lexer::T_NOT);
                $options['not'] = true;
                break;
        }

        $fieldName = $this->DocumentFieldName();

        $operator = $this->lexer->lookahead['value'];

        $value = $this->Value($parameters);

        $query->field($fieldName);
        switch ($operator) {
            case '=':
                $query->equals($value, $options);
                break;
            case '!=':
                $query->notEqual($value, $options);
                break;
            case '>=':
                $query->greaterThanOrEq($value, $options);
                break;
            case '<=':
                $query->lessThanOrEq($value, $options);
                break;
            case '>':
                $query->greaterThan($value, $options);
                break;
            case '<':
                $query->lessThan($value, $options);
                break;
            case 'in':
                $query->in($value, $options);
                break;
            case 'notIn':
                $query->notIn($value, $options);
                break;
            case 'all':
                $query->all($value, $options);
                break;
            case 'size':
                $query->size($value, $options);
                break;
            case 'exists':
                $query->exists($value, $options);
                break;
            case 'type':
                $query->type($value, $options);
                break;
            case 'mod':
                $query->mod($value, $options);
                break;
            default:
                $this->syntaxError('Invalid atomic update operator.');
        }
    }

    /**
     * Value ::= LiteralValue | JsonObject | JsonArray
     */
    public function Value(array &$parameters)
    {
        $this->match($this->lexer->lookahead['type']);
        $this->match($this->lexer->lookahead['type']);
        $value = $this->lexer->token['value'];
        if (isset($parameters[$value])) {
            $value = $parameters[$value];
        }
        if ($value === '?') {
            $value = array_shift($parameters);
        }
        // detect and decode json values
        if ($value[0] === '[' || $value[0] === '{') {
            return json_decode($value);
        }
        if ($value === 'true') {
            $value = true;
        }
        if ($value === 'false') {
            $value = false;
        }
        return $value;
    }

    /**
     * SetExpression ::= "SET" DocumentFieldName "=" Value {"," SetExpression}
     */
    public function SetExpression(Query $query, array &$parameters)
    {
        $fieldName = $this->DocumentFieldName();
        $value = $this->Value($parameters);
        $query->field($fieldName)->set($value);
    }

    /**
     * UnsetExpression ::= "UNSET" DocumentFieldName {"," UnsetExpression}
     */
    public function UnsetExpression(Query $query, array &$parameters)
    {
        $this->match(Lexer::T_IDENTIFIER);
        $query->field($this->lexer->token['value'])->unsetField();
    }

    /**
     * PushExpression ::= "PUSH" DocumentFieldName Value {"," PushExpression}
     */
    public function PushExpression(Query $query, array &$parameters)
    {
        $fieldName = $this->DocumentFieldName();
        $value = $this->Value($parameters);
        $query->field($fieldName)->push($value);
    }

    /**
     * PushAllExpression ::= "PUSHALL" DocumentFieldName Value {"," PushAllExpression}
     */
    public function PushAllExpression(Query $query, array &$parameters)
    {
        $fieldName = $this->DocumentFieldName();
        $value = $this->Value($parameters);
        $query->field($fieldName)->pushAll($value);
    }

    /**
     * PullExpression ::= "PULL" DocumentFieldName Value {"," PullExpression}
     */
    public function PullExpression(Query $query, array &$parameters)
    {
        $fieldName = $this->DocumentFieldName();
        $value = $this->Value($parameters);
        $query->field($fieldName)->pull($value);
    }

    /**
     * PullAllExpression ::= "PULLALL" DocumentFieldName Value {"," PullAllExpression}
     */
    public function PullAllExpression(Query $query, array &$parameters)
    {
        $fieldName = $this->DocumentFieldName();
        $value = $this->Value($parameters);
        $query->field($fieldName)->pullAll($value);
    }

    /**
     * PopFirstExpression ::= "POPFIRST" DocumentFieldName {"," PopFirstExpression}
     */
    public function PopFirstExpression(Query $query, array &$parameters)
    {
        $this->match(Lexer::T_IDENTIFIER);
        $query->field($this->lexer->token['value'])->popFirst();
    }

    /**
     * PopLastExpression ::= "POPLAST" DocumentFieldName {"," PopLastExpression}
     */
    public function PopLastExpression(Query $query, array &$parameters)
    {
        $this->match(Lexer::T_IDENTIFIER);
        $query->field($this->lexer->token['value'])->popLast();
    }

    /**
     * AddToSetExpression ::= "ADDTOSET" DocumentFieldName Value {"," AddToSetExpression}
     */
    public function AddToSetExpression(Query $query, array &$parameters)
    {
        $fieldName = $this->DocumentFieldName();
        $value = $this->Value($parameters);
        $query->field($fieldName)->addToSet($value);
    }

    /**
     * AddManyToSetExpression ::= "ADDMANYTOSET" DocumentFieldName Value {"," AddManyToSetExpression}
     */
    public function AddManyToSetExpression(Query $query, array &$parameters)
    {
        $fieldName = $this->DocumentFieldName();
        $value = $this->Value($parameters);
        $query->field($fieldName)->addManyToSet($value);
    }

    /**
     * IncrementExpression ::= "INC" DocumentFieldName "=" IncrementInteger {"," IncrementExpression}
     */
    public function IncrementExpression(Query $query, array &$parameters)
    {
        $fieldName = $this->DocumentFieldName();
        $value = $this->Value($parameters);
        $query->field($fieldName)->inc($value);
    }
}
Return current item: MongoDB Object Document Mapper