Disponible pour vos projets! Contactez-moi

Utiliser Symfony Expression Language

Utiliser Symfony Expression Language Image

Expression Language est un très bon composant Symfony permettant de créer des expressions pouvant être évaluées et compilées en langage PHP. Il est principalement utilisé par le framework Symfony2 pour ajouter une autre dimension de configuration mais peut être intégré dans tout projet PHP.

Un des cas d'utilisation auquel j'ai pensé est un moteur de règles qui pourrait être utilisé dans les applications d'e-commerce. Les règles de remise peuvent être définies comme des expressions et appliquées au prix de base du produit afin de calculer son prix final. Les développeurs seraient en mesure de configurer les règles de tarification pour leurs clients sans avoir à écrire une ligne de code.

Dans la plupart des CMS e-commerce, les utilisateurs doivent sélectionner des valeurs, des opérateurs, des dates afin de produire des règles de remise très limitées. alors qu'ils pourraient simplement entrer des expressions d'une ligne pour créer des règles de n'importe quelle complexité.

Pour comprendre cet article, vous devez déjà connaître le composant. Vous pouvez trouver la documentation ici.

Le modèle de produit

Premièrement, nous devons définir notre modèle de produit car les règles de remise dépendent des attributs du produit.

<?php

namespace MyProject\Model;

class Product
{
    /**
     * Le prix de base.
     *
     * @var float
     */
    private $basePrice;

    /**
     * Le nombre d'articles en stock.
     *
     * @var integer
     */
    private $stock;

    /**
     * La date de création du produit.
     *
     * @var \DateTime
     */
    private $creationDate;

    public function setBasePrice($basePrice)
    {
        $this->basePrice = $basePrice;
    }

    public function getBasePrice()
    {
        return $this->basePrice;
    }

    public function setStock($stock)
    {
        $this->stock = $stock;
    }

    public function getStock()
    {
        return $this->stock;
    }

    public function setCreationDate(\DateTime $creationDate)
    {
        $this->creationDate = $creationDate;
    }

    public function getCreationDate()
    {
        return $this->creationDate;
    }
}

Déterminer les règles et fonctions de base

Avant de coder, nous devons déterminer quelles fonctions nous voulons rendre disponibles dans notre language.

Nous aimerions créer ce type de règles:

appliquer une remise de 10% s'il ne reste qu'un article en stock
appliquer une remise de 5% pendant les soldes (du 20/01/2014 au 02/02/2014 par exemple)
appliquer 50% de réduction si le produit a été créé il y a plus d'un an et que nous avons moins de cinq articles en stock

Nous avons donc besoin des fonctions suivantes:

  • date(time) Cette fonction renvoie l'heure spécifiée en tant que DateTime. Il faudra transformer les dates du 20/01/2014 et du 02/02/2014 en instances de DateTime pour effectuer des opérations sur elles comme spécifié dans la deuxième règle. Cette fonction fait la même chose que DateTime::__construct(time) en PHP.

  • date_modify(date, modify) Cette fonction modifie la date et la retourne. Il sera nécessaire d'incrémenter la date de création du produit d'un an dans la troisième règle. C'est la même fonction que DateTime::modify(time) en PHP.

Traduites en expressions (en utilisant nos fonctions), les règles de remise deviennent:

product.getStock() == 1 ? 0.1 : 0
date('now') >= date('2014-01-20') and date('now') <= date('2014-02-02') ? 0.05 : 0
date('now') < date_modify(product.getCreationDate(), '+1 year') and product.getStock() < 5 ? 0.5 : 0

A l'aide de l'opérateur ternaire, chaque règle évalue le coefficient de remise qui sera appliqué au prix du produit (par exemple 0,1 pour 10%).

Créer notre DSL

La classe Language étend la classe ExpressionLanguage de base et représente notre Domain Specific Language.

<?php

namespace MyProject\Price;

use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

final class Language extends ExpressionLanguage
{
    protected function registerFunctions()
    {
        // Enregistrer notre fonction 'date'
        $this->register('date', function ($date) {
            return sprintf('(new \DateTime(%s))', $date);
        }, function (array $values, $date) {
            return new \DateTime($date);
        });

        // Enregistrer notre fonction 'date_modify'
        $this->register('date_modify', function ($date, $modify) {
            return sprintf('%s->modify(%s)', $date, $modify);
        }, function (array $values, $date, $modify) {
            if (!$date instanceof \DateTime) {
                throw new \RuntimeException('date_modify() expects parameter 1 to be a Date');
            }
            return $date->modify($modify);
        });
    }
}

Construire le moteur

Le moteur de tarification utilisera notre language personnalisé pour calculer le prix d'un produit.

<?php

namespace MyProject\Price;

use MyProject\Model\Product;

class Engine
{
    private $language;
    private $discountRules = array();

    /**
     * Crée un nouveau moteur de tarification.
     *
     * @param Language $language Notre language personnalisé
     */
    public function __construct(Language $language)
    {
        $this->language = $language;
    }

    /**
     * Ajoute une règle de remise.
     *
     * @param string $expression L'expression de remise
     */
    public function addDiscountRule($expression)
    {
        $this->discountRules[] = $expression;
    }

    /**
     * Calcule le prix du produit.
     *
     * @param Product $product Le produit
     *
     * @return float Le prix
     */
    public function calculatePrice(Product $product)
    {
        $price = $product->getBasePrice();
        foreach ($this->discountRules as $discountRule) {
            $price -= $price * $this->language->evaluate($discountRule, array('product' => $product));
        }

        return $price;
    }
}

Exemples

Maintenant que le moteur a été développé, nous pouvons l'utiliser avec quelques exemples.

Selon le moment où vous exécuterez ce code, vous obtiendrez des résultats différents en raison des règles basées sur la date d'aujourd'hui!

<?php

use MyProject\Price\Language;
use MyProject\Price\Engine;
use MyProject\Model\Product;

$language = new Language();
$engine = new Engine($language);

// Ajout des règles précédemment définies
$engine->addDiscountRule("product.getStock() == 1 ? 0.1 : 0");
$engine->addDiscountRule("date('now') >= date('2014-01-20') and date('now') <= date('2014-02-02') ? 0.05 : 0");
$engine->addDiscountRule("date('now') < date_modify(product.getCreationDate(), '+1 year') and product.getStock() < 5 ? 0.5 : 0");

// Création d'un nouveau produit
$product = new Product();
$product->setStock(10);
$product->setCreationDate(new DateTime());
$product->setBasePrice(100);

// Affiche 100 (pas de remise)
echo $engine->calculatePrice($product);

// On change le stock à 1
$product->setStock(1);

// Affiche 45 car
// - la première règle est exécutée => -10% => 90
// - la troisième règle est exécutée => -50% => 45
echo $engine->calculatePrice($product);

Conclusion

Le système que nous avons construit est très simpliste car nous ne prenons pas en compte si des remises peuvent être cumulées. Dans la plupart des systèmes, les remises sont ordonnées (par exemple, les taxes sont appliquées à la fin). Cela pourrait être fait en créant des groupes de règles. En outre, nos règles s'appliquent à tous les produits, tandis que dans les systèmes réels, ils peuvent également être associés à des produits ou à des catégories spécifiques.

En résumé, le composant Symfony2 Expression Language est très puissant et permet de créer des moteurs de règles personnalisés en utilisant des fonctions simples.