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. Madison PHP
    November 16th, 2013
    Writing Silex Service Providers
    and Controller Providers

    View Slide

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

    View Slide

  3. View Slide

  4. What Is Silex?

    View Slide

  5. What Is Silex?
    Pimple Container + Symfony Router

    View Slide

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

    View Slide

  7. Pimple
    Dependency Injection Container

    View Slide

  8. $container = new Pimple();
    Pimple

    View Slide

  9. Parameters

    View Slide

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

    View Slide

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

    View Slide

  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']);
    };

    View Slide

  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']);
    };

    View Slide

  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);

    View Slide

  15. Shared Services
    Created the first time they are requested.
    (Cached)

    View Slide

  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']);
    });

    View Slide

  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']);
    });

    View Slide

  18. You Almost Always
    Want Shared Services

    View Slide

  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.)

    View Slide

  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']);
    });

    View Slide

  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.)

    View Slide

  22. Extending Services
    Run additional code around a service.
    (Decorate)

    View Slide

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

    View Slide

  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;
    });

    View Slide

  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'];

    View Slide

  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'];

    View Slide

  27. Extending Services
    If you extend a shared service you should share it again.

    View Slide

  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;
    }));

    View Slide

  29. Extending Services
    Decorating services.

    View Slide

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

    View Slide

  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();
    });

    View Slide

  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'];

    View Slide

  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);

    View Slide

  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);
    }
    ));

    View Slide

  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;

    View Slide

  36. Protecting Parameters
    Because sometimes you need a callback as a parameter.

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  40. Definition Ordering
    Services and properties can be overwritten at any time.
    To some degree ordering is not important.

    View Slide

  41. Definition Ordering
    You cannot extend a service that has not been defined.

    View Slide

  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.)

    View Slide

  43. Laziness Is Important

    View Slide

  44. Silex

    View Slide

  45. Silex\Application
    Extends Pimple

    View Slide

  46. A Silex Application Is
    Its Own Container

    View Slide

  47. • Wires Up Symfony Components
    • Adds Helpful Tools
    • Has a Lifecycle
    • Provides Extension Points
    What Does Silex’s
    Application Do?

    View Slide

  48. • $app->get()
    • $app->post()
    • $app->put()
    • $app->delete()
    • $app->match()
    Routing Tools

    View Slide

  49. • $app->before() — before controller
    • $app->after() — after controller
    • $app->finish() — after response sent
    • $route->before() — before controller
    • $route->after() — after controller
    Middleware Tools

    View Slide

  50. Service Providers and
    Controller Providers

    View Slide

  51. Service Providers

    View Slide

  52. Service Providers
    The responsibility of a service provider is to configure
    the container. This means defining services.
    (Booting is kinda tricky...)

    View Slide

  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);
    }

    View Slide

  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);
    }

    View Slide

  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.)

    View Slide

  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;

    View Slide

  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.)

    View Slide

  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.
    }
    }

    View Slide

  59. register(Application $app)
    Setting parameters, defining new services, or extending
    existing services.

    View Slide

  60. register(Application $app)
    If you can’t do it with Pimple, you probably shouldn’t be
    doing it in register()!

    View Slide

  61. register(Application $app)
    Forget about anything else you can do with Application.
    This means no get(), post(), mount(), before(), etc.

    View Slide

  62. boot(Application $app)
    Dynamic configuration using services.

    View Slide

  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.

    View Slide

  64. boot(Application $app)
    Called after all service providers are registered.

    View Slide

  65. boot(Application $app)
    Safe to access services at this point but keep in mind
    those services will be defined on every request.

    View Slide

  66. Laziness
    Services should be lazy. If a service is accessed before
    run(), you’re probably doing it wrong.

    View Slide

  67. $app['whirligig']  =  $app-­‐>share(function()  {
           return  new  Whirligig;
    });
     
    $app['whizbang.thingy']  =  $app-­‐>share(function($app)  {
           return  new  Whizbang\Thingy($app['whirligig']);
    });
    Laziness

    View Slide

  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.

    View Slide

  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.

    View Slide

  70. Laziness
    // This is a BAD example.
    $app['some.service']->addThing($app['another.service']);

    View Slide

  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']);

    View Slide

  72. Service Provider
    Registering Another
    Service Provider

    View Slide

  73. class MyServiceProvider implements ServiceProviderInterface
    {
    public function boot(Application $app)
    {
    }
    public function register(Application $app)
    {
    // This is probably not wise.
    $app->register(new DoctrineServiceProvider);
    }
    }

    View Slide

  74. Why Not?
    Hidden dependencies.

    View Slide

  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'];
                   });
           }
    }

    View Slide

  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?

    View Slide

  77. Alternative?

    View Slide

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

    View Slide

  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.

    View Slide

  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

    View Slide

  81. Controller Providers

    View Slide

  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.

    View Slide

  83. Controller Providers
    Silex applications “mount” the controller
    collection to a specific path.

    View Slide

  84. namespace Silex;
    interface ControllerProviderInterface
    {
    // Create a controller collection
    public function connect(Application $app);
    }

    View Slide

  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;
    }
    }

    View Slide

  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());

    View Slide

  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

    View Slide

  88. Mount() in Boot()?

    View Slide

  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.

    View Slide

  90. Implementing Both
    Interfaces
    Because interfaces work that way. :)

    View Slide

  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
    }
    }

    View Slide

  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);

    View Slide

  93. Controllers As A
    Service

    View Slide

  94. Controllers As A
    Service
    Dependency Injection > Service Locator

    View Slide

  95. Controllers As A
    Service
    Framework independence.

    View Slide

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

    View Slide

  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);
    });

    View Slide

  98. Bringing These
    Concepts Together
    By implementing the service provider interface,
    implementing the controller provider interface, and
    using controllers as a service...

    View Slide

  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;
    }
    }

    View Slide

  100. Laziness

    View Slide

  101. Laziness
    Laziness, laziness, laziness...

    View Slide

  102. #silex-php
    Seriously, come join us if you want to discuss awesome
    things and learn Silex best practices!

    View Slide

  103. Questions?
    https://joind.in/10056
    @beausimensen

    View Slide