CRUD Final 4 : Vue, Inertia & Laravel
Thème :
Intermédiaire
1 vues ·7/7 leçons

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 programs en 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 étudiants
  • create() : Affiche le formulaire de création
  • store() : Enregistre un nouvel étudiant
  • show() : Affiche un étudiant spécifique
  • edit() : Affiche le formulaire d’édition
  • update() : Met à jour un étudiant
  • destroy() : 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

  1. Laravel → Traite les requêtes, valide, interagit avec la DB
  2. Inertia → Transforme la réponse en composant Vue avec props
  3. Vue → Affiche l’interface et gère les interactions utilisateur
  4. 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 table students

  • Types de colonnes :

    • string, enum, json, text → selon le type de données.
  • timestamps() → ajoute created_at et updated_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 JSON programs en 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>

🧩

  • Form d’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”

  1. Un administrateur se connecte.
  2. Il clique sur “Créer un étudiant”.
  3. Le formulaire Vue s’affiche (Create.vue).
  4. Lorsqu’il soumet, Inertia envoie les données à Laravel via POST /students.
  5. Le controller valide, enregistre, puis redirige.
  6. Inertia recharge la page Index.vue avec 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'));
    
  • form est 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 profile s’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 :

  • StudentRequest valide les données (name, email, profile, etc.)
  • handleProfileUpload() stocke l’image dans storage/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é dans store et update.

👉 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.