Repositories are just Collections

Repositories are just collections of things. A post repository is a collection of posts, a user repository a collection of users. They allow to abstract away all the persistence details. Yet, many people think of Doctrine repositories as purely related to Doctrine, leading to strange abstractions. Let's create a normal collection with Symfony and Doctrine today!

It all starts with the Interface

When designing your application, start with designing your interfaces. If you like your interfaces, the implementation will probably be nice as well. In this post, we're going to design a collection of products. The interface first contains some basic collection methods:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// src/AppBundle/Product/ProductRepository.php
namespace AppBundle\Product;

interface ProductRepository extends \Countable
{
    /** @return Product[] */
    public function all();

    /** @return bool */
    public function includes(Product $product);

    /** @return int */
    public function count();
}

We can now count the number of products in the collection, get all products in the collection and check if a product is included in the collection. Now, let's add two simple methods for more specific operations on this collection:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// src/AppBundle/Product/ProductRepository.php

// ...
interface ProductRepository extends \Countable
{
    // ...
    public function add(Product $product);

    /** @return null|Product */
    public function byId($id);
}

A Basic Implementation

The most straight-forward way to implement collections in PHP is by using arrays. Let's create an array based product collection:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// src/AppBundle/Product/ArrayBasedProductRepository.php
namespace AppBundle\Product;

class ArrayBasedProductRepository implements ProductRepository
{
    private $products = [];

    public function all()
    {
        return $this->products;
    }

    public function includes(Product $product)
    {
        return in_array($product, $this->products);
    }

    public function count()
    {
        return count($this->products);
    }

    public function add(Product $product)
    {
        $this->products[$product->getId()] = $product;
    }

    public function byId($id)
    {
        if (!isset($this->products[$id])) {
            return null;
        }

        return $this->products[$id];
    }
}

Most of the methods in the interface can be mapped to simple PHP array methods. This repository can be defined as a service:

1
2
3
4
# app/config/services.yml
services:
    app.product_repository:
        class: AppBundle\Product\ArrayBasedProductRepository

Then you can probably imagine how we would use this product collection in a controller:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/AppBundle/Controller/ShopController.php
namespace AppBundle\Controller;

// ...
class ShopController extends Controller
{
    public function listAction()
    {
        $products = $this->get('app.product_repository')->all();

        return $this->render('shop/products.twig', [
            'products' => $products,
        ]);
    }

    public function productInfoAction($productId)
    {
        $product = $this->get('app.product_repository')->byId($productId);

        return $this->render('shop/product_info.twig', [
            'product' => $product,
        ]);
    }
}

Using Doctrine

So far, we've only looked at normal repositories. I hope this brought the point across that repositories are just collections. The nice thing about repositories is that we can implement all sort of different backends for our collections. As this post is also about Doctrine, let's create a product collection based on Doctrine.

The EntityRepository shipped with the Doctrine ORM is a simple wrapper class around the entity manager and the unit of work. The class isn't used in Doctrine itself, it's purely an easy starting point for the users. This means we can completely drop it and base our doctrine collection on top of the entity manager:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// src/AppBundle/Product/DoctrineBasedProductRepository.php
namespace AppBundle\Product;

use Doctrine\ORM\EntityManager;

class DoctrineBasedProductRepository implements ProductRepository
{
    private $entityManager;

    public function __construct(EntityManager $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function all()
    {
        // Create a basic DQL query to fetch all entities
        return $this->entityManager->createQuery('SELECT p FROM '.Product::class.' p')
            ->getResult();
    }

    public function includes(Product $product)
    {
        // Check if the entity is managed by the entity manager
        return $this->entityManager->contains($product);
    }

    public function count()
    {
        // Create a basic count DQL query
        return $this->entityManager->createQuery('SELECT count(p) FROM '.Product::class.' p')
            ->getSingleScalarResult();
    }

    public function add(Product $product)
    {
        $this->entityManager->persist($product);
    }

    public function byId($id)
    {
        // Fetch a product by id (note: No need to use DQL or the EntityRepository here either!)
        return $this->entityManager->find(Product::class, $id);
    }
}

As you can see, the code in here is just a bit more complex than using the array repository. Even better, the usage of this repository has not changed a single bit when comparing it with the ArrayBasedProductRepository. So we only have to update the service definitions to use doctrine:

1
2
3
4
5
# app/config/services.yml
services:
    app.product_repository:
        class: AppBundle\Product\DoctrineBasedProductRepository
        arguments: ['@doctrine.orm.entity_manager']

Treat Repositories as Collections

As I tried to show in this post, treating repositories as collections has some significant advantages:

  • The collection implementation is abstracted away, not much hassle with Doctrine anymore;
  • All collection methods are located in one class/service. This means you never has to rely on both the entity repository and manager (in order to fetch and persist) anymore;
  • Working with Doctrine as if it's a collection makes programming easier to follow.

My last warning of this post is to not add non-collection related methods in the repository. I sometimes see people adding getXxxQuery() methods in the repository, as e.g. the form type requires you to set a query. These methods tell something about the implementation. They are no longer related to collections (try to think what this method should return in the ArrayBasedProductRepository).

Take Home's

  • The EntityRepository class shipped by Doctrine is purely to ease usage, it's not mandatory at all;
  • Writing your own repositories allow you to abstract away Doctrine almost completely from your services and controllers;
  • Repositories are just collections, please treat them as such.
comments powered by Disqus