Apply Twig Extension via Middleware in Slim 4
If we use Slim 4, we can use Twig for template engine, and there is slim/twig-view
for that. However, when dealing with extension, especially routing and uri related stuff, we can’t use a way like in Slim 3, at least, by default, at least, what I know right now. There is an alternative way to add twig extension, which is via Middleware!
First, I assume that the Twig
service already registered in app/dependencies.php
:
<?php // app/dependencies.php declare(strict_types=1); use DI\ContainerBuilder; use Psr\Container\ContainerInterface; use Slim\Views\Twig; return function (ContainerBuilder $containerBuilder) { $containerBuilder->addDefinitions([ // ... Twig::class => function (ContainerInterface $c) { return new Twig(__DIR__ . '/../templates', [ 'cache' => __DIR__ . '/../var/cache', 'auto_reload' => true ]); }, // ... ]); };
Now, we need to add extension, which consume Slim\Routing\RouteCollector
and Slim\Psr7\Uri
instance. Now, we can create middleware on the fly in app/middleware.php
, eg: via anonymous class that implements Psr\Http\Server\MiddlewareInterface
.
<?php // app/middleware.php declare(strict_types=1); // ... use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\UriInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Slim\App; use Slim\Routing\RouteCollector; use Slim\Views\Twig; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; // ... return function (App $app) { // ... $container = $app->getContainer(); $twig = $container->get(Twig::class); $router = $app->getRouteCollector(); $app->add(new class ($twig, $router) implements MiddlewareInterface { private $twig; private $router; public function __construct(Twig $twig, RouteCollector $router) { $this->twig = $twig; $this->router = $router; } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $uri = $request->getUri(); $this->twig->addExtension(new class ($this->router, $uri) extends AbstractExtension { private $router; private $uri; public function __construct(RouteCollector $router, UriInterface $uri) { $this->router = $router; $this->uri = $uri; } public function getFunctions() { return [ new TwigFunction('base_path', function () : string { return $this->router->getBasePath(); }), new TwigFunction('full_url_for', function (string $routeName, array $data = [], array $queryParams = []) : string { return $this->router->getRouteParser()->fullUrlFor($this->uri, $routeName, $data, $queryParams); }), ]; } }); return $handler->handle($request); } }); // ... };
In its __construct
, we inject with Slim\Views\Twig
and Slim\Routing\RouteCollector
instance. In its process()
function, we call Slim\Views\Twig::addExtension()
function to add extension, which we can create another anonymous class that extends Twig\Extension\AbstractExtension
, injected with Slim\Routing\RouteCollector
and Slim\Psr7\Uri
(which pulled from object instance of Psr\Http\Message\ServerRequestInterface
), which we can add getFunctions()
method to add twig functions.
That’s it, in above example, I added base_path
and full_url_for
function which now can be used in twig view, eg:
{# display base path url #} {{ base_path() }} {# display full url for route with name "postpagedetail" with parameter id = 1 and query parameter action = view #} {{ full_url_for('postpagedetail', { 'id': 1 }, { 'action': 'view' }) }}
You are feeling have too much code in
app/middleware.php
? Of course, you can create a separate class for it!
Using DDD architecture with zend-db in Slim 4
Slim 4 provides a skeleton that uses DDD directory style. By this, we can implement DDD architecture easier. Let’s try with zend-db
for it. In this post, I assume that you already uses Twig
as view template engine like in previous post.
Requirements
First, we need to require zend-db via composer:
$ composer require zendframework/zend-db \ zendframework/zend-hydrator \ --sort-packages
We are going to make pages to display post
table record(s) with the following data:
# create and use database create database slim; use slim; # create table create table post(id int not null primary key auto_increment, title varchar(50) not null, content text not null); # insert data insert into post(title, content) values('first post', 'first post content'); insert into post(title, content) values('second post', 'second post content');
Database Configuration
<?php // app/settings.php // ... return function (ContainerBuilder $containerBuilder) { $containerBuilder->addDefinitions([ // ... 'db' => [ 'username' => 'root', 'password' => '', 'driver' => 'pdo_mysql', 'database' => 'slim', 'host' => 'localhost', 'driver_options' => [ \PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\'', ], ], // ... ], ]); };
We can register database configuration at app/settings.php
, for example, use MySQL database as configured above.
DB Service
We are using service named Zend\Db\Adapter\AdapterInterface
, so we can register as follow in app/dependencies.php
:
<?php // app/dependencies.php declare(strict_types=1); use DI\ContainerBuilder; use Psr\Container\ContainerInterface; use Zend\Db\Adapter\Adapter; use Zend\Db\Adapter\AdapterInterface; return function (ContainerBuilder $containerBuilder) { $containerBuilder->addDefinitions([ // ... AdapterInterface::class => function (ContainerInterface $c) { return new Adapter($c->get('settings')['db']); }, // ... ]); };
Domain
a. Post
class:
<?php // src/Domain/Post/Post.php declare(strict_types=1); namespace App\Domain\Post; final class Post { public $id; public $title; public $content; public const TABLE = 'post'; public function __construct(Post $post = null) { if ($post instanceof Post) { $this->id = +$post->id; $this->title = ucwords($post->title); $this->content = ucfirst($post->content); } } }
b. PostRepository
interface:
<?php // src/Domain/Post/PostRepository.php declare(strict_types=1); namespace App\Domain\Post; interface PostRepository { public function findAll(): array; public function findPostOfId(int $id): Post; }
c. PostNotFoundException
class:
<?php // src/Domain/Post/PostNotFoundException.php declare(strict_types=1); namespace App\Domain\Post; use App\Domain\DomainException\DomainRecordNotFoundException; class PostNotFoundException extends DomainRecordNotFoundException { public $message = 'The post you requested does not exist.'; }
Infrastructure
We can create a repository that implements App\Domain\Post\PostRepository
, eg: named ZendDbPostRepository
:
<?php // src/Infrastructure/Persitence/Post/ZendDbPostRepository.php declare(strict_types=1); namespace App\Infrastructure\Persistence\Post; use function compact; use App\Domain\Post\Post; use App\Domain\Post\PostNotFoundException; use App\Domain\Post\PostRepository; use Zend\Db\TableGateway\AbstractTableGateway; class ZendDbPostRepository implements PostRepository { private $tableGateway; public function __construct(AbstractTableGateway $tableGateway) { $this->tableGateway = $tableGateway; } public function findAll(): array { $results = $this->tableGateway->select(); $posts = []; foreach ($results as $result) { $posts[] = new Post($result); } return $posts; } public function findPostOfId(int $id): Post { $current = $this->tableGateway->select(compact('id'))->current(); if (! $current) { throw new PostNotFoundException(); } return new Post($current); } }
The Repository class above needs to be registered to app/repositories.php
:
<?php // app/repositories.php declare(strict_types=1); use App\Domain\Post\Post; use App\Domain\Post\PostRepository; use App\Infrastructure\Persistence\Post\ZendDbPostRepository; use DI\ContainerBuilder; use Psr\Container\ContainerInterface; use Zend\Db\Adapter\AdapterInterface; use Zend\Db\ResultSet\HydratingResultSet; use Zend\Db\TableGateway\TableGateway; use Zend\Hydrator\ObjectPropertyHydrator; return function (ContainerBuilder $containerBuilder) { $containerBuilder->addDefinitions([ // ... PostRepository::class => function (ContainerInterface $c) { $tableGateway = new TableGateway( Post::TABLE, $c->get(AdapterInterface::class), null, new HydratingResultSet(new ObjectPropertyHydrator(), new Post()) ); return new ZendDbPostRepository($tableGateway); }, // ... ]); };
Application
For application, we can create 2 classes to display all posts and display post by id.
a. All posts via PostPage
class
<?php // src/Application/Page/PostPage.php declare(strict_types=1); namespace App\Application\Page; use App\Domain\Post\PostRepository; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use Slim\Psr7\Response; use Slim\Views\Twig; class PostPage implements RequestHandlerInterface { private $view; private $postRepository; public function __construct(Twig $view, PostRepository $postRepository) { $this->view = $view; $this->postRepository = $postRepository; } public function handle(ServerRequestInterface $request) : ResponseInterface { return $this->view->render( new Response(), 'page/post/index.html.twig', [ 'posts' => $this->postRepository->findAll(), ] ); } }
b. Post By Id via PostPageDetail
class
<?php // src/Application/Page/PostPageDetail.php declare(strict_types=1); namespace App\Application\Page; use App\Domain\DomainException\DomainRecordNotFoundException; use App\Domain\Post\PostRepository; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use Slim\Exception\HttpNotFoundException; use Slim\Psr7\Response; use Slim\Views\Twig; class PostPageDetail implements RequestHandlerInterface { private $view; private $postRepository; public function __construct(Twig $view, PostRepository $postRepository) { $this->view = $view; $this->postRepository = $postRepository; } public function handle(ServerRequestInterface $request) : ResponseInterface { $id = +$request->getAttribute('id'); try { $post = $this->postRepository->findPostOfId($id); } catch (DomainRecordNotFoundException $e) { throw new HttpNotFoundException($request, $e->getMessage()); } return $this->view->render( new Response(), 'page/post/detail.html.twig', [ 'post' => $post, ] ); } }
Routing
Now, time to register routing:
<?php // app/routes.php declare(strict_types=1); use App\Application\Page\PostPage; use App\Application\Page\PostPageDetail; use Slim\App; use Slim\Handlers\Strategies\RequestHandler; return function (App $app) { $routeCollector = $app->getRouteCollector(); $routeCollector->setDefaultInvocationStrategy( new RequestHandler(true) ); $app->get('/posts', PostPage::class); $app->get('/posts/{id:[0-9]+}', PostPageDetail::class); };
View
a. templates/page/post/index.html.twig
:
{# templates/page/post/index.html.twig #} {% extends "layout.html.twig" %} {% block body %} {% for post in posts %} <h3> {{ post.title }} </h3> <p> {{ post.content }} </p> {% endfor %} {% endblock %}
b. templates/page/post/detail.html.twig
:
{# templates/page/post/detail.html.twig #} {% extends "layout.html.twig" %} {% block body %} <h3> {{ post.title }} </h3> <p> {{ post.content }} </p> {% endblock %}
Run PHP Server
php -S localhost:8080 -t public
Now, we can see the pages, http://localhost:8080/posts for all posts:
and http://localhost:8080/posts/1 for detail post:
leave a comment