Les données
La relation n:n
Imaginez une relation entre deux tables A et B qui permet de dire :- je peux avoir une ligne de la table A en relation avec plusieurs lignes de la table B,
- je peux avoir une ligne de la table B en relation avec plusieurs lignes de la table A.
La solution consiste à créer une table intermédiaire (nommée table pivot) qui sert à mémoriser les clés étrangères.
Voici un schéma de ce que nous allons réaliser : La table pivot contient les clés des deux tables :- post_id pour mémoriser la clé de la table posts,
- tag_id pour mémoriser la clé de la table tags.
Par convention le nom de la table pivot est composé des deux noms des tables au singulier pris dans l'ordre alphabétique.
Les migrations
Nous allons continuer à utiliser les tables users et posts que nous avons vues aux chapitres précédents. Nous allons créer une nouvelle table tags destinée à mémoriser les mots-clés. Commencez par supprimer toutes les tables de votre base de données, sinon vous risquez de tomber sur des conflits avec les enregistrements que nous allons créer.Supprimez aussi la table migrations.
Normalement vous devez déjà disposer des migrations pour les tables users, password_resets et posts. Nous allons ajouter les deux tables : tags et post_tag. Créez une nouvelle migration pour la table tags :php artisan make:migration create_tags_tableModifiez ainsi le code :
<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateTagsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('tags', function(Blueprint $table) { $table->increments('id'); $table->timestamps(); $table->string('tag', 50)->unique(); $table->string('tag_url', 60)->unique(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::drop('tags'); } }On prévoit les champs :
- id : clé unique incrémentée,
- created_at et updated_at créées par timestamps,
- tag : le mot clé unique limité à 50 caractères,
- tag_url : la version du tag à inclure dans l'url (avec 60 comme limite pour couvrir les cas les plus défavorables).
Il nous faut deux champs pour le tag, en effet il va falloir qu'on le transmette dans l'url pour la recherche par tag, or l'utilisateur risque de rentrer des accents par exemple (ou pire des "/"), nous allons convertir ces caractères spéciaux en caractères adaptés aux urls.
Créez une nouvelle migration pour la table post_tag :php artisan make:migration create_post_tag_tableComplétez ainsi le code :
<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreatePostTagTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('post_tag', function(Blueprint $table) { $table->increments('id'); $table->integer('post_id')->unsigned(); $table->integer('tag_id')->unsigned(); $table->foreign('post_id')->references('id')->on('posts') ->onDelete('restrict') ->onUpdate('restrict'); $table->foreign('tag_id')->references('id')->on('tags') ->onDelete('restrict') ->onUpdate('restrict'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('post_tag', function(Blueprint $table) { $table->dropForeign('post_tag_post_id_foreign'); $table->dropForeign('post_tag_tag_id_foreign'); }); Schema::drop('post_tag'); } }On prévoit les champs :
- post_id : clé étrangère pour la table posts,
- tag_id : clé étrangère pour la table tags.
J'ai encore prévu l'option restrict pour les cascades pour sécuriser les opérations sur la base.
Normalement vous devez avoir ces migrations : Lancez les migrations : Vous devez ainsi vous retrouver avec ces 6 tables dans votre base :Pour ne pas recevoir d'erreur il faut que les migrations se fassent dans le bon ordre ! L'ordre des migrations est donné par leur date de création, il suffit donc de changer la date dans le nom des migrations pour en changer l'ordre.
Les modèles
On va avoir besoin de déclarer la relation n:n dans le modèle Post :<?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); } public function tags() { return $this->belongsToMany(\App\Tag::class); } }La méthode tags permet de récupérer les tags qui sont en relation avec l'article. On utilise la méthode belongsToMany d'Eloquent pour le faire. On va aussi avoir besoin d'un modèle pour les tags :
php artisan make:model TagComplétez ainsi le code :
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Tag extends Model { protected $fillable = [ 'tag','tag_url' ]; public function posts() { return $this->belongsToMany('App\Post'); } }On a la méthode réciproque de la précédente : posts permet de récupérer les articles en relation avec le tag. Voici une schématisation de cette relation avec les deux méthodes symétriques : On se retrouve avec ces trois modèles :
On pourrait créer un dossier pour les ranger, mais comme il y en a peu on va les garder comme cela. Si on le faisait il faudrait adapter les espaces de noms en conséquence. Il faudrait surtout bien renseigner l'espace de nom dans le fichier config/auth.php pour le modèle User.
La population
Dans le précédent chapitre on a créé les fabriques (model factories) pour les modèles User et Post. On va ajouter (fichier database/factories/ModelFactory.php) celle pour le modèle Tag :$factory->define(App\Tag::class, function (Faker\Generator $faker) { $tag = $faker->unique()->word(); return [ 'tag' => $tag, 'tag_url' => $tag, ]; });On va se contenter d'un simple mot.
Faker nous offre la méthode unique pour nous assurer de l'unicité du mot choisi.
On va aussi modifier le fichier pour la population (database/seeds/DatabaseSeeder.php) :<?php use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { factory(App\User::class, 5) ->create() ->each(function ($user) { $user->posts()->saveMany(factory(App\Post::class, rand(2, 5))->make()); } ); factory(App\Tag::class, 10)->create(); $posts = App\Post::all(); foreach ($posts as $post) { $numbers = range(1, 10); shuffle($numbers); $n = rand(2, 4); for($i = 0; $i < $n; ++$i) { $post->tags()->attach($numbers[$i]); } } } }On crée 10 tags et ensuite de façon aléatoire on en affecte aux articles. Il ne reste plus qu'à lancer la population :
php artisan db:seedVérifiez dans votre base de données que vous avez des informations correctes.
La validation
Nous allons avoir un cas de validation un peu particulier. En effet comme je l'ai dit ci-dessus les tags vont être entrés dans un contrôle de texte séparés par des virgules. On a prévu dans la table tags qu'ils ne devraient pas dépasser 50 caractères.On ne dispose pas dans l'arsenal des règles de validation de Laravel d'une telle possibilité, il va donc falloir la créer.
On a déjà créé une classe PostRequest dans le chapitre précédent, il faut ajouter la règle pour les tags :<?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', 'tags' => ['Regex:/^[A-Za-z0-9-éèàù]{1,50}?(,[A-Za-z0-9-éèàù]{1,50})*$/'], ]; } }
Comme le cas est particulier j'ai utilisé une expression rationnelle. Il ne reste plus qu'à traiter le message.
Si vous regardez dans le fichier resources/lang/en/validation.php vous trouvez ce code :'custom' => [ 'attribute-name' => [ 'rule-name' => 'custom-message', ], ],C'est ici qu'on peut ajouter des messages spécifiques. On va donc écrire :
'custom' => [ 'tags' => [ 'regex' => "tags, separated by commas (no spaces), should have a maximum of 50 characters.", ], ],On va faire la même chose dans le fichier du français (puisqu'on a localisé en français) :
'custom' => [ 'tags' => [ 'regex' => "Les mots-clefs, séparés par des virgules (sans espaces), doivent avoir au maximum 50 caractères alphanumériques.", ], ],
Le contrôleur et les routes
Le contrôleur
Maintenant que tout est en place au niveau des données et de la validation voyons un peu la gestion de tout ça. On a déjà un contrôleur PostController mais on doit le compléter pour le fonctionnement avec les tags :<?php namespace App\Http\Controllers; use App\Repositories\PostRepository; use App\Repositories\TagRepository; 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', 'indexTag'); $this->middleware('admin')->only('destroy'); $this->postRepository = $postRepository; } public function index() { $posts = $this->postRepository->getWithUserAndTagsPaginate($this->nbrPerPage); return view('posts.liste', compact('posts')); } public function create() { return view('posts.create'); } public function store(PostRequest $request, TagRepository $tagRepository) { $inputs = array_merge($request->all(), ['user_id' => $request->user()->id]); $post = $this->postRepository->store($inputs); if(isset($inputs['tags'])) { $tagRepository->store($post, $inputs['tags']); } return redirect(route('post.index')); } public function destroy(Post $post) { $this->postRepository->destroy($post); return back(); } public function indexTag($tag) { $posts = $this->postRepository->getWithUserAndTagsForTagPaginate($tag, $this->nbrPerPage); return view('posts.liste', compact('posts')) ->with('info', 'Résultats pour la recherche du mot-clé : ' . $tag); } }J'ai ajouté la méthode indexTag qui doit lancer la recherche des articles qui comportent ce tag et envoyer les informations dans la vue liste. J'ai aussi un peu remanié le code.
Les routes
Il faut ajouter la route pour aboutir sur cette nouvelle méthode :Route::resource('post', 'PostController', ['except' => ['show', 'edit', 'update']]); Route::get('post/tag/{tag}', 'PostController@indexTag');Vous devez donc avoir toutes ces routes :
Les repositories
Le repository pour les articles
Voici le repository pour les articles (app/Repositories/PostRepository.php) modifié pour tenir compte des tags :<?php namespace App\Repositories; use App\Post; class PostRepository { protected $post; public function __construct(Post $post) { $this->post = $post; } protected function queryWithUserAndTags() { return $this->post->with('user', 'tags')->latest(); } public function getWithUserAndTagsPaginate($n) { return $this->queryWithUserAndTags()->paginate($n); } public function getWithUserAndTagsForTagPaginate($tag, $n) { return $this->queryWithUserAndTags() ->whereHas('tags', function($query) use ($tag) { $query->where('tags.tag_url', $tag); })->paginate($n); } public function store($inputs) { return $this->post->create($inputs); } public function destroy(Post $post) { $post->tags()->detach(); $post->delete(); } }Vous êtes peut-être surpris par la longueur de certains des noms de fonctions. C'est un choix syntaxique. Je préfère des noms explicites, quitte à les allonger.
Le repositoy pour les tags
Et voici le repository pour les tags (app/Repositories/TagRepository.php) :<?php namespace App\Repositories; use App\Tag; use Illuminate\Support\Str; class TagRepository { protected $tag; public function __construct(Tag $tag) { $this->tag = $tag; } public function store($post, $tags) { $tags = explode(',', $tags); foreach ($tags as $tag) { $tag = trim($tag); $tag_url = Str::slug($tag); $tag_ref = $this->tag->where('tag_url', $tag_url)->first(); if(is_null($tag_ref)) { $tag_ref = new $this->tag([ 'tag' => $tag, 'tag_url' => $tag_url ]); $post->tags()->save($tag_ref); } else { $post->tags()->attach($tag_ref->id); } } } }Dans le prochain chapitre nous verrons comment tout ça fonctionne ainsi que les vues.
En résumé
- Une relation de type n:n nécessite la création d'une table pivot.
- Eloquent gère élégamment les tables pivots avec des méthodes adaptées.
- On peut créer des règles de validation personnalisée avec une expression rationnelle.
Par bestmomo
Aucun commentaire