diff --git a/core/includes/entity.api.php b/core/includes/entity.api.php index e9fb855..fec3fc8 100644 --- a/core/includes/entity.api.php +++ b/core/includes/entity.api.php @@ -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. } /** diff --git a/core/includes/entity.inc b/core/includes/entity.inc index 48ee728..d87793f 100644 --- a/core/includes/entity.inc +++ b/core/includes/entity.inc @@ -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); +} diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php b/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php index 07849c6..f317d7c 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php @@ -374,4 +374,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.'); + } } diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index aeb270c..46ce0e1 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -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') diff --git a/core/lib/Drupal/Core/Database/Connection.php b/core/lib/Drupal/Core/Database/Connection.php index 1960874..462699b 100644 --- a/core/lib/Drupal/Core/Database/Connection.php +++ b/core/lib/Drupal/Core/Database/Connection.php @@ -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. diff --git a/core/lib/Drupal/Core/Database/Query/Select.php b/core/lib/Drupal/Core/Database/Query/Select.php index 22aca8f..1bfe9a0 100644 --- a/core/lib/Drupal/Core/Database/Query/Select.php +++ b/core/lib/Drupal/Core/Database/Query/Select.php @@ -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); } diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php index 6342502..d496ea0 100644 --- a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php +++ b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php @@ -8,6 +8,7 @@ namespace Drupal\Core\Entity; use PDO; +use Drupal\Core\Entity\Query\QueryInterface; use Exception; use Drupal\Component\Uuid\Uuid; @@ -264,30 +265,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); } } @@ -353,6 +348,7 @@ protected function buildQuery($ids, $revision_id = FALSE) { if ($ids) { $query->condition("base.{$this->idKey}", $ids, 'IN'); } + return $query; } @@ -699,4 +695,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'; + } } diff --git a/core/lib/Drupal/Core/Entity/EntityFieldQuery.php b/core/lib/Drupal/Core/Entity/EntityFieldQuery.php deleted file mode 100644 index 0c054ce..0000000 --- a/core/lib/Drupal/Core/Entity/EntityFieldQuery.php +++ /dev/null @@ -1,1168 +0,0 @@ -changed), field values, and generic entity meta data (bundle, - * entity type, entity id, and revision ID). It is not possible to query across - * multiple entity types. For example, there is no facility to find published - * nodes written by users created in the last hour, as this would require - * querying both node->status and user->created. - * - * Normally we would not want to have public properties on the object, as that - * allows the object's state to become inconsistent too easily. However, this - * class's standard use case involves primarily code that does need to have - * direct access to the collected properties in order to handle alternate - * execution routines. We therefore use public properties for simplicity. Note - * that code that is simply creating and running a field query should still use - * the appropriate methods to add conditions on the query. - * - * Storage engines are not required to support every type of query. By default, - * an EntityFieldQueryException will be raised if an unsupported condition is - * specified or if the query has field conditions or sorts that are stored in - * different field storage engines. However, this logic can be overridden in - * hook_entity_query_alter(). - * - * Also note that this query does not automatically respect entity access - * restrictions. Node access control is performed by the SQL storage engine but - * other storage engines might not do this. - */ -class EntityFieldQuery { - - /** - * Indicates that both deleted and non-deleted fields should be returned. - * - * @see Drupal\Core\Entity\EntityFieldQuery::deleted() - */ - const RETURN_ALL = NULL; - - /** - * TRUE if the query has already been altered, FALSE if it hasn't. - * - * Used in alter hooks to check for cloned queries that have already been - * altered prior to the clone (for example, the pager count query). - * - * @var boolean - */ - public $altered = FALSE; - - /** - * Associative array of entity-generic metadata conditions. - * - * @var array - * - * @see Drupal\Core\Entity\EntityFieldQuery::entityCondition() - */ - public $entityConditions = array(); - - /** - * List of field conditions. - * - * @var array - * - * @see Drupal\Core\Entity\EntityFieldQuery::fieldCondition() - */ - public $fieldConditions = array(); - - /** - * List of field meta conditions (language and delta). - * - * Field conditions operate on columns specified by hook_field_schema(), - * the meta conditions operate on columns added by the system: delta - * and language. These can not be mixed with the field conditions because - * field columns can have any name including delta and language. - * - * @var array - * - * @see Drupal\Core\Entity\EntityFieldQuery::fieldLanguageCondition() - * @see Drupal\Core\Entity\EntityFieldQuery::fieldDeltaCondition() - */ - public $fieldMetaConditions = array(); - - /** - * List of property conditions. - * - * @var array - * - * @see Drupal\Core\Entity\EntityFieldQuery::propertyCondition() - */ - public $propertyConditions = array(); - - /** - * List of order clauses. - * - * @var array - */ - public $order = array(); - - /** - * The query range. - * - * @var array - * - * @see Drupal\Core\Entity\EntityFieldQuery::range() - */ - public $range = array(); - - /** - * The query pager data. - * - * @var array - * - * @see Drupal\Core\Entity\EntityFieldQuery::pager() - */ - public $pager = array(); - - /** - * Query behavior for deleted data. - * - * TRUE to return only deleted data, FALSE to return only non-deleted data, - * EntityFieldQuery::RETURN_ALL to return everything. - * - * @see Drupal\Core\Entity\EntityFieldQuery::deleted() - */ - public $deleted = FALSE; - - /** - * A list of field arrays used. - * - * Field names passed to EntityFieldQuery::fieldCondition() and - * EntityFieldQuery::fieldOrderBy() are run through field_info_field() before - * stored in this array. This way, the elements of this array are field - * arrays. - * - * @var array - */ - public $fields = array(); - - /** - * A list of used properties keyed by group. - * - * @var array - */ - public $properties = array(); - - /** - * TRUE if this is a count query, FALSE if it isn't. - * - * @var boolean - */ - public $count = FALSE; - - /** - * Flag indicating whether this is querying current or all revisions. - * - * @var int - * - * @see Drupal\Core\Entity\EntityFieldQuery::age() - */ - public $age = FIELD_LOAD_CURRENT; - - /** - * A list of the tags added to this query. - * - * @var array - * - * @see Drupal\Core\Entity\EntityFieldQuery::addTag() - */ - public $tags = array(); - - /** - * A list of metadata added to this query. - * - * @var array - * - * @see Drupal\Core\Entity\EntityFieldQuery::addMetaData() - */ - public $metaData = array(); - - /** - * The ordered results. - * - * @var array - * - * @see Drupal\Core\Entity\EntityFieldQuery::execute(). - */ - public $orderedResults = array(); - - /** - * The method executing the query, if it is overriding the default. - * - * @var string - * - * @see Drupal\Core\Entity\EntityFieldQuery::execute() - */ - public $executeCallback = ''; - - /** - * Adds a condition on entity-generic metadata. - * - * If the overall query contains only entity conditions or ordering, or if - * there are property conditions, then specifying the entity type is - * mandatory. If there are field conditions or ordering but no property - * conditions or ordering, then specifying an entity type is optional. While - * the field storage engine might support field conditions on more than one - * entity type, there is no way to query across multiple entity base tables by - * default. To specify the entity type, pass in 'entity_type' for $name, - * the type as a string for $value, and no $operator (it's disregarded). - * - * 'bundle', 'revision_id' and 'entity_id' have no such restrictions. - * - * Note: The "comment" entity type does not support bundle conditions. - * - * @param $name - * 'entity_type', 'bundle', 'revision_id' or 'entity_id'. - * @param $value - * The value for $name. In most cases, this is a scalar. For more complex - * options, it is an array. The meaning of each element in the array is - * dependent on $operator. - * @param $operator - * Possible values: - * - '=', '<>', '>', '>=', '<', '<=', 'STARTS_WITH', 'CONTAINS': 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. - * The operator can be omitted, and will default to 'IN' if the value is an - * array, or to '=' otherwise. - * - * @return Drupal\Core\Entity\EntityFieldQuery - * The called object. - */ - public function entityCondition($name, $value, $operator = NULL) { - $this->entityConditions[$name] = array( - 'value' => $value, - 'operator' => $operator, - ); - return $this; - } - - /** - * Adds a condition on field values. - * - * Note that entities with empty field values will be excluded from the - * EntityFieldQuery results when using this method. - * - * @param $field - * Either a field name or a field array. - * @param $column - * The column that should hold the value to be matched. - * @param $value - * The value to test the column value against. - * @param $operator - * The operator to be used to test the given value. - * @param $delta_group - * An arbitrary identifier: conditions in the same group must have the same - * $delta_group. - * @param $langcode_group - * An arbitrary identifier: conditions in the same group must have the same - * $langcode_group. - * - * @return Drupal\Core\Entity\EntityFieldQuery - * The called object. - * - * @see Drupal\Core\Entity\EntityFieldQuery::addFieldCondition() - * @see Drupal\Core\Entity\EntityFieldQuery::deleted() - */ - public function fieldCondition($field, $column = NULL, $value = NULL, $operator = NULL, $delta_group = NULL, $langcode_group = NULL) { - return $this->addFieldCondition($this->fieldConditions, $field, $column, $value, $operator, $delta_group, $langcode_group); - } - - /** - * Adds a condition on the field language column. - * - * @param $field - * Either a field name or a field array. - * @param $value - * The value to test the column value against. - * @param $operator - * The operator to be used to test the given value. - * @param $delta_group - * An arbitrary identifier: conditions in the same group must have the same - * $delta_group. - * @param $langcode_group - * An arbitrary identifier: conditions in the same group must have the same - * $langcode_group. - * - * @return Drupal\Core\Entity\EntityFieldQuery - * The called object. - * - * @see Drupal\Core\Entity\EntityFieldQuery::addFieldCondition() - * @see Drupal\Core\Entity\EntityFieldQuery::deleted() - */ - public function fieldLanguageCondition($field, $value = NULL, $operator = NULL, $delta_group = NULL, $langcode_group = NULL) { - return $this->addFieldCondition($this->fieldMetaConditions, $field, 'langcode', $value, $operator, $delta_group, $langcode_group); - } - - /** - * Adds a condition on the field delta column. - * - * @param $field - * Either a field name or a field array. - * @param $value - * The value to test the column value against. - * @param $operator - * The operator to be used to test the given value. - * @param $delta_group - * An arbitrary identifier: conditions in the same group must have the same - * $delta_group. - * @param $langcode_group - * An arbitrary identifier: conditions in the same group must have the same - * $langcode_group. - * - * @return Drupal\Core\Entity\EntityFieldQuery - * The called object. - * - * @see Drupal\Core\Entity\EntityFieldQuery::addFieldCondition() - * @see Drupal\Core\Entity\EntityFieldQuery::deleted() - */ - public function fieldDeltaCondition($field, $value = NULL, $operator = NULL, $delta_group = NULL, $langcode_group = NULL) { - return $this->addFieldCondition($this->fieldMetaConditions, $field, 'delta', $value, $operator, $delta_group, $langcode_group); - } - - /** - * Adds the given condition to the proper condition array. - * - * @param $conditions - * A reference to an array of conditions. - * @param $field - * Either a field name or a field array. - * @param $column - * A column defined in the hook_field_schema() of this field. If this is - * omitted then the query will find only entities that have data in this - * field, using the entity and property conditions if there are any. - * @param $value - * The value to test the column value against. In most cases, this is a - * scalar. For more complex options, it is an array. The meaning of each - * element in the array is dependent on $operator. - * @param $operator - * Possible values: - * - '=', '<>', '>', '>=', '<', '<=', 'STARTS_WITH', 'CONTAINS': 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. - * The operator can be omitted, and will default to 'IN' if the value is an - * array, or to '=' otherwise. - * @param $delta_group - * An arbitrary identifier: conditions in the same group must have the same - * $delta_group. For example, let's presume a multivalue field which has - * two columns, 'color' and 'shape', and for entity id 1, there are two - * values: red/square and blue/circle. Entity ID 1 does not have values - * corresponding to 'red circle', however if you pass 'red' and 'circle' as - * conditions, it will appear in the results - by default queries will run - * against any combination of deltas. By passing the conditions with the - * same $delta_group it will ensure that only values attached to the same - * delta are matched, and entity 1 would then be excluded from the results. - * @param $langcode_group - * An arbitrary identifier: conditions in the same group must have the same - * $langcode_group. - * - * @return Drupal\Core\Entity\EntityFieldQuery - * The called object. - */ - protected function addFieldCondition(&$conditions, $field, $column = NULL, $value = NULL, $operator = NULL, $delta_group = NULL, $langcode_group = NULL) { - if (is_scalar($field)) { - $field_definition = field_info_field($field); - if (empty($field_definition)) { - throw new EntityFieldQueryException(t('Unknown field: @field_name', array('@field_name' => $field))); - } - $field = $field_definition; - } - // Ensure the same index is used for field conditions as for fields. - $index = count($this->fields); - $this->fields[$index] = $field; - if (isset($column)) { - $conditions[$index] = array( - 'field' => $field, - 'column' => $column, - 'value' => $value, - 'operator' => $operator, - 'delta_group' => $delta_group, - 'langcode_group' => $langcode_group, - ); - } - return $this; - } - - /** - * Adds a condition on an entity-specific property. - * - * An $entity_type must be specified by calling - * EntityFieldCondition::entityCondition('entity_type', $entity_type) before - * executing the query. Also, by default only entities stored in SQL are - * supported; however, EntityFieldQuery::executeCallback can be set to handle - * different entity storage. - * - * @param $column - * A column defined in the hook_schema() of the base table of the entity. - * @param $value - * The value to test the field against. In most cases, this is a scalar. For - * more complex options, it is an array. The meaning of each element in the - * array is dependent on $operator. - * @param $operator - * Possible values: - * - '=', '<>', '>', '>=', '<', '<=', 'STARTS_WITH', 'CONTAINS': 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. - * The operator can be omitted, and will default to 'IN' if the value is an - * array, or to '=' otherwise. - * @param $langcode_group - * (optional) An arbitrary identifier: conditions in the same group must - * have the same group identifier. This is used to group the condition with - * a related set of other property conditions and meta conditions. By - * default all conditions belong to the same group. - * - * @return Drupal\Core\Entity\EntityFieldQuery - * The called object. - * - * @see Drupal\entity\EntityFieldQuery::propertyLanguageCondition() - */ - public function propertyCondition($column, $value, $operator = NULL, $langcode_group = 0) { - $this->properties[$langcode_group][$column] = $column; - $this->propertyConditions[] = array( - 'column' => $column, - 'value' => $value, - 'operator' => $operator, - 'langcode_group' => $langcode_group, - ); - return $this; - } - - - /** - * Adds a condition on the property language. - * - * Since the entity storage controller may support multilingual properties, - * there may be cases where different conditions on different languages may be - * needed. This method allows to specify which language a particular set of - * conditions and order settings should have. For example: - * @code - * $query = new EntityFieldQuery(); - * $query->entityCondition('entity_type', 'entity_test'); - * // Find all English entities. - * $query->entityCondition('langcode', 'en'); - * // Having the specified English values for uid and name. - * $query->propertyCondition('uid', $uid, '=', 'original'); - * $query->propertyCondition('name', $name, '=', 'original'); - * $query->propertyLanguageCondition('en', '=', 'original'); - * // And having the specified Italian value for name. - * $query->propertyCondition('name', $name_it, '=', 'translation'); - * $query->propertyLanguageCondition('it', '=', 'translation'); - * // Order the result set by the English name. - * $query->propertyOrderBy('name', 'ASC', 'original'); - * $result = $query->execute(); - * @endcode - * Without specifiying two different language groupings there would be no way - * to apply both name conditions, as they would mutually exclude each other. - * - * @param $langcode - * The language code that the properties belonging to the language group - * should match. - * @param $operator - * The operator to be used to test the given value. - * @param $langcode_group - * (optional) An arbitrary identifier: conditions in the same group must - * have the same group identifier. This is used to group the language meta - * condition with a related set of property conditions. By default all - * conditions belong to the same group. - * - * @return Drupal\entity\EntityFieldQuery - * The called object. - * - * @see Drupal\entity\EntityFieldQuery::addFieldCondition() - * @see Drupal\entity\EntityFieldQuery::deleted() - */ - public function propertyLanguageCondition($langcode = NULL, $operator = NULL, $langcode_group = 0) { - // We have a separate method here to ensure there is a distinction at API - // level between properties and metadata, even if from the implementation - // perspective both conditions are implemented the same way. However this - // might not be the case in other storages. - // @todo Actually we could also implement the same functionality and keep - // this distinction by using language codes as group identifiers: - // - // $query->propertyCondition('title', 'english_title', NULL, 'en'); - // $query->propertyCondition('uid', 1, NULL, 'en'); - // $query->propertyCondition('title', 'german_title', NULL, 'de'); - // - // We probably want to move to this approach when refactoring EFQ to work - // with storage-independent entities. For now we are keeping the current - // approach for consistency with field meta conditions. - return $this->propertyCondition('langcode', $langcode, $operator, $langcode_group); - } - - /** - * Orders the result set by entity-generic metadata. - * - * If called multiple times, the query will order by each specified column in - * the order this method is called. - * - * Note: The "comment" and "taxonomy_term" entity types don't support ordering - * by bundle. For "taxonomy_term", propertyOrderBy('vid') can be used instead. - * - * @param $name - * 'entity_type', 'bundle', 'revision_id' or 'entity_id'. - * @param $direction - * The direction to sort. Legal values are "ASC" and "DESC". - * - * @return Drupal\Core\Entity\EntityFieldQuery - * The called object. - */ - public function entityOrderBy($name, $direction = 'ASC') { - $this->order[] = array( - 'type' => 'entity', - 'specifier' => $name, - 'direction' => $direction, - ); - return $this; - } - - /** - * Orders the result set by a given field column. - * - * If called multiple times, the query will order by each specified column in - * the order this method is called. Note that entities with empty field - * values will be excluded from the EntityFieldQuery results when using this - * method. - * - * @param $field - * Either a field name or a field array. - * @param $column - * A column defined in the hook_field_schema() of this field. entity_id and - * bundle can also be used. - * @param $direction - * The direction to sort. Legal values are "ASC" and "DESC". - * - * @return Drupal\Core\Entity\EntityFieldQuery - * The called object. - */ - public function fieldOrderBy($field, $column, $direction = 'ASC') { - if (is_scalar($field)) { - $field_definition = field_info_field($field); - if (empty($field_definition)) { - throw new EntityFieldQueryException(t('Unknown field: @field_name', array('@field_name' => $field))); - } - $field = $field_definition; - } - // Save the index used for the new field, for later use in field storage. - $index = count($this->fields); - $this->fields[$index] = $field; - $this->order[] = array( - 'type' => 'field', - 'specifier' => array( - 'field' => $field, - 'index' => $index, - 'column' => $column, - ), - 'direction' => $direction, - ); - return $this; - } - - /** - * Orders the result set by an entity-specific property. - * - * An $entity_type must be specified by calling - * EntityFieldCondition::entityCondition('entity_type', $entity_type) before - * executing the query. - * - * If called multiple times, the query will order by each specified column in - * the order this method is called. - * - * @param $column - * The column on which to order. - * @param $direction - * The direction to sort. Legal values are "ASC" and "DESC". - * @param $langcode_group - * (optional) An arbitrary identifier: order settings in the same group must - * have the same group identifier. This is used to group the property order - * setting with a related set of property conditions, meta conditions and - * other order settings. By default all conditions and order settings belong - * to the same group. - * - * @return Drupal\Core\Entity\EntityFieldQuery - * The called object. - * - * @see Drupal\entity\EntityFieldQuery::propertyLanguageCondition() - */ - public function propertyOrderBy($column, $direction = 'ASC', $langcode_group = 0) { - $this->properties[$langcode_group][$column] = $column; - $this->order[] = array( - 'type' => 'property', - 'specifier' => $column, - 'direction' => $direction, - 'langcode_group' => $langcode_group, - ); - return $this; - } - - /** - * Sets the query to be a count query only. - * - * @return Drupal\Core\Entity\EntityFieldQuery - * The called object. - */ - public function count() { - $this->count = TRUE; - return $this; - } - - /** - * Restricts a query to a given range in the result set. - * - * @param $start - * The first entity from the result set to return. If NULL, removes any - * range directives that are set. - * @param $length - * The number of entities to return from the result set. - * - * @return Drupal\Core\Entity\EntityFieldQuery - * The called object. - */ - public function range($start = NULL, $length = NULL) { - $this->range = array( - 'start' => $start, - 'length' => $length, - ); - return $this; - } - - /** - * Enables a pager for the query. - * - * @param $limit - * An integer specifying the number of elements per page. If passed a false - * value (FALSE, 0, NULL), the pager is disabled. - * @param $element - * An optional integer to distinguish between multiple pagers on one page. - * If not provided, one is automatically calculated. - * - * @return Drupal\Core\Entity\EntityFieldQuery - * The called object. - */ - public function pager($limit = 10, $element = NULL) { - if (!isset($element)) { - $element = PagerSelectExtender::$maxElement++; - } - elseif ($element >= PagerSelectExtender::$maxElement) { - PagerSelectExtender::$maxElement = $element + 1; - } - - $this->pager = array( - 'limit' => $limit, - 'element' => $element, - ); - return $this; - } - - /** - * Enables sortable tables for this query. - * - * @param $headers - * An EFQ Header array based on which the order clause is added to the - * query. - * - * @return Drupal\Core\Entity\EntityFieldQuery - * The called object. - */ - 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'])) { - if ($header['type'] == 'field') { - $this->fieldOrderBy($header['specifier']['field'], $header['specifier']['column'], $direction); - } - else { - $header['direction'] = $direction; - $this->order[] = $header; - } - } - } - - return $this; - } - - /** - * Filters on the data being deleted. - * - * @param $deleted - * TRUE to only return deleted data, FALSE to return non-deleted data, - * EntityFieldQuery::RETURN_ALL to return everything. Defaults to FALSE. - * - * @return Drupal\Core\Entity\EntityFieldQuery - * The called object. - */ - public function deleted($deleted = TRUE) { - $this->deleted = $deleted; - return $this; - } - - /** - * Queries the current or every revision. - * - * Note that this only affects field conditions. Property conditions always - * apply to the current revision. - * @TODO: Once revision tables have been cleaned up, revisit this. - * - * @param $age - * - FIELD_LOAD_CURRENT (default): Query the most recent revisions for all - * entities. The results will be keyed by entity type and entity ID. - * - FIELD_LOAD_REVISION: Query all revisions. The results will be keyed by - * entity type and entity revision ID. - * - * @return Drupal\Core\Entity\EntityFieldQuery - * The called object. - */ - public function age($age) { - $this->age = $age; - return $this; - } - - /** - * Adds a tag to the query. - * - * Tags are strings that mark a query so that hook_query_alter() and - * hook_query_TAG_alter() implementations may decide if they wish to alter - * the query. A query may have any number of tags, and they must be valid PHP - * identifiers (composed of letters, numbers, and underscores). For example, - * queries involving nodes that will be displayed for a user need to add the - * tag 'node_access', so that the node module can add access restrictions to - * the query. - * - * If an entity field query has tags, it must also have an entity type - * specified, because the alter hook will need the entity base table. - * - * @param string $tag - * The tag to add. - * - * @return Drupal\Core\Entity\EntityFieldQuery - * The called object. - */ - public function addTag($tag) { - $this->tags[$tag] = $tag; - return $this; - } - - /** - * Adds additional metadata to the query. - * - * Sometimes a query may need to provide additional contextual data for the - * alter hook. The alter hook implementations may then use that information - * to decide if and how to take action. - * - * @param $key - * The unique identifier for this piece of metadata. Must be a string that - * follows the same rules as any other PHP identifier. - * @param $object - * The additional data to add to the query. May be any valid PHP variable. - * - * @return Drupal\Core\Entity\EntityFieldQuery - * The called object. - */ - public function addMetaData($key, $object) { - $this->metaData[$key] = $object; - return $this; - } - - /** - * Executes the query. - * - * After executing the query, $this->orderedResults will contain a list of - * the same entity ids in the order returned by the query. This is only - * relevant if there are multiple entity types in the returned value and - * a field ordering was requested. In every other case, the returned value - * contains everything necessary for processing. - * - * @return - * Either a number if count() was called or an array of associative arrays - * of the entity ids. The outer array keys are entity types, and the inner - * array keys are the relevant ID. (In most cases this will be the entity - * ID. The only exception is when age=FIELD_LOAD_REVISION is used and field - * conditions or sorts are present -- in this case, the key will be the - * revision ID.) The entity type will only exist in the outer array if - * results were found. The inner array values consist of an object with the - * entity_id, revision_id and bundle properties. To traverse the returned - * array: - * @code - * foreach ($query->execute() as $entity_type => $entities) { - * foreach ($entities as $entity_id => $entity) { - * @endcode - * Note if the entity type is known, then the following snippet will load - * the entities found: - * @code - * $result = $query->execute(); - * if (!empty($result[$my_type])) { - * $entities = entity_load_multiple($my_type, array_keys($result[$my_type])); - * } - * @endcode - */ - public function execute() { - // Give a chance to other modules to alter the query. - drupal_alter('entity_query', $this); - $this->altered = TRUE; - - // Initialize the pager. - $this->initializePager(); - - // Execute the query using the correct callback. - $result = call_user_func($this->queryCallback(), $this); - - return $result; - } - - /** - * Determines the query callback to use for this entity query. - * - * @return - * A callback that can be used with call_user_func(). - */ - public function queryCallback() { - // Use the override from $this->executeCallback. It can be set either - // while building the query, or using hook_entity_query_alter(). - if (function_exists($this->executeCallback)) { - return $this->executeCallback; - } - // If there are no field conditions and sorts, and no execute callback - // then we default to querying entity tables in SQL. - if (empty($this->fields)) { - return array($this, 'propertyQuery'); - } - // If no override, find the storage engine to be used. - foreach ($this->fields as $field) { - if (!isset($storage)) { - $storage = $field['storage']['module']; - } - elseif ($storage != $field['storage']['module']) { - throw new EntityFieldQueryException(t("Can't handle more than one field storage engine")); - } - } - if ($storage) { - // Use hook_field_storage_query() from the field storage. - return $storage . '_field_storage_query'; - } - else { - throw new EntityFieldQueryException(t("Field storage engine not found.")); - } - } - - /** - * Queries entity tables in SQL for property conditions and sorts. - * - * This method is only used if there are no field conditions and sorts. - * - * @return - * See EntityFieldQuery::execute(). - */ - protected function propertyQuery() { - if (empty($this->entityConditions['entity_type'])) { - throw new EntityFieldQueryException(t('For this query an entity type must be specified.')); - } - - $entity_type = $this->entityConditions['entity_type']['value']; - $entity_info = entity_get_info($entity_type); - if (empty($entity_info['base table'])) { - throw new EntityFieldQueryException(t('Entity %entity has no base table.', array('%entity' => $entity_type))); - } - - $base_table = $entity_info['base table']; - $select_query = db_select($base_table); - $select_query->addExpression(':entity_type', 'entity_type', array(':entity_type' => $entity_type)); - $sql_field = $entity_info['entity keys']['id']; - - // If a data table is defined we need to join it and make sure that only one - // record per entity is returned. - $this->joinPropertyData($select_query, $entity_type, $base_table); - - // Process the property conditions. - $this->addPropertyConditions($select_query, $entity_type); - - // Process the six possible entity condition. - // The id field is always present in entity keys. - $id_map['entity_id'] = $sql_field; - $select_query->addField($base_table, $sql_field, 'entity_id'); - if (isset($this->entityConditions['entity_id'])) { - $this->addCondition($select_query, "$base_table.$sql_field", $this->entityConditions['entity_id']); - } - - // If there is a revision key defined, use it. - if (!empty($entity_info['entity keys']['revision'])) { - $sql_field = $entity_info['entity keys']['revision']; - $select_query->addField($base_table, $sql_field, 'revision_id'); - if (isset($this->entityConditions['revision_id'])) { - $this->addCondition($select_query, "$base_table.$sql_field", $this->entityConditions['revision_id']); - } - } - else { - $sql_field = 'revision_id'; - $select_query->addExpression('NULL', 'revision_id'); - } - $id_map['revision_id'] = $sql_field; - - // Handle bundles. - if (!empty($entity_info['entity keys']['bundle'])) { - $base_table_schema = drupal_get_schema($base_table); - $sql_field = $entity_info['entity keys']['bundle']; - $having = FALSE; - if (!empty($base_table_schema['fields'][$sql_field])) { - $select_query->addField($base_table, $sql_field, 'bundle'); - } - } - else { - $sql_field = 'bundle'; - $select_query->addExpression(':bundle', 'bundle', array(':bundle' => $entity_type)); - $having = TRUE; - } - - $id_map['bundle'] = $sql_field; - - if (isset($this->entityConditions['bundle'])) { - if (!empty($entity_info['entity keys']['bundle'])) { - $this->addCondition($select_query, "$base_table.$sql_field", $this->entityConditions['bundle'], $having); - } - else { - // This entity has no bundle, so invalidate the query. - $select_query->where('1 = 0'); - } - } - - foreach (array('uuid', 'langcode') as $key) { - if (isset($this->entityConditions[$key])) { - $sql_field = !empty($entity_info['entity keys'][$key]) ? $entity_info['entity keys'][$key] : $key; - if (isset($base_table_schema[$sql_field])) { - $this->addCondition($select_query, "$base_table.$sql_field", $this->entityConditions[$key]); - } - } - } - - // Order the query. - foreach ($this->order as $order) { - if ($order['type'] == 'entity') { - $key = $order['specifier']; - if (!isset($id_map[$key])) { - throw new EntityFieldQueryException(t('Do not know how to order on @key for @entity_type', array('@key' => $key, '@entity_type' => $entity_type))); - } - $select_query->orderBy($id_map[$key], $order['direction']); - } - elseif ($order['type'] == 'property') { - $this->addPropertyOrderBy($select_query, $entity_type, $order); - } - } - - return $this->finishQuery($select_query); - } - - /** - * 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. - */ - 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']); - } - } - - /** - * Finishes the query. - * - * Adds tags, metaData, range and returns the requested list or count. - * - * @param SelectQuery $select_query - * A SelectQuery which has entity_type, entity_id, revision_id and bundle - * fields added. - * @param $id_key - * Which field's values to use as the returned array keys. - * - * @return - * See EntityFieldQuery::execute(). - */ - function finishQuery($select_query, $id_key = 'entity_id') { - foreach ($this->tags as $tag) { - $select_query->addTag($tag); - } - foreach ($this->metaData as $key => $object) { - $select_query->addMetaData($key, $object); - } - $select_query->addMetaData('entity_field_query', $this); - if ($this->range) { - $select_query->range($this->range['start'], $this->range['length']); - } - if ($this->count) { - return $select_query->countQuery()->execute()->fetchField(); - } - $return = array(); - foreach ($select_query->execute() as $ids) { - if (!isset($ids->bundle)) { - $ids->bundle = NULL; - } - $return[$ids->entity_type][$ids->$id_key] = $ids; - $this->ordered_results[] = $ids; - } - return $return; - } - - /** - * Adds a condition to an already built SelectQuery (internal function). - * - * This is a helper for hook_entity_query() and hook_field_storage_query(). - * - * @param SelectQuery $select_query - * A SelectQuery object. - * @param $sql_field - * The name of the field. - * @param $condition - * A condition as described in EntityFieldQuery::fieldCondition() and - * EntityFieldQuery::entityCondition(). - * @param $having - * HAVING or WHERE. This is necessary because SQL can't handle WHERE - * conditions on aliased columns. - */ - public function addCondition(Select $select_query, $sql_field, $condition, $having = FALSE) { - $method = $having ? 'havingCondition' : 'condition'; - $like_prefix = ''; - switch ($condition['operator']) { - case 'CONTAINS': - $like_prefix = '%'; - case 'STARTS_WITH': - $select_query->$method($sql_field, $like_prefix . db_like($condition['value']) . '%', 'LIKE'); - break; - default: - $select_query->$method($sql_field, $condition['value'], $condition['operator']); - } - } - - /** - * Adds property conditions to a select query performing the needed joins. - * - * @param SelectQuery $select_query - * The SelectQuery the conditions should be applied to. - * @param $entity_type - * The entity type the query applies to. - */ - public function addPropertyConditions(Select $select_query, $entity_type) { - $entity_info = entity_get_info($entity_type); - $entity_base_table = $entity_info['base table']; - list($data_table, $data_table_schema) = $this->getPropertyDataSchema($entity_type); - - foreach ($this->propertyConditions as $property_condition) { - $column = $property_condition['column']; - // @todo Property conditions should always apply to the data table (if - // available), however UUIDs are used in load conditions and thus treated - // as properties, instead of being set as entity conditions. Remove this - // once we can reliably distinguish between properties and metadata living - // on the base table. - $table = !empty($data_table_schema['fields'][$column]) ? $data_table . '_' . $property_condition['langcode_group'] : $entity_base_table; - $this->addCondition($select_query, "$table.$column", $property_condition); - } - } - - /** - * Adds a property order by to the given select query. - * - * @param SelectQuery $select_query - * The SelectQuery the conditions should be applied to. - * @param $entity_type - * The entity type the query applies to. - * @param array $order - * An order array as defined in EntityFieldQuery::propertyOrderBy(). - */ - public function addPropertyOrderBy(Select $select_query, $entity_type, array $order) { - $entity_info = entity_get_info($entity_type); - list($data_table, $data_table_schema) = $this->getPropertyDataSchema($entity_type); - $specifier = $order['specifier']; - $table = !empty($data_table_schema['fields'][$specifier]) ? $data_table . '_' . $order['langcode_group'] : $entity_info['base table']; - $select_query->orderBy("$table.$specifier", $order['direction']); - } - - /** - * Joins the needed data tables based on the specified property conditions. - * - * @param SelectQuery $select_query - * A SelectQuery containing at least one table as specified by $base_table. - * @param $entity_type - * The entity type the query applies to. - * @param $base_table - * The name of the base table to join on. - * @param $base_id_key - * The primary id column name to use to join on the base table. - */ - public function joinPropertyData(Select $select_query, $entity_type, $base_table, $base_id_key = NULL) { - list($data_table, $data_table_schema) = $this->getPropertyDataSchema($entity_type); - - // If we have no data table there are no property meta conditions to handle. - if (!empty($data_table)) { - $entity_info = entity_get_info($entity_type); - $id_key = $entity_info['entity keys']['id']; - $base_id_key = !empty($base_id_key) ? $base_id_key : $id_key; - - foreach ($this->properties as $key => $property) { - // Every property needs a new join on the data table. - $table_alias = $data_table . '_' . $key; - $table_aliases[$key] = $table_alias; - $select_query->join($data_table, $table_alias, "$table_alias.$id_key = $base_table.$base_id_key"); - } - - // Ensure we return just one value. - $select_query->distinct(); - } - } - - /** - * Returns the data table schema for the given entity type. - * - * @param $entity_type - * The entity type the query applies to. - * - * @return array - * An array containing the table data name (or FALSE if none is defined) and - * its schema. - */ - protected function getPropertyDataSchema($entity_type) { - $entity_info = entity_get_info($entity_type); - - if (!empty($entity_info['data table'])) { - $data_table = $entity_info['data table']; - $data_table_schema = drupal_get_schema($data_table); - } - else { - $data_table = FALSE; - $data_table_schema = array(); - } - - return array($data_table, $data_table_schema); - } -} diff --git a/core/lib/Drupal/Core/Entity/EntityFieldQueryException.php b/core/lib/Drupal/Core/Entity/EntityFieldQueryException.php deleted file mode 100644 index bf792a5..0000000 --- a/core/lib/Drupal/Core/Entity/EntityFieldQueryException.php +++ /dev/null @@ -1,19 +0,0 @@ -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 @@ -152,4 +152,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(); + } diff --git a/core/lib/Drupal/Core/Entity/Query/ConditionBase.php b/core/lib/Drupal/Core/Entity/Query/ConditionBase.php new file mode 100644 index 0000000..7a77580 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Query/ConditionBase.php @@ -0,0 +1,78 @@ +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']); + } + } + } + +} diff --git a/core/lib/Drupal/Core/Entity/Query/ConditionInterface.php b/core/lib/Drupal/Core/Entity/Query/ConditionInterface.php new file mode 100644 index 0000000..e81a1d0 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Query/ConditionInterface.php @@ -0,0 +1,80 @@ +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; + } + +} diff --git a/core/lib/Drupal/Core/Entity/Query/QueryException.php b/core/lib/Drupal/Core/Entity/Query/QueryException.php new file mode 100644 index 0000000..a1d54d6 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Query/QueryException.php @@ -0,0 +1,17 @@ +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); + } + +} diff --git a/core/lib/Drupal/Core/Entity/Query/QueryInterface.php b/core/lib/Drupal/Core/Entity/Query/QueryInterface.php new file mode 100644 index 0000000..477de7d --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Query/QueryInterface.php @@ -0,0 +1,243 @@ +condition('greetings', 'merhaba', '=', 'tr'); + * ->condition('greetings.value', 'siema', '=', 'pl'); + * ->execute(); + * $entity_ids = $query->execute(); + * @endcode + * + * @param $value + * The value for $field. In most cases, this is a scalar. For more complex + * options, it is an array. The meaning of each element in the array is + * dependent on $operator. + * @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 $langcode + * Language code (optional). + * + * @return \Drupal\Core\Entity\Query\QueryInterface + * @see Drupal\Core\Entity\Query\andConditionGroup + * @see Drupal\Core\Entity\Query\orConditionGroup + */ + public function condition($field, $value = NULL, $operator = NULL, $langcode = NULL); + + /** + * Queries for the existence of a field. + * + * @param $field + * Name of a field. + * @param $langcode + * Language code (optional). + * @return \Drupal\Core\Entity\Query\QueryInterface + */ + public function exists($field, $langcode = NULL); + + /** + * Queries for the nonexistence of a field. + * + * @param $field. + * Name of a field. + * @param $langcode + * Language code (optional). + * @return \Drupal\Core\Entity\Query\QueryInterface + */ + public function notExists($field, $langcode = NULL); + + /** + * Enables a pager for the query. + * + * @param $limit + * An integer specifying the number of elements per page. If passed a false + * value (FALSE, 0, NULL), the pager is disabled. + * @param $element + * An optional integer to distinguish between multiple pagers on one page. + * If not provided, one is automatically calculated. + * + * @return \Drupal\Core\Entity\Query\QueryInterface + * The called object. + */ + public function pager($limit = 10, $element = NULL); + + /** + * @param null $start + * @param null $length + * @return \Drupal\Core\Entity\Query\QueryInterface + * The called object. + */ + public function range($start = NULL, $length = NULL); + + /** + * @param $field + * Name of a field. + * @param string $direction + * @param $langcode + * Language code (optional). + * @return \Drupal\Core\Entity\Query\QueryInterface + * The called object. + */ + public function sort($field, $direction = 'ASC', $langcode = NULL); + + /** + * Makes this a count query. + * + * For count queries, execute() returns the number entities found. + * + * @return \Drupal\Core\Entity\Query\QueryInterface + * The called object. + */ + public function count(); + + /** + * Enables sortable tables for this query. + * + * @param $headers + * An array of headers of the same struucture as described in + * theme_table(). Use a 'specifier' in place of a 'field' to specify what + * to sort on. This can be an entity or a field as described in + * condition(). + * @return \Drupal\Core\Entity\Query\QueryInterface + * The called object. + */ + public function tableSort(&$headers); + + /** + * @return \Drupal\Core\Entity\Query\QueryInterface + * The called object. + */ + public function accessCheck($access_check = TRUE); + + /** + * Queries the current or every revision. + * + * Note that this only affects field conditions. Property conditions always + * apply to the current revision. + * @TODO: Once revision tables have been cleaned up, revisit this. + * + * @param $age + * - FIELD_LOAD_CURRENT (default): Query the most recent revisions only, + * - FIELD_LOAD_REVISION: Query all revisions. + * + * @return \Drupal\Core\Entity\Query\QueryInterface + * The called object. + */ + public function age($age = FIELD_LOAD_CURRENT); + + /** + * Execute the query. + * + * @return int|array + * Returns an integer for count queries or an array of ids. The values of + * the array are always entity ids. The keys will be revision ids if the + * entity supports revision and entity ids if not. + */ + public function execute(); + + /** + * Creates an object holding a group of conditions. + * + * See andConditionGroup() and orConditionGroup() for more. + * + * @param $conjunction + * - AND (default): this is the equivalent of andConditionGroup(). + * - OR: this is the equivalent of andConditionGroup(). + * + * return ConditionInterface + * An object holding a group of conditions. + */ + public function conditionGroupFactory($conjunction = 'AND'); + + /** + * Creates a new group of conditions ANDed together. + * + * For example, consider a drawing entity type with a 'figures' multi-value + * field containing 'shape' and 'color' columns. To find all drawings + * containing both a red triangle and a blue circle: + * @code + * $query = entity_query('drawing'); + * $group = $query->andConditionGroup() + * ->condition('figures.color', 'red') + * ->condition('figures.shape', 'triangle'); + * $query->condition($group); + * $group = $query->andConditionGroup() + * ->condition('figures.color', 'blue') + * ->condition('figures.shape', 'circle'); + * $query->condition($group); + * $entity_ids = $query->execute(); + * @endcode + * + * @return \Drupal\Core\Entity\Query\ConditionInterface + */ + public function andConditionGroup(); + + /** + * Creates a new group of conditions ORed together. + * + * For example, consider a map entity with an 'attributes' field + * containing 'building_type' and 'color' columns. To find all green and + * red bikesheds: + * @code + * $query = entity_query('map'); + * $group = $query->orConditionGroup() + * ->condition('attributes.color', 'red') + * ->condition('attributes.color', 'green'); + * $entity_ids = $query + * ->condition('attributes.building_type', 'bikeshed') + * ->condition($group) + * ->execute(); + * @endcode + * Note that this particular example can be simplified: + * @code + * $entity_ids = $query + * ->condition('attributes.color', array('red', 'green')) + * ->condition('attributes.building_type', 'bikeshed') + * ->execute(); + * @endcode + * + * @return \Drupal\Core\Entity\Query\ConditionInterface + */ + public function orConditionGroup(); + +} diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module index df0a33a..57cc799 100644 --- a/core/modules/comment/comment.module +++ b/core/modules/comment/comment.module @@ -1432,7 +1432,7 @@ function comment_delete_multiple($cids) { * An array of comment objects, indexed by comment ID. * * @see entity_load() - * @see Drupal\Core\Entity\EntityFieldQuery + * @see Drupal\Core\Entity\Query\QueryInterface */ function comment_load_multiple(array $cids = NULL, $reset = FALSE) { return entity_load_multiple('comment', $cids, $reset); diff --git a/core/modules/field/field.crud.inc b/core/modules/field/field.crud.inc index ba2f09e..45389ca 100644 --- a/core/modules/field/field.crud.inc +++ b/core/modules/field/field.crud.inc @@ -6,7 +6,6 @@ */ use Drupal\field\FieldException; -use Drupal\Core\Entity\EntityFieldQuery; /** * @defgroup field_crud Field CRUD API @@ -137,6 +136,9 @@ function field_create_field($field) { $schema += array('columns' => array(), 'indexes' => array(), 'foreign keys' => array()); // 'columns' are hardcoded in the field type. $field['columns'] = $schema['columns']; + if (array_intersect(array_keys($field['columns']), field_reserved_columns())) { + throw new FieldException(t('Illegal field type columns.')); + } // 'foreign keys' are hardcoded in the field type. $field['foreign keys'] = $schema['foreign keys']; // 'indexes' can be both hardcoded in the field type, and specified in the @@ -873,31 +875,37 @@ function field_purge_batch($batch_size) { // Retrieve all deleted field instances. We cannot use field_info_instances() // because that function does not return deleted instances. $instances = field_read_instances(array('deleted' => 1), array('include_deleted' => 1)); - + $factory = drupal_container()->get('entity.query'); + $info = entity_get_info(); foreach ($instances as $instance) { + $entity_type = $instance['entity_type']; + $ids = (object) array( + 'entity_type' => $entity_type, + 'bundle' => $instance['bundle'], + ); // field_purge_data() will need the field array. $field = field_info_field_by_id($instance['field_id']); // Retrieve some entities. - $query = new EntityFieldQuery(); - $results = $query - ->fieldCondition($field) - ->entityCondition('bundle', $instance['bundle']) - ->deleted(TRUE) + $results = $factory->get($entity_type) + ->condition('id:' . $field['id'] . '.deleted', 1) + ->condition($info[$entity_type]['entity keys']['bundle'], $ids->bundle) ->range(0, $batch_size) ->execute(); if ($results) { - foreach ($results as $entity_type => $ids_array) { - $entities = array(); - foreach ($ids_array as $ids) { - $entities[$ids->entity_id] = _field_create_entity_from_ids($ids); - } - - field_attach_load($entity_type, $entities, FIELD_LOAD_CURRENT, array('field_id' => $field['id'], 'deleted' => 1)); - foreach ($entities as $entity) { - // Purge the data for the entity. - field_purge_data($entity_type, $entity, $field, $instance); - } + $entities = array(); + foreach ($results as $revision_id => $entity_id) { + // This might not be the revision id if the entity type does not support + // revisions but _field_create_entity_from_ids() checks that and + // disregards this value so there is no harm setting it. + $ids->revision_id = $revision_id; + $ids->entity_id = $entity_id; + $entities[$entity_id] = _field_create_entity_from_ids($ids); + } + field_attach_load($entity_type, $entities, FIELD_LOAD_CURRENT, array('field_id' => $field['id'], 'deleted' => 1)); + foreach ($entities as $entity) { + // Purge the data for the entity. + field_purge_data($entity_type, $entity, $field, $instance); } } else { diff --git a/core/modules/field/field.module b/core/modules/field/field.module index 7fac3d2..fb10433 100644 --- a/core/modules/field/field.module +++ b/core/modules/field/field.module @@ -4,8 +4,8 @@ * Attach custom data fields to Drupal entities. */ -use Drupal\Core\Entity\EntityFieldQuery; use Drupal\Core\Template\Attribute; +use Drupal\Core\Entity\Query\QueryFactory; /* * Load all public Field API functions. Drupal currently has no @@ -1045,16 +1045,27 @@ function field_get_items($entity_type, $entity, $field_name, $langcode = NULL) { * TRUE if the field has data for any entity; FALSE otherwise. */ function field_has_data($field) { - $query = new EntityFieldQuery(); - return (bool) $query - ->fieldCondition($field) - ->range(0, 1) - ->count() - // Neutralize the 'entity_field_access' query tag added by - // field_sql_storage_field_storage_query(). The result cannot depend on the - // access grants of the current user. - ->addTag('DANGEROUS_ACCESS_CHECK_OPT_OUT') - ->execute(); + $field = field_info_field_by_id($field['id']); + $columns = array_keys($field['columns']); + $factory = drupal_container()->get('entity.query'); + foreach ($field['bundles'] as $entity_type => $bundle) { + $query = $factory->get($entity_type); + $group = $query->orConditionGroup(); + foreach ($columns as $column) { + $group->exists($field['field_name'] . '.' . $column); + } + $result = $query + ->condition($group) + ->count() + ->accessCheck(FALSE) + ->range(0, 1) + ->execute(); + if ($result) { + return TRUE; + } + } + + return FALSE; } /** @@ -1287,14 +1298,11 @@ function theme_field($variables) { /** * Assembles a partial entity structure with initial IDs. * - * This can be used to create an entity based on the the ids object returned by - * EntityFieldQuery. - * * @param stdClass $ids * An object with the properties entity_type (required), entity_id (required), * revision_id (optional) and bundle (optional). * - * @return + * @return \Drupal\Core\Entity\EntityInterface * An entity, initialized with the provided IDs. */ function _field_create_entity_from_ids($ids) { @@ -1308,4 +1316,13 @@ function _field_create_entity_from_ids($ids) { $id_properties[$info['entity keys']['bundle']] = $ids->bundle; } return entity_create($ids->entity_type, $id_properties); - } +} + +/** + * A list of columns that can not be used as field type columns. + * + * @return array + */ +function field_reserved_columns() { + return array('deleted'); +} diff --git a/core/modules/field/lib/Drupal/field/Tests/BulkDeleteTest.php b/core/modules/field/lib/Drupal/field/Tests/BulkDeleteTest.php index 8211ce8..cc47e59 100644 --- a/core/modules/field/lib/Drupal/field/Tests/BulkDeleteTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/BulkDeleteTest.php @@ -7,8 +7,6 @@ namespace Drupal\field\Tests; -use Drupal\Core\Entity\EntityFieldQuery; - /** * Unit test class for field bulk delete and batch purge functionality. */ @@ -115,7 +113,7 @@ function setUp() { // For each bundle, create an instance of each field, and 10 // entities with values for each field. - $id = 0; + $id = 1; $this->entity_type = 'test_entity'; foreach ($this->bundles as $bundle) { foreach ($this->fields as $field) { @@ -135,14 +133,15 @@ function setUp() { foreach ($this->fields as $field) { $entity->{$field['field_name']}[LANGUAGE_NOT_SPECIFIED] = $this->_generateTestFieldValues($field['cardinality']); } - - $this->entities[$id] = $entity; - // Also keep track of the entities per bundle. - $this->entities_by_bundles[$bundle][$id] = $entity; - field_attach_insert($this->entity_type, $entity); + $entity->save(); $id++; } } + $this->entities = entity_load_multiple($this->entity_type, range(1, $id)); + foreach ($this->entities as $entity) { + // Also keep track of the entities per bundle. + $this->entities_by_bundles[$entity->fttype][$entity->ftid] = $entity; + } } /** @@ -157,14 +156,14 @@ function setUp() { function testDeleteFieldInstance() { $bundle = reset($this->bundles); $field = reset($this->fields); + $field_name = $field['field_name']; + $factory = drupal_container()->get('entity.query'); // There are 10 entities of this bundle. - $query = new EntityFieldQuery(); - $found = $query - ->fieldCondition($field) - ->entityCondition('bundle', $bundle) + $found = $factory->get('test_entity') + ->condition('fttype', $bundle) ->execute(); - $this->assertEqual(count($found['test_entity']), 10, 'Correct number of entities found before deleting'); + $this->assertEqual(count($found), 10, 'Correct number of entities found before deleting'); // Delete the instance. $instance = field_info_instance($this->entity_type, $field['field_name'], $bundle); @@ -176,27 +175,30 @@ function testDeleteFieldInstance() { $this->assertEqual($instances[0]['bundle'], $bundle, 'The deleted instance is for the correct bundle'); // There are 0 entities of this bundle with non-deleted data. - $query = new EntityFieldQuery(); - $found = $query - ->fieldCondition($field) - ->entityCondition('bundle', $bundle) + $found = $factory->get('test_entity') + ->condition('fttype', $bundle) + ->condition("$field_name.deleted", 0) ->execute(); - $this->assertTrue(!isset($found['test_entity']), 'No entities found after deleting'); + $this->assertFalse($found, 'No entities found after deleting'); // There are 10 entities of this bundle when deleted fields are allowed, and // their values are correct. - $query = new EntityFieldQuery(); - $found = $query - ->fieldCondition($field) - ->entityCondition('bundle', $bundle) - ->deleted(TRUE) + $found = $factory->get('test_entity') + ->condition('fttype', $bundle) + ->condition("$field_name.deleted", 1) + ->sort('ftid') ->execute(); + $ids = (object) array( + 'entity_type' => 'test_entity', + 'bundle' => $bundle, + ); $entities = array(); - foreach ($found[$this->entity_type] as $ids) { - $entities[$ids->entity_id] = _field_create_entity_from_ids($ids); + foreach ($found as $entity_id) { + $ids->entity_id = $entity_id; + $entities[$entity_id] = _field_create_entity_from_ids($ids); } field_attach_load($this->entity_type, $entities, FIELD_LOAD_CURRENT, array('field_id' => $field['id'], 'deleted' => 1)); - $this->assertEqual(count($found['test_entity']), 10, 'Correct number of entities found after deleting'); + $this->assertEqual(count($found), 10, 'Correct number of entities found after deleting'); foreach ($entities as $id => $entity) { $this->assertEqual($this->entities[$id]->{$field['field_name']}, $entity->{$field['field_name']}, "Entity $id with deleted data loaded correctly"); } @@ -227,13 +229,11 @@ function testPurgeInstance() { field_purge_batch($batch_size); // There are $count deleted entities left. - $query = new EntityFieldQuery(); - $found = $query - ->fieldCondition($field) - ->entityCondition('bundle', $bundle) - ->deleted(TRUE) + $found = entity_query('test_entity') + ->condition('fttype', $bundle) + ->condition($field['field_name'] . '.deleted', 1) ->execute(); - $this->assertEqual($count ? count($found['test_entity']) : count($found), $count, 'Correct number of entities found after purging 2'); + $this->assertEqual(count($found), $count, 'Correct number of entities found after purging 2'); } // Check hooks invocations. diff --git a/core/modules/field/modules/field_sql_storage/field_sql_storage.module b/core/modules/field/modules/field_sql_storage/field_sql_storage.module index dcd6ba1..d354f51 100644 --- a/core/modules/field/modules/field_sql_storage/field_sql_storage.module +++ b/core/modules/field/modules/field_sql_storage/field_sql_storage.module @@ -6,9 +6,6 @@ */ use Drupal\Core\Database\Database; -use Drupal\Core\Database\Query\Select; -use Drupal\Core\Entity\EntityFieldQuery; -use Drupal\Core\Entity\EntityFieldQueryException; use Drupal\field\FieldUpdateForbiddenException; /** @@ -85,7 +82,7 @@ function _field_sql_storage_revision_tablename($field) { * unique among all other fields. */ function _field_sql_storage_columnname($name, $column) { - return $name . '_' . $column; + return in_array($column, field_reserved_columns()) ? $column : $name . '_' . $column; } /** @@ -525,191 +522,6 @@ function field_sql_storage_field_storage_purge($entity_type, $entity, $field, $i } /** - * Implements hook_field_storage_query(). - */ -function field_sql_storage_field_storage_query(EntityFieldQuery $query) { - if ($query->age == FIELD_LOAD_CURRENT) { - $tablename_function = '_field_sql_storage_tablename'; - $id_key = 'entity_id'; - } - else { - $tablename_function = '_field_sql_storage_revision_tablename'; - $id_key = 'revision_id'; - } - $table_aliases = array(); - // Add tables for the fields used. - foreach ($query->fields as $key => $field) { - $tablename = $tablename_function($field); - // Every field needs a new table. - $table_alias = $tablename . $key; - $table_aliases[$key] = $table_alias; - if ($key) { - $select_query->join($tablename, $table_alias, "$table_alias.entity_type = $field_base_table.entity_type AND $table_alias.$id_key = $field_base_table.$id_key"); - } - else { - $select_query = db_select($tablename, $table_alias); - // Allow queries internal to the Field API to opt out of the access - // check, for situations where the query's results should not depend on - // the access grants for the current user. - if (!isset($query->tags['DANGEROUS_ACCESS_CHECK_OPT_OUT'])) { - $select_query->addTag('entity_field_access'); - } - $select_query->addMetaData('base_table', $tablename); - $select_query->fields($table_alias, array('entity_type', 'entity_id', 'revision_id', 'bundle')); - $field_base_table = $table_alias; - } - if ($field['cardinality'] != 1 || $field['translatable']) { - $select_query->distinct(); - } - } - - // Add field conditions. We need a fresh grouping cache. - drupal_static_reset('_field_sql_storage_query_field_conditions'); - _field_sql_storage_query_field_conditions($query, $select_query, $query->fieldConditions, $table_aliases, '_field_sql_storage_columnname'); - - // Add field meta conditions. - _field_sql_storage_query_field_conditions($query, $select_query, $query->fieldMetaConditions, $table_aliases, function ($field_name, $column) { return $column; }); - - if (isset($query->deleted)) { - $select_query->condition("$field_base_table.deleted", (int) $query->deleted); - } - - // Is there a need to sort the query by property? - $has_property_order = FALSE; - foreach ($query->order as $order) { - if ($order['type'] == 'property') { - $has_property_order = TRUE; - } - } - - $entity_type = FALSE; - $entity_base_table = FALSE; - if ($query->propertyConditions || $has_property_order) { - if (empty($query->entityConditions['entity_type']['value'])) { - throw new EntityFieldQueryException('Property conditions and orders must have an entity type defined.'); - } - $entity_type = $query->entityConditions['entity_type']['value']; - $entity_base_table = _field_sql_storage_query_join_entity($select_query, $entity_type, $field_base_table); - $query->entityConditions['entity_type']['operator'] = '='; - $query->joinPropertyData($select_query, $entity_type, $field_base_table, 'entity_id'); - $query->addPropertyConditions($select_query, $entity_type); - } - - // The entity base keys require joining the entity base table. - $entity_base_keys = array('langcode', 'uuid'); - foreach ($query->entityConditions as $key => $condition) { - $table = FALSE; - if (!in_array($key, $entity_base_keys)) { - $table = $field_base_table; - } - else { - // Join only if we did not already before. - if (empty($entity_base_table)) { - if (empty($query->entityConditions['entity_type']['value'])) { - throw new EntityFieldQueryException('Property conditions and orders must have an entity type defined.'); - } - $entity_type = $query->entityConditions['entity_type']['value']; - $entity_base_table = _field_sql_storage_query_join_entity($select_query, $entity_type, $field_base_table); - } - if (empty($entity_schema)) { - $entity_schema = drupal_get_schema($entity_base_table); - $entity_info = entity_get_info($entity_type); - } - $key = !empty($entity_info['entity keys'][$key]) ? $entity_info['entity keys'][$key] : $key; - if (!empty($entity_schema['fields'][$key])) { - $table = $entity_base_table; - } - } - // Apply the condition only if we have a valid table: entity conditions may - // refer to non existing columns. - if ($table) { - $query->addCondition($select_query, "$table.$key", $condition); - } - } - - // Order the query. - foreach ($query->order as $order) { - if ($order['type'] == 'entity') { - $key = $order['specifier']; - $select_query->orderBy("$field_base_table.$key", $order['direction']); - } - elseif ($order['type'] == 'field') { - $specifier = $order['specifier']; - $field = $specifier['field']; - $table_alias = $table_aliases[$specifier['index']]; - $sql_field = "$table_alias." . _field_sql_storage_columnname($field['field_name'], $specifier['column']); - $select_query->orderBy($sql_field, $order['direction']); - } - elseif ($order['type'] == 'property') { - $query->addPropertyOrderBy($select_query, $entity_type, $order); - } - } - - return $query->finishQuery($select_query, $id_key); -} - -/** - * Adds the base entity table to a field query object. - * - * @param SelectQuery $select_query - * A SelectQuery containing at least one table as specified by - * _field_sql_storage_tablename(). - * @param $entity_type - * The entity type for which the base table should be joined. - * @param $field_base_table - * Name of a table in $select_query. As only INNER JOINs are used, it does not - * matter which. - * - * @return - * The name of the entity base table that was joined in. - */ -function _field_sql_storage_query_join_entity(Select $select_query, $entity_type, $field_base_table) { - $entity_info = entity_get_info($entity_type); - $entity_base_table = $entity_info['base table']; - $entity_field = $entity_info['entity keys']['id']; - $select_query->join($entity_base_table, $entity_base_table, "$entity_base_table.$entity_field = $field_base_table.entity_id"); - return $entity_base_table; -} - -/** - * Adds field (meta) conditions to the given query objects respecting groupings. - * - * @param Drupal\Core\Entity\EntityFieldQuery $query - * The field query object to be processed. - * @param SelectQuery $select_query - * The SelectQuery that should get grouping conditions. - * @param condtions - * The conditions to be added. - * @param $table_aliases - * An associative array of table aliases keyed by field index. - * @param $column_callback - * A callback that should return the column name to be used for the field - * conditions. Accepts a field name and a field column name as parameters. - */ -function _field_sql_storage_query_field_conditions(EntityFieldQuery $query, Select $select_query, $conditions, $table_aliases, $column_callback) { - $groups = &drupal_static(__FUNCTION__, array()); - foreach ($conditions as $key => $condition) { - $table_alias = $table_aliases[$key]; - $field = $condition['field']; - // Add the specified condition. - $sql_field = "$table_alias." . $column_callback($field['field_name'], $condition['column']); - $query->addCondition($select_query, $sql_field, $condition); - // Add delta / langcode group conditions. - foreach (array('delta', 'langcode') as $column) { - if (isset($condition[$column . '_group'])) { - $group_name = $condition[$column . '_group']; - if (!isset($groups[$column][$group_name])) { - $groups[$column][$group_name] = $table_alias; - } - else { - $select_query->where("$table_alias.$column = " . $groups[$column][$group_name] . ".$column"); - } - } - } - } -} - -/** * Implements hook_field_storage_delete_revision(). * * This function actually deletes the data from the database. diff --git a/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Condition.php b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Condition.php new file mode 100644 index 0000000..a0d6d6a --- /dev/null +++ b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Condition.php @@ -0,0 +1,79 @@ +sqlQuery; + $tables = new Tables($sqlQuery); + foreach ($this->conditions as $condition) { + if ($condition['field'] instanceOf ConditionInterface) { + $sqlCondition = new SqlCondition($condition['field']->getConjunction()); + // Add the SQL query to the object before calling this method again. + $sqlCondition->sqlQuery = $sqlQuery; + $condition['field']->compile($sqlCondition); + $sqlQuery->condition($sqlCondition); + } + else { + $type = strtoupper($this->conjunction) == 'OR' || $condition['operator'] == 'IS NULL' ? 'LEFT' : 'INNER'; + $this->translateCondition($condition); + $field = $tables->addField($condition['field'], $type, $condition['langcode']); + $conditionContainer->condition($field, $condition['value'], $condition['operator']); + } + } + } + + /** + * Implements Drupal\Core\Entity\Query\ConditionInterface::exists(). + */ + public function exists($field, $langcode = NULL) { + return $this->condition($field, NULL, 'IS NOT NULL', $langcode); + } + + /** + * Implements Drupal\Core\Entity\Query\ConditionInterface::notExists(). + */ + public function notExists($field, $langcode = NULL) { + return $this->condition($field, NULL, 'IS NULL', $langcode); + } + + protected function translateCondition(&$condition) { + switch ($condition['operator']) { + case 'STARTS_WITH': + $condition['value'] .= '%'; + $condition['operator'] = 'LIKE'; + break; + + case 'CONTAINS': + $condition['value'] = '%' . $condition['value'] . '%'; + $condition['operator'] = 'LIKE'; + break; + + case 'ENDS_WITH': + $condition['value'] = '%' . $condition['value']; + $condition['operator'] = 'LIKE'; + break; + + } + } + +} diff --git a/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Query.php b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Query.php new file mode 100644 index 0000000..c87265b --- /dev/null +++ b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Query.php @@ -0,0 +1,172 @@ +connection = $connection; + } + + /** + * Implements Drupal\Core\Entity\Query\QueryInterface::conditionGroupFactory(). + */ + public function conditionGroupFactory($conjunction = 'AND') { + return new Condition($conjunction); + } + + /** + * Implements Drupal\Core\Entity\Query\QueryInterface::execute(). + */ + public function execute() { + $entity_type = $this->entityType; + $entity_info = entity_get_info($entity_type); + if (!isset($entity_info['base table'])) { + throw new QueryException("No base table, nothing to query."); + } + $configurable_fields = array_map(function ($data) use ($entity_type) { + return isset($data['bundles'][$entity_type]); + }, field_info_field_map()); + $base_table = $entity_info['base table']; + // Assemble a list of entity tables, primarily for use in + // \Drupal\field_sql_storage\Entity\Tables::ensureEntityTable(). + $entity_tables = array(); + $simple_query = TRUE; + // ensureEntityTable() decides whether an entity property will be queried + // from the data table or the base table based on where it finds the + // property first. The data table is prefered, which is why it gets added + // before the base table. + if (isset($entity_info['data table'])) { + $entity_tables[$entity_info['data table']] = drupal_get_schema($entity_info['data table']); + $simple_query = FALSE; + } + $entity_tables[$base_table] = drupal_get_schema($base_table); + $sqlQuery = $this->connection->select($base_table, 'base_table', array('conjunction' => $this->conjunction)); + $sqlQuery->addMetaData('configurable_fields', $configurable_fields); + // Determines the key of the column to join on. This is either the entity + // id key or the revision id key, depending on whether the entity type + // supports revisions. + $id_key = 'id'; + $id_field = $entity_info['entity keys']['id']; + $fields[$id_field] = TRUE; + if (empty($entity_info['entity keys']['revision'])) { + // Add the key field for fetchAllKeyed(). When there is no revision + // support, this is the entity key. + $sqlQuery->addField('base_table', $entity_info['entity keys']['id']); + } + else { + // Add the key field for fetchAllKeyed(). When there is revision + // support, this is the revision key. + $revision_field = $entity_info['entity keys']['revision']; + $fields[$revision_field] = TRUE; + $sqlQuery->addField('base_table', $revision_field); + // Now revision id is column 0 and the value column is 1. + if ($this->age == FIELD_LOAD_CURRENT) { + $id_key = 'revision'; + } + } + // Now add the value column for fetchAllKeyed(). This is always the + // entity id. + $sqlQuery->addField('base_table', $id_field); + if ($this->accessCheck) { + $sqlQuery->addTag($entity_type . '_access'); + } + $sqlQuery->addTag('entity_query'); + $sqlQuery->addTag('entity_query_' . $this->entityType); + // This now contains first the table containing entity properties and + // last the entity base table. They might be the same. + $sqlQuery->addMetaData('entity_tables', $entity_tables); + $sqlQuery->addMetaData('age', $this->age); + // This contains the relevant SQL field to be used when joining entity + // tables. + $sqlQuery->addMetaData('entity_id_field', $entity_info['entity keys'][$id_key]); + // This contains the relevant SQL field to be used when joining field + // tables. + $sqlQuery->addMetaData('field_id_field', $id_key == 'id' ? 'entity_id' : 'revision_id'); + $sqlQuery->addMetaData('simple_query', $simple_query); + $this->condition->compile($sqlQuery); + if ($this->count) { + $this->sort = FALSE; + } + // Gather the SQL field aliases first to make sure every field table + // necessary is added. This might change whether the query is simple or + // not. See below for more on simple queries. + $sort = array(); + if ($this->sort) { + $tables = new Tables($sqlQuery); + foreach ($this->sort as $property => $data) { + $sort[$property] = isset($fields[$property]) ? $property : $tables->addField($property, 'LEFT', $data['langcode']); + } + } + // If the query is set up for paging either via pager or by range or a + // count is requested, then the correct amount of rows returned is + // important. If the entity has a data table or multiple value fields are + // involved then each revision might appear in several rows and this needs + // a significantly more complex query. + $simple_query = (!$this->pager && !$this->range && !$this->count) || $sqlQuery->getMetaData('simple_query'); + if (!$simple_query) { + // First, GROUP BY revision id (if it has been added) and entity id. + // Now each group contains a single revision of an entity. + foreach (array_keys($fields) as $field) { + $sqlQuery->groupBy($field); + } + } + // Now we know whether this is a simple query or not, actually do the + // sorting. + foreach ($sort as $property => $sql_alias) { + $direction = $this->sort[$property]['direction']; + if ($simple_query || isset($fields[$property])) { + // Simple queries, and the grouped columns of complicated queries + // can be ordered normally, without the aggregation function. + $sqlQuery->orderBy($sql_alias, $direction); + } + else { + // Order based on the smallest element of each group if the + // direction is ascending, or on the largest element of each group + // if the direction is descending. + $function = $direction == 'ASC' ? 'min' : 'max'; + $expression = "$function($sql_alias)"; + $sqlQuery->addExpression($expression, "order_by_{$property}_$direction"); + $sqlQuery->orderBy($expression, $direction); + } + } + $this->initializePager(); + if ($this->range) { + $sqlQuery->range($this->range['start'], $this->range['length']); + } + if ($this->count) { + return $sqlQuery->countQuery()->execute()->fetchField(); + } + // Return a keyed array of results. The key is either the revision_id or + // the entity_id depending on whether the entity type supports revisions. + // The value is always the entity id. + return $sqlQuery->execute()->fetchAllKeyed(); + } + +} diff --git a/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/QueryFactory.php b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/QueryFactory.php new file mode 100644 index 0000000..22739df --- /dev/null +++ b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/QueryFactory.php @@ -0,0 +1,24 @@ +connection = $connection; + } + + function get($entity_type, $conjunction) { + return new Query($entity_type, $conjunction, $this->connection); + } +} diff --git a/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Tables.php b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Tables.php new file mode 100644 index 0000000..068b26c --- /dev/null +++ b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Tables.php @@ -0,0 +1,147 @@ +sqlQuery = $sql_query; + } + + /** + * @param string $field + * If it contains a dot, then field name dot field column. If it doesn't + * then entity property name. + * @param string $type + * Join type, can either be INNER or LEFT. + * @return string + * The return value is a string containing the alias of the table, a dot + * and the appropriate SQL column as passed in. This allows the direct use + * of this in a query for a condition or sort. + */ + function addField($field, $type, $langcode) { + $parts = explode('.', $field); + $property = $parts[0]; + $configurable_fields = $this->sqlQuery->getMetaData('configurable_fields'); + if (!empty($configurable_fields[$property]) || substr($property, 0, 3) == 'id:') { + $field_name = $property; + $table = $this->ensureFieldTable($field_name, $type, $langcode); + // Default to .value. + $column = isset($parts[1]) ? $parts[1] : 'value'; + $sql_column = _field_sql_storage_columnname($field_name, $column); + } + else { + $sql_column = $property; + $table = $this->ensureEntityTable($property, $type, $langcode); + } + return "$table.$sql_column"; + } + + /** + * Join entity table if necessary and return the alias for it. + * + * @param string $property + * @return string + * @throws \Drupal\Core\Entity\Query\QueryException + */ + protected function ensureEntityTable($property, $type, $langcode) { + $entity_tables = $this->sqlQuery->getMetaData('entity_tables'); + if (!$entity_tables) { + throw new QueryException('Can not query entity properties without entity tables.'); + } + foreach ($entity_tables as $table => $schema) { + if (isset($schema['fields'][$property])) { + if (!isset($this->entityTables[$table])) { + $id_field = $this->sqlQuery->getMetaData('entity_id_field'); + $this->entityTables[$table] = $this->addJoin($type, $table, "%alias.$id_field = base_table.$id_field", $langcode); + } + return $this->entityTables[$table]; + } + } + throw new QueryException(format_string('@property not found', array('@property' => $property))); + } + + /** + * Join field table if necessary. + * + * @param $field_name + * Name of the field. + * @return string + * @throws \Drupal\Core\Entity\Query\QueryException + */ + protected function ensureFieldTable(&$field_name, $type, $langcode) { + if (!isset($this->fieldTables[$field_name])) { + // This is field_purge_batch() passing in a field id. + if (substr($field_name, 0, 3) == 'id:') { + $field = field_info_field_by_id(substr($field_name, 3)); + } + else { + $field = field_info_field($field_name); + } + if (!$field) { + throw new QueryException(format_string('field @field_name not found', array('@field_name' => $field_name))); + } + // This is really necessary only for the id: case but it can't be run + // before throwing the exception. + $field_name = $field['field_name']; + $table = $this->sqlQuery->getMetaData('age') == FIELD_LOAD_CURRENT ? _field_sql_storage_tablename($field) : _field_sql_storage_revision_tablename($field); + $field_id_field = $this->sqlQuery->getMetaData('field_id_field'); + $entity_id_field = $this->sqlQuery->getMetaData('entity_id_field'); + if ($field['cardinality'] != 1) { + $this->sqlQuery->addMetaData('simple_query', FALSE); + } + $this->fieldTables[$field_name] = $this->addJoin($type, $table, "%alias.$field_id_field = base_table.$entity_id_field", $langcode); + } + return $this->fieldTables[$field_name]; + } + + protected function addJoin($type, $table, $join_condition, $langcode) { + $arguments = array(); + if ($langcode) { + $placeholder = ':langcode' . $this->sqlQuery->nextPlaceholder(); + $join_condition .= ' AND %alias.langcode = ' . $placeholder; + $arguments[$placeholder] = $langcode; + } + return $this->sqlQuery->addJoin($type, $table, NULL, $join_condition, $arguments); + } + +} diff --git a/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/FieldSqlStorageBundle.php b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/FieldSqlStorageBundle.php new file mode 100644 index 0000000..2d5f996 --- /dev/null +++ b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/FieldSqlStorageBundle.php @@ -0,0 +1,22 @@ +register('entity.query.field_sql_storage', 'Drupal\field_sql_storage\Entity\QueryFactory') + ->addArgument(new Reference('database')); + } + +} diff --git a/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Tests/FieldSqlStorageTest.php b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Tests/FieldSqlStorageTest.php index 3cabd58..5681f59 100644 --- a/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Tests/FieldSqlStorageTest.php +++ b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Tests/FieldSqlStorageTest.php @@ -309,7 +309,7 @@ function testUpdateFieldSchemaWithData() { $instance = field_create_instance($instance); $entity = field_test_create_entity(0, 0, $instance['bundle']); $entity->decimal52[LANGUAGE_NOT_SPECIFIED][0]['value'] = '1.235'; - field_attach_insert('test_entity', $entity); + $entity->save(); // Attempt to update the field in a way that would work without data. $field['settings']['scale'] = 3; @@ -367,9 +367,9 @@ function testFieldUpdateIndexesWithData() { } // Add data so the table cannot be dropped. - $entity = field_test_create_entity(0, 0, $instance['bundle']); + $entity = field_test_create_entity(1, 1, $instance['bundle']); $entity->{$field_name}[LANGUAGE_NOT_SPECIFIED][0]['value'] = 'field data'; - field_attach_insert('test_entity', $entity); + $entity->save(); // Add an index $field = array('field_name' => $field_name, 'indexes' => array('value' => array(array('value', 255)))); @@ -387,8 +387,8 @@ function testFieldUpdateIndexesWithData() { } // Verify that the tables were not dropped. - $entity = field_test_create_entity(0, 0, $instance['bundle']); - field_attach_load('test_entity', array(0 => $entity)); + $entity = field_test_create_entity(1, 1, $instance['bundle']); + field_attach_load('test_entity', array(1 => $entity)); $this->assertEqual($entity->{$field_name}[LANGUAGE_NOT_SPECIFIED][0]['value'], 'field data', t("Index changes performed without dropping the tables")); } diff --git a/core/modules/field/modules/options/options.module b/core/modules/field/modules/options/options.module index 31652e3..e958510 100644 --- a/core/modules/field/modules/options/options.module +++ b/core/modules/field/modules/options/options.module @@ -6,7 +6,7 @@ */ use Drupal\field\FieldUpdateForbiddenException; -use Drupal\Core\Entity\EntityFieldQuery; +use Drupal\Core\Entity\Query\QueryFactory; /** * Implements hook_help(). @@ -384,12 +384,19 @@ function options_field_update_forbid($field, $prior_field, $has_data) { */ function _options_values_in_use($field, $values) { if ($values) { - $query = new EntityFieldQuery(); - $found = $query - ->fieldCondition($field['field_name'], 'value', $values) - ->range(0, 1) - ->execute(); - return !empty($found); + $field = field_info_field_by_id($field['id']); + $factory = drupal_container()->get('entity.query'); + foreach ($field['bundles'] as $entity_type => $bundle) { + $result = $factory->get($entity_type) + ->condition($field['field_name'] . '.value', $values) + ->count() + ->accessCheck(FALSE) + ->range(0, 1) + ->execute(); + if ($result) { + return TRUE; + } + } } return FALSE; diff --git a/core/modules/field/tests/modules/field_test/field_test.field.inc b/core/modules/field/tests/modules/field_test/field_test.field.inc index f47ccad..2538b91 100644 --- a/core/modules/field/tests/modules/field_test/field_test.field.inc +++ b/core/modules/field/tests/modules/field_test/field_test.field.inc @@ -66,7 +66,7 @@ function field_test_field_load($entity_type, $entities, $field, $instances, $lan foreach ($items as $id => $item) { // To keep the test non-intrusive, only act for instances with the // test_hook_field_load setting explicitly set to TRUE. - if ($instances[$id]['settings']['test_hook_field_load']) { + if (!empty($instances[$id]['settings']['test_hook_field_load'])) { foreach ($item as $delta => $value) { // Don't add anything on empty values. if ($value) { diff --git a/core/modules/field/tests/modules/field_test/field_test.module b/core/modules/field/tests/modules/field_test/field_test.module index 6d3195c..832f21c 100644 --- a/core/modules/field/tests/modules/field_test/field_test.module +++ b/core/modules/field/tests/modules/field_test/field_test.module @@ -13,8 +13,6 @@ * test helper functions */ -use Drupal\Core\Entity\EntityFieldQuery; - require_once DRUPAL_ROOT . '/core/modules/field/tests/modules/field_test/field_test.entity.inc'; require_once DRUPAL_ROOT . '/core/modules/field/tests/modules/field_test/field_test.field.inc'; require_once DRUPAL_ROOT . '/core/modules/field/tests/modules/field_test/field_test.storage.inc'; @@ -188,27 +186,6 @@ function field_test_field_create_field($field) { } /** - * Implements hook_entity_query_alter(). - */ -function field_test_entity_query_alter(&$query) { - if (!empty($query->alterMyExecuteCallbackPlease)) { - $query->executeCallback = 'field_test_dummy_field_storage_query'; - } -} - -/** - * Pseudo-implements hook_field_storage_query(). - */ -function field_test_dummy_field_storage_query(EntityFieldQuery $query) { - // Return dummy values that will be checked by the test. - return array( - 'user' => array( - 1 => (object) array('entity_id' => 1, 'revision_id' => NULL, 'bundle' => NULL), - ), - ); -} - -/** * Entity label callback. * * @param $entity_type diff --git a/core/modules/file/file.field.inc b/core/modules/file/file.field.inc index 28a32a9..4688926 100644 --- a/core/modules/file/file.field.inc +++ b/core/modules/file/file.field.inc @@ -930,6 +930,29 @@ function file_field_formatter_view($entity_type, $entity, $field, $instance, $la } /** + * Determine whether a field references files stored in {file_managed}. + * + * @param array $field + * A field array. + * + * @return + * The field column if the field references {file_managed}.fid, typically + * fid, FALSE if it doesn't. + */ +function file_field_find_file_reference_column($field) { + foreach ($field['foreign keys'] as $data) { + if ($data['table'] == 'file_managed') { + foreach ($data['columns'] as $field_column => $column) { + if ($column == 'fid') { + return $field_column; + } + } + } + } + return FALSE; +} + +/** * Returns HTML for a file attachments table. * * @param $variables diff --git a/core/modules/file/file.module b/core/modules/file/file.module index 4e6c8eb..5578848 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -5,7 +5,6 @@ * Defines a "managed_file" Form API field and a "file" field for Field module. */ -use Drupal\Core\Entity\EntityFieldQuery; use Drupal\file\File; use Drupal\Core\Template\Attribute; use Symfony\Component\HttpFoundation\JsonResponse; @@ -124,7 +123,7 @@ function file_entity_info() { * @see hook_file_load() * @see file_load() * @see entity_load() - * @see Drupal\Core\Entity\EntityFieldQuery + * @see Drupal\Core\Entity\Query\EntityQueryInterface */ function file_load_multiple(array $fids = NULL) { return entity_load_multiple('file', $fids); @@ -668,7 +667,7 @@ function file_file_download($uri, $field_type = 'file') { // an image preview on a node/add form) in which case, allow download by the // file's owner. if (empty($references) && ($file->status == FILE_STATUS_PERMANENT || $file->uid != $user->uid)) { - return; + return; } // Default to allow access. @@ -680,34 +679,13 @@ function file_file_download($uri, $field_type = 'file') { // and no reference denied access, access is granted as well. If at least one // reference denied access, access is denied. foreach ($references as $field_name => $field_references) { - foreach ($field_references as $entity_type => $type_references) { - foreach ($type_references as $id => $reference) { - // Try to load $entity and $field. - $entity = entity_load($entity_type, $id); + foreach ($field_references as $entity_type => $entities) { + foreach ($entities as $entity) { $field = field_info_field($field_name); - - // Load the field item that references the file. - $match = FALSE; - if ($entity) { - // Load all fields items for that entity. - $field_items = field_get_items($entity_type, $entity, $field_name); - - // Try to find the field item that references the given file. - foreach ($field_items as $item) { - if ($file->fid == $item['fid']) { - $match = TRUE; - break; - } - } - } - - // Check that $entity and $field were loaded successfully, a field - // item that references the file exists and check if access to that - // field is not disallowed. If any of these checks fail, stop checking - // access for this reference. - if (empty($entity) || empty($field) || !$match || !field_access('view', $field, $entity_type, $entity)) { + // Check if access to this field is not disallowed. + if (!field_access('view', $field, $entity_type, $entity)) { $denied = TRUE; - break; + continue; } // Invoke hook and collect grants/denies for download access. @@ -1555,27 +1533,83 @@ function file_icon_map(File $file) { * FIELD_LOAD_CURRENT to retrieve references only in the current revisions. * @param $field_type * (optional) The name of a field type. If given, limits the reference check - * to fields of the given type. + * to fields of the given type. If both $field and $field_type is given but + * $field is not the same type as $field_type, an empty array will be + * returned. * * @return - * An integer value. + * A multidimensional array. The keys are field_name, entity_type, + * entity_id and the value is an entity referencing this file. */ function file_get_file_references(File $file, $field = NULL, $age = FIELD_LOAD_REVISION, $field_type = 'file') { - $references = drupal_static(__FUNCTION__, array()); - $fields = isset($field) ? array($field['field_name'] => $field) : field_info_fields(); - - foreach ($fields as $field_name => $file_field) { - if ((empty($field_type) || $file_field['type'] == $field_type) && !isset($references[$field_name])) { - // Get each time this file is used within a field. - $query = new EntityFieldQuery(); - $query - ->fieldCondition($file_field, 'fid', $file->fid) - ->age($age); - $references[$field_name] = $query->execute(); + $references = &drupal_static(__FUNCTION__, array()); + $field_columns = &drupal_static(__FUNCTION__ . ':field_columns', array()); + + // Fill the static cache, disregard $field and $field_type for now. + if (!isset($references[$file->fid][$age])) { + $references[$file->fid][$age] = array(); + $usage_list = file_usage()->listUsage($file); + $file_usage_list = isset($usage_list['file']) ? $usage_list['file'] : array(); + foreach ($file_usage_list as $entity_type => $entity_ids) { + $entity_info = entity_get_info($entity_type); + // The usage table contains usage of every revision. If we are looking + // for every revision or the entity does not support revisions then + // every usage is already a match. + $match_entity_type = $age == FIELD_LOAD_REVISION || !isset($entity_info['entity keys']['revision']); + $entities = entity_load_multiple($entity_type, $entity_ids); + foreach ($entities as $entity) { + $bundle = $entity->bundle(); + // We need to find file fields for this entity type and bundle. + if (!isset($file_fields[$entity_type][$bundle])) { + $file_fields[$entity_type][$bundle] = array(); + // This contains the possible field names. + $instances = field_info_instances($entity_type, $bundle); + foreach ($instances as $field_name => $instance) { + $current_field = field_info_field($field_name); + // If this is the first time this field type is seen, check + // whether it references files. + if (!isset($field_columns[$current_field['type']])) { + $field_columns[$current_field['type']] = file_field_find_file_reference_column($current_field); + } + // If the field type does reference files then record it. + if ($field_columns[$current_field['type']]) { + $file_fields[$entity_type][$bundle][$field_name] = $field_columns[$current_field['type']]; + } + } + } + foreach ($file_fields[$entity_type][$bundle] as $field_name => $field_column) { + $match = $match_entity_type; + // If we didn't match yet then iterate over the field items to find + // the referenced file. This will fail if the usage checked is in a + // non-current revision because field items are from the current + // revision. + if (!$match && ($field_items = field_get_items($entity_type, $entity, $field_name))) { + foreach ($field_items as $item) { + if ($file->fid == $item[$field_column]) { + $match = TRUE; + break; + } + } + } + if ($match) { + $references[$file->fid][$age][$field_name][$entity_type][$entity->id()] = $entity; + } + } + } } } - - return isset($field) ? $references[$field['field_name']] : array_filter($references); + $return = $references[$file->fid][$age]; + // Filter the static cache down to the requested entries. The usual static + // cache is very small so this will be very fast. + if ($field || $field_type) { + foreach ($return as $field_name => $data) { + $current_field = field_info_field($field_name); + if (($field_type && $current_field['type'] != $field_type) || ($field && $field['id'] != $current_field['id'])) { + unset($return[$field_name]); + } + } + } + return $return; } /** diff --git a/core/modules/node/node.module b/core/modules/node/node.module index 55989de..dd08f84 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -1007,7 +1007,7 @@ function node_invoke($node, $hook, $a2 = NULL, $a3 = NULL, $a4 = NULL) { * An array of node entities indexed by nid. * * @see entity_load_multiple() - * @see Drupal\Core\Entity\EntityFieldQuery + * @see Drupal\Core\Entity\Query\EntityQueryInterface */ function node_load_multiple(array $nids = NULL, $reset = FALSE) { return entity_load_multiple('node', $nids, $reset); @@ -3081,34 +3081,6 @@ function node_access_view_all_nodes($account = NULL) { * 'update' and 'delete'). */ function node_query_node_access_alter(AlterableInterface $query) { - _node_query_node_access_alter($query, 'node'); -} - -/** - * Implements hook_query_TAG_alter(). - * - * This function implements the same functionality as - * node_query_node_access_alter() for the SQL field storage engine. Node access - * conditions are added for field values belonging to nodes only. - */ -function node_query_entity_field_access_alter(AlterableInterface $query) { - _node_query_node_access_alter($query, 'entity'); -} - -/** - * Helper for node access functions. - * - * @param $query - * The query to add conditions to. - * @param $type - * Either 'node' or 'entity' depending on what sort of query it is. See - * node_query_node_access_alter() and node_query_entity_field_access_alter() - * for more. - * - * @see node_query_node_access_alter() - * @see node_query_entity_field_access_alter() - */ -function _node_query_node_access_alter($query, $type) { global $user; // Read meta-data from query, if provided. @@ -3157,27 +3129,6 @@ function _node_query_node_access_alter($query, $type) { // the node_access table. $grants = node_access_grants($op, $account); - if ($type == 'entity') { - // The original query looked something like: - // @code - // SELECT nid FROM sometable s - // INNER JOIN node_access na ON na.nid = s.nid - // WHERE ($node_access_conditions) - // @endcode - // - // Our query will look like: - // @code - // SELECT entity_type, entity_id - // FROM field_data_something s - // LEFT JOIN node_access na ON s.entity_id = na.nid - // WHERE (entity_type = 'node' AND $node_access_conditions) OR (entity_type <> 'node') - // @endcode - // - // So instead of directly adding to the query object, we need to collect - // all of the node access conditions in a separate db_and() object and - // then add it to the query at the end. - $node_conditions = db_and(); - } foreach ($tables as $nalias => $tableinfo) { $table = $tableinfo['table']; if (!($table instanceof SelectInterface) && $table == $base_table) { @@ -3204,37 +3155,11 @@ function _node_query_node_access_alter($query, $type) { $subquery->condition('na.grant_' . $op, 1, '>='); $field = 'nid'; // Now handle entities. - if ($type == 'entity') { - // Set a common alias for entities. - $base_alias = $nalias; - $field = 'entity_id'; - } $subquery->where("$nalias.$field = na.nid"); - // For an entity query, attach the subquery to entity conditions. - if ($type == 'entity') { - $node_conditions->exists($subquery); - } - // Otherwise attach it to the node query itself. - else { - $query->exists($subquery); - } + $query->exists($subquery); } } - - if ($type == 'entity' && count($subquery->conditions())) { - // All the node access conditions are only for field values belonging to - // nodes. - $node_conditions->condition("$base_alias.entity_type", 'node'); - $or = db_or(); - $or->condition($node_conditions); - // If the field value belongs to a non-node entity type then this function - // does not do anything with it. - $or->condition("$base_alias.entity_type", 'node', '<>'); - // Add the compiled set of rules to the query. - $query->condition($or); - } - } /** diff --git a/core/modules/node/tests/modules/node_access_test/node_access_test.module b/core/modules/node/tests/modules/node_access_test/node_access_test.module index a503217..f5fc00f 100644 --- a/core/modules/node/tests/modules/node_access_test/node_access_test.module +++ b/core/modules/node/tests/modules/node_access_test/node_access_test.module @@ -7,8 +7,6 @@ * a special 'node test view' permission. */ -use Drupal\Core\Entity\EntityFieldQuery; - use Drupal\node\Node; /** @@ -152,12 +150,13 @@ function node_access_test_page() { function node_access_entity_test_page() { $output = ''; try { - $query = new EntityFieldQuery; - $result = $query->fieldCondition('body', 'value', 'A', 'STARTS_WITH')->execute(); - if (!empty($result['node'])) { - $output .= '
Yes, ' . count($result['node']) . ' nodes
'; + $result = entity_query('node') + ->condition('body.value', 'A', 'STARTS_WITH') + ->execute(); + if (!empty($result)) { + $output .= 'Yes, ' . count($result) . ' nodes
'; $output .= '