workflow-8.x-1.x-dev/src/Plugin/Field/FieldWidget/WorkflowDefaultWidget.php
src/Plugin/Field/FieldWidget/WorkflowDefaultWidget.php
<?php
namespace Drupal\workflow\Plugin\Field\FieldWidget;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\workflow\Element\WorkflowTransitionButtons;
use Drupal\workflow\Element\WorkflowTransitionElement;
use Drupal\workflow\Entity\WorkflowTransitionInterface;
use Drupal\workflow\Form\WorkflowTransitionForm;
/**
* Plugin implementation of the 'workflow_default' widget.
*
* @FieldWidget(
* id = "workflow_default",
* label = @Translation("Workflow Transition form"),
* description = @Translation("A complex widget showing the Transition form."),
* field_types = {
* "workflow",
* },
* multiple_values = true,
* )
*/
class WorkflowDefaultWidget extends WidgetBase {
/**
* The form builder.
*
* @var \Drupal\Core\Form\FormBuilderInterface
*/
protected $formBuilder;
/**
* The request stack, as used in FormBuilder.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* Generates a widget.
*
* @param \Drupal\workflow\Entity\WorkflowTransitionInterface $transition
* A WorkflowTransition.
*
* @return \Drupal\workflow\Plugin\Field\FieldWidget\WorkflowDefaultWidget
* The WorkflowTransition widget.
*/
public static function createInstance(WorkflowTransitionInterface $transition): WorkflowDefaultWidget {
// Function called in: F___, F___ ______, WorkflowStateActionBase.
$entity_type_manager = \Drupal::service('entity_type.manager');
$entity = $transition->getTargetEntity();
$entity_type_id = $entity->getEntityTypeId();
$entity_bundle = $entity->bundle();
$view_mode = 'default';
$field_name = $transition->getFieldName();
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display */
$entity_form_display = $entity_type_manager->getStorage('entity_form_display');
$form_display = $entity_form_display->load("$entity_type_id.$entity_bundle.$view_mode");
// @todo Fix $widget is NULL for hidden or removed fields.
$widget = $form_display->getRenderer($field_name);
return $widget;
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
$settings = parent::defaultSettings();
$settings['form_mode'] = 'add';
return $settings;
}
/**
* {@inheritdoc}
*/
public function form(FieldItemListInterface $items, array &$form, FormStateInterface $form_state, $get_delta = NULL) {
// Function called in: F___, F___ ______, F________, Widget, Widget submit.
$field_name = $items->getName();
if ($this->isDefaultValueWidget($form_state)) {
// On the Field settings page, User may not set a default value.
// (This is done by the Workflow module).
return [];
}
$element = parent::form($items, $form, $form_state, $get_delta);
// Prepare a UI wrapper. It might be a (collapsible) fieldset.
// Note: It will be overridden in WorkflowTransitionForm.
// Note: Align ['#parents'] in Widget::form...(), Form::copy..(), ...
$transition = $element['widget'][0]['#workflow_transition'];
WorkflowDefaultWidget::addWrapper($transition, $element);
return $element;
}
/**
* {@inheritdoc}
*/
public static function afterBuild(array $element, FormStateInterface $form_state) {
$field_name = $element['#field_name'];
$element = parent::afterBuild($element, $form_state);
return $element;
}
/**
* {@inheritdoc}
*
* Gets the TransitionWidget in a form (for e.g., Workflow History Tab).
*
* This is a minimized version of FormBuilder::retrieveForm().
* As a drawback, the form_alter hooks must be implemented separately.
*
* Be careful: Widget may be shown in very different places. Test carefully!!
* - On a entity add/edit page;
* - On a entity preview page;
* - On a entity view page;
* - Obsolete: On a entity 'workflow history' tab;
* - On a comment display, in the comment history;
* - On a comment form, below the comment history.
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
// Function called in: F___, F___ ______, F________, Widget, Widget submit.
// Note: no parent::call, since parent is an abstract method.
/** @var \Drupal\workflow\Plugin\Field\WorkflowItemListInterface $items */
if (!$workflow = $items?->getWorkflow()) {
// @todo Add error message.
return $element;
}
if ($this->isDefaultValueWidget($form_state)) {
// On the Field settings page, User may not set a default value.
// (This is done by the Workflow module).
return [];
}
$field_name = $items->getName();
/** @var \Drupal\workflow\Plugin\Field\WorkflowItemListInterface $items */
$transition = $items->getDefaultTransition();
$field_name = $transition->getFieldName();
// Overwrite settings from WidgetBase::formSingleElement().
// Required sign is not needed - we have predefined multivalue fields.
$element['#required'] = FALSE;
// -- The following is copied from FileWidget.
// Allows this field to return an array instead of a single value.
$element['#extended'] = TRUE;
// Save original element; we need it in return value.
$workflow_form = $element;
// Return a handles_multivalue element.
// Note: Align ['#parents'] in Widget::form...(), Form::copy..(), ...
$workflow_form['#parents'] = array_merge($element['#field_parents'],
[
$items->getName(),
// 'value',
// 0,
]);
// To prepare Transition widget, use the Form, to get attached fields.
// Add result to $element, respecting existing formSingleElement attributes.
// Create a new $form_display, to replace Node by WorkflowTransition.
// --- Start Copy from Profile module ---.
$form_mode = $this->getSetting('form_mode');
$form_display = EntityFormDisplay::collectRenderDisplay($transition, $form_mode);
$form_display->removeComponent('revision_log_message');
$form_display->buildForm($transition, $workflow_form, $form_state);
$form_process_callback = [get_class($this), 'attachSubmit'];
// Make sure the #process callback doesn't get added more than once
// if the widget is used on multiple fields.
if (!isset($form['#process']) || !in_array($form_process_callback, $form['#process'])) {
// $form['#process'][] = [get_class($this), 'attachSubmit'];
}
// --- End Copy from Profile module ---.
// Remove the processForm() callbacks, since we are a widget, not a form.
$workflow_form['#process'] = [];
$workflow_form['#entity_builders'] = [];
// Remove action submit buttons, to make sure the save button will not
// be involved by any means.
unset($workflow_form['actions']);
// Workflow: remove ['#type' => 'form'] elements.
$workflow_form['#validate'] = []; // Do clear, do not unset.
// Avoid $form_state->setSubmitHandlers in FormBuilder::doBuildForm().
$workflow_form['#submit'] = []; // Do clear, do not unset.
unset($workflow_form['#theme']);
// Override WorkflowTransitionElement baseFields, created by Field UI.
// Note: Align ['#parents'] in Widget::form...(), Form::copy..(), ...
// Add '#workflow_transition' for WorkflowTransitionElement::alter().
$workflow_form['#workflow_transition'] = $transition;
WorkflowTransitionElement::alter($workflow_form, $form_state, $form);
// Return a handles_multivalue element.
// Note: Align ['#parents'] in Widget::form...(), Form::copy..(), ...
$element[0] = $workflow_form + [
'#type' => 'workflow_transition',
// '#element_validate' => [
// [get_class($this), 'validateTransitionWidget'],
// ],
'#form_mode' => $form_mode,
]
+ $workflow_form;
return $element;
}
/**
* Internal function to generate a wrapper with title for an element.
*
* @param \Drupal\workflow\Entity\WorkflowTransitionInterface $transition
* The transition at hand.
* @param array $element
* The form element to be altered.
*
* @return array
* The form element $element.
*/
protected static function addWrapper(WorkflowTransitionInterface $transition, array &$element): array {
$workflow_settings = $transition->getWorkflow()?->getSettings();
$element = [
'#type' => ($workflow_settings['fieldset'] != 0) ? 'details' : 'container',
// Title may be NULL, since it will overwrite the 'History' page.
'#title' => $workflow_settings['name_as_title']
? (string) $transition->getFieldLabel()
: NULL,
'#collapsible' => ($workflow_settings['fieldset'] != 0),
'#open' => ($workflow_settings['fieldset'] != 2),
'#tree' => TRUE,
] + $element;
// Check if user wants to show single state option field. Hide if needed.
$options_type = $transition->getFromState()->get('single_state_widget');
if ($options_type == 'hide_fieldset') {
// A 'details' element is still visible. Override it.
$element['#type'] = 'container';
$element['#access'] = FALSE;
$element['widget']['#access'] = FALSE;
}
return $element;
}
/**
* Removes elements that are needed for a form, but not for a form element.
*
* @param \Drupal\workflow\Entity\WorkflowTransitionInterface $transition
* The transition at hand.
* @param array $workflow_form
* The gross form.
*
* @return array
* The trimmed form.
*/
public static function trimFormElement(WorkflowTransitionInterface $transition, array $workflow_form) {
// The following are not in Element::children.
$attributes = [
// The container settings.
'#type',
// '#title',
// '#collapsible',
// '#open',
'#tree',
// The WorkflowTransaction at hand.
'#default_value',
'#workflow_transition',
];
foreach ($attributes as $attribute_name) {
if (isset($workflow_form[$attribute_name])) {
$element[$attribute_name] ??= $workflow_form[$attribute_name];
}
unset($workflow_form[$attribute_name]);
}
// Determine and move the (attached) fields to the form.
foreach (Element::children($workflow_form) as $attribute_name) {
if ($transition->hasField($attribute_name)) {
if (isset($workflow_form[$attribute_name])) {
$element[$attribute_name] ??= $workflow_form[$attribute_name];
}
unset($workflow_form[$attribute_name]);
}
}
return $element;
}
/**
* Validates the Workflow Transition form.
*
* @param array $element
* The 'workflow_transition' form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @deprecated in workflow:2.1.9 and is removed from workflow:3.0.0. Replaced by default core code.
*/
public static function validateTransitionWidget(array &$element, FormStateInterface $form_state) {
// @todo Implement validate callback (for attached fields).
$transition = $element['#workflow_transition'];
$field_name = $transition->getFieldName();
if (!empty($transition)) {
// assert($transition instanceof WorkflowTransitionInterface);
$form_mode = $element['#form_mode'];
$form_display = EntityFormDisplay::collectRenderDisplay($transition, $form_mode);
// $form_display->extractFormValues($transition, $element, $form_state);
$form_display->validateFormValues($transition, $element, $form_state);
}
}
/**
* {@inheritdoc}
*/
public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
// Function called in: F___, F___ ______, F________, W_____, Widget submit.
// parent::extractFormValues($items, $form, $form_state);
if ($this->isDefaultValueWidget($form_state)) {
// On the Field settings page, User may not set a default value.
// (This is done by the Workflow module).
return [];
}
$field_name = $this->fieldDefinition->getName();
/** @var \Drupal\workflow\Plugin\Field\WorkflowItemListInterface $items */
$transition = $items->getDefaultTransition();
$field_name = $transition->getFieldName();
// Extract the values from $form_state->getValues().
// Note: Align ['#parents'] in Widget::form...(), Form::copy..(), ...
$parents = $form['#parents'];
$path = array_merge($parents, [$field_name]);
$key_exists = NULL;
$values = NestedArray::getValue($form_state->getValues(), $path, $key_exists);
if ($key_exists) {
// Function called in: F___, F___ ______, F________, W_____, Widget submit.
// Let the widget massage the submitted values.
$values = $this->massageFormValues($values, $form, $form_state);
// --- Start duplicate code in Widget. ------------------------------- //.
// To prepare Transition widget, use the Form, to get attached fields.
// We now promote the Workflow Widget to a complete form,
// and do extractFormValues() on that form,
// using the given $form_state->values().
// For that, create new $form_state and call wrapper function buildEntity().
// Create a new $form_state, to replace Node by WorkflowTransition.
/** @var \Drupal\Core\Entity\EntityFormInterface $form_object */
$workflow_form_state = NULL;
$workflow_form_state = clone $form_state;
// $workflow_form_state = &$form_state;
// The following line creates a $form_state with WT object.
$form_state_additions = [];
$form_object = WorkflowTransitionForm::createInstance(
$transition,
$workflow_form_state,
$form_state_additions
);
// --- End of (almost) duplicate code in Widget. --------------------- //.
// Find the first workflow. Quit if there is no Workflow on this page.
// @todo Support multiple workflows per entity.
$workflow_element = WorkflowTransitionButtons::getFirstWorkflowElement($form);
// Override WorkflowTransitionElement baseFields, created by Field UI.
// Especially to call the setter useActionButtons($options_type).
WorkflowTransitionElement::alter($workflow_element, $form_state, $form);
// Now, let core do its job and get the new transition.
if ($call_buildEntity = FALSE) {
$transition = $form_object->buildEntity($form, $form_state);
}
else {
// Use $form copy, and alter some attributes.
$workflow_form = $form;
// Add a layer of parents. $path is determined above already.
// Note: Align ['#parents'] in Widget::form...(), Form::copy..(), ...
$workflow_form['#parents'] = $path;
// Remove EntityBuilders, avoiding menu_ui error, but skipping translation.
unset($workflow_form['#entity_builders']);
$transition = $form_object->buildEntity($workflow_form, $form_state);
}
// Refresh the target entity, since multiple versions are lingering around.
// This is at least necessary for 'entity_create' form.
$transition->setTargetEntity($items->getEntity());
// // Assign the values and remove the empty ones.
// $items->setValue($values);
// $items->filterEmptyItems();
// Update targetEntity's itemList with the workflow field in two formats.
$transition->setEntityWorkflowField();
}
}
/**
* {@inheritdoc}
*/
public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
$values = parent::massageFormValues($values, $form, $form_state);
$field_name = $this->fieldDefinition->getName();
// @todo This is a hack. It is done in Form and Widget.
if (0 == count($values['field_name'])) {
// For some reason, (only?) the field name is not returned from core.
// This happens on a test case,
// where a WorkflowField contains a (nested) WorkflowField.
$values['field_name'][] = ['value' => $field_name];
}
return $values;
}
}
