-
Notifications
You must be signed in to change notification settings - Fork 0
[Live] add batch action controller (2) #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5738d77
cf0a8ea
896f48f
d4ccc90
094deb7
7b6f3a2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\UX\LiveComponent\Controller; | ||
|
||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\HttpFoundation\Response; | ||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; | ||
use Symfony\Component\HttpKernel\HttpKernelInterface; | ||
use Symfony\UX\TwigComponent\MountedComponent; | ||
|
||
/** | ||
* @author Kevin Bond <[email protected]> | ||
* | ||
* @internal | ||
*/ | ||
final class BatchActionController | ||
{ | ||
public function __construct(private HttpKernelInterface $kernel) | ||
{ | ||
} | ||
|
||
public function __invoke(Request $request, MountedComponent $_mounted_component, string $serviceId, array $actions): ?Response | ||
{ | ||
foreach ($actions as $action) { | ||
$name = $action['name'] ?? throw new BadRequestHttpException('Invalid JSON'); | ||
|
||
$subRequest = $request->duplicate(attributes: [ | ||
'_controller' => [$serviceId, $name], | ||
'_component_action_args' => $action['args'] ?? [], | ||
'_mounted_component' => $_mounted_component, | ||
'_route' => 'live_component', | ||
]); | ||
|
||
$response = $this->kernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST, false); | ||
|
||
if ($response->isRedirection()) { | ||
return $response; | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -69,6 +69,10 @@ public function onKernelRequest(RequestEvent $event): void | |
return; | ||
} | ||
|
||
if ($request->attributes->has('_controller')) { | ||
return; | ||
} | ||
|
||
// the default "action" is get, which does nothing | ||
$action = $request->get('action', 'get'); | ||
$componentName = (string) $request->get('component'); | ||
|
@@ -107,6 +111,23 @@ public function onKernelRequest(RequestEvent $event): void | |
throw new BadRequestHttpException('Invalid CSRF token.'); | ||
} | ||
|
||
if ('_batch' === $action) { | ||
// use batch controller | ||
$data = $this->parseDataFor($request); | ||
|
||
$request->attributes->set('_controller', 'ux.live_component.batch_action_controller'); | ||
$request->attributes->set('serviceId', $metadata->getServiceId()); | ||
$request->attributes->set('actions', $data['actions']); | ||
$request->attributes->set('_mounted_component', $this->container->get(LiveComponentHydrator::class)->hydrate( | ||
$this->container->get(ComponentFactory::class)->get($componentName), | ||
$data['data'], | ||
$componentName, | ||
)); | ||
$request->attributes->set('_is_live_batch_action', true); | ||
|
||
return; | ||
} | ||
|
||
$request->attributes->set('_controller', sprintf('%s::%s', $metadata->getServiceId(), $action)); | ||
} | ||
|
||
|
@@ -118,18 +139,13 @@ public function onKernelController(ControllerEvent $event): void | |
return; | ||
} | ||
|
||
$actionArguments = []; | ||
if ($request->query->has('data')) { | ||
// ?data= | ||
$data = json_decode($request->query->get('data'), true, 512, \JSON_THROW_ON_ERROR); | ||
} else { | ||
// OR body of the request is JSON | ||
$requestData = json_decode($request->getContent(), true, 512, \JSON_THROW_ON_ERROR); | ||
$data = $requestData['data'] ?? []; | ||
$actionArguments = $requestData['args'] ?? []; | ||
if ($request->attributes->get('_is_live_batch_action')) { | ||
return; | ||
} | ||
|
||
if (!\is_array($controller = $event->getController()) || 2 !== \count($controller)) { | ||
$controller = $event->getController(); | ||
|
||
if (!\is_array($controller) || 2 !== \count($controller)) { | ||
throw new \RuntimeException('Not a valid live component.'); | ||
} | ||
|
||
|
@@ -143,14 +159,29 @@ public function onKernelController(ControllerEvent $event): void | |
throw new NotFoundHttpException(sprintf('The action "%s" either doesn\'t exist or is not allowed in "%s". Make sure it exist and has the LiveAction attribute above it.', $action, \get_class($component))); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could move this up into Btw: motivation for this is potential simplification... but there are a lot of moving pieces, so I'm not sure how it would all "land". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I can't move this up because it requires a component instance. For non-batch requests, this isn't available until |
||
|
||
$mounted = $this->container->get(LiveComponentHydrator::class)->hydrate( | ||
$component, | ||
$data, | ||
$request->attributes->get('_component_name') | ||
); | ||
|
||
$request->attributes->set('_mounted_component', $mounted); | ||
/* | ||
* Either we: | ||
* A) To not have a _mounted_component, so hydrate $component | ||
* B) We DO have a _mounted_component, so no need to hydrate, | ||
* but we DO need to make sure it's set as the controller. | ||
*/ | ||
if (!$request->attributes->has('_mounted_component')) { | ||
$request->attributes->set('_mounted_component', $this->container->get(LiveComponentHydrator::class)->hydrate( | ||
$component, | ||
$this->parseDataFor($request)['data'], | ||
$request->attributes->get('_component_name') | ||
)); | ||
} else { | ||
// override the component with our already-mounted version | ||
$component = $request->attributes->get('_mounted_component')->getComponent(); | ||
$event->setController([ | ||
$component, | ||
$action, | ||
]); | ||
} | ||
|
||
// read the action arguments from the request, unless they're already set (batch sub-requests) | ||
$actionArguments = $request->attributes->get('_component_action_args', $this->parseDataFor($request)['args']); | ||
// extra variables to be made available to the controller | ||
// (for "actions" only) | ||
foreach (LiveArg::liveArgs($component, $action) as $parameter => $arg) { | ||
|
@@ -160,12 +191,49 @@ public function onKernelController(ControllerEvent $event): void | |
} | ||
} | ||
|
||
/** | ||
* @return array{ | ||
* data: array, | ||
* args: array, | ||
* actions: array | ||
* } | ||
*/ | ||
private function parseDataFor(Request $request): array | ||
{ | ||
if (!$request->attributes->has('_live_request_data')) { | ||
if ($request->query->has('data')) { | ||
return [ | ||
'data' => json_decode($request->query->get('data'), true, 512, \JSON_THROW_ON_ERROR), | ||
'args' => [], | ||
'actions' => [], | ||
]; | ||
} | ||
|
||
$requestData = $request->toArray(); | ||
|
||
$request->attributes->set('_live_request_data', [ | ||
'data' => $requestData['data'] ?? [], | ||
'args' => $requestData['args'] ?? [], | ||
'actions' => $requestData['actions'] ?? [], | ||
]); | ||
} | ||
|
||
return $request->attributes->get('_live_request_data'); | ||
} | ||
|
||
public function onKernelView(ViewEvent $event): void | ||
{ | ||
if (!$this->isLiveComponentRequest($request = $event->getRequest())) { | ||
return; | ||
} | ||
|
||
if (!$event->isMainRequest()) { | ||
// sub-request, so skip rendering | ||
$event->setResponse(new Response()); | ||
|
||
return; | ||
} | ||
|
||
$event->setResponse($this->createResponse($request->attributes->get('_mounted_component'))); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\UX\LiveComponent\Tests\Fixtures\Component; | ||
|
||
use Symfony\Component\HttpFoundation\RedirectResponse; | ||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; | ||
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; | ||
use Symfony\UX\LiveComponent\Attribute\LiveAction; | ||
use Symfony\UX\LiveComponent\Attribute\LiveArg; | ||
use Symfony\UX\LiveComponent\Attribute\LiveProp; | ||
use Symfony\UX\LiveComponent\DefaultActionTrait; | ||
|
||
#[AsLiveComponent('with_actions')] | ||
final class WithActions | ||
{ | ||
use DefaultActionTrait; | ||
|
||
#[LiveProp] | ||
public array $items = ['initial']; | ||
|
||
#[LiveAction] | ||
public function add(#[LiveArg] string $what, UrlGeneratorInterface $router): void | ||
{ | ||
$this->items[] = $what; | ||
} | ||
|
||
#[LiveAction] | ||
public function redirect(UrlGeneratorInterface $router): RedirectResponse | ||
{ | ||
return new RedirectResponse($router->generate('homepage')); | ||
} | ||
|
||
#[LiveAction] | ||
public function exception(): void | ||
{ | ||
throw new \RuntimeException('Exception message'); | ||
} | ||
|
||
public function nonLive(): void | ||
{ | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
<ul{{ attributes }}> | ||
{% for item in items %} | ||
<li>{{ item }}</li> | ||
{% endfor %} | ||
</ul> |
Uh oh!
There was an error while loading. Please reload this page.