Share
Embed
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
Transcript
103 slides
-
Madison PHP November 16th, 2013 Writing Silex Service Providers and
Controller Providers -
What Is Silex? $app = new Silex\Application(); $app->get('/hello/{name}', function($name) use($app)
{ return 'Hello '.$app->escape($name); }); $app->run(); -
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']); }; -
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']); }; -
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); -
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']); }); -
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']); }); -
Pimple 2 Which is why the default behavior in Pimple
2 is currently to create shared services. (Pimple 2.x API is still unstable.) -
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']); }); -
Consistency We’ll use Pimple 1.x for the rest of our
examples. (But since 2.x is a thing now, important to know.) -
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; }); -
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']; -
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']; -
Extending Services If you extend a shared service you should
share it again. -
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; })); -
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(); }); -
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']; -
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); -
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); } )); -
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; -
Protecting Parameters $container['r'](); $container['r'] = $container->protect(function ($max = 10) {
return rand(0, $max); }); -
Protecting Parameters $container['r'](); $container['r'] = $container->protect(function ($max = 10) {
return rand(0, $max); }); $container['r'](100); -
Definition Ordering Services and properties can be overwritten at any
time. To some degree ordering is not important. -
Definition Ordering Overwriting services and properties after they have been
accessed can lead to unpredictable behavior. (Pimple 2 “freezes” services after they are accessed.) -
• Wires Up Symfony Components • Adds Helpful Tools •
Has a Lifecycle • Provides Extension Points What Does Silex’s Application Do? -
• $app->before() — before controller • $app->after() — after controller
• $app->finish() — after response sent • $route->before() — before controller • $route->after() — after controller Middleware Tools -
Service Providers The responsibility of a service provider is to
configure the container. This means defining services. (Booting is kinda tricky...) -
namespace Silex; interface ServiceProviderInterface { // Define services here. public
function register(Application $app); // Configure the application and *carefully* use services. public function boot(Application $app); } -
namespace Silex; interface ServiceProviderInterface { // Define services here. public
function register(\Pimple $container); // Configure the application and *carefully* use services. public function boot(Application $app); } -
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.) -
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; -
Consistency We’ll use Silex 1.x for the rest of our
examples. (But since 2.x is a thing now, important to know.) -
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. } } -
register(Application $app) If you can’t do it with Pimple, you
probably shouldn’t be doing it in register()! -
register(Application $app) Forget about anything else you can do with
Application. This means no get(), post(), mount(), before(), etc. -
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. -
boot(Application $app) Safe to access services at this point but
keep in mind those services will be defined on every request. -
Laziness Services should be lazy. If a service is accessed
before run(), you’re probably doing it wrong. -
$app['whirligig'] = $app-‐>share(function() { return new Whirligig;
}); $app['whizbang.thingy'] = $app-‐>share(function($app) { return new Whizbang\Thingy($app['whirligig']); }); Laziness -
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. -
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. -
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']); -
class MyServiceProvider implements ServiceProviderInterface { public function boot(Application $app) {
} public function register(Application $app) { // This is probably not wise. $app->register(new DoctrineServiceProvider); } } -
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']; }); } } -
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? -
Alternative? Document that the user needs to register the Doctrine
service provider themselves. -
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. -
“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 -
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. -
namespace Silex; interface ControllerProviderInterface { // Create a controller collection
public function connect(Application $app); } -
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; } } -
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()); -
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 -
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. -
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 } } -
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); -
Controllers As A Service // Dependency injection $app['myapp.hellocontroller'] = $app->share(function()
use ($app) { return new MyApp\HelloController($app['some.service']); }); -
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); }); -
Bringing These Concepts Together By implementing the service provider interface,
implementing the controller provider interface, and using controllers as a service... -
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; } } -
#silex-php Seriously, come join us if you want to discuss
awesome things and learn Silex best practices!