Middleware architectures

in PHP with Zend Expressive

Middleware

SOA Middleware

Not today's topic

Node.JS Middleware


var express = require('express')
var app = express()

app.get('/', function (req, res) {
    res.send('Hello World!')
})

app.listen(3000)
                    

Marco

Explorer

Avid learner

Chocolate Lover

Steve

Enjoys Travelling

Software Architectures

In a few years...

Software Engineers at

How about you?

Who uses?



Dependency Injection

Value Objects

Dependency Injection

Value Objects

PSR-7

Dependency Injection

Value Objects

PSR-7

Middleware

Zend Expressive

Plans for the day...

A Nice Side effect...

Zend Expressive

  • easy to use micro-framework
  • PSR-15, PSR-11, PSR-7 compliant
  • supports middleware
  • component based

Let's Go!


cd /var/www/html/summercamp/phpmiddleware

git checkout 01-expressive-skeleton
                    

Tips & Hints:


docs/readme.html
                    

http://phpmiddleware.websc

using Postman


postman --disable-gpu
                    

Default Installation


cd /var/www/html/summercamp/

composer create-project zendframework/zend-expressive-skeleton phpmiddleware
                    

PIMPLE

FastRoute

Interoperability

Keyword for today

Component Glue

Filesystem layout

Let's open PHPStorm at:


/var/www/html/summercamp/phpmiddleware
                    

├── bin
├── config
├── data
├── docs
├── public
│     └── index.php
├── src
├── test
└── vendor
                    

Apache's configuration


/etc/apache2/sites-available/phpmiddleware.conf
                    

Document root


/var/www/html/summercamp/phpmiddleware/public
                    

public/index.php


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

call_user_func(function () {
    /** @var \Interop\Container\ContainerInterface $container */
    $container = require 'config/container.php';

    /** @var \Zend\Expressive\Application $app */
    $app = $container->get(\Zend\Expressive\Application::class);

    require 'config/pipeline.php';
    require 'config/routes.php';

    $app->run();
});

config/config.php


$aggregator = new ConfigAggregator([
    App\ConfigProvider::class,
    new PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'),
    new PhpFileProvider('config/development.config.php')
]);

return $aggregator->getMergedConfig();

config/pipeline.php


$app->pipe(ErrorHandler::class);

$app->pipeRoutingMiddleware();

$app->pipeDispatchMiddleware();

$app->pipe(NotFoundHandler::class);
                    

config/routes.php


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

config/routes.php


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

config/routes.php


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

src/App/Action/PingAction.php


namespace App\Action;

class PingAction implements ServerMiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        DelegateInterface $delegate
    ) {
        return new JsonResponse(['ack' => time()]);
    }
}
                    

PSR-7 interfaces

PSR-7 interfaces

PSR-7 MessageInterface


namespace Psr\Http\Message;

interface MessageInterface
{
    public function getProtocolVersion();
    public function withProtocolVersion($version);
    public function getHeaders();
    public function hasHeader($name);
    public function getHeader($name);
    public function getHeaderLine($name);
    public function withHeader($name, $value);
    public function withAddedHeader($name, $value);
    public function withoutHeader($name);
    public function getBody();
    public function withBody(StreamInterface $body);
}
                        

PSR-7 RequestInterface


namespace Psr\Http\Message;

interface RequestInterface extends MessageInterface
{
    public function getRequestTarget();
    public function withRequestTarget($requestTarget);
    public function getMethod();
    public function withMethod($method);
    public function getUri();
    public function withUri(UriInterface $uri, $preserveHost = false);
}
                        

PSR-7 ServerRequestInterface


namespace Psr\Http\Message;

interface ServerRequestInterface extends RequestInterface
{
    public function getServerParams();
    public function getCookieParams();
    public function withCookieParams(array $cookies);
    public function getQueryParams();
    public function withQueryParams(array $query);
    public function getUploadedFiles();
    public function withUploadedFiles(array $uploadedFiles);
    public function getParsedBody();
    public function withParsedBody($data);
    public function getAttributes();
    public function getAttribute($name, $default = null);
    public function withAttribute($name, $value);
    public function withoutAttribute($name);
}
                        

PSR-7 ResponseInterface


namespace Psr\Http\Message;

interface ResponseInterface extends MessageInterface
{
    public function getStatusCode();
    public function withStatus($code, $reasonPhrase = '');
    public function getReasonPhrase();
}
                    

Interacting with PSR-7 messages


$response->setStatusCode(418);
                    

$response = $response->withStatusCode(418);
                    

A Value object is forever

Time to do stuff...


git checkout 03-welcome
                    

Exercise 1 (10 mins)

get to branch 03-welcome

Create an hello route (/hello)

Display output {"hello":"NAME"}

NAME is either the value of the "name" query string parameter, or the string "random phper" if no name parameter was provided



docs/readme.html
                    

Exercise 1 Solution

config/routes.php


$app->get('/', \App\Action\IndexAction::class, 'index');

$app->get('/hello', \App\Action\HelloAction::class, 'hello');
                    

Exercise 1 Solution

src/App/Action/HelloAction.php


class HelloAction implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        DelegateInterface $delegate
    ): ResponseInterface
    {
        $query = $request->getQueryParams();

        $name = $query['name'] ?? 'random phper';

        return new JsonResponse([
            'hello' => $name
        ]);
    }
}
                    

Check it out with


git checkout 04-hello-name
                    

Let's move on...


git checkout 06-chocolates-route
                    

Our Domain

config/routes.php


$app->get('/', \App\Action\IndexAction::class, 'index');

$app->get('/hello', \App\Action\HelloAction::class, 'hello');

$app->get('/chocolates', \App\Action\ChocolatesAction::class, 'chocolates');
                    

src/App/Action/ChocolatesAction.php


final class ChocolatesAction implements MiddlewareInterface
{
    /**
     * @var ChocolatesServiceInterface
     */
    private $chocolates;

    public function __construct(ChocolatesServiceInterface $chocolates)
    {
        $this->chocolates = $chocolates;
    }

    public function process(
        ServerRequestInterface $request,
        DelegateInterface $delegate
    ): ResponseInterface
    {
        return new JsonResponse($this->chocolates->getAll());
    }
}
                    

config/config.php


$aggregator = new ConfigAggregator([
    App\ConfigProvider::class,
    new PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'),
    new PhpFileProvider('config/development.config.php')
]);

return $aggregator->getMergedConfig();

src/App/ConfigProvider.php


public function getDependencies()
{
    return [
        'factories'  => [
            // ACTIONS
            ChocolatesAction::class => ChocolatesActionFactory::class,

            // SERVICES
            ChocolatesServiceInterface::class => ChocolatesServiceFactory::class,

            // REPOSITORIES
            Chocolates::class => SqlChocolatesFactory::class,
        ],
    ];
}

src/App/Container/Action/ChocolatesActionFactory.php


final class ChocolatesActionFactory
{
    public function __invoke(ContainerInterface $container): ChocolatesAction
    {
        return new ChocolatesAction(
            $container->get(ChocolatesServiceInterface::class)
        );
    }
}
                    

PSR-11 ContainerInterface


interface ContainerInterface
{
    /**
     * @param string $id Identifier of the entry to look for.
     *
     * @throws NotFoundExceptionInterface No entry was found for the identifier.
     * @throws ContainerExceptionInterface Error while retrieving the entry.
     *
     * @return mixed Entry.
     */
    public function get($id);

    /**
     * @param string $id Identifier of the entry to look for.
     *
     * @return bool
     */
    public function has($id);
}
                    

Exercise 2 (25 mins)

get to branch 06-chocolates-route

Alternative A - User List

Create a chocolate-details (/chocolate/ID) route where ID is a proper ID of a chocolate wrapper in our domain

Interact with src/App/Domain/Service/ChocolatesService.php

Alternative B - Chocolate Details

Create a users (/users) route


docs/readme.html
                    

Exercise 2 Solution


final class ChocolateDetailsAction implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        DelegateInterface $delegate
    ): ResponseInterface
    {
        $chocolateId = ChocolateId::fromString($request->getAttribute('id'));

        $chocolate = $this->chocolatesService->getChocolate($chocolateId);

        return new JsonResponse($chocolate);
    }
}

You can check them out with


git checkout 07-chocolate-details-route
                    

git checkout 08-users
                    

But hey, did you notice?


class ...Action implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        DelegateInterface $delegate
    ): ResponseInterface
    {
        ...
    }
}
                    

We used Middleware


interface MiddlewareInterface
{
    /**
     * @param ServerRequestInterface $request
     * @param DelegateInterface $delegate
     *
     * @return ResponseInterface
     */
    public function process(
        ServerRequestInterface $request,
        DelegateInterface $delegate
    );
}
                    

Middleware

Middleware

Back to code...


git checkout 10-access-log
                    

config/pipeline.php


$app->pipe(ErrorHandler::class);

$app->pipe(new \Middlewares\ClientIp());
$app->pipe(\Middlewares\AccessLog::class);

$app->pipeRoutingMiddleware();

$app->pipeDispatchMiddleware();

$app->pipe(NotFoundHandler::class);
                    

config/autoload/dependencies.global.php


return [
    'dependencies' => [
        'factories'  => [
            \Middlewares\AccessLog::class =>
                \App\Container\Middleware\AccessLogFactory::class,
        ],
    ],
];
                    

src/App/Container/Middleware/AccessLogFactory.php


final class AccessLogFactory
{
    public function __invoke(ContainerInterface $container)
    {
        $logger = new Logger('access');
        $filePath = $container->get('config')['access_log']['path'];
        $logger->pushHandler(new StreamHandler($filePath));

        $accessLog = new AccessLog($logger);

        $format = '%v %h %u %t "%r" %>s %b "%{Referer}i" -> %U';
        $accessLog->format($format);
        $accessLog->ipAttribute('client-ip');

        return $accessLog;
    }
}

Exercise 3 (25 mins)

Get to branch 10-access-log

Create a caching middleware

For all http GET requests, make sure that a cached result is returned, when available

Bonus: have cache expire after a certain time period; have cache file path configuration within config files


docs/readme.html
                    

Solution to exercise 3


git checkout 11-response-cache
                    

Reinventing the Wheel

Not a Smart Idea!

Already Available Middleware

  • Storage-Less Sessions
  • Device Detection
  • Analytics Support
  • Robot-Blocking
  • Request Rate Limiting
  • And More...

Easy, isn't it?

How about other actions?


git checkout 15-delete-chocolate
                    

config/routes.php


$app->post('/submit', \App\Action\SubmitChocolateAction::class, 'submit');

$app->post(
    '/approve/{id}',
    \App\Action\ApproveChocolateAction::class,
    'approve'
);

$app->post(
    '/delete/{id}',
    \App\Action\DeleteChocolateAction::class,
    'delete'
);
                    

Related Actions


App\Action\SubmitChocolateAction.php
                    

App\Action\ApproveChocolateAction.php
                    

App\Action\DeleteChocolateAction.php
                    

What about authentication?


git checkout 16.1-basic-http-authentication
                    

config/routes.php


$app->post(
    '/submit',
    [
        \Middlewares\HttpAuthentication::class,
        \App\Action\SubmitChocolateAction::class,
    ],
    'submit'
);

$app->post(
    '/approve/{id}',
    [
        \Middlewares\HttpAuthentication::class,
        \App\Action\ApproveChocolateAction::class,
    ],
    'approve'
);

[...]

                    

src/App/Container/Middleware/BasicHttpAuthenticationFactory.php


final class BasicHttpAuthenticationFactory
{
    public function __invoke(ContainerInterface $container): HttpAuthentication
    {
        /** @var UsersServiceInterface $users */
        $users = $container->get(UsersServiceInterface::class);

        $credentials = array_reduce(
            $users->getAll(),
            function (array $carry, User $user) {
                $carry[$user->username()] = $user->password();
                return $carry;
            },
            []
        );

        return (new BasicAuthentication($credentials))
            ->attribute(HttpAuthentication::class);
    }
}

Let's switch to JWT...


git checkout 17.3-jwt-authentication
                    

config/routes.php


$app->post(
    '/submit',
    [
        \Middlewares\HttpAuthentication::class,
        \App\Action\SubmitChocolateAction::class,
    ],
    'submit'
);

$app->post(
    '/approve/{id}',
    [
        \Middlewares\HttpAuthentication::class,
        \App\Action\ApproveChocolateAction::class,
    ],
    'approve'
);
                    

JWT

config/routes.php


$app->post('/token', \App\Action\TokenAction::class, 'token');
                    

Exercise 4 (30 mins)

Alternative A - JWT Authentication

Get to branch 17.1-jwt-authentication

Implement JWT Authentication

Alternative B - Authorization

Get to branch 16.1-basic-http-authentication

Allow for authenticated users only to submit new wrappers

Allow for administrators only to approve/delete wrappers

Solution to exercise(s) 4


git checkout 17.3-jwt-authentication
                    

git checkout 18-authorization
                    

config/autoload/dependencies.global.php


return [
    'dependencies' => [
        'factories' => [
            \Middlewares\HttpAuthentication::class =>
                \App\Container\Middleware\JwtAuthenticationFactory::class,
        ]
    ]
];

config/routes.php


$app->post(
    '/submit',
    [
        \Middlewares\HttpAuthentication::class,
        \App\Action\SubmitChocolateAction::class,
    ],
    'submit'
);

$app->post(
    '/approve/{id}',
    [
        \Middlewares\HttpAuthentication::class,
        \App\Middleware\Authorization::class,
        \App\Action\ApproveChocolateAction::class,
    ],
    'approve'
);
                    

src/App/Middleware/Authorisation.php


public function process(
    ServerRequestInterface $request,
    DelegateInterface $delegate
): ResponseInterface
{
    $username = $request->getAttribute(JwtAuthentication::class)->data->username;

    $user = $this->usersService->getByUsername($username);

    if (! $user->isAdministrator()) {
        return new EmptyResponse(403);
    }

    return $delegate->process($request);
}
                    

What About Testing

Exercise 5 (bonus)

Get to branch 19-phpunit or 20-behat

let's write a test for the Authorization Middleware of the last exercise


docs/readme.html
                    

Solution to exercise 5


git checkout 19.1-phpunit
                    

git checkout 20.1-behat
                    

Takeaways

Middleware

What about other frameworks?

Slim

Slim


$app->add(new AccessLog($logger));

$app->add(new ClientIp());

$app->add(new BasicAuthentication($users));

Other middleware dispatchers

Middleman


$dispatcher = new Dispatcher([
    new BasicAuthentication($users),
    new ClientIp(),
    new AccessLog($logger)
]);
                    

Told'ya!

Thank you very much

Stay tuned for more...

Speakers love feedback

Leave your feedback at https://joind.in/talk/668cc

Marco @marcoshuttle m.perone@mvlabs.it

Steve @maraspin s.maraspin@mvlabs.it

Resources

Credits

Feng Shui at pixabay.com

SOA Middleware by David McCaldin

Board by ericfleming8

Chocolate at pixabay.com

Traveling at pixabay.com

Architecture at pixabay.com

Orioles Fan by Keith Allison

Programmer at pixabay.com

Sleepy Dog at pixabay.com

Small Board at pixabay.com

Lake Bump at pixabay.com

Networking Equipment at pixabay.com

Chefs at pixabay.com

Parts on Table at pixabay.com

PSR-7 Diagram by Matthew Weier O'Phinney

Diamond by EWAR

Chocolate Wrappers by A. Olin

Cutting onion by Lali Masriera

Middleware Flow by Matthew Weier O'Phinney

Weird Bicicle by Thomas Guest

Dinosaur at pixabay.com

Deer at Self Service from pixabay.com

JWT Flow by Mikey Stecky-Efantis

Car Crash at pixabay.com

Cow with a Prize at pixabay.com

Closing Curtains at pixabay.com