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

Avoid Costly Framework Upgrades

Avoid Costly Framework Upgrades

If your framework version was no longer supported, how much effort would it take to upgrade to the most recent version, or swap for another framework? Does it look like you might need to rewrite your entire application? You can write your code in a way that will make the inevitable framework upgrade a piece of cake, instead of a 12-month sustained effort. Learn from the experience of dozens of framework upgrades, some easier than others.

Anna Filina
PRO

October 16, 2021
Tweet

More Decks by Anna Filina

Other Decks in Programming

Transcript

  1. Avoid Costly
    Framework Upgrades
    LONGHORN PHP | OCT 2021 @afilina

    View Slide

  2. "I want to replace Zend Framework 1,
    but it looks like a complete rewrite."

    View Slide

  3. "I want to keep up with my framework,
    but it's a lot of work with each new version."

    View Slide

  4. "I am stuck on this old framework,
    which also makes me stuck on PHP 5."

    View Slide

  5. Anna Filina
    • Coding since 1997 (VB4)
    • PHP since 2003
    • Legacy archaeology
    • Test automation
    • Public speaking
    • Mentorship
    • YouTube videos

    View Slide

  6. Upgrade or
    Change?
    It depends

    View Slide

  7. • Was it abandoned?
    • Is the new version compatible?
    • Current version might not last forever.

    View Slide

  8. 2005
    2006
    2007
    2008
    2009
    2010
    2011
    2012
    2013
    2014
    2015
    2016
    2017
    2018
    2019
    2020
    Major incompatibility
    Symfony 1
    Symfony 2
    Symfony 3
    Symfony 4
    Symfony 5 Four years worth of apps
    that are hard to upgrade.

    View Slide

  9. 2005
    2006
    2007
    2008
    2009
    2010
    2011
    2012
    2013
    2014
    2015
    2016
    2017
    2018
    2019
    2020
    Zend Framework 1
    Zend Framework 2
    Zend Framework 3
    Renamed to Laminas
    Major incompatibility
    The different between 1 and 2 is so
    big, it's like a different framework.

    View Slide

  10. 2005
    2006
    2007
    2008
    2009
    2010
    2011
    2012
    2013
    2014
    2015
    2016
    2017
    2018
    2019
    2020
    Laravel 1, Laravel 2
    Laravel 3
    Laravel 4
    Laravel 5
    Laravel 6
    Laravel 7, Laravel 8
    Many applications
    written here
    A little more time on recent
    versions means less legacy.

    View Slide

  11. 2005
    2006
    2007
    2008
    2009
    2010
    2011
    2012
    2013
    2014
    2015
    2016
    2017
    2018
    2019
    2020
    Laravel 1, Laravel 2
    Laravel 3
    Laravel 4
    Laravel 5
    Laravel 6
    Laravel 7, Laravel 8
    No support after Sept 2022 You have 10 months to
    upgrade and deploy.

    View Slide

  12. It takes more than 10 months to upgrade?
    You're already late.

    View Slide

  13. • Spend a year upgrading your app.
    • New framework version comes out.
    • You need another upgrade.
    It's a common situation with
    large applications.

    View Slide

  14. Should you upgrade less often?
    No, it will hurt more.
    The more versions you skip, the
    harder it will be to upgrade.

    View Slide

  15. Should frameworks be supported longer?
    No, unless you pay a maintainer.
    You can't expect maintainers to
    maintain dozens of versions.

    View Slide

  16. Should frameworks avoid breaking changes?
    No, innovation is important.
    We want frameworks to get better,
    and that means change.

    View Slide

  17. What can I do to make upgrades easier?
    Decouple your code from the framework.

    View Slide

  18. How Am
    I Coupled?

    View Slide

  19. • Controllers
    Controllers can access sessions, validation,
    form builders, logging, service manager, etc.

    View Slide

  20. • Controllers
    • Views & helpers
    Partial views, layout, helpers to
    generate HTML.

    View Slide

  21. • Controllers
    • Views & helpers
    • Routing
    Not always complex, but each
    framework configures it differently.

    View Slide

  22. • Controllers
    • Views & helpers
    • Routing
    • Databases
    ORMs map objects to tables, build complex
    queries and provide various shortcuts.

    View Slide

  23. • Controllers
    • Views & helpers
    • Routing
    • Databases
    • Utility classes
    Utility classes may not exist
    elsewhere, or become discontinues.

    View Slide

  24. • Controllers
    • Views & helpers
    • Routing
    • Databases
    • Utility classes
    • Etc.
    Each framework does things in a unique way that that
    may be completely incompatible with another framework.

    View Slide

  25. Controllers

    View Slide

  26. // Symfony 5
    class LuckyController extends AbstractController
    {
    /**
    * @Route("/lucky/number")
    */
    public function number(): Response
    {
    $number = random_int(0, 100);
    return $this->render('lucky/number.html.twig', [
    'number' => $number,
    ]);
    }
    } We use methods inherited from
    the parent.

    View Slide

  27. AbstractController
    LuckyController
    template files
    configuration
    service container
    etc.
    AbstractController requires bootstrapping the framework. It can be mitigated
    with framework-specific testing tools, but then you're coupled to those.

    View Slide

  28. // Symfony 5
    class LuckyController extends AbstractController
    {
    /**
    * @Route("/lucky/number")
    */
    public function number(): Response
    {
    $number = random_int(0, 100);
    return $this->render('lucky/number.html.twig', [
    'number' => $number,
    ]);
    }
    } We shouldn't use a partial mock
    here. It's a bad practice.

    View Slide

  29. return $this->container
    ->get('twig')
    ->render($view, $parameters);
    Under the hood, it's just a tiny shortcut,
    but comes at a cost of maintainability

    View Slide

  30. Dependency
    Inversion
    Almost every framework today
    already provides that mechanism.

    View Slide

  31. LuckyController
    Twig
    Instead of extending AbstractController,
    we'll inject the services that we need

    View Slide

  32. TwigTemplating LaminasViewTemplating
    LuckyController
    Templating
    We can improve further by depending on an interface,
    decoupling us from the specific templating engine.

    View Slide

  33. interface Templating
    {
    public function render(string $templateName, array $data): string;
    }
    This is the method we'll call from
    our controller.

    View Slide

  34. final class TwigTemplating implements Templating
    {
    public function __construct(
    private Environment $twig
    ) {}
    public function render(string $template, array $data): string
    {
    return $this->twig->render($template . '.html.twig', $data);
    }
    }
    Inject Twig, the Implement method by using Twig to
    render. This is now an adapter for Twig.

    View Slide

  35. class LuckyController
    {
    public function __construct(
    private Templating $templating
    ) {}
    public function number(Request $request): Response
    {
    $content = $this->templating->render(
    'lucky/number',
    ['number' => random_int(0, 100)]
    );
    return new Response($content);
    }
    } Drop the parent. Add the interface to the constructor. The
    service container will decide which concrete class to inject.

    View Slide

  36. LuckyController
    Templating
    TwigTemplating LaminasViewTemplating
    At no point does our controller depend on
    Symfony or Twig directly.

    View Slide

  37. LuckyController
    Templating
    This makes it easy to mock
    the dependencies.

    View Slide

  38. $templatingMock = $this->createMock(Templating::class);
    $controller = new LuckyController($templatingMock);
    $response = $controller->number(new Request(/**/));
    We're not relying on any specialized testing tools that come with the
    framework. When switching frameworks, we don't need to update the tests.

    View Slide

  39. Other Frameworks

    View Slide

  40. // Zend Framework 1
    class LuckyController extends Zend_Controller_Action
    {
    public function numberAction()
    {
    //...
    $this->view->number = $number;
    $this->render('lucky/number');
    if ($number < 10) {
    $this->render('lucky/below-ten');
    }
    }
    }
    ZF1 assigns variables to parent, then renders
    directly to the browser and continues execution.

    View Slide

  41. $viewVars['number'] = $number;
    $content .= $this->templating
    ->render('lucky/number', $viewVars);
    if ($number < 10) {
    $content .= $this->templating
    ->render('lucky/below-ten', $viewVars);
    }
    echo $content;
    Add variables to an array, then pass it to render. Concatenate and
    output all at once. This uses the same templating interface.

    View Slide

  42. MVC frameworks return
    from controllers differently.

    View Slide

  43. void
    Zend Framework 1
    Response
    Symfony 5
    Response
    Laravel
    ViewModel
    Laminas MVC
    This means that controllers are not 100% portable, but
    decoupling already saves us a lot of effort in an upgrade.

    View Slide

  44. Invest once.
    Save on each upgrade.
    Upgrade more often.
    Gain in testability.
    Even if you don't end up changing the framework, what you'll
    gain in testability will make you save time today.

    View Slide

  45. More Decoupling

    View Slide

  46. // Zend Framework 1 view
    products as $product): ?>
    = $this->escapeHtml($product->name) ?>
    = $this->customerViewHelper($product->amount) ?>

    Those helpers may or may not exist in the target framework, or they may
    exist but accept different arguments, or exhibit a different behavior.

    View Slide

  47. EscapeHtml
    escapeHtml
    Zf1EscapeHtml LaminasEscapeHtml
    Same approach as with templating. Depend on interfaces,
    so you can swap implementations.

    View Slide

  48. EscapeHtml
    escapeHtml
    Zf1EscapeHtml LaminasEscapeHtml
    MyEscapeHtml
    Trivial helpers can be simply copy-pasted and
    become part of your code.

    View Slide

  49. Routing

    View Slide

  50. // Symfony 5
    class LuckyController extends AbstractController
    {
    /**
    * @Route("/lucky/number")
    */
    public function number(): Response
    {
    $number = random_int(0, 100);
    return $this->render('lucky/number.html.twig', [
    'number' => $number,
    ]);
    }
    } If you move this code to a different framework, you'd
    have to redesign your routing.

    View Slide

  51. // Zend Framework 1
    class LuckyController extends Zend_Controller_Action
    {
    public function numberAction()
    {
    //...
    }
    }
    ZF1 has no concept of routing at all. It's all based
    on convention: class and method name.

    View Slide

  52. # Symfony 1
    # apps/frontend/config/routing.yml
    default:
    url: /:module/:action/*
    Symfony 1 has a similar approach using convention, but
    can also define custom routes in YAML.

    View Slide

  53. • Annotations
    • Attributes
    • XML config
    • PHP config
    • YAML config
    Symfony 5 has a lot more options, which also
    means that every project has a different challenge.

    View Slide

  54. # Symfony 1
    blog_list:
    path: /blog
    param: { module: blog, action: list }
    # Symfony 5
    blog_list:
    path: /blog
    controller: App\Controller\BlogController::list
    Even if both can use YAML, it has differences. I typically write a script
    that converts the routing information from and to whatever I need.

    View Slide

  55. We need a PSR for routing.
    This would be a nice way to avoid having to convert the
    routing each time a new format comes out.

    View Slide

  56. ORMs
    I used to love them, but they
    really don't age well.

    View Slide

  57. // Eloquent 8
    class Flight extends Model
    {
    protected $table = 'my_flights';
    protected $dateFormat = 'U';
    }
    Gives access to methods, but that also means it's hard to
    mock, or need to be mocked with framework-specific tools.

    View Slide

  58. // Eloquent 8
    class Flight extends Model
    {
    protected $table = 'my_flights';
    protected $dateFormat = 'U';
    }
    Mapping is done differently in every ORM, and there are
    features that might simply not exist in the new ORM.

    View Slide

  59. // Doctrine 1
    class App_Model_Flight extends Doctrine_Record
    {
    public function setTableDefinition()
    {
    $this->setTableName('my_flights');
    }
    }
    Doctrine 1 also extends, but it's a different parent, with
    different methods and a new way to define mapping.

    View Slide

  60. // Doctrine 2
    /**
    * @ORM\Entity
    * @ORM\Table(name="my_flights")
    */
    class Flight
    {
    }
    In Doctrine 2, we don't extend, so we can't call save on it.
    We need the EntityManager for that. Mapping can be done
    many different ways, including annotations.

    View Slide

  61. Do you really need an ORM?
    I personally don't use ORMs anymore. The maintenance
    effort far outweighs all advantages, in my opinion.

    View Slide

  62. FlightRepository Flight
    (needs configuration)
    Instead of automatically mapping my objects to tables based on
    configuration, I can just instantiate the Flight myself, using plain old PHP.

    View Slide

  63. final class DatabaseFlightRepository implements FlightRepository
    {
    public function findByRoute(Airport $origin, Airport $destination)
    {
    $rows = $this->db->prepare(/**/)->execute(/**/);
    $flights = array_map(function(array $row) {
    return Flight::fromRow($row);
    }, $rows);
    return $flights;
    }
    }
    I can then prepare and execute a plain query. Complex query builders
    can make it hard to upgrade. I then convert from array to object manually.

    View Slide

  64. FlightRepository Flight
    (needs configuration)
    Because I map the record myself, I don't need any ORM-
    specific configuration. These classes become portable.

    View Slide

  65. • Extending a FW class.
    • Statically calling or instantiating a FW class.
    • FW-specific configuration files.
    • Automatic behavior based on conventions.
    • FW-specific annotations.
    • Interacting with a complex API.
    Most common ways in which you can be
    tightly coupled with a framework

    View Slide

  66. Framework
    Maintainers

    View Slide

  67. Can frameworks make it easier?
    Yes, but it's a tall order.
    It's a lot of work, and maintainers might have their own idea about
    what a framework should and should not be responsible for.

    View Slide

  68. Can frameworks show better examples?
    Yes, but it's a tall order.
    Many frameworks compete on
    simplicity to attract users.

    View Slide

  69. Decoupled code makes for more
    complex examples.

    View Slide

  70. "Look how simple it is."
    vs
    "Look at this harder example.
    Now you can move off
    our framework more easily."

    View Slide

  71. Is There A Good
    Example?

    View Slide

  72. class LuckyNumberHandler implements RequestHandlerInterface
    {
    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
    $content = $this->templating->render('lucky/number',
    ['number' => $number]
    );
    return new Response($content);
    }
    }
    Mezzio is already in line with how I do things myself already.
    One action per controller, reducing dependencies per class.

    View Slide

  73. class LuckyNumberHandler implements RequestHandlerInterface
    {
    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
    $content = $this->templating->render('lucky/number',
    ['number' => $number]
    );
    return new Response($content);
    }
    }
    All these interfaces are part of PSR-7 and PSR-15. All
    frameworks could start offering this alternative.

    View Slide

  74. Request Handler Response
    It's the not the only thing I like in Mezzio, but also there are things I
    don't like. It still feels like the right direction for frameworks.

    View Slide

  75. Decouple now.
    No need to wait for a framework upgrade.
    Even if you don't plan to upgrade your framework any time soon,
    you'll have more testable code that is easier to maintain.

    View Slide

  76. @afilina
    Questions?

    View Slide