Je profite de la semaine des générateurs organisée par Pascal Martin pour vous faire un retour sur l’usage sur decitre.fr de cette fonctionnalité super pratique.
Depuis juin 2013 et l’arrivée PHP 5.5, nous avons la possibilité d’utiliser les générateurs dans nos codes.
Récemment, nous avons eu le besoin de construire une commande CLI afin de générer les visuels de notre catalogue produit.
Pour cette commande, nous avons identifié différents points d’entrée possibles pour récupérer nos produits :
l’identifiant en autoincrement du produit,
l’identifiant naturel du produit (son SKU),
un fichier contenant une liste de SKU,
une date de référence pour récupérer les produits modifiés depuis cette date.
Pour préciser un peu plus le contexte de cette commande, nos produits sont stockés dans un Solr, tandis que les informations concernant les dates d’intégration des images restent dans notre base MySQL de référentiel produit, ce qui implique de réaliser la récupération du produit en 2 étapes (MySQL puis Solr).
De plus requêter Solr sur un grand nombre de produits coûte cher en mémoire et en ressource CPU (décodage du json), c’est pourquoi nous préférons faire une requête par produit.
La méthode “old school”
En reprenant les structures de base php (quand on est fan des array ou lorsqu’on ne connaît pas encore les Generator), le premier réflexe serait de remplir un tableau de produits, de le remplir en fonction des critères dont on a besoin, et de boucler dessus pour effectuer les opérations souhaitées.
SPOILER ALERT
N’hurlez pas tout de suite pour le non-découpage des méthodes :)
Voici l’exemple de code tel qu’on aurait pu le faire il y a quelques années :
L’inconvénient de cette méthode est que tous les objets doivent être chargés en mémoire. Si on doit travailler sur un nombre important de produits, le tableau va tous les contenir et on risque d’avoir une memory limit.
Bonne nouvelle : qui dit boucle dit, depuis PHP 5.0, Iterator.
On peut donc construire un objet implémentant Iterator qui pourra être parcouru et chargé à la demande, afin de réduire la charge mémoire.
Sauf que :
il faut créer un nouvel objet pour gérer cela,
à chaque méthode de récupération des produits, il faut être capable de déterminer où reprendre dans la liste.
Bref, l’Iterator est une solution possible, mais qui, dans ce cas, nécessitera d’écrire plusieurs classes et beaucoup de code, compte tenu des différentes sources de chargement (Solr, requête SQL, fichier de sku) et de la volonté de ne pas charger tous les objets en mémoire, mais au fur et à mesure.
Bonne nouvelle (bis) : qui dit boucle dit, depuis PHP 5.5, Generator.
Mais c’est quoi un générateur ? La documentation de php va fournir l’explication :
Les générateurs fournissent une façon simple de mettre en place des itérateurs sans le coût ni la complexité du développement d’une classe qui implémente l’interface Iterator.
Un générateur vous permet d’écrire du code qui utilise foreach pour parcourir un jeu de données, sans avoir à construire un tableau en mémoire pouvant conduire à dépasser la limite de la mémoire ou nécessiter un temps important pour sa génération. Au lieu de cela, vous pouvez écrire une fonction générateur, qui est identique à une fonction normale, mis à part le fait qu’au lieu de retourner une seule fois, un générateur peut utiliser yield autant de fois que nécessaire, afin de fournir les valeurs à parcourir.
En reprenant le premier exemple, il suffit de remplacer l’assignation dans le tableau $products par le mots clé yield pour retourner au fur et à mesure les objets
Et voilà : notre méthode est désormais capable de boucler sur des objets produits, chargés au fur et à mesure, sans risque de memory limit et tout ça avec un refactoring très simple et n’impactant pas les appels effectués : la consommmation de la méthode continue de se faire grâce à un foreach.
Et PHP 7 dans tout ça ?
Pour ceux qui ne le savent pas encore, PHP 7 est sorti depuis le début du mois de décembre et apporte de nombreuses évolutions. Les générateurs n’ont pas été oublié parmi la liste des nouveautés et des évolutions, notamment suite à cette RFC.
Il est désormais possible de déléguer le générateur à un autre sous ensemble en faisant les yield en cascade, de manière à mieux découper et séparer le code, comme le montre ce portage du précédent exemple.
Pour finir
Avec 5000 produits pour lesquels les visuels doivent être générés, notre script avec le tableau fourre-tout a un pic de consommation mémoire de près 300M.
Lorsqu’on utilise les générateurs, le pic passe à 20M et reste stable quelque soit le volume de produit impacté.
Si vous êtes confrontés à des collections dont la charge mémoire est trop forte, ou plus simplement si vous êtes à la recherche d’une syntaxe facile à écrire et efficace, pensez aux Generator.
Merci à Pascal d’avoir organisé cette semaine d’articles :)