Since I've been writing a lot about decoupled application development it made sense that one of my readers asked the following question: "Why should we use a framework?" The quick answer is: because you need it. A summary of the reasons:
- It would be too much work to replace all the work that the framework does for you with code written by yourself. Software development is too costly for this.
- Framework maintainers have fixed many issues before you even encountered them. They have done everything to make the code secure, and when a new security issue pops up, they fix it so you can just pull the latest version of the framework.
- By not using a framework you will be decoupled from Symfony, Laravel, etc. but you will be coupled to Your Own Framework, which is a bigger problem since you're the maintainer and it's likely that you won't actually maintain it (in my experience, this is what often happens to projects that use their own home-grown framework).
So, yes, you/we need a framework. At the same time you may want to write framework-decoupled code whenever possible.
Here's a summary of the reasons. If all of your code is coupled to the framework:
- It will be hard to keep up with the framework's changes. When their API changes, or when their conventions or best practices change, it takes just too much time to update the code base.
- It's hard to test any business logic without going through the front controller, that is, by making fake or real web requests to your application, analyzing the response HTML, or peeking into the database.
- It's hard to test anything at all, because nothing allows itself to be tested in isolation. You always have to set up a database schema, populate it with data, or boot a service container of some kind.
Pushing for a big and strong core of decoupled code, that isn't tied to the database technology, or a particular web framework, will give you a lot of freedom, and prevents all of the above problems. How to write decoupled code? There's no need to reinvent the wheel there either. You can rely on a catalog of design patterns, like:
- Application services and command objects
- Entities and repository interfaces
- Domain events and domain event subscribers
None of these classes will use framework-specific things like:
- Request, Response, Session, Token storage, or security User classes,
- Service locators, configuration helpers, dependency resolvers,
- Database connections, query builders, relation mappers, or whatever your framework calls them.
For me good rules of thumb to test the "decoupledness" of my business logic are:
- Can I migrate this application from a web to a CLI application without touching any of the core classes?
- Can I instantiate all the classes in the core of my application without preparing some special context or setting up external services?
- Can I migrate this application from an SQL database to a document database without touching any of the core classes?
1 and 2 should be unconditionally true, 3 allows some room for coupling due to the age-old problem of mapping entities to their stored format. For instance, you can have some mapping logic in your entity (i.e. instructions for your ORM on how to save the entities). But at least there shouldn't be any service dependencies that are specific to your choice of persistence, e.g. you can't inject an
EntityManagerInterface or use a
QueryBuilder anywhere in your code classes. Also, calling methods should never trigger actual calls to a database, even if it's an Sqlite one.
If you do all of this, your framework will be like a layer wrapped around your decoupled core:
This layer contains all the technical stuff. This is where you find the acronyms: SQL, ORM, AMQP, HTTP, and so on. This is where we shouldn't do everything on our own. We leverage the power of many frameworks and libraries that save us from dealing with all the low-level concerns, so we can focus on business logic and user experience.
A framework should help you:
- Make a smooth jump from an incoming HTTP request to a call to one of your controllers.
- Load, parse, and validate application configuration.
- Instantiate any service needed to let you do your work.
- Translate your data to queued messages that can be consumed by external workers.
- Parse command-line arguments and pass them as ready-to-consume primitive-type values.
- Turn your application's data into database records and back to data you can use in your application.
- Make HTTP requests to external services and deal with connection issues and error status codes for you.
- And so on...
For me, a sufficiently good framework will be one that:
- Does a good job at all of this, so I don't have to replace (or extend) any classes with my own implementations.
- Provides a good developer experience by helping me use it. I don't have to read the code to find out why it doesn't work.
- Saves me time thinking about all that stuff by providing the right abstractions.
- Has good, complete, and up to date documentation, so everyone can look up how it's done.
- Has clear upgrade instructions that make transitioning between versions easy (or even automatable).
Ideally, a framework also:
- Makes a clear distinction between service objects and other types of objects.
- Offers immutable objects for me to work with, and stateless services that can be called any number of times.
- Does not offer globally accessible functions for retrieving services or configuration.
- Doesn't even make it possible for me to change its behavior by extending their classes. Inheritance is a dangerous form of coupling after all.
Note that these aren't necessities. When limited to the technology layer around your decoupled core, a framework can use all kinds of practices that I consider bad. As long as these practices don't influence the design of the decoupled core, this could be fine. However, it requires some discipline. No cutting corners when it comes to your decoupled core!
What is your opinion on using the QueryBuilder in repository classes? Do you consider repository classes as part of the core? I use Symfony and Doctrine ORM and I use the QueryBuilder in my repository classes. And at the same time I inject the EntityManagerInterface (indirectly using ManagerRegistry when extending ServiceEntityRepository) into my repository classes.
And what about the persist() and flush() calls on the EntityManagerInterface. I like to call them from a service object, which I would consider part of the core, but according to your blog I should not do this. Where would you call persist() and flush() ?
I'm not sure how common this is in the PHP world, but if I had to guess: the concepts are gaining popularity, but it's still hard to apply them in practice. Of course, the ideas are quite old.