Commit 6824ba0e authored by Dries's avatar Dries

Issue #1696640 by fago, effulgentsia, tim.plunkett, dixon_, plach: Add a...

Issue #1696640 by fago, effulgentsia, tim.plunkett, dixon_, plach: Add a uniform Entity Property API.
parent b9af7fe8
......@@ -222,6 +222,14 @@
*/
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.
*/
......@@ -2475,6 +2483,19 @@ function state() {
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.
*
......
......@@ -466,3 +466,68 @@ function hook_entity_view_mode_alter(&$view_mode, Drupal\Core\Entity\EntityInter
$view_mode = 'my_custom_view_mode';
}
}
/**
* Define custom entity properties.
*
* @param string $entity_type
* The entity type for which to define entity properties.
*
* @return array
* An array of property information having the following optional entries:
* - definitions: An array of property definitions to add all entities of this
* type, keyed by property name. See
* Drupal\Core\TypedData\TypedDataManager::create() for a list of supported
* keys in property definitions.
* - optional: An array of property definitions for optional properties keyed
* by property name. Optional properties are properties that only exist for
* certain bundles of the entity type.
* - bundle map: An array keyed by bundle name containing the names of
* optional properties that entities of this bundle have.
*
* @see Drupal\Core\TypedData\TypedDataManager::create()
* @see hook_entity_field_info_alter()
* @see Drupal\Core\Entity\StorageControllerInterface::getPropertyDefinitions()
*/
function hook_entity_field_info($entity_type) {
if (mymodule_uses_entity_type($entity_type)) {
$info = array();
$info['definitions']['mymodule_text'] = array(
'type' => 'string_item',
'list' => TRUE,
'label' => t('The text'),
'description' => t('A text property added by mymodule.'),
'computed' => TRUE,
'class' => '\Drupal\mymodule\EntityComputedText',
);
if ($entity_type == 'node') {
// Add a property only to entities of the 'article' bundle.
$info['optional']['mymodule_text_more'] = array(
'type' => 'string_item',
'list' => TRUE,
'label' => t('More text'),
'computed' => TRUE,
'class' => '\Drupal\mymodule\EntityComputedMoreText',
);
$info['bundle map']['article'][0] = 'mymodule_text_more';
}
return $info;
}
}
/**
* Alter defined entity properties.
*
* @param array $info
* The property info array as returned by hook_entity_field_info().
* @param string $entity_type
* The entity type for which entity properties are defined.
*
* @see hook_entity_field_info()
*/
function hook_entity_field_info_alter(&$info, $entity_type) {
if (!empty($info['definitions']['mymodule_text'])) {
// Alter the mymodule_text property to use a custom class.
$info['definitions']['mymodule_text']['class'] = '\Drupal\anothermodule\EntityComputedText';
}
}
......@@ -346,6 +346,13 @@ protected function preDelete($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.
*
......
......@@ -52,6 +52,7 @@ public function build(ContainerBuilder $container) {
->setFactoryClass('Drupal\Core\Database\Database')
->setFactoryMethod('getConnection')
->addArgument('slave');
$container->register('typed_data', 'Drupal\Core\TypedData\TypedDataManager');
// @todo Replace below lines with the commented out block below it when it's
// performant to do so: http://drupal.org/node/1706064.
......
......@@ -45,6 +45,15 @@ class DatabaseStorageController implements EntityStorageControllerInterface {
*/
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().
*
......@@ -201,7 +210,7 @@ public function load(array $ids = NULL) {
// Remove any invalid ids from the array.
$passed_ids = array_intersect_key($passed_ids, $entities);
foreach ($entities as $entity) {
$passed_ids[$entity->{$this->idKey}] = $entity;
$passed_ids[$entity->id()] = $entity;
}
$entities = $passed_ids;
}
......@@ -470,7 +479,7 @@ public function save(EntityInterface $entity) {
if (!$entity->isNew()) {
$return = drupal_write_record($this->entityInfo['base table'], $entity, $this->idKey);
$this->resetCache(array($entity->{$this->idKey}));
$this->resetCache(array($entity->id()));
$this->postSave($entity, TRUE);
$this->invokeHook('update', $entity);
}
......@@ -547,4 +556,57 @@ protected function invokeHook($hook, EntityInterface $entity) {
// Invoke the respective entity-level hook.
module_invoke_all('entity_' . $hook, $entity, $this->entityType);
}
/**
* Implements Drupal\Core\Entity\EntityStorageControllerInterface::getFieldDefinitions().
*/
public function getFieldDefinitions(array $constraints) {
// @todo: Add caching for $this->propertyInfo.
if (!isset($this->entityFieldInfo)) {
$this->entityFieldInfo = array(
'definitions' => $this->baseFieldDefinitions(),
// Contains definitions of optional (per-bundle) properties.
'optional' => array(),
// An array keyed by bundle name containing the names of the per-bundle
// properties.
'bundle map' => array(),
);
// Invoke hooks.
$result = module_invoke_all($this->entityType . '_property_info');
$this->entityFieldInfo = array_merge_recursive($this->entityFieldInfo, $result);
$result = module_invoke_all('entity_field_info', $this->entityType);
$this->entityFieldInfo = array_merge_recursive($this->entityFieldInfo, $result);
$hooks = array('entity_field_info', $this->entityType . '_property_info');
drupal_alter($hooks, $this->entityFieldInfo, $this->entityType);
// Enforce fields to be multiple by default.
foreach ($this->entityFieldInfo['definitions'] as &$definition) {
$definition['list'] = TRUE;
}
foreach ($this->entityFieldInfo['optional'] as &$definition) {
$definition['list'] = TRUE;
}
}
$definitions = $this->entityFieldInfo['definitions'];
// Add in per-bundle properties.
// @todo: Should this be statically cached as well?
if (!empty($constraints['bundle']) && isset($this->entityFieldInfo['bundle map'][$constraints['bundle']])) {
$definitions += array_intersect_key($this->entityFieldInfo['optional'], array_flip($this->entityFieldInfo['bundle map'][$constraints['bundle']]));
}
return $definitions;
}
/**
* Defines the base properties of the entity type.
*
* @todo: Define abstract once all entity types have been converted.
*/
public function baseFieldDefinitions() {
return array();
}
}
<?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;
}
}
......@@ -9,6 +9,7 @@
use Drupal\Component\Uuid\Uuid;
use Drupal\Core\Language\Language;
use IteratorAggregate;
/**
* Defines a base entity class.
......@@ -18,7 +19,7 @@
* This class can be used as-is by simple entity types. Entity types requiring
* special handling can extend the class.
*/
class Entity implements EntityInterface {
class Entity implements IteratorAggregate, EntityInterface {
/**
* The language code of the entity's default language.
......@@ -146,14 +147,109 @@ public function uri() {
}
/**
* Implements EntityInterface::language().
* Implements EntityInterface::get().
*/
public function get($property_name, $langcode = NULL) {
// @todo: Replace by EntityNG implementation once all entity types have been
// converted to use the entity field API.
return isset($this->{$property_name}) ? $this->{$property_name} : NULL;
}
/**
* Implements ComplexDataInterface::set().
*/
public function set($property_name, $value) {
// @todo: Replace by EntityNG implementation once all entity types have been
// converted to use the entity field API.
$this->{$property_name} = $value;
}
/**
* Implements ComplexDataInterface::getProperties().
*/
public function getProperties($include_computed = FALSE) {
// @todo: Replace by EntityNG implementation once all entity types have been
// converted to use the entity field API.
}
/**
* Implements ComplexDataInterface::getPropertyValues().
*/
public function getPropertyValues() {
// @todo: Replace by EntityNG implementation once all entity types have been
// converted to use the entity field API.
}
/**
* Implements ComplexDataInterface::setPropertyValues().
*/
public function setPropertyValues($values) {
// @todo: Replace by EntityNG implementation once all entity types have been
// converted to use the entity field API.
}
/**
* Implements ComplexDataInterface::getPropertyDefinition().
*/
public function getPropertyDefinition($name) {
// @todo: Replace by EntityNG implementation once all entity types have been
// converted to use the entity field API.
}
/**
* Implements ComplexDataInterface::getPropertyDefinitions().
*/
public function getPropertyDefinitions() {
// @todo: Replace by EntityNG implementation once all entity types have been
// converted to use the entity field API.
}
/**
* Implements ComplexDataInterface::isEmpty().
*/
public function isEmpty() {
// @todo: Replace by EntityNG implementation once all entity types have been
// converted to use the entity field API.
}
/**
* Implements ComplexDataInterface::getIterator().
*/
public function getIterator() {
// @todo: Replace by EntityNG implementation once all entity types have been
// converted to use the entity field API.
}
/**
* Implements AccessibleInterface::access().
*/
public function access(\Drupal\user\User $account = NULL) {
// TODO: Implement access() method.
}
/**
* Implements TranslatableInterface::language().
*/
public function language() {
// @todo: Replace by EntityNG implementation once all entity types have been
// converted to use the entity field API.
return !empty($this->langcode) ? language_load($this->langcode) : new Language(array('langcode' => LANGUAGE_NOT_SPECIFIED));
}
/**
* Implements EntityInterface::translations().
* Implements TranslatableInterface::getTranslation().
*/
public function getTranslation($langcode, $strict = TRUE) {
// @todo: Replace by EntityNG implementation once all entity types have been
// converted to use the entity field API.
}
/**
* Returns the languages the entity is translated to.
*
* @todo: Remove once all entity types implement the entity field API. This
* is deprecated by
* TranslatableInterface::getTranslationLanguages().
*/
public function translations() {
$languages = array();
......@@ -177,108 +273,11 @@ public function translations() {
}
/**
* Implements EntityInterface::get().
*/
public function get($property_name, $langcode = NULL) {
// Handle fields.
$entity_info = $this->entityInfo();
if ($entity_info['fieldable'] && field_info_instance($this->entityType, $property_name, $this->bundle())) {
$field = field_info_field($property_name);
// Prevent getFieldLangcode() from throwing an exception in case a
// $langcode has been passed and it is invalid for the field.
$langcode = $this->getFieldLangcode($field, $langcode, FALSE);
return isset($this->{$property_name}[$langcode]) ? $this->{$property_name}[$langcode] : NULL;
}
else {
// Handle properties being not fields.
// @todo: Add support for translatable properties being not fields.
return isset($this->{$property_name}) ? $this->{$property_name} : NULL;
}
}
/**
* Implements EntityInterface::set().
* Implements TranslatableInterface::getTranslationLanguages().
*/
public function set($property_name, $value, $langcode = NULL) {
// Handle fields.
$entity_info = $this->entityInfo();
if ($entity_info['fieldable'] && field_info_instance($this->entityType, $property_name, $this->bundle())) {
$field = field_info_field($property_name);
// Throws an exception if the $langcode is invalid.
$langcode = $this->getFieldLangcode($field, $langcode);
$this->{$property_name}[$langcode] = $value;
}
else {
// Handle properties being not fields.
// @todo: Add support for translatable properties being not fields.
$this->{$property_name} = $value;
}
}
/**
* Determines the language code for accessing a field value.
*
* The effective language code to be used for a field varies:
* - If the entity is language-specific and the requested field is
* translatable, the entity's language code should be used to access the
* field value when no language is explicitly provided.
* - If the entity is not language-specific, LANGUAGE_NOT_SPECIFIED should be
* used to access all field values.
* - If a field's values are non-translatable (shared among all language
* versions of an entity), LANGUAGE_NOT_SPECIFIED should be used to access
* them.
*
* There cannot be valid field values if a field is not translatable and the
* requested langcode is not LANGUAGE_NOT_SPECIFIED. Therefore, this function
* throws an exception in that case (or returns NULL when $strict is FALSE).
*
* @param string $field
* Field the language code is being determined for.
* @param string|null $langcode
* (optional) The language code attempting to be applied to the field.
* Defaults to the entity language.
* @param bool $strict
* (optional) When $strict is TRUE, an exception is thrown if the field is
* not translatable and the langcode is not LANGUAGE_NOT_SPECIFIED. When
* $strict is FALSE, NULL is returned and no exception is thrown. For
* example, EntityInterface::set() passes TRUE, since it must not set field
* values for invalid langcodes. EntityInterface::get() passes FALSE to
* determine whether any field values exist for a specific langcode.
* Defaults to TRUE.
*
* @return string|null
* The langcode if appropriate, LANGUAGE_NOT_SPECIFIED for non-translatable
* fields, or NULL when an invalid langcode was used in non-strict mode.
*
* @throws \InvalidArgumentException
* Thrown in case a $langcode other than LANGUAGE_NOT_SPECIFIED is passed
* for a non-translatable field and $strict is TRUE.
*/
protected function getFieldLangcode($field, $langcode = NULL, $strict = TRUE) {
// Only apply the given langcode if the entity is language-specific.
// Otherwise translatable fields are handled as non-translatable fields.
if (field_is_translatable($this->entityType, $field) && ($default_language = $this->language()) && !language_is_locked($this->langcode)) {
// For translatable fields the values in default language are stored using
// the language code of the default language.
return isset($langcode) ? $langcode : $default_language->langcode;
}
else {
// The field is not translatable, but the caller requested a specific
// langcode that does not exist.
if (isset($langcode) && $langcode !== LANGUAGE_NOT_SPECIFIED) {
if ($strict) {
throw new \InvalidArgumentException(format_string('Unable to resolve @langcode for non-translatable field @field_name. Use langcode LANGUAGE_NOT_SPECIFIED instead.', array(
'@field_name' => $field['field_name'],
'@langcode' => $langcode,
)));
}
else {
return NULL;
}
}