Rapid Development of Zend Expressive Modules

Kirk Madera
Kirk Madera
Share

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.

Zend expressive logo

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.

Blog

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!

Frequently Asked Questions (FAQs) about Rapid Development with Zend Expressive Modules

What is Zend Expressive and how does it work?

Zend Expressive is a middleware microframework for PHP. It’s built on top of Zend Stratigility, which allows for PSR-7 middleware. Middleware is a software design pattern that provides a way to add additional behavior to HTTP requests and responses. In Zend Expressive, each middleware is a PHP callable that accepts a PSR-7 ServerRequestInterface instance, a PSR-7 ResponseInterface instance, and a callable that can produce the next middleware in the queue.

How can I install Zend Expressive?

You can install Zend Expressive using Composer, a tool for dependency management in PHP. You can download Composer and then run the following command in your terminal: composer require zendframework/zend-expressive. This will install Zend Expressive and its dependencies in your project.

What are the benefits of using Zend Expressive?

Zend Expressive allows for rapid application development with a focus on simplicity and flexibility. It supports any kind of application – from microservices to monolithic applications. It also supports multiple routing and templating systems, allowing developers to choose the tools that best suit their needs.

How can I create a module in Zend Expressive?

To create a module in Zend Expressive, you need to create a new directory in the src directory of your application. This directory should contain a ConfigProvider class that returns an array of configuration for the module. The configuration array should include keys for dependencies, routes, and templates.

How can I add a route in Zend Expressive?

To add a route in Zend Expressive, you need to add a new entry to the routes key in the configuration array returned by the ConfigProvider class of your module. Each entry should be an array with keys for name, path, middleware, and allowed_methods.

How can I use templates in Zend Expressive?

Zend Expressive supports multiple templating systems, including Twig, Plates, and Zend View. To use templates, you need to add a new entry to the templates key in the configuration array returned by the ConfigProvider class of your module. Each entry should be an array with keys for layout and map.

How can I handle errors in Zend Expressive?

Zend Expressive includes a default error handler middleware that catches any exceptions thrown during the execution of your application. You can customize the error handler by creating a new middleware that implements the Zend\Stratigility\Middleware\ErrorHandlerInterface.

How can I test my Zend Expressive application?

You can test your Zend Expressive application using PHPUnit, a popular testing framework for PHP. Zend Expressive includes a phpunit.xml.dist file that you can use as a starting point for your test configuration.

How can I deploy my Zend Expressive application?

You can deploy your Zend Expressive application like any other PHP application. You can use a web server like Apache or Nginx, or you can use a PHP built-in server for development purposes.

Where can I find more resources about Zend Expressive?

You can find more resources about Zend Expressive on the official Zend Framework website, the Zend Expressive documentation, and the Zend Framework community forums. You can also find many tutorials and articles online.