Skip to content

Commit abd8a44

Browse files
committed
bug #1694 [LiveComponent] set LiveArg value to null if empty string (jannes-io)
This PR was squashed before being merged into the 2.x branch. Discussion ---------- [LiveComponent] set LiveArg value to null if empty string Checks the type to see if the argument is nullable and is not a string, if so, and an empty string is provided, the value will be overwritten to null. This should be backwards compatible since before users were manually casting to string "null" in this case. | Q | A | ------------- | --- | Bug fix? | kinda? | New feature? | kinda? | Issues | Fix #1691 | License | MIT Includes type information in the `LiveArg` attribute, this type information is then used in the controller subscriber of `LiveAction` to convert empty strings (`"`") to `null`, only when `null` is accepted **and** the types does not allow `string`. Commits ------- 538cc61 [LiveComponent] set LiveArg value to null if empty string
2 parents eaa1276 + 538cc61 commit abd8a44

File tree

10 files changed

+286
-110
lines changed

10 files changed

+286
-110
lines changed

src/LiveComponent/src/Attribute/LiveArg.php

Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,44 +11,49 @@
1111

1212
namespace Symfony\UX\LiveComponent\Attribute;
1313

14-
/**
15-
* An attribute to configure a LiveArg (custom argument passed to a LiveAction).
16-
*
17-
* @see https://symfony.com/bundles/ux-live-component/current/index.html#actions-arguments
18-
*
19-
* @author Tomas Norkūnas <[email protected]>
20-
*/
21-
#[\Attribute(\Attribute::TARGET_PARAMETER)]
22-
final class LiveArg
23-
{
24-
public function __construct(
25-
/**
26-
* @param string|null $name The name of the argument received by the LiveAction
27-
*/
28-
public ?string $name = null,
29-
) {
30-
}
14+
use Symfony\Component\HttpKernel\Attribute\ValueResolver;
15+
use Symfony\UX\LiveComponent\ValueResolver\LiveArgValueResolver;
3116

17+
if (class_exists(ValueResolver::class)) {
3218
/**
33-
* @internal
19+
* An attribute to configure a LiveArg (custom argument passed to a LiveAction).
3420
*
35-
* @return array<string, string>
21+
* @see https://symfony.com/bundles/ux-live-component/current/index.html#actions-arguments
22+
*
23+
* @author Tomas Norkūnas <[email protected]>
24+
* @author Jannes Drijkoningen <[email protected]>
3625
*/
37-
public static function liveArgs(object $component, string $action): array
26+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
27+
final class LiveArg extends ValueResolver
3828
{
39-
$method = new \ReflectionMethod($component, $action);
40-
$liveArgs = [];
41-
42-
foreach ($method->getParameters() as $parameter) {
43-
foreach ($parameter->getAttributes(self::class) as $liveArg) {
44-
/** @var LiveArg $attr */
45-
$attr = $liveArg->newInstance();
46-
$parameterName = $parameter->getName();
47-
48-
$liveArgs[$parameterName] = $attr->name ?? $parameterName;
49-
}
29+
public function __construct(
30+
/**
31+
* @param string|null $name The name of the argument received by the LiveAction
32+
*/
33+
public ?string $name = null,
34+
bool $disabled = false,
35+
string $resolver = LiveArgValueResolver::class,
36+
) {
37+
parent::__construct($resolver, $disabled);
38+
}
39+
}
40+
} else {
41+
/**
42+
* An attribute to configure a LiveArg (custom argument passed to a LiveAction).
43+
*
44+
* @see https://symfony.com/bundles/ux-live-component/current/index.html#actions-arguments
45+
*
46+
* @author Tomas Norkūnas <[email protected]>
47+
*/
48+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
49+
final class LiveArg
50+
{
51+
public function __construct(
52+
/**
53+
* @param string|null $name The name of the argument received by the LiveAction
54+
*/
55+
public ?string $name = null,
56+
) {
5057
}
51-
52-
return $liveArgs;
5358
}
5459
}

src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
use Symfony\UX\LiveComponent\Util\LiveControllerAttributesCreator;
4848
use Symfony\UX\LiveComponent\Util\QueryStringPropsExtractor;
4949
use Symfony\UX\LiveComponent\Util\TwigAttributeHelperFactory;
50+
use Symfony\UX\LiveComponent\ValueResolver\LiveArgValueResolver;
5051
use Symfony\UX\TwigComponent\ComponentFactory;
5152
use Symfony\UX\TwigComponent\ComponentRenderer;
5253

@@ -261,6 +262,9 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
261262
'%kernel.secret%',
262263
])
263264
->addTag('kernel.cache_warmer');
265+
266+
$container->register(LiveArgValueResolver::class, LiveArgValueResolver::class)
267+
->addTag('controller.argument_value_resolver', ['priority' => 0]);
264268
}
265269

266270
private function isAssetMapperAvailable(ContainerBuilder $container): bool

src/LiveComponent/src/EventListener/LiveComponentSubscriber.php

Lines changed: 7 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313

1414
use Psr\Container\ContainerInterface;
1515
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
16-
use Symfony\Component\HttpFoundation\Exception\JsonException;
1716
use Symfony\Component\HttpFoundation\Request;
1817
use Symfony\Component\HttpFoundation\Response;
1918
use Symfony\Component\HttpKernel\Event\ControllerEvent;
@@ -29,10 +28,10 @@
2928
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
3029
use Symfony\Contracts\Service\ServiceSubscriberInterface;
3130
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
32-
use Symfony\UX\LiveComponent\Attribute\LiveArg;
3331
use Symfony\UX\LiveComponent\LiveComponentHydrator;
3432
use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadataFactory;
3533
use Symfony\UX\LiveComponent\Util\LiveControllerAttributesCreator;
34+
use Symfony\UX\LiveComponent\Util\LiveRequestDataParser;
3635
use Symfony\UX\TwigComponent\ComponentFactory;
3736
use Symfony\UX\TwigComponent\ComponentMetadata;
3837
use Symfony\UX\TwigComponent\ComponentRenderer;
@@ -116,7 +115,7 @@ public function onKernelRequest(RequestEvent $event): void
116115

117116
if ('_batch' === $action) {
118117
// use batch controller
119-
$data = $this->parseDataFor($request);
118+
$data = LiveRequestDataParser::parseDataFor($request);
120119

121120
$request->attributes->set('_controller', 'ux.live_component.batch_action_controller');
122121
$request->attributes->set('serviceId', $metadata->getServiceId());
@@ -195,61 +194,6 @@ public function onKernelController(ControllerEvent $event): void
195194
$action,
196195
]);
197196
}
198-
199-
// read the action arguments from the request, unless they're already set (batch sub-requests)
200-
$actionArguments = $request->attributes->get('_component_action_args', $this->parseDataFor($request)['args']);
201-
// extra variables to be made available to the controller
202-
// (for "actions" only)
203-
foreach (LiveArg::liveArgs($component, $action) as $parameter => $arg) {
204-
if (isset($actionArguments[$arg])) {
205-
$request->attributes->set($parameter, $actionArguments[$arg]);
206-
}
207-
}
208-
}
209-
210-
/**
211-
* @return array{
212-
* data: array,
213-
* args: array,
214-
* actions: array
215-
* // has "fingerprint" and "tag" string key, keyed by component id
216-
* children: array
217-
* propsFromParent: array
218-
* }
219-
*/
220-
private static function parseDataFor(Request $request): array
221-
{
222-
if (!$request->attributes->has('_live_request_data')) {
223-
if ($request->query->has('props')) {
224-
$liveRequestData = [
225-
'props' => self::parseJsonFromQuery($request, 'props'),
226-
'updated' => self::parseJsonFromQuery($request, 'updated'),
227-
'args' => [],
228-
'actions' => [],
229-
'children' => self::parseJsonFromQuery($request, 'children'),
230-
'propsFromParent' => self::parseJsonFromQuery($request, 'propsFromParent'),
231-
];
232-
} else {
233-
try {
234-
$requestData = json_decode($request->request->get('data'), true, 512, \JSON_BIGINT_AS_STRING | \JSON_THROW_ON_ERROR);
235-
} catch (\JsonException $e) {
236-
throw new JsonException('Could not decode request body.', $e->getCode(), $e);
237-
}
238-
239-
$liveRequestData = [
240-
'props' => $requestData['props'] ?? [],
241-
'updated' => $requestData['updated'] ?? [],
242-
'args' => $requestData['args'] ?? [],
243-
'actions' => $requestData['actions'] ?? [],
244-
'children' => $requestData['children'] ?? [],
245-
'propsFromParent' => $requestData['propsFromParent'] ?? [],
246-
];
247-
}
248-
249-
$request->attributes->set('_live_request_data', $liveRequestData);
250-
}
251-
252-
return $request->attributes->get('_live_request_data');
253197
}
254198

255199
public function onKernelView(ViewEvent $event): void
@@ -354,34 +298,22 @@ private function hydrateComponent(object $component, string $componentName, Requ
354298
$metadataFactory = $this->container->get(LiveComponentMetadataFactory::class);
355299
\assert($metadataFactory instanceof LiveComponentMetadataFactory);
356300

301+
$liveRequestData = LiveRequestDataParser::parseDataFor($request);
357302
$componentAttributes = $hydrator->hydrate(
358303
$component,
359-
$this->parseDataFor($request)['props'],
360-
$this->parseDataFor($request)['updated'],
304+
$liveRequestData['props'],
305+
$liveRequestData['updated'],
361306
$metadataFactory->getMetadata($componentName),
362-
$this->parseDataFor($request)['propsFromParent']
307+
$liveRequestData['propsFromParent']
363308
);
364309

365310
$mountedComponent = new MountedComponent($componentName, $component, $componentAttributes);
366311

367312
$mountedComponent->addExtraMetadata(
368313
InterceptChildComponentRenderSubscriber::CHILDREN_FINGERPRINTS_METADATA_KEY,
369-
$this->parseDataFor($request)['children']
314+
$liveRequestData['children']
370315
);
371316

372317
return $mountedComponent;
373318
}
374-
375-
private static function parseJsonFromQuery(Request $request, string $key): array
376-
{
377-
if (!$request->query->has($key)) {
378-
return [];
379-
}
380-
381-
try {
382-
return json_decode($request->query->get($key), true, 512, \JSON_THROW_ON_ERROR);
383-
} catch (\JsonException $exception) {
384-
throw new JsonException(sprintf('Invalid JSON on query string %s.', $key), 0, $exception);
385-
}
386-
}
387319
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Util;
13+
14+
use Symfony\Component\HttpFoundation\Exception\JsonException;
15+
use Symfony\Component\HttpFoundation\Request;
16+
17+
/**
18+
* @author Kevin Bond <[email protected]>
19+
* @author Ryan Weaver <[email protected]>
20+
*
21+
* @internal
22+
*/
23+
final class LiveRequestDataParser
24+
{
25+
/**
26+
* @return array{
27+
* data: array,
28+
* args: array,
29+
* actions: array
30+
* // has "fingerprint" and "tag" string key, keyed by component id
31+
* children: array
32+
* propsFromParent: array
33+
* }
34+
*/
35+
public static function parseDataFor(Request $request): array
36+
{
37+
if (!$request->attributes->has('_live_request_data')) {
38+
if ($request->query->has('props')) {
39+
$liveRequestData = [
40+
'props' => self::parseJsonFromQuery($request, 'props'),
41+
'updated' => self::parseJsonFromQuery($request, 'updated'),
42+
'args' => [],
43+
'actions' => [],
44+
'children' => self::parseJsonFromQuery($request, 'children'),
45+
'propsFromParent' => self::parseJsonFromQuery($request, 'propsFromParent'),
46+
];
47+
} else {
48+
try {
49+
$requestData = json_decode($request->request->get('data'), true, 512, \JSON_BIGINT_AS_STRING | \JSON_THROW_ON_ERROR);
50+
} catch (\JsonException $e) {
51+
throw new JsonException('Could not decode request body.', $e->getCode(), $e);
52+
}
53+
54+
$liveRequestData = [
55+
'props' => $requestData['props'] ?? [],
56+
'updated' => $requestData['updated'] ?? [],
57+
'args' => $requestData['args'] ?? [],
58+
'actions' => $requestData['actions'] ?? [],
59+
'children' => $requestData['children'] ?? [],
60+
'propsFromParent' => $requestData['propsFromParent'] ?? [],
61+
];
62+
}
63+
64+
$request->attributes->set('_live_request_data', $liveRequestData);
65+
}
66+
67+
return $request->attributes->get('_live_request_data');
68+
}
69+
70+
private static function parseJsonFromQuery(Request $request, string $key): array
71+
{
72+
if (!$request->query->has($key)) {
73+
return [];
74+
}
75+
76+
try {
77+
return json_decode($request->query->get($key), true, 512, \JSON_THROW_ON_ERROR);
78+
} catch (\JsonException $exception) {
79+
throw new JsonException(sprintf('Invalid JSON on query string %s.', $key), 0, $exception);
80+
}
81+
}
82+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\ValueResolver;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
16+
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
17+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
18+
19+
if (interface_exists(ValueResolverInterface::class)) {
20+
/**
21+
* @author Jannes Drijkoningen <[email protected]>
22+
*
23+
* @internal
24+
*/
25+
class LiveArgValueResolver implements ValueResolverInterface
26+
{
27+
use LiveArgValueResolverTrait {
28+
resolve as resolveArgument;
29+
}
30+
31+
public function resolve(Request $request, ArgumentMetadata $argument): iterable
32+
{
33+
if (!$this->supports($argument)) {
34+
return [];
35+
}
36+
37+
return $this->resolveArgument($request, $argument);
38+
}
39+
}
40+
} else {
41+
/**
42+
* @author Jannes Drijkoningen <[email protected]>
43+
*
44+
* @internal
45+
*
46+
* @deprecated should be removed when Symfony 6.1 is no longer supported
47+
*/
48+
class LiveArgValueResolver implements ArgumentValueResolverInterface
49+
{
50+
use LiveArgValueResolverTrait {
51+
supports as supportsArgument;
52+
}
53+
54+
public function supports(Request $request, ArgumentMetadata $argument): bool
55+
{
56+
return $this->supportsArgument($argument);
57+
}
58+
}
59+
}

0 commit comments

Comments
 (0)