Introduction

When you’re writing a software application you’ll need many, many things. Sometimes it’s quite disappointing. Just look at how many packages need to be installed only to run a very basic web application with a framework like Symfony or Laravel, and a test framework like PHPUnit. Most projects will have fewer lines of code in their own src directory than in the vendor/ directory.

If we don’t install all those dependencies, we can still create an application. But we would have to “reinvent a lot of wheels”. It would make development so inefficient, we would never be allowed to make another application. So we rightfully trade development speed against an increased dependency on other people’s code.

Once we have decided on a certain framework, we experience the highest speed of development if we do everything “their way”. We follow their documentation, community blog posts, video trainings, e-books, and conferences. We apply the best practices that are being spread by visible community members, and we always prefer so-called idiomatic use of the framework. There are many advantages to this approach: it will be easy to find answers on Stack Overflow (“How to do X with Laravel?”), you can add the framework to job descriptions (“5+ years of experience with Symfony”), you can even raise your team’s standard by requesting everyone to join a certification program.

Coupling, Why is it Bad?

Going all-in on a framework also has downsides. It’s commonly known as vendor lock-in. This term already has enough negative connotations to trigger some alarms. For almost 20 years now, I’ve worked on PHP projects that first benefited incredibly from strictly following the framework approach. In the end, they had become an unmaintainable mess and were sometimes considered total-loss.

Looking at the code of such projects, and I guess you also have experience with them, a lot of work would be needed to save them. You’d have to migrate to a different framework, a different ORM, a different test framework. All of this is very hard to do because the code is tangled up with all those frameworks. Business logic is not in plain sight, but hidden in controllers, hooks, SQL queries, foreign key constraints, migrations, even in templates. Tests are completely tied to the framework, and can’t be run without it.

All code has the potential to become legacy code over time. Still, based on my experience with these projects, it’s clear that tightly coupled code is more likely to become legacy code, and to be abandoned completely. Which is a waste of the expensive development effort that went into it. A part of the application (if not the whole application) still works and is relied upon by its users, and now it has to be rewritten. A rewrite is not guaranteed to be a successful process, and the user is likely to miss some features that weren’t noticed by the developers because they were obscured by framework integration code.

How can we prevent this from happening in the future? I think we, as software developers today, should make an effort to decouple from our frameworks. All our domain logic should be in plain sight, and our tests should be executed with only the most basic test framework. When the time comes to migrate to a different framework, maybe even upgrade to the next major version, it should be an easy job, finished within the course of a few weeks. It should never endanger the life of the project itself.

Decoupling, How to Do it Efficiently?

The trick is to keep a safe distance from your framework, while using some of its powerful features, so you don’t have to write them yourself. This can be achieved by decoupling, which has two components: you need to decouple your code, and you need to decouple your approach to software development from the approach advocated by the framework authors. You need to think about what your application has to offer to the user, then implement it with code that leverages the framework’s powers, but only in a few places.

Complete decoupling is undesirable as it would lead to too much work. Loose coupling is what we’re after. When you want to replace the thing you’re coupled to, that shouldn’t be too much work, and it should only involve a few local changes. In other words, the framework shouldn’t be all over the place. If you manage to do so, an automated refactoring tool like Rector will help you transform all the old-style integration code to code that matches the expectations of the new framework.

Objection

Whenever I propose to decouple from a framework, there’s always a common objection: “YAGNI” (You Ain’t Gonna Need It). Although this is not really an argument by itself - it could benefit from a few extra words - I think it hints at a valid concern. Should we decouple, even if we don’t know how long the project will live, or if we’ll ever face a framework migration? It shouldn’t be a surprise, but I think “yes”. If we don’t do it now, we’re introducing technical debt in the project. Of course, we can take the shortcut now, and write the code a bit faster today, but we know that we’ll have to do a lot of work later. As with any technical debt, we can accept it. However,

  1. I’m not entirely sure that we will be so much faster. Yes, in the very beginning of the project we can quickly slap a prototype together, but soon we’ll start running into framework limitations, quirks, and magic that we didn’t understand correctly. Personally I’ve spent a lot of time debugging forms, validation, entity mapping, and templating issues, and I’m a certified Symfony developer (well, I was part of the first class, 11 years ago at SymfonyLive Paris, so maybe that doesn’t count anymore).
  2. I’m not entirely sure that everyone is aware of how much debt they introduce when relying on frameworks and ORMs, or that going all-in on a framework should even be considered technical debt. This is something you only realize when you look at one of those almost-dying projects.

I think I’m not the only one who thinks that decoupling deserves to be part of our daily work as a programmer. I think programmers have always done this, because I keep recognizing design patterns that seem to have been invented for the sake of decoupling. Consider patterns from the old “Gang of Four” book, like Observer, later evolved into Event dispatcher, but also Adapter, etc. Or from Domain-Driven Design, e.g. Aggregate, Repository, Application service, and so on. Domain-Driven Design itself seems to be an exercise in moving focus away from frameworks and databases, the things we developers like so much, and divert our attention to the business domain itself. The same goes for development approaches like BDD, which explicitly aim to decouple specifications written as scenarios from the underlying technology, to keep a clear focus on what we’re doing.

With my writing I’m always looking to place myself as a developer in this long-standing tradition of software development that doesn’t revolve around a specific language, nor a specific framework. How can we write software in a way that is timeless, focuses on the business domain that it tries to serve, and produce applications that are long-term maintainable?

What’s Special About This Book?

In another book, Advanced Web Application Architecture, I’ve already described in detail how to create an application that primarily consists of a decoupled application core. It also shows how you can wrap this core inside any framework or connect it to any ORM you like. That book presents one over-arching vision of what I consider a good approach for application design. It helps you do domain-oriented, test-first development.

In this book, “Recipes for Decoupling”, I’m taking a much more light-weight approach. The focus isn’t on design patterns or architecture. I will sometimes reference the relevant concepts or patterns, but only as a way to point out how a decoupling recipe is related to this larger culture of software design practices. Instead of spending much time exploring the theory, we’ll mostly focus on the practice of decoupling. We’ll consider various popular frameworks and libraries that are used in web applications, and how you can decouple from them. This is done using small refactoring steps, showing how you can improve the structure of your code, while preserving the existing behavior. Something that’s very important with legacy code, of course.

As a reader, you can pick specific topics that are of interest to you, or that pose a particular risk in your current project, like “ORMs”, or “mocking libraries”. The decoupling approaches shown in each chapter can be applied in isolation. You don’t have to decouple your entire application at once. You also don’t need to go all the way on each refactoring. Most chapters offer an incremental series of refactorings that allow you to turn the dial on the level of decoupling you need today. If you like, you can do some more work if it provides additional benefits in the area of maintainability or maybe testability. You decide when to stop.

How to Stay Decoupled?

A refactor a day keeps the doctor away, but once your code is decoupled, you’ll need to be very disciplined to keep it decoupled. For instance, if you don’t want the service container to be used outside of controller classes, you have to continuously be aware of this rule. Not just you, all the other team members need to remember that they’re not allowed to use the container anywhere else. This is a risk for your project. It’ll be difficult to maintain that discipline over a longer period of time. We may oversee a particular “violation” of this rule in a PR, and accidentally merge it into the main branch. This single mistake becomes a precedent and will be copied many times; apparently “this is allowed now”. When all the developers who cared about decoupling have left the team, we can be sure that in the end all the rules will be ignored.

I believe that this tendency towards deterioration of a code base can be overcome by automation. Many of us have already worked with a project that performs some kind of automated build for each commit that we push to a remote branch. The build often consists of installing all dependencies (and checking that none of them have security vulnerabilities), linting the PHP code to catch potential syntax errors, running the tests, and verifying that the coding standard has been applied. These tools catch a lot of potential issues before we release them to main or even deploy them to production. If there is an issue, the build turns “red” and merging or deploying isn’t even possible.

With an automated build phase for a project, we have a way to prevent bad code from being merged. So if we can define our own decoupling rules as checks that run during the build, we have a way to prevent coupled code from being merged. We only need to find a relatively easy way to write those decoupling rules. Once we have a platform for this, we can specify rules and run them as part of the build, now and forever. This will prevent the developers from falling back into old habits, and will save our code from ever becoming coupled again.

While working on the Rector bookwith Tomas Votruba, he told me how he deals with user-contributed PRs for the Rector project itself. He noticed that he had to point out the same issues again and again. This took a lot of time, and led to delayed merges, and some frustration on both sides. He started using PHPStanin the project build to automatically report mistakes directly to the user, without human intervention.

PHPStan is a tool that statically analyses PHP code and points out potential problems related to incorrect types, argument counts, undefined methods, etc. It has a lot of built-in rules that can catch a lot of common programming mistakes before you ever run your code. On top of that, it also allows you to define your own rules. That’s what Tomas did for the Rector project. It didn’t only help its contributors; he was also able to worry less. If he’d forget about some rule, PHPStan would remind him of it.

Following Tomas’ lead, I’m doing the same for this book. Whenever we discuss a refactoring that leads to decoupled code, we’ll also look for ways to solidify the decoupling in the long run by writing a custom PHPStan rule. This is going to be much safer than relying on the personal discipline of every developer on the team.

Before we can do this, we should take a look at the process of writing a PHPStan rule. This involves a short introduction to PHPStan and how to write custom rules for it. You’ll find this introduction in the first chapter.

Who Should Read This Book?

I hope this book will be relevant for any developer who has used a framework, has sometimes been bitten by its magic, or has been unable to upgrade to the next framework version. This book will be useful if you’re looking for ways to make applications more maintainable in the long run. I expect only some experience with PHP and one of its currently popular frameworks.

Overview of the Contents

We start with a chapter that introduces PHPStan and how you can create custom rules for it. If you already use PHPStan and have some experience writing rules for it, feel free to skip this chapter.

Chapter 2 introduces common coupling issues related to web frameworks. We consider what would be needed to migrate a controller from a Symfony application to a Mezzio application, and use this process to gain some understanding about any potential framework migration. This gives us decoupling rules for dealing with request and response objects, service dependencies, and template rendering.

Chapter 3 covers similar issues, but for CLI frameworks. We take a look at a Symfony-based console command and find out how to decouple the business logic of this command from the terminal-based input and output objects.

Chapter 4 looks at form validation and how Laravel applications deal with this. We look for a way out of framework coupling by shifting our focus away from form validation. We introduce value objects and try to enforce constraints at the level of the model itself.

Chapter 5 zooms in on a type of library that often takes over a big part of our application and might therefore be considered a framework itself: the ORM. How to decouple from it in such a way that we don’t lose all the benefits? Part of the solution is in separating the “write” from the “read” activities of the ORM. At some point we realize that maybe we don’t need an ORM at all. This chapter has examples based on Laravel’s Eloquent as well as Doctrine ORM, because they don’t have much in common and both require a different approach to decoupling.

Just as tests are usually an after-thought (sadly so), the final chapter of this book dives into test frameworks and mocking libraries and describes some options for decoupling from them.

By means of a conclusion we’ll look back at all the refactoring and decoupling activities we did in this book, and we try to extract some common aspects. Rephrased as decoupling strategies they should help you decouple from things that don’t exist yet, or that we otherwise didn’t cover in this book.

About the Author

Matthias Noback has been building web applications with PHP since 2003. He is the author of the Object Design Style Guide and Advanced Web Application Architecture. He’s also a regular blogger, speaker and trainer. In his spare time he plays the violin and builds wooden 17th-century ship models.

Changelog

If you’ve bought an early version of this book, you can download all future updates for free. After each update I’ll describe what has been added or changed.

10 May 2022

Initial release.

17 May 2022

- Added Chapter 2, Web frameworks

  • Fixed the output of a PHPUnit run in Chapter 1 that was expected to show echo-ed node types (thanks for reporting, Christopher L Bray!).
  • Changed the returned rule in NoErrorSilencingRuleTest to NoErrorSilencingRule (thanks for reporting, Quentin Delcourt!)

26 May 2022

  • Added Chapter 3, CLI frameworks
  • Fixed some broken sentences, layout issues, and updated the link registry (thanks for reporting, Amir Ziapoor!)

31 May 2022

7 June 2022

15 June 2022

  • Added the missing sections about PHPStan rules to Chapter 5:
    • PHPStan rule: disallow auto-incrementing model IDs
    • PHPStan rule: only allow calls to fromDatabaseRecord() from repository classes
  • Added Chapter 6, Test frameworks
  • Processed feedback about Chapter 1 (thanks, Paul Rijke!)
    • Added a hint about running composer dump-autoload
    • Improved the comment about using customRulesetUsed instead of level
    • Added a hint about running only the rule tests
    • Improved the section about configuring a suffix

24 June 2022

-