Cómo decorar nuestros repositorios en Symfony

Hoy os quiero hablar de una técnica bastante útil a la hora de añadir funcionalidad a nuestros repositorios sin tener que recurrir a la herencia. Ya sabéis ese principio de diseño que dice:

Favorece la composición frente a la herencia

Además, ésto nos va a permitir repasar el patrón decorador, el cual, si no lo conocéis, permite añadir funcionalidad en tiempo real a nuestros objetos decorándolos con nuevos métodos que son añadidos mediante composición. De este modo, no ahorramos tener que crear una superclase con los métodos que queremos añadir y que después nuestras clases extiendan de dicha clase para adquirirlos.

Si queréis leer más sobre este patrón podéis hacerlo en la wikipedia:

o en cualquier libro de patrones de diseño que encontréis por ahí.

Y ahora, ¡vamos al lío!

Descripción del problema

Imaginad dos entidades A y B (las cuales no tienen que ver la una con la otra en nada) que sin embargo poseen todas la misma propiedad createdAt y el cliente quiere que le hagamos el típico panel de estadísticas con las entidades que se han ido creando para cada tipo según el día seleccionado.

Lo primero que se nos viene a la cabeza es añadir a nuestros repositorios lo siguiente:

class RepositoryA {
  public function findTodayStats(): int {
    $today = new \DateTime();
    return $this->createQueryBuilder('o')
      ->select('COUNT(o) as total')
      ->andWhere('o.createdAt = :today')
      ->setParameter('today', $today)
      ->getQuery()
      ->getSingleScalarResult();
    ;
  }
}

Y lo mismo para RepositoryB . Claro, en el momento en que tengamos 100 entidades vamos a tener el mismo código repetido otras tantas veces, lo cual no mola nada:

Solución con herencia

Es aquí donde se nos puede iluminar la bombilla y decir… Oye. Creamos una clase padre y que todos los repositorios extiendan de ella para obtener el mismo método:

class ParentRepository {
  public function findTodayStats(): int {
    $today = new \DateTime();
    return $this->createQueryBuilder('o')
      ->select('COUNT(o) as total')
      ->andWhere('o.createdAt = :today')
      ->setParameter('today', $today)
      ->getQuery()
      ->getSingleScalarResult();
    ;
  }
}
class RepositoryA extends ParentRepository { // }
class RepositoryB extends ParentRepository { // }

El tema es que (a mí por lo menos) me suena algo raro que repositorios que no tienen nada que ver empleen la herencia para adquirir comportamiento. Es más, si tenemos 4 o 5 casos de estos os podéis imaginar la cascada que se nos puede formar de clases (porque puede ser que no todas las clases necesiten los mismos métodos y si extendieran todas de la misma poseerían métodos que no necesitan).

Es aquí donde entra al rescate el patrón decorador.

El patrón decorador

Pese a que no sea una de las características más conocidas de Symfony, es posible decorar nuestros servicios para añadirles nueva funcionalidad sin tener que recurrir a la herencia. Y, salvo que hayamos especificado que las clases de nuestra carpeta Repository no sean inyectadas como repositorios, todos ellos serán ya de por sí servicios que podemos inyectar directamente (aunque la práctica más habitual sea inyectar el EntityManagerInterface y recoger de ahí los repositorios).

Aprovechándonos de esto podremos decorar nuestros repositorios de la siguiente manera.

Lo primero será crear un repositorio que posea el método que queremos:

Este servicio también extiende la clase ServiceEntityRepository así que, al igual que en los repositorios que son generados cuando empleamos el comando make:entity , en el constructor inyectaremos el servicio RegistryInterface . Pero además emplearemos un segundo argumento que será con el que implementemos el patrón decorador.

Este segundo argumento llamado$repository es el repositorio que estamos decorando de modo que, al invocar el constructor de la clase padre, lo hacemos con la clase de este repositorio. Por tanto, nuestra clase EntityStatsDecoratorRepository poseerá todos los métodos típicos del repositorio de la clase A (por ejemplo, findOneBy , findBy ) además del nuevo método findTodayStats .

Vale muy bien, pero… ¿y ahora cómo empleamos este repositorio decorado?

Supongamos que necesitamos emplear el repositorio de la clase A pero con el método findTodayStats decorado en un servicio cualquiera ( AnyService ).

Partiremos del siguiente código:

(Nota: Cómo veis, estoy inyectando directamente el repositorio en el servicio, ya que para los servicios decorados no nos servirá emplear $doctrine->getRepository )

use App\Repository\RepositoryA;
class AnyService {
  public function __construct(RepositoryA $repo) {

$this->repo = $repo
  }
  .... // rest

Lo que haremos será declarar un nuevo servicios en nuestro archivo services.yaml del siguiente modo:

app.repository.a.stats:
  class: App\Repository\EntityStatsRepositoryDecorator
  decorates: App\Repository\RepositoryA
  arguments:
    $repository: '@app.repository.a.stats.inner'

Es aquí donde sucede la magia.

  • La línea 1 declara el nombre de nuestro nuevo repositorio, en este caso app.repository.a.stats , al que he añadido el .stats para identificar que está decorado (convención propia mía).
  • La línea 2 declara la clase que usa, que en nuestro caso es la clase que hemos creado anteriormente App\Repository\EntityStatsRepositoryDecorator .
  • La línea 4 declara los argumentos de nuestro servicio, y puesto que RegistryInterface ya se inyecta mediante el autowire de Symfony, lo que haremos será declarar que la propiedad $repository de nuestro constructor será el servicio que está siendo decorado por este servicio: @app.repository.a.stats.inner es decir, App\Repository\RepositoryA .

Con esto ya podremos usar nuestro repositorio decorado en nuestro servicio AnyService de la siguiente forma:

App\Service\AnyService

arguments:
    $repo: '@app.repository.a.stats'

Puesto que estaréis de acuerdo conmigo que hacer esto todo el rato que queramos inyectar el servicio decorado es muy tedioso, podemos realizar lo siguiente:

services:
  _defaults:
    autowire: true
    autoconfigure: true
    bind:
      $aRepoWithStats: '@app.repository.a.stats'

De modo que siempre que usemos en el constructor de un servicio el argumento $aRepoWithStats obtendremos nuestro repositorio:

use App\Repository\RepositoryA;
class AnyService {
public function __construct(ServiceEntityRepository $aRepoWithStats) {

$this->repo = $aRepoWithStats
}
.... // rest

Ahora ya decorar el resto de repositorios es tan sencillo como declarar un nuevo servicio:

app.repository.b.stats:
  class: App\Repository\EntityStatsRepositoryDecorator
  decorates: App\Repository\RepositoryB
  arguments:
    $repository: '@app.repository.b.stats.inner'

Añadirlo al bind de services.yaml :

bind:
  $bRepoWithStats: '@app.repository.b.stats'

Y todo listo!

Nota. El servicio decorado queda convertido en un alias del servicio decorador. Es decir, que si inyectáis el servicio App\Repository\RepositoryA lo que obtendréis es el servicio ya decorado.

Si queréis leer más sobre la decoración de servicios podéis hacerlo en la documentación oficial: