diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index 59d80d4..99097de 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -201,6 +201,14 @@ const LANGUAGE_NOT_APPLICABLE = 'zxx'; const LANGUAGE_MULTIPLE = 'mul'; /** + * Language code referring to the default language of data, e.g. of an entity. + * + * @todo Change value to differ from LANGUAGE_NOT_SPECIFIED. Try to find a code + * defined by a standard (e.g., ISO or W3C). + */ +const LANGUAGE_DEFAULT = 'und'; + +/** * The language state when referring to configurable languages. */ const LANGUAGE_CONFIGURABLE = 1; diff --git a/core/includes/common.inc b/core/includes/common.inc index c9913a2..d0bd908 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -7286,6 +7286,84 @@ function drupal_get_updaters() { } /** + * Gets information about all data types. + * + * @see drupal_wrap_data() + * @see hook_data_type_info() + */ +function drupal_get_data_type_info() { + $items = &drupal_static('data_type_info'); + if (!isset($items)) { + $items = module_invoke_all('data_type_info'); + drupal_alter('data_type_info', $items); + } + return $items; +} + +/** + * Wraps a data value into a typed data wrapper. + * + * @param array $definition + * The data definition array with the following array keys and values: + * - type: The data type of the data to wrap. Required. + * - label: A human readable label. + * - description: A human readable description. + * - list: Whether the wrapped data is multi-valued, i.e. a list of data + * items. Defaults to FALSE. + * - computed: A boolean specifying whether the data value is computed by the + * wrapper, e.g. depending on some other values. + * - read-only: A boolean specifying whether the wrapped data is read-only. + * Defaults to TRUE for computed properties, to FALSE otherwise. + * - class: If set and 'list' is FALSE, the class to use for creating the + * typed data wrapper. Else the default class as specified for the data type + * will be used. + * - list class: If set and 'list' is TRUE, the class to use for creating the + * typed data wrapper. Else the default list class as specified for the data + * type will be used. + * - settings: An array of settings, as required by the used class. See the + * documentation of the used class for supported or required settings. + * - constraints: An array of type specific value constraints, e.g. for data + * of type 'entity' the 'entity type' and 'bundle' may be specified. See the + * documentation of the data type class for supported constraints. + * - required: A boolean specifying whether a non-NULL value is mandatory. + * Further keys may be supported in certain usages, e.g. for further keys + * supported for entity property definitions see + * Drupal\entity\StorageControllerInterface::getPropertyDefinitions(). + * @param mixed $value + * (optional) The data value. If set, it has to match the data type format as + * documented for the data type classes. + * @param array $context + * (optional) An array describing the context of the data. It should be + * passed if a data value is wrapped as part of a data structure. The + * following keys are supported: + * - name: The name of the data being wrapped. + * - parent: The parent object containing the data. Must be an instance of + * Drupal\Core\TypedData\StructureInterface or + * Drupal\Core\TypedData\ListInterface. + * + * @return Drupal\Core\TypedData\WrapperInterface + * + * @see drupal_get_data_type_info() + * @see Drupal\Core\TypedData\Type\Integer + * @see Drupal\Core\TypedData\Type\Decimal + * @see Drupal\Core\TypedData\Type\String + * @see Drupal\Core\TypedData\Type\Boolean + * @see Drupal\Core\TypedData\Type\Duration + * @see Drupal\Core\TypedData\Type\Date + * @see Drupal\Core\TypedData\Type\Uri + * @see Drupal\Core\TypedData\Type\Binary + * @see Drupal\entity\Property\EntityWrapper + */ +function drupal_wrap_data(array $definition, $value = NULL, array $context = array()) { + $type_info = drupal_get_data_type_info(); + + // Allow per-data definition overrides of the used classes. + $full_definition = $definition + $type_info[$definition['type']]; + $class = empty($full_definition['list']) ? $full_definition['class'] : $full_definition['list class']; + return new $class($full_definition, $value, $context); +} + +/** * Assembles the Drupal FileTransfer registry. * * @return diff --git a/core/lib/Drupal/Core/TypedData/AccessibleInterface.php b/core/lib/Drupal/Core/TypedData/AccessibleInterface.php new file mode 100644 index 0000000..3c42f7a --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/AccessibleInterface.php @@ -0,0 +1,27 @@ +handle) && isset($this->uri)) { + $this->handle = fopen($this->uri, 'rb'); + } + return $this->handle; + } + + /** + * Implements WrapperInterface::setValue(). + */ + public function setValue($value) { + if (!isset($value)) { + $this->handle = NULL; + $this->uri = NULL; + } + elseif (is_resource($value)) { + $this->handle = $value; + } + elseif (is_string($value)) { + $this->uri = $value; + } + else { + throw new InvalidArgumentException("Invalid value for binary data given."); + } + } + + /** + * Implements WrapperInterface::getString(). + */ + public function getString() { + $contents = ''; + while (!feof($this->getValue())) { + $contents .= fread($this->handle, 8192); + } + return $contents; + } + + /** + * Implements WrapperInterface::validate(). + */ + public function validate() { + // TODO: Implement validate() method. + } +} diff --git a/core/lib/Drupal/Core/TypedData/Type/Boolean.php b/core/lib/Drupal/Core/TypedData/Type/Boolean.php new file mode 100644 index 0000000..de67e06 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Type/Boolean.php @@ -0,0 +1,39 @@ +value = isset($value) ? (bool) $value : $value; + } + + /** + * Implements WrapperInterface::validate(). + */ + public function validate() { + // TODO: Implement validate() method. + } +} diff --git a/core/lib/Drupal/Core/TypedData/Type/Date.php b/core/lib/Drupal/Core/TypedData/Type/Date.php new file mode 100644 index 0000000..1e20b5d --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Type/Date.php @@ -0,0 +1,68 @@ +value; + } + + /** + * Implements WrapperInterface::setValue(). + */ + public function setValue($value) { + if ($value instanceof DateTime || !isset($value)) { + $this->value = $value; + } + elseif (is_integer($value)) { + // Value is a timestamp. + $this->value = new DateTime('@' . $value); + } + elseif (is_string($value)) { + $this->value = new DateTime($value); + } + else { + throw new InvalidArgumentException("Invalid date format given."); + } + } + + /** + * Implements WrapperInterface::getString(). + */ + public function getString() { + return (string) $this->getValue()->format(DateTime::ISO8601); + } + + /** + * Implements WrapperInterface::validate(). + */ + public function validate() { + // TODO: Implement validate() method. + } +} diff --git a/core/lib/Drupal/Core/TypedData/Type/Decimal.php b/core/lib/Drupal/Core/TypedData/Type/Decimal.php new file mode 100644 index 0000000..3bede7e --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Type/Decimal.php @@ -0,0 +1,39 @@ +value = isset($value) ? (float) $value : $value; + } + + /** + * Implements WrapperInterface::validate(). + */ + public function validate() { + // TODO: Implement validate() method. + } +} diff --git a/core/lib/Drupal/Core/TypedData/Type/Duration.php b/core/lib/Drupal/Core/TypedData/Type/Duration.php new file mode 100644 index 0000000..b069bd2 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Type/Duration.php @@ -0,0 +1,64 @@ +value = $value; + } + elseif (is_integer($value)) { + // Value is a time span in seconds. + $this->value = new DateInterval('PT' . $value . 'S'); + } + elseif (is_string($value)) { + // @todo: Add support for negative intervals on top of the DateInterval + // constructor. + $this->value = new DateInterval($value); + } + else { + throw new InvalidArgumentException("Invalid duration format given."); + } + } + + /** + * Implements WrapperInterface::getString(). + */ + public function getString() { + return (string) $this->getValue()->format('%rP%yY%mM%dDT%hH%mM%sS'); + } + + /** + * Implements WrapperInterface::validate(). + */ + public function validate() { + // TODO: Implement validate() method. + } +} diff --git a/core/lib/Drupal/Core/TypedData/Type/Integer.php b/core/lib/Drupal/Core/TypedData/Type/Integer.php new file mode 100644 index 0000000..1d6bf3f --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Type/Integer.php @@ -0,0 +1,39 @@ +value = isset($value) ? (int) $value : $value; + } + + /** + * Implements WrapperInterface::validate(). + */ + public function validate() { + // TODO: Implement validate() method. + } +} diff --git a/core/lib/Drupal/Core/TypedData/Type/Language.php b/core/lib/Drupal/Core/TypedData/Type/Language.php new file mode 100644 index 0000000..068a32e --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Type/Language.php @@ -0,0 +1,93 @@ +definition = $definition; + + if (isset($context['parent']) && !empty($this->definition['settings']['langcode source'])) { + $this->langcode = $context['parent']->get($this->definition['settings']['langcode source']); + } + else { + // No context given, so just initialize an langcode property for storing + // the code. + $this->langcode = drupal_wrap_data(array('type' => 'string')); + } + + if (isset($value)) { + $this->setValue($value); + } + } + + /** + * Implements WrapperInterface::getValue(). + */ + public function getValue() { + $langcode = $this->langcode->getValue(); + return $langcode ? language_load($langcode) : NULL; + } + + /** + * Implements WrapperInterface::setValue(). + * + * Both the langcode and the language object may be passed as value. + */ + public function setValue($value) { + if (!isset($value)) { + $this->langcode->setValue(NULL); + } + elseif (is_scalar($value)) { + $this->langcode->setValue($value); + } + elseif (is_object($value)) { + $this->langcode->setValue($value->langcode); + } + else { + throw new InvalidArgumentException('Value is no valid langcode or language object.'); + } + } + + /** + * Implements WrapperInterface::getString(). + */ + public function getString() { + $language = $this->getValue(); + return $language ? $language->name : ''; + } + + /** + * Implements WrapperInterface::validate(). + */ + public function validate() { + // TODO: Implement validate() method. + } +} diff --git a/core/lib/Drupal/Core/TypedData/Type/String.php b/core/lib/Drupal/Core/TypedData/Type/String.php new file mode 100644 index 0000000..2adce82 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Type/String.php @@ -0,0 +1,39 @@ +value = isset($value) ? (string) $value : $value; + } + + /** + * Implements WrapperInterface::validate(). + */ + public function validate() { + // TODO: Implement validate() method. + } +} diff --git a/core/lib/Drupal/Core/TypedData/Type/Uri.php b/core/lib/Drupal/Core/TypedData/Type/Uri.php new file mode 100644 index 0000000..9b933ea --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Type/Uri.php @@ -0,0 +1,38 @@ +value = isset($value) ? (string) $value : $value; + } + + /** + * Implements WrapperInterface::validate(). + */ + public function validate() { + // TODO: Implement validate() method. + } +} diff --git a/core/lib/Drupal/Core/TypedData/Type/WrapperBase.php b/core/lib/Drupal/Core/TypedData/Type/WrapperBase.php new file mode 100644 index 0000000..6411cb6 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Type/WrapperBase.php @@ -0,0 +1,86 @@ +definition = $definition; + if (isset($value)) { + $this->setValue($value); + } + } + + /** + * Implements WrapperInterface::getType(). + * + * @return string + */ + public function getType() { + return $this->definition['type']; + } + + /** + * Implements WrapperInterface::getDefinition(). + * + * @return array + */ + public function getDefinition() { + return $this->definition; + } + + /** + * Implements WrapperInterface::getValue(). + * + * @return mixed + */ + public function getValue() { + return $this->value; + } + + /** + * Implements WrapperInterface::setValue(). + * + * @param mixed $value + */ + public function setValue($value) { + $this->value = $value; + } + + /** + * Implements WrapperInterface::getString(). + * + * @return string + */ + public function getString() { + return (string) $this->getValue(); + } +} diff --git a/core/lib/Drupal/Core/TypedData/WrapperInterface.php b/core/lib/Drupal/Core/TypedData/WrapperInterface.php new file mode 100644 index 0000000..75acaf5 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/WrapperInterface.php @@ -0,0 +1,83 @@ +assertTrue($wrapper instanceof \Drupal\Core\TypedData\WrapperInterface, 'Wrapper is an instance of the wrapper interface.'); + $definition = $wrapper->getDefinition(); + $this->assertTrue(!empty($definition['label']), $definition['label'] . ' wrapper definition was returned.'); + // Assert that the correct type was constructed. + $this->assertEqual($wrapper->getType(), $type, $definition['label'] . ' wrapper returned type.'); + return $wrapper; + } + + /** * Pass if the internal browser's URL matches the given path. * * @param $path diff --git a/core/modules/system/lib/Drupal/system/Tests/TypedData/DataWrapperTest.php b/core/modules/system/lib/Drupal/system/Tests/TypedData/DataWrapperTest.php new file mode 100644 index 0000000..23dd238 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/TypedData/DataWrapperTest.php @@ -0,0 +1,130 @@ + 'Test data wrappers', + 'description' => 'Tests the functionality of all core data wrappers.', + 'group' => 'Typed Data API', + ); + } + + /** + * Tests the basics around constructing and working with data wrappers. + */ + public function testGetAndSet() { + // Boolean type. + $wrapper = $this->drupalWrapData(array('type' => 'boolean'), TRUE); + $this->assertTrue($wrapper->getValue() === TRUE, 'Boolean value was fetched.'); + $wrapper->setValue(FALSE); + $this->assertTrue($wrapper->getValue() === FALSE, 'Boolean value was changed.'); + $this->assertTrue(is_string($wrapper->getString()), 'Boolean value was converted to string'); + $wrapper->setValue(NULL); + $this->assertNull($wrapper->getValue(), 'Boolean wrapper is null-able.'); + + // String type. + $value = $this->randomString(); + $wrapper = $this->drupalWrapData(array('type' => 'string'), $value); + $this->assertTrue($wrapper->getValue() === $value, 'String value was fetched.'); + $new_value = $this->randomString(); + $wrapper->setValue($new_value); + $this->assertTrue($wrapper->getValue() === $new_value, 'String value was changed.'); + // Funky test. + $this->assertTrue(is_string($wrapper->getString()), 'String value was converted to string'); + $wrapper->setValue(NULL); + $this->assertNull($wrapper->getValue(), 'String wrapper is null-able.'); + + // Integer type. + $value = rand(); + $wrapper = $this->drupalWrapData(array('type' => 'integer'), $value); + $this->assertTrue($wrapper->getValue() === $value, 'Integer value was fetched.'); + $new_value = rand(); + $wrapper->setValue($new_value); + $this->assertTrue($wrapper->getValue() === $new_value, 'Integer value was changed.'); + $this->assertTrue(is_string($wrapper->getString()), 'Integer value was converted to string'); + $wrapper->setValue(NULL); + $this->assertNull($wrapper->getValue(), 'Integer wrapper is null-able.'); + + // Decimal type. + $value = 123.45; + $wrapper = $this->drupalWrapData(array('type' => 'decimal'), $value); + $this->assertTrue($wrapper->getValue() === $value, 'Decimal value was fetched.'); + $new_value = 678.90; + $wrapper->setValue($new_value); + $this->assertTrue($wrapper->getValue() === $new_value, 'Decimal value was changed.'); + $this->assertTrue(is_string($wrapper->getString()), 'Decimal value was converted to string'); + $wrapper->setValue(NULL); + $this->assertNull($wrapper->getValue(), 'Decimal wrapper is null-able.'); + + // Date type. + $value = new DateTime('@' . REQUEST_TIME); + $wrapper = $this->drupalWrapData(array('type' => 'date'), $value); + $this->assertTrue($wrapper->getValue() === $value, 'Date value was fetched.'); + $new_value = REQUEST_TIME + 1; + $wrapper->setValue($new_value); + $this->assertTrue($wrapper->getValue()->getTimestamp() === $new_value, 'Date value was changed and set by timestamp.'); + $wrapper->setValue('2000-01-01'); + $this->assertTrue($wrapper->getValue()->format('Y-m-d') == '2000-01-01', 'Date value was changed and set by date string.'); + $this->assertTrue(is_string($wrapper->getString()), 'Date value was converted to string'); + $wrapper->setValue(NULL); + $this->assertNull($wrapper->getValue(), 'Date wrapper is null-able.'); + + // Duration type. + $value = new DateInterval('PT20S'); + $wrapper = $this->drupalWrapData(array('type' => 'duration'), $value); + $this->assertTrue($wrapper->getValue() === $value, 'Duration value was fetched.'); + $wrapper->setValue(10); + $this->assertTrue($wrapper->getValue()->s == 10, 'Duration value was changed and set by time span in seconds.'); + $wrapper->setValue('P40D'); + $this->assertTrue($wrapper->getValue()->d == 40, 'Duration value was changed and set by duration string.'); + $this->assertTrue(is_string($wrapper->getString()), 'Duration value was converted to string'); + // Test getting the string and passing it back as value. + $duration = $wrapper->getString(); + $wrapper->setValue($duration); + $this->assertEqual($wrapper->getString(), $duration, 'Duration formatted as string can be used to set the duration value.'); + $wrapper->setValue(NULL); + $this->assertNull($wrapper->getValue(), 'Duration wrapper is null-able.'); + + // Generate some files that will be used to test the URI and the binary + // data types. + $files = $this->drupalGetTestFiles('image'); + + // URI type. + $wrapper = $this->drupalWrapData(array('type' => 'uri'), $files[0]->uri); + $this->assertTrue($wrapper->getValue() === $files[0]->uri, 'URI value was fetched.'); + $wrapper->setValue($files[1]->uri); + $this->assertTrue($wrapper->getValue() === $files[1]->uri, 'URI value was changed.'); + $this->assertTrue(is_string($wrapper->getString()), 'URI value was converted to string'); + $wrapper->setValue(NULL); + $this->assertNull($wrapper->getValue(), 'URI wrapper is null-able.'); + + // Binary type. + $wrapper = $this->drupalWrapData(array('type' => 'binary'), $files[0]->uri); + $this->assertTrue(is_resource($wrapper->getValue()), 'Binary value was fetched.'); + // Try setting by URI. + $wrapper->setValue($files[1]->uri); + $this->assertEqual(is_resource($wrapper->getValue()), fopen($files[1]->uri, 'r'), 'Binary value was changed.'); + $this->assertTrue(is_string($wrapper->getString()), 'Binary value was converted to string'); + // Try setting by resource. + $wrapper->setValue(fopen($files[2]->uri, 'r')); + $this->assertEqual(is_resource($wrapper->getValue()), fopen($files[2]->uri, 'r'), 'Binary value was changed.'); + $this->assertTrue(is_string($wrapper->getString()), 'Binary value was converted to string'); + $wrapper->setValue(NULL); + $this->assertNull($wrapper->getValue(), 'Binary wrapper is null-able.'); + } +} diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php index 14f3630..e1dfabd 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -147,6 +147,63 @@ function hook_cron() { } /** + * Defines available data types for typed data wrappers. + * + * Typed data wrappers allow modules to support any kind of data based upon + * pre-defined primitive types and interfaces for data structures and lists. + * + * Defined data types may map to one of the pre-defined primitive types in + * \Drupal\Core\TypedData\Primitive or may be data structures, containing one or + * more data properties. Wrapper classes of data structures have to implement + * the \Drupal\Core\TypedData\StructureInterface. Further interfaces + * that may be implemented are: + * - \Drupal\Core\TypedData\AccessibleInterface + * - \Drupal\Core\TypedData\StructureTranslatableInterface + * + * Furthermore, lists of data items are represented with wrappers implementing + * the \Drupal\Core\TypedData\ListInterface, for which the class may be + * specified using the 'list class' key. + * + * @return array + * An associative array where the key is the data type name and the value is + * again an associative array. Supported keys are: + * - label: The human readable label of the data type. + * - class: The associated typed data wrapper class. Must implement the + * \Drupal\Core\TypedData\WrapperInterface. + * - list class: (optional) A typed data wrapper class used to wrap multiple + * data items of the type. Must implement the + * \Drupal\Core\TypedData\ListInterface. + * - primitive type: (optional) Maps the data type to one of the pre-defined + * primitive types in \Drupal\Core\TypedData\Primitive. If set, it must be + * a constant defined by \Drupal\Core\TypedData\Primitive such as + * \Drupal\Core\TypedData\Primitive::String. + * + * @see drupal_wrap_data() + * @see drupal_get_data_type_info() + */ +function hook_data_type_info() { + return array( + 'email' => array( + 'label' => t('Email'), + 'class' => '\Drupal\email\Type\Email', + 'primitive type' => \Drupal\Core\TypedData\Primitive::String, + ), + ); +} + +/** + * Alter available data types for typed data wrappers. + * + * @param array $data_types + * An array of data type information. + * + * @see hook_data_type_info() + */ +function hook_data_type_info_alter(&$data_types) { + $data_types['email']['class'] = '\Drupal\mymodule\Type\Email'; +} + +/** * Declare queues holding items that need to be run periodically. * * While there can be only one hook_cron() process running at the same time, diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 922b8a7..8c81d4c 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -6,6 +6,7 @@ */ use Drupal\Core\Utility\ModuleInfo; +use Drupal\Core\TypedData\Primitive; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; @@ -2004,6 +2005,59 @@ function system_stream_wrappers() { } /** + * Implements hook_data_type_info(). + */ +function system_data_type_info() { + return array( + 'boolean' => array( + 'label' => t('Boolean'), + 'class' => '\Drupal\Core\TypedData\Type\Boolean', + 'primitive type' => Primitive::BOOLEAN, + ), + 'string' => array( + 'label' => t('String'), + 'class' => '\Drupal\Core\TypedData\Type\String', + 'primitive type' => Primitive::STRING, + ), + 'integer' => array( + 'label' => t('Integer'), + 'class' => '\Drupal\Core\TypedData\Type\Integer', + 'primitive type' => Primitive::INTEGER, + ), + 'decimal' => array( + 'label' => t('Decimal'), + 'class' => '\Drupal\Core\TypedData\Type\Decimal', + 'primitive type' => Primitive::DECIMAL, + ), + 'date' => array( + 'label' => t('Date'), + 'class' => '\Drupal\Core\TypedData\Type\Date', + 'primitive type' => Primitive::DATE, + ), + 'duration' => array( + 'label' => t('Duration'), + 'class' => '\Drupal\Core\TypedData\Type\Duration', + 'primitive type' => Primitive::DURATION, + ), + 'uri' => array( + 'label' => t('URI'), + 'class' => '\Drupal\Core\TypedData\Type\Uri', + 'primitive type' => Primitive::URI, + ), + 'binary' => array( + 'label' => t('Binary'), + 'class' => '\Drupal\Core\TypedData\Type\Binary', + 'primitive type' => Primitive::BINARY, + ), + 'language' => array( + 'label' => t('Language'), + 'description' => t('A language object.'), + 'class' => '\Drupal\Core\TypedData\Type\Language', + ), + ); +} + +/** * Retrieve a blocked IP address from the database. * * @param $iid integer