From 5479a31cff846570e08a047abe230bd5a6380399 Mon Sep 17 00:00:00 2001 From: David RENARD Date: Sat, 9 May 2026 11:47:03 +0200 Subject: [PATCH] feat: implement initial Twig datatable function --- README.md | 8 +- config/services.php | 18 ++++ docs/architecture.md | 19 ++++ docs/basic-usage.md | 10 +- src/Twig/DatatableTwigExtension.php | 33 ++++++ .../Unit/Twig/DatatableTwigExtensionTest.php | 100 ++++++++++++++++++ 6 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 config/services.php create mode 100644 src/Twig/DatatableTwigExtension.php create mode 100644 tests/Unit/Twig/DatatableTwigExtensionTest.php diff --git a/README.md b/README.md index 723e395..c8adb7d 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,13 @@ Expected Twig usage: {{ zhortein_datatable('users') }} ``` -The Twig helper is not implemented yet. +With runtime options: + +```twig +{{ zhortein_datatable('users', { + search: true +}) }} +``` ## Documentation diff --git a/config/services.php b/config/services.php new file mode 100644 index 0000000..c575cb8 --- /dev/null +++ b/config/services.php @@ -0,0 +1,18 @@ +services() + ->defaults() + ->autowire() + ->autoconfigure() + ; + + $services->set(DatatableRenderer::class); + $services->set(DatatableTwigExtension::class); +}; diff --git a/docs/architecture.md b/docs/architecture.md index 5168df8..4b06f9e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -119,6 +119,25 @@ At this stage, the renderer does not load data and does not depend on Doctrine. Future steps will connect this renderer to the provider layer and Ajax fragments. +### Twig datatable function + +The first public rendering API is the `zhortein_datatable` Twig function. + +Expected usage: + +```twig +{{ zhortein_datatable('users') }} +``` + +The Twig extension is intentionally thin: + +- it resolves the datatable by name through the registry; +- it creates a `DatatableDefinition`; +- it lets the datatable class build the definition; +- it delegates HTML rendering to `DatatableRenderer`. + +Business rendering logic must remain in the renderer and Twig templates, not in the Twig extension. + ## Ajax controller diff --git a/docs/basic-usage.md b/docs/basic-usage.md index 0901f97..46df906 100644 --- a/docs/basic-usage.md +++ b/docs/basic-usage.md @@ -60,13 +60,19 @@ $definition ## Render the datatable -Expected Twig direction: +Use the `zhortein_datatable` Twig function: ```twig {{ zhortein_datatable('users') }} ``` -This Twig helper is not implemented yet. +Runtime options can be passed as the second argument: + +```twig +{{ zhortein_datatable('users', { + search: true +}) }} +``` ## Ajax behavior diff --git a/src/Twig/DatatableTwigExtension.php b/src/Twig/DatatableTwigExtension.php new file mode 100644 index 0000000..e796765 --- /dev/null +++ b/src/Twig/DatatableTwigExtension.php @@ -0,0 +1,33 @@ + $options + */ + #[AsTwigFunction('zhortein_datatable', isSafe: ['html'])] + public function renderDatatable(string $name, array $options = []): string + { + $datatable = $this->registry->get($name); + $definition = new DatatableDefinition($name); + + $datatable->buildDatatable($definition); + + return $this->renderer->render($definition, $options); + } +} diff --git a/tests/Unit/Twig/DatatableTwigExtensionTest.php b/tests/Unit/Twig/DatatableTwigExtensionTest.php new file mode 100644 index 0000000..5b61ebd --- /dev/null +++ b/tests/Unit/Twig/DatatableTwigExtensionTest.php @@ -0,0 +1,100 @@ +getAttributes(AsTwigFunction::class); + + self::assertCount(1, $attributes); + + $attribute = $attributes[0]->newInstance(); + + self::assertSame('zhortein_datatable', $attribute->name); + self::assertSame(['html'], $attribute->isSafe); + } + + public function test_it_renders_datatable_by_name(): void + { + $extension = $this->createExtension(); + + $html = $extension->renderDatatable('users'); + + self::assertStringContainsString('id="zhortein-datatable-users"', $html); + self::assertStringContainsString('data-controller="zhortein-datatable"', $html); + self::assertStringContainsString('Email', $html); + self::assertStringContainsString('No data available.', $html); + } + + public function test_it_renders_datatable_with_runtime_options(): void + { + $extension = $this->createExtension(); + + $html = $extension->renderDatatable('users', [ + 'search' => true, + ]); + + self::assertStringContainsString('id="zhortein-datatable-users"', $html); + self::assertStringContainsString('type="search"', $html); + self::assertStringContainsString('data-zhortein-datatable-target="searchInput"', $html); + self::assertStringContainsString('Email', $html); + } + + private function createExtension(?Environment $twig = null): DatatableTwigExtension + { + $twig ??= $this->createTwigEnvironment(); + + $datatable = new TwigExtensionTestDatatable(); + + $registry = new DatatableRegistry( + new ServiceLocator([ + 'users' => static fn (): TwigExtensionTestDatatable => $datatable, + ]), + ['users' => TwigExtensionTestDatatable::class], + ); + + return new DatatableTwigExtension( + registry: $registry, + renderer: new DatatableRenderer($twig), + ); + } + + private function createTwigEnvironment(): Environment + { + $loader = new FilesystemLoader(); + $loader->addPath(__DIR__.'/../../../templates', 'ZhorteinDatatable'); + + return new Environment($loader, [ + 'strict_variables' => true, + 'autoescape' => 'html', + ]); + } +} + +final class TwigExtensionTestDatatable implements DatatableInterface +{ + public function buildDatatable(DatatableDefinition $definition): void + { + $definition + ->setEntityClass(\stdClass::class) + ->addColumn('e.id', visible: false) + ->addColumn('e.email', label: 'Email') + ; + } +}