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é.

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.

eZ publish : utiliser ezdesign en PHP

Avec le CMS eZ publish une des fonctions fréquemment utilisée dans les templates est la fonction ezdesign. Celle-ci permet de charger n’importe quel fichier contenu dans les répertoires « design », sans tenir compte des « siteaccess ».

Normalement cette fonction ne devrait-être utilisée que dans les templates, les fichiers contenus dans les répertoires « design » étant relatifs à la vue. Malheureusement il arrive que l’on doive reprendre du code tellement mauvais que l’affichage est généré en PHP. Lorsque c’est le cas et que vous devez afficher des éléments graphiques, il faut toujours utiliser la fonction « ezdesign » afin de générer des URLs correctes.

Après un peu de recherche dans le code source d’eZ publish, voici comme faire pour utiliser la fonction « ezdesign » en PHP :

eZURLOperator::eZDesign(null, 'stylesheets/core.css', 'ezdesign')

Le premier paramètre est la template dans laquelle la fonction est utilisée. Etant donné que nous l’utilisons en PHP, aucune.
Le second paramètre est le chemin du fichier à charger en commencer dans les répertoires « design ».
Le troisième paramètre est le nom de l’opérateur de template, ‘ezdesign’.