Skip to content

Commit dec76be

Browse files
committed
feature #2019 Adding convention to load Anonymous components from bundles (yceruto)
This PR was merged into the 2.x branch. Discussion ---------- Adding convention to load Anonymous components from bundles | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Issues | Fix #2003 (partially) | License | MIT This adds a fallback convention to check if a requested anonymous component, `<twig:Acme:Alert>`, exists on the bundle side using the current Twig loading convention. The resolution process would work like this: * Currently, the finder will check if `components/Acme/Alert.html.twig` exists (resolving to `<app>/templates/components/Acme/Alert.html.twig`) * If not, the finder will check if ``@Acme`/components/Alert.html.twig` exists (resolving to `<bundle>/templates/components/Alert.html.twig`) (this is the new code) Here, the `components` directory is hardcoded for bundles, as `anonymous_template_directory` is exclusively a userland configuration. ``` acme-bundle/ └─ templates/ └─ components/ └─ Alert.html.twig ``` From here, you can organize your components into subdirectories if desired. For example, a component like `<twig:Acme:Table:Header>` will be located in `<acme-bundle>/templates/components/Table/Header.html.twig`. TODO: - [x] Add some tests - [x] Add doc Commits ------- c207af6 Add Anonymous Components support for 3rd-party bundles
2 parents 4340c81 + c207af6 commit dec76be

File tree

9 files changed

+143
-8
lines changed

9 files changed

+143
-8
lines changed

src/TwigComponent/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# CHANGELOG
22

3+
## 2.20.0
4+
5+
- Add Anonymous Component support for 3rd-party bundles #2019
6+
37
## 2.17.0
48

59
- Add nested attribute support #1405

src/TwigComponent/doc/index.rst

+43-5
Original file line numberDiff line numberDiff line change
@@ -1622,6 +1622,42 @@ controls how components are named and where their templates live:
16221622
If a component class matches multiple namespaces, the first matched will
16231623
be used.
16241624

1625+
3rd-Party Bundle
1626+
~~~~~~~~~~~~~~~~
1627+
1628+
The flexibility of Twig Components is extended even further when integrated
1629+
with third-party bundles, allowing developers to seamlessly include pre-built
1630+
components into their projects.
1631+
1632+
Anonymous Components
1633+
--------------------
1634+
1635+
.. versionadded:: 2.20
1636+
1637+
The bundle convention for Anonymous components was added in TwigComponents 2.18.
1638+
1639+
Using a component from a third-party bundle is just as straightforward as using
1640+
one from your own application. Once the bundle is installed and configured, you
1641+
can reference its components directly within your Twig templates:
1642+
1643+
.. code-block:: html+twig
1644+
1645+
<twig:Shadcn:Button type="primary">
1646+
Click me
1647+
</twig:Shadcn:Button>
1648+
1649+
Here, the component name is composed of the bundle's Twig namespace ``Shadcn``, followed
1650+
by a colon, and then the component path Button.
1651+
1652+
.. note::
1653+
1654+
You can discover the Twig namespace of every registered bundle by inspecting the
1655+
``bin/console debug:twig`` command.
1656+
1657+
The component must be located in the bundle's ``templates/components/`` directory. For
1658+
example, the component referenced as ``<twig:Shadcn:Button>`` should have its template
1659+
file at ``templates/components/Button.html.twig`` within the Shadcn bundle.
1660+
16251661
Debugging Components
16261662
--------------------
16271663

@@ -1635,13 +1671,14 @@ that live in ``templates/components/``:
16351671
$ php bin/console debug:twig-component
16361672
16371673
+---------------+-----------------------------+------------------------------------+------+
1638-
| Component | Class | Template | Live |
1674+
| Component | Class | Template | Type |
16391675
+---------------+-----------------------------+------------------------------------+------+
16401676
| Coucou | App\Components\Alert | components/Coucou.html.twig | |
1641-
| RandomNumber | App\Components\RandomNumber | components/RandomNumber.html.twig | X |
1677+
| RandomNumber | App\Components\RandomNumber | components/RandomNumber.html.twig | Live |
16421678
| Test | App\Components\foo\Test | components/foo/Test.html.twig | |
1643-
| Button | Anonymous component | components/Button.html.twig | |
1644-
| foo:Anonymous | Anonymous component | components/foo/Anonymous.html.twig | |
1679+
| Button | | components/Button.html.twig | Anon |
1680+
| foo:Anonymous | | components/foo/Anonymous.html.twig | Anon |
1681+
| Acme:Button | | @Acme/components/Button.html.twig | Anon |
16451682
+---------------+-----------------------------+------------------------------------+------+
16461683
16471684
Pass the name of some component as an argument to print its details:
@@ -1654,9 +1691,10 @@ Pass the name of some component as an argument to print its details:
16541691
| Property | Value |
16551692
+---------------------------------------------------+-----------------------------------+
16561693
| Component | RandomNumber |
1657-
| Live | X |
16581694
| Class | App\Components\RandomNumber |
16591695
| Template | components/RandomNumber.html.twig |
1696+
| Type | Live |
1697+
+---------------------------------------------------+-----------------------------------+
16601698
| Properties (type / name / default value if exist) | string $name = toto |
16611699
| | string $type = test |
16621700
| Live Properties | int $max = 1000 |

src/TwigComponent/src/Command/TwigComponentDebugCommand.php

+37-3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use Symfony\UX\TwigComponent\ComponentMetadata;
2828
use Symfony\UX\TwigComponent\Twig\PropsNode;
2929
use Twig\Environment;
30+
use Twig\Loader\FilesystemLoader;
3031

3132
#[AsCommand(name: 'debug:twig-component', description: 'Display components and them usages for an application')]
3233
class TwigComponentDebugCommand extends Command
@@ -148,13 +149,46 @@ private function findComponents(): array
148149
*/
149150
private function findAnonymousComponents(): array
150151
{
152+
$componentsDir = $this->twigTemplatesPath.'/'.$this->anonymousDirectory;
153+
$dirs = [$componentsDir => FilesystemLoader::MAIN_NAMESPACE];
154+
$twigLoader = $this->twig->getLoader();
155+
if ($twigLoader instanceof FilesystemLoader) {
156+
foreach ($twigLoader->getNamespaces() as $namespace) {
157+
if (str_starts_with($namespace, '!')) {
158+
continue; // ignore parent convention namespaces
159+
}
160+
161+
foreach ($twigLoader->getPaths($namespace) as $path) {
162+
if (FilesystemLoader::MAIN_NAMESPACE === $namespace) {
163+
$componentsDir = $path.'/'.$this->anonymousDirectory;
164+
} else {
165+
$componentsDir = $path.'/components';
166+
}
167+
168+
if (!is_dir($componentsDir)) {
169+
continue;
170+
}
171+
172+
$dirs[$componentsDir] = $namespace;
173+
}
174+
}
175+
}
176+
151177
$components = [];
152-
$anonymousPath = $this->twigTemplatesPath.'/'.$this->anonymousDirectory;
153178
$finderTemplates = new Finder();
154-
$finderTemplates->files()->in($anonymousPath)->notPath('/_')->name('*.html.twig');
179+
$finderTemplates->files()
180+
->in(array_keys($dirs))
181+
->notPath('/_')
182+
->name('*.html.twig')
183+
;
155184
foreach ($finderTemplates as $template) {
156185
$component = str_replace('/', ':', $template->getRelativePathname());
157-
$component = substr($component, 0, -10);
186+
$component = substr($component, 0, -10); // remove file extension ".html.twig"
187+
188+
if (isset($dirs[$template->getPath()]) && FilesystemLoader::MAIN_NAMESPACE !== $dirs[$template->getPath()]) {
189+
$component = $dirs[$template->getPath()].':'.$component;
190+
}
191+
158192
$components[$component] = $component;
159193
}
160194

src/TwigComponent/src/ComponentTemplateFinder.php

+10
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,16 @@ public function findAnonymousComponentTemplate(string $name): ?string
6666
return $template;
6767
}
6868

69+
$parts = explode('/', $componentPath, 2);
70+
if (\count($parts) < 2) {
71+
return null;
72+
}
73+
74+
$template = '@'.$parts[0].'/components/'.$parts[1].'.html.twig';
75+
if ($loader->exists($template)) {
76+
return $template;
77+
}
78+
6979
return null;
7080
}
7181
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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\Tests\Fixtures\Bundle\AcmeBundle;
13+
14+
use Symfony\Component\HttpKernel\Bundle\Bundle;
15+
16+
class AcmeBundle extends Bundle
17+
{
18+
public function getPath(): string
19+
{
20+
return __DIR__;
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

src/TwigComponent/tests/Fixtures/Kernel.php

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Bundle\TwigBundle\TwigBundle;
1818
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
1919
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
20+
use Symfony\UX\TwigComponent\Tests\Fixtures\Bundle\AcmeBundle\AcmeBundle;
2021
use Symfony\UX\TwigComponent\Tests\Fixtures\Component\ComponentB;
2122
use Symfony\UX\TwigComponent\TwigComponentBundle;
2223

@@ -32,6 +33,7 @@ public function registerBundles(): iterable
3233
yield new FrameworkBundle();
3334
yield new TwigBundle();
3435
yield new TwigComponentBundle();
36+
yield new AcmeBundle();
3537
}
3638

3739
protected function configureContainer(ContainerConfigurator $c): void

src/TwigComponent/tests/Integration/Command/TwigComponentDebugCommandTest.php

+15
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,21 @@ public function testWithAnonymousComponent(): void
150150
$this->assertStringContainsString('primary = true', $display);
151151
}
152152

153+
public function testWithBundleAnonymousComponent(): void
154+
{
155+
$commandTester = $this->createCommandTester();
156+
$commandTester->execute(['name' => 'Acme:Button']);
157+
158+
$commandTester->assertCommandIsSuccessful();
159+
160+
$display = $commandTester->getDisplay();
161+
162+
$this->tableDisplayCheck($display);
163+
$this->assertStringContainsString('Acme:Button', $display);
164+
$this->assertStringContainsString('@Acme/components/Button.html.twig', $display);
165+
$this->assertStringContainsString('Anonymous', $display);
166+
}
167+
153168
public function testWithoutPublicProps(): void
154169
{
155170
$commandTester = $this->createCommandTester();

src/TwigComponent/tests/Integration/ComponentFactoryTest.php

+9
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,15 @@ public function testAnonymous(): void
169169
$this->factory()->metadataFor('anonymous:AButton');
170170
}
171171

172+
public function testLoadingAnonymousComponentFromBundle(): void
173+
{
174+
$metadata = $this->factory()->metadataFor('Acme:Button');
175+
176+
$this->assertSame('@Acme/components/Button.html.twig', $metadata->getTemplate());
177+
$this->assertSame('Acme:Button', $metadata->getName());
178+
$this->assertNull($metadata->get('class'));
179+
}
180+
172181
public function testAutoNamingInSubDirectory(): void
173182
{
174183
$metadata = $this->factory()->metadataFor('SubDirectory:ComponentInSubDirectory');

0 commit comments

Comments
 (0)