Skip to content

Commit 12f4ae5

Browse files
committed
refactor to TwigComponent PHP8 attribute
- removes ComponentInterface - requires PHP8+
1 parent b6ebd2a commit 12f4ae5

File tree

13 files changed

+144
-121
lines changed

13 files changed

+144
-121
lines changed

src/TwigComponent/README.md

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,13 @@ Every component consists of (1) a class:
1313
// src/Components/AlertComponent.php
1414
namespace App\Components;
1515

16-
use Symfony\UX\TwigComponent\ComponentInterface;
16+
use Symfony\UX\TwigComponent\Attribute\TwigComponent;
1717

18-
class AlertComponent implements ComponentInterface
18+
#[TwigComponent('alert')]
19+
class AlertComponent
1920
{
2021
public string $type = 'success';
2122
public string $message;
22-
23-
public static function getComponentName(): string
24-
{
25-
return 'alert';
26-
}
2723
}
2824
```
2925

@@ -64,28 +60,25 @@ That's it! We're ready to go!
6460

6561
Let's create a reusable "alert" element that we can use to show
6662
success or error messages across our site. Step 1 is always to create
67-
a component that implements `ComponentInterface`. Let's start as simple
68-
as possible:
63+
a component has a `TwigComponent` class attribute. Let's start as
64+
simple as possible:
6965

7066
```php
7167
// src/Components/AlertComponent.php
7268
namespace App\Components;
7369

74-
use Symfony\UX\TwigComponent\ComponentInterface;
70+
use Symfony\UX\TwigComponent\Attribute\TwigComponent;
7571

76-
class AlertComponent implements ComponentInterface
72+
#[TwigComponent('alert')]
73+
class AlertComponent
7774
{
78-
public static function getComponentName(): string
79-
{
80-
return 'alert';
81-
}
8275
}
8376
```
8477

85-
Step 2 is to create a template for this component. Templates live
86-
in `templates/components/{Component Name}.html.twig`, where
87-
`{Component Name}` is whatever you return from the `getComponentName()`
88-
method:
78+
Step 2 is to create a template for this component. By default,
79+
templates live in `templates/components/{Component Name}.html.twig`,
80+
where `{Component Name}` is whatever you set as the `TwigComponent`
81+
attribute's `name` parameter:
8982

9083
```twig
9184
{# templates/components/alert.html.twig #}
@@ -116,7 +109,8 @@ that, create a public property for each:
116109
// src/Components/AlertComponent.php
117110
// ...
118111

119-
class AlertComponent implements ComponentInterface
112+
#[TwigComponent('alert')]
113+
class AlertComponent
120114
{
121115
+ public string $message;
122116

@@ -153,6 +147,23 @@ property of the object. Then, the component is rendered! If a
153147
property has a setter method (e.g. `setMessage()`), that will
154148
be called instead of setting the property directly.
155149

150+
### Customize the Twig Template
151+
152+
You can customize the template used to render the template by
153+
passing it as the second argument to the `TwigComponent` attribute:
154+
155+
```diff
156+
// src/Components/AlertComponent.php
157+
// ...
158+
159+
-#[TwigComponent('alert')]
160+
+#[TwigComponent('alert', 'my/custom/template.html.twig')]
161+
class AlertComponent
162+
{
163+
// ...
164+
}
165+
```
166+
156167
### The mount() Method
157168

158169
If, for some reason, you don't want an option to the `component()`
@@ -163,7 +174,8 @@ a `mount()` method in your component:
163174
// src/Components/AlertComponent.php
164175
// ...
165176

166-
class AlertComponent implements ComponentInterface
177+
#[TwigComponent('alert')]
178+
class AlertComponent
167179
{
168180
public string $message;
169181
public string $type = 'success';
@@ -209,9 +221,10 @@ Doctrine entity and `ProductRepository`:
209221
namespace App\Components;
210222

211223
use App\Repository\ProductRepository;
212-
use Symfony\UX\TwigComponent\ComponentInterface;
224+
use Symfony\UX\TwigComponent\Attribute\TwigComponent;
213225

214-
class FeaturedProductsComponent implements ComponentInterface
226+
#[TwigComponent('featured_products')]
227+
class FeaturedProductsComponent
215228
{
216229
private ProductRepository $productRepository;
217230

@@ -225,11 +238,6 @@ class FeaturedProductsComponent implements ComponentInterface
225238
// an example method that returns an array of Products
226239
return $this->productRepository->findFeatured();
227240
}
228-
229-
public static function getComponentName() : string
230-
{
231-
return 'featured_products';
232-
}
233241
}
234242
```
235243

@@ -289,7 +297,8 @@ method), you can store its result on a private property:
289297
namespace App\Components;
290298
// ...
291299

292-
class FeaturedProductsComponent implements ComponentInterface
300+
#[TwigComponent('featured_products')]
301+
class FeaturedProductsComponent
293302
{
294303
private ProductRepository $productRepository;
295304

src/TwigComponent/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
}
2727
},
2828
"require": {
29-
"php": ">=7.2.5",
29+
"php": ">=8.0",
3030
"twig/twig": "^2.0|^3.0",
3131
"symfony/property-access": "^4.4|^5.0",
3232
"symfony/dependency-injection": "^4.4|^5.0"
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace Symfony\UX\TwigComponent\Attribute;
4+
5+
/*
6+
* This file is part of the Symfony package.
7+
*
8+
* (c) Fabien Potencier <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
/**
15+
* @author Kevin Bond <[email protected]>
16+
*
17+
* @experimental
18+
*/
19+
#[\Attribute(\Attribute::TARGET_CLASS)]
20+
class TwigComponent
21+
{
22+
private string $name;
23+
private ?string $template;
24+
25+
public function __construct(string $name, ?string $template = null)
26+
{
27+
$this->name = $name;
28+
$this->template = $template;
29+
}
30+
31+
public function getName(): string
32+
{
33+
return $this->name;
34+
}
35+
36+
public function getTemplate(): string
37+
{
38+
return $this->template ?? "components/{$this->name}.html.twig";
39+
}
40+
41+
/**
42+
* @internal
43+
*/
44+
public static function forClass(string $class): ?self
45+
{
46+
$class = new \ReflectionClass($class);
47+
48+
if (!$attribute = $class->getAttributes(self::class, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) {
49+
return null;
50+
}
51+
52+
return $attribute->newInstance();
53+
}
54+
}

src/TwigComponent/src/ComponentFactory.php

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,19 @@
2121
*/
2222
final class ComponentFactory
2323
{
24-
private $components;
25-
private $propertyAccessor;
26-
private $serviceIdMap;
24+
private ServiceLocator $components;
25+
private PropertyAccessorInterface $propertyAccessor;
2726

28-
/**
29-
* @param ServiceLocator|ComponentInterface[] $components
30-
*/
31-
public function __construct(ServiceLocator $components, PropertyAccessorInterface $propertyAccessor, array $serviceIdMap)
27+
public function __construct(ServiceLocator $components, PropertyAccessorInterface $propertyAccessor)
3228
{
3329
$this->components = $components;
3430
$this->propertyAccessor = $propertyAccessor;
35-
$this->serviceIdMap = $serviceIdMap;
3631
}
3732

3833
/**
3934
* Creates the component and "mounts" it with the passed data.
4035
*/
41-
public function create(string $name, array $data = []): ComponentInterface
36+
public function create(string $name, array $data = []): object
4237
{
4338
$component = $this->getComponent($name);
4439

@@ -59,21 +54,12 @@ public function create(string $name, array $data = []): ComponentInterface
5954
/**
6055
* Returns the "unmounted" component.
6156
*/
62-
public function get(string $name): ComponentInterface
57+
public function get(string $name): object
6358
{
6459
return $this->getComponent($name);
6560
}
6661

67-
public function serviceIdFor(string $name): string
68-
{
69-
if (!isset($this->serviceIdMap[$name])) {
70-
throw new \InvalidArgumentException('Component not found.');
71-
}
72-
73-
return $this->serviceIdMap[$name];
74-
}
75-
76-
private function mount(ComponentInterface $component, array &$data): void
62+
private function mount(object $component, array &$data): void
7763
{
7864
try {
7965
$method = (new \ReflectionClass($component))->getMethod('mount');
@@ -102,10 +88,10 @@ private function mount(ComponentInterface $component, array &$data): void
10288
$component->mount(...$parameters);
10389
}
10490

105-
private function getComponent(string $name): ComponentInterface
91+
private function getComponent(string $name): object
10692
{
10793
if (!$this->components->has($name)) {
108-
throw new \InvalidArgumentException(sprintf('Unknown component "%s". The registered components are: %s', $name, implode(', ', array_keys($this->serviceIdMap))));
94+
throw new \InvalidArgumentException(sprintf('Unknown component "%s". The registered components are: %s', $name, implode(', ', array_keys($this->components->getProvidedServices()))));
10995
}
11096

11197
return $this->components->get($name);

src/TwigComponent/src/ComponentInterface.php

Lines changed: 0 additions & 22 deletions
This file was deleted.

src/TwigComponent/src/ComponentRenderer.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\UX\TwigComponent;
1313

14+
use Symfony\UX\TwigComponent\Attribute\TwigComponent;
1415
use Twig\Environment;
1516

1617
/**
@@ -20,19 +21,20 @@
2021
*/
2122
final class ComponentRenderer
2223
{
23-
private $twig;
24+
private Environment $twig;
2425

2526
public function __construct(Environment $twig)
2627
{
2728
$this->twig = $twig;
2829
}
2930

30-
public function render(ComponentInterface $component): string
31+
public function render(object $component): string
3132
{
32-
// TODO: Template attribute/annotation/interface to customize
3333
// TODO: Self-Rendering components?
34-
$templateName = sprintf('components/%s.html.twig', $component::getComponentName());
34+
if (!$attribute = TwigComponent::forClass($component::class)) {
35+
throw new \InvalidArgumentException(\sprintf('"%s" is not a Twig Component, did you forget to add the TwigComponent attribute?', $component::class));
36+
}
3537

36-
return $this->twig->render($templateName, ['this' => $component]);
38+
return $this->twig->render($attribute->getTemplate(), ['this' => $component]);
3739
}
3840
}

src/TwigComponent/src/DependencyInjection/Compiler/TwigComponentPass.php

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
1515
use Symfony\Component\DependencyInjection\ContainerBuilder;
1616
use Symfony\Component\DependencyInjection\Exception\LogicException;
17-
use Symfony\UX\TwigComponent\ComponentFactory;
17+
use Symfony\Component\DependencyInjection\Reference;
18+
use Symfony\UX\TwigComponent\Attribute\TwigComponent;
1819

1920
/**
2021
* @author Kevin Bond <[email protected]>
@@ -27,23 +28,31 @@ public function process(ContainerBuilder $container): void
2728
{
2829
$serviceIdMap = [];
2930

30-
foreach (array_keys($container->findTaggedServiceIds('twig.component')) as $serviceId) {
31-
$definition = $container->getDefinition($serviceId);
31+
foreach ($container->getDefinitions() as $id => $definition) {
32+
$class = $definition->getClass();
33+
34+
if (!\class_exists($class) || !$attribute = TwigComponent::forClass($class)) {
35+
continue;
36+
}
37+
38+
$name = $attribute->getName();
3239

3340
// make all component services non-shared
3441
$definition->setShared(false);
3542

36-
$name = $definition->getClass()::getComponentName();
37-
3843
// ensure component not already defined
3944
if (\array_key_exists($name, $serviceIdMap)) {
4045
throw new LogicException(sprintf('Component "%s" is already registered as "%s", components cannot be registered more than once.', $definition->getClass(), $serviceIdMap[$name]));
4146
}
4247

43-
// add to service id map for ComponentFactory
44-
$serviceIdMap[$name] = $serviceId;
48+
$serviceIdMap[$name] = new Reference($id);
49+
50+
// add a consistent alias for use by LiveComponent
51+
$container->setAlias("ux.twig.component.{$name}", $id);
4552
}
4653

47-
$container->getDefinition(ComponentFactory::class)->setArgument(2, $serviceIdMap);
54+
$container->findDefinition('ux.twig.component_locator')
55+
->setArgument(0, $serviceIdMap)
56+
;
4857
}
4958
}

0 commit comments

Comments
 (0)