1<?php
2
3namespace Drupal\Core\Config\Entity\Query;
4
5use Drupal\Core\Config\Config;
6use Drupal\Core\Config\ConfigCrudEvent;
7use Drupal\Core\Config\ConfigEvents;
8use Drupal\Core\Config\ConfigFactoryInterface;
9use Drupal\Core\Config\ConfigManagerInterface;
10use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
11use Drupal\Core\Entity\EntityTypeInterface;
12use Drupal\Core\Entity\Query\QueryBase;
13use Drupal\Core\Entity\Query\QueryException;
14use Drupal\Core\Entity\Query\QueryFactoryInterface;
15use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
16use Symfony\Component\EventDispatcher\EventSubscriberInterface;
17
18/**
19 * Provides a factory for creating entity query objects for the config backend.
20 */
21class QueryFactory implements QueryFactoryInterface, EventSubscriberInterface {
22
23  /**
24   * The prefix for the key value collection for fast lookups.
25   */
26  const CONFIG_LOOKUP_PREFIX = 'config.entity.key_store.';
27
28  /**
29   * The config factory used by the config entity query.
30   *
31   * @var \Drupal\Core\Config\ConfigFactoryInterface
32   */
33  protected $configFactory;
34
35  /**
36   * The namespace of this class, the parent class etc.
37   *
38   * @var array
39   */
40  protected $namespaces;
41
42  /**
43   * Constructs a QueryFactory object.
44   *
45   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
46   *   The config storage used by the config entity query.
47   * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value
48   *   The key value factory.
49   * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
50   *   The configuration manager.
51   */
52  public function __construct(ConfigFactoryInterface $config_factory, KeyValueFactoryInterface $key_value, ConfigManagerInterface $config_manager) {
53    $this->configFactory = $config_factory;
54    $this->keyValueFactory = $key_value;
55    $this->configManager = $config_manager;
56    $this->namespaces = QueryBase::getNamespaces($this);
57  }
58
59  /**
60   * {@inheritdoc}
61   */
62  public function get(EntityTypeInterface $entity_type, $conjunction) {
63    return new Query($entity_type, $conjunction, $this->configFactory, $this->keyValueFactory, $this->namespaces);
64  }
65
66  /**
67   * {@inheritdoc}
68   */
69  public function getAggregate(EntityTypeInterface $entity_type, $conjunction) {
70    throw new QueryException('Aggregation over configuration entities is not supported');
71  }
72
73  /**
74   * Gets the key value store used to store fast lookups.
75   *
76   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
77   *   The entity type.
78   *
79   * @return \Drupal\Core\KeyValueStore\KeyValueStoreInterface
80   *   The key value store used to store fast lookups.
81   */
82  protected function getConfigKeyStore(EntityTypeInterface $entity_type) {
83    return $this->keyValueFactory->get(static::CONFIG_LOOKUP_PREFIX . $entity_type->id());
84  }
85
86  /**
87   * Updates or adds lookup data.
88   *
89   * @param \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $entity_type
90   *   The entity type.
91   * @param \Drupal\Core\Config\Config $config
92   *   The configuration object that is being saved.
93   */
94  protected function updateConfigKeyStore(ConfigEntityTypeInterface $entity_type, Config $config) {
95    $config_key_store = $this->getConfigKeyStore($entity_type);
96    foreach ($entity_type->getLookupKeys() as $lookup_key) {
97      foreach ($this->getKeys($config, $lookup_key, 'get', $entity_type) as $key) {
98        $values = $config_key_store->get($key, []);
99        if (!in_array($config->getName(), $values, TRUE)) {
100          $values[] = $config->getName();
101          $config_key_store->set($key, $values);
102        }
103      }
104    }
105  }
106
107  /**
108   * Deletes lookup data.
109   *
110   * @param \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $entity_type
111   *   The entity type.
112   * @param \Drupal\Core\Config\Config $config
113   *   The configuration object that is being deleted.
114   */
115  protected function deleteConfigKeyStore(ConfigEntityTypeInterface $entity_type, Config $config) {
116    $config_key_store = $this->getConfigKeyStore($entity_type);
117    foreach ($entity_type->getLookupKeys() as $lookup_key) {
118      foreach ($this->getKeys($config, $lookup_key, 'getOriginal', $entity_type) as $key) {
119        $values = $config_key_store->get($key, []);
120        $pos = array_search($config->getName(), $values, TRUE);
121        if ($pos !== FALSE) {
122          unset($values[$pos]);
123        }
124        if (empty($values)) {
125          $config_key_store->delete($key);
126        }
127        else {
128          $config_key_store->set($key, $values);
129        }
130      }
131    }
132  }
133
134  /**
135   * Creates lookup keys for configuration data.
136   *
137   * @param \Drupal\Core\Config\Config $config
138   *   The configuration object.
139   * @param string $key
140   *   The configuration key to look for.
141   * @param string $get_method
142   *   Which method on the config object to call to get the value. Either 'get'
143   *   or 'getOriginal'.
144   * @param \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $entity_type
145   *   The configuration entity type.
146   *
147   * @return array
148   *   An array of lookup keys concatenated to the configuration values.
149   *
150   * @throws \Drupal\Core\Config\Entity\Query\InvalidLookupKeyException
151   *   The provided $key cannot end with a wildcard. This makes no sense since
152   *   you cannot do fast lookups against this.
153   */
154  protected function getKeys(Config $config, $key, $get_method, ConfigEntityTypeInterface $entity_type) {
155    if (substr($key, -1) == '*') {
156      throw new InvalidLookupKeyException(strtr('%entity_type lookup key %key ends with a wildcard this can not be used as a lookup', ['%entity_type' => $entity_type->id(), '%key' => $key]));
157    }
158    $parts = explode('.*', $key);
159    // Remove leading dots.
160    array_walk($parts, function (&$value) {
161      $value = trim($value, '.');
162    });
163
164    $values = (array) $this->getValues($config, $parts[0], $get_method, $parts);
165
166    $output = [];
167    // Flatten the array to a single dimension and add the key to all the
168    // values.
169    array_walk_recursive($values, function ($current) use (&$output, $key) {
170      if (is_scalar($current)) {
171        $current = $key . ':' . $current;
172      }
173      $output[] = $current;
174    });
175    return $output;
176  }
177
178  /**
179   * Finds all the values for a configuration key in a configuration object.
180   *
181   * @param \Drupal\Core\Config\Config $config
182   *   The configuration object.
183   * @param string $key
184   *   The current key being checked.
185   * @param string $get_method
186   *   Which method on the config object to call to get the value.
187   * @param array $parts
188   *   All the parts of a configuration key we are checking.
189   * @param int $start
190   *   Which position of $parts we are processing. Defaults to 0.
191   *
192   * @return array|null
193   *   The array of configuration values the match the provided key. NULL if
194   *   the configuration object does not have a value that corresponds to the
195   *   key.
196   */
197  protected function getValues(Config $config, $key, $get_method, array $parts, $start = 0) {
198    $value = $config->$get_method($key);
199    if (is_array($value)) {
200      $new_value = [];
201      $start++;
202      if (!isset($parts[$start])) {
203        // The configuration object does not have a value that corresponds to
204        // the key.
205        return NULL;
206      }
207      foreach (array_keys($value) as $key_bit) {
208        $new_key = $key . '.' . $key_bit;
209        if (!empty($parts[$start])) {
210          $new_key .= '.' . $parts[$start];
211        }
212        $new_value[] = $this->getValues($config, $new_key, $get_method, $parts, $start);
213      }
214      $value = $new_value;
215    }
216    return $value;
217  }
218
219  /**
220   * Updates configuration entity in the key store.
221   *
222   * @param \Drupal\Core\Config\ConfigCrudEvent $event
223   *   The configuration event.
224   */
225  public function onConfigSave(ConfigCrudEvent $event) {
226    $saved_config = $event->getConfig();
227    $entity_type_id = $this->configManager->getEntityTypeIdByName($saved_config->getName());
228    if ($entity_type_id) {
229      $entity_type = $this->configManager->getEntityTypeManager()->getDefinition($entity_type_id);
230      $this->updateConfigKeyStore($entity_type, $saved_config);
231    }
232  }
233
234  /**
235   * Removes configuration entity from key store.
236   *
237   * @param \Drupal\Core\Config\ConfigCrudEvent $event
238   *   The configuration event.
239   */
240  public function onConfigDelete(ConfigCrudEvent $event) {
241    $saved_config = $event->getConfig();
242    $entity_type_id = $this->configManager->getEntityTypeIdByName($saved_config->getName());
243    if ($entity_type_id) {
244      $entity_type = $this->configManager->getEntityTypeManager()->getDefinition($entity_type_id);
245      $this->deleteConfigKeyStore($entity_type, $saved_config);
246    }
247  }
248
249  /**
250   * {@inheritdoc}
251   */
252  public static function getSubscribedEvents() {
253    $events[ConfigEvents::SAVE][] = ['onConfigSave', 128];
254    $events[ConfigEvents::DELETE][] = ['onConfigDelete', 128];
255    return $events;
256  }
257
258}
259