diff --git a/core/core.services.yml b/core/core.services.yml index 05067ab598..6c0483a230 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1771,6 +1771,9 @@ services: class: Drupal\Core\EventSubscriber\RssResponseRelativeUrlFilter tags: - { name: event_subscriber } + plugin.manager.entity.display_component_handler: + class: Drupal\Core\Entity\DisplayComponentHandlerPluginManager + parent: default_plugin_manager messenger: class: Drupal\Core\Messenger\Messenger arguments: ['@session.flash_bag', '@page_cache_kill_switch'] diff --git a/core/lib/Drupal/Core/Entity/Annotation/DisplayComponent.php b/core/lib/Drupal/Core/Entity/Annotation/DisplayComponent.php new file mode 100644 index 0000000000..9bd43281be --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Annotation/DisplayComponent.php @@ -0,0 +1,21 @@ +context = $context; + } + + /** + * {@inheritdoc} + */ + public function prepareDisplayComponents(array &$components, array &$hidden_components) { + } + + /** + * {@inheritdoc} + */ + public function hasElement($name) { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function massageIn($name, array $options) { + return $options; + } + + /** + * {@inheritdoc} + */ + public function massageOut($properties) { + return $properties; + } + + /** + * {@inheritdoc} + */ + public function getRenderer($name, array $options) { + } + +} diff --git a/core/lib/Drupal/Core/Entity/DisplayComponentHandlerInterface.php b/core/lib/Drupal/Core/Entity/DisplayComponentHandlerInterface.php new file mode 100644 index 0000000000..ef04680bfb --- /dev/null +++ b/core/lib/Drupal/Core/Entity/DisplayComponentHandlerInterface.php @@ -0,0 +1,83 @@ +alterInfo('display_component_handler_info'); + $this->setCacheBackend($cache_backend, 'display_component_handlers'); + } + + /** + * {@inheritdoc} + */ + public function getInstance(array $options) { + $plugin_id = $options['type']; + + if (!isset($this->plugins[$plugin_id]) && !array_key_exists($plugin_id, $this->plugins)) { + $this->plugins[$plugin_id] = $this->getDiscovery()->getDefinition($plugin_id) ? $this->createInstance($plugin_id) : NULL; + } + + return $this->plugins[$plugin_id]; + } + +} diff --git a/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php b/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php index 3dcffd2cfa..918ac21ee2 100644 --- a/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php +++ b/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php @@ -139,33 +139,6 @@ public function __construct(array $values, $entity_type) { parent::__construct($values, $entity_type); } - /** - * {@inheritdoc} - */ - public function getRenderer($field_name) { - if (isset($this->plugins[$field_name])) { - return $this->plugins[$field_name]; - } - - // Instantiate the widget object from the stored display properties. - if (($configuration = $this->getComponent($field_name)) && isset($configuration['type']) && ($definition = $this->getFieldDefinition($field_name))) { - $widget = $this->pluginManager->getInstance([ - 'field_definition' => $definition, - 'form_mode' => $this->originalMode, - // No need to prepare, defaults have been merged in setComponent(). - 'prepare' => FALSE, - 'configuration' => $configuration, - ]); - } - else { - $widget = NULL; - } - - // Persist the widget object. - $this->plugins[$field_name] = $widget; - return $widget; - } - /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Entity/EntityDisplayBase.php b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php index ee07817187..248bef7f3f 100644 --- a/core/lib/Drupal/Core/Entity/EntityDisplayBase.php +++ b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php @@ -78,6 +78,13 @@ abstract class EntityDisplayBase extends ConfigEntityBase implements EntityDispl */ protected $hidden = []; + /** + * The renderer objects used for this display, keyed by component name. + * + * @var array + */ + protected $renderers = []; + /** * The original view or form mode that was requested (case of view/form modes * being configured to fall back to the 'default' display). @@ -114,6 +121,18 @@ abstract class EntityDisplayBase extends ConfigEntityBase implements EntityDispl */ protected $renderer; + /** + * A mapping of display elements and its corresponding handler. + */ + protected $handlers; + + /** + * The display component handler plugin manager. + * + * @var \Drupal\Core\Entity\DisplayComponentHandlerPluginManager + */ + protected $handlerManager; + /** * {@inheritdoc} */ @@ -138,6 +157,8 @@ public function __construct(array $values, $entity_type) { parent::__construct($values, $entity_type); + $this->handlerManager = \Drupal::service('plugin.manager.entity.display_component_handler'); + $this->originalMode = $this->mode; $this->init(); @@ -296,13 +317,14 @@ public function calculateDependencies() { */ public function toArray() { $properties = parent::toArray(); - // Do not store options for fields whose display is not set to be - // configurable. - foreach ($this->getFieldDefinitions() as $field_name => $definition) { - if (!$definition->isDisplayConfigurable($this->displayContext)) { - unset($properties['content'][$field_name]); - unset($properties['hidden'][$field_name]); - } + + // Let the component handlers add missing components. + if (!$this->handlerManager) { + $this->handlerManager = \Drupal::service('plugin.manager.entity.display_component_handler'); + } + $handlers = $this->handlerManager->getDefinitions(); + foreach (array_keys($handlers) as $type) { + $properties = $this->getComponentHandler($type)->massageOut($properties); } return $properties; @@ -341,16 +363,19 @@ public function setComponent($name, array $options = []) { $options['weight'] = isset($max) ? $max + 1 : 0; } - // For a field, fill in default options. - if ($field_definition = $this->getFieldDefinition($name)) { - $options = $this->pluginManager->prepareConfiguration($field_definition->getType(), $options); + // Massage in some values. + if ($handler = $this->getComponentHandlerByElementName($name)) { + $options = $handler->massageIn($name, $options); } // Ensure we always have an empty settings and array. $options += ['settings' => [], 'third_party_settings' => []]; $this->content[$name] = $options; + + // Unset these to ensure new settings are picked up. unset($this->hidden[$name]); + unset($this->renderers[$name]); unset($this->plugins[$name]); return $this; @@ -571,4 +596,72 @@ protected function getLogger() { return \Drupal::logger('system'); } + /** + * Finds component handler by element name. + * + * @param string $name + * The name of the element. + * + * @return \Drupal\Core\Entity\DisplayComponentHandlerInterface|false + * The display component handler. + */ + public function getComponentHandlerByElementName($name) { + if (!isset($this->handlers[$name])) { + $handlers = $this->handlerManager->getDefinitions(); + $handler = FALSE; + foreach (array_keys($handlers) as $type) { + $handler = $this->getComponentHandler($type); + if ($handler && $handler->hasElement($name)) { + break; + } + $handler = FALSE; + } + $this->handlers[$name] = $handler; + } + + return $this->handlers[$name]; + } + + /** + * Instantiates component handler. + * + * @param string $type + * The type of component handler (field, extra_field). + * + * @return \Drupal\Core\Entity\DisplayComponentHandlerInterface + * The display component handler. + */ + public function getComponentHandler($type) { + $handler = $this->handlerManager->getInstance(['type' => $type]); + if ($handler) { + $handler->setContext([ + 'entity_type' => $this->targetEntityType, + 'bundle' => $this->bundle, + 'mode' => $this->originalMode, + 'display_context' => $this->displayContext, + ]); + } + return $handler; + } + + /** + * {@inheritdoc} + */ + public function getRenderer($name) { + if (!isset($this->content[$name])) { + return NULL; + } + + if (!array_key_exists($name, $this->renderers)) { + if ($handler = $this->getComponentHandlerByElementName($name)) { + $options = $this->getComponent($name); + $this->renderers[$name] = $handler->getRenderer($name, $options); + } + else { + $this->renderers[$name] = NULL; + } + } + return $this->renderers[$name]; + } + } diff --git a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php index 2ff43fcb16..9da338c096 100644 --- a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php +++ b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php @@ -80,6 +80,20 @@ class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterf */ protected $singleFieldDisplays; + /** + * The entity field manager. + * + * @var \Drupal\Core\Entity\EntityFieldManagerInterface + */ + protected $entityFieldManager; + + /** + * The entity field manager. + * + * @var \Drupal\Core\Field\FormatterPluginManager + */ + protected $formatterPluginManager; + /** * Constructs a new EntityViewBuilder. * @@ -101,6 +115,8 @@ public function __construct(EntityTypeInterface $entity_type, EntityRepositoryIn $this->languageManager = $language_manager; $this->themeRegistry = $theme_registry; $this->entityDisplayRepository = $entity_display_repository; + $this->entityFieldManager = \Drupal::service('entity_field.manager'); + $this->formatterPluginManager = \Drupal::service('plugin.manager.field.formatter'); } /** @@ -523,6 +539,10 @@ protected function getSingleFieldDisplay($entity, $field_name, $display_options) $bundle = $entity->bundle(); $key = $entity_type_id . ':' . $bundle . ':' . $field_name . ':' . Crypt::hashBase64(serialize($display_options)); if (!isset($this->singleFieldDisplays[$key])) { + $definitions = $this->entityFieldManager->getBaseFieldDefinitions($entity_type_id); + if (isset($definitions[$field_name])) { + $display_options = $this->formatterPluginManager->prepareConfiguration($definitions[$field_name]->getType(), $display_options); + } $this->singleFieldDisplays[$key] = EntityViewDisplay::create([ 'targetEntityType' => $entity_type_id, 'bundle' => $bundle, diff --git a/core/lib/Drupal/Core/Entity/Plugin/DisplayComponent/ExtraFieldDisplayComponentHandler.php b/core/lib/Drupal/Core/Entity/Plugin/DisplayComponent/ExtraFieldDisplayComponentHandler.php new file mode 100644 index 0000000000..171d6b3406 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Plugin/DisplayComponent/ExtraFieldDisplayComponentHandler.php @@ -0,0 +1,93 @@ +entityFieldManager = $entity_field_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_field.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function prepareDisplayComponents(array &$components, array &$hidden_components) { + // Fill in defaults for extra fields. + $extra_fields = $this->fetchExtraFields(); + foreach ($extra_fields as $name => $definition) { + if (!isset($components[$name]) && !isset($hidden_components[$name])) { + // Extra fields are visible by default unless they explicitly say so. + if (!isset($definition['visible']) || $definition['visible'] == TRUE) { + $components[$name] = [ + 'weight' => $definition['weight'], + ]; + } + else { + $hidden_components[$name] = TRUE; + } + } + } + } + + /** + * {@inheritdoc} + */ + public function hasElement($name) { + $extra_fields = $this->fetchExtraFields(); + return isset($extra_fields[$name]); + } + + /** + * Fetches all the extra fields. + */ + protected function fetchExtraFields() { + $context = $this->context['display_context'] == 'view' ? 'display' : $this->context['display_context']; + $extra_fields = $this->entityFieldManager->getExtraFields($this->context['entity_type'], $this->context['bundle']); + return $extra_fields[$context] ?? []; + } + +} diff --git a/core/lib/Drupal/Core/Entity/Plugin/DisplayComponent/FieldDisplayComponentHandler.php b/core/lib/Drupal/Core/Entity/Plugin/DisplayComponent/FieldDisplayComponentHandler.php new file mode 100644 index 0000000000..6dd3a89ecf --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Plugin/DisplayComponent/FieldDisplayComponentHandler.php @@ -0,0 +1,198 @@ +formatterPluginManager = $formatter_plugin_manager; + $this->widgetPluginManager = $widget_plugin_manager; + $this->entityFieldManager = $entity_field_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static($configuration, $plugin_id, $plugin_definition, + $container->get('plugin.manager.field.formatter'), + $container->get('plugin.manager.field.widget'), + $container->get('entity_field.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function massageIn($name, array $options) { + $field_definition = $this->getFieldDefinition($name); + if (!isset($field_definition)) { + // The field in process of removal from display. + return $options; + } + if ($this->context['display_context'] == 'view') { + return $this->formatterPluginManager->prepareConfiguration($field_definition->getType(), $options); + } + else { + return $this->widgetPluginManager->prepareConfiguration($field_definition->getType(), $options); + } + } + + /** + * {@inheritdoc} + */ + public function massageOut($properties) { + // Do not store options for fields whose display is not set to be + // configurable. + foreach ($this->getDisplayableFields() as $field_name => $definition) { + if (!$definition->isDisplayConfigurable($this->context['display_context'])) { + unset($properties['content'][$field_name]); + unset($properties['hidden'][$field_name]); + } + } + + return $properties; + } + + /** + * {@inheritdoc} + */ + public function prepareDisplayComponents(array &$components, array &$hidden_components) { + if ($this->context['display_context'] == 'view') { + $plugin_manager = $this->formatterPluginManager; + } + else { + $plugin_manager = $this->widgetPluginManager; + } + + // Fill in defaults for fields. + $fields = $this->getDisplayableFields(); + foreach ($fields as $name => $definition) { + if (!$definition->isDisplayConfigurable($this->context['display_context']) || (!isset($components[$name]) && !isset($hidden_components[$name]))) { + $options = $definition->getDisplayOptions($this->context['display_context']); + + if (!empty($options['type']) && $options['type'] == 'hidden') { + $hidden_components[$name] = TRUE; + } + elseif ($options) { + $components[$name] = $plugin_manager->prepareConfiguration($definition->getType(), $options); + } + // Note: (base) fields that do not specify display options are not + // tracked in the display at all, in order to avoid cluttering the + // configuration that gets saved back. + } + } + } + + /** + * {@inheritdoc} + */ + public function getRenderer($name, array $options) { + if (isset($options['type']) && ($definition = $this->getFieldDefinition($name))) { + if ($this->context['display_context'] == 'view') { + $plugin_manager = $this->formatterPluginManager; + $mode_key = 'view_mode'; + } + else { + $plugin_manager = $this->widgetPluginManager; + $mode_key = 'form_mode'; + } + + return $plugin_manager->getInstance([ + 'field_definition' => $definition, + $mode_key => $this->context['mode'], + // No need to prepare, defaults have been merged when the options were + // written in the display. + 'prepare' => FALSE, + 'configuration' => $options, + ]); + } + return NULL; + } + + /** + * {@inheritdoc} + */ + public function hasElement($name) { + $field_definition = $this->getFieldDefinition($name); + return isset($field_definition); + } + + /** + * Returns the field definition of a field. + */ + protected function getFieldDefinition($field_name) { + $definitions = $this->getDisplayableFields(); + return $definitions[$field_name] ?? NULL; + } + + /** + * Returns the definitions of the fields that are candidate for display. + */ + protected function getDisplayableFields() { + $entity_type = $this->context['entity_type']; + $bundle = $this->context['bundle']; + $display_context = $this->context['display_context']; + $definitions = $this->entityFieldManager->getFieldDefinitions($entity_type, $bundle); + + // The display only cares about fields that specify display options. + // Discard base fields that are not rendered through formatters / widgets. + return array_filter($definitions, function (FieldDefinitionInterface $definition) use ($display_context) { + return $definition->getDisplayOptions($display_context); + }); + } + +} diff --git a/core/lib/Drupal/Core/Entity/entity.api.php b/core/lib/Drupal/Core/Entity/entity.api.php index deb6a75e29..0c54dbbcc0 100644 --- a/core/lib/Drupal/Core/Entity/entity.api.php +++ b/core/lib/Drupal/Core/Entity/entity.api.php @@ -2255,6 +2255,21 @@ function hook_entity_extra_field_info_alter(&$info) { } } +/** + * Modify the list of available component handler plugins. + * + * This hook may be used to modify plugin properties after they have been + * specified by other modules. + * + * @param string[] $plugins + * An array of all the existing plugin definitions, passed by reference. + * + * @see DisplayComponentHandlerPluginManager + */ +function hook_display_component_handler_info_alter(array &$plugins) { + $plugins['some_plugin']['label'] = t('Better name'); +} + /** * @} End of "addtogroup hooks". */ diff --git a/core/lib/Drupal/Core/Field/FormatterPluginManager.php b/core/lib/Drupal/Core/Field/FormatterPluginManager.php index c4df0f5f2f..70a4313e30 100644 --- a/core/lib/Drupal/Core/Field/FormatterPluginManager.php +++ b/core/lib/Drupal/Core/Field/FormatterPluginManager.php @@ -56,6 +56,12 @@ public function createInstance($plugin_id, array $configuration = []) { $plugin_definition = $this->getDefinition($plugin_id); $plugin_class = DefaultFactory::getPluginClass($plugin_id, $plugin_definition); + // @todo is this missing somewhere else? + if (!isset($configuration['label'])) { + $configuration['label'] = ''; + assert(FALSE, sprintf('The %s does not have a label', $plugin_id)); + } + // @todo This is copied from \Drupal\Core\Plugin\Factory\ContainerFactory. // Find a way to restore sanity to // \Drupal\Core\Field\FormatterBase::__construct(). diff --git a/core/modules/views/src/Entity/Render/EntityFieldRenderer.php b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php index 7002c73403..d0ed45d0ca 100644 --- a/core/modules/views/src/Entity/Render/EntityFieldRenderer.php +++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php @@ -250,6 +250,7 @@ protected function buildFields(array $values) { $display->setComponent($field->definition['field_name'], [ 'type' => $field->options['type'], 'settings' => $field->options['settings'], + 'label' => $field->label(), ]); } // Let the display build the render array for the entities.