Archives de catégorie : PHP7

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…

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 : un exemple simple du design pattern Template Method

Vous cherchiez un design pattern facile à aborder ? Le design pattern template method est celui qu’il vous faut ! Son principe est très simple : dans une classe, une méthode dite template est composée de sous-méthodes dont on sait que chaque sous-classe l’implémentera à sa manière. Ces sous-méthodes sont généralement en type d’accès protégé car invoquées uniquement par cette fameuse méthode template; l’extérieur n’a pas à connaître les mystères de votre implémentation (encapsulation, vous dîtes ?). Bien entendu, étant donné que chaque classe fille implémentera ces méthodes comme bon lui semble, il convient de les signifier comme abstraites dans la classe mère.

Imaginons une classe TunnelCommande qui expose une méthode template nommée finaliserCommande; cette méthode décrit un algorithme en spécifiant ce qui devra être fait par ses sous-classes et dans quel ordre. Cette classe comporte la méthode payePort dite « adaptateur » (hook) qui peut être réécrite dans les classes filles. Elle sert à conditionner une partie du flot d’exécution de l’algorithme de la méthode template. Son utilité dans notre cas est de permettre à un certain type d’utilisateur de s’affranchir du paiement des frais de port.

abstract class TunnelCommande
{
    public function finaliserCommande(): void
    {
        $this->faireTotal();
        
        if ($this->payePort()) {
            $this->ajouterFraisPort();
        }

        $this->rediriger('page_paiement');
    }

    public function payePort(): bool
    {
    	return true;
    }

    public function rediriger(string $template): void
    {
    	echo "Redirection vers ", $template, PHP_EOL;
    }

    abstract protected function faireTotal(): void;
    abstract protected function ajouterFraisPort(): void;
}

Nous avons deux classes concrètes qui dérivent TunnelCommande et implémentent les méthodes abstraites en leur donnant un comportement spécifique. Dans CommandePremium, la méthode ajouterFraisPort qui est imposée par la classe mère abstraite ne fait rien, payePort renvoyant false elle ne sera de toutes façons jamais invoquée dans ce scénario là.


class CommandeClient extends TunnelCommande
{
    protected function faireTotal(): void
    {
        echo "Je fais le total", PHP_EOL;
    }

    protected function ajouterFraisPort(): void 
    {
        echo "J'applique les frais de port du client normal", PHP_EOL;
    }
}

class CommandePremium extends TunnelCommande
{
    protected function faireTotal(): void
    {
        echo "Appliquer 5% de rabais pour les clients Premium", PHP_EOL;
    }

    protected function ajouterFraisPort(): void
    {
        return;
    }

    public function payePort(): bool
    {
    	return false;
    }
}

$premium = new CommandePremium;
$premium->finaliserCommande();

$standard = new CommandeClient;
$standard->finaliserCommande();

Dans ce design pattern, tout le travail est fait dans la classe mère, abstraite. Quand je dis « tout le travail », je parle de la structure générale de l’algorithme, de l’ordre des opérations. Evidemment, la responsabilité de l’implémentation des détails de cet algorithme « général » est déléguée aux classes dérivées, via le mécanisme d’abstraction.