Skip to content

Commit 4314322

Browse files
committed
[Toolkit] Introduce KitContextRunner, to run code in the context of a given Kit
1 parent 58b0eb2 commit 4314322

File tree

4 files changed

+182
-52
lines changed

4 files changed

+182
-52
lines changed

src/Toolkit/config/services.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\UX\Toolkit\Command\DebugKitCommand;
1515
use Symfony\UX\Toolkit\Command\InstallComponentCommand;
16+
use Symfony\UX\Toolkit\Kit\KitContextRunner;
1617
use Symfony\UX\Toolkit\Kit\KitFactory;
1718
use Symfony\UX\Toolkit\Kit\KitSynchronizer;
1819
use Symfony\UX\Toolkit\Registry\GitHubRegistry;
@@ -75,5 +76,12 @@
7576
->args([
7677
service('filesystem'),
7778
])
79+
80+
->set('ux_toolkit.kit.kit_context_runner', KitContextRunner::class)
81+
->public()
82+
->args([
83+
service('twig'),
84+
service('ux.twig_component.component_factory'),
85+
])
7886
;
7987
};
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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\Toolkit\Kit;
13+
14+
use Symfony\Component\Filesystem\Path;
15+
use Symfony\UX\Toolkit\File\FileType;
16+
use Symfony\UX\TwigComponent\ComponentFactory;
17+
use Symfony\UX\TwigComponent\ComponentTemplateFinderInterface;
18+
use Twig\Loader\ChainLoader;
19+
use Twig\Loader\FilesystemLoader;
20+
21+
/**
22+
* @author Hugo Alliaume <[email protected]>
23+
*
24+
* @internal
25+
*/
26+
final class KitContextRunner
27+
{
28+
public function __construct(
29+
private readonly \Twig\Environment $twig,
30+
private readonly ComponentFactory $componentFactory,
31+
) {
32+
}
33+
34+
/**
35+
* @template TResult of mixed
36+
*
37+
* @param callable(Kit): TResult $callback
38+
*
39+
* @return TResult
40+
*/
41+
public function runForKit(Kit $kit, callable $callback): mixed
42+
{
43+
$resetServices = $this->contextualizeServicesForKit($kit);
44+
45+
try {
46+
return $callback($kit);
47+
} finally {
48+
$resetServices();
49+
}
50+
}
51+
52+
/**
53+
* @return callable(): void Reset the services when called
54+
*/
55+
private function contextualizeServicesForKit(Kit $kit): callable
56+
{
57+
// Configure Twig
58+
$initialTwigLoader = $this->twig->getLoader();
59+
$this->twig->setLoader(new ChainLoader([
60+
new FilesystemLoader(Path::join($kit->path, 'templates/components')),
61+
$initialTwigLoader,
62+
]));
63+
64+
// Configure Twig Components
65+
$reflComponentFactory = new \ReflectionClass($this->componentFactory);
66+
67+
$reflComponentFactoryConfig = $reflComponentFactory->getProperty('config');
68+
$initialComponentFactoryConfig = $reflComponentFactoryConfig->getValue($this->componentFactory);
69+
$reflComponentFactoryConfig->setValue($this->componentFactory, []);
70+
71+
$reflComponentFactoryComponentTemplateFinder = $reflComponentFactory->getProperty('componentTemplateFinder');
72+
$initialComponentFactoryComponentTemplateFinder = $reflComponentFactoryComponentTemplateFinder->getValue($this->componentFactory);
73+
$reflComponentFactoryComponentTemplateFinder->setValue($this->componentFactory, $this->createComponentTemplateFinder($kit));
74+
75+
return function () use ($initialTwigLoader, $reflComponentFactoryConfig, $initialComponentFactoryConfig, $reflComponentFactoryComponentTemplateFinder, $initialComponentFactoryComponentTemplateFinder) {
76+
$this->twig->setLoader($initialTwigLoader);
77+
$reflComponentFactoryConfig->setValue($this->componentFactory, $initialComponentFactoryConfig);
78+
$reflComponentFactoryComponentTemplateFinder->setValue($this->componentFactory, $initialComponentFactoryComponentTemplateFinder);
79+
};
80+
}
81+
82+
private function createComponentTemplateFinder(Kit $kit): ComponentTemplateFinderInterface
83+
{
84+
static $instances = [];
85+
86+
return $instances[$kit->name] ?? new class($kit) implements ComponentTemplateFinderInterface {
87+
public function __construct(private readonly Kit $kit)
88+
{
89+
}
90+
91+
public function findAnonymousComponentTemplate(string $name): ?string
92+
{
93+
if (null === $component = $this->kit->getComponent($name)) {
94+
throw new \RuntimeException(\sprintf('Component "%s" does not exist in kit "%s".', $name, $this->kit->name));
95+
}
96+
97+
foreach ($component->files as $file) {
98+
if (FileType::Twig === $file->type) {
99+
return $file->relativePathName;
100+
}
101+
}
102+
103+
throw new \LogicException(\sprintf('No Twig files found for component "%s" in kit "%s", it should not happens.', $name, $this->kit->name));
104+
}
105+
};
106+
}
107+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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\Toolkit\Tests\Kit;
13+
14+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
15+
use Symfony\UX\Toolkit\Kit\KitContextRunner;
16+
use Symfony\UX\TwigComponent\ComponentFactory;
17+
use Symfony\UX\TwigComponent\ComponentTemplateFinder;
18+
use Symfony\UX\TwigComponent\ComponentTemplateFinderInterface;
19+
20+
class KitContextRunnerTest extends KernelTestCase
21+
{
22+
public function testRunForKitShouldConfigureThenResetServices(): void
23+
{
24+
$twig = self::getContainer()->get('twig');
25+
$initialTwigLoader = $twig->getLoader();
26+
27+
$componentFactory = self::getContainer()->get('ux.twig_component.component_factory');
28+
$initialComponentFactoryState = $this->extractComponentFactoryState($componentFactory);
29+
$this->assertInstanceOf(ComponentTemplateFinder::class, $initialComponentFactoryState['componentTemplateFinder']);
30+
$this->assertIsArray($initialComponentFactoryState['config']);
31+
32+
$executed = false;
33+
$kitContextRunner = self::getContainer()->get('ux_toolkit.kit.kit_context_runner');
34+
$kitContextRunner->runForKit(self::getContainer()->get('ux_toolkit.registry.local')->getKit('shadcn'), function () use (&$executed, $twig, $initialTwigLoader, $componentFactory, $initialComponentFactoryState) {
35+
$executed = true;
36+
37+
$this->assertNotEquals($initialTwigLoader, $twig->getLoader(), 'The Twig loader must be different in this current kit-aware context.');
38+
$this->assertNotEquals($initialComponentFactoryState, $this->extractComponentFactoryState($componentFactory), 'The ComponentFactory state must be different in this current kit-aware context.');
39+
40+
$template = $twig->createTemplate('<twig:Alert>Hello world</twig:Alert>');
41+
$renderedTemplate = $template->render();
42+
43+
$this->assertNotEmpty($renderedTemplate);
44+
$this->assertStringContainsString('Hello world', $renderedTemplate);
45+
$this->assertStringContainsString('class="', $renderedTemplate);
46+
});
47+
$this->assertTrue($executed, \sprintf('The callback passed to %s::runForKit() has not been executed.', KitContextRunner::class));
48+
49+
$this->assertEquals($initialTwigLoader, $twig->getLoader(), 'The Twig loader must be back to its original implementation.');
50+
$this->assertEquals($initialComponentFactoryState, $this->extractComponentFactoryState($componentFactory), 'The ComponentFactory must be back to its original state.');
51+
}
52+
53+
/**
54+
* @return array{componentTemplateFinder: ComponentTemplateFinderInterface::class, config: array}
55+
*/
56+
private function extractComponentFactoryState(ComponentFactory $componentFactory): array
57+
{
58+
$componentTemplateFinder = \Closure::bind(fn (ComponentFactory $componentFactory) => $componentFactory->componentTemplateFinder, null, $componentFactory)($componentFactory);
59+
$config = \Closure::bind(fn (ComponentFactory $componentFactory) => $componentFactory->config, null, $componentFactory)($componentFactory);
60+
61+
return ['componentTemplateFinder' => $componentTemplateFinder, 'config' => $config];
62+
}
63+
}

ux.symfony.com/src/Controller/Toolkit/ComponentsController.php

Lines changed: 4 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,7 @@
2323
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
2424
use Symfony\Component\HttpKernel\Profiler\Profiler;
2525
use Symfony\Component\Routing\Attribute\Route;
26-
use Symfony\UX\Toolkit\File\FileType;
27-
use Symfony\UX\Toolkit\Kit\Kit;
28-
use Symfony\UX\TwigComponent\ComponentFactory;
29-
use Symfony\UX\TwigComponent\ComponentTemplateFinderInterface;
30-
use Twig\Loader\ChainLoader;
31-
use Twig\Loader\FilesystemLoader;
26+
use Symfony\UX\Toolkit\Kit\KitContextRunner;
3227

3328
class ComponentsController extends AbstractController
3429
{
@@ -75,8 +70,8 @@ public function previewComponent(
7570
#[MapQueryParameter] string $height,
7671
UriSigner $uriSigner,
7772
\Twig\Environment $twig,
78-
#[Autowire(service: 'ux.twig_component.component_factory')]
79-
ComponentFactory $componentFactory,
73+
#[Autowire(service: 'ux_toolkit.kit.kit_context_runner')]
74+
KitContextRunner $kitContextRunner,
8075
#[Autowire(service: 'profiler')]
8176
?Profiler $profiler,
8277
): Response {
@@ -88,34 +83,6 @@ public function previewComponent(
8883

8984
$kit = $this->toolkitService->getKit($kitId);
9085

91-
$twig->setLoader(new ChainLoader([
92-
new FilesystemLoader($kit->path.\DIRECTORY_SEPARATOR.'templates'.\DIRECTORY_SEPARATOR.'components'),
93-
$twig->getLoader(),
94-
]));
95-
96-
$this->tweakComponentFactory(
97-
$componentFactory,
98-
new class($kit) implements ComponentTemplateFinderInterface {
99-
public function __construct(
100-
private readonly Kit $kit,
101-
) {
102-
}
103-
104-
public function findAnonymousComponentTemplate(string $name): ?string
105-
{
106-
if ($component = $this->kit->getComponent($name)) {
107-
foreach ($component->files as $file) {
108-
if (FileType::Twig === $file->type) {
109-
return $file->relativePathName;
110-
}
111-
}
112-
}
113-
114-
return null;
115-
}
116-
}
117-
);
118-
11986
$template = $twig->createTemplate(<<<HTML
12087
<html lang="en">
12188
<head>
@@ -129,24 +96,9 @@ public function findAnonymousComponentTemplate(string $name): ?string
12996
HTML);
13097

13198
return new Response(
132-
$twig->render($template),
99+
$kitContextRunner->runForKit($kit, fn() => $twig->render($template)),
133100
Response::HTTP_OK,
134101
['X-Robots-Tag' => 'noindex, nofollow']
135102
);
136103
}
137-
138-
/**
139-
* Tweak the ComponentFactory to render anonymous components from the Toolkit kit.
140-
* TODO: In the future, we should implement multiple directories for anonymous components.
141-
*/
142-
private function tweakComponentFactory(ComponentFactory $componentFactory, ComponentTemplateFinderInterface $componentTemplateFinder): void
143-
{
144-
$refl = new \ReflectionClass($componentFactory);
145-
146-
$propertyConfig = $refl->getProperty('config');
147-
$propertyConfig->setValue($componentFactory, []);
148-
149-
$propertyComponentTemplateFinder = $refl->getProperty('componentTemplateFinder');
150-
$propertyComponentTemplateFinder->setValue($componentFactory, $componentTemplateFinder);
151-
}
152104
}

0 commit comments

Comments
 (0)