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 Slide

  2. @davedevelopment
    childcare.co.uk

    View Slide

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

    View Slide

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

    View Slide

  5. Silex

    View Slide

  6. Organised Chaos

    View Slide

  7. Intro
    Best Practices
    Conventions
    Extensions
    Performance

    View Slide

  8. Don’t listen to me

    View Slide

  9. Pimple
    A simple Dependency Injection
    Container for PHP 5.3

    View Slide

  10. Silex
    Pimple

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  15. Lazyness
    The lazy way of spelling
    "laziness"

    View Slide

  16. The Problem

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  20. // 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 Slide

  21. Theoretical Lifecycle

    View Slide

  22. Compile Time
    Defining Services

    View Slide

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

    View Slide

  24. Compile Time
    Extending Services

    View Slide

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

    View Slide

  26. Runtime
    Accessing Services

    View Slide

  27. Do not redefine services
    once they have been
    accessed

    View Slide

  28. Pimple 2.0
    Services are frozen once
    accessed

    View Slide

  29. Performance
    Everything adds up

    View Slide

  30. Lazyness helps a lot...

    View Slide

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

    View Slide

  32. app/console cache:clear

    View Slide

  33. Pimple 2.0
    Major performance
    improvements

    View Slide

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

    View Slide

  35. $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 Slide

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

    View Slide

  37. Auto-wiring

    View Slide

  38. 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 Slide

  39. // 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

  43. 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 Slide

  44. 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 Slide

  45. 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 Slide

  46. 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 Slide

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

    View Slide

  48. Silex
    HttpKernelInterface
    {abstract}
    HttpKernel
    Routing EventDispatcher
    HttpFoundation

    View Slide

  49. Service Providers

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  53. File Structure

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  57. > 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 Slide

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

    View Slide

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

    View Slide

  60. Writing Service Providers

    View Slide

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

    View Slide

  62. register() is for registering
    parameters and services

    View Slide

  63. Do register parameters
    preferably defaults

    View Slide

  64. Do register services

    View Slide

  65. Do extend services

    View Slide

  66. Don’t access services

    View Slide

  67. Don’t do anything else

    View Slide

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

    View Slide

  69. Generally acceptable to
    access services here
    Try not to

    View Slide

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

    View Slide

  71. Controller Providers

    View Slide

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

    View Slide

  73. // 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 Slide

  74. Modules
    Bundles, Packages...

    View Slide

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

    View Slide

  76. 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 Slide

  77. // 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 Slide

  78. Controllers

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  82. Anonymous Functions

    View Slide

  83. Named Functions

    View Slide

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

    View Slide

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

    View Slide

  86. Controllers as Classes

    View Slide

  87. // 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 Slide

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

    View Slide

  89. Injecting the container
    BaseController and helpers

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  94. 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 Slide

  95. 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 Slide

  96. 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 Slide

  97. Controllers as Services

    View Slide

  98. 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 Slide

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

  100. 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 Slide

  101. // 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 Slide

  102. // 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 Slide

  103. Resource based Routing
    RAD

    View Slide

  104. 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 Slide

  105. 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 Slide

  106. Performance
    Everything adds up

    View Slide

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

    View Slide

  108. 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 Slide

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

    View Slide

  110. View Slide

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

    View Slide

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

    View Slide

  113. Event Listeners

    View Slide

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

    View Slide

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

    View Slide

  116. Performance
    Lazyness

    View Slide

  117. 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 Slide

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

    View Slide

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

    View Slide

  120. $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 Slide

  121. View Renderers

    View Slide

  122. // 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 Slide

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

  124. // 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 Slide

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

  126. $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 Slide

  127. $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 Slide

  128. $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 Slide

  129. 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 Slide

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

    View Slide

  131. 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 Slide

  132. Honorary mention
    /dcousineau/orlex

    View Slide