Loguer des messages dans un fichier spécifique avec Symfony2 et Monolog

Les fichiers de logs de Symfony sont très utiles pour comprendre ce qu’il se passe dans votre application et la débuguer. Je m’en sers constamment, la configuration de base répondant à mes besoins, jusqu’ici.

Travaillant sur un service de synchronisation très sensible il est intéressant d’y loguer ce qu’il s’y passe (quand la synchronisation commence, qui la déclenche, quels sont les éléments synchronisés, etc.). dans un fichier dédié, pour plus de clarté.

Pour loguer dans un fichier autre que ceux par défaut (dev.log, prod.log et test.log) il existe une documentation qui pour moi n’est pas des plus détaillée. Pourtant, une fois le mécanisme compris l’écriture de logs Symfony dans d’autres fichiers est des plus simple.

Partons du service de synchronisation suivant :

<?php 
class SynchronizerService
{
    /** @var LoggerInterface $logger */
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    /**
     * @param SynchronizationItem[] $synchronizationItems
     */
    public function sync($synchronizationItems)
    {
        $this->logger->notice('Synchronization started.');
    }
}

L’idée d’un service étant qu’il soit réutilisable partout, les dépendances sont injectées dans le constructeur et celles-ci sont le plus génériques possible. Ce n’est pas parce que le logger par défaut de Symfony2 est Monolog que notre service doit en dépendre.

Mon premier point de blocage à ce stade fut le suivant : comment faire à partir de LoggerInterface pour spécifier le fichier dans lequel écrire ? Réponse : on ne fait pas. Tout se passe dans la configuration du service et de Monolog.

La définition du SynchronizerService est classique, à l’exception de la présence d’un tag :

# services.yml
foo.sync.synchronizer:
  class: Foobar\SyncBundle\Service\SynchronizerService
  arguments:
    - "@logger"
  tags:
    - { name: monolog.logger, channel: sync }

Les tags portent bien leur nom puisqu’ils vous permettent de taguer vos services au même titre que des articles de blogs. On peut parler de catégorisation également. Pour plus d’information sur les tags, une page de documentation dédiée est disponible.

Maintenant que l’injection du logger est faite et que le service est tagué la dernière étape est de configurer Monolog afin d’utiliser un nouvel handler :

# config.yml    
monolog:
  handlers:
    sync:
      type: stream
      path: "%kernel.logs_dir%/sync_%kernel.environment%.log"
      level: debug
      channels: ['sync']   

La propriété qui nous intéresse ici est « channels ». Elle indique au handler quels channels écouter, ou plus simplement quels tags écouter.

La configuration est finie, la prochaine fois que vous exécuterez le service un nouveau fichier de log commencant par « sync_ » sera créé, en fonction de l’environnement dans lequel il est exécuté.

AngularJS et ngOptions : binder une valeur primitive à un tableau d’objet dans un select

La directive select d’AngularJS combinée à l’attribut ngOptions permet de générer une liste d’options pour le tag HTML select à partir d’un tableau ou d’un objet. La valeur sélectionnée étant attachée (bindé) au modèle spécifié par l’attribut ngModel.

<select ng-options="category for category in categories" ng-model="book.category"></select>

Dans cet exemple, la syntaxe utilisée est la suivante : « label for item in array ». « label » est la valeur affichée dans les éléments HTML option, « item » représente un élément du tableau et « array » le tableau de valeurs. Etant donné qu’un tableau JavaScript ne contient pas de clés, le label doit ici avoir le même nom que la valeur. Dans le cas d’un tableau d’objets, ça peut-être n’importe quelle propriété des objets.

Une fois la tableau de valeurs transformé en tableau d’objets, l’écriture est la suivante :

<select ng-options="category.name for category in categories" ng-model="book.category"></select>

L’utilisation basique du ngOptions ne pose pas de problèmes mais ça se complique dès qu’il faut associer le select à une valeur existante. A partir de l’exemple précédent, imaginons que nous récupérons notre livre d’une base de données et que celui-ci ait déjà sa catégorie de précisée. Dans ce cas le select doit ajouter l’attribut « selected » à l’option correspondant à la catégorie du livre.

En gardant exactement la même notation pour le ngOptions et en précisant la catégory du livre, aucune option n’est sélectionnée :

$scope.book.category = 'SF';

Si aucune n’option n’est sélectionnée c’est parce que AngularJS ne sait pas à quel objet la catégory ‘SF’ correspond. Pour lui indiquer sur quelle valeur nous voulons lier la propriété du modèle il faut ajouter la notation « select as ».

<select ng-options="category.name as category.name for category in categories" ng-model="book.category"></select>

De cette façon category.name devient le modèle. Sans le « select as » la comparaison est faite sur l’objet entier.

Configurer Doctrine DBAL avec Oracle

Il y a quelques jours j’ai eu à configurer Doctrine DBAL avec un serveur Oracle. Je n’ai que très peu de connaissances en configuration d’Oracle, mais la documentation de Doctrine semblait claire.

Voici les informations dont je disposais : un utilisateur, un mot de passe et un hôte de la forme ip:port/service.domaine (merci à ce sujet de Stackoverflow d’ailleurs).

Les informations à compléter pour connecter Doctrine à Oracle sont les suivantes : utilisateur, mot de passe, hôte, port, nom de la base de données, charset. Après un peu de lecture supplémentaire pour savoir ce qu’est un service, un SID et en quoi ils diffèrent du nom de la base de données, il semblerait que je dispose de toutes les informations nécessaires pour connecter Doctrine DBAL à Oracle.

Ma première tentative fut de découper la chaîne de caractères hôte afin d’avoir le port et le nom de la base de données à renseigner pour Doctrine. Première tentative infructueuse puisque Doctrine n’arrive pas à se connecter au serveur et lance l’exception suivante :

ORA-12505: TNS:listener does not currently know of SID given in connect descriptor

En y regardant de plus près l’exception est levée dans le constructeur de la classe Doctrine\DBAL\Driver\OCI8\OCI8Connection lors de l’appel à oci_connect ou oci_pconnect.

Un petit tour sur la documentation de oci_connect nous indique que la fonction prend 5 paramètres : username, password, connection_string, character_set et session_mode. N’ayant pas de doutes sur les deux premiers paramètres, c’est le connection_string qui nous intéresse.
Le dump de la variable $db servant de connection_string donne la chaîne de caractères suivante :

(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=1554)))(CONNECT_DATA=(SID=dbname.world)))

Partant d’un code existant utilisant directement la fonction oci_connect avec un troisième paramètre formaté comme ceci : ip:port/service.domaine je fus surpris de voir une telle chaîne de caractères à la place. Pourtant, il semblerait que cette chaîne soit bien formée.

Après un peu de reverse engineering supplémentaire, on découvre que c’est la classe Doctrine\DBAL\Driver\OCI8\Driver qui instancie la classe Doctrine\DBAL\Driver\OCI8\OCI8Connection :

public function connect(array $params, $username = null, $password = null, array $driverOptions = array())
{
    return new OCI8Connection(
        $username,
        $password,
        $this->_constructDsn($params),
        isset($params['charset']) ? $params['charset'] : null,
        isset($params['sessionMode']) ? $params['sessionMode'] : OCI_DEFAULT,
        isset($params['persistent']) ? $params['persistent'] : false
    );
}

Ce qui nous intéresse ici est la méthode _constructDsn de cette première classe qui renvoie la connection_string :

protected function _constructDsn(array $params)
{
    $dsn = '';
    if (isset($params['host']) && $params['host'] != '') {
        $dsn .= '(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)' .
               '(HOST=' . $params['host'] . ')';

        if (isset($params['port'])) {
            $dsn .= '(PORT=' . $params['port'] . ')';
        } else {
            $dsn .= '(PORT=1521)';
        }

        if (isset($params['service']) && $params['service'] == true) {
            $dsn .= '))(CONNECT_DATA=(SERVICE_NAME=' . $params['dbname'] . '))';
        } else {
            $dsn .= '))(CONNECT_DATA=(SID=' . $params['dbname'] . '))';
        }
        if (isset($params['pooled']) && $params['pooled'] == true) {
            $dsn .= '(SERVER=POOLED)';
        }
        $dsn .= ')';
    } else {
        $dsn .= $params['dbname'];
    }

    return $dsn;
}

Si le paramètre « host » est défini la connection_string sera de la même forme que celle générée un peu plus haut. Cependant, si le paramètre « host » n’est pas défini la connection_string sera directement égale au paramètre « dbname », plus précisément une chaîne EZ Connect de la forme ip:port/service.domaine.
Pour rappel, en PHP, une variable n’est pas définie quand elle n’existe pas ou que sa valeur est à « null ».

Seconde tentative donc, cette fois-ci avec la paramètre « host » défini à « null » et la chaine EZ Connect affectée au paramètre « dbname » :

$app->register(new Silex\Provider\DoctrineServiceProvider(), array(
    'db.options' => array(
        'driver'   => 'oci8',
        'host' => null,
        'dbname' => HOST,
        'user' => LOGIN,
        'password' => PWD,
        'charset' => 'UTF8',
    ),
));

Et Doctrine DBAL arrive à correctement se connecter à la base Oracle.

Symfony2 et Twitter Bootstrap : insérer un input checkbox ou radio dans un label

Ayant récemment utilisé Twitter Bootstrap pour l’interface d’administration d’un projet en Symfony2, je me suis heurté à un problème basique à propos de la gestion des formulaires.

Pour aligner verticalement un input de type radio ou checkbox avec son label il faut inclure le premier dans le second :

<label class="checkbox">
    <input type="checkbox"> Check me out
</label>

Ce code peut paraître simple, mais produire ce même HTML avec Symfony2 l’est moins. En effet, pour afficher les formulaires, Symfony2 utilise des blocs de templates Twig. Chaque bloc correspond à un type de champs, à l’affichage des labels ou plus globalement à l’affichage du formulaire. On peut retrouver leur définition dans le fichier form_div_layout.html.twig.

Première méthode : redéfinir le bloc dans la template

La façon la plus simple d’écraser la définition du bloc est de le faire à l’intérieur de la template du formulaire concernée :

{% form_theme form _self %}

{% block checkbox_widget %}
    {% spaceless %}
        <label class="checkbox">
            <input type="checkbox" {{ block('widget_attributes') }}{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %} />
            {{ form_label(form) }}
        </label>
    {% endspaceless %}
{% endblock %}

La première ligne indique à Twig de chercher à l’intérieur de la template si des fragments de blocs ne sont pas disponibles. Pour le reste du code, nul besoin d’explication. Il suffit de copier-coller le bloc défini dans form_div_layout.html.twig puis de réordonner le HTML.

Seconde méthode : redéfinir le bloc de façon globale

Si vous avez besoin d’appliquer ce fragment de template sur tous les formulaires de votre projet, il est plus simple de le définir à un seul endroit plutôt que de le copier-coller dans chaque template. Symfony permet de faire cela très rapidement et simplement.

Créer un fichier Resources/views/Form/fields.html.twig dans le répertoire de votre bundle avec le code ci-dessous :

{% block checkbox_widget %}
    {% spaceless %}
        <label class="checkbox">
            <input type="checkbox" {{ block('widget_attributes') }}{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %} />
            {{ form_label(form) }}
        </label>
    {% endspaceless %}
{% endblock %}

Pour rendre Twig au courant de la présence de ce fichier il existe là encore deux méthodes.
La première suit l’exemple précédent, à savoir indiquer à Twig dans la template la location du fichier concerné :

{% form_theme form 'AcmeDemoBundle:Form:fields.html.twig' %}

La seconde, qui permet de ne plus avoir à modifier chaque template, consiste à indiquer à Twig l’emplacement du fichier concerné, mais cette fois dans le fichier de configuration de Symfony : app/config/config.yml.

twig:
    form:
        resources:
            - 'AcmeDemoBundle:Form:fields.html.twig'

Conclusion

Tout au long de cet article nous nous sommes intéressés aux checkbox. Le fonctionnement est identique pour les radios en adaptant les bons blocs.

Il n’est finalement pas très compliqué de modifier les templates de formulaires Symfony2. Cela requiert un minimum de connaissances quant à l’héritage des blocs Twig mais en quelques dizaines de minutes maximum il est possible de personnaliser un projet complet.

GruntJS, le script d’automatisation pour le web

Une des bonnes pratiques web consiste à minifier tous les scripts JavaScript et CSS puis à les concaténer afin de ne servir que deux fichiers lorsque le site est en production. Ce a des fins de performances.

Il existe déjà des outils le permettant, tels que Capistrano, Ant, ou le traditionnel script Bash. Depuis début 2012 on entend parler de GruntJS, plus encore ces derniers mois.

GruntJS reprend le même principe qu’Ant, à savoir exécuter des tâches définies par l’utilisateur. Il est basé sur NodeJS et est actuellement disponible en 0.3.9.

L’article qui suit est un tutorial expliquant pas à pas comment installer et configurer GruntJS pour minifier et concaténer les fichiers JavaScript et CSS.

Lire le reste de cet article »