$30 off During Our Annual Pro Sale. View Details »

Eating spaghetti with Symfony

Eating spaghetti with Symfony

Big-bang migrations hardly ever work and usually take significantly more effort than expected. It's also hard to convince the stakeholders there's any value in the whole operation. It's much more effective to make gradual improvements. It's also more rewarding to celebrate success after every sprint. Learn how to leverage Symfony to move away from an Old School PHP Spaghetti Project™ to a modern Symfony based application. Release after the first iteration and keep improving the code base, delivering value in the same time.

Jakub Zalas

February 18, 2016
Tweet

More Decks by Jakub Zalas

Other Decks in Programming

Transcript

  1. @jakub_zalas
    @jakzal
    Jakub Zalas
    EATING SPAGHETTI WITH
    SYMFONY
    PHP UK 2016
    https://www.flickr.com/photos/stijnnieuwendijk/8839597194/in/photostream/

    View Slide

  2. https://www.flickr.com/photos/ocarchives/3952964087/
    THE PLAN
    ➤ Spaghetti/legacy/mud
    ➤ Migration strategies
    ➤ Migration cookbook
    https://flic.kr/p/72iYve

    View Slide

  3. SPAGHETTI, LEGACY, BIG BALL OF MUD
    Twisted and tangled as a bowl of spaghetti.
    Legacy code relates to a no-longer supported technology.
    Legacy code is code with no tests.
    Legacy code is source code inherited from someone else.
    A big ball of mud is haphazardly
    structured, sprawling, sloppy, duct-tape
    and bailing wire, spaghetti code jungle.

    View Slide

  4. THERE’S A BRIGHT SIDE
    ➤ it works
    ➤ it brings (or saves) money
    ➤ it’s a source of domain knowledge
    and… always look on the bright side of life

    View Slide

  5. WHY MIGRATE?
    ➤ adding features takes ages
    ➤ obsolete technology
    ➤ scaling
    https://www.flickr.com/photos/sdobie/8226246821/

    View Slide

  6. MIGRATION STRATEGIES
    Migrating Legacy Systems: Gateways, Interfaces & the Incremental Approach by Michael Stonebraker and Michael L. Brodie
    Cold Turkey Chicken Little
    vs

    View Slide

  7. COOKBOOK
    ideas to build on
    https://flic.kr/p/7EbFkK

    View Slide

  8. Write tests for any new feature, bug, or any piece of code you’re changing.
    Writing tests-first is the best way to enforce a better design,
    and eventually crawl out of legacy.
    https://flic.kr/p/DmE99L

    View Slide

  9. COMPONENTS
    taking small steps
    https://flic.kr/p/7PQ15Q

    View Slide

  10. PUTTING TWIG TO WORK
    $loader = new Twig_Loader_Filesystem(__DIR__.'/templates');


    $twig = new Twig_Environment($loader, [

    'cache' => __DIR__.'/../var/cache/legacy/twig',

    'debug' => true,

    ]);

    View Slide

  11. PUTTING TWIG TO WORK
    echo $twig->render('home.html.twig', ['foo' => 'bar']);

    View Slide

  12. LEVERAGING THE SERVICE CONTAINER - CREATING SERVICE DEFINITIONS
    $container = new ContainerBuilder();

    $container->setParameter('database_path', __DIR__.'/../db');

    $container

    ->register('db', 'PDO')

    ->addArgument('sqlite:%database_path%');


    $container

    ->register('foo_repository', 'FooRepository')

    ->addArgument(new Reference('db'));

    View Slide

  13. LEVERAGING THE SERVICE CONTAINER - CREATING SERVICE DEFINITIONS
    use Symfony\Component\DependencyInjection\ContainerBuilder;

    use Symfony\Component\Config\FileLocator;

    use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;


    $container = new ContainerBuilder();

    $loader = new YamlFileLoader($container, new FileLocator(__DIR__));

    $loader->load('services.yml');

    View Slide

  14. LEVERAGING THE SERVICE CONTAINER - CREATING SERVICE DEFINITIONS
    # services.yml
    parameters:

    database_path: '%kernel.root_dir%/db'


    services:

    pdo:

    class: PDO

    arguments: [sqlite:%database_path%]

    foo_repository:

    class: FooRepository

    arguments: [@pdo]

    View Slide

  15. LEVERAGING THE SERVICE CONTAINER - CACHING
    $file = __DIR__.'/../cache/container.php';


    if (file_exists($file)) {

    require_once $file;

    $container = new ProjectServiceContainer();

    } else {

    $container = new ContainerBuilder();

    // ...

    $container->compile();


    $dumper = new PhpDumper($container);

    file_put_contents($file, $dumper->dump());

    }

    View Slide

  16. LEVERAGING THE SERVICE CONTAINER - USAGE
    $foo = $container->get('foo_repository')->findFoo(42);

    View Slide

  17. LEVERAGING THE EVENT DISPATCHER
    Subjects Listeners
    Event Dispatcher
    Mailer Listener
    Registration Dispatcher
    call
    notify
    registration.success
    add listener
    registration.success
    register

    View Slide

  18. LEVERAGING THE EVENT DISPATCHER
    Subjects Listeners
    Event Dispatcher
    Mailer
    Listener
    Registration Dispatcher
    call
    notify
    registration.success
    add listener
    registration.success
    SMS
    Listener
    add listener
    registration.success
    call
    register

    View Slide

  19. LEVERAGING THE EVENT DISPATCHER
    use Symfony\Component\EventDispatcher\EventDispatcher;


    $dispatcher = new EventDispatcher();

    $dispatcher->addListener(

    'registration.success',

    function (Event $event) {}

    );

    $dispatcher->addListener(

    'registration.success',

    [$listener, 'onRegistrationSuccess']

    );

    View Slide

  20. LEVERAGING THE EVENT DISPATCHER
    $event = new UserRegisteredEvent($user);

    $dispatcher->dispatch('registration.success', $event);

    View Slide

  21. LEVERAGING MESSAGE QUEUES
    Consumer
    Legacy
    Message
    broker
    produce consume
    Consumer
    Consumer

    View Slide

  22. LEVERAGING MESSAGE QUEUES
    curl -i -u user:pass \
    -H "content-type:application/json" \
    -XPOST http://rabbtmiq:15672/api/exchanges/%2f/image-resize/publish \
    -d '{
    “properties":{},
    “payload":"cat.png",
    "payload_encoding":"string",
    “routing_key”:""
    }'
    https://github.com/php-amqplib/php-amqplib
    https://github.com/php-amqplib/RabbitMqBundle

    View Slide

  23. CUT, SLICE, EXTRACT
    Clear separation
    https://flic.kr/p/4vRMY3

    View Slide

  24. WRAPPING THE LEGACY APP
    Symfony
    FooController
    BarController
    Router
    LegacyController Legacy
    GET /products

    View Slide

  25. WRAPPING THE LEGACY APP
    # app/config/routing.yml


    app:

    resource: "@AppBundle/Controller/"

    type: annotation


    legacy:

    path: /{path}

    defaults: { _controller: AppBundle:Legacy:forward }

    requirements:

    path: .*

    View Slide

  26. WRAPPING THE LEGACY APP
    // src/AppBundle/Controller/BlogController.php


    namespace AppBundle\Controller;


    use Symfony\Component\HttpFoundation\Response;


    final class LegacyController

    {

    public function forwardAction($path)

    {

    ob_start();

    require __DIR__.’/../../../legacy/index.php';

    $content = ob_get_clean();


    return new Response($content);

    }

    }

    View Slide

  27. SHARING THE SESSION
    framework:

    session:

    storage_id: session.storage.php_bridge

    save_path: /tmp

    handler_id: session.handler.native_file
    Hint: to access the legacy session from Symfony:
    https://github.com/theodo/TheodoEvolutionSessionBundle

    View Slide

  28. PROXYING TO THE NEW APP
    Symfony
    Proxy
    Legacy
    GET /products

    View Slide

  29. PROXYING TO THE NEW APP
    # my-app.conf
    server {

    listen 80;

    server_name my-app.dev;

    root /my-app/legacy;


    location / {

    try_files $uri $uri/ /index.php$is_args$args;

    }


    location ~ ^/index\.php(/|$) {

    fastcgi_index index.php;

    fastcgi_pass my-app-php:9000;

    fastcgi_split_path_info ^(.+\.php)(/.*)$;

    include fastcgi_params;

    fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;

    fastcgi_param DOCUMENT_ROOT $realpath_root;

    }

    }

    View Slide

  30. PROXYING TO THE NEW APP
    # my-app.conf
    server {

    listen 80;

    server_name my-app.dev;

    root /my-app/legacy;


    location ~ ^/products/.+ {

    proxy_pass http://v2.my-app.dev;

    }


    location / {

    try_files $uri $uri/ /index.php$is_args$args;

    }


    # ...

    }

    View Slide

  31. PROXYING TO THE NEW APP
    # v2.my-app.conf
    server {

    listen 80;

    server_name v2.my-app.dev;

    root /my-app/web;


    location / {

    try_files $uri $uri/ /app.php$is_args$args;

    }


    location ~ ^/app\.php(/|$) {

    fastcgi_index app.php;

    fastcgi_pass localhost:9000;

    fastcgi_split_path_info ^(.+\.php)(/.*)$;

    include fastcgi_params;

    fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;

    fastcgi_param DOCUMENT_ROOT $realpath_root;

    fastcgi_param HTTP_HOST "my-app.dev";

    }


    View Slide

  32. GOING MICRO WITH THE MICRO KERNEL

    View Slide

  33. GOING MICRO WITH THE MICRO KERNEL
    final class AppKernel extends Kernel

    {

    use MicroKernelTrait;


    public function registerBundles()

    {

    return [

    new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),

    new Symfony\Bundle\TwigBundle\TwigBundle(),

    ];

    }
    // ...

    }

    View Slide

  34. GOING MICRO WITH THE MICRO KERNEL
    final class AppKernel extends Kernel

    {

    // ...


    protected function configureContainer(
    ContainerBuilder $c, LoaderInterface $loader
    ) {

    $c->loadFromExtension('framework', [

    'secret' => '$ecret',

    'templating' => [

    'engines' => ['twig'],

    ],

    ]);

    }


    // ...

    }

    View Slide

  35. GOING MICRO WITH THE MICRO KERNEL
    final class AppKernel extends Kernel

    {

    // ...

    protected function configureRoutes(RouteCollectionBuilder $routes)

    {

    $routes->add('/products/{slug}', 'kernel:productAction');

    }


    public function productAction($slug)

    {

    $twig = $this->container->get('twig');

    $content = $twig->render(‘product.html.twig', ['slug' => $slug]);


    return new Response($content);

    }

    }

    View Slide

  36. GOING MICRO WITH THE MICRO KERNEL
    // web/app.php


    // use statements omitted


    require __DIR__.'/../vendor/autoload.php';


    final class AppKernel extends Kernel

    {

    // ...

    }

    $kernel = new AppKernel('dev', true);

    $request = Request::createFromGlobals();

    $response = $kernel->handle($request);

    $response->send();

    $kernel->terminate($request, $response);

    View Slide

  37. GOING FULL STACK
    final class AppKernel extends Kernel

    {

    // remove the trait

    // ...


    public function registerContainerConfiguration(LoaderInterface $loader)

    {

    return $loader->load(
    $this->getRootDir()
    .’/config/config_'.$this->getEnvironment().'.yml'
    );

    }

    }

    View Slide

  38. GOING FULL STACK
    # take parts relevant to you from symfony/symfony-standard configs

    imports:

    - { resource: parameters.yml }

    - { resource: security.yml }

    - { resource: services.yml }


    framework:

    secret: "%secret%"

    router:

    resource: "%kernel.root_dir%/config/routing.yml"

    strict_requirements: ~

    # ...


    twig:

    debug: "%kernel.debug%"

    strict_variables: "%kernel.debug%"

    View Slide

  39. DESIGNING SMALLER APPS
    Legacy
    Web
    Checkout
    Image
    Converter
    Web Product
    Catalog
    Inventory
    API

    View Slide

  40. HYBRID APPROACH
    a bit of both
    https://flic.kr/p/sqfbS

    View Slide

  41. ACCESSING THE SYMFONY KERNEL FROM A LEGACY APP
    Legacy
    GET /products
    Symfony

    View Slide

  42. ACCESSING THE SYMFONY KERNEL FROM A LEGACY APP
    use Symfony\Component\HttpFoundation\Request;
    $request = Request::createFromGlobals();

    $request->attributes->set('is_legacy', true);

    $request->server->set('SCRIPT_FILENAME', 'app.php');


    $kernel = new \AppKernel($environment, $debug);

    $kernel->boot();


    $container = $kernel->getContainer();

    $container->get('request_stack')->push($request);

    View Slide

  43. ACCESSING THE SYMFONY KERNEL FROM A LEGACY APP
    $container = $kernel->getContainer();



    $container->get('event_dispatcher')

    ->dispatch('registration.success', new Event());


    $container->get('twig')->render('template.html.twig');


    View Slide

  44. SHARING EVENTS - SYMFONY KERNEL EVENTS
    Request Response
    Controller Resolver
    Event Dispatcher
    kernel.request
    FragmentListener
    SessionListener
    LocaleListener
    RouterListener
    Firewall

    View Slide

  45. SHARING EVENTS - SYMFONY KERNEL EVENTS
    Request Response
    Controller Resolver
    Event Dispatcher
    ControllerListener
    kernel.controller
    RequestDataCollector
    Listener
    ParamConverter
    Listener
    TemplateListener

    View Slide

  46. SHARING EVENTS - SYMFONY KERNEL EVENTS
    Request Response
    Controller Resolver
    Event Dispatcher
    kernel.response
    CacheListener
    ResponseListener
    ProfilerListener
    WebDebugToolbarListener

    View Slide

  47. SHARING EVENTS - SYMFONY KERNEL EVENTS
    Request Response
    Controller Resolver
    Event Dispatcher
    kernel.finish_request
    LocaleListener RouterListener
    Firewall

    View Slide

  48. SHARING EVENTS - SYMFONY KERNEL EVENTS
    Request Response
    Controller Resolver
    Event Dispatcher
    TemplateListener
    kernel.view

    View Slide

  49. SHARING EVENTS - SYMFONY KERNEL EVENTS
    Request Response
    Controller Resolver
    Event Dispatcher
    kernel.terminate
    ProfilerListener
    EmailSenderListener

    View Slide

  50. SHARING EVENTS - SYMFONY KERNEL EVENTS
    $eventDispatcher = $container->get('event_dispatcher');


    $event = new GetResponseEvent(
    $kernel, $request, HttpKernelInterface::MASTER_REQUEST
    );

    $eventDispatcher->dispatch(KernelEvents::REQUEST, $event);

    View Slide

  51. EMBEDDING CONTROLLERS / ESI
    Legacy page

    View Slide

  52. EMBEDDING CONTROLLERS / ESI
    $inlineRenderer = $container->get('fragment.renderer.esi');


    echo $inlineRenderer->render('/foo', $request)->getContent();


    some foo content


    has surrogate capability no surrogate capability

    View Slide

  53. @jakub_zalas @jakzal
    Thank you.
    Rate my talk, please:
    https://joind.in/talk/85830

    View Slide