Pro Yearly is on sale from $80 to $50! »

Taming Runaway Silex Apps - SymfonyCon Warsaw 2013

B423daa9c89538f919aec9f86f767821?s=47 Dave Marshall
December 13, 2013

Taming Runaway Silex Apps - SymfonyCon Warsaw 2013

These may not stand up too well on their own, audio recording will be available from SensioLabs sometime soon.

B423daa9c89538f919aec9f86f767821?s=128

Dave Marshall

December 13, 2013
Tweet

Transcript

  1. Taming Runaway Silex Apps http://www.flickr.com/photos/tal_axl/4321730058

  2. @davedevelopment childcare.co.uk

  3. Legacy http://www.flickr.com/photos/14993459@N08/5704123809

  4. Pimple + Services http://www.flickr.com/photos/wikidave/7440588732/sizes/l/

  5. Silex

  6. Organised Chaos

  7. Intro Best Practices Conventions Extensions Performance

  8. Don’t listen to me

  9. Pimple A simple Dependency Injection Container for PHP 5.3

  10. Silex Pimple

  11. $app['kernel'] = function ($app) { return new HttpKernel( $app['dispatcher'], $app['resolver'],

    $app['request_stack'] ); };
  12. class Pimple implements ArrayAccess { function share($callable); function protect($callable); function

    raw($id); function extend($id, $callable); function keys(); }
  13. class Pimple implements ArrayAccess { function share($callable); function protect($callable); function

    raw($id); function extend($id, $callable); function keys(); }
  14. Namespace everything $app['twig'] = ... $app['twig.loader'] = ... $app['twig.loader.filesystem'] =

    ...
  15. Lazyness The lazy way of spelling "laziness"

  16. The Problem

  17. // user.repository gets defined $app['user.repository'] = $app->share(function() { return new

    UserRepository(); });
  18. // user.service depends on user.repository $app['user.service'] = $app->share(function($app) { return

    new UserService($app['user.repository']); });
  19. // user.service is accessed $app['user.service']->setOption('debug', true);

  20. // user.repository is redefined/extended $app['user.repository'] = $app->share($app->extend( 'user.repository', function ($repo)

    { return new CachingRepository($repo); } )); // but user.service already has the uncached version $app['user.service']-> ....
  21. Theoretical Lifecycle

  22. Compile Time Defining Services

  23. Do not access any services until all services have been

    registered
  24. Compile Time Extending Services

  25. Do not access any services until all services have been

    extended as required
  26. Runtime Accessing Services

  27. Do not redefine services once they have been accessed

  28. Pimple 2.0 Services are frozen once accessed

  29. Performance Everything adds up

  30. Lazyness helps a lot...

  31. ...but the container is “recompiled” for every request

  32. app/console cache:clear

  33. Pimple 2.0 Major performance improvements

  34. Not everything has to be defined as a service Extract

    when necessary
  35. $app['sw.service'] = $app->share(function($app) { return new Stopwords\Service( $app['sw.repo'] ); });

    $app['sw.repo'] = $app->share(function($app) { return new Stopwords\Repository($app['db']); });
  36. $app['sw.service'] = $app->share(function($app) { return new Stopwords\Service( new Stopwords\Repository($app['db']) );

    });
  37. Auto-wiring

  38. <?php namespace Acme\Common; trait PimpleAutoWiring { public function bindType($type, $service)

    { $this->boundTypes[$type] = $service; } public function resolve($type, array $fixedArgs = array()) { $boundTypes = &$this->boundTypes; return function ($app) use ($type, &$boundTypes, $fixedArgs) { $rfc = new \ReflectionClass($type); $ctor = $rfc->getConstructor(); $args = array(); foreach ($ctor->getParameters() as $param) { $classHint = $param->getClass()->getName(); if ($classHint) { if (isset($boundTypes[$classHint])) { $args[] = $app[$boundTypes[$classHint]]; } else if (isset($app[$classHint])) { $args[] = $app[$classHint]; } else if (isset($app[$param->getName()])) { $args[] = $app[$param->getName()]; } else { throw new \RuntimeException("Could not resolve service for $classHint"); } } else { if (isset($fixedArgs[$param->getName()])) { $args[] = $fixedArgs[$param->getName]; } else { throw new \RuntimeException("Could not resolve parameter for {$param->getName} for $type"); } } } return $rfc->newInstanceArgs($args); }; } } ~50 slocs Could do with some caching
  39. // Acme\User\Service\Authentication public function __construct(UserRepository $userRepo) { $this->repo = $userRepo;

    } // usage $app->bindType( 'Acme\User\Repository\UserRepository', 'user.repository' ); $app['user.service'] = $app->resolve( 'Acme\User\Service\Authentication' );
  40. Silex The PHP micro-framework based on the Symfony2 Components

  41. $app = new Silex\Application; $app->get("/hello/{name}", function ($name, $app) { return

    "Hello" . $app->escape($name); }); $app->run();
  42. Silex exposes an intuitive and concise API that is fun

    to use
  43. function get($pattern, $to = null); function post($pattern, $to = null);

    function put($pattern, $to = null); function delete($pattern, $to = null); function match($pattern, $to = null);
  44. function get($pattern, $to = null); function post($pattern, $to = null);

    function put($pattern, $to = null); function delete($pattern, $to = null); function match($pattern, $to = null);
  45. function before($callback, $priority = 0); function after($callback, $priority = 0);

    function finish($callback, $priority = 0); function error($callback, $priority = -8); function on($eventName, $callback, $priority = 0);
  46. function before($callback, $priority = 0); function after($callback, $priority = 0);

    function finish($callback, $priority = 0); function error($callback, $priority = -8); function on($eventName, $callback, $priority = 0);
  47. Silex defines a range of services which can be used

    or replaced You probably don't want to mess with most of them
  48. Silex HttpKernelInterface {abstract} HttpKernel Routing EventDispatcher HttpFoundation

  49. Service Providers

  50. $app = new Silex\Application; $app->register( new Silex\Provider\TwigServiceProvider(), [ 'twig.path' =>

    __DIR__.'/views', ] );
  51. symfony/security symfony/form symfony/translation symfony/validator twig/twig doctrine/dbal swiftmailer/swiftmailer monolog/monolog

  52. doctrine/orm mustache/mustache kriswallsmith/assetic guzzle/guzzle imagine/imagine symfony/web-profiler-bundle jms/serializer aws/aws-sdk-php

  53. File Structure

  54. web/ # assets + front controllers app/ # app config/bootstrap

    src/ # app code bin/ # cli scripts + binstubs
  55. app/ config/ views/ app.php controllers.php # optional

  56. <?php // app/app.php require __DIR__.'/../vendor/autoload.php'; $app = new Silex\Application(); //

    config... // services $app['user.service'] = function() {}; // routes $app->get('/', function() { return 'Hello'; } return $app;
  57. > export APPLICATION_ENV=production > psysh app/app.phpT Psy Shell v0.1.0-dev (PHP

    5.4.9 — cli) by Justin Hileman >>> $app[‘user.service’]->deleteAll(...
  58. web/ assets/ index.php index_dev.php # optional

  59. src/ Acme/ functions.php WebApp/ ServiceProvider.php User/ Service/ Authentication.php Email/ Service/

    Mailer.php
  60. Writing Service Providers

  61. namespace Silex; interface ServiceProviderInterface { public function register(Application $app); public

    function boot(Application $app); }
  62. register() is for registering parameters and services

  63. Do register parameters preferably defaults

  64. Do register services

  65. Do extend services

  66. Don’t access services

  67. Don’t do anything else

  68. boot() is for doing things before handling a request Usually

    adding event listeners
  69. Generally acceptable to access services here Try not to

  70. Silex 2.0 [Experimental] boot() is moved to a separate provider

    interface
  71. Controller Providers

  72. namespace Silex; interface ControllerProviderInterface { function connect(Application $app); }

  73. // Acme/Silex/ControllerProvider public function connect(Application $app) { $controllers = $app['controllers_factory'];

    $controllers->get( '/hello/{name}', function ($name, Application $app) { return 'Hello'.$app->escape($name); } )->bind('site.hello_world'); return $controllers; } // app/app.php $app->mount('/', new SiteControllerProvider());
  74. Modules Bundles, Packages...

  75. src/Acme/User/ServiceProvider.php src/Acme/User/ControllerProvider.php

  76. namespace Acme\User; use Silex\Application; use Silex\ControllerProviderInterface; use Silex\ServiceProviderInterface; class Provider

    implements ControllerProviderInterface, ServiceProviderInterface { public function register(Application $app) {} public function boot(Application $app) {} public function connect(Application $app) {} }
  77. // Acme\User\Module public static function register(Application $app) { $app->register(new ServiceProvider());

    $app->mount('/', new ControllerProvider()); $app->mount('/', new AdminControllerProvider()); } // app/app.php Acme\User\Module::register($app);
  78. Controllers

  79. Name(space) all routes Silex default mirrors the pattern

  80. $app->get( '/hello/{name}', function ($name, Application $app) { return "Hello" .

    $app->escape($name); } )->bind('site.welcome');
  81. > bin/routes site.welcome GET /hello/{name} app/app.php:12

  82. Anonymous Functions

  83. Named Functions

  84. function welcome($name, Application $app) { return "Hello " . $app->escape($name);

    }; $app->get('/hello/{name}', 'welcome') ->bind('site.welcome');
  85. $app = mock('Silex\Application'); $app->shouldReceive('escape') ->with('Bob') ->andReturn('Dave'); assertThat( welcome('Bob', $app), equalTo('Hello

    Dave') );
  86. Controllers as Classes

  87. // Acme\Controller\SiteController public function welcome($name, Application $app) { return 'Hello

    ' . $app->escape($name); } // Acme\ControllerProvider.php $app->get( '/hello/{name}', 'Acme\Controller\SiteController::welcome' )->bind('site.welcome');
  88. // S\C\HttpKernel\Controller\ControllerResolver list($class, $method) = explode('::', $controller, 2); return array(new

    $class(), $method);
  89. Injecting the container BaseController and helpers

  90. // class BaseControllerResolver list($class, $method) = explode('::', $controller, 2); $controller

    = new $class(); if ($controller instanceof BaseController) { $controller->setApplication($app); } return array($controller, $method);
  91. $app['resolver'] = $this->share($app->extend( 'resolver', function ($resolver, $app) { return new

    BaseControllerResolver( $resolver, $app ); } ));
  92. // abstract class BaseController public function setApplication(Application $app) { $this->app

    = $app; } public function get($key) { return $this->app[$key]; }
  93. // abstract class BaseController public function render($template, array $context) {

    return $this ->get('twig') ->render($template, $context); }
  94. class SiteController extends BaseController { public function welcome($name) { $user

    = $this ->get('user.repository') ->findByName($name); return $this->render( 'user.html.twig', [ 'user' => $user, 'name' => $name, ] ); } }
  95. class SiteController extends BaseController { public function welcome($name) { $user

    = $this ->get('user.repository') ->findByName($name); return $this->render( 'user.html.twig', [ 'user' => $user, 'name' => $name, ] ); } }
  96. class SiteController extends BaseController { public function welcome($name) { $user

    = $this ->get('user.repository') ->findByName($name); return $this->render( 'user.html.twig', [ 'user' => $user, 'name' => $name, ] ); } }
  97. Controllers as Services

  98. use Silex\Provider\ServiceControllerServiceProvider; $app->register( new ServiceControllerServiceProvider(), ); $app['site.controller'] = $app->share(function($app) {

    return new Acme\Controller\SiteController( $app['user.repository'], $app['twig'] ); }); $app->get('/hello/{name}', 'site.controller:welcome') ->bind('site.welcome');
  99. use Silex\Provider\ServiceControllerServiceProvider; $app->register( new ServiceControllerServiceProvider(), ); $app['site.controller'] = $app->share(function($app) {

    return new Acme\Controller\SiteController( $app['user.repository'], $app['twig'] ); }); $app->get('/hello/{name}', 'site.controller:welcome') ->bind('site.welcome');
  100. use Silex\Provider\ServiceControllerServiceProvider; $app->register( new ServiceControllerServiceProvider(), ); $app['site.controller'] = $app->share(function($app) {

    return new Acme\Controller\SiteController( $app['user.repository'], $app['twig'] ); }); $app->get('/hello/{name}', 'site.controller:welcome') ->bind('site.welcome');
  101. // Acme\Controller\SiteController public function __construct($repo, $twig) { $this->repo = $repo;

    $this->twig = $twig; } public function welcome($name) { $user = $this->repo->find($name); return $this->twig->render( 'user.html.twig', [ 'user' => $user, 'name' => $name ] ); }
  102. // Acme\Controller\SiteController public function __construct($repo, $twig) { $this->repo = $repo;

    $this->twig = $twig; } public function welcome($name) { $user = $this->repo->find($name); return $this->twig->render( 'user.html.twig', [ 'user' => $user, 'name' => $name, ] ); }
  103. Resource based Routing RAD

  104. Name Method Pattern Controller Method users.index GET /users indexAction users.new

    GET /users/new newAction users.create POST /users newAction users.show GET /users/{id} showAction($id) users.edit GET /users/{id}/edit editAction($id) users.update PUT /users/{id} editAction($id) users.delete DELETE /users/{id} deleteAction($id)
  105. function resource($path, $ns, $service, $app) { $app->get($path, "$service:indexAction") ->bind("$ns.index"); $app->get($path."/{id}",

    "$service:showAction") ->bind("$ns.show"); $app->put($path."/{id}", "$service:updateAction") ->bind("$ns.update"); /* ... */ } resource("/users", "users", "users.controller", $app); resource("/posts", "posts", "posts.controller", $app);
  106. Performance Everything adds up

  107. Silex doesn’t use the full Router, only the UrlMatcher Routes

    are “recompiled” for every request
  108. Laravel 4.1 features a totally re-written routing layer. The API

    is the same; however, registering routes is a full 100% faster compared to 4.0. The entire engine has been greatly simplified, and the dependency on Symfony Routing has been removed.
  109. if (false == getenv('SKIP_ADMIN_CONTROLLERS')) { $app->mount( '/', new AdminPanelControllerProvider() );

    }
  110. None
  111. /flint/flint Enhancements to Silex with structure and conventions.

  112. Replaces the UrlMatcher with the full Router At the cost

    of some flexibility
  113. Event Listeners

  114. Use the shortcut methods like $app->on()

  115. Just like controllers anonymous functions are good, services are better

  116. Performance Lazyness

  117. function lazy($app, $service, $method) { return function() use ($app, $service,

    $method) { return call_user_func_array( [$app[$service], $method], func_get_args() ); }; } $app->on( AppEvents::USER_UPGRADE, lazy($app, "user.event_logger", "onUserEvent") );
  118. // Silex 1.2 $app->on( AppEvents::USER_UPGRADE, "user.event_logger:onUserEvent" );

  119. /davedevelopment/ pimple-aware-event-dispatcher

  120. $app['dispatcher'] = $app->share($app->extend( 'dispatcher', function($dispatcher) use ($app) { return new

    PimpleAwareEventDispatcher( $dispatcher, $app ); } )); $app['dispatcher']->addSubscriberService( “user.event_logger", "Acme\User\EventLogger" );
  121. View Renderers

  122. // Acme\Controller\UserController public function showAction($id) { $user = $this->repo->find($id); return

    ['user' => $user]; } $app->get('/user/{id}', 'user.controller:showAction') ->value('template', 'user.html.twig');
  123. // Acme\Controller\UserController public function showAction($id) { $user = $this->repo->find($id); return

    ['user' => $user]; } $app->get('/user/{id}', 'user.controller:showAction') ->value('template', 'user.html.twig');
  124. // Acme\Controller\UserController public function showAction($id) { $user = $this->repo->find($id); return

    ['user' => $user]; } $app->get('/user/{id}', 'user.controller:showAction') ->value('template', 'user.html.twig');
  125. $app->on(KernelEvents::VIEW, function ($e) use ($app) { $view = $e->getControllerResult(); if

    (!is_array($view)) { return; } $request = $event->getRequest(); $template = $request->attributes->get('template'); $body = $app['twig']->render($template, $view); $e->setResponse(new Response($body)); });
  126. $app->on(KernelEvents::VIEW, function ($e) use ($app) { $view = $e->getControllerResult(); if

    (!is_array($view)) { return; } $request = $event->getRequest(); $template = $request->attributes->get('template'); $body = $app['twig']->render($template, $view); $e->setResponse(new Response($body)); });
  127. $app->on(KernelEvents::VIEW, function ($e) use ($app) { $view = $e->getControllerResult(); if

    (!is_array($view)) { return; } $request = $event->getRequest(); $template = $request->attributes->get('template'); $body = $app['twig']->render($template, $view); $e->setResponse(new Response($body)); });
  128. $app->on(KernelEvents::VIEW, function ($e) use ($app) { $view = $event->getControllerResult(); if

    (!is_array($view)) { return; } $request = $event->getRequest(); // user.show.html.twig $template = $request->attributes->get('_route'); $template.= ".html.twig"; $body = $app['twig']->render($template, $view); $event->setResponse(new Response($body)); });
  129. Summary •Treat a Silex app like any other app •Pimple

    requires care when you have a lot of services •Namespace and name everything •Lazyness for as much as possible •Silex’ API is narrow, scope widens when you dig around inside •Silex has to do a lot of work for every request, grows linearly with your app
  130. @davedevelopment dave@atst.io joind.in/10374 #silex-php Questions?

  131. Further Reading https://igor.io/2013/09/02/how-heavy-is- silex.html http://srcmvn.com/blog/2013/03/08/silex- service-providers-and-controller-providers- what-is-safe-to-do-where/ https://igor.io/2012/11/09/scaling-silex.html

  132. Honorary mention /dcousineau/orlex