diff --git a/core/includes/entity.api.php b/core/includes/entity.api.php index 9c9d123..23f72b4 100644 --- a/core/includes/entity.api.php +++ b/core/includes/entity.api.php @@ -345,20 +345,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\EntityQueryInterface $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 @@ -366,8 +355,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\EntityQueryInterface $query) { + } /** diff --git a/core/includes/entity.inc b/core/includes/entity.inc index 75c3386..0836a1a 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; @@ -122,7 +119,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); @@ -214,7 +211,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) { diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index ed15dd5..86bd7f0 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -66,6 +66,9 @@ public function build(ContainerBuilder $container) { $container->register('router.builder', 'Drupal\Core\Routing\RouteBuilder') ->addArgument(new Reference('router.dumper')); + $container->register('entity.query', 'Drupal\Core\Entity\Query\QueryFactory') + ->addArgument(new Reference('service_container')); + // @todo Replace below lines with the commented out block below it when it's // performant to do so: http://drupal.org/node/1706064. $dispatcher = $container->get('dispatcher'); 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 1fa59ec..9db3013 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; @@ -247,30 +248,24 @@ public function loadRevision($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 = drupal_container()->get('entity.query')->get($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\EntityQueryInterface $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); } } 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 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..fdc9028 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Query/ConditionBase.php @@ -0,0 +1,92 @@ +conjunction = $conjunction; + } + + public function getConjunction() { + return $this->conjunction; + } + + /** + * Implements Countable::count(). + * + * Returns the size of this conditional. The size of the conditional is the + * size of its conditional array minus one, because one element is the the + * conjunction. + */ + public function count() { + return count($this->conditions) - 1; + } + + public function condition($field, $value = NULL, $operator = NULL) { + $this->conditions[] = array( + 'field' => $field, + 'value' => $value, + 'operator' => $operator, + ); + + $this->changed = TRUE; + + return $this; + } + + public function isNull($field) { + return $this->condition($field, NULL, 'IS NULL'); + } + + public function isNotNull($field) { + return $this->condition($field, NULL, 'IS NOT NULL'); + } + + public function &conditions() { + return $this->conditions; + } + + 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..3c12363 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Query/ConditionInterface.php @@ -0,0 +1,65 @@ +entityType = $entity_type; + $this->condition = $this->conditionFactory($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) { + $this->condition->condition($property, $value, $operator); + 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::andCondition(). + */ + public function andCondition() { + return $this->conditionFactory('and'); + } + + /** + * Implements Drupal\Core\Entity\Query\QueryInterface::orCondition(). + */ + public function orCondition() { + return $this->conditionFactory('or'); + } + + /** + * Implements Drupal\Core\Entity\Query\QueryInterface::orderBy(). + */ + public function orderBy($property, $direction = 'ASC') { + $this->sort[$property] = $direction; + return $this; + } + + /** + * Implements Drupal\Core\Entity\Query\QueryInterface::count(). + */ + public function count() { + $this->count = TRUE; + return $this; + } + + /** + * Implements Drupal\Core\Entity\Query\QueryInterface::skipAccessCheck(). + */ + public function skipAccessCheck() { + $this->accessCheck = FALSE; + 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") { + $storages = array(); + $storage_type = ''; + foreach (field_info_instances($entity_type) as $instances) { + foreach ($instances as $instance) { + $field = field_info_field_by_id($instance['field_id']); + $field_id = $field['id']; + $storage_type = $field['storage']['type']; + $storages[$storage_type][$field_id] = $field_id; + } + } + if ($storage_type) { + if (count($storages) > 1) { + throw new QueryException('Can not query across storage engines'); + } + $storage_info = field_info_storage_types($storage_type); + $module = $storage_info['module']; + } + else { + $module = 'field_sql_storage'; + } + return $this->container->get("entity.query.$module")->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..db24892 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Query/QueryInterface.php @@ -0,0 +1,78 @@ + 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_common_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,33 @@ 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 $entity_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 1c3c4fb..3d78652 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 @@ -1075,16 +1075,21 @@ 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']); + $factory = drupal_container()->get('entity.query'); + foreach($field['bundles'] as $entity_type => $bundle) { + $result = $factory->get($entity_type) + ->condition($field['field_name'] .'.delta', 0, '>=') + ->count() + ->skipAccessCheck() + ->range(0, 1) + ->execute(); + if ($result) { + return TRUE; + } + } + + return FALSE; } /** @@ -1317,9 +1322,6 @@ 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). @@ -1338,4 +1340,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_common_columns() { + return array(/*'entity_type', 'bundle', 'entity_id', 'revision_id', */'deleted', 'langcode', 'delta'); +} diff --git a/core/modules/field/lib/Drupal/field/Tests/BulkDeleteTest.php b/core/modules/field/lib/Drupal/field/Tests/BulkDeleteTest.php index 84b6f06..f1404a5 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. */ @@ -50,7 +48,7 @@ protected function convertToPartialEntities($entities, $field_name) { foreach ($entities as $id => $entity) { // Re-create the entity with only the required keys, remove label as that // is not present when using _field_create_entity_from_ids(). - $partial_entities[$id] = field_test_create_entity($entity->ftid, $entity->ftvid, $entity->fttype, $entity->ftlabel); + $partial_entities[$id] = field_test_create_entity($entity->ftid, NULL, $entity->fttype, $entity->ftlabel); // Remove the label and set enforceIsNew to NULL to make sure that the // entity classes match the actual arguments. unset($partial_entities[$id]->ftlabel); @@ -113,7 +111,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) { @@ -133,14 +131,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; + } } /** @@ -155,14 +154,13 @@ function setUp() { function testDeleteFieldInstance() { $bundle = reset($this->bundles); $field = reset($this->fields); + $field_name = $field['field_name']; // There are 10 entities of this bundle. - $query = new EntityFieldQuery(); - $found = $query - ->fieldCondition($field) - ->entityCondition('bundle', $bundle) + $found = drupal_container()->get('entity.query')->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); @@ -174,27 +172,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 = drupal_container()->get('entity.query')->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 = drupal_container()->get('entity.query')->get('test_entity') + ->condition('fttype', $bundle) + ->condition("$field_name.deleted", 1) + ->orderBy('ftid') ->execute(); - $entities = array(); - foreach ($found[$this->entity_type] as $ids) { - $entities[$ids->entity_id] = _field_create_entity_from_ids($ids); + $ids = (object) array( + 'entity_type' => 'test_entity', + 'bundle' => $bundle, + ); + $entities = array();; + 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"); } @@ -225,13 +226,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 = drupal_container()->get('entity.query')->get($this->entity_type) + ->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 26b2ba8..9809bf0 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_common_columns()) ? $column : $name . '_' . $column; } /** @@ -505,191 +502,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..a0d549e --- /dev/null +++ b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Condition.php @@ -0,0 +1,66 @@ + array(), 'field' => array()); + + /** + * @param \Drupal\Core\Database\Query\ConditionInterface $sqlQuery + */ + public function compile($conditionContainer) { + // If this is not the top level condition group then the sql query is + // added to the $conditionContainer object by this function itself. The SQL query + // object is only necessary to pass to Query::getField() so that it can + // join tables as necessary. conditions need to be added to the $conditionContainer + // object to keep grouping. + $sqlQuery = $conditionContainer instanceof SelectInterface ? $conditionContainer : $conditionContainer->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 { + $field = Query::getField($sqlQuery, $this->tables, $condition['field']); + $this->translateCondition($condition); + $conditionContainer->condition($field, $condition['value'], $condition['operator']); + } + } + } + + 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..37d6712 --- /dev/null +++ b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Query.php @@ -0,0 +1,139 @@ + array(), 'field' => array()); + + /** + * @var \Drupal\Core\Database\Connection + */ + protected $connection; + + /** + * @param string $entity_type + * @param string $conjunction + * @param * @param \Symfony\Component\DependencyInjection\ContainerInterface $container + */ + public function __construct($entity_type, $conjunction, $connection) { + parent::__construct($entity_type, $conjunction); + $this->connection = $connection; + } + + public function conditionFactory($conjunction = 'AND') { + return new Condition($conjunction); + } + + /** + * @param $headers + * @return mixed + */ + public function tableSort(&$headers) { + // TODO: Implement tableSort() method. + } + + + /** + * @return mixed + */ + public function execute() { + $entity_info = entity_get_info($this->entityType); + if (!isset($entity_info['base table'])) { + throw new QueryException("No base table, nothing to query."); + } + $base_table = $entity_info['base table']; + $this->tables['entity'][$base_table] = 'base_table'; + $tables = array(); + if (isset($entity_info['data table'])) { + $tables[$entity_info['data table']] = drupal_get_schema($entity_info['data table']); + } + $tables[$base_table] = drupal_get_schema($base_table); + $sqlQuery = $this->connection->select($base_table, 'base_table', array('conjunction' => $this->condition->getConjunction())); + $id_field = $entity_info['entity keys']['id']; + $sqlQuery->addField('base_table', $id_field); + if ($this->accessCheck) { + $sqlQuery->addTag($this->entityType . '_access'); + } + $sqlQuery->addTag('entity_query'); + $sqlQuery->addTag('entity_query_' . $this->entityType); + $sqlQuery->addMetaData('entity_tables', $tables); + $sqlQuery->addMetaData('entity_id_field', $id_field); + $this->condition->compile($sqlQuery); + foreach ($this->sort as $property => $direction) { + $field = Query::getField($sqlQuery, $this->tables, $property); + $sqlQuery->orderBy($field, $direction); + } + if ($this->range) { + $sqlQuery->range($this->range['start'], $this->range['length']); + } + if ($this->count) { + return $sqlQuery->countQuery()->execute()->fetchField(); + } + return $sqlQuery->execute()->fetchCol(); + } + + static function getField(SelectInterface $sqlQuery, array &$tables, $property) { + $parts = explode('.', $property); + if (count($parts) == 1) { + $sql_column = $property; + $table = Query::ensureEntityTable($sqlQuery, $tables['entity'], $property); + } + else { + list($field_name, $column) = $parts; + $sql_column = _field_sql_storage_columnname($field_name, $column); + $table = Query::ensureFieldTable($sqlQuery, $tables['field'], $field_name); + } + return "$table.$sql_column"; + } + + + static function ensureEntityTable(SelectInterface $sqlQuery, array &$tables, $property) { + $entity_tables = $sqlQuery->getMetaData('entity_tables'); + if (!$entity_tables) { + throw new QueryException('Can not query entity properties without an entity type.'); + } + foreach ($entity_tables as $table => $schema) { + if (isset($schema['fields'][$property])) { + if (!isset($tables[$table])) { + $id_field = $sqlQuery->getMetaData('entity_id_field'); + $tables[$table] = $sqlQuery->join($table, NULL, "%alias.$id_field = base_table.$id_field"); + } + return $tables[$table]; + } + } + throw new QueryException(format_string('@property not found', array('@property' => $property))); + } + + static function ensureFieldTable(SelectInterface $sqlQuery, array &$tables, $field_name) { + if (!isset($tables[$field_name])) { + $field = field_info_field($field_name); + // This is field_purge_batch() passing in a field id. + if (!$field && substr($field_name, 0, 3) == 'id:') { + $field = field_info_field_by_id(substr($field_name, 3)); + } + if (!$field) { + throw new QueryException(format_string('field @field_name not found', array('@field_name' => $field_name))); + } + $table = _field_sql_storage_tablename($field); + $id_field = $sqlQuery->getMetaData('entity_id_field'); + $tables[$field_name] = $sqlQuery->join($table, NULL, "%alias.entity_id = base_table.$id_field"); + } + return $tables[$field_name]; + } + +} 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..25e0705 --- /dev/null +++ b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/QueryFactory.php @@ -0,0 +1,21 @@ +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/FieldSqlStorageBundle.php b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/FieldSqlStorageBundle.php new file mode 100644 index 0000000..23c75e1 --- /dev/null +++ b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/FieldSqlStorageBundle.php @@ -0,0 +1,17 @@ +register('entity.query.field_sql_storage', 'Drupal\field_sql_storage\Entity\QueryFactory') + ->addArgument(new Reference('database')); + } + +} diff --git a/core/modules/field/modules/options/options.module b/core/modules/field/modules/options/options.module index 31652e3..08fbf27 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() + ->skipAccessCheck() + ->range(0, 1) + ->execute(); + if ($result) { + return TRUE; + } + } } return FALSE; diff --git a/core/modules/file/file.field.inc b/core/modules/file/file.field.inc index cbc2246..0f4e932 100644 --- a/core/modules/file/file.field.inc +++ b/core/modules/file/file.field.inc @@ -930,6 +930,30 @@ 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 0f6cb64..9b79e0f 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; @@ -116,7 +115,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); @@ -780,7 +779,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. @@ -792,34 +791,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. @@ -1667,27 +1645,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_list($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 2dd1b57..0c9c353 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -1044,7 +1044,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); @@ -3264,34 +3264,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. @@ -3340,27 +3312,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) { @@ -3387,37 +3338,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..2b6ed07 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 = drupal_container()->get('entity.query')->get('node') + ->condition('body.value', 'A', 'STARTS_WITH') + ->execute(); + if (!empty($result)) { + $output .= 'Yes, ' . count($result) . ' nodes
'; $output .= '