Welcome to Abdul Malik Ikhsan's Blog

Apply Twig Extension via Middleware in Slim 4

Posted in Slim 4 by samsonasik on October 28, 2019

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

Posted in Slim 4 by samsonasik on October 27, 2019

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 class:

<?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: