À partir de ce chapitre, il serait souhaitable que vous installiez une barre de débogage. La plus utile est celle proposée par barryvdh. Suivez les indications fournies pour l'installation, ça vous fera un bon exercice.
Dans ce chapitre nous allons commencer à aborder les bases de données. C'est un vaste sujet auquel Laravel apporte des réponses efficaces. Nous allons commencer par voir les migrations et les modèles.
Pour ce chapitre je vais encore prendre un exemple simple en imaginant un formulaire destiné à l'inscription à une lettre d'information. On va se contenter d'envoyer un email et mémoriser cet email dans une table d'une base de données.
Les migrations
Une migration permet de créer et de mettre à jour un schéma de base de données. Autrement dit, vous pouvez créer des tables, des colonnes dans ces tables, en supprimer, créer des index... Tout ce qui concerne la maintenance de vos tables peut être pris en charge par cet outil. Vous avez ainsi un suivi de vos modifications.
La configuration de la base
Vous devez dans un premier temps avoir une base de données. Laravel permet de gérer les bases de type MySQL, Postgres, SQLite et SQL Server. Je ferai tous les exemples avec MySQL mais le code sera aussi valable pour les autres types de bases.
Il faut indiquer où se trouve votre base, son nom, le nom de l'utilisateur, le mot de passe dans le fichier de configuration .env :
DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=homestead DB_USERNAME=homestead DB_PASSWORD=secret
Ici nous avons les valeurs par défaut à l'installation de Laravel.
Voici par exemple mes réglages pour ma base de test MySQL nommée "tuto" avec MySQL en local non sécurisé :
DB_DATABASE=tuto DB_USERNAME=root DB_PASSWORD=
Artisan
Nous avons déjà utilisé Artisan qui permet de faire beaucoup de choses, vous avez un aperçu des commandes en entrant :
php artisan
Vous avez une longue liste. Pour ce chapitre nous allons nous intéresser uniquement à celles qui concernent les migrations :
migrate Run the database migrations ... migrate migrate:install Create the migration repository migrate:refresh Reset and re-run all migrations migrate:reset Rollback all database migrations migrate:rollback Rollback the last database migration migrate:status Show the status of each migration
Créer la migration
On va créer la migration pour notre table :
php artisan make:migration create_emails_table
Si vous regardez maintenant dans le dossier database/migrations vous trouvez un fichier du genre 2016_08_31_212841_create_emails_table.php (la partie numérique qui inclut la date sera évidemment différente pour vous) :
Mais il y a déjà des migrations présentes, à quoi servent-elles ?
Il y a déjà effectivement 2 migrations présentes :
-
table users : c'est une migration de base pour créer une table des utilisateurs,
-
table password_resets : c'est une migration liée à la précédente qui permet de gérer le renouvellement des mots de passe en toute sécurité.
Nous nous intéresserons à ces migrations dans un chapitre ultérieur. Comme nous n'allons pas avoir besoin immédiatement de ces migrations le mieux est de les supprimer pour le moment pour éviter de créer des tables inutiles :
Voici le contenu de la migration que nous venons de créer :
<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateEmailsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { // } /** * Reverse the migrations. * * @return void */ public function down() { // } }
On dispose dans cette classe de deux fonctions :
-
up : ici on mettra le code de création
-
down : ici on mettra le code de suppression
On veut créer une table "emails" avec :
-
un id auto-incrémenté,
-
un champ "email" de type texte,
-
des champs pour mémoriser les dates de création et de modification.
Voilà le code correspondant :
public function up() { Schema::create('emails', function(Blueprint $table) { $table->increments('id'); $table->string('email'); $table->timestamps(); }); }
On demande au constructeur de schéma (Schema) de créer (create) la table "emails". Dans la fonction anonyme on définit ce qu'on veut pour la table :
-
une colonne "id" auto-incrémentée qui sera ainsi la clé primaire de la table,
-
une colonne "email" de type string,
-
deux colonnes pour les dates (générées par la méthode timestamps).
Pour la méthode down on va juste supprimer la table avec un drop :
public function down() { Schema::drop('emails'); }
Notre migration est maintenant créée.
Utiliser la migration
On va maintenant utiliser la migration (méthode up de la migration) :
Si on regarde maintenant dans la base on trouve la table "emails" avec ces 4 colonnes :
Vous avez aussi la création d'une table migrations :
Si vous avez fait une erreur vous pouvez revenir en arrière avec un rollback qui annule la dernière migration effectuée (utilisation de la méthode down de la migration) :
La table a maintenant été supprimée de la base. Comme on va avoir besoin de cette table on relance la migration.
Vous disposez également de la commande status pour savoir où vous en êtes :
Mais pour le moment avec une seule migration il n'y a vraiment pas de quoi se perdre .
Eloquent et le modèle
Laravel propose un ORM (acronyme de object-relational mapping ou en bon Français un mappage objet-relationnel) très performant.
De quoi s'agit-il ?
Tout simplement que tous les éléments de la base de données ont une représentation sous forme d'objets manipulables.
Quel intérêt ?
Tout simplement de simplifier grandement les opérations sur la base comme nous allons le voir dans toute cette partie du cours.
Avec Eloquent une table est représentée par une classe qui étend la classe Model. Pour notre table emails on va à nouveau utiliser Artisan pour la création du modèle :
php artisan make:model Email
On trouve le fichier ici :
Avec cette trame de base :
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Email extends Model { // }
On va se contenter de ce code pour notre exemple.
La validation
Pour la validation on va encore créer une requête de formulaire :
php artisan make:request EmailRequest
On trouve la requête dans son dossier :
La voici avec le code complété :
<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class EmailRequest extends FormRequest { /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; } /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return ['email' => 'bail|required|email|unique:emails']; } }
On a 4 règles :
-
bail : on s'arrête à la première erreur,
-
required : le champ est requis,
-
email : on doit avoir une adresse email valide,
-
unique : l'email ne doit pas déjà exister (unique) dans la table emails (on sous-entend qu'il s'agit de la colonne email).
Les routes et le contrôleur
Routes
On va avoir deux routes :
Route::get('email', 'EmailController@create'); Route::post('email', 'EmailController@store')->name('store.email');
L'url de base sera : http://monsite.fr/email
Contrôleur
On crée le contrôleur avec Artisan :
php artisan make:controller EmailController
On le trouve dans son dossier :
Le code du contrôleur va reprendre l'essentiel de ce que nous avons vu dans les chapitres précédents en utilisant à nouveau la validation injectée. Modifiez ainsi le code :
<?php namespace App\Http\Controllers; use App\Email; use App\Http\Requests\EmailRequest; class EmailController extends Controller { public function create() { return view('email'); } public function store(EmailRequest $request) { $email = new Email; $email->email = $request->email; $email->save(); return view('email_ok'); } }
La nouveauté réside uniquement dans l'utilisation du modèle :
$email = new Email; $email->email = $request->email; $email->save();
Ici on crée une nouvelle instance de Email. On affecte l'attribut email avec la valeur de l'entrée. Enfin on demande au modèle d'enregistrer cette ligne effectivement dans la table (save).
Les vues
On va utiliser le même tempate que dans les précédents chapitres (resources/views/template.blade.php) :
<!DOCTYPE html> <html lang="fr"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Les emails</title> {!! Html::style('<a href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css</a>') !!} {!! Html::style('<a href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css">https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css</a>') !!} <!--[if lt IE 9]> {{ Html::style('<a href="https://oss.maxcdn.com/libs/html5shiv/3.7.2/html5shiv.js">https://oss.maxcdn.com/libs/html5shiv/3.7.2/html5shiv.js</a>') }} {{ Html::style('<a href="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js">https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js</a>') }} <![endif]--> </head> <body> @yield('contenu') </body> </html>
Voici la vue pour le formulaire(resources/views/email.blade.php) :
@extends('template') @section('contenu') <br> <div class="col-sm-offset-4 col-sm-4"> <div class="panel panel-info"> <div class="panel-heading">Inscription à la lettre d'information</div> <div class="panel-body"> {!! Form::open(['route' => 'store.email']) !!} <div class="form-group {!! $errors->has('email') ? 'has-error' : '' !!}"> {!! Form::email('email', null, array('class' => 'form-control', 'placeholder' => 'Entrez votre email')) !!} {!! $errors->first('email', '<small class="help-block">:message</small>') !!} </div> {!! Form::submit('Envoyer !', ['class' => 'btn btn-info pull-right']) !!} {!! Form::close() !!} </div> </div> </div> @endsection
Cette vue ne présente aucune nouveauté pour vous si ce n'est l'utilisation du nom de la route, elle répond à l'url (avec le verbe get) : http://monsite.fr/email
L'aspect est le suivant :
Voici maintenant la vue de confirmation (resources/views/email_ok.blade.php) :
@extends('template') @section('contenu') <br> <div class="col-sm-offset-3 col-sm-6"> <div class="panel panel-info"> <div class="panel-heading">Inscription à la lettre d'information</div> <div class="panel-body"> Merci. Votre adresse a bien été prise en compte. </div> </div> </div> @endsection
Avec cet aspect :
Le fonctionnement
Voyons maintenant si tout se passe bien. Je soumets une adresse :
Je reçois la confirmation :
Je regarde dans la base :
Je soumets la même adresse :
Voyons un peu les requêtes générées par Eloquent avec par exemple la soumission de l'adresse toto@gui.com (vous les trouvez à la rubrique Queries de la barre de débogage) :
select count(*) as aggregate from `emails` where `email` = 'toto@gui.com' insert into `emails` (`email`) values ('toto@gui.com')
La première requête est destinée à tester la présence éventuelle de l'adresse dans la table pour répondre à la règle "unique". La seconde insère l'enregistrement dans la table. Vous voyez qu'Eloquent vous simplifie la tâche, vous n'avez pas besoin d'écrire les requêtes SQL, il le fait pour vous. Vous vous contentez de manipuler un objet.
N'hésitez pas à regarder les informations de la barre de débogage, vous y trouverez de précieux renseignements sur les requêtes (HTTP et SQL), les vues utilisées, les routes, les délais, les exceptions générées... Vous avez aussi un historique en cliquant sur la petite image de dossier :
Organisation du code
Maintenant posons-nous à nouveau la question de l'organisation du code. Dans le contrôleur nous avons mis la gestion du modèle :
$email = new Email; $email->email = $request->email; $email->save();
Autrement dit nous avons lié de façon étroite le contrôleur et le modèle. Supposons que nous faisions des modifications dans notre base de données et que nous placions l'email dans une autre table. Nous devrions évidemment intervenir dans le code du contrôleur pour tenir compte de cette modification.
Vous pouvez évidemment considérer que c'est peu probable, que la modification du code n'est pas très importante... Mais ici on a une application très simple, dans une situation réelle l'utilisation des modèles sont nombreux et alors la question devient bien plus pertinente.
Première version
Dans ce cours je m'efforce de vous entraîner à prendre de bonnes habitudes. Plutôt que d'instancier directement une classe dans une autre il vaut mieux une injection et laisser faire le conteneur. Regardez cette nouvelle version du contrôleur :
<?php namespace App\Http\Controllers; use App\Email; use App\Http\Requests\EmailRequest; class EmailController extends Controller { public function create() { return view('email'); } public function store(EmailRequest $request, Email $email) { $email->email = $request->email; $email->save(); return view('email_ok'); } }
Maintenant le modèle est injecté dans la méthode, c'est plus élégant et efficace. Si jamais vous changez de modèle vous n'avez plus qu'un changement de code limité sur le contrôleur. Mais ce n'est pas encore parfait.
Seconde version
Dans l'idéal on veut que notre contrôleur ne soit pas du tout concerné par un changement dans la gestion des modèles. Voici une façon de procéder :
<?php namespace App\Http\Controllers; use App\Http\Requests\EmailRequest; use App\Repositories\EmailRepository; class EmailController extends Controller { public function create() { return view('email'); } public function store(EmailRequest $request, EmailRepository $emailRepository) { $emailRepository->save($request->email); return view('email_ok'); } }
Maintenant j'injecte une classe de gestion qui possède la méthode save. Voici le contrat avec une interface (app/Repositories/EmailRepositoryInterface) :
<?php namespace App\Repositories; interface EmailRepositoryInterface { public function save($mail); }
Et voici la classe qui implémente cette interface (app/Repositories/EmailRepository) :
<?php namespace App\Repositories; use App\Email; class EmailRepository implements EmailRepositoryInterface { protected $email; public function __construct(Email $email) { $this->email = $email; } public function save($mail) { $this->email->email = $mail; $this->email->save(); } }
Le modèle est injecté dans cette classe. Je l'ai injecté dans le constructeur pour généraliser la démarche en imaginant qu'on créera d'autres méthodes que l'on peut regrouper ici pour gérer les enregistrements. Le code est maintenant parfaitement organisé, facile à modifier et à tester.
Troisième version
On peut enfin, comme on l'a déjà vu, référencer l'interface plutôt que la classe mais dans ce cas il faut informer le conteneur de la dépendance. Modifiez ainsi le fichier app/Http/Providers/AppServiceProvider.php :
public function register() { $this->app->bind( 'App\Repositories\EmailRepositoryInterface', 'App\Repositories\EmailRepository' ); }
Et le contrôleur :
<?php namespace App\Http\Controllers; use App\Http\Requests\EmailRequest; use App\Repositories\EmailRepositoryInterface; class EmailController extends Controller { public function create() { return view('email'); } public function store(EmailRequest $request, EmailRepositoryInterface $emailRepository) { $emailRepository->save($request->email); return view('email_ok'); } }
Maintenant, étant donné que le conteneur sait quelle classe instancier à partir de l'interface passée en paramètre, vous avez un code propre et facile à maintenir et à tester. Si vous changez d'avis sur la manière de stocker les emails il vous suffit de décider d'instancier une autre classe à partir de l'interface, tant que le contrat passé avec le contrôleur ne change pas !
En résumé
-
La base de données doit être configurée pour fonctionner avec Laravel.
-
Les migrations permettent d'intervenir sur le schéma des tables de la base.
-
Eloquent permet une représentation des tables sous forme d'objets pour simplifier les manipulations des enregistrements.
-
Il est judicieux de prévoir la gestion du modèle dans une classe injectée dans le contrôleur.
-
La barre de débogage donne de précieux renseignements sur les requêtes.
Par bestmomo
Nombre de commentaires : 8