Dans mon précédent article, je vous ai présenté le design pattern Etat. Il est temps de vous présenter une méthode pour l’implémenter dans vos projets PHP.

J’ai écrit la bibliothèque States qui permet d’implémenter facilement ce comportement à vos objets, sans à avoir recours à une extension PHP tierce.

Nous pouvons l’installer facilement avec composer avec la commande suivante :

composer require teknoo/states


Présentation

Dans la bibliothèque States, les classes Context sont appelés Proxy et nécessite l’implémentation de l’interface ProxyInterface. Un trait est fourni pour vous éviter de l’écrire vous même.

Depuis la version 3, la bibliothèque nécessite PHP7.1 ou plus, les classes Proxy sont autonomes. Dans les versions précédentes, elles nécessitent une factory pour être instanciées.

Les états sont représentés par des classes implémentant l’interface StateInterface. Un trait est également disponible. Ces dernières doivent être listées dans le proxy, dans la méthode statique statesListDeclaration.

Les méthodes de ces états seront les méthodes appelées par notre objet Proxy. Les mots clés $this->, self:: et static:: représenteront l’objet (l’instance) Proxy ou la classe et non la classe State.

La visibilité des attributs de la classe Proxy, ainsi que des méthodes de cette même classe et de ses états sont conservés. Ainsi un élément protected ne sera accessible qu’aux méthodes de la classe et à ses enfants, les éléments private uniquement aux méthodes de ladite classe.

Les classes enfants à la classe Proxy hérite des états de leurs classes parentes. Il est possible de les étendre ou de les redéfinir.

Attention,
Suite à une restriction interne à PHP, les méthodes des états doivent retourner une closure implémentant le corps de la méthode. C’est cette dernière qui sera appelée.
De plus, aucun attribut ne peut être déclaré dans ces états, ils seront ignorés.

Implementation

En suivant notre pseudo code de l’article précédent, son implémentation nous donne :

<?php

namespace Acme;

require 'vendor/autoload.php';

use Closure;
use DateTimeImmutable;
use DateTimeInterface;
use Teknoo\States\Proxy\ProxyInterface;
use Teknoo\States\Proxy\ProxyTrait;
use Teknoo\States\State\StateInterface;
use Teknoo\States\State\StateTrait;

class French implements StateInterface
{
    use StateTrait;

    private function sayHello(): Closure
    {
        return function(string $you): string {
            return 'Bonjour ' . $you . ', ' . $this->name . ', je viens de ' . $this->country;
        };
    }

    private function displayDate(): Closure
    {
        return function(): string {
            return $this->birthday->format('d m Y');
        };
    }
}

class English implements StateInterface
{
    use StateTrait;

    private function sayHello(): Closure
    {
        return function(string $you): string {
            return 'Hello ' . $you . ', ' . $this->name . ', I come from ' . $this->country;
        };
    }

    private function displayDate(): Closure
    {
        return function(): string {
            return $this->birthday->format('m d, Y');
        };
    }
}

class Person implements ProxyInterface
{
    use ProxyTrait;

    public function __construct(
        private string $name,
        private DateTimeInterface $birthday,
        private string $country
    ) {
        $this->initializeStateProxy();
    }

    protected static function statesListDeclaration(): array
    {
        return [
            French::class,
            English::class,
        ];
    }

    private function selectLanguage(string $country): self
    {
        if ('France' === $country) {
            return $this->switchState(French::class);
        }

        return $this->switchState(English::class);
    }

    public function hello(string $country, string $you)
    {
        return $this->selectLanguage($country)->sayHello($you);
    }

    public function birthday(string $country)
    {
        return $this->selectLanguage($country)->displayDate();
    }
}

$frenchMan = new Person('Roger', new DateTimeImmutable('1970-04-10'), 'France');

echo $frenchMan->hello('France', 'Jean').PHP_EOL;
echo $frenchMan->birthday('England').PHP_EOL;

//Display
//Bonjour Jean, Roger, je viens de France
//04 10, 1970

Automatisation

La bibliothèque propose un mécanisme permettant de sélectionner automatiquement le ou les états appropriés en fonction des attributs de l’objet. La sélection s’effectue à l’aide d’une liste d’assertions à définir via la méthode listAssertions().

La sélection n’est cependant pas exécuté automatiquement, mais uniquement lors de l’appel à la méthode updateStates(). Cette dernière peut être appelé à tout moment, dans le constructeur, avant ou après chaque appel, dans une méthode de la classe Proxy ou de ses états, voir depuis une méthode ou fonction extérieure.

Nous pouvons adapter notre classe Person pour parler la langue de son pays natal lorsque son interlocuteur ne lui spécifie pas le langage demandé.

<?php

namespace Acme;

require 'vendor/autoload.php';

use Closure;
use DateTimeImmutable;
use DateTimeInterface;
use Teknoo\States\Automated\AutomatedInterface;
use Teknoo\States\Automated\AutomatedTrait;
use Teknoo\States\Automated\Assertion\Property;
use Teknoo\States\Automated\Assertion\Property\IsEqual;
use Teknoo\States\Proxy\ProxyInterface;
use Teknoo\States\Proxy\ProxyTrait;
use Teknoo\States\State\StateInterface;
use Teknoo\States\State\StateTrait;

class French implements StateInterface
{
    use StateTrait;

    public function hello(): Closure
    {
        return function($you): string {
            return 'Bonjour ' . $you . ', ' . $this->name . ', je viens de ' . $this->country;
        };
    }

    public function birthday(): Closure
    {
        return function(): string {
            return $this->birthday->format('d m Y');
        };
    }
}

class English implements StateInterface
{
    use StateTrait;

    public function hello(): Closure
    {
        return function($you): string {
            return 'Good morning ' . $you . ', ' . $this->name . ', I come from ' . $this->country;
        };
    }

    public function birthday(): Closure
    {
        return function(): string {
            return $this->birthday->format('m d, Y');
        };
    }
}

class Person implements ProxyInterface, AutomatedInterface
{
    use ProxyTrait;
    use AutomatedTrait;

    public function __construct(
        private string $name,
        private DateTimeInterface $birthday,
        private string $country
    ) {
        $this->initializeStateProxy();
        $this->updateStates();
    }

    protected static function statesListDeclaration(): array
    {
        return [
            French::class,
            English::class,
        ];
    }

    protected function listAssertions(): array
    {
        return [
            (new Property([English::class]))
                ->with('country', new IsEqual('England')),
            (new Property([French::class]))
                ->with('country', new IsEqual('France')),
        ];
    }

    public function spoke(string $country): self
    {
        $this->country = $country;
        return $this->updateStates();
    }
}

$frenchMan = new Person('Roger', new DateTimeImmutable('1970-04-10'), 'France');

echo $frenchMan->hello('Jean').PHP_EOL;
echo $frenchMan->spoke('England')->birthday().PHP_EOL;

//Display
//Bonjour Jean, Roger, je viens de France
//04 10, 1970

Avec cette ultime évolution, notre objet va automatiquement parler la langue de son pays. Il est possible d’aller plus loin, la bibliothèque implémente d’autres assertions, basées sur les opérations numériques, ou sur des closures.

Cette évolution implémente également une variante. L’interlocuteur lui indique via la méthode spoke() la langue désirée, elle n’est plus passée en paramètre. Les méthodes hello() et birthday() sont directement implémentées dans les états et donc accessibles (si l’état est actif).

Commentaires

zzz
zzz
eee

aaaa

fff
fff
ss

dd

qqq
qqq
www

eee

aaa
aaa
aaaa

aa

sdsf
sdsf
sdfsdf@sdf.fr

sdf