• adamquaile.com

    Composition over inheritance in doctrine repositories

    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.

    You can read more on the topic here, and by watching the talk Nothing is Something by Sandy Metz.

    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.

    class ProductRepository extends EntityRepository
    {
        public function findAllOrderedByName()
        {
            return $this->getEntityManager()
                ->createQuery(
                    'SELECT p FROM AppBundle:Product p ORDER BY p.name ASC'
                )
                ->getResult();
        }
    }

    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 findBy, findOneBy, findAll methods, 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:

    interface ProductRepository
    {
        public function findById($productId);
        public function save(Product $product);
    }

    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.

    class DoctrineProductRepository implements ProductRepository
    {
        private $manager;
        private $repo;
    
        public function __construct(EntityManager $manager, EntityRepository $repository)
        {
            $this->manager = $manager;
            $this->repo = $repo;
        }
        public function findById($productId)
        {
            return $this->repo->find($productId);
        }
        public function save(Product $product)
        {
            $this->manager->persist($product);
            $this->manager->flush();
        }
    }

    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:

    # src/AppBundle/Resources/config/doctrine/Product.orm.yml
    AppBundle\Entity\Product:
        type: entity
        repositoryClass: AppBundle\Entity\ProductRepository
        # ...

    Instead, we will define our class (and the doctrine entity repository) as services:

    services:
        example.doctrine_product_repository:
            class: Doctrine\ORM\EntityRepository
            factory_service: doctrine.orm.default_entity_manager
            factory_method: getRepository
            arguments:
                - Example\Product\Product
            public: false
        example.product_repository:
            class: Example\Product\DoctrineProductRepository
            arguments:
              - @doctrine.orm.default_entity_manager
              - @example.doctrine_product_repository

    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.