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