Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
18 changes: 18 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Zhortein\DatatableBundle\Renderer\DatatableRenderer;
use Zhortein\DatatableBundle\Twig\DatatableTwigExtension;

return static function (ContainerConfigurator $container): void {
$services = $container->services()
->defaults()
->autowire()
->autoconfigure()
;

$services->set(DatatableRenderer::class);
$services->set(DatatableTwigExtension::class);
};
19 changes: 19 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 8 additions & 2 deletions docs/basic-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
33 changes: 33 additions & 0 deletions src/Twig/DatatableTwigExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Zhortein\DatatableBundle\Twig;

use Twig\Attribute\AsTwigFunction;
use Zhortein\DatatableBundle\Definition\DatatableDefinition;
use Zhortein\DatatableBundle\Registry\DatatableRegistry;
use Zhortein\DatatableBundle\Renderer\DatatableRenderer;

final readonly class DatatableTwigExtension
{
public function __construct(
private DatatableRegistry $registry,
private DatatableRenderer $renderer,
) {
}

/**
* @param array<string, mixed> $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);
}
}
100 changes: 100 additions & 0 deletions tests/Unit/Twig/DatatableTwigExtensionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

declare(strict_types=1);

namespace Zhortein\DatatableBundle\Tests\Unit\Twig;

use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Twig\Attribute\AsTwigFunction;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
use Zhortein\DatatableBundle\Contract\DatatableInterface;
use Zhortein\DatatableBundle\Definition\DatatableDefinition;
use Zhortein\DatatableBundle\Registry\DatatableRegistry;
use Zhortein\DatatableBundle\Renderer\DatatableRenderer;
use Zhortein\DatatableBundle\Twig\DatatableTwigExtension;

final class DatatableTwigExtensionTest extends TestCase
{
public function test_render_method_is_exposed_as_twig_function(): void
{
$reflectionMethod = new \ReflectionMethod(DatatableTwigExtension::class, 'renderDatatable');
$attributes = $reflectionMethod->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')
;
}
}