diff --git a/core/includes/module.inc b/core/includes/module.inc index f6d47b6..2fdf63c 100644 --- a/core/includes/module.inc +++ b/core/includes/module.inc @@ -491,11 +491,13 @@ function module_enable($module_list, $enable_dependencies = TRUE) { $modules_installed = array(); $modules_enabled = array(); - $schema_store = drupal_container()->get('keyvalue')->get('system.schema'); - $module_config = config('system.module'); - $disabled_config = config('system.module.disabled'); - $module_filenames = drupal_container()->getParameter('container.modules'); foreach ($module_list as $module) { + // Each iteration through this loop, there's potentially a new + // drupal_container() so refetch these objects. + $module_config = config('system.module'); + $disabled_config = config('system.module.disabled'); + $module_filenames = drupal_container()->getParameter('container.modules'); + // Only process modules that are not already enabled. $enabled = $module_config->get("enabled.$module") !== NULL; if (!$enabled) { diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php b/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php index b472b8f..f8b4c45 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php @@ -424,7 +424,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/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index 2076707..30708c8 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -52,8 +52,7 @@ public function build(ContainerBuilder $container) { $container->register('config.factory', 'Drupal\Core\Config\ConfigFactory') ->addArgument(new Reference('config.storage')) - ->addArgument(new Reference('event_dispatcher')) - ->addTag('persist'); + ->addArgument(new Reference('event_dispatcher')); // Register staging configuration storage. $container diff --git a/core/lib/Drupal/Core/Database/Query/Query.php b/core/lib/Drupal/Core/Database/Query/Query.php index 95ab4b9..049ed1c 100644 --- a/core/lib/Drupal/Core/Database/Query/Query.php +++ b/core/lib/Drupal/Core/Database/Query/Query.php @@ -177,4 +177,5 @@ public function comment($comment) { public function &getComments() { return $this->comments; } + } diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterKernelListenersPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterKernelListenersPass.php index e580c31..e25c4e5 100644 --- a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterKernelListenersPass.php +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterKernelListenersPass.php @@ -21,7 +21,6 @@ public function process(ContainerBuilder $container) { $definition = $container->getDefinition('event_dispatcher'); foreach ($container->findTaggedServiceIds('event_subscriber') as $id => $attributes) { - // We must assume that the class value has been correcly filled, even if the service is created by a factory $class = $container->getDefinition($id)->getClass(); diff --git a/core/modules/entity/entity.install b/core/modules/entity/entity.install index 2e1efd7..e118044 100644 --- a/core/modules/entity/entity.install +++ b/core/modules/entity/entity.install @@ -8,6 +8,51 @@ use Drupal\Component\Uuid\Uuid; /** + * Implements hook_schema(). + */ +function entity_schema() { + $schema['config_entity_denormalized'] = array( + 'description' => 'This table is storing denormalized configuration entity objects for query purposes.', + 'fields' => array( + 'entity_type' => array( + 'description' => 'Tne type of the config entity.', + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'default' => '', + ), + 'entity_id' => array( + 'description' => 'Tne ID of the config entity.', + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + 'default' => '', + ), + 'name' => array( + 'description' => 'The key of the key-value pair. As KEY is a SQL reserved keyword, name was chosen instead.', + 'type' => 'varchar', + 'length' => 237, + 'not null' => TRUE, + 'default' => '', + ), + 'value' => array( + 'description' => 'The value.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + ), + 'primary key' => array('entity_type', 'entity_id', 'name'), + 'index' => array( + 'query_name' => array('entity_type', 'name', array('value', 64)), + 'query_value' => array('entity_type', array('value', 64), 'name'), + ), + ); + return $schema; +} + +/** * Returns the raw configuration object for an EntityDisplay entity. * * The function returns the existing configuration entry if it exists, or diff --git a/core/modules/entity/lib/Drupal/entity/ConfigQuery/Condition.php b/core/modules/entity/lib/Drupal/entity/ConfigQuery/Condition.php new file mode 100644 index 0000000..ee507e3 --- /dev/null +++ b/core/modules/entity/lib/Drupal/entity/ConfigQuery/Condition.php @@ -0,0 +1,123 @@ +sqlQuery; + } + + // Iterate over all conditions and act based on whether they are a condition + // group or a single condition. + foreach ($this->conditions as $condition) { + // If it's a condition group, compile the condition group and add it to + // the sql. + 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); + } + // If it's a simple condition apply the query logic. + else { + $and = strtoupper($this->conjunction) == 'AND'; + $or = !$and; + $type = $or || $condition['operator'] == 'IS NULL' ? 'LEFT' : 'INNER'; + #$this->translateCondition($condition); + if ($and || !$added) { + $alias = $sqlQuery->join('config_entity_denormalized', NULL, '%alias.entity_type = base_table.entity_type AND %alias.entity_id = base_table.entity_id'); + $added = TRUE; + } + if ($and) { + $container = $conditionContainer; + } + else { + $container = new SqlCondition('AND'); + } + $this->translateCondition($condition); + $container->condition("$alias.name", $condition['field'], $condition['field_operator']); + $container->condition("$alias.value", $condition['value'], $condition['operator']); + if ($or) { + $conditionContainer->condition($container); + } + } + } + } + + /** + * 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); + } + + /** + * Translates a condition array to it's sql equivalents. + * + * @param array $condition + * An associative array containing the follow keys: + * - field: The entity property to filter on. + * - operator: The operator used in the condition, for example "=". + * - value: The actual value to filter by. + */ + protected function translateCondition(array &$condition) { + if (strpos($condition['field'], '*') === FALSE) { + $condition['field_operator'] = '='; + } + else { + $condition['field_operator'] = 'LIKE'; + $condition['field'] = str_replace('*', '%', $condition['field']); + } + 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/entity/lib/Drupal/entity/ConfigQuery/Denormalize.php b/core/modules/entity/lib/Drupal/entity/ConfigQuery/Denormalize.php new file mode 100644 index 0000000..b2100d5 --- /dev/null +++ b/core/modules/entity/lib/Drupal/entity/ConfigQuery/Denormalize.php @@ -0,0 +1,121 @@ +connection = $connection; + } + + /** + * Reacts on the config.save event to denormalize the entity values. + * + * @param \Drupal\Core\Config\ConfigEvent $event + * The config event object containing the config object being written. + */ + public function configSave(ConfigEvent $event) { + $transaction = $this->connection->startTransaction(); + $this->configDelete($event); + if ($this->entityType && $this->connection->schema()->tableExists('config_entity_denormalized')) { + $this->insert = $this->connection->insert('config_entity_denormalized') + ->fields(array('entity_type', 'entity_id', 'name', 'value')); + $this->values($this->config->get()); + $this->insert->execute(); + } + } + + /** + * Reacts on the config.delete event to remove the entity values. + * + * @param \Drupal\Core\Config\ConfigEvent $event + * The config event object containing the config object being written. + */ + public function configDelete(ConfigEvent $event) { + $this->config = $event->getConfig(); + $name = $this->config->getName(); + if ($this->entityType = config_get_entity_type_by_name($name)) { + $entity_info = entity_get_info($this->entityType); + $this->entityID = substr($name, strlen($entity_info['config_prefix']) + 1); + $this->connection->delete('config_entity_denormalized') + ->condition('entity_type', $this->entityType) + ->condition('entity_id', $this->entityID) + ->execute(); + } + } + + /** + * Set all entity values to the database insert object. + * + * @param array $data + * The contents of the config object. + * @param string $prefix + * The current config prefix. + */ + protected function values($data, $prefix = '') { + foreach ($data as $key => $value) { + if (is_array($value)) { + $this->values($value, "$prefix$key."); + } + else { + $this->insert->values(array($this->entityType, $this->entityID, "$prefix$key", $value)); + } + } + } + + /** + * Implements \Symfony\Component\EventDispatcher\EventSubscriberInterface::getSubscribedEvents(). + * + * @return array + * Returns the subscribed events. + */ + public static function getSubscribedEvents() { + $events['config.save'][] = array('configSave', 20); + $events['config.delete'][] = array('configDelete', 20); + return $events; + } +} diff --git a/core/modules/entity/lib/Drupal/entity/ConfigQuery/Query.php b/core/modules/entity/lib/Drupal/entity/ConfigQuery/Query.php new file mode 100644 index 0000000..8cea0b1 --- /dev/null +++ b/core/modules/entity/lib/Drupal/entity/ConfigQuery/Query.php @@ -0,0 +1,56 @@ +connection = $connection; + } + + /** + * Implements \Drupal\Core\Entity\Query\QueryInterface::conditionGroupFactory(). + */ + public function conditionGroupFactory($conjunction = 'AND') { + return new Condition($conjunction); + } + + /** + * Implements \Drupal\Core\Entity\Query\QueryInterface::execute(). + */ + public function execute() { + $sqlQuery = $this->connection->select('config_entity_denormalized', 'base_table', array('conjunction' => $this->conjunction)); + $sqlQuery->addField('base_table', 'entity_id'); + $sqlQuery->addField('base_table', 'entity_id'); + $this->condition->compile($sqlQuery); + return $sqlQuery->execute()->fetchAllKeyed(); + } +} diff --git a/core/modules/entity/lib/Drupal/entity/ConfigQuery/QueryFactory.php b/core/modules/entity/lib/Drupal/entity/ConfigQuery/QueryFactory.php new file mode 100644 index 0000000..5fcd711 --- /dev/null +++ b/core/modules/entity/lib/Drupal/entity/ConfigQuery/QueryFactory.php @@ -0,0 +1,45 @@ +connection = $connection; + } + + /** + * 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'. + * + * @return \Drupal\entity\ConfigQuery\Query + * A entity query for a specific configuration entity type. + */ + function get($entity_type, $conjunction = 'AND') { + return new Query($entity_type, $conjunction, $this->connection); + } +} diff --git a/core/modules/entity/lib/Drupal/entity/EntityBundle.php b/core/modules/entity/lib/Drupal/entity/EntityBundle.php new file mode 100644 index 0000000..303055f --- /dev/null +++ b/core/modules/entity/lib/Drupal/entity/EntityBundle.php @@ -0,0 +1,30 @@ +register('entity.query.config', 'Drupal\entity\ConfigQuery\QueryFactory') + ->addArgument(new Reference('database')); + $container->register('entity.query.denormalize', 'Drupal\entity\ConfigQuery\Denormalize') + ->addArgument(new Reference('database')) + ->addTag('event_subscriber'); + } + +} diff --git a/core/modules/entity/lib/Drupal/entity/Tests/ConfigEntityQueryTest.php b/core/modules/entity/lib/Drupal/entity/Tests/ConfigEntityQueryTest.php new file mode 100644 index 0000000..fa69fc8 --- /dev/null +++ b/core/modules/entity/lib/Drupal/entity/Tests/ConfigEntityQueryTest.php @@ -0,0 +1,210 @@ + 'Config Entity Query', + 'description' => 'Tests Config Entity Query functionality.', + 'group' => 'Configuration', + ); + } + + protected function setUp() { + parent::setUp(); + $this->enableModules(array('entity'), TRUE); + $this->factory = $this->container->get('entity.query'); + + $entity = entity_create('config_test', array( + 'label' => $this->randomName(), + 'id' => 1, + ) + ); + $this->entities[] = $entity; + $entity->enforceIsNew(); + $entity->save(); + + $entity = entity_create('config_test', array( + 'label' => $this->randomName(), + 'id' => 2, + ) + ); + $this->entities[] = $entity; + $entity->enforceIsNew(); + $entity->save(); + + $entity = entity_create('config_test', array( + 'label' => 'test_prefix_' . $this->randomName(), + 'id' => 3, + ) + ); + $this->entities[] = $entity; + $entity->enforceIsNew(); + $entity->save(); + + $entity = entity_create('config_test', array( + 'label' => $this->randomName() . '_test_suffix', + 'id' => 4, + ) + ); + $this->entities[] = $entity; + $entity->enforceIsNew(); + $entity->save(); + + $entity = entity_create('config_test', array( + 'label' => $this->randomName() . '_test_contains_' . $this->randomName(), + 'id' => 5, + ) + ); + $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_test') + ->execute(); + + $this->assertEqual(count($this->queryResults), count($this->entities)); + + // Filter by ID with equality. + $this->queryResults = $this->factory->get('config_test') + ->condition('id', 3) + ->execute(); + + $this->assertEqual(count($this->queryResults), 1); + + // Filter by label with a known prefix. + $this->queryResults = $this->factory->get('config_test') + ->condition('label', 'test_prefix', 'STARTS_WITH') + ->execute(); + + $this->assertEqual(count($this->queryResults), 1); + $this->assertEqual($this->queryResults[3], '3'); + + // Filter by label with a known suffix. + $this->queryResults = $this->factory->get('config_test') + ->condition('label', 'test_suffix', 'ENDS_WITH') + ->execute(); + + $this->assertEqual(count($this->queryResults), 1); + $this->assertEqual($this->queryResults[4], 4); + + // Filter by label with a known containing word. + $this->queryResults = $this->factory->get('config_test') + ->condition('label', 'test_contains', 'CONTAINS') + ->execute(); + + $this->assertEqual(count($this->queryResults), 1); + $this->assertEqual($this->queryResults[5], 5); + + // Filter by ID with the IN operator. + $this->queryResults = $this->factory->get('config_test') + ->condition('id', array(2, 3), 'IN') + ->execute(); + + $this->assertEqual(count($this->queryResults), 2); + $this->assertEqual(array_keys($this->queryResults), array(2, 3)); + + // Filter by two conditions on the same field. + $this->queryResults = $this->factory->get('config_test') + ->condition('label', 'test_pref', 'STARTS_WITH') + ->condition('label', 'test_prefix', 'STARTS_WITH') + ->execute(); + + $this->assertEqual(count($this->queryResults), 1); + $this->assertEqual($this->queryResults[3], 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_test') + ->condition('label', 'test_prefix', 'STARTS_WITH') + ->condition('id', 5) + ->execute(); + + $this->assertEqual(count($this->queryResults), 0); + + $this->queryResults = $this->factory->get('config_test') + ->condition('label', 'test_prefix', 'STARTS_WITH') + ->condition('id', 3) + ->execute(); + + $this->assertEqual(count($this->queryResults), 1); + $this->assertEqual($this->queryResults[3], 3); + + // Filter with an OR condition group. + $this->queryResults = $this->factory->get('config_test', 'OR') + ->condition('id', 1) + ->condition('id', 2) + ->execute(); + $this->assertEqual(count($this->queryResults), 2); + + // Filters an OR condition group which both contain an AND group. + $query = $this->factory->get('config_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->factory->get('config_test', 'OR') + ->condition($and_condition_1) + ->condition($and_condition_2) + ->execute(); + $this->assertEqual(count($this->queryResults), 2); + $this->assertEqual($this->queryResults, array(1 => 1, 2 => 2)); + } + + /** + * Test entity count query. + */ + protected function xtestCount() { + $count = $this->factory->get('config_test') + ->count() + ->execute(); + + $this->assertEqual($count, count($this->entities)); + } + +} diff --git a/core/modules/filter/filter.info b/core/modules/filter/filter.info index 03ed179..04ca94f 100644 --- a/core/modules/filter/filter.info +++ b/core/modules/filter/filter.info @@ -5,3 +5,4 @@ version = VERSION core = 8.x required = TRUE configure = admin/config/content/formats +dependencies[] = entity