Commit a2c2367b authored by alexpott's avatar alexpott

Issue #1969728 by yched, effulgentsia, swentel, fago, plach: Implement Field...

Issue #1969728 by yched, effulgentsia, swentel, fago, plach: Implement Field API 'field types' as TypedData Plugins.
parent 377521e2
......@@ -30,6 +30,13 @@ services:
factory_method: get
factory_service: cache_factory
arguments: [cache]
cache.entity:
class: Drupal\Core\Cache\CacheBackendInterface
tags:
- { name: cache.bin }
factory_method: get
factory_service: cache_factory
arguments: [entity]
cache.menu:
class: Drupal\Core\Cache\CacheBackendInterface
tags:
......@@ -151,6 +158,9 @@ services:
plugin.manager.entity:
class: Drupal\Core\Entity\EntityManager
arguments: ['@container.namespaces', '@service_container', '@module_handler', '@cache.cache', '@language_manager']
plugin.manager.entity.field.field_type:
class: Drupal\Core\Entity\Field\FieldTypePluginManager
arguments: ['@container.namespaces', '@cache.entity', '@language_manager', '@module_handler']
plugin.manager.archiver:
class: Drupal\Core\Archiver\ArchiverManager
arguments: ['@container.namespaces', '@cache.cache', '@language_manager', '@module_handler']
......
......@@ -44,6 +44,7 @@ function entity_info_cache_clear() {
drupal_static_reset('entity_get_bundles');
// Clear all languages.
Drupal::entityManager()->clearCachedDefinitions();
Drupal::entityManager()->clearCachedFieldDefinitions();
}
/**
......
<?php
/**
* @file
* Contains \Drupal\Core\Entity\Annotation\FieldType.
*/
namespace Drupal\Core\Entity\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a FieldType annotation object.
*
* Additional annotation keys for field types can be defined in
* hook_field_info_alter().
*
* @Annotation
*/
class FieldType extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The name of the module providing the field type plugin.
*
* @var string
*/
public $module;
/**
* The human-readable name of the field type.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $label;
/**
* A short human readable description for the field type.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $description;
/**
* An array of field-level settings available for the field type.
*
* Keys are the names of the settings, and values are the default values for
* those settings.
*
* @var array
*/
public $settings;
/**
* An array of instance-level settings available for the field type.
*
* Keys are the names of the settings, and values are the default values for
* those settings.
*
* Instance-level settings can have different values on each field instance,
* and thus allow greater flexibility than field-level settings. It is
* recommended to put settings at the instance level whenever possible.
* Notable exceptions: settings acting on the storage schema, or settings that
* Views needs to use across field instances (for example, settings defining
* the list of allowed values for the field).
*
* @var array
*/
public $instance_settings;
/**
* The plugin_id of the default widget for this field type.
*
* This widget must be available whenever the field type is available (i.e.
* provided by the field type module, or by a module the field type module
* depends on).
*
* @var string
*/
public $default_widget;
/**
* The plugin_id of the default formatter for this field type.
*
* This formatter must be available whenever the field type is available (i.e.
* provided by the field type module, or by a module the field type module
* depends on).
*
* @var string
*/
public $default_formatter;
/**
* A boolean stating that fields of this type are configurable.
*
* @todo: Make field module respect this.
*
* @var boolean
*/
public $configurable = TRUE;
/**
* A boolean stating that fields of this type cannot be created through the UI.
*
* If TRUE, fields of this type can only be created programmatically.
*
* @var boolean
*/
public $no_ui = FALSE;
}
......@@ -214,6 +214,7 @@ public function deleteRevision($revision_id) {
$this->database->delete($this->revisionTable)
->condition($this->revisionKey, $revision->getRevisionId())
->execute();
$this->invokeFieldMethod('deleteRevision', $revision);
$this->invokeHook('revision_delete', $revision);
}
}
......@@ -406,6 +407,7 @@ public function delete(array $entities) {
$entity_class::postDelete($this, $entities);
foreach ($entities as $id => $entity) {
$this->invokeFieldMethod('delete', $entity);
$this->invokeHook('delete', $entity);
}
// Ignore slave server temporarily.
......@@ -430,6 +432,7 @@ public function save(EntityInterface $entity) {
}
$entity->preSave($this);
$this->invokeFieldMethod('preSave', $entity);
$this->invokeHook('presave', $entity);
if (!$entity->isNew()) {
......@@ -446,6 +449,7 @@ public function save(EntityInterface $entity) {
}
$this->resetCache(array($entity->id()));
$entity->postSave($this, TRUE);
$this->invokeFieldMethod('update', $entity);
$this->invokeHook('update', $entity);
}
else {
......@@ -458,6 +462,7 @@ public function save(EntityInterface $entity) {
$entity->enforceIsNew(FALSE);
$entity->postSave($this, FALSE);
$this->invokeFieldMethod('insert', $entity);
$this->invokeHook('insert', $entity);
}
......@@ -518,7 +523,8 @@ protected function saveRevision(EntityInterface $entity) {
* Invokes a hook on behalf of the entity.
*
* @param $hook
* One of 'presave', 'insert', 'update', 'predelete', or 'delete'.
* One of 'presave', 'insert', 'update', 'predelete', 'delete', or
* 'revision_delete'.
* @param $entity
* The entity object.
*/
......
......@@ -219,28 +219,7 @@ protected function buildQuery($ids, $revision_id = FALSE) {
protected function attachLoad(&$queried_entities, $load_revision = FALSE) {
// Map the loaded stdclass records into entity objects and according fields.
$queried_entities = $this->mapFromStorageRecords($queried_entities, $load_revision);
if ($this->entityInfo['fieldable']) {
if ($load_revision) {
field_attach_load_revision($this->entityType, $queried_entities);
}
else {
field_attach_load($this->entityType, $queried_entities);
}
}
// 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);
}
parent::attachLoad($queried_entities, $load_revision);
}
/**
......@@ -363,6 +342,7 @@ public function save(EntityInterface $entity) {
}
$entity->preSave($this);
$this->invokeFieldMethod('preSave', $entity);
$this->invokeHook('presave', $entity);
// Create the storage record to be saved.
......@@ -385,6 +365,7 @@ public function save(EntityInterface $entity) {
}
$this->resetCache(array($entity->id()));
$entity->postSave($this, TRUE);
$this->invokeFieldMethod('update', $entity);
$this->invokeHook('update', $entity);
}
else {
......@@ -403,6 +384,7 @@ public function save(EntityInterface $entity) {
$entity->enforceIsNew(FALSE);
$entity->postSave($this, FALSE);
$this->invokeFieldMethod('insert', $entity);
$this->invokeHook('insert', $entity);
}
......@@ -503,28 +485,6 @@ protected function savePropertyData(EntityInterface $entity) {
$query->execute();
}
/**
* Overrides DatabaseStorageController::invokeHook().
*
* Invokes field API attachers with a BC entity.
*/
protected function invokeHook($hook, EntityInterface $entity) {
$function = 'field_attach_' . $hook;
// @todo: field_attach_delete_revision() is named the wrong way round,
// consider renaming it.
if ($function == 'field_attach_revision_delete') {
$function = 'field_attach_delete_revision';
}
if (!empty($this->entityInfo['fieldable']) && function_exists($function)) {
$function($entity);
}
// 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.
*
......@@ -638,6 +598,7 @@ public function delete(array $entities) {
$entity_class::postDelete($this, $entities);
foreach ($entities as $id => $entity) {
$this->invokeFieldMethod('delete', $entity);
$this->invokeHook('delete', $entity);
}
// Ignore slave server temporarily.
......
......@@ -107,7 +107,7 @@ public function &__get($name) {
foreach ($this->decorated->fields[$name] as $langcode => $field) {
// Only set if it's not empty, otherwise there can be ghost values.
if (!$field->isEmpty()) {
$this->decorated->values[$name][$langcode] = $field->getValue();
$this->decorated->values[$name][$langcode] = $field->getValue(TRUE);
}
}
// The returned values might be changed by reference, so we need to remove
......@@ -124,11 +124,10 @@ public function &__get($name) {
$this->decorated->values[$name][Language::LANGCODE_DEFAULT][0]['value'] = NULL;
}
if (is_array($this->decorated->values[$name][Language::LANGCODE_DEFAULT])) {
// This will work with all defined properties that have a single value.
// We need to ensure the key doesn't matter. Mostly it's 'value' but
// e.g. EntityReferenceItem uses target_id.
if (isset($this->decorated->values[$name][Language::LANGCODE_DEFAULT][0]) && count($this->decorated->values[$name][Language::LANGCODE_DEFAULT][0]) == 1) {
return $this->decorated->values[$name][Language::LANGCODE_DEFAULT][0][key($this->decorated->values[$name][Language::LANGCODE_DEFAULT][0])];
// e.g. EntityReferenceItem uses target_id - so just take the first one.
if (isset($this->decorated->values[$name][Language::LANGCODE_DEFAULT][0]) && is_array($this->decorated->values[$name][Language::LANGCODE_DEFAULT][0])) {
return $this->decorated->values[$name][Language::LANGCODE_DEFAULT][0][current(array_keys($this->decorated->values[$name][Language::LANGCODE_DEFAULT][0]))];
}
}
return $this->decorated->values[$name][Language::LANGCODE_DEFAULT];
......
......@@ -8,7 +8,6 @@
namespace Drupal\Core\Entity;
use Drupal\entity\EntityFormDisplayInterface;
use Drupal\Core\Language\Language;
/**
......@@ -241,13 +240,49 @@ protected function actions(array $form, array &$form_state) {
* Implements \Drupal\Core\Entity\EntityFormControllerInterface::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();
$entity_langcode = $entity->language()->langcode;
if (!empty($info['fieldable'])) {
field_attach_form_validate($entity, $form, $form_state);
$violations = array();
// @todo Simplify when all entity types are converted to EntityNG.
if ($entity instanceof EntityNG) {
foreach ($entity as $field_name => $field) {
$field_violations = $field->validate();
if (count($field_violations)) {
$violations[$field_name] = $field_violations;
}
}
}
else {
// For BC entities, iterate through each field instance and
// instantiate NG items objects manually.
$definitions = \Drupal::entityManager()->getFieldDefinitions($entity->entityType(), $entity->bundle());
foreach (field_info_instances($entity->entityType(), $entity->bundle()) as $field_name => $instance) {
$langcode = field_is_translatable($entity->entityType(), $instance->getField()) ? $entity_langcode : Language::LANGCODE_NOT_SPECIFIED;
// Create the field object.
$items = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();
// @todo Exception : calls setValue(), tries to set the 'formatted'
// property.
$field = \Drupal::typedData()->create($definitions[$field_name], $items, $field_name, $entity);
$field_violations = $field->validate();
if (count($field_violations)) {
$violations[$field->getName()] = $field_violations;
}
}
}
// Map errors back to form elements.
if ($violations) {
foreach ($violations as $field_name => $field_violations) {
$langcode = field_is_translatable($entity->entityType(), field_info_field($field_name)) ? $entity_langcode : Language::LANGCODE_NOT_SPECIFIED;
$field_state = field_form_get_state($form['#parents'], $field_name, $langcode, $form_state);
$field_state['constraint_violations'] = $field_violations;
field_form_set_state($form['#parents'], $field_name, $langcode, $form_state, $field_state);
}
field_invoke_method('flagErrors', _field_invoke_widget_target($form_state['form_display']), $entity, $form, $form_state);
}
// @todo Remove this.
......
......@@ -41,25 +41,6 @@ public function form(array $form, array &$form_state) {
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 fields.
$entity = $this->buildEntity($form, $form_state);
$info = $entity->entityInfo();
if (!empty($info['fieldable'])) {
field_attach_form_validate($entity, $form, $form_state);
}
// @todo Remove this.
// Execute legacy global validation handlers.
unset($form_state['validate_handlers']);
form_execute_handlers('validate', $form, $form_state);
}
/**
* Overrides EntityFormController::submitEntityLanguage().
*/
......
......@@ -60,6 +60,13 @@ class EntityNG extends Entity {
*/
protected $bcEntity;
/**
* Local cache for the entity language.
*
* @var \Drupal\Core\Language\Language
*/
protected $language;
/**
* Local cache for field definitions.
*
......@@ -326,15 +333,30 @@ public function isEmpty() {
* Implements \Drupal\Core\TypedData\TranslatableInterface::language().
*/
public function language() {
// Get the language code if the property exists.
if ($this->getPropertyDefinition('langcode')) {
$language = $this->get('langcode')->language;
// Keep a local cache of the language object and clear it if the langcode
// gets changed, see EntityNG::onChange().
if (!isset($this->language)) {
// Get the language code if the property exists.
if ($this->getPropertyDefinition('langcode')) {
$this->language = $this->get('langcode')->language;
}
if (empty($this->language)) {
// Make sure we return a proper language object.
$this->language = new Language(array('langcode' => Language::LANGCODE_NOT_SPECIFIED));
}
}
if (empty($language)) {
// Make sure we return a proper language object.
$language = new Language(array('langcode' => Language::LANGCODE_NOT_SPECIFIED));
return $this->language;
}
/**
* {@inheritdoc}
*/
public function onChange($property_name) {
if ($property_name == 'langcode') {
// Avoid using unset as this unnecessarily triggers magic methods later
// on.
$this->language = NULL;
}
return $language;
}
/**
......@@ -386,18 +408,20 @@ public function getTranslationLanguages($include_default = TRUE) {
$definitions = $this->getPropertyDefinitions();
// Build an array with the translation langcodes set as keys. Empty
// translations should not be included and must be skipped.
foreach ($this->getProperties() as $name => $property) {
foreach ($this->fields[$name] as $langcode => $field) {
if (!$field->isEmpty()) {
$translations[$langcode] = TRUE;
foreach ($definitions as $name => $definition) {
if (isset($this->fields[$name])) {
foreach ($this->fields[$name] as $langcode => $field) {
if (!$field->isEmpty()) {
$translations[$langcode] = TRUE;
}
}
if (isset($this->values[$name])) {
foreach ($this->values[$name] as $langcode => $values) {
// If a value is there but the field object is empty, it has been
// unset, so we need to skip the field also.
if ($values && !empty($definitions[$name]['translatable']) && !(isset($this->fields[$name][$langcode]) && $this->fields[$name][$langcode]->isEmpty())) {
$translations[$langcode] = TRUE;
}
}
if (isset($this->values[$name])) {
foreach ($this->values[$name] as $langcode => $values) {
// If a value is there but the field object is empty, it has been
// unset, so we need to skip the field also.
if ($values && !empty($definition['translatable']) && !(isset($this->fields[$name][$langcode]) && $this->fields[$name][$langcode]->isEmpty())) {
$translations[$langcode] = TRUE;
}
}
}
......
......@@ -137,4 +137,101 @@ protected function cacheSet($entities) {
}
}
/**
* {@inheritdoc}
*/
public function invokeFieldMethod($method, EntityInterface $entity) {
foreach (array_keys($entity->getTranslationLanguages()) as $langcode) {
// @todo getTranslation() only works on NG entities. Remove the condition
// and the second code branch when all core entity types are converted.
if ($translation = $entity->getTranslation($langcode)) {
foreach ($translation as $field_name => $field) {
$field->$method();
}
}
else {
// For BC entities, iterate through fields and instantiate NG items
// objects manually.
$definitions = \Drupal::entityManager()->getFieldDefinitions($entity->entityType(), $entity->bundle());
foreach ($definitions as $field_name => $definition) {
if (!empty($definition['configurable'])) {
// Create the items object.
$itemsBC = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();
// @todo Exception : this calls setValue(), tries to set the
// 'formatted' property. For now, this is worked around by
// commenting out the Exception in TextProcessed::setValue().
$items = \Drupal::typedData()->create($definition, $itemsBC, $field_name, $entity);
$items->$method();
// Put back the items values in the entity.
$itemsBC = $items->getValue(TRUE);
if ($itemsBC !== array() || isset($entity->{$field_name}[$langcode])) {
$entity->{$field_name}[$langcode] = $itemsBC;
}
}
}
}
}
}
/**
* {@inheritdoc}
*/
public function invokeFieldItemPrepareCache(EntityInterface $entity) {
foreach (array_keys($entity->getTranslationLanguages()) as $langcode) {
// @todo getTranslation() only works on NG entities. Remove the condition
// and the second code branch when all core entity types are converted.
if ($translation = $entity->getTranslation($langcode)) {
foreach ($translation->getPropertyDefinitions() as $property => $definition) {
$type_definition = \Drupal::typedData()->getDefinition($definition['type']);
// Only create the item objects if needed.
if (is_subclass_of($type_definition['class'], '\Drupal\Core\Entity\Field\PrepareCacheInterface')
// Prevent legacy field types from skewing performance too much by
// checking the existence of the legacy function directly, instead
// of making LegacyConfigFieldItem implement PrepareCacheInterface.
// @todo Remove once all core field types have been converted (see
// http://drupal.org/node/2014671).
|| (is_subclass_of($type_definition['class'], '\Drupal\field\Plugin\field\field_type\LegacyConfigFieldItem') && function_exists($type_definition['module'] . '_field_load'))) {
// Call the prepareCache() method directly on each item
// individually.
foreach ($translation->get($property) as $item) {
$item->prepareCache();
}
}
}
}
else {
// For BC entities, iterate through the fields and instantiate NG items
// objects manually.
$definitions = \Drupal::entityManager()->getFieldDefinitions($entity->entityType(), $entity->bundle());
foreach ($definitions as $field_name => $definition) {
if (!empty($definition['configurable'])) {
$type_definition = \Drupal::typedData()->getDefinition($definition['type']);