Composition over inheritance in doctrine repositories
This article was originally published at http://adamquaile.com/composition-over-inheritance-with-doctrine-repo on 16th May 2015, but has since been migrated to Medium.
One of the first things many of us learn when getting to grips with OOP is inheritance. It allows us to reuse code and only modify the bits we need.
This is good, but if you’ve ever worked on a system that’s even moderately complex you may have felt the pain of overuse of this pattern. It can quickly lead to trouble. Most of the time, there is a better way. Most of the time, that way is composition.
Now, if you read the Symfony documentation on custom repository classes in doctrine, you’ll read that we can extend Doctrine’s
EntityRepository and add or override methods.
This is okay, but I have concerns. Not only do we have a nice repository class with a
findAllOrderedByName method, we have all of Doctrine's
findAllmethods, plus some magic methods based on your classes properties.
Slowly but surely, code which uses those repository classes will start to use all this inherent Doctrine functionality and we’ve let our implementation details leak into the rest of the application.
A better way?
Consider a different approach. To keep the layers properly separated and follow the dependency inversion principle, let’s think about what interface we want our product repository to have. For example:
Now we need a concrete implementation for Doctrine. But this time instead of extending our EntityRepository, let’s use composition. We’ll provide doctrine’s default repository and the entity manager in our constructor.
Now we have a class which presents the interface we want, and users of this class can’t rely specifically on Doctrine features, allowing our application code to remain nicely decoupled. Should we wish to change Doctrine for something else, maybe a file-based storage system, or an API, we’ll have a much easier time.
If you’re testing your code (which you should be) you can also create a Mock class implementing this interface.
You might notice that now we’re not extending the doctrine repository, we won’t have access to any private or protected methods from our new class. If you need these, you can mix the two approaches (inheritance and composition). Just make sure your application is reliant on the interface and not the implementation.
Implementing in Symfony
As for the config, the original class was configured via the Doctrine mapping file:
Instead, we will define our class (and the doctrine entity repository) as services:
Note that this would mean that calling
$entityManager->getRepository(Product::class)would no longer work, but it's generally better to inject your repository class anyway.