PHP

Mieux développer avec Symfony 1.2 et Doctrine

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 :)

Pourquoi les formulaires Zend Framework sont une calamité

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 !

Tests unitaires et fonctionnels avec Symfony

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 :
<?php
require_once dirname(__FILE__).'/../bootstrap/unit.php';</p>

<p>$t = new lime_test(2, new lime_output_color());</p>

<p>$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 :
<?php
include(dirname(__FILE__).'/../../bootstrap/functional.php');
$browser = new sfTestFunctional(new sfBrowser());</p>

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

<p>    ->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 :)

Pourquoi je n'utiliserai pas Zend Server

Pour l'instant du moins.

Comme vous le savez peut-être déjà, Zend a publié hier la première beta publique de son Zend Server. L'idée est en soi sympa. Le produit, qui a des versions Windows, Mac et Linux, inclut :

  • Apache (ou le support de IIS si vous êtes un grand fou sous windows)
  • PHP
  • Zend Framework
  • MySQL (optionnel)
Alors après un petit test rapide, le produit est sympa. L'interface web d'administration du serveur est toute jolie, ergonomique toussa (même si sans licence on ne peut pas voir grand chose). A savoir qu'elle a été développée avec le Zend Framework et Dojo.

Mais cette interface ne permet de gérer que PHP, les extensions installées et les logiciels Zend installés. C'est bien maigre lorsque l'on sait que pour faire fonctionner PHP il faut aussi un Apache correctement configuré. Du coup pour configurer son Apache, il faut passer par le fichier de configuration normal.

Ainsi en "natif", pas de multi projets. Vous placez votre application développée avec Zend à la base du DocumentRoot et puis c'est tout. Quand j'ai vu l'interface, j'ai tout de suite pensé à une génération automatique de virtualhosts moi (je me suis même pris à rêver de modification automatique du fichier hosts, c'est dire).

Pour finir le Apache utilisé est celui disponible chez apache.org, sans aucune implémentation native dans le Zend Server. Du coup vous avez (sous windows) deux icônes, une pour le ZS et une pour Apache. Quand des logiciels comme WAMP sont capables de monitorer seuls le serveur http ainsi que le serveur mysql, c'est plutôt dommage.

Du coup je me tâte pour regarder si ce serveur est intégrable à wamp justement. Même si l'intérêt en est finalement assez faible puisque ce dernier permet de gérer les extensions php installées de manière parfaitement correcte.

Du coup je vire le Zend Server et je reste avec wamp pour l'instant. On verra d'ici un an ou deux si la version stable intègre une gestion plus avancée des serveurs http et mysql.

P.S. : Aujourd'hui, O2Sources vient de sortir son premier projet Open Source, développé avec Zend Framework justement.

Symfony : identifier les utilisateurs

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.

Configurer Symfony 1.2 pour MySQL

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.

Zend Framework et Mysqli

Je publie ce billet un peu rapidement comme une note car j'ai eu du mal à trouver l'information lorsque j'en ai eu besoin.

La documentation de Zend Framework n'indique que les informations de connexion pour SQLite dans app.ini. Mais tout le monde n'utilise pas SQLite (et pour donner un avis totalement personnel, je dirais heureusement).

Voici donc les informations requises pour la configuration de MySQL dans votre application utilisant Zend Framework.

[general] database.adapter = "MYSQLI" database.params.host = "localhost" database.params.username = "root" database.params.password = "" database.params.dbname = "myDatabase"

Et puis rien que pour le fun, Oracle : [general] database.adapter = "Oracle" database.params.host = "localhost" database.params.username = "root" database.params.password = "" database.params.dbname = "myDatabase"

SQL Server [general] database.adapter = "Pdo_Mssql" database.params.host = "localhost" database.params.username = "root" database.params.password = "" database.params.dbname = "myDatabase"

Et PostgreSQL [general] database.adapter = "Pdo_Pgsql" database.params.host = "localhost" database.params.username = "root" database.params.password = "" database.params.dbname = "myDatabase"

Générer des graphiques avec Artichow et CakePHP

Note : cet article est une reprise d'un ancien article anciennement présent sur kazhar.org et légèrement étoffé.

La librairie php artichow permet de générer des images de manière assez simple. Cela rends la génération de graphiques presque amusante ;-)

Malheureusement il faut trifouiller un peu pour l’intégrer avec CakePHP. En effet, les deux applications implémentent une classe nommée "Component". Et les espaces de nom n’étant pas encore disponibles (ils sont prévus pour PHP 5.3), il n’est pas possible de ruser de cette manière pour implémenter l’un avec l’autre. Heureusement il existe une autre solution.

Dans le fichier Artichow.cfg.php, il y a la ligne suivante : define(‘ARTICHOW_PREFIX’, ”);

Placez le préfixe que vous désirez (moi j’ai mis arti. Mais on se moque un peu de la valeur).

Maintenant vous n’avez plus de souci de conflit avec la classe Component. Mais vous avez des erreurs disant que des classes telles que Driver n’existent pas. Modifiez les trois ou quatre erreurs signalées, notamment dans inc/drivers/gd.class.php en ajoutant ‘aw’ devant la classe appelée. Ainsi, Driver devient awDriver.

Enfin l'inclusion. Dans l'action ou vous souhaitez générer un graphique, placez :

function graph() {

App::import('Vendor', 'artichow/LinePlot.class'); $graph = new Graph(400, 400); $graph->setAntiAliasing(TRUE);

$values = array(1, 4, 5, -2.5, 3); $plot = new LinePlot($values);

$plot->setBackgroundGradient( new LinearGradient( new Color(210, 210, 210), new Color(250, 250, 250), 0 ) ); $plot->yAxis->setLabelPrecision(1); $plot->setSpace(5, 5, NULL, NULL);

$graph->add($plot); $graph->draw(); }

Vous verrez alors en appellant cette action le graphique généré.

PHP : détecter la langue favorite de l'utilisateur

Supposons le cas d'un site multilingue, qu'il soit architecturé en sous-répertoires ou sous-domaines. Lorsque vous arrivez sur la base du site (/ ou www), aucune langue n'est définie. Et vous n'avez aucun contenu à afficher.

Il vous faut donc rediriger l'utilisateur vers une autre page qui correspondra à la langue qu'il est le plus susceptible de désirer. De nombreux sites, corporate y compris, ne s'embêtent pas et proposent simplement une liste de liens vers les diverses langues.

Ceci est à ne surtout pas faire. Cela avait notemment été dit par Elie Sloïm lors des ateliers Paris Web de l'an dernier (d'ailleurs les dates du prochain Paris Web ont été divulguées !) Suite à une discussion sur le forum seosphere, je publie ici le code que j'utilise sur RefStats pour détecter la langue préférée de l'utilisateur et le rediriger vers le répertoire approprié. Si vous utilisez des sous-domaines, il suffit de déplacer /fr en fr. Note : le code ici est adapté afin d'enleve la dépendance à CakePHP.

$langs = array('en' => array('title' => 'English', 'visible' => 0) , 'fr' => array('title' => 'Français', 'visible' => 1)); $defaultLang = 'fr';

$_d = split ('[,;]', $_SERVER['HTTP_ACCEPT_LANGUAGE']); $f = false; $selected = null; foreach($_d as $l) { if (isset($langs[$l]) && $langs[$l]['visible'] == 1 && $f == false) { $selected = $l; $f = true; } } if ($f == false) $selected = $defaultLang;

header('location: /'.selected, true, 301); die();

Quelques explications. Ce code doit être placé sur votre page /index.php. la variable $langs contient toutes les langues disponibles. Ici, j'ai mis un élément 'nom' car dans le cas de RefStats, cette variable sert également à mettre le nom de la langue en haut de page. Le 'visible' n'est pas non plus utile ici.

Nous regardons donc le contenu de HTTP_ACCEPT_LANGUAGE, qui contient les langues préférées de l'utilisateur, par ordre de préférence. On prends la première que l'on connaisse. Si il n'y en a aucune, on prends la valeur de $defaultLang. Et on redirige vers le répertoire de la langue.

Il est totalement inutile de placer la langue dans la session. En effet, vu que vous serez ensuite dans le répertoire de la langue, vous aurez toujours l'information de celle-ci. Et si vous placez l'information en session et forcez toujours cette langue, vous courrez le risque d'avoir un utilisateur qui aurait une configuration inappropriée (cela peut arriver lors de vacances à l'étranger par exemple) qui ne pourra pas voir votre site dans sa langue maternelle.

Pour finir, voici le code non modifié pour ceux qui utiliseraient CakePHP.

Dans app_controller.php, avant la déclaration de classe : uses('L10n');

Dans app_controller.php, en dehors de toute méthode : var $langs = array('en' => array('title' => 'English', 'id' => 'eng', 'visible' => 0) , 'fr' => array('title' => 'Francais', 'id' => 'fre', 'visible' => 1, 'wp_cat' => 3)); var $defaultLang = 'fr';

Dans app_controller.php, fonction beforeFilter : if (!isset($this->params['lang']) && !empty($this->params)) { $_d = split ('[,;]', env('HTTP_ACCEPT_LANGUAGE')); $f = false; $selected = null; foreach($_d as $l) { if (isset($this->langs[$l]) && $this->langs[$l]['visible'] == 1 && $f == false) { $selected = $l; $f = true; } } if ($f == false) $selected = $this->defaultLang;

$this->redirect('/'.$selected, 301); die(); } elseif (!empty($this->params['lang']) && isset($this->langs[$this->params['lang']])) { Configure::write('Config.language', $this->langs[$this->params['lang']]['id']); } elseif (!empty($this->params)) { $this->cakeError('error404', array(array('action' => $this->action))); } else { $url = env('argv'); $url = $url[0]; $url = '/'.str_replace('url=', '', $url);

$l = explode('/', $url); $l = $l[1];

if (!isset($this->langs[$l]) && Configure::read('debug') == 0) { $this->cakeError('error404', array(array('action' => $this->action))); } } $this->params['langs'] = $this->langs;

Et enfin dans routes.php, pour permettre l'ajout du paramètre de langue avant le contrôleur et l'action : Supprimer la ligne suivante : Router::connect('/', array('controller' => 'pages', 'action' => 'display', 'home')); Et ajouter à la place les deux lignes suivantes : Router::connect('/:lang/', array('controller' => 'pages', 'action' => 'display', 'home')); Router::connect('/:lang/:controller/:action/*');

Ceci définira la langue dans la configuration de CakePHP, vous permettant par la suite d'utiliser les outils d'internationalisation du framework.

CakePHP : exécuter une "task" via l'interface web

CakePHP fournit de base quelques méthodes permettant de développer des applications dans le but de les exécuter avec PHP-cli, c’est à dire de les exécuter comme des scripts shell au lieu de passer par l’interface graphique.

Je ne propose pas ici un tutoriel permettant de développer ce genre de scripts. Il en existe déjà un résumant assez bien la chose sur cakebaker (en).

Après avoir développé une application, nous avons plusieurs tâches qui sont exécutées dans le script shell principal.

Cependant il peut être utile, pour diverses raisons de tests, d’analyses de bugs voir statistiques, d’exécuter les mêmes méthodes présentes dans nos tasks mais via une page web.

Il serait fortement bête de devoir faire un copier/coller de notre méthode et de la remettre dans un contrôleur. Cela ne serait pas vraiment en concordance avec le mojo du framework ("think twice, code once").

Supposons donc que dans votre contrôleur "admin", vous désiriez utiliser la méthode "makeTest" de la tâche "test". Créez une nouvelle action dans votre contrôleur.

Dans celle-ci, placez les lignes suivantes :

include(APP.'console/libs/shell.php');
vendor('shells/tasks/test');

Vous avez ici inclu dans votre action les fichiers nécessaires l’instanciation de votre tâche.

Le premier fichier contient la classe parente à celle que doit avoir votre tâche. La seconde contient votre tâche (remplacer "test" par le nom de votre tâche).

$tache = $test->makeTest();

Maintenant initialisons notre tâche :

$test = new TestTask();

Et exécutons la méthode que nous désirons pour notre tâche.

$tache = $text->makeTest();

Vous n’avez maintenant plus qu’à effectuer l’action que vous désirez avec les données que votre méthode vous renvoie.

Irish Coffee