Skip to content

Commit cdaa112

Browse files
authored
Merge pull request #5 from kbond/batch-action-controller-2
[Live] add batch action controller (2)
2 parents 0631817 + 7b6f3a2 commit cdaa112

File tree

6 files changed

+358
-17
lines changed

6 files changed

+358
-17
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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_component, string $serviceId, array $actions): ?Response
32+
{
33+
foreach ($actions as $action) {
34+
$name = $action['name'] ?? throw new BadRequestHttpException('Invalid JSON');
35+
36+
$subRequest = $request->duplicate(attributes: [
37+
'_controller' => [$serviceId, $name],
38+
'_component_action_args' => $action['args'] ?? [],
39+
'_mounted_component' => $_mounted_component,
40+
'_route' => 'live_component',
41+
]);
42+
43+
$response = $this->kernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST, false);
44+
45+
if ($response->isRedirection()) {
46+
return $response;
47+
}
48+
}
49+
50+
return null;
51+
}
52+
}

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: 85 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ public function onKernelRequest(RequestEvent $event): void
6969
return;
7070
}
7171

72+
if ($request->attributes->has('_controller')) {
73+
return;
74+
}
75+
7276
// the default "action" is get, which does nothing
7377
$action = $request->get('action', 'get');
7478
$componentName = (string) $request->get('component');
@@ -107,6 +111,23 @@ public function onKernelRequest(RequestEvent $event): void
107111
throw new BadRequestHttpException('Invalid CSRF token.');
108112
}
109113

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

@@ -118,18 +139,13 @@ public function onKernelController(ControllerEvent $event): void
118139
return;
119140
}
120141

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'] ?? [];
142+
if ($request->attributes->get('_is_live_batch_action')) {
143+
return;
130144
}
131145

132-
if (!\is_array($controller = $event->getController()) || 2 !== \count($controller)) {
146+
$controller = $event->getController();
147+
148+
if (!\is_array($controller) || 2 !== \count($controller)) {
133149
throw new \RuntimeException('Not a valid live component.');
134150
}
135151

@@ -143,14 +159,29 @@ public function onKernelController(ControllerEvent $event): void
143159
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)));
144160
}
145161

146-
$mounted = $this->container->get(LiveComponentHydrator::class)->hydrate(
147-
$component,
148-
$data,
149-
$request->attributes->get('_component_name')
150-
);
151-
152-
$request->attributes->set('_mounted_component', $mounted);
162+
/*
163+
* Either we:
164+
* A) To not have a _mounted_component, so hydrate $component
165+
* B) We DO have a _mounted_component, so no need to hydrate,
166+
* but we DO need to make sure it's set as the controller.
167+
*/
168+
if (!$request->attributes->has('_mounted_component')) {
169+
$request->attributes->set('_mounted_component', $this->container->get(LiveComponentHydrator::class)->hydrate(
170+
$component,
171+
$this->parseDataFor($request)['data'],
172+
$request->attributes->get('_component_name')
173+
));
174+
} else {
175+
// override the component with our already-mounted version
176+
$component = $request->attributes->get('_mounted_component')->getComponent();
177+
$event->setController([
178+
$component,
179+
$action,
180+
]);
181+
}
153182

183+
// read the action arguments from the request, unless they're already set (batch sub-requests)
184+
$actionArguments = $request->attributes->get('_component_action_args', $this->parseDataFor($request)['args']);
154185
// extra variables to be made available to the controller
155186
// (for "actions" only)
156187
foreach (LiveArg::liveArgs($component, $action) as $parameter => $arg) {
@@ -160,12 +191,49 @@ public function onKernelController(ControllerEvent $event): void
160191
}
161192
}
162193

194+
/**
195+
* @return array{
196+
* data: array,
197+
* args: array,
198+
* actions: array
199+
* }
200+
*/
201+
private function parseDataFor(Request $request): array
202+
{
203+
if (!$request->attributes->has('_live_request_data')) {
204+
if ($request->query->has('data')) {
205+
return [
206+
'data' => json_decode($request->query->get('data'), true, 512, \JSON_THROW_ON_ERROR),
207+
'args' => [],
208+
'actions' => [],
209+
];
210+
}
211+
212+
$requestData = $request->toArray();
213+
214+
$request->attributes->set('_live_request_data', [
215+
'data' => $requestData['data'] ?? [],
216+
'args' => $requestData['args'] ?? [],
217+
'actions' => $requestData['actions'] ?? [],
218+
]);
219+
}
220+
221+
return $request->attributes->get('_live_request_data');
222+
}
223+
163224
public function onKernelView(ViewEvent $event): void
164225
{
165226
if (!$this->isLiveComponentRequest($request = $event->getRequest())) {
166227
return;
167228
}
168229

230+
if (!$event->isMainRequest()) {
231+
// sub-request, so skip rendering
232+
$event->setResponse(new Response());
233+
234+
return;
235+
}
236+
169237
$event->setResponse($this->createResponse($request->attributes->get('_mounted_component')));
170238
}
171239

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)