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

Writing Silex Service Providers and Controller Providers (Madison PHP)

Writing Silex Service Providers and Controller Providers (Madison PHP)

So you've gotten to the point in your Silex application that you want to start breaking it out into modular pieces. Silex service providers and controller providers to the rescue! These interfaces appear to be really simple but do you know which things can safely be done in each method?

Find out the intended purpose for each interface and which operations should be done (or avoided) in each of their methods. Get a quick tour of Silex and Pimple and learn about why laziness is so important when writing code for Silex. By following some best practices you can avoid headaches for both you and your users.

Beau Simensen

November 16, 2013
Tweet

More Decks by Beau Simensen

Other Decks in Programming

Transcript

  1. Services $container['cookie_name'] = 'SESSION_ID'; $container['session_storage_class'] = 'SessionStorage'; $container['session_storage']  =  function

     ($c)  {        return  new  $c['session_storage_class']($c['cookie_name']); }; $container['session']  =  function  ($c)  {        return  new  Session($c['session_storage']); };
  2. Services $session  =  $container['session']; $container['cookie_name'] = 'SESSION_ID'; $container['session_storage_class'] = 'SessionStorage';

    $container['session_storage']  =  function  ($c)  {        return  new  $c['session_storage_class']($c['cookie_name']); }; $container['session']  =  function  ($c)  {        return  new  Session($c['session_storage']); };
  3. Services $session  =  $container['session']; $container['cookie_name'] = 'SESSION_ID'; $container['session_storage_class'] = 'SessionStorage';

    $container['session_storage']  =  function  ($c)  {        return  new  $c['session_storage_class']($c['cookie_name']); }; $container['session']  =  function  ($c)  {        return  new  Session($c['session_storage']); }; //  $storage  =  new  SessionStorage('SESSION_ID'); //  $session  =  new  Session($storage);
  4. Shared Services $container['cookie_name'] = 'SESSION_ID'; $container['session_storage_class'] = 'SessionStorage'; $container['session_storage']  =

     $container-­‐>share(function  ($c)  {        return  new  $c['session_storage_class']($c['cookie_name']); }); $container['session']  =  $container-­‐>share(function  ($c)  {        return  new  Session($c['session_storage']); });
  5. Shared Services $container['cookie_name'] = 'SESSION_ID'; $container['session_storage_class'] = 'SessionStorage'; $container['session_storage']  =

     $container-­‐>share(function  ($c)  {        return  new  $c['session_storage_class']($c['cookie_name']); }); $container['session']  =  $container-­‐>share(function  ($c)  {        return  new  Session($c['session_storage']); });
  6. Pimple 2 Which is why the default behavior in Pimple

    2 is currently to create shared services. (Pimple 2.x API is still unstable.)
  7. Pimple 2 //  Shared  services $container['session_storage']  =  function  ($c)  {

           return  new  SessionStorage($c['cookie_name']); }; $container['session']  =  function  ($c)  {        return  new  Session($c['session_storage']); }; //  Factory  services $container['session_storage']  =  $container-­‐>factory(function  ($c)  {        return  new  SessionStorage($c['cookie_name']); }); $container['session']  =  $container-­‐>factory(function  ($c)  {        return  new  Session($c['session_storage']); });
  8. Consistency We’ll use Pimple 1.x for the rest of our

    examples. (But since 2.x is a thing now, important to know.)
  9. Extending Services $c['mail'] = function ($c) { return new \Zend_Mail();

    }; $c['mail'] = $c->extend('mail', function($mail, $c) { $mail->setFrom($c['mail.default_from']); return $mail; });
  10. Extending Services $c['mail'] = function ($c) { return new \Zend_Mail();

    }; $c['mail'] = $c->extend('mail', function($mail, $c) { $mail->setFrom($c['mail.default_from']); return $mail; }); $mail  =  $c['mail'];
  11. Extending Services $c['mail'] = function ($c) { return new \Zend_Mail();

    }; $c['mail'] = $c->extend('mail', function($mail, $c) { $mail->setFrom($c['mail.default_from']); return $mail; }); //  $mail  =  new  \Zend_Mail(); //  $mail-­‐>setFrom(‘...’); $mail  =  $c['mail'];
  12. Extending Services $c['mail'] = $c->share(function ($c) { return new \Zend_Mail();

    }); $c['mail'] = $c->share($c->extend('mail', function($mail, $c) { $mail->setFrom($c['mail.default_from']); return $mail; }));
  13. Extending Services $c['dispatcher'] = $c->share( $c->extend('dispatcher', function($dispatcher, $c) { return

    new PimpleAwareEventDispatcher($dispatcher, $c); } )); use Symfony\Component\EventDispatcher\EventDispatcher; $c['dispatcher'] = $c->share(function($c) { return new EventDispatcher(); });
  14. Extending Services $c['dispatcher'] = $c->share( $c->extend('dispatcher', function($dispatcher, $c) { return

    new PimpleAwareEventDispatcher($dispatcher, $c); } )); use Symfony\Component\EventDispatcher\EventDispatcher; $c['dispatcher'] = $c->share(function($c) { return new EventDispatcher(); }); $dispatcher  =  $c['dispatcher'];
  15. Extending Services $c['dispatcher'] = $c->share( $c->extend('dispatcher', function($dispatcher, $c) { return

    new PimpleAwareEventDispatcher($dispatcher, $c); } )); use Symfony\Component\EventDispatcher\EventDispatcher; $c['dispatcher'] = $c->share(function($c) { return new EventDispatcher(); }); $dispatcher  =  $c['dispatcher']; //  $dispatcher  =  new  EventDispatcher(); //  $dispatcher  =  new  PimpleAwareEventDispatcher($dispatcher,  $c);
  16. Extending Services $c['dispatcher'] = $c['pimple_aware_dispatcher'] = $c->share( $c->extend('dispatcher', function($dispatcher, $c)

    { return new PimpleAwareEventDispatcher($dispatcher, $c); } )); $c['dispatcher'] = $c->share( $c->extend('dispatcher', function($dispatcher, $c) { return new PimpleAwareEventDispatcher($dispatcher, $c); } ));
  17. Extending Services $c['dispatcher'] = $c['pimple_aware_dispatcher'] = $c->share( $c->extend('dispatcher', function($dispatcher, $c)

    { return new PimpleAwareEventDispatcher($dispatcher, $c); } )); $c['dispatcher'] = $c->share( $c->extend('dispatcher', function($dispatcher, $c) { return new PimpleAwareEventDispatcher($dispatcher, $c); } )); //  $dispatcher  =  new  EventDispatcher(); //  $pimpleAware  =  new  PimpleAwareEventDispatcher($dispatcher,  $c); //  $dispatcher  =  $pimpleAware;
  18. Definition Ordering Services and properties can be overwritten at any

    time. To some degree ordering is not important.
  19. Definition Ordering Overwriting services and properties after they have been

    accessed can lead to unpredictable behavior. (Pimple 2 “freezes” services after they are accessed.)
  20. • Wires Up Symfony Components • Adds Helpful Tools •

    Has a Lifecycle • Provides Extension Points What Does Silex’s Application Do?
  21. • $app->before() — before controller • $app->after() — after controller

    • $app->finish() — after response sent • $route->before() — before controller • $route->after() — after controller Middleware Tools
  22. Service Providers The responsibility of a service provider is to

    configure the container. This means defining services. (Booting is kinda tricky...)
  23. namespace Silex; interface ServiceProviderInterface { // Define services here. public

    function register(Application $app); // Configure the application and *carefully* use services. public function boot(Application $app); }
  24. namespace Silex; interface ServiceProviderInterface { // Define services here. public

    function register(\Pimple $container); // Configure the application and *carefully* use services. public function boot(Application $app); }
  25. Silex 2 In Silex 2, service providers and bootable providers

    are broken out and service providers register against Pimple and not a Silex Application. (Silex 2.x API is still unstable.)
  26. interface ServiceProviderInterface { // Define services here. public function register(\Pimple

    $app); } use Silex\Application; interface BootableProviderInterface { // Configure the application and *carefully* use services. public function boot(Application $app); } namespace Silex\Api;
  27. Consistency We’ll use Silex 1.x for the rest of our

    examples. (But since 2.x is a thing now, important to know.)
  28. namespace Acme\AwesomePackage; use Silex\Application; use Silex\ServiceProviderInterface; class WhizbangServiceProvider implements ServiceProviderInterface

    { public function register(Application $app) { // Define services here. } public function boot(Application $app) { // Configure the application and *carefully* use services. } }
  29. register(Application $app) If you can’t do it with Pimple, you

    probably shouldn’t be doing it in register()!
  30. register(Application $app) Forget about anything else you can do with

    Application. This means no get(), post(), mount(), before(), etc.
  31. boot(Application $app) Safe to use Application specific methods like before()

    and after(). Technically it should be OK to call mount() but more on that later.
  32. boot(Application $app) Safe to access services at this point but

    keep in mind those services will be defined on every request.
  33. Laziness Services should be lazy. If a service is accessed

    before run(), you’re probably doing it wrong.
  34. $app['whirligig']  =  $app-­‐>share(function()  {        return  new  Whirligig;

    });   $app['whizbang.thingy']  =  $app-­‐>share(function($app)  {        return  new  Whizbang\Thingy($app['whirligig']); }); Laziness
  35. Laziness It is extremely important to pay attention to laziness

    in service provider register() methods. Doing it wrong means at best services will be instantiated every request. At worst, it can result in unpredictable behavior that can be hard to track down.
  36. Laziness General rule of thumb: If $app or $container are

    passed into a closure or are provided as function arguments, your code is likely lazy.
  37. Laziness // This is a GOOD example. It accomplishes the

    same thing as // the bad example but in a way that is lazy. $app['some.service'] = $app->share( $app->extend( 'some.service', function($someService, $app) { // Note: we are in a closure and $app was passed // to the function; an indicator that this is // properly lazy code! $someService->addThing($app['another.service']); return $someService; } ) ); // This is a BAD example. $app['some.service']->addThing($app['another.service']);
  38. class MyServiceProvider implements ServiceProviderInterface { public function boot(Application $app) {

    } public function register(Application $app) { // This is probably not wise. $app->register(new DoctrineServiceProvider); } }
  39. use  Silex\Application; use  Silex\Provider\DoctrineServiceProvider; use  Silex\ServiceProviderInterface;   class  MyServiceProvider  implements

     ServiceProviderInterface {        public  function  boot(Application  $app)        {        }          public  function  register(Application  $app)        {                //  This  is  probably  not  wise.                $app-­‐>register(new  DoctrineServiceProvider);                  $app['myapp.someservice']  =  $app-­‐>share(function  ()  use  ($app)  {                        //  do  something  with  $app['db'];                });        } }
  40. Why Not? What if the user wants to register it

    themselves? What if another service provider registers it? How will the user know to configure it?
  41. Better Yet? Document exactly what they need, namely $app[‘db’] to

    be available and that it needs to be an instance of Doctrine DBAL Connection.
  42. “Currently requires both dbs and dbs.event_manager services in order to

    work. These can be provided by a Doctrine Service Provider like the Silex or Cilex service providers. If you can or want to fake it, go for it. :)” — Doctrine ORM Service Provider
  43. Controller Providers The responsibility of a controller provider is to

    wire up controllers. This means that services should not be defined in a controller provider.
  44. use Silex\Application; use Silex\ControllerProviderInterface; class DemoControllerProvider implements ControllerProviderInterface { public

    function connect(Application $app) { $controllers = $app['controllers_factory']; $controllers->get('/', function () { return "hello"; }); $controllers->get('/say/{what}', function ($what) { return $what; }); return $controllers; } }
  45. use Silex\Application; use Silex\ControllerProviderInterface; class DemoControllerProvider implements ControllerProviderInterface { public

    function connect(Application $app) { $controllers = $app['controllers_factory']; $controllers->get('/', function () { return "hello"; }); $controllers->get('/say/{what}', function ($what) { return $what; }); return $controllers; } } $app = new Silex\Application(); $app->mount('/demo', new DemoControllerProvider());
  46. use Silex\Application; use Silex\ControllerProviderInterface; class DemoControllerProvider implements ControllerProviderInterface { public

    function connect(Application $app) { $controllers = $app['controllers_factory']; $controllers->get('/', function () { return "hello"; }); $controllers->get('/say/{what}', function ($what) { return $what; }); return $controllers; } } $app = new Silex\Application(); $app->mount('/demo', new DemoControllerProvider()); //  http://example.com/demo  -­‐>  hello //  http://example.com/demo/say/bonjour  -­‐>  bonjour
  47. Mount() in Boot()? Technically yes, but probably not the best

    idea. This is similar to the idea registering a service provider in register(). By mount()ing in boot(), you are obscuring important details from your user and limiting their choice and control.
  48. class MyAppControllersProvider implements ServiceProviderInterface, ControllerProviderInterface { function boot(Application $app) {

    // Configure the application and *carefully* use services. } function register(Application $app) { // Define services here. } public function connect(Application $app) { // Create a controller collection } }
  49. class MyAppControllersProvider implements ServiceProviderInterface, ControllerProviderInterface { function boot(Application $app) {

    // Configure the application and *carefully* use services. } function register(Application $app) { // Define services here. } public function connect(Application $app) { // Create a controller collection } } $myAppControllersProvider  =  new  MyAppControllersProvider(); $app-­‐>register($myAppControllersProvider); $app-­‐>mount('/myapp',  $myAppControllersProvider);
  50. Controllers As A Service // Dependency injection $app['myapp.hellocontroller'] = $app->share(function()

    use ($app) { return new MyApp\HelloController($app['some.service']); });
  51. Controllers As A Service // Dependency injection $app['myapp.hellocontroller'] = $app->share(function()

    use ($app) { return new MyApp\HelloController($app['some.service']); }); // Service locator $app['myapp.hellocontroller'] = $app->share(function() use ($app) { return new MyApp\HelloController($app); });
  52. Bringing These Concepts Together By implementing the service provider interface,

    implementing the controller provider interface, and using controllers as a service...
  53. class MyAppControllersProvider implements ServiceProviderInterface, ControllerProviderInterface { function boot(Application $app) {

    } function register(Application $app) { // Define controller services $app['myapp.hellocontroller'] = $app->share(function() use ($app) { return new MyApp\Controller\HelloController($app['some.service']); }); } public function connect(Application $app) { $controllers = $app['controllers_factory']; // Define routing referring to controller services $controllers->get('/hello/{name}', 'myapp.hellocontroller:say') ->method('GET') ->bind('myapp.hello'); return $controllers; } }
  54. #silex-php Seriously, come join us if you want to discuss

    awesome things and learn Silex best practices!