Verified Commit 1f5b3ee8 authored by Dave Long's avatar Dave Long
Browse files

task: #3536726 Use CallableResolver for form callbacks

By: berdir
By: nicxvan
By: kim.pepper
By: joachim
(cherry picked from commit d3a9ff85)
parent ff47fed7
Loading
Loading
Loading
Loading
Loading
+4 −4
Original line number Diff line number Diff line
@@ -519,15 +519,15 @@ services:
  Drupal\Core\File\FileUrlGeneratorInterface: '@file_url_generator'
  form_builder:
    class: Drupal\Core\Form\FormBuilder
    arguments: ['@form_validator', '@form_submitter', '@form_cache', '@module_handler', '@event_dispatcher', '@request_stack', '@class_resolver', '@element_info', '@theme.manager', '@?csrf_token']
    arguments: ['@form_validator', '@form_submitter', '@form_cache', '@module_handler', '@event_dispatcher', '@request_stack', '@class_resolver', '@element_info', '@theme.manager', '@?csrf_token', '@callable_resolver']
  Drupal\Core\Form\FormBuilderInterface: '@form_builder'
  form_validator:
    class: Drupal\Core\Form\FormValidator
    arguments: ['@request_stack', '@string_translation', '@csrf_token', '@logger.channel.form', '@form_error_handler']
    arguments: ['@request_stack', '@string_translation', '@csrf_token', '@logger.channel.form', '@form_error_handler', '@callable_resolver']
  Drupal\Core\Form\FormValidatorInterface: '@form_validator'
  form_submitter:
    class: Drupal\Core\Form\FormSubmitter
    arguments: ['@request_stack', '@url_generator', '@redirect_response_subscriber']
    arguments: ['@request_stack', '@url_generator', '@redirect_response_subscriber', '@callable_resolver']
  Drupal\Core\Form\FormSubmitterInterface: '@form_submitter'
  form_error_handler:
    class: Drupal\Core\Form\FormErrorHandler
@@ -1350,7 +1350,7 @@ services:
  Drupal\Core\Entity\HtmlEntityFormController: '@controller.entity_form'
  form_ajax_response_builder:
    class: Drupal\Core\Form\FormAjaxResponseBuilder
    arguments: ['@main_content_renderer.ajax', '@current_route_match']
    arguments: ['@main_content_renderer.ajax', '@current_route_match', '@callable_resolver']
  Drupal\Core\Form\FormAjaxResponseBuilderInterface: '@form_ajax_response_builder'
  router_listener:
    class: Symfony\Component\HttpKernel\EventListener\RouterListener
+24 −7
Original line number Diff line number Diff line
@@ -3,11 +3,12 @@
namespace Drupal\Core\Entity;

use Drupal\Component\Serialization\Json;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Utility\CallableResolver;

/**
 * Base class for entity forms.
@@ -304,18 +305,34 @@ public function buildEntity(array $form, FormStateInterface $form_state) {
    // properties.
    if (isset($form['#entity_builders'])) {
      foreach ($form['#entity_builders'] as $function) {
        call_user_func_array($form_state->prepareCallback($function), [
          $entity->getEntityTypeId(),
          $entity,
          &$form,
          &$form_state,
        ]);
        $callable = $this->getCallableFromDefinition($form_state->prepareCallback($function));
        $callable($entity->getEntityTypeId(), $entity, $form, $form_state);
      }
    }

    return $entity;
  }

  /**
   * Gets a callable from a string or array definition if possible.
   *
   * Checks if the service container is available and uses the callable
   * resolver service to get the callable from the definition. Otherwise, it
   * returns the definition as is.
   *
   * @param string|array $definition
   *   The callable definition.
   *
   * @return callable|string|array
   *   The callable, or the original definition if it could not be resolved.
   */
  protected function getCallableFromDefinition(string | array $definition): callable | string | array {
    if (\Drupal::hasService(CallableResolver::class)) {
      return \Drupal::service(CallableResolver::class)->getCallableFromDefinition($definition);
    }
    return $definition;
  }

  /**
   * Copies top-level form values to entity properties.
   *
+21 −8
Original line number Diff line number Diff line
@@ -6,6 +6,7 @@
use Drupal\Core\Ajax\UpdateBuildIdCommand;
use Drupal\Core\Render\MainContent\MainContentRendererInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Utility\CallableResolver;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;

@@ -32,16 +33,22 @@ class FormAjaxResponseBuilder implements FormAjaxResponseBuilderInterface {
  protected $routeMatch;

  /**
   * Constructs a new FormAjaxResponseBuilder.
   *
   * @param \Drupal\Core\Render\MainContent\MainContentRendererInterface $ajax_renderer
   *   The ajax renderer.
   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
   *   The current route match.
   * The callable resolver service.
   */
  public function __construct(MainContentRendererInterface $ajax_renderer, RouteMatchInterface $route_match) {
  protected CallableResolver $callableResolver;

  public function __construct(
    MainContentRendererInterface $ajax_renderer,
    RouteMatchInterface $route_match,
    ?CallableResolver $callableResolver = NULL,
  ) {
    $this->ajaxRenderer = $ajax_renderer;
    $this->routeMatch = $route_match;
    if (!$callableResolver) {
      @trigger_error(sprintf('Calling %s() without the $callableResolver param is deprecated in drupal:11.3.0 and is required in drupal:12.0.0. See https://www.drupal.org/node/3548821', __METHOD__), E_USER_DEPRECATED);
      $callableResolver = \Drupal::service(CallableResolver::class);
    }
    $this->callableResolver = $callableResolver;
  }

  /**
@@ -63,9 +70,15 @@ public function buildResponse(Request $request, array $form, FormStateInterface
      $callback = $triggering_element['#ajax']['callback'];
    }
    $callback = $form_state->prepareCallback($callback);
    if (empty($callback) || !is_callable($callback)) {
    if (empty($callback)) {
      throw new HttpException(500, 'The specified #ajax callback is empty or not callable.');
    }
    try {
      $callback = $this->callableResolver->getCallableFromDefinition($callback);
    }
    catch (\InvalidArgumentException $e) {
      throw new HttpException(500, 'The specified #ajax callback is empty or not callable.', $e);
    }
    $result = call_user_func_array($callback, [&$form, &$form_state, $request]);

    // If the callback is an #ajax callback, the result is a render array, and
+29 −32
Original line number Diff line number Diff line
@@ -18,11 +18,12 @@
use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Drupal\Core\Utility\CallableResolver;
use Symfony\Component\HttpFoundation\FileBag;
use Symfony\Component\HttpFoundation\InputBag;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

/**
 * Provides form building and processing.
@@ -108,6 +109,11 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormS
   */
  protected $formCache;

  /**
   * The callable resolver.
   */
  protected CallableResolver $callableResolver;

  /**
   * Defines callables that are safe to run with invalid CSRF tokens.
   *
@@ -149,31 +155,19 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormS
    'Drupal\Core\Render\Element\Weight::valueCallback',
  ];

  /**
   * Constructs a new FormBuilder.
   *
   * @param \Drupal\Core\Form\FormValidatorInterface $form_validator
   *   The form validator.
   * @param \Drupal\Core\Form\FormSubmitterInterface $form_submitter
   *   The form submission processor.
   * @param \Drupal\Core\Form\FormCacheInterface $form_cache
   *   The form cache.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher
   *   The event dispatcher.
   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   The request stack.
   * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
   *   The class resolver.
   * @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
   *   The element info manager.
   * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
   *   The theme manager.
   * @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
   *   The CSRF token generator.
   */
  public function __construct(FormValidatorInterface $form_validator, FormSubmitterInterface $form_submitter, FormCacheInterface $form_cache, ModuleHandlerInterface $module_handler, EventDispatcherInterface $event_dispatcher, RequestStack $request_stack, ClassResolverInterface $class_resolver, ElementInfoManagerInterface $element_info, ThemeManagerInterface $theme_manager, ?CsrfTokenGenerator $csrf_token = NULL) {
  public function __construct(
    FormValidatorInterface $form_validator,
    FormSubmitterInterface $form_submitter,
    FormCacheInterface $form_cache,
    ModuleHandlerInterface $module_handler,
    EventDispatcherInterface $event_dispatcher,
    RequestStack $request_stack,
    ClassResolverInterface $class_resolver,
    ElementInfoManagerInterface $element_info,
    ThemeManagerInterface $theme_manager,
    ?CsrfTokenGenerator $csrf_token = NULL,
    ?CallableResolver $callableResolver = NULL,
  ) {
    $this->formValidator = $form_validator;
    $this->formSubmitter = $form_submitter;
    $this->formCache = $form_cache;
@@ -184,6 +178,11 @@ public function __construct(FormValidatorInterface $form_validator, FormSubmitte
    $this->elementInfo = $element_info;
    $this->csrfToken = $csrf_token;
    $this->themeManager = $theme_manager;
    if (!$callableResolver) {
      @trigger_error(sprintf('Calling %s() without the $callableResolver param is deprecated in drupal:11.3.0 and is required in drupal:12.0.0. See https://www.drupal.org/node/3548821', __METHOD__), E_USER_DEPRECATED);
      $callableResolver = \Drupal::service(CallableResolver::class);
    }
    $this->callableResolver = $callableResolver;
  }

  /**
@@ -1062,11 +1061,8 @@ public function doBuildForm($form_id, &$element, FormStateInterface &$form_state
    if (isset($element['#process']) && !$element['#processed']) {
      foreach ($element['#process'] as $callback) {
        $complete_form = &$form_state->getCompleteForm();
        $element = call_user_func_array($form_state->prepareCallback($callback), [
          &$element,
          &$form_state,
          &$complete_form,
        ]);
        $callable = $this->callableResolver->getCallableFromDefinition($form_state->prepareCallback($callback));
        $element = $callable($element, $form_state, $complete_form);
      }
      $element['#processed'] = TRUE;
    }
@@ -1137,7 +1133,8 @@ public function doBuildForm($form_id, &$element, FormStateInterface &$form_state
    // after normal input parsing has been completed.
    if (isset($element['#after_build']) && !isset($element['#after_build_done'])) {
      foreach ($element['#after_build'] as $callback) {
        $element = call_user_func_array($form_state->prepareCallback($callback), [$element, &$form_state]);
        $callable = $this->callableResolver->getCallableFromDefinition($form_state->prepareCallback($callback));
        $element = $callable($element, $form_state);
      }
      $element['#after_build_done'] = TRUE;
    }
+13 −10
Original line number Diff line number Diff line
@@ -3,11 +3,12 @@
namespace Drupal\Core\Form;

use Drupal\Core\EventSubscriber\RedirectResponseSubscriber;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\Core\Url;
use Drupal\Core\Utility\CallableResolver;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Drupal\Core\Routing\UrlGeneratorInterface;

/**
 * Provides submission processing for forms.
@@ -15,20 +16,21 @@
class FormSubmitter implements FormSubmitterInterface {

  /**
   * Constructs a new FormSubmitter.
   *
   * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
   *   The request stack.
   * @param \Drupal\Core\Routing\UrlGeneratorInterface $urlGenerator
   *   The URL generator.
   * @param \Drupal\Core\EventSubscriber\RedirectResponseSubscriber $redirectResponseSubscriber
   *   The redirect response subscriber.
   * The callable resolver.
   */
  protected CallableResolver $callableResolver;

  public function __construct(
    protected RequestStack $requestStack,
    protected UrlGeneratorInterface $urlGenerator,
    protected RedirectResponseSubscriber $redirectResponseSubscriber,
    ?CallableResolver $callableResolver = NULL,
  ) {
    if (!$callableResolver) {
      @trigger_error(sprintf('Calling %s() without the $callableResolver param is deprecated in drupal:11.3.0 and is required in drupal:12.0.0. See https://www.drupal.org/node/3548821', __METHOD__), E_USER_DEPRECATED);
      $callableResolver = \Drupal::service(CallableResolver::class);
    }
    $this->callableResolver = $callableResolver;
  }

  /**
@@ -102,7 +104,8 @@ public function executeSubmitHandlers(&$form, FormStateInterface &$form_state) {
        $batch['has_form_submits'] = TRUE;
      }
      else {
        call_user_func_array($form_state->prepareCallback($callback), [&$form, &$form_state]);
        $callable = $this->callableResolver->getCallableFromDefinition($form_state->prepareCallback($callback));
        $callable($form, $form_state);
      }
    }
  }
Loading