Commit 2171aedc authored by catch's avatar catch

Issue #1854708 by chx, dawehner, damiankloip: EntityQuery aggregation support.

parent 33b21571
......@@ -729,6 +729,23 @@ function entity_query($entity_type, $conjunction = 'AND') {
return drupal_container()->get('entity.query')->get($entity_type, $conjunction);
}
/**
* Returns the entity query aggregate object for this entity type.
*
* @param $entity_type
* The entity type, e.g. node, for which the query object should be
* returned.
* @param $conjunction
* AND if all conditions in the query need to apply, OR if any of them is
* enough. Optional, defaults to AND.
*
* @return \Drupal\Core\Entity\Query\QueryInterface
* The query object that can query the given entity type.
*/
function entity_query_aggregate($entity_type, $conjunction = 'AND') {
return drupal_container()->get('entity.query')->getAggregate($entity_type, $conjunction);
}
/**
* Generic access callback for entity pages.
*
......
......@@ -11,11 +11,12 @@
use Drupal\Core\Entity\EntityManager;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\Core\Entity\Query\QueryBase;
use Drupal\Core\Entity\Query\QueryInterface;
/**
* Defines the entity query for configuration entities.
*/
class Query extends QueryBase {
class Query extends QueryBase implements QueryInterface {
/**
* Stores the entity manager.
......@@ -93,10 +94,11 @@ public function execute() {
$result = $this->condition->compile($configs);
// Apply sort settings.
foreach ($this->sort as $property => $sort) {
foreach ($this->sort as $sort) {
$direction = $sort['direction'] == 'ASC' ? -1 : 1;
uasort($result, function($a, $b) use ($property, $direction) {
return ($a[$property] <= $b[$property]) ? $direction : -$direction;
$field = $sort['field'];
uasort($result, function($a, $b) use ($field, $direction) {
return ($a[$field] <= $b[$field]) ? $direction : -$direction;
});
}
......
......@@ -10,6 +10,8 @@
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Entity\EntityManager;
use Drupal\Core\Entity\Query\QueryAggregateInterface;
use Drupal\Core\Entity\Query\QueryException;
/**
* Provides a factory for creating entity query objects for the config backend.
......@@ -34,7 +36,7 @@ public function __construct(StorageInterface $config_storage) {
}
/**
* Instantiate a entity query for a certain entity type.
* Instantiates an entity query for a given entity type.
*
* @param string $entity_type
* The entity type for the query.
......@@ -50,4 +52,24 @@ public function get($entity_type, $conjunction, EntityManager $entity_manager) {
return new Query($entity_type, $conjunction, $entity_manager, $this->configStorage);
}
/**
* Returns a aggregation query object for a given entity type.
*
* @param string $entity_type
* The entity type.
* @param string $conjunction
* - AND: all of the conditions on the query need to match.
* - OR: at least one of the conditions on the query need to match.
*
* @param \Drupal\Core\Entity\EntityManager $entity_manager
* The entity manager.
*
* @throws \Drupal\Core\Entity\Query\QueryException
* @return \Drupal\Core\Entity\Query\QueryAggregateInterface
* The query object that can query the given entity type.
*/
public function getAggregate($entity_type, $conjunction, EntityManager $entity_manager) {
throw new QueryException('Aggregation over configuration enitties is not supported');
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Entity\Query\ConditionAggregateBase.
*/
namespace Drupal\Core\Entity\Query;
/**
* Defines a common base class for all aggregation entity condition implementations.
*/
abstract class ConditionAggregateBase extends ConditionFundamentals implements ConditionAggregateInterface {
/**
* Implements \Drupal\Core\Entity\Query\ConditionAggregateInterface::condition().
*/
public function condition($field, $function = NULL, $value = NULL, $operator = NULL, $langcode = NULL) {
$this->conditions[] = array(
'field' => $field,
'function' => $function,
'value' => $value,
'operator' => $operator,
'langcode' => $langcode,
);
return $this;
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Entity\Query\ConditionAggregateInterface.
*/
namespace Drupal\Core\Entity\Query;
/**
* Defines aggregated entity query conditions.
*/
interface ConditionAggregateInterface extends \Countable {
/**
* Gets the current conjunction.
*
* @return string
* Can be AND or OR.
*/
public function getConjunction();
/**
* Adds a condition.
*
* @param string|ConditionAggregateInterface $field
* @param string $function
* @param mixed $value
* @param string $operator
* @param string $langcode
*
* @return \Drupal\Core\Entity\Query\ConditionAggregateInterface
* The called object.
* @see \Drupal\Core\Entity\Query\QueryInterface::condition()
*/
public function condition($field, $function = NULL, $value = NULL, $operator = NULL, $langcode = NULL);
/**
* Queries for the existence of a field.
*
* @param $field
* @param string $langcode
* @return ConditionInterface
* @see \Drupal\Core\Entity\Query\QueryInterface::exists()
*/
public function exists($field, $function, $langcode = NULL);
/**
* Queries for the nonexistence of a field.
*
* @param string $field
* @return ConditionInterface;
* @see \Drupal\Core\Entity\Query\QueryInterface::notexists()
*/
public function notExists($field, $function, $langcode = NULL);
/**
* Gets a complete list of all conditions in this conditional clause.
*
* This method returns by reference. That allows alter hooks to access the
* data structure directly and manipulate it before it gets compiled.
*
* @return array
*/
public function &conditions();
/**
* Compiles this conditional clause.
*
* @param $query
* The query object this conditional clause belongs to.
*/
public function compile($query);
}
......@@ -8,40 +8,9 @@
namespace Drupal\Core\Entity\Query;
/**
* Common code for all implementations of the entity query condition interface.
* Defines a common base class for all entity condition implementations.
*/
abstract class ConditionBase implements ConditionInterface {
/**
* Array of conditions.
*
* @var array
*/
protected $conditions = array();
/**
* Constructs a Condition object.
*
* @param string $conjunction
* The operator to use to combine conditions: 'AND' or 'OR'.
*/
public function __construct($conjunction = 'AND') {
$this->conjunction = $conjunction;
}
/**
* Implements \Drupal\Core\Entity\Query\ConditionInterface::getConjunction().
*/
public function getConjunction() {
return $this->conjunction;
}
/**
* Implements \Countable::count().
*/
public function count() {
return count($this->conditions) - 1;
}
abstract class ConditionBase extends ConditionFundamentals implements ConditionInterface {
/**
* Implements \Drupal\Core\Entity\Query\ConditionInterface::compile().
......@@ -56,23 +25,4 @@ public function condition($field, $value = NULL, $operator = NULL, $langcode = N
return $this;
}
/**
* Implements \Drupal\Core\Entity\Query\ConditionInterface::conditions().
*/
public function &conditions() {
return $this->conditions;
}
/**
* Makes sure condition groups are cloned as well.
*/
function __clone() {
foreach ($this->conditions as $key => $condition) {
if ($condition['field'] instanceOf ConditionInterface) {
$this->conditions[$key]['field'] = clone($condition['field']);
}
}
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Entity\Query\ConditionFundamentals.
*/
namespace Drupal\Core\Entity\Query;
/**
* Common code for all implementations of the entity query condition interfaces.
*/
abstract class ConditionFundamentals {
/**
* Array of conditions.
*
* @var array
*/
protected $conditions = array();
/**
* The conjunction of this condition group. The value is one of the following:
*
* - AND (default)
* - OR
*
* @var string
*/
protected $conjunction;
/**
* Constructs a Condition object.
*
* @param string $conjunction
* The operator to use to combine conditions: 'AND' or 'OR'.
*/
public function __construct($conjunction = 'AND') {
$this->conjunction = $conjunction;
}
/**
* Implements \Drupal\Core\Entity\Query\ConditionInterface::getConjunction().
*/
public function getConjunction() {
return $this->conjunction;
}
/**
* Implements \Countable::count().
*/
public function count() {
return count($this->conditions) - 1;
}
/**
* Implements \Drupal\Core\Entity\Query\ConditionInterface::conditions().
*/
public function &conditions() {
return $this->conditions;
}
/**
* Implements the magic __clone function.
*
* Makes sure condition groups are cloned as well.
*/
public function __clone() {
foreach ($this->conditions as $key => $condition) {
if ($condition['field'] instanceOf ConditionInterface) {
$this->conditions[$key]['field'] = clone($condition['field']);
}
}
}
}
......@@ -32,7 +32,7 @@ public function count();
/**
* Adds a condition.
*
* @param string $field
* @param string|\Drupal\Core\Entity\Query\ConditionInterface $field
* @param mixed $value
* @param string $operator
* @param string $langcode
......
<?php
/**
* @file
* Contains \Drupal\Core\Entity\Query\QueryAggregateInterface.
*/
namespace Drupal\Core\Entity\Query;
/**
* Defines a interface for aggregated entity queries.
*/
interface QueryAggregateInterface extends QueryInterface {
/**
* Specifies a field and a function to aggregate on.
*
* Available functions: SUM, AVG, MIN, MAX and COUNT.
*
* @todo What about GROUP_CONCAT support?
*
* @param string $field
* The name of the field to aggregate by.
* @param string $function
* The aggregation function, for example COUNT or MIN.
* @param string $langcode
* (optional) The language code.
* @param string $alias
* (optional) The key that will be used on the resultset.
*
* @return \Drupal\Core\Entity\Query\QueryAggregateInterface
* The called object.
*/
public function aggregate($field, $function, $langcode = NULL, &$alias = NULL);
/**
* Specifies the field to group on.
*
* @param string $field
* The name of the field to group by.
*
* @return \Drupal\Core\Entity\Query\QueryAggregateInterface
* The called object.
*/
public function groupBy($field);
/**
* Sets a condition for an aggregated value.
*
* @param string $field
* The name of the field to aggregate by.
* @param string $function
* The aggregation function, for example COUNT or MIN.
* @param mixed $value
* The actual value of the field.
*
* @param $operator
* Possible values:
* - '=', '<>', '>', '>=', '<', '<=', 'STARTS_WITH', 'CONTAINS',
* 'ENDS_WITH': These operators expect $value to be a literal of the
* same type as the column.
* - 'IN', 'NOT IN': These operators expect $value to be an array of
* literals of the same type as the column.
* - 'BETWEEN': This operator expects $value to be an array of two literals
* of the same type as the column.
*
* @param string $langcode
* (optional) The language code.
*
* @return \Drupal\Core\Entity\Query\QueryAggregateInterface
* The called object.
*
* @see \Drupal\Core\Entity\Query\QueryInterface::condition().
*/
public function conditionAggregate($field, $function = NULL, $value = NULL, $operator = '=', $langcode = NULL);
/**
* Queries for the existence of a field.
*
* @param string $field
* The name of the field.
* @param string $function
* The aggregate function.
* @param $langcode
* (optional) The language code.
*
* @return \Drupal\Core\Entity\Query\QueryAggregateInterface
* The called object.
*/
public function existsAggregate($field, $function, $langcode = NULL);
/**
* Queries for the nonexistence of a field.
*
* @param string $field.
* The name of a field.
* @param string $function
* The aggregate function.
* @param string $langcode
* (optional) The language code.
*
* @return \Drupal\Core\Entity\Query\QueryAggregateInterface
* The called object.
*/
public function notExistsAggregate($field, $function, $langcode = NULL);
/**
* Creates an object holding a group of conditions.
*
* See andConditionAggregateGroup() and orConditionAggregateGroup() for more.
*
* @param string $conjunction
* - AND (default): this is the equivalent of andConditionAggregateGroup().
* - OR: this is the equivalent of andConditionAggregateGroup().
*
* @return ConditionInterface
* An object holding a group of conditions.
*/
public function conditionAggregateGroupFactory($conjunction = 'AND');
/**
* Sorts by an aggregated value.
*
* @param string $field
* The name of a field.
* @param string $function
* The aggregate function. This is only marked optional for interface
* compatibility, it is illegal to leave it out.
* @param string $direction
* The order of sorting, either DESC for descending of ASC for ascending.
* @param string $langcode
* (optional) The language code.
*
* @return \Drupal\Core\Entity\Query\QueryAggregateInterface
* The called object.
*/
public function sortAggregate($field, $function, $direction = 'ASC', $langcode = NULL);
/**
* Executes the aggregate query.
*
* @return array
* A list of result row arrays. Each result row contains the aggregate
* results as keys and also the groupBy columns as keys:
* @code
* $result = $query
* ->aggregate('nid', 'count')
* ->condition('status', 1)
* ->groupby('type')
* ->executeAggregate();
* @endcode
* Will return:
* @code
* $result[0] = array('count_nid' => 3, 'type' => 'page');
* $result[1] = array('count_nid' => 1, 'type' => 'poll');
* $result[2] = array('count_nid' => 4, 'type' => 'story');
* @endcode
*/
// public function execute();
}
......@@ -12,7 +12,7 @@
/**
* The base entity query class.
*/
abstract class QueryBase implements QueryInterface {
abstract class QueryBase {
/**
* The entity type this query runs against.
......@@ -22,7 +22,7 @@ abstract class QueryBase implements QueryInterface {
protected $entityType;
/**
* The sort data.
* The list of sorts.
*
* @var array
*/
......@@ -38,10 +38,38 @@ abstract class QueryBase implements QueryInterface {
/**
* Conditions.
*
* @var ConditionInterface
* @var \Drupal\Core\Entity\Query\ConditionInterface
*/
protected $condition;
/**
* The list of aggregate expressions.
*
* @var array
*/
protected $aggregate = array();
/**
* The list of columns to group on.
*
* @var array
*/
protected $groupBy = array();
/**
* Aggregate Conditions
*
* @var \Drupal\Core\Entity\Query\ConditionAggregateInterface
*/
protected $conditionAggregate;
/**
* The list of sorts over the aggregate results.
*
* @var array
*/
protected $sortAggregate = array();
/**
* The query range.
*
......@@ -49,6 +77,20 @@ abstract class QueryBase implements QueryInterface {
*/
protected $range = array();
/**
* The query metadata for alter purposes.
*
* @var array
*/
protected $alterMetaData;
/**
* The query tags.
*
* @var array
*/
protected $alterTags;
/**
* Whether access check is requested or not. Defaults to TRUE.
*
......@@ -81,6 +123,9 @@ public function __construct($entity_type, $conjunction) {
$this->entityType = $entity_type;
$this->conjunction = $conjunction;
$this->condition = $this->conditionGroupFactory($conjunction);
if ($this instanceof QueryAggregateInterface) {
$this->conditionAggregate = $this->conditionAggregateGroupFactory($conjunction);
}
}
/**
......@@ -142,8 +187,9 @@ public function orConditionGroup() {
/**
* Implements \Drupal\Core\Entity\Query\QueryInterface::sort().
*/
public function sort($property, $direction = 'ASC', $langcode = NULL) {
$this->sort[$property] = array(
public function sort($field, $direction = 'ASC', $langcode = NULL) {
$this->sort[] = array(
'field' => $field,
'direction' => $direction,
'langcode' => $langcode,
);
......@@ -283,4 +329,77 @@ public function addMetaData($key, $object) {
public function getMetaData($key) {
return isset($this->alterMetaData[$key]) ? $this->alterMetaData[$key] : NULL;
}
/**
* Implements \Drupal\Core\Entity\Query\QueryAggregateInterface::aggregate()
*/
public function aggregate($field, $function, $langcode = NULL, &$alias = NULL) {
if (!isset($alias)) {
$alias = $this->getAggregationAlias($field, $function);
}
$this->aggregate[$alias] = array(
'field' => $field,
'function' => $function,
'alias' => $alias,
'langcode' => $langcode,
);
return $this;
}
/**
* Implements \Drupal\Core\Entity\Query\QueryAggregateInterface::conditionAggregate().
*/
public function conditionAggregate($field, $function = NULL, $value = NULL, $operator = '=', $langcode = NULL) {
$this->aggregate($field, $function, $langcode);
$this->conditionAggregate->condition($field, $function, $value, $operator, $langcode);
return $this;
}
/**
* Implements \Drupal\Core\Entity\Query\QueryAggregateInterface::sortAggregate().
*/
public function sortAggregate($field, $function, $direction = 'ASC', $langcode = NULL) {
$alias = $this->getAggregationAlias($field, $function);
$this->sortAggregate[$alias] = array(
'field' => $field,
'function' => $function,
'direction' => $direction,
'langcode' => $langcode,
);
$this->aggregate($field, $function, $langcode, $alias);
return $this;
}
/**
* Implements \Drupal\Core\Entity\Query\QueryAggregateInterface::execute().
*/
public function groupBy($field, $langcode = NULL) {
$this->groupBy[] = array(
'field' => $field,
'langcode' => $langcode,
);
return $this;
}
/**
* Generates an alias for a field and it's aggregated function.
*
* @param string $field
* The field name used in the alias.
* @param string $function
* The aggregation function used in the alias.
*
* @return string
* The alias for the field.
*/
protected function getAggregationAlias($field, $function) {
return strtolower($field . '_'. $function);
}
}
......@@ -49,4 +49,21 @@ public function get($entity_type, $conjunction = 'AND') {
return $this->container->get($service_name)->get($entity_type, $conjunction, $this->entityManager);
}
/**
* Returns an aggregated query object for a given entity type.
*
* @param string $entity_type
* The entity type.
* @param string $conjunction
* - AND: all of the conditions on the query need to match.
* - OR: at least one of the conditions on the query need to match.
*
* @return \Drupal\Core\Entity\Query\QueryAggregateInterface
* The aggregated query object that can query the given entity type.
*/
public function getAggregate($entity_type, $conjunction = 'AND') {
$service_name = $this->entityManager->getStorageController($entity_type)->getQueryServicename();
return $this->container->get($service_name)->getAggregate($entity_type, $conjunction, $this->entityManager);
}
}
......@@ -56,6 +56,11 @@ public function notExists($field, $langcode = NULL) {
return $this->condition($field, NULL, 'IS NULL', $langcode);
}
/**
* Translates the string operators to SQL equivalents.
*
* @param array $condition
*/