Skip to content

Commit

Permalink
[LiveComponent] use ValueResolver instead of onRequest to parse LiveArgs
Browse files Browse the repository at this point in the history
  • Loading branch information
jannes-io committed Apr 15, 2024
1 parent 304044d commit eba19c0
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 120 deletions.
58 changes: 7 additions & 51 deletions src/LiveComponent/src/Attribute/LiveArg.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@

namespace Symfony\UX\LiveComponent\Attribute;

use Symfony\Component\HttpKernel\Attribute\ValueResolver;
use Symfony\UX\LiveComponent\ValueResolver\LiveArgValueResolver;

/**
* An attribute to configure a LiveArg (custom argument passed to a LiveAction).
*
Expand All @@ -19,63 +22,16 @@
* @author Tomas Norkūnas <[email protected]>
*/
#[\Attribute(\Attribute::TARGET_PARAMETER)]
final class LiveArg
final class LiveArg extends ValueResolver
{
public function __construct(
/**
* @param string|null $name The name of the argument received by the LiveAction
*/
public ?string $name = null,
bool $disabled = false,
string $resolver = LiveArgValueResolver::class,
) {
}

/**
* @internal
*
* @return array<string, mixed>
*/
public static function liveArgs(object $component, string $action, array $arguments = []): array
{
$method = new \ReflectionMethod($component, $action);
$liveArgs = [];

foreach ($method->getParameters() as $parameter) {
foreach ($parameter->getAttributes(self::class) as $liveArg) {
/** @var LiveArg $attr */
$attr = $liveArg->newInstance();
$parameterName = $attr->name ?? $parameter->getName();
$type = $parameter->getType();

if (!isset($arguments[$parameterName])) {
continue;
}

$value = $arguments[$parameterName];
if (null !== $type && !self::allowsString($type) && '' === $value) {
$value = null;
}

$liveArgs[$parameter->getName()] = $value;
}
}

return $liveArgs;
}

private static function allowsString(\ReflectionType $type): bool
{
if ($type instanceof \ReflectionNamedType) {
return 'string' === $type->getName();
}

if ($type instanceof \ReflectionUnionType) {
foreach ($type->getTypes() as $subType) {
if ('string' === $subType->getName()) {
return true;
}
}
}

return false;
parent::__construct($resolver, $disabled);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
use Symfony\UX\LiveComponent\Util\LiveControllerAttributesCreator;
use Symfony\UX\LiveComponent\Util\QueryStringPropsExtractor;
use Symfony\UX\LiveComponent\Util\TwigAttributeHelperFactory;
use Symfony\UX\LiveComponent\ValueResolver\LiveArgValueResolver;
use Symfony\UX\TwigComponent\ComponentFactory;
use Symfony\UX\TwigComponent\ComponentRenderer;

Expand Down Expand Up @@ -261,6 +262,9 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
'%kernel.secret%',
])
->addTag('kernel.cache_warmer');

$container->register(LiveArgValueResolver::class, LiveArgValueResolver::class)
->addTag('controller.argument_value_resolver', ['priority' => 0]);
}

private function isAssetMapperAvailable(ContainerBuilder $container): bool
Expand Down
76 changes: 7 additions & 69 deletions src/LiveComponent/src/EventListener/LiveComponentSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

use Psr\Container\ContainerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Exception\JsonException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
Expand All @@ -29,10 +28,10 @@
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveArg;
use Symfony\UX\LiveComponent\LiveComponentHydrator;
use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadataFactory;
use Symfony\UX\LiveComponent\Util\LiveControllerAttributesCreator;
use Symfony\UX\LiveComponent\Util\LiveRequestDataParser;
use Symfony\UX\TwigComponent\ComponentFactory;
use Symfony\UX\TwigComponent\ComponentMetadata;
use Symfony\UX\TwigComponent\ComponentRenderer;
Expand Down Expand Up @@ -116,7 +115,7 @@ public function onKernelRequest(RequestEvent $event): void

if ('_batch' === $action) {
// use batch controller
$data = $this->parseDataFor($request);
$data = LiveRequestDataParser::parseDataFor($request);

$request->attributes->set('_controller', 'ux.live_component.batch_action_controller');
$request->attributes->set('serviceId', $metadata->getServiceId());
Expand Down Expand Up @@ -195,55 +194,6 @@ public function onKernelController(ControllerEvent $event): void
$action,
]);
}

// extra variables to be made available to the controller (for "LiveAction" only)
$actionArguments = $request->attributes->get('_component_action_args', $this->parseDataFor($request)['args']);
$request->attributes->add(LiveArg::liveArgs($component, $action, $actionArguments));
}

/**
* @return array{
* data: array,
* args: array,
* actions: array
* // has "fingerprint" and "tag" string key, keyed by component id
* children: array
* propsFromParent: array
* }
*/
private static function parseDataFor(Request $request): array
{
if (!$request->attributes->has('_live_request_data')) {
if ($request->query->has('props')) {
$liveRequestData = [
'props' => self::parseJsonFromQuery($request, 'props'),
'updated' => self::parseJsonFromQuery($request, 'updated'),
'args' => [],
'actions' => [],
'children' => self::parseJsonFromQuery($request, 'children'),
'propsFromParent' => self::parseJsonFromQuery($request, 'propsFromParent'),
];
} else {
try {
$requestData = json_decode($request->request->get('data'), true, 512, \JSON_BIGINT_AS_STRING | \JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
throw new JsonException('Could not decode request body.', $e->getCode(), $e);
}

$liveRequestData = [
'props' => $requestData['props'] ?? [],
'updated' => $requestData['updated'] ?? [],
'args' => $requestData['args'] ?? [],
'actions' => $requestData['actions'] ?? [],
'children' => $requestData['children'] ?? [],
'propsFromParent' => $requestData['propsFromParent'] ?? [],
];
}

$request->attributes->set('_live_request_data', $liveRequestData);
}

return $request->attributes->get('_live_request_data');
}

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

$liveRequestData = LiveRequestDataParser::parseDataFor($request);
$componentAttributes = $hydrator->hydrate(
$component,
$this->parseDataFor($request)['props'],
$this->parseDataFor($request)['updated'],
$liveRequestData['props'],
$liveRequestData['updated'],
$metadataFactory->getMetadata($componentName),
$this->parseDataFor($request)['propsFromParent']
$liveRequestData['propsFromParent']
);

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

$mountedComponent->addExtraMetadata(
InterceptChildComponentRenderSubscriber::CHILDREN_FINGERPRINTS_METADATA_KEY,
$this->parseDataFor($request)['children']
$liveRequestData['children']
);

return $mountedComponent;
}

private static function parseJsonFromQuery(Request $request, string $key): array
{
if (!$request->query->has($key)) {
return [];
}

try {
return json_decode($request->query->get($key), true, 512, \JSON_THROW_ON_ERROR);
} catch (\JsonException $exception) {
throw new JsonException(sprintf('Invalid JSON on query string %s.', $key), 0, $exception);
}
}
}
82 changes: 82 additions & 0 deletions src/LiveComponent/src/Util/LiveRequestDataParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?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\Util;

use Symfony\Component\HttpFoundation\Exception\JsonException;
use Symfony\Component\HttpFoundation\Request;

/**
* @author Kevin Bond <[email protected]>
* @author Ryan Weaver <[email protected]>
*
* @internal
*/
final class LiveRequestDataParser
{
/**
* @return array{
* data: array,
* args: array,
* actions: array
* // has "fingerprint" and "tag" string key, keyed by component id
* children: array
* propsFromParent: array
* }
*/
public static function parseDataFor(Request $request): array
{
if (!$request->attributes->has('_live_request_data')) {
if ($request->query->has('props')) {
$liveRequestData = [
'props' => self::parseJsonFromQuery($request, 'props'),
'updated' => self::parseJsonFromQuery($request, 'updated'),
'args' => [],
'actions' => [],
'children' => self::parseJsonFromQuery($request, 'children'),
'propsFromParent' => self::parseJsonFromQuery($request, 'propsFromParent'),
];
} else {
try {
$requestData = json_decode($request->request->get('data'), true, 512, \JSON_BIGINT_AS_STRING | \JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
throw new JsonException('Could not decode request body.', $e->getCode(), $e);
}

$liveRequestData = [
'props' => $requestData['props'] ?? [],
'updated' => $requestData['updated'] ?? [],
'args' => $requestData['args'] ?? [],
'actions' => $requestData['actions'] ?? [],
'children' => $requestData['children'] ?? [],
'propsFromParent' => $requestData['propsFromParent'] ?? [],
];
}

$request->attributes->set('_live_request_data', $liveRequestData);
}

return $request->attributes->get('_live_request_data');
}

private static function parseJsonFromQuery(Request $request, string $key): array
{
if (!$request->query->has($key)) {
return [];
}

try {
return json_decode($request->query->get($key), true, 512, \JSON_THROW_ON_ERROR);
} catch (\JsonException $exception) {
throw new JsonException(sprintf('Invalid JSON on query string %s.', $key), 0, $exception);
}
}
}
52 changes: 52 additions & 0 deletions src/LiveComponent/src/ValueResolver/LiveArgValueResolver.php
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\ValueResolver;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;

/**
* @author Jannes Drijkoningen <[email protected]>
*
* @internal
*/
if (\interface_exists(ValueResolverInterface::class)) {
class LiveArgValueResolver implements ValueResolverInterface
{
use LiveArgValueResolverTrait {
resolve as resolveArgument;
}

public function resolve(Request $request, ArgumentMetadata $argument): iterable
{
if (!$this->supports($argument)) {
return [];
}

return $this->resolveArgument($request, $argument);
}
}
} else {
class LiveArgValueResolver implements ArgumentValueResolverInterface
{
use LiveArgValueResolverTrait {
supports as supportsArgument;
}

public function supports(Request $request, ArgumentMetadata $argument): bool
{
return $this->supportsArgument($argument);
}
}
}
Loading

0 comments on commit eba19c0

Please sign in to comment.