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') { ...@@ -729,6 +729,23 @@ function entity_query($entity_type, $conjunction = 'AND') {
return drupal_container()->get('entity.query')->get($entity_type, $conjunction); 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. * Generic access callback for entity pages.
* *
......
...@@ -11,11 +11,12 @@ ...@@ -11,11 +11,12 @@
use Drupal\Core\Entity\EntityManager; use Drupal\Core\Entity\EntityManager;
use Drupal\Core\Entity\EntityStorageControllerInterface; use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\Core\Entity\Query\QueryBase; use Drupal\Core\Entity\Query\QueryBase;
use Drupal\Core\Entity\Query\QueryInterface;
/** /**
* Defines the entity query for configuration entities. * Defines the entity query for configuration entities.
*/ */
class Query extends QueryBase { class Query extends QueryBase implements QueryInterface {
/** /**
* Stores the entity manager. * Stores the entity manager.
...@@ -93,10 +94,11 @@ public function execute() { ...@@ -93,10 +94,11 @@ public function execute() {
$result = $this->condition->compile($configs); $result = $this->condition->compile($configs);
// Apply sort settings. // Apply sort settings.
foreach ($this->sort as $property => $sort) { foreach ($this->sort as $sort) {
$direction = $sort['direction'] == 'ASC' ? -1 : 1; $direction = $sort['direction'] == 'ASC' ? -1 : 1;
uasort($result, function($a, $b) use ($property, $direction) { $field = $sort['field'];
return ($a[$property] <= $b[$property]) ? $direction : -$direction; uasort($result, function($a, $b) use ($field, $direction) {
return ($a[$field] <= $b[$field]) ? $direction : -$direction;
}); });
} }
......
...@@ -10,6 +10,8 @@ ...@@ -10,6 +10,8 @@
use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\StorageInterface; use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Entity\EntityManager; 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. * Provides a factory for creating entity query objects for the config backend.
...@@ -34,7 +36,7 @@ public function __construct(StorageInterface $config_storage) { ...@@ -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 * @param string $entity_type
* The entity type for the query. * The entity type for the query.
...@@ -50,4 +52,24 @@ public function get($entity_type, $conjunction, EntityManager $entity_manager) { ...@@ -50,4 +52,24 @@ public function get($entity_type, $conjunction, EntityManager $entity_manager) {
return new Query($entity_type, $conjunction, $entity_manager, $this->configStorage); 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 @@ ...@@ -8,40 +8,9 @@
namespace Drupal\Core\Entity\Query; 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 { abstract class ConditionBase extends ConditionFundamentals 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;
}
/** /**
* Implements \Drupal\Core\Entity\Query\ConditionInterface::compile(). * Implements \Drupal\Core\Entity\Query\ConditionInterface::compile().
...@@ -56,23 +25,4 @@ public function condition($field, $value = NULL, $operator = NULL, $langcode = N ...@@ -56,23 +25,4 @@ public function condition($field, $value = NULL, $operator = NULL, $langcode = N
return $this; 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(); ...@@ -32,7 +32,7 @@ public function count();
/** /**
* Adds a condition. * Adds a condition.
* *
* @param string $field * @param string|\Drupal\Core\Entity\Query\ConditionInterface $field
* @param mixed $value * @param mixed $value
* @param string $operator * @param string $operator
* @param string $langcode * @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 @@ ...@@ -12,7 +12,7 @@
/** /**
* The base entity query class. * The base entity query class.
*/ */
abstract class QueryBase implements QueryInterface { abstract class QueryBase {
/** /**
* The entity type this query runs against. * The entity type this query runs against.
...@@ -22,7 +22,7 @@ abstract class QueryBase implements QueryInterface { ...@@ -22,7 +22,7 @@ abstract class QueryBase implements QueryInterface {
protected $entityType; protected $entityType;
/** /**
* The sort data. * The list of sorts.
* *
* @var array * @var array
*/ */
...@@ -38,10 +38,38 @@ abstract class QueryBase implements QueryInterface { ...@@ -38,10 +38,38 @@ abstract class QueryBase implements QueryInterface {
/** /**
* Conditions. * Conditions.
* *
* @var ConditionInterface * @var \Drupal\Core\Entity\Query\ConditionInterface
*/ */
protected $condition; 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. * The query range.
* *
...@@ -49,6 +77,20 @@ abstract class QueryBase implements QueryInterface { ...@@ -49,6 +77,20 @@ abstract class QueryBase implements QueryInterface {
*/ */
protected $range = array(); 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. * Whether access check is requested or not. Defaults to TRUE.
* *
...@@ -81,6 +123,9 @@ public function __construct($entity_type, $conjunction) { ...@@ -81,6 +123,9 @@ public function __construct($entity_type, $conjunction) {
$this->entityType = $entity_type; $this->entityType = $entity_type;
$this->conjunction = $conjunction; $this->conjunction = $conjunction;
$this->condition = $this->conditionGroupFactory($conjunction); $this->condition = $this->conditionGroupFactory($conjunction);
if ($this instanceof QueryAggregateInterface) {
$this->conditionAggregate = $this->conditionAggregateGroupFactory($conjunction);
}
} }
/** /**
...@@ -142,8 +187,9 @@ public function orConditionGroup() { ...@@ -142,8 +187,9 @@ public function orConditionGroup() {
/** /**
* Implements \Drupal\Core\Entity\Query\QueryInterface::sort(). * Implements \Drupal\Core\Entity\Query\QueryInterface::sort().
*/ */
public function sort($property, $direction = 'ASC', $langcode = NULL) { public function sort($field, $direction = 'ASC', $langcode = NULL) {
$this->sort[$property] = array( $this->sort[] = array(
'field' => $field,