Commit 1aeee1cd authored by catch's avatar catch

Issue #2004244 by Berdir, plach, tim.plunkett, msonnabaum, das-peter: Move...

Issue #2004244 by Berdir, plach, tim.plunkett, msonnabaum, das-peter: Move entity revision, content translation, validation and field methods to ContentEntityInterface.
parent 6976d52a
......@@ -535,52 +535,6 @@ function entity_get_form(EntityInterface $entity, $operation = 'default', array
return \Drupal::entityManager()->getForm($entity, $operation, $form_state);
}
/**
* Copies submitted values to entity properties for simple entity forms.
*
* During the submission handling of an entity form's "Save", "Preview", and
* possibly other buttons, the form state's entity needs to be updated with the
* submitted form values. Each entity form implements its own builder function
* for doing this, appropriate for the particular entity and form, whereas
* modules may specify additional builder functions in $form['#entity_builders']
* for copying the form values of added form elements to entity properties.
* Many of the main entity builder functions can call this helper function to
* re-use its logic of copying $form_state['values'][PROPERTY] values to
* $entity->PROPERTY for all entries in $form_state['values'] that are not
* field data, and calling field_attach_extract_form_values() to copy field
* data. Apart from that this helper invokes any additional builder functions
* that have been specified in $form['#entity_builders'].
*
* For some entity forms (e.g., forms with complex non-field data and forms that
* simultaneously edit multiple entities), this behavior may be inappropriate,
* so the builder function for such forms needs to implement the required
* functionality instead of calling this function.
*/
function entity_form_submit_build_entity($entity_type, $entity, $form, &$form_state, array $options = array()) {
$info = entity_get_info($entity_type);
// Copy top-level form values that are not for fields to entity properties,
// without changing existing entity properties that are not being edited by
// this form. Copying field values must be done using
// field_attach_extract_form_values().
$values_excluding_fields = $info['fieldable'] ? array_diff_key($form_state['values'], field_info_instances($entity_type, $entity->bundle())) : $form_state['values'];
foreach ($values_excluding_fields as $key => $value) {
$entity->set($key, $value);
}
// Invoke all specified builders for copying form values to entity properties.
if (isset($form['#entity_builders'])) {
foreach ($form['#entity_builders'] as $function) {
call_user_func_array($function, array($entity_type, $entity, &$form, &$form_state));
}
}
// Copy field values to the entity.
if ($info['fieldable']) {
field_attach_extract_form_values($entity, $form, $form_state, $options);
}
}
/**
* Returns an entity list controller for a given entity type.
*
......
......@@ -74,24 +74,16 @@ public function isNew() {
}
/**
* Overrides Entity::get().
*
* EntityInterface::get() implements support for fieldable entities, but
* configuration entities are not fieldable.
* {@inheritdoc}
*/
public function get($property_name, $langcode = NULL) {
// @todo: Add support for translatable properties being not fields.
public function get($property_name) {
return isset($this->{$property_name}) ? $this->{$property_name} : NULL;
}
/**
* Overrides Entity::set().
*
* EntityInterface::set() implements support for fieldable entities, but
* configuration entities are not fieldable.
* {@inheritdoc}
*/
public function set($property_name, $value, $langcode = NULL, $notify = TRUE) {
// @todo: Add support for translatable properties being not fields.
public function set($property_name, $value) {
$this->{$property_name} = $value;
}
......@@ -149,7 +141,7 @@ public static function sort($a, $b) {
}
/**
* Overrides \Drupal\Core\Entity\Entity::getExportProperties().
* {@inheritdoc}
*/
public function getExportProperties() {
// Configuration objects do not have a schema. Extract all key names from
......
......@@ -75,6 +75,27 @@ public function setStatus($status);
*/
public function status();
/**
* Returns the value of a property.
*
* @param string $property_name
* The name of the property that should be returned.
*
* @return mixed
* The property, if existing, NULL otherwise.
*/
public function get($property_name);
/**
* Sets the value of a property.
*
* @param string $property_name
* The name of the property that should be set.
* @param mixed $value
* The value the property should be set to.
*/
public function set($property_name, $value);
/**
* Retrieves the exportable properties of the entity.
*
......
......@@ -2,27 +2,20 @@
/**
* @file
* Contains \Drupal\Core\Entity\EntityNG.
* Contains \Drupal\Core\Entity\ContentEntityBase.
*/
namespace Drupal\Core\Entity;
use Drupal\Core\Entity\Plugin\DataType\EntityReference;
use Drupal\Core\Language\Language;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\TypedDataInterface;
/**
* Implements Entity Field API specific enhancements to the Entity class.
*
* Entity(..)NG classes are variants of the Entity(...) classes that implement
* the next generation (NG) entity field API. They exist during conversion to
* the new API only and changes will be merged into the respective original
* classes once the conversion is complete.
*
* @todo: Once all entity types have been converted, merge improvements into the
* Entity class and overhaul the EntityInterface.
*/
class EntityNG extends Entity {
abstract class ContentEntityBase extends Entity implements \IteratorAggregate, ContentEntityInterface {
/**
* Status code indentifying a removed translation.
......@@ -85,7 +78,7 @@ class EntityNG extends Entity {
/**
* Local cache for field definitions.
*
* @see EntityNG::getPropertyDefinitions()
* @see ContentEntityBase::getPropertyDefinitions()
*
* @var array
*/
......@@ -126,6 +119,20 @@ class EntityNG extends Entity {
*/
protected $translationInitialize = FALSE;
/**
* Boolean indicating whether a new revision should be created on save.
*
* @var bool
*/
protected $newRevision = FALSE;
/**
* Indicates whether this is the default revision.
*
* @var bool
*/
protected $isDefaultRevision = TRUE;
/**
* Overrides Entity::__construct().
*/
......@@ -159,6 +166,154 @@ public function __construct(array $values, $entity_type, $bundle = FALSE, $trans
$this->init();
}
/**
* {@inheritdoc}
*/
public function setNewRevision($value = TRUE) {
$this->newRevision = $value;
}
/**
* {@inheritdoc}
*/
public function isNewRevision() {
$info = $this->entityInfo();
return $this->newRevision || (!empty($info['entity_keys']['revision']) && !$this->getRevisionId());
}
/**
* {@inheritdoc}
*/
public function isDefaultRevision($new_value = NULL) {
$return = $this->isDefaultRevision;
if (isset($new_value)) {
$this->isDefaultRevision = (bool) $new_value;
}
return $return;
}
/**
* {@inheritdoc}
*/
public function getRevisionId() {
return NULL;
}
/**
* {@inheritdoc}
*/
public function isTranslatable() {
// @todo Inject the entity manager and retrieve bundle info from it.
$bundles = entity_get_bundles($this->entityType);
return !empty($bundles[$this->bundle()]['translatable']);
}
/**
* {@inheritdoc}
*/
public function preSaveRevision(EntityStorageControllerInterface $storage_controller, \stdClass $record) {
}
/**
* {@inheritdoc}
*/
public function getDefinition() {
// @todo: This does not make much sense, so remove once TypedDataInterface
// is removed. See https://drupal.org/node/2002138.
if ($this->bundle() != $this->entityType()) {
$type = 'entity:' . $this->entityType() . ':' . $this->bundle();
}
else {
$type = 'entity:' . $this->entityType();
}
return array('type' => $type);
}
/**
* {@inheritdoc}
*/
public function getValue() {
// @todo: This does not make much sense, so remove once TypedDataInterface
// is removed. See https://drupal.org/node/2002138.
return $this->getPropertyValues();
}
/**
* {@inheritdoc}
*/
public function setValue($value, $notify = TRUE) {
// @todo: This does not make much sense, so remove once TypedDataInterface
// is removed. See https://drupal.org/node/2002138.
$this->setPropertyValues($value);
}
/**
* {@inheritdoc}
*/
public function getString() {
return $this->label();
}
/**
* {@inheritdoc}
*/
public function validate() {
// @todo: Add the typed data manager as proper dependency.
return \Drupal::typedData()->getValidator()->validate($this);
}
/**
* {@inheritdoc}
*/
public function applyDefaultValue($notify = TRUE) {
foreach ($this->getProperties() as $property) {
$property->applyDefaultValue(FALSE);
}
}
/**
* {@inheritdoc}
*/
public function getConstraints() {
return array();
}
/**
* {@inheritdoc}
*/
public function getName() {
return NULL;
}
/**
* {@inheritdoc}
*/
public function getRoot() {
return $this;
}
/**
* {@inheritdoc}
*/
public function getPropertyPath() {
return '';
}
/**
* {@inheritdoc}
*/
public function getParent() {
return NULL;
}
/**
* {@inheritdoc}
*/
public function setContext($name = NULL, TypedDataInterface $parent = NULL) {
// As entities are always the root of the tree of typed data, we do not need
// to set any parent or name.
}
/**
* Initialize the object. Invoked upon construction and wake up.
*/
......@@ -188,14 +343,14 @@ public function __wakeup() {
}
/**
* Implements \Drupal\Core\Entity\EntityInterface::id().
* {@inheritdoc}
*/
public function id() {
return $this->id->value;
}
/**
* Implements \Drupal\Core\Entity\EntityInterface::bundle().
* {@inheritdoc}
*/
public function bundle() {
return $this->bundle;
......@@ -209,7 +364,7 @@ public function uuid() {
}
/**
* Implements \Drupal\Core\TypedData\ComplexDataInterface::get().
* {@inheritdoc}
*/
public function get($property_name) {
if (!isset($this->fields[$property_name][$this->activeLangcode])) {
......@@ -254,14 +409,14 @@ protected function getTranslatedField($property_name, $langcode) {
}
/**
* Implements \Drupal\Core\TypedData\ComplexDataInterface::set().
* {@inheritdoc}
*/
public function set($property_name, $value, $notify = TRUE) {
$this->get($property_name)->setValue($value, FALSE);
}
/**
* Implements \Drupal\Core\TypedData\ComplexDataInterface::getProperties().
* {@inheritdoc}
*/
public function getProperties($include_computed = FALSE) {
$properties = array();
......@@ -274,14 +429,14 @@ public function getProperties($include_computed = FALSE) {
}
/**
* Implements \IteratorAggregate::getIterator().
* {@inheritdoc}
*/
public function getIterator() {
return new \ArrayIterator($this->getProperties());
}
/**
* Implements \Drupal\Core\TypedData\ComplexDataInterface::getPropertyDefinition().
* {@inheritdoc}
*/
public function getPropertyDefinition($name) {
if (!isset($this->fieldDefinitions)) {
......@@ -296,7 +451,7 @@ public function getPropertyDefinition($name) {
}
/**
* Implements \Drupal\Core\TypedData\ComplexDataInterface::getPropertyDefinitions().
* {@inheritdoc}
*/
public function getPropertyDefinitions() {
if (!isset($this->fieldDefinitions)) {
......@@ -307,7 +462,7 @@ public function getPropertyDefinitions() {
}
/**
* Implements \Drupal\Core\TypedData\ComplexDataInterface::getPropertyValues().
* {@inheritdoc}
*/
public function getPropertyValues() {
$values = array();
......@@ -318,7 +473,7 @@ public function getPropertyValues() {
}
/**
* Implements \Drupal\Core\TypedData\ComplexDataInterface::setPropertyValues().
* {@inheritdoc}
*/
public function setPropertyValues($values) {
foreach ($values as $name => $value) {
......@@ -327,7 +482,7 @@ public function setPropertyValues($values) {
}
/**
* Implements \Drupal\Core\TypedData\ComplexDataInterface::isEmpty().
* {@inheritdoc}
*/
public function isEmpty() {
if (!$this->isNew()) {
......@@ -378,7 +533,7 @@ public function language() {
*/
protected function getDefaultLanguage() {
// Keep a local cache of the language object and clear it if the langcode
// gets changed, see EntityNG::onChange().
// gets changed, see ContentEntityBase::onChange().
if (!isset($this->language)) {
// Get the language code if the property exists.
if ($this->getPropertyDefinition('langcode') && ($item = $this->get('langcode')) && isset($item->language)) {
......@@ -404,7 +559,7 @@ public function onChange($property_name) {
}
/**
* Implements \Drupal\Core\TypedData\TranslatableInterface::getTranslation().
* {@inheritdoc}
*
* @return \Drupal\Core\Entity\EntityInterface
*/
......@@ -768,4 +923,25 @@ public function label($langcode = NULL) {
return $label;
}
/**
* {@inheritdoc}
*/
public function referencedEntities() {
$referenced_entities = array();
// Gather a list of referenced entities.
foreach ($this->getProperties() as $field_items) {
foreach ($field_items as $field_item) {
// Loop over all properties of a field item.
foreach ($field_item->getProperties(TRUE) as $property) {
if ($property instanceof EntityReference && $entity = $property->getTarget()) {
$referenced_entities[] = $entity;
}
}
}
}
return $referenced_entities;
}
}
......@@ -2,7 +2,7 @@
/**
* @file
* Contains \Drupal\Core\Entity\EntityNGConfirmFormBase.
* Contains \Drupal\Core\Entity\ContentEntityConfirmFormBase.
*/
namespace Drupal\Core\Entity;
......@@ -13,7 +13,7 @@
/**
* Provides a generic base class for an entity-based confirmation form.
*/
abstract class EntityNGConfirmFormBase extends EntityFormControllerNG implements ConfirmFormInterface {
abstract class ContentEntityConfirmFormBase extends ContentEntityFormController implements ConfirmFormInterface {
/**
* {@inheritdoc}
......
......@@ -2,25 +2,22 @@
/**
* @file
* Contains \Drupal\Core\Entity\EntityFormControllerNG.
* Contains \Drupal\Core\Entity\ContentEntityFormController.
*/
namespace Drupal\Core\Entity;
use Drupal\Core\Language\Language;
/**
* Entity form controller variant for entity types using the new property API.
*
* @todo: Merge with EntityFormController and overhaul once all entity types
* are converted to the new entity field API.
*
* See the EntityNG documentation for an explanation of "NG".
* Entity form controller variant for content entity types.
*
* @see \Drupal\Core\EntityNG
* @see \Drupal\Core\ContentEntityBase
*/
class EntityFormControllerNG extends EntityFormController {
class ContentEntityFormController extends EntityFormController {
/**
* Overrides EntityFormController::form().
* {@inheritdoc}
*/
public function form(array $form, array &$form_state) {
$entity = $this->entity;
......@@ -38,7 +35,103 @@ public function form(array $form, array &$form_state) {
}
/**
* Overrides EntityFormController::buildEntity().
* {@inheritdoc}
*/
public function validate(array $form, array &$form_state) {
$entity = $this->buildEntity($form, $form_state);
$entity_type = $entity->entityType();
$entity_langcode = $entity->language()->id;
$violations = array();
foreach ($entity as $field_name => $field) {
$field_violations = $field->validate();
if (count($field_violations)) {
$violations[$field_name] = $field_violations;
}
}
// Map errors back to form elements.
if ($violations) {
foreach ($violations as $field_name => $field_violations) {
$langcode = field_is_translatable($entity_type, field_info_field($entity_type, $field_name)) ? $entity_langcode : Language::LANGCODE_NOT_SPECIFIED;
$field_state = field_form_get_state($form['#parents'], $field_name, $form_state);
$field_state['constraint_violations'] = $field_violations;
field_form_set_state($form['#parents'], $field_name, $form_state, $field_state);
}
field_invoke_method('flagErrors', _field_invoke_widget_target($form_state['form_display']), $entity, $form, $form_state);
}
// @todo Remove this.
// Execute legacy global validation handlers.
unset($form_state['validate_handlers']);
form_execute_handlers('validate', $form, $form_state);
}
/**
* Initialize the form state and the entity before the first form build.
*/
protected function init(array &$form_state) {
// Ensure we act on the translation object corresponding to the current form
// language.
$this->entity = $this->getTranslatedEntity($form_state);
parent::init($form_state);
}
/**
* Returns the translation object corresponding to the form language.
*
* @param array $form_state
* A keyed array containing the current state of the form.
*/
protected function getTranslatedEntity(array $form_state) {
$langcode = $this->getFormLangcode($form_state);
$translation = $this->entity->getTranslation($langcode);
// Ensure that the entity object is a BC entity if the original one is.
return $this->entity instanceof EntityBCDecorator ? $translation->getBCEntity() : $translation;
}
/**
* {@inheritdoc}
*/
public function getFormLangcode(array $form_state) {
$entity = $this->entity;
if (!empty($form_state['langcode'])) {
$langcode = $form_state['langcode'];
}
else {
// If no form langcode was provided we default to the current content
// language and inspect existing translations to find a valid fallback,
// if any.
$translations = $entity->getTranslationLanguages();
$languageManager = \Drupal::languageManager();
$langcode = $languageManager->getLanguage(Language::TYPE_CONTENT)->id;
$fallback = $languageManager->isMultilingual() ? language_fallback_get_candidates() : array();
while (!empty($langcode) && !isset($translations[$langcode])) {
$langcode = array_shift($fallback);
}
}
// If the site is not multilingual or no translation for the given form
// language is available, fall back to the entity language.
if (!empty($langcode)) {
return $langcode;
}
else {
// If the entity is translatable, return the original language.
return $entity->getUntranslated()->language()->id;
}
}
/**
* {@inheritdoc}
*/
public function isDefaultFormLangcode(array $form_state) {
return $this->getFormLangcode($form_state) == $this->entity->getUntranslated()->language()->id;
}
/**
* {@inheritdoc}
*/
public function buildEntity(array $form, array &$form_state) {
$entity = clone $this->entity;
......
......@@ -7,9 +7,52 @@
namespace Drupal\Core\Entity;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\TranslatableInterface;
/**
* Defines a common interface for all content entity objects.
*
* This interface builds upon the general interfaces provided by the typed data
* API, while extending them with content entity-specific additions. I.e., a
* content entity implements the ComplexDataInterface among others, thus is
* complex data containing fields as its data properties. The contained fields
* have to implement \Drupal\Core\Entity\Field\FieldItemListInterface,
* which builds upon typed data interfaces as well.
*
* When implementing this interface which extends Traversable, make sure to list
* IteratorAggregate or Iterator before this interface in the implements clause.
*
* @see \Drupal\Core\TypedData\TypedDataManager
* @see \Drupal\Core\Entity\Field\FieldItemListInterface
*/
interface ContentEntityInterface extends EntityInterface {
interface ContentEntityInterface extends EntityInterface, RevisionableInterface, TranslatableInterface, ComplexDataInterface {
/**
* Marks the translation identified by the given language code as existing.
*
* @param string $langcode
* The language code identifying the translation to be initialized.
*
* @todo Remove this as soon as translation metadata have been converted to
* regular fields.
*/
public function initTranslation($langcode);
/**
* Defines the base fields of the entity type.
*
* @param string $entity_type
* The entity type to return properties for. Useful when a single class is
* used for multiple, possibly dynamic entity types.
*
* @return array
* An array of entity field definitions as specified by
* \Drupal\Core\Entity\EntityManager::getFieldDefinitions(), keyed by field
* name.
*
* @see \Drupal\Core\Entity\EntityManager::getFieldDefinitions()
*/
public static function baseFieldDefinitions($entity_type);
}
......@@ -22,9 +22,7 @@
* @todo: Once all entity types have been converted, merge improvements into the
* DatabaseStorageController class.
*
* See the EntityNG documentation for an explanation of "NG".
*
* @see \Drupal\Core\EntityNG
* @see \Drupal\Core\ContentEntityBase
*/
class DatabaseStorageControllerNG extends DatabaseStorageController {
......