Récemment, je me suis posé pour réfléchir autour des solutions que propose Twig pour accéder aux propriétés d’un objet ou d’un tableau.
Accéder à une propriété d’un objet
Twig a été conçu pour simplifier nos templates, aussi bien en terme de code qu’en terme de propreté. Il a également été conçu pour permettre aux intégrateurs, personnes n’ayant pas tous de connaissances en développement, un accès facile aux propriétés d’un objet ou autres. Cela grâce à une syntaxe simplifiée.
Seulement voilà, étant développeur avant-tout, je me suis posé la question de comment twig faisait pour déterminer quelle méthode d’un objet doit être appelé.
Syntaxe Twig
Dans mon code suivant, je partirai du principe que j’utilise une classe comme ci-dessous.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
classe Objet { privé Nom ; public Nom d’utilisateur ; public fonction getName() { retour $this->nom ; } public fonction getUsername() { retour $this->nom d’utilisateur ; } } |
Voici comment j’appellerai mon template depuis un contrôleur Symfony.
1 2 3 |
public fonction indexAction() { retour tableau( » objet « => ; $object) ; } |
Jusque là, tout est clair. Mon objet ressemble parfaitement aux objets que je peux utiliser dans mes projets.
Analyse des déclarations dans Twig
À partir de maintenant nous allons voir comment Twig travaille avec nos appels. Ici, on demande à Twig de prendre la valeur nom de mon objet, sauf que pour Twig qui n’est qu’un simple appel PHP au final, cette variable est inaccessible. Twig a donc ajouter un proxy permettant de faire abstraction et de regarder quelles sont les propriétés et méthodes disponibles.
Twig va donc demander dans l’ordre ci-dessous de faire des vérifications sur l’objet.
- Regarde si objet est un tableau et si nom est une clé ;
- Regarde si objet est un objet et si nom est un attribut accessible ;
- Regarde si objet est un objet et si nom est une méthode ;
- Regarde si objet est un objet et si getName est une méthode ;
- Regarde si objet est un objet et si isName est une méthode.
Ici, avec la notation que nous avons utilisée, il nous a fallu attendre la 4ème condition pour parvenir à notre élément. Car nom n’est pas un attribut accessible, ni une méthode.
Dans ce cas, nous essayons d’accéder à nom d’utilisateur. Cet attribut est un attribut public (dans Symfony, j’ai rarement rencontré ce cas). Il nous faut attendre la 2ème condition.
Dans notre troisième cas, je tente de faire appel à ma méthode de manière directe. Je récupère mon résultat dès la troisième condition, soit une condition plus rapide.
1 |
{{ object.getUsername() }} |
Dans notre quatrième et dernier cas, je tente de faire appel à ma méthode de manière directe. Je récupère mon résultat dès la troisième condition. Dans ce cas, je mets une condition supplémentaire pour accéder à ma valeur.
Interprétation des templates Twig
Lorsque je me suis penché sur ce détail, j’ai réfléchi à comment les développeurs de Twig avait pu réaliser la mise en cache et la compilation des templates. Il ne me fallut pas beaucoup de temps pour en venir à l’évidence, qu’avec le peu d’information et la liberté qu’offre le gestionnaire de template, nos fichiers sont simplement transcrit en PHP d’une manière similaire à ce que l’on pourrait tout à fait faire. Vous pouvez d’ailleurs constater par vous même en cherchant dans votre dossier de cache.
1 |
echo twig_escape_filter($this->env, $this->getAttribute((isset($context[« object »]) ? $context[« object »] : $this->getContext($context, » object « )), » name « ), » html « , null, true) ; |
Le premier bloque correspond à ce que j’ai noté dans mon template twig, le deuxième bloque à la version traduite que j’ai trouvé dans le dossier de cache.
On remarque bien que le twig a été traduit en PHP mais qu’aucun élément ne lui a permis de prédire quelle méthode devrait exactement être appelée. La méthode twig_escape_filter pourrait être comparé à un proxy. C’est dans cette méthode qu’on va déterminer comment on accède à l’attribut.
Conclusion
Malgré que les templates twig soit mis en cache automatiquement par Symfony, seul la version PHP est gardé mais sans interprétation de ce que l’on souhaite récupérer. En théorie, il y a donc ici, le moyen d’optimiser des appels et le temps de génération de nos templates car la vérification est effectuée à chaque appel.
Benchmark
J’ai quand même voulu avoir une idée concernant les gains qui peuvent être fait en appelant les méthodes plutôt que des » alias « .
Dans un premier cas, j’appelle un template qui va appeler 10 fois un même objet qui appelle à son tour 25 alias. Cela représente 250 appels. Les résultats sont grossis par la boucle de 10 pour permettre de faire des calculs juste quant au gain de performance.
Dans un second temps j’appelle un template qui va appeler 10 fois le même objet et qui appelle à son tour 25 méthodes (toujours via le même proxy que pour les alias). Cela représente 250 encore une fois.
J’ai réalisé l’appel de ces templates 5 fois chacun.
On remarque que l’intégralité des appels au template faisant appels à des méthodes est plus rapide. En faisant les moyennes, on remarque que le template faisant appels aux alias est plus long de 54,6 millisecondes (197.4 – 142.8).
En faisant un rapide calcul, on remarque que si on ramenait ça à un cas général, le template utilisant des appels à des méthodes est plus rapide en moyenne sur des mêmes données de approximativement 26.7%. Cela peut être intéressant lorsqu’on réalise de nombreux appels à des objets.
Ce deuxième article fait suite à un premier post donnant des solutions rapides pour optimiser la génération des templates. Il est le fruit d’optimisation déjà utilisé sur des sites en production qui possédait un taux de latence trop important.
Nous utilisons tous les inclut de Brindille pour déporter ou factoriser nos vues. Mais peut-être n’avez-vous jamais compris pourquoi nous avions la possibilité de passer des paramètres, alors qu’un include de base hérite des variables du contexte en cours.
1 |
{% inclure » ProjectMyBundle:Sub:template.html.twig » %} |
Souvent nous utilisons cette notation. Dans ce cas, l’include donnera une copie de toutes nos variables à notre template. Cela n’est pas toujours nécessaire, et nous avons tort de copier l’intégralité de nos variables.
Intérêt de l’option » only «
Je me suis longtemps demandé qu’elle était l’intérêt de cette option. D’ailleurs il faut dire que la documentation ne donne pas beaucoup d’élément. Vous pouvez aller jeter un coup d’œil sur la documentation des includes Twig. Elle donne peu d’élément mais elle donne surtout un inconvénient et aucun avantage : se passer de certaines variables. Vue de cette manière, cela n’est pas rentable ; pourquoi je devrais me passer de certaines variables.
Mettons en œuvre nos includes ! Admettons que notre template de base ait 5 variables et que j’inclue un template qui n’utilise qu’une seule de ces variable, voilà la notation qui sera préférable.
1 |
{% inclure » ProjectMyBundle:Sub:template.html.twig » with { » object » : myVar} only %} |
De cette manière seule une variable » object » sera disponible dans mon template. Cela limite la copie de variable inutile, et donc d’allocation de mémoire (gain de temps et de consommation mémoire).
Code et cas d’utilisation
1 2 3 4 5 6 |
public fonction testAction() { $objects = $this->container->get( » doctrine.orm.default_entity_manager « )->getRepository( » ProjetMonBundle:Test « )->findAll() ; retour tableau( » objets « => ; $objects) ; } |
Nous effectuons une boucle sur notre collection et nous donnons un template chaque itération de la collection au template. Dans ce cas, chaque fois que l’on fait appel à l’intégration d’un template, l’intégralité des variables sont copiées ainsi que celles qui sont passées avec l’option » with « . Dans notre premier cas on pourrait très bien imaginer accéder à notre collection (objets) ainsi que notre itération en cours (objet).
1 2 3 4 5 |
{% pour object in objects %} {% inclure » ProjectMyBundle:Sub:template.html.twig » avec { » object » : objet} %} {% endfor %} |
Dans notre deuxième cas, j’active l’option seulement ce qui permet de copier uniquement les variables passées en paramètres. Facile ?
1 2 3 4 5 |
{% pour object in objects %} {% inclure » ProjectMyBundle:Sub:template.html.twig » with { » object » : object} only %} {% endfor %} |
Point de repère
J’ai réalisé les tests avec les templates et le code donnés dans la partie ci-dessus. J’ai effectué avec 5 itérations de chaque template, en pensant à vider le cache avant chaque premier test.
Avec ce graphe, on voit bien le chargement est globalement plus long pour ceux qui n’utilisent pas l’option seulement. La différence a tendance à se réduire après la mise en cache. Cela est dû au fait que mon template est petit et que peu de variable sont utilisés. Dans des cas plus appliqués on note des gains pouvant aller jusqu’à 30%.
Ici, si on fait une moyenne des valeurs, on atteint une différence moyenne de 9 ms (48,6 – 39,6 = 9). On peut donc calculer un gain approximatif de 20% même si cela est à relativiser dans notre cas puisque le premier shot est terriblement long sans l’utilisation de » seulement « .