A way to implement generic repositories in Symfony

Davide Romanelli
2 min readJul 9, 2024

--

It often happens that when we use the repository pattern all the interfaces we write have the same basic methods, for example save(), findOneById() or findAll(). Unlike Java and C#, PHP still doesn’t support generics. However, we can use PHPDoc and static analysis tools like PHPStan to cleanly abstract the basic methods shared by all repositories without the need to rewrite them in each class.

First of all, let’s create a GenericRepositoryInterface. To declare interface methods generic, we need to use @template annotation:

<?php

namespace App\Core\Repository;


/**
* @template T
*/
interface GenericRepositoryInterface
{
/**
* Save an entity
*
* @param T $entity
* @return T
*/
public function save($entity);

/**
* Find an entity by id
*
* @param int $id
* @return T|null
*/
public function findOneById(int $id);

/**
* Find all entities
*
* @return T[]
*/
public function findAll(): array;
}

Now, we need to implement methods in an AbstractRepository class:

<?php declare(strict_types=1);

namespace App\Core\Repository\ORM;

use App\Core\Repository\GenericRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\ObjectRepository;

/**
* @template T
* @implements GenericRepositoryInterface<T>
*/
abstract class AbstractRepository implements GenericRepositoryInterface
{
protected EntityManagerInterface $em;
protected ObjectRepository $repository;

/**
* @param EntityManagerInterface $em
* @param string $entityClass
*/
public function __construct(EntityManagerInterface $em, string $entityClass)
{
$this->em = $em;
$this->repository = $this->em->getRepository($entityClass);
}

/**
* @inheritDoc
*/
public function save($entity)
{
$this->repository->persist($entity);
$this->repository->flush();

return $entity;
}

/**
* @inheritDoc
*/
public function findOneById(int $id)
{
return $this->repository->findOneBy(['id' => $id]);
}

/**
* @inheritDoc
*/
public function findAll(): array
{
return $this->repository->findBy([]);
}
}

Now let’s create a Customer entity:

<?php declare(strict_types=1);

namespace App\Core\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity()
* @ORM\Table(name="customers")
*/
class Customer extends AbstractEntity
{
/** @ORM\Column(name="first_name", type="string", nullable=false) */
private string $firstName;

/** @ORM\Column(name="last_name", type="string", nullable=false) */
private string $lastName;

/** @ORM\Column(name="email", type="string", unique=true, nullable=false) */
private string $email;

/**
* @param string $firstName
* @param string $lastName
* @param string $email
*/
public function __construct(string $firstName, string $lastName, string $email)
{
$this->firstName = $firstName;
$this->lastName = $lastName;
$this->email = $email;
}

public function getFirstName(): string
{
return $this->firstName;
}

public function getLastName(): string
{
return $this->lastName;
}

public function getEmail(): string
{
return $this->email;
}
}

Let’s create a CustomerRepository interface and its implementation.

<?php declare(strict_types=1);

namespace App\Core\Repository;

use App\Core\Entity\Customer;

/**
* @extends GenericRepositoryInterface<Customer>
*/
interface CustomerRepositoryInterface extends GenericRepositoryInterface
{
}
<?php declare(strict_types=1);

namespace App\Core\Repository\ORM;

use App\Core\Entity\Customer;
use App\Core\Repository\CustomerRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;

/**
* @implements CustomerRepositoryInterface<Customer>
*/
final class CustomerRepository extends AbstractRepository implements CustomerRepositoryInterface
{
public function __construct(EntityManagerInterface $em,)
{
parent::__construct($em, Customer::class);
}
}

Now we can use CustomerRepositoryInterface in our services and inherit all methods written in GenericRepositoryInterface having signatures that support Customer entity:

Happy coding! :)

--

--