<?php
namespace PHPCR\Util\QOM;
use InvalidArgumentException;
use PHPCR\Query\QOM\ColumnInterface;
use PHPCR\Query\QOM\ConstraintInterface;
use PHPCR\Query\QOM\DynamicOperandInterface;
use PHPCR\Query\QOM\JoinConditionInterface;
use PHPCR\Query\QOM\OrderingInterface;
use PHPCR\Query\QOM\QueryObjectModelConstantsInterface;
use PHPCR\Query\QOM\QueryObjectModelFactoryInterface;
use PHPCR\Query\QOM\QueryObjectModelInterface;
use PHPCR\Query\QOM\SourceInterface;
use PHPCR\Query\QueryInterface;
use PHPCR\Query\QueryResultInterface;
use RuntimeException;
/**
* QueryBuilder class is responsible for dynamically create QOM queries.
*
* @license http://www.apache.org/licenses Apache License Version 2.0, January 2004
* @license http://opensource.org/licenses/MIT MIT License
* @author Nacho MartÃn <nitram.ohcan@gmail.com>
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Benjamin Eberlei <kontakt@beberlei.de>
*/
class QueryBuilder
{
/** The builder states. */
const STATE_DIRTY = 0;
const STATE_CLEAN = 1;
/**
* @var int The state of the query object. Can be dirty or clean.
*/
private $state = self::STATE_CLEAN;
/**
* @var QueryObjectModelFactoryInterface QOMFactory
*/
private $qomFactory;
/**
* @var int The maximum number of results to retrieve.
*/
private $firstResult = null;
/**
* @var int The maximum number of results to retrieve.
*/
private $maxResults = null;
/**
* @var array with the orderings that determine the order of the result
*/
private $orderings = [];
/**
* @var ConstraintInterface to apply to the query.
*/
private $constraint = null;
/**
* @var array with the columns to be selected.
*/
private $columns = [];
/**
* @var SourceInterface source of the query.
*/
private $source = null;
/**
* QOM tree.
*
* @var QueryObjectModelInterface
*/
private $query = null;
/**
* @var array The query parameters.
*/
private $params = [];
/**
* Initializes a new QueryBuilder.
*
* @param QueryObjectModelFactoryInterface $qomFactory
*/
public function __construct(QueryObjectModelFactoryInterface $qomFactory)
{
$this->qomFactory = $qomFactory;
}
/**
* Get a query builder instance from an existing query.
*
* @param string $statement the statement in the specified language
* @param string $language the query language
*
* @throws InvalidArgumentException
*
* @return QueryBuilder This QueryBuilder instance.
*/
public function setFromQuery($statement, $language)
{
if (QueryInterface::JCR_SQL2 === $language) {
$converter = new Sql2ToQomQueryConverter($this->qomFactory);
$statement = $converter->parse($statement);
}
if (!$statement instanceof QueryObjectModelInterface) {
throw new InvalidArgumentException("Language '$language' not supported");
}
$this->state = self::STATE_DIRTY;
$this->source = $statement->getSource();
$this->constraint = $statement->getConstraint();
$this->orderings = $statement->getOrderings();
$this->columns = $statement->getColumns();
return $this;
}
/**
* Get the associated QOMFactory for this query builder.
*
* @return QueryObjectModelFactoryInterface
*/
public function getQOMFactory()
{
return $this->qomFactory;
}
/**
* Shortcut for getQOMFactory().
*/
public function qomf()
{
return $this->getQOMFactory();
}
/**
* sets the position of the first result to retrieve (the "offset").
*
* @param int $firstResult The First result to return.
*
* @return QueryBuilder This QueryBuilder instance.
*/
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 QueryBuilder.
*
* @return int The position of the first result.
*/
public function getFirstResult()
{
return $this->firstResult;
}
/**
* Sets the maximum number of results to retrieve (the "limit").
*
* @param int $maxResults The maximum number of results to retrieve.
*
* @return QueryBuilder This QueryBuilder instance.
*/
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 builder.
*
* @return int Maximum number of results.
*/
public function getMaxResults()
{
return $this->maxResults;
}
/**
* Gets the array of orderings.
*
* @return OrderingInterface[] Orderings to apply.
*/
public function getOrderings()
{
return $this->orderings;
}
/**
* Adds an ordering to the query results.
*
* @param DynamicOperandInterface $sort The ordering expression.
* @param string $order The ordering direction.
*
* @throws InvalidArgumentException
*
* @return QueryBuilder This QueryBuilder instance.
*/
public function addOrderBy(DynamicOperandInterface $sort, $order = 'ASC')
{
$order = strtoupper($order);
if (!in_array($order, ['ASC', 'DESC'])) {
throw new InvalidArgumentException('Order must be one of "ASC" or "DESC"');
}
$this->state = self::STATE_DIRTY;
if ($order === 'DESC') {
$ordering = $this->qomFactory->descending($sort);
} else {
$ordering = $this->qomFactory->ascending($sort);
}
$this->orderings[] = $ordering;
return $this;
}
/**
* Specifies an ordering for the query results.
* Replaces any previously specified orderings, if any.
*
* @param DynamicOperandInterface $sort The ordering expression.
* @param string $order The ordering direction.
*
* @return QueryBuilder This QueryBuilder instance.
*/
public function orderBy(DynamicOperandInterface $sort, $order = 'ASC')
{
$this->orderings = [];
$this->addOrderBy($sort, $order);
return $this;
}
/**
* Specifies one restriction (may be simple or composed).
* Replaces any previously specified restrictions, if any.
*
* @param ConstraintInterface $constraint
*
* @return QueryBuilder This QueryBuilder instance.
*/
public function where(ConstraintInterface $constraint)
{
$this->state = self::STATE_DIRTY;
$this->constraint = $constraint;
return $this;
}
/**
* Returns the constraint to apply.
*
* @return ConstraintInterface the constraint to be applied
*/
public function getConstraint()
{
return $this->constraint;
}
/**
* Creates a new constraint formed by applying a logical AND to the
* existing constraint and the new one.
*
* Order of ands is important:
*
* Given $this->constraint = $constraint1
* running andWhere($constraint2)
* resulting constraint will be $constraint1 AND $constraint2
*
* If there is no previous constraint then it will simply store the
* provided one
*
* @param ConstraintInterface $constraint
*
* @return QueryBuilder This QueryBuilder instance.
*/
public function andWhere(ConstraintInterface $constraint)
{
$this->state = self::STATE_DIRTY;
if ($this->constraint) {
$this->constraint = $this->qomFactory->andConstraint($this->constraint, $constraint);
} else {
$this->constraint = $constraint;
}
return $this;
}
/**
* Creates a new constraint formed by applying a logical OR to the
* existing constraint and the new one.
*
* Order of ands is important:
*
* Given $this->constraint = $constraint1
* running orWhere($constraint2)
* resulting constraint will be $constraint1 OR $constraint2
*
* If there is no previous constraint then it will simply store the
* provided one
*
* @param ConstraintInterface $constraint
*
* @return QueryBuilder This QueryBuilder instance.
*/
public function orWhere(ConstraintInterface $constraint)
{
$this->state = self::STATE_DIRTY;
if ($this->constraint) {
$this->constraint = $this->qomFactory->orConstraint($this->constraint, $constraint);
} else {
$this->constraint = $constraint;
}
return $this;
}
/**
* Returns the columns to be selected.
*
* @return ColumnInterface[] The columns to be selected
*/
public function getColumns()
{
return $this->columns;
}
/**
* Sets the columns to be selected.
*
* @param ColumnInterface[] $columns The columns to be selected
*
* @return QueryBuilder This QueryBuilder instance.
*/
public function setColumns(array $columns)
{
$this->columns = $columns;
return $this;
}
/**
* Identifies a property in the specified or default selector to include in the tabular view of query results.
* Replaces any previously specified columns to be selected if any.
*
* @param string $selectorName
* @param string $propertyName
* @param string $columnName
*
* @return QueryBuilder This QueryBuilder instance.
*/
public function select($selectorName, $propertyName, $columnName = null)
{
$this->state = self::STATE_DIRTY;
$this->columns = [$this->qomFactory->column($selectorName, $propertyName, $columnName)];
return $this;
}
/**
* Adds a property in the specified or default selector to include in the tabular view of query results.
*
* @param string $selectorName
* @param string $propertyName
* @param string $columnName
*
* @return QueryBuilder This QueryBuilder instance.
*/
public function addSelect($selectorName, $propertyName, $columnName = null)
{
$this->state = self::STATE_DIRTY;
$this->columns[] = $this->qomFactory->column($selectorName, $propertyName, $columnName);
return $this;
}
/**
* Sets the default Selector or the node-tuple Source. Can be a selector
* or a join.
*
* @param SourceInterface $source
*
* @return QueryBuilder This QueryBuilder instance.
*/
public function from(SourceInterface $source)
{
$this->state = self::STATE_DIRTY;
$this->source = $source;
return $this;
}
/**
* Gets the default Selector.
*
* @return SourceInterface The default selector.
*/
public function getSource()
{
return $this->source;
}
/**
* Performs an inner join between the stored source and the supplied source.
*
* @param SourceInterface $rightSource
* @param JoinConditionInterface $joinCondition
*
* @throws RuntimeException if there is not an existing source.
*
* @return QueryBuilder This QueryBuilder instance.
*/
public function join(SourceInterface $rightSource, JoinConditionInterface $joinCondition)
{
return $this->innerJoin($rightSource, $joinCondition);
}
/**
* Performs an inner join between the stored source and the supplied source.
*
* @param SourceInterface $rightSource
* @param JoinConditionInterface $joinCondition
*
* @throws RuntimeException if there is not an existing source.
*
* @return QueryBuilder This QueryBuilder instance.
*/
public function innerJoin(SourceInterface $rightSource, JoinConditionInterface $joinCondition)
{
return $this->joinWithType($rightSource, QueryObjectModelConstantsInterface::JCR_JOIN_TYPE_INNER, $joinCondition);
}
/**
* Performs an left outer join between the stored source and the supplied source.
*
* @param SourceInterface $rightSource
* @param JoinConditionInterface $joinCondition
*
* @throws RuntimeException if there is not an existing source.
*
* @return QueryBuilder This QueryBuilder instance.
*/
public function leftJoin(SourceInterface $rightSource, JoinConditionInterface $joinCondition)
{
return $this->joinWithType($rightSource, QueryObjectModelConstantsInterface::JCR_JOIN_TYPE_LEFT_OUTER, $joinCondition);
}
/**
* Performs a right outer join between the stored source and the supplied source.
*
* @param SourceInterface $rightSource
* @param JoinConditionInterface $joinCondition
*
* @throws RuntimeException if there is not an existing source.
*
* @return QueryBuilder This QueryBuilder instance.
*/
public function rightJoin(SourceInterface $rightSource, JoinConditionInterface $joinCondition)
{
return $this->joinWithType($rightSource, QueryObjectModelConstantsInterface::JCR_JOIN_TYPE_RIGHT_OUTER, $joinCondition);
}
/**
* Performs an join between the stored source and the supplied source.
*
* @param SourceInterface $rightSource
* @param string $joinType as specified in PHPCR\Query\QOM\QueryObjectModelConstantsInterface
* @param JoinConditionInterface $joinCondition
*
* @throws RuntimeException if there is not an existing source.
*
* @return QueryBuilder This QueryBuilder instance.
*/
public function joinWithType(SourceInterface $rightSource, $joinType, JoinConditionInterface $joinCondition)
{
if (!$this->source) {
throw new RuntimeException('Cannot perform a join without a previous call to from');
}
$this->state = self::STATE_DIRTY;
$this->source = $this->qomFactory->join($this->source, $rightSource, $joinType, $joinCondition);
return $this;
}
/**
* Gets the query built.
*
* @return QueryObjectModelInterface
*/
public function getQuery()
{
if ($this->query !== null && $this->state === self::STATE_CLEAN) {
return $this->query;
}
$this->state = self::STATE_CLEAN;
$this->query = $this->qomFactory->createQuery($this->source, $this->constraint, $this->orderings, $this->columns);
if ($this->firstResult) {
$this->query->setOffset($this->firstResult);
}
if ($this->maxResults) {
$this->query->setLimit($this->maxResults);
}
return $this->query;
}
/**
* Executes the query setting firstResult and maxResults.
*
* @return QueryResultInterface
*/
public function execute()
{
if ($this->query === null || $this->state === self::STATE_DIRTY) {
$this->query = $this->getQuery();
}
foreach ($this->params as $key => $value) {
$this->query->bindValue($key, $value);
}
return $this->query->execute();
}
/**
* Sets a query parameter for the query being constructed.
*
* @param string $key The parameter name.
* @param mixed $value The parameter value.
*
* @return QueryBuilder This QueryBuilder instance.
*/
public function setParameter($key, $value)
{
$this->params[$key] = $value;
return $this;
}
/**
* Gets a (previously set) query parameter of the query being constructed.
*
* @param string $key The key (name) of the bound parameter.
*
* @return mixed The value of the bound parameter.
*/
public function getParameter($key)
{
return isset($this->params[$key]) ? $this->params[$key] : null;
}
/**
* Sets a collection of query parameters for the query being constructed.
*
* @param array $params The query parameters to set.
*
* @return QueryBuilder This QueryBuilder instance.
*/
public function setParameters(array $params)
{
$this->params = $params;
return $this;
}
/**
* Gets all defined query parameters for the query being constructed.
*
* @return array The currently defined query parameters.
*/
public function getParameters()
{
return $this->params;
}
}