Commit 9c4cd8d8 authored by webchick's avatar webchick

Issue #2144919 by yched, fago, effulgentsia, amateescu, tstoeckler, chx,...

Issue #2144919 by yched, fago, effulgentsia, amateescu, tstoeckler, chx, larowlan, swentel: Allow widgets and formatters for base fields to be configured in Field UI.
parent d329ec0f
......@@ -195,6 +195,69 @@ public function setPropertyConstraints($name, array $constraints) {
return $this;
}
/**
* Sets the display options for the field in forms or rendered entities.
*
* This enables generic rendering of the field with widgets / formatters,
* including automated support for "In place editing", and with optional
* configurability in the "Manage display" / "Manage form display" UI screens.
*
* Unless this method is called, the field remains invisible (or requires
* ad-hoc rendering logic).
*
* @param string $display_context
* The display context. Either 'view' or 'form'.
* @param array $options
* An array of display options. Refer to
* \Drupal\Core\Field\FieldDefinitionInterface::getDisplayOptions() for
* a list of supported keys. The options should include at least a 'weight',
* or specify 'type' = 'hidden'. The 'default_widget' / 'default_formatter'
* for the field type will be used if no 'type' is specified.
*
* @return static
* The object itself for chaining.
*/
public function setDisplayOptions($display_context, array $options) {
$this->definition['display'][$display_context]['options'] = $options;
return $this;
}
/**
* Sets whether the display for the field can be configured.
*
* @param string $display_context
* The display context. Either 'view' or 'form'.
* @param bool $configurable
* Whether the display options can be configured (e.g., via the "Manage
* display" / "Manage form display" UI screens). If TRUE, the options
* specified via getDisplayOptions() act as defaults.
*
* @return static
* The object itself for chaining.
*/
public function setDisplayConfigurable($display_context, $configurable) {
// If no explicit display options have been specified, default to 'hidden'.
if (empty($this->definition['display'][$display_context])) {
$this->definition['display'][$display_context]['options'] = array('type' => 'hidden');
}
$this->definition['display'][$display_context]['configurable'] = $configurable;
return $this;
}
/**
* {@inheritdoc}
*/
public function getDisplayOptions($display_context) {
return isset($this->definition['display'][$display_context]['options']) ? $this->definition['display'][$display_context]['options'] : NULL;
}
/**
* {@inheritdoc}
*/
public function isDisplayConfigurable($display_context) {
return isset($this->definition['display'][$display_context]['configurable']) ? $this->definition['display'][$display_context]['configurable'] : FALSE;
}
/**
* {@inheritdoc}
*/
......
......@@ -127,13 +127,59 @@ public function getPropertyNames();
public function isTranslatable();
/**
* Determines whether the field is configurable via field.module.
* Returns whether the field is configurable via field.module.
*
* @return bool
* TRUE if the field is configurable.
*/
public function isConfigurable();
/**
* Returns whether the display for the field can be configured.
*
* @param string $display_context
* The display context. Either 'view' or 'form'.
*
* @return bool
* TRUE if the display for this field is configurable in the given context.
* If TRUE, the display options returned by getDisplayOptions() may be
* overridden via the respective EntityDisplay.
*
* @see \Drupal\Core\Entity\Display\EntityDisplayInterface
*/
public function isDisplayConfigurable($display_context);
/**
* Returns the default display options for the field.
*
* If the field's display is configurable, the returned display options act
* as default values and may be overridden via the respective EntityDisplay.
* Otherwise, the display options will be applied to entity displays as is.
*
* @param string $display_context
* The display context. Either 'view' or 'form'.
*
* @return array|null
* The array of display options for the field, or NULL if the field is not
* displayed. The following key/value pairs may be present:
* - label: (string) Position of the field label. The default 'field' theme
* implementation supports the values 'inline', 'above' and 'hidden'.
* Defaults to 'above'. Only applies to 'view' context.
* - type: (string) The plugin (widget or formatter depending on
* $display_context) to use, or 'hidden'. If not specified or if the
* requested plugin is unknown, the 'default_widget' / 'default_formatter'
* for the field type will be used.
* - settings: (array) Settings for the plugin specified above. The default
* settings for the plugin will be used for settings left unspecified.
* - weight: (float) The weight of the element. Not needed if 'type' is
* 'hidden'.
* The defaults of the various display options above get applied by the used
* entity display.
*
* @see \Drupal\Core\Entity\Display\EntityDisplayInterface
*/
public function getDisplayOptions($display_context);
/**
* Determines whether the field is queryable via QueryInterface.
*
......
......@@ -281,7 +281,7 @@ function testTwoPagers() {
->setComponent('comment_2', array(
'label' => 'hidden',
'type' => 'comment_default',
'weight' => 20,
'weight' => 30,
'settings' => array(
'pager_id' => 1,
)
......
......@@ -30,12 +30,16 @@
*/
class EntityDisplay extends EntityDisplayBase implements EntityViewDisplayInterface {
/**
* {@inheritdoc}
*/
protected $displayContext = 'view';
/**
* {@inheritdoc}
*/
public function __construct(array $values, $entity_type) {
$this->pluginManager = \Drupal::service('plugin.manager.field.formatter');
$this->displayContext = 'display';
parent::__construct($values, $entity_type);
}
......
......@@ -30,12 +30,16 @@
*/
class EntityFormDisplay extends EntityDisplayBase implements EntityFormDisplayInterface, \Serializable {
/**
* {@inheritdoc}
*/
protected $displayContext = 'form';
/**
* {@inheritdoc}
*/
public function __construct(array $values, $entity_type) {
$this->pluginManager = \Drupal::service('plugin.manager.field.widget');
$this->displayContext = 'form';
parent::__construct($values, $entity_type);
}
......
......@@ -8,8 +8,10 @@
namespace Drupal\entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldDefinition;
use Drupal\Core\Entity\Display\EntityDisplayInterface;
use Drupal\Core\Entity\ContentEntityInterface;
/**
* Base class for config entity types that store configuration for entity forms
......@@ -46,15 +48,11 @@ abstract class EntityDisplayBase extends ConfigEntityBase implements EntityDispl
public $bundle;
/**
* A partial entity, created via _field_create_entity_from_ids() from
* $targetEntityType and $bundle.
* A list of field definitions eligible for configuration in this display.
*
* @var \Drupal\Core\Entity\EntityInterface
*
* @todo Remove when getFieldDefinition() is fixed to not need it.
* https://drupal.org/node/2114707
* @var \Drupal\Core\Field\FieldDefinitionInterface[]
*/
private $targetEntity;
protected $fieldDefinitions;
/**
* View or form mode to be displayed.
......@@ -78,6 +76,13 @@ abstract class EntityDisplayBase extends ConfigEntityBase implements EntityDispl
*/
protected $content = array();
/**
* List of components that are set to be hidden.
*
* @var array
*/
protected $hidden = array();
/**
* The original view or form mode that was requested (case of view/form modes
* being configured to fall back to the 'default' display).
......@@ -130,6 +135,8 @@ public function __construct(array $values, $entity_type) {
parent::__construct($values, $entity_type);
$this->originalMode = $this->mode;
$this->init();
}
/**
......@@ -142,15 +149,19 @@ public function id() {
/**
* {@inheritdoc}
*/
public function save() {
$return = parent::save();
public function preSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
// Sort elements by weight before saving.
uasort($this->content, 'Drupal\Component\Utility\SortArray::sortByWeightElement');
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
// Reset the render cache for the target entity type.
if (\Drupal::entityManager()->hasController($this->targetEntityType, 'view_builder')) {
\Drupal::entityManager()->getViewBuilder($this->targetEntityType)->resetCache();
}
return $return;
}
/**
......@@ -164,15 +175,70 @@ public function getExportProperties() {
'bundle',
'mode',
'content',
'hidden',
'status',
);
$properties = array();
foreach ($names as $name) {
$properties[$name] = $this->get($name);
}
// 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]);
}
}
return $properties;
}
/**
* Initializes the display.
*
* This fills in default options for components:
* - that are not explicitly known as either "visible" or "hidden" in the
* display,
* - or that are not supposed to be configurable.
*/
protected function init() {
// Fill in defaults for extra fields.
$extra_fields = field_info_extra_fields($this->targetEntityType, $this->bundle, ($this->displayContext == 'view' ? 'display' : $this->displayContext));
foreach ($extra_fields as $name => $definition) {
if (!isset($this->content[$name]) && !isset($this->hidden[$name])) {
// Extra fields are visible by default unless they explicitly say so.
if (!isset($definition['visible']) || $definition['visible'] == TRUE) {
$this->content[$name] = array(
'weight' => $definition['weight']
);
}
else {
$this->hidden[$name] = TRUE;
}
}
}
// Fill in defaults for fields.
$fields = $this->getFieldDefinitions();
foreach ($fields as $name => $definition) {
if (!$definition->isDisplayConfigurable($this->displayContext) || (!isset($this->content[$name]) && !isset($this->hidden[$name]))) {
$options = $definition->getDisplayOptions($this->displayContext);
if (!empty($options['type']) && $options['type'] == 'hidden') {
$this->hidden[$name] = TRUE;
}
elseif ($options) {
$this->content[$name] = $this->pluginManager->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}
*/
......@@ -186,64 +252,14 @@ public function createCopy($mode) {
* {@inheritdoc}
*/
public function getComponents() {
$result = array();
foreach ($this->content as $name => $options) {
if (!isset($options['visible']) || $options['visible'] == TRUE) {
unset($options['visible']);
$result[$name] = $options;
}
}
return $result;
return $this->content;
}
/**
* {@inheritdoc}
*/
public function getComponent($name) {
// Until https://drupal.org/node/2144919 allows base fields to be configured
// in the UI, many base fields are also still registered as "extra fields"
// to keep appearing in the "Manage (form) display" screens.
// - Field UI still treats them as "base fields", saving only the weight
// and visibility flag in the EntityDisplay.
// - For some of them (e.g. node title), the custom rendering code has been
// removed in favor of regular widgets/formatters. Their display options
// are "upgraded" to those of a field (widget/formatter + settings) at
// runtime using hook_entity_display_alter().
// The getComponent() / setComponent() methods handle this by treating
// components as "extra fields" if they are registered as such, *and* if
// their display options contain no 'type' entry specifying a widget or
// formatter.
// @todo Cleanup after https://drupal.org/node/2144919 is fixed.
$extra_fields = field_info_extra_fields($this->targetEntityType, $this->bundle, $this->displayContext);
if (isset($extra_fields[$name]) && !isset($this->content[$name]['type'])) {
// If we have explicit settings, return an array or NULL depending on
// visibility.
if (isset($this->content[$name])) {
if ($this->content[$name]['visible']) {
return array(
'weight' => $this->content[$name]['weight'],
);
}
else {
return NULL;
}
}
// If no explicit settings for the extra field, look at the default
// visibility in its definition.
$definition = $extra_fields[$name];
if (!isset($definition['visible']) || $definition['visible'] == TRUE) {
return array(
'weight' => $definition['weight']
);
}
else {
return NULL;
}
}
elseif (isset($this->content[$name])) {
return $this->content[$name];
}
return isset($this->content[$name]) ? $this->content[$name] : NULL;
}
/**
......@@ -255,20 +271,15 @@ public function setComponent($name, array $options = array()) {
$max = $this->getHighestWeight();
$options['weight'] = isset($max) ? $max + 1 : 0;
}
// See remark in getComponent().
// @todo Cleanup after https://drupal.org/node/2144919 is fixed.
$extra_fields = field_info_extra_fields($this->targetEntityType, $this->bundle, $this->displayContext);
if (isset($extra_fields[$name]) && !isset($options['type'])) {
$options['visible'] = TRUE;
}
elseif ($field_definition = $this->getFieldDefinition($name)) {
// For a field, fill in default options.
if ($field_definition = $this->getFieldDefinition($name)) {
$options = $this->pluginManager->prepareConfiguration($field_definition->getType(), $options);
}
// Clear the persisted plugin, if any.
unset($this->plugins[$name]);
$this->content[$name] = $options;
unset($this->hidden[$name]);
unset($this->plugins[$name]);
return $this;
}
......@@ -277,22 +288,8 @@ public function setComponent($name, array $options = array()) {
* {@inheritdoc}
*/
public function removeComponent($name) {
// See remark in getComponent().
// @todo Cleanup after https://drupal.org/node/2144919 is fixed.
$extra_fields = field_info_extra_fields($this->targetEntityType, $this->bundle, $this->displayContext);
if (isset($extra_fields[$name]) && !isset($this->content[$name]['type'])) {
// 'Extra fields' are exposed in hooks and can appear at any given time.
// Therefore we store extra fields that are explicitly being hidden, so
// that we can differenciate with those that are simply not configured
// yet.
$this->content[$name] = array(
'visible' => FALSE,
);
}
else {
unset($this->content[$name]);
}
$this->hidden[$name] = TRUE;
unset($this->content[$name]);
unset($this->plugins[$name]);
return $this;
......@@ -321,14 +318,38 @@ public function getHighestWeight() {
* Returns the field definition of a field.
*/
protected function getFieldDefinition($field_name) {
// @todo Replace this entire implementation with
// \Drupal::entityManager()->getFieldDefinition() when it can hand the
// $instance objects - https://drupal.org/node/2114707
if (!isset($this->targetEntity)) {
$this->targetEntity = _field_create_entity_from_ids((object) array('entity_type' => $this->targetEntityType, 'bundle' => $this->bundle, 'entity_id' => NULL));
}
if (($this->targetEntity instanceof ContentEntityInterface) && $this->targetEntity->hasField($field_name)) {
return $this->targetEntity->get($field_name)->getFieldDefinition();
$definitions = $this->getFieldDefinitions();
return isset($definitions[$field_name]) ? $definitions[$field_name] : NULL;
}
/**
* Returns the definitions of the fields that are candidate for display.
*/
protected function getFieldDefinitions() {
if (!isset($this->fieldDefinitions)) {
// @todo Replace this with \Drupal::entityManager()->getFieldDefinition()
// when it can hand the $instance objects (and then reconsider the
// $this->fieldDefinitions static cache ?)
// https://drupal.org/node/2114707
$entity_manager = \Drupal::entityManager();
$entity_info = $entity_manager->getDefinition($this->targetEntityType);
$definitions = array();
if ($entity_info->isSubclassOf('\Drupal\Core\Entity\ContentEntityInterface')) {
$entity = _field_create_entity_from_ids((object) array('entity_type' => $this->targetEntityType, 'bundle' => $this->bundle, 'entity_id' => NULL));
foreach ($entity as $field_name => $items) {
$definitions[$field_name] = $items->getFieldDefinition();
}
}
// The display only cares about fields that specify display options.
// Discard base fields that are not rendered through formatters / widgets.
$display_context = $this->displayContext;
$this->fieldDefinitions = array_filter($definitions, function (FieldDefinitionInterface $definition) use ($display_context) {
return $definition->getDisplayOptions($display_context);
});
}
return $this->fieldDefinitions;
}
}
......@@ -14,7 +14,7 @@
*/
class EntityDisplayTest extends DrupalUnitTestBase {
public static $modules = array('entity', 'field', 'entity_test', 'user');
public static $modules = array('entity', 'field', 'entity_test', 'user', 'text');
public static function getInfo() {
return array(
......@@ -109,9 +109,10 @@ public function testEntityGetDisplay() {
* Tests the behavior of a field component within an EntityDisplay object.
*/
public function testExtraFieldComponent() {
entity_test_create_bundle('bundle_with_extra_fields');
$display = entity_create('entity_display', array(
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'bundle' => 'bundle_with_extra_fields',
'mode' => 'default',
));
......@@ -133,12 +134,6 @@ public function testExtraFieldComponent() {
public function testFieldComponent() {
$this->enableModules(array('field_test'));
$display = entity_create('entity_display', array(
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
));
$field_name = 'test_field';
// Create a field and an instance.
$field = entity_create('field_entity', array(
......@@ -154,23 +149,29 @@ public function testFieldComponent() {
));
$instance->save();
$display = entity_create('entity_display', array(
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
));
// Check that providing no options results in default values being used.
$display->setComponent($field_name);
$field_type_info = \Drupal::service('plugin.manager.field.field_type')->getDefinition($field->type);
$default_formatter = $field_type_info['default_formatter'];
$formatter_settings = \Drupal::service('plugin.manager.field.formatter')->getDefinition($default_formatter);
$formatter_settings = \Drupal::service('plugin.manager.field.formatter')->getDefaultSettings($default_formatter);
$expected = array(
'weight' => 0,
'label' => 'above',
'type' => $default_formatter,
'settings' => $formatter_settings['settings'],
'settings' => $formatter_settings,
);
$this->assertEqual($display->getComponent($field_name), $expected);
// Check that the getFormatter() method returns the correct formatter plugin.
$formatter = $display->getRenderer($field_name);
$this->assertEqual($formatter->getPluginId(), $default_formatter);
$this->assertEqual($formatter->getSettings(), $formatter_settings['settings']);
$this->assertEqual($formatter->getSettings(), $formatter_settings);
// Check that the formatter is statically persisted, by assigning an
// arbitrary property and reading it back.
......@@ -199,6 +200,65 @@ public function testFieldComponent() {
$this->assertEqual($formatter->getPluginId(), $default_formatter);
}
/**
* Tests the behavior of a field component for a base field.
*/
public function testBaseFieldComponent() {
$display = entity_create('entity_display', array(
'targetEntityType' => 'entity_test_base_field_display',
'bundle' => 'entity_test_base_field_display',
'mode' => 'default',
));
// Check that default options are correctly filled in.
$formatter_settings = \Drupal::service('plugin.manager.field.formatter')->getDefaultSettings('text_default');
$expected = array(
'test_no_display' => NULL,
'test_display_configurable' => array(
'label' => 'above',
'type' => 'text_default',
'settings' => $formatter_settings,
'weight' => 10,
),
'test_display_non_configurable' => array(
'label' => 'above',
'type' => 'text_default',
'settings' => $formatter_settings,
'weight' => 11,
),
);
foreach ($expected as $field_name => $options) {
$this->assertEqual($display->getComponent($field_name), $options);
}
// Check that saving the display only writes data for fields whose display
// is configurable.
$display->save();
$config = \Drupal::config('entity.display.' . $display->id());
$data = $config->get();
$this->assertFalse(isset($data['content']['test_no_display']));
$this->assertFalse(isset($data['hidden']['test_no_display']));
$this->assertEqual($data['content']['test_display_configurable'], $expected['test_display_configurable']);
$this->assertFalse(isset($data['content']['test_display_non_configurable']));
$this->assertFalse(isset($data['hidden']['test_display_non_configurable']));
// Check that defaults are correctly filled when loading the display.
$display = entity_load('entity_display', $display->id());
foreach ($expected as $field_name => $options) {
$this->assertEqual($display->getComponent($field_name), $options);
}
// Check that data manually written for fields whose display is not
// configurable is discarded when loading the display.
$data['content']['test_display_non_configurable'] = $expected['test_display_non_configurable'];
$data['content']['test_display_non_configurable']['weight']++;
$config->setData($data)->save();
$display = entity_load('entity_display', $display->id());
foreach ($expected as $field_name => $options) {