Commit 5348e2e3 authored by catch's avatar catch

Issue #2283977 by alexpott, Gábor Hojtsy, effulgentsia, fago: Create a new...

Issue #2283977 by alexpott, Gábor Hojtsy, effulgentsia, fago: Create a new ConfigEntity type for storing bundle-specific customizations of base fields.
parent b6ba35f0
......@@ -339,3 +339,45 @@ base_entity_reference_field_settings:
target_bundle:
type: string
label: 'Bundle of item to reference'
field_config_base:
type: config_entity
mapping:
id:
type: string
label: 'ID'
field_name:
type: string
label: 'Field name'
entity_type:
type: string
label: 'Entity type'
bundle:
type: string
label: 'Bundle'
label:
type: label
label: 'Label'
description:
type: text
label: 'Help text'
required:
type: boolean
label: 'Required field'
translatable:
type: boolean
label: 'Translatable'
default_value:
type: field.[%parent.field_type].value
default_value_function:
type: string
label: 'Default value function'
settings:
type: field.[%parent.field_type].instance_settings
field_type:
type: string
label: 'Field type'
core.base_field_override.*.*.*:
type: field_config_base
label: 'Base field bundle override'
......@@ -67,6 +67,12 @@ function entity_invoke_bundle_hook($hook, $entity_type, $bundle, $bundle_new = N
if (method_exists($storage, $method)) {
$storage->$method($bundle, $bundle_new);
}
// Notify the entity manager.
if (method_exists($entity_manager, $method)) {
$entity_manager->$method($entity_type, $bundle, $bundle_new);
}
// Invoke hook_entity_bundle_*() hooks.
\Drupal::moduleHandler()->invokeAll('entity_bundle_' . $hook, array($entity_type, $bundle, $bundle_new));
// Clear the cached field definitions (not needed in case of 'create').
......
......@@ -1033,7 +1033,7 @@ protected function getEntityKey($key) {
if (!isset($this->entityKeys[$key]) || !array_key_exists($key, $this->entityKeys)) {
if ($this->getEntityType()->hasKey($key)) {
$field_name = $this->getEntityType()->getKey($key);
$property = $this->getFieldDefinition($field_name)->getMainPropertyName();
$property = $this->getFieldDefinition($field_name)->getFieldStorageDefinition()->getMainPropertyName();
$this->entityKeys[$key] = $this->get($field_name)->$property;
}
else {
......
......@@ -44,17 +44,22 @@ public function initTranslation($langcode);
/**
* Provides base field definitions for an entity type.
*
* Implementations typically use the class \Drupal\Core\Field\BaseFieldDefinition
* for creating the field definitions; for example a 'name' field could be
* defined as the following:
* Implementations typically use the class
* \Drupal\Core\Field\BaseFieldDefinition for creating the field definitions;
* for example a 'name' field could be defined as the following:
* @code
* $fields['name'] = BaseFieldDefinition::create('string')
* ->setLabel(t('Name'));
* @endcode
*
* If some elements in a field definition need to vary by bundle, use
* By definition, base fields are fields that exist for every bundle. To
* provide definitions for fields that should only exist on some bundles, use
* \Drupal\Core\Entity\ContentEntityInterface::bundleFieldDefinitions().
*
* The definitions returned by this function can be overridden for all
* bundles by hook_entity_base_field_info_alter() or overridden on a
* per-bundle basis via 'base_field_override' configuration entities.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition. Useful when a single class is used for multiple,
* possibly dynamic entity types.
......@@ -69,14 +74,24 @@ public function initTranslation($langcode);
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.
* Provides field definitions for a specific bundle.
*
* This function can return definitions both for bundle fields (fields that
* are not defined in $base_field_definitions, and therefore might not exist
* on some bundles) as well as bundle-specific overrides of base fields
* (fields that are defined in $base_field_definitions, and therefore exist
* for all bundles). However, bundle-specific base field overrides can also
* be provided by 'base_field_override' configuration entities, and that is
* the recommended approach except in cases where an entity type needs to
* provide a bundle-specific base field override that is decoupled from
* configuration. Note that for most entity types, the bundles themselves are
* derived from configuration (e.g., 'node' bundles are managed via
* 'node_type' configuration entities), so decoupling bundle-specific base
* field overrides from configuration only makes sense for entity types that
* also decouple their bundles from configuration. In cases where both this
* function returns a bundle-specific override of a base field and a
* 'base_field_override' configuration entity exists, the latter takes
* precedence.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition. Useful when a single class is used for multiple,
......
......@@ -471,8 +471,22 @@ protected function buildBundleFieldDefinitions($entity_type_id, $bundle, array $
$entity_type = $this->getDefinition($entity_type_id);
$class = $entity_type->getClass();
// Allow the entity class to override the base fields.
// Allow the entity class to provide bundle fields and bundle-specific
// overrides of base fields.
$bundle_field_definitions = $class::bundleFieldDefinitions($entity_type, $bundle, $base_field_definitions);
// Load base field overrides from configuration. These take precedence over
// base field overrides returned above.
$base_field_override_ids = array_map(function($field_name) use ($entity_type_id, $bundle) {
return $entity_type_id . '.' . $bundle . '.' . $field_name;
}, array_keys($base_field_definitions));
$base_field_overrides = $this->getStorage('base_field_override')->loadMultiple($base_field_override_ids);
foreach ($base_field_overrides as $base_field_override) {
/** @var \Drupal\Core\Field\Entity\BaseFieldOverride $base_field_override */
$field_name = $base_field_override->getName();
$bundle_field_definitions[$field_name] = $base_field_override;
}
$provider = $entity_type->getProvider();
foreach ($bundle_field_definitions as $definition) {
// @todo Remove this check once FieldDefinitionInterface exposes a proper
......@@ -959,4 +973,28 @@ public function getEntityTypeFromClass($class_name) {
throw new NoCorrespondingEntityClassException($class_name);
}
/**
* Acts on entity bundle rename.
*
* @param string $entity_type_id
* The entity type to which the bundle is bound.
* @param string $bundle_old
* The previous name of the bundle.
* @param string $bundle_new
* The new name of the bundle.
*
* @see entity_invoke_bundle_hook()
* @see entity_crud
*/
public function onBundleRename($entity_type_id, $bundle_old, $bundle_new) {
// Rename existing base field bundle overrides.
$overrides = $this->getStorage('base_field_override')->loadByProperties(array('entity_type' => $entity_type_id, 'bundle' => $bundle_old));
foreach ($overrides as $override) {
$override->set('id', $entity_type_id . '.' . $bundle_new . '.' . $override->field_name);
$override->bundle = $bundle_new;
$override->allowBundleRename();
$override->save();
}
}
}
......@@ -8,6 +8,7 @@
namespace Drupal\Core\Field;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Field\Entity\BaseFieldOverride;
use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
use Drupal\Core\TypedData\ListDataDefinition;
use Drupal\field\FieldException;
......@@ -632,4 +633,15 @@ public function getUniqueStorageIdentifier() {
return $this->getTargetEntityTypeId() . '-' . $this->getName();
}
/**
* {@inheritdoc}
*/
public function getConfig($bundle) {
$override = BaseFieldOverride::loadByName($this->getTargetEntityTypeId(), $bundle, $this->getName());
if ($override) {
return $override;
}
return BaseFieldOverride::createFromBaseFieldDefinition($this, $bundle);
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Field\BaseFieldOverrideStorage.
*/
namespace Drupal\Core\Field;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Component\Uuid\UuidInterface;
/**
* Storage class for base field overrides.
*/
class BaseFieldOverrideStorage extends FieldConfigStorageBase {
/**
* Constructs a BaseFieldOverrideStorage object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\Component\Uuid\UuidInterface $uuid_service
* The UUID service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
* The field type plugin manager.
*/
public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, UuidInterface $uuid_service, LanguageManagerInterface $language_manager, FieldTypePluginManagerInterface $field_type_manager) {
parent::__construct($entity_type, $config_factory, $uuid_service, $language_manager);
$this->fieldTypeManager = $field_type_manager;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('config.factory'),
$container->get('uuid'),
$container->get('language_manager'),
$container->get('plugin.manager.field.field_type')
);
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Field\Entity\BaseFieldOverride.
*/
namespace Drupal\Core\Field\Entity;
use Drupal\Component\Utility\String;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\FieldConfigBase;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\field\FieldException;
/**
* Defines the base field override entity.
*
* Allows base fields to be overridden on the bundle level.
*
* @ConfigEntityType(
* id = "base_field_override",
* label = @Translation("Base field override"),
* controllers = {
* "storage" = "Drupal\Core\Field\BaseFieldOverrideStorage"
* },
* config_prefix = "base_field_override",
* entity_keys = {
* "id" = "id",
* "label" = "label"
* }
* )
*/
class BaseFieldOverride extends FieldConfigBase {
/**
* The base field definition.
*
* @var \Drupal\Core\Field\BaseFieldDefinition
*/
protected $baseFieldDefinition;
/**
* Creates a base field override object.
*
* @param \Drupal\Core\Field\BaseFieldDefinition $base_field_definition
* The base field definition to override.
* @param string $bundle
* The bundle to which the override applies.
*
* @return \Drupal\Core\Field\Entity\BaseFieldOverride
* A new base field override object.
*/
public static function createFromBaseFieldDefinition(BaseFieldDefinition $base_field_definition, $bundle) {
$values = $base_field_definition->toArray();
$values['bundle'] = $bundle;
$values['baseFieldDefinition'] = $base_field_definition;
return \Drupal::entityManager()->getStorage('base_field_override')->create($values);
}
/**
* Constructs a BaseFieldOverride object.
*
* In most cases, base field override entities are created via
* BaseFieldOverride::createFromBaseFieldDefinition($definition, 'bundle')
*
* @param array $values
* An array of base field bundle override properties, keyed by property
* name. The field to override is specified by referring to an existing
* field with:
* - field_name: The field name.
* - entity_type: The entity type.
* Additionally, a 'bundle' property is required to indicate the entity
* bundle to which the bundle field override is attached to. Other array
* elements will be used to set the corresponding properties on the class;
* see the class property documentation for details.
* @param string $entity_type
* (optional) The type of the entity to create. Defaults to
* 'base_field_override'.
*
* @see entity_create()
*
* @throws \Drupal\field\FieldException
* Exception thrown if $values does not contain a field_name, entity_type or
* bundle value.
*/
public function __construct(array $values, $entity_type = 'base_field_override') {
if (empty($values['field_name'])) {
throw new FieldException('Attempt to create a base field bundle override of a field without a field_name');
}
if (empty($values['entity_type'])) {
throw new FieldException(String::format('Attempt to create a base field bundle override of field @field_name without an entity_type', array('@field_name' => $values['field_name'])));
}
if (empty($values['bundle'])) {
throw new FieldException(String::format('Attempt to create a base field bundle override of field @field_name without a bundle', array('@field_name' => $values['field_name'])));
}
// Discard the 'field_type' entry that is added in config records to ease
// schema generation. See self::toArray().
unset($values['field_type']);
parent::__construct($values, $entity_type);
}
/**
* {@inheritdoc}
*/
public function getFieldStorageDefinition() {
return $this->getBaseFieldDefinition()->getFieldStorageDefinition();
}
/**
* {@inheritdoc}
*/
public function isDisplayConfigurable($context) {
return $this->getBaseFieldDefinition()->isDisplayConfigurable($context);
}
/**
* {@inheritdoc}
*/
public function getDisplayOptions($display_context) {
return $this->getBaseFieldDefinition()->getDisplayOptions($display_context);
}
/**
* {@inheritdoc}
*/
public function isReadOnly() {
return $this->getBaseFieldDefinition()->isReadOnly();
}
/**
* {@inheritdoc}
*/
public function isComputed() {
return $this->getBaseFieldDefinition()->isComputed();
}
/**
* Gets the base field definition.
*
* @return \Drupal\Core\Field\BaseFieldDefinition
*/
protected function getBaseFieldDefinition() {
if (!isset($this->baseFieldDefinition)) {
$fields = $this->entityManager()->getBaseFieldDefinitions($this->entity_type);
$this->baseFieldDefinition = $fields[$this->getName()];
}
return $this->baseFieldDefinition;
}
/**
* {@inheritdoc}
*
* @throws \Drupal\field\FieldException
* If the bundle is being changed and
* BaseFieldOverride::allowBundleRename() has not been called.
*/
public function preSave(EntityStorageInterface $storage) {
// Set the default instance settings.
$this->settings += \Drupal::service('plugin.manager.field.field_type')->getDefaultInstanceSettings($this->getType());
// Call the parent's presave method to perform validate and calculate
// dependencies.
parent::preSave($storage);
if ($this->isNew()) {
// @todo This assumes that the previous definition isn't some
// non-config-based override, but that might not be the case:
// https://www.drupal.org/node/2321071.
$previous_definition = $this->getBaseFieldDefinition();
}
else {
// Some updates are always disallowed.
if ($this->entity_type != $this->original->entity_type) {
throw new FieldException(String::format('Cannot change the entity_type of an existing base field bundle override (entity type:@entity_type, bundle:@bundle, field name: @field_name)', array('@field_name' => $this->field_name, '@entity_type' => $this->entity_type, '@bundle' => $this->original->bundle)));
}
if ($this->bundle != $this->original->bundle && empty($this->bundleRenameAllowed)) {
throw new FieldException(String::format('Cannot change the bundle of an existing base field bundle override (entity type:@entity_type, bundle:@bundle, field name: @field_name)', array('@field_name' => $this->field_name, '@entity_type' => $this->entity_type, '@bundle' => $this->original->bundle)));
}
$previous_definition = $this->original;
}
// Notify the entity storage.
$this->entityManager()->getStorage($this->getTargetEntityTypeId())->onFieldDefinitionUpdate($this, $previous_definition);
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $field_overrides) {
$entity_manager = \Drupal::entityManager();
// Clear the cache upfront, to refresh the results of getBundles().
$entity_manager->clearCachedFieldDefinitions();
/** @var \Drupal\Core\Field\Entity\BaseFieldOverride $field_override */
foreach ($field_overrides as $field_override) {
// Inform the system that the field definition is being updated back to
// its non-overridden state.
// @todo This assumes that there isn't a non-config-based override that
// we're returning to, but that might not be the case:
// https://www.drupal.org/node/2321071.
$entity_manager->getStorage($field_override->getTargetEntityTypeId())->onFieldDefinitionUpdate($field_override->getBaseFieldDefinition(), $field_override);
}
}
/**
* Loads a base field bundle override config entity.
*
* @param string $entity_type_id
* ID of the entity type.
* @param string $bundle
* Bundle name.
* @param string $field_name
* Name of the field.
*
* @return static
* The base field bundle override config entity if one exists for the
* provided field name, otherwise NULL.
*/
public static function loadByName($entity_type_id, $bundle, $field_name) {
return \Drupal::entityManager()->getStorage('base_field_override')->load($entity_type_id . '.' . $bundle . '.' . $field_name);
}
/**
* Implements the magic __sleep() method.
*/
public function __sleep() {
// Only serialize necessary properties, excluding those that can be
// recalculated.
unset($this->baseFieldDefinition);
return parent::__sleep();
}
}
This diff is collapsed.
<?php
/**
* @file
* Contains \Drupal\Core\Field\FieldConfigInterface.
*/
namespace Drupal\Core\Field;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
/**
* Defines an interface for configurable field definitions.
*
* This interface allows both configurable fields and overridden base fields to
* share a common interface. The interface also extends ConfigEntityInterface
* to ensure that implementations have the expected save() method.
*
* @see \Drupal\Core\Field\Entity\BaseFieldOverride
* @see \Drupal\field\Entity\FieldInstanceConfig
*/
interface FieldConfigInterface extends FieldDefinitionInterface, ConfigEntityInterface {
/**
* Sets the field definition label.
*
* @param string $label
* The label to set.
*
* @return $this
*/
public function setLabel($label);
/**
* Sets whether the field is translatable.
*
* @param bool $translatable
* Whether the field is translatable.
*
* @return $this
*/
public function setTranslatable($translatable);
/**
* Allows a bundle to be renamed.
*
* Renaming a bundle on the instance is allowed when an entity's bundle
* is renamed and when field_entity_bundle_rename() does internal
* housekeeping.
*/
public function allowBundleRename();
/**
* Returns the name of the bundle this field instance is attached to.
*
* @return string
* The name of the bundle this field instance is attached to.
*/
public function targetBundle();
/**
* Sets a default value.
*
* Note that if a default value callback is set, it will take precedence over
* any value set here.
*
* @param mixed $value
* The default value in the format as returned by
* \Drupal\Core\Field\FieldDefinitionInterface::getDefaultValue().
*
* @return $this
*/
public function setDefaultValue($value);
}
<?php
/**
* @file
* Contains \Drupal\Core\Field\FieldConfigStorageBase.
*/
namespace Drupal\Core\Field;
use Drupal\Core\Config\Entity\ConfigEntityStorage;
use Drupal\Core\Entity\EntityInterface;
/**
* Base storage class for field config entities.
*/
abstract class FieldConfigStorageBase extends ConfigEntityStorage {
/**
* The field type plugin manager.
*
* @var \Drupal\Core\Field\FieldTypePluginManagerInterface
*/
protected $fieldTypeManager;
/**
* {@inheritdoc}
*/
protected function mapFromStorageRecords(array $records) {
foreach ($records as &$record) {
$class = $this->fieldTypeManager->getPluginClass($record['field_type']);
$record['settings'] = $class::instanceSettingsFromConfigData($record['settings']);
}
return parent::mapFromStorageRecords($records);
}
/**
* {@inheritdoc}
*/
protected function mapToStorageRecord(EntityInterface $entity) {
$record = parent::mapToStorageRecord($entity);
$class = $this->fieldTypeManager->getPluginClass($record['field_type']);
$record['settings'] = $class::instanceSettingsToConfigData($record['settings']);
return $record;
}
}
......@@ -169,16 +169,6 @@ public function getDefaultValue(ContentEntityInterface $entity);
*/
public function isTranslatable();
/**
* Sets whether the field is translatable.
*
* @param bool $translatable
* Whether the field is translatable.
*
* @return $this
*/
public function setTranslatable($translatable);
/**
* Returns the field storage definition.
*
......@@ -186,4 +176,21 @@ public function setTranslatable($translatable);
* The field storage definition.
*/
public function getFieldStorageDefinition();
/**
* Gets an object that can be saved in configuration.
*
* Base fields are defined in code. In order to configure field definition
* properties per bundle use this method to create an override that can be
* saved in configuration.
*
* @see \Drupal\Core\Field\Entity\BaseFieldBundleOverride
*
* @param string $bundle
* The bundle to get the configurable field for.
*
* @return \Drupal\Core\Field\FieldConfigInterface
*/
public function getConfig($bundle);
}
# Changes the default value of the promote base field on the book node type.
langcode: en
status: true
dependencies:
entity:
- node.type.book
id: node.book.promote
field_name: promote
entity_type: node
bundle: book
label: Promote
description: 'A boolean indicating whether the node should be displayed on the front page.'
required: false
translatable: true
default_value:
-
value: 0
default_value_function: ''
settings: { }
field_type: boolean
......@@ -2,15 +2,10 @@ type: book
name: 'Book page'
description: '<em>Books</em> have a built-in hierarchical navigation. Use for handbooks or tutorials.'
help: ''
title_label: Title
settings:
node:
preview: 1
options:
status: true
# Not promoted to front page.
promote: false
sticky: false
revision: false
submitted: true
status: true
......
......@@ -99,7 +99,6 @@ function _content_translation_form_language_content_settings_form_alter(array &$
'#default_value' => content_translation_enabled($entity_type_id, $bundle),
);
$field_settings