Soyez spécifiques

Kévin Gomez / @KPhoen

Le problème

Gérer l'accès aux données dans une application

Les contraintes

Isolation nette entre la “couche données” et le “reste du monde”

Pourquoi ?

SGBD X en dév/prod, In memory en test
Pas de Doctrine qui fuite

La solution ?

Facile : on fait des repositories !

À la Doctrine


class DbCompanyRepository implements CompanyRepository
{
    public function findBySlug($slug) { }
    public function findPublic() { }

    # …
}
                

Problèmes ?

  • persistance/suppression d’entités ?
    • facile : on encapsule ces actions dans le repository.
  • 1 requête ≊ 1 méthode
    • Hum.

État des lieux


class DbCompanyRepository implements CompanyRepository
{
    public function add(Company $company) { }
    public function remove(Company $company) { }

    public function findBySlug($slug) { }
    public function findPublic() { }
    public function findPrivate() { }
    public function findDeleted() { }
    public function findByNameLike($search) { }
    public function findByNameOrDescriptionLike($search) { }
    public function findPublicByNameOrDescriptionLike($search) { }

    # …
}
                

Ce que nous dit la Bible

“A Repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection. Client objects construct query specifications declaratively and submit them to Repository for satisfaction.”
Martin Fowler — http://martinfowler.com/eaaCatalog/repository.html

Ce que nous dit la Bible

Le pattern “Specification”

Retour à la Bible

“The central idea of Specification is to separate the statement of how to match a candidate, from the candidate object that it is matched against.”
Martin Fowler — http://martinfowler.com/apsupp/spec.pdf

Spécifions

  • une spécification = une règle métier
  • les spécifications sont composables
    • AND, OR, NOT

Spécifions

  • En tant que X, je veux lister les entreprises :
    • publiques ET vers Lyon

$specification = new Specification('publique ET vers Lyon');
                

Repositories


class DbCompanyRepository implements CompanyRepository
{
    public function add(Company $company) { }
    public function remove(Company $company) { }

    public function matching(Specification $spec) {}
}
                

RulerZ !

Une spécification


class CompanyPublic implements Specification
{
    public function getRule()
    {
        return 'public = 1';
    }

    public function getParameters()
    {
        return [];
    }
}
                

Dans l'application


$specs = new RulerZ\Spec\AndX([
    new Domain\Spec\CompanyPublic(),
    new Domain\Spec\CompanyNear('Lyon'),
]);

$companies = $repository->matching($specs);
                

Le repository


public function matching(Specification $spec)
{
    $qb = $this->createQueryBuilder('c');

    return $this->rulerz->satisfiesSpec($qb, $spec);
}
                

État des lieux

  • isolation des données via les repositories : ✓
  • séparation du métier et des opérations de sélection : ✓
    • Doctrine/Pomm/Elasticsearch/in-memory/…

Dans la vraie vie

Un repository


class DbCompanyRepository implements CompanyRepository
{
    public function add(Company $company) { }
    public function remove(Company $company) { }

    public function matching(Specification $spec) {}

    public function findBySlug($slug) { }
}
                

RulerZ + Form Component


class CompanySearchType extends AbstractType
{
  public function buildForm($builder, $options)
  {
    $who = $builder
      ->create('who')
      ->addModelTransformer(
          new SpecToStringTransformer(Spec\CompanyName::class, 'terms')
      );

    $builder->add($who);
  }

  // ...
}
                

RulerZ + Form Component


public function searchAction(Request $request)
{
    $results = [];
    $form    = $this->formFactory->create('company_search');
    $form->handleRequest($request);

    if ($form->isValid()) {
        $spec = new Spec\AndX(array_merge(   // aggregate
            array_filter($form->getData()),  // remove empty specs
            [new AppSpec\CompanyPublished()] // only display published companies
        ));
        $results = $this->companyRepository->matchingSpec($spec);
    }

    return $this->render('...');
}
                

Pour aller plus loin