Tutoriel : Création d'un projet avec FuelPHP
La semaine dernière, j’ai rejoint l’équipe du projet open-source Novius OS basé sur le framework FuelPHP. Mes collègues de Novius Labs m’ont initié à ce nouveau framework. Je prends donc la suite de Julian pour vous en expliquer le fonctionnement. Aujourd’hui, on rentre dans le détail et passons à un cas concret.
Note du 20/12/2011 : cet article a été mis à jour par rapport à la nouvelle version 1.1 de Fuel sortie récemment. Les codes affichés, les commandes ainsi que certaines prises d'écran ont donc sensiblement changés depuis la version initiale.
Objectif
Sommaire du tutoriel
Développer sous FuelPHP
- Choix du framework
- Mise en place
- Création d'un projet
- Application type
- Cheat sheet
- L'ORM : gestion des relations
L’objectif de cet article n’est pas d’être exhaustif sur le framework, mais de vous donner les armes nécessaires pour commencer un projet. La connaissance d’un framework PHP ou autre (Ruby on Rails, Symfony, CakePHP...) est un plus mais n’est pas nécessaire pour suivre ce tutoriel.
Tout au long des articles 3 et 4 de notre tutoriel FuelPHP, nous allons nous baser sur un exemple : l’élaboration d’un agenda accessible via internet (où il est possible d’écrire des notes, d’entrer ses contacts etc...).
Pour les curieux, voici un aperçu :
Vous vous en doutez, la réalisation graphique ne fait pas partie des objectifs de ce tutoriel. Je tiens néanmoins à donner le crédit des icônes issues de l’excellent site the Noun Project.
Ce tutoriel part du principe que vous avez lu l’article précédent de Julian et installé une instance du projet. Nous utiliserons MySQL et un logiciel de gestion de base de données (phpMyAdmin).
Configuration de la base de données
Via votre logiciel de gestion de base de données préféré, créez une base de données que vous nommerez tuto_agenda.
Il faut maintenant configurer FuelPHP pour qu’il puisse accéder à cette base.
Il faut d’abord modifier le fichier fuel/app/config/db.php qui va vous permettre d’accéder à votre serveur MySQL. Voici ce que contient le fichier de base :
return array(
'active' => 'default',
/**
* Base config, just need to set the DSN, username and password in env. config.
*/
'default' => array(
'type' => 'pdo',
'connection' => array(
'persistent' => false,
),
'identifier' => '`',
'table_prefix' => '',
'charset' => 'utf8',
'caching' => false,
'profiling' => false,
),
'redis' => array(
'default' => array(
'hostname' => '127.0.0.1',
'port' => 6379,
)
),
);
On peut voir qu’on peut ici spécifier les informations de connexion (le type, l’adresse et le port du serveur - type, hostname et port) ainsi que certaines options (gestion du cache, le préfixe à utiliser devant chaque table...).
Le type de base est PDO. Il est possible de le laisser tel quel, cependant certaines opérations de migration de ce type ne sont pas encore supportées par FuelPHP. Nous allons donc modifier ce type en MySQL. Nous ne modifierons aucun des autres paramètres, ce n’est pas l’objet de ce tutoriel.
return array(
'active' => 'default',
/**
* Base config, just need to set the DSN, username and password in env. config.
*/
'default' => array(
'type' => 'mysql',
'connection' => array(
'persistent' => false,
),
'identifier' => '`',
'table_prefix' => '',
'charset' => 'utf8',
'caching' => false,
'profiling' => false,
),
'redis' => array(
'default' => array(
'hostname' => '127.0.0.1',
'port' => 6379,
)
),
);
Il faut maintenant spécifier les identifiants et la base sur laquelle se connecter. Le framework FuelPHP propose, par défaut, de séparer cette partie de la configuration entre mode de développement et mode de production. Nous allons suivre cette démarche, mais sachez qu’il est possible de tout configurer dans le fichier précédent. A l'inverse, il est possible de spécifier des bases de données différentes et d’autres configurations dans les fichiers de configuration développement / production.
Si la configuration n’a pas été modifiée, nous sommes normalement en mode développement. Ouvrez donc le fichier fuel/app/config/development/db.php. Le contenu de base est le suivant :
return array(
'default' => array(
'connection' => array(
'dsn' => 'mysql:host=localhost;dbname=fuel_dev',
'username' => 'root',
'password' => 'root',
),
),
);
Cette configuration est adaptée au type de connexion PDO. Il faut l’adapter au type MySQL et à vos propres informations de connexion. Remplacez par le code ci-dessous en modifiant votre identifiant et votre mot de passe :
return array(
'default' => array(
'connection' => array(
'hostname' => 'localhost',
'database' => 'tuto_agenda',
'username' => 'IDENTIFIANT',
'password' => 'MOT DE PASSE',
),
),
);
La configuration de la base de données est normalement maintenant terminée. Il est temps de passer à la création de l'agenda.
Le principe
1. Génération de code par Scaffold
Nous allons maintenant générer un code de base, qui va nous permettre d’avoir un aperçu du fonctionnement de FuelPHP. Il existe, dans beaucoup de frameworks (comme Rails), la possibilité de générer du code via Scaffold. Ce code permet de gérer les actions de base d’un objet (visualisation, création, modification, suppression - CRUD en anglais). C’est l’idéal pour avoir une base de développement !
Beaucoup d’opérations peuvent s’effectuer sous FuelPHP avec le script Oil (remarquez fuel / oil). Ce script permet notamment de générer des migrations, des modèles, des contrôleurs... et le scaffolding.
Un agenda contient notamment une partie où il est possible de gérer des notes, qui contiennent un titre et une description. Nous allons générer le code via la ligne de commande suivante (après s’être rendu dans le répertoire root du projet) :
php oil generate scaffold/crud notes titre:string description:text
Vous pouvez remarquer que l’écriture de cette ligne de commande est assez simple :
php oil generate scaffold/crud <NOM DE LA TABLE> <ATTRIBUT>:<TYPE> <ATTRIBUT>:<TYPE> <ATTRIBUT>:<TYPE>...
Notez que l'extension scaffold/crud indique que l'on souhaite utiliser la structure CRUD. Oil peut générer du code suivant d'autres structure (comme orm). La différence se fera notamment voir au niveau du code généré dans les modèles (qui n'étenderont pas les mêmes classes).
Lorsque la commande est exécutée, Oil nous indique l’ensemble des fichiers qui ont été modifiés :
Vous pouvez voir qu’il y en a un certain nombre :
- fuel/app/classes/model/note.php : le modèle associé aux notes
- fuel/app/migrations/001_create_notes.php : le fichier migration, qui nous permettra de migrer la base de donnée en créant la table notes.
- fuel/app/classes/controller/notes.php : le contrôleur associé aux notes
- tous les fichiers dans fuel/app/views/notes/ qui sont les vues associées aux actions
- fuel/app/views/template.php qui est la vue implémentant la structure de la page
2. Migration de la base de données
Si le fichier migration a bel et bien été généré, la table n’a pas été créée pour autant. Il y a dans Oil une commande permettant d’éxécuter ces fichiers migration.
php oil refine migrate
Oil permet une liberté beaucoup plus grande au niveau des migrations, nous y reviendrons plus tard.
Une fois la commande executée, vous pouvez voir que la base de données n’est plus vide. Elle contient maintenant deux tables :
Il y a la table migration, qui enregistre quelles migrations ont été effectuées. Nous y reviendrons plus tard.
Il y a ensuite la table notes que nous voulions créer. Jetons un coup d’œil à la structure :
A partir des deux colonnes que nous avons spécifiées à Oil pour le scaffold (titre et description), il en a généré cinq :
- id : identifiant de la note avec son index primary associé
- titre : le titre de la note. Nous avons spécifié un type string, vous savez maintenant que l’équivalence MySQL est varchar(255)
- description : la description de la note
- created_at : date de création de la note. Est gérée par défaut dans le modèle.
- updated_at : date de modification de la note. Est gérée par défaut dans le modèle.
A noter qu’il est possible de désactiver la création des colonnes created_at et updated_at en éditant le fichier de migration (si vous générez le modèle via generate model, il y a aussi l’option --no-timestamp, mais pour le moment elle ne semble pas prise en compte pour le scaffolding).
La présence de ces colonnes n’est pas gênante et sera probablement utile, donc laissons les.
3. Aperçu
La page d’accueil n’a toujours pas changé, mais il est possible maintenant d’accéder aux notes via l’adresse suivante (si vous avez suivi l’installation de Julian) :
http://localhost/mon_site_fuel/public/notes si la redirection est activée
http://localhost/mon_site_fuel/public/index.php/notes sinon
Le tout doit normalement ressembler à :
Liste des notes
Création / édition d'une note
Vue d'une note
Le code généré gère même les notifications :
Après mise à jour d'une note
4. Le code généré
Maintenant que le système fonctionne, nous pouvons jeter un coup d’œil au code généré.
Le scaffold a généré un modèle, situé dans fuel/app/classes/model/note.php qui contient :
<?php
class Model_Note extends Model_Crud
{
protected static $_table_name = 'notes';
public static function validate($factory)
{
$val = Validation::forge($factory);
$val->add_field('titre', 'Titre', 'required|max_length[255]');
$val->add_field('description', 'Description', 'required');
return $val;
}
}
Le contenu est assez simple, car notre modèle étend la classe Model_Crud, qui contient déjà les fonctions de bases, nous permettant de manipuler l’objet dans le code. Il y a une seule variable, $_table_name qui, comme son nom l’indique, définit le nom de la table associée au modèle. Il y a aussi une fonction, validate, qui permet d'imposer des conditions lors de la sauvegarde d'un élément. Par exemple, lors de la sauvegarde d'une note, le titre ne doit pas être vide et sa longueur ne doit pas dépasser 255 caractères. La description elle aussi ne doit pas être vide. Il est possible d'avoir dans ce domaine un degré beaucoup plus grand de liberté, nous y reviendrons plus tard.Le scaffold a aussi généré un contrôleur, situé dans fuel/app/classes/controller/notes.php, qui contient (j’ai résumé certaines actions) :
<?php
class Controller_Notes extends Controller_Template
{
public function action_index()
{
$data['notes'] = Model_Note::find_all();
$this->template->title = "Notes";
$this->template->content = View::forge('notes/index', $data);
}
public function action_view($id = null)
{
$data['note'] = Model_Note::find_by_pk($id);
$this->template->title = "Note";
$this->template->content = View::forge('notes/view', $data);
}
public function action_create($id = null)
{
//...
}
public function action_edit($id = null)
{
//...
}
public function action_delete($id = null)
{
//...
}
}
On peut voir que le contrôleur est composé de cinq fonctions, commençant toutes par action_. Il s’agit des actions du contrôleur. Si, par exemple, vous vous rendez sur :
http://localhost/mon_site_fuel/public/notes/create ou
http://localhost/mon_site_fuel/public/index.php/notes/create sans redirection
C’est la fonction action_create qui sera appelée dans le contrôleur.
Quant à l’adresse :
http://localhost/mon_site_fuel/public/notes/
elle est équivalente (par défaut) à l’adresse :
http://localhost/mon_site_fuel/public/notes/index
Donc appelera bien l’action action_index.
Analysons la fonction action_index (qui, comme vous pouvez le déduire de l’adresse, liste l’ensemble des notes qui ont été créées) :
- La première ligne charge l’ensemble des notes grâce à la fonction statique find_all. Cette fonction peut bien sûr recevoir d’autres paramètres, je vous invite à consulter la documentation associée.
- La seconde et la troisième lignes modifient toutes les deux des attributs de $this->template, un attribut title (qui est vraisemblablement le titre de la page) et un attribut content, auquel est affectée la vue. Je reviendrai un peu plus tard sur cette affectation.
L’objet qui a été modifié est l’instanciation d’un template (qui est la structure de la page). Pour faire court, les attributs de cet objet vont pouvoir être récupérés par le template. Ce template a lui aussi été généré via le scaffold. Il est présent dans l’adresse fuel/app/views/template.php. Son contenu est le suivant (j’ai résumé le style pour plus de lisibilité) :
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title><?php echo $title; ?></title>
<?php echo Asset::css('bootstrap.css'); ?>
<style>
body { margin: 40px; }
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="span16">
<h1><?php echo $title; ?></h1>
<hr>
<?php if (Session::get_flash('success')): ?>
<div class="alert-message success">
<p>
<?php echo implode('</p><p>', (array) Session::get_flash('success')); ?>
</p>
</div>
<?php endif; ?>
<?php if (Session::get_flash('error')): ?>
<div class="alert-message error">
<p>
<?php echo implode('</p><p>', (array) Session::get_flash('error')); ?>
</p>
</div>
<?php endif; ?>
</div>
<div class="span16">
<?php echo $content; ?>
</div>
</div>
<footer>
<p class="pull-right">Page rendered in {exec_time}s using {mem_usage}mb of memory.</p>
<p>
<a href="http://fuelphp.com">FuelPHP</a> is released under the MIT license.<br>
<small>Version: <?php echo e(Fuel::VERSION); ?></small>
</p>
</footer>
</div>
</body>
</html>
Ainsi, la valeur de l’attribut $this->template->title est accessible via la variable $title dans le template. Si on modifie dans action_index :
$this->template->title = "Mes notes";
Lorsqu’on se rendra de nouveau sur http://localhost/mon_site_fuel/public/notes/, le titre aura été modifié :
Sachez qu’il est possible de modifier tous les attributs de $this->template (comme $this->template->description par exemple). Cet attribut sera alors accessible en tant que variable ($description) dans le template. Il est aussi possible de changer de template, nous y reviendrons plus tard.
Revenons au contrôleur, la dernière affectation, comme je le disais, affecte le contenu de la vue :
$this->template->content = View::forge('notes/index', $data);
View::forge retourne la vue située dans fuel/app/views/notes/index.php avec les variables définies dans $data. Jetons un coup d’oeil à ce fichier :
<h2>Listing Notes</h2>
<br>
<?php if ($notes): ?>
<table class="zebra-striped">
<thead>
<tr>
<th>Titre</th>
<th>Description</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($notes as $note): ?> <tr>
<td><?php echo $note->titre; ?></td>
<td><?php echo $note->description; ?></td>
<td>
<?php echo Html::anchor('notes/view/'.$note->id, 'View'); ?> |
<?php echo Html::anchor('notes/edit/'.$note->id, 'Edit'); ?> |
<?php echo Html::anchor('notes/delete/'.$note->id, 'Delete', array('onclick' => "return confirm('Are you sure?')")); ?>
</td>
</tr>
<?php endforeach; ?> </tbody>
</table>
<?php else: ?>
<p>No Notes.</p>
<?php endif; ?><p>
<?php echo Html::anchor('notes/create', 'Add new Note', array('class' => 'btn success')); ?>
</p>
La fonction View::forge retourne donc le contenu du fichier executé en PHP avec les variables définies dans $data. Ainsi $data[‘notes’] est accessible via la variable $notes dans la vue index.php.
On peut remarquer au passage dans le fichier de nombreux appels à la fonction Html::anchor. Il s’agit d’un helper, c’est à dire d’une fonction retournant des bouts de code en Html. Dans ce cas ci il s’agit d’un lien, avec comme classes css btn success :
<?php echo Html::anchor('notes/create', 'Add new Note', array('class' => 'btn success')); ?>
est équivalent à
<a class="btn success" href="http://localhost/mon_site_fuel/public/notes/create">Add new Note</a>
Il est parfaitement possible de s’en passer et d’écrire ce code HTML directement. Html::anchor permet cependant de se simplifier la vie (vous pouvez voir qu’il écrit l’url complète), et l’utilisation d’helpers de manière générale permet d’améliorer la maintenabilité (on peut imaginer faire une surcouche à Html::anchor pour passer de liens directs à des requêtes en ajax par exemple, le tout en ne modifiant qu’un seul bout de code).
La fonction Html::anchor supporte d’autres paramètres. Je vous invite à consulter la documentation associée.
Pour résumer :
- Lorsqu’un utilisateur se rend dans une adresse d’un site fuelPHP
http://localhost/mon_site_fuel/public/notes/ - Une fonction du contrôleur notes est appelée (le cas présent action_index)
- Cette fonction charge les variables qui lui sont nécessaires, affecte les attributs de $this->template (que ce soit directement une chaine de caractère ou le retour d’un View::Forge)
- Le template est affiché à l’utilisateur
Si vous avez eu l’occasion d’utiliser d’autres framework, vous avez probablement remarqué qu’il n’est souvent pas nécessaire de spécifier la vue dans l’action. Par exemple dans l’action action_index :
public function action_index()
{
$data['notes'] = Model_Note::find_all();
$this->template->title = "Mes notes";
$this->template->content = View::forge('notes/index', $data);
}
Il n’est pas là nécessaire de définir $this->template->title, ni $this->template->content (pour chaque action, une vue est attibuée par défaut). Cette politique permet de limiter les répétitions dans le code.
Avec FuelPHP, ce comportement n’est pas disponible par défaut mais, grâce à la fonction after, que nous verrons plus tard, il est possible de l’imiter. C’est donc avant tout une force du framework, qui laisse une liberté plus grande au niveau des templates.
5. Paramètres et actions
Le traitement des paramètres dans les actions est un point important, car il va vous permettre notamment d’interagir avec l’internaute.
a. Via les paramètres d’une action
Jetons un coup d’oeil à l’action action_view (qui permet de visualiser une note en particulier) dans le contrôleur notes :
public function action_view($id = null)
{
$data['note'] = Model_Note::find_by_pk($id);
$this->template->title = "Note";
$this->template->content = View::forge('notes/view', $data);
}
On peut remarquer la présence d’un paramètre dans la déclaration, contrairement à action_index. Lorsqu’on fait appel à l’action action_view, c’est qu’on veut charger une note en particulier, que l’on récupère grâce à l’identifiant.
Ainsi, lorsque l’utilisateur accédera à l’adresse :
http://localhost/mon_site_fuel/public/notes/view/1
La fonction action_view sera appelée avec comme paramètre $id = 1. Il est parfaitement possible de multiplier le nombre de paramètres.
b. Via la classe Input
Si vous jetez un coup d’œil à la fonction action_create, vous pouvez remarquer la présence de Input::post.
Input::post et Input::get permettent de récupérer les valeurs $_POST et $_GET respectivement. Je vous invite à consulter la documentation associée.
6. Les partials
Si vous consultez les fichiers vues fuel/app/views/notes/edit.php et fuel/app/views/notes/create.php, vous remarquez une ligne en commun dans les deux fichiers :
<?php echo render('notes/_form'); ?>
Ils incluent tous les deux un partial, c’est à dire un bout de vue. En effet, si on compare le formulaire de création et d’édition, les deux vues sont très similaires. Elles font donc appel toutes les deux à un même partial (bout de vue), reprenant le formulaire. L’appel à des partials permet de limiter la répétition du code et donc d’améliorer la maintenabilité d’un logiciel. A utiliser tant que possible !
La fonction render permet donc de renvoyer le contenu executé d’un partial, dans notre cas fuel/app/views/notes/_form.php. Voici le contenu :
<?php echo Form::open(array('class' => 'form-stacked')); ?>
<fieldset>
<div class="clearfix">
<?php echo Form::label('Titre', 'titre'); ?>
<div class="input">
<?php echo Form::input('titre', Input::post('titre', isset($note) ? $note->titre : ''), array('class' => 'span6')); ?>
</div>
</div>
<div class="clearfix">
<?php echo Form::label('Description', 'description'); ?>
<div class="input">
<?php echo Form::textarea('description', Input::post('description', isset($note) ? $note->description : ''), array('class' => 'span10', 'rows' => 8)); ?>
</div>
</div>
<div class="actions">
<?php echo Form::submit('submit', 'Save', array('class' => 'btn primary')); ?>
</div>
</fieldset>
<?php echo Form::close(); ?>
A noter qu’on accède à la variable $note. En effet, cette variable est accessible car dans l'action edit on peut voir cette ligne :
$this->template->set_global('note', $note, false);
Cette variable est affectée au template en tant que globale et est donc accessible à toutes les vues, sans avoir à la passer dans leurs paramètres. Ce comportement peut être pratique si on a une variable qu'on veut accéder partout (comme l'utilisateur enregistré en session ou une configuration globale), mais est peu recommandée pour les autres cas car elle réduit la lisibilité du code.
Le partial fait notamment appel à la classe helper Form, qui permet d’instancier un formulaire et des champs en HTML. Je vous invite à consulter la documentation associée.
J'espère que cet article vous a permis de vous lancer sur FuelPHP. N'hésitez pas à me contacter en commentaire ou sur Twitter si vous avez des questions. Dans un article prochain, nous passerons à l'action ! Avec les connaissances acquises dans cet article, nous finaliserons notre agenda en améliorant l'affichage, en organisant un menu, en complexifiant notre modèle de données et, enfin, en mettant en place un système d'authentification.