Lorsque vous développez une application en utilisant un framework quelconque, vous vous retrouvez inévitablement à avoir un layout qui contient tout ce qui est censé être le design global de votre application.
Et parce qu’il faut des exceptions qui confirment la règle de ce design global, vous avez des pages qui doivent se comporter différemment :
Ne pas afficher le menu de gauche ou avoir un body avec une class différente.

Pour cela, Django et Symfony proposent deux concepts fortement intéressants qui sont similaires, bien qu’ayant des noms différents et que le concept soit beaucoup développé avec Django.
Je vais faire l’explication avec Django. Et je vous montrerai ensuite l’équivalent en PHP avec Symfony.

Avec Django

Dans votre layout, nous avons le code suivant :
<div>
  {% block 'menu' %}
    Contenu par défaut
  {% endblock %}
</div>

Et dans la vue de l’une de vos actions :
{% extend 'layout' %}
{% block 'menu' %}
  Contenu spécifique à la vue
{% endblock %}

Notre vue va charger le layout (celui à charger est précisé par le extend).
Le layout, en détectant le block va alors aller voir si la vue n’en définit pas un elle-même. Vu qu’elle en définit un, à l’endroit du block du layout sera affiché le contenu du block de la vue.

En gros ici, nous aurons le message « Contenu spécifique à la vue » d’affiché.
Si la vue ne définissait pas de block menu, nous aurions le message « Contenu par défaut d’affiché ».
Vous pouvez ainsi définir divers éléments de votre design spécifiques à votre vue tout en ayant également une valeur « par défaut ».

C’est d’ailleurs ainsi qu’il est conseillé de placer, en plus des éléments du design, le contenu de la vue en lui même, en créant un block « content ».
Plus plus d’informations, je vous invite à voir la documentation de Django.

Avec Symfony

Les slots de Symfony sont beaucoup moins utilisés que les blocks de Django. Il s’agit d’une fonctionnalité disponible dans les templates. Pas de la fonctionnalité autour de laquelle vous construisez vos templates.

Dans notre nous allons donc définir un slot dans notre layout :
<div>
  <?php if (has_slot('menu')): ?>
    <?php include_slot('menu') ?>
  
    Contenu par défaut
  
</div>

Et en redéfinir son contenu dans notre vue :
<?php slot('menu') ?>
  Contenu spécifique à la vue
<?php end_slot() ?>

Ou encore, si vous avez peu de contenu à définir dans votre slot :
<?php slot('menu', 'Contenu spécifique à la vue') ?>

Le première forme en fera tiquer certains puisqu’il s’agit d’un yield-like (pour les autres, c’est le genre de choses qui rendrait php encore mieux si c’était implémenté).
Le code donné ici fonctionne exactement de la même manière que celui donné plus haut pour Django.
Pour plus d’informations, je vous invite à voir la documentation Symfony.

Ainsi avec les slots et blocks à utiliser en fonction de la technologie que vous utilisez, vous pouvez rendre votre design entièrement DRY sans pour autant vous fixer de limites :)

S’il vous plait ! Et je m’explique.

J’utilise, comme outil d’intégration continue, Integrity.
Avec un « post-receive » sur le repository github, à chacun de mes commits, Integrity est mis au courant et il exécute les tests.

Ca fonctionne à merveille avec mes projets Rails (l’API RefStats et mon portfolio); mes projets Django (la documentation de RefStats).
Mais pour les projets Zend Framework (l’appli web RefStats) et un autre sur lequel je vais bosser en aout et qui sera fait avec Symfony, c’est pas cool.
Les tests, bien qu’ils ne passent pas tous, sont considérés par Integrity, comme valides.

La raison de cela est très simple : lorsqu’un processus console est exécuté, il peut retourner un code de status. Un peu comme le code HTTP sur les pages web.
Ce code doit être à 0 pour que les tests soient valides et à une autre valeur, quelconque, si les tests ne passent pas.
Voir notamment la documentation Integrity.
En PHP, un simple exit(0) ou exit(1) à la fin des tests suffirait à exécuter la chose correctement.

Et vraissemblablement, Symfony tout comme le Zend Framework ne renvoient pas le bon code de status.
Alors s’il vous plait, PHP a déjà pas mal de retard au niveau des tests automatisés d’applications, qu’il serait cool de rattraper. Faire ceci serait déjà un premier pas :)

Symfony
Vous en avez peut-être entendu parler, il y a quelques semaines de cela est sorti le premier livre Symfony en Français, aux éditions Eyrolles.

A vrai dire il s’agit de la version Française du Jobeet, qui a été publié en anglais comme calendrier de l’avent en décembre dernier.
Vu la qualité du Jobeet, je ne peux que vous conseiller ce livre.
Rédigé par Fabien Potencier, le créateur de Symfony et Hugo Hamon de Apprendre PHP, c’est le livre à acheter de la saison (non non j’ai aucune action).

Pour ma part je ne l’acheterai pas (ça, c’est dit).
Y’a Romain qui a fait la relecture de cet ouvrage et qui en a, par conséquent, reçu trois exemplaires gratuitement.
Il voulait au départ les encadrer au mur de sa chambre. Mais c’est pas assez grand. Donc comme il est trop cool, il m’en offre un.

Ca tombe bien. Après avoir développé l’API de Refstats avec rails; le site avec Zend Framework et l’interface de documentation (pas déployée encore) avec Django, je voulais justement faire l’interface de passage en mode professionnel avec Symfony :)

Si vous avez déjà touché à Zend Framework, vous vous êtes peut-être déjà pris la tête sur ses formulaires.
Et dans ce cas la, vous comprenez déjà probablement ce que je veux dire avec ce titre. Sinon, la suite détaille.

Voici un exemple de formulaire un petit peu complexe.
Vous pouvez en voir le résultat sur la page de gestion des mots clés de RefStats.

Comme vous pouvez le constater, il est assez difficile de comprendre l’intérêt d’un tel formulaire, ce qu’il affichera et même les champs qu’il contient.

Le problème des formulaires ZF est le même que dans beaucoup d’autres composants du framework : ils veulent trop en faire.
Le but d’un formulaire côté serveur est pourtant simple : faciliter la validation des données.
Que cela soit dans Rails ou ceux-ci sont directement inclus dans les modèles ou dans Django et Symfony ou ceux-ci ne servent que à faire la validation, à chaque fois, les formulaires ne génèrent pas d’HTML automatiquement.

C’est, malheureusement, ce qu’essaye de faire Zend Framework.
Mais cela rends la chose illisible car pas ergonomique; impossible à modifier pour la première raison.
Et surtout, absolument pas DRY. Si vous avez besoin de placer deux fois le même formulaire pour les mêmes données. Mais les afficher différemment ou simplement ne pas permettre la modification de certaines données, il vous faut créer deux formulaires différents !
Alors qu’il serait tellement plus simple de ne faire qu’un seul formulaire mais deux pages HTML affichant celui-ci.

Du coup comme vous l’avez deviné, je ne vous conseille absolument pas d’utiliser ces formulaires.
Après, au niveau alternatives, je vous épargnerai Rails et Django. Tout autant que Symfony, qui requiert l’inclusion complète du framework.
En revanche Loic me souffle sur Twitter que JForms serait pas mal.
Mais il est l’un des développeurs du framework et je n’ai pas eu l’occasion de tester la chose. Donc vous êtes sans filet !

Comme tout framework qui se respecte, Symfony possède une interface permettant d’écrire et exécuter des tests unitaires et fonctionnels.
Voyons un petit peu celle-ci.

Dans votre projet, vous avez le dossier test qui contient trois dossiers.

Bootstrap

Ce dossier contient deux fichiers. functional.php et unit.php.
Ils sont à inclure au début de chacun de vos tests et permettent d’instancier l’environnement.

Unit

Passons donc maintenant aux choses sérieuses avec les tests unitaires :)
Voici un exemple de tests :
require_once dirname(__FILE__).'/../bootstrap/unit.php';

$t = new lime_test(2, new lime_output_color());

$t->comment('is the valid() method valid?');
$t->is(Post::my_test(true), true, 'valid() return the argument we give him');
$t->is(Post::my_test(false), false, 'valid() return the argument we give him');

A côté de ce test, nous avons un modèle Post qui contient une méthode my_test, celle-ci étant on ne peut plus basique puisqu’elle retourne la valeur que l’on lui donne.

Pour commencer, vous constatez que en première ligne de nos tests, nous incluons le bootstrap des tests unitaires.

Ensuite nous créons notre nouveau test, en y signalant que nous aurons deux assertions.
$t = new lime_test(2, new lime_output_color());
La méthode comment() permet de placer un commentaire dans l’interface de test, facilitant ainsi le débugguage ensuite.

Puis vient le moment des tests à proprement parler.
$t->is(Post::my_test(true), true, 'valid() return the argument we give him');
Le test échouera si la valeur retournée par Post::my_test n’est pas égale à true.

De nombreuses méthodes sont disponibles en plus de is().

  • comment($msg) – Affiche un commentaire mais n’exécute aucun test.
  • ok($test, $msg) – Affiche le commentaire et vérifie que $test est vrai.
  • is($value1, $value2, $msg) – Affiche le commentaire et vérifie que $value1 est égal à $value2.
  • isnt($value1, $value2, $msg) – Affiche le commentaire et vérifie que $value1 n’est pas égal à $value2.
  • like($string, $regexp, $msg) – Affiche le commentaire et vérifie que $string respecte l’expression régulière $regexp<./li>
  • unlike($string, $regexp, $msg) – Affiche le commentaire et vérifie que $string ne respecte pas l’expression régulière $regexp.
  • cmp_ok($value1, $operator, $value2, $msg) – Compare les deux arguments avec l’opérateur.
  • isa_ok($value, $type, $msg) – Vérifie le type de l’argument donné.
  • isa_ok($object, $class, $msg) – Vérifie que l’objet est bien de la classe mentionnée.
  • can_ok($object, $method, $msg) – Vérifie la disponibilité de la méthode pour un objet ou une classe.
  • is_deeply($array, $array2, $msg) – Vérifie que deux tableaux ont les mêmes valeurs.
  • include_ok($file, $msg) – Vérifie qu’un fichier existe et qu’il est correctement inclu.
  • fail() – Echoue toujours. Utile pour tester des exceptions.
  • pass() – Passe toujours. Utile pour tester des exceptions.
  • skip($msg, $nb_tests) – Compte comme $nb_tests tests. Utile pour les tests conditionnels
  • todo() – Compte comme un test. Utile pour les tests restant à écrire.

Functional

Et les tests fonctionnels. Voici un exemple de test :
include(dirname(__FILE__).'/../../bootstrap/functional.php');
$browser = new sfTestFunctional(new sfBrowser());

$browser->get('/')
  ->info('1 - The culture is appropriately fr')
  ->with('user')
  ->isCulture('fr')

  ->info('2 - The response is 200 and the content has a title')
  ->with('response')
  ->begin()
  ->isStatusCode(200)
  ->checkElement('body', '/<h1>/i')
  ->end();

Comme précédemment, nous commencons par implémenter le bootstrap.
Puis nous instancions un objet sfTestFunctional, qui fera office de navigateur et s’occupera de faire les appels http et de faire nos validations.

Ensuite nous faisons une requête get sur l’url /.
Et nous entrons dans la vue culture, pour vérifier que la langue définie pas défaut est bien le français. Voir internationalisation

Ensuite nous passons dans la vue response, nous vérifions que le code HTTP est bien 200 et que la page contient une balise h1.
Puis nous fermons cette requête.

Il serait bien évidemment possible de faire d’autres requêtes sans réinstancier le navigateur. Il suffit de refaire un $browser->get('/chemin');

Par ailleurs, comme précédemment, de multiples méthodes sont présentes :

  • get($url, $parameters) – Fait une requête get
  • post($url, $parameters) – Fait une requête post
  • call($url, $method, $paramaters) – Permet de faire un appel en précisant sa méthode (utilisé pour les requêtes PUT et DELETE).
  • back() – Retourne à la page précédente dans l’historique
  • forward() – Va à la page suivante dans l’historique
  • reload() – Recharge la page courante
  • click($name, $arguments, $options) – Clique sur un lien ou un bouton
  • select($name) – Sélectionne un bouton radio ou une case à cocher
  • deselect($name) – Désélectionne un bouton radio ou une case à cocher
  • restart() – Redémarre le navigateur (et la session)

Ainsi que d’autres méthodes pour configurer le comportement du navigateur

  • setHttpHeader($header, $value) – Définit une entête HTTP
  • setAuth($username, $password) – Définit l’identification http (basique)
  • setCookie($name, $value, $expire, $path, $domain, $secure, $httpOnly) – Définit un cookie
  • removeCookie($name) – Supprime un cookie
  • clearCookie() – Supprime tous les cookies
  • followRedirect() – Suis la redirection (et retourne une exception si le lien n’en est pas une)

Une fois que la requête est correctement paramétrée, il vous faut définir la vue que vous désirez avoir et exécuter celle-ci.
Il existe cinq vues. Mais nous n’en verrons que deux dans cet article.

request

Cette vue apporte des méthodes permettant de tester la requête.

  • isParameter($key, $value)> – Teste qu’un paramètre est présent et que sa valeur est correcte
  • isFormat($format) – Vérifie le format de la requête
  • isMethod($method) – Vérifie la méthode de la requête
  • hasCookie($key, $exists) – Vérifie que le cookie existe
  • isCookie($key, $value) – Vérifie la valeur du cookie

response

Cette méthode permet de vérifier le contenu renvoyé par la page.

  • checkElement($selector, $value, $options) – Vérifie qu’un sélecteur CSS respecte les critères donnés
  • isHeader($key, $value) – Vérifie la valeur d’une entête HTTP
  • isStatusCode($code) – Vérifie le code HTTP renvoyé
  • isRedirected() – Vérifie qu’il s’agit d’une redirection

Autres vues

Nous avons donc vu les deux vues les plus couramment utilisées. Cependant il en existe trois autres, que nous ne détaillerons pas ici :

Pour finir, comme vous pouvez le constater, tester ses applications est assez aisé avec Symfony. Alors vous n’avez plus aucune raison de ne pas le faire :)

Le cas « général »

Lorsque l’on utilise un framework tel que Zend, Symfony ou encore n’importe quelle librairie, il y a deux écoles :

  • Installer une fois le framework dans un répertoire spécifique. Et toutes les applications situées sur la machine utilisent cette version.
  • Installer une version du framework pour chaque application.

A première vue, la première solution pourrait paraitre intéressante. On y gagne en place disque et on a pas des copies de fichiers similaires partout sur sa machine.

Cependant il s’agit d’une très mauvaise idée.
Vous allez en effet vous retrouver bloqué sur une version de la librairie à utiliser pour toutes vos applications. Et lorsque une nouvelle version majeure sortira et que la rétrocompatibilité avec la version précédente ne sera pas forcément très poussée, soit vous ne mettrez pas à jour.
Soit vous devrez mettre à jour toutes vos applications en même temps pour faire cette mise à jour. C’est un petit peu lourd. Autant donc dire que vous ne mettrez pas à jour.
Et si vous le faites, vous ne prendrez probablement pas le temps de tester chaque application correctement et vous retrouverez avec des bugs.

En revanche si vous avez une version spécifique de la librairie spécialement pour l’application, vous pouvez mettre la librairie à jour pour une application, tester celle-ci proprement et puis passer à l’application suivante.

Et en versionnant la librairie globale

Avec Rails

Rails propose quelque chose d’assez sympatique. Dans le fichier config/environment.rb, vous avez
RAILS_GEM_VERSION = '2.3.2' unless defined? RAILS_GEM_VERSION

Vous définissez la version de la librairie utilisée dans l’application.
A côté de cela, vous avez donc plusieurs versions de rails installées dans le répertoire des librairies globales. Et vous pouvez choisir celle que vous désirez en fonction de l’application.

Plus de librairie inclue directement dans l’application (ce qui est tout de même assez crade).
Plus de problèmes de changement de version multi applications. Vous pouvez installer Rails 2.3.2 et l’utiliser dans l’une de vos applications tout en ayant toutes les autres qui tournent encore avec Rails 2.2.

Symfony

Symfony possède un fichier config/ProjectConfiguration.class.php
Dans lequel vous trouverez
require_once dirname(__FILE__).'/../lib/symfony/autoload/sfCoreAutoload.class.php';
Remplacez le chemin par celui vers la version de Symfony de votre choix. Et le tour est joué !

Zend Framework

Avec Zend Framework, c’est en définissant le include_path que vous pouvez définir la version de la librairie que vous utilisez.
Si vous avez, dans votre document public/index.php
set_include_path(
APPLICATION_PATH . '/../library' . PATH_SEPARATOR
. APPLICATION_PATH . PATH_SEPARATOR
. get_include_path()
);

require_once 'Zend/Loader.php';
Zend_Loader::registerAutoload();

Il vous suffit de remplacer ‘/../library’ par le chemin vers le dossier library versionné pour votre application.

Alors après entre utiliser des librairies versionnées et implémenter directement la version de votre choix dans l’application, c’est à vous de faire la part des choses. Je pense que cela dépends surtout des gouts.
La seule chose à éviter est bien évidemment la librairie installée une seule fois, utilisée par toutes les applications et impossible à mettre à jour.
Et ceci est bien évidemment faisable avec d’autres frameworks et librairies que les 3 mentionnés ici. Documentez vous ! ;)

Cet article reprends les principes expliqués dans l’article du Jobeet Symfony du 13 décembre.
Simplement cet article est un petit peu « chatty » et j’ai voulu représenter les fondamentaux de son contenu de manière plus synthétique.

Dans le dossier lib de votre application symfony (apps/<votre application>/lib), vous verrez un fichier myUser.class.php.
Celui-ci est instancié lors de chaque chargement de page et vous permet d’identifier l’utilisateur actif et de gérer sa session.

Ainsi, pour ajouter et lire une variable de session dans le contrôleur :
$this->getUser()->setAttribute('myVar', 'moncontenu');
$this->getUser()->getAttribute('myVar');

Et dans la vue :
$sf_user->setAttribute('myVar', 'moncontenu');
$sf_user->getAttribute('myVar');

Jusque la, aucune difficulté. Mais cet objet va beaucoup plus loin. En effet, vous pouvez forcer quelqu’un à s’identifier pour accéder à un module particulier. Dans le fichier apps/<votre application>/config/security.yml, placez le code suivant :
default:
is_secure: on

Et pour identifier notre utilisateur automatiquement quelque soit le contrôleur, remplissons notre fichier myUser.class.php :
class myUser extends sfBasicSecurityUser {
function __construct($arg, $arg2) {
parent::__construct($arg, $arg2);
if (!$this->isAuthenticated())
$this->setAuthenticated(true);
}
}

Ici, nous identifions l’utilisateur quoi qu’il arrive. Mais bien évidemment, il en revient à vous de ne faire cela que lorsque cela est nécessaire. Dans mon cas, je vérifie l’identité de l’utilisateur avec un appel à l’API JSON de RefStats (en développement). Mais vous pouvez utiliser un modèle pour faire cela.

Pour finir, simplement identifier un utilisateur ne suffit pas toujours. Il peut arriver que vous ayez besoin de gérer divers niveaux d’identification. Symfony permet, pour cela, de gérer des « groupes », appellés credentials.

Dans votre fichier security.yml, placez :
default:
is_secure: on
credentials: admin

Seuls les utilisateurs ayant le credential admin pourront accéder à la page. Puis pour attribuer ce credential à l’utilisateur courant :
$this->getUser()->addCredential('admin');

Et vous pouvez vérifier directement dans votre application que l’utilisateur a bien le credential demandé :
$this->getUser()->hasCredential('admin');

Pour plusde détails sur tout cela, je ne peux que vous inviter à lire l’article « The User » du Jobeet.
Mais celui-ci peut déjà, je l’espère, vous renseigner sur l’utilité de myUser.class.php et une des manières de l’utiliser.

Ayant, en ce moment, un peu de temps pour cela, j’en profite pour regarder un petit peu à quoi ressemble Symfony.
Je ferai donc probablement quelques articles à ce propos ici dans les jours à venir.

Si vous lisez cette documentation, vous constaterez qu’ils indiquent de placer le « dsn » suivant dans votre configuration :
mysql://root:pass@localhost/database

Cependant la nouvelle version de Propel force une légère modification de cette configuration. Il nous faut donc modifier le « dsn » en :
mysql:dbname=database;host=localhost

Tous les autres paramètres peuvent être conservé de manière similaire.
Cependant, si vous n’avez pas une application « from scratch », vous devez, pour mettre à jour depuis la 1.1, suivez les conseils de mise à jour donnés sur cette page.

 
Fork me on GitHub