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

Taming Runaway Silex Apps - SymfonyCon Warsaw 2013

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.

Dave Marshall

December 13, 2013
Tweet

More Decks by Dave Marshall

Other Decks in Programming

Transcript

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

    View full-size slide

  2. @davedevelopment
    childcare.co.uk

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  5. Organised Chaos

    View full-size slide

  6. Intro
    Best Practices
    Conventions
    Extensions
    Performance

    View full-size slide

  7. Don’t listen to me

    View full-size slide

  8. Pimple
    A simple Dependency Injection
    Container for PHP 5.3

    View full-size slide

  9. $app['kernel'] = function ($app) {
    return new HttpKernel(
    $app['dispatcher'],
    $app['resolver'],
    $app['request_stack']
    );
    };

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  12. Namespace everything
    $app['twig'] = ...
    $app['twig.loader'] = ...
    $app['twig.loader.filesystem'] = ...

    View full-size slide

  13. Lazyness
    The lazy way of spelling
    "laziness"

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  16. // user.service is accessed
    $app['user.service']->setOption('debug', true);

    View full-size slide

  17. // 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']-> ....

    View full-size slide

  18. Theoretical Lifecycle

    View full-size slide

  19. Compile Time
    Defining Services

    View full-size slide

  20. Do not access any services
    until all services have been
    registered

    View full-size slide

  21. Compile Time
    Extending Services

    View full-size slide

  22. Do not access any services
    until all services have been
    extended as required

    View full-size slide

  23. Runtime
    Accessing Services

    View full-size slide

  24. Do not redefine services
    once they have been
    accessed

    View full-size slide

  25. Pimple 2.0
    Services are frozen once
    accessed

    View full-size slide

  26. Performance
    Everything adds up

    View full-size slide

  27. Lazyness helps a lot...

    View full-size slide

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

    View full-size slide

  29. app/console cache:clear

    View full-size slide

  30. Pimple 2.0
    Major performance
    improvements

    View full-size slide

  31. Not everything has to be
    defined as a service
    Extract when necessary

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  35. // 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'
    );

    View full-size slide

  36. Silex
    The PHP micro-framework
    based on the Symfony2
    Components

    View full-size slide

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

    View full-size slide

  38. Silex exposes an intuitive
    and concise API that is fun
    to use

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  43. Silex defines a range of
    services which can be used
    or replaced
    You probably don't want to
    mess with most of them

    View full-size slide

  44. Silex
    HttpKernelInterface
    {abstract}
    HttpKernel
    Routing EventDispatcher
    HttpFoundation

    View full-size slide

  45. Service Providers

    View full-size slide

  46. $app = new Silex\Application;
    $app->register(
    new Silex\Provider\TwigServiceProvider(),
    [
    'twig.path' => __DIR__.'/views',
    ]
    );

    View full-size slide

  47. symfony/security
    symfony/form
    symfony/translation
    symfony/validator
    twig/twig
    doctrine/dbal
    swiftmailer/swiftmailer
    monolog/monolog

    View full-size slide

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

    View full-size slide

  49. File Structure

    View full-size slide

  50. web/ # assets + front controllers
    app/ # app config/bootstrap
    src/ # app code
    bin/ # cli scripts + binstubs

    View full-size slide

  51. app/
    config/
    views/
    app.php
    controllers.php # optional

    View full-size slide

  52. require __DIR__.'/../vendor/autoload.php';
    $app = new Silex\Application();
    // config...
    // services
    $app['user.service'] = function() {};
    // routes
    $app->get('/', function() { return 'Hello'; }
    return $app;

    View full-size slide

  53. > 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(...

    View full-size slide

  54. web/
    assets/
    index.php
    index_dev.php # optional

    View full-size slide

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

    View full-size slide

  56. Writing Service Providers

    View full-size slide

  57. namespace Silex;
    interface ServiceProviderInterface
    {
    public function register(Application $app);
    public function boot(Application $app);
    }

    View full-size slide

  58. register() is for registering
    parameters and services

    View full-size slide

  59. Do register parameters
    preferably defaults

    View full-size slide

  60. Do register services

    View full-size slide

  61. Do extend services

    View full-size slide

  62. Don’t access services

    View full-size slide

  63. Don’t do anything else

    View full-size slide

  64. boot() is for doing things
    before handling a request
    Usually adding event
    listeners

    View full-size slide

  65. Generally acceptable to
    access services here
    Try not to

    View full-size slide

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

    View full-size slide

  67. Controller Providers

    View full-size slide

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

    View full-size slide

  69. // 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());

    View full-size slide

  70. Modules
    Bundles, Packages...

    View full-size slide

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

    View full-size slide

  72. 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) {}
    }

    View full-size slide

  73. // 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);

    View full-size slide

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

    View full-size slide

  75. $app->get(
    '/hello/{name}',
    function ($name, Application $app) {
    return "Hello" . $app->escape($name);
    }
    )->bind('site.welcome');

    View full-size slide

  76. > bin/routes
    site.welcome GET /hello/{name} app/app.php:12

    View full-size slide

  77. Anonymous Functions

    View full-size slide

  78. Named Functions

    View full-size slide

  79. function welcome($name, Application $app) {
    return "Hello " . $app->escape($name);
    };
    $app->get('/hello/{name}', 'welcome')
    ->bind('site.welcome');

    View full-size slide

  80. $app = mock('Silex\Application');
    $app->shouldReceive('escape')
    ->with('Bob')
    ->andReturn('Dave');
    assertThat(
    welcome('Bob', $app),
    equalTo('Hello Dave')
    );

    View full-size slide

  81. Controllers as Classes

    View full-size slide

  82. // 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');

    View full-size slide

  83. // S\C\HttpKernel\Controller\ControllerResolver
    list($class, $method) = explode('::', $controller, 2);
    return array(new $class(), $method);

    View full-size slide

  84. Injecting the container
    BaseController and helpers

    View full-size slide

  85. // class BaseControllerResolver
    list($class, $method) = explode('::', $controller, 2);
    $controller = new $class();
    if ($controller instanceof BaseController) {
    $controller->setApplication($app);
    }
    return array($controller, $method);

    View full-size slide

  86. $app['resolver'] = $this->share($app->extend(
    'resolver',
    function ($resolver, $app) {
    return new BaseControllerResolver(
    $resolver,
    $app
    );
    }
    ));

    View full-size slide

  87. // abstract class BaseController
    public function setApplication(Application $app)
    {
    $this->app = $app;
    }
    public function get($key)
    {
    return $this->app[$key];
    }

    View full-size slide

  88. // abstract class BaseController
    public function render($template, array $context)
    {
    return $this
    ->get('twig')
    ->render($template, $context);
    }

    View full-size slide

  89. 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,
    ]
    );
    }
    }

    View full-size slide

  90. 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,
    ]
    );
    }
    }

    View full-size slide

  91. 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,
    ]
    );
    }
    }

    View full-size slide

  92. Controllers as Services

    View full-size slide

  93. 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');

    View full-size slide

  94. 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');

    View full-size slide

  95. 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');

    View full-size slide

  96. // 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
    ]
    );
    }

    View full-size slide

  97. // 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,
    ]
    );
    }

    View full-size slide

  98. Resource based Routing
    RAD

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  101. Performance
    Everything adds up

    View full-size slide

  102. Silex doesn’t use the full
    Router, only the UrlMatcher
    Routes are “recompiled”
    for every request

    View full-size slide

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

    View full-size slide

  104. if (false == getenv('SKIP_ADMIN_CONTROLLERS')) {
    $app->mount(
    '/',
    new AdminPanelControllerProvider()
    );
    }

    View full-size slide

  105. /flint/flint
    Enhancements to Silex with
    structure and conventions.

    View full-size slide

  106. Replaces the UrlMatcher
    with the full Router
    At the cost of some flexibility

    View full-size slide

  107. Event Listeners

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  110. Performance
    Lazyness

    View full-size slide

  111. 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")
    );

    View full-size slide

  112. // Silex 1.2
    $app->on(
    AppEvents::USER_UPGRADE,
    "user.event_logger:onUserEvent"
    );

    View full-size slide

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

    View full-size slide

  114. $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"
    );

    View full-size slide

  115. View Renderers

    View full-size slide

  116. // 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');

    View full-size slide

  117. // 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');

    View full-size slide

  118. // 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');

    View full-size slide

  119. $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));
    });

    View full-size slide

  120. $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));
    });

    View full-size slide

  121. $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));
    });

    View full-size slide

  122. $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));
    });

    View full-size slide

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

    View full-size slide

  124. @davedevelopment
    [email protected]
    joind.in/10374
    #silex-php
    Questions?

    View full-size slide

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

    View full-size slide

  126. Honorary mention
    /dcousineau/orlex

    View full-size slide