Skip to content

Commit b47ece1

Browse files
kbondweaverryan
authored andcommitted
[Live] add batch action controller
1 parent 8b1e879 commit b47ece1

File tree

6 files changed

+349
-16
lines changed

6 files changed

+349
-16
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\LiveComponent\Controller;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpFoundation\Response;
16+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
17+
use Symfony\Component\HttpKernel\HttpKernelInterface;
18+
use Symfony\UX\TwigComponent\MountedComponent;
19+
20+
/**
21+
* @author Kevin Bond <kevinbond@gmail.com>
22+
*
23+
* @internal
24+
*/
25+
final class BatchActionController
26+
{
27+
public function __construct(private HttpKernelInterface $kernel)
28+
{
29+
}
30+
31+
public function __invoke(Request $request, MountedComponent $mounted, string $serviceId, array $actions): ?Response
32+
{
33+
$request->attributes->set('_mounted_component', $mounted);
34+
35+
foreach ($actions as $action) {
36+
$name = $action['name'] ?? throw new BadRequestHttpException('Invalid JSON');
37+
38+
$subRequest = $request->duplicate(attributes: [
39+
'_controller' => [$serviceId, $name],
40+
'_component_action_args' => $action['args'] ?? [],
41+
'_mounted_component' => $mounted,
42+
'_route' => 'live_component',
43+
]);
44+
45+
$response = $this->kernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST, false);
46+
47+
if ($response->isRedirection()) {
48+
return $response;
49+
}
50+
}
51+
52+
return null;
53+
}
54+
}

src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
2121
use Symfony\UX\LiveComponent\ComponentValidator;
2222
use Symfony\UX\LiveComponent\ComponentValidatorInterface;
23+
use Symfony\UX\LiveComponent\Controller\BatchActionController;
2324
use Symfony\UX\LiveComponent\EventListener\AddLiveAttributesSubscriber;
2425
use Symfony\UX\LiveComponent\EventListener\LiveComponentSubscriber;
2526
use Symfony\UX\LiveComponent\Form\Type\LiveCollectionType;
@@ -70,6 +71,13 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
7071
])
7172
;
7273

74+
$container->register('ux.live_component.batch_action_controller', BatchActionController::class)
75+
->setPublic(true)
76+
->setArguments([
77+
new Reference('http_kernel'),
78+
])
79+
;
80+
7381
$container->register('ux.live_component.event_subscriber', LiveComponentSubscriber::class)
7482
->addTag('kernel.event_subscriber')
7583
->addTag('container.service_subscriber', ['key' => ComponentFactory::class, 'id' => 'ux.twig_component.component_factory'])

src/LiveComponent/src/EventListener/LiveComponentSubscriber.php

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use Symfony\Contracts\Service\ServiceSubscriberInterface;
3030
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
3131
use Symfony\UX\LiveComponent\Attribute\LiveArg;
32+
use Symfony\UX\LiveComponent\Controller\BatchActionController;
3233
use Symfony\UX\LiveComponent\LiveComponentHydrator;
3334
use Symfony\UX\TwigComponent\ComponentFactory;
3435
use Symfony\UX\TwigComponent\ComponentMetadata;
@@ -69,6 +70,11 @@ public function onKernelRequest(RequestEvent $event): void
6970
return;
7071
}
7172

73+
if (!$event->isMainRequest()) {
74+
// sub request
75+
return;
76+
}
77+
7278
// the default "action" is get, which does nothing
7379
$action = $request->get('action', 'get');
7480
$componentName = (string) $request->get('component');
@@ -107,6 +113,22 @@ public function onKernelRequest(RequestEvent $event): void
107113
throw new BadRequestHttpException('Invalid CSRF token.');
108114
}
109115

116+
if ('_batch' === $action) {
117+
// use batch controller
118+
$data = $this->parseDataFor($request);
119+
120+
$request->attributes->set('_controller', 'ux.live_component.batch_action_controller');
121+
$request->attributes->set('serviceId', $metadata->getServiceId());
122+
$request->attributes->set('actions', $data['actions']);
123+
$request->attributes->set('mounted', $this->container->get(LiveComponentHydrator::class)->hydrate(
124+
$this->container->get(ComponentFactory::class)->get($componentName),
125+
$data['data'],
126+
$componentName,
127+
));
128+
129+
return;
130+
}
131+
110132
$request->attributes->set('_controller', sprintf('%s::%s', $metadata->getServiceId(), $action));
111133
}
112134

@@ -118,18 +140,13 @@ public function onKernelController(ControllerEvent $event): void
118140
return;
119141
}
120142

121-
$actionArguments = [];
122-
if ($request->query->has('data')) {
123-
// ?data=
124-
$data = json_decode($request->query->get('data'), true, 512, \JSON_THROW_ON_ERROR);
125-
} else {
126-
// OR body of the request is JSON
127-
$requestData = json_decode($request->getContent(), true, 512, \JSON_THROW_ON_ERROR);
128-
$data = $requestData['data'] ?? [];
129-
$actionArguments = $requestData['args'] ?? [];
143+
$controller = $event->getController();
144+
145+
if ($controller instanceof BatchActionController) {
146+
return;
130147
}
131148

132-
if (!\is_array($controller = $event->getController()) || 2 !== \count($controller)) {
149+
if (!\is_array($controller) || 2 !== \count($controller)) {
133150
throw new \RuntimeException('Not a valid live component.');
134151
}
135152

@@ -143,13 +160,21 @@ public function onKernelController(ControllerEvent $event): void
143160
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)));
144161
}
145162

146-
$mounted = $this->container->get(LiveComponentHydrator::class)->hydrate(
147-
$component,
148-
$data,
149-
$request->attributes->get('_component_name')
150-
);
163+
if ($event->isMainRequest()) {
164+
$data = $this->parseDataFor($request);
165+
166+
$request->attributes->set('_component_action_args', $data['args']);
167+
$request->attributes->set('_mounted_component', $this->container->get(LiveComponentHydrator::class)->hydrate(
168+
$component,
169+
$data['data'],
170+
$request->attributes->get('_component_name')
171+
));
172+
} else {
173+
// sub-request
174+
$event->setController([$request->attributes->get('_mounted_component')->getComponent(), $action]);
175+
}
151176

152-
$request->attributes->set('_mounted_component', $mounted);
177+
$actionArguments = $request->attributes->get('_component_action_args', []);
153178

154179
// extra variables to be made available to the controller
155180
// (for "actions" only)
@@ -160,12 +185,45 @@ public function onKernelController(ControllerEvent $event): void
160185
}
161186
}
162187

188+
/**
189+
* @return array{
190+
* data: array,
191+
* args: array,
192+
* actions: array
193+
* }
194+
*/
195+
private function parseDataFor(Request $request): array
196+
{
197+
if ($request->query->has('data')) {
198+
return [
199+
'data' => json_decode($request->query->get('data'), true, 512, \JSON_THROW_ON_ERROR),
200+
'args' => [],
201+
'actions' => [],
202+
];
203+
}
204+
205+
$requestData = json_decode($request->getContent(), true, 512, \JSON_THROW_ON_ERROR);
206+
207+
return [
208+
'data' => $requestData['data'] ?? [],
209+
'args' => $requestData['args'] ?? [],
210+
'actions' => $requestData['actions'] ?? [],
211+
];
212+
}
213+
163214
public function onKernelView(ViewEvent $event): void
164215
{
165216
if (!$this->isLiveComponentRequest($request = $event->getRequest())) {
166217
return;
167218
}
168219

220+
if (!$event->isMainRequest()) {
221+
// sub-request, so skip rendering
222+
$event->setResponse(new Response());
223+
224+
return;
225+
}
226+
169227
$event->setResponse($this->createResponse($request->attributes->get('_mounted_component')));
170228
}
171229

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\LiveComponent\Tests\Fixtures\Component;
13+
14+
use Symfony\Component\HttpFoundation\RedirectResponse;
15+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
16+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
17+
use Symfony\UX\LiveComponent\Attribute\LiveAction;
18+
use Symfony\UX\LiveComponent\Attribute\LiveArg;
19+
use Symfony\UX\LiveComponent\Attribute\LiveProp;
20+
use Symfony\UX\LiveComponent\DefaultActionTrait;
21+
22+
#[AsLiveComponent('with_actions')]
23+
final class WithActions
24+
{
25+
use DefaultActionTrait;
26+
27+
#[LiveProp]
28+
public array $items = ['initial'];
29+
30+
#[LiveAction]
31+
public function add(#[LiveArg] string $what, UrlGeneratorInterface $router): void
32+
{
33+
$this->items[] = $what;
34+
}
35+
36+
#[LiveAction]
37+
public function redirect(UrlGeneratorInterface $router): RedirectResponse
38+
{
39+
return new RedirectResponse($router->generate('homepage'));
40+
}
41+
42+
#[LiveAction]
43+
public function exception(): void
44+
{
45+
throw new \RuntimeException('Exception message');
46+
}
47+
48+
public function nonLive(): void
49+
{
50+
}
51+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<ul{{ attributes }}>
2+
{% for item in items %}
3+
<li>{{ item }}</li>
4+
{% endfor %}
5+
</ul>

0 commit comments

Comments
 (0)