Archives pour la catégorie PHP

PHP – Le design pattern Etat

Le design pattern Etat est un design pattern comportemental; son utilisation est préconisée dès lors que le comportement d’un objet dépend directement de l’état dans lequel il se trouve à l’instant T.

Programmeur énervé par l'absence de pattern Etat

Programmeur se mettant dans un état pas possible à la lecture d’un code écrit voilà 10 ans

Architecture du design pattern Etat

Les participants à ce design pattern Etat sont au nombre de 3:

  • Une abstraction (interface ou classe abstraite) qui liste les différents comportements devant être implémentés par des états concrets
  • Des états concrets réalisant chacune des méthodes listées dans l’abstraction évoquée ci-dessus
  • Un contexte définissant l’interface que le code client va utiliser et ayant en composition les états concrets auquel il va simplement déléguer les requêtes lui parvenant

Where’s the bill, Bill?

Prenons un exemple parlant: le commerce en ligne ! Une commande faite en ligne traverse toute une série d’états; elle peut être créée, validée, payée ou mise en recouvrement, annulée, remboursée, expédiée etc. Evidemment certaines transitions d’état ne peuvent pas se faire: quand elle est expédiée, elle ne peut pas être annulée et inversement.

Notre site de vente en ligne d’ouvrages informatique CODEBOOKS reçoit plusieurs centaines de commandes par jour.
Une commande est créée dans l’état « En Attente », ce qui signifie qu’elle est en attente d’une validation, manuelle ou automatique. Cette validation peut intervenir ou pas, si le moyen de paiement s’avère frauduleux ou s’il ne parvient pas dans les délais précisés dans les conditions générales de vente.

Une fois qu’elle est validée, elle est préparée puis expédiée afin d’être livrée. C’est notre scénario nominal, le Best Case Scenario.
D’autres scenarii peuvent se produire: l’acheteur réalise à la livraison qu’il a déjà le livre ou bien qu’il l’a commandé dans une langue qu’il ne sait pas lire; il devra être remboursé après renvoi du livre à CODEBOOKS. Une erreur de stock peut également conduire à valider une commande portant sur un livre non disponible, elle devra donc être annulée puis remboursée.

Tes états d’âme, Eric…(air bien connu)

Nos états

Chacun de nos états fera l’objet d’une classe concrète dérivant une classe abstraite (ou implémentant une interface, au choix). J’ai choisi une classe abstraite dans laquelle je factorise mon constructeur, pour éviter de le répéter bêtement dans chaque classe fille car il fait dans tous les cas la même chose. Cette classe abstraite implémente une interface qui va contraindre chacune des classes filles à implémenter l’ensemble des méthodes qu’un état pourra mettre à disposition. Notez bien que le contexte est en composition de chacun des états concrets, pour garder la trace de l’état courant d’une commande.

interface EtatInterface
{
    public function mettreEnAttente(): void;
    public function valider(): void;
    public function annuler(): void;
    public function rembourser(): void;
    public function expedier(): void;
    public function signalerLivre(): void;
}

abstract class EtatAbstract implements EtatInterface
{
    protected $contexte;
    
    public function __construct(ContexteInterface $contexte)
    {
        $this->contexte = $contexte;
    }
}

Après étude, nous avons déterminé les états possibles d’une commande:

  • En Attente: c’est l’état par défaut, celui dans lequel se trouve toute commande « atterrissant » sur notre système d’information
  • Annulé
  • Validé
  • Expédié
  • Remboursé
  • Livré

Vous allez me dire « Mais avec cette interface, l’état Annulé va se retrouver à devoir implémenter une méthode expedier alors qu’il ne doit pas pouvoir transiter vers cet état ! » et vous aurez tout à fait raison ! C’est comme ça, il nous faut une interface qui liste toutes les méthodes qu’on peut invoquer sur l’ensemble des états pour faire plaisir à ce bon vieux Liskov !

Dès qu’une méthode sera appelée sur un état qui ne doit pas l’implémenter, nous lèverons une exception maison nommée MethodNotImplementedException:

class MethodNotImplementedException extends \Exception {}

Le contexte

Notre contexte va être appelé par le code client, c’est lui qui va garder une trace de l’état courant de notre commande. Il a en composition une instance de chacun des différents types d’états disponibles dans lesquels il va s’auto-injecter et à sa construction, il mettra l’état courant à sa valeur par défaut, à savoir « en attente ». Il propose une série de getters (sauf pour enAttente car nous postulons qu’aucun état ne peut effectuer de transition vers cet état initial) ainsi que les méthodes que le code client appellera. Vous notez que ces méthodes font de la délégation bête et méchante. L’unique setter est crucial pour tenir le contexte à jour des changements d’état intervenus dans l’application.

interface ContexteInterface
{
    public function etatValide(): EtatInterface;
    public function etatAnnule(): EtatInterface;
    public function etatExpedie(): EtatInterface;
    public function etatLivre(): EtatInterface;
    public function etatActuel(): EtatInterface;
    public function changerEtat(EtatInterface $etat): void;
}

class Contexte implements ContexteInterface
{
    private $enAttente;
    private $valide;
    private $expedie;
    private $livre;
    private $rembourse;
    private $annule;
    private $etatActuel;

    public function __construct()
    {
        $this->enAttente = new EnAttente($this);
        $this->valide = new Valide($this);
        $this->expedie = new Expedie($this);
        $this->livre = new Livre($this);
        $this->rembourse = new Rembourse($this);
        $this->annule = new Annule($this);
        
        $this->etatActuel = $this->enAttente;
    }
    
    public function etatValide(): EtatInterface
    {
        return $this->valide;
    }
    
    public function etatAnnule(): EtatInterface
    {
        return $this->annule;
    }
    
    public function etatExpedie(): EtatInterface
    {
        return $this->expedie;
    }
    
    public function etatLivre(): EtatInterface
    {
        return $this->livre;
    }
    
    public function etatRembourse(): EtatInterface
    {
        return $this->rembourse;
    }
    
    public function etatActuel(): EtatInterface
    {
        return $this->etatActuel;
    }
    
    public function changerEtat(EtatInterface $etat): void
    {
        $this->etatActuel = $etat;
    }
    
    public function valider(): void
    {
        $this->etatActuel->valider();
    }
    
    public function expedier(): void
    {
        $this->etatActuel->expedier();
    }
    
    public function signalerCommeLivre(): void
    {
        $this->etatActuel->signalerLivre();
    }
    
    public function effectuerRemboursement(): void
    {
        $this->etatActuel->rembourser();
    }
    
    public function effectuerAnnulation(): void
    {
        $this->etatActuel->annuler();
    }
}

Nos états…concrets !

EnAttente

Prenons notre première classe concrète: EnAttente. Cet état peut effectuer des transitions vers les états Validé (si le paiement est valide) ou Annulé (dans le cas contraire). Il ne peut pas le faire vers lui-même ainsi que vers les états Remboursé, Expédié et Livré car à ce stade le paiement de la commande n’est pas une certitude.

class EnAttente extends EtatAbstract
{   
    public function mettreEnAttente(): void
    {
        throw new MethodNotImplementedException('Déjà en attente');
    }
    
    public function valider(): void
    {
        $this->contexte->changerEtat($this->contexte->etatValide());
    }
    
    public function annuler(): void
    {
        $this->contexte->changerEtat($this->contexte->etatAnnule());
    }
    
    public function rembourser(): void
    {
        throw new MethodNotImplementedException('En attente');
    }
    
    public function expedier(): void
    {
        throw new MethodNotImplementedException('En attente');
    }
    
    public function signalerLivre(): void
    {
        throw new MethodNotImplementedException('En attente');
    }
}

Annulé

Comme tous les états, Annule ne peut aller vers lui-même et il n’effectue de transition qu’en direction de Remboursé.

class Annule extends EtatAbstract
{
    public function mettreEnAttente(): void
    {
        throw new MethodNotImplementedException('Annulé');
    }
    
    public function valider(): void
    {
        throw new MethodNotImplementedException('Annulé');
    }
    
    public function annuler(): void
    {
        throw new MethodNotImplementedException('Déjà annulé');
    }
    
    public function rembourser(): void
    {
        $this->contexte->changerEtat($this->contexte->etatRembourse());
    }
    
    public function expedier(): void
    {
        throw new MethodNotImplementedException('Annulé');
    }
    
    public function signalerLivre(): void
    {
        throw new MethodNotImplementedException('Annulé');
    }
}

Validé

Valide peut évoluer en Annulé (opposition bancaire ou autres) ou en Expédié, si tout se déroule normalement.

class Valide extends EtatAbstract
{    
    public function mettreEnAttente(): void
    {
        throw new MethodNotImplementedException('Validé');
    }
    
    public function valider(): void
    {
        throw new MethodNotImplementedException('Déjà validé');
    }
    
    public function annuler(): void
    {
        $this->contexte->changerEtat($this->contexte->etatAnnule());
    }
    
    public function rembourser(): void
    {
        throw new MethodNotImplementedException('Validé');
    }
    
    public function expedier(): void
    {
        $this->contexte->changerEtat($this->contexte->etatExpedie());
    }
    
    public function signalerLivre(): void
    {
        throw new MethodNotImplementedException('Validé');
    }
}

Expedié

Cet état peut effectuer des transitions uniquement vers Livré.

class Expedie extends EtatAbstract
{   
    public function mettreEnAttente(): void
    {
        throw new MethodNotImplementedException('Expédié');
    }
    
    public function valider(): void
    {
        throw new MethodNotImplementedException('Expédié');
    }
    
    public function annuler(): void
    {
        throw new MethodNotImplementedException('Expédié');
    }
    
    public function rembourser(): void
    {
        throw new MethodNotImplementedException('Expédié');
    }
    
    public function expedier(): void
    {
        throw new MethodNotImplementedException('Déjà expédié');
    }
    
    public function signalerLivre(): void
    {
        $this->contexte->changerEtat($this->contexte->etatLivre());
    }
}

Remboursé

C’est un état terminal.

class Rembourse extends EtatAbstract
{
    public function mettreEnAttente(): void
    {
        throw new MethodNotImplementedException('Remboursé');
    }
    
    public function valider(): void
    {
        throw new MethodNotImplementedException('Remboursé');
    }
    
    public function annuler(): void
    {
        throw new MethodNotImplementedException('Remboursé');
    }
    
    public function rembourser(): void
    {
        throw new MethodNotImplementedException('Déjà Remboursé');
    }
    
    public function expedier(): void
    {
        throw new MethodNotImplementedException('Remboursé');
    }
    
    public function signalerLivre(): void
    {
        throw new MethodNotImplementedException('Remboursé');
    }
}

Livré

Cet état ne peut transiter que vers Annulé, quand le client reçoit un produit qui ne lui convient pas. L’annulation donnera ensuite lieu à un remboursement.

class Livre extends EtatAbstract
{
    public function mettreEnAttente(): void
    {
        throw new MethodNotImplementedException('Livré');
    }
    
    public function valider(): void
    {
        throw new MethodNotImplementedException('Livré');
    }
    
    public function annuler(): void
    {
        $this->contexte->changerEtat($this->contexte->etatAnnule());
    }
    
    public function rembourser(): void
    {
        throw new MethodNotImplementedException('Livré');
    }
    
    public function expedier(): void
    {
        throw new MethodNotImplementedException('Livré');
    }
    
    public function signalerLivre(): void
    {
        throw new MethodNotImplementedException('Déjà Livré');
    }
}

Le code client

Nous avons à notre disposition une classe Commande; elle possède une référence vers le contexte, qui va ainsi garder trace des différents états qu’elle traverse. C’est elle que notre code client va appeler.

class Commande
{
    private $contexte;
    
    public function __construct(ContexteInterface $contexte)
    {
        $this->contexte = $contexte;
    }
    
    public function valider(): void
    {
        $this->contexte->valider();
        echo "Etat = ".get_class($this->contexte->etatActuel()).PHP_EOL;
    }

    public function expedier(): void
    {
        $this->contexte->expedier();
        echo "Etat = ".get_class($this->contexte->etatActuel()).PHP_EOL;
    }
    
    public function signalerCommeEtantLivre(): void
    {
        $this->contexte->signalerCommeLivre();
        echo "Etat = ".get_class($this->contexte->etatActuel()).PHP_EOL;
    }
    
    public function procederAuRemboursement(): void
    {
        $this->contexte->effectuerRemboursement();
        echo "Etat = ".get_class($this->contexte->etatActuel()).PHP_EOL;
    }
 
    public function effectuerUneAnnulation(): void
    {
        $this->contexte->effectuerAnnulation();
        echo "Etat = ".get_class($this->contexte->etatActuel()).PHP_EOL;
    }
    
}

$contexte = new Contexte();
$commande = new Commande($contexte);

Voici notre scénario nominal, ou best case scenario (fingaz crossed!) :

$commande->valider();
$commande->expedier();
$commande->signalerCommeEtantLivre();

La commande va traverser 4 états: elle va passer de « en attente » à « validée », puis à « expédiée » et enfin à « livrée ».

Voici le scénario où la commande est validée mais une erreur de stock conduit à devoir l’annuler et la rembourser:

$commande->valider();
$commande->effectuerUneAnnulation();
$commande->procederAuRemboursement(); // état TERMINAL

Dans le scénario suivant, le produit est livré mais l’acheteur réalise qu’il l’a déjà ou bien qu’il s’est trompé lors de sa commande et le produit de remplacement n’est plus disponible dans le stock:

$commande->valider();
$commande->expedier();
$commande->signalerCommeEtantLivre();
$commande->effectuerUneAnnulation();
$commande->procederAuRemboursement(); // état TERMINAL

Enfin, dans notre dernier scénario, le paiement n’est pas arrivé à temps (mandat, virement bancaire, chèque…) et il faut annuler la commande ! C’est le plus simple:

$commande->effectuerUneAnnulation();

PHP – Le design pattern Strategy

Le design pattern Strategy fait partie de la famille des design patterns comportementaux; il facilite l’utilisation d’algorithmes interchangeables à l’exécution, c’est à dire dynamiquement. Il obéit au bon vieux principe de la programmation orientée objet : Encapsuler ce qui varie.

Design pattern Strategy

Les algorithmes à encapsuler

Pour un comportement identique (reagir) nous avons trois implémentations différentes. Ce sont elles que nous souhaitons isoler pour réduire au maximum l’impact du changement qui tôt ou tard se produira dans notre code. Ce comportement commun attend en paramètre un objet se conformant à l’interface StrategieInterface et ses implémentations variables consistent à ici à appliquer un traitement rudimentaire à la méthode donnerPhrase de cet objet en paramètre.

interface StrategieInterface
{
    public function reagir(PersonneInterface $personne): string;
}
 
class Enerve implements StrategieInterface
{
    public function reagir(PersonneInterface $personne): string
    {
        return strtoupper($personne->donnerPhrase().' !!!').PHP_EOL;
    }
}

class Geek implements StrategieInterface
{
    public function reagir(PersonneInterface $personne): string
    {
        return str_replace('o', '0', $personne->donnerPhrase()).PHP_EOL;
    }
}

class Jovial implements StrategieInterface
{
    public function reagir(PersonneInterface $personne): string
    {
        return ucfirst($personne->donnerPhrase()).' :)'.PHP_EOL;
    }
}

Voici notre classe Personne et l’interface qu’elle implémente. La seule chose qu’elle fait est de retourner la chaîne de caractères bonjour !

interface PersonneInterface
{
    public function donnerPhrase(): string;
}

class Personne implements PersonneInterface
{
    public function donnerPhrase(): string
    {
        return 'bonjour';
    }
}

L’objet Contexte

Voici enfin notre objet Contexte, partie intégrante de notre pattern. Il gère simplement une référence à la stratégie, à laquelle il transmet les requêtes des clients via la délégation à la méthode reagir. C’est en quelque sorte le liant entre le code client et nos différentes stratégies concrètes.

class Contexte
{
    private $strategie;

    public function __construct(StrategieInterface $strategie)
    {
        $this->strategie = $strategie;
    }
    
    public function exprimeReaction(PersonneInterface $personne): string
    {
      return $this->strategie->reagir($personne);
    }
}

Pour finir, écrivons le code client qui va utiliser notre design pattern. Evidemment, nous ne nous adressons pas directement aux stratégies mais nous passons par l’objet Contexte, que nous initialiserons successivement avec des instances de chacune des stratégies concrètes.

$personne = new Personne();
$humeurs = [new Enerve(), new Geek(), new Jovial()];

foreach ($humeurs as $humeur) {
    $contexte = new Contexte($humeur);
    echo $contexte->exprimeReaction($personne);
}

A garder en tête:

  • Un trop grand nombre de if (syndrome appelé la forêt d’ifs) est souvent le signe que Strategy doit être envisagé
  • Gare à la multiplication intempestive des stratégies concrètes, leur nombre peut vite augmenter !
  • Toutes les stratégies concrètes implémentent la même abstraction, il peut parfois arriver qu’elles reçoivent des informations qui ne leur seront pas utiles

Le design pattern Prototype en PHP

Prototype est un design pattern de création: son but est de répliquer des instances dites « prototypes » via un mécanisme de clonage.
Il est prescrit lorsqu’il convient d’éviter de trop nombreuses instanciations d’une classe, notamment lorsque celle-ci possède une logique assez complexe dans son constructeur, ce qui idéalement ne devrait jamais arriver.

Il est important d’économiser les ressources car le coût d’un clonage d’objet reste bien inférieur à celui d’une instanciation. Evidemment, vous ne verrez pas la différence si vous travaillez avec une dizaine d’objets mais ce ne sera sans doute pas le cas si votre application doit gérer 100 000 objets simultanément le jour d’une facturation par exemple.

prototype de voiture

Protype attendant un clonage incognito

Les composants de Protoype

  • une abstraction (classe abstraite ou interface) qui force la présence d’une méthode dédiée au clonage dans les classes concrètes
  • une ou plusieurs classes concrètes dériveront l’abstraction, implémentant de fait la méthode de clonage. Les instanciations seront faites sur ces classes
  • des clones de ces classes seront effectués par le code client, qui participe à ce pattern

Voici notre abstraction; elle force l’implémentation d’une méthode __clone() et par chance, PHP en fourni une « out of the box » comme on dit (oui, on peut aussi dire « nativement ») ! Le reste est très basique: un constructeur sans aucune logique métier, qui prend ce qu’on lui donne et le range consciencieusement, un setter et deux getters.

L’abstraction

abstract class HumainAbstract
{   
    protected $prenom;
    
    protected $sexe;
        
    public function __construct(string $prenom)
    {
        $this->prenom = $prenom;
    }
    
    public function donnerSexe(): string
    {
        return $this->sexe; 
    }
    
    public function changerPrenom(string $prenom)
    {
        $this->prenom = $prenom;
    }
    
    public function donnerPrenom(): string
    {
        return $this->prenom; 
    }
    
    abstract public function __clone();
}

Les classes concrètes

Les classes concrètes – les prototypes – vont dériver cette classe abstraite et, pour rester simples, ne feront absolument rien dans leur méthode __clone() ! Elles ont une variable d’instance qu’il est juste possible de lire mais pas de modifier. Seul le prénom est modifiable (regardez dans la classe abstraite !).

class Male extends HumainAbstract
{
    protected $sexe = 'M';

    public function __clone()
    {
    }
}

class Femelle extends HumainAbstract
{
    protected $sexe = 'F';
    
    public function __clone()
    {
    }
}

Le code client

Le client est un acteur à part entière du design pattern prototype, c’est lui qui va cloner les prototypes.
Ici nous décidons que, les premiers humains ayant sans doute communiqué entre eux par un langage très primitif, notre prototype de mâle s’appellera « Rrrnnngrrwggl », tandis que la femelle portera le doux sobriquet de « Nyyyynyaaa ».

Tous les êtres humains descendant de ces respectables parents à la pilosité prononcée ne seront que des clones dont nous changerons uniquement le prénom. Vous noterez que nous utilisons l’affectation dynamique, c’est à dire que chaque clone sera rangé dans une variable dont le nom sera incrémenté à chaque itération de notre boucle.

$prenoms = [
        'René', 'Eric', 'Jean', 'Robert', 'Marius',
        'Kevin', 'Léo', 'Jacques', 'Loïc', 'John',
        'Alexis', 'Kenneth', 'Nathanaël', 'Christophe'
    ];
    
$male1 = new Male('Rrrnnngrrwggl');

$numeroClone = 0;
$clones = [];

foreach ($prenoms as $prenom) {
    ++$numeroClone;
    
    $nomClone = 'clone'.$numeroClone;
    $$nomClone = clone $male1;
    $$nomClone->changerPrenom($prenom);
    $clones[] = $$nomClone;
}

$prenoms = [
        'Lise', 'Marie', 'Ninon', 'Rachida', 'Ana',
        'Martine', 'Svetlana', 'Eve', 'Carole',
        'Sylvie', 'Laurie', 'Zhang', 'Fatoumata'
    ];

$femelle1 = new Femelle('Nyyyynyaaa');
    
foreach ($prenoms as $prenom) {
    ++$numeroClone;
    
    $nomClone = 'clone'.$numeroClone;
    $$nomClone = clone $femelle1;
    $$nomClone->changerPrenom($prenom);
    $clones[] = $$nomClone;
}

foreach ($clones as $clone) {
    echo $clone->donnerSexe().'/'.$clone->donnerPrenom().PHP_EOL;
}

A retenir

  • N’implémentez pas Prototype si vous devez gérer un petit nombre de copies, ce serait tuer une mouche au lance-roquettes
  • Lors d’un appel à __clone(), le constructeur n’est pas appelé (c’est le but)
  • Si votre objet à cloner a des objets en compositions, pensez à les cloner eux aussi pour éviter de pointer vers une référence qui n’est pas la bonne…
  • Prenez garde aux références circulaires (A dépend de B qui dépend de C qui dépend de A…) entre les objets

PHP 7: Un tri(vial) avec l’opérateur combiné spaceship

Si vous êtes au courant des quelques nouveautés – plus si nouvelles que ça d’ailleurs – de PHP 7, cet opérateur combiné au nom amusant ne vous est pas inconnu ! Ce spaceship, censé imiter la forme des TIE Fighters de Star Wars, est utilisé pour effectuer des comparaisons. Cet opérateur existe déjà depuis longtemps en Ruby, avec lequel je travaille également. PHP emprunte ce qu’il y a de meilleurs à ses concurrents depuis des années et c’est très bien ainsi ! Peut-être sera-t-il un jour fortement typé ? Je digresse…

TIE Fighter

« Votre manque de typage me consterne »


Revenons à notre « vaisseau spatial »…Son rôle est de comparer deux valeurs: 0 est renvoyé si elles sont égales, 1 si la valeur à gauche est la plus grande et -1 dans le cas contraire. Il nous évite d’avoir à écrire ce genre de one-liner peu ragoûtant:

$ret = ($a === $b ? 0 : ($a > $b ? 1: -1));

Nous allons l’utiliser dans un cas assez trivial, celui d’un tri effectué à l’aide de la fonction usort. Je dis trivial car au final, on aurait pu utiliser sort et le résultat aurait été le même. Le but ici est de s’amuser un peu avec les nouveautés PHP 7 et notamment le STH (Scalar Type Hinting), dont j’ai déjà parlé ici en 2015.

La fonction usort trie les éléments d’un tableau en leur appliquant la fonction de tri que vous avez défini. C’est dans cette dernière que nous allons faire usage du spaceship. Attention toutefois, elle doit obligatoirement retourner une valeur de type entier, sinon le résultat risque de vous décevoir…Notez aussi qu’usort trie votre tableau en place, c’est à dire qu’il le modifie (passage par référence dans la liste des paramètres de la fonction) et n’en génère pas un nouveau.

Voici ce que donne le code, en faisant usage des pratiques PHP 7:


$tableau = array (10, 6, 12, 0, 687, 10, 6, 238, -6);

function compare(int $a, int $b) : int {
  return $a <=> $b;
}

usort($tableau,'compare');

print_r($tableau);

J’avais dit « trivial »…je n’ai pas menti ! En réalité on utilise plutôt usort quand le tri a effectuer est assez complexe, avec par exemple des tableaux associatifs, mais ceci est une autre histoire…

Zend Framework 2 : supprimer l’appel à getServiceLocator dans les contrôleurs

Vous avez peut-être récemment joué avec la Skeleton Application de Zend Framework (ou pire, remarqué ça lors d’une mise à jour) et en lançant votre application, vous avez vu s’afficher le message suivant, du à l’appel de la méthode getServiceLocator dans votre contrôleur (AlbumController, pour ceux qui ont tenté de suivre le tutoriel) :

Deprecated: You are retrieving the service locator from within the class Album\Controller\AlbumController. Please be aware that ServiceLocatorAwareInterface is deprecated and will be removed in version 3.0, along with the ServiceLocatorAwareInitializer. You will need to update your class to accept all dependencies at creation, either via constructor arguments or setters, and use a factory to perform the injections.

Pour faire simple, on vous avertit que dans la version future de ZF, cette façon de faire sera considérée comme obsolète (j’aime entendre les gens dire « déprécatède ») et qu’il vaudrait mieux injecter la dépendance à la création de votre objet, en l’occurrence ici celui qui résulte de l’instanciation de la classe AlbumController (voir cette page).

Ah...heu...bon...OK, soit !

Ah…heu…bon…OK, soit !


Pour mettre en place quelque chose d’un peu élégant, je vous invite à faire appel à une factory dans votre fichier module/Album/config/module.config.php :

     'factories' => array(
                      'Album\Controller\Album' => 'Album\Factory\AlbumControllerFactory'
                    ),
     ...

Voici le code de votre factory, placée si ce n’est pas déjà fait, dans un répertoire éponyme:

namespace Album\Factory;

use Album\Controller\AlbumController;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

use Album\Service\ControllerService;

class AlbumControllerFactory implements FactoryInterface
{
    /**
    * Create service
    *
    * @param ServiceLocatorInterface $sl
    *
    * @return Album\Controller\AlbumController
    */
    public function createService(ServiceLocatorInterface $sl)
    {
        $cs = new ControllerService($sl->getServiceLocator()->get('doctrine.entitymanager.orm_default'));
        return new AlbumController($cs);
    }
}

Dans ma factory, qui est la seule à savoir comment se fabrique un contrôleur de type Album, je récupère le service que j’injecte dans le constructeur d’AlbumController (relisez le message d’erreur ci-dessus, on vous dit bien qu’il vaudrait mieux injecter la dépendance à la création de votre objet, ce que vous faites). Ici j’utilise comme service l’Entity Manager de Doctrine, mais ça pourrait être n’importe quel autre service, évidemment !

Regardons maintenant le constructeur que j’ai rajouté dans mon contrôleur:

    protected $service;

    public function __construct(\Album\Service\ControllerServiceInterface $service) {
        $this->service = $service->getService();
    }

J’injecte dans mon constructeur les dépendances de celui-ci, sous la forme d’une interface (le po-ly-mor-phisme, que diable !). Cette interface c’est moi qui l’ai écrite, dans mon répertoire Service, elle est simplissime :

 namespace Album\Service;

 interface ControllerServiceInterface
 {
    public function getService();
 }

Elle dit que tout objet qui l’implémente se doit d’avoir une méthode qui permette d’accéder au service qu’il contient.

Dans le même répertoire, j’ai écrit un service dédié à mon contrôleur :

 namespace Album\Service;

 class ControllerService implements ControllerServiceInterface
 {
    protected $service;

    public function __construct($service) {
        $this->service = $service;
    }

    public function getService() {
        return $this->service;
    }
 }

On me reprochait d’avoir fait ça dans mon contrôleur:

$this->em = $this->getServiceLocator()->get('doctrine.entitymanager.orm_default');

Désormais c’est ma factory qui se charge de faire ça, on a donc déporté les responsabilités dans la fabrique, qui est la seule à savoir comment se constitue l’objet qu’elle fabrique. J’ai choisi d’injecter les dépendances (le service) à la création de l’objet et pas via un setter, c’est un choix strictement personnel. Rien ne vous empêche de procéder autrement.

PHP 7 : les types de retour

Un futur vers les retours

On continue dans la série « Ces nouveautés qui rendent heureux » avec l’annonce de la possibilité de forcer des types de retour de fonctions dans PHP 7 ! Cette avancée majeure a fait là encore l’objet de débats assez vifs au sein de la communauté des développeurs PHP, avec des RFC critiquées, d’autres écartées et d’autres qui reviendront un jour ou l’autre d’outre-tombe !

Terminator, un type de retour

Le type de retour le plus connu au monde !

C’est l’histoire d’un type…

Scalaire ou pas, nous pouvons maintenant signifier un type de retour dans les déclarations de nos fonctions, sous la forme suivante :

<MODE D'ACCES> function <NOM> (liste de paramètres) : <TYPE DE RETOUR>

La position de ce type de retour a fait débat d’entrée de jeu, certains développeurs ne souhaitant pas le voir devant le nom de la fonction, comme c’est le cas en C ou Java, pour des raisons de commodité lors des recherches dans le code (chercher « function mafonc » n’aurait plus fonctionné si elle avait du s’appeler « function int mafonc »).

Commençons avec des types non scalaires :

function eleves(): array {
    return ["jean", "eric", null];
}

var_dump(eleves());

/*
array(3) {
  [0]=>
  string(4) "jean"
  [1]=>
  string(4) "eric"
  [2]=>
  NULL
}
*/

ou bien encore :

class Eleve {
    private $_nom;
    private $_prenom;

    public function __construct ($nom, $prenom) {
        $this->_nom = $nom;
        $this->_prenom = $prenom;
    }
}

function eleve($n, $p): Eleve {
    return new Eleve ($n, $p);
}

var_dump(eleve("Rack", "Eric"));

/*
object(Eleve)#1 (2) {
  ["_nom":"Eleve":private]=>
  string(4) "Rack"
  ["_prenom":"Eleve":private]=>
  string(4) "Eric"
}
*/

Nous avons utilisé dans l’exemple précédent des types non scalaires, respectivement tableau (array) et objet (de la classe Eleve).

Nous pouvons aussi renvoyer des types scalaires, tels que int, float, bool ou encore string :

function qi(): int {
    return 150;
}

var_dump(qi());

Si je modifie le type de retour pour essayer les quatre types susnommés, voici ce que j’obtiens :

int(150)
bool(true)
string(3) "150" 
float(150)

Pensez à activer le typage strict en faisant figurer en première ligne de votre fichier de test (ou plus proprement, dans l’autoloader de votre application) la directive maintenant bien connue de ceux qui veulent bénéficier des plus récentes fonctionnalités en matière de typage strict, à savoir:

declare(strict_types=1);

Comme toujours, il faut respecter le contrat, sinon gare ! Vous ne pouvez pas écrire :

function quotientintellectuel(): int {
    return null;
}

ou bien encore:

function quotientintellectuel(): int {
    return "150";
}

Sous peine de courroucer PHP, qui, pour le premier exemple, vous jettera à la figure un joli :

Fatal error: Uncaught TypeError: Return value of quotientintellectuel()
 must be of the type integer, null returned 

Invariance

Elle reste toujours de mise, à savoir que les signatures des méthodes héritées ou implémentées doivent rester EXACTEMENT les mêmes dans les sous-types. Ainsi, si vous écrivez ceci, vous vous provoquerez naturellement l’ire du compilateur :

class Eleve {}
 
interface EleveDao {
    function trouverParNom($nom): Eleve; 
}
 
class EleveSqlDao implements EleveDao {

    function trouverParNom($nom) {
        // mes plus belles requêtes SQL 
        return new Eleve();
    }
}
Fatal error: Declaration of EleveSqlDao::trouverParNom($nom) must be compatible with EleveDao::trouverParNom($nom): Eleve

Notez que ce comportement pourrait être modifié dans les versions futures du langage.

Peut-on renvoyer « rien » ?

Il y a eu une tentative d’introduire un type void comme en C/C++ ou Java mais cette proposition a été rejetée. Vous savez que par défaut une fonction PHP renvoie null :

function rien() {}

var_dump(rien());
# NULL

Quand on écrit void, on notifie que la fonction ne renvoie rien. Or null par défaut est retourné par PHP, ce sont deux choses différentes.

Avec void on ne renvoie rien, avec return null on renvoie « absence de valeur » !

Quid des fonctions spéciales ?

Certaines fonctions ne sont pas autorisées à faire usage des types de retour :

  • les constructeurs
  • les destructeurs
  • les fonctions de clonage

En tentant de le faire, vous provoqueriez des erreurs fatales.

En résumé

Coluche disait « Voilà une nouvelle qu’elle est bonne ! »; une délicieuse formule qui s’applique à cette nouvelle fonctionnalité qui, avec le typage strict et de nombreuses autres features, constitue une avancée majeure dans le cycle de vie de notre langage préféré. L’introduction de ces possibilités est l’occasion une fois de plus de renforcer la bonne hygiène de vos développements ! Il reste encore des points à régler (void notamment, qu’on espère voir arriver un jour !) mais tout va dans la bonne direction !

PHP 7 : le typage strict

Une des grandes forces de PHP – qui est aussi, hélas !, une de ses plus grandes faiblesses – est l’absence de typage fort comme c’est le cas en Java, en C#, C++ ou dans bien d’autres langages dits « orientés objet ». PHP s’occupe de faire les conversions de type qui l’arrangent, notamment lorsque l’on effectue des comparaisons entre des types différents (voilà pourquoi les opérateurs stricts ont été introduits qui comparent en valeur et surtout en type). Tout ça est décidé de façon dynamique au runtime (à l’exécution, en bon français) et non pas de façon statique, à la compilation.

Une nouveauté qui fait des heureuses !

Une nouveauté qui fait des heureuses !

Depuis les versions 5.X, on pouvait déjà typer des paramètres de fonction avec certains types non-scalaires : objet depuis 5.0 et array depuis 5.1 (et callable par la suite, mais passons…). Dans des temps reculés (avant 5.X), on n’avait pas trop le choix:
function peuimporte ($param, $reparam) {
var_dump($param);
var_dump($reparam);
}

L’appel de cette fonction pouvait se faire avec des arguments de divers types : entiers, flottants, tableaux, objets, chaînes de caractères…Pour faire court, on pouvait un peu passer tout et (surtout) n’importe quoi !

peuimporte (1, 1.23);
peuimporte ("a", 1);
peuimporte (array(), new StdClass());

Un type scalaire est un type qui ne contient qu’une valeur à la fois: un entier, une chaine de caractères, un booléen etc. Un type non scalaire peut contenir plusieurs valeurs; un tableau, un objet…ce sont des types composites – ou des collections de valeurs.

Depuis PHP 5.1 disais-je, on pouvait forcer le typage des paramètres des fonctions avec des types non scalaires comme un tableau:


function peuimporte (Array $param) {
// faites ce que bon vous semble
}

ou bien un objet d’un type particulier:


class UnTypeAuPif {}
function peuimporte (UnTypeAuPif $param) {
// faites ce que bon vous semble
}

ou encore d’un super-type, comme une interface:


interface Bidon {}
class UnTypeAuPif implements Bidon {}
class UnAutreTypeAuPif implements Bidon {}
function peuimporte (Bidon $param) {
// faites ce que bon vous semble
}

Il suffit alors de passer en argument une instance qui ne se conforme pas aux spécifications que constitue la signature de notre fonction et VLAN!


Catchable fatal error: Argument 1 passed to peuimporte() must be an instance of UnTypeAuPif, instance of stdClass given

Ici j’ai tenté de réaliser l’invocation suivante :
peuimporte(new StdClass);

J’ai allégrement violé le contrat qui me lie à cette fonction et qui stipule qu’il ne faut passer que des instances de UnTypeAuPif (dans le premier exemple).

Depuis mars 2015 (et une RFC qui a fait l’objet d’un débat, puis d’un vote) il est donc prévu que PHP 7 nous donne la possibilité de spécifier des types scalaires pour nos arguments…ENFIN ! Il deviendra donc possible d’écrire:


function peuimporte (int $param) {
// faites ce que bon vous semble
}

ou bien:


function peuimporte (string $param) {
// faites ce que bon vous semble
}

ou encore:


function peuimporte (float $param) {
// faites ce que bon vous semble
}

Le fait de passer en argument d’un appel une variable du mauvais type provoque une erreur fatale, par exemple:


Fatal error: Uncaught TypeError: Argument 1 passed to peuimporte() must be of the type float, string given

Pour activer ce typage strict sous PHP 7, il faut utiliser declare que l’on connait depuis 5.3 et qui sert lors de la compilation du fichier. Ce language construct sera placé en tout début de fichier (sous peine de provoquer une erreur fatale)  comme suit:


declare(strict_types = 1);
function peuimporte (float $param) {
// TODO
}
peuimporte(1.23);

Notez que le mode bloc, originellement proposé pour strict_types , a été interdit dans PHP 7. Cette fonctionnalité n’est pas forcée par défaut, nul risque donc de BC break dans le code existant. L’aspect lâche du typage en PHP reste encore la règle (pour combien de temps ?), ce qui continuera de faciliter l’intégration des gens qui ne sont pas des développeurs de formation.

Pour les développeurs qui viennent du monde des langages fortement typés et qui se sentent frustrés par le typage faible de PHP, c’est une bonne nouvelle et sans doute une raison de plus de cesser de prendre PHP pour un langage de seconde zone.

Il me tarde que PHP 7 fasse l’objet d’une release officielle pour bénéficier de cette tant attendue fonctionnalité !

Testé avec un PHP7 bêta compilé sur Debian Jessie…et approuvé !

PHP : le design pattern Proxy

Proxy, proxy…proxy de cache, proxy Web…vous avez sans doute déjà lu ce terme quelque part, n’est-ce pas ? Si oui, alors vous avez déjà sa principale raison d’être en tête : un proxy s’intercale entre vous et…quelque chose !

En programmation, ce quelque chose est un objet « distant »…distant parce qu’il peut se trouver ailleurs sur le réseau mais pas seulement ! Il peut très bien se trouver sur la même machine mais dans un autre espace d’adressage. En pratique, le proxy implémente la même interface que l’objet auquel il sert d’écran, car il va se substituer à lui !

Ce design pattern fait état de relations entre des objets, voilà pourquoi on dit qu’il est structurel ! Proxy (ou Procuration ou encore Surrogate en anglais) a des similitudes avec un autre pattern structurel : Décorateur. Cependant, il convient de bien garder à l’esprit que si Décorateur a pour but d’ajouter des fonctionnalités à l’objet décoré, Proxy est souvent là pour effectuer un contrôle d’accès à un objet.

Si vous avez déjà travaillé avec le framework Symfony et l’ORM Doctrine, cette notion de proxy ne vous est pas inconnue car vous manipulez des proxies en bien des occasions !

Gardes du corps peu enclins à la discussion

Les proxies du président de la République ! Pour émettre une requête à ce dernier, c’est par eux qu’il faudra passer !

L’interprète, un mandataire idéal !

Voilà un nouveau terme français pour notre Proxy : mandataire. Quel meilleur exemple que celui de l’interprète, par lequel il est impératif de passer lors des sommets internationaux, si l’on veut être compris des grands de ce monde (et éventuellement lui faire porter la responsabilité d’un incident diplomatique) ! Voilà un exemple très simple pour illustrer ça : des interfaces que le sujet réel (le président russe) et son proxy (l’interprète russe-français) implémentent tous deux et un président hôte qui prend en composition un interprète avec lequel il va parler tandis que celui-ci va de son côté discuter avec le sujet réel. Dans cet exemple, j’en profite pour utiliser la dérivation d’interfaces, le mot clé final, une classe abstraite…En réalité j’aurais largement pu simplifier le code, j’aurais pu aussi injecter directement l’interprète dans le constructeur du président hôte, qui mène les conversations, au lieu de mettre un setter dont je ne suis pas spécialement adepte, bref j’aurais pu faire des tas de choses autrement mais je veux garder le code efficace et amusant autant que possible !

interface PersonneInterface
{
    public function parlerDuTemps(): string;
}

interface PresidentInterface extends PersonneInterface
{
    public function parlerDuRechauffementClimatique(): string;
    public function parlerDesGuerres(): string;
}
 
final class PresidentRusse implements PresidentInterface
{
    public function parlerDuTemps(): string
    {
        return 'Я очень рад быть здесь, погода прекрасная в Париже';
    }

    public function parlerDuRechauffementClimatique(): string
    {
        return 'это очень серьезная проблема !';
    }

    public function parlerDesGuerres(): string
    {
        return 'какая война?';
    }
}

interface InterpreteInterface
{
    public function boireUnVerreEau(): string;
}

abstract class Interprete implements InterpreteInterface
{
    protected $presidentHote;
    
    protected $presidentInvite;

    public function __construct(PresidentInterface $presidentHote,
                              PresidentInterface $presidentInvite)
    {
        $this->presidentHote = $presidentHote;
        $this->presidentInvite = $presidentInvite;
    }
    
    public function boireUnVerreEau(): string
    {
        return 'Glou Glou Glou'.PHP_EOL;
    }
}
 
final class InterpreteRusse extends Interprete implements PresidentInterface
{
    public function parlerDuTemps(): string
    {
        $temps = 'Au sujet du temps, le président russe me dit : "';
        $temps .= $this->presidentInvite->parlerDuTemps().'"'.PHP_EOL;
        
        return $temps;
    }

    public function parlerDuRechauffementClimatique(): string
    {
        $climat = 'Au sujet du climat, le président russe me dit : "';
        $climat .= $this->presidentInvite->parlerDuRechauffementClimatique().'"';
        
        return $climat.PHP_EOL;
    }

    public function parlerDesGuerres(): string
    {
        if ($this->presidentHote instanceof PresidentFrancais) {
            return 'Le président russe ne souhaite pas évoquer le sujet 
                    avec le président français ! Prenons plutôt Vodka !'.PHP_EOL;
        }
        
        $guerre = 'Au sujet des guerres, le président russe me dit : "';
        $guerre .= $this->presidentInvite->parlerDesGuerres().'"'.PHP_EOL;
        
        return $guerre;
    }
}
 
final class PresidentFrancais implements PresidentInterface
{
    private $interprete;
     
    public function attacherInterprete(InterpreteInterface $interprete): void
    {
        $this->interprete = $interprete;
    }
    
    public function parlerDuTemps(): string
    {
        return 'Président, il fait bon vivre à Paris, n'est-ce pas ?'.PHP_EOL;
    }

    public function parlerDuRechauffementClimatique(): string
    {
        return 'Que pensez-vous du réchauffement climatique ?'.PHP_EOL;
    }

    public function parlerDesGuerres(): string
    {
        return 'Que vous inspirent les conflits mondiaux ?'.PHP_EOL;
    }
    
    public function discuterSurPerron(): void
    {
        if (!$this->interprete) {
            throw new RuntimeException('Où est l\'interprète ?');
        }
         
        echo $this->parlerDuTemps();
        echo $this->interprete->parlerDuTemps();
        
        echo $this->parlerDuRechauffementClimatique();
        echo $this->interprete->parlerDuRechauffementClimatique();
        
        echo $this->parlerDesGuerres();
        echo $this->interprete->parlerDesGuerres();
    }
}
 
$presidentFrancais = new PresidentFrancais();
$presidentRusse = new PresidentRusse();
$InterpreteRusse = new InterpreteRusse($presidentFrancais, $presidentRusse);

$presidentFrancais->attacherInterprete($InterpreteRusse);
 
try {
    $presidentFrancais->discuterSurPerron();
} catch (RuntimeException $exception) {
    echo "Allô, ici le chef du protocole !", PHP_EOL;
    echo "Le président vient de me dire '" . 
              $exception->getMessage(), "'", PHP_EOL;
    echo "Vite, allez chercher un interprète !", PHP_EOL;
}

echo $InterpreteRusse->boireUnVerreEau();

Une fois de plus, voilà un design pattern fait la part belle à l’abstraction !

Vous notez ici que la tentation serait grande pour notre interprète de proposer des fonctionnalités que ne propose pas le président dont il assure la traduction. En faisant ainsi, nous nous éloignerions de Proxy – dont le rôle consiste majoritairement à faire de la délégation – pour adopter une approche Décorateur.

Notre interprète russe joue également le rôle de proxy de protection en filtrant les accès au président russe sur des questions épineuses: en l’occurrence, le président russe n’a pas tellement envie de parler des guerres avec son homologue français et l’interprète a reçu lors de son briefing des instructions sans équivoque…heureusement qu’en bon russe, il a prévu un verre de vodka pour désamorcer tout début de crise diplomatique ! Faire des vérifications de droit d’accès, voilà aussi un autre aspect de ce design pattern.

Le chargement fainéant

Personne n’emploie ce terme en réalité, mais il me fait rire ! Le lazy loading consiste à différer le chargement du vrai objet (celui qui nous mandate, qui nous donne procuration) jusqu’au moment où l’on s’en servira effectivement ! Ceci est notamment utile lorsqu’un objet est un plutôt gourmand en ressources.

Prenons l’exemple d’une classe qui sert habituellement à manipuler des images. Lorsqu’on l’instancie, elle range dans une variable d’instance prévue à cette effet la totalité du flux de l’image qu’on lui passe en paramètre.

Notre proxy, qui possède le même super-type que la classe image (une classe abstraite implémentant une interface) ne va pas effectuer ce chargement mais il va attendre le dernier moment pour instancier la classe qui le mandate et appeler dessus les méthodes demandées par le client :

interface ImageInterface
{
    public function donnerTaille(): int;
    public function aContenu(): bool;
    public function afficherContenu(): ?string;
}

abstract class AbstractImage implements ImageInterface
{
	protected $cheminFichier;
	protected $contenuFichier;
	protected $tailleFichier;
	
	public function __construct($cheminFichier)
	{
		$this->cheminFichier = $cheminFichier;
	}
	
	public function donnerTaille(): int
	{
		return $this->tailleFichier;
	}
	
	public function aContenu(): bool
	{
		return null !== $this->contenuFichier;
	}
}

class StandardImage extends AbstractImage
{
	public function __construct($cheminFichier)
	{
		parent::__construct($cheminFichier);
		$this->contenuFichier = file_get_contents(
                                          $this->cheminFichier);
		$this->tailleFichier = filesize(
                                          $this->cheminFichier);
	}
	
	public function afficherContenu(): ?string
	{
		return $this->contenuFichier;
	}
}

class ProxyImage extends AbstractImage {
	
	private $vraieImage;
	
	public function __construct($cheminFichier)
	{
		parent::__construct($cheminFichier);
		$this->tailleFichier = filesize($this->cheminFichier);
	}
	
	public function afficherContenu(): ?string
	{
		if (!$this->vraieImage) {
			$this->vraieImage = new StandardImage(
                                              $this->cheminFichier);
		}
		
		return $this->vraieImage->afficherContenu();
	}
}

final class GestionnaireImage
{
	public function traiterImage (ImageInterface $image): void
	{
		echo $image->donnerTaille() . ' octets';
		echo 'Contenu présent ?'.($image->aContenu());
		echo $image->afficherContenu();
	}
}

$gestionnaireImage = new GestionnaireImage();

$image = new StandardImage('elephant.jpg');
echo $gestionnaireImage->traiterImage($image);

$proxy = new ProxyImage('elephant.jpg');
echo $gestionnaireImage->traiterImage($proxy);

Le client (GestionnaireImage) travaille d’ordinaire avec des objets de la classe StandardImage, qui, dès qu’elle est instanciée, stocke le flux complet du fichier ciblé dans une variable d’instance. Ceci peut s’avérer extrêmement coûteux si l’image est de grande taille ou si un grand nombre d’images sont requises en même temps par différents utilisateurs de notre classe, voire les deux !

Nous intercalons donc un objet Proxy entre le client de notre code et le sujet ciblé : ProxyImage possède le même super-type que StandardImage, il est donc tout à fait capable d’agir en qualité de mandataire ! Son rôle sera de différer la construction du sujet ciblé jusqu’au moment où son utilisation sera requise; il doit pour cela posséder une référence à ce sujet, voilà pourquoi vous voyez la donnée membre privée $vraieImage dans ProxyImage ! C’est le mandataire qui instancie le sujet, au départ il possède sur le fichier image une référence indirecte (le nom de fichier) puis finit par obtenir une référence directe (l’objet StandardImage, avec le flux complet).

Dans le cas de StandardImage, le contenu du fichier cible est intégralement stocké dans la propriété privée dédiée à cet effet, mais pas dans le cas de ProxyImage qui n’instanciera la classe mandatée que lorsque la méthode afficherContenu sera invoquée. La valeur retournée par $image->aContenu() vaudra TRUE dans le cas de StandardImage et FALSE dans le cas du Proxy; c’est bien le signe que le Proxy fait l’économie de la lecture du flux du fichier image sur lequel nous travaillons. Cependant, lorsque le client demande l’affichage de l’image, le Proxy ne peut faire autrement que d’instancier StandardImage pour invoquer dessus la méthode qui va retourner le flux utile à cet affichage.

Au final, le mandataire est plus efficace que le sujet réel qu’il masque puisque lors de l’appel à donnerTaille dans traiterImage, il n’a pas récupéré l’intégralité du flux binaire du fichier cible. C’est évidemment ce qu’il fait lorsqu’il doit afficher celui-ci, ne pouvant faire autrement.

Pour utiliser cet exemple, il vous faudra bien entendu une image nommée elephant.jpg que je vous fournis un peu plus bas !

Quand on doit différer les opérations qui s’avèrent coûteuses lors de la création d’un objet au moment où elle seront effectivement requises alors le design pattern Proxy peut s’avérer d’une aide précieuse !

La belle image qui a servi à mes tests !

La belle image qui a servi à mes tests !

Symfony 2 : en finir avec le nettoyage du cache via cache:clear !

Symfony, qu’est-ce que tu nous cache ?

Que vous soyez un utilisateur avancé ou débutant du célèbre framework MVC basé sur PHP, vous avez forcément tôt ou tard du affronter cet écueil : mettre à jour le cache fichier de Symfony !

Le scénario est le suivant : vous installez la dernière version de votre framework préféré, vous vous rendez sur la page Web de configuration http://monsite/app_dev.php comme on vous l’a dit et là, c’est le drame :

RuntimeException: Unable to create the cache directory (/var/www/Symfony/app/cache/dev)

Rien de choquant, j’ai décompressé l’archive en tant qu’utilisateur seb, c’est donc lui qui possède tous les droits sur l’arborescence, que j’ai simplifiée dans l’exemple pour ne garder que les répertoires :

ls -l Symfony
total 120
drwxr-xr-x 6 seb seb 4096 mai 15 10:23 app
drwxr-xr-x 2 seb seb 4096 mai 15 10:23 bin
drwxr-xr-x 3 seb seb 4096 mai 15 10:23 src
drwxr-xr-x 13 seb seb 4096 mai 15 10:23 vendor
drwxr-xr-x 3 seb seb 4096 avril 27 20:36 web

L’utilisateur par défaut de mon serveur HTTP Apache est www-data : lorsque le malheureux tente de créer le répertoire de cache, il échoue lamentablement, faute de droits suffisants ! Comment faire ?

Bougredane et Bougredandouille ne font qu’un, mes users aussi !

Et si nous mettions www-data et notre user seb dans le même panier ? Après tout, ils vont se partager l’accès aux différents répertoires, autant officialiser leur collaboration ! Nous n’aurions plus qu’à donner les droits au groupe et nos deux utilisateurs pourraient enfin se partager cette arborescence dans la joie et la bonne humeur (et accessoirement, l’efficacité !)

Je vais ajouter seb à mon groupe www-data, dont fait partie l’utilisateur www-data. Je répète : il existe un groupe www-data, dont l’utilisateur principal est lui-même nommé www-data, attention à ne pas confondre ! Rien ne vous interdit d’y rajouter d’autres utilisateurs ! C’est même ce que nous allons faire de suite, notre utilisateur seb va rejoindre le groupe www-data :

sudo usermod -aG www-data votre_utilisateur

N’oubliez pas l’option -a (append) ! Si vous l’omettez, vous écrasez l’ensemble des groupes auxquels vous appartenez avec le nouveau groupe ! Pour vous assurer que le changement a bien été pris en compte, tapez la commande groups :

seb@debian:/var/www$ groups
seb www-data lpadmin

Mission (presque) accomplie : il vous faut en effet vous déconnecter/reconnecter pour que ces changements soient pris en compte par le système d’exploitation.

Extrait d'un strip de Dicentim

Ah, les lectures de jeunesse…

Changer les droits d’accès à l’arborescence de Symfony

Nous avons deux utilisateurs qui font partie du même groupe mais à ce stade, une arborescence qui est toujours la propriété de l’utilisateur qui l’a créée. Il faut donc la changer ! Disons dans un premier temps qu’elle appartient dorénavant au groupe www-data :

chgrp -R www-data Symfony/

Puis, que tout utilisateur membre de ce groupe possède les droits en écriture dessus :

chmod -R g+w Symfony/

Regardons l’impact du changement :

drwxrwxr-x  6 seb www-data  4096 mai   15 10:23 app
drwxrwxr-x  2 seb www-data  4096 mai   15 10:23 bin
drwxrwxr-x  3 seb www-data  4096 mai   15 10:23 src
drwxrwxr-x 13 seb www-data  4096 mai   15 10:23 vendor
drwxrwxr-x  3 seb www-data  4096 avril 27 20:36 web

A ce stade là, la joie nous anime et nous retournons immédiatement sur l’URL de notre page de config’ pour contempler avec satisfaction les conséquences du changement : TOUT MARCHE, c’est magnifique ! Alors nous paramétrons la connexion à notre base de données ! Tout se déroule parfaitement, nous prenons un moment pour nous extasier sur la démo Hello World puis très vite, nous développons nos premiers contrôleurs d’action, nos premières entités, nos premiers templates Twig…Et un beau jour, nous décidons de nettoyer le cache en ligne de commande avec la célèbre commande app/console cache:clear – -env=dev (le env étant optionnel car valant dev par défaut). La sanction est immédiate :

[UnexpectedValueException]
The stream or file « /var/www/Symfony/app/logs/dev.log » could not be opened: failed to open stream: Permission denied

Image d'une personne pleurant

Un développeur qui vient de faire son premier cache:clear en CLI

Mais…attendez une seconde…je ne comprends pas, j’ai deux utilisateurs du même groupe maintenant, pourquoi tant de haine à mon égard ? Si vous regardez le fichier incriminé (qui se trouve dans le répertoire de logs et pas de cache), voici ce que vous y verrez :

-rw-r--r-- 1 www-data www-data 94637 mai   15 11:01 dev.log
-rw-r--r-- 1 www-data www-data  2626 mai   15 11:00 prod.log

SCANDALE ! Ce sagouin de www-data n’est pas partageur, il n’a pas donné les droits d’écriture au groupe ! Mon utilisateur seb qui utilise la ligne de commande ne peut donc pas écrire dans ce fichier ! Que faire ? M’accaparer de force les droits d’écriture sur le répertoire logs ? Allons-y !

Mon utilisateur seb n’a pas crée ces fichiers, il doit donc passer par sudo pour faire cette modification :

sudo chmod -R g+w app/logs/

A la bonne heure ! Si www-data ne prête pas, je lui arrache des mains !
Sûr de moi je relance un app/console cache:clear et je manque de m’étrangler !

[RuntimeException]
Unable to write in the « /var/www/Symfony/app/cache/dev » directory

Bulle de bédé avec des pictogrammes

Le réflexe primaire du développeur contrarié : la bordée d’injures !

Sans surprise, je constate les mêmes dégâts dans le répertoire cache :

drwxr-xr-x 7 www-data www-data 4096 mai   15 11:00 dev
drwxr-xr-x 5 www-data www-data 4096 mai   15 11:00 prod

Pourtant j’avais bien dit à chmod de donner les droits en écriture sur ce maudit répertoire ! On dirait qu’il ne les a pas propagés aux sous-répertoires…Bizarre.
Qu’à cela ne tienne, je continue dans ma logique Rambo, je les aurais tous à la force de la Ranger cirée !

sudo chmod -R g+w app/cache/

ENFIN JE TRIOMPHE ! *rire démoniaque*
Je viens de lancer le nettoyage du cache en dev et il s’est effectué comme il faut, la preuve !

seb@debian:/var/www/Symfony$ app/console cache:clear –env=dev
Clearing the cache for the dev environment with debug true

Passablement ragaillardi par cette victoire sur la machine, je retourne donc en découdre avec la programmation, puis je fais un F5 sur mon client HTTP favori. H-O-R-R-E-U-R !!!

RuntimeException: Failed to write cache file « /var/www/Symfony/app/cache/dev/classes.php ».

Ce coup-ci c’en est trop, des tas d’idées vous passent par la tête et notamment la reconversion professionnelle…C’est sûr et certain, la machine vous en veut !

Regardons le répertoire de cache :

seb@debian:/var/www/Symfony$ ls -l app/cache/
total 8
drwxrwxr-x 6 seb      seb      4096 mai   15 11:32 dev
drwxrwxr-x 5 www-data www-data 4096 mai   15 11:00 prod

Bon sang, l’utilisateur seb a osé imposer SON groupe en lieu et place de celui que j’avais défini !!! De deux choses l’une, soit je trouve une solution à ce problème et vite, soit je joue éternellement au ping-pong avec les droits de mes deux utilisateurs et je ne garantis aucunement de continuer de jouir de l’ensemble de mes facultés mentales à la fin de la journée. Pire : dégoûté de ce cirque, je pourrais me tourner vers Zend ! (je plaisante, ne tapez pas !)

Ouère iz zeu problème ?

Mes utilisateurs accèdent en concurrence aux mêmes répertoires : dès que je modifie les permissions de l’un, c’est l’autre qui se voit refuser l’écriture. Ça peut durer longtemps ! J’étais sur la bonne piste en décidant de créer un seul groupe, mais je ne suis pas allé au bout de la solution !

Victime de chmod, tel est ton nom de code !

Un de mes problèmes vient du fait qu’un utilisateur, www-data ou seb, ne respecte pas les droits du répertoire parent dans lequel il crée le sien. Pour cela, il me faut faire usage de l’option +s de chmod, voyez plutôt :

chmod g+s cache/ logs/

Cette option setgid (set group id), lorsqu’elle est appliquée sur un répertoire, permet de dire la chose suivante : « tout fichier/répertoire créé dans CE répertoire héritera des droits DE GROUPE du répertoire de base au lieu d’hériter des droits DU GROUPE de l’utilisateur qui l’a crée ». Ceci va résoudre une partie de mon problème car seb ne s’arrogera pas le droit d’écraser la valeur de groupe www-data par la sienne (seb) ! Cependant, il faut un moyen de dire que nous souhaitons que nos deux utilisateurs maintiennent les droits EN ECRITURE de leur groupe commun, qui rappelons le est www-data !

Umask to the rescue !

Nous voulons pouvoir dire :

  • Les utilisateurs seb et www-data doivent répliquer les droits de groupe du répertoire de base
  • Les utilisateurs seb et www-data doivent garantir que tout fichier ou tout répertoire créé sera writeable par quiconque appartient au groupe www-data

Nous avons déjà assuré la première partie, comment garantir que mes deux utilisateurs donneront le droit d’écriture à leur groupe en toutes circonstances ?

En positionnant le umask de l’un et de l’autre à la même valeur : 0002.
Pourquoi ce 0002 ? Le premier 0 indique une notation octale, mais ce n’est pas très intéressant…Ce qui nous intéresse c’est 002 ! 0 correspond aux droits de l’utilisateur (U), le second 0 aux droits du groupe (G) et le 2 aux autres (O, pour others), un bon moyen mnémotechnique consiste à retenir le prénom Hugo pour se souvenir de la place de chacun (U-G-O).
Bref, ce masque nous dit en gros « voici ce que j’enlève lorsque tu crée un fichier ou un répertoire » et donc il nous dit dans notre cas « J’enlève le droit d’écriture aux autres ». Souvenez-vous que les droits sont les suivants : lecture = 4, écriture = 2, exécution = 1.
En positionnant mon umask à 0002, je dis en substance « enlève les droits d’écriture à others et laisse le reste intact », donc j’aurais finalement 775 comme droits sur tout ce que je m’apprête à créer !

Pour voir l’état de votre umask, et donc de voir s’il y a besoin de le modifier, tapez simplement umask en ligne de commande.

Il y a de fortes chances pour qu’il soit par défaut à 0022, dans ce cas là il faudra le changer en faisant simplement umask 0002 puis umask pour vérifier que le bon s’affiche dorénavant. Attention toutefois, ce changement ne dure que le temps de votre session, il faudra penser à reporter cette commande dans votre fichier .bashrc, par exemple.

Nous y sommes ! Enfin, presque !

Un seul utilisateur a basculé son umask, c’est l’utilisateur CLI ! Il faut faire la même chose côté utilisateur Web, www-data, donc ! Nous rajoutons cette ligne au début des fichiers app_dev.php et console.php : umask(0002);

Ainsi, que nous créions des objets dans le filesystem avec la commande Symfony console, avec l’utilisateur Web via l’appel au contrôleur frontal de dev ou encore directement avec l’utilisateur qui manipule la ligne de commande, nous aurons à terme les mêmes droits !

Que dit la doc ?

Il est dit qu’il faut privilégier les ACL au détriment du umask car umask n’est pas totalement fiable. Soyons francs, quand on débute, on n’a pas forcément envie de s’improviser sysadmin et de s’attaquer aux ACL (de plus on peut très bien ne pas y avoir accès, en entreprise par exemple). Pour débuter ou encore développer en environnement local, nul besoin de mettre la barre trop haut, une modification du umask suffira largement.

Je vous recommande dans tous les cas la lecture à l’URL suivant : http://symfony.com/doc/current/book/installation.html#configuration-and-setup

Résumé

La TODO list, pour finir :

  • Mettez votre utilisateur CLI et votre utilisateur Web dans le même groupe
  • Faites un chmod g+s sur les répertoires qui posent problème : cache et logs
  • Changez le umask de votre utilisateur CLI dans .bashrc et dans le fichier app/console, faites de même pour l’utilisateur Web dans app_dev.php et app.php

Bon développement à tous et à toutes !

Beau ciel bleu

Le cache n’est plus un soucis, l’horizon est dégagé, vous êtes calme et détendu(e)…

PHP : le design pattern Observateur

Observateur, un design pattern comportemental

Après avoir vu Adaptateur, Décorateur, Template Method ou Factory, nous allons nous concentrer sur un design pattern comportemental : Observateur. Comme tous les design patterns comportementaux (au sens GoF du terme), Observateur décrit la manière dont des objets interagissent entre eux.

Quels sont ces objets ?

Ce design pattern met en jeu deux types d’objets :

  • un sujet
  • des observateurs

Un sujet notifie un ou plusieurs observateurs qu’un changement d’état vient de se produire chez lui.

Personne observant le ciel aux jumelles

Un observateur attendant avec impatience une notification en provenance d’un sujet.

Dans quel état j’erre ?

Le sujet possède un état interne; lorsque cet état est altéré, il va notifier ses observateurs de ce changement. Prenons un exemple trivial, qui va parler au plus grand nombre : lorsqu’une personne présente sur un réseau social (notre sujet) fête son anniversaire, son état change car sa propriété âge est incrémenté d’une unité et quiconque suit ce sujet (un observateur, donc) reçoit une notification l’avertissant de ce changement d’état. L’observateur se met à jour et affiche la bonne nouvelle à l’écran: « X fête son anniversaire, n’oubliez pas de lui souhaiter ! »…

Structure du design pattern Observateur

Voici la version telle que définie par le Gang of Four :

Structure du design pattern Observateur

Toute la puissance de ce design pattern réside dans le fait que sa structure, à base d’abstractions, induit un couplage faible entre les deux types d’objets qui le composent : un sujet ne sait rien du type concret de ses observateurs, si ce n’est qu’il se conforme en tous points à l’interface Observer, pas plus qu’il n’est au courant de leur nombre. Le sujet n’aura donc aucunement à se risquer à d’hasardeuses hypothèses sur la nature de ceux qui l’observent et n’importe quel objet sera en mesure de l’observer, du moment que la classe concrète dont il est issu implémente l’interface Observer.

Lorsque j’évoque ici la notion d’interface, c’est au sens large du terme et pas stricto sensu. A l’origine – voilà maintenant 20 ans – GoF préconisait l’utilisation de classes abstraites, à dériver en classes concrètes. Mais vous ne le savez que trop bien, l’héritage n’est pas une panacée universelle, loin s’en faut.

Quelques points clés de ce modèle :

  • Sujet connait l’ensemble de ses observateurs. Leur nombre n’est pas limité.
  • Les observateurs s’enregistrent lorsqu’ils veulent observer un sujet et s’en détachent lorsqu’ils ne le veulent plus.
  • Lorsque son état est altéré, Sujet notifie l’ensemble de ses observateurs, sans notion d’ordre et sans distinction !
  • Lorsqu’il est notifié, un observateur peut obtenir des informations en provenance du sujet. Il peut décider de gérer une notification ou bien de l’ignorer.
  • Rien n’interdit à un observateur de suivre plusieurs sujets. Il faut évidemment que cet observateur puisse déterminer l’origine des notifications qu’il reçoit.

Un petit bémol, toutefois : en l’état actuel des choses, la fonction de mise à jour de l’observateur (Update) ne permet pas de savoir ce qui a changé dans le sujet, laissant à la charge de l’observateur la responsabilité de « deviner » ce qui a entrainé une modification.

La mise à jour, côté observateur

Nous allons voir comment à l’autre bout du fil, les observateurs gèrent leur mise à jour d’état avec la fonction Update.

Tu tires ou tu pousses ?

Deux modes de fonctionnement s’offrent à nous :

  • par poussée (push)
  • par traction (pull)

Le mode push – « Tiens, prends ça ! »

Le sujet pousse des informations aux observateurs. L’inconvénient de cette façon de faire est que le lien entre le sujet et ses observateurs se resserre. Le sujet peut se retrouver à pousser des informations dont certains observateurs n’auront pas besoin. Que pousser et à qui ?

Le mode pull – « Viens te servir ! »

Le sujet notifie simplement ses observateurs qu’un changement vient d’avoir lieu, à eux de savoir ce qui a changé !

Des versions basiques d’Observateur

Les contrats à respecter

Vous le savez maintenant, un sujet notifie N observateurs. On lui attache/détache ces observateurs à l’aide des méthodes appropriées et il les informe d’un changement via l’appel à la méthode mettreAJour de chacun d’entre eux. Voici à quoi vont ressembler nos contrats d’interface (dans la version GoF, ces abstractions sont des classes abstraites, ici nous utilisons des interfaces).

interface SujetInterface
{
    public function attacher(ObservateurInterface $observateur): void;
    public function detacher(ObservateurInterface $observateur): void;
    public function notifier(): void;
}

interface ObservateurInterface
{
    public function mettreAJour(SujetInterface $sujet): void;
}

La notification en version pull

Le sujet

Notre sujet stocke l’ensemble de ses observateurs dans une variable d’instance privée prévue à cet effet. Ce tableau est rempli ou vidé au fur et à mesure que les observateurs sont attachés ou détachés. Un setter mettreAJourLesNouvelles est prévu pour modifier la variable d’instance $nouvelles et un getter donnerLesNouvelles servira aux observateurs pour aller récupérer le dernier état du sujet. Souvenez-vous qu’en mode pull, ce sont les observateurs qui doivent se renseigner sur l’état du sujet et éventuellement mettre à jour le leur avec ces informations tirées depuis le sujet.

class Sujet implements SujetInterface
{
    private $nouvelles;

    private $observateurs;
    
    public function attacher(ObservateurInterface $observateur): void
    {
        $this->observateurs[] = $observateur;
    }
 
    public function detacher(ObservateurInterface $observateur): void
    {
        $key = array_search($observateur, $this->observateurs);
 
        if (false !== $key) {
            unset($this->observateurs[$key]);
        }
    }
 
    public function notifier(): void 
    {
        foreach ($this->observateurs as $observateur) {
            $observateur->mettreAJour($this);
        }
    }

    public function mettreAJourLesNouvelles(string $nouvelles): void 
    {
        $this->nouvelles = $nouvelles;
        $this->notifier();
    }
    
    public function donnerLesNouvelles(): string 
    {
        return $this->nouvelles;
    }
}

L’observateur

C’est lui qui fait le travail, une fois qu’il est notifié à travers sa méthode mettreAJour. Il reçoit le sujet à l’origine de la notification de mise à jour car il peut observer plusieurs sujets; il nous faut donc impérativement savoir qui a changé parmi les objets qu’on observe. Ici nous avons réduit la structure de la classe à sa plus simple expression: l’observateur reçoit une notification de nouvelle de la part d’un sujet, il va TIRER l’information depuis ce sujet et mettre son état interne en accord avec celui du sujet, afin d’avoir les toutes dernières nouvelles.

class Observateur implements ObservateurInterface
{
    private $dernieresNouvelles;
    
    public function mettreAJour(SujetInterface $sujet): void
    {
        $this->dernieresNouvelles = $sujet->donnerLesNouvelles();
    }
}

Le code client

Voici le code pour utiliser cette structure en mode pull, il est très simple: nous créons deux observateurs que nous attachons à notre unique sujet et nous mettons à jour l’état de notre sujet, qui va déclencher deux notifications (une pour chaque observateur).

$sujet = new Sujet();
$observateur = new Observateur();
$autreObservateur = new Observateur();
$sujet->attacher($observateur);
$sujet->attacher($autreObservateur);
$sujet->mettreAJourLesNouvelles("La bourse dévisse !");

La notification en version push

Changement d’interface

En mode push, c’est le sujet qui pousse les données modifiées à ses observateurs. Il sait donc ce qu’il leur faut, ce qui induit un couplage plus serré que dans le mode pull. Il peut aussi leur passer son état complet mais attention si les informations sont volumineuses…Les observateurs ont-ils besoin de recevoir un sac postal complet alors qu’ils attendent une carte postale ?

Quoiqu’il en soit, l’interface de l’observateur change et donc l’appel de la méthode aussi, dans le sujet:

interface ObservateurInterface
{
    public function mettreAJour(SujetInterface $sujet, array $donnees): void;
}

Le sujet

Le sujet fera désormais:

    public function notifier(): void 
    {
        foreach ($this->observateurs as $observateur) {
            $observateur->mettreAJour($this, ["nouvelles" => $this->nouvelles]);
        }
    }

L’observateur

L’observateur ira quant à lui se servir à ladite clé dans le tableau $donnees, qui dans la pratique sera évidemment bien plus fourni:

    public function mettreAJour(SujetInterface $sujet, array $donnees): void
    {
        $this->dernieresNouvelles = $donnees["nouvelles"];
    }

Le code client ne bougera pas d’un pouce, c’est la cuisine interne de l’objet qui change très légèrement. Le principe est rigoureusement identique !

Notre version d’Observateur

Qu’est-ce qui interdit à nos sujets d’être des observateurs d’autre sujets ? Rien !
Nous allons simuler un réseau social.

Nos interfaces

Elles ne changent pas beaucoup des précédentes:

interface SujetInterface
{
    public function attacher(ObservateurInterface $observateur): void;
    public function detacher(ObservateurInterface $observateur): void;
    public function notifier(): void;
}

interface SuiveurInterface
{
    public function suivre(SujetInterface $sujet): void;
    public function nePlusSuivre(SujetInterface $sujet): void;
}

interface ObservateurInterface
{
    public function mettreAJour(SujetInterface $sujet): void;
}

Nous avons juste rajouté une interface SuiveurInterface qui liste ce que doit savoir faire un suiveur: suivre et cesser de suivre.

Le membre

Dans notre réseau social, nous avons deux types de membres: des personnes et des entreprises ou des associations qui ont des pages. Ces deux catégories de membres peuvent suivre des membres, comme sur Facebook. Pouvant être à la fois sujet et observateur, ils doivent implémenter l’ensemble des interfaces. Nous factorisons les comportements dans une classe abstraite qui va contenir pour un membre, les membres qu’il suit et les membres qui le suivent. Les membres suivis iront dans $sujets et les suiveurs, dans $observateurs.

Faisons simple: de base, un membre a juste un nom et nous mettons un getter dessus, même pas de setter.
Vous constatez que la méthode mettreAJour ne contient que le sujet, la notification se fera donc en mode pull.

abstract class AbstractMembre implements SujetInterface, SuiveurInterface, ObservateurInterface
{
    protected $nom;
    
    protected $observateurs;
    
    protected $sujets;
    
    public function __construct(string $nom)
    {
        $this->nom = $nom;
    }
    
    public function attacher(ObservateurInterface $observateur): void
    {
        echo $this->donnerNom().PHP_EOL;
        echo "\t".$observateur->donnerNom()." vous à ajouté à ses contacts".PHP_EOL;
        $this->observateurs[] = $observateur;
    }
 
    public function detacher(ObservateurInterface $observateur): void
    {
        $key = array_search($observateur, $this->observateurs);
 
        if (false !== $key) {
            echo $this->donnerNom().PHP_EOL;
            unset($this->observateurs[$key]);
            echo "\tVous avez enlevé ".$observateur->donnerNom()." de vos contacts".PHP_EOL;
        }
    }
 
    public function notifier(): void 
    {
        foreach ($this->observateurs as $observateur) {
            $observateur->mettreAJour($this);
        }
    }

    public function donnerNom(): string
    {
        return $this->nom;
    }
    
    public function suivre(SujetInterface $sujet): void
    {
        // pour éviter qu'un membre puisse s'auto-suivre
        if ($sujet === $this) {
            return;
        }
        
        $this->sujets[$sujet->donnerNom()] = clone $sujet;
        $sujet->attacher($this);
    }
    
    public function nePlusSuivre(SujetInterface $sujet): void
    {
        echo $this->donnerNom().PHP_EOL;
    
        $key = array_search($sujet, $this->sujets);
 
        if (false !== $key) {
            unset($this->sujets[$key]);
        }
        
        echo "\tVous ne suivez plus ".$sujet->donnerNom().PHP_EOL;
    }
    
    public function mettreAJour(SujetInterface $sujet): void
    {
        echo $this->donnerNom().PHP_EOL;
        $nom = $sujet->donnerNom();
        
        if (array_key_exists($nom, $this->sujets)) {
            $sujetStocke = $this->sujets[$nom];
            
            if ($sujet instanceof PageCommerciale) {
                $urlMagasin = $sujet->donnerUrlMagasin();
                $urlMagasinStocke = $sujetStocke->donnerUrlMagasin();
                
                if ($urlMagasin !== $urlMagasinStocke) {
                    echo "\tLe site web de $nom vaut désormais $urlMagasin";
                }

            } elseif ($sujet instanceof Membre) {
                $age = $sujet->donnerAge();
                $ageStocke = $sujetStocke->donnerAge();

                if ($ageStocke !== $age) {
                    echo "\t$nom fête son anniversaire, il a $age ans";
                    $this->sujets[$nom] = clone $sujet;
                }
                
                $hobbies = $sujet->donnerHobbies();
                $hobbiesStocke = $sujetStocke->donnerHobbies();

                if ($hobbiesStocke !== $hobbies) {
                    echo "\t$nom a de nouveaux hobbies: ".implode(", ", $hobbies);
                    $this->sujets[$nom] = clone $sujet;
                }
            }
            echo PHP_EOL;
        }
    }
}

Les classes concrètes

On en compte deux, une pour les membres et une pour les pages commerciales. Un membre a un âge et des hobbies, une page commerciale ne comporte que l’URL du site marchand. Là aussi j’ai vraiment gardé l’essentiel dans chaque classe, qui est sur-simplifiée. Voici notre classe Membre:

final class Membre extends AbstractMembre {
 
    private $age;
     
    private $hobbies;
     
    public function changerAge(int $age): void {
        $this->age = $age;
        $this->notifier();
    }
     
    public function changerHobbies(array $hobbies): void {
        $this->hobbies = $hobbies;
        $this->notifier();
    }
 
    public function donnerAge(): ?int {
        return $this->age;
    }
    
    public function donnerHobbies(): ?array {
        return $this->hobbies;
    }
}

et voici notre classe PageCommerciale:

final class PageCommerciale extends AbstractMembre {
 
    private $urlMagasin;
     
    public function changerUrlMagasin(string $urlMagasin): void {
        $this->urlMagasin = $urlMagasin;
        $this->notifier();
    }
 
    public function donnerUrlMagasin(): ?string {
        return $this->urlMagasin;
    }
}

Chaque classe comporte des setters qui vont modifier l’état des objets et donc générer des notifications aux objets qui les observent.

Retour sur la méthode mettreAJour

    public function mettreAJour(SujetInterface $sujet): void
    {
        echo $this->donnerNom().PHP_EOL;
        $nom = $sujet->donnerNom();
        
        if (array_key_exists($nom, $this->sujets)) {
            $sujetStocke = $this->sujets[$nom];
            
            if ($sujet instanceof PageCommerciale) {
                $urlMagasin = $sujet->donnerUrlMagasin();
                $urlMagasinStocke = $sujetStocke->donnerUrlMagasin();
                
                if ($urlMagasin !== $urlMagasinStocke) {
                    echo "\tLe site web de $nom vaut désormais $urlMagasin";
                }

            } elseif ($sujet instanceof Membre) {
                $age = $sujet->donnerAge();
                $ageStocke = $sujetStocke->donnerAge();

                if ($ageStocke !== $age) {
                    echo "\t$nom fête son anniversaire, il a $age ans";
                    $this->sujets[$nom] = clone $sujet;
                }
                
                $hobbies = $sujet->donnerHobbies();
                $hobbiesStocke = $sujetStocke->donnerHobbies();

                if ($hobbiesStocke !== $hobbies) {
                    echo "\t$nom a de nouveaux hobbies: ".implode(", ", $hobbies);
                    $this->sujets[$nom] = clone $sujet;
                }
            }
            echo PHP_EOL;
        }
    }

Cette méthode est au niveau de la classe abstraite; elle fait une vérification sur le type concret des sujets car les informations à tirer depuis ce sujet ne seront pas les mêmes pour les observateurs concrets. Vous voyez arriver le problème si nous rajoutons des classes concrètes en bas de l’arbre d’héritage, votre if va enfler et il vous faudra peut-être avoir recours à un nouveau design pattern pour gérer ces différentes façons d’opérer selon les types concrets, lequel ?

Notez que nous clonons les objets; il nous faut fossiliser leur état à l’instant t pour pouvoir le comparer avec celui du sujet reçu en paramètre de la méthode.

Le code client

$magasin = new PageCommerciale('ACME Web Store');
$star = new Membre('Rasmus Lerdorf');
$copain = new Membre('Jean-Michel Apeuprey');
$autreStar = new Membre('Novak Djokovic');
$membre = new Membre('Sébastien Ferrandez');

$membre->suivre($magasin);
$membre->suivre($star);
$membre->suivre($autreStar);
$membre->suivre($copain);
$copain->suivre($membre);
$magasin->suivre($autreStar);
$copain->changerAge(41);
$membre->changerHobbies(["guitare"]);
$star->changerAge(36);
$star->changerHobbies(["linux", "c++", "c"]);
$autreStar->changerHobbies(["musculation", "karaoké"]);
$magasin->changerUrlMagasin('http://www.acmestore.com');
// Je n'aime plus le tennis !
$membre->nePlusSuivre($autreStar);
// Je me suis fâché avec Jean-Michel, il n'est plus mon ami !
$membre->detacher($copain);

TADAAAM !

Voici la sortie générée par l’exécution du code client, on n’est pas trop mal on dirait ! N’hésitez pas à proposer des alternatives ou poser des questions dans les commentaires !

Sortie écran pour les observateurs

Où trouve t-on des implémentations d’Observateur ?

Typiquement, dans la programmation par événements où des classes souscrivent auprès d’une classe gestionnaire d’évènements qui les informera lorsque l’évènement auquel elles ont souscrit se produit (en appelant parfois des fonctions de callback).

Lorsque vous cliquez sur un article pour l’ajouter et que votre panier se met à jour, vous avez un bel exemple d’Observateur.

Sur les réseaux sociaux, comme dans notre exemple trivial, nous recevons des notifications dès qu’un événement d’une personne que l’on suit se produit (changement de statut, nouveau post etc.)

En PHP, la SPL propose des classes pour mettre en place le design pattern Observateur avec SPLObserver.

Enfin dans Symfony, les évènements sont gérés selon ce principe…Les observateurs sont des listeners ou des event subscribers auquel on dispatche des notifications contenant des events.