Commit 90261479 authored by alexpott's avatar alexpott
Browse files

Issue #2298687 by tstoeckler, Jose Reyero, hussainweb, rpayanm: Sequence and...

Issue #2298687 by tstoeckler, Jose Reyero, hussainweb, rpayanm: Sequence and Mapping implement interfaces incorrectly, make them honest about what they support
parent f08f699e
......@@ -6,30 +6,34 @@
*/
namespace Drupal\Core\Config\Schema;
use Drupal\Core\TypedData\TraversableTypedDataInterface;
use Drupal\Component\Utility\String;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\TypedData\TypedData;
/**
* Defines a generic configuration element that contains multiple properties.
*/
abstract class ArrayElement extends Element implements \IteratorAggregate, TraversableTypedDataInterface, \ArrayAccess, \Countable {
abstract class ArrayElement extends TypedData implements \IteratorAggregate, TypedConfigInterface {
/**
* Parsed elements.
* The typed config manager.
*
* @var \Drupal\Core\Config\TypedConfigManagerInterface
*/
protected $elements;
protected $typedConfig;
/**
* Gets an array of contained elements.
* The configuration value.
*
* @return \Drupal\Core\TypedData\TypedDataInterface[]
* An array of elements contained in this element.
* @var mixed
*/
protected function getElements() {
if (!isset($this->elements)) {
$this->elements = $this->parse();
}
return $this->elements;
}
protected $value;
/**
* Parsed elements.
*/
protected $elements;
/**
* Gets valid configuration data keys.
......@@ -47,59 +51,97 @@ protected function getAllKeys() {
* @return \Drupal\Core\TypedData\TypedDataInterface[]
* An array of elements contained in this element.
*/
protected abstract function parse();
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);
/**
* Implements TypedDataInterface::validate().
* {@inheritdoc}
*/
public function validate() {
foreach ($this->getElements() as $element) {
if (!$element->validate()) {
return FALSE;
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;
}
}
}
return TRUE;
if (isset($element)) {
return $element;
}
else {
throw new \InvalidArgumentException(String::format("The configuration property @key doesn't exist.", array('@key' => $name)));
}
}
/**
* Implements ArrayAccess::offsetExists().
* {@inheritdoc}
*/
public function offsetExists($offset) {
return array_key_exists($offset, $this->getElements());
public function set($key, $value) {
$this->value[$key] = $value;
// Parsed elements must be rebuilt with new values
unset($this->elements);
// Directly notify ourselves.
$this->onChange($key, $value);
return $this;
}
/**
* Implements ArrayAccess::offsetGet().
* {@inheritdoc}
*/
public function offsetGet($offset) {
$elements = $this->getElements();
return $elements[$offset];
public function getElements() {
if (!isset($this->elements)) {
$this->elements = $this->parse();
}
return $this->elements;
}
/**
* Implements ArrayAccess::offsetSet().
* {@inheritdoc}
*/
public function offsetSet($offset, $value) {
if ($value instanceof TypedDataInterface) {
$value = $value->getValue();
}
$this->value[$offset] = $value;
unset($this->elements);
public function isEmpty() {
return empty($this->value);
}
/**
* Implements ArrayAccess::offsetUnset().
* {@inheritdoc}
*/
public function offsetUnset($offset) {
unset($this->value[$offset]);
unset($this->elements);
public function toArray() {
return isset($this->value) ? $this->value : array();
}
/**
* Implements Countable::count().
* {@inheritdoc}
*/
public function count() {
return count($this->getElements());
public function onChange($name) {
// Notify the parent of changes.
if (isset($this->parent)) {
$this->parent->onChange($this->name);
}
}
/**
......@@ -109,4 +151,52 @@ 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;
}
}
......@@ -7,7 +7,6 @@
namespace Drupal\Core\Config\Schema;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\TypedData\TypedData;
/**
......@@ -15,13 +14,6 @@
*/
abstract class Element extends TypedData {
/**
* The typed config manager.
*
* @var \Drupal\Core\Config\TypedConfigManagerInterface
*/
protected $typedConfig;
/**
* The configuration value.
*
......@@ -29,32 +21,4 @@ abstract class Element extends TypedData {
*/
protected $value;
/**
* Create typed config object.
*/
protected function parseElement($key, $data, $definition) {
return $this->typedConfig->create($definition, $data, $key, $this);
}
/**
* Build data definition object for contained elements.
*
* @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;
}
}
......@@ -12,10 +12,4 @@
*/
class Ignore extends Element {
/**
* {@inheritdoc}.
*/
public function validate() {
return TRUE;
}
}
......@@ -7,151 +7,28 @@
namespace Drupal\Core\Config\Schema;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Component\Utility\String;
/**
* 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.
* 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://drupal.org/node/1905070 for more details about configuration
* schema, types and type resolution.
*/
class Mapping extends ArrayElement implements ComplexDataInterface {
/**
* An array of data definitions.
*
* @var \Drupal\Core\TypedData\DataDefinitionInterface[]
*/
protected $propertyDefinitions;
/**
* {@inheritdoc}
*/
protected function parse() {
$elements = array();
foreach ($this->getPropertyDefinitions() as $key => $definition) {
$elements[$key] = $this->parseElement($key, $this->value[$key], $definition);
}
return $elements;
}
/**
* {@inheritdoc}
*
* Since all configuration objects are mappings the function will except a dot
* delimited key to access nested values, for example, 'page.front'.
*/
public function get($property_name) {
$parts = explode('.', $property_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 (method_exists($element, 'get')) {
$element = $element->get($key);
}
else {
$element = NULL;
}
}
}
if (isset($element)) {
return $element;
}
else {
throw new \InvalidArgumentException(String::format("The configuration property @key doesn't exist.", array('@key' => $property_name)));
}
}
/**
* Implements Drupal\Core\TypedData\ComplexDataInterface::set().
*/
public function set($property_name, $value, $notify = TRUE) {
// 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;
$property = $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);
}
// Notify the parent of any changes.
if ($notify && isset($this->parent)) {
$this->parent->onChange($this->name);
}
return $this;
}
/**
* Implements Drupal\Core\TypedData\ComplexDataInterface::getProperties().
*/
public function getProperties($include_computed = FALSE) {
return $this->getElements();
}
class Mapping extends ArrayElement {
/**
* {@inheritdoc}
*/
public function toArray() {
return $this->getValue();
}
/**
* Gets the definition of a contained property.
*
* @param string $name
* The name of property.
*
* @return \Drupal\Core\TypedData\DataDefinitionInterface|null
* The definition of the property or NULL if the property does not exist.
*/
public function getPropertyDefinition($name) {
$definitions = $this->getPropertyDefinitions();
return isset($definitions[$name]) ? isset($definitions[$name]) : NULL;
}
/**
* Gets an array of property definitions of contained properties.
*
* @return \Drupal\Core\TypedData\DataDefinitionInterface[]
* An array of property definitions of contained properties, keyed by
* property name.
*/
public function getPropertyDefinitions() {
if (!isset($this->propertyDefinitions)) {
$this->propertyDefinitions = array();
foreach ($this->getAllKeys() as $key) {
$definition = isset($this->definition['mapping'][$key]) ? $this->definition['mapping'][$key] : array();
$this->propertyDefinitions[$key] = $this->buildDataDefinition($definition, $this->value[$key], $key);
}
}
return $this->propertyDefinitions;
}
/**
* Implements Drupal\Core\TypedData\ComplexDataInterface::isEmpty().
*/
public function isEmpty() {
return empty($this->value);
}
/**
* Implements \Drupal\Core\TypedData\ComplexDataInterface::onChange().
*/
public function onChange($property_name) {
// Notify the parent of changes.
if (isset($this->parent)) {
$this->parent->onChange($this->name);
}
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);
}
}
......@@ -7,112 +7,24 @@
namespace Drupal\Core\Config\Schema;
use Drupal\Core\TypedData\ListInterface;
/**
* 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://drupal.org/node/1905070 for more details about configuration
* schema, types and type resolution.
*/
class Sequence extends ArrayElement implements ListInterface {
/**
* Data definition
*
* @var \Drupal\Core\TypedData\DataDefinitionInterface
*/
protected $itemDefinition;
class Sequence extends ArrayElement {
/**
* {@inheritdoc}
*/
protected function parse() {
// Creates a new data definition object for each item from the generic type
// definition array and actual configuration data for that item. Type
// definitions may contain variables to be replaced and those depend on
// each item's data.
protected function getElementDefinition($key) {
$value = isset($this->value[$key]) ? $this->value[$key] : NULL;
$definition = isset($this->definition['sequence'][0]) ? $this->definition['sequence'][0] : array();
$elements = array();
foreach ($this->value as $key => $value) {
$data_definition = $this->buildDataDefinition($definition, $value, $key);
$elements[$key] = $this->parseElement($key, $value, $data_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() {
if (!isset($this->itemDefinition)) {
$definition = isset($this->definition['sequence'][0]) ? $this->definition['sequence'][0] : array();
$this->itemDefinition = $this->buildDataDefinition($definition, NULL);
}
return $this->itemDefinition;
}
/**
* Implements \Drupal\Core\TypedData\ListInterface::onChange().
*/
public function onChange($delta) {
// Notify the parent of changes.
if (isset($this->parent)) {
$this->parent->onChange($this->name);
}
}
/**
* {@inheritdoc}
*/
public function get($key) {
$elements = $this->getElements();
return $elements[$key];
}
/**
* {@inheritdoc}
*/
public function first() {
return $this->get(0);
}
/**
* {@inheritdoc}
*/
public function set($index, $value) {
$this->offsetSet($index, $value);
return $this;
}
/**
* {@inheritdoc}
*/
public function removeItem($index) {
$this->offsetUnset($index);
return $this;
}
/**
* {@inheritdoc}
*/
public function appendItem($value = NULL) {
$offset = $this->count();
$this->offsetSet($offset, $value);
return $this->offsetGet($offset);
}
/**
* {@inheritdoc}
*/
public function filter($callback) {
$this->value = array_filter($this->value, $callback);
unset($this->elements);
return $this;
return $this->buildDataDefinition($definition, $value, $key);
}
}
<?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.
<