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 ...@@ -213,9 +213,7 @@ public function preSaveRevision(EntityStorageControllerInterface $storage_contro
*/ */
public function getDataDefinition() { public function getDataDefinition() {
$definition = EntityDataDefinition::create($this->getEntityTypeId()); $definition = EntityDataDefinition::create($this->getEntityTypeId());
if ($this->bundle() != $this->getEntityTypeId()) { $definition->setBundles(array($this->bundle()));
$definition->setBundles(array($this->bundle()));
}
return $definition; return $definition;
} }
...@@ -485,8 +483,7 @@ public function getFieldDefinition($name) { ...@@ -485,8 +483,7 @@ public function getFieldDefinition($name) {
*/ */
public function getFieldDefinitions() { public function getFieldDefinitions() {
if (!isset($this->fieldDefinitions)) { if (!isset($this->fieldDefinitions)) {
$bundle = $this->bundle != $this->entityTypeId ? $this->bundle : NULL; $this->fieldDefinitions = \Drupal::entityManager()->getFieldDefinitions($this->entityTypeId, $this->bundle());
$this->fieldDefinitions = \Drupal::entityManager()->getFieldDefinitions($this->entityTypeId, $bundle);
} }
return $this->fieldDefinitions; return $this->fieldDefinitions;
} }
...@@ -987,4 +984,11 @@ public function referencedEntities() { ...@@ -987,4 +984,11 @@ public function referencedEntities() {
return $referenced_entities; 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, ...@@ -40,7 +40,7 @@ interface ContentEntityInterface extends EntityInterface, RevisionableInterface,
public function initTranslation($langcode); 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 * Implementations typically use the class \Drupal\Core\Field\FieldDefinition
* for creating the field definitions; for example a 'name' field could be * for creating the field definitions; for example a 'name' field could be
...@@ -50,16 +50,47 @@ public function initTranslation($langcode); ...@@ -50,16 +50,47 @@ public function initTranslation($langcode);
* ->setLabel(t('Name')); * ->setLabel(t('Name'));
* @endcode * @endcode
* *
* @param string $entity_type * If some elements in a field definition need to vary by bundle, use
* The entity type to return properties for. Useful when a single class is * \Drupal\Core\Entity\ContentEntityInterface::bundleFieldDefinitions().
* used for multiple, possibly dynamic entity types. *
* @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[] * @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\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. * Returns whether the entity has a field with the given name.
......
...@@ -78,13 +78,11 @@ class EntityManager extends PluginManagerBase implements EntityManagerInterface ...@@ -78,13 +78,11 @@ class EntityManager extends PluginManagerBase implements EntityManagerInterface
protected $languageManager; protected $languageManager;
/** /**
* An array of field information per entity type, i.e. containing definitions. * Static cache of base field definitions.
* *
* @var array * @var array
*
* @see hook_entity_field_info()
*/ */
protected $entityFieldInfo; protected $baseFieldDefinitions;
/** /**
* Static cache of field definitions per bundle and entity type. * Static cache of field definitions per bundle and entity type.
...@@ -296,87 +294,140 @@ public function getAdminRouteInfo($entity_type_id, $bundle) { ...@@ -296,87 +294,140 @@ public function getAdminRouteInfo($entity_type_id, $bundle) {
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function getFieldDefinitions($entity_type_id, $bundle = NULL) { public function getBaseFieldDefinitions($entity_type_id) {
if (!isset($this->entityFieldInfo[$entity_type_id])) { // Check the static cache.
// First, try to load from cache. if (!isset($this->baseFieldDefinitions[$entity_type_id])) {
$cid = 'entity_field_definitions:' . $entity_type_id . ':' . $this->languageManager->getCurrentLanguage()->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)) { if ($cache = $this->cache->get($cid)) {
$this->entityFieldInfo[$entity_type_id] = $cache->data; $this->baseFieldDefinitions[$entity_type_id] = $cache->data;
} }
else { else {
// @todo: Refactor to allow for per-bundle overrides. // Rebuild the definitions and put it into the cache.
// See https://drupal.org/node/2114707. $this->baseFieldDefinitions[$entity_type_id] = $this->buildBaseFieldDefinitions($entity_type_id);
$entity_type = $this->getDefinition($entity_type_id); $this->cache->set($cid, $this->baseFieldDefinitions[$entity_type_id], Cache::PERMANENT, array('entity_types' => TRUE, 'entity_field_info' => TRUE));
$class = $entity_type->getClass(); }
}
$base_definitions = $class::baseFieldDefinitions($entity_type_id); return $this->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);
}
}
}
// Invoke alter hooks. /**
$hooks = array('entity_field_info', $entity_type_id . '_field_info'); * Builds base field definitions for an entity type.
$this->moduleHandler->alter($hooks, $this->entityFieldInfo[$entity_type_id], $entity_type_id); *
* @param string $entity_type_id
// Ensure all basic fields are not defined as translatable. * The entity type ID. Only entity types that implement
$keys = array_intersect_key(array_filter($entity_type->getKeys()), array_flip(array('id', 'revision', 'uuid', 'bundle'))); * \Drupal\Core\Entity\ContentEntityInterface are supported
$untranslatable_fields = array_flip(array('langcode') + $keys); *
foreach (array('definitions', 'optional') as $key) { * @return \Drupal\Core\Field\FieldDefinitionInterface[]
foreach ($this->entityFieldInfo[$entity_type_id][$key] as $field_name => &$definition) { * An array of field definitions, keyed by field name.
if (isset($untranslatable_fields[$field_name]) && $definition->isTranslatable()) { *
throw new \LogicException(String::format('The @field field cannot be translatable.', array('@field' => $definition->getLabel()))); * @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) { // Invoke alter hook.
return $this->entityFieldInfo[$entity_type_id]['definitions']; $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. return $base_field_definitions;
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])); * {@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} * {@inheritdoc}
*/ */
public function clearCachedFieldDefinitions() { public function clearCachedFieldDefinitions() {
unset($this->entityFieldInfo); $this->baseFieldDefinitions = array();
unset($this->fieldDefinitions); $this->fieldDefinitions = array();
Cache::deleteTags(array('entity_field_info' => TRUE)); Cache::deleteTags(array('entity_field_info' => TRUE));
} }
......
...@@ -23,23 +23,38 @@ interface EntityManagerInterface extends PluginManagerInterface { ...@@ -23,23 +23,38 @@ interface EntityManagerInterface extends PluginManagerInterface {
public function getEntityTypeLabels(); 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 * @param string $entity_type_id
* The entity type to get field definitions for. Only entity types that * The entity type ID. Only entity types that implement
* implement \Drupal\Core\Entity\ContentEntityInterface are supported. * \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.
* *
* @return \Drupal\Core\Field\FieldDefinitionInterface[] * @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. * Creates a new access controller instance.
......
...@@ -268,7 +268,7 @@ protected function attachPropertyData(array &$entities) { ...@@ -268,7 +268,7 @@ protected function attachPropertyData(array &$entities) {
} }
$data = $query->execute(); $data = $query->execute();
$field_definitions = \Drupal::entityManager()->getFieldDefinitions($this->entityTypeId); $field_definitions = \Drupal::entityManager()->getBaseFieldDefinitions($this->entityTypeId);
$translations = array(); $translations = array();
if ($this->revisionDataTable) { if ($this->revisionDataTable) {
$data_column_names = array_flip(array_diff(drupal_schema_fields_sql($this->entityType->getRevisionDataTable()), drupal_schema_fields_sql($this->entityType->getBaseTable()))); $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 ...@@ -1187,7 +1187,7 @@ public static function _fieldSqlSchema(FieldConfigInterface $field, array $schem
$entity_type_id = $field->entity_type; $entity_type_id = $field->entity_type;
$entity_manager = \Drupal::entityManager(); $entity_manager = \Drupal::entityManager();
$entity_type = $entity_manager->getDefinition($entity_type_id); $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. // Define the entity ID schema based on the field definitions.
$id_definition = $definitions[$entity_type->getKey('id')]; $id_definition = $definitions[$entity_type->getKey('id')];
......
...@@ -60,8 +60,12 @@ public function getPropertyDefinitions() { ...@@ -60,8 +60,12 @@ public function getPropertyDefinitions() {
// @todo: Add support for handling multiple bundles. // @todo: Add support for handling multiple bundles.
// See https://drupal.org/node/2169813. // See https://drupal.org/node/2169813.
$bundles = $this->getBundles(); $bundles = $this->getBundles();
$bundle = is_array($bundles) && count($bundles) == 1 ? reset($bundles) : NULL; if (is_array($bundles) && count($bundles) == 1) {
$this->propertyDefinitions = \Drupal::entityManager()->getFieldDefinitions($entity_type_id, $bundle); $this->propertyDefinitions = \Drupal::entityManager()->getFieldDefinitions($entity_type_id, reset($bundles));
}
else {
$this->propertyDefinitions = \Drupal::entityManager()->getBaseFieldDefinitions($entity_type_id);
}
} }
else { else {
// No entity type given. // No entity type given.
......
...@@ -7,60 +7,16 @@ ...@@ -7,60 +7,16 @@
namespace Drupal\Core\Field; namespace Drupal\Core\Field;
use Drupal\field\FieldInstanceConfigInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\field\Field;
/** /**
* Represents a configurable entity field item list. * Represents a configurable entity field item list.
*/ */
class ConfigFieldItemList extends FieldItemList implements ConfigFieldItemListInterface { 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} * {@inheritdoc}
*/ */
public function getConstraints() { public function getConstraints() {
$constraints = array(); $constraints = parent::getConstraints();
// Check that the number of values doesn't exceed the field cardinality. For // Check that the number of values doesn't exceed the field cardinality. For
// form submitted values, this can only happen with 'multiple value' // form submitted values, this can only happen with 'multiple value'
// widgets. // widgets.
......
...@@ -437,4 +437,13 @@ public function getConstraints(DataDefinitionInterface $definition) { ...@@ -437,4 +437,13 @@ public function getConstraints(DataDefinitionInterface $definition) {
return $constraints; return $constraints;
} }
/**
* {@inheritdoc}
*/
public function clearCachedDefinitions() {
parent::clearCachedDefinitions();
$this->prototypes = array();
}
} }
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
namespace Drupal\aggregator\Entity; namespace Drupal\aggregator\Entity;
use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldDefinition; use Drupal\Core\Field\FieldDefinition;
use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\Container;
use Drupal\Core\Entity\EntityStorageControllerInterface; use Drupal\Core\Entity\EntityStorageControllerInterface;
...@@ -120,7 +121,7 @@ public static function postDelete(EntityStorageControllerInterface $storage_cont ...@@ -120,7 +121,7 @@ public static function postDelete(EntityStorageControllerInterface $storage_cont
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public static function baseFieldDefinitions($entity_type) { public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields['fid'] = FieldDefinition::create('integer') $fields['fid'] = FieldDefinition::create('integer')
->setLabel(t('Feed ID')) ->setLabel(t('Feed ID'))
->setDescription(t('The ID of the aggregator feed.')) ->setDescription(t('The ID of the aggregator feed.'))
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityStorageControllerInterface; use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\aggregator\ItemInterface;