Save 37% off PRO during our Black Friday Sale! »

Modernising the Legacy

2bd48651cd01e0ca2e0a255a63da77aa?s=47 Marek Matulka
September 18, 2015

Modernising the Legacy

No one likes to work with the legacy projects - it's not fun. There are no specs, there are no behat examples and you're afraid to touch it. The customer may not have time or budget to spend on rewriting it from the scratch, but is likely to keep asking for the new features.

Learn how to work with the legacy code, how to add new features without breaking existing ones. Learn to use Symfony components to support building sustainable features.

2bd48651cd01e0ca2e0a255a63da77aa?s=128

Marek Matulka

September 18, 2015
Tweet

Transcript

  1. Modernising the Legacy Marek Matulka UK

  2. Marek Matulka Software Engineer SensioLabs UK @super_marek

  3. Legacy

  4. What is legacy code? - someone else’s code - your

    old code - an old framework you don’t know - or too coupled to the framework - built over years
  5. What is legacy code? Old code where... - most of

    it is procedural - no namespaces used - no code convention - no separation of concerns
  6. What is legacy code? Old code where... - PHP mixed

    with HTML, CSS - overused Global variables - collection of include files - *.inc files in public folder
  7. What is legacy code? Old code where... - no concept

    of environments - no error reporting - no logging - no version control system used
  8. What is legacy code? Any code... - without tests

  9. It’s too hard - understand the logic - add new

    features - debug - fix bugs - adopt new technologies
  10. How do we get out of legacy?

  11. Start with the goal

  12. Why? Business goal

  13. Why? Who? Who? Actors

  14. Why? Who? Who? How? How? How? Impacts

  15. Why? Who? Who? How? How? How? What? What? What? What?

    What? What? Deliverables
  16. Why? Who? Who? How? How? How? What? What? What? What?

    What? What? Prioritise
  17. Goal: Reduce drop-outs by 10% In order to see the

    total cost of order As a shopper I want to see postage cost Example Why? Who? How? What?
  18. Write scenarios Feature: Shopper can see postage cost In order

    to see the total cost of order As a shopper I want to see a postage cost Scenario: Shopper can see a postage cost Given I have added a "Clean Code" book priced £34.50 to the basket And the postage cost for one book is £3.50 When I check the basket Then I should see the postage cost of £3.50 And the total of the basket should be £38.00
  19. Implement scenarios

  20. Given-When-Then Feature: Shopper can see postage cost In order to

    see the total cost of order As a shopper I want to see a postage cost Scenario: Shopper can see a postage cost Given I have added a "Clean Code" book priced £34.50 to the basket And the postage cost for one book is £3.50 When I check the basket Then I should see the postage cost of £3.50 And the total of the basket should be £38.00
  21. Write a failing scenario (feature)

  22. Write a failing scenario (feature) Write a failing test

  23. Write a failing scenario (feature) Write a failing test Make

    your test pass
  24. Write a failing scenario (feature) Write a failing test Make

    your test pass Refactor your code
  25. Write a failing scenario (feature) Write a failing test Make

    your test pass Refactor your code Repeat the fail-pass-refactor cycle as necessary
  26. Write a failing scenario (feature) Write a failing test Make

    your test pass Refactor your code Make your scenario pass
  27. Write a failing scenario (feature) Write a failing test Make

    your test pass Refactor your code Make your scenario pass BDD TDD
  28. Write a failing scenario (feature) Write a failing test Make

    your test pass Refactor your code Make your scenario pass BDD TDD External Quality Internal Quality
  29. (everything?) What to test?

  30. Unit tests Acceptance tests Integration tests Smoke tests

  31. Unit tests Acceptance tests Integration tests Smoke tests Manual Automated

  32. Unit tests Acceptance tests Integration tests Smoke tests Slowest Fastest

  33. Why test?

  34. Why test? - code level documentation - features described through

    examples - clean code - you’re not afraid to change your code - peace of mind
  35. Why test? delivered features project’s life

  36. Why use Symfony components?

  37. Symfony Components The standard foundation on which the best PHP

    applications are built. - fully tested - decoupled - reusable - de-facto standard
  38. Symfony Components Build a micro framework. Use components you need

    in your application.
  39. symfony/dependency-injection Dependency Injection

  40. Dependency Injection In software engineering, dependency injection is a software

    design pattern that implements inversion of control for resolving dependencies. Dependency injection means giving an object its instance variables. Really. That's it. https://en.wikipedia.org/wiki/Dependency_injection
  41. Dependency Injection PostageCalculator PostageRepository

  42. Dependency Inversion PostageCalculator PostageRepository

  43. Dependency Inversion PostageCalculator PostageRepository PostageSqliteRepository

  44. Dependency Inversion PostageCalculator PostageRepository PostageSqliteRepository PostageDoctrineRepository

  45. Install dependency injection $ composer require symfony/dependency-injection $ composer require

    symfony/config
  46. Create container.php <?php // app/container.php use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use

    Symfony\Component\Config\FileLocator; require __DIR__ . '/../vendor/autoload.php'; $container = new ContainerBuilder(); $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/app/config')); $loader->load('services.yml'); $container->compile();
  47. Create container.php <?php // app/container.php use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use

    Symfony\Component\Config\FileLocator; require __DIR__ . '/../vendor/autoload.php'; $container = new ContainerBuilder(); $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/app/config')); $loader->load('services.yml'); $container->compile();
  48. Create container.php <?php // app/container.php use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use

    Symfony\Component\Config\FileLocator; require __DIR__ . '/../vendor/autoload.php'; $container = new ContainerBuilder(); $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/app/config')); $loader->load('services.yml'); $container->compile();
  49. Create container.php <?php // app/container.php use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use

    Symfony\Component\Config\FileLocator; require __DIR__ . '/../vendor/autoload.php'; $container = new ContainerBuilder(); $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/app/config')); $loader->load('services.yml'); $container->compile();
  50. Create container.php <?php // app/container.php use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use

    Symfony\Component\Config\FileLocator; require __DIR__ . '/../vendor/autoload.php'; $container = new ContainerBuilder(); $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/app/config')); $loader->load('services.yml'); $container->compile();
  51. Create container.php <?php // app/container.php use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use

    Symfony\Component\Config\FileLocator; require __DIR__ . '/../vendor/autoload.php'; $container = new ContainerBuilder(); $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/app/config')); $loader->load('services.yml'); $container->compile();
  52. # app/config/services.yml services: acme.postage.repository: class: Acme\Infrastructure\Sqlite\PostageSqliteRepository acme.postage.calculator: class: Acme\Basket\PostageCalculator arguments:

    - @acme.postage.repository Create services.yml
  53. # app/config/services.yml services: acme.postage.repository: class: Acme\Infrastructure\Sqlite\PostageSqliteRepository acme.postage.calculator: class: Acme\Basket\PostageCalculator arguments:

    - @acme.postage.repository Create services.yml
  54. # app/config/services.yml services: acme.postage.repository: class: Acme\Infrastructure\Sqlite\PostageSqliteRepository acme.postage.calculator: class: Acme\Basket\PostageCalculator arguments:

    - @acme.postage.repository Create services.yml
  55. Use container require '../_inc/db.php'; require '../app/container.php'; // ... $postageCalculator =

    $container->get('acme.postage.calculator'); if ($postage = $postageCalculator->calculateForBasket($_SESSION['basket'])) { $postage = $postage->toView(); echo sprintf("<p>Estimated postage for %d item(s): £%1.02f.</p>", $postage['quantity'], $postage['price'] ); } // ...
  56. Use container require '../_inc/db.php'; require '../app/container.php'; // ... $postageCalculator =

    $container->get('acme.postage.calculator'); if ($postage = $postageCalculator->calculateForBasket($_SESSION['basket'])) { $postage = $postage->toView(); echo sprintf("<p>Estimated postage for %d item(s): £%1.02f.</p>", $postage['quantity'], $postage['price'] ); } // ...
  57. Use container require '../_inc/db.php'; require '../app/container.php'; // ... $postageCalculator =

    $container->get('acme.postage.calculator'); if ($postage = $postageCalculator->calculateForBasket($_SESSION['basket'])) { $postage = $postage->toView(); echo sprintf("<p>Estimated postage for %d item(s): £%1.02f.</p>", $postage['quantity'], $postage['price'] ); } // ...
  58. Dependency Injection with Drupal 7?

  59. container.php <?php use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\Config\FileLocator; require_once DRUPAL_ROOT

    . '/../vendor/autoload.php'; $container = new ContainerBuilder(); $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/app/config')); $loader->load('services.yml'); $container->compile(); $conf['di_container'] = $container;
  60. container.php <?php use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\Config\FileLocator; require_once DRUPAL_ROOT

    . '/../vendor/autoload.php'; $container = new ContainerBuilder(); $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/app/config')); $loader->load('services.yml'); $container->compile(); $conf['di_container'] = $container;
  61. depot_locator.module <?php function get_depot_json($postcode) { global $conf; $depotEntity = $conf['di_container']

    ->get('acme.depot_locator')->locateDepot($postcode); $depot = $depotEntity->toArray(); set_depot_cookies($depot); return ['depot' => $depot]; }
  62. twig/twig Twig

  63. What is twig?

  64. Install twig $ composer require twig/twig

  65. How to use twig? $loader = new Twig_Loader_Filesystem('/path/to/templates'); $twig =

    new Twig_Environment($loader); echo $twig->render('index.html', ['variable' => 'value']);
  66. # app/config/services.yml parameters: twig.paths: - app/Resources/views services: twig.template_loader: class: Twig_Loader_Filesystem

    arguments: - %twig.paths% twig.renderer: class: Twig_Environment arguments: - @twig.template_loader Enabling twig in DI container
  67. # app/config/services.yml parameters: twig.paths: - app/Resources/views services: twig.template_loader: class: Twig_Loader_Filesystem

    arguments: - %twig.paths% twig.renderer: class: Twig_Environment arguments: - @twig.template_loader Enabling twig in DI container
  68. # app/config/services.yml parameters: twig.paths: - app/Resources/views services: twig.template_loader: class: Twig_Loader_Filesystem

    arguments: - %twig.paths% twig.renderer: class: Twig_Environment arguments: - @twig.template_loader Enabling twig in DI container
  69. # app/config/services.yml parameters: twig.paths: - app/Resources/views services: twig.template_loader: class: Twig_Loader_Filesystem

    arguments: - %twig.paths% twig.renderer: class: Twig_Environment arguments: - @twig.template_loader Enabling twig in DI container
  70. Use twig $basketViewData = $container->get('acme.basket.view') ->toView($_SESSION['basket']); $basketView = $container->get('twig.renderer') ->render(

    'basket.twig.html', $basketViewData ); echo $basketView;
  71. symfony/console Add console commands

  72. Install console component $ composer require symfony/console

  73. class ImportAndPersistCommand extends Command { private $action; public function __construct(ImportAndPersistActionHandler

    $action) { $this->action = $action; parent::__construct(); } protected function configure() { $this->setName('products:import'); } protected function execute(InputInterface $input, OutputInterface $output) { $startTime = microtime(true); $this->action->importAndPersistProducts(); $executionTime = microtime(true) - $startTime; $output->writeln( sprintf("Products import completed in %1.02fs.", $executionTime) ); } }
  74. app/console #!/usr/bin/php <?php require_once (__DIR__ . '/../vendor/autoload.php'); require_once (__DIR__ .

    '/app/container.php'); use Symfony\Component\Console\Application; $application = new Application(); $application->addCommands([ $container->get('acme.command.import_products'), $container->get('acme.command.delete_products'), $container->get('acme.command.depot_locate'), ]); $application->run();
  75. symfony/http-foundation Decouple from global state

  76. Http application’s flow Controller Request Response

  77. Install HttpFoundation component $ composer require symfony/http-foundation

  78. De-coupling from global state # /public/index.php <?php require __DIR__ .

    '/../app/container.php'; use Symfony\Component\HttpFoundation\Request; $request = Request::createFromGlobals(); $response = $container->get('acme.basket_view.controller') ->viewAction($request); $response->send();
  79. De-coupling from global state # /public/index.php <?php require __DIR__ .

    '/../app/container.php'; use Symfony\Component\HttpFoundation\Request; $request = Request::createFromGlobals(); $response = $container->get('acme.basket_view.controller') ->viewAction($request); $response->send();
  80. De-coupling from global state # /public/index.php <?php require __DIR__ .

    '/../app/container.php'; use Symfony\Component\HttpFoundation\Request; $request = Request::createFromGlobals(); $response = $container->get('acme.basket_view.controller') ->viewAction($request); $response->send();
  81. De-coupling from global state # /public/index.php <?php require __DIR__ .

    '/../app/container.php'; use Symfony\Component\HttpFoundation\Request; $request = Request::createFromGlobals(); $response = $container->get('acme.basket_view.controller') ->viewAction($request); $response->send();
  82. Going forward

  83. Try it yourself!

  84. Playground https://github.com/mareg/legacy-summercamp-2015 - four exercises: - add dependency injection -

    add twig - decouple from global state - controller as service
  85. Questions? Slides: speakerdeck.com/super_marek Feedback: joind.in/talk/view/14977 Connect: @super_marek