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

Eating spaghetti with Symfony

Jakub Zalas
February 18, 2016

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/
  2. 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.
  3. 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
  4. WHY MIGRATE? ➤ adding features takes ages ➤ obsolete technology

    ➤ scaling https://www.flickr.com/photos/sdobie/8226246821/
  5. MIGRATION STRATEGIES Migrating Legacy Systems: Gateways, Interfaces & the Incremental

    Approach by Michael Stonebraker and Michael L. Brodie Cold Turkey Chicken Little vs
  6. 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
  7. PUTTING TWIG TO WORK $loader = new Twig_Loader_Filesystem(__DIR__.'/templates');
 
 $twig

    = new Twig_Environment($loader, [
 'cache' => __DIR__.'/../var/cache/legacy/twig',
 'debug' => true,
 ]);
  8. 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'));
  9. 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');
  10. 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]
  11. 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());
 }
  12. LEVERAGING THE EVENT DISPATCHER Subjects Listeners Event Dispatcher Mailer Listener

    Registration Dispatcher call notify registration.success add listener registration.success register
  13. 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
  14. 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']
 );
  15. 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
  16. WRAPPING THE LEGACY APP # app/config/routing.yml
 
 app:
 resource: "@AppBundle/Controller/"


    type: annotation
 
 legacy:
 path: /{path}
 defaults: { _controller: AppBundle:Legacy:forward }
 requirements:
 path: .*
  17. 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);
 }
 }
  18. 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
  19. 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;
 }
 }
  20. 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;
 }
 
 # ...
 }
  21. 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";
 }

  22. 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(),
 ];
 } // ...
 }
  23. 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'],
 ],
 ]);
 }
 
 // ...
 }
  24. 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);
 }
 }
  25. 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);
  26. 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' );
 }
 }
  27. 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%"
  28. 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);
  29. 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');

  30. SHARING EVENTS - SYMFONY KERNEL EVENTS Request Response Controller Resolver

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

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

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

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

    Event Dispatcher kernel.terminate ProfilerListener EmailSenderListener
  35. SHARING EVENTS - SYMFONY KERNEL EVENTS $eventDispatcher = $container->get('event_dispatcher');
 


    $event = new GetResponseEvent( $kernel, $request, HttpKernelInterface::MASTER_REQUEST );
 $eventDispatcher->dispatch(KernelEvents::REQUEST, $event);
  36. EMBEDDING CONTROLLERS / ESI $inlineRenderer = $container->get('fragment.renderer.esi');
 
 echo $inlineRenderer->render('/foo',

    $request)->getContent(); <esi:include src="/foo" /> <div clas="foo">
 some foo content
 </div> has surrogate capability no surrogate capability