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

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.

B3b2139e4f2c0eca4efe2379fcebc1c5?s=128

Anna Filina
PRO

October 16, 2021
Tweet

Transcript

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

  2. "I want to replace Zend Framework 1, but it looks

    like a complete rewrite."
  3. "I want to keep up with my framework, but it's

    a lot of work with each new version."
  4. "I am stuck on this old framework, which also makes

    me stuck on PHP 5."
  5. Anna Filina • Coding since 1997 (VB4) • PHP since

    2003 • Legacy archaeology • Test automation • Public speaking • Mentorship • YouTube videos
  6. Upgrade or Change? It depends

  7. • Was it abandoned? • Is the new version compatible?

    • Current version might not last forever.
  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.
  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.
  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.
  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.
  12. It takes more than 10 months to upgrade? You're already

    late.
  13. • Spend a year upgrading your app. • New framework

    version comes out. • You need another upgrade. It's a common situation with large applications.
  14. Should you upgrade less often? No, it will hurt more.

    The more versions you skip, the harder it will be to upgrade.
  15. Should frameworks be supported longer? No, unless you pay a

    maintainer. You can't expect maintainers to maintain dozens of versions.
  16. Should frameworks avoid breaking changes? No, innovation is important. We

    want frameworks to get better, and that means change.
  17. What can I do to make upgrades easier? Decouple your

    code from the framework.
  18. How Am I Coupled?

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

    service manager, etc.
  20. • Controllers • Views & helpers Partial views, layout, helpers

    to generate HTML.
  21. • Controllers • Views & helpers • Routing Not always

    complex, but each framework configures it differently.
  22. • Controllers • Views & helpers • Routing • Databases

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

    • Utility classes Utility classes may not exist elsewhere, or become discontinues.
  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.
  25. Controllers

  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.
  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.
  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.
  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
  30. Dependency Inversion Almost every framework today already provides that mechanism.

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

    that we need
  32. TwigTemplating LaminasViewTemplating LuckyController Templating We can improve further by depending

    on an interface, decoupling us from the specific templating engine.
  33. interface Templating { public function render(string $templateName, array $data): string;

    } This is the method we'll call from our controller.
  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.
  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.
  36. LuckyController Templating TwigTemplating LaminasViewTemplating At no point does our controller

    depend on Symfony or Twig directly.
  37. LuckyController Templating This makes it easy to mock the dependencies.

  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.
  39. Other Frameworks

  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.
  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.
  42. MVC frameworks return from controllers differently.

  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.
  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.
  45. More Decoupling

  46. // Zend Framework 1 view <?php foreach ($this->products as $product):

    ?> <?= $this->escapeHtml($product->name) ?> <?= $this->customerViewHelper($product->amount) ?> <?php endforeach ?> Those helpers may or may not exist in the target framework, or they may exist but accept different arguments, or exhibit a different behavior.
  47. EscapeHtml escapeHtml Zf1EscapeHtml LaminasEscapeHtml Same approach as with templating. Depend

    on interfaces, so you can swap implementations.
  48. EscapeHtml escapeHtml Zf1EscapeHtml LaminasEscapeHtml MyEscapeHtml Trivial helpers can be simply

    copy-pasted and become part of your code.
  49. Routing

  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.
  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.
  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.
  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.
  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.
  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.
  56. ORMs I used to love them, but they really don't

    age well.
  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.
  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.
  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.
  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.
  61. Do you really need an ORM? I personally don't use

    ORMs anymore. The maintenance effort far outweighs all advantages, in my opinion.
  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.
  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.
  64. FlightRepository Flight (needs configuration) Because I map the record myself,

    I don't need any ORM- specific configuration. These classes become portable.
  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
  66. Framework Maintainers

  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.
  68. Can frameworks show better examples? Yes, but it's a tall

    order. Many frameworks compete on simplicity to attract users.
  69. Decoupled code makes for more complex examples.

  70. "Look how simple it is." vs "Look at this harder

    example. Now you can move off our framework more easily."
  71. Is There A Good Example?

  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.
  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.
  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.
  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.
  76. @afilina Questions?