1. Les méthodes magiques
1.1. Quesako?
Les méthodes magiques constituent une fonctionnalité avancée du langage PHP. Elles permettent de définir sur une classe des méthodes qui sont appelées automatiquement et implicitement par PHP lorsque certains événements se produisent sur une instance de cette classe. La plus connue et la plus utilisée est sans doute __construct(), appelée constructeur de la classe et qui est déclenchée lors de la construction d'un nouvel objet. Vient ensuite __destruct(), appelée lors de la destruction d'une instance. Il en existe appelées lors de la lecture ou de l'écriture d'une variable inaccessible, lors de l'appel de méthodes, lors de la conversion de l'objet en chaîne de caractères... Il y en a toute une petite série, que vous pourrez découvrir sur la page du manuel officiel de PHP qui leur est consacrée (http://php.net/manual/fr/language.oop5.magic.php). Leur utilisation peut affecter considérablement le comportement habituel de PHP, ce qui peut poser problème.
1.2. Le contre
Cet article étant tout entier une explication de l'aspect positif de l'utilisation des méthodes magiques, il nous faut tout de même faire un point sur leurs côtés négatifs. Ainsi, c'est en toute lucidité que vous les utiliserez (ou pas).
La principale critique qui peut être faite à l'utilisation des méthodes magiques est qu'une fois qu'elles sont en place, il est facile de perdre conscience de leur action, en particulier lorsqu'il y a un changement de développeur sur un projet. En effet, la magie se glissant à des moments où PHP a un comportement attendu relativement simple, la lecture de la valeur d'une propriété par exemple, on peut complètement ignorer que PHP va se montrer bien plus actif.
print $lukeSkywalker->filsde;
Comment savoir en lisant cette ligne de code que quelque chose de bien plus fort peut se dérouler ? Pourtant, en détaillant un peu le contexte, on peut se rendre compte que la moindre tentative de lecture ou d'écriture sur la propriété filsde va déclencher l'appel à la méthode prevenirPapa...
class Jedi {
function __get($name) {
if ($name=='filsde') $this->prevenirPapa();
}
function __set($name,$value) {
if ($name=='filsde') {
$this->prevenirPapa();
$this->filsde = $value;
}
}
class Skywalker extends Jedi {}
$darkVador = new Skywalker();
$lukeSkywalker = new Skywalker();
$lukeSkywalker->filsde = $darkVador;
Ici, pour rendre les choses compréhensibles, nous avons isolé le code pertinent pour notre exemple. Mais imaginez maintenant que votre code soit réparti entre une dizaine de classes dont votre objet final va hériter, il vous sera beaucoup plus difficile de repérer une méthode magique dans la dizaine de fichiers de classes et les quelques milliers de lignes de codes qu'ils vont peut-être contenir, surtout si vous n'en êtes pas l'auteur et que vous n'avez pas conscience de leur présence a priori. Et si un bug s'y glisse, vous risquez d'avoir les plus grandes difficultés à comprendre le comportement de PHP.
À cela, une réponse : ces méthodes ne sont pas appelées « magiques » pour rien. Tout le monde ne peut pas se prétendre Gandalf, c'est évident. Mais cela ne veut pas dire qu'il faut interdire à Gandalf d'être lui-même, non? Surtout si on peut en attendre un bénéfice... Retenons-en qu'il faut user de la magie avec sagesse et parcimonie.
2. Notre exemple : les propriétés autodéterminées
2.1. Pourquoi ?
Dans la pratique quotidienne de la programmation objet, le développeur tend à séparer de manière de plus en plus précise tous les traitements d'une classe dans des méthodes séparées. Celles-ci se multiplient d'autant plus que le développeur est expérimenté et qu'il sait distinguer plus de blocs de traitements pertinents. Il parvient ainsi à la fois à anticiper la factorisation du code et à lui donner une plus grande souplesse, permettant de réorganiser les traitements plus facilement lorsqu'un nouvel usage de la classe apparaît. Tout l'intérêt de la programmation objet est là.
Ce faisant, il va découvrir que certaines de ces méthodes peuvent être appelées plusieurs fois au cours de la vie de l'objet, pour fournir un résultat qui sera toujours le même. Il suffit qu'un nouvel usage de la classe implique l'utilisation de deux autres méthodes qui font toutes les deux appel à une troisième; résultat d'une factorisation. Si le traitement de cette troisième méthode est lourd, comme une requête SQL sur un serveur distant, par exemple, il s'en suit nécessairement une dégradation importante des performances de l'objet. Il peut y avoir bien d'autres cas similaires qui se produisent dans le même objet. Lorsqu'il en a conscience, le programmeur prend la décision d'enregistrer le résultat de la troisième méthode dans une propriété de l'objet. Il ne lui reste alors qu'à tester si la propriété est définie pour savoir s'il doit exécuter le traitement. Il peut le faire avant l'appel de la méthode, et il devra essayer d'y penser à chaque fois ; ou alors, il peut le faire dans la méthode elle-même et l'oublier. Il créé ainsi une sorte de cache interne à l'objet.
À l'usage, il se peut même qu'il implémente ce cache pour chaque propriété dont il sait que la valeur ne variera plus durant la vie de l'objet, sans même se poser la question de savoir si cette méthode va être utilisée plusieurs fois. Une autre pratique qui tend à dégrader les performances consiste à appeler ces méthodes juste pour être sûr que ces propriétés ont été définies, n'étant plus certain du contexte d'utilisation dans lequel on se trouve, ou même de celui où l'on pourrait se trouver... Là, le programmeur commence à se demander s'il sait vraiment ce qu'il fait.
Une solution peut être de définir ces propriétés une fois pour toutes dans le constructeur de la classe, mais cette solution ne conviendra pas dans tous les cas – ce n'est pas parce que la propriété est définie une seule fois qu'elle peut l'être dès la création de l'objet ; et puis cela implique d'exécuter à chaque instance de la classe le traitement permettant de déterminer ces propriétés, alors qu'il se pourrait très bien que ces traitements soient inutiles.
C'est le moment pour le programmeur de devenir magicien.
2.2. Comment ?
#/usr/bin/php
<?php
class withAutocalc {
protected $ArrayOfAutocalcProperties;
function __get($name) {
if (!isset($this->$name)) {
$method = 'get'.$name.'Autocalc';
if (method_exists($this,$method)) $this->$method();
return $this->$name;
}
}
function getArrayOfAutocalcPropertiesAutocalc() {
$methods = get_class_methods($this);
$pattern = '/^get(?P<prop>\w+)Autocalc$/';
foreach ($methods as $method) {
preg_match($pattern,$method,$matches);
if (count($matches)) $array[] = $matches['prop'];
}
$this->ArrayOfAutocalcProperties = $array;
function getTestVarAutocalc() {
return $this->TestVar = "Abracadabra";
}
}
$test = new withAutocalc();
var_dump($test->ArrayOfAutocalcProperties);
print $test->TestVar."\n";
Si vous exécutez ce code, il affichera:
array(2) {
[0]=>
string(25) "ArrayOfAutocalcProperties"
[1]=>
string(7) "TestVar"
}
Abracadabra
Qu'avons-nous fait ? Que s'est-il passé ici ?
Nous avons défini une nouvelle classe withAutocalc, avec deux méthodes __get et getArrayOfAutocalcProperties. Celle qui nous intéresse prioritairement est la première : en effet, si vous essayez d'accéder en lecture à une propriété dite « inaccessible » et que la méthode __get est définie sur l'objet, alors celle-ci est appelée, avec en paramètre le nom de la propriété à laquelle vous essayez d'accéder.
Qu'est-ce qu'une propriété inaccessible ? Il y a plusieurs cas : tout d'abord, une propriété non encore définie ; puis le cas d'une propriété protected à laquelle vous essayez d'accéder depuis l'extérieur de l'objet ; enfin la propriété private à laquelle vous essayez d'accéder depuis une classe fille ou depuis l'extérieur de l'objet.
Que fait notre méthode __get()? En premier lieu, elle teste si la propriété est déjà définie. Si elle l'est, alors il s'agit d'une propriété private ou protected inaccessible, mais définie. Dans ce cas, notre méthode n'a rien à faire, puisque nous ne voulons l'utiliser que pour définir automatiquement des propriétés indéfinies. Si la propriété n'est pas définie, alors notre méthode va vérifier s'il existe une autre méthode de l'objet qui permettrait de définir cette propriété. Pour cela, __get() s'appuie sur la convention de nommage que voici : pour une propriété X, s'il existe une méthode pour la définir automatiquement, celle-ci doit avoir pour nom getXAutocalc. Si __get() découvre qu'il existe une telle méthode, celle-ci est appelée. Si elle retourne un résultat, celui-ci est retourné.
2.3. Effets indésirables inattendus
2.3.1. Sac percé
Si avez la (mauvaise) habitude d'utiliser un objet indéfini comme sac à variables et que vous essayez d'appliquer notre nouvelle méthode, vous aurez la mauvaise surprise de constater que votre sac est percé...
Par exemple, ajoutons une méthode à notre classe withAutocalc:
class testSacAVar extends withAutocalc {
function test() {
$this->sac->mapropriete = 'test';
var_dump($this->sac->mapropriete);
}
}
$test = new testSacAVar();
$test->test();
En toute logique, le résultat attendu est :
string(4) "test"
Pourtant, on obtient :
NULL
En fait, PHP ne parvient pas à créer un objet standard lorsqu'on lui affecte une variable si la méthode __get est présente... Il faut légèrement modifier le code comme suit :
class testSacAVar extends withAutocalc {
function test() {
$this->sac = new stdClass();
$this->sac->mapropriete = 'test';
var_dump($this->sac->mapropriete);
}
}
$test = new testSacAVar();
$test->test();
2.3.2. Propriétés private lisibles et redéterminables depuis l'extérieur
Dans notre exemple, nous avons pris soin de déclarer la propriété ArrayOfAutocalcProperties comme étant privée. Pourtant, nous sommes parvenus à provoquer sa définition depuis l'extérieur de l'objet et à en lire la valeur. À bien y réfléchir, en l'état actuel de notre code, il serait même possible de provoquer plusieurs fois la redéfinition de la propriété depuis l'extérieur de l'objet. Il est aisé de s'en prévenir : on peut vérifier avant l'appel à la méthode de calcul si la variable ne serait pas déjà définie. Toutefois, il n'y a pas de moyen d'empêcher que la propriété devienne au moins lisible depuis l'extérieur. La chose en soi a peu de conséquences, mais tout de même, on évitera de s'appuyer sur cette méthode pour des classes prenant en charge des éléments de la sécurité d'un site ou d'une application.
2.4. Un bilan globalement positif
Nous ne présenterons pas ici de script écrit selon les techniques habituelles de cache, car pour bien montrer les avantages de la nôtre, il nous faudrait produire beaucoup de code pour seulement en faire ressortir la redondance, ce qui est assez peu intéressant. Sachez simplement que dans le script qui m'a mené à implémenter cette méthode et qui utilise cette technique de façon intensive, j'ai pu réduire mes lignes de code de 30% et celles qui restent sont bien plus lisibles et compréhensibles, presque élémentaires. Les quelques tests que j'ai effectués sur ce même script pour effectuer des redimensionnements d'images en série, m'ont montré que j'avais obtenu un gain de performances de l'ordre de 1%, ce qui n'est pas négligeable pour un traitement identique suivant un algorithme identique – hors le fonctionnement du cache et s'appuyant, comme il est classique de le faire en PHP, massivement sur une bibliothèque externe (ImageMagick en l'occurrence). Enfin, étant donné la simplicité du nouveau code, je pense que la généralisation des variables autodéterminées dans mon travail devrait me permettre également de gagner en productivité. Difficile de dire combien aujourd'hui.
Pour conclure avec une bonne nouvelle, en relisant ce script, je me suis aperçu que la conception en parait moins procédurale et que l'objet devient capable de déterminer lui-même le parcours de méthodes de définition de variables qui lui est nécessaire pour produire le résultat attendu, ne définissant ainsi que le strict minimum nécessaire pour le traitement. L'algorithme s'optimise ainsi sans effort.
class optimum extends withAutocalc {
function getApropAutocalc() {
return 'A';
}
function getBpropAutocalc() {
return $this->A.'B';
}
function getCpropAutocalc() {
return $this->B.'C';
}
}
$test = new optimum();
print $test->Bprop;
Ce code affichera :
AB
Dans cet exemple, la lecture de Bprop provoque sa détermination, qui entraîne la détermination de Aprop. Cprop n'est jamais déterminée, car cela est inutile et non demandé. Il n'y a même nulle part de décision du programmeur pour y parvenir, ni de recherche d'optimisation ou du meilleur parcours possible, il n'a pas même à se poser la question.