From f755b6942cdf19b833b68e887c9caf67c883317f Mon Sep 17 00:00:00 2001
From: catch <catch@35733.no-reply.drupal.org>
Date: Mon, 28 Jan 2013 14:08:49 +0000
Subject: [PATCH] Issue #1846454 by chx, dawehner, amateescu: Add Entity query
 to Config entities.

---
 core/includes/entity.inc                      |   3 +
 .../Config/Entity/ConfigStorageController.php |   2 +-
 .../Core/Config/Entity/Query/Condition.php    | 172 +++++++
 .../Drupal/Core/Config/Entity/Query/Query.php | 121 +++++
 .../Core/Config/Entity/Query/QueryFactory.php |  53 +++
 core/lib/Drupal/Core/CoreBundle.php           |   7 +-
 .../Drupal/Core/Entity/Query/QueryFactory.php |  34 +-
 .../Plugin/Core/Entity/ConfigQueryTest.php    |  52 ++
 .../Drupal/field_sql_storage/Entity/Query.php |   1 +
 .../Tests/Entity/ConfigEntityQueryTest.php    | 444 ++++++++++++++++++
 10 files changed, 875 insertions(+), 14 deletions(-)
 create mode 100644 core/lib/Drupal/Core/Config/Entity/Query/Condition.php
 create mode 100644 core/lib/Drupal/Core/Config/Entity/Query/Query.php
 create mode 100644 core/lib/Drupal/Core/Config/Entity/Query/QueryFactory.php
 create mode 100644 core/modules/config/tests/config_test/lib/Drupal/config_test/Plugin/Core/Entity/ConfigQueryTest.php
 create mode 100644 core/modules/system/lib/Drupal/system/Tests/Entity/ConfigEntityQueryTest.php

diff --git a/core/includes/entity.inc b/core/includes/entity.inc
index 08ed91753046..4ae754beebc6 100644
--- a/core/includes/entity.inc
+++ b/core/includes/entity.inc
@@ -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);
diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php b/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php
index 2dd7ace43311..b2a94b07dca2 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 000000000000..a9d434a66850
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/Entity/Query/Condition.php
@@ -0,0 +1,172 @@
+<?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';
+  }
+
+}
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 000000000000..baba97356ae2
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/Entity/Query/Query.php
@@ -0,0 +1,121 @@
+<?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;
+  }
+
+}
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 000000000000..fe937139efde
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/Entity/Query/QueryFactory.php
@@ -0,0 +1,53 @@
+<?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);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php
index 624c7167c9dc..2a5011585a91 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 cc955a706607..da739fd17e0b 100644
--- a/core/lib/Drupal/Core/Entity/Query/QueryFactory.php
+++ b/core/lib/Drupal/Core/Entity/Query/QueryFactory.php
@@ -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);
   }
 
 }
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 000000000000..88e75835760e
--- /dev/null
+++ b/core/modules/config/tests/config_test/lib/Drupal/config_test/Plugin/Core/Entity/ConfigQueryTest.php
@@ -0,0 +1,52 @@
+<?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;
+
+}
diff --git a/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Query.php b/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Query.php
index 7f78e87d4cc1..41091a6113b6 100644
--- a/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Query.php
+++ b/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Query.php
@@ -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.");
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 000000000000..f0d3d6a13c32
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/ConfigEntityQueryTest.php
@@ -0,0 +1,444 @@
+<?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->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();
+  }
+
+  /**
+   * Tests 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'));
+  }
+
+  /**
+   * Tests 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'));
+  }
+
+  /**
+   * Tests dotted path matching.
+   */
+  protected 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.
+   */
+  protected 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);
+    }
+  }
+
+}
-- 
GitLab