Symfony ExpressionLanguage : Comment utiliser ce composant ?
Le composant Symfony ExpressionLanguage : qu'est-ce que c'est ? Quand et comment l'utiliser ? Comment créer des expressions lors de cas plus complexes ?
Au cours de mes différentes expériences professionnelles, j'ai dû mettre en place de nombreuses règles métier dans diverses applications riches fonctionnellement. Un jour, j'ai été confronté à une façon de faire différente : l'utilisation du pattern specification. Cette méthode s’est avérée structurante pour les projets, et si vous ne la connaissez pas encore elle mérite qu’on s’y attarde.
Imaginez une application bancaire par exemple. Cette dernière comprend des clients et des comptes bancaires. Un client peut avoir un ou plusieurs comptes bancaires. Vous devez mettre en place un système hyper simple de virement bancaire entre comptes d’un même client, comprenant la règle métier suivante :
Vous apercevez donc clairement la condition qui empêcherait le virement bancaire de se faire pour deux comptes d’un même client.
Dans un cas classique, il vous serait possible d’écrire cela sous cette forme :
<?php
namespace ElevenLabs\Application;
use ElevenLabs\Domain\Payment\Entity\Account;
class TransferMoneyCommand
{
/**
* @param Amount $accountToDebit
* @param Amount $accountToCredit
* @param float $amount
*/
public function execute(Account $accountToDebit, Account $accountToCredit, $amount)
{
if ($accountToDebit->getBalance() - $amount > 0 && $accountToDebit->getOwner()->isActive()) {
//transfer authorized
//...
}
}
}
Cette règle métier, bien que triviale, doit être implémentée dès que l’on souhaite effectuer un virement. Il apparait plusieurs contraintes, suite à cette implémentation.
Tout d’abord, si notre règle métier évolue, nous devrons modifier la (ou les) classe(s) qui l’utilise(nt). Ensuite, une telle implémentation dans une condition if n’est pas explicite du tout.
C’est là qu’entre en scène le pattern spécification. L’idée de la spécification est d’isoler une règle métier, en la séparant de son utilisation. Elle est utilisée dans le cas de la validation, de la sélection et dans la construction de logique métier.
Il existe principalement trois types de spécifications :
Une spécification est régie par l'interface suivante :
<?php
namespace ElevenLabs\Domain;
interface Specification
{
/**
* @param $candidate
*
* @return bool
*/
public function isSatisfiedBy($candidate);
}
### Spécifications Hard-coded
Ce type de specifications permet de déclarer en dur la connaissance métier sans pouvoir modifier la règle métier de l'extérieur.
Une règle métier peut donc être, par exemple, traduite de la sorte :
<?php
namespace ElevenLabs\Domain\Payment;
use ElevenLabs\Domain\Specification;
class AccountCanTransferMoney implements Specification
{
/**
* @param \ElevenLabs\Domain\Payment\Entity\Account $account
*
* @return boolean
*/
public function isSatisfiedBy($account)
{
return $account->getBalance() > 0 && $account->getOwner()->isActive();
}
}
En ayant créé une classe séparée pour appliquer notre règle, nous gagnons en découplage et en clarté. Cependant, il apparait évident que nous sommes cantonnés à l'object $account, et qu'aucune information ne peut être apportée de l'extérieur. Nous ne pouvons toujours pas utiliser ce type de spécification dans notre TransferMoneyCommand car il ne répond pas totalement à notre règle métier (seul le solde actuel du compte est comparé).
Les spécifications paramétrées sont identiques au point précédent, sauf qu'elles résolvent le problème que nous venons d'indiquer en permettant de passer des paramètres extérieurs à notre candidate.
<?php
namespace ElevenLabs\Domain\Payment;
use ElevenLabs\Domain\Specification;
class AccountCanTransferMoney implements Specification
{
/** @var float */
private $amount;
/**
* @param float $amount
*/
public function __construct($amount)
{
$this->amount = $amount;
}
/**
* @param \ElevenLabs\Domain\Payment\Entity\Account $account
*
* @return boolean
*/
public function isSatisfiedBy($account)
{
return $account->getBalance() - $this->amount > 0 && $account->getOwner()->isActive();
}
}
Avec ce type de spécifications, nous gardons les mêmes avantages que précédemment, et nous gagnons en flexibilité.
Voici ce que donnerait notre commande avec l'utilisation de notre spécification paramétrée :
<?php
namespace ElevenLabs\Application;
use ElevenLabs\Domain\Payment\Entity\Account;
use ElevenLabs\Domain\Payment\Specification\AccountCanTransferMoney;
class TransferMoneyCommand
{
/**
* @param Account $accountToDebit
* @param Account $accountToCredit
* @param float $amount
*/
public function execute(Account $accountToDebit, Account $accountToCredit, $amount)
{
$accountCanTransferMoney = new AccountCanTransferMoney($amount);
if (true === $accountCanTransferMoney->isSatisfiedBy($accountToDebit)) {
//transfer authorized
//...
}
}
}
Pour simplifier l'explication des spécifications paramétrées, j'ai instancié la class AccountCanTransferMoney en dur. Une amélioration notable de cette utilisation serait d'injecter dans la commande la spécification, au lieu de l'instancier en dur, afin de pouvoir tester unitairement notre commande.
Le dernier type de spécification que nous aborderons aujourd'hui concerne la spécification composite. Cette dernière se base sur ce que nous venons de voir. En effet, ce pattern utilise une composition de spécifications pour exister. Les opérations logiques entre deux (ou plus) spécifications font parties des composite specifications.
L'exemple suivant vous explique l'implémentation de l'opération logique AND :
<?php
namespace ElevenLabs\Domain;
abstract class Composite implements Specification
{
/**
* {@inheritdoc}
*/
abstract public function isSatisfiedBy($candidate);
/**
* @param Specification $spec
*
* @return AndSpecification
*/
public function andIsSatisfiedBy(Specification $spec)
{
return new AndSpecification($this, $spec);
}
//...
}
class AndSpecification extends Composite
{
/** @var Specification */
private $a;
/** @var Specification */
private $b;
/**
* @param Specification $a
* @param Specification $b
*/
public function __construct(Specification $a, Specification $b)
{
$this->a = $a;
$this->b = $b;
}
/**
* {@inheritdoc}
*/
public function isSatisfiedBy($candidate)
{
return $this->a->isSatisfiedBy($candidate) && $this->b->isSatisfiedBy($candidate);
}
}
Ainsi, si l'on déclare une spécification composite, on peut la chainer à d'autres spécifications, comme ci-dessous, en modifiant notre spécification précédente AccountCanTransferMoney :
<?php
namespace ElevenLabs\Domain\Payment;
use ElevenLabs\Domain\Composite;
class AccountCanTransferMoney extends Composite
{
/** @var float */
private $amount;
/**
* @param float $amount
*/
public function __construct($amount = 0)
{
$this->amount = $amount;
}
/**
* @param \ElevenLabs\Domain\Payment\Entity\Account $account
*
* @return boolean
*/
public function isSatisfiedBy($account)
{
return $account->getBalance() - $this->amount > 0;
}
}
<?php
namespace ElevenLabs\Domain\Payment\Specification;
use ElevenLabs\Domain\Specification;
class AccountOwnerIsActive implements Specification
{
/**
* @param \ElevenLabs\Domain\Payment\Entity\Account $account
*
* @return boolean
*/
public function isSatisfiedBy($account)
{
return $account->getOwner()->isActive();
}
}
Enfin, voici comment utiliser notre composition :
<?php
namespace ElevenLabs\Application;
use ElevenLabs\Domain\Payment\Entity\Account;
use ElevenLabs\Domain\Payment\Specification\AccountCanTransferMoney;
class TransferMoneyCommand
{
/**
* @param Account $accountToDebit
* @param Account $accountToCredit
* @param float $amount
*/
public function execute(Account $accountToDebit, Account $accountToCredit, $amount)
{
$accountCanTransferMoney = new AccountCanTransferMoney($amount);
$accountOwnerIsActive = new AccountOwnerIsActive();
$compositeSpecification = $accountCanTransferMoney->andIsSatisfiedBy($accountOwnerIsActive);
if (true === $compositeSpecification->isSatisfiedBy($accountToDebit)) {
//transfer authorized
//...
}
}
}
Les avantages de ce type de spécifications sont bien sûr le support des opérations logiques, et donc la création de règles métier plus complexes. Il est maintenant possible de combiner les spécifications. La flexibilité est encore accrue, mais attention à la complexité générée !
Les avantages du pattern spécification sont les suivants :
Auteur(s)
Romain Pierlot
Diplomé de l'ISEP en 2013, Romain Pierlot est ingénieur en Etudes et Développement chez Eleven Labs, avec qui il s'amuse comme un petit fou.
Vous souhaitez en savoir plus sur le sujet ?
Organisons un échange !
Notre équipe d'experts répond à toutes vos questions.
Nous contacterDécouvrez nos autres contenus dans le même thème
Le composant Symfony ExpressionLanguage : qu'est-ce que c'est ? Quand et comment l'utiliser ? Comment créer des expressions lors de cas plus complexes ?
Découvrez comment réaliser du typage générique en PHP : introduction et définition du concept, conseils et explications pas-à-pas de cas pratique.
Découvrez un cas d'usage d'intégration d'un CRM avec une application e-commerce, en asynchrone, avec Hubspot et RabbitMQ