Advertisement
Advertisement

More Related Content

Advertisement

More from James Titcumb(20)

Advertisement

Kicking off with Zend Expressive and Doctrine ORM (PHP South Africa 2018)

  1. @asgrim Kicking off with Zend Expressive and Doctrine ORM James Titcumb PHP South Africa 2018 Follow along (optional): https://github.com/asgrim/book-library
  2. $ whoami James Titcumb www.jamestitcumb.com www.roave.com @asgrim Follow along (optional): https://github.com/asgrim/book-library
  3. @asgrim What is Zend Expressive?
  4. @asgrim Layers of an Expressive application Expressive Stratigility Diactoros PSR-7 Interface DIRouter Template Your Application
  5. @asgrim Layers of an Expressive application Expressive Stratigility Diactoros PSR-7 Interface DIRouter Template Your Application
  6. @asgrim PSR-7 HTTP Message Interfaces
  7. @asgrim HTTP Request POST /talk HTTP/1.1 Host: phpminds.org foo=bar&baz=bat
  8. @asgrim HTTP Response HTTP/1.1 200 OK Content-Type: text/plain This is the response body
  9. @asgrim Layers of an Expressive application Expressive Stratigility Diactoros PSR-7 Interface DIRouter Template Your Application
  10. @asgrim Zend Diactoros PSR-7 implementation
  11. @asgrim Node http.Server using Diactoros $server = ZendDiactorosServer::createServer( function ($request, $response, $done) { return $response->getBody()->write('hello world'); }, $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES ); $server->listen();
  12. @asgrim Layers of an Expressive application Expressive Stratigility Diactoros PSR-7 Interface DIRouter Template Your Application
  13. @asgrim Zend Stratigility Creating & dispatching middleware pipelines
  14. @asgrim So what is “middleware”?
  15. @asgrim Middleware in the middle function(Request $request, RequestHandler $handler) : Response { // ... some code before ... $response = $handler->handle($request); // ... some code after ... return $response; }
  16. @asgrim Some people call it an onion SessionInitialisingMiddleware AuthenticationMiddleware ThingyMiddleware DashboardHandler
  17. @asgrim Some people call it an onion SessionInitialisingMiddleware AuthenticationMiddleware ThingyMiddleware DashboardHandler
  18. @asgrim Some people call it an onion SessionInitialisingMiddleware AuthenticationMiddleware ThingyMiddleware DashboardHandler
  19. @asgrim psr/http-server-middleware
  20. @asgrim Middleware using MiddlewareInterface <?php declare(strict_types=1); namespace AppMiddleware; use PsrHttpMessageResponseInterface; use PsrHttpMessageServerRequestInterface; use PsrHttpServerMiddlewareInterface; use PsrHttpServerRequestHandlerInterface; final class MyMiddleware implements MiddlewareInterface { public function process( ServerRequestInterface $request, RequestHandlerInterface $requestHandler ) : ResponseInterface { return $requestHandler>handle($request); } }
  21. @asgrim Double-pass middleware public function __invoke( Request $request, Response $response, callable $next ): Response { return $next($request, $response); } !!! DEPRECATED !!!
  22. @asgrim Modifying the response function(Request $request, RequestHandler $handler) : Response { $response = $handler->handle($request); return $response->withHeader( 'X-Clacks-Overhead', 'GNU Terry Pratchett' ); }
  23. @asgrim Pipe all the things! $pipe = new ZendStratigilityMiddlewarePipe(); $pipe->pipe($logUncaughtErrorsMiddleware); $pipe->pipe($sessionInitialisingMiddleware); $pipe->pipe($authenticationMiddleware); $pipe->pipe('/', $indexHandlerMiddleware); $pipe->pipe(new NotFoundHandler(...));
  24. @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 ); } }
  25. @asgrim SessionInitialisingMiddleware function(Request $request, RequestHandler $handler) : Response { session_start(); return $handler->handle($request); }
  26. @asgrim function(Request $request, RequestHandler $handler) : Response { if (!$this->doSomeOAuthCheck($request)) { return new JsonResponse( [ 'message' => 'Invalid OAuth token', ], 403 ); } return $handler->handle($request); } AuthenticationMiddleware
  27. @asgrim IndexHandlerMiddleware function(Request $request, RequestHandler $handler) : Response { // ... some code ... return new JsonResponse($someData, 200); }
  28. @asgrim Layers of an Expressive application Expressive Stratigility Diactoros PSR-7 Interface DIRouter Template Your Application
  29. @asgrim Zend Expressive One Ring to bring them all and in the darkness bind them.
  30. @asgrim Routing
  31. @asgrim container-interop
  32. @asgrim Optionally, templating
  33. @asgrim Piping and Routing
  34. @asgrim Zend Framework 2/3 What of them?
  35. @asgrim Middleware vs MVC
  36. @asgrim Using ZF components in Expressive
  37. @asgrim ZF’s Module.php class Module { public function getConfig() { return include __DIR__ . '/../config/module.config.php'; } }
  38. @asgrim ConfigProvider
  39. @asgrim ConfigProvider namespace ZendForm; class ConfigProvider { public function __invoke() { return [ 'dependencies' => $this->getDependencyConfig(), 'view_helpers' => $this->getViewHelperConfig(), ]; } }
  40. @asgrim ConfigProvider#getDependencyConfig() public function getDependencyConfig() { return [ 'abstract_factories' => [ FormAbstractServiceFactory::class, ], 'aliases' => [ 'ZendFormAnnotationFormAnnotationBuilder' => 'FormAnnotationBuilder', AnnotationAnnotationBuilder::class => 'FormAnnotationBuilder', FormElementManager::class => 'FormElementManager', ], 'factories' => [ 'FormAnnotationBuilder' => AnnotationAnnotationBuilderFactory::class, 'FormElementManager' => FormElementManagerFactory::class, ],
  41. @asgrim ZendForm’s Module.php (for Zend Framework) class Module { public function getConfig() { $provider = new ConfigProvider(); return [ 'service_manager' => $provider->getDependencyConfig(), 'view_helpers' => $provider->getViewHelperConfig(), ]; } }
  42. @asgrim config/config.php <?php $aggregator = new ConfigAggregator([ ZendFormConfigProvider::class, // .. other config ... ], $cacheConfig['config_cache_path']); return $aggregator->getMergedConfig();
  43. @asgrim ConfigAggregator
  44. @asgrim config/config.php $aggregator = new ConfigAggregator([ ZendExpressiveRouterFastRouteRouterConfigProvider::class, ZendHttpHandlerRunnerConfigProvider::class, new ArrayProvider($cacheConfig), ZendExpressiveHelperConfigProvider::class, ZendExpressiveConfigProvider::class, ZendExpressiveRouterConfigProvider::class, AppConfigProvider::class, new PhpFileProvider(realpath(__DIR__) . '/autoload/{{,*.}global,{,*.}local}.php'), new PhpFileProvider(realpath(__DIR__) . '/development.config.php'), ], $cacheConfig['config_cache_path']);
  45. @asgrim Layers of an Expressive application Expressive Stratigility Diactoros PSR-7 Interface DIRouter Template Your Application
  46. @asgrim Getting started with Zend Expressive
  47. @asgrim https://github.com/asgrim/book-library
  48. @asgrim Expressive Skeleton
  49. @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 > ExpressiveInstallerOptionalPackages::install Setting up optional packages Setup data and cache dir Removing installer development dependencie
  50. @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
  51. @asgrim Modular structure zend-config-aggregator
  52. @asgrim Modular structure zend-config-aggregator
  53. @asgrim Flat or Modular?
  54. @asgrim Default modular structure src/ MyModule/ src/ ConfigProvider.php Handler/ Entity/ Middleware/ templates/ test/
  55. @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
  56. @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
  57. @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):
  58. @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
  59. @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/" }
  60. @asgrim Create the endpoints
  61. @asgrim Book entity class Book { /** * @var string */ private $id; /** * @var bool */ private $inStock = true; public function __construct() { $this->id = (string)Uuid::uuid4(); }
  62. @asgrim Book entity class Book { /** * @return void * @throws AppEntityExceptionBookNotAvailable */ public function checkOut() { if (!$this->inStock) { throw ExceptionBookNotAvailable::fromBook($this); } $this->inStock = false; }
  63. @asgrim Book entity class Book { /** * @return void * @throws AppEntityExceptionBookAlreadyStocked */ public function checkIn() { if ($this->inStock) { throw ExceptionBookAlreadyStocked::fromBook($this); } $this->inStock = true; }
  64. @asgrim FindBookByUuidInterface interface FindBookByUuidInterface { /** * @param UuidInterface $slug * @return Book * @throws ExceptionBookNotFound */ public function __invoke(UuidInterface $slug): Book; }
  65. @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()), ]); }
  66. @asgrim return function ( Application $app, MiddlewareFactory $factory, ContainerInterface $container ): void { $app->pipe(ZendStratigilityMiddlewareErrorHandler::class); $app->pipe(ZendExpressiveRouterMiddlewarePathBasedRoutingMiddleware::class); // Routing $app->pipe(ZendExpressiveRouterMiddlewareDispatchMiddleware::class); // Dispatch $app->pipe(ZendExpressiveHandlerNotFoundHandler::class); }; Define application pipeline - pipeline.php
  67. @asgrim return function ( Application $app, MiddlewareFactory $factory, ContainerInterface $container ): void { $app->get( '/book/{id}/check-out', AppHandlerCheckOutHandler::class, 'check-out' ); $app->get( '/book/{id}/check-in', AppHandlerCheckInHandler::class, 'check-in' ); }; Define routes - routes.php
  68. @asgrim Adding some ORM
  69. @asgrim Your application Doctrine quick overview DB DBAL ORM (EntityManager) Entities Finders, Services, ...
  70. @asgrim container-interop-doctrine
  71. @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
  72. @asgrim src/App/ConfigProvider.php use DoctrineORMEntityManagerInterface; use ContainerInteropDoctrineEntityManagerFactory; final class ConfigProvider { // ... public function getDependencies() : array { return [ 'factories' => [ EntityManagerInterface::class => EntityManagerFactory::class, // ... other services ], ];
  73. @asgrim 'doctrine' => [ 'connection' => [ 'orm_default' => [ 'driver_class' => PDOPgSqlDriver::class, 'params' => [ 'url' => 'postgres://user:pass@localhost/book_library', ], ], ], 'driver' => [ 'orm_default' => [ 'class' => MappingDriverChain::class, 'drivers' => [ // ... and so on ... config/autoload/doctrine.global.php
  74. @asgrim 'doctrine' => [ 'connection' => [ 'orm_default' => [ 'driver_class' => PDOPgSqlDriver::class, 'params' => [ 'url' => 'postgres://user:pass@localhost/book_library', ], ], ], 'driver' => [ 'orm_default' => [ 'class' => MappingDriverChain::class, 'drivers' => [ // ... and so on ... config/autoload/doctrine.global.php
  75. @asgrim <?php declare(strict_types = 1); use DoctrineORMEntityManagerInterface; use DoctrineORMToolsConsoleHelperEntityManagerHelper; use SymfonyComponentConsoleHelperHelperSet; $container = require __DIR__ . '/container.php'; return new HelperSet([ 'em' => new EntityManagerHelper( $container->get(EntityManagerInterface::class) ), ]); config/cli-config.php
  76. @asgrim Annotating the Entities
  77. @asgrim /** * @ORMEntity * @ORMTable(name="book") */ class Book { /** * @ORMId * @ORMColumn(name="id", type="guid") * @ORMGeneratedValue(strategy="NONE") * @var string */ private $id; src/App/Entity/Book.php
  78. @asgrim /** * @ORMEntity * @ORMTable(name="book") */ class Book { /** * @ORMId * @ORMColumn(name="id", type="guid") * @ORMGeneratedValue(strategy="NONE") * @var string */ private $id; src/App/Entity/Book.php
  79. @asgrim public function __invoke(UuidInterface $id): Book { /** @var Book|null $book */ $book = $this->repository->find((string)$id); if (null === $book) { throw ExceptionBookNotFound::fromUuid($id); } return $book; } src/App/Service/Book/DoctrineFindBookByUuid.php
  80. @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
  81. @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! $
  82. @asgrim Insert some data INSERT INTO book (id, name, in_stock) VALUES ( '1c06bec9-adae-47c2-b411-73b1db850e6f', 'The Great Escape', true );
  83. @asgrim /book/1c06bec9-adae-47c2-b411-.../check-out {"info":"You have checked out The Great Escape"}
  84. @asgrim /book/1c06bec9-adae-47c2-b411-.../check-in {"info":"You have checked in The Great Escape"}
  85. @asgrim Automatic Flushing Middleware
  86. @asgrim FlushingMiddleware public function process(Request $request, RequestHandler $handler) { $response = $handler->handle($request); if ($this->entityManager->isOpen()) { $this->entityManager->flush(); } return $response; }
  87. @asgrim FlushingMiddleware public function process(Request $request, RequestHandler $handler) { $response = $handler->handle($request); if ($this->entityManager->isOpen()) { $this->entityManager->flush(); } return $response; }
  88. @asgrim FlushingMiddleware public function process(Request $request, RequestHandler $handler) { $response = $handler->handle($request); if ($this->entityManager->isOpen()) { $this->entityManager->flush(); } return $response; }
  89. @asgrim Add to pipeline return function ( Application $app, MiddlewareFactory $factory, ContainerInterface $container ): void { $app->pipe(ZendStratigilityMiddlewareErrorHandler::class); $app->pipe(ZendExpressiveRouterMiddlewarePathBasedRoutingMiddleware::class); // Routing $app->pipe(AppMiddlewareFlushingMiddleware::class); $app->pipe(ZendExpressiveRouterMiddlewareDispatchMiddleware::class); // Dispatch $app->pipe(ZendExpressiveHandlerNotFoundHandler::class); };
  90. @asgrim Doing more with middleware
  91. @asgrim Authentication
  92. @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
  93. @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
  94. @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
  95. @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
  96. @asgrim return function ( Application $app, MiddlewareFactory $factory, ContainerInterface $container ): void { $app->pipe(ZendStratigilityMiddlewareErrorHandler::class); $app->pipe(ZendExpressiveRouterMiddlewarePathBasedRoutingMiddleware::class); // Routing $app->pipe(AppMiddlewareFlushingMiddleware::class); $app->pipe(AppMiddlewareAuthenticationMiddleware::class); $app->pipe(ZendExpressiveRouterMiddlewareDispatchMiddleware::class); // Dispatch $app->pipe(ZendExpressiveHandlerNotFoundHandler::class); }; Add middleware to pipe
  97. @asgrim Helios composer require dasprid/helios
  98. @asgrim PSR7-Session composer require psr7-sessions/storageless
  99. @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 SignerHmacSha256(), $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
  100. @asgrim return function ( Application $app, MiddlewareFactory $factory, ContainerInterface $container ): void { $app->pipe(ZendStratigilityMiddlewareErrorHandler::class); $app->pipe(ZendExpressiveRouterMiddlewarePathBasedRoutingMiddleware::class); // Routing $app->pipe(AppMiddlewareFlushingMiddleware::class); $app->pipe(PSR7SessionsStoragelessHttpSessionMiddleware::class); $app->pipe(AppMiddlewareAuthenticationMiddleware::class); $app->pipe(ZendExpressiveRouterMiddlewareDispatchMiddleware::class); // Dispatch $app->pipe(ZendExpressiveHandlerNotFoundHandler::class); }; Add middleware to pipe
  101. @asgrim $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); $session->set('counter', $session->get('counter', 0) + 1); Session is stored in Request
  102. @asgrim $skills++;
  103. @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)
  104. @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/
  105. Any questions? James Titcumb @asgrim
Advertisement