diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index f6fb6d6..fd588c2 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -223,6 +223,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; @@ -2476,6 +2484,19 @@ function state() { } /** + * Returns the typed data manager service. + * + * Use the typed data manager service for creating typed data objects. + * + * @see Drupal\Core\TypedData\TypedDataManager::create() + * + * @return Drupal\Core\TypedData\TypedDataManager + */ +function typed_data() { + return drupal_container()->get('typed_data'); +} + +/** * Returns the test prefix if this is an internal request from SimpleTest. * * @return diff --git a/core/includes/entity.api.php b/core/includes/entity.api.php index 5afd67c..102fa70 100644 --- a/core/includes/entity.api.php +++ b/core/includes/entity.api.php @@ -466,3 +466,68 @@ function hook_entity_view_mode_alter(&$view_mode, Drupal\Core\Entity\EntityInter $view_mode = 'my_custom_view_mode'; } } + +/** + * Define custom entity properties. + * + * @param string $entity_type + * The entity type for which to define entity properties. + * + * @return array + * An array of property information having the following optional entries: + * - definitions: An array of property definitions to add all entities of this + * type, keyed by property name. See + * Drupal\Core\TypedData\TypedDataManager::create() for a list of supported + * keys in property definitions. + * - optional: An array of property definitions for optional properties keyed + * by property name. Optional properties are properties that only exist for + * certain bundles of the entity type. + * - bundle map: An array keyed by bundle name containing the names of + * optional properties that entities of this bundle have. + * + * @see Drupal\Core\TypedData\TypedDataManager::create() + * @see hook_entity_field_info_alter() + * @see Drupal\Core\Entity\StorageControllerInterface::getPropertyDefinitions() + */ +function hook_entity_field_info($entity_type) { + if (mymodule_uses_entity_type($entity_type)) { + $info = array(); + $info['definitions']['mymodule_text'] = array( + 'type' => '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_field_info(). + * @param string $entity_type + * The entity type for which entity properties are defined. + * + * @see hook_entity_field_info() + */ +function hook_entity_field_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/lib/Drupal/Core/Config/Entity/ConfigStorageController.php b/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php index 36abe68..b5e5738 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php @@ -347,6 +347,13 @@ class ConfigStorageController implements EntityStorageControllerInterface { } /** + * Implements Drupal\Core\Entity\EntityStorageControllerInterface::getPropertyDefinitions(). + */ + public function getPropertyDefinitions(array $constraints) { + return array(); + } + + /** * Invokes a hook on behalf of the entity. * * @param $hook diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index 4ad6726..4dca804 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -52,6 +52,7 @@ class CoreBundle extends Bundle ->setFactoryClass('Drupal\Core\Database\Database') ->setFactoryMethod('getConnection') ->addArgument('slave'); + $container->register('typed_data', 'Drupal\Core\TypedData\TypedDataManager'); // @todo Replace below lines with the commented out block below it when it's // performant to do so: http://drupal.org/node/1706064. diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php index db3d793..15cf267 100644 --- a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php +++ b/core/lib/Drupal/Core/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_field_info() + */ + protected $entityFieldInfo; + + /** * Additional arguments to pass to hook_TYPE_load(). * * Set before calling Drupal\Core\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,57 @@ class DatabaseStorageController implements EntityStorageControllerInterface { // Invoke the respective entity-level hook. module_invoke_all('entity_' . $hook, $entity, $this->entityType); } + + /** + * Implements Drupal\Core\Entity\EntityStorageControllerInterface::getFieldDefinitions(). + */ + public function getFieldDefinitions(array $constraints) { + // @todo: Add caching for $this->propertyInfo. + if (!isset($this->entityFieldInfo)) { + $this->entityFieldInfo = array( + 'definitions' => $this->baseFieldDefinitions(), + // 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->entityFieldInfo = array_merge_recursive($this->entityFieldInfo, $result); + $result = module_invoke_all('entity_field_info', $this->entityType); + $this->entityFieldInfo = array_merge_recursive($this->entityFieldInfo, $result); + + $hooks = array('entity_field_info', $this->entityType . '_property_info'); + drupal_alter($hooks, $this->entityFieldInfo, $this->entityType); + + // Enforce fields to be multiple by default. + foreach ($this->entityFieldInfo['definitions'] as &$definition) { + $definition['list'] = TRUE; + } + foreach ($this->entityFieldInfo['optional'] as &$definition) { + $definition['list'] = TRUE; + } + } + + $definitions = $this->entityFieldInfo['definitions']; + + // Add in per-bundle properties. + // @todo: Should this be statically cached as well? + if (!empty($constraints['bundle']) && isset($this->entityFieldInfo['bundle map'][$constraints['bundle']])) { + $definitions += array_intersect_key($this->entityFieldInfo['optional'], array_flip($this->entityFieldInfo['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 baseFieldDefinitions() { + return array(); + } } diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php b/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php new file mode 100644 index 0000000..007d6d3 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php @@ -0,0 +1,242 @@ +bundleKey = !empty($this->entityInfo['entity keys']['bundle']) ? $this->entityInfo['entity keys']['bundle'] : FALSE; + $this->entityClass = $this->entityInfo['entity class']; + + // Work-a-round to let load() get stdClass storage records without having to + // override it. We map storage records to entities in + // DatabaseStorageControllerNG:: mapFromStorageRecords(). + // @todo: Remove this once this is moved in the main controller. + unset($this->entityInfo['entity class']); + } + + /** + * Overrides DatabaseStorageController::create(). + * + * @param array $values + * An array of values to set, keyed by field name. The value has to be + * the plain value of an entity field, i.e. an array of field items. + * If no numerically indexed array is given, the value will be set for the + * first field item. For example, 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 + * If the 'name' field is a defined as 'string_item' which supports + * setting by string value, it's also possible to just pass the name string: + * @code + * $values = array('name' => 'the name'); + * @endcode + * + * @return Drupal\Core\Entity\EntityInterface + * A new entity object. + */ + public function create(array $values) { + $entity = new $this->entityClass(array(), $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) { + $entity->$name = $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 DatabaseStorageController::attachLoad(). + * + * Added mapping from storage records to entities. + */ + protected function attachLoad(&$queried_entities, $load_revision = FALSE) { + // Now map the record values to the according entity properties and + // activate compatibility mode. + $queried_entities = $this->mapFromStorageRecords($queried_entities); + + // Attach fields. + if ($this->entityInfo['fieldable']) { + if ($load_revision) { + field_attach_load_revision($this->entityType, $queried_entities); + } + else { + field_attach_load($this->entityType, $queried_entities); + } + } + + // Loading is finished, so disable compatibility mode now. + foreach ($queried_entities as $entity) { + $entity->setCompatibilityMode(FALSE); + } + + // Call hook_entity_load(). + foreach (module_implements('entity_load') as $module) { + $function = $module . '_entity_load'; + $function($queried_entities, $this->entityType); + } + // Call hook_TYPE_load(). The first argument for hook_TYPE_load() are + // always the queried entities, followed by additional arguments set in + // $this->hookLoadArguments. + $args = array_merge(array($queried_entities), $this->hookLoadArguments); + foreach (module_implements($this->entityType . '_load') as $module) { + call_user_func_array($module . '_' . $this->entityType . '_load', $args); + } + } + + /** + * 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); + + foreach ($record as $name => $value) { + $entity->{$name}[LANGUAGE_DEFAULT][0]['value'] = $value; + } + $records[$id] = $entity; + } + return $records; + } + + /** + * 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()); + } + + $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 Field API, move + // this after insert/update hooks. + $entity->updateOriginalValues(); + + if (!$entity->isNew()) { + $return = drupal_write_record($this->entityInfo['base table'], $record, $this->idKey); + $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->{$this->idKey}; + $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); + } + + // 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(); + foreach ($this->entityInfo['schema_fields_sql']['base table'] as $name) { + $record->$name = $entity->$name->value; + } + return $record; + } +} diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php index fb75ee2..4384e1f 100644 --- a/core/lib/Drupal/Core/Entity/Entity.php +++ b/core/lib/Drupal/Core/Entity/Entity.php @@ -9,6 +9,7 @@ namespace Drupal\Core\Entity; use Drupal\Component\Uuid\Uuid; use Drupal\Core\Language\Language; +use IteratorAggregate; /** * Defines a base entity class. @@ -18,7 +19,7 @@ use Drupal\Core\Language\Language; * This class can be used as-is by simple entity types. Entity types requiring * special handling can extend the class. */ -class Entity implements EntityInterface { +class Entity implements IteratorAggregate, EntityInterface { /** * The language code of the entity's default language. @@ -146,14 +147,109 @@ 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 field API. + return isset($this->{$property_name}) ? $this->{$property_name} : NULL; + } + + /** + * Implements ComplexDataInterface::set(). + */ + public function set($property_name, $value) { + // @todo: Replace by EntityNG implementation once all entity types have been + // converted to use the entity field API. + $this->{$property_name} = $value; + } + + /** + * Implements ComplexDataInterface::getProperties(). + */ + public function getProperties($include_computed = FALSE) { + // @todo: Replace by EntityNG implementation once all entity types have been + // converted to use the entity field API. + } + + /** + * Implements ComplexDataInterface::getPropertyValues(). + */ + public function getPropertyValues() { + // @todo: Replace by EntityNG implementation once all entity types have been + // converted to use the entity field API. + } + + /** + * Implements ComplexDataInterface::setPropertyValues(). + */ + public function setPropertyValues($values) { + // @todo: Replace by EntityNG implementation once all entity types have been + // converted to use the entity field API. + } + + /** + * Implements ComplexDataInterface::getPropertyDefinition(). + */ + public function getPropertyDefinition($name) { + // @todo: Replace by EntityNG implementation once all entity types have been + // converted to use the entity field API. + } + + /** + * Implements ComplexDataInterface::getPropertyDefinitions(). + */ + public function getPropertyDefinitions() { + // @todo: Replace by EntityNG implementation once all entity types have been + // converted to use the entity field API. + } + + /** + * Implements ComplexDataInterface::isEmpty(). + */ + public function isEmpty() { + // @todo: Replace by EntityNG implementation once all entity types have been + // converted to use the entity field API. + } + + /** + * Implements ComplexDataInterface::getIterator(). + */ + public function getIterator() { + // @todo: Replace by EntityNG implementation once all entity types have been + // converted to use the entity field API. + } + + /** + * Implements AccessibleInterface::access(). + */ + public function access(\Drupal\user\User $account = NULL) { + // TODO: Implement access() method. + } + + /** + * Implements TranslatableInterface::language(). */ public function language() { + // @todo: Replace by EntityNG implementation once all entity types have been + // converted to use the entity field API. return !empty($this->langcode) ? language_load($this->langcode) : new Language(array('langcode' => LANGUAGE_NOT_SPECIFIED)); } /** - * Implements EntityInterface::translations(). + * Implements TranslatableInterface::getTranslation(). + */ + public function getTranslation($langcode, $strict = TRUE) { + // @todo: Replace by EntityNG implementation once all entity types have been + // converted to use the entity field API. + } + + /** + * Returns the languages the entity is translated to. + * + * @todo: Remove once all entity types implement the entity field API. This + * is deprecated by + * TranslatableInterface::getTranslationLanguages(). */ public function translations() { $languages = array(); @@ -177,108 +273,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); - // Prevent getFieldLangcode() from throwing an exception in case a - // $langcode has been passed and it is invalid for the field. - $langcode = $this->getFieldLangcode($field, $langcode, FALSE); - 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 TranslatableInterface::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); - // Throws an exception if the $langcode is invalid. - $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 for accessing a field value. - * - * The effective language code to be used for a field varies: - * - If the entity is language-specific and the requested field is - * translatable, the entity's language code should be used to access the - * field value when no language is explicitly provided. - * - If the entity is not language-specific, LANGUAGE_NOT_SPECIFIED should be - * used to access all field values. - * - If a field's values are non-translatable (shared among all language - * versions of an entity), LANGUAGE_NOT_SPECIFIED should be used to access - * them. - * - * There cannot be valid field values if a field is not translatable and the - * requested langcode is not LANGUAGE_NOT_SPECIFIED. Therefore, this function - * throws an exception in that case (or returns NULL when $strict is FALSE). - * - * @param string $field - * Field the language code is being determined for. - * @param string|null $langcode - * (optional) The language code attempting to be applied to the field. - * Defaults to the entity language. - * @param bool $strict - * (optional) When $strict is TRUE, an exception is thrown if the field is - * not translatable and the langcode is not LANGUAGE_NOT_SPECIFIED. When - * $strict is FALSE, NULL is returned and no exception is thrown. For - * example, EntityInterface::set() passes TRUE, since it must not set field - * values for invalid langcodes. EntityInterface::get() passes FALSE to - * determine whether any field values exist for a specific langcode. - * Defaults to TRUE. - * - * @return string|null - * The langcode if appropriate, LANGUAGE_NOT_SPECIFIED for non-translatable - * fields, or NULL when an invalid langcode was used in non-strict mode. - * - * @throws \InvalidArgumentException - * Thrown in case a $langcode other than LANGUAGE_NOT_SPECIFIED is passed - * for a non-translatable field and $strict is TRUE. - */ - protected function getFieldLangcode($field, $langcode = NULL, $strict = TRUE) { - // 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 { - // The field is not translatable, but the caller requested a specific - // langcode that does not exist. - if (isset($langcode) && $langcode !== LANGUAGE_NOT_SPECIFIED) { - if ($strict) { - throw new \InvalidArgumentException(format_string('Unable to resolve @langcode for non-translatable field @field_name. Use langcode LANGUAGE_NOT_SPECIFIED instead.', array( - '@field_name' => $field['field_name'], - '@langcode' => $langcode, - ))); - } - else { - return NULL; - } - } - // The field is not translatable and no $langcode was specified. - return 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 field API. } /** @@ -337,5 +336,4 @@ class Entity implements EntityInterface { } return $return; } - } diff --git a/core/lib/Drupal/Core/Entity/EntityFormController.php b/core/lib/Drupal/Core/Entity/EntityFormController.php index 2828d5f..c4ebe28 100644 --- a/core/lib/Drupal/Core/Entity/EntityFormController.php +++ b/core/lib/Drupal/Core/Entity/EntityFormController.php @@ -71,7 +71,7 @@ class EntityFormController implements EntityFormControllerInterface { * @see Drupal\Core\Entity\EntityFormController::build() */ public function form(array $form, array &$form_state, EntityInterface $entity) { - // @todo Exploit the Property API to generate the default widgets for the + // @todo Exploit the Field API to generate the default widgets for the // entity properties. $info = $entity->entityInfo(); if (!empty($info['fieldable'])) { @@ -145,7 +145,7 @@ class EntityFormController implements EntityFormControllerInterface { * Implements Drupal\Core\Entity\EntityFormControllerInterface::validate(). */ public function validate(array $form, array &$form_state) { - // @todo Exploit the Property API to validate the values submitted for the + // @todo Exploit the Field API to validate the values submitted for the // entity properties. $entity = $this->buildEntity($form, $form_state); $info = $entity->entityInfo(); @@ -236,7 +236,7 @@ class EntityFormController implements EntityFormControllerInterface { public function buildEntity(array $form, array &$form_state) { $entity = clone $this->getEntity($form_state); // @todo Move entity_form_submit_build_entity() here. - // @todo Exploit the Property API to process the submitted entity property. + // @todo Exploit the Field API to process the submitted entity field. entity_form_submit_build_entity($entity->entityType(), $entity, $form, $form_state); return $entity; } diff --git a/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php b/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php new file mode 100644 index 0000000..d06614d --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php @@ -0,0 +1,86 @@ +entityInfo(); + if (!empty($info['fieldable'])) { + $entity->setCompatibilityMode(TRUE); + field_attach_form($entity->entityType(), $entity, $form, $form_state, $this->getFormLangcode($form_state)); + $entity->setCompatibilityMode(FALSE); + } + return $form; + } + + /** + * Overrides EntityFormController::validate(). + */ + public function validate(array $form, array &$form_state) { + // @todo Exploit the Field API to validate the values submitted for the + // entity properties. + $entity = $this->buildEntity($form, $form_state); + $info = $entity->entityInfo(); + + if (!empty($info['fieldable'])) { + $entity->setCompatibilityMode(TRUE); + field_attach_form_validate($entity->entityType(), $entity, $form, $form_state); + $entity->setCompatibilityMode(FALSE); + } + + // @todo Remove this. + // Execute legacy global validation handlers. + unset($form_state['validate_handlers']); + form_execute_handlers('validate', $form, $form_state); + } + + /** + * Overrides EntityFormController::buildEntity(). + */ + public function buildEntity(array $form, array &$form_state) { + $entity = clone $this->getEntity($form_state); + $entity_type = $entity->entityType(); + $info = entity_get_info($entity_type); + // @todo Exploit the Field API to process the submitted entity field. + + // Copy top-level form values that are not for fields to entity properties, + // without changing existing entity properties that are not being edited by + // this form. Copying field values must be done using field_attach_submit(). + $values_excluding_fields = $info['fieldable'] ? array_diff_key($form_state['values'], field_info_instances($entity_type, $entity->bundle())) : $form_state['values']; + foreach ($values_excluding_fields as $key => $value) { + $entity->$key = $value; + } + + // Invoke all specified builders for copying form values to entity properties. + if (isset($form['#entity_builders'])) { + foreach ($form['#entity_builders'] as $function) { + $function($entity_type, $entity, $form, $form_state); + } + } + + // Copy field values to the entity. + if ($info['fieldable']) { + $entity->setCompatibilityMode(TRUE); + field_attach_submit($entity_type, $entity, $form, $form_state); + $entity->setCompatibilityMode(FALSE); + } + return $entity; + } +} diff --git a/core/lib/Drupal/Core/Entity/EntityInterface.php b/core/lib/Drupal/Core/Entity/EntityInterface.php index d8b6eae..a6d21bb 100644 --- a/core/lib/Drupal/Core/Entity/EntityInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityInterface.php @@ -7,10 +7,17 @@ namespace Drupal\Core\Entity; +use Drupal\Core\TypedData\AccessibleInterface; +use Drupal\Core\TypedData\ComplexDataInterface; +use Drupal\Core\TypedData\TranslatableInterface; + /** * Defines a common interface for all entity objects. + * + * When implementing this interface which extends Traversable, make sure to list + * IteratorAggregate or Iterator before this interface in the implements clause. */ -interface EntityInterface { +interface EntityInterface extends ComplexDataInterface, AccessibleInterface, TranslatableInterface { /** * Constructs a new entity object. @@ -111,60 +118,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\Core\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\Core\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\Core\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\Core\Entity\EntityInterface::language() - */ - public function set($property_name, $value, $langcode = NULL); - - /** * Saves an entity permanently. * * @return diff --git a/core/lib/Drupal/Core/Entity/EntityNG.php b/core/lib/Drupal/Core/Entity/EntityNG.php new file mode 100644 index 0000000..8ed4faa --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityNG.php @@ -0,0 +1,424 @@ + array(LANGUAGE_DEFAULT => array(0 => array('value' => LANGUAGE_NOT_SPECIFIED))), + ); + + /** + * The array of fields, each being an instance of FieldInterface. + * + * @var array + */ + protected $fields = array(); + + /** + * Whether the entity is in pre-Entity Field API compatibility mode. + * + * If set to TRUE, field values are written directly to $this->values, thus + * must be plain property values keyed by language code. This must be enabled + * when calling legacy 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 ComplexDataInterface::get(). + */ + public function get($property_name) { + // Values in default language are always stored using the LANGUAGE_DEFAULT + // constant. + if (!isset($this->fields[$property_name][LANGUAGE_DEFAULT])) { + return $this->getTranslatedField($property_name, LANGUAGE_DEFAULT); + } + return $this->fields[$property_name][LANGUAGE_DEFAULT]; + } + + /** + * Gets a translated field. + * + * @return \Drupal\Core\Entity\Field\FieldInterface + */ + protected function getTranslatedField($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->fields[$property_name][$langcode])) { + $definition = $this->getPropertyDefinition($property_name); + if (!$definition) { + throw new InvalidArgumentException('Field ' . check_plain($property_name) . ' is unknown.'); + } + // Non-translatable properties always use default language. + if ($langcode != LANGUAGE_DEFAULT && empty($definition['translatable'])) { + $this->fields[$property_name][$langcode] = $this->getTranslatedField($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->fields[$property_name][$langcode] = typed_data()->create($definition, $value, $context); + } + } + return $this->fields[$property_name][$langcode]; + } + + /** + * Implements ComplexDataInterface::set(). + */ + public function set($property_name, $value) { + $this->get($property_name)->setValue($value); + } + + /** + * Implements ComplexDataInterface::getProperties(). + */ + public function getProperties($include_computed = FALSE) { + $properties = array(); + foreach ($this->getPropertyDefinitions() as $name => $definition) { + if ($include_computed || empty($definition['computed'])) { + $properties[$name] = $this->get($name); + } + } + return $properties; + } + + /** + * Implements IteratorAggregate::getIterator(). + */ + public function getIterator() { + return new ArrayIterator($this->getProperties()); + } + + /** + * Implements ComplexDataInterface::getPropertyDefinition(). + */ + public function getPropertyDefinition($name) { + // First try getting property definitions which apply to all entities of + // this type. Then if this fails add in definitions of optional properties + // as well. That way we can use property definitions of base properties + // when determining the optional properties of an entity. + $definitions = entity_get_controller($this->entityType)->getFieldDefinitions(array()); + + if (isset($definitions[$name])) { + return $definitions[$name]; + } + // Add in optional properties if any. + if ($definitions = $this->getPropertyDefinitions()) { + return isset($definitions[$name]) ? $definitions[$name] : FALSE; + } + } + + /** + * Implements ComplexDataInterface::getPropertyDefinitions(). + */ + public function getPropertyDefinitions() { + return entity_get_controller($this->entityType)->getFieldDefinitions(array( + 'entity type' => $this->entityType, + 'bundle' => $this->bundle(), + )); + } + + /** + * Implements ComplexDataInterface::getPropertyValues(). + */ + public function getPropertyValues() { + $values = array(); + foreach ($this->getProperties() as $name => $property) { + $values[$name] = $property->getValue(); + } + return $values; + } + + /** + * Implements ComplexDataInterface::setPropertyValues(). + */ + public function setPropertyValues($values) { + foreach ($values as $name => $value) { + $this->get($name)->setValue($value); + } + } + + /** + * Implements ComplexDataInterface::isEmpty(). + */ + public function isEmpty() { + if (!$this->isNew()) { + return FALSE; + } + foreach ($this->getProperties() as $property) { + if ($property->getValue() !== NULL) { + return FALSE; + } + } + return TRUE; + } + + /** + * Implements TranslatableInterface::language(). + */ + public function language() { + return $this->get('langcode')->language; + } + + /** + * Implements TranslatableInterface::getTranslation(). + * + * @return \Drupal\Core\Entity\Field\Type\EntityTranslation + */ + public function getTranslation($langcode, $strict = TRUE) { + // If the default language is LANGUAGE_NOT_SPECIFIED, the entity is not + // translatable, so we use LANGUAGE_DEFAULT. + if ($langcode == LANGUAGE_DEFAULT || in_array($this->language()->langcode, array(LANGUAGE_NOT_SPECIFIED, $langcode))) { + // 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) { + // Load only translatable properties in strict mode. + if (!empty($definition['translatable']) || !$strict) { + $properties[$name] = $this->getTranslatedField($name, $langcode); + } + } + $translation_definition = array( + 'type' => 'entity_translation', + 'constraints' => array( + 'entity type' => $this->entityType(), + 'bundle' => $this->bundle(), + ), + ); + $translation = typed_data()->create($translation_definition, $properties, array( + 'parent' => $this, + 'name' => $langcode, + )); + $translation->setStrictMode($strict); + return $translation; + } + + /** + * Implements TranslatableInterface::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->fields[$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; + } + + /** + * Overrides Entity::translations(). + * + * @todo: Remove once Entity::translations() gets removed. + */ + public function translations() { + return $this->getTranslationLanguages(FALSE); + } + + /** + * 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; + if ($enabled) { + $this->updateOriginalValues(); + $this->fields = array(); + } + } + + /** + * 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->fields 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->fields[$name][LANGUAGE_DEFAULT])) { + return $this->fields[$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) { + // Support setting values via property objects. + if ($value instanceof TypedDataInterface) { + $value = $value->getValue(); + } + + if ($this->compatibilityMode) { + $this->values[$name] = $value; + } + elseif (isset($this->fields[$name][LANGUAGE_DEFAULT])) { + $this->fields[$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]); + } + elseif ($this->getPropertyDefinition($name)) { + return (bool) count($this->get($name)); + } + } + + /** + * Magic method. + */ + public function __unset($name) { + if ($this->compatibilityMode) { + unset($this->values[$name]); + } + elseif ($this->getPropertyDefinition($name)) { + $this->get($name)->setValue(array()); + } + } + + /** + * 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->fields as $name => $properties) { + foreach ($properties as $langcode => $property) { + $this->fields[$name][$langcode] = clone $property; + if ($property instanceof ContextAwareInterface) { + $this->fields[$name][$langcode]->setParent($this); + } + } + } + } +} diff --git a/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php b/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php index 3bc2328..b0267e5 100644 --- a/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php @@ -109,4 +109,37 @@ interface EntityStorageControllerInterface { */ public function save(EntityInterface $entity); + /** + * Gets an array of entity field definitions. + * + * If a 'bundle' key is present in the given entity definition, fields + * specific to this bundle are included. + * Entity fields are always multi-valued, so 'list' is TRUE for each + * returned field 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 field definitions of entity fields, keyed by field + * name. In addition to the typed data definition keys as described at + * typed_data()->create() the follow keys are supported: + * - queryable: Whether the field is queryable via EntityFieldQuery. + * Defaults to TRUE if 'computed' is FALSE or not set, to FALSE otherwise. + * - translatable: Whether the field is translatable. Defaults to FALSE. + * - configurable: A boolean indicating whether the field is configurable + * via field.module. Defaults to FALSE. + * + * @see Drupal\Core\TypedData\TypedDataManager::create() + * @see typed_data() + */ + public function getFieldDefinitions(array $constraints); } diff --git a/core/lib/Drupal/Core/Entity/Field/FieldInterface.php b/core/lib/Drupal/Core/Entity/Field/FieldInterface.php new file mode 100644 index 0000000..760f07a --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Field/FieldInterface.php @@ -0,0 +1,81 @@ + + */ + protected $properties = array(); + + /** + * Implements TypedDataInterface::__construct(). + */ + public function __construct(array $definition) { + $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] = typed_data()->create($definition, NULL, $context); + } + else { + $step2[$name] = $definition; + } + } + + foreach ($step2 as $name => $definition) { + $context = array('name' => $name, 'parent' => $this); + $this->properties[$name] = typed_data()->create($definition, NULL, $context); + } + } + + /** + * Implements TypedDataInterface::getValue(). + */ + public function getValue() { + $values = array(); + foreach ($this->getProperties() as $name => $property) { + $values[$name] = $property->getValue(); + } + return $values; + } + + /** + * Implements TypedDataInterface::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); + } + + 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 TypedDataInterface::getString(). + */ + public function getString() { + $strings = array(); + foreach ($this->getProperties() as $property) { + $strings[] = $property->getString(); + } + return implode(', ', array_filter($strings)); + } + + /** + * Implements TypedDataInterface::validate(). + */ + public function validate() { + // @todo implement + } + + /** + * Implements ComplexDataInterface::get(). + */ + public function get($property_name) { + if (!isset($this->properties[$property_name])) { + throw new InvalidArgumentException('Field ' . check_plain($property_name) . ' is unknown.'); + } + return $this->properties[$property_name]; + } + + /** + * Implements ComplexDataInterface::set(). + */ + public function set($property_name, $value) { + $this->get($property_name)->setValue($value); + } + + /** + * Implements FieldItemInterface::__get(). + */ + public function __get($name) { + return $this->get($name)->getValue(); + } + + /** + * Implements FieldItemInterface::__set(). + */ + public function __set($name, $value) { + // Support setting values via property objects. + if ($value instanceof TypedDataInterface) { + $value = $value->getValue(); + } + $this->get($name)->setValue($value); + } + + /** + * Implements FieldItemInterface::__isset(). + */ + public function __isset($name) { + return isset($this->properties[$name]) && $this->properties[$name]->getValue() !== NULL; + } + + /** + * Implements FieldItemInterface::__unset(). + */ + public function __unset($name) { + if (isset($this->properties[$name])) { + $this->properties[$name]->setValue(NULL); + } + } + + /** + * Implements ContextAwareInterface::getName(). + */ + public function getName() { + return $this->name; + } + + /** + * Implements ContextAwareInterface::setName(). + */ + public function setName($name) { + $this->name = $name; + } + + /** + * Implements ContextAwareInterface::getParent(). + * + * @return \Drupal\Core\Entity\Field\FieldInterface + */ + public function getParent() { + return $this->parent; + } + + /** + * Implements ContextAwareInterface::setParent(). + */ + public function setParent($parent) { + $this->parent = $parent; + } + + /** + * Implements ComplexDataInterface::getProperties(). + */ + public function getProperties($include_computed = FALSE) { + $properties = array(); + foreach ($this->getPropertyDefinitions() as $name => $definition) { + if ($include_computed || empty($definition['computed'])) { + $properties[$name] = $this->properties[$name]; + } + } + return $properties; + } + + /** + * Implements ComplexDataInterface::getPropertyValues(). + */ + public function getPropertyValues() { + return $this->getValue(); + } + + /** + * Implements ComplexDataInterface::setPropertyValues(). + */ + public function setPropertyValues($values) { + foreach ($values as $name => $value) { + $this->get($name)->setValue($value); + } + } + + /** + * Implements IteratorAggregate::getIterator(). + */ + public function getIterator() { + return new ArrayIterator($this->getProperties()); + } + + /** + * Implements ComplexDataInterface::getPropertyDefinition(). + */ + public function getPropertyDefinition($name) { + $definitions = $this->getPropertyDefinitions(); + return isset($definitions[$name]) ? $definitions[$name] : FALSE; + } + + /** + * Implements ComplexDataInterface::isEmpty(). + */ + public function isEmpty() { + foreach ($this->getProperties() as $property) { + if ($property->getValue() !== NULL) { + return FALSE; + } + } + return TRUE; + } + + /** + * Implements a deep clone. + */ + public function __clone() { + foreach ($this->properties as $name => $property) { + $this->properties[$name] = clone $property; + if ($property instanceof ContextAwareInterface) { + $this->properties[$name]->setParent($this); + } + } + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Entity/Field/FieldItemInterface.php b/core/lib/Drupal/Core/Entity/Field/FieldItemInterface.php new file mode 100644 index 0000000..2b94e7a --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Field/FieldItemInterface.php @@ -0,0 +1,73 @@ + 'boolean', + 'label' => t('Boolean value'), + ); + } + return self::$propertyDefinitions; + } +} diff --git a/core/lib/Drupal/Core/Entity/Field/Type/DateItem.php b/core/lib/Drupal/Core/Entity/Field/Type/DateItem.php new file mode 100644 index 0000000..eb1ab12 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Field/Type/DateItem.php @@ -0,0 +1,39 @@ + 'date', + 'label' => t('Date value'), + ); + } + return self::$propertyDefinitions; + } +} diff --git a/core/lib/Drupal/Core/Entity/Field/Type/EntityReferenceItem.php b/core/lib/Drupal/Core/Entity/Field/Type/EntityReferenceItem.php new file mode 100644 index 0000000..19cd8ec --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Field/Type/EntityReferenceItem.php @@ -0,0 +1,82 @@ +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 FieldItemBase::setValue(). + */ + public function setValue($values) { + // Treat the values as property value of the entity field, 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 field 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/lib/Drupal/Core/Entity/Field/Type/EntityTranslation.php b/core/lib/Drupal/Core/Entity/Field/Type/EntityTranslation.php new file mode 100644 index 0000000..0db4657 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Field/Type/EntityTranslation.php @@ -0,0 +1,246 @@ +strict; + } + + /** + * Sets whether the entity translation acts in strict mode. + * + * @param boolean $strict + * Whether the entity translation acts in strict mode. + * + * @see \Drupal\Core\TypedData\TranslatableInterface::getTranslation() + */ + public function setStrictMode($strict = TRUE) { + $this->strict = $strict; + } + + /** + * Implements ContextAwareInterface::getName(). + */ + public function getName() { + // The name of the translation is the language code. + return $this->langcode; + } + + /** + * Implements ContextAwareInterface::setName(). + */ + public function setName($name) { + // The name of the translation is the language code. + $this->langcode = $name; + } + + /** + * Implements ContextAwareInterface::getParent(). + * + * @return \Drupal\Core\Entity\EntityInterface + */ + public function getParent() { + return $this->parent; + } + + /** + * Implements ContextAwareInterface::setParent(). + */ + public function setParent($parent) { + $this->parent = $parent; + } + /** + * Implements TypedDataInterface::getValue(). + */ + public function getValue() { + // The value of the translation is the array of translated property objects. + return $this->properties; + } + + /** + * Implements TypedDataInterface::setValue(). + */ + public function setValue($values) { + $this->properties = $values; + } + + /** + * Implements TypedDataInterface::getString(). + */ + public function getString() { + $strings = array(); + foreach ($this->getProperties() as $property) { + $strings[] = $property->getString(); + } + return implode(', ', array_filter($strings)); + } + + /** + * Implements TypedDataInterface::get(). + */ + public function get($property_name) { + $definitions = $this->getPropertyDefinitions(); + if (!isset($definitions[$property_name])) { + throw new InvalidArgumentException(format_string('Field @name is unknown or not translatable.', array('@name' => $property_name))); + } + return $this->properties[$property_name]; + } + + /** + * Implements ComplexDataInterface::set(). + */ + public function set($property_name, $value) { + $this->get($property_name)->setValue($value); + } + + /** + * Implements ComplexDataInterface::getProperties(). + */ + public function getProperties($include_computed = FALSE) { + $properties = array(); + foreach ($this->getPropertyDefinitions() as $name => $definition) { + if ($include_computed || empty($definition['computed'])) { + $properties[$name] = $this->get($name); + } + } + return $properties; + } + + /** + * 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 ComplexDataInterface::getPropertyDefinition(). + */ + public function getPropertyDefinition($name) { + $definitions = $this->getPropertyDefinitions(); + return isset($definitions[$name]) ? $definitions[$name] : FALSE; + } + + /** + * Implements ComplexDataInterface::getPropertyDefinitions(). + */ + public function getPropertyDefinitions() { + $definitions = array(); + foreach ($this->parent->getPropertyDefinitions() as $name => $definition) { + if (!empty($definition['translatable']) || !$this->strict) { + $definitions[$name] = $definition; + } + } + return $definitions; + } + + /** + * Implements ComplexDataInterface::getPropertyValues(). + */ + public function getPropertyValues() { + return $this->getValue(); + } + + /** + * Implements ComplexDataInterface::setPropertyValues(). + */ + public function setPropertyValues($values) { + foreach ($values as $name => $value) { + $this->get($name)->setValue($value); + } + } + + /** + * Implements ComplexDataInterface::isEmpty(). + */ + public function isEmpty() { + foreach ($this->getProperties() as $property) { + if ($property->getValue() !== NULL) { + return FALSE; + } + } + return TRUE; + } + + /** + * Implements AccessibleInterface::access(). + */ + public function access(\Drupal\user\User $account = NULL) { + // @todo implement + } + + /** + * Implements TypedDataInterface::validate(). + */ + public function validate($value = NULL) { + // @todo implement + } +} diff --git a/core/lib/Drupal/Core/Entity/Field/Type/EntityWrapper.php b/core/lib/Drupal/Core/Entity/Field/Type/EntityWrapper.php new file mode 100644 index 0000000..97449e7 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Field/Type/EntityWrapper.php @@ -0,0 +1,233 @@ +definition = $definition + array('constraints' => array()); + $this->entityType = isset($this->definition['constraints']['entity type']) ? $this->definition['constraints']['entity type'] : NULL; + } + + /** + * Implements TypedDataInterface::getValue(). + */ + public function getValue() { + $source = $this->getIdSource(); + $id = $source ? $source->getValue() : $this->id; + return $id ? entity_load($this->entityType, $id) : NULL; + } + + /** + * Helper to get the typed data object holding the source entity ID. + * + * @return \Drupal\Core\TypedData\TypedDataInterface|FALSE + */ + protected function getIdSource() { + return !empty($this->definition['settings']['id source']) ? $this->parent->get($this->definition['settings']['id source']) : FALSE; + } + + /** + * Implements TypedDataInterface::setValue(). + * + * Both the entity ID and the entity object may be passed as value. + */ + public function setValue($value) { + // Support passing in the entity object. + if ($value instanceof EntityInterface) { + $this->entityType = $value->entityType(); + $value = $value->id(); + } + elseif (isset($value) && !(is_scalar($value) && !empty($this->definition['constraints']['entity type']))) { + throw new InvalidArgumentException('Value is no valid entity.'); + } + + $source = $this->getIdSource(); + if ($source) { + $source->setValue($value); + } + else { + $this->id = $value; + } + } + + /** + * Implements TypedDataInterface::getString(). + */ + public function getString() { + $entity = $this->getValue(); + return $entity ? $entity->label() : ''; + } + + /** + * Implements TypedDataInterface::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 ComplexDataInterface::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 ComplexDataInterface::set(). + */ + public function set($property_name, $value) { + $this->get($property_name)->setValue($value); + } + + /** + * Implements ContextAwareInterface::getName(). + */ + public function getName() { + return $this->name; + } + + /** + * Implements ContextAwareInterface::setName(). + */ + public function setName($name) { + $this->name = $name; + } + + /** + * Implements ContextAwareInterface::getParent(). + */ + public function getParent() { + return $this->parent; + } + + /** + * Implements ContextAwareInterface::setParent(). + */ + public function setParent($parent) { + $this->parent = $parent; + } + + /** + * Implements ComplexDataInterface::getProperties(). + */ + public function getProperties($include_computed = FALSE) { + $entity = $this->getValue(); + return $entity ? $entity->getProperties($include_computed) : array(); + } + + /** + * Implements ComplexDataInterface::getPropertyDefinition(). + */ + public function getPropertyDefinition($name) { + $definitions = $this->getPropertyDefinitions(); + return isset($definitions[$name]) ? $definitions[$name] : FALSE; + } + + /** + * Implements ComplexDataInterface::getPropertyDefinitions(). + */ + public function getPropertyDefinitions() { + // @todo: Support getting definitions if multiple bundles are specified. + return entity_get_controller($this->entityType)->getFieldDefinitions($this->definition['constraints']); + } + + /** + * Implements ComplexDataInterface::getPropertyValues(). + */ + public function getPropertyValues() { + $entity = $this->getValue(); + return $entity ? $entity->getPropertyValues() : array(); + } + + /** + * Implements ComplexDataInterface::setPropertyValues(). + */ + public function setPropertyValues($values) { + if ($entity = $this->getValue()) { + $entity->setPropertyValues($values); + } + } + + /** + * Implements ComplexDataInterface::isEmpty(). + */ + public function isEmpty() { + return (bool) $this->getValue(); + } +} diff --git a/core/lib/Drupal/Core/Entity/Field/Type/Field.php b/core/lib/Drupal/Core/Entity/Field/Type/Field.php new file mode 100644 index 0000000..f4b4e5c --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Field/Type/Field.php @@ -0,0 +1,303 @@ +list as $delta => $item) { + $values[$delta] = !$item->isEmpty() ? $item->getValue() : NULL; + } + return $values; + } + + /** + * Implements TypedDataInterface::setValue(). + * + * @param array $values + * An array of values of the field items. + */ + public function setValue($values) { + if (!empty($values)) { + + // Support passing in only the value of the first item. + if (!is_array($values) || !is_numeric(current(array_keys($values)))) { + $values = array(0 => $values); + } + + if (!is_array($values)) { + throw new InvalidArgumentException("An entity field 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 field. + * + * @return string + */ + public function getString() { + $strings = array(); + foreach ($this->list() as $item) { + $strings[] = $item->getString(); + } + return implode(', ', array_filter($strings)); + } + + /** + * Implements TypedDataInterface::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 (!is_numeric($offset)) { + throw new InvalidArgumentException('Unable to get a value with a non-numeric delta in a list.'); + } + // Allow getting not yet existing items as well. + // @todo: Maybe add a public createItem() method in addition? + elseif (!isset($this->list[$offset])) { + $this->list[$offset] = $this->createItem(); + } + return $this->list[$offset]; + } + + /** + * Helper for creating a list item object. + * + * @return \Drupal\Core\TypedData\TypedDataInterface + */ + protected function createItem($value = NULL) { + $context = array('parent' => $this); + return typed_data()->create(array('list' => FALSE) + $this->definition, $value, $context); + } + + /** + * Implements ArrayAccess::offsetSet(). + */ + public function offsetSet($offset, $value) { + if (!isset($offset)) { + // The [] operator has been used so point at a new entry. + $offset = $this->list ? max(array_keys($this->list)) + 1 : 0; + } + if (is_numeric($offset)) { + // Support setting values via typed data objects. + if ($value instanceof TypedDataInterface) { + $value = $value->getValue(); + } + $this->offsetGet($offset)->setValue($value); + } + else { + throw new InvalidArgumentException('Unable to set a value with a non-numeric delta in a list.'); + } + } + + /** + * Implements IteratorAggregate::getIterator(). + */ + public function getIterator() { + return new ArrayIterator($this->list); + } + + /** + * Implements Countable::count(). + */ + public function count() { + return count($this->list); + } + + /** + * Implements ContextAwareInterface::getName(). + */ + public function getName() { + return $this->name; + } + + /** + * Implements ContextAwareInterface::setName(). + */ + public function setName($name) { + $this->name = $name; + } + + /** + * Implements ContextAwareInterface::getParent(). + * + * @return \Drupal\Core\Entity\EntityInterface + */ + public function getParent() { + return $this->parent; + } + + /** + * Implements ContextAwareInterface::setParent(). + */ + public function setParent($parent) { + $this->parent = $parent; + } + + /** + * 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); + } + + /** + * Delegate. + */ + public function __isset($property_name) { + return $this->offsetGet(0)->__isset($property_name); + } + + /** + * Delegate. + */ + public function __unset($property_name) { + return $this->offsetGet(0)->__unset($property_name); + } + + /** + * Implements ListInterface::isEmpty(). + */ + public function isEmpty() { + foreach ($this->list as $item) { + if (!$item->isEmpty()) { + return FALSE; + } + } + return TRUE; + } + + /** + * 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/lib/Drupal/Core/Entity/Field/Type/IntegerItem.php b/core/lib/Drupal/Core/Entity/Field/Type/IntegerItem.php new file mode 100644 index 0000000..1f4b4e6 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Field/Type/IntegerItem.php @@ -0,0 +1,39 @@ + 'integer', + 'label' => t('Integer value'), + ); + } + return self::$propertyDefinitions; + } +} diff --git a/core/lib/Drupal/Core/Entity/Field/Type/LanguageItem.php b/core/lib/Drupal/Core/Entity/Field/Type/LanguageItem.php new file mode 100644 index 0000000..f7b2a91 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Field/Type/LanguageItem.php @@ -0,0 +1,72 @@ + '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 FieldItemBase::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/lib/Drupal/Core/Entity/Field/Type/StringItem.php b/core/lib/Drupal/Core/Entity/Field/Type/StringItem.php new file mode 100644 index 0000000..7c8c57a --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Field/Type/StringItem.php @@ -0,0 +1,39 @@ + 'string', + 'label' => t('Text value'), + ); + } + return self::$propertyDefinitions; + } +} diff --git a/core/lib/Drupal/Core/TypedData/AccessibleInterface.php b/core/lib/Drupal/Core/TypedData/AccessibleInterface.php new file mode 100644 index 0000000..669f1d7 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/AccessibleInterface.php @@ -0,0 +1,26 @@ +handle) && isset($this->uri)) { + $this->handle = fopen($this->uri, 'rb'); + } + return $this->handle; + } + + /** + * Implements TypedDataInterface::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)) { + // Note: For performance reasons we store the given URI and access the + // resource upon request. See Binary::getValue() + $this->uri = $value; + } + else { + throw new InvalidArgumentException("Invalid value for binary data given."); + } + } + + /** + * Implements TypedDataInterface::getString(). + */ + public function getString() { + $contents = ''; + while (!feof($this->getValue())) { + $contents .= fread($this->handle, 8192); + } + return $contents; + } + + /** + * Implements TypedDataInterface::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..5714599 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Type/Boolean.php @@ -0,0 +1,40 @@ +value = isset($value) ? (bool) $value : $value; + } + + /** + * Implements TypedDataInterface::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..07767e9 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Type/Date.php @@ -0,0 +1,62 @@ +value = $value; + } + // Treat integer values as timestamps, even if supplied as PHP string. + elseif ((string) (int) $value === (string) $value) { + $this->value = new DateTime('@' . $value); + } + elseif (is_string($value)) { + $this->value = new DateTime($value); + } + else { + throw new InvalidArgumentException("Invalid date format given."); + } + } + + /** + * Implements TypedDataInterface::getString(). + */ + public function getString() { + return (string) $this->getValue()->format(DateTime::ISO8601); + } + + /** + * Implements TypedDataInterface::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..9fd8ceb --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Type/Duration.php @@ -0,0 +1,68 @@ +value = $value; + } + // Treat integer values as time spans in seconds, even if supplied as PHP + // string. + elseif ((string) (int) $value === (string) $value) { + $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 TypedDataInterface::getString(). + */ + public function getString() { + // Generate an ISO 8601 formatted string as supported by + // DateInterval::__construct() and setValue(). + return (string) $this->getValue()->format('%rP%yY%mM%dDT%hH%mM%sS'); + } + + /** + * Implements TypedDataInterface::validate(). + */ + public function validate() { + // TODO: Implement validate() method. + } +} diff --git a/core/lib/Drupal/Core/TypedData/Type/Float.php b/core/lib/Drupal/Core/TypedData/Type/Float.php new file mode 100644 index 0000000..798499c --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Type/Float.php @@ -0,0 +1,40 @@ +value = isset($value) ? (float) $value : $value; + } + + /** + * Implements TypedDataInterface::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..4e9b59a --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Type/Integer.php @@ -0,0 +1,40 @@ +value = isset($value) ? (int) $value : $value; + } + + /** + * Implements TypedDataInterface::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..67f4df8 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Type/Language.php @@ -0,0 +1,134 @@ +name; + } + + /** + * Implements ContextAwareInterface::setName(). + */ + public function setName($name) { + $this->name = $name; + } + + /** + * Implements ContextAwareInterface::getParent(). + */ + public function getParent() { + return $this->parent; + } + + /** + * Implements ContextAwareInterface::setParent(). + */ + public function setParent($parent) { + $this->parent = $parent; + } + + /** + * Implements TypedDataInterface::getValue(). + */ + public function getValue() { + $source = $this->getLanguageCodeSource(); + $langcode = $source ? $source->getValue() : $this->langcode; + return $langcode ? language_load($langcode) : NULL; + } + + /** + * Helper to get the typed data object holding the source language code. + * + * @return \Drupal\Core\TypedData\TypedDataInterface|FALSE + */ + protected function getLanguageCodeSource() { + return !empty($this->definition['settings']['langcode source']) ? $this->parent->get($this->definition['settings']['langcode source']) : FALSE; + } + + /** + * Implements TypedDataInterface::setValue(). + * + * Both the langcode and the language object may be passed as value. + */ + public function setValue($value) { + // Support passing language objects. + if (is_object($value)) { + $value = $value->langcode; + } + elseif (isset($value) && !is_scalar($value)) { + throw new InvalidArgumentException('Value is no valid langcode or language object.'); + } + + $source = $this->getLanguageCodeSource(); + if ($source) { + $source->setValue($value); + } + else { + $this->langcode = $value; + } + } + + /** + * Implements TypedDataInterface::getString(). + */ + public function getString() { + $language = $this->getValue(); + return $language ? $language->name : ''; + } + + /** + * Implements TypedDataInterface::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..fef3248 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Type/String.php @@ -0,0 +1,40 @@ +value = isset($value) ? (string) $value : $value; + } + + /** + * Implements TypedDataInterface::validate(). + */ + public function validate() { + // TODO: Implement validate() method. + } +} diff --git a/core/lib/Drupal/Core/TypedData/Type/TypedData.php b/core/lib/Drupal/Core/TypedData/Type/TypedData.php new file mode 100644 index 0000000..6afc860 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Type/TypedData.php @@ -0,0 +1,68 @@ +definition = $definition; + } + + /** + * Implements TypedDataInterface::getType(). + */ + public function getType() { + return $this->definition['type']; + } + + /** + * Implements TypedDataInterface::getDefinition(). + */ + public function getDefinition() { + return $this->definition; + } + + /** + * Implements TypedDataInterface::getValue(). + */ + public function getValue() { + return $this->value; + } + + /** + * Implements TypedDataInterface::setValue(). + */ + public function setValue($value) { + $this->value = $value; + } + + /** + * Implements TypedDataInterface::getString(). + */ + public function getString() { + return (string) $this->getValue(); + } +} 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..010fa03 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Type/Uri.php @@ -0,0 +1,39 @@ +value = isset($value) ? (string) $value : $value; + } + + /** + * Implements TypedDataInterface::validate(). + */ + public function validate() { + // TODO: Implement validate() method. + } +} diff --git a/core/lib/Drupal/Core/TypedData/TypedDataFactory.php b/core/lib/Drupal/Core/TypedData/TypedDataFactory.php new file mode 100644 index 0000000..d245157 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/TypedDataFactory.php @@ -0,0 +1,52 @@ +discovery->getDefinition($plugin_id); + + // Allow per-data definition overrides of the used classes and generally + // default to the data type definition. + $definition = $configuration + $type_definition; + + if (empty($definition['list'])) { + if (empty($definition['class'])) { + throw new PluginException(sprintf('The plugin (%s) did not specify an instance class.', $plugin_id)); + } + $plugin_class = $definition['class']; + } + else { + if (empty($definition['list class'])) { + throw new PluginException(sprintf('The plugin (%s) did not specify a list instance class.', $plugin_id)); + } + $plugin_class = $definition['list class']; + } + return new $plugin_class($definition, $plugin_id, $this->discovery); + } +} diff --git a/core/lib/Drupal/Core/TypedData/TypedDataInterface.php b/core/lib/Drupal/Core/TypedData/TypedDataInterface.php new file mode 100644 index 0000000..680b92e --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/TypedDataInterface.php @@ -0,0 +1,73 @@ +discovery = new CacheDecorator(new HookDiscovery('data_type_info'), 'typed_data:types'); + $this->factory = new TypedDataFactory($this->discovery); + } + + /** + * Implements Drupal\Component\Plugin\PluginManagerInterface::createInstance(). + * + * @param string $plugin_id + * The id of a plugin, i.e. the data type. + * @param array $configuration + * The plugin configuration, i.e. the data definition. + * + * @return Drupal\Core\TypedData\TypedDataInterface + */ + public function createInstance($plugin_id, array $configuration) { + return $this->factory->createInstance($plugin_id, $configuration); + } + + /** + * Creates a new typed data object wrapping the passed value. + * + * @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 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 object, e.g. depending on some other values. + * - read-only: A boolean specifying whether the 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 object; otherwise the default class of the data type will be + * used. + * - list class: If set and 'list' is TRUE, the class to use for creating + * the typed data object; otherwise the default list class of the data + * type will be used. + * - settings: An array of settings, as required by the used 'class'. See + * the documentation of the class for supported or required settings. + * - list settings: An array of settings as required by the used + * 'list class'. See the documentation of the list class for support 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 field definitions see + * Drupal\Core\Entity\StorageControllerInterface::getPropertyDefinitions(). + * @param mixed $value + * (optional) The data value. If set, it has to match one of the supported + * data type format as documented for the data type classes. + * @param array $context + * (optional) An array describing the context of the data object, e.g. its + * name or parent data structure. The context should be passed if a typed + * data object is created as part of a data structure. The following keys + * are supported: + * - name: The name associated with the data. + * - parent: The parent object containing the data. Must be an instance of + * Drupal\Core\TypedData\ComplexDataInterface or + * Drupal\Core\TypedData\ListInterface. + * + * @return Drupal\Core\TypedData\TypedDataInterface + * + * @see typed_data() + * @see Drupal\Core\TypedData\Type\Integer + * @see Drupal\Core\TypedData\Type\Float + * @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\Core\Entity\Field\EntityWrapper + */ + function create(array $definition, $value = NULL, array $context = array()) { + $wrapper = $this->createInstance($definition['type'], $definition); + if (isset($value)) { + $wrapper->setValue($value); + } + if ($wrapper instanceof ContextAwareInterface) { + if (isset($context['name'])) { + $wrapper->setName($context['name']); + } + if (isset($context['parent'])) { + $wrapper->setParent($context['parent']); + } + } + return $wrapper; + } +} diff --git a/core/modules/field/field.default.inc b/core/modules/field/field.default.inc index fa91330..961c59b 100644 --- a/core/modules/field/field.default.inc +++ b/core/modules/field/field.default.inc @@ -80,7 +80,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 e13f386..f42df5f 100644 --- a/core/modules/field/field.module +++ b/core/modules/field/field.module @@ -365,6 +365,65 @@ 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 'field item class' mandatory. + foreach ($field_types as $type_name => $type_info) { + + if (!empty($type_info['field item class'])) { + $items[$type_name . '_field'] = array( + 'label' => t('Field !label item', array('!label' => $type_info['label'])), + 'class' => $type_info['field item class'], + 'list class' => !empty($type_info['field class']) ? $type_info['field class'] : '\Drupal\Core\Entity\Field\Type\Field', + ); + } + } + return $items; +} + +/** + * Implements hook_entity_field_info() to define all configured fields. + */ +function field_entity_field_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']]['field item class'])) { + + // @todo: Allow for adding field type settings. + $definition = array( + 'label' => t('Field !name', array('!name' => $field_name)), + 'type' => $field['type'] . '_field', + 'configurable' => 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/modules/text/lib/Drupal/text/TextProcessed.php b/core/modules/field/modules/text/lib/Drupal/text/TextProcessed.php new file mode 100644 index 0000000..9284c2d --- /dev/null +++ b/core/modules/field/modules/text/lib/Drupal/text/TextProcessed.php @@ -0,0 +1,124 @@ +definition = $definition; + + 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."); + } + } + + /** + * Implements ContextAwareInterface::getName(). + */ + public function getName() { + return $this->name; + } + + /** + * Implements ContextAwareInterface::setName(). + */ + public function setName($name) { + $this->name = $name; + } + + /** + * Implements ContextAwareInterface::getParent(). + * + * @return \Drupal\Core\Entity\Field\FieldItemInterface + */ + public function getParent() { + return $this->parent; + } + + /** + * Implements ContextAwareInterface::setParent(). + */ + public function setParent($parent) { + $this->parent = $parent; + $this->text = $parent->get($this->definition['settings']['text source']); + $this->format = $parent->get('format'); + } + + /** + * Implements TypedDataInterface::getValue(). + */ + public function getValue($langcode = NULL) { + + if (!isset($this->text)) { + throw new InvalidArgumentException('Computed properties require context for computation.'); + } + + $field = $this->parent->getParent(); + $entity = $field->getParent(); + $instance = field_info_instance($entity->entityType(), $field->getName(), $entity->bundle()); + + if (!empty($instance['settings']['text_processing']) && $this->format->value) { + return check_markup($this->text->value, $this->format->value, $entity->language()->langcode); + } + else { + // If no format is available, still make sure to sanitize the text. + return check_plain($this->text->value); + } + } + + /** + * Implements TypedDataInterface::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/Type/TextItem.php b/core/modules/field/modules/text/lib/Drupal/text/Type/TextItem.php new file mode 100644 index 0000000..07ae7f9 --- /dev/null +++ b/core/modules/field/modules/text/lib/Drupal/text/Type/TextItem.php @@ -0,0 +1,53 @@ + '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\TextProcessed', + 'settings' => array( + 'text source' => 'value', + ), + ); + } + return self::$propertyDefinitions; + } +} diff --git a/core/modules/field/modules/text/lib/Drupal/text/Type/TextSummaryItem.php b/core/modules/field/modules/text/lib/Drupal/text/Type/TextSummaryItem.php new file mode 100644 index 0000000..b6438ff --- /dev/null +++ b/core/modules/field/modules/text/lib/Drupal/text/Type/TextSummaryItem.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\TextProcessed', + '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 4387ff2..f362fb7 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', + 'field item class' => '\Drupal\text\Type\TextItem', ), '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', + 'field item class' => '\Drupal\text\Type\TextItem', ), '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', + 'field item class' => '\Drupal\text\Type\TextSummaryItem', ), ); } diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php index 20cc70b..4d86f8d 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php @@ -2033,6 +2033,25 @@ abstract class WebTestBase extends TestBase { } /** + * Creates a typed data object and executes some basic assertions. + * + * @see Drupal\Core\TypedData\TypedDataManager::create(). + */ + protected function createTypedData($definition, $value = NULL, $context = array()) { + // Save the type that was passed in so we can compare with it later. + $type = $definition['type']; + // Construct the object. + $data = typed_data()->create($definition, $value, $context); + // Assert the definition of the wrapper. + $this->assertTrue($data instanceof \Drupal\Core\TypedData\TypedDataInterface, 'Typed data object is an instance of the typed data interface.'); + $definition = $data->getDefinition(); + $this->assertTrue(!empty($definition['label']), $definition['label'] . ' data definition was returned.'); + // Assert that the correct type was constructed. + $this->assertEqual($data->getType(), $type, $definition['label'] . ' object returned type.'); + return $data; + } + + /** * Pass if the internal browser's URL matches the given path. * * @param $path diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityApiTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityApiTest.php index 587b6f8..7cf3403 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityApiTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/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/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php new file mode 100644 index 0000000..beb40f6 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php @@ -0,0 +1,395 @@ + 'Entity Field API', + 'description' => 'Tests the Entity Field API', + 'group' => 'Entity API', + ); + } + + /** + * Creates a test entity. + * + * @return \Drupal\Core\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 field when creating. With the user + // field we test setting a field 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 field items. + */ + public function testReadWrite() { + $entity = $this->createTestEntity(); + + // Access the name field. + $this->assertTrue($entity->name instanceof FieldInterface, 'Field implements interface'); + $this->assertTrue($entity->name[0] instanceof FieldItemInterface, 'Field 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 field 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 field 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 field. + $this->assertTrue($entity->user_id instanceof FieldInterface, 'Field implements interface'); + $this->assertTrue($entity->user_id[0] instanceof FieldItemInterface, 'Field 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 field. + $entity->name->value = NULL; + $entity->user_id->value = NULL; + $this->assertNull($entity->name->value, 'Name field is not set.'); + $this->assertNull($entity->user_id->value, 'User ID field is not set.'); + $this->assertNull($entity->user_id->entity, 'User entity field is not set.'); + + // Test using isset(), empty() and unset(). + $entity->name->value = 'test unset'; + unset($entity->name->value); + $this->assertFalse(isset($entity->name->value), 'Name is not set.'); + $this->assertFalse(isset($entity->name[0]->value), 'Name is not set.'); + $this->assertTrue(empty($entity->name->value), 'Name is empty.'); + $this->assertTrue(empty($entity->name[0]->value), 'Name is empty.'); + + $entity->name->value = 'a value'; + $this->assertTrue(isset($entity->name->value), 'Name is set.'); + $this->assertTrue(isset($entity->name[0]->value), 'Name is set.'); + $this->assertFalse(empty($entity->name->value), 'Name is not empty.'); + $this->assertFalse(empty($entity->name[0]->value), 'Name is not empty.'); + $this->assertTrue(isset($entity->name[0]), 'Name string item is set.'); + $this->assertFalse(isset($entity->name[1]), 'Second name string item is not set as it does not exist'); + $this->assertTrue(isset($entity->name), 'Name field is set.'); + $this->assertFalse(isset($entity->nameInvalid), 'Not existing field is not set.'); + + unset($entity->name[0]); + $this->assertFalse(isset($entity->name[0]), 'Name field item is not set.'); + $this->assertFalse(isset($entity->name[0]->value), 'Name is not set.'); + $this->assertFalse(isset($entity->name->value), 'Name is not set.'); + + $entity->name->value = 'a value'; + $this->assertTrue(isset($entity->name->value), 'Name is set.'); + unset($entity->name); + $this->assertFalse(isset($entity->name), 'Name field is not set.'); + $this->assertFalse(isset($entity->name[0]), 'Name field item is not set.'); + $this->assertFalse(isset($entity->name[0]->value), 'Name is not set.'); + $this->assertFalse(isset($entity->name->value), 'Name is not set.'); + + // Access the language field. + $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.'); + + // Test copying field values. + $entity2 = $this->createTestEntity(); + $entity2->name = $entity->name; + $entity2->user_id = $entity->user_id; + $entity2->field_test_text = $entity->field_test_text; + + $this->assertTrue($entity->name !== $entity2->name, 'Copying properties results in a different field object.'); + $this->assertEqual($entity->name->value, $entity2->name->value, 'Name field copied.'); + $this->assertEqual($entity->user_id->value, $entity2->user_id->value, 'User id field copied.'); + $this->assertEqual($entity->field_test_text->value, $entity2->field_test_text->value, 'Text field copied.'); + + // Tests adding a value to a field item list. + $entity->name[] = 'Another name'; + $this->assertEqual($entity->name[1]->value == 'Another name', 'List item added via [].'); + $entity->name[2]->value = 'Third name'; + $this->assertEqual($entity->name[2]->value == 'Third name', 'List item added by a accessing not yet created item.'); + + // Test removing and empty-ing list items. + $this->assertEqual(count($entity->name), 3, 'List has 3 items.'); + unset($entity->name[1]); + $this->assertEqual(count($entity->name), 2, 'Second list item has been removed.'); + $entity->name[2] = NULL; + $this->assertEqual(count($entity->name), 2, 'Assigning NULL does not reduce array count.'); + $this->assertTrue($entity->name[2]->isEmpty(), 'Assigning NULL empties the item.'); + + // Test using isEmpty(). + unset($entity->name[2]); + $this->assertFalse($entity->name[0]->isEmpty(), 'Name item is not empty.'); + $entity->name->value = NULL; + $this->assertTrue($entity->name[0]->isEmpty(), 'Name item is empty.'); + $this->assertTrue($entity->name->isEmpty(), 'Name field is empty.'); + $this->assertEqual(count($entity->name), 1, 'Empty item is considered when counting.'); + $this->assertEqual(count(iterator_to_array($entity->name->getIterator())), count($entity->name), 'Count matches iterator count.'); + $this->assertTrue($entity->name->getValue() === array(0 => NULL), 'Name field value contains a NULL value.'); + + // Test get and set field values. + $entity->name = 'foo'; + $this->assertEqual($entity->name[0]->getPropertyValues(), array('value' => 'foo'), 'Field value has been retrieved via getPropertyValue()'); + $entity->name[0]->setPropertyValues(array('value' => 'bar')); + $this->assertEqual($entity->name->value, 'bar', 'Field value has been set via setPropertyValue()'); + + $values = $entity->getPropertyValues(); + $this->assertEqual($values['name'], array(0 => array('value' => 'bar')), 'Field value has been retrieved via getPropertyValue() from an entity.'); + $entity->setPropertyValues(array('name' => 'foo')); + $this->assertEqual($entity->name->value, 'foo', 'Field value has been set via setPropertyValue() on an entity.'); + } + + /** + * 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 field. + $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 = typed_data()->create($definition); + $definitions = $wrapped_entity->getPropertyDefinitions($definition); + $this->assertEqual($definitions['name']['type'], 'string_field', 'Name field found.'); + $this->assertEqual($definitions['user_id']['type'], 'entityreference_field', 'User field found.'); + $this->assertEqual($definitions['field_test_text']['type'], 'text_field', 'Test-text-field field 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_field', 'Name field found.'); + $this->assertEqual($definitions['user_id']['type'], 'entityreference_field', 'User field found.'); + $this->assertEqual($definitions['field_test_text']['type'], 'text_field', 'Test-text-field field 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 field 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 => $field) { + $this->assertTrue($field instanceof FieldInterface, "Field $name implements interface."); + + foreach ($field as $delta => $item) { + $this->assertTrue($field[0] instanceof FieldItemInterface, "Item $delta of field $name implements interface."); + + foreach ($item as $value_name => $value_property) { + $this->assertTrue($value_property instanceof TypedDataInterface, "Value $value_name of item $delta of field $name implements interface."); + + $value = $value_property->getValue(); + $this->assertTrue(!isset($value) || is_scalar($value) || $value instanceof EntityInterface, "Value $value_name of item $delta of field $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 = typed_data()->create($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(TypedDataInterface $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\ComplexDataInterface) { + 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/system/lib/Drupal/system/Tests/Entity/EntityFormTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFormTest.php index da1ff3f..cfb6ffc 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFormTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFormTest.php @@ -45,7 +45,7 @@ class EntityFormTest extends WebTestBase { $edit = array( 'name' => $name1, - 'uid' => mt_rand(0, 128), + 'user_id' => mt_rand(0, 128), "field_test_text[$langcode][0][value]" => $this->randomName(16), ); @@ -59,7 +59,7 @@ class EntityFormTest extends WebTestBase { $this->assertFalse($entity, 'The entity has been modified.'); $entity = $this->loadEntityByName($name2); $this->assertTrue($entity, 'Modified entity found in the database.'); - $this->assertNotEqual($entity->get('name'), $name1, 'The entity name has been modified.'); + $this->assertNotEqual($entity->name->value, $name1, 'The entity name has been modified.'); $this->drupalPost('entity-test/manage/' . $entity->id() . '/edit', array(), t('Delete')); $entity = $this->loadEntityByName($name2); diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationFormTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationFormTest.php index 2036d23..cf793c2 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationFormTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationFormTest.php @@ -113,7 +113,7 @@ class EntityTranslationFormTest extends WebTestBase { // Create a body translation and check the form language. $langcode2 = $this->langcodes[1]; - $node->set('body', array(array('value' => $this->randomName(16))), $langcode2); + $node->body[$langcode2][0]['value'] = $this->randomName(16); $node->save(); $this->drupalGet($langcode2 . '/node/' . $node->nid . '/edit'); $form_langcode = variable_get('entity_form_langcode', FALSE); diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php index a571f61..e0b25fb 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php @@ -78,76 +78,99 @@ 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.'); - - $message = "An exception is thrown when trying to set a field with an invalid language"; + $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 will throw an exception. - try { - $entity->set($this->field_name, array(0 => array('value' => 'default value2')), $this->langcodes[1]); - $this->fail($message); - } - catch (Exception $e) { - $this->assertTrue($e instanceof InvalidArgumentException, $message); - } + // language-specific it should use the default language and so ignore the + // specified language. + $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 a specific language for a not // language-specific entity. - $value = $entity->get($this->field_name, $this->langcodes[1]); - $this->assertNull($value, 'Returned NULL for getter with invalid language.'); + $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 is NULL.'); + } + catch (InvalidArgumentException $e) { + $this->pass('A translation for an invalid language is NULL.'); + } + + // Try to get an unstranslatable value from a translation in strict mode. + try { + $field_name = 'field_test_text'; + $value = $entity->getTranslation($this->langcodes[1])->get($field_name); + $this->fail('Getting an unstranslatable value from a translation in strict mode throws an exception.'); + } + catch (InvalidArgumentException $e) { + $this->pass('Getting an unstranslatable value from a translation in strict mode throws an exception.'); + } + + // Try to get an unstranslatable value from a translation in non-strict + // mode. + $entity->set($field_name, array(0 => array('value' => 'default value'))); + $value = $entity->getTranslation($this->langcodes[1], FALSE)->get($field_name)->value; + $this->assertEqual($value, 'default value', 'Untranslated value retrieved from translation in non-strict mode.'); // 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 (InvalidArgumentException $e) { + $this->pass("Setting a translation for an invalid language throws an exception."); + } + + // Try to set an unstranslatable value into a translation in strict mode. + try { + $entity->getTranslation($this->langcodes[1])->set($field_name, NULL); + $this->fail("Setting an unstranslatable value into a translation in strict mode throws an exception."); } - catch (Exception $e) { - $this->assertTrue($e instanceof InvalidArgumentException, $message); + catch (InvalidArgumentException $e) { + $this->pass("Setting an unstranslatable value into a translation in strict mode throws an exception."); } + + // Set the value in default language. + $entity->getTranslation($this->langcodes[1], FALSE)->set($field_name, array(0 => array('value' => 'default value2'))); + // Get the value. + $this->assertEqual($entity->get($field_name)->value, 'default value2', 'Untranslated value set into a translation in non-strict mode.'); } /** @@ -160,29 +183,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(); @@ -190,17 +217,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)->setPropertyValues($properties[$langcode]); } $entity->save(); @@ -208,8 +235,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). @@ -217,7 +244,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)); @@ -226,15 +257,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.'); // Test property conditions and orders with multiple languages in the same @@ -242,7 +274,7 @@ class EntityTranslationTest extends WebTestBase { $query = new EntityFieldQuery(); $query->entityCondition('entity_type', 'entity_test'); $query->entityCondition('langcode', $default_langcode); - $query->propertyCondition('uid', $properties[$default_langcode]['uid'], NULL, 'original'); + $query->propertyCondition('user_id', $properties[$default_langcode]['user_id'], NULL, 'original'); $query->propertyCondition('name', $properties[$default_langcode]['name'], NULL, 'original'); $query->propertyLanguageCondition($default_langcode, NULL, 'original'); $query->propertyCondition('name', $properties[$langcode]['name'], NULL, 'translation'); @@ -254,12 +286,12 @@ class EntityTranslationTest extends WebTestBase { // Test mixed property and field conditions. $entity = entity_load('entity_test', key($result['entity_test']), TRUE); $field_value = $this->randomString(); - $entity->set($this->field_name, array(array('value' => $field_value)), $langcode); + $entity->getTranslation($langcode)->set($this->field_name, array(array('value' => $field_value))); $entity->save(); $query = new EntityFieldQuery(); $query->entityCondition('entity_type', 'entity_test'); $query->entityCondition('langcode', $default_langcode); - $query->propertyCondition('uid', $properties[$default_langcode]['uid'], NULL, 'original'); + $query->propertyCondition('user_id', $properties[$default_langcode]['user_id'], NULL, 'original'); $query->propertyCondition('name', $properties[$default_langcode]['name'], NULL, 'original'); $query->propertyLanguageCondition($default_langcode, NULL, 'original'); $query->propertyCondition('name', $properties[$langcode]['name'], NULL, 'translation'); diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityUUIDTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityUUIDTest.php index 741069b..1692d7b 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityUUIDTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityUUIDTest.php @@ -64,7 +64,7 @@ class EntityUUIDTest extends WebTestBase { // Verify that entity_load_by_uuid() loads the same entity. $entity_loaded_by_uuid = entity_load_by_uuid('entity_test', $uuid, TRUE); $this->assertIdentical($entity_loaded_by_uuid->uuid(), $uuid); - $this->assertEqual($entity_loaded_by_uuid, $entity_loaded); + $this->assertEqual($entity_loaded_by_uuid->id(), $entity_loaded->id()); // Creating a duplicate needs to result in a new UUID. $entity_duplicate = $entity->createDuplicate(); diff --git a/core/modules/system/lib/Drupal/system/Tests/TypedData/TypedDataTest.php b/core/modules/system/lib/Drupal/system/Tests/TypedData/TypedDataTest.php new file mode 100644 index 0000000..62bd35e --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/TypedData/TypedDataTest.php @@ -0,0 +1,130 @@ + 'Test typed data objects', + 'description' => 'Tests the functionality of all core data types.', + 'group' => 'Typed Data API', + ); + } + + /** + * Tests the basics around constructing and working with data wrappers. + */ + public function testGetAndSet() { + // Boolean type. + $wrapper = $this->createTypedData(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->createTypedData(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->createTypedData(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.'); + + // Float type. + $value = 123.45; + $wrapper = $this->createTypedData(array('type' => 'float'), $value); + $this->assertTrue($wrapper->getValue() === $value, 'Float value was fetched.'); + $new_value = 678.90; + $wrapper->setValue($new_value); + $this->assertTrue($wrapper->getValue() === $new_value, 'Float value was changed.'); + $this->assertTrue(is_string($wrapper->getString()), 'Float value was converted to string'); + $wrapper->setValue(NULL); + $this->assertNull($wrapper->getValue(), 'Float wrapper is null-able.'); + + // Date type. + $value = new DateTime('@' . REQUEST_TIME); + $wrapper = $this->createTypedData(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->createTypedData(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->createTypedData(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->createTypedData(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 4f87ff6..66aa5e8 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -147,6 +147,65 @@ function hook_cron() { } /** + * Defines available data types for the typed data API. + * + * The typed data API allows modules to support any kind of data based upon + * pre-defined primitive types and interfaces for complex data and lists. + * + * Defined data types may map to one of the pre-defined primitive types in + * \Drupal\Core\TypedData\Primitive or may be complex data types, containing one + * or more data properties. Typed data objects for complex data types have to + * implement the \Drupal\Core\TypedData\ComplexDataInterface. Further interfaces + * that may be implemented are: + * - \Drupal\Core\TypedData\AccessibleInterface + * - \Drupal\Core\TypedData\TranslatableInterface + * + * Furthermore, lists of data items are represented by objects implementing + * the \Drupal\Core\TypedData\ListInterface. A list contains items of the same + * data type, is ordered and may contain duplicates. The classed used for a list + * of items of a certain type 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 class. Must implement the + * \Drupal\Core\TypedData\TypedDataInterface. + * - list class: (optional) A typed data class used for wrapping 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 typed_data() + * @see Drupal\Core\TypedData\TypedDataManager::create() + * @see hook_data_type_info_alter() + */ +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 6fcfa84..bfa91db 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; @@ -1942,6 +1943,105 @@ 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, + ), + 'float' => array( + 'label' => t('Float'), + 'class' => '\Drupal\Core\TypedData\Type\Float', + 'primitive type' => Primitive::FLOAT, + ), + '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', + ), + 'entity' => array( + 'label' => t('Entity'), + 'description' => t('All kind of entities, e.g. nodes, comments or users.'), + 'class' => '\Drupal\Core\Entity\Field\Type\EntityWrapper', + ), + 'entity_translation' => array( + 'label' => t('Entity translation'), + 'description' => t('A translation of an entity'), + 'class' => '\Drupal\Core\Entity\Field\Type\EntityTranslation', + ), + 'boolean_field' => array( + 'label' => t('Boolean field item'), + 'description' => t('An entity field containing a boolean value.'), + 'class' => '\Drupal\Core\Entity\Field\Type\BooleanItem', + 'list class' => '\Drupal\Core\Entity\Field\Type\Field', + ), + 'string_field' => array( + 'label' => t('String field item'), + 'description' => t('An entity field containing a string value.'), + 'class' => '\Drupal\Core\Entity\Field\Type\StringItem', + 'list class' => '\Drupal\Core\Entity\Field\Type\Field', + ), + 'integer_field' => array( + 'label' => t('Integer field item'), + 'description' => t('An entity field containing an integer value.'), + 'class' => '\Drupal\Core\Entity\Field\Type\IntegerItem', + 'list class' => '\Drupal\Core\Entity\Field\Type\Field', + ), + 'date_field' => array( + 'label' => t('Date field item'), + 'description' => t('An entity field containing a date value.'), + 'class' => '\Drupal\Core\Entity\Field\Type\DateItem', + 'list class' => '\Drupal\Core\Entity\Field\Type\Field', + ), + 'language_field' => array( + 'label' => t('Language field item'), + 'description' => t('An entity field referencing a language.'), + 'class' => '\Drupal\Core\Entity\Field\Type\LanguageItem', + 'list class' => '\Drupal\Core\Entity\Field\Type\Field', + ), + 'entityreference_field' => array( + 'label' => t('Entity reference field item'), + 'description' => t('An entity field containing an entity reference.'), + 'class' => '\Drupal\Core\Entity\Field\Type\EntityReferenceItem', + 'list class' => '\Drupal\Core\Entity\Field\Type\Field', + ), + ); +} + +/** * Menu item access callback - only enabled themes can be accessed. */ function _system_themes_access($theme) { diff --git a/core/modules/system/tests/modules/entity_test/entity_test.info b/core/modules/system/tests/modules/entity_test/entity_test.info index bf616ec..ce49e8a 100644 --- a/core/modules/system/tests/modules/entity_test/entity_test.info +++ b/core/modules/system/tests/modules/entity_test/entity_test.info @@ -3,4 +3,5 @@ description = Provides entity types based upon the CRUD API. package = Testing version = VERSION core = 8.x +dependencies[] = field hidden = TRUE diff --git a/core/modules/system/tests/modules/entity_test/entity_test.install b/core/modules/system/tests/modules/entity_test/entity_test.install index 3621eb6..4122d01 100644 --- a/core/modules/system/tests/modules/entity_test/entity_test.install +++ b/core/modules/system/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/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module index c634834..46def82 100644 --- a/core/modules/system/tests/modules/entity_test/entity_test.module +++ b/core/modules/system/tests/modules/entity_test/entity_test.module @@ -86,7 +86,7 @@ function entity_test_add() { * Menu callback: displays the 'Edit existing entity_test' form. */ function entity_test_edit($entity) { - drupal_set_title(t('entity_test @id', array('@id' => $entity->id)), PASS_THROUGH); + drupal_set_title(t('entity_test @id', array('@id' => $entity->id())), PASS_THROUGH); return entity_get_form($entity); } diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTest.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTest.php index f88b685..17e8470 100644 --- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTest.php +++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTest.php @@ -7,137 +7,52 @@ namespace Drupal\entity_test; -use InvalidArgumentException; - -use Drupal\Core\Entity\Entity; +use Drupal\Core\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\Core\Entity\Field\FieldInterface */ - 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\Core\Entity\Field\FieldInterface */ - 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\Core\Entity\Field\FieldInterface */ - 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\Core\Entity\Field\FieldInterface */ - 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/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php index 0c70b13..e51f474 100644 --- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php +++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php @@ -7,12 +7,12 @@ namespace Drupal\entity_test; use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Entity\EntityFormController; +use Drupal\Core\Entity\EntityFormControllerNG; /** * Form controller for the test entity edit forms. */ -class EntityTestFormController extends EntityFormController { +class EntityTestFormController extends EntityFormControllerNG { /** * Overrides Drupal\Core\Entity\EntityFormController::form(). @@ -21,23 +21,22 @@ class EntityTestFormController extends EntityFormController { $form = parent::form($form, $form_state, $entity); $langcode = $this->getFormLangcode($form_state); - $name = $entity->get('name', $langcode); - $uid = $entity->get('uid', $langcode); + $translation = $entity->getTranslation($langcode); $form['name'] = array( '#type' => 'textfield', '#title' => t('Name'), - '#default_value' => !empty($name) ? $name : '', + '#default_value' => $translation->name->value, '#size' => 60, '#maxlength' => 128, '#required' => TRUE, '#weight' => -10, ); - $form['uid'] = array( + $form['user_id'] = array( '#type' => 'textfield', '#title' => 'UID', - '#default_value' => !empty($uid) ? $uid : '', + '#default_value' => $translation->user_id->value, '#size' => 60, '#maxlength' => 128, '#required' => TRUE, @@ -53,12 +52,11 @@ class EntityTestFormController extends EntityFormController { public function submit(array $form, array &$form_state) { $entity = parent::submit($form, $form_state); $langcode = $this->getFormLangcode($form_state); - // Updates multilingual properties. - foreach (array('name', 'uid') as $property) { - $entity->set($property, $form_state['values'][$property], $langcode); + $translation = $entity->getTranslation($langcode); + foreach (array('name', 'user_id') as $name) { + $translation->$name->setValue($form_state['values'][$name]); } - return $entity; } diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php index cef6593..d3ae306 100644 --- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php +++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php @@ -9,9 +9,9 @@ namespace Drupal\entity_test; use PDO; -use Drupal\Core\Entity\DatabaseStorageController; -use Drupal\Core\Entity\EntityFieldQuery; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\DatabaseStorageControllerNG; +use Drupal\Core\Entity\EntityFieldQuery; /** * Defines the controller class for the test entity. @@ -19,10 +19,10 @@ use Drupal\Core\Entity\EntityInterface; * This extends the Drupal\Core\Entity\DatabaseStorageController class, adding * required special handling for test entities. */ -class EntityTestStorageController extends DatabaseStorageController { +class EntityTestStorageController extends DatabaseStorageControllerNG { /** - * Overrides Drupal\Core\Entity\DatabaseStorageController::loadByProperties(). + * Overrides Drupal\Core\Entity\DatabaseStorageController::buildPropertyQuery(). */ protected function buildPropertyQuery(EntityFieldQuery $entity_query, array $values) { // @todo We should not be using a condition to specify whether conditions @@ -43,9 +43,23 @@ class EntityTestStorageController extends DatabaseStorageController { } /** - * Overrides Drupal\Core\Entity\DatabaseStorageController::attachLoad(). + * Maps from storage records to entity objects. + * + * @return array + * An array of entity objects implementing the EntityInterface. + */ + protected function mapFromStorageRecords(array $records) { + $records = parent::mapFromStorageRecords($records); + + // Load data of translatable properties. + $this->attachPropertyData($records); + return $records; + } + + /** + * Attaches property data in all languages for translatable properties. */ - protected function attachLoad(&$queried_entities, $load_revision = FALSE) { + 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)) @@ -53,35 +67,34 @@ 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); - } - // Make sure only real properties are stored. - unset($values['id'], $values['default_langcode']); - $entity->setProperties($values, $langcode); - } + $id = $values['id']; + // Field values in default language are stored with + // LANGUAGE_DEFAULT as key. + $langcode = empty($values['default_langcode']) ? $values['langcode'] : LANGUAGE_DEFAULT; - parent::attachLoad($queried_entities, $load_revision); + $queried_entities[$id]->name[$langcode][0]['value'] = $values['name']; + $queried_entities[$id]->user_id[$langcode][0]['value'] = $values['user_id']; + } } /** * Overrides Drupal\Core\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) @@ -99,4 +112,40 @@ class EntityTestStorageController extends DatabaseStorageController { ->condition('id', array_keys($entities)) ->execute(); } + + /** + * Implements \Drupal\Core\Entity\DataBaseStorageControllerNG::baseFieldDefinitions(). + */ + public function baseFieldDefinitions() { + $properties['id'] = array( + 'label' => t('ID'), + 'description' => t('The ID of the test entity.'), + 'type' => 'integer_field', + 'read-only' => TRUE, + ); + $properties['uuid'] = array( + 'label' => t('UUID'), + 'description' => t('The UUID of the test entity.'), + 'type' => 'string_field', + ); + $properties['langcode'] = array( + 'label' => t('Language code'), + 'description' => t('The language code of the test entity.'), + 'type' => 'language_field', + ); + $properties['name'] = array( + 'label' => t('Name'), + 'description' => t('The name of the test entity.'), + 'type' => 'string_field', + 'translatable' => TRUE, + ); + $properties['user_id'] = array( + 'label' => t('User ID'), + 'description' => t('The ID of the associated user.'), + 'type' => 'entityreference_field', + 'settings' => array('entity type' => 'user'), + 'translatable' => TRUE, + ); + return $properties; + } }