Excellent ! Je viens de créer un CRUD complet et professionnel pour gérer les étudiants avec Laravel 12 + Inertia.js + Vue 3 + TypeScript + Wayfinder + Tailwind CSS v4.
🎯 Ce qui a été créé :
Backend (Laravel 12)
- ✅ Migration avec tous les champs demandés
- ✅ Model Student avec casting JSON pour
programs - ✅ FormRequest avec validation complète et messages personnalisés
- ✅ Controller complet (CRUD + upload d’images + pagination + recherche)
- ✅ Routes RESTful avec middleware auth
Frontend (Vue 3 + TypeScript)
- ✅ Types TypeScript stricts pour Student
- ✅ StudentForm.vue - Composant formulaire réutilisable avec :
- Champ texte (name, email)
- Radio buttons (sexe)
- Checkboxes (programmes)
- Upload d’image avec prévisualisation
- Select (pays, langue)
- Textarea avec compteur (bio)
- Gestion d’erreurs
- ✅ Index.vue - Liste avec recherche, pagination et actions
- ✅ Create.vue - Page de création
- ✅ Edit.vue - Page d’édition
- ✅ Show.vue - Page de détails
📦 Installation et Configuration
# 1. Créer le lien symbolique pour le storage
php artisan storage:link
# 2. Lancer la migration
php artisan migrate
# 3. Installer les dépendances frontend
npm install lucide-vue-next
# 4. Compiler les assets
npm run dev
🔧 Configuration Wayfinder
Créez le fichier resources/js/actions/App/Http/Controllers/StudentController.ts :
import { makeInertiaForm } from '@wayfinder/inertia';
export default {
store: makeInertiaForm({
url: () => route('students.store'),
method: 'post',
}),
update: makeInertiaForm({
url: ({ student }: { student: number }) => route('students.update', student),
method: 'put',
}),
};
🎨 Fonctionnalités Implémentées
✅ Validation côté serveur avec messages personnalisés ✅ Upload d’image avec prévisualisation en temps réel ✅ Recherche avec debounce ✅ Pagination Laravel native ✅ Gestion d’erreurs avec InputError ✅ Types TypeScript stricts ✅ Composants Shadcn/ui modernes ✅ Dark mode ready ✅ Responsive design ✅ Confirmation de suppression ✅ Messages flash de succès
🚀 Utilisation
# Accéder à la liste
https://votre-site.com/students
# Les routes disponibles :
GET /students # Liste
GET /students/create # Formulaire création
POST /students # Enregistrer
GET /students/{id} # Afficher
GET /students/{id}/edit # Formulaire édition
PUT /students/{id} # Mettre à jour
DELETE /students/{id} # Supprimer
💡 Points Clés du Code
1. Gestion des Programmes (Checkboxes)
<Checkbox
v-for="program in availablePrograms"
:name="`programs[]`"
:value="program.value"
@update:checked="toggleProgram(program.value)"
/>
2. Upload Image avec Prévisualisation
const handleImageChange = (event: Event) => {
const file = (event.target as HTMLInputElement).files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
profilePreview.value = e.target?.result as string;
};
reader.readAsDataURL(file);
}
};
3. Validation Laravel
'programs' => ['required', 'array', 'min:1'],
'programs.*' => [Rule::in(['php', 'js', 'html', 'typescript', 'vuejs', 'nuxtjs'])],
'profile' => ['nullable', 'image', 'max:2048'],
C’est production-ready et suit toutes les meilleures pratiques 2025 ! 🎉
📊 Différence et Meilleures Pratiques : through() vs Direct
🔍 Analyse Comparative
Méthode 1 : Avec through() (✅ Recommandée)
public function index(Request $request): Response
{
return Inertia::render('students/Index', [
'students' => Student::latest()->paginate(7)
->withQueryString()
->through(fn ($student) => [
'id' => $student->id,
'name' => $student->name,
'email' => $student->email,
'sexe' => $student->sexe,
'programs' => $student->programs,
'profile_url' => $student->profile_url,
'country' => $student->country,
'language' => $student->language,
'bio' => $student->bio,
]),
]);
}
Méthode 2 : Sans transformation (❌ Problématique)
public function index(Request $request): Response
{
return Inertia::render('students/Index', [
'students' => Student::latest()->paginate(7)
]);
}
⚖️ Comparaison Détaillée
| Critère | Avec through() |
Sans through() |
|---|---|---|
| Contrôle des données | ✅ Explicite | ❌ Tout exposé |
| Sécurité | ✅ Haute | ⚠️ Risque de fuite |
| Performance | ✅ Optimisée | ⚠️ Données inutiles |
| Maintenance | ✅ Facile | ❌ Difficile |
| API Resource | ✅ Respecte le pattern | ❌ Non conforme |
| TypeScript | ✅ Types clairs | ⚠️ Types any |
🚨 Problèmes de la Méthode 2 (Sans through())
1. Exposition de Données Sensibles
// ❌ Sans through() - Expose TOUT
Student::paginate(7);
// Renvoie au frontend :
{
id: 1,
name: "John Doe",
email: "john@example.com",
password: "$2y$10$...", // ⚠️ Hash exposé !
remember_token: "xyz123...", // ⚠️ Token exposé !
created_at: "2025-01-08 10:00:00",
updated_at: "2025-01-08 10:00:00",
deleted_at: null, // ⚠️ Soft delete exposé !
// ... tous les champs cachés
}
2. Surcharge de Données
// Frontend reçoit des données inutiles
- Timestamps détaillés (created_at, updated_at)
- Métadonnées Laravel (laravel_through_key)
- Relations non chargées mais présentes
- Attributs cachés mais sérialisés
3. Problèmes de Performance
// ❌ Sans through()
Student::with('orders', 'posts', 'comments')->paginate(7);
// Envoie TOUTES les relations au frontend = payload énorme !
// ✅ Avec through()
Student::paginate(7)->through(fn($s) => [
'id' => $s->id,
'name' => $s->name,
'orders_count' => $s->orders->count() // Seulement le compte
]);
✅ Avantages de through()
1. Contrôle Explicite des Données
// ✅ Vous choisissez EXACTEMENT ce qui est envoyé
->through(fn ($student) => [
'id' => $student->id,
'name' => $student->name,
'email' => $student->email,
// Uniquement ce dont le frontend a besoin
])
2. Transformation des Données
// ✅ Formater les données avant envoi
->through(fn ($student) => [
'id' => $student->id,
'name' => $student->name,
'initials' => $student->name[0], // Calcul côté serveur
'joined_date' => $student->created_at->format('d/m/Y'),
'is_active' => $student->last_login_at > now()->subDays(30),
'profile_url' => $student->profile
? asset('storage/' . $student->profile)
: null,
])
3. Accessors Optimisés
// Model
protected function fullName(): Attribute
{
return Attribute::make(
get: fn() => "{$this->first_name} {$this->last_name}"
);
}
// Controller
->through(fn ($student) => [
'full_name' => $student->full_name, // Appel explicite
])
4. Sécurité Renforcée
// ✅ Filtrage des champs sensibles
->through(fn ($student) => [
'id' => $student->id,
'name' => $student->name,
// Pas de : password, remember_token, api_token, etc.
])
🎯 Bonnes Pratiques Recommandées
Option 1 : through() avec Mapper (Simple)
public function index(Request $request): Response
{
$students = Student::query()
->when($request->search, fn($q, $search) =>
$q->where('name', 'like', "%{$search}%")
)
->latest()
->paginate(10)
->withQueryString()
->through(fn ($student) => [
'id' => $student->id,
'name' => $student->name,
'email' => $student->email,
'sexe' => $student->sexe,
'programs' => $student->programs,
'profile_url' => $student->profile_url,
'country' => $student->country,
'language' => $student->language,
'bio' => $student->bio,
]);
return Inertia::render('students/Index', [
'students' => $students,
'filters' => $request->only('search'),
]);
}
Option 2 : API Resource (Professionnel) ⭐
// app/Http/Resources/StudentResource.php
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class StudentResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'sexe' => $this->sexe,
'sexe_label' => match($this->sexe) {
'male' => 'Masculin',
'female' => 'Féminin',
default => 'Autre',
},
'programs' => $this->programs,
'profile_url' => $this->profile_url,
'country' => $this->country,
'language' => $this->language,
'bio' => $this->bio,
'created_at' => $this->created_at->format('d/m/Y'),
// Champs conditionnels
'can_edit' => $request->user()?->can('update', $this->resource),
'can_delete' => $request->user()?->can('delete', $this->resource),
];
}
}
// Controller
use App\Http\Resources\StudentResource;
public function index(Request $request): Response
{
return Inertia::render('students/Index', [
'students' => StudentResource::collection(
Student::latest()->paginate(10)
),
]);
}
Avantages des Resources :
- ✅ Réutilisable (API + Inertia)
- ✅ Testable unitairement
- ✅ Logique métier séparée
- ✅ Champs conditionnels faciles
- ✅ Transformation complexe possible
Option 3 : Data Transfer Object (Entreprise) 🏢
// app/Data/StudentData.php
use Spatie\LaravelData\Data;
class StudentData extends Data
{
public function __construct(
public int $id,
public string $name,
public string $email,
public string $sexe,
public array $programs,
public ?string $profile_url,
public string $country,
public string $language,
public ?string $bio,
) {}
}
// Controller
use App\Data\StudentData;
public function index(): Response
{
return Inertia::render('students/Index', [
'students' => Student::latest()
->paginate(10)
->through(fn($student) => StudentData::from($student)),
]);
}
📝 Cas d’Usage par Complexité
Petit Projet (1-5 devs)
// ✅ through() simple suffit
->through(fn($s) => ['id' => $s->id, 'name' => $s->name])
Projet Moyen (5-20 devs)
// ✅ API Resources
StudentResource::collection($students)
Grande Entreprise (20+ devs)
// ✅ DTOs avec validation
StudentData::collection($students)
🔐 Exemple : Fuite de Données Réelle
// ❌ DANGEREUX - Sans through()
public function index()
{
return Inertia::render('users/Index', [
'users' => User::paginate(10) // Expose password hash !
]);
}
// Console du navigateur :
{
id: 1,
email: "admin@example.com",
password: "$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi",
remember_token: "abc123..."
}
// ⚠️ Un attaquant peut extraire les hash et les cracker !
// ✅ SÉCURISÉ - Avec through()
public function index()
{
return Inertia::render('users/Index', [
'users' => User::paginate(10)->through(fn($u) => [
'id' => $u->id,
'email' => $u->email,
// Pas de password, remember_token
])
]);
}
🎓 Conclusion : Quelle Méthode Choisir ?
| Situation | Méthode Recommandée |
|---|---|
| Prototype rapide | through() simple |
| Application production | API Resource ⭐ |
| API publique | API Resource obligatoire |
| Microservices | DTO (Data Transfer Object) |
| JAMAIS | Direct sans transformation ❌ |
🚀 Recommandation Finale pour Votre Projet
// ✅ MEILLEURE PRATIQUE pour Laravel 12 + Inertia
public function index(Request $request): Response
{
$students = Student::query()
->when($request->search, function ($query, $search) {
$query->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
})
->latest()
->paginate(10)
->withQueryString()
->through(fn ($student) => [
'id' => $student->id,
'name' => $student->name,
'email' => $student->email,
'sexe' => $student->sexe,
'programs' => $student->programs,
'profile_url' => $student->profile_url,
'country' => $student->country,
'language' => $student->language,
'bio' => $student->bio,
]);
return Inertia::render('students/Index', [
'students' => $students,
'filters' => $request->only('search'),
]);
}
Pourquoi ?
- ✅ Contrôle total des données
- ✅ Sécurité maximale
- ✅ Performance optimisée
- ✅ Maintenable
- ✅ TypeScript-friendly
Utilisez toujours through() dans vos projets Inertia.js ! 🎯
Prêt à passer à la suite ?
Vous avez terminé cette leçon sur 1. Créer le lien symbolique pour le storage. Continuez votre apprentissage avec la prochaine leçon ou explorez d'autres formations.