diff --git a/core/includes/entity.inc b/core/includes/entity.inc index 702a56e..d9ba83c 100644 --- a/core/includes/entity.inc +++ b/core/includes/entity.inc @@ -44,6 +44,7 @@ function entity_info_cache_clear() { drupal_static_reset('entity_get_bundles'); // Clear all languages. Drupal::entityManager()->clearCachedDefinitions(); + Drupal::entityManager()->clearCachedFieldDefinitions(); } /** diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php index a3cd9df..5e18462 100644 --- a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php +++ b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php @@ -230,6 +230,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); } } @@ -422,6 +423,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. @@ -446,6 +448,7 @@ public function save(EntityInterface $entity) { } $entity->preSave($this); + $this->invokeFieldMethod('preSave', $entity); $this->invokeHook('presave', $entity); if (!$entity->isNew()) { @@ -462,6 +465,7 @@ public function save(EntityInterface $entity) { } $this->resetCache(array($entity->id())); $entity->postSave($this, TRUE); + $this->invokeFieldMethod('update', $entity); $this->invokeHook('update', $entity); } else { @@ -474,6 +478,7 @@ public function save(EntityInterface $entity) { $entity->enforceIsNew(FALSE); $entity->postSave($this, FALSE); + $this->invokeFieldMethod('insert', $entity); $this->invokeHook('insert', $entity); } @@ -534,7 +539,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. */ diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php b/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php index c3a76e2..889a115 100644 --- a/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php +++ b/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php @@ -215,28 +215,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); } /** @@ -359,6 +338,7 @@ public function save(EntityInterface $entity) { } $entity->preSave($this); + $this->invokeFieldMethod('preSave', $entity); $this->invokeHook('presave', $entity); // Create the storage record to be saved. @@ -381,6 +361,7 @@ public function save(EntityInterface $entity) { } $this->resetCache(array($entity->id())); $entity->postSave($this, TRUE); + $this->invokeFieldMethod('update', $entity); $this->invokeHook('update', $entity); } else { @@ -399,6 +380,7 @@ public function save(EntityInterface $entity) { $entity->enforceIsNew(FALSE); $entity->postSave($this, FALSE); + $this->invokeFieldMethod('insert', $entity); $this->invokeHook('insert', $entity); } @@ -500,28 +482,6 @@ protected function savePropertyData(EntityInterface $entity) { } /** - * 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. * * @param \Drupal\Core\Entity\EntityInterface $entity @@ -634,6 +594,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. diff --git a/core/lib/Drupal/Core/Entity/EntityBCDecorator.php b/core/lib/Drupal/Core/Entity/EntityBCDecorator.php index 93bafb4..f0eef75 100644 --- a/core/lib/Drupal/Core/Entity/EntityBCDecorator.php +++ b/core/lib/Drupal/Core/Entity/EntityBCDecorator.php @@ -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 @@ -126,9 +126,9 @@ public function &__get($name) { 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]; diff --git a/core/lib/Drupal/Core/Entity/EntityFormController.php b/core/lib/Drupal/Core/Entity/EntityFormController.php index 7c5f7ed..a05e42b 100644 --- a/core/lib/Drupal/Core/Entity/EntityFormController.php +++ b/core/lib/Drupal/Core/Entity/EntityFormController.php @@ -8,7 +8,6 @@ namespace Drupal\Core\Entity; use Drupal\entity\EntityFormDisplayInterface; - use Drupal\Core\Language\Language; /** @@ -241,13 +240,52 @@ 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 + // instanciate NG items objects manually. + $definitions = \Drupal::entityManager()->getStorageController($entity->entityType())->getFieldDefinitions(array( + 'EntityType' => $entity->entityType(), + 'Bundle' => $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. diff --git a/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php b/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php index 0c9dabd..f44b417 100644 --- a/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php +++ b/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php @@ -42,25 +42,6 @@ public function form(array $form, array &$form_state) { } /** - * 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(). */ protected function submitEntityLanguage(array $form, array &$form_state) { diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php index ae931f6..909c477 100644 --- a/core/lib/Drupal/Core/Entity/EntityManager.php +++ b/core/lib/Drupal/Core/Entity/EntityManager.php @@ -265,4 +265,10 @@ public function getAdminPath($entity_type, $bundle) { return $admin_path; } + // @todo temporary - Revisit after http://drupal.org/node/1893820. + public function clearCachedFieldDefinitions() { + unset($this->controllers['storage']); + cache()->deleteTags(array('entity_info')); + } + } diff --git a/core/lib/Drupal/Core/Entity/EntityNG.php b/core/lib/Drupal/Core/Entity/EntityNG.php index de0fb46..7e14fac 100644 --- a/core/lib/Drupal/Core/Entity/EntityNG.php +++ b/core/lib/Drupal/Core/Entity/EntityNG.php @@ -392,18 +392,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; } } } diff --git a/core/lib/Drupal/Core/Entity/EntityStorageControllerBase.php b/core/lib/Drupal/Core/Entity/EntityStorageControllerBase.php index 1b70d17..f53512e 100644 --- a/core/lib/Drupal/Core/Entity/EntityStorageControllerBase.php +++ b/core/lib/Drupal/Core/Entity/EntityStorageControllerBase.php @@ -137,4 +137,168 @@ 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 instanciate NG items + // objects manually. + $definitions = $this->getFieldDefinitions(array( + 'EntityType' => $entity->entityType(), + 'Bundle' => $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 invokeFieldMethodMultiple($method, array $entities, $langcode) { + $entities_by_bundle = array(); + foreach ($entities as $id => $entity) { + $entities_by_bundle[$entity->bundle()][$id] = $entity; + } + + foreach ($entities_by_bundle as $bundle => $bundle_entities) { + $definitions = $this->getFieldDefinitions(array( + 'EntityType' => $this->entityType, + 'Bundle' => $bundle, + )); + + foreach ($definitions as $field_name => $definition) { + $entities_items = array(); + + foreach ($bundle_entities as $id => $entity) { + // @todo Remove the condition and the second code branch when all core + // entity types are converted. + if ($entity->getNGEntity() instanceof EntityNG) { + // @todo $entity->getTranslation()->get($name) sometimes fails, + // because Entity\Translation::getPropertyDefinitions() is empty() ?? + try { + $entities_items[$id] = $entity->getNGEntity()->getTranslation($langcode)->get($field_name); + } + catch (\InvalidArgumentException $e) { + break; + } + } + else { + $BC_mode = TRUE; + // Support BC entities. + if (!empty($definition['configurable'])) { + $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(). + $entities_items[$id] = \Drupal::typedData()->create($definition, $itemsBC, $field_name, $entity); + } + } + } + + $type_definition = \Drupal::typedData()->getDefinition($definition['type']); + $type_definition['class']::$method($entities_items); + + // @todo Remove when all core entity types are converted. + if (!empty($BC_mode)) { + // Put back the items values in the entity. + foreach ($bundle_entities as $id => $entity) { + $itemsBC = $entities_items[$id]->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\field\Plugin\Type\FieldType\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 instanciate NG items + // objects manually. + $definitions = $this->getFieldDefinitions(array( + 'EntityType' => $entity->entityType(), + 'Bundle' => $entity->bundle(), + )); + foreach ($definitions as $field_name => $definition) { + if (!empty($definition['configurable'])) { + $type_definition = \Drupal::typedData()->getDefinition($definition['type']); + // Only create the item objects if needed. + if (is_subclass_of($type_definition['class'], '\Drupal\field\Plugin\Type\FieldType\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'))) { + + // Create the items object. + $items = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array(); + $itemsNG = \Drupal::typedData()->create($definition, $items, $field_name, $entity); + + foreach ($itemsNG as $item) { + $item->prepareCache(); + } + + // Put back the items values in the entity. + $items = $itemsNG->getValue(TRUE); + if ($items !== array() || isset($entity->{$field_name}[$langcode])) { + $entity->{$field_name}[$langcode] = $items; + } + } + } + } + } + } + } + } diff --git a/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php b/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php index e533dd7..b2aadf7 100644 --- a/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php @@ -174,4 +174,37 @@ public function getFieldDefinitions(array $constraints); */ public function getQueryServicename(); + /** + * Invokes a method on the Field objects within an entity. + * + * @param string $method + * The method name. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity object. + */ + public function invokeFieldMethod($method, EntityInterface $entity); + + /** + * Invokes a method on the Field objects within a set of entities. + * + * This only iterates on the field values in a given language. + * + * @param string $method + * The method name. + * @param array $entities + * An array of \Drupal\Core\Entity\EntityInterface entities, keyed by entity + * id. + * @param string $langcode + * The langcode. + */ + public function invokeFieldMethodMultiple($method, array $entities, $langcode); + + /** + * Invokes the prepareCache() method on all the relevant FieldItem objects. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity object. + */ + public function invokeFieldItemPrepareCache(EntityInterface $entity); + } diff --git a/core/lib/Drupal/Core/Entity/Field/FieldInterface.php b/core/lib/Drupal/Core/Entity/Field/FieldInterface.php index dc05036..c2dfbdb 100644 --- a/core/lib/Drupal/Core/Entity/Field/FieldInterface.php +++ b/core/lib/Drupal/Core/Entity/Field/FieldInterface.php @@ -80,4 +80,46 @@ public function getPropertyDefinition($name); * @see \Drupal\Core\Entity\Field\FieldItemInterface::getPropertyDefinitions() */ public function getPropertyDefinitions(); + + /** + * Defines custom presave behavior for field values. + * + * This method is called before either insert() or update() methods, and + * before values are written into storage. + */ + public function preSave(); + + /** + * Defines custom insert behavior for field values. + * + * This method is called after the save() method, and before values are + * written into storage. + */ + public function insert(); + + /** + * Defines custom update behavior for field values. + * + * This method is called after the save() method, and before values are + * written into storage. + */ + public function update(); + + /** + * Defines custom delete behavior for field values. + * + * This method is called during the process of deleting an entity, just before + * values are deleted from storage. + */ + public function delete(); + + /** + * Defines custom revision delete behavior for field values. + * + * This method is called from during the process of deleting an entity + * revision, just before the field values are deleted from storage. It is only + * called for entity types that support revisioning. + */ + public function deleteRevision(); + } diff --git a/core/lib/Drupal/Core/Entity/Field/FieldItemBase.php b/core/lib/Drupal/Core/Entity/Field/FieldItemBase.php index 7ba7ea6..d21c8cc 100644 --- a/core/lib/Drupal/Core/Entity/Field/FieldItemBase.php +++ b/core/lib/Drupal/Core/Entity/Field/FieldItemBase.php @@ -152,4 +152,34 @@ public function getConstraints() { return $constraints; } + /** + * {@inheritdoc} + */ + public function preSave() { } + + /** + * {@inheritdoc} + */ + public function insert() { } + + /** + * {@inheritdoc} + */ + public function update() { } + + /** + * {@inheritdoc} + */ + public function delete() { } + + /** + * {@inheritdoc} + */ + public function deleteRevision() { } + + /** + * {@inheritdoc} + */ + public static function prepareView(array $entities_items) { } + } diff --git a/core/lib/Drupal/Core/Entity/Field/FieldItemInterface.php b/core/lib/Drupal/Core/Entity/Field/FieldItemInterface.php index 31adec9..059a477 100644 --- a/core/lib/Drupal/Core/Entity/Field/FieldItemInterface.php +++ b/core/lib/Drupal/Core/Entity/Field/FieldItemInterface.php @@ -70,4 +70,46 @@ public function __isset($property_name); * The name of the property to get; e.g., 'title' or 'name'. */ public function __unset($property_name); + + /** + * Defines custom presave behavior for field values. + * + * This method is called before either insert() or update() methods, and + * before values are written into storage. + */ + public function preSave(); + + /** + * Defines custom insert behavior for field values. + * + * This method is called after the save() method, and before values are + * written into storage. + */ + public function insert(); + + /** + * Defines custom update behavior for field values. + * + * This method is called after the save() method, and before values are + * written into storage. + */ + public function update(); + + /** + * Defines custom delete behavior for field values. + * + * This method is called during the process of deleting an entity, just before + * values are deleted from storage. + */ + public function delete(); + + /** + * Defines custom revision delete behavior for field values. + * + * This method is called from during the process of deleting an entity + * revision, just before the field values are deleted from storage. It is only + * called for entity types that support revisioning. + */ + public function deleteRevision(); + } diff --git a/core/lib/Drupal/Core/Entity/Field/Type/Field.php b/core/lib/Drupal/Core/Entity/Field/Type/Field.php index af45ada..19ef8c6 100644 --- a/core/lib/Drupal/Core/Entity/Field/Type/Field.php +++ b/core/lib/Drupal/Core/Entity/Field/Type/Field.php @@ -57,6 +57,20 @@ public function filterEmptyValues() { } /** + * {@inheritdoc} + * @todo Revisit the need when all entity types are converted to NG entities. + */ + public function getValue($include_computed = FALSE) { + if (isset($this->list)) { + $values = array(); + foreach ($this->list as $delta => $item) { + $values[$delta] = $item->getValue($include_computed); + } + return $values; + } + } + + /** * Overrides \Drupal\Core\TypedData\ItemList::setValue(). */ public function setValue($values, $notify = TRUE) { @@ -209,4 +223,57 @@ public function getConstraints() { } return $constraints; } + + /** + * {@inheritdoc} + */ + public function preSave() { + // Filter out empty items. + $this->filterEmptyValues(); + + $this->delegateMethod('presave'); + } + + /** + * {@inheritdoc} + */ + public function insert() { + $this->delegateMethod('insert'); + } + + /** + * {@inheritdoc} + */ + public function update() { + $this->delegateMethod('update'); + } + + /** + * {@inheritdoc} + */ + public function delete() { + $this->delegateMethod('delete'); + } + + /** + * {@inheritdoc} + */ + public function deleteRevision() { + $this->delegateMethod('deleteRevision'); + } + + /** + * Calls a method on each FieldItem. + * + * @param string $method + * The name of the method. + */ + protected function delegateMethod($method) { + if (isset($this->list)) { + foreach ($this->list as $item) { + $item->{$method}(); + } + } + } + } diff --git a/core/lib/Drupal/Core/TypedData/Type/Map.php b/core/lib/Drupal/Core/TypedData/Type/Map.php index 43e3790..ea6bddf 100644 --- a/core/lib/Drupal/Core/TypedData/Type/Map.php +++ b/core/lib/Drupal/Core/TypedData/Type/Map.php @@ -53,11 +53,11 @@ public function getPropertyDefinitions() { /** * Overrides \Drupal\Core\TypedData\TypedData::getValue(). */ - public function getValue() { + public function getValue($include_computed = FALSE) { // Update the values and return them. foreach ($this->properties as $name => $property) { $definition = $property->getDefinition(); - if (empty($definition['computed'])) { + if ($include_computed || empty($definition['computed'])) { $value = $property->getValue(); // Only write NULL values if the whole map is not NULL. if (isset($this->values) || isset($value)) { @@ -230,4 +230,5 @@ public function onChange($property_name) { $this->parent->onChange($this->name); } } + } diff --git a/core/lib/Drupal/Core/TypedData/TypedData.php b/core/lib/Drupal/Core/TypedData/TypedData.php index 7ea33f0..62fb8d9 100644 --- a/core/lib/Drupal/Core/TypedData/TypedData.php +++ b/core/lib/Drupal/Core/TypedData/TypedData.php @@ -7,13 +7,15 @@ namespace Drupal\Core\TypedData; +use Drupal\Component\Plugin\PluginInspectionInterface; + /** * 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 { +abstract class TypedData implements TypedDataInterface, PluginInspectionInterface { /** * The data definition. @@ -64,6 +66,20 @@ public function getType() { } /** + * {@inheritdoc} + */ + public function getPluginId() { + return $this->definition['type']; + } + + /** + * {@inheritdoc} + */ + public function getPluginDefinition() { + return \Drupal::typedData()->getDefinition($this->definition['type']); + } + + /** * Implements \Drupal\Core\TypedData\TypedDataInterface::getDefinition(). */ public function getDefinition() { diff --git a/core/lib/Drupal/Core/TypedData/TypedDataManager.php b/core/lib/Drupal/Core/TypedData/TypedDataManager.php index 874ca6f..c0f81b9 100644 --- a/core/lib/Drupal/Core/TypedData/TypedDataManager.php +++ b/core/lib/Drupal/Core/TypedData/TypedDataManager.php @@ -9,6 +9,7 @@ use InvalidArgumentException; use Drupal\Component\Plugin\Discovery\ProcessDecorator; +use Drupal\Component\Plugin\Discovery\DerivativeDiscoveryDecorator; use Drupal\Component\Plugin\PluginManagerBase; use Drupal\Core\Plugin\Discovery\CacheDecorator; use Drupal\Core\Plugin\Discovery\HookDiscovery; @@ -57,6 +58,7 @@ class TypedDataManager extends PluginManagerBase { public function __construct() { $this->discovery = new HookDiscovery('data_type_info'); + $this->discovery = new DerivativeDiscoveryDecorator($this->discovery); $this->discovery = new ProcessDecorator($this->discovery, array($this, 'processDefinition')); $this->discovery = new CacheDecorator($this->discovery, 'typed_data:types'); diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/CountConstraint.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/CountConstraint.php new file mode 100644 index 0000000..5a343f5 --- /dev/null +++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/CountConstraint.php @@ -0,0 +1,37 @@ + 'datetime_default', 'default_formatter' => 'datetime_default', - 'default_token_formatter' => 'datetime_plain', - 'field item class' => '\Drupal\datetime\Type\DateTimeItem', + 'class' => '\Drupal\datetime\Type\DateTimeItem', ), ); } diff --git a/core/modules/datetime/lib/Drupal/datetime/Type/DateTimeItem.php b/core/modules/datetime/lib/Drupal/datetime/Type/DateTimeItem.php index 2d14e5a..57954b8 100644 --- a/core/modules/datetime/lib/Drupal/datetime/Type/DateTimeItem.php +++ b/core/modules/datetime/lib/Drupal/datetime/Type/DateTimeItem.php @@ -7,12 +7,12 @@ namespace Drupal\datetime\Type; -use Drupal\Core\Entity\Field\FieldItemBase; +use Drupal\field\Plugin\field\field_type\LegacyConfigFieldItem; /** * Defines the 'datetime' entity field item. */ -class DateTimeItem extends FieldItemBase { +class DateTimeItem extends LegacyConfigFieldItem { /** * Field definitions of the contained properties. diff --git a/core/modules/email/email.module b/core/modules/email/email.module index 28b9b21..2e86ab3 100644 --- a/core/modules/email/email.module +++ b/core/modules/email/email.module @@ -28,7 +28,7 @@ function email_field_info() { 'description' => t('This field stores an e-mail address in the database.'), 'default_widget' => 'email_default', 'default_formatter' => 'email_mailto', - 'field item class' => 'Drupal\email\Type\EmailItem', + 'class' => 'Drupal\email\Type\EmailItem', ), ); } diff --git a/core/modules/email/lib/Drupal/email/Type/EmailItem.php b/core/modules/email/lib/Drupal/email/Type/EmailItem.php index c1705f5..a493b36 100644 --- a/core/modules/email/lib/Drupal/email/Type/EmailItem.php +++ b/core/modules/email/lib/Drupal/email/Type/EmailItem.php @@ -7,12 +7,12 @@ namespace Drupal\email\Type; -use Drupal\Core\Entity\Field\FieldItemBase; +use Drupal\field\Plugin\field\field_type\LegacyConfigFieldItem; /** * Defines the 'email_field' entity field item. */ -class EmailItem extends FieldItemBase { +class EmailItem extends LegacyConfigFieldItem { /** * Definitions of the contained properties. diff --git a/core/modules/entity_reference/entity_reference.module b/core/modules/entity_reference/entity_reference.module index cd302e9..80309e5 100644 --- a/core/modules/entity_reference/entity_reference.module +++ b/core/modules/entity_reference/entity_reference.module @@ -29,8 +29,7 @@ function entity_reference_field_info() { ), 'default_widget' => 'entity_reference_autocomplete', 'default_formatter' => 'entity_reference_label', - 'data_type' => 'entity_reference_configurable_field', - 'field item class' => '\Drupal\entity_reference\Type\ConfigurableEntityReferenceItem', + 'class' => '\Drupal\entity_reference\Type\ConfigurableEntityReferenceItem', ); return $field_info; } @@ -90,17 +89,6 @@ function entity_reference_get_selection_handler(FieldDefinitionInterface $field_ } /** - * Implements hook_field_is_empty(). - */ -function entity_reference_field_is_empty($item, $field_type) { - if (empty($item['target_id']) && !empty($item['entity']) && $item['entity']->isNew()) { - // Allow auto-create entities. - return FALSE; - } - return !isset($item['target_id']) || !is_numeric($item['target_id']); -} - -/** * Implements hook_field_presave(). * * Create an entity on the fly. diff --git a/core/modules/entity_reference/lib/Drupal/entity_reference/Plugin/field/widget/AutocompleteWidgetBase.php b/core/modules/entity_reference/lib/Drupal/entity_reference/Plugin/field/widget/AutocompleteWidgetBase.php index 440b626..da70748 100644 --- a/core/modules/entity_reference/lib/Drupal/entity_reference/Plugin/field/widget/AutocompleteWidgetBase.php +++ b/core/modules/entity_reference/lib/Drupal/entity_reference/Plugin/field/widget/AutocompleteWidgetBase.php @@ -10,6 +10,7 @@ use Drupal\Component\Annotation\Plugin; use Drupal\Core\Annotation\Translation; use Drupal\field\Plugin\Type\Widget\WidgetBase; +use Symfony\Component\Validator\ConstraintViolationInterface; /** * Parent plugin for entity reference autocomplete widgets. @@ -86,7 +87,7 @@ public function formElement(array $items, $delta, array $element, $langcode, arr /** * {@inheritdoc} */ - public function errorElement(array $element, array $error, array $form, array &$form_state) { + public function errorElement(array $element, ConstraintViolationInterface $error, array $form, array &$form_state) { return $element['target_id']; } diff --git a/core/modules/entity_reference/lib/Drupal/entity_reference/Type/ConfigurableEntityReferenceItem.php b/core/modules/entity_reference/lib/Drupal/entity_reference/Type/ConfigurableEntityReferenceItem.php index cc7a3f4..f32b903 100644 --- a/core/modules/entity_reference/lib/Drupal/entity_reference/Type/ConfigurableEntityReferenceItem.php +++ b/core/modules/entity_reference/lib/Drupal/entity_reference/Type/ConfigurableEntityReferenceItem.php @@ -8,6 +8,8 @@ namespace Drupal\entity_reference\Type; use Drupal\Core\Entity\Field\Type\EntityReferenceItem; +use Drupal\field\Plugin\Type\FieldType\ConfigFieldItemInterface; +use Drupal\field\Plugin\Core\Entity\Field; /** * Defines the 'entity_reference_configurable' entity field item. @@ -18,7 +20,7 @@ * Required settings (below the definition's 'settings' key) are: * - target_type: The entity type to reference. */ -class ConfigurableEntityReferenceItem extends EntityReferenceItem { +class ConfigurableEntityReferenceItem extends EntityReferenceItem implements ConfigFieldItemInterface { /** * Definitions of the contained properties. @@ -30,7 +32,29 @@ class ConfigurableEntityReferenceItem extends EntityReferenceItem { static $propertyDefinitions; /** - * Overrides \Drupal\Core\Entity\Field\Type\EntityReferenceItem::getPropertyDefinitions(). + * The Field instance definition. + * + * @var \Drupal\field\Plugin\Core\Entity\FieldInstance + */ + protected $instance; + + /** + * Returns the field instance definition. + * + * Copied from \Drupal\field\Plugin\Type\FieldType\ConfigFieldItemBase, + * since we cannot extend it. + * + * @var \Drupal\field\Plugin\Core\Entity\FieldInstance + */ + public function getInstance() { + if (!isset($this->instance) && $parent = $this->getParent()) { + $this->instance = $parent->getInstance(); + } + return $this->instance; + } + + /** + * {@inheritdoc} */ public function getPropertyDefinitions() { // Definitions vary by entity type, so key them by entity type. @@ -62,4 +86,84 @@ public function getPropertyDefinitions() { return static::$propertyDefinitions[$target_type]; } + /** + * {@inheritdoc} + * + * Copied from \Drupal\field\Plugin\field\field_type\LegacyConfigFieldItem, + * since we cannot extend it. + */ + public static function schema(Field $field) { + $definition = \Drupal::typedData()->getDefinition('configurable_field_type:' . $field->type); + $module = $definition['module']; + module_load_install($module); + $callback = "{$module}_field_schema"; + if (function_exists($callback)) { + return $callback($field); + } + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + // Avoid loading the entity by first checking the 'target_id'. + $target_id = $this->get('target_id')->getValue(); + if (!empty($target_id) && is_numeric($target_id)) { + return FALSE; + } + if (empty($target_id) && ($entity = $this->get('entity')->getValue()) && $entity->isNew()) { + return FALSE; + } + return TRUE; + } + + /** + * {@inheritdoc} + * + * Copied from \Drupal\field\Plugin\field\field_type\LegacyConfigFieldItem, + * since we cannot extend it. + */ + public function settingsForm(array $form, array &$form_state) { + if ($callback = $this->getLegacyCallback('settings_form')) { + // hook_field_settings_form() used to receive the $instance (not actually + // needed), and the value of field_has_data(). + return $callback($this->getInstance()->getField(), $this->getInstance(), $this->getInstance()->getField()->hasData()); + } + return array(); + } + + /** + * {@inheritdoc} + * + * Copied from \Drupal\field\Plugin\field\field_type\LegacyConfigFieldItem, + * since we cannot extend it. + */ + public function instanceSettingsForm(array $form, array &$form_state) { + if ($callback = $this->getLegacyCallback('instance_settings_form')) { + return $callback($this->getInstance()->getField(), $this->getInstance(), $form_state); + } + return array(); + } + + /** + * Returns the legacy callback for a given field type "hook". + * + * Copied from \Drupal\field\Plugin\field\field_type\LegacyConfigFieldItem, + * since we cannot extend it. + * + * @param string $hook + * The name of the hook, e.g. 'settings_form', 'is_empty'. + * + * @return string|null + * The name of the legacy callback, or NULL if it does not exist. + */ + protected function getLegacyCallback($hook) { + $definition = $this->getPluginDefinition(); + $module = $definition['module']; + $callback = "{$module}_field_{$hook}"; + if (function_exists($callback)) { + return $callback; + } + } + } diff --git a/core/modules/field/field.api.php b/core/modules/field/field.api.php index 3535b00..057027d 100644 --- a/core/modules/field/field.api.php +++ b/core/modules/field/field.api.php @@ -105,9 +105,7 @@ function hook_field_extra_fields_alter(&$info) { * In the Field API, each field has a type, which determines what kind of data * (integer, string, date, etc.) the field can hold, which settings it provides, * and so on. The data type(s) accepted by a field are defined in - * hook_field_schema(); other basic properties of a field are defined in - * hook_field_info(). The other hooks below are called by the Field Attach API - * to perform field-type-specific actions. + * hook_field_schema(). * * The Field Types API also defines two kinds of pluggable handlers: widgets * and formatters. @link field_widget Widgets @endlink specify how the field @@ -121,76 +119,13 @@ function hook_field_extra_fields_alter(&$info) { * the Field API. */ -/** - * Define Field API field types. - * - * @return - * An array whose keys are field type names and whose values are arrays - * describing the field type, with the following key/value pairs: - * - label: The human-readable name of the field type. - * - description: A short description for the field type. - * - settings: An array whose keys are the names of the settings available - * for the field type, and whose values are the default values for those - * settings. - * - instance_settings: An array whose keys are the names of the settings - * available for instances of the field type, and whose 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 schema definition, or settings that Views needs to use - * across field instances (for example, the list of allowed values). - * - default_widget: The machine name of the default widget to be used by - * instances of this field type, when no widget is specified in the instance - * definition. 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). - * - default_formatter: The machine name of the default formatter to be used - * by instances of this field type, when no formatter is specified in the - * instance definition. 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). - * - no_ui: (optional) A boolean specifying that users should not be allowed - * to create fields and instances of this field type through the UI. Such - * fields can only be created programmatically. Defaults to FALSE. - * - * @see hook_field_info_alter() - */ -function hook_field_info() { - return array( - 'text' => array( - 'label' => t('Text'), - 'description' => t('This field stores varchar text in the database.'), - 'settings' => array('max_length' => 255), - 'instance_settings' => array('text_processing' => 0), - 'default_widget' => 'text_textfield', - 'default_formatter' => 'text_default', - ), - 'text_long' => array( - 'label' => t('Long text'), - 'description' => t('This field stores long text in the database.'), - 'settings' => array('max_length' => ''), - 'instance_settings' => array('text_processing' => 0), - 'default_widget' => 'text_textarea', - 'default_formatter' => 'text_default', - ), - 'text_with_summary' => array( - 'label' => t('Long text and summary'), - 'description' => t('This field stores long text in the database along with optional summary text.'), - 'settings' => array('max_length' => ''), - 'instance_settings' => array('text_processing' => 1, 'display_summary' => 0), - 'default_widget' => 'text_textarea_with_summary', - 'default_formatter' => 'text_summary_or_trimmed', - ), - ); -} /** * Perform alterations on Field API field types. * * @param $info - * Array of information on field types exposed by hook_field_info() - * implementations. + * Array of information on field types as collected by the "field type" plugin + * manager. */ function hook_field_info_alter(&$info) { // Add a setting to all field types. @@ -207,466 +142,6 @@ function hook_field_info_alter(&$info) { } /** - * Define the Field API schema for a field structure. - * - * This hook MUST be defined in .install for it to be detected during - * installation and upgrade. - * - * @param $field - * A field structure. - * - * @return - * An associative array with the following keys: - * - columns: An array of Schema API column specifications, keyed by column - * name. This specifies what comprises a value for a given field. For - * example, a value for a number field is simply 'value', while a value for - * a formatted text field is the combination of 'value' and 'format'. It is - * recommended to avoid having the column definitions depend on field - * settings when possible. No assumptions should be made on how storage - * engines internally use the original column name to structure their - * storage. - * - indexes: (optional) An array of Schema API index definitions. Only - * columns that appear in the 'columns' array are allowed. Those indexes - * will be used as default indexes. Individual field definitions can - * specify additional indexes or modify, at their own risk, the indexes - * specified by the field type. Some storage engines might not support - * indexes. - * - foreign keys: (optional) An array of Schema API foreign key definitions. - * Note, however, that the field data is not necessarily stored in SQL. - * Also, the possible usage is limited, as you cannot specify another field - * as related, only existing SQL tables, such as {taxonomy_term_data}. - */ -function hook_field_schema($field) { - if ($field['type'] == 'text_long') { - $columns = array( - 'value' => array( - 'type' => 'text', - 'size' => 'big', - 'not null' => FALSE, - ), - ); - } - else { - $columns = array( - 'value' => array( - 'type' => 'varchar', - 'length' => $field['settings']['max_length'], - 'not null' => FALSE, - ), - ); - } - $columns += array( - 'format' => array( - 'type' => 'varchar', - 'length' => 255, - 'not null' => FALSE, - ), - ); - return array( - 'columns' => $columns, - 'indexes' => array( - 'format' => array('format'), - ), - 'foreign keys' => array( - 'format' => array( - 'table' => 'filter_format', - 'columns' => array('format' => 'format'), - ), - ), - ); -} - -/** - * Define custom load behavior for this module's field types. - * - * Unlike most other field hooks, this hook operates on multiple entities. The - * $entities, $instances and $items parameters are arrays keyed by entity ID. - * For performance reasons, information for all available entity should be - * loaded in a single query where possible. - * - * Note that the changes made to the field values get cached by the field cache - * for subsequent loads. You should never use this hook to load fieldable - * entities, since this is likely to cause infinite recursions when - * hook_field_load() is run on those as well. Use - * hook_field_formatter_prepare_view() instead. - * - * Make changes or additions to field values by altering the $items parameter by - * reference. There is no return value. - * - * @param $entity_type - * The type of $entity. - * @param $entities - * Array of entities being loaded, keyed by entity ID. - * @param $field - * The field structure for the operation. - * @param $instances - * Array of instance structures for $field for each entity, keyed by entity - * ID. - * @param $langcode - * The language code associated with $items. - * @param $items - * Array of field values already loaded for the entities, keyed by entity ID. - * Store your changes in this parameter (passed by reference). - * @param $age - * FIELD_LOAD_CURRENT to load the most recent revision for all fields, or - * FIELD_LOAD_REVISION to load the version indicated by each entity. - */ -function hook_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) { - // Sample code from text.module: precompute sanitized strings so they are - // stored in the field cache. - foreach ($entities as $id => $entity) { - foreach ($items[$id] as $delta => $item) { - // Only process items with a cacheable format, the rest will be handled - // by formatters if needed. - if (empty($instances[$id]['settings']['text_processing']) || filter_format_allowcache($item['format'])) { - $items[$id][$delta]['safe_value'] = isset($item['value']) ? text_sanitize($instances[$id]['settings']['text_processing'], $langcode, $item, 'value') : ''; - if ($field['type'] == 'text_with_summary') { - $items[$id][$delta]['safe_summary'] = isset($item['summary']) ? text_sanitize($instances[$id]['settings']['text_processing'], $langcode, $item, 'summary') : ''; - } - } - } - } -} - -/** - * Prepare field values prior to display. - * - * This hook is invoked before the field values are handed to formatters for - * display, and runs before the formatters' own - * hook_field_formatter_prepare_view(). - * - * Unlike most other field hooks, this hook operates on multiple entities. The - * $entities, $instances and $items parameters are arrays keyed by entity ID. - * For performance reasons, information for all available entities should be - * loaded in a single query where possible. - * - * Make changes or additions to field values by altering the $items parameter by - * reference. There is no return value. - * - * @param $entity_type - * The type of $entity. - * @param $entities - * Array of entities being displayed, keyed by entity ID. - * @param $field - * The field structure for the operation. - * @param $instances - * Array of instance structures for $field for each entity, keyed by entity - * ID. - * @param $langcode - * The language associated with $items. - * @param $items - * $entity->{$field['field_name']}, or an empty array if unset. - */ -function hook_field_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items) { - // Sample code from image.module: if there are no images specified at all, - // use the default image. - foreach ($entities as $id => $entity) { - if (empty($items[$id]) && $field['settings']['default_image']) { - if ($file = file_load($field['settings']['default_image'])) { - $items[$id][0] = (array) $file + array( - 'is_default' => TRUE, - 'alt' => '', - 'title' => '', - ); - } - } - } -} - -/** - * Validate this module's field data. - * - * If there are validation problems, add to the $errors array (passed by - * reference). There is no return value. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity for the operation. - * @param $field - * The field structure for the operation. - * @param $instance - * The instance structure for $field on $entity's bundle. - * @param $langcode - * The language associated with $items. - * @param $items - * $entity->{$field['field_name']}[$langcode], or an empty array if unset. - * @param $errors - * The array of errors (keyed by field name, language code, and delta) that - * have already been reported for the entity. The function should add its - * errors to this array. Each error is an associative array with the following - * keys and values: - * - error: An error code (should be a string prefixed with the module name). - * - message: The human-readable message to be displayed. - */ -function hook_field_validate(\Drupal\Core\Entity\EntityInterface $entity = NULL, $field, $instance, $langcode, $items, &$errors) { - foreach ($items as $delta => $item) { - if (!empty($item['value'])) { - if (!empty($field['settings']['max_length']) && drupal_strlen($item['value']) > $field['settings']['max_length']) { - $errors[$field['field_name']][$langcode][$delta][] = array( - 'error' => 'text_max_length', - 'message' => t('%name: the value may not be longer than %max characters.', array('%name' => $instance['label'], '%max' => $field['settings']['max_length'])), - ); - } - } - } -} - -/** - * Define custom presave behavior for this module's field types. - * - * Make changes or additions to field values by altering the $items parameter by - * reference. There is no return value. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity for the operation. - * @param $field - * The field structure for the operation. - * @param $instance - * The instance structure for $field on $entity's bundle. - * @param $langcode - * The language associated with $items. - * @param $items - * $entity->{$field['field_name']}[$langcode], or an empty array if unset. - */ -function hook_field_presave(\Drupal\Core\Entity\EntityInterface $entity, $field, $instance, $langcode, &$items) { - if ($field['type'] == 'number_decimal') { - // Let PHP round the value to ensure consistent behavior across storage - // backends. - foreach ($items as $delta => $item) { - if (isset($item['value'])) { - $items[$delta]['value'] = round($item['value'], $field['settings']['scale']); - } - } - } -} - -/** - * Define custom insert behavior for this module's field data. - * - * This hook is invoked from field_attach_insert() on the module that defines a - * field, during the process of inserting an entity object (node, taxonomy term, - * etc.). It is invoked just before the data for this field on the particular - * entity object is inserted into field storage. Only field modules that are - * storing or tracking information outside the standard field storage mechanism - * need to implement this hook. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity for the operation. - * @param $field - * The field structure for the operation. - * @param $instance - * The instance structure for $field on $entity's bundle. - * @param $langcode - * The language associated with $items. - * @param $items - * $entity->{$field['field_name']}[$langcode], or an empty array if unset. - * - * @see hook_field_update() - * @see hook_field_delete() - */ -function hook_field_insert(\Drupal\Core\Entity\EntityInterface $entity, $field, $instance, $langcode, &$items) { - if (config('taxonomy.settings')->get('maintain_index_table') && $field['storage']['type'] == 'field_sql_storage' && $entity->entityType() == 'node' && $entity->status) { - $query = db_insert('taxonomy_index')->fields(array('nid', 'tid', 'sticky', 'created', )); - foreach ($items as $item) { - $query->values(array( - 'nid' => $entity->nid, - 'tid' => $item['tid'], - 'sticky' => $entity->sticky, - 'created' => $entity->created, - )); - } - $query->execute(); - } -} - -/** - * Define custom update behavior for this module's field data. - * - * This hook is invoked from field_attach_update() on the module that defines a - * field, during the process of updating an entity object (node, taxonomy term, - * etc.). It is invoked just before the data for this field on the particular - * entity object is updated into field storage. Only field modules that are - * storing or tracking information outside the standard field storage mechanism - * need to implement this hook. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity for the operation. - * @param $field - * The field structure for the operation. - * @param $instance - * The instance structure for $field on $entity's bundle. - * @param $langcode - * The language associated with $items. - * @param $items - * $entity->{$field['field_name']}[$langcode], or an empty array if unset. - * - * @see hook_field_insert() - * @see hook_field_delete() - */ -function hook_field_update(\Drupal\Core\Entity\EntityInterface $entity, $field, $instance, $langcode, &$items) { - if (config('taxonomy.settings')->get('maintain_index_table') && $field['storage']['type'] == 'field_sql_storage' && $entity->entityType() == 'node') { - $first_call = &drupal_static(__FUNCTION__, array()); - - // We don't maintain data for old revisions, so clear all previous values - // from the table. Since this hook runs once per field, per object, make - // sure we only wipe values once. - if (!isset($first_call[$entity->nid])) { - $first_call[$entity->nid] = FALSE; - db_delete('taxonomy_index')->condition('nid', $entity->nid)->execute(); - } - // Only save data to the table if the node is published. - if ($entity->status) { - $query = db_insert('taxonomy_index')->fields(array('nid', 'tid', 'sticky', 'created')); - foreach ($items as $item) { - $query->values(array( - 'nid' => $entity->nid, - 'tid' => $item['tid'], - 'sticky' => $entity->sticky, - 'created' => $entity->created, - )); - } - $query->execute(); - } - } -} - -/** - * Update the storage information for a field. - * - * This is invoked on the field's storage module when updating the field, - * before the new definition is saved to the database. The field storage module - * should update its storage tables according to the new field definition. If - * there is a problem, the field storage module should throw an exception. - * - * @param $field - * The updated field structure to be saved. - * @param $prior_field - * The previously-saved field structure. - */ -function hook_field_storage_update_field($field, $prior_field) { - if (!$field->hasData()) { - // There is no data. Re-create the tables completely. - $prior_schema = _field_sql_storage_schema($prior_field); - foreach ($prior_schema as $name => $table) { - db_drop_table($name, $table); - } - $schema = _field_sql_storage_schema($field); - foreach ($schema as $name => $table) { - db_create_table($name, $table); - } - } - else { - // There is data. See field_sql_storage_field_storage_update_field() for - // an example of what to do to modify the schema in place, preserving the - // old data as much as possible. - } - drupal_get_schema(NULL, TRUE); -} - -/** - * Define custom delete behavior for this module's field data. - * - * This hook is invoked from field_attach_delete() on the module that defines a - * field, during the process of deleting an entity object (node, taxonomy term, - * etc.). It is invoked just before the data for this field on the particular - * entity object is deleted from field storage. Only field modules that are - * storing or tracking information outside the standard field storage mechanism - * need to implement this hook. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity for the operation. - * @param $field - * The field structure for the operation. - * @param $instance - * The instance structure for $field on $entity's bundle. - * @param $langcode - * The language associated with $items. - * @param $items - * $entity->{$field['field_name']}[$langcode], or an empty array if unset. - * - * @see hook_field_insert() - * @see hook_field_update() - */ -function hook_field_delete(\Drupal\Core\Entity\EntityInterface $entity, $field, $instance, $langcode, &$items) { - // Delete all file usages within this entity. - foreach ($items as $delta => $item) { - file_usage()->delete(file_load($item['fid']), 'file', $entity->entityType(), $entity->id(), 0); - } -} - -/** - * Define custom revision delete behavior for this module's field types. - * - * This hook is invoked just before the data is deleted from field storage in - * field_attach_delete_revision(), and will only be called for fieldable types - * that are versioned. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity for the operation. - * @param $field - * The field structure for the operation. - * @param $instance - * The instance structure for $field on $entity's bundle. - * @param $langcode - * The language associated with $items. - * @param $items - * $entity->{$field['field_name']}[$langcode], or an empty array if unset. - */ -function hook_field_delete_revision(\Drupal\Core\Entity\EntityInterface $entity, $field, $instance, $langcode, &$items) { - foreach ($items as $delta => $item) { - // Decrement the file usage count by 1. - file_usage()->delete(file_load($item['fid']), 'file', $entity->entityType(), $entity->id()); - } -} - -/** - * Define custom prepare_translation behavior for this module's field types. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity for the operation. - * @param $field - * The field structure for the operation. - * @param $instance - * The instance structure for $field on $entity's bundle. - * @param $langcode - * The language associated with $items. - * @param $items - * $entity->{$field['field_name']}[$langcode], or an empty array if unset. - * @param $source_entity - * The source entity from which field values are being copied. - * @param $source_langcode - * The source language from which field values are being copied. - */ -function hook_field_prepare_translation(\Drupal\Core\Entity\EntityInterface $entity, $field, $instance, $langcode, &$items, $source_entity, $source_langcode) { - // If the translating user is not permitted to use the assigned text format, - // we must not expose the source values. - $field_name = $field['field_name']; - $formats = filter_formats(); - $format_id = $source_entity->{$field_name}[$source_langcode][0]['format']; - if (!filter_access($formats[$format_id])) { - $items = array(); - } -} - -/** - * Define what constitutes an empty item for a field type. - * - * @param array $item - * An item that may or may not be empty. - * @param string $field_type - * The field type to which $item belongs. - * - * @return bool - * TRUE if the field type considers $item not to contain any data; FALSE - * otherwise. - */ -function hook_field_is_empty($item, $field_type) { - if (empty($item['value']) && (string) $item['value'] !== '0') { - return TRUE; - } - return FALSE; -} - -/** * @} End of "defgroup field_types". */ @@ -876,25 +351,6 @@ function hook_field_attach_load($entity_type, $entities, $age, $options) { } /** - * Act on field_attach_validate(). - * - * This hook is invoked after the field module has performed the operation. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity with fields to validate. - * @param $errors - * The array of errors (keyed by field name, language code, and delta) that - * have already been reported for the entity. The function should add its - * errors to this array. Each error is an associative array with the following - * keys and values: - * - error: An error code (should be a string prefixed with the module name). - * - message: The human-readable message to be displayed. - */ -function hook_field_attach_validate(\Drupal\Core\Entity\EntityInterface $entity, &$errors) { - // @todo Needs function body. -} - -/** * Act on field_attach_extract_form_values(). * * This hook is invoked after the field module has performed the operation. @@ -920,42 +376,6 @@ function hook_field_attach_extract_form_values(\Drupal\Core\Entity\EntityInterfa } /** - * Act on field_attach_presave(). - * - * This hook is invoked after the field module has performed the operation. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * the entity with fields to process. - */ -function hook_field_attach_presave(\Drupal\Core\Entity\EntityInterface $entity) { - // @todo Needs function body. -} - -/** - * Act on field_attach_insert(). - * - * This hook is invoked after the field module has performed the operation. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * the entity with fields to process. - */ -function hook_field_attach_insert(\Drupal\Core\Entity\EntityInterface $entity) { - // @todo Needs function body. -} - -/** - * Act on field_attach_update(). - * - * This hook is invoked after the field module has performed the operation. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * the entity with fields to process. - */ -function hook_field_attach_update(\Drupal\Core\Entity\EntityInterface $entity) { - // @todo Needs function body. -} - -/** * Alter field_attach_preprocess() variables. * * This hook is invoked while preprocessing field templates in @@ -974,30 +394,6 @@ function hook_field_attach_preprocess_alter(&$variables, $context) { } /** - * Act on field_attach_delete(). - * - * This hook is invoked after the field module has performed the operation. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * the entity with fields to process. - */ -function hook_field_attach_delete(\Drupal\Core\Entity\EntityInterface $entity) { - // @todo Needs function body. -} - -/** - * Act on field_attach_delete_revision(). - * - * This hook is invoked after the field module has performed the operation. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * the entity with fields to process. - */ -function hook_field_attach_delete_revision(\Drupal\Core\Entity\EntityInterface $entity) { - // @todo Needs function body. -} - -/** * Act on field_purge_data(). * * This hook is invoked in field_purge_data() and allows modules to act on @@ -1061,25 +457,6 @@ function hook_field_attach_view_alter(&$output, $context) { } /** - * Perform alterations on field_attach_prepare_translation(). - * - * This hook is invoked after the field module has performed the operation. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity being prepared for translation. - * @param $context - * An associative array containing: - * - langcode: The language the entity will be translated to. - * - source_entity: The entity holding the field values to be translated. - * - source_langcode: The source language from which to translate. - */ -function hook_field_attach_prepare_translation_alter(\Drupal\Core\Entity\EntityInterface $entity, $context) { - if ($entity->entityType() == 'custom_entity_type') { - $entity->custom_field = $context['source_entity']->custom_field; - } -} - -/** * Perform alterations on field_language() values. * * This hook is invoked to alter the array of display language codes for the @@ -1595,6 +972,39 @@ function hook_field_storage_create_field($field) { } /** + * Update the storage information for a field. + * + * This is invoked on the field's storage module when updating the field, + * before the new definition is saved to the database. The field storage module + * should update its storage tables according to the new field definition. If + * there is a problem, the field storage module should throw an exception. + * + * @param $field + * The updated field structure to be saved. + * @param $prior_field + * The previously-saved field structure. + */ +function hook_field_storage_update_field($field, $prior_field) { + if (!$field->hasData()) { + // There is no data. Re-create the tables completely. + $prior_schema = _field_sql_storage_schema($prior_field); + foreach ($prior_schema as $name => $table) { + db_drop_table($name, $table); + } + $schema = _field_sql_storage_schema($field); + foreach ($schema as $name => $table) { + db_create_table($name, $table); + } + } + else { + // There is data. See field_sql_storage_field_storage_update_field() for + // an example of what to do to modify the schema in place, preserving the + // old data as much as possible. + } + drupal_get_schema(NULL, TRUE); +} + +/** * Act on deletion of a field. * * This hook is invoked during the deletion of a field to ask the field storage diff --git a/core/modules/field/field.attach.inc b/core/modules/field/field.attach.inc index 7965679..862dfa1 100644 --- a/core/modules/field/field.attach.inc +++ b/core/modules/field/field.attach.inc @@ -5,10 +5,11 @@ * Field attach API, allowing entities (nodes, users, ...) to be 'fieldable'. */ -use Drupal\field\FieldValidationException; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityNG; use Drupal\entity\Plugin\Core\Entity\EntityDisplay; use Drupal\entity\Plugin\Core\Entity\EntityFormDisplay; +use Drupal\Core\Language\Language; /** * @defgroup field_storage Field Storage API @@ -116,9 +117,6 @@ /** * Invokes a method on all the fields of a given entity. * - * @todo Remove _field_invoke() and friends when field types and formatters are - * turned into plugins. - * * @param string $method * The name of the method to invoke. * @param callable $target_function @@ -337,331 +335,21 @@ function field_invoke_method_multiple($method, $target_function, array $entities } /** - * Invoke a field hook. - * - * @param $op - * Possible operations include: - * - form - * - validate - * - presave - * - insert - * - update - * - delete - * - delete revision - * - view - * - prepare translation - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity object. - * @param $a - * - The $form in the 'form' operation. - * - The value of $view_mode in the 'view' operation. - * - Otherwise NULL. - * @param $b - * - The $form_state in the 'submit' operation. - * - Otherwise NULL. - * @param $options - * An associative array of additional options, with the following keys: - * - 'field_name': The name of the field whose operation should be - * invoked. By default, the operation is invoked on all the fields - * in the entity's bundle. NOTE: This option is not compatible with - * the 'deleted' option; the 'field_id' option should be used - * instead. - * - 'field_id': The id of the field whose operation should be - * invoked. By default, the operation is invoked on all the fields - * in the entity's' bundles. - * - 'default': A boolean value, specifying which implementation of - * the operation should be invoked. - * - if FALSE (default), the field types implementation of the operation - * will be invoked (hook_field_[op]) - * - If TRUE, the default field implementation of the field operation - * will be invoked (field_default_[op]) - * Internal use only. Do not explicitely set to TRUE, but use - * _field_invoke_default() instead. - * - 'deleted': If TRUE, the function will operate on deleted fields - * as well as non-deleted fields. If unset or FALSE, only - * non-deleted fields are operated on. - * - 'langcode': A language code or an array of language codes keyed by field - * name. It will be used to narrow down to a single value the available - * languages to act on. - */ -function _field_invoke($op, EntityInterface $entity, &$a = NULL, &$b = NULL, $options = array()) { - // Merge default options. - $default_options = array( - 'default' => FALSE, - 'deleted' => FALSE, - 'langcode' => NULL, - ); - $options += $default_options; - - // Determine the list of instances to iterate on. - $instances = _field_invoke_get_instances($entity->entityType(), $entity->bundle(), $options); - - // Iterate through the instances and collect results. - $return = array(); - foreach ($instances as $instance) { - // field_info_field() is not available for deleted fields, so use - // field_info_field_by_id(). - $field = field_info_field_by_id($instance['field_id']); - $field_name = $field['field_name']; - $function = $options['default'] ? 'field_default_' . $op : $field['module'] . '_field_' . $op; - if (function_exists($function)) { - // Determine the list of languages to iterate on. - $available_langcodes = field_available_languages($entity->entityType(), $field); - $langcodes = _field_language_suggestion($available_langcodes, $options['langcode'], $field_name); - - foreach ($langcodes as $langcode) { - $items = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array(); - $result = $function($entity, $field, $instance, $langcode, $items, $a, $b); - if (isset($result)) { - // For hooks with array results, we merge results together. - // For hooks with scalar results, we collect results in an array. - if (is_array($result)) { - $return = array_merge($return, $result); - } - else { - $return[] = $result; - } - } - - // Populate $items back in the field values, but avoid replacing missing - // fields with an empty array (those are not equivalent on update). - if ($items !== array() || isset($entity->{$field_name}[$langcode])) { - $entity->{$field_name}[$langcode] = $items; - } - } - } - } - - return $return; -} - -/** - * Invokes a field hook across fields on multiple entities. - * - * @param $op - * Possible operations include: - * - load - * - prepare_view - * For all other operations, use _field_invoke() / field_invoke_default() - * instead. - * @param $entity_type - * The type of entities in $entities; e.g. 'node' or 'user'. - * @param $entities - * An array of entities, keyed by entity ID. - * @param $a - * - The $age parameter in the 'load' operation. - * - Otherwise NULL. - * @param $b - * Currently always NULL. - * @param $options - * An associative array of additional options, with the following keys: - * - field_name: The name of the field whose operation should be invoked. By - * default, the operation is invoked on all the fields in the entity's - * bundle. NOTE: This option is not compatible with the 'deleted' option; - * the 'field_id' option should be used instead. - * - field_id: The ID of the field whose operation should be invoked. By - * default, the operation is invoked on all the fields in the entity's - * bundles. - * - default: A boolean value, specifying which implementation of the - * operation should be invoked. - * - if FALSE (default), the field types implementation of the operation - * will be invoked (hook_field_[op]) - * - If TRUE, the default field implementation of the field operation will - * be invoked (field_default_[op]) - * Internal use only. Do not explicitely set to TRUE, but use - * _field_invoke_multiple_default() instead. - * - deleted: If TRUE, the function will operate on deleted fields as well as - * non-deleted fields. If unset or FALSE, only non-deleted fields are - * operated on. - * - langcode: A language code or an array of arrays of language codes keyed - * by entity ID and field name. It will be used to narrow down to a single - * value the available languages to act on. - * - * @return - * An array of returned values keyed by entity ID. - */ -function _field_invoke_multiple($op, $entity_type, $entities, &$a = NULL, &$b = NULL, $options = array()) { - // Merge default options. - $default_options = array( - 'default' => FALSE, - 'deleted' => FALSE, - 'langcode' => NULL, - ); - $options += $default_options; - - $fields = array(); - $grouped_instances = array(); - $grouped_entities = array(); - $grouped_items = array(); - $return = array(); - - // Go through the entities and collect the fields on which the hook should be - // invoked. - // - // We group fields by ID, not by name, because this function can operate on - // deleted fields which may have non-unique names. However, entities can only - // contain data for a single field for each name, even if that field - // is deleted, so we reference field data via the - // $entity->$field_name property. - foreach ($entities as $entity) { - // Determine the list of instances to iterate on. - $instances = _field_invoke_get_instances($entity_type, $entity->bundle(), $options); - $id = $entity->id(); - - foreach ($instances as $instance) { - $field_id = $instance['field_id']; - $field_name = $instance['field_name']; - $field = field_info_field_by_id($field_id); - $function = $options['default'] ? 'field_default_' . $op : $field['module'] . '_field_' . $op; - if (function_exists($function)) { - // Add the field to the list of fields to invoke the hook on. - if (!isset($fields[$field_id])) { - $fields[$field_id] = $field; - } - // Extract the field values into a separate variable, easily accessed - // by hook implementations. - // Unless a language code suggestion is provided we iterate on all the - // available language codes. - $available_langcodes = field_available_languages($entity_type, $field); - $langcode = !empty($options['langcode'][$id]) ? $options['langcode'][$id] : $options['langcode']; - $langcodes = _field_language_suggestion($available_langcodes, $langcode, $field_name); - foreach ($langcodes as $langcode) { - $grouped_items[$field_id][$langcode][$id] = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array(); - // Group the instances and entities corresponding to the current - // field. - $grouped_instances[$field_id][$langcode][$id] = $instance; - $grouped_entities[$field_id][$langcode][$id] = $entities[$id]; - } - } - } - // Initialize the return value for each entity. - $return[$id] = array(); - } - - // For each field, invoke the field hook and collect results. - foreach ($fields as $field_id => $field) { - $field_name = $field['field_name']; - $function = $options['default'] ? 'field_default_' . $op : $field['module'] . '_field_' . $op; - // Iterate over all the field translations. - foreach ($grouped_items[$field_id] as $langcode => &$items) { - $entities = $grouped_entities[$field_id][$langcode]; - $instances = $grouped_instances[$field_id][$langcode]; - $results = $function($entity_type, $entities, $field, $instances, $langcode, $items, $a, $b); - if (isset($results)) { - // Collect results by entity. - // For hooks with array results, we merge results together. - // For hooks with scalar results, we collect results in an array. - foreach ($results as $id => $result) { - if (is_array($result)) { - $return[$id] = array_merge($return[$id], $result); - } - else { - $return[$id][] = $result; - } - } - } - } - - // Populate field values back in the entities, but avoid replacing missing - // fields with an empty array (those are not equivalent on update). - foreach ($grouped_entities[$field_id] as $langcode => $entities) { - foreach ($entities as $id => $entity) { - if ($grouped_items[$field_id][$langcode][$id] !== array() || isset($entity->{$field_name}[$langcode])) { - $entity->{$field_name}[$langcode] = $grouped_items[$field_id][$langcode][$id]; - } - } - } - } - - return $return; -} - -/** - * Invoke field.module's version of a field hook. - * - * This function invokes the field_default_[op]() function. - * Use _field_invoke() to invoke the field type implementation, - * hook_field_[op](). - * - * @see _field_invoke() - */ -function _field_invoke_default($op, EntityInterface $entity, &$a = NULL, &$b = NULL, $options = array()) { - $options['default'] = TRUE; - return _field_invoke($op, $entity, $a, $b, $options); -} - -/** - * Invoke field.module's version of a field hook on multiple entities. - * - * This function invokes the field_default_[op]() function. - * Use _field_invoke_multiple() to invoke the field type implementation, - * hook_field_[op](). - * - * @param $op - * Possible operations include: - * - load - * - prepare_view - * For all other operations, use _field_invoke() / field_invoke_default() - * instead. - * @param $entity_type - * The type of entities in $entities; e.g. 'node' or 'user'. - * @param $entities - * An array of entities, keyed by entity ID. - * @param $a - * - The $age parameter in the 'load' operation. - * - Otherwise NULL. - * @param $b - * Currently always NULL. - * @param $options - * An associative array of additional options, with the following keys: - * - field_name: The name of the field whose operation should be invoked. By - * default, the operation is invoked on all the fields in the entity's - * bundle. NOTE: This option is not compatible with the 'deleted' option; - * the 'field_id' option should be used instead. - * - field_id: The ID of the field whose operation should be invoked. By - * default, the operation is invoked on all the fields in the entity's - * bundles. - * - default': A boolean value, specifying which implementation of the - * operation should be invoked. - * - if FALSE (default), the field types implementation of the operation - * will be invoked (hook_field_[op]) - * - If TRUE, the default field implementation of the field operation will - * be invoked (field_default_[op]) - * Internal use only. Do not explicitely set to TRUE, but use - * _field_invoke_multiple_default() instead. - * - deleted: If TRUE, the function will operate on deleted fields as well as - * non-deleted fields. If unset or FALSE, only non-deleted fields are - * operated on. - * - language: A language code or an array of arrays of language codes keyed - * by entity ID and field name. It will be used to narrow down to a single - * value the available languages to act on. - * - * @return - * An array of returned values keyed by entity ID. - * - * @see _field_invoke_multiple() - */ -function _field_invoke_multiple_default($op, $entity_type, $entities, &$a = NULL, &$b = NULL, $options = array()) { - $options['default'] = TRUE; - return _field_invoke_multiple($op, $entity_type, $entities, $a, $b, $options); -} - -/** * Retrieves a list of instances to operate on. * - * Helper for _field_invoke(). + * Helper for field_invoke_method(). * * @param $entity_type * The entity type. * @param $bundle * The bundle name. * @param $options - * An associative array of options, as provided to _field_invoke(). Only the - * following keys are considered: + * An associative array of options, as provided to field_invoke_method(). Only + * the following keys are considered: * - deleted * - field_name * - field_id - * See _field_invoke() for details. + * See field_invoke_method() for details. * * @return * The array of selected instance definitions. @@ -732,8 +420,7 @@ function _field_invoke_widget_target($form_display) { * appear within the same $form element, or within the same '#parents' space. * * For each call to field_attach_form(), field values are processed by calling - * field_attach_form_validate() and field_attach_extract_form_values() on the - * same $form element. + * field_attach_extract_form_values() on the same $form element. * * Sample resulting structure in $form: * @code @@ -872,28 +559,30 @@ function field_attach_form(EntityInterface $entity, &$form, &$form_state, $langc * FIELD_LOAD_REVISION. * @param $options * An associative array of additional options, with the following keys: - * - field_id: The field ID that should be loaded, instead of loading all - * fields, for each entity. Note that returned entities may contain data for - * other fields, for example if they are read from a cache. - * - deleted: If TRUE, the function will operate on deleted fields as well as - * non-deleted fields. If unset or FALSE, only non-deleted fields are - * operated on. + * - instance: A field instance entity, If provided, only values for the + * corresponding field will be loaded, and the cache is bypassed. This + * option is only supported when all $entities are within the same bundle. */ function field_attach_load($entity_type, $entities, $age = FIELD_LOAD_CURRENT, $options = array()) { $load_current = $age == FIELD_LOAD_CURRENT; + $load_deleted = !empty($options['instance']->deleted); // Merge default options. - $default_options = array( - 'deleted' => FALSE, + $options += array('instance' => NULL); + // Set options for hook invocations. + $hook_options = array( + 'deleted' => $load_deleted, ); - $options += $default_options; + if ($options['instance']) { + $hook_options['field_id'] = $options['instance']->field_uuid; + } $info = entity_get_info($entity_type); // Only the most current revision of non-deleted fields for cacheable entity // types can be cached. - $cache_read = $load_current && $info['field_cache'] && empty($options['deleted']); + $cache_read = $load_current && $info['field_cache'] && !$load_deleted; // In addition, do not write to the cache when loading a single field. - $cache_write = $cache_read && !isset($options['field_id']); + $cache_write = $cache_read && !isset($options['instance']); if (empty($entities)) { return; @@ -934,7 +623,7 @@ function field_attach_load($entity_type, $entities, $age = FIELD_LOAD_CURRENT, $ // The invoke order is: // - hook_field_storage_pre_load() // - storage backend's hook_field_storage_load() - // - field-type module's hook_field_load() + // - Field class's prepareCache() method. // - hook_field_attach_load() // Invoke hook_field_storage_pre_load(): let any module load field @@ -942,28 +631,31 @@ function field_attach_load($entity_type, $entities, $age = FIELD_LOAD_CURRENT, $ $skip_fields = array(); foreach (module_implements('field_storage_pre_load') as $module) { $function = $module . '_field_storage_pre_load'; - $function($entity_type, $queried_entities, $age, $skip_fields, $options); + $function($entity_type, $queried_entities, $age, $skip_fields, $hook_options); } - $instances = array(); - // Collect the storage backends used by the remaining fields in the entities. $storages = array(); foreach ($queried_entities as $entity) { - $instances = _field_invoke_get_instances($entity_type, $entity->bundle(), $options); $id = $entity->id(); $vid = $entity->getRevisionId(); + + // Determine the list of field instances to work on. + if ($options['instance']) { + $instances = array($options['instance']); + } + else { + $instances = field_info_instances($entity_type, $entity->bundle()); + } + foreach ($instances as $instance) { - $field_name = $instance['field_name']; - $field_id = $instance['field_id']; - // Make sure all fields are present at least as empty arrays. + $field = $instance->getField(); + $field_name = $field->id(); if (!isset($queried_entities[$id]->{$field_name})) { $queried_entities[$id]->{$field_name} = array(); } - // Collect the storage backend if the field has not been loaded yet. - if (!isset($skip_fields[$field_id])) { - $field = field_info_field_by_id($field_id); - $storages[$field['storage']['type']][$field_id][] = $load_current ? $id : $vid; + if (!isset($skip_fields[$field->uuid])) { + $storages[$field->storage['type']][$field->uuid][] = $load_current ? $id : $vid; } } } @@ -971,12 +663,36 @@ function field_attach_load($entity_type, $entities, $age = FIELD_LOAD_CURRENT, $ // Invoke hook_field_storage_load() on the relevant storage backends. foreach ($storages as $storage => $fields) { $storage_info = field_info_storage_types($storage); - module_invoke($storage_info['module'], 'field_storage_load', $entity_type, $queried_entities, $age, $fields, $options); + module_invoke($storage_info['module'], 'field_storage_load', $entity_type, $queried_entities, $age, $fields, $hook_options); } - // Invoke field-type module's hook_field_load(). - $null = NULL; - _field_invoke_multiple('load', $entity_type, $queried_entities, $age, $null, $options); + // Invoke the field type's prepareCache() method. + if (empty($options['instance'])) { + foreach ($queried_entities as $entity) { + \Drupal::entityManager() + ->getStorageController($entity_type) + ->invokeFieldItemPrepareCache($entity); + } + } + else { + // Do not rely on invokeFieldItemPrepareCache(), which only works on + // fields listed in getFieldDefinitions(), and will fail if we are loading + // values for a deleted field. Instead, generate FieldItem objects + // directly, and call their prepareCache() method. + foreach ($queried_entities as $entity) { + $field = $options['instance']->getField(); + $field_name = $field->id(); + // Call the prepareCache() method on each item. + foreach ($entity->{$field_name} as $langcode => $values) { + $definition = _field_generate_entity_field_definition($field, $options['instance']); + $items = \Drupal::typedData()->create($definition, $values, $field_name, $entity); + foreach ($items as $item) { + $item->prepareCache(); + } + $entity->{$field_name}[$langcode] = $items->getValue(); + } + } + } // Invoke hook_field_attach_load(): let other modules act on loading the // entity. @@ -1023,46 +739,6 @@ function field_attach_load_revision($entity_type, $entities, $options = array()) } /** - * Performs field validation against the field data in an entity. - * - * This function does not perform field widget validation on form submissions. - * It is intended to be called during API save operations. Use - * field_attach_form_validate() to validate form submissions. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity with fields to validate. - * @throws Drupal\field\FieldValidationException - * If validation errors are found, a FieldValidationException is thrown. The - * 'errors' property contains the array of errors, keyed by field name, - * language and delta. - * @param array $options - * An associative array of additional options. See field_invoke_method() for - * details. - */ -function field_attach_validate(EntityInterface $entity, array $options = array()) { - // Ensure we are working with a BC mode entity. - $entity = $entity->getBCEntity(); - - $errors = array(); - // Check generic, field-type-agnostic errors first. - $null = NULL; - _field_invoke_default('validate', $entity, $errors, $null, $options); - // Check field-type specific errors. - _field_invoke('validate', $entity, $errors, $null, $options); - - // Let other modules validate the entity. - // Avoid module_invoke_all() to let $errors be taken by reference. - foreach (module_implements('field_attach_validate') as $module) { - $function = $module . '_field_attach_validate'; - $function($entity, $errors); - } - - if ($errors) { - throw new FieldValidationException($errors); - } -} - -/** * Performs field validation against form-submitted field values. * * There are two levels of validation for fields in forms: widget validation and @@ -1092,25 +768,31 @@ function field_attach_validate(EntityInterface $entity, array $options = array() * details. */ function field_attach_form_validate(EntityInterface $entity, $form, &$form_state, array $options = array()) { - // Ensure we are working with a BC mode entity. - $entity = $entity->getBCEntity(); - - // Perform field_level validation. - try { - field_attach_validate($entity, $options); + // Only support NG entities. + if (!($entity->getNGEntity() instanceof EntityNG)) { + return; } - catch (FieldValidationException $e) { - // Pass field-level validation errors back to widgets for accurate error - // flagging. - foreach ($e->errors as $field_name => $field_errors) { - foreach ($field_errors as $langcode => $errors) { + + $has_violations = FALSE; + foreach ($entity as $field_name => $field) { + $definition = $field->getDefinition(); + if (!empty($definition['configurable']) && (empty($options['field_name']) || $options['field_name'] == $field_name)) { + $field_violations = $field->validate(); + if (count($field_violations)) { + $has_violations = TRUE; + + // Place violations in $form_state. + $langcode = field_is_translatable($entity->entityType(), field_info_field($field_name)) ? $entity->language()->langcode : Language::LANGCODE_NOT_SPECIFIED; $field_state = field_form_get_state($form['#parents'], $field_name, $langcode, $form_state); - $field_state['errors'] = $errors; + $field_state['constraint_violations'] = $field_violations; field_form_set_state($form['#parents'], $field_name, $langcode, $form_state, $field_state); } } - $form_display = $form_state['form_display']; - field_invoke_method('flagErrors', _field_invoke_widget_target($form_display), $entity, $form, $form_state, $options); + } + + if ($has_violations) { + // Map errors back to form elements. + field_invoke_method('flagErrors', _field_invoke_widget_target($form_state['form_display']), $entity, $form, $form_state, $options); } } @@ -1149,25 +831,6 @@ function field_attach_extract_form_values(EntityInterface $entity, $form, &$form } /** - * Performs necessary operations just before fields data get saved. - * - * We take no specific action here, we just give other modules the opportunity - * to act. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity with fields to process. - */ -function field_attach_presave($entity) { - // Ensure we are working with a BC mode entity. - $entity = $entity->getBCEntity(); - - _field_invoke('presave', $entity); - - // Let other modules act on presaving the entity. - module_invoke_all('field_attach_presave', $entity); -} - -/** * Save field data for a new entity. * * The passed-in entity must already contain its id and (if applicable) @@ -1185,8 +848,6 @@ function field_attach_insert(EntityInterface $entity) { // Ensure we are working with a BC mode entity. $entity = $entity->getBCEntity(); - _field_invoke('insert', $entity); - // Let any module insert field data before the storage engine, accumulating // saved fields along the way. $skip_fields = array(); @@ -1214,9 +875,6 @@ function field_attach_insert(EntityInterface $entity) { $storage_info = field_info_storage_types($storage); module_invoke($storage_info['module'], 'field_storage_write', $entity, FIELD_STORAGE_INSERT, $fields); } - - // Let other modules act on inserting the entity. - module_invoke_all('field_attach_insert', $entity); } /** @@ -1229,8 +887,6 @@ function field_attach_update(EntityInterface $entity) { // Ensure we are working with a BC mode entity. $entity = $entity->getBCEntity(); - _field_invoke('update', $entity); - // Let any module update field data before the storage engine, accumulating // saved fields along the way. $skip_fields = array(); @@ -1263,9 +919,6 @@ function field_attach_update(EntityInterface $entity) { module_invoke($storage_info['module'], 'field_storage_write', $entity, FIELD_STORAGE_UPDATE, $fields); } - // Let other modules act on updating the entity. - module_invoke_all('field_attach_update', $entity); - $entity_info = $entity->entityInfo(); if ($entity_info['field_cache']) { cache('field')->delete('field:' . $entity->entityType() . ':' . $entity->id()); @@ -1283,8 +936,6 @@ function field_attach_delete(EntityInterface $entity) { // Ensure we are working with a BC mode entity. $entity = $entity->getBCEntity(); - _field_invoke('delete', $entity); - // Collect the storage backends used by the fields in the entities. $storages = array(); foreach (field_info_instances($entity->entityType(), $entity->bundle()) as $instance) { @@ -1299,9 +950,6 @@ function field_attach_delete(EntityInterface $entity) { module_invoke($storage_info['module'], 'field_storage_delete', $entity, $fields); } - // Let other modules act on deleting the entity. - module_invoke_all('field_attach_delete', $entity); - $entity_info = $entity->entityInfo(); if ($entity_info['field_cache']) { cache('field')->delete('field:' . $entity->entityType() . ':' . $entity->id()); @@ -1319,8 +967,6 @@ function field_attach_delete_revision(EntityInterface $entity) { // Ensure we are working with a BC mode entity. $entity = $entity->getBCEntity(); - _field_invoke('delete_revision', $entity); - // Collect the storage backends used by the fields in the entities. $storages = array(); foreach (field_info_instances($entity->entityType(), $entity->bundle()) as $instance) { @@ -1334,9 +980,6 @@ function field_attach_delete_revision(EntityInterface $entity) { $storage_info = field_info_storage_types($storage); module_invoke($storage_info['module'], 'field_storage_delete_revision', $entity, $fields); } - - // Let other modules act on deleting the revision. - module_invoke_all('field_attach_delete_revision', $entity); } /** @@ -1362,11 +1005,8 @@ function field_attach_delete_revision(EntityInterface $entity) { * @param $langcode * (Optional) The language the field values are to be shown in. If no language * is provided the current language is used. - * @param array $options - * An associative array of additional options. See field_invoke_method() for - * details. */ -function field_attach_prepare_view($entity_type, array $entities, array $displays, $langcode = NULL, array $options = array()) { +function field_attach_prepare_view($entity_type, array $entities, array $displays, $langcode = NULL) { $options['langcode'] = array(); // To ensure hooks are only run once per entity, only process items without @@ -1390,9 +1030,11 @@ function field_attach_prepare_view($entity_type, array $entities, array $display } } - $null = NULL; // First let the field types do their preparation. - _field_invoke_multiple('prepare_view', $entity_type, $prepare, $null, $null, $options); + Drupal::entityManager() + ->getStorageController($entity_type) + ->invokeFieldMethodMultiple('prepareView', $entities, $langcode); + // Then let the formatters do their own specific massaging. For each // instance, call the prepareView() method on the formatter object handed by // the entity display. @@ -1401,7 +1043,8 @@ function field_attach_prepare_view($entity_type, array $entities, array $display return $displays[$instance['bundle']]->getFormatter($instance['field_name']); } }; - field_invoke_method_multiple('prepareView', $target_function, $prepare, $null, $null, $options); + $null = NULL; + field_invoke_method_multiple('prepareView', $target_function, $prepare, $null, $null); } /** @@ -1501,45 +1144,6 @@ function field_attach_preprocess(EntityInterface $entity, $element, &$variables) } /** - * Prepares an entity for translation. - * - * This function is used to fill in the form default values for Field API fields - * while performing entity translation. By default it copies all the source - * values in the given source language to the new entity and assigns them the - * target language. - * - * This is used as part of the 'per entity' translation pattern, which is - * implemented only for nodes by translation.module. Other entity types may be - * supported through contributed modules. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity to be prepared for translation. - * @param $langcode - * The language the entity has to be translated in. - * @param $source_entity - * The source entity holding the field values to be translated. - * @param $source_langcode - * The source language from which translate. - */ -function field_attach_prepare_translation(EntityInterface $entity, $langcode, EntityInterface $source_entity, $source_langcode) { - // Ensure we are working with a BC mode entity. - $entity = $entity->getBCEntity(); - - $options = array('langcode' => $langcode); - // Copy source field values into the entity to be prepared. - _field_invoke_default('prepare_translation', $entity, $source_entity, $source_langcode, $options); - // Let field types handle their own advanced translation pattern if needed. - _field_invoke('prepare_translation', $entity, $source_entity, $source_langcode, $options); - // Let other modules alter the entity translation. - $context = array( - 'langcode' => $langcode, - 'source_entity' => $source_entity, - 'source_langcode' => $source_langcode, - ); - drupal_alter('field_attach_prepare_translation', $entity, $context); -} - -/** * Implements hook_entity_bundle_create(). */ function field_entity_bundle_create($entity_type, $bundle) { diff --git a/core/modules/field/field.crud.inc b/core/modules/field/field.crud.inc index bbd0d45..647fef7 100644 --- a/core/modules/field/field.crud.inc +++ b/core/modules/field/field.crud.inc @@ -459,7 +459,7 @@ function field_purge_batch($batch_size) { $ids->entity_id = $entity_id; $entities[$entity_id] = _field_create_entity_from_ids($ids); } - field_attach_load($entity_type, $entities, FIELD_LOAD_CURRENT, array('field_id' => $field['uuid'], 'deleted' => 1)); + field_attach_load($entity_type, $entities, FIELD_LOAD_CURRENT, array('instance' => $instance)); foreach ($entities as $entity) { // Purge the data for the entity. field_purge_data($entity, $field, $instance); @@ -497,10 +497,11 @@ function field_purge_batch($batch_size) { * The deleted field instance whose data is being purged. */ function field_purge_data(EntityInterface $entity, $field, $instance) { - // Each field type's hook_field_delete() only expects to operate on a single - // field at a time, so we can use it as-is for purging. - $options = array('field_id' => $instance['field_id'], 'deleted' => TRUE); - _field_invoke('delete', $entity, $dummy, $dummy, $options); + foreach ($entity->{$field->id()} as $value) { + $definition = _field_generate_entity_field_definition($field, $instance); + $items = \Drupal::typedData()->create($definition, $value, $field->id(), $entity); + $items->delete(); + } // Tell the field storage system to purge the data. module_invoke($field['storage']['module'], 'field_storage_purge', $entity, $field, $instance); diff --git a/core/modules/field/field.default.inc b/core/modules/field/field.default.inc deleted file mode 100644 index cc6a393..0000000 --- a/core/modules/field/field.default.inc +++ /dev/null @@ -1,85 +0,0 @@ -{$field['field_name']}[$langcode], or an empty array if unset. - * @param $errors - * The array of errors, keyed by field name and by value delta, that have - * already been reported for the entity. The function should add its errors to - * this array. Each error is an associative array, with the following keys and - * values: - * - error: An error code (should be a string, prefixed with the module name). - * - message: The human readable message to be displayed. - */ -function field_default_validate(EntityInterface $entity, $field, $instance, $langcode, $items, &$errors) { - // Filter out empty values. - $items = _field_filter_items($field['type'], $items); - - // Check that the number of values doesn't exceed the field cardinality. - // For form submitted values, this can only happen with 'multiple value' - // widgets. - if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED && count($items) > $field['cardinality']) { - $errors[$field['field_name']][$langcode][0][] = array( - 'error' => 'field_cardinality', - 'message' => t('%name: this field cannot hold more than @count values.', array('%name' => $instance['label'], '@count' => $field['cardinality'])), - ); - } -} - -/** - * Copies source field values into the entity to be prepared. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity to be prepared for translation. - * @param $field - * The field structure for the operation. - * @param $instance - * The instance structure for $field in $entity's bundle. - * @param $langcode - * The language the entity has to be translated to. - * @param $items - * $entity->{$field['field_name']}[$langcode], or an empty array if unset. - * @param \Drupal\Core\Entity\EntityInterface $source_entity - * The source entity holding the field values to be translated. - * @param $source_langcode - * The source language from which to translate. - */ -function field_default_prepare_translation(EntityInterface $entity, $field, $instance, $langcode, &$items, EntityInterface $source_entity, $source_langcode) { - $field_name = $field['field_name']; - // If the field is untranslatable keep using Language::LANGCODE_NOT_SPECIFIED. - if ($langcode == Language::LANGCODE_NOT_SPECIFIED) { - $source_langcode = Language::LANGCODE_NOT_SPECIFIED; - } - if (isset($source_entity->{$field_name}[$source_langcode])) { - $items = $source_entity->{$field_name}[$source_langcode]; - } -} diff --git a/core/modules/field/field.form.inc b/core/modules/field/field.form.inc index 198aa47..f819c42 100644 --- a/core/modules/field/field.form.inc +++ b/core/modules/field/field.form.inc @@ -183,8 +183,8 @@ function field_add_more_js($form, $form_state) { * - items_count: The number of widgets to display for the field. * - array_parents: The location of the field's widgets within the $form * structure. This entry is populated at '#after_build' time. - * - errors: The array of field validation errors reported on the field. This - * entry is populated at field_attach_form_validate() time. + * - constraint_violations: The array of validation errors reported on the + * field. This entry is populated at form validate time. * * @see field_form_set_state() */ diff --git a/core/modules/field/field.info.inc b/core/modules/field/field.info.inc index f69649d..cbe7af8 100644 --- a/core/modules/field/field.info.inc +++ b/core/modules/field/field.info.inc @@ -37,6 +37,9 @@ function field_info_cache_clear() { // functions are moved to the entity API. entity_info_cache_clear(); + // Clear typed data definitions. + Drupal::typedData()->clearCachedDefinitions(); + _field_info_collate_types_reset(); Field::fieldInfo()->flush(); } @@ -46,11 +49,6 @@ function field_info_cache_clear() { * * @return * An associative array containing: - * - 'field types': Array of hook_field_info() results, keyed by field_type. - * Each element has the following components: label, description, settings, - * instance_settings, default_widget, default_formatter, and behaviors - * from hook_field_info(), as well as module, giving the module that exposes - * the field type. * - 'storage types': Array of hook_field_storage_info() results, keyed by * storage type names. Each element has the following components: label, * description, and settings from hook_field_storage_info(), as well as @@ -79,25 +77,9 @@ function _field_info_collate_types() { } else { $info = array( - 'field types' => array(), 'storage types' => array(), ); - // Populate field types. - foreach (module_implements('field_info') as $module) { - $field_types = (array) module_invoke($module, 'field_info'); - foreach ($field_types as $name => $field_info) { - // Provide defaults. - $field_info += array( - 'settings' => array(), - 'instance_settings' => array(), - ); - $info['field types'][$name] = $field_info; - $info['field types'][$name]['module'] = $module; - } - } - drupal_alter('field_info', $info['field types']); - // Populate storage types. foreach (module_implements('field_storage_info') as $module) { $storage_types = (array) module_invoke($module, 'field_storage_info'); @@ -178,25 +160,21 @@ function field_info_field_map() { } /** - * Returns information about field types from hook_field_info(). + * Returns information about field types. * * @param $field_type * (optional) A field type name. If omitted, all field types will be returned. * * @return - * Either a field type description, as provided by hook_field_info(), or an - * array of all existing field types, keyed by field type name. + * Either a field type definition, or an array of all existing field types, + * keyed by field type name. */ function field_info_field_types($field_type = NULL) { - $info = _field_info_collate_types(); - $field_types = $info['field types']; if ($field_type) { - if (isset($field_types[$field_type])) { - return $field_types[$field_type]; - } + return Drupal::service('plugin.manager.field.field_type')->getDefinition($field_type); } else { - return $field_types; + return Drupal::service('plugin.manager.field.field_type')->getDefinitions(); } } @@ -488,8 +466,8 @@ function field_info_extra_fields($entity_type, $bundle, $context) { * A field type name. * * @return - * The field type's default settings, as provided by hook_field_info(), or an - * empty array if type or settings are not defined. + * The field type's default settings, or an empty array if type or settings + * are not defined. */ function field_info_field_settings($type) { $info = field_info_field_types($type); @@ -503,8 +481,8 @@ function field_info_field_settings($type) { * A field type name. * * @return - * The field type's default instance settings, as provided by - * hook_field_info(), or an empty array if type or settings are not defined. + * The field type's default instance settings, or an empty array if type or + * settings are not defined. */ function field_info_instance_settings($type) { $info = field_info_field_types($type); diff --git a/core/modules/field/field.module b/core/modules/field/field.module index 299ada6..9c7ee3a 100644 --- a/core/modules/field/field.module +++ b/core/modules/field/field.module @@ -8,6 +8,8 @@ use Drupal\Core\Language\Language; use Drupal\Core\Template\Attribute; use Drupal\field\FieldInterface; +use Drupal\field\FieldInstanceInterface; +use Drupal\Core\Entity\EntityNG; /* * Load all public Field API functions. Drupal currently has no @@ -15,7 +17,6 @@ * every page request. */ require_once __DIR__ . '/field.crud.inc'; -require_once __DIR__ . '/field.default.inc'; require_once __DIR__ . '/field.info.inc'; require_once __DIR__ . '/field.multilingual.inc'; require_once __DIR__ . '/field.attach.inc'; @@ -230,21 +231,14 @@ function field_system_info_alter(&$info, $file, $type) { } /** - * Implements hook_data_type_info() to register data types for all field types. + * Implements hook_data_type_info(). */ function field_data_type_info() { - $field_types = field_info_field_types(); - $items = array(); - - // Expose data types for all the field type items. - foreach ($field_types as $type_name => $type_info) { - $data_type = isset($type_info['data_type']) ? $type_info['data_type'] : $type_name . '_field'; - $items[$data_type] = 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', - ); - } + // Expose each "configurable field" type as a data type. We add one single + // entry, which will be expanded through plugin derivatives. + $items['configurable_field_type'] = array( + 'derivative' => '\Drupal\field\Plugin\DataType\ConfigFieldDataTypeDerivative', + ); return $items; } @@ -293,21 +287,12 @@ function field_populate_default_values(EntityInterface $entity, $langcode = NULL */ 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); - - // @todo: Allow for adding field type settings. - $definition = array( - 'label' => t('Field !name', array('!name' => $field_name)), - 'type' => isset($field_types[$field['type']]['data_type']) ? $field_types[$field['type']]['data_type'] : $field['type'] . '_field', - 'configurable' => TRUE, - 'translatable' => !empty($field['translatable']) - ); + $definition = _field_generate_entity_field_definition($instance->getField()); if ($optional) { $property_info['optional'][$field_name] = $definition; @@ -323,6 +308,33 @@ function field_entity_field_info($entity_type) { } /** + * Generates an entity field definition for a configurable field. + * + * @param \Drupal\field\FieldInterface $field + * The field definition. + * @param \Drupal\field\FieldInstanceInterface $instance + * (Optional) The field instance definition. + * + * @return array + * The entity field definition. + */ +function _field_generate_entity_field_definition(FieldInterface $field, FieldInstanceInterface $instance = NULL) { + // @todo: Allow for adding field type settings. + $definition = array( + 'label' => t('Field !name', array('!name' => $field->id())), + 'type' => 'configurable_field_type:' . $field->type, + 'list' => TRUE, + 'configurable' => TRUE, + 'translatable' => !empty($field->translatable), + ); + if ($instance) { + $definition['instance'] = $instance; + } + + return $definition; +} + +/** * Implements hook_field_widget_info_alter(). */ function field_field_widget_info_alter(&$info) { @@ -500,64 +512,6 @@ function field_get_default_value(EntityInterface $entity, $field, $instance, $la } /** - * Filters out empty field values. - * - * @param $field_type - * The field type. - * @param $items - * The field values to filter. - * - * @return - * The array of items without empty field values. The function also renumbers - * the array keys to ensure sequential deltas. - */ -function _field_filter_items($field_type, $items) { - $field_type_info = field_info_field_types($field_type); - $function = $field_type_info['module'] . '_field_is_empty'; - foreach ((array) $items as $delta => $item) { - // Explicitly break if the function is undefined. - if ($function($item, $field_type)) { - unset($items[$delta]); - } - } - return array_values($items); -} - -/** - * Sorts items in a field according to user drag-and-drop reordering. - * - * @param $field - * The field definition. - * @param $items - * The field values to sort. - * - * @return - * The sorted array of field items. - */ -function _field_sort_items($field, $items) { - if (($field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED) && isset($items[0]['_weight'])) { - usort($items, '_field_sort_items_helper'); - foreach ($items as $delta => $item) { - if (is_array($items[$delta])) { - unset($items[$delta]['_weight']); - } - } - } - return $items; -} - -/** - * Callback for usort() within _field_sort_items(). - * - * Copied form element_sort(), which acts on #weight keys. - */ -function _field_sort_items_helper($a, $b) { - $a_weight = (is_array($a) ? $a['_weight'] : 0); - $b_weight = (is_array($b) ? $b['_weight'] : 0); - return $a_weight - $b_weight; -} - -/** * Callback for usort() within theme_field_multiple_value_form(). * * Sorts using ['_weight']['#value'] @@ -769,11 +723,10 @@ function field_view_value(EntityInterface $entity, $field_name, $item, $display * implementation supports the values 'inline', 'above' and 'hidden'. * Defaults to 'above'. * - type: (string) The formatter to use. Defaults to the - * 'default_formatter' for the field type, specified in hook_field_info(). - * The default formatter will also be used if the requested formatter is - * not available. + * 'default_formatter' for the field type. The default formatter will also + * be used if the requested formatter is not available. * - settings: (array) Settings specific to the formatter. Defaults to the - * formatter's default settings, specified in hook_field_formatter_info(). + * formatter's default settings. * - weight: (float) The weight to assign to the renderable element. * Defaults to 0. * @param $langcode @@ -790,9 +743,10 @@ function field_view_value(EntityInterface $entity, $field_name, $item, $display function field_view_field(EntityInterface $entity, $field_name, $display_options = array(), $langcode = NULL) { $output = array(); $bundle = $entity->bundle(); + $entity_type = $entity->entityType(); // Return nothing if the field doesn't exist. - $instance = field_info_instance($entity->entityType(), $field_name, $bundle); + $instance = field_info_instance($entity_type, $field_name, $bundle); if (!$instance) { return $output; } @@ -819,33 +773,39 @@ function field_view_field(EntityInterface $entity, $field_name, $display_options if ($formatter) { $display_langcode = field_language($entity, $field_name, $langcode); - $items = array(); - // Ensure the BC entity is used. - $entity = $entity->getBCEntity(); - if (isset($entity->{$field_name}[$display_langcode])) { - $items = $entity->{$field_name}[$display_langcode]; + + // Get the items. + if ($entity->getNGEntity() instanceof EntityNG) { + $items = $entity->getTranslation($display_langcode)->get($field_name); + $definition = $entity->getPropertyDefinition($field_name); + } + else { + $controller = \Drupal::entityManager()->getStorageController($entity_type); + $definitions = $controller->getFieldDefinitions(array( + 'EntityType' => $entity_type, + 'Bundle' => $bundle, + )); + $definition = $definitions[$field_name]; + $itemsBC = isset($entity->{$field_name}[$display_langcode]) ? $entity->{$field_name}[$display_langcode] : array(); + $items = \Drupal::typedData()->create($definitions[$field_name], $itemsBC, $field_name, $entity); } // Invoke prepare_view steps if needed. - if (empty($entity->_field_view_prepared)) { - $id = $entity->id(); - - // First let the field types do their preparation. - $options = array('field_name' => $field_name, 'langcode' => $display_langcode); - $null = NULL; - _field_invoke_multiple('prepare_view', $entity->entityType(), array($id => $entity), $null, $null, $options); - - // Then let the formatter do its own specific massaging. - $items_multi = array($id => array()); - if (isset($entity->{$field_name}[$display_langcode])) { - $items_multi[$id] = $entity->{$field_name}[$display_langcode]; - } - $formatter->prepareView(array($id => $entity), $display_langcode, $items_multi); - $items = $items_multi[$id]; - } + $id = $entity->id(); + + // First let the field type do its preparation. prepareView() is a static + // method. + $type_definition = \Drupal::typedData()->getDefinition($definition['type']); + $class = $type_definition['class']; + $class::prepareView(array($id => $items), $definition); + + // Then let the formatter do its own specific massaging. + $itemsBC_multi = array($id => $items->getValue()); + $formatter->prepareView(array($id => $entity), $display_langcode, $itemsBC_multi); + $itemsBC = $itemsBC_multi[$id]; // Build the renderable array. - $result = $formatter->view($entity, $display_langcode, $items); + $result = $formatter->view($entity, $display_langcode, $itemsBC); // Invoke hook_field_attach_view_alter() to let other modules alter the // renderable array, as in a full field_attach_view() execution. diff --git a/core/modules/field/field.multilingual.inc b/core/modules/field/field.multilingual.inc index 6f6e2d6..c04c218 100644 --- a/core/modules/field/field.multilingual.inc +++ b/core/modules/field/field.multilingual.inc @@ -34,10 +34,9 @@ * property returned by field_info_field() and whether the entity type the field * is attached to supports translation. * - * By default, _field_invoke() and _field_invoke_multiple() process a field in - * all available languages, unless they are given a language code suggestion. - * Based on that suggestion, _field_language_suggestion() determines the - * languages to act on. + * By default, field_invoke_method() processes a field in all available + * languages, unless they are given a language code suggestion. Based on that + * suggestion, _field_language_suggestion() determines the languages to act on. * * Most field_attach_*() functions act on all available language codes, except * for the following: diff --git a/core/modules/field/field.services.yml b/core/modules/field/field.services.yml index f4c28db..ebf9019 100644 --- a/core/modules/field/field.services.yml +++ b/core/modules/field/field.services.yml @@ -1,4 +1,7 @@ services: + plugin.manager.field.field_type: + class: Drupal\field\Plugin\Type\FieldType\ConfigFieldTypePluginManager + arguments: ['@container.namespaces', '@cache.field', '@language_manager', '@module_handler'] plugin.manager.field.widget: class: Drupal\field\Plugin\Type\Widget\WidgetPluginManager arguments: ['@container.namespaces'] diff --git a/core/modules/field/lib/Drupal/field/Annotation/ConfigFieldType.php b/core/modules/field/lib/Drupal/field/Annotation/ConfigFieldType.php new file mode 100644 index 0000000..7a1568f --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Annotation/ConfigFieldType.php @@ -0,0 +1,112 @@ +errors = $errors; - parent::__construct(t('Field validation errors')); - } -} diff --git a/core/modules/field/lib/Drupal/field/Plugin/Core/Entity/Field.php b/core/modules/field/lib/Drupal/field/Plugin/Core/Entity/Field.php index a8eb4b2..8bcb64c 100644 --- a/core/modules/field/lib/Drupal/field/Plugin/Core/Entity/Field.php +++ b/core/modules/field/lib/Drupal/field/Plugin/Core/Entity/Field.php @@ -68,8 +68,6 @@ class Field extends ConfigEntityBase implements FieldInterface { /** * The field type. * - * Field types are defined by modules that implement hook_field_info(). - * * Example: text, number_integer. * * @var string @@ -94,7 +92,7 @@ class Field extends ConfigEntityBase implements FieldInterface { * Field-type specific settings. * * An array of key/value pairs, The keys and default values are defined by the - * field type in the 'settings' entry of hook_field_info(). + * field type. * * @var array */ @@ -196,6 +194,13 @@ class Field extends ConfigEntityBase implements FieldInterface { public $deleted = FALSE; /** + * The field type handler. + * + * @var \Drupal\field\Plugin\Type\FieldType\ConfigFieldItemInterface + */ + protected $handler; + + /** * The field schema. * * @var array @@ -494,15 +499,12 @@ public function delete() { */ public function getSchema() { if (!isset($this->schema)) { - $module_handler = \Drupal::moduleHandler(); - - // Collect the schema from the field type. - // @todo Use $module_handler->loadInclude() once - // http://drupal.org/node/1941000 is fixed. - module_load_install($this->module); - // Invoke hook_field_schema() for the field. - $schema = (array) $module_handler->invoke($this->module, 'field_schema', array($this)); - $schema += array('columns' => array(), 'indexes' => array(), 'foreign keys' => array()); + // Get the schema from the field item class. + $definition = \Drupal::service('plugin.manager.field.field_type')->getDefinition($this->type); + $class = $definition['class']; + $schema = $class::schema($this); + // Fill in default values for optional entries. + $schema += array('indexes' => array(), 'foreign keys' => array()); // Check that the schema does not include forbidden column names. if (array_intersect(array_keys($schema['columns']), static::getReservedColumns())) { @@ -522,6 +524,20 @@ public function getSchema() { /** * {@inheritdoc} */ + public function getColumns() { + $schema = $this->getSchema(); + // A typical use case for the method is to iterate on the columns, while + // some other use cases rely on identifying the first column with the key() + // function. Since the schema is persisted in the Field object, we take care + // of resetting the array pointer so that the former does not interfere with + // the latter. + reset($schema['columns']); + return $schema['columns']; + } + + /** + * {@inheritdoc} + */ public function getStorageDetails() { if (!isset($this->storageDetails)) { $module_handler = \Drupal::moduleHandler(); diff --git a/core/modules/field/lib/Drupal/field/Plugin/Core/Entity/FieldInstance.php b/core/modules/field/lib/Drupal/field/Plugin/Core/Entity/FieldInstance.php index 2856acf..449cb68 100644 --- a/core/modules/field/lib/Drupal/field/Plugin/Core/Entity/FieldInstance.php +++ b/core/modules/field/lib/Drupal/field/Plugin/Core/Entity/FieldInstance.php @@ -102,7 +102,7 @@ class FieldInstance extends ConfigEntityBase implements FieldInstanceInterface { * Field-type specific settings. * * An array of key/value pairs. The keys and default values are defined by the - * field type in the 'instance_settings' entry of hook_field_info(). + * field type. * * @var array */ diff --git a/core/modules/field/lib/Drupal/field/Plugin/DataType/ConfigFieldDataTypeDerivative.php b/core/modules/field/lib/Drupal/field/Plugin/DataType/ConfigFieldDataTypeDerivative.php new file mode 100644 index 0000000..9209e9b --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Plugin/DataType/ConfigFieldDataTypeDerivative.php @@ -0,0 +1,51 @@ +derivatives)) { + $this->getDerivativeDefinitions($base_plugin_definition); + } + if (isset($this->derivatives[$derivative_id])) { + return $this->derivatives[$derivative_id]; + } + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions(array $base_plugin_definition) { + foreach (\Drupal::service('plugin.manager.field.field_type')->getDefinitions() as $plugin_id => $definition) { + // Typed data API expects a 'list class' property, but annotations do not + // support spaces in property names. + $definition['list class'] = $definition['list_class']; + unset($definition['list_class']); + + $this->derivatives[$plugin_id] = $definition; + } + return $this->derivatives; + } + +} diff --git a/core/modules/field/lib/Drupal/field/Plugin/Type/FieldType/ConfigField.php b/core/modules/field/lib/Drupal/field/Plugin/Type/FieldType/ConfigField.php new file mode 100644 index 0000000..f425846 --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Plugin/Type/FieldType/ConfigField.php @@ -0,0 +1,70 @@ +instance = $definition['instance']; + } + } + + /** + * {@inheritdoc} + */ + public function getInstance() { + if (!isset($this->instance) && $parent = $this->getParent()) { + $instances = FieldAPI::fieldInfo()->getBundleInstances($parent->entityType(), $parent->bundle()); + $this->instance = $instances[$this->getName()]; + } + return $this->instance; + } + + /** + * {@inheritdoc} + */ + public function getConstraints() { + $constraints = array(); + // Check that the number of values doesn't exceed the field cardinality. For + // form submitted values, this can only happen with 'multiple value' + // widgets. + $cardinality = $this->getInstance()->getField()->cardinality; + if ($cardinality != FIELD_CARDINALITY_UNLIMITED) { + $constraints[] = \Drupal::typedData() + ->getValidationConstraintManager() + ->create('Count', array( + 'max' => $cardinality, + 'maxMessage' => t('%name: this field cannot hold more than @count values.', array('%name' => $this->getInstance()->label, '@count' => $cardinality)), + )); + } + + return $constraints; + } + +} diff --git a/core/modules/field/lib/Drupal/field/Plugin/Type/FieldType/ConfigFieldInterface.php b/core/modules/field/lib/Drupal/field/Plugin/Type/FieldType/ConfigFieldInterface.php new file mode 100644 index 0000000..cd91c47 --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Plugin/Type/FieldType/ConfigFieldInterface.php @@ -0,0 +1,24 @@ +instance) && $parent = $this->getParent()) { + $this->instance = $parent->getInstance(); + } + return $this->instance; + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, array &$form_state) { + return array(); + } + + /** + * {@inheritdoc} + */ + public function instanceSettingsForm(array $form, array &$form_state) { + return array(); + } + +} diff --git a/core/modules/field/lib/Drupal/field/Plugin/Type/FieldType/ConfigFieldItemInterface.php b/core/modules/field/lib/Drupal/field/Plugin/Type/FieldType/ConfigFieldItemInterface.php new file mode 100644 index 0000000..bf2024b --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Plugin/Type/FieldType/ConfigFieldItemInterface.php @@ -0,0 +1,108 @@ + array(), + 'instance_settings' => array(), + 'list_class' => '\Drupal\field\Plugin\Type\FieldType\ConfigField', + ); + + /** + * Constructs the ConfigFieldTypePluginManager object + * + * @param \Traversable $namespaces + * An object that implements \Traversable which contains the root paths + * keyed by the corresponding namespace to look for plugin implementations. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * Cache backend instance to use. + * @param \Drupal\Core\Language\LanguageManager $language_manager + * The language manager. + * @param \Drupal\Core\Extension\ModuleHandlerInterface + * The module handler. + */ + public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, LanguageManager $language_manager, ModuleHandlerInterface $module_handler) { + $annotation_namespaces = array('Drupal\field\Annotation' => $namespaces['Drupal\field']); + parent::__construct('field/field_type', $namespaces, $annotation_namespaces, 'Drupal\field\Annotation\ConfigFieldType'); + $this->alterInfo($module_handler, 'field_info'); + $this->setCacheBackend($cache_backend, $language_manager, 'field_types'); + + // @todo Remove once all core field types have been converted (see + // http://drupal.org/node/2014671). + $this->discovery = new LegacyFieldTypeDiscoveryDecorator($this->discovery, $module_handler); + } + +} diff --git a/core/modules/field/lib/Drupal/field/Plugin/Type/FieldType/LegacyFieldTypeDiscoveryDecorator.php b/core/modules/field/lib/Drupal/field/Plugin/Type/FieldType/LegacyFieldTypeDiscoveryDecorator.php new file mode 100644 index 0000000..9aa6ab7 --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Plugin/Type/FieldType/LegacyFieldTypeDiscoveryDecorator.php @@ -0,0 +1,76 @@ +decorated = $decorated; + $this->moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} + */ + public function getDefinition($plugin_id) { + $definitions = $this->getDefinitions(); + return isset($definitions[$plugin_id]) ? $definitions[$plugin_id] : NULL; + } + + /** + * {@inheritdoc} + */ + public function getDefinitions() { + $definitions = $this->decorated->getDefinitions(); + + // We cannot use HookDiscovery, since it uses module_implements(), which + // throws exceptions during upgrades. + foreach (array_keys($this->moduleHandler->getModuleList()) as $module) { + $function = $module . '_field_info'; + if (function_exists($function)) { + foreach ($function() as $plugin_id => $definition) { + $definition['id'] = $plugin_id; + $definition['module'] = $module; + $definition['list_class'] = '\Drupal\field\Plugin\field\field_type\LegacyConfigField'; + $definitions[$plugin_id] = $definition; + } + } + } + + return $definitions; + } + +} diff --git a/core/modules/field/lib/Drupal/field/Plugin/Type/FieldType/PrepareCacheInterface.php b/core/modules/field/lib/Drupal/field/Plugin/Type/FieldType/PrepareCacheInterface.php new file mode 100644 index 0000000..6aadec1 --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Plugin/Type/FieldType/PrepareCacheInterface.php @@ -0,0 +1,30 @@ + count($items), 'array_parents' => array(), - 'errors' => array(), + 'constraint_violations' => array(), ); field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state); } @@ -315,7 +316,18 @@ public function extractFormValues(EntityInterface $entity, $langcode, array &$it $this->sortItems($items); // Remove empty values. - $items = _field_filter_items($this->fieldDefinition->getFieldType(), $items); + if ($entity instanceof \Drupal\Core\Entity\EntityNG) { + $itemsNG = \Drupal::typedData()->getPropertyInstance($entity, $field_name, $items); + } + else { + $definitions = \Drupal::entityManager()->getStorageController($entity->entityType())->getFieldDefinitions(array( + 'EntityType' => $entity->entityType(), + 'Bundle' => $entity->bundle(), + )); + $itemsNG = \Drupal::typedData()->create($definitions[$field_name], $items, $field_name, $entity); + } + $itemsNG->filterEmptyValues(); + $items = $itemsNG->getValue(TRUE); // Put delta mapping in $form_state, so that flagErrors() can use it. $field_state = field_form_get_state($form['#parents'], $field_name, $langcode, $form_state); @@ -335,7 +347,7 @@ public function flagErrors(EntityInterface $entity, $langcode, array $items, arr $field_state = field_form_get_state($form['#parents'], $field_name, $langcode, $form_state); - if (!empty($field_state['errors'])) { + if (!empty($field_state['constraint_violations'])) { // Locate the correct element in the the form. $element = NestedArray::getValue($form_state['complete_form'], $field_state['array_parents']); @@ -344,7 +356,16 @@ public function flagErrors(EntityInterface $entity, $langcode, array $items, arr $definition = $this->getPluginDefinition(); $is_multiple = $definition['multiple_values']; - foreach ($field_state['errors'] as $delta => $delta_errors) { + $violations_by_delta = array(); + foreach ($field_state['constraint_violations'] as $violation) { + // Separate violations by delta. + $property_path = explode('.', $violation->getPropertyPath()); + $delta = array_shift($property_path); + $violation->arrayPropertyPath = $property_path; + $violations_by_delta[$delta][] = $violation; + } + + foreach ($violations_by_delta as $delta => $delta_violations) { // For a multiple-value widget, pass all errors to the main widget. // For single-value widgets, pass errors by delta. if ($is_multiple) { @@ -354,13 +375,14 @@ public function flagErrors(EntityInterface $entity, $langcode, array $items, arr $original_delta = $field_state['original_deltas'][$delta]; $delta_element = $element[$original_delta]; } - foreach ($delta_errors as $error) { - $error_element = $this->errorElement($delta_element, $error, $form, $form_state); - form_error($error_element, $error['message']); + foreach ($delta_violations as $violation) { + // @todo: Pass $violation->arrayPropertyPath as property path. + $error_element = $this->errorElement($delta_element, $violation, $form, $form_state); + form_error($error_element, $violation->getMessage()); } } // Reinitialize the errors list for the next submit. - $field_state['errors'] = array(); + $field_state['constraint_violations'] = array(); field_form_set_state($form['#parents'], $field_name, $langcode, $form_state, $field_state); } } @@ -376,7 +398,7 @@ public function settingsForm(array $form, array &$form_state) { /** * Implements Drupal\field\Plugin\Type\Widget\WidgetInterface::errorElement(). */ - public function errorElement(array $element, array $error, array $form, array &$form_state) { + public function errorElement(array $element, ConstraintViolationInterface $error, array $form, array &$form_state) { return $element; } diff --git a/core/modules/field/lib/Drupal/field/Plugin/Type/Widget/WidgetInterface.php b/core/modules/field/lib/Drupal/field/Plugin/Type/Widget/WidgetInterface.php index e00005b..d4a7b8c 100644 --- a/core/modules/field/lib/Drupal/field/Plugin/Type/Widget/WidgetInterface.php +++ b/core/modules/field/lib/Drupal/field/Plugin/Type/Widget/WidgetInterface.php @@ -9,6 +9,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\field\Plugin\Core\Entity\FieldInstance; +use Symfony\Component\Validator\ConstraintViolationInterface; /** * Interface definition for field widget plugins. @@ -112,12 +113,8 @@ public function formElement(array $items, $delta, array $element, $langcode, arr * @param array $element * An array containing the form element for the widget, as generated by * formElement(). - * @param array $error - * An associative array with the following key-value pairs, as returned by - * hook_field_validate(): - * - error: the error code. Complex widgets might need to report different - * errors to different form elements inside the widget. - * - message: the human readable message to be displayed. + * @param \Symfony\Component\Validator\ConstraintViolationInterface $violations + * The list of constraint violations reported during the validation phase. * @param array $form * The form structure where field elements are attached to. This might be a * full form structure, or a sub-element of a larger form. @@ -127,7 +124,7 @@ public function formElement(array $items, $delta, array $element, $langcode, arr * @return array * The element on which the error should be flagged. */ - public function errorElement(array $element, array $error, array $form, array &$form_state); + public function errorElement(array $element, ConstraintViolationInterface $violations, array $form, array &$form_state); /** * Massages the form values into the format expected for field values. diff --git a/core/modules/field/lib/Drupal/field/Plugin/Type/Widget/WidgetPluginManager.php b/core/modules/field/lib/Drupal/field/Plugin/Type/Widget/WidgetPluginManager.php index eb9cc87..6f22fa8 100644 --- a/core/modules/field/lib/Drupal/field/Plugin/Type/Widget/WidgetPluginManager.php +++ b/core/modules/field/lib/Drupal/field/Plugin/Type/Widget/WidgetPluginManager.php @@ -64,9 +64,8 @@ public function __construct(\Traversable $namespaces) { * following key value pairs are allowed, and are all optional if * 'prepare' is TRUE: * - type: (string) The widget to use. Defaults to the - * 'default_widget' for the field type, specified in - * hook_field_info(). The default widget will also be used if the - * requested widget is not available. + * 'default_widget' for the field type. The default widget will also be + * used if the requested widget is not available. * - settings: (array) Settings specific to the widget. Each setting * defaults to the default value specified in the widget definition. * diff --git a/core/modules/field/lib/Drupal/field/Plugin/field/field_type/LegacyConfigField.php b/core/modules/field/lib/Drupal/field/Plugin/field/field_type/LegacyConfigField.php new file mode 100644 index 0000000..ca37842 --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Plugin/field/field_type/LegacyConfigField.php @@ -0,0 +1,128 @@ +filterEmptyValues(); + + $legacy_errors = array(); + $this->legacyCallback('validate', array(&$legacy_errors)); + + $entity = $this->getParent(); + $langcode = $entity->language()->langcode; + + if (isset($legacy_errors[$this->getInstance()->getField()->id()][$langcode])) { + foreach ($legacy_errors[$this->getInstance()->getField()->id()][$langcode] as $delta => $item_errors) { + foreach ($item_errors as $item_error) { + // We do not have the information about which column triggered the + // error, so assume the first column... + $column = key($this->getInstance()->getField()->getColumns()); + $violations->add(new ConstraintViolation($item_error['message'], $item_error['message'], array(), $this, $delta . '.' . $column, $this->offsetGet($delta)->get($column)->getValue(), NULL, $item_error['error'])); + } + } + } + + return $violations; + } + + /** + * {@inheritdoc} + */ + public function preSave() { + // Filter out empty items. + $this->filterEmptyValues(); + + $this->legacyCallback('presave'); + } + + /** + * {@inheritdoc} + */ + public function insert() { + $this->legacyCallback('insert'); + } + + /** + * {@inheritdoc} + */ + public function update() { + $this->legacyCallback('update'); + } + + /** + * {@inheritdoc} + */ + public function delete() { + $this->legacyCallback('delete'); + } + + /** + * {@inheritdoc} + */ + public function deleteRevision() { + $this->legacyCallback('delete_revision'); + } + + /** + * Calls the legacy callback for a given field type "hook", if it exists. + * + * @param string $hook + * The name of the hook, e.g. 'presave', 'validate'. + */ + protected function legacyCallback($hook, $args = array()) { + $definition = $this->getPluginDefinition(); + $module = $definition['module']; + $callback = "{$module}_field_{$hook}"; + if (function_exists($callback)) { + $entity = $this->getParent(); + $langcode = $entity->language()->langcode; + + // We need to remove the empty "prototype" item here. + // @todo Revisit after http://drupal.org/node/1988492. + $this->filterEmptyValues(); + // Legcacy callbacks alter $items by reference. + $items = (array) $this->getValue(TRUE); + $args = array_merge(array( + $entity, + $this->getInstance()->getField(), + $this->getInstance(), + $langcode, + &$items + ), $args); + call_user_func_array($callback, $args); + $this->setValue($items); + } + } + +} diff --git a/core/modules/field/lib/Drupal/field/Plugin/field/field_type/LegacyConfigFieldItem.php b/core/modules/field/lib/Drupal/field/Plugin/field/field_type/LegacyConfigFieldItem.php new file mode 100644 index 0000000..89db4ae --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Plugin/field/field_type/LegacyConfigFieldItem.php @@ -0,0 +1,171 @@ +getDefinition($field->type); + $module = $definition['module']; + module_load_install($module); + $callback = "{$module}_field_schema"; + if (function_exists($callback)) { + return $callback($field); + } + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + $callback = $this->getLegacyCallback('is_empty'); + // Make sure the array received by the legacy callback includes computed + // properties. + $item = $this->getValue(TRUE); + // The previous hook was never called on an empty item, but EntityNG always + // creates a FieldItem element for an empty field. + return empty($item) || $callback($item, $this->getInstance()->getField()->type); + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, array &$form_state) { + if ($callback = $this->getLegacyCallback('settings_form')) { + // hook_field_settings_form() used to receive the $instance (not actually + // needed), and the value of field_has_data(). + return $callback($this->getInstance()->getField(), $this->getInstance(), $this->getInstance()->getField()->hasData()); + } + return array(); + } + + /** + * {@inheritdoc} + */ + public function instanceSettingsForm(array $form, array &$form_state) { + if ($callback = $this->getLegacyCallback('instance_settings_form')) { + return $callback($this->getInstance()->getField(), $this->getInstance(), $form_state); + } + return array(); + } + + /** + * Massages loaded field values before they enter the field cache. + * + * This impelments the prepareCache() method defined in PrepareCacheInterface + * even if the class does explicitly implements its, so as to preserve + * the optimizations of only creating Field and FieldItem objects and invoking + * the method if are actually needed. + * + * @see \Drupal\Core\Entity\DatabaseStorageController::invokeFieldItemPrepareCache() + */ + public function prepareCache() { + if ($callback = $this->getLegacyCallback('load')) { + $entity = $this->getParent()->getParent(); + $langcode = $entity->language()->langcode; + $entity_id = $entity->id(); + + // hook_field_attach_load() receives items keyed by entity id, and alter + // then by reference. + $items = array($entity_id => array(0 => $this->getValue(TRUE))); + $args = array( + $entity->entityType(), + array($entity_id => $entity), + $this->getInstance()->getField(), + array($entity_id => $this->getInstance()), + $langcode, + &$items, + FIELD_LOAD_CURRENT, + ); + call_user_func_array($callback, $args); + $this->setValue($items[$entity_id][0]); + } + } + + /** + * {@inherotdoc} + */ + public static function prepareView(array $entities_items) { + if ($entities_items) { + // Determine the legacy callback. + $field_type_definition = current($entities_items)->getPluginDefinition(); + $module = $field_type_definition['module']; + $callback = "{$module}_field_prepare_view"; + if (function_exists($callback)) { + $entities = array(); + $instances = array(); + $itemsBC = array(); + foreach ($entities_items as $id => $items) { + $entities[$id] = $items->getParent(); + $instances[$id] = $items->offsetGet(0)->getInstance(); + // We need to remove the empty "prototype" item here. + // @todo Revisit after http://drupal.org/node/1988492. + $items->filterEmptyValues(); + $itemsBC[$id] = $items->getValue(TRUE); + } + + // Determine the entity type, langcode and field. + $entity_type = current($entities)->entityType(); + $langcode = current($entities)->language()->langcode; + $field = current($instances)->getField(); + + $args = array( + $entity_type, + $entities, + $field, + $instances, + $langcode, + &$itemsBC, + ); + call_user_func_array($callback, $args); + + foreach ($entities_items as $id => $items) { + $items->setValue($itemsBC[$id]); + } + } + } + } + + /** + * Returns the legacy callback for a given field type "hook". + * + * @param string $hook + * The name of the hook, e.g. 'settings_form', 'is_empty'. + * + * @return string|null + * The name of the legacy callback, or NULL if it does not exist. + */ + protected function getLegacyCallback($hook) { + $definition = $this->getPluginDefinition(); + $module = $definition['module']; + $callback = "{$module}_field_{$hook}"; + if (function_exists($callback)) { + return $callback; + } + } + +} diff --git a/core/modules/field/lib/Drupal/field/Tests/BulkDeleteTest.php b/core/modules/field/lib/Drupal/field/Tests/BulkDeleteTest.php index d27bd7d..c0f7695 100644 --- a/core/modules/field/lib/Drupal/field/Tests/BulkDeleteTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/BulkDeleteTest.php @@ -167,7 +167,8 @@ function testDeleteFieldInstance() { // The instance still exists, deleted. $instances = field_read_instances(array('field_id' => $field['uuid'], 'deleted' => TRUE), array('include_deleted' => TRUE, 'include_inactive' => TRUE)); $this->assertEqual(count($instances), 1, 'There is one deleted instance'); - $this->assertEqual($instances[0]['bundle'], $bundle, 'The deleted instance is for the correct bundle'); + $instance = $instances[0]; + $this->assertEqual($instance['bundle'], $bundle, 'The deleted instance is for the correct bundle'); // There are 0 entities of this bundle with non-deleted data. $found = $factory->get('test_entity') @@ -192,7 +193,7 @@ function testDeleteFieldInstance() { $ids->entity_id = $entity_id; $entities[$entity_id] = _field_create_entity_from_ids($ids); } - field_attach_load($this->entity_type, $entities, FIELD_LOAD_CURRENT, array('field_id' => $field['uuid'], 'deleted' => TRUE)); + field_attach_load($this->entity_type, $entities, FIELD_LOAD_CURRENT, array('instance' => $instance)); $this->assertEqual(count($found), 10, 'Correct number of entities found after deleting'); foreach ($entities as $id => $entity) { $this->assertEqual($this->entities[$id]->{$field['field_name']}, $entity->{$field['field_name']}, "Entity $id with deleted data loaded correctly"); @@ -232,17 +233,13 @@ function testPurgeInstance() { } // Check hooks invocations. - // - hook_field_load() (multiple hook) should have been called on all - // entities by pairs of two. - // - hook_field_delete() should have been called once for each entity in the - // bundle. + // hook_field_load() and hook_field_delete() should have been called once + // for each entity in the bundle. $actual_hooks = field_test_memorize(); $hooks = array(); $entities = $this->convertToPartialEntities($this->entities_by_bundles[$bundle], $field['field_name']); - foreach (array_chunk($entities, $batch_size, TRUE) as $chunk_entity) { - $hooks['field_test_field_load'][] = $chunk_entity; - } - foreach ($entities as $entity) { + foreach ($entities as $id => $entity) { + $hooks['field_test_field_load'][] = array($id => $entity); $hooks['field_test_field_delete'][] = $entity; } $this->checkHooksInvocations($hooks, $actual_hooks); @@ -286,15 +283,15 @@ function testPurgeField() { field_purge_batch(10); // Check hooks invocations. - // - hook_field_load() (multiple hook) should have been called once, for all - // entities in the bundle. - // - hook_field_delete() should have been called once for each entity in the - // bundle. + // hook_field_load() and hook_field_delete() should have been called once + // for each entity in the bundle. $actual_hooks = field_test_memorize(); $hooks = array(); $entities = $this->convertToPartialEntities($this->entities_by_bundles[$bundle], $field['field_name']); - $hooks['field_test_field_load'][] = $entities; - $hooks['field_test_field_delete'] = $entities; + foreach ($entities as $id => $entity) { + $hooks['field_test_field_load'][] = array($id => $entity); + $hooks['field_test_field_delete'][] = $entity; + } $this->checkHooksInvocations($hooks, $actual_hooks); // Purge again to purge the instance. @@ -320,8 +317,10 @@ function testPurgeField() { $actual_hooks = field_test_memorize(); $hooks = array(); $entities = $this->convertToPartialEntities($this->entities_by_bundles[$bundle], $field['field_name']); - $hooks['field_test_field_load'][] = $entities; - $hooks['field_test_field_delete'] = $entities; + foreach ($entities as $id => $entity) { + $hooks['field_test_field_load'][] = array($id => $entity); + $hooks['field_test_field_delete'][] = $entity; + } $this->checkHooksInvocations($hooks, $actual_hooks); // The field still exists, deleted. diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldAttachOtherTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldAttachOtherTest.php index d16b25a..4e78e82 100644 --- a/core/modules/field/lib/Drupal/field/Tests/FieldAttachOtherTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/FieldAttachOtherTest.php @@ -8,7 +8,6 @@ namespace Drupal\field\Tests; use Drupal\Core\Language\Language; -use Drupal\field\FieldValidationException; /** * Unit test class for non-storage related field_attach_* functions. @@ -84,21 +83,6 @@ function testFieldAttachView() { $this->content = $output; $this->assertRaw("$formatter_setting_2|{$value['value']}", "Value $delta is displayed, formatter settings are applied."); } - // View single field (the second field). - field_attach_prepare_view($entity_type, array($entity->ftid => $entity), $displays, $langcode, $options); - $entity->content = field_attach_view($entity, $display, $langcode, $options); - $output = drupal_render($entity->content); - $this->content = $output; - $this->assertNoRaw($this->instance['label'], "First field's label is not displayed."); - foreach ($values as $delta => $value) { - $this->content = $output; - $this->assertNoRaw("$formatter_setting|{$value['value']}", "Value $delta is displayed, formatter settings are applied."); - } - $this->assertRaw($this->instance_2['label'], "Second field's label is displayed."); - foreach ($values_2 as $delta => $value) { - $this->content = $output; - $this->assertRaw("$formatter_setting_2|{$value['value']}", "Value $delta is displayed, formatter settings are applied."); - } // Label hidden. $entity = clone($entity_init); @@ -283,9 +267,10 @@ function testFieldAttachCache() { // Load a single field, and check that no cache entry is present. $entity = clone($entity_init); - field_attach_load($entity_type, array($entity->ftid => $entity), FIELD_LOAD_CURRENT, array('field_id' => $this->field_id)); + $instance = field_info_instance($entity->entityType(), $this->field_name, $entity->bundle()); + field_attach_load($entity_type, array($entity->ftid => $entity), FIELD_LOAD_CURRENT, array('instance' => $instance)); $cache = cache('field')->get($cid); - $this->assertFalse(cache('field')->get($cid), 'Cached: no cache entry on loading a single field'); + $this->assertFalse($cache, 'Cached: no cache entry on loading a single field'); // Load, and check that a cache entry is present with the expected values. $entity = clone($entity_init); @@ -331,98 +316,6 @@ function testFieldAttachCache() { } /** - * Test field_attach_validate(). - * - * Verify that field_attach_validate() invokes the correct - * hook_field_validate. - */ - function testFieldAttachValidate() { - $this->createFieldWithInstance('_2'); - - $entity_type = 'test_entity'; - $entity = field_test_create_entity(0, 0, $this->instance['bundle']); - $langcode = Language::LANGCODE_NOT_SPECIFIED; - - // Set up all but one values of the first field to generate errors. - $values = array(); - for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { - $values[$delta]['value'] = -1; - } - // Arrange for item 1 not to generate an error. - $values[1]['value'] = 1; - $entity->{$this->field_name}[$langcode] = $values; - - // Set up all values of the second field to generate errors. - $values_2 = array(); - for ($delta = 0; $delta < $this->field_2['cardinality']; $delta++) { - $values_2[$delta]['value'] = -1; - } - $entity->{$this->field_name_2}[$langcode] = $values_2; - - // Validate all fields. - try { - field_attach_validate($entity); - } - catch (FieldValidationException $e) { - $errors = $e->errors; - } - - foreach ($values as $delta => $value) { - if ($value['value'] != 1) { - $this->assertIdentical($errors[$this->field_name][$langcode][$delta][0]['error'], 'field_test_invalid', "Error set on first field's value $delta"); - $this->assertEqual(count($errors[$this->field_name][$langcode][$delta]), 1, "Only one error set on first field's value $delta"); - unset($errors[$this->field_name][$langcode][$delta]); - } - else { - $this->assertFalse(isset($errors[$this->field_name][$langcode][$delta]), "No error set on first field's value $delta"); - } - } - foreach ($values_2 as $delta => $value) { - $this->assertIdentical($errors[$this->field_name_2][$langcode][$delta][0]['error'], 'field_test_invalid', "Error set on second field's value $delta"); - $this->assertEqual(count($errors[$this->field_name_2][$langcode][$delta]), 1, "Only one error set on second field's value $delta"); - unset($errors[$this->field_name_2][$langcode][$delta]); - } - $this->assertEqual(count($errors[$this->field_name][$langcode]), 0, 'No extraneous errors set for first field'); - $this->assertEqual(count($errors[$this->field_name_2][$langcode]), 0, 'No extraneous errors set for second field'); - - // Validate a single field. - $options = array('field_name' => $this->field_name_2); - try { - field_attach_validate($entity, $options); - } - catch (FieldValidationException $e) { - $errors = $e->errors; - } - - foreach ($values_2 as $delta => $value) { - $this->assertIdentical($errors[$this->field_name_2][$langcode][$delta][0]['error'], 'field_test_invalid', "Error set on second field's value $delta"); - $this->assertEqual(count($errors[$this->field_name_2][$langcode][$delta]), 1, "Only one error set on second field's value $delta"); - unset($errors[$this->field_name_2][$langcode][$delta]); - } - $this->assertFalse(isset($errors[$this->field_name]), 'No validation errors are set for the first field, despite it having errors'); - $this->assertEqual(count($errors[$this->field_name_2][$langcode]), 0, 'No extraneous errors set for second field'); - - // Check that cardinality is validated. - $entity->{$this->field_name_2}[$langcode] = $this->_generateTestFieldValues($this->field_2['cardinality'] + 1); - // When validating all fields. - try { - field_attach_validate($entity); - } - catch (FieldValidationException $e) { - $errors = $e->errors; - } - $this->assertEqual($errors[$this->field_name_2][$langcode][0][0]['error'], 'field_cardinality', 'Cardinality validation failed.'); - // When validating a single field (the second field). - try { - field_attach_validate($entity, $options); - } - catch (FieldValidationException $e) { - $errors = $e->errors; - } - $this->assertEqual($errors[$this->field_name_2][$langcode][0][0]['error'], 'field_cardinality', 'Cardinality validation failed.'); - } - - /** * Test field_attach_form(). * * This could be much more thorough, but it does verify that the correct diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldAttachStorageTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldAttachStorageTest.php index 7192067..fb6a79f 100644 --- a/core/modules/field/lib/Drupal/field/Tests/FieldAttachStorageTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/FieldAttachStorageTest.php @@ -161,7 +161,8 @@ function testFieldAttachLoadMultiple() { // Check that the single-field load option works. $entity = field_test_create_entity(1, 1, $bundles[1]); - field_attach_load($entity_type, array(1 => $entity), FIELD_LOAD_CURRENT, array('field_id' => $field_ids[1])); + $instance = field_info_instance($entity->entityType(), $field_names[1], $entity->bundle()); + field_attach_load($entity_type, array(1 => $entity), FIELD_LOAD_CURRENT, array('instance' => $instance)); $this->assertEqual($entity->{$field_names[1]}[$langcode][0]['value'], $values[1][$field_names[1]], format_string('Entity %index: expected value was found.', array('%index' => 1))); $this->assertEqual($entity->{$field_names[1]}[$langcode][0]['additional_key'], 'additional_value', format_string('Entity %index: extra information was found', array('%index' => 1))); $this->assert(!isset($entity->{$field_names[2]}), format_string('Entity %index: field %field_name is not loaded.', array('%index' => 2, '%field_name' => $field_names[2]))); diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldUnitTestBase.php b/core/modules/field/lib/Drupal/field/Tests/FieldUnitTestBase.php index ed3c151..652e561 100644 --- a/core/modules/field/lib/Drupal/field/Tests/FieldUnitTestBase.php +++ b/core/modules/field/lib/Drupal/field/Tests/FieldUnitTestBase.php @@ -48,8 +48,14 @@ function setUp() { * @param string $suffix * (optional) A string that should only contain characters that are valid in * PHP variable names as well. + * @param string $entity_type + * (optional) The entity type on which the instance should be created. + * Defaults to 'test_entity'. + * @param string $bundle + * (optional) The entity type on which the instance should be created. + * Defaults to 'test_bundle'. */ - function createFieldWithInstance($suffix = '') { + function createFieldWithInstance($suffix = '', $entity_type = 'test_entity', $bundle = 'test_bundle') { $field_name = 'field_name' . $suffix; $field = 'field' . $suffix; $field_id = 'field_id' . $suffix; @@ -61,11 +67,10 @@ function createFieldWithInstance($suffix = '') { $this->$field_id = $this->{$field}['uuid']; $this->$instance = array( 'field_name' => $this->$field_name, - 'entity_type' => 'test_entity', - 'bundle' => 'test_bundle', + 'entity_type' => $entity_type, + 'bundle' => $bundle, 'label' => $this->randomName() . '_label', 'description' => $this->randomName() . '_description', - 'weight' => mt_rand(0, 127), 'settings' => array( 'test_instance_setting' => $this->randomName(), ), diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldValidationTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldValidationTest.php new file mode 100644 index 0000000..3dd2e32 --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Tests/FieldValidationTest.php @@ -0,0 +1,95 @@ + 'Field validation', + 'description' => 'Tests field validation.', + 'group' => 'Field API', + ); + } + + public function setUp() { + parent::setUp(); + + // Create a field and instance of type 'test_field', on the 'entity_test' + // entity type. + $this->entityType = 'entity_test'; + $this->bundle = 'entity_test'; + $this->createFieldWithInstance('', $this->entityType, $this->bundle); + + // Create an 'entity_test' entity. + $this->entity = entity_create($this->entityType, array( + 'type' => $this->bundle, + )); + } + + /** + * Tests that the number of values is validated against the field cardinality. + */ + function testCardinalityConstraint() { + $cardinality = $this->field->cardinality; + $entity = $this->entity; + + for ($delta = 0; $delta < $cardinality + 1; $delta++) { + $entity->{$this->field_name}->offsetGet($delta)->set('value', 1); + } + + // Validate the field. + $violations = $entity->{$this->field_name}->validate(); + + // Check that the expected constraint violations are reported. + $this->assertEqual(count($violations), 1); + $this->assertEqual($violations[0]->getPropertyPath(), ''); + $this->assertEqual($violations[0]->getMessage(), t('%name: this field cannot hold more than @count values.', array('%name' => $this->instance['label'], '@count' => $cardinality))); + } + + /** + * Tests that constraints defined by the field type are validated. + */ + function testFieldConstraints() { + $cardinality = $this->field->cardinality; + $entity = $this->entity; + + // The test is only valid if the field cardinality is greater than 2. + $this->assertTrue($cardinality >= 2); + + // Set up values for the field. + $expected_violations = array(); + for ($delta = 0; $delta < $cardinality; $delta++) { + // All deltas except '1' have incorrect values. + if ($delta == 1) { + $value = 1; + } + else { + $value = -1; + $expected_violations[$delta . '.value'][] = t('%name does not accept the value -1.', array('%name' => $this->instance['label'])); + } + $entity->{$this->field_name}->offsetGet($delta)->set('value', $value); + } + + // Validate the field. + $violations = $entity->{$this->field_name}->validate(); + + // Check that the expected constraint violations are reported. + $violations_by_path = array(); + foreach ($violations as $violation) { + $violations_by_path[$violation->getPropertyPath()][] = $violation->getMessage(); + } + $this->assertEqual($violations_by_path, $expected_violations); + } + +} diff --git a/core/modules/field/lib/Drupal/field/Tests/FormTest.php b/core/modules/field/lib/Drupal/field/Tests/FormTest.php index 6e20523..aba8c56 100644 --- a/core/modules/field/lib/Drupal/field/Tests/FormTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/FormTest.php @@ -497,121 +497,6 @@ function testFieldFormAccess() { } /** - * Tests Field API form integration within a subform. - */ - function testNestedFieldForm() { - // Add two instances on the 'test_bundle' - field_create_field($this->field_single); - field_create_field($this->field_unlimited); - $this->instance['field_name'] = 'field_single'; - $this->instance['label'] = 'Single field'; - field_create_instance($this->instance); - entity_get_form_display($this->instance['entity_type'], $this->instance['bundle'], 'default') - ->setComponent($this->instance['field_name']) - ->save(); - $this->instance['field_name'] = 'field_unlimited'; - $this->instance['label'] = 'Unlimited field'; - field_create_instance($this->instance); - entity_get_form_display($this->instance['entity_type'], $this->instance['bundle'], 'default') - ->setComponent($this->instance['field_name']) - ->save(); - - // Create two entities. - $entity_1 = field_test_create_entity(1, 1); - $entity_1->is_new = TRUE; - $entity_1->field_single[Language::LANGCODE_NOT_SPECIFIED][] = array('value' => 0); - $entity_1->field_unlimited[Language::LANGCODE_NOT_SPECIFIED][] = array('value' => 1); - field_test_entity_save($entity_1); - - $entity_2 = field_test_create_entity(2, 2); - $entity_2->is_new = TRUE; - $entity_2->field_single[Language::LANGCODE_NOT_SPECIFIED][] = array('value' => 10); - $entity_2->field_unlimited[Language::LANGCODE_NOT_SPECIFIED][] = array('value' => 11); - field_test_entity_save($entity_2); - - // Display the 'combined form'. - $this->drupalGet('test-entity/nested/1/2'); - $this->assertFieldByName('field_single[und][0][value]', 0, 'Entity 1: field_single value appears correctly is the form.'); - $this->assertFieldByName('field_unlimited[und][0][value]', 1, 'Entity 1: field_unlimited value 0 appears correctly is the form.'); - $this->assertFieldByName('entity_2[field_single][und][0][value]', 10, 'Entity 2: field_single value appears correctly is the form.'); - $this->assertFieldByName('entity_2[field_unlimited][und][0][value]', 11, 'Entity 2: field_unlimited value 0 appears correctly is the form.'); - - // Submit the form and check that the entities are updated accordingly. - $edit = array( - 'field_single[und][0][value]' => 1, - 'field_unlimited[und][0][value]' => 2, - 'field_unlimited[und][1][value]' => 3, - 'entity_2[field_single][und][0][value]' => 11, - 'entity_2[field_unlimited][und][0][value]' => 12, - 'entity_2[field_unlimited][und][1][value]' => 13, - ); - $this->drupalPost(NULL, $edit, t('Save')); - field_cache_clear(); - $entity_1 = field_test_create_entity(1); - $entity_2 = field_test_create_entity(2); - $this->assertFieldValues($entity_1, 'field_single', Language::LANGCODE_NOT_SPECIFIED, array(1)); - $this->assertFieldValues($entity_1, 'field_unlimited', Language::LANGCODE_NOT_SPECIFIED, array(2, 3)); - $this->assertFieldValues($entity_2, 'field_single', Language::LANGCODE_NOT_SPECIFIED, array(11)); - $this->assertFieldValues($entity_2, 'field_unlimited', Language::LANGCODE_NOT_SPECIFIED, array(12, 13)); - - // Submit invalid values and check that errors are reported on the - // correct widgets. - $edit = array( - 'field_unlimited[und][1][value]' => -1, - ); - $this->drupalPost('test-entity/nested/1/2', $edit, t('Save')); - $this->assertRaw(t('%label does not accept the value -1', array('%label' => 'Unlimited field')), 'Entity 1: the field validation error was reported.'); - $error_field = $this->xpath('//input[@id=:id and contains(@class, "error")]', array(':id' => 'edit-field-unlimited-und-1-value')); - $this->assertTrue($error_field, 'Entity 1: the error was flagged on the correct element.'); - $edit = array( - 'entity_2[field_unlimited][und][1][value]' => -1, - ); - $this->drupalPost('test-entity/nested/1/2', $edit, t('Save')); - $this->assertRaw(t('%label does not accept the value -1', array('%label' => 'Unlimited field')), 'Entity 2: the field validation error was reported.'); - $error_field = $this->xpath('//input[@id=:id and contains(@class, "error")]', array(':id' => 'edit-entity-2-field-unlimited-und-1-value')); - $this->assertTrue($error_field, 'Entity 2: the error was flagged on the correct element.'); - - // Test that reordering works on both entities. - $edit = array( - 'field_unlimited[und][0][_weight]' => 0, - 'field_unlimited[und][1][_weight]' => -1, - 'entity_2[field_unlimited][und][0][_weight]' => 0, - 'entity_2[field_unlimited][und][1][_weight]' => -1, - ); - $this->drupalPost('test-entity/nested/1/2', $edit, t('Save')); - field_cache_clear(); - $this->assertFieldValues($entity_1, 'field_unlimited', Language::LANGCODE_NOT_SPECIFIED, array(3, 2)); - $this->assertFieldValues($entity_2, 'field_unlimited', Language::LANGCODE_NOT_SPECIFIED, array(13, 12)); - - // Test the 'add more' buttons. Only Ajax submission is tested, because - // the two 'add more' buttons present in the form have the same #value, - // which confuses drupalPost(). - // 'Add more' button in the first entity: - $this->drupalGet('test-entity/nested/1/2'); - $this->drupalPostAJAX(NULL, array(), 'field_unlimited_add_more'); - $this->assertFieldByName('field_unlimited[und][0][value]', 3, 'Entity 1: field_unlimited value 0 appears correctly is the form.'); - $this->assertFieldByName('field_unlimited[und][1][value]', 2, 'Entity 1: field_unlimited value 1 appears correctly is the form.'); - $this->assertFieldByName('field_unlimited[und][2][value]', '', 'Entity 1: field_unlimited value 2 appears correctly is the form.'); - $this->assertFieldByName('field_unlimited[und][3][value]', '', 'Entity 1: an empty widget was added for field_unlimited value 3.'); - // 'Add more' button in the first entity (changing field values): - $edit = array( - 'entity_2[field_unlimited][und][0][value]' => 13, - 'entity_2[field_unlimited][und][1][value]' => 14, - 'entity_2[field_unlimited][und][2][value]' => 15, - ); - $this->drupalPostAJAX(NULL, $edit, 'entity_2_field_unlimited_add_more'); - $this->assertFieldByName('entity_2[field_unlimited][und][0][value]', 13, 'Entity 2: field_unlimited value 0 appears correctly is the form.'); - $this->assertFieldByName('entity_2[field_unlimited][und][1][value]', 14, 'Entity 2: field_unlimited value 1 appears correctly is the form.'); - $this->assertFieldByName('entity_2[field_unlimited][und][2][value]', 15, 'Entity 2: field_unlimited value 2 appears correctly is the form.'); - $this->assertFieldByName('entity_2[field_unlimited][und][3][value]', '', 'Entity 2: an empty widget was added for field_unlimited value 3.'); - // Save the form and check values are saved correclty. - $this->drupalPost(NULL, array(), t('Save')); - field_cache_clear(); - $this->assertFieldValues($entity_1, 'field_unlimited', Language::LANGCODE_NOT_SPECIFIED, array(3, 2)); - $this->assertFieldValues($entity_2, 'field_unlimited', Language::LANGCODE_NOT_SPECIFIED, array(13, 14, 15)); - } - - /** * Tests the Hidden widget. */ function testFieldFormHiddenWidget() { diff --git a/core/modules/field/lib/Drupal/field/Tests/NestedFormTest.php b/core/modules/field/lib/Drupal/field/Tests/NestedFormTest.php new file mode 100644 index 0000000..a3d4dc9 --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Tests/NestedFormTest.php @@ -0,0 +1,197 @@ + 'Nested form', + 'description' => 'Test the support for field elements in nested forms.', + 'group' => 'Field API', + ); + } + + public function setUp() { + parent::setUp(); + + $web_user = $this->drupalCreateUser(array('view test entity', 'administer entity_test content')); + $this->drupalLogin($web_user); + + $this->field_single = array('field_name' => 'field_single', 'type' => 'test_field'); + $this->field_unlimited = array('field_name' => 'field_unlimited', 'type' => 'test_field', 'cardinality' => FIELD_CARDINALITY_UNLIMITED); + + $this->instance = array( + 'entity_type' => 'entity_test', + 'bundle' => 'entity_test', + 'label' => $this->randomName() . '_label', + 'description' => '[site:name]_description', + 'weight' => mt_rand(0, 127), + 'settings' => array( + 'test_instance_setting' => $this->randomName(), + ), + ); + } + + /** + * Tests Field API form integration within a subform. + */ + function testNestedFieldForm() { + // Add two instances on the 'entity_test' + field_create_field($this->field_single); + field_create_field($this->field_unlimited); + $this->instance['field_name'] = 'field_single'; + $this->instance['label'] = 'Single field'; + field_create_instance($this->instance); + entity_get_form_display($this->instance['entity_type'], $this->instance['bundle'], 'default') + ->setComponent($this->instance['field_name']) + ->save(); + $this->instance['field_name'] = 'field_unlimited'; + $this->instance['label'] = 'Unlimited field'; + field_create_instance($this->instance); + entity_get_form_display($this->instance['entity_type'], $this->instance['bundle'], 'default') + ->setComponent($this->instance['field_name']) + ->save(); + + // Create two entities. + $entity_type = 'entity_test'; + $entity_1 = entity_create($entity_type, array('id' => 1)); + $entity_1->enforceIsNew(); + $entity_1->field_single->value = 0; + $entity_1->field_unlimited->value = 1; + $entity_1->save(); + + $entity_2 = entity_create($entity_type, array('id' => 2)); + $entity_2->enforceIsNew(); + $entity_2->field_single->value = 10; + $entity_2->field_unlimited->value = 11; + $entity_2->save(); + + // Display the 'combined form'. + $this->drupalGet('test-entity/nested/1/2'); + $this->assertFieldByName('field_single[und][0][value]', 0, 'Entity 1: field_single value appears correctly is the form.'); + $this->assertFieldByName('field_unlimited[und][0][value]', 1, 'Entity 1: field_unlimited value 0 appears correctly is the form.'); + $this->assertFieldByName('entity_2[field_single][und][0][value]', 10, 'Entity 2: field_single value appears correctly is the form.'); + $this->assertFieldByName('entity_2[field_unlimited][und][0][value]', 11, 'Entity 2: field_unlimited value 0 appears correctly is the form.'); + + // Submit the form and check that the entities are updated accordingly. + $edit = array( + 'field_single[und][0][value]' => 1, + 'field_unlimited[und][0][value]' => 2, + 'field_unlimited[und][1][value]' => 3, + 'entity_2[field_single][und][0][value]' => 11, + 'entity_2[field_unlimited][und][0][value]' => 12, + 'entity_2[field_unlimited][und][1][value]' => 13, + ); + $this->drupalPost(NULL, $edit, t('Save')); + field_cache_clear(); + $entity_1 = entity_load($entity_type, 1); + $entity_2 = entity_load($entity_type, 2); + $this->assertFieldValues($entity_1, 'field_single', Language::LANGCODE_NOT_SPECIFIED, array(1)); + $this->assertFieldValues($entity_1, 'field_unlimited', Language::LANGCODE_NOT_SPECIFIED, array(2, 3)); + $this->assertFieldValues($entity_2, 'field_single', Language::LANGCODE_NOT_SPECIFIED, array(11)); + $this->assertFieldValues($entity_2, 'field_unlimited', Language::LANGCODE_NOT_SPECIFIED, array(12, 13)); + + // Submit invalid values and check that errors are reported on the + // correct widgets. + $edit = array( + 'field_unlimited[und][1][value]' => -1, + ); + $this->drupalPost('test-entity/nested/1/2', $edit, t('Save')); + $this->assertRaw(t('%label does not accept the value -1', array('%label' => 'Unlimited field')), 'Entity 1: the field validation error was reported.'); + $error_field = $this->xpath('//input[@id=:id and contains(@class, "error")]', array(':id' => 'edit-field-unlimited-und-1-value')); + $this->assertTrue($error_field, 'Entity 1: the error was flagged on the correct element.'); + $edit = array( + 'entity_2[field_unlimited][und][1][value]' => -1, + ); + $this->drupalPost('test-entity/nested/1/2', $edit, t('Save')); + $this->assertRaw(t('%label does not accept the value -1', array('%label' => 'Unlimited field')), 'Entity 2: the field validation error was reported.'); + $error_field = $this->xpath('//input[@id=:id and contains(@class, "error")]', array(':id' => 'edit-entity-2-field-unlimited-und-1-value')); + $this->assertTrue($error_field, 'Entity 2: the error was flagged on the correct element.'); + + // Test that reordering works on both entities. + $edit = array( + 'field_unlimited[und][0][_weight]' => 0, + 'field_unlimited[und][1][_weight]' => -1, + 'entity_2[field_unlimited][und][0][_weight]' => 0, + 'entity_2[field_unlimited][und][1][_weight]' => -1, + ); + $this->drupalPost('test-entity/nested/1/2', $edit, t('Save')); + field_cache_clear(); + $this->assertFieldValues($entity_1, 'field_unlimited', Language::LANGCODE_NOT_SPECIFIED, array(3, 2)); + $this->assertFieldValues($entity_2, 'field_unlimited', Language::LANGCODE_NOT_SPECIFIED, array(13, 12)); + + // Test the 'add more' buttons. Only Ajax submission is tested, because + // the two 'add more' buttons present in the form have the same #value, + // which confuses drupalPost(). + // 'Add more' button in the first entity: + $this->drupalGet('test-entity/nested/1/2'); + $this->drupalPostAJAX(NULL, array(), 'field_unlimited_add_more'); + $this->assertFieldByName('field_unlimited[und][0][value]', 3, 'Entity 1: field_unlimited value 0 appears correctly is the form.'); + $this->assertFieldByName('field_unlimited[und][1][value]', 2, 'Entity 1: field_unlimited value 1 appears correctly is the form.'); + $this->assertFieldByName('field_unlimited[und][2][value]', '', 'Entity 1: field_unlimited value 2 appears correctly is the form.'); + $this->assertFieldByName('field_unlimited[und][3][value]', '', 'Entity 1: an empty widget was added for field_unlimited value 3.'); + // 'Add more' button in the first entity (changing field values): + $edit = array( + 'entity_2[field_unlimited][und][0][value]' => 13, + 'entity_2[field_unlimited][und][1][value]' => 14, + 'entity_2[field_unlimited][und][2][value]' => 15, + ); + $this->drupalPostAJAX(NULL, $edit, 'entity_2_field_unlimited_add_more'); + $this->assertFieldByName('entity_2[field_unlimited][und][0][value]', 13, 'Entity 2: field_unlimited value 0 appears correctly is the form.'); + $this->assertFieldByName('entity_2[field_unlimited][und][1][value]', 14, 'Entity 2: field_unlimited value 1 appears correctly is the form.'); + $this->assertFieldByName('entity_2[field_unlimited][und][2][value]', 15, 'Entity 2: field_unlimited value 2 appears correctly is the form.'); + $this->assertFieldByName('entity_2[field_unlimited][und][3][value]', '', 'Entity 2: an empty widget was added for field_unlimited value 3.'); + // Save the form and check values are saved correctly. + $this->drupalPost(NULL, array(), t('Save')); + field_cache_clear(); + $this->assertFieldValues($entity_1, 'field_unlimited', Language::LANGCODE_NOT_SPECIFIED, array(3, 2)); + $this->assertFieldValues($entity_2, 'field_unlimited', Language::LANGCODE_NOT_SPECIFIED, array(13, 14, 15)); + } + + /** + * Assert that a field has the expected values in an entity. + * + * This function only checks a single column in the field values. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to test. + * @param string $field_name + * The name of the field to test. + * @param string $langcode + * The language code for the values. + * @param array $expected_values + * The array of expected values. + * @param string $column + * (Optional) the name of the column to check. + */ + function assertFieldValues(EntityInterface $entity, $field_name, $langcode, $expected_values, $column = 'value') { + // Re-load the entity to make sure we have the latest changes. + entity_get_controller($entity->entityType())->resetCache(array($entity->id())); + $e = entity_load($entity->entityType(), $entity->id()); + $field = $values = $e->getTranslation($langcode, FALSE)->$field_name; + // Filter out empty values so that they don't mess with the assertions. + $field->filterEmptyValues(); + $values = $field->getValue(); + $this->assertEqual(count($values), count($expected_values), 'Expected number of values were saved.'); + foreach ($expected_values as $key => $value) { + $this->assertEqual($values[$key][$column], $value, format_string('Value @value was saved correctly.', array('@value' => $value))); + } + } + +} diff --git a/core/modules/field/lib/Drupal/field/Tests/TranslationTest.php b/core/modules/field/lib/Drupal/field/Tests/TranslationTest.php index ae52f2e..9e97e34 100644 --- a/core/modules/field/lib/Drupal/field/Tests/TranslationTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/TranslationTest.php @@ -12,7 +12,7 @@ /** * Unit test class for the multilanguage fields logic. * - * The following tests will check the multilanguage logic of _field_invoke() and + * The following tests will check the multilanguage logic in field handling, and * that only the correct values are returned by field_available_languages(). */ class TranslationTest extends FieldUnitTestBase { @@ -101,117 +101,6 @@ function testFieldAvailableLanguages() { } /** - * Test the multilanguage logic of _field_invoke(). - */ - function testFieldInvoke() { - // Enable field translations for the entity. - field_test_entity_info_translatable('test_entity', TRUE); - - $entity_type = 'test_entity'; - $entity = field_test_create_entity(0, 0, $this->instance['bundle']); - - // Populate some extra languages to check if _field_invoke() correctly uses - // the result of field_available_languages(). - $values = array(); - $extra_langcodes = mt_rand(1, 4); - $langcodes = $available_langcodes = field_available_languages($this->entity_type, $this->field); - for ($i = 0; $i < $extra_langcodes; ++$i) { - $langcodes[] = $this->randomName(2); - } - - // For each given language provide some random values. - foreach ($langcodes as $langcode) { - for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { - $values[$langcode][$delta]['value'] = mt_rand(1, 127); - } - } - $entity->{$this->field_name} = $values; - - $results = _field_invoke('test_op', $entity); - foreach ($results as $langcode => $result) { - $hash = hash('sha256', serialize(array($entity, $this->field_name, $langcode, $values[$langcode]))); - // Check whether the parameters passed to _field_invoke() were correctly - // forwarded to the callback function. - $this->assertEqual($hash, $result, format_string('The result for %language is correctly stored.', array('%language' => $langcode))); - } - - $this->assertEqual(count($results), count($available_langcodes), 'No unavailable language has been processed.'); - } - - /** - * Test the multilanguage logic of _field_invoke_multiple(). - */ - function testFieldInvokeMultiple() { - // Enable field translations for the entity. - field_test_entity_info_translatable('test_entity', TRUE); - - $values = array(); - $options = array(); - $entities = array(); - $entity_type = 'test_entity'; - $entity_count = 5; - $available_langcodes = field_available_languages($this->entity_type, $this->field); - - for ($id = 1; $id <= $entity_count; ++$id) { - $entity = field_test_create_entity($id, $id, $this->instance['bundle']); - $langcodes = $available_langcodes; - - // Populate some extra languages to check whether _field_invoke() - // correctly uses the result of field_available_languages(). - $extra_langcodes = mt_rand(1, 4); - for ($i = 0; $i < $extra_langcodes; ++$i) { - $langcodes[] = $this->randomName(2); - } - - // For each given language provide some random values. - $language_count = count($langcodes); - for ($i = 0; $i < $language_count; ++$i) { - $langcode = $langcodes[$i]; - // Avoid to populate at least one field translation to check that - // per-entity language suggestions work even when available field values - // are different for each language. - if ($i !== $id) { - for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { - $values[$id][$langcode][$delta]['value'] = mt_rand(1, 127); - } - } - // Ensure that a language for which there is no field translation is - // used as display language to prepare per-entity language suggestions. - elseif (!isset($display_langcode)) { - $display_langcode = $langcode; - } - } - - $entity->{$this->field_name} = $values[$id]; - $entities[$id] = $entity; - - // Store per-entity language suggestions. - $options['langcode'][$id] = field_language($entity, NULL, $display_langcode); - } - - $grouped_results = _field_invoke_multiple('test_op_multiple', $entity_type, $entities); - foreach ($grouped_results as $id => $results) { - foreach ($results as $langcode => $result) { - if (isset($values[$id][$langcode])) { - $hash = hash('sha256', serialize(array($entity_type, $entities[$id], $this->field_name, $langcode, $values[$id][$langcode]))); - // Check whether the parameters passed to _field_invoke_multiple() - // were correctly forwarded to the callback function. - $this->assertEqual($hash, $result, format_string('The result for entity %id/%language is correctly stored.', array('%id' => $id, '%language' => $langcode))); - } - } - $this->assertEqual(count($results), count($available_langcodes), format_string('No unavailable language has been processed for entity %id.', array('%id' => $id))); - } - - $null = NULL; - $grouped_results = _field_invoke_multiple('test_op_multiple', $entity_type, $entities, $null, $null, $options); - foreach ($grouped_results as $id => $results) { - foreach ($results as $langcode => $result) { - $this->assertTrue(isset($options['langcode'][$id]), format_string('The result language code %langcode for entity %id was correctly suggested (display language: %display_langcode).', array('%id' => $id, '%langcode' => $langcode, '%display_langcode' => $display_langcode))); - } - } - } - - /** * Test translatable fields storage/retrieval. */ function testTranslatableFieldSaveLoad() { diff --git a/core/modules/field/tests/modules/field_test/field_test.entity.inc b/core/modules/field/tests/modules/field_test/field_test.entity.inc index f00a57e..fb5dfc7 100644 --- a/core/modules/field/tests/modules/field_test/field_test.entity.inc +++ b/core/modules/field/tests/modules/field_test/field_test.entity.inc @@ -224,10 +224,10 @@ function field_test_entity_edit(TestEntity $entity) { */ function field_test_entity_nested_form($form, &$form_state, $entity_1, $entity_2) { // First entity. - foreach (array('ftid', 'ftvid', 'fttype') as $key) { + foreach (array('id', 'type') as $key) { $form[$key] = array( '#type' => 'value', - '#value' => $entity_1->$key, + '#value' => $entity_1->$key->value, ); } $form_state['form_display'] = entity_get_form_display($entity_1->entityType(), $entity_1->bundle(), 'default'); @@ -241,10 +241,10 @@ function field_test_entity_nested_form($form, &$form_state, $entity_1, $entity_2 '#parents' => array('entity_2'), '#weight' => 50, ); - foreach (array('ftid', 'ftvid', 'fttype') as $key) { + foreach (array('id', 'type') as $key) { $form['entity_2'][$key] = array( '#type' => 'value', - '#value' => $entity_2->$key, + '#value' => $entity_2->$key->value, ); } $form_state['form_display'] = entity_get_form_display($entity_1->entityType(), $entity_1->bundle(), 'default'); @@ -263,11 +263,11 @@ function field_test_entity_nested_form($form, &$form_state, $entity_1, $entity_2 * Validate handler for field_test_entity_nested_form(). */ function field_test_entity_nested_form_validate($form, &$form_state) { - $entity_1 = entity_create('test_entity', $form_state['values']); + $entity_1 = entity_create('entity_test', $form_state['values']); field_attach_extract_form_values($entity_1, $form, $form_state); field_attach_form_validate($entity_1, $form, $form_state); - $entity_2 = entity_create('test_entity', $form_state['values']['entity_2']); + $entity_2 = entity_create('entity_test', $form_state['values']['entity_2']); field_attach_extract_form_values($entity_2, $form['entity_2'], $form_state); field_attach_form_validate($entity_2, $form['entity_2'], $form_state); } @@ -276,13 +276,13 @@ function field_test_entity_nested_form_validate($form, &$form_state) { * Submit handler for field_test_entity_nested_form(). */ function field_test_entity_nested_form_submit($form, &$form_state) { - $entity_1 = entity_create('test_entity', $form_state['values']); + $entity_1 = entity_create('entity_test', $form_state['values']); field_attach_extract_form_values($entity_1, $form, $form_state); field_test_entity_save($entity_1); - $entity_2 = entity_create('test_entity', $form_state['values']['entity_2']); + $entity_2 = entity_create('entity_test', $form_state['values']['entity_2']); field_attach_extract_form_values($entity_2, $form['entity_2'], $form_state); field_test_entity_save($entity_2); - drupal_set_message(t('test_entities @id_1 and @id_2 have been updated.', array('@id_1' => $entity_1->ftid, '@id_2' => $entity_2->ftid))); + drupal_set_message(t('test_entities @id_1 and @id_2 have been updated.', array('@id_1' => $entity_1->id(), '@id_2' => $entity_2->id()))); } diff --git a/core/modules/field/tests/modules/field_test/field_test.field.inc b/core/modules/field/tests/modules/field_test/field_test.field.inc index 0ff40e7..db22e81 100644 --- a/core/modules/field/tests/modules/field_test/field_test.field.inc +++ b/core/modules/field/tests/modules/field_test/field_test.field.inc @@ -28,7 +28,7 @@ function field_test_field_info() { ), 'default_widget' => 'test_field_widget', 'default_formatter' => 'field_test_default', - 'field item class' => 'Drupal\field_test\Type\TestItem', + 'class' => 'Drupal\field_test\Type\TestItem', ), 'shape' => array( 'label' => t('Shape'), @@ -39,7 +39,7 @@ function field_test_field_info() { 'instance_settings' => array(), 'default_widget' => 'test_field_widget', 'default_formatter' => 'field_test_default', - 'field item class' => 'Drupal\field_test\Type\ShapeItem', + 'class' => 'Drupal\field_test\Type\ShapeItem', ), 'hidden_test_field' => array( 'no_ui' => TRUE, @@ -49,7 +49,7 @@ function field_test_field_info() { 'instance_settings' => array(), 'default_widget' => 'test_field_widget', 'default_formatter' => 'field_test_default', - 'field item class' => 'Drupal\field_test\Type\TestItem', + 'class' => 'Drupal\field_test\Type\HiddenTestItem', ), ); } @@ -139,7 +139,10 @@ function field_test_field_validate(EntityInterface $entity = NULL, $field, $inst * Implements hook_field_is_empty(). */ function field_test_field_is_empty($item, $field_type) { - return empty($item['value']); + if ($field_type == 'test_field') { + return empty($item['value']); + } + return empty($item['shape']) && empty($item['color']); } /** diff --git a/core/modules/field/tests/modules/field_test/field_test.module b/core/modules/field/tests/modules/field_test/field_test.module index 04f1c83..67f3229 100644 --- a/core/modules/field/tests/modules/field_test/field_test.module +++ b/core/modules/field/tests/modules/field_test/field_test.module @@ -63,11 +63,11 @@ function field_test_menu() { 'type' => MENU_NORMAL_ITEM, ); - $items['test-entity/nested/%field_test_entity_test/%field_test_entity_test'] = array( + $items['test-entity/nested/%entity_test/%entity_test'] = array( 'title' => 'Nested entity form', 'page callback' => 'drupal_get_form', 'page arguments' => array('field_test_entity_nested_form', 2, 3), - 'access arguments' => array('administer field_test content'), + 'access arguments' => array('administer entity_test content'), 'type' => MENU_NORMAL_ITEM, ); @@ -75,34 +75,6 @@ function field_test_menu() { } /** - * Generic op to test _field_invoke behavior. - * - * This simulates a field operation callback to be invoked by _field_invoke(). - */ -function field_test_field_test_op(EntityInterface $entity, $field, $instance, $langcode, &$items) { - return array($langcode => hash('sha256', serialize(array($entity, $field['field_name'], $langcode, $items)))); -} - -/** - * Generic op to test _field_invoke_multiple behavior. - * - * This simulates a multiple field operation callback to be invoked by - * _field_invoke_multiple(). - */ -function field_test_field_test_op_multiple($entity_type, $entities, $field, $instances, $langcode, &$items) { - $result = array(); - foreach ($entities as $id => $entity) { - // Entities, instances and items are assumed to be consistently grouped by - // language. To verify this we try to access all the passed data structures - // by entity id. If they are grouped correctly, one entity, one instance and - // one array of items should be available for each entity id. - $field_name = $instances[$id]['field_name']; - $result[$id] = array($langcode => hash('sha256', serialize(array($entity_type, $entity, $field_name, $langcode, $items[$id])))); - } - return $result; -} - -/** * Implements hook_field_available_languages_alter(). */ function field_test_field_available_languages_alter(&$langcodes, $context) { diff --git a/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/field/widget/TestFieldWidget.php b/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/field/widget/TestFieldWidget.php index 293ef18..21445d4 100644 --- a/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/field/widget/TestFieldWidget.php +++ b/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/field/widget/TestFieldWidget.php @@ -10,6 +10,7 @@ use Drupal\Component\Annotation\Plugin; use Drupal\Core\Annotation\Translation; use Drupal\field\Plugin\Type\Widget\WidgetBase; +use Symfony\Component\Validator\ConstraintViolationInterface; /** * Plugin implementation of the 'test_field_widget' widget. @@ -57,7 +58,7 @@ public function formElement(array $items, $delta, array $element, $langcode, arr /** * Implements Drupal\field\Plugin\Type\Widget\WidgetInterface::errorElement(). */ - public function errorElement(array $element, array $error, array $form, array &$form_state) { + public function errorElement(array $element, ConstraintViolationInterface $error, array $form, array &$form_state) { return $element['value']; } diff --git a/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/field/widget/TestFieldWidgetMultiple.php b/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/field/widget/TestFieldWidgetMultiple.php index 95b3cf2..d65f14f 100644 --- a/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/field/widget/TestFieldWidgetMultiple.php +++ b/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/field/widget/TestFieldWidgetMultiple.php @@ -10,6 +10,7 @@ use Drupal\Component\Annotation\Plugin; use Drupal\Core\Annotation\Translation; use Drupal\field\Plugin\Type\Widget\WidgetBase; +use Symfony\Component\Validator\ConstraintViolationInterface; /** * Plugin implementation of the 'test_field_widget_multiple' widget. @@ -63,7 +64,7 @@ public function formElement(array $items, $delta, array $element, $langcode, arr /** * Implements Drupal\field\Plugin\Type\Widget\WidgetInterface::errorElement(). */ - public function errorElement(array $element, array $error, array $form, array &$form_state) { + public function errorElement(array $element, ConstraintViolationInterface $error, array $form, array &$form_state) { return $element; } diff --git a/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Type/TestItem.php b/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Type/HiddenTestItem.php similarity index 83% copy from core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Type/TestItem.php copy to core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Type/HiddenTestItem.php index 0c61d15..7a9dd30 100644 --- a/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Type/TestItem.php +++ b/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Type/HiddenTestItem.php @@ -2,17 +2,15 @@ /** * @file - * Contains \Drupal\field_test\Type\TestItem. + * Contains \Drupal\field_test\Type\HiddenTestItem. */ namespace Drupal\field_test\Type; -use Drupal\Core\Entity\Field\FieldItemBase; - /** * Defines the 'test_field' entity field item. */ -class TestItem extends FieldItemBase { +class HiddenTestItem extends TestItem { /** * Property definitions of the contained properties. diff --git a/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Type/ShapeItem.php b/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Type/ShapeItem.php index 1a67329..4c0d4c9 100644 --- a/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Type/ShapeItem.php +++ b/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Type/ShapeItem.php @@ -7,12 +7,12 @@ namespace Drupal\field_test\Type; -use Drupal\Core\Entity\Field\FieldItemBase; +use Drupal\field\Plugin\field\field_type\LegacyConfigFieldItem; /** * Defines the 'shape_field' entity field item. */ -class ShapeItem extends FieldItemBase { +class ShapeItem extends LegacyConfigFieldItem { /** * Property definitions of the contained properties. diff --git a/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Type/TestItem.php b/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Type/TestItem.php index 0c61d15..20c34e3 100644 --- a/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Type/TestItem.php +++ b/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Type/TestItem.php @@ -7,12 +7,12 @@ namespace Drupal\field_test\Type; -use Drupal\Core\Entity\Field\FieldItemBase; +use Drupal\field\Plugin\field\field_type\LegacyConfigFieldItem; /** * Defines the 'test_field' entity field item. */ -class TestItem extends FieldItemBase { +class TestItem extends LegacyConfigFieldItem { /** * Property definitions of the contained properties. diff --git a/core/modules/field_sql_storage/field_sql_storage.module b/core/modules/field_sql_storage/field_sql_storage.module index 3ccac54..ef8a67f 100644 --- a/core/modules/field_sql_storage/field_sql_storage.module +++ b/core/modules/field_sql_storage/field_sql_storage.module @@ -416,7 +416,8 @@ function field_sql_storage_field_storage_load($entity_type, $entities, $age, $fi // from the prefixed database column. foreach ($field['columns'] as $column => $attributes) { $column_name = _field_sql_storage_columnname($field_name, $column); - $item[$column] = $row->$column_name; + // Unserialize the value if specified in the column schema. + $item[$column] = (!empty($attributes['serialize'])) ? unserialize($row->$column_name) : $row->$column_name; } // Add the item to the field values for the entity. @@ -496,7 +497,10 @@ function field_sql_storage_field_storage_write(EntityInterface $entity, $op, $fi 'langcode' => $langcode, ); foreach ($field['columns'] as $column => $attributes) { - $record[_field_sql_storage_columnname($field_name, $column)] = isset($item[$column]) ? $item[$column] : NULL; + $column_name = _field_sql_storage_columnname($field_name, $column); + $value = isset($item[$column]) ? $item[$column] : NULL; + // Serialize the value if specified in the column schema. + $record[$column_name] = (!empty($attributes['serialize'])) ? serialize($value) : $value; } $query->values($record); if (isset($vid)) { diff --git a/core/modules/field_ui/field_ui.api.php b/core/modules/field_ui/field_ui.api.php index b3bf03e..bece316 100644 --- a/core/modules/field_ui/field_ui.api.php +++ b/core/modules/field_ui/field_ui.api.php @@ -11,86 +11,6 @@ */ /** - * Add settings to a field settings form. - * - * Invoked from \Drupal\field_ui\Form\FieldInstanceEditForm to allow the module - * defining the field to add global settings (i.e. settings that do not depend - * on the bundle or instance) to the field settings form. If the field already - * has data, only include settings that are safe to change. - * - * @todo: Only the field type module knows which settings will affect the - * field's schema, but only the field storage module knows what schema - * changes are permitted once a field already has data. Probably we need an - * easy way for a field type module to ask whether an update to a new schema - * will be allowed without having to build up a fake $prior_field structure - * for hook_field_update_forbid(). - * - * @param $field - * The field structure being configured. - * @param $instance - * The instance structure being configured. - * - * @return - * The form definition for the field settings. - */ -function hook_field_settings_form($field, $instance) { - $settings = $field['settings']; - $form['max_length'] = array( - '#type' => 'number', - '#title' => t('Maximum length'), - '#default_value' => $settings['max_length'], - '#required' => FALSE, - '#min' => 1, - '#description' => t('The maximum length of the field in characters. Leave blank for an unlimited size.'), - ); - return $form; -} - -/** - * Add settings to an instance field settings form. - * - * Invoked from \Drupal\field_ui\Form\FieldInstanceEditForm to allow the module - * defining the field to add settings for a field instance. - * - * @param $field - * The field structure being configured. - * @param $instance - * The instance structure being configured. - * @param array $form_state - * The form state of the (entire) configuration form. - * - * @return - * The form definition for the field instance settings. - */ -function hook_field_instance_settings_form($field, $instance, $form_state) { - $settings = $instance['settings']; - - $form['text_processing'] = array( - '#type' => 'radios', - '#title' => t('Text processing'), - '#default_value' => $settings['text_processing'], - '#options' => array( - t('Plain text'), - t('Filtered text (user selects text format)'), - ), - ); - if ($field['type'] == 'text_with_summary') { - $form['display_summary'] = array( - '#type' => 'select', - '#title' => t('Display summary'), - '#options' => array( - t('No'), - t('Yes'), - ), - '#description' => t('Display the summary to allow the user to input a summary value. Hide the summary to automatically fill it with a trimmed portion from the main post.'), - '#default_value' => !empty($settings['display_summary']) ? $settings['display_summary'] : 0, - ); - } - - return $form; -} - -/** * Alters the formatter settings form. * * @param $element diff --git a/core/modules/field_ui/lib/Drupal/field_ui/Form/FieldEditForm.php b/core/modules/field_ui/lib/Drupal/field_ui/Form/FieldEditForm.php index c2f0a45..1f6dbcb 100644 --- a/core/modules/field_ui/lib/Drupal/field_ui/Form/FieldEditForm.php +++ b/core/modules/field_ui/lib/Drupal/field_ui/Form/FieldEditForm.php @@ -9,6 +9,7 @@ use Drupal\Core\Controller\ControllerInterface; use Drupal\Core\Entity\EntityManager; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormInterface; use Drupal\field\FieldInstanceInterface; use Drupal\field\Field; @@ -134,10 +135,11 @@ public function buildForm(array $form, array &$form_state, FieldInstanceInterfac $form['field']['settings'] = array( '#weight' => 10, ); - $additions = \Drupal::moduleHandler()->invoke($field['module'], 'field_settings_form', array($field, $this->instance)); - if (is_array($additions)) { - $form['field']['settings'] += $additions; - } + // Create an arbitrary entity object, so that we can have an instanciated + // FieldItem. + $ids = (object) array('entity_type' => $this->instance['entity_type'], 'bundle' => $this->instance['bundle'], 'entity_id' => NULL); + $entity = _field_create_entity_from_ids($ids); + $form['field']['settings'] += $this->getFieldItem($entity, $field['field_name'])->settingsForm($form, $form_state); $form['actions'] = array('#type' => 'actions'); $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save field settings')); @@ -193,4 +195,31 @@ public function submitForm(array &$form, array &$form_state) { } } + /** + * Returns a FieldItem object for an entity. + * + * @todo Remove when all entity types extend EntityNG. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * An entity. + * @param string $field_name + * The field name. + * + * @return \Drupal\field\Plugin\Type\FieldType\ConfigFieldItemInterface + * The field item object. + */ + protected function getFieldItem(EntityInterface $entity, $field_name) { + if ($entity instanceof \Drupal\Core\Entity\EntityNG) { + $item = $entity->get($field_name)->offsetGet(0); + } + else { + $definitions = \Drupal::entityManager()->getStorageController($entity->entityType())->getFieldDefinitions(array( + 'EntityType' => $entity->entityType(), + 'Bundle' => $entity->bundle(), + )); + $item = \Drupal::typedData()->create($definitions[$field_name], array(), $field_name, $entity)->offsetGet(0); + } + return $item; + } + } diff --git a/core/modules/field_ui/lib/Drupal/field_ui/Form/FieldInstanceEditForm.php b/core/modules/field_ui/lib/Drupal/field_ui/Form/FieldInstanceEditForm.php index 393997a..5bf1e10 100644 --- a/core/modules/field_ui/lib/Drupal/field_ui/Form/FieldInstanceEditForm.php +++ b/core/modules/field_ui/lib/Drupal/field_ui/Form/FieldInstanceEditForm.php @@ -7,8 +7,10 @@ namespace Drupal\field_ui\Form; -use Drupal\field\FieldInstanceInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityNG; use Drupal\Core\Language\Language; +use Drupal\field\FieldInstanceInterface; /** * Provides a form for the field instance settings form. @@ -97,12 +99,9 @@ public function buildForm(array $form, array &$form_state, FieldInstanceInterfac '#weight' => -5, ); - // Add additional field instance settings from the field module. - $additions = \Drupal::moduleHandler()->invoke($field['module'], 'field_instance_settings_form', array($field, $this->instance, $form_state)); - if (is_array($additions)) { - $form['instance']['settings'] = $additions; - $form['instance']['settings']['#weight'] = 10; - } + // Add instance settings for the field type. + $form['instance']['settings'] = $this->getFieldItem($form['#entity'], $this->instance['field_name'])->instanceSettingsForm($form, $form_state); + $form['instance']['settings']['#weight'] = 10; // Add widget settings for the widget type. $additions = $entity_form_display->getWidget($this->instance->getField()->id)->settingsForm($form, $form_state); @@ -136,7 +135,6 @@ public function validateForm(array &$form, array &$form_state) { $field_name = $this->instance['field_name']; $entity = $form['#entity']; $entity_form_display = $form['#entity_form_display']; - $field = $this->instance->getField(); if (isset($form['instance']['default_value_widget'])) { $element = $form['instance']['default_value_widget']; @@ -145,20 +143,28 @@ public function validateForm(array &$form, array &$form_state) { $items = array(); $entity_form_display->getWidget($this->instance->getField()->id)->extractFormValues($entity, Language::LANGCODE_NOT_SPECIFIED, $items, $element, $form_state); - // Get the field state. - $field_state = field_form_get_state($element['#parents'], $field_name, Language::LANGCODE_NOT_SPECIFIED, $form_state); - - // Validate the value. - $errors = array(); - $function = $field['module'] . '_field_validate'; - if (function_exists($function)) { - $function(NULL, $field, $this->instance, Language::LANGCODE_NOT_SPECIFIED, $items, $errors); + // @todo Simplify when all entity types are converted to EntityNG. + if ($entity instanceof EntityNG) { + $entity->{$field_name}->setValue($items); + $itemsNG = $entity->{$field_name}; + } + else { + // For BC entities, instanciate NG items objects manually. + $definitions = \Drupal::entityManager()->getStorageController($entity->entityType())->getFieldDefinitions(array( + 'EntityType' => $entity->entityType(), + 'Bundle' => $entity->bundle(), + )); + $itemsNG = \Drupal::typedData()->create($definitions[$field_name], $items, $field_name, $entity); } + $violations = $itemsNG->validate(); + + // Grab the field definition from $form_state. // Report errors. - if (isset($errors[$field_name][Language::LANGCODE_NOT_SPECIFIED])) { + if (count($violations)) { + $field_state = field_form_get_state($element['#parents'], $field_name, Language::LANGCODE_NOT_SPECIFIED, $form_state); // Store reported errors in $form_state. - $field_state['errors'] = $errors[$field_name][Language::LANGCODE_NOT_SPECIFIED]; + $field_state['constraint_violations'] = $violations; field_form_set_state($element['#parents'], $field_name, Language::LANGCODE_NOT_SPECIFIED, $form_state, $field_state); // Assign reported errors to the correct form element. @@ -265,4 +271,31 @@ protected function getDefaultValueWidget($field, array &$form, &$form_state) { return $element; } + /** + * Returns a FieldItem object for an entity. + * + * @todo Remove when all entity types extend EntityNG. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * An entity. + * @param string $field_name + * The field name. + * + * @return \Drupal\field\Plugin\Type\FieldType\ConfigFieldItemInterface + * The field item object. + */ + protected function getFieldItem(EntityInterface $entity, $field_name) { + if ($entity instanceof EntityNG) { + $item = $entity->get($field_name)->offsetGet(0); + } + else { + $definitions = \Drupal::entityManager()->getStorageController($entity->entityType())->getFieldDefinitions(array( + 'EntityType' => $entity->entityType(), + 'Bundle' => $entity->bundle(), + )); + $item = \Drupal::typedData()->create($definitions[$field_name], array(), $field_name, $entity)->offsetGet(0); + } + return $item; + } + } diff --git a/core/modules/field_ui/lib/Drupal/field_ui/Tests/ManageFieldsTest.php b/core/modules/field_ui/lib/Drupal/field_ui/Tests/ManageFieldsTest.php index c0f5471..8101532 100644 --- a/core/modules/field_ui/lib/Drupal/field_ui/Tests/ManageFieldsTest.php +++ b/core/modules/field_ui/lib/Drupal/field_ui/Tests/ManageFieldsTest.php @@ -390,7 +390,7 @@ function testLockedField() { } /** - * Tests that Field UI respects the 'no_ui' option in hook_field_info(). + * Tests that Field UI respects the 'no_ui' flag in the field type definition. */ function testHiddenFields() { $bundle_path = 'admin/structure/types/manage/' . $this->type . '/fields/'; diff --git a/core/modules/file/file.field.inc b/core/modules/file/file.field.inc index 9a327c9..d4ba70e 100644 --- a/core/modules/file/file.field.inc +++ b/core/modules/file/file.field.inc @@ -29,7 +29,7 @@ function file_field_info() { ), 'default_widget' => 'file_generic', 'default_formatter' => 'file_default', - 'field item class' => '\Drupal\file\Type\FileItem', + 'class' => '\Drupal\file\Type\FileItem', ), ); } @@ -191,27 +191,23 @@ function file_field_prepare_view($entity_type, $entities, $field, $instances, $l $fids = array(); foreach ($entities as $id => $entity) { foreach ($items[$id] as $delta => $item) { - if (!file_field_displayed($item, $field)) { - unset($items[$id][$delta]); - } - elseif (!empty($item['fid'])) { + // @todo Fixes from http://drupal.org/node/2020677 + if (file_field_displayed($item, $field) && !empty($item['fid'])) { // Load the files from the files table. $fids[] = $item['fid']; } } - // Ensure consecutive deltas. - $items[$id] = array_values($items[$id]); } - $files = file_load_multiple($fids); - foreach ($entities as $id => $entity) { - foreach ($items[$id] as $delta => $item) { - // If the file does not exist, mark the entire item as empty. - if (empty($item['fid']) || !isset($files[$item['fid']])) { - $items[$id][$delta] = NULL; - } - else { - $items[$id][$delta]['entity'] = $files[$item['fid']]; + if ($fids) { + $files = file_load_multiple($fids); + + foreach ($entities as $id => $entity) { + foreach ($items[$id] as $delta => $item) { + // If the file does not exist, mark the entire item as empty. + if (!empty($item['fid'])) { + $items[$id][$delta]['entity'] = isset($files[$item['fid']]) ? $files[$item['fid']] : NULL; + } } } } diff --git a/core/modules/file/lib/Drupal/file/Plugin/field/formatter/GenericFileFormatter.php b/core/modules/file/lib/Drupal/file/Plugin/field/formatter/GenericFileFormatter.php index 3cb0eda..a6dbee1 100644 --- a/core/modules/file/lib/Drupal/file/Plugin/field/formatter/GenericFileFormatter.php +++ b/core/modules/file/lib/Drupal/file/Plugin/field/formatter/GenericFileFormatter.php @@ -33,11 +33,14 @@ public function viewElements(EntityInterface $entity, $langcode, array $items) { $elements = array(); foreach ($items as $delta => $item) { - $elements[$delta] = array( - '#theme' => 'file_link', - '#file' => $item['entity'], - '#description' => $item['description'], - ); + // @todo Fixes from http://drupal.org/node/2020677 + if ($item['display'] && $item['entity']) { + $elements[$delta] = array( + '#theme' => 'file_link', + '#file' => $item['entity'], + '#description' => $item['description'], + ); + } } return $elements; diff --git a/core/modules/file/lib/Drupal/file/Type/FileItem.php b/core/modules/file/lib/Drupal/file/Type/FileItem.php index 0b2c74b..1e4ea71 100644 --- a/core/modules/file/lib/Drupal/file/Type/FileItem.php +++ b/core/modules/file/lib/Drupal/file/Type/FileItem.php @@ -7,13 +7,12 @@ namespace Drupal\file\Type; -use Drupal\Core\Entity\Field\FieldItemBase; -use Drupal\Core\TypedData\TypedDataInterface; +use Drupal\field\Plugin\field\field_type\LegacyConfigFieldItem; /** * Defines the 'file_field' entity field item. */ -class FileItem extends FieldItemBase { +class FileItem extends LegacyConfigFieldItem { /** * Property definitions of the contained properties. diff --git a/core/modules/image/image.field.inc b/core/modules/image/image.field.inc index ab64ffe..a122ba6 100644 --- a/core/modules/image/image.field.inc +++ b/core/modules/image/image.field.inc @@ -48,7 +48,7 @@ function image_field_info() { ), 'default_widget' => 'image_image', 'default_formatter' => 'image', - 'field item class' => '\Drupal\image\Type\ImageItem', + 'class' => '\Drupal\image\Type\ImageItem', ), ); } diff --git a/core/modules/image/lib/Drupal/image/Type/ImageItem.php b/core/modules/image/lib/Drupal/image/Type/ImageItem.php index f6cedf2..9b8877f 100644 --- a/core/modules/image/lib/Drupal/image/Type/ImageItem.php +++ b/core/modules/image/lib/Drupal/image/Type/ImageItem.php @@ -7,12 +7,12 @@ namespace Drupal\image\Type; -use Drupal\Core\Entity\Field\FieldItemBase; +use Drupal\field\Plugin\field\field_type\LegacyConfigFieldItem; /** * Defines the 'image_field' entity field item. */ -class ImageItem extends FieldItemBase { +class ImageItem extends LegacyConfigFieldItem { /** * Property definitions of the contained properties. @@ -33,7 +33,7 @@ public function getPropertyDefinitions() { 'label' => t('Referenced file id.'), ); static::$propertyDefinitions['alt'] = array( - 'type' => 'boolean', + 'type' => 'string', 'label' => t("Alternative image text, for the image's 'alt' attribute."), ); static::$propertyDefinitions['title'] = array( diff --git a/core/modules/link/lib/Drupal/link/Type/LinkItem.php b/core/modules/link/lib/Drupal/link/Type/LinkItem.php index 913a8f2..d41d3be 100644 --- a/core/modules/link/lib/Drupal/link/Type/LinkItem.php +++ b/core/modules/link/lib/Drupal/link/Type/LinkItem.php @@ -7,12 +7,12 @@ namespace Drupal\link\Type; -use Drupal\Core\Entity\Field\FieldItemBase; +use Drupal\field\Plugin\field\field_type\LegacyConfigFieldItem; /** * Defines the 'link_field' entity field item. */ -class LinkItem extends FieldItemBase { +class LinkItem extends LegacyConfigFieldItem { /** * Property definitions of the contained properties. diff --git a/core/modules/link/link.module b/core/modules/link/link.module index bfd6ebf..a0d3aaa 100644 --- a/core/modules/link/link.module +++ b/core/modules/link/link.module @@ -32,7 +32,7 @@ function link_field_info() { ), 'default_widget' => 'link_default', 'default_formatter' => 'link', - 'field item class' => '\Drupal\link\Type\LinkItem', + 'class' => '\Drupal\link\Type\LinkItem', ); return $types; } @@ -55,24 +55,6 @@ function link_field_instance_settings_form($field, $instance) { } /** - * Implements hook_field_load(). - */ -function link_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) { - foreach ($entities as $id => $entity) { - foreach ($items[$id] as $delta => &$item) { - // Unserialize the attributes into an array. The value stored in the - // field data should either be NULL or a non-empty serialized array. - if (empty($item['attributes'])) { - $item['attributes'] = array(); - } - else { - $item['attributes'] = unserialize($item['attributes']); - } - } - } -} - -/** * Implements hook_field_is_empty(). */ function link_field_is_empty($item, $field_type) { @@ -87,9 +69,6 @@ function link_field_presave(EntityInterface $entity, $field, $instance, $langcod // Trim any spaces around the URL and link text. $item['url'] = trim($item['url']); $item['title'] = trim($item['title']); - - // Serialize the attributes array. - $item['attributes'] = !empty($item['attributes']) ? serialize($item['attributes']) : NULL; } } diff --git a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageController.php b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageController.php index 493b939..90fbdbb 100644 --- a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageController.php +++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageController.php @@ -165,6 +165,7 @@ public function save(EntityInterface $entity) { // Unlike the save() method from DatabaseStorageController, we invoke the // 'presave' hook first because we want to allow modules to alter the // entity before all the logic from our preSave() method. + $this->invokeFieldMethod('preSave', $entity); $this->invokeHook('presave', $entity); $entity->preSave($this); @@ -180,6 +181,7 @@ public function save(EntityInterface $entity) { if (!$entity->isNew()) { $this->resetCache(array($entity->{$this->idKey})); $entity->postSave($this, TRUE); + $this->invokeFieldMethod('update', $entity); $this->invokeHook('update', $entity); } else { @@ -188,6 +190,7 @@ public function save(EntityInterface $entity) { $entity->enforceIsNew(FALSE); $entity->postSave($this, FALSE); + $this->invokeFieldMethod('insert', $entity); $this->invokeHook('insert', $entity); } } diff --git a/core/modules/node/node.api.php b/core/modules/node/node.api.php index e15a08e..2214746 100644 --- a/core/modules/node/node.api.php +++ b/core/modules/node/node.api.php @@ -99,7 +99,6 @@ * - Validating a node during editing form submit (calling * node_form_validate()): * - hook_node_validate() (all) - * - field_attach_form_validate() * - Searching (calling node_search_execute()): * - hook_ranking() (all) * - Query is executed to find matching nodes diff --git a/core/modules/node/node.pages.inc b/core/modules/node/node.pages.inc index 9830e1f..ac63ef0 100644 --- a/core/modules/node/node.pages.inc +++ b/core/modules/node/node.pages.inc @@ -108,7 +108,6 @@ function node_add($node_type) { */ function node_preview(EntityInterface $node) { if (node_access('create', $node) || node_access('update', $node)) { - _field_invoke_multiple('load', 'node', array($node->nid => $node)); // Load the user's name when needed. if (isset($node->name)) { // The use of isset() is mandatory in the context of user IDs, because diff --git a/core/modules/number/lib/Drupal/number/Plugin/field/widget/NumberWidget.php b/core/modules/number/lib/Drupal/number/Plugin/field/widget/NumberWidget.php index 7cca027..39eddf9 100644 --- a/core/modules/number/lib/Drupal/number/Plugin/field/widget/NumberWidget.php +++ b/core/modules/number/lib/Drupal/number/Plugin/field/widget/NumberWidget.php @@ -10,6 +10,7 @@ use Drupal\Component\Annotation\Plugin; use Drupal\Core\Annotation\Translation; use Drupal\field\Plugin\Type\Widget\WidgetBase; +use Symfony\Component\Validator\ConstraintViolationInterface; /** * Plugin implementation of the 'number' widget. @@ -91,7 +92,7 @@ public function formElement(array $items, $delta, array $element, $langcode, arr /** * {@inheritdoc} */ - public function errorElement(array $element, array $error, array $form, array &$form_state) { + public function errorElement(array $element, ConstraintViolationInterface $error, array $form, array &$form_state) { return $element['value']; } diff --git a/core/modules/number/lib/Drupal/number/Type/DecimalItem.php b/core/modules/number/lib/Drupal/number/Type/DecimalItem.php index 5235f21..83284d4 100644 --- a/core/modules/number/lib/Drupal/number/Type/DecimalItem.php +++ b/core/modules/number/lib/Drupal/number/Type/DecimalItem.php @@ -7,12 +7,12 @@ namespace Drupal\number\Type; -use Drupal\Core\Entity\Field\FieldItemBase; +use Drupal\field\Plugin\field\field_type\LegacyConfigFieldItem; /** * Defines the 'number_decimal_field' entity field item. */ -class DecimalItem extends FieldItemBase { +class DecimalItem extends LegacyConfigFieldItem { /** * Definitions of the contained properties. diff --git a/core/modules/number/lib/Drupal/number/Type/FloatItem.php b/core/modules/number/lib/Drupal/number/Type/FloatItem.php index 8f8fddd..ea135e5 100644 --- a/core/modules/number/lib/Drupal/number/Type/FloatItem.php +++ b/core/modules/number/lib/Drupal/number/Type/FloatItem.php @@ -7,12 +7,12 @@ namespace Drupal\number\Type; -use Drupal\Core\Entity\Field\FieldItemBase; +use Drupal\field\Plugin\field\field_type\LegacyConfigFieldItem; /** * Defines the 'number_float_field' entity field item. */ -class FloatItem extends FieldItemBase { +class FloatItem extends LegacyConfigFieldItem { /** * Definitions of the contained properties. diff --git a/core/modules/number/lib/Drupal/number/Type/IntegerItem.php b/core/modules/number/lib/Drupal/number/Type/IntegerItem.php index 6b126f1..fb7daa0 100644 --- a/core/modules/number/lib/Drupal/number/Type/IntegerItem.php +++ b/core/modules/number/lib/Drupal/number/Type/IntegerItem.php @@ -7,12 +7,12 @@ namespace Drupal\number\Type; -use Drupal\Core\Entity\Field\FieldItemBase; +use Drupal\field\Plugin\field\field_type\LegacyConfigFieldItem; /** * Defines the 'number_integer_field' entity field item. */ -class IntegerItem extends FieldItemBase { +class IntegerItem extends LegacyConfigFieldItem { /** * Definitions of the contained properties. diff --git a/core/modules/number/number.module b/core/modules/number/number.module index 1504e4a..5e6bd92 100644 --- a/core/modules/number/number.module +++ b/core/modules/number/number.module @@ -31,7 +31,7 @@ function number_field_info() { 'instance_settings' => array('min' => '', 'max' => '', 'prefix' => '', 'suffix' => ''), 'default_widget' => 'number', 'default_formatter' => 'number_integer', - 'field item class' => '\Drupal\number\Type\IntegerItem', + 'class' => '\Drupal\number\Type\IntegerItem', ), 'number_decimal' => array( 'label' => t('Decimal'), @@ -40,7 +40,7 @@ function number_field_info() { 'instance_settings' => array('min' => '', 'max' => '', 'prefix' => '', 'suffix' => ''), 'default_widget' => 'number', 'default_formatter' => 'number_decimal', - 'field item class' => '\Drupal\number\Type\DecimalItem', + 'class' => '\Drupal\number\Type\DecimalItem', ), 'number_float' => array( 'label' => t('Float'), @@ -48,7 +48,7 @@ function number_field_info() { 'instance_settings' => array('min' => '', 'max' => '', 'prefix' => '', 'suffix' => ''), 'default_widget' => 'number', 'default_formatter' => 'number_decimal', - 'field item class' => '\Drupal\number\Type\FloatItem', + 'class' => '\Drupal\number\Type\FloatItem', ), ); } diff --git a/core/modules/options/lib/Drupal/options/Tests/OptionsDynamicValuesValidationTest.php b/core/modules/options/lib/Drupal/options/Tests/OptionsDynamicValuesValidationTest.php index 6651a17..f978a79 100644 --- a/core/modules/options/lib/Drupal/options/Tests/OptionsDynamicValuesValidationTest.php +++ b/core/modules/options/lib/Drupal/options/Tests/OptionsDynamicValuesValidationTest.php @@ -7,9 +7,6 @@ namespace Drupal\options\Tests; -use Drupal\Core\Language\Language; -use Drupal\field\FieldValidationException; - /** * Tests the Options field allowed values function. */ @@ -26,29 +23,19 @@ public static function getInfo() { * Test that allowed values function gets the entity. */ function testDynamicAllowedValues() { - // Verify that the test passes against every value we had. + // Verify that validation passes against every value we had. foreach ($this->test as $key => $value) { $this->entity->test_options->value = $value; - try { - field_attach_validate($this->entity); - $this->pass("$key should pass"); - } - catch (FieldValidationException $e) { - // This will display as an exception, no need for a separate error. - throw($e); - } + $violations = $this->entity->test_options->validate(); + $this->assertEqual(count($violations), 0, "$key is a valid value"); } - // Now verify that the test does not pass against anything else. + + // Now verify that validation does not pass against anything else. foreach ($this->test as $key => $value) { $this->entity->test_options->value = is_numeric($value) ? (100 - $value) : ('X' . $value); - $pass = FALSE; - try { - field_attach_validate($this->entity); - } - catch (FieldValidationException $e) { - $pass = TRUE; - } - $this->assertTrue($pass, $key . ' should not pass'); + $violations = $this->entity->test_options->validate(); + $this->assertEqual(count($violations), 1, "$key is not a valid value"); } } + } diff --git a/core/modules/options/lib/Drupal/options/Type/ListBooleanItem.php b/core/modules/options/lib/Drupal/options/Type/ListBooleanItem.php new file mode 100644 index 0000000..109a1c7 --- /dev/null +++ b/core/modules/options/lib/Drupal/options/Type/ListBooleanItem.php @@ -0,0 +1,13 @@ + 'string', - 'label' => t('Telephone number'), + 'label' => t('Text value'), ); } return static::$propertyDefinitions; } + } diff --git a/core/modules/options/options.module b/core/modules/options/options.module index 88a2b48..d95846e 100644 --- a/core/modules/options/options.module +++ b/core/modules/options/options.module @@ -36,7 +36,7 @@ function options_field_info() { 'settings' => array('allowed_values' => array(), 'allowed_values_function' => ''), 'default_widget' => 'options_select', 'default_formatter' => 'list_default', - 'field item class' => '\Drupal\number\Type\IntegerItem', + 'class' => '\Drupal\options\Type\ListIntegerItem', ), 'list_float' => array( 'label' => t('List (float)'), @@ -44,7 +44,7 @@ function options_field_info() { 'settings' => array('allowed_values' => array(), 'allowed_values_function' => ''), 'default_widget' => 'options_select', 'default_formatter' => 'list_default', - 'field item class' => '\Drupal\number\Type\FloatItem', + 'class' => '\Drupal\options\Type\ListFloatItem', ), 'list_text' => array( 'label' => t('List (text)'), @@ -52,7 +52,7 @@ function options_field_info() { 'settings' => array('allowed_values' => array(), 'allowed_values_function' => ''), 'default_widget' => 'options_select', 'default_formatter' => 'list_default', - 'field item class' => '\Drupal\text\Type\TextItem', + 'class' => '\Drupal\options\Type\ListTextItem', ), 'list_boolean' => array( 'label' => t('Boolean'), @@ -60,7 +60,7 @@ function options_field_info() { 'settings' => array('allowed_values' => array(), 'allowed_values_function' => ''), 'default_widget' => 'options_buttons', 'default_formatter' => 'list_default', - 'field item class' => '\Drupal\number\Type\IntegerItem', + 'class' => '\Drupal\options\Type\ListBooleanItem', ), ); } diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php index 8c46872..842ea8c 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php @@ -1043,14 +1043,16 @@ protected function curlInitialize() { protected function curlExec($curl_options, $redirect = FALSE) { $this->curlInitialize(); - // cURL incorrectly handles URLs with a fragment by including the - // fragment in the request to the server, causing some web servers - // to reject the request citing "400 - Bad Request". To prevent - // this, we strip the fragment from the request. - // TODO: Remove this for Drupal 8, since fixed in curl 7.20.0. - if (!empty($curl_options[CURLOPT_URL]) && strpos($curl_options[CURLOPT_URL], '#')) { - $original_url = $curl_options[CURLOPT_URL]; - $curl_options[CURLOPT_URL] = strtok($curl_options[CURLOPT_URL], '#'); + if (!empty($curl_options[CURLOPT_URL])) { + // cURL incorrectly handles URLs with a fragment by including the + // fragment in the request to the server, causing some web servers + // to reject the request citing "400 - Bad Request". To prevent + // this, we strip the fragment from the request. + // TODO: Remove this for Drupal 8, since fixed in curl 7.20.0. + if (strpos($curl_options[CURLOPT_URL], '#')) { + $original_url = $curl_options[CURLOPT_URL]; + $curl_options[CURLOPT_URL] = strtok($curl_options[CURLOPT_URL], '#'); + } } $url = empty($curl_options[CURLOPT_URL]) ? curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL) : $curl_options[CURLOPT_URL]; diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php index 93fc552..b1fb682 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php @@ -362,7 +362,7 @@ protected function assertIntrospection($entity_type) { $definitions = $wrapped_entity->getPropertyDefinitions($definition); $this->assertEqual($definitions['name']['type'], 'string_field', $entity_type .': Name field found.'); $this->assertEqual($definitions['user_id']['type'], 'entity_reference_field', $entity_type .': User field found.'); - $this->assertEqual($definitions['field_test_text']['type'], 'text_field', $entity_type .': Test-text-field field found.'); + $this->assertEqual($definitions['field_test_text']['type'], 'configurable_field_type:text', $entity_type .': Test-text-field field found.'); // Test introspecting an entity object. // @todo: Add bundles and test bundles as well. @@ -371,7 +371,7 @@ protected function assertIntrospection($entity_type) { $definitions = $entity->getPropertyDefinitions(); $this->assertEqual($definitions['name']['type'], 'string_field', $entity_type .': Name field found.'); $this->assertEqual($definitions['user_id']['type'], 'entity_reference_field', $entity_type .': User field found.'); - $this->assertEqual($definitions['field_test_text']['type'], 'text_field', $entity_type .': Test-text-field field found.'); + $this->assertEqual($definitions['field_test_text']['type'], 'configurable_field_type:text', $entity_type .': Test-text-field field found.'); $name_properties = $entity->name->getPropertyDefinitions(); $this->assertEqual($name_properties['value']['type'], 'string', $entity_type .': String value property of the name found.'); diff --git a/core/modules/system/lib/Drupal/system/Tests/TypedData/TypedDataTest.php b/core/modules/system/lib/Drupal/system/Tests/TypedData/TypedDataTest.php index 998e22e..83f5f4e 100644 --- a/core/modules/system/lib/Drupal/system/Tests/TypedData/TypedDataTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/TypedData/TypedDataTest.php @@ -29,7 +29,7 @@ class TypedDataTest extends DrupalUnitTestBase { * * @var array */ - public static $modules = array('system', 'file'); + public static $modules = array('system', 'field', 'file'); public static function getInfo() { return array( diff --git a/core/modules/system/lib/Drupal/system/Tests/Upgrade/FieldUpgradePathTest.php b/core/modules/system/lib/Drupal/system/Tests/Upgrade/FieldUpgradePathTest.php index ab3e56d..2dfeae7 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Upgrade/FieldUpgradePathTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Upgrade/FieldUpgradePathTest.php @@ -214,7 +214,7 @@ function testFieldUpgradeToConfig() { 'entity_id' => 2, 'revision_id' => 2, )); - field_attach_load('node', array(2 => $entity), FIELD_LOAD_CURRENT, array('field_id' => $deleted_field['uuid'], 'deleted' => 1)); + field_attach_load('node', array(2 => $entity), FIELD_LOAD_CURRENT, array('instance' => entity_create('field_instance', $deleted_instance))); $deleted_value = $entity->get('test_deleted_field'); $this->assertEqual($deleted_value[Language::LANGCODE_NOT_SPECIFIED][0]['value'], 'Some deleted value'); diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermFieldTest.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermFieldTest.php index 53a53b1..37a39a8 100644 --- a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermFieldTest.php +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermFieldTest.php @@ -8,7 +8,6 @@ namespace Drupal\taxonomy\Tests; use Drupal\Core\Language\Language; -use Drupal\field\FieldValidationException; /** * Tests for taxonomy term field and formatter. @@ -81,29 +80,19 @@ function setUp() { * Test term field validation. */ function testTaxonomyTermFieldValidation() { - // Test valid and invalid values with field_attach_validate(). - $langcode = Language::LANGCODE_NOT_SPECIFIED; - $entity = entity_create('entity_test', array()); + // Test validation with a valid value. $term = $this->createTerm($this->vocabulary); + $entity = entity_create('entity_test', array()); $entity->{$this->field_name}->tid = $term->id(); - try { - field_attach_validate($entity); - $this->pass('Correct term does not cause validation error.'); - } - catch (FieldValidationException $e) { - $this->fail('Correct term does not cause validation error.'); - } + $violations = $entity->{$this->field_name}->validate(); + $this->assertEqual(count($violations) , 0, 'Correct term does not cause validation error.'); - $entity = entity_create('entity_test', array()); + // Test validation with an invalid valid value (wrong vocabulary). $bad_term = $this->createTerm($this->createVocabulary()); + $entity = entity_create('entity_test', array()); $entity->{$this->field_name}->tid = $bad_term->id(); - try { - field_attach_validate($entity); - $this->fail('Wrong term causes validation error.'); - } - catch (FieldValidationException $e) { - $this->pass('Wrong term causes validation error.'); - } + $violations = $entity->{$this->field_name}->validate(); + $this->assertEqual(count($violations) , 1, 'Wrong term causes validation error.'); } /** diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Type/TaxonomyTermReferenceItem.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Type/TaxonomyTermReferenceItem.php index b558d97..7b40075 100644 --- a/core/modules/taxonomy/lib/Drupal/taxonomy/Type/TaxonomyTermReferenceItem.php +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Type/TaxonomyTermReferenceItem.php @@ -7,13 +7,12 @@ namespace Drupal\taxonomy\Type; -use Drupal\Core\Entity\Field\FieldItemBase; -use Drupal\Core\TypedData\TypedDataInterface; +use Drupal\field\Plugin\field\field_type\LegacyConfigFieldItem; /** * Defines the 'taxonomy_term_reference' entity field item. */ -class TaxonomyTermReferenceItem extends FieldItemBase { +class TaxonomyTermReferenceItem extends LegacyConfigFieldItem { /** * Property definitions of the contained properties. diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module index 980ad00..f9b7d42 100644 --- a/core/modules/taxonomy/taxonomy.module +++ b/core/modules/taxonomy/taxonomy.module @@ -903,7 +903,7 @@ function taxonomy_field_info() { 'description' => t('This field stores a reference to a taxonomy term.'), 'default_widget' => 'options_select', 'default_formatter' => 'taxonomy_term_reference_link', - 'field item class' => 'Drupal\taxonomy\Type\TaxonomyTermReferenceItem', + 'class' => 'Drupal\taxonomy\Type\TaxonomyTermReferenceItem', 'settings' => array( 'options_list_callback' => NULL, 'allowed_values' => array( diff --git a/core/modules/telephone/lib/Drupal/telephone/Type/TelephoneItem.php b/core/modules/telephone/lib/Drupal/telephone/Type/TelephoneItem.php index 195e4a5..137dcbc 100644 --- a/core/modules/telephone/lib/Drupal/telephone/Type/TelephoneItem.php +++ b/core/modules/telephone/lib/Drupal/telephone/Type/TelephoneItem.php @@ -7,12 +7,12 @@ namespace Drupal\telephone\Type; -use Drupal\Core\Entity\Field\FieldItemBase; +use Drupal\field\Plugin\field\field_type\LegacyConfigFieldItem; /** * Defines the 'telephone_field' entity field items. */ -class TelephoneItem extends FieldItemBase { +class TelephoneItem extends LegacyConfigFieldItem { /** * Definitions of the contained properties. diff --git a/core/modules/telephone/telephone.module b/core/modules/telephone/telephone.module index 5033bbc..e6d2e7f 100644 --- a/core/modules/telephone/telephone.module +++ b/core/modules/telephone/telephone.module @@ -15,7 +15,7 @@ function telephone_field_info() { 'description' => t('This field stores a telephone number in the database.'), 'default_widget' => 'telephone_default', 'default_formatter' => 'telephone_link', - 'field item class' => 'Drupal\telephone\Type\TelephoneItem', + 'class' => 'Drupal\telephone\Type\TelephoneItem', ), ); } diff --git a/core/modules/text/lib/Drupal/text/Plugin/field/field_type/ConfigTextItem.php b/core/modules/text/lib/Drupal/text/Plugin/field/field_type/ConfigTextItem.php new file mode 100644 index 0000000..ddae5ed --- /dev/null +++ b/core/modules/text/lib/Drupal/text/Plugin/field/field_type/ConfigTextItem.php @@ -0,0 +1,104 @@ + array( + 'value' => array( + 'type' => 'varchar', + 'length' => $field->settings['max_length'], + 'not null' => FALSE, + ), + 'format' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => FALSE, + ), + ), + 'indexes' => array( + 'format' => array('format'), + ), + 'foreign keys' => array( + 'format' => array( + 'table' => 'filter_format', + 'columns' => array('format' => 'format'), + ), + ), + ); + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, array &$form_state) { + $element = array(); + $field = $this->getInstance()->getField(); + + $element['max_length'] = array( + '#type' => 'number', + '#title' => t('Maximum length'), + '#default_value' => $field->settings['max_length'], + '#required' => TRUE, + '#description' => t('The maximum length of the field in characters.'), + '#min' => 1, + // @todo: If $has_data, add a validate handler that only allows + // max_length to increase. + '#disabled' => $field->hasData(), + ); + + return $element; + } + + /** + * {@inheritdoc} + */ + public function instanceSettingsForm(array $form, array &$form_state) { + $element = array(); + + $element['text_processing'] = array( + '#type' => 'radios', + '#title' => t('Text processing'), + '#default_value' => $this->getInstance()->settings['text_processing'], + '#options' => array( + t('Plain text'), + t('Filtered text (user selects text format)'), + ), + ); + + return $element; + } + +} diff --git a/core/modules/text/lib/Drupal/text/Plugin/field/field_type/ConfigTextItemBase.php b/core/modules/text/lib/Drupal/text/Plugin/field/field_type/ConfigTextItemBase.php new file mode 100644 index 0000000..686c318 --- /dev/null +++ b/core/modules/text/lib/Drupal/text/Plugin/field/field_type/ConfigTextItemBase.php @@ -0,0 +1,102 @@ + 'string', + 'label' => t('Text value'), + ); + static::$propertyDefinitions['format'] = array( + 'type' => 'string', + 'label' => t('Text format'), + ); + static::$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 static::$propertyDefinitions; + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + $value = $this->get('value')->getValue(); + return $value === NULL || $value === ''; + } + + /** + * {@inheritdoc} + */ + public function getConstraints() { + $constraint_manager = \Drupal::typedData()->getValidationConstraintManager(); + $constraints = parent::getConstraints(); + + if (!empty($this->getInstance()->getField()->settings['max_length'])) { + $max_length = $this->getInstance()->getField()->settings['max_length']; + $constraints[] = $constraint_manager->create('ComplexData', array( + 'value' => array( + 'Length' => array( + 'max' => $max_length, + 'maxMessage' => t('%name: the text may not be longer than @max characters.', array('%name' => $this->getInstance()->label, '@max' => $max_length)), + ) + ), + )); + } + + return $constraints; + } + + /** + * {@inheritdoc} + */ + public function prepareCache() { + // Where possible, generate the sanitized version of each field early so + // that it is cached in the field cache. This avoids the need to look up the + // field in the filter cache separately. + if (!$this->getInstance()->settings['text_processing'] || filter_format_allowcache($this->get('format')->getValue())) { + $itemBC = $this->getValue(); + $langcode = $this->getParent()->getParent()->language()->langcode; + $this->set('safe_value', text_sanitize($this->getInstance()->settings['text_processing'], $langcode, $itemBC, 'value')); + if ($this->getType() == 'configurable_field_type:text_with_summary') { + $this->set('safe_summary', text_sanitize($this->getInstance()->settings['text_processing'], $langcode, $itemBC, 'summary')); + } + } + } + +} diff --git a/core/modules/text/lib/Drupal/text/Plugin/field/field_type/ConfigTextLongItem.php b/core/modules/text/lib/Drupal/text/Plugin/field/field_type/ConfigTextLongItem.php new file mode 100644 index 0000000..578aaa8 --- /dev/null +++ b/core/modules/text/lib/Drupal/text/Plugin/field/field_type/ConfigTextLongItem.php @@ -0,0 +1,79 @@ + array( + 'value' => array( + 'type' => 'text', + 'size' => 'big', + 'not null' => FALSE, + ), + 'format' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => FALSE, + ), + ), + 'indexes' => array( + 'format' => array('format'), + ), + 'foreign keys' => array( + 'format' => array( + 'table' => 'filter_format', + 'columns' => array('format' => 'format'), + ), + ), + ); + } + + /** + * {@inheritdoc} + */ + public function instanceSettingsForm(array $form, array &$form_state) { + $element = array(); + + $element['text_processing'] = array( + '#type' => 'radios', + '#title' => t('Text processing'), + '#default_value' => $this->getInstance()->settings['text_processing'], + '#options' => array( + t('Plain text'), + t('Filtered text (user selects text format)'), + ), + ); + + return $element; + } + +} diff --git a/core/modules/text/lib/Drupal/text/Plugin/field/field_type/ConfigTextWithSummaryItem.php b/core/modules/text/lib/Drupal/text/Plugin/field/field_type/ConfigTextWithSummaryItem.php new file mode 100644 index 0000000..0c42014 --- /dev/null +++ b/core/modules/text/lib/Drupal/text/Plugin/field/field_type/ConfigTextWithSummaryItem.php @@ -0,0 +1,153 @@ + 'string', + 'label' => t('Summary text value'), + ); + static::$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 static::$propertyDefinitions; + } + + /** + * {@inheritdoc} + */ + public static function schema(Field $field) { + return array( + 'columns' => array( + 'value' => array( + 'type' => 'text', + 'size' => 'big', + 'not null' => FALSE, + ), + 'summary' => array( + 'type' => 'text', + 'size' => 'big', + 'not null' => FALSE, + ), + 'format' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => FALSE, + ), + ), + 'indexes' => array( + 'format' => array('format'), + ), + 'foreign keys' => array( + 'format' => array( + 'table' => 'filter_format', + 'columns' => array('format' => 'format'), + ), + ), + ); + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + $value = $this->get('summary')->getValue(); + return parent::isEmpty() && ($value === NULL || $value === ''); + } + + /** + * {@inheritdoc} + */ + public function getConstraints() { + $constraint_manager = \Drupal::typedData()->getValidationConstraintManager(); + $constraints = parent::getConstraints(); + + if (!empty($this->getInstance()->getField()->settings['max_length'])) { + $max_length = $this->getInstance()->getField()->settings['max_length']; + $constraints[] = $constraint_manager->create('ComplexData', array( + 'value' => array( + 'Length' => array( + 'max' => $max_length, + 'maxMessage' => t('%name: the summary may not be longer than @max characters.', array('%name' => $this->getInstance()->label, '@max' => $max_length)), + ) + ), + )); + } + + return $constraints; + } + + /** + * {@inheritdoc} + */ + public function instanceSettingsForm(array $form, array &$form_state) { + $element = array(); + + $element['text_processing'] = array( + '#type' => 'radios', + '#title' => t('Text processing'), + '#default_value' => $this->getInstance()->settings['text_processing'], + '#options' => array( + t('Plain text'), + t('Filtered text (user selects text format)'), + ), + ); + $element['display_summary'] = array( + '#type' => 'checkbox', + '#title' => t('Summary input'), + '#default_value' => $this->getInstance()->settings['display_summary'], + '#description' => t('This allows authors to input an explicit summary, to be displayed instead of the automatically trimmed text when using the "Summary or trimmed" display type.'), + ); + + return $element; + } + +} diff --git a/core/modules/text/lib/Drupal/text/Plugin/field/widget/TextareaWithSummaryWidget.php b/core/modules/text/lib/Drupal/text/Plugin/field/widget/TextareaWithSummaryWidget.php index b98ce32..ef7dc0d 100644 --- a/core/modules/text/lib/Drupal/text/Plugin/field/widget/TextareaWithSummaryWidget.php +++ b/core/modules/text/lib/Drupal/text/Plugin/field/widget/TextareaWithSummaryWidget.php @@ -9,6 +9,7 @@ use Drupal\Component\Annotation\Plugin; use Drupal\Core\Annotation\Translation; +use Symfony\Component\Validator\ConstraintViolationInterface; /** * Plugin implementation of the 'text_textarea_with_summary' widget. @@ -57,18 +58,8 @@ function formElement(array $items, $delta, array $element, $langcode, array &$fo /** * {@inheritdoc} */ - public function errorElement(array $element, array $error, array $form, array &$form_state) { - switch ($error['error']) { - case 'text_summary_max_length': - $error_element = $element['summary']; - break; - - default: - $error_element = $element; - break; - } - - return $error_element; + public function errorElement(array $element, ConstraintViolationInterface $violation, array $form, array &$form_state) { + return $element[$violation->arrayPropertyPath[0]]; } } diff --git a/core/modules/text/lib/Drupal/text/Tests/TextFieldTest.php b/core/modules/text/lib/Drupal/text/Tests/TextFieldTest.php index 86368d0..fc8a1d5 100644 --- a/core/modules/text/lib/Drupal/text/Tests/TextFieldTest.php +++ b/core/modules/text/lib/Drupal/text/Tests/TextFieldTest.php @@ -8,7 +8,6 @@ namespace Drupal\text\Tests; use Drupal\Core\Language\Language; -use Drupal\field\FieldValidationException; use Drupal\simpletest\WebTestBase; /** @@ -66,17 +65,16 @@ function testTextFieldValidation() { ); field_create_instance($this->instance); - // Test valid and invalid values with field_attach_validate(). + // Test validation with valid and invalid values. $entity = entity_create('entity_test', array()); - $langcode = Language::LANGCODE_NOT_SPECIFIED; for ($i = 0; $i <= $max_length + 2; $i++) { $entity->{$this->field['field_name']}->value = str_repeat('x', $i); - try { - field_attach_validate($entity); - $this->assertTrue($i <= $max_length, "Length $i does not cause validation error when max_length is $max_length"); + $violations = $entity->{$this->field['field_name']}->validate(); + if ($i <= $max_length) { + $this->assertEqual(count($violations), 0, "Length $i does not cause validation error when max_length is $max_length"); } - catch (FieldValidationException $e) { - $this->assertTrue($i > $max_length, "Length $i causes validation error when max_length is $max_length"); + else { + $this->assertEqual(count($violations), 1, "Length $i causes validation error when max_length is $max_length"); } } } diff --git a/core/modules/text/lib/Drupal/text/Tests/TextTranslationTest.php b/core/modules/text/lib/Drupal/text/Tests/TextTranslationTest.php deleted file mode 100644 index 113c724..0000000 --- a/core/modules/text/lib/Drupal/text/Tests/TextTranslationTest.php +++ /dev/null @@ -1,136 +0,0 @@ - 'Text translation', - 'description' => 'Check if the text field is correctly prepared for translation.', - 'group' => 'Field types', - ); - } - - function setUp() { - parent::setUp(); - - $full_html_format = filter_format_load('full_html'); - $this->format = $full_html_format->format; - $this->admin = $this->drupalCreateUser(array( - 'administer languages', - 'administer content types', - 'administer node fields', - 'access administration pages', - 'bypass node access', - filter_permission_name($full_html_format), - )); - $this->translator = $this->drupalCreateUser(array('create article content', 'edit own article content', 'translate all content')); - - // Enable an additional language. - $this->drupalLogin($this->admin); - $edit = array('langcode' => 'fr'); - $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); - - // Set "Article" content type to use multilingual support with translation. - $edit = array('language_configuration[language_show]' => TRUE, 'node_type_language_translation_enabled' => TRUE); - $this->drupalPost('admin/structure/types/manage/article', $edit, t('Save content type')); - $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Article')), 'Article content type has been updated.'); - } - - /** - * Test that a plaintext textfield widget is correctly populated. - */ - function testTextField() { - // Disable text processing for body. - $edit = array('instance[settings][text_processing]' => 0); - $this->drupalPost('admin/structure/types/manage/article/fields/node.article.body', $edit, t('Save settings')); - - // Login as translator. - $this->drupalLogin($this->translator); - - // Create content. - $langcode = Language::LANGCODE_NOT_SPECIFIED; - $body = $this->randomName(); - $edit = array( - 'title' => $this->randomName(), - 'langcode' => 'en', - "body[$langcode][0][value]" => $body, - ); - - // Translate the article in french. - $this->drupalPost('node/add/article', $edit, t('Save')); - $node = $this->drupalGetNodeByTitle($edit['title']); - $this->drupalGet("node/$node->nid/translate"); - $this->clickLink(t('Add translation')); - $this->assertFieldByXPath("//textarea[@name='body[$langcode][0][value]']", $body, 'The textfield widget is populated.'); - } - - /** - * Check that user that does not have access the field format cannot see the - * source value when creating a translation. - */ - function testTextFieldFormatted() { - // Make node body multiple. - $edit = array('field[cardinality]' => FIELD_CARDINALITY_UNLIMITED); - $this->drupalPost('admin/structure/types/manage/article/fields/node.article.body/field', $edit, t('Save field settings')); - $this->drupalGet('node/add/article'); - $this->assertFieldByXPath("//input[@name='body_add_more']", t('Add another item'), 'Body field cardinality set to multiple.'); - - $body = array( - $this->randomName(), - $this->randomName(), - ); - - // Create an article with the first body input format set to "Full HTML". - $title = $this->randomName(); - $edit = array( - 'title' => $title, - 'langcode' => 'en', - ); - $this->drupalPost('node/add/article', $edit, t('Save')); - - // Populate the body field: the first item gets the "Full HTML" input - // format, the second one "Basic HTML". - $formats = array('full_html', 'basic_html'); - $langcode = Language::LANGCODE_NOT_SPECIFIED; - foreach ($body as $delta => $value) { - $edit = array( - "body[$langcode][$delta][value]" => $value, - "body[$langcode][$delta][format]" => array_shift($formats), - ); - $this->drupalPost('node/1/edit', $edit, t('Save')); - $this->assertText($body[$delta], t('The body field with delta @delta has been saved.', array('@delta' => $delta))); - } - - // Login as translator. - $this->drupalLogin($this->translator); - - // Translate the article in french. - $node = $this->drupalGetNodeByTitle($title); - $this->drupalGet("node/$node->nid/translate"); - $this->clickLink(t('Add translation')); - $this->assertNoText($body[0], t('The body field with delta @delta is hidden.', array('@delta' => 0))); - $this->assertText($body[1], t('The body field with delta @delta is shown.', array('@delta' => 1))); - } -} diff --git a/core/modules/text/lib/Drupal/text/TextProcessed.php b/core/modules/text/lib/Drupal/text/TextProcessed.php index 3a44722..eff563a 100644 --- a/core/modules/text/lib/Drupal/text/TextProcessed.php +++ b/core/modules/text/lib/Drupal/text/TextProcessed.php @@ -83,7 +83,9 @@ public function getValue($langcode = NULL) { */ public function setValue($value, $notify = TRUE) { if (isset($value)) { - throw new ReadOnlyException('Unable to set a computed property.'); + // @todo This is triggered from DatabaseStorageController::invokeFieldMethod() + // in the case of case of non-NG entity types. + // throw new ReadOnlyException('Unable to set a computed property.'); } } diff --git a/core/modules/text/lib/Drupal/text/Type/TextItem.php b/core/modules/text/lib/Drupal/text/Type/TextItem.php deleted file mode 100644 index b71812f..0000000 --- a/core/modules/text/lib/Drupal/text/Type/TextItem.php +++ /dev/null @@ -1,61 +0,0 @@ - 'string', - 'label' => t('Text value'), - ); - static::$propertyDefinitions['format'] = array( - 'type' => 'string', - 'label' => t('Text format'), - ); - static::$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 static::$propertyDefinitions; - } - - /** - * {@inheritdoc} - */ - public function isEmpty() { - $value = $this->get('value')->getValue(); - return $value === NULL || $value === ''; - } -} diff --git a/core/modules/text/lib/Drupal/text/Type/TextSummaryItem.php b/core/modules/text/lib/Drupal/text/Type/TextSummaryItem.php deleted file mode 100644 index 32a7444..0000000 --- a/core/modules/text/lib/Drupal/text/Type/TextSummaryItem.php +++ /dev/null @@ -1,58 +0,0 @@ - 'string', - 'label' => t('Summary text value'), - ); - static::$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 static::$propertyDefinitions; - } - - /** - * Overrides \Drupal\text\Type\TextItem::isEmpty(). - */ - public function isEmpty() { - $value = $this->get('summary')->getValue(); - return parent::isEmpty() && ($value === NULL || $value === ''); - } -} diff --git a/core/modules/text/text.module b/core/modules/text/text.module index 4bc0f30..3c239c0 100644 --- a/core/modules/text/text.module +++ b/core/modules/text/text.module @@ -41,164 +41,6 @@ function text_help($path, $arg) { } /** - * Implements hook_field_info(). - * - * Field settings: - * - max_length: The maximum length for a varchar field. - * Instance settings: - * - text_processing: Whether text input filters should be used. - * - display_summary: Whether the summary field should be displayed. When - * empty and not displayed the summary will take its value from the trimmed - * value of the main text field. - */ -function text_field_info() { - return array( - 'text' => array( - 'label' => t('Text'), - 'description' => t('This field stores varchar text in the database.'), - 'settings' => array('max_length' => 255), - 'instance_settings' => array('text_processing' => 0), - 'default_widget' => 'text_textfield', - 'default_formatter' => 'text_default', - 'field item class' => '\Drupal\text\Type\TextItem', - ), - 'text_long' => array( - 'label' => t('Long text'), - 'description' => t('This field stores long text in the database.'), - 'instance_settings' => array('text_processing' => 0), - 'default_widget' => 'text_textarea', - 'default_formatter' => 'text_default', - 'field item class' => '\Drupal\text\Type\TextItem', - ), - 'text_with_summary' => array( - 'label' => t('Long text and summary'), - 'description' => t('This field stores long text in the database along with optional summary text.'), - 'instance_settings' => array('text_processing' => 1, 'display_summary' => 0), - 'default_widget' => 'text_textarea_with_summary', - 'default_formatter' => 'text_default', - 'field item class' => '\Drupal\text\Type\TextSummaryItem', - ), - ); -} - -/** - * Implements hook_field_settings_form(). - */ -function text_field_settings_form($field, $instance) { - $settings = $field['settings']; - - $form = array(); - - if ($field['type'] == 'text') { - $form['max_length'] = array( - '#type' => 'number', - '#title' => t('Maximum length'), - '#default_value' => $settings['max_length'], - '#required' => TRUE, - '#description' => t('The maximum length of the field in characters.'), - '#min' => 1, - // @todo: If $has_data, add a validate handler that only allows - // max_length to increase. - '#disabled' => $field->hasData(), - ); - } - - return $form; -} - -/** - * Implements hook_field_instance_settings_form(). - */ -function text_field_instance_settings_form($field, $instance) { - $settings = $instance['settings']; - - $form['text_processing'] = array( - '#type' => 'radios', - '#title' => t('Text processing'), - '#default_value' => $settings['text_processing'], - '#options' => array( - t('Plain text'), - t('Filtered text (user selects text format)'), - ), - ); - if ($field['type'] == 'text_with_summary') { - $form['display_summary'] = array( - '#type' => 'checkbox', - '#title' => t('Summary input'), - '#default_value' => $settings['display_summary'], - '#description' => t('This allows authors to input an explicit summary, to be displayed instead of the automatically trimmed text when using the "Summary or trimmed" display type.'), - ); - } - - return $form; -} - -/** - * Implements hook_field_validate(). - * - * Possible error codes: - * - text_value_max_length: The value exceeds the maximum length. - * - text_summary_max_length: The summary exceeds the maximum length. - */ -function text_field_validate(EntityInterface $entity = NULL, $field, $instance, $langcode, $items, &$errors) { - foreach ($items as $delta => $item) { - // @todo Length is counted separately for summary and value, so the maximum - // length can be exceeded very easily. - foreach (array('value', 'summary') as $column) { - if (!empty($item[$column])) { - if (!empty($field['settings']['max_length']) && drupal_strlen($item[$column]) > $field['settings']['max_length']) { - switch ($column) { - case 'value': - $message = t('%name: the text may not be longer than %max characters.', array('%name' => $instance['label'], '%max' => $field['settings']['max_length'])); - break; - - case 'summary': - $message = t('%name: the summary may not be longer than %max characters.', array('%name' => $instance['label'], '%max' => $field['settings']['max_length'])); - break; - } - $errors[$field['field_name']][$langcode][$delta][] = array( - 'error' => "text_{$column}_length", - 'message' => $message, - ); - } - } - } - } -} - -/** - * Implements hook_field_load(). - * - * Where possible, the function generates the sanitized version of each field - * early so that it is cached in the field cache. This avoids the need to look - * up the field in the filter cache separately. - */ -function text_field_load($entity_type, $entities, $field, $instances, $langcode, &$items) { - foreach ($entities as $id => $entity) { - foreach ($items[$id] as $delta => $item) { - // Only process items with a cacheable format, the rest will be handled - // by formatters if needed. - if (empty($instances[$id]['settings']['text_processing']) || filter_format_allowcache($item['format'])) { - $items[$id][$delta]['safe_value'] = isset($item['value']) ? text_sanitize($instances[$id]['settings']['text_processing'], $langcode, $item, 'value') : ''; - if ($field['type'] == 'text_with_summary') { - $items[$id][$delta]['safe_summary'] = isset($item['summary']) ? text_sanitize($instances[$id]['settings']['text_processing'], $langcode, $item, 'summary') : ''; - } - } - } - } -} - -/** - * Implements hook_field_is_empty(). - */ -function text_field_is_empty($item, $field_type) { - if (!isset($item['value']) || $item['value'] === '') { - return !isset($item['summary']) || $item['summary'] === ''; - } - return FALSE; -} - -/** * Sanitizes the 'value' or 'summary' data of a text value. * * Depending on whether the field instance uses text processing, data is run @@ -217,11 +59,15 @@ function text_field_is_empty($item, $field_type) { * The sanitized string. */ function text_sanitize($text_processing, $langcode, $item, $column) { - // If the value uses a cacheable text format, text_field_load() precomputes - // the sanitized string. if (isset($item["safe_$column"])) { return $item["safe_$column"]; } + + // Optimize by opting out for the trivial 'empty string' case. + if ($item[$column] == '') { + return ''; + } + if ($text_processing) { return check_markup($item[$column], $item['format'], $langcode); } @@ -360,24 +206,6 @@ function text_summary($text, $format = NULL, $size = NULL) { } /** - * Implements hook_field_prepare_translation(). - */ -function text_field_prepare_translation(EntityInterface $entity, $field, $instance, $langcode, &$items, EntityInterface $source_entity, $source_langcode) { - // If the translating user is not permitted to use the assigned text format, - // we must not expose the source values. - $field_name = $field['field_name']; - if (!empty($source_entity->{$field_name}[$source_langcode])) { - $formats = filter_formats(); - foreach ($source_entity->{$field_name}[$source_langcode] as $delta => $item) { - $format_id = $item['format']; - if (!empty($format_id) && !filter_access($formats[$format_id])) { - unset($items[$delta]); - } - } - } -} - -/** * Implements hook_filter_format_update(). */ function text_filter_format_update($format) { diff --git a/core/modules/translation/lib/Drupal/translation/Tests/TranslationTest.php b/core/modules/translation/lib/Drupal/translation/Tests/TranslationTest.php index ff55606..dff34a0 100644 --- a/core/modules/translation/lib/Drupal/translation/Tests/TranslationTest.php +++ b/core/modules/translation/lib/Drupal/translation/Tests/TranslationTest.php @@ -380,7 +380,6 @@ function createTranslation(EntityInterface $node, $title, $body, $langcode) { $field_langcode = Language::LANGCODE_NOT_SPECIFIED; $body_key = "body[$field_langcode][0][value]"; $this->assertFieldByXPath('//input[@id="edit-title"]', $node->label(), "Original title value correctly populated."); - $this->assertFieldByXPath("//textarea[@name='$body_key']", $node->body[Language::LANGCODE_NOT_SPECIFIED][0]['value'], "Original body value correctly populated."); $edit = array(); $edit["title"] = $title; diff --git a/core/modules/translation/translation.module b/core/modules/translation/translation.module index 115be1a..e8e872e 100644 --- a/core/modules/translation/translation.module +++ b/core/modules/translation/translation.module @@ -341,10 +341,6 @@ function translation_node_prepare(EntityInterface $node) { $node->langcode = $langcode; $node->translation_source = $source_node; $node->title = $source_node->title; - - // Add field translations and let other modules module add custom translated - // fields. - field_attach_prepare_translation($node, $node->langcode, $source_node, $source_node->langcode); } } diff --git a/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationSettingsTest.php b/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationSettingsTest.php index cb90b6d..292d6e4 100644 --- a/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationSettingsTest.php +++ b/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationSettingsTest.php @@ -117,6 +117,7 @@ protected function assertSettings($entity_type, $bundle, $enabled, $edit) { $this->drupalPost('admin/config/regional/content-language', $edit, t('Save')); $args = array('@entity_type' => $entity_type, '@bundle' => $bundle, '@enabled' => $enabled ? 'enabled' : 'disabled'); $message = format_string('Translation for entity @entity_type (@bundle) is @enabled.', $args); + field_info_cache_clear(); entity_info_cache_clear(); return $this->assertEqual(translation_entity_enabled($entity_type, $bundle), $enabled, $message); } diff --git a/core/modules/translation_entity/translation_entity.admin.inc b/core/modules/translation_entity/translation_entity.admin.inc index d8cf685..9353150 100644 --- a/core/modules/translation_entity/translation_entity.admin.inc +++ b/core/modules/translation_entity/translation_entity.admin.inc @@ -589,20 +589,20 @@ function translation_entity_translatable_batch($translatable, $field_name, &$con */ function _translation_entity_update_field($entity_type, EntityInterface $entity, $field_name) { $empty = 0; - $field = field_info_field($field_name); + $translations = $entity->getTranslationLanguages(); // Ensure that we are trying to store only valid data. - foreach ($entity->{$field_name} as $langcode => $items) { - $entity->{$field_name}[$langcode] = _field_filter_items($field['type'], $entity->{$field_name}[$langcode]); - $empty += empty($entity->{$field_name}[$langcode]); + foreach (array_keys($translations) as $langcode) { + $items = $entity->getTranslation($langcode)->get($field_name); + $items->filterEmptyValues(); + $empty += $items->isEmpty(); } // Save the field value only if there is at least one item available, // otherwise any stored empty field value would be deleted. If this happens // the range queries would be messed up. - if ($empty < count($entity->{$field_name})) { - field_attach_presave($entity); - field_attach_update($entity); + if ($empty < count($translations)) { + $entity->save(); } } diff --git a/core/modules/translation_entity/translation_entity.module b/core/modules/translation_entity/translation_entity.module index 9f78d89..70dd2a9 100644 --- a/core/modules/translation_entity/translation_entity.module +++ b/core/modules/translation_entity/translation_entity.module @@ -862,10 +862,11 @@ function translation_entity_field_info_alter(&$info) { } /** - * Implements hook_field_attach_presave(). + * Implements hook_entity_presave(). */ -function translation_entity_field_attach_presave(EntityInterface $entity) { - if ($entity->isTranslatable()) { +function translation_entity_entity_presave(EntityInterface $entity) { + $entity_info = $entity->entityInfo(); + if ($entity->isTranslatable() && !empty($entity_info['fieldable'])) { $attributes = drupal_container()->get('request')->attributes; Drupal::service('translation_entity.synchronizer')->synchronizeFields($entity, $attributes->get('working_langcode'), $attributes->get('source_langcode')); }