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

Decorating Applications with Stack (Symfony Live London 2014)

Beau Simensen
September 24, 2014

Decorating Applications with Stack (Symfony Live London 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.

Beau Simensen

September 24, 2014
Tweet

More Decks by Beau Simensen

Other Decks in Programming

Transcript

  1. 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?
  2. • 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...
  3. :(

  4. :)

  5. – http://silex.sensiolabs.org/doc/middlewares.html “Silex allows you to run code, that changes

    the default Silex behavior, at different stages during the handling of a request through middlewares”
  6. – https://docs.djangoproject.com/en/dev/topics/http/middleware/ “Middleware is a framework of hooks into Django’s

    request/response processing. It’s a light, low-level “plugin” system for globally altering Django’s input or output.”
  7. – https://docs.djangoproject.com/en/dev/topics/http/middleware/ "Each middleware component is responsible for doing some

    specific function. For example, Django includes a middleware component [...] that associates users with requests using sessions."
  8. – http://rubylearning.com/blog/a-quick-introduction-to-rack/#C12 “The fundamental idea behind Rack middleware is –

    come between the calling client and the server, process the HTTP request before sending it to the server, and processing the HTTP response before returning it to the client.”
  9. – http://zaiste.net/2012/08/concisely_about_rack_applications/ "Rack middleware provides a way to implement a

    chained process execution for web applications. It's an implementation of the pipeline design pattern. It acts "in the middle" between the client and the server processing requests before they reach the server and responses before they are returned to the client."
  10. 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) } }
  11. class Passthru implements HKI { private $app; public function __construct(HKI

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

    $app) { $this->app = $app; } public function handle($request) { return $this->app->handle($request) } }
  13. 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; } }
  14. 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); } }
  15. 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); } } }
  16. 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); } }
  17. $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);
  18. 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; } }
  19. 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; } }
  20. 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; } }
  21. 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; } }
  22. 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; } }
  23. $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);
  24. 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); } }
  25. 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); } }
  26. 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); } }
  27. 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); } }
  28. 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); } }
  29. 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); } }
  30. 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); } }
  31. 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); } }
  32. 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); } }
  33. $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);
  34. 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; } }
  35. 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); } }
  36. $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);
  37. $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);
  38. 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); } }
  39. $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);
  40. $ echo "<h1>Down for Maintenance</h1>" > /path/to/your/app/maintenance.html ! # do

    your thang ! $ rm /path/to/your/app/maintenance.html
  41. • HTTP Cache • CookieGuard • GeoIp • IpRestrict •

    Backstage • OAuth • Basic Authentication • Hawk • CORS • Robots • Negotiation • Honeypot • Logger • ... and more
  42. 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...
  43. 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...
  44. dflydev/stack-authentication A collection of Stack middlewares designed to help authentication

    middleware implementors adhere to the STACK-2 Authentication conventions.