@asgrim
Kicking off with Zend Expressiveand Doctrine ORM
James Titcumbphp[MiNDS] March 2018
Follow along (optional): https://github.com/asgrim/book-library
$ whoami
James Titcumb
www.jamestitcumb.com
www.roave.com
@asgrim
@asgrim
What is Zend Expressive?
@asgrim
Layers of an Expressive application
Expressive
Stratigility
Diactoros
PSR-7 Interface
DIRouter Template
Your Application
@asgrim
Layers of an Expressive application
Expressive
Stratigility
Diactoros
PSR-7 Interface
DIRouter Template
Your Application
@asgrim
PSR-7HTTP Message Interfaces
@asgrim
HTTP Request
POST /talk HTTP/1.1
Host: phpminds.org
foo=bar&baz=bat
@asgrim
HTTP Response
HTTP/1.1 200 OK
Content-Type: text/plain
This is the response body
@asgrim
Layers of an Expressive application
Expressive
Stratigility
Diactoros
PSR-7 Interface
DIRouter Template
Your Application
@asgrim
Zend DiactorosPSR-7 implementation
@asgrim
Node http.Server using Diactoros
$server = \Zend\Diactoros\Server::createServer(
function ($request, $response, $done) {
return $response->getBody()->write('hello world');
},
$_SERVER,
$_GET,
$_POST,
$_COOKIE,
$_FILES
);
$server->listen();
@asgrim
Layers of an Expressive application
Expressive
Stratigility
Diactoros
PSR-7 Interface
DIRouter Template
Your Application
@asgrim
Zend StratigilityCreating & dispatching middleware pipelines
@asgrim
So what is “middleware”?
@asgrim
Middleware in the middle
function(Request $request, RequestHandler $handler) : Response
{
// ... some code before ...
$response = $handler->handle($request);
// ... some code after ...
return $response;
}
@asgrim
Some people call it an onion
SessionInitialisingMiddleware
AuthenticationMiddleware
ThingyMiddleware
DashboardHandler
@asgrim
Some people call it an onion
SessionInitialisingMiddleware
AuthenticationMiddleware
ThingyMiddleware
DashboardHandler
@asgrim
Some people call it an onion
SessionInitialisingMiddleware
AuthenticationMiddleware
ThingyMiddleware
DashboardHandler
@asgrim
psr/http-server-middleware
@asgrim
Middleware using MiddlewareInterface
<?php
declare(strict_types=1);
namespace App\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class MyMiddleware implements MiddlewareInterface
{
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $requestHandler
) : ResponseInterface {
return $requestHandler>handle($request);
}
}
@asgrim
Double-pass middleware
public function __invoke(
Request $request,
Response $response,
callable $next
): Response {
return $next($request, $response);
}
!!! DEPRECATED !!!
@asgrim
Modifying the response
function(Request $request, RequestHandler $handler) : Response
{
$response = $handler->handle($request);
return $response->withHeader(
'X-Clacks-Overhead',
'GNU Terry Pratchett'
);
}
@asgrim
Pipe all the things!
$pipe = new \Zend\Stratigility\MiddlewarePipe();
$pipe->pipe($logUncaughtErrorsMiddleware);
$pipe->pipe($sessionInitialisingMiddleware);
$pipe->pipe($authenticationMiddleware);
$pipe->pipe('/', $indexHandlerMiddleware);
$pipe->pipe(new NotFoundHandler(...));
@asgrim
LogErrorsCaughtMiddleware
function(Request $request, RequestHandler $handler) : Response
{
try {
return $handler->handle($request);
} catch (\Throwable $throwable) {
// Log the error, handle it with error page etc.
return new JsonResponse(
[
'message' => $throwable->getMessage(),
],
500
);
}
}
@asgrim
SessionInitialisingMiddleware
function(Request $request, RequestHandler $handler) : Response
{
session_start();
return $handler->handle($request);
}
@asgrim
function(Request $request, RequestHandler $handler) : Response
{
if (!$this->doSomeOAuthCheck($request)) {
return new JsonResponse(
[
'message' => 'Invalid OAuth token',
],
403
);
}
return $handler->handle($request);
}
AuthenticationMiddleware
@asgrim
IndexHandlerMiddleware
function(Request $request, RequestHandler $handler) : Response
{
// ... some code ...
return new JsonResponse($someData, 200);
}
@asgrim
Layers of an Expressive application
Expressive
Stratigility
Diactoros
PSR-7 Interface
DIRouter Template
Your Application
@asgrim
Zend ExpressiveOne Ring to bring them all and in the darkness bind them.
@asgrim
Routing
@asgrim
container-interop
@asgrim
Optionally, templating
@asgrim
Piping and Routing
@asgrim
Zend Framework 2/3What of them?
@asgrim
Middleware vs MVC
@asgrim
Using ZF componentsin Expressive
@asgrim
ZF’s Module.php
class Module
{
public function getConfig()
{
return include __DIR__ . '/../config/module.config.php';
}
}
@asgrim
ConfigProvider
@asgrim
ConfigProvider
namespace Zend\Form;
class ConfigProvider
{
public function __invoke()
{
return [
'dependencies' => $this->getDependencyConfig(),
'view_helpers' => $this->getViewHelperConfig(),
];
}
}
@asgrim
ConfigProvider#getDependencyConfig()
public function getDependencyConfig()
{
return [
'abstract_factories' => [
FormAbstractServiceFactory::class,
],
'aliases' => [
'Zend\Form\Annotation\FormAnnotationBuilder' => 'FormAnnotationBuilder',
Annotation\AnnotationBuilder::class => 'FormAnnotationBuilder',
FormElementManager::class => 'FormElementManager',
],
'factories' => [
'FormAnnotationBuilder' => Annotation\AnnotationBuilderFactory::class,
'FormElementManager' => FormElementManagerFactory::class,
],
@asgrim
Zend\Form’s Module.php (for Zend Framework)
class Module
{
public function getConfig()
{
$provider = new ConfigProvider();
return [
'service_manager' => $provider->getDependencyConfig(),
'view_helpers' => $provider->getViewHelperConfig(),
];
}
}
@asgrim
config/config.php
<?php
$aggregator = new ConfigAggregator([
\Zend\Form\ConfigProvider::class,
// .. other config ...
], $cacheConfig['config_cache_path']);
return $aggregator->getMergedConfig();
@asgrim
ConfigAggregator
@asgrim
config/config.php
$aggregator = new ConfigAggregator([
\Zend\Expressive\Router\FastRouteRouter\ConfigProvider::class,
\Zend\HttpHandlerRunner\ConfigProvider::class,
new ArrayProvider($cacheConfig),
\Zend\Expressive\Helper\ConfigProvider::class,
\Zend\Expressive\ConfigProvider::class,
\Zend\Expressive\Router\ConfigProvider::class,
\App\ConfigProvider::class,
new PhpFileProvider(realpath(__DIR__) . '/autoload/{{,*.}global,{,*.}local}.php'),
new PhpFileProvider(realpath(__DIR__) . '/development.config.php'),
], $cacheConfig['config_cache_path']);
@asgrim
Layers of an Expressive application
Expressive
Stratigility
Diactoros
PSR-7 Interface
DIRouter Template
Your Application
@asgrim
Getting startedwith Zend Expressive
@asgrim
https://github.com/asgrim/book-library
@asgrim
Expressive Skeleton
@asgrim
Expressive installer - start
composer create-project zendframework/zend-expressive-skeleton:3.0.0-rc1 book-library
Installing zendframework/zend-expressive-skeleton (3.0.0rc1)
- Installing zendframework/zend-expressive-skeleton (3.0.0rc1): Downloading (100%)
Created project in test
> ExpressiveInstaller\OptionalPackages::install
Setting up optional packages
Setup data and cache dir
Removing installer development dependencie
@asgrim
Expressive installer - structure
What type of installation would you like?
[1] Minimal (no default middleware, templates, or assets; configuration only)
[2] Flat (flat source code structure; default selection)
[3] Modular (modular source code structure; recommended)
Make your selection (2): 2
- Copying src/App/ConfigProvider.php
@asgrim
Modular structurezend-config-aggregator
@asgrim
Modular structurezend-config-aggregator
@asgrim
Flat or Modular?
@asgrim
Default modular structure
src/ MyModule/ src/ ConfigProvider.php Handler/ Entity/ Middleware/ templates/ test/
@asgrim
Expressive installer - container?
Which container do you want to use for dependency injection?
[1] Aura.Di
[2] Pimple
[3] zend-servicemanager
[4] Auryn
[5] Symfony DI Container
Make your selection or type a composer package name and version (zend-servicemanager):
- Adding package zendframework/zend-servicemanager (^3.3)
- Copying config/container.php
@asgrim
Expressive installer - router?
Which router do you want to use?
[1] Aura.Router
[2] FastRoute
[3] zend-router
Make your selection or type a composer package name and version (FastRoute):
- Adding package zendframework/zend-expressive-fastroute (^3.0.0alpha1)
- Whitelist package zendframework/zend-expressive-fastroute
- Copying config/routes.php
@asgrim
Expressive installer - template?
Which template engine do you want to use?
[1] Plates
[2] Twig
[3] zend-view installs zend-servicemanager
[n] None of the above
Make your selection or type a composer package name and version (n):
@asgrim
Expressive installer - whoops?
Which error handler do you want to use during development?
[1] Whoops
[n] None of the above
Make your selection or type a composer package name and version (Whoops): n
@asgrim
Expressive installer - run it!
$ composer serve
> php -S 0.0.0.0:8080 -t public/ public/index.php
[Thu Sep 1 20:29:33 2016] 127.0.0.1:48670 [200]: /favicon.ico
{ "welcome": "Congratulations! You have installed the zend-expressive skeleton application.", "docsUrl": "https://docs.zendframework.com/zend-expressive/"}
@asgrim
Create the endpoints
@asgrim
Book entity
class Book
{
/**
* @var string
*/
private $id;
/**
* @var bool
*/
private $inStock = true;
public function __construct()
{
$this->id = (string)Uuid::uuid4();
}
@asgrim
Book entity
class Book
{
/**
* @return void
* @throws \App\Entity\Exception\BookNotAvailable
*/
public function checkOut()
{
if (!$this->inStock) {
throw Exception\BookNotAvailable::fromBook($this);
}
$this->inStock = false;
}
@asgrim
Book entity
class Book
{
/**
* @return void
* @throws \App\Entity\Exception\BookAlreadyStocked
*/
public function checkIn()
{
if ($this->inStock) {
throw Exception\BookAlreadyStocked::fromBook($this);
}
$this->inStock = true;
}
@asgrim
FindBookByUuidInterface
interface FindBookByUuidInterface
{
/**
* @param UuidInterface $slug
* @return Book
* @throws Exception\BookNotFound
*/
public function __invoke(UuidInterface $slug): Book;
}
@asgrim
CheckOutHandler
public function process(ServerRequestInterface $request, RequestHandler $handler): JsonResponse{ try { $book = $this->findBookByUuid->__invoke(Uuid::fromString($request->getAttribute('id'))); } catch (BookNotFound $bookNotFound) { return new JsonResponse(['info' => $bookNotFound->getMessage()], 404); }
try { $book->checkOut(); } catch (BookNotAvailable $bookNotAvailable) { return new JsonResponse(['info' => $bookNotAvailable->getMessage()], 423); }
return new JsonResponse([ 'info' => sprintf('You have checked out %s', $book->getId()), ]);}
@asgrim
return function (
Application $app,
MiddlewareFactory $factory,
ContainerInterface $container
): void {
$app->pipe(\Zend\Stratigility\Middleware\ErrorHandler::class);
$app->pipe(\Zend\Expressive\Router\Middleware\PathBasedRoutingMiddleware::class); // Routing
$app->pipe(\Zend\Expressive\Router\Middleware\DispatchMiddleware::class); // Dispatch
$app->pipe(\Zend\Expressive\Handler\NotFoundHandler::class);
};
Define application pipeline - pipeline.php
@asgrim
return function (
Application $app,
MiddlewareFactory $factory,
ContainerInterface $container
): void {
$app->get(
'/book/{id}/check-out',
\App\Handler\CheckOutHandler::class,
'check-out'
);
$app->get(
'/book/{id}/check-in',
\App\Handler\CheckInHandler::class,
'check-in'
);
};
Define routes - routes.php
@asgrim
Adding some ORM
@asgrim
Your application
Doctrine quick overview
DBDBAL ORM
(EntityManager)Entities
Finders,Services,
...
@asgrim
container-interop-doctrine
@asgrim
Installation
$ composer require dasprid/container-interop-doctrine
Using version ^1.1 for dasprid/container-interop-doctrine
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 9 installs, 0 updates, 0 removals
- Installing doctrine/lexer (v1.0.1): Loading from cache
- Installing doctrine/inflector (v1.3.0): Loading from cache
- Installing doctrine/collections (v1.5.0): Loading from cache
- Installing doctrine/cache (v1.7.1): Loading from cache
- Installing doctrine/annotations (v1.6.0): Loading from cache
- Installing doctrine/common (v2.8.1): Loading from cache
- Installing doctrine/dbal (v2.6.3): Loading from cache
- Installing doctrine/orm (v2.6.1): Loading from cache
- Installing dasprid/container-interop-doctrine (1.1.0): Loading from cache
Writing lock file
Generating autoload files
@asgrim
src/App/ConfigProvider.php
use Doctrine\ORM\EntityManagerInterface;
use ContainerInteropDoctrine\EntityManagerFactory;
final class ConfigProvider
{
// ...
public function getDependencies() : array
{
return [
'factories' => [
EntityManagerInterface::class => EntityManagerFactory::class,
// ... other services
],
];
@asgrim
'doctrine' => [
'connection' => [
'orm_default' => [
'driver_class' => PDOPgSql\Driver::class,
'params' => [
'url' => 'postgres://user:pass@localhost/book_library',
],
],
],
'driver' => [
'orm_default' => [
'class' => MappingDriverChain::class,
'drivers' => [
// ... and so on ...
config/autoload/doctrine.global.php
@asgrim
'doctrine' => [
'connection' => [
'orm_default' => [
'driver_class' => PDOPgSql\Driver::class,
'params' => [
'url' => 'postgres://user:pass@localhost/book_library',
],
],
],
'driver' => [
'orm_default' => [
'class' => MappingDriverChain::class,
'drivers' => [
// ... and so on ...
config/autoload/doctrine.global.php
@asgrim
<?php
declare(strict_types = 1);
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper;
use Symfony\Component\Console\Helper\HelperSet;
$container = require __DIR__ . '/container.php';
return new HelperSet([
'em' => new EntityManagerHelper(
$container->get(EntityManagerInterface::class)
),
]);
config/cli-config.php
@asgrim
Annotating the Entities
@asgrim
/**
* @ORM\Entity
* @ORM\Table(name="book")
*/
class Book
{
/**
* @ORM\Id
* @ORM\Column(name="id", type="guid")
* @ORM\GeneratedValue(strategy="NONE")
* @var string
*/
private $id;
src/App/Entity/Book.php
@asgrim
/**
* @ORM\Entity
* @ORM\Table(name="book")
*/
class Book
{
/**
* @ORM\Id
* @ORM\Column(name="id", type="guid")
* @ORM\GeneratedValue(strategy="NONE")
* @var string
*/
private $id;
src/App/Entity/Book.php
@asgrim
public function __invoke(UuidInterface $id): Book
{
/** @var Book|null $book */
$book = $this->repository->find((string)$id);
if (null === $book) {
throw Exception\BookNotFound::fromUuid($id);
}
return $book;
}
src/App/Service/Book/DoctrineFindBookByUuid.php
@asgrim
try {
$this->entityManager->transactional(function () use ($book) {
$book->checkOut();
});
} catch (BookNotAvailable $bookNotAvailable) {
return new JsonResponse(['info' => $bookNotAvailable->getMessage()], 423);
}
src/App/Handler/CheckOutHandler.php
@asgrim
Generate the schema
$ vendor/bin/doctrine orm:schema-tool:create
ATTENTION: This operation should not be executed in a production
environment.
Creating database schema...
Database schema created successfully!
$
@asgrim
Insert some data
INSERT INTO book (id, name, in_stock) VALUES (
'1c06bec9-adae-47c2-b411-73b1db850e6f',
'The Great Escape',
true
);
@asgrim
/book/1c06bec9-adae-47c2-b411-.../check-out
{"info":"You have checked out The Great Escape"}
@asgrim
/book/1c06bec9-adae-47c2-b411-.../check-in
{"info":"You have checked in The Great Escape"}
@asgrim
Automatic Flushing Middleware
@asgrim
FlushingMiddleware
public function process(Request $request, RequestHandler $handler)
{
$response = $handler->handle($request);
if ($this->entityManager->isOpen()) {
$this->entityManager->flush();
}
return $response;
}
@asgrim
FlushingMiddleware
public function process(Request $request, RequestHandler $handler)
{
$response = $handler->handle($request);
if ($this->entityManager->isOpen()) {
$this->entityManager->flush();
}
return $response;
}
@asgrim
FlushingMiddleware
public function process(Request $request, RequestHandler $handler)
{
$response = $handler->handle($request);
if ($this->entityManager->isOpen()) {
$this->entityManager->flush();
}
return $response;
}
@asgrim
Add to pipeline
return function (
Application $app,
MiddlewareFactory $factory,
ContainerInterface $container
): void {
$app->pipe(\Zend\Stratigility\Middleware\ErrorHandler::class);
$app->pipe(\Zend\Expressive\Router\Middleware\PathBasedRoutingMiddleware::class); // Routing
$app->pipe(\App\Middleware\FlushingMiddleware::class);
$app->pipe(\Zend\Expressive\Router\Middleware\DispatchMiddleware::class); // Dispatch
$app->pipe(\Zend\Expressive\Handler\NotFoundHandler::class);
};
@asgrim
Doing more with middleware
@asgrim
Authentication
@asgrim
public function process(
ServerRequestInterface $request,
RequestHandler $handler
) : Response {
$queryParams = $request->getQueryParams();
// DO NOT DO THIS IN REAL LIFE
// It's really not secure ;)
if (!array_key_exists('authenticated', $queryParams)
|| $queryParams['authenticated'] !== '1') {
return new JsonResponse(['error' => 'You are not authenticated.'], 403);
}
return $handler->handle($request);
}
Create the middleware
@asgrim
public function process(
ServerRequestInterface $request,
RequestHandler $handler
) : Response {
$queryParams = $request->getQueryParams();
// DO NOT DO THIS IN REAL LIFE
// It's really not secure ;)
if (!array_key_exists('authenticated', $queryParams)
|| $queryParams['authenticated'] !== '1') {
return new JsonResponse(['error' => 'You are not authenticated.'], 403);
}
return $handler->handle($request);
}
Create the middleware
@asgrim
public function process(
ServerRequestInterface $request,
RequestHandler $handler
) : Response {
$queryParams = $request->getQueryParams();
// DO NOT DO THIS IN REAL LIFE
// It's really not secure ;)
if (!array_key_exists('authenticated', $queryParams)
|| $queryParams['authenticated'] !== '1') {
return new JsonResponse(['error' => 'You are not authenticated.'], 403);
}
return $handler->handle($request);
}
Create the middleware
@asgrim
public function process(
ServerRequestInterface $request,
RequestHandler $handler
) : Response {
$queryParams = $request->getQueryParams();
// DO NOT DO THIS IN REAL LIFE
// It's really not secure ;)
if (!array_key_exists('authenticated', $queryParams)
|| $queryParams['authenticated'] !== '1') {
return new JsonResponse(['error' => 'You are not authenticated.'], 403);
}
return $handler->handle($request);
}
Create the middleware
@asgrim
return function (
Application $app,
MiddlewareFactory $factory,
ContainerInterface $container
): void {
$app->pipe(\Zend\Stratigility\Middleware\ErrorHandler::class);
$app->pipe(\Zend\Expressive\Router\Middleware\PathBasedRoutingMiddleware::class); // Routing
$app->pipe(\App\Middleware\FlushingMiddleware::class);
$app->pipe(\App\Middleware\AuthenticationMiddleware::class);
$app->pipe(\Zend\Expressive\Router\Middleware\DispatchMiddleware::class); // Dispatch
$app->pipe(\Zend\Expressive\Handler\NotFoundHandler::class);
};
Add middleware to pipe
@asgrim
Helioscomposer require dasprid/helios
@asgrim
PSR7-Sessioncomposer require psr7-sessions/storageless
@asgrim
public function __invoke(ContainerInterface $container, $_, array $_ = null)
{
$symmetricKey = 'do-not-store-this-key-in-git-store-it-in-configuration-instead-please';
$expirationTime = 1200; // 20 minutes
return new SessionMiddleware(
new Signer\Hmac\Sha256(),
$symmetricKey,
$symmetricKey,
SetCookie::create(SessionMiddleware::DEFAULT_COOKIE)
->withSecure(false) // false on purpose, unless you have https locally
->withHttpOnly(true)
->withPath('/'),
new Parser(),
$expirationTime,
new SystemClock()
);
}
Factory the middleware
@asgrim
return function (
Application $app,
MiddlewareFactory $factory,
ContainerInterface $container
): void {
$app->pipe(\Zend\Stratigility\Middleware\ErrorHandler::class);
$app->pipe(\Zend\Expressive\Router\Middleware\PathBasedRoutingMiddleware::class); // Routing
$app->pipe(\App\Middleware\FlushingMiddleware::class);
$app->pipe(\PSR7Sessions\Storageless\Http\SessionMiddleware::class);
$app->pipe(\App\Middleware\AuthenticationMiddleware::class);
$app->pipe(\Zend\Expressive\Router\Middleware\DispatchMiddleware::class); // Dispatch
$app->pipe(\Zend\Expressive\Handler\NotFoundHandler::class);
};
Add middleware to pipe
@asgrim
$session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE);
$session->set('counter', $session->get('counter', 0) + 1);
Session is stored in Request
@asgrim
$skills++;
@asgrim
To summarise...
● PSR-7 & PSR-15 lay the foundations!● Diactoros is just a PSR-7 implementation● Stratigility is a middleware pipeline: the main bit● Expressive is a glue for everything● ConfigProvider is great for aggregating (merging) config● container-interop-doctrine makes Doctrine work easier● Middleware all the things!● Similar concepts in other frameworks (e.g. Slim)
@asgrim
Useful references
● https://github.com/asgrim/book-library● https://framework.zend.com/blog/2017-12-14-expressive-3-dev.html● https://framework.zend.com/blog/2017-03-13-expressive-2-migration.html● https://framework.zend.com/blog/2017-03-30-expressive-config-routes.html● https://mwop.net/blog/2016-05-16-programmatic-expressive.html● https://docs.zendframework.com/zend-expressive/
Any questions?
James Titcumb@asgrim