Commit b9c4744e authored by catch's avatar catch

Issue #1801726 by chx, bojanz, plach: EntityFieldQuery v2.

parent a598aa46
......@@ -347,20 +347,9 @@ function hook_entity_delete(Drupal\Core\Entity\EntityInterface $entity) {
}
/**
* Alter or execute an Drupal\Core\Entity\EntityFieldQuery.
*
* @param Drupal\Core\Entity\EntityFieldQuery $query
* An EntityFieldQuery. One of the most important properties to be changed is
* EntityFieldQuery::executeCallback. If this is set to an existing function,
* this function will get the query as its single argument and its result
* will be the returned as the result of EntityFieldQuery::execute(). This can
* be used to change the behavior of EntityFieldQuery entirely. For example,
* the default implementation can only deal with one field storage engine, but
* it is possible to write a module that can query across field storage
* engines. Also, the default implementation presumes entities are stored in
* SQL, but the execute callback could instead query any other entity storage,
* local or remote.
* Alter or execute an Drupal\Core\Entity\Query\EntityQueryInterface.
*
* @param \Drupal\Core\Entity\Query\QueryInterface $query
* Note the $query->altered attribute which is TRUE in case the query has
* already been altered once. This happens with cloned queries.
* If there is a pager, then such a cloned query will be executed to count
......@@ -368,8 +357,8 @@ function hook_entity_delete(Drupal\Core\Entity\EntityInterface $entity) {
* ($query->pager && $query->count), allowing the driver to return 0 from
* the count query and disable the pager.
*/
function hook_entity_query_alter(Drupal\Core\Entity\EntityFieldQuery $query) {
$query->executeCallback = 'my_module_query_callback';
function hook_entity_query_alter(\Drupal\Core\Entity\Query\QueryInterface $query) {
// @todo: code example.
}
/**
......
......@@ -5,10 +5,7 @@
* Entity API for handling entities like nodes or users.
*/
use \InvalidArgumentException;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Entity\EntityFieldQuery;
use Drupal\Core\Entity\EntityMalformedException;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityInterface;
......@@ -123,7 +120,7 @@ function entity_info_cache_clear() {
* @see entity_load_multiple()
* @see Drupal\Core\Entity\EntityStorageControllerInterface
* @see Drupal\Core\Entity\DatabaseStorageController
* @see Drupal\Core\Entity\EntityFieldQuery
* @see Drupal\Core\Entity\Query\QueryInterface
*/
function entity_load($entity_type, $id, $reset = FALSE) {
$entities = entity_load_multiple($entity_type, array($id), $reset);
......@@ -227,7 +224,7 @@ function entity_load_by_uuid($entity_type, $uuid, $reset = FALSE) {
* @see hook_entity_info()
* @see Drupal\Core\Entity\EntityStorageControllerInterface
* @see Drupal\Core\Entity\DatabaseStorageController
* @see Drupal\Core\Entity\EntityFieldQuery
* @see Drupal\Core\Entity\Query\QueryInterface
*/
function entity_load_multiple($entity_type, array $ids = NULL, $reset = FALSE) {
if ($reset) {
......@@ -576,3 +573,17 @@ function entity_view(EntityInterface $entity, $view_mode, $langcode = NULL) {
function entity_view_multiple(array $entities, $view_mode, $langcode = NULL) {
return entity_render_controller(reset($entities)->entityType())->viewMultiple($entities, $view_mode, $langcode);
}
/**
* Returns the entity query 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.
*/
function entity_query($entity_type, $conjunction = 'AND') {
return drupal_container()->get('entity.query')->get($entity_type, $conjunction);
}
......@@ -393,4 +393,11 @@ protected function invokeHook($hook, EntityInterface $entity) {
// Invoke the respective entity-level hook.
module_invoke_all('entity_' . $hook, $entity, $this->entityType);
}
/**
* Implements Drupal\Core\Entity\EntityStorageControllerInterface::getQueryServicename().
*/
public function getQueryServicename() {
throw new \LogicException('Querying configuration entities is not supported.');
}
}
......@@ -64,6 +64,10 @@ public function build(ContainerBuilder $container) {
->addArgument(new Reference('database'))
->addArgument(new Reference('lock'));
// Add the entity query factory.
$container->register('entity.query', 'Drupal\Core\Entity\Query\QueryFactory')
->addArgument(new Reference('service_container'));
$container->register('router.dumper', 'Drupal\Core\Routing\MatcherDumper')
->addArgument(new Reference('database'));
$container->register('router.builder', 'Drupal\Core\Routing\RouteBuilder')
......
......@@ -631,7 +631,7 @@ public function getDriverClass($class) {
* @param $options
* An array of options on the query.
*
* @return Drupal\Core\Database\Query\SelectInterface
* @return \Drupal\Core\Database\Query\SelectInterface
* An appropriate SelectQuery object for this database connection. Note that
* it may be a driver-specific subclass of SelectQuery, depending on the
* driver.
......
......@@ -124,8 +124,9 @@ class Select extends Query implements SelectInterface {
public function __construct($table, $alias = NULL, Connection $connection, $options = array()) {
$options['return'] = Database::RETURN_STATEMENT;
parent::__construct($connection, $options);
$this->where = new Condition('AND');
$this->having = new Condition('AND');
$conjunction = isset($options['conjunction']) ? $options['conjunction'] : 'AND';
$this->where = new Condition($conjunction);
$this->having = new Condition($conjunction);
$this->addJoin(NULL, $table, $alias);
}
......
......@@ -8,6 +8,7 @@
namespace Drupal\Core\Entity;
use PDO;
use Drupal\Core\Entity\Query\QueryInterface;
use Exception;
use Drupal\Component\Uuid\Uuid;
......@@ -266,30 +267,24 @@ public function deleteRevision($revision_id) {
*/
public function loadByProperties(array $values = array()) {
// Build a query to fetch the entity IDs.
$entity_query = new EntityFieldQuery();
$entity_query->entityCondition('entity_type', $this->entityType);
$entity_query = entity_query($this->entityType);
$this->buildPropertyQuery($entity_query, $values);
$result = $entity_query->execute();
if (empty($result[$this->entityType])) {
return array();
}
// Load and return the found entities.
return $this->load(array_keys($result[$this->entityType]));
return $result ? $this->load($result) : array();
}
/**
* Builds an entity query.
*
* @param Drupal\Core\Entity\EntityFieldQuery $entity_query
* EntityFieldQuery instance.
* @param \Drupal\Core\Entity\Query\QueryInterface $entity_query
* EntityQuery instance.
* @param array $values
* An associative array of properties of the entity, where the keys are the
* property names and the values are the values those properties must have.
*/
protected function buildPropertyQuery(EntityFieldQuery $entity_query, array $values) {
protected function buildPropertyQuery(QueryInterface $entity_query, array $values) {
foreach ($values as $name => $value) {
$entity_query->propertyCondition($name, $value);
$entity_query->condition($name, $value);
}
}
......@@ -355,6 +350,7 @@ protected function buildQuery($ids, $revision_id = FALSE) {
if ($ids) {
$query->condition("base.{$this->idKey}", $ids, 'IN');
}
return $query;
}
......@@ -701,4 +697,11 @@ public function getFieldDefinitions(array $constraints) {
public function baseFieldDefinitions() {
return array();
}
/**
* Implements Drupal\Core\Entity\EntityStorageControllerInterface::getQueryServiceName().
*/
public function getQueryServiceName() {
return 'entity.query.field_sql_storage';
}
}
This diff is collapsed.
......@@ -71,7 +71,7 @@ public function deleteRevision($revision_id);
* @return array
* An array of entity objects indexed by their ids.
*/
public function loadByProperties(array $values);
public function loadByProperties(array $values = array());
/**
* Constructs a new entity object, without permanently saving it.
......@@ -134,7 +134,7 @@ public function save(EntityInterface $entity);
* An array of field definitions of entity fields, keyed by field
* name. In addition to the typed data definition keys as described at
* typed_data()->create() the follow keys are supported:
* - queryable: Whether the field is queryable via EntityFieldQuery.
* - queryable: Whether the field is queryable via QueryInterface.
* Defaults to TRUE if 'computed' is FALSE or not set, to FALSE otherwise.
* - translatable: Whether the field is translatable. Defaults to FALSE.
* - configurable: A boolean indicating whether the field is configurable
......@@ -144,4 +144,12 @@ public function save(EntityInterface $entity);
* @see typed_data()
*/
public function getFieldDefinitions(array $constraints);
/**
* Gets the name of the service for the query for this entity storage.
*
* @return string
*/
public function getQueryServicename();
}
<?php
/**
* @file
* Definition of Drupal\Core\Entity\Query\ConditionBase.
*/
namespace Drupal\Core\Entity\Query;
/**
* Common code for all implementations of the entity query condition interface.
*/
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;
}
/**
* Implements Drupal\Core\Entity\Query\ConditionInterface::compile().
*/
public function condition($field, $value = NULL, $operator = NULL, $langcode = NULL) {
$this->conditions[] = array(
'field' => $field,
'value' => $value,
'operator' => $operator,
'langcode' => $langcode,
);
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
* Definition of Drupal\Core\Entity\ConditionInterface.
*/
namespace Drupal\Core\Entity\Query;
/**
* Defines the entity query condition interface.
*/
interface ConditionInterface {
/**
* Gets the current conjunction.
*
* @return string
* Can be AND or OR.
*/
public function getConjunction();
/**
* Implements Countable::count().
*
* Returns the size of this conditional. The size of the conditional is the
* size of its conditional array minus one, because one element is the the
* conjunction.
*/
public function count();
/**
* Adds a condition.
*
* @param string $field
* @param mixed $value
* @param string $operator
* @param string $langcode
* @return ConditionInterface
* @see \Drupal\Core\Entity\Query\QueryInterface::condition()
*/
public function condition($field, $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, $langcode = NULL);
/**
* Queries for the existence of a field.
*
* @param string $field
* @return ConditionInterface;
* @see \Drupal\Core\Entity\Query\QueryInterface::notexists()
*/
public function notExists($field, $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);
}
<?php
/**
* @file
* Definition of Drupal\Core\Entity\Query\QueryBase.
*/
namespace Drupal\Core\Entity\Query;
use Drupal\Core\Database\Query\PagerSelectExtender;
/**
* The base entity query class.
*/
abstract class QueryBase implements QueryInterface {
/**
* The entity type this query runs against.
*
* @var string
*/
protected $entityType;
/**
* The sort data.
*
* @var array
*/
protected $sort = array();
/**
* TRUE if this is a count query, FALSE if it isn't.
*
* @var boolean
*/
protected $count = FALSE;
/**
* Conditions.
*
* @var ConditionInterface
*/
protected $condition;
/**
* The query range.
*
* @var array
*/
protected $range = array();
/**
* Whether access check is requested or not. Defaults to TRUE.
*
* @var bool
*/
protected $accessCheck = TRUE;
/**
* Flag indicating whether to query the current revision or all revisions.
*
* Can be either FIELD_LOAD_CURRENT or FIELD_LOAD_REVISION.
*
* @var string
*/
protected $age = FIELD_LOAD_CURRENT;
/**
* The query pager data.
*
* @var array
*
* @see Query::pager()
*/
protected $pager = array();
/**
* Constructs this object.
*/
public function __construct($entity_type, $conjunction) {
$this->entityType = $entity_type;
$this->conjunction = $conjunction;
$this->condition = $this->conditionGroupFactory($conjunction);
}
/**
* Implements Drupal\Core\Entity\Query\QueryInterface::getEntityType().
*/
public function getEntityType() {
return $this->entityType;
}
/**
* Implements Drupal\Core\Entity\Query\QueryInterface::condition().
*/
public function condition($property, $value = NULL, $operator = NULL, $langcode = NULL) {
$this->condition->condition($property, $value, $operator, $langcode);
return $this;
}
/**
* Implements Drupal\Core\Entity\Query\QueryInterface::exists().
*/
public function exists($property, $langcode = NULL) {
$this->condition->exists($property, $langcode);
return $this;
}
/**
* Implements Drupal\Core\Entity\Query\QueryInterface::notExists().
*/
public function notExists($property, $langcode = NULL) {
$this->condition->notExists($property, $langcode);
return $this;
}
/**
* Implements Drupal\Core\Entity\Query\QueryInterface::range().
*/
public function range($start = NULL, $length = NULL) {
$this->range = array(
'start' => $start,
'length' => $length,
);
return $this;
}
/**
* Implements Drupal\Core\Entity\Query\QueryInterface::andConditionGroup().
*/
public function andConditionGroup() {
return $this->conditionGroupFactory('and');
}
/**
* Implements Drupal\Core\Entity\Query\QueryInterface::orConditionGroup().
*/
public function orConditionGroup() {
return $this->conditionGroupFactory('or');
}
/**
* Implements Drupal\Core\Entity\Query\QueryInterface::sort().
*/
public function sort($property, $direction = 'ASC', $langcode = NULL) {
$this->sort[$property] = array(
'direction' => $direction,
'langcode' => $langcode,
);
return $this;
}
/**
* Implements Drupal\Core\Entity\Query\QueryInterface::count().
*/
public function count() {
$this->count = TRUE;
return $this;
}
/**
* Implements Drupal\Core\Entity\Query\QueryInterface::accessCheck().
*/
public function accessCheck($access_check = TRUE) {
$this->accessCheck = $access_check;
return $this;
}
/**
* Implements Drupal\Core\Entity\Query\QueryInterface::age().
*/
public function age($age = FIELD_LOAD_CURRENT) {
$this->age = $age;
return $this;
}
/**
* Implements Drupal\Core\Entity\Query\QueryInterface::pager().
*/
public function pager($limit = 10, $element = NULL) {
// Even when not using SQL, storing the element PagerSelectExtender is as
// good as anywhere else.
if (!isset($element)) {
$element = PagerSelectExtender::$maxElement++;
}
elseif ($element >= PagerSelectExtender::$maxElement) {
PagerSelectExtender::$maxElement = $element + 1;
}
$this->pager = array(
'limit' => $limit,
'element' => $element,
);
return $this;
}
/**
* Gets the total number of results and initialize a pager for the query.
*
* The pager can be disabled by either setting the pager limit to 0, or by
* setting this query to be a count query.
*/
protected function initializePager() {
if ($this->pager && !empty($this->pager['limit']) && !$this->count) {
$page = pager_find_page($this->pager['element']);
$count_query = clone $this;
$this->pager['total'] = $count_query->count()->execute();
$this->pager['start'] = $page * $this->pager['limit'];
pager_default_initialize($this->pager['total'], $this->pager['limit'], $this->pager['element']);
$this->range($this->pager['start'], $this->pager['limit']);
}
}
/**
* Implements Drupal\Core\Entity\Query\QueryInterface::tableSort().
*/
public function tableSort(&$headers) {
// If 'field' is not initialized, the header columns aren't clickable.
foreach ($headers as $key =>$header) {
if (is_array($header) && isset($header['specifier'])) {
$headers[$key]['field'] = '';
}
}
$order = tablesort_get_order($headers);
$direction = tablesort_get_sort($headers);
foreach ($headers as $header) {
if (is_array($header) && ($header['data'] == $order['name'])) {
$this->sort($header['specifier'], $direction, isset($header['langcode']) ? $header['langcode'] : NULL);
}
}
return $this;
}
}
......@@ -2,18 +2,16 @@
/**
* @file
* Definition of Drupal\Core\Entity\EntityFieldQueryException.
* Definition of Drupal\Core\Entity\QueryException.
*/
namespace Drupal\Core\Entity;
use Exception;
namespace Drupal\Core\Entity\Query;
/**
* Exception thrown by EntityFieldQuery() on unsupported query syntax.
* Exception thrown by Query() on unsupported query syntax.
*
* Some storage modules might not support the full range of the syntax for
* conditions, and will raise an EntityFieldQueryException when an unsupported
* conditions, and will raise a QueryException when an unsupported
* condition was specified.
*/
class EntityFieldQueryException extends Exception { }
class QueryException extends \Exception { }
<?php
/**
* @file
* Definition of Drupal\Core\Entity\Query\QueryFactory.
*/
namespace Drupal\Core\Entity\Query;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Factory class Creating entity query objects.
*/
class QueryFactory {
/**
* var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
/**
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
*/
function __construct(ContainerInterface $container) {
$this->container = $container;
}
/**
* @param string $entity_type
* @param string $conjunction
* @return QueryInterface
*/
public function get($entity_type, $conjunction = 'AND') {
$service_name = entity_get_controller($entity_type)->getQueryServicename();
return $this->container->get($service_name)->get($entity_type, $conjunction);
}
}
<?php
/**
* @file
* Definition of Drupal\Core\Entity\QueryInterface.
*/
namespace Drupal\Core\Entity\Query;
/**
* Interface for entity queries.
*
* Never instantiate classes implementing this interface directly. Always use
* the QueryFactory class.
*/
interface QueryInterface {
/**
* Gets the entity type for this query.
*
* @return string
*/
public function getEntityType();
/**
* Add a condition to the query or a condition group.
*
* @param $field
* Name of the field being queried.