Skip to content

Commit 48117f4

Browse files
committed
add a required "default" action for live components
1 parent d6bacc2 commit 48117f4

File tree

11 files changed

+81
-41
lines changed

11 files changed

+81
-41
lines changed

src/LiveComponent/src/Attribute/AsLiveComponent.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,32 @@
2121
#[\Attribute(\Attribute::TARGET_CLASS)]
2222
final class AsLiveComponent extends AsTwigComponent
2323
{
24+
private string $defaultAction;
25+
26+
public function __construct(string $name, ?string $template = null, string $defaultAction = '__invoke')
27+
{
28+
parent::__construct($name, $template);
29+
30+
$this->defaultAction = trim($defaultAction, '()');
31+
}
32+
33+
/**
34+
* @internal
35+
*
36+
* @param string|object $component
37+
*/
38+
public static function defaultActionFor($component): string
39+
{
40+
$component = \is_object($component) ? \get_class($component) : $component;
41+
$method = self::forClass($component)->defaultAction;
42+
43+
if (!method_exists($component, $method)) {
44+
throw new \LogicException(sprintf('Live component "%s" requires the default action method "%s".%s', $component, $method, $method === '__invoke' ? ' Either add this method or use the DefaultActionTrait' : ''));
45+
}
46+
47+
return $method;
48+
}
49+
2450
/**
2551
* @internal
2652
*
@@ -51,6 +77,10 @@ public static function liveProps(object $component): \Traversable
5177
*/
5278
public static function isActionAllowed(object $component, string $action): bool
5379
{
80+
if (self::defaultActionFor($component) === $action) {
81+
return true;
82+
}
83+
5484
foreach (self::attributeMethodsFor(LiveAction::class, $component) as $method) {
5585
if ($action === $method->getName()) {
5686
return true;

src/LiveComponent/src/DefaultComponentController.php renamed to src/LiveComponent/src/DefaultActionTrait.php

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,19 @@
1515
* @author Kevin Bond <kevinbond@gmail.com>
1616
*
1717
* @experimental
18-
*
19-
* @internal
2018
*/
21-
final class DefaultComponentController
19+
trait DefaultActionTrait
2220
{
23-
private object $component;
24-
25-
public function __construct(object $component)
26-
{
27-
$this->component = $component;
28-
}
29-
21+
/**
22+
* The "default" action for a component.
23+
*
24+
* This is executed when your component is being re-rendered,
25+
* but no custom action is being called. You probably don't
26+
* want to do any work here because this method is *not*
27+
* executed when a custom action is triggered.
28+
*/
3029
public function __invoke(): void
3130
{
32-
}
33-
34-
public function getComponent(): object
35-
{
36-
return $this->component;
31+
// noop - this is the default action
3732
}
3833
}

src/LiveComponent/src/DependencyInjection/Compiler/LiveComponentPass.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,18 @@ public function process(ContainerBuilder $container): void
2727
$componentServiceMap = [];
2828

2929
foreach (array_keys($container->findTaggedServiceIds('twig.component')) as $id) {
30+
$class = $container->findDefinition($id)->getClass();
31+
3032
try {
31-
$attribute = AsLiveComponent::forClass($container->findDefinition($id)->getClass());
33+
$attribute = AsLiveComponent::forClass($class);
3234
} catch (\InvalidArgumentException $e) {
3335
continue;
3436
}
3537

36-
$componentServiceMap[$attribute->getName()] = $id;
38+
$componentServiceMap[$attribute->getName()] = [$id, $class];
39+
40+
// Ensure default action method is configured correctly
41+
AsLiveComponent::defaultActionFor($class);
3742
}
3843

3944
$container->findDefinition('ux.live_component.event_subscriber')->setArgument(0, $componentServiceMap);

src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
use Symfony\UX\LiveComponent\PropertyHydratorInterface;
3131
use Symfony\UX\LiveComponent\Twig\LiveComponentExtension as LiveComponentTwigExtension;
3232
use Symfony\UX\LiveComponent\Twig\LiveComponentRuntime;
33-
use Symfony\UX\TwigComponent\ComponentFactory;
3433
use Symfony\UX\TwigComponent\ComponentRenderer;
3534

3635
/**
@@ -78,7 +77,6 @@ public function load(array $configs, ContainerBuilder $container): void
7877
class_exists(AbstractArgument::class) ? new AbstractArgument(sprintf('Added in %s.', LiveComponentPass::class)) : [],
7978
])
8079
->addTag('kernel.event_subscriber')
81-
->addTag('container.service_subscriber', ['key' => ComponentFactory::class, 'id' => 'ux.twig_component.component_factory'])
8280
->addTag('container.service_subscriber', ['key' => ComponentRenderer::class, 'id' => 'ux.twig_component.component_renderer'])
8381
->addTag('container.service_subscriber', ['key' => LiveComponentHydrator::class, 'id' => 'ux.live_component.component_hydrator'])
8482
->addTag('container.service_subscriber')

src/LiveComponent/src/EventListener/LiveComponentSubscriber.php

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,7 @@
2929
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
3030
use Symfony\Contracts\Service\ServiceSubscriberInterface;
3131
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
32-
use Symfony\UX\LiveComponent\DefaultComponentController;
3332
use Symfony\UX\LiveComponent\LiveComponentHydrator;
34-
use Symfony\UX\TwigComponent\ComponentFactory;
3533
use Symfony\UX\TwigComponent\ComponentRenderer;
3634

3735
/**
@@ -45,7 +43,7 @@ class LiveComponentSubscriber implements EventSubscriberInterface, ServiceSubscr
4543
private const JSON_FORMAT = 'live-component-json';
4644
private const JSON_CONTENT_TYPE = 'application/vnd.live-component+json';
4745

48-
/** @var array<string, string> */
46+
/** @var array<string, string[]> */
4947
private array $componentServiceMap;
5048
private ContainerInterface $container;
5149

@@ -58,7 +56,6 @@ public function __construct(array $componentServiceMap, ContainerInterface $cont
5856
public static function getSubscribedServices(): array
5957
{
6058
return [
61-
ComponentFactory::class,
6259
ComponentRenderer::class,
6360
LiveComponentHydrator::class,
6461
'?'.CsrfTokenManagerInterface::class,
@@ -79,11 +76,17 @@ public function onKernelRequest(RequestEvent $event): void
7976
$action = $request->get('action', 'get');
8077
$componentName = (string) $request->get('component');
8178

79+
if (!\array_key_exists($componentName, $this->componentServiceMap)) {
80+
throw new NotFoundHttpException(sprintf('Component "%s" not found.', $componentName));
81+
}
82+
83+
[$componentServiceId, $componentClass] = $this->componentServiceMap[$componentName];
84+
8285
if ('get' === $action) {
8386
// set default controller for "default" action
8487
$request->attributes->set(
8588
'_controller',
86-
new DefaultComponentController($this->container->get(ComponentFactory::class)->get($componentName))
89+
sprintf('%s::%s', $componentServiceId, AsLiveComponent::defaultActionFor($componentClass))
8790
);
8891

8992
return;
@@ -99,11 +102,7 @@ public function onKernelRequest(RequestEvent $event): void
99102
throw new BadRequestHttpException('Invalid CSRF token.');
100103
}
101104

102-
if (!\array_key_exists($componentName, $this->componentServiceMap)) {
103-
throw new NotFoundHttpException(sprintf('Component "%s" not found.', $componentName));
104-
}
105-
106-
$request->attributes->set('_controller', sprintf('%s::%s', $this->componentServiceMap[$componentName], $action));
105+
$request->attributes->set('_controller', sprintf('%s::%s', $componentServiceId, $action));
107106
}
108107

109108
public function onKernelController(ControllerEvent $event): void
@@ -119,20 +118,17 @@ public function onKernelController(ControllerEvent $event): void
119118
$request->request->all()
120119
);
121120

122-
$component = $event->getController();
123-
$action = null;
124-
125-
if (\is_array($component)) {
126-
// action is being called
127-
$action = $component[1];
128-
$component = $component[0];
121+
if (!\is_array($controller = $event->getController()) || 2 !== \count($controller)) {
122+
throw new \RuntimeException('Not a valid live component.');
129123
}
130124

131-
if ($component instanceof DefaultComponentController) {
132-
$component = $component->getComponent();
125+
[$component, $action] = $controller;
126+
127+
if (!\is_object($component)) {
128+
throw new \RuntimeException('Not a valid live component.');
133129
}
134130

135-
if (null !== $action && !AsLiveComponent::isActionAllowed($component, $action)) {
131+
if (!AsLiveComponent::isActionAllowed($component, $action)) {
136132
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)));
137133
}
138134

src/LiveComponent/tests/Fixture/Component/Component1.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
1515
use Symfony\UX\LiveComponent\Attribute\LiveAction;
1616
use Symfony\UX\LiveComponent\Attribute\LiveProp;
17+
use Symfony\UX\LiveComponent\DefaultActionTrait;
1718
use Symfony\UX\LiveComponent\Tests\Fixture\Entity\Entity1;
1819

1920
/**
@@ -22,6 +23,8 @@
2223
#[AsLiveComponent('component1')]
2324
final class Component1
2425
{
26+
use DefaultActionTrait;
27+
2528
#[LiveProp]
2629
public ?Entity1 $prop1;
2730

src/LiveComponent/tests/Fixture/Component/Component2.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
/**
2424
* @author Kevin Bond <kevinbond@gmail.com>
2525
*/
26-
#[AsLiveComponent('component2')]
26+
#[AsLiveComponent('component2', defaultAction: 'defaultAction()')]
2727
final class Component2
2828
{
2929
#[LiveProp]
@@ -35,6 +35,10 @@ final class Component2
3535

3636
public bool $beforeReRenderCalled = false;
3737

38+
public function defaultAction(): void
39+
{
40+
}
41+
3842
#[LiveAction]
3943
public function increase(): void
4044
{

src/LiveComponent/tests/Fixture/Component/Component3.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,16 @@
1313

1414
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
1515
use Symfony\UX\LiveComponent\Attribute\LiveProp;
16+
use Symfony\UX\LiveComponent\DefaultActionTrait;
1617

1718
/**
1819
* @author Kevin Bond <kevinbond@gmail.com>
1920
*/
2021
#[AsLiveComponent('component3')]
2122
final class Component3
2223
{
24+
use DefaultActionTrait;
25+
2326
#[LiveProp(fieldName: 'myProp1')]
2427
public $prop1;
2528

src/LiveComponent/tests/Fixture/Component/Component5.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,14 @@
1111

1212
namespace Symfony\UX\LiveComponent\Tests\Fixture\Component;
1313

14+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
15+
use Symfony\UX\LiveComponent\DefaultActionTrait;
16+
1417
/**
1518
* @author Kevin Bond <kevinbond@gmail.com>
1619
*/
20+
#[AsLiveComponent('component5')]
1721
final class Component5 extends Component4
1822
{
23+
use DefaultActionTrait;
1924
}

src/LiveComponent/tests/Unit/Attribute/AsLiveComponentTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,6 @@ public function testCanCheckIfMethodIsAllowed(): void
5959

6060
$this->assertTrue(AsLiveComponent::isActionAllowed($component, 'method1'));
6161
$this->assertFalse(AsLiveComponent::isActionAllowed($component, 'method2'));
62+
$this->assertTrue(AsLiveComponent::isActionAllowed($component, '__invoke'));
6263
}
6364
}

src/TwigComponent/src/Attribute/AsTwigComponent.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ final public static function forClass(string $class): self
4848
$class = new \ReflectionClass($class);
4949

5050
if (!$attribute = $class->getAttributes(static::class, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) {
51-
throw new \InvalidArgumentException(sprintf('"%s" is not a Twig Component, did you forget to add the "%s" attribute?', $class, static::class));
51+
throw new \InvalidArgumentException(sprintf('"%s" is not a Twig Component, did you forget to add the "%s" attribute?', $class->getName(), static::class));
5252
}
5353

5454
return $attribute->newInstance();

0 commit comments

Comments
 (0)