Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Decorating Applications with Stack (ZendCon 2014)

Decorating Applications with Stack (ZendCon 2014)

Stack is a convention for composing HttpKernelInterface middlewares. By following Stack's conventions you can add behavior and functionality to any application based on Symfony's HttpKernelInterface. This means Stack middlewares can be applied to Silex, Laravel 4, and Drupal 8 applications in addition to any other HttpKernelInterface based application. Learn the conventions, see community middlewares, and find out how to get started with Stack.

23d971deeb3975a7d28246192fbbe7b7?s=128

Beau Simensen

October 28, 2014
Tweet

Transcript

  1. Decorating Applications with Stack ZendCon October 28th, 2014 @beausimensen beau.io

    joind.in/12530
  2. None
  3. None
  4. None
  5. None
  6. Hello, Stack!

  7. #silex-php

  8. None
  9. February 1st, 2013 <simensen> igorw: what is the closest analogy

    to middlewares is there in silex/symfony? anything? <igorw> simensen: what kind of middlewares? <simensen> like ruby rack middlewares or python middlewares. <igorw> httpkernel decorators ... <igorw> I guess you can think of it like a stack of httpkernels?
  10. May 23rd, 2013

  11. So what is Stack?

  12. Stack is a convention for composing HttpKernelInterface middlewares

  13. Stack is a convention for composing HttpKernelInterface middlewares

  14. Stack is NOT a library

  15. Stack is NOT a framework

  16. • A Stack middleware MUST implement the HttpKernelInterface. • A

    Stack middleware MAY delegate to a decorated HttpKernelInterface instance. • A Stack middleware MUST take an HttpKernelInterface as its first constructor argument. Good old RFC 2119...
  17. That’s it. That’s a Stack middleware.

  18. Stack is a convention for composing HttpKernelInterface middlewares

  19. None
  20. –Fabien Potencier, HttpKernelInterface.php “HttpKernelInterface handles a Request to convert it

    to a Response.”
  21. None
  22. "Stack is framework agnostic."

  23. None
  24. "Framework agnostic," my foot!

  25. ... and more!

  26. Stack is a convention for composing HttpKernelInterface middlewares

  27. HAS-A vs IS-A

  28. HAS-A <3

  29. Interfaces?

  30. Decorate them!

  31. WRAPS-A? :)

  32. MUST implement every method Good old RFC 2119...

  33. :(

  34. None
  35. just one method

  36. handle()

  37. :)

  38. Stack is a convention for composing HttpKernelInterface middlewares

  39. None
  40. None
  41. None
  42. Stack is a convention for composing HttpKernelInterface middlewares

  43. Anatomy of a Middleware

  44. class Passthru implements HttpKernelInterface { private $app; public function __construct(HttpKernelInterface

    $app) { $this->app = $app; } public function handle( Request $request, $type = self::MASTER_REQUEST, $catch = true ) { return $this->app->handle($request, $type, $catch) } }
  45. for the sake of brevity

  46. "use HttpKernelInterface as HKI"

  47. public function handle($request)

  48. class Passthru implements HKI { private $app; public function __construct(HKI

    $app) { $this->app = $app; } public function handle($request) { return $this->app->handle($request) } }
  49. class Passthru implements HKI { private $app; public function __construct(HKI

    $app) { $this->app = $app; } public function handle($request) { // Request / before ! // Delegate $response = $this->app->handle($request) ! // Response /after return $response; } }
  50. class Passthru implements HKI { private $app; public function __construct(HKI

    $app) { $this->app = $app; } public function handle($request) { // Request / before ! // Delegate $response = $this->app->handle($request) ! // Response /after return $response; } }
  51. None
  52. Stack Builder

  53. Builder is a tool to help "implement Stack"

  54. Builder is NOT required to "implement Stack"

  55. $stack = (new Stack\Builder()) ! ->push('Stack\Session') ! ->push('Dflydev\Stack\BasicAuthentication') ! ->push('Silpion\Stack\Logger');

    ! $app = new YourAppKernel(); ! $app = $stack->resolve($app);
  56. push() accepts the classname of a Stack middleware !

  57. push() accepts the classname of a Stack middleware ! ...

    or ...
  58. a callable whose first argument is an HttpKernelInterface instance !

  59. class Application implements HttpKernelInterface { public function run(SymfonyRequest $request =

    null) { $request = $request ?: $this['request']; ! $response = with($stack = $this->getStackedClient())->handle($request); ! $response->send(); ! $stack->terminate($request, $response); } ! protected function getStackedClient() { $sessionReject = $this->bound('session.reject') ? $this['session.reject'] : null; ! $client = (new \Stack\Builder) ->push('Illuminate\Cookie\Guard', $this['encrypter']) ->push('Illuminate\Cookie\Queue', $this['cookie']) ->push('Illuminate\Session\Middleware', $this['session'], $sessionReject); ! $this->mergeCustomMiddlewares($client); ! return $client->resolve($this); } }
  60. class StackedKernelPass implements CompilerPassInterface { public function process(ContainerBuilder $container) {

    if (!$container->hasDefinition('http_kernel_factory')) { return; } ! $http_kernel_factory = $container->getDefinition('http_kernel_factory'); $middleware_priorities = array(); $middleware_arguments = array(); foreach ($container->findTaggedServiceIds('http_middleware') as $id => $attributes) { $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0; $middleware_priorities[$id] = $priority; $definition = $container->getDefinition($id); $middleware_arguments[$id] = $definition->getArguments(); array_unshift($middleware_arguments[$id], $definition->getClass()); } array_multisort($middleware_priorities, SORT_DESC, $middleware_arguments, SORT_DESC); ! foreach ($middleware_arguments as $id => $push_arguments) { $http_kernel_factory->addMethodCall('push', $push_arguments); } } }
  61. Middlewares

  62. Inline Middleware (Inline functions / callables)

  63. class Inline implements HKI { public function __construct(HKI $app, $callable)

    { $this->app = $app; $this->callable = $callable; } public function handle($request) { return call_user_func($this->callable, $this->app); } }
  64. $app = new Silex\Application(); ! $app->get('/', function (Request $request) {

    if ('success' === $request->attributes->get('callable_middleware')) { return new Response('SUCCESS'); } ! return new Response('FAILED', 500); }); ! $inlineMiddleware = function(HKI $app) { $request->attributes->set('callable_middleware', 'success'); $response = $app->handle($request, $type, $catch); $response->setContent('['.$response->getContent().']'); return $response; }; ! $stack = (new Stack\Builder()) ->push('Stack\Inline', $inlineMiddleware); ! $app = $stack->resolve($app);
  65. 200, "[SUCCESS]"

  66. Great for testing

  67. Session Middleware

  68. class Session implements HKI { public function __construct(HKI $app, $session)

    { $this->app = $app; $this->session = $session; } public function handle($request) { $request->setSession($this->session); $this->session->setId($request->cookies->get($this->session->getName())); ! $response = $this->app->handle($request); ! if ($this->session && $this->session->isStarted()) { $response->headers->setCookie(new Cookie( $session->getName(), $session->getId(), /* ... */ )); } ! return $response; } }
  69. class Session implements HKI { public function __construct(HKI $app, $session)

    { $this->app = $app; $this->session = $session; } public function handle($request) { $request->setSession($this->session); $this->session->setId($request->cookies->get($this->session->getName())); ! $response = $this->app->handle($request); ! if ($this->session && $this->session->isStarted()) { $response->headers->setCookie(new Cookie( $session->getName(), $session->getId(), /* ... */ )); } ! return $response; } }
  70. class Session implements HKI { public function __construct(HKI $app, $session)

    { $this->app = $app; $this->session = $session; } public function handle($request) { $request->setSession($this->session); $this->session->setId($request->cookies->get($this->session->getName())); ! $response = $this->app->handle($request); ! if ($this->session && $this->session->isStarted()) { $response->headers->setCookie(new Cookie( $session->getName(), $session->getId(), /* ... */ )); } ! return $response; } }
  71. class Session implements HKI { public function __construct(HKI $app, $session)

    { $this->app = $app; $this->session = $session; } public function handle($request) { $request->setSession($this->session); $this->session->setId($request->cookies->get($this->session->getName())); ! $response = $this->app->handle($request); ! if ($this->session && $this->session->isStarted()) { $response->headers->setCookie(new Cookie( $session->getName(), $session->getId(), /* ... */ )); } ! return $response; } }
  72. class Session implements HKI { public function __construct(HKI $app, $session)

    { $this->app = $app; $this->session = $session; } public function handle($request) { $request->setSession($this->session); $this->session->setId($request->cookies->get($this->session->getName())); ! $response = $this->app->handle($request); ! if ($this->session && $this->session->isStarted()) { $response->headers->setCookie(new Cookie( $session->getName(), $session->getId(), /* ... */ )); } ! return $response; } }
  73. (It is actually a little more complicated than that)

  74. $app = new Silex\Application(); ! $app->get('/login', function (Request $request) {

    $session = $request->getSession(); $username = $request->server->get('PHP_AUTH_USER'); $password = $request->server->get('PHP_AUTH_PW'); if ('igor' === $username && 'password' === $password) { $session->set('user', array('username' => $username)); return new RedirectResponse('/account'); } return new Response('Please sign in.', 401, [ 'WWW-Authenticate' => sprintf('Basic realm="%s"', 'site_login'), ]); }); ! $app->get('/account', function (Request $request) { $session = $request->getSession(); if (null === $user = $session->get('user')) { return new RedirectResponse('/login'); } return sprintf('Welcome %s!', $user['username']); }); ! $stack = (new Stack\Builder()) ->push('Stack\Session'); ! $app = $stack->resolve($app);
  75. URL Map

  76. class UrlMap implements HKI { public function __construct(HKI $app, $map)

    { $this->app = $app; $this->map = $map; } public function handle($request) { $pathInfo = rawurldecode($request->getPathInfo()); foreach ($this->map as $path => $app) { if (0 === strpos($pathInfo, $path)) { $server = $request->server->all(); $server['SCRIPT_FILENAME'] = $server['SCRIPT_NAME'] = $server['PHP_SELF'] = $request->getBaseUrl().$path; $attributes = $request->attributes->all(); $attributes['stack.url_map.prefix'] = $request->getBaseUrl().$path; $newRequest = $request->duplicate(null, null, $attributes, null, null, $server); return $app->handle($newRequest, $type, $catch); } } return $this->app->handle($request); } }
  77. class UrlMap implements HKI { public function __construct(HKI $app, $map)

    { $this->app = $app; $this->map = $map; } public function handle($request) { $pathInfo = rawurldecode($request->getPathInfo()); foreach ($this->map as $path => $app) { if (0 === strpos($pathInfo, $path)) { $server = $request->server->all(); $server['SCRIPT_FILENAME'] = $server['SCRIPT_NAME'] = $server['PHP_SELF'] = $request->getBaseUrl().$path; $attributes = $request->attributes->all(); $attributes['stack.url_map.prefix'] = $request->getBaseUrl().$path; $newRequest = $request->duplicate(null, null, $attributes, null, null, $server); return $app->handle($newRequest, $type, $catch); } } return $this->app->handle($request); } }
  78. class UrlMap implements HKI { public function __construct(HKI $app, $map)

    { $this->app = $app; $this->map = $map; } public function handle($request) { $pathInfo = rawurldecode($request->getPathInfo()); foreach ($this->map as $path => $app) { if (0 === strpos($pathInfo, $path)) { $server = $request->server->all(); $server['SCRIPT_FILENAME'] = $server['SCRIPT_NAME'] = $server['PHP_SELF'] = $request->getBaseUrl().$path; $attributes = $request->attributes->all(); $attributes['stack.url_map.prefix'] = $request->getBaseUrl().$path; $newRequest = $request->duplicate(null, null, $attributes, null, null, $server); return $app->handle($newRequest, $type, $catch); } } return $this->app->handle($request); } }
  79. class UrlMap implements HKI { public function __construct(HKI $app, $map)

    { $this->app = $app; $this->map = $map; } public function handle($request) { $pathInfo = rawurldecode($request->getPathInfo()); foreach ($this->map as $path => $app) { if (0 === strpos($pathInfo, $path)) { $server = $request->server->all(); $server['SCRIPT_FILENAME'] = $server['SCRIPT_NAME'] = $server['PHP_SELF'] = $request->getBaseUrl().$path; $attributes = $request->attributes->all(); $attributes['stack.url_map.prefix'] = $request->getBaseUrl().$path; $newRequest = $request->duplicate(null, null, $attributes, null, null, $server); return $app->handle($newRequest, $type, $catch); } } return $this->app->handle($request); } }
  80. class UrlMap implements HKI { public function __construct(HKI $app, $map)

    { $this->app = $app; $this->map = $map; } public function handle($request) { $pathInfo = rawurldecode($request->getPathInfo()); foreach ($this->map as $path => $app) { if (0 === strpos($pathInfo, $path)) { $server = $request->server->all(); $server['SCRIPT_FILENAME'] = $server['SCRIPT_NAME'] = $server['PHP_SELF'] = $request->getBaseUrl().$path; $attributes = $request->attributes->all(); $attributes['stack.url_map.prefix'] = $request->getBaseUrl().$path; $newRequest = $request->duplicate(null, null, $attributes, null, null, $server); return $app->handle($newRequest, $type, $catch); } } return $this->app->handle($request); } }
  81. class UrlMap implements HKI { public function __construct(HKI $app, $map)

    { $this->app = $app; $this->map = $map; } public function handle($request) { $pathInfo = rawurldecode($request->getPathInfo()); foreach ($this->map as $path => $app) { if (0 === strpos($pathInfo, $path)) { $server = $request->server->all(); $server['SCRIPT_FILENAME'] = $server['SCRIPT_NAME'] = $server['PHP_SELF'] = $request->getBaseUrl().$path; $attributes = $request->attributes->all(); $attributes['stack.url_map.prefix'] = $request->getBaseUrl().$path; $newRequest = $request->duplicate(null, null, $attributes, null, null, $server); return $app->handle($newRequest, $type, $catch); } } return $this->app->handle($request); } }
  82. class UrlMap implements HKI { public function __construct(HKI $app, $map)

    { $this->app = $app; $this->map = $map; } public function handle($request) { $pathInfo = rawurldecode($request->getPathInfo()); foreach ($this->map as $path => $app) { if (0 === strpos($pathInfo, $path)) { $server = $request->server->all(); $server['SCRIPT_FILENAME'] = $server['SCRIPT_NAME'] = $server['PHP_SELF'] = $request->getBaseUrl().$path; $attributes = $request->attributes->all(); $attributes['stack.url_map.prefix'] = $request->getBaseUrl().$path; $newRequest = $request->duplicate(null, null, $attributes, null, null, $server); return $app->handle($newRequest, $type, $catch); } } return $this->app->handle($request); } }
  83. class UrlMap implements HKI { public function __construct(HKI $app, $map)

    { $this->app = $app; $this->map = $map; } public function handle($request) { $pathInfo = rawurldecode($request->getPathInfo()); foreach ($this->map as $path => $app) { if (0 === strpos($pathInfo, $path)) { $server = $request->server->all(); $server['SCRIPT_FILENAME'] = $server['SCRIPT_NAME'] = $server['PHP_SELF'] = $request->getBaseUrl().$path; $attributes = $request->attributes->all(); $attributes['stack.url_map.prefix'] = $request->getBaseUrl().$path; $newRequest = $request->duplicate(null, null, $attributes, null, null, $server); return $app->handle($newRequest, $type, $catch); } } return $this->app->handle($request); } }
  84. class UrlMap implements HKI { public function __construct(HKI $app, $map)

    { $this->app = $app; $this->map = $map; } public function handle($request) { $pathInfo = rawurldecode($request->getPathInfo()); foreach ($this->map as $path => $app) { if (0 === strpos($pathInfo, $path)) { $server = $request->server->all(); $server['SCRIPT_FILENAME'] = $server['SCRIPT_NAME'] = $server['PHP_SELF'] = $request->getBaseUrl().$path; $attributes = $request->attributes->all(); $attributes['stack.url_map.prefix'] = $request->getBaseUrl().$path; $newRequest = $request->duplicate(null, null, $attributes, null, null, $server); return $app->handle($newRequest, $type, $catch); } } return $this->app->handle($request); } }
  85. $app = new Application(); $app->get('/', function () { return "Main

    Application!"; }); ! $blog = new Application(); $blog->get('/', function () { return "This is the blog!"; }); ! $forum = new Application(); $forum->get('/', function () { return "This is the forum!"; }); ! $map = [ "/blog" => $blog, "/forum" => $forum, ]; ! $stack = (new Stack\Builder()) ->push('Stack\UrlMap', $map); ! $app = $stack->resolve($app);
  86. HTTP Cache

  87. The original Stack middleware

  88. class HttpCache implements HKI { public function __construct(HKI $app, StoreInterface

    $store) { $this->app = $app; $this->store = $store; } public function handle($request) { if ($this->store->isStillCached($request)) { $response = $this->store->getFromCache($request); } else { $response = $this->app->handle($request); } ! $response = $this->app->handle($request); ! if ($this->store->shouldCache($request, $response)) { $this->store->cache($request, $response); } ! return $response; } }
  89. (That was not even close to what actually happens...)

  90. Geo IP

  91. class GeoIp implements HKI { public function __construct(HKI $app, $geocoder,

    $header = 'X-Country') { $this->app = $app; $this->geocoder = $geocoder; $this->header = $header; } public function handle($request) { $results = $this->geocoder->geocode($request->getClientIp()); if ($country = $results->getCountryCode()) { $request->headers->set($this->header, $country, true); } return $this->app->handle($request); } }
  92. $app = new Silex\Application(); ! $app->get('/', function(Request $request) { $ip

    = $request->getClientIp(); $country = $request->headers->get('X-Country', 'UNKNOWN'); ! return new Response($ip . ' => '. $country, 200); }); ! $stack = (new Stack\Builder()) ->push('Geocoder\Stack\GeoIp'); ! $app = $stack->resolve($app);
  93. $app = new Silex\Application(); ! $app->get('/', function(Request $request) { $ip

    = $request->getClientIp(); $country = $request->headers->get('X-Country', 'UNKNOWN'); ! return new Response($ip . ' => '. $country, 200); }); ! $blockAllButUs = function (HKI $app, $request) { if ($request->headers->get('X-Country') != 'US') { return new RedirectResponse('http://google.com); } return $app->handle($request); }; ! $stack = (new Stack\Builder()) ->push('Geocoder\Stack\GeoIp') ->push('Stack\Inline', $blockAllButUs); ! $app = $stack->resolve($app);
  94. Backstage (maintenance pages)

  95. class Backstage implements HKI { public function __construct(HKI $app, $path)

    { $this->app = $app; $this->path = $path; } ! public function handle($request) { $path = realpath($this->path); ! if (false !== $path) { return new Response(file_get_contents($path), 503); } ! return $this->app->handle($request); } }
  96. $app = new Silex\Application(); ! $app->get('/', function () { return

    'my app is working'; }); ! $stack = (new Stack\Builder()) ->push('Atst\StackBackstage', __DIR__.'/maintenance.html'); ! $app = $stack->resolve($app);
  97. $ echo "<h1>Down for Maintenance</h1>" > /path/to/your/app/maintenance.html ! # do

    your thang ! $ rm /path/to/your/app/maintenance.html
  98. So many more middlewares...

  99. • HTTP Cache • CookieGuard • GeoIp • IpRestrict •

    Backstage • OAuth • Basic Authentication • Hawk • CORS • Robots • Negotiation • Honeypot • Logger • ... and more
  100. The Future!

  101. [this page intentionally left blank]

  102. Stack is NOT a library

  103. Stack is NOT a framework

  104. Stack is a convention for composing HttpKernelInterface middlewares

  105. ... and more!

  106. Questions? @beausimensen • @stackphp • @dflydev @thatpodcast • thatpodcast.io ddd.io/zendcon2014

    joind.in/12530
  107. [this page intentionally left blank]

  108. @igorwhiletrue @beausimensen @hochchristoph (plus many others!)

  109. Stack Conventions

  110. STACK-0 Proposals Draft

  111. STACK-0 Proposals Draft rfc.zeromq.org

  112. Stack-0 Proposals • MUST look like STACK-{NUMBER}. • MUST have

    a descriptive name. Good old RFC 2119...
  113. STACK-1 Core Draft

  114. Stack-1 Core • Middlewares MUST implement the HttpKernelInterface. • Middlewares

    MAY delegate to the decorated HttpKernelInterface. • Middlewares MUST take an HttpKernelInterface as its first constructor argument. Good old RFC 2119...
  115. WIP Stack-1 Core • Middlewares MUST implement the HttpKernelInterface. •

    Middlewares MAY delegate to the decorated HttpKernelInterface. • Middlewares MUST take an HttpKernelInterface as its first constructor argument. Good old RFC 2119...
  116. STACK-2 Authentication Draft

  117. stack.authn.token (string or serializable)

  118. 401 Unauthorized WWW-Authenticate: Stack

  119. dflydev/stack-authentication A collection of Stack middlewares designed to help authentication

    middleware implementors adhere to the STACK-2 Authentication conventions.
  120. STACK-3 Authorization Draft

  121. stack.authn.token

  122. 401 Unauthorized WWW-Authenticate: Stack

  123. 403 Forbidden