diff options
Diffstat (limited to 'core')
76 files changed, 1458 insertions, 389 deletions
diff --git a/core/.phpstan-baseline.php b/core/.phpstan-baseline.php index 07367cd8a2d0..e0660c7dc93e 100644 --- a/core/.phpstan-baseline.php +++ b/core/.phpstan-baseline.php @@ -9716,12 +9716,6 @@ $ignoreErrors[] = [ 'path' => __DIR__ . '/lib/Drupal/Core/Render/Element/Weight.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\Core\\\\Render\\\\ElementInfoManager\\:\\:clearCachedDefinitions\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/lib/Drupal/Core/Render/ElementInfoManager.php', -]; -$ignoreErrors[] = [ 'message' => '#^Method Drupal\\\\Core\\\\Render\\\\HtmlResponseAttachmentsProcessor\\:\\:renderHtmlResponseAttachmentPlaceholders\\(\\) has no return type specified\\.$#', 'identifier' => 'missingType.return', 'count' => 1, diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt index 91d836c34659..648959a56b32 100644 --- a/core/MAINTAINERS.txt +++ b/core/MAINTAINERS.txt @@ -120,14 +120,12 @@ Cache - Kristiaan Van den Eynde 'kristiaanvandeneynde' https://www.drupal.org/u/kristiaanvandeneynde CKEditor 5 -- Lauri Timmanee 'lauriii' https://www.drupal.org/u/lauriii - Wim Leers 'Wim Leers' https://www.drupal.org/u/wim-leers - Ben Mullins 'bnjmnm' https://www.drupal.org/u/bnjmnm Claro - Cristina Chumillas 'ckrina' https://www.drupal.org/u/ckrina - Sascha Eggenberger 'saschaeggi' https://www.drupal.org/u/saschaeggi -- Lauri Timmanee 'lauriii' https://www.drupal.org/u/lauriii - Ben Mullins 'bnjmnm' https://www.drupal.org/u/bnjmnm Comment @@ -260,7 +258,6 @@ JavaScript JSON:API - Mateu Aguiló Bosch 'e0ipso' https://www.drupal.org/u/e0ipso - Björn Brala 'bbrala' https://www.drupal.org/u/bbrala -- Gabe Sullice 'gabesullice' https://www.drupal.org/u/gabesullice - Wim Leers 'Wim Leers' https://www.drupal.org/u/wim-leers Language diff --git a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php index 132aaa439974..11422c6790c9 100644 --- a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php +++ b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php @@ -19,29 +19,38 @@ use Drupal\Core\Site\Settings; * entities, which can come from all or specific bundles of an entity type. * * Properties: - * - #target_type: (required) The ID of the target entity type. - * - #tags: (optional) TRUE if the element allows multiple selection. Defaults + * + * @property $target_type + * (required) The ID of the target entity type. + * @property $tags + * (optional) TRUE if the element allows multiple selection. Defaults * to FALSE. - * - #default_value: (optional) The default entity or an array of default + * @property $default_value + * (optional) The default entity or an array of default * entities, depending on the value of #tags. - * - #selection_handler: (optional) The plugin ID of the entity reference + * @property $selection_handler + * (optional) The plugin ID of the entity reference * selection handler (a plugin of type EntityReferenceSelection). The default * value is the lowest-weighted plugin that is compatible with #target_type. - * - #selection_settings: (optional) An array of settings for the selection + * @property $selection_settings + * (optional) An array of settings for the selection * handler. Settings for the default selection handler * \Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection are: * - target_bundles: Array of bundles to allow (omit to allow all bundles). * - sort: Array with 'field' and 'direction' keys, determining how results * will be sorted. Defaults to unsorted. - * - #autocreate: (optional) Array of settings used to auto-create entities + * @property $autocreate + * (optional) Array of settings used to auto-create entities * that do not exist (omit to not auto-create entities). Elements: * - bundle: (required) Bundle to use for auto-created entities. * - uid: User ID to use as the author of auto-created entities. Defaults to * the current user. - * - #process_default_value: (optional) Set to FALSE if the #default_value + * @property $process_default_value + * (optional) Set to FALSE if the #default_value * property is processed and access checked elsewhere (such as by a Field API * widget). Defaults to TRUE. - * - #validate_reference: (optional) Set to FALSE if validation of the selected + * @property $validate_reference + * (optional) Set to FALSE if validation of the selected * entities is performed elsewhere. Defaults to TRUE. * * Usage example: diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/OptionsWidgetBase.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/OptionsWidgetBase.php index 60920fad60f5..2a3e3657354e 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/OptionsWidgetBase.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/OptionsWidgetBase.php @@ -9,6 +9,7 @@ use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\WidgetBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\OptGroup; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; /** @@ -56,8 +57,8 @@ abstract class OptionsWidgetBase extends WidgetBase { /** * {@inheritdoc} */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings) { - parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ?ElementInfoManagerInterface $elementInfoManager = NULL) { + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $elementInfoManager); $property_names = $this->fieldDefinition->getFieldStorageDefinition()->getPropertyNames(); $this->column = $property_names[0]; } diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/StringTextfieldWidget.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/StringTextfieldWidget.php index 1523e4f618f7..2f8893243111 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/StringTextfieldWidget.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/StringTextfieldWidget.php @@ -6,6 +6,9 @@ use Drupal\Core\Field\Attribute\FieldWidget; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\WidgetBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element\ElementInterface; +use Drupal\Core\Render\Element\Textfield; +use Drupal\Core\Render\Element\Widget; use Drupal\Core\StringTranslation\TranslatableMarkup; /** @@ -66,17 +69,14 @@ class StringTextfieldWidget extends WidgetBase { /** * {@inheritdoc} */ - public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { - $element['value'] = $element + [ - '#type' => 'textfield', - '#default_value' => $items[$delta]->value ?? NULL, - '#size' => $this->getSetting('size'), - '#placeholder' => $this->getSetting('placeholder'), - '#maxlength' => $this->getFieldSetting('max_length'), - '#attributes' => ['class' => ['js-text-full', 'text-full']], - ]; - - return $element; + public function singleElementObject(FieldItemListInterface $items, $delta, Widget $widget, ElementInterface $form, FormStateInterface $form_state): ElementInterface { + $value = $widget->createChild('value', Textfield::class, copyProperties: TRUE); + $value->default_value = $items[$delta]->value ?? NULL; + $value->size = $this->getSetting('size'); + $value->placeholder = $this->getSetting('placeholder'); + $value->maxlength = $this->getFieldSetting('max_length'); + $value->attributes = ['class' => ['js-text-full', 'text-full']]; + return $widget; } } diff --git a/core/lib/Drupal/Core/Field/WidgetBase.php b/core/lib/Drupal/Core/Field/WidgetBase.php index 5c3da0a1bf59..b6d7a283830e 100644 --- a/core/lib/Drupal/Core/Field/WidgetBase.php +++ b/core/lib/Drupal/Core/Field/WidgetBase.php @@ -11,6 +11,9 @@ use Drupal\Core\Ajax\InsertCommand; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Render\Element; +use Drupal\Core\Render\Element\ElementInterface; +use Drupal\Core\Render\Element\Widget; +use Drupal\Core\Render\ElementInfoManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\ConstraintViolationListInterface; @@ -49,19 +52,32 @@ abstract class WidgetBase extends PluginSettingsBase implements WidgetInterface, * The widget settings. * @param array $third_party_settings * Any third party settings. + * @param \Drupal\Core\Render\ElementInfoManagerInterface $elementInfoManager + * The element info manager. */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings) { + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, protected ?ElementInfoManagerInterface $elementInfoManager = NULL) { parent::__construct([], $plugin_id, $plugin_definition); $this->fieldDefinition = $field_definition; $this->settings = $settings; $this->thirdPartySettings = $third_party_settings; + if (!$this->elementInfoManager) { + @trigger_error('Calling ' . __METHOD__ . '() without the $elementInfoManager argument is deprecated in drupal:11.3.0 and it will be required in drupal:12.0.0. See https://www.drupal.org/node/3526683', E_USER_DEPRECATED); + $this->elementInfoManager = \Drupal::service('plugin.manager.element_info'); + } } /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static($plugin_id, $plugin_definition, $configuration['field_definition'], $configuration['settings'], $configuration['third_party_settings']); + return new static( + $plugin_id, + $plugin_definition, + $configuration['field_definition'], + $configuration['settings'], + $configuration['third_party_settings'], + $container->get('plugin.manager.element_info'), + ); } /** @@ -461,7 +477,9 @@ abstract class WidgetBase extends PluginSettingsBase implements WidgetInterface, '#weight' => $delta, ]; - $element = $this->formElement($items, $delta, $element, $form, $form_state); + $formObject = $this->elementInfoManager->fromRenderable($form); + $widget = $this->elementInfoManager->fromRenderable($element, Widget::class); + $element = $this->singleElementObject($items, $delta, $widget, $formObject, $form_state)->toRenderable(); if ($element) { // Allow modules to alter the field widget form element. @@ -484,6 +502,21 @@ abstract class WidgetBase extends PluginSettingsBase implements WidgetInterface, /** * {@inheritdoc} */ + public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { + return $element; + } + + /** + * {@inheritdoc} + */ + public function singleElementObject(FieldItemListInterface $items, $delta, Widget $widget, ElementInterface $form, FormStateInterface $form_state): ElementInterface { + $element = $this->formElement($items, $delta, $widget->toRenderable(), $form->toRenderable(), $form_state); + return $this->elementInfoManager->fromRenderable($element); + } + + /** + * {@inheritdoc} + */ public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) { $field_name = $this->fieldDefinition->getName(); diff --git a/core/lib/Drupal/Core/Field/WidgetInterface.php b/core/lib/Drupal/Core/Field/WidgetInterface.php index ab78308291bd..9107bd437f74 100644 --- a/core/lib/Drupal/Core/Field/WidgetInterface.php +++ b/core/lib/Drupal/Core/Field/WidgetInterface.php @@ -3,15 +3,17 @@ namespace Drupal\Core\Field; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element\ElementInterface; +use Drupal\Core\Render\Element\Widget; use Symfony\Component\Validator\ConstraintViolationInterface; /** * Interface definition for field widget plugins. * * This interface details the methods that most plugin implementations will want - * to override. See Drupal\Core\Field\WidgetBaseInterface for base + * to override. See \Drupal\Core\Field\WidgetBaseInterface for base * wrapping methods that should most likely be inherited directly from - * Drupal\Core\Field\WidgetBase.. + * \Drupal\Core\Field\WidgetBase. * * @ingroup field_widget */ @@ -104,6 +106,51 @@ interface WidgetInterface extends WidgetBaseInterface { public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state); /** + * Returns the form for a single field widget. + * + * Field widget form elements should be based on the passed-in $element, which + * contains the base form element properties derived from the field + * configuration. + * + * The BaseWidget methods will set the weight, field name and delta values for + * each form element. If there are multiple values for this field, the + * formElement() method will be called as many times as needed. + * + * Other modules may alter the form element provided by this function using + * hook_field_widget_single_element_form_alter() or + * hook_field_widget_single_element_WIDGET_TYPE_form_alter(). + * + * The FAPI element callbacks (such as #process, #element_validate, + * #value_callback, etc.) used by the widget do not have access to the + * original $field_definition passed to the widget's constructor. Therefore, + * if any information is needed from that definition by those callbacks, the + * widget implementing this method, or a + * hook_field_widget[_WIDGET_TYPE]_form_alter() implementation, must extract + * the needed properties from the field definition and set them as ad-hoc + * $element['#custom'] properties, for later use by its element callbacks. + * + * @param \Drupal\Core\Field\FieldItemListInterface $items + * Array of default values for this field. + * @param int $delta + * The order of this item in the array of sub-elements (0, 1, 2, etc.). + * @param \Drupal\Core\Render\Element\Widget $widget + * A widget element. + * @param \Drupal\Core\Render\Element\ElementInterface $form + * The form structure where widgets are being attached to. This might be a + * full form structure, or a sub-element of a larger form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return \Drupal\Core\Render\Element\ElementInterface + * The wrapper object. Some widgets need to change the type of it so the + * returned object might not be a Wrapper object. + * + * @see hook_field_widget_single_element_form_alter() + * @see hook_field_widget_single_element_WIDGET_TYPE_form_alter() + */ + public function singleElementObject(FieldItemListInterface $items, $delta, Widget $widget, ElementInterface $form, FormStateInterface $form_state): ElementInterface; + + /** * Assigns a field-level validation error to the right widget sub-element. * * Depending on the widget's internal structure, a field-level validation diff --git a/core/lib/Drupal/Core/Form/FormBase.php b/core/lib/Drupal/Core/Form/FormBase.php index d88810943acd..44ca953ab077 100644 --- a/core/lib/Drupal/Core/Form/FormBase.php +++ b/core/lib/Drupal/Core/Form/FormBase.php @@ -6,6 +6,7 @@ use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\DependencyInjection\DependencySerializationTrait; use Drupal\Core\Logger\LoggerChannelTrait; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\Routing\RedirectDestinationTrait; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Url; @@ -76,6 +77,13 @@ abstract class FormBase implements FormInterface, ContainerInjectionInterface { protected $routeMatch; /** + * The element info manager. + * + * @var \Drupal\Core\Render\ElementInfoManagerInterface + */ + protected ElementInfoManagerInterface $elementInfoManager; + + /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { @@ -126,6 +134,29 @@ abstract class FormBase implements FormInterface, ContainerInjectionInterface { } /** + * The element info manager. + * + * @return \Drupal\Core\Render\ElementInfoManagerInterface + * The element info manager. + */ + protected function elementInfoManager(): ElementInfoManagerInterface { + if (!isset($this->elementInfoManager)) { + $this->elementInfoManager = $this->container()->get('plugin.manager.element_info'); + } + return $this->elementInfoManager; + } + + /** + * Sets the element info manager for this form. + * + * @return $this + */ + public function setElementInfoManager(ElementInfoManagerInterface $elementInfoManager): static { + $this->elementInfoManager = $elementInfoManager; + return $this; + } + + /** * Sets the config factory for this form. * * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory diff --git a/core/lib/Drupal/Core/Render/Element/Button.php b/core/lib/Drupal/Core/Render/Element/Button.php index a8f12e939636..95b78d9c4907 100644 --- a/core/lib/Drupal/Core/Render/Element/Button.php +++ b/core/lib/Drupal/Core/Render/Element/Button.php @@ -18,11 +18,15 @@ use Drupal\Core\Render\Element; * using JavaScript or other mechanisms. * * Properties: - * - #limit_validation_errors: An array of form element keys that will block + * + * @property $limit_validation_errors + * An array of form element keys that will block * form submission when validation for these elements or any child elements * fails. Specify an empty array to suppress all form validation errors. - * - #value: The text to be shown on the button. - * - #submit_button: This has a default value of TRUE. If set to FALSE, the + * @property $value + * The text to be shown on the button. + * @property $submit_button + * This has a default value of TRUE. If set to FALSE, the * 'type' attribute is set to 'button.' * * diff --git a/core/lib/Drupal/Core/Render/Element/Checkbox.php b/core/lib/Drupal/Core/Render/Element/Checkbox.php index 65be5d22bf10..220d1c8f9693 100644 --- a/core/lib/Drupal/Core/Render/Element/Checkbox.php +++ b/core/lib/Drupal/Core/Render/Element/Checkbox.php @@ -10,7 +10,9 @@ use Drupal\Core\Render\Element; * Provides a form element for a single checkbox. * * Properties: - * - #return_value: The value to return when the checkbox is checked. + * + * @property $return_value + * The value to return when the checkbox is checked. * * Usage example: * @code diff --git a/core/lib/Drupal/Core/Render/Element/Checkboxes.php b/core/lib/Drupal/Core/Render/Element/Checkboxes.php index 234f25aa9044..c713cc9cb531 100644 --- a/core/lib/Drupal/Core/Render/Element/Checkboxes.php +++ b/core/lib/Drupal/Core/Render/Element/Checkboxes.php @@ -9,7 +9,9 @@ use Drupal\Core\Render\Attribute\FormElement; * Provides a form element for a set of checkboxes. * * Properties: - * - #options: An associative array whose keys are the values returned for each + * + * @property $options + * An associative array whose keys are the values returned for each * checkbox, and whose values are the labels next to each checkbox. The * #options array cannot have a 0 key, as it would not be possible to discern * checked and unchecked states. diff --git a/core/lib/Drupal/Core/Render/Element/Color.php b/core/lib/Drupal/Core/Render/Element/Color.php index 254300d976f9..616ed9dbe08e 100644 --- a/core/lib/Drupal/Core/Render/Element/Color.php +++ b/core/lib/Drupal/Core/Render/Element/Color.php @@ -11,7 +11,9 @@ use Drupal\Component\Utility\Color as ColorUtility; * Provides a form element for choosing a color. * * Properties: - * - #default_value: Default value, in a format like #ffffff. + * + * @property $default_value + * Default value, in a format like #ffffff. * * Example usage: * @code diff --git a/core/lib/Drupal/Core/Render/Element/ComponentElement.php b/core/lib/Drupal/Core/Render/Element/ComponentElement.php index 62db902c0681..befd6aa269b6 100644 --- a/core/lib/Drupal/Core/Render/Element/ComponentElement.php +++ b/core/lib/Drupal/Core/Render/Element/ComponentElement.php @@ -11,11 +11,16 @@ use Drupal\Core\Security\DoTrustedCallbackTrait; * Provides a Single-Directory Component render element. * * Properties: - * - #component: The machine name of the component. - * - #variant: (optional) The variant to be used for the component. - * - #props: an associative array where the keys are the names of the + * + * @property $component + * The machine name of the component. + * @property $variant + * (optional) The variant to be used for the component. + * @property $props + * an associative array where the keys are the names of the * component props, and the values are the prop values. - * - #slots: an associative array where the keys are the slot names, and the + * @property $slots + * an associative array where the keys are the slot names, and the * values are the slot values. Expected slot values are renderable arrays. * - #propsAlter: an array of trusted callbacks. These are used to prepare the * context. Typical uses include replacing tokens in props. diff --git a/core/lib/Drupal/Core/Render/Element/Container.php b/core/lib/Drupal/Core/Render/Element/Container.php index d5a2092718e7..105f1413efdd 100644 --- a/core/lib/Drupal/Core/Render/Element/Container.php +++ b/core/lib/Drupal/Core/Render/Element/Container.php @@ -14,7 +14,9 @@ use Drupal\Core\Render\Element; * an HTML ID. * * Properties: - * - #optional: Indicates whether the container should render when it has no + * + * @property $optional + * Indicates whether the container should render when it has no * visible children. Defaults to FALSE. * * Usage example: diff --git a/core/lib/Drupal/Core/Render/Element/Date.php b/core/lib/Drupal/Core/Render/Element/Date.php index 8304167ae707..fbaeae4d600c 100644 --- a/core/lib/Drupal/Core/Render/Element/Date.php +++ b/core/lib/Drupal/Core/Render/Element/Date.php @@ -9,13 +9,18 @@ use Drupal\Core\Render\Element; * Provides a form element for date or time selection. * * Properties: - * - #attributes: An associative array containing: + * + * @property $attributes + * An associative array containing: * - type: The type of date field rendered, valid values include 'date', * 'time', 'datetime', and 'datetime-local'. - * - #date_date_format: The date format used in PHP formats. - * - #default_value: A string representing the date formatted as Y-m-d, or + * @property $date_date_format + * The date format used in PHP formats. + * @property $default_value + * A string representing the date formatted as Y-m-d, or * hh:mm for time. - * - #size: The size of the input element in characters. + * @property $size + * The size of the input element in characters. * * @code * $form['expiration'] = [ diff --git a/core/lib/Drupal/Core/Render/Element/Details.php b/core/lib/Drupal/Core/Render/Element/Details.php index 28e7396887d8..99a982727043 100644 --- a/core/lib/Drupal/Core/Render/Element/Details.php +++ b/core/lib/Drupal/Core/Render/Element/Details.php @@ -13,10 +13,14 @@ use Drupal\Core\Render\Element; * element, showing or hiding the contained elements. * * Properties: - * - #title: The title of the details container. Defaults to "Details". - * - #open: Indicates whether the container should be open by default. + * + * @property $title + * The title of the details container. Defaults to "Details". + * @property $open + * Indicates whether the container should be open by default. * Defaults to FALSE. - * - #summary_attributes: An array of attributes to apply to the <summary> + * @property $summary_attributes + * An array of attributes to apply to the <summary> * element. * * Usage example: diff --git a/core/lib/Drupal/Core/Render/Element/Dropbutton.php b/core/lib/Drupal/Core/Render/Element/Dropbutton.php index 20bb375ea879..4ac2d6dda8a5 100644 --- a/core/lib/Drupal/Core/Render/Element/Dropbutton.php +++ b/core/lib/Drupal/Core/Render/Element/Dropbutton.php @@ -17,9 +17,12 @@ use Drupal\Core\Render\Attribute\RenderElement; * element property #links to provide $variables['links'] for theming. * * Properties: - * - #links: An array of links to actions. See template_preprocess_links() for + * + * @property $links + * An array of links to actions. See template_preprocess_links() for * documentation the properties of links in this array. - * - #dropbutton_type: A string defining a type of dropbutton variant for + * @property $dropbutton_type + * A string defining a type of dropbutton variant for * styling proposes. Renders as class `dropbutton--#dropbutton_type`. * * Usage Example: diff --git a/core/lib/Drupal/Core/Render/Element/ElementInterface.php b/core/lib/Drupal/Core/Render/Element/ElementInterface.php index f7debae9efd4..dae671666c3a 100644 --- a/core/lib/Drupal/Core/Render/Element/ElementInterface.php +++ b/core/lib/Drupal/Core/Render/Element/ElementInterface.php @@ -41,6 +41,22 @@ interface ElementInterface extends PluginInspectionInterface, RenderCallbackInte public function getInfo(); /** + * Initialize storage. + * + * This will only have an effect the first time it is called, once it has + * been called, subsequent calls will not have an effect. + * Only the plugin manager should ever call this method. + * + * @param array $element + * The containing element. + * + * @return $this + * + * @internal + */ + public function initializeInternalStorage(array &$element): static; + + /** * Sets a form element's class attribute. * * Adds 'required' and 'error' classes as needed. @@ -52,4 +68,101 @@ interface ElementInterface extends PluginInspectionInterface, RenderCallbackInte */ public static function setAttributes(&$element, $class = []); + /** + * Returns a render array. + * + * @param string|null $wrapper_key + * An optional wrapper. + * + * @return array|\Drupal\Core\Render\Element\ElementInterface + * A render array. Make sure to take the return value as a reference. + * If $wrapper_key is not given then the stored render element is returned. + * If $wrapper_key is given then [$wrapper_key => &$element] is returned. + * The return value is typed with array|ElementInterface to prepare for + * Drupal 12, where the plan for this method is to return an + * ElementInterface object. If that plan goes through then in Drupal 13 + * support for render arrays will be dropped. + */ + public function &toRenderable(?string $wrapper_key = NULL): array|ElementInterface; + + /** + * Returns child elements. + * + * @return \Traversable<\Drupal\Core\Render\Element\ElementInterface> + * Keys will be children names, values are render objects. + */ + public function getChildren(): \Traversable; + + /** + * Gets a child. + * + * @param int|string|list<int|string> $name + * The name of the child. Can also be an integer. Or a list of these. + * It is an integer when the field API uses the delta for children. + * + * @return ?\Drupal\Core\Render\Element\ElementInterface + * The child render object. + */ + public function getChild(int|string|array $name): ?ElementInterface; + + /** + * Adds a child render element. + * + * @param int|string $name + * The name of the child. Can also be an integer when the child is a delta. + * @param array|\Drupal\Core\Render\Element\ElementInterface $child + * A render array or a render object. + * + * @return \Drupal\Core\Render\Element\ElementInterface + * The added child as a render object. + */ + public function addChild(int|string $name, ElementInterface|array &$child): ElementInterface; + + /** + * Creates a render object and attaches it to the current render object. + * + * @param int|string $name + * The name of the child. Can also be an integer. + * @param class-string<T> $class + * The class of the render object. + * @param array $configuration + * An array of configuration relevant to the render object. + * @param bool $copyProperties + * Copy properties (but not children) from the parent. This is useful for + * widgets for example. + * + * @return T + * The child render object. + * + * @template T of \Drupal\Core\Render\Element\ElementInterface + */ + public function createChild(int|string $name, string $class, array $configuration = [], bool $copyProperties = FALSE): ElementInterface; + + /** + * Removes a child. + * + * @param int|string $name + * The name of the child. Can also be an integer. + * + * @return ?\Drupal\Core\Render\Element\ElementInterface + * The removed render object if any, or NULL if the child could not be + * found. + */ + public function removeChild(int|string $name): ?ElementInterface; + + /** + * Change the type of the element. + * + * Changes only the #type all other properties and children are preserved. + * + * @param class-string<T> $class + * The class of the new render object. + * + * @return T + * The new render object. + * + * @template T of \Drupal\Core\Render\Element\ElementInterface + */ + public function changeType(string $class): ElementInterface; + } diff --git a/core/lib/Drupal/Core/Render/Element/Email.php b/core/lib/Drupal/Core/Render/Element/Email.php index 82e688ce38e2..debb2e10f446 100644 --- a/core/lib/Drupal/Core/Render/Element/Email.php +++ b/core/lib/Drupal/Core/Render/Element/Email.php @@ -10,9 +10,13 @@ use Drupal\Core\Render\Element; * Provides a form input element for entering an email address. * * Properties: - * - #default_value: An RFC-compliant email address. - * - #size: The size of the input element in characters. - * - #pattern: A string for the native HTML5 pattern attribute. + * + * @property $default_value + * An RFC-compliant email address. + * @property $size + * The size of the input element in characters. + * @property $pattern + * A string for the native HTML5 pattern attribute. * * Example usage: * @code diff --git a/core/lib/Drupal/Core/Render/Element/File.php b/core/lib/Drupal/Core/Render/Element/File.php index df50492a9ec4..9336eda06960 100644 --- a/core/lib/Drupal/Core/Render/Element/File.php +++ b/core/lib/Drupal/Core/Render/Element/File.php @@ -13,8 +13,11 @@ use Drupal\Core\Render\Element; * will automatically be added to the form element. * * Properties: - * - #multiple: A Boolean indicating whether multiple files may be uploaded. - * - #size: The size of the file input element in characters. + * + * @property $multiple + * A Boolean indicating whether multiple files may be uploaded. + * @property $size + * The size of the file input element in characters. * * The value of this form element will always be an array of * \Symfony\Component\HttpFoundation\File\UploadedFile objects, regardless of diff --git a/core/lib/Drupal/Core/Render/Element/FormElement.php b/core/lib/Drupal/Core/Render/Element/FormElement.php index 14ab865dd3b1..e991e7e7aaca 100644 --- a/core/lib/Drupal/Core/Render/Element/FormElement.php +++ b/core/lib/Drupal/Core/Render/Element/FormElement.php @@ -18,7 +18,8 @@ abstract class FormElement extends FormElementBase { * {@inheritdoc} */ public function __construct(array $configuration, $plugin_id, $plugin_definition) { - parent::__construct($configuration, $plugin_id, $plugin_definition); + $elementInfoManager = \Drupal::service('plugin.manager.element_info'); + parent::__construct($configuration, $plugin_id, $plugin_definition, $elementInfoManager); @trigger_error('\Drupal\Core\Render\Element\FormElement is deprecated in drupal:10.3.0 and is removed from drupal:12.0.0. Use \Drupal\Core\Render\Element\FormElementBase instead. See https://www.drupal.org/node/3436275', E_USER_DEPRECATED); } diff --git a/core/lib/Drupal/Core/Render/Element/FormElementBase.php b/core/lib/Drupal/Core/Render/Element/FormElementBase.php index 581607d716af..320ba9a75a1a 100644 --- a/core/lib/Drupal/Core/Render/Element/FormElementBase.php +++ b/core/lib/Drupal/Core/Render/Element/FormElementBase.php @@ -11,6 +11,15 @@ use Drupal\Core\Url; * * Form elements are a subset of render elements, representing elements for * HTML forms, which can be referenced in form arrays. See the + * + * @see \Drupal\Core\Render\Attribute\FormElement + * @see \Drupal\Core\Render\Element\FormElementInterface + * @see \Drupal\Core\Render\ElementInfoManager + * @see \Drupal\Core\Render\Element\RenderElementBase + * @see plugin_api + * + * @ingroup theme_render + * @see \Drupal\Core\Form\FormHelper::processStates() * @link theme_render Render API topic @endlink for an overview of render * arrays and render elements, and the @link form_api Form API topic @endlink * for an overview of forms and form arrays. @@ -30,69 +39,75 @@ use Drupal\Core\Url; * processing of form elements, besides those properties documented in * \Drupal\Core\Render\Element\RenderElementBase (for example: #prefix, * #suffix): - * - #after_build: (array) Array of callables or function names, which are - * called after the element is built. Arguments: $element, $form_state. - * - #ajax: (array) Array of elements to specify Ajax behavior. See - * the @link ajax Ajax API topic @endlink for more information. - * - #array_parents: (string[], read-only) Array of names of all the element's - * parents (including itself) in the render array. See also #parents, #tree. - * - #default_value: Default value for the element. See also #value. - * - #description: (string) Help or description text for the element. In an - * ideal user interface, the #title should be enough to describe the element, - * so most elements should not have a description; if you do need one, make - * sure it is translated. If it is not already wrapped in a safe markup - * object, it will be filtered for XSS safety. - * - #disabled: (bool) If TRUE, the element is shown but does not accept - * user input. - * - #element_validate: (array) Array of callables or function names, which - * are called to validate the input. Arguments: $element, $form_state, $form. - * - #field_prefix: (string) Prefix to display before the HTML input element. - * Should be translated, normally. If it is not already wrapped in a safe - * markup object, will be filtered for XSS safety. Note that the contents of - * this prefix are wrapped in a <span> element, so the value should not - * contain block level HTML. Any HTML added must be valid, i.e. any tags - * introduced inside this prefix must also be terminated within the prefix. - * - #field_suffix: (string) Suffix to display after the HTML input element. - * Should be translated, normally. If it is not already wrapped in a safe - * markup object, will be filtered for XSS safety. Note that the contents of - * this suffix are wrapped in a <span> element, so the value should not - * contain block level HTML. Any HTML must also be valid, i.e. any tags - * introduce inside this suffix must also be terminated within the suffix. - * - #value: (mixed) A value that cannot be edited by the user. - * - #has_garbage_value: (bool) Internal only. Set to TRUE to indicate that the - * #value property of an element should not be used or processed. - * - #input: (bool, internal) Whether or not the element accepts input. - * - #parents: (string[], read-only) Array of names of the element's parents - * for purposes of getting values out of $form_state. See also - * #array_parents, #tree. - * - #process: (array) Array of callables or function names, which are - * called during form building. Arguments: $element, $form_state, $form. - * - #processed: (bool, internal) Set to TRUE when the element is processed. - * - #required: (bool) Whether or not input is required on the element. - * - #states: (array) Information about JavaScript states, such as when to - * hide or show the element based on input on other elements. - * See \Drupal\Core\Form\FormHelper::processStates() for documentation. - * - #title: (string) Title of the form element. Should be translated. - * - #title_display: (string) Where and how to display the #title. Possible - * values: - * - before: Label goes before the element (default for most elements). - * - after: Label goes after the element (default for radio elements). - * - invisible: Label is there but is made invisible using CSS. - * - attribute: Make it the title attribute (hover tooltip). - * - #tree: (bool) TRUE if the values of this element and its children should - * be hierarchical in $form_state; FALSE if the values should be flat. - * See also #parents, #array_parents. - * - #value_callback: (callable) Callable or function name, which is called - * to transform the raw user input to the element's value. Arguments: - * $element, $input, $form_state. - * - * @see \Drupal\Core\Render\Attribute\FormElement - * @see \Drupal\Core\Render\Element\FormElementInterface - * @see \Drupal\Core\Render\ElementInfoManager - * @see \Drupal\Core\Render\Element\RenderElementBase - * @see plugin_api - * - * @ingroup theme_render + * @property array $after_build + * Array of callables or function names, which are called after the element + * is built. Arguments: $element, $form_state. + * @property array $ajax + * Array of elements to specify Ajax behavior. See the @link ajax Ajax API + * topic @endlink for more information. + * @property array<string> $array_parents + * Array of names of all the element's parents (including itself) in the + * render array. See also #parents, #tree. + * @property mixed $default_value + * Default value for the element. See also #value. + * @property scalar|\Stringable|\Drupal\Core\Render\RenderableInterface|array $description + * Help or description text for the element. In an ideal user interface, + * the #title should be enough to describe the element, so most elements + * should not have a description; if you do need one, make sure it is + * translated. It can be anything that Twig can print and will be filtered + * for XSS as necessary. + * @property bool $disabled + * If TRUE, the element is shown but does not accept user input. + * @property array<callable> $element_validate + * Array of callables or function names, which are called to validate the + * input. Arguments: $element, $form_state, $form. + * @property string $field_prefix + * Prefix to display before the HTML input element. Should be translated, + * normally. If it is not already wrapped in a safe markup object, will be + * filtered for XSS safety. Note that the contents of this prefix are + * wrapped in a <span> element, so the value should not contain block level + * HTML. Any HTML added must be valid, i.e. any tags introduced inside this + * prefix must also be terminated within the prefix. + * @property string $field_suffix + * Suffix to display after the HTML input element. Should be translated, + * normally. If it is not already wrapped in a safe markup object, will be + * filtered for XSS safety. Note that the contents of this suffix are + * wrapped in a <span> element, so the value should not contain block + * level HTML. Any HTML must also be valid, i.e. any tags introduce inside + * this suffix must also be terminated within the suffix. + * @property mixed $value + * A value that cannot be edited by the user. + * @property bool $has_garbage_value + * @internal + * Set to TRUE to indicate that the #value property of an + * element should not be used or processed. + * @property bool $input + * @internal + * Whether the element accepts input. + * @property array<string> $parents + * Array of names of the element's parents for purposes of getting values + * out of $form_state. See also #array_parents, #tree. + * @property array $process + * Array of callables or function names, which are called during form + * building. Arguments: $element, $form_state, $form. + * @property bool, internal $processed + * Set to TRUE when the element is processed. + * @property bool $required + * Whether input is required on the element. + * @property array $states + * Information about JavaScript states, such as when to hide or show the + * element based on input on other elements. + * @property string $title + * Title of the form element. Should be translated. + * @property \Drupal\Core\Render\Element\TitleDisplay $title_display + * Where and how to display the #title. + * @property bool $tree + * TRUE if the values of this element and its children should be hierarchical + * in $form_state; FALSE if the values should be flat. See also #parents, + * #array_parents. + * @property callable $value_callback + * Callable or function name, which is called to transform the raw user + * input to the element's value. Arguments: $element, $input, $form_state. */ abstract class FormElementBase extends RenderElementBase implements FormElementInterface { diff --git a/core/lib/Drupal/Core/Render/Element/Generic.php b/core/lib/Drupal/Core/Render/Element/Generic.php new file mode 100644 index 000000000000..4a0b6f09ebd9 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Generic.php @@ -0,0 +1,31 @@ +<?php + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Render\Attribute\RenderElement; + +/** + * Provides a generic, empty element. + * + * Manually creating this element is not necessary; however, the system + * often needs to convert render arrays that do not have a type. While + * arrays without a #type are valid PHP code, it is not possible to create + * an object without a class. + */ +#[RenderElement('generic')] +class Generic extends RenderElementBase { + + /** + * {@inheritdoc} + */ + public function getInfo() { + return []; + } + + /** + * {@inheritdoc} + */ + protected function setType(): void { + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Hidden.php b/core/lib/Drupal/Core/Render/Element/Hidden.php index db3b23a08cce..1c8856143e68 100644 --- a/core/lib/Drupal/Core/Render/Element/Hidden.php +++ b/core/lib/Drupal/Core/Render/Element/Hidden.php @@ -11,9 +11,12 @@ use Drupal\Core\Render\Element; * Specify either #default_value or #value but not both. * * Properties: - * - #default_value: The initial value of the form element. JavaScript may + * + * @property $default_value + * The initial value of the form element. JavaScript may * alter the value prior to submission. - * - #value: The value of the form element. The Form API ensures that this + * @property $value + * The value of the form element. The Form API ensures that this * value remains unchanged by the browser. * * Usage example: diff --git a/core/lib/Drupal/Core/Render/Element/HtmlTag.php b/core/lib/Drupal/Core/Render/Element/HtmlTag.php index 3b4dbd25d462..277f121df2cf 100644 --- a/core/lib/Drupal/Core/Render/Element/HtmlTag.php +++ b/core/lib/Drupal/Core/Render/Element/HtmlTag.php @@ -13,12 +13,17 @@ use Drupal\Core\Template\Attribute; * Provides a render element for any HTML tag, with properties and value. * * Properties: - * - #tag: The tag name to output. - * - #attributes: (array, optional) HTML attributes to apply to the tag. The + * + * @property $tag + * The tag name to output. + * @property $attributes + * (array, optional) HTML attributes to apply to the tag. The * attributes are escaped, see \Drupal\Core\Template\Attribute. - * - #value: (string|MarkupInterface, optional) The textual contents of the tag. + * @property $value + * (string|MarkupInterface, optional) The textual contents of the tag. * Strings will be XSS admin filtered. - * - #noscript: (bool, optional) When set to TRUE, the markup + * @property $noscript + * (bool, optional) When set to TRUE, the markup * (including any prefix or suffix) will be wrapped in a <noscript> element. * * Usage example: diff --git a/core/lib/Drupal/Core/Render/Element/Icon.php b/core/lib/Drupal/Core/Render/Element/Icon.php index d903f0d17006..e71771044e78 100644 --- a/core/lib/Drupal/Core/Render/Element/Icon.php +++ b/core/lib/Drupal/Core/Render/Element/Icon.php @@ -12,9 +12,13 @@ use Drupal\Core\Theme\Icon\IconDefinition; * Provides a render element to display an icon. * * Properties: - * - #pack_id: (string) Icon Pack provider plugin id. - * - #icon_id: (string) Name of the icon. - * - #settings: (array) Settings sent to the inline Twig template. + * + * @property $pack_id + * (string) Icon Pack provider plugin id. + * @property $icon_id + * (string) Name of the icon. + * @property $settings + * (array) Settings sent to the inline Twig template. * * Usage Example: * @code diff --git a/core/lib/Drupal/Core/Render/Element/InlineTemplate.php b/core/lib/Drupal/Core/Render/Element/InlineTemplate.php index 313bc4f13710..68f5fbf7091d 100644 --- a/core/lib/Drupal/Core/Render/Element/InlineTemplate.php +++ b/core/lib/Drupal/Core/Render/Element/InlineTemplate.php @@ -8,8 +8,11 @@ use Drupal\Core\Render\Attribute\RenderElement; * Provides a render element where the user supplies an in-line Twig template. * * Properties: - * - #template: The inline Twig template used to render the element. - * - #context: (array) The variables to substitute into the Twig template. + * + * @property $template + * The inline Twig template used to render the element. + * @property $context + * (array) The variables to substitute into the Twig template. * Each variable may be a string or a render array. * * Usage example: diff --git a/core/lib/Drupal/Core/Render/Element/Link.php b/core/lib/Drupal/Core/Render/Element/Link.php index 2a00e0526b68..b04704b1681d 100644 --- a/core/lib/Drupal/Core/Render/Element/Link.php +++ b/core/lib/Drupal/Core/Render/Element/Link.php @@ -14,8 +14,11 @@ use Drupal\Core\Url as CoreUrl; * Provides a link render element. * * Properties: - * - #title: The link text. - * - #url: \Drupal\Core\Url object containing URL information pointing to an + * + * @property $title + * The link text. + * @property $url + * \Drupal\Core\Url object containing URL information pointing to an * internal or external link. See \Drupal\Core\Utility\LinkGeneratorInterface. * * Usage example: diff --git a/core/lib/Drupal/Core/Render/Element/MachineName.php b/core/lib/Drupal/Core/Render/Element/MachineName.php index 11647cd02c57..93fbfe5be7f1 100644 --- a/core/lib/Drupal/Core/Render/Element/MachineName.php +++ b/core/lib/Drupal/Core/Render/Element/MachineName.php @@ -21,7 +21,9 @@ use Drupal\Core\Render\Attribute\FormElement; * machine name form element. * * Properties: - * - #machine_name: An associative array containing: + * + * @property $machine_name + * An associative array containing: * - exists: A callable to invoke for checking whether a submitted machine * name value already exists. The arguments passed to the callback will be: * - The submitted value. @@ -49,9 +51,11 @@ use Drupal\Core\Render\Attribute\FormElement; * form element rather than in the suffix of the source element. The source * element must appear in the form structure before this element. Defaults * to FALSE. - * - #maxlength: (optional) Maximum allowed length of the machine name. Defaults + * @property $maxlength + * (optional) Maximum allowed length of the machine name. Defaults * to 64. - * - #disabled: (optional) Should be set to TRUE if an existing machine name + * @property $disabled + * (optional) Should be set to TRUE if an existing machine name * must not be changed after initial creation. * * Usage example: diff --git a/core/lib/Drupal/Core/Render/Element/MoreLink.php b/core/lib/Drupal/Core/Render/Element/MoreLink.php index 9d990942953d..57303585d5ba 100644 --- a/core/lib/Drupal/Core/Render/Element/MoreLink.php +++ b/core/lib/Drupal/Core/Render/Element/MoreLink.php @@ -8,7 +8,9 @@ use Drupal\Core\Render\Attribute\RenderElement; * Provides a link render element for a "more" link, like those used in blocks. * * Properties: - * - #title: The text of the link to generate (defaults to 'More'). + * + * @property $title + * The text of the link to generate (defaults to 'More'). * * See \Drupal\Core\Render\Element\Link for additional properties. * diff --git a/core/lib/Drupal/Core/Render/Element/Number.php b/core/lib/Drupal/Core/Render/Element/Number.php index 7096b2925beb..2be176fd363e 100644 --- a/core/lib/Drupal/Core/Render/Element/Number.php +++ b/core/lib/Drupal/Core/Render/Element/Number.php @@ -11,10 +11,15 @@ use Drupal\Component\Utility\Number as NumberUtility; * Provides a form element for numeric input, with special numeric validation. * * Properties: - * - #default_value: A valid floating point number. - * - #min: Minimum value. - * - #max: Maximum value. - * - #step: Ensures that the number is an even multiple of step, offset by #min + * + * @property $default_value + * A valid floating point number. + * @property $min + * Minimum value. + * @property $max + * Maximum value. + * @property $step + * Ensures that the number is an even multiple of step, offset by #min * if specified. A #min of 1 and a #step of 2 would allow values of 1, 3, 5, * etc. * diff --git a/core/lib/Drupal/Core/Render/Element/Page.php b/core/lib/Drupal/Core/Render/Element/Page.php index f271c6adfcf1..cc7cd16dc1ee 100644 --- a/core/lib/Drupal/Core/Render/Element/Page.php +++ b/core/lib/Drupal/Core/Render/Element/Page.php @@ -8,7 +8,7 @@ use Drupal\Core\Render\Attribute\RenderElement; * Provides a render element for the content of an HTML page. * * This represents the "main part" of the HTML page's body; see html.html.twig. - */ + */ #[RenderElement('page')] class Page extends RenderElementBase { diff --git a/core/lib/Drupal/Core/Render/Element/Pager.php b/core/lib/Drupal/Core/Render/Element/Pager.php index a31c7f2f3719..b5aa9bacf2c4 100644 --- a/core/lib/Drupal/Core/Render/Element/Pager.php +++ b/core/lib/Drupal/Core/Render/Element/Pager.php @@ -13,15 +13,22 @@ use Drupal\Core\Render\Attribute\RenderElement; * extend a select query with \Drupal\Core\Database\Query\PagerSelectExtender. * * Properties: - * - #element: (optional, int) The pager ID, to distinguish between multiple + * + * @property $element + * (optional, int) The pager ID, to distinguish between multiple * pagers on the same page (defaults to 0). - * - #pagination_heading_level: (optional) A heading level for the pager. - * - #parameters: (optional) An associative array of query string parameters to + * @property $pagination_heading_level + * (optional) A heading level for the pager. + * @property $parameters + * (optional) An associative array of query string parameters to * append to the pager. - * - #quantity: The maximum number of numbered page links to create (defaults + * @property $quantity + * The maximum number of numbered page links to create (defaults * to 9). - * - #tags: (optional) An array of labels for the controls in the pages. - * - #route_name: (optional) The name of the route to be used to build pager + * @property $tags + * (optional) An array of labels for the controls in the pages. + * @property $route_name + * (optional) The name of the route to be used to build pager * links. Defaults to '<none>', which will make links relative to the current * URL. This makes the page more effectively cacheable. * diff --git a/core/lib/Drupal/Core/Render/Element/Password.php b/core/lib/Drupal/Core/Render/Element/Password.php index 0c2e99d054b7..3b0b5d3a3785 100644 --- a/core/lib/Drupal/Core/Render/Element/Password.php +++ b/core/lib/Drupal/Core/Render/Element/Password.php @@ -10,8 +10,11 @@ use Drupal\Core\Render\Element; * Provides a form element for entering a password, with hidden text. * * Properties: - * - #size: The size of the input element in characters. - * - #pattern: A string for the native HTML5 pattern attribute. + * + * @property $size + * The size of the input element in characters. + * @property $pattern + * A string for the native HTML5 pattern attribute. * * Usage example: * @code diff --git a/core/lib/Drupal/Core/Render/Element/PasswordConfirm.php b/core/lib/Drupal/Core/Render/Element/PasswordConfirm.php index 3ca411682a53..95a1677c7f42 100644 --- a/core/lib/Drupal/Core/Render/Element/PasswordConfirm.php +++ b/core/lib/Drupal/Core/Render/Element/PasswordConfirm.php @@ -12,7 +12,9 @@ use Drupal\Core\Render\Attribute\FormElement; * entered passwords match. * * Properties: - * - #size: The size of the input element in characters. + * + * @property $size + * The size of the input element in characters. * * Usage example: * @code diff --git a/core/lib/Drupal/Core/Render/Element/Radios.php b/core/lib/Drupal/Core/Render/Element/Radios.php index 7dc1815b36e7..08880ce1689f 100644 --- a/core/lib/Drupal/Core/Render/Element/Radios.php +++ b/core/lib/Drupal/Core/Render/Element/Radios.php @@ -10,7 +10,9 @@ use Drupal\Component\Utility\Html as HtmlUtility; * Provides a form element for a set of radio buttons. * * Properties: - * - #options: An associative array, where the keys are the returned values for + * + * @property $options + * An associative array, where the keys are the returned values for * each radio button, and the values are the labels next to each radio button. * * Usage example: diff --git a/core/lib/Drupal/Core/Render/Element/Range.php b/core/lib/Drupal/Core/Render/Element/Range.php index 0588dd555230..210b19c4ca34 100644 --- a/core/lib/Drupal/Core/Render/Element/Range.php +++ b/core/lib/Drupal/Core/Render/Element/Range.php @@ -12,8 +12,11 @@ use Drupal\Core\Render\Element; * Provides an HTML5 input element with type of "range". * * Properties: - * - #min: Minimum value (defaults to 0). - * - #max: Maximum value (defaults to 100). + * + * @property $min + * Minimum value (defaults to 0). + * @property $max + * Maximum value (defaults to 100). * Refer to \Drupal\Core\Render\Element\Number for additional properties. * * Usage example: diff --git a/core/lib/Drupal/Core/Render/Element/RenderElement.php b/core/lib/Drupal/Core/Render/Element/RenderElement.php index c37338c3219f..fd29573aad11 100644 --- a/core/lib/Drupal/Core/Render/Element/RenderElement.php +++ b/core/lib/Drupal/Core/Render/Element/RenderElement.php @@ -18,7 +18,8 @@ abstract class RenderElement extends RenderElementBase { * {@inheritdoc} */ public function __construct(array $configuration, $plugin_id, $plugin_definition) { - parent::__construct($configuration, $plugin_id, $plugin_definition); + $elementInfoManager = \Drupal::service('plugin.manager.element_info'); + parent::__construct($configuration, $plugin_id, $plugin_definition, $elementInfoManager); @trigger_error('\Drupal\Core\Render\Element\RenderElement is deprecated in drupal:10.3.0 and is removed from drupal:12.0.0. Use \Drupal\Core\Render\Element\RenderElementBase instead. See https://www.drupal.org/node/3436275', E_USER_DEPRECATED); } diff --git a/core/lib/Drupal/Core/Render/Element/RenderElementBase.php b/core/lib/Drupal/Core/Render/Element/RenderElementBase.php index 5ff3a5a0f98e..a2562144b2d8 100644 --- a/core/lib/Drupal/Core/Render/Element/RenderElementBase.php +++ b/core/lib/Drupal/Core/Render/Element/RenderElementBase.php @@ -2,12 +2,16 @@ namespace Drupal\Core\Render\Element; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Plugin\PluginBase; use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Render\Element; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\Url; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides a base class for render element plugins. @@ -37,88 +41,110 @@ use Drupal\Core\Url; * \Drupal\Core\StringTranslation\TranslatableMarkup objects instead. * * Here is the list of the properties used during the rendering of all render - * elements: - * - #access: (bool or AccessResultInterface) + * elements. These are available as properties on the render element (handled + * by magic setter/getter) and also the render array starting with a # + * character. For example $element['#access'] or $elementObject->access. + * + * @property bool|\Drupal\Core\Access\AccessResultInterface $access * Whether the element is accessible or not. - * When the value is FALSE (if boolean) - * or the isAllowed() method returns FALSE (if AccessResultInterface), - * the element is not rendered and user-submitted values are not taken - * into consideration. - * - #access_callback: A callable or function name to call to check access. - * Argument: element. - * - #allowed_tags: (array) Array of allowed HTML tags for XSS filtering of - * #markup, #prefix, #suffix, etc. - * - #attached: (array) Array of attachments associated with the element. - * See the "Attaching libraries in render arrays" section of the + * When the value is FALSE (if boolean) or the isAllowed() method returns + * FALSE (if AccessResultInterface), the element is not rendered and + * user-submitted values are not taken into consideration. + * @property callable $access_callback + * A callable or function name to call to check access. Argument: element. + * @property array<string> $allowed_tags + * Array of allowed HTML tags for XSS filtering of #markup, #prefix, #suffix, + * etc. + * @property array $attached + * Array of attachments associated with the element. See the "Attaching + * libraries in render arrays" section of the * @link theme_render Render API topic @endlink for an overview, and * \Drupal\Core\Render\AttachmentsResponseProcessorInterface::processAttachments - * for a list of what this can contain. Besides this list, it may also contain - * a 'placeholders' element; see the Placeholders section of the + * for a list of what this can contain. Besides this list, it may also + * contain a 'placeholders' element; see the Placeholders section of the * @link theme_render Render API topic @endlink for an overview. - * - #attributes: (array) HTML attributes for the element. The first-level - * keys are the attribute names, such as 'class', and the attributes are - * usually given as an array of string values to apply to that attribute - * (the rendering system will concatenate them together into a string in - * the HTML output). - * - #cache: (array) Cache information. See the Caching section of the + * @property array $attributes + * HTML attributes for the element. The first-level keys are the attribute + * names, such as 'class', and the attributes are usually given as an array + * of string values to apply to that attribute (the rendering system will + * concatenate them together into a string in the HTML output). + * @property array $cache + * Cache information. See the Caching section of the * @link theme_render Render API topic @endlink for more information. - * - #children: (array, internal) Array of child elements of this element. - * Set and used during the rendering process. - * - #create_placeholder: (bool) TRUE if the element has placeholders that - * are generated by #lazy_builder callbacks. Set internally during rendering - * in some cases. See also #attached. - * - #defaults_loaded: (bool) Set to TRUE during rendering when the defaults - * for the element #type have been added to the element. - * - #value: (mixed) A value that cannot be edited by the user. - * - #has_garbage_value: (bool) Internal only. Set to TRUE to indicate that the - * #value property of an element should not be used or processed. - * - #id: (string) The HTML ID on the element. This is automatically set for - * form elements, but not for all render elements; you can override the - * default value or add an ID by setting this property. - * - #lazy_builder: (array) Array whose first element is a lazy building - * callback (callable), and whose second is an array of scalar arguments to - * the callback. To use lazy building, the element array must be very - * simple: no properties except #lazy_builder, #cache, #weight, and - * #create_placeholder, and no children. A lazy builder callback typically - * generates #markup and/or placeholders; see the Placeholders section of the + * @property array $children + * Array of child elements of this element. Set and used during the + * rendering process. + * @property bool $create_placeholder + * TRUE if the element has placeholders that are generated by #lazy_builder + * callbacks. Set internally during rendering in some cases. See also + * #attached. + * @property bool $defaults_loaded + * Set to TRUE during rendering when the defaults for the element #type have + * been added to the element. + * @property mixed $value + * A value that cannot be edited by the user. + * @property bool $has_garbage_value + * @internal + * Set to TRUE to indicate that the #value property of an element should not + * be used or processed. + * @property string $id + * The HTML ID on the element. This is automatically set for form elements, + * but not for all render elements; you can override the default value or + * add an ID by setting this property. + * @property array<callable, array<scalar>> $lazy_builder + * Array whose first element is a lazy building callback (callable), and + * whose second is an array of scalar arguments to the callback. To use + * lazy building, the element array must be very simple: no properties + * except #lazy_builder, #cache, #weight, and #create_placeholder, and no + * children. A lazy builder callback typically generates #markup and/or + * placeholders; see the Placeholders section of the * @link theme_render Render API topic @endlink for information about * placeholders. - * - #markup: (string) During rendering, this will be set to the HTML markup - * output. It can also be set on input, as a fallback if there is no - * theming for the element. This will be filtered for XSS problems during - * rendering; see also #plain_text and #allowed_tags. - * - #plain_text: (string) Elements can set this instead of #markup. All HTML - * tags will be escaped in this text, and if both #plain_text and #markup - * are provided, #plain_text is used. - * - #post_render: (array) Array of callables or function names, which are - * called after the element is rendered. Arguments: rendered element string, - * children. - * - #pre_render: (array) Array of callables or function names, which are - * called just before the element is rendered. Argument: $element. - * Return value: an altered $element. - * - #prefix: (string) Text to render before the entire element output. See - * also #suffix. If it is not already wrapped in a safe markup object, will - * be filtered for XSS safety. - * - #printed: (bool, internal) Set to TRUE when an element and its children - * have been rendered. - * - #render_children: (bool, internal) Set to FALSE by the rendering process - * if the #theme call should be bypassed (normally, the theme is used to - * render the children). Set to TRUE by the rendering process if the children - * should be rendered by rendering each one separately and concatenating. - * - #suffix: (string) Text to render after the entire element output. See - * also #prefix. If it is not already wrapped in a safe markup object, will - * be filtered for XSS safety. - * - #theme: (string) Name of the theme hook to use to render the element. - * A default is generally set for elements; users of the element can - * override this (typically by adding __suggestion suffixes). - * - #theme_wrappers: (array) Array of theme hooks, which are invoked - * after the element and children are rendered, and before #post_render - * functions. - * - #type: (string) The machine name of the type of render/form element. - * - #weight: (float) The sort order for rendering, with lower numbers coming - * before higher numbers. Default if not provided is zero; elements with - * the same weight are rendered in the order they appear in the render - * array. + * @property string $markup + * During rendering, this will be set to the HTML markup output. It can also + * be set on input, as a fallback if there is no theming for the element. + * This will be filtered for XSS problems during rendering; see also + * #plain_text and #allowed_tags. + * @property string $plain_text + * Elements can set this instead of #markup. All HTML tags will be escaped + * in this text, and if both #plain_text and #markup are provided, + * #plain_text is used. + * @property array<callable> $post_render + * Array of callables or function names, which are called after the element + * is rendered. Arguments: rendered element string, children. + * @property array<callable> $pre_render + * Array of callables or function names, which are called just before the + * element is rendered. Argument: $element. Return value: an altered + * $element. + * @property string $prefix + * Text to render before the entire element output. See also #suffix. If it + * is not already wrapped in a safe markup object, will be filtered for XSS + * safety. + * @property bool $printed + * Set to TRUE when an element and its children have been rendered. + * @property bool $render_children + * @internal + * Set to FALSE by the rendering process if the #theme call should be + * bypassed (normally, the theme is used to render the children). Set to + * TRUE by the rendering process if the children should be rendered by + * rendering each one separately and concatenating. + * @property string $suffix + * Text to render after the entire element output. See also #prefix. If it + * is not already wrapped in a safe markup object, will be filtered for XSS + * safety. + * @property string $theme + * Name of the theme hook to use to render the element. A default is + * generally set for elements; users of the element can override this + * (typically by adding __suggestion suffixes). + * @property array<string> $theme_wrappers + * Array of theme hooks, which are invoked after the element and children + * are rendered, and before #post_render functions. + * @property string $type + * The machine name of the type of render/form element. + * @property float $weight + * The sort order for rendering, with lower numbers coming before higher + * numbers. Default if not provided is zero; elements with the same weight + * are rendered in the order they appear in the render array. * * @see \Drupal\Core\Render\Attribute\RenderElement * @see \Drupal\Core\Render\ElementInterface @@ -127,7 +153,60 @@ use Drupal\Core\Url; * * @ingroup theme_render */ -abstract class RenderElementBase extends PluginBase implements ElementInterface { +abstract class RenderElementBase extends PluginBase implements ElementInterface, ContainerFactoryPluginInterface { + + /** + * Constructs a new render element object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin ID for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Render\ElementInfoManagerInterface|null $elementInfoManager + * The element info manager. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, protected ?ElementInfoManagerInterface $elementInfoManager = NULL) { + if (!$this->elementInfoManager) { + @trigger_error('Calling ' . __METHOD__ . '() without the $elementInfoManager argument is deprecated in drupal:11.3.0 and it will be required in drupal:12.0.0. See https://www.drupal.org/node/3526683', E_USER_DEPRECATED); + $this->elementInfoManager = \Drupal::service('plugin.manager.element_info'); + } + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@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.element_info'), + ); + } + + /** + * The storage. + * + * @internal + */ + protected array $storage = []; + + /** + * The parent element. + * + * @var static + */ + protected ElementInterface $renderParent; + + /** + * The parent key. + * + * @var string + */ + protected string $renderParentName; /** * {@inheritdoc} @@ -474,4 +553,183 @@ abstract class RenderElementBase extends PluginBase implements ElementInterface return $element; } + /** + * {@inheritdoc} + */ + public function initializeInternalStorage(array &$element): static { + $this->storage = &$element; + $element['##object'] = $this; + $this->setType(); + return $this; + } + + /** + * Set type on initialize. + * + * There is no need to either call or override this method. + * + * @internal + */ + protected function setType(): void { + $this->storage['#type'] = $this->getPluginId(); + } + + /** + * {@inheritdoc} + */ + public function &toRenderable(?string $wrapper_key = NULL): array { + if ($wrapper_key) { + $return = [$wrapper_key => &$this->storage]; + return $return; + } + return $this->storage; + } + + /** + * Magic method: Sets a property value. + * + * @param string $name + * The name of a property. $value will be accessible with $this->name and + * also $element['#' . $name] where the element is the render array this + * object was created from. + * @param mixed $value + * The value. + */ + public function __set(string $name, $value): void { + $this->storage['#' . $name] = $value; + } + + /** + * Magic method: gets a property value. + * + * @param string $name + * The name of the property. $value is accessible with $this->name and + * also $element['#' . $name] where the element is the render array this + * object was created from. + * + * @return mixed + * The value. + */ + public function __get(string $name): mixed { + return $this->storage['#' . $name] ?? NULL; + } + + /** + * Magic method: unsets a property value. + * + * @param string $name + * The name of the property. This will unset both the object property + * $this->name and also the render key $element['#' . $name] where the + * element is the render array this object was created from. + */ + public function __unset(string $name): void { + unset($this->storage['#' . $name]); + } + + /** + * Magic method: checks if a property value is set. + * + * @param string $name + * The name of the property. Check whether the render key + * $element['#' . $name] is set where element is the render array this + * object was created from. This value is also accessible as $this->name. + * + * @return bool + * Whether it is set or not. + */ + public function __isset(string $name): bool { + return isset($this->storage['#' . $name]); + } + + /** + * {@inheritdoc} + */ + public function getChildren(): \Traversable { + foreach (Element::children($this->storage) as $key) { + yield $key => $this->elementInfoManager()->fromRenderable($this->storage[$key]); + } + } + + /** + * {@inheritdoc} + */ + public function getChild(int|string|array $name): ?ElementInterface { + $value = &NestedArray::getValue($this->storage, (array) $name, $exists); + return $exists ? $this->elementInfoManager()->fromRenderable($value) : NULL; + } + + /** + * {@inheritdoc} + */ + public function addChild(string|int $name, ElementInterface|array &$child): ElementInterface { + if ($name[0] === '#') { + throw new \LogicException('The name of children can not start with a #.'); + } + $childObject = $this->elementInfoManager()->fromRenderable($child); + $childObject->renderParent = $this; + $childObject->renderParentName = $name; + $this->storage[$name] = &$childObject->toRenderable(); + return $childObject; + } + + /** + * {@inheritdoc} + */ + public function createChild(int|string $name, string $class, array $configuration = [], bool $copyProperties = FALSE): ElementInterface { + $childObject = $this->elementInfoManager()->fromClass($class, $configuration); + $childObject = $this->addChild($name, $childObject); + if ($copyProperties) { + $childObject->storage += array_filter($this->storage, Element::property(...), \ARRAY_FILTER_USE_KEY); + } + return $childObject; + } + + /** + * {@inheritdoc} + */ + public function removeChild(int|string $name): ?ElementInterface { + $return = $this->storage[$name] ?? NULL; + unset($this->storage[$name]); + return $return ? $this->elementInfoManager()->fromRenderable($return) : NULL; + } + + /** + * {@inheritdoc} + */ + public function changeType(string $class): ElementInterface { + $this->storage['#type'] = $this->elementInfoManager()->getIdFromClass($class); + unset($this->storage['##object']); + return $this->elementInfoManager()->fromRenderable($this->storage); + } + + /** + * Returns the element info manager. + * + * @return \Drupal\Core\Render\ElementInfoManagerInterface + * The element info manager/ + */ + protected function elementInfoManager(): ElementInfoManagerInterface { + if (!$this->elementInfoManager) { + $this->elementInfoManager = \Drupal::service('plugin.manager.element_info'); + } + return $this->elementInfoManager; + } + + /** + * {@inheritdoc} + */ + public function __sleep(): array { + $vars = parent::__sleep(); + unset($this->storage['##object']); + return $vars; + } + + /** + * {@inheritdoc} + */ + public function __wakeup(): void { + parent::__wakeup(); + $this->storage['##object'] = $this; + } + } diff --git a/core/lib/Drupal/Core/Render/Element/Select.php b/core/lib/Drupal/Core/Render/Element/Select.php index 094b963212bd..c38582d4b23b 100644 --- a/core/lib/Drupal/Core/Render/Element/Select.php +++ b/core/lib/Drupal/Core/Render/Element/Select.php @@ -10,7 +10,9 @@ use Drupal\Core\Render\Element; * Provides a form element for a drop-down menu or scrolling selection box. * * Properties: - * - #options: An associative array of options for the select. Do not use + * + * @property $options + * An associative array of options for the select. Do not use * placeholders that sanitize data in any labels, as doing so will lead to * double-escaping. Each array value can be: * - A single translated string representing an HTML option element, where @@ -28,18 +30,22 @@ use Drupal\Core\Render\Element; * is ignored, and the contents of the 'option' property are interpreted as * an array of options to be merged with any other regular options and * option groups found in the outer array. - * - #sort_options: (optional) If set to TRUE (default is FALSE), sort the + * @property $sort_options + * (optional) If set to TRUE (default is FALSE), sort the * options by their labels, after rendering and translation is complete. * Can be set within an option group to sort that group. - * - #sort_start: (optional) Option index to start sorting at, where 0 is the + * @property $sort_start + * (optional) Option index to start sorting at, where 0 is the * first option. Can be used within an option group. If an empty option is * being added automatically (see #empty_option and #empty_value properties), * this defaults to 1 to keep the empty option at the top of the list. * Otherwise, it defaults to 0. - * - #empty_option: (optional) The label to show for the first default option. + * @property $empty_option + * (optional) The label to show for the first default option. * By default, the label is automatically set to "- Select -" for a required * field and "- None -" for an optional field. - * - #empty_value: (optional) The value for the first default option, which is + * @property $empty_value + * (optional) The value for the first default option, which is * used to determine whether the user submitted a value or not. * - If #required is TRUE, this defaults to '' (an empty string). Note that * if #empty_value is the same as a key in #options then the value of @@ -57,15 +63,19 @@ use Drupal\Core\Render\Element; * - If #required is not TRUE and this value is set (most commonly to an * empty string), then an extra option (see #empty_option above) * representing a "non-selection" is added with this as its value. - * - #multiple: (optional) Indicates whether one or more options can be + * @property $multiple + * (optional) Indicates whether one or more options can be * selected. Defaults to FALSE. - * - #default_value: Must be NULL or not set in case there is no value for the + * @property $default_value + * Must be NULL or not set in case there is no value for the * element yet, in which case a first default option is inserted by default. * Whether this first option is a valid option depends on whether the field * is #required or not. - * - #required: (optional) Whether the user needs to select an option (TRUE) + * @property $required + * (optional) Whether the user needs to select an option (TRUE) * or not (FALSE). Defaults to FALSE. - * - #size: The number of rows in the list that should be visible at one time. + * @property $size + * The number of rows in the list that should be visible at one time. * * Usage example: * @code diff --git a/core/lib/Drupal/Core/Render/Element/Submit.php b/core/lib/Drupal/Core/Render/Element/Submit.php index 980f4ff6c591..d47ca57cb668 100644 --- a/core/lib/Drupal/Core/Render/Element/Submit.php +++ b/core/lib/Drupal/Core/Render/Element/Submit.php @@ -11,10 +11,13 @@ use Drupal\Core\Render\Attribute\FormElement; * the form's submit handler. * * Properties: - * - #submit: Specifies an alternate callback for form submission when the + * + * @property $submit + * Specifies an alternate callback for form submission when the * submit button is pressed. Use '::methodName' format or an array containing * the object and method name (for example, [ $this, 'methodName'] ). - * - #value: The text to be shown on the button. + * @property $value + * The text to be shown on the button. * * Usage Example: * @code diff --git a/core/lib/Drupal/Core/Render/Element/Table.php b/core/lib/Drupal/Core/Render/Element/Table.php index 85111505e9bb..c9af86146829 100644 --- a/core/lib/Drupal/Core/Render/Element/Table.php +++ b/core/lib/Drupal/Core/Render/Element/Table.php @@ -14,19 +14,27 @@ use Drupal\Component\Utility\Html as HtmlUtility; * context of a form. * * Properties: - * - #header: An array of table header labels. - * - #rows: An array of the rows to be displayed. Each row is either an array + * + * @property $header + * An array of table header labels. + * @property $rows + * An array of the rows to be displayed. Each row is either an array * of cell contents or an array of properties as described in table.html.twig * Alternatively specify the data for the table as child elements of the table * element. Table elements would contain rows elements that would in turn * contain column elements. - * - #empty: Text to display when no rows are present. - * - #responsive: Indicates whether to add the drupal.tableresponsive library + * @property $empty + * Text to display when no rows are present. + * @property $responsive + * Indicates whether to add the drupal.tableresponsive library * providing responsive tables. Defaults to TRUE. - * - #sticky: Indicates whether to make the table headers sticky at + * @property $sticky + * Indicates whether to make the table headers sticky at * the top of the page. Defaults to FALSE. - * - #footer: Table footer rows, in the same format as the #rows property. - * - #caption: A localized string for the <caption> tag. + * @property $footer + * Table footer rows, in the same format as the #rows property. + * @property $caption + * A localized string for the <caption> tag. * * Usage example 1: A simple form with an additional information table which * doesn't include any other form field. diff --git a/core/lib/Drupal/Core/Render/Element/Tableselect.php b/core/lib/Drupal/Core/Render/Element/Tableselect.php index cb38a0722d22..297115a30151 100644 --- a/core/lib/Drupal/Core/Render/Element/Tableselect.php +++ b/core/lib/Drupal/Core/Render/Element/Tableselect.php @@ -12,13 +12,19 @@ use Drupal\Core\StringTranslation\TranslatableMarkup; * Provides a form element for a table with radios or checkboxes in left column. * * Properties: - * - #header: An array of table header labels. - * - #options: An associative array where each key is the value returned when + * + * @property $header + * An array of table header labels. + * @property $options + * An associative array where each key is the value returned when * a user selects the radio button or checkbox, and each value is the row of * table data. - * - #empty: The message to display if table does not have any options. - * - #multiple: Set to FALSE to render the table with radios instead checkboxes. - * - #js_select: Set to FALSE if you don't want the select all checkbox added to + * @property $empty + * The message to display if table does not have any options. + * @property $multiple + * Set to FALSE to render the table with radios instead checkboxes. + * @property $js_select + * Set to FALSE if you don't want the select all checkbox added to * the header. * * Other properties of the \Drupal\Core\Render\Element\Table element are also diff --git a/core/lib/Drupal/Core/Render/Element/Tel.php b/core/lib/Drupal/Core/Render/Element/Tel.php index 9d5951e7d4e6..3d752708ebc9 100644 --- a/core/lib/Drupal/Core/Render/Element/Tel.php +++ b/core/lib/Drupal/Core/Render/Element/Tel.php @@ -12,8 +12,11 @@ use Drupal\Core\Render\Element; * validation. * * Properties: - * - #size: The size of the input element in characters. - * - #pattern: A string for the native HTML5 pattern attribute. + * + * @property $size + * The size of the input element in characters. + * @property $pattern + * A string for the native HTML5 pattern attribute. * * Usage example: * @code diff --git a/core/lib/Drupal/Core/Render/Element/Textarea.php b/core/lib/Drupal/Core/Render/Element/Textarea.php index c3fc60219244..9e41f15a891a 100644 --- a/core/lib/Drupal/Core/Render/Element/Textarea.php +++ b/core/lib/Drupal/Core/Render/Element/Textarea.php @@ -9,11 +9,16 @@ use Drupal\Core\Render\Attribute\FormElement; * Provides a form element for input of multiple-line text. * * Properties: - * - #rows: Number of rows in the text box. - * - #cols: Number of columns in the text box. - * - #resizable: Controls whether the text area is resizable. Allowed values + * + * @property $rows + * Number of rows in the text box. + * @property $cols + * Number of columns in the text box. + * @property $resizable + * Controls whether the text area is resizable. Allowed values * are "none", "vertical", "horizontal", or "both" (defaults to "vertical"). - * - #maxlength: The maximum amount of characters to accept as input. + * @property $maxlength + * The maximum amount of characters to accept as input. * * Usage example: * @code diff --git a/core/lib/Drupal/Core/Render/Element/Textfield.php b/core/lib/Drupal/Core/Render/Element/Textfield.php index de144ab4348d..47dbaf2b325a 100644 --- a/core/lib/Drupal/Core/Render/Element/Textfield.php +++ b/core/lib/Drupal/Core/Render/Element/Textfield.php @@ -10,13 +10,21 @@ use Drupal\Core\Render\Element; * Provides a one-line text field form element. * * Properties: - * - #maxlength: Maximum number of characters of input allowed. - * - #size: The size of the input element in characters. - * - #autocomplete_route_name: A route to be used as callback URL by the + * + * @property $maxlength + * Maximum number of characters of input allowed. + * @property $size + * The size of the input element in characters. + * @property $autocomplete_route_name + * A route to be used as callback URL by the * autocomplete JavaScript library. - * - #autocomplete_route_parameters: An array of parameters to be used in + * @property $autocomplete_route_parameters + * An array of parameters to be used in * conjunction with the route name. - * - #pattern: A string for the native HTML5 pattern attribute. + * @property $pattern + * A string for the native HTML5 pattern attribute. + * @property $placeholder + * A string to displayed in a textfield when it has no value. * * Usage example: * diff --git a/core/lib/Drupal/Core/Render/Element/TitleDisplay.php b/core/lib/Drupal/Core/Render/Element/TitleDisplay.php new file mode 100644 index 000000000000..d194fd1b27a8 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/TitleDisplay.php @@ -0,0 +1,22 @@ +<?php + +namespace Drupal\Core\Render\Element; + +/** + * Defines how and where a title should be displayed for a form element. + */ +enum TitleDisplay: string { + + // Label goes before the element (default for most elements). + case Before = 'before'; + + // Label goes after the element (default for radio elements). + case After = 'after'; + + // Label is present in the markup but made invisible using CSS. + case Invisible = 'invisible'; + + // Label is set as the title attribute, displayed as a tooltip on hover. + case Attribute = 'attribute'; + +} diff --git a/core/lib/Drupal/Core/Render/Element/Url.php b/core/lib/Drupal/Core/Render/Element/Url.php index 7c5a9af2fbb4..1e7d499d8372 100644 --- a/core/lib/Drupal/Core/Render/Element/Url.php +++ b/core/lib/Drupal/Core/Render/Element/Url.php @@ -11,9 +11,13 @@ use Drupal\Core\Render\Element; * Provides a form element for input of a URL. * * Properties: - * - #default_value: A valid URL string. - * - #size: The size of the input element in characters. - * - #pattern: A string for the native HTML5 pattern attribute. + * + * @property $default_value + * A valid URL string. + * @property $size + * The size of the input element in characters. + * @property $pattern + * A string for the native HTML5 pattern attribute. * * Usage example: * @code diff --git a/core/lib/Drupal/Core/Render/Element/Value.php b/core/lib/Drupal/Core/Render/Element/Value.php index ce6efe9620ff..c1bb28348528 100644 --- a/core/lib/Drupal/Core/Render/Element/Value.php +++ b/core/lib/Drupal/Core/Render/Element/Value.php @@ -12,7 +12,9 @@ use Drupal\Core\Render\Attribute\FormElement; * in validation and submit processing. * * Properties: - * - #value: The value of the form element that cannot be edited by the user. + * + * @property $value + * The value of the form element that cannot be edited by the user. * * Usage Example: * @code diff --git a/core/lib/Drupal/Core/Render/Element/VerticalTabs.php b/core/lib/Drupal/Core/Render/Element/VerticalTabs.php index bf55119913b0..083b79018c0c 100644 --- a/core/lib/Drupal/Core/Render/Element/VerticalTabs.php +++ b/core/lib/Drupal/Core/Render/Element/VerticalTabs.php @@ -13,7 +13,9 @@ use Drupal\Core\Render\Element; * this element's name as vertical tabs. * * Properties: - * - #default_tab: The HTML ID of the rendered details element to be used as + * + * @property $default_tab + * The HTML ID of the rendered details element to be used as * the default tab. View the source of the rendered page to determine the ID. * * Usage example: diff --git a/core/lib/Drupal/Core/Render/Element/Weight.php b/core/lib/Drupal/Core/Render/Element/Weight.php index 89df69b1879f..3fa4e2c8a969 100644 --- a/core/lib/Drupal/Core/Render/Element/Weight.php +++ b/core/lib/Drupal/Core/Render/Element/Weight.php @@ -12,7 +12,9 @@ use Drupal\Core\Render\Attribute\FormElement; * the order. * * Properties: - * - #delta: The range of possible weight values used. A delta of 10 would + * + * @property $delta + * The range of possible weight values used. A delta of 10 would * indicate possible weight values between -10 and 10. * * Usage example: diff --git a/core/lib/Drupal/Core/Render/Element/Widget.php b/core/lib/Drupal/Core/Render/Element/Widget.php new file mode 100644 index 000000000000..ca5d87ea183e --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Widget.php @@ -0,0 +1,26 @@ +<?php + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Render\Attribute\RenderElement; + +/** + * Provides a widget element. + * + * A form wrapper containing basic properties for the widget, attach + * the widget elements to this wrapper. This element renders to an empty + * string. + * + * @property $field_parents + * The 'parents' space for the field in the form. Most widgets can simply + * overlook this property. This identifies the location where the field + * values are placed within $form_state->getValues(), and is used to + * access processing information for the field through the + * WidgetBase::getWidgetState() and WidgetBase::setWidgetState() methods. + * @property $delta + * The order of this item in the array of sub-elements. (0, 1, 2, etc.) + */ +#[RenderElement('widget')] +class Widget extends Generic { + +} diff --git a/core/lib/Drupal/Core/Render/ElementInfoManager.php b/core/lib/Drupal/Core/Render/ElementInfoManager.php index 9f1c171cf569..b906c6204c26 100644 --- a/core/lib/Drupal/Core/Render/ElementInfoManager.php +++ b/core/lib/Drupal/Core/Render/ElementInfoManager.php @@ -2,6 +2,9 @@ namespace Drupal\Core\Render; +use Drupal\Component\Plugin\Discovery\DiscoveryInterface; +use Drupal\Component\Plugin\Discovery\DiscoveryTrait; +use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Extension\ThemeHandlerInterface; @@ -11,6 +14,7 @@ use Drupal\Core\PreWarm\PreWarmableInterface; use Drupal\Core\Render\Attribute\RenderElement; use Drupal\Core\Render\Element\ElementInterface; use Drupal\Core\Render\Element\FormElementInterface; +use Drupal\Core\Render\Element\Generic; use Drupal\Core\Theme\ThemeManagerInterface; /** @@ -36,6 +40,16 @@ class ElementInfoManager extends DefaultPluginManager implements ElementInfoMana protected $elementInfo; /** + * Class => plugin id mapping. + * + * More performant than reflecting runtime. + * + * @var array + * @internal + */ + protected array $reverseMapping = []; + + /** * Constructs an ElementInfoManager object. * * @param \Traversable $namespaces @@ -65,6 +79,79 @@ class ElementInfoManager extends DefaultPluginManager implements ElementInfoMana /** * {@inheritdoc} */ + protected function getDiscovery(): DiscoveryInterface { + $discovery = parent::getDiscovery(); + return new class ($discovery, $this->reverseMapping) implements DiscoveryInterface { + use DiscoveryTrait; + + public function __construct(protected DiscoveryInterface $decorated, protected array &$reverseMapping) {} + + public function getDefinitions(): array { + $definitions = $this->decorated->getDefinitions(); + foreach ($definitions as $element_type => $definition) { + $this->reverseMapping[$definition['class']] = $element_type; + } + return $definitions; + } + + }; + } + + /** + * {@inheritdoc} + */ + protected function getCachedDefinitions(): ?array { + if (!isset($this->definitions) && $cache = $this->cacheGet($this->cacheKey)) { + $this->definitions = $cache->data['definitions']; + $this->reverseMapping = $cache->data['reverse_mapping']; + } + return $this->definitions; + } + + /** + * {@inheritdoc} + */ + protected function setCachedDefinitions($definitions): void { + $data = [ + 'definitions' => $definitions, + 'reverse_mapping' => $this->reverseMapping, + ]; + $this->cacheSet($this->cacheKey, $data, Cache::PERMANENT, $this->cacheTags); + $this->definitions = $definitions; + } + + /** + * {@inheritdoc} + */ + public function clearCachedDefinitions(): void { + $this->elementInfo = NULL; + + $cids = []; + foreach ($this->themeHandler->listInfo() as $theme_name => $info) { + $cids[] = $this->getCid($theme_name); + } + + $this->cacheBackend->deleteMultiple($cids); + + parent::clearCachedDefinitions(); + } + + /** + * Returns the CID used to cache the element info. + * + * @param string $theme_name + * The theme name. + * + * @return string + * The cache ID. + */ + protected function getCid($theme_name): string { + return 'element_info_build:' . $theme_name; + } + + /** + * {@inheritdoc} + */ public function getInfo($type) { $theme_name = $this->themeManager->getActiveTheme()->getName(); if (!isset($this->elementInfo[$theme_name])) { @@ -144,37 +231,47 @@ class ElementInfoManager extends DefaultPluginManager implements ElementInfoMana * @return \Drupal\Core\Render\Element\ElementInterface * The render element plugin instance. */ - public function createInstance($plugin_id, array $configuration = []) { - return parent::createInstance($plugin_id, $configuration); + public function createInstance($plugin_id, array $configuration = [], &$element = []): ElementInterface { + $instance = parent::createInstance($plugin_id, $configuration); + assert($instance instanceof ElementInterface); + $instance->initializeInternalStorage($element); + return $instance; } /** * {@inheritdoc} */ - public function clearCachedDefinitions() { - $this->elementInfo = NULL; - - $cids = []; - foreach ($this->themeHandler->listInfo() as $theme_name => $info) { - $cids[] = $this->getCid($theme_name); + public function fromClass(string $class, array $configuration = []): ElementInterface { + $this->getDefinitions(); + if ($id = $this->getIdFromClass($class)) { + return $this->createInstance($id, $configuration); } + throw new \LogicException("$class is not a valid element class."); + } - $this->cacheBackend->deleteMultiple($cids); - - parent::clearCachedDefinitions(); + /** + * {@inheritdoc} + */ + public function getIdFromClass(string $class): ?string { + $this->getDefinitions(); + return $this->reverseMapping[$class] ?? NULL; } /** - * Returns the CID used to cache the element info. - * - * @param string $theme_name - * The theme name. - * - * @return string - * The cache ID. + * {@inheritdoc} */ - protected function getCid($theme_name) { - return 'element_info_build:' . $theme_name; + public function fromRenderable(ElementInterface|array &$element, string $class = Generic::class): ElementInterface { + if ($element instanceof ElementInterface) { + return $element; + } + if (isset($element['##object']) && $element['##object'] instanceof ElementInterface) { + return $element['##object']->initializeInternalStorage($element); + } + $type = $element['#type'] ?? $this->getIdFromClass($class); + if (!$type) { + throw new \LogicException('The element passed to ElementInfoManager::fromRenderable must have a #type or a valid class must be provided.'); + } + return $this->createInstance($type, element: $element); } } diff --git a/core/lib/Drupal/Core/Render/ElementInfoManagerInterface.php b/core/lib/Drupal/Core/Render/ElementInfoManagerInterface.php index ea6f21b849cd..e1a891f47643 100644 --- a/core/lib/Drupal/Core/Render/ElementInfoManagerInterface.php +++ b/core/lib/Drupal/Core/Render/ElementInfoManagerInterface.php @@ -3,11 +3,14 @@ namespace Drupal\Core\Render; use Drupal\Component\Plugin\Discovery\DiscoveryInterface; +use Drupal\Component\Plugin\Factory\FactoryInterface; +use Drupal\Core\Render\Element\ElementInterface; +use Drupal\Core\Render\Element\Form; /** * Collects available render array element types. */ -interface ElementInfoManagerInterface extends DiscoveryInterface { +interface ElementInfoManagerInterface extends DiscoveryInterface, FactoryInterface { /** * Retrieves the default properties for the defined element type. @@ -61,4 +64,61 @@ interface ElementInfoManagerInterface extends DiscoveryInterface { */ public function getInfoProperty($type, $property_name, $default = NULL); + /** + * Creates a render object from a render array. + * + * @param \Drupal\Core\Render\Element\ElementInterface|array $element + * A render array or render objects. The latter is returned unchanged. + * @param class-string<T> $class + * The class of the render object being created. + * + * @return T + * A render object. + * + * @template T of ElementInterface + */ + public function fromRenderable(ElementInterface|array &$element, string $class = Form::class): ElementInterface; + + /** + * {@inheritdoc} + * + * @return \Drupal\Core\Render\Element\ElementInterface + * A fully configured render object. + */ + public function createInstance($plugin_id, array $configuration = []): ElementInterface; + + /** + * Creates a render object based on the provided class and configuration. + * + * @param class-string<T> $class + * The class of the render object being instantiated. + * @param array $configuration + * An array of configuration relevant to the render object. + * + * @return T + * A fully configured render object. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + * If the render object cannot be created, such as if the class is invalid. + * + * @template T of ElementInterface + */ + public function fromClass(string $class, array $configuration = []): ElementInterface; + + /** + * Get the plugin ID from the class. + * + * Whenever possible, use the class type inference. Calling this method + * should not be necessary. + * + * @param string $class + * The class of an element object. + * + * @return ?string + * The plugin ID or null if not found. + * + * @internal + */ + public function getIdFromClass(string $class): ?string; + } diff --git a/core/lib/Drupal/Core/TempStore/Element/BreakLockLink.php b/core/lib/Drupal/Core/TempStore/Element/BreakLockLink.php index 4467632eeb77..7052aab1d5d0 100644 --- a/core/lib/Drupal/Core/TempStore/Element/BreakLockLink.php +++ b/core/lib/Drupal/Core/TempStore/Element/BreakLockLink.php @@ -7,6 +7,7 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Render\Attribute\RenderElement; use Drupal\Core\Render\Element\RenderElementBase; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\Render\RendererInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -14,9 +15,13 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * Provides a link to break a tempstore lock. * * Properties: - * - #label: The label of the object that is locked. - * - #lock: \Drupal\Core\TempStore\Lock object. - * - #url: \Drupal\Core\Url object pointing to the break lock form. + * + * @property $label + * The label of the object that is locked. + * @property $lock + * \Drupal\Core\TempStore\Lock object. + * @property $url + * \Drupal\Core\Url object pointing to the break lock form. * * Usage example: * @code @@ -67,9 +72,19 @@ class BreakLockLink extends RenderElementBase implements ContainerFactoryPluginI * The entity type manager. * @param \Drupal\Core\Render\RendererInterface $renderer * The renderer. + * @param \Drupal\Core\Render\ElementInfoManagerInterface $elementInfoManager + * The element info manager. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, DateFormatterInterface $date_formatter, EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer) { - parent::__construct($configuration, $plugin_id, $plugin_definition); + public function __construct( + array $configuration, + $plugin_id, + $plugin_definition, + DateFormatterInterface $date_formatter, + EntityTypeManagerInterface $entity_type_manager, + RendererInterface $renderer, + ElementInfoManagerInterface $elementInfoManager, + ) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $elementInfoManager); $this->dateFormatter = $date_formatter; $this->entityTypeManager = $entity_type_manager; @@ -86,7 +101,8 @@ class BreakLockLink extends RenderElementBase implements ContainerFactoryPluginI $plugin_definition, $container->get('date.formatter'), $container->get('entity_type.manager'), - $container->get('renderer') + $container->get('renderer'), + $container->get('plugin.manager.element_info') ); } diff --git a/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php b/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php index 4e8e77ff7da2..5a130bf160c3 100644 --- a/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php +++ b/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php @@ -9,6 +9,7 @@ use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\Plugin\Field\FieldWidget\OptionsSelectWidget; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\Session\AccountInterface; use Drupal\content_moderation\ModerationInformation; use Drupal\content_moderation\StateTransitionValidationInterface; @@ -66,6 +67,8 @@ class ModerationStateWidget extends OptionsSelectWidget { * Field settings. * @param array $third_party_settings * Third party settings. + * @param \Drupal\Core\Render\ElementInfoManagerInterface $elementInfoManager + * The element info manager. * @param \Drupal\Core\Session\AccountInterface $current_user * Current user service. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager @@ -75,8 +78,8 @@ class ModerationStateWidget extends OptionsSelectWidget { * @param \Drupal\content_moderation\StateTransitionValidationInterface $validator * Moderation state transition validation service. */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, ModerationInformation $moderation_information, StateTransitionValidationInterface $validator) { - parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ElementInfoManagerInterface $elementInfoManager, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, ModerationInformation $moderation_information, StateTransitionValidationInterface $validator) { + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $elementInfoManager); $this->entityTypeManager = $entity_type_manager; $this->currentUser = $current_user; $this->moderationInformation = $moderation_information; @@ -93,6 +96,7 @@ class ModerationStateWidget extends OptionsSelectWidget { $configuration['field_definition'], $configuration['settings'], $configuration['third_party_settings'], + $container->get('plugin.manager.element_info'), $container->get('current_user'), $container->get('entity_type.manager'), $container->get('content_moderation.moderation_information'), diff --git a/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeDefaultWidget.php b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeDefaultWidget.php index 9055d44982bf..4fc259a760a1 100644 --- a/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeDefaultWidget.php +++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeDefaultWidget.php @@ -7,6 +7,7 @@ use Drupal\Core\Field\Attribute\FieldWidget; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -31,8 +32,8 @@ class DateTimeDefaultWidget extends DateTimeWidgetBase { /** * {@inheritdoc} */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityStorageInterface $date_storage) { - parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ElementInfoManagerInterface $elementInfoManager, EntityStorageInterface $date_storage) { + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $elementInfoManager); $this->dateStorage = $date_storage; } @@ -47,7 +48,8 @@ class DateTimeDefaultWidget extends DateTimeWidgetBase { $configuration['field_definition'], $configuration['settings'], $configuration['third_party_settings'], - $container->get('entity_type.manager')->getStorage('date_format') + $container->get('plugin.manager.element_info'), + $container->get('entity_type.manager')->getStorage('date_format'), ); } diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php b/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php index 04c6f4eaf1c2..b2f90d662e9a 100644 --- a/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php +++ b/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php @@ -7,6 +7,7 @@ use Drupal\Core\Field\Attribute\FieldWidget; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\datetime_range\Plugin\Field\FieldType\DateRangeItem; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -31,8 +32,8 @@ class DateRangeDefaultWidget extends DateRangeWidgetBase { /** * {@inheritdoc} */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityStorageInterface $date_storage) { - parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ElementInfoManagerInterface $elementInfoManager, EntityStorageInterface $date_storage) { + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $elementInfoManager); $this->dateStorage = $date_storage; } @@ -47,6 +48,7 @@ class DateRangeDefaultWidget extends DateRangeWidgetBase { $configuration['field_definition'], $configuration['settings'], $configuration['third_party_settings'], + $container->get('plugin.manager.element_info'), $container->get('entity_type.manager')->getStorage('date_format') ); } diff --git a/core/modules/field/tests/modules/field_third_party_test/src/Hook/FieldThirdPartyTestHooks.php b/core/modules/field/tests/modules/field_third_party_test/src/Hook/FieldThirdPartyTestHooks.php index 369575020135..dd9a4167ba53 100644 --- a/core/modules/field/tests/modules/field_third_party_test/src/Hook/FieldThirdPartyTestHooks.php +++ b/core/modules/field/tests/modules/field_third_party_test/src/Hook/FieldThirdPartyTestHooks.php @@ -9,6 +9,9 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\WidgetInterface; use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Render\Element\Number; +use Drupal\Core\Render\Element\Textfield; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; /** @@ -18,17 +21,17 @@ class FieldThirdPartyTestHooks { use StringTranslationTrait; + public function __construct(protected ElementInfoManagerInterface $elementInfoManager) {} + /** * Implements hook_field_widget_third_party_settings_form(). */ #[Hook('field_widget_third_party_settings_form')] public function fieldWidgetThirdPartySettingsForm(WidgetInterface $plugin, FieldDefinitionInterface $field_definition, $form_mode, $form, FormStateInterface $form_state): array { - $element['field_test_widget_third_party_settings_form'] = [ - '#type' => 'textfield', - '#title' => $this->t('3rd party widget settings form'), - '#default_value' => $plugin->getThirdPartySetting('field_third_party_test', 'field_test_widget_third_party_settings_form'), - ]; - return $element; + $textfield = $this->elementInfoManager->fromClass(Textfield::class); + $textfield->title = $this->t('3rd party widget settings form'); + $textfield->default_value = $plugin->getThirdPartySetting('field_third_party_test', 'field_test_widget_third_party_settings_form'); + return $textfield->toRenderable('field_test_widget_third_party_settings_form'); } /** @@ -36,12 +39,10 @@ class FieldThirdPartyTestHooks { */ #[Hook('field_widget_third_party_settings_form')] public function fieldWidgetThirdPartySettingsFormAdditionalImplementation(WidgetInterface $plugin, FieldDefinitionInterface $field_definition, $form_mode, $form, FormStateInterface $form_state): array { - $element['second_field_widget_third_party_settings_form'] = [ - '#type' => 'number', - '#title' => $this->t('Second 3rd party widget settings form'), - '#default_value' => $plugin->getThirdPartySetting('field_third_party_test', 'second_field_widget_third_party_settings_form'), - ]; - return $element; + $number = $this->elementInfoManager->fromClass(Number::class); + $number->title = $this->t('Second 3rd party widget settings form'); + $number->default_value = $plugin->getThirdPartySetting('field_third_party_test', 'second_field_widget_third_party_settings_form'); + return $number->toRenderable('second_field_widget_third_party_settings_form'); } /** @@ -57,12 +58,10 @@ class FieldThirdPartyTestHooks { */ #[Hook('field_formatter_third_party_settings_form')] public function fieldFormatterThirdPartySettingsForm(FormatterInterface $plugin, FieldDefinitionInterface $field_definition, $view_mode, $form, FormStateInterface $form_state): array { - $element['field_test_field_formatter_third_party_settings_form'] = [ - '#type' => 'textfield', - '#title' => $this->t('3rd party formatter settings form'), - '#default_value' => $plugin->getThirdPartySetting('field_third_party_test', 'field_test_field_formatter_third_party_settings_form'), - ]; - return $element; + $textfield = $this->elementInfoManager->fromClass(Textfield::class); + $textfield->title = $this->t('3rd party formatter settings form'); + $textfield->default_value = $plugin->getThirdPartySetting('field_third_party_test', 'field_test_field_formatter_third_party_settings_form'); + return $textfield->toRenderable('field_test_field_formatter_third_party_settings_form'); } /** @@ -70,12 +69,10 @@ class FieldThirdPartyTestHooks { */ #[Hook('field_formatter_third_party_settings_form')] public function fieldFormatterThirdPartySettingsFormAdditionalImplementation(FormatterInterface $plugin, FieldDefinitionInterface $field_definition, $view_mode, $form, FormStateInterface $form_state): array { - $element['second_field_formatter_third_party_settings_form'] = [ - '#type' => 'number', - '#title' => $this->t('Second 3rd party formatter settings form'), - '#default_value' => $plugin->getThirdPartySetting('field_third_party_test', 'second_field_formatter_third_party_settings_form'), - ]; - return $element; + $number = $this->elementInfoManager->fromClass(Number::class); + $number->title = $this->t('Second 3rd party formatter settings form'); + $number->default_value = $plugin->getThirdPartySetting('field_third_party_test', 'second_field_formatter_third_party_settings_form'); + return $number->toRenderable('second_field_formatter_third_party_settings_form'); } /** diff --git a/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php b/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php index a38e64ab53cc..bb4268ff389e 100644 --- a/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php +++ b/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php @@ -29,16 +29,10 @@ use Symfony\Component\Validator\ConstraintViolationListInterface; class FileWidget extends WidgetBase { /** - * The element info manager. - */ - protected ElementInfoManagerInterface $elementInfo; - - /** * {@inheritdoc} */ public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ElementInfoManagerInterface $element_info) { - parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); - $this->elementInfo = $element_info; + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $element_info); } /** @@ -238,7 +232,7 @@ class FileWidget extends WidgetBase { // Essentially we use the managed_file type, extended with some // enhancements. - $element_info = $this->elementInfo->getInfo('managed_file'); + $element_info = $this->elementInfoManager->getInfo('managed_file'); $element += [ '#type' => 'managed_file', '#upload_location' => $items[$delta]->getUploadLocation(), diff --git a/core/modules/filter/src/Element/TextFormat.php b/core/modules/filter/src/Element/TextFormat.php index 38ca133e3cd2..6b1fc25c3ffb 100644 --- a/core/modules/filter/src/Element/TextFormat.php +++ b/core/modules/filter/src/Element/TextFormat.php @@ -12,11 +12,15 @@ use Drupal\Core\Url; * Provides a text format render element. * * Properties: - * - #base_type: The form element #type to use for the 'value' element. + * + * @property $base_type + * The form element #type to use for the 'value' element. * 'textarea' by default. - * - #format: (optional) The text format ID to preselect. If omitted, the + * @property $format + * (optional) The text format ID to preselect. If omitted, the * default format for the current user will be used. - * - #allowed_formats: (optional) An array of text format IDs that are available + * @property $allowed_formats + * (optional) An array of text format IDs that are available * for this element. If omitted, all text formats that the current user has * access to will be allowed. * diff --git a/core/modules/layout_builder/src/Element/LayoutBuilder.php b/core/modules/layout_builder/src/Element/LayoutBuilder.php index d673d24d1d38..2b128f931887 100644 --- a/core/modules/layout_builder/src/Element/LayoutBuilder.php +++ b/core/modules/layout_builder/src/Element/LayoutBuilder.php @@ -11,6 +11,7 @@ use Drupal\Core\Plugin\PluginFormInterface; use Drupal\Core\Render\Attribute\RenderElement; use Drupal\Core\Render\Element; use Drupal\Core\Render\Element\RenderElementBase; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\Security\Attribute\TrustedCallback; use Drupal\Core\Url; use Drupal\layout_builder\Context\LayoutBuilderContextTrait; @@ -52,9 +53,11 @@ class LayoutBuilder extends RenderElementBase implements ContainerFactoryPluginI * The plugin implementation definition. * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher * The event dispatcher service. + * @param \Drupal\Core\Render\ElementInfoManagerInterface|null $elementInfoManager + * The element info manager. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher) { - parent::__construct($configuration, $plugin_id, $plugin_definition); + public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, ?ElementInfoManagerInterface $elementInfoManager = NULL) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $elementInfoManager); $this->eventDispatcher = $event_dispatcher; } @@ -66,7 +69,8 @@ class LayoutBuilder extends RenderElementBase implements ContainerFactoryPluginI $configuration, $plugin_id, $plugin_definition, - $container->get('event_dispatcher') + $container->get('event_dispatcher'), + $container->get('plugin.manager.element_info') ); } diff --git a/core/modules/media/src/Plugin/Field/FieldWidget/OEmbedWidget.php b/core/modules/media/src/Plugin/Field/FieldWidget/OEmbedWidget.php index 5fa53b2b9e92..0230cc19b465 100644 --- a/core/modules/media/src/Plugin/Field/FieldWidget/OEmbedWidget.php +++ b/core/modules/media/src/Plugin/Field/FieldWidget/OEmbedWidget.php @@ -7,6 +7,8 @@ use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\Plugin\Field\FieldWidget\StringTextfieldWidget; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element\ElementInterface; +use Drupal\Core\Render\Element\Widget; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\media\Entity\MediaType; use Drupal\media\Plugin\media\Source\OEmbedInterface; @@ -28,24 +30,35 @@ class OEmbedWidget extends StringTextfieldWidget { /** * {@inheritdoc} */ - public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { - $element = parent::formElement($items, $delta, $element, $form, $form_state); + public function singleElementObject(FieldItemListInterface $items, $delta, Widget $widget, ElementInterface $form, FormStateInterface $form_state): ElementInterface { + $widget = parent::singleElementObject($items, $delta, $widget, $form, $form_state); + $value = $widget->getChild('value'); + $value->description = $this->getValueDescription($items, $value->description); + return $widget; + } + /** + * Merges description and provider messages. + * + * @param \Drupal\Core\Field\FieldItemListInterface $items + * FieldItemList containing the values to be edited. + * @param scalar|\Stringable|\Drupal\Core\Render\RenderableInterface|array $description + * The description on the form element. + * + * @return string|array + * The description on the value child. + */ + protected function getValueDescription(FieldItemListInterface $items, mixed $description): string|array { /** @var \Drupal\media\Plugin\media\Source\OEmbedInterface $source */ $source = $items->getEntity()->getSource(); $message = $this->t('You can link to media from the following services: @providers', ['@providers' => implode(', ', $source->getProviders())]); - - if (!empty($element['value']['#description'])) { - $element['value']['#description'] = [ + if ($description) { + return [ '#theme' => 'item_list', - '#items' => [$element['value']['#description'], $message], + '#items' => [$description, $message], ]; } - else { - $element['value']['#description'] = $message; - } - - return $element; + return $message; } /** diff --git a/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php index 3b36ee5e3773..33ffe1a39920 100644 --- a/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php @@ -18,6 +18,7 @@ use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Field\WidgetBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; @@ -78,6 +79,8 @@ class MediaLibraryWidget extends WidgetBase implements TrustedCallbackInterface * The widget settings. * @param array $third_party_settings * Any third party settings. + * @param \Drupal\Core\Render\ElementInfoManagerInterface $elementInfoManager + * The element info manager. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * Entity type manager service. * @param \Drupal\Core\Session\AccountInterface $current_user @@ -85,8 +88,8 @@ class MediaLibraryWidget extends WidgetBase implements TrustedCallbackInterface * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler. */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, AccountInterface $current_user, ModuleHandlerInterface $module_handler) { - parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ElementInfoManagerInterface $elementInfoManager, EntityTypeManagerInterface $entity_type_manager, AccountInterface $current_user, ModuleHandlerInterface $module_handler) { + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $elementInfoManager); $this->entityTypeManager = $entity_type_manager; $this->currentUser = $current_user; $this->moduleHandler = $module_handler; @@ -102,6 +105,7 @@ class MediaLibraryWidget extends WidgetBase implements TrustedCallbackInterface $configuration['field_definition'], $configuration['settings'], $configuration['third_party_settings'], + $container->get('plugin.manager.element_info'), $container->get('entity_type.manager'), $container->get('current_user'), $container->get('module_handler') diff --git a/core/modules/node/node.module b/core/modules/node/node.module index f14d843faa10..1fb4071ba47d 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -36,8 +36,12 @@ use Drupal\node\NodeTypeInterface; * @return array|false * A renderable array containing a list of linked node titles fetched from * $result, or FALSE if there are no rows in $result. + * + * @deprecated in drupal:11.3.0 and is removed from drupal:12.0.0. There is no replacement. + * @see https://www.drupal.org/node/3531959 */ function node_title_list(StatementInterface $result, $title = NULL) { + @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.3.0 and is removed from drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3531959', E_USER_DEPRECATED); $items = []; $num_rows = FALSE; $nids = []; diff --git a/core/modules/system/tests/modules/element_info_test/src/Element/Deprecated.php b/core/modules/system/tests/modules/element_info_test/src/Element/Deprecated.php index f54ad8f9a9a3..c052c5a488fb 100644 --- a/core/modules/system/tests/modules/element_info_test/src/Element/Deprecated.php +++ b/core/modules/system/tests/modules/element_info_test/src/Element/Deprecated.php @@ -17,7 +17,8 @@ class Deprecated extends RenderElementBase { * {@inheritdoc} */ public function __construct(array $configuration, $plugin_id, $plugin_definition) { - parent::__construct($configuration, $plugin_id, $plugin_definition); + $elementInfoManager = \Drupal::service('plugin.manager.element_info'); + parent::__construct($configuration, $plugin_id, $plugin_definition, $elementInfoManager); @trigger_error(__CLASS__ . ' is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. See https://www.drupal.org/node/3068104', E_USER_DEPRECATED); } diff --git a/core/modules/system/tests/modules/element_info_test/src/Hook/ElementInfoTestHooks.php b/core/modules/system/tests/modules/element_info_test/src/Hook/ElementInfoTestHooks.php index ea72bd033fb9..53150495cb9b 100644 --- a/core/modules/system/tests/modules/element_info_test/src/Hook/ElementInfoTestHooks.php +++ b/core/modules/system/tests/modules/element_info_test/src/Hook/ElementInfoTestHooks.php @@ -4,8 +4,9 @@ declare(strict_types=1); namespace Drupal\element_info_test\Hook; -use Drupal\element_info_test\ElementInfoTestNumberBuilder; use Drupal\Core\Hook\Attribute\Hook; +use Drupal\element_info_test\ElementInfoTestNumberBuilder; +use Drupal\element_info_test\Render\Element\Details; /** * Hook implementations for element_info_test. @@ -30,6 +31,9 @@ class ElementInfoTestHooks { if (\Drupal::state()->get('hook_element_plugin_alter:remove_weight', FALSE)) { unset($definitions['weight']); } + + $definitions['details']['class'] = Details::class; + $definitions['details']['provider'] = 'element_info_test'; } } diff --git a/core/modules/system/tests/modules/element_info_test/src/Render/Element/Details.php b/core/modules/system/tests/modules/element_info_test/src/Render/Element/Details.php new file mode 100644 index 000000000000..8e4d84558ca4 --- /dev/null +++ b/core/modules/system/tests/modules/element_info_test/src/Render/Element/Details.php @@ -0,0 +1,57 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\element_info_test\Render\Element; + +use Drupal\Core\Render\Attribute\RenderElement; +use Drupal\Core\Render\Element; +use Drupal\Core\Render\Element\RenderElementBase; + +/** + * Provides a render element for a details element. + * + * Properties: + * + * @property $title + * The title of the details container. Defaults to "Details". + * @property $open + * Indicates whether the container should be open by default. + * Defaults to FALSE. + * @property $custom + * Confirm that this class has been swapped properly. + * @property $summary_attributes + * An array of attributes to apply to the <summary> + * element. + */ +#[RenderElement('details')] +class Details extends RenderElementBase { + + /** + * {@inheritdoc} + */ + public function getInfo(): array { + return [ + '#open' => FALSE, + '#summary_attributes' => [], + '#custom' => 'Custom', + ]; + } + + /** + * Adds form element theming to details. + * + * @param array $element + * An associative array containing the properties and children of the + * details. + * + * @return array + * The modified element. + */ + public static function preRenderDetails($element): array { + Element::setAttributes($element, ['custom']); + + return $element; + } + +} diff --git a/core/modules/system/tests/modules/form_test/src/Hook/FormTestHooks.php b/core/modules/system/tests/modules/form_test/src/Hook/FormTestHooks.php index cda2b92b3477..441ebaa1d128 100644 --- a/core/modules/system/tests/modules/form_test/src/Hook/FormTestHooks.php +++ b/core/modules/system/tests/modules/form_test/src/Hook/FormTestHooks.php @@ -6,6 +6,8 @@ namespace Drupal\form_test\Hook; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Render\Element\Submit; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\form_test\Callbacks; @@ -16,6 +18,8 @@ class FormTestHooks { use StringTranslationTrait; + public function __construct(protected ElementInfoManagerInterface $elementInfoManager) {} + /** * Implements hook_form_FORM_ID_alter(). */ @@ -55,13 +59,10 @@ class FormTestHooks { */ #[Hook('form_user_register_form_alter')] public function formUserRegisterFormAlter(&$form, FormStateInterface $form_state) : void { - $form['test_rebuild'] = [ - '#type' => 'submit', - '#value' => $this->t('Rebuild'), - '#submit' => [ - [Callbacks::class, 'userRegisterFormRebuild'], - ], - ]; + $submit = $this->elementInfoManager->fromRenderable($form) + ->createChild('test_rebuild', Submit::class); + $submit->value = $this->t('Rebuild'); + $submit->submit = [[Callbacks::class, 'userRegisterFormRebuild']]; } /** @@ -69,11 +70,12 @@ class FormTestHooks { */ #[Hook('form_form_test_vertical_tabs_access_form_alter')] public function formFormTestVerticalTabsAccessFormAlter(&$form, &$form_state, $form_id) : void { - $form['vertical_tabs1']['#access'] = FALSE; - $form['vertical_tabs2']['#access'] = FALSE; - $form['tabs3']['#access'] = TRUE; - $form['fieldset1']['#access'] = FALSE; - $form['container']['#access'] = FALSE; + $element_object = $this->elementInfoManager->fromRenderable($form); + $element_object->getChild('vertical_tabs1')->access = FALSE; + $element_object->getChild('vertical_tabs2')->access = FALSE; + $element_object->getChild('tab3')->access = FALSE; + $element_object->getChild('fieldset1')->access = FALSE; + $element_object->getChild('container')->access = FALSE; } } diff --git a/core/modules/text/src/Plugin/Field/FieldWidget/TextfieldWidget.php b/core/modules/text/src/Plugin/Field/FieldWidget/TextfieldWidget.php index 16b7549681c1..289fbbd332cd 100644 --- a/core/modules/text/src/Plugin/Field/FieldWidget/TextfieldWidget.php +++ b/core/modules/text/src/Plugin/Field/FieldWidget/TextfieldWidget.php @@ -6,7 +6,10 @@ use Drupal\Core\Field\Attribute\FieldWidget; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\Plugin\Field\FieldWidget\StringTextfieldWidget; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element\ElementInterface; +use Drupal\Core\Render\Element\Widget; use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\filter\Element\TextFormat; use Symfony\Component\Validator\ConstraintViolationInterface; /** @@ -22,20 +25,20 @@ class TextfieldWidget extends StringTextfieldWidget { /** * {@inheritdoc} */ - public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { - $main_widget = parent::formElement($items, $delta, $element, $form, $form_state); + public function singleElementObject(FieldItemListInterface $items, $delta, Widget $widget, ElementInterface $form, FormStateInterface $form_state): ElementInterface { + $widget = parent::singleElementObject($items, $delta, $widget, $form, $form_state); $allowed_formats = $this->getFieldSetting('allowed_formats'); - $element = $main_widget['value']; - $element['#type'] = 'text_format'; - $element['#format'] = $items[$delta]->format ?? NULL; - $element['#base_type'] = $main_widget['value']['#type']; - + $widget = $widget->getChild('value'); + $type = $widget->type; + $widget = $widget->changeType(TextFormat::class); + $widget->format = $items[$delta]->format ?? NULL; + $widget->base_type = $type; if ($allowed_formats && !$this->isDefaultValueWidget($form_state)) { - $element['#allowed_formats'] = $allowed_formats; + $widget->allowed_formats = $allowed_formats; } - return $element; + return $widget; } /** diff --git a/core/tests/Drupal/KernelTests/Core/Render/Element/PluginAlterTest.php b/core/tests/Drupal/KernelTests/Core/Render/Element/PluginAlterTest.php index 7ad8afa75be6..c300cd4c019f 100644 --- a/core/tests/Drupal/KernelTests/Core/Render/Element/PluginAlterTest.php +++ b/core/tests/Drupal/KernelTests/Core/Render/Element/PluginAlterTest.php @@ -23,7 +23,7 @@ class PluginAlterTest extends KernelTestBase { $info_manager = $this->container->get('plugin.manager.element_info'); $this->assertArrayHasKey('weight', $info_manager->getDefinitions()); - // @see element_info_test_element_plugin_alter() + // @see ElementInfoTestHooks::elementPluginAlter(). $this->container->get('state')->set('hook_element_plugin_alter:remove_weight', TRUE); // The definition will be cached. $this->assertArrayHasKey('weight', $info_manager->getDefinitions()); @@ -33,4 +33,27 @@ class PluginAlterTest extends KernelTestBase { $this->assertArrayNotHasKey('weight', $info_manager->getDefinitions()); } + /** + * Tests hook_element_plugin_alter(). + */ + public function testPluginClassSwap(): void { + $info_manager = $this->container->get('plugin.manager.element_info'); + $test_details = [ + '#type' => 'details', + '#title' => 'Title', + '#description' => 'Description', + '#open' => TRUE, + ]; + + // @see ElementInfoTestHooks::elementPluginAlter(). + $expected = [ + 'class' => 'Drupal\element_info_test\Render\Element\Details', + 'provider' => 'element_info_test', + 'id' => 'details', + ]; + $this->assertEquals($expected, $info_manager->getDefinitions()['details']); + \Drupal::service('renderer')->renderRoot($test_details); + $this->assertArrayHasKey('#custom', $test_details); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Render/Element/WeightTest.php b/core/tests/Drupal/KernelTests/Core/Render/Element/WeightTest.php index 00b7948f2a81..e36da16d9028 100644 --- a/core/tests/Drupal/KernelTests/Core/Render/Element/WeightTest.php +++ b/core/tests/Drupal/KernelTests/Core/Render/Element/WeightTest.php @@ -8,6 +8,7 @@ use Drupal\Core\Form\FormState; use Drupal\Core\Render\Element\Number; use Drupal\Core\Render\Element\Select; use Drupal\Core\Render\Element\Weight; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\element_info_test\ElementInfoTestNumberBuilder; use Drupal\KernelTests\KernelTestBase; @@ -40,7 +41,7 @@ class WeightTest extends KernelTestBase { $form_state = new FormState(); $complete_form = []; - $element_object = new Weight([], 'weight', []); + $element_object = new Weight([], 'weight', [], elementInfoManager: $this->createStub(ElementInfoManagerInterface::class)); $info = $element_object->getInfo(); $element += $info; diff --git a/core/tests/Drupal/Tests/Core/Render/Element/HtmlTagTest.php b/core/tests/Drupal/Tests/Core/Render/Element/HtmlTagTest.php index d0c09e97fc53..8490a5c0876e 100644 --- a/core/tests/Drupal/Tests/Core/Render/Element/HtmlTagTest.php +++ b/core/tests/Drupal/Tests/Core/Render/Element/HtmlTagTest.php @@ -6,6 +6,7 @@ namespace Drupal\Tests\Core\Render\Element; use Drupal\Core\Render\Markup; use Drupal\Tests\Core\Render\RendererTestBase; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\Render\Element\HtmlTag; /** @@ -18,7 +19,7 @@ class HtmlTagTest extends RendererTestBase { * @covers ::getInfo */ public function testGetInfo(): void { - $htmlTag = new HtmlTag([], 'test', 'test'); + $htmlTag = new HtmlTag([], 'test', 'test', elementInfoManager: $this->createStub(ElementInfoManagerInterface::class)); $info = $htmlTag->getInfo(); $this->assertArrayHasKey('#pre_render', $info); $this->assertArrayHasKey('#attributes', $info); diff --git a/core/tests/Drupal/Tests/Core/Render/Element/ModernRenderElementTest.php b/core/tests/Drupal/Tests/Core/Render/Element/ModernRenderElementTest.php new file mode 100644 index 000000000000..c91b74f8c0a4 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Render/Element/ModernRenderElementTest.php @@ -0,0 +1,60 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\Core\Render\Element; + +use Drupal\Component\Plugin\Factory\FactoryInterface; +use Drupal\Core\Render\Element; +use Drupal\Core\Render\Element\Textfield; +use Drupal\Core\Render\ElementInfoManager; +use Drupal\Tests\UnitTestCase; + +/** + * @coversDefaultClass \Drupal\Core\Render\Element\RenderElementBase + * @group Render + */ +class ModernRenderElementTest extends UnitTestCase { + + public function testChildren(): void { + $factory = $this->createMock(FactoryInterface::class); + $elementInfoManager = new class ($factory) extends ElementInfoManager { + + public function __construct(protected $factory) {} + + }; + $factory->expects($this->any()) + ->method('createInstance') + ->willReturnCallback(fn () => new Textfield([], '', NULL, $elementInfoManager)); + // If the type is not given ::fromRenderable presumes "form" and uses the + // plugin discovery to find which class provides the form element. This + // test does not set up discovery so some type must be provided. + $element = ['#type' => 'ignored by the mock factory']; + $elementObject = $elementInfoManager->fromRenderable($element); + for ($i = 0; $i <= 2; $i++) { + $child = [ + '#type' => 'ignored by the mock factory', + '#test' => $i, + ]; + $elementObject->addChild("test$i", $child); + // addChild() takes the $child render array by reference and stores a + // reference to it in the render object. To avoid modifying the + // previously created render object when reusing the $child variable, + // unset() it to break the reference before reassigning. + unset($child); + } + foreach ([1 => ['test0', 'test1', 'test2'], 2 => ['test0', 'test2']] as $delta => $expectedChildrenKeys) { + $i = 0; + foreach ($elementObject->getChildren() as $name => $child) { + $this->assertSame($name, "test$i"); + $this->assertSame($i, $child->test); + $i += $delta; + } + $this->assertSame(Element::children($elementObject->toRenderable()), $expectedChildrenKeys); + // The first iteration tests removing an existing child. The second + // iteration tests removing a nonexistent child. + $elementObject->removeChild('test1'); + } + } + +} diff --git a/core/tests/Drupal/Tests/Core/Render/Element/TableSelectTest.php b/core/tests/Drupal/Tests/Core/Render/Element/TableSelectTest.php index fc58c1db4efe..7acfef4ca509 100644 --- a/core/tests/Drupal/Tests/Core/Render/Element/TableSelectTest.php +++ b/core/tests/Drupal/Tests/Core/Render/Element/TableSelectTest.php @@ -7,6 +7,7 @@ namespace Drupal\Tests\Core\Render\Element; use Drupal\Core\Form\FormState; use Drupal\Core\Link; use Drupal\Core\Render\Element\Tableselect; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Url; use Drupal\Tests\UnitTestCase; @@ -25,7 +26,7 @@ class TableSelectTest extends UnitTestCase { $form_state = new FormState(); $complete_form = []; - $element_object = new Tableselect([], 'table_select', []); + $element_object = new Tableselect([], 'table_select', [], elementInfoManager: $this->createStub(ElementInfoManagerInterface::class)); $info = $element_object->getInfo(); $element += $info; @@ -50,7 +51,7 @@ class TableSelectTest extends UnitTestCase { $form_state = new FormState(); $complete_form = []; - $element_object = new Tableselect([], 'table_select', []); + $element_object = new Tableselect([], 'table_select', [], elementInfoManager: $this->createStub(ElementInfoManagerInterface::class)); $info = $element_object->getInfo(); $element += $info; diff --git a/core/tests/Drupal/Tests/Core/Theme/Icon/IconTest.php b/core/tests/Drupal/Tests/Core/Theme/Icon/IconTest.php index 2c67cf9193fb..d1a6e6433407 100644 --- a/core/tests/Drupal/Tests/Core/Theme/Icon/IconTest.php +++ b/core/tests/Drupal/Tests/Core/Theme/Icon/IconTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Drupal\Tests\Core\Theme\Icon; use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\Render\Element\Icon; use Drupal\Core\Template\Attribute; use Drupal\Core\Theme\Icon\IconDefinition; @@ -41,7 +42,7 @@ class IconTest extends UnitTestCase { * Test the Icon::getInfo method. */ public function testGetInfo(): void { - $icon = new Icon([], 'test', 'test'); + $icon = new Icon([], 'test', 'test', elementInfoManager: $this->createStub(ElementInfoManagerInterface::class)); $info = $icon->getInfo(); $this->assertArrayHasKey('#pre_render', $info); |