3 namespace Drupal\Core\Config\Entity;
5 use Drupal\Core\Cache\CacheableMetadata;
6 use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
7 use Drupal\Core\Config\ConfigFactoryInterface;
8 use Drupal\Core\Config\ConfigImporterException;
9 use Drupal\Core\Entity\EntityInterface;
10 use Drupal\Core\Entity\EntityMalformedException;
11 use Drupal\Core\Entity\EntityStorageBase;
12 use Drupal\Core\Config\Config;
13 use Drupal\Core\Config\Entity\Exception\ConfigEntityIdLengthException;
14 use Drupal\Core\Entity\EntityTypeInterface;
15 use Drupal\Component\Uuid\UuidInterface;
16 use Drupal\Core\Language\LanguageManagerInterface;
17 use Symfony\Component\DependencyInjection\ContainerInterface;
20 * Defines the storage class for configuration entities.
22 * Configuration object names of configuration entities are comprised of two
23 * parts, separated by a dot:
24 * - config_prefix: A string denoting the owner (module/extension) of the
25 * configuration object, followed by arbitrary other namespace identifiers
26 * that are declared by the owning extension; e.g., 'node.type'. The
27 * config_prefix does NOT contain a trailing dot. It is defined by the entity
29 * - ID: A string denoting the entity ID within the entity type namespace; e.g.,
30 * 'article'. Entity IDs may contain dots/periods. The entire remaining string
31 * after the config_prefix in a config name forms the entity ID. Additional or
32 * custom suffixes are not possible.
36 class ConfigEntityStorage extends EntityStorageBase implements ConfigEntityStorageInterface, ImportableEntityStorageInterface {
39 * Length limit of the configuration entity ID.
41 * Most file systems limit a file name's length to 255 characters, so
42 * ConfigBase::MAX_NAME_LENGTH restricts the full configuration object name
43 * to 250 characters (leaving 5 for the file extension). The config prefix
44 * is limited by ConfigEntityType::PREFIX_LENGTH to 83 characters, so this
45 * leaves 166 remaining characters for the configuration entity ID, with 1
46 * additional character needed for the joining dot.
48 * @see \Drupal\Core\Config\ConfigBase::MAX_NAME_LENGTH
49 * @see \Drupal\Core\Config\Entity\ConfigEntityType::PREFIX_LENGTH
51 const MAX_ID_LENGTH = 166;
56 protected $uuidKey = 'uuid';
59 * The config factory service.
61 * @var \Drupal\Core\Config\ConfigFactoryInterface
63 protected $configFactory;
66 * The config storage service.
68 * @var \Drupal\Core\Config\StorageInterface
70 protected $configStorage;
73 * The language manager.
75 * @var \Drupal\Core\Language\LanguageManagerInterface
77 protected $languageManager;
80 * Static cache of entities, keyed first by entity ID, then by an extra key.
82 * The additional cache key is to maintain separate caches for different
83 * states of config overrides.
86 * @see \Drupal\Core\Config\ConfigFactoryInterface::getCacheKeys().
88 protected $entities = [];
91 * Determines if the underlying configuration is retrieved override free.
95 protected $overrideFree = FALSE;
98 * Constructs a ConfigEntityStorage object.
100 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
101 * The entity type definition.
102 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
103 * The config factory service.
104 * @param \Drupal\Component\Uuid\UuidInterface $uuid_service
106 * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
107 * The language manager.
108 * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface|null $memory_cache
109 * The memory cache backend.
111 public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, UuidInterface $uuid_service, LanguageManagerInterface $language_manager, MemoryCacheInterface $memory_cache = NULL) {
112 parent::__construct($entity_type, $memory_cache);
114 $this->configFactory = $config_factory;
115 $this->uuidService = $uuid_service;
116 $this->languageManager = $language_manager;
122 public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
125 $container->get('config.factory'),
126 $container->get('uuid'),
127 $container->get('language_manager'),
128 $container->get('entity.memory_cache')
135 public function loadRevision($revision_id) {
142 public function deleteRevision($revision_id) {
147 * Returns the prefix used to create the configuration name.
149 * The prefix consists of the config prefix from the entity type plus a dot
150 * for separating from the ID.
153 * The full configuration prefix, for example 'views.view.'.
155 protected function getPrefix() {
156 return $this->entityType->getConfigPrefix() . '.';
162 public static function getIDFromConfigName($config_name, $config_prefix) {
163 return substr($config_name, strlen($config_prefix . '.'));
169 protected function doLoadMultiple(array $ids = NULL) {
170 $prefix = $this->getPrefix();
172 // Get the names of the configuration entities we are going to load.
174 $names = $this->configFactory->listAll($prefix);
178 foreach ($ids as $id) {
179 // Add the prefix to the ID to serve as the configuration object name.
180 $names[] = $prefix . $id;
184 // Load all of the configuration entities.
185 /** @var \Drupal\Core\Config\Config[] $configs */
188 foreach ($this->configFactory->loadMultiple($names) as $config) {
189 $id = $config->get($this->idKey);
190 $records[$id] = $this->overrideFree ? $config->getOriginal(NULL, FALSE) : $config->get();
191 $configs[$id] = $config;
193 $entities = $this->mapFromStorageRecords($records, $configs);
195 // Config entities wrap config objects, and therefore they need to inherit
196 // the cacheability metadata of config objects (to ensure e.g. additional
197 // cacheability metadata added by config overrides is not lost).
198 foreach ($entities as $id => $entity) {
199 // But rather than simply inheriting all cacheability metadata of config
200 // objects, we need to make sure the self-referring cache tag that is
201 // present on Config objects is not added to the Config entity. It must be
202 // removed for 3 reasons:
203 // 1. When renaming/duplicating a Config entity, the cache tag of the
204 // original config object would remain present, which would be wrong.
205 // 2. Some Config entities choose to not use the cache tag that the under-
206 // lying Config object provides by default (For performance and
207 // cacheability reasons it may not make sense to have a unique cache
208 // tag for every Config entity. The DateFormat Config entity specifies
209 // the 'rendered' cache tag for example, because A) date formats are
210 // changed extremely rarely, so invalidating all render cache items is
211 // fine, B) it means fewer cache tags per page.).
212 // 3. Fewer cache tags is better for performance.
213 $self_referring_cache_tag = ['config:' . $configs[$id]->getName()];
214 $config_cacheability = CacheableMetadata::createFromObject($configs[$id]);
215 $config_cacheability->setCacheTags(array_diff($config_cacheability->getCacheTags(), $self_referring_cache_tag));
216 $entity->addCacheableDependency($config_cacheability);
225 protected function doCreate(array $values) {
226 // Set default language to current language if not provided.
227 $values += [$this->langcodeKey => $this->languageManager->getCurrentLanguage()->getId()];
228 $entity = new $this->entityClass($values, $this->entityTypeId);
236 protected function doDelete($entities) {
237 foreach ($entities as $entity) {
238 $this->configFactory->getEditable($this->getPrefix() . $entity->id())->delete();
243 * Implements Drupal\Core\Entity\EntityStorageInterface::save().
245 * @throws EntityMalformedException
246 * When attempting to save a configuration entity that has no ID.
248 public function save(EntityInterface $entity) {
249 // Configuration entity IDs are strings, and '0' is a valid ID.
251 if ($id === NULL || $id === '') {
252 throw new EntityMalformedException('The entity does not have an ID.');
255 // Check the configuration entity ID length.
256 // @see \Drupal\Core\Config\Entity\ConfigEntityStorage::MAX_ID_LENGTH
257 // @todo Consider moving this to a protected method on the parent class, and
258 // abstracting it for all entity types.
259 if (strlen($entity->get($this->idKey)) > self::MAX_ID_LENGTH) {
260 throw new ConfigEntityIdLengthException("Configuration entity ID {$entity->get($this->idKey)} exceeds maximum allowed length of " . self::MAX_ID_LENGTH . " characters.");
263 return parent::save($entity);
269 protected function doSave($id, EntityInterface $entity) {
270 $is_new = $entity->isNew();
271 $prefix = $this->getPrefix();
272 $config_name = $prefix . $entity->id();
273 if ($id !== $entity->id()) {
274 // Renaming a config object needs to cater for:
275 // - Storage needs to access the original object.
276 // - The object needs to be renamed/copied in ConfigFactory and reloaded.
277 // - All instances of the object need to be renamed.
278 $this->configFactory->rename($prefix . $id, $config_name);
280 $config = $this->configFactory->getEditable($config_name);
282 // Retrieve the desired properties and set them in config.
283 $config->setData($this->mapToStorageRecord($entity));
284 $config->save($entity->hasTrustedData());
286 // Update the entity with the values stored in configuration. It is possible
287 // that configuration schema has casted some of the values.
288 if (!$entity->hasTrustedData()) {
289 $data = $this->mapFromStorageRecords([$config->get()]);
290 $updated_entity = current($data);
292 foreach (array_keys($config->get()) as $property) {
293 $value = $updated_entity->get($property);
294 $entity->set($property, $value);
298 return $is_new ? SAVED_NEW : SAVED_UPDATED;
302 * Maps from an entity object to the storage record.
304 * @param \Drupal\Core\Entity\EntityInterface $entity
308 * The record to store.
310 protected function mapToStorageRecord(EntityInterface $entity) {
311 return $entity->toArray();
317 protected function has($id, EntityInterface $entity) {
318 $prefix = $this->getPrefix();
319 $config = $this->configFactory->get($prefix . $id);
320 return !$config->isNew();
326 public function hasData() {
327 return (bool) $this->configFactory->listAll($this->getPrefix());
333 protected function buildCacheId($id) {
334 return parent::buildCacheId($id) . ':' . ($this->overrideFree ? '' : implode(':', $this->configFactory->getCacheKeys()));
338 * Invokes a hook on behalf of the entity.
341 * One of 'presave', 'insert', 'update', 'predelete', or 'delete'.
345 protected function invokeHook($hook, EntityInterface $entity) {
347 $this->moduleHandler->invokeAll($this->entityTypeId . '_' . $hook, [$entity]);
348 // Invoke the respective entity-level hook.
349 $this->moduleHandler->invokeAll('entity_' . $hook, [$entity, $this->entityTypeId]);
355 protected function getQueryServiceName() {
356 return 'entity.query.config';
362 public function importCreate($name, Config $new_config, Config $old_config) {
363 $entity = $this->_doCreateFromStorageRecord($new_config->get(), TRUE);
371 public function importUpdate($name, Config $new_config, Config $old_config) {
372 $id = static::getIDFromConfigName($name, $this->entityType->getConfigPrefix());
373 $entity = $this->load($id);
375 throw new ConfigImporterException("Attempt to update non-existing entity '$id'.");
377 $entity->setSyncing(TRUE);
378 $entity = $this->updateFromStorageRecord($entity, $new_config->get());
386 public function importDelete($name, Config $new_config, Config $old_config) {
387 $id = static::getIDFromConfigName($name, $this->entityType->getConfigPrefix());
388 $entity = $this->load($id);
389 $entity->setSyncing(TRUE);
397 public function importRename($old_name, Config $new_config, Config $old_config) {
398 return $this->importUpdate($old_name, $new_config, $old_config);
404 public function createFromStorageRecord(array $values) {
405 return $this->_doCreateFromStorageRecord($values);
409 * Helps create a configuration entity from storage values.
411 * Allows the configuration entity storage to massage storage values before
412 * creating an entity.
414 * @param array $values
415 * The array of values from the configuration storage.
416 * @param bool $is_syncing
417 * Is the configuration entity being created as part of a config sync.
419 * @return \Drupal\Core\Config\ConfigEntityInterface
420 * The configuration entity.
422 * @see \Drupal\Core\Config\Entity\ConfigEntityStorageInterface::createFromStorageRecord()
423 * @see \Drupal\Core\Config\Entity\ImportableEntityStorageInterface::importCreate()
425 protected function _doCreateFromStorageRecord(array $values, $is_syncing = FALSE) {
426 // Assign a new UUID if there is none yet.
427 if ($this->uuidKey && $this->uuidService && !isset($values[$this->uuidKey])) {
428 $values[$this->uuidKey] = $this->uuidService->generate();
430 $data = $this->mapFromStorageRecords([$values]);
431 $entity = current($data);
432 $entity->original = clone $entity;
433 $entity->setSyncing($is_syncing);
434 $entity->enforceIsNew();
435 $entity->postCreate($this);
437 // Modules might need to add or change the data initially held by the new
438 // entity object, for instance to fill-in default values.
439 $this->invokeHook('create', $entity);
447 public function updateFromStorageRecord(ConfigEntityInterface $entity, array $values) {
448 $entity->original = clone $entity;
450 $data = $this->mapFromStorageRecords([$values]);
451 $updated_entity = current($data);
453 /** @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $entity_type */
454 $entity_type = $this->getEntityType();
455 $id_key = $entity_type->getKey('id');
456 $properties = $entity_type->getPropertiesToExport($updated_entity->get($id_key));
458 if (empty($properties)) {
459 // Fallback to using the provided values. If the properties cannot be
460 // determined for the config entity type annotation or configuration
462 $properties = array_keys($values);
464 foreach ($properties as $property) {
465 if ($property === $this->uuidKey) {
466 // During an update the UUID field should not be copied. Under regular
467 // circumstances the values will be equal. If configuration is written
468 // twice during configuration install the updated entity will not have a
470 // @see \Drupal\Core\Config\ConfigInstaller::createConfiguration()
473 $entity->set($property, $updated_entity->get($property));
482 public function loadOverrideFree($id) {
483 $entities = $this->loadMultipleOverrideFree([$id]);
484 return isset($entities[$id]) ? $entities[$id] : NULL;
490 public function loadMultipleOverrideFree(array $ids = NULL) {
491 $this->overrideFree = TRUE;
492 $entities = $this->loadMultiple($ids);
493 $this->overrideFree = FALSE;