diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index e94f8e1..b90212d 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 once field API + * leverages the property API. + */ +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 @@ + 'string_item', + 'list' => TRUE, + 'label' => t('The text'), + 'description' => t('A text property added by mymodule.'), + 'computed' => TRUE, + 'class' => '\Drupal\mymodule\EntityComputedText', + ); + if ($entity_type == 'node') { + // Add a property only to entities of the 'article' bundle. + $info['optional']['mymodule_text_more'] = array( + 'type' => 'string_item', + 'list' => TRUE, + 'label' => t('More text'), + 'computed' => TRUE, + 'class' => '\Drupal\mymodule\EntityComputedMoreText', + ); + $info['bundle map']['article'][0] = 'mymodule_text_more'; + } + return $info; + } +} + +/** + * Alter defined entity properties. + * + * @param array $info + * The property info array as returned by hook_entity_property_info(). + * @param string $entity_type + * The entity type for which entity properties are defined. + * + * @see hook_entity_property_info() + */ +function hook_entity_property_info_alter(&$info, $entity_type) { + if (!empty($info['definitions']['mymodule_text'])) { + // Alter the mymodule_text property to use a custom class. + $info['definitions']['mymodule_text']['class'] = '\Drupal\anothermodule\EntityComputedText'; + } +} diff --git a/core/modules/entity/entity.module b/core/modules/entity/entity.module index 2c54daa..6283e69 100644 --- a/core/modules/entity/entity.module +++ b/core/modules/entity/entity.module @@ -5,7 +5,6 @@ * Entity API for handling entities like nodes or users. */ -use \InvalidArgumentException; use Drupal\entity\EntityFieldQuery; use Drupal\entity\EntityMalformedException; use Drupal\entity\EntityStorageException; @@ -556,3 +555,45 @@ function entity_form_submit_build_entity($entity_type, $entity, $form, &$form_st field_attach_submit($entity_type, $entity, $form, $form_state); } } + +/** + * Implements hook_data_type_info(). + */ +function entity_data_type_info() { + return array( + 'string_item' => array( + 'label' => t('String item'), + 'description' => t('An entity property containing a string value.'), + 'class' => '\Drupal\entity\Property\PropertyStringItem', + 'list class' => '\Drupal\entity\Property\ItemList', + ), + 'integer_item' => array( + 'label' => t('Integer item'), + 'description' => t('An entity property containing an integer value.'), + 'class' => '\Drupal\entity\Property\PropertyIntegerItem', + 'list class' => '\Drupal\entity\Property\ItemList', + ), + 'language_item' => array( + 'label' => t('Language item'), + 'description' => t('An entity property referencing a language.'), + 'class' => '\Drupal\entity\Property\PropertyLanguageItem', + 'list class' => '\Drupal\entity\Property\ItemList', + ), + 'entityreference_item' => array( + 'label' => t('Entity reference item'), + 'description' => t('An entity property containing an entity reference.'), + 'class' => '\Drupal\entity\Property\EntityReferenceItem', + 'list class' => '\Drupal\entity\Property\ItemList', + ), + 'entity' => array( + 'label' => t('Entity'), + 'description' => t('All kind of entities, e.g. nodes, comments or users.'), + 'class' => '\Drupal\entity\Property\EntityWrapper', + ), + 'entity_translation' => array( + 'label' => t('Entity translation'), + 'description' => t('A translation of an entity'), + 'class' => '\Drupal\entity\EntityTranslation', + ), + ); +} diff --git a/core/modules/entity/lib/Drupal/entity/DatabaseStorageController.php b/core/modules/entity/lib/Drupal/entity/DatabaseStorageController.php index 6dfe17a..c0f598b 100644 --- a/core/modules/entity/lib/Drupal/entity/DatabaseStorageController.php +++ b/core/modules/entity/lib/Drupal/entity/DatabaseStorageController.php @@ -46,6 +46,15 @@ class DatabaseStorageController implements EntityStorageControllerInterface { protected $entityInfo; /** + * An array of property information, i.e. containing definitions. + * + * @var array + * + * @see hook_entity_property_info() + */ + protected $propertyInfo; + + /** * Additional arguments to pass to hook_TYPE_load(). * * Set before calling Drupal\entity\DatabaseStorageController::attachLoad(). @@ -201,7 +210,7 @@ class DatabaseStorageController implements EntityStorageControllerInterface { // Remove any invalid ids from the array. $passed_ids = array_intersect_key($passed_ids, $entities); foreach ($entities as $entity) { - $passed_ids[$entity->{$this->idKey}] = $entity; + $passed_ids[$entity->id()] = $entity; } $entities = $passed_ids; } @@ -470,7 +479,7 @@ class DatabaseStorageController implements EntityStorageControllerInterface { if (!$entity->isNew()) { $return = drupal_write_record($this->entityInfo['base table'], $entity, $this->idKey); - $this->resetCache(array($entity->{$this->idKey})); + $this->resetCache(array($entity->id())); $this->postSave($entity, TRUE); $this->invokeHook('update', $entity); } @@ -547,4 +556,58 @@ class DatabaseStorageController implements EntityStorageControllerInterface { // Invoke the respective entity-level hook. module_invoke_all('entity_' . $hook, $entity, $this->entityType); } + + /** + * Implements Drupal\entity\EntityStorageControllerInterface::getPropertyDefinitions(). + */ + public function getPropertyDefinitions(array $constraints) { + // @todo: Add caching for $this->propertyInfo. + if (!isset($this->propertyInfo)) { + $this->propertyInfo = array( + 'definitions' => $this->basePropertyDefinitions(), + // Contains definitions of optional (per-bundle) properties. + 'optional' => array(), + // An array keyed by bundle name containing the names of the per-bundle + // properties. + 'bundle map' => array(), + ); + + // Invoke hooks. + $result = module_invoke_all($this->entityType . '_property_info'); + $this->propertyInfo = array_merge_recursive($this->propertyInfo, $result); + $result = module_invoke_all('entity_property_info', $this->entityType); + $this->propertyInfo = array_merge_recursive($this->propertyInfo, $result); + + $hooks = array('entity_property_info', $this->entityType . '_property_info'); + drupal_alter($hooks, $this->propertyInfo, $this->entityType); + + // Enforce properties to be multiple by default. + foreach ($this->propertyInfo['definitions'] as &$definition) { + $definition['list'] = TRUE; + } + foreach ($this->propertyInfo['optional'] as &$definition) { + $definition['list'] = TRUE; + } + } + + $definitions = $this->propertyInfo['definitions']; + + // Add in per-bundle properties. + // @todo: Should this be statically cached as well? + if (!empty($constraints['bundle']) && isset($this->propertyInfo['bundle map'][$constraints['bundle']])) { + $definitions += array_intersect_key($this->propertyInfo['optional'], array_flip($this->propertyInfo['bundle map'][$constraints['bundle']])); + } + + return $definitions; + } + + /** + * Defines the base properties of the entity type. + * + * @todo: Define abstract once all entity types have been converted. + */ + public function basePropertyDefinitions() { + return array(); + } } + diff --git a/core/modules/entity/lib/Drupal/entity/Entity.php b/core/modules/entity/lib/Drupal/entity/Entity.php index 9701d28..6e244d3 100644 --- a/core/modules/entity/lib/Drupal/entity/Entity.php +++ b/core/modules/entity/lib/Drupal/entity/Entity.php @@ -145,16 +145,94 @@ class Entity implements EntityInterface { } /** - * Implements EntityInterface::language(). + * Implements EntityInterface::get(). + */ + public function get($property_name, $langcode = NULL) { + // @todo: Replace by EntityNG implementation once all entity types have been + // converted to use the entity property API. + return isset($this->{$property_name}) ? $this->{$property_name} : NULL; + } + + /** + * Implements StructureInterface::set(). + */ + public function set($property_name, $value) { + // @todo: Replace by EntityNG implementation once all entity types have been + // converted to use the entity property API. + $this->{$property_name} = $value; + } + + /** + * Implements StructureTranslatableInterface::getProperties(). + */ + public function getProperties($include_computed = FALSE) { + // @todo: Replace by EntityNG implementation once all entity types have been + // converted to use the entity property API. + } + + /** + * Implements StructureTranslatableInterface::setProperties(). + */ + public function setProperties($properties) { + // @todo: Replace by EntityNG implementation once all entity types have been + // converted to use the entity property API. + } + + /** + * Implements StructureTranslatableInterface::getPropertyDefinition(). + */ + public function getPropertyDefinition($name) { + // @todo: Replace by EntityNG implementation once all entity types have been + // converted to use the entity property API. + } + + /** + * Implements StructureTranslatableInterface::getPropertyDefinitions(). + */ + public function getPropertyDefinitions() { + // @todo: Replace by EntityNG implementation once all entity types have been + // converted to use the entity property API. + } + + /** + * Implements StructureTranslatableInterface::toArray(). + */ + public function toArray() { + // @todo: Replace by EntityNG implementation once all entity types have been + // converted to use the entity property API. + } + + /** + * Implements StructureTranslatableInterface::getIterator(). + */ + public function getIterator() { + // @todo: Replace by EntityNG implementation once all entity types have been + // converted to use the entity property API. + } + + /** + * Implements StructureTranslatableInterface::language(). */ public function language() { - // @todo: Check for language.module instead, once Field API language - // handling depends upon it too. + // @todo: Replace by EntityNG implementation once all entity types have been + // converted to use the entity property API. return module_exists('locale') ? language_load($this->langcode) : FALSE; } /** - * Implements EntityInterface::translations(). + * Implements StructureTranslatableInterface::getTranslation(). + */ + public function getTranslation($langcode) { + // @todo: Replace by EntityNG implementation once all entity types have been + // converted to use the entity property API. + } + + /** + * Returns the languages the entity is translated to. + * + * @todo: Remove once all entity types implement the entity property API. This + * is deprecated by + * StructureTranslatableInterface::getTranslationLanguages(). */ public function translations() { $languages = array(); @@ -178,57 +256,11 @@ class Entity implements EntityInterface { } /** - * Implements EntityInterface::get(). - */ - public function get($property_name, $langcode = NULL) { - // Handle fields. - $entity_info = $this->entityInfo(); - if ($entity_info['fieldable'] && field_info_instance($this->entityType, $property_name, $this->bundle())) { - $field = field_info_field($property_name); - $langcode = $this->getFieldLangcode($field, $langcode); - return isset($this->{$property_name}[$langcode]) ? $this->{$property_name}[$langcode] : NULL; - } - else { - // Handle properties being not fields. - // @todo: Add support for translatable properties being not fields. - return isset($this->{$property_name}) ? $this->{$property_name} : NULL; - } - } - - /** - * Implements EntityInterface::set(). + * Implements StructureTranslatableInterface::getTranslationLanguages(). */ - public function set($property_name, $value, $langcode = NULL) { - // Handle fields. - $entity_info = $this->entityInfo(); - if ($entity_info['fieldable'] && field_info_instance($this->entityType, $property_name, $this->bundle())) { - $field = field_info_field($property_name); - $langcode = $this->getFieldLangcode($field, $langcode); - $this->{$property_name}[$langcode] = $value; - } - else { - // Handle properties being not fields. - // @todo: Add support for translatable properties being not fields. - $this->{$property_name} = $value; - } - } - - /** - * Determines the language code to use for accessing a field value in a certain language. - */ - protected function getFieldLangcode($field, $langcode = NULL) { - // Only apply the given langcode if the entity is language-specific. - // Otherwise translatable fields are handled as non-translatable fields. - if (field_is_translatable($this->entityType, $field) && ($default_language = $this->language()) && !language_is_locked($this->langcode)) { - // For translatable fields the values in default language are stored using - // the language code of the default language. - return isset($langcode) ? $langcode : $default_language->langcode; - } - else { - // If there is a langcode defined for this field, just return it. Otherwise - // return LANGUAGE_NOT_SPECIFIED. - return (isset($this->langcode) ? $this->langcode : LANGUAGE_NOT_SPECIFIED); - } + public function getTranslationLanguages($include_default = TRUE) { + // @todo: Replace by EntityNG implementation once all entity types have been + // converted to use the entity property API. } /** diff --git a/core/modules/entity/lib/Drupal/entity/EntityInterface.php b/core/modules/entity/lib/Drupal/entity/EntityInterface.php index 68fee11..a185637 100644 --- a/core/modules/entity/lib/Drupal/entity/EntityInterface.php +++ b/core/modules/entity/lib/Drupal/entity/EntityInterface.php @@ -6,11 +6,12 @@ */ namespace Drupal\entity; +use Drupal\Core\TypedData\StructureTranslatableInterface; /** * Defines a common interface for all entity objects. */ -interface EntityInterface { +interface EntityInterface extends StructureTranslatableInterface { /** * Constructs a new entity object. @@ -111,60 +112,6 @@ interface EntityInterface { public function uri(); /** - * Returns the default language of a language-specific entity. - * - * @return - * The language object of the entity's default language, or FALSE if the - * entity is not language-specific. - * - * @see Drupal\entity\EntityInterface::translations() - */ - public function language(); - - /** - * Returns the languages the entity is translated to. - * - * @return - * An array of language objects, keyed by language codes. - * - * @see Drupal\entity\EntityInterface::language() - */ - public function translations(); - - /** - * Returns the value of an entity property. - * - * @param $property_name - * The name of the property to return; e.g., 'title'. - * @param $langcode - * (optional) If the property is translatable, the language code of the - * language that should be used for getting the property. If set to NULL, - * the entity's default language is being used. - * - * @return - * The property value, or NULL if it is not defined. - * - * @see Drupal\entity\EntityInterface::language() - */ - public function get($property_name, $langcode = NULL); - - /** - * Sets the value of an entity property. - * - * @param $property_name - * The name of the property to set; e.g., 'title'. - * @param $value - * The value to set, or NULL to unset the property. - * @param $langcode - * (optional) If the property is translatable, the language code of the - * language that should be used for setting the property. If set to NULL, - * the entity's default language is being used. - * - * @see Drupal\entity\EntityInterface::language() - */ - public function set($property_name, $value, $langcode = NULL); - - /** * Saves an entity permanently. * * @return diff --git a/core/modules/entity/lib/Drupal/entity/EntityNG.php b/core/modules/entity/lib/Drupal/entity/EntityNG.php new file mode 100644 index 0000000..547c93a --- /dev/null +++ b/core/modules/entity/lib/Drupal/entity/EntityNG.php @@ -0,0 +1,372 @@ +values, thus + * must be plain property values keyed by language code. This must be enabled + * when calling field API attachers. + * + * @var bool + */ + protected $compatibilityMode = FALSE; + + + /** + * Overrides Entity::id(). + */ + public function id() { + return $this->get('id')->value; + } + + /** + * Overrides Entity::uuid(). + */ + public function uuid() { + return $this->get('uuid')->value; + } + + /** + * Implements StructureInterface::get(). + */ + public function get($property_name) { + // Values in default language are always stored using the LANGUAGE_DEFAULT + // constant. + if (!isset($this->properties[$property_name][LANGUAGE_DEFAULT])) { + return $this->getTranslatedProperty($property_name, LANGUAGE_DEFAULT); + } + return $this->properties[$property_name][LANGUAGE_DEFAULT]; + } + + /** + * Gets a translated property. + * + * @return \Drupal\entity\Property\ItemListInterface + */ + protected function getTranslatedProperty($property_name, $langcode) { + // Populate $this->properties to fasten further lookups and to keep track of + // property objects, possibly holding changes to properties. + if (!isset($this->properties[$property_name][$langcode])) { + $definition = $this->getPropertyDefinition($property_name); + if (!$definition) { + throw new InvalidArgumentException('Property ' . check_plain($property_name) . ' is unknown.'); + } + // Non-translatable properties always use default language. + if ($langcode != LANGUAGE_DEFAULT && empty($definition['translatable'])) { + $this->properties[$property_name][$langcode] = $this->getTranslatedProperty($property_name, LANGUAGE_DEFAULT); + } + else { + $value = isset($this->values[$property_name][$langcode]) ? $this->values[$property_name][$langcode] : NULL; + $context = array('parent' => $this, 'name' => $property_name); + $this->properties[$property_name][$langcode] = drupal_wrap_data($definition, $value, $context); + } + } + return $this->properties[$property_name][$langcode]; + } + + /** + * Implements StructureInterface::set(). + */ + public function set($property_name, $value) { + $this->get($property_name)->setValue($value); + } + + /** + * Implements StructureInterface::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 StructureInterface::setProperties(). + */ + public function setProperties($properties) { + foreach ($properties as $name => $property) { + // Copy the value to our property object. + $value = $property instanceof WrapperInterface ? $property->getValue() : $property; + $this->get($name)->setValue($value); + } + } + + /** + * Implements IteratorAggregate::getIterator(). + */ + public function getIterator() { + return new ArrayIterator($this->getProperties()); + } + + /** + * Implements StructureInterface::getPropertyDefinition(). + */ + public function getPropertyDefinition($name) { + $definitions = $this->getPropertyDefinitions(); + return isset($definitions[$name]) ? $definitions[$name] : FALSE; + } + + /** + * Implements StructureInterface::getPropertyDefinitions(). + */ + public function getPropertyDefinitions() { + return entity_get_controller($this->entityType)->getPropertyDefinitions(array( + 'entity type' => $this->entityType, + 'bundle' => $this->bundle(), + )); + } + + /** + * Implements StructureInterface::toArray(). + */ + public function toArray() { + $values = array(); + foreach ($this->getProperties() as $name => $property) { + $values[$name] = $property->getValue(); + } + return $values; + } + + /** + * Implements StructureTranslatableInterface::language(). + */ + public function language() { + return $this->langcode->language; + } + + /** + * Implements StructureTranslatableInterface::getTranslation(). + */ + public function getTranslation($langcode) { + // If the default language is LANGUAGE_NOT_SPECIFIED, the entity is not + // translatable, so we use LANGUAGE_DEFAULT. + if ($langcode == LANGUAGE_DEFAULT || $langcode == $this->language()->langcode || LANGUAGE_NOT_SPECIFIED == $this->langcode->value) { + // No translation needed, return the entity. + return $this; + } + // Check whether the language code is valid, thus is of an available + // language. + $languages = language_list(LANGUAGE_ALL); + if (!isset($languages[$langcode])) { + throw new InvalidArgumentException("Unable to get translation for the invalid language '$langcode'."); + } + $properties = array(); + foreach ($this->getPropertyDefinitions() as $name => $definition) { + $properties[$name] = $this->getTranslatedProperty($name, $langcode); + } + $translation_definition = array( + 'type' => 'entity_translation', + 'constraints' => array( + 'entity type' => $this->entityType(), + 'bundle' => $this->bundle(), + ), + ); + return drupal_wrap_data($translation_definition, $properties, array('parent' => $this, 'langcode' => $langcode)); + } + + /** + * Implements StructureTranslatableInterface::getTranslationLanguages(). + */ + public function getTranslationLanguages($include_default = TRUE) { + $translations = array(); + // Build an array with the translation langcodes set as keys. + foreach ($this->getProperties() as $name => $property) { + if (isset($this->values[$name])) { + $translations += $this->values[$name]; + } + $translations += $this->properties[$name]; + } + unset($translations[LANGUAGE_DEFAULT]); + + if ($include_default) { + $translations[$this->language()->langcode] = TRUE; + } + + // Now get languages based upon translation langcodes. + $languages = array_intersect_key(language_list(LANGUAGE_ALL), $translations); + return $languages; + } + + /** + * Implements AccessibleInterface::access(). + */ + public function access(\Drupal\user\User $account = NULL) { + // TODO: Implement access() method. + } + + /** + * Enables or disable the compatibility mode. + * + * @param bool $enabled + * Whether to enable the mode. + * + * @see EntityNG::compatibilityMode + */ + public function setCompatibilityMode($enabled) { + $this->compatibilityMode = (bool) $enabled; + } + + /** + * Returns whether the compatibility mode is active. + */ + public function getCompatibilityMode() { + return $this->compatibilityMode; + } + + /** + * Updates the original values with the interim changes. + * + * Note: This should be called by the storage controller during a save + * operation. + */ + public function updateOriginalValues() { + foreach ($this->properties as $name => $properties) { + foreach ($properties as $langcode => $property) { + $this->values[$name][$langcode] = $property->getValue(); + } + } + } + + /** + * Magic getter: Gets the property in default language. + * + * For compatibility mode to work this must return a reference. + */ + public function &__get($name) { + if ($this->compatibilityMode) { + if (!isset($this->values[$name])) { + $this->values[$name] = NULL; + } + return $this->values[$name]; + } + if (isset($this->properties[$name][LANGUAGE_DEFAULT])) { + return $this->properties[$name][LANGUAGE_DEFAULT]; + } + if ($this->getPropertyDefinition($name)) { + $return = $this->get($name); + return $return; + } + if (!isset($this->$name)) { + $this->$name = NULL; + } + return $this->$name; + } + + /** + * Magic getter: Sets the property in default language. + */ + public function __set($name, $value) { + if ($this->compatibilityMode) { + $this->values[$name] = $value; + } + elseif (isset($this->properties[$name][LANGUAGE_DEFAULT])) { + $this->properties[$name][LANGUAGE_DEFAULT]->setValue($value); + } + elseif ($this->getPropertyDefinition($name)) { + $this->get($name)->setValue($value); + } + else { + $this->$name = $value; + } + } + + /** + * Magic method. + */ + public function __isset($name) { + if ($this->compatibilityMode) { + return isset($this->values[$name]); + } + else { + return isset($this->properties[$name]); + } + } + + /** + * Magic method. + */ + public function __unset($name) { + if ($this->compatibilityMode) { + unset($this->values[$name]); + } + else { + unset($this->properties[$name]); + } + } + + /** + * Overrides Entity::createDuplicate(). + */ + public function createDuplicate() { + $duplicate = clone $this; + $entity_info = $this->entityInfo(); + $this->{$entity_info['entity keys']['id']}->value = NULL; + + // Check if the entity type supports UUIDs and generate a new one if so. + if (!empty($entity_info['entity keys']['uuid'])) { + $uuid = new Uuid(); + $duplicate->{$entity_info['entity keys']['uuid']}->value = $uuid->generate(); + } + return $duplicate; + } + + /** + * Implements a deep clone. + */ + public function __clone() { + foreach ($this->properties as $name => $properties) { + foreach ($properties as $langcode => $property) { + $this->properties[$name][$langcode] = clone $property; + } + } + } +} + diff --git a/core/modules/entity/lib/Drupal/entity/EntityStorageControllerInterface.php b/core/modules/entity/lib/Drupal/entity/EntityStorageControllerInterface.php index 22b6b4c..9ba4ac0 100644 --- a/core/modules/entity/lib/Drupal/entity/EntityStorageControllerInterface.php +++ b/core/modules/entity/lib/Drupal/entity/EntityStorageControllerInterface.php @@ -109,4 +109,37 @@ interface EntityStorageControllerInterface { */ public function save(EntityInterface $entity); + /** + * Gets an array of entity property definitions. + * + * If a 'bundle' key is present in the given entity definition, properties + * specific to this bundle are included. + * Entity properties are always multi-valued, so 'list' is TRUE for each + * returned property definition. + * + * @param array $constraints + * An array of entity constraints as used for entities in typed data + * definitions, i.e. an array having an 'entity type' and optionally a + * 'bundle' key. For example: + * @code + * array( + * 'entity type' => 'node', + * 'bundle' => 'article', + * ) + * @endcode + * + * @return array + * An array of property definitions of entity properties, keyed by property + * name. In addition to the typed data definition keys as described at + * drupal_wrap_data() the follow keys are supported: + * - queryable: Whether the property is queryable via EntityFieldQuery. + * Defaults to TRUE if 'computed' is FALSE or not set, to FALSE otherwise. + * - translatable: Whether the property is translatable. Defaults to FALSE. + * - field: A boolean indicating whether the property is a field. Defaults + * to FALSE. + * + * @see drupal_wrap_data() + */ + public function getPropertyDefinitions(array $constraints); + } diff --git a/core/modules/entity/lib/Drupal/entity/EntityTranslation.php b/core/modules/entity/lib/Drupal/entity/EntityTranslation.php new file mode 100644 index 0000000..3fbf672 --- /dev/null +++ b/core/modules/entity/lib/Drupal/entity/EntityTranslation.php @@ -0,0 +1,196 @@ +definition = $definition; + $this->properties = (array) $value; + $this->langcode = $context['langcode']; + } + + /** + * Implements WrapperInterface::getValue(). + */ + public function getValue() { + $values = array(); + foreach ($this->getProperties() as $name => $property) { + $values[$name] = $property->getValue(); + } + return $values; + } + + /** + * Implements WrapperInterface::setValue(). + */ + public function setValue($values) { + foreach ($this->getProperties() as $name => $property) { + $property->setValue(isset($values[$name]) ? $values[$name] : NULL); + unset($values[$name]); + } + if ($values) { + throw new InvalidArgumentException('Property ' . check_plain(key($values)) . ' is unknown or not translatable.'); + } + } + + /** + * Implements WrapperInterface::getString(). + */ + public function getString() { + $strings = array(); + foreach ($this->getProperties() as $property) { + $strings[] = $property->getString(); + } + return implode(', ', array_filter($strings)); + } + + /** + * Implements WrapperInterface::get(). + */ + public function get($property_name) { + $definitions = $this->getPropertyDefinitions(); + if (!isset($definitions[$property_name])) { + throw new InvalidArgumentException('Property ' . check_plain($property_name) . ' is unknown or not translatable.'); + } + return $this->properties[$property_name]; + } + + /** + * Implements StructureInterface::set(). + */ + public function set($property_name, $value) { + $this->get($property_name)->setValue($value); + } + + /** + * Implements StructureInterface::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 StructureInterface::setProperties(). + */ + public function setProperties($properties) { + foreach ($this->getProperties() as $name => $property) { + if (isset($properties[$name])) { + // Copy the value to our property object. + $value = $properties[$name] instanceof WrapperInterface ? $properties[$name]->getValue() : $properties[$name]; + $property->setValue($value); + unset($properties[$name]); + } + } + if ($properties) { + throw new InvalidArgumentException('Property ' . check_plain(key($values)) . ' is unknown or not translatable.'); + } + } + + /** + * Magic getter: Gets the translated property. + */ + public function __get($name) { + return $this->get($name); + } + + /** + * Magic getter: Sets the translated property. + */ + public function __set($name, $value) { + $this->get($name)->setValue($value); + } + + /** + * Implements IteratorAggregate::getIterator(). + */ + public function getIterator() { + return new ArrayIterator($this->getProperties()); + } + + /** + * Implements StructureInterface::getPropertyDefinition(). + */ + public function getPropertyDefinition($name) { + $definitions = $this->getPropertyDefinitions(); + return isset($definitions[$name]) ? $definitions[$name] : FALSE; + } + + /** + * Implements StructureInterface::getPropertyDefinitions(). + */ + public function getPropertyDefinitions() { + $definitions = array(); + $entity_properties = entity_get_controller($this->definition['constraints']['entity type'])->getPropertyDefinitions(array( + 'entity type' => $this->definition['constraints']['entity type'], + 'bundle' => $this->definition['constraints']['bundle'], + )); + foreach ($entity_properties as $name => $definition) { + if (!empty($definition['translatable'])) { + $definitions[$name] = $definition; + } + } + return $definitions; + } + + /** + * Implements StructureInterface::toArray(). + */ + public function toArray() { + return $this->getValue(); + } + + /** + * Implements AccessibleInterface::access(). + */ + public function access(\Drupal\user\User $account = NULL) { + // @todo implement + } + + /** + * Implements WrapperInterface::validate(). + */ + public function validate($value = NULL) { + // @todo implement + } +} diff --git a/core/modules/entity/lib/Drupal/entity/Property/EntityReferenceItem.php b/core/modules/entity/lib/Drupal/entity/Property/EntityReferenceItem.php new file mode 100644 index 0000000..f7449db --- /dev/null +++ b/core/modules/entity/lib/Drupal/entity/Property/EntityReferenceItem.php @@ -0,0 +1,81 @@ +definition['settings']['entity type']; + + if (!isset(self::$propertyDefinitions[$entity_type])) { + self::$propertyDefinitions[$entity_type]['value'] = array( + // @todo: Lookup the entity type's ID data type and use it here. + 'type' => 'integer', + 'label' => t('Entity ID'), + ); + self::$propertyDefinitions[$entity_type]['entity'] = array( + 'type' => 'entity', + 'constraints' => array( + 'entity type' => $entity_type, + ), + 'label' => t('Entity'), + 'description' => t('The referenced entity'), + // The entity object is computed out of the entity id. + 'computed' => TRUE, + 'read-only' => FALSE, + 'settings' => array('id source' => 'value'), + ); + } + return self::$propertyDefinitions[$entity_type]; + } + + /** + * Overrides ItemBase::setValue(). + */ + public function setValue($values) { + // Treat the values as property value of the entity property, if no array + // is given. + if (!is_array($values)) { + $values = array('entity' => $values); + } + + // Entity is computed out of the ID, so we only need to update the ID. Only + // set the entity property if no ID is given. + if (!empty($values['value'])) { + $this->properties['value']->setValue($values['value']); + } + else { + $this->properties['entity']->setValue(isset($values['entity']) ? $values['entity'] : NULL); + } + unset($values['entity'], $values['value']); + if ($values) { + throw new InvalidArgumentException('Property ' . key($values) . ' is unknown.'); + } + } +} diff --git a/core/modules/entity/lib/Drupal/entity/Property/EntityWrapper.php b/core/modules/entity/lib/Drupal/entity/Property/EntityWrapper.php new file mode 100644 index 0000000..e788d23 --- /dev/null +++ b/core/modules/entity/lib/Drupal/entity/Property/EntityWrapper.php @@ -0,0 +1,180 @@ +definition = $definition + array('constraints' => array()); + $this->entityType = isset($this->definition['constraints']['entity type']) ? $this->definition['constraints']['entity type'] : NULL; + + // If an ID source is specified, act as computed property. + if (isset($context['parent']) && !empty($this->definition['settings']['id source'])) { + $this->id = $context['parent']->get($this->definition['settings']['id source']); + } + else { + // No context given, so just initialize an ID property for storing the + // entity ID of the wrapped entity. + $this->id = drupal_wrap_data(array('type' => 'string')); + } + + if (isset($value)) { + $this->setValue($value); + } + } + + /** + * Implements WrapperInterface::getValue(). + */ + public function getValue() { + $id = $this->id->getValue(); + return $id ? entity_load($this->entityType, $id) : NULL; + } + + /** + * Implements WrapperInterface::setValue(). + * + * Both the entity ID and the entity object may be passed as value. + */ + public function setValue($value) { + if (!isset($value)) { + $this->id->setValue(NULL); + } + elseif (is_scalar($value) && !empty($this->definition['constraints']['entity type'])) { + $this->id->setValue($value); + } + elseif ($value instanceof EntityInterface) { + $this->id->setValue($value->id()); + $this->entityType = $value->entityType(); + } + else { + throw new InvalidArgumentException('Value is no valid entity.'); + } + } + + /** + * Implements WrapperInterface::getString(). + */ + public function getString() { + $entity = $this->getValue(); + return $entity ? $entity->label() : ''; + } + + /** + * Implements WrapperInterface::validate(). + */ + public function validate($value = NULL) { + // TODO: Implement validate() method. + } + + /** + * Implements IteratorAggregate::getIterator(). + */ + public function getIterator() { + $entity = $this->getValue(); + return $entity ? $entity->getIterator() : new ArrayIterator(array()); + } + + /** + * Implements StructureInterface::get(). + */ + public function get($property_name) { + $entity = $this->getValue(); + // @todo: Allow navigating through the tree without data as well. + return $entity ? $entity->get($property_name) : NULL; + } + + /** + * Implements StructureInterface::set(). + */ + public function set($property_name, $value) { + $this->get($property_name)->setValue($value); + } + + /** + * Implements StructureInterface::getProperties(). + */ + public function getProperties($include_computed = FALSE) { + $entity = $this->getValue(); + return $entity ? $entity->getProperties($include_computed) : array(); + } + + /** + * Implements StructureInterface::setProperties(). + */ + public function setProperties($properties) { + if ($entity = $this->getValue()) { + $entity->setProperties($properties); + } + } + + /** + * Implements StructureInterface::getPropertyDefinition(). + */ + public function getPropertyDefinition($name) { + $definitions = $this->getPropertyDefinitions(); + return isset($definitions[$name]) ? $definitions[$name] : FALSE; + } + + /** + * Implements StructureInterface::getPropertyDefinitions(). + */ + public function getPropertyDefinitions() { + // @todo: Support getting definitions if multiple bundles are specified. + return entity_get_controller($this->entityType)->getPropertyDefinitions($this->definition['constraints']); + } + + /** + * Implements StructureInterface::toArray(). + */ + public function toArray() { + $entity = $this->getValue(); + return $entity ? $entity->toArray() : array(); + } +} diff --git a/core/modules/entity/lib/Drupal/entity/Property/ItemBase.php b/core/modules/entity/lib/Drupal/entity/Property/ItemBase.php new file mode 100644 index 0000000..32b3cdf --- /dev/null +++ b/core/modules/entity/lib/Drupal/entity/Property/ItemBase.php @@ -0,0 +1,211 @@ + + */ + protected $properties = array(); + + /** + * Implements WrapperInterface::__construct(). + */ + public function __construct(array $definition, $value = NULL, array $context = array()) { + $this->definition = $definition; + + // Initialize all property objects, but postpone the creating of computed + // properties to a second step. That way computed properties can safely get + // references on non-computed properties during construction. + $step2 = array(); + foreach ($this->getPropertyDefinitions() as $name => $definition) { + if (empty($definition['computed'])) { + $context = array('name' => $name, 'parent' => $this); + $this->properties[$name] = drupal_wrap_data($definition, NULL, $context); + } + else { + $step2[$name] = $definition; + } + } + + foreach ($step2 as $name => $definition) { + $context = array('name' => $name, 'parent' => $this); + $this->properties[$name] = drupal_wrap_data($definition, NULL, $context); + } + + if (isset($value)) { + $this->setValue($value); + } + } + + /** + * Implements WrapperInterface::getValue(). + */ + public function getValue() { + $values = array(); + foreach ($this->getProperties() as $name => $property) { + $values[$name] = $property->getValue(); + } + return $values; + } + + /** + * Implements WrapperInterface::setValue(). + * + * @param array $values + * An array of property values. + */ + public function setValue($values) { + // Treat the values as property value of the first property, if no array is + // given and we only have one property. + if (!is_array($values) && count($this->properties) == 1) { + $keys = array_keys($this->properties); + $values = array($keys[0] => $values); + } + // Support passing in property objects as value. + elseif ($values instanceof WrapperInterface) { + $values = $values->getValue(); + } + + foreach ($this->properties as $name => $property) { + $property->setValue(isset($values[$name]) ? $values[$name] : NULL); + } + // @todo: Throw an exception for invalid values once conversion is + // totally completed. + } + + /** + * Implements WrapperInterface::getString(). + */ + public function getString() { + $strings = array(); + foreach ($this->getProperties() as $property) { + $strings[] = $property->getString(); + } + return implode(', ', array_filter($strings)); + } + + /** + * Implements WrapperInterface::validate(). + */ + public function validate() { + // @todo implement + } + + /** + * Implements StructureInterface::get(). + */ + public function get($property_name) { + if (!isset($this->properties[$property_name])) { + throw new InvalidArgumentException('Property ' . check_plain($property_name) . ' is unknown.'); + } + return $this->properties[$property_name]; + } + + /** + * Implements StructureInterface::set(). + */ + public function set($property_name, $value) { + $this->get($property_name)->setValue($value); + } + + /** + * Implements ItemInterface::__get(). + */ + public function __get($name) { + return $this->get($name)->getValue(); + } + + /** + * Implements ItemInterface::__set(). + */ + public function __set($name, $value) { + $this->get($name)->setValue($value); + } + + /** + * Implements StructureInterface::getProperties(). + */ + public function getProperties($include_computed = FALSE) { + $properties = array(); + foreach ($this->getPropertyDefinitions() as $name => $definition) { + if ($include_computed || empty($definition['computed'])) { + $properties[$name] = $this->properties[$name]; + } + } + return $properties; + } + + /** + * Implements StructureInterface::setProperties(). + */ + public function setProperties($properties) { + foreach ($properties as $name => $property) { + if (isset($this->properties[$name])) { + // Copy the value to our property object. + $value = $property instanceof WrapperInterface ? $property->getValue() : $property; + $this->properties[$name]->setValue($value); + } + else { + throw new InvalidArgumentException('Property ' . check_plain($name) . ' is unknown.'); + } + } + } + + /** + * Implements IteratorAggregate::getIterator(). + */ + public function getIterator() { + return new ArrayIterator($this->getProperties()); + } + + /** + * Implements StructureInterface::getPropertyDefinition(). + */ + public function getPropertyDefinition($name) { + $definitions = $this->getPropertyDefinitions(); + return isset($definitions[$name]) ? $definitions[$name] : FALSE; + } + + /** + * Implements StructureInterface::toArray(). + */ + public function toArray() { + return $this->getValue(); + } + + /** + * Implements a deep clone. + */ + public function __clone() { + foreach ($this->properties as $name => $property) { + $this->properties[$name] = clone $property; + } + } +} \ No newline at end of file diff --git a/core/modules/entity/lib/Drupal/entity/Property/ItemInterface.php b/core/modules/entity/lib/Drupal/entity/Property/ItemInterface.php new file mode 100644 index 0000000..221c66e --- /dev/null +++ b/core/modules/entity/lib/Drupal/entity/Property/ItemInterface.php @@ -0,0 +1,31 @@ +list as $delta => $item) { + // @todo: Filter out empty items and add an isEmpty() method to them. + $values[$delta] = $item->getValue(); + } + return $values; + } + + /** + * Implements WrapperInterface::setValue(). + * + * @param array $values + * An array of values of the property items. + */ + public function setValue($values) { + if (isset($values)) { + + // Support passing in property objects as value. + if ($values instanceof DataWrapperInterface) { + $values = $values->getValue(); + } + if (!is_array($values)) { + throw new InvalidArgumentException("An entity property requires a numerically indexed array of items as value."); + } + + // Clear the values of properties for which no value has been passed. + foreach (array_diff_key($this->list, $values) as $delta => $item) { + unset($this->list[$delta]); + } + + // 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($value); + } + else { + $this->list[$delta]->setValue($value); + } + } + } + else { + $this->list = array(); + } + } + + /** + * Returns a string representation of the property. + * + * @return string + */ + public function getString() { + $strings = array(); + foreach ($this->list() as $item) { + $strings[] = $item->getString(); + } + return implode(', ', array_filter($strings)); + } + + /** + * Implements WrapperInterface::validate(). + */ + public function validate() { + // @todo implement + } + + /** + * Implements ArrayAccess::offsetExists(). + */ + public function offsetExists($offset) { + return array_key_exists($offset, $this->list); + } + + /** + * Implements ArrayAccess::offsetUnset(). + */ + public function offsetUnset($offset) { + unset($this->list[$offset]); + } + + /** + * Implements ArrayAccess::offsetGet(). + */ + public function offsetGet($offset) { + if (!isset($offset)) { + // @todo: Needs tests. + // 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)) { + 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 instead or in addition? + // @todo: Needs tests. + elseif (!isset($this->list[$offset])) { + $this->list[$offset] = $this->createItem(); + } + + return $this->list[$offset]; + } + + /** + * Helper for creating a list item object. + * + * @return \Drupal\Core\TypedData\WrapperInterface + */ + protected function createItem($value = NULL) { + $context = array('parent' => $this); + return drupal_wrap_data(array('list' => FALSE) + $this->definition, $value, $context); + } + + /** + * Implements ArrayAccess::offsetSet(). + */ + public function offsetSet($offset, $value) { + // @todo: Throw exception if the value does not implement the interface. + if (is_numeric($offset)) { + $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() { + return new ArrayIterator($this->list); + } + + /** + * Implements Countable::count(). + */ + public function count() { + return count($this->list); + } + + /** + * Delegate. + */ + public function getProperties() { + return $this->offsetGet(0)->getProperties(); + } + + /** + * Delegate. + */ + public function getPropertyDefinition($name) { + return $this->offsetGet(0)->getPropertyDefinition($name); + } + + /** + * Delegate. + */ + public function getPropertyDefinitions() { + return $this->offsetGet(0)->getPropertyDefinitions(); + } + + /** + * Delegate. + */ + public function __get($property_name) { + return $this->offsetGet(0)->__get($property_name); + } + + /** + * Delegate. + */ + public function get($property_name) { + return $this->offsetGet(0)->get($property_name); + } + + /** + * Delegate. + */ + public function __set($property_name, $value) { + $this->offsetGet(0)->__set($property_name, $value); + } + + /** + * Gets the the raw array representation of the entity property. + * + * @return array + * The raw array representation of the entity property, i.e. an array + * containing the raw values of all contained items. + */ + public function toArray() { + return $this->getValue(); + } + + /** + * Implements a deep clone. + */ + public function __clone() { + foreach ($this->list as $delta => $property) { + $this->list[$delta] = clone $property; + } + } + + /** + * Implements AccessibleInterface::access(). + */ + public function access(User $account = NULL) { + // TODO: Implement access() method. Use item access. + } +} diff --git a/core/modules/entity/lib/Drupal/entity/Property/ItemListInterface.php b/core/modules/entity/lib/Drupal/entity/Property/ItemListInterface.php new file mode 100644 index 0000000..26ff941 --- /dev/null +++ b/core/modules/entity/lib/Drupal/entity/Property/ItemListInterface.php @@ -0,0 +1,44 @@ + 'integer', + 'label' => t('Integer value'), + ); + } + return self::$propertyDefinitions; + } +} + diff --git a/core/modules/entity/lib/Drupal/entity/Property/PropertyLanguageItem.php b/core/modules/entity/lib/Drupal/entity/Property/PropertyLanguageItem.php new file mode 100644 index 0000000..3ccf50e --- /dev/null +++ b/core/modules/entity/lib/Drupal/entity/Property/PropertyLanguageItem.php @@ -0,0 +1,70 @@ + 'string', + 'label' => t('Language code'), + ); + self::$propertyDefinitions['language'] = array( + 'type' => 'language', + 'label' => t('Language object'), + // The language object is retrieved via the language code. + 'computed' => TRUE, + 'read-only' => FALSE, + 'settings' => array('langcode source' => 'value'), + ); + } + return self::$propertyDefinitions; + } + + /** + * Overrides ItemBase::setValue(). + */ + public function setValue($values) { + // Treat the values as property value of the object property, if no array + // is given. + if (!is_array($values)) { + $values = array('language' => $values); + } + + // Language is computed out of the langcode, so we only need to update the + // langcode. Only set the language property if no langcode is given. + if (!empty($values['value'])) { + $this->properties['value']->setValue($values['value']); + } + else { + $this->properties['language']->setValue(isset($values['language']) ? $values['language'] : NULL); + } + unset($values['language'], $values['value']); + if ($values) { + throw new InvalidArgumentException('Property ' . key($values) . ' is unknown.'); + } + } +} diff --git a/core/modules/entity/lib/Drupal/entity/Property/PropertyStringItem.php b/core/modules/entity/lib/Drupal/entity/Property/PropertyStringItem.php new file mode 100644 index 0000000..71fb0ed --- /dev/null +++ b/core/modules/entity/lib/Drupal/entity/Property/PropertyStringItem.php @@ -0,0 +1,39 @@ + 'string', + 'label' => t('Text value'), + ); + } + return self::$propertyDefinitions; + } +} + diff --git a/core/modules/entity/lib/Drupal/entity/Tests/EntityApiTest.php b/core/modules/entity/lib/Drupal/entity/Tests/EntityApiTest.php index e8d979f..c009161 100644 --- a/core/modules/entity/lib/Drupal/entity/Tests/EntityApiTest.php +++ b/core/modules/entity/lib/Drupal/entity/Tests/EntityApiTest.php @@ -36,21 +36,20 @@ class EntityApiTest extends WebTestBase { $user1 = $this->drupalCreateUser(); // Create some test entities. - $entity = entity_create('entity_test', array('name' => 'test', 'uid' => $user1->uid)); + $entity = entity_create('entity_test', array('name' => 'test', 'user_id' => $user1->uid)); $entity->save(); - $entity = entity_create('entity_test', array('name' => 'test2', 'uid' => $user1->uid)); + $entity = entity_create('entity_test', array('name' => 'test2', 'user_id' => $user1->uid)); $entity->save(); - $entity = entity_create('entity_test', array('name' => 'test', 'uid' => NULL)); + $entity = entity_create('entity_test', array('name' => 'test', 'user_id' => NULL)); $entity->save(); $entities = array_values(entity_load_multiple_by_properties('entity_test', array('name' => 'test'))); - - $this->assertEqual($entities[0]->get('name'), 'test', 'Created and loaded entity.'); - $this->assertEqual($entities[1]->get('name'), 'test', 'Created and loaded entity.'); + $this->assertEqual($entities[0]->name->value, 'test', 'Created and loaded entity.'); + $this->assertEqual($entities[1]->name->value, 'test', 'Created and loaded entity.'); // Test loading a single entity. - $loaded_entity = entity_test_load($entity->id); - $this->assertEqual($loaded_entity->id, $entity->id, 'Loaded a single entity by id.'); + $loaded_entity = entity_test_load($entity->id()); + $this->assertEqual($loaded_entity->id(), $entity->id(), 'Loaded a single entity by id.'); // Test deleting an entity. $entities = array_values(entity_load_multiple_by_properties('entity_test', array('name' => 'test2'))); @@ -60,10 +59,10 @@ class EntityApiTest extends WebTestBase { // Test updating an entity. $entities = array_values(entity_load_multiple_by_properties('entity_test', array('name' => 'test'))); - $entities[0]->set('name', 'test3'); + $entities[0]->name->value = 'test3'; $entities[0]->save(); - $entity = entity_test_load($entities[0]->id); - $this->assertEqual($entity->get('name'), 'test3', 'Entity updated.'); + $entity = entity_test_load($entities[0]->id()); + $this->assertEqual($entity->name->value, 'test3', 'Entity updated.'); // Try deleting multiple test entities by deleting all. $ids = array_keys(entity_test_load_multiple()); @@ -72,26 +71,4 @@ class EntityApiTest extends WebTestBase { $all = entity_test_load_multiple(); $this->assertTrue(empty($all), 'Deleted all entities.'); } - - /** - * Tests Entity getters/setters. - */ - function testEntityGettersSetters() { - $entity = entity_create('entity_test', array('name' => 'test', 'uid' => NULL)); - $this->assertNull($entity->get('uid'), 'Property is not set.'); - - $entity->set('uid', $GLOBALS['user']->uid); - $this->assertEqual($entity->get('uid'), $GLOBALS['user']->uid, 'Property has been set.'); - - $value = $entity->get('uid'); - $this->assertEqual($value, $entity->get('uid'), 'Property has been retrieved.'); - - // Make sure setting/getting translations boils down to setting/getting the - // regular value as the entity and property are not translatable. - $entity->set('uid', NULL, 'en'); - $this->assertNull($entity->uid, 'Language neutral property has been set.'); - - $value = $entity->get('uid', 'en'); - $this->assertNull($value, 'Language neutral property has been retrieved.'); - } } diff --git a/core/modules/entity/lib/Drupal/entity/Tests/EntityPropertyTest.php b/core/modules/entity/lib/Drupal/entity/Tests/EntityPropertyTest.php new file mode 100644 index 0000000..7225e77 --- /dev/null +++ b/core/modules/entity/lib/Drupal/entity/Tests/EntityPropertyTest.php @@ -0,0 +1,318 @@ + 'Entity property API', + 'description' => 'Tests the Entity property API', + 'group' => 'Entity API', + ); + } + + /** + * Creates a test entity. + * + * @return \Drupal\entity\EntityInterface + */ + protected function createTestEntity() { + $this->entity_name = $this->randomName(); + $this->entity_user = $this->drupalCreateUser(); + $this->entity_field_text = $this->randomName(); + + // Pass in the value of the name property when creating. With the user + // property we test setting a property after creation. + $entity = entity_create('entity_test', array()); + $entity->user_id->value = $this->entity_user->uid; + $entity->name->value = $this->entity_name; + + // Set a value for the test field. + $entity->field_test_text->value = $this->entity_field_text; + + return $entity; + } + + /** + * Tests reading and writing properties and property items. + */ + public function testReadWrite() { + $entity = $this->createTestEntity(); + + // Access the name property. + $this->assertTrue($entity->name instanceof ItemListInterface, 'Property implements interface'); + $this->assertTrue($entity->name[0] instanceof ItemInterface, 'Property item implements interface'); + + $this->assertEqual($this->entity_name, $entity->name->value, 'Name value can be read.'); + $this->assertEqual($this->entity_name, $entity->name[0]->value, 'Name value can be read through list access.'); + $this->assertEqual($entity->name->getValue(), array(0 => array('value' => $this->entity_name)), 'Plain property value returned.'); + + // Change the name. + $new_name = $this->randomName(); + $entity->name->value = $new_name; + $this->assertEqual($new_name, $entity->name->value, 'Name can be updated and read.'); + $this->assertEqual($entity->name->getValue(), array(0 => array('value' => $new_name)), 'Plain property value reflects the update.'); + + $new_name = $this->randomName(); + $entity->name[0]->value = $new_name; + $this->assertEqual($new_name, $entity->name->value, 'Name can be updated and read through list access.'); + + // Access the user property. + $this->assertTrue($entity->user_id instanceof ItemListInterface, 'Property implements interface'); + $this->assertTrue($entity->user_id[0] instanceof ItemInterface, 'Property item implements interface'); + + $this->assertEqual($this->entity_user->uid, $entity->user_id->value, 'User id can be read.'); + $this->assertEqual($this->entity_user->name, $entity->user_id->entity->name, 'User name can be read.'); + + // Change the assigned user by entity. + $new_user = $this->drupalCreateUser(); + $entity->user_id->entity = $new_user; + $this->assertEqual($new_user->uid, $entity->user_id->value, 'Updated user id can be read.'); + $this->assertEqual($new_user->name, $entity->user_id->entity->name, 'Updated user name value can be read.'); + + // Change the assigned user by id. + $new_user = $this->drupalCreateUser(); + $entity->user_id->value = $new_user->uid; + $this->assertEqual($new_user->uid, $entity->user_id->value, 'Updated user id can be read.'); + $this->assertEqual($new_user->name, $entity->user_id->entity->name, 'Updated user name value can be read.'); + + // Try unsetting a property. + $entity->name->value = NULL; + $entity->user_id->value = NULL; + $this->assertNull($entity->name->value, 'Name property is not set.'); + $this->assertNull($entity->user_id->value, 'User ID property is not set.'); + $this->assertNull($entity->user_id->entity, 'User entity property is not set.'); + + // Access the language property. + $this->assertEqual(LANGUAGE_NOT_SPECIFIED, $entity->langcode->value, 'Language code can be read.'); + $this->assertEqual(language_load(LANGUAGE_NOT_SPECIFIED), $entity->langcode->language, 'Language object can be read.'); + + // Change the language by code. + $entity->langcode->value = language_default()->langcode; + $this->assertEqual(language_default()->langcode, $entity->langcode->value, 'Language code can be read.'); + $this->assertEqual(language_default(), $entity->langcode->language, 'Language object can be read.'); + + // Revert language by code then try setting it by language object. + $entity->langcode->value = LANGUAGE_NOT_SPECIFIED; + $entity->langcode->language = language_default(); + $this->assertEqual(language_default()->langcode, $entity->langcode->value, 'Language code can be read.'); + $this->assertEqual(language_default(), $entity->langcode->language, 'Language object can be read.'); + + // Access the text field and test updating. + $this->assertEqual($entity->field_test_text->value, $this->entity_field_text, 'Text field can be read.'); + $new_text = $this->randomName(); + $entity->field_test_text->value = $new_text; + $this->assertEqual($entity->field_test_text->value, $new_text, 'Updated text field can be read.'); + + // Test creating the entity by passing in plain values. + $this->entity_name = $this->randomName(); + $name_item[0]['value'] = $this->entity_name; + $this->entity_user = $this->drupalCreateUser(); + $user_item[0]['value'] = $this->entity_user->uid; + $this->entity_field_text = $this->randomName(); + $text_item[0]['value'] = $this->entity_field_text; + + $entity = entity_create('entity_test', array( + 'name' => $name_item, + 'user_id' => $user_item, + 'field_test_text' => $text_item, + )); + $this->assertEqual($this->entity_name, $entity->name->value, 'Name value can be read.'); + $this->assertEqual($this->entity_user->uid, $entity->user_id->value, 'User id can be read.'); + $this->assertEqual($this->entity_user->name, $entity->user_id->entity->name, 'User name can be read.'); + $this->assertEqual($this->entity_field_text, $entity->field_test_text->value, 'Text field can be read.'); + } + + /** + * Tries to save and load an entity again. + */ + public function testSave() { + $entity = $this->createTestEntity(); + $entity->save(); + $this->assertTrue((bool) $entity->id(), 'Entity has received an id.'); + + $entity = entity_load('entity_test', $entity->id()); + $this->assertTrue((bool) $entity->id(), 'Entity loaded.'); + + // Access the name property. + $this->assertEqual(1, $entity->id->value, 'ID value can be read.'); + $this->assertTrue(is_string($entity->uuid->value), 'UUID value can be read.'); + $this->assertEqual(LANGUAGE_NOT_SPECIFIED, $entity->langcode->value, 'Language code can be read.'); + $this->assertEqual(language_load(LANGUAGE_NOT_SPECIFIED), $entity->langcode->language, 'Language object can be read.'); + $this->assertEqual($this->entity_user->uid, $entity->user_id->value, 'User id can be read.'); + $this->assertEqual($this->entity_user->name, $entity->user_id->entity->name, 'User name can be read.'); + $this->assertEqual($this->entity_field_text, $entity->field_test_text->value, 'Text field can be read.'); + } + + /** + * Tests introspection and getting metadata upfront. + */ + public function testIntrospection() { + // Test getting metadata upfront, i.e. without having an entity object. + $definition = array( + 'type' => 'entity', + 'constraints' => array( + 'entity type' => 'entity_test', + ), + 'label' => t('Test entity'), + ); + $wrapped_entity = drupal_wrap_data($definition); + $definitions = $wrapped_entity->getPropertyDefinitions($definition); + $this->assertEqual($definitions['name']['type'], 'string_item', 'Name property found.'); + $this->assertEqual($definitions['user_id']['type'], 'entityreference_item', 'User property found.'); + $this->assertEqual($definitions['field_test_text']['type'], 'text_item', 'Test-text-field property found.'); + + // Test introspecting an entity object. + // @todo: Add bundles and test bundles as well. + $entity = entity_create('entity_test', array()); + + $definitions = $entity->getPropertyDefinitions(); + $this->assertEqual($definitions['name']['type'], 'string_item', 'Name property found.'); + $this->assertEqual($definitions['user_id']['type'], 'entityreference_item', 'User property found.'); + $this->assertEqual($definitions['field_test_text']['type'], 'text_item', 'Test-text-field property found.'); + + $name_properties = $entity->name->getPropertyDefinitions(); + $this->assertEqual($name_properties['value']['type'], 'string', 'String value property of the name found.'); + + $userref_properties = $entity->user_id->getPropertyDefinitions(); + $this->assertEqual($userref_properties['value']['type'], 'integer', 'Entity id property of the user found.'); + $this->assertEqual($userref_properties['entity']['type'], 'entity', 'Entity reference property of the user found.'); + + $textfield_properties = $entity->field_test_text->getPropertyDefinitions(); + $this->assertEqual($textfield_properties['value']['type'], 'string', 'String value property of the test-text field found.'); + $this->assertEqual($textfield_properties['format']['type'], 'string', 'String format property of the test-text field found.'); + $this->assertEqual($textfield_properties['processed']['type'], 'string', 'String processed property of the test-text field found.'); + + // @todo: Once the user entity has definitions, continue testing getting + // them from the $userref_values['entity'] property. + } + + /** + * Tests iterating over properties. + */ + public function testIterator() { + $entity = $this->createTestEntity(); + + foreach ($entity as $name => $property) { + $this->assertTrue($property instanceof ItemListInterface, "Property $name implements interface."); + + foreach ($property as $delta => $item) { + $this->assertTrue($property[0] instanceof ItemInterface, "Item $delta of property $name implements interface."); + + foreach ($item as $value_name => $value_property) { + $this->assertTrue($value_property instanceof WrapperInterface, "Value $value_name of item $delta of property $name implements interface."); + + $value = $value_property->getValue(); + $this->assertTrue(!isset($value) || is_scalar($value) || $value instanceof EntityInterface, "Value $value_name of item $delta of property $name is a primitive or an entity."); + } + } + } + + $properties = $entity->getProperties(); + $this->assertEqual(array_keys($properties), array_keys($entity->getPropertyDefinitions()), 'All properties returned.'); + $this->assertEqual($properties, iterator_to_array($entity->getIterator()), 'Entity iterator iterates over all properties.'); + } + + /** + * Tests working with entity properties based upon data structure and data + * list interfaces. + */ + public function testDataStructureInterfaces() { + $entity = $this->createTestEntity(); + $entity->save(); + $entity_definition = array( + 'type' => 'entity', + 'constraints' => array( + 'entity type' => 'entity_test', + ), + 'label' => t('Test entity'), + ); + $wrapped_entity = drupal_wrap_data($entity_definition, $entity); + + // For the test we navigate through the tree of contained properties and get + // all contained strings, limited by a certain depth. + $strings = array(); + $this->getContainedStrings($wrapped_entity, 0, $strings); + + // @todo: Once the user entity has defined properties this should contain + // the user name and other user entity strings as well. + $target_strings = array( + $entity->uuid->value, + LANGUAGE_NOT_SPECIFIED, + $this->entity_name, + $this->entity_field_text, + // Field format. + NULL, + ); + $this->assertEqual($strings, $target_strings, 'All contained strings found.'); + } + + /** + * Recursive helper for getting all contained strings, + * i.e. properties of type string. + */ + public function getContainedStrings(WrapperInterface $wrapper, $depth, array &$strings) { + + if ($wrapper->getType() == 'string') { + $strings[] = $wrapper->getValue(); + } + + // Recurse until a certain depth is reached if possible. + if ($depth < 7) { + if ($wrapper instanceof \Drupal\Core\TypedData\ListInterface) { + foreach ($wrapper as $item) { + $this->getContainedStrings($item, $depth + 1, $strings); + } + } + elseif ($wrapper instanceof \Drupal\Core\TypedData\StructureInterface) { + foreach ($wrapper as $name => $property) { + $this->getContainedStrings($property, $depth + 1, $strings); + } + } + } + } + + /** + * Tests getting processed property values via a computed property. + */ + public function testComputedProperties() { + // Make the test text field processed. + $instance = field_info_instance('entity_test', 'field_test_text', 'entity_test'); + $instance['settings']['text_processing'] = 1; + field_update_instance($instance); + + $entity = $this->createTestEntity(); + $entity->field_test_text->value = "The text text to filter."; + $entity->field_test_text->format = filter_default_format(); + + $target = "

The <strong>text</strong> text to filter.

\n"; + $this->assertEqual($entity->field_test_text->processed, $target, 'Text is processed with the default filter.'); + + // Save and load entity and make sure it still works. + $entity->save(); + $entity = entity_load('entity_test', $entity->id()); + $this->assertEqual($entity->field_test_text->processed, $target, 'Text is processed with the default filter.'); + } +} diff --git a/core/modules/entity/lib/Drupal/entity/Tests/EntityTranslationTest.php b/core/modules/entity/lib/Drupal/entity/Tests/EntityTranslationTest.php index 252e048..bd2a1d0 100644 --- a/core/modules/entity/lib/Drupal/entity/Tests/EntityTranslationTest.php +++ b/core/modules/entity/lib/Drupal/entity/Tests/EntityTranslationTest.php @@ -76,71 +76,67 @@ class EntityTranslationTest extends WebTestBase { function testEntityLanguageMethods() { $entity = entity_create('entity_test', array( 'name' => 'test', - 'uid' => $GLOBALS['user']->uid, + 'user_id' => $GLOBALS['user']->uid, )); $this->assertEqual($entity->language()->langcode, LANGUAGE_NOT_SPECIFIED, 'Entity language not specified.'); - $this->assertFalse($entity->translations(), 'No translations are available'); + $this->assertFalse($entity->getTranslationLanguages(FALSE), 'No translations are available'); // Set the value in default language. $entity->set($this->field_name, array(0 => array('value' => 'default value'))); // Get the value. - $value = $entity->get($this->field_name); - $this->assertEqual($value, array(0 => array('value' => 'default value')), 'Untranslated value retrieved.'); + $this->assertEqual($entity->getTranslation(LANGUAGE_DEFAULT)->get($this->field_name)->value, 'default value', 'Untranslated value retrieved.'); // Set the value in a certain language. As the entity is not // language-specific it should use the default language and so ignore the // specified language. - $entity->set($this->field_name, array(0 => array('value' => 'default value2')), $this->langcodes[1]); - $value = $entity->get($this->field_name); - $this->assertEqual($value, array(0 => array('value' => 'default value2')), 'Untranslated value updated.'); - $this->assertFalse($entity->translations(), 'No translations are available'); + $entity->getTranslation($this->langcodes[1])->set($this->field_name, array(0 => array('value' => 'default value2'))); + $this->assertEqual($entity->get($this->field_name)->value, 'default value2', 'Untranslated value updated.'); + $this->assertFalse($entity->getTranslationLanguages(FALSE), 'No translations are available'); // Test getting a field value using the default language for a not // language-specific entity. - $value = $entity->get($this->field_name, $this->langcodes[1]); - $this->assertEqual($value, array(0 => array('value' => 'default value2')), 'Untranslated value retrieved.'); + $this->assertEqual($entity->getTranslation($this->langcodes[1])->get($this->field_name)->value, 'default value2', 'Untranslated value retrieved.'); // Now, make the entity language-specific by assigning a language and test // translating it. - $entity->setLangcode($this->langcodes[0]); + $entity->langcode->value = $this->langcodes[0]; $entity->{$this->field_name} = array(); $this->assertEqual($entity->language(), language_load($this->langcodes[0]), 'Entity language retrieved.'); - $this->assertFalse($entity->translations(), 'No translations are available'); + $this->assertFalse($entity->getTranslationLanguages(FALSE), 'No translations are available'); // Set the value in default language. $entity->set($this->field_name, array(0 => array('value' => 'default value'))); // Get the value. - $value = $entity->get($this->field_name); - $this->assertEqual($value, array(0 => array('value' => 'default value')), 'Untranslated value retrieved.'); + $this->assertEqual($entity->get($this->field_name)->value, 'default value', 'Untranslated value retrieved.'); // Set a translation. - $entity->set($this->field_name, array(0 => array('value' => 'translation 1')), $this->langcodes[1]); - $value = $entity->get($this->field_name, $this->langcodes[1]); - $this->assertEqual($value, array(0 => array('value' => 'translation 1')), 'Translated value set.'); + $entity->getTranslation($this->langcodes[1])->set($this->field_name, array(0 => array('value' => 'translation 1'))); + $this->assertEqual($entity->getTranslation($this->langcodes[1])->{$this->field_name}->value, 'translation 1', 'Translated value set.'); + // Make sure the untranslated value stays. - $value = $entity->get($this->field_name); - $this->assertEqual($value, array(0 => array('value' => 'default value')), 'Untranslated value stays.'); + $this->assertEqual($entity->get($this->field_name)->value, 'default value', 'Untranslated value stays.'); $translations[$this->langcodes[1]] = language_load($this->langcodes[1]); - $this->assertEqual($entity->translations(), $translations, 'Translations retrieved.'); + $this->assertEqual($entity->getTranslationLanguages(FALSE), $translations, 'Translations retrieved.'); // Try to get a not available translation. - $value = $entity->get($this->field_name, $this->langcodes[2]); - $this->assertNull($value, 'A translation that is not available is NULL.'); + $this->assertNull($entity->getTranslation($this->langcodes[2])->get($this->field_name)->value, 'A translation that is not available is NULL.'); // Try to get a value using an invalid language code. - $value = $entity->get($this->field_name, 'invalid'); - $this->assertNull($value, 'A translation for an invalid language is NULL.'); - + try { + $entity->getTranslation('invalid')->get($this->field_name)->value; + $this->fail('Getting a translation for an invalid language throws an exception.'); + } + catch (InvalidArgumentException $e) { + $this->pass('A translation for an invalid language is NULL.'); + } // Try to set a value using an invalid language code. - $message = "An exception is thrown when trying to set an invalid translation."; try { - $entity->set($this->field_name, NULL, 'invalid'); - // This line is not expected to be executed unless something goes wrong. - $this->fail($message); + $entity->getTranslation('invalid')->set($this->field_name, NULL); + $this->fail("Setting a translation for an invalid language throws an exception."); } - catch (Exception $e) { - $this->assertTrue($e instanceof InvalidArgumentException, $message); + catch (InvalidArgumentException $e) { + $this->pass("Setting a translation for an invalid language throws an exception."); } } @@ -154,29 +150,33 @@ class EntityTranslationTest extends WebTestBase { // Create a language neutral entity and check that properties are stored // as language neutral. - $entity = entity_create('entity_test', array('name' => $name, 'uid' => $uid)); + $entity = entity_create('entity_test', array('name' => $name, 'user_id' => $uid)); $entity->save(); $entity = entity_test_load($entity->id()); $this->assertEqual($entity->language()->langcode, LANGUAGE_NOT_SPECIFIED, 'Entity created as language neutral.'); - $this->assertEqual($name, $entity->get('name', LANGUAGE_NOT_SPECIFIED), 'The entity name has been correctly stored as language neutral.'); - $this->assertEqual($uid, $entity->get('uid', LANGUAGE_NOT_SPECIFIED), 'The entity author has been correctly stored as language neutral.'); - $this->assertNull($entity->get('name', $langcode), 'The entity name is not available as a language-aware property.'); - $this->assertNull($entity->get('uid', $langcode), 'The entity author is not available as a language-aware property.'); - $this->assertEqual($name, $entity->get('name'), 'The entity name can be retrieved without specifying a language.'); - $this->assertEqual($uid, $entity->get('uid'), 'The entity author can be retrieved without specifying a language.'); + $this->assertEqual($name, $entity->getTranslation(LANGUAGE_DEFAULT)->get('name')->value, 'The entity name has been correctly stored as language neutral.'); + $this->assertEqual($uid, $entity->getTranslation(LANGUAGE_DEFAULT)->get('user_id')->value, 'The entity author has been correctly stored as language neutral.'); + // As fields, translatable properties should ignore the given langcode and + // use neutral language if the entity is not translatable. + $this->assertEqual($name, $entity->getTranslation($langcode)->get('name')->value, 'The entity name defaults to neutral language.'); + $this->assertEqual($uid, $entity->getTranslation($langcode)->get('user_id')->value, 'The entity author defaults to neutral language.'); + $this->assertEqual($name, $entity->get('name')->value, 'The entity name can be retrieved without specifying a language.'); + $this->assertEqual($uid, $entity->get('user_id')->value, 'The entity author can be retrieved without specifying a language.'); // Create a language-aware entity and check that properties are stored // as language-aware. - $entity = entity_create('entity_test', array('name' => $name, 'uid' => $uid, 'langcode' => $langcode)); + $entity = entity_create('entity_test', array('name' => $name, 'user_id' => $uid, 'langcode' => $langcode)); $entity->save(); $entity = entity_test_load($entity->id()); $this->assertEqual($entity->language()->langcode, $langcode, 'Entity created as language specific.'); - $this->assertEqual($name, $entity->get('name', $langcode), 'The entity name has been correctly stored as a language-aware property.'); - $this->assertEqual($uid, $entity->get('uid', $langcode), 'The entity author has been correctly stored as a language-aware property.'); - $this->assertNull($entity->get('name', LANGUAGE_NOT_SPECIFIED), 'The entity name is not available as a language neutral property.'); - $this->assertNull($entity->get('uid', LANGUAGE_NOT_SPECIFIED), 'The entity author is not available as a language neutral property.'); - $this->assertEqual($name, $entity->get('name'), 'The entity name can be retrieved without specifying a language.'); - $this->assertEqual($uid, $entity->get('uid'), 'The entity author can be retrieved without specifying a language.'); + $this->assertEqual($name, $entity->getTranslation($langcode)->get('name')->value, 'The entity name has been correctly stored as a language-aware property.'); + $this->assertEqual($uid, $entity->getTranslation($langcode)->get('user_id')->value, 'The entity author has been correctly stored as a language-aware property.'); + // Translatable properties on a translatable entity should use default + // language if LANGUAGE_NOT_SPECIFIED is passed. + $this->assertEqual($name, $entity->getTranslation(LANGUAGE_NOT_SPECIFIED)->get('name')->value, 'The entity name defaults to the default language.'); + $this->assertEqual($uid, $entity->getTranslation(LANGUAGE_NOT_SPECIFIED)->get('user_id')->value, 'The entity author defaults to the default language.'); + $this->assertEqual($name, $entity->get('name')->value, 'The entity name can be retrieved without specifying a language.'); + $this->assertEqual($uid, $entity->get('user_id')->value, 'The entity author can be retrieved without specifying a language.'); // Create property translations. $properties = array(); @@ -184,17 +184,17 @@ class EntityTranslationTest extends WebTestBase { foreach ($this->langcodes as $langcode) { if ($langcode != $default_langcode) { $properties[$langcode] = array( - 'name' => $this->randomName(), - 'uid' => mt_rand(0, 127), + 'name' => array(0 => $this->randomName()), + 'user_id' => array(0 => mt_rand(0, 127)), ); } else { $properties[$langcode] = array( - 'name' => $name, - 'uid' => $uid, + 'name' => array(0 => $name), + 'user_id' => array(0 => $uid), ); } - $entity->setProperties($properties[$langcode], $langcode); + $entity->getTranslation($langcode)->setProperties($properties[$langcode]); } $entity->save(); @@ -202,8 +202,8 @@ class EntityTranslationTest extends WebTestBase { $entity = entity_test_load($entity->id()); foreach ($this->langcodes as $langcode) { $args = array('%langcode' => $langcode); - $this->assertEqual($properties[$langcode]['name'], $entity->get('name', $langcode), format_string('The entity name has been correctly stored for language %langcode.', $args)); - $this->assertEqual($properties[$langcode]['uid'], $entity->get('uid', $langcode), format_string('The entity author has been correctly stored for language %langcode.', $args)); + $this->assertEqual($properties[$langcode]['name'][0], $entity->getTranslation($langcode)->get('name')->value, format_string('The entity name has been correctly stored for language %langcode.', $args)); + $this->assertEqual($properties[$langcode]['user_id'][0], $entity->getTranslation($langcode)->get('user_id')->value, format_string('The entity author has been correctly stored for language %langcode.', $args)); } // Test query conditions (cache is reset at each call). @@ -211,7 +211,11 @@ class EntityTranslationTest extends WebTestBase { // Create an additional entity with only the uid set. The uid for the // original language is the same of one used for a translation. $langcode = $this->langcodes[1]; - entity_create('entity_test', array('uid' => $properties[$langcode]['uid']))->save(); + entity_create('entity_test', array( + 'user_id' => $properties[$langcode]['user_id'], + 'name' => 'some name', + ))->save(); + $entities = entity_test_load_multiple(); $this->assertEqual(count($entities), 3, 'Three entities were created.'); $entities = entity_test_load_multiple(array($translated_id)); @@ -220,15 +224,16 @@ class EntityTranslationTest extends WebTestBase { $this->assertEqual(count($entities), 2, 'Two entities correctly loaded by name.'); // @todo The default language condition should go away in favor of an // explicit parameter. - $entities = entity_load_multiple_by_properties('entity_test', array('name' => $properties[$langcode]['name'], 'default_langcode' => 0)); + $entities = entity_load_multiple_by_properties('entity_test', array('name' => $properties[$langcode]['name'][0], 'default_langcode' => 0)); $this->assertEqual(count($entities), 1, 'One entity correctly loaded by name translation.'); $entities = entity_load_multiple_by_properties('entity_test', array('langcode' => $default_langcode, 'name' => $name)); $this->assertEqual(count($entities), 1, 'One entity correctly loaded by name and language.'); - $entities = entity_load_multiple_by_properties('entity_test', array('langcode' => $langcode, 'name' => $properties[$langcode]['name'])); + + $entities = entity_load_multiple_by_properties('entity_test', array('langcode' => $langcode, 'name' => $properties[$langcode]['name'][0])); $this->assertEqual(count($entities), 0, 'No entity loaded by name translation specifying the translation language.'); - $entities = entity_load_multiple_by_properties('entity_test', array('langcode' => $langcode, 'name' => $properties[$langcode]['name'], 'default_langcode' => 0)); + $entities = entity_load_multiple_by_properties('entity_test', array('langcode' => $langcode, 'name' => $properties[$langcode]['name'][0], 'default_langcode' => 0)); $this->assertEqual(count($entities), 1, 'One entity loaded by name translation and language specifying to look for translations.'); - $entities = entity_load_multiple_by_properties('entity_test', array('uid' => $properties[$langcode]['uid'], 'default_langcode' => NULL)); + $entities = entity_load_multiple_by_properties('entity_test', array('user_id' => $properties[$langcode]['user_id'][0], 'default_langcode' => NULL)); $this->assertEqual(count($entities), 2, 'Two entities loaded by uid without caring about property translatability.'); } } diff --git a/core/modules/entity/tests/modules/entity_test/entity_test.info b/core/modules/entity/tests/modules/entity_test/entity_test.info index ae6006b..6458417 100644 --- a/core/modules/entity/tests/modules/entity_test/entity_test.info +++ b/core/modules/entity/tests/modules/entity_test/entity_test.info @@ -4,4 +4,5 @@ package = Testing version = VERSION core = 8.x dependencies[] = entity +dependencies[] = field hidden = TRUE diff --git a/core/modules/entity/tests/modules/entity_test/entity_test.install b/core/modules/entity/tests/modules/entity_test/entity_test.install index 3621eb6..4122d01 100644 --- a/core/modules/entity/tests/modules/entity_test/entity_test.install +++ b/core/modules/entity/tests/modules/entity_test/entity_test.install @@ -91,7 +91,7 @@ function entity_test_schema() { 'not null' => TRUE, 'default' => '', ), - 'uid' => array( + 'user_id' => array( 'type' => 'int', 'unsigned' => TRUE, 'not null' => FALSE, @@ -100,10 +100,10 @@ function entity_test_schema() { ), ), 'indexes' => array( - 'uid' => array('uid'), + 'user_id' => array('user_id'), ), 'foreign keys' => array( - 'uid' => array('users' => 'uid'), + 'user_id' => array('users' => 'uid'), 'id' => array('entity_test' => 'id'), ), 'primary key' => array('id', 'langcode'), diff --git a/core/modules/entity/tests/modules/entity_test/lib/Drupal/entity_test/EntityTest.php b/core/modules/entity/tests/modules/entity_test/lib/Drupal/entity_test/EntityTest.php index 6c95f75..d36d758 100644 --- a/core/modules/entity/tests/modules/entity_test/lib/Drupal/entity_test/EntityTest.php +++ b/core/modules/entity/tests/modules/entity_test/lib/Drupal/entity_test/EntityTest.php @@ -7,137 +7,52 @@ namespace Drupal\entity_test; -use InvalidArgumentException; - -use Drupal\entity\Entity; +use Drupal\entity\EntityNG; /** * Defines the test entity class. */ -class EntityTest extends Entity { +class EntityTest extends EntityNG { /** - * An array keyed by language code where the entity properties are stored. + * The entity ID. * - * @var array + * @var \Drupal\entity\Property\ItemListInterface */ - protected $properties; + public $id; /** - * An array of allowed language codes. + * The entity UUID. * - * @var array - */ - protected static $langcodes; - - /** - * Constructs a new entity object. + * @var \Drupal\entity\Property\ItemListInterface */ - public function __construct(array $values, $entity_type) { - parent::__construct($values, $entity_type); - - if (!isset(self::$langcodes)) { - // The allowed languages are simply all the available ones in the system. - self::$langcodes = drupal_map_assoc(array_keys(language_list(LANGUAGE_ALL))); - } - - // Initialize the original entity language with the provided value or fall - // back to LANGUAGE_NOT_SPECIFIED if none was specified. We do not check - // against allowed languages here, since throwing an exception would make an - // entity created in a subsequently uninstalled language not instantiable. - $this->langcode = !empty($values['langcode']) ? $values['langcode'] : LANGUAGE_NOT_SPECIFIED; - - // Set initial values ensuring that only real properties are stored. - // @todo For now we have no way to mark a property as multlingual hence we - // just assume that all of them are. - unset($values['id'], $values['uuid'], $values['default_langcode']); - $this->setProperties($values, $this->langcode); - } + public $uuid; /** - * Sets the entity original langcode. + * The name of the test entity. * - * @param $langcode - */ - public function setLangcode($langcode) { - // If the original language is changed the related properties must change - // their language accordingly. - $prev_langcode = $this->langcode; - if (isset($this->properties[$prev_langcode])) { - $this->properties[$langcode] = $this->properties[$prev_langcode]; - unset($this->properties[$prev_langcode]); - } - $this->langcode = $langcode; - } - - /** - * Overrides EntityInterface::get(). + * @var \Drupal\entity\Property\ItemListInterface */ - public function get($property_name, $langcode = NULL) { - $langcode = !empty($langcode) ? $langcode : $this->langcode; - $entity_info = $this->entityInfo(); - if ($entity_info['fieldable'] && field_info_instance($this->entityType, $property_name, $this->bundle())) { - return parent::get($property_name, $langcode); - } - elseif (isset($this->properties[$langcode][$property_name])) { - return $this->properties[$langcode][$property_name]; - } - else { - // @todo Remove this. All properties should be stored in the $properties - // array once we have a Property API in place. - return property_exists($this, $property_name) ? $this->{$property_name} : NULL; - } - } - - /** - * Overrides EntityInterface::set(). - */ - public function set($property_name, $value, $langcode = NULL) { - $langcode = !empty($langcode) ? $langcode : $this->langcode; - if (!isset(self::$langcodes[$langcode])) { - throw new InvalidArgumentException("Detected an invalid language '$langcode' while setting '$property_name' to '$value'."); - } - $entity_info = $this->entityInfo(); - if ($entity_info['fieldable'] && field_info_instance($this->entityType, $property_name, $this->bundle())) { - parent::set($property_name, $value, $langcode); - } - else { - $this->properties[$langcode][$property_name] = $value; - } - } - - /** - * Overrides EntityInterface::translations(). - */ - public function translations() { - $translations = !empty($this->properties) ? $this->properties : array(); - $languages = array_intersect_key(self::$langcodes, $translations); - unset($languages[$this->langcode]); - return $languages + parent::translations(); - } + public $name; /** - * Returns the property array for the given language. + * The associated user. * - * @param string $langcode - * (optional) The language code to be used to retrieve the properties. + * @var \Drupal\entity\Property\ItemListInterface */ - public function getProperties($langcode = NULL) { - $langcode = !empty($langcode) ? $langcode : $this->langcode; - return isset($this->properties[$langcode]) ? $this->properties[$langcode] : array(); - } + public $user_id; /** - * Sets the property array for the given language. - * - * @param array $properties - * A keyed array of properties to be set with their 'langcode' as one of the - * keys. If no language is provided the entity language is used. - * @param string $langcode - * (optional) The language code to be used to set the properties. + * Overrides Entity::__construct(). */ - public function setProperties(array $properties, $langcode = NULL) { - $langcode = !empty($langcode) ? $langcode : $this->langcode; - $this->properties[$langcode] = $properties; + public function __construct(array $values, $entity_type) { + parent::__construct($values, $entity_type); + + // We unset all defined properties, so magic getters apply. + unset($this->id); + unset($this->langcode); + unset($this->uuid); + unset($this->name); + unset($this->user_id); } } diff --git a/core/modules/entity/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php b/core/modules/entity/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php index b31e5ce..d127196 100644 --- a/core/modules/entity/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php +++ b/core/modules/entity/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php @@ -11,6 +11,8 @@ use PDO; use Drupal\entity\EntityInterface; use Drupal\entity\DatabaseStorageController; +use Drupal\entity\EntityStorageException; +use Drupal\Component\Uuid\Uuid; /** * Defines the controller class for the test entity. @@ -21,6 +23,93 @@ use Drupal\entity\DatabaseStorageController; class EntityTestStorageController extends DatabaseStorageController { /** + * The entity class to use. + * + * @todo: Remove this once this is moved in the main controller. + * + * @var string + */ + protected $entityClass; + + /** + * The entity bundle key. + * + * @var string|bool + */ + protected $bundleKey; + + /** + * Overrides DatabaseStorageController::__construct(). + */ + public function __construct($entityType) { + parent::__construct($entityType); + $this->bundleKey = !empty($this->entityInfo['entity keys']['bundle']) ? $this->entityInfo['entity keys']['bundle'] : FALSE; + + // Let load() get stdClass storage records. We map them to entities in + // attachLoad(). + // @todo: Remove this once this is moved in the main controller. + $this->entityClass = $this->entityInfo['entity class']; + unset($this->entityInfo['entity class']); + } + + /** + * Overrides DatabaseStorageController::create(). + * + * @param array $values + * An array of values to set, keyed by property name. The value has to be + * the plain value of an entity property, i.e. an array of property items. + * If no array is given, the value will be set for the first property item. + * Thus to set the first item of a 'name' property one can pass: + * @code + * $values = array('name' => array(0 => array('value' => 'the name'))); + * @endcode + * or + * @code + * $values = array('name' => array('value' => 'the name')); + * @endcode + * + * Furthermore, property items having only a single value support setting + * this value without passing an array of values, making it possible to + * set the 'name' property via: + * @code + * $values = array('name' => 'the name'); + * @endcode + * + * @todo: Remove this once this is moved in the main controller. + */ + public function create(array $values) { + // Pass in default values. + $defaults = array(); + $defaults['langcode'][LANGUAGE_NOT_SPECIFIED][0]['value'] = LANGUAGE_NOT_SPECIFIED; + + $entity = new $this->entityClass(array('values' => $defaults), $this->entityType); + + // Make sure to set the bundle first. + if ($this->bundleKey) { + $entity->{$this->bundleKey} = $values[$this->bundleKey]; + unset($values[$this->bundleKey]); + } + + // Set all other given values. + foreach ($values as $name => $value) { + if (is_array($value) && is_numeric(current(array_keys($value)))) { + $entity->$name = $value; + } + else { + // Support passing in the first value of a property item. + $entity->{$name}[0] = $value; + } + } + + // Assign a new UUID if there is none yet. + if ($this->uuidKey && !isset($entity->{$this->uuidKey})) { + $uuid = new Uuid(); + $entity->{$this->uuidKey}->value = $uuid->generate(); + } + return $entity; + } + + /** * Overrides Drupal\entity\DatabaseStorageController::loadByProperties(). */ public function loadByProperties(array $values) { @@ -39,7 +128,7 @@ class EntityTestStorageController extends DatabaseStorageController { if (!array_key_exists('default_langcode', $values)) { $values['default_langcode'] = 1; } - // If the 'default_langcode' flag is esplicitly not set, we do not care + // If the 'default_langcode' flag is explicitly not set, we do not care // whether the queried values are in the original entity language or not. elseif ($values['default_langcode'] === NULL) { unset($values['default_langcode']); @@ -58,9 +147,51 @@ class EntityTestStorageController extends DatabaseStorageController { } /** - * Overrides Drupal\entity\DatabaseStorageController::attachLoad(). + * Overrides DatabaseStorageController::attachLoad(). + * + * Added mapping from storage records to entities. */ - protected function attachLoad(&$queried_entities, $load_revision = FALSE) { + protected function attachLoad(&$queried_entities, $revision_id = FALSE) { + // Now map the record values to the according entity properties and + // activate compatibility mode. + $queried_entities = $this->mapFromStorageRecords($queried_entities); + + // Load data of translatable properties. + $this->attachPropertyData($queried_entities); + + parent::attachLoad($queried_entities, $revision_id); + + // Loading is finished, so disable compatibility mode now. + foreach ($queried_entities as $entity) { + $entity->setCompatibilityMode(FALSE); + } + } + + /** + * Maps from storage records to entity objects. + * + * @return array + * An array of entity objects implementing the EntityInterface. + */ + protected function mapFromStorageRecords(array $records) { + + foreach ($records as $id => $record) { + $entity = new $this->entityClass(array(), $this->entityType); + $entity->setCompatibilityMode(TRUE); + + $entity->id[LANGUAGE_NOT_SPECIFIED][0]['value'] = $id; + $entity->uuid[LANGUAGE_NOT_SPECIFIED][0]['value'] = $record->uuid; + $entity->langcode[LANGUAGE_NOT_SPECIFIED][0]['value'] = $record->langcode; + + $records[$id] = $entity; + } + return $records; + } + + /** + * Attaches property data in all languages for translatable properties. + */ + protected function attachPropertyData(&$queried_entities) { $data = db_select('entity_test_property_data', 'data', array('fetch' => PDO::FETCH_ASSOC)) ->fields('data') ->condition('id', array_keys($queried_entities)) @@ -68,35 +199,118 @@ class EntityTestStorageController extends DatabaseStorageController { ->execute(); foreach ($data as $values) { - $entity = $queried_entities[$values['id']]; - $langcode = $values['langcode']; - if (!empty($values['default_langcode'])) { - $entity->setLangcode($langcode); + $id = $values['id']; + // Property values in default language are stored with + // LANGUAGE_NOT_SPECIFIED as key. + $langcode = empty($values['default_langcode']) ? $values['langcode'] : LANGUAGE_NOT_SPECIFIED; + + $queried_entities[$id]->name[$langcode][0]['value'] = $values['name']; + $queried_entities[$id]->user_id[$langcode][0]['value'] = $values['user_id']; + } + } + + /** + * Overrides DatabaseStorageController::save(). + * + * Added mapping from entities to storage records before saving. + */ + public function save(EntityInterface $entity) { + $transaction = db_transaction(); + try { + // Load the stored entity, if any. + if (!$entity->isNew() && !isset($entity->original)) { + $entity->original = entity_load_unchanged($this->entityType, $entity->id()); } - // Make sure only real properties are stored. - unset($values['id'], $values['default_langcode']); - $entity->setProperties($values, $langcode); + + $this->preSave($entity); + $this->invokeHook('presave', $entity); + + // Create the storage record to be saved. + $record = $this->maptoStorageRecord($entity); + // Update the original values so that the compatibility mode works with + // the update values, what is required by field API attachers. + // @todo Once field API has been converted to use the Property API, move + // this after insert/update hooks. + $entity->updateOriginalValues(); + + if (!$entity->isNew()) { + $return = drupal_write_record($this->entityInfo['base table'], $record, 'id'); + $this->resetCache(array($entity->id())); + $this->postSave($entity, TRUE); + $this->invokeHook('update', $entity); + } + else { + $return = drupal_write_record($this->entityInfo['base table'], $record); + // Reset general caches, but keep caches specific to certain entities. + $this->resetCache(array()); + + $entity->{$this->idKey}->value = $record->id; + $entity->enforceIsNew(FALSE); + $this->postSave($entity, FALSE); + $this->invokeHook('insert', $entity); + } + + // Ignore slave server temporarily. + db_ignore_slave(); + unset($entity->original); + + return $return; + } + catch (Exception $e) { + $transaction->rollback(); + watchdog_exception($this->entityType, $e); + throw new EntityStorageException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Overrides DatabaseStorageController::invokeHook(). + * + * Invokes field API attachers in compatibility mode and disables it + * afterwards. + */ + protected function invokeHook($hook, EntityInterface $entity) { + if (!empty($this->entityInfo['fieldable']) && function_exists($function = 'field_attach_' . $hook)) { + $entity->setCompatibilityMode(TRUE); + $function($this->entityType, $entity); + $entity->setCompatibilityMode(FALSE); } - parent::attachLoad($queried_entities, $load_revision); + // Invoke the hook. + module_invoke_all($this->entityType . '_' . $hook, $entity); + // Invoke the respective entity-level hook. + module_invoke_all('entity_' . $hook, $entity, $this->entityType); + } + + /** + * Maps from an entity object to the storage record of the base table. + */ + protected function mapToStorageRecord(EntityInterface $entity) { + $record = new \stdClass(); + $record->id = $entity->id(); + $record->langcode = $entity->langcode->value; + $record->uuid = $entity->uuid->value; + return $record; } /** * Overrides Drupal\entity\DatabaseStorageController::postSave(). + * + * Stores values of translatable properties. */ protected function postSave(EntityInterface $entity, $update) { - $default_langcode = ($language = $entity->language()) ? $language->langcode : LANGUAGE_NOT_SPECIFIED; - $langcodes = array_keys($entity->translations()); - $langcodes[] = $default_langcode; + $default_langcode = $entity->language()->langcode; - foreach ($langcodes as $langcode) { - $properties = $entity->getProperties($langcode); + foreach ($entity->getTranslationLanguages() as $langcode => $language) { + $translation = $entity->getTranslation($langcode); $values = array( 'id' => $entity->id(), 'langcode' => $langcode, 'default_langcode' => intval($default_langcode == $langcode), - ) + $properties; + 'name' => $translation->name->value, + 'user_id' => $translation->user_id->value, + ); db_merge('entity_test_property_data') ->fields($values) @@ -114,4 +328,68 @@ class EntityTestStorageController extends DatabaseStorageController { ->condition('id', array_keys($entities)) ->execute(); } + + /** + * Overrides \Drupal\entity\DataBaseStorageController::basePropertyDefinitions(). + */ + public function basePropertyDefinitions() { + $properties['id'] = array( + 'label' => t('ID'), + 'description' => ('The ID of the test entity.'), + 'type' => 'integer_item', + 'list' => TRUE, + 'read-only' => TRUE, + ); + $properties['uuid'] = array( + 'label' => t('UUID'), + 'description' => ('The UUID of the test entity.'), + 'type' => 'string_item', + 'list' => TRUE, + ); + $properties['langcode'] = array( + 'label' => t('Language code'), + 'description' => ('The language code of the test entity.'), + 'type' => 'language_item', + 'list' => TRUE, + ); + $properties['name'] = array( + 'label' => t('Name'), + 'description' => ('The name of the test entity.'), + 'type' => 'string_item', + 'list' => TRUE, + 'translatable' => TRUE, + ); + $properties['user_id'] = array( + 'label' => t('User ID'), + 'description' => t('The ID of the associated user.'), + 'type' => 'entityreference_item', + 'settings' => array('entity type' => 'user'), + 'list' => TRUE, + 'translatable' => TRUE, + ); + return $properties; + } + + /** + * Overrides \Drupal\entity\DataBaseStorageController::cacheGet(). + */ + protected function cacheGet($ids, $conditions = array()) { + $entities = parent::cacheGet($ids, array()); + + // Exclude any entities loaded from cache if they don't match $conditions. + // This ensures the same behavior whether loading from memory or database. + if ($conditions) { + if (!$ids) { + $entities = $this->entityCache; + } + + foreach ($entities as $entity) { + $entity_values = $entity->toArray(); + if (array_diff_assoc($conditions, $entity_values)) { + unset($entities[$entity->id()]); + } + } + } + return $entities; + } } diff --git a/core/modules/field/field.default.inc b/core/modules/field/field.default.inc index b4a6f50..bb7f8d6 100644 --- a/core/modules/field/field.default.inc +++ b/core/modules/field/field.default.inc @@ -107,7 +107,7 @@ function field_default_insert($entity_type, $entity, $field, $instance, $langcod // assigning it a default value. This way we ensure that only the intended // languages get a default value. Otherwise we could have default values for // not yet open languages. - if (empty($entity) || !property_exists($entity, $field['field_name']) || + if (empty($entity) || (!isset($entity->{$field['field_name']}[$langcode]) && !property_exists($entity, $field['field_name'])) || (isset($entity->{$field['field_name']}[$langcode]) && count($entity->{$field['field_name']}[$langcode]) == 0)) { $items = field_get_default_value($entity_type, $entity, $field, $instance, $langcode); } diff --git a/core/modules/field/field.module b/core/modules/field/field.module index e75111e..5daa3ac 100644 --- a/core/modules/field/field.module +++ b/core/modules/field/field.module @@ -370,6 +370,66 @@ function field_system_info_alter(&$info, $file, $type) { } /** + * Implements hook_data_type_info() to register data types for all field types. + */ +function field_data_type_info() { + $field_types = field_info_field_types(); + $items = array(); + + // Expose data types for all the field type items. + // @todo: Make 'property class' mandatory. + foreach ($field_types as $type_name => $type_info) { + + if (!empty($type_info['property class'])) { + $items[$type_name . '_item'] = array( + 'label' => t('Field !label item', array('!label' => $type_info['label'])), + 'class' => $type_info['property class'], + 'list class' => !empty($type_info['property list class']) ? $type_info['property list class'] : '\Drupal\field\FieldItemList', + ); + } + } + return $items; +} + +/** + * Implements hook_entity_property_info() to define properties for all fields. + */ +function field_entity_property_info($entity_type) { + $property_info = array(); + $field_types = field_info_field_types(); + + foreach (field_info_instances($entity_type) as $bundle_name => $instances) { + $optional = $bundle_name != $entity_type; + + foreach ($instances as $field_name => $instance) { + $field = field_info_field($field_name); + + if (!empty($field_types[$field['type']]['property class'])) { + + // @todo: Allow for adding field type settings. + $definition = array( + 'label' => t('Field !name', array('!name' => $field_name)), + 'type' => $field['type'] . '_item', + 'field' => TRUE, + 'list' => TRUE, + 'translatable' => !empty($field['translatable']) + ); + + if ($optional) { + $property_info['optional'][$field_name] = $definition; + $property_info['bundle map'][$bundle_name][] = $field_name; + } + else { + $property_info['definitions'][$field_name] = $definition; + } + } + } + } + + return $property_info; +} + +/** * Applies language fallback rules to the fields attached to the given entity. * * Core language fallback rules simply check if fields have a field translation diff --git a/core/modules/field/lib/Drupal/field/FieldItemBase.php b/core/modules/field/lib/Drupal/field/FieldItemBase.php new file mode 100644 index 0000000..87ccd33 --- /dev/null +++ b/core/modules/field/lib/Drupal/field/FieldItemBase.php @@ -0,0 +1,65 @@ +fieldContext = $context['parent']->getFieldContext(); + } + } + + /** + * Implements FieldItemListInterface::setFieldContext(). + */ + public function setFieldContext($entity_type, $name, $bundle) { + $this->fieldContext['entity type'] = $entity_type; + $this->fieldContext['name'] = $name; + $this->fieldContext['bundle'] = $bundle; + } + + /** + * Implements FieldItemListInterface::getFieldContext(). + */ + public function getFieldContext() { + return $this->fieldContext; + } + + /** + * Implements FieldItemList::getInstance(). + */ + public function getInstance() { + if (!isset($this->fieldContext)) { + throw new MissingContextException('Unable to get the field instance without field context.'); + } + return field_info_instance($this->fieldContext['entity type'], $this->fieldContext['name'], $this->fieldContext['bundle']); + } +} diff --git a/core/modules/field/lib/Drupal/field/FieldItemInterface.php b/core/modules/field/lib/Drupal/field/FieldItemInterface.php new file mode 100644 index 0000000..b6a126c --- /dev/null +++ b/core/modules/field/lib/Drupal/field/FieldItemInterface.php @@ -0,0 +1,45 @@ +setFieldContext($context['parent']->entityType(), $context['name'], $context['parent']->bundle()); + } + } + + /** + * Implements FieldItemListInterface::setFieldContext(). + */ + public function setFieldContext($entity_type, $name, $bundle) { + $this->fieldContext['entity type'] = $entity_type; + $this->fieldContext['name'] = $name; + $this->fieldContext['bundle'] = $bundle; + } + + /** + * Implements FieldItemListInterface::getFieldContext(). + */ + public function getFieldContext() { + return $this->fieldContext; + } + + /** + * Implements FieldItemList::getInstance(). + */ + public function getInstance() { + if (!isset($this->fieldContext)) { + throw new MissingContextException('Unable to get the field instance without field context.'); + } + return field_info_instance($this->fieldContext['entity type'], $this->fieldContext['name'], $this->fieldContext['bundle']); + } +} diff --git a/core/modules/field/lib/Drupal/field/FieldItemListInterface.php b/core/modules/field/lib/Drupal/field/FieldItemListInterface.php new file mode 100644 index 0000000..392bc56 --- /dev/null +++ b/core/modules/field/lib/Drupal/field/FieldItemListInterface.php @@ -0,0 +1,50 @@ + 'string', + 'label' => t('Text value'), + ); + self::$propertyDefinitions['format'] = array( + 'type' => 'string', + 'label' => t('Text format'), + ); + self::$propertyDefinitions['processed'] = array( + 'type' => 'string', + 'label' => t('Processed text'), + 'description' => t('The text value with the text format applied.'), + 'computed' => TRUE, + 'class' => '\Drupal\text\FieldTextProcessed', + 'settings' => array( + 'text source' => 'value', + ), + ); + } + return self::$propertyDefinitions; + } +} + diff --git a/core/modules/field/modules/text/lib/Drupal/text/FieldTextProcessed.php b/core/modules/field/modules/text/lib/Drupal/text/FieldTextProcessed.php new file mode 100644 index 0000000..425f266 --- /dev/null +++ b/core/modules/field/modules/text/lib/Drupal/text/FieldTextProcessed.php @@ -0,0 +1,79 @@ +definition = $definition; + + if (!isset($context['parent'])) { + throw new InvalidArgumentException('Computed properties require context for computation.'); + } + if (!isset($definition['settings']['text source'])) { + throw new InvalidArgumentException("The definition's 'source' key has to specify the name of the text property to be processed."); + } + + $this->text = $context['parent']->get($definition['settings']['text source']); + $this->format = $context['parent']->get('format'); + } + + /** + * Implements WrapperInterface::getValue(). + */ + public function getValue($langcode = NULL) { + // @todo: Determine a way to get the field $instance here. + // Either implement per-bundle property definition overrides or pass on + // entity-context (entity type, bundle, property name). For now, we assume + // text processing is enabled if a format is given. + + if ($this->format->value) { + return check_markup($this->text->value, $this->format->value, $langcode); + } + else { + // If no format is available, still make sure to sanitize the text. + return check_plain($this->text->value); + } + } + + /** + * Implements WrapperInterface::setValue(). + */ + public function setValue($value) { + if (isset($value)) { + throw new ReadOnlyException('Unable to set a computed property.'); + } + } +} diff --git a/core/modules/field/modules/text/lib/Drupal/text/FieldTextWithSummaryItem.php b/core/modules/field/modules/text/lib/Drupal/text/FieldTextWithSummaryItem.php new file mode 100644 index 0000000..1fda540 --- /dev/null +++ b/core/modules/field/modules/text/lib/Drupal/text/FieldTextWithSummaryItem.php @@ -0,0 +1,50 @@ + 'string', + 'label' => t('Summary text value'), + ); + self::$propertyDefinitions['summary_processed'] = array( + 'type' => 'string', + 'label' => t('Processed summary text'), + 'description' => t('The summary text value with the text format applied.'), + 'computed' => TRUE, + 'class' => '\Drupal\text\FieldTextProcessed', + 'settings' => array( + 'text source' => 'summary', + ), + ); + } + return self::$propertyDefinitions; + } +} + diff --git a/core/modules/field/modules/text/text.module b/core/modules/field/modules/text/text.module index dcf4d1e..0046691 100644 --- a/core/modules/field/modules/text/text.module +++ b/core/modules/field/modules/text/text.module @@ -38,6 +38,7 @@ function text_field_info() { 'instance_settings' => array('text_processing' => 0), 'default_widget' => 'text_textfield', 'default_formatter' => 'text_default', + 'property class' => '\Drupal\text\FieldTextItem', ), 'text_long' => array( 'label' => t('Long text'), @@ -45,6 +46,7 @@ function text_field_info() { 'instance_settings' => array('text_processing' => 0), 'default_widget' => 'text_textarea', 'default_formatter' => 'text_default', + 'property class' => '\Drupal\text\FieldTextItem', ), 'text_with_summary' => array( 'label' => t('Long text and summary'), @@ -52,6 +54,7 @@ function text_field_info() { 'instance_settings' => array('text_processing' => 1, 'display_summary' => 0), 'default_widget' => 'text_textarea_with_summary', 'default_formatter' => 'text_default', + 'property class' => '\Drupal\text\FieldTextWithSummaryItem', ), ); } diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php index 10501d0..53ef8ec 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php @@ -1998,6 +1998,26 @@ abstract class WebTestBase extends TestBase { } /** + * Wraps a data value into a typed data wrapper and executes some basic + * assertions. + * + * @see drupal_wrap_data(). + */ + protected function drupalWrapData($definition, $value = NULL, $context = array()) { + // Save the type that was passed in so we can compare with it later. + $type = $definition['type']; + // Construct the wrapper. + $wrapper = drupal_wrap_data($definition, $value, $context); + // Assert the definition of the wrapper. + $this->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 0ea066b..066f9fd 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; @@ -2003,6 +2004,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