Skip to content

Commit b1a46cd

Browse files
committed
feature #802 Anonymous TwigComponent (matheo, WebMamba)
This PR was merged into the 2.x branch. Discussion ---------- Anonymous TwigComponent | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Tickets | | License | MIT Hey all! TwigComponent getting really cool: a custom syntax, embedded component, nested component, and so on. Why not extends all of this to anonymous components? What I call an anonymous component is a simple template. For example if you have the following template: ```php // {% components/button/button.html.twig %} <button class="primary large actions"> <span> {% block content %} {% endblock %} </span> </button> ``` You can now use it like so: ```php {% index.html.twig %} ... <div class="cart-actions"> <twig:Button:Button> Click Me! </twig:Button:Button> </div> ``` As can see you can use `:` to navigate through the directories. And this PR came also with twig_entension configuration key (default to .html.twig), to find your template with the extension you want. 😁 You now have the {% props %} tag. This tag allows you to define required props for a static component or to set a default value. ```php // {% components/button/button.html.twig %} {% props label, primary = true %} <button class="{{ primary ? = 'primary' ? 'secondary' }} large actions"> <span> {{ label }} </span> </button> ``` So here you have one require prop `label`, and one optional prop primary with a default value set to true. You can now use your anonymous component like so: ```php {% index.html.twig %} ... <div class="cart-actions"> <twig:Button.Button label="Click me"> </div> Commits ------- 3c9ec5e order checks to match best pratice first 904a613 remove .idea folder b19c71a destruct properties 82d9a9f use attributes in template 226c301 remove tag for ComponentTemplateFinderInterface 5369c25 fixing syntax 8defd7a make class finals 708ff5d use RuntimeExeception for prop errors 58c927a fix 3453468 ComponentTemplateFinder 03504fc destruct option to ExposeInTemplate attribute 1d55891 ComponentTemplateFinderInterface to isolate logic 6a162a1 add files header 6906714 Add test for component in nested directory 7f4e71e rename static to anonymous 78a576d remove unused files 2bffe7c Render Statics Components and Props tags 1b7020e rename twig extension 718f3a1 add configuration for template extension c8a10a7 add the ability to have static component
2 parents 670a37a + 3c9ec5e commit b1a46cd

17 files changed

+311
-11
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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\TwigComponent;
13+
14+
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
15+
16+
/**
17+
* @author Matheo Daninos <[email protected]>
18+
*
19+
* @internal
20+
*/
21+
final class AnonymousComponent
22+
{
23+
private array $props;
24+
25+
public function mount($props = []): void
26+
{
27+
$this->props = $props;
28+
}
29+
30+
#[ExposeInTemplate(destruct: true)]
31+
public function getProps(): array
32+
{
33+
return $this->props;
34+
}
35+
}

src/TwigComponent/src/Attribute/ExposeInTemplate.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@
2222
final class ExposeInTemplate
2323
{
2424
/**
25-
* @param string|null $name The variable name to expose. Leave as null
26-
* to default to property name.
27-
* @param string|null $getter The getter method to use. Leave as null
28-
* to default to PropertyAccessor logic.
25+
* @param string|null $name The variable name to expose. Leave as null
26+
* to default to property name.
27+
* @param string|null $getter The getter method to use. Leave as null
28+
* to default to PropertyAccessor logic.
29+
* @param bool $destruct The content should be used as array of variable
30+
* names
2931
*/
30-
public function __construct(public ?string $name = null, public ?string $getter = null)
32+
public function __construct(public ?string $name = null, public ?string $getter = null, public bool $destruct = false)
3133
{
3234
}
3335
}

src/TwigComponent/src/ComponentFactory.php

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@ final class ComponentFactory
3030
* @param array<class-string, string> $classMap
3131
*/
3232
public function __construct(
33+
private ComponentTemplateFinderInterface $componentTemplateFinder,
3334
private ServiceLocator $components,
3435
private PropertyAccessorInterface $propertyAccessor,
3536
private EventDispatcherInterface $eventDispatcher,
3637
private array $config,
37-
private array $classMap,
38+
private array $classMap
3839
) {
3940
}
4041

@@ -43,6 +44,13 @@ public function metadataFor(string $name): ComponentMetadata
4344
$name = $this->classMap[$name] ?? $name;
4445

4546
if (!$config = $this->config[$name] ?? null) {
47+
if (($template = $this->componentTemplateFinder->findAnonymousComponentTemplate($name)) !== null) {
48+
return new ComponentMetadata([
49+
'key' => $name,
50+
'template' => $template,
51+
]);
52+
}
53+
4654
$this->throwUnknownComponentException($name);
4755
}
4856

@@ -124,6 +132,12 @@ private function mount(object $component, array &$data): void
124132
return;
125133
}
126134

135+
if ($component instanceof AnonymousComponent) {
136+
$component->mount($data);
137+
138+
return;
139+
}
140+
127141
$parameters = [];
128142

129143
foreach ($method->getParameters() as $refParameter) {
@@ -149,6 +163,10 @@ private function getComponent(string $name): object
149163
$name = $this->classMap[$name] ?? $name;
150164

151165
if (!$this->components->has($name)) {
166+
if ($this->isAnonymousComponent($name)) {
167+
return new AnonymousComponent();
168+
}
169+
152170
$this->throwUnknownComponentException($name);
153171
}
154172

@@ -189,11 +207,16 @@ private function postMount(object $component, array $data): array
189207
return $data;
190208
}
191209

210+
private function isAnonymousComponent(string $name): bool
211+
{
212+
return null !== $this->componentTemplateFinder->findAnonymousComponentTemplate($name);
213+
}
214+
192215
/**
193216
* @return never
194217
*/
195218
private function throwUnknownComponentException(string $name): void
196219
{
197-
throw new \InvalidArgumentException(sprintf('Unknown component "%s". The registered components are: %s', $name, implode(', ', array_keys($this->config))));
220+
throw new \InvalidArgumentException(sprintf('Unknown component "%s". The registered components are: %s. And no matching anonymous component template was found', $name, implode(', ', array_keys($this->config))));
198221
}
199222
}

src/TwigComponent/src/ComponentRenderer.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,12 @@ private function exposedVariables(object $component, bool $exposePublicProps): \
131131
/** @var ExposeInTemplate $attribute */
132132
$value = $attribute->getter ? $component->{rtrim($attribute->getter, '()')}() : $this->propertyAccessor->getValue($component, $property->name);
133133

134+
if ($attribute->destruct) {
135+
foreach ($value as $key => $destructedValue) {
136+
yield $key => $destructedValue;
137+
}
138+
}
139+
134140
yield $attribute->name ?? $property->name => $value;
135141
}
136142

@@ -148,6 +154,14 @@ private function exposedVariables(object $component, bool $exposePublicProps): \
148154
throw new \LogicException(sprintf('Cannot use %s on methods with required parameters (%s::%s).', ExposeInTemplate::class, $component::class, $method->name));
149155
}
150156

157+
if ($attribute->destruct) {
158+
foreach ($component->{$method->name}() as $prop => $value) {
159+
yield $prop => $value;
160+
}
161+
162+
return;
163+
}
164+
151165
yield $name => $component->{$method->name}();
152166
}
153167
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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\TwigComponent;
13+
14+
use Twig\Environment;
15+
16+
/**
17+
* @author Matheo Daninos <[email protected]>
18+
*/
19+
final class ComponentTemplateFinder implements ComponentTemplateFinderInterface
20+
{
21+
public function __construct(
22+
private Environment $environment
23+
) {
24+
}
25+
26+
public function findAnonymousComponentTemplate(string $name): ?string
27+
{
28+
$loader = $this->environment->getLoader();
29+
$componentPath = rtrim(str_replace(':', '/', $name));
30+
31+
if ($loader->exists('components/'.$componentPath.'.html.twig')) {
32+
return 'components/'.$componentPath.'.html.twig';
33+
}
34+
35+
if ($loader->exists($componentPath.'.html.twig')) {
36+
return $componentPath.'.html.twig';
37+
}
38+
39+
if ($loader->exists('components/'.$componentPath)) {
40+
return 'components/'.$componentPath;
41+
}
42+
43+
if ($loader->exists($componentPath)) {
44+
return $componentPath;
45+
}
46+
47+
return null;
48+
}
49+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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\TwigComponent;
13+
14+
/**
15+
* @author Matheo Daninos <[email protected]>
16+
*/
17+
interface ComponentTemplateFinderInterface
18+
{
19+
public function findAnonymousComponentTemplate(string $name): ?string;
20+
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ public function process(ContainerBuilder $container): void
5858
}
5959

6060
$factoryDefinition = $container->findDefinition('ux.twig_component.component_factory');
61-
$factoryDefinition->setArgument(0, ServiceLocatorTagPass::register($container, $componentReferences));
62-
$factoryDefinition->setArgument(3, $componentConfig);
63-
$factoryDefinition->setArgument(4, $componentClassMap);
61+
$factoryDefinition->setArgument(1, ServiceLocatorTagPass::register($container, $componentReferences));
62+
$factoryDefinition->setArgument(4, $componentConfig);
63+
$factoryDefinition->setArgument(5, $componentClassMap);
6464
}
6565
}

src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Symfony\UX\TwigComponent\ComponentRenderer;
2323
use Symfony\UX\TwigComponent\ComponentRendererInterface;
2424
use Symfony\UX\TwigComponent\ComponentStack;
25+
use Symfony\UX\TwigComponent\ComponentTemplateFinder;
2526
use Symfony\UX\TwigComponent\DependencyInjection\Compiler\TwigComponentPass;
2627
use Symfony\UX\TwigComponent\Twig\ComponentExtension;
2728
use Symfony\UX\TwigComponent\Twig\ComponentLexer;
@@ -40,6 +41,14 @@ public function load(array $configs, ContainerBuilder $container): void
4041
throw new LogicException('The TwigBundle is not registered in your application. Try running "composer require symfony/twig-bundle".');
4142
}
4243

44+
$container->register('ux.twig_component.component_template_finder', ComponentTemplateFinder::class)
45+
->setArguments([
46+
new Reference('twig'),
47+
])
48+
;
49+
50+
$container->setAlias(ComponentRendererInterface::class, 'ux.twig_component.component_renderer');
51+
4352
$container->registerAttributeForAutoconfiguration(
4453
AsTwigComponent::class,
4554
static function (ChildDefinition $definition, AsTwigComponent $attribute) {
@@ -49,6 +58,7 @@ static function (ChildDefinition $definition, AsTwigComponent $attribute) {
4958

5059
$container->register('ux.twig_component.component_factory', ComponentFactory::class)
5160
->setArguments([
61+
new Reference('ux.twig_component.component_template_finder'),
5262
class_exists(AbstractArgument::class) ? new AbstractArgument(sprintf('Added in %s.', TwigComponentPass::class)) : null,
5363
new Reference('property_accessor'),
5464
new Reference('event_dispatcher'),
@@ -68,7 +78,7 @@ class_exists(AbstractArgument::class) ? new AbstractArgument(sprintf('Added in %
6878
])
6979
;
7080

71-
$container->setAlias(ComponentRendererInterface::class, 'ux.twig_component.component_renderer');
81+
$container->register(ComponentTemplateFinder::class, 'ux.twig_component.component_template_finder');
7282

7383
$container->register('ux.twig_component.twig.component_extension', ComponentExtension::class)
7484
->addTag('twig.extension')

src/TwigComponent/src/Twig/ComponentExtension.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public function getTokenParsers(): array
4949
{
5050
return [
5151
new ComponentTokenParser(fn () => $this->container->get(ComponentFactory::class)),
52+
new PropsTokenParser(),
5253
];
5354
}
5455

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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\TwigComponent\Twig;
13+
14+
use Twig\Compiler;
15+
use Twig\Node\Node;
16+
17+
/**
18+
* @author Matheo Daninos <[email protected]>
19+
*
20+
* @internal
21+
*/
22+
class PropsNode extends Node
23+
{
24+
public function __construct(array $propsNames, array $values, $lineno = 0, string $tag = null)
25+
{
26+
parent::__construct($values, ['names' => $propsNames], $lineno, $tag);
27+
}
28+
29+
public function compile(Compiler $compiler): void
30+
{
31+
foreach ($this->getAttribute('names') as $name) {
32+
$compiler
33+
->addDebugInfo($this)
34+
->write('if (!isset($context[\''.$name.'\'])) {')
35+
;
36+
37+
if (!$this->hasNode($name)) {
38+
$compiler
39+
->write('throw new \Twig\Error\RuntimeError("'.$name.' should be defined for component '.$this->getTemplateName().'");')
40+
->write('}')
41+
;
42+
43+
continue;
44+
}
45+
46+
$compiler
47+
->write('$context[\''.$name.'\'] = ')
48+
->subcompile($this->getNode($name))
49+
->raw(";\n")
50+
->write('}')
51+
;
52+
}
53+
}
54+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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\TwigComponent\Twig;
13+
14+
use Twig\Node\Node;
15+
use Twig\Token;
16+
use Twig\TokenParser\AbstractTokenParser;
17+
18+
/**
19+
* @author Matheo Daninos <[email protected]>
20+
*
21+
* @internal
22+
*/
23+
class PropsTokenParser extends AbstractTokenParser
24+
{
25+
public function parse(Token $token): Node
26+
{
27+
$parser = $this->parser;
28+
$stream = $parser->getStream();
29+
30+
$names = [];
31+
$values = [];
32+
while (!$stream->nextIf(Token::BLOCK_END_TYPE)) {
33+
$name = $stream->expect(\Twig\Token::NAME_TYPE)->getValue();
34+
35+
if ($stream->nextIf(Token::OPERATOR_TYPE, '=')) {
36+
$values[$name] = $parser->getExpressionParser()->parseExpression();
37+
}
38+
39+
$names[] = $name;
40+
41+
if (!$stream->nextIf(Token::PUNCTUATION_TYPE)) {
42+
break;
43+
}
44+
}
45+
46+
$stream->expect(\Twig\Token::BLOCK_END_TYPE);
47+
48+
return new PropsNode($names, $values, $token->getLine(), $token->getValue());
49+
}
50+
51+
public function getTag(): string
52+
{
53+
return 'props';
54+
}
55+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<twig:Button label='Click me'/>

0 commit comments

Comments
 (0)