Symfony – Best Bundles

En tant que développeur Symfony, j’essaye lorsque c’est possible, de ne jamais réinventer la roue. Cela tombe bien car Symfony et sa vaste communauté, offrent de nombreux Bundles très bien faits répondant aux besoins les plus courants notamment en terme d’implémentation de bonnes pratiques.

Devenez un expert en utilisant les « Best Bundles » Symfony

The FOSUserBundle adds support for a database-backed user system in Symfony2. It provides a flexible framework for user management that aims to handle common tasks such as user registration and password retrieval.

This bundle provides various tools to rapidly develop RESTful API’s & applications with Symfony2.

Elastica and ElasticSearch integration in Symfony2

This Bundle provides a Symfony2 authentication provider so that users can login to a Symfony2 application via Facebook. Furthermore via custom user provider support the Facebook login can also be integrated with other data sources like the database based solution provided by FOSUserBundle.

JMSDiExtraBundle adds more powerful dependency injection features to Symfony2

This bundle allows you to create i18n routes.

This bundle adds AOP capabilities to Symfony2.

If you haven’t heard of AOP yet, it basically allows you to separate a cross-cutting concern (for example, security checks) into a dedicated class, and not having to repeat that code in all places where it is needed.

In other words, this allows you to execute custom code before, and after the invocation of certain methods in your service layer, or your controllers. You can also choose to skip the invocation of the original method, or throw exceptions.

XHProf is a hierarchical profiler for PHP. It reports function-level call counts and inclusive and exclusive metrics such as wall (elapsed) time, CPU time and memory usage. A function’s profile can be broken down by callers or callees. The raw data collection component is implemented in C as a PHP Zend extension called xhprof. XHProf has a simple HTML based user interface (written in PHP). The browser based UI for viewing profiler results makes it easy to view results or to share results with peers. A callgraph image view is also supported.

This bundle is a fork of AvalancheImagineBundle which provides easy image manipulation support for Symfony2. The goal of the fork is to make the code more extensible and as a result applicable for more use cases.

This php 5.4+ library is a collection of traits that add behaviors to Doctrine2 entites and repositories.

This bundle integrates the Doctrine2 Migrations library. into Symfony so that you can safely and quickly manage database migrations.

Provide markdown conversion (based on Michel Fortin work) to your Symfony2 projects.

Alice allows you to create a ton of fixtures/fake data for use while developing or testing your project. It gives you a few essential tools to make it very easy to generate complex data with constraints in a readable and easy to edit way, so that everyone on your team can tweak the fixtures if needed.

It adds in your WebProfiler extra sections (routing, container, twig,…)

This bundle allows you to expose your routing in your JavaScript code. That means you’ll be able to generate URL with given parameters like you can do with the Router component provided in the Symfony2 core.

Pour finir…

…la liste n’est bien sûr pas exhaustive, retenez que vous trouverez souvent des bundles existant répondant à vos besoins. Attention cependant, car certains sont de meilleure qualité que d’autres, certains sont bien maintenus d’autres pas, enfin plus grave, certains ne fonctionnent pas ou contiennent de nombreux bugs. Alors attention !

Quelques liens utiles

Elasticsearch vagrant cluster, the easy way

En tant que fanatique d’elasticsearch, j’utilise régulièrement l’outil pour réaliser des POC, tester des applicatifs comme Logstash et Kibana, ou plus généralement, pour découvrir les extraordinaires fonctionnalités offertes par ElasticSearch (percolator, aggregations, sharding, scalability,…).

Cependant, il est souvent compliqué et chronophage de mettre en place un cluster ElasticSearch (vagrant cluster) complet simplement pour un POC ou des tests.

A l’inverse, utiliser la configuration minimale et par défaut d’ES (très rapide à mettre en place et qui fonctionne très bien) a ses limites lorsque l’on souhaite travailler en prenant en compte les problématiques suivantes :

  • Sharding sur plusieurs nœuds physiques (routing, filtres, rack, …).
  • Configuration réseau d’un cluster (unicast, multicast, ip:port, …)
  • Configuration de snapshot/recovery
  • Configuration spcécifiques (load balancer, data nodes et master nodes)
  • Simulation de coupure réseaux et autres incidents

J’ai donc décidé de créer un projet permettant de lancer un cluster elasticsearch de machines virtuelles (virtualbox/vagrant) en quelques minutes et en une seule commande bash :

vagrant up

Vous devriez alors obtenir un shell ressemblant à ceci :

$ vagrant up
Cluster size: 5
Cluster IP: 10.0.0.0
Bringing machine 'vm1' up with 'virtualbox' provider...
Bringing machine 'vm2' up with 'virtualbox' provider...
...

ElasticSearch Vagrant cluster

Voici le lien vers le dépôt github du projet : https://github.com/ypereirareis/vagrant-elasticsearch-cluster

La gestion du cluster se fait ensuite en utilisant vagrant et elasticsearch de manière classique. Tout est expliqué sur le dépôt (en anglais) :

  1. Pré-requis
  2. Installation
  3. Configuration
  4. Plugins

J’illustrerai, prochainement et de façon concrète, différentes configurations, problématiques ou bonnes pratiques à prendre en compte ou mettre en œuvre lorsque l’on travaille avec elasticsearch.

Le projet de cluster ES avec Vagrant vous permettra de rapidement reproduire les exemples et de modifier les configurations comme vous le souhaitez.

Designez des API asynchrones VRAIMENT asynchrones

Je suis tombé récemment sur un vieil article d’Isaac Zimmitti Schlueter à lire ABSOLUMENT.

Pour résumer rapidement le propos, IZS explique suite à un avertissement on ne peut plus clair (Do Not Release Zalgo) que lors du design d’une API Asynchrone, il faut éviter à tout prix la situation suivante :

var cachedData;
function getSomeData(callback) {
  if (cachedData === undefined) {
    $.get('some/random/api').then(function(data) {
      callback(cachedData = data);
    });
  } else {
    callback(cachedData);
  }
}

Quel est le problème avec ce bout de code ?

Si vous êtes développeur AngularJs / EmberJs / votre framework javascript préféré, je suis certain que vous avez vous-même été amené à écrire du code similaire ou au moins à en rencontrer au sein de vos applications.

Alors quel est le problème ?

Mon API fait un calcul coûteux (requête HTTP vers une API tiers) au premier appel, stocke la donnée retournée dans une variable servant de cache et appelle le callback. Au deuxième appel, mon API constate que la donnée est déjà en cache et appelle immédiatement le callback.

L’utilisation d’une API de ce type est extrêmement complexe et source de bugs. Pourquoi ? Son comportement est difficile à prédire…

L’auteur l’explique en ces termes sur son article :

If you have an API which takes a callback, and sometimes that callback is called immediately, and other times that callback is called at some point in the future, then you will render any code using this API impossible to reason about, and cause the release of Zalgo.

Prenons l’exemple du code javascript suivant :

// Quelque part dans mon code
getSomeData(function(result) {
  // Faire quelque chose avec mon résultat
});

function clickHandler() {
  var data;
  getSomeData(function(result) {
    data = result;
  });

  if (data.id === 1) {
    // Traitement important
    alert('gouzigouza');
  }
}

Ligne 2, on appelle notre API asynchrone. Un peu plus bas, on définit une fonction appelée lors d’un clic sur un bouton dans une page web. Au sein de ce clickHandler on utilise l’objet data en partant du principe qu’il est disponible immédiatement. L’erreur saute ici évidemment aux yeux. Rien ne garantit que notre objet data ait déjà été retourné arrivé à la condition if.

Malheureusement, le design de notre API et l’utilisation qui en a été faite ici fait que le bug n’est pas identifiable immédiatement. En revanche, si le développeur supprime le premier appel à notre API ligne 2 ou si le clic sur le bouton au sein de la page web intervient avant que le résultat de notre API asynchrone n’ait été mis en cache, alors on se retrouvera avec une belle erreur javascript :

TypeError: Cannot read property 'id' of undefined

Quelle solution ?

Pour une API asynchrone, l’objectif est donc de toujours respecter le caractère asynchrone.
Pour se faire, une approche simpliste est de modifier notre API de la manière suivante :

var cachedData;
function getSomeData(callback) {
  if (cachedData === undefined) {
    $.get('some/random/api').then(function(data) {
      callback(cachedData = data);
    });
  } else {
    setTimeout(function() {
      callback(cachedData = data);
    }, 0);
  }
}

Ici, l’utilisation du setTimeout() est une astuce qui permet de forcer l’appel du callback de manière asynchrone. Si l’API était codée avec nodejs, il serait bien plus judicieux d’utiliser process.nextTick() qui permet de mettre en place un comportement similaire.

Ainsi, à l’usage, il est désormais systématiquement impossible de faire fonctionner le code suivant :

var data;
getSomeData(function(result) {
  data = result;
});

if (data.id === 1) {
  // Bug systématique
  alert('gouzigouza');
}

Le développeur identifie immédiatement son erreur et est obligé alors de modifier son code de la manière suivante :

getSomeData(function(result) {
  var data = result;
  if (data.id === 1) {
    alert('gouzigouza'); // OK
  }
});

Pour aller plus loin

Voici quelques liens pour approfondir le sujet. Je vous conseille évidemment la lecture de l’article de IZS qui ajoute des exemples de design pour des API où les données sont généralement présentes immédiatement et où les performances sont très importantes.
Vous pouvez également aller jeter un oeil à la partie Keeping callbacks truly asynchronous de cet article sur la fonction process.nextTick de nodejs.

AngularJs : ngIf ngSwitch VS ngShow ngHide

Depuis la sortie de Angular 1.1.5 (Triangle Squarification), la directive ngIf a été ajoutée au framework AngularJs pour permettre de créer ou supprimer un bout de DOM en fonction de l’évaluation d’une expression booléenne, comportement auparavant possible uniquement grâce à la directive ngSwitch.

La directive ngIf s’utilise de manière similaire à la directive ngShow. Voici deux exemples qui produisent visiblement le même résultat.

ngIf

<!-- Exemple avec ngIf -->
<label>Afficher / masquer : <input type="checkbox" ng-model="show" /></label>
<span ng-if="show">Ceci est un test</span>

ngShow

<!-- Exemple avec ngShow -->
<label>Afficher / masquer : <input type="checkbox" ng-model="show" /></label>
<span ng-show="show">Ceci est un test</span>

Quelle différence ?

Dans les faits, ces deux directives ne se comportent absolument pas de la même manière. ngShow se contente d’afficher ou masquer un élément du DOM en lui ajoutant ou supprimant la classe CSS ng-hide définie par défaut de la manière suivante :

.ng-hide {
  display:none !important;
}

L’élément en question reste bien systématiquement présent dans le DOM, seule sa visibilité varie grâce à cette classe CSS.

Concernant la directive Angular ngIf, cette dernière procède à une suppression ou un ajout dans le DOM de l’élément concerné, en fonction de l’évaluation de l’expression booléenne associée.

ngIf differs from ngShow and ngHide in that ngIf completely removes and recreates the element in the DOM rather than changing its visibility via the display css property.

La première différence qui saute aux yeux est l’impact au niveau des sélecteurs de type pseudo-classe (pseudo-class selector) que ce soit pour le CSS ou via une directive faisant usage de jQuery ou jqlite.

A common case when this difference is significant is when using css selectors that rely on an element’s position within the DOM, such as the :first-child or :last-child pseudo-classes

.menu li {
  display: inline-block;
  margin: 10px 0;
  padding: 5px;
}

.menu li:first-child {
  margin-left: 30px;
}

.menu li:last-child {
  margin-right: 30px;
}
<label>Je suis un administrateur : <input type="checkbox" ng-model="user.isAdmin" /></label>
<ul class="menu">
  <li>Menu 1</li>
  <li>Contact</li>
  <li ng-show="user.isAdmin">Menu Admin</li>
</ul>

Dans l’exemple ci-dessus, on ajoute une marge spécifique au premier et au dernier élément du menu. Mais avec l’utilisation de la directive ngShow on n’obtient pas le rendu initialement souhaité par notre intégrateur. Le menu spécifique à l’administrateur, bien que non visible pour un utilisateur lambda, reste bien présent dans le DOM et l’entrée contact n’est dès lors pas ciblée par notre pseudo-classe :last-child.

Impact sur les performances

Dans la majorité des cas, l’utilisation de ngIf par rapport à ngShow n’a pas d’impact perceptible sur les performances. Il est pourtant légitime de se poser la question lorsqu’on travaille au sein d’une liste importante en terme de volumétrie, typiquement au sein d’un ngRepeat sur plusieurs centaines voire milliers d’éléments.

Prenons l’exemple suivant. Nous avons une liste importante (> 1000 éléments) et nous avons deux types d’informations à afficher ; le nom de l’élément et potentiellement sa description.

<ul>
  <li ng-repeat="element in elements">
    <span>{{ element.name }}</span>

    <!-- Avec la directive ngShow -->
    <span ng-show="element.showDescription">{{ element.description }}</span>

    <!-- Avec la directive ngIf -->
    <span ng-if="element.showDescription">{{ element.description }}</span>
  </li>
</ul>

Deux cas très différents se profilent. Soit la condition d’affichage / masquage est susceptible de varier régulièrement en fonction des interactions de l’utilisateur, soit elle varie rarement.
Par exemple, si l’affichage / masquage de la description est déclenché suite au cochage / décochage d’une option en haut de page, il semble plus avantageux d’utiliser la directive ngShow. L’élément est déjà inséré dans le DOM et il ne reste plus qu’au navigateur à recalculer sa visibilité suite à l’ajout ou la suppression de la classe ng-hide. D’autre part l’utilisation de ngIf a pour effet de créer un scope spécifique à l’élément ciblé ce qui entraîne nécessairement sa destruction lorsque la condition booléenne est évaluée à false, et donc un temps de traitement associé.

En revanche, si la condition d’affichage de la description dépend par exemple des droits associés à l’utilisateur connecté, mieux vaut utiliser la directive ngIf. Ainsi, si l’utilisateur n’a pas le droit de voir la description, le DOM est très largement allégé (et donc l’emprunte mémoire associée). C’est d’autant plus vrai que l’élément ciblé par la directive ngIf/ngShow/ngHide est important.

Attention toutefois, il peut être bien plus avantageux de travailler à limiter le nombre de $watch générés au sein d’un ng-repeat en codant sa propre directive qui ne crée aucun watchers ou en faisant usage de directives optimisées telles que la désormais fameuse BindOnce.

ngIf et scope enfant (child scope)

Je l’ai évoqué un peu plus haut, contrairement à ngShow ou ngHide, ngIf crée un scope enfant pour l’élément ciblé.

Note that when an element is removed using ngIf its scope is destroyed and a new scope is created when the element is restored. The scope created within ngIf inherits from its parent scope using prototypal inheritance.

Ainsi, sur le bout de code qui suit, le binding au model ne se comporte pas de la même manière alors que la seule différence d’implémentation réside au niveau de l’utilisation d’un ngShow VS ngIf.

function MyCtrl($scope) {
    $scope.show = true;
    $scope.value = "gouzigouza";
}
<div ng-app>
    <div ng-controller="MyCtrl">
        <label>Afficher / masquer</label> <input type="checkbox" ng-model="show" />
        <div>
            <label>Scope parent</label> <input type="text" ng-model="value" />
        </div>
        <div ng-show="show">
            <label>ngShow</label> <input type="text" ng-model="value" />
        </div>
        <div ng-if="show">
            <label>ngIf</label> <input type="text" ng-model="value" />
        </div>
    </div>
</div>

Vous pouvez voir le résultat sur le fiddle ci-dessous. Essayez de modifier la valeur des différents inputs. Vous constaterez que le troisième input est bindé sur la même valeur que les 2 premiers, tant que sa propre valeur n’a pas été modifiée via le champ input lui-même. Vous pouvez également constater que si la valeur de ce dernier est modifiée et que la case à cocher permettant d’activer ou non la condition du ngIf varie, la valeur de l’input est ré-initialisée à celle du parent scope.

Conclusion

Même si les directives ngShow, ngHide et ngIf semblent produire le même résultat, leur implémentation et leur comportement diffèrent. Il est important de bien comprendre leur fonctionnement pour les utiliser à bon escient.

Pour aller plus loin