Cours Laravel 5.3 – les données – la relation 1:n

Pour le moment nous n’avons manipulé qu’une table avec Eloquent. Dans le présent chapitre nous allons utiliser deux tables et les mettre en relation.

La relation la plus répandue et la plus simple entre deux tables est celle qui fait correspondre un enregistrement d’une table à plusieurs enregistrements de l’autre table, on parle de relation de un à plusieurs ou encore de relation de type 1:n. Nous verrons également dans ce chapitre comment créer un middleware.

Comme exemple pour ce chapitre, je vais prendre le cas d’un petit blog personnel avec :

  • un affichage des articles,
  • des visiteurs qui pourront consulter les articles,
  • des utilisateurs enregistrés qui pourront aussi rédiger des articles (donc possibilité de se connecter et se déconnecter),
  • des administrateurs qui pourront aussi supprimer des articles.

Pour ne pas trop alourdir le code, je ne vais pas prévoir la modification des articles.

Partez d’une installation fraîche de Laravel et utilisez la commande d’Artisan que nous avons vu précédemment pour créer les éléments de l’authentification :

php artisan make:auth

Nous allons utiliser cette infrastructure de base pour créer notre exemple.

Les migrations

Table users

Pour distinguer les rôles nous allons ajouter dans la table users une colonne admin. Dans la migration par défaut ajoutez cette ligne :

public function up()
{
    Schema::create('users', function (Blueprint $table) {
        ...
        $table->boolean('admin')->default(false);
        ...
    });
}

On va ainsi créer un champ booléen admin avec comme valeur par défaut false. Ne changez rien au reste du code.

Table posts

On va créer une migration pour la table des articles posts :

php artisan make:migration create_posts_table

Complétez le code ainsi :

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreatePostsTable extends Migration
{
    public function up()
    {
        Schema::create('posts', function(Blueprint $table) {
            $table->increments('id');
            $table->timestamps();
            $table->string('titre');
            $table->text('contenu');
            $table->integer('user_id')->unsigned();
            $table->foreign('user_id')
                  ->references('id')
                  ->on('users')
                  ->onDelete('restrict')
                  ->onUpdate('restrict');
        });
    }

    public function down()
    {
        Schema::table('posts', function(Blueprint $table) {
            $table->dropForeign('posts_user_id_foreign');
        });
        Schema::drop('posts');        
    }
}

Normalement vous devez avoir ces 3 migrations :

Les 3 migrations

Lancez les migrations :

Lancement des migrations

Vous devez vous retrouver avec les trois tables dans votre base ainsi que la table migrations :

La 4 tables générées

La relation

On a la situation suivante :

  • un utilisateur peut écrire plusieurs articles,
  • un article est écrit par un seul utilisateur.

Il faut trouver un moyen pour référencer cette relation dans les tables. Le principe est simple : on prévoit dans la table posts une ligne destinée à recevoir l’identifiant de l’utilisateur rédacteur de l’article. On appelle cette ligne une clé étrangère parce qu’on enregistre ici la clé d’une autre table. Voici une représentation visuelle de cette relation :

La clé étrangère

Vous voyez la relation dessinée entre la clé id dans la table users et la clé étrangère user_id dans la table posts. La migration que l’on a créée ci-dessus est destinée aussi à informer la base de cette relation. Regardez ce code :

$table->foreign('user_id')
      ->references('id')
      ->on('users')
      ->onDelete('restrict')
      ->onUpdate('restrict');

Dans la table on déclare une clé étrangère (foreign) nommée user_id qui référence (references) la ligne id dans la table (on) users. En cas de suppression (onDelete) ou de modification (onUpdate) on a une restriction (restrict).

Que signifient ces deux dernières conditions ?

Imaginez que vous avez un utilisateur avec l’id 5 qui a deux articles, donc dans la table posts on a deux enregistrements avec user_id qui a la valeur 5. Si on supprime l’utilisateur que va-t-il se passer ? On risque de se retrouver avec nos deux enregistrements dans la table posts avec une clé étrangère qui ne correspond à aucun enregistrement dans la table users. En mettant restrict on empêche la suppression d’un utilisateur qui a des articles. On doit donc commencer par supprimer ses articles avant de le supprimer lui-même. On dit que la base assure l’intégrité référentielle. Elle n’acceptera pas non plus qu’on utilise pour user_id une valeur qui n’existe pas dans la table users.

Une autre possibilité est cascade à la place de restrict. Dans ce cas si vous supprimez un utilisateur ça supprimera en cascade les articles de cet utilisateur.

C’est une option qui est rarement utilisée parce qu’elle peut s’avérer dangereuse, surtout dans une base comportant de multiples tables en relation. Mais c’est aussi une stratégie très efficace parce que c’est le moteur de la base de données qui se charge de gérer les enregistrements en relation, vous n’avez ainsi pas à vous en soucier au niveau du code.

On pourrait aussi ne pas signaler à la base qu’il existe une relation et la gérer seulement dans notre code. Mais c’est encore plus dangereux parce que la moindre erreur de gestion des enregistrements dans votre code risque d’avoir des conséquences importantes dans votre base avec de multiples incohérences.

Les modèles

Nous avons déjà un modèle User (app/User.php). Il va juste falloir ajouter une méthode pour pouvoir facilement aller trouver les articles d’un utilisateur. Ajoutez ce code dans le modèle User :

public function posts() 
{
    return $this->hasMany(\App\Post::class);
}

On déclare ici qu’un utilisateur a plusieurs (hasMany) articles (posts). On aura ainsi une méthode pratique pour récupérer les articles d’un utilisateur.

Soyez vigilant avec les espaces de noms !

Il nous faut aussi le modèle Post :

php artisan make:model Post

Complétez ainsi le code :

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected $fillable = [
        'titre','contenu','user_id'
    ];

    public function user() 
    {
        return $this->belongsTo(\App\User::class);
    }
}

Ici on a la méthode user (au singulier) qui permet de trouver l’utilisateur auquel appartient (belongsTo) l’article. C’est donc la réciproque de la méthode précédente.

Voici une schématisation de cette relation avec les deux méthodes :

Les deux méthodes de la relation

Si vous ne spécifiez pas de manière explicite le nom de la table dans un modèle, Laravel le déduit à partir du nom du modèle en le mettant au pluriel (à la mode anglaise) et en mettant la première lettre en minuscule. Donc avec le modèle Post il en conclut que la table s’appelle posts. Si ce n’était pas satisfaisant il faudrait créer une propriété $table.

Les deux méthodes mises en place permettent de récupérer facilement un enregistrement lié. Par exemple pour avoir tous les articles de l’utilisateur qui a l’id 1 :

$articles = App\User::find(1)->posts;

De la même manière on peut trouver l’utilisateur qui a écrit l’article d’id 1 :

$user = App\Post::find(1)->user;

Vous voyez que le codage devient limpide avec ces méthodes .

Le contrôleur et les routes

Le contrôleur

Maintenant que tout est en place au niveau des données voyons un peu la gestion de tout ça. On va créer un contrôleur de ressource pour les articles qu’on va appeler PostController :

php artisan make:controller PostController --resource

Ce contrôleur devra gérer plusieurs chose :

  • la réception de la requête pour afficher les articles du blog et la réponse adaptée,
  • la réception de la requête pour le formulaire pour créer un nouvel article et son envoi,
  • la réception de la soumission du formulaire de création d’un nouvel article (réservé à un utilisateur connecté) et son enregistrement,
  • la réception de la demande de suppression d’un article (réservé à un administrateur) et sa suppression.

Pour simplifier je ne vais pas prévoir la possibilité de modifier un article.

Voici le code modifié du contrôleur :

<?php

namespace App\Http\Controllers;

use App\Repositories\PostRepository;
use App\Http\Requests\PostRequest;
use App\Post;

class PostController extends Controller
{
    protected $postRepository;

    protected $nbrPerPage = 4;

    public function __construct(PostRepository $postRepository)
    {
        $this->middleware('auth')->except('index');
        $this->middleware('admin')->only('destroy');

        $this->postRepository = $postRepository;
    }

    public function index()
    {
        $posts = $this->postRepository->getPaginate($this->nbrPerPage);

        return view('posts.liste', compact('posts'));
    }

    public function create()
    {
        return view('posts.create');
    }

    public function store(PostRequest $request)
    {
        $inputs = array_merge($request->all(), ['user_id' => $request->user()->id]);

        $this->postRepository->store($inputs);

        return redirect()->route('post.index');
    }

    public function destroy(Post $post)
    {
        $this->postRepository->destroy($post);

        return back();
    }
}

Comme à l’accoutumée j’injecte la requête de formulaire et le repository.

Notez l’utilisation des middleware pour filtrer les utilisateurs, nous allons voir cela un peu plus loin.

Les routes

On a vu dans le chapitre sur les ressources comment créer les routes de ce genre de contrôleur. Il va juste falloir indiquer qu’on ne veut pas utiliser les 7 méthodes disponibles mais juste certaines. On va aussi établir une redirection de la route de base « / » vers la ressource :

Route::get('/', function () {
    return redirect()->route('post.index');
});

Auth::routes();

Route::resource('post', 'PostController', ['except' => ['show', 'edit', 'update']]);

Pour mémoire Auth::routes(); a été généré par Artisan pour créer les routes de l’authentification.

Vous avez ainsi toutes ces routes :

Toutes les routes

Le repository

Pour la gestion on va placer les fichiers dans le dossier app/Repositories comme nous l’avons déjà fait pour la gestion des utilisateurs :

Le repository

Avec ce code :

<?php

namespace App\Repositories;

use App\Post;

class PostRepository
{
    protected $post;

    public function __construct(Post $post)
    {
        $this->post = $post;
    }

    public function getPaginate($n)
    {
        return $this->post->with('user')
            ->orderBy('posts.created_at', 'desc')
            ->paginate($n);
    }

    public function store($inputs)
    {
        $this->post->create($inputs);
    }

    public function destroy(Post $post)
    {
        $post->delete();
    }
}

Nous allons voir plus loin l’utilité de toutes ces méthodes.

Les middlewares

On a vu que dans le contrôleur on applique deux middlewares :

  • auth : accès réservé aux utilisateurs authentifiés à part pour la méthode index pour afficher le blog,
  • admin : accès réservé aux administrateurs pour la méthode destroy.

On a déjà rencontré le premier middleware, il est déjà prévu dans Laravel, par contre le second n’existe pas.

Il faut donc le créer. Encore une fois nous allons utiliser Artisan :

php artisan make:middleware Admin

On trouve le fichier bien rangé :

Le middleware pour l'administration

Modifiez ainsi le code :

<?php

namespace App\Http\Middleware;

use Closure;

class Admin
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if ($request->user()->admin) {
            return $next($request);
        }

        return redirect('post');
    }
}

Si l’utilisateur n’est pas un administrateur on redirige sur l’affichage du blog. Remarquez qu’on ne vérifie pas à ce niveau qu’on a un utilisateur authentifié parce que dans le constructeur du contrôleur le filtre auth est placé avant le filtre admin. Si c’était l’inverse on tomberait évidemment sur une erreur en cas de tentative d’accès à l’url pour la suppression d’un article.

On a créé le middleware mais ça ne suffit pas, il faut maintenant un lien entre son nom et la classe qu’on vient de créer.

Regardez dans le fichier app/Http/Kernel.php ces lignes de code :

protected $routeMiddleware = [
    'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
    'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
    'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
    'can' => \Illuminate\Auth\Middleware\Authorize::class,
    'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
    'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
];

Vous trouvez ici tous les middlewares déclarés, il suffit d’ajouter le nouveau :

protected $routeMiddleware = [
    ...
    'admin' => \App\Http\Middleware\Admin::class,
];

La validation

Voyons maintenant la validation. On va encore créer une requête de formulaire :

php artisan make:request PostRequest

Elle se place dans le dossier qui se crée pour l’occasion :

La requête de formulaire

Et on complète ainsi le code :

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class PostRequest 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 [
            'titre' => 'bail|required|max:255',
            'contenu' => 'required'
        ];
    }
}

La population

Les fabriques (model factories)

Nous allons voir maintenant comment remplir nos tables avec des enregistrements pour faire nos essais. Laravel nous permet de définir un tableau d’attributs pour les modèles avec les fabriques (model factories). Regardez le fichier database/factories/ModelFactory.php :

<?php

/*
|--------------------------------------------------------------------------
| Model Factories
|--------------------------------------------------------------------------
|
| Here you may define all of your model factories. Model factories give
| you a convenient way to create models for testing and seeding your
| database. Just tell the factory how a default model should look.
|
*/

$factory->define(App\User::class, function (Faker\Generator $faker) {
    static $password;

    return [
        'name' => $faker->name,
        'email' => $faker->safeEmail,
        'password' => $password ?: $password = bcrypt('secret'),
        'remember_token' => str_random(10),
    ];
});

Faker. C’est un générateur de données virtuelles aléatoires qui sait pratiquement tout faire.

Comme on a ajouté la colonne admin on va la prévoir dans la fabrique. D’autre part on va utiliser un mot de passe systématique pour nous simplifier la vie :

$factory->define(App\User::class, function (Faker\Generator $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->safeEmail,
        'password' => bcrypt('secret'),
        'admin' => $faker->boolean,
        'remember_token' => str_random(10),
    ];
});

On va aussi prévoir une fabrique pour les articles dans le même fichier :

$factory->define(App\Post::class, function (Faker\Generator $faker) {
    return [
        'titre' => $faker->sentence(2),
        'contenu' => $faker->paragraph(rand(8, 15)),
        'created_at' => $faker->dateTimeThisYear(),
    ];
});

la documentation détaillée.

La population

Regardez le fichier database/seeds/DatabaseSeeder :

Le fichier de la population

Par défaut il comporte ce code :

<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        // $this->call(UsersTableSeeder::class);
    }
}

La méthode run est destinée à exécuter les fichiers pour la population. Vous avez déjà la ligne commentée de lancement pour la table users. Comme on n’a que deux tables à remplir on va mettre tout le code dans ce fichier :

public function run()
{
    factory(App\User::class, 5)
        ->create()
        ->each(function ($user) {
            $user->posts()->saveMany(factory(App\Post::class, rand(2, 5))->make());
        });
}

On appelle la fabrique (factory) pour le modèle des utilisateurs (App\User::class), on en veut 5, on les crée (create) pour chacun des utilisateurs créés (each $user) on utilise la relation (posts()) pour créer plusieurs (saveMany) articles (App\Post::class) en utilisant la fabrique (factory), on en veut entre 2 et 5 (rand(2, 5)).

Il ne vous reste plus qu’à lancer la population :

php artisan db:seed

Vous aurez dans la base 5 utilisateurs :

Les 5 utilisateurs

Et des articles (table posts) affectés aux utilisateurs :

Les articles créés

On a des dates aléatoires pour la colonne created_at, ce qui va nous permettre de les trier.

Fonctionnement

La liste des articles

La liste des articles est obtenue avec l’url (méthode get) : …/post

Elle arrive sur la méthode index du contrôleur :

public function index()
{
    $posts = $this->postRepository->getPaginate($this->nbrPerPage);

    return view('posts.liste', compact('posts'));
}

Ici on envoie le nombre d’articles par page (placé dans la propriété $nbrPerPage) à la méthode getPaginate du repository :

public function getPaginate($n)
{
    return $this->post->with('user')
        ->orderBy('created_at', 'desc')
        ->paginate($n);
}

On veut les articles avec (with) l’utilisateur (user), dans l’ordre des dates de création (created_at) descendant (desc) avec une pagination de n articles ($n).

Il existe la méthode latest (et oldest pour l’inverse) qui permet de simplifier la syntaxe :

return $this->post->with('user')
->latest()
->paginate($n);

L’ajout d’un article

La demande du formulaire de création d’un article se fait avec l’url (méthode get) : …/post/create

Le contrôleur renvoie directement la vue :

public function create()
{
    return view('posts.create');
}

Le retour du formulaire se fait avec l’url (méthode post) : …/post

Remarquez que cette méthode est protégée par le middleware auth :

$this->middleware('auth')->except('index');

On arrive sur la méthode store du contrôleur :

public function store(PostRequest $request)
{
    $inputs = array_merge($request->all(), ['user_id' => $request->user()->id]);

    $this->postRepository->store($inputs);

    return redirect()->route('post.index');
}

On injecte la requête de formulaire pour la validation, je n’insiste pas parce qu’il n’y a rien de nouveau à ce niveau. On récupère les entrées du formulaire pour le titre et le contenu. Pour l’identifiant de l’utilisateur on sait qu’il est forcément connecté alors on récupère cet identifiant avec la requête. Si la validation se passe bien on envoie à la méthode store du repository :

public function store($inputs)
{
    $this->post->create($inputs);
}

L’assignement de masse fonctionne parce qu’on a prévu la propriété $fillable dans le modèle Post :

protected $fillable = [
   'titre','contenu','user_id'
];

Suppression d’un article

Enfin on supprime un article avec l’url (méthode delete) : …/post/id

id représente l’identifiant de l’article à supprimer. On tombe sur la méthode destroy du contrôleur :

public function destroy(Post $post)
{
    $this->postRepository->destroy($post);

    return back();
}

Remarquez que cette méthode est protégée par le middleware admin :

$this->middleware('admin')->only('destroy');

Avec la liaison implicite on a directement le modèle de l’article correspondant dans la variable $post. On l’envoie à la méthode destroy du repository :

public function destroy(Post $post)
{
    $post->delete();
}

Là on supprime l’article avec la méthode delete du modèle.

Le template

Voyons à présent les vues. On dispose déjà d’un template (resources/views/layouts/app.blade.php) avec l’installation de l’authentification :

Le template de l'authentification

Alors on va l’utiliser pour avoir une cohérence visuelle de l’application. On va juste lui apporter des petites modifications (changement du titre, francisation, ajout d’un item dans la barre de navigation) , voici le code résultant :

<!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">

    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>Mon joli blog</title>

    <!-- Styles -->
    <link href="/css/app.css" rel="stylesheet">

    <!-- Scripts -->
    <script>
        window.Laravel = <?php echo json_encode([
            'csrfToken' => csrf_token(),
        ]); ?>
    </script>
</head>
<body>
    <nav class="navbar navbar-default navbar-static-top">
        <div class="container">
            <div class="navbar-header">

                <!-- Collapsed Hamburger -->
                <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#app-navbar-collapse">
                    <span class="sr-only">Toggle Navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>

                <!-- Branding Image -->
                <a class="navbar-brand" href="{{ url('/') }}">
                    Mon joli Blog
                </a>
            </div>

            <div class="collapse navbar-collapse" id="app-navbar-collapse">
                <!-- Left Side Of Navbar -->
                <ul class="nav navbar-nav">
                     
                </ul>

                <!-- Right Side Of Navbar -->
                <ul class="nav navbar-nav navbar-right">
                    <!-- Authentication Links -->
                    @if (Auth::guest())
                        <li><a href="{{ url('/login') }}">Se connecter</a></li>
                        <li><a href="{{ url('/register') }}">S'enregistrer</a></li>
                    @else
                        <li><a href="{{ url('/post/create') }}">Créer un article</a></li>
                        <li class="dropdown">
                            <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
                                {{ Auth::user()->name }} <span class="caret"></span>
                            </a>

                            <ul class="dropdown-menu" role="menu">
                                <li>
                                    <a href="{{ url('/logout') }}"
                                        onclick="event.preventDefault();
                                                 document.getElementById('logout-form').submit();">
                                        Logout
                                    </a>

                                    <form id="logout-form" action="{{ url('/logout') }}" method="POST" style="display: none;">
                                        {{ csrf_field() }}
                                    </form>
                                </li>
                            </ul>
                        </li>
                    @endif
                </ul>
            </div>
        </div>
    </nav>

    @yield('content')

    <!-- Scripts -->
    <script src="/js/app.js"></script>
</body>
</html>

L’affichage des articles

Nous avons besoin d’une vue pour afficher les articles du blog (resources/views/posts/liste.blade.php) :

La vue pour l'affichage des articles

Avec ce code :

@extends('layouts.app')

@section('content')
    <div class="container">
        @if(isset($info))
            <div class="row alert alert-info">{{ $info }}</div>
        @endif
        {!! $posts->links() !!}
        @foreach($posts as $post)
            <article class="row bg-primary">
                <div class="col-md-12">
                    <header>
                        <h1>{{ $post->titre }}</h1>
                    </header>
                    <hr>
                    <section>
                        <p>{{ $post->contenu }}</p>
                        @if(auth()->check() and auth()->user()->admin)
                            <form method="POST" action="{{ route('post.destroy', ['id' => $post->id]) }}">
                                {{ method_field('DELETE') }}
                                {{ csrf_field() }}
                                <input class="btn btn-danger btn-xs" onclick="return confirm('Vraiment supprimer cet article ?')" type="submit" value="Supprimer cet article">
                            </form>
                        @endif
                        <em class="pull-right">
                            {{ $post->user->name }} le {!! $post->created_at->format('d-m-Y') !!}
                        </em>
                    </section>
                </div>
            </article>
            <br>
        @endforeach
        {!! $posts->links() !!}
    </div>
@endsection

Avec cet aspect pour un utilisateur non connecté :

L'aspect pour un utilisateur non connecté

Le lien « Se connecter » ouvre le formulaire de connexion comme on l’a vu dans le chapitre sur l’authentification.

Pour avoir une redirection correcte après la connexion il faut bien renseigner la propriété $redirectTo dans le contrôleur LoginController :

protected $redirectTo = 'post';

La génération des articles dans la vue se fait avec un foreach :

@foreach($posts as $post)
    ...
@endforeach

Si vous vous connectez vous retournez au blog avec deux liens supplémentaires :

Les liens d'un utilisateur authentifié

On utilise une condition pour adapter les liens :

@if (Auth::guest())
    <li><a href="{{ url('/login') }}">Se connecter</a></li>
    <li><a href="{{ url('/register') }}">S'enregistrer</a></li>
@else
    <li><a href="{{ url('/post/create') }}">Créer un article</a></li>
    <li class="dropdown">
        ...
        Logout
        ...
    </li>
@endif

La méthode guest permet de savoir si un utilisateur est un simple visiteur non authentifié. C’est le contraire de la méthode check qu’on pourrait utiliser en inversant la logique.

Si un administrateur est connecté il dispose en plus de boutons pour supprimer les articles :

Un bouton pour supprimer l'article

On utilise encore une condition pour détecter un administrateur :

@if(auth()->check() and auth()->user()->admin)
    ...
@endif

Il faut que l’utilisateur soit connecté (check) et que ce soit un administrateur (auth()->user()->admin).

Comme on n’utilise pas laravelcollective/html dans cet exemple (c’est bien aussi de voir comment faire sans ce composant) il faut construire les formulaires avec tout le code nécessaire.

Pour la destruction des articles on doit aboutir sur cette route :

La route pour supprimer un article

Il nous faut une méthode DELETE, une méthode qui n’est pas supportée dans les formulaires (tout comme PUT et PATCH). Alors on déclare une méthode POST comme action mais on ajoute un contrôle caché _method avec la valeur DELETE. A l’arrivée la requête est interprétée comme une requête DELETE. Regardez le code de ce formulaire :

<form method="POST" action="{{ route('post.destroy', ['id' => $post->id]) }}">
    {{ method_field('DELETE') }}
    {{ csrf_field() }}
    <input class="btn btn-danger btn-xs" onclick="return confirm('Vraiment supprimer cet article ?')" type="submit" value="Supprimer cet article">
</form>

On utilise l’helper method_field pour générer ce code :

<input type="hidden" name="_method" value="DELETE">

J’ai aussi prévu une simple confirmation en JavaScript avant de supprimer effectivement l’article.

La création d’un article

Voici maintenant la vue pour le formulaire de création d’un article (resources/views/posts/create.blade.php) :

La vue pour la création d'un article

Avec ce code :

@extends('layouts.app')

@section('content')
    <div class="col-sm-offset-3 col-sm-6">
        <div class="panel panel-default">
            <div class="panel-heading">Ajout d'un article</div>
            <div class="panel-body"> 
                <form method="POST" action="{{ url('/post') }}">
                    {{ csrf_field() }}
                    <div class="form-group{{ $errors->has('titre') ? ' has-error' : '' }}">
                        <input class="form-control" placeholder="Titre" name="titre" type="text" value="{{ old('titre') }}" autofocus>
                        @if ($errors->has('titre'))
                            <span class="help-block">
                                <strong>{{ $errors->first('titre') }}</strong>
                            </span>
                        @endif
                    </div>
                    <div class="form-group{{ $errors->has('contenu') ? ' has-error' : '' }}">
                        <textarea class="form-control" placeholder="Contenu" name="contenu" cols="50" rows="10">{{ old('contenu') }}</textarea>
                        @if ($errors->has('contenu'))
                            <span class="help-block">
                                <strong>{{ $errors->first('contenu') }}</strong>
                            </span>
                        @endif
                    </div>
                    <button type="submit" class="btn btn-primary pull-right">Envoyer !</button>
                </form>

            </div>
        </div>
    </div>
@endsection

Voici l’apparence du formulaire :

Le formulaire de création d'un article

Là encore le fait de ne pas utiliser le composant laravellollective/html oblige à :

  • écrire tout le code,
  • prévoir de remplir les contrôles avec les anciennes valeurs saisies en cas d’erreur dans la validation.

Cette dernière action se fait en utilisant l’helper old. Par exemple pour le titre on a :

<input class="form-control" placeholder="Titre" name="titre" type="text" value="{{ old('titre') }}" autofocus>

Imaginez que vous ayez ‌saisi le titre mais pas le contenu, au retour de la validation vous allez retrouver votre titre :

On retrouve le titre après l'erreur de validation du contenu

C’est exactement pareil pour le contenu.

En résumé

  • Une relation de type 1:n nécessite la création d’une clé étrangère côté n.
  • On peut remplir les tables d’enregistrements avec la population.
  • Une relation dans la base nécessite la mise en place de méthodes spéciales dans les modèles.
  • Avec les middlewares il est facile de gérer l’accès aux méthodes des contrôleurs.

2 réflexions sur “Cours Laravel 5.3 – les données – la relation 1:n

  1. Anardil dit :

    Bonjour,

    Je suis en train de flooder les commentaires, mais je découvre Laravel, donc j’ai souvent des questions :).
    Est-ce que l’on est obligé d’utiliser des clés étrangères pour avoir des relations 1:N ?
    Si je désire être sur du MyISAM sur MySQL et non sur InnoDB, comment cela se passe ?
    Je viens du monde Zend_Framework v1.12 et j’utilisais une méthode « join() » lorsque j’avais besoin de faire des jointures avec d’autres tables.

    Merci.

    • bestmomo dit :

      Bonjour,

      Désolé pour le retard mais j’avais un peu raté la question. Pour le fonctionnement d’Eloquent la clé étrangère est nécessaire, sinon il faut passer par le Query Builder et établir une jointure.

Laisser un commentaire