Catégorie « PHP »

Quelques trucs sur PHPMyAdmin #1

Durée de la session

C'est toujours lourd d'avoir une session qui expire et de devoir se reconnecter. Pour augmenter la durée de la session, rendez-vous dans le fichier config.inc.php situé à la racine du site et modifiez la valeur de la clé 'LoginCookieValidity' du tableau $cfg :

$cfg['LoginCookieValidity'] = 3600 * 48; // 48 hours

Évidemment, c'est à éviter sur les serveurs en production, pour des raisons de sécurité mais sur un poste de développement c'est tout à fait approprié.

Si ça ne passe pas c'est que la valeur de session.gc_maxlifetime est trop restrictive dans le php.ini. Ceci peut être changé là bas ou bien directement via ini_set() dans le fichier de configuration de PHPMyAdmin si votre configuration serveur l'autorise.

Pagination des tables

Un projet Change comporte en facilement plus de 200 à 300 tables (selon le nombre de modules utilisés) du coup, ça active la pagination de la liste. Pour augmenter le nombre d'éléments par page et éviter ce problème, rendez-vous dans le fichier config.inc.php situé à la racine du site ajoutez l'entrée suivante :

$cfg['MaxTableList'] = 500;

Défilement des colonnes

Toujours dans Change, les tables comportent un assez grand nombre de champs puisqu'on a toujours au moins la 15aine de champs standards des documents qui se retrouvent devant... Du coup quand on défile pour voir les champs utiles, on perd les menus, notamment le bouton "actualiser", ce qui oblige à défiler en sens inverse (puisque PHPMyAdmin a la mauvaise idée d'être basé sur des frames, faire juste un F5 n'est pas forcément toujours une bonne idée).

Du coup je me suis ajouté les styles suivants via Stylish sur Firefox qui place le défilement horizontal sur le tableau plutôt que sur la page complète (en remplaçant évidemment l'URL du site) :

@namespace url(http://www.w3.org/1999/xhtml);
@-moz-document url-prefix("<URL_DU_SITE>") {
  .data {
    overflow-x: auto !important;
    overflow-y: hidden !important;
    max-width: 100% !important;
  }
}

Historique des méthodes de surcharge de code PHP dans RBS Change

Le but de cet article est de revenir sur les différentes méthodes de surcharge du code PHP qu'on a eu (et que pour beaucoup on a encore) dans RBS Change depuis que je travaille dessus (au départ en faisant des sites avec puis par la suite en tant que développeur du produit).

Je n'entrerai pas dans les détails concrets d'utilisation mais ça me semble intéressant de voir un peu l'évolution des concepts, les différentes tentatives qu'il y a eu et les raisons qui ont pu orienter certains choix.

En fin d'article je présente ce qui sera fait dans la version 4.0 dont la sortie n'est prévue que courant 2013. En conséquence, ce que j'évoque pourrait potentiellement encore évoluer d'ici là et ne reflète que ce qui a été mis en œuvre pour l'instant, pas une vérité gravée dans le marbre.

Écrasement des fichiers dans la webapp

J'ai commencé à travailler avec Change sur la version 1.2 en 2006. À cette époque le seul moyen offert pour surcharger du code du produit était d'écraser certains fichiers (PHP ou autres, cela fonctionnait pour tous les fichiers des modules et même du framework : PHP, XML, templates, styles, etc) dans le dossier "webapp". La mécanique était (trop) simple : à chaque chargement de fichier PHP, l'application commençait par le chercher dans le dossier webapp. S'il était présent cette version était chargée, sinon celle du code standard l'était. Le fichier de surcharge était donc contraint de reprendre l'intégralité du code qu'il surchargeait, même au cas où il réécrivait seulement une méthode d'une classe en comptant cinquante.

C'était toujours un peu mieux que de modifier directement le code du produit mais à peine puisque lors d'une mise à jour, il fallait passer sur chaque fichier surchargé pour ré-appliquer les modification.

Dès la version 2, l'année suivante, la possibilité de surcharge de code PHP directement dans le dossier webapp a été supprimée car elle posait beaucoup trop de problèmes. En remplacement, une autre méthode plus cadrée a été mise en place : "l'injection" de service et peu après, de document.

Injection de service et de document

L'injection de service (toujours d'actualité dans la version 3.6) fonctionne sur un principe qui reste très simple mais qui est nettement plus cadré. Dans la configuration projet déclare des injections consistant à dire "le servie A est à remplacer par le service B". Tous les service sont instanciés via une méthode getInstance() (selon le patron de conception singleton) qui inclut une mécanique qui va chercher dans la configuration projet si une injection a été déclarée sur ce service. S'il y en a une, on retourne une instance de la classe remplaçante au lieu de la classe appelée.

Le code travaille donc simplement avec une autre classe que celle qu'il a demandé. Pour que les choses marchent bien, il faut évidemment que la classe remplaçante étende la classe remplacée.

Peu après le même principe a été appliqué aux documents : en déclarant une injection de document on remplace son modèle, sa classe finale et son service là encore en renvoyant des instances de classes étendant celles que le code demande en réalité.

Globalement ça marche assez bien mais avec deux limitations :

  1. Dans le cas où on injecte une classe déjà étendue par ailleurs, les classes étendues ne profiteront pas de l'injection. Par exemple, si une classe C étend A et que j'injecte A par une classe B. Si je demande une instance de A, j'obtiendrai une instance de B mais si je demande une instance de C j'obtiendrai une instance de C étendant A et non B.

    Cette limitation peut rapidement s'avérer gênante quand on développe des modules complexes avec des documents de base étendus par d'autres (par exemple les différents types de produits dans le module Catalogue et boutiques).

  2. Une même classe ne peut être injectée qu'une seule fois. Cela peut devenir problématique si plusieurs modules veulent se greffer sur un module existant et ont besoin de remplacer du code : il faudra alors écrire une classe en spécifique dans le projet qui cumules les différentes modifications ce qui n'est pas très bon en terme de maintenabilité (on se rapproche à nouveau un peu de l'écrasement décrit plus haut même si ça reste plus restreint).

Points d'entrée spécifiques (stratégies, section "mvc" du project.xml, etc.)

Parallèlement à l'injection, d'autres mécaniques plus ciblées ont été mises en place dans certains modules pour permettre de remplacer du code standard par des implémentations spécifiques.

Les modules liés à l'e-commerce se sont ainsi enrichis d'un certain nombre de "stratégies" (du patron de conception strategy). Ces points d'entrée explicites permettent de choisir entre plusieurs implémentations pour un traitement donné. Par exemple on proposait une stratégie sur l'arrondi des prix avec une implémentation standard qui se contentait d'arrondir à deux chiffres après la virgule, là où certains projets avaient d'autres règles.

On trouve encore ce genre de chose en version 3.6, en particulier sur les points le choix n'est pas unique au sein d'un projet donné (par exemple le calcul du montant d'un frais : deux frais peuvent s'appliquer avec des stratégies de calcul différentes).

On en trouve encore quelques autres dont l'implémentation est choisie une fois pour toutes dans la configuration projet mais elles tendent à disparaitre (cette mécanique spécifiquement mise en place à chaque fois étant alors redondante avec les mécaniques génériques d'injection).

D'autres points d'entrée spécifiques ont été mis en place au fur et à mesure, notamment pour les différentes classes entrant en jeu dans le modèle MVC (notamment les différents contrôleurs) qui peuvent être remplacée via une section dédiée dans la configuration projet.

Dans tous les cas ces points d'entrée ont été mis en place spécifiquement dans certaines parties du code. Les mettre en place implique donc de prévoir volontairement que telle ou telle partie doit pouvoir être remplacé au contraire de l'injection qui fonctionne de manière plus globale.

Réécriture de classe et AOP

Dans la version 3.0 de Change, un nouveau mécanisme a été implémenté, tiré de certains concepts de la programmation orientée aspect (AOP).

Cette mécanique propose plusieurs choses reposant sur la réécriture des classes lors d'une phase de compilation.

Premièrement le remplacement d'une classe par une autre. Il s'agit là non pas de simplement retourner une instance d'une autre classe (comme c'était le cas avec l'injection décrite plus haut) mais bien de réécrire la classe pour y intégrer les modifications. Pour ce faire, on renomme la classe d'origine (avec un suffixe quelconque) puis on renomme la remplaçante avec le nom de l'originale. Ainsi le remplacement est totalement transparent pour le code qui l'entoure : il travaille bien physiquement avec une instance de la classe qu'il a demandé, à ceci près que cette classe a été modifiée.

Par exemple on part d'une classe A que l'on veut remplacer par une classe B (qui doit étendre A). On commence par renommer A en A_replaced0. Puis on renomme B en A et on la fait étendre A_replaced0. On obtient alors une nouvelle classe A qui a toutes les méthodes et propriétés de la classe B, y compris celles de la classe A d'origine.

Cette méthode fait sauter la limitation de l'injection décrite plus haut sur les classe étendues puisque du coup une classe C qui étendrait notre classe A d'origine héritera du même coup du code introduit par B. Dans son implémentation proposé dans la version 3.0 (qui reste la même en 3.6), elle ne fait par contre pas sauter l'autre limitation liée aux sources multiples.

Depuis la version 3.0, c'est sur cette mécanique que repose l'injection de documents et plus sur la mécanique décrite auparavant (ainsi une injection du document catalog/product se répercutera sur tous les documents qui l'étendent : catalog/simpleproduct, catalog/productdeclination, etc.).

Au delà de cette simple mécanique de réécriture (qui n'est pas de l'AOP à proprement parler mais juste un préalable), d'autres possibilités ont été mises en place permettant d'ajouter du code avant ou après l'exécution d'une méthode, d'ajouter une méthode à une classe ou d'en remplacer une (il s'agit là effectivement des concept de l'AOP). Là aussi on fait de la réécriture de classes mais avec l'avantage de pouvoir faire venir des modifications de plusieurs sources.

L'inconvénient est que dans les fait c'est un peu compliqué à comprendre et expliquer (le développeur web n'étant pas forcément familier avec ça) et pas forcément très lisible ni intuitif à écrire. Du coup finalement ça a été assez peu utilisé dans la pratique à part pour contourner la limitation sur les sources multiples ou bien pour agir sur des méthodes privées d'une classe (ce que permet dans une certaine mesure l’implémentation actuelle mais relève à mon sens un peu de l'hérésie... même si dans certains cas c'est tentant, réécrire des méthodes privées c'est réécrire du code qui n'est pas censé être accessible par l'extérieur et c'est très mauvais lors des mises à jour).

Injection de blocs

La dernière mécanique en date à avoir été mise en place est l'injection de blocs qui arrive avec la version 3.5.

Lorsqu'on insère un bloc dans une page, on ne référence pas directement un nom de classe mais un "type" (celui qui est indiqué dans le fichier blocks.xml du module) qui en pratique découle du nom de la classe. Mais a grosse différence avec les service c'est que le bloc n'est pas utilisé en tant que tel par du code des modules. Rien n'oblige donc au final à ce que le bloc finalement rendu soit réellement le bloc identifié par le type.

Lorsqu'on utilise un service on va faire appel à ses méthodes pour effectuer des traitements si une méthode manque ou n'a pas la bonne signature la plupart du temps ça finit avec une belle page dont le contenu commence par "Fatal error:". Lorsqu'on utilise un bloc, on va simplement lui demander de se rendre mais son implémentation importe peu en terme de réussite ou d'échec de l’exécution.

L'injection de bloc peut donc être nettement plus libre que l'injection de service ou le remplacement de classe. En effet, elle se contente de changer le mapping entre le "type" du bloc et la classe PHP effectivement utilisée sans obligation que la nouvelle classe étende la classe d'origine (du moment que ça reste un bloc). Si on reste sur un fonctionnement proche, on étendra le bloc d'origine mais si ce n'est pas le cas on peut aussi repartir d'une feuille blanche et le remplacer par une nouveau bloc totalement différent (voire étendant un autre bloc existant qui se rapproche plus de ce qu'on veut faire).

À venir en 4.0 : unification et simplification

Comme on a pu le voir, les méthodes de remplacement de code disponibles sont très nombreuses avec chacune leur limitations. De tout ce que j'ai pu évoquer, trois sortent du lot car elles apportent vraiment un plus en terme de possibilités (hors des limitation liées à l'implémentation). En effet toutes finalement visent à remplacer des classes par des classes qui les étendent une fois pour toutes globalement pour le projet à l'exception de :

  • l'injection de document est un peu à part puisqu'elle implique plusieurs classes (modèle, classe finale du document, service) mais au final reste du remplacement de classe
  • les stratégies dont le choix ne relève pas de la configuration projet mais se fait au cours de l'exécution selon le contexte (exemples : calcul de frais, calcul de réductions, etc)
  • l'injection de blocs qui n'impose pas de reprendre le code du bloc existant mais peut repartir de zéro dans une autre direction si le besoin s'en fait sentir

Toutes les autres (injection de service, remplacement de classe, AOP, stratégies définies dans la configuration projet, remplacement de classes spécifiques) répondent bien au même besoin : introduire du code spécifique dans une classe existante en ciblant au mieux la partie réécrite pour avoir le moins de problème possible lors des mises à jour. En version 4.0 ces différentes mécaniques parallèles seront donc supprimées au profit d'une seule et unique mécanique reprenant le meilleur de chacune.

Elle s'appellera "injection de classe" et pourra d'une manière générale être appliquée à n'importe quelle classe (à part une poignée qui sont chargées trop tôt dans l'exécution pour pouvoir en profiter). Elle reposera sur le concept de réécriture de classe (comme la mécanique de remplacement de classe) et sera déclarée dans la section injection sur le modèle de l'injection de service actuel.

Reste à couvrir un point que permettait l'AOP : injecter une classe avec du code provenant de plusieurs sources. Conceptuellement, rien ne l'empêche. C'est uniquement l'implémentation mise en œuvre dans les versions précédentes qui ne le permettait pas.

En version 4.0, on pourra donc déclarer des "injections chainées" en gros au lieu d'indiquer une seule classe de remplacement, on pourra en indiquer plusieurs séparées par des virgules. La mécanique de remplacement sera appliquée successivement dans l'ordre indiqué.

Par exemple, si une classe A est déclarée injectée par trois classes B, C et D (qui doivent toujours toutes étendre A), on obtiendra la chaine d'extension suivante : A_injected0 (anciennement A), A_injected1 (anciennement B) qui étend A_injected0, A_injected2 (anciennement C) qui étend A_injected1 et enfin A (anciennement D) qui étend A_injected2. De cette manière plusieurs modules peuvent proposer leur injections spécifiques sans exclure les autres.

Ceci évitera au développeur de se retrouver à devoir choisir entre une multitude de mécaniques proches avec chacune ses limitations pour n'utiliser toujours qu'une seule et même mécanique simple à comprendre et à mettre en œuvre.

Conclusion

Voilà, j'ai fini mon petit tour de l'historique des méthodes de surcharge. Je me suis concentré sur ce qui est lié directement aux classes PHP, omettant volontairement tout ce qui a trait aux templates, styles et fichiers de configuration des modules (déjà comme ça ça donne un sacré pavé).

Comme je le disais en introduction, je ne suis volontairement pas entré dans les détails de mises en œuvre, pour cela référez-vous à la documentation de Change ou posez vos questions sur le forum.

Comme je l'ai dit également, la dernière partie concernant la version 4.0 ne reflète que l'état actuel des développement et pourrait éventuellement évoluer encore un peu d'ici la sortie. Mais globalement les grandes lignes devraient rester valables. On notera du coup qu'il n'est pas conseillé d'abuser de l'AOP dans un projet actuel car cela nécessitera beaucoup de boulot de migration vers la version 4.0 au contraire d'un remplacement de classe qui reste le plus proche de ce qu'on aura au final (c'est d'ailleurs pour cela que l'AOP n'a pas été documentée dans le wiki).

Voilà, félicitations à ceux qui liront mon pavé jusqu'au bout... à supposer qu'il y en ait :euh:


Quelques trucs sur PHP #3

Auto-complétion du code avec PDT

PDT est un IDE pour PHP basé sur Eclipse relativement performant. Notamment il permet l'auto-complétion de code, ce qu'on n'a que rarement avec des éditeurs plus légers.

Sous certaines conditions (notamment avec le plugin SVN), l'auto-complétion n'est pas activée par défaut. Il y a peut-être moyen de le régler dans le labyrinthe qui sert de système d'options à Eclipse mais sinon, un moyen simple reste d'ajouter la ligne suivante dans la section natures du fichier .project du projet :

<nature>org.eclipse.php.core.PHPNature</nature>

Flux RSS et format de date

Pour formater une date en vue de générer un flux RSS, il y a plusieurs méthodes :

  1. chercher le détail du format sur le net et l'appliquer... c'est possible mais risque d'erreur.
  2. utiliser le format ‘r’ qui renvoie le bon format.
  3. utiliser la constante DateTime::RFC822... sauf que là le flux ne sera pas forcément valide et qu'il y aura potentiellement des problèmes avec certains agrégateurs.

Préférez donc la seconde solution, celle-là en principe elle marche (et en bonus c'est la plus compacte, que demander de plus ?).

Méthodes privées en PHP 5.1.6

Le principe d'une méthode privée c'est d'être interne à la classe et aucunement visible ou surchargeable depuis une classe qui l'étend (contrairement à une méthode protégée). Cependant dans certaines versions de PHP, dont la 5.1.6 (je ne sais pas quand exactement il a été corrigé mais il n'est plus présent dans les dernières 5.2.x), permet de surcharger les méthodes privées d'une classe... Ça peut arriver involontairement et ne prête pas à conséquence, sauf quand par la suite vous tentez d'exécuter votre script sur une version corrigée de PHP et là c'est pas évident de comprendre pourquoi tout d'un coup on ne passe plus dans la surcharge !

L'angoisse de la page blanche #2

J'évoquais ici le fait qu'on risquait une page blanche sans autre forme de procès en déclarant deux fois une même méthode dans une même classe et en voici un autre : déclarer comme abstraite une méthode d'une interface.

C'est peut être une déformation due au fait qu'on me l'a présenté comme ça lors de mes premiers cours de programmation objet Java mais pour moi une interface c'est pousser à l'extrême le concept de la classe abstraite : toutes les méthodes sont abstraites (et on n'a pas de champs). En Java, il me semble bien me souvenir que dans le cas d'une interface, le mot-clé abstract sur une méthode est facultatif (vu que de toutes façons toutes les méthodes d'une interface le sont). Eh bien en PHP il n'est pas facultatif mais interdit. Sinon, page blanche (du moins sous certaines versions/configurations) et bon courage pour retrouver l'erreur \o/


Petit bench sur la recherche dans un tableau PHP

Préambule...

Hier, j'avais à parcourir un tableau d'objets (pouvant avoir potentiellement des centaines voire exceptionnellement milliers d'entrée) pour rechercher si l'identifiant de l'un d'eux se trouvait dans un second tableau. J'avais commencé par utiliser pour ça la fonction in_array() à chaque itération pour voir si l'identifiant de l'objet était présent ou non dans le second tableau.

En voyant cela, un collègue m'a fait remarquer que ce serait peut-être plus performant de construire un tableau dont les clés sont les valeurs du second tableau (via array_flip()) pour pouvoir utiliser isset() au lieu de in_array() et voici les résultats obtenus :

Structure du bench

Le bench consiste à rechercher 100 000 fois la même valeur dans le tableau array('11345', '7437', '7329', '45494', '7894311', 'sdfsdg', 'qsqsdcirt', 'd787 sdfs df'), avec trois méthodes différentes :

  • in_array()
  • array_flip() suivi de isset()
  • array_flip() suivi de array_key_exists()

Le test est effectué avec deux valeurs différentes : d'abord avec la première valeur du tableau (cas théoriquement le plus favorable puisqu'on arrête la recherche une fois la valeur trouvée) puis avec une valeur qui n'est pas dans le tableau (cas théoriquement le plus défavorable puisqu'on est obligé de parcourir tout le tableau). Le résultat en conditions réelles sera donc compris dans cette fourchette.

Cas in_array()

Code exécuté :

for ($i = 0; $i < 100000; $i++)
{
	in_array($value, $values);
}

Cas favorable ($value = '11345') : ~0.33 secondes
Cas défavorable ($value = 'uottuyi') : ~0.52 secondes

Cas isset()

Code exécuté :

$keys = array_flip($values);
for ($i = 0; $i < 100000; $i++)
{
	isset($keys[$value]);
}

Cas favorable ($value = '11345') : ~0.12 secondes
Cas défavorable ($value = 'uottuyi') : ~0.09 secondes

Cas array_key_exists()

Code exécuté :

$keys = array_flip($values);
for ($i = 0; $i < 100000; $i++)
{
	array_key_exists($value, $keys);
}

Cas favorable ($value = '11345') : ~0.27secondes
Cas défavorable ($value = 'uottuyi') : ~0.24 secondes

Et pour de plus petites quantités ?

Les grands volumes c'est bien mais qu'est-ce que ça donne quand on a peu d'itérations ?

Un test à 5 itérations au lieu de 100 000 donne environ le même résultat pour les trois méthodes : avec ~6E-05 secondes pour les méthodes 1 et 3 et ~5E-05 pour la méthode 2.

Tandis qu'un test sur une unique itération donne la première méthode gagnante avec ~4E-05 secondes contre ~5E-05 pour les deux autres (à ce niveau c'est le array_flip pour transformer les valeurs en clés qui coute cher).

Conclusion

À moins d'avoir toujours très peu d'itérations (moins de 5), la méthode passant par array_flip() puis isset() est d'assez loin la meilleure (environ quatre fois plus rapide sur des grands nombres et pas plus lente sur des petits).

En passant, on remarque aussi qu'avec cette méthode, rechercher une valeur qui n'existe pas dans le tableau est plus rapide que de rechercher la première valeur du tableau, même si je ne vois pas forcément trop pourquoi :pense:


Quelques trucs sur PHP #2

Une deuxième série de petits trucs sur PHP...

Page blanche

Plus j'utilise PHP, plus je me rends compte qu'il y a quand même des trucs bien foireux dedans... Notamment ceci : lorsqu'une classe contient deux définitions de la même méthode (du moins dans certains cas, j'ai pas trop approfondi pour voir si c'est vraiment systématique), on n'obtient pas d'exception, ni même la traditionnelle "fatal error" non-catchable et sans trace, mais bel et bien une page blanche sans aucune explication \o/ J'imagine que derrière PHP doit mourir lamentablement sur un "Segmentation fault"... Bref, quand vous obtenez une page blanche, pensez à vérifier si vous n'avez pas raté un copier/coller quelque part...

Duplication de tableaux

Après avoir tenté en vain de dupliquer un tableau avec le mot-clé clone (qui retourne null), j'ai cherché un peu et je suis tombé sur cet article. Donc apparemment une simple affectation suffit à dupliquer un tableau (ce qui explique au passage certaines choses concernant la quantité astronomique de mémoire qu'arrive à bouffer PHP dans certains cas) ! Comme quoi même en utilisant un langage pendant des années, on peut passer à côté de certains trucs de base...

L'instruction continue

Je parlais il y a quelque temps de l'instruction break qui admet un paramètre permettant de sortir de plusieurs boucles imbriquées d'un coup, eh bien l'instruction continue se comporte de la même façon.

Je n'avais pas insisté dessus à l'époque (parce que dans le cas du break, c'est évident) mais dans les deux cas, un switch est considéré comme une boucle. Donc si vous êtes dans un switch contenu dans une boucle et que vous voulez passer à la prochaine itération de la boucle il faut appeler un continue 2;.

Pour plus de détails sur l'instruction continue, rendez-vous sur le manuel officiel de PHP.