Commit 94278ddd authored by Dries's avatar Dries

Issue #1866610 by Jose Reyero, Gábor Hojtsy, effulgentsia, alexpott,...

Issue #1866610 by Jose Reyero, Gábor Hojtsy, effulgentsia, alexpott, aspilicious, YesCT, fago, sun, dawehner, heyrocker, yched, spearhead93: Introduce Kwalify-inspired schema format for configuration.
parent 2735dbb4
......@@ -317,3 +317,16 @@ function config_get_entity_type_by_name($name) {
});
return key($entities);
}
/*
* Returns the typed config manager service.
*
* Use the typed data manager service for creating typed configuration objects.
*
* @see Drupal\Core\TypedData\TypedDataManager::create()
*
* @return Drupal\Core\TypedData\TypedConfigManager
*/
function config_typed() {
return drupal_container()->get('config.typed');
}
<?php
/**
* @file
* Contains \Drupal\Core\Config\Schema\ArrayElement.
*/
namespace Drupal\Core\Config\Schema;
use \ArrayAccess;
use \ArrayIterator;
use \Countable;
use \IteratorAggregate;
use \Traversable;
/**
* Defines a generic configuration element that contains multiple properties.
*/
abstract class ArrayElement extends Element implements IteratorAggregate, ArrayAccess, Countable {
/**
* Parsed elements.
*/
protected $elements;
/**
* Gets an array of contained elements.
*
* @return array
* Array of \Drupal\Core\Config\Schema\ArrayElement objects.
*/
protected function getElements() {
if (!isset($this->elements)) {
$this->elements = $this->parse();
}
return $this->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 array
* Array of \Drupal\Core\Config\Schema\ArrayElement objects.
*/
protected abstract function parse();
/**
* Implements TypedDataInterface::validate().
*/
public function validate() {
foreach ($this->getElements() as $element) {
if (!$element->validate()) {
return FALSE;
}
}
return TRUE;
}
/**
* Implements ArrayAccess::offsetExists().
*/
public function offsetExists($offset) {
return array_key_exists($offset, $this->getElements());
}
/**
* Implements ArrayAccess::offsetGet().
*/
public function offsetGet($offset) {
$elements = $this->getElements();
return $elements[$offset];
}
/**
* Implements ArrayAccess::offsetSet().
*/
public function offsetSet($offset, $value) {
if ($value instanceof TypedDataInterface) {
$value = $value->getValue();
}
$this->value[$offset] = $value;
unset($this->elements);
}
/**
* Implements ArrayAccess::offsetUnset().
*/
public function offsetUnset($offset) {
unset($this->value[$offset]);
unset($this->elements);
}
/**
* Implements Countable::count().
*/
public function count() {
return count($this->getElements());
}
/**
* Implements IteratorAggregate::getIterator();
*/
public function getIterator() {
return new ArrayIterator($this->getElements());
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Config\Schema\Element.
*/
namespace Drupal\Core\Config\Schema;
use Drupal\Core\TypedData\ContextAwareTypedData;
/**
* Defines a generic configuration element.
*/
abstract class Element extends ContextAwareTypedData {
/**
* The configuration value.
*
* @var mixed
*/
protected $value;
/**
* Create typed config object.
*/
protected function parseElement($key, $data, $definition) {
return config_typed()->create($definition, $data, $key, $this);
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Config\Schema\Mapping.
*/
namespace Drupal\Core\Config\Schema;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\TypedData\ComplexDataInterface;
use \InvalidArgumentException;
/**
* Defines a mapping configuration element.
*
* Wraps configuration data and metadata allowing access to configuration data
* using the ComplexDataInterface API. This object may contain any number and
* type of nested properties.
*/
class Mapping extends ArrayElement implements ComplexDataInterface {
/**
* Overrides ArrayElement::parse()
*/
protected function parse() {
$elements = array();
foreach ($this->definition['mapping'] as $key => $definition) {
if (isset($this->value[$key])) {
$elements[$key] = $this->parseElement($key, $this->value[$key], $definition);
}
}
return $elements;
}
/**
* Implements Drupal\Core\TypedData\ComplexDataInterface::get().
*/
public function get($property_name) {
$elements = $this->getElements();
if (isset($elements[$property_name])) {
return $elements[$property_name];
}
else {
throw new InvalidArgumentException(format_string("The configuration property @key doesn't exist.", array(
'@key' => $property_name,
)));
}
}
/**
* Implements Drupal\Core\TypedData\ComplexDataInterface::set().
*/
public function set($property_name, $value) {
// Set the data into the configuration array but behave according to the
// interface specification when we've got a null value.
if (isset($value)) {
$this->value[$property_name] = $value;
return $this->get($property_name);
}
else {
// In these objects, when clearing the value, the property is gone.
// As this needs to return a property, we get it before we delete it.
$property = $this->get($property_name);
unset($this->value[$property_name]);
$property->setValue($value);
return $property;
}
}
/**
* Implements Drupal\Core\TypedData\ComplexDataInterface::getProperties().
*/
public function getProperties($include_computed = FALSE) {
return $this->getElements();
}
/**
* Implements Drupal\Core\TypedData\ComplexDataInterface::getPropertyValues().
*/
public function getPropertyValues() {
return $this->getValue();
}
/**
* Implements Drupal\Core\TypedData\ComplexDataInterface::setPropertyValues().
*/
public function setPropertyValues($values) {
foreach ($values as $name => $value) {
$this->value[$name] = $value;
}
return $this;
}
/**
* Implements Drupal\Core\TypedData\ComplexDataInterface::getPropertyDefinition().
*/
public function getPropertyDefinition($name) {
if (isset($this->definition['mapping'][$name])) {
return $this->definition['mapping'][$name];
}
else {
return FALSE;
}
}
/**
* Implements Drupal\Core\TypedData\ComplexDataInterface::getPropertyDefinitions().
*/
public function getPropertyDefinitions() {
$list = array();
foreach ($this->getAllKeys() as $key) {
$list[$key] = $this->getPropertyDefinition($key);
}
return $list;
}
/**
* Implements Drupal\Core\TypedData\ComplexDataInterface::isEmpty().
*/
public function isEmpty() {
return empty($this->value);
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Config\Schema\Parser.
*/
namespace Drupal\Core\Config\Schema;
/**
* Parser.
*/
class Parser {
/**
* Parse configuration data against schema data.
*/
static function parse($data, $definition, $name = NULL, $parent = NULL) {
// Set default type depending on data and context.
if (!isset($definition['type'])) {
if (is_array($data) || !$context) {
$definition += array('type' => 'any');
}
else {
$definition += array('type' => 'str');
}
}
// Create typed data object.
config_typed()->create($definition, $data, $name, $parent);
}
/**
* Validate configuration data against schema data.
*/
static function validate($config_data, $schema_data) {
return self::parse($config_data, $schema_data)->validate();
}
static function getDefinition($type, $data) {
return config_definition($type);
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Config\Schema\Sequence.
*/
namespace Drupal\Core\Config\Schema;
/**
* Generic configuration property.
*/
class Property extends Element {
/**
* Implements TypedDataInterface::validate().
*/
public function validate() {
return isset($this->value);
}
}
<?php
/**
* @file
* Contains \Drupal\Config\Schema\SchemaDiscovery.
*/
namespace Drupal\Core\Config\Schema;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
use Drupal\Component\Utility\NestedArray;
/**
* A discovery mechanism that reads plugin definitions from schema data
* in YAML format.
*/
class SchemaDiscovery implements DiscoveryInterface {
/**
* A storage controller instance for reading configuration schema data.
*
* @var Drupal\Core\Config\StorageInterface
*/
protected $storage;
/**
* The array of plugin definitions, keyed by plugin id.
*
* @var array
*/
protected $definitions = array();
/**
* Public constructor.
*
* @param Drupal\Core\Config\StorageInterface $storage
* The storage controller object to use for reading schema data
*/
public function __construct($storage) {
$this->storage = $storage;
// Load definitions for all enabled modules.
foreach (module_list() as $module) {
$this->loadSchema($module);
}
// @todo Load definitions for all enabled themes.
}
/**
* Implements Drupal\Component\Plugin\Discovery\DiscoveryInterface::getDefinition().
*/
public function getDefinition($base_plugin_id) {
if (isset($this->definitions[$base_plugin_id])) {
$type = $base_plugin_id;
}
elseif (strpos($base_plugin_id, '.') && ($name = $this->getFallbackName($base_plugin_id)) && isset($this->definitions[$name])) {
// Found a generic name, replacing the last element by '*'.
$type = $name;
}
else {
// If we don't have definition, return the 'default' element.
// This should map to 'undefined' type by default, unless overridden.
$type = 'default';
}
$definition = $this->definitions[$type];
// Check whether this type is an extension of another one and compile it.
if (isset($definition['type'])) {
$merge = $this->getDefinition($definition['type']);
$definition = NestedArray::mergeDeep($merge, $definition);
// Unset type so we try the merge only once per type.
unset($definition['type']);
$this->definitions[$type] = $definition;
}
return $definition;
}
/**
* Implements Drupal\Component\Plugin\Discovery\DiscoveryInterface::getDefinitions().
*/
public function getDefinitions() {
return $this->definitions;
}
/**
* Load schema for module / theme.
*/
protected function loadSchema($component) {
if ($schema = $this->storage->read($component . '.schema')) {
foreach ($schema as $type => $definition) {
$this->definitions[$type] = $definition;
}
}
}
/**
* Gets fallback metadata name.
*
* @param string $name
* Configuration name or key.
*
* @return string
* Same name with the last part replaced by the filesystem marker.
*/
protected static function getFallbackName($name) {
$replaced = preg_replace('/\.[^.]+$/', '.' . '*', $name);
if ($replaced != $name) {
return $replaced;
}
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Config\Schema\Sequence.
*/
namespace Drupal\Core\Config\Schema;
use Drupal\Core\TypedData\ListInterface;
/**
* Defines a configuration element of type Sequence.
*/
class Sequence extends ArrayElement implements ListInterface {
/**
* Overrides ArrayElement::parse()
*/
protected function parse() {
$definition = $definition = $this->getItemDefinition();
$elements = array();
foreach ($this->value as $key => $value) {
$elements[$key] = $this->parseElement($key, $value, $definition);
}
return $elements;
}
/**
* Implements Drupal\Core\TypedData\ListInterface::isEmpty().
*/
public function isEmpty() {
return empty($this->value);
}
/**
* Implements Drupal\Core\TypedData\ListInterface::getItemDefinition().
*/
public function getItemDefinition() {
return $this->definition['sequence'][0];
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Config\TypedConfigElementFactory.
*/
namespace Drupal\Core\Config;
use Drupal\Core\TypedData\TypedDataFactory;
/**
* A factory for typed config element objects.
*
* This factory merges the type definition into the element definition prior to
* creating the instance.
*/
class TypedConfigElementFactory extends TypedDataFactory {
/**
* Overrides Drupal\Core\TypedData\TypedDataFactory::createInstance().
*/
public function createInstance($plugin_id, array $configuration, $name = NULL, $parent = NULL) {
$type_definition = $this->discovery->getDefinition($plugin_id);
$configuration += $type_definition;
return parent::createInstance($plugin_id, $configuration, $name, $parent);
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Config\TypedConfigManager.
*/
namespace Drupal\Core\Config;
use Drupal\Core\Config\Schema\SchemaDiscovery;
use Drupal\Core\TypedData\TypedDataManager;
/**
* Manages config type plugins.
*/
class TypedConfigManager extends TypedDataManager {
/**
* A storage controller instance for reading configuration data.
*
* @var Drupal\Core\Config\StorageInterface
*/
protected $storage;
/**
* Creates a new typed configuration manager.
*
* @param Drupal\Core\Config\StorageInterface $storage
* The storage controller object to use for reading schema data
*/
public function __construct($storage) {
$this->storage = $storage;
$this->discovery = new SchemaDiscovery($storage);
$this->factory = new TypedConfigElementFactory($this->discovery);
}
/**
* Gets typed configuration data.
*
* @param string $name
* Configuration object name.
*
* @return Drupal\Core\Config\Schema\Element
* Typed configuration element.
*/
public function get($name) {
$data = $this->storage->read($name);
$definition = $this->getDefinition($name);
return $this->create($definition, $data);
}
/**
* Overrides Drupal\Core\TypedData\TypedDataManager::create()
*
* Fills in default type and does variable replacement.
*/
public function create(array $definition, $value = NULL, $name = NULL, $parent = NULL) {
if (!isset($definition['type'])) {
// Set default type 'str' if possible. If not it will be 'any'.
if (is_string($value)) {
$definition['type'] = 'str';
}
else {
$definition['type'] = 'any';
}
}
elseif (strpos($definition['type'], ']')) {
// Replace variable names in definition.
$replace = is_array($value) ? $value : array();
if (isset($parent)) {
$replace['%parent'] = $parent->getValue();
}
if (isset($name)) {
$replace['%key'] = $name;
}
$definition['type'] = $this->replaceName($definition['type'], $replace);
}
// Create typed config object.
return parent::create($definition, $value, $name, $parent);
}
/**
* 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