Construire un système simple de commentaires Laravel
Fait partie de la série Améliorer votre blog VitePress avec une API Laravel
L'authentification GitHub est en place. Nous pouvons maintenant construire la fonctionnalité qu'elle rend possible : les commentaires.
Avant d'écrire du code, définissons la première version du système de commentaires. Nous voulons prendre en charge les interactions essentielles sans ajouter plus de complexité que nécessaire :
- Les utilisateurs doivent pouvoir laisser des commentaires sur les articles.
- Les utilisateurs doivent pouvoir modifier ou supprimer leurs commentaires.
- Les réponses aux commentaires ne sont pas prévues.
- La prise en charge du Markdown n'est pas prévue.
Pour l'instant, nous voulons un système de commentaires volontairement simple.
Note
Je souhaite garder le code simple pour deux raisons. D'abord, le processus et les décisions qui le sous-tendent sont plus importants que la quantité de code. Ensuite, des fonctionnalités plus avancées pourront être explorées dans une future série.
Construire le système de commentaires
Maintenant que les exigences sont claires, mettons-les en relation avec les concepts de notre application.
Un commentaire est un message laissé par un utilisateur sur un article. Dans notre système, le modèle possédera les propriétés suivantes :
id: L'identifiant unique du commentaire.post_id: L'identifiant de l'article auquel le commentaire appartient.user_id: L'identifiant de l'utilisateur qui a laissé le commentaire.content: Le contenu du commentaire.created_at: La date et l'heure de création du commentaire.updated_at: La date et l'heure de sa dernière modification.
user_id référence la table users, et le modèle User possédera une relation comments. Qu'en est-il de post_id ?
Pour post_id, nous avons besoin d'un modèle Post avec les propriétés suivantes :
id: L'identifiant unique de l'article.meta_id: L'identifiant unique provenant du front-end.title: Le titre de l'article.created_at: La date et l'heure de création de l'article.updated_at: La date et l'heure de sa dernière modification.
Le modèle Post possédera une relation comments, ce qui nous permettra d'associer plusieurs commentaires à chaque article. Nous parlerons de meta_id dans l'article suivant, vous pouvez donc ignorer son rôle pour le moment.
Les relations ressembleront à ceci :
Une fois les modèles et les relations définis, nous pouvons concevoir l'API qui permettra de récupérer, créer, modifier et supprimer les commentaires.
Commençons par lister les endpoints nécessaires :
- Lister tous les commentaires d'un article, quelle que soit leur origine
- Créer un commentaire avec un utilisateur authentifié
- Modifier un commentaire avec l'utilisateur qui l'a créé
- Supprimer un commentaire avec l'utilisateur qui l'a créé
Associons ensuite les endpoints aux routes :
GET /posts/{post}/comments: Lister tous les commentaires d'un articlePOST /posts/{post}/comments: Créer un commentairePUT /comments/{comment}: Modifier un commentaireDELETE /comments/{comment}: Supprimer un commentaire
L'association est simple. Pourquoi utiliser /comments/{comment} plutôt que /posts/{post}/comments/{comment} pour les deux dernières routes ? L'identifiant d'un commentaire est unique, il permet donc de l'identifier sans répéter celui de l'article. Dans Laravel, on appelle cela le shallow nesting, ou imbrication superficielle.
Résumé avant d'écrire le code
Avant de commencer à coder, résumons les étapes d'implémentation :
- Créer un modèle
Postavec sa migration.
php artisan make:model Post -m -f- Créer un modèle
Commentavec sa migration.
php artisan make:model Comment -m -f- Créer un
CommentControllerdansapp/Http/Api.
php artisan make:controller Api/CommentController- Créer un
PostCommentControllerdansapp/Http/Api.
php artisan make:controller Api/PostCommentController- Ajouter les routes à
routes/api.php.
Nous ajouterons les tests au fur et à mesure.
Note
Je n'expliquerai pas chaque test en détail. Je me concentrerai sur les plus importants.
Prêt ? C'est parti !
Consulter et créer des commentaires
Créez le modèle Post avec Artisan :
php artisan make:model Post -m -fOuvrons maintenant le fichier de migration et ajoutons le code suivant :
return new class extends Migration
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->unsignedInteger('meta_id')->unique();
$table->timestamps();
});
};Mettez à jour la factory pour qu'elle corresponde à la migration :
class PostFactory extends Factory
{
public function definition(): array
{
return [
'title' => fake()->sentence(),
'meta_id' => fake()->unique()->numberBetween(1, 1000),
];
}
}Ajoutons un test pour vérifier que le modèle possède la forme attendue.
Commençons par créer le fichier de test :
mkdir -p tests/Unit/Models && touch tests/Unit/Models/PostTest.phpPuis ajoutons le code suivant :
<?php
declare(strict_types=1);
use App\Models\Post;
test('to array', function () {
$post = Post::factory()->create()->fresh();
expect(array_keys($post->toArray()))
->toEqual([
'id',
'title',
'meta_id',
'created_at',
'updated_at',
]);
});Ce test minimal vérifie les propriétés persistées du modèle. Il peut détecter la suppression ou l'ajout accidentel d'une colonne dans une migration. Nous ajouterons un test similaire pour chaque modèle créé.
Pour vérifier que le test fonctionne, le dossier tests/Unit doit avoir accès à une base de données fonctionnelle. Ajoutons Unit à la méthode in du fichier tests/Pest.php :
pest()->extend(Tests\TestCase::class)
->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
->in('Http', 'Unit');Créons le modèle Comment de la même manière :
php artisan make:model Comment -m -fMettez à jour sa migration :
use App\Models\Post;
use App\Models\User;
return new class extends Migration
{
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(Post::class)->constrained()->cascadeOnDelete();
$table->foreignIdFor(User::class)->constrained()->cascadeOnDelete();
$table->text('content');
$table->timestamps();
});
};Mettez à jour la factory pour qu'elle corresponde à la migration :
use App\Models\Post;
use App\Models\User;
// ...
class CommentFactory extends Factory
{
public function definition(): array
{
return [
'post_id' => Post::factory(),
'user_id' => User::factory(),
'content' => fake()->paragraph(),
];
}
}Ajoutez le test correspondant :
<?php
declare(strict_types=1);
use App\Models\Comment;
test('to array', function () {
$comment = Comment::factory()->create()->fresh();
expect(array_keys($comment->toArray()))
->toEqual([
'id',
'post_id',
'user_id',
'content',
'created_at',
'updated_at',
]);
});Ajoutons maintenant les relations. Un article possède plusieurs commentaires, un commentaire appartient à un article, un commentaire appartient à un utilisateur et un utilisateur possède plusieurs commentaires.
Note
Nous utilisons belongs to plutôt que has one, car un commentaire est un enfant d'un article et ne peut pas exister sans lui. Une relation has one décrit le modèle parent qui possède un enfant unique, comme un utilisateur qui possède un profil alors que le profil appartient à l'utilisateur.
Voici ces relations :
class Post extends Model
{
/**
* Get all of the comments for the Post
*
* @return HasMany<Comment, covariant Post>
*/
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
}class Comment extends Model
{
/**
* Get the post that owns the comment.
*
* @return BelongsTo<Post, covariant Comment>
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
/**
* Get the user that owns the comment.
*
* @return BelongsTo<User, covariant Comment>
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}class User extends Authenticatable
{
/**
* Get comments that belong to the user.
*
* @return HasMany<Comment, covariant User>
*/
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
}Ces relations simplifient les requêtes Eloquent. Pour récupérer tous les commentaires d'un article, nous pouvons écrire :
Post::find(1)->comments;Elles facilitent également l'utilisation de méthodes de factory comme has et for pour créer des modèles liés.
Ajoutons des tests de relations pour vérifier que les modèles sont reliés comme prévu.
Note
Lorsque je travaille sur des modèles, j'écris souvent les tests après la première implémentation, car la conception peut évoluer rapidement. Écrire le modèle m'aide à clarifier ce que les tests doivent vérifier.
use App\Models\Comment;
test('relations', function () {
$post = Post::factory()
->hasComments(1)
->create();
expect($post->comments)->each->toBeInstanceOf(Comment::class);
});use App\Models\Post;
use App\Models\User;
test('relations', function () {
$comment = Comment::factory()
->forPost()
->create();
expect($comment->post)->toBeInstanceOf(Post::class)
->and($comment->user)->toBeInstanceOf(User::class);
});<?php
declare(strict_types=1);
use App\Models\Comment;
use App\Models\User;
test('to array', function () {
$user = User::factory()->create()->fresh();
expect(array_keys($user->toArray()))
->toEqual([
'id',
'name',
'username',
'email',
'email_verified_at',
'github_id',
'avatar',
'created_at',
'updated_at',
]);
});
test('relations', function () {
$user = User::factory()
->hasComments(1)
->create();
expect($user->comments)->each->toBeInstanceOf(Comment::class);
});Nous n'avions pas encore ajouté de tests pour User, c'est donc une bonne occasion d'ajouter également son test de forme.
Le fonctionnement reste cohérent et Laravel rend chaque étape simple.
Créer les contrôleurs
Maintenant que les modèles sont prêts, nous pouvons créer les contrôleurs.
PostCommentController possédera deux méthodes :
index: Liste tous les commentaires d'un article avecGET /posts/{post}/comments.store: Crée un commentaire avecPOST /posts/{post}/comments.
Comme dans l'article précédent, commençons par écrire un test pour la méthode index :
<?php
declare(strict_types=1);
use App\Models\Comment;
use App\Models\Post;
use Illuminate\Testing\Fluent\AssertableJson;
it('returns comments', function () {
$post = Post::factory()->create();
$comments = Comment::factory()
->count(2)
->for($post)
->create();
$this->getJson(route('posts.comments', $post))
->assertJson(
fn (AssertableJson $json) => $json
->has(
'data',
2,
fn (AssertableJson $json) => $json
->where('id', $comments->first()->id)
->where('content', $comments->first()->content)
->has(
'author',
fn (AssertableJson $json) => $json
->where('name', $comments->first()->user->name)
->where('username', $comments->first()->user->username)
->where('avatar', $comments->first()->user->avatar)
)
)
);
});Pour les tests HTTP, j'utilise le pattern AAA :
- Arrange : préparer les données
- Act : appeler la méthode
- Assert : vérifier le résultat
Dans ce test, nous créons un article et deux commentaires associés. C'est l'étape Arrange. Nous appelons ensuite GET /posts/{post}/comments, l'étape Act. Enfin, nous vérifions que la réponse contient deux commentaires avec la structure attendue, l'étape Assert.
Le test en échec nous demande d'ajouter une route dans routes/api.php :
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\PostCommentController;
Route::get('/posts/{post}/comments', [PostCommentController::class, 'index'])
->name('posts.comments');Nous devons ensuite demander à Laravel de lire ce fichier en mettant à jour bootstrap/app.php :
<?php
declare(strict_types=1);
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)Enfin, créons le contrôleur avec une méthode index :
php artisan make:controller Api/PostCommentController<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class PostCommentController extends Controller
{
public function index(Request $request)
{
//
}
}La route existe maintenant, mais le test indique que la réponse n'est pas un JSON valide, car le contrôleur ne retourne rien. 😄
Utilisons cet échec comme prochaine étape d'implémentation :
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\CommentResource;
use App\Models\Comment;
use Illuminate\Http\Resources\Json\ResourceCollection;
final class PostCommentController extends Controller
{
public function index(): ResourceCollection
{
$comments = Comment::query()
->with('user')
->get();
return CommentResource::collection($comments);
}
}La méthode index récupère les commentaires, charge la relation utilisateur à l'avance et retourne une collection de ressources CommentResource.
Note
Récupérer tous les enregistrements n'est généralement pas une bonne pratique ; la pagination est plus sûre pour une grande collection. Dans cet exemple, nous nous attendons à un petit nombre de commentaires, le retour de la collection est donc acceptable. Nous pourrons ajouter la pagination si cette hypothèse change.
Créons maintenant la classe CommentResource :
php artisan make:resource CommentResource<?php
declare(strict_types=1);
namespace App\Http\Resources;
use App\Models\Comment;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin Comment
*/
final class CommentResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'content' => $this->content,
'author' => UserResource::make($this->whenLoaded('user')),
];
}
}Et une seconde ressource pour l'auteur :
php artisan make:resource UserResource<?php
declare(strict_types=1);
namespace App\Http\Resources;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin User
*/
final class UserResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'name' => $this->name,
'username' => $this->username,
'avatar' => $this->avatar,
];
}
}Que sont ces ressources ?
Une ressource est une couche de transformation entre nos modèles Eloquent et les réponses JSON envoyées au client. Elle définit comment un modèle est transformé en JSON.
Ici, nous retournons l'id, le content et l'author d'un commentaire. La ressource imbriquée de l'auteur n'expose que le name, le username et l'avatar de l'utilisateur. Cela permet de modifier la forme de la réponse sans toucher au contrôleur et évite d'exposer accidentellement des champs comme l'e-mail de l'utilisateur.
Le test devrait maintenant réussir. Mais ne nous précipitons pas : ajoutons un test pour vérifier que cette méthode ne retourne que les commentaires de l'article demandé :
it('returns comments for a post', function () {
Comment::factory()
->count(2)
->create();
$this->getJson(route('posts.comments', Post::factory()->create()))
->assertJson(
fn (AssertableJson $json) => $json
->has('data', 0)
);
});Ce test crée deux commentaires pour un article, appelle la route avec un autre article et attend une réponse vide.
Le test échoue, car le contrôleur ne filtre pas encore les commentaires par article.
Corrigeons la méthode index :
use App\Models\Post;
final class PostCommentController extends Controller
{
public function index(Post $post): ResourceCollection
{
$comments = Comment::query()
->whereBelongsTo($post)
->with('user')
->get();
return CommentResource::collection($comments);
}
}Le binding de modèle des routes de Laravel nous fournit directement l'instance Post. Comme la route contient {post} et que la méthode accepte un Post, Laravel recherche l'enregistrement correspondant et l'injecte. Le contrôleur reste ainsi concis.
Les tests devraient maintenant réussir. 👌
Passons à la méthode store.
Vous connaissez la méthode : commençons par écrire un test :
<?php
use App\Models\Post;
it('requires authentication', function () {
$post = Post::factory()->create();
$this->postJson(route('posts.comments.store', $post))
->assertUnauthorized();
});Ce test vérifie que l'authentification est requise. Ajoutez la route :
Route::middleware('auth')->group(function () {
Route::post('/posts/{post}/comments', [PostCommentController::class, 'store'])
->name('posts.comments.store');
});Continuons avec un autre test :
use App\Models\User;
// ...
it('stores a comment', function () {
$user = User::factory()->create();
$post = Post::factory()->create();
$this->actingAs($user)
->postJson(route('posts.comments.store', $post), [
'content' => 'This is a comment',
]);
$this->assertDatabaseHas('comments', [
'post_id' => $post->id,
'user_id' => $user->id,
'content' => 'This is a comment',
]);
});Ce test vérifie que le commentaire est enregistré avec le bon post_id, le bon user_id et le bon contenu. Le contrôleur définit les deux clés étrangères.
Nous pouvons maintenant implémenter la méthode store :
final class PostCommentController extends Controller
{
public function store(Request $request, Post $post)
{
Comment::create([
'post_id' => $post->id,
'user_id' => $request->user()->id,
'content' => $request->input('content')
]);
}
}Note
Nous écrivons uniquement le minimum nécessaire pour faire réussir les tests. Cela ne signifie pas que l'implémentation est terminée. Par exemple, la requête doit encore être validée, ce que nous ajouterons avec le prochain test.
Ajoutons un test pour la validation de la requête :
it('requires a valid data', function (array $badData, array|string $errors) {
$post = Post::factory()->create();
$response = $this->actingAs(User::factory()->create())
->postJson(route('posts.comments.store', $post), $badData);
$response->assertJsonValidationErrors($errors);
})->with([
[['content' => ''], ['content' => 'The content field is required.']],
[['content' => 1], ['content' => 'The content field must be a string.']],
[['content' => 'a'], ['content' => 'The content field must be at least 6 characters.']],
[['content' => 'a'.str_repeat('a', 2001)], ['content' => 'The content field must not be greater than 2000 characters.']],
]);Pest exécute ce test quatre fois avec des données différentes. Un dataset permet de couvrir plusieurs entrées invalides sans dupliquer le corps du test.
Créons une classe StoreCommentRequest pour contenir ces règles :
php artisan make:request StoreCommentRequest<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreCommentRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'content' => ['required', 'string', 'max:2000', 'min:6'],
];
}
}Mettez à jour store pour utiliser la nouvelle requête :
use App\Http\Requests\StoreCommentRequest;
use Illuminate\Http\Request;
final class PostCommentController extends Controller
{
public function store(StoreCommentRequest $request, Post $post)
{
// ...
}
}Laravel valide maintenant la requête avant d'exécuter le contrôleur. ✨
Ajoutons enfin un test pour vérifier que les données retournées sont correctes :
it('returns the created comment', function () {
$user = User::factory()->create();
$post = Post::factory()->create();
$comment = Comment::factory()->make();
$response = $this->actingAs($user)
->postJson(route('posts.comments.store', $post), [
'content' => $comment->content,
]);
$comment = Comment::first();
$response
->assertCreated()
->assertJson(
fn (AssertableJson $json) => $json
->has(
'data',
fn (AssertableJson $json) => $json
->where('id', $comment->id)
->where('content', $comment->content)
->has(
'author',
fn (AssertableJson $json) => $json
->where('name', $user->name)
->where('username', $user->username)
->where('avatar', $user->avatar)
)
)
);
});Ce test vérifie les données retournées et s'assure que le code HTTP est 201 Created grâce à assertCreated.
Mettons à jour la méthode store pour retourner le commentaire créé :
use Illuminate\Http\Resources\Json\JsonResource;
final class PostCommentController extends Controller
{
public function store(StoreCommentRequest $request, Post $post): JsonResource
{
$comment = Comment::create([
'post_id' => $post->id,
'user_id' => $request->user()->id,
'content' => $request->input('content')
]);
$comment->load('user');
return CommentResource::make($comment);
}
}Tous les tests devraient maintenant réussir.
Ces tests représentent les tests que vous pouvez écrire pour les autres contrôleurs.
Modifier et supprimer des commentaires
Nous pouvons maintenant implémenter la modification et la suppression des commentaires.
Note
Je vais avancer plus vite ici, car le workflow est maintenant familier. Les tests complets sont inclus afin que vous puissiez suivre chaque exigence.
Pour la modification, créons un nouveau fichier de test :
mkdir -p tests/Http/Api/Comment && touch tests/Http/Api/Comment/UpdateTest.phpAjoutons ensuite le test suivant :
<?php
declare(strict_types=1);
use App\Models\Comment;
use App\Models\User;
use Illuminate\Testing\Fluent\AssertableJson;
it('requires authentication', function () {
$comment = Comment::factory()->create();
$this->putJson(route('comments.update', $comment))
->assertUnauthorized();
});
it('requires the user to be the author of the comment', function () {
$user = User::factory()->create();
$comment = Comment::factory()->create();
$this->actingAs($user)
->putJson(route('comments.update', $comment), [
'content' => 'Updated content',
])
->assertForbidden();
});
it('updates a comment', function () {
$user = User::factory()->create();
$comment = Comment::factory()
->for($user)
->create();
$this->actingAs($user)
->putJson(route('comments.update', $comment), [
'content' => 'Updated content',
]);
$this->assertDatabaseHas('comments', [
'id' => $comment->id,
'content' => 'Updated content',
]);
});
it('requires a valid data', function (array $badData, array|string $errors) {
$user = User::factory()->create();
$comment = Comment::factory()->for($user)->create();
$this->actingAs($user)
->putJson(route('comments.update', $comment), $badData)
->assertJsonValidationErrors($errors);
})->with([
[['content' => ''], ['content' => 'The content field is required.']],
[['content' => 1], ['content' => 'The content field must be a string.']],
[['content' => 'a'], ['content' => 'The content field must be at least 6 characters.']],
[['content' => 'a' . str_repeat('a', 2001)], ['content' => 'The content field must not be greater than 2000 characters.']],
]);
it('returns the comment', function () {
$user = User::factory()->create();
$comment = Comment::factory()->for($user)->create();
$this->actingAs($user)
->putJson(route('comments.update', $comment), [
'content' => 'Updated content',
])
->assertOk()
->assertJson(
fn(AssertableJson $json) => $json
->has(
'data',
fn(AssertableJson $json) => $json
->where('id', $comment->id)
->where('content', 'Updated content')
->has(
'author',
fn(AssertableJson $json) => $json
->where('name', $user->name)
->where('username', $user->username)
->where('avatar', $user->avatar)
)
)
);
});Ajoutons ensuite la route :
<?php
use App\Http\Controllers\Api\CommentController;
Route::middleware('auth')->group(function () {
Route::put('/comments/{comment}', [CommentController::class, 'update'])
->name('comments.update');
});Créons le contrôleur :
php artisan make:controller Api/CommentControllerImplémentons la méthode update :
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\UpdateCommentRequest;
use App\Http\Resources\CommentResource;
use App\Models\Comment;
use Illuminate\Http\Resources\Json\JsonResource;
final class CommentController extends Controller
{
public function update(UpdateCommentRequest $request, Comment $comment): JsonResource
{
$comment->update($request->validated());
$comment->load('user');
return CommentResource::make($comment);
}
}Cette méthode a besoin d'une form request dédiée :
php artisan make:request UpdateCommentRequestImplémentons cette requête :
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use App\Models\Comment;
use Illuminate\Foundation\Http\FormRequest;
/**
* @property Comment $comment
*/
final class UpdateCommentRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return $this->user()->can('update', $this->comment);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'content' => ['required', 'string', 'max:2000', 'min:6'],
];
}
}La méthode authorize de la requête vérifie si l'utilisateur courant peut modifier le commentaire. Elle délègue cette décision à une policy, que nous devons créer.
php artisan make:policy CommentPolicyImplémentons ensuite la policy :
<?php
namespace App\Policies;
use App\Models\Comment;
use App\Models\User;
class CommentPolicy
{
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Comment $comment): bool
{
return $user->id === $comment->user_id;
}
}Le flux de modification est maintenant terminé.
Terminons l'article en ajoutant la suppression d'un commentaire :
<?php
declare(strict_types=1);
use App\Models\Comment;
use App\Models\User;
it('requires authentification', function () {
$comment = Comment::factory()->create();
$this->deleteJson(route('comments.destroy', $comment))
->assertUnauthorized();
});
it('requires the user to be the author of the comment', function () {
$user = User::factory()->create();
$comment = Comment::factory()->create();
$this->actingAs($user)
->deleteJson(route('comments.destroy', $comment))
->assertForbidden();
});
it('deletes a comment', function () {
$user = User::factory()->create();
$comment = Comment::factory()->for($user)->create();
$this->actingAs($user)
->deleteJson(route('comments.destroy', $comment));
$this->assertDatabaseMissing('comments', [
'id' => $comment->id,
]);
});<?php
// ...
Route::middleware('auth')->group(function () {
// ...
Route::delete('/comments/{comment}', [CommentController::class, 'destroy'])
->name('comments.destroy');
});use Illuminate\Http\Response;
use Illuminate\Support\Facades\Gate;
final class CommentController extends Controller
{
// ...
public function destroy(Comment $comment): Response
{
Gate::authorize('delete', $comment);
$comment->delete();
return response()->noContent();
}
}La façade Gate autorise la suppression. Laravel résout CommentPolicy à partir du modèle transmis à authorize.
final class CommentPolicy
{
// ...
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Comment $comment): bool
{
return $user->id === $comment->user_id;
}
}Le système de commentaires est prêt et les tests nous donnent confiance dans ses principaux comportements sans nécessiter de tout vérifier manuellement.
Et maintenant ?
L'API de commentaires est maintenant en place. Elle permet de lister, créer, modifier et supprimer des commentaires, tandis que les tests décrivent les comportements et les permissions qui protègent ces opérations.
Le développement piloté par les tests a permis de rester concentré : chaque échec indiquait le comportement à ajouter, et les tests terminés nous protègent désormais contre les régressions.
Ensuite, nous connecterons le front-end statique au back-end. Nous générerons les métadonnées des articles pendant le build VitePress et utiliserons Laravel pour synchroniser automatiquement la table posts.
Merci de me lire ! Je m'appelle Estéban, et j'adore écrire sur le développement web et le parcours humain qui l'entoure.
Je code depuis plusieurs années maintenant, et j'apprends encore de nouvelles choses chaque jour. J'aime partager mes connaissances avec les autres, car j'aurais aimé avoir accès à des ressources aussi claires et complètes lorsque j'ai commencé à apprendre la programmation.
Si vous avez des questions ou souhaitez discuter, n'hésitez pas à commenter ci-dessous ou à me contacter sur Bluesky, X, et LinkedIn.
J'espère que vous avez apprécié cet article et appris quelque chose de nouveau. N'hésitez pas à le partager avec vos amis ou sur les réseaux sociaux, et laissez un commentaire ou une réaction ci-dessous, cela me ferait très plaisir ! Si vous souhaitez soutenir mon travail, vous pouvez me sponsoriser sur GitHub !
Discussions
Ajouter un commentaire
Vous devez être connecté pour accéder à cette fonctionnalité.