Architecture Globale
Il s’agit d’une application Laravel 12 + Inertia.js + Vue 3 + TypeScript avec une architecture moderne qui combine le backend Laravel et le frontend Vue.js de manière seamless.
1. Laravel (Backend)
Migration - Structure de la base de données
Schema::create('students', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
// ... autres champs
});
Rôle : Définit la structure de la table students dans la base de données avec tous les champs nécessaires.
Model Student - Représentation des données
class Student extends Model
{
protected $fillable = ['name', 'email', 'sexe', /* ... */];
protected $casts = ['programs' => 'array'];
protected function profileUrl(): Attribute
{
return Attribute::make(get: fn () => $this->profile ? asset('storage/' . $this->profile) : null);
}
}
Rôle :
- Définit les champs modifiables (
fillable) - Cast le champ
programsen array automatiquement - Crée un accesseur pour l’URL complète de l’image
StudentRequest - Validation des données
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255', Rule::unique('students')->ignore($studentId)],
// ... autres règles
];
}
Rôle : Valide les données entrantes avec des règles spécifiques et des messages d’erreur personnalisés.
2. StudentController - Logique Métier
Méthodes CRUD :
index(): Liste paginée des étudiantscreate(): Affiche le formulaire de créationstore(): Enregistre un nouvel étudiantshow(): Affiche un étudiant spécifiqueedit(): Affiche le formulaire d’éditionupdate(): Met à jour un étudiantdestroy(): Supprime un étudiant
Particularité Inertia : Utilise Inertia::render() pour retourner des composants Vue au lieu de vues Blade.
3. Inertia.js - Bridge Backend/Frontend
Rôle Principal :
Crée une application monopage (SPA) sans avoir à construire une API REST séparée.
Fonctionnement :
// Backend (Laravel)
return Inertia::render('students/Index', [
'students' => Student::latest()->paginate(7)
]);
// Frontend (Vue) - Reçoit les props directement
const props = defineProps<{
students: PaginatedStudents;
}>();
Avantages :
- Pas besoin d’API REST
- Partage de données direct entre Laravel et Vue
- Routing côté Laravel
- Composants Vue côté frontend
⚡ 4. Vue 3 + Composition API
Structure des Composants :
<script setup lang="ts">
// Logique JavaScript/TypeScript
</script>
<template>
<!-- UI -->
</template>
<style scoped>
/* Styles */
</style>
Particularités :
<script setup>: Syntaxe simplifiée de Composition API- Props Typés : Interface TypeScript pour la validation
- Réactivité : Mise à jour automatique de l’UI
5. TypeScript - Typage Statique
Types Définis :
export type Sexe = 'male' | 'female' | 'other';
export type Language = 'fr' | 'en';
export type Program = 'php' | 'js' | 'html' | 'typescript' | 'vuejs' | 'nuxtjs';
export interface Student {
id: number;
name: string;
email: string;
sexe: Sexe;
programs: Program[];
// ...
}
Avantages :
- Validation à la compilation
- Auto-complétion intelligente
- Meilleure maintenabilité
- Documentation intégrée
6. Wayfinder (Routing)
Déclaration des Routes :
// routes/web.php
Route::resource('students', StudentController::class);
Utilisation dans Vue :
<Link :href="studentsRoute.index().url" class="btn-secondary">
Retour aux étudiants
</Link>
Rôle : Gère la navigation entre les pages sans rechargement complet.
7. Flow de Données
- Laravel → Traite les requêtes, valide, interagit avec la DB
- Inertia → Transforme la réponse en composant Vue avec props
- Vue → Affiche l’interface et gère les interactions utilisateur
- TypeScript → Assure la cohérence des types throughout
8. UI/UX Features
Dans les Composants :
- Formulaires : Validation en temps réel, erreurs affichées
- Tableau : Pagination, actions inline, design responsive
- Upload d’images : Gestion des fichiers avec preview
- Feedback utilisateur : Messages de succès/erreur
9. Sécurité
- Validation : Coté serveur (Laravel Request)
- CSRF Protection : Automatique avec Inertia
- Auth Middleware : Routes protégées
- Upload Sécurisé : Validation des images
10. Performance
- Pagination : Chargement partiel des données
- Assets Optimisés : Vite pour le build
- SPA : Navigation fluide sans rechargements
Cette architecture combine le meilleur de Laravel (backend solide) et de Vue (frontend réactif) grâce à Inertia.js, le tout renforcé par TypeScript pour une codebase robuste et maintenable.
Parfait 👍 Tu as un projet complet avec Laravel 12 + Inertia.js + Vue 3 + TypeScript, qui illustre très bien un CRUD moderne. Voici une explication claire et structurée, pour bien comprendre la logique de chaque couche :
🧩 Vue d’ensemble de l’architecture
Ton application suit le pattern MVC étendu :
[Client] Vue.js + TypeScript → Inertia.js → Laravel Controller → Model + Database
Chaque partie a un rôle bien défini :
| Couche | Rôle principal |
|---|---|
| Vue 3 + TypeScript | Interface utilisateur (composants dynamiques, formulaires, tableaux) |
| Inertia.js | Pont entre le front (Vue) et le back (Laravel) sans API REST |
| Laravel Controller | Logique métier : création, édition, suppression |
| Model (Eloquent) | Représente la table students et gère la persistance des données |
| Migration | Structure de la table dans la base de données |
⚙️ 1. Migration : structure de la base
Schema::create('students', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->enum('sexe', ['male', 'female', 'other']);
$table->json('programs')->nullable();
$table->string('profile')->nullable();
$table->string('country')->nullable();
$table->enum('language', ['fr', 'en'])->default('fr');
$table->text('bio')->nullable();
$table->timestamps();
});
🧠 Explication
-
Schema::create→ crée la tablestudents -
Types de colonnes :
string,enum,json,text→ selon le type de données.
-
timestamps()→ ajoutecreated_atetupdated_at.
💡 Objectif : définir la structure de ton modèle Student en base.
🧱 2. Model : app/Models/Student.php
Le modèle représente un enregistrement dans la table students.
Exemple :
class Student extends Model
{
protected $fillable = [
'name', 'email', 'sexe', 'programs', 'profile', 'country', 'language', 'bio',
];
protected $casts = [
'programs' => 'array',
];
protected function profileUrl(): Attribute
{
return Attribute::make(
get: fn () => $this->profile ? asset('storage/' . $this->profile) : null,
);
}
}
🧠 Explication
$fillable→ autorise le remplissage de ces champs via un formulaire.$casts→ transforme automatiquement la colonne JSONprogramsen tableau PHP.profileUrl()→ un accessor, pour générer l’URL complète d’une image stockée.
💡 Objectif : fournir une interface objet propre à la table SQL.
🎛️ 3. Controller : StudentController.php
C’est ici que vit la logique métier du CRUD.
a) Index : Liste des étudiants
public function index(Request $request): Response
{
return Inertia::render('students/Index', [
'students' => Student::latest()->paginate(7)
]);
}
🧩
- Récupère tous les étudiants triés du plus récent au plus ancien.
- Envoie les données vers Vue via
Inertia::render().
b) Create + Store : Créer un étudiant
public function create(): Response
{
return Inertia::render('students/Create');
}
public function store(StudentRequest $request)
{
$validated = $request->validated();
if ($request->hasFile('profile')) {
$validated['profile'] = $request->file('profile')->store('students/profiles', 'public');
}
Student::create($validated);
return redirect()->route('students.index')->with('success', 'Étudiant créé avec succès.');
}
🧩
create()→ renvoie la vue de création.store()→ valide, sauvegarde et redirige avec un message de succès.
c) Edit + Update : Modifier un étudiant
public function edit(Student $student): Response
{
return Inertia::render('students/Edit', ['student' => $student]);
}
public function update(StudentRequest $request, Student $student)
{
$validated = $request->validated();
if ($request->hasFile('profile')) {
Storage::disk('public')->delete($student->profile);
$validated['profile'] = $request->file('profile')->store('students/profiles', 'public');
}
$student->update($validated);
return redirect()->route('students.index')->with('success', 'Étudiant mis à jour avec succès.');
}
🧩
- Récupère l’étudiant existant, charge le formulaire, puis met à jour les données.
d) Destroy : Supprimer
public function destroy(Student $student)
{
if ($student->profile) {
Storage::disk('public')->delete($student->profile);
}
$student->delete();
return redirect()->route('students.index')->with('success', 'Étudiant supprimé avec succès.');
}
🧩 Supprime le fichier image et l’enregistrement.
🔗 4. Inertia.js : la passerelle
Inertia.js fait le lien entre Laravel et Vue. Au lieu d’échanger du JSON via une API, Inertia rend directement des composants Vue.
💡 Exemple :
return Inertia::render('students/Index', [
'students' => Student::paginate(7)
]);
➡️ Cela envoie les données au composant resources/js/Pages/students/Index.vue.
🖥️ 5. Vue 3 + TypeScript
Chaque page (Create, Edit, Index) est un composant Vue dans resources/js/Pages/students.
Exemple logique :
<Form v-bind="StudentController.store.form()" enctype="multipart/form-data">
<input id="name" name="name" type="text" />
<button type="submit">Créer l'étudiant</button>
</Form>
🧩
Formd’Inertia → gère les formulaires réactifs et les erreurs de validation.props.student→ récupère les données envoyées par Laravel via Inertia.- TypeScript → assure la sécurité des types (
Student,Program, etc.).
🧮 6. Typage TypeScript (resources/js/types/student.ts)
Les interfaces permettent à ton IDE (VS Code) de t’aider :
export interface Student {
id: number;
name: string;
email: string;
sexe: 'male' | 'female' | 'other';
programs: ('php' | 'js' | 'html' | 'typescript' | 'vuejs' | 'nuxtjs')[];
profile_url: string | null;
country: string;
language: 'fr' | 'en';
bio: string | null;
}
💡 Objectif : rendre le code Vue plus fiable et prévisible.
🧭 7. Exemple concret de cycle CRUD
💡 Cas réel : “Gestion d’école Devinsto”
- Un administrateur se connecte.
- Il clique sur “Créer un étudiant”.
- Le formulaire Vue s’affiche (
Create.vue). - Lorsqu’il soumet, Inertia envoie les données à Laravel via
POST /students. - Le controller valide, enregistre, puis redirige.
- Inertia recharge la page
Index.vueavec la liste mise à jour.
Aucune API REST, aucun JSON brut — tout se fait en navigation fluide côté client. ⚡
🚀 En résumé
| Couche | Rôle | Exemple |
|---|---|---|
| Migration | Structure SQL | students |
| Model | Objet métier | Student::create() |
| FormRequest | Validation | StudentRequest |
| Controller | Logique CRUD | store(), update() |
| Inertia | Pont backend ↔ frontend | Inertia::render() |
| Vue + TS | Interface et typage | Create.vue, Index.vue |
diagramme visuel du flux complet (du clic utilisateur jusqu’à la base de données) pour bien visualiser comment Inertia et Vue interagissent avec Laravel ? Excellent 👏 — oui, c’est une super idée, surtout pour bien comprendre comment ton stack VILT (Vue + Inertia + Laravel + TypeScript) fonctionne ensemble.
Voici donc un diagramme visuel clair et expliqué pas à pas, du clic utilisateur jusqu’à la base de données, et inversement.
🧭 Diagramme du flux complet : VILT (Vue + Inertia + Laravel + TypeScript)
[Utilisateur]
│
▼
[Interface Vue.js (Inertia Page)]
│
│ 1️⃣ L’utilisateur clique sur "Créer un étudiant" ou "Enregistrer"
▼
[Inertia.js envoie une requête AJAX (XHR/Fetch)]
│
│ 2️⃣ Requête POST / PUT / DELETE vers Laravel (via route Inertia)
▼
[Contrôleur Laravel (StudentController)]
│
│ 3️⃣ Valide les données (StudentRequest)
│ 4️⃣ Gère upload / suppression fichier (Storage)
│ 5️⃣ Crée ou met à jour Student (Eloquent ORM)
▼
[Base de données MySQL]
│
│ 6️⃣ Enregistre ou met à jour les données
▼
[Laravel renvoie une réponse Inertia]
│
│ 7️⃣ Retourne les données transformées via `Inertia::render()`
▼
[Inertia.js met à jour la page Vue sans rechargement complet]
│
│ 8️⃣ Vue met à jour son état réactif → interface rafraîchie
▼
[Utilisateur voit les nouvelles données immédiatement]
🧩 Étape par étape avec ton cas concret (StudentController)
1️⃣ Le clic utilisateur
-
Dans Vue (Inertia page
students/Create.vue), l’utilisateur clique sur “Enregistrer”. -
Un composant Inertia exécute :
form.post(route('students.store')); -
formest un objet Inertia qui gère automatiquement les requêtes AJAX.
2️⃣ Inertia envoie la requête
-
La requête est envoyée sans recharger la page, en AJAX, au backend Laravel :
POST /students Content-Type: multipart/form-data -
Elle contient :
- les champs du formulaire
- et le fichier
profiles’il existe.
3️⃣ Laravel reçoit la requête
Le contrôleur :
public function store(StudentRequest $request)
{
$validated = $request->validated();
$validated['profile'] = $this->handleProfileUpload($request);
Student::create($validated);
return redirect()->route('students.index')
->with('success', 'Étudiant créé avec succès.');
}
Ici :
StudentRequestvalide les données (name,email,profile, etc.)handleProfileUpload()stocke l’image dansstorage/app/public/students/profiles.
4️⃣ Écriture dans la base de données
Laravel Eloquent exécute automatiquement la requête SQL :
INSERT INTO students (name, email, profile, ...) VALUES (...);
5️⃣ Réponse Inertia
Laravel ne renvoie pas de JSON brut, mais une réponse Inertia :
return redirect()->route('students.index')
->with('success', 'Étudiant créé avec succès.');
Inertia intercepte cette réponse et recharge la page Vue students/Index.vue, en récupérant les nouvelles données via :
return Inertia::render('students/Index', [
'students' => Student::latest()->paginate(7)->through(fn($s) => $this->transformStudent($s))
]);
6️⃣ Vue met à jour l’interface sans rechargement complet
Grâce à Inertia, Vue ne recharge que les composants nécessaires. Tu gardes ton état, ton layout, etc.
Le tableau des étudiants est donc automatiquement mis à jour avec les nouvelles données.
💡 En résumé — le cycle Inertia
| Étape | Action | Côté |
|---|---|---|
| 1️⃣ | L’utilisateur interagit | Front (Vue) |
| 2️⃣ | Requête Inertia envoyée | Inertia.js |
| 3️⃣ | Contrôleur Laravel traite | Backend |
| 4️⃣ | Eloquent met à jour MySQL | Backend |
| 5️⃣ | Réponse Inertia avec vue mise à jour | Laravel |
| 6️⃣ | Vue réaffiche la page sans reload | Front |
🪄 Exemple concret (simplifié)
Dans Vue (Create.vue):
<script setup>
import { useForm } from '@inertiajs/vue3';
const form = useForm({
name: '',
email: '',
profile: null,
});
function submit() {
form.post(route('students.store'));
}
</script>
<template>
<form @submit.prevent="submit">
<input v-model="form.name" placeholder="Nom" />
<input v-model="form.email" placeholder="Email" />
<input type="file" @change="(e) => (form.profile = e.target.files[0])" />
<button type="submit">Enregistrer</button>
</form>
</template>
Excellent réflexe 👏
Ton StudentController est propre mais contient plusieurs redondances :
- Les mêmes champs du modèle sont répétés dans
index,show,edit. - Le traitement d’image (
profile) est dupliqué dansstoreetupdate.
👉 Voici une version factorisée, plus claire et plus maintenable, sans modifier le comportement.
✅ Version factorisée de ton StudentController
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Http\Requests\StudentRequest;
use App\Models\Student;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Inertia\Inertia;
use Inertia\Response;
class StudentController extends Controller
{
/**
* 🔧 Factorisation : transformation uniforme d’un étudiant en tableau
*/
private function transformStudent(Student $student): array
{
return [
'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,
'created_at' => optional($student->created_at)->format('d/m/Y'),
];
}
/**
* 🔧 Factorisation : gestion de l’upload de profil
*/
private function handleProfileUpload(Request $request, ?Student $student = null): ?string
{
if ($request->hasFile('profile')) {
if ($student?->profile) {
Storage::disk('public')->delete($student->profile);
}
return $request->file('profile')->store('students/profiles', 'public');
}
return $student?->profile;
}
// 🧾 Liste des étudiants
public function index(Request $request): Response
{
$students = Student::latest()
->paginate(7)
->withQueryString()
->through(fn (Student $student) => $this->transformStudent($student));
return Inertia::render('students/Index', compact('students'));
}
// ➕ Formulaire de création
public function create(): Response
{
return Inertia::render('students/Create');
}
// 💾 Enregistrer un étudiant
public function store(StudentRequest $request)
{
$validated = $request->validated();
$validated['profile'] = $this->handleProfileUpload($request);
Student::create($validated);
return redirect()->route('students.index')
->with('success', 'Étudiant créé avec succès.');
}
// 👀 Afficher un étudiant
public function show(Student $student): Response
{
return Inertia::render('students/Show', [
'student' => $this->transformStudent($student),
]);
}
// ✏️ Formulaire d’édition
public function edit(Student $student): Response
{
return Inertia::render('students/Edit', [
'student' => $this->transformStudent($student),
]);
}
// 🔁 Mettre à jour un étudiant
public function update(StudentRequest $request, Student $student)
{
$validated = $request->validated();
$validated['profile'] = $this->handleProfileUpload($request, $student);
$student->update($validated);
return redirect()->route('students.index')
->with('success', 'Étudiant mis à jour avec succès.');
}
// 🗑️ Supprimer un étudiant
public function destroy(Student $student)
{
if ($student->profile) {
Storage::disk('public')->delete($student->profile);
}
$student->delete();
return redirect()->route('students.index')
->with('success', 'Étudiant supprimé avec succès.');
}
}
💡 Explications de la factorisation
1. Méthode transformStudent()
➡️ Évite de répéter manuellement tous les champs dans :
index()show()edit()
Tu centralises la logique de transformation d’un modèle Eloquent en un tableau adapté pour Vue/Inertia.
2. Méthode handleProfileUpload()
➡️ Gère en une seule fonction :
- L’ajout d’une nouvelle image
- La suppression de l’ancienne image (dans
update) - La conservation du profil existant si aucun fichier n’est uploadé
3. Code principal simplifié
Le contrôleur devient :
- plus court
- plus expressif
- et surtout moins sujet aux oublis
🔍 Bonus — Exemple de résultat JSON
Après la pagination, chaque étudiant ressemble à ceci :
{
"id": 3,
"name": "Jean Dupont",
"email": "jean@example.com",
"sexe": "male",
"programs": ["php", "vuejs"],
"profile_url": "https://devinsto.com/storage/students/profiles/jean.jpg",
"country": "France",
"language": "fr",
"bio": "Étudiant en développement full-stack",
"created_at": "02/11/2025"
}
Souhaites-tu que je te montre la même factorisation côté Vue (dans Index.vue, Edit.vue, Create.vue) pour éviter la duplication des champs de formulaire et centraliser la logique dans un composable (useStudentForm.ts) ?
Prêt à passer à la suite ?
Vous avez terminé cette leçon sur Crud04 into. Continuez votre apprentissage avec la prochaine leçon ou explorez d'autres formations.