Commit dad3ef55 authored by webchick's avatar webchick

Issue #1913328 by fago: Provide general list and map classes.

parent b62c536c
......@@ -10,11 +10,7 @@
use Drupal\Core\Entity\Field\FieldInterface;
use Drupal\user\Plugin\Core\Entity\User;
use Drupal\Core\TypedData\ContextAwareInterface;
use Drupal\Core\TypedData\ContextAwareTypedData;
use Drupal\Core\TypedData\TypedDataInterface;
use ArrayIterator;
use IteratorAggregate;
use InvalidArgumentException;
use Drupal\Core\TypedData\ItemList;
/**
* Represents an entity field; that is, a list of field item objects.
......@@ -27,7 +23,7 @@
*
* @see \Drupal\Core\Entity\Field\FieldInterface
*/
class Field extends ContextAwareTypedData implements IteratorAggregate, FieldInterface {
class Field extends ItemList implements FieldInterface {
/**
* Numerically indexed array of field items, implementing the
......@@ -50,7 +46,7 @@ public function __construct(array $definition, $name = NULL, ContextAwareInterfa
}
/**
* Overrides \Drupal\Core\TypedData\TypedData::getValue().
* Overrides \Drupal\Core\TypedData\ItemList::getValue().
*/
public function getValue() {
if (isset($this->list)) {
......@@ -68,10 +64,7 @@ public function getValue() {
}
/**
* Overrides \Drupal\Core\TypedData\TypedData::setValue().
*
* @param array|null $values
* An array of values of the field items, or NULL to unset the field.
* Overrides \Drupal\Core\TypedData\ItemList::setValue().
*/
public function setValue($values) {
if (!isset($values) || $values === array()) {
......@@ -91,7 +84,7 @@ public function setValue($values) {
// Set the values.
foreach ($values as $delta => $value) {
if (!is_numeric($delta)) {
throw new InvalidArgumentException('Unable to set a value with a non-numeric delta in a list.');
throw new \InvalidArgumentException('Unable to set a value with a non-numeric delta in a list.');
}
elseif (!isset($this->list[$delta])) {
$this->list[$delta] = $this->createItem($delta, $value);
......@@ -103,111 +96,6 @@ public function setValue($values) {
}
}
/**
* Overrides \Drupal\Core\TypedData\TypedData::getString().
*/
public function getString() {
$strings = array();
if (isset($this->list)) {
foreach ($this->list as $item) {
$strings[] = $item->getString();
}
return implode(', ', array_filter($strings));
}
}
/**
* Overrides \Drupal\Core\TypedData\TypedData::getConstraints().
*/
public function getConstraints() {
// Apply the constraints to the list items only.
return array();
}
/**
* Implements \ArrayAccess::offsetExists().
*/
public function offsetExists($offset) {
return isset($this->list) && array_key_exists($offset, $this->list);
}
/**
* Implements \ArrayAccess::offsetUnset().
*/
public function offsetUnset($offset) {
if (isset($this->list)) {
unset($this->list[$offset]);
}
}
/**
* Implements \ArrayAccess::offsetGet().
*/
public function offsetGet($offset) {
if (!is_numeric($offset)) {
throw new InvalidArgumentException('Unable to get a value with a non-numeric delta in a list.');
}
// Allow getting not yet existing items as well.
// @todo: Maybe add a public createItem() method in addition?
elseif (!isset($this->list[$offset])) {
$this->list[$offset] = $this->createItem($offset);
}
return $this->list[$offset];
}
/**
* Helper for creating a list item object.
*
* @return \Drupal\Core\TypedData\TypedDataInterface
*/
protected function createItem($offset = 0, $value = NULL) {
return typed_data()->getPropertyInstance($this, $offset, $value);
}
/**
* Implements \Drupal\Core\TypedData\ListInterface::getItemDefinition().
*/
public function getItemDefinition() {
return array('list' => FALSE) + $this->definition;
}
/**
* Implements \ArrayAccess::offsetSet().
*/
public function offsetSet($offset, $value) {
if (!isset($offset)) {
// The [] operator has been used so point at a new entry.
$offset = $this->list ? max(array_keys($this->list)) + 1 : 0;
}
if (is_numeric($offset)) {
// Support setting values via typed data objects.
if ($value instanceof TypedDataInterface) {
$value = $value->getValue();
}
$this->offsetGet($offset)->setValue($value);
}
else {
throw new InvalidArgumentException('Unable to set a value with a non-numeric delta in a list.');
}
}
/**
* Implements \IteratorAggregate::getIterator().
*/
public function getIterator() {
if (isset($this->list)) {
return new ArrayIterator($this->list);
}
return new ArrayIterator(array());
}
/**
* Implements \Countable::count().
*/
public function count() {
return isset($this->list) ? count($this->list) : 0;
}
/**
* Implements \Drupal\Core\Entity\Field\FieldInterface::getPropertyDefinition().
*/
......@@ -257,34 +145,6 @@ public function __unset($property_name) {
return $this->offsetGet(0)->__unset($property_name);
}
/**
* Implements \Drupal\Core\TypedData\ListInterface::isEmpty().
*/
public function isEmpty() {
if (isset($this->list)) {
foreach ($this->list as $item) {
if (!$item->isEmpty()) {
return FALSE;
}
}
}
return TRUE;
}
/**
* Magic method: Implements a deep clone.
*/
public function __clone() {
if (isset($this->list)) {
foreach ($this->list as $delta => $item) {
$this->list[$delta] = clone $item;
if ($item instanceof ContextAwareInterface) {
$this->list[$delta]->setContext($delta, $this);
}
}
}
}
/**
* Implements \Drupal\Core\TypedData\AccessibleInterface::access().
*/
......
<?php
/**
* @file
* Contains \Drupal\Core\TypedData\List.
*/
namespace Drupal\Core\TypedData;
/**
* A generic list class.
*
* This class can serve as list for any type of items.
* Note: The class cannot be called "List" as list is a reserved PHP keyword.
*/
class ItemList extends ContextAwareTypedData implements \IteratorAggregate, ListInterface {
/**
* Numerically indexed array items.
*
* @var array
*/
protected $list = array();
/**
* Overrides \Drupal\Core\TypedData\TypedData::getValue().
*/
public function getValue() {
if (isset($this->list)) {
$values = array();
foreach ($this->list as $delta => $item) {
$values[$delta] = $item->getValue();
}
return $values;
}
}
/**
* Overrides \Drupal\Core\TypedData\TypedData::setValue().
*
* @param array|null $values
* An array of values of the field items, or NULL to unset the field.
*/
public function setValue($values) {
if (!isset($values) || $values === array()) {
$this->list = $values;
}
else {
if (!is_array($values)) {
throw new \InvalidArgumentException('Cannot set a list with a non-array value.');
}
// Clear the values of properties for which no value has been passed.
if (isset($this->list)) {
$this->list = array_intersect_key($this->list, $values);
}
// Set the values.
foreach ($values as $delta => $value) {
if (!is_numeric($delta)) {
throw new \InvalidArgumentException('Unable to set a value with a non-numeric delta in a list.');
}
elseif (!isset($this->list[$delta])) {
$this->list[$delta] = $this->createItem($delta, $value);
}
else {
$this->list[$delta]->setValue($value);
}
}
}
}
/**
* Overrides \Drupal\Core\TypedData\TypedData::getString().
*/
public function getString() {
$strings = array();
if (isset($this->list)) {
foreach ($this->list as $item) {
$strings[] = $item->getString();
}
// Remove any empty strings resulting from empty items.
return implode(', ', array_filter($strings, 'drupal_strlen'));
}
}
/**
* Overrides \Drupal\Core\TypedData\TypedData::getConstraints().
*/
public function getConstraints() {
// Apply the constraints to the list items only.
return array();
}
/**
* Implements \ArrayAccess::offsetExists().
*/
public function offsetExists($offset) {
return isset($this->list) && array_key_exists($offset, $this->list) && $this->offsetGet($offset)->getValue() !== NULL;
}
/**
* Implements \ArrayAccess::offsetUnset().
*/
public function offsetUnset($offset) {
if (isset($this->list)) {
unset($this->list[$offset]);
}
}
/**
* Implements \ArrayAccess::offsetGet().
*/
public function offsetGet($offset) {
if (!is_numeric($offset)) {
throw new \InvalidArgumentException('Unable to get a value with a non-numeric delta in a list.');
}
// Allow getting not yet existing items as well.
// @todo: Maybe add a public createItem() method in addition?
elseif (!isset($this->list[$offset])) {
$this->list[$offset] = $this->createItem($offset);
}
return $this->list[$offset];
}
/**
* Helper for creating a list item object.
*
* @return \Drupal\Core\TypedData\TypedDataInterface
*/
protected function createItem($offset = 0, $value = NULL) {
return typed_data()->getPropertyInstance($this, $offset, $value);
}
/**
* Implements \Drupal\Core\TypedData\ListInterface::getItemDefinition().
*/
public function getItemDefinition() {
return array('list' => FALSE) + $this->definition;
}
/**
* Implements \ArrayAccess::offsetSet().
*/
public function offsetSet($offset, $value) {
if (!isset($offset)) {
// The [] operator has been used so point at a new entry.
$offset = $this->list ? max(array_keys($this->list)) + 1 : 0;
}
if (is_numeric($offset)) {
// Support setting values via typed data objects.
if ($value instanceof TypedDataInterface) {
$value = $value->getValue();
}
$this->offsetGet($offset)->setValue($value);
}
else {
throw new \InvalidArgumentException('Unable to set a value with a non-numeric delta in a list.');
}
}
/**
* Implements \IteratorAggregate::getIterator().
*/
public function getIterator() {
if (isset($this->list)) {
return new \ArrayIterator($this->list);
}
return new \ArrayIterator(array());
}
/**
* Implements \Countable::count().
*/
public function count() {
return isset($this->list) ? count($this->list) : 0;
}
/**
* Implements \Drupal\Core\TypedData\ListInterface::isEmpty().
*/
public function isEmpty() {
if (isset($this->list)) {
foreach ($this->list as $item) {
if ($item instanceof ComplexDataInterface || $item instanceof ListInterface) {
if (!$item->isEmpty()) {
return FALSE;
}
}
// Other items are treated as empty if they have no value only.
elseif ($item->getValue() !== NULL) {
return FALSE;
}
}
}
return TRUE;
}
/**
* Magic method: Implements a deep clone.
*/
public function __clone() {
if (isset($this->list)) {
foreach ($this->list as $delta => $item) {
$this->list[$delta] = clone $item;
if ($item instanceof ContextAwareInterface) {
$this->list[$delta]->setContext($delta, $this);
}
}
}
}
}
<?php
/**
* @file
* Contains \Drupal\Core\TypedData\Type\Any.
*/
namespace Drupal\Core\TypedData\Type;
use Drupal\Core\TypedData\TypedData;
/**
* The "any" data type.
*
* The "any" data type does not implement a list or complex data interface, nor
* is it mappable to any primitive type. Thus, it may contain any PHP data for
* which no further metadata is available.
*/
class Any extends TypedData {
/**
* The data value.
*
* @var mixed
*/
protected $value;
}
<?php
/**
* @file
* Contains \Drupal\Core\TypedData\Type\Map.
*/
namespace Drupal\Core\TypedData\Type;
use Drupal\Core\TypedData\ContextAwareTypedData;
use Drupal\Core\TypedData\ComplexDataInterface;
/**
* The "map" data type.
*
* The "map" data type represent a simple complex data type, e.g. for
* representing associative arrays. It can also serve as base class for any
* complex data type.
*
* By default there is no metadata for contained properties. Extending classes
* may want to override Map::getPropertyDefinitions() to define it.
*/
class Map extends ContextAwareTypedData implements \IteratorAggregate, ComplexDataInterface {
/**
* An array of values for the contained properties.
*
* @var array
*/
protected $values = array();
/**
* The array of properties, each implementing the TypedDataInterface.
*
* @var array
*/
protected $properties;
/**
* Implements \Drupal\Core\TypedData\ComplexDataInterface::getPropertyDefinitions().
*/
public function getPropertyDefinitions() {
$definitions = array();
foreach ($this->values as $name => $value) {
$definitions[$name] = array(
'type' => 'any',
);
}
return $definitions;
}
/**
* Overrides \Drupal\Core\TypedData\TypedData::getValue().
*/
public function getValue() {
return $this->values;
}
/**
* Overrides \Drupal\Core\TypedData\TypedData::setValue().
*
* @param array|null $values
* An array of property values.
*/
public function setValue($values) {
if (isset($values) && !is_array($values)) {
throw new \InvalidArgumentException("Invalid values given. Values must be represented as an associative array.");
}
$this->values = $values;
unset($this->properties);
}
/**
* Overrides \Drupal\Core\TypedData\TypedData::getString().
*/
public function getString() {
$strings = array();
foreach ($this->getProperties() as $property) {
$strings[] = $property->getString();
}
// Remove any empty strings resulting from empty items.
return implode(', ', array_filter($strings, 'drupal_strlen'));
}
/**
* Implements \Drupal\Core\TypedData\ComplexDataInterface::get().
*/
public function get($property_name) {
if (!$this->getPropertyDefinition($property_name)) {
throw new \InvalidArgumentException('Property ' . check_plain($property_name) . ' is unknown.');
}
if (!isset($this->properties[$property_name])) {
$this->properties[$property_name] = typed_data()->getPropertyInstance($this, $property_name, isset($this->values[$property_name]) ? $this->values[$property_name] : NULL);
}
return $this->properties[$property_name];
}
/**
* Implements \Drupal\Core\TypedData\ComplexDataInterface::set().
*/
public function set($property_name, $value) {
$this->get($property_name)->setValue($value);
}
/**
* Implements \Drupal\Core\TypedData\ComplexDataInterface::getProperties().
*/
public function getProperties($include_computed = FALSE) {
$properties = array();
foreach ($this->getPropertyDefinitions() as $name => $definition) {
if ($include_computed || empty($definition['computed'])) {
$properties[$name] = $this->get($name);
}
}
return $properties;
}
/**
* Implements \Drupal\Core\TypedData\ComplexDataInterface::getPropertyValues().
*/
public function getPropertyValues() {
$values = array();
foreach ($this->getProperties() as $name => $property) {
$values[$name] = $property->getValue();
}
return $values;
}
/**
* Implements \Drupal\Core\TypedData\ComplexDataInterface::setPropertyValues().
*/
public function setPropertyValues($values) {
foreach ($values as $name => $value) {
$this->get($name)->setValue($value);
}
}
/**
* Implements \IteratorAggregate::getIterator().
*/
public function getIterator() {
return new \ArrayIterator($this->getProperties());
}
/**
* Implements \Drupal\Core\TypedData\ComplexDataInterface::getPropertyDefinition().
*/
public function getPropertyDefinition($name) {
$definitions = $this->getPropertyDefinitions();
if (isset($definitions[$name])) {
return $definitions[$name];
}
else {
return FALSE;
}
}
/**
* Implements \Drupal\Core\TypedData\ComplexDataInterface::isEmpty().
*/
public function isEmpty() {
foreach ($this->getProperties() as $property) {
if ($property->getValue() !== NULL) {
return FALSE;
}
}
return TRUE;
}
/**
* Magic method: Implements a deep clone.
*/
public function __clone() {
foreach ($this->getProperties() as $name => $property) {
$this->properties[$name] = clone $property;
if ($property instanceof ContextAwareInterface) {
$this->properties[$name]->setContext($name, $this);
}
}
}
}
......@@ -8,6 +8,7 @@
namespace Drupal\Core\TypedData;
use InvalidArgumentException;
use Drupal\Component\Plugin\Discovery\ProcessDecorator;
use Drupal\Component\Plugin\PluginManagerBase;
use Drupal\Core\Plugin\Discovery\CacheDecorator;
use Drupal\Core\Plugin\Discovery\HookDiscovery;
......@@ -36,6 +37,17 @@ class TypedDataManager extends PluginManagerBase {
*/
protected $constraintManager;
/**
* Type definition defaults which are merged in by the ProcessDecorator.
*
* @see \Drupal\Component\Plugin\PluginManagerBase::processDefinition()
*
* @var array
*/
protected $defaults = array(
'list class' => '\Drupal\Core\TypedData\ItemList',
);
/**
* An array of typed data property prototypes.
*
......@@ -44,7 +56,10 @@ class TypedDataManager extends PluginManagerBase {
protected $prototypes = array();
public function __construct() {
$this->discovery = new CacheDecorator(new HookDiscovery('data_type_info'), 'typed_data:types');
$this->discovery = new HookDiscovery('data_type_info');
$this->discovery = new ProcessDecorator($this->discovery, array($this, 'processDefinition'));
$this->discovery = new CacheDecorator($this->discovery, 'typed_data:types');
$this->factory = new TypedDataFactory($this->discovery);
}
......@@ -191,14 +206,21 @@ public function getInstance(array $options) {
* @see \Drupal\Core\TypedData\TypedDataManager::create()
*/
public function getPropertyInstance(ContextAwareInterface $object, $property_name, $value = NULL) {
$key = $object->getRoot()->getType() . ':' . $object->getPropertyPath() . '.';
// If we are creating list items, we always use 0 in the key as all list
// items look the same.
$key .= is_numeric($property_name) ? 0 : $property_name;
if ($root = $object->getRoot()) {
$key = $root->getType() . ':' . $object->getPropertyPath() . '.';
// If we are creating list items, we always use 0 in the key as all list
// items look the same.
$key .= is_numeric($property_name) ? 0 : $property_name;
}
else {
// Missing context, thus we cannot determine a unique key for prototyping.
// Fall back to create a new prototype on each call.
$key = FALSE;
}
// Make sure we have a prototype. Then, clone the prototype and set object
// specific values, i.e. the value and the context.
if (!isset($this->prototypes[$key])) {
if (!isset($this->prototypes[$key]) || !$key) {
// Create the initial prototype. For that we need to fetch the definition
// of the to be created property instance from the parent.
if ($object instanceof ComplexDataInterface) {
......
......@@ -197,6 +197,208 @@ public function testGetAndSet() {
$this->assertEqual($typed_data->validate()->count(), 0);
$typed_data->setValue('invalid');
$this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.');
// Any type.
$value = array('foo');
$typed_data = $this->createTypedData(array('type' => 'any'), $value);
$this->assertIdentical($typed_data->getValue(), $value, 'Any value was fetched.');
$new_value = 'test@example.com';
$typed_data->setValue($new_value);
$this->assertIdentical($typed_data->getValue(), $new_value, 'Any value was changed.');
$this->assertTrue(is_string($typed_data->getString()), 'Any value was converted to string');
$this->assertEqual($typed_data->validate()->count(), 0);
$typed_data->setValue(NULL);
$this->assertNull($typed_data->getValue(), 'Any wrapper is null-able.');
$this->assertEqual($typed_data->validate()->count(), 0);
// We cannot test invalid values as everything is valid for the any type,
// but make sure an array or object value passes validation also.
$typed_data->setValue(array('entry'));
$this->assertEqual($typed_data->validate()->count(), 0);
$typed_data->setValue((object) array('entry'));
$this->assertEqual($typed_data->validate()->count(), 0);
}