Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176

This commit is contained in:
Pantheon Automation 2015-08-17 17:00:26 -07:00 committed by Greg Anderson
commit 9921556621
13277 changed files with 1459781 additions and 0 deletions

View file

@ -0,0 +1,56 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\BootstrapConfigStorageFactory.
*/
namespace Drupal\Core\Config;
use Drupal\Core\Database\Database;
use Drupal\Core\Site\Settings;
/**
* Defines a factory for retrieving the config storage used pre-kernel.
*/
class BootstrapConfigStorageFactory {
/**
* Returns a configuration storage implementation.
*
* @param $class_loader
* The class loader. Normally Composer's ClassLoader, as included by the
* front controller, but may also be decorated; e.g.,
* \Symfony\Component\ClassLoader\ApcClassLoader.
*
* @return \Drupal\Core\Config\StorageInterface
* A configuration storage implementation.
*/
public static function get($class_loader = NULL) {
$bootstrap_config_storage = Settings::get('bootstrap_config_storage');
$storage_backend = FALSE;
if (!empty($bootstrap_config_storage) && is_callable($bootstrap_config_storage)) {
$storage_backend = call_user_func($bootstrap_config_storage, $class_loader);
}
// Fallback to the DatabaseStorage.
return $storage_backend ?: self::getDatabaseStorage();
}
/**
* Returns a Database configuration storage implementation.
*
* @return \Drupal\Core\Config\DatabaseStorage
*/
public static function getDatabaseStorage() {
return new DatabaseStorage(Database::getConnection(), 'config');
}
/**
* Returns a File-based configuration storage implementation.
*
* @return \Drupal\Core\Config\FileStorage
*/
public static function getFileStorage() {
return new FileStorage(config_get_config_directory(CONFIG_ACTIVE_DIRECTORY));
}
}

View file

@ -0,0 +1,308 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\CachedStorage.
*/
namespace Drupal\Core\Config;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
/**
* Defines the cached storage.
*
* The class gets another storage and a cache backend injected. It reads from
* the cache and delegates the read to the storage on a cache miss. It also
* handles cache invalidation.
*/
class CachedStorage implements StorageInterface, StorageCacheInterface {
use DependencySerializationTrait;
/**
* The configuration storage to be cached.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $storage;
/**
* The instantiated Cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* List of listAll() prefixes with their results.
*
* @var array
*/
protected $findByPrefixCache = array();
/**
* Constructs a new CachedStorage.
*
* @param \Drupal\Core\Config\StorageInterface $storage
* A configuration storage to be cached.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* A cache backend used to store configuration.
*/
public function __construct(StorageInterface $storage, CacheBackendInterface $cache) {
$this->storage = $storage;
$this->cache = $cache;
}
/**
* Implements Drupal\Core\Config\StorageInterface::exists().
*/
public function exists($name) {
// The cache would read in the entire data (instead of only checking whether
// any data exists), and on a potential cache miss, an additional storage
// lookup would have to happen, so check the storage directly.
return $this->storage->exists($name);
}
/**
* Implements Drupal\Core\Config\StorageInterface::read().
*/
public function read($name) {
$cache_key = $this->getCacheKey($name);
if ($cache = $this->cache->get($cache_key)) {
// The cache contains either the cached configuration data or FALSE
// if the configuration file does not exist.
return $cache->data;
}
// Read from the storage on a cache miss and cache the data. Also cache
// information about missing configuration objects.
$data = $this->storage->read($name);
$this->cache->set($cache_key, $data);
return $data;
}
/**
* {@inheritdoc}
*/
public function readMultiple(array $names) {
$data_to_return = array();
$cache_keys_map = $this->getCacheKeys($names);
$cache_keys = array_values($cache_keys_map);
$cached_list = $this->cache->getMultiple($cache_keys);
if (!empty($cache_keys)) {
// $cache_keys_map contains the full $name => $cache_key map, while
// $cache_keys contains just the $cache_key values that weren't found in
// the cache.
// @see \Drupal\Core\Cache\CacheBackendInterface::getMultiple()
$names_to_get = array_keys(array_intersect($cache_keys_map, $cache_keys));
$list = $this->storage->readMultiple($names_to_get);
// Cache configuration objects that were loaded from the storage, cache
// missing configuration objects as an explicit FALSE.
$items = array();
foreach ($names_to_get as $name) {
$data = isset($list[$name]) ? $list[$name] : FALSE;
$data_to_return[$name] = $data;
$items[$cache_keys_map[$name]] = array('data' => $data);
}
$this->cache->setMultiple($items);
}
// Add the configuration objects from the cache to the list.
$cache_keys_inverse_map = array_flip($cache_keys_map);
foreach ($cached_list as $cache_key => $cache) {
$name = $cache_keys_inverse_map[$cache_key];
$data_to_return[$name] = $cache->data;
}
// Ensure that only existing configuration objects are returned, filter out
// cached information about missing objects.
return array_filter($data_to_return);
}
/**
* Implements Drupal\Core\Config\StorageInterface::write().
*/
public function write($name, array $data) {
if ($this->storage->write($name, $data)) {
// While not all written data is read back, setting the cache instead of
// just deleting it avoids cache rebuild stampedes.
$this->cache->set($this->getCacheKey($name), $data);
$this->findByPrefixCache = array();
return TRUE;
}
return FALSE;
}
/**
* Implements Drupal\Core\Config\StorageInterface::delete().
*/
public function delete($name) {
// If the cache was the first to be deleted, another process might start
// rebuilding the cache before the storage is gone.
if ($this->storage->delete($name)) {
$this->cache->delete($this->getCacheKey($name));
$this->findByPrefixCache = array();
return TRUE;
}
return FALSE;
}
/**
* Implements Drupal\Core\Config\StorageInterface::rename().
*/
public function rename($name, $new_name) {
// If the cache was the first to be deleted, another process might start
// rebuilding the cache before the storage is renamed.
if ($this->storage->rename($name, $new_name)) {
$this->cache->delete($this->getCacheKey($name));
$this->cache->delete($this->getCacheKey($new_name));
$this->findByPrefixCache = array();
return TRUE;
}
return FALSE;
}
/**
* Implements Drupal\Core\Config\StorageInterface::encode().
*/
public function encode($data) {
return $this->storage->encode($data);
}
/**
* Implements Drupal\Core\Config\StorageInterface::decode().
*/
public function decode($raw) {
return $this->storage->decode($raw);
}
/**
* {@inheritdoc}
*/
public function listAll($prefix = '') {
// Do not cache when a prefix is not provided.
if ($prefix) {
return $this->findByPrefix($prefix);
}
return $this->storage->listAll();
}
/**
* Finds configuration object names starting with a given prefix.
*
* Given the following configuration objects:
* - node.type.article
* - node.type.page
*
* Passing the prefix 'node.type.' will return an array containing the above
* names.
*
* @param string $prefix
* The prefix to search for
*
* @return array
* An array containing matching configuration object names.
*/
protected function findByPrefix($prefix) {
$cache_key = $this->getCacheKey($prefix);
if (!isset($this->findByPrefixCache[$cache_key])) {
$this->findByPrefixCache[$cache_key] = $this->storage->listAll($prefix);
}
return $this->findByPrefixCache[$cache_key];
}
/**
* Implements Drupal\Core\Config\StorageInterface::deleteAll().
*/
public function deleteAll($prefix = '') {
// If the cache was the first to be deleted, another process might start
// rebuilding the cache before the storage is renamed.
$names = $this->storage->listAll($prefix);
if ($this->storage->deleteAll($prefix)) {
$this->cache->deleteMultiple($this->getCacheKeys($names));
return TRUE;
}
return FALSE;
}
/**
* Clears the static list cache.
*/
public function resetListCache() {
$this->findByPrefixCache = array();
}
/**
* {@inheritdoc}
*/
public function createCollection($collection) {
return new static(
$this->storage->createCollection($collection),
$this->cache
);
}
/**
* {@inheritdoc}
*/
public function getAllCollectionNames() {
return $this->storage->getAllCollectionNames();
}
/**
* {@inheritdoc}
*/
public function getCollectionName() {
return $this->storage->getCollectionName();
}
/**
* Returns a cache key for a configuration name using the collection.
*
* @param string $name
* The configuration name.
*
* @return string
* The cache key for the configuration name.
*/
protected function getCacheKey($name) {
return $this->getCollectionPrefix() . $name;
}
/**
* Returns a cache key map for an array of configuration names.
*
* @param array $names
* The configuration names.
*
* @return array
* An array of cache keys keyed by configuration names.
*/
protected function getCacheKeys(array $names) {
$prefix = $this->getCollectionPrefix();
$cache_keys = array_map(function($name) use ($prefix) {
return $prefix . $name;
}, $names);
return array_combine($names, $cache_keys);
}
/**
* Returns a cache ID prefix to use for the collection.
*
* @return string
* The cache ID prefix.
*/
protected function getCollectionPrefix() {
$collection = $this->storage->getCollectionName();
if ($collection == StorageInterface::DEFAULT_COLLECTION) {
return '';
}
return $collection . ':';
}
}

View file

@ -0,0 +1,311 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Config.
*/
namespace Drupal\Core\Config;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\Cache;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Defines the default configuration object.
*
* Encapsulates all capabilities needed for configuration handling for a
* specific configuration object, including support for runtime overrides. The
* overrides are handled on top of the stored configuration so they are not
* saved back to storage.
*
* @ingroup config_api
*/
class Config extends StorableConfigBase {
/**
* An event dispatcher instance to use for configuration events.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* The current runtime data.
*
* The configuration data from storage merged with module and settings
* overrides.
*
* @var array
*/
protected $overriddenData;
/**
* The current module overrides.
*
* @var array
*/
protected $moduleOverrides;
/**
* The current settings overrides.
*
* @var array
*/
protected $settingsOverrides;
/**
* Constructs a configuration object.
*
* @param string $name
* The name of the configuration object being constructed.
* @param \Drupal\Core\Config\StorageInterface $storage
* A storage object to use for reading and writing the
* configuration data.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* An event dispatcher instance to use for configuration events.
* @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config
* The typed configuration manager service.
*/
public function __construct($name, StorageInterface $storage, EventDispatcherInterface $event_dispatcher, TypedConfigManagerInterface $typed_config) {
$this->name = $name;
$this->storage = $storage;
$this->eventDispatcher = $event_dispatcher;
$this->typedConfigManager = $typed_config;
}
/**
* {@inheritdoc}
*/
public function initWithData(array $data) {
parent::initWithData($data);
$this->resetOverriddenData();
return $this;
}
/**
* {@inheritdoc}
*/
public function get($key = '') {
if (!isset($this->overriddenData)) {
$this->setOverriddenData();
}
if (empty($key)) {
return $this->overriddenData;
}
else {
$parts = explode('.', $key);
if (count($parts) == 1) {
return isset($this->overriddenData[$key]) ? $this->overriddenData[$key] : NULL;
}
else {
$value = NestedArray::getValue($this->overriddenData, $parts, $key_exists);
return $key_exists ? $value : NULL;
}
}
}
/**
* {@inheritdoc}
*/
public function setData(array $data, $validate_keys = TRUE) {
parent::setData($data, $validate_keys);
$this->resetOverriddenData();
return $this;
}
/**
* Sets settings.php overrides for this configuration object.
*
* The overridden data only applies to this configuration object.
*
* @param array $data
* The overridden values of the configuration data.
*
* @return \Drupal\Core\Config\Config
* The configuration object.
*/
public function setSettingsOverride(array $data) {
$this->settingsOverrides = $data;
$this->resetOverriddenData();
return $this;
}
/**
* Sets module overrides for this configuration object.
*
* @param array $data
* The overridden values of the configuration data.
*
* @return \Drupal\Core\Config\Config
* The configuration object.
*/
public function setModuleOverride(array $data) {
$this->moduleOverrides = $data;
$this->resetOverriddenData();
return $this;
}
/**
* Sets the current data for this configuration object.
*
* Configuration overrides operate at two distinct layers: modules and
* settings.php. Overrides in settings.php take precedence over values
* provided by modules. Precedence or different module overrides is
* determined by the priority of the config.factory.override tagged services.
*
* @return \Drupal\Core\Config\Config
* The configuration object.
*/
protected function setOverriddenData() {
$this->overriddenData = $this->data;
if (isset($this->moduleOverrides) && is_array($this->moduleOverrides)) {
$this->overriddenData = NestedArray::mergeDeepArray(array($this->overriddenData, $this->moduleOverrides), TRUE);
}
if (isset($this->settingsOverrides) && is_array($this->settingsOverrides)) {
$this->overriddenData = NestedArray::mergeDeepArray(array($this->overriddenData, $this->settingsOverrides), TRUE);
}
return $this;
}
/**
* Resets the current data, so overrides are re-applied.
*
* This method should be called after the original data or the overridden data
* has been changed.
*
* @return \Drupal\Core\Config\Config
* The configuration object.
*/
protected function resetOverriddenData() {
unset($this->overriddenData);
return $this;
}
/**
* {@inheritdoc}
*/
public function set($key, $value) {
parent::set($key, $value);
$this->resetOverriddenData();
return $this;
}
/**
* {@inheritdoc}
*/
public function clear($key) {
parent::clear($key);
$this->resetOverriddenData();
return $this;
}
/**
* {@inheritdoc}
*/
public function save($has_trusted_data = FALSE) {
// Validate the configuration object name before saving.
static::validateName($this->name);
// If there is a schema for this configuration object, cast all values to
// conform to the schema.
if (!$has_trusted_data) {
if ($this->typedConfigManager->hasConfigSchema($this->name)) {
// Ensure that the schema wrapper has the latest data.
$this->schemaWrapper = NULL;
foreach ($this->data as $key => $value) {
$this->data[$key] = $this->castValue($key, $value);
}
}
else {
foreach ($this->data as $key => $value) {
$this->validateValue($key, $value);
}
}
}
$this->storage->write($this->name, $this->data);
if (!$this->isNew) {
Cache::invalidateTags($this->getCacheTags());
}
$this->isNew = FALSE;
$this->eventDispatcher->dispatch(ConfigEvents::SAVE, new ConfigCrudEvent($this));
$this->originalData = $this->data;
// Potentially configuration schema could have changed the underlying data's
// types.
$this->resetOverriddenData();
return $this;
}
/**
* Deletes the configuration object.
*
* @return \Drupal\Core\Config\Config
* The configuration object.
*/
public function delete() {
$this->data = array();
$this->storage->delete($this->name);
Cache::invalidateTags($this->getCacheTags());
$this->isNew = TRUE;
$this->resetOverriddenData();
$this->eventDispatcher->dispatch(ConfigEvents::DELETE, new ConfigCrudEvent($this));
$this->originalData = $this->data;
return $this;
}
/**
* Gets the raw data without overrides.
*
* @return array
* The raw data.
*/
public function getRawData() {
return $this->data;
}
/**
* Gets original data from this configuration object.
*
* Original data is the data as it is immediately after loading from
* configuration storage before any changes. If this is a new configuration
* object it will be an empty array.
*
* @see \Drupal\Core\Config\Config::get()
*
* @param string $key
* A string that maps to a key within the configuration data.
* @param bool $apply_overrides
* Apply any overrides to the original data. Defaults to TRUE.
*
* @return mixed
* The data that was requested.
*/
public function getOriginal($key = '', $apply_overrides = TRUE) {
$original_data = $this->originalData;
if ($apply_overrides) {
// Apply overrides.
if (isset($this->moduleOverrides) && is_array($this->moduleOverrides)) {
$original_data = NestedArray::mergeDeepArray(array($original_data, $this->moduleOverrides), TRUE);
}
if (isset($this->settingsOverrides) && is_array($this->settingsOverrides)) {
$original_data = NestedArray::mergeDeepArray(array($original_data, $this->settingsOverrides), TRUE);
}
}
if (empty($key)) {
return $original_data;
}
else {
$parts = explode('.', $key);
if (count($parts) == 1) {
return isset($original_data[$key]) ? $original_data[$key] : NULL;
}
else {
$value = NestedArray::getValue($original_data, $parts, $key_exists);
return $key_exists ? $value : NULL;
}
}
}
}

View file

@ -0,0 +1,289 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigBase.
*/
namespace Drupal\Core\Config;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use \Drupal\Core\DependencyInjection\DependencySerializationTrait;
/**
* Provides a base class for configuration objects with get/set support.
*
* Encapsulates all capabilities needed for runtime configuration handling for
* a specific configuration object.
*
* Extend directly from this class for non-storable configuration where the
* configuration API is desired but storage is not possible; for example, if
* the data is derived at runtime. For storable configuration, extend
* \Drupal\Core\Config\StorableConfigBase.
*
* @see \Drupal\Core\Config\StorableConfigBase
* @see \Drupal\Core\Config\Config
* @see \Drupal\Core\Theme\ThemeSettings
*/
abstract class ConfigBase implements CacheableDependencyInterface {
use DependencySerializationTrait;
/**
* The name of the configuration object.
*
* @var string
*/
protected $name;
/**
* The data of the configuration object.
*
* @var array
*/
protected $data = array();
/**
* The maximum length of a configuration object name.
*
* Many filesystems (including HFS, NTFS, and ext4) have a maximum file name
* length of 255 characters. To ensure that no configuration objects
* incompatible with this limitation are created, we enforce a maximum name
* length of 250 characters (leaving 5 characters for the file extension).
*
* @see http://en.wikipedia.org/wiki/Comparison_of_file_systems
*
* Configuration objects not stored on the filesystem should still be
* restricted in name length so name can be used as a cache key.
*/
const MAX_NAME_LENGTH = 250;
/**
* Returns the name of this configuration object.
*
* @return string
* The name of the configuration object.
*/
public function getName() {
return $this->name;
}
/**
* Sets the name of this configuration object.
*
* @param string $name
* The name of the configuration object.
*
* @return $this
* The configuration object.
*/
public function setName($name) {
$this->name = $name;
return $this;
}
/**
* Validates the configuration object name.
*
* @param string $name
* The name of the configuration object.
*
* @throws \Drupal\Core\Config\ConfigNameException
*
* @see Config::MAX_NAME_LENGTH
*/
public static function validateName($name) {
// The name must be namespaced by owner.
if (strpos($name, '.') === FALSE) {
throw new ConfigNameException(SafeMarkup::format('Missing namespace in Config object name @name.', array(
'@name' => $name,
)));
}
// The name must be shorter than Config::MAX_NAME_LENGTH characters.
if (strlen($name) > self::MAX_NAME_LENGTH) {
throw new ConfigNameException(SafeMarkup::format('Config object name @name exceeds maximum allowed length of @length characters.', array(
'@name' => $name,
'@length' => self::MAX_NAME_LENGTH,
)));
}
// The name must not contain any of the following characters:
// : ? * < > " ' / \
if (preg_match('/[:?*<>"\'\/\\\\]/', $name)) {
throw new ConfigNameException(SafeMarkup::format('Invalid character in Config object name @name.', array(
'@name' => $name,
)));
}
}
/**
* Gets data from this configuration object.
*
* @param string $key
* A string that maps to a key within the configuration data.
* For instance in the following configuration array:
* @code
* array(
* 'foo' => array(
* 'bar' => 'baz',
* ),
* );
* @endcode
* A key of 'foo.bar' would return the string 'baz'. However, a key of 'foo'
* would return array('bar' => 'baz').
* If no key is specified, then the entire data array is returned.
*
* @return mixed
* The data that was requested.
*/
public function get($key = '') {
if (empty($key)) {
return $this->data;
}
else {
$parts = explode('.', $key);
if (count($parts) == 1) {
return isset($this->data[$key]) ? $this->data[$key] : NULL;
}
else {
$value = NestedArray::getValue($this->data, $parts, $key_exists);
return $key_exists ? $value : NULL;
}
}
}
/**
* Replaces the data of this configuration object.
*
* @param array $data
* The new configuration data.
* @param bool $validate_keys
* (optional) Whether the data should be verified for valid keys. Set to
* FALSE if the $data is known to be valid already (for example, being
* loaded from the config storage).
*
* @return $this
* The configuration object.
*
* @throws \Drupal\Core\Config\ConfigValueException
* If any key in $data in any depth contains a dot.
*/
public function setData(array $data, $validate_keys = TRUE) {
if ($validate_keys) {
$this->validateKeys($data);
}
$this->data = $data;
return $this;
}
/**
* Sets a value in this configuration object.
*
* @param string $key
* Identifier to store value in configuration.
* @param mixed $value
* Value to associate with identifier.
*
* @return $this
* The configuration object.
*
* @throws \Drupal\Core\Config\ConfigValueException
* If $value is an array and any of its keys in any depth contains a dot.
*/
public function set($key, $value) {
// The dot/period is a reserved character; it may appear between keys, but
// not within keys.
if (is_array($value)) {
$this->validateKeys($value);
}
$parts = explode('.', $key);
if (count($parts) == 1) {
$this->data[$key] = $value;
}
else {
NestedArray::setValue($this->data, $parts, $value);
}
return $this;
}
/**
* Validates all keys in a passed in config array structure.
*
* @param array $data
* Configuration array structure.
*
* @return null
*
* @throws \Drupal\Core\Config\ConfigValueException
* If any key in $data in any depth contains a dot.
*/
protected function validateKeys(array $data) {
foreach ($data as $key => $value) {
if (strpos($key, '.') !== FALSE) {
throw new ConfigValueException(SafeMarkup::format('@key key contains a dot which is not supported.', array('@key' => $key)));
}
if (is_array($value)) {
$this->validateKeys($value);
}
}
}
/**
* Unsets a value in this configuration object.
*
* @param string $key
* Name of the key whose value should be unset.
*
* @return $this
* The configuration object.
*/
public function clear($key) {
$parts = explode('.', $key);
if (count($parts) == 1) {
unset($this->data[$key]);
}
else {
NestedArray::unsetValue($this->data, $parts);
}
return $this;
}
/**
* Merges data into a configuration object.
*
* @param array $data_to_merge
* An array containing data to merge.
*
* @return $this
* The configuration object.
*/
public function merge(array $data_to_merge) {
// Preserve integer keys so that configuration keys are not changed.
$this->setData(NestedArray::mergeDeepArray(array($this->data, $data_to_merge), TRUE));
return $this;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return [];
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return ['config:' . $this->name];
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return Cache::PERMANENT;
}
}

View file

@ -0,0 +1,79 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigCollectionInfo.
*/
namespace Drupal\Core\Config;
use Symfony\Component\EventDispatcher\Event;
/**
* Gets information on all the possible configuration collections.
*/
class ConfigCollectionInfo extends Event {
/**
* Configuration collection information keyed by collection name.
*
* The value is either the configuration factory override that is responsible
* for the collection or null if there is not one.
*
* @var array
*/
protected $collections = array();
/**
* Adds a collection to the list of possible collections.
*
* @param string $collection
* Collection name to add.
* @param \Drupal\Core\Config\ConfigFactoryOverrideInterface
* (optional) The configuration factory override service responsible for the
* collection.
*
* @throws \InvalidArgumentException
* Exception thrown if $collection is equal to
* \Drupal\Core\Config\StorageInterface::DEFAULT_COLLECTION
*/
public function addCollection($collection, ConfigFactoryOverrideInterface $override_service = NULL) {
if ($collection == StorageInterface::DEFAULT_COLLECTION) {
throw new \InvalidArgumentException('Can not add the default collection to the ConfigCollectionInfo object');
}
$this->collections[$collection] = $override_service;
}
/**
* Gets the list of possible collection names.
*
* @param bool $include_default
* (Optional) Include the default collection. Defaults to TRUE.
*
* @return array
* The list of possible collection names.
*/
public function getCollectionNames($include_default = TRUE) {
$collection_names = array_keys($this->collections);
sort($collection_names);
if ($include_default) {
array_unshift($collection_names, StorageInterface::DEFAULT_COLLECTION);
}
return $collection_names;
}
/**
* Gets the config factory override service responsible for the collection.
*
* @param string $collection
* The configuration collection.
*
* @return \Drupal\Core\Config\ConfigFactoryOverrideInterface|NULL
* The override service responsible for the collection if one exists. NULL
* if not.
*/
public function getOverrideService($collection) {
return isset($this->collections[$collection]) ? $this->collections[$collection] : NULL;
}
}

View file

@ -0,0 +1,57 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigCrudEvent.
*/
namespace Drupal\Core\Config;
use Symfony\Component\EventDispatcher\Event;
/**
* Wraps a configuration event for event listeners.
*/
class ConfigCrudEvent extends Event {
/**
* Configuration object.
*
* @var \Drupal\Core\Config\Config
*/
protected $config;
/**
* Constructs a configuration event object.
*
* @param \Drupal\Core\Config\Config
* Configuration object.
*/
public function __construct(Config $config) {
$this->config = $config;
}
/**
* Gets configuration object.
*
* @return \Drupal\Core\Config\Config
* The configuration object that caused the event to fire.
*/
public function getConfig() {
return $this->config;
}
/**
* Checks to see if the provided configuration key's value has changed.
*
* @param string $key
* The configuration key to check if it has changed.
*
* @return bool
*/
public function isChanged($key) {
return $this->config->get($key) !== $this->config->getOriginal($key);
}
}

View file

@ -0,0 +1,15 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigDuplicateUUIDException.
*/
namespace Drupal\Core\Config;
/**
* Exception thrown when a config object UUID causes a conflict.
*/
class ConfigDuplicateUUIDException extends ConfigException {
}

View file

@ -0,0 +1,136 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigEvents.
*/
namespace Drupal\Core\Config;
/**
* Defines events for the configuration system.
*
* @see \Drupal\Core\Config\ConfigCrudEvent
*/
final class ConfigEvents {
/**
* Name of the event fired when saving a configuration object.
*
* This event allows modules to perform an action whenever a configuration
* object is saved. The event listener method receives a
* \Drupal\Core\Config\ConfigCrudEvent instance.
*
* @Event
*
* @see \Drupal\Core\Config\ConfigCrudEvent
* @see \Drupal\Core\Config\Config::save()
* @see \Drupal\Core\Config\ConfigFactory::onConfigSave()
*
* @var string
*/
const SAVE = 'config.save';
/**
* Name of the event fired when deleting a configuration object.
*
* This event allows modules to perform an action whenever a configuration
* object is deleted. The event listener method receives a
* \Drupal\Core\Config\ConfigCrudEvent instance.
*
* @Event
*
* @see \Drupal\Core\Config\ConfigCrudEvent
* @see \Drupal\Core\Config\Config::delete()
* @see \Drupal\Core\Config\ConfigFactory::onConfigDelete()
*
* @var string
*/
const DELETE = 'config.delete';
/**
* Name of the event fired when renaming a configuration object.
*
* This event allows modules to perform an action whenever a configuration
* object's name is changed. The event listener method receives a
* \Drupal\Core\Config\ConfigRenameEvent instance.
*
* @Event
*
* @see \Drupal\Core\Config\ConfigRenameEvent
* @see \Drupal\Core\Config\ConfigFactoryInterface::rename().
*
* @var string
*/
const RENAME = 'config.rename';
/**
* Name of the event fired when validating imported configuration.
*
* This event allows modules to perform additional validation operations when
* configuration is being imported. The event listener method receives a
* \Drupal\Core\Config\ConfigImporterEvent instance.
*
* @Event
*
* @see \Drupal\Core\Config\ConfigImporterEvent
* @see \Drupal\Core\Config\ConfigImporter::validate().
* @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber::onConfigImporterValidate().
*
* @var string
*/
const IMPORT_VALIDATE = 'config.importer.validate';
/**
* Name of the event fired when importing configuration to target storage.
*
* This event allows modules to perform additional actions when configuration
* is imported. The event listener method receives a
* \Drupal\Core\Config\ConfigImporterEvent instance.
*
* @Event
*
* @see \Drupal\Core\Config\ConfigImporterEvent
* @see \Drupal\Core\Config\ConfigImporter::import().
* @see \Drupal\Core\EventSubscriber\ConfigSnapshotSubscriber::onConfigImporterImport().
*
* @var string
*/
const IMPORT = 'config.importer.import';
/**
* Name of event fired when missing content dependencies are detected.
*
* Events subscribers are fired as part of the configuration import batch.
* Each subscribe should call
* \Drupal\Core\Config\MissingContentEvent::resolveMissingContent() when they
* address a missing dependency. To address large amounts of dependencies
* subscribers can call
* \Drupal\Core\Config\MissingContentEvent::stopPropagation() which will stop
* calling other events and guarantee that the configuration import batch will
* fire the event again to continue processing missing content dependencies.
*
* @see \Drupal\Core\Config\ConfigImporter::processMissingContent()
* @see \Drupal\Core\Config\MissingContentEvent
*/
const IMPORT_MISSING_CONTENT = 'config.importer.missing_content';
/**
* Name of event fired to collect information on all config collections.
*
* This event allows modules to add to the list of configuration collections
* retrieved by \Drupal\Core\Config\ConfigManager::getConfigCollectionInfo().
* The event listener method receives a
* \Drupal\Core\Config\ConfigCollectionInfo instance.
*
* @Event
*
* @see \Drupal\Core\Config\ConfigCollectionInfo
* @see \Drupal\Core\Config\ConfigManager::getConfigCollectionInfo()
* @see \Drupal\Core\Config\ConfigFactoryOverrideBase
*
* @var string
*/
const COLLECTION_INFO = 'config.collection_info';
}

View file

@ -0,0 +1,13 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigException.
*/
namespace Drupal\Core\Config;
/**
* A base exception thrown in any configuration system operations.
*/
class ConfigException extends \RuntimeException {}

View file

@ -0,0 +1,383 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigFactory.
*/
namespace Drupal\Core\Config;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\Cache;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Defines the configuration object factory.
*
* The configuration object factory instantiates a Config object for each
* configuration object name that is accessed and returns it to callers.
*
* @see \Drupal\Core\Config\Config
*
* Each configuration object gets a storage object injected, which
* is used for reading and writing the configuration data.
*
* @see \Drupal\Core\Config\StorageInterface
*
* @ingroup config_api
*/
class ConfigFactory implements ConfigFactoryInterface, EventSubscriberInterface {
/**
* A storage instance for reading and writing configuration data.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $storage;
/**
* An event dispatcher instance to use for configuration events.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* Cached configuration objects.
*
* @var \Drupal\Core\Config\Config[]
*/
protected $cache = array();
/**
* The typed config manager.
*
* @var \Drupal\Core\Config\TypedConfigManagerInterface
*/
protected $typedConfigManager;
/**
* An array of config factory override objects ordered by priority.
*
* @var \Drupal\Core\Config\ConfigFactoryOverrideInterface[]
*/
protected $configFactoryOverrides = array();
/**
* Constructs the Config factory.
*
* @param \Drupal\Core\Config\StorageInterface $storage
* The configuration storage engine.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* An event dispatcher instance to use for configuration events.
* @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config
* The typed configuration manager.
*/
public function __construct(StorageInterface $storage, EventDispatcherInterface $event_dispatcher, TypedConfigManagerInterface $typed_config) {
$this->storage = $storage;
$this->eventDispatcher = $event_dispatcher;
$this->typedConfigManager = $typed_config;
}
/**
* {@inheritdoc}
*/
public function getEditable($name) {
return $this->doGet($name, FALSE);
}
/**
* {@inheritdoc}
*/
public function get($name) {
return $this->doGet($name);
}
/**
* Returns a configuration object for a given name.
*
* @param string $name
* The name of the configuration object to construct.
* @param bool $immutable
* (optional) Create an immutable configuration object. Defaults to TRUE.
*
* @return \Drupal\Core\Config\Config|\Drupal\Core\Config\ImmutableConfig
* A configuration object.
*/
protected function doGet($name, $immutable = TRUE) {
if ($config = $this->doLoadMultiple(array($name), $immutable)) {
return $config[$name];
}
else {
// If the configuration object does not exist in the configuration
// storage, create a new object and add it to the static cache.
$cache_key = $this->getConfigCacheKey($name, $immutable);
$this->cache[$cache_key] = $this->createConfigObject($name, $immutable);
if ($immutable) {
// Get and apply any overrides.
$overrides = $this->loadOverrides(array($name));
if (isset($overrides[$name])) {
$this->cache[$cache_key]->setModuleOverride($overrides[$name]);
}
// Apply any settings.php overrides.
if (isset($GLOBALS['config'][$name])) {
$this->cache[$cache_key]->setSettingsOverride($GLOBALS['config'][$name]);
}
}
return $this->cache[$cache_key];
}
}
/**
* {@inheritdoc}
*/
public function loadMultiple(array $names) {
return $this->doLoadMultiple($names);
}
/**
* Returns a list of configuration objects for the given names.
*
* @param array $names
* List of names of configuration objects.
* @param bool $immutable
* (optional) Create an immutable configuration objects. Defaults to TRUE.
*
* @return \Drupal\Core\Config\Config[]|\Drupal\Core\Config\ImmutableConfig[]
* List of successfully loaded configuration objects, keyed by name.
*/
protected function doLoadMultiple(array $names, $immutable = TRUE) {
$list = array();
foreach ($names as $key => $name) {
$cache_key = $this->getConfigCacheKey($name, $immutable);
if (isset($this->cache[$cache_key])) {
$list[$name] = $this->cache[$cache_key];
unset($names[$key]);
}
}
// Pre-load remaining configuration files.
if (!empty($names)) {
// Initialise override information.
$module_overrides = array();
$storage_data = $this->storage->readMultiple($names);
if ($immutable && !empty($storage_data)) {
// Only get module overrides if we have configuration to override.
$module_overrides = $this->loadOverrides($names);
}
foreach ($storage_data as $name => $data) {
$cache_key = $this->getConfigCacheKey($name, $immutable);
$this->cache[$cache_key] = $this->createConfigObject($name, $immutable);
$this->cache[$cache_key]->initWithData($data);
if ($immutable) {
if (isset($module_overrides[$name])) {
$this->cache[$cache_key]->setModuleOverride($module_overrides[$name]);
}
if (isset($GLOBALS['config'][$name])) {
$this->cache[$cache_key]->setSettingsOverride($GLOBALS['config'][$name]);
}
}
$list[$name] = $this->cache[$cache_key];
}
}
return $list;
}
/**
* Get arbitrary overrides for the named configuration objects from modules.
*
* @param array $names
* The names of the configuration objects to get overrides for.
*
* @return array
* An array of overrides keyed by the configuration object name.
*/
protected function loadOverrides(array $names) {
$overrides = array();
foreach ($this->configFactoryOverrides as $override) {
// Existing overrides take precedence since these will have been added
// by events with a higher priority.
$overrides = NestedArray::mergeDeepArray(array($override->loadOverrides($names), $overrides), TRUE);
}
return $overrides;
}
/**
* {@inheritdoc}
*/
public function reset($name = NULL) {
if ($name) {
// Clear all cached configuration for this name.
foreach ($this->getConfigCacheKeys($name) as $cache_key) {
unset($this->cache[$cache_key]);
}
}
else {
$this->cache = array();
}
// Clear the static list cache if supported by the storage.
if ($this->storage instanceof StorageCacheInterface) {
$this->storage->resetListCache();
}
return $this;
}
/**
* {@inheritdoc}
*/
public function rename($old_name, $new_name) {
Cache::invalidateTags($this->get($old_name)->getCacheTags());
$this->storage->rename($old_name, $new_name);
// Clear out the static cache of any references to the old name.
foreach ($this->getConfigCacheKeys($old_name) as $old_cache_key) {
unset($this->cache[$old_cache_key]);
}
// Prime the cache and load the configuration with the correct overrides.
$config = $this->get($new_name);
$this->eventDispatcher->dispatch(ConfigEvents::RENAME, new ConfigRenameEvent($config, $old_name));
return $this;
}
/**
* {@inheritdoc}
*/
public function getCacheKeys() {
// Because get() adds overrides both from $GLOBALS and from
// $this->configFactoryOverrides, add cache keys for each.
$keys[] = 'global_overrides';
foreach($this->configFactoryOverrides as $override) {
$keys[] = $override->getCacheSuffix();
}
return $keys;
}
/**
* Gets the static cache key for a given config name.
*
* @param string $name
* The name of the configuration object.
* @param bool $immutable
* Whether or not the object is mutable.
*
* @return string
* The cache key.
*/
protected function getConfigCacheKey($name, $immutable) {
$suffix = '';
if ($immutable) {
$suffix = ':' . implode(':', $this->getCacheKeys());
}
return $name . $suffix;
}
/**
* Gets all the cache keys that match the provided config name.
*
* @param string $name
* The name of the configuration object.
*
* @return array
* An array of cache keys that match the provided config name.
*/
protected function getConfigCacheKeys($name) {
return array_filter(array_keys($this->cache), function($key) use ($name) {
// Return TRUE if the key is the name or starts with the configuration
// name plus the delimiter.
return $key === $name || strpos($key, $name . ':') === 0;
});
}
/**
* {@inheritdoc}
*/
public function clearStaticCache() {
$this->cache = array();
return $this;
}
/**
* {@inheritdoc}
*/
public function listAll($prefix = '') {
return $this->storage->listAll($prefix);
}
/**
* Updates stale static cache entries when configuration is saved.
*
* @param ConfigCrudEvent $event
* The configuration event.
*/
public function onConfigSave(ConfigCrudEvent $event) {
// Ensure that the static cache contains up to date configuration objects by
// replacing the data on any entries for the configuration object apart
// from the one that references the actual config object being saved.
$saved_config = $event->getConfig();
foreach ($this->getConfigCacheKeys($saved_config->getName()) as $cache_key) {
$cached_config = $this->cache[$cache_key];
if ($cached_config !== $saved_config) {
// We can not just update the data since other things about the object
// might have changed. For example, whether or not it is new.
$this->cache[$cache_key]->initWithData($saved_config->getRawData());
}
}
}
/**
* Removes stale static cache entries when configuration is deleted.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
* The configuration event.
*/
public function onConfigDelete(ConfigCrudEvent $event) {
// Ensure that the static cache does not contain deleted configuration.
foreach ($this->getConfigCacheKeys($event->getConfig()->getName()) as $cache_key) {
unset($this->cache[$cache_key]);
}
}
/**
* {@inheritdoc}
*/
static function getSubscribedEvents() {
$events[ConfigEvents::SAVE][] = array('onConfigSave', 255);
$events[ConfigEvents::DELETE][] = array('onConfigDelete', 255);
return $events;
}
/**
* {@inheritdoc}
*/
public function addOverride(ConfigFactoryOverrideInterface $config_factory_override) {
$this->configFactoryOverrides[] = $config_factory_override;
}
/**
* Creates a configuration object.
*
* @param string $name
* Configuration object name.
* @param bool $immutable
* Determines whether a mutable or immutable config object is returned.
*
* @return \Drupal\Core\Config\Config|\Drupal\Core\Config\ImmutableConfig
* The configuration object.
*/
protected function createConfigObject($name, $immutable) {
if ($immutable) {
return new ImmutableConfig($name, $this->storage, $this->eventDispatcher, $this->typedConfigManager);
}
return new Config($name, $this->storage, $this->eventDispatcher, $this->typedConfigManager);
}
}

View file

@ -0,0 +1,125 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigFactoryInterface.
*/
namespace Drupal\Core\Config;
/**
* Defines the interface for a configuration object factory.
*
* @ingroup config_api
*/
interface ConfigFactoryInterface {
/**
* Returns an immutable configuration object for a given name.
*
* @param string $name
* The name of the configuration object to construct.
*
* @return \Drupal\Core\Config\ImmutableConfig
* A configuration object.
*/
public function get($name);
/**
* Returns an mutable configuration object for a given name.
*
* Should not be used for config that will have runtime effects. Therefore it
* is always loaded override free.
*
* @param string $name
* The name of the configuration object to construct.
*
* @return \Drupal\Core\Config\Config
* A configuration object.
*/
public function getEditable($name);
/**
* Returns a list of configuration objects for the given names.
*
* This will pre-load all requested configuration objects does not create
* new configuration objects. This method always return immutable objects.
* ConfigFactoryInterface::getEditable() should be used to retrieve mutable
* configuration objects, one by one.
*
* @param array $names
* List of names of configuration objects.
*
* @return \Drupal\Core\Config\ImmutableConfig[]
* List of successfully loaded configuration objects, keyed by name.
*/
public function loadMultiple(array $names);
/**
* Resets and re-initializes configuration objects. Internal use only.
*
* @param string|null $name
* (optional) The name of the configuration object to reset. If omitted, all
* configuration objects are reset.
*
* @return $this
*/
public function reset($name = NULL);
/**
* Renames a configuration object using the storage.
*
* @param string $old_name
* The old name of the configuration object.
* @param string $new_name
* The new name of the configuration object.
*
* @return $this
*/
public function rename($old_name, $new_name);
/**
* The cache keys associated with the state of the config factory.
*
* All state information that can influence the result of a get() should be
* included. Typically, this includes a key for each override added via
* addOverride(). This allows external code to maintain caches of
* configuration data in addition to or instead of caches maintained by the
* factory.
*
* @return array
* An array of strings, used to generate a cache ID.
*/
public function getCacheKeys();
/**
* Clears the config factory static cache.
*
* @return $this
*/
public function clearStaticCache();
/**
* Gets configuration object names starting with a given prefix.
*
* @see \Drupal\Core\Config\StorageInterface::listAll()
*
* @param string $prefix
* (optional) The prefix to search for. If omitted, all configuration object
* names that exist are returned.
*
* @return array
* An array containing matching configuration object names.
*/
public function listAll($prefix = '');
/**
* Adds config factory override services.
*
* @param \Drupal\Core\Config\ConfigFactoryOverrideInterface $config_factory_override
* The config factory override service to add. It is added at the end of the
* priority list (lower priority relative to existing ones).
*/
public function addOverride(ConfigFactoryOverrideInterface $config_factory_override);
}

View file

@ -0,0 +1,121 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigFactoryOverrideBase.
*/
namespace Drupal\Core\Config;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Defines a base event listener implementation configuration overrides.
*/
abstract class ConfigFactoryOverrideBase implements EventSubscriberInterface {
/**
* Reacts to the ConfigEvents::COLLECTION_INFO event.
*
* @param \Drupal\Core\Config\ConfigCollectionInfo $collection_info
* The configuration collection names event.
*/
abstract public function addCollections(ConfigCollectionInfo $collection_info);
/**
* Actions to be performed to configuration override on configuration save.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
* The config CRUD event.
*/
abstract public function onConfigSave(ConfigCrudEvent $event);
/**
* Actions to be performed to configuration override on configuration delete.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
* The config CRUD event.
*/
abstract public function onConfigDelete(ConfigCrudEvent $event);
/**
* Actions to be performed to configuration override on configuration rename.
*
* @param \Drupal\Core\Config\ConfigRenameEvent $event
* The config rename event.
*/
abstract public function onConfigRename(ConfigRenameEvent $event);
/**
* {@inheritdoc}
*/
static function getSubscribedEvents() {
$events[ConfigEvents::COLLECTION_INFO][] = array('addCollections');
$events[ConfigEvents::SAVE][] = array('onConfigSave', 20);
$events[ConfigEvents::DELETE][] = array('onConfigDelete', 20);
$events[ConfigEvents::RENAME][] = array('onConfigRename', 20);
return $events;
}
/**
* Filters data in the override based on what is currently in configuration.
*
* @param \Drupal\Core\Config\Config $config
* Current configuration object.
* @param \Drupal\Core\Config\StorableConfigBase $override
* Override object corresponding to the configuration to filter data in.
*/
protected function filterOverride(Config $config, StorableConfigBase $override) {
$override_data = $override->get();
$changed = $this->filterNestedArray($config->get(), $override_data);
if (empty($override_data)) {
// If no override values are left that would apply, remove the override.
$override->delete();
}
elseif ($changed) {
// Otherwise set the filtered override values back.
$override->setData($override_data)->save();
}
}
/**
* Filters data in nested arrays.
*
* @param array $original_data
* Original data array to filter against.
* @param array $override_data
* Override data to filter.
*
* @return bool
* TRUE if $override_data was changed, FALSE otherwise.
*/
protected function filterNestedArray(array $original_data, array &$override_data) {
$changed = FALSE;
foreach ($override_data as $key => $value) {
if (!isset($original_data[$key])) {
// The original data is not there anymore, remove the override.
unset($override_data[$key]);
$changed = TRUE;
}
elseif (is_array($override_data[$key])) {
if (is_array($original_data[$key])) {
// Do the filtering one level deeper.
$changed = $this->filterNestedArray($original_data[$key], $override_data[$key]);
// If no overrides are left under this level, remove the level.
if (empty($override_data[$key])) {
unset($override_data[$key]);
$changed = TRUE;
}
}
else {
// The override is an array but the value is not, this will not go
// well, remove the override.
unset($override_data[$key]);
$changed = TRUE;
}
}
}
return $changed;
}
}

View file

@ -0,0 +1,61 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigFactoryOverrideInterface.
*/
namespace Drupal\Core\Config;
/**
* Defines the interface for a configuration factory override object.
*/
interface ConfigFactoryOverrideInterface {
/**
* Returns config overrides.
*
* @param array $names
* A list of configuration names that are being loaded.
*
* @return array
* An array keyed by configuration name of override data. Override data
* contains a nested array structure of overrides.
*/
public function loadOverrides($names);
/**
* The string to append to the configuration static cache name.
*
* @return string
* A string to append to the configuration static cache name.
*/
public function getCacheSuffix();
/**
* Creates a configuration object for use during install and synchronization.
*
* If the overrider stores its overrides in configuration collections then
* it can have its own implementation of
* \Drupal\Core\Config\StorableConfigBase. Configuration overriders can link
* themselves to a configuration collection by listening to the
* \Drupal\Core\Config\ConfigEvents::COLLECTION_INFO event and adding the
* collections they are responsible for. Doing this will allow installation
* and synchronization to use the overrider's implementation of
* StorableConfigBase.
*
* @see \Drupal\Core\Config\ConfigCollectionInfo
* @see \Drupal\Core\Config\ConfigImporter::importConfig()
* @see \Drupal\Core\Config\ConfigInstaller::createConfiguration()
*
* @param string $name
* The configuration object name.
* @param string $collection
* The configuration collection.
*
* @return \Drupal\Core\Config\StorableConfigBase
* The configuration object for the provided name and collection.
*/
public function createConfigObject($name, $collection = StorageInterface::DEFAULT_COLLECTION);
}

View file

@ -0,0 +1,35 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigImportValidateEventSubscriberBase.
*/
namespace Drupal\Core\Config;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Defines a base event listener implementation for config sync validation.
*/
abstract class ConfigImportValidateEventSubscriberBase implements EventSubscriberInterface {
use StringTranslationTrait;
/**
* Checks that the configuration synchronization is valid.
*
* @param ConfigImporterEvent $event
* The config import event.
*/
abstract public function onConfigImporterValidate(ConfigImporterEvent $event);
/**
* {@inheritdoc}
*/
static function getSubscribedEvents() {
$events[ConfigEvents::IMPORT_VALIDATE][] = array('onConfigImporterValidate', 20);
return $events;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,59 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigImporterEvent.
*/
namespace Drupal\Core\Config;
use Symfony\Component\EventDispatcher\Event;
class ConfigImporterEvent extends Event {
/**
* Configuration import object.
*
* @var \Drupal\Core\Config\ConfigImporter
*/
protected $configImporter;
/**
* Constructs ConfigImporterEvent.
*
* @param \Drupal\Core\Config\ConfigImporter $config_importer
* A config import object to notify listeners about.
*/
public function __construct(ConfigImporter $config_importer) {
$this->configImporter = $config_importer;
}
/**
* Gets the config import object.
*
* @return \Drupal\Core\Config\ConfigImporter
* The ConfigImporter object.
*/
public function getConfigImporter() {
return $this->configImporter;
}
/**
* Gets the list of changes that will be imported.
*
* @param string $op
* (optional) A change operation. Either delete, create or update. If
* supplied the returned list will be limited to this operation.
* @param string $collection
* (optional) The collection to get the changelist for. Defaults to the
* default collection.
*
* @return array
* An array of config changes that are yet to be imported.
*
* @see \Drupal\Core\Config\StorageComparerInterface::getChangelist()
*/
public function getChangelist($op = NULL, $collection = StorageInterface::DEFAULT_COLLECTION) {
return $this->configImporter->getStorageComparer()->getChangelist($op, $collection);
}
}

View file

@ -0,0 +1,13 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigImporterException.
*/
namespace Drupal\Core\Config;
/**
* Exception thrown when a config import fails.
*/
class ConfigImporterException extends ConfigException {}

View file

@ -0,0 +1,630 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigInstaller.
*/
namespace Drupal\Core\Config;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\Entity\ConfigDependencyManager;
use Drupal\Core\Config\Entity\ConfigEntityDependency;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Site\Settings;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class ConfigInstaller implements ConfigInstallerInterface {
/**
* The configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The active configuration storages, keyed by collection.
*
* @var \Drupal\Core\Config\StorageInterface[]
*/
protected $activeStorages;
/**
* The typed configuration manager.
*
* @var \Drupal\Core\Config\TypedConfigManagerInterface
*/
protected $typedConfig;
/**
* The configuration manager.
*
* @var \Drupal\Core\Config\ConfigManagerInterface
*/
protected $configManager;
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* The configuration storage that provides the default configuration.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $sourceStorage;
/**
* Is configuration being created as part of a configuration sync.
*
* @var bool
*/
protected $isSyncing = FALSE;
/**
* Constructs the configuration installer.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
* @param \Drupal\Core\Config\StorageInterface $active_storage
* The active configuration storage.
* @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config
* The typed configuration manager.
* @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
* The configuration manager.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
*/
public function __construct(ConfigFactoryInterface $config_factory, StorageInterface $active_storage, TypedConfigManagerInterface $typed_config, ConfigManagerInterface $config_manager, EventDispatcherInterface $event_dispatcher) {
$this->configFactory = $config_factory;
$this->activeStorages[$active_storage->getCollectionName()] = $active_storage;
$this->typedConfig = $typed_config;
$this->configManager = $config_manager;
$this->eventDispatcher = $event_dispatcher;
}
/**
* {@inheritdoc}
*/
public function installDefaultConfig($type, $name) {
$extension_path = $this->drupalGetPath($type, $name);
// Refresh the schema cache if the extension provides configuration schema
// or is a theme.
if (is_dir($extension_path . '/' . InstallStorage::CONFIG_SCHEMA_DIRECTORY) || $type == 'theme') {
$this->typedConfig->clearCachedDefinitions();
}
$default_install_path = $this->getDefaultConfigDirectory($type, $name);
if (is_dir($default_install_path)) {
if (!$this->isSyncing()) {
$storage = new FileStorage($default_install_path, StorageInterface::DEFAULT_COLLECTION);
$prefix = '';
}
else {
// The configuration importer sets the source storage on the config
// installer. The configuration importer handles all of the
// configuration entity imports. We only need to ensure that simple
// configuration is created when the extension is installed.
$storage = $this->getSourceStorage();
$prefix = $name . '.';
}
// Gets a profile storage to search for overrides if necessary.
$profile_storage = $this->getProfileStorage($name);
// Gather information about all the supported collections.
$collection_info = $this->configManager->getConfigCollectionInfo();
foreach ($collection_info->getCollectionNames() as $collection) {
$config_to_create = $this->getConfigToCreate($storage, $collection, $prefix, $profile_storage);
// If we're installing a profile ensure configuration that is overriding
// is excluded.
if ($name == $this->drupalGetProfile()) {
$existing_configuration = $this->getActiveStorages($collection)->listAll();
$config_to_create = array_diff_key($config_to_create, array_flip($existing_configuration));
}
if (!empty($config_to_create)) {
$this->createConfiguration($collection, $config_to_create);
}
}
}
// During a drupal installation optional configuration is installed at the
// end of the installation process.
// @see install_install_profile()
if (!$this->isSyncing() && !$this->drupalInstallationAttempted()) {
$optional_install_path = $extension_path . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY;
if (is_dir($optional_install_path)) {
// Install any optional config the module provides.
$storage = new FileStorage($optional_install_path, StorageInterface::DEFAULT_COLLECTION);
$this->installOptionalConfig($storage, '');
}
// Install any optional configuration entities whose dependencies can now
// be met. This searches all the installed modules config/optional
// directories.
$storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, FALSE);
$this->installOptionalConfig($storage, [$type => $name]);
}
// Reset all the static caches and list caches.
$this->configFactory->reset();
}
/**
* {@inheritdoc}
*/
public function installOptionalConfig(StorageInterface $storage = NULL, $dependency = []) {
$profile = $this->drupalGetProfile();
if (!$storage) {
// Search the install profile's optional configuration too.
$storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, TRUE);
// The extension install storage ensures that overrides are used.
$profile_storage = NULL;
}
elseif (isset($profile)) {
// Creates a profile storage to search for overrides.
$profile_install_path = $this->drupalGetPath('module', $profile) . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY;
$profile_storage = new FileStorage($profile_install_path, StorageInterface::DEFAULT_COLLECTION);
}
else {
// Profile has not been set yet. For example during the first steps of the
// installer or during unit tests.
$profile_storage = NULL;
}
$enabled_extensions = $this->getEnabledExtensions();
$existing_config = $this->getActiveStorages()->listAll();
$list = array_filter($storage->listAll(), function($config_name) use ($existing_config) {
// Only list configuration that:
// - does not already exist
// - is a configuration entity (this also excludes config that has an
// implicit dependency on modules that are not yet installed)
return !in_array($config_name, $existing_config) && $this->configManager->getEntityTypeIdByName($config_name);
});
$all_config = array_merge($existing_config, $list);
$config_to_create = $storage->readMultiple($list);
// Check to see if the corresponding override storage has any overrides.
if ($profile_storage) {
$config_to_create = $profile_storage->readMultiple($list) + $config_to_create;
}
foreach ($config_to_create as $config_name => $data) {
// Exclude configuration where its dependencies cannot be met.
if (!$this->validateDependencies($config_name, $data, $enabled_extensions, $all_config)) {
unset($config_to_create[$config_name]);
}
// Exclude configuration that does not have a matching dependency.
elseif (!empty($dependency)) {
// Create a light weight dependency object to check dependencies.
$config_entity = new ConfigEntityDependency($config_name, $data);
if (!$config_entity->hasDependency(key($dependency), reset($dependency))) {
unset($config_to_create[$config_name]);
}
}
}
if (!empty($config_to_create)) {
$this->createConfiguration(StorageInterface::DEFAULT_COLLECTION, $config_to_create, TRUE);
}
}
/**
* Gets configuration data from the provided storage to create.
*
* @param StorageInterface $storage
* The configuration storage to read configuration from.
* @param string $collection
* The configuration collection to use.
* @param string $prefix
* (optional) Limit to configuration starting with the provided string.
*
* @return array
* An array of configuration data read from the source storage keyed by the
* configuration object name.
*/
protected function getConfigToCreate(StorageInterface $storage, $collection, $prefix = '', StorageInterface $profile_storage = NULL) {
if ($storage->getCollectionName() != $collection) {
$storage = $storage->createCollection($collection);
}
$data = $storage->readMultiple($storage->listAll($prefix));
// Check to see if the corresponding override storage has any overrides.
if ($profile_storage) {
if ($profile_storage->getCollectionName() != $collection) {
$profile_storage = $profile_storage->createCollection($collection);
}
$data = $profile_storage->readMultiple(array_keys($data)) + $data;
}
return $data;
}
/**
* Creates configuration in a collection based on the provided list.
*
* @param string $collection
* The configuration collection.
* @param array $config_to_create
* An array of configuration data to create, keyed by name.
*/
protected function createConfiguration($collection, array $config_to_create) {
// Order the configuration to install in the order of dependencies.
if ($collection == StorageInterface::DEFAULT_COLLECTION) {
$dependency_manager = new ConfigDependencyManager();
$config_names = $dependency_manager
->setData($config_to_create)
->sortAll();
}
else {
$config_names = array_keys($config_to_create);
}
foreach ($config_names as $name) {
// Allow config factory overriders to use a custom configuration object if
// they are responsible for the collection.
$overrider = $this->configManager->getConfigCollectionInfo()->getOverrideService($collection);
if ($overrider) {
$new_config = $overrider->createConfigObject($name, $collection);
}
else {
$new_config = new Config($name, $this->getActiveStorages($collection), $this->eventDispatcher, $this->typedConfig);
}
if ($config_to_create[$name] !== FALSE) {
$new_config->setData($config_to_create[$name]);
}
if ($collection == StorageInterface::DEFAULT_COLLECTION && $entity_type = $this->configManager->getEntityTypeIdByName($name)) {
// If we are syncing do not create configuration entities. Pluggable
// configuration entities can have dependencies on modules that are
// not yet enabled. This approach means that any code that expects
// default configuration entities to exist will be unstable after the
// module has been enabled and before the config entity has been
// imported.
if ($this->isSyncing()) {
continue;
}
/** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $entity_storage */
$entity_storage = $this->configManager
->getEntityManager()
->getStorage($entity_type);
// It is possible that secondary writes can occur during configuration
// creation. Updates of such configuration are allowed.
if ($this->getActiveStorages($collection)->exists($name)) {
$id = $entity_storage->getIDFromConfigName($name, $entity_storage->getEntityType()->getConfigPrefix());
$entity = $entity_storage->load($id);
$entity = $entity_storage->updateFromStorageRecord($entity, $new_config->get());
}
else {
$entity = $entity_storage->createFromStorageRecord($new_config->get());
}
if ($entity->isInstallable()) {
$entity->trustData()->save();
}
}
else {
$new_config->save(TRUE);
}
}
}
/**
* {@inheritdoc}
*/
public function installCollectionDefaultConfig($collection) {
$storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_INSTALL_DIRECTORY, $collection, $this->drupalInstallationAttempted());
// Only install configuration for enabled extensions.
$enabled_extensions = $this->getEnabledExtensions();
$config_to_install = array_filter($storage->listAll(), function ($config_name) use ($enabled_extensions) {
$provider = Unicode::substr($config_name, 0, strpos($config_name, '.'));
return in_array($provider, $enabled_extensions);
});
if (!empty($config_to_install)) {
$this->createConfiguration($collection, $storage->readMultiple($config_to_install));
// Reset all the static caches and list caches.
$this->configFactory->reset();
}
}
/**
* {@inheritdoc}
*/
public function setSourceStorage(StorageInterface $storage) {
$this->sourceStorage = $storage;
return $this;
}
/**
* Gets the configuration storage that provides the default configuration.
*
* @return \Drupal\Core\Config\StorageInterface|null
* The configuration storage that provides the default configuration.
* Returns null if the source storage has not been set.
*/
public function getSourceStorage() {
return $this->sourceStorage;
}
/**
* Gets the configuration storage that provides the active configuration.
*
* @param string $collection
* (optional) The configuration collection. Defaults to the default
* collection.
*
* @return \Drupal\Core\Config\StorageInterface
* The configuration storage that provides the default configuration.
*/
protected function getActiveStorages($collection = StorageInterface::DEFAULT_COLLECTION) {
if (!isset($this->activeStorages[$collection])) {
$this->activeStorages[$collection] = reset($this->activeStorages)->createCollection($collection);
}
return $this->activeStorages[$collection];
}
/**
* {@inheritdoc}
*/
public function setSyncing($status) {
if (!$status) {
$this->sourceStorage = NULL;
}
$this->isSyncing = $status;
return $this;
}
/**
* {@inheritdoc}
*/
public function isSyncing() {
return $this->isSyncing;
}
/**
* Finds pre-existing configuration objects for the provided extension.
*
* Extensions can not be installed if configuration objects exist in the
* active storage with the same names. This can happen in a number of ways,
* commonly:
* - if a user has created configuration with the same name as that provided
* by the extension.
* - if the extension provides default configuration that does not depend on
* it and the extension has been uninstalled and is about to the
* reinstalled.
*
* @return array
* Array of configuration object names that already exist keyed by
* collection.
*/
protected function findPreExistingConfiguration(StorageInterface $storage) {
$existing_configuration = array();
// Gather information about all the supported collections.
$collection_info = $this->configManager->getConfigCollectionInfo();
foreach ($collection_info->getCollectionNames() as $collection) {
$config_to_create = array_keys($this->getConfigToCreate($storage, $collection));
$active_storage = $this->getActiveStorages($collection);
foreach ($config_to_create as $config_name) {
if ($active_storage->exists($config_name)) {
$existing_configuration[$collection][] = $config_name;
}
}
}
return $existing_configuration;
}
/**
* {@inheritdoc}
*/
public function checkConfigurationToInstall($type, $name) {
if ($this->isSyncing()) {
// Configuration is assumed to already be checked by the config importer
// validation events.
return;
}
$config_install_path = $this->getDefaultConfigDirectory($type, $name);
if (!is_dir($config_install_path)) {
return;
}
$storage = new FileStorage($config_install_path, StorageInterface::DEFAULT_COLLECTION);
$enabled_extensions = $this->getEnabledExtensions();
// Add the extension that will be enabled to the list of enabled extensions.
$enabled_extensions[] = $name;
// Gets a profile storage to search for overrides if necessary.
$profile_storage = $this->getProfileStorage($name);
// Check the dependencies of configuration provided by the module.
$invalid_default_config = $this->findDefaultConfigWithUnmetDependencies($storage, $enabled_extensions, $profile_storage);
if (!empty($invalid_default_config)) {
throw UnmetDependenciesException::create($name, $invalid_default_config);
}
// Install profiles can not have config clashes. Configuration that
// has the same name as a module's configuration will be used instead.
if ($name != $this->drupalGetProfile()) {
// Throw an exception if the module being installed contains configuration
// that already exists. Additionally, can not continue installing more
// modules because those may depend on the current module being installed.
$existing_configuration = $this->findPreExistingConfiguration($storage);
if (!empty($existing_configuration)) {
throw PreExistingConfigException::create($name, $existing_configuration);
}
}
}
/**
* Finds default configuration with unmet dependencies.
*
* @param array $enabled_extensions
* A list of all the currently enabled modules and themes.
*
* @return array
* List of configuration that has unmet dependencies
*/
protected function findDefaultConfigWithUnmetDependencies(StorageInterface $storage, array $enabled_extensions, StorageInterface $profile_storage = NULL) {
$config_to_create = $this->getConfigToCreate($storage, StorageInterface::DEFAULT_COLLECTION, '', $profile_storage);
$all_config = array_merge($this->configFactory->listAll(), array_keys($config_to_create));
return array_filter(array_keys($config_to_create), function($config_name) use ($enabled_extensions, $all_config, $config_to_create) {
return !$this->validateDependencies($config_name, $config_to_create[$config_name], $enabled_extensions, $all_config);
});
}
/**
* Validates an array of config data that contains dependency information.
*
* @param string $config_name
* The name of the configuration object that is being validated.
* @param array $data
* Configuration data.
* @param array $enabled_extensions
* A list of all the currently enabled modules and themes.
* @param array $all_config
* A list of all the active configuration names.
*
* @return bool
* TRUE if the dependencies are met, FALSE if not.
*/
protected function validateDependencies($config_name, array $data, array $enabled_extensions, array $all_config) {
if (isset($data['dependencies'])) {
$all_dependencies = $data['dependencies'];
// Ensure enforced dependencies are included.
if (isset($all_dependencies['enforced'])) {
$all_dependencies = array_merge($all_dependencies, $data['dependencies']['enforced']);
unset($all_dependencies['enforced']);
}
// Ensure the configuration entity type provider is in the list of
// dependencies.
list($provider) = explode('.', $config_name, 2);
if (!isset($all_dependencies['module'])) {
$all_dependencies['module'][] = $provider;
}
elseif (!in_array($provider, $all_dependencies['module'])) {
$all_dependencies['module'][] = $provider;
}
foreach ($all_dependencies as $type => $dependencies) {
$list_to_check = [];
switch ($type) {
case 'module':
case 'theme':
$list_to_check = $enabled_extensions;
break;
case 'config':
$list_to_check = $all_config;
break;
}
if (!empty($list_to_check)) {
$missing = array_diff($dependencies, $list_to_check);
if (!empty($missing)) {
return FALSE;
}
}
}
}
return TRUE;
}
/**
* Gets the list of enabled extensions including both modules and themes.
*
* @return array
* A list of enabled extensions which includes both modules and themes.
*/
protected function getEnabledExtensions() {
// Read enabled extensions directly from configuration to avoid circular
// dependencies on ModuleHandler and ThemeHandler.
$extension_config = $this->configFactory->get('core.extension');
$enabled_extensions = (array) $extension_config->get('module');
$enabled_extensions += (array) $extension_config->get('theme');
// Core can provide configuration.
$enabled_extensions['core'] = 'core';
return array_keys($enabled_extensions);
}
/**
* Gets the profile storage to use to check for profile overrides.
*
* @param string $installing_name
* (optional) The name of the extension currently being installed.
*
* @return \Drupal\Core\Config\StorageInterface|null
* A storage to access configuration from the installation profile. If a
* Drupal installation is not in progress or we're installing the profile
* itself, then it will return NULL as the profile storage should not be
* used.
*/
protected function getProfileStorage($installing_name = '') {
$profile = $this->drupalGetProfile();
if ($this->drupalInstallationAttempted() && $profile != $installing_name) {
// Profiles should not contain optional configuration so always use the
// install directory.
$profile_install_path = $this->getDefaultConfigDirectory('module', $profile);
$profile_storage = new FileStorage($profile_install_path, StorageInterface::DEFAULT_COLLECTION);
}
else {
$profile_storage = NULL;
}
return $profile_storage;
}
/**
* Gets an extension's default configuration directory.
*
* @param string $type
* Type of extension to install.
* @param string $name
* Name of extension to install.
*
* @return string
* The extension's default configuration directory.
*/
protected function getDefaultConfigDirectory($type, $name) {
return $this->drupalGetPath($type, $name) . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY;
}
/**
* Wrapper for drupal_get_path().
*
* @param $type
* The type of the item; one of 'core', 'profile', 'module', 'theme', or
* 'theme_engine'.
* @param $name
* The name of the item for which the path is requested. Ignored for
* $type 'core'.
*
* @return string
* The path to the requested item or an empty string if the item is not
* found.
*/
protected function drupalGetPath($type, $name) {
return drupal_get_path($type, $name);
}
/**
* Gets the install profile from settings.
*
* @return string|null $profile
* The name of the installation profile or NULL if no installation profile
* is currently active. This is the case for example during the first steps
* of the installer or during unit tests.
*/
protected function drupalGetProfile() {
// Settings is safe to use because settings.php is written before any module
// is installed.
return Settings::get('install_profile');
}
/**
* Wrapper for drupal_installation_attempted().
*
* @return bool
* TRUE if a Drupal installation is currently being attempted.
*/
protected function drupalInstallationAttempted() {
return drupal_installation_attempted();
}
}

View file

@ -0,0 +1,113 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigInstallerInterface.
*/
namespace Drupal\Core\Config;
/**
* Interface for classes that install config.
*/
interface ConfigInstallerInterface {
/**
* Installs the default configuration of a given extension.
*
* When an extension is installed, it searches all the default configuration
* directories for all other extensions to locate any configuration with its
* name prefix. For example, the Node module provides the frontpage view as a
* default configuration file:
* core/modules/node/config/install/views.view.frontpage.yml
* When the Views module is installed after the Node module is already
* enabled, the frontpage view will be installed.
*
* Additionally, the default configuration directory for the extension being
* installed is searched to discover if it contains default configuration
* that is owned by other enabled extensions. So, the frontpage view will also
* be installed when the Node module is installed after Views.
*
* @param string $type
* The extension type; e.g., 'module' or 'theme'.
* @param string $name
* The name of the module or theme to install default configuration for.
*
* @see \Drupal\Core\Config\ExtensionInstallStorage
*/
public function installDefaultConfig($type, $name);
/**
* Installs optional configuration.
*
* Optional configuration is only installed if:
* - the configuration does not exist already.
* - it's a configuration entity.
* - its dependencies can be met.
*
* @param \Drupal\Core\Config\StorageInterface
* (optional) The configuration storage to search for optional
* configuration. If not provided, all enabled extension's optional
* configuration directories will be searched.
* @param array $dependency
* (optional) If set, ensures that the configuration being installed has
* this dependency. The format is dependency type as the key ('module',
* 'theme', or 'config') and the dependency name as the value
* ('book', 'bartik', 'views.view.frontpage').
*/
public function installOptionalConfig(StorageInterface $storage = NULL, $dependency = []);
/**
* Installs all default configuration in the specified collection.
*
* The function is useful if the site needs to respond to an event that has
* just created another collection and we need to check all the installed
* extensions for any matching configuration. For example, if a language has
* just been created.
*
* @param string $collection
* The configuration collection.
*/
public function installCollectionDefaultConfig($collection);
/**
* Sets the configuration storage that provides the default configuration.
*
* @param \Drupal\Core\Config\StorageInterface $storage
*
* @return $this
*/
public function setSourceStorage(StorageInterface $storage);
/**
* Sets the status of the isSyncing flag.
*
* @param bool $status
* The status of the sync flag.
*
* @return $this
*/
public function setSyncing($status);
/**
* Gets the syncing state.
*
* @return bool
* Returns TRUE is syncing flag set.
*/
public function isSyncing();
/**
* Checks the configuration that will be installed for an extension.
*
* @param string $type
* Type of extension to install.
* @param string $name
* Name of extension to install.
*
* @throws \Drupal\Core\Config\UnmetDependenciesException
* @throws \Drupal\Core\Config\PreExistingConfigException
*/
public function checkConfigurationToInstall($type, $name);
}

View file

@ -0,0 +1,472 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigManager.
*/
namespace Drupal\Core\Config;
use Drupal\Component\Diff\Diff;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Config\Entity\ConfigDependencyManager;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* The ConfigManager provides helper functions for the configuration system.
*/
class ConfigManager implements ConfigManagerInterface {
use StringTranslationTrait;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The typed config manager.
*
* @var \Drupal\Core\Config\TypedConfigManagerInterface
*/
protected $typedConfigManager;
/**
* The active configuration storage.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $activeStorage;
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* The configuration collection info.
*
* @var \Drupal\Core\Config\ConfigCollectionInfo
*/
protected $configCollectionInfo;
/**
* The configuration storages keyed by collection name.
*
* @var \Drupal\Core\Config\StorageInterface[]
*/
protected $storages;
/**
* Creates ConfigManager objects.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
* @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager
* The typed config manager.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
* @param \Drupal\Core\Config\StorageInterface $active_storage
* The active configuration storage.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
*/
public function __construct(EntityManagerInterface $entity_manager, ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typed_config_manager, TranslationInterface $string_translation, StorageInterface $active_storage, EventDispatcherInterface $event_dispatcher) {
$this->entityManager = $entity_manager;
$this->configFactory = $config_factory;
$this->typedConfigManager = $typed_config_manager;
$this->stringTranslation = $string_translation;
$this->activeStorage = $active_storage;
$this->eventDispatcher = $event_dispatcher;
}
/**
* {@inheritdoc}
*/
public function getEntityTypeIdByName($name) {
$entities = array_filter($this->entityManager->getDefinitions(), function (EntityTypeInterface $entity_type) use ($name) {
return ($entity_type instanceof ConfigEntityTypeInterface && $config_prefix = $entity_type->getConfigPrefix()) && strpos($name, $config_prefix . '.') === 0;
});
return key($entities);
}
/**
* {@inheritdoc}
*/
public function loadConfigEntityByName($name) {
$entity_type_id = $this->getEntityTypeIdByName($name);
if ($entity_type_id) {
$entity_type = $this->entityManager->getDefinition($entity_type_id);
$id = substr($name, strlen($entity_type->getConfigPrefix()) + 1);
return $this->entityManager->getStorage($entity_type_id)->load($id);
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function getEntityManager() {
return $this->entityManager;
}
/**
* {@inheritdoc}
*/
public function getConfigFactory() {
return $this->configFactory;
}
/**
* {@inheritdoc}
*/
public function diff(StorageInterface $source_storage, StorageInterface $target_storage, $source_name, $target_name = NULL, $collection = StorageInterface::DEFAULT_COLLECTION) {
if ($collection != StorageInterface::DEFAULT_COLLECTION) {
$source_storage = $source_storage->createCollection($collection);
$target_storage = $target_storage->createCollection($collection);
}
if (!isset($target_name)) {
$target_name = $source_name;
}
// The output should show configuration object differences formatted as YAML.
// But the configuration is not necessarily stored in files. Therefore, they
// need to be read and parsed, and lastly, dumped into YAML strings.
$source_data = explode("\n", Yaml::encode($source_storage->read($source_name)));
$target_data = explode("\n", Yaml::encode($target_storage->read($target_name)));
// Check for new or removed files.
if ($source_data === array('false')) {
// Added file.
$source_data = array($this->t('File added'));
}
if ($target_data === array('false')) {
// Deleted file.
$target_data = array($this->t('File removed'));
}
return new Diff($source_data, $target_data);
}
/**
* {@inheritdoc}
*/
public function createSnapshot(StorageInterface $source_storage, StorageInterface $snapshot_storage) {
// Empty the snapshot of all configuration.
$snapshot_storage->deleteAll();
foreach ($snapshot_storage->getAllCollectionNames() as $collection) {
$snapshot_collection = $snapshot_storage->createCollection($collection);
$snapshot_collection->deleteAll();
}
foreach ($source_storage->listAll() as $name) {
$snapshot_storage->write($name, $source_storage->read($name));
}
// Copy collections as well.
foreach ($source_storage->getAllCollectionNames() as $collection) {
$source_collection = $source_storage->createCollection($collection);
$snapshot_collection = $snapshot_storage->createCollection($collection);
foreach ($source_collection->listAll() as $name) {
$snapshot_collection->write($name, $source_collection->read($name));
}
}
}
/**
* {@inheritdoc}
*/
public function uninstall($type, $name) {
$entities = $this->getConfigEntitiesToChangeOnDependencyRemoval($type, [$name], FALSE);
// Fix all dependent configuration entities.
/** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */
foreach ($entities['update'] as $entity) {
$entity->save();
}
// Remove all dependent configuration entities.
foreach ($entities['delete'] as $entity) {
$entity->setUninstalling(TRUE);
$entity->delete();
}
$config_names = $this->configFactory->listAll($name . '.');
foreach ($config_names as $config_name) {
$this->configFactory->getEditable($config_name)->delete();
}
// Remove any matching configuration from collections.
foreach ($this->activeStorage->getAllCollectionNames() as $collection) {
$collection_storage = $this->activeStorage->createCollection($collection);
$collection_storage->deleteAll($name . '.');
}
$schema_dir = drupal_get_path($type, $name) . '/' . InstallStorage::CONFIG_SCHEMA_DIRECTORY;
if (is_dir($schema_dir)) {
// Refresh the schema cache if uninstalling an extension that provides
// configuration schema.
$this->typedConfigManager->clearCachedDefinitions();
}
}
/**
* {@inheritdoc}
*/
public function getConfigDependencyManager() {
$dependency_manager = new ConfigDependencyManager();
// Read all configuration using the factory. This ensures that multiple
// deletes during the same request benefit from the static cache. Using the
// factory also ensures configuration entity dependency discovery has no
// dependencies on the config entity classes. Assume data with UUID is a
// config entity. Only configuration entities can be depended on so we can
// ignore everything else.
$data = array_map(function($config) {
$data = $config->get();
if (isset($data['uuid'])) {
return $data;
}
return FALSE;
}, $this->configFactory->loadMultiple($this->activeStorage->listAll()));
$dependency_manager->setData(array_filter($data));
return $dependency_manager;
}
/**
* {@inheritdoc}
*/
public function findConfigEntityDependents($type, array $names, ConfigDependencyManager $dependency_manager = NULL) {
if (!$dependency_manager) {
$dependency_manager = $this->getConfigDependencyManager();
}
$dependencies = array();
foreach ($names as $name) {
$dependencies = array_merge($dependencies, $dependency_manager->getDependentEntities($type, $name));
}
return $dependencies;
}
/**
* {@inheritdoc}
*/
public function findConfigEntityDependentsAsEntities($type, array $names, ConfigDependencyManager $dependency_manager = NULL) {
$dependencies = $this->findConfigEntityDependents($type, $names, $dependency_manager);
$entities = array();
$definitions = $this->entityManager->getDefinitions();
foreach ($dependencies as $config_name => $dependency) {
// Group by entity type to efficient load entities using
// \Drupal\Core\Entity\EntityStorageInterface::loadMultiple().
$entity_type_id = $this->getEntityTypeIdByName($config_name);
// It is possible that a non-configuration entity will be returned if a
// simple configuration object has a UUID key. This would occur if the
// dependents of the system module are calculated since system.site has
// a UUID key.
if ($entity_type_id) {
$id = substr($config_name, strlen($definitions[$entity_type_id]->getConfigPrefix()) + 1);
$entities[$entity_type_id][] = $id;
}
}
$entities_to_return = array();
foreach ($entities as $entity_type_id => $entities_to_load) {
$storage = $this->entityManager->getStorage($entity_type_id);
// Remove the keys since there are potential ID clashes from different
// configuration entity types.
$entities_to_return = array_merge($entities_to_return, array_values($storage->loadMultiple($entities_to_load)));
}
return $entities_to_return;
}
/**
* {@inheritdoc}
*/
public function getConfigEntitiesToChangeOnDependencyRemoval($type, array $names, $dry_run = TRUE) {
// Determine the current list of dependent configuration entities and set up
// initial values.
$dependency_manager = $this->getConfigDependencyManager();
$dependents = $this->findConfigEntityDependentsAsEntities($type, $names, $dependency_manager);
$original_dependencies = $dependents;
$update_uuids = [];
$return = [
'update' => [],
'delete' => [],
'unchanged' => [],
];
// Try to fix any dependencies and find out what will happen to the
// dependency graph.
foreach ($dependents as $dependent) {
/** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $dependent */
if ($dry_run) {
// Clone the entity so any changes do not change any static caches.
$dependent = clone $dependent;
}
if ($this->callOnDependencyRemoval($dependent, $original_dependencies, $type, $names)) {
// Recalculate dependencies and update the dependency graph data.
$dependency_manager->updateData($dependent->getConfigDependencyName(), $dependent->calculateDependencies());
// Based on the updated data rebuild the list of dependents.
$dependents = $this->findConfigEntityDependentsAsEntities($type, $names, $dependency_manager);
// Ensure that the dependency has actually been fixed. It is possible
// that the dependent has multiple dependencies that cause it to be in
// the dependency chain.
$fixed = TRUE;
foreach ($dependents as $entity) {
if ($entity->uuid() == $dependent->uuid()) {
$fixed = FALSE;
break;
}
}
if ($fixed) {
$return['update'][] = $dependent;
$update_uuids[] = $dependent->uuid();
}
}
}
// Now that we've fixed all the possible dependencies the remaining need to
// be deleted. Reverse the deletes so that entities are removed in the
// correct order of dependence. For example, this ensures that fields are
// removed before field storages.
$return['delete'] = array_reverse($dependents);
$delete_uuids = array_map(function($dependent) {
return $dependent->uuid();
}, $return['delete']);
// Use the lists of UUIDs to filter the original list to work out which
// configuration entities are unchanged.
$return['unchanged'] = array_filter($original_dependencies, function ($dependent) use ($delete_uuids, $update_uuids) {
return !(in_array($dependent->uuid(), $delete_uuids) || in_array($dependent->uuid(), $update_uuids));
});
return $return;
}
/**
* {@inheritdoc}
*/
public function getConfigCollectionInfo() {
if (!isset($this->configCollectionInfo)) {
$this->configCollectionInfo = new ConfigCollectionInfo();
$this->eventDispatcher->dispatch(ConfigEvents::COLLECTION_INFO, $this->configCollectionInfo);
}
return $this->configCollectionInfo;
}
/**
* Calls an entity's onDependencyRemoval() method.
*
* A helper method to call onDependencyRemoval() with the correct list of
* affected entities. This list should only contain dependencies on the
* entity. Configuration and content entity dependencies will be converted
* into entity objects.
*
* @param \Drupal\Core\Config\Entity\ConfigEntityInterface $entity
* The entity to call onDependencyRemoval() on.
* @param \Drupal\Core\Config\Entity\ConfigEntityInterface[] $dependent_entities
* The list of dependent configuration entities.
* @param string $type
* The type of dependency being checked. Either 'module', 'theme', 'config'
* or 'content'.
* @param array $names
* The specific names to check. If $type equals 'module' or 'theme' then it
* should be a list of module names or theme names. In the case of 'config'
* or 'content' it should be a list of configuration dependency names.
*
* @return bool
* TRUE if the entity has changed as a result of calling the
* onDependencyRemoval() method, FALSE if not.
*/
protected function callOnDependencyRemoval(ConfigEntityInterface $entity, array $dependent_entities, $type, array $names) {
$entity_dependencies = $entity->getDependencies();
if (empty($entity_dependencies)) {
// No dependent entities nothing to do.
return FALSE;
}
$affected_dependencies = array(
'config' => array(),
'content' => array(),
'module' => array(),
'theme' => array(),
);
// Work out if any of the entity's dependencies are going to be affected.
if (isset($entity_dependencies[$type])) {
// Work out which dependencies the entity has in common with the provided
// $type and $names.
$affected_dependencies[$type] = array_intersect($entity_dependencies[$type], $names);
// If the dependencies are entities we need to convert them into objects.
if ($type == 'config' || $type == 'content') {
$affected_dependencies[$type] = array_map(function ($name) use ($type) {
if ($type == 'config') {
return $this->loadConfigEntityByName($name);
}
else {
// Ignore the bundle.
list($entity_type_id,, $uuid) = explode(':', $name);
return $this->entityManager->loadEntityByConfigTarget($entity_type_id, $uuid);
}
}, $affected_dependencies[$type]);
}
}
// Merge any other configuration entities into the list of affected
// dependencies if necessary.
if (isset($entity_dependencies['config'])) {
foreach ($dependent_entities as $dependent_entity) {
if (in_array($dependent_entity->getConfigDependencyName(), $entity_dependencies['config'])) {
$affected_dependencies['config'][] = $dependent_entity;
}
}
}
// Key the entity arrays by config dependency name to make searching easy.
foreach (['config', 'content'] as $dependency_type) {
$affected_dependencies[$dependency_type] = array_combine(
array_map(function ($entity) { return $entity->getConfigDependencyName(); }, $affected_dependencies[$dependency_type]),
$affected_dependencies[$dependency_type]
);
}
// Inform the entity.
return $entity->onDependencyRemoval($affected_dependencies);
}
/**
* {@inheritdoc}
*/
public function findMissingContentDependencies() {
$content_dependencies = array();
$missing_dependencies = array();
foreach ($this->activeStorage->readMultiple($this->activeStorage->listAll()) as $config_data) {
if (isset($config_data['dependencies']['content'])) {
$content_dependencies = array_merge($content_dependencies, $config_data['dependencies']['content']);
}
}
foreach (array_unique($content_dependencies) as $content_dependency) {
// Format of the dependency is entity_type:bundle:uuid.
list($entity_type, $bundle, $uuid) = explode(':', $content_dependency, 3);
if (!$this->entityManager->loadEntityByUuid($entity_type, $uuid)) {
$missing_dependencies[$uuid] = array(
'entity_type' => $entity_type,
'bundle' => $bundle,
'uuid' => $uuid,
);
}
}
return $missing_dependencies;
}
}

View file

@ -0,0 +1,182 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigManagerInterface.
*/
namespace Drupal\Core\Config;
/**
* Provides an interface for configuration manager.
*/
interface ConfigManagerInterface {
/**
* Returns the entity type of a configuration object.
*
* @param string $name
* The configuration object name.
*
* @return string|null
* Either the entity type name, or NULL if none match.
*/
public function getEntityTypeIdByName($name);
/**
* Loads a configuration entity using the configuration name.
*
* @param string $name
* The configuration object name.
*
* @return \Drupal\Core\Entity\EntityInterface|null
* The configuration entity or NULL if it does not exist.
*/
public function loadConfigEntityByName($name);
/**
* Gets the entity manager.
*
* @return \Drupal\Core\Entity\EntityManagerInterface
* The entity manager.
*/
public function getEntityManager();
/**
* Gets the config factory.
*
* @return \Drupal\Core\Config\ConfigFactoryInterface
* The entity manager.
*/
public function getConfigFactory();
/**
* Creates a Diff object using the config data from the two storages.
*
* @param \Drupal\Core\Config\StorageInterface $source_storage
* The storage to diff configuration from.
* @param \Drupal\Core\Config\StorageInterface $target_storage
* The storage to diff configuration to.
* @param string $source_name
* The name of the configuration object in the source storage to diff.
* @param string $target_name
* (optional) The name of the configuration object in the target storage.
* If omitted, the source name is used.
* @param string $collection
* (optional) The configuration collection name. Defaults to the default
* collection.
*
* @return \Drupal\Component\Diff\Diff
* A Diff object using the config data from the two storages.
*
* @todo Make renderer injectable
*
* @see \Drupal\Core\Diff\DiffFormatter
*/
public function diff(StorageInterface $source_storage, StorageInterface $target_storage, $source_name, $target_name = NULL, $collection = StorageInterface::DEFAULT_COLLECTION);
/**
* Creates a configuration snapshot following a successful import.
*
* @param \Drupal\Core\Config\StorageInterface $source_storage
* The storage to synchronize configuration from.
* @param \Drupal\Core\Config\StorageInterface $snapshot_storage
* The storage to synchronize configuration to.
*/
public function createSnapshot(StorageInterface $source_storage, StorageInterface $snapshot_storage);
/**
* Uninstalls the configuration of a given extension.
*
* @param string $type
* The extension type; e.g., 'module' or 'theme'.
* @param string $name
* The name of the module or theme to install configuration for.
*/
public function uninstall($type, $name);
/**
* Creates and populates a ConfigDependencyManager object.
*
* The configuration dependency manager is populated with data from the active
* store.
*
* @return \Drupal\Core\Config\Entity\ConfigDependencyManager
*/
public function getConfigDependencyManager();
/**
* Finds config entities that are dependent on extensions or entities.
*
* @param string $type
* The type of dependency being checked. Either 'module', 'theme', 'config'
* or 'content'.
* @param array $names
* The specific names to check. If $type equals 'module' or 'theme' then it
* should be a list of module names or theme names. In the case of 'config'
* or 'content' it should be a list of configuration dependency names.
*
* @return \Drupal\Core\Config\Entity\ConfigEntityDependency[]
* An array of configuration entity dependency objects.
*/
public function findConfigEntityDependents($type, array $names);
/**
* Finds config entities that are dependent on extensions or entities.
*
* @param string $type
* The type of dependency being checked. Either 'module', 'theme', 'config'
* or 'content'.
* @param array $names
* The specific names to check. If $type equals 'module' or 'theme' then it
* should be a list of module names or theme names. In the case of 'config'
* or 'content' it should be a list of configuration dependency names.
*
* @return \Drupal\Core\Config\Entity\ConfigEntityInterface[]
* An array of dependencies as configuration entities.
*/
public function findConfigEntityDependentsAsEntities($type, array $names);
/**
* Lists which config entities to update and delete on removal of a dependency.
*
* @param string $type
* The type of dependency being checked. Either 'module', 'theme', 'config'
* or 'content'.
* @param array $names
* The specific names to check. If $type equals 'module' or 'theme' then it
* should be a list of module names or theme names. In the case of 'config'
* or 'content' it should be a list of configuration dependency names.
* @param bool $dry_run
* If set to FALSE the entities returned in the list of updates will be
* modified. In order to make the changes the caller needs to save them. If
* set to TRUE the entities returned will not be modified.
*
* @return array
* An array with the keys: 'update', 'delete' and 'unchanged'. The value of
* each is a list of configuration entities that need to have that action
* applied when the supplied dependencies are removed. Updates need to be
* processed before deletes. The order of the deletes is significant and
* must be processed in the returned order.
*/
public function getConfigEntitiesToChangeOnDependencyRemoval($type, array $names, $dry_run = TRUE);
/**
* Gets available collection information using the event system.
*
* @return \Drupal\Core\Config\ConfigCollectionInfo
* The object which contains information about the available collections.
*/
public function getConfigCollectionInfo();
/**
* Finds missing content dependencies declared in configuration entities.
*
* @return array
* A list of missing content dependencies. The array is keyed by UUID. Each
* value is an array with the following keys: 'entity_type', 'bundle' and
* 'uuid'.
*/
public function findMissingContentDependencies();
}

View file

@ -0,0 +1,108 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigModuleOverridesEvent.
*/
namespace Drupal\Core\Config;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Language\LanguageInterface;
use Symfony\Component\EventDispatcher\Event;
/**
* Event object to allow configuration to be overridden by modules.
*/
class ConfigModuleOverridesEvent extends Event {
/**
* Configuration names.
*
* @var array
*/
protected $names;
/**
* Configuration overrides.
*
* @var array
*/
protected $overrides;
/**
* The Language object used to override configuration data.
*
* @var \Drupal\Core\Language\LanguageInterface
*/
protected $language;
/**
* Constructs a configuration overrides event object.
*
* @param array $names
* A list of configuration names.
* @param \Drupal\Core\Language\LanguageInterface
* (optional) The language for this configuration.
*/
public function __construct(array $names, LanguageInterface $language = NULL) {
$this->names = $names;
$this->language = $language;
$this->overrides = array();
}
/**
* Gets configuration names.
*
* @return array
* The list of configuration names that can be overridden.
*/
public function getNames() {
return $this->names;
}
/**
* Gets configuration language.
*
* @return \Drupal\Core\Language\LanguageInterface
* The configuration language object.
*/
public function getLanguage() {
return $this->language;
}
/**
* Get configuration overrides.
*
* @return array.
* The array of configuration overrides.
*/
public function getOverrides() {
return $this->overrides;
}
/**
* Sets a configuration override for the given name.
*
* @param string $name
* The configuration object name to override.
* @param array $values
* The values in the configuration object to override.
*
* @return $this
*/
public function setOverride($name, array $values) {
if (in_array($name, $this->names)) {
if (isset($this->overrides[$name])) {
// Existing overrides take precedence since these will have been added
// by events with a higher priority.
$this->overrides[$name] = NestedArray::mergeDeepArray(array($values, $this->overrides[$name]), TRUE);
}
else {
$this->overrides[$name] = $values;
}
}
return $this;
}
}

View file

@ -0,0 +1,13 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigNameException.
*/
namespace Drupal\Core\Config;
/**
* Exception thrown when a config object name is invalid.
*/
class ConfigNameException extends ConfigException {}

View file

@ -0,0 +1,13 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigPrefixLengthException.
*/
namespace Drupal\Core\Config;
/**
* Exception thrown when the config prefix length is exceeded.
*/
class ConfigPrefixLengthException extends ConfigException {}

View file

@ -0,0 +1,45 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigRenameEvent.
*/
namespace Drupal\Core\Config;
/**
* Configuration event fired when renaming a configuration object.
*/
class ConfigRenameEvent extends ConfigCrudEvent {
/**
* The old configuration object name.
*
* @var string
*/
protected $oldName;
/**
* Constructs the config rename event.
*
* @param \Drupal\Core\Config\Config $config
* The configuration that has been renamed.
* @param string $old_name
* The old configuration object name.
*/
public function __construct(Config $config, $old_name) {
$this->config = $config;
$this->oldName = $old_name;
}
/**
* Gets the old configuration object name.
*
* @return string
* The old configuration object name.
*/
public function getOldName() {
return $this->oldName;
}
}

View file

@ -0,0 +1,13 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigValueException.
*/
namespace Drupal\Core\Config;
/**
* Exception thrown when config object values are invalid.
*/
class ConfigValueException extends ConfigException {}

View file

@ -0,0 +1,334 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\DatabaseStorage.
*/
namespace Drupal\Core\Config;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\SchemaObjectExistsException;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
/**
* Defines the Database storage.
*/
class DatabaseStorage implements StorageInterface {
use DependencySerializationTrait;
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* The database table name.
*
* @var string
*/
protected $table;
/**
* Additional database connection options to use in queries.
*
* @var array
*/
protected $options = array();
/**
* The storage collection.
*
* @var string
*/
protected $collection = StorageInterface::DEFAULT_COLLECTION;
/**
* Constructs a new DatabaseStorage.
*
* @param \Drupal\Core\Database\Connection $connection
* A Database connection to use for reading and writing configuration data.
* @param string $table
* A database table name to store configuration data in.
* @param array $options
* (optional) Any additional database connection options to use in queries.
* @param string $collection
* (optional) The collection to store configuration in. Defaults to the
* default collection.
*/
public function __construct(Connection $connection, $table, array $options = array(), $collection = StorageInterface::DEFAULT_COLLECTION) {
$this->connection = $connection;
$this->table = $table;
$this->options = $options;
$this->collection = $collection;
}
/**
* Implements Drupal\Core\Config\StorageInterface::exists().
*/
public function exists($name) {
try {
return (bool) $this->connection->queryRange('SELECT 1 FROM {' . $this->connection->escapeTable($this->table) . '} WHERE collection = :collection AND name = :name', 0, 1, array(
':collection' => $this->collection,
':name' => $name,
), $this->options)->fetchField();
}
catch (\Exception $e) {
// If we attempt a read without actually having the database or the table
// available, just return FALSE so the caller can handle it.
return FALSE;
}
}
/**
* {@inheritdoc}
*/
public function read($name) {
$data = FALSE;
try {
$raw = $this->connection->query('SELECT data FROM {' . $this->connection->escapeTable($this->table) . '} WHERE collection = :collection AND name = :name', array(':collection' => $this->collection, ':name' => $name), $this->options)->fetchField();
if ($raw !== FALSE) {
$data = $this->decode($raw);
}
}
catch (\Exception $e) {
// If we attempt a read without actually having the database or the table
// available, just return FALSE so the caller can handle it.
}
return $data;
}
/**
* {@inheritdoc}
*/
public function readMultiple(array $names) {
$list = array();
try {
$list = $this->connection->query('SELECT name, data FROM {' . $this->connection->escapeTable($this->table) . '} WHERE collection = :collection AND name IN ( :names[] )', array(':collection' => $this->collection, ':names[]' => $names), $this->options)->fetchAllKeyed();
foreach ($list as &$data) {
$data = $this->decode($data);
}
}
catch (\Exception $e) {
// If we attempt a read without actually having the database or the table
// available, just return an empty array so the caller can handle it.
}
return $list;
}
/**
* {@inheritdoc}
*/
public function write($name, array $data) {
$data = $this->encode($data);
try {
return $this->doWrite($name, $data);
}
catch (\Exception $e) {
// If there was an exception, try to create the table.
if ($this->ensureTableExists()) {
return $this->doWrite($name, $data);
}
// Some other failure that we can not recover from.
throw $e;
}
}
/**
* Helper method so we can re-try a write.
*
* @param string $name
* The config name.
* @param string $data
* The config data, already dumped to a string.
*
* @return bool
*/
protected function doWrite($name, $data) {
$options = array('return' => Database::RETURN_AFFECTED) + $this->options;
return (bool) $this->connection->merge($this->table, $options)
->keys(array('collection', 'name'), array($this->collection, $name))
->fields(array('data' => $data))
->execute();
}
/**
* Check if the config table exists and create it if not.
*
* @return bool
* TRUE if the table was created, FALSE otherwise.
*
* @throws \Drupal\Core\Config\StorageException
* If a database error occurs.
*/
protected function ensureTableExists() {
try {
if (!$this->connection->schema()->tableExists($this->table)) {
$this->connection->schema()->createTable($this->table, static::schemaDefinition());
return TRUE;
}
}
// If another process has already created the config table, attempting to
// recreate it will throw an exception. In this case just catch the
// exception and do nothing.
catch (SchemaObjectExistsException $e) {
return TRUE;
}
catch (\Exception $e) {
throw new StorageException($e->getMessage(), NULL, $e);
}
return FALSE;
}
/**
* Defines the schema for the configuration table.
*/
protected static function schemaDefinition() {
$schema = array(
'description' => 'The base table for configuration data.',
'fields' => array(
'collection' => array(
'description' => 'Primary Key: Config object collection.',
'type' => 'varchar_ascii',
'length' => 255,
'not null' => TRUE,
'default' => '',
),
'name' => array(
'description' => 'Primary Key: Config object name.',
'type' => 'varchar_ascii',
'length' => 255,
'not null' => TRUE,
'default' => '',
),
'data' => array(
'description' => 'A serialized configuration object data.',
'type' => 'blob',
'not null' => FALSE,
'size' => 'big',
),
),
'primary key' => array('collection', 'name'),
);
return $schema;
}
/**
* Implements Drupal\Core\Config\StorageInterface::delete().
*
* @throws PDOException
*
* @todo Ignore replica targets for data manipulation operations.
*/
public function delete($name) {
$options = array('return' => Database::RETURN_AFFECTED) + $this->options;
return (bool) $this->connection->delete($this->table, $options)
->condition('collection', $this->collection)
->condition('name', $name)
->execute();
}
/**
* Implements Drupal\Core\Config\StorageInterface::rename().
*
* @throws PDOException
*/
public function rename($name, $new_name) {
$options = array('return' => Database::RETURN_AFFECTED) + $this->options;
return (bool) $this->connection->update($this->table, $options)
->fields(array('name' => $new_name))
->condition('name', $name)
->condition('collection', $this->collection)
->execute();
}
/**
* Implements Drupal\Core\Config\StorageInterface::encode().
*/
public function encode($data) {
return serialize($data);
}
/**
* Implements Drupal\Core\Config\StorageInterface::decode().
*
* @throws ErrorException
* unserialize() triggers E_NOTICE if the string cannot be unserialized.
*/
public function decode($raw) {
$data = @unserialize($raw);
return is_array($data) ? $data : FALSE;
}
/**
* {@inheritdoc}
*/
public function listAll($prefix = '') {
try {
$query = $this->connection->select($this->table);
$query->fields($this->table, array('name'));
$query->condition('collection', $this->collection, '=');
$query->condition('name', $prefix . '%', 'LIKE');
$query->orderBy('collection')->orderBy('name');
return $query->execute()->fetchCol();
}
catch (\Exception $e) {
return array();
}
}
/**
* {@inheritdoc}
*/
public function deleteAll($prefix = '') {
try {
$options = array('return' => Database::RETURN_AFFECTED) + $this->options;
return (bool) $this->connection->delete($this->table, $options)
->condition('name', $prefix . '%', 'LIKE')
->condition('collection', $this->collection)
->execute();
}
catch (\Exception $e) {
return FALSE;
}
}
/**
* {@inheritdoc}
*/
public function createCollection($collection) {
return new static(
$this->connection,
$this->table,
$this->options,
$collection
);
}
/**
* {@inheritdoc}
*/
public function getCollectionName() {
return $this->collection;
}
/**
* {@inheritdoc}
*/
public function getAllCollectionNames() {
try {
return $this->connection->query('SELECT DISTINCT collection FROM {' . $this->connection->escapeTable($this->table) . '} WHERE collection <> :collection ORDER by collection', array(
':collection' => StorageInterface::DEFAULT_COLLECTION)
)->fetchCol();
}
catch (\Exception $e) {
return array();
}
}
}

View file

@ -0,0 +1,130 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\ConfigDependencyDeleteFormTrait.
*/
namespace Drupal\Core\Config\Entity;
use Drupal\Core\Config\ConfigManagerInterface;
use Drupal\Core\Entity\EntityManagerInterface;
/**
* Lists affected configuration entities by a dependency removal.
*
* This trait relies on the StringTranslationTrait.
*/
trait ConfigDependencyDeleteFormTrait {
/**
* Translates a string to the current language or to a given language.
*
* Provided by \Drupal\Core\StringTranslation\StringTranslationTrait.
*/
abstract protected function t($string, array $args = array(), array $options = array());
/**
* Adds form elements to list affected configuration entities.
*
* @param array $form
* The form array to add elements to.
* @param string $type
* The type of dependency being checked. Either 'module', 'theme', 'config'
* or 'content'.
* @param array $names
* The specific names to check. If $type equals 'module' or 'theme' then it
* should be a list of module names or theme names. In the case of 'config'
* or 'content' it should be a list of configuration dependency names.
* @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
* The config manager.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
*
* @see \Drupal\Core\Config\ConfigManagerInterface::getConfigEntitiesToChangeOnDependencyRemoval()
*/
protected function addDependencyListsToForm(array &$form, $type, array $names, ConfigManagerInterface $config_manager, EntityManagerInterface $entity_manager) {
// Get the dependent entities.
$dependent_entities = $config_manager->getConfigEntitiesToChangeOnDependencyRemoval($type, $names);
$entity_types = array();
$form['entity_updates'] = array(
'#type' => 'details',
'#title' => $this->t('Configuration updates'),
'#description' => $this->t('The listed configuration will be updated.'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#access' => FALSE,
);
foreach ($dependent_entities['update'] as $entity) {
/** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */
$entity_type_id = $entity->getEntityTypeId();
if (!isset($form['entity_updates'][$entity_type_id])) {
$entity_type = $entity_manager->getDefinition($entity_type_id);
// Store the ID and label to sort the entity types and entities later.
$label = $entity_type->getLabel();
$entity_types[$entity_type_id] = $label;
$form['entity_updates'][$entity_type_id] = array(
'#theme' => 'item_list',
'#title' => $label,
'#items' => array(),
);
}
$form['entity_updates'][$entity_type_id]['#items'][] = $entity->label() ?: $entity->id();
}
if (!empty($dependent_entities['update'])) {
$form['entity_updates']['#access'] = TRUE;
// Add a weight key to the entity type sections.
asort($entity_types, SORT_FLAG_CASE);
$weight = 0;
foreach ($entity_types as $entity_type_id => $label) {
$form['entity_updates'][$entity_type_id]['#weight'] = $weight;
// Sort the list of entity labels alphabetically.
sort($form['entity_updates'][$entity_type_id]['#items'], SORT_FLAG_CASE);
$weight++;
}
}
$form['entity_deletes'] = array(
'#type' => 'details',
'#title' => $this->t('Configuration deletions'),
'#description' => $this->t('The listed configuration will be deleted.'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#access' => FALSE,
);
foreach ($dependent_entities['delete'] as $entity) {
$entity_type_id = $entity->getEntityTypeId();
if (!isset($form['entity_deletes'][$entity_type_id])) {
$entity_type = $entity_manager->getDefinition($entity_type_id);
// Store the ID and label to sort the entity types and entities later.
$label = $entity_type->getLabel();
$entity_types[$entity_type_id] = $label;
$form['entity_deletes'][$entity_type_id] = array(
'#theme' => 'item_list',
'#title' => $label,
'#items' => array(),
);
}
$form['entity_deletes'][$entity_type_id]['#items'][] = $entity->label() ?: $entity->id();
}
if (!empty($dependent_entities['delete'])) {
$form['entity_deletes']['#access'] = TRUE;
// Add a weight key to the entity type sections.
asort($entity_types, SORT_FLAG_CASE);
$weight = 0;
foreach ($entity_types as $entity_type_id => $label) {
$form['entity_deletes'][$entity_type_id]['#weight'] = $weight;
// Sort the list of entity labels alphabetically.
sort($form['entity_deletes'][$entity_type_id]['#items'], SORT_FLAG_CASE);
$weight++;
}
}
}
}

View file

@ -0,0 +1,322 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\ConfigDependencyManager.
*/
namespace Drupal\Core\Config\Entity;
use Drupal\Component\Graph\Graph;
use Drupal\Component\Utility\SortArray;
/**
* Provides a class to discover configuration entity dependencies.
*
* Configuration entities can depend on modules, themes and other configuration
* entities. The dependency system is used during configuration installation,
* uninstallation, and synchronization to ensure that configuration entities are
* handled in the correct order. For example, node types are created before
* their fields, and both are created before the view display configuration.
*
* The configuration dependency value is structured like this:
* @code
* array(
* 'config => array(
* // An array of configuration entity object names. Recalculated on save.
* ),
* 'content => array(
* // An array of content entity configuration dependency names. The default
* // format is "ENTITY_TYPE_ID:BUNDLE:UUID". Recalculated on save.
* ),
* 'module' => array(
* // An array of module names. Recalculated on save.
* ),
* 'theme' => array(
* // An array of theme names. Recalculated on save.
* ),
* 'enforced' => array(
* // An array of configuration dependencies that the config entity is
* // ensured to have regardless of the details of the configuration. These
* // dependencies are not recalculated on save.
* 'config' => array(),
* 'content' => array(),
* 'module' => array(),
* 'theme' => array(),
* ),
* );
* @endcode
*
* Configuration entity dependencies are recalculated on save based on the
* current values of the configuration. For example, a filter format will depend
* on the modules that provide the filter plugins it configures. The filter
* format can be reconfigured to use a different filter plugin provided by
* another module. If this occurs, the dependencies will be recalculated on save
* and the old module will be removed from the list of dependencies and replaced
* with the new one.
*
* Configuration entity classes usually extend
* \Drupal\Core\Config\Entity\ConfigEntityBase. The base class provides a
* generic implementation of the calculateDependencies() method that can
* discover dependencies due to enforced dependencies, plugins, and third party
* settings. If the configuration entity has dependencies that cannot be
* discovered by the base class's implementation, then it needs to implement
* \Drupal\Core\Config\Entity\ConfigEntityInterface::calculateDependencies() to
* calculate (and return) the dependencies. In this method, use
* \Drupal\Core\Config\Entity\ConfigEntityBase::addDependency() to add
* dependencies. Implementations should call the base class implementation to
* inherit the generic functionality.
*
* Classes for configurable plugins are a special case. They can either declare
* their configuration dependencies using the calculateDependencies() method
* described in the paragraph above, or if they have only static dependencies,
* these can be declared using the 'config_dependencies' annotation key.
*
* If an extension author wants a configuration entity to depend on something
* that is not calculable then they can add these dependencies to the enforced
* dependencies key. For example, the Forum module provides the forum node type
* and in order for it to be deleted when the forum module is uninstalled it has
* an enforced dependency on the module. The dependency on the Forum module can
* not be calculated since there is nothing inherent in the state of the node
* type configuration entity that depends on functionality provided by the Forum
* module.
*
* Once declared properly, dependencies are saved to the configuration entity's
* configuration object so that they can be checked without the module that
* provides the configuration entity class being installed. This is important
* for configuration synchronization, which needs to be able to validate
* configuration in the staging directory before the synchronization has
* occurred. Also, if you have a configuration entity object and you want to
* get the current dependencies without recalculation, you can use
* \Drupal\Core\Config\Entity\ConfigEntityInterface::getDependencies().
*
* When uninstalling a module or a theme, configuration entities that are
* dependent will also be removed. This default behavior can lead to undesirable
* side effects, such as a node view mode being entirely removed when the module
* defining a field or formatter it uses is uninstalled. To prevent this,
* configuration entity classes can implement
* \Drupal\Core\Config\Entity\ConfigEntityInterface::onDependencyRemoval(),
* which allows the entity class to remove dependencies instead of being deleted
* themselves. Implementations should save the entity if dependencies have been
* successfully removed, in order to register the newly cleaned-out dependency
* list. So, for example, the node view mode configuration entity class
* should implement this method to remove references to formatters if the plugin
* that supplies them depends on a module that is being uninstalled.
*
* If a configuration entity is provided as default configuration by an
* extension (module, theme, or profile), the extension has to depend on any
* modules or themes that the configuration depends on. For example, if a view
* configuration entity is provided by an installation profile and the view will
* not work without a certain module, the profile must declare a dependency on
* this module in its info.yml file. If you do not want your extension to always
* depend on a particular module that one of its default configuration entities
* depends on, you can use a sub-module: move the configuration entity to the
* sub-module instead of including it in the main extension, and declare the
* module dependency in the sub-module only.
*
* @see \Drupal\Core\Config\Entity\ConfigEntityInterface::calculateDependencies()
* @see \Drupal\Core\Config\Entity\ConfigEntityInterface::onDependencyRemoval()
* @see \Drupal\Core\Config\Entity\ConfigEntityBase::addDependency()
* @see \Drupal\Core\Config\ConfigInstallerInterface::installDefaultConfig()
* @see \Drupal\Core\Config\ConfigManagerInterface::uninstall()
* @see \Drupal\Core\Config\Entity\ConfigEntityDependency
* @see \Drupal\Core\Entity\EntityInterface::getConfigDependencyName()
* @see \Drupal\Core\Plugin\PluginDependencyTrait
*/
class ConfigDependencyManager {
/**
* The config entity data.
*
* @var \Drupal\Core\Config\Entity\ConfigEntityDependency[]
*/
protected $data = array();
/**
* The directed acyclic graph.
*
* @var array
*/
protected $graph;
/**
* Gets dependencies.
*
* @param string $type
* The type of dependency being checked. Either 'module', 'theme', 'config'
* or 'content'.
* @param string $name
* The specific name to check. If $type equals 'module' or 'theme' then it
* should be a module name or theme name. In the case of entity it should be
* the full configuration object name.
*
* @return \Drupal\Core\Config\Entity\ConfigEntityDependency[]
* An array of config entity dependency objects that are dependent.
*/
public function getDependentEntities($type, $name) {
$dependent_entities = array();
$entities_to_check = array();
if ($type == 'config') {
$entities_to_check[] = $name;
}
else {
if ($type == 'module' || $type == 'theme' || $type == 'content') {
$dependent_entities = array_filter($this->data, function (ConfigEntityDependency $entity) use ($type, $name) {
return $entity->hasDependency($type, $name);
});
}
// If checking content, module, or theme dependencies, discover which
// entities are dependent on the entities that have a direct dependency.
foreach ($dependent_entities as $entity) {
$entities_to_check[] = $entity->getConfigDependencyName();
}
}
$dependencies = array_merge($this->createGraphConfigEntityDependencies($entities_to_check), $dependent_entities);
// Sort dependencies in the reverse order of the graph. So the least
// dependent is at the top. For example, this ensures that fields are
// always after field storages. This is because field storages need to be
// created before a field.
return array_reverse(array_intersect_key($this->graph, $dependencies));
}
/**
* Sorts the dependencies in order of most dependent last.
*
* @return array
* The list of entities in order of most dependent last, otherwise
* alphabetical.
*/
public function sortAll() {
$graph = $this->getGraph();
// Sort by reverse weight and alphabetically. The most dependent entities
// are last and entities with the same weight are alphabetically ordered.
uasort($graph, array($this, 'sortGraph'));
return array_keys($graph);
}
/**
* Sorts the dependency graph by reverse weight and alphabetically.
*
* @param array $a
* First item for comparison. The compared items should be associative
* arrays that include a 'weight' and a 'component' key.
* @param array $b
* Second item for comparison.
*
* @return int
* The comparison result for uasort().
*/
public function sortGraph(array $a, array $b) {
$weight_cmp = SortArray::sortByKeyInt($a, $b, 'weight') * -1;
if ($weight_cmp === 0) {
return SortArray::sortByKeyString($a, $b, 'component');
}
return $weight_cmp;
}
/**
* Creates a graph of config entity dependencies.
*
* @param array $entities_to_check
* The configuration entity full configuration names to determine the
* dependencies for.
*
* @return \Drupal\Core\Config\Entity\ConfigEntityDependency[]
* A graph of config entity dependency objects that are dependent on the
* supplied entities to check.
*/
protected function createGraphConfigEntityDependencies($entities_to_check) {
$dependent_entities = array();
$graph = $this->getGraph();
foreach ($entities_to_check as $entity) {
if (isset($graph[$entity]) && !empty($graph[$entity]['reverse_paths'])){
foreach ($graph[$entity]['reverse_paths'] as $dependency => $value) {
$dependent_entities[$dependency] = $this->data[$dependency];
}
}
}
return $dependent_entities;
}
/**
* Gets the dependency graph of all the config entities.
*
* @return array
* The dependency graph of all the config entities.
*/
protected function getGraph() {
if (!isset($this->graph)) {
$graph = array();
foreach ($this->data as $entity) {
$graph_key = $entity->getConfigDependencyName();
$graph[$graph_key]['edges'] = array();
$dependencies = $entity->getDependencies('config');
if (!empty($dependencies)) {
foreach ($dependencies as $dependency) {
$graph[$graph_key]['edges'][$dependency] = TRUE;
}
}
}
$graph_object = new Graph($graph);
$this->graph = $graph_object->searchAndSort();
}
return $this->graph;
}
/**
* Sets data to calculate dependencies for.
*
* The data is converted into lightweight ConfigEntityDependency objects.
*
* @param array $data
* Configuration data keyed by configuration object name. Typically the
* output of \Drupal\Core\Config\StorageInterface::loadMultiple().
*
* @return $this
*/
public function setData(array $data) {
array_walk($data, function (&$config, $name) {
$config = new ConfigEntityDependency($name, $config);
});
$this->data = $data;
$this->graph = NULL;
return $this;
}
/**
* Updates one of the lightweight ConfigEntityDependency objects.
*
* @param $name
* The configuration dependency name.
* @param array $dependencies
* The configuration dependencies. The array is structured like this:
* @code
* array(
* 'config => array(
* // An array of configuration entity object names.
* ),
* 'content => array(
* // An array of content entity configuration dependency names. The default
* // format is "ENTITY_TYPE_ID:BUNDLE:UUID".
* ),
* 'module' => array(
* // An array of module names.
* ),
* 'theme' => array(
* // An array of theme names.
* ),
* );
* @endcode
*
* @return $this
*/
public function updateData($name, array $dependencies) {
$this->graph = NULL;
$this->data[$name] = new ConfigEntityDependency($name, ['dependencies' => $dependencies]);
return $this;
}
}

View file

@ -0,0 +1,608 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\ConfigEntityBase.
*/
namespace Drupal\Core\Config\Entity;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\ConfigException;
use Drupal\Core\Config\Schema\SchemaIncompleteException;
use Drupal\Core\Entity\Entity;
use Drupal\Core\Config\ConfigDuplicateUUIDException;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Plugin\PluginDependencyTrait;
/**
* Defines a base configuration entity class.
*
* @ingroup entity_api
*/
abstract class ConfigEntityBase extends Entity implements ConfigEntityInterface {
use PluginDependencyTrait {
addDependency as addDependencyTrait;
}
/**
* The original ID of the configuration entity.
*
* The ID of a configuration entity is a unique string (machine name). When a
* configuration entity is updated and its machine name is renamed, the
* original ID needs to be known.
*
* @var string
*/
protected $originalId;
/**
* The name of the property that is used to store plugin configuration.
*
* This is needed when the entity uses a LazyPluginCollection, to dictate
* where the plugin configuration should be stored.
*
* @var string
*/
protected $pluginConfigKey;
/**
* The enabled/disabled status of the configuration entity.
*
* @var bool
*/
protected $status = TRUE;
/**
* The UUID for this entity.
*
* @var string
*/
protected $uuid;
/**
* Whether the config is being created, updated or deleted through the
* import process.
*
* @var bool
*/
private $isSyncing = FALSE;
/**
* Whether the config is being deleted by the uninstall process.
*
* @var bool
*/
private $isUninstalling = FALSE;
/**
* The language code of the entity's default language.
*
* Assumed to be English by default. ConfigEntityStorage will set an
* appropriate language when creating new entities. This default applies to
* imported default configuration where the language code is missing. Those
* should be assumed to be English. All configuration entities support third
* party settings, so even configuration entities that do not directly
* store settings involving text in a human language may have such third
* party settings attached. This means configuration entities should be in one
* of the configured languages or the built-in English.
*
* @var string
*/
protected $langcode = 'en';
/**
* Third party entity settings.
*
* An array of key/value pairs keyed by provider.
*
* @var array
*/
protected $third_party_settings = array();
/**
* Trust supplied data and not use configuration schema on save.
*
* @var bool
*/
protected $trustedData = FALSE;
/**
* Overrides Entity::__construct().
*/
public function __construct(array $values, $entity_type) {
parent::__construct($values, $entity_type);
// Backup the original ID, if any.
// Configuration entity IDs are strings, and '0' is a valid ID.
$original_id = $this->id();
if ($original_id !== NULL && $original_id !== '') {
$this->setOriginalId($original_id);
}
}
/**
* {@inheritdoc}
*/
public function getOriginalId() {
return $this->originalId;
}
/**
* {@inheritdoc}
*/
public function setOriginalId($id) {
// Do not call the parent method since that would mark this entity as no
// longer new. Unlike content entities, new configuration entities have an
// ID.
// @todo https://www.drupal.org/node/2478811 Document the entity life cycle
// and the differences between config and content.
$this->originalId = $id;
return $this;
}
/**
* Overrides Entity::isNew().
*
* EntityInterface::enforceIsNew() is only supported for newly created
* configuration entities but has no effect after saving, since each
* configuration entity is unique.
*/
public function isNew() {
return !empty($this->enforceIsNew);
}
/**
* {@inheritdoc}
*/
public function get($property_name) {
return isset($this->{$property_name}) ? $this->{$property_name} : NULL;
}
/**
* {@inheritdoc}
*/
public function set($property_name, $value) {
if ($this instanceof EntityWithPluginCollectionInterface) {
$plugin_collections = $this->getPluginCollections();
if (isset($plugin_collections[$property_name])) {
// If external code updates the settings, pass it along to the plugin.
$plugin_collections[$property_name]->setConfiguration($value);
}
}
$this->{$property_name} = $value;
return $this;
}
/**
* {@inheritdoc}
*/
public function enable() {
return $this->setStatus(TRUE);
}
/**
* {@inheritdoc}
*/
public function disable() {
// An entity was disabled, invalidate its own cache tag.
Cache::invalidateTags($this->getCacheTags());
return $this->setStatus(FALSE);
}
/**
* {@inheritdoc}
*/
public function setStatus($status) {
$this->status = (bool) $status;
return $this;
}
/**
* {@inheritdoc}
*/
public function status() {
return !empty($this->status);
}
/**
* {@inheritdoc}
*/
public function setSyncing($syncing) {
$this->isSyncing = $syncing;
return $this;
}
/**
* {@inheritdoc}
*/
public function isSyncing() {
return $this->isSyncing;
}
/**
* {@inheritdoc}
*/
public function setUninstalling($uninstalling) {
$this->isUninstalling = $uninstalling;
}
/**
* {@inheritdoc}
*/
public function isUninstalling() {
return $this->isUninstalling;
}
/**
* {@inheritdoc}
*/
public function createDuplicate() {
$duplicate = parent::createDuplicate();
// Prevent the new duplicate from being misinterpreted as a rename.
$duplicate->setOriginalId(NULL);
return $duplicate;
}
/**
* Helper callback for uasort() to sort configuration entities by weight and label.
*/
public static function sort(ConfigEntityInterface $a, ConfigEntityInterface $b) {
$a_weight = isset($a->weight) ? $a->weight : 0;
$b_weight = isset($b->weight) ? $b->weight : 0;
if ($a_weight == $b_weight) {
$a_label = $a->label();
$b_label = $b->label();
return strnatcasecmp($a_label, $b_label);
}
return ($a_weight < $b_weight) ? -1 : 1;
}
/**
* {@inheritdoc}
*/
public function toArray() {
$properties = array();
/** @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $entity_type */
$entity_type = $this->getEntityType();
$properties_to_export = $entity_type->getPropertiesToExport();
if (empty($properties_to_export)) {
$config_name = $entity_type->getConfigPrefix() . '.' . $this->id();
$definition = $this->getTypedConfig()->getDefinition($config_name);
if (!isset($definition['mapping'])) {
throw new SchemaIncompleteException(SafeMarkup::format('Incomplete or missing schema for @config_name', array('@config_name' => $config_name)));
}
$properties_to_export = array_combine(array_keys($definition['mapping']), array_keys($definition['mapping']));
}
$id_key = $entity_type->getKey('id');
foreach ($properties_to_export as $property_name => $export_name) {
// Special handling for IDs so that computed compound IDs work.
// @see \Drupal\Core\Entity\EntityDisplayBase::id()
if ($property_name == $id_key) {
$properties[$export_name] = $this->id();
}
else {
$properties[$export_name] = $this->get($property_name);
}
}
if (empty($this->third_party_settings)) {
unset($properties['third_party_settings']);
}
return $properties;
}
/**
* Gets the typed config manager.
*
* @return \Drupal\Core\Config\TypedConfigManagerInterface
*/
protected function getTypedConfig() {
return \Drupal::service('config.typed');
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
if ($this instanceof EntityWithPluginCollectionInterface) {
// Any changes to the plugin configuration must be saved to the entity's
// copy as well.
foreach ($this->getPluginCollections() as $plugin_config_key => $plugin_collection) {
$this->set($plugin_config_key, $plugin_collection->getConfiguration());
}
}
// Ensure this entity's UUID does not exist with a different ID, regardless
// of whether it's new or updated.
$matching_entities = $storage->getQuery()
->condition('uuid', $this->uuid())
->execute();
$matched_entity = reset($matching_entities);
if (!empty($matched_entity) && ($matched_entity != $this->id()) && $matched_entity != $this->getOriginalId()) {
throw new ConfigDuplicateUUIDException(SafeMarkup::format('Attempt to save a configuration entity %id with UUID %uuid when this UUID is already used for %matched', array('%id' => $this->id(), '%uuid' => $this->uuid(), '%matched' => $matched_entity)));
}
// If this entity is not new, load the original entity for comparison.
if (!$this->isNew()) {
$original = $storage->loadUnchanged($this->getOriginalId());
// Ensure that the UUID cannot be changed for an existing entity.
if ($original && ($original->uuid() != $this->uuid())) {
throw new ConfigDuplicateUUIDException(SafeMarkup::format('Attempt to save a configuration entity %id with UUID %uuid when this entity already exists with UUID %original_uuid', array('%id' => $this->id(), '%uuid' => $this->uuid(), '%original_uuid' => $original->uuid())));
}
}
if (!$this->isSyncing() && !$this->trustedData) {
// Ensure the correct dependencies are present. If the configuration is
// being written during a configuration synchronization then there is no
// need to recalculate the dependencies.
$this->calculateDependencies();
}
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
// Dependencies should be recalculated on every save. This ensures stale
// dependencies are never saved.
if (isset($this->dependencies['enforced'])) {
$dependencies = $this->dependencies['enforced'];
$this->dependencies = $dependencies;
$this->dependencies['enforced'] = $dependencies;
}
else {
$this->dependencies = array();
}
if ($this instanceof EntityWithPluginCollectionInterface) {
// Configuration entities need to depend on the providers of any plugins
// that they store the configuration for.
foreach ($this->getPluginCollections() as $plugin_collection) {
foreach ($plugin_collection as $instance) {
$this->calculatePluginDependencies($instance);
}
}
}
if ($this instanceof ThirdPartySettingsInterface) {
// Configuration entities need to depend on the providers of any third
// parties that they store the configuration for.
foreach ($this->getThirdPartyProviders() as $provider) {
$this->addDependency('module', $provider);
}
}
return $this->dependencies;
}
/**
* {@inheritdoc}
*/
public function urlInfo($rel = 'edit-form', array $options = []) {
return parent::urlInfo($rel, $options);
}
/**
* {@inheritdoc}
*/
public function url($rel = 'edit-form', $options = array()) {
return parent::url($rel, $options);
}
/**
* {@inheritdoc}
*/
public function link($text = NULL, $rel = 'edit-form', array $options = []) {
return parent::link($text, $rel, $options);
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
// Use cache tags that match the underlying config object's name.
// @see \Drupal\Core\Config\ConfigBase::getCacheTags()
return ['config:' . $this->getConfigDependencyName()];
}
/**
* Overrides \Drupal\Core\Entity\DependencyTrait:addDependency().
*
* Note that this function should only be called from implementations of
* \Drupal\Core\Config\Entity\ConfigEntityInterface::calculateDependencies(),
* as dependencies are recalculated during every entity save.
*
* @see \Drupal\Core\Config\Entity\ConfigEntityDependency::hasDependency()
*/
protected function addDependency($type, $name) {
// A config entity is always dependent on its provider. There is no need to
// explicitly declare the dependency. An explicit dependency on Core, which
// provides some plugins, is also not needed.
if ($type == 'module' && ($name == $this->getEntityType()->getProvider() || $name == 'core')) {
return $this;
}
return $this->addDependencyTrait($type, $name);
}
/**
* {@inheritdoc}
*/
public function getDependencies() {
return $this->dependencies;
}
/**
* {@inheritdoc}
*/
public function getConfigDependencyName() {
return $this->getEntityType()->getConfigPrefix() . '.' . $this->id();
}
/**
* {@inheritdoc}
*/
public function getConfigTarget() {
// For configuration entities, use the config ID for the config target
// identifier. This ensures that default configuration (which does not yet
// have UUIDs) can be provided and installed with references to the target,
// and also makes config dependencies more readable.
return $this->id();
}
/**
* {@inheritdoc}
*/
public function onDependencyRemoval(array $dependencies) {
$changed = FALSE;
if (!empty($this->third_party_settings)) {
$old_count = count($this->third_party_settings);
$this->third_party_settings = array_diff_key($this->third_party_settings, array_flip($dependencies['module']));
$changed = $old_count != count($this->third_party_settings);
}
return $changed;
}
/**
* {@inheritdoc}
*
* Override to never invalidate the entity's cache tag; the config system
* already invalidates it.
*/
protected function invalidateTagsOnSave($update) {
Cache::invalidateTags($this->getEntityType()->getListCacheTags());
}
/**
* {@inheritdoc}
*
* Override to never invalidate the individual entities' cache tags; the
* config system already invalidates them.
*/
protected static function invalidateTagsOnDelete(EntityTypeInterface $entity_type, array $entities) {
Cache::invalidateTags($entity_type->getListCacheTags());
}
/**
* {@inheritdoc}
*/
public function setThirdPartySetting($module, $key, $value) {
$this->third_party_settings[$module][$key] = $value;
return $this;
}
/**
* {@inheritdoc}
*/
public function getThirdPartySetting($module, $key, $default = NULL) {
if (isset($this->third_party_settings[$module][$key])) {
return $this->third_party_settings[$module][$key];
}
else {
return $default;
}
}
/**
* {@inheritdoc}
*/
public function getThirdPartySettings($module) {
return isset($this->third_party_settings[$module]) ? $this->third_party_settings[$module] : array();
}
/**
* {@inheritdoc}
*/
public function unsetThirdPartySetting($module, $key) {
unset($this->third_party_settings[$module][$key]);
// If the third party is no longer storing any information, completely
// remove the array holding the settings for this module.
if (empty($this->third_party_settings[$module])) {
unset($this->third_party_settings[$module]);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function getThirdPartyProviders() {
return array_keys($this->third_party_settings);
}
/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageInterface $storage, array $entities) {
parent::preDelete($storage, $entities);
foreach ($entities as $entity) {
if ($entity->isUninstalling() || $entity->isSyncing()) {
// During extension uninstall and configuration synchronization
// deletions are already managed.
break;
}
// Fix or remove any dependencies.
$config_entities = static::getConfigManager()->getConfigEntitiesToChangeOnDependencyRemoval('config', [$entity->getConfigDependencyName()], FALSE);
/** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $dependent_entity */
foreach ($config_entities['update'] as $dependent_entity) {
$dependent_entity->save();
}
foreach ($config_entities['delete'] as $dependent_entity) {
$dependent_entity->delete();
}
}
}
/**
* Gets the configuration manager.
*
* @return \Drupal\Core\Config\ConfigManager
* The configuration manager.
*/
protected static function getConfigManager() {
return \Drupal::service('config.manager');
}
/**
* {@inheritdoc}
*/
public function isInstallable() {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function trustData() {
$this->trustedData = TRUE;
return $this;
}
/**
* {@inheritdoc}
*/
public function hasTrustedData() {
return $this->trustedData;
}
/**
* {@inheritdoc}
*/
public function save() {
$return = parent::save();
$this->trustedData = FALSE;
return $return;
}
}

View file

@ -0,0 +1,124 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\ConfigEntityBundleBase.
*/
namespace Drupal\Core\Config\Entity;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\EntityStorageInterface;
/**
* A base class for config entity types that act as bundles.
*
* Entity types that want to use this base class must use bundle_of in their
* annotation to specify for which entity type they are providing bundles for.
*/
abstract class ConfigEntityBundleBase extends ConfigEntityBase {
/**
* Renames displays when a bundle is renamed.
*/
protected function renameDisplays() {
// Rename entity displays.
if ($this->getOriginalId() !== $this->id()) {
foreach ($this->loadDisplays('entity_view_display') as $display) {
$new_id = $this->getEntityType()->getBundleOf() . '.' . $this->id() . '.' . $display->getMode();
$display->set('id', $new_id);
$display->setTargetBundle($this->id());
$display->save();
}
}
// Rename entity form displays.
if ($this->getOriginalId() !== $this->id()) {
foreach ($this->loadDisplays('entity_form_display') as $form_display) {
$new_id = $this->getEntityType()->getBundleOf() . '.' . $this->id() . '.' . $form_display->getMode();
$form_display->set('id', $new_id);
$form_display->setTargetBundle($this->id());
$form_display->save();
}
}
}
/**
* Deletes display if a bundle is deleted.
*/
protected function deleteDisplays() {
// Remove entity displays of the deleted bundle.
if ($displays = $this->loadDisplays('entity_view_display')) {
$storage = $this->entityManager()->getStorage('entity_view_display');
$storage->delete($displays);
}
// Remove entity form displays of the deleted bundle.
if ($displays = $this->loadDisplays('entity_form_display')) {
$storage = $this->entityManager()->getStorage('entity_form_display');
$storage->delete($displays);
}
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
$entity_manager = $this->entityManager();
$bundle_of = $this->getEntityType()->getBundleOf();
if (!$update) {
$entity_manager->onBundleCreate($this->id(), $bundle_of);
}
else {
// Invalidate the render cache of entities for which this entity
// is a bundle.
if ($entity_manager->hasHandler($bundle_of, 'view_builder')) {
$entity_manager->getViewBuilder($bundle_of)->resetCache();
}
// Entity bundle field definitions may depend on bundle settings.
$entity_manager->clearCachedFieldDefinitions();
if ($this->getOriginalId() != $this->id()) {
// If the entity was renamed, update the displays.
$this->renameDisplays();
$entity_manager->onBundleRename($this->getOriginalId(), $this->id(), $bundle_of);
}
}
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
foreach ($entities as $entity) {
$entity->deleteDisplays();
\Drupal::entityManager()->onBundleDelete($entity->id(), $entity->getEntityType()->getBundleOf());
}
}
/**
* Returns view or form displays for this bundle.
*
* @param string $entity_type_id
* The entity type ID of the display type to load.
*
* @return \Drupal\Core\Entity\Display\EntityDisplayInterface[]
* A list of matching displays.
*/
protected function loadDisplays($entity_type_id) {
$ids = \Drupal::entityQuery($entity_type_id)
->condition('id', $this->getEntityType()->getBundleOf() . '.' . $this->getOriginalId() . '.', 'STARTS_WITH')
->execute();
if ($ids) {
$storage = $this->entityManager()->getStorage($entity_type_id);
return $storage->loadMultiple($ids);
}
return array();
}
}

View file

@ -0,0 +1,103 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\ConfigEntityDependency.
*/
namespace Drupal\Core\Config\Entity;
/**
* Provides a value object to discover configuration dependencies.
*
* @see \Drupal\Core\Config\Entity\ConfigDependencyManager
*/
class ConfigEntityDependency {
/**
* The configuration entity's configuration object name.
*
* @var string
*/
protected $name;
/**
* The configuration entity's dependencies.
*
* @var array
*/
protected $dependencies;
/**
* Constructs the configuration entity dependency from the entity values.
*
* @param string $name
* The configuration entity's configuration object name.
* @param array $values
* (optional) The configuration entity's values.
*/
public function __construct($name, $values = array()) {
$this->name = $name;
if (isset($values['dependencies'])) {
$this->dependencies = $values['dependencies'];
}
else {
$this->dependencies = array();
}
}
/**
* Gets the configuration entity's dependencies of the supplied type.
*
* @param string $type
* The type of dependency to return. Either 'module', 'theme', 'config' or
* 'content'.
*
* @return array
* The list of dependencies of the supplied type.
*/
public function getDependencies($type) {
$dependencies = array();
if (isset($this->dependencies[$type])) {
$dependencies = $this->dependencies[$type];
}
if ($type == 'module') {
$dependencies[] = substr($this->name, 0, strpos($this->name, '.'));
}
return $dependencies;
}
/**
* Determines if the entity is dependent on extensions or entities.
*
* @param string $type
* The type of dependency being checked. Either 'module', 'theme', 'config'
* or 'content'.
* @param string $name
* The specific name to check. If $type equals 'module' or 'theme' then it
* should be a module name or theme name. In the case of entity it should be
* the full configuration object name.
*
* @return bool
*/
public function hasDependency($type, $name) {
// A config entity is always dependent on its provider.
if ($type == 'module' && strpos($this->name, $name . '.') === 0) {
return TRUE;
}
return isset($this->dependencies[$type]) && array_search($name, $this->dependencies[$type]) !== FALSE;
}
/**
* Gets the configuration entity's configuration dependency name.
*
* @see \Drupal\Core\Entity\EntityInterface::getConfigDependencyName()
*
* @return string
* The configuration dependency name for the entity.
*/
public function getConfigDependencyName() {
return $this->name;
}
}

View file

@ -0,0 +1,229 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\ConfigEntityInterface.
*/
namespace Drupal\Core\Config\Entity;
use Drupal\Core\Entity\EntityInterface;
/**
* Defines a common interface for configuration entities.
*
* @ingroup config_api
* @ingroup entity_api
*/
interface ConfigEntityInterface extends EntityInterface, ThirdPartySettingsInterface {
/**
* Enables the configuration entity.
*
* @return $this
*/
public function enable();
/**
* Disables the configuration entity.
*
* @return $this
*/
public function disable();
/**
* Sets the status of the configuration entity.
*
* @param bool $status
* The status of the configuration entity.
*
* @return $this
*/
public function setStatus($status);
/**
* Sets the status of the isSyncing flag.
*
* @param bool $status
* The status of the sync flag.
*
* @return $this
*/
public function setSyncing($status);
/**
* Returns whether the configuration entity is enabled.
*
* Status implementations for configuration entities should follow these
* general rules:
* - Status does not affect the loading of entities. I.e. Disabling
* configuration entities should only have UI/access implications.
* - It should only take effect when a 'status' key is explicitly declared
* in the entity_keys info of a configuration entity's annotation data.
* - Each entity implementation (entity/controller) is responsible for
* checking and managing the status.
*
* @return bool
* Whether the entity is enabled or not.
*/
public function status();
/**
* Returns whether this entity is being changed as part of an import process.
*
* If you are writing code that responds to a change in this entity (insert,
* update, delete, presave, etc.), and your code would result in a
* configuration change (whether related to this configuration entity, another
* configuration entity, or non-entity configuration) or your code would
* result in a change to this entity itself, you need to check and see if this
* entity change is part of an import process, and skip executing your code if
* that is the case.
*
* For example, \Drupal\node\Entity\NodeType::postSave() adds the default body
* field to newly created node type configuration entities, which is a
* configuration change. You would not want this code to run during an import,
* because imported entities were already given the body field when they were
* originally created, and the imported configuration includes all of their
* currently-configured fields. On the other hand,
* \Drupal\field\Entity\FieldStorageConfig::preSave() and the methods it calls
* make sure that the storage tables are created or updated for the field
* storage configuration entity, which is not a configuration change, and it
* must be done whether due to an import or not. So, the first method should
* check $entity->isSyncing() and skip executing if it returns TRUE, and the
* second should not perform this check.
*
* @return bool
* TRUE if the configuration entity is being created, updated, or deleted
* through the import process.
*/
public function isSyncing();
/**
* Returns whether this entity is being changed during the uninstall process.
*
* If you are writing code that responds to a change in this entity (insert,
* update, delete, presave, etc.), and your code would result in a
* configuration change (whether related to this configuration entity, another
* configuration entity, or non-entity configuration) or your code would
* result in a change to this entity itself, you need to check and see if this
* entity change is part of an uninstall process, and skip executing your code
* if that is the case.
*
* For example, \Drupal\language\Entity\ConfigurableLanguage::preDelete()
* prevents the API from deleting the default language. However during an
* uninstall of the language module it is expected that the default language
* should be deleted.
*
* @return bool
*/
public function isUninstalling();
/**
* Returns the value of a property.
*
* @param string $property_name
* The name of the property that should be returned.
*
* @return mixed
* The property if it exists, or NULL otherwise.
*/
public function get($property_name);
/**
* Sets the value of a property.
*
* @param string $property_name
* The name of the property that should be set.
* @param mixed $value
* The value the property should be set to.
*
* @return $this
*/
public function set($property_name, $value);
/**
* Calculates dependencies and stores them in the dependency property.
*
* @return array
* An array of dependencies grouped by type (module, theme, entity).
*
* @see \Drupal\Core\Config\Entity\ConfigDependencyManager
*/
public function calculateDependencies();
/**
* Informs the entity that entities it depends on will be deleted.
*
* This method allows configuration entities to remove dependencies instead
* of being deleted themselves. Configuration entities can use this method to
* avoid being unnecessarily deleted during an extension uninstallation.
* For example, entity displays remove references to widgets and formatters if
* the plugin that supplies them depends on a module that is being
* uninstalled.
*
* If this method returns TRUE then the entity needs to be re-saved by the
* caller for the changes to take effect. Implementations should not save the
* entity.
*
* @param array $dependencies
* An array of dependencies that will be deleted keyed by dependency type.
* Dependency types are, for example, entity, module and theme.
*
* @return bool
* TRUE if the entity has changed, FALSE if not.
*
* @return bool
* TRUE if the entity has been changed as a result, FALSE if not.
*
* @see \Drupal\Core\Config\Entity\ConfigDependencyManager
* @see \Drupal\Core\Config\ConfigEntityBase::preDelete()
* @see \Drupal\Core\Config\ConfigManager::uninstall()
* @see \Drupal\Core\Entity\EntityDisplayBase::onDependencyRemoval()
*/
public function onDependencyRemoval(array $dependencies);
/**
* Gets the configuration dependencies.
*
* @return array
* An array of dependencies, keyed by $type.
*
* @see \Drupal\Core\Config\Entity\ConfigDependencyManager
*/
public function getDependencies();
/**
* Checks whether this entity is installable.
*
* For example, a default view might not be installable if the base table
* doesn't exist.
*
* @retun bool
* TRUE if the entity is installable, FALSE otherwise.
*/
public function isInstallable();
/**
* Sets that the data should be trusted.
*
* If the data is trusted then dependencies will not be calculated on save and
* schema will not be used to cast the values. Generally this is only used
* during module and theme installation. Once the config entity has been saved
* the data will no longer be marked as trusted. This is an optimization for
* creation of configuration during installation.
*
* @return $this
*
* @see \Drupal\Core\Config\ConfigInstaller::createConfiguration()
*/
public function trustData();
/**
* Gets whether on not the data is trusted.
*
* @return bool
* TRUE if the configuration data is trusted, FALSE if not.
*/
public function hasTrustedData();
}

View file

@ -0,0 +1,60 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\ConfigEntityListBuilder.
*/
namespace Drupal\Core\Config\Entity;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityListBuilder;
/**
* Defines the default class to build a listing of configuration entities.
*
* @ingroup entity_api
*/
class ConfigEntityListBuilder extends EntityListBuilder {
/**
* {@inheritdoc}
*/
public function load() {
$entity_ids = $this->getEntityIds();
$entities = $this->storage->loadMultipleOverrideFree($entity_ids);
// Sort the entities using the entity class's sort() method.
// See \Drupal\Core\Config\Entity\ConfigEntityBase::sort().
uasort($entities, array($this->entityType->getClass(), 'sort'));
return $entities;
}
/**
* {@inheritdoc}
*/
public function getDefaultOperations(EntityInterface $entity) {
/** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */
$operations = parent::getDefaultOperations($entity);
if ($this->entityType->hasKey('status')) {
if (!$entity->status() && $entity->hasLinkTemplate('enable')) {
$operations['enable'] = array(
'title' => t('Enable'),
'weight' => -10,
'url' => $entity->urlInfo('enable'),
);
}
elseif ($entity->hasLinkTemplate('disable')) {
$operations['disable'] = array(
'title' => t('Disable'),
'weight' => 40,
'url' => $entity->urlInfo('disable'),
);
}
}
return $operations;
}
}

View file

@ -0,0 +1,458 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\ConfigEntityStorage.
*/
namespace Drupal\Core\Config\Entity;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ConfigImporterException;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityMalformedException;
use Drupal\Core\Entity\EntityStorageBase;
use Drupal\Core\Config\Config;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Config\Entity\Exception\ConfigEntityIdLengthException;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines the storage class for configuration entities.
*
* Configuration object names of configuration entities are comprised of two
* parts, separated by a dot:
* - config_prefix: A string denoting the owner (module/extension) of the
* configuration object, followed by arbitrary other namespace identifiers
* that are declared by the owning extension; e.g., 'node.type'. The
* config_prefix does NOT contain a trailing dot. It is defined by the entity
* type's annotation.
* - ID: A string denoting the entity ID within the entity type namespace; e.g.,
* 'article'. Entity IDs may contain dots/periods. The entire remaining string
* after the config_prefix in a config name forms the entity ID. Additional or
* custom suffixes are not possible.
*
* @ingroup entity_api
*/
class ConfigEntityStorage extends EntityStorageBase implements ConfigEntityStorageInterface, ImportableEntityStorageInterface {
/**
* Length limit of the configuration entity ID.
*
* Most file systems limit a file name's length to 255 characters, so
* ConfigBase::MAX_NAME_LENGTH restricts the full configuration object name
* to 250 characters (leaving 5 for the file extension). The config prefix
* is limited by ConfigEntityType::PREFIX_LENGTH to 83 characters, so this
* leaves 166 remaining characters for the configuration entity ID, with 1
* additional character needed for the joining dot.
*
* @see \Drupal\Core\Config\ConfigBase::MAX_NAME_LENGTH
* @see \Drupal\Core\Config\Entity\ConfigEntityType::PREFIX_LENGTH
*/
const MAX_ID_LENGTH = 166;
/**
* {@inheritdoc}
*/
protected $uuidKey = 'uuid';
/**
* The config factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The config storage service.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $configStorage;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* Static cache of entities, keyed first by entity ID, then by an extra key.
*
* The additional cache key is to maintain separate caches for different
* states of config overrides.
*
* @var array
* @see \Drupal\Core\Config\ConfigFactoryInterface::getCacheKeys().
*/
protected $entities = array();
/**
* Determines if the underlying configuration is retrieved override free.
*
* @var bool
*/
protected $overrideFree = FALSE;
/**
* Constructs a ConfigEntityStorage object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\Component\Uuid\UuidInterface $uuid_service
* The UUID service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
*/
public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, UuidInterface $uuid_service, LanguageManagerInterface $language_manager) {
parent::__construct($entity_type);
$this->configFactory = $config_factory;
$this->uuidService = $uuid_service;
$this->languageManager = $language_manager;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('config.factory'),
$container->get('uuid'),
$container->get('language_manager')
);
}
/**
* {@inheritdoc}
*/
public function loadRevision($revision_id) {
return NULL;
}
/**
* Implements Drupal\Core\Entity\EntityStorageInterface::deleteRevision().
*/
public function deleteRevision($revision_id) {
return NULL;
}
/**
* Returns the prefix used to create the configuration name.
*
* The prefix consists of the config prefix from the entity type plus a dot
* for separating from the ID.
*
* @return string
* The full configuration prefix, for example 'views.view.'.
*/
protected function getPrefix() {
return $this->entityType->getConfigPrefix() . '.';
}
/**
* {@inheritdoc}
*/
public static function getIDFromConfigName($config_name, $config_prefix) {
return substr($config_name, strlen($config_prefix . '.'));
}
/**
* {@inheritdoc}
*/
protected function doLoadMultiple(array $ids = NULL) {
$prefix = $this->getPrefix();
// Get the names of the configuration entities we are going to load.
if ($ids === NULL) {
$names = $this->configFactory->listAll($prefix);
}
else {
$names = array();
foreach ($ids as $id) {
// Add the prefix to the ID to serve as the configuration object name.
$names[] = $prefix . $id;
}
}
// Load all of the configuration entities.
$records = array();
foreach ($this->configFactory->loadMultiple($names) as $config) {
$records[$config->get($this->idKey)] = $this->overrideFree ? $config->getOriginal(NULL, FALSE) : $config->get();
}
return $this->mapFromStorageRecords($records);
}
/**
* {@inheritdoc}
*/
protected function doCreate(array $values) {
// Set default language to current language if not provided.
$values += array($this->langcodeKey => $this->languageManager->getCurrentLanguage()->getId());
$entity = new $this->entityClass($values, $this->entityTypeId);
return $entity;
}
/**
* {@inheritdoc}
*/
protected function doDelete($entities) {
foreach ($entities as $entity) {
$this->configFactory->getEditable($this->getPrefix() . $entity->id())->delete();
}
}
/**
* Implements Drupal\Core\Entity\EntityStorageInterface::save().
*
* @throws EntityMalformedException
* When attempting to save a configuration entity that has no ID.
*/
public function save(EntityInterface $entity) {
// Configuration entity IDs are strings, and '0' is a valid ID.
$id = $entity->id();
if ($id === NULL || $id === '') {
throw new EntityMalformedException('The entity does not have an ID.');
}
// Check the configuration entity ID length.
// @see \Drupal\Core\Config\Entity\ConfigEntityStorage::MAX_ID_LENGTH
// @todo Consider moving this to a protected method on the parent class, and
// abstracting it for all entity types.
if (strlen($entity->get($this->idKey)) > self::MAX_ID_LENGTH) {
throw new ConfigEntityIdLengthException(SafeMarkup::format('Configuration entity ID @id exceeds maximum allowed length of @length characters.', array(
'@id' => $entity->get($this->idKey),
'@length' => self::MAX_ID_LENGTH,
)));
}
return parent::save($entity);
}
/**
* {@inheritdoc}
*/
protected function doSave($id, EntityInterface $entity) {
$is_new = $entity->isNew();
$prefix = $this->getPrefix();
$config_name = $prefix . $entity->id();
if ($id !== $entity->id()) {
// Renaming a config object needs to cater for:
// - Storage needs to access the original object.
// - The object needs to be renamed/copied in ConfigFactory and reloaded.
// - All instances of the object need to be renamed.
$this->configFactory->rename($prefix . $id, $config_name);
}
$config = $this->configFactory->getEditable($config_name);
// Retrieve the desired properties and set them in config.
$config->setData($this->mapToStorageRecord($entity));
$config->save($entity->hasTrustedData());
// Update the entity with the values stored in configuration. It is possible
// that configuration schema has casted some of the values.
if (!$entity->hasTrustedData()) {
$data = $this->mapFromStorageRecords(array($config->get()));
$updated_entity = current($data);
foreach (array_keys($config->get()) as $property) {
$value = $updated_entity->get($property);
$entity->set($property, $value);
}
}
return $is_new ? SAVED_NEW : SAVED_UPDATED;
}
/**
* Maps from an entity object to the storage record.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity object.
*
* @return array
* The record to store.
*/
protected function mapToStorageRecord(EntityInterface $entity) {
return $entity->toArray();
}
/**
* {@inheritdoc}
*/
protected function has($id, EntityInterface $entity) {
$prefix = $this->getPrefix();
$config = $this->configFactory->get($prefix . $id);
return !$config->isNew();
}
/**
* Gets entities from the static cache.
*
* @param array $ids
* If not empty, return entities that match these IDs.
*
* @return \Drupal\Core\Entity\EntityInterface[]
* Array of entities from the entity cache.
*/
protected function getFromStaticCache(array $ids) {
$entities = array();
// Load any available entities from the internal cache.
if ($this->entityType->isStaticallyCacheable() && !empty($this->entities)) {
$config_overrides_key = $this->overrideFree ? '' : implode(':', $this->configFactory->getCacheKeys());
foreach ($ids as $id) {
if (!empty($this->entities[$id])) {
if (isset($this->entities[$id][$config_overrides_key])) {
$entities[$id] = $this->entities[$id][$config_overrides_key];
}
}
}
}
return $entities;
}
/**
* Stores entities in the static entity cache.
*
* @param \Drupal\Core\Entity\EntityInterface[] $entities
* Entities to store in the cache.
*/
protected function setStaticCache(array $entities) {
if ($this->entityType->isStaticallyCacheable()) {
$config_overrides_key = $this->overrideFree ? '' : implode(':', $this->configFactory->getCacheKeys());
foreach ($entities as $id => $entity) {
$this->entities[$id][$config_overrides_key] = $entity;
}
}
}
/**
* Invokes a hook on behalf of the entity.
*
* @param $hook
* One of 'presave', 'insert', 'update', 'predelete', or 'delete'.
* @param $entity
* The entity object.
*/
protected function invokeHook($hook, EntityInterface $entity) {
// Invoke the hook.
$this->moduleHandler->invokeAll($this->entityTypeId . '_' . $hook, array($entity));
// Invoke the respective entity-level hook.
$this->moduleHandler->invokeAll('entity_' . $hook, array($entity, $this->entityTypeId));
}
/**
* {@inheritdoc}
*/
protected function getQueryServiceName() {
return 'entity.query.config';
}
/**
* {@inheritdoc}
*/
public function importCreate($name, Config $new_config, Config $old_config) {
$entity = $this->createFromStorageRecord($new_config->get());
$entity->setSyncing(TRUE);
$entity->save();
return TRUE;
}
/**
* {@inheritdoc}
*/
public function importUpdate($name, Config $new_config, Config $old_config) {
$id = static::getIDFromConfigName($name, $this->entityType->getConfigPrefix());
$entity = $this->load($id);
if (!$entity) {
throw new ConfigImporterException(SafeMarkup::format('Attempt to update non-existing entity "@id".', array('@id' => $id)));
}
$entity->setSyncing(TRUE);
$entity = $this->updateFromStorageRecord($entity, $new_config->get());
$entity->save();
return TRUE;
}
/**
* {@inheritdoc}
*/
public function importDelete($name, Config $new_config, Config $old_config) {
$id = static::getIDFromConfigName($name, $this->entityType->getConfigPrefix());
$entity = $this->load($id);
$entity->setSyncing(TRUE);
$entity->delete();
return TRUE;
}
/**
* {@inheritdoc}
*/
public function importRename($old_name, Config $new_config, Config $old_config) {
return $this->importUpdate($old_name, $new_config, $old_config);
}
/**
* {@inheritdoc}
*/
public function createFromStorageRecord(array $values) {
// Assign a new UUID if there is none yet.
if ($this->uuidKey && $this->uuidService && !isset($values[$this->uuidKey])) {
$values[$this->uuidKey] = $this->uuidService->generate();
}
$data = $this->mapFromStorageRecords(array($values));
$entity = current($data);
$entity->original = clone $entity;
$entity->enforceIsNew();
$entity->postCreate($this);
// Modules might need to add or change the data initially held by the new
// entity object, for instance to fill-in default values.
$this->invokeHook('create', $entity);
return $entity;
}
/**
* {@inheritdoc}
*/
public function updateFromStorageRecord(ConfigEntityInterface $entity, array $values) {
$entity->original = clone $entity;
$data = $this->mapFromStorageRecords(array($values));
$updated_entity = current($data);
foreach (array_keys($values) as $property) {
$value = $updated_entity->get($property);
$entity->set($property, $value);
}
return $entity;
}
/**
* {@inheritdoc}
*/
public function loadOverrideFree($id) {
$entities = $this->loadMultipleOverrideFree([$id]);
return isset($entities[$id]) ? $entities[$id] : NULL;
}
/**
* {@inheritdoc}
*/
public function loadMultipleOverrideFree(array $ids = NULL) {
$this->overrideFree = TRUE;
$entities = $this->loadMultiple($ids);
$this->overrideFree = FALSE;
return $entities;
}
}

View file

@ -0,0 +1,91 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\ConfigEntityStorageInterface.
*/
namespace Drupal\Core\Config\Entity;
use Drupal\Core\Config\Config;
use Drupal\Core\Entity\EntityStorageInterface;
/**
* Provides an interface for configuration entity storage.
*/
interface ConfigEntityStorageInterface extends EntityStorageInterface {
/**
* Extracts the configuration entity ID from the full configuration name.
*
* @param string $config_name
* The full configuration name to extract the ID from. E.g.
* 'views.view.archive'.
* @param string $config_prefix
* The config prefix of the configuration entity. E.g. 'views.view'
*
* @return string
* The ID of the configuration entity.
*/
public static function getIDFromConfigName($config_name, $config_prefix);
/**
* Creates a configuration entity from storage values.
*
* Allows the configuration entity storage to massage storage values before
* creating an entity.
*
* @param array $values
* The array of values from the configuration storage.
*
* @return ConfigEntityInterface
* The configuration entity.
*
* @see \Drupal\Core\Entity\EntityStorageBase::mapFromStorageRecords()
* @see \Drupal\field\FieldStorageConfigStorage::mapFromStorageRecords()
*/
public function createFromStorageRecord(array $values);
/**
* Updates a configuration entity from storage values.
*
* Allows the configuration entity storage to massage storage values before
* updating an entity.
*
* @param ConfigEntityInterface $entity
* The configuration entity to update.
* @param array $values
* The array of values from the configuration storage.
*
* @return ConfigEntityInterface
* The configuration entity.
*
* @see \Drupal\Core\Entity\EntityStorageBase::mapFromStorageRecords()
* @see \Drupal\field\FieldStorageConfigStorage::mapFromStorageRecords()
*/
public function updateFromStorageRecord(ConfigEntityInterface $entity, array $values);
/**
* Loads one entity in their original form without overrides.
*
* @param mixed $id
* The ID of the entity to load.
*
* @return \Drupal\Core\Entity\EntityInterface|null
* An entity object. NULL if no matching entity is found.
*/
public function loadOverrideFree($id);
/**
* Loads one or more entities in their original form without overrides.
*
* @param $ids
* An array of entity IDs, or NULL to load all entities.
*
* @return \Drupal\Core\Entity\EntityInterface[]
* An array of entity objects indexed by their IDs. Returns an empty array
* if no matching entities are found.
*/
public function loadMultipleOverrideFree(array $ids = NULL);
}

View file

@ -0,0 +1,200 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\ConfigEntityType.
*/
namespace Drupal\Core\Config\Entity;
use Drupal\Core\Config\Entity\Exception\ConfigEntityStorageClassException;
use Drupal\Core\Entity\EntityType;
use Drupal\Core\Config\ConfigPrefixLengthException;
use Drupal\Component\Utility\SafeMarkup;
/**
* Provides an implementation of a configuration entity type and its metadata.
*/
class ConfigEntityType extends EntityType implements ConfigEntityTypeInterface {
/**
* The config prefix set in the configuration entity type annotation.
*
* @see \Drupal\Core\Config\Entity\ConfigEntityTypeInterface::getConfigPrefix()
*/
protected $config_prefix;
/**
* {@inheritdoc}
*/
protected $static_cache = FALSE;
/**
* Keys that are stored key value store for fast lookup.
*
* @var array
*/
protected $lookup_keys = [];
/**
* The list of configuration entity properties to export from the annotation.
*
* @var array
*/
protected $config_export = [];
/**
* The result of merging config_export annotation with the defaults.
*
* This is stored on the class so that it does not have to be recalculated.
*
* @var array
*/
protected $mergedConfigExport = [];
/**
* {@inheritdoc}
*
* @throws \Drupal\Core\Config\Entity\Exception\ConfigEntityStorageClassException
* Exception thrown when the provided class is not an instance of
* \Drupal\Core\Config\Entity\ConfigEntityStorage.
*/
public function __construct($definition) {
// Ensure a default list cache tag is set; do this before calling the parent
// constructor, because we want "Configuration System style" cache tags.
if (empty($this->list_cache_tags)) {
$this->list_cache_tags = ['config:' . $definition['id'] . '_list'];
}
parent::__construct($definition);
// Always add a default 'uuid' key.
$this->entity_keys['uuid'] = 'uuid';
$this->entity_keys['langcode'] = 'langcode';
if (isset($this->handlers['storage'])) {
$this->checkStorageClass($this->handlers['storage']);
}
$this->handlers += array(
'storage' => 'Drupal\Core\Config\Entity\ConfigEntityStorage',
);
$this->lookup_keys[] = 'uuid';
}
/**
* {@inheritdoc}
*/
public function getConfigPrefix() {
// Ensure that all configuration entities are prefixed by the name of the
// module that provides the configuration entity type.
if (isset($this->config_prefix)) {
$config_prefix = $this->provider . '.' . $this->config_prefix;
}
else {
$config_prefix = $this->provider . '.' . $this->id();
}
if (strlen($config_prefix) > static::PREFIX_LENGTH) {
throw new ConfigPrefixLengthException(SafeMarkup::format('The configuration file name prefix @config_prefix exceeds the maximum character limit of @max_char.', array(
'@config_prefix' => $config_prefix,
'@max_char' => static::PREFIX_LENGTH,
)));
}
return $config_prefix;
}
/**
* {@inheritdoc}
*/
public function getBaseTable() {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getRevisionDataTable() {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getRevisionTable() {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getDataTable() {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getConfigDependencyKey() {
return 'config';
}
/**
* {@inheritdoc}
*
* @throws \Drupal\Core\Config\Entity\Exception\ConfigEntityStorageClassException
* Exception thrown when the provided class is not an instance of
* \Drupal\Core\Config\Entity\ConfigEntityStorage.
*/
public function setStorageClass($class) {
$this->checkStorageClass($class);
parent::setStorageClass($class);
}
/**
* Checks that the provided class is an instance of ConfigEntityStorage.
*
* @param string $class
* The class to check.
*
* @see \Drupal\Core\Config\Entity\ConfigEntityStorage.
*/
protected function checkStorageClass($class) {
if (!is_a($class, 'Drupal\Core\Config\Entity\ConfigEntityStorage', TRUE)) {
throw new ConfigEntityStorageClassException(SafeMarkup::format('@class is not \Drupal\Core\Config\Entity\ConfigEntityStorage or it does not extend it', ['@class' => $class]));
}
}
/**
* {@inheritdoc}
*/
public function getPropertiesToExport() {
if (!empty($this->config_export)) {
if (empty($this->mergedConfigExport)) {
// Always add default properties to be exported.
$this->mergedConfigExport = [
'uuid' => 'uuid',
'langcode' => 'langcode',
'status' => 'status',
'dependencies' => 'dependencies',
'third_party_settings' => 'third_party_settings',
];
foreach ($this->config_export as $property => $name) {
if (is_numeric($property)) {
$this->mergedConfigExport[$name] = $name;
}
else {
$this->mergedConfigExport[$property] = $name;
}
}
}
return $this->mergedConfigExport;
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function getLookupKeys() {
return $this->lookup_keys;
}
}

View file

@ -0,0 +1,87 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\ConfigEntityTypeInterface.
*/
namespace Drupal\Core\Config\Entity;
use Drupal\Core\Entity\EntityTypeInterface;
/**
* Provides an interface for a configuration entity type and its metadata.
*/
interface ConfigEntityTypeInterface extends EntityTypeInterface {
/**
* Length limit of the configuration entity prefix.
*
* Configuration entity names are composed of two parts:
* - The config prefix, which is returned by getConfigPrefix() and is
* composed of:
* - The provider module name (limited to 50 characters by
* DRUPAL_EXTENSION_NAME_MAX_LENGTH).
* - The module-specific namespace identifier, which defaults to the
* configuration entity type ID. Entity type IDs are limited to 32
* characters by EntityTypeInterface::ID_MAX_LENGTH.
* - The configuration entity ID.
* So, a typical configuration entity filename will look something like:
* provider_module_name.namespace_identifier.config_entity_id.yml
*
* Most file systems limit a file name's length to 255 characters, so
* ConfigBase::MAX_NAME_LENGTH restricts the full configuration object name
* to 250 characters (leaving 5 for the file extension). Therefore, in
* order to leave sufficient characters to construct a configuration ID,
* the configuration entity prefix is limited to 83 characters: up to 50
* characters for the module name, 1 for the dot, and 32 for the namespace
* identifier. This also allows modules with shorter names to define longer
* namespace identifiers if desired.
*
* @see \Drupal\Core\Config\ConfigBase::MAX_NAME_LENGTH
* @see \Drupal\Core\Config\Entity\ConfigEntityTypeInterface::getConfigPrefix()
* @see DRUPAL_EXTENSION_NAME_MAX_LENGTH
* @see \Drupal\Core\Config\Entity\ConfigEntityStorage::MAX_ID_LENGTH
* @see \Drupal\Core\Entity\EntityTypeInterface::ID_MAX_LENGTH
*/
const PREFIX_LENGTH = 83;
/**
* Gets the config prefix used by the configuration entity type.
*
* The config prefix is used to prefix configuration entity IDs when they are
* stored in the configuration system. The default config prefix is
* constructed from the name of the module that provides the entity type and
* the ID of the entity type. If a config_prefix annotation is present it will
* be used in place of the entity type ID.
*
* Prefixing with the module that provides the configuration entity type
* ensures that configuration entities depend on the module that provides the
* configuration entity type.
*
* @return string
* The config prefix.
*
* @throws \Drupal\Core\Config\ConfigPrefixLengthException
* Exception thrown when the length of the prefix exceeds PREFIX_LENGTH.
*/
public function getConfigPrefix();
/**
* Gets the config entity properties to export if declared on the annotation.
*
* @return array|NULL
* The properties to export or NULL if they can not be determine from the
* config entity type annotation.
*/
public function getPropertiesToExport();
/**
* Gets the keys that are available for fast lookup.
*
* @return string[]
* The list of lookup keys.
*/
public function getLookupKeys();
}

View file

@ -0,0 +1,183 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\DraggableListBuilder.
*/
namespace Drupal\Core\Config\Entity;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Defines a class to build a draggable listing of configuration entities.
*/
abstract class DraggableListBuilder extends ConfigEntityListBuilder implements FormInterface {
/**
* The key to use for the form element containing the entities.
*
* @var string
*/
protected $entitiesKey = 'entities';
/**
* The entities being listed.
*
* @var \Drupal\Core\Entity\EntityInterface[]
*/
protected $entities = array();
/**
* Name of the entity's weight field or FALSE if no field is provided.
*
* @var string|bool
*/
protected $weightKey = FALSE;
/**
* The form builder.
*
* @var \Drupal\Core\Form\FormBuilderInterface
*/
protected $formBuilder;
/**
* {@inheritdoc}
*/
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage) {
parent::__construct($entity_type, $storage);
// Check if the entity type supports weighting.
if ($this->entityType->hasKey('weight')) {
$this->weightKey = $this->entityType->getKey('weight');
}
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header = array();
if (!empty($this->weightKey)) {
$header['weight'] = t('Weight');
}
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
$row = array();
if (!empty($this->weightKey)) {
// Override default values to markup elements.
$row['#attributes']['class'][] = 'draggable';
$row['#weight'] = $entity->get($this->weightKey);
// Add weight column.
$row['weight'] = array(
'#type' => 'weight',
'#title' => t('Weight for @title', array('@title' => $entity->label())),
'#title_display' => 'invisible',
'#default_value' => $entity->get($this->weightKey),
'#attributes' => array('class' => array('weight')),
);
}
return $row + parent::buildRow($entity);
}
/**
* {@inheritdoc}
*/
public function render() {
if (!empty($this->weightKey)) {
return $this->formBuilder()->getForm($this);
}
return parent::render();
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form[$this->entitiesKey] = array(
'#type' => 'table',
'#header' => $this->buildHeader(),
'#empty' => t('There is no @label yet.', array('@label' => $this->entityType->getLabel())),
'#tabledrag' => array(
array(
'action' => 'order',
'relationship' => 'sibling',
'group' => 'weight',
),
),
);
$this->entities = $this->load();
$delta = 10;
// Change the delta of the weight field if have more than 20 entities.
if (!empty($this->weightKey)) {
$count = count($this->entities);
if ($count > 20) {
$delta = ceil($count / 2);
}
}
foreach ($this->entities as $entity) {
$row = $this->buildRow($entity);
if (isset($row['label'])) {
$row['label'] = array('#markup' => $row['label']);
}
if (isset($row['weight'])) {
$row['weight']['#delta'] = $delta;
}
$form[$this->entitiesKey][$entity->id()] = $row;
}
$form['actions']['#type'] = 'actions';
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => t('Save order'),
'#button_type' => 'primary',
);
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
// No validation.
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
foreach ($form_state->getValue($this->entitiesKey) as $id => $value) {
if (isset($this->entities[$id]) && $this->entities[$id]->get($this->weightKey) != $value['weight']) {
// Save entity only when its weight was changed.
$this->entities[$id]->set($this->weightKey, $value['weight']);
$this->entities[$id]->save();
}
}
}
/**
* Returns the form builder.
*
* @return \Drupal\Core\Form\FormBuilderInterface
* The form builder.
*/
protected function formBuilder() {
if (!$this->formBuilder) {
$this->formBuilder = \Drupal::formBuilder();
}
return $this->formBuilder;
}
}

View file

@ -0,0 +1,15 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\Exception\ConfigEntityIdLengthException.
*/
namespace Drupal\Core\Config\Entity\Exception;
use Drupal\Core\Config\ConfigException;
/**
* Defines an exception thrown when a configuration entity ID is too long.
*/
class ConfigEntityIdLengthException extends ConfigException {}

View file

@ -0,0 +1,15 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\Exception\ConfigEntityStorageClassException.
*/
namespace Drupal\Core\Config\Entity\Exception;
use Drupal\Core\Config\ConfigException;
/**
* Thrown when a storage class is not an instance of ConfigEntityStorage.
*/
class ConfigEntityStorageClassException extends ConfigException {}

View file

@ -0,0 +1,71 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\ImportableEntityStorageInterface.
*/
namespace Drupal\Core\Config\Entity;
use Drupal\Core\Config\Config;
/**
* Provides an interface for responding to configuration imports.
*
* When configuration is synchronized between storages, the entity storage must
* handle the synchronization of configuration data for its entity.
*/
interface ImportableEntityStorageInterface {
/**
* Creates entities upon synchronizing configuration changes.
*
* @param string $name
* The name of the configuration object.
* @param \Drupal\Core\Config\Config $new_config
* A configuration object containing the new configuration data.
* @param \Drupal\Core\Config\Config $old_config
* A configuration object containing the old configuration data.
*/
public function importCreate($name, Config $new_config, Config $old_config);
/**
* Updates entities upon synchronizing configuration changes.
*
* @param string $name
* The name of the configuration object.
* @param \Drupal\Core\Config\Config $new_config
* A configuration object containing the new configuration data.
* @param \Drupal\Core\Config\Config $old_config
* A configuration object containing the old configuration data.
*
* @throws \Drupal\Core\Config\ConfigImporterException
* Thrown when the config entity that should be updated can not be found.
*/
public function importUpdate($name, Config $new_config, Config $old_config);
/**
* Delete entities upon synchronizing configuration changes.
*
* @param string $name
* The name of the configuration object.
* @param \Drupal\Core\Config\Config $new_config
* A configuration object containing the new configuration data.
* @param \Drupal\Core\Config\Config $old_config
* A configuration object containing the old configuration data.
*/
public function importDelete($name, Config $new_config, Config $old_config);
/**
* Renames entities upon synchronizing configuration changes.
*
* @param string $old_name
* The original name of the configuration object.
* @param \Drupal\Core\Config\Config $new_config
* A configuration object containing the new configuration data.
* @param \Drupal\Core\Config\Config $old_config
* A configuration object containing the old configuration data.
*/
public function importRename($old_name, Config $new_config, Config $old_config);
}

View file

@ -0,0 +1,202 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\Query\Condition.
*/
namespace Drupal\Core\Config\Entity\Query;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Entity\Query\ConditionBase;
use Drupal\Core\Entity\Query\ConditionInterface;
use Drupal\Core\Entity\Query\QueryException;
/**
* 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' : '=';
}
// Lowercase condition value(s) for case-insensitive matches.
if (is_array($condition['value'])) {
$condition['value'] = array_map('Drupal\Component\Utility\Unicode::strtolower', $condition['value']);
}
elseif (!is_bool($condition['value'])) {
$condition['value'] = Unicode::strtolower($condition['value']);
}
$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);
if ($parent === '*') {
$candidates = array_keys($data);
}
else {
// Avoid a notice when calling match() later.
if (!isset($data[$parent])) {
$data[$parent] = NULL;
}
$candidates = array($parent);
}
foreach ($candidates as $key) {
if ($needs_matching) {
if (is_array($data[$key])) {
$new_parents = $parents;
$new_parents[] = $key;
if ($this->matchArray($condition, $data[$key], $needs_matching, $new_parents)) {
return TRUE;
}
}
}
// Only try to match a scalar if there are no remaining keys in
// $needs_matching as this indicates that we are looking for a specific
// subkey and a scalar can never match that.
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)) {
// We always want a case-insensitive match.
if (!is_bool($value)) {
$value = Unicode::strtolower($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 '<>':
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;
case 'IS NULL':
return FALSE;
default:
throw new QueryException('Invalid condition operator.');
}
}
return $condition['operator'] === 'IS NULL';
}
}

View file

@ -0,0 +1,14 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\Query\InvalidLookupKeyException.
*/
namespace Drupal\Core\Config\Entity\Query;
/**
* Exception thrown when a config entity uses an invalid lookup key.
*/
class InvalidLookupKeyException extends \LogicException {
}

View file

@ -0,0 +1,233 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\Query\Query.
*/
namespace Drupal\Core\Config\Entity\Query;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Query\QueryBase;
use Drupal\Core\Entity\Query\QueryInterface;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
/**
* Defines the entity query for configuration entities.
*/
class Query extends QueryBase implements QueryInterface {
/**
* Information about the entity type.
*
* @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface
*/
protected $entityType;
/**
* The config factory used by the config entity query.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The key value factory.
*
* @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface
*/
protected $keyValueFactory;
/**
* Constructs a Query object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @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\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_factory
* The key value factory.
* @param array $namespaces
* List of potential namespaces of the classes belonging to this query.
*/
function __construct(EntityTypeInterface $entity_type, $conjunction, ConfigFactoryInterface $config_factory, KeyValueFactoryInterface $key_value_factory, array $namespaces) {
parent::__construct($entity_type, $conjunction, $namespaces);
$this->configFactory = $config_factory;
$this->keyValueFactory = $key_value_factory;
}
/**
* 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 the relevant config records.
$configs = $this->loadRecords();
// Apply conditions.
$result = $this->condition->compile($configs);
// Apply sort settings.
foreach ($this->sort as $sort) {
$direction = $sort['direction'] == 'ASC' ? -1 : 1;
$field = $sort['field'];
uasort($result, function($a, $b) use ($field, $direction) {
return ($a[$field] <= $b[$field]) ? $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;
}
/**
* Loads the config records to examine for the query.
*
* @return array
* Config records keyed by entity IDs.
*/
protected function loadRecords() {
$prefix = $this->entityType->getConfigPrefix() . '.';
$prefix_length = strlen($prefix);
// Search the conditions for restrictions on configuration object names.
$names = FALSE;
$id_condition = NULL;
$id_key = $this->entityType->getKey('id');
if ($this->condition->getConjunction() == 'AND') {
$lookup_keys = $this->entityType->getLookupKeys();
$conditions = $this->condition->conditions();
foreach ($conditions as $condition_key => $condition) {
$operator = $condition['operator'] ?: (is_array($condition['value']) ? 'IN' : '=');
if (is_string($condition['field']) && ($operator == 'IN' || $operator == '=')) {
// Special case ID lookups.
if ($condition['field'] == $id_key) {
$ids = (array) $condition['value'];
$names = array_map(function ($id) use ($prefix) {
return $prefix . $id;
}, $ids);
}
elseif (in_array($condition['field'], $lookup_keys)) {
// If we don't find anything then there are no matches. No point in
// listing anything.
$names = array();
$keys = (array) $condition['value'];
$keys = array_map(function ($value) use ($condition) {
return $condition['field'] . ':' . $value;
}, $keys);
foreach ($this->getConfigKeyStore()->getMultiple($keys) as $list) {
$names = array_merge($names, $list);
}
}
}
// Save the first ID condition that is not an 'IN' or '=' for narrowing
// down later.
elseif (!$id_condition && $condition['field'] == $id_key) {
$id_condition = $condition;
}
// We stop at the first restricting condition on name. In the case where
// there are additional restricting conditions, results will be
// eliminated when the conditions are checked on the loaded records.
if ($names !== FALSE) {
// If the condition has been responsible for narrowing the list of
// configuration to check there is no point in checking it further.
unset($conditions[$condition_key]);
break;
}
}
}
// If no restrictions on IDs were found, we need to parse all records.
if ($names === FALSE) {
$names = $this->configFactory->listAll($prefix);
}
// In case we have an ID condition, try to narrow down the list of config
// objects to load.
if ($id_condition && !empty($names)) {
$value = $id_condition['value'];
$filter = NULL;
switch ($id_condition['operator']) {
case '<>':
$filter = function ($name) use ($value, $prefix_length) {
$id = substr($name, $prefix_length);
return $id !== $value;
};
break;
case 'STARTS_WITH':
$filter = function ($name) use ($value, $prefix_length) {
$id = substr($name, $prefix_length);
return strpos($id, $value) === 0;
};
break;
case 'CONTAINS':
$filter = function ($name) use ($value, $prefix_length) {
$id = substr($name, $prefix_length);
return strpos($id, $value) !== FALSE;
};
break;
case 'ENDS_WITH':
$filter = function ($name) use ($value, $prefix_length) {
$id = substr($name, $prefix_length);
return strrpos($id, $value) === strlen($id) - strlen($value);
};
break;
}
if ($filter) {
$names = array_filter($names, $filter);
}
}
// Load the corresponding records.
$records = array();
foreach ($this->configFactory->loadMultiple($names) as $config) {
$records[substr($config->getName(), $prefix_length)] = $config->get();
}
return $records;
}
/**
* Gets the key value store used to store fast lookups.
*
* @return \Drupal\Core\KeyValueStore\KeyValueStoreInterface
* The key value store used to store fast lookups.
*/
protected function getConfigKeyStore() {
return $this->keyValueFactory->get(QueryFactory::CONFIG_LOOKUP_PREFIX . $this->entityTypeId);
}
}

View file

@ -0,0 +1,263 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\Query\QueryFactory.
*/
namespace Drupal\Core\Config\Entity\Query;
use Drupal\Core\Config\Config;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ConfigManagerInterface;
use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Query\QueryBase;
use Drupal\Core\Entity\Query\QueryException;
use Drupal\Core\Entity\Query\QueryFactoryInterface;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Provides a factory for creating entity query objects for the config backend.
*/
class QueryFactory implements QueryFactoryInterface, EventSubscriberInterface {
/**
* The prefix for the key value collection for fast lookups.
*/
const CONFIG_LOOKUP_PREFIX = 'config.entity.key_store.';
/**
* The config factory used by the config entity query.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface;
*/
protected $configFactory;
/**
* The namespace of this class, the parent class etc.
*
* @var array
*/
protected $namespaces;
/**
* Constructs a QueryFactory object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config storage used by the config entity query.
* @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value
* The key value factory.
* @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
* The configuration manager.
*/
public function __construct(ConfigFactoryInterface $config_factory, KeyValueFactoryInterface $key_value, ConfigManagerInterface $config_manager) {
$this->configFactory = $config_factory;
$this->keyValueFactory = $key_value;
$this->configManager = $config_manager;
$this->namespaces = QueryBase::getNamespaces($this);
}
/**
* {@inheritdoc}
*/
public function get(EntityTypeInterface $entity_type, $conjunction) {
return new Query($entity_type, $conjunction, $this->configFactory, $this->keyValueFactory, $this->namespaces);
}
/**
* {@inheritdoc}
*/
public function getAggregate(EntityTypeInterface $entity_type, $conjunction) {
throw new QueryException('Aggregation over configuration entities is not supported');
}
/**
* Gets the key value store used to store fast lookups.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return \Drupal\Core\KeyValueStore\KeyValueStoreInterface
* The key value store used to store fast lookups.
*/
protected function getConfigKeyStore(EntityTypeInterface $entity_type) {
return $this->keyValueFactory->get(static::CONFIG_LOOKUP_PREFIX . $entity_type->id());
}
/**
* Updates or adds lookup data.
*
* @param \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $entity_type
* The entity type.
* @param \Drupal\Core\Config\Config $config
* The configuration object that is being saved.
*/
protected function updateConfigKeyStore(ConfigEntityTypeInterface $entity_type, Config $config) {
$config_key_store = $this->getConfigKeyStore($entity_type);
foreach ($entity_type->getLookupKeys() as $lookup_key) {
foreach ($this->getKeys($config, $lookup_key, 'get', $entity_type) as $key) {
$values = $config_key_store->get($key, []);
if (!in_array($config->getName(), $values, TRUE)) {
$values[] = $config->getName();
$config_key_store->set($key, $values);
}
}
}
}
/**
* Deletes lookup data.
*
* @param \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $entity_type
* The entity type.
* @param \Drupal\Core\Config\Config $config
* The configuration object that is being deleted.
*/
protected function deleteConfigKeyStore(ConfigEntityTypeInterface $entity_type, Config $config) {
$config_key_store = $this->getConfigKeyStore($entity_type);
foreach ($entity_type->getLookupKeys() as $lookup_key) {
foreach ($this->getKeys($config, $lookup_key, 'getOriginal', $entity_type) as $key) {
$values = $config_key_store->get($key, []);
$pos = array_search($config->getName(), $values, TRUE);
if ($pos !== FALSE) {
unset($values[$pos]);
}
if (empty($values)) {
$config_key_store->delete($key);
}
else {
$config_key_store->set($key, $values);
}
}
}
}
/**
* Creates lookup keys for configuration data.
*
* @param \Drupal\Core\Config\Config $config
* The configuration object.
* @param string $key
* The configuration key to look for.
* @param string $get_method
* Which method on the config object to call to get the value. Either 'get'
* or 'getOriginal'.
* @param \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $entity_type
* The configuration entity type.
*
* @return array
* An array of lookup keys concatenated to the configuration values.
*
* @throws \Drupal\Core\Config\Entity\Query\InvalidLookupKeyException
* The provided $key cannot end with a wildcard. This makes no sense since
* you cannot do fast lookups against this.
*/
protected function getKeys(Config $config, $key, $get_method, ConfigEntityTypeInterface $entity_type) {
if (substr($key, -1) == '*') {
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]));
}
$parts = explode('.*', $key);
// Remove leading dots.
array_walk($parts, function (&$value) {
$value = trim($value, '.');
});
$values = (array) $this->getValues($config, $parts[0], $get_method, $parts);
$output = array();
// Flatten the array to a single dimension and add the key to all the
// values.
array_walk_recursive($values, function ($current) use (&$output, $key) {
if (is_scalar($current)) {
$current = $key . ':' . $current;
}
$output[] = $current;
});
return $output;
}
/**
* Finds all the values for a configuration key in a configuration object.
*
* @param \Drupal\Core\Config\Config $config
* The configuration object.
* @param string $key
* The current key being checked.
* @param string $get_method
* Which method on the config object to call to get the value.
* @param array $parts
* All the parts of a configuration key we are checking.
* @param int $start
* Which position of $parts we are processing. Defaults to 0.
*
* @return array|NULL
* The array of configuration values the match the provided key. NULL if
* the configuration object does not have a value that corresponds to the
* key.
*/
protected function getValues(Config $config, $key, $get_method, array $parts, $start = 0) {
$value = $config->$get_method($key);
if (is_array($value)) {
$new_value = [];
$start++;
if (!isset($parts[$start])) {
// The configuration object does not have a value that corresponds to
// the key.
return NULL;
}
foreach (array_keys($value) as $key_bit) {
$new_key = $key . '.' . $key_bit;
if (!empty($parts[$start])) {
$new_key .= '.' . $parts[$start];
}
$new_value[] = $this->getValues($config, $new_key, $get_method, $parts, $start);
}
$value = $new_value;
}
return $value;
}
/**
* Updates configuration entity in the key store.
*
* @param ConfigCrudEvent $event
* The configuration event.
*/
public function onConfigSave(ConfigCrudEvent $event) {
$saved_config = $event->getConfig();
$entity_type_id = $this->configManager->getEntityTypeIdByName($saved_config->getName());
if ($entity_type_id) {
$entity_type = $this->configManager->getEntityManager()->getDefinition($entity_type_id);
$this->updateConfigKeyStore($entity_type, $saved_config);
}
}
/**
* Removes configuration entity from key store.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
* The configuration event.
*/
public function onConfigDelete(ConfigCrudEvent $event) {
$saved_config = $event->getConfig();
$entity_type_id = $this->configManager->getEntityTypeIdByName($saved_config->getName());
if ($entity_type_id) {
$entity_type = $this->configManager->getEntityManager()->getDefinition($entity_type_id);
$this->deleteConfigKeyStore($entity_type, $saved_config);
}
}
/**
* {@inheritdoc}
*/
static function getSubscribedEvents() {
$events[ConfigEvents::SAVE][] = array('onConfigSave', 128);
$events[ConfigEvents::DELETE][] = array('onConfigDelete', 128);
return $events;
}
}

View file

@ -0,0 +1,82 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\ThirdPartySettingsInterface.
*/
namespace Drupal\Core\Config\Entity;
/**
* Interface for configuration entities to store third party information.
*
* A third party is a module that needs to store tightly coupled information to
* the configuration entity. For example, a module alters the node type form
* can use this to store its configuration so that it will be deployed with the
* node type.
*/
interface ThirdPartySettingsInterface {
/**
* Sets the value of a third-party setting.
*
* @param string $module
* The module providing the third-party setting.
* @param string $key
* The setting name.
* @param mixed $value
* The setting value.
*
* @return $this
*/
public function setThirdPartySetting($module, $key, $value);
/**
* Gets the value of a third-party setting.
*
* @param string $module
* The module providing the third-party setting.
* @param string $key
* The setting name.
* @param mixed $default
* The default value
*
* @return mixed
* The value.
*/
public function getThirdPartySetting($module, $key, $default = NULL);
/**
* Gets all third-party settings of a given module.
*
* @param string $module
* The module providing the third-party settings.
*
* @return array
* An array of key-value pairs.
*/
public function getThirdPartySettings($module);
/**
* Unsets a third-party setting.
*
* @param string $module
* The module providing the third-party setting.
* @param string $key
* The setting name.
*
* @return mixed
* The value.
*/
public function unsetThirdPartySetting($module, $key);
/**
* Gets the list of third parties that store information.
*
* @return array
* The list of third parties.
*/
public function getThirdPartyProviders();
}

View file

@ -0,0 +1,140 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\ExtensionInstallStorage.
*/
namespace Drupal\Core\Config;
use Drupal\Core\Site\Settings;
use Drupal\Core\Extension\ExtensionDiscovery;
/**
* Storage to access configuration and schema in enabled extensions.
*
* @see \Drupal\Core\Config\ConfigInstaller
* @see \Drupal\Core\Config\TypedConfigManager
*/
class ExtensionInstallStorage extends InstallStorage {
/**
* The active configuration store.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $configStorage;
/**
* Flag to include the profile in the list of enabled modules.
*
* @var bool
*/
protected $includeProfile = TRUE;
/**
* Overrides \Drupal\Core\Config\InstallStorage::__construct().
*
* @param \Drupal\Core\Config\StorageInterface $config_storage
* The active configuration store where the list of enabled modules and
* themes is stored.
* @param string $directory
* The directory to scan in each extension to scan for files. Defaults to
* 'config/install'.
* @param string $collection
* (optional) The collection to store configuration in. Defaults to the
* default collection.
* @param bool $include_profile
* (optional) Whether to include the install profile in extensions to
* search and to get overrides from.
*/
public function __construct(StorageInterface $config_storage, $directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION, $include_profile = TRUE) {
$this->configStorage = $config_storage;
$this->directory = $directory;
$this->collection = $collection;
$this->includeProfile = $include_profile;
}
/**
* {@inheritdoc}
*/
public function createCollection($collection) {
return new static(
$this->configStorage,
$this->directory,
$collection
);
}
/**
* Returns a map of all config object names and their folders.
*
* The list is based on enabled modules and themes. The active configuration
* storage is used rather than \Drupal\Core\Extension\ModuleHandler and
* \Drupal\Core\Extension\ThemeHandler in order to resolve circular
* dependencies between these services and \Drupal\Core\Config\ConfigInstaller
* and \Drupal\Core\Config\TypedConfigManager.
*
* @return array
* An array mapping config object names with directories.
*/
protected function getAllFolders() {
if (!isset($this->folders)) {
$this->folders = array();
$this->folders += $this->getCoreNames();
$install_profile = Settings::get('install_profile');
$profile = drupal_get_profile();
$extensions = $this->configStorage->read('core.extension');
// @todo Remove this scan as part of https://www.drupal.org/node/2186491
$listing = new ExtensionDiscovery(\Drupal::root());
if (!empty($extensions['module'])) {
$modules = $extensions['module'];
// Remove the install profile as this is handled later.
unset($modules[$install_profile]);
$profile_list = $listing->scan('profile');
if ($profile && isset($profile_list[$profile])) {
// Prime the drupal_get_filename() static cache with the profile info
// file location so we can use drupal_get_path() on the active profile
// during the module scan.
// @todo Remove as part of https://www.drupal.org/node/2186491
drupal_get_filename('profile', $profile, $profile_list[$profile]->getPathname());
}
$module_list_scan = $listing->scan('module');
$module_list = array();
foreach (array_keys($modules) as $module) {
if (isset($module_list_scan[$module])) {
$module_list[$module] = $module_list_scan[$module];
}
}
$this->folders += $this->getComponentNames($module_list);
}
if (!empty($extensions['theme'])) {
$theme_list_scan = $listing->scan('theme');
foreach (array_keys($extensions['theme']) as $theme) {
if (isset($theme_list_scan[$theme])) {
$theme_list[$theme] = $theme_list_scan[$theme];
}
}
$this->folders += $this->getComponentNames($theme_list);
}
if ($this->includeProfile) {
// The install profile can override module default configuration. We do
// this by replacing the config file path from the module/theme with the
// install profile version if there are any duplicates.
if (isset($profile)) {
if (!isset($profile_list)) {
$profile_list = $listing->scan('profile');
}
if (isset($profile_list[$profile])) {
$profile_folders = $this->getComponentNames(array($profile_list[$profile]));
$this->folders = $profile_folders + $this->folders;
}
}
}
}
return $this->folders;
}
}

View file

@ -0,0 +1,338 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\FileStorage.
*/
namespace Drupal\Core\Config;
use Drupal\Component\Serialization\Yaml;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\Component\Utility\SafeMarkup;
/**
* Defines the file storage.
*/
class FileStorage implements StorageInterface {
/**
* The storage collection.
*
* @var string
*/
protected $collection;
/**
* The filesystem path for configuration objects.
*
* @var string
*/
protected $directory = '';
/**
* Constructs a new FileStorage.
*
* @param string $directory
* A directory path to use for reading and writing of configuration files.
* @param string $collection
* (optional) The collection to store configuration in. Defaults to the
* default collection.
*/
public function __construct($directory, $collection = StorageInterface::DEFAULT_COLLECTION) {
$this->directory = $directory;
$this->collection = $collection;
}
/**
* Returns the path to the configuration file.
*
* @return string
* The path to the configuration file.
*/
public function getFilePath($name) {
return $this->getCollectionDirectory() . '/' . $name . '.' . static::getFileExtension();
}
/**
* Returns the file extension used by the file storage for all configuration files.
*
* @return string
* The file extension.
*/
public static function getFileExtension() {
return 'yml';
}
/**
* Check if the directory exists and create it if not.
*/
protected function ensureStorage() {
$dir = $this->getCollectionDirectory();
$success = file_prepare_directory($dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
// Only create .htaccess file in root directory.
if ($dir == $this->directory) {
$success = $success && file_save_htaccess($this->directory, TRUE, TRUE);
}
if (!$success) {
throw new StorageException('Failed to create config directory ' . $dir);
}
return $this;
}
/**
* Implements Drupal\Core\Config\StorageInterface::exists().
*/
public function exists($name) {
return file_exists($this->getFilePath($name));
}
/**
* Implements Drupal\Core\Config\StorageInterface::read().
*
* @throws \Drupal\Core\Config\UnsupportedDataTypeConfigException
*/
public function read($name) {
if (!$this->exists($name)) {
return FALSE;
}
$data = file_get_contents($this->getFilePath($name));
try {
$data = $this->decode($data);
}
catch (InvalidDataTypeException $e) {
throw new UnsupportedDataTypeConfigException(SafeMarkup::format('Invalid data type in config @name: !message', array(
'@name' => $name,
'!message' => $e->getMessage(),
)));
}
return $data;
}
/**
* {@inheritdoc}
*/
public function readMultiple(array $names) {
$list = array();
foreach ($names as $name) {
if ($data = $this->read($name)) {
$list[$name] = $data;
}
}
return $list;
}
/**
* {@inheritdoc}
*/
public function write($name, array $data) {
try {
$data = $this->encode($data);
}
catch (InvalidDataTypeException $e) {
throw new StorageException(SafeMarkup::format('Invalid data type in config @name: !message', array(
'@name' => $name,
'!message' => $e->getMessage(),
)));
}
$target = $this->getFilePath($name);
$status = @file_put_contents($target, $data);
if ($status === FALSE) {
// Try to make sure the directory exists and try writing again.
$this->ensureStorage();
$status = @file_put_contents($target, $data);
}
if ($status === FALSE) {
throw new StorageException('Failed to write configuration file: ' . $this->getFilePath($name));
}
else {
drupal_chmod($target);
}
return TRUE;
}
/**
* Implements Drupal\Core\Config\StorageInterface::delete().
*/
public function delete($name) {
if (!$this->exists($name)) {
$dir = $this->getCollectionDirectory();
if (!file_exists($dir)) {
throw new StorageException($dir . '/ not found.');
}
return FALSE;
}
return drupal_unlink($this->getFilePath($name));
}
/**
* Implements Drupal\Core\Config\StorageInterface::rename().
*/
public function rename($name, $new_name) {
$status = @rename($this->getFilePath($name), $this->getFilePath($new_name));
if ($status === FALSE) {
throw new StorageException('Failed to rename configuration file from: ' . $this->getFilePath($name) . ' to: ' . $this->getFilePath($new_name));
}
return TRUE;
}
/**
* Implements Drupal\Core\Config\StorageInterface::encode().
*/
public function encode($data) {
return Yaml::encode($data);
}
/**
* Implements Drupal\Core\Config\StorageInterface::decode().
*/
public function decode($raw) {
$data = Yaml::decode($raw);
// A simple string is valid YAML for any reason.
if (!is_array($data)) {
return FALSE;
}
return $data;
}
/**
* Implements Drupal\Core\Config\StorageInterface::listAll().
*/
public function listAll($prefix = '') {
// glob() silently ignores the error of a non-existing search directory,
// even with the GLOB_ERR flag.
$dir = $this->getCollectionDirectory();
if (!file_exists($dir)) {
return array();
}
$extension = '.' . static::getFileExtension();
// \GlobIterator on Windows requires an absolute path.
$files = new \GlobIterator(realpath($dir) . '/' . $prefix . '*' . $extension);
$names = array();
foreach ($files as $file) {
$names[] = $file->getBasename($extension);
}
return $names;
}
/**
* Implements Drupal\Core\Config\StorageInterface::deleteAll().
*/
public function deleteAll($prefix = '') {
$success = TRUE;
$files = $this->listAll($prefix);
foreach ($files as $name) {
if (!$this->delete($name) && $success) {
$success = FALSE;
}
}
if ($success && $this->collection != StorageInterface::DEFAULT_COLLECTION) {
// Remove empty directories.
if (!(new \FilesystemIterator($this->getCollectionDirectory()))->valid()) {
drupal_rmdir($this->getCollectionDirectory());
}
}
return $success;
}
/**
* {@inheritdoc}
*/
public function createCollection($collection) {
return new static(
$this->directory,
$collection
);
}
/**
* {@inheritdoc}
*/
public function getCollectionName() {
return $this->collection;
}
/**
* {@inheritdoc}
*/
public function getAllCollectionNames() {
$collections = $this->getAllCollectionNamesHelper($this->directory);
sort($collections);
return $collections;
}
/**
* Helper function for getAllCollectionNames().
*
* If the file storage has the following subdirectory structure:
* ./another_collection/one
* ./another_collection/two
* ./collection/sub/one
* ./collection/sub/two
* this function will return:
* @code
* array(
* 'another_collection.one',
* 'another_collection.two',
* 'collection.sub.one',
* 'collection.sub.two',
* );
* @endcode
*
* @param string $directory
* The directory to check for sub directories. This allows this
* function to be used recursively to discover all the collections in the
* storage.
*
* @return array
* A list of collection names contained within the provided directory.
*/
protected function getAllCollectionNamesHelper($directory) {
$collections = array();
foreach (new \DirectoryIterator($directory) as $fileinfo) {
if ($fileinfo->isDir() && !$fileinfo->isDot()) {
$collection = $fileinfo->getFilename();
// Recursively call getAllCollectionNamesHelper() to discover if there
// are subdirectories. Subdirectories represent a dotted collection
// name.
$sub_collections = $this->getAllCollectionNamesHelper($directory . '/' . $collection);
if (!empty($sub_collections)) {
// Build up the collection name by concatenating the subdirectory
// names with the current directory name.
foreach ($sub_collections as $sub_collection) {
$collections[] = $collection . '.' . $sub_collection;
}
}
// Check that the collection is valid by searching if for configuration
// objects. A directory without any configuration objects is not a valid
// collection.
// \GlobIterator on Windows requires an absolute path.
$files = new \GlobIterator(realpath($directory . '/' . $collection) . '/*.' . $this->getFileExtension());
if (count($files)) {
$collections[] = $collection;
}
}
}
return $collections;
}
/**
* Gets the directory for the collection.
*
* @return string
* The directory for the collection.
*/
protected function getCollectionDirectory() {
if ($this->collection == StorageInterface::DEFAULT_COLLECTION) {
$dir = $this->directory;
}
else {
$dir = $this->directory . '/' . str_replace('.', '/', $this->collection);
}
return $dir;
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\FileStorageFactory.
*/
namespace Drupal\Core\Config;
/**
* Provides a factory for creating config file storage objects.
*/
class FileStorageFactory {
/**
* Returns a FileStorage object working with the active config directory.
*
* @return \Drupal\Core\Config\FileStorage FileStorage
*/
static function getActive() {
return new FileStorage(config_get_config_directory(CONFIG_ACTIVE_DIRECTORY));
}
/**
* Returns a FileStorage object working with the staging config directory.
*
* @return \Drupal\Core\Config\FileStorage FileStorage
*/
static function getStaging() {
return new FileStorage(config_get_config_directory(CONFIG_STAGING_DIRECTORY));
}
}

View file

@ -0,0 +1,61 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\ImmutableConfig.
*/
namespace Drupal\Core\Config;
use Drupal\Component\Utility\SafeMarkup;
/**
* Defines the immutable configuration object.
*
* Encapsulates all capabilities needed for runtime configuration handling
* except being able to change the configuration.
*
* If you need to be able to change configuration use
* \Drupal\Core\Form\ConfigFormBaseTrait or
* \Drupal\Core\Config\ConfigFactoryInterface::getEditable().
*
* @see \Drupal\Core\Form\ConfigFormBaseTrait
* @see \Drupal\Core\Config\ConfigFactoryInterface::getEditable()
* @see \Drupal\Core\Config\ConfigFactoryInterface::get()
*
* @ingroup config_api
*/
class ImmutableConfig extends Config {
/**
* {@inheritdoc}
*/
public function set($key, $value) {
throw new ImmutableConfigException(SafeMarkup::format('Can not set values on immutable configuration !name:!key. Use \Drupal\Core\Config\ConfigFactoryInterface::getEditable() to retrieve a mutable configuration object', ['!name' => $this->getName(), '!key' => $key]));
}
/**
* {@inheritdoc}
*/
public function clear($key) {
throw new ImmutableConfigException(SafeMarkup::format('Can not clear !key key in immutable configuration !name. Use \Drupal\Core\Config\ConfigFactoryInterface::getEditable() to retrieve a mutable configuration object', ['!name' => $this->getName(), '!key' => $key]));
}
/**
* {@inheritdoc}
*/
public function save($has_trusted_data = FALSE) {
throw new ImmutableConfigException(SafeMarkup::format('Can not save immutable configuration !name. Use \Drupal\Core\Config\ConfigFactoryInterface::getEditable() to retrieve a mutable configuration object', ['!name' => $this->getName()]));
}
/**
* Deletes the configuration object.
*
* @return \Drupal\Core\Config\Config
* The configuration object.
*/
public function delete() {
throw new ImmutableConfigException(SafeMarkup::format('Can not delete immutable configuration !name. Use \Drupal\Core\Config\ConfigFactoryInterface::getEditable() to retrieve a mutable configuration object', ['!name' => $this->getName()]));
}
}

View file

@ -0,0 +1,16 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\ImmutableConfigException.
*/
namespace Drupal\Core\Config;
/**
* Exception throw when an immutable config object is altered.
*
* @see \Drupal\Core\Config\ImmutableConfig
*/
class ImmutableConfigException extends \LogicException {
}

View file

@ -0,0 +1,45 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Importer\FinalMissingContentSubscriber.
*/
namespace Drupal\Core\Config\Importer;
use Drupal\Core\Config\ConfigEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Final event subscriber to the missing content event.
*
* Ensure that all missing content dependencies are removed from the event so
* the importer can complete.
*
* @see \Drupal\Core\Config\ConfigImporter::processMissingContent()
*/
class FinalMissingContentSubscriber implements EventSubscriberInterface {
/**
* Handles the missing content event.
*
* @param \Drupal\Core\Config\Importer\MissingContentEvent $event
* The missing content event.
*/
public function onMissingContent(MissingContentEvent $event) {
foreach (array_keys($event->getMissingContent()) as $uuid) {
$event->resolveMissingContent($uuid);
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
// This should always be the final event as it will mark all content
// dependencies as resolved.
$events[ConfigEvents::IMPORT_MISSING_CONTENT][] = array('onMissingContent', -1024);
return $events;
}
}

View file

@ -0,0 +1,64 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Importer\MissingContentEvent.
*/
namespace Drupal\Core\Config\Importer;
use Symfony\Component\EventDispatcher\Event;
/**
* Wraps a configuration event for event listeners.
*
* @see \Drupal\Core\Config\Config\ConfigEvents::IMPORT_MISSING_CONTENT
*/
class MissingContentEvent extends Event {
/**
* A list of missing content dependencies.
*
* @var array
*/
protected $missingContent;
/**
* Constructs a configuration import missing content event object.
*
* @param array $missing_content
* Missing content information.
*/
public function __construct(array $missing_content) {
$this->missingContent = $missing_content;
}
/**
* Gets missing content information.
*
* @return array
* A list of missing content dependencies. The array is keyed by UUID. Each
* value is an array with the following keys: 'entity_type', 'bundle' and
* 'uuid'.
*/
public function getMissingContent() {
return $this->missingContent;
}
/**
* Resolves the missing content by removing it from the list.
*
* @param string $uuid
* The UUID of the content entity to mark resolved.
*
* @return $this
* The MissingContentEvent object.
*/
public function resolveMissingContent($uuid) {
if (isset($this->missingContent[$uuid])) {
unset($this->missingContent[$uuid]);
}
return $this;
}
}

View file

@ -0,0 +1,268 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\InstallStorage.
*/
namespace Drupal\Core\Config;
use Drupal\Core\Extension\ExtensionDiscovery;
use Drupal\Core\Extension\Extension;
/**
* Storage used by the Drupal installer.
*
* This storage performs a full filesystem scan to discover all available
* extensions and reads from all default config directories that exist.
*
* This special implementation MUST NOT be used anywhere else than the early
* installer environment.
*
* @see \Drupal\Core\DependencyInjection\InstallServiceProvider
*/
class InstallStorage extends FileStorage {
/**
* Extension sub-directory containing default configuration for installation.
*/
const CONFIG_INSTALL_DIRECTORY = 'config/install';
/**
* Extension sub-directory containing optional configuration for installation.
*/
const CONFIG_OPTIONAL_DIRECTORY = 'config/optional';
/**
* Extension sub-directory containing configuration schema.
*/
const CONFIG_SCHEMA_DIRECTORY = 'config/schema';
/**
* Folder map indexed by configuration name.
*
* @var array
*/
protected $folders;
/**
* The directory to scan in each extension to scan for files.
*
* @var string
*/
protected $directory;
/**
* Constructs an InstallStorage object.
*
* @param string $directory
* The directory to scan in each extension to scan for files. Defaults to
* 'config/install'.
* @param string $collection
* (optional) The collection to store configuration in. Defaults to the
* default collection.
*/
public function __construct($directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION) {
$this->directory = $directory;
$this->collection = $collection;
}
/**
* Overrides Drupal\Core\Config\FileStorage::getFilePath().
*
* Returns the path to the configuration file.
*
* Determines the owner and path to the default configuration file of a
* requested config object name located in the installation profile, a module,
* or a theme (in this order).
*
* @return string
* The path to the configuration file.
*
* @todo Improve this when figuring out how we want to handle configuration in
* installation profiles. E.g., a config object actually has to be searched
* in the profile first (whereas the profile is never the owner), only
* afterwards check for a corresponding module or theme.
*/
public function getFilePath($name) {
$folders = $this->getAllFolders();
if (isset($folders[$name])) {
return $folders[$name] . '/' . $name . '.' . $this->getFileExtension();
}
// If any code in the early installer requests a configuration object that
// does not exist anywhere as default config, then that must be mistake.
throw new StorageException(format_string('Missing configuration file: @name', array(
'@name' => $name,
)));
}
/**
* {@inheritdoc}
*/
public function exists($name) {
return array_key_exists($name, $this->getAllFolders());
}
/**
* Overrides Drupal\Core\Config\FileStorage::write().
*
* @throws \Drupal\Core\Config\StorageException
*/
public function write($name, array $data) {
throw new StorageException('Write operation is not allowed.');
}
/**
* Overrides Drupal\Core\Config\FileStorage::delete().
*
* @throws \Drupal\Core\Config\StorageException
*/
public function delete($name) {
throw new StorageException('Delete operation is not allowed.');
}
/**
* Overrides Drupal\Core\Config\FileStorage::rename().
*
* @throws \Drupal\Core\Config\StorageException
*/
public function rename($name, $new_name) {
throw new StorageException('Rename operation is not allowed.');
}
/**
* Implements Drupal\Core\Config\StorageInterface::listAll().
*/
public function listAll($prefix = '') {
$names = array_keys($this->getAllFolders());
if (!$prefix) {
return $names;
}
else {
$return = array();
foreach ($names as $index => $name) {
if (strpos($name, $prefix) === 0 ) {
$return[$index] = $names[$index];
}
}
return $return;
}
}
/**
* Returns a map of all config object names and their folders.
*
* @return array
* An array mapping config object names with directories.
*/
protected function getAllFolders() {
if (!isset($this->folders)) {
$this->folders = array();
$this->folders += $this->getCoreNames();
// Perform an ExtensionDiscovery scan as we cannot use drupal_get_path()
// yet because the system module may not yet be enabled during install.
// @todo Remove as part of https://www.drupal.org/node/2186491
$listing = new ExtensionDiscovery(\Drupal::root());
if ($profile = drupal_get_profile()) {
$profile_list = $listing->scan('profile');
if (isset($profile_list[$profile])) {
// Prime the drupal_get_filename() static cache with the profile info
// file location so we can use drupal_get_path() on the active profile
// during the module scan.
// @todo Remove as part of https://www.drupal.org/node/2186491
drupal_get_filename('profile', $profile, $profile_list[$profile]->getPathname());
$this->folders += $this->getComponentNames(array($profile_list[$profile]));
}
}
// @todo Remove as part of https://www.drupal.org/node/2186491
$this->folders += $this->getComponentNames($listing->scan('module'));
$this->folders += $this->getComponentNames($listing->scan('theme'));
}
return $this->folders;
}
/**
* Get all configuration names and folders for a list of modules or themes.
*
* @param \Drupal\Core\Extension\Extension[] $list
* An associative array of Extension objects, keyed by extension name.
*
* @return array
* Folders indexed by configuration name.
*/
public function getComponentNames(array $list) {
$extension = '.' . $this->getFileExtension();
$folders = array();
foreach ($list as $extension_object) {
// We don't have to use ExtensionDiscovery here because our list of
// extensions was already obtained through an ExtensionDiscovery scan.
$directory = $this->getComponentFolder($extension_object);
if (file_exists($directory)) {
$files = new \GlobIterator(\Drupal::root() . '/' . $directory . '/*' . $extension);
foreach ($files as $file) {
$folders[$file->getBasename($extension)] = $directory;
}
}
}
return $folders;
}
/**
* Get all configuration names and folders for Drupal core.
*
* @return array
* Folders indexed by configuration name.
*/
public function getCoreNames() {
$extension = '.' . $this->getFileExtension();
$folders = array();
$directory = $this->getCoreFolder();
if (file_exists($directory)) {
$files = new \GlobIterator(\Drupal::root() . '/' . $directory . '/*' . $extension);
foreach ($files as $file) {
$folders[$file->getBasename($extension)] = $directory;
}
}
return $folders;
}
/**
* Get folder inside each component that contains the files.
*
* @param \Drupal\Core\Extension\Extension $extension
* The Extension object for the component.
*
* @return string
* The configuration folder name for this component.
*/
protected function getComponentFolder(Extension $extension) {
return $extension->getPath() . '/' . $this->getCollectionDirectory();
}
/**
* Get folder inside Drupal core that contains the files.
*
* @return string
* The configuration folder name for core.
*/
protected function getCoreFolder() {
return drupal_get_path('core', 'core') . '/' . $this->getCollectionDirectory();
}
/**
* Overrides Drupal\Core\Config\FileStorage::deleteAll().
*
* @throws \Drupal\Core\Config\StorageException
*/
public function deleteAll($prefix = '') {
throw new StorageException('Delete operation is not allowed.');
}
/**
* Resets the static cache.
*/
public function reset() {
$this->folders = NULL;
}
}

View file

@ -0,0 +1,117 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\NullStorage.
*/
namespace Drupal\Core\Config;
/**
* Defines a stub storage.
*
* This storage is always empty; the controller reads and writes nothing.
*
* The stub implementation is needed for synchronizing configuration during
* installation of a module, in which case all configuration being shipped with
* the module is known to be new. Therefore, the module installation process is
* able to short-circuit the full diff against the active configuration; the
* diff would yield all currently available configuration as items to remove,
* since they do not exist in the module's default configuration directory.
*
* This also can be used for testing purposes.
*/
class NullStorage implements StorageInterface {
/**
* Implements Drupal\Core\Config\StorageInterface::exists().
*/
public function exists($name) {
return FALSE;
}
/**
* Implements Drupal\Core\Config\StorageInterface::read().
*/
public function read($name) {
return array();
}
/**
* Implements Drupal\Core\Config\StorageInterface::readMultiple().
*/
public function readMultiple(array $names) {
return array();
}
/**
* Implements Drupal\Core\Config\StorageInterface::write().
*/
public function write($name, array $data) {
return FALSE;
}
/**
* Implements Drupal\Core\Config\StorageInterface::delete().
*/
public function delete($name) {
return FALSE;
}
/**
* Implements Drupal\Core\Config\StorageInterface::rename().
*/
public function rename($name, $new_name) {
return FALSE;
}
/**
* Implements Drupal\Core\Config\StorageInterface::encode().
*/
public function encode($data) {
return $data;
}
/**
* Implements Drupal\Core\Config\StorageInterface::decode().
*/
public function decode($raw) {
return $raw;
}
/**
* Implements Drupal\Core\Config\StorageInterface::listAll().
*/
public function listAll($prefix = '') {
return array();
}
/**
* Implements Drupal\Core\Config\StorageInterface::deleteAll().
*/
public function deleteAll($prefix = '') {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function createCollection($collection) {
// No op.
}
/**
* {@inheritdoc}
*/
public function getAllCollectionNames() {
return array();
}
/**
* {@inheritdoc}
*/
public function getCollectionName() {
return '';
}
}

View file

@ -0,0 +1,101 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\PreExistingConfigException.
*/
namespace Drupal\Core\Config;
use Drupal\Component\Utility\SafeMarkup;
/**
* An exception thrown if configuration with the same name already exists.
*/
class PreExistingConfigException extends ConfigException {
/**
* A list of configuration objects that already exist in active configuration.
*
* @var array
*/
protected $configObjects = [];
/**
* The name of the module that is being installed.
*
* @var string
*/
protected $extension;
/**
* Gets the list of configuration objects that already exist.
*
* @return array
* A list of configuration objects that already exist in active
* configuration keyed by collection.
*/
public function getConfigObjects() {
return $this->configObjects;
}
/**
* Gets the name of the extension that is being installed.
*
* @return string
* The name of the extension that is being installed.
*/
public function getExtension() {
return $this->extension;
}
/**
* Creates an exception for an extension and a list of configuration objects.
*
* @param $extension
* The name of the extension that is being installed.
* @param array $config_objects
* A list of configuration objects that already exist in active
* configuration, keyed by config collection.
*
* @return \Drupal\Core\Config\PreExistingConfigException
*/
public static function create($extension, array $config_objects) {
$message = SafeMarkup::format('Configuration objects (@config_names) provided by @extension already exist in active configuration',
array(
'@config_names' => implode(', ', static::flattenConfigObjects($config_objects)),
'@extension' => $extension
)
);
$e = new static($message);
$e->configObjects = $config_objects;
$e->extension = $extension;
return $e;
}
/**
* Flattens the config object array to a single dimensional list.
*
* @param array $config_objects
* A list of configuration objects that already exist in active
* configuration, keyed by config collection.
*
* @return array
* A list of configuration objects that have been prefixed with their
* collection.
*/
public static function flattenConfigObjects(array $config_objects) {
$flat_config_objects = array();
foreach ($config_objects as $collection => $config_names) {
$config_names = array_map(function ($config_name) use ($collection) {
if ($collection != StorageInterface::DEFAULT_COLLECTION) {
$config_name = str_replace('.', DIRECTORY_SEPARATOR, $collection) . DIRECTORY_SEPARATOR . $config_name;
}
return $config_name;
}, $config_names);
$flat_config_objects = array_merge($flat_config_objects, $config_names);
}
return $flat_config_objects;
}
}

View file

@ -0,0 +1,190 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Schema\ArrayElement.
*/
namespace Drupal\Core\Config\Schema;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\TypedData\TypedData;
/**
* Defines a generic configuration element that contains multiple properties.
*/
abstract class ArrayElement extends TypedData implements \IteratorAggregate, TypedConfigInterface {
/**
* The typed config manager.
*
* @var \Drupal\Core\Config\TypedConfigManagerInterface
*/
protected $typedConfig;
/**
* The configuration value.
*
* @var mixed
*/
protected $value;
/**
* Parsed elements.
*/
protected $elements;
/**
* Gets valid configuration data keys.
*
* @return array
* Array of valid configuration data keys.
*/
protected function getAllKeys() {
return is_array($this->value) ? array_keys($this->value) : array();
}
/**
* Builds an array of contained elements.
*
* @return \Drupal\Core\TypedData\TypedDataInterface[]
* An array of elements contained in this element.
*/
protected function parse() {
$elements = array();
foreach ($this->getAllKeys() as $key) {
$value = isset($this->value[$key]) ? $this->value[$key] : NULL;
$definition = $this->getElementDefinition($key);
$elements[$key] = $this->createElement($definition, $value, $key);
}
return $elements;
}
/**
* Gets data definition object for contained element.
*
* @param int|string $key
* Property name or index of the element.
*
* @return \Drupal\Core\TypedData\DataDefinitionInterface
*/
protected abstract function getElementDefinition($key);
/**
* {@inheritdoc}
*/
public function get($name) {
$parts = explode('.', $name);
$root_key = array_shift($parts);
$elements = $this->getElements();
if (isset($elements[$root_key])) {
$element = $elements[$root_key];
// If $property_name contained a dot recurse into the keys.
while ($element && ($key = array_shift($parts)) !== NULL) {
if ($element instanceof TypedConfigInterface) {
$element = $element->get($key);
}
else {
$element = NULL;
}
}
}
if (isset($element)) {
return $element;
}
else {
throw new \InvalidArgumentException(SafeMarkup::format("The configuration property @key doesn't exist.", array('@key' => $name)));
}
}
/**
* {@inheritdoc}
*/
public function getElements() {
if (!isset($this->elements)) {
$this->elements = $this->parse();
}
return $this->elements;
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
return empty($this->value);
}
/**
* {@inheritdoc}
*/
public function toArray() {
return isset($this->value) ? $this->value : array();
}
/**
* {@inheritdoc}
*/
public function onChange($name) {
// Notify the parent of changes.
if (isset($this->parent)) {
$this->parent->onChange($this->name);
}
}
/**
* Implements IteratorAggregate::getIterator();
*/
public function getIterator() {
return new \ArrayIterator($this->getElements());
}
/**
* Creates a contained typed configuration object.
*
* @param \Drupal\Core\TypedData\DataDefinitionInterface $definition
* The data definition object.
* @param mixed $value
* (optional) The data value. If set, it has to match one of the supported
* data type format as documented for the data type classes.
* @param string $key
* The key of the contained element.
*
* @return \Drupal\Core\TypedData\TypedDataInterface
*/
protected function createElement($definition, $value, $key) {
return $this->typedConfig->create($definition, $value, $key, $this);
}
/**
* Creates a new data definition object from a type definition array and
* actual configuration data.
*
* @param array $definition
* The base type definition array, for which a data definition should be
* created.
* @param $value
* The value of the configuration element.
* @param string $key
* The key of the contained element.
*
* @return \Drupal\Core\TypedData\DataDefinitionInterface
*/
protected function buildDataDefinition($definition, $value, $key) {
return $this->typedConfig->buildDataDefinition($definition, $value, $key, $this);
}
/**
* Sets the typed config manager on the instance.
*
* This must be called immediately after construction to enable
* self::parseElement() and self::buildDataDefinition() to work.
*
* @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config
*/
public function setTypedConfig(TypedConfigManagerInterface $typed_config) {
$this->typedConfig = $typed_config;
}
}

View file

@ -0,0 +1,14 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Schema\ConfigSchemaAlterException.
*/
namespace Drupal\Core\Config\Schema;
/**
* Exception for when hook_config_schema_info_alter() adds or removes schema.
*/
class ConfigSchemaAlterException extends \RuntimeException {
}

View file

@ -0,0 +1,50 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Schema\ConfigSchemaDiscovery.
*/
namespace Drupal\Core\Config\Schema;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
use Drupal\Component\Plugin\Discovery\DiscoveryTrait;
use Drupal\Core\Config\StorageInterface;
/**
* Allows YAML files to define config schema types.
*/
class ConfigSchemaDiscovery implements DiscoveryInterface {
use DiscoveryTrait;
/**
* A storage instance for reading configuration schema data.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $schemaStorage;
/**
* Constructs a ConfigSchemaDiscovery object.
*
* @param $schema_storage
* The storage object to use for reading schema data.
*/
function __construct(StorageInterface $schema_storage) {
$this->schemaStorage = $schema_storage;
}
/**
* {@inheritdoc}
*/
public function getDefinitions() {
$definitions = array();
foreach ($this->schemaStorage->readMultiple($this->schemaStorage->listAll()) as $schema) {
foreach ($schema as $type => $definition) {
$definitions[$type] = $definition;
}
}
return $definitions;
}
}

View file

@ -0,0 +1,24 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Schema\Element.
*/
namespace Drupal\Core\Config\Schema;
use Drupal\Core\TypedData\TypedData;
/**
* Defines a generic configuration element.
*/
abstract class Element extends TypedData {
/**
* The configuration value.
*
* @var mixed
*/
protected $value;
}

View file

@ -0,0 +1,15 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Schema\Ignore.
*/
namespace Drupal\Core\Config\Schema;
/**
* Configuration property to ignore.
*/
class Ignore extends Element {
}

View file

@ -0,0 +1,34 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Schema\Mapping.
*/
namespace Drupal\Core\Config\Schema;
/**
* Defines a mapping configuration element.
*
* This object may contain any number and type of nested properties and each
* property key may have its own definition in the 'mapping' property of the
* configuration schema.
*
* Properties in the configuration value that are not defined in the mapping
* will get the 'undefined' data type.
*
* Read https://www.drupal.org/node/1905070 for more details about configuration
* schema, types and type resolution.
*/
class Mapping extends ArrayElement {
/**
* {@inheritdoc}
*/
protected function getElementDefinition($key) {
$value = isset($this->value[$key]) ? $this->value[$key] : NULL;
$definition = isset($this->definition['mapping'][$key]) ? $this->definition['mapping'][$key] : array();
return $this->buildDataDefinition($definition, $value, $key);
}
}

View file

@ -0,0 +1,130 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Schema\SchemaCheckTrait.
*/
namespace Drupal\Core\Config\Schema;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\TypedData\PrimitiveInterface;
use Drupal\Core\TypedData\TraversableTypedDataInterface;
use Drupal\Core\TypedData\Type\BooleanInterface;
use Drupal\Core\TypedData\Type\StringInterface;
use Drupal\Core\TypedData\Type\FloatInterface;
use Drupal\Core\TypedData\Type\IntegerInterface;
/**
* Provides a trait for checking configuration schema.
*/
trait SchemaCheckTrait {
/**
* The config schema wrapper object for the configuration object under test.
*
* @var \Drupal\Core\Config\Schema\Element
*/
protected $schema;
/**
* The configuration object name under test.
*
* @var string
*/
protected $configName;
/**
* Checks the TypedConfigManager has a valid schema for the configuration.
*
* @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config
* The TypedConfigManager.
* @param string $config_name
* The configuration name.
* @param array $config_data
* The configuration data.
*
* @return array|bool
* FALSE if no schema found. List of errors if any found. TRUE if fully
* valid.
*/
public function checkConfigSchema(TypedConfigManagerInterface $typed_config, $config_name, $config_data) {
$this->configName = $config_name;
if (!$typed_config->hasConfigSchema($config_name)) {
return FALSE;
}
$definition = $typed_config->getDefinition($config_name);
$data_definition = $typed_config->buildDataDefinition($definition, $config_data);
$this->schema = $typed_config->create($data_definition, $config_data);
$errors = array();
foreach ($config_data as $key => $value) {
$errors = array_merge($errors, $this->checkValue($key, $value));
}
if (empty($errors)) {
return TRUE;
}
return $errors;
}
/**
* Helper method to check data type.
*
* @param string $key
* A string of configuration key.
* @param mixed $value
* Value of given key.
*
* @return array
* List of errors found while checking with the corresponding schema.
*/
protected function checkValue($key, $value) {
$error_key = $this->configName . ':' . $key;
$element = $this->schema->get($key);
if ($element instanceof Undefined) {
return array($error_key => 'missing schema');
}
// Do not check value if it is defined to be ignored.
if ($element && $element instanceof Ignore) {
return array();
}
if ($element && is_scalar($value) || $value === NULL) {
$success = FALSE;
$type = gettype($value);
if ($element instanceof PrimitiveInterface) {
$success =
($type == 'integer' && $element instanceof IntegerInterface) ||
// Allow integer values in a float field.
(($type == 'double' || $type == 'integer') && $element instanceof FloatInterface) ||
($type == 'boolean' && $element instanceof BooleanInterface) ||
($type == 'string' && $element instanceof StringInterface) ||
// Null values are allowed for all types.
($value === NULL);
}
$class = get_class($element);
if (!$success) {
return array($error_key => "variable type is $type but applied schema class is $class");
}
}
else {
$errors = array();
if (!$element instanceof TraversableTypedDataInterface) {
$errors[$error_key] = 'non-scalar value but not defined as an array (such as mapping or sequence)';
}
// Go on processing so we can get errors on all levels. Any non-scalar
// value must be an array so cast to an array.
if (!is_array($value)) {
$value = (array) $value;
}
// Recurse into any nested keys.
foreach ($value as $nested_value_key => $nested_value) {
$errors = array_merge($errors, $this->checkValue($key . '.' . $nested_value_key, $nested_value));
}
return $errors;
}
// No errors found.
return array();
}
}

View file

@ -0,0 +1,14 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Schema\SchemaIncompleteException.
*/
namespace Drupal\Core\Config\Schema;
/**
* An exception thrown when a config schema is incomplete.
*/
class SchemaIncompleteException extends \RuntimeException {
}

View file

@ -0,0 +1,37 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Schema\Sequence.
*/
namespace Drupal\Core\Config\Schema;
/**
* Defines a configuration element of type Sequence.
*
* This object may contain any number and type of nested elements that share
* a common definition in the 'sequence' property of the configuration schema.
*
* Read https://www.drupal.org/node/1905070 for more details about configuration
* schema, types and type resolution.
*/
class Sequence extends ArrayElement {
/**
* {@inheritdoc}
*/
protected function getElementDefinition($key) {
$value = isset($this->value[$key]) ? $this->value[$key] : NULL;
// @todo: Remove BC layer for sequence with hyphen in front. https://www.drupal.org/node/2444979
$definition = array();
if (isset($this->definition['sequence'][0])) {
$definition = $this->definition['sequence'][0];
}
elseif ($this->definition['sequence']) {
$definition = $this->definition['sequence'];
}
return $this->buildDataDefinition($definition, $value, $key);
}
}

View file

@ -0,0 +1,64 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Schema\TypedConfigInterface.
*/
namespace Drupal\Core\Config\Schema;
use Drupal\Core\TypedData\TraversableTypedDataInterface;
/**
* Interface for a typed configuration object that contains multiple elements.
*
* A list of typed configuration contains any number of items whose type
* will depend on the configuration schema but also on the configuration
* data being parsed.
*
* When implementing this interface which extends Traversable, make sure to list
* IteratorAggregate or Iterator before this interface in the implements clause.
*/
interface TypedConfigInterface extends TraversableTypedDataInterface {
/**
* Determines whether the data structure is empty.
*
* @return boolean
* TRUE if the data structure is empty, FALSE otherwise.
*/
public function isEmpty();
/**
* Gets an array of contained elements.
*
* @return array
* Array of \Drupal\Core\TypedData\TypedDataInterface objects.
*/
public function getElements();
/**
* Gets a contained typed configuration element.
*
* @param $name
* The name of the property to get; e.g., 'title' or 'name'. Nested
* elements can be get using multiple dot delimited names, for example,
* 'page.front'.
*
* @throws \InvalidArgumentException
* If an invalid property name is given.
*
* @return \Drupal\Core\TypedData\TypedDataInterface
* The property object.
*/
public function get($name);
/**
* Returns an array of all property values.
*
* @return array
* An array of property values, keyed by property name.
*/
public function toArray();
}

View file

@ -0,0 +1,15 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Schema\Undefined.
*/
namespace Drupal\Core\Config\Schema;
/**
* Undefined configuration element.
*/
class Undefined extends Element {
}

View file

@ -0,0 +1,229 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\StorableConfigBase.
*/
namespace Drupal\Core\Config;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Config\Schema\Ignore;
use Drupal\Core\TypedData\PrimitiveInterface;
use Drupal\Core\TypedData\Type\FloatInterface;
use Drupal\Core\TypedData\Type\IntegerInterface;
use Drupal\Core\Config\Schema\Undefined;
/**
* Provides a base class for configuration objects with storage support.
*
* Encapsulates all capabilities needed for configuration handling for a
* specific configuration object, including storage and data type casting.
*
* The default implementation in \Drupal\Core\Config\Config adds support for
* runtime overrides. Extend from StorableConfigBase directly to manage
* configuration with a storage backend that does not support overrides.
*
* @see \Drupal\Core\Config\Config
*/
abstract class StorableConfigBase extends ConfigBase {
/**
* The storage used to load and save this configuration object.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $storage;
/**
* The config schema wrapper object for this configuration object.
*
* @var \Drupal\Core\Config\Schema\Element
*/
protected $schemaWrapper;
/**
* The typed config manager.
*
* @var \Drupal\Core\Config\TypedConfigManagerInterface
*/
protected $typedConfigManager;
/**
* Whether the configuration object is new or has been saved to the storage.
*
* @var bool
*/
protected $isNew = TRUE;
/**
* The data of the configuration object.
*
* @var array
*/
protected $originalData = array();
/**
* Saves the configuration object.
*
* @param bool $has_trusted_data
* Set to TRUE if the configuration data has already been checked to ensure
* it conforms to schema. Generally this is only used during module and
* theme installation.
*
* Must invalidate the cache tags associated with the configuration object.
*
* @return $this
*
* @see \Drupal\Core\Config\ConfigInstaller::createConfiguration()
*/
abstract public function save($has_trusted_data = FALSE);
/**
* Deletes the configuration object.
*
* Must invalidate the cache tags associated with the configuration object.
*
* @return $this
*/
abstract public function delete();
/**
* Initializes a configuration object with pre-loaded data.
*
* @param array $data
* Array of loaded data for this configuration object.
*
* @return $this
* The configuration object.
*/
public function initWithData(array $data) {
$this->isNew = FALSE;
$this->setData($data, FALSE);
$this->originalData = $this->data;
return $this;
}
/**
* Returns whether this configuration object is new.
*
* @return bool
* TRUE if this configuration object does not exist in storage.
*/
public function isNew() {
return $this->isNew;
}
/**
* Retrieves the storage used to load and save this configuration object.
*
* @return \Drupal\Core\Config\StorageInterface
* The configuration storage object.
*/
public function getStorage() {
return $this->storage;
}
/**
* Gets the schema wrapper for the whole configuration object.
*
* The schema wrapper is dependent on the configuration name and the whole
* data structure, so if the name or the data changes in any way, the wrapper
* should be reset.
*
* @return \Drupal\Core\Config\Schema\Element
*/
protected function getSchemaWrapper() {
if (!isset($this->schemaWrapper)) {
$definition = $this->typedConfigManager->getDefinition($this->name);
$data_definition = $this->typedConfigManager->buildDataDefinition($definition, $this->data);
$this->schemaWrapper = $this->typedConfigManager->create($data_definition, $this->data);
}
return $this->schemaWrapper;
}
/**
* Validate the values are allowed data types.
*
* @param string $key
* A string that maps to a key within the configuration data.
* @param string $value
* Value to associate with the key.
*
* @return null
*
* @throws \Drupal\Core\Config\UnsupportedDataTypeConfigException
* If the value is unsupported in configuration.
*/
protected function validateValue($key, $value) {
// Minimal validation. Should not try to serialize resources or non-arrays.
if (is_array($value)) {
foreach ($value as $nested_value_key => $nested_value) {
$this->validateValue($key . '.' . $nested_value_key, $nested_value);
}
}
elseif ($value !== NULL && !is_scalar($value)) {
throw new UnsupportedDataTypeConfigException(SafeMarkup::format('Invalid data type for config element @name:@key', array(
'@name' => $this->getName(),
'@key' => $key,
)));
}
}
/**
* Casts the value to correct data type using the configuration schema.
*
* @param string $key
* A string that maps to a key within the configuration data.
* @param string $value
* Value to associate with the key.
*
* @return mixed
* The value cast to the type indicated in the schema.
*
* @throws \Drupal\Core\Config\UnsupportedDataTypeConfigException
* If the value is unsupported in configuration.
*/
protected function castValue($key, $value) {
$element = $this->getSchemaWrapper()->get($key);
// Do not cast value if it is unknown or defined to be ignored.
if ($element && ($element instanceof Undefined || $element instanceof Ignore)) {
// Do validate the value (may throw UnsupportedDataTypeConfigException)
// to ensure unsupported types are not supported in this case either.
$this->validateValue($key, $value);
return $value;
}
if (is_scalar($value) || $value === NULL) {
if ($element && $element instanceof PrimitiveInterface) {
// Special handling for integers and floats since the configuration
// system is primarily concerned with saving values from the Form API
// we have to special case the meaning of an empty string for numeric
// types. In PHP this would be casted to a 0 but for the purposes of
// configuration we need to treat this as a NULL.
$empty_value = $value === '' && ($element instanceof IntegerInterface || $element instanceof FloatInterface);
if ($value === NULL || $empty_value) {
$value = NULL;
}
else {
$value = $element->getCastedValue();
}
}
}
else {
// Throw exception on any non-scalar or non-array value.
if (!is_array($value)) {
throw new UnsupportedDataTypeConfigException(SafeMarkup::format('Invalid data type for config element @name:@key', array(
'@name' => $this->getName(),
'@key' => $key,
)));
}
// Recurse into any nested keys.
foreach ($value as $nested_value_key => $nested_value) {
$value[$nested_value_key] = $this->castValue($key . '.' . $nested_value_key, $nested_value);
}
}
return $value;
}
}

View file

@ -0,0 +1,20 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\StorageCacheInterface.
*/
namespace Drupal\Core\Config;
/**
* Defines an interface for cached configuration storage.
*/
interface StorageCacheInterface {
/**
* Reset the static cache of the listAll() cache.
*/
public function resetListCache();
}

View file

@ -0,0 +1,468 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\StorageComparer.
*/
namespace Drupal\Core\Config;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Cache\MemoryBackend;
use Drupal\Core\Config\Entity\ConfigDependencyManager;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
/**
* Defines a config storage comparer.
*/
class StorageComparer implements StorageComparerInterface {
use DependencySerializationTrait;
/**
* The source storage used to discover configuration changes.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $sourceStorage;
/**
* The source storages keyed by collection.
*
* @var \Drupal\Core\Config\StorageInterface[]
*/
protected $sourceStorages;
/**
* The target storage used to write configuration changes.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $targetStorage;
/**
* The target storages keyed by collection.
*
* @var \Drupal\Core\Config\StorageInterface[]
*/
protected $targetStorages;
/**
* The configuration manager.
*
* @var \Drupal\Core\Config\ConfigManagerInterface
*/
protected $configManager;
/**
* List of changes to between the source storage and the target storage.
*
* The list is keyed by storage collection name.
*
* @var array
*/
protected $changelist;
/**
* Sorted list of all the configuration object names in the source storage.
*
* The list is keyed by storage collection name.
*
* @var array
*/
protected $sourceNames = array();
/**
* Sorted list of all the configuration object names in the target storage.
*
* The list is keyed by storage collection name.
*
* @var array
*/
protected $targetNames = array();
/**
* A memory cache backend to statically cache source configuration data.
*
* @var \Drupal\Core\Cache\MemoryBackend
*/
protected $sourceCacheStorage;
/**
* A memory cache backend to statically cache target configuration data.
*
* @var \Drupal\Core\Cache\MemoryBackend
*/
protected $targetCacheStorage;
/**
* Constructs the Configuration storage comparer.
*
* @param \Drupal\Core\Config\StorageInterface $source_storage
* Storage object used to read configuration.
* @param \Drupal\Core\Config\StorageInterface $target_storage
* Storage object used to write configuration.
* @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
* The configuration manager.
*/
public function __construct(StorageInterface $source_storage, StorageInterface $target_storage, ConfigManagerInterface $config_manager) {
// Wrap the storages in a static cache so that multiple reads of the same
// raw configuration object are not costly.
$this->sourceCacheStorage = new MemoryBackend(__CLASS__ . '::source');
$this->sourceStorage = new CachedStorage(
$source_storage,
$this->sourceCacheStorage
);
$this->targetCacheStorage = new MemoryBackend(__CLASS__ . '::target');
$this->targetStorage = new CachedStorage(
$target_storage,
$this->targetCacheStorage
);
$this->configManager = $config_manager;
$this->changelist[StorageInterface::DEFAULT_COLLECTION] = $this->getEmptyChangelist();
}
/**
* {@inheritdoc}
*/
public function getSourceStorage($collection = StorageInterface::DEFAULT_COLLECTION) {
if (!isset($this->sourceStorages[$collection])) {
if ($collection == StorageInterface::DEFAULT_COLLECTION) {
$this->sourceStorages[$collection] = $this->sourceStorage;
}
else {
$this->sourceStorages[$collection] = $this->sourceStorage->createCollection($collection);
}
}
return $this->sourceStorages[$collection];
}
/**
* {@inheritdoc}
*/
public function getTargetStorage($collection = StorageInterface::DEFAULT_COLLECTION) {
if (!isset($this->targetStorages[$collection])) {
if ($collection == StorageInterface::DEFAULT_COLLECTION) {
$this->targetStorages[$collection] = $this->targetStorage;
}
else {
$this->targetStorages[$collection] = $this->targetStorage->createCollection($collection);
}
}
return $this->targetStorages[$collection];
}
/**
* {@inheritdoc}
*/
public function getEmptyChangelist() {
return array(
'create' => array(),
'update' => array(),
'delete' => array(),
'rename' => array(),
);
}
/**
* {@inheritdoc}
*/
public function getChangelist($op = NULL, $collection = StorageInterface::DEFAULT_COLLECTION) {
if ($op) {
return $this->changelist[$collection][$op];
}
return $this->changelist[$collection];
}
/**
* Adds changes to the changelist.
*
* @param string $collection
* The storage collection to add changes for.
* @param string $op
* The change operation performed. Either delete, create, rename, or update.
* @param array $changes
* Array of changes to add to the changelist.
* @param array $sort_order
* Array to sort that can be used to sort the changelist. This array must
* contain all the items that are in the change list.
*/
protected function addChangeList($collection, $op, array $changes, array $sort_order = NULL) {
// Only add changes that aren't already listed.
$changes = array_diff($changes, $this->changelist[$collection][$op]);
$this->changelist[$collection][$op] = array_merge($this->changelist[$collection][$op], $changes);
if (isset($sort_order)) {
$count = count($this->changelist[$collection][$op]);
// Sort the changelist in the same order as the $sort_order array and
// ensure the array is keyed from 0.
$this->changelist[$collection][$op] = array_values(array_intersect($sort_order, $this->changelist[$collection][$op]));
if ($count != count($this->changelist[$collection][$op])) {
throw new \InvalidArgumentException(SafeMarkup::format('Sorting the @op changelist should not change its length.', array('@op' => $op)));
}
}
}
/**
* {@inheritdoc}
*/
public function createChangelist() {
foreach ($this->getAllCollectionNames() as $collection) {
$this->changelist[$collection] = $this->getEmptyChangelist();
$this->getAndSortConfigData($collection);
$this->addChangelistCreate($collection);
$this->addChangelistUpdate($collection);
$this->addChangelistDelete($collection);
// Only collections that support configuration entities can have renames.
if ($collection == StorageInterface::DEFAULT_COLLECTION) {
$this->addChangelistRename($collection);
}
}
return $this;
}
/**
* Creates the delete changelist.
*
* The list of deletes is sorted so that dependencies are deleted after
* configuration entities that depend on them. For example, fields should be
* deleted after field storages.
*
* @param string $collection
* The storage collection to operate on.
*/
protected function addChangelistDelete($collection) {
$deletes = array_diff(array_reverse($this->targetNames[$collection]), $this->sourceNames[$collection]);
$this->addChangeList($collection, 'delete', $deletes);
}
/**
* Creates the create changelist.
*
* The list of creates is sorted so that dependencies are created before
* configuration entities that depend on them. For example, field storages
* should be created before fields.
*
* @param string $collection
* The storage collection to operate on.
*/
protected function addChangelistCreate($collection) {
$creates = array_diff($this->sourceNames[$collection], $this->targetNames[$collection]);
$this->addChangeList($collection, 'create', $creates);
}
/**
* Creates the update changelist.
*
* The list of updates is sorted so that dependencies are created before
* configuration entities that depend on them. For example, field storages
* should be updated before fields.
*
* @param string $collection
* The storage collection to operate on.
*/
protected function addChangelistUpdate($collection) {
$recreates = array();
foreach (array_intersect($this->sourceNames[$collection], $this->targetNames[$collection]) as $name) {
$source_data = $this->getSourceStorage($collection)->read($name);
$target_data = $this->getTargetStorage($collection)->read($name);
if ($source_data !== $target_data) {
if (isset($source_data['uuid']) && $source_data['uuid'] !== $target_data['uuid']) {
// The entity has the same file as an existing entity but the UUIDs do
// not match. This means that the entity has been recreated so config
// synchronization should do the same.
$recreates[] = $name;
}
else {
$this->addChangeList($collection, 'update', array($name));
}
}
}
if (!empty($recreates)) {
// Recreates should become deletes and creates. Deletes should be ordered
// so that dependencies are deleted first.
$this->addChangeList($collection, 'create', $recreates, $this->sourceNames[$collection]);
$this->addChangeList($collection, 'delete', $recreates, array_reverse($this->targetNames[$collection]));
}
}
/**
* Creates the rename changelist.
*
* The list of renames is created from the different source and target names
* with same UUID. These changes will be removed from the create and delete
* lists.
*
* @param string $collection
* The storage collection to operate on.
*/
protected function addChangelistRename($collection) {
// Renames will be present in both the create and delete lists.
$create_list = $this->getChangelist('create', $collection);
$delete_list = $this->getChangelist('delete', $collection);
if (empty($create_list) || empty($delete_list)) {
return;
}
$create_uuids = array();
foreach ($this->sourceNames[$collection] as $name) {
$data = $this->getSourceStorage($collection)->read($name);
if (isset($data['uuid']) && in_array($name, $create_list)) {
$create_uuids[$data['uuid']] = $name;
}
}
if (empty($create_uuids)) {
return;
}
$renames = array();
// Renames should be ordered so that dependencies are renamed last. This
// ensures that if there is logic in the configuration entity class to keep
// names in sync it will still work. $this->targetNames is in the desired
// order due to the use of configuration dependencies in
// \Drupal\Core\Config\StorageComparer::getAndSortConfigData().
// Node type is a good example of a configuration entity that renames other
// configuration when it is renamed.
// @see \Drupal\node\Entity\NodeType::postSave()
foreach ($this->targetNames[$collection] as $name) {
$data = $this->getTargetStorage($collection)->read($name);
if (isset($data['uuid']) && isset($create_uuids[$data['uuid']])) {
// Remove the item from the create list.
$this->removeFromChangelist($collection, 'create', $create_uuids[$data['uuid']]);
// Remove the item from the delete list.
$this->removeFromChangelist($collection, 'delete', $name);
// Create the rename name.
$renames[] = $this->createRenameName($name, $create_uuids[$data['uuid']]);
}
}
$this->addChangeList($collection, 'rename', $renames);
}
/**
* Removes the entry from the given operation changelist for the given name.
*
* @param string $collection
* The storage collection to operate on.
* @param string $op
* The changelist to act on. Either delete, create, rename or update.
* @param string $name
* The name of the configuration to remove.
*/
protected function removeFromChangelist($collection, $op, $name) {
$key = array_search($name, $this->changelist[$collection][$op]);
if ($key !== FALSE) {
unset($this->changelist[$collection][$op][$key]);
}
}
/**
* {@inheritdoc}
*/
public function moveRenameToUpdate($rename, $collection = StorageInterface::DEFAULT_COLLECTION) {
$names = $this->extractRenameNames($rename);
$this->removeFromChangelist($collection, 'rename', $rename);
$this->addChangeList($collection, 'update', array($names['new_name']), $this->sourceNames[$collection]);
}
/**
* {@inheritdoc}
*/
public function reset() {
$this->changelist = array(StorageInterface::DEFAULT_COLLECTION => $this->getEmptyChangelist());
$this->sourceNames = $this->targetNames = array();
// Reset the static configuration data caches.
$this->sourceCacheStorage->deleteAll();
$this->targetCacheStorage->deleteAll();
return $this->createChangelist();
}
/**
* {@inheritdoc}
*/
public function hasChanges() {
foreach ($this->getAllCollectionNames() as $collection) {
foreach (array('delete', 'create', 'update', 'rename') as $op) {
if (!empty($this->changelist[$collection][$op])) {
return TRUE;
}
}
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function validateSiteUuid() {
$source = $this->sourceStorage->read('system.site');
$target = $this->targetStorage->read('system.site');
return $source['uuid'] === $target['uuid'];
}
/**
* Gets and sorts configuration data from the source and target storages.
*/
protected function getAndSortConfigData($collection) {
$source_storage = $this->getSourceStorage($collection);
$target_storage = $this->getTargetStorage($collection);
$target_names = $target_storage->listAll();
$source_names = $source_storage->listAll();
// Prime the static caches by reading all the configuration in the source
// and target storages.
$target_data = $target_storage->readMultiple($target_names);
$source_data = $source_storage->readMultiple($source_names);
// If the collection only supports simple configuration do not use
// configuration dependencies.
if ($collection == StorageInterface::DEFAULT_COLLECTION) {
$dependency_manager = new ConfigDependencyManager();
$this->targetNames[$collection] = $dependency_manager->setData($target_data)->sortAll();
$this->sourceNames[$collection] = $dependency_manager->setData($source_data)->sortAll();
}
else {
$this->targetNames[$collection] = $target_names;
$this->sourceNames[$collection] = $source_names;
}
}
/**
* Creates a rename name from the old and new names for the object.
*
* @param string $old_name
* The old configuration object name.
* @param string $new_name
* The new configuration object name.
*
* @return string
* The configuration change name that encodes both the old and the new name.
*
* @see \Drupal\Core\Config\StorageComparerInterface::extractRenameNames()
*/
protected function createRenameName($name1, $name2) {
return $name1 . '::' . $name2;
}
/**
* {@inheritdoc}
*/
public function extractRenameNames($name) {
$names = explode('::', $name, 2);
return array(
'old_name' => $names[0],
'new_name' => $names[1],
);
}
/**
* {@inheritdoc}
*/
public function getAllCollectionNames($include_default = TRUE) {
$collections = array_unique(array_merge($this->sourceStorage->getAllCollectionNames(), $this->targetStorage->getAllCollectionNames()));
if ($include_default) {
array_unshift($collections, StorageInterface::DEFAULT_COLLECTION);
}
return $collections;
}
}

View file

@ -0,0 +1,130 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\StorageComparerInterface.
*/
namespace Drupal\Core\Config;
/**
* Defines an interface for comparison of configuration storage objects.
*/
interface StorageComparerInterface {
/**
* Gets the configuration source storage.
*
* @param string $collection
* (optional) The storage collection to use. Defaults to the
* default collection.
*
* @return \Drupal\Core\Config\StorageInterface
* Storage object used to read configuration.
*/
public function getSourceStorage($collection = StorageInterface::DEFAULT_COLLECTION);
/**
* Gets the configuration target storage.
*
* @param string $collection
* (optional) The storage collection to use. Defaults to the
* default collection.
*
* @return \Drupal\Core\Config\StorageInterface
* Storage object used to write configuration.
*/
public function getTargetStorage($collection = StorageInterface::DEFAULT_COLLECTION);
/**
* Gets an empty changelist.
*
* @return array
* An empty changelist array.
*/
public function getEmptyChangelist();
/**
* Gets the list of differences to import.
*
* @param string $op
* (optional) A change operation. Either delete, create or update. If
* supplied the returned list will be limited to this operation.
* @param string $collection
* (optional) The collection to get the changelist for. Defaults to the
* default collection.
*
* @return array
* An array of config changes that are yet to be imported.
*/
public function getChangelist($op = NULL, $collection = StorageInterface::DEFAULT_COLLECTION);
/**
* Recalculates the differences.
*
* @return \Drupal\Core\Config\StorageComparerInterface
* An object which implements the StorageComparerInterface.
*/
public function reset();
/**
* Checks if there are any operations with changes to process.
*
* Until the changelist has been calculated this will always be FALSE.
*
* @see \Drupal\Core\Config\StorageComparerInterface::createChangelist().
*
* @return bool
* TRUE if there are changes to process and FALSE if not.
*/
public function hasChanges();
/**
* Validates that the system.site::uuid in the source and target match.
*
* @return bool
* TRUE if identical, FALSE if not.
*/
public function validateSiteUuid();
/**
* Moves a rename operation to an update.
*
* @param string $rename
* The rename name, as provided by ConfigImporter::createRenameName().
* @param string $collection
* (optional) The collection where the configuration is stored. Defaults to
* the default collection.
*
* @see \Drupal\Core\Config\ConfigImporter::createRenameName()
*/
public function moveRenameToUpdate($rename, $collection = StorageInterface::DEFAULT_COLLECTION);
/**
* Extracts old and new configuration names from a configuration change name.
*
* @param string $name
* The configuration change name, as provided by
* ConfigImporter::createRenameName().
*
* @return array
* An associative array of configuration names. The array keys are
* 'old_name' and and 'new_name' representing the old and name configuration
* object names during a rename operation.
*
* @see \Drupal\Core\Config\StorageComparer::createRenameNames()
*/
public function extractRenameNames($name);
/**
* Gets the existing collections from both the target and source storage.
*
* @param bool $include_default
* (optional) Include the default collection. Defaults to TRUE.
*
* @return array
* An array of existing collection names.
*/
public function getAllCollectionNames($include_default = TRUE);
}

View file

@ -0,0 +1,13 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\StorageException.
*/
namespace Drupal\Core\Config;
/**
* An exception thrown in case of storage operation errors.
*/
class StorageException extends ConfigException {}

View file

@ -0,0 +1,206 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\StorageInterface.
*/
namespace Drupal\Core\Config;
/**
* Defines an interface for configuration storage.
*
* Classes implementing this interface allow reading and writing configuration
* data from and to the storage.
*/
interface StorageInterface {
/**
* The default collection name.
*/
const DEFAULT_COLLECTION = '';
/**
* Returns whether a configuration object exists.
*
* @param string $name
* The name of a configuration object to test.
*
* @return bool
* TRUE if the configuration object exists, FALSE otherwise.
*/
public function exists($name);
/**
* Reads configuration data from the storage.
*
* @param string $name
* The name of a configuration object to load.
*
* @return array|bool
* The configuration data stored for the configuration object name. If no
* configuration data exists for the given name, FALSE is returned.
*/
public function read($name);
/**
* Reads configuration data from the storage.
*
* @param array $names
* List of names of the configuration objects to load.
*
* @return array
* A list of the configuration data stored for the configuration object name
* that could be loaded for the passed list of names.
*/
public function readMultiple(array $names);
/**
* Writes configuration data to the storage.
*
* @param string $name
* The name of a configuration object to save.
* @param array $data
* The configuration data to write.
*
* @return bool
* TRUE on success, FALSE in case of an error.
*
* @throws \Drupal\Core\Config\StorageException
* If the back-end storage does not exist and cannot be created.
*/
public function write($name, array $data);
/**
* Deletes a configuration object from the storage.
*
* @param string $name
* The name of a configuration object to delete.
*
* @return bool
* TRUE on success, FALSE otherwise.
*/
public function delete($name);
/**
* Renames a configuration object in the storage.
*
* @param string $name
* The name of a configuration object to rename.
* @param string $new_name
* The new name of a configuration object.
*
* @return bool
* TRUE on success, FALSE otherwise.
*/
public function rename($name, $new_name);
/**
* Encodes configuration data into the storage-specific format.
*
* @param array $data
* The configuration data to encode.
*
* @return string
* The encoded configuration data.
*
* This is a publicly accessible static method to allow for alternative
* usages in data conversion scripts and also tests.
*/
public function encode($data);
/**
* Decodes configuration data from the storage-specific format.
*
* @param string $raw
* The raw configuration data string to decode.
*
* @return array
* The decoded configuration data as an associative array.
*
* This is a publicly accessible static method to allow for alternative
* usages in data conversion scripts and also tests.
*/
public function decode($raw);
/**
* Gets configuration object names starting with a given prefix.
*
* Given the following configuration objects:
* - node.type.article
* - node.type.page
*
* Passing the prefix 'node.type.' will return an array containing the above
* names.
*
* @param string $prefix
* (optional) The prefix to search for. If omitted, all configuration object
* names that exist are returned.
*
* @return array
* An array containing matching configuration object names.
*/
public function listAll($prefix = '');
/**
* Deletes configuration objects whose names start with a given prefix.
*
* Given the following configuration object names:
* - node.type.article
* - node.type.page
*
* Passing the prefix 'node.type.' will delete the above configuration
* objects.
*
* @param string $prefix
* (optional) The prefix to search for. If omitted, all configuration
* objects that exist will be deleted.
*
* @return boolean
* TRUE on success, FALSE otherwise.
*/
public function deleteAll($prefix = '');
/**
* Creates a collection on the storage.
*
* A configuration storage can contain multiple sets of configuration objects
* in partitioned collections. The collection name identifies the current
* collection used.
*
* Implementations of this method must provide a new instance to avoid side
* effects caused by the fact that Config objects have their storage injected.
*
* @param string $collection
* The collection name. Valid collection names conform to the following
* regex [a-zA-Z_.]. A storage does not need to have a collection set.
* However, if a collection is set, then storage should use it to store
* configuration in a way that allows retrieval of configuration for a
* particular collection.
*
* @return \Drupal\Core\Config\StorageInterface
* A new instance of the storage backend with the collection set.
*/
public function createCollection($collection);
/**
* Gets the existing collections.
*
* A configuration storage can contain multiple sets of configuration objects
* in partitioned collections. The collection key name identifies the current
* collection used.
*
* @return array
* An array of existing collection names.
*/
public function getAllCollectionNames();
/**
* Gets the name of the current collection the storage is using.
*
* @return string
* The current collection name.
*/
public function getCollectionName();
}

View file

@ -0,0 +1,111 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Testing\ConfigSchemaChecker.
*/
namespace Drupal\Core\Config\Testing;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\Config\Schema\SchemaCheckTrait;
use Drupal\Core\Config\Schema\SchemaIncompleteException;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Listens to the config save event and validates schema.
*
* If tests have the $strictConfigSchema property set to TRUE this event
* listener will be added to the container and throw exceptions if configuration
* is invalid.
*
* @see \Drupal\simpletest\WebTestBase::setUp()
* @see \Drupal\simpletest\KernelTestBase::containerBuild()
*/
class ConfigSchemaChecker implements EventSubscriberInterface {
use SchemaCheckTrait;
/**
* The typed config manger.
*
* @var \Drupal\Core\Config\TypedConfigManagerInterface
*/
protected $typedManager;
/**
* An array of config checked already. Keyed by config name and a checksum.
*
* @var array
*/
protected $checked = array();
/**
* Constructs the ConfigSchemaChecker object.
*
* @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_manager
* The typed config manager.
*/
public function __construct(TypedConfigManagerInterface $typed_manager) {
$this->typedManager = $typed_manager;
}
/**
* Checks that configuration complies with its schema on config save.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
* The configuration event.
*
* @throws \Drupal\Core\Config\Schema\SchemaIncompleteException
* Exception thrown when configuration does not match its schema.
*/
public function onConfigSave(ConfigCrudEvent $event) {
// Only validate configuration if in the default collection. Other
// collections may have incomplete configuration (for example language
// overrides only). These are not valid in themselves.
$saved_config = $event->getConfig();
if ($saved_config->getStorage()->getCollectionName() != StorageInterface::DEFAULT_COLLECTION) {
return;
}
$name = $saved_config->getName();
$data = $saved_config->get();
$checksum = crc32(serialize($data));
$exceptions = array(
// Following are used to test lack of or partial schema. Where partial
// schema is provided, that is explicitly tested in specific tests.
'config_schema_test.noschema',
'config_schema_test.someschema',
'config_schema_test.schema_data_types',
'config_schema_test.no_schema_data_types',
// Used to test application of schema to filtering of configuration.
'config_test.dynamic.system',
);
if (!in_array($name, $exceptions) && !isset($this->checked[$name . ':' . $checksum])) {
$this->checked[$name . ':' . $checksum] = TRUE;
$errors = $this->checkConfigSchema($this->typedManager, $name, $data);
if ($errors === FALSE) {
throw new SchemaIncompleteException(SafeMarkup::format('No schema for @config_name', array('@config_name' => $name)));
}
elseif (is_array($errors)) {
$text_errors = [];
foreach ($errors as $key => $error) {
$text_errors[] = SafeMarkup::format('@key @error', array('@key' => $key, '@error' => $error));
}
throw new SchemaIncompleteException(SafeMarkup::format('Schema errors for @config_name with the following errors: @errors', array('@config_name' => $name, '@errors' => implode(', ', $text_errors))));
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[ConfigEvents::SAVE][] = array('onConfigSave', 255);
return $events;
}
}

View file

@ -0,0 +1,355 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\TypedConfigManager.
*/
namespace Drupal\Core\Config;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\Schema\ArrayElement;
use Drupal\Core\Config\Schema\ConfigSchemaAlterException;
use Drupal\Core\Config\Schema\ConfigSchemaDiscovery;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\TypedData\TypedDataManager;
/**
* Manages config type plugins.
*/
class TypedConfigManager extends TypedDataManager implements TypedConfigManagerInterface {
/**
* A storage instance for reading configuration data.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $configStorage;
/**
* A storage instance for reading configuration schema data.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $schemaStorage;
/**
* The array of plugin definitions, keyed by plugin id.
*
* @var array
*/
protected $definitions;
/**
* Creates a new typed configuration manager.
*
* @param \Drupal\Core\Config\StorageInterface $configStorage
* The storage object to use for reading schema data
* @param \Drupal\Core\Config\StorageInterface $schemaStorage
* The storage object to use for reading schema data
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend to use for caching the definitions.
*/
public function __construct(StorageInterface $configStorage, StorageInterface $schemaStorage, CacheBackendInterface $cache, ModuleHandlerInterface $module_handler) {
$this->configStorage = $configStorage;
$this->schemaStorage = $schemaStorage;
$this->setCacheBackend($cache, 'typed_config_definitions');
$this->alterInfo('config_schema_info');
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
protected function getDiscovery() {
if (!isset($this->discovery)) {
$this->discovery = new ConfigSchemaDiscovery($this->schemaStorage);
}
return $this->discovery;
}
/**
* Gets typed configuration data.
*
* @param string $name
* Configuration object name.
*
* @return \Drupal\Core\Config\Schema\TypedConfigInterface
* Typed configuration data.
*/
public function get($name) {
$data = $this->configStorage->read($name);
$type_definition = $this->getDefinition($name);
$data_definition = $this->buildDataDefinition($type_definition, $data);
return $this->create($data_definition, $data);
}
/**
* {@inheritdoc}
*/
public function buildDataDefinition(array $definition, $value, $name = NULL, $parent = NULL) {
// Add default values for data type and replace variables.
$definition += array('type' => 'undefined');
$type = $definition['type'];
if (strpos($type, ']')) {
// Replace variable names in definition.
$replace = is_array($value) ? $value : array();
if (isset($parent)) {
$replace['%parent'] = $parent;
}
if (isset($name)) {
$replace['%key'] = $name;
}
$type = $this->replaceName($type, $replace);
// Remove the type from the definition so that it is replaced with the
// concrete type from schema definitions.
unset($definition['type']);
}
// Add default values from type definition.
$definition += $this->getDefinition($type);
$data_definition = $this->createDataDefinition($definition['type']);
// Pass remaining values from definition array to data definition.
foreach ($definition as $key => $value) {
if (!isset($data_definition[$key])) {
$data_definition[$key] = $value;
}
}
return $data_definition;
}
/**
* {@inheritdoc}
*/
public function getDefinition($base_plugin_id, $exception_on_invalid = TRUE) {
$definitions = $this->getDefinitions();
if (isset($definitions[$base_plugin_id])) {
$type = $base_plugin_id;
}
elseif (strpos($base_plugin_id, '.') && $name = $this->getFallbackName($base_plugin_id)) {
// Found a generic name, replacing the last element by '*'.
$type = $name;
}
else {
// If we don't have definition, return the 'undefined' element.
$type = 'undefined';
}
$definition = $definitions[$type];
// Check whether this type is an extension of another one and compile it.
if (isset($definition['type'])) {
$merge = $this->getDefinition($definition['type'], $exception_on_invalid);
// Preserve integer keys on merge, so sequence item types can override
// parent settings as opposed to adding unused second, third, etc. items.
$definition = NestedArray::mergeDeepArray(array($merge, $definition), TRUE);
// Unset type so we try the merge only once per type.
unset($definition['type']);
$this->definitions[$type] = $definition;
}
// Add type and default definition class.
$definition += array(
'definition_class' => '\Drupal\Core\TypedData\DataDefinition',
'type' => $type,
);
return $definition;
}
/**
* {@inheritdoc}
*/
public function clearCachedDefinitions() {
$this->schemaStorage->reset();
parent::clearCachedDefinitions();
}
/**
* Gets fallback configuration schema name.
*
* @param string $name
* Configuration name or key.
*
* @return null|string
* The resolved schema name for the given configuration name or key. Returns
* null if there is no schema name to fallback to. For example,
* breakpoint.breakpoint.module.toolbar.narrow will check for definitions in
* the following order:
* breakpoint.breakpoint.module.toolbar.*
* breakpoint.breakpoint.module.*.*
* breakpoint.breakpoint.module.*
* breakpoint.breakpoint.*.*.*
* breakpoint.breakpoint.*
* breakpoint.*.*.*.*
* breakpoint.*
* Colons are also used, for example,
* block.settings.system_menu_block:footer will check for definitions in the
* following order:
* block.settings.system_menu_block:*
* block.settings.*:*
* block.settings.*
* block.*.*:*
* block.*
*/
protected function getFallbackName($name) {
// Check for definition of $name with filesystem marker.
$replaced = preg_replace('/([^\.:]+)([\.:\*]*)$/', '*\2', $name);
if ($replaced != $name) {
if (isset($this->definitions[$replaced])) {
return $replaced;
}
else {
// No definition for this level. Collapse multiple wildcards to a single
// wildcard to see if there is a greedy match. For example,
// breakpoint.breakpoint.*.* becomes
// breakpoint.breakpoint.*
$one_star = preg_replace('/\.([:\.\*]*)$/', '.*', $replaced);
if ($one_star != $replaced && isset($this->definitions[$one_star])) {
return $one_star;
}
// Check for next level. For example, if breakpoint.breakpoint.* has
// been checked and no match found then check breakpoint.*.*
return $this->getFallbackName($replaced);
}
}
}
/**
* Replaces variables in configuration name.
*
* The configuration name may contain one or more variables to be replaced,
* enclosed in square brackets like '[name]' and will follow the replacement
* rules defined by the replaceVariable() method.
*
* @param string $name
* Configuration name with variables in square brackets.
* @param mixed $data
* Configuration data for the element.
* @return string
* Configuration name with variables replaced.
*/
protected function replaceName($name, $data) {
if (preg_match_all("/\[(.*)\]/U", $name, $matches)) {
// Build our list of '[value]' => replacement.
$replace = array();
foreach (array_combine($matches[0], $matches[1]) as $key => $value) {
$replace[$key] = $this->replaceVariable($value, $data);
}
return strtr($name, $replace);
}
else {
return $name;
}
}
/**
* Replaces variable values in included names with configuration data.
*
* Variable values are nested configuration keys that will be replaced by
* their value or some of these special strings:
* - '%key', will be replaced by the element's key.
* - '%parent', to reference the parent element.
* - '%type', to reference the schema definition type. Can only be used in
* combination with %parent.
*
* There may be nested configuration keys separated by dots or more complex
* patterns like '%parent.name' which references the 'name' value of the
* parent element.
*
* Example patterns:
* - 'name.subkey', indicates a nested value of the current element.
* - '%parent.name', will be replaced by the 'name' value of the parent.
* - '%parent.%key', will be replaced by the parent element's key.
* - '%parent.%type', will be replaced by the schema type of the parent.
* - '%parent.%parent.%type', will be replaced by the schema type of the
* parent's parent.
*
* @param string $value
* Variable value to be replaced.
* @param mixed $data
* Configuration data for the element.
*
* @return string
* The replaced value if a replacement found or the original value if not.
*/
protected function replaceVariable($value, $data) {
$parts = explode('.', $value);
// Process each value part, one at a time.
while ($name = array_shift($parts)) {
if (!is_array($data) || !isset($data[$name])) {
// Key not found, return original value
return $value;
}
elseif (!$parts) {
// If no more parts left, this is the final property.
return (string)$data[$name];
}
else {
// Get nested value and continue processing.
if ($name == '%parent') {
/** @var \Drupal\Core\Config\Schema\ArrayElement $parent */
// Switch replacement values with values from the parent.
$parent = $data['%parent'];
$data = $parent->getValue();
$data['%type'] = $parent->getDataDefinition()->getDataType();
// The special %parent and %key values now need to point one level up.
if ($new_parent = $parent->getParent()) {
$data['%parent'] = $new_parent;
$data['%key'] = $new_parent->getName();
}
}
else {
$data = $data[$name];
}
}
}
}
/**
* {@inheritdoc}
*/
public function hasConfigSchema($name) {
// The schema system falls back on the Undefined class for unknown types.
$definition = $this->getDefinition($name);
return is_array($definition) && ($definition['class'] != '\Drupal\Core\Config\Schema\Undefined');
}
/**
* {@inheritdoc}
*/
protected function alterDefinitions(&$definitions) {
$discovered_schema = array_keys($definitions);
parent::alterDefinitions($definitions);
$altered_schema = array_keys($definitions);
if ($discovered_schema != $altered_schema) {
$added_keys = array_diff($altered_schema, $discovered_schema);
$removed_keys = array_diff($discovered_schema, $altered_schema);
if (!empty($added_keys) && !empty($removed_keys)) {
$message = 'Invoking hook_config_schema_info_alter() has added (@added) and removed (@removed) schema definitions';
}
elseif (!empty($added_keys)) {
$message = 'Invoking hook_config_schema_info_alter() has added (@added) schema definitions';
}
else {
$message = 'Invoking hook_config_schema_info_alter() has removed (@removed) schema definitions';
}
throw new ConfigSchemaAlterException(SafeMarkup::format($message, ['@added' => implode(',', $added_keys), '@removed' => implode(',', $removed_keys)]));
}
}
/**
* {@inheritdoc}
*/
public function createInstance($data_type, array $configuration = array()) {
$instance = parent::createInstance($data_type, $configuration);
// Enable elements to construct their own definitions using the typed config
// manager.
if ($instance instanceof ArrayElement) {
$instance->setTypedConfig($this);
}
return $instance;
}
}

View file

@ -0,0 +1,121 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\TypedConfigManagerInterface.
*/
namespace Drupal\Core\Config;
use Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\TypedData\DataDefinitionInterface;
/**
* Defines an interface for typed configuration manager.
*
* @package Drupal\Core\Config
*/
Interface TypedConfigManagerInterface extends PluginManagerInterface, CachedDiscoveryInterface {
/**
* Gets typed configuration data.
*
* @param string $name
* Configuration object name.
*
* @return \Drupal\Core\TypedData\TraversableTypedDataInterface
* Typed configuration element.
*/
public function get($name);
/**
* Instantiates a typed configuration object.
*
* @param string $data_type
* The data type, for which a typed configuration object should be
* instantiated.
* @param array $configuration
* The plugin configuration array, i.e. an array with the following keys:
* - data definition: The data definition object, i.e. an instance of
* \Drupal\Core\TypedData\DataDefinitionInterface.
* - name: (optional) If a property or list item is to be created, the name
* of the property or the delta of the list item.
* - parent: (optional) If a property or list item is to be created, the
* parent typed data object implementing either the ListInterface or the
* ComplexDataInterface.
*
* @return \Drupal\Core\Config\Schema\Element
* The instantiated typed configuration object.
*/
public function createInstance($data_type, array $configuration = array());
/**
* Creates a new typed configuration object instance.
*
* @param \Drupal\Core\TypedData\DataDefinitionInterface $definition
* The data definition of the typed data object.
* @param mixed $value
* The data value. If set, it has to match one of the supported
* data type format as documented for the data type classes.
* @param string $name
* (optional) If a property or list item is to be created, the name of the
* property or the delta of the list item.
* @param mixed $parent
* (optional) If a property or list item is to be created, the parent typed
* data object implementing either the ListInterface or the
* ComplexDataInterface.
*
* @return \Drupal\Core\Config\Schema\Element
* The instantiated typed data object.
*/
public function create(DataDefinitionInterface $definition, $value, $name = NULL, $parent = NULL);
/**
* Creates a new data definition object from a type definition array and
* actual configuration data. Since type definitions may contain variables
* to be replaced, we need the configuration value to create it.
*
* @param array $definition
* The base type definition array, for which a data definition should be
* created.
* @param $value
* Optional value of the configuration element.
* @param string $name
* Optional name of the configuration element.
* @param object $parent
* Optional parent element.
*
* @return \Drupal\Core\TypedData\DataDefinitionInterface
* A data definition for the given data type.
*/
public function buildDataDefinition(array $definition, $value, $name = NULL, $parent = NULL);
/**
* Checks if the configuration schema with the given config name exists.
*
* @param string $name
* Configuration name.
*
* @return bool
* TRUE if configuration schema exists, FALSE otherwise.
*/
public function hasConfigSchema($name);
/**
* Gets a specific plugin definition.
*
* @param string $plugin_id
* A plugin id.
* @param bool $exception_on_invalid
* Ignored with TypedConfigManagerInterface. Kept for compatibility with
* DiscoveryInterface.
*
* @return array
* A plugin definition array. If the given plugin id does not have typed
* configuration definition assigned, the definition of an undefined
* element type is returned.
*/
public function getDefinition($plugin_id, $exception_on_invalid = TRUE);
}

View file

@ -0,0 +1,95 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\UnmetDependenciesException.
*/
namespace Drupal\Core\Config;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\StringTranslation\TranslationInterface;
/**
* An exception thrown if configuration has unmet dependencies.
*/
class UnmetDependenciesException extends ConfigException {
/**
* A list of configuration objects that have unmet dependencies.
*
* @var array
*/
protected $configObjects = [];
/**
* The name of the extension that is being installed.
*
* @var string
*/
protected $extension;
/**
* Gets the list of configuration objects that have unmet dependencies.
*
* @return array
* A list of configuration objects that have unmet dependencies.
*/
public function getConfigObjects() {
return $this->configObjects;
}
/**
* Gets the name of the extension that is being installed.
*
* @return string
* The name of the extension that is being installed.
*/
public function getExtension() {
return $this->extension;
}
/**
* Gets a translated message from the exception.
*
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
*
* @return string
*/
public function getTranslatedMessage(TranslationInterface $string_translation, $extension) {
return $string_translation->formatPlural(
count($this->getConfigObjects()),
'Unable to install @extension, %config_names has unmet dependencies.',
'Unable to install @extension, %config_names have unmet dependencies.',
[
'%config_names' => implode(', ', $this->getConfigObjects()),
'@extension' => $extension,
]
);
}
/**
* Creates an exception for an extension and a list of configuration objects.
*
* @param $extension
* The name of the extension that is being installed.
* @param array $config_objects
* A list of configuration object names that have unmet dependencies
*
* @return \Drupal\Core\Config\PreExistingConfigException
*/
public static function create($extension, array $config_objects) {
$message = SafeMarkup::format('Configuration objects (@config_names) provided by @extension have unmet dependencies',
array(
'@config_names' => implode(', ', $config_objects),
'@extension' => $extension
)
);
$e = new static($message);
$e->configObjects = $config_objects;
$e->extension = $extension;
return $e;
}
}

View file

@ -0,0 +1,14 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\UnsupportedDataTypeConfigException.
*/
namespace Drupal\Core\Config;
/**
* Exception thrown when a config data type is invalid.
*/
class UnsupportedDataTypeConfigException extends ConfigException {
}