Commit f755b694 authored by catch's avatar catch
Browse files

Issue #1846454 by chx, dawehner, amateescu: Add Entity query to Config entities.

parent 1e26472d
......@@ -730,6 +730,9 @@ function entity_get_render_display(EntityInterface $entity, $view_mode) {
* @param $conjunction
* AND if all conditions in the query need to apply, OR if any of them is
* enough. Optional, defaults to AND.
*
* @return \Drupal\Core\Entity\Query\QueryInterface
* The query object that can query the given entity type.
*/
function entity_query($entity_type, $conjunction = 'AND') {
return drupal_container()->get('entity.query')->get($entity_type, $conjunction);
......
......@@ -428,7 +428,7 @@ protected function invokeHook($hook, EntityInterface $entity) {
* Implements Drupal\Core\Entity\EntityStorageControllerInterface::getQueryServicename().
*/
public function getQueryServicename() {
throw new \LogicException('Querying configuration entities is not supported.');
return 'entity.query.config';
}
/**
......
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\Query\Condition.
*/
namespace Drupal\Core\Config\Entity\Query;
use Drupal\Core\Entity\Query\ConditionBase;
use Drupal\Core\Entity\Query\ConditionInterface;
/**
* Defines the condition class for the config entity query.
*
* @see \Drupal\Core\Config\Entity\Query\Query
*/
class Condition extends ConditionBase {
/**
* Implements \Drupal\Core\Entity\Query\ConditionInterface::compile().
*/
public function compile($configs) {
$and = strtoupper($this->conjunction) == 'AND';
$single_conditions = array();
$condition_groups = array();
foreach ($this->conditions as $condition) {
if ($condition['field'] instanceOf ConditionInterface) {
$condition_groups[] = $condition;
}
else {
if (!isset($condition['operator'])) {
$condition['operator'] = is_array($condition['value']) ? 'IN' : '=';
}
$single_conditions[] = $condition;
}
}
$return = array();
if ($single_conditions) {
foreach ($configs as $config_name => $config) {
foreach ($single_conditions as $condition) {
$match = $this->matchArray($condition, $config, explode('.', $condition['field']));
// If AND and it's not matching, then the rest of conditions do not
// matter and this config object does not match.
// If OR and it is matching, then the rest of conditions do not
// matter and this config object does match.
if ($and != $match ) {
break;
}
}
if ($match) {
$return[$config_name] = $config;
}
}
}
elseif (!$condition_groups || $and) {
// If there were no single conditions then either:
// - Complex conditions, OR: need to start from no entities.
// - Complex conditions, AND: need to start from all entities.
// - No complex conditions (AND/OR doesn't matter): need to return all
// entities.
$return = $configs;
}
foreach ($condition_groups as $condition) {
$group_entities = $condition['field']->compile($configs);
if ($and) {
$return = array_intersect_key($return, $group_entities);
}
else {
$return = $return + $group_entities;
}
}
return $return;
}
/**
* 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);
}
/**
* Matches for an array representing one or more config paths.
*
* @param array $condition
* The condition array as created by the condition() method.
* @param array $data
* The config array or part of it.
* @param array $needs_matching
* The list of config array keys needing a match. Can contain config keys
* and the * wildcard.
* @param array $parents
* The current list of parents.
*
* @return bool
* TRUE when the condition matched to the data else FALSE.
*/
protected function matchArray(array $condition, array $data, array $needs_matching, array $parents = array()) {
$parent = array_shift($needs_matching);
$candidates = array();
if ($parent === '*') {
$candidates = array_keys($data);
}
elseif (isset($data[$parent])) {
$candidates = array($parent);
}
foreach ($candidates as $key) {
if ($needs_matching && is_array($data[$key])) {
$new_parents = $parents;
$new_parents[] = $key;
if ($this->matchArray($condition, $data[$key], $needs_matching, $new_parents)) {
return TRUE;
}
}
elseif ($this->match($condition, $data[$key])) {
return TRUE;
}
}
return FALSE;
}
/**
* Perform the actual matching.
*
* @param array $condition
* The condition array as created by the condition() method.
* @param string $value
* The value to match against.
*
* @return bool
* TRUE when matches else FALSE.
*/
protected function match(array $condition, $value) {
if (isset($value)) {
switch ($condition['operator']) {
case '=':
return $value == $condition['value'];
case '>':
return $value > $condition['value'];
case '<':
return $value < $condition['value'];
case '>=':
return $value >= $condition['value'];
case '<=':
return $value <= $condition['value'];
case 'IN':
return array_search($value, $condition['value']) !== FALSE;
case 'NOT IN':
return array_search($value, $condition['value']) === FALSE;
case 'STARTS_WITH':
return strpos($value, $condition['value']) === 0;
case 'CONTAINS':
return strpos($value, $condition['value']) !== FALSE;
case 'ENDS_WITH':
return substr($value, -strlen($condition['value'])) === (string) $condition['value'];
case 'IS NOT NULL':
return TRUE;
}
}
return $condition['operator'] === 'IS NULL';
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\Query\Query.
*/
namespace Drupal\Core\Config\Entity\Query;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Entity\EntityManager;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\Core\Entity\Query\QueryBase;
/**
* Defines the entity query for configuration entities.
*/
class Query extends QueryBase {
/**
* Stores the entity manager.
*
* @var \Drupal\Core\Entity\EntityManager
*/
protected $entityManager;
/**
* The config storage used by the config entity query.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $configStorage;
/**
* Constructs a Query object.
*
* @param string $entity_type
* The entity type.
* @param string $conjunction
* - AND: all of the conditions on the query need to match.
* - OR: at least one of the conditions on the query need to match.
* @param \Drupal\Core\Entity\EntityManager $entity_manager
* The entity manager that stores all meta information.
* @param \Drupal\Core\Config\StorageInterface $config_storage
* The actual config storage which is used to list all config items.
*/
function __construct($entity_type, $conjunction, EntityManager $entity_manager, StorageInterface $config_storage) {
parent::__construct($entity_type, $conjunction);
$this->entityManager = $entity_manager;
$this->configStorage = $config_storage;
}
/**
* Implements \Drupal\Core\Entity\Query\QueryInterface::conditionGroupFactory().
*/
public function conditionGroupFactory($conjunction = 'AND') {
return new Condition($conjunction);
}
/**
* Overrides \Drupal\Core\Entity\Query\QueryBase::condition().
*
* Additional to the syntax defined in the QueryInterface you can use
* placeholders (*) to match all keys of an subarray. Let's take the follow
* yaml file as example:
* @code
* level1:
* level2a:
* level3: 1
* level2b:
* level3: 2
* @endcode
* Then you can filter out via $query->condition('level1.*.level3', 1).
*/
public function condition($property, $value = NULL, $operator = NULL, $langcode = NULL) {
return parent::condition($property, $value, $operator, $langcode);
}
/**
* Implements \Drupal\Core\Entity\Query\QueryInterface::execute().
*/
public function execute() {
// Load all config files.
$entity_info = $this->entityManager->getDefinition($this->getEntityType());
$prefix = $entity_info['config_prefix'];
$prefix_length = strlen($prefix) + 1;
$names = $this->configStorage->listAll($prefix);
$configs = array();
foreach ($names as $name) {
$configs[substr($name, $prefix_length)] = config($name)->get();
}
$result = $this->condition->compile($configs);
// Apply sort settings.
foreach ($this->sort as $property => $sort) {
$direction = $sort['direction'] == 'ASC' ? -1 : 1;
uasort($result, function($a, $b) use ($property, $direction) {
return ($a[$property] <= $b[$property]) ? $direction : -$direction;
});
}
// Let the pager do its work.
$this->initializePager();
if ($this->range) {
$result = array_slice($result, $this->range['start'], $this->range['length'], TRUE);
}
if ($this->count) {
return count($result);
}
// Create the expected structure of entity_id => entity_id. Config
// entities have string entity IDs.
foreach ($result as $key => &$value) {
$value = (string) $key;
}
return $result;
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\Query\QueryFactory.
*/
namespace Drupal\Core\Config\Entity\Query;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Entity\EntityManager;
/**
* Provides a factory for creating entity query objects for the config backend.
*/
class QueryFactory {
/**
* The config storage used by the config entity query.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $configStorage;
/**
* Constructs a QueryFactory object.
*
* @param \Drupal\Core\Config\StorageInterface $config_storage
* The config storage used by the config entity query.
*/
public function __construct(StorageInterface $config_storage) {
return $this->configStorage = $config_storage;
}
/**
* Instantiate a entity query for a certain entity type.
*
* @param string $entity_type
* The entity type for the query.
* @param string $conjunction
* The operator to use to combine conditions: 'AND' or 'OR'.
* @param \Drupal\Core\Entity\EntityManager $entity_manager
* The entity manager that handles the entity type.
*
* @return \Drupal\Core\Config\Entity\Query\Query
* An entity query for a specific configuration entity type.
*/
public function get($entity_type, $conjunction, EntityManager $entity_manager) {
return new Query($entity_type, $conjunction, $entity_manager, $this->configStorage);
}
}
......@@ -155,9 +155,12 @@ public function build(ContainerBuilder $container) {
$this->registerTwig($container);
$this->registerRouting($container);
// Add the entity query factory.
// Add the entity query factories.
$container->register('entity.query', 'Drupal\Core\Entity\Query\QueryFactory')
->addArgument(new Reference('service_container'));
->addArgument(new Reference('plugin.manager.entity'))
->addMethodCall('setContainer', array(new Reference('service_container')));
$container->register('entity.query.config', 'Drupal\Core\Config\Entity\Query\QueryFactory')
->addArgument(new Reference('config.storage'));
$container->register('router.dumper', 'Drupal\Core\Routing\MatcherDumper')
->addArgument(new Reference('database'));
......
......@@ -7,34 +7,46 @@
namespace Drupal\Core\Entity\Query;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Entity\EntityManager;
use Symfony\Component\DependencyInjection\ContainerAware;
/**
* Factory class Creating entity query objects.
*/
class QueryFactory {
class QueryFactory extends ContainerAware {
/**
* var \Symfony\Component\DependencyInjection\ContainerInterface
* Stores the entity manager used by the query.
*
* @var \Drupal\Core\Entity\EntityManager
*/
protected $container;
protected $entityManager;
/**
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* Constructs a QueryFactory object.
*
* @param \Drupal\Core\Entity\EntityManager $entity_manager
* The entity manager used by the query.
*/
function __construct(ContainerInterface $container) {
$this->container = $container;
public function __construct(EntityManager $entity_manager) {
$this->entityManager = $entity_manager;
}
/**
* Returns a query object for a given entity type.
*
* @param string $entity_type
* The entity type.
* @param string $conjunction
* @return QueryInterface
* - AND: all of the conditions on the query need to match.
* - OR: at least one of the conditions on the query need to match.
*
* @return \Drupal\Core\Entity\Query\QueryInterface
* The query object that can query the given entity type.
*/
public function get($entity_type, $conjunction = 'AND') {
$service_name = drupal_container()->get('plugin.manager.entity')->getStorageController($entity_type)->getQueryServicename();
return $this->container->get($service_name)->get($entity_type, $conjunction);
$service_name = $this->entityManager->getStorageController($entity_type)->getQueryServicename();
return $this->container->get($service_name)->get($entity_type, $conjunction, $this->entityManager);
}
}
<?php
/**
* @file
* Contains \Drupal\config_test\Plugin\Core\Entity\ConfigQueryTest.
*/
namespace Drupal\config_test\Plugin\Core\Entity;
use Drupal\Core\Annotation\Plugin;
use Drupal\Core\Annotation\Translation;
/**
* Defines the ConfigQueryTest configuration entity used by the query test.
*
* @Plugin(
* id = "config_query_test",
* label = @Translation("Test configuration for query"),
* module = "config_test",
* controller_class = "Drupal\config_test\ConfigTestStorageController",
* list_controller_class = "Drupal\Core\Config\Entity\ConfigEntityListController",
* form_controller_class = {
* "default" = "Drupal\config_test\ConfigTestFormController"
* },
* uri_callback = "config_test_uri",
* config_prefix = "config_query_test.dynamic",
* entity_keys = {
* "id" = "id",
* "label" = "label",
* "uuid" = "uuid"
* }
* )
*
* @see \Drupal\entity\Tests\ConfigEntityQueryTest
*/
class ConfigQueryTest extends ConfigTest {
/**
* A number used by the sort tests.
*
* @var int
*/
public $number;
/**
* An array used by the wildcard tests.
*
* @var array
*/
public $array;
}
......@@ -46,6 +46,7 @@ public function conditionGroupFactory($conjunction = 'AND') {
*/
public function execute() {
$entity_type = $this->entityType;
// @todo change to a method call once http://drupal.org/node/1892462 is in.
$entity_info = entity_get_info($entity_type);
if (!isset($entity_info['base_table'])) {
throw new QueryException("No base table, invalid query.");
......
<?php
/**
* @file
* Contains \Drupal\system\Tests\ConfigEntityQueryTest.
*/
namespace Drupal\system\Tests\Entity;
use Drupal\simpletest\DrupalUnitTestBase;
/**
* Tests the config entity query.
*
* @see \Drupal\Core\Config\Entity\Query
*/
class ConfigEntityQueryTest extends DrupalUnitTestBase {
/**
* Modules to enable.
*
* @var array
*/
static $modules = array('config_test');
/**
* Stores the search results for alter comparision.
*
* @var array
*/
protected $queryResults;
/**
* The query factory used to construct all queries in the test.
*
* @var \Drupal\Core\Entity\Query\QueryFactory
*/
protected $factory;
/**
* Stores all config entities created for the test.
*
* @var array
*/
protected $entities;
public static function getInfo() {
return array(
'name' => 'Config Entity Query',
'description' => 'Tests Config Entity Query functionality.',
'group' => 'Configuration',
);
}
protected function setUp() {
parent::setUp();
$this->entities = array();
$this->enableModules(array('entity'), TRUE);
$this->factory = $this->container->get('entity.query');
// These two are here to make sure that matchArray needs to go over several
// non-matches on every levels.
$array['level1']['level2a'] = 9;
$array['level1a']['level2'] = 9;
// The tests match array.level1.level2.
$array['level1']['level2'] = 1;
$entity = entity_create('config_query_test', array(
'label' => $this->randomName(),
'id' => '1',
'number' => 31,
'array' => $array,
));
$this->entities[] = $entity;
$entity->enforceIsNew();
$entity->save();
$array['level1']['level2'] = 2;
$entity = entity_create('config_query_test', array(
'label' => $this->randomName(),
'id' => '2',
'number' => 41,
'array' => $array,
));
$this->entities[] = $entity;
$entity->enforceIsNew();
$entity->save();
$array['level1']['level2'] = 1;
$entity = entity_create('config_query_test', array(
'label' => 'test_prefix_' . $this->ran