Skip to content

Commit

Permalink
feature #2019 Adding convention to load Anonymous components from bun…
Browse files Browse the repository at this point in the history
…dles (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
  • Loading branch information
kbond committed Aug 13, 2024
2 parents 4340c81 + c207af6 commit dec76be
Show file tree
Hide file tree
Showing 9 changed files with 143 additions and 8 deletions.
4 changes: 4 additions & 0 deletions src/TwigComponent/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# CHANGELOG

## 2.20.0

- Add Anonymous Component support for 3rd-party bundles #2019

## 2.17.0

- Add nested attribute support #1405
Expand Down
48 changes: 43 additions & 5 deletions src/TwigComponent/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1622,6 +1622,42 @@ controls how components are named and where their templates live:
If a component class matches multiple namespaces, the first matched will
be used.

3rd-Party Bundle
~~~~~~~~~~~~~~~~

The flexibility of Twig Components is extended even further when integrated
with third-party bundles, allowing developers to seamlessly include pre-built
components into their projects.

Anonymous Components
--------------------

.. versionadded:: 2.20

The bundle convention for Anonymous components was added in TwigComponents 2.18.

Using a component from a third-party bundle is just as straightforward as using
one from your own application. Once the bundle is installed and configured, you
can reference its components directly within your Twig templates:

.. code-block:: html+twig

<twig:Shadcn:Button type="primary">
Click me
</twig:Shadcn:Button>

Here, the component name is composed of the bundle's Twig namespace ``Shadcn``, followed
by a colon, and then the component path Button.

.. note::

You can discover the Twig namespace of every registered bundle by inspecting the
``bin/console debug:twig`` command.

The component must be located in the bundle's ``templates/components/`` directory. For
example, the component referenced as ``<twig:Shadcn:Button>`` should have its template
file at ``templates/components/Button.html.twig`` within the Shadcn bundle.

Debugging Components
--------------------

Expand All @@ -1635,13 +1671,14 @@ that live in ``templates/components/``:
$ php bin/console debug:twig-component
+---------------+-----------------------------+------------------------------------+------+
| Component | Class | Template | Live |
| Component | Class | Template | Type |
+---------------+-----------------------------+------------------------------------+------+
| Coucou | App\Components\Alert | components/Coucou.html.twig | |
| RandomNumber | App\Components\RandomNumber | components/RandomNumber.html.twig | X |
| RandomNumber | App\Components\RandomNumber | components/RandomNumber.html.twig | Live |
| Test | App\Components\foo\Test | components/foo/Test.html.twig | |
| Button | Anonymous component | components/Button.html.twig | |
| foo:Anonymous | Anonymous component | components/foo/Anonymous.html.twig | |
| Button | | components/Button.html.twig | Anon |
| foo:Anonymous | | components/foo/Anonymous.html.twig | Anon |
| Acme:Button | | @Acme/components/Button.html.twig | Anon |
+---------------+-----------------------------+------------------------------------+------+
Pass the name of some component as an argument to print its details:
Expand All @@ -1654,9 +1691,10 @@ Pass the name of some component as an argument to print its details:
| Property | Value |
+---------------------------------------------------+-----------------------------------+
| Component | RandomNumber |
| Live | X |
| Class | App\Components\RandomNumber |
| Template | components/RandomNumber.html.twig |
| Type | Live |
+---------------------------------------------------+-----------------------------------+
| Properties (type / name / default value if exist) | string $name = toto |
| | string $type = test |
| Live Properties | int $max = 1000 |
Expand Down
40 changes: 37 additions & 3 deletions src/TwigComponent/src/Command/TwigComponentDebugCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use Symfony\UX\TwigComponent\ComponentMetadata;
use Symfony\UX\TwigComponent\Twig\PropsNode;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;

#[AsCommand(name: 'debug:twig-component', description: 'Display components and them usages for an application')]
class TwigComponentDebugCommand extends Command
Expand Down Expand Up @@ -148,13 +149,46 @@ private function findComponents(): array
*/
private function findAnonymousComponents(): array
{
$componentsDir = $this->twigTemplatesPath.'/'.$this->anonymousDirectory;
$dirs = [$componentsDir => FilesystemLoader::MAIN_NAMESPACE];
$twigLoader = $this->twig->getLoader();
if ($twigLoader instanceof FilesystemLoader) {
foreach ($twigLoader->getNamespaces() as $namespace) {
if (str_starts_with($namespace, '!')) {
continue; // ignore parent convention namespaces
}

foreach ($twigLoader->getPaths($namespace) as $path) {
if (FilesystemLoader::MAIN_NAMESPACE === $namespace) {
$componentsDir = $path.'/'.$this->anonymousDirectory;
} else {
$componentsDir = $path.'/components';
}

if (!is_dir($componentsDir)) {
continue;
}

$dirs[$componentsDir] = $namespace;
}
}
}

$components = [];
$anonymousPath = $this->twigTemplatesPath.'/'.$this->anonymousDirectory;
$finderTemplates = new Finder();
$finderTemplates->files()->in($anonymousPath)->notPath('/_')->name('*.html.twig');
$finderTemplates->files()
->in(array_keys($dirs))
->notPath('/_')
->name('*.html.twig')
;
foreach ($finderTemplates as $template) {
$component = str_replace('/', ':', $template->getRelativePathname());
$component = substr($component, 0, -10);
$component = substr($component, 0, -10); // remove file extension ".html.twig"

if (isset($dirs[$template->getPath()]) && FilesystemLoader::MAIN_NAMESPACE !== $dirs[$template->getPath()]) {
$component = $dirs[$template->getPath()].':'.$component;
}

$components[$component] = $component;
}

Expand Down
10 changes: 10 additions & 0 deletions src/TwigComponent/src/ComponentTemplateFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ public function findAnonymousComponentTemplate(string $name): ?string
return $template;
}

$parts = explode('/', $componentPath, 2);
if (\count($parts) < 2) {
return null;
}

$template = '@'.$parts[0].'/components/'.$parts[1].'.html.twig';
if ($loader->exists($template)) {
return $template;
}

return null;
}
}
22 changes: 22 additions & 0 deletions src/TwigComponent/tests/Fixtures/Bundle/AcmeBundle/AcmeBundle.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?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\TwigComponent\Tests\Fixtures\Bundle\AcmeBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class AcmeBundle extends Bundle
{
public function getPath(): string
{
return __DIR__;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

2 changes: 2 additions & 0 deletions src/TwigComponent/tests/Fixtures/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Symfony\Bundle\TwigBundle\TwigBundle;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\UX\TwigComponent\Tests\Fixtures\Bundle\AcmeBundle\AcmeBundle;
use Symfony\UX\TwigComponent\Tests\Fixtures\Component\ComponentB;
use Symfony\UX\TwigComponent\TwigComponentBundle;

Expand All @@ -32,6 +33,7 @@ public function registerBundles(): iterable
yield new FrameworkBundle();
yield new TwigBundle();
yield new TwigComponentBundle();
yield new AcmeBundle();
}

protected function configureContainer(ContainerConfigurator $c): void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,21 @@ public function testWithAnonymousComponent(): void
$this->assertStringContainsString('primary = true', $display);
}

public function testWithBundleAnonymousComponent(): void
{
$commandTester = $this->createCommandTester();
$commandTester->execute(['name' => 'Acme:Button']);

$commandTester->assertCommandIsSuccessful();

$display = $commandTester->getDisplay();

$this->tableDisplayCheck($display);
$this->assertStringContainsString('Acme:Button', $display);
$this->assertStringContainsString('@Acme/components/Button.html.twig', $display);
$this->assertStringContainsString('Anonymous', $display);
}

public function testWithoutPublicProps(): void
{
$commandTester = $this->createCommandTester();
Expand Down
9 changes: 9 additions & 0 deletions src/TwigComponent/tests/Integration/ComponentFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,15 @@ public function testAnonymous(): void
$this->factory()->metadataFor('anonymous:AButton');
}

public function testLoadingAnonymousComponentFromBundle(): void
{
$metadata = $this->factory()->metadataFor('Acme:Button');

$this->assertSame('@Acme/components/Button.html.twig', $metadata->getTemplate());
$this->assertSame('Acme:Button', $metadata->getName());
$this->assertNull($metadata->get('class'));
}

public function testAutoNamingInSubDirectory(): void
{
$metadata = $this->factory()->metadataFor('SubDirectory:ComponentInSubDirectory');
Expand Down

0 comments on commit dec76be

Please sign in to comment.