PHP - - By Kirk Madera

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.

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!

Sponsors