Le refuge : exercices
Bloc Laravel · séance 9 · ~3 h
Trois refuges du Brabant wallon, à Wavre, Rixensart et Ottignies, publient sur un même site les animaux qu'ils proposent à l'adoption. Les bénévoles y ajoutent un animal, corrigent sa fiche, cochent ses traits de caractère, le marquent adopté et suppriment une fiche créée par erreur. Le site n'a pas de compte : l'authentification arrive au cours suivant. Dans ce chapitre, vous écrivez ce site en huit katas, avec ce que la friterie vous a appris aux chapitres 15 et 16.
À la fin de ce chapitre, vous serez capables de :
- écrire les routes, les contrôleurs et les vues d'un site, à partir d'un tableau de routes imposées,
- lire un test rouge, et trouver ce qui manque dans votre code,
- valider un formulaire, et afficher ses erreurs en français sous chaque champ,
- enregistrer des cases à cocher, une adoption et une suppression depuis des formulaires,
- vérifier la mise en forme de chaque page avec une liste de critères.
1. Le refuge
Installer le dépôt (10 minutes, une seule fois)
Sur github.com/opmvpc/refuge-26, créez votre copie avec le bouton Fork. Ouvrez ensuite un terminal dans le dossier de Herd ou de Laragon, et préparez le projet :
git clone https://github.com/VOTRE-COMPTE/refuge-26.git
cd refuge-26
composer install
cp .env.example .env
php artisan key:generate
php artisan migrate --seed
npm install
composer run dev
Ce sont les commandes de la friterie, au chapitre 15. Si migrate propose de créer database/database.sqlite, appuyez sur Entrée pour répondre yes. La base reçoit trois refuges, huit traits de caractère et douze animaux, dont deux adoptés, avec des dates fixes : vos pages ressemblent aux captures. Laissez composer run dev tourner. Avec Laragon, rechargez d'abord Laragon depuis son menu, puis ouvrez http://refuge-26.test. Au départ, seule la page /composants s'affiche, car les autres routes sont à écrire.
Ce que le dépôt contient
| Déjà dans le dépôt | Ce que vous écrivez |
|---|---|
les migrations des tables shelters, tags, animals et animal_tag |
les routes de routes/web.php, sauf la redirection de / vers /animaux et la route styleguide |
l'enum App\Enums\Species et les modèles Shelter, Animal et Tag, avec leurs relations, #[Fillable] et casts() |
AnimalController et ShelterController |
| les fabriques, et les seeders des trois refuges, des huit traits et des douze animaux | les vues animals/index, show, create, edit et shelters/show |
le layout vert mousse <x-layouts.app>, les composants <x-button>, <x-badge>, <x-card> et <x-nav-link>, la page /composants |
les règles de validation, dans validate() ou dans une Form Request AnimalRequest |
les composants de champ de la friterie : <x-label>, <x-input>, <x-select> et <x-textarea> |
les traductions françaises, avec le paquet Laravel-Lang |
les pictogrammes public/images/species/chien.svg, chat.svg et lapin.svg |
l'affichage du message flash dans le layout |
les tests des huit katas dans tests/Feature/, et la CI dans .github/workflows/tests.yml |
La page /composants montre chaque composant dans ses variantes, avec ses props. Gardez-la ouverte : <x-button> y accepte une prop href, qui en fait un lien en forme de bouton, et la prop variant de <x-badge> vaut adopted, species ou tag.
Le schéma
class Shelter {
+name: string
+city: string
+animals: Animal [0..*]
}
class Animal {
+name: string
+species: Species
+birth_date: date [0..1]
+description: string [0..1]
+adopted_at: date [0..1]
+shelter: Shelter [1]
+tags: Tag [0..*]
+age(): int [0..1]
+isAdopted(): bool
}
class Tag {
+name: string
+animals: Animal [0..*]
}
enum Species <<enumeration>> {
Dog
Cat
Rabbit
}
Shelter "1 +shelter" *-- "0..* +animals" Animal : héberge >
Animal "0..* +animals" -- "0..* +tags" Tag : présente >
Le losange plein {uml:composition} correspond à la clé étrangère shelter_id, qui supprime en cascade les animaux d'un refuge. La table pivot animal_tag relie les animaux et leurs traits, comme allergen_product à la friterie. La base enregistre l'espèce en texte, chien, chat ou lapin. $animal->species->label() affiche « Chien », et $animal->species->image() donne le chemin du pictogramme.
Le modèle des traits de caractère s'appelle Tag, et non Trait. trait est le mot-clé des traits du chapitre 7 de la POO, et class Trait {} s'arrête sur Parse error: syntax error, unexpected token "trait", expecting identifier. Dans les textes affichés, écrivez « trait de caractère ».
Le menu écrit en dur
Le layout écrit les adresses du menu à la main, sans route() :
<li><x-nav-link href="/animaux" :active="request()->is('animaux')">Les animaux</x-nav-link></li>
<li><x-nav-link href="/animaux/nouveau" :active="request()->is('animaux/nouveau')">Ajouter un animal</x-nav-link></li>
Tant que la route animals.index n'existe pas, route('animals.index') lève une exception. Écrite dans le layout, elle ferait planter toutes les pages, /composants compris. request()->is('animaux') compare l'adresse de la page ouverte, là où request()->routeIs() du chapitre 13, section 6, compare le nom de la route.
2. Les routes imposées
Les tests envoient des requêtes sur ces adresses, avec ces verbes, et vérifient ces noms. Dans routes/web.php, le paramètre s'écrit {animal} avec la liaison de modèle du chapitre 15, section 6, ou {id} avec findOrFail(), comme au chapitre 10. Déclarez /animaux/nouveau avant /animaux/{animal}, ou posez whereNumber sur le paramètre, pour éviter le piège de l'ordre du chapitre 7.
Ce tableau décrit le site terminé. Le kata 17.2 crée un animal sans validation, le kata 17.3 ajoute la validation, et le kata 17.5 les cases des traits. Lisez ce tableau avant le kata 17.1. Les deux suivants servent aux katas 17.3 et 17.5, qui y renvoient. Le dernier se relit à chaque kata.
| Verbe | Adresse | Nom | Méthode | Effet attendu |
|---|---|---|---|---|
GET |
/animaux |
animals.index |
AnimalController@index |
les animaux non adoptés, triés par nom ; bonus : ?espece=chat filtre, valeur inconnue ignorée |
GET |
/animaux/nouveau |
animals.create |
create |
le formulaire d'ajout, avec le <select> des refuges et les cases des traits |
POST |
/animaux |
animals.store |
store |
valide, crée, sync les traits, redirige vers la fiche, flash status = « L'animal a été ajouté. » |
GET |
/animaux/{id} |
animals.show |
show |
nom, espèce (libellé), refuge, âge, traits, badge « Adopté » le cas échéant, boutons Modifier, Adopter (si non adopté), Supprimer ; 404 sinon |
GET |
/animaux/{id}/modifier |
animals.edit |
edit |
le formulaire pré-rempli, traits pré-cochés, @method('PUT') |
PUT |
/animaux/{id} |
animals.update |
update |
valide, met à jour, sync, redirige vers la fiche, flash « L'animal a été modifié. » |
PATCH |
/animaux/{id}/adoption |
animals.adopt |
adopt |
pose adopted_at à aujourd'hui si nul (ne change rien sinon), redirige vers la fiche, flash « {nom} a trouvé une famille. » |
DELETE |
/animaux/{id} |
animals.destroy |
destroy |
supprime, redirige vers la liste, flash « L'animal a été supprimé. » |
GET |
/refuges/{id} |
shelters.show |
ShelterController@show |
bonus : le refuge, sa ville, ses animaux à adopter |
Les champs portent les mêmes noms à l'ajout et à la modification :
| Champ | Élément | Règles |
|---|---|---|
name |
<input type="text"> |
required, string, max:50 |
species |
<select> sur Species::cases(), value="{{ $species->value }}" |
required, Rule::enum(Species::class) |
shelter_id |
<select> sur les refuges |
required, integer, exists:shelters,id |
birth_date |
<input type="date"> |
nullable, date, before_or_equal:today |
description |
<textarea> |
nullable, string, max:1000 |
tags |
cases name="tags[]" value="{{ $tag->id }}" |
nullable, array ; tags.* : integer, exists:tags,id |
Dans les messages d'erreur, chaque champ porte un nom français :
| Champ | Nom dans les messages |
|---|---|
name |
nom |
species |
espèce |
shelter_id |
refuge |
birth_date |
date de naissance |
description |
description |
tags |
traits de caractère |
tags.* |
trait de caractère |
Les tests cherchent ces textes dans les pages ou dans la session :
| Texte | Quand |
|---|---|
| « L'animal a été ajouté. » | message flash, après l'ajout |
| « L'animal a été modifié. » | message flash, après la modification |
| « Rex a trouvé une famille. », avec le nom de l'animal adopté | message flash, après l'adoption |
| « L'animal a été supprimé. » | message flash, après la suppression |
| « Adopté » | badge, sur la fiche d'un animal adopté |
| « Aucun animal » | liste vide |
| « Chien », « Chat », « Lapin » | libellé de l'espèce, sur la fiche |
Dans le HTML des formulaires, les tests cherchent aussi method="POST" et l'attribut action écrit avec route(), entre guillemets doubles, comme dans les tableaux des katas. Le reste du HTML, les classes Tailwind, l'ordre des champs et les autres textes sont libres. Les critères de chaque kata décrivent la mise en forme attendue.
3. Afficher et ajouter
Les katas 17.1 à 17.3 affichent les animaux, puis ajoutent un animal avec un formulaire validé. Ils reprennent le chapitre 15 : gardez-le ouvert à côté.
| Kata | Groupe | À relire |
|---|---|---|
| 17.1 La liste et la fiche (20 minutes) | liste |
ProductController de la friterie (chapitre 15, section 1), la liaison de modèle (chapitre 15, section 6), la carte et sa grille (exercice 14.3) |
| 17.2 Ajouter un animal (25 minutes) | ajouter |
chapitre 15, sections 2 et 5 |
| 17.3 Valider le formulaire (25 minutes, installation comprise) | valider |
chapitre 15, sections 3 et 4 |
Lire un kata
Un kata est un groupe de tests Pest, dans un fichier de tests/Feature/. Les tests sont l'énoncé : chacun porte une phrase qui dit ce qu'il attend, et le tableau de chaque kata reprend ces phrases mot pour mot. Lancez le groupe avant d'écrire la moindre ligne :
php artisan test --group=liste
FAIL Tests\Feature\ListeTest
⨯ la route animals.index répond à GET /animaux 0.77s
⨯ la liste affiche les animaux à adopter, triés par nom 0.21s
⨯ la liste ne montre pas les animaux adoptés 0.04s
⨯ une liste vide affiche « Aucun animal » 0.03s
⨯ la fiche affiche le nom, l'espèce, le refuge et les traits de caractère 0.04s
⨯ la fiche d'un animal adopté affiche « Adopté » 0.03s
✓ la fiche d'un animal inconnu répond 404 0.03s
…
Sous la liste, Pest détaille chaque échec : la phrase, la raison, puis la ligne du test. Quatre raisons reviennent souvent :
FAILED Tests\Feature\ListeTest > la route animals.index répond à GET /animaux
La route animals.index n'existe pas.
Failed asserting that false is true.
La route manque, ou elle porte un autre nom. La première ligne vient du test, la seconde de Pest.
FAILED Tests\Feature\ListeTest > la liste affiche les animaux à adopter, triés par nom
Expected response status code [200] but received 404.
Failed asserting that 404 is identical to 200.
Aucune route ne répond à GET /animaux.
FAILED Tests\Feature\ModifierTest > envoyer le formulaire de mod… RouteNotFoundException
Route [animals.show] not defined.
Le test appelle route('animals.show') pour construire l'adresse de la redirection attendue. La route d'un kata précédent manque : écrivez d'abord celle du kata 17.1.
FAILED Tests\Feature\ValiderTest > les traductions françaises sont installées
Lancez composer require laravel-lang/common --dev, puis php artisan lang:add fr, et faites un commit de lang/.
Failed asserting that false is true.
Le message dit quoi faire. Pour le reste, ouvrez le fichier du test : il montre les données créées, l'adresse demandée et le texte cherché. Lancé depuis un assistant IA, php artisan test peut répondre en JSON, avec les mêmes phrases et les mêmes messages. Pour relancer un seul test, filtrez sur un morceau de sa phrase :
php artisan test --filter="triés par nom"
Six tests sur 41 sont verts dès le départ : les cinq « … répond 404 », et « adopter un animal déjà adopté ne change pas sa date d'adoption ». Ils doivent le rester quand vous écrivez les routes. Avant chaque commit, lancez php artisan test sans groupe : les katas 17.4 et 17.5 modifient store(), et peuvent faire repasser au rouge les groupes ajouter et valider. Un groupe vert valide ce que ses tests contrôlent. Les couleurs, les champs pré-remplis à l'écran et les boîtes de confirmation se vérifient dans le navigateur, avec les critères de chaque kata. Une largeur, le clavier et un attribut se vérifient comme au chapitre 14, section 1 : F12 puis Ctrl+Maj+M, la touche Tab, l'onglet Éléments.
Dès qu'un groupe est vert, faites un commit et poussez. La première fois, l'onglet Actions de votre fork demande d'activer les workflows, comme au chapitre 2 de la POO. Il affiche ensuite une coche par kata.
17.1 La liste et la fiche (20 minutes)
Créez AnimalController avec index() et show(), et les routes animals.index et animals.show. La liste ne montre que les animaux à adopter, triés par nom, dans une grille de cartes <x-card>. Chaque carte montre le pictogramme de l'espèce dans le slot image, avec asset($animal->species->image()), comme la couverture des jeux à l'exercice 14.3. Viennent ensuite le nom, en lien vers la fiche, puis le refuge. La fiche affiche le nom, l'espèce, le refuge, l'âge, les traits et la description. $animal->age() renvoie null quand la date de naissance est inconnue. Relisez ProductController de la friterie, et le chapitre 15, section 6.
Écrivez le nom du refuge sans lien pour l'instant. route('shelters.show', …) ferait planter la page tant que la route du bonus 17.8 n'existe pas, comme le menu écrit en dur. Les captures viennent de la solution, qui contient ce bonus.
php artisan make:controller AnimalController
php artisan test --group=liste
| Test | Ce que vous écrivez |
|---|---|
| la route animals.index répond à GET /animaux | Route::get('/animaux', …)->name('animals.index') |
| la liste affiche les animaux à adopter, triés par nom | orderBy('name') dans index() |
| la liste ne montre pas les animaux adoptés | whereNull('adopted_at'), qui ne garde que les lignes où la colonne est vide |
| une liste vide affiche « Aucun animal » | @forelse avec @empty, ou @if ($animals->isEmpty()) |
| la fiche affiche le nom, l'espèce, le refuge et les traits de caractère | $animal->species->label(), $animal->shelter->name et un @foreach sur $animal->tags |
| la fiche d'un animal adopté affiche « Adopté » | @if ($animal->isAdopted()) autour de <x-badge variant="adopted">Adopté</x-badge> |
| la fiche d'un animal inconnu répond 404 | findOrFail(), ou la liaison de modèle |

La rangée de filtres, au-dessus de la grille, vient du bonus 17.8.
Critères
- La liste affiche une carte par ligne à 375 pixels, deux à partir de 640, et trois à partir de 1 024.
- Dans une même ligne, les cartes ont la même hauteur, comme au catalogue du chapitre 14.
- Sur la fiche d'Oscar,
/animaux/5, absent de la liste parce qu'il est adopté, « Adopté » est un<x-badge variant="adopted">vert, à côté du badge de l'espèce. - Les traits de la fiche sont des badges
variant="tag", qui passent à la ligne quand la place manque.
17.2 Ajouter un animal (25 minutes)
Ajoutez create() et store(), et les routes animals.create et animals.store. La vue animals/create.blade.php envoie en POST cinq champs : name, species, shelter_id, birth_date et description. Les traits arrivent au kata 17.5. Écrivez les champs avec les composants du dépôt, comme le formulaire de la friterie au chapitre 15, section 2. Pour l'instant, store() ne valide rien : il crée l'animal avec $request->only([...]), puis redirige vers sa fiche avec un message flash. Affichez ce message dans le layout, comme au chapitre 15, section 5, à l'endroit marqué par un commentaire.
php artisan test --group=ajouter
| Test | Ce que vous écrivez |
|---|---|
| GET /animaux/nouveau affiche un formulaire qui envoie en POST vers /animaux | <form method="POST" action="{{ route('animals.store') }}">, @csrf, les cinq champs, et les refuges passés par create() |
| POST /animaux enregistre l'animal et redirige vers sa fiche avec un message | Animal::create(...), puis redirect()->route('animals.show', $animal->id)->with('status', …) |
| le message de confirmation s'affiche sur la fiche après l'ajout | @session('status') dans le layout |
Le <select> des espèces fait une boucle sur App\Enums\Species::cases(), avec value="{{ $species->value }}" et le libellé {{ $species->label() }}.
Critères
- Le formulaire est centré dans le cadre d'une carte, comme celui de l'exercice 14.7, et chaque libellé est au-dessus de son champ.
- Après l'ajout, le message s'affiche au-dessus de la fiche, dans un encadré vert pâle au texte vert foncé.
- Dans l'onglet Éléments, le texte du message est dans une balise qui porte
role="status". - Rechargée avec F5, la fiche n'affiche plus le message.
17.3 Valider le formulaire (25 minutes, installation comprise)
Installez les traductions françaises, comme au chapitre 15, section 4 :
composer require laravel-lang/common --dev
php artisan lang:add fr
APP_LOCALE=fr est déjà dans .env.example et dans phpunit.xml. Enregistrez le dossier lang/ dans un commit : la CI ne lance pas lang:add.
Dans store(), remplacez $request->only() par $request->validate(), avec les règles de la section 2 sauf celles de tags, comme au chapitre 15, section 3. Rule::enum(Species::class) refuse toute valeur qui n'est pas celle d'un cas de l'enum. Traduisez les noms des champs dans le tableau attributes de lang/fr/validation.php. Sous chaque champ, affichez le message avec @error, et rendez la valeur tapée avec old().
php artisan test --group=valider
| Test | Ce que vous écrivez |
|---|---|
| les traductions françaises sont installées | composer require, lang:add fr, et le dossier lang/ enregistré dans un commit |
| un formulaire vide est refusé : erreurs sur name, species et shelter_id, rien en base | required sur les trois champs, et Animal::create($validated) |
| un nom de plus de 50 caractères est refusé | max:50 |
| une espèce inconnue est refusée | Rule::enum(Species::class), avec use Illuminate\Validation\Rule; et use App\Enums\Species; |
| un refuge qui n'existe pas est refusé | exists:shelters,id |
| une date de naissance dans le futur est refusée | before_or_equal:today |
| la date de naissance et la description sont facultatives | nullable sur les deux champs |
| le formulaire réaffiché garde les valeurs tapées et montre le message en français | :value="old('name')" dans le champ et @error('name') dessous, pour chaque champ |

Critères
- Un champ refusé a une bordure rouge, et son message rouge s'affiche juste en dessous.
- Chaque champ refusé porte
aria-invalid="true", et sonaria-describedbydésigne son propre message :aria-describedby="species-error"sur l'espèce, etid="species-error"sur le message de l'espèce. - Chaque message nomme le champ en français, comme « Le champ espèce est obligatoire. »
- Après un refus, les champs bien remplis gardent leur valeur, le refuge choisi compris.
- Aucun champ ne porte l'attribut
required: envoyé vide, le formulaire part au serveur, et trois messages s'affichent.
4. Modifier, adopter, supprimer
Les katas 17.4 à 17.7 modifient un animal, cochent ses traits, l'adoptent et le suppriment. Ils reprennent le chapitre 16. Le bonus 17.8 se fait à la maison, après la séance.
| Kata | Groupe | À relire |
|---|---|---|
| 17.4 Modifier un animal (25 minutes) | modifier |
chapitre 16, sections 2 et 3, et section 5 pour messages() |
| 17.5 Les traits de caractère (25 minutes) | caracteres |
chapitre 16, section 5 |
| 17.6 Adopter (15 minutes) | adopter |
chapitre 16, section 6 |
| 17.7 Supprimer (15 minutes) | supprimer |
chapitre 16, section 7 |
| 17.8 Bonus : le refuge et le filtre (30 minutes, à la maison) | bonus |
le composant du chapitre 16, section 5, $request->query() du chapitre 8, section 4, tryFrom() du chapitre 7 de la POO, section 5 |
17.4 Modifier un animal (25 minutes)
Ajoutez edit() et update(), et les routes animals.edit et animals.update. La vue animals/edit.blade.php reprend le formulaire d'ajout, envoyé vers route('animals.update', $animal->id) avec @method('PUT'), comme au chapitre 16, section 2. Chaque champ est pré-rempli avec old() à deux arguments. Pour les deux listes, @selected() compare chaque option à old('species', $animal->species->value), puis à old('shelter_id', $animal->shelter_id). update() valide avec les règles de 17.3, puis redirige vers la fiche avec « L'animal a été modifié. ». Ajoutez enfin un bouton « Modifier » sur la fiche, avec la prop href de <x-button>.
Rangez de préférence les règles dans une Form Request AnimalRequest, que store() et update() partagent. Faites renvoyer true à authorize(), comme au chapitre 16, section 3 : sinon, chaque envoi répond 403. Les tests acceptent aussi deux validate(), mais le message « au today » reste alors en place.
La Form Request corrige ce message du kata 17.3. Sa méthode facultative messages(), citée au chapitre 16, section 5, remplace le message d'une règle pour un champ, sous la clé champ.règle :
/**
* Les messages qui remplacent ceux de lang/fr/validation.php.
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'birth_date.before_or_equal' => 'La date de naissance ne peut pas être dans le futur.',
];
}
Aucun test ne lit ce message : vérifiez-le dans le navigateur avec une date de l'an prochain. Les autres messages viennent toujours de lang/fr/validation.php, avec les noms que vous avez ajoutés à son tableau attributes au kata 17.3.
php artisan make:request AnimalRequest
php artisan test --group=modifier
| Test | Ce que vous écrivez |
|---|---|
| GET /animaux/{id}/modifier affiche le formulaire pré-rempli, qui envoie en PUT | @method('PUT'), et old('name', $animal->name) dans chaque champ |
| envoyer le formulaire de modification met l'animal à jour et redirige vers sa fiche | $animal->update($validated), puis la redirection avec le message |
| la modification est validée comme l'ajout | AnimalRequest, ou les règles de 17.3 dans update() |
| modifier un animal inconnu répond 404 | findOrFail(), ou la liaison de modèle, dans edit() et dans update() |
Critères
- Sur la fiche, « Modifier » est un lien en forme de bouton secondaire, blanc bordé de gris.
- Le formulaire de modification a la mise en forme du formulaire d'ajout, et son titre nomme l'animal : « Modifier Rex ».
- La date de naissance de Rex s'affiche dans son champ.
- Avec une date de naissance de l'an prochain, le formulaire affiche « La date de naissance ne peut pas être dans le futur. ».
17.5 Les traits de caractère (25 minutes)
Dans les deux formulaires, ajoutez une case par trait de caractère, nommée tags[]. Pour ne pas écrire les cases deux fois, rangez tous les champs dans un composant <x-animal-fields :animal="$animal" :shelters="$shelters" :tags="$tags" />, comme au chapitre 16, section 5. create() lui passe alors new Animal, dont l'espèce vaut null. $animal->species->value s'arrête sur Attempt to read property "value" on null : écrivez $animal->species?->value. L'opérateur ?-> du chapitre 5 de la POO renvoie null au lieu de lire value quand l'espèce est absente.
Au formulaire de modification, les traits de l'animal sont pré-cochés avec @checked, comme les allergènes au chapitre 16, section 5. Ajoutez les règles tags et tags.* du tableau de la section 2, et enregistrez les traits cochés avec sync() dans store() et update(). Dans le tableau attributes de lang/fr/validation.php, remplacez « mots clés » par « traits de caractère », et ajoutez 'tags.*' => 'trait de caractère'.
php artisan make:component animal-fields --view
php artisan test --group=caracteres
| Test | Ce que vous écrivez |
|---|---|
| le formulaire d'ajout propose une case par trait de caractère | les traits passés par create() et edit(), une case name="tags[]" value="{{ $tag->id }}" par trait |
| à l'ajout, les traits cochés sont enregistrés dans animal_tag | $animal->tags()->sync(...) dans store() |
| le formulaire de modification pré-coche les traits de l'animal | @checked(in_array($tag->id, $checkedTags)) sur chaque case |
| à la modification, la liste des traits est remplacée | sync() dans update() |
| décocher toutes les cases retire tous les traits | sync($validated['tags'] ?? []) |
| un trait qui n'existe pas est refusé | 'tags.*' => ['integer', 'exists:tags,id'] |
Critères
- Les huit cases s'affichent sur deux colonnes à 375 pixels, et sur quatre à partir de 640.
- Les cases sont regroupées dans un
<fieldset>, dont la<legend>dit « Traits de caractère ». - Un clic sur le nom d'un trait coche ou décoche sa case.
- Après un refus sur le nom, les cases gardent l'état choisi avant l'envoi, toutes décochées comprises.
17.6 Adopter (15 minutes)
Ajoutez la route animals.adopt et la méthode adopt(). Sur la fiche d'un animal à adopter, un formulaire d'un bouton « Adopter » envoie la requête avec @method('PATCH'), comme le bouton « Marquer en rupture » de la friterie au chapitre 16, section 6. adopt() pose adopted_at à today() si l'animal n'est pas encore adopté, puis redirige vers la fiche avec « Rex a trouvé une famille. », écrit avec le nom de l'animal. today() renvoie la date du jour à minuit, comme now() au chapitre 16 mais sans l'heure.
php artisan test --group=adopter
| Test | Ce que vous écrivez |
|---|---|
| PATCH /animaux/{id}/adoption enregistre la date d'adoption et redirige vers la fiche | $animal->update(['adopted_at' => today()]), puis la redirection avec "{$animal->name} a trouvé une famille." |
| la fiche d'un animal à adopter contient le formulaire d'adoption, celle d'un animal adopté ne le contient plus | @if (! $animal->isAdopted()) autour du formulaire |
| un animal adopté disparaît de la liste | rien de plus : le whereNull('adopted_at') du kata 17.1 |
| adopter un animal déjà adopté ne change pas sa date d'adoption | un if (! $animal->isAdopted()) dans adopt() |
| adopter un animal inconnu répond 404 | findOrFail(), ou la liaison de modèle |
Critères
- « Adopter » est un bouton principal, vert, de type
submit. - Après l'adoption, le message s'affiche dans l'encadré vert du kata 17.2, et le badge remplace le bouton.
- L'animal adopté n'apparaît plus sur
/animaux.
17.7 Supprimer (15 minutes)
Ajoutez la route animals.destroy et la méthode destroy(). Sur la fiche, ajoutez un formulaire POST avec @csrf, @method('DELETE') et un bouton <x-button type="submit" variant="danger">. Il demande une confirmation avec onsubmit="return confirm('Supprimer cet animal ?')", comme au chapitre 16, section 7. <x-button> écrit type="button" par défaut : sans type="submit", le clic n'envoie rien. destroy() supprime l'animal, puis redirige vers la liste avec « L'animal a été supprimé. ».
php artisan test --group=supprimer
| Test | Ce que vous écrivez |
|---|---|
| la fiche contient un formulaire de suppression qui envoie en DELETE | <form method="POST" action="{{ route('animals.destroy', $animal->id) }}"> avec @method('DELETE') |
| DELETE /animaux/{id} supprime l'animal et redirige vers la liste | $animal->delete(), puis redirect()->route('animals.index')->with('status', …) |
| supprimer un animal supprime aussi ses lignes dans animal_tag | rien de plus : cascadeOnDelete() est dans la migration de animal_tag |
| supprimer un animal inconnu répond 404 | findOrFail(), ou la liaison de modèle |

Critères
- Les trois boutons de la fiche ont trois variantes : « Modifier » secondaire, « Adopter » principal, « Supprimer » rouge.
- Les trois boutons sont sur une ligne, et passent à la ligne quand la place manque.
- Un clic sur « Supprimer » ouvre une boîte de confirmation, et « Annuler » laisse l'animal en place.
- Un clic sur « Supprimer », puis sur OK, ramène à la liste sous « L'animal a été supprimé. ».
- Au clavier, chaque bouton reçoit un contour de sa couleur.
5. Bonus
17.8 Bonus : le refuge et le filtre (30 minutes, à la maison)
Créez ShelterController, la route shelters.show et la vue shelters/show.blade.php : le nom et la ville du refuge, puis ses animaux à adopter, dans la grille de la liste. Sur la fiche et sur les cartes, le nom du refuge devient un lien vers sa page.
Sur /animaux, ajoutez un filtre par espèce : trois liens vers route('animals.index', ['espece' => 'chat']) et ses voisines, qui ajoutent ?espece=chat à l'adresse. Dans index(), $request->query('espece', '') lit la valeur, ou une chaîne vide. Species::tryFrom(), vue au chapitre 7 de la POO, la change en cas de l'enum, ou renvoie null si elle est inconnue : ne filtrez que dans le premier cas. Pour aller plus loin, remplacez confirm() par une boîte <dialog>, ouverte par sa méthode showModal() et fermée par close(). Son script va dans resources/js/app.js, comme celui du menu à l'exercice 14.5.
php artisan make:controller ShelterController
php artisan test --group=bonus
| Test | Ce que vous écrivez |
|---|---|
| GET /refuges/{id} affiche le refuge, sa ville et ses animaux à adopter | $shelter->animals()->whereNull('adopted_at')->orderBy('name')->get() |
| GET /refuges/999 répond 404 | findOrFail(), ou la liaison de modèle |
| /animaux?espece=chat ne garde que les chats | where('species', $species) quand tryFrom() a trouvé un cas |
| une espèce inconnue dans le filtre est ignorée | aucun filtre quand tryFrom() renvoie null |
Critères
- Le lien de l'espèce choisie se distingue des autres, et porte
aria-current="page". - La page d'un refuge reprend les cartes de la liste sans les recopier, avec un composant
<x-animal-card :animal="$animal" />, comme<x-animal-fields>au kata 17.5. - La boîte
<dialog>a les couleurs du site, et se ferme avec Échap.
À retenir
- Les tests d'un kata sont son énoncé, et le fichier du test montre comment chaque phrase est vérifiée.
- Une route imposée fixe le verbe, l'adresse et le nom, et laisse libres le contrôleur et la vue.
- Le message flash s'affiche dans le layout, sur la page où mène la redirection.
- Un navigateur n'envoie pas une case décochée, et
sync()reçoit alors un tableau vide. - Tout ce qui modifie la base passe par un formulaire
POST, avec@method()pourPUT,PATCHouDELETE.