Entries for tag php

Thu Jun 03 2010

PHP, Programming

php, traits

8 comments

Un point sur les Traits de PHP

Dernièrement, les Traits ont fait leur entrée dans le trunk de PHP. Je ne vais pas les présenter dans leur intégralité puisque la RFC correspondante dont les premières esquisses ont plus de deux ans, est suffisamment claire et détaillée.

Cependant, le document n'est pas très explicite en ce qui concerne l'utilisation d'interfaces et de traits au sein d'une même classe, ainsi que le statut des variables membres et leur accessibilité dans les méthodes des traits. Nous allons voir ce qu'il en est et les conséquences que cela implique.

Avant de commencer, il est vivement recommandé d'avoir lu la RFC. Juste la première partie qui concerne les traits. La seconde partie présente les Grafts que nous n'aborderons pas ici [1].

L'opération de flattening, aka le lissage

L'ordre dans lequel les traits sont manipulés par php est le suivant:

  • Un fichier source est chargé, et comme par hasard, celui-ci contient des traits.
  • Le parser rencontre une déclaration de trait. Ce dernier est pris en compte comme n'importe quelle classe [2]. C'est à dire qu'une structure zend_class_entry est crée et complétée. Seul un flag spécial précise qu'il s'agit d'un trait.
  • Le même parser rencontre une classe qui utilise un trait via le mot clé use ou un trait qui dépend d'un autre trait (on dit qu'il s'agit d'un trait composite). A la fin de la déclaration de la classe, que l'on appelle une client class ou host class, les opérations s'enchainent ainsi:
    • Les méthodes provenant des traits sont triées selon les directives as et insteadof. Une nouvelle table temporaire est construite accueillant ces méthodes candidates à la future intégration dans la classe. Certaines méthodes peuvent être ignorées s'il y a des conflits. On se chope alors un "Warning: Trait method blah has not been applied, because there are collisions with other trait methods".
    • L'opération de flattening (lissage) commence. Pour chaque méthode candidate qui n'est pas redéfinie (overridden) dans la classe client:
      • Vérification du prototype (la signature) par rapport aux éventuelles méthodes des classes parents. S'il y a des différences, et selon la configuration, c'est là qu'on reçoit un "Strict Standards: Declaration of Machin::truc() should be compatible with..".
      • La méthode est dupliquée (son zend_op_array est littéralement copié comme le fait l'extension runkit) et est ajoutée à la table des méthodes de la classe client. Certaines opérations irréversibles se produisent, notamment le fait que la méthode perd toute notion et tout lien avec son trait d'origine [3].
    • Vérification de la bonne conformance de la classe (maintenant dotée de nouvelles méthodes provenant des traits) par rapport aux méthodes abstraites ainsi qu'aux interfaces sensées être implémentées.
  • Le parser continue son job jusqu'à la fin du fichier.

OK, voila une bonne chose de faite.

Sachant que les méthodes des traits sont injectées pratiquement "les yeux fermés", tout se passe comme si celles-ci étaient définies naturellement dans la classe client. Les accès à $this se font exactement de la même manière, rien ne change.

On peut dès lors s'interroger sur certains idiomes qui ne devraient pas tarder à fleurir sur les coins de tables en cette période pré-estivale. En effet, la nature stateless des traits tels qu'implémentés à l'heure actuelle dans php n'est pas sans conséquence quant à l'organisation du code.

L'état des traits

En fait, ils n'en ont pas. On dit qu'ils sont stateless. C'est un choix délibéré et argumenté dans les documents académiques du Software Composition Group (SCG) sur lesquels l'implémentation de php est basée.

Les traits sont uniquement fonctionnels. Ils ne sont pas liés à un quelconque état qui leur serait propre, comme cela peut être le cas dans de l'héritage multiple. Par état, il faut comprendre variables membres. Seule la classe client peut en fournir.

Or, il y a fort à parier que des traits ne manipulant aucun état se feront rares.

Donc, que faire ?

Je vais mettre de coté l'utilisation de variables globales, statiques ou de Singletons. On veut juste faire en sorte que les traits puissent utiliser les variables membres de la classe client. Il n'y a pas 50 possibilités, il y en a juste 2:

  • Accès direct aux variables via $this->
  • Recours à des getters/setters fournis par la classe client

Quoique l'on fasse, ou que je sois, rien ne t'efface, je pense à toi, le trait est dépendant de sa classe client. Accéder directement à une variable avec $this->var peut (pourrait, pourra, pouvait, a pu?) sembler briser l'encapsulation, et nous reviendrons sur ce point. Utiliser des getters et setters est la méthode conseillée par le SCG en particulier parce que leur démonstration et implémentation est basée sur le langage Smalltalk et que de toute manière ils ne peuvent faire autrement.

OK, OK. Il est grand temps qu'on balance un code illustratif car toute cette prose commence à devenir lourdingue ;)

trait T1
{
  public function doSomething() {
    $this->var = 42;
    echo $this->var;
  }
}

trait T2
{
  public function doSomethingElseButDoItWell() {
    $this->setVar(43);
    echo $this->getVar();
  }
}

class C
{
  use T1, T2;
  
  private $var;
  
  private function getVar() {
    return $this->var;
  }
  private function setVar($value) {
    $this->var = $value;
  }
}

$c = new C;
$c->doSomething();
$c->doSomethingElseButDoItWell(); // please

var_dump($c);

Cela donne, comme attendu:

4243object(C)#1 (1) { ["var":"C":private]=> int(43) }

Dépendances et requirements

La dépendance classe -> trait est simplement exprimée dans le code via le mot clé use. Celle-ci est vérifiée dès la compilation. La dépendance trait -> classe n'est quant à elle pas exprimée du tout dans le code précédent, en tout cas pas avant l'exécution. T1 dépend de la présence de la propriété $var et T2 dépend des getter et setter getVar() et setVar().

La RFC préconise l'usage de méthodes abstraites au sein des traits pour exprimer et vérifier à la compilation cette dépendance trait -> classe. Le document parle de requirements, ce qui est la même chose.

Réécrivons T2 en le bardant de méthodes abstraites afin de se conformer aux bonnes pratiques au paragraphe du dessus:

trait T2
{
  public function doSomethingElseButDoItWell() {
    $this->setVar(43);
    echo $this->getVar();
  }
  
  /*private*/ abstract function getVar(); 
  /*private*/ abstract function setVar($value);
}

Ce frêle bout de code est en fait plus profond que ce que l'on pourrait imaginer à première vue, surtout quand on sait que ça compile et que ça fonctionne.

Premièrement, bien que les getters/setters de la classe client soient de visibilité private, cela ne pose pas de problème de les déclarer public dans le trait. La règle des méthodes abstract dit pourtant que la méthode concrète doit avoir une visibilité identique ou moins restrictive que la déclaration abstraite.

Cela fonctionne parce que l'opération de lissage décrite plus haut ne concerne que les méthodes des traits qui ne sont pas redéfinies dans la classe client. Cette redéfinition ne se fait que sur le nom de la méthode. La signature n'intervient pas du tout. La méthode qui porte le même nom dans la classe va complètement masquer celle du trait. Il est donc tout à fait possible de remplacer setVar($value) dans le trait par ceci:

/*private*/ abstract function setVar($value, array $bonus);

On pourrait s'attendre à un beau "Fatal error: Declaration of C::setVar() must be compatible with that of T2::setVar()" car les signatures sont clairement différentes. Et bien non. La phase de lissage rend possible ce comportement.

Exprimer les requirements d'un trait par l'intermédiaire de méthodes abstraites n'est donc que partiel. Seul le nom de la fonction compte. La signature et les éventuels type hintings sont ignorés.

Mais alors pourquoi ça fonctionne quand même? C'est simple, toutes les méthodes abstraites du trait qui ne sont pas masquées par celles de la classe client se retrouvent dans cette même classe toujours avec le statut abstract. Là php peut détecter la non implémentation de ces méthodes et ainsi s'opère la vérification partielle des dépendances trait -> class.

Deuxièmement (le premièrement est 4 ou 5 paragraphes plus haut;) pourquoi ai-je supprimé la visibilité private pour les méthodes abstraites du trait? Après tout, les getter et setter sont privés dans la classe C.

C'est une bien sage question.

La réponse est que l'on veut éviter cet effet indésirable: "Fatal error: Abstract function T2::getVar() cannot be declared private". C'est une conséquence du fait que les traits sont traités (c'est le cas de le dire) par php comme de simples classes lors de la compilation. Ce point fut brièvement abordé plus haut ici. Or, une méthode abstraite ne peut être déclarée private car php considère que celle-ci n'est pas accessible depuis des classes dérivées et qu'il est donc impossible d'y apporter une implémentation. Quelque part, ça a du sens...

De l'existentialisme

Jusqu'ici, nos getters et setters font leur job. On n'en demande pas plus. Pourtant, un sérieux problème se profile à l'horizon.

C::getVar() retourne une copie de la variable membre $var. Sa valeur n'est pas systématiquement dupliquée, php faisant du copy-on-write, mais nous restons dans une sémantique de copie. Toute modification opérée sur ce que retourne getVar() n'aura pas de répercussion sur la véritable variable membre. Cela peut être tout à fait justifié et suffisant.

Cependant, prenons un peu de recul. Nous avons affaire à un getter privé dont l'appel se fait d'une méthode pleinement intégrée à la classe et qui pourrait tout à fait être privée elle aussi. Nous évoluons dans un no man's land à la frontière de l'encapsulation, l'indirection et l'abstraction. Le fameux triptyque des mots en ion.

Le trait pourrait avoir besoins de manipuler autre chose qu'un simple scalaire, par exemple un array. Va-t-il falloir définir toujours plus de setters pour toujours plus d'opérations telles que l'ajout, l'insertion ou le retrait d'éléments? Faut-il gonfler la classe client en méthodes annexes à faible valeur ajoutée, annulant au passage les bénéfices en copy'n'paste sensés provenir de l'utilisation même du trait?

Une solution à mi-chemin entre rien du tout, c'est à dire accéder directement à la variable membre via $this->var, et une belle série de getters/setters serait de recourir à un simple accesseur retournant une référence sur la variable. Coté trait, l'affectation classique = ou par référence =& permet de controller la nature de la liaison suivant l'opération que l'on désire effectuer, ce qui laisse à php la possibilité de faire son job routinier de passage (retour) par copie.

Il ne reste plus qu'à dégainer les variables temporaires. Oui, car comme chacun sait, php est très frileux quand il s'agit de déréférencer automatiquement les retours de fonctions. Vous aurez beau bidouiller en ajoutant des parenthèses ou des accolades, seule la dernière ligne n'est pas une erreur dans les constructions suivantes:

$this->func()[]
$this->func()()
++$this->func()
$this->func() = 
$this->func()->chain()

C'est navrant, et c'est la syntaxe qui n'est pas bonne, le problème détecté étant un parse error dans la plupart des cas. Pour contourner ce problème, il faudra d'abord récupérer le retour de la fonction dans une variable puis travailler à partir de celle-ci.

Traits et Interfaces

Nous allons clore cet article par un exemple qui réunit tout ce qui a été vu: faire endosser à un trait l'implémentation d'une interface héritée de la classe client. Cette construction est à mon sens potentiellement utile pour les auteurs de libraires et frameworks.

Une telle classe client peut être utilisée dans un contexte polymorphique ou de type hinting sans reposer pour autant sur une hiérarchie parfois bancale de classes abstraites, ni sur la présence d'adaptateurs spécifiques et verbeux dans le cadre d'une composition ou aggrégation.

On choisit d'implémenter ArrayAccess qui est automatiquement disponible dans php car fournie par la SPL.

/*
interface ArrayAccess
{
  public function offsetGet($offset);
  public function offsetSet($offset, $value);
  public function offsetExists($offset);
  public function offsetUnset($offset);
} */

trait ArrayAccessImpl
{
  abstract function &data();
  
  public function offsetGet($offset) {
    $d = $this->data();
    return $d[$offset];
  }
  
  public function offsetSet($offset, $value) {
    $d = &$this->data();
    $offset === null ? $d[] = $value : $d[$offset] = $value;
  }
  
  public function offsetExists($offset) {
    return array_key_exists($offset, $this->data());
  }
  
  public function offsetUnset($offset) {
    $d = &$this->data();
    unset($d[$offset]);
  }
}

class MyClass implements ArrayAccess
                     //, Iterator // laissé en exercice
{
  private $data = array(
    'a' => 3.14,
    'b' => null,
    'c' => true,
  );
  
  use ArrayAccessImpl;
  
  private function &data() {
    return $this->data;
  }
}

$c = new MyClass;

var_dump(isset($c['b']));
$c[] = $c['a'];
unset($c['c']);
var_dump($c);

Note: l'utilité plus que douteuse de MyClass n'est pas fortuite, il s'agit juste d'un code d'exemple (ma spécialité;)

Conclusions

Comme d'hab, RAS?

Traits're in da trunk, mais cela ne veut pas dire pour autant qu'ils seront dans le futur php, ni sous leur forme actuelle. Cependant, vu l'avancement du projet, le contraire serait étonnant.

Si tout se passe comme prévu, alors j'ai hâte de voir comment ils seront employés à l'avenir. Il s'agit d'un réel nouveau paradigme pour composer du code OOP. Par exemple, pour valider la pertinence des traits, le SCG a réimplémenté toute la hiérarchie des Collections de Smalltalk.

Je salue mageekguy pour son billet Et si on tirait des traits ? qui est à l'origine de mon soudain regain d'intérêt.

 

[1] Le statut des grafts est bien plus hypothétique puisqu'aucune implémentation n'existe à l'heure actuelle.

[2] Un trait peut déclarer des variables membres ou dériver d'une classe. Cela n'est pas interdit (pour l'instant?). Il ne peut cependant pas implémenter une interface. Cela résulte en une fatal error.

[3] Les éventuelles variables membres déclarées dans le trait auquel la méthode appartenait sont purement et simplement ignorées.

Thu Feb 25 2010

PHP

hiphop, php

9 comments

HipHop, 32-bit et github

HipHop sur github, ca se présente plutôt pas mal. Une petite communauté de forkers travaille à rendre la chose compilable et surtout utilisable par le plus grand nombre (pour le bien de l'humanité?). Scott MacVicar, un des responsables du projet et commiter en chef sur github, semble bien disposé à appliquer les patches et suggestions qui gravitent autour du truc. Ca le fait.

A propos de fork, je viens de mettre le mien à jour. Sans vouloir jouer les caïds (loin de moi cette idée saugrenue ;) je pense que c'est celui qu'il faut puller si vous souhaitez installer le matos sur une machine 32-bit. Car, je le répète, HipHop est 64-bit only, au moins pour un certain temps. De plus, je n'ai rien ajouté ou modifié à l'artillerie de CMake par rapport au code de facebook pour que le wiki officiel concernant la partie installation reste pertinent.

Un conseil: Installer les dépendances à la main, c'est à dire en récupérant les tarballs sur leurs sites respectifs. Ensuite, procédez aux installations avec un --prefix=$HOME/hiphopdep (ou un truc du genre). Le principe est de placer ces dépendances dans un répertoire isolé et ne rentrant pas en conflit avec quelque système de packages que ce soit. De toute manière il vous faut patcher libcurl et libevent, on n'y coupera pas. Une fois que tout est soigneusement installé, bien penser à mettre $HOME/hiphopdep/bin dans votre PATH (si vous avez installé des programmes tels que re2c ou flex) et faire un export CMAKE_PREFIX_PATH=$HOME/hiphopdep avant de lancer CMake. Normalement, ça devrait bien se passer.

Maintenant, à part compiler le bordel, compiler du php et lancer des servers ou des daemons, je n'ai pas encore eu le temps de vraiment me pencher sur ce que fait HipHop et comment ça marche, autre que les généralités librement disponibles.

Avec un peu de chance, je devrai avoir du temps dans les prochains jours pour m'adonner à l'un de mes hobbies, à savoir glandouiller devant des lignes de codes et attendre qu'une idée ou une illumination surgisse comme par enchantement. Elle est pas belle la vie ? ;)

Sun Feb 21 2010

PHP

hiphop, php, slackware

0 comment

Compiler HipHop sur du 32-bit

Je rentre du taff (oui je bossais ce samedi), et que vois-je, les sources de HipHop sont dispo sur github! C'est cool. J'ai donc entrepris de tester la bête mais ce ne fut pas sans embuches...

A la base ma machine de jeux est un Macbook Pro 13" avec OSX 10.6. Ayant parcouru le wiki ces jours précédents, je savais que je n'allais pas tenter d'installer le bordel sur OSX à cause de libcap. En effet, imposible d'installer cette lib qui est trop linux-centric. Par contre, sous le coude j'ai un server qui tourne sous Slackware en 32-bit. Un des avantages de Slackware est que si ça merde, alors c'est de notre faute. Cette distrib est très archaïque et c'est la raison pour laquelle je l'ai choisi: j'aime bien quand ça merde ;)

Et ça a merdé quand même pas mal.

Pour les dépendances, pas de problèmes. Bien penser à patcher libcurl et libevent avec les diffs fournis dans le source tree de HipHop. Le plus gros problème a consisté à trouver le lien pour choper les sources de Intel® Threading Building Blocks (Intel® TBB). Un sacré paquet de ®, désolé. Et bien le lien le voila. Prendre le .src.tgz.

Ensuite, il suffit de suivre le wiki, puis on arrive au moment où on lance cmake. Si ça commence à déconner chez vous à partir de là, ne pas hésiter à modifier les fichiers concernés dans le répertoire CMake pour que le système puisse correctement trouver vos librairies. Au préalable, bien penser à définir CMAKE_PREFIX_PATH comme stipulé dans le wiki.

Ensuite, make. Là bison 2.4 générait des erreurs pour cause de grammaires mal formées. Il y a plusieurs oublis de ";" dans divers fichiers y. Les lignes concernées sont indiquées dans les messages.

Le gros du travail ensuite a été de fixer de manière incrémentale (make, erreur, fix, make, ok, erreur, fix, make...) toutes les parties du code qui concerne le 64-bit parce que, en fait, HipHop est 64-bit only à l'heure actuelle. Franchement j'ai passé 3 heures à faire ça. Relancer make à chaque fois. Ca m'a tellement gonflé que de une, mes corrections sont plus qu'hasardeuses, et de deux, je n'ai pas pris soin de glisser des #ifdef/#endif pour conserver le code original et pouvoir jouer avec des defines. Au début, trop optimiste probablement, j'ai pensé que seul util/hash.h était concerné. Une grossière erreur ;) Pour info, une bonne partie des fixes consiste à ajouter le suffix ll aux literals, ainsi que de transformer des ssize_t en long int [1].

Finalement, et là je peux vous dire que je balisais, le linker a paqueté tout ça sans encombre. J'en avais plein le cul. J'ai pushé sur github mes modifs temporaires et d'ailleurs au moment où je rédige ce billet, d'autres forks pour 32-bit sont en cours d'élaborations.

Un echo "hello world, bastard\n" vaut mieux qu'un long discours:

Hello world from HipHop

[1]: J'ai repris mes modifs et wrappé les parties concernées entre #ifdef/#endif. C'est censé compiler indifféremment sur 32 ou 64-bit. Mon fork a été mis à jour.

Wed Feb 10 2010

PHP

extension, php, xhp

0 comment

Compiler l'extension XHP de facebook

Jamais autant parlé de facebook que depuis une semaine. Commence à devenir relou!

XHP est une extension développée par facebook qui augmente la syntaxe de php en permettant d'écrire des fragments html au sein même des scripts. Ca a la couleur de E4X mais le but premier de XHP est de garantir un document correctement formé et respectant un schema défini. On a donc une sorte de mix entre E4X et RELAX NG. Le but est louable.

Ce court billet n'a pas vocation à présenter l'extension. Je vous invite à lire la présentation faite par son auteur ainsi que les pages wiki sur github (là où réside le code source, nous y reviendrons). A voir aussi, un billet de Rasmus Lerdorf qui a effectué des benchmarks. Je vous laisse matter les conclusions.

Pour compiler l'extension, il faut l'artillerie usuelle nécessaire à la modification de la grammaire php: flex, Bison et re2c. RAS pour ça. Le problème c'est que ça ne compilait pas chez moi. Mon php de test est un CLI 5.3.1 en mode debug ainsi qu'en thread safety forcée (--enable-maintainer-zts). Il y a un oubli de TSRMLS_CC dans ext.cpp mais également un problème d'initialisation de globals dans le MINIT. Un fork patché est disponible sur github ici en attendant que le repo officiel prenne en compte les patches.

Est-ce que ça vaut le coup d'utiliser cette extension? C'est une bonne question simon! Avec HipHop peut être bien? En tout cas Marcel Laverdet, l'auteur de l'extension, précise que facebook lite tourne sous HipHop et XHP. Pour facebook lite, cherchez pas, suis pas inscrit.

older »