workflow-8.x-1.x-dev/src/Form/WorkflowTransitionForm.php

src/Form/WorkflowTransitionForm.php
<?php

namespace Drupal\workflow\Form;

use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\workflow\Element\WorkflowTransitionButtons;
use Drupal\workflow\Element\WorkflowTransitionElement;
use Drupal\workflow\Entity\WorkflowTransitionInterface;

/**
 * Provides a Transition Form to be used in the Workflow Widget.
 */
class WorkflowTransitionForm extends ContentEntityForm {

  /*************************************************************************
   * Implementation of interface FormInterface.
   */

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    // We need a proprietary Form ID, to identify the unique forms
    // when multiple fields or entities are shown on 1 page.
    // Test this f.i. by checking the 'scheduled' box. It will not unfold.
    // $form_id = parent::getFormId();

    /** @var \Drupal\workflow\Entity\WorkflowTransitionInterface $transition */
    $transition = $this->getEntity();
    $field_name = $transition->getFieldName();

    // Entity may be empty on VBO bulk form.
    // $entity = $transition->getTargetEntity();
    // Compose Form ID from string + Entity ID + Field name.
    // Field ID contains entity_type, bundle, field_name.
    // The Form ID is unique, to allow for multiple forms per page.
    // $workflow_type_id = $transition->getWorkflowId();
    // Field name contains implicit entity_type & bundle (since 1 field per entity)
    // $entity_type = $transition->getTargetEntityTypeId();
    // $entity_id = $transition->getTargetEntityId();
    //
    // Emulate nodeForm convention.
    $suffix = $transition->id() ? 'edit_form' : 'form';
    $form_id = implode('_', [
      'workflow_transition_form',
      $transition->getTargetEntityTypeId(),
      $transition->getTargetEntityId() ?? 'new',
      $field_name,
      $suffix,
    ]);
    // $form_id = Html::getUniqueId($form_id);
    return $form_id;
  }

  /**
   * Gets the Transition Form Element (for e.g., Workflow History Tab)
   *
   * @param \Drupal\workflow\Entity\WorkflowTransitionInterface $transition
   *   The current transition.
   *
   * @return array
   *   The form render element.
   *
   * @usage Use WorkflowTransitionForm::getForm() in WT forms, and
   *   WorkflowDefaultWidget::form() in entity field widgets.
   */
  public static function getForm(WorkflowTransitionInterface $transition) {
    // Function called in: Form, Form submit, Formatter, W_____, W_____ s_____.
    // Build the form via the entityBuilder, not directly via formObject.
    // This will add alter hooks etc.
    /** @var \Drupal\Core\Entity\EntityFormBuilder $entity_form_builder */
    $entity_form_builder = \Drupal::getContainer()->get('entity.form_builder');
    $operation = 'add';

    $form_state_additions = [];
    $form = $entity_form_builder->getForm($transition, $operation, $form_state_additions);

    return $form;
  }

  /**
   * Gets the Form object, so it can be used by WorkflowWidget.
   *
   * @param \Drupal\workflow\Entity\WorkflowTransitionInterface $transition
   *   The entity at hand.
   * @param \Drupal\Core\Form\FormStateInterface|null $form_state
   *   The form state. Will be changed/created by reference(!).
   * @param array $form_state_additions
   *   Some additions.
   *
   * @return \Drupal\workflow\Form\WorkflowTransitionForm
   *   The ContentEntityForm object for WorkflowTransition.
   */
  public static function createInstance(WorkflowTransitionInterface $transition, ?FormStateInterface &$form_state, $form_state_additions = []): WorkflowTransitionForm {
    // Function called in: F___, F___ ______, F________, W_____, Widget submit.
    // Completely override EntityFormBuilder::getForm, since we need the $form_state.
    // EntityFormBuilder::entityTypeManager is protected, so create explicitly.
    $entity_type_manager = \Drupal::service('entity_type.manager');
    $operation = 'add';

    /** @var \Drupal\workflow\Form\WorkflowTransitionForm $form_object */
    // $form_state->getFormObject() returns NodeForm, CommentForm: wrong.
    $form_object = $entity_type_manager->getFormObject($transition->getEntityTypeId(), $operation);
    $form_object->setEntity($transition);

    $form_state ??= (new FormState)->setFormState($form_state_additions);
    $form_state->setFormObject($form_object);

    // Remove any submit handlers, since this is only used in Widget, not Form.
    if ($handlers = $form_state->getSubmitHandlers()) {
      $form_state->setSubmitHandlers([]);
    }

    $form_display = EntityFormDisplay::collectRenderDisplay($transition, $operation);
    $form_object->setFormDisplay($form_display, $form_state);

    return $form_object;
  }

  /**
   * {@inheritdoc}
   */
  public function getFormDisplay(FormStateInterface $form_state) {
    // Function called in: Form, Form submit, Formatter, W_____, W_____ ______.
    // Initializes the form state and the entity before the first form build.
    // parent::init($form_state);

    $form_display = parent::getFormDisplay($form_state);
    if (!$form_display) {
      // Display Error: Please save 'Manage form display' page of your Workflow.
      // $page = "/admin/config/workflow/workflow/$entity_bundle/form-display";
      // $page = "/admin/config/workflow/workflow";
      // $url = Url::fromRoute('entity.entity_form_display.node.default', ['node' => $entity_bundle]);
      // // Create a clickable link object.
      // $link = Link::fromTextAndUrl('View this page', $url)->toString();
      // Add the message.
      \Drupal::messenger()->addMessage(
        t('Please save "Manage form display" page of your Workflow type',
          // ['@link' => $link]
        ),
        MessengerInterface::TYPE_ERROR
      );
    }

    return $form_display;
  }

  /* *************************************************************************
   *
   * Implementation of interface EntityFormInterface (extends FormInterface).
   *
   */

  /**
   * Implements ContentEntityForm::form() and is called by buildForm().
   *
   * Caveat: !! It is not declared in the EntityFormInterface !!
   *
   * {@inheritdoc}
   */
  public function form(array $form, FormStateInterface $form_state) {
    // Function called in: Form, Form submit, F________, W_____, W_____ ______.
    /** @var \Drupal\workflow\Entity\WorkflowTransitionInterface $transition */
    $transition = $this->getEntity();
    $field_name = $transition->getFieldName();

    // The following determines correct WidgetBase::getWidgetState().
    // Note: Align ['#parents'] in Widget::form...(), Form::copy..(), ...
    $form['#parents'] = [$field_name];
    // Add '#workflow_transition' for WorkflowTransitionElement::alter().
    $form['#workflow_transition'] = $transition;
    $form = parent::form($form, $form_state);

    // The following is done in WorkflowTransitionElement::processTransition,
    // but only if the element type is set correctly.
    // Override WorkflowTransitionElement baseFields, created by Field UI.
    // Add '#workflow_transition' for WorkflowTransitionElement::alter().
    $form = WorkflowTransitionElement::alter($form, $form_state, $form);

    // Remove unwanted title. It overwrites the page title (in Claro theme).
    unset($form['#title']);

    return $form;
  }

  /**
   * Implements ContentEntityForm::actions() and is called by buildForm().
   *
   * Returns an array of supported actions for the current entity form.
   * Caveat: !! It is not declared in the EntityFormInterface !!
   *
   * {@inheritdoc}
   */
  protected function actions(array $form, FormStateInterface $form_state) {
    $actions = parent::actions($form, $form_state);
    // Action buttons are added in common workflow_form_alter(),
    // addActionButtons(), since it will be done in many form_id's.
    // Keep aligned: workflow_form_alter(), WorkflowTransitionForm::actions().
    if (!empty($actions['submit']['#value'])) {
      $actions['submit']['#value'] = $this->t('Update workflow');
    }
    return $actions;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    // Function called in: F___, Form submit, F________, W_____, W_____ ______.
    /** @var \Drupal\workflow\Entity\WorkflowTransitionInterface $transition */
    $transition = $this->entity;
    $field_name = $transition->getFieldName();

    parent::submitForm($form, $form_state);
  }

  /**
   * Implements ContentEntityForm::buildEntity().
   *
   * {@inheritdoc}
   */
  public function buildEntity(array $form, FormStateInterface $form_state): WorkflowTransitionInterface {
    // Function called in: F___, Form submit, F________, W_____, Widget submit.
    /** @var \Drupal\workflow\Entity\WorkflowTransitionInterface $transition */
    $transition = parent::buildEntity($form, $form_state);
    return $transition;
  }

  /**
   * Implements ContentEntityForm::copyFormValuesToEntity().
   *
   * This is called from:
   * - WorkflowTransitionForm::copyFormValuesToEntity(),
   * - WorkflowDefaultWidget.
   *
   * {@inheritdoc}
   */
  public function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
    // Function called in: F___, Form submit, F________, W_____, Widget submit.
    /*
    // The following call to parent should set all data correct to $transition.
    // 20250815 test results:
    // field\case | node widget | WTH create | WTH edit | WTH revert | Action widget | ActionButtons | Comment |
    // field_name | n/a (4)     | ok idem    | ok idem  |  n/a       |               |               |         |
    // from_sid   |             | ok idem    | ok idem  |  n/a       |               |               |         |
    // to_sid     |             | ok update  | ok idem  |  n/a       |               |               |         |
    // timestamp  |             | OK(20250929) | NOK (1) | n/a       |               |               |         |
    // scheduled.tStamp |       | nok (3)      | NOK     |  n/a      |               |               |         |
    // comment    |             | ok         | ok       |  n/a       |               |               |         |
    // forced     |             | ok 'no'    | ok 'no'  |  n/a       |               |               |         |
    // executed   |             | ok 'yes'   | ok 'yes' |  n/a       |               |               |         |
    // extra field|             | ok (2)     | ok (2)   |  n/a       |               |               |         |
    // @todo 20250815 Known issues for WTF:copyFormValuesToEntity():
    // (1): Executed/Scheduled timestamp set to current time (on History page).
    // (2): Extra field on WT~edit form are editable in UI, so 'updated' is ok.
    // -    Extra field for scheduled WT is not supported yet.
    // (3): $is_scheduled wrong, due to (1).
    // (4): Field widget calls copyFormValuesToTransition() directly.
     */

    /** @var \Drupal\workflow\Entity\WorkflowTransitionInterface $transition */
    $transition = $entity;
    $field_name = $transition->getFieldName();

    // Update the Transition $entity.
    // Extract the values from $form_state->getValues().
    // Note: Align ['#parents'] in Widget::form...(), Form::copy..(), ...
    $parents = $form['#parents'];
    $path = $parents;
    $key_exists = NULL;
    $values = NestedArray::getValue($form_state->getValues(), $path, $key_exists);

    /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
    if ($call_parent = FALSE) {
      // Swap entities (Node vs Transition) via FormDisplay.
      $original = $this->getFormDisplay($form_state);
      $this->setFormDisplay($this->getFormDisplay($form_state), $form_state);
      parent::copyFormValuesToEntity($entity, $form, $form_state);
      $this->setFormDisplay($original, $form_state);
    }
    else {
      // Workflow: do not determine $form_display from $from_state.
      // First, extract values from widgets.
      // $extracted = $this->getFormDisplay($form_state)->extractFormValues($entity, $form, $form_state);
      $operation = 'add';
      $form_display = EntityFormDisplay::collectRenderDisplay($transition, $operation);
      $extracted = $form_display->extractFormValues($entity, $form, $form_state);

      // @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.
        // Restore the field.
        $values['field_name'][] = ['value' => $field_name];
        $entity->set('field_name', $field_name);
      }

      // Then extract the values of fields that are not rendered through widgets,
      // by simply copying from top-level form values. This leaves the fields
      // that are not being edited within this form untouched.
      // $values = $form_state->getValues();
      foreach ($values as $name => $item_values) {
        if ($entity->hasField($name) && !isset($extracted[$name])) {
          $entity->set($name, $item_values);
        }
      }
    }

    if ($transition->isExecuted()) {
      // For executed transitions,
      // only comments and attached fields are updated.
      // That happens also without this function, perhaps with above hook.
      return;
    }

    // Update the Transition $entity.
    // Use own version of copyFormValuesToEntity() to fix missing fields.
    // Note: Pay attention use case where WT changes to WST and v.v.
    // @todo This is not needed (anymore) on WFH, only for action buttons.
    if ($debug = FALSE) {
      // For debugging/testing, toggle above value,
      // so you can compare the values from transition vs. widget.
      // The transition may already be OK by core's copyFormValuesToEntity().
      $uid = $transition->getOwnerId();
      $field_name = $transition->getFieldName();
      $from_sid = $transition->getFromSid();
      $to_sid = $transition->getToSid();
      $scheduled = $transition->isScheduled();
      $timestamp = $transition->getTimestamp();
      $timestamp_formatted = $transition->getTimestampFormatted();
      $comment = $transition->getComment();
      $force = $transition->isForced();
      $executed = $transition->isExecuted();
      $transition->dpm();
    }

    // On Node form, restore fact that uid is overwritten by Node owner.
    $transition->setOwner(workflow_current_user());
    $uid = $transition->getOwnerId();

    // Repair when a future timestamp is set, but schedule toggle is disabled.
    $scheduled = $transition->isScheduled();
    $scheduled = $values['scheduled']['value'] ?? NULL;
    $timestamp = ($scheduled)
      // Timestamp also determines $transition::is_scheduled().
      ? $transition->getTimestamp()
      : $transition->getDefaultRequestTime();
    $transition->setTimestamp($timestamp);
    $timestamp = $transition->getTimestamp();
    $timestamp_formatted = $transition->getTimestampFormatted();

    // Get new SID, taking into account action buttons vs. options.
    // Behavior is different between History view and Node edit widget,
    // since buttons are lost from widget's $new_form_state.
    $action_values = WorkflowTransitionButtons::getTriggeringButton($transition, $form_state, $values);
    $transition->setValues($action_values['to_sid']);
    $to_sid = $transition->getToSid();

    // Update targetEntity's itemList (aka input $items) with the transition.
    // This is also needed for parent::extractFormValues().
    // Note: This is a wrapper around $items->setValue($values);
    $transition->setEntityWorkflowField();

    // Add attached fields.
    // Oct-2025 v2.1.8: Leave active, since needed for file upload hook
    // Nov-2025 v2.1.10: Can be removed, since attached widgets work fine.
    // via hook copy_form_values_to_transition_field_alter.
    $transition->copyAttachedFields($form, $form_state);
  }

  /**
   * {@inheritdoc}
   *
   * Execute transition and update the target entity.
   */
  public function save(array $form, FormStateInterface $form_state) {
    // Function called in: F___, Form submit, F________, W_____, W_____ ______.
    /** @var \Drupal\workflow\Entity\WorkflowTransitionInterface $transition */
    $transition = $this->getEntity();
    return $transition->executeAndUpdateEntity();
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc