Page d'accueil Mes articles

Pourquoi et comment faire de l'open source ?

Au travers d'un retour d'expérience sur une petite contribution concernant une résolution de bug, je vous présente ma façon de contribuer à l'open source en mode Escape Game 🧐 😁

Publié en December 2021.
Dernière mise à jour February 2022.

Pourquoi contribuer ?

Les langages et frameworks que j'utilise quotidiennement sont open source. Ils sont le fruit d'un travail collaboratif de développeurs intervenant pendant leur temps libre. Certaines solutions que j'utilise sont portées seulement par une communauté, la plus connue étant PHP. D'autres projets sont soutenus par des sociétés. On peut prendre l'exemple de Sensiolabs pour Symfony ou encore Google pour Angular.

Lorsque c'est l'open source qui nous permet de remplir le frigo, je pense que c'est pas mal de contribuer à sa pérennité. Pour cela, plusieurs possibilités : On peut par exemple sponsoriser des projets, ou directement des contributeurs open source, via la plate-forme Github. Une autre possibilité est simplement contribuer aux outils que nous utilisons en aidant les communautés. Pour ce faire, on peut participer à des revues de code, contribuer à l'amélioration de la documentation ou encore proposer des corrections de bugs voir des nouvelles fonctionnalités.

Personne ne vous oblige à contribuer à l'open source. Il est tout à fait possible de rester dans un rôle d'utilisateur. Gardez seulement à l'esprit que les fonctionnalités proposées par la solution open-source que vous utilisez répondent à un besoin commun et non individuel. Autrement dit, si vous avez un besoin très spécifique à votre métier, il y a très peu de chances que quelqu'un fasse le travail à votre place sur GitHub. Si vous rencontrez un bug, la bonne démarche est de le signaler sur GitHub en expliquant clairement le scénario pour que les membres de la communauté puissent reproduire le problème.

Ce genre de discours est fréquemment répété, vous l'avez sûrement déjà entendu lors de conférences. Peut-être avez-vous la volonté de contribuer mais vous ne savez pas comment vous y prendre ? Et bien j'espère que cet article va vous permettre d'y voir plus clair. 😄

Réparer un bug dans vos dépendances

Vos mains tapotent sur le clavier et les lignes de code s'enchainent. Le café du matin fait son effet et votre projet avance de façon très fluide. Avec du bon son dans les oreilles, vous kiffez votre métier... et tout d'un coup, boum 💥 !

Non-static method Liip\ImagineBundle\Templating\LazyFilterRuntime::filter() cannot be called statically

C'est exactement ce qui m'est arrivé lorsque je travaillais sur mon blog un week-end de novembre. Je ne m'y attendais pas et clairement ce n'était pas prévu au programme de la journée. Une erreur fatale qui m'explique qu'on ne peut pas appeler une méthode non statique de façon statique. Effectivement, nous n'avons pas le droit de faire ça depuis la version 8. Sur PHP 7, le code s’exécutait mais cela générait un "deprecated". Premières impressions à chaud, le bug doit venir de LiipImagineBundle ... le code s'exécutait sur PHP 7 mais plus sur PHP 8. Mais je mets quand même très rapidement cette hypothèse de côté. Ce bundle est maintenu par David Buchmann et ce gars est l'incarnation de la rigueur. 🤩

Je regarde alors la pile des erreurs pour voir où l'exception est déclenchée.

// easycorp/easyadmin-bundle/src/Twig/EasyAdminTwigExtension.php (line 80) 
return $filter->getCallable()($value, ...$filterArguments);

J'ouvre ce fichier présent dans mes vendors et je découvre que c'est le filtre ea_apply_filter_if_exists sur EasyAdminBundle qui pose problème. Je lance une recherche dans mes vendors pour savoir où ce filtre est utilisé.

❯ grep -nr "ea_apply_filter_if_exists" .
./easycorp/easyadmin-bundle/src/Resources/views/crud/form_theme.html.twig:361:                        <img style="cursor: initial" src="{{ (asset_helper is same as(true) ? asset(image_uri) : image_uri)|ea_apply_filter_if_exists('imagine_filter', formTypeOptions.imagine_pattern) }}">
./easycorp/easyadmin-bundle/src/Resources/views/crud/form_theme.html.twig:371:                        <img src="{{ (asset_helper is same as(true) ? asset(download_uri) : download_uri)|ea_apply_filter_if_exists('imagine_filter', formTypeOptions.imagine_pattern) }}">
./easycorp/easyadmin-bundle/src/Resources/views/crud/form_theme.html.twig:379:                        <img src="{{ (asset_helper is same as(true) ? asset(download_uri) : download_uri)|ea_apply_filter_if_exists('imagine_filter', formTypeOptions.imagine_pattern) }}">
./easycorp/easyadmin-bundle/src/Twig/EasyAdminTwigExtension.php:39:            new TwigFilter('ea_apply_filter_if_exists', [$this, 'applyFilterIfExists'], ['needs_environment' => true]),

C'est plutôt une bonne nouvelle car les thèmes de formulaire Twig se surchargent très facilement sur EasyAdmin. Et vu que je ne compte pas passer tout mon week-end à coder, la solution est toute trouvée. Je suis la documentation et je surcharge le block vich_image_widget en changeant ea_apply_filter_if_exists('imagine_filter', formTypeOptions.imagine_pattern) par imagine_filter( formTypeOptions.imagine_pattern ) puisque dans mon cas le filtre existe dans la mesure où j'ai installé LiipImagineBundle.

Image correctement affichée, le bug est réparé

Ça fonctionne ! Le bug est réparé 🥳 ! ... enfin, pas vraiment. Disons que j'ai trouvé une rustine pour contourner le bug. Je colle alors un post-it sur mon bureau en me disant que je verrai ça un soir dans la semaine.🔖

Poser le diagnostic

Suis-je le seul à rencontrer ce bug ? Mon site Internet a bientôt presque un an, qu'ai-je changé récemment ayant pu provoquer ce bug ? Étant donné que le problème concerne LiipImagineBundle, je regarde de plus près les modifications de configuration que j'ai pu effectuer.

# liip_imagine.yaml
...
    twig:
        mode: 'lazy'

Et je découvre que j'ai rajouté ces quelques lignes dans liip_imagine.yaml suite à l'apparition du deprecated suivant

Texte alternatif

Je supprime ce mode "lazy", désactive temporairement ma rustine, et le bug disparaît. Je décide alors d'aller consulter cette classe FilterExtension générant le deprecated. J'y trouve une méthode getFilters permettant de fournir le filtre qui m'intéresse, à savoir imagine_filter.

Dans ce même répertoire Templating est présent le fichier LazyFilterExtension.php ... Celui qui ne peut pas être appelé de façon statique lorsque j'active le mode Lazy.

FilterExtension est déprécié depuis la version 2.7 et sera supprimé dans la version 3.0. Configurez twig_mode en lazy

Je décide alors d'aller consulter la documentation au sujet de la création d'extension Twig . En parcourant les différentes versions, je découvre que cette notion de Lazy Extension est présente sur Symfony depuis la 3.4. Si l'on souhaite créer une extension sur Twig n'ayant pas de dépendances à des services, la création peut se faire de façon classique. Par contre, si notre extension fait appel à des services pouvant être gourmand en ressources, il est plus pertinent de créer une extension en mode lazy. Twig initialisera alors l'extension uniquement lorsque l'on fera appel au filtre. Dans le cas de notre extension pour LiipImagineBundle, elle doit faire appel à un service CacheManager pour fonctionner. Cette dépréciation est totalement cohérente avec les bonnes pratiques.

J'y vois plus clair 😁, et je peux maintenant être certain que le problème ne vient pas de LiipImagineBundle, mais plutôt de la façon dont l'extension est récupérée via le filtre ea_apply_filter_if_exists de EasyAdmin.

Tenter une correction

La première chose à faire est de forker le dépôt de EasyAdmin sur GitHub. Vous avez trois actions possibles sur les dépôts publics, vous pouvez surveiller un dépôt "Watch", mettre en favoris un projet "Star" ou bien faire une copie du code qui vous intéresse vers votre compte Github "Fork".

Le dépôt EasyAdminBundle est forké sur mon environnement github

Une fois forké, nous n'avons plus qu'à récupérer le projet à l'aide d'un git clone et le tour est joué. J'ouvre alors deux VSCode, l'un avec mon projet et l'autre avec EasyAdmin. L'idée maintenant est de pouvoir utiliser le code que je viens de cloner plutôt que celui présent dans mes vendors. La solution la plus simple et la plus rapide est de supprimer easyadmin-bundle de mes vendors et de le remplacer par un lien symbolique pointant vers le projet que je viens de cloner. Ce qui est génial avec Composer, c'est qu'il est possible de faire cette action automatiquement en modifiant le fichier composer.json

...
"require": {
    ....
    "easycorp/easyadmin-bundle": "@dev",
    ....
},
"repositories": [
    {
        "type": "path",
        "url": "/Users/yoann/dev/EasyAdminBundle"
    }
]
...

Ensuite, lors de votre composer update le lien symbolique sera créé automatiquement.

  - Removing easycorp/easyadmin-bundle (v3.5.14)
  - Installing easycorp/easyadmin-bundle (dev-master): Symlinking from /Users/yoann/dev/EasyAdminBundle

Mon environnement de travail est prêt, il ne me reste plus qu'à comprendre comment cette méthode devrait fonctionner. Pour cela, je vais utiliser une fonction très pratique sur Symfony : dump ! 🤩

if (false === $filter = $environment->getFilter($filterName)) {
    return $value;
}

Je commence par regarder cette première méthode getFilter du projet Twig et je reste perplexe. Cette méthode retourne soit TwigFilter soit null... mais jamais false. En regardant sur le GitHub de Twig, je découvre que le typage a changé entre deux versions. Pas cool, mais en même temps nous ne devrions pas utiliser cette méthode puisqu'elle est taguée en @internal. Je creuse rapidement dans le projet de Twig pour voir comment contourner cette méthode et donc récupérer le filtre d'une autre façon, mais je ne trouve pas de moyen simple 🤔. Je décide donc pour le moment de laisser ça de côté, je transforme l'opérateur d'égalité stricte en un opérateur d'égalité classique de façon à ce que à la fois false et null soient pris en compte. Je signalerai cette histoire de méthode interne lors de ma Pull Request.

Sur Symfony, on adore les interfaces 💚 ! Elles permettent notamment de typer nos services. En parcourant le code des extensions twig de LiipImagineBundle, je comprends rapidement qu'une extension paresseuse est identifiable par l'implémentation de l'interface RuntimeExtensionInterface. Une extension classique, elle, implémentera ExtensionInterface.

Pour ne plus avoir mon erreur fatale, je décide de rajouter une condition afin de m'assurer que la classe implémente bien l'interface ExtensionInterface

list($class, $method) = $filter->getCallable();
if ($class instanceof ExtensionInterface) {
    return $filter->getCallable()($value, ...$filterArguments);
}

Par contre, pour exécuter mon extension configurée en mode lazy, je passe un peu plus de temps à trouver 🤨. En parcourant la classe Environment de Twig je finis par découvrir une méthode getRuntime dont la définition est "Returns the runtime implementation of a Twig element (filter/function/tag/test)". C'est exactement ce dont j'ai besoin !

$object = $environment->getRuntime($class);
if ($object instanceof RuntimeExtensionInterface && method_exists($object, $method)) {
    return $object->$method($value, ...$filterArguments);
}

Et là, victoire 🎉🎉 ! Mon code fonctionne parfaitement sans la rustine que j'avais mis au début. Je lance ensuite une recherche dans le répertoire tests pour voir si la méthode est testée afin de faire évoluer le test.

❯ grep -nri "applyFilterIfExists" tests

Rien... 🙄 Je me dis que ça serait pas mal d'en rajouter un, mais qu'en même temps il est 1h30 du mat' et on est en pleine semaine 😅 . Je verrai en fonction des retours que je pourrais avoir sur la PR. Je crée alors une branche qui suit master et je commit et push sur mon dépôt GitHub.

❯ git checkout -b fix_lazy_filter_imagine

Ensuite, je me rends sur Github sur la page du projet EasyAdminBundle. On me propose de créer une Pull Request à partir de ma branche fix_lazy_filter_imagine.

Je prends le temps d'expliquer en détail ma correction et le scénario pour reproduire le bug. La personne qui va me relire n'a pas forcément beaucoup de temps à consacrer à cette tâche. L'idée est donc d'être le plus clair possible pour éviter les incompréhensions. Javier merge ma Pull Request dès le lendemain en y apportant une modification au niveau de la comparaison. Effectivement, je trouve également plus propre de faire deux comparaisons stricts, l'une avec null et l'autre avec false, plutôt qu'une comparaison simple de valeur.

Le correction a été ensuite embarqué dans la release 3.5.15 quelques semaines plus tard.

En conclusion

Si vous n'avez pas encore contribué à des projets open source, j'espère que cet article vous aura donné envie de sauter le pas. Personnellement, j'aime beaucoup m'aventurer dans du code que je ne connais pas. Je trouve qu'il y a un petit côté "Escape Game" 🧐, il faut trouver tous les indices, comprendre la logique, résoudre le problème et ainsi réussir le défi ! En plus de ça, cela nous permet réellement de progresser dans notre métier de développeur.

Cet article vous a été utile ? Faites le connaître sur les réseaux sociaux Twitter twitter LinkedIn linkedin

Un commentaire ?

codeur
Ou connectez-vous avec GitHub