Welcome to Abdul Malik Ikhsan's Blog

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

Tagged with: