kicking off with zend expressive and doctrine orm (sunshine php 2017)

94
@asgrim Kicking off with Zend Expressive and Doctrine ORM James Titcumb SunshinePHP 2017

Upload: james-titcumb

Post on 09-Feb-2017

79 views

Category:

Technology


0 download

TRANSCRIPT

@asgrim

Kicking off with Zend Expressiveand Doctrine ORM

James TitcumbSunshinePHP 2017

Who is this guy?James Titcumbwww.jamestitcumb.com

www.roave.com

www.phphants.co.uk

www.phpsouthcoast.co.uk

@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 /zendcon2016/foo HTTP/1.1

Host: www.zendcon.com

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 middlefunction(Request $request, DelegateInterface $delegate) : Response

{

// ... some code before ...

$response = $delegate->process($request);

// ... some code after ...

return $response;

}

@asgrim

http-interop/http-middleware

@asgrim

Modifying the responsefunction(Request $request, DelegateInterface $delegate) : Response

{

$response = $delegate->process($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('/', $indexActionMiddleware);

$pipe->pipe(new NotFoundHandler(...));

@asgrim

LogErrorsCaughtMiddlewarefunction(Request $request, DelegateInterface $delegate) : Response

{

try {

return $delegate->process($request);

} catch (\Throwable $throwable) {

// Log the error, handle it with error page etc.

return new JsonResponse(

[

'message' => $throwable->getMessage(),

],

500

);

}

}

@asgrim

SessionInitialisingMiddlewarefunction(Request $request, DelegateInterface $delegate) : Response

{

session_start();

return $delegate->process($request);

}

@asgrim

function(Request $request, DelegateInterface $delegate) : Response

{

if (!$this->doSomeOAuthCheck($request)) {

return new JsonResponse(

[

'message' => 'Invalid OAuth token',

],

403

);

}

return $delegate->process($request);

}

AuthenticationMiddleware

@asgrim

IndexActionMiddlewarefunction(Request $request, DelegateInterface $delegate) : 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

Module.phpclass Module

{

public function getConfig()

{

return include __DIR__ . '/../config/module.config.php';

}

}

@asgrim

ConfigProvider

@asgrim

ConfigProvidernamespace 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.phpclass Module

{

public function getConfig()

{

$provider = new ConfigProvider();

return [

'service_manager' => $provider->getDependencyConfig(),

'view_helpers' => $provider->getViewHelperConfig(),

];

}

}

@asgrim

config/autoload/zend-form.global.php

<?php

return (new \Zend\Form\ConfigProvider())->__invoke();

@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 book-library

Installing zendframework/zend-expressive-skeleton (1.0.3)

- Installing zendframework/zend-expressive-skeleton (1.0.3)

Downloading: 100%

Created project in book-library

> ExpressiveInstaller\OptionalPackages::install

Setup data and cache dir

Setting up optional packages

@asgrim

Expressive installer - minimal? Minimal skeleton? (no default middleware, templates, or assets; configuration only)

[y] Yes (minimal)

[n] No (full; recommended)

Make your selection (No): n

@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): 2

@asgrim

Expressive installer - container? Which container do you want to use for dependency injection?

[1] Aura.Di

[2] Pimple

[3] Zend ServiceManager

Make your selection or type a composer package name and version (Zend ServiceManager): 3

@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): 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": "zend-expressive.readthedocs.org"}

@asgrim

Create the endpoints

@asgrim

Book entityclass Book

{

/**

* @var string

*/

private $id;

/**

* @var bool

*/

private $inStock = true;

public function __construct()

{

$this->id = (string)Uuid::uuid4();

}

@asgrim

Book entityclass Book

{

/**

* @return void

* @throws \App\Entity\Exception\BookNotAvailable

*/

public function checkOut()

{

if (!$this->inStock) {

throw Exception\BookNotAvailable::fromBook($this);

}

$this->inStock = false;

}

@asgrim

Book entityclass Book

{

/**

* @return void

* @throws \App\Entity\Exception\BookAlreadyStocked

*/

public function checkIn()

{

if ($this->inStock) {

throw Exception\BookAlreadyStocked::fromBook($this);

}

$this->inStock = true;

}

@asgrim

FindBookByUuidInterfaceinterface FindBookByUuidInterface

{

/**

* @param UuidInterface $slug

* @return Book

* @throws Exception\BookNotFound

*/

public function __invoke(UuidInterface $slug): Book;

}

@asgrim

CheckOutActionpublic function process(ServerRequestInterface $request, DelegateInterface $delegate): 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

Adding some ORM

@asgrim

Your application

Doctrine quick overview

DB DBAL ORM(EntityManager) Entities

Finders,Services,

...

@asgrim

DoctrineORMModule

@asgrim

DoctrineORMModule + Expressive?

@asgrim

config/autoload/doctrine-modules.global.php$vendorPath = __DIR__ . '/../../vendor';

$doctrineModuleConfig = require_once $vendorPath . '/doctrine/doctrine-module/config/module.config.php';

$doctrineModuleConfig['dependencies'] = $doctrineModuleConfig['service_manager'];

unset($doctrineModuleConfig['service_manager']);

$ormModuleConfig = require_once $vendorPath . '/doctrine/doctrine-orm-module/config/module.config.php';

$ormModuleConfig['dependencies'] = $ormModuleConfig['service_manager'];

unset($ormModuleConfig['service_manager']);

return ArrayUtils::merge($doctrineModuleConfig, $ormModuleConfig);

@asgrim

But wait!

@asgrim

container-interop-doctrinesaves the day!!!

@asgrim

Installation$ composer require dasprid/container-interop-doctrine

Using version ^0.2.2 for dasprid/container-interop-doctrine

./composer.json has been updated

Loading composer repositories with package information

Updating dependencies (including require-dev)

- Installing dasprid/container-interop-doctrine (0.2.2)

Loading from cache

Writing lock file

Generating autoload files

$

@asgrim

config/autoload/doctrine.global.php<?php

declare(strict_types = 1);

use Doctrine\ORM\EntityManagerInterface;

use ContainerInteropDoctrine\EntityManagerFactory;

return [

'dependencies' => [

'factories' => [

EntityManagerInterface::class => EntityManagerFactory::class,

],

],

];

@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/Action/CheckOutAction.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 dataINSERT 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

FlushingMiddlewarepublic function __invoke(Request $request, Response $response, callable $out = null) : Response

{

if (null === $out) {

throw new \InvalidArgumentException('Middleware in ' . __CLASS__ . ' must be piped');

}

$response = $out($request, $response);

if ($this->entityManager->isOpen()) {

$this->entityManager->flush();

}

return $response;

}

@asgrim

FlushingMiddlewarepublic function __invoke(Request $request, Response $response, callable $out = null) : Response

{

if (null === $out) {

throw new \InvalidArgumentException('Middleware in ' . __CLASS__ . ' must be piped');

}

$response = $out($request, $response);

if ($this->entityManager->isOpen()) {

$this->entityManager->flush();

}

return $response;

}

@asgrim

FlushingMiddlewarepublic function __invoke(Request $request, Response $response, callable $out = null) : Response

{

if (null === $out) {

throw new \InvalidArgumentException('Middleware in ' . __CLASS__ . ' must be piped');

}

$response = $out($request, $response);

if ($this->entityManager->isOpen()) {

$this->entityManager->flush();

}

return $response;

}

@asgrim

FlushingMiddlewarepublic function __invoke(Request $request, Response $response, callable $out = null) : Response

{

if (null === $out) {

throw new \InvalidArgumentException('Middleware in ' . __CLASS__ . ' must be piped');

}

$response = $out($request, $response);

if ($this->entityManager->isOpen()) {

$this->entityManager->flush();

}

return $response;

}

@asgrim

FlushingMiddlewarepublic function __invoke(Request $request, Response $response, callable $out = null) : Response

{

if (null === $out) {

throw new \InvalidArgumentException('Middleware in ' . __CLASS__ . ' must be piped');

}

$response = $out($request, $response);

if ($this->entityManager->isOpen()) {

$this->entityManager->flush();

}

return $response;

}

@asgrim

Doing more with middleware

@asgrim

Authentication

@asgrim

public function __invoke(

Request $request, Response $response, callable $next = null

) : Response {

$queryParams = $request->getQueryParams();

if (!array_key_exists('authenticated', $queryParams)||$queryParams['authenticated'] !== '1') {

return new JsonResponse(

['error' => 'You are not authenticated.'],

403

);

}

return $next($request, $response);

}

Create the middleware

@asgrim

public function __invoke(

Request $request, Response $response, callable $next = null

) : Response {

$queryParams = $request->getQueryParams();

if (!array_key_exists('authenticated', $queryParams)||$queryParams['authenticated'] !== '1') {

return new JsonResponse(

['error' => 'You are not authenticated.'],

403

);

}

return $next($request, $response);

}

Create the middleware

@asgrim

public function __invoke(

Request $request, Response $response, callable $next = null

) : Response {

$queryParams = $request->getQueryParams();

if (!array_key_exists('authenticated', $queryParams)||$queryParams['authenticated'] !== '1') {

return new JsonResponse(

['error' => 'You are not authenticated.'],

403

);

}

return $next($request, $response);

}

Create the middleware

@asgrim

public function __invoke(

Request $request, Response $response, callable $next = null

) : Response {

$queryParams = $request->getQueryParams();

if (!array_key_exists('authenticated', $queryParams)||$queryParams['authenticated'] !== '1') {

return new JsonResponse(

['error' => 'You are not authenticated.'],

403

);

}

return $next($request, $response);

}

Create the middleware

@asgrim

Helioscomposer require dasprid/helios

@asgrim

PSR7-Sessioncomposer require psr7-sessions/storageless

@asgrim

public function __invoke(ContainerInterface $container, $_, array $_ = null)

{

$symmetricKey = 'super-secure-key-you-should-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 SystemCurrentTime()

);

}

Factory the middleware

@asgrim

'routing' => [

'middleware' => [

ApplicationFactory::ROUTING_MIDDLEWARE,

Helper\UrlHelperMiddleware::class,

PSR7Session\Http\SessionMiddleware::class,

App\Middleware\AuthenticationMiddleware::class,

ApplicationFactory::DISPATCH_MIDDLEWARE,

],

'priority' => 1,

],

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

To summarise...

● PSR-7 is the foundation for this!● Diactoros is just a PSR-7 implementation● Stratigility is a middleware pipeline: the main bit● Expressive is a glue for everything● DoctrineModule can be used (with some fiddling)● container-interop-doctrine makes Doctrine work easier● Middleware all the things!

Any questions?

https://joind.in/talk/3bf19

James Titcumb @asgrim