diff --git a/core/core.services.yml b/core/core.services.yml index ddd4268..89048f3 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -896,7 +896,7 @@ services: class: Drupal\Core\Entity\HtmlEntityFormController arguments: ['@controller_resolver', '@form_builder', '@entity.manager'] ajax_form_handler_rename_me: - class: Drupal\Core\Form\FormAjaxHandler + class: Drupal\Core\Form\FormAjaxResponseHandler arguments: ['@main_content_renderer.ajax', '@current_route_match'] router_listener: class: Symfony\Component\HttpKernel\EventListener\RouterListener diff --git a/core/lib/Drupal/Core/Controller/FormController.php b/core/lib/Drupal/Core/Controller/FormController.php index b803c10..317a8ee 100644 --- a/core/lib/Drupal/Core/Controller/FormController.php +++ b/core/lib/Drupal/Core/Controller/FormController.php @@ -58,8 +58,9 @@ public function __construct(ControllerResolverInterface $controller_resolver, Fo * @param \Drupal\Core\Routing\RouteMatchInterface $route_match * The route match. * - * @return array - * The render array that results from invoking the controller. + * @return \Drupal\Core\Form\FormAndFormState + * An object containing the render array that results from invoking the + * controller and the current state of the form. */ public function getContentResult(Request $request, RouteMatchInterface $route_match) { $form_arg = $this->getFormArgument($route_match); diff --git a/core/lib/Drupal/Core/EventSubscriber/FormControllerSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FormControllerSubscriber.php index 8eec270..8a878c5 100644 --- a/core/lib/Drupal/Core/EventSubscriber/FormControllerSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/FormControllerSubscriber.php @@ -9,7 +9,7 @@ use Drupal\Core\Ajax\UpdateBuildIdCommand; use Drupal\Core\Controller\ControllerResolverInterface; -use Drupal\Core\Form\FormAjaxHandlerInterface; +use Drupal\Core\Form\FormAjaxResponseHandlerInterface; use Drupal\Core\Form\FormAndFormState; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; @@ -30,7 +30,7 @@ class FormControllerSubscriber implements EventSubscriberInterface { /** * @todo. * - * @var \Drupal\Core\Form\FormAjaxHandlerInterface + * @var \Drupal\Core\Form\FormAjaxResponseHandlerInterface */ protected $formAjaxHandler; @@ -44,10 +44,10 @@ class FormControllerSubscriber implements EventSubscriberInterface { /** * @todo. * - * @param \Drupal\Core\Form\FormAjaxHandlerInterface $form_ajax_handler + * @param \Drupal\Core\Form\FormAjaxResponseHandlerInterface $form_ajax_handler * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver */ - public function __construct(FormAjaxHandlerInterface $form_ajax_handler, ControllerResolverInterface $controller_resolver) { + public function __construct(FormAjaxResponseHandlerInterface $form_ajax_handler, ControllerResolverInterface $controller_resolver) { $this->formAjaxHandler = $form_ajax_handler; $this->controllerResolver = $controller_resolver; } @@ -69,14 +69,16 @@ public function onKernelController(FilterControllerEvent $event) { // controller.entity_form:getContentResult). $arguments = $this->controllerResolver->getArguments($request, $original_controller); $response = call_user_func_array($original_controller, $arguments); + if (!$response instanceof FormAndFormState) { + throw new \LogicException('asdf'); + } - /** @var \Drupal\Core\Form\FormAndFormState $response */ $form = $response->getForm(); $form_state = $response->getFormState(); - $form_build_id = $request->request->get('form_build_id'); + $form_build_id = $request->get('form_build_id'); $commands = []; - if ($form_build_id != $form['#build_id']) { + if ($form_build_id !== $form['#build_id']) { // If the form build ID has changed, issue an Ajax command to update it. $commands[] = new UpdateBuildIdCommand($form_build_id, $form['#build_id']); } diff --git a/core/lib/Drupal/Core/Form/FormAjaxHandler.php b/core/lib/Drupal/Core/Form/FormAjaxResponseHandler.php similarity index 81% rename from core/lib/Drupal/Core/Form/FormAjaxHandler.php rename to core/lib/Drupal/Core/Form/FormAjaxResponseHandler.php index dd76d03..f841d50 100644 --- a/core/lib/Drupal/Core/Form/FormAjaxHandler.php +++ b/core/lib/Drupal/Core/Form/FormAjaxResponseHandler.php @@ -2,7 +2,7 @@ /** * @file - * Contains \Drupal\Core\Form\FormAjaxHandler. + * Contains \Drupal\Core\Form\FormAjaxResponseHandler. */ namespace Drupal\Core\Form; @@ -16,7 +16,7 @@ /** * @todo. */ -class FormAjaxHandler implements FormAjaxHandlerInterface { +class FormAjaxResponseHandler implements FormAjaxResponseHandlerInterface { /** * The main content to AJAX Response renderer. @@ -46,13 +46,13 @@ public function __construct(MainContentRendererInterface $ajax_renderer, RouteMa */ public function handle(Request $request, array $form, FormStateInterface $form_state, array $commands) { // We need to return the part of the form (or some other content) that needs - // to be re-rendered so the browser can update the page with changed content. - // Since this is the generic menu callback used by many Ajax elements, it is - // up to the #ajax['callback'] function of the element (may or may not be a - // button) that triggered the Ajax request to determine what needs to be - // rendered. + // to be re-rendered so the browser can update the page with changed + // content. Since this is the generic menu callback used by many Ajax + // elements, it is up to the #ajax['callback'] function of the element (may + // or may not be a button) that triggered the Ajax request to determine what + // needs to be rendered. $callback = NULL; - if ($triggering_element = $form_state->getTriggeringElement()) { + if (($triggering_element = $form_state->getTriggeringElement()) && isset($triggering_element['#ajax']['callback'])) { $callback = $triggering_element['#ajax']['callback']; } $callback = $form_state->prepareCallback($callback); diff --git a/core/lib/Drupal/Core/Form/FormAjaxHandlerInterface.php b/core/lib/Drupal/Core/Form/FormAjaxResponseHandlerInterface.php similarity index 84% rename from core/lib/Drupal/Core/Form/FormAjaxHandlerInterface.php rename to core/lib/Drupal/Core/Form/FormAjaxResponseHandlerInterface.php index faaabe3..bc28674 100644 --- a/core/lib/Drupal/Core/Form/FormAjaxHandlerInterface.php +++ b/core/lib/Drupal/Core/Form/FormAjaxResponseHandlerInterface.php @@ -2,7 +2,7 @@ /** * @file - * Contains \Drupal\Core\Form\FormAjaxHandlerInterface. + * Contains \Drupal\Core\Form\FormAjaxResponseHandlerInterface. */ namespace Drupal\Core\Form; @@ -12,7 +12,7 @@ /** * @todo. */ -interface FormAjaxHandlerInterface { +interface FormAjaxResponseHandlerInterface { /** * @todo. diff --git a/core/lib/Drupal/Core/Render/Element/RenderElement.php b/core/lib/Drupal/Core/Render/Element/RenderElement.php index 53f7a23..e97f33b 100644 --- a/core/lib/Drupal/Core/Render/Element/RenderElement.php +++ b/core/lib/Drupal/Core/Render/Element/RenderElement.php @@ -7,6 +7,7 @@ namespace Drupal\Core\Render\Element; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\EventSubscriber\FormControllerSubscriber; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\PluginBase; @@ -133,8 +134,7 @@ public static function processAjaxForm(&$element, FormStateInterface $form_state $form_state->setPostToOriginalUrl(FALSE); } if ($form_state->willPostToOriginalUrl()) { - $element['#ajax']['url'] = Url::fromRoute(''); - $element['#ajax']['options']['query'][FormControllerSubscriber::MAGIC_STRING_TO_BE_RENAMED] = 'drupal_ajax_post'; + $element['#ajax']['url'] = Url::fromRoute('', [], ['query' => [FormControllerSubscriber::MAGIC_STRING_TO_BE_RENAMED => 'drupal_ajax_post']]); } } $element = static::preRenderAjaxForm($element); @@ -260,7 +260,9 @@ public static function preRenderAjaxForm($element) { // Convert \Drupal\Core\Url object to string. if (isset($settings['url']) && $settings['url'] instanceof Url) { - $settings['url'] = $settings['url']->setOptions($settings['options'])->toString(); + $url_options = $settings['url']->getOptions(); + $url_options = NestedArray::mergeDeep($settings['options'], $url_options); + $settings['url'] = $settings['url']->setOptions($url_options)->toString(); } else { $settings['url'] = NULL; diff --git a/core/modules/file/src/Controller/FileWidgetAjaxController.php b/core/modules/file/src/Controller/FileWidgetAjaxController.php index 44967f0..607f996 100644 --- a/core/modules/file/src/Controller/FileWidgetAjaxController.php +++ b/core/modules/file/src/Controller/FileWidgetAjaxController.php @@ -10,7 +10,7 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\ReplaceCommand; -use Drupal\Core\Form\FormAjaxHandlerInterface; +use Drupal\Core\Form\FormAjaxResponseHandlerInterface; use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\Render\RendererInterface; use Drupal\system\Controller\FormAjaxController; @@ -39,12 +39,14 @@ class FileWidgetAjaxController extends FormAjaxController { * A logger instance. * @param \Drupal\Core\Form\FormBuilderInterface $form_builder * The form builder. - * @param \Drupal\Core\Form\FormAjaxHandlerInterface $form_ajax_handler - * @todo. - * @param \Drupal\Core\Render\RendererInterface $renderer + * @param \Drupal\Core\Form\FormAjaxResponseHandlerInterface $form_ajax_handler + * +*@todo. + * +*@param \Drupal\Core\Render\RendererInterface $renderer * The renderer. */ - public function __construct(LoggerInterface $logger, FormBuilderInterface $form_builder, FormAjaxHandlerInterface $form_ajax_handler, RendererInterface $renderer) { + public function __construct(LoggerInterface $logger, FormBuilderInterface $form_builder, FormAjaxResponseHandlerInterface $form_ajax_handler, RendererInterface $renderer) { parent::__construct($logger, $form_builder, $form_ajax_handler); $this->renderer = $renderer; } diff --git a/core/modules/system/src/Controller/FormAjaxController.php b/core/modules/system/src/Controller/FormAjaxController.php index 5894d28..a91e7c0 100644 --- a/core/modules/system/src/Controller/FormAjaxController.php +++ b/core/modules/system/src/Controller/FormAjaxController.php @@ -9,7 +9,7 @@ use Drupal\Core\Ajax\UpdateBuildIdCommand; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; -use Drupal\Core\Form\FormAjaxHandlerInterface; +use Drupal\Core\Form\FormAjaxResponseHandlerInterface; use Drupal\Core\Form\FormState; use Drupal\Core\Form\FormBuilderInterface; use Drupal\system\FileAjaxForm; @@ -40,7 +40,7 @@ class FormAjaxController implements ContainerInjectionInterface { /** * @todo. * - * @var \Drupal\Core\Form\FormAjaxHandlerInterface + * @var \Drupal\Core\Form\FormAjaxResponseHandlerInterface */ protected $formAjaxHandler; @@ -51,10 +51,11 @@ class FormAjaxController implements ContainerInjectionInterface { * A logger instance. * @param \Drupal\Core\Form\FormBuilderInterface $form_builder * The form builder. - * @param \Drupal\Core\Form\FormAjaxHandlerInterface $form_ajax_handler - * @todo. + * @param \Drupal\Core\Form\FormAjaxResponseHandlerInterface $form_ajax_handler + * +*@todo. */ - public function __construct(LoggerInterface $logger, FormBuilderInterface $form_builder, FormAjaxHandlerInterface $form_ajax_handler) { + public function __construct(LoggerInterface $logger, FormBuilderInterface $form_builder, FormAjaxResponseHandlerInterface $form_ajax_handler) { $this->logger = $logger; $this->formBuilder = $form_builder; $this->formAjaxHandler = $form_ajax_handler; diff --git a/core/tests/Drupal/Tests/Core/EventSubscriber/FormControllerSubscriberTest.php b/core/tests/Drupal/Tests/Core/EventSubscriber/FormControllerSubscriberTest.php index b769670..3e3419c 100644 --- a/core/tests/Drupal/Tests/Core/EventSubscriber/FormControllerSubscriberTest.php +++ b/core/tests/Drupal/Tests/Core/EventSubscriber/FormControllerSubscriberTest.php @@ -7,6 +7,7 @@ namespace Drupal\Tests\Core\EventSubscriber; +use Drupal\Core\Ajax\UpdateBuildIdCommand; use Drupal\Core\EventSubscriber\FormControllerSubscriber; use Drupal\Core\Form\FormAndFormState; use Drupal\Core\Form\FormState; @@ -30,7 +31,7 @@ class FormControllerSubscriberTest extends UnitTestCase { * @dataProvider providerTestOnViewRenderArray */ public function testOnViewRenderArray($attribute_key, $expect_form) { - $form_ajax_handler = $this->getMock('Drupal\Core\Form\FormAjaxHandlerInterface'); + $form_ajax_handler = $this->getMock('Drupal\Core\Form\FormAjaxResponseHandlerInterface'); $controller_resolver = $this->getMock('Drupal\Core\Controller\ControllerResolverInterface'); $subscriber = new FormControllerSubscriber($form_ajax_handler, $controller_resolver); @@ -66,7 +67,7 @@ public function testOnKernelController() { $request = new Request([FormControllerSubscriber::MAGIC_STRING_TO_BE_RENAMED => 'drupal_ajax_post'], ['form_build_id' => 'the_build_id']); $commands = []; - $form_ajax_handler = $this->getMock('Drupal\Core\Form\FormAjaxHandlerInterface'); + $form_ajax_handler = $this->getMock('Drupal\Core\Form\FormAjaxResponseHandlerInterface'); $form_ajax_handler->expects($this->once()) ->method('handle') ->with($request, $form, $form_state, $commands) @@ -94,4 +95,110 @@ public function testOnKernelController() { $this->assertSame('the ajax result', $result); } + /** + * @covers ::onKernelController + */ + public function testOnKernelControllerUsingGet() { + $form = ['#type' => 'form', '#build_id' => 'the_build_id']; + $form_state = new FormState(); + $request = new Request([FormControllerSubscriber::MAGIC_STRING_TO_BE_RENAMED => 'drupal_ajax_post', 'form_build_id' => 'the_build_id']); + $commands = []; + + $form_ajax_handler = $this->getMock('Drupal\Core\Form\FormAjaxResponseHandlerInterface'); + $form_ajax_handler->expects($this->once()) + ->method('handle') + ->with($request, $form, $form_state, $commands) + ->willReturn('the ajax result'); + $controller_resolver = $this->getMock('Drupal\Core\Controller\ControllerResolverInterface'); + $controller_resolver->expects($this->once()) + ->method('getArguments') + ->willReturn([$form, $form_state]); + $subscriber = new FormControllerSubscriber($form_ajax_handler, $controller_resolver); + + $http_kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'); + + $controller = function ($form, $form_state) { + return new FormAndFormState($form, $form_state); + }; + + $event = new FilterControllerEvent($http_kernel, $controller, $request, HttpKernelInterface::MASTER_REQUEST); + + $subscriber->onKernelController($event); + $expected = $controller; + $resulting_controller = $event->getController(); + $this->assertNotSame($expected, $resulting_controller); + + $result = $resulting_controller($request); + $this->assertSame('the ajax result', $result); + } + + /** + * @covers ::onKernelController + */ + public function testOnKernelControllerMismatchedBuildId() { + $form = ['#type' => 'form', '#build_id' => 'the_build_id']; + $form_state = new FormState(); + $request = new Request([FormControllerSubscriber::MAGIC_STRING_TO_BE_RENAMED => 'drupal_ajax_post', 'form_build_id' => 'a_new_build_id']); + $commands = []; + $commands[] = new UpdateBuildIdCommand('a_new_build_id', 'the_build_id'); + + $form_ajax_handler = $this->getMock('Drupal\Core\Form\FormAjaxResponseHandlerInterface'); + $form_ajax_handler->expects($this->once()) + ->method('handle') + ->with($request, $form, $form_state, $commands) + ->willReturn('the ajax result'); + $controller_resolver = $this->getMock('Drupal\Core\Controller\ControllerResolverInterface'); + $controller_resolver->expects($this->once()) + ->method('getArguments') + ->willReturn([$form, $form_state]); + $subscriber = new FormControllerSubscriber($form_ajax_handler, $controller_resolver); + + $http_kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'); + + $controller = function ($form, $form_state) { + return new FormAndFormState($form, $form_state); + }; + + $event = new FilterControllerEvent($http_kernel, $controller, $request, HttpKernelInterface::MASTER_REQUEST); + + $subscriber->onKernelController($event); + $expected = $controller; + $resulting_controller = $event->getController(); + $this->assertNotSame($expected, $resulting_controller); + + $result = $resulting_controller($request); + $this->assertSame('the ajax result', $result); + } + + /** + * @covers ::onKernelController + * + * @expectedException \LogicException + * @expectedExceptionMessage qwer + */ + public function testOnKernelControllerException() { + $request = new Request([FormControllerSubscriber::MAGIC_STRING_TO_BE_RENAMED => 'drupal_ajax_post']); + + $form_ajax_handler = $this->getMock('Drupal\Core\Form\FormAjaxResponseHandlerInterface'); + $form_ajax_handler->expects($this->never()) + ->method('handle'); + $controller_resolver = $this->getMock('Drupal\Core\Controller\ControllerResolverInterface'); + $controller_resolver->expects($this->once()) + ->method('getArguments') + ->willReturn([]); + $subscriber = new FormControllerSubscriber($form_ajax_handler, $controller_resolver); + + $http_kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'); + + $controller = function () { + return []; + }; + + $event = new FilterControllerEvent($http_kernel, $controller, $request, HttpKernelInterface::MASTER_REQUEST); + + $subscriber->onKernelController($event); + $resulting_controller = $event->getController(); + $resulting_controller($request); + } + } diff --git a/core/tests/Drupal/Tests/Core/Form/FormAjaxResponseHandlerTest.php b/core/tests/Drupal/Tests/Core/Form/FormAjaxResponseHandlerTest.php new file mode 100644 index 0000000..dd6cabf --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Form/FormAjaxResponseHandlerTest.php @@ -0,0 +1,189 @@ +renderer = $this->getMock('Drupal\Core\Render\MainContent\MainContentRendererInterface'); + $this->routeMatch = $this->getMock('Drupal\Core\Routing\RouteMatchInterface'); + $this->handler = new FormAjaxResponseHandler($this->renderer, $this->routeMatch); + } + + /** + * @covers ::handle + */ + public function _testHandle() { + $request = new Request(); + $form = []; + $form_state = new FormState(); + $commands = []; + + $expected = []; + $this->assertSame($expected, $this->handler->handle($request, $form, $form_state, $commands)); + } + + /** + * @covers ::handle + * + * @expectedException \Symfony\Component\HttpKernel\Exception\HttpException + */ + public function testHandleNoTriggeringElement() { + $this->renderer->expects($this->never()) + ->method('renderResponse'); + + $request = new Request(); + $form = []; + $form_state = new FormState(); + $commands = []; + + $expected = []; + $this->assertSame($expected, $this->handler->handle($request, $form, $form_state, $commands)); + } + + /** + * @covers ::handle + * + * @expectedException \Symfony\Component\HttpKernel\Exception\HttpException + */ + public function testHandleNoCallable() { + $this->renderer->expects($this->never()) + ->method('renderResponse'); + + $request = new Request(); + $form = []; + $form_state = new FormState(); + $triggering_element = []; + $form_state->setTriggeringElement($triggering_element); + $commands = []; + + $expected = []; + $this->assertSame($expected, $this->handler->handle($request, $form, $form_state, $commands)); + } + + /** + * @covers ::handle + */ + public function testHandleRenderArray() { + $triggering_element = [ + '#ajax' => [ + 'callback' => function (array $form, FormStateInterface $form_state) { + return $form['test']; + } + ], + ]; + $request = new Request(); + $form = [ + 'test' => [ + '#type' => 'textfield', + ], + ]; + $form_state = new FormState(); + $form_state->setTriggeringElement($triggering_element); + $commands = []; + + $this->renderer->expects($this->once()) + ->method('renderResponse') + ->with($form['test'], $request, $this->routeMatch) + ->willReturn(new AjaxResponse([])); + + $result = $this->handler->handle($request, $form, $form_state, $commands); + $this->assertInstanceOf('\Drupal\Core\Ajax\AjaxResponse', $result); + $this->assertSame($commands, $result->getCommands()); + } + + /** + * @covers ::handle + */ + public function testHandleResponse() { + $triggering_element = [ + '#ajax' => [ + 'callback' => function (array $form, FormStateInterface $form_state) { + return new AjaxResponse([]); + } + ], + ]; + $request = new Request(); + $form = []; + $form_state = new FormState(); + $form_state->setTriggeringElement($triggering_element); + $commands = []; + + $this->renderer->expects($this->never()) + ->method('renderResponse'); + + $result = $this->handler->handle($request, $form, $form_state, $commands); + $this->assertInstanceOf('\Drupal\Core\Ajax\AjaxResponse', $result); + $this->assertSame($commands, $result->getCommands()); + } + + /** + * @covers ::handle + */ + public function testHandleWithCommands() { + $triggering_element = [ + '#ajax' => [ + 'callback' => function (array $form, FormStateInterface $form_state) { + return new AjaxResponse([]); + } + ], + ]; + $request = new Request(); + $form = [ + 'test' => [ + '#type' => 'textfield', + ], + ]; + $form_state = new FormState(); + $form_state->setTriggeringElement($triggering_element); + $commands = [ + new AlertCommand('alert!'), + ]; + $commands_expected = []; + $commands_expected[] = ['command' => 'alert', 'text' => 'alert!']; + + $this->renderer->expects($this->never()) + ->method('renderResponse'); + + $result = $this->handler->handle($request, $form, $form_state, $commands); + $this->assertInstanceOf('\Drupal\Core\Ajax\AjaxResponse', $result); + $this->assertSame($commands_expected, $result->getCommands()); + } + +}