Commit a32aca1d authored by mxh's avatar mxh
Browse files

Issue #3272273 by mxh: Add a form context stack

parent 35d10d14
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
type: module
name: "Context Stack Form"
description: "Provides a context stack for objects that belong to forms."
core_version_requirement: ^9 || ^10
package: Other
dependencies:
  - context_stack:context_stack
+72 −0
Original line number Diff line number Diff line
<?php

/**
 * @file
 * The Context Stack Form module file.
 */

use Drupal\context_stack\ContextStackFactory;
use Drupal\context_stack\Plugin\Context\GenericEntityContext;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityFormInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;

/**
 * Implements hook_form_alter().
 */
function context_stack_form_form_alter(&$form, FormStateInterface $form_state) {
  $form_object = $form_state->getFormObject();
  $entities = [];
  foreach ($form as $entry) {
    if (($entry instanceof EntityInterface) && !isset($entities[$entry->getEntityTypeId()])) {
      $entities[$entry->getEntityTypeId()] = $entry;
    }
  }
  if ($form_object instanceof EntityFormInterface) {
    $entity = $form_object->getEntity();
    if (!isset($entities[$entity->getEntityTypeId()])) {
      $entities[$entity->getEntityTypeId()] = $entity;
    }
  }
  if (empty($entities)) {
    return;
  }

  $collection = ContextStackFactory::get()->createCollection();
  foreach ($entities as $entity_type_id => $entity) {
    $collection->addContext(GenericEntityContext::fromEntity($entity, $entity->getEntityType()->getLabel()), $entity_type_id);
  }
  $form['#context_stack']['form'] = $collection;
  // Add cacheability from the context stack to the render array. This is done
  // by temporarily adding the collection to the stack, fetch its calculated
  // metadata, then remove it so the render system would render every entity
  // one-by-one, using the current context that is being pushed via pre-render.
  /** @var \Drupal\context_stack\ContextStackInterface $context_stack */
  $context_stack = \Drupal::service('context_stack.form');
  $context_stack->push(clone $collection);
  CacheableMetadata::createFromRenderArray($form)
    ->addCacheableDependency($context_stack)
    ->applyTo($form);
  $context_stack->pop();
  // Add the pre-render callback that will push the current context into the
  // context stack. The post-render callback is responsible for removing the
  // added context collection from the stack afterwards.
  $form['#pre_render'][] = [
    '\Drupal\context_stack_form\Render\FormStack',
    'preRender',
  ];
  $form['#post_render'][] = [
    '\Drupal\context_stack_form\Render\FormStack',
    'postRender',
  ];
}

/**
 * Implements hook_inline_entity_form_entity_form_alter().
 *
 * This hook is only available when inline_entity_form is installed.
 */
function context_stack_form_inline_entity_form_entity_form_alter(&$form, FormStateInterface $form_state) {
  context_stack_form_form_alter($form, $form_state);
}
+7 −0
Original line number Diff line number Diff line
services:
  context_stack.form:
    class: Drupal\context_stack\ContextStack
    factory: ['@context_stack.factory', createStack]
    arguments: [form]
    tags:
      - { name: context_stack }
+63 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\context_stack_form\Render;

use Drupal\Core\Security\TrustedCallbackInterface;

/**
 * Trusted callbacks for pushing entities into the "form" stack.
 */
final class FormStack implements TrustedCallbackInterface {

  /**
   * Pre render callback to add form entities to the stack.
   *
   * @param array $element
   *   The render element.
   *
   * @return array
   *   The (preprocessed) render element.
   */
  public static function preRender(array $element) {
    if (!empty($element['#context_stack']['form'])) {
      /** @var \Drupal\context_stack\ContextCollectionInterface $collection */
      $collection = $element['#context_stack']['form'];
      /** @var \Drupal\context_stack\ContextStackInterface $context_stack */
      $context_stack = \Drupal::service('context_stack.form');
      if (!$collection->getParent() && $collection !== $context_stack->current()) {
        $context_stack->push($collection);
      }
      unset($element['#context_stack']['form']);
    }
    return $element;
  }

  /**
   * Post render callback to pop form entities off the stack.
   *
   * @param mixed $children
   *   The result of rendering the children.
   * @param mixed $element
   *   The element as render array.
   *
   * @return mixed
   *   The post-processed result.
   */
  public static function postRender($children, $element) {
    if (!empty($element['#context_stack']['form'])) {
      $collection = $element['#context_stack']['form'];
      /** @var \Drupal\context_stack\ContextStackInterface $context_stack */
      $context_stack = \Drupal::service('context_stack.form');
      while ($collection !== $context_stack->pop() && $context_stack->current());
    }
    return $children;
  }

  /**
   * {@inheritdoc}
   */
  public static function trustedCallbacks() {
    return ['preRender', 'postRender'];
  }

}