Building middleware applications with Zend Framework 3

by Enrico Zimuel / @ezimuel
Senior Software Engineer
Zend Technologies, a Rogue Wave Company


Oct 18, ZendCon 2016, Las Vegas

About me

HTTP is the foundation
of the web

  • A client sends a request
  • A server returns a response

HTTP messages

Request


GET /path HTTP/1.1
Host: example.com
Accept: application/json
				            

Response


HTTP/1.1 200 OK
Content-Type: application/json

{"foo":"bar"}
				            

Frameworks model messages

But every framework does it differently.


$method = $request->getMethod();
$method = $request->getRequestMethod();
$method = $request->method;
				          

PSR-7

Shared HTTP Message Interfaces

Request


$method     = $request->getMethod();
$accept     = $request->getHeader('Accept');
$path       = $request->getUri()->getPath();
$controller = $request->getAttribute('controller');
				          

Response


$response->getBody()->write('Hello world!');
$response = $response->withStatus(200, 'OK')
            ->withHeader('Content-Type', 'text/plain');
				          

Middleware

A function that gets a request and generates a response


use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;

function (Request $request, Response $response) {
  // manipulate $request to generate a $response
  return $response;
}

Web application

Middleware app

Middleware in zend-expressive

Use an additional callable during the invoke ($next)


class Middleware
{
  public function __invoke(
    \Psr\Http\Message\ServerRequestInterface $request,
    \Psr\Http\Message\ResponseInterface $response,
    callable $next = null
  ) {
    // do something before
    if ($next) {
        $next($request, $response);
    }
    // do something after
    return $response;
  }
}

Zend-expressive

  • PSR-7 support (using zend-diactoros)
  • Middleware using a callable ($next):
    
    function ($request, $response, $next)
    
  • Piping workflow (using zend-stratigility)
  • Features: routing, container-interop, templating, error handling
  • Stable version 1.0 (28 Jan 2016)

Components layer

Flow overview

Basic example


use Zend\Expressive\AppFactory;

chdir(dirname(__DIR__));
require 'vendor/autoload.php';

$app = AppFactory::create();
$app->get('/', function ($request, $response, $next) {
  $response->getBody()->write('Hello, world!');
  return $response;
});

$app->pipeRoutingMiddleware();
$app->pipeDispatchMiddleware();
$app->run();

Routing


namespace Zend\Expressive\Router;
use Psr\Http\Message\ServerRequestInterface as Request;

interface RouterInterface
{
  public function addRoute(Route $route);
  public function match(Request $request);
  public function generateUri($name, array $substitutions = []);
}

Routing example


// $app is an instance of Zend\Expressive\AppFactory

$app->get('/', function ($request, $response, $next) {
  $response->getBody()->write('Hello, world!');
  return $response;
});

Piping Middleware


// $app is an instance of Zend\Expressive\AppFactory

// Executed in all the requests
$app->pipe($apiMiddleware);
$app->pipe('middleware service name');

// Pipe to a specific URL
$app->pipe('/api', $apiMiddleware);
$app->pipe('/api', 'middleware service name');

// Error handler
$app->pipeErrorHandler('error handler service name');
$app->pipeErrorHandler('/api', 'error handler service name');

Using a Service Container


use Zend\Expressive\AppFactory;
use Zend\ServiceManager\ServiceManager;

$container = new ServiceManager();
$container->setFactory('HelloWorld', function ($container) {
  return function ($request, $response, $next) {
    $response->write('Hello, world!');
    return $response;
  };
});

$app = AppFactory::create($container);
$app->get('/', 'HelloWorld');

We support container-interop

Templating

  • While Expressive does not assume templating is being used, it provides a templating abstraction.
  • Default adapters: Plates, Twig, Zend-View

namespace Zend\Expressive\Template;

interface TemplateRendererInterface
{
  public function render($name, $params = []);
  public function addPath($path, $namespace = null);
  public function getPaths();
  public function addDefaultParam($templateName, $param, $value);
}

Error Handling

  • Expressive provides error handling out of the box, via zend-stratigility's FinalHandler
  • This pseudo-middleware is executed in the following conditions:
    • If the middleware stack is exhausted, and no middleware has returned a response
    • If an error has been passed via $next(), but not handled by any error middleware

Templated Errors


use Zend\Expressive\Application;
use Zend\Expressive\Plates\PlatesRenderer;
use Zend\Expressive\TemplatedErrorHandler as TErrHandler;

$plates = new PlatesRenderer();
$plates->addPath(__DIR__ . '/templates/error', 'error');
$final = new TErrHandler($plates, 'error::404', 'error::500');

$app = new Application($router, $container, $final);

Using Whoops

Hands-on

Setup

Use Vagrant to set up the environment.

Clone the repository ezimuel/zend-expressive-workshop


git clone https://github.com/ezimuel/zend-expressive-workshop

Run composer:


cd zend-expressive-workshop
composer install

Turn on Vagrant:


vagrant up

The PHP server runs at localhost:8080

Application tree

├── config
│   └── autoload
├── data
│   └── cache
├── public
├── src
│   └── App
│       └── Action
├── templates
│   ├── app
│   ├── error
│   └── layout
└── test

Config

The application configuration files, including:

  • the registered services (middleware, db, etc);
  • the template file paths;
  • the error handler;

Front Controller

public/index.php (programmatic version)


$container = require 'config/container.php';
$app = $container->get(Application::class);

// Pipeline
$app->pipe(Helper\ServerUrlMiddleware::class);
// here add custom middleware ...
$app->pipeRoutingMiddleware();
$app->pipe(Helper\UrlHelperMiddleware::class);
$app->pipeDispatchMiddleware();

// Routes
$app->get('/', Action\HomePageAction::class, 'home');
$app->get('/api/ping', Action\PingAction::class, 'api.ping');
$app->run();

Middleware Factory

src/App/Action/HomePageFactory.php


class HomePageFactory
{
  public function __invoke(ContainerInterface $container)
  {
    $router   = $container->get(RouterInterface::class);
    $template = ($container->has(TemplateRendererInterface::class))
      ? $container->get(TemplateRendererInterface::class)
      : null;

    return new HomePageAction($router, $template);
  }
}

Middleware Action

src/App/Action/HomePageAction.php


class HomePageAction
{
  private $router;
  private $template;

  public function __construct(
    Router\RouterInterface $router,
    Template\TemplateRendererInterface $template = null
  ){
    $this->router   = $router;
    $this->template = $template;
  }

  public function __invoke(
    ServerRequestInterface $request,
    ResponseInterface $response,
    callable $next = null
  ){
    $data = [];
    // insert value into $data
    return new HtmlResponse($this->template->render(
      'app::home-page',
      $data
    ));
  }
}

Build a conference website

  • Publish 3 web pages: speakers, talks, and schedule
  • The data are stored in data/db.sqlite3
  • We want to have the following URLs:
    • /speaker[/{id:}]
    • /talk[/{id:}]
    • /schedule

Exercise 1

  • Add routes for speakers, talks, and schedule
  • Add links to navbar in templates\layout\default.phtml

Solution 1

You can check out a solution in exercise1 branch:


git checkout exercise1

Model Layer

We use a Model layer to interact with the database

We built a Speaker and Talk classes in src/App/Model

Exercise 2

  • Build the Talk Action and Factory
  • Build the pages to resolve /talk and talk[/{id:\d+}] URLs
  • Note: have a look at the SpeakerAction and templates

Solution 2

You can check out a solution in exercise2 branch:


git checkout exercise2

Exercise 3

  • Build the Schedule page

Solution 3

You can check out a solution in exercise3 branch:


	git checkout exercise3
	

Thanks!

Please rate this workshop at:
https://legacy.joind.in/19407