PHP5 : la magie continue

Magazine
Marque
GNU/Linux Magazine
Numéro
153
Mois de parution
octobre 2012
Spécialité(s)


Résumé
Nous continuons notre découverte des méthodes magiques de PHP en élaborant un cache, non sur les propriétés de l'objet, mais sur les méthodes.

Body

1. Rappel sur les méthodes magiques

Pour ceux d'entre vous qui auraient manqué l'épisode précédent (GNU/Linux Magazine n°151), les méthodes magiques de PHP sont une fonctionnalité avancée de ce langage, permettant de modifier profondément le comportement attendu des objets de la classe à laquelle elle s'applique. La difficulté que posent de telles méthodes est qu'elles se font facilement oublier par le développeur, qui pourrait avoir de grandes difficultés à comprendre l'origine de certains bogues, surtout dans le contexte d'un travail d'équipe ou de longs enchaînements d'héritage dans les classes. La dernière fois, nous avions utilisé la méthode __get pour permettre à un objet de déterminer lui-même ses propriétés, une fois pour toutes. Dans notre exemple, il en avait résulté un code bien plus court et plus lisible, ainsi qu'un gain de performance notable.

Cette fois-ci, notre travail sera plus élaboré, puisqu'il s'agira d'implémenter un cache automatique sur les méthodes d'une classe. Un premier intérêt est évident : éviter la ré-exécution inutile d'une méthode appelée avec les mêmes paramètres sans avoir à gérer le cache à l'intérieur même de la fonction.

Il y a toutefois un autre intérêt, en programmation PHP avancée dans le cadre d'un projet MVC (Modèle/Vue/Contrôleur). Dans ce contexte, il est fréquent de définir une méthode statique sur la classe parente des modèles, qui crée une instance de cette classe à partir de paramètres restreints, typiquement, un identifiant permettant d'instancier un objet PHP depuis un enregistrement en base de données. Une telle méthode doit veiller à ne pas créer deux objets identiques qui pourraient ensuite évoluer indépendamment l'un de l'autre : lors de l'enregistrement de cet objet, une part des modifications serait perdue... Cette méthode doit donc renvoyer le même objet et ne pas le récréer. Ce point est simplifié par le fait que PHP passe les objets par référence et non par valeur, un simple cache sur la méthode permet donc de résoudre le problème. Un cache sur la classe parente de tous les modèles serait donc une solution définitive.

En plus des méthodes magiques, nous allons devoir faire appel aux « fonctions sur la gestion des fonctions » (http://php.net/manual/fr/ref.funchand.php). Celles-ci peuvent se montrer très utiles dans le cadre d'une programmation objet avancée, elles sont cependant assez retorses à utiliser.

Pour en savoir plus sur les méthodes magiques en général, je vous renvoie à la page du manuel officiel de PHP qui leur est consacrée (http://php.net/manual/fr/language.oop5.magic.php). Sachez seulement que ces méthodes ne sont pas appelées « magiques » par hasard : autant leur utilisation peut améliorer grandement votre code à de nombreux points de vue, autant les bogues qu'elles pourraient provoquer sont difficiles à déceler... N'est pas Gandalf qui veut !

2. Présentations des méthodes magiques à utiliser

Nous allons travailler avec deux méthodes magiques ici : __call et __callStatic. Ces fonctions sont appelées lorsque PHP détecte un appel à une méthode inaccessible (inexistante, protégée et appelée hors de l'objet ou encore privée et appelée hors de la classe qui la définit).

La première, __call() est appelée dynamiquement, c'est-à-dire depuis un objet existant :

$monobjet->maMethodeInaccessibleDynamique();

La seconde, __callStatic() est appelée de façon statique, c'est-à-dire comme méthode de la classe :

maclasse::maMethodeInaccessibleStatique();

Remarquez que si vous avez la mauvaise habitude d'appeler de manière dynamique des méthodes statiques, vous devrez implémenter deux fois le même traitement (ou le mutualiser dans une méthode que vous appellerez les deux fois) pour couvrir ces deux types d'appel. Sachez tout de même que PHP est considérablement plus performant avec des méthodes statiques que des méthodes dynamiques. En fait, vous ne devriez recourir aux appels dynamiques que si vous avez besoin de $this, et si tel est le cas, vous ne pourrez appeler ces méthodes de manière statique... Tout cela est cohérent.

3. Convention de nommage

Comme dans notre précédent article, nous ne voulons pas rendre accessibles toutes les méthodes inaccessibles de l'objet, mais nous voulons seulement rendre inaccessibles certaines méthodes pour permettre à l'objet d'utiliser son cache avant d'appeler la méthode si nécessaire. Il faut donc que l'objet ait un moyen de déterminer s'il doit ou non appliquer ce traitement lors d'un appel de méthode inaccessible. Pour y parvenir, nous allons utiliser une convention de nommage : nous allons préfixer toutes les méthodes auxquelles le cache s'appliquera par _C_. Ainsi, il nous sera possible d'activer ou de désactiver le cache sur une méthode simplement en ajoutant ou en supprimant ce préfixe.

4. Implémentation dynamique

4.1. Le code

classwithCachedMethods{
 var $methodsCache=array();
 static $staticMethodsCache=array();
 function __call($method,$args) {
 $key = md5(serialize($args));
 // traitement de cas particuliers
 if (substr($method,0,3)=='_C_') {
 if (method_exists($this,$method)) {
 // la méthode cachée a été appelée explicitement: on rafraichit le cache
 return $this->methodsCache[$method][$key]=call_user_func_array(array(get_called_class(),$method),$args);
 }else{
 // le cache a été désactivé pour cette fonction, mais le code demande un rafraichissement du cache, on rappelle simplement la fonction
 $method = substr($method,3);
 return call_user_func_array(array(get_called_class(),$method),$args);
 }
 }
 
 // traitement normal
 $cachedMethod = '_C_'.$method;
 if (method_exists($this,$cachedMethod)) {
 if (!is_array($this->methodsCache[$cachedMethod]) || !array_key_exists($key,$this->methodsCache[$cachedMethod])) {
 $this->methodsCache[$cachedMethod][$key]=call_user_func_array(array(get_called_class(),$cachedMethod),$args);
 }
 return $this->methodsCache[$cachedMethod][$key];

 Voici donc notre nouvelle classe que nous avons appelée withCachedMethods. Nous n'avons pour le moment implémenté qu'une seule méthode, la fonction __call, qui prend en charge l'interception de méthodes inaccessibles. Par ailleurs, une propriété est définie : $methodsCache, qui contient le cache des méthodes sous forme d'un tableau.

Détaillons le fonctionnement de notre méthode magique __call. Remarquons, en guise de préambule, que le code qui la compose se structure en deux parties. La première est consacrée au traitement de cas particuliers sur lesquels nous reviendrons plus loin. La seconde est le traitement normal, celui qui permet le fonctionnement de notre cache de la façon la plus ordinaire.

__call() reçoit deux arguments : le premier est le nom de la méthode inaccessible, le second un tableau contenant les arguments envoyés à cette méthode.

Du nom de la méthode, nous déduisons celui de la méthode cible que nous plaçons dans la variable $cachedMethod. Si un appel est fait à ousontmeslunettes() et que ousontmeslunettes() n'existe pas ou n'est pas accessible, nous allons travailler avec_C_ousontmeslunettes().

Nous utilisons la fonction method_exists pour savoir si elle existe effectivement. Si tel est le cas, cela signifie que le traitement du cache doit être effectué. Il peut être de deux types : soit on trouve la valeur recherchée dans le cache, soit on exécute la méthode et on place le résultat dans le cache.

Notre cache, nous l'avons dit, est un tableau. Pour y stocker les données, la première clé que nous utilisons est le nom de la méthode cachée, _C_ousontmeslunettes, dans notre exemple. Le deuxième élément qui nous permet de déterminer si le résultat recherché se trouve déjà dans le cache est la liste des arguments. Ceux-ci nous sont connus sous la forme d'un tableau. Toutefois, PHP ne nous permettant pas d'utiliser un tableau comme index d'un autre tableau (ce qui ne serait d'ailleurs pas judicieux), nous ne pouvons les utiliser en l'état. PHP nous fournit heureusement une fonction qui permet de convertir un tableau en chaîne de caractères sans perte de données (une autre fonction existe permettant le traitement inverse si nécessaire). Il s'agit de serialize(). Mais cette transformation, bien que fonctionnant, n'est sans doute pas optimale : il est fréquent d'appeler une fonction avec des arguments très complexes et très lourds, des objets contenant des tableaux de valeurs, d'autres objets... Bref, une telle liste d'arguments peut se révéler très longue et il n'est pas utile de conserver toutes ces informations. Nous allons utiliser le hash md5() des arguments sérialisés comme deuxième clef de notre cache :

$key=md5(serialize($args));

Ensuite, nous déterminons s'il existe un cache pour notre méthode et si oui, si ce cache contient la clef correspondant à nos arguments. Il est important de vérifier si le cache existe bel et bien (is_array()) avant d'y chercher la clef car array_key_exists() peut, selon la configuration et la version de PHP, lever des erreurs.

Si une telle clef n'existe pas, il faut créer cette clef et déterminer la valeur qui lui correspond. Ici, il va nous falloir faire appel à la fonction fournie par PHP call_user_func_array. Celle-ci permet d'appeler une autre fonction en lui passant un tableau d'arguments de longueur arbitraire, comme si cette fonction cible avait été appelée normalement. Par exemple :

functionY($a,$b,$c){

//traitement

}

Peut être appelée ainsi :

call_user_func_array('Y',array('argA,'argB','argC'));

Le premier argument de call_user_func_array() permet d'appeler une méthode plutôt qu'une fonction lorsqu'il contient un tableau dont le premier élément est le nom de la classe et le second le nom de la méthode. Et c'est ce que nous faisons ici. Il est à noter que cet appel est effectué de manière dynamique.

4.2. Le test

Pour tester notre cache, nous allons ajouter à notre objet une méthode simple, qui aura pour seule tâche d'effectuer une addition. Pour savoir si le résultat que nous obtenons est issu du cache ou non, nous allons ajouter une ligne à notre méthode qui affichera un petit texte indiquant que non. Voici donc la méthode que nous ajoutons :

function _C_plus($a,$b){
 print "No cache!\n";
 return $a+$b;

 Maintenant, écrivons encore quelques lignes en dehors de la classe pour la tester :

$test = newwithCachedMethods();
print $test->plus(1,2)."\n";
print $test->plus(3,4)."\n";

 Le résultat sera :

No cache!

3

No cache!

7

3

Nous constatons que le premier appel à plus() avec comme arguments 1 et 2, puis 3 et 4, nous fournit un résultat déterminé par la méthode _C_plus, mais que le deuxième appel à plus() avec comme arguments 1 et 2 nous fournit un résultat issu du cache.

4.3. Traitement des cas particuliers

Nous l'avons indiqué plus haut, notre méthode __call supporte le traitement de certains cas particuliers. Quels sont-ils ? Il y en a deux. Dans les deux cas, le code appelant la méthode y accède explicitement en utilisant le préfixe de cache. Cela signifie que le programmeur qui a écrit cet appel sait qu'il s'agit d'une méthode cachée : c'est exactement ce qu'il ferait s'il voulait contourner le cache. Cet appel étant tout de même intercepté par __call(), cela implique que même préfixée, la méthode appelée reste inaccessible, car elle a été déclarée protected et appelée extérieurement à l'objet. Pour autant, nous n'allons pas laisser tomber notre développeur : s'il appelle explicitement la méthode de cette façon, c'est que le résultat présent dans le cache ne le satisfait pas. Peut-être a-t-il détecté une erreur, ou bien sait-il que la donnée est périmée suite à une action de sa part ? Peu importe : nous pouvons interpréter cet appel comme une demande de correction du cache. Nous allons donc rappeler la fonction et rafraîchir la valeur stockée dans le cache.

Toutefois, dans ce cas particulier, se dissimule un autre cas particulier : nous avons défini que nous pouvions désactiver le cache d'une méthode simplement en supprimant son préfixe (et sa protection). Si nous voulons pouvoir continuer ainsi, il faut également que notre interception soit capable de traiter le cas où, d'une part, le cache aurait été désactivé pour la méthode et, d'autre part, une demande de rafraîchissement de cache serait fait pour cette méthode. Dans ce cas-là, il nous faut alors appeler la méthode non-préfixée et renvoyer directement ce résultat.

Dès lors, le code suivant :

print $test->plus(1,2)."\n";
print $test->plus(1,2)."\n";
print $test->_C_plus(1,2)."\n";

produira :

No cache!

3

3

No cache!

3

Voilà, nous en avons terminé avec la partie dynamique. Nous pouvons donc maintenant passer à l'...

5. Implémentation statique

5.1. Avertissement

La version 5 de PHP a beaucoup évolué dans ses différentes sous-versions en élargissant les possibilités offertes au programmeur pour utiliser les classes d'objets de manière statique. En ce qui concerne les techniques de programmation développées dans la suite de cet article, il est impératif de disposer d'une version de PHP au moins égale à 5.3, sans quoi, cela ne fonctionnera pas.

5.2. Le code

Ajoutons maintenant le code suivant à notre classe withCachedMethods :

static $staticMethodsCache = array();
 static function __callStatic($method,$args) {
 $key = md5(serialize($args));
 $calledClass = get_called_class();
 
 // traitement des cas particuliers
 if (substr($method,0,3)=='_C_') {
 if (method_exists($calledClass,$method)) {
 // la méthode cachée a été appelée explicitement: on rafraichit le cache
 return static::$staticMethodsCache[$calledClass][$method][$key]=forward_static_call_array(array($calledClass,$method),$args);
 }else{
 // le cache a été désactivé pour cette fonction, mais le code demande un rafraichissement du cache, on rappelle simplement la fonction
 $method = substr($method,3);
 return forward_static_call_array(array($calledClass,$method),$args);
 }
 }
 
 // traitement normal
 $cachedMethod = '_C_'.$method;
 if (method_exists($calledClass,$cachedMethod)) {
 if (!is_array(static::$staticMethodsCache[$calledClass][$cachedMethod]) || !array_key_exists($key,static::$staticMethodsCache[$calledClass][$cachedMethod])) {
 static::$staticMethodsCache[$calledClass][$cachedMethod][$key]=forward_static_call_array(array($calledClass,$cachedMethod),$args);
 }
 return static::$staticMethodsCache[$calledClass][$cachedMethod][$key];

 L'algorithme fondamental étant le même que pour l'implémentation dynamique, il n'y a rien de surprenant à ce que les deux méthodes se ressemblent autant. Les différences tiennent au fonctionnement statique.

Notons que nous commençons par définir une propriété de la classe $staticMethodsCache, qui contiendra le cache statique. Une telle variable est habituellement appelée en utilisant la syntaxe nom_de_la_classe::$nom_de_la_propriété. PHP permet également d'appeler ces propriétés de manière réflexive et de deux façons différentes. A l'intérieur d'un objet, un appel réflexif est fait en utilisant la variable $this. A l'intérieur d'une classe, on peut utiliser soit self, soit static. Il y a une grande différence entre les deux, qui tient à l'héritage : self va faire une réflexion sur la classe où il est écrit, alors que static va faire une réflexion sur la classe dans laquelle la méthode est exécutée.

Un peu confus ? Prenons un exemple :

class Vador {
 static $force;
 static function deQuelCote() {
 return "Obscur";
 }
 static function faisTonChoix() {
 return self::deQuelCote();
 }
 
 static function quelEstTonDestin() {
 return static::deQuelCote();
 }
}
class Luke extends Vador {
 static function deQuelCote() {
 return "Lumière";

 
print Luke::faisTonChoix()."\n";
print Luke::quelEstTonDestin()."\n";

Un tel code affichera :

Obscur

Lumière

Qu'avons-nous fait ? Nous avons défini une classe Vador avec trois méthodes statiques : dequelCote() qui retourne toujours "Obscur", puis deux autres qui appellent cette première, mais l'une en utilisant self et l'autre en utilisant static. Sur la classe Vador, ces méthodes fourniraient toujours le même résultat. Mais dès lors que nous les utilisons sur la classe Luke, qui en hérite mais qui redéfinit la méthode deQuelCote, il n'en est plus de même et la différence entre static et self devient flagrante.

Dans notre contexte, nous ne connaîtrons pas les dilemmes de Luke, nous voulons que nos classes aient chacune un cache bien indépendant, aussi utilisons-nous systématiquement static dans nos méthodes qui gèrent le cache.

forward_static_call_array() effectue le même travail que call_user_func_array(), mais de manière statique.

Attention ! Il y a une différence fondamentale entre le fonctionnement statique et le fonctionnement dynamique, qui si elle n'est pas prise en compte au moment d'une implémentation statique, pourrait être à l'origine de bogues très retords : les variables statiques sont héritées par référence. Cela signifie que ce code :

class maman {

 static $test;

}

class fille extends maman {}

maman::$test = 'ko';

fille::$test = 'ok';

print maman::$test."\n";

print fille::$test."\n";

n'affichera pas :

ko

ok

comme on pourrait s'y attendre, mais :

ok

ok

La solution, si c'est un problème, consiste à redéfinir la variable statique dans la classe fille. Ainsi, avec le code suivant :

class fille extends maman {

 static $test;

}

Les variables statiques $test de la fille et de la mère seront bien différentes.

Dans notre code, nous ne voulons pas que les caches de l'une ou l'autre classe se mélangent, simplement parce que ces classes auraient des méthodes synonymes. Même héritées, elles pourraient fournir des résultats différents, leurs contextes statiques pouvant être différents. Pour pallier cela, nous avons ajouté une dimension à notre tableau, le premier index étant toujours le nom de la classe appelée, tel que donné par get_call_class(). Cette précaution est suffisante et permet d'éviter de redéfinir le cache dans toutes les classes qui utiliseront cette méthode de cache.

6. Pour finir

Nous avons vu lors de ces deux articles comment implémenter des caches automatiques et puissants sur des classes d'objets PHP en utilisant certaines des fonctions les plus avancées de PHP, les méthodes magiques et les fonctions de manipulation de fonctions. Tout cela est bien joli, mais dans un contexte où les développeurs sont amenés de plus en plus souvent à étendre des classes fournies par des frameworks et PHP ne permettant pas l'héritage multiple, cela peut rester lettre morte dans les cas les plus intéressants. Heureusement est arrivé... PHP 5.4, qui propose les « traits » pour répondre à cette difficulté. Nous aborderons ce concept dans un prochain article.




Article rédigé par

Par le(s) même(s) auteur(s)

Borg Backup, la sauvegarde facile, économique et efficace

Magazine
Marque
Linux Pratique
Numéro
142
Mois de parution
mars 2024
Spécialité(s)
Résumé

Retour aux fondamentaux : aucun système d'information n'a plus de valeur que sa sauvegarde ! Malgré cela, cet aspect est souvent négligé, tant on n'en ressent pas l'absence jusqu'au jour où elle se faire cruellement sentir. Mais ce jour-là, il est trop tard. Voyons comment prévenir le pire avec Borg Backup, et son outil compagnon Borgmatic.

Développement de macros en Rust

Magazine
Marque
GNU/Linux Magazine
Numéro
268
Mois de parution
mars 2024
Spécialité(s)
Résumé

Programmer en Rust, c’est bien. Mais programmer, toujours en Rust, des générateurs de code Rust, exécutés tout juste au moment de la compilation, c’est mieux ! Voilà ce que permettent les macros, avec toujours cette efficacité redoutable à laquelle nous a habitués ce langage.

Alpine.js : un framework JavaScript minimaliste

Magazine
Marque
GNU/Linux Magazine
Numéro
268
Mois de parution
mars 2024
Spécialité(s)
Résumé

Ces dernières années, les frameworks JavaScript se sont multipliés à foison, apportant toujours plus de fonctionnalités et de réactivité. Mais cette effervescence a pour conséquence une plus grande difficulté à comprendre l'offre et un accroissement de technicité. Alors, pourquoi ajouter un autre framework ? Pour retrouver la simplicité, découvrez Alpine.js, ou comment utiliser JavaScript sans écrire de JavaScript.

Les derniers articles Premiums

Les derniers articles Premium

Quarkus : applications Java pour conteneurs

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Initié par Red Hat, il y a quelques années le projet Quarkus a pris son envol et en est désormais à sa troisième version majeure. Il propose un cadre d’exécution pour une application de Java radicalement différente, où son exécution ultra optimisée en fait un parfait candidat pour le déploiement sur des conteneurs tels que ceux de Docker ou Podman. Quarkus va même encore plus loin, en permettant de transformer l’application Java en un exécutable natif ! Voici une rapide introduction, par la pratique, à cet incroyable framework, qui nous offrira l’opportunité d’illustrer également sa facilité de prise en main.

De la scytale au bit quantique : l’avenir de la cryptographie

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Imaginez un monde où nos données seraient aussi insaisissables que le célèbre chat de Schrödinger : à la fois sécurisées et non sécurisées jusqu'à ce qu'un cryptographe quantique décide d’y jeter un œil. Cet article nous emmène dans les méandres de la cryptographie quantique, où la physique quantique n'est pas seulement une affaire de laboratoires, mais la clé d'un futur numérique très sécurisé. Entre principes quantiques mystérieux, défis techniques, et applications pratiques, nous allons découvrir comment cette technologie s'apprête à encoder nos données dans une dimension où même les meilleurs cryptographes n’y pourraient rien faire.

Les nouvelles menaces liées à l’intelligence artificielle

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Sommes-nous proches de la singularité technologique ? Peu probable. Même si l’intelligence artificielle a fait un bond ces dernières années (elle est étudiée depuis des dizaines d’années), nous sommes loin d’en perdre le contrôle. Et pourtant, une partie de l’utilisation de l’intelligence artificielle échappe aux analystes. Eh oui ! Comme tout système, elle est utilisée par des acteurs malveillants essayant d’en tirer profit pécuniairement. Cet article met en exergue quelques-unes des applications de l’intelligence artificielle par des acteurs malveillants et décrit succinctement comment parer à leurs attaques.

Les listes de lecture

9 article(s) - ajoutée le 01/07/2020
Vous désirez apprendre le langage Python, mais ne savez pas trop par où commencer ? Cette liste de lecture vous permettra de faire vos premiers pas en découvrant l'écosystème de Python et en écrivant de petits scripts.
11 article(s) - ajoutée le 01/07/2020
La base de tout programme effectuant une tâche un tant soit peu complexe est un algorithme, une méthode permettant de manipuler des données pour obtenir un résultat attendu. Dans cette liste, vous pourrez découvrir quelques spécimens d'algorithmes.
10 article(s) - ajoutée le 01/07/2020
À quoi bon se targuer de posséder des pétaoctets de données si l'on est incapable d'analyser ces dernières ? Cette liste vous aidera à "faire parler" vos données.
Voir les 64 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous