Commit c63f99d6 authored by webchick's avatar webchick

Issue #1785256 by yched, Stalski, zuuperman, sun, tstoeckler, aspilicious,...

Issue #1785256 by yched, Stalski, zuuperman, sun, tstoeckler, aspilicious, EclipseGc, larowlan: Widgets as Plugins.
parent 66b28910
This diff is collapsed.
......@@ -107,6 +107,95 @@
* the Field API.
*/
/**
* Invoke a method on all the fields of a given entity.
*
* @todo Remove _field_invoke() and friends when field types and formatters are
* turned into plugins.
*
* @param string $method
* The name of the method to invoke.
* @param Closure $target
* A closure that receives an $instance object and returns the object on
* which the method should be invoked.
* @param Drupal\Core\Entity\EntityInterface $entity
* The fully formed $entity_type entity.
* @param mixed $a
* A parameter for the invoked method. Defaults to NULL.
* @param mixed $b
* A parameter for the invoked method. Defaults to NULL.
* @param array $options
* An associative array of additional options, with the following keys:
* - field_name: The name of the field whose operation should be invoked. By
* default, the operation is invoked on all the fields in the entity's
* bundle. NOTE: This option is not compatible with the 'deleted' option;
* the 'field_id' option should be used instead.
* - field_id: The ID of the field whose operation should be invoked. By
* default, the operation is invoked on all the fields in the entity's'
* bundles.
* - deleted: If TRUE, the function will operate on deleted fields as well
* as non-deleted fields. If unset or FALSE, only non-deleted fields are
* operated on.
* - langcode: A language code or an array of language codes keyed by field
* name. It will be used to narrow down to a single value the available
* languages to act on.
*/
function field_invoke_method($method, \Closure $target_closure, EntityInterface $entity, &$a = NULL, &$b = NULL, array $options = array()) {
// Merge default options.
$default_options = array(
'deleted' => FALSE,
'langcode' => NULL,
);
$options += $default_options;
$entity_type = $entity->entityType();
// Determine the list of instances to iterate on.
$instances = _field_invoke_get_instances($entity_type, $entity->bundle(), $options);
// Iterate through the instances and collect results.
$return = array();
foreach ($instances as $instance) {
// Let the closure determine the target object on which the method should be
// called.
$target = $target_closure($instance);
if (method_exists($target, $method)) {
$field = field_info_field_by_id($instance['field_id']);
$field_name = $field['field_name'];
// Determine the list of languages to iterate on.
$available_langcodes = field_available_languages($entity_type, $field);
$langcodes = _field_language_suggestion($available_langcodes, $options['langcode'], $field_name);
foreach ($langcodes as $langcode) {
$items = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();
$result = $target->$method($entity, $langcode, $items, $a, $b);
if (isset($result)) {
// For methods with array results, we merge results together.
// For methods with scalar results, we collect results in an array.
if (is_array($result)) {
$return = array_merge($return, $result);
}
else {
$return[] = $result;
}
}
// Populate $items back in the field values, but avoid replacing missing
// fields with an empty array (those are not equivalent on update).
if ($items !== array() || isset($entity->{$field_name}[$langcode])) {
$entity->{$field_name}[$langcode] = $items;
}
}
}
}
return $return;
}
/**
* Invoke a field hook.
*
......@@ -431,6 +520,20 @@ function _field_invoke_get_instances($entity_type, $bundle, $options) {
return $instances;
}
/**
* Defines a 'target closure' for field_invoke_method().
*
* Used to invoke methods on an instance's widget.
*
* @return Closure
* A 'target closure' for field_invoke_method().
*/
function _field_invoke_widget_target() {
return function ($instance) {
return $instance->getWidget();
};
}
/**
* Add form elements for all fields for an entity to a form structure.
*
......@@ -543,7 +646,7 @@ function field_attach_form($entity_type, EntityInterface $entity, &$form, &$form
// If no language is provided use the default site language.
$options = array('langcode' => field_valid_language($langcode));
$form += (array) _field_invoke_default('form', $entity_type, $entity, $form, $form_state, $options);
$form += (array) field_invoke_method('form', _field_invoke_widget_target(), $entity, $form, $form_state, $options);
// Add custom weight handling.
$form['#pre_render'][] = '_field_extra_fields_pre_render';
......@@ -795,9 +898,6 @@ function field_attach_validate($entity_type, $entity) {
* An associative array containing the current state of the form.
*/
function field_attach_form_validate($entity_type, EntityInterface $entity, $form, &$form_state) {
// Extract field values from submitted values.
_field_invoke_default('extract_form_values', $entity_type, $entity, $form, $form_state);
// Perform field_level validation.
try {
field_attach_validate($entity_type, $entity);
......@@ -812,7 +912,7 @@ function field_attach_form_validate($entity_type, EntityInterface $entity, $form
field_form_set_state($form['#parents'], $field_name, $langcode, $form_state, $field_state);
}
}
_field_invoke_default('form_errors', $entity_type, $entity, $form, $form_state);
field_invoke_method('flagErrors', _field_invoke_widget_target(), $entity, $form, $form_state);
}
}
......@@ -836,9 +936,7 @@ function field_attach_form_validate($entity_type, EntityInterface $entity, $form
*/
function field_attach_submit($entity_type, EntityInterface $entity, $form, &$form_state) {
// Extract field values from submitted values.
_field_invoke_default('extract_form_values', $entity_type, $entity, $form, $form_state);
_field_invoke_default('submit', $entity_type, $entity, $form, $form_state);
field_invoke_method('submit', _field_invoke_widget_target(), $entity, $form, $form_state);
// Let other modules act on submitting the entity.
// Avoid module_invoke_all() to let $form_state be taken by reference.
......
......@@ -559,6 +559,14 @@ function _field_write_instance($instance, $update = FALSE) {
$field = field_read_field($instance['field_name']);
$field_type = field_info_field_types($field['type']);
// Temporary workaround to allow incoming $instance as arrays or classed
// objects.
// @todo remove once the external APIs have been converted to use
// FieldInstance objects.
if (is_object($instance) && get_class($instance) == 'Drupal\field\FieldInstance') {
$instance = $instance->getArray();
}
// Set defaults.
$instance += array(
'settings' => array(),
......
......@@ -10,39 +10,6 @@
* the corresponding field_attach_[operation]() function.
*/
/**
* Extracts field values from submitted form values.
*
* @param $entity_type
* The type of $entity.
* @param $entity
* The entity for the operation.
* @param $field
* The field structure for the operation.
* @param $instance
* The instance structure for $field on $entity's bundle.
* @param $langcode
* The language associated to $items.
* @param $items
* The field values. This parameter is altered by reference to receive the
* incoming form values.
* @param $form
* The form structure where field elements are attached to. This might be a
* full form structure, or a sub-element of a larger form.
* @param $form_state
* The form state.
*/
function field_default_extract_form_values($entity_type, $entity, $field, $instance, $langcode, &$items, $form, &$form_state) {
$path = array_merge($form['#parents'], array($field['field_name'], $langcode));
$key_exists = NULL;
$values = drupal_array_get_nested_value($form_state['values'], $path, $key_exists);
if ($key_exists) {
// Remove the 'value' of the 'add more' button.
unset($values['add_more']);
$items = $values;
}
}
/**
* Generic field validation handler.
*
......@@ -86,13 +53,6 @@ function field_default_validate($entity_type, $entity, $field, $instance, $langc
}
}
function field_default_submit($entity_type, $entity, $field, $instance, $langcode, &$items, $form, &$form_state) {
// Filter out empty values.
$items = _field_filter_items($field, $items);
// Reorder items to account for drag-n-drop reordering.
$items = _field_sort_items($field, $items);
}
/**
* Default field 'insert' operation.
*
......
This diff is collapsed.
......@@ -5,6 +5,8 @@
* Field Info API, providing information about available fields and field types.
*/
use Drupal\field\FieldInstance;
/**
* @defgroup field_info Field Info API
* @{
......@@ -88,7 +90,6 @@ function _field_info_collate_types() {
else {
$info = array(
'field types' => array(),
'widget types' => array(),
'formatter types' => array(),
'storage types' => array(),
);
......@@ -108,21 +109,6 @@ function _field_info_collate_types() {
}
drupal_alter('field_info', $info['field types']);
// Populate widget types.
foreach (module_implements('field_widget_info') as $module) {
$widget_types = (array) module_invoke($module, 'field_widget_info');
foreach ($widget_types as $name => $widget_info) {
// Provide defaults.
$widget_info += array(
'settings' => array(),
);
$info['widget types'][$name] = $widget_info;
$info['widget types'][$name]['module'] = $module;
}
}
drupal_alter('field_widget_info', $info['widget types']);
uasort($info['widget types'], 'drupal_sort_weight');
// Populate formatter types.
foreach (module_implements('field_formatter_info') as $module) {
$formatter_types = (array) module_invoke($module, 'field_formatter_info');
......@@ -227,7 +213,7 @@ function _field_info_collate_fields() {
foreach ($definitions['instances'] as $instance) {
$field = $info['fields'][$instance['field_id']];
$instance = _field_info_prepare_instance($instance, $field);
$info['instances'][$instance['entity_type']][$instance['bundle']][$instance['field_name']] = $instance;
$info['instances'][$instance['entity_type']][$instance['bundle']][$instance['field_name']] = new FieldInstance($instance);
// Enrich field definitions with the list of bundles where they have
// instances. NOTE: Deleted fields in $info['field_ids'] are not
// enriched because all of their instances are deleted, too, and
......@@ -310,8 +296,6 @@ function _field_info_prepare_instance($instance, $field) {
$instance['default_value'] = NULL;
}
$instance['widget'] = _field_info_prepare_instance_widget($field, $instance['widget']);
foreach ($instance['display'] as $view_mode => $display) {
$instance['display'][$view_mode] = _field_info_prepare_instance_display($field, $display);
}
......@@ -371,37 +355,6 @@ function _field_info_prepare_instance_display($field, $display) {
return $display;
}
/**
* Prepares widget specifications for the current run-time context.
*
* @param $field
* The field structure for the instance.
* @param $widget
* Widget specifications as found in $instance['widget'].
*/
function _field_info_prepare_instance_widget($field, $widget) {
$field_type = field_info_field_types($field['type']);
// Fill in default values.
$widget += array(
'type' => $field_type['default_widget'],
'settings' => array(),
'weight' => 0,
);
$widget_type = field_info_widget_types($widget['type']);
// Fallback to default formatter if formatter type is not available.
if (!$widget_type) {
$widget['type'] = $field_type['default_widget'];
$widget_type = field_info_widget_types($widget['type']);
}
$widget['module'] = $widget_type['module'];
// Fill in default settings for the widget.
$widget['settings'] += field_info_widget_settings($widget['type']);
return $widget;
}
/**
* Prepares 'extra fields' for the current run-time context.
*
......@@ -496,27 +449,22 @@ function field_info_field_types($field_type = NULL) {
}
/**
* Returns information about field widgets from hook_field_widget_info().
* Returns information about field widgets from AnnotatedClassDiscovery.
*
* @param $widget_type
* @param string $widget_type
* (optional) A widget type name. If omitted, all widget types will be
* returned.
*
* @return
* Either a single widget type description, as provided by
* hook_field_widget_info(), or an array of all existing widget types, keyed
* by widget type name.
* @return array
* Either a single widget type description, as provided by class annotations,
* or an array of all existing widget types, keyed by widget type name.
*/
function field_info_widget_types($widget_type = NULL) {
$info = _field_info_collate_types();
$widget_types = $info['widget types'];
if ($widget_type) {
if (isset($widget_types[$widget_type])) {
return $widget_types[$widget_type];
}
return field_get_plugin_manager('widget')->getDefinition($widget_type);
}
else {
return $widget_types;
return field_get_plugin_manager('widget')->getDefinitions();
}
}
......
......@@ -489,6 +489,35 @@ function field_associate_fields($module) {
}
}
/**
* Returns the PluginManager object for a given field plugin type.
*
* @param string $plugin_type
* The plugin type. One of:
* - field_type
* - widget
* - formatter
* - storage
*
* @return Drupal\Component\Plugin\PluginManagerInterface
* The PluginManager object.
*/
function field_get_plugin_manager($plugin_type) {
$plugin_types = &drupal_static(__FUNCTION__, array());
$classes = array(
'widget' => 'Drupal\field\Plugin\Type\Widget\WidgetPluginManager',
);
if (isset($classes[$plugin_type])) {
if (!isset($plugin_types[$plugin_type])) {
$plugin_types[$plugin_type] = new $classes[$plugin_type]();
}
return $plugin_types[$plugin_type];
}
}
/**
* Helper function to get the default value for a field on an entity.
*
......
<?php
/**
* @file
* Definition of Drupal\field\FieldInstance.
*/
namespace Drupal\field;
/**
* Class for field instance objects.
*/
class FieldInstance implements \ArrayAccess {
/**
* The instance definition, as read from configuration storage.
*
* @var array
*/
public $definition;
/**
* The widget object used for this instance.
*
* @var Drupal\field\Plugin\Type\Widget\WidgetInterface
*/
protected $widget;
/**
* Constructs a FieldInstance object.
*
* @param array $definition
* The instance definition array, as read from configuration storage.
*/
public function __construct(array $definition) {
$this->definition = $definition;
}
/**
* Returns the Widget plugin for the instance.
*
* @return Drupal\field\Plugin\Type\Widget\WidgetInterface
* The Widget plugin to be used for the instance.
*/
public function getWidget() {
if (empty($this->widget)) {
$widget_properties = $this->definition['widget'];
// Let modules alter the widget properties.
$context = array(
'entity_type' => $this->definition['entity_type'],
'bundle' => $this->definition['bundle'],
'field' => field_info_field($this->definition['field_name']),
'instance' => $this,
);
drupal_alter(array('field_widget_properties', 'field_widget_properties_' . $this->definition['entity_type']), $widget_properties, $context);
$options = array(
'instance' => $this,
'type' => $widget_properties['type'],
'settings' => $widget_properties['settings'],
'weight' => $widget_properties['weight'],
);
$this->widget = field_get_plugin_manager('widget')->getInstance($options);
}
return $this->widget;
}
/**
* Implements ArrayAccess::offsetExists().
*/
public function offsetExists($offset) {
return isset($this->definition[$offset]) || array_key_exists($offset, $this->definition);
}
/**
* Implements ArrayAccess::offsetGet().
*/
public function &offsetGet($offset) {
return $this->definition[$offset];
}
/**
* Implements ArrayAccess::offsetSet().
*/
public function offsetSet($offset, $value) {
if (!isset($offset)) {
// Do nothing; $array[] syntax is not supported by this temporary wrapper.
return;
}
$this->definition[$offset] = $value;
// If the widget properties changed, the widget plugin needs to be
// re-instanciated.
if ($offset == 'widget') {
unset($this->widget);
}
}
/**
* Implements ArrayAccess::offsetUnset().
*/
public function offsetUnset($offset) {
unset($this->definition[$offset]);
// If the widget properties changed, the widget plugin needs to be
// re-instanciated.
if ($offset == 'widget') {
unset($this->widget);
}
}
/**
* Returns the instance definition as a regular array.
*
* This is used as a temporary BC layer.
* @todo Remove once the external APIs have been converted to use
* FieldInstance objects.
*
* @return array
* The instance definition as a regular array.
*/
public function getArray() {
return $this->definition;
}
/**
* Handles serialization of Drupal\field\FieldInstance objects.
*/
public function __sleep() {
return array('definition');
}
}
<?php
/**
* @file
* Definition of Drupal\field\Plugin\PluginSettingsBase.
*/
namespace Drupal\field\Plugin;
use Drupal\Component\Plugin\PluginBase;
use Drupal\field\Plugin\PluginSettingsInterface;
/**
* Base class for the Field API plugins.
*
* This class handles lazy replacement of default settings values.
*/
abstract class PluginSettingsBase extends PluginBase implements PluginSettingsInterface {
/**
* The plugin settings.
*
* @var array
*/
protected $settings = array();
/**
* Whether default settings have been merged into the current $settings.
*
* @var bool
*/
protected $defaultSettingsMerged = FALSE;
/**
* Implements Drupal\field\Plugin\PluginSettingsInterface::getSettings().
*/
public function getSettings() {
// Merge defaults before returning the array.
if (!$this->defaultSettingsMerged) {
$this->mergeDefaults();
}
return $this->settings;
}
/**
* Implements Drupal\field\Plugin\PluginSettingsInterface::getSetting().
*/
public function getSetting($key) {
// Merge defaults if we have no value for the key.
if (!$this->defaultSettingsMerged && !array_key_exists($key, $this->settings)) {
$this->mergeDefaults();
}
return isset($this->settings[$key]) ? $this->settings[$key] : NULL;
}
/**
* Merges default settings values into $settings.
*/
protected function mergeDefaults() {
$this->settings += $this->getDefaultSettings();
$this->defaultSettingsMerged = TRUE;
}
/**
* Implements Drupal\field\Plugin\PluginSettingsInterface::getDefaultSettings().
*/
public function getDefaultSettings() {
$definition = $this->getDefinition();
return $definition['settings'];
}
/**
* Implements Drupal\field\Plugin\PluginSettingsInterface::setSettings().
*/
public function setSettings(array $settings) {
$this->settings = $settings;
$this->defaultSettingsMerged = FALSE;
return $this;
}
/**
* Implements Drupal\field\Plugin\PluginSettingsInterface::setSetting().
*/
public function setSetting($key, $value) {
$this->settings[$key] = $value;
return $this;
}
}
<?php
/**
* @file
* Definition of Drupal\field\Plugin\PluginSettingsInterface.
*/
namespace Drupal\field\Plugin;
use Drupal\Component\Plugin\PluginInspectionInterface;
/**
* Interface definition for plugin with settings.
*/
interface PluginSettingsInterface extends PluginInspectionInterface {
/**
* Returns the array of settings, including defaults for missing settings.
*
* @return array
* The array of settings.
*/
public function getSettings();
/**
* Returns the value of a setting, or its default value if absent.
*
* @param string $key
* The setting name.
*
* @return mixed
* The setting value.
*/
public function getSetting($key);
/**
* Returns the default settings for the plugin.
*
* @return array
* The array of default setting values, keyed by setting names.
*/
public function getDefaultSettings();
/**
* Sets the settings for the plugin.
*
* @param array $settings
* The array of settings, keyed by setting names. Missing settings will be
* assigned their default values.
*
* @return Drupal\field\Plugin\PluginSettingsInterface
* The plugin itself.
*/
public function setSettings(array $settings);
/**
* Sets the value of a setting for the plugin.
*
* @param string $key
* The setting name.
* @param mixed $value
* The setting value.
*
* @return Drupal\field\Plugin\PluginSettingsInterface
* The plugin itself.
*/
public function setSetting($key, $value);
}
<?php
/**
* @file
* Definition of Drupal\field\Plugin\Type\LegacyDiscoveryDecorator.
*/
namespace Drupal\field\Plugin\Type;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
use Drupal\Core\Plugin\Discovery\HookDiscovery;
/**
* Custom decorator to add legacy widgets.
*
* Legacy widgets are discovered through the old hook_field_widget_info() hook,
* and handled by the Drupal\field\Plugin\field\widget\LegacyWidget class.
*
* @todo Remove once all core widgets have been converted.
*/
class LegacyDiscoveryDecorator implements DiscoveryInterface {
/**
* The decorated discovery object.
*
* @var Drupal\Component\Plugin\Discovery\DiscoveryInterface
*/
protected $decorated;
/**