diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php b/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php index 2dd7ace..b2a94b0 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php @@ -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'; } /** diff --git a/core/lib/Drupal/Core/Config/Entity/Query/Condition.php b/core/lib/Drupal/Core/Config/Entity/Query/Condition.php new file mode 100644 index 0000000..74c54df --- /dev/null +++ b/core/lib/Drupal/Core/Config/Entity/Query/Condition.php @@ -0,0 +1,165 @@ +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); + } + + /** + * Match for an array representing one or more config paths. + * + * @param $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 matched. + */ + protected function matchArray($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; + } + + /** + * Do the actual matching. + * + * @param $condition + * The condition array as created by the condition() method. + * @param $value + * The value to match against. + * @return bool + * TRUE when matches. + */ + function match($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'; + } + +} diff --git a/core/lib/Drupal/Core/Config/Entity/Query/Query.php b/core/lib/Drupal/Core/Config/Entity/Query/Query.php new file mode 100644 index 0000000..bdbb5cf --- /dev/null +++ b/core/lib/Drupal/Core/Config/Entity/Query/Query.php @@ -0,0 +1,111 @@ +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; + } + +} diff --git a/core/lib/Drupal/Core/Config/Entity/Query/QueryFactory.php b/core/lib/Drupal/Core/Config/Entity/Query/QueryFactory.php new file mode 100644 index 0000000..fe93713 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Entity/Query/QueryFactory.php @@ -0,0 +1,53 @@ +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); + } + +} diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index bc6afef..0729c64 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -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')); diff --git a/core/lib/Drupal/Core/Entity/Query/QueryFactory.php b/core/lib/Drupal/Core/Entity/Query/QueryFactory.php index d9b57bc..22c7799 100644 --- a/core/lib/Drupal/Core/Entity/Query/QueryFactory.php +++ b/core/lib/Drupal/Core/Entity/Query/QueryFactory.php @@ -7,24 +7,24 @@ 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 + * @var \Drupal\Core\Entity\EntityManager */ - protected $container; - + protected $entityManager; /** - * @param \Symfony\Component\DependencyInjection\ContainerInterface $container + * @param \Drupal\Core\Entity\EntityManager $entity_manager */ - function __construct(ContainerInterface $container) { - $this->container = $container; + public function __construct(EntityManager $entity_manager) { + $this->entityManager = $entity_manager; } /** @@ -33,8 +33,8 @@ function __construct(ContainerInterface $container) { * @return QueryInterface */ 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); } } diff --git a/core/modules/config/tests/config_test/lib/Drupal/config_test/Plugin/Core/Entity/ConfigQueryTest.php b/core/modules/config/tests/config_test/lib/Drupal/config_test/Plugin/Core/Entity/ConfigQueryTest.php new file mode 100644 index 0000000..c1f6b8e --- /dev/null +++ b/core/modules/config/tests/config_test/lib/Drupal/config_test/Plugin/Core/Entity/ConfigQueryTest.php @@ -0,0 +1,47 @@ +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."); diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/ConfigEntityQueryTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/ConfigEntityQueryTest.php new file mode 100644 index 0000000..aa68d67 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/ConfigEntityQueryTest.php @@ -0,0 +1,444 @@ + '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->randomName(), + 'id' => '3', + 'number' => 59, + 'array' => $array, + )); + $this->entities[] = $entity; + $entity->enforceIsNew(); + $entity->save(); + + $array['level1']['level2'] = 2; + $entity = entity_create('config_query_test', array( + 'label' => $this->randomName() . '_test_suffix', + 'id' => '4', + 'number' => 26, + 'array' => $array, + )); + $this->entities[] = $entity; + $entity->enforceIsNew(); + $entity->save(); + + $array['level1']['level2'] = 3; + $entity = entity_create('config_query_test', array( + 'label' => $this->randomName() . '_test_contains_' . $this->randomName(), + 'id' => '5', + 'number' => 53, + 'array' => $array, + )); + $this->entities[] = $entity; + $entity->enforceIsNew(); + $entity->save(); + } + + /** + * Test basic functionality. + */ + public function testConfigEntityQuery() { + // Run a test without any condition. + $this->queryResults = $this->factory->get('config_query_test') + ->execute(); + $this->assertResults(array('1', '2', '3', '4', '5')); + // No conditions, OR. + $this->queryResults = $this->factory->get('config_query_test', 'OR') + ->execute(); + $this->assertResults(array('1', '2', '3', '4', '5')); + + // Filter by ID with equality. + $this->queryResults = $this->factory->get('config_query_test') + ->condition('id', '3') + ->execute(); + $this->assertResults(array('3')); + + // Filter by label with a known prefix. + $this->queryResults = $this->factory->get('config_query_test') + ->condition('label', 'test_prefix', 'STARTS_WITH') + ->execute(); + $this->assertResults(array('3')); + + // Filter by label with a known suffix. + $this->queryResults = $this->factory->get('config_query_test') + ->condition('label', 'test_suffix', 'ENDS_WITH') + ->execute(); + $this->assertResults(array('4')); + + // Filter by label with a known containing word. + $this->queryResults = $this->factory->get('config_query_test') + ->condition('label', 'test_contains', 'CONTAINS') + ->execute(); + $this->assertResults(array('5')); + + // Filter by ID with the IN operator. + $this->queryResults = $this->factory->get('config_query_test') + ->condition('id', array('2', '3'), 'IN') + ->execute(); + $this->assertResults(array('2', '3')); + + // Filter by ID with the implicit IN operator. + $this->queryResults = $this->factory->get('config_query_test') + ->condition('id', array('2', '3')) + ->execute(); + $this->assertResults(array('2', '3')); + + // Filter by ID with the > operator. + $this->queryResults = $this->factory->get('config_query_test') + ->condition('id', '3', '>') + ->execute(); + $this->assertResults(array('4', '5')); + + // Filter by ID with the >= operator. + $this->queryResults = $this->factory->get('config_query_test') + ->condition('id', '3', '>=') + ->execute(); + $this->assertResults(array('3', '4', '5')); + + // Filter by ID with the < operator. + $this->queryResults = $this->factory->get('config_query_test') + ->condition('id', '3', '<') + ->execute(); + $this->assertResults(array('1', '2')); + + // Filter by ID with the <= operator. + $this->queryResults = $this->factory->get('config_query_test') + ->condition('id', '3', '<=') + ->execute(); + $this->assertResults(array('1', '2', '3')); + + // Filter by two conditions on the same field. + $this->queryResults = $this->factory->get('config_query_test') + ->condition('label', 'test_pref', 'STARTS_WITH') + ->condition('label', 'test_prefix', 'STARTS_WITH') + ->execute(); + $this->assertResults(array('3')); + + // Filter by two conditions on different fields. The first query matches for + // a different ID, so the result is empty. + $this->queryResults = $this->factory->get('config_query_test') + ->condition('label', 'test_prefix', 'STARTS_WITH') + ->condition('id', '5') + ->execute(); + $this->assertResults(array()); + + // Filter by two different conditions on different fields. This time the + // first condition matches on one item, but the second one does as well. + $this->queryResults = $this->factory->get('config_query_test') + ->condition('label', 'test_prefix', 'STARTS_WITH') + ->condition('id', '3') + ->execute(); + $this->assertResults(array('3')); + + // Filter by two different conditions, of which the first one matches for + // every entry, the second one as well, but just the third one filters so + // that just two are left. + $this->queryResults = $this->factory->get('config_query_test') + ->condition('id', '1', '>=') + ->condition('number', 10, '>=') + ->condition('number', 50, '>=') + ->execute(); + $this->assertResults(array('3', '5')); + + // Filter with an OR condition group. + $this->queryResults = $this->factory->get('config_query_test', 'OR') + ->condition('id', 1) + ->condition('id', '2') + ->execute(); + $this->assertResults(array('1', '2')); + + // Simplify it with IN. + $this->queryResults = $this->factory->get('config_query_test') + ->condition('id', array('1', '2')) + ->execute(); + $this->assertResults(array('1', '2')); + // Try explicit IN. + $this->queryResults = $this->factory->get('config_query_test') + ->condition('id', array('1', '2'), 'IN') + ->execute(); + $this->assertResults(array('1', '2')); + // Try not IN. + $this->queryResults = $this->factory->get('config_query_test') + ->condition('id', array('1', '2'), 'NOT IN') + ->execute(); + $this->assertResults(array('3', '4', '5')); + + // Filter with an OR condition group on different fields. + $this->queryResults = $this->factory->get('config_query_test', 'OR') + ->condition('id', 1) + ->condition('number', 41) + ->execute(); + $this->assertResults(array('1', '2')); + + // Filter with an OR condition group on different fields but matching on the + // same entity. + $this->queryResults = $this->factory->get('config_query_test', 'OR') + ->condition('id', 1) + ->condition('number', 31) + ->execute(); + $this->assertResults(array('1')); + + // NO simple conditions, YES complex conditions, 'AND'. + $query = $this->factory->get('config_query_test', 'AND'); + $and_condition_1 = $query->orConditionGroup() + ->condition('id', '2') + ->condition('label', $this->entities[0]->label); + $and_condition_2 = $query->orConditionGroup() + ->condition('id', 1) + ->condition('label', $this->entities[3]->label); + $this->queryResults = $query + ->condition($and_condition_1) + ->condition($and_condition_2) + ->execute(); + $this->assertResults(array('1')); + + // NO simple conditions, YES complex conditions, 'OR'. + $query = $this->factory->get('config_query_test', 'OR'); + $and_condition_1 = $query->andConditionGroup() + ->condition('id', 1) + ->condition('label', $this->entities[0]->label); + $and_condition_2 = $query->andConditionGroup() + ->condition('id', '2') + ->condition('label', $this->entities[1]->label); + $this->queryResults = $query + ->condition($and_condition_1) + ->condition($and_condition_2) + ->execute(); + $this->assertResults(array('1', '2')); + + // YES simple conditions, YES complex conditions, 'AND'. + $query = $this->factory->get('config_query_test', 'AND'); + $and_condition_1 = $query->orConditionGroup() + ->condition('id', '2') + ->condition('label', $this->entities[0]->label); + $and_condition_2 = $query->orConditionGroup() + ->condition('id', 1) + ->condition('label', $this->entities[3]->label); + $this->queryResults = $query + ->condition('number', 31) + ->condition($and_condition_1) + ->condition($and_condition_2) + ->execute(); + $this->assertResults(array('1')); + + // YES simple conditions, YES complex conditions, 'OR'. + $query = $this->factory->get('config_query_test', 'OR'); + $and_condition_1 = $query->orConditionGroup() + ->condition('id', '2') + ->condition('label', $this->entities[0]->label); + $and_condition_2 = $query->orConditionGroup() + ->condition('id', 1) + ->condition('label', $this->entities[3]->label); + $this->queryResults = $query + ->condition('number', 53) + ->condition($and_condition_1) + ->condition($and_condition_2) + ->execute(); + $this->assertResults(array('1', '2', '4', '5')); + } + + /** + * Test count query. + */ + protected function testCount() { + // Test count on no conditions. + $count = $this->factory->get('config_query_test') + ->count() + ->execute(); + $this->assertIdentical($count, count($this->entities)); + + // Test count on a complex query. + $query = $this->factory->get('config_query_test', 'OR'); + $and_condition_1 = $query->andConditionGroup() + ->condition('id', 1) + ->condition('label', $this->entities[0]->label); + $and_condition_2 = $query->andConditionGroup() + ->condition('id', '2') + ->condition('label', $this->entities[1]->label); + $count = $query + ->condition($and_condition_1) + ->condition($and_condition_2) + ->count() + ->execute(); + $this->assertIdentical($count, 2); + } + + /** + * Tests sorting and range on config entity queries. + */ + protected function testSortRange() { + // Sort by simple ascending/descending. + $this->queryResults = $this->factory->get('config_query_test') + ->sort('number', 'DESC') + ->execute(); + $this->assertIdentical(array_values($this->queryResults), array('3', '5', '2', '1', '4')); + + $this->queryResults = $this->factory->get('config_query_test') + ->sort('number', 'ASC') + ->execute(); + $this->assertIdentical(array_values($this->queryResults), array('4', '1', '2', '5', '3')); + + // Apply some filters and sort. + $this->queryResults = $this->factory->get('config_query_test') + ->condition('id', '3', '>') + ->sort('number', 'DESC') + ->execute(); + $this->assertIdentical(array_values($this->queryResults), array('5', '4')); + + $this->queryResults = $this->factory->get('config_query_test') + ->condition('id', '3', '>') + ->sort('number', 'ASC') + ->execute(); + $this->assertIdentical(array_values($this->queryResults), array('4', '5')); + + // Apply a pager and sort. + $this->queryResults = $this->factory->get('config_query_test') + ->sort('number', 'DESC') + ->range('2', '2') + ->execute(); + $this->assertIdentical(array_values($this->queryResults), array('2', '1')); + + $this->queryResults = $this->factory->get('config_query_test') + ->sort('number', 'ASC') + ->range('2', '2') + ->execute(); + $this->assertIdentical(array_values($this->queryResults), array('2', '5')); + + // Add a range to a query without a start parameter. + $this->queryResults = $this->factory->get('config_query_test') + ->range(0, '3') + ->sort('id', 'ASC') + ->execute(); + $this->assertIdentical(array_values($this->queryResults), array('1', '2', '3')); + + // Apply a pager with limit 4. + $this->queryResults = $this->factory->get('config_query_test') + ->pager('4', 0) + ->sort('id', 'ASC') + ->execute(); + $this->assertIdentical(array_values($this->queryResults), array('1', '2', '3', '4')); + } + + /** + * Test dotted path matching. + */ + function testDotted() { + $this->queryResults = $this->factory->get('config_query_test') + ->condition('array.level1.*', 1) + ->execute(); + $this->assertResults(array('1', '3')); + $this->queryResults = $this->factory->get('config_query_test') + ->condition('*.level1.level2', 2) + ->execute(); + $this->assertResults(array('2', '4')); + $this->queryResults = $this->factory->get('config_query_test') + ->condition('array.level1.*', 3) + ->execute(); + $this->assertResults(array('5')); + $this->queryResults = $this->factory->get('config_query_test') + ->condition('array.level1.level2', 3) + ->execute(); + $this->assertResults(array('5')); + } + + /** + * Asserts the results as expected regardless of order. + * + * @param array $expected + * Array of expected entity IDs. + */ + function assertResults($expected) { + $this->assertIdentical(count($this->queryResults), count($expected)); + foreach ($expected as $value) { + // This also tests whether $this->queryResults[$value] is even set at + // all. + $this->assertIdentical($this->queryResults[$value], $value); + } + } + +}