Skip to main content

Rapid Development of Zend Expressive Modules

By Kirk Madera

PHP

Share:

Free JavaScript Book!

Write powerful, clean and maintainable JavaScript.

RRP $11.95

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.

Learn PHP for free!

Make the leap into server-side programming with a comprehensive cover of PHP & MySQL.

Normally RRP $11.95 Yours absolutely free

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!

I am the Director of Technology at Robofirm with experience in Magento, Zend Framework, Symfony, and DevOps technologies. I work to ensure Robofirm continues to grow in its technical strengths and maintains a leading edge over the competition in order to allow delivery of higher quality work to our clients with less time and budget. I am a father of two girls and happily married. I code in my free time because it's fun to build new things. I also play guitar and listen to lots of heavy metal.

New books out now!

Learn valuable skills with a practical introduction to Python programming!


Give yourself more options and write higher quality CSS with CSS Optimization Basics.