Tutoriel FuelPHP #4 : une application type

Cet article s'inscrit dans la suite du précédent article Création d'un projet avec FuelPHP. Le but de cet article n'est pas d'être complet, mais de vous donner les bases pour développer une application. L'exemple que nous allons étudier est un agenda.


Conception de l'agenda

Sommaire du tutoriel
Développer sous FuelPHP

  1. Choix du framework
  2. Mise en place
  3. Création d'un projet
  4. Application type
  5. Cheat sheet
  6. L'ORM : gestion des relations

1. Structure des pages

Commençons par la structure des pages, très classique (voir rétro) pour ce type d’application.

En partie supérieure, le header, qui indique que nous nous trouvons dans l’agenda.
Juste en dessous, un menu, nous indiquant dans quelle catégorie nous nous trouvons.
En dessous et à gauche, la liste. Lorsqu’on se trouve dans la catégorie Notes, il s’agit de la liste des notes. Quand on se trouve dans la catégorie Adresses et numéros, il s’agit de la liste des adresses.
À droite la vue : quand un utilisateur sélectionne un élément dans la liste, il peut alors le visualiser et effectuer les actions de base dans cette partie.
Tout en bas, le footer, reprenant le footer de FuelPHP.

Le header, le menu et le footer peuvent se présenter directement dans le template.
Par contre, la liste et la vue seront générées dans des vues différentes.

 

2. Modèle de données

Nous avons déjà créé les notes qui contiennent chacune un titre et une description.
Nous rajouterons les personnes (catégorie adresses et numéros), qui contiennent un nom, un prénom, un numéro de téléphone, une adresse et des informations additionnelles.

 

Installation de fichiers annexes

Nous aurons besoin du framework jQuery, de fichiers CSS et d’images pour que le style soit déjà en place. Ces ajouts cosmétiques (si on veut :)) ne sont pas l’objet de ce tutoriel (heureusement !). Vous pouvez trouver la source finale ici. Contentez vous de remplacer votre dossier public/assets par celui de la source finale.

À noter que les fichiers CSS ont été générés avec le framework Compass. Encore une fois, ce n'est pas l’objet de ce tutoriel.

 

Implémentation

1. Implémentation du template

Avant de modifier les vues ou les actions, on va modifier le template qui représente la structure de la page.

Ouvrez le fichier fuel/app/views/template.php, on y insérera le code suivant :

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title><?php echo $title; ?></title>
    <?php echo Asset::css('screen.css'); ?>
    <?php echo Asset::css('print.css'); ?>
    <!--[if IE]>
    <?php echo Asset::css('ie.css'); ?>
    <![endif]-->
    <?php echo Asset::css('agenda.css'); ?>
    <?php echo Asset::js('jquery-1.7.1.min.js'); ?>
</head>
<body>
<div id="main">
    <div id="header">
        Agenda
    </div>
    <?php echo render('_menu'); ?>
    <div id="content">
        <?php if (Session::get_flash('error')): ?>
        <div class="error"><?php echo implode('<br/>', (array) Session::get_flash('error')); ?></div>
        <?php endif; ?>
        <?php if (Session::get_flash('success')): ?>
        <div class="success"><?php echo implode('<br/>', (array) Session::get_flash('success')); ?></div>
        <?php endif; ?>
            <div class="inside">
                <?php echo $content; ?>
            </div>
    </div>
    <div id="footer">
        <a href="http://fuelphp.com">Fuel</a> is released under the MIT license.<br />Page rendered in {exec_time}s using {mem_usage}mb of memory.
    </div>
</div>
</body>
</html>

On ajoute les fichiers CSS et JS via la classe Asset (respectivement Asset::css et Asset::js). Par exemple :

<?php echo Asset::css('screen.css'); ?>

générera le code suivant en HTML :

<link rel="stylesheet" type="text/css" href="http://localhost/mon_site_fuel/public/assets/css/screen.css?1322929428" />

Il s'agit encore d'helpers qui simplifient la tâche de l'utilisateur. En plus de limiter le nombre de caractères à écrire, vous remarquerez, à la fin de l'URL du fichier CSS, un nombre obligeant le navigateur à recharger le fichier s'il a été modifié depuis la dernière visite (sans passer par le cache). Ces deux fonctions ont aussi d'autres paramètres, je vous invite à consulter la documentation associée.

Sinon, vous pouvez facilement séparer les différentes parties qu'on avait définies lors de l'introduction. Le header et le footer sont de simples DIV. Le menu est affiché grâce au partial _menu. Dans le DIV content, on constate que, pour le moment, aucune différentiation n'existe entre la zone "Liste" et la zone "Vue". On y reviendra un peu plus tard dans cet article.

 

2. Implémentation du menu

Maintenant que nous faisons appel au partial _menu, il faut bien implémenter le fichier. Créez le fichier fuel/app/views/_menu.php et ouvrez-le avec votre éditeur préféré.

<div id="menu">
    <?php echo Html::anchor('notes/index', 'Notes', array('class' => 'item '.(\\Request::active()->controller == 'Controller_Notes' ? 'active' : ''))) ?>
    <?php echo Html::anchor('people/index', 'Adresses et numéros', array('class' => 'item '.(\\Request::active()->controller == 'Controller_People' ? 'active' : ''))) ?>
</div>

On peut voir qu'il s'agit avant tout d'un ensemble de liens. Vous connaissez déjà la fonction Html::anchor qui permet de générer des liens. On ajoute par défaut à ces liens la classe item, puis, conditionnellement, la classe active permettant de surligner le menu actuel. Jetons un coup d'œil à la première condition :

\\Request::active()->controller == '\\Controller_Notes' ? 'active' : ''

La fonction \\Request::active() permet de récupérer les information sur la requête de l'utilisateur. Notamment :

\\Request::active()->controller

qui permet de connaître le contrôleur par lequel l'utilisateur est passé, et

\\Request::active()->action

qui permet de connaître l'action appelée. Je vous invite à consulter la documentation associée.

Connaître le contrôleur et l'action qui ont été appelés peut être pratique dans certains cas dans les vues, mais c'est plus souvent le cas dans les contrôleurs. Dans le cas du menu, cela nous évite d'envoyer un paramètre au niveau de chaque contrôleur pour indiquer le menu sélectionné.

 

3. Généralisation du fonctionnement des contrôleurs

Dans l'article précédent, nous avons eu l'occasion de remarquer un certain nombre de répétitions dans les contrôleurs générés par Scaffold :

<?php
class Controller_Notes extends Controller_Template 
{

	public function action_index()
	{
		//...
		$this->template->title = "Mes notes";
		$this->template->content = View::forge('notes/index', $data);
	}

	public function action_view($id = null)
	{
		//...
		$this->template->title = "Note";
		$this->template->content = View::forge('notes/view', $data);
	}

	public function action_create($id = null)
	{
		//...
		$this->template->title = "Notes";
		$this->template->content = View::forge('notes/create');
	}

	public function action_edit($id = null)
	{
		//...
		$this->template->title = "Notes";
		$this->template->content = View::forge('notes/edit');
	}

	//...


}

Ces répétions peuvent sembler un peu excessives, notamment au niveau de l'attribut $this->template->content, où l'on charge tout le temps le fichier vue. En effet, lorsqu'on appelle l'action index, on affiche notes/index, quand on appelle l'action view on affiche notes/view et, de manière générale, lorsqu'on appelle l'action NOM_ACTION du contrôleur NOM_CONTROLEUR, on s'attend à afficher NOM_CONTROLEUR/NOM_ACTION (c'est du moins le fonctionnement dans plusieurs frameworks, comme Rails ou Symfony).

Il est, cependant, parfaitement possible de reproduire ces fonctionnements. Vu qu'on veut appliquer ces changements sur plusieurs contrôleurs, on peut créer un nouveau contrôleur parent, qui sera étendu par les autres contrôleurs. Créons donc ce contrôleur à l'adresse fuel/app/classes/controller/agenda.php.

À noter qu'il est possible de créer ce contrôleur via Oil :

oil generate controller agenda

Mais, cette commande générera aussi des fichiers vues qui ne nous sont pas utiles (donc supprimez le dossier fuel/app/views/agenda/ si vous utilisez ce moyen). Pour atteindre le comportement cité plus haut, une implémentation possible d'un tel fichier est :

<?php
class Controller_Agenda extends Controller_Template{
    var $data = array(); // Données à envoyer dans la vue
    var $title = 'Agenda'; // Titre de la page. Défaut : Agenda
    var $view_dir; // Dossier vue du controlleur.

    public function after($response)
    {
        $action_name     = Request::active()->action;
        $this->template->title   = $this->title;
        $this->template->content = View::forge($this->view_dir.'/'.$action_name, $this->data);

        return parent::after($response);
    }
}
?>

Lors d'un précédent article, nous avons expliqué l'ordre d'exécution lorsqu'une requête est exécutée. Tout ce qu'il faut comprendre ici, c'est que lorsqu'une requête est exécutée :

  • La fonction before est appelée
  • Puis c'est la fonction correspondant à l'action
  • Puis la fonction after
  • Puis le template est affiché

Ainsi, la fonction after sera toujours appelée après les actions des contrôleur étendant le contrôleur agenda.

Analysons le contenu de cette fonction :

  • Tout d'abord, on récupère le nom de l'action appelée via Request::active()->action.
  • On remplit l'attribut $this->template->title avec $this->title qui, par défaut, vaut 'Agenda' (donc pas besoin de le définir ; il y aura une valeur par défaut).
  • On remplit enfin l'attribut $this->template->content, qui va afficher la vue issue du fichier $this->view_dir/fichier-au-nom-de-l-action-appelee (le dossier sera défini dans le contrôleur). Les paramètres du fichier vue proviennent de $this->data, défini à l'appel à l'action.

Ainsi, le nouveau contrôleur Notes doit ressembler à :

<?php
class Controller_Notes extends Controller_Agenda
{
    var $view_dir = 'notes';

	public function action_index()
	{
		$this->data['notes'] = Model_Note::find_all();

	}

	public function action_view($id = null)
	{
		$this->data['note'] = Model_Note::find_by_pk($id);

	}

	public function action_create($id = null)
	{
		if (Input::method() == 'POST')
		{
			$val = Model_Note::validate('create');
			
			if ($val->run())
			{
				$note = Model_Note::forge(array(
					'titre' => Input::post('titre'),
					'description' => Input::post('description'),
				));

				if ($note and $note->save())
				{
					Session::set_flash('success', 'Added note #'.$note->id.'.');
					Response::redirect('notes');
				}
				else
				{
					Session::set_flash('error', 'Could not save note.');
				}
			}
			else
			{
				Session::set_flash('error', $val->show_errors());
			}
		}

	}

	public function action_edit($id = null)
	{
		$note = Model_Note::find_one_by_id($id);

		if (Input::method() == 'POST')
		{
			$val = Model_Note::validate('edit');

			if ($val->run())
			{
				$note->titre = Input::post('titre');
				$note->description = Input::post('description');

				if ($note->save())
				{
					Session::set_flash('success', 'Updated note #'.$id);
					Response::redirect('notes');
				}
				else
				{
					Session::set_flash('error', 'Nothing updated.');
				}
			}
			else
			{
				Session::set_flash('error', $val->show_errors());
			}
		}

		$this->template->set_global('note', $note, false);

	}

	public function action_delete($id = null)
	{
		if ($note = Model_Note::find_one_by_id($id))
		{
			$note->delete();

			Session::set_flash('success', 'Deleted note #'.$id);
		}

		else
		{
			Session::set_flash('error', 'Could not delete note #'.$id);
		}

		Response::redirect('notes');

	}


}

Les actions sont ainsi un peu plus courtes (le contenu de action_index ne fait qu'une seule ligne), on n'a plus besoin de spécifier la vue à afficher (ça nous permet aussi de forcer la norme "une action / une vue d'un même nom"), on ressemble plus aux frameworks existants et, enfin, on ne se préoccupe même plus du template.

On est d'accord ; le fonctionnement précédent était tout à fait acceptable. Le but de cette implémentation est avant tout de se simplifier la vie.

 

4. Affichage vue et liste

Dans la partie précédente, on est parti de la supposition qu'à chaque requête on n'affichait qu'une seule zone dans le template. Le cas de l'agenda est un peu plus spécifique, on affiche deux zones : la zone liste et la zone vue.

Tout d'abord, remplaçons dans le template :

<div class="inside">
     <?php echo $content; ?>
</div>

par

<div id="list_zone">
    <?php echo $list; ?>
</div>
<div id="view_zone">
    <div class="inside">
        <?php echo $view; ?>
   </div>
</div>

On peut maintenant afficher deux zones dans notre template $list et $view. Il suffit de modifier le contrôleur agenda pour remplacer l'attribut $this->template->content par $this->template->view. On peut également considérer que $this->template->list affichera toujours la même vue liste. On remplace alors la fonction after dans le contrôleur agenda par :

public function after($response)
{
    $action_name     = Request::active()->action;
    $this->template->title   = $this->title;
    $this->template->list = View::forge($this->view_dir.'/list', $this->data);
    $this->template->view = View::forge($this->view_dir.'/'.$action_name, $this->data);

    return parent::after($response);
}

Il faut cependant que la liste soit chargée. On peut le faire via la fonction after du contrôleur Notes :

public function after($response)
{
    $this->data['list'] = Model_Note::find(array('order_by' => array('titre' => 'asc')));

    return parent::after($response);
}

Il faut maintenant intégrer la vue liste affichant l’ensemble des notes. Créez le fichier fuel/app/views/notes/list.php.

<div class="list notes_list">
<?php if ($list): ?>
    <?php foreach ($list as $item): ?>
        <?php echo Html::anchor('notes/view/'.$item->id, $item->titre, array('class' => (isset($note) && $note->id == $item->id) ? 'active' : '')); ?>
    <?php endforeach; ?>
<?php else: ?>
<div>Pas de notes pour le moment</div>
<?php endif; ?>
</div>

Le script ressemble un peu au script index.php actuel. Il liste les notes, et si une note est sélectionnée (si $note est définie), il la surligne.

 

5. Vues CRUD

Quelques modifications doivent être apportées aux fichiers vues précédemment générés par Scaffold.

La vue index est affichée quand aucune note n’est sélectionnée. Il faut modifier son contenu en conséquence :

<div class="no_content">
    Vous pouvez sélectionner une note<br/>
    ou <a href="<?php echo Uri::create('notes/create/') ?>">en créer une</a>
</div>

Lorsque l’on visualise une note, on aimerait, au lieu de devoir cliquer sur des liens, pouvoir effectuer des actions via un ensemble de boutons :

Étant donné qu’on veut pouvoir réutiliser ces boutons, il convient de créer un partial. Nous pouvons créer le partial fuel/app/views/_action_buttons.php :

<?php
    $menus = array(
        'edit'        => 'Modifier',
        'delete'    => 'Supprimer',
        'cancel'    => 'Annuler',
        'save'        => 'Sauvegarder',
        'add'        => 'Nouveau',
    );
?>
<div class="context_menu">
    <?php foreach ($menu as $item => $attr) {
        if (!is_array($attr)) {
            $attr = array('href' => $attr);
        }
        $attr['class'] = $item;
?>
        <a <?php echo array_to_attr($attr) ?> ><?php echo $menus[$item] ?></a>
    <?php } ?>
</div>

À noter que ce partial utilise la fonction array_to_attr, qui transforme un tableau en attributs pour les élément du DOM. Je vous invite à aller voir la documentation associée.

Les modifications suivantes tirent parti du partial _action_button et modifient légèrement le code pour des raisons cosmétiques.

fuel/app/views/notes/_form.php

<?php echo Form::open(); ?>
    <p>
        <?php echo Form::label('Titre', 'titre'); ?>
<?php echo Form::input('titre', Input::post('titre', isset($note) ? $note->titre : '')); ?>
    </p>
    <p>
        <?php echo Form::label('Description', 'description'); ?>
<?php echo Form::textarea('description', Input::post('description', isset($note) ? $note->description : '')); ?>
    </p>
<?php echo Form::close(); ?>

fuel/app/views/notes/create.php

<?php
echo render('_action_buttons', array(
            'menu' => array(
                'save' => array('href' => '#', 'onclick' => "$(this).closest('#view_zone').find('form').submit(); return false;"),
                'cancel' => Uri::create('notes/index'),
            )
        )
    );
?>
<?php echo render('notes/_form'); ?>

fuel/app/views/notes/edit.php

<?php
echo render('_action_buttons', array(
            'menu' => array(
                'save' => array('href' => '#', 'onclick' => "$(this).closest('#view_zone').find('form').submit(); return false;"),
                'cancel' => Uri::create('notes/view/'.$note->id),
            )
        )
    );
?>
<?php echo render('notes/_form'); ?>

fuel/app/views/notes/view.php

<?php
    echo render('_action_buttons', array(
            'menu' => array(
                'add' => Uri::create('notes/create'),
                'edit' => Uri::create('notes/edit/'.$note->id),
                'delete' => Uri::create('notes/delete/'.$note->id),
            )
        )
    );
?>
<label>Titre</label>
    <?php echo $note->titre; ?>
<label>Description</label>
    <?php echo nl2br($note->description); ?>

 

6. Support de la catégorie “Adresses et numéros”

On peut désormais ajouter le support pour la catégorie “Adresses et numéros”. Cette catégorie affiche, en fait, une liste de personnes. Nous appellerons donc son contrôleur People.
On génère d’abord le code par Scaffold :

php oil generate scaffold/crud person name:string surname:string tel:string email:string address:text additionals:text

Élément intéressant dans la génération, la gestion du singulier / pluriel. On a appelé un scaffold basé sur le nouvel objet Person, mais Oil a généré un modèle person.php ainsi qu'un fichier migration 002_create_people.php. C’est un choix que fait aussi Rails. Attention cependant, le singulier / pluriel n’est probablement bien géré qu’en anglais.

La liste des fichiers générés est presque similaire, seul le template n’a pas été généré car il est déjà présent.

Il faut maintenant lancer la migration :

php oil refine migrate

Pour l’implémentation des vues et du contrôleur People, vous pouvez copier les fichiers à partir du dossier source. Sinon, ça peut être une bon exercice d’adapter les fichiers générés à partir du scaffold.

Juste un remarque : rendez-vous dans le fichier modèle généré fuel/app/classes/model/person.php.

<?php
class Model_Person extends Model_Crud
{
	protected static $_table_name = 'people';
	
	public static function validate($factory)
	{
		$val = Validation::forge($factory);
		$val->add_field('name', 'Name', 'required|max_length[255]');
		$val->add_field('surname', 'Surname', 'required|max_length[255]');
		$val->add_field('tel', 'Tel', 'required|max_length[255]');
		$val->add_field('email', 'Email', 'required|valid_email|max_length[255]');
		$val->add_field('address', 'Address', 'required');
		$val->add_field('additionals', 'Additionals', 'required');

		return $val;
	}

}

Si on analyse la fonction validate, tous les champs string ont le validateur required|max_length[255] et tous les champs text ont le validateur required (hormis email qui a le validateur required|valid_email|max_length[255], qui teste s'il s'agit d'un email valide). Lorsqu'un champ s'appelle email, Oil ajoute automatiquement ce validateur. Il s'agit d'une valeur spéciale (apparemment la seule, pour le moment), donc sachez que le choix du nom du champ peut aussi avoir un impact sur le code généré par Scaffold.

 

7. Renommer la colonne titre

Les plus attentifs d'entre vous ont dû remarquer qu'on est passé à une notation en anglais. Pourtant, dans le modèle note, on a une colonne titre en français. Nous allons renommer cette colonne par le système de migration.

php oil generate migration rename_field_titre_to_title_in_notes

Jetons un coup d'œil au fichier généré :

<?php

namespace Fuel\\Migrations;

class Rename_field_titre_to_title_in_notes
{
	public function up()
	{
		\\DBUtil::modify_fields('notes', array(
			'titre' => array('name' => 'title', 'type' => 'varchar', 'constraint' => 255)
		));
	}

	public function down()
	{
    \\DBUtil::modify_fields('notes', array(
			'title' => array('name' => 'titre', 'type' => 'varchar', 'constraint' => 255)
		));
	}
}

Le générateur nous a généré une classe avec deux fonctions : la fonction up et la fonction down. Ces deux fonctions permettent d'effectuer les migrations. Up est appelée lorsqu'on veut effectuer la migration, down quand on souhaite l'annuler.

Nous avons fait appel à une migration magique, si bien que ces fonctions sont pré-remplies. Si on avait généré la migration avec un nom différent, les fonctions auraient probablement été vides. Je vous invite à consulter la documentation associée.

La fonction \\DBUtil::modify_fields semble souffrir, pour l'instant, d'un léger dysfonctionnement. Je vous conseille donc de remplacer les fonctions up et down. C'est l'occasion de comprendre les requêtes sensées être exécutées.

public function up()
{
        \\DB::query('ALTER TABLE `notes` CHANGE `titre` `title` VARCHAR( 255 )')->execute();
}

public function down()
{
        \\DB::query('ALTER TABLE `notes` CHANGE `title` `titre` VARCHAR( 255 )')->execute();
}

Lorsqu'on exécute :

php oil refine migrate

C'est la fonction up qui est appelée. La colonne titre est alors renommée en title. Si on veut revenir en arrière (à cause, mettons, d'une fausse manipulation), on peut exécuter :

php oil refine migrate:down

C'est la fonction down qui est alors exécutée.

Je vous invite à consulter la documentation associée.

Une fois la migration effectuée, n'oubliez pas d'aller modifier le code.

 

Pistes d'améliorations

L'agenda que nous avons développé est loin d'être terminé. Voici quelques améliorations possibles. Certains de ces points feront l'objet de tutoriels prochainement.

  • Il serait parfaitement possible de rajouter un système d'authentification. On pourrait, par exemple, créer une table user, puis rajouter des colonnes user_id dans les tables notes et person. On utiliserait la classe Session. Vous pouvez consulter la documentation associée.
  • Pour le modèle Person, les champs ne sont-ils peut-être pas tous obligatoires par défaut ? Peut-être faut-il rajouter des conditions de validation supplémentaires ? Vous pouvez aussi consulter la documentation associée.
  • La page d'accueil demande aussi à être changée (elle est toujours sur welcome). Si on veut que la page d'accueil pointe vers les notes, on peut modifier le fichier fuel/app/config/routes.php et remplacer : '_root_' => 'welcome/index' par '_root_' => 'notes/index'. Les possibilité des routes sont multiples, je vous conseille de consulter la documentation associée.
Vus : 3624
Publié par NoviusLabs : 11