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é !

Les formes normales

Relation, es-tu bien normale ?

Le but de la normalisation d’un schéma de base de données relationnelle ? Principalement l’élimination de la redondance et l’assurance d’avoir des données cohérentes (pas la même adresse orthographiée de N façons différentes, par exemple). Je dis « de la redondance inutile » car même si cela semble un pléonasme, il existe des cas où elle peut avoir une utilité et notamment quand la rapidité des accès aux données est placée au centre des préoccupations du concepteur.

normaliser

Normalisez avant qu’il ne faille tout jeter !

Les formes normales, des règles à suivre pour rester en forme !

Les formes normales sont des règles à suivre pour parvenir à éliminer cette redondance inutile. Nous n’irons pas en détail dans les aspects formels de ces formes normales, parce qu’ils ne sont pas indispensables à la compréhension de celles-ci et parce qu’ils rebutent inutilement le débutant. Sim-pli-ci-té !

1NF

Que nous dit cette première forme normale (1st Normal Form) ? Elle nous dit que tout attribut d’une relation (toute colonne d’une table, si vous préférez) ne doit posséder qu’une valeur pour chacun de ses enregistrements (ou tuples).

Plus concrètement, voici une table enseignant qui viole allégrement 1NF :

Table violant 1NF

La colonne prenom contient deux valeurs, cette colonne n’est donc pas mono-valuée comme nous l’impose 1NF mais multi-valuée, on dit que prenom ne contient pas de valeur atomique, que c’est un groupe de valeurs.

Comment se mettre en conformité avec 1NF ?

Dans notre cas ici, il ne faut pas faire l’économie d’une colonne dédiée au deuxième prénom (il pourrait même y en avoir un troisième, voire N car « le nombre de prénoms qui peuvent être attribués par les parents à un même enfant n’est pas fixé par la loi » nous dit le site Service Public). Créons donc cette nouvelle colonne et notons que la création de celle-ci nous permettra de faire des recherches de meilleure qualité sur les prénoms, par exemple en utilisant un index !

Table en conformité avec 1NF

Plutôt simple, n’est-ce pas ? Que dire de cette relation :

Table violant 1NF

Encore une fois, notre colonne adresse est multi-valuée ! Même si elle ne contient qu’une seule adresse, cette adresse elle-même contient plusieurs informations, de nature différente: un nom de rue, un numéro dans cette rue, un code postal, un nom de ville…Pourquoi ne pas créer une table adresse avec un champ numero, type_voie (rue, avenue, boulevard etc.), code_postal, ville etc. ? Cela peut servir si l’on veut normaliser nos adresses grâce à des outils tierce-partie et les rendre « propres ». Parfois il arrive que l’on laisse volontairement l’adresse dans cet état pour minimiser les jointures et donc accéder le plus rapidement possible à l’information…A vous d’étudier la pertinence de chacune de ces façons de faire : gardez simplement à l’esprit que l’absence de normalisation peut coûter cher plus tard…quand les données auront enflé !

2NF

Tout d’abord, pour être en 2NF, il faut être en 1NF : on ne peut pas être sans avoir été, lorsque l’on normalise ! Pour évoquer 2NF, il nous faut parler des dépendances fonctionnelles ! Qu’est-ce qu’une DF ? Rien de bien compliqué, regardons de plus près cette relation enseigne :

2nf

Nous avons ici une clé primaire composée du couple (identifiant_enseignant, identifiant_cours). Notre colonne ville nous dit dans quelle ville a lieu le cours.
Nous voyons que la colonne ville dépend de identifiant_cours : le cours 1 est toujours donné à Avignon et le 2, toujours à Aix.

On dit ici que identifiant_cours détermine ville ou bien, dans le sens inverse, que ville dépend fonctionnellement de identifiant_cours.

Si pour des valeurs identiques d’une colonne C on a les mêmes valeurs dans la colonne C2, alors C détermine C2 et nous le noterons C → C2.

Ici, pour la valeur 1 dans la colonne identifiant_cours nous avons toujours la valeur Avignon en face et pour la valeur 2 dans la colonne identifiant_cours nous avons toujours la valeur Aix. Ce n’est pas le cas de identifiant_enseignant, qui n’a pas les mêmes valeurs de ville en face (regardez l’enseignant 1, il enseigne dans la ville d’Aix et dans la ville d’Avignon).

Dans notre relation enseigne nous distinguons:

  • des attributs (des colonnes, c’est pareil) qui forment une clé primaire
  • un attribut non clé (ville)

Ici nous avons identifié une dépendance fonctionnelle entre une partie de la clé (la colonne identifiant_cours) et une colonne non clé (ville). Voilà pourquoi cette relation viole 2NF.

Pour qu’une relation soit conforme à 2NF, les attributs non clé de cette relation doivent dépendre fonctionnellement de toute la clé et non d’une partie seulement

Ici nous avions : identifiant_cours → ville. La colonne identifiant_cours est une partie (la moitié pour être parfaitement exact) de la clé. CQFD.

Comment se mettre en conformité avec 2NF ?

Nous allons isoler la DF identifiant_cours → ville qui nous pose problème dans sa propre table, que nous pourrions appeler cours par exemple :

2nf-bis

Vous noterez que nous avons mis fin à la redondance inutile jusqu’alors présente dans la table (la ville potentiellement répétée N fois); cela nous aide aussi à assurer comme je vous l’ai dit en introduction la cohérence des données (que se serait-il passé avant si nous avions écrit Avignon « Avinion », « Avignom » et « Avignon » ? Nous aurions eu trois villes différentes !). Dorénavant, Avignon figure une fois et une seule…et le nom est écrit comme il faut.

Que va devenir notre ancienne relation ? Eh bien oui, nous l’avons quelque peu rabotée !

2nf3

Nous avons sorti ville mais il nous faut bien garder le lien entre le cours et la ville, voilà pourquoi la source de la DF identifiant_cours → ville, c’est à dire identifiant_cours, RESTE dans la table d’origine. Grâce à identifiant_cours, qui existe maintenant dans les deux tables, nous pouvons effectuer des jointures et ainsi garantir qu’on a maintenu l’information initiale qui était « tel cours se donne dans telle ville ».

En pratique ici, identifiant_cours sera clé primaire dans la nouvelle table cours et une contrainte d’intégrité référentielle partira depuis identifiant_cours situé dans enseigne vers identifiant_cours situé dans cours.

3NF

Tout comme il nous faut être en 1NF pour être en 2NF, il nous faut être en 2NF pour être en 3NF : nous procédons de manière incrémentale !

Pour être en 3NF, il faut que toute colonne non clé ne dépende que de la clé de la relation

Revenons un instant à nos enseignants, dont la structure a quelque peu évolué :

3nf

Nous avons souhaité pour chaque enseignant dire à quel département il était rattaché et où ce dernier se situait. Dans le cas présent, localisation dépend de departement (noté departement → localisation). Notre attribut departement ne fait pas partie de la clé, pas plus que localisation, dont il dépend. Voilà pourquoi cette relation n’est pas conforme à 3NF !

Les problèmes posés par cet exemple sont les mêmes que pour la 2NF, à savoir :

  • la localisation du département est répétée dans chaque enregistrement (regardez les deux premiers) et elle peut aussi être orthographiée différemment suite à une erreur de saisie, compromettant ainsi la cohérence des données et donc leur qualité
  • si la localisation du département change, il faut mettre à jour tous les enseignants qui y travaillent
  • Si le département n’a plus d’enseignant, nous perdons l’information de sa localisation

Comment se mettre en conformité avec 3NF ?

Comme pour 2NF, il va nous falloir isoler la DF anormale departement → localisation dans sa propre table, comme ceci :

3nf2

Là encore vous noterez que nous avons mis fin à la redondance qui jusque là existait dans notre table. Notre relation enseignant va « maigrir » elle aussi mais on doit également y faire exister un lien vers notre nouvelle relation, ce lien ce sera la colonne identifiant_departement :

3nf3

Conclusion

Nous entretenons désormais des relations tout à fait normales !

En réalité, il existe d’autres formes normales après 3NF, mais quand on a normalisé en 3NF, on a déjà éliminé la grande partie de ces vilaines redondances que nous voulons éviter.

Pour retenir facilement 2NF et 3NF, les gens qui travaillent sur les bases de données connaissent par cœur cette phrase de feu William Kent :

« The key, the whole key, and nothing but the key »

qui reprend de façon assez humoristique le serment que font les témoins devant un tribunal : « Je jure de dire la vérité, toute la vérité, rien que la vérité » (des petits malins on rajouté « So help me Codd » – E.F Codd étant considéré comme le père des bases de données relationnelles – pour remplacer le « so help me God » (Dieu me vienne en aide) qui termine la phrase aux Etats-Unis).

En effet :

  • En 2NF, tout attribut (ou colonne) non clé doit dépendre de la clé toute entière (the whole key)
  • En 3NF, tout attribut (ou colonne) non clé ne doit dépendre QUE de la clé (nothing but the key)

Vous voilà maintenant armés pour partir à l’assaut de vos tables et les normaliser ! Bon courage !

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.

PHP : le design pattern Adaptateur

Chez Auto Load, tout est permis !

Pour illustrer notre exemple du design pattern Adaptateur, nous allons cette fois nous prendre pour des inspecteurs du permis de conduire ! Chez AUTO LOAD, nous nous vantons d’obtenir un taux de réussite de 99% à l’examen du permis de conduire et ce sans soudoyer de quelque manière que ce soit nos candidats ! Voyons sans tarder les principales classes qui composent notre exemple. Tout d’abord, la classe InspecteurPermisConduire, qui est notre classe cliente. C’est elle qui va faire usage de nos autres classes.

class InspecteurPermisConduire {
    private $_candidat;
	
    public function __construct(ConducteurInterface $conducteur)
    {
        $this->_candidat = $conducteur;
    }
    public function changerCandidat(ConducteurInterface $conducteur)
    {
        $this->_candidat = $conducteur;
    }
    public function fairePasserExamen()
    {
        $this->_candidat->demarrer();
        $this->_candidat->accelerer();
        $this->_candidat->tournerDroite();
        $this->_candidat->accelerer();
        $this->_candidat->tournerGauche();
        $this->_candidat->ralentir();
        $this->_candidat->reculer();
        $this->_candidat->immobiliser();
    }
}

Une instance donnée d’InspecteurPermisConduire prend en composition tout objet se conformant à l’interface ConducteurInterface : tout d’abord, elle l’exige dans son constructeur, c’est à dire qu’aucune instance d’InspecteurPermisConduire ne peut exister sans un objet qui implémente en totalité ConducteurInterface. Ensuite, nous nous réservons la possibilité de changer de type de candidat à l’exécution (runtime) grâce à un setter. Enfin, la valeur ajoutée de notre classe réside dans sa méthode fairePasserExamen, qui fait effectuer à tout conducteur les manœuvres de base, indispensables pour décrocher le précieux sésame !

Voici justement notre Automobiliste :

interface ConducteurInterface {
	public function demarrer();
	public function tournerGauche();
	public function tournerDroite();
	public function accelerer();
	public function ralentir();
	public function reculer();
	public function immobiliser();
}

class Automobiliste implements ConducteurInterface {
	public function demarrer() {
		echo "tourner la clé de contact ou mettre la carte";
	}
	public function tournerGauche() {
		echo "tourner le volant vers la gauche";
	}
	public function tournerDroite() {
		echo "tourner le volant vers la droite";
	}
	public function accelerer() {
		echo "appuyer sur la pédale d'accélération";
	}
	public function ralentir() {
		echo "relâcher la pédale d'accélération et/ou", 
                      "appuyer sur la pédale de frein";
	}
	public function reculer() {
		echo "passer la marche arrière et accélérer";
	}
	public function immobiliser() {
		echo "mettre le frein à main";
	}
}

Notre classe concrète Automobiliste implémente les fonctions imposées par l’interface ConducteurInterface. Cette interface recense les principales manoeuvres dont la maîtrise est exigée de tout conducteur de véhicule qui se respecte : démarrer et arrêter le véhicule, avancer, reculer, tourner, accélérer et ralentir. Attention cependant, certains véhicules – comme un vélo – ne se démarrent pas, il va falloir prendre en compte cet aspect là…

Bref, à ce stade là, tout est pour le mieux dans le meilleur des mondes ! [Candide]

Schéma UML de l'exemple de design pattern adaptateur

Dès que le vent soufflera, je recodera… (air bien connu)

Jusqu’au jour où Otto Hekohl, le propriétaire de notre école de conduite, vient nous annoncer qu’étant donné que nous sommes situés en bord de mer, il serait plutôt judicieux – en plus d’être diablement bénéfique pour notre chiffre d’affaire – que nous puissions faire passer le permis bateau à des candidats qui sont de plus en plus nombreux et demandeurs…

Photos de gens sautant d'un yacht

Elle est parfois dure la vie à bord d’un frêle esquif !

Avant de se faire congédier par notre patron pour avoir regardé trop de vidéos de chatons montant sur le dos de tortues durant ses heures de travail, Jojo Dingo, le deuxième programmeur de l’entreprise, avait envisagé d’intégrer ce changement de notre stratégie commerciale. Voici la structure logicielle dont nous héritons, maintenant que Jojo a toutes ses journées de libre pour s’adonner à sa félinophilie maladive :

interface NavigateurInterface {
	public function demarrer();
	public function reculer();
	public function tournerBabord();
	public function tournerTribord();
	public function accelerer();
	public function ralentir();
	public function jeterAncre();
}

abstract class Marin implements NavigateurInterface {
	public function jeterAncre() {
		echo "jeter l'ancre à la mer";
	}
}

class UnsupportedMethodException extends Exception {}

class MarinVoile extends Marin {
	public function demarrer() {
		throw new UnsupportedMethodException
                     ("Cette fonctionnalité n'est pas disponible");
	}
	public function tournerBabord() {
		echo "diriger les voiles et la barre pour aller à babord";
	}
	public function tournerTribord() {
		echo "diriger les voiles et la barre pour aller à tribord";
	}
	public function accelerer() {
		echo "positionner les voiles et déterminer l'allure";
	}
	public function ralentir() {
		echo "positionner les voiles et déterminer l'allure";
	}
	public function reculer() {
		echo "positionner les voiles et manœuvrer pour reculer";
	}
}

class MarinMoteur extends Marin {
	public function demarrer() {
		echo "démarrer le moteur";
	}
	public function tournerBabord() {
		echo "manoeuvrer la barre ou le volant pour aller à babord";
	}
	public function tournerTribord() {
		echo "manoeuvrer la barre ou le volant pour aller à tribord";
	}
	public function accelerer() {
		echo "augmenter la vitesse du moteur";
	}
	public function ralentir() {
		echo "dimininuer la vitesse du moteur ou le couper";
	}
	public function reculer() {
		echo "passer la marche arrière";
	}
}

Jojo a bien compris qu’il nous fallait des classes qui nous permettent de gérer les gens qui s’adonnent à la navigation de plaisance motorisée ou à voile et qu’un bateau ne se manœuvrait pas comme une voiture.

Attardons-nous sur la structure de ces classes : nous avons une classe abstraite Marin qui implémente une interface NavigateurInterface et dont les deux filles concrètes MarinVoile et MarinMoteur réalisent la majeure partie des fonctions.

Schéma UML du design pattern adaptateur

Marin est abstraite car trop générique, elle réalise cependant l’implémentation de jeterAncre, qui est commune à toute forme de navigation (bon, c’est vrai, les Optimist n’ont pas d’ancre). Jojo a choisi de créer une exception pour gérer le cas où la fonction démarrer n’est pas prise en charge, plutôt que de l’isoler dans une interface NavigateurMoteurInterface dédiée aux bateaux qui ont ce mode de propulsion. C’est un choix de conception, qui a le mérite de favoriser la généricité, inutile de faire une vérification de la présence de cette méthode en utilisant par exemple method_exists dans la classe client InspecteurPermisConduire. C’est la classe concrète qui gère l’absence de cette méthode en levant une exception correctement typée.

Comment ne pas se faire mener en bateau ?

Adeptes de longue date de la programmation orientée objet, nous souhaitons minimiser l’impact de ce changement d’architecture logicielle dans nos classes existantes. C’est là où l’adaptateur entre en jeu !

L’adaptateur fonctionne exactement sur le même principe que l’adaptateur de votre sèche-cheveux quand vous allez, en Irlande, par exemple ! Il prend en entrée la prise française de votre sèche-cheveux favori et l’adapte aux prises à trois broches que vous trouvez sur l’île Émeraude ! Vous n’avez pas eu à dénuder les fils de votre appareil pour y fixer une prise irlandaise, et heureusement : l’adaptateur fait le travail pour vous ! Et vous n’y avez vu que du bleu !

S’adapter…au changement

L’adaptateur a pour but de faire collaborer des interfaces de prime abord incompatibles, comme ConducteurInterface et NavigateurInterface. C’est un intermédiaire précieux (et réutilisable, ce qui nous met en joie !) qui va nous éviter de déstabiliser notre code base et qui va découpler le client (notre inspecteur du permis de conduire) de notre adapté (ici, nos marins). Plus le couplage est lâche, mieux c’est, je ne vous apprends rien !

L’adaptateur prend en composition l’adapté, et il sera chargé d’effectuer la translation entre les demandes à destination de l’interface originelle (ConducteurInterface) et la nouvelle interface cible (NavigateurInterface).

Pour que cette translation s’effectue de manière transparente pour le système, il faut naturellement que l’adaptateur implémente l’interface originelle. Regardez donc :

class AdaptateurMarin implements ConducteurInterface {
	private $_marin;
	
	public function __construct(Marin $marin) {
		$this->_marin = $marin;
	}
	public function demarrer() {
		$this->_marin->demarrer();
	}
	public function tournerGauche() {
		$this->_marin->tournerBabord();
	}
	public function tournerDroite() {
		$this->_marin->tournerTribord();
	}
	public function accelerer() {
		$this->_marin->accelerer();
	}
	public function ralentir() {
		$this->_marin->ralentir();
	}
	public function reculer() {
		$this->_marin->reculer();
	}
	public function immobiliser() {
		$this->_marin->jeterAncre();
	}
}

Le polymorphisme avant tout ! Notre adaptateur prend en charge des objets de la classe Marin, c’est à dire potentiellement toutes les sous-classes qui en dérivent. Tant mieux car nous en avons deux : MarinVoile et MarinMoteur !

Nous n’avons pas du tout modifié notre classe existante, nous avons isolé la nouveauté dans une nouvelle classe, autonome, avec des responsabilités clairement identifiées.

Les comportements que vous isolez dans des unités de code indépendantes sont réutilisables, ceux que vous enfermez dans des classes ne le sont pas forcément !

Pour que le système prenne en compte cette nouvelle possibilité (à nous l’argent facile !), il nous faudra juste modifier le code appelant. Notre client avait déjà un modificateur d’implémenté, à la bonne heure, servons nous en donc !

$adaptateur = new AdaptateurMarin(new MarinMoteur());
$inspecteur->changerCandidat($adaptateur);
$inspecteur->fairePasserExamen();

La méthode changerCandidat exige en paramètre une implémentation de la classe ConducteurInterface, ça tombe bien c’est précisément ce qu’est Marin et ses filles concrètes.

Souvenez vous toutefois qu’une exception est levée dans notre adaptateur MarinVoile, il faut donc en réalité modifier quelque peu le code appelant, qui va devoir les intercepter et les traiter le cas échéant. N’allons pas chercher midi à quatorze heures, nous nous contenterons d’en afficher le message. « KISS » is the law !

try {
	$inspecteur->fairePasserExamen();
} catch (UnsupportedMethodException $exception) {
	echo $exception->getMessage();
}

Voici maintenant l’architecture de notre code, avec l’adaptateur qui est venu s’intercaler entre l’existant et nos nouvelles fonctionnalités, qui vont nous permettre de gérer marins d’eau douce, flibustiers, jeunes loups de mer, moules à gaufres et autres ectoplasmes !

schema2

Pour lire cet article dans des conditions optimales, il est fortement conseillé d’écouter autant de fois que nécessaire ce titre (en plus d’aller les voir en concert en mai 2014) :

PHP : strlen vs mb_strlen

De l’utilisation dévoyée de strlen

Une erreur communément faite par nombre de développeurs PHP consiste à se servir de strlen pour mesurer la longueur d’une chaîne de caractères. Mais gare ! Car, même si la documentation PHP est quelque peu ambiguë à ce sujet, il est toutefois fait mention du fait suivant :

strlen() retourne le nombres d’octets plutôt que le nombre de caractères dans une chaîne.

Pour des chaînes de caractères ASCII (codées sur un octet – 7 bits, en réalité), pas de problèmes particulier, comme vous le constatez :

strlen

Attention toutefois, les espaces sont aussi des caractères ! Vous pouvez appliquer trim au besoin pour élaguer les espaces (avant et après, pas au milieu de votre châine de caractères !)

strlen-espace

Photo d'un ruban à mesurer

La taille, ça compte !


Là où les choses se corsent quelque peu, c’est lorsque votre chaîne de caractères est susceptible de contenir des caractères codés sur plus d’un octet…

strlen-utf8

La fonction strlen me renvoie 10…elle ne compte pas les caractères (il y en a 9 !) mais les octets nécessaires pour représenter notre chaîne de caractères ! Attention donc aux étourdis qui pensent compter le nombre de caractères, vous faites fausse route ! Mon caractère accentué compte pour deux, mais deux octets…et non deux caractères !

mb_strlen à la rescousse !

Avant de voir ce que donnerait mb_strlen appliquée à notre exemple, tâchons de voir quel codage de caractères nous avons par défaut :
strlen-internal-encoding

Par défaut, notre jeu de caractères est donc latin1. Si nous utilisons mb_strlen sans préciser le jeu de caractères que nous ciblons, le résultat sera le même qu’en utilisant strlen, et ça, nous n’en voulons pas.

Voyons quel est le jeu de caractères de notre chaîne, même si nous nous en doutons un peu ! 😉
7

Nous y voilà donc ! Notre chaîne est codée en UTF-8. Cette information est capitale si nous souhaitons utiliser mb_strlen correctement. Voici donc l’appel à cette fonction tel qu’il doit être fait pour fonctionner :

7

La fonction mb_strlen nous renvoie bien 9, qui est bien le nombre de caractères de notre chaîne !

Attention, le type de retour de mb_strlen est mixed, ce qui signifie que la fonction renvoie plusieurs types :

  • Dans le cas où elle s’exécute correctement, elle renvoie bien le nombre de caractères selon le codage que vous avez choisi (ou celui par défaut)
  • Dans le cas où elle échoue, elle renvoie le booléen false

Faites donc attention à utiliser la triple égalité (en valeur et en nombre). Si vous passez un codage invalide, la fonction vous renverra false et un Warning. Attention cependant pour les petits malins qui abusent du @ pour cacher les erreurs sous le tapis :
+strlen-badencoding

Que votre chaîne soit vide ou que vous mentionniez un codage invalide, avec une égalité simple (non stricte), le résultat sera le même.

Les tests unitaires dans Eclipse PDT avec MakeGood

MakeGood est un plug-in pour Eclipse qui vous permettra de lancer de manière continue (à l’issue d’une simple modification du code) vos tests unitaires. Rien de plus énervant que de devoir alterner entre ligne de commande et IDE pour développer, n’est-ce pas ? Ce plug-in, développé par une entreprise japonaise, est là pour mettre un terme à vos souffrances de développeur PHP consciencieux ! Voyons sans plus tarder comment il s’installe. Je suppose évidemment que vous respectez les pré-requis, à savoir une installation fonctionnelle d’Eclipse PDT, de phpUnit (via PEAR) et de XDebug.

Télécharger et installer le plug-in

Cliquez sur Aide / Installer de nouveaux logiciels

1

Voici la fenêtre qui apparaît à l’écran :

1-bis

Créez un nouveau dépôt pour récupérer les sources du plug-in

Cliquez sur le bouton « Ajouter » (« Add ») et renseignez les champs comme suit :

2

Sélectionnez le paquetage cible

3

Une fois le plug-in installé et Eclipse PDT relancé pour prendre en compte ce nouvel ajout, il va nous falloir paramétrer un projet !

Créer un projet intégrant MakeGood

Créons tout d’abord un projet PHP « local », c’est à dire hébergé sur votre machine. Vous devez avoir un exécutable PHP préalablement installé (PHP cli).

4

Donnons lui un nom trivial, c’est un simple essai, ne cherchons pas les complications inutiles !

5

Voilà maintenant votre projet déployé dans l’explorateur :

6

Ouvrons la vue dédiée à MakeGood :

7

Vous voyez un message d’erreur s’afficher dans cette vue : « MakeGood is not configured yet. Fix… », c’est le signe qu’il nous reste du paramétrage à faire !

8

Paramétrer votre projet PHP pour utiliser MakeGood

L’exécutable PHP

MakeGood aura besoin de localiser votre exécutable PHP, celui que vous utilisez en ligne de commande. Ouvrez donc les préférences générales d’Eclipse PDT :

9

Déployez le noeud intitulé « PHP » et rendez-vous à la sous-section « PHP Executables ». Notez au passage la section sur MakeGood en haut de l’image.

10

Cliquez sur « Add » (« Ajouter ») pour y paramétrer votre executable PHP. A titre d’exemple, voici le mien tel qu’il est configuré sur ma machine GNU/Linux Debian (c’est pourquoi la fenêtre signale « Edit » et pas « Add »).

11

Voici la raison pour laquelle vous devez avoir un PHP Cli installé sur votre machine, c’est une condition sine qua non pour que MakeGood fonctionne, mais ce n’est pas la seule ! Rendez-vous à présent sur le noeud « Debug » de vos préférences PHP :

12

Voici mes paramétrages : nous avons paramétré notre exécutable PHP, autant nous en servir en y faisant référence !

13

PEAR

Il va falloir faire mention de PEAR dans les librairies que devra prendre en charge Eclipse PDT. Pour ce faire, dans les préférences générales de votre IDE, vous vous rendrez à la section « PHP Librairies » :

14

Cliquez sur Nouveau et donnez un nom à votre librairie PEAR…comme PEAR par exemple ! 😉
Une fois que votre librairie apparaît dans la liste, sélectionnez-la et cliquez sur le bouton « Add external folder » pour faire référence au répertoire qui héberge votre installation de PEAR (la racine de tous vos extensions PEAR si vous préférez…), une fois ceci fait, vous devriez avoir quelque chose qui ressemble à peu près à ça à l’écran :

15

Reste à faire mention de votre librairie PEAR cette fois-ci dans les propriétés de votre projet et non plus d’Eclipse PDT. Sélectionnez votre projet dans l’explorateur et sélectionnez les propriétés avec un clic droit.

16

Votre cible est la rubrique « PHP Include Path » où vous allez renseigner la librairie PEAR créée un peu plus haut, mais au niveau Eclipse et pas projet. Cliquez sur « Add Library » ou son équivalent français et sélectionnez ensuite votre bibliothèque PEAR.

17

18

Voici venu le moment de créer un répertoire destiné à contenir nos tests unitaires, faisons preuve d’une imagination sans limites et nommons le « Tests ».
19

20

Toujours dans les propriétés du projet (clic droit ou bien Alt+Entrée, une fois ce projet sélectionné dans l’explorateur), rendez vous dans la partie dédiée à MakeGood et cliquez sur « Add » dans l’onglet « General » pour ajouter votre joli répertoire de test nouvellement créé !

21

22

Désormais, le message d’erreur rouge qui apparaissait tout à l’heure a laissé place à la mention « Waiting a for test run… » dans la vue MakeGood et vous êtes prêt(e) à vous lancer dans le test unitaire :

23

Créer des tests unitaires simples

Notre classe à tester

Créer un fichier PHP à la racine de « MonSuperProjet », nommez le Calculette.php. Dans votre répertoire Tests, créez un fichier PHP nommé « CalculetteTest.php ». Respectez le nommage, dicté par les bonnes pratiques.

24

Vous avez peut-être remarqué ce faisant que, et même si c’est allé très vite, quelque chose s’est passé alors même que vous avez crée ce fichier CalculetteTest.php dans le répertoire Tests…le statut de MakeGood a changé, il a tenté de lancer vos tests mais comme votre fichier est vide, il affiche « Running a test… » avec 0 tests indiqués.

Calculette.php

Ce fichier de test est très librement adapté de l’exemple de la documentation officielle de phpUnit, qu’évidemment je vous encourage à aller voir en ligne ! Voici son contenu :

class Calculette {
    public function RacineCarree($nombre) {

        if (0 > $nombre) {
	    throw new InvalidArgumentException('Le nombre fourni est négatif');
	}

	return sqrt($nombre);
    }
}

CalculetteTest.php

Notre classe de test aura, quant à elle, cette allure là :


require_once 'Calculette.php';

class CalculetteTest extends PHPUnit_Framework_Testcase
{
    protected $_calculette;

    public function setUp() {
	$this->_calculette = new Calculette;
    }

    public function testRacineCarreeNegative() {
	$this->setExpectedException('InvalidArgumentException');
	$this->_calculette->RacineCarree(-1);
    }

    public function testRacineCarreePositive() {
	$this->assertEquals(3, $this->_calculette->RacineCarree(9));
    }
}

N’hésitez pas à ajouter des méthodes dans la classe à tester et évidemment à les tester dans CalculetteTest.php, vous verrez qu’à chaque Ctrl+S dans votre code (dans Calculette.php comme dans CalculetteTest.php), les tests vont se relancer, c’est ça l’idée du « Continuous Test Runner » que se veut être MakeGood. Comme nos tests sont bien écrits, c’est du vert que nous avons; les deux passent !

26

N’oubliez pas d’être curieux(se) et d’aller inspecter les temps d’exécution des tests, vous aurez parfois des surprises !
27

A vous de jouer maintenant !

MySQL – les clés étrangères et l’intégrité référentielle

L’heure est venue de nous pencher sur la notion de clé étrangère dans une table. Les clés étrangères servent à établir des contraintes dites d’ « intégrité référentielle ».
192-246-thickbox

Qu’est-ce qu’une clé étrangère ?

C’est un champ (ou une colonne, ou un attribut, ou une propriété) d’une table. Jusque là, voilà qui est plutôt rassurant !

Qu’est-ce l’intégrité référentielle ?

Dans cette expression, nous avons la notion centrale de « référence ». Il est en effet question d’un champ qui fait référence à un autre. Qu’est-ce que cela signifie, dans les faits ? Et bien simplement que, si A est un champ qui référence B, alors A aura pour valeurs admissibles un sous-ensemble des valeurs de B.

Plus concrètement, si votre champ B, référencé par A, a pour valeurs entières 1,2 et 3, alors A ne pourra avoir lui aussi que ces valeurs et pas 4 ou 0 !

Les clés étrangères dans MySQL

En SQL, quelque soit le SGBDR cible, la notion de clé étrangère s’applique à l’aide du mot clé FOREIGN KEY. Comme vous vous apprêtez à modifier la structure de la table pour lui imposer cette contrainte, vous devrez utiliser ALTER TABLE.

Nous allons voir la syntaxe en détail dans l’exemple à venir.

Un exemple, tout de suite !

Soit une table qui recense les joueurs d’un sport quelconque (qui se joue à deux):

create table joueur (
id smallint unsigned not null primary key,
nom varchar(20),
prenom varchar(20));

La table joueur possède un champ qui est son identifiant primaire : c’est id ! Ce champ là possède un index parce que je l’ai explicitement déclaré comme clé primaire de ma table (primary key). Voilà pour ma table joueur, qui est somme toute très simple.

Nous allons maintenant créer une table qui va servir à opposer un joueur à un autre :

create table matches (
id smallint unsigned not null primary key,
id_joueur1 smallint unsigned not null,
id_joueur2 smallint unsigned not null,
duree tinyint unsigned not null);

Cette table là possède elle aussi un identifiant (une clé primaire): en plus de cet identifiant, elle possède deux champs qui vont correspondre à des identifiants de joueurs, donc à des valeurs du champ id présentes dans notre table joueur ! Dans l’état rudimentaire de cette table il est tout à fait possible de rentrer des joueurs, mais comment forcer l’utilisateur de nos données à ne rentrer que des identifiants de joueurs qui existent ? C’est là que la notion de clé étrangère entre en jeu !

Nous allons faire en sorte que les valeurs admissibles pour les champs id_joueur1 et id_joueur2 de la table matches soient uniquement celles présentes dans le champ id de la table joueur, en clair : interdit de faire s’affronter des joueurs qui n’existent pas !

Il va nous falloir modifier la structure de la table matches, il y a donc de l’ALTER dans l’air ! Notez qu’il va falloir créer un index sur le champ référençant (celui qui va servir à référencer l’autre, qui est le référencé) : cet index est INDISPENSABLE car il évite les full table scans et accélère donc la vérification des contraintes d’intégrité référentielle !

ALTER TABLE matches ADD INDEX (id_joueur1); 
ALTER TABLE matches ADD FOREIGN KEY (id_joueur1)
REFERENCES joueur (id);

La voilà enfin notre référence ! Nous disons en substance « crée moi un lien entre le champ id de joueur et le champ id_joueur1 de matches » ! Dorénavant, toute tentative d’insérer un identifiant de joueur qui n’existe pas se soldera par un échec ! Faisons le de suite :

mysql> insert into matches (id_joueur1) values (1);
ERROR 1452 (23000): Cannot add or update a child row:
a foreign key constraint fails (`test`.`matches`, 
CONSTRAINT `matches_ibfk_1` FOREIGN KEY (`id_joueur1`) 
REFERENCES `joueur` (`id`))

Il n’y a pas de joueur dans la table qui leur est réservée, donc toute tentative de rentrer un numéro de joueur dans id_joueur1 sera sanctionnée par ce message d’erreur !

Pour que la même contrainte pèse sur le champ id_joueur2, il faut effectuer exactement la même opération que précédemment, à avoir : la pose d’un index sur le champ ET la pose de la clé étrangère :

ALTER TABLE matches ADD INDEX (id_joueur2); 
ALTER TABLE matches ADD FOREIGN KEY (id_joueur)
REFERENCES joueur (id);

Voilà que maintenant pèse sur notre table matches deux contraintes d’intégrité référentielle qui font qu’il n’est possible de faire s’affronter que des joueurs répertoriés dans notre table joueur !

La fin d’une idée reçue

Je me suis aperçu que beaucoup de gens étaient encore persuadés qu’une contrainte de clé étrangère ne pouvait s’appliquer qu’entre un champ référençant et un champ référéncé obligatoirement clé primaire de sa table. C’est absolument faux et ce même si en pratique on s’en sert le plus souvent comme ça ! Pour qu’un champ puisse en référencer un autre, il suffit que tous les deux soient indexés et rien de plus. Comme la clé primaire est par définition un index, c’est elle qui rend cette référence possible mais autrement, vous pouvez tout à fait dans T1(A,B) faire que B référence D de T2(C,D) qui n’est pas le moins du monde la clé primaire de T2 ! (les C.P sont en gras)

A retenir

  • Pour poser une clé étrangère sur A qui référence B, il faut que A et B soient rigoureusement du même type ! Attention quand vous utilisez des types numériques, il faut que les signes (SIGNED/UNSIGNED) soient identiques !
  • Seul le moteur InnoDB autorise l’utilisation de ce mécanisme !
  • On remplit d’abord les tables référencées avant de remplir les référençantes !
  • On vide les tables référençantes avant de vider les référencées !
  • Si vous souhaitez contourner momentanément la vérification des contraintes d’intégrité, pour charger des données en masse par exemple, utilisez SET FOREIGN_KEY_CHECKS = 0;

MySQL – update et delete avec des jointures

Le but de ce billet est de réaliser des opérations de mise à jour ou de suppression à l’aide d’une jointure, externe ou interne.
Voici le schéma qui nous sert de base de travail :

  • une table client
  • une table produit
  • une table commande, avec deux contraintes de clé étrangère partant vers chacune des tables

Hoan_Bridge

Le SQL du schéma

create table client(
id tinyint unsigned primary key auto_increment,
nom varchar(30) not null,
prenom varchar(20) not null);

insert into client (nom, prenom) values 
('Ferrandez', 'Sébastien'), ('Jambon', 'Paul');

create table produit(
id tinyint unsigned primary key auto_increment,
designation varchar(30) not null,
prix float (6,2) unsigned not null);

insert into produit (designation, prix) values 
('Mousse à raser', 7.99), ('Stylo BIC', 4.99);

create table commande (
id tinyint unsigned primary key auto_increment, 
id_client tinyint unsigned , 
id_produit tinyint unsigned, 
quantite tinyint unsigned default 1,
date timestamp);

alter table commande add constraint foreign key (id_client) 
references client(id);

alter table commande add constraint foreign key (id_produit)
references produit(id);

insert into commande(id_client,id_produit, quantite )
values (1,1,2), (1,2,1), (2,2,1), (2,1,2);

Les jointures, externes et internes

Le principe de la jointure est trivial, on crée comme une sorte de « pont » entre au moins 2 tables selon une condition qu’on va très simplement appeler « condition de jointure ». Une condition de jointure n’est pas forcément une clause d’égalité !

Prenons l’exemple de deux tables sans rapport quelconque :

create table eleve (nom varchar(20), age tinyint unsigned);
insert into eleve values ('Ferrandez', 10), ('Lucas', 30);
create table produit (nom varchar(20), prix float(6,2) unsigned);
insert into produit values ('iPhone5', 499), ('Jambon', 10);

Qu’est-ce qui nous empêche de faire une jointure entre un élève et un produit basé sur l’égalité de valeur de l’âge de l’élève et du prix du produit ?

SELECT * 
FROM  eleve 
INNER JOIN produit ON ( eleve.age = produit.prix ) 

Nous obtenons

+-----------+------+--------+-------+
| nom       | age  | nom    | prix  |
+-----------+------+--------+-------+
| Ferrandez |   10 | Jambon | 10.00 |
+-----------+------+--------+-------+
1 row in set (0.00 sec)

Une jointure ne se fait pas uniquement sur la base d’une égalité de valeur, on peut utiliser d’autres opérateurs arithmétiques :

SELECT * 
FROM  eleve
INNER JOIN produit ON ( eleve.age <= produit.prix ) 

Celle-ci remontera les tuples suivants :

+-----------+------+---------+--------+
| nom       | age  | nom     | prix   |
+-----------+------+---------+--------+
| Ferrandez |   10 | iPhone5 | 499.00 |
| Lucas     |   30 | iPhone5 | 499.00 |
| Ferrandez |   10 | Jambon  |  10.00 |
+-----------+------+---------+--------+
3 rows in set (0.00 sec)   

Et pire, qu’est-ce qui nous empêche de faire une jointure sur des champs qui n’ont pas le même type de données (horreur !) :

SELECT * 
FROM  eleve
INNER JOIN produit ON ( eleve.age <= produit.nom ) 

Si on a le droit de faire ça, c’est que l’optimiseur MySQL effectue des conversions de type pour effectuer des comparaisons (différence, égalité etc.). Mais recentrons-nous sur le sujet du billet ! Une requête de jointure externe, contrairement aux internes, retourne à la fois les correspondances (c’est à dire, les enregistrements satisfaisant la clause de jointure) et les non-correspondances (celles qui ne la satisfont pas). Voyons ce que donne :

SELECT * 
FROM  eleve 
LEFT JOIN produit ON ( eleve.age = produit.prix ) 
+-----------+------+--------+-------+
| nom       | age  | nom    | prix  |
+-----------+------+--------+-------+
| Ferrandez |   10 | Jambon | 10.00 |
| Lucas     |   30 | NULL   |  NULL |
+-----------+------+--------+-------+
2 rows in set (0.00 sec)

Les non-correspondances sont signalées par la présence du marqueur NULL. Si vous choisissez de les remonter, rien de plus facile :

SELECT eleve.*
FROM  eleve 
LEFT JOIN produit ON ( eleve.age = produit.prix ) 
WHERE produit.prix IS NULL

Notez bien que le même test, mais sur produit.nom IS NULL aurait également fonctionné !
Bref, nous savons maintenant ce qu’est une jointure interne (que ce qui correspond) et une jointure externe (ce qui correspond + ce qui ne correspond pas).

La mise à jour

Nous allons mettre à jour les commandes du client Sébastien Ferrandez en disant qu’elles ont eu lieu le 24 Décembre 2013 à 23:59:59 :

UPDATE commande INNER JOIN client
ON (commande.id_client = client.id)
SET commande.date = '2013-12-24 23:59:59'
WHERE client. nom = 'ferrandez'
AND client.prenom = 'sebastien'

Notez que nous sommes en collation latin1_general_ci et que nous ne tenons donc compte ni des accents ni des majuscules…
Si d’aventure vous souhaitez modifier d’autres champs (y compris dans la table client, pas que commande !), séparez les dans SET avec des virgules !

Vous pouvez très bien utiliser la jointure externe pour mettre a jour des enregistrements ne se trouvant pas dans la table commande (nous aurions par exemple dans la table client un champ booléen A_DEJA_COMMANDE que nous mettrions à 0 si aucune commande n’était trouvée pour celui-ci).

La suppression

Insérons un nouveau tuple dans client :

insert into client (nom, prenom) values ('Diego', 'Miguel');

Et effectuons sa suppression immédiate en disant « J’efface les clients qui n’ont pas passé de commande »

DELETE client.* FROM client
LEFT OUTER JOIN commande 
ON ( client.id = commande.id_client )
WHERE commande.id_client IS NULL

N’oubliez pas que vous n’êtes absolument pas limités à UNE jointure et que vous pouvez très bien alterner jointures externes et internes dans la même requête ! Entraînez-vous sur des petits schémas tout simples du même genre que celui que nous avons vu dans ce billet et tout ça n’aura bientôt plus de secret pour vous !