Rapid Development of Zend Expressive Modules
I have learned a few tricks when writing Zend Expressive modules that I want to share with you.
Please follow the previous post first to set up a proper working environment. I explained how to install and configure Zend Expressive with Doctrine, Gulp, and an abstract reflection factory – it’ll take a total of 10 minutes.
In this tutorial, we’ll build a simple read-only blog module (a page listing blog posts from a database) in minutes, demonstrating the kind of rapid development one is capable of with Zend Expressive.
Module Setup
Run this command from your expressive app to get started:
./vendor/bin/expressive module:create Blog
This will generate some base code for a Blog module and will register your module automatically with your application. It will also register your module with the Composer autoloader.
Doctrine Entity and Database Table
Let’s make our Blog entity and database tables. First, we need to let our application know that this module provides Doctrine entities.
Open src/Blog/src/ConfigProvider.php
and add the following:
public function __invoke()
{
return [
'dependencies' => $this->getDependencies(),
'doctrine' => $this->getDoctrine(),
'templates' => $this->getTemplates(),
];
}
/**
* @return array
*/
public function getDoctrine(): array
{
return [
'driver' => [
'orm_default' => [
'drivers' => [
'Blog\Entity' => 'blog_entity',
],
],
'blog_entity' => [
'class' => \Doctrine\ORM\Mapping\Driver\SimplifiedYamlDriver::class,
'cache' => 'array',
'paths' => [
dirname(__DIR__) . '/config/doctrine' => 'Blog\Entity',
],
],
],
];
}
Create a blog post entity config at src/Blog/config/doctrine/BlogPost.orm.yml
:
---
Blog\Entity\BlogPost:
type: entity
table: blog_post
id:
id:
type: integer
generator:
strategy: AUTO
fields:
title:
type: string
length: 255
content:
type: string
length: 16777215
Then, run ./vendor/bin/doctrine orm:generate-entities src
.
Sadly, Doctrine doesn’t and probably won’t support PSR-4 because the standard doesn’t force a directory structure.
To get around this, we need to move src/Blog/Entity
to src/Blog/src/Entity
.
Then, run this command to create your database table:
./vendor/bin/doctrine orm:schema-tool:create
Now, you can populate the database table by running the following SQL:
INSERT INTO expressive.blog_post VALUES
(null, 'Post 1', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'),
(null, 'Post 2', 'Mauris in libero laoreet, euismod lorem eget, tincidunt justo.'),
(null, 'Post 3', 'Donec sed diam congue, ultrices tellus at, venenatis felis.');
Routing
Modules in Expressive do not register their own routes. We can make them
do so, however, with this handy trick. You don’t have to understand it. Just put the files in place and know that it works.
Create a src/Blog/src/Factory/RoutesDelegator.php
with the following contents:
<?php
namespace Blog\Factory;
use Blog\Action;
use Psr\Container\ContainerInterface;
use Zend\Expressive\Application;
class RoutesDelegator
{
/**
* @param ContainerInterface $container
* @param string $serviceName Name of the service being created.
* @param callable $callback Creates and returns the service.
* @return Application
*/
public function __invoke(ContainerInterface $container, $serviceName, callable $callback)
{
/** @var $app Application */
$app = $callback();
include __DIR__ . '/../../config/routes.php';
return $app;
}
}
In src/Blog/src/ConfigProvider.php
, add this as a top level array key to the getDependencies()
method:
'delegators' => [
\Zend\Expressive\Application::class => [
Factory\RoutesDelegator::class,
],
],
Now you can create a src/Blog/config/routes.php
file and start adding blog routes.
<?php
/**
* Setup routes with a single request method:
* @var \Zend\Expressive\Application $app
*
* $app->post('/album', App\Action\AlbumCreateAction::class, 'album.create');
* $app->put('/album/:id', App\Action\AlbumUpdateAction::class, 'album.put');
* $app->patch('/album/:id', App\Action\AlbumUpdateAction::class, 'album.patch');
* $app->delete('/album/:id', App\Action\AlbumDeleteAction::class, 'album.delete');
*
* Or with multiple request methods:
*
* $app->route('/contact', App\Action\ContactAction::class, ['GET', 'POST', ...], 'contact');
*
* Or handling all request methods:
*
* $app->route('/contact', App\Action\ContactAction::class)->setName('contact');
*
* or:
*
* $app->route(
* '/contact',
* App\Action\ContactAction::class,
* Zend\Expressive\Router\Route::HTTP_METHOD_ANY,
* 'contact'
* );
*/
use Blog\Action;
// Setup routes:
$app->get('/blog', Action\BlogPostListAction::class, 'blog_post_list');
$app->get('/blog/view/:blog_post_id', Action\BlogPostViewAction::class, 'blog_post_view');
Actions
Then, we need to create an action to respond to each route.
Create src/Blog/src/Action/BlogPostListAction.php
:
<?php
namespace Blog\Action;
use Blog\Entity\BlogPost;
use Doctrine\ORM\EntityManager;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface as ServerMiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Expressive\Router;
use Zend\Expressive\Template;
class BlogPostListAction implements ServerMiddlewareInterface
{
/**
* @var Template\TemplateRendererInterface
*/
private $templateRenderer;
/**
* @var EntityManager
*/
private $entityManager;
public function __construct(
EntityManager $entityManager,
Template\TemplateRendererInterface $templateRenderer = null
) {
$this->templateRenderer = $templateRenderer;
$this->entityManager = $entityManager;
}
public function process(ServerRequestInterface $request, DelegateInterface $delegate)
{
$posts = $this->entityManager->getRepository(BlogPost::class)
->findAll();
$data = [
'posts' => $posts,
];
return new HtmlResponse($this->templateRenderer->render('blog::list', $data));
}
}
Create src/Blog/src/Action/BlogPostViewAction.php
:
<?php
namespace Blog\Action;
use Blog\Entity\BlogPost;
use Doctrine\ORM\EntityManager;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface as ServerMiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Expressive\Router;
use Zend\Expressive\Router\RouteResult;
use Zend\Expressive\Template;
class BlogPostViewAction implements ServerMiddlewareInterface
{
/**
* @var Router\RouterInterface
*/
private $router;
/**
* @var Template\TemplateRendererInterface
*/
private $templateRenderer;
/**
* @var EntityManager
*/
private $entityManager;
public function __construct(
EntityManager $entityManager,
Router\RouterInterface $router,
Template\TemplateRendererInterface $templateRenderer = null
) {
$this->router = $router;
$this->templateRenderer = $templateRenderer;
$this->entityManager = $entityManager;
}
public function process(ServerRequestInterface $request, DelegateInterface $delegate)
{
/** @var RouteResult $routeResult */
$routeResult = $request->getAttribute(RouteResult::class);
$routeMatchedParams = $routeResult->getMatchedParams();
if (empty($routeMatchedParams['blog_post_id'])) {
throw new \RuntimeException('Invalid route: "blog_post_id" not set in matched route params.');
}
$blogId = $routeMatchedParams['blog_post_id'];
/** @var BlogPost $blogPost */
$blogPost = $this->entityManager->find(BlogPost::class, $blogId);
if (!$blogPost) {
return new HtmlResponse($this->templateRenderer->render('error::404'), 404);
}
$data = [
'post' => $blogPost,
];
return new HtmlResponse($this->templateRenderer->render('blog::view', $data));
}
}
Templates
Open src/Blog/src/ConfigProvider.php
. and update the getTemplates()
method to this:
public function getTemplates()
{
return [
'paths' => [
'blog' => [__DIR__ . '/../templates/blog'],
],
];
}
Now we can make some quick templates:
Create src/Blog/templates/blog/list.html.twig
:
{% extends '@layout/default.html.twig' %}
{% block title %}Blog{% endblock %}
{% block content %}
<div class="row">
{% for post in posts %}
<div class="col-md-4">
<h2>
<a href="/blog/view/{{ post.id }}">
<i class="fa fa-refresh"></i> {{ post.title }}
</a>
</h2>
<p>
{{ post.content }}
</p>
</div>
{% endfor %}
</div>
{% endblock %}
Create src/Blog/templates/blog/view.html.twig
:
{% extends '@layout/default.html.twig' %}
{% block title %}{{ post.title }} | Blog {% endblock %}
{% block content %}
<div class="row">
<div class="col-xs-12">
<h1>{{ post.title }}</h1>
<p>
{{ post.content }}
</p>
</div>
</div>
{% endblock %}
If you open the /blog
URL, you’ll have a functional, database driven blog list and will be able to view pages.
We’ll leave the implementation of create, edit, and delete functionality up to you as homework.
Conclusion
In this short tutorial, we saw how simple it was to implement a read-only blog module with Zend Expressive. In no more than a handful of files and 10 minutes of work, the list page could display our posts from the database and was ready for additional routes, like /edit
, and /delete
.
Do you use Zend Expressive in your projects? What do you like/dislike about it? Let us know how you get on!