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

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)

  1. ✅ Migration avec tous les champs demandés
  2. ✅ Model Student avec casting JSON pour programs
  3. ✅ FormRequest avec validation complète et messages personnalisés
  4. ✅ Controller complet (CRUD + upload d’images + pagination + recherche)
  5. ✅ Routes RESTful avec middleware auth

Frontend (Vue 3 + TypeScript)

  1. ✅ Types TypeScript stricts pour Student
  2. ✅ 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
  3. ✅ Index.vue - Liste avec recherche, pagination et actions
  4. ✅ Create.vue - Page de création
  5. ✅ Edit.vue - Page d’édition
  6. ✅ 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.