Commit 7a6fb338 authored by alexpott's avatar alexpott
Browse files

Issue #2114707 by Berdir, yched, amateescu, effulgentsia, fago: Allow...

Issue #2114707 by Berdir, yched, amateescu, effulgentsia, fago: Allow per-bundle overrides of field definitions.
parent eeefed73
......@@ -213,9 +213,7 @@ public function preSaveRevision(EntityStorageControllerInterface $storage_contro
*/
public function getDataDefinition() {
$definition = EntityDataDefinition::create($this->getEntityTypeId());
if ($this->bundle() != $this->getEntityTypeId()) {
$definition->setBundles(array($this->bundle()));
}
$definition->setBundles(array($this->bundle()));
return $definition;
}
......@@ -485,8 +483,7 @@ public function getFieldDefinition($name) {
*/
public function getFieldDefinitions() {
if (!isset($this->fieldDefinitions)) {
$bundle = $this->bundle != $this->entityTypeId ? $this->bundle : NULL;
$this->fieldDefinitions = \Drupal::entityManager()->getFieldDefinitions($this->entityTypeId, $bundle);
$this->fieldDefinitions = \Drupal::entityManager()->getFieldDefinitions($this->entityTypeId, $this->bundle());
}
return $this->fieldDefinitions;
}
......@@ -987,4 +984,11 @@ public function referencedEntities() {
return $referenced_entities;
}
/**
* {@inheritdoc}
*/
public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
return array();
}
}
......@@ -40,7 +40,7 @@ interface ContentEntityInterface extends EntityInterface, RevisionableInterface,
public function initTranslation($langcode);
/**
* Defines the base fields of the entity type.
* Provides base field definitions for an entity type.
*
* Implementations typically use the class \Drupal\Core\Field\FieldDefinition
* for creating the field definitions; for example a 'name' field could be
......@@ -50,16 +50,47 @@ public function initTranslation($langcode);
* ->setLabel(t('Name'));
* @endcode
*
* @param string $entity_type
* The entity type to return properties for. Useful when a single class is
* used for multiple, possibly dynamic entity types.
* If some elements in a field definition need to vary by bundle, use
* \Drupal\Core\Entity\ContentEntityInterface::bundleFieldDefinitions().
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition. Useful when a single class is used for multiple,
* possibly dynamic entity types.
*
* @return \Drupal\Core\Field\FieldDefinitionInterface[]
* An array of base field definitions for the entity type, keyed by field
* name.
*
* @see \Drupal\Core\Entity\EntityManagerInterface::getFieldDefinitions()
* @see \Drupal\Core\Entity\ContentEntityInterface::bundleFieldDefinitions()
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type);
/**
* Provides or alters field definitions for a specific bundle.
*
* The field definitions returned here for the bundle take precedence on the
* base field definitions specified by baseFieldDefinitions() for the entity
* type.
*
* @todo Provide a better DX for field overrides.
* See https://drupal.org/node/2145115.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition. Useful when a single class is used for multiple,
* possibly dynamic entity types.
* @param string $bundle
* The bundle.
* @param \Drupal\Core\Field\FieldDefinitionInterface[] $base_field_definitions
* The list of base field definitions.
*
* @return \Drupal\Core\Field\FieldDefinitionInterface[]
* An array of entity field definitions, keyed by field name.
* An array of bundle field definitions, keyed by field name.
*
* @see \Drupal\Core\Entity\EntityManagerInterface::getFieldDefinitions()
* @see \Drupal\Core\Entity\ContentEntityInterface::baseFieldDefinitions()
*/
public static function baseFieldDefinitions($entity_type);
public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions);
/**
* Returns whether the entity has a field with the given name.
......
......@@ -78,13 +78,11 @@ class EntityManager extends PluginManagerBase implements EntityManagerInterface
protected $languageManager;
/**
* An array of field information per entity type, i.e. containing definitions.
* Static cache of base field definitions.
*
* @var array
*
* @see hook_entity_field_info()
*/
protected $entityFieldInfo;
protected $baseFieldDefinitions;
/**
* Static cache of field definitions per bundle and entity type.
......@@ -296,87 +294,140 @@ public function getAdminRouteInfo($entity_type_id, $bundle) {
/**
* {@inheritdoc}
*/
public function getFieldDefinitions($entity_type_id, $bundle = NULL) {
if (!isset($this->entityFieldInfo[$entity_type_id])) {
// First, try to load from cache.
$cid = 'entity_field_definitions:' . $entity_type_id . ':' . $this->languageManager->getCurrentLanguage()->id;
public function getBaseFieldDefinitions($entity_type_id) {
// Check the static cache.
if (!isset($this->baseFieldDefinitions[$entity_type_id])) {
// Not prepared, try to load from cache.
$cid = 'entity_base_field_definitions:' . $entity_type_id . ':' . $this->languageManager->getCurrentLanguage()->id;
if ($cache = $this->cache->get($cid)) {
$this->entityFieldInfo[$entity_type_id] = $cache->data;
$this->baseFieldDefinitions[$entity_type_id] = $cache->data;
}
else {
// @todo: Refactor to allow for per-bundle overrides.
// See https://drupal.org/node/2114707.
$entity_type = $this->getDefinition($entity_type_id);
$class = $entity_type->getClass();
$base_definitions = $class::baseFieldDefinitions($entity_type_id);
foreach ($base_definitions as &$base_definition) {
$base_definition->setTargetEntityTypeId($entity_type_id);
}
$this->entityFieldInfo[$entity_type_id] = array(
'definitions' => $base_definitions,
// Contains definitions of optional (per-bundle) fields.
'optional' => array(),
// An array keyed by bundle name containing the optional fields added
// by the bundle.
'bundle map' => array(),
);
// Invoke hooks.
$result = $this->moduleHandler->invokeAll($entity_type_id . '_field_info');
$this->entityFieldInfo[$entity_type_id] = NestedArray::mergeDeep($this->entityFieldInfo[$entity_type_id], $result);
$result = $this->moduleHandler->invokeAll('entity_field_info', array($entity_type_id));
$this->entityFieldInfo[$entity_type_id] = NestedArray::mergeDeep($this->entityFieldInfo[$entity_type_id], $result);
// Automatically set the field name for non-configurable fields.
foreach (array('definitions', 'optional') as $key) {
foreach ($this->entityFieldInfo[$entity_type_id][$key] as $field_name => &$definition) {
if ($definition instanceof FieldDefinition) {
$definition->setName($field_name);
}
}
}
// Rebuild the definitions and put it into the cache.
$this->baseFieldDefinitions[$entity_type_id] = $this->buildBaseFieldDefinitions($entity_type_id);
$this->cache->set($cid, $this->baseFieldDefinitions[$entity_type_id], Cache::PERMANENT, array('entity_types' => TRUE, 'entity_field_info' => TRUE));
}
}
return $this->baseFieldDefinitions[$entity_type_id];
}
// Invoke alter hooks.
$hooks = array('entity_field_info', $entity_type_id . '_field_info');
$this->moduleHandler->alter($hooks, $this->entityFieldInfo[$entity_type_id], $entity_type_id);
// Ensure all basic fields are not defined as translatable.
$keys = array_intersect_key(array_filter($entity_type->getKeys()), array_flip(array('id', 'revision', 'uuid', 'bundle')));
$untranslatable_fields = array_flip(array('langcode') + $keys);
foreach (array('definitions', 'optional') as $key) {
foreach ($this->entityFieldInfo[$entity_type_id][$key] as $field_name => &$definition) {
if (isset($untranslatable_fields[$field_name]) && $definition->isTranslatable()) {
throw new \LogicException(String::format('The @field field cannot be translatable.', array('@field' => $definition->getLabel())));
}
}
}
/**
* Builds base field definitions for an entity type.
*
* @param string $entity_type_id
* The entity type ID. Only entity types that implement
* \Drupal\Core\Entity\ContentEntityInterface are supported
*
* @return \Drupal\Core\Field\FieldDefinitionInterface[]
* An array of field definitions, keyed by field name.
*
* @throws \LogicException
* Thrown if one of the entity keys is flagged as translatable.
*/
protected function buildBaseFieldDefinitions($entity_type_id) {
$entity_type = $this->getDefinition($entity_type_id);
$class = $entity_type->getClass();
$base_field_definitions = $class::baseFieldDefinitions($entity_type);
// Invoke hook.
$result = $this->moduleHandler->invokeAll('entity_base_field_info', array($entity_type));
$base_field_definitions = NestedArray::mergeDeep($base_field_definitions, $result);
$this->cache->set($cid, $this->entityFieldInfo[$entity_type_id], Cache::PERMANENT, array('entity_types' => TRUE, 'entity_field_info' => TRUE));
// Automatically set the field name for non-configurable fields.
foreach ($base_field_definitions as $field_name => $base_field_definition) {
if ($base_field_definition instanceof FieldDefinition) {
$base_field_definition->setName($field_name);
$base_field_definition->setTargetEntityTypeId($entity_type_id);
}
}
if (!$bundle) {
return $this->entityFieldInfo[$entity_type_id]['definitions'];
// Invoke alter hook.
$this->moduleHandler->alter('entity_base_field_info', $base_field_definitions, $entity_type);
// Ensure all basic fields are not defined as translatable.
$keys = array_intersect_key(array_filter($entity_type->getKeys()), array_flip(array('id', 'revision', 'uuid', 'bundle')));
$untranslatable_fields = array_flip(array('langcode') + $keys);
foreach ($base_field_definitions as $field_name => $definition) {
if (isset($untranslatable_fields[$field_name]) && $definition->isTranslatable()) {
throw new \LogicException(String::format('The @field field cannot be translatable.', array('@field' => $definition->getLabel())));
}
}
else {
// Add in per-bundle fields.
if (!isset($this->fieldDefinitions[$entity_type_id][$bundle])) {
$this->fieldDefinitions[$entity_type_id][$bundle] = $this->entityFieldInfo[$entity_type_id]['definitions'];
if (isset($this->entityFieldInfo[$entity_type_id]['bundle map'][$bundle])) {
$this->fieldDefinitions[$entity_type_id][$bundle] += array_intersect_key($this->entityFieldInfo[$entity_type_id]['optional'], array_flip($this->entityFieldInfo[$entity_type_id]['bundle map'][$bundle]));
}
return $base_field_definitions;
}
/**
* {@inheritdoc}
*/
public function getFieldDefinitions($entity_type_id, $bundle) {
if (!isset($this->fieldDefinitions[$entity_type_id][$bundle])) {
$base_field_definitions = $this->getBaseFieldDefinitions($entity_type_id);
// Not prepared, try to load from cache.
$cid = 'entity_bundle_field_definitions:' . $entity_type_id . ':' . $bundle . ':' . $this->languageManager->getCurrentLanguage()->id;
if ($cache = $this->cache->get($cid)) {
$bundle_field_definitions = $cache->data;
}
else {
// Rebuild the definitions and put it into the cache.
$bundle_field_definitions = $this->buildBuildFieldDefinitions($entity_type_id, $bundle, $base_field_definitions);
$this->cache->set($cid, $bundle_field_definitions, Cache::PERMANENT, array('entity_types' => TRUE, 'entity_field_info' => TRUE));
}
// Field definitions consist of the bundle specific overrides and the
// base fields, merge them together. Use array_replace() to replace base
// fields with by bundle overrides and keep them in order, append
// additional by bundle fields.
$this->fieldDefinitions[$entity_type_id][$bundle] = array_replace($base_field_definitions, $bundle_field_definitions);
}
return $this->fieldDefinitions[$entity_type_id][$bundle];
}
/**
* Builds field definitions for a specific bundle within an entity type.
*
* @param string $entity_type_id
* The entity type ID. Only entity types that implement
* \Drupal\Core\Entity\ContentEntityInterface are supported.
* @param string $bundle
* The bundle.
* @param \Drupal\Core\Field\FieldDefinitionInterface[] $base_field_definitions
* The list of base field definitions.
*
* @return \Drupal\Core\Field\FieldDefinitionInterface[]
* An array of bundle field definitions, keyed by field name. Does
* not include base fields.
*/
protected function buildBuildFieldDefinitions($entity_type_id, $bundle, array $base_field_definitions) {
$entity_type = $this->getDefinition($entity_type_id);
$class = $entity_type->getClass();
// Allow the entity class to override the base fields.
$bundle_field_definitions = $class::bundleFieldDefinitions($entity_type, $bundle, $base_field_definitions);
// Invoke 'per bundle' hook.
$result = $this->moduleHandler->invokeAll('entity_bundle_field_info', array($entity_type, $bundle, $base_field_definitions));
$bundle_field_definitions = NestedArray::mergeDeep($bundle_field_definitions, $result);
// Automatically set the field name for non-configurable fields.
foreach ($bundle_field_definitions as $field_name => $field_definition) {
if ($field_definition instanceof FieldDefinition) {
$field_definition->setName($field_name);
$field_definition->setTargetEntityTypeId($entity_type_id);
}
return $this->fieldDefinitions[$entity_type_id][$bundle];
}
// Invoke 'per bundle' alter hook.
$this->moduleHandler->alter('entity_bundle_field_info', $bundle_field_definitions, $entity_type, $bundle);
return $bundle_field_definitions;
}
/**
* {@inheritdoc}
*/
public function clearCachedFieldDefinitions() {
unset($this->entityFieldInfo);
unset($this->fieldDefinitions);
$this->baseFieldDefinitions = array();
$this->fieldDefinitions = array();
Cache::deleteTags(array('entity_field_info' => TRUE));
}
......
......@@ -23,23 +23,38 @@ interface EntityManagerInterface extends PluginManagerInterface {
public function getEntityTypeLabels();
/**
* Gets an array of content entity field definitions.
* Gets the base field definitions for a content entity type.
*
* If a bundle is passed, fields specific to this bundle are included.
* Only fields that are not specific to a given bundle or set of bundles are
* returned. This excludes configurable fields, as they are always attached
* to a specific bundle.
*
* @param string $entity_type_id
* The entity type to get field definitions for. Only entity types that
* implement \Drupal\Core\Entity\ContentEntityInterface are supported.
* @param string $bundle
* (optional) The entity bundle for which to get field definitions. If NULL
* is passed, no bundle-specific fields are included. Defaults to NULL.
* The entity type ID. Only entity types that implement
* \Drupal\Core\Entity\ContentEntityInterface are supported.
*
* @return \Drupal\Core\Field\FieldDefinitionInterface[]
* An array of entity field definitions, keyed by field name.
* The array of base field definitions for the entity type, keyed by field
* name.
*
* @throws \LogicException
* Thrown if one of the entity keys is flagged as translatable.
*/
public function getBaseFieldDefinitions($entity_type_id);
/**
* Gets the field definitions for a specific bundle.
*
* @see \Drupal\Core\TypedData\TypedDataManager::create()
* @param string $entity_type_id
* The entity type ID. Only entity types that implement
* \Drupal\Core\Entity\ContentEntityInterface are supported.
* @param string $bundle
* The bundle.
*
* @return \Drupal\Core\Field\FieldDefinitionInterface[]
* The array of field definitions for the bundle, keyed by field name.
*/
public function getFieldDefinitions($entity_type_id, $bundle = NULL);
public function getFieldDefinitions($entity_type_id, $bundle);
/**
* Creates a new access controller instance.
......
......@@ -268,7 +268,7 @@ protected function attachPropertyData(array &$entities) {
}
$data = $query->execute();
$field_definitions = \Drupal::entityManager()->getFieldDefinitions($this->entityTypeId);
$field_definitions = \Drupal::entityManager()->getBaseFieldDefinitions($this->entityTypeId);
$translations = array();
if ($this->revisionDataTable) {
$data_column_names = array_flip(array_diff(drupal_schema_fields_sql($this->entityType->getRevisionDataTable()), drupal_schema_fields_sql($this->entityType->getBaseTable())));
......@@ -1187,7 +1187,7 @@ public static function _fieldSqlSchema(FieldConfigInterface $field, array $schem
$entity_type_id = $field->entity_type;
$entity_manager = \Drupal::entityManager();
$entity_type = $entity_manager->getDefinition($entity_type_id);
$definitions = $entity_manager->getFieldDefinitions($entity_type_id);
$definitions = $entity_manager->getBaseFieldDefinitions($entity_type_id);
// Define the entity ID schema based on the field definitions.
$id_definition = $definitions[$entity_type->getKey('id')];
......
......@@ -60,8 +60,12 @@ public function getPropertyDefinitions() {
// @todo: Add support for handling multiple bundles.
// See https://drupal.org/node/2169813.
$bundles = $this->getBundles();
$bundle = is_array($bundles) && count($bundles) == 1 ? reset($bundles) : NULL;
$this->propertyDefinitions = \Drupal::entityManager()->getFieldDefinitions($entity_type_id, $bundle);
if (is_array($bundles) && count($bundles) == 1) {
$this->propertyDefinitions = \Drupal::entityManager()->getFieldDefinitions($entity_type_id, reset($bundles));
}
else {
$this->propertyDefinitions = \Drupal::entityManager()->getBaseFieldDefinitions($entity_type_id);
}
}
else {
// No entity type given.
......
......@@ -7,60 +7,16 @@
namespace Drupal\Core\Field;
use Drupal\field\FieldInstanceConfigInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\field\Field;
/**
* Represents a configurable entity field item list.
*/
class ConfigFieldItemList extends FieldItemList implements ConfigFieldItemListInterface {
/**
* The Field instance definition.
*
* @var \Drupal\field\FieldInstanceConfigInterface
*/
protected $instance;
/**
* {@inheritdoc}
*/
public function __construct($definition, $name = NULL, TypedDataInterface $parent = NULL) {
parent::__construct($definition, $name, $parent);
// Definition can be the field config or field instance.
if ($definition instanceof FieldInstanceConfigInterface) {
$this->instance = $definition;
}
}
/**
* {@inheritdoc}
*/
public function getFieldDefinition() {
// Configurable fields have the field_config entity injected as definition,
// but we want to return the more specific field instance here.
// @todo: Overhaul this once we have per-bundle field definitions injected,
// see https://drupal.org/node/2114707.
if (!isset($this->instance)) {
$entity = $this->getEntity();
$instances = Field::fieldInfo()->getBundleInstances($entity->getEntityTypeId(), $entity->bundle());
if (isset($instances[$this->getName()])) {
$this->instance = $instances[$this->getName()];
}
else {
// For base fields, fall back to return the general definition.
return parent::getFieldDefinition();
}
}
return $this->instance;
}
/**
* {@inheritdoc}
*/
public function getConstraints() {
$constraints = array();
$constraints = parent::getConstraints();
// Check that the number of values doesn't exceed the field cardinality. For
// form submitted values, this can only happen with 'multiple value'
// widgets.
......
......@@ -437,4 +437,13 @@ public function getConstraints(DataDefinitionInterface $definition) {
return $constraints;
}
/**
* {@inheritdoc}
*/
public function clearCachedDefinitions() {
parent::clearCachedDefinitions();
$this->prototypes = array();
}
}
......@@ -8,6 +8,7 @@
namespace Drupal\aggregator\Entity;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldDefinition;
use Symfony\Component\DependencyInjection\Container;
use Drupal\Core\Entity\EntityStorageControllerInterface;
......@@ -120,7 +121,7 @@ public static function postDelete(EntityStorageControllerInterface $storage_cont
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions($entity_type) {
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields['fid'] = FieldDefinition::create('integer')
->setLabel(t('Feed ID'))
->setDescription(t('The ID of the aggregator feed.'))
......
......@@ -10,6 +10,7 @@
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\aggregator\ItemInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldDefinition;
/**
......@@ -60,7 +61,7 @@ public function postCreate(EntityStorageControllerInterface $storage_controller)
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions($entity_type) {
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields['iid'] = FieldDefinition::create('integer')
->setLabel(t('Aggregator item ID'))
->setDescription(t('The ID of the feed item.'))
......
......@@ -9,6 +9,7 @@
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldDefinition;
use Drupal\custom_block\CustomBlockInterface;
......@@ -163,7 +164,7 @@ public function delete() {
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions($entity_type) {
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields['id'] = FieldDefinition::create('integer')
->setLabel(t('Custom block ID'))
->setDescription(t('The custom block ID.'))
......
......@@ -11,6 +11,7 @@
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\comment\CommentInterface;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldDefinition;
use Drupal\Core\Language\Language;
use Drupal\Core\TypedData\DataDefinition;
......@@ -210,7 +211,7 @@ public function permalink() {
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions($entity_type) {
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields['cid'] = FieldDefinition::create('integer')
->setLabel(t('Comment ID'))
->setDescription(t('The comment ID.'))
......
......@@ -69,6 +69,7 @@ public function testValidation() {
'entity_id' => $node->id(),
'entity_type' => 'node',
'field_name' => 'comment',
'comment_body' => $this->randomName(),
));
$violations = $comment->validate();
......
......@@ -9,6 +9,7 @@
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\contact\MessageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldDefinition;
/**
......@@ -139,7 +140,7 @@ public function getPersonalRecipient() {
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions($entity_type) {
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields['category'] = FieldDefinition::create('entity_reference')
->setLabel(t('Category ID'))
->setDescription(t('The ID of the associated category.'))
......
......@@ -9,6 +9,8 @@
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityFormControllerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldDefinition;
use Drupal\Core\Language\Language;