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.

23d971deeb3975a7d28246192fbbe7b7?s=128

Beau Simensen

November 16, 2013
Tweet

More Decks by Beau Simensen

Other Decks in Programming

Transcript

  1. Madison PHP November 16th, 2013 Writing Silex Service Providers and

    Controller Providers
  2. @beausimensen simensen on Freenode #silex-php #madisonphp

  3. None
  4. What Is Silex?

  5. What Is Silex? Pimple Container + Symfony Router

  6. What Is Silex? $app = new Silex\Application(); $app->get('/hello/{name}', function($name) use($app)

    { return 'Hello '.$app->escape($name); }); $app->run();
  7. Pimple Dependency Injection Container

  8. $container = new Pimple(); Pimple

  9. Parameters

  10. $container['cookie_name'] = 'SESSION_ID'; $container['session_storage_class'] = 'SessionStorage'; Parameters

  11. Services Created every time they are requested. (Objects)

  12. 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']); };
  13. 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']); };
  14. 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);
  15. Shared Services Created the first time they are requested. (Cached)

  16. 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']); });
  17. 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']); });
  18. You Almost Always Want Shared Services

  19. Pimple 2 Which is why the default behavior in Pimple

    2 is currently to create shared services. (Pimple 2.x API is still unstable.)
  20. 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']); });
  21. Consistency We’ll use Pimple 1.x for the rest of our

    examples. (But since 2.x is a thing now, important to know.)
  22. Extending Services Run additional code around a service. (Decorate)

  23. Extending Services $c['mail'] = function ($c) { return new \Zend_Mail();

    };
  24. 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; });
  25. 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'];
  26. 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'];
  27. Extending Services If you extend a shared service you should

    share it again.
  28. 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; }));
  29. Extending Services Decorating services.

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

    EventDispatcher(); });
  31. 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(); });
  32. 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'];
  33. 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);
  34. 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); } ));
  35. 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;
  36. Protecting Parameters Because sometimes you need a callback as a

    parameter.
  37. Protecting Parameters $container['r'] = $container->protect(function ($max = 10) { return

    rand(0, $max); });
  38. Protecting Parameters $container['r'](); $container['r'] = $container->protect(function ($max = 10) {

    return rand(0, $max); });
  39. Protecting Parameters $container['r'](); $container['r'] = $container->protect(function ($max = 10) {

    return rand(0, $max); }); $container['r'](100);
  40. Definition Ordering Services and properties can be overwritten at any

    time. To some degree ordering is not important.
  41. Definition Ordering You cannot extend a service that has not

    been defined.
  42. Definition Ordering Overwriting services and properties after they have been

    accessed can lead to unpredictable behavior. (Pimple 2 “freezes” services after they are accessed.)
  43. Laziness Is Important

  44. Silex

  45. Silex\Application Extends Pimple

  46. A Silex Application Is Its Own Container

  47. • Wires Up Symfony Components • Adds Helpful Tools •

    Has a Lifecycle • Provides Extension Points What Does Silex’s Application Do?
  48. • $app->get() • $app->post() • $app->put() • $app->delete() • $app->match()

    Routing Tools
  49. • $app->before() — before controller • $app->after() — after controller

    • $app->finish() — after response sent • $route->before() — before controller • $route->after() — after controller Middleware Tools
  50. Service Providers and Controller Providers

  51. Service Providers

  52. Service Providers The responsibility of a service provider is to

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

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

    function register(\Pimple $container); // Configure the application and *carefully* use services. public function boot(Application $app); }
  55. 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.)
  56. 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;
  57. Consistency We’ll use Silex 1.x for the rest of our

    examples. (But since 2.x is a thing now, important to know.)
  58. 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. } }
  59. register(Application $app) Setting parameters, defining new services, or extending existing

    services.
  60. register(Application $app) If you can’t do it with Pimple, you

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

    Application. This means no get(), post(), mount(), before(), etc.
  62. boot(Application $app) Dynamic configuration using services.

  63. 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.
  64. boot(Application $app) Called after all service providers are registered.

  65. boot(Application $app) Safe to access services at this point but

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

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

    });   $app['whizbang.thingy']  =  $app-­‐>share(function($app)  {        return  new  Whizbang\Thingy($app['whirligig']); }); Laziness
  68. 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.
  69. 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.
  70. Laziness // This is a BAD example. $app['some.service']->addThing($app['another.service']);

  71. 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']);
  72. Service Provider Registering Another Service Provider

  73. class MyServiceProvider implements ServiceProviderInterface { public function boot(Application $app) {

    } public function register(Application $app) { // This is probably not wise. $app->register(new DoctrineServiceProvider); } }
  74. Why Not? Hidden dependencies.

  75. 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'];                });        } }
  76. 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?
  77. Alternative?

  78. Alternative? Document that the user needs to register the Doctrine

    service provider themselves.
  79. 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.
  80. “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
  81. Controller Providers

  82. 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.
  83. Controller Providers Silex applications “mount” the controller collection to a

    specific path.
  84. namespace Silex; interface ControllerProviderInterface { // Create a controller collection

    public function connect(Application $app); }
  85. 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; } }
  86. 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());
  87. 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
  88. Mount() in Boot()?

  89. 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.
  90. Implementing Both Interfaces Because interfaces work that way. :)

  91. 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 } }
  92. 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);
  93. Controllers As A Service

  94. Controllers As A Service Dependency Injection > Service Locator

  95. Controllers As A Service Framework independence.

  96. Controllers As A Service // Dependency injection $app['myapp.hellocontroller'] = $app->share(function()

    use ($app) { return new MyApp\HelloController($app['some.service']); });
  97. 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); });
  98. Bringing These Concepts Together By implementing the service provider interface,

    implementing the controller provider interface, and using controllers as a service...
  99. 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; } }
  100. Laziness

  101. Laziness Laziness, laziness, laziness...

  102. #silex-php Seriously, come join us if you want to discuss

    awesome things and learn Silex best practices!
  103. Questions? https://joind.in/10056 @beausimensen