Blog (part 2)
Recherche dans la liste des articles
Nous allons ajouter une barre de recherche à la liste des articles de la page /articles. Pour cela, nous allons modifier la méthode index du controller ArticleController et la vue resources/views/front/articles/index.blade.php.
Controller
Dans le controller ArticleController, nous allons modifier la méthode index pour ajouter des clauses where à la requête de récupération des articles qui prendront en compte la recherche de l'utilisateur.
public function index(Request $request)
{
$articles = Article::query()
->where('published_at', '<', now())
->when($request->query('search'), function ($query) use ($request) {
$query->where('body', 'LIKE', '%'.$request->query('search').'%')
->orWhere('title', 'LIKE', '%'.$request->query('search').'%')
->orWhereHas('user', function ($query) use ($request) {
$query->where('name', 'LIKE', '%'.$request->query('search').'%');
})
;
})
->orderByDesc('published_at')
->paginate(12)
;
return view('front.articles.index', [
'articles' => $articles,
]);
}
Nous avons ajouté les lignes suivantes :
->where('body', 'LIKE', '%'.$request->query('search').'%')
->orWhere('title', 'LIKE', '%'.$request->query('search').'%')
->orWhereHas('user', function ($query) use ($request) {
$query->where('name', 'LIKE', '%'.$request->query('search').'%');
})
La méthode where permet de filtrer les résultats de la requête. La méthode orWhere permet d'ajouter une clause OR à la requête. La méthode orWhereHas permet d'ajouter une clause OR à la requête en vérifiant si une relation existe.
- La première clause
wherepermet de filtrer les articles dont le texte contient la chaîne de caractères recherchée. - La deuxième clause
orWherepermet de filtrer les articles dont le titre contient la chaîne de caractères recherchée. - La troisième clause
orWhereHaspermet de filtrer les articles dont l'auteur contient la chaîne de caractères recherchée.
Vue
Dans la vue resources/views/front/articles/index.blade.php, nous allons ajouter un formulaire de recherche à la page. Il utilisera la méthode GET et la route front.articles.index qui est la route qui pointe vers la méthode index du controller ArticleController.
<x-guest-layout>
<h1 class="font-bold text-xl mb-4">Liste des articles</h1>
<form action="{{ route('front.articles.index') }}" method="GET" class="mb-4">
<div class="flex items-center">
<input
type="text"
name="search"
id="search"
placeholder="Rechercher un article"
class="flex-grow border border-gray-300 rounded shadow px-4 py-2 mr-4"
value="{{ request()->search }}"
autofocus
/>
<button
type="submit"
class="bg-white text-gray-600 px-4 py-2 rounded-lg shadow"
>
<x-heroicon-o-magnifying-glass class="h-5 w-5" />
</button>
</div>
</form>
<ul class="grid sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-8">
@foreach ($articles as $article)
<li>
<x-article-card :article="$article" />
</li>
@endforeach
</ul>
<div class="mt-8">{{ $articles->links() }}</div>
</x-guest-layout>
Nous avons aussi ajouté un attribut value à l'input de recherche pour que la valeur de la recherche soit toujours présente dans le champ de recherche après avoir soumis le formulaire.
Gestion des rôles des utilisateurs
Nous allons ajouter une gestion des rôles des utilisateurs. Nous allons créer un nouveau modèle Role qui va permettre de définir les rôles des utilisateurs.
L'application va utiliser les rôles suivants :
admin: l'utilisateur est administrateur de l'application. Il peut tout faire.author: l'utilisateur est auteur de l'application. Il peut créer et modifier ses articles. Il peut aussi commenter les articles.user: l'utilisateur est un utilisateur lambda. Il peut seulement commenter les articles.
Création du modèle
Nous allons créer un nouveau modèle Role avec la commande suivante :
php artisan make:model Role -m
On ne demande que la création du modèle et de la migration.
Modèle
Nous allons modifier le modèle Role pour y définir les constantes des rôles disponibles ainsi qu'une méthode statique roles qui retourne un tableau contenant les rôles disponibles. Nous ajoutons aussi une relation users qui permet de récupérer les utilisateurs qui ont ce rôle.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
use HasFactory;
// Constantes des rôles disponibles
public const ADMIN = 'Administrateur';
public const AUTHOR = 'Auteur';
public const USER = 'Utilisateur';
public static function roles(): array
{
return [
self::ADMIN,
self::AUTHOR,
self::USER,
];
}
public function users(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(User::class);
}
}
Nous devons aussi ajouter la méthode role dans le modèle User pour récupérer le rôle de l'utilisateur après la méthode articles :
public function role(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Role::class);
}
Migration
Nous allons modifier la date au début du nom du fichier de migration [DATETIME]_create_roles_table.php pour qu'il soit exécuté avant la migration de création de la table users.
0000_01_01_000000_create_roles_table.php
0001_01_01_000000_create_users_table.php
# autres migrations
Nous allons modifier la migration des rôles 0000_01_01_000000_create_roles_table.php comme ceci :
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class() extends Migration {
/**
* Run the migrations.
*/
public function up()
{
Schema::create('roles', function (Blueprint $table) {
$table->id();
$table->enum('name', \App\Models\Role::roles());
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down()
{
Schema::dropIfExists('roles');
}
};
Nous avons ajouté une colonne name de type enum qui contient les rôles disponibles. Nous avons utilisé la méthode statique roles du modèle Role pour récupérer les rôles disponibles. Cela veut dire que le champ name de la table roles ne peut contenir que les valeurs Administrateur, Auteur et Utilisateur.
Nous allons modifier la migration de création de la table users, 0001_01_01_000000_create_users_table.php, comme ceci :
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->foreignId('role_id')->constrained()->default(3);
$table->rememberToken();
$table->timestamps();
});
}
Nous avons ajouté un champ role_id qui est une clé étrangère vers la table roles. Nous avons aussi ajouté une valeur par défaut de 3 qui correspond au rôle Utilisateur.
Seeders
Nous allons commencer par remettre de l'ordre dans les seeders. Nous allons créer des seeders pour les rôles et les utilisateurs. Le seeder pour les articles existe déjà.
Pour créer les seeders, nous allons utiliser la commande suivante :
php artisan make:seeder RolesTableSeeder
php artisan make:seeder UsersTableSeeder
Nous allons modifier le fichier DatabaseSeeder.php pour qu'il appelle les seeders de rôles, d'utilisateurs et d'articles :
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Storage;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*/
public function run(): void
{
// On supprime les anciennes images de test
$images = Storage::disk('public')->files('images');
Storage::disk('public')->delete($images);
$this->call([
RolesTableSeeder::class,
UsersTableSeeder::class,
ArticlesTableSeeder::class,
]);
}
}
Ensuite, nous allons modifier les seeders pour qu'ils créent les rôles, les utilisateurs et les articles.
Nous allons commencer par le seeder de rôles RolesTableSeeder.php :
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class RolesTableSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run()
{
// On crée les rôles
\App\Models\Role::create([
'name' => \App\Models\Role::ADMIN,
]);
\App\Models\Role::create([
'name' => \App\Models\Role::AUTHOR,
]);
\App\Models\Role::create([
'name' => \App\Models\Role::USER,
]);
}
}
Nous allons ensuite modifier le seeder d'utilisateurs UsersTableSeeder.php :
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class UsersTableSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run()
{
// On crée un admin
User::factory()->create([
'name' => 'Admin User',
'email' => 'admin@example.com',
'role_id' => 1,
]);
// 10 auteurs
User::factory(10)->create([
'role_id' => 2,
]);
// On crée notre utilisateur de test qui sera maintenant un utilisateur lambda
User::factory()->create([
'name' => 'User User',
'email' => 'test@example.com',
'role_id' => 3,
]);
// 10 utilisateurs lambda
User::factory(10)->create([
'role_id' => 3,
]);
}
}
Et, on va aussi modifier la factory des articles pour qu’il n’y ait que des articles créés par des auteurs :
<?php
namespace Database\Factories;
use App\Models\Role;
use App\Models\User;
use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Article>
*/
class ArticleFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'title' => fake()->bs(),
'body' => fake()->realTextBetween($minNbChars = 500, $maxNbChars = 2000),
'img_path' => function () {
$randomName = Str::uuid();
$imageUrl = "https://picsum.photos/1024/768.webp?random={$randomName}";
$path = "images/{$randomName}.webp";
Storage::disk('public')->put($path, file_get_contents($imageUrl));
return $path;
},
'published_at' => fake()->dateTimeBetween('-2 months', '+ 1 month'),
'user_id' => User::query()->whereHas('role', function (Builder $query) {
$query->where('name', Role::AUTHOR);
})->get()->random()->id,
];
}
}
Nous allons maintenant recréer la base de données et la remplir avec les données de test :
php artisan migrate:fresh --seed
Output :
λ php artisan migrate:fresh --seed
Dropping all tables .................................................. 4.75ms DONE
INFO Preparing database.
Creating migration table ............................................. 6.52ms DONE
INFO Running migrations.
0000_01_01_000000_create_roles_table ................................. 4.67ms DONE
0001_01_01_000000_create_users_table ................................ 23.48ms DONE
0001_01_01_000001_create_cache_table ................................. 7.59ms DONE
0001_01_01_000002_create_jobs_table ................................. 18.68ms DONE
2024_10_13_101030_create_articles_table .............................. 3.63ms DONE
2024_10_13_152719_add_avatar_path_to_users_table ..................... 3.70ms DONE
INFO Seeding database.
Database\Seeders\RolesTableSeeder ........................................ RUNNING
Database\Seeders\RolesTableSeeder ..................................... 14 ms DONE
Database\Seeders\UsersTableSeeder ........................................ RUNNING
Database\Seeders\UsersTableSeeder .................................... 289 ms DONE
Database\Seeders\ArticlesTableSeeder ..................................... RUNNING
Database\Seeders\ArticlesTableSeeder .............................. 49,433 ms DONE
Si vous avez des erreurs, vérifiez que vous avez bien réalisé les étapes précédentes.
Gestion des utilisateurs
Nous allons maintenant créer un système de gestion des utilisateurs avec les fonctionnalités suivantes :
- Uniquement accessible aux utilisateurs ayant le rôle
Administateur - Afficher la liste des utilisateurs
- Afficher les détails et modifier le rôle d'un utilisateur
Création du contrôleur
Nous allons créer un contrôleur de type resource pour gérer les utilisateurs :
php artisan make:controller Admin/UserController --resource --model=User
Création des routes
On va ajouter la route resource pour le contrôleur UserController après celle des articles :
Route::middleware(['auth', 'verified'])->prefix('admin')->group(function () {
// Gestion des articles (création, modification, suppression)
Route::resource('/articles', AdminArticleController::class);
// Gestion des utilisateurs (Détails et changement de rôle)
Route::resource('/users', UserController::class);
});
Création des vues
Nous allons créer les vues pour les actions index, edit du contrôleur UserController.
Vue index
Nous allons d'abord ajouter un dossier users dans le dossier resources/views/admin et ensuite, créer la vue index.blade.php dans le dossier resources/views/admin/users :
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Utilisateurs') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
<div class="p-6 sm:px-20 bg-white border-b border-gray-200">
<div class="flex justify-between mt-8">
<div class="text-2xl">Liste des utilisateurs</div>
<div class="flex items-center justify-center space-x-8"></div>
</div>
<div class="mt-6 text-gray-500">
<table class="table-auto w-full">
<thead>
<tr class="uppercase text-left">
<th class="px-4 py-2 border">Nom</th>
<th class="px-4 py-2 border">Email</th>
<th class="px-4 py-2 border">Rôle</th>
<th class="px-4 py-2 border">Actions</th>
</tr>
</thead>
<tbody>
@foreach ($users as $user)
<tr
class="hover:bg-gray-50 odd:bg-gray-100 hover:odd:bg-gray-200 transition"
>
<td class="border px-4 py-2">{{ $user->name }}</td>
<td class="border px-4 py-2">{{ $user->email }}</td>
<td class="border px-4 py-2">{{ $user->role->name }}</td>
<td class="border px-4 py-2 space-x-4">
<div class="flex space-x-4">
<a
href="{{ route('users.edit', $user) }}"
class="text-blue-400"
>
<x-heroicon-o-pencil class="w-5 h-5" />
</a>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
<div class="mt-4">{{ $users->links() }}</div>
</div>
</div>
</div>
</div>
</div>
</x-app-layout>
Nous affichons la liste des utilisateurs dans un tableau avec les informations suivantes :
- Nom
- Rôle
- Actions: Modifier
Nous utilisons la pagination de Laravel pour afficher les utilisateurs par page.
Vue edit
Nous allons créer la vue edit.blade.php dans le dossier resources/views/admin/users :
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Utilisateurs') }}
</h2>
</x-slot>
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 py-12">
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-6">
<div class="flex justify-between mt-8">
<div class=" text-2xl">
Détails de l'utilisateur
</div>
</div>
<div class="flex text-gray-500">
<div class="flex justify-center items-center">
<x-avatar :user="$user" class="w-24 h-24" />
</div>
<dl class="mt-6 space-y-6">
<div class="flex space-x-4">
<dt class="text-sm font-medium text-gray-500 w-20 text-right">
Nom
</dt>
<dd class="text-sm text-gray-900">
{{ $user->name }}
</dd>
</div>
<div class="flex space-x-4">
<dt class="text-sm font-medium text-gray-500 w-20 text-right">
Email
</dt>
<dd class="text-sm text-gray-900">
{{ $user->email }}
</dd>
</div>
<div class="flex space-x-4">
<dt class="text-sm font-medium text-gray-500 w-20 text-right">
Rôle
</dt>
<dd class="text-sm text-gray-900">
{{ $user->role->name }}
</dd>
</div>
</dl>
</div>
</div>
</div>
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 py-12">
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-6">
<div class="flex justify-between mt-8">
<div class=" text-2xl">
Modifier le rôle de l'utilisateur
</div>
</div>
<div class="text-gray-500">
<form method="POST" action="{{ route('users.update', $user) }}" class="flex flex-col space-y-4">
@csrf
@method('PUT')
<div>
<x-input-label for="role" :value="__('Rôle')" />
<select id="role" name="role"
class="block mt-1 w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
required>
@foreach ($roles as $role)
<option value="{{ $role }}" {{ $role == $user->role->name ? 'selected' : '' }}>
{{ $role }}
</option>
@endforeach
</select>
<x-input-error :messages="$errors->get('role')" class="mt-2" />
</div>
<div class="flex justify-end">
<x-primary-button type="submit">
{{ __('Modifier') }}
</x-primary-button>
</div>
</form>
</div>
</div>
</div>
</x-app-layout>
Nous affichons les informations de l'utilisateur et nous créons un formulaire pour modifier le rôle de l'utilisateur. Nous utilisons un input de type select pour afficher la liste des rôles.
Controller
Nous allons modifier le controller UserController dans le fichier app/Http/Controllers/Admin/UserController.php en commençant par la méthode index :
public function index()
{
// On récupère tous les utilisateurs avec pagination de 10 par page
$users = User::paginate(10);
// On passe les utilisateurs à la vue `admin.users.index`
return view('admin.users.index', [
'users' => $users,
]);
}
Rien de nouveau ici, nous récupérons tous les utilisateurs avec pagination de 10 par page et nous les passons à la vue admin.users.index.
Nous allons maintenant modifier la méthode edit :
public function edit(User $user)
{
// On récupère tous les rôles disponibles
// On utilise la méthode pluck() pour récupérer uniquement le nom des rôles dans un tableau
$roles = \App\Models\Role::pluck('name');
// On passe l'utilisateur et les rôles à la vue `admin.users.edit`
return view('admin.users.edit', [
'user' => $user,
'roles' => $roles,
]);
}
Pour rappel, la méthode edit reçoit en paramètre un objet User grâce à la route users/{user}/edit. Cette route est responsable de générer la vue comportant le formulaire de modification du rôle de l'utilisateur. Nous récupérons donc tous les rôles disponibles et nous les passons à la vue admin.users.edit avec l'utilisateur.
Nous allons maintenant modifier la méthode update :
public function update(Request $request, User $user)
{
// Validation du rôle
$request->validate([
// On vérifie que le rôle est bien un des rôles définis dans le modèle Role
'role' => 'required|in:'.implode(',', \App\Models\Role::roles()),
]);
// Mise à jour du rôle
// On récupère le rôle correspondant au nom du rôle passé dans la requête
$role = \App\Models\Role::where('name', $request->role)->first();
// On associe le rôle à l'utilisateur en passant par la relation
$user->role()->associate($role);
// On sauvegarde l'utilisateur en base de données
$user->save();
// Redirection vers la page de modification de l'utilisateur
return redirect()->back();
}
Nous validons le rôle passé dans la requête. Nous récupérons le rôle correspondant au nom du rôle passé dans la requête et nous l'associons à l'utilisateur en passant par la relation role. Nous sauvegardons l'utilisateur en base de données et nous redirigeons vers la page de modification de l'utilisateur.
Autorisations avec les policies
Nous n'avons pas encore protégé l'accès aux pages réservées aux administrateurs. Pour gérer finement les autorisations des différents groupes d'utilisateurs, Laravel nous propose les policies.
Les policies sont des classes qui permettent de définir les autorisations des utilisateurs. Pour chaque Model, nous pouvons définir une policy qui permettra de définir les autorisations pour les actions CRUD (Create, Read, Update, Delete).
Nous allons créer une policy pour le modèle User en utilisant la commande Artisan suivante :
php artisan make:policy UserPolicy --model=User
Nous obtenons le fichier app/Policies/UserPolicy.php qui contient les méthodes create, view, update et delete qui correspondent aux actions CRUD. Nous allons commencer par définir les autorisations pour la méthode viewAny qui nous servira à déterminer si l'utilisateur peut accéder à la liste des utilisateurs :
public function viewAny(User $user)
{
// Seul l'administrateur peut voir la liste des utilisateurs
return \App\Models\Role::ADMIN === $user->role->name;
}
Cette méthode retourne true si l'utilisateur est un administrateur et false sinon.
Nous allons maintenant définir les autorisations pour la méthode update qui nous servira à déterminer si l'utilisateur peut modifier un utilisateur :
public function update(User $user, User $model)
{
return $user->role->name === \App\Models\Role::ADMIN;
}
La méthode update reçoit en paramètre l'utilisateur connecté et l'utilisateur à modifier. Cette méthode retourne true si l'utilisateur connecté est un administrateur et false sinon, comme pour la méthode viewAny.
On n'a pas besoin de définir les autorisations pour les méthodes create, view et delete car nous n'utilisons pas ces méthodes dans notre application pour le modèle User.
Nous allons maintenant modifier le controller UserController pour utiliser les policies. Nous allons commencer par modifier la méthode index :
public function index()
{
// On vérifie que l'utilisateur courant est un administrateur
Gate::authorize('viewAny', User::class);
// On récupère tous les utilisateurs avec pagination de 10 par page
$users = User::paginate(10);
// On passe les utilisateurs à la vue `admin.users.index`
return view('admin.users.index', [
'users' => $users,
]);
}
Nous utilisons la classe Gate pour appeler la méthode viewAny de la policy UserPolicy dans laquelle nous vérifions que l'utilisateur courant est un administrateur. Si ce n'est pas le cas, une exception Illuminate\Auth\Access\AuthorizationException sera levée.
N'oubliez pas d'importer la classe Gate en haut du fichier app/Http/Controllers/Admin/UserController.php :
use Illuminate\Support\Facades\Gate;
Nous allons maintenant modifier la méthode edit :
public function edit(User $user)
{
// On vérifie que l'utilisateur courant est un administrateur
Gate::authorize('update', $user);
// On récupère tous les rôles disponibles
// On utilise la méthode pluck() pour récupérer uniquement le nom des rôles dans un tableau
$roles = \App\Models\Role::pluck('name');
// On passe l'utilisateur et les rôles à la vue `admin.users.edit`
return view('admin.users.edit', [
'user' => $user,
'roles' => $roles,
]);
}
Nous utilisons la classe Gate pour appeler la méthode update de la policy UserPolicy. Cette méthode reçoit en paramètre l'utilisateur courant et l'utilisateur à modifier. Si l'utilisateur courant n'est pas un administrateur, une exception Illuminate\Auth\Access\AuthorizationException sera levée.
Nous allons maintenant ajouter la même ligne de code dans la méthode update :
public function update(Request $request, User $user)
{
// On vérifie que l'utilisateur courant est un administrateur
Gate::authorize('update', $user);
// ... code existant ...
}
Nous avons maintenant protégé l'accès aux pages réservées aux administrateurs. Vous pouvez tester en vous connectant avec un utilisateur qui n'est pas un administrateur et en essayant d'accéder à la page /admin/users ou à la page de modification d'un utilisateur /admin/users/1/edit.
Navigation
On doit aussi afficher le lien vers la page de gestion des utilisateurs dans la barre de navigation seulement si l'utilisateur est un administrateur. Pour cela, nous allons modifier le fichier resources/views/layouts/navigation.blade.php :
<nav
x-data="{ open: false }"
class="bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700"
>
<!-- Primary Navigation Menu -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<!-- Logo -->
<div class="shrink-0 flex items-center">
<a href="{{ route('dashboard') }}">
<x-application-logo
class="block h-9 w-auto fill-current text-gray-800 dark:text-gray-200"
/>
</a>
</div>
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
<x-nav-link
:href="route('dashboard')"
:active="request()->routeIs('dashboard')"
>
{{ __('Dashboard') }}
</x-nav-link>
<x-nav-link
:href="route('articles.index')"
:active="request()->routeIs('articles.*')"
>
{{ __('Articles') }}
</x-nav-link>
@can('viewAny', App\Models\User::class)
<x-nav-link
:href="route('users.index')"
:active="request()->routeIs('users.*')"
>
{{ __('Utilisateurs') }}
</x-nav-link>
@endcan
</div>
</div>
<!-- Settings Dropdown -->
<div class="hidden sm:flex sm:items-center sm:ml-6">
<x-dropdown align="right" width="48">
<x-slot name="trigger">
<button
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white dark:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300 focus:outline-none transition ease-in-out duration-150"
>
<div>{{ Auth::user()->name }}</div>
<div class="ml-1">
<svg
class="fill-current h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
>
<path
fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
</x-slot>
<x-slot name="content">
<x-dropdown-link :href="route('profile.edit')">
{{ __('Profile') }}
</x-dropdown-link>
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-dropdown-link
:href="route('logout')"
onclick="event.preventDefault();
this.closest('form').submit();"
>
{{ __('Log Out') }}
</x-dropdown-link>
</form>
</x-slot>
</x-dropdown>
</div>
<!-- Hamburger -->
<div class="-mr-2 flex items-center sm:hidden">
<button
@click="open = ! open"
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-900 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-900 focus:text-gray-500 dark:focus:text-gray-400 transition duration-150 ease-in-out"
>
<svg
class="h-6 w-6"
stroke="currentColor"
fill="none"
viewBox="0 0 24 24"
>
<path
:class="{ 'hidden': open, 'inline-flex': !open }"
class="inline-flex"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
<path
:class="{ 'hidden': !open, 'inline-flex': open }"
class="hidden"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
</div>
<!-- Responsive Navigation Menu -->
<div :class="{ 'block': open, 'hidden': !open }" class="hidden sm:hidden">
<div class="pt-2 pb-3 space-y-1">
<x-responsive-nav-link
:href="route('dashboard')"
:active="request()->routeIs('dashboard')"
>
{{ __('Dashboard') }}
</x-responsive-nav-link>
<x-responsive-nav-link
:href="route('articles.index')"
:active="request()->routeIs('articles.*')"
>
{{ __('Articles') }}
</x-responsive-nav-link>
@can('viewAny', App\Models\User::class)
<x-responsive-nav-link
:href="route('users.index')"
:active="request()->routeIs('users.*')"
>
{{ __('Utilisateurs') }}
</x-responsive-nav-link>
@endcan
</div>
<!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-600">
<div class="px-4">
<div class="font-medium text-base text-gray-800 dark:text-gray-200">
{{ Auth::user()->name }}
</div>
<div class="font-medium text-sm text-gray-500">
{{ Auth::user()->email }}
</div>
</div>
<div class="mt-3 space-y-1">
<x-responsive-nav-link :href="route('profile.edit')">
{{ __('Profile') }}
</x-responsive-nav-link>
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-responsive-nav-link
:href="route('logout')"
onclick="event.preventDefault();
this.closest('form').submit();"
>
{{ __('Log Out') }}
</x-responsive-nav-link>
</form>
</div>
</div>
</div>
</nav>
Nous avons ajouté le lien vers la liste des utilisateurs dans le menu de navigation pour PC et pour téléphone :
@can('viewAny', App\Models\User::class)
<x-nav-link
:href="route('users.index')"
:active="request()->routeIs('users.*')"
>
{{ __('Utilisateurs') }}
</x-nav-link>
@endcan
et
@can('viewAny', App\Models\User::class)
<x-responsive-nav-link
:href="route('users.index')"
:active="request()->routeIs('users.*')"
>
{{ __('Utilisateurs') }}
</x-responsive-nav-link>
@endcan
Nous utilisons la directive @can pour vérifier si l'utilisateur connecté a la permission de voir la liste des utilisateurs. Si c'est le cas, nous affichons le lien.
Autorisations pour les articles
Vous pouvez maintenant faire la même chose pour protéger la partie articles. Vous devrez créer une policy pour les articles et définir les autorisations. Ensuite, utilisez la classe Gate dans les contrôleurs et la directive @can dans les vues pour vérifier les autorisations.
- Les utilisateurs ne peuvent rien faire avec les articles.
- Les auteurs peuvent créer des articles, modifier leurs propres articles et supprimer leurs propres articles.
- Les administrateurs peuvent créer des articles, modifier tous les articles et supprimer tous les articles.
Système de commentaires
Nous allons ajouter un système de commentaires à notre blog. Les utilisateurs connectés pourront commenter les articles. Les administrateurs et le propriétaire d'un article pourront supprimer les commentaires.
Pour cela, nous allons devoir créer un modèle Comment et une table comments dans la base de données. La table comments aura les champs suivants :
- id
- article_id
- user_id
- body
- created_at
- updated_at
Le modèle Comment aura les relations suivantes:
- article: belongsTo
- user: belongsTo
Nous devrons aussi définir les relations inverse dans les modèles Article et User.
Ensuite, nous devrons modifier l'ArticleController pour ajouter la possibilité de commenter un article. Nous allons aussi modifier la vue show pour afficher les commentaires et ajouter un formulaire pour ajouter un commentaire.
Nous n'oublierons pas de gérer les autorisations pour les commentaires.
Modèle Comment
Nous allons créer le modèle Comment avec la commande artisan :
php artisan make:model Comment -mfs
Nous avons utilisé les options -mfs pour créer le modèle, la migration, la factory et le seeder en une seule commande.
Nous allons d'abord modifier le fichier de migration create_comments_table pour ajouter les champs body, article_id et user_id :
public function up()
{
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('article_id')->nullable()->constrained()->nullOnDelete();
$table->text('body');
$table->timestamps();
});
}
Nous avons ajouté les champs user_id et article_id qui seront utilisés pour les relations. Nous avons aussi ajouté le champ body qui contiendra le texte du commentaire.
Nous allons ensuite modifier le modèle Comment pour ajouter les relations :
class Comment extends Model
{
use HasFactory;
public function article()
{
return $this->belongsTo(Article::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
}
Nous devons aussi modifier les modèles Article et User pour ajouter les relations inverses:
class Article extends Model
{
// ... code précédent
public function comments()
{
return $this->hasMany(Comment::class);
}
}
et
class User extends Authenticatable
{
// ... code précédent
public function comments()
{
return $this->hasMany(Comment::class);
}
}
Données de test
Nous allons ajouter des données de test pour les commentaires. Nous allons modifier la CommentFactory pour générer des données aléatoires :
class CommentFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
return [
// n'importe quel utilisateur
'user_id' => \App\Models\User::get()->random()->id,
// n'importe quel article
'article_id' => \App\Models\Article::get()->random()->id,
// un texte aléatoire de 20 à 400 caractères
'body' => fake()->realTextBetween($minNbChars = 20, $maxNbChars = 400),
// une date aléatoire entre -2 mois et maintenant
'created_at' => fake()->dateTimeBetween('-2 months', 'now'),
];
}
}
Nous allons modifier le seeder CommentSeeder pour utiliser la factory :
class CommentSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run()
{
\App\Models\Comment::factory(300)->create();
}
}
Nous allons ensuite modifier la méthode run du seeder DatabaseSeeder pour appeler le seeder CommentSeeder :
public function run(): void
{
// On supprime les anciennes images de test
$images = Storage::disk('public')->files('images');
Storage::disk('public')->delete($images);
// On appelle les seeders dans l'ordre pour éviter les erreurs de clés étrangères
$this->call([
RolesTableSeeder::class,
UsersTableSeeder::class,
ArticlesTableSeeder::class,
CommentSeeder::class,
]);
}
Vous pouvez maintenant recréer la base de données et exécuter les seeders:
php artisan migrate:fresh --seed
Contrôleur ArticleController
Nous allons modifier le contrôleur ArticleController de la partie front-office de notre blog pour afficher les commentaires.
Nous allons modifier la méthode show pour récupérer les commentaires de l'article et les passer à la vue :
public function show($id)
{
// On récupère l'article et on renvoie une erreur 404 si l'article n'existe pas
$article = Article::findOrFail($id);
// On récupère les commentaires de l'article, avec les utilisateurs associés (via la relation)
// On les trie par date de création (le plus ancien en premier)
$comments = $article
->comments()
->with('user')
->orderBy('created_at')
->get()
;
// On renvoie la vue avec les données
return view('front.articles.show', [
'article' => $article,
'comments' => $comments,
]);
}
Nous allons ensuite modifier la vue front.articles.show pour afficher les commentaires :
<x-guest-layout>
<h1 class="font-bold text-xl mb-4 capitalize">{{ $article->title }}</h1>
<div class="mb-4 text-xs text-gray-500">
{{ $article->published_at?->diffForHumans() }}
</div>
<div class="flex items-center justify-center">
<img src="{{ asset('storage/' . $article->img_path) }}" alt="illustration de l'article"
class="rounded shadow aspect-auto object-cover object-center">
</div>
<div class="mt-4">
{!! \nl2br(e($article->body)) !!}
</div>
<div class="flex mt-8">
<x-avatar class="h-20 w-20" :user="$article->user" />
<div class="ml-4 flex flex-col justify-center">
<div class="text-gray-700">
{{ $article->user->name }}
</div>
<div class="text-gray-500">
{{ $article->user->email }}
</div>
</div>
</div>
<div class="mt-8 flex items-center justify-center">
<a href="{{ route('front.articles.index') }}" class="font-bold bg-white text-gray-700 px-4 py-2 rounded shadow">
Retour à la liste des articles
</a>
</div>
<div class="mt-8">
<h2 class="font-bold text-xl mb-4">Commentaires</h2>
<div class="flex-col space-y-4">
@forelse ($article->comments as $comment)
<div class="flex bg-white rounded-md shadow p-4 space-x-4">
<div class="flex justify-start items-start h-full">
<x-avatar class="h-10 w-10" :user="$comment->user" />
</div>
<div class="flex flex-col justify-center">
<div class="text-gray-700">
{{ $comment->user->name }}
</div>
<div class="text-gray-500">
{{ $comment->created_at->diffForHumans() }}
</div>
<div class="text-gray-700">
{{ $comment->body }}
</div>
</div>
</div>
@empty
<div class="flex bg-white rounded-md shadow p-4 space-x-4">
Aucun commentaire pour l'instant
</div>
@endforelse
</div>
</x-guest-layout>
Si vous visitez la page d'un article, vous devriez voir les commentaires en bas de la page.
Routes pour les commentaires
Nous allons maintenant ajouter les routes pour les commentaires, après la route front.articles.show :
// ... autres routes
// Détail d'un article
Route::get('/articles/{id}', [ArticleController::class, 'show'])->name('front.articles.show');
// Gestion des commentaires, uniquement pour les utilisateurs authentifiés
Route::middleware('auth')->group(function () {
// Ajout d'un commentaire
Route::post('/articles/{article}/comments', [ArticleController::class, 'addComment'])->name('front.articles.comments.add');
// Suppression d'un commentaire
Route::delete('/articles/{article}/comments/{comment}', [ArticleController::class, 'deleteComment'])->name('front.articles.comments.delete');
});
// ... autres routes
Nous avons donc ajouté deux routes :
front.articles.comments.addpour ajouter un commentairefront.articles.comments.deletepour supprimer un commentaire
Méthodes addComment et deleteComment
Nous allons maintenant ajouter les méthodes addComment et deleteComment au contrôleur ArticleController.
Nous allons commencer par la méthode addComment :
public function addComment(Request $request, Article $article)
{
// On vérifie que l'utilisateur est authentifié
$request->validate([
'body' => 'required|string|max:2000',
]);
// On crée le commentaire
$comment = $article->comments()->make();
// On remplit les données
$comment->body = $request->input('body');
$comment->user_id = Auth::user()->id;
// On sauvegarde le commentaire
$comment->save();
// On redirige vers la page de l'article
return redirect()->back();
}
Nous allons ensuite ajouter la méthode deleteComment :
public function deleteComment(Article $article, Comment $comment)
{
// On vérifie que l'utilisateur à le droit de supprimer le commentaire
Gate::authorize('delete', $comment);
// On supprime le commentaire
$comment->delete();
// On redirige vers la page de l'article
return redirect()->back();
}
Autorisation
Nous allons maintenant ajouter une autorisation pour la suppression d'un commentaire.
Nous allons créer un fichier CommentPolicy.php à l'aide de la commande suivante :
php artisan make:policy CommentPolicy
Nous allons ensuite modifier le fichier app/Policies/CommentPolicy.php pour ajouter la méthode delete :
<?php
namespace App\Policies;
use App\Models\Comment;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class CommentPolicy
{
use HandlesAuthorization;
/**
* Create a new policy instance.
*/
public function __construct()
{
}
public function delete(User $user, Comment $comment)
{
// Seul l'administateur ou le créateur du commentaire peut supprimer un commentaire
return \App\Models\Role::ADMIN === $user->role->name || $user->id === $comment->user_id;
}
}
On vérifie que l'utilisateur est soit l'administrateur, soit qu'il est le créateur du commentaire en comparant les identifiants.
Formulaire de création de commentaire
Nous allons maintenant ajouter un formulaire pour créer un commentaire et ajouter le bouton de suppression d'un commentaire visible uniquement par l'administrateur et le créateur du commentaire.
Nous allons modifier la vue front.articles.show pour ajouter le formulaire et le bouton de suppression :
<x-guest-layout>
<h1 class="font-bold text-xl mb-4 capitalize">{{ $article->title }}</h1>
<div class="mb-4 text-xs text-gray-500">
{{ $article->published_at?->diffForHumans() }}
</div>
<div class="flex items-center justify-center">
<img src="{{ asset('storage/' . $article->img_path) }}" alt="illustration de l'article"
class="rounded shadow aspect-auto object-cover object-center" />
</div>
<div class="mt-4">{!! \nl2br(e($article->body)) !!}</div>
<div class="flex mt-8">
<x-avatar class="h-20 w-20" :user="$article->user" />
<div class="ml-4 flex flex-col justify-center">
<div class="text-gray-700">{{ $article->user->name }}</div>
<div class="text-gray-500">{{ $article->user->email }}</div>
</div>
</div>
<div class="mt-8 flex items-center justify-center">
<a href="{{ route('front.articles.index') }}" class="font-bold bg-white text-gray-700 px-4 py-2 rounded shadow">
Retour à la liste des articles
</a>
</div>
<div class="mt-8">
<h2 class="font-bold text-xl mb-4">Commentaires</h2>
<div class="flex-col space-y-4">
@forelse ($article->comments as $comment)
<div class="flex bg-white rounded-md shadow p-4 space-x-4">
<div class="flex justify-start items-start h-full">
<x-avatar class="h-10 w-10" :user="$comment->user" />
</div>
<div class="flex flex-col justify-center w-full space-y-4">
<div class="flex justify-between">
<div class="flex space-x-4 items-center justify-center">
<div class="flex flex-col justify-center">
<div class="text-gray-700">{{ $comment->user->name }}</div>
<div class="text-gray-500 text-sm">
{{ $comment->created_at->diffForHumans() }}
</div>
</div>
</div>
<div class="flex justify-center">
@can('delete', $comment)
<button x-data="{ id: {{ $comment->id }} }"
x-on:click.prevent="window.selected = id; $dispatch('open-modal', 'confirm-comment-deletion');"
type="submit" class="font-bold bg-white text-gray-700 px-4 py-2 rounded shadow">
<x-heroicon-o-trash class="h-5 w-5" />
</button>
@endcan
</div>
</div>
<div class="flex flex-col justify-center w-full text-gray-700">
<p class="border bg-gray-100 rounded-md p-4">
{{ $comment->body }}
</p>
</div>
</div>
</div>
@empty
<div class="flex bg-white rounded-md shadow p-4 space-x-4">
Aucun commentaire pour l'instant
</div>
@endforelse @auth
<form action="{{ route('front.articles.comments.add', $article) }}" method="POST"
class="flex bg-white rounded-md shadow p-4">
@csrf
<div class="flex justify-start items-start h-full mr-4">
<x-avatar class="h-10 w-10" :user="auth()->user()" />
</div>
<div class="flex flex-col justify-center w-full">
<div class="text-gray-700">{{ auth()->user()->name }}</div>
<div class="text-gray-500 text-sm">{{ auth()->user()->email }}</div>
<div class="text-gray-700">
<textarea name="body" id="body" rows="4" placeholder="Votre commentaire"
class="w-full rounded-md shadow-sm border-gray-300 bg-gray-100 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 mt-4"></textarea>
</div>
<div class="text-gray-700 mt-2 flex justify-end">
<button type="submit" class="font-bold bg-white text-gray-700 px-4 py-2 rounded shadow">
Ajouter un commentaire
</button>
</div>
</div>
</form>
@else
<div class="flex bg-white rounded-md shadow p-4 text-gray-700 justify-between items-center">
<span> Vous devez être connecté pour ajouter un commentaire </span>
<a href="{{ route('login') }}" class="font-bold bg-white text-gray-700 px-4 py-2 rounded shadow-md">Se
connecter</a>
</div>
@endauth
</div>
<x-modal name="confirm-comment-deletion" focusable>
<form method="post"
onsubmit="event.target.action= '/articles/{{ $article->id }}/comments/' + window.selected"
class="p-6">
@csrf @method('DELETE')
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
Êtes-vous sûr de vouloir supprimer ce commentaire ?
</h2>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Cette action est irréversible. Toutes les données seront supprimées.
</p>
<div class="mt-6 flex justify-end">
<x-secondary-button x-on:click="$dispatch('close')">
Annuler
</x-secondary-button>
<x-danger-button class="ml-3" type="submit">
Supprimer
</x-danger-button>
</div>
</form>
</x-modal>
</div>
</x-guest-layout>
Nous avons maintenant un formulaire pour ajouter un commentaire, et un bouton pour supprimer un commentaire. Nous avons également ajouté un modal pour confirmer la suppression d'un commentaire.
Vous pouvez essayer d'écrire un commentaire, puis de le supprimer pour voir si tout fonctionne correctement. Si vous avez des erreurs, vérifiez que vous avez bien suivi les étapes précédentes.
Failles XSS
Les failles XSS sont des failles de sécurité qui permettent à un attaquant d'exécuter du code JavaScript dans le navigateur d'un utilisateur. Cela peut être utilisé pour voler des informations confidentielles, des cookies, ou pour envoyer des données à un serveur malveillant.
Pour faire cela, un attaquant peut envoyer un commentaire avec du code JavaScript. Par exemple, si un utilisateur peut ajouter un commentaire, il peut envoyer le commentaire suivant :
<script>
alert("Hello world");
</script>
Lorsque le navigateur de l'utilisateur affichera le commentaire, il exécutera le code JavaScript. Cela peut être utilisé pour voler des informations confidentielles, des cookies, ou pour envoyer des données à un serveur malveillant.
Pour éviter cela, nous utilisons depuis le début du cours les doubles moustaches ou la fonction helper e() de Laravel pour afficher des données. Cela permet d'échapper automatiquement les données pour éviter les failles XSS.
Vous pouvez aussi supprimer les balises HTML avec la fonction strip_tags de PHP :
$body = strip_tags($request->body);
Faites toujours attention aux données que vous pouvez recevoir de l'utilisateur. Si vous utilisez une librairie tierce, vérifiez que celle-ci n'est pas vulnérable aux failles XSS.
Afficher le nombre de commentaires
Nous allons maintenant afficher le nombre de commentaires sur l'article. Pour cela, nous allons devoir modifier les méthodes index de HomepageController et ArticlesController pour récupérer le nombre de commentaires.
Commençons par modifier la méthode index de HomepageController :
public function index()
{
$articles = Article::query()
->where('published_at', '<', now())
->withCount('comments')
->orderByDesc('published_at')
->take(4)
->get()
;
return view('homepage.index', [
'articles' => $articles,
]);
}
Il faut aussi retirer les liens de paginations de la vue homepage.index et ajouter un lien vers tous les articles :
<x-guest-layout>
<h1 class="font-bold text-xl mb-4">Liste des articles</h1>
<ul class="grid sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-8">
@foreach ($articles as $article)
<li>
<x-article-card :article="$article" />
</li>
@endforeach
</ul>
<div class="mt-8 flex items-center justify-center">
<a href="{{ route('front.articles.index') }}" class="font-bold bg-white text-gray-700 px-4 py-2 rounded shadow">
Tous les articles
</a>
</div>
</x-guest-layout>
Nous avons ajouté la méthode withCount pour récupérer le nombre de commentaires. Cela va ajouter une colonne comments_count à chaque article.
Nous allons maintenant modifier la méthode index de ArticlesController :
public function index(Request $request)
{
$articles = Article::query()
->where('published_at', '<', now())
->when($request->query('search'), function ($query) use ($request) {
$query->where('body', 'LIKE', '%'.$request->query('search').'%')
->orWhere('title', 'LIKE', '%'.$request->query('search').'%')
->orWhereHas('user', function ($query) use ($request) {
$query->where('name', 'LIKE', '%'.$request->query('search').'%');
})
;
})
->withCount('comments')
->orderByDesc('published_at')
->paginate(12)
;
return view('front.articles.index', [
'articles' => $articles,
]);
}
Même chose ici, nous avons ajouté la méthode withCount pour récupérer le nombre de commentaires.
Nous allons maintenant modifier le component article-card pour afficher le nombre de commentaires :
<a class="flex flex-col h-full space-y-4 bg-white rounded-md shadow-md p-5 w-full hover:shadow-lg hover:scale-105 transition"
href="{{ route('front.articles.show', $article) }}">
<img src="{{ Storage::url($article->img_path) }}" alt="illustration de l'article" />
<div class="uppercase font-bold text-gray-800">{{ $article->title }}</div>
<div class="flex-grow text-gray-700 text-sm text-justify">
{{ Str::limit($article->body, 120) }}
</div>
<div class="flex justify-between items-center">
<div class="text-sm text-gray-500">
{{ $article->published_at->diffForHumans() }}
</div>
<div class="flex items-center space-x-2">
<x-heroicon-o-chat-bubble-bottom-center-text class="h-5 w-5 text-gray-500" />
<div class="text-sm text-gray-500">{{ $article->comments_count }}</div>
</div>
</div>
</a>
Nous avons ajouté une icône et le nombre de commentaires en bas à droite de la carte.
Vous pouvez essayer d'ajouter un commentaire, puis de voir si le nombre de commentaires est bien actualisé.
Profil public des utilisateurs
Nous allons à présent ajouter un profil public pour les utilisateurs. Nous allons afficher les articles publiés par l'utilisateur, et les commentaires qu'il a écrits.
Créer la route
Nous allons commencer par créer la route pour afficher le profil public d'un utilisateur. Placez cette route dans le fichier routes/web.php, juste en dessous des routes de gestion du profil :
// Gestion du profil utilisateur
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
Route::patch('/profile/avatar', [ProfileController::class, 'updateAvatar'])->name('profile.avatar.update');
});
// Détail d'un profil utilisateur
Route::get('/profile/{user}', [ProfileController::class, 'show'])->name('profile.show');
Controller
Nous allons ajouter une méthode show au début du controller ProfileController :
public function show(User $user): View
{
// Les articles publiés par l'utilisateur
$articles = $user
->articles()
->where('published_at', '<', now())
->withCount('comments')
->orderByDesc('published_at')
->get()
;
// Les commentaires de l'utilisateur triés par date de création
$comments = $user
->comments()
->orderByDesc('created_at')
->get()
;
// On renvoie la vue avec les données
return view('profile.show', [
'user' => $user,
'articles' => $articles,
'comments' => $comments,
]);
}
Nous récupérons les articles publiés par l'utilisateur, et les commentaires qu'il a écrits.
Vue
Nous allons maintenant créer la vue profile.show :
<x-guest-layout>
<div class="flex w-full">
<x-avatar class="h-20 w-20" :user="$user" />
<div class="ml-4 flex flex-col">
<div class="text-gray-800 font-bold">{{ $user->name }}</div>
<div class="text-gray-700 text-sm">{{ $user->email }}</div>
<div class="text-gray-500 text-xs">{{ $user->role->name }}</div>
<div class="text-gray-500 text-xs">
Membre depuis {{ $user->created_at->diffForHumans() }}
</div>
</div>
</div>
<div class="mt-8">
<h2 class="font-bold text-xl mb-4">Articles</h2>
<ul class="grid sm:grid-cols-2 gap-8">
@forelse ($articles as $article)
<li>
<x-article-card :article="$article" />
</li>
@empty
<div class="text-gray-700">Aucun article</div>
@endforelse
</ul>
</div>
<div class="mt-8">
<h2 class="font-bold text-xl mb-4">Commentaires</h2>
<div class="flex-col space-y-4">
@forelse ($comments as $comment)
<div class="flex bg-white rounded-md shadow p-4 space-x-4">
<div class="flex justify-start items-start h-full">
<x-avatar class="h-10 w-10" :user="$comment->user" />
</div>
<div class="flex flex-col justify-center w-full space-y-4">
<div class="flex justify-between">
<div class="flex space-x-4 items-center justify-center">
<div class="flex flex-col justify-center">
<div class="text-gray-700">{{ $comment->user->name }}</div>
<div class="text-gray-500 text-sm">
{{ $comment->created_at->diffForHumans() }}
</div>
</div>
</div>
<div class="flex justify-center">
@can('delete', $comment)
<button
x-data="{ id: {{ $comment->id }} }"
x-on:click.prevent="window.selected = id; $dispatch('open-modal', 'confirm-comment-deletion');"
type="submit"
class="font-bold bg-white text-gray-700 px-4 py-2 rounded shadow"
>
<x-heroicon-o-trash class="h-5 w-5" />
</button>
@endcan
</div>
</div>
<div class="flex flex-col justify-center w-full text-gray-700">
<p class="border bg-gray-100 rounded-md p-4">
{{ $comment->body }}
</p>
</div>
</div>
</div>
@empty
<div class="flex bg-white rounded-md shadow p-4 space-x-4">
Aucun commentaire pour l'instant
</div>
@endforelse
</div>
<x-modal name="confirm-comment-deletion" focusable>
<form
method="post"
onsubmit="event.target.action= '/articles/{{ $article->id ?? 1 }}/comments/' + window.selected"
class="p-6"
>
@csrf @method('DELETE')
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
Êtes-vous sûr de vouloir supprimer ce commentaire ?
</h2>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Cette action est irréversible. Toutes les données seront supprimées.
</p>
<div class="mt-6 flex justify-end">
<x-secondary-button x-on:click="$dispatch('close')">
Annuler
</x-secondary-button>
<x-danger-button class="ml-3" type="submit">
Supprimer
</x-danger-button>
</div>
</form>
</x-modal>
</div>
</x-guest-layout>
Nous pouvons maintenant ajouter des liens vers le profil de l'utilisateur dans les articles et les commentaires dans la vue article.show :
<x-guest-layout>
<h1 class="font-bold text-xl mb-4 capitalize">{{ $article->title }}</h1>
<div class="mb-4 text-xs text-gray-500">
{{ $article->published_at?->diffForHumans() }}
</div>
<div class="flex items-center justify-center">
<img src="{{ asset('storage/' . $article->img_path) }}" alt="illustration de l'article"
class="rounded shadow aspect-auto object-cover object-center" />
</div>
<div class="mt-4">{!! \nl2br(e($article->body)) !!}</div>
<a class="flex mt-8 hover:-translate-y-1 transition
" href="{{ route('profile.show', $article->user) }}">
<x-avatar class="h-20 w-20" :user="$article->user" />
<div class="ml-4 flex flex-col justify-center">
<div class="text-gray-700">{{ $article->user->name }}</div>
<div class="text-gray-500">{{ $article->user->email }}</div>
</div>
</a>
<div class="mt-8 flex items-center justify-center">
<a href="{{ route('front.articles.index') }}" class="font-bold bg-white text-gray-700 px-4 py-2 rounded shadow">
Retour à la liste des articles
</a>
</div>
<div class="mt-8">
<h2 class="font-bold text-xl mb-4">Commentaires</h2>
<div class="flex-col space-y-4">
@forelse ($article->comments as $comment)
<div class="flex bg-white rounded-md shadow p-4 space-x-4">
<a class="flex justify-start items-start h-full" href="{{ route('profile.show', $comment->user) }}">
<x-avatar class="h-10 w-10" :user="$comment->user" />
</a>
<div class="flex flex-col justify-center w-full space-y-4">
<div class="flex justify-between">
<div class="flex space-x-4 items-center justify-center">
<div class="flex flex-col justify-center">
<a class="text-gray-700" href="{{ route('profile.show', $comment->user) }}">
{{ $comment->user->name }}
</a>
<div class="text-gray-500 text-sm">
{{ $comment->created_at->diffForHumans() }}
</div>
</div>
</div>
<div class="flex justify-center">
@can('delete', $comment)
<button x-data="{ id: {{ $comment->id }} }"
x-on:click.prevent="window.selected = id; $dispatch('open-modal', 'confirm-comment-deletion');"
type="submit" class="font-bold bg-white text-gray-700 px-4 py-2 rounded shadow">
<x-heroicon-o-trash class="h-5 w-5" />
</button>
@endcan
</div>
</div>
<div class="flex flex-col justify-center w-full text-gray-700">
<p class="border bg-gray-100 rounded-md p-4">
{{ $comment->body }}
</p>
</div>
</div>
</div>
@empty
<div class="flex bg-white rounded-md shadow p-4 space-x-4">
Aucun commentaire pour l'instant
</div>
@endforelse @auth
<form action="{{ route('front.articles.comments.add', $article) }}" method="POST"
class="flex bg-white rounded-md shadow p-4">
@csrf
<div class="flex justify-start items-start h-full mr-4">
<x-avatar class="h-10 w-10" :user="auth()->user()" />
</div>
<div class="flex flex-col justify-center w-full">
<div class="text-gray-700">{{ auth()->user()->name }}</div>
<div class="text-gray-500 text-sm">{{ auth()->user()->email }}</div>
<div class="text-gray-700">
<textarea name="body" id="body" rows="4" placeholder="Votre commentaire"
class="w-full rounded-md shadow-sm border-gray-300 bg-gray-100 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 mt-4"></textarea>
</div>
<div class="text-gray-700 mt-2 flex justify-end">
<button type="submit" class="font-bold bg-white text-gray-700 px-4 py-2 rounded shadow">
Ajouter un commentaire
</button>
</div>
</div>
</form>
@else
<div class="flex bg-white rounded-md shadow p-4 text-gray-700 justify-between items-center">
<span> Vous devez être connecté pour ajouter un commentaire </span>
<a href="{{ route('login') }}" class="font-bold bg-white text-gray-700 px-4 py-2 rounded shadow-md">Se
connecter</a>
</div>
@endauth
</div>
<x-modal name="confirm-comment-deletion" focusable>
<form method="post"
onsubmit="event.target.action= '/articles/{{ $article->id }}/comments/' + window.selected"
class="p-6">
@csrf @method('DELETE')
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
Êtes-vous sûr de vouloir supprimer ce commentaire ?
</h2>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Cette action est irréversible. Toutes les données seront supprimées.
</p>
<div class="mt-6 flex justify-end">
<x-secondary-button x-on:click="$dispatch('close')">
Annuler
</x-secondary-button>
<x-danger-button class="ml-3" type="submit">
Supprimer
</x-danger-button>
</div>
</form>
</x-modal>
</div>
</x-guest-layout>
Les visiteurs peuvent maintenant accéder aux profils des utilisateurs en cliquant sur leurs noms ou avatars.
Exercices complémentaires
- Ajoutez la possibilité de liker un article pour un utilisateur connecté
- Ajoutez la possibilité de suivre un utilisateur (Follow)
- Ajouter une gestion des personnes qu’on follow et la liste des personnes qui nous follow
Code source
Le code source de ce tutoriel est disponible sur GitHub : https://github.com/opmvpc/blog-24-laravel11
