Par Pascal MARTIN le mardi 20 novembre 2012 07:00 1 commentaire

L’extension PHP ext/mysql est celle qui est historiquement utilisée pour accéder, depuis PHP, à une base de données MySQL : c’est elle qui fournit l’ensemble des fonctions mysql_*() (msqyl_connect(), mysql_query(), mysql_real_escape_string(), …)

ext/mysql, l’extension “historique”

ext/mysql a été introduite dès PHP 2.0 : historiquement parlant, c’est une des plus anciennes extensions fournies par PHP, elle a pour ainsi dire toujours été considérée comme “étant là”, et en ne généralisant probablement pas tant que ça, nous l’avons tous utilisé un jour ou l’autre.

Une extension manquant de fonctionnalités, et plus tellement maintenue

Cela dit, cela fait maintenant plusieurs années que ext/mysql n’est plus en développement actif : en résumant quelque peu, cette extension ne reçoit que des correctifs de sécurité (et il n’y en n’a pas eu tellement ces dernières années), et ne bénéficie plus d’évolution, que ce soit ajout de nouvelles fonctionnalités ou optimisations.


En fait, l’API exportée par ext/mysql ne supporte pas les nouveautés qui ont été introduites par MySQL 4.1 (MySQL 4.1.0 a été publiée en 2003, et le gros des versions releases date s’étale entre 2004 et 2006, avec quelques corrections de bugs jusque 2008) et versions supérieures.

En particulier, ext/mysql ne fournit pas de support pour :

  • Les requêtes préparées (prepared statements), que ce soit côté client ou côté serveur,
  • Les procédures stockées,
  • L’exécution de requêtes multiples,
  • Les transactions (il est tout de même possible d’invoquer les instructions SQL begin/commit/rollback comme des requêtes SQL, cela dit),
  • Les requêtes non-bloquantes.

Autrement dit, en 2012, ext/mysql, ce n’est plus vraiment la pointe du progrès !


En conséquence, l’introduction de la section MySQL du manuel de PHP dit fort clairement (je cite) :

Cette extension n’est pas recommandé pour écrire du nouveau code.
A la place, soit l’extension mysqli ou PDO_MySQL devrait être utilisée.

D’ailleurs, pour compléter, suite à la discussion ext/mysql updates (~deprecated) commencée en avril 2012 sur la mailing-list de l’équipe de documentation, et au ticket #62213: Soft-deprecation notices for ext/mysql, le même type d’avertissement se retrouve sur la documentation de chaque fonction de l’extension ext/mysql, avec des liens vers les alternatives proposées par ext/mysqli et ext/pdo_mysql :

La documentation de mysql_connect() renvoit vers mysqli_connect() et PDO::__construct()

Ou, pour ceux d’entre vous qui préférent la version anglaise de la documentation1 :

La documentation de mysql_connect() renvoit vers mysqli_connect() et PDO::__construct()

Une extension qui pourrait “prochainement” disparaitre ?

ext/mysql étant plutôt “obsolète”, du moins en termes de fonctionnalités et de maintenance, le sujet d’une éventuelle suppression de cette extension revient régulièrement sur le tapis.

Juin 2010 : une idée levée

Par exemple, en juin 2010, au moment où le déplacement de ext/sqlite2 du trunk de PHP vers PECL était discuté, des voix s’élevaient déjà pour demander à ce qu’il en soit de même pour ext/mysql ; par exemple, vous pouvez jeter un coup d’oeil à ces deux mails sur internals@ :

Le sujet ne va pas bien plus loin ; une idée levée, mais rapidement mise de côté, en somme.

Juillet 2011 : le sujet revient ; et fait parler de lui un peu plus largement

Plus proche de nous, en juillet 2011, le sujet est remis sur le devant de la scène :

Cette fois, le sujet attire plus l’attention — mais, une seconde fois, aucune action concrète ne suit.

Avril 2012 : première action concrète : la documentation est mise à jour

J’en parlais un peu plus haut : en avril 2012, nouvelle discussion, sur la mailing-list de l’équipe gérant la documentation de PHP : ext/mysql updates (~deprecated).

Cette fois-ci, cette discussion est suivie d’un effet : la documentation de chaque fonction de ext/mysql explique que cette extension ne devrait plus être utilisée, et renvoit vers les équivalents proposés par ext/mysqli et ext/pdo_mysql.

Novembre 2012 : “RFC: ext/mysql deprecation”

Enfin, ces derniers jours (la discussion a commencé le 12 novembre), une fois de plus, le sujet a été relancé.

Cette fois-ci, les choses sont faites de manière un peu plus formelle, puisque Adam Harvey a choisi de suivre le processus mis en place depuis un peu plus d’un an, en rédigeant une RFC, et en postant en parallèle sur la mailing-list des développeurs de PHP pour encourager à la discussion.

Cette fois-ci, la discussion semble bien lancée, puisqu’elle en est à plus de 120 mails en une semaine ; aucune décision n’a encore été prise, mais, dans les grandes lignes, on peut être amené à penser que :

  • Un jour, l’extension ext/mysql va disparaitre du coeur de PHP,
  • Peut-être pour être migrée vers PECL — ce qui est une suite logique pour une extension qui sort du coeur de PHP — où elle trouvera peut-être des mainteneurs s’il s’avère qu’elle est encore utile dans certains cas.
  • Un jour, probablement dans la version précédent cette suppression, l’extension ext/mysql sera officiellement marquée comme déprécié2 — ce qui signifie que l’utiliser devrait lever des avertissements de type E_DEPRECATED
  • Et enfin, il est nécessaire de communiquer3 sur le fait que ext/mysql n’est plus la solution de choix lorsque l’on veut interroger un serveur MySQL : ext/mysqli et ext/pdo_mysql sont à privilégier.

Dans l’ensemble, les différents intervenants ont l’air d’accord sur ces points ; les points de discussion restant portent surtout sur les versions de PHP pour lesquelles ces changements devraient être appliqués :

  • Est-ce que, dès maintenant, la documentation doit insister plus lourdement sur le fait que ext/mysql disparaitra un jour ?
  • Est-ce que les fonctions mysql_*() doivent lever des avertissements E_DEPRECATED dès PHP 5.5 ? Ou à partir de PHP 5.6 ?
  • Dans quel cas est-ce que des avertissements E_DEPRECATED devront être levés ? Lors du chargement de l’extension ext/mysql ? Lors de la connexion à une base de donnée avec mysql_connect() et/ou mysql_pconnect() ? Ou à chaque appel de fonction mysql_*() ?
  • Est-ce que ext/mysql doit être déplacée vers PECL (et donc, ne plus être fournie avec PHP) dès PHP 5.6 ? Ou à partir de la version suivante ?

Bref, comme je disais : aucune décision n’a encore été prise ; mais il semblerait que, cette fois-ci, il y ait une véritable volonté de faire évoluer les choses !

Quoi qu’il en soit, il est plus que temps de cesser d’utiliser ext/mysql, en particulier pour les nouveaux projets — et de passer à MySQLi ou à PDO !

Voici quelques liens correspondant :

L’extension ext/mysqli

L’extension ext/mysqli — comme MySQL improved — a fait son apparition avec PHP 5.0 ; elle a été développée pour tirer parti des nouvelles fonctionnalités apportées par MySQL >= 4.1.

Cette extension a un grand nombre d’avantages, et fournit plusieurs améliorations, par rapport à ext/mysql :

  • Support des requêtes préparées (prepared statements),
  • Support des transactions (sans avoir à jouer “à la main” des requêtes begin/commit/rollback),
  • Deux interfaces : procédurale, et orientée objet,
  • Support des requêtes multiples (plusieurs requêtes SQL en un seul envoi au serveur).

Notez que si vous utilisez le pilote natif MysQL mysqlnd (ajouté en PHP 5.3, et activé par défaut à partir de PHP 5.4) comme bibliothèque de connexion à MySQL (au lieu de libmysql), en dessous de l’extension ext/mysqli, vous bénéficierez aussi :

  • De la possibilité de jouer des requêtes en arrière-plan, en jouant avec mysqli_query() et l’option MYSQLI_ASYNC :
    • A partir de votre code PHP, envoyez une requête longue au serveur MySQL,
    • pendant qu’elle est jouée du côté du serveur MySQL, continuez l’exécution de votre code PHP,
    • et utilisez son résultat plus tard, une fois qu’elle sera terminée.
    • Autrement dit : votre script PHP n’attend plus systématiquement “à ne rien faire” pendant qu’une requête SQL est jouée.
  • Et de plus de statistiques.

En somme, si votre application PHP doit communiquer avec une base de données MySQL, en particulier si vous n’avez pas besoin que votre code soit compatible avec un autre SGBD, il est fortement recommandé d’utiliser l’extension ext/mysqli.

Et PDO, alors ?

L’extension PDO, introduite avec PHP 5.1, a comme avantage majeur de proposer une API qui permet d’interroger plusieurs moteurs de bases de données différents, sans avoir à modifier votre code PHP — pour peu que vos requêtes SQL se limitent au sous-ensemble de SQL compris par ces différents moteurs.

Il est tout à fait envisageable, en utilisant PDO, d’écrire du code qui fonctionne avec MySQL et PostgreSQL, sans avoir à effectuer de remaniement important de code.

Le contre-coup, directement lié à cette API simple et portable, est que PDO ne supporte pas ou peu les fonctionnalités avancées, spécifiques à chaque moteur de bases de données — par exemple, avec MySQL, PDO ne supporte pas les requêtes en arrière-plan, et ne permet pas d’obtenir autant de statistiques que ext/mysqli.

Autrement dit, si vous n’avez besoin de supporter que MySQL, et/ou si vous voulez profiter des fonctionnalités avancées que PDO ne supporte pas, vous aurez tendance à privilégier l’utilisation de ext/mysqli.

Voir aussi

  • Request for Comments: ext/mysql deprecation
  • La page de manuel Choisir une API
  • La section overview du manuel de MySQLi, qui fournit des informations sur les solutions qui s’offrent à vous quand vous développez une application PHP qui a besoin de s’interfacer avec une base de données MySQL.
  • Johannes Schlüter, membre de l’équipe responsable des connecteurs MySQL chez Oracle sur internals@ il y a quelques jours (je cite) :

    We, from the MySQL Connector Team at Oracle, add new features to mysqli (and eventually PDO) only, though, for quite some time already.
    Users should use mysqli for having access to everything MySQL has to offer.

  • Converting to MySQLi

  • L’avis de @nacin, Lead Developper de Wordpress, sur la RFC proposant de marquer ext/mysql comme dépréciée :

    ...

  • D’ailleurs, même si une partie du texte du ticket est fausse, il y a déjà du travail en cours côté Wordpress : Use PDO for MySQL queries when PDO is available

  • Côté eZ Publish aussi, les développeurs ont tendance à s’éloigner de ext/mysql.



  1. Si la qualité de la documentation française ne vous convient pas, vous pouvez facilement participer à son amélioration ; par exemple, en utilisant l’éditeur en ligne, qui vous permet de modifier les sources de la documentation sans même avoir à les extraire depuis le repository SVN. 

  2. Oui, je sais, “dépréciée” est une mauvaise traduction de “deprecated”, et je devrais écrire “obsolète”… désolé, mauvaise habitude.
    (D’un autre côté, à chaque fois que je lis/entends “expression régulière” au lieu de “expression rationelle”, je m’étrangle ^^) 

  3. D’où cet article, qui a purement pour but de communiquer autour du fait que ext/mysql n’est plus, depuis plusieurs années déjà, “la meilleure façon” de se connecter à un serveur MySQL. 

Par Pascal MARTIN le lundi 19 novembre 2012 07:00

J’ai parlé plusieurs fois ce dernier mois de nouveautés apportées par ce qui est en train de devenir PHP 5.5… Mais la meilleure façon de découvrir celles-ci n’est-elle pas d’expérimenter, de les utiliser depuis vos scripts et applications ?

Avec la sortie de la première version alpha il y a quelques jours, c’est aussi l’occasion de commencer à vérifier le bon fonctionnement de vos applications — et, si besoin est, de rapporter des bugs.

Pour faciliter un peu les choses, voici quelques notes que j’ai pris en installant un des environnements de test que j’ai moi-même utilisé pour expérimenter, et écrire ces quelques articles. Au moment où j’ai commencé à rédiger cet article, la première version alpha n’était pas encore sortie, et je compilais régulièrement la branche master de PHP depuis son repository git ; depuis, j’ai ajouté quelques informations quant à la compilation d’une version packagée, comme la version alpha-1.

Installer une version de développement / test de PHP ?

Le but de cet article est de présenter une façon possible d’installer la version de développement courante de PHP, ou une version de test (comme une release alpha/beta/RC) ; opération que vous pouvez vouloir réaliser sur un environnement de test ou de développement, éventuellement sur une machine virtuelle — et jamais sur un serveur de production1 !

Le gros des développements aujourd’hui, mis à part d’éventuelles corrections de bugs et/ou évolutions mineures sur PHP 5.3 et PHP 5.4, se concentrent sur la branche master du repository git hébergeant les sources de PHP — et, depuis la création de la branche PHP-5.5, ceux destinés à être intégrés à PHP 5.5 arrivent jusqu’à cette branche.

En fonction de la version que vous voulez tester, vous pouvez extraire depuis le repository git la branche de PHP qui vous intéresse :

  • master pour les tous derniers développements ; possiblement même des fonctionnalités qui ne seront pas intégrées à PHP 5.5 et qui pourraient n’arriver qu’avec la version suivante
  • PHP-5.5, PHP-5.4, PHP-5.3, … pour les développements correspondant à une version mineure donnée,
  • Ou même des versions release, avec des branches comme PHP-5.4.9, PHP-5.3.19, …

Tout de même, en particulier si vous vous préparez à tester PHP 5.5 alors que la première version alpha vient tout juste de sortir, rappelez-vous que rien n’est figé dans le marbre tant qu’une version est encore en développement et n’est pas sortie en version stable !.

Obtenir les sources de PHP

A partir du moment où nous souhaitons compiler PHP depuis ses sources, deux options s’offrent à nous :

  • Extraire les sources depuis le repository git les hébergeant,
  • Ou utiliser un package contenant les sources d’une version spécifique de PHP.

Obtenir les sources de PHP depuis git

Cette section correspond au cas où vous voulez compiler et tester les tous derniers développements destinés à une future version de PHP — et pas une version packagée comme une alpha/beta/RC (même si, bien sûr, il est possible de construire une telle version depuis le tag git qui va bien).

Les sources de PHP sont hébergées sur un repository git.

La solution pour installer une version de développement de PHP, non packagée sous forme d’archive téléchargeable, est donc :

  • d’extraire les sources depuis la branche qui vous intéresse — ici, je partirai du principe que nous voulons travailler avec la branche PHP-5.5,
  • de configurer la compilation,
  • et d’effectuer celle-ci,
  • avant de déployer les binaires obtenus,
  • et de configurer quelques éléments.

Pour obtenir les sources de PHP, tout d’abord, il faut récupérer les sources depuis le repository git qui les héberge2 :

git clone https://git.php.net/repository/php-src.git
cd php-src

A titre d’information, vous pouvez lister toutes les branches existantes (vous constaterez en particulier qu’il en existe une pour chaque version du langage) :

git branch -a

Si on regarde avec un outil graphique (j’ai utilisé qgit, ici, mais vous obtiendriez quelque chose d’équivalent avec un autre programme — y compris avec git log en ligne de commandes), nous avons accès à l’historique des commits sur PHP.
Par exemple, ici, j’ai sélectionné le moment où les développements autour des Generators ont été mergés vers la branche master :

Capture d'écran de qgit

La branche PHP-5.5 a été créée il y a quelques jours, et correspond aux développements destinés à (assez probablement, même si certains points peuvent encore évoluer) figurer dans la prochaine version de PHP — celle que nous voulons tester.

Passons donc sur cette branche :

git checkout PHP-5.5

Ici, puisque nous avons extrait les sources brutes depuis le repository de développement, le script configure n’existe pas, et nous allons devoir le créer à l’aide de la commande buildconf :

$ ./buildconf 
buildconf: checking installation...
buildconf: autoconf version 2.68 (ok)
rebuilding aclocal.m4
rebuilding configure
rebuilding main/php_config.h.in

Une fois ceci fait, nous allons pouvoir lancer le script configure — rendez-vous un peu plus bas ;-)

Obtenir les sources d’une version packagée de PHP

Cette section correspond au cas où voulez compiler et tester une version packagée de PHP, comme une alpha/beta/RC ou même une version stable — et pas extraire depuis le repository git de PHP les sources correspondant aux tous derniers développements.

A chaque fois qu’une version de test est taggée, les sources correspondantes sont disponibles sous forme d’une archive .tar.gz ou .tar.bz2 (ou .tar.xz depuis PHP 5.5) sur la page de l’équipe PHP Quality Assurance.

Deux choses à faire :

  • Télécharger une des archives correspondant à la version qui nous intéresse,
  • et décompresser cette archive.

Typiquement, cela peut se résumer aux quelques commandes suivantes (si vous souhaitez tester une autre version que l’alpha1, comme l’alpha2 qui sortira dans quelques jours, vous aurez bien sûr quelques éléments à adapter) :

cd $HOME/temp
wget http://downloads.php.net/dsp/php-5.5.0alpha1.tar.bz2
tar xvf php-5.5.0alpha1.tar.bz2
cd php-5.5.0alpha1

Une fois ceci fait, nous allons pouvoir lancer le script configure — rendez-vous juste ci-dessous ;-)

Compiler et installer PHP depuis les sources que nous venons d’obtenir

La compilation et l’installation de PHP à partir des sources sous Linux se fait, de manière fort classique, en appelant sucessivement configure, make, et make install.

Configure

Maintenant, puisque nous avons obtenu les sources de la version de PHP qui nous intéresse, vous pouvez invoquer le script configure pour lister les options de configuration disponibles (il y en a un petit nombre, permettant pour la plupart d’activer ou non certaines extensions, les informations de débuggages, les différentes SAPI, …) :

$ ./configure --help
`configure' configures this package to adapt to many kinds of systems.

Usage: ./configure [OPTION]... [VAR=VALUE]...

To assign environment variables (e.g., CC, CFLAGS...), specify them as
VAR=VALUE.  See below for descriptions of some of the useful variables.

Defaults for the options are specified in brackets.

[...]

Use these variables to override the choices made by `configure' or to help
it to find libraries and programs with nonstandard names/locations.

Report bugs to the package provider.

Voici un exemple de configure que j’ai tendance à utiliser pour obtenir une version de test de PHP, avec pas mal d’extensions activées (c’est avec cette commande de configuration que j’ai compilé les versions de test, depuis la branche master, utilisées pour ces articles) :

Attention : n’utilisez pas ceci sans réfléchir, pour un environnement de production !

./configure --prefix=$HOME/bin/php-5.5 \
    --with-apxs2=/usr/bin/apxs2 --disable-cgi --disable-ipv6 --with-openssl \
    --with-zlib --enable-bcmath --with-bz2 --enable-calendar --with-curl \
    --enable-ftp --with-gd --with-imap-ssl --enable-intl --enable-mbstring \
    --with-mcrypt --with-mhash --with-mysql=mysqlnd --with-mysqli=mysqlnd \
    --enable-pcntl --with-pdo-mysql=mysqlnd --with-pdo-pgsql --with-pgsql \
    --with-readline --enable-soap --enable-sockets --with-xmlrpc --with-xsl \
    --enable-zip --with-pear 

Notez que j’ai choisi d’installer les binaires créés vers une répertoire non-système, afin de ne pas risquer d’entrer en conflit avec la version de PHP, stable, installée depuis le gestionnaire de paquets de ma distribution — pour une version de test, c’est probablement plus sage ;-)

Si vous obtenez des erreurs lors de l’exécution de la commande de configuration, c’est généralement parce que certaines bibliothèques requises pour la compilation ne sont pas installées sur votre système (et doivent donc l’être, en version de développement).
En général, chercher le nom des bibliothèques concernées via votre outil préféré de gestion de packages et installer les paquets de développement correspondant devrait corriger ce type de problème.

A titre d’illustration et pour essayer de faciliter les choses à ceux d’entre-vous qui voudraient essayer, sous Ubuntu 12.04.1-server, la série de commandes suivantes devrait permettre d’installer le gros des paquets requis pour compiler PHP avec les options reproduites ci-dessus :

sudo apt-get update && apt-get dist-upgrade
sudo apt-get install apache2-mpm-prefork zip unzip make apache2 build-essential \
automake1.9 autoconf manpages-dev debian-keyring apache2-prefork-dev libxml2-dev \
libbz2-dev libcurl4-openssl-dev libpng12-dev libicu-dev libmcrypt-dev \
libxslt1-dev libltdl-dev

Si configure se termine correctement, vous devriez obtenir, à la fin de sa sortie, quelque chose de ce type :

Generating files
configure: creating ./config.status
creating main/internal_functions.c
creating main/internal_functions_cli.c
+--------------------------------------------------------------------+
| License:                                                           |
| This software is subject to the PHP License, available in this     |
| distribution in the file LICENSE.  By continuing this installation |
| process, you are bound by the terms of this license agreement.     |
| If you do not agree with the terms of this license, you must abort |
| the installation process at this point.                            |
+--------------------------------------------------------------------+

Thank you for using PHP.

config.status: creating php5.spec
config.status: creating main/build-defs.h
config.status: creating scripts/phpize
config.status: creating scripts/man1/phpize.1
config.status: creating scripts/php-config
config.status: creating scripts/man1/php-config.1
config.status: creating sapi/cli/php.1
config.status: creating sapi/fpm/php-fpm.conf
config.status: creating sapi/fpm/init.d.php-fpm
config.status: creating sapi/fpm/php-fpm.8
config.status: creating sapi/fpm/status.html
config.status: creating main/php_config.h
config.status: executing default commands

Une fois configure terminé avec succès, il est temps de passer à la compilation en elle-même.

Compilation

Passons maintenant à la compilation en elle-même, en invoquant la commande suivante :

make

En fonction des capacités de votre machine, cette étape peut durer un bon moment3

XKCD: The #1 programmer excuse for legitimately slacking off Crédits : XKCD #303

Si la compilation se termine correctement, vous devriez obtenir, en fin de sortie, quelque chose ressemblant à ceci :

Generating phar.php
Generating phar.phar
PEAR package PHP_Archive not installed: generated phar will require PHP's phar extension be enabled.
pharcommand.inc
directorygraphiterator.inc
clicommand.inc
invertedregexiterator.inc
directorytreeiterator.inc
phar.inc

Build complete.
Don't forget to run 'make test'.

Le moment est donc venu, comme suggéré, de lancer les tests.

Lancement des tests

Lancer les tests se fait, comme indiqué en sortie de make juste au-dessous, en exécutant la commande suivante (là aussi, selon la puissance de votre machine, ça peut durer un bon moment) :

make test

La sortie de cette commande commence par un rappel de la version de PHP utilisée — celle que nous venons juste de compiler :

=====================================================================
PHP         : /home/squale/temp/php-5.5.0alpha1/sapi/cli/php 
PHP_SAPI    : cli
PHP_VERSION : 5.5.0alpha1
ZEND_VERSION: 2.5.0-dev
PHP_OS      : Linux - Linux shark 3.2.0-31-generic #50-Ubuntu SMP Fri Sep 7 16:16:45 UTC 2012 x86_64
INI actual  : /home/squale/temp/php-5.5.0alpha1/tmp-php.ini
More .INIs  :  
CWD         : /home/squale/temp/php-5.5.0alpha1
Extra dirs  : 
VALGRIND    : Not used
=====================================================================

Vient ensuite l’exécution des tests en eux-même :

[...]
PASS Bug #26528 (HTML entities are not being decoded) [ext/xml/tests/bug26528.phpt] 
SKIP Bug #26614 (CDATA sections skipped on line count) [ext/xml/tests/bug26614.phpt] reason: expat test
PASS Bug #26614 (CDATA sections skipped on line count) [ext/xml/tests/bug26614_libxml.phpt]
[...]

Et, finalement, en fonction du nombre de tests en échec ou en succès, un résumé est affiché :

=====================================================================
TEST RESULT SUMMARY
---------------------------------------------------------------------
Exts skipped    :   27
Exts tested     :   51
---------------------------------------------------------------------

Number of tests : 12687             10275
Tests skipped   : 2412 ( 19.0%) --------
Tests warned    :    0 (  0.0%) (  0.0%)
Tests failed    :    2 (  0.0%) (  0.0%)
Expected fail   :   37 (  0.3%) (  0.4%)
Tests passed    : 10236 ( 80.7%) ( 99.6%)
---------------------------------------------------------------------
Time taken      :  525 seconds
=====================================================================

Avec une mise en évidence des éventuels tests en échec :

=====================================================================
FAILED TEST SUMMARY
---------------------------------------------------------------------
PDO MySQL specific class constants [ext/pdo_mysql/tests/pdo_mysql_class_constants.phpt]
show information about extension [sapi/cli/tests/006.phpt]
=====================================================================

Et, plus bas, vous avez la possibilité d’envoyer le rapport d’exécution des tests à la mailing-list QA de PHP.

Installation

Si vous jugez le résultat de l’exécution des tests satisfaisant — puisque vous installez une version de test, sur un environnement de développement ou une machine virtuelle, ignorer quelques échecs n’est probablement pas très génant —, vous pouvez passer à l’installation en elle-même, en lançant la commande suivante :

make install --ignore-errors

Deux points qui peuvent être à noter :

  • Si vous avez utilisé l’option --prefix de configure pour spécifier que les répertoires de PHP devaient être à un endroit où vous avez accès en écriture (sous votre $HOME, typiquement), cette commande n’a pas besoin d’être lancée en administrateur avec sudo,
  • Si cette commande n’est pas lancée en administrateur, l’installation du module PHP pour Apache 2 (option --with-apxs2 de configure) échouera, car elle essaye d’écrire vers /usr/lib/apache2/modules/ ; pour éviter cet échec (entre autres), make install accepte l’option --ignore-errors ; au besoin, nous déploierons manuellement ce module ultérieurement (l’idée étant de ne pas installer ce module, en version de développement non-stable, au niveau système, cet échec est une “bonne chose” !).

Configuration : php.ini

A la racine des sources de PHP, vous pouvez trouver deux exemples de fichier de configuration de PHP : php.ini-development et php.ini-production.

Puisque, ici, nous mettons en place un environnement de test, déployons le premier vers l’endroit attendu par la version de PHP que nous venons d’installer :

cp php.ini-development $HOME/bin/php-5.5/lib/php.ini

Par défaut, php.ini-development correspond assez bien à ce que l’on pourrait espérer sur un environnement de développement / test.
Quelques adaptations peuvent tout de même être nécessaires ; je pense en particulier à la directive date.timezone.

Vérification de l’installation de base

Maintenant que l’installation de PHP (version de développement, ou version alpha/beta/RC) est terminée, vérifions que celle-ci s’est bien passée.

Tout d’abord, en invoquant php en ligne de commande, en utilisant le chemin complet si vous avez installé vers un répertoire qui n’est pas dans le PATH de votre système :

$ $HOME/bin/php-5.5/bin/php --version
PHP 5.5.0alpha1 (cli) (built: Nov 17 2012 13:59:40) 
Copyright (c) 1997-2012 The PHP Group
Zend Engine v2.5.0-dev, Copyright (c) 1998-2012 Zend Technologies

Selon les sources que vous avez compilé, la sortie reproduite ci-dessus peut être différente, indiquant une version comme PHP 5.5.0-dev ou PHP 5.5.0alpha1.

Nous pouvons aussi vérifier que notre fichier php.ini est bien pris en compte :

$ $HOME/bin/php-5.5/bin/php --ini
Configuration File (php.ini) Path: /home/user/bin/php-5.5/lib
Loaded Configuration File:         /home/user/bin/php-5.5/lib/php.ini
Scan for additional .ini files in: (none)
Additional .ini files parsed:      (none)

Ensuite, vous pouvez utiliser le serveur web intégré à PHP (depuis PHP 5.4), pour accéder à vos pages en HTTP ; par exemple, après avoir créé un fichier $HOME/www/phpinfo.php contenant ceci :

<?php 
phpinfo();
?>

Vous pouvez lancer le serveur comme suit (en adaptant l’adresse IP ou le nom de machine, et, éventuellement, le numéro de port) :

cd $HOME/www
$HOME/bin/php-5.5/bin/php -S 192.168.0.10:8888

Et accéder à la page qui vous intéresse en ouvrant un navigateur sur une URL de la forme http://localhost:8888/phpinfo.php donnera quelque chose de similaire à la capture d’écran que je reproduis ci-dessous (la capture d’écran reproduite ci-dessous correspond à la compilation d’une version des sources datant d’avant la publication de la première version alpha — ce qui explique le numéro de version différent de celui obtenu avec la sortie CLI un peu plus haut) :

Capture d'écran de phpinfo.php servi par le serveur intégré à PHP

En parallèle, dans la console depuis laquelle le serveur intégré à PHP a été lancé, vous devriez obtenir ce type de sortie :

PHP 5.5.0-dev Development Server started at Fri Sep 21 10:53:40 2012
Listening on http://192.168.0.10:8888
Document root is /home/user/www
Press Ctrl-C to quit.
[Fri Sep 21 10:53:46 2012] 192.168.0.6:40549 [200]: /phpinfo.php
[Fri Sep 21 10:53:46 2012] 192.168.0.6:40550 [404]: /favicon.ico - No such file or directory

Configuration Apache

Si vous avez choisi d’installer PHP vers un de vos répertoires personnels, et de ne pas effectuer une installation système, le module correspondant pour votre serveur web n’a probablement pas été déployé ; typiquement, souvenez-vous de l’échec obtenu plus haut lors du make install.

Plutôt que de déployer manuellement (en violant le système de gestion de paquets de votre distribution) une version instable du module PHP, une solution pratique lorsqu’il s’agit de tester est de lancer une instance de votre serveur web spécifique à cette version de test.

Avec Apache 2, que je prendrai ici comme exemple, en supposant que vous l’avez déjà installé de manière conforme à votre distribution, cela signifie :

  • Mettre en place un fichier de configuration d’Apache, indiquant notamment :
    • sur quel port il se lancera (le port 80 étant déjà pris par l’instance “normale”, et ne vous étant pas accessible puisque vous n’êtes pas root),
    • quel répertoire servir,
    • et quels modules charger.
  • Lancer cette instance de test d’Apache
  • \o/

Commençons par créer un sous-répertoire qui nous servira pour stocker les fichiers liés à Apache, et y déployer le module que nous venons de compiler :

mkdir -p $HOME/bin/apache-php55/modules
cp libs/libphp5.so $HOME/bin/apache-php55/modules/

Créons un fichier de configuration Apache, qui chargera le module PHP, et servira les fichiers placés dans le répertoire www de notre utilisateur :

# /home/user/bin/apache-php55/apache.conf

LockFile /home/user/bin/apache-php55/accept.lock
PidFile /home/user/bin/apache-php55/pid.pid

User user
Group user

ErrorLog /home/user/bin/apache-php55/error.log
LogLevel warn

NameVirtualHost *:8888
Listen 8888

Include /etc/apache2/mods-available/mime.load
Include /etc/apache2/mods-available/mime.conf

# On veut que notre serveur Apache utilise le module PHP que nous venons de compiler
LoadModule php5_module        /home/user/bin/apache-php55/modules/libphp5.so
AddType application/x-httpd-php .php .phtml
AddType application/x-httpd-php-source .phps

<VirtualHost *:8888>
    ServerAdmin user@localhost

    DocumentRoot /home/user/www
    <Directory /home/user/www/>
        Options Indexes FollowSymLinks MultiViews
        AllowOverride All
    </Directory>

    CustomLog /home/user/bin/apache-php55/access.log combined
</VirtualHost>      

Et enfin, lançons Apache en utilisant ce fichier de configuration :

apache2 -f /home/user/bin/apache-php55/apache.conf

Et normalement, vous pouvez à nouveau interroger votre phpinfo.php créé plus haut… et servi, à présent, par Apache :

Capture d'écran de phpinfo.php servi par Apache

Arrêter Apache revient à killer le processus dont le PID est stocké dans le fichier pointé par la directive PidFile :

kill `cat /home/user/bin/apache-php55/pid.pid`


Arrivés ici, vous avez installé la version de développement de PHP sur un environnement de test, et elle est utilisable à la fois depuis la ligne de commandes, et depuis un serveur Web.


A vous, maintenant, d’expérimenter avec les nouveautés apportées par cette version !

Pour plus d’informations quant à ces nouveautés, je me permet de vous pointer vers quelques uns des articles que j’ai publié à propos de PHP 5.5 ces dernières semaines :

Et n’oubliez pas : PHP 5.5 n’est pas encore figée : des choses peuvent encore bouger !



  1. Il s’agit ici d’installer une version de développement ou de test, pas vraiment / pas suffisamment testée (si vous compilez une version alpha/beta/RC, c’est justement le but : tester que vos applications fonctionnent sur cette version), absolument non garantie, pouvant être très différente de l’éventuelle future version stable, éventuellement issue d’une extraction plus ou moins aléatoire du repository hébergeant les sources du langage ; l’idée d’installer une telle version sur un environnement de production ne devrait même pas vous traverser l’esprit… 

  2. Sous une distribution Linux comme Ubuntu, il vous faudra installer le paquet git-core

  3. Vous pouvez éventuellement jouer avec les options -j et -l de la commande make pour paralléliser les processus de compilation, et ainsi gagner sur la durée totale de compilation.
    Sur une machine avec un CPU quad-core, maje -j 8 -l 6 a tendance à bien réduire la durée de compilation, tout en permettant de continuer à utiliser le PC en même temps — sans le mettre à genoux, autrement dit. 

Par Pascal MARTIN le jeudi 15 novembre 2012 13:00

La première version alpha de PHP 5.5 a été publiée ce matin.

Téléchargement :

  • Les sources packagées, comme d’habitude, sont accessibles depuis : qa.php.net
  • Et des binaires pour Windows sont disponibles sur windows.php.net/qa

Il est important de noter que la sortie de cette première version alpha ne signifie pas que les développements s’arrêtent : PHP 5.5 peut encore évoluer, que ce soit par l’ajout de fonctionnalités1, la suppression de certains points2, ou encore la modification de nouveautés déjà implémentées3.

Maintenant, pour stabiliser PHP 5.5, à nous de tester cette version, de trouver les bugs, d’effectuer des retours constructifs.




Comme prévu lors de l’annonce effectuée par David Soria Parra dans son post “Branching PHP-5.5” le 30 octobre sur la mailing-list internals@ de PHP, le tag correspondant a été créé il y a quelques jours :

Tweet de @dsp annonçant le tag

Dans ce mail datant d’il y a deux semaines, David faisait le parallèle avec la sortie de PHP 5.4 : cette première version alpha de PHP 5.5 est publiée quelque chose comme environ un mois et demi plus tard que la première alpha de PHP 5.4 ne l’avait été ; on pourrait donc espérer une sortie de la première version stable, PHP 5.5.0, pour avril 2013.
Bien sûr, aucune date n’est figée : tout dépend de la manière dont se déroulera le cycle de versions alpha / bêta / RC.


Sur le repository git de PHP, la branche PHP 5.5 a fait son apparition le 13 novembre (cf post HEADS UP: PHP-5.5 branched), et va vivre au fur et à mesure de l’évolution de cette nouvelle version du langage :

$ git pull
remote: Counting objects: 213, done.
remote: Compressing objects: 100% (137/137), done.
remote: Total 137 (delta 100), reused 0 (delta 0)
Receiving objects: 100% (137/137), 25.53 KiB, done.
Resolving deltas: 100% (100/100), completed with 41 local objects.
From https://git.php.net/repository/php-src
   e383d4d..2e6a4a2  master     -> origin/master
   417b1b2..019bdff  PHP-5.3    -> origin/PHP-5.3
   9d95f7d..117d2dd  PHP-5.4    -> origin/PHP-5.4
 * [new branch]      PHP-5.5    -> origin/PHP-5.5
Updating 261e32f..2e6a4a2

Quant au tag en lui-même, il rejoint la longue liste de tags correspondant à chacune des versions publiées :

Screenshot de gitweb


  1. Il reste des propositions de nouvelles fonctionnalités en cours de discussions, parallèlement à leurs RFC.
    Et, bien entendu, il n’est pas interdit de penser à PHP 5.6 ;-) 

  2. Par exemple, une discussion est en cours autour du possible marquage comme deprecated des fonctions mysql_*() dès PHP 5.5, de façon à pouvoir les supprimer pour la version suivante (PHP 5.6, peut-être)

  3. Je pense par exemple au séparateur d’espaces de noms, qui était :: pour les toutes premières versions alpha de PHP 5.3, et qui a été transformé en \ en plein milieu de cette phase du cycle de release. 

Par Pascal MARTIN le mardi 6 novembre 2012 06:00

De manière générale, PHP a tendance à garder un maximum de compatibilité entre versions, de manière à ce qu’un code écrit pour PHP 5.x continue à fonctionner avec PHP 5.(x+1)1.

Cela dit, chaque nouvelle version vient avec quelques changements qui peuvent avoir des impacts sur votre code ; dans l’ensemble, ces changements peuvent être de l’un des types suivants (qui ne sont pas mutuellement exclusifs) :

  • Nouvelle fonctionnalité : nouvelle classe ou fonction (ou même nouveau mot-clef) fournie par le langage, que vous ne pouvez pas redéfinir (plus définir) dans votre code,
  • Evolution qui irait à l’encontre de la manière dont votre code est écrite (par exemple : passage de paramètres par référence lors de l’appel d’une fonction),
  • Correctif pensé dans un but de sécurité ; qui peut avoir un impact si votre code n’était pas écrit correctement ou avait un comportement / répondait à un besoin inhabituel (par exemple, suppression de register_globals, ou directive max_input_vars)
  • Arrêt de support d’une bibliothèque / extension ou d’un système d’exploitation (par exemple, arrêt du support des ereg au profit des preg)

Autant que faire se peut, entre deux versions PHP 5.x, les changements susceptibles de casser votre code ou vos applications sont minimisés (et plus deux versions sont “proches”, moins il y a de risque d’en avoir — typiquement, le passage de PHP 5.2 à 5.3 est réputé plus difficile que le passage de PHP 5.3 à 5.4, qui ont été moins espacées dans le temps)

… Mais, bien entendu, à vous de lire les release notes et guides de mise à jour ; ainsi que de tester soigneusement en environnements de développement et de tests, avant de basculer un changement de version sur un serveur de production : c’est aussi / exactement à cela que servent les versions bêta et RC qui sont systématiquement publiées avant une mise à jour importante2.

Arrêt du support de Windows XP et 2003

La prochaine version de PHP (probablement PHP 5.5) devrait mettre fin au support de Windows XP et de Windows 2003.

Avant de râler, si je puis me permettre, voici quelques points pour remettre les choses en perspective :

  • Windows XP est sorti en octobre 2001 ; la dernière version stable étant le Service Pack 3 publié en 2008,
  • Windows Server 2003 est sorti en avril 2003 ; son successeur ayant été publié en 2008.
  • Supporter des systèmes d’exploitation qui datent de dix ans ou plus demande du travail… et du temps… que les développeurs / mainteneurs de PHP peuvent vouloir passer à développer de nouvelles fonctionnalités, plutôt que supporter des OS obsolètes.
  • Oh, au passage, pour ceux d’entre nous qui font du développement front end et qui n’en peuvent plus de devoir supporter IE6 ou qui ont la chance de l’avoir laissé tomber : Windows XP, si on compte, c’est quand même un peu l’IE6 du monde des OS ;-)

Déprécié : modificateur /e pour preg_replace()

Jusqu’à présent, la fonction preg_replace() acceptait le modificateur d’expression rationnelle /e (PREG_REPLACE_EVAL), qui permet d’inclure du code dans la chaine de caractères de remplacement.

preg_replace() avec exécution de code

Par exemple, la portion de code suivante était valide :

$subject = "une suite de mots";
$pattern = "/([^\s]+)/ei";
$replacement = 'ucfirst(\'$1\')';

$result = preg_replace($pattern, $replacement, $subject);
var_dump($result);

En exécutant cette portion de code avec PHP <= 5.4, nous aurions obtenu :

string(17) "Une Suite De Mots"

Maintenant, si vous exécutez cette même portion de code avec la branche master de PHP (et donc, probablement, PHP 5.5), vous obtiendrez un avertissement indiquant que ce modificateur /e est déprécié :

Deprecated:  preg_replace(): The /e modifier is deprecated, use preg_replace_callback instead 
    in /.../php-5.5/tests/preg_replace/preg_replace-1.php on line 8

string(17) "Une Suite De Mots"

Autrement dit, ce modificateur /e devrait disparaitre dans une prochaine version du langage, et vous ne devriez donc plus l’utiliser (niveau sécurité, ce n’est pas une solution vraiment optimale : il est relativement facile d’oublier un cas qui permettrait une injection de code).

preg_replace_callback()

A la place de la portion de code reproduite plus haut, vous pourrez vouloir utiliser quelque chose de ce type, en utilisant la fonction preg_replace_callback() :

$subject = "une suite de mots";
$pattern = "/([^\s]+)/i";
$callback = function ($item) {
    return ucfirst($item[1]);
};

$result = preg_replace_callback($pattern, $callback, $subject);
var_dump($result);

La sortie obtenue sera la même — sans l’avertissement à propos de /e, bien sûr :

string(17) "Une Suite De Mots"

(oui, pour un cas aussi simple, il n’était pas nécessaire de passer par une fonction anonyme, et il aurait été possible de juste passer en paramètre le nom de la fonction ucfirst)

Nouveaux mots-clefs, nouvelles classes et fonctions

Cette future nouvelle version de PHP (probablement PHP 5.5, encore une fois) apporte un nouveau mot-clef, et plusieurs nouvelles classes et fonctions.

Nouveau mot-clef yield

La version actuelle de la branche master de PHP (et donc, probablement, PHP 5.5) implémente les generators, à l’aide du mot-clef yield.

Celui-ci ne peut donc plus être utilisé comme identifieur dans votre code ; par exemple, il n’est plus possible d’utiliser une portion de code comme celle que je reproduis ci-dessous, déclarant une classe nommée yield :

<?php

class yield {
    public $plop = "Hello";
}
$obj = new yield();
var_dump($obj);

Exécuter cette portion de code entraine désormais l’erreur suivante :

$ $HOME/bin/php-5.5/bin/php ./bc-breaks/keyword-yield.php 
PHP Parse error:  syntax error, unexpected 'yield' (T_YIELD), expecting identifier (T_STRING) 
    in /.../php-5.5/tests/bc-breaks/keyword-yield.php on line 3

Alors que, à titre de vérification, elle s’exécutait sans aucun problème en PHP 5.3 / 5.4 :

$ php ./bc-breaks/keyword-yield.php 
class yield#1 (1) {
  public $plop =>
  string(5) "Hello"
}

De la même manière et pour la même raison, on ne peut plus déclarer une fonction nommée yield() ; autrement dit, la portion de code suivante :

<?php

function yield($plop) {
    echo "yield >> $plop\n";
}
yield("test");

Mène à présent à l’erreur suivante :

$ $HOME/bin/php-5.5/bin/php ./bc-breaks/keyword-yield-2.php 
PHP Parse error:  syntax error, unexpected 'yield' (T_YIELD), expecting '(' 
    in /.../php-5.5/tests/bc-breaks/keyword-yield-2.php on line 3

Alors qu’elle était tout à fait valide en PHP 5.3 :

$ php ./bc-breaks/keyword-yield-2.php 
yield >> test

Fonctions supprimées et ajoutées

La version courante de la branche master de PHP (et donc, probablement, PHP 5.5) introduit un nombre important de nouvelles fonctions :

  • La fonction de conversion en booléen boolval(), dont j’ai parlé ici
  • Quelques fonctions ont été ajoutées à l’extension curl : curl_escape(), curl_reset(), curl_share_close(), curl_share_init(), curl_share_setopt(), curl_unescape()
  • Quelques nouvelles fonctions de manipulation de dates : datefmt_format_object(), datefmt_get_calendar_object(), datefmt_get_timezone(), datefmt_set_timezone()
  • hash_pbkdf2(), dont je parlais ici
  • Toute une série de fonctions intlcal_*(), intlgregcal_*(), intltz_*() correspondant aux évolutions de l’extension intl, dont je parlais ici.
  • json_last_error_msg() ; cf commit : retourne le message d’erreur correspondant au dernier appel à json_encode() ou à json_decode() ; un peu sur le modèle de json_last_error() qui existe depuis PHP 5.3 mais ne retourne qu’un code.
  • openssl_pbkdf2()
  • Les fonctions password_get_info(), password_hash(), password_needs_rehash(), password_verify() correspondant à l’API simple pour le hachage de mots de passe, dont je parlais ici.

Cela signifie que vous ne pouvez plus définir, dans votre code PHP, de fonction portant un de ces noms.

A titre d’illustration, la portion de code suivante fonctionne en PHP 5.4 :

<?php
function boolval($valeur) {
    return (bool)$valeur;
}

var_dump( boolval('plop') );

Mais elle entraine une Fatal Error en PHP 5.5 :

$ $HOME/bin/php-5.5/bin/php ./bc-breaks/redefinition-fonction.php 
PHP Fatal error:  Cannot redeclare boolval() 
    in /.../php-5.5/tests/bc-breaks/redefinition-fonction.php on line 4


En parallèle, les fonctions supprimées sont, jusqu’à présent, au nombre de trois :

Ces fonctions qui permettaient d’obtenir les identifiants des logos de PHP n’étaient pas vraiment bien utiles… et exposaient des informations de PHP.

Leur suppression ne devrait probablement pas vraiment nous impacter (à vrai dire, je ne les ai jamais vu être utilisées dans un code “réel” d’application).

Nouvelles classes

Au niveau des ajouts de classes, moins de choses à signaler, puisque la liste des classes ajoutées, aujourd’hui, sur la branche master de PHP (et donc, probablement, PHP 5.5) s’arrête à celle que je reproduis ci-dessous :

  • Generator : la classe correspondant aux Generators
  • Et une série de nouvelles classes pour l’extension d’internationalisation : IntlBreakIterator, IntlCalendar, IntlCodePointBreakIterator, IntlException, IntlGregorianCalendar, IntlIterator, IntlPartsIterator, IntlRuleBasedBreakIterator, IntlTimeZone ; j’ai parlé de plusieurs d’entre elles ici.

Ici encore, sauf à jouer avec les espaces de noms, veillez à ne pas redéfinir ces classes dans votre code PHP.

Voir aussi

  • RFC: Remove preg_replace /e modifier : la RFC en rapport avec l’arrêt du support de /e par preg_replace() — elle donne notamment plus d’explications sur le pourquoi de ce changement.

Et je terminerai en vous pointant vers le fichier UPGRADING, qui contient un ensemble de sections intéressantes (dont Backward Incompatible Changes, Changed Functions, Changes to INI File Handling, …) et devrait être mise à jour à chaque changement incompatible avec les versions précédentes. Ce fichier sera bien évidemment à consulter après la sortie de PHP 5.5, pour déterminer ce qui risque de casser dans vos applications.


  1. Cette volonté de conserver un maximum de compatibilité entre les versions de PHP (et ça va plus loin que PHP 5.x -> PHP 5.(x+1)) est d’ailleurs une raison pour laquelle PHP a trainé un bon nombre de boulets “historiques” pendant fort longtemps (voire continue de le faire) — je pense par exemple à register_globals ou à safe_mode, aux nommages de fonctions pas toujours homogènes, ou aux ordres de paramètres pas toujours logiques. 

  2. J’ajouterais volontier qu’avoir des tests automatisés (unitaires, fonctionnels, de non-régression, de performance, …), de qualité sur votre application aide fortement lorsqu’il s’agit de monter de version de PHP — ne serait-ce que parce qu’ils permettent d’identifier des points que vous aurez peut-être à adapter, mais sont aussi en mesure de vous montrer que le travail à effectuer de votre côté n’est peut-être pas aussi important que vous pourriez le craindre. 

Par Pascal MARTIN le lundi 5 novembre 2012 07:00 6 commentaires

La version actuelle de la branche master de PHP (et donc, probablement, PHP 5.5) implémente le concept de Generator.

Pour faire simple, disons que les Generators sont une manière simple d’implémenter des itérateurs, sans à avoir à écrire beaucoup de code décoratif ; autrement dit, les generators ne viennent pas révolutionner notre langage, mais, comme beaucoup d’autres nouveautés de PHP 5.5, visent à nous faciliter le travail et à rendre notre code plus concis.

Exemple : Chargement en mémoire vs Iterator vs Generator

La meilleure façon d’aborder le concept de Generators est sans aucun doute à travers un exemple.

Considérant la situation suivante :

Nous souhaitons afficher une liste de nombres, multiples d’une valeur donnée, en commençant à 0, et en allant jusqu’à une valeur maximale de notre choix.
Bien sûr, nous voulons séparer la logique de génération de cette liste de nombres, de la logique d’affichage.

Fonction chargeant “tout” en mémoire et retournant un tableau

La première solution qui vient à l’esprit, la plus naive si je puis me permettre, serait de créer une fonction, qui :

  • prenne en paramètre la valeur dont nous souhaitons obtenir les multiples et la valeur maximale à atteindre,
  • et retourne un tableau contenant tous les nombres multiples de la première valeur, entre 0 et la seconde valeur.

Typiquement, cette fonction pourrait être écrite comme ceci :

function multiplesDe($diviseur, $max) {
    $result = array();
    for ($num = 0 ; $num <= $max ; $num += $diviseur) {
        $result[] = $num;
    }
    return $result;
}

Et nous l’utiliserions ainsi, en bouclant sur le tableau qu’elle retourne, pour afficher les valeurs qui nous intéressent :

$listeMultiples = multiplesDe(3, 15);
foreach ($listeMultiples as $i => $multiple) {
    echo "$i => $multiple\n";
}

Le résultat obtenu en sortie sera le suivant :

0 => 0
1 => 3
2 => 6
3 => 9
4 => 12
5 => 15

Cette première implémentation est simple à écrire, et ne demande que peu de code ; mais elle souffre d’un inconvénient majeur : l’ensemble des résultats sont chargés en mémoire par la fonction — ce qui ne serait pas adapté si notre liste de résultats était d’une taille beaucoup plus importante.

Avec un itérateur

Pour répondre à cette problèmatique en évitant de tout charger en mémoire tout en conservant la séparation entre la logique de calculs et la logique d’affichage, PHP propose depuis sa version 5.0 d’utiliser des itérateurs.

Ré-écrivons donc notre fonction, sous la forme d’une classe implémentant l’interface Iterator :

class MultiplesIterator implements Iterator {
    protected $diviseur, $max;
    protected $current, $indiceCurrent;
    public function __construct($diviseur, $max) {
        $this->diviseur = $diviseur;
        $this->max = $max;
    }
    public function rewind() {
        $this->current = 0;
        $this->indiceCurrent = 0;
    }
    public function valid() {
        return $this->current <= $this->max;
    }
    public function current() {
        return $this->current;
    }
    public function key() {
        return $this->indiceCurrent;
    }
    public function next() {
        $this->indiceCurrent += 1;
        $this->current += $this->diviseur;
    }
}

Et l’utilisation de cette classe devient la suivante ; assez peu différente de la boucle de parcours que nous avions mis en place un peu plus haut :

$iterator = new MultiplesIterator(3, 15);
foreach ($iterator as $i => $multiple) {
    echo "$i => $multiple\n";
}

Cette proposition répond à nos deux besoins :

  • séparation calculs / affichage,
  • sans consommation excessive de mémoire dûe à un stockage de l’ensemble des résultats dans une liste.

Mais passer par un itérateur demande d’écrire une quantité de code que j’ai presque envie de qualifier d’incroyable, dans le cadre d’un exemple qui était censé être aussi simple… 25 lignes de code pour générer une liste de nombres multiples d’une valeur donnée ?

Et avec un Generator

A présent, ré-écrivons une nouvelle fois notre portion de code, en utilisant un Generator.

En quelques mots, un Generator est une fonction au sein de laquelle est utilisé le nouveau mot-clef yield. Lors que celui-ci est atteint, la valeur qu’il spécifie est renvoyée à l’appelant, et l’exécution de la fonction est mise en pause jusqu’à ce que l’appelant demande à ce qu’elle soit continuée — ce qui sera le cas jusqu’à l’instruction yield suivante, et ainsi de suite.

Dans notre cas, nous pouvons repartir de notre première implémentation ; mais, au lieu de stocker les nombres générés dans un tableau retourné à la fin de la fonction, il devient possible d’utiliser yield pour qu’ils soient immédiatement re-passés à l’appelant :

function multiplesDe($diviseur, $max) {
    for ($i = 0, $num = 0 ; $num <= $max ; $i += 1, $num += $diviseur) {
        yield $i => $num;
    }
}

L’utilisation de cette fonction, de ce Generator, est identique à celle que nous avions écrit initialement :

$listeMultiples = multiplesDe(3, 15);
foreach ($listeMultiples as $i => $multiple) {
    echo "$i => $multiple\n";
}

En somme, en utilisant un Generator, nous combinons le meilleur des deux solutions vues précédemment :

  • Les logiques de calcul et d’affichage sont séparées,
  • Nous ne consommons pas de mémoire inutilement en stockant tous les résultats dans une liste, puisque chaque valeur est yieldée au moment où elle est générée,
  • Et cette implémentation à base de Generator est courte : seulement 5 lignes de code (contrairement à la solution basée sur un itérateur, qui en demandait 25)

Un Generator est un Iterator

Le parcours à base de foreach() que nous venons d’utiliser fonctionne car, en interne, un Generator est un itérateur.

Parcourir un generator ?

Prenons comme exemple une fonction Generator très simple, qui yielde successivement deux valeurs :

function monGenerator() {
    echo "debut monGenerator\n";
    yield "Premier appel";
    yield "Second appel";
    echo "fin monGenerator\n";
}

Obtenons un generator depuis cette fonction, et, par curiosité, essayons de l’afficher :

$generator = monGenerator();
var_dump($generator);

La sortie obtenue sera la suivante :

object(Generator)#1 (0) {
}

Autrement dit, un generator est un objet, instance de la classe Generator.


A titre de vérification, essayons l’opérateur instanceof :

var_dump($generator instanceof Generator);
var_dump($generator instanceof Iterator);

La sortie obtenue sera la suivante :

bool(true)
bool(true)

Nous confirmons ainsi qu’un générateur est une instance de la classe Generator ; et nous prouvons ce que je disais plus haut, à savoir qu’un generator est un itérateur, puisqu’il implémente l’interface Iterator.


Puisqu’un generator est un itérateur, il peut être parcouru manuellement, sans forcément être utilisé avec foreach() ; cela permet d’ailleurs de mieux voir ce qu’il se passe ;-)

Par exemple, si nous continuons notre exemple, en exécutant la portion de code suivante (sur chaque ligne de code, j’ai indiqué en commentaire quelle sortie elle entrainait) :

$valeur = $generator->current();    // debut monGenerator
echo " >> $valeur\n";               //  >> Premier appel

$generator->next();
$valeur = $generator->current();
echo " >> $valeur\n";               //  >> Second appel

$generator->next();                 // fin monGenerator
$valeur = $generator->current();
echo " >> $valeur\n";               //  >> 

La sortie globale obtenue sera la suivante:

debut monGenerator
 >> Premier appel
 >> Second appel
fin monGenerator
 >> 

Mais quelques points sont à noter :

  • Lorsque nous avons appelé la fonction monGenerator(), aucune sortie n’a été générée ; le echo de sa première ligne n’a pas été exécuté, puisque nous n’avons pas obtenu la sortie "debut monGenerator" à ce moment là.
  • Par contre, nous avons obtenu cette sortie "debut monGenerator" au moment où nous avons réellement commencé à utiliser notre générateur : lors du premier appel à current()
  • next() permet d’avancer d’instruction yield en instruction yield, alors que current() permet d’obtenir la valeur yieldée.
  • Une fois la dernière instruction yield atteinte, appeler à nouveau next() permet de poursuivre l’exécution de la fonction jusqu’à sa fin.

Nous obtenons la preuve qu’une fonction contenant l’instruction yield — un Generator donc — ne se comporte pas vraiment comme une fonction normale.

Les méthodes d’un generator

Un generator est un itérateur ; il a donc les méthodes de l’interface Iterator :

  • rewind() : voir un peu plus bas : ne peut être utilisé que si le generator est à son premier yield ou avant,
  • valid() : retourne true tant que le generator n’est pas fermé,
  • current() : retourne la valeur yieldée ; ou null si aucune valeur n’a été yieldée ou que le generator est déjà fermé,
  • key() : retourne la clef yieldée ; ou si aucune clef n’a été spécifiée lors du yield, une clef numérique auto-incrémentée ; ou null si le générateur est fermé,
  • next() : reprend l’exécution du générateur, s’il n’est pas encore fermé.

Voici un exemple d’utilisation de la plupart de ces méthodes (à côté de chaque ligne de code figure la sortie qu’elle provoque) :

function monGenerator() {
    echo "debut monGenerator\n";
    yield "Premier appel";
    yield "Second appel";
    echo "fin monGenerator\n";
}

$generator = monGenerator();        // pas de sortie

$key = $generator->key();           // debut monGenerator
echo "key = $key\n";                // key = 0

$valeur = $generator->current();    // pas de sortie
echo "valeur = $valeur\n";          // valeur = Premier appel

var_dump($generator->valid());      // bool(true)

$generator->next();                 // pas de sortie ; avance d'un élément

$key = $generator->key();           // pas de sortie
echo "key = $key\n";                // key = 1

$valeur = $generator->current();    // pas de sortie
echo "valeur = $valeur\n";          // valeur = Second appel

var_dump($generator->valid());      // bool(true)

$generator->next();                 // fin monGenerator

var_dump($generator->valid());      // bool(false)

Et voici la sortie qui est obtenue au final :

debut monGenerator
key = 0
valeur = Premier appel
bool(true)
key = 1
valeur = Second appel
bool(true)
fin monGenerator
bool(false)

Attention avec rewind() : dans l’idée, appeler la fonction rewind() revient à effectuer un retour au contexte d’exécution de l’appel initial du generator, ce qui peut mener à des comportements inattendus.

Pour éviter cela, rewind() ne peut être appelée que s’il revenait à ne rien faire ; autrement dit, que si le generator est actuellement avant son premier yield, ou à son premier yield.
Appeler rewind() dans une autre situation entrainera une exception.

Par exemple, si nous considérons la portion de code suivante :

$generator = monGenerator();        // pas de sortie

// Generator pas après le 1er yield => peut être rewindé
$generator->rewind();               // debut monGenerator
echo $generator->current() . "\n";  // Premier appel

// Generator toujours pas après le 1er yield => peut être rewindé
$generator->rewind();               // pas de sortie
echo $generator->current() . "\n";  // Premier appel
$generator->next();

// Generator après le 1er yield (car next() appelée) => ne peut pas être rewindé
$generator->rewind();               // Uncaught exception 'Exception' with message 'Cannot rewind a generator that was already run'

Nous obtiendrons la sortie suivante :

debut monGenerator
Premier appel
Premier appel

Fatal error:  Uncaught exception 'Exception' with message 'Cannot rewind a generator that was already run' 
    in /.../php-5.5/tests/generators/generators-006.php:24

Ici :

  • Le premier appel à rewind() a fonctionné : le générateur n’avait pas encore atteint son premier yield,
  • Le second appel à rewind() est possible : le generator était sur son premier yield — et pas après celui-ci,
  • Par contre, le troisième appel à rewind() entraine une exception : le generator était après son premier yield.

Syntaxe de yield

Comme je l’ai laissé entendre plus haut, la version courante de la branche master de PHP (et donc, probablement, PHP 5.5) introduit un nouveau mot-clef : yield.

Nouveau mot-clef yield

Ce nouveau mot-clef supporte trois syntaxes :

  • yield $key => $value : Yielde la valeur $value avec la clef $key,
  • yield $value : Yielde la valeur $value avec une clef numérique auto-incrémentée,
  • et yield seul : Yielde la valeur null, avec un clef numérique auto-incrémentée.

Par exemple, nous pouvons utiliser la portion de code suivante, qui utilise chacune des trois syntaxes deux fois de suite :

function monGenerator() {
    // Valeur 'null' avec clef numérique en auto-increment
    yield;
    yield;

    // En ne spécifiant qu'une valeur ; clef numérique en auto-increment
    yield "première valeur seule";
    yield "seconde valeur seule";

    // En spécifiant clef et valeur
    yield "première clef" => "première valeur ayant une clef";
    yield "seconde clef" => "seconde valeur ayant une clef";
}

$generator = monGenerator();
foreach ($generator as $clef => $valeur) {
    echo "$clef => $valeur\n";
}

Exécuter cette portion de code nous donnerait la sortie suivante :

0 => 
1 => 
2 => première valeur seule
3 => seconde valeur seule
première clef => première valeur ayant une clef
seconde clef => seconde valeur ayant une clef

On retrouve les trois cas présentés plus haut ; avec une clef numérique auto-incrémentée pour les deux syntaxes où nous ne spécifions pas nous-même cette clef.

Yielding de clefs

Pour ceux qui auraient déjà utilisé des generators avec d’autres langages que PHP (Python, Javascript, C#, …), vous aurez peut-être remarqué que la syntaxe proposée par PHP permet de yielder des clefs en plus des valeurs.

Ceci n’est pas supporté par les autres langages, car ils ne supportent pas de clef dans les itérateurs.

Mais, en PHP, le yielding de clefs :

  • Colle au fonctionnement des itérateurs, et de leurs méthodes key() et current(),
  • Et, même si c’est plutôt lié, s’adapte parfaitement à la syntaxe foreach clef => valeur qui est très utilisée.

Dans les cas où une clef n’est pas yieldée explicitement, le fonctionnement sera le même que pour les clefs de tableaux : un numérique auto-incrémenté est produit, commençant à 0 et s’incrémentant systématiquement de 1. Dans le cas où une clef numérique plus grande que la valeur d’auto-incrément courante est yieldée, celle-ci sera utilisée comme point de départ pour les futurs clefs générées de manière automatiques ; les clefs yieldée explicitement tout en étant d’autres types n’influeront pas sur ce mécanisme.

Une seule différence avec les clefs de tableaux, tout de même : lorsqu’une clef numérique mais qui n’est pas un integer est levée, elle ne sera avec les generators pas automatiquement transtypée en entier — alors qu’avec les tableaux, une clef numérique l’est (la clef de tableau '10' est automatiquement convertie en 10).

Yielding par référence

Les yieldings que nous avons vus jusqu’à présent étaient tous effectués par valeur ; mais il est possible de yielder par référence, pour donner à l’appelant accès à la donnée yieldée.

En m’inspirant assez fortement de l’exemple donné dans la RFC, considérons la portion de code suivante : nous avons une classe avec une donnée, et un generator qui yielde par référence (notez les & devant le nom de la fonction, et dans la boucle foreach) :

class TestGeneratorByRef {
    protected $valeurs;
    public function __construct(array $valeurs) {
        $this->valeurs = $valeurs;
    }
    public function getValeurs() {
        return $this->valeurs;
    }

    public function & getGenerator() {
        foreach ($this->valeurs as & $valeur) {
            yield $valeur;
        }
        unset($valeur);
    }
}

Instancions cette classe, et à titre de vérification, affichons les valeurs qui y sont stockées :

$obj = new TestGeneratorByRef(range(0, 3));
var_dump($obj->getValeurs());

Nous obtiendrons, comme nous pouvions nous y attendre, la sortie suivante (le tableau de valeurs stocké par l’objet a été initialisé) :

array(4) {
  [0]=>
  int(0)
  [1]=>
  int(1)
  [2]=>
  int(2)
  [3]=>
  int(3)
}

Maintenant, utilisons le generator retourné par référence par la méthode getGenerator(), pour parcourir (par référence, encore) la liste de valeurs, en effectuant une opération simple sur chacune d’enrte elles :

foreach ($obj->getGenerator() as & $valeur) {
    $valeur *= 100;
}
unset($valeur);
var_dump($obj->getValeurs());

La sortie obtenue, cette fois-ci, sera la suivante :

array(4) {
  [0]=>
  int(0)
  [1]=>
  int(10)
  [2]=>
  int(20)
  [3]=>
  int(30)
}

Autrement dit, travailler avec un generator par référence permet d’accéder en écriture aux données sur lesquelles nous itérons — ce qui n’est pas possible avec un itérateur normal.

Renvoyer une valeur au Generator

Le mot-clef yield correspond en fait à une expression, dont la valeur peut correspondre à celle envoyée au générateur à l’aide de sa méthode send() — oui, le code appelant, utilisant le generator et travaillant avec les données yieldées par celui-ci, peut renvoyer des données au générateur depuis lequel il obtient ses données.

La syntaxe à utiliser correspond à une simple affectation, sans oublier les parenthèses autour de l’instruction yield et de son couple clef-valeur :

$donnees = (yield $clef => $valeur);
$donnees = (yield $valeur);
$donnees = (yield);
$donnees = yield;       // Parenthèses non requises, ici

A titre d’exemple fonctionnel, considérons la portion de code que je reproduis ci-dessous :

function monGenerator() {
    $donnee = (yield "première valeur");
    var_dump($donnee);

    $donnee = (yield "seconde valeur");
    var_dump($donnee);
}

$generator = monGenerator();

$valeur = $generator->current();
echo "$valeur\n";                           // première valeur
$generator->send("retour 1er yield");       // string(16) "retour 1er yield"

// Pas d'appel à next()

$valeur = $generator->current();
echo "$valeur\n";                           // seconde valeur
$generator->send("retour 2nd yield");       // string(16) "retour 2nd yield"

Le generator utilisé ici yielde successivement deux valeurs ; et affiche à l’aide de var_dump() chacune des données retournées par les expressions de yielding.
Depuis le code appelant, nous utilisons la méthode send() du générateur pour lui spécifier la valeur que l’expression yield devra retourner, et poursuivre l’exécution de celui-ci.

Exécuter cette portion de code ménerait à la sortie suivante :

première valeur
string(16) "retour 1er yield"
seconde valeur
string(16) "retour 2nd yield"

Cet exemple nous montre que les generators n’ont pas un fonctionnement à sens unique (génération de données) : ils peuvent aussi, d’une certaine manière, adapter leur comportement en fonction de celui du code appelant.

Quelques points supplémentaires

Cet article est a priori déjà le plus long de cette série “En route vers PHP 5.5”, mais voici encore quelques points, peut-être moins importants, dont je voulais parler.

Fermeture d’un generator

Un generator est fermé dans trois cas :

  • L’instruction return est appelée depuis le generator,
  • Une exception est levée depuis le generator, et n’est pas catchée au sein de celui-ci,
  • Ou toutes les références à l’objet generator sont perdues / supprimées.

Lorsque le generator est fermé :

  • Son contexte d’exécution est libéré,
  • valid() retourne false,
  • Et key() et current() retournent null.

Quelques cas d’erreurs

Sans faire le tour de tous les “cas d’erreur” possibles, en voici quelques uns qui sont potentiellement intéressants :

Tout d’abord, notons qu’il n’est pas permis d’utiliser une instruction return avec une valeur dans une fonction generator.
Par exemple, la portion de code suivante :

<?php
function monGenerator() {
    yield "Premier appel";
    yield "Second appel";
    return "valeur de retour";
}

Entrainerait une sortie de ce type, en Fatal Error :

Fatal error: Generators cannot return values using "return" 
    in /.../php-5.5/tests/generators/generators-error-001.php on line 5


Je disais un peu plus haut qu’un generator est un objet, instance de la classe Generator.
Suivant la même logique que la classe Closure, cette classe Generator ne peut pas être instanciée depuis du code utilisateur (ça casserait un peu la magie…).

Typiquement, une portion de code comme celle que je reproduis ci-dessous n’est pas autorisée :

<?php
$obj = new Generator();

Tenter d’exécuter ceci mène à une autre erreur fatale :

Catchable fatal error: The "Generator" class is reserved for internal use and cannot be manually instantiated 
    in /.../php-5.5/tests/generators/generators-error-002.php on line 2


Et purement pour la plaisir, quelques autres points méritant attention, qui entrainent soit une erreur, soit une levée d’exception :

  • Yielding d’une clef qui ne soit pas une clef de tableau valide,
  • Utilisation de l’instruction yield en dehors d’une fonction / méthode,
  • Tentative de parcours par référence d’un generator non-ref,

Pour une liste plus complète, mais pas forcément exhaustive pour autant, je vous invite à lire cette section de la RFC.

Voir aussi

Enfin, comme d’habitude, je termine cet article en vous pointant vers quelques pages qui peuvent être intéressantes :

Et aussi, même s’il s’agit ici de Python (qui supporte les generators depuis bien plus longtemps que PHP, qui ne les inclut pas encore dans une version stable), voici deux autres lectures qui pourraient vous intéresser :