Merge branch '8.x' of git.drupal.org:project/drupal into 8.x

8.0.x
Jennifer Hodgdon 2012-09-28 10:04:12 -07:00
commit fe147abd4b
67 changed files with 5029 additions and 419 deletions

View File

@ -222,6 +222,14 @@ const LANGUAGE_NOT_APPLICABLE = 'zxx';
*/ */
const LANGUAGE_MULTIPLE = 'mul'; 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. * The language state when referring to configurable languages.
*/ */
@ -2475,6 +2483,19 @@ function state() {
return drupal_container()->get('state.storage'); return drupal_container()->get('state.storage');
} }
/**
* 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. * Returns the test prefix if this is an internal request from SimpleTest.
* *

View File

@ -466,3 +466,68 @@ function hook_entity_view_mode_alter(&$view_mode, Drupal\Core\Entity\EntityInter
$view_mode = 'my_custom_view_mode'; $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';
}
}

View File

@ -346,6 +346,13 @@ class ConfigStorageController implements EntityStorageControllerInterface {
protected function postDelete($entities) { protected function postDelete($entities) {
} }
/**
* Implements Drupal\Core\Entity\EntityStorageControllerInterface::getFieldDefinitions().
*/
public function getFieldDefinitions(array $constraints) {
return array();
}
/** /**
* Invokes a hook on behalf of the entity. * Invokes a hook on behalf of the entity.
* *

View File

@ -52,6 +52,7 @@ class CoreBundle extends Bundle
->setFactoryClass('Drupal\Core\Database\Database') ->setFactoryClass('Drupal\Core\Database\Database')
->setFactoryMethod('getConnection') ->setFactoryMethod('getConnection')
->addArgument('slave'); ->addArgument('slave');
$container->register('typed_data', 'Drupal\Core\TypedData\TypedDataManager');
// @todo Replace below lines with the commented out block below it when it's // @todo Replace below lines with the commented out block below it when it's
// performant to do so: http://drupal.org/node/1706064. // performant to do so: http://drupal.org/node/1706064.

View File

@ -45,6 +45,15 @@ class DatabaseStorageController implements EntityStorageControllerInterface {
*/ */
protected $entityInfo; protected $entityInfo;
/**
* An array of field information, i.e. containing definitions.
*
* @var array
*
* @see hook_entity_field_info()
*/
protected $entityFieldInfo;
/** /**
* Additional arguments to pass to hook_TYPE_load(). * Additional arguments to pass to hook_TYPE_load().
* *
@ -201,7 +210,7 @@ class DatabaseStorageController implements EntityStorageControllerInterface {
// Remove any invalid ids from the array. // Remove any invalid ids from the array.
$passed_ids = array_intersect_key($passed_ids, $entities); $passed_ids = array_intersect_key($passed_ids, $entities);
foreach ($entities as $entity) { foreach ($entities as $entity) {
$passed_ids[$entity->{$this->idKey}] = $entity; $passed_ids[$entity->id()] = $entity;
} }
$entities = $passed_ids; $entities = $passed_ids;
} }
@ -470,7 +479,7 @@ class DatabaseStorageController implements EntityStorageControllerInterface {
if (!$entity->isNew()) { if (!$entity->isNew()) {
$return = drupal_write_record($this->entityInfo['base table'], $entity, $this->idKey); $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->postSave($entity, TRUE);
$this->invokeHook('update', $entity); $this->invokeHook('update', $entity);
} }
@ -547,4 +556,57 @@ class DatabaseStorageController implements EntityStorageControllerInterface {
// Invoke the respective entity-level hook. // Invoke the respective entity-level hook.
module_invoke_all('entity_' . $hook, $entity, $this->entityType); 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();
}
} }

View File

@ -0,0 +1,242 @@
<?php
/**
* @file
* Definition of Drupal\Core\Entity\DatabaseStorageControllerNG.
*/
namespace Drupal\Core\Entity;
use PDO;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\DatabaseStorageController;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Component\Uuid\Uuid;
/**
* Implements Field API specific enhancements to the DatabaseStorageController class.
*
* @todo: Once all entity types have been converted, merge improvements into the
* DatabaseStorageController class.
*/
class DatabaseStorageControllerNG extends DatabaseStorageController {
/**
* The entity class to use.
*
* @var string
*/
protected $entityClass;
/**
* The entity bundle key.
*
* @var string|bool
*/
protected $bundleKey;
/**
* Overrides DatabaseStorageController::__construct().
*/
public function __construct($entityType) {
parent::__construct($entityType);
$this->bundleKey = !empty($this->entityInfo['entity keys']['bundle']) ? $this->entityInfo['entity keys']['bundle'] : FALSE;
$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;
}
}

View File

@ -9,6 +9,7 @@ namespace Drupal\Core\Entity;
use Drupal\Component\Uuid\Uuid; use Drupal\Component\Uuid\Uuid;
use Drupal\Core\Language\Language; use Drupal\Core\Language\Language;
use IteratorAggregate;
/** /**
* Defines a base entity class. * 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 * This class can be used as-is by simple entity types. Entity types requiring
* special handling can extend the class. * special handling can extend the class.
*/ */
class Entity implements EntityInterface { class Entity implements IteratorAggregate, EntityInterface {
/** /**
* The language code of the entity's default language. * 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() { 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)); 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() { public function translations() {
$languages = array(); $languages = array();
@ -177,108 +273,11 @@ class Entity implements EntityInterface {
} }
/** /**
* Implements EntityInterface::get(). * Implements TranslatableInterface::getTranslationLanguages().
*/ */
public function get($property_name, $langcode = NULL) { public function getTranslationLanguages($include_default = TRUE) {
// Handle fields. // @todo: Replace by EntityNG implementation once all entity types have been
$entity_info = $this->entityInfo(); // converted to use the entity field API.
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().
*/
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;
}
} }
/** /**
@ -337,5 +336,4 @@ class Entity implements EntityInterface {
} }
return $return; return $return;
} }
} }

View File

@ -71,7 +71,7 @@ class EntityFormController implements EntityFormControllerInterface {
* @see Drupal\Core\Entity\EntityFormController::build() * @see Drupal\Core\Entity\EntityFormController::build()
*/ */
public function form(array $form, array &$form_state, EntityInterface $entity) { 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. // entity properties.
$info = $entity->entityInfo(); $info = $entity->entityInfo();
if (!empty($info['fieldable'])) { if (!empty($info['fieldable'])) {
@ -145,7 +145,7 @@ class EntityFormController implements EntityFormControllerInterface {
* Implements Drupal\Core\Entity\EntityFormControllerInterface::validate(). * Implements Drupal\Core\Entity\EntityFormControllerInterface::validate().
*/ */
public function validate(array $form, array &$form_state) { 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 properties.
$entity = $this->buildEntity($form, $form_state); $entity = $this->buildEntity($form, $form_state);
$info = $entity->entityInfo(); $info = $entity->entityInfo();
@ -236,7 +236,7 @@ class EntityFormController implements EntityFormControllerInterface {
public function buildEntity(array $form, array &$form_state) { public function buildEntity(array $form, array &$form_state) {
$entity = clone $this->getEntity($form_state); $entity = clone $this->getEntity($form_state);
// @todo Move entity_form_submit_build_entity() here. // @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); entity_form_submit_build_entity($entity->entityType(), $entity, $form, $form_state);
return $entity; return $entity;
} }

View File

@ -0,0 +1,86 @@
<?php
/**
* @file
* Definition of Drupal\Core\Entity\EntityFormControllerNG.
*/
namespace Drupal\Core\Entity;
/**
* Entity form controller variant for entity types using the new property API.
*
* @todo: Merge with EntityFormController and overhaul once all entity types
* are converted to the new property API.
*/
class EntityFormControllerNG extends EntityFormController {
/**
* Overrides EntityFormController::form().
*/
public function form(array $form, array &$form_state, EntityInterface $entity) {
// @todo Exploit the Field API to generate the default widgets for the
// entity properties.
$info = $entity->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;
}
}

View File

@ -7,10 +7,17 @@
namespace Drupal\Core\Entity; 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. * 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. * Constructs a new entity object.
@ -110,60 +117,6 @@ interface EntityInterface {
*/ */
public function uri(); 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. * Saves an entity permanently.
* *

View File

@ -0,0 +1,424 @@
<?php
/**
* @file
* Definition of Drupal\Core\Entity\EntityNG.
*/
namespace Drupal\Core\Entity;
use Drupal\Core\TypedData\ContextAwareInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\Component\Uuid\Uuid;
use ArrayIterator;
use InvalidArgumentException;
/**
* Implements Entity Field API specific enhancements to the Entity class.
*
* An entity implements the ComplexDataInterface, thus is complex data
* containing fields as its data properties. The entity fields have to implement
* the \Drupal\Core\Entity\Field\FieldInterface.
*
* @todo: Once all entity types have been converted, merge improvements into the
* Entity class and overhaul the EntityInterface.
*/
class EntityNG extends Entity {
/**
* The plain data values of the contained fields.
*
* This always holds the original, unchanged values of the entity. The values
* are keyed by language code, whereas LANGUAGE_NOT_SPECIFIED is used for
* values in default language.
*
* @todo: Add methods for getting original fields and for determining
* changes.
* @todo: Provide a better way for defining default values.
*
* @var array
*/
protected $values = array(
'langcode' => 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'.");
}
$fields = array();
foreach ($this->getPropertyDefinitions() as $name => $definition) {
// Load only translatable properties in strict mode.
if (!empty($definition['translatable']) || !$strict) {
$fields[$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, $fields, 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);
}
}
}
}
}

View File

@ -109,4 +109,37 @@ interface EntityStorageControllerInterface {
*/ */
public function save(EntityInterface $entity); 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);
} }

View File

@ -0,0 +1,81 @@
<?php
/**
* @file
* Definition of Drupal\Core\Entity\Field\FieldInterface.
*/
namespace Drupal\Core\Entity\Field;
use Drupal\Core\TypedData\AccessibleInterface;
use Drupal\Core\TypedData\ContextAwareInterface;
use Drupal\Core\TypedData\ListInterface;
use Drupal\Core\TypedData\TypedDataInterface;
/**
* Interface for fields, being lists of field items.
*
* Contained items must implement the FieldItemInterface. This
* interface is required for every property of an entity. Some methods are
* delegated to the first contained item, in particular get() and set() as well
* as their magic equivalences.
*
* Optionally, a typed data object implementing
* Drupal\Core\TypedData\TypedDataInterface may be passed to
* ArrayAccess::offsetSet() instead of a plain value.
*
* When implementing this interface which extends Traversable, make sure to list
* IteratorAggregate or Iterator before this interface in the implements clause.
*/
interface FieldInterface extends ListInterface, AccessibleInterface, ContextAwareInterface, TypedDataInterface {
/**
* Delegates to the first item.
*
* @see \Drupal\Core\Entity\Field\FieldItemInterface::get()
*/
public function get($property_name);
/**
* Magic getter: Delegates to the first item.
*
* @see \Drupal\Core\Entity\Field\FieldItemInterface::__get()
*/
public function __get($property_name);
/**
* Magic setter: Delegates to the first item.
*
* @see \Drupal\Core\Entity\Field\FieldItemInterface::__set()
*/
public function __set($property_name, $value);
/**
* Magic method for isset(): Delegates to the first item.
*
* @see \Drupal\Core\Entity\Field\FieldItemInterface::__isset()
*/
public function __isset($property_name);
/**
* Magic method for unset(): Delegates to the first item.
*
* @see \Drupal\Core\Entity\Field\FieldItemInterface::__unset()
*/
public function __unset($property_name);
/**
* Delegates to the first item.
*
* @see \Drupal\Core\Entity\Field\FieldItemInterface::getPropertyDefinition()
*/
public function getPropertyDefinition($name);
/**
* Delegates to the first item.
*
* @see \Drupal\Core\Entity\Field\FieldItemInterface::getPropertyDefinitions()
*/
public function getPropertyDefinitions();
}

View File

@ -0,0 +1,278 @@
<?php
/**
* @file
* Definition of Drupal\Core\Entity\Field\FieldItemBase.
*/
namespace Drupal\Core\Entity\Field;
use Drupal\Core\TypedData\Type\TypedData;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\ContextAwareInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\user;
use ArrayIterator;
use IteratorAggregate;
use InvalidArgumentException;
/**
* An entity field item.
*
* Entity field items making use of this base class have to implement
* ComplexDataInterface::getPropertyDefinitions().
*
* @see \Drupal\Core\Entity\Field\FieldItemInterface
*/
abstract class FieldItemBase extends TypedData implements IteratorAggregate, FieldItemInterface {
/**
* The item delta or name.
*
* @var integer
*/
protected $name;
/**
* The parent entity field.
*
* @var \Drupal\Core\Entity\Field\FieldInterface
*/
protected $parent;
/**
* The array of properties.
*
* Field objects are instantiated during object construction and cannot be
* replaced by others, so computed properties can safely store references on
* other properties.
*
* @var array<TypedDataInterface>
*/
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);
}
}
}
}

View File

@ -0,0 +1,73 @@
<?php
/**
* @file
* Definition of Drupal\Core\Entity\Field\FieldItemInterface.
*/
namespace Drupal\Core\Entity\Field;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\ContextAwareInterface;
use Drupal\Core\TypedData\TypedDataInterface;
/**
* Interface for entity field items, which are complex data objects
* containing the values.
*
* When implementing this interface which extends Traversable, make sure to list
* IteratorAggregate or Iterator before this interface in the implements clause.
*
* @see \Drupal\Core\Entity\Field\FieldInterface
* @see \Drupal\Core\Entity\Field\FieldItemBase
*/
interface FieldItemInterface extends ComplexDataInterface, ContextAwareInterface, TypedDataInterface {
/**
* Magic getter: Get the property value.
*
* @param $property_name
* The name of the property to get; e.g., 'title' or 'name'.
*
* @throws \InvalidArgumentException
* If a not existing property is accessed.
*
* @return \Drupal\Core\TypedData\TypedDataInterface
* The property object.
*/
public function __get($property_name);
/**
* Magic setter: Set the property value.
*
* @param $property_name
* The name of the property to set; e.g., 'title' or 'name'.
* @param $value
* The value to set, or NULL to unset the property. Optionally, a typed
* data object implementing Drupal\Core\TypedData\TypedDataInterface may be
* passed instead of a plain value.
*
* @throws \InvalidArgumentException
* If a not existing property is set.
*/
public function __set($property_name, $value);
/**
* Magic method for isset().
*
* @param $property_name
* The name of the property to get; e.g., 'title' or 'name'.
*
* @return boolean
* Returns TRUE if the property exists and is set, FALSE otherwise.
*/
public function __isset($property_name);
/**
* Magic method for unset().
*
* @param $property_name
* The name of the property to get; e.g., 'title' or 'name'.
*/
public function __unset($property_name);
}

View File

@ -0,0 +1,39 @@
<?php
/**
* @file
* Definition of Drupal\Core\Entity\Field\Type\BooleanItem.
*/
namespace Drupal\Core\Entity\Field\Type;
use Drupal\Core\Entity\Field\FieldItemBase;
/**
* Defines the 'boolean_field' entity field item.
*/
class BooleanItem extends FieldItemBase {
/**
* Field definitions of the contained properties.
*
* @see self::getPropertyDefinitions()
*
* @var array
*/
static $propertyDefinitions;
/**
* Implements ComplexDataInterface::getPropertyDefinitions().
*/
public function getPropertyDefinitions() {
if (!isset(self::$propertyDefinitions)) {
self::$propertyDefinitions['value'] = array(
'type' => 'boolean',
'label' => t('Boolean value'),
);
}
return self::$propertyDefinitions;
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* @file
* Definition of Drupal\Core\Entity\Field\Type\DateItem.
*/
namespace Drupal\Core\Entity\Field\Type;
use Drupal\Core\Entity\Field\FieldItemBase;
/**
* Defines the 'date_field' entity field item.
*/
class DateItem extends FieldItemBase {
/**
* Field definitions of the contained properties.
*
* @see self::getPropertyDefinitions()
*
* @var array
*/
static $propertyDefinitions;
/**
* Implements ComplexDataInterface::getPropertyDefinitions().
*/
public function getPropertyDefinitions() {
if (!isset(self::$propertyDefinitions)) {
self::$propertyDefinitions['value'] = array(
'type' => 'date',
'label' => t('Date value'),
);
}
return self::$propertyDefinitions;
}
}

View File

@ -0,0 +1,82 @@
<?php
/**
* @file
* Definition of Drupal\Core\Entity\Field\Type\EntityReferenceItem.
*/
namespace Drupal\Core\Entity\Field\Type;
use Drupal\Core\Entity\Field\FieldItemBase;
use InvalidArgumentException;
/**
* Defines the 'entityreference_field' entity field item.
*
* Required settings (below the definition's 'settings' key) are:
* - entity type: The entity type to reference.
*/
class EntityReferenceItem extends FieldItemBase {
/**
* Field definitions of the contained properties.
*
* @see self::getPropertyDefinitions()
*
* @var array
*/
static $propertyDefinitions;
/**
* Implements ComplexDataInterface::getPropertyDefinitions().
*/
public function getPropertyDefinitions() {
// Definitions vary by entity type, so key them by entity type.
$entity_type = $this->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.');
}
}
}

View File

@ -0,0 +1,246 @@
<?php
/**
* @file
* Definition of Drupal\Core\Entity\Type\EntityTranslation.
*/
namespace Drupal\Core\Entity\Field\Type;
use Drupal\Core\TypedData\Type\TypedData;
use Drupal\Core\TypedData\AccessibleInterface;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\ContextAwareInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use ArrayIterator;
use IteratorAggregate;
use InvalidArgumentException;
/**
* Makes translated entity properties available via the Field API.
*/
class EntityTranslation extends TypedData implements IteratorAggregate, AccessibleInterface, ComplexDataInterface, ContextAwareInterface {
/**
* The array of translated properties, each being an instance of
* FieldInterface.
*
* @var array
*/
protected $properties = array();
/**
* The language code of the translation.
*
* @var string
*/
protected $langcode;
/**
* The parent entity.
*
* @var \Drupal\Core\Entity\EntityInterface
*/
protected $parent;
/**
* Whether the entity translation acts in strict mode.
*
* @var boolean
*/
protected $strict = TRUE;
/**
* Returns whether the entity translation acts in strict mode.
*
* @return boolean
* Whether the entity translation acts in strict mode.
*/
public function getStrictMode() {
return $this->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
}
}

View File

@ -0,0 +1,233 @@
<?php
/**
* @file
* Definition of Drupal\Core\Entity\Field\Type\EntityWrapper.
*/
namespace Drupal\Core\Entity\Field\Type;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\TypedData\Type\TypedData;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\ContextAwareInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use ArrayIterator;
use IteratorAggregate;
use InvalidArgumentException;
/**
* Defines an 'entity' data type, e.g. the computed 'entity' property of entity references.
*
* This object wraps the regular entity object and implements the
* ComplexDataInterface by forwarding most of its methods to the wrapped entity
* (if set).
*
* The plain value of this wrapper is the entity object, i.e. an instance of
* Drupal\Core\Entity\EntityInterface. For setting the value the entity object
* or the entity ID may be passed, whereas passing the ID is only supported if
* an 'entity type' constraint is specified.
*
* Supported constraints (below the definition's 'constraints' key) are:
* - entity type: The entity type.
* - bundle: The bundle or an array of possible bundles.
*
* Supported settings (below the definition's 'settings' key) are:
* - id source: If used as computed property, the ID property used to load
* the entity object.
*/
class EntityWrapper extends TypedData implements IteratorAggregate, ComplexDataInterface, ContextAwareInterface {
/**
* The name.
*
* @var string
*/
protected $name;
/**
* The parent data structure.
*
* @var mixed
*/
protected $parent;
/**
* The referenced entity type.
*
* @var string
*/
protected $entityType;
/**
* The entity ID if no 'id source' is used.
*
* @var string
*/
protected $id;
/**
* Implements TypedDataInterface::__construct().
*/
public function __construct(array $definition) {
$this->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();
}
}

View File

@ -0,0 +1,303 @@
<?php
/**
* @file
* Definition of Drupal\Core\Entity\Field\Type\Field.
*/
namespace Drupal\Core\Entity\Field\Type;
use Drupal\Core\Entity\Field\FieldInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\Core\TypedData\Type\TypedData;
use Drupal\user\User;
use ArrayIterator;
use IteratorAggregate;
use InvalidArgumentException;
/**
* An entity field, i.e. a list of field items.
*
* An entity field is a list of field items, which contain only primitive
* properties or entity references. Note that even single-valued entity
* fields are represented as list of items, however for easy access to the
* contained item the entity field delegates __get() and __set() calls
* directly to the first item.
*
* @see \Drupal\Core\Entity\Field\FieldInterface
*/
class Field extends TypedData implements IteratorAggregate, FieldInterface {
/**
* The entity field name.
*
* @var string
*/
protected $name;
/**
* The parent entity.
*
* @var \Drupal\Core\Entity\EntityInterface
*/
protected $parent;
/**
* Numerically indexed array of field items, implementing the
* FieldItemInterface.
*
* @var array
*/
protected $list = array();
/**
* Implements TypedDataInterface::getValue().
*/
public function getValue() {
$values = array();
foreach ($this->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.
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* @file
* Definition of Drupal\Core\Entity\Field\Type\IntegerItem.
*/
namespace Drupal\Core\Entity\Field\Type;
use Drupal\Core\Entity\Field\FieldItemBase;
/**
* Defines the 'integer_field' entity field item.
*/
class IntegerItem extends FieldItemBase {
/**
* Field definitions of the contained properties.
*
* @see self::getPropertyDefinitions()
*
* @var array
*/
static $propertyDefinitions;
/**
* Implements ComplexDataInterface::getPropertyDefinitions().
*/
public function getPropertyDefinitions() {
if (!isset(self::$propertyDefinitions)) {
self::$propertyDefinitions['value'] = array(
'type' => 'integer',
'label' => t('Integer value'),
);
}
return self::$propertyDefinitions;
}
}

View File

@ -0,0 +1,72 @@
<?php
/**
* @file
* Definition of Drupal\Core\Entity\Field\Type\LanguageItem.
*/
namespace Drupal\Core\Entity\Field\Type;
use Drupal\Core\Entity\Field\FieldItemBase;
use InvalidArgumentException;
/**
* Defines the 'language_field' entity field item.
*/
class LanguageItem extends FieldItemBase {
/**
* Array of property definitions of contained properties.
*
* @see PropertyEntityReferenceItem::getPropertyDefinitions()
*
* @var array
*/
static $propertyDefinitions;
/**
* Implements ComplexDataInterface::getPropertyDefinitions().
*/
public function getPropertyDefinitions() {
if (!isset(self::$propertyDefinitions)) {
self::$propertyDefinitions['value'] = array(
'type' => '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.');
}
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* @file
* Definition of Drupal\Core\Entity\Field\Type\StringItem.
*/
namespace Drupal\Core\Entity\Field\Type;
use Drupal\Core\Entity\Field\FieldItemBase;
/**
* Defines the 'string_field' entity field item.
*/
class StringItem extends FieldItemBase {
/**
* Field definitions of the contained properties.
*
* @see self::getPropertyDefinitions()
*
* @var array
*/
static $propertyDefinitions;
/**
* Implements ComplexDataInterface::getPropertyDefinitions().
*/
public function getPropertyDefinitions() {
if (!isset(self::$propertyDefinitions)) {
self::$propertyDefinitions['value'] = array(
'type' => 'string',
'label' => t('Text value'),
);
}
return self::$propertyDefinitions;
}
}

View File

@ -0,0 +1,26 @@
<?php
/**
* @file
* Definition of Drupal\Core\TypedData\AccessibleInterface.
*/
namespace Drupal\Core\TypedData;
/**
* Interface for checking access.
*/
interface AccessibleInterface {
/**
* Checks data value access.
*
* @param \Drupal\user\User $account
* (optional) The user account to check access for. Defaults to the current
* user.
*
* @return bool
* TRUE if the given user has access; otherwise FALSE.
*/
public function access(\Drupal\user\User $account = NULL);
}

View File

@ -0,0 +1,120 @@
<?php
/**
* @file
* Definition of Drupal\Core\TypedData\ComplexDataInterface.
*/
namespace Drupal\Core\TypedData;
use Traversable;
/**
* Interface for complex data; i.e. data containing named and typed properties.
*
* This is implemented by entities as well as by field item classes of
* entities.
*
* When implementing this interface which extends Traversable, make sure to list
* IteratorAggregate or Iterator before this interface in the implements clause.
*/
interface ComplexDataInterface extends Traversable {
/**
* Gets a property object.
*
* @param $property_name
* The name of the property to get; e.g., 'title' or 'name'.
*
* @throws \InvalidArgumentException
* If an invalid property name is given.
*
* @return \Drupal\Core\TypedData\TypedDataInterface
* The property object.
*/
public function get($property_name);
/**
* Sets a property value.
*
* @param $property_name
* The name of the property to set; e.g., 'title' or 'name'.
* @param $value
* The value to set, or NULL to unset the property.
*
* @throws \InvalidArgumentException
* If the specified property does not exist.
*
* @return \Drupal\Core\TypedData\TypedDataInterface
* The property object.
*/
public function set($property_name, $value);
/**
* Gets an array of property objects.
*
* @param bool $include_computed
* If set to TRUE, computed properties are included. Defaults to FALSE.
*
* @return array
* An array of property objects implementing the TypedDataInterface, keyed
* by property name.
*/
public function getProperties($include_computed = FALSE);
/**
* Gets an array of property values.
*
* Gets an array of plain property values including all not-computed
* properties.
*
* @return array
* An array keyed by property name containing the property value.
*/
public function getPropertyValues();
/**
* Sets multiple property values.
*
* @param array
* The array of property values to set, keyed by property name.
*
* @throws \InvalidArgumentException
* If the value of a not existing property is to be set.
* @throws \Drupal\Core\TypedData\ReadOnlyException
* If a read-only property is set.
*/
public function setPropertyValues($values);
/**
* Gets the definition of a contained property.
*
* @param string $name
* The name of property.
*
* @return array|FALSE
* The definition of the property or FALSE if the property does not exist.
*/
public function getPropertyDefinition($name);
/**
* Gets an array property definitions of contained properties.
*
* @param array $definition
* The definition of the container's property, e.g. the definition of an
* entity reference property.
*
* @return array
* An array of property definitions of contained properties, keyed by
* property name.
*/
public function getPropertyDefinitions();
/**
* Determines whether the data structure is empty.
*
* @return boolean
* TRUE if the data structure is empty, FALSE otherwise.
*/
public function isEmpty();
}

View File

@ -0,0 +1,58 @@
<?php
/**
* @file
* Definition of Drupal\Core\TypedData\ContextAwareInterface.
*/
namespace Drupal\Core\TypedData;
/**
* Interface for context aware data.
*/
interface ContextAwareInterface {
/**
* Returns the name of a property or item.
*
* @return string
* If the data is a property of some complex data, the name of the property.
* If the data is an item of a list, the name is the numeric position of the
* item in the list, starting with 0. Otherwise, NULL is returned.
*/
public function getName();
/**
* Sets the name of a property or item.
*
* This method is supposed to be used by the parental data structure in order
* to provide appropriate context only.
*
* @param string $name
* The name to set for a property or item.
*
* @see ContextAwareInterface::getName()
*/
public function setName($name);
/**
* Returns the parent data structure; i.e. either complex data or a list.
*
* @return Drupal\Core\TypedData\ComplexDataInterface|Drupal\Core\TypedData\ListInterface
* The parent data structure; either complex data or a list.
*/
public function getParent();
/**
* Sets the parent of a property or item.
*
* This method is supposed to be used by the parental data structure in order
* to provide appropriate context only.
*
* @param mixed $parent
* The parent data structure; either complex data or a list.
*
* @see ContextAwareInterface::getParent()
*/
public function setParent($parent);
}

View File

@ -0,0 +1,32 @@
<?php
/**
* @file
* Definition of Drupal\Core\TypedData\ListInterface.
*/
namespace Drupal\Core\TypedData;
use ArrayAccess;
use Countable;
use Traversable;
/**
* Interface for a list of typed data.
*
* A list of typed data contains only items of the same type, is ordered and may
* contain duplicates.
*
* When implementing this interface which extends Traversable, make sure to list
* IteratorAggregate or Iterator before this interface in the implements clause.
*/
interface ListInterface extends ArrayAccess, Countable, Traversable {
/**
* Determines whether the list contains any non-empty items.
*
* @return boolean
* TRUE if the list is empty, FALSE otherwise.
*/
public function isEmpty();
}

View File

@ -0,0 +1,17 @@
<?php
/**
* @file
* Definition of Drupal\Core\TypedData\MissingContextException.
*/
namespace Drupal\Core\TypedData;
use Exception;
/**
* Exception thrown when data wrappers miss contextual information.
*/
class MissingContextException extends Exception {
}

View File

@ -0,0 +1,54 @@
<?php
/**
* @file
* Definition of Drupal\Core\TypedData\Primitive.
*/
namespace Drupal\Core\TypedData;
/**
* Class that holds constants for all primitive data types.
*/
final class Primitive {
/**
* The BOOLEAN primitive data type.
*/
const BOOLEAN = 1;
/**
* The STRING primitive data type.
*/
const STRING = 2;
/**
* The INTEGER primitive data type.
*/
const INTEGER = 3;
/**
* The FLOAT primitive data type.
*/
const FLOAT = 4;
/**
* The DATE primitive data type.
*/
const DATE = 5;
/**
* The DURATION primitive data type.
*/
const DURATION = 6;
/**
* The URI primitive data type.
*/
const URI = 7;
/**
* The BINARY primitive data type.
*/
const BINARY = 8;
}

View File

@ -0,0 +1,17 @@
<?php
/**
* @file
* Definition of Drupal\Core\TypedData\ReadOnlyException.
*/
namespace Drupal\Core\TypedData;
use Exception;
/**
* Exception thrown when trying to write or set ready-only data.
*/
class ReadOnlyException extends Exception {
}

View File

@ -0,0 +1,55 @@
<?php
/**
* @file
* Definition of Drupal\Core\TypedData\TranslatableInterface.
*/
namespace Drupal\Core\TypedData;
/**
* Interface for translatable data.
*/
interface TranslatableInterface {
/**
* Returns the default language.
*
* @return
* The language object.
*/
public function language();
/**
* Returns the languages the data is translated to.
*
* @param bool $include_default
* Whether the default language should be included.
*
* @return
* An array of language objects, keyed by language codes.
*/
public function getTranslationLanguages($include_default = TRUE);
/**
* Gets a translation of the data.
*
* The returned translation has to be implement the same typed data interfaces
* as this typed data object, excluding the TranslatableInterface. E.g., if
* this typed data object implements the ComplexDataInterface and
* AccessibleInterface, the translation object has to implement both as well.
*
* @param $langcode
* The language code of the translation to get or LANGUAGE_DEFAULT to get
* the data in default language.
* @param $strict
* (optional) If the data is complex, whether the translation should include
* only translatable properties. If set to FALSE, untranslatable properties
* are included (in default language) as well as translatable properties in
* the specified language. Defaults to TRUE.
*
* @return \Drupal\Core\TypedData\TypedDataInterface
* A typed data object for the translated data.
*/
public function getTranslation($langcode, $strict = TRUE);
}

View File

@ -0,0 +1,84 @@
<?php
/**
* @file
* Definition of Drupal\Core\TypedData\Type\Binary.
*/
namespace Drupal\Core\TypedData\Type;
use Drupal\Core\TypedData\TypedDataInterface;
use InvalidArgumentException;
/**
* The binary data type.
*
* The plain value of binary data is a PHP file resource, see
* http://php.net/manual/en/language.types.resource.php. For setting the value
* a PHP file resource or a (absolute) stream resource URI may be passed.
*/
class Binary extends TypedData implements TypedDataInterface {
/**
* The file resource URI.
*
* @var string
*/
protected $uri;
/**
* A generic file resource handle.
*
* @var resource
*/
public $handle = NULL;
/**
* Implements TypedDataInterface::getValue().
*/
public function getValue() {
if (!isset($this->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.
}
}

View File

@ -0,0 +1,40 @@
<?php
/**
* @file
* Definition of Drupal\Core\TypedData\Type\Boolean.
*/
namespace Drupal\Core\TypedData\Type;
use Drupal\Core\TypedData\TypedDataInterface;
/**
* The boolean data type.
*
* The plain value of a boolean is a regular PHP boolean. For setting the value
* any PHP variable that casts to a boolean may be passed.
*/
class Boolean extends TypedData implements TypedDataInterface {
/**
* The data value.
*
* @var boolean
*/
protected $value;
/**
* Implements TypedDataInterface::setValue().
*/
public function setValue($value) {
$this->value = isset($value) ? (bool) $value : $value;
}
/**
* Implements TypedDataInterface::validate().
*/
public function validate() {
// TODO: Implement validate() method.
}
}

View File

@ -0,0 +1,62 @@
<?php
/**
* @file
* Definition of Drupal\Core\TypedData\Type\Date.
*/
namespace Drupal\Core\TypedData\Type;
use Drupal\Core\TypedData\TypedDataInterface;
use DateTime;
use InvalidArgumentException;
/**
* The date data type.
*
* The plain value of a date is an instance of the DateTime class. For setting
* the value an instance of the DateTime class, any string supported by
* DateTime::__construct(), or a timestamp as integer may be passed.
*/
class Date extends TypedData implements TypedDataInterface {
/**
* The data value.
*
* @var DateTime
*/
protected $value;
/**
* Implements TypedDataInterface::setValue().
*/
public function setValue($value) {
if ($value instanceof DateTime || !isset($value)) {
$this->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.
}
}

View File

@ -0,0 +1,68 @@
<?php
/**
* @file
* Definition of Drupal\Core\TypedData\Type\Duration.
*/
namespace Drupal\Core\TypedData\Type;
use Drupal\Core\TypedData\TypedDataInterface;
use DateInterval;
use InvalidArgumentException;
/**
* The duration data type.
*
* The plain value of a duration is an instance of the DateInterval class. For
* setting the value an instance of the DateInterval class, a ISO8601 string as
* supported by DateInterval::__construct, or an integer in seconds may be
* passed.
*/
class Duration extends TypedData implements TypedDataInterface {
/**
* The data value.
*
* @var \DateInterval
*/
protected $value;
/**
* Implements TypedDataInterface::setValue().
*/
public function setValue($value) {
if ($value instanceof DateInterval || !isset($value)) {
$this->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.
}
}

View File

@ -0,0 +1,40 @@
<?php
/**
* @file
* Definition of Drupal\Core\TypedData\Type\Float.
*/
namespace Drupal\Core\TypedData\Type;
use Drupal\Core\TypedData\TypedDataInterface;
/**
* The float data type.
*
* The plain value of a float is a regular PHP float. For setting the value
* any PHP variable that casts to a float may be passed.
*/
class Float extends TypedData implements TypedDataInterface {
/**
* The data value.
*
* @var float
*/
protected $value;
/**
* Implements TypedDataInterface::setValue().
*/
public function setValue($value) {
$this->value = isset($value) ? (float) $value : $value;
}
/**
* Implements TypedDataInterface::validate().
*/
public function validate() {
// TODO: Implement validate() method.
}
}

View File

@ -0,0 +1,40 @@
<?php
/**
* @file
* Definition of Drupal\Core\TypedData\Type\Integer.
*/
namespace Drupal\Core\TypedData\Type;
use Drupal\Core\TypedData\TypedDataInterface;
/**
* The integer data type.
*
* The plain value of an integer is a regular PHP integer. For setting the value
* any PHP variable that casts to an integer may be passed.
*/
class Integer extends TypedData implements TypedDataInterface {
/**
* The data value.
*
* @var integer
*/
protected $value;
/**
* Implements TypedDataInterface::setValue().
*/
public function setValue($value) {
$this->value = isset($value) ? (int) $value : $value;
}
/**
* Implements TypedDataInterface::validate().
*/
public function validate() {
// TODO: Implement validate() method.
}
}

View File

@ -0,0 +1,134 @@
<?php
/**
* @file
* Definition of Drupal\Core\TypedData\Type\Language.
*/
namespace Drupal\Core\TypedData\Type;
use Drupal\Core\TypedData\ContextAwareInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use InvalidArgumentException;
/**
* Defines the 'language' data type.
*
* The plain value of a language is the language object, i.e. an instance of
* Drupal\Core\Language\Language. For setting the value the language object or
* the language code as string may be passed.
*
* Optionally, this class may be used as computed property, see the supported
* settings below. E.g., it is used as 'language' property of language items.
*
* Supported settings (below the definition's 'settings' key) are:
* - langcode source: If used as computed property, the langcode property used
* to load the language object.
*/
class Language extends TypedData implements TypedDataInterface, ContextAwareInterface {
/**
* The name.
*
* @var string
*/
protected $name;
/**
* The parent data structure.
*
* @var mixed
*/
protected $parent;
/**
* The language code of the language if no 'langcode source' is used.
*
* @var string
*/
protected $langcode;
/**
* 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 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.
}
}

View File

@ -0,0 +1,40 @@
<?php
/**
* @file
* Definition of Drupal\Core\TypedData\Type\String.
*/
namespace Drupal\Core\TypedData\Type;
use Drupal\Core\TypedData\TypedDataInterface;
/**
* The string data type.
*
* The plain value of a string is a regular PHP string. For setting the value
* any PHP variable that casts to a string may be passed.
*/
class String extends TypedData implements TypedDataInterface {
/**
* The data value.
*
* @var string
*/
protected $value;
/**
* Implements TypedDataInterface::setValue().
*/
public function setValue($value) {
$this->value = isset($value) ? (string) $value : $value;
}
/**
* Implements TypedDataInterface::validate().
*/
public function validate() {
// TODO: Implement validate() method.
}
}

View File

@ -0,0 +1,68 @@
<?php
/**
* @file
* Definition of Drupal\Core\TypedData\Type\TypedData.
*/
namespace Drupal\Core\TypedData\Type;
use Drupal\Core\TypedData\TypedDataInterface;
/**
* The abstract base class for typed data.
*
* Classes deriving from this base class have to declare $value
* or override getValue() or setValue().
*/
abstract class TypedData implements TypedDataInterface {
/**
* The data definition.
*
* @var array
*/
protected $definition;
/**
* Implements TypedDataInterface::__construct().
*/
public function __construct(array $definition) {
$this->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();
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* @file
* Definition of Drupal\Core\TypedData\Type\Uri.
*/
namespace Drupal\Core\TypedData\Type;
use Drupal\Core\TypedData\TypedDataInterface;
/**
* The URI data type.
*
* The plain value of a URI is an absolute URI represented as PHP string.
*/
class Uri extends TypedData implements TypedDataInterface {
/**
* The data value.
*
* @var string
*/
protected $value;
/**
* Implements TypedDataInterface::setValue().
*/
public function setValue($value) {
$this->value = isset($value) ? (string) $value : $value;
}
/**
* Implements TypedDataInterface::validate().
*/
public function validate() {
// TODO: Implement validate() method.
}
}

View File

@ -0,0 +1,52 @@
<?php
/**
* @file
* Definition of Drupal\Core\TypedData\TypedDataFactory.
*/
namespace Drupal\Core\TypedData;
use Drupal\Component\Plugin\Factory\DefaultFactory;
use Drupal\Component\Plugin\Exception\PluginException;
/**
* A factory for typed data objects.
*
* The factory incorporates list classes if the typed data is a list as well as
* class overrides that are specified in data definitions.
*/
class TypedDataFactory extends DefaultFactory {
/**
* Implements Drupal\Component\Plugin\Factory\FactoryInterface::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) {
$type_definition = $this->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);
}
}

View File

@ -0,0 +1,73 @@
<?php
/**
* @file
* Definition of Drupal\Core\TypedData\TypedDataInterface.
*/
namespace Drupal\Core\TypedData;
use Drupal\user;
/**
* Interface for typed data objects.
*/
interface TypedDataInterface {
/**
* Creates a typed data object given its definition.
*
* @param array $definition
* The data definition.
*
* @see Drupal\Core\TypedData\TypedDataManager::create()
*/
public function __construct(array $definition);
/**
* Gets the data type.
*
* @return string
* The data type of the wrapped data.
*/
public function getType();
/**
* Gets the data definition.
*
* @return array
* The data definition array.
*/
public function getDefinition();
/**
* Gets the data value.
*
* @return mixed
*/
public function getValue();
/**
* Sets the data value.
*
* @param mixed $value
* The value to set in the format as documented for the data type or NULL to
* unset the data value.
*
* @throws \Drupal\Core\TypedData\ReadOnlyException
* If the data is read-only.
*/
public function setValue($value);
/**
* Returns a string representation of the data.
*
* @return string
*/
public function getString();
/**
* Validates the currently set data value.
*/
public function validate();
}

View File

@ -0,0 +1,111 @@
<?php
/**
* @file
* Definition of Drupal\Core\TypedData\TypedDataManager.
*/
namespace Drupal\Core\TypedData;
use Drupal\Component\Plugin\PluginManagerBase;
use Drupal\Core\Plugin\Discovery\CacheDecorator;
use Drupal\Core\Plugin\Discovery\HookDiscovery;
/**
* Manages data type plugins.
*/
class TypedDataManager extends PluginManagerBase {
public function __construct() {
$this->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;
}
}

View File

@ -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 // 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 // languages get a default value. Otherwise we could have default values for
// not yet open languages. // 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)) { (isset($entity->{$field['field_name']}[$langcode]) && count($entity->{$field['field_name']}[$langcode]) == 0)) {
$items = field_get_default_value($entity_type, $entity, $field, $instance, $langcode); $items = field_get_default_value($entity_type, $entity, $field, $instance, $langcode);
} }

View File

@ -364,6 +364,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. * Applies language fallback rules to the fields attached to the given entity.
* *

View File

@ -0,0 +1,124 @@
<?php
/**
* @file
* Definition of Drupal\text\TextProcessed.
*/
namespace Drupal\text;
use Drupal\Core\TypedData\ContextAwareInterface;
use Drupal\Core\TypedData\Type\String;
use Drupal\Core\TypedData\ReadOnlyException;
use InvalidArgumentException;
/**
* A computed property for processing text with a format.
*
* Required settings (below the definition's 'settings' key) are:
* - text source: The text property containing the to be processed text.
*/
class TextProcessed extends String implements ContextAwareInterface {
/**
* The text property.
*
* @var \Drupal\Core\TypedData\TypedDataInterface
*/
protected $text;
/**
* The text format property.
*
* @var \Drupal\Core\TypedData\TypedDataInterface
*/
protected $format;
/**
* The name.
*
* @var string
*/
protected $name;
/**
* The parent data structure.
*
* @var \Drupal\Core\Entity\Field\FieldItemInterface
*/
protected $parent;
/**
* Implements TypedDataInterface::__construct().
*/
public function __construct(array $definition) {
$this->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.');
}
}
}

View File

@ -0,0 +1,53 @@
<?php
/**
* @file
* Definition of Drupal\text\Type\TextItem.
*/
namespace Drupal\text\Type;
use Drupal\Core\Entity\Field\FieldItemBase;
/**
* Defines the 'text_item' and 'text_long_item' entity field items.
*/
class TextItem extends FieldItemBase {
/**
* Field definitions of the contained properties.
*
* @see self::getPropertyDefinitions()
*
* @var array
*/
static $propertyDefinitions;
/**
* Implements ComplexDataInterface::getPropertyDefinitions().
*/
public function getPropertyDefinitions() {
if (!isset(self::$propertyDefinitions)) {
self::$propertyDefinitions['value'] = array(
'type' => '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;
}
}

View File

@ -0,0 +1,50 @@
<?php
/**
* @file
* Definition of Drupal\text\Type\TextSummaryItem.
*/
namespace Drupal\text\Type;
/**
* Defines the 'text_with_summary' entity field item.
*/
class TextSummaryItem extends TextItem {
/**
* Field definitions of the contained properties.
*
* @see self::getPropertyDefinitions()
*
* @var array
*/
static $propertyDefinitions;
/**
* Implements ComplexDataInterface::getPropertyDefinitions().
*/
public function getPropertyDefinitions() {
if (!isset(self::$propertyDefinitions)) {
self::$propertyDefinitions = parent::getPropertyDefinitions();
self::$propertyDefinitions['summary'] = array(
'type' => '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;
}
}

View File

@ -38,6 +38,7 @@ function text_field_info() {
'instance_settings' => array('text_processing' => 0), 'instance_settings' => array('text_processing' => 0),
'default_widget' => 'text_textfield', 'default_widget' => 'text_textfield',
'default_formatter' => 'text_default', 'default_formatter' => 'text_default',
'field item class' => '\Drupal\text\Type\TextItem',
), ),
'text_long' => array( 'text_long' => array(
'label' => t('Long text'), 'label' => t('Long text'),
@ -45,6 +46,7 @@ function text_field_info() {
'instance_settings' => array('text_processing' => 0), 'instance_settings' => array('text_processing' => 0),
'default_widget' => 'text_textarea', 'default_widget' => 'text_textarea',
'default_formatter' => 'text_default', 'default_formatter' => 'text_default',
'field item class' => '\Drupal\text\Type\TextItem',
), ),
'text_with_summary' => array( 'text_with_summary' => array(
'label' => t('Long text and summary'), 'label' => t('Long text and summary'),
@ -52,6 +54,7 @@ function text_field_info() {
'instance_settings' => array('text_processing' => 1, 'display_summary' => 0), 'instance_settings' => array('text_processing' => 1, 'display_summary' => 0),
'default_widget' => 'text_textarea_with_summary', 'default_widget' => 'text_textarea_with_summary',
'default_formatter' => 'text_default', 'default_formatter' => 'text_default',
'field item class' => '\Drupal\text\Type\TextSummaryItem',
), ),
); );
} }

View File

@ -2032,6 +2032,25 @@ abstract class WebTestBase extends TestBase {
$this->drupalSettings = $settings; $this->drupalSettings = $settings;
} }
/**
* 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. * Pass if the internal browser's URL matches the given path.
* *

View File

@ -36,21 +36,20 @@ class EntityApiTest extends WebTestBase {
$user1 = $this->drupalCreateUser(); $user1 = $this->drupalCreateUser();
// Create some test entities. // 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->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->save();
$entity = entity_create('entity_test', array('name' => 'test', 'uid' => NULL)); $entity = entity_create('entity_test', array('name' => 'test', 'user_id' => NULL));
$entity->save(); $entity->save();
$entities = array_values(entity_load_multiple_by_properties('entity_test', array('name' => 'test'))); $entities = array_values(entity_load_multiple_by_properties('entity_test', array('name' => 'test')));
$this->assertEqual($entities[0]->name->value, 'test', 'Created and loaded entity.');
$this->assertEqual($entities[0]->get('name'), 'test', 'Created and loaded entity.'); $this->assertEqual($entities[1]->name->value, 'test', 'Created and loaded entity.');
$this->assertEqual($entities[1]->get('name'), 'test', 'Created and loaded entity.');
// Test loading a single entity. // Test loading a single entity.
$loaded_entity = entity_test_load($entity->id); $loaded_entity = entity_test_load($entity->id());
$this->assertEqual($loaded_entity->id, $entity->id, 'Loaded a single entity by id.'); $this->assertEqual($loaded_entity->id(), $entity->id(), 'Loaded a single entity by id.');
// Test deleting an entity. // Test deleting an entity.
$entities = array_values(entity_load_multiple_by_properties('entity_test', array('name' => 'test2'))); $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. // Test updating an entity.
$entities = array_values(entity_load_multiple_by_properties('entity_test', array('name' => 'test'))); $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(); $entities[0]->save();
$entity = entity_test_load($entities[0]->id); $entity = entity_test_load($entities[0]->id());
$this->assertEqual($entity->get('name'), 'test3', 'Entity updated.'); $this->assertEqual($entity->name->value, 'test3', 'Entity updated.');
// Try deleting multiple test entities by deleting all. // Try deleting multiple test entities by deleting all.
$ids = array_keys(entity_test_load_multiple()); $ids = array_keys(entity_test_load_multiple());
@ -72,26 +71,4 @@ class EntityApiTest extends WebTestBase {
$all = entity_test_load_multiple(); $all = entity_test_load_multiple();
$this->assertTrue(empty($all), 'Deleted all entities.'); $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.');
}
} }

View File

@ -0,0 +1,395 @@
<?php
/**
* @file
* Definition of Drupal\Core\Entity\Tests\EntityFieldTest.
*/
namespace Drupal\system\Tests\Entity;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Field\FieldInterface;
use Drupal\Core\Entity\Field\FieldItemInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\simpletest\WebTestBase;
/**
* Tests Entity API base functionality.
*/
class EntityFieldTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('entity_test');
public static function getInfo() {
return array(
'name' => '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 <strong>text</strong> text to filter.";
$entity->field_test_text->format = filter_default_format();
$target = "<p>The &lt;strong&gt;text&lt;/strong&gt; text to filter.</p>\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.');
}
}

View File

@ -45,7 +45,7 @@ class EntityFormTest extends WebTestBase {
$edit = array( $edit = array(
'name' => $name1, 'name' => $name1,
'uid' => mt_rand(0, 128), 'user_id' => mt_rand(0, 128),
"field_test_text[$langcode][0][value]" => $this->randomName(16), "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.'); $this->assertFalse($entity, 'The entity has been modified.');
$entity = $this->loadEntityByName($name2); $entity = $this->loadEntityByName($name2);
$this->assertTrue($entity, 'Modified entity found in the database.'); $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')); $this->drupalPost('entity-test/manage/' . $entity->id() . '/edit', array(), t('Delete'));
$entity = $this->loadEntityByName($name2); $entity = $this->loadEntityByName($name2);

View File

@ -113,7 +113,7 @@ class EntityTranslationFormTest extends WebTestBase {
// Create a body translation and check the form language. // Create a body translation and check the form language.
$langcode2 = $this->langcodes[1]; $langcode2 = $this->langcodes[1];
$node->set('body', array(array('value' => $this->randomName(16))), $langcode2); $node->body[$langcode2][0]['value'] = $this->randomName(16);
$node->save(); $node->save();
$this->drupalGet($langcode2 . '/node/' . $node->nid . '/edit'); $this->drupalGet($langcode2 . '/node/' . $node->nid . '/edit');
$form_langcode = variable_get('entity_form_langcode', FALSE); $form_langcode = variable_get('entity_form_langcode', FALSE);

View File

@ -78,76 +78,99 @@ class EntityTranslationTest extends WebTestBase {
function testEntityLanguageMethods() { function testEntityLanguageMethods() {
$entity = entity_create('entity_test', array( $entity = entity_create('entity_test', array(
'name' => 'test', 'name' => 'test',
'uid' => $GLOBALS['user']->uid, 'user_id' => $GLOBALS['user']->uid,
)); ));
$this->assertEqual($entity->language()->langcode, LANGUAGE_NOT_SPECIFIED, 'Entity language not specified.'); $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. // Set the value in default language.
$entity->set($this->field_name, array(0 => array('value' => 'default value'))); $entity->set($this->field_name, array(0 => array('value' => 'default value')));
// Get the value. // Get the value.
$value = $entity->get($this->field_name); $this->assertEqual($entity->getTranslation(LANGUAGE_DEFAULT)->get($this->field_name)->value, 'default value', 'Untranslated value retrieved.');
$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";
// Set the value in a certain language. As the entity is not // Set the value in a certain language. As the entity is not
// language-specific it will throw an exception. // language-specific it should use the default language and so ignore the
try { // specified language.
$entity->set($this->field_name, array(0 => array('value' => 'default value2')), $this->langcodes[1]); $entity->getTranslation($this->langcodes[1])->set($this->field_name, array(0 => array('value' => 'default value2')));
$this->fail($message); $this->assertEqual($entity->get($this->field_name)->value, 'default value2', 'Untranslated value updated.');
} $this->assertFalse($entity->getTranslationLanguages(FALSE), 'No translations are available');
catch (Exception $e) {
$this->assertTrue($e instanceof InvalidArgumentException, $message);
}
// Test getting a field value using a specific language for a not // Test getting a field value using a specific language for a not
// language-specific entity. // language-specific entity.
$value = $entity->get($this->field_name, $this->langcodes[1]); $this->assertEqual($entity->getTranslation($this->langcodes[1])->get($this->field_name)->value, 'default value2', 'Untranslated value retrieved.');
$this->assertNull($value, 'Returned NULL for getter with invalid language.');
// Now, make the entity language-specific by assigning a language and test // Now, make the entity language-specific by assigning a language and test
// translating it. // translating it.
$entity->setLangcode($this->langcodes[0]); $entity->langcode->value = $this->langcodes[0];
$entity->{$this->field_name} = array(); $entity->{$this->field_name} = array();
$this->assertEqual($entity->language(), language_load($this->langcodes[0]), 'Entity language retrieved.'); $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. // Set the value in default language.
$entity->set($this->field_name, array(0 => array('value' => 'default value'))); $entity->set($this->field_name, array(0 => array('value' => 'default value')));
// Get the value. // Get the value.
$value = $entity->get($this->field_name); $this->assertEqual($entity->get($this->field_name)->value, 'default value', 'Untranslated value retrieved.');
$this->assertEqual($value, array(0 => array('value' => 'default value')), 'Untranslated value retrieved.');
// Set a translation. // Set a translation.
$entity->set($this->field_name, array(0 => array('value' => 'translation 1')), $this->langcodes[1]); $entity->getTranslation($this->langcodes[1])->set($this->field_name, array(0 => array('value' => 'translation 1')));
$value = $entity->get($this->field_name, $this->langcodes[1]); $this->assertEqual($entity->getTranslation($this->langcodes[1])->{$this->field_name}->value, 'translation 1', 'Translated value set.');
$this->assertEqual($value, array(0 => array('value' => 'translation 1')), 'Translated value set.');
// Make sure the untranslated value stays. // Make sure the untranslated value stays.
$value = $entity->get($this->field_name); $this->assertEqual($entity->get($this->field_name)->value, 'default value', 'Untranslated value stays.');
$this->assertEqual($value, array(0 => array('value' => 'default value')), 'Untranslated value stays.');
$translations[$this->langcodes[1]] = language_load($this->langcodes[1]); $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. // Try to get a not available translation.
$value = $entity->get($this->field_name, $this->langcodes[2]); $this->assertNull($entity->getTranslation($this->langcodes[2])->get($this->field_name)->value, 'A translation that is not available is NULL.');
$this->assertNull($value, 'A translation that is not available is NULL.');
// Try to get a value using an invalid language code. // Try to get a value using an invalid language code.
$value = $entity->get($this->field_name, 'invalid'); try {
$this->assertNull($value, 'A translation for an invalid language is NULL.'); $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. // Try to set a value using an invalid language code.
$message = "An exception is thrown when trying to set an invalid translation.";
try { try {
$entity->set($this->field_name, NULL, 'invalid'); $entity->getTranslation('invalid')->set($this->field_name, NULL);
// This line is not expected to be executed unless something goes wrong. $this->fail("Setting a translation for an invalid language throws an exception.");
$this->fail($message);
} }
catch (Exception $e) { catch (InvalidArgumentException $e) {
$this->assertTrue($e instanceof InvalidArgumentException, $message); $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 (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 // Create a language neutral entity and check that properties are stored
// as language neutral. // 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->save();
$entity = entity_test_load($entity->id()); $entity = entity_test_load($entity->id());
$this->assertEqual($entity->language()->langcode, LANGUAGE_NOT_SPECIFIED, 'Entity created as language neutral.'); $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($name, $entity->getTranslation(LANGUAGE_DEFAULT)->get('name')->value, '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->assertEqual($uid, $entity->getTranslation(LANGUAGE_DEFAULT)->get('user_id')->value, '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.'); // As fields, translatable properties should ignore the given langcode and
$this->assertNull($entity->get('uid', $langcode), 'The entity author is not available as a language-aware property.'); // use neutral language if the entity is not translatable.
$this->assertEqual($name, $entity->get('name'), 'The entity name can be retrieved without specifying a language.'); $this->assertEqual($name, $entity->getTranslation($langcode)->get('name')->value, 'The entity name defaults to neutral language.');
$this->assertEqual($uid, $entity->get('uid'), 'The entity author can be retrieved without specifying a 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 // Create a language-aware entity and check that properties are stored
// as language-aware. // 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->save();
$entity = entity_test_load($entity->id()); $entity = entity_test_load($entity->id());
$this->assertEqual($entity->language()->langcode, $langcode, 'Entity created as language specific.'); $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($name, $entity->getTranslation($langcode)->get('name')->value, '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->assertEqual($uid, $entity->getTranslation($langcode)->get('user_id')->value, '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.'); // Translatable properties on a translatable entity should use default
$this->assertNull($entity->get('uid', LANGUAGE_NOT_SPECIFIED), 'The entity author is not available as a language neutral property.'); // language if LANGUAGE_NOT_SPECIFIED is passed.
$this->assertEqual($name, $entity->get('name'), 'The entity name can be retrieved without specifying a language.'); $this->assertEqual($name, $entity->getTranslation(LANGUAGE_NOT_SPECIFIED)->get('name')->value, 'The entity name defaults to the default language.');
$this->assertEqual($uid, $entity->get('uid'), 'The entity author can be retrieved without specifying a 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. // Create property translations.
$properties = array(); $properties = array();
@ -190,17 +217,17 @@ class EntityTranslationTest extends WebTestBase {
foreach ($this->langcodes as $langcode) { foreach ($this->langcodes as $langcode) {
if ($langcode != $default_langcode) { if ($langcode != $default_langcode) {
$properties[$langcode] = array( $properties[$langcode] = array(
'name' => $this->randomName(), 'name' => array(0 => $this->randomName()),
'uid' => mt_rand(0, 127), 'user_id' => array(0 => mt_rand(0, 127)),
); );
} }
else { else {
$properties[$langcode] = array( $properties[$langcode] = array(
'name' => $name, 'name' => array(0 => $name),
'uid' => $uid, 'user_id' => array(0 => $uid),
); );
} }
$entity->setProperties($properties[$langcode], $langcode); $entity->getTranslation($langcode)->setPropertyValues($properties[$langcode]);
} }
$entity->save(); $entity->save();
@ -208,8 +235,8 @@ class EntityTranslationTest extends WebTestBase {
$entity = entity_test_load($entity->id()); $entity = entity_test_load($entity->id());
foreach ($this->langcodes as $langcode) { foreach ($this->langcodes as $langcode) {
$args = array('%langcode' => $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]['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]['uid'], $entity->get('uid', $langcode), format_string('The entity author 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). // 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 // Create an additional entity with only the uid set. The uid for the
// original language is the same of one used for a translation. // original language is the same of one used for a translation.
$langcode = $this->langcodes[1]; $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(); $entities = entity_test_load_multiple();
$this->assertEqual(count($entities), 3, 'Three entities were created.'); $this->assertEqual(count($entities), 3, 'Three entities were created.');
$entities = entity_test_load_multiple(array($translated_id)); $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.'); $this->assertEqual(count($entities), 2, 'Two entities correctly loaded by name.');
// @todo The default language condition should go away in favor of an // @todo The default language condition should go away in favor of an
// explicit parameter. // 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.'); $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)); $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.'); $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.'); $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.'); $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.'); $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 // Test property conditions and orders with multiple languages in the same
@ -242,7 +274,7 @@ class EntityTranslationTest extends WebTestBase {
$query = new EntityFieldQuery(); $query = new EntityFieldQuery();
$query->entityCondition('entity_type', 'entity_test'); $query->entityCondition('entity_type', 'entity_test');
$query->entityCondition('langcode', $default_langcode); $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->propertyCondition('name', $properties[$default_langcode]['name'], NULL, 'original');
$query->propertyLanguageCondition($default_langcode, NULL, 'original'); $query->propertyLanguageCondition($default_langcode, NULL, 'original');
$query->propertyCondition('name', $properties[$langcode]['name'], NULL, 'translation'); $query->propertyCondition('name', $properties[$langcode]['name'], NULL, 'translation');
@ -254,12 +286,12 @@ class EntityTranslationTest extends WebTestBase {
// Test mixed property and field conditions. // Test mixed property and field conditions.
$entity = entity_load('entity_test', key($result['entity_test']), TRUE); $entity = entity_load('entity_test', key($result['entity_test']), TRUE);
$field_value = $this->randomString(); $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(); $entity->save();
$query = new EntityFieldQuery(); $query = new EntityFieldQuery();
$query->entityCondition('entity_type', 'entity_test'); $query->entityCondition('entity_type', 'entity_test');
$query->entityCondition('langcode', $default_langcode); $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->propertyCondition('name', $properties[$default_langcode]['name'], NULL, 'original');
$query->propertyLanguageCondition($default_langcode, NULL, 'original'); $query->propertyLanguageCondition($default_langcode, NULL, 'original');
$query->propertyCondition('name', $properties[$langcode]['name'], NULL, 'translation'); $query->propertyCondition('name', $properties[$langcode]['name'], NULL, 'translation');

View File

@ -64,7 +64,7 @@ class EntityUUIDTest extends WebTestBase {
// Verify that entity_load_by_uuid() loads the same entity. // Verify that entity_load_by_uuid() loads the same entity.
$entity_loaded_by_uuid = entity_load_by_uuid('entity_test', $uuid, TRUE); $entity_loaded_by_uuid = entity_load_by_uuid('entity_test', $uuid, TRUE);
$this->assertIdentical($entity_loaded_by_uuid->uuid(), $uuid); $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. // Creating a duplicate needs to result in a new UUID.
$entity_duplicate = $entity->createDuplicate(); $entity_duplicate = $entity->createDuplicate();

View File

@ -0,0 +1,130 @@
<?php
/**
* @file
* Definition of Drupal\system\Tests\TypedData\TypedDataTest.
*/
namespace Drupal\system\Tests\TypedData;
use Drupal\simpletest\WebTestBase;
use DateTime;
use DateInterval;
/**
* Tests primitive data types.
*/
class TypedDataTest extends WebTestBase {
public static function getInfo() {
return array(
'name' => '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.');
}
}

View File

@ -146,6 +146,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. * Declare queues holding items that need to be run periodically.
* *

View File

@ -6,6 +6,7 @@
*/ */
use Drupal\Core\Utility\ModuleInfo; use Drupal\Core\Utility\ModuleInfo;
use Drupal\Core\TypedData\Primitive;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
@ -1941,6 +1942,105 @@ function system_stream_wrappers() {
return $wrappers; return $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. * Menu item access callback - only enabled themes can be accessed.
*/ */

View File

@ -3,4 +3,5 @@ description = Provides entity types based upon the CRUD API.
package = Testing package = Testing
version = VERSION version = VERSION
core = 8.x core = 8.x
dependencies[] = field
hidden = TRUE hidden = TRUE

View File

@ -91,7 +91,7 @@ function entity_test_schema() {
'not null' => TRUE, 'not null' => TRUE,
'default' => '', 'default' => '',
), ),
'uid' => array( 'user_id' => array(
'type' => 'int', 'type' => 'int',
'unsigned' => TRUE, 'unsigned' => TRUE,
'not null' => FALSE, 'not null' => FALSE,
@ -100,10 +100,10 @@ function entity_test_schema() {
), ),
), ),
'indexes' => array( 'indexes' => array(
'uid' => array('uid'), 'user_id' => array('user_id'),
), ),
'foreign keys' => array( 'foreign keys' => array(
'uid' => array('users' => 'uid'), 'user_id' => array('users' => 'uid'),
'id' => array('entity_test' => 'id'), 'id' => array('entity_test' => 'id'),
), ),
'primary key' => array('id', 'langcode'), 'primary key' => array('id', 'langcode'),

View File

@ -86,7 +86,7 @@ function entity_test_add() {
* Menu callback: displays the 'Edit existing entity_test' form. * Menu callback: displays the 'Edit existing entity_test' form.
*/ */
function entity_test_edit($entity) { 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); return entity_get_form($entity);
} }

View File

@ -7,137 +7,52 @@
namespace Drupal\entity_test; namespace Drupal\entity_test;
use InvalidArgumentException; use Drupal\Core\Entity\EntityNG;
use Drupal\Core\Entity\Entity;
/** /**
* Defines the test entity class. * 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 * @var \Drupal\Core\Entity\Field\FieldInterface
*/ */
protected static $langcodes; public $uuid;
/** /**
* Constructs a new entity object. * The name of the test entity.
*
* @var \Drupal\Core\Entity\Field\FieldInterface
*/
public $name;
/**
* The associated user.
*
* @var \Drupal\Core\Entity\Field\FieldInterface
*/
public $user_id;
/**
* Overrides Entity::__construct().
*/ */
public function __construct(array $values, $entity_type) { public function __construct(array $values, $entity_type) {
parent::__construct($values, $entity_type); parent::__construct($values, $entity_type);
if (!isset(self::$langcodes)) { // We unset all defined properties, so magic getters apply.
// The allowed languages are simply all the available ones in the system. unset($this->id);
self::$langcodes = drupal_map_assoc(array_keys(language_list(LANGUAGE_ALL))); unset($this->langcode);
} unset($this->uuid);
unset($this->name);
// Initialize the original entity language with the provided value or fall unset($this->user_id);
// 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);
}
/**
* Sets the entity original langcode.
*
* @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().
*/
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();
}
/**
* Returns the property array for the given language.
*
* @param string $langcode
* (optional) The language code to be used to retrieve the properties.
*/
public function getProperties($langcode = NULL) {
$langcode = !empty($langcode) ? $langcode : $this->langcode;
return isset($this->properties[$langcode]) ? $this->properties[$langcode] : array();
}
/**
* 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.
*/
public function setProperties(array $properties, $langcode = NULL) {
$langcode = !empty($langcode) ? $langcode : $this->langcode;
$this->properties[$langcode] = $properties;
} }
} }

View File

@ -7,12 +7,12 @@
namespace Drupal\entity_test; namespace Drupal\entity_test;
use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityFormController; use Drupal\Core\Entity\EntityFormControllerNG;
/** /**
* Form controller for the test entity edit forms. * Form controller for the test entity edit forms.
*/ */
class EntityTestFormController extends EntityFormController { class EntityTestFormController extends EntityFormControllerNG {
/** /**
* Overrides Drupal\Core\Entity\EntityFormController::form(). * Overrides Drupal\Core\Entity\EntityFormController::form().
@ -21,23 +21,22 @@ class EntityTestFormController extends EntityFormController {
$form = parent::form($form, $form_state, $entity); $form = parent::form($form, $form_state, $entity);
$langcode = $this->getFormLangcode($form_state); $langcode = $this->getFormLangcode($form_state);
$name = $entity->get('name', $langcode); $translation = $entity->getTranslation($langcode);
$uid = $entity->get('uid', $langcode);
$form['name'] = array( $form['name'] = array(
'#type' => 'textfield', '#type' => 'textfield',
'#title' => t('Name'), '#title' => t('Name'),
'#default_value' => !empty($name) ? $name : '', '#default_value' => $translation->name->value,
'#size' => 60, '#size' => 60,
'#maxlength' => 128, '#maxlength' => 128,
'#required' => TRUE, '#required' => TRUE,
'#weight' => -10, '#weight' => -10,
); );
$form['uid'] = array( $form['user_id'] = array(
'#type' => 'textfield', '#type' => 'textfield',
'#title' => 'UID', '#title' => 'UID',
'#default_value' => !empty($uid) ? $uid : '', '#default_value' => $translation->user_id->value,
'#size' => 60, '#size' => 60,
'#maxlength' => 128, '#maxlength' => 128,
'#required' => TRUE, '#required' => TRUE,
@ -53,12 +52,11 @@ class EntityTestFormController extends EntityFormController {
public function submit(array $form, array &$form_state) { public function submit(array $form, array &$form_state) {
$entity = parent::submit($form, $form_state); $entity = parent::submit($form, $form_state);
$langcode = $this->getFormLangcode($form_state); $langcode = $this->getFormLangcode($form_state);
// Updates multilingual properties. // Updates multilingual properties.
foreach (array('name', 'uid') as $property) { $translation = $entity->getTranslation($langcode);
$entity->set($property, $form_state['values'][$property], $langcode); foreach (array('name', 'user_id') as $name) {
$translation->$name->setValue($form_state['values'][$name]);
} }
return $entity; return $entity;
} }

View File

@ -9,9 +9,9 @@ namespace Drupal\entity_test;
use PDO; use PDO;
use Drupal\Core\Entity\DatabaseStorageController;
use Drupal\Core\Entity\EntityFieldQuery;
use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\DatabaseStorageControllerNG;
use Drupal\Core\Entity\EntityFieldQuery;
/** /**
* Defines the controller class for the test entity. * 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 * This extends the Drupal\Core\Entity\DatabaseStorageController class, adding
* required special handling for test entities. * 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) { protected function buildPropertyQuery(EntityFieldQuery $entity_query, array $values) {
// @todo We should not be using a condition to specify whether conditions // @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 attachLoad(&$queried_entities, $load_revision = FALSE) { 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 attachPropertyData(&$queried_entities) {
$data = db_select('entity_test_property_data', 'data', array('fetch' => PDO::FETCH_ASSOC)) $data = db_select('entity_test_property_data', 'data', array('fetch' => PDO::FETCH_ASSOC))
->fields('data') ->fields('data')
->condition('id', array_keys($queried_entities)) ->condition('id', array_keys($queried_entities))
@ -53,35 +67,34 @@ class EntityTestStorageController extends DatabaseStorageController {
->execute(); ->execute();
foreach ($data as $values) { foreach ($data as $values) {
$entity = $queried_entities[$values['id']]; $id = $values['id'];
$langcode = $values['langcode']; // Field values in default language are stored with
if (!empty($values['default_langcode'])) { // LANGUAGE_DEFAULT as key.
$entity->setLangcode($langcode); $langcode = empty($values['default_langcode']) ? $values['langcode'] : LANGUAGE_DEFAULT;
}
// Make sure only real properties are stored.
unset($values['id'], $values['default_langcode']);
$entity->setProperties($values, $langcode);
}
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(). * Overrides Drupal\Core\Entity\DatabaseStorageController::postSave().
*
* Stores values of translatable properties.
*/ */
protected function postSave(EntityInterface $entity, $update) { protected function postSave(EntityInterface $entity, $update) {
$default_langcode = ($language = $entity->language()) ? $language->langcode : LANGUAGE_NOT_SPECIFIED; $default_langcode = $entity->language()->langcode;
$langcodes = array_keys($entity->translations());
$langcodes[] = $default_langcode;
foreach ($langcodes as $langcode) { foreach ($entity->getTranslationLanguages() as $langcode => $language) {
$properties = $entity->getProperties($langcode); $translation = $entity->getTranslation($langcode);
$values = array( $values = array(
'id' => $entity->id(), 'id' => $entity->id(),
'langcode' => $langcode, 'langcode' => $langcode,
'default_langcode' => intval($default_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') db_merge('entity_test_property_data')
->fields($values) ->fields($values)
@ -99,4 +112,40 @@ class EntityTestStorageController extends DatabaseStorageController {
->condition('id', array_keys($entities)) ->condition('id', array_keys($entities))
->execute(); ->execute();
} }
/**
* Implements \Drupal\Core\Entity\DataBaseStorageControllerNG::baseFieldDefinitions().
*/
public function baseFieldDefinitions() {
$fields['id'] = array(
'label' => t('ID'),
'description' => t('The ID of the test entity.'),
'type' => 'integer_field',
'read-only' => TRUE,
);
$fields['uuid'] = array(
'label' => t('UUID'),
'description' => t('The UUID of the test entity.'),
'type' => 'string_field',
);
$fields['langcode'] = array(
'label' => t('Language code'),
'description' => t('The language code of the test entity.'),
'type' => 'language_field',
);
$fields['name'] = array(
'label' => t('Name'),
'description' => t('The name of the test entity.'),
'type' => 'string_field',
'translatable' => TRUE,
);
$fields['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 $fields;
}
} }