skilling.maSkill. Scale. Soar.
Partie 1 — Fondations Laravel Frameworks PHP MVC · Design Patterns
⏱ 16 min

Frameworks PHP & Architecture MVC

Pourquoi Laravel domine le marché PHP, comparaison avec Symfony et Zend, architecture 3-tiers MVC, Design Patterns fondamentaux (Repository, Singleton, Factory, Facade) et introduction aux API PHP.

✓ Ce que vous allez apprendre
  • Pourquoi utiliser un framework PHP plutôt que du PHP natif
  • Comparatif Laravel / Symfony / Zend / CodeIgniter sur 6 critères
  • Architecture 3-tiers MVC — Model, View, Controller et leurs responsabilités
  • Design Patterns essentiels utilisés par Laravel en interne
  • Présentation des API PHP et des CMS (WordPress, PrestaShop)

1
Pourquoi un framework PHP ?
PHP natif vs Framework · Productivité · Sécurité · Maintenabilité

En PHP natif, chaque développeur réinvente la roue : gestion des connexions BDD, sécurité CSRF, routage, validation — tout est à réécrire pour chaque projet. Une startup casablancaise qui lance une plateforme e-commerce avec PHP natif passera 60% de son temps à reconstruire de l'infrastructure plutôt qu'à créer de la valeur métier.

Un framework impose une structure et fournit des composants éprouvés, testés par des millions de projets. Résultat : une API complète CRUD en Laravel se construit en 20 minutes là où PHP natif en nécessiterait 3 heures.

CritèrePHP natifAvec un framework
Temps de démarrageLent — tout à construireRapide — structure prête
SécuritéResponsabilité du dev (oublis fréquents)CSRF, XSS, injection SQL gérés par défaut
MaintenanceCode souvent non structuréConventions partagées, lisible par tous
TestsDifficiles à mettre en placePHPUnit intégré, injection de dépendances
CommunautéDocumentation éparpilléeÉcosystème riche, packages Composer

2
Comparatif des frameworks PHP majeurs
Laravel · Symfony · Zend · CodeIgniter · Lumen
FrameworkPhilosophieCourbe apprentissageCas d'usage idéalTendance 2024
LaravelÉlégance & productivité — convention over configurationMoyenneStartups, SaaS, API REST, MVP rapideLeader absolu — 75 000+ GitHub stars
SymfonyComposants réutilisables, flexibilité maximaleÉlevéeApplications d'entreprise complexes, long termeFort en entreprise
Zend/LaminasArchitecture modulaire stricteTrès élevéeGrands projets bancaires, télécomsDéclin progressif
CodeIgniterLégèreté, simplicité, faible empreinteFaibleSites simples, hébergements mutualisésStable mais limité
LumenLaravel micro-frameworkFaibleMicroservices PHP, API haute performanceDéprécié — Laravel remplace
ℹ Pourquoi Laravel domine au Maroc

La communauté PHP marocaine a massivement adopté Laravel. Des entreprises comme Maroc Telecom, BMCE Bank, et de nombreuses startups à Casablanca Tech City et Technopark utilisent Laravel pour leurs back-ends. La documentation en français, les nombreux tutoriels arabophones et la simplicité de déploiement sur les hébergeurs marocains expliquent cette adoption.


3
Architecture 3-tiers et pattern MVC
Model · View · Controller · Séparation des responsabilités
ℹ MVC — définition et flux

MVC (Model-View-Controller) est un pattern architectural qui sépare une application en trois couches aux responsabilités distinctes. Chaque couche communique avec les autres selon des règles précises.

CoucheRôleDans LaravelExemple DariGo
ModelDonnées et logique métier — interaction avec la BDDClasses Eloquent dans app/Models/Restaurant.php, Commande.php, Livreur.php
ViewPrésentation — affichage des donnéesTemplates Blade dans resources/views/restaurants/index.blade.php
ControllerOrchestration — reçoit la requête, appelle le Model, retourne la ViewClasses dans app/Http/Controllers/RestaurantController.php
textFlux d'une requête Laravel — DariGo
Navigateur  →  routes/api.php  →  RestaurantController@index
                                         ↓
                                  Restaurant::all()  ⇄  MySQL
                                         ↓
                             return response()->json($restaurants)
                                         ↓
                                  JSON  →  App mobile DariGo

4
Design Patterns utilisés par Laravel
Repository · Singleton · Factory · Facade · Observer
PatternDéfinitionOù dans LaravelExemple concret DariGo
SingletonUne seule instance pour toute l'applicationService Container (app())Connexion BDD — partagée partout
FactoryCréer des objets sans spécifier leur classeModel::factory()Restaurant::factory(10)->create() en tests
RepositoryAbstraction de la couche d'accès aux donnéesEloquent ORM masque SQL$restaurant->commandes()->where('statut','livree')->get()
FacadeInterface statique vers un objet du containerCache::, Storage::, Mail::Cache::remember('restaurants', 60, fn() => ...)
ObserverRéagir à des événements sur un modèleEvents/Listeners, Model ObserversNotifier le livreur quand une commande est créée

✓
Vérification des acquis
Une startup à Casablanca veut lancer une API REST consommée par une app mobile en 3 semaines. Quel choix est le plus pertinent ?
Dans le pattern MVC, quelle couche est responsable de la logique d'affichage HTML ?
Partie 1 — Fondations Laravel Composer Artisan · Structure
⏱ 20 min

Environnement Laravel & Premier projet

Installer Composer et Laravel, comprendre la structure des dossiers, maîtriser les commandes Artisan essentielles, configurer le fichier .env et lancer le serveur de développement.

✓ Ce que vous allez apprendre
  • Prérequis système : PHP 8.2+, Composer, MySQL 8, Node.js
  • Installer Laravel via Composer et via Laravel Installer
  • Structure complète des dossiers — rôle de chaque répertoire
  • Fichier .env — variables d'environnement et configuration BDD
  • Commandes Artisan essentielles pour le quotidien du développeur
  • Tinker — REPL interactif pour tester Eloquent en direct

1
Installation de Laravel
PHP 8.2+ · Composer · Laravel Installer · Herd
bashVérifier les prérequis
php --version        # PHP 8.2.x ou supérieur requis
composer --version   # Composer 2.x
mysql --version      # MySQL 8.0
node --version       # Node.js 18+ (pour Vite)
bashInstaller Laravel — 2 méthodes
# Méthode 1 — Via Composer (recommandée, universelle)
composer create-project laravel/laravel darigo-api

# Méthode 2 — Via Laravel Installer (plus rapide)
composer global require laravel/installer
laravel new darigo-api
ℹ Ce cours utilise Laravel 11

Laravel 11 (mars 2024) simplifie la structure : bootstrap/app.php unifié pour middleware et providers, moins de fichiers de config par défaut, support PHP 8.2+ uniquement. Pour vérifier : php artisan --version.


2
Structure complète des dossiers
app/ · routes/ · resources/ · database/ · public/
textArborescence darigo-api/
darigo-api/
├── app/
│   ├── Http/
│   │   ├── Controllers/    # Contrôleurs MVC
│   │   ├── Middleware/     # Middleware personnalisés
│   │   └── Requests/       # Form Requests (validation)
│   ├── Models/         # Modèles Eloquent
│   ├── Policies/       # Politiques d'autorisation
│   ├── Jobs/           # Tâches en arrière-plan (Queue)
│   ├── Mail/           # Classes de mail (Mailable)
│   └── Notifications/  # Notifications multi-canaux
├── bootstrap/app.php   # Point d'entrée Laravel 11 — Middleware, providers
├── config/             # Fichiers de configuration
├── database/
│   ├── migrations/     # Structure des tables SQL
│   ├── seeders/        # Données initiales
│   └── factories/      # Factories pour les tests
├── public/             # SEUL dossier exposé au web
│   └── index.php       # Point d'entrée HTTP — TOUT passe par ici
├── resources/
│   ├── views/          # Templates Blade (.blade.php)
│   └── lang/           # Traductions (fr/, ar/, en/)
├── routes/
│   ├── web.php         # Routes navigateur (session, CSRF)
│   └── api.php         # Routes API (stateless, préfixe /api)
├── storage/            # Logs, cache, fichiers uploadés
├── tests/              # Tests PHPUnit
├── vendor/             # Packages Composer — jamais versionné
├── .env                # Variables d'environnement — JAMAIS sur Git
└── artisan             # CLI Laravel
⚠ public/ est le seul dossier exposé au web

Sur un serveur de production, le VirtualHost Apache/Nginx doit pointer vers darigo-api/public/, pas vers darigo-api/. Si DocumentRoot pointe vers la racine, le fichier .env serait accessible — faille de sécurité critique.


3
Le fichier .env — configuration de l'environnement
APP_KEY · DB_CONNECTION · CACHE_STORE · QUEUE_CONNECTION
ℹ Principe du .env

Le fichier .env contient toutes les variables qui changent entre les environnements (local, staging, production). Il ne doit jamais être versionné sur Git. Chaque développeur a son propre .env local.

bash.env — configuration complète DariGo
APP_NAME="DariGo API"
APP_ENV=local
APP_KEY=              # Généré par : php artisan key:generate
APP_DEBUG=true        # false en production OBLIGATOIRE
APP_URL=http://localhost:8000

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=darigo_db
DB_USERNAME=root
DB_PASSWORD=

CACHE_STORE=file      # file (dev) | redis (prod)
QUEUE_CONNECTION=sync # sync (dev) | database | redis (prod)
SESSION_DRIVER=database
SESSION_LIFETIME=120

MAIL_MAILER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_FROM_ADDRESS=noreply@darigo.ma
MAIL_FROM_NAME="DariGo"

REDIS_HOST=127.0.0.1
REDIS_PORT=6379
phpLire une variable .env — bonne pratique
// A EVITER dans les controllers
$appName = env('APP_NAME');         // Lent, ne fonctionne plus apres config:cache

// RECOMMANDE — via le cache de config
$appName = config('app.name');      // Toujours disponible, mis en cache

// Pourquoi config() ?
// Apres php artisan config:cache, env() retourne null
// config() lit le cache compile — rapide et fiable en production

4
PHP Artisan — 12 commandes essentielles
make: · migrate · serve · tinker · key:generate
CommandeActionRésultat
php artisan serveLancer le serveur de dévhttp://localhost:8000
php artisan key:generateGénérer APP_KEYClé d'encryption dans .env
php artisan make:model Restaurant -mModèle + migrationapp/Models/Restaurant.php + migration
php artisan make:controller RestaurantController --resourceContrôleur 7 méthodes CRUDapp/Http/Controllers/RestaurantController.php
php artisan make:request StoreRestaurantRequestForm Request validationapp/Http/Requests/StoreRestaurantRequest.php
php artisan migrateExécuter les migrationsTables créées en BDD
php artisan migrate:fresh --seedReset BDD + remigrer + seederBDD réinitialisée
php artisan tinkerREPL PHP interactifTest Eloquent en direct
php artisan route:listLister toutes les routesTableau méthode/URI/controller/middleware
php artisan config:cacheMettre en cache la configDémarrage accéléré en production
php artisan queue:workDémarrer le worker de queueTraitement des jobs en arrière-plan
php artisan testLancer tous les testsRapport PHPUnit
bashCréer le projet DariGo — séquence complète
# 1 — Créer le projet
composer create-project laravel/laravel darigo-api
cd darigo-api

# 2 — Générer la clé d'application
php artisan key:generate

# 3 — Créer la base de données MySQL
mysql -u root -p -e "CREATE DATABASE darigo_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"

# 4 — Configurer .env : DB_DATABASE=darigo_db, DB_USERNAME=root...

# 5 — Exécuter les migrations par défaut
php artisan migrate
# Tables créées : users, password_reset_tokens, sessions, cache, jobs, failed_jobs

# 6 — Lancer le serveur
php artisan serve
# http://localhost:8000 — page d'accueil Laravel

5
Tinker — REPL interactif Laravel
Test Eloquent en direct · Debug · Exploration
ℹ Tinker — l'outil de debug indispensable

Tinker permet d'exécuter du code PHP Laravel en temps réel dans le terminal, avec accès complet à Eloquent, aux Facades et au Service Container. Idéal pour tester une requête avant de l'écrire dans un Controller.

bashSession Tinker — exemples DariGo
php artisan tinker

>>> App\Models\User::count()
=> 0

>>> App\Models\User::factory()->create(['name' => 'Karim Alaoui', 'email' => 'karim@darigo.ma'])
=> App\Models\User { id: 1, name: "Karim Alaoui", email: "karim@darigo.ma" }

>>> App\Models\User::where('email', 'karim@darigo.ma')->first()->name
=> "Karim Alaoui"

>>> DB::table('users')->count()
=> 1

>>> cache()->put('darigo_test', 'fonctionne !', 60)
>>> cache()->get('darigo_test')
=> "fonctionne !"

>>> exit

✓
Vérification des acquis
Après php artisan config:cache, que se passe-t-il si on appelle env('APP_NAME') dans un Controller ?
Quel est le rôle du dossier public/ dans un projet Laravel ?
La commande php artisan migrate:fresh --seed est utilisée en production pour réinitialiser la BDD. Vrai ou faux ?
Partie 2 — Le cœur MVC Routage Route Model Binding · CORS
⏱ 22 min

Routage Laravel complet

Routes basiques, paramètres requis et facultatifs, contraintes regex, routes nommées, groupes de routes, Route Model Binding, routes de repli, spoofing de méthode et CORS.

✓ Ce que vous allez apprendre
  • Routes basiques GET/POST/PUT/PATCH/DELETE/OPTIONS sur web.php et api.php
  • Paramètres requis {id} et facultatifs {name?} avec valeur par défaut
  • Contraintes regex sur les paramètres avec ->where()
  • Routes nommées ->name() et génération d'URL avec route()
  • Groupes de routes — middleware, préfixe, namespace
  • Route Model Binding implicite et explicite
  • CORS, route de repli (Route::fallback), cache des routes

1
Routes basiques et méthodes HTTP
web.php vs api.php · GET POST PUT PATCH DELETE · match() · any()
ℹ web.php vs api.php

routes/web.php — routes pour le navigateur. Groupe middleware web : session, protection CSRF, cookies chiffrés. routes/api.php — routes API stateless. Groupe middleware api : préfixe /api automatique, throttle. Pas de session, pas de CSRF. DariGo utilise uniquement api.php.

phproutes/api.php — routes basiques DariGo
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\RestaurantController;
use App\Http\Controllers\CommandeController;

// Route simple avec closure
Route::get('/ping', function () {
    return response()->json(['message' => 'DariGo API fonctionne !']);
});

// Routes vers un controller
Route::get('/restaurants', [RestaurantController::class, 'index']);
Route::post('/restaurants', [RestaurantController::class, 'store']);
Route::get('/restaurants/{id}', [RestaurantController::class, 'show']);
Route::put('/restaurants/{id}', [RestaurantController::class, 'update']);
Route::delete('/restaurants/{id}', [RestaurantController::class, 'destroy']);

// Repondre a plusieurs methodes
Route::match(['get', 'post'], '/recherche', function () {
    return 'recherche';
});

// Repondre a TOUTES les methodes HTTP
Route::any('/webhook', [WebhookController::class, 'handle']);
ℹ Spoofing de méthode — formulaires HTML

Les formulaires HTML ne supportent que GET et POST. Pour envoyer PUT ou DELETE depuis un formulaire Blade, ajouter @method('PUT') ou @method('DELETE'). Laravel lit le champ caché _method et route correctement. Pour les API avec fetch ou Axios : pas besoin, envoyer la méthode HTTP directement.


2
Paramètres de routage
Paramètres requis · facultatifs · contraintes where() · whereNumber()
phpParamètres — exemples DariGo
// Parametre requis — {id} est obligatoire
Route::get('/restaurants/{id}', function ($id) {
    return "Restaurant $id";
});

// Plusieurs parametres
Route::get('/restaurants/{restoId}/plats/{platId}', function ($restoId, $platId) {
    return "Restaurant $restoId, plat $platId";
});

// Parametre facultatif — {ville?} avec valeur par defaut
Route::get('/restaurants/ville/{ville?}', function ($ville = 'Casablanca') {
    return "Restaurants a $ville";
});

// Contraintes regex — id doit etre numerique
Route::get('/restaurants/{id}', [RestaurantController::class, 'show'])
    ->where('id', '[0-9]+');

// Methodes de contrainte courtes (Laravel 8+)
Route::get('/restaurants/{id}', [RestaurantController::class, 'show'])
    ->whereNumber('id');                          // Equivalent a [0-9]+

Route::get('/villes/{slug}', [VilleController::class, 'show'])
    ->whereAlpha('slug');                         // Lettres uniquement

Route::get('/restaurants/{slug}', [RestaurantController::class, 'show'])
    ->whereAlphaNumeric('slug');                  // Lettres et chiffres

// Contrainte globale dans RouteServiceProvider (appliquee partout)
// Route::pattern('id', '[0-9]+');

3
Routes nommées et génération d'URL
->name() · route() · redirect()->route() · current()
ℹ Pourquoi nommer les routes ?

Si l'URL /restaurants change en /api/v2/restaurants, sans routes nommées il faut chercher-remplacer dans tout le code. Avec route('restaurants.index'), changer l'URI dans routes/api.php suffit — tous les route() se mettent à jour automatiquement.

phpRoutes nommées — DariGo
// Nommer une route
Route::get('/restaurants', [RestaurantController::class, 'index'])->name('restaurants.index');
Route::get('/restaurants/{id}', [RestaurantController::class, 'show'])->name('restaurants.show');
Route::post('/commandes', [CommandeController::class, 'store'])->name('commandes.store');

// Utiliser le nom pour generer une URL
$url = route('restaurants.index');
// http://localhost:8000/api/restaurants

$url = route('restaurants.show', ['id' => 5]);
// http://localhost:8000/api/restaurants/5

$url = route('restaurants.show', ['id' => 5, 'avis' => 'true']);
// http://localhost:8000/api/restaurants/5?avis=true

// Redirection vers une route nommee
return redirect()->route('restaurants.index');

// Verifier si on est sur la route courante
if (request()->routeIs('restaurants.*')) {
    // Toutes les routes qui commencent par restaurants.
}

// Obtenir le nom de la route courante
$routeName = request()->route()->getName();

4
Groupes de routes
middleware · prefix · name · controller · API versioning
phproutes/api.php — groupes DariGo complets
// Routes publiques (sans authentification)
Route::prefix('auth')->name('auth.')->group(function () {
    Route::post('/register', [AuthController::class, 'register'])->name('register');
    Route::post('/login',    [AuthController::class, 'login'])->name('login');
});
// URLs generees : /api/auth/register, /api/auth/login
// Noms generes  : auth.register, auth.login

// Routes protegees par Sanctum
Route::middleware('auth:sanctum')->group(function () {

    Route::post('/auth/logout', [AuthController::class, 'logout'])->name('auth.logout');

    // Routes restaurants
    Route::prefix('restaurants')->name('restaurants.')->group(function () {
        Route::get('/',                [RestaurantController::class, 'index'])->name('index');
        Route::post('/',               [RestaurantController::class, 'store'])->name('store');
        Route::get('/{restaurant}',    [RestaurantController::class, 'show'])->name('show');
        Route::put('/{restaurant}',    [RestaurantController::class, 'update'])->name('update');
        Route::delete('/{restaurant}', [RestaurantController::class, 'destroy'])->name('destroy');

        // Routes imbriquees — plats d'un restaurant
        Route::get('/{restaurant}/plats',       [PlatController::class, 'index']);
        Route::post('/{restaurant}/plats',      [PlatController::class, 'store']);
        Route::put('/{restaurant}/plats/{plat}', [PlatController::class, 'update']);
    });

    // Routes commandes
    Route::apiResource('commandes', CommandeController::class);
    // Genere automatiquement : index, store, show, update, destroy

    // Routes livraisons — livreurs uniquement
    Route::middleware('role:livreur')->prefix('livraisons')->group(function () {
        Route::get('/',                         [LivraisonController::class, 'index']);
        Route::patch('/{livraison}/accepter',   [LivraisonController::class, 'accepter']);
        Route::patch('/{livraison}/statut',     [LivraisonController::class, 'updateStatut']);
    });
});

5
Route Model Binding — injection automatique du modèle
Binding implicite · explicite · findOrFail · 404 automatique
ℹ Avant et après Route Model Binding

Sans Route Model Binding : le contrôleur reçoit un $id et doit faire Restaurant::findOrFail($id). Avec : Laravel injecte directement l'objet $restaurant, lève automatiquement une 404 si non trouvé. Moins de code, comportement standardisé.

phpRoute Model Binding — implicite (le plus courant)
// AVANT — sans Route Model Binding
Route::get('/restaurants/{id}', function ($id) {
    $restaurant = Restaurant::findOrFail($id); // Manuel + risque d'oubli
    return response()->json($restaurant);
});

// APRES — binding implicite
// Le parametre {restaurant} correspond au modele Restaurant par convention
Route::get('/restaurants/{restaurant}', function (Restaurant $restaurant) {
    return response()->json($restaurant);
    // Si restaurant non trouve : 404 automatique
});

// Dans un controller — meme principe
Route::get('/restaurants/{restaurant}', [RestaurantController::class, 'show']);

// RestaurantController.php
public function show(Restaurant $restaurant): JsonResponse
{
    return response()->json([
        'restaurant' => $restaurant->load('plats'), // eager loading
    ]);
}

// Binding par un champ autre que l'id (slug par exemple)
Route::get('/restaurants/{restaurant:slug}', [RestaurantController::class, 'show']);
// Laravel cherche Restaurant::where('slug', $valeur)->firstOrFail()

6
CORS, route de repli et cache des routes
config/cors.php · Route::fallback · route:cache
phpconfig/cors.php — configuration CORS DariGo
// config/cors.php — Laravel gere CORS via le middleware HandleCors (automatique)
return [
    'paths'               => ['api/*', 'sanctum/csrf-cookie'],
    'allowed_methods'     => ['*'],
    'allowed_origins'     => [
        'http://localhost:3000',          // Front React en dev
        'https://app.darigo.ma',          // Frontend production
    ],
    'allowed_origins_patterns' => [],
    'allowed_headers'     => ['*'],
    'exposed_headers'     => [],
    'max_age'             => 0,
    'supports_credentials' => true,       // Requis pour Sanctum (cookies)
];
phpRoute de repli et cache
// Route::fallback — executee si aucune route ne correspond
Route::fallback(function () {
    return response()->json([
        'message' => 'Route introuvable',
        'docs'    => 'https://api.darigo.ma/docs',
    ], 404);
});

// Cache des routes — PRODUCTION UNIQUEMENT
// php artisan route:cache    → compile toutes les routes
// php artisan route:clear    → vider le cache
// ATTENTION : route:cache ne fonctionne pas avec les closures dans les routes
// Toujours pointer vers des controllers en production
bashCommandes de debug des routes
php artisan route:list                    # Toutes les routes
php artisan route:list --path=api         # Filtrer par prefixe
php artisan route:list -v                 # Avec middleware
php artisan route:list --name=restaurants # Filtrer par nom
Dans DariGo, Route::get('/restaurants/{restaurant}', [RestaurantController::class, 'show']) — un client envoie GET /api/restaurants/999 et l'id 999 n'existe pas en BDD. Que retourne Laravel ?
php artisan route:cache est exécuté en production. Que se passe-t-il si routes/api.php contient des closures (fonctions anonymes) ?
Partie 2 — Le cœur MVC Middleware · CSRF Cycle requête/réponse
⏱ 20 min

Middleware & Protection CSRF

Créer des middleware personnalisés, les enregistrer (global, groupe, route), passer des paramètres, utiliser le middleware terminate, et comprendre la protection CSRF avec les tokens.

✓ Ce que vous allez apprendre
  • Définition — un middleware comme couche de filtrage avant/après le Controller
  • Créer un middleware avec php artisan make:middleware
  • Enregistrement global, de groupe et de route dans bootstrap/app.php
  • Passer des paramètres à un middleware
  • Middleware terminate — exécuté après l'envoi de la réponse
  • Protection CSRF — @csrf, X-CSRF-Token, X-XSRF-Token pour Axios

1
Définition et cycle de vie d'un middleware
Couche de filtrage · avant et après le Controller ·$next
ℹ Analogie — le middleware comme un agent de sécurité

Imaginez l'entrée d'un restaurant DariGo. Avant que le client (requête) atteigne le serveur (Controller), il passe par un agent de sécurité (middleware) qui vérifie sa réservation (token), l'hébergement (CORS), le débit carbone (rate limiting). Après le service, le middleware peut aussi faire quelque chose lors du départ (logger, analytics).

Middleware Laravel built-inRôleOù appliqué
authVérifier que l'utilisateur est authentifiéRoutes protégées
auth:sanctumAuthentification via token SanctumRoutes API DariGo
verifiedEmail vérifiéRoutes nécessitant un compte validé
throttle:60,1Rate limiting — 60 req/minRoutes API
webSession, CSRF, cookies — groupe completGroupe routes/web.php
apiThrottle + stateless — groupe completGroupe routes/api.php

2
Créer un middleware personnalisé
make:middleware · handle() · $next · avant et après
bashCréer les middleware DariGo
php artisan make:middleware CheckRole        # Vérifier le rôle de l'utilisateur
php artisan make:middleware LogApiRequest    # Logger toutes les requetes API
php artisan make:middleware EnsureRestaurantOwner  # Proprietaire du restaurant
phpapp/Http/Middleware/CheckRole.php — DariGo
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class CheckRole
{
    /**
     * Handle an incoming request.
     * $next = la prochaine couche (middleware suivant ou controller)
     */
    public function handle(Request $request, Closure $next, string ...$roles): Response
    {
        // --- CODE AVANT le Controller (avant $next) ---

        // Verifier que l'utilisateur est authentifie
        if (! $request->user()) {
            return response()->json(['erreur' => 'Non authentifie'], 401);
        }

        // Verifier le role
        if (! in_array($request->user()->role, $roles)) {
            return response()->json([
                'erreur'          => 'Acces refuse',
                'votre_role'      => $request->user()->role,
                'roles_requis'    => $roles,
            ], 403);
        }

        // Passer a la suite (prochain middleware ou Controller)
        $response = $next($request);

        // --- CODE APRES le Controller (apres $next) ---
        // Ajouter un header de reponse par exemple
        $response->headers->set('X-DariGo-Role', $request->user()->role);

        return $response;
    }
}
phpapp/Http/Middleware/LogApiRequest.php
public function handle(Request $request, Closure $next): Response
{
    // AVANT : logger la requete entrante
    \Log::info('DariGo API Request', [
        'method'  => $request->method(),
        'url'     => $request->fullUrl(),
        'user_id' => $request->user()?->id,
        'ip'      => $request->ip(),
    ]);

    return $next($request);
}

3
Enregistrement des middleware — Laravel 11
bootstrap/app.php · global · groupe · alias · route
ℹ Changement Laravel 11 — plus de Kernel.php

Dans Laravel 10 et antérieures, les middleware étaient enregistrés dans app/Http/Kernel.php. Laravel 11 a supprimé ce fichier. Tout se fait dans bootstrap/app.php.

phpbootstrap/app.php — enregistrement Laravel 11
use App\Http\Middleware\CheckRole;
use App\Http\Middleware\LogApiRequest;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
    )
    ->withMiddleware(function (Middleware $middleware) {

        // Middleware global — execute sur TOUTES les requetes
        $middleware->append(LogApiRequest::class);

        // Alias — utiliser un alias court dans les routes
        $middleware->alias([
            'role'  => CheckRole::class,
            'owner' => EnsureRestaurantOwner::class,
        ]);

        // Exclure une route du CSRF (webhooks par exemple)
        $middleware->validateCsrfTokens(except: [
            '/stripe/webhook',
            '/api/*',          // Toutes les routes API sont exclues du CSRF
        ]);
    })
    ->create();
phproutes/api.php — utiliser les alias
// Middleware role avec parametre
Route::middleware(['auth:sanctum', 'role:admin'])->group(function () {
    Route::delete('/restaurants/{restaurant}', [RestaurantController::class, 'destroy']);
});

// Plusieurs roles acceptes
Route::middleware(['auth:sanctum', 'role:restaurateur,admin'])->group(function () {
    Route::post('/restaurants', [RestaurantController::class, 'store']);
    Route::put('/restaurants/{restaurant}', [RestaurantController::class, 'update']);
});

// Middleware directement sur une route
Route::patch('/commandes/{commande}/statut', [CommandeController::class, 'updateStatut'])
    ->middleware(['auth:sanctum', 'role:livreur,admin']);

4
Middleware terminate — après l'envoi de la réponse
terminate() · analytics · nettoyage · logging temps de réponse
phpMiddleware terminate — mesurer les temps de réponse DariGo
class LogApiRequest
{
    private float $startTime;

    public function handle(Request $request, Closure $next): Response
    {
        $this->startTime = microtime(true);  // Demarrer le chrono
        return $next($request);
    }

    // terminate() est appele APRES que la reponse a ete envoyee au client
    // Ideal pour les operations lentes (BDD, API tierces) sans ralentir la reponse
    public function terminate(Request $request, Response $response): void
    {
        $duration = round((microtime(true) - $this->startTime) * 1000, 2); // ms

        \Log::info('DariGo API Response', [
            'url'         => $request->path(),
            'method'      => $request->method(),
            'status'      => $response->getStatusCode(),
            'duration_ms' => $duration,
            'user_id'     => $request->user()?->id,
        ]);

        // Alerter si la reponse depasse 500ms
        if ($duration > 500) {
            \Log::warning('Reponse lente DariGo', ['url' => $request->path(), 'ms' => $duration]);
        }
    }
}

5
Protection CSRF
@csrf · X-CSRF-Token · X-XSRF-Token pour Axios · routes API exclues
ℹ Qu'est-ce qu'une attaque CSRF ?

Un site malveillant faux-darigo.ma fait soumettre un formulaire à darigo.ma/commande depuis le navigateur d'un utilisateur connecté. Sans protection CSRF, la requête aboutit car le navigateur envoie automatiquement les cookies de session. Le token CSRF — généré par Laravel et valide uniquement pour le vrai site — empêche cette attaque.

phpProtection CSRF dans les templates Blade
<!-- Formulaire Blade : @csrf genere un champ cache _token -->
<form method="POST" action="/commandes">
    @csrf
    <!-- Genere : <input type="hidden" name="_token" value="aBcXyz..."> -->
    <input type="text" name="adresse">
    <button type="submit">Commander</button>
</form>

<!-- Spoofing de methode pour PUT/DELETE -->
<form method="POST" action="/restaurants/{{ $restaurant->id }}">
    @csrf
    @method('PUT')
    <!-- Laravel lit le champ _method=PUT et route correctement -->
</form>
javascriptCSRF avec Axios (SPA React/Vue qui consomme l'API DariGo)
// Etape 1 : recuperer le cookie XSRF-TOKEN via la route sanctum
await axios.get('/sanctum/csrf-cookie');  // Sanctum cree le cookie XSRF-TOKEN

// Etape 2 : Axios lit automatiquement XSRF-TOKEN et l'envoie en header X-XSRF-TOKEN
// Pas besoin de configuration manuelle avec Axios
const response = await axios.post('/api/commandes', {
    restaurant_id: 5,
    lignes: [{ plat_id: 12, quantite: 2 }]
});

// Pour les routes API (routes/api.php) : CSRF n'est PAS requis
// Les API utilisent Bearer tokens (Sanctum), pas les sessions/cookies
L'API DariGo utilise routes/api.php avec Sanctum Bearer tokens. Faut-il ajouter @csrf dans les requêtes Postman ?
Un middleware CheckRestaurantHoraires doit bloquer les commandes après 23h. Où placer le code de vérification dans la méthode handle() ?
Partie 2 — Le cœur MVC Controllers · REST Form Request · Injection
⏱ 22 min

Contrôleurs & Injection de dépendances

Contrôleur basique vs contrôleur ressource (7 méthodes CRUD), apiResource pour les API pures, injection de dépendances, Form Request, contrôleur avec middleware intégré.

✓ Ce que vous allez apprendre
  • Contrôleur basique vs contrôleur ressource — quand choisir quoi
  • Les 7 méthodes d'un Resource Controller : index, create, store, show, edit, update, destroy
  • --api — 5 méthodes pour API (sans create et edit)
  • Injection de dépendances dans le constructeur et dans les méthodes
  • Form Request — déléguer la validation hors du Controller
  • Single Action Controller — __invoke() pour une action unique

1
Resource Controller — 7 méthodes CRUD
--resource · --api · Route::apiResource · only() · except()
bashGénérer les controllers DariGo
# Controller resource complet (web — avec create et edit pour formulaires Blade)
php artisan make:controller RestaurantController --resource

# Controller API (sans create et edit — pas de formulaires HTML)
php artisan make:controller RestaurantController --api

# Controller + model + migration + factory + seeder en une commande
php artisan make:controller RestaurantController --api --model=Restaurant

# Single action controller (une seule action)
php artisan make:controller CalculerTarif --invokable
MéthodeVerbe HTTPURINom de routeDescriptionAPI ?
index()GET/restaurantsrestaurants.indexListe tous les restaurantsOui
create()GET/restaurants/createrestaurants.createFormulaire de créationNon
store()POST/restaurantsrestaurants.storeEnregistrer un nouveau restaurantOui
show()GET/restaurants/{id}restaurants.showAfficher un restaurantOui
edit()GET/restaurants/{id}/editrestaurants.editFormulaire de modificationNon
update()PUT/PATCH/restaurants/{id}restaurants.updateMettre à jour un restaurantOui
destroy()DELETE/restaurants/{id}restaurants.destroySupprimer un restaurantOui
phproutes/api.php — enregistrement resource
// Route::apiResource — enregistre les 5 routes API (sans create et edit)
Route::apiResource('restaurants', RestaurantController::class);

// Limiter les routes enregistrees
Route::apiResource('commandes', CommandeController::class)->only(['index', 'store', 'show']);
Route::apiResource('plats', PlatController::class)->except(['destroy']);

// Verifier les routes generees
// php artisan route:list --name=restaurants

2
RestaurantController complet — DariGo
JsonResponse · load() · paginate() · response()->json()
phpapp/Http/Controllers/RestaurantController.php
<?php

namespace App\Http\Controllers;

use App\Models\Restaurant;
use App\Http\Requests\StoreRestaurantRequest;
use App\Http\Requests\UpdateRestaurantRequest;
use Illuminate\Http\JsonResponse;

class RestaurantController extends Controller
{
    // Proteger toutes les methodes sauf index et show
    public function __construct()
    {
        $this->middleware('auth:sanctum')->except(['index', 'show']);
        $this->middleware('role:restaurateur,admin')->only(['store', 'update', 'destroy']);
    }

    // GET /api/restaurants — liste avec pagination et filtres
    public function index(): JsonResponse
    {
        $query = Restaurant::with('plats')              // Eager loading — eviter N+1
            ->where('actif', true);

        // Filtres via query string : /api/restaurants?ville=Casablanca&categorie=tajine
        if (request('ville')) {
            $query->where('ville', 'like', '%' . request('ville') . '%');
        }
        if (request('categorie')) {
            $query->where('categorie', request('categorie'));
        }

        $restaurants = $query->orderBy('note', 'desc')->paginate(10);

        return response()->json($restaurants);
    }

    // POST /api/restaurants — creer un restaurant
    public function store(StoreRestaurantRequest $request): JsonResponse
    {
        // $request->validated() retourne uniquement les champs valides
        $restaurant = Restaurant::create([
            ...$request->validated(),
            'proprietaire_id' => auth()->id(),
        ]);

        return response()->json([
            'message'     => 'Restaurant cree avec succes',
            'restaurant'  => $restaurant,
        ], 201);
    }

    // GET /api/restaurants/{restaurant} — Route Model Binding
    public function show(Restaurant $restaurant): JsonResponse
    {
        return response()->json([
            'restaurant' => $restaurant->load(['plats', 'avis']),
        ]);
    }

    // PUT /api/restaurants/{restaurant}
    public function update(UpdateRestaurantRequest $request, Restaurant $restaurant): JsonResponse
    {
        // Verifier que l'utilisateur est bien le proprietaire
        $this->authorize('update', $restaurant);   // Policy

        $restaurant->update($request->validated());

        return response()->json([
            'message'    => 'Restaurant mis a jour',
            'restaurant' => $restaurant->fresh(), // Recharger depuis la BDD
        ]);
    }

    // DELETE /api/restaurants/{restaurant}
    public function destroy(Restaurant $restaurant): JsonResponse
    {
        $this->authorize('delete', $restaurant);
        $restaurant->delete();

        return response()->json(['message' => 'Restaurant supprime'], 200);
    }
}

3
Form Request — valider hors du Controller
make:request · rules() · authorize() · messages() · attributes()
ℹ Pourquoi Form Request plutôt que $request->validate() dans le Controller ?

Valider dans le Controller crée un Controller gras et difficile à tester. La Form Request encapsule la logique de validation dans une classe dédiée, réutilisable et testable indépendamment. Le Controller reste "mince" — il reçoit des données déjà validées.

bashGénérer les Form Requests DariGo
php artisan make:request StoreRestaurantRequest
php artisan make:request UpdateRestaurantRequest
php artisan make:request StoreCommandeRequest
phpapp/Http/Requests/StoreRestaurantRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreRestaurantRequest extends FormRequest
{
    // Autoriser ou non cette requete (avant les regles)
    public function authorize(): bool
    {
        // Seuls les restaurateurs et admins peuvent creer un restaurant
        return in_array(auth()->user()?->role, ['restaurateur', 'admin']);
    }

    // Regles de validation
    public function rules(): array
    {
        return [
            'nom'        => ['required', 'string', 'min:3', 'max:100'],
            'ville'      => ['required', 'string'],
            'adresse'    => ['required', 'string', 'max:255'],
            'telephone'  => ['nullable', 'string', 'regex:/^(0[5-7][0-9]{8})$/'],
            'categorie'  => ['required', 'in:tajine,couscous,pizza,burger,sushi,fruits_de_mer,marocain'],
            'horaires'   => ['nullable', 'array'],
            'horaires.*' => ['string'],
        ];
    }

    // Messages d'erreur personnalises
    public function messages(): array
    {
        return [
            'nom.required'       => 'Le nom du restaurant est obligatoire',
            'nom.min'            => 'Le nom doit contenir au moins :min caracteres',
            'categorie.in'       => 'Categorie invalide. Valeurs : tajine, couscous, pizza, burger...',
            'telephone.regex'    => 'Telephone invalide. Format marocain : 06XXXXXXXX ou 07XXXXXXXX',
        ];
    }

    // Noms conviviaux des champs dans les messages d'erreur
    public function attributes(): array
    {
        return [
            'nom'       => 'nom du restaurant',
            'adresse'   => 'adresse du restaurant',
            'categorie' => 'categorie culinaire',
        ];
    }
}

✓
Vérification des acquis
DariGo est une API pure. Quelle différence entre Route::resource() et Route::apiResource() ?
Dans StoreRestaurantRequest, la méthode authorize() retourne false. Que se passe-t-il ?
Partie 2 — Le cœur MVC Blade · Templates Layouts · Composants
⏱ 20 min

Vues & Templates Blade

Créer des vues Blade, transmettre des données, utiliser @extends/@section/@yield pour les layouts, les directives (@if, @foreach, @auth), les composants et les View Composers.

✓ Ce que vous allez apprendre
  • Créer et retourner une vue avec view() — transmettre des données
  • Layout principal avec @extends, @section, @yield
  • Directives de contrôle : @if, @foreach, @forelse, @for, @while
  • Directives d'authentification : @auth, @guest, @can, @role
  • {{ }} vs {!! !!} — échappement XSS
  • Composants Blade @component et Anonymous Components
  • View Composers — injecter des données automatiquement dans des vues

1
Créer une vue et transmettre des données
view() · with() · compact() · {{ }} vs {!! !!}
ℹ Blade — moteur de templates PHP

Blade est le moteur de templates de Laravel. Les fichiers Blade ont l'extension .blade.php et sont compilés en PHP pur puis mis en cache. Performance identique à du PHP pur, avec la lisibilité des directives. Pour DariGo : les vues Blade servent pour le panneau d'administration (non API).

phpController — retourner une vue
// Methode 1 — tableau associatif
return view('restaurants.index', [
    'restaurants' => Restaurant::paginate(12),
    'villes'      => ['Casablanca', 'Rabat', 'Marrakech', 'Fes'],
    'titre'       => 'Tous les restaurants DariGo',
]);

// Methode 2 — compact() (variables PHP → cles du tableau)
$restaurants = Restaurant::paginate(12);
$villes      = ['Casablanca', 'Rabat', 'Marrakech'];
return view('restaurants.index', compact('restaurants', 'villes'));

// Methode 3 — with() chainable
return view('restaurants.index')
    ->with('restaurants', Restaurant::paginate(12))
    ->with('titre', 'DariGo Restaurants');
phpresources/views/restaurants/index.blade.php — affichage des données
{{-- Commentaire Blade — non visible dans le HTML generé --}}

{{-- {{ }} : echappe automatiquement le HTML — protection XSS --}}
<h1>{{ $titre }}</h1>
{{-- Si $titre = "<script>alert('XSS')</script>" → affiche le texte sans executer --}}

{{-- {!! !!} : affiche le HTML brut — UNIQUEMENT pour du HTML de confiance --}}
<div class="description">{!! $restaurant->description_html !!}</div>

{{-- Valeur par defaut si null --}}
<p>{{ $restaurant->telephone ?? 'Non renseigne' }}</p>

{{-- Variable globale $errors (toujours disponible) --}}
@if ($errors->any())
    <ul>
        @foreach ($errors->all() as $error)
            <li>{{ $error }}</li>
        @endforeach
    </ul>
@endif

2
Layout principal avec @extends, @section, @yield
Héritage de templates · @stack · @push
phpresources/views/layouts/app.blade.php — layout principal DariGo
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>@yield('title', 'DariGo — Livraison de repas')</title>
    <link rel="stylesheet" href="{{ asset('css/app.css') }}">
    @stack('styles')  {{-- Les vues enfants peuvent ajouter du CSS --}}
</head>
<body>
    @include('partials.navbar')  {{-- Inclure la navbar --}}

    <main class="container">
        @yield('content')  {{-- Zone principale — remplie par les vues enfants --}}
    </main>

    @include('partials.footer')

    <script src="{{ asset('js/app.js') }}"></script>
    @stack('scripts')  {{-- Scripts specifiques aux pages --}}
</body>
</html>
phpresources/views/restaurants/index.blade.php — vue enfant
@extends('layouts.app')  {{-- Heriter du layout principal --}}

@section('title', 'Restaurants — DariGo')  {{-- Remplir le titre --}}

@section('content')
    <h1>Restaurants disponibles à {{ $ville ?? 'Casablanca' }}</h1>

    @if($restaurants->isEmpty())
        <p>Aucun restaurant disponible pour le moment.</p>
    @else
        <div class="grid">
            @foreach($restaurants as $restaurant)
                @include('partials.restaurant-card', ['restaurant' => $restaurant])
            @endforeach
        </div>

        {{-- Pagination --}}
        {{ $restaurants->links() }}
    @endif
@endsection

@push('scripts')
    {{-- Script specifique a cette page --}}
    <script src="{{ asset('js/restaurants-filter.js') }}"></script>
@endpush

3
Directives essentielles
@if @foreach @forelse @auth @guest @can @env
phpDirectives Blade — référence DariGo
{{-- Conditions --}}
@if($restaurant->actif)
    <span class="badge-green">Ouvert</span>
@elseif($restaurant->horaire_fermeture > now())
    <span class="badge-orange">Ferme bientot</span>
@else
    <span class="badge-red">Ferme</span>
@endif

{{-- @unless — l'inverse de @if --}}
@unless($restaurant->actif)
    <p>Ce restaurant est temporairement ferme.</p>
@endunless

{{-- @forelse — foreach avec gestion du cas vide --}}
@forelse($restaurants as $restaurant)
    <div>{{ $restaurant->nom }}</div>
@empty
    <p>Aucun restaurant dans votre ville.</p>
@endforelse

{{-- Variable $loop dans @foreach --}}
@foreach($plats as $plat)
    <tr class="{{ $loop->even ? 'bg-gray' : '' }}">
        <td>{{ $loop->iteration }}</td>   {{-- 1, 2, 3... --}}
        <td>{{ $plat->nom }}</td>
        @if($loop->last) {{-- Dernier element --}}
            <td class="last">{{ $plat->prix }} MAD</td>
        @endif
    </tr>
@endforeach

{{-- Authentification --}}
@auth
    <p>Bonjour, {{ auth()->user()->prenom }} !</p>
    <a href="/commandes">Mes commandes</a>
@endauth

@guest
    <a href="/login">Se connecter pour commander</a>
@endguest

{{-- Autorisation (Gates/Policies) --}}
@can('update', $restaurant)
    <a href="/restaurants/{{ $restaurant->id }}/edit">Modifier</a>
@endcan

@cannot('delete', $restaurant)
    {{-- Masquer le bouton supprimer si non autorise --}}
@endcannot

{{-- Environnement --}}
@env('local')
    <div class="debug-bar">Mode développement</div>
@endenv

4
Composants Blade et View Composers
make:component · Anonymous Components · View::composer()
bashComposants DariGo
php artisan make:component RestaurantCard  # Composant avec classe PHP
php artisan make:component Alert --view    # Composant anonyme (sans classe)
phpresources/views/components/restaurant-card.blade.php
{{-- Composant anonyme — reusable partout --}}
@props([
    'restaurant',           // Obligatoire
    'showRating' => true,   // Facultatif avec valeur par defaut
])

<div class="restaurant-card {{ $restaurant->actif ? 'active' : 'closed' }}">
    <h3>{{ $restaurant->nom }}</h3>
    <p>{{ $restaurant->ville }} — {{ $restaurant->categorie }}</p>
    @if($showRating)
        <span>★ {{ number_format($restaurant->note, 1) }}</span>
    @endif
    {{ $slot }}  {{-- Contenu injecte entre les balises --}}
</div>
phpUtiliser le composant dans une vue
@foreach($restaurants as $restaurant)
    <x-restaurant-card :restaurant="$restaurant" :show-rating="true">
        <!-- Contenu dans $slot -->
        <a href="/restaurants/{{ $restaurant->id }}">Voir le menu</a>
    </x-restaurant-card>
@endforeach
Dans un template Blade, {{ $restaurant->description }} vs {!! $restaurant->description !!}. Un utilisateur a stocké <script>alert('hack')</script> comme description. Que se passe-t-il avec chaque syntaxe ?
Partie 2 — Le cœur MVC Request · Response Sessions · Cookies
⏱ 18 min

Requêtes, Réponses & Sessions

L'objet Request (input, fichiers, headers, IP), les types de réponses (response()->json(), redirect(), fichiers), sessions HTTP (get/put/flash/forget), Response Macro.

✓ Ce que vous allez apprendre
  • L'objet Request — accès à l'input, aux fichiers, aux headers, à l'IP
  • Méthodes input(), only(), except(), has(), filled()
  • Upload de fichiers — store(), validation MIME, taille max
  • Types de réponses — JSON, redirect, fichier, stream
  • Sessions — put(), get(), flash(), forget(), regenerate()

1
L'objet Request — accéder aux données de la requête
input() · only() · except() · has() · filled() · method() · ip()
phpAccéder à la requête — exemples DariGo
public function store(Request $request): JsonResponse
{
    // ---- LIRE LES DONNEES ----

    // Tous les champs (POST body + query string)
    $donnees = $request->all();

    // Un seul champ
    $nom = $request->input('nom');               // null si absent
    $nom = $request->input('nom', 'Sans nom');   // Valeur par defaut
    $nom = $request->nom;                        // Propriete magique — equivalent

    // Plusieurs champs specifiques
    $champs = $request->only(['nom', 'ville', 'categorie']);
    // ['nom' => 'DariGo', 'ville' => 'Casablanca', 'categorie' => 'tajine']

    // Tous sauf certains champs
    $sansPassword = $request->except(['password', 'password_confirmation']);

    // ---- VERIFIER LA PRESENCE ----
    if ($request->has('telephone')) {         // Existe dans la requete (meme vide)
        $tel = $request->input('telephone');
    }
    if ($request->filled('adresse')) {        // Existe ET non vide
        $adresse = $request->input('adresse');
    }
    if ($request->missing('categorie')) {     // Manquant
        return response()->json(['erreur' => 'Categorie manquante'], 400);
    }

    // ---- INFORMATIONS SUR LA REQUETE ----
    $method   = $request->method();           // 'POST', 'GET', 'PUT'...
    $isPost   = $request->isMethod('post');   // true/false
    $url      = $request->url();              // http://api.darigo.ma/api/restaurants
    $fullUrl  = $request->fullUrl();          // Avec query string
    $path     = $request->path();             // api/restaurants
    $ip       = $request->ip();              // IP du client
    $isApi    = $request->expectsJson();      // Attend une reponse JSON (Accept: application/json)
    $isAjax   = $request->ajax();            // Requete AJAX (X-Requested-With: XMLHttpRequest)

    // ---- HEADERS ----
    $token    = $request->header('Authorization');
    $accept   = $request->header('Accept', 'application/json'); // Valeur par defaut
    $allHeaders = $request->headers->all();
}

2
Upload de fichiers
file() · store() · storeAs() · isValid() · extension() · size()
phpUpload photo de restaurant — DariGo
public function uploadPhoto(Request $request, Restaurant $restaurant): JsonResponse
{
    $request->validate([
        'photo' => ['required', 'file', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
        //                                               Types MIME autorises  Max 2 Mo
    ]);

    $file = $request->file('photo');

    // Verifications
    if (! $file->isValid()) {
        return response()->json(['erreur' => 'Fichier corrompu'], 422);
    }

    $extension = $file->extension();         // 'jpg', 'png'
    $taille    = $file->getSize();           // Taille en octets
    $mimetype  = $file->getMimeType();       // 'image/jpeg'

    // Stocker le fichier
    // store() genere un nom unique automatiquement
    $path = $file->store('restaurants/photos', 'public');
    // Stocke dans storage/app/public/restaurants/photos/nomUnique.jpg
    // Accessible via /storage/restaurants/photos/nomUnique.jpg

    // storeAs() pour garder le nom original
    $path = $file->storeAs(
        'restaurants/photos',
        'restaurant-' . $restaurant->id . '.' . $extension,
        'public'
    );

    // Mettre a jour le restaurant
    $restaurant->update(['photo' => $path]);

    return response()->json([
        'message'    => 'Photo mise a jour',
        'photo_url'  => Storage::url($path),
    ]);
}

3
Types de réponses HTTP
response()->json() · redirect() · back() · response()->file() · Response Macro
phpRéponses Laravel — référence complète
// ---- REPONSES JSON (API DariGo) ----
return response()->json($data);                          // 200 OK
return response()->json($data, 201);                     // 201 Created
return response()->json(['erreur' => 'Not found'], 404); // 404
return response()->json($data)->header('X-Version', '1.0');

// ---- REDIRECTIONS (vues Blade) ----
return redirect('/restaurants');                          // Vers un URI
return redirect()->route('restaurants.index');            // Vers une route nommee
return redirect()->back();                                // Page precedente
return redirect()->back()->withErrors($validator);        // Avec erreurs de validation
return redirect()->back()->with('success', 'Commande passee !'); // Avec message flash

// ---- TELECHARGEMENT DE FICHIER ----
return response()->download(storage_path('app/factures/fact-001.pdf'));
return response()->download($path, 'facture-darigo.pdf'); // Nom personnalise
return response()->file(storage_path('app/public/menu.pdf')); // Afficher dans navigateur

// ---- RESPONSE MACRO — reponse API personnalisee ----
// Dans un ServiceProvider (AppServiceProvider::boot())
Response::macro('darigo', function ($data, $message = 'Succes', $status = 200) {
    return Response::json([
        'succes'  => $status < 400,
        'message' => $message,
        'data'    => $data,
    ], $status);
});

// Utilisation dans les controllers
return response()->darigo($restaurant, 'Restaurant cree', 201);
// {"succes":true, "message":"Restaurant cree", "data":{...}}

4
Sessions HTTP
put() · get() · flash() · forget() · regenerate()
ℹ Sessions — uniquement pour les routes web.php

Les sessions n'ont de sens que dans les routes web.php (middleware web avec gestion de session). Les routes API (api.php) sont stateless — pas de sessions. DariGo utilise les sessions uniquement dans le panneau d'administration Blade.

phpSessions — panneau admin DariGo
// Stocker en session
session()->put('ville_preference', 'Casablanca');
session(['panier' => ['plat_id' => 5, 'quantite' => 2]]);

// Lire en session
$ville = session('ville_preference');              // null si absent
$ville = session('ville_preference', 'Casablanca'); // Valeur par defaut
$ville = session()->get('ville_preference');        // Equivalent

// Verifier
if (session()->has('panier')) { ... }               // Existe (meme null)
if (session()->exists('panier')) { ... }            // Existe (meme si null)

// Messages flash — disponibles uniquement pour la PROCHAINE requete
session()->flash('success', 'Restaurant cree avec succes !');
session()->flash('error', 'Telephone invalide');

// Lire en Blade :
// @if(session('success')) 
{{ session('success') }}
@endif // Supprimer session()->forget('ville_preference'); session()->flush(); // Vider TOUTE la session // Regenerer l'ID de session (apres login — prevenir session fixation) session()->regenerate();
Dans l'API DariGo (routes/api.php), un Controller appelle session()->put('commande', $data). Que se passe-t-il ?
Partie 2 — Le cœur MVC Validation · Erreurs Logging · Exceptions
⏱ 20 min

Validation, Gestion des erreurs & Logging

Règles de validation complètes, messages personnalisés, règles custom, gestion globale des exceptions dans bootstrap/app.php, logging avec Monolog (channels, niveaux).

✓ Ce que vous allez apprendre
  • Validation inline avec $request->validate() vs Form Request
  • 30+ règles Laravel : required, unique, exists, regex, between, confirmed...
  • Créer une règle de validation custom avec php artisan make:rule
  • Réponse automatique 422 pour les API avec le détail des erreurs
  • Gestion globale des exceptions — bootstrap/app.php
  • Logging — niveaux, canaux (stack, daily, slack), contexte

1
Règles de validation — référence complète
required · unique · exists · regex · confirmed · Rule::
phpValidation complète — commande DariGo
use Illuminate\Validation\Rule;

// Dans un Controller ou Form Request
$request->validate([
    // Chaines
    'nom'          => ['required', 'string', 'min:3', 'max:100'],
    'description'  => ['nullable', 'string', 'max:1000'],

    // Email
    'email'        => ['required', 'email:rfc,dns', 'unique:users,email'],
    //                                   Validation DNS   Unique dans table users

    // Numerique
    'prix'         => ['required', 'numeric', 'between:0,9999.99', 'gt:0'],
    'stock'        => ['required', 'integer', 'min:0'],
    'quantite'     => ['required', 'integer', 'min:1', 'max:100'],

    // Mot de passe
    'password'     => ['required', 'string', 'min:8', 'confirmed'],
    //                                                  password_confirmation requis

    // Telephone marocain
    'telephone'    => ['required', 'regex:/^(0[5-7][0-9]{8})$/'],

    // Valeur dans une liste
    'categorie'    => ['required', Rule::in(['tajine', 'couscous', 'pizza', 'burger'])],
    'statut'       => ['required', Rule::in(['en_attente', 'confirmee', 'livree', 'annulee'])],

    // Reference vers un enregistrement BDD (FK)
    'restaurant_id' => ['required', 'exists:restaurants,id'],
    'plat_id'       => ['required', Rule::exists('plats', 'id')->where('actif', true)],
    //                               Doit exister ET etre actif

    // Unique avec exception (lors d'un update — ignorer l'enregistrement courant)
    'email'         => ['required', Rule::unique('users')->ignore($user->id)],

    // Tableau et sous-elements
    'lignes'        => ['required', 'array', 'min:1'],
    'lignes.*.plat_id'   => ['required', 'exists:plats,id'],
    'lignes.*.quantite'  => ['required', 'integer', 'min:1'],

    // Fichier
    'photo'        => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],

    // Conditions
    'adresse_livraison' => ['required_if:type_livraison,domicile'],
    //                       Requis si type_livraison = domicile
]);

// Reponse automatique si validation echoue :
// - Routes web.php : redirect()->back()->withErrors()
// - Routes api.php : 422 JSON avec {'message': 'The given data was invalid.', 'errors': {...}}

2
Règle de validation personnalisée
make:rule · passes() · message() · Closure rule
bashCréer une règle custom
php artisan make:rule HorairesOuverture  # Valider les horaires d'ouverture DariGo
phpapp/Rules/HorairesOuverture.php
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class HorairesOuverture implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        // $value doit etre un tableau de type ['lundi' => '09:00-22:00', ...]
        if (! is_array($value)) {
            $fail("Les :attribute doivent etre un tableau.");
            return;
        }

        $joursValides = ['lundi','mardi','mercredi','jeudi','vendredi','samedi','dimanche'];

        foreach ($value as $jour => $horaire) {
            if (! in_array($jour, $joursValides)) {
                $fail("Jour invalide dans :attribute : $jour");
                return;
            }
            // Format heure : 09:00-22:00
            if (! preg_match('/^\d{2}:\d{2}-\d{2}:\d{2}$/', $horaire)) {
                $fail("Format invalide pour $jour. Attendu : HH:MM-HH:MM (ex: 09:00-22:00)");
            }
        }
    }
}

// Utilisation :
// 'horaires' => ['nullable', new HorairesOuverture]

3
Gestion globale des exceptions
bootstrap/app.php · render() · ModelNotFoundException · 404/500 JSON
phpbootstrap/app.php — gestion centralisée des exceptions
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Auth\Access\AuthorizationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

->withExceptions(function (Exceptions $exceptions) {

    // Quand un modele n'est pas trouve (Route Model Binding)
    $exceptions->render(function (ModelNotFoundException $e, Request $request) {
        if ($request->expectsJson()) {
            return response()->json([
                'erreur' => 'Ressource introuvable',
                'type'   => class_basename($e->getModel()),
            ], 404);
        }
    });

    // Route non trouvee
    $exceptions->render(function (NotFoundHttpException $e, Request $request) {
        if ($request->expectsJson()) {
            return response()->json([
                'erreur' => 'Route introuvable',
                'docs'   => 'https://api.darigo.ma/docs',
            ], 404);
        }
    });

    // Non authentifie
    $exceptions->render(function (AuthenticationException $e, Request $request) {
        if ($request->expectsJson()) {
            return response()->json(['erreur' => 'Non authentifie'], 401);
        }
    });

    // Non autorise (Gate/Policy)
    $exceptions->render(function (AuthorizationException $e, Request $request) {
        if ($request->expectsJson()) {
            return response()->json(['erreur' => 'Action non autorisee'], 403);
        }
    });
})

4
Logging avec Monolog
Niveaux · Canaux · Contexte · stack · daily · slack
NiveauUtilisationExemple DariGo
Log::debug()Débogage détaillé — dev uniquementValeurs de variables pendant le dev
Log::info()Information normaleNouvelle commande créée, livreur affecté
Log::notice()Événement notable mais normalRestaurant fermé manuellement par admin
Log::warning()Situation à surveillerRabbitMQ indisponible, tentative rate limit
Log::error()Erreur récupérablePaiement échoué, SMS non envoyé
Log::critical()Erreur critique — intervention requiseBase de données inaccessible
phpLogging dans les controllers DariGo
use Illuminate\Support\Facades\Log;

// Log simple
Log::info('Nouvelle commande DariGo');

// Log avec contexte (tableau de donnees supplementaires)
Log::info('Commande creee', [
    'commande_id'  => $commande->id,
    'client_id'    => auth()->id(),
    'restaurant'   => $commande->restaurant->nom,
    'total'        => $commande->total . ' MAD',
]);

// Log conditionnel — seulement en production
Log::channel('slack')->critical('BDD inaccessible !', [
    'error' => $e->getMessage(),
]);

// config/logging.php — canal 'daily' cree un fichier par jour
// storage/logs/laravel-2024-11-15.log

// Exemple de log genere :
// [2024-11-15 14:23:01] local.INFO: Commande creee {"commande_id":123,"client_id":7,...}
Un client envoie POST /api/restaurants avec { "email": "invalid-email" }. L'API attend un email valide avec la règle 'email' => 'required|email'. Quel code HTTP est retourné ?
La règle 'email' => 'unique:users,email' est utilisée lors d'un update de profil. L'utilisateur soumet le même email que le sien (sans changement). Que se passe-t-il ?
Partie 3 — Laravel Avancé Artisan · Scheduling Commandes custom
⏱ 18 min

Artisan avancé & Task Scheduling

Créer des commandes Artisan personnalisées avec arguments et options, planifier des tâches récurrentes avec le scheduler Laravel (cron unique), et maîtriser les commandes Artisan avancées du quotidien.

✓ Ce que vous allez apprendre
  • Créer une commande custom avec php artisan make:command
  • Définir des arguments obligatoires et des options facultatives
  • Afficher de la progression avec info(), warn(), error(), progressBar()
  • Planifier des tâches avec schedule:run — un seul cron pour toute l'application
  • Fréquences : everyMinute(), hourly(), daily(), cron()
  • Conditions et callbacks : when(), skip(), then()

1
Commandes Artisan personnalisées
make:command · handle() · arguments · options · output
bashGénérer des commandes DariGo
php artisan make:command EnvoyerRappelsLivraison
php artisan make:command SynchroniserMenus
php artisan make:command NettoyerCommandesExpirees
phpapp/Console/Commands/EnvoyerRappelsLivraison.php
<?php

namespace App\Console\Commands;

use App\Models\Commande;
use App\Notifications\RappelLivraisonNotification;
use Illuminate\Console\Command;

class EnvoyerRappelsLivraison extends Command
{
    // Signature : nom + arguments + options
    // {ville} = argument obligatoire
    // {--dry-run} = option sans valeur (flag boolean)
    // {--delai=30} = option avec valeur par defaut
    protected $signature = 'darigo:rappels-livraison
                            {ville : Ville cible (ex: Casablanca)}
                            {--dry-run : Simuler sans envoyer}
                            {--delai=30 : Delai en minutes avant rappel}';

    protected $description = 'Envoyer des rappels aux clients dont la livraison est en retard';

    public function handle(): int
    {
        $ville   = $this->argument('ville');
        $dryRun  = $this->option('dry-run');
        $delai   = (int) $this->option('delai');

        $this->info("DariGo — Rappels livraison pour : $ville");
        $this->info("Delai : {$delai} minutes | Simulation : " . ($dryRun ? 'Oui' : 'Non'));

        // Recuperer les commandes en retard
        $commandes = Commande::where('ville', $ville)
            ->where('statut', 'en_livraison')
            ->where('updated_at', '<', now()->subMinutes($delai))
            ->get();

        if ($commandes->isEmpty()) {
            $this->warn("Aucune commande en retard trouvee pour $ville.");
            return Command::SUCCESS;
        }

        $this->info("Commandes en retard : {$commandes->count()}");

        // Barre de progression
        $bar = $this->output->createProgressBar($commandes->count());
        $bar->start();

        $envoyes = 0;
        foreach ($commandes as $commande) {
            if (! $dryRun) {
                $commande->client->notify(new RappelLivraisonNotification($commande));
                $envoyes++;
            }
            $bar->advance();
        }

        $bar->finish();
        $this->newLine();

        if ($dryRun) {
            $this->warn("Mode simulation — aucun rappel envoye.");
        } else {
            $this->info("$envoyes rappel(s) envoye(s) avec succes.");
        }

        return Command::SUCCESS;
    }
}
bashExécuter la commande
# Simulation — sans envoyer
php artisan darigo:rappels-livraison Casablanca --dry-run

# Production — avec delai de 45 minutes
php artisan darigo:rappels-livraison Casablanca --delai=45

# Appeler depuis un Controller ou un Job
Artisan::call('darigo:rappels-livraison', [
    'ville'   => 'Rabat',
    '--delai' => 20,
]);
Méthode de sortieCouleurUsage
$this->info()VertMessages normaux, succès
$this->warn()JauneAvertissements, simulations
$this->error()RougeErreurs, échecs
$this->line()BlancTexte neutre
$this->comment()Jaune clairCommentaires secondaires
$this->ask()—Demander une saisie interactive
$this->confirm()—Confirmation oui/non
$this->table()—Afficher un tableau formété

2
Task Scheduling — planifier les tâches DariGo
routes/console.php · schedule:run · cron unique · fréquences
ℹ Principe du Task Scheduler Laravel

Sans Laravel Scheduler : chaque tâche récurrente nécessite sa propre ligne cron sur le serveur. Avec le Scheduler : un seul cron * * * * * php artisan schedule:run qui s'exécute chaque minute et décide lui-même quelles tâches lancer selon leurs fréquences. Avantage : toute la planification est dans le code versionné, pas sur le serveur.

phproutes/console.php — planification DariGo (Laravel 11)
use Illuminate\Support\Facades\Schedule;

// T1 — Envoyer les rappels de livraison toutes les 30 minutes
Schedule::command('darigo:rappels-livraison Casablanca --delai=30')
    ->everyThirtyMinutes()
    ->withoutOverlapping()    // Ne pas lancer si l'execution precedente est encore en cours
    ->onSuccess(function () {
        \Log::info('Rappels livraison Casablanca envoyes');
    });

// T2 — Nettoyer les commandes expirees chaque nuit a 2h
Schedule::command('darigo:nettoyer-commandes --statut=annulee --jours=30')
    ->dailyAt('02:00')
    ->environments(['production'])  // Uniquement en production
    ->emailOutputOnFailure('ops@darigo.ma');

// T3 — Synchroniser les menus des restaurants toutes les heures
Schedule::command('darigo:synchroniser-menus')
    ->hourly()
    ->when(function () {
        // Ne lancer que si RabbitMQ est disponible
        try {
            return app('amqp')->isConnected();
        } catch (\Exception $e) {
            return false;
        }
    });

// T4 — Rapport journalier par email a 8h du matin
Schedule::command('darigo:rapport-journalier')
    ->dailyAt('08:00')
    ->weekdays()   // Uniquement en semaine

// T5 — Job en arriere-plan (Queue)
Schedule::job(new \App\Jobs\AnalyserPerformancesLivreurs)
    ->weekly()
    ->sundays()
    ->at('23:00');

// T6 — Closure simple
Schedule::call(function () {
    \App\Models\Commande::where('statut', 'en_attente')
        ->where('created_at', '<', now()->subHours(2))
        ->update(['statut' => 'expiree']);
})
    ->everyFifteenMinutes()
    ->name('expirer-commandes-anciennes')
    ->onOneServer();  // Sur un seul serveur si cluster
bashCron unique — ajouter sur le serveur de production
# Editer le crontab : crontab -e
* * * * * cd /var/www/darigo-api && php artisan schedule:run >> /dev/null 2>&1

# Tester localement sans attendre le cron
php artisan schedule:run          # Executer les taches dues maintenant
php artisan schedule:list         # Lister toutes les taches planifiees
php artisan schedule:test         # Tester une tache specifique interactivement
php artisan schedule:work         # Mode daemon (dev) — execute toutes les 60s
MéthodeFréquence
->everyMinute()Chaque minute
->everyFiveMinutes()Toutes les 5 min
->everyThirtyMinutes()Toutes les 30 min
->hourly()Chaque heure
->daily()Chaque jour à minuit
->dailyAt('08:00')Chaque jour à 8h
->weekly()Chaque dimanche à minuit
->monthly()1er du mois à minuit
->cron('0 9 * * 1-5')Cron personnalisé
L'option ->withoutOverlapping() est ajoutée à une tâche planifiée. Que se passe-t-il si l'exécution précédente n'est pas terminée quand la suivante est due ?
Une commande Artisan retourne Command::FAILURE au lieu de Command::SUCCESS. Quelle est la différence ?
Partie 3 — Laravel Avancé Collections · Cache Helpers Str/Arr
⏱ 20 min

Collections, Cache & Helpers

Les Collections Laravel (collect()) — méthodes chaînées puissantes pour manipuler des données, le système de Cache (Cache::remember()) pour optimiser les performances, et les Helpers globaux (Str::, Arr::, now()).

✓ Ce que vous allez apprendre
  • Créer une Collection avec collect() et depuis Eloquent
  • Méthodes essentielles : map(), filter(), groupBy(), pluck(), reduce(), each()
  • Méthodes d'agrégation : sum(), avg(), max(), count(), sortBy()
  • Cache drivers : file, database, Redis — Cache::remember()
  • Tags de cache, invalidation, Cache::tags()->flush()
  • Helpers Str:: et Arr:: les plus utiles

1
Collections Laravel — manipulation fluide des données
collect() · map() · filter() · groupBy() · pluck() · reduce()
ℹ Qu'est-ce qu'une Collection ?

Une Collection est un tableau PHP amélioré avec 100+ méthodes chaînables. Eloquent retourne toujours des Collections (pas des tableaux simples). collect([...]) transforme n'importe quel tableau en Collection. Les méthodes sont immutables — elles retournent une nouvelle Collection sans modifier l'originale.

phpCollections — exemples DariGo complets
$commandes = Commande::with('plats')->where('client_id', 5)->get();
// $commandes est une Collection d'objets Commande

// ---- TRANSFORMATION ----

// map() : transformer chaque element
$totaux = $commandes->map(function ($commande) {
    return [
        'id'     => $commande->id,
        'total'  => $commande->total,
        'statut' => $commande->statut,
    ];
});

// Syntaxe courte PHP 8 (arrow function)
$totaux = $commandes->map(fn($c) => ['id' => $c->id, 'total' => $c->total]);

// ---- FILTRAGE ----

// filter() : garder les elements qui passent le test
$commandesLivrees = $commandes->filter(fn($c) => $c->statut === 'livree');
$commandesPlus100 = $commandes->filter(fn($c) => $c->total > 100);

// reject() : inverse de filter
$commandesNonAnnulees = $commandes->reject(fn($c) => $c->statut === 'annulee');

// ---- RECHERCHE ----

// first() / last()
$premierLivree = $commandes->first(fn($c) => $c->statut === 'livree');

// contains()
$aDesCommandesEnAttente = $commandes->contains('statut', 'en_attente');

// ---- GROUPEMENT ET TRI ----

// groupBy() : grouper par valeur d'un champ
$parStatut = $commandes->groupBy('statut');
// ['livree' => Collection, 'en_attente' => Collection, ...]

// groupBy avec callback
$parJour = $commandes->groupBy(fn($c) => $c->created_at->format('Y-m-d'));

// sortBy() / sortByDesc()
$parTotal = $commandes->sortByDesc('total');

// ---- EXTRACTION ----

// pluck() : extraire un seul champ
$ids     = $commandes->pluck('id');          // Collection [1, 2, 3, ...]
$totaux  = $commandes->pluck('total', 'id'); // Collection [1 => 150.00, 2 => 75.50, ...]

// ---- AGREGATION ----

$total     = $commandes->sum('total');        // Somme des totaux
$moyenne   = $commandes->avg('total');        // Moyenne
$maximum   = $commandes->max('total');        // Maximum
$count     = $commandes->count();            // Nombre d'elements
$countLivrees = $commandes->where('statut', 'livree')->count();

// ---- REDUCE — calculer un chiffre d'affaires ----
$caParRestaurant = $commandes->reduce(function ($carry, $commande) {
    $carry[$commande->restaurant_id] = ($carry[$commande->restaurant_id] ?? 0) + $commande->total;
    return $carry;
}, []);
// ['1' => 1250.00, '2' => 890.50, ...]

// ---- DIVERS ----
$commandes->each(fn($c) => $this->info("Commande {$c->id} — {$c->total} MAD"));
$tableau = $commandes->toArray();   // Convertir en tableau PHP
$json    = $commandes->toJson();    // Convertir en JSON
$unique  = $commandes->unique('restaurant_id'); // Supprimer les doublons

2
Cache Laravel — optimiser les performances DariGo
Cache::remember() · put() · get() · forget() · tags() · Redis
ℹ Pourquoi le cache est critique pour DariGo

La liste des restaurants est demandée 10 000 fois par heure. Sans cache : 10 000 requêtes MySQL. Avec Cache::remember('restaurants', 300, ...) : 1 requête MySQL toutes les 5 minutes, 9 999 lectures depuis la mémoire Redis. Généralement 100× plus rapide et libère la BDD pour les écritures (commandes).

phpCache — patterns DariGo
use Illuminate\Support\Facades\Cache;

// ---- PATTERN remember() : lire ou calculer ----
// Si la cle existe en cache : retourner la valeur
// Sinon : executer la closure, stocker le resultat, retourner
$restaurants = Cache::remember('restaurants.casablanca', 300, function () {
    return Restaurant::where('ville', 'Casablanca')
        ->where('actif', true)
        ->with('plats')
        ->orderBy('note', 'desc')
        ->get();
});
// 300 secondes = 5 minutes | Cle unique par ville

// rememberForever() : sans expiration
$villes = Cache::rememberForever('villes.liste', function () {
    return ['Casablanca', 'Rabat', 'Marrakech', 'Fes', 'Agadir'];
});

// ---- OPERATIONS MANUELLES ----
Cache::put('commande.123.statut', 'en_livraison', now()->addHours(2));
$statut  = Cache::get('commande.123.statut', 'inconnu');  // Valeur par defaut
$exists  = Cache::has('commande.123.statut');
Cache::forget('commande.123.statut');   // Supprimer une cle
Cache::flush();                         // Vider tout le cache — DANGER en production !

// ---- CACHE TAGS (Redis ou Memcached uniquement) ----
// Tags permettent d'invalider un groupe de cles en une commande
Cache::tags(['restaurants', 'casablanca'])->put(
    'restaurants.casablanca.page.1', $data, 300
);

// Invalider tout ce qui concerne les restaurants de Casablanca
Cache::tags(['restaurants', 'casablanca'])->flush();

// Pattern : invalider quand un restaurant est modifie
// Dans RestaurantObserver::updated()
Cache::tags(['restaurants', strtolower($restaurant->ville)])->flush();

// ---- CACHE ATOMIQUE — eviter les race conditions ----
$valeur = Cache::lock('commande.paiement.123')->get(function () {
    // Section critique — un seul processus a la fois
    return $this->traiterPaiement(123);
});

// ---- CONFIGURATION .env ----
// CACHE_STORE=redis    → Redis (production recommandee)
// CACHE_STORE=file     → Fichiers (developpement)
// CACHE_STORE=database → Table cache en BDD

3
Helpers globaux — Str::, Arr::, now()
Str::slug · Str::uuid · Arr::get · now() · Carbon
phpHelpers Str:: — chaînes DariGo
use Illuminate\Support\Str;

// Slug pour les URLs
Str::slug('Tajine Poulet Citron Confit');   // 'tajine-poulet-citron-confit'
Str::slug('Restaurant Al Baraka', '_');      // 'restaurant_al_baraka'

// UUID pour les references de commande
$ref = Str::uuid();                          // '550e8400-e29b-41d4-a716-446655440000'
$ref = (string) Str::orderedUuid();          // UUID ordonne par temps — meilleur pour BDD

// Manipulation de chaines
Str::upper('casablanca');                    // 'CASABLANCA'
Str::title('livraison rapide darigo');       // 'Livraison Rapide Darigo'
Str::camel('restaurant_name');               // 'restaurantName'
Str::snake('RestaurantName');                // 'restaurant_name'
Str::limit('Description tres longue...', 50, '...'); // Tronquer a 50 chars

Str::startsWith('DariGo-CMD-001', 'DariGo');  // true
Str::endsWith('photo.jpg', ['.jpg', '.png']); // true
Str::contains('commande en livraison', 'livraison'); // true
Str::replace('ancien', 'nouveau', $texte);

// Generer des chaines aleatoires
Str::random(32);                             // Cle API aleatoire
Str::password(16);                           // Mot de passe securise

// Fluent strings (Laravel 8+)
$nomFichier = Str::of('Restaurant Al Baraka')
    ->lower()
    ->slug()
    ->append('.jpg');                        // 'restaurant-al-baraka.jpg'
phpHelpers Arr:: et Carbon now()
use Illuminate\Support\Arr;

$commande = [
    'client' => ['nom' => 'Karim', 'email' => 'karim@darigo.ma'],
    'total'  => 150.00,
];

// Arr::get — acces a un champ imbrique avec notation pointee
$email = Arr::get($commande, 'client.email', 'N/A');   // 'karim@darigo.ma'
$tel   = Arr::get($commande, 'client.tel', 'Absent');   // 'Absent' (defaut)

// Arr::set — modifier un champ imbrique
Arr::set($commande, 'client.ville', 'Casablanca');

// Arr::has — verifier l'existence
Arr::has($commande, 'client.email');     // true
Arr::has($commande, 'client.password'); // false

// Arr::only / Arr::except
$public = Arr::only($commande, ['total', 'client.nom']);
$safe   = Arr::except($commande, ['client.password']);

// Carbon / now() — dates Laravel
$maintenant   = now();                              // Carbon instance
$demain       = now()->addDay();
$hier         = now()->subDay();
$lundi        = now()->startOfWeek();
$finMois      = now()->endOfMonth();

// Formatage
now()->format('d/m/Y H:i');                        // '15/11/2024 14:23'
now()->isoFormat('dddd D MMMM YYYY');              // 'Vendredi 15 Novembre 2024'
now()->diffForHumans();                             // 'il y a quelques secondes'
now()->addMinutes(30)->diffForHumans();             // 'dans 30 minutes'

// Comparaisons
now()->isAfter('2024-01-01');            // true
$commande->created_at->lt(now()->subHours(2)); // Commande de plus de 2h
$restaurants = Cache::remember('restaurants', 300, fn() => Restaurant::all()). Un nouveau restaurant est créé. Combien de temps faut-il attendre pour qu'il apparaisse dans la liste ?
Quelle différence entre $collection->map() et $collection->each() ?
Partie 3 — Laravel Avancé Events · Queues Broadcasting
⏱ 22 min

Événements, Files d'attente & Broadcasting

Système Events/Listeners pour découpler la logique, Jobs et Queue drivers (database/Redis) pour le traitement asynchrone, Broadcasting temps-réel avec Pusher/Laravel Echo pour les notifications instantanées DariGo.

✓ Ce que vous allez apprendre
  • Créer des Events et Listeners avec php artisan make:event / make:listener
  • Enregistrer les listeners dans EventServiceProvider
  • Déclencher un événement avec event()
  • Jobs et Queues — php artisan make:job, dispatch(), queue:work
  • Queue drivers : sync (dev), database, redis (prod)
  • Broadcasting — Pusher + Laravel Echo pour les notifications temps-réel

1
Events & Listeners — découpler la logique DariGo
make:event · make:listener · EventServiceProvider · event()
ℹ Pourquoi les Events — principe du découplage

Sans Events, le CommandeController appelle directement : $sms->envoyer(), $email->envoyer(), $livreur->notifier(), $analytics->enregistrer(). Le Controller connait trop de choses — difficile à tester et à maintenir. Avec Events : le Controller déclenche event(new CommandeCreee($commande)) et les Listeners s'abonnent indépendamment. Ajouter une nouvelle action (badge fidélité) = créer un nouveau Listener sans toucher au Controller.

bashGénérer les événements DariGo
php artisan make:event CommandeCreee
php artisan make:event StatutCommandeModifie
php artisan make:listener EnvoyerEmailConfirmation --event=CommandeCreee
php artisan make:listener NotifierLivreur --event=CommandeCreee
php artisan make:listener MettreAJourAnalytics --event=CommandeCreee
phpapp/Events/CommandeCreee.php
<?php

namespace App\Events;

use App\Models\Commande;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class CommandeCreee
{
    use Dispatchable, SerializesModels;
    // SerializesModels : serialize/deserialize correctement les modeles Eloquent en queue

    public function __construct(public readonly Commande $commande)
    {
        // PHP 8 : constructor property promotion — public $commande est cree automatiquement
    }
}
phpapp/Listeners/EnvoyerEmailConfirmation.php
<?php

namespace App\Listeners;

use App\Events\CommandeCreee;
use App\Mail\ConfirmationCommandeMail;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Mail;

// ShouldQueue : ce listener s'execute en arriere-plan (queue)
// Sans ShouldQueue : execution synchrone (ralentit la reponse HTTP)
class EnvoyerEmailConfirmation implements ShouldQueue
{
    public $queue = 'emails';      // File d'attente specifique
    public $tries = 3;             // Nombre de tentatives si echec
    public $backoff = [10, 30, 60]; // Delais entre tentatives (secondes)

    public function handle(CommandeCreee $event): void
    {
        $commande = $event->commande;

        Mail::to($commande->client->email)
            ->send(new ConfirmationCommandeMail($commande));
    }

    // Que faire si toutes les tentatives echouent ?
    public function failed(CommandeCreee $event, \Throwable $exception): void
    {
        \Log::error("Email confirmation echoue pour commande {$event->commande->id}", [
            'error' => $exception->getMessage(),
        ]);
    }
}
phpEnregistrement dans EventServiceProvider
// app/Providers/EventServiceProvider.php (ou AppServiceProvider::boot())
protected $listen = [
    CommandeCreee::class => [
        EnvoyerEmailConfirmation::class,    // Queue emails
        NotifierLivreur::class,             // Queue livraisons
        MettreAJourAnalytics::class,        // Queue analytics
    ],
    StatutCommandeModifie::class => [
        NotifierClientStatut::class,
        BroadcastStatutChange::class,       // Temps reel
    ],
];

// Declencher l'evenement dans le Controller
public function store(StoreCommandeRequest $request): JsonResponse
{
    $commande = Commande::create([...]);

    // Une ligne — les 3 listeners se declenchent automatiquement
    event(new CommandeCreee($commande));
    // ou : CommandeCreee::dispatch($commande);

    return response()->json(['message' => 'Commande creee'], 201);
}

2
Jobs & Queues — traitement asynchrone
make:job · dispatch() · queue:work · failed_jobs
bashGénérer et configurer les Jobs
php artisan make:job TraiterPaiementLivraison
php artisan make:job EnvoyerRapportLivreur

# Creer la table de queue (si QUEUE_CONNECTION=database)
php artisan queue:table
php artisan migrate

# Lancer le worker
php artisan queue:work                   # Traiter tous les jobs
php artisan queue:work --queue=emails    # Traiter uniquement la file emails
php artisan queue:work --tries=3         # Max 3 tentatives par job
php artisan queue:listen                 # Ecouter et recharger (dev)

# Gestion des jobs echoues
php artisan queue:failed                 # Lister les jobs echoues
php artisan queue:retry all              # Relancer tous les jobs echoues
php artisan queue:flush                  # Supprimer les jobs echoues
phpapp/Jobs/TraiterPaiementLivraison.php
<?php

namespace App\Jobs;

use App\Models\Commande;
use App\Services\PaiementService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class TraiterPaiementLivraison implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;            // 3 tentatives max
    public int $timeout = 60;         // Timeout par tentative (secondes)
    public array $backoff = [5, 15, 30]; // Delais entre tentatives

    public function __construct(private readonly Commande $commande)
    {}

    public function handle(PaiementService $paiement): void
    {
        // PaiementService est injecte automatiquement par le container
        $resultat = $paiement->traiter($this->commande);

        $this->commande->update([
            'paiement_statut' => $resultat->statut,
            'paiement_ref'    => $resultat->reference,
        ]);
    }

    public function failed(\Throwable $e): void
    {
        \Log::error("Paiement echoue pour commande {$this->commande->id}: " . $e->getMessage());
        $this->commande->update(['paiement_statut' => 'echec']);
    }
}

// Dispatcher le job depuis n'importe ou
TraiterPaiementLivraison::dispatch($commande);

// Avec delai
TraiterPaiementLivraison::dispatch($commande)->delay(now()->addMinutes(5));

// Avec file specifique
TraiterPaiementLivraison::dispatch($commande)->onQueue('paiements');

// Job en batch
Bus::batch([
    new TraiterPaiementLivraison($commande1),
    new TraiterPaiementLivraison($commande2),
])->dispatch();

3
Broadcasting temps-réel avec Pusher
ShouldBroadcast · Pusher · Laravel Echo · Channel
ℹ Broadcasting — notifications temps-réel dans DariGo

Le client commande via l'app mobile DariGo. Le livreur accepte la mission. L'app mobile doit afficher "Livreur en route" instantanément — sans polling toutes les 5 secondes. Broadcasting + Pusher + Laravel Echo = WebSocket push du serveur vers le client.

bashInstallation
composer require pusher/pusher-php-server
npm install --save-dev laravel-echo pusher-js

# .env
BROADCAST_DRIVER=pusher
PUSHER_APP_ID=xxxxxxxx
PUSHER_APP_KEY=xxxxxxxx
PUSHER_APP_SECRET=xxxxxxxx
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=eu
phpapp/Events/StatutLivraisonChange.php — broadcast
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class StatutLivraisonChange implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(
        public readonly Commande $commande,
        public readonly string   $statut,
        public readonly ?string  $nomLivreur = null,
    ) {}

    // Canal prive par client — securise
    public function broadcastOn(): PrivateChannel
    {
        return new PrivateChannel('commandes.' . $this->commande->client_id);
    }

    // Nom de l'evenement cote JavaScript
    public function broadcastAs(): string
    {
        return 'statut.change';
    }

    // Donnees envoyees au client
    public function broadcastWith(): array
    {
        return [
            'commande_id'  => $this->commande->id,
            'statut'       => $this->statut,
            'nom_livreur'  => $this->nomLivreur,
            'timestamp'    => now()->toIsoString(),
        ];
    }
}
Un Listener implémente ShouldQueue. QUEUE_CONNECTION=sync dans le .env de développement. Comment ce Listener se comporte-t-il ?
Pourquoi utiliser des Events/Listeners plutôt qu'appeler directement les services depuis le Controller ?
Partie 3 — Laravel Avancé Storage · Mail Notifications
⏱ 20 min

Fichiers, Mail & Notifications

La facade Storage:: pour gérer les fichiers (local/public/S3), les classes Mailable pour envoyer des emails HTML avec Blade, et le système de Notifications multi-canaux (email, SMS, base de données).

✓ Ce que vous allez apprendre
  • Facade Storage:: — disques (local, public, s3), put/get/delete/url
  • Lien symbolique storage:link pour rendre les fichiers publics
  • Mailable — php artisan make:mail, template Blade, Mail::to()->send()
  • Mailtrap pour tester les emails en développement
  • Notifications — php artisan make:notification, canaux mail/database/SMS
  • Notifications via $user->notify() et Notification::send()

1
Storage — gestion des fichiers DariGo
Storage:: · disques · put/get/url · storage:link · S3
phpStorage — opérations complètes
use Illuminate\Support\Facades\Storage;

// ---- ECRITURE ----
// Disque 'local' : storage/app/
Storage::put('fichiers/rapport.csv', $contenu);

// Disque 'public' : storage/app/public/ → accessible via /storage/
Storage::disk('public')->put('restaurants/menus/menu-5.pdf', $contenu);
Storage::disk('public')->putFile('photos/plats', $request->file('photo'));

// Generer un nom unique
$path = Storage::disk('public')->putFile('photos/restaurants', $file);
// Retourne : 'photos/restaurants/abc123def456.jpg'

// ---- LECTURE ----
$contenu  = Storage::get('fichiers/rapport.csv');
$url      = Storage::disk('public')->url('photos/restaurants/abc123.jpg');
// http://api.darigo.ma/storage/photos/restaurants/abc123.jpg

// URL temporaire (S3 — lien expirable)
$urlTemp  = Storage::disk('s3')->temporaryUrl('factures/fact-001.pdf', now()->addHours(1));

// ---- INFORMATIONS ----
$exists   = Storage::exists('fichiers/rapport.csv');      // true/false
$taille   = Storage::size('fichiers/rapport.csv');         // octets
$modifie  = Storage::lastModified('fichiers/rapport.csv'); // timestamp Unix
$fichiers = Storage::files('photos/restaurants');          // Lister les fichiers
$dossiers = Storage::directories('photos');                // Lister les dossiers

// ---- SUPPRESSION ----
Storage::delete('fichiers/rapport.csv');
Storage::delete(['photo1.jpg', 'photo2.jpg']);
Storage::deleteDirectory('temp');  // Supprimer un dossier entier

// ---- COPIE ET DEPLACEMENT ----
Storage::copy('photos/orig.jpg', 'photos/copie.jpg');
Storage::move('temp/upload.jpg', 'photos/restaurants/restaurant-5.jpg');
bashLien symbolique — rendre les fichiers publics
# Creer le lien symbolique : public/storage → storage/app/public
php artisan storage:link

# Resultat : les fichiers dans storage/app/public/ sont accessibles via :
# https://api.darigo.ma/storage/photos/restaurants/abc123.jpg

# Pour plusieurs liens (config/filesystems.php)
'links' => [
    public_path('storage') => storage_path('app/public'),
    public_path('uploads') => storage_path('app/uploads'),  // Lien suppl.
],

2
Mail — envoyer des emails HTML avec Blade
make:mail · Mailable · envelope() · content() · Mailtrap
bashGénérer les Mailables DariGo
php artisan make:mail ConfirmationCommandeMail
php artisan make:mail NotificationStatutLivraison
php artisan make:mail BienvenueDariGo
phpapp/Mail/ConfirmationCommandeMail.php
<?php

namespace App\Mail;

use App\Models\Commande;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

class ConfirmationCommandeMail extends Mailable implements ShouldQueue
{
    use Queueable, SerializesModels;

    public function __construct(public readonly Commande $commande)
    {}

    // Objet et expediteur
    public function envelope(): Envelope
    {
        return new Envelope(
            from: new Address('noreply@darigo.ma', 'DariGo — Livraison'),
            subject: "Confirmation de votre commande #{$this->commande->id}",
        );
    }

    // Template Blade utilise
    public function content(): Content
    {
        return new Content(
            view: 'emails.commandes.confirmation',
            // Variables disponibles dans le template
            with: [
                'commande'    => $this->commande,
                'client'      => $this->commande->client,
                'lignes'      => $this->commande->lignes,
                'urlSuivi'    => route('commandes.show', $this->commande->id),
            ]
        );
    }

    // Pieces jointes
    public function attachments(): array
    {
        return [
            // Attachment::fromPath(storage_path('app/factures/fact-' . $this->commande->id . '.pdf'))
            //     ->as('Facture-DariGo.pdf')
            //     ->withMime('application/pdf'),
        ];
    }
}

// Envoyer l'email
Mail::to($commande->client->email)
    ->cc('support@darigo.ma')
    ->send(new ConfirmationCommandeMail($commande));

// Tester sans envoyer (preview dans le navigateur)
// Route::get('/mail-preview', fn() => new ConfirmationCommandeMail(Commande::first()));
phpresources/views/emails/commandes/confirmation.blade.php
<!-- Template email HTML -->
<x-mail::message>

# Votre commande est confirmée !

Bonjour **{{ $client->prenom }}**,

Votre commande **#{{ $commande->id }}** a bien été reçue.

<x-mail::table>
| Plat | Prix |
|:-----|-----:|
@foreach ($lignes as $ligne)
| {{ $ligne->nom_plat }} × {{ $ligne->quantite }} | {{ number_format($ligne->sous_total, 2) }} MAD |
@endforeach
| **Total** | **{{ number_format($commande->total, 2) }} MAD** |
</x-mail::table>

<x-mail::button :url="$urlSuivi" color="green">
Suivre ma commande
</x-mail::button>

Merci de faire confiance à **DariGo** !

</x-mail::message>

3
Notifications multi-canaux
make:notification · toMail · toDatabase · toNexmo · $user->notify()
ℹ Différence Mailable vs Notification

Un Mailable = un email. Une Notification = un message qui peut être envoyé sur plusieurs canaux simultanémément (email + SMS + notification push + BDD). Préférer les Notifications pour les alertes utilisateur (livraison en retard, commande confirmée).

phpapp/Notifications/CommandeLivreeNotification.php
<?php

namespace App\Notifications;

use App\Models\Commande;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class CommandeLivreeNotification extends Notification implements ShouldQueue
{
    use Queueable;

    public function __construct(private readonly Commande $commande) {}

    // Canaux utilises : mail ET base de donnees
    public function via(object $notifiable): array
    {
        return ['mail', 'database'];
        // Autres canaux : 'vonage' (SMS), 'slack', 'broadcast'
    }

    // Canal email
    public function toMail(object $notifiable): MailMessage
    {
        return (new MailMessage)
            ->subject("Votre commande #{$this->commande->id} a été livrée !")
            ->greeting("Bonjour {$notifiable->prenom} !")
            ->line("Votre commande de {$this->commande->total} MAD a bien été livrée.")
            ->action('Voir mes commandes', route('commandes.index'))
            ->line('Merci d\'avoir choisi DariGo !')
            ->salutation('L\'équipe DariGo');
    }

    // Canal base de donnees (table notifications)
    public function toDatabase(object $notifiable): array
    {
        return [
            'commande_id'   => $this->commande->id,
            'message'       => "Commande #{$this->commande->id} livrée",
            'total'         => $this->commande->total,
        ];
    }
}

// Creer la table notifications
// php artisan notifications:table && php artisan migrate

// Envoyer la notification
$commande->client->notify(new CommandeLivreeNotification($commande));

// Envoyer a plusieurs utilisateurs
Notification::send($livreurs, new NouvelleCommandeDisponible($commande));

// Lire les notifications non lues depuis le modele User
$user->unreadNotifications;       // Collection
$user->notifications;             // Toutes
$user->unreadNotifications->markAsRead();
php artisan storage:link doit être exécuté sur le serveur de production après chaque git pull. Pourquoi cette commande ne suffit-elle pas en production si le disque de la config est s3 ?
Partie 3 — Laravel Avancé Packages · Contrats Localisation AR/FR
⏱ 18 min

Packages, Contrats & Localisation

Service Providers et Facades pour créer des packages réutilisables, Contracts (interfaces) pour le couplage faible, et localisation multi-langue (français/arabe) adaptée au marché marocain.

✓ Ce que vous allez apprendre
  • Service Container — binding, singleton, app()->make()
  • Service Providers — register() vs boot()
  • Facades — créer une Facade pour son propre service
  • Contracts (interfaces) — implements SomeContract
  • Localisation — lang/fr/, lang/ar/, __('messages.bienvenue')
  • Switcher de langue dans DariGo — URL paramètre ou session

1
Service Container & Service Providers
bind() · singleton() · register() · boot()
ℹ Service Container — le cœur de Laravel

Le Service Container est un conteneur d'injection de dépendances. Il résout automatiquement les dépendances des classes : quand Laravel instancie un Controller qui dépend d'un PaiementService, il crée automatiquement PaiementService et l'injecte. Le Container sait comment créer chaque service grâce aux bindings enregistrés dans les Service Providers.

phpService Provider DariGo — app/Providers/DariGoServiceProvider.php
<?php

namespace App\Providers;

use App\Contracts\PaiementContract;
use App\Contracts\SMSContract;
use App\Services\CMIPaiementService;
use App\Services\OrangeSMSService;
use Illuminate\Support\ServiceProvider;

class DariGoServiceProvider extends ServiceProvider
{
    /**
     * register() : UNIQUEMENT des bindings dans le container
     * Ne jamais utiliser d'autres services ici — ils peuvent ne pas etre charges
     */
    public function register(): void
    {
        // bind() : nouvelle instance a chaque appel
        $this->app->bind(PaiementContract::class, CMIPaiementService::class);

        // singleton() : meme instance pour toute la requete
        $this->app->singleton(SMSContract::class, function ($app) {
            return new OrangeSMSService(
                apiKey:  config('services.orange_sms.key'),
                sender:  'DARIGO'
            );
        });

        // Alias court
        $this->app->alias(SMSContract::class, 'darigo.sms');
    }

    /**
     * boot() : executer apres TOUS les providers registers
     * Ici on peut utiliser les autres services
     */
    public function boot(): void
    {
        // Enregistrer une commande Artisan
        if ($this->app->runningInConsole()) {
            $this->commands([
                \App\Console\Commands\EnvoyerRappelsLivraison::class,
                \App\Console\Commands\NettoyerCommandesExpirees::class,
            ]);
        }

        // Enregistrer des macros globales
        \Illuminate\Http\Response::macro('darigo', function ($data, $msg = 'OK', $status = 200) {
            return response()->json(['succes' => true, 'message' => $msg, 'data' => $data], $status);
        });
    }
}
phpUtilisation du container — injection automatique
// Injection automatique dans un Controller (le plus courant)
class CommandeController extends Controller
{
    public function __construct(
        private readonly PaiementContract $paiement,   // Container injecte automatiquement
        private readonly SMSContract       $sms
    ) {}

    public function store(StoreCommandeRequest $request): JsonResponse
    {
        $commande = Commande::create([...]);
        $this->paiement->traiter($commande);    // CMIPaiementService
        $this->sms->envoyer('+212600000000', 'Commande confirmee !');
        return response()->darigo($commande, 'Commande creee', 201);
    }
}

// Resoudre manuellement (dans un script Artisan par exemple)
$sms = app(SMSContract::class);
$sms = app('darigo.sms');   // Via alias

2
Contracts — interfaces pour le couplage faible
Interface · implements · Testabilite · Interchangeabilite
phpContracts DariGo — interfaces de services
<?php
// app/Contracts/PaiementContract.php

namespace App\Contracts;

use App\Models\Commande;

interface PaiementContract
{
    public function traiter(Commande $commande): PaiementResultat;
    public function rembourser(string $reference): bool;
    public function verifier(string $reference): string; // statut
}

// app/Services/CMIPaiementService.php — implémentation réelle
class CMIPaiementService implements PaiementContract
{
    public function traiter(Commande $commande): PaiementResultat
    {
        // Integration API CMI (Centre Monétique Interbancaire Maroc)
        $response = Http::post('https://api.cmi.ma/paiements', [...]);
        return new PaiementResultat($response->json());
    }
    // ...
}

// app/Services/FakePaiementService.php — pour les tests
class FakePaiementService implements PaiementContract
{
    public function traiter(Commande $commande): PaiementResultat
    {
        // Toujours reussir en tests
        return new PaiementResultat(['statut' => 'succes', 'ref' => 'TEST-' . $commande->id]);
    }
    // ...
}

// Dans les tests : substituer l'implementation
$this->app->bind(PaiementContract::class, FakePaiementService::class);

3
Localisation — DariGo en arabe et français
lang/ar/ · lang/fr/ · __() · App::setLocale() · pluralisation
bashStructure des fichiers de traduction
lang/
├── fr/
│   ├── messages.php     # Traductions generales
│   ├── validation.php   # Messages de validation
│   └── auth.php         # Messages d'authentification
└── ar/
    ├── messages.php
    └── validation.php
phplang/fr/messages.php
<?php

return [
    'bienvenue'    => 'Bienvenue sur DariGo !',
    'commande_ok'  => 'Votre commande a été passée avec succès.',
    'livraison'    => 'Livraison en cours vers :adresse',  // :adresse = param

    // Pluralisation
    'plat'         => '{0} Aucun plat|{1} :count plat|[2,*] :count plats',
    'commande'     => '{0} Aucune commande|{1} :count commande|[2,*] :count commandes',
];
phplang/ar/messages.php
<?php

return [
    'bienvenue'    => 'مرحباً بكم في DariGo !',
    'commande_ok'  => 'تم' . ' تسجيل طلبك بنجاح.',
    'livraison'    => 'التوصيل جاري إلى :adresse',
];
phpUtilisation dans le code
// Recuperer une traduction
__('messages.bienvenue');                               // 'Bienvenue sur DariGo !'
__('messages.livraison', ['adresse' => '14 rue Hassan II, Casablanca']);

// Pluralisation
trans_choice('messages.plat', 0);   // 'Aucun plat'
trans_choice('messages.plat', 1);   // '1 plat'
trans_choice('messages.plat', 5);   // '5 plats'

// Changer la langue pour la requete courante
app()->setLocale('ar');
app()->getLocale();   // 'ar'

// Dans un middleware LocaleMiddleware
class LocaleMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        $locale = $request->header('Accept-Language', 'fr');
        $supported = ['fr', 'ar', 'en'];
        app()->setLocale(in_array($locale, $supported) ? $locale : 'fr');
        return $next($request);
    }
}

// Dans Blade
<html lang="{{ app()->getLocale() }}" dir="{{ app()->getLocale() === 'ar' ? 'rtl' : 'ltr' }}">
ℹ DariGo multilingue — configuration recommandée

DariGo cible des utilisateurs marocains : francophone (Casablanca, Rabat) et arabophone (reste du Maroc). Approche recommandée pour l'API : header Accept-Language: ar ou Accept-Language: fr. Laravel lit ce header et sélectionne la bonne langue. L'application mobile envoie la préférence de l'utilisateur dans chaque requête.

Quelle est la différence entre register() et boot() dans un Service Provider ?
DariGo utilise PaiementContract (interface) au lieu de CMIPaiementService (classe concrète) dans les Controllers. Si l'équipe décide de changer de prestataire de paiement (de CMI à HPS), combien de fichiers faut-il modifier ?
Partie 4 — Sécurité JWT · Passport · Sanctum API Tokens · Cookies
⏱ 22 min

JWT, Passport & Sanctum — Authentification API

Comparatif des trois systèmes d'authentification API de Laravel, installation et configuration complète de Sanctum pour DariGo, tokens personnels, createToken(), middleware auth:sanctum et auth SPA avec cookies.

✓ Ce que vous allez apprendre
  • Comparatif JWT / Passport / Sanctum — lequel choisir pour DariGo
  • Installer et configurer Sanctum en 4 étapes
  • HasApiTokens sur le modèle User — createToken()
  • Routes POST /auth/register, POST /auth/login, POST /auth/logout
  • Middleware auth:sanctum pour protéger les routes API
  • Abilities (scopes) — tokens limités par permission

1
Comparatif JWT / Passport / Sanctum
Stateless · Overhead · Revocation · SPA
ℹ Pourquoi ne pas utiliser l'auth session classique pour DariGo ?

L'auth Laravel par défaut (session + cookie) est conçue pour les navigateurs. DariGo est une API consommée par une app mobile React Native — elle ne gère pas les cookies de session. Il faut un système de tokens transmis dans le header Authorization: Bearer xxx.

API publique (Twitter, GitHub)
CritèreJWT (tymon/jwt-auth)Passport (OAuth2)Sanctum (recommandé)
ComplexitéMoyenne — librairie tierceÉlevée — OAuth2 completFaible — intégré Laravel
Token statelessOui — pas de BDDOui (access token)Non — token en BDD
RévocationComplexe — blacklist nécessaireNativeNative — supprimer de la BDD
SPA + MobileMobile uniquementToutMobile + SPA (double mode)
Abilities (scopes)Claims JWTScopes OAuth2Abilities Sanctum
Cas d'usageMicroservices statelessApps internes, SaaS
DariGoPossible mais sur-engineeredTrop complexe pour usage interneRecommandé — simple et puissant

2
Installation et configuration de Sanctum
composer require · HasApiTokens · migration personal_access_tokens · middleware
bashInstallation Sanctum — 4 étapes
# Etape 1 : Installer Sanctum (inclus par defaut dans Laravel 11)
composer require laravel/sanctum

# Etape 2 : Publier la config et la migration
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"

# Etape 3 : Executer la migration
php artisan migrate
# Cree la table personal_access_tokens

# Etape 4 : Verifier la config
# config/sanctum.php — expiration, middleware, stateful domains
phpapp/Models/User.php — ajouter HasApiTokens
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;  // Import Sanctum

class User extends Authenticatable
{
    use HasApiTokens,  // Ajouter cette trait
        Notifiable;

    protected $fillable = [
        'prenom', 'nom', 'email', 'password',
        'telephone',
        'role',        // 'client' | 'restaurateur' | 'livreur' | 'admin'
        'ville',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password'          => 'hashed',  // Hash automatique a l'assignation
        ];
    }
}

3
AuthController complet — Register, Login, Logout
createToken() · abilities · plainTextToken · currentAccessToken()
phpapp/Http/Controllers/AuthController.php — DariGo complet
<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;

class AuthController extends Controller
{
    // ── POST /api/auth/register ──────────────────────────────
    public function register(Request $request): JsonResponse
    {
        $donnees = $request->validate([
            'prenom'    => ['required', 'string', 'max:50'],
            'nom'       => ['required', 'string', 'max:50'],
            'email'     => ['required', 'email:rfc,dns', 'unique:users,email'],
            'telephone' => ['required', 'regex:/^(0[5-7][0-9]{8})$/'],
            'password'  => ['required', 'confirmed',
                            Password::min(8)->mixedCase()->numbers()],
            'role'      => ['sometimes', 'in:client,restaurateur,livreur'],
            'ville'     => ['required', 'string'],
        ]);

        // 'password' => 'hashed' dans le cast User : hashage automatique
        $user = User::create([
            ...$donnees,
            'role' => $donnees['role'] ?? 'client',
        ]);

        // Creer le token initial
        $token = $user->createToken(
            'darigo-' . $user->role,           // Nom du token (pour identifier)
            $this->abilitiesPourRole($user->role) // Abilities selon le role
        );

        return response()->json([
            'message'    => 'Compte cree avec succes',
            'user'       => $user,
            'token'      => $token->plainTextToken, // Affiche UNE SEULE FOIS
            'token_type' => 'Bearer',
        ], 201);
    }

    // ── POST /api/auth/login ─────────────────────────────────
    public function login(Request $request): JsonResponse
    {
        $request->validate([
            'email'    => ['required', 'email'],
            'password' => ['required', 'string'],
        ]);

        if (! Auth::attempt($request->only('email', 'password'))) {
            return response()->json([
                'message' => 'Identifiants incorrects',
            ], 401);
        }

        $user = Auth::user();

        // Revoquer les anciens tokens du meme appareil (optionnel)
        $user->tokens()->where('name', 'darigo-' . $user->role)->delete();

        $token = $user->createToken(
            'darigo-' . $user->role,
            $this->abilitiesPourRole($user->role)
        );

        return response()->json([
            'message'    => 'Connexion reussie',
            'user'       => [
                'id'        => $user->id,
                'prenom'    => $user->prenom,
                'email'     => $user->email,
                'role'      => $user->role,
                'ville'     => $user->ville,
            ],
            'token'      => $token->plainTextToken,
            'token_type' => 'Bearer',
            'abilities'  => $this->abilitiesPourRole($user->role),
        ]);
    }

    // ── POST /api/auth/logout ────────────────────────────────
    public function logout(Request $request): JsonResponse
    {
        // Revoquer uniquement le token courant
        $request->user()->currentAccessToken()->delete();

        return response()->json(['message' => 'Deconnexion reussie']);
    }

    // ── POST /api/auth/logout-all ────────────────────────────
    public function logoutAll(Request $request): JsonResponse
    {
        // Revoquer TOUS les tokens (deconnexion de tous les appareils)
        $request->user()->tokens()->delete();

        return response()->json(['message' => 'Deconnexion de tous les appareils']);
    }

    // ── GET /api/auth/me ─────────────────────────────────────
    public function me(Request $request): JsonResponse
    {
        return response()->json([
            'user'      => $request->user(),
            'abilities' => $request->user()->currentAccessToken()->abilities,
        ]);
    }

    // Abilities par role DariGo
    private function abilitiesPourRole(string $role): array
    {
        return match($role) {
            'admin'         => ['*'],  // Tout
            'restaurateur'  => ['restaurants:manage', 'plats:manage', 'commandes:view'],
            'livreur'       => ['livraisons:view', 'livraisons:update'],
            default         => ['restaurants:view', 'plats:view', 'commandes:create', 'commandes:view'],
        };
    }
}
phproutes/api.php — routes auth + protection Sanctum
// Routes publiques
Route::prefix('auth')->group(function () {
    Route::post('/register', [AuthController::class, 'register']);
    Route::post('/login',    [AuthController::class, 'login']);
});

// Routes protegees par Sanctum
Route::middleware('auth:sanctum')->group(function () {
    Route::post('/auth/logout',     [AuthController::class, 'logout']);
    Route::post('/auth/logout-all', [AuthController::class, 'logoutAll']);
    Route::get('/auth/me',          [AuthController::class, 'me']);

    Route::apiResource('restaurants', RestaurantController::class);
    Route::apiResource('commandes',   CommandeController::class);
    Route::apiResource('livraisons',  LivraisonController::class);
});
bashTester avec Postman — flux complet
# 1 — Register
POST /api/auth/register
Content-Type: application/json
{
  "prenom":"Karim","nom":"Alaoui","email":"karim@darigo.ma",
  "telephone":"0612345678","password":"Karim2024!","password_confirmation":"Karim2024!",
  "role":"client","ville":"Casablanca"
}
# → 201 + token

# 2 — Login
POST /api/auth/login
{ "email":"karim@darigo.ma", "password":"Karim2024!" }
# → 200 + token (copier)

# 3 — Route protegee
GET /api/auth/me
Authorization: Bearer 1|xxxxxxxxxxxx
# → 200 + user + abilities

# 4 — Logout
POST /api/auth/logout
Authorization: Bearer 1|xxxxxxxxxxxx
# → 200 "Deconnexion reussie"

# 5 — Verifier que le token est revoque
GET /api/auth/me
Authorization: Bearer 1|xxxxxxxxxxxx
# → 401 Unauthenticated
$token->plainTextToken est affiché uniquement au moment de la création. Pourquoi est-il impossible de le récupérer après ?
DariGo crée un token avec les abilities ['livraisons:view', 'livraisons:update'] pour un livreur. Ce livreur appelle POST /api/restaurants (créer un restaurant). Quel code HTTP reçoit-il ?
Partie 4 — Sécurité Auth avancée Email · Password Reset
⏱ 20 min

Authentification avancée

Authentification manuelle avec la façade Auth, middlewares auth et guest, gardes Laravel (config/auth.php), vérification d'email (MustVerifyEmail), réinitialisation de mot de passe, et kit Breeze.

✓ Ce que vous allez apprendre
  • Façade Auth — attempt(), login(), logout(), user(), check()
  • Middlewares auth (utilisateurs connectés) et guest (non connectés)
  • Gardes : web (session+cookie) vs api (token)
  • Vérification d'email — MustVerifyEmail, route email/verify
  • Réinitialisation de mot de passe — Password::sendResetLink()
  • Laravel Breeze — kit de démarrage tout-en-un

1
Façade Auth — méthodes essentielles
attempt() · login() · logout() · check() · user() · id()
phpAuth facade — référence complète DariGo
use Illuminate\Support\Facades\Auth;

// ---- CONNEXION ----

// attempt() — verifier identifiants et connecter
$connecte = Auth::attempt(['email' => $email, 'password' => $password]);
// Retourne true si les identifiants sont corrects et connecte l'utilisateur

// attempt() avec "remember me"
$connecte = Auth::attempt(['email' => $email, 'password' => $password], $remember = true);
// Si true : cookie remember_token — reste connecte meme apres fermeture navigateur

// login() — connecter un utilisateur deja recupere
$user = User::where('email', $email)->first();
Auth::login($user);          // Session
Auth::login($user, true);    // Avec remember me

// loginUsingId() — connecter par ID (admin qui usurpe un utilisateur)
Auth::loginUsingId(5);

// ---- VERIFICATION ----

Auth::check();               // true si connecte, false sinon
Auth::guest();               // true si NON connecte (inverse de check)
Auth::user();                // Retourne l'objet User ou null
Auth::id();                  // Retourne l'ID de l'utilisateur ou null

// Dans Blade
// @auth → Auth::check()
// @guest → Auth::guest()
// auth()->user() → raccourci global

// ---- DECONNEXION ----

Auth::logout();
// Recommande : invalider la session et regenerer le token CSRF apres logout
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');

// ---- GARDES ----
// La garde par defaut est 'web' (session)
Auth::guard('web')->user();    // Utilisateur de la garde web
Auth::guard('api')->user();    // Utilisateur de la garde api (tokens)
Auth::guard('sanctum')->user(); // Utilisateur Sanctum

// Dans les routes API : auth():sanctum est equivalent a Auth::guard('sanctum')
MéthodeRetourneUsage
Auth::check()boolVérifier si connecté
Auth::guest()boolVérifier si non connecté
Auth::user()User|nullRécupérer l'utilisateur connecté
Auth::id()int|nullRécupérer l'ID
Auth::attempt($creds)boolTenter une connexion
Auth::login($user)voidConnecter un objet User
Auth::logout()voidDéconnecter
Auth::viaRemember()boolVérifier si connecté via remember me

2
Middlewares auth et guest — gardes et config/auth.php
auth · guest · gardes web/api · providers · config/auth.php
phproutes/web.php — middleware auth et guest
// Routes accessibles uniquement aux utilisateurs NON connectes (formulaires connexion)
Route::middleware('guest')->group(function () {
    Route::get('/login',  [ConnexionController::class, 'show'])->name('login');
    Route::post('/login', [ConnexionController::class, 'login']);
    Route::get('/register', [InscriptionController::class, 'show'])->name('register');
    Route::post('/register', [InscriptionController::class, 'register']);
});

// Routes accessibles uniquement aux utilisateurs CONNECTES
Route::middleware('auth')->group(function () {
    Route::get('/dashboard',     [DashboardController::class, 'index'])->name('dashboard');
    Route::get('/profil',        [ProfilController::class, 'show'])->name('profil');
    Route::post('/auth/logout',  [DeconnexionController::class, 'logout'])->name('logout');
});

// Si un utilisateur non connecte tente d'acceder a /dashboard :
// → redirige vers la route nommee 'login' (definie dans bootstrap/app.php)
phpconfig/auth.php — gardes et providers DariGo
return [
    // Garde par defaut
    'defaults' => [
        'guard'     => 'web',     // Session + cookie
        'passwords' => 'users',
    ],

    'guards' => [
        // Garde web : navigateur, session, cookies
        'web' => [
            'driver'   => 'session',
            'provider' => 'users',
        ],
        // Garde api : tokens, stateless (routes api.php)
        'api' => [
            'driver'   => 'sanctum',  // Sanctum pour les APIs DariGo
            'provider' => 'users',
        ],
    ],

    // Providers : comment recuperer les utilisateurs
    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model'  => App\Models\User::class,
        ],
        // Exemple : garde separee pour les restaurateurs
        // 'restaurateurs' => [
        //     'driver' => 'eloquent',
        //     'model'  => App\Models\Restaurateur::class,
        // ],
    ],

    // Configuration de reinitialisation de mot de passe
    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table'    => 'password_reset_tokens',
            'expire'   => 60,   // Token expire apres 60 minutes
            'throttle' => 60,   // Attendre 60s entre deux demandes
        ],
    ],
];

3
Vérification d'email et réinitialisation de mot de passe
MustVerifyEmail · verified · Password::sendResetLink() · Password::reset()
phpVérification d'email DariGo — activation du compte
// 1 — Ajouter MustVerifyEmail au modele User
use Illuminate\Contracts\Auth\MustVerifyEmail;

class User extends Authenticatable implements MustVerifyEmail
{
    // Laravel envoie automatiquement un email de verification apres register
}

// 2 — Routes de verification (Laravel Sanctum — API)
Route::middleware('auth:sanctum')->group(function () {

    // Renvoyer l'email de verification
    Route::post('/email/verification-notification', function (Request $request) {
        if ($request->user()->hasVerifiedEmail()) {
            return response()->json(['message' => 'Email deja verifie']);
        }
        $request->user()->sendEmailVerificationNotification();
        return response()->json(['message' => 'Email de verification envoye']);
    })->middleware('throttle:6,1');

    // Verifier l'email (lien dans l'email)
    Route::get('/email/verify/{id}/{hash}', function (EmailVerificationRequest $request) {
        $request->fulfill();  // Marque l'email comme verifie
        return response()->json(['message' => 'Email verifie avec succes']);
    })->middleware('signed')->name('verification.verify');

    // Routes qui necessitent l'email verifie
    Route::middleware('verified')->group(function () {
        Route::post('/commandes', [CommandeController::class, 'store']);
        // Un client doit avoir un email verifie pour commander
    });
});
phpRéinitialisation de mot de passe — DariGo API
use Illuminate\Support\Facades\Password;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Support\Str;

// ── Etape 1 : Demander un lien de reinitialisation ────────
public function envoyerLienReset(Request $request): JsonResponse
{
    $request->validate(['email' => ['required', 'email']]);

    $statut = Password::sendResetLink($request->only('email'));
    // Envoie un email avec un lien signe (token expire en 60 min)

    return $statut === Password::RESET_LINK_SENT
        ? response()->json(['message' => 'Lien de reinitialisation envoye'])
        : response()->json(['message' => 'Email introuvable'], 422);
}

// ── Etape 2 : Valider le token et changer le mot de passe ─
public function reinitialiserPassword(Request $request): JsonResponse
{
    $request->validate([
        'token'                 => ['required'],
        'email'                 => ['required', 'email'],
        'password'              => ['required', 'confirmed', 'min:8'],
    ]);

    $statut = Password::reset(
        $request->only('email', 'password', 'password_confirmation', 'token'),
        function (User $user, string $password) {
            $user->forceFill(['password' => Hash::make($password)])
                 ->setRememberToken(Str::random(60));
            $user->save();
            event(new PasswordReset($user));
            // Revoquer tous les tokens Sanctum existants
            $user->tokens()->delete();
        }
    );

    return $statut === Password::PASSWORD_RESET
        ? response()->json(['message' => 'Mot de passe reinitialise avec succes'])
        : response()->json(['message' => 'Token invalide ou expire'], 422);
}
ℹ Laravel Breeze — kit de démarrage all-in-one

Laravel Breeze génère automatiquement tout le système d'auth : register, login, logout, vérification email, réinitialisation mot de passe, vues Blade Bootstrap, routes, controllers, migrations. Idéal pour un prototype rapide. Commandes : composer require laravel/breeze --dev puis php artisan breeze:install. Pour DariGo (API pure) : Breeze n'est pas utilisé — on gère manuellement avec Sanctum.

Un utilisateur non connecté tente d'accéder à GET /dashboard protégé par le middleware auth. Il envoie une requête JSON (Accept: application/json). Que retourne Laravel ?
Quelle différence entre le middleware auth et le middleware guest ?
Partie 4 — Sécurité Gates · Policies @can · authorize()
⏱ 22 min

Autorisation — Gates & Policies

Les Gates (closures d'autorisation fine) et les Policies (classes d'autorisation par modèle) — définir, enregistrer, appliquer et tester les autorisations DariGo (restaurateur propriétaire, admin universel).

✓ Ce que vous allez apprendre
  • Gates — Gate::define(), Gate::allows(), Gate::authorize(), Response::deny()
  • Policies — php artisan make:policy --model, 7 méthodes CRUD
  • Filtre before() — admin bypass automatique
  • 5 façons d'appliquer une Policy : Gate, $user->can(), $this->authorize(), middleware can, authorizeResource()
  • Directives Blade : @can, @cannot, @canany
  • Différence Gates vs Policies — quand choisir quoi

1
Gates — autorisation fine par action
Gate::define() · allows() · authorize() · Response::deny() · AppServiceProvider
ℹ Gates vs Policies — quand choisir

Gates : pour des autorisations qui ne sont pas liées à un seul modèle Eloquent. Ex : "peut accéder au tableau de bord admin", "peut exporter les statistiques". Policies : pour des autorisations liées à un modèle spécifique. Ex : "peut modifier CE restaurant", "peut voir CETTE commande". Dans DariGo : Gates pour les permissions générales de rôle, Policies pour les ressources.

phpapp/Providers/AppServiceProvider.php — définir les Gates DariGo
use Illuminate\Support\Facades\Gate;
use Illuminate\Auth\Access\Response;

public function boot(): void
{
    // Gate simple — acces au tableau de bord admin
    Gate::define('acceder-dashboard', function (User $user) {
        return $user->role === 'admin';
    });

    // Gate avec modele — voir les statistiques d'un restaurant
    Gate::define('voir-stats-restaurant', function (User $user, Restaurant $restaurant) {
        return $user->role === 'admin'
            || $user->id === $restaurant->proprietaire_id;
    });

    // Gate avec reponse detaillee (message d'erreur personnalise)
    Gate::define('gerer-livreurs', function (User $user) {
        return $user->role === 'admin'
            ? Response::allow()
            : Response::deny('Seuls les administrateurs peuvent gerer les livreurs.');
    });

    // Gate avec denyWithStatus (404 au lieu de 403 — masquer l'existence)
    Gate::define('voir-commande-privee', function (User $user, Commande $commande) {
        return $user->id === $commande->client_id || $user->role === 'admin'
            ? Response::allow()
            : Response::denyWithStatus(404);  // Le client ne sait pas que la commande existe
    });
}
phpUtiliser les Gates dans les Controllers et Blade
// Dans un Controller
Gate::allows('acceder-dashboard');         // true/false
Gate::denies('acceder-dashboard');         // true/false
Gate::authorize('acceder-dashboard');      // Lance 403 si refuse
Gate::authorize('voir-stats-restaurant', $restaurant); // Avec modele

// Via le modele User
auth()->user()->can('acceder-dashboard');    // true/false
auth()->user()->cannot('acceder-dashboard'); // true/false

// Via la methode authorize() du Controller (herite de Controller)
$this->authorize('voir-stats-restaurant', $restaurant);

// Dans Blade
@can('acceder-dashboard')
    <a href="/admin">Tableau de bord</a>
@endcan

@cannot('acceder-dashboard')
    <p>Acces restreint aux administrateurs.</p>
@endcannot

// @canany — si au moins une permission est accordee
@canany(['voir-stats-restaurant', 'acceder-dashboard'], $restaurant)
    <div class="stats">...</div>
@endcanany

2
Policies DariGo — RestaurantPolicy et CommandePolicy
make:policy --model · before() · viewAny/view/create/update/delete
bashGénérer les Policies DariGo
php artisan make:policy RestaurantPolicy --model=Restaurant
php artisan make:policy CommandePolicy --model=Commande
php artisan make:policy PlatPolicy --model=Plat
phpapp/Policies/RestaurantPolicy.php — DariGo complet
<?php

namespace App\Policies;

use App\Models\Restaurant;
use App\Models\User;
use Illuminate\Auth\Access\Response;

class RestaurantPolicy
{
    /**
     * before() : s'execute AVANT toutes les methodes de la Policy
     * Utilise pour autoriser automatiquement l'admin
     */
    public function before(User $user, string $ability): bool|null
    {
        if ($user->role === 'admin') {
            return true;  // Admin peut tout faire — court-circuite toutes les methodes
        }
        return null;  // null = continuer vers la methode specifique
    }

    // Voir la liste des restaurants (GET /restaurants)
    public function viewAny(User $user): bool
    {
        return true; // Tous les utilisateurs connectes peuvent voir la liste
    }

    // Voir un restaurant specifique (GET /restaurants/{id})
    public function view(User $user, Restaurant $restaurant): bool
    {
        return true; // Tout le monde peut voir un restaurant
    }

    // Creer un restaurant (POST /restaurants)
    public function create(User $user): bool
    {
        return $user->role === 'restaurateur';
    }

    // Modifier un restaurant (PUT /restaurants/{id})
    public function update(User $user, Restaurant $restaurant): bool
    {
        // Seul le proprietaire peut modifier SON restaurant
        return $user->id === $restaurant->proprietaire_id;
    }

    // Supprimer un restaurant (DELETE /restaurants/{id})
    public function delete(User $user, Restaurant $restaurant): bool
    {
        return $user->id === $restaurant->proprietaire_id;
    }

    // Activer/desactiver un restaurant (action personnalisee)
    public function toggleActif(User $user, Restaurant $restaurant): Response
    {
        return $user->id === $restaurant->proprietaire_id
            ? Response::allow()
            : Response::deny('Seul le proprietaire peut activer ou desactiver son restaurant.');
    }
}
phpRestaurantController — 5 façons d'appliquer la Policy
class RestaurantController extends Controller
{
    // Methode 1 : authorizeResource() — lie automatiquement toutes les methodes CRUD
    public function __construct()
    {
        $this->middleware('auth:sanctum');
        $this->authorizeResource(Restaurant::class, 'restaurant');
        // Equivalent a appeler $this->authorize('view/create/update/delete', $restaurant)
        // dans chaque methode correspondante
    }

    // Methode 2 : $this->authorize() dans la methode
    public function update(UpdateRestaurantRequest $request, Restaurant $restaurant): JsonResponse
    {
        $this->authorize('update', $restaurant); // Lance 403 si refuse
        $restaurant->update($request->validated());
        return response()->json(['message' => 'Restaurant mis a jour']);
    }

    // Methode 3 : Gate::authorize()
    public function destroy(Restaurant $restaurant): JsonResponse
    {
        Gate::authorize('delete', $restaurant);
        $restaurant->delete();
        return response()->json(['message' => 'Restaurant supprime']);
    }
}

// Methode 4 : Middleware can sur la route
Route::delete('/restaurants/{restaurant}', [RestaurantController::class, 'destroy'])
    ->middleware('can:delete,restaurant');

// Methode 5 : $user->can() dans le Controller
public function toggleActif(Restaurant $restaurant): JsonResponse
{
    if (auth()->user()->cannot('toggleActif', $restaurant)) {
        return response()->json(['erreur' => 'Non autorise'], 403);
    }
    $restaurant->update(['actif' => !$restaurant->actif]);
    return response()->json(['actif' => $restaurant->actif]);
}
La méthode before() de RestaurantPolicy retourne null pour un utilisateur non-admin. Que se passe-t-il ensuite ?
Un restaurateur (proprietaire du restaurant #5) appelle DELETE /api/restaurants/7 (restaurant #7 qui lui appartient PAS). Quel code HTTP reçoit-il ?
Partie 4 — Sécurité Crypt · Hash bcrypt · argon2
⏱ 16 min

Chiffrement & Hachage

Différence fondamentale entre chiffrement (réversible) et hachage (irréversible), Crypt::encrypt/decrypt() pour les données sensibles, Hash::make() (bcrypt/argon2) pour les mots de passe et Hash::check() pour la vérification.

✓ Ce que vous allez apprendre
  • Chiffrement vs Hachage — différence fondamentale et cas d'usage
  • Crypt::encrypt() / Crypt::decrypt() — basé sur APP_KEY
  • Hash::make() — bcrypt (défaut) et argon2id
  • Hash::check() — comparer un mot de passe et son hash
  • Cast automatique 'password' => 'hashed' dans Eloquent
  • Pourquoi ne jamais stocker un mot de passe en clair

1
Chiffrement vs Hachage — différence fondamentale
Réversible vs irréversible · Cas d'usage · APP_KEY
CritèreChiffrement (Crypt::)Hachage (Hash::)
RéversibilitéRéversible — on peut déchiffrer avec la cléIrréversible — impossible de retrouver l'original
Clé requiseOui — APP_KEY dans .envNon — algorithme one-way
AlgorithmeAES-256-CBC (symétrique)bcrypt ou argon2id
Cas d'usageStocker données à récupérer (IBAN, numéro CB masqué)Mots de passe — uniquement vérification
DariGoNuméro de téléphone livreur, coordonnées GPSMot de passe de tous les utilisateurs
⚠ Ne jamais confondre les deux

Utiliser du chiffrement pour les mots de passe = fuite de données catastrophique si APP_KEY est compromise. Utiliser du hachage pour des données à afficher (numéro de téléphone) = impossible d'afficher le numéro à l'utilisateur. Chaque techno à son usage.


2
Crypt:: — chiffrer des données sensibles
encrypt() · decrypt() · encryptString() · APP_KEY · DecryptException
phpChiffrement DariGo — données sensibles livreurs
use Illuminate\Support\Facades\Crypt;
use Illuminate\Contracts\Encryption\DecryptException;

// ---- CHIFFRER ----

// Chiffrer n'importe quelle valeur (seriabsle : tableau, objet, chaine)
$chiffre = Crypt::encrypt('0612345678');
// Retourne une chaine Base64 longue : "eyJpdiI6Ij..."

// Chiffrer uniquement une chaine (plus rapide, non serialise)
$chiffre = Crypt::encryptString('0612345678');

// Stocker dans la BDD
$livreur->update(['telephone_chiffre' => Crypt::encryptString($telephone)]);

// ---- DECHIFFRER ----

// Dechiffrer (exception si APP_KEY change ou donnee corrompue)
try {
    $telephone = Crypt::decryptString($livreur->telephone_chiffre);
} catch (DecryptException $e) {
    // APP_KEY a change ou la donnee est corrompue
    \Log::error("Impossible de dechiffrer le telephone du livreur {$livreur->id}");
    $telephone = null;
}

// ---- CAST ELOQUENT AUTOMATIQUE ----
// Dans le modele Livreur : chiffrement/dechiffrement automatique
protected function casts(): array
{
    return [
        'telephone'         => 'encrypted',         // Crypt::encrypt/decrypt auto
        'iban'              => 'encrypted',
        'coords_domicile'   => 'encrypted:array',    // Tableau chiffre
    ];
}

// Utilisation transparente :
$livreur->telephone = '0612345678';   // Stocke chiffre en BDD
echo $livreur->telephone;             // Retourne '0612345678' (dechiffre auto)
ℹ APP_KEY — la clé de chiffrement critique

Toute la sécurité de Crypt:: repose sur APP_KEY dans le .env. Générée par php artisan key:generate, elle est unique à chaque projet. Conséquences : si APP_KEY change, toutes les données chiffrées deviennent indéchiffrables. Jamais de key:generate en production. Stocker APP_KEY dans un gestionnaire de secrets (Vault, Azure Key Vault) — jamais en dur dans le code.


3
Hash:: — hacher et vérifier les mots de passe
Hash::make() · bcrypt · argon2id · Hash::check() · cost factor
ℹ Pourquoi bcrypt est lent — et pourquoi c'est voulu

bcrypt est intentionnellement lent. Le paramètre rounds (10 par défaut) détermine le coût. Chaque hash prend ~100ms. Pour une connexion normale : imperceptible. Pour un attaquant qui essaie 1 million de mots de passe : 100ms × 1 000 000 = ~28 heures. C'est l'objectif. Ne jamais réduire rounds en production (les tests utilisent rounds=4).

phpHash:: — hachage complet DariGo
use Illuminate\Support\Facades\Hash;

// ---- HACHER ----

// bcrypt (driver par defaut) — rounds = 10
$hash = Hash::make('MonMotDePasse2024!');
// '$2y$10$...' — 60 caracteres

// Chaque appel produit un hash DIFFERENT (salt aleatoire inclus)
Hash::make('abc') !== Hash::make('abc')  // true — differents !

// Personnaliser le cout (dev = 4, prod = 10-12)
$hash = Hash::make('MonMotDePasse2024!', ['rounds' => 12]);

// argon2id (recommande pour les nouvelles applications)
// Dans config/hashing.php : 'driver' => 'argon2id'

// ---- VERIFIER ----

// Hash::check() — comparer un mot de passe et son hash
$valide = Hash::check('MonMotDePasse2024!', $user->password); // true
$valide = Hash::check('MauvaisMotDePasse', $user->password);  // false

// needsRehash() — verifier si le hash doit etre recalcule (apres augmentation rounds)
if (Hash::needsRehash($user->password)) {
    $user->update(['password' => Hash::make($nouveauMotDePasse)]);
}

// ---- CAST ELOQUENT (Laravel 11) ----
// Dans le modele User :
// 'password' => 'hashed'
// → Hash::make() automatique a l'assignation
$user->password = 'NouveauMdp2024!';   // Hash automatique via le cast
$user->save();

// ---- DANS UN CONTROLLER ----
public function changerMotDePasse(Request $request): JsonResponse
{
    $request->validate([
        'ancien_password' => ['required'],
        'nouveau_password' => ['required', 'confirmed', 'min:8'],
    ]);

    // Verifier l'ancien mot de passe
    if (! Hash::check($request->ancien_password, auth()->user()->password)) {
        return response()->json(['message' => 'Ancien mot de passe incorrect'], 422);
    }

    auth()->user()->update(['password' => Hash::make($request->nouveau_password)]);
    // Revoquer tous les tokens (forcer reconnexion sur tous les appareils)
    auth()->user()->tokens()->delete();

    return response()->json(['message' => 'Mot de passe mis a jour']);
}
AlgoConfigLongueur hashUsage recommandé
bcryptrounds: 10 (défaut)60 caractèresProjets Laravel existants, compatibilité large
argon2imemory: 65536, time: 4VariableGPU-resistant
argon2idRecommandé PHP 7.3+VariableNouveaux projets — meilleur compromis
Un développeur stocke le mot de passe d'un livreur avec Crypt::encrypt($password) au lieu de Hash::make($password). Quel est le risque principal ?
Hash::make('abc') est appelé deux fois de suite. Les deux hashs sont-ils identiques ?
Partie 5 — Base de données DB · SQL brut Connexions multiples · Transactions
⏱ 18 min

Configuration BDD, SQL brut & Transactions

Configurer config/database.php pour MySQL, connexions multiples, exécuter du SQL brut avec DB::select/insert/update/delete, transactions (DB::transaction()), débogage avec DB::getQueryLog() et Tinker.

✓ Ce que vous allez apprendre
  • Configurer config/database.php — drivers supportés, charset utf8mb4
  • Connexions multiples — BDD principale + BDD archives
  • SQL brut : DB::select(), DB::insert(), DB::update(), DB::delete()
  • Transactions : DB::transaction() et gestion manuelle beginTransaction()
  • Débogage : DB::enableQueryLog(), DB::getQueryLog()
  • Tinker — tester des requêtes en temps réel

1
config/database.php — configuration complète DariGo
mysql · utf8mb4 · connexions multiples · sqlite (tests)
phpconfig/database.php — DariGo multi-connexion
return [
    'default' => env('DB_CONNECTION', 'mysql'),  // Connexion par defaut

    'connections' => [

        // Connexion principale DariGo
        'mysql' => [
            'driver'    => 'mysql',
            'host'      => env('DB_HOST', '127.0.0.1'),
            'port'      => env('DB_PORT', '3306'),
            'database'  => env('DB_DATABASE', 'darigo_db'),
            'username'  => env('DB_USERNAME', 'root'),
            'password'  => env('DB_PASSWORD', ''),
            'charset'   => 'utf8mb4',    // Supporte les emojis et l'arabe
            'collation' => 'utf8mb4_unicode_ci',
            'prefix'    => '',
            'strict'    => true,         // Mode strict MySQL — bonnes pratiques
            'engine'    => 'InnoDB',     // InnoDB : transactions + FK
        ],

        // Connexion secondaire — BDD archives (lecture seule)
        'archives' => [
            'driver'   => 'mysql',
            'host'     => env('DB_ARCHIVES_HOST', '127.0.0.1'),
            'database' => env('DB_ARCHIVES_DATABASE', 'darigo_archives'),
            'username' => env('DB_ARCHIVES_USERNAME', 'root'),
            'password' => env('DB_ARCHIVES_PASSWORD', ''),
            'charset'  => 'utf8mb4',
            'collation'=> 'utf8mb4_unicode_ci',
        ],

        // SQLite — tests unitaires (rapide, sans serveur)
        'sqlite' => [
            'driver'   => 'sqlite',
            'database' => ':memory:',   // BDD en memoire — detruite apres les tests
        ],
    ],

    'migrations' => [
        'table'     => 'migrations',
        'update_date_on_publish' => true,
    ],
];
phpUtiliser une connexion spécifique
// Connexion par defaut (mysql)
$commandes = DB::table('commandes')->get();

// Connexion specifique
$archives = DB::connection('archives')->table('commandes_archivees')->get();

// Dans un modele Eloquent
class CommandeArchive extends Model
{
    protected $connection = 'archives';
    protected $table      = 'commandes_archivees';
}

2
SQL brut avec la facade DB
DB::select() · DB::insert() · DB::update() · DB::delete() · bindings
⚠ Toujours utiliser des bindings — jamais d'interpolation de chaine

Jamais : DB::select("SELECT * FROM commandes WHERE id = $id") — injection SQL possible. Toujours : DB::select("SELECT * FROM commandes WHERE id = ?", [$id]). Les bindings (?) ou les bindings nommés (:id) remplacent les valeurs après échappement automatique par PDO.

phpSQL brut — exemples DariGo
use Illuminate\Support\Facades\DB;

// ---- SELECT ----
// Retourne un tableau d'objets stdClass (pas de modeles Eloquent)
$commandes = DB::select('SELECT * FROM commandes WHERE statut = ?', ['livree']);

// Bindings nommes
$restaurant = DB::select(
    'SELECT * FROM restaurants WHERE ville = :ville AND actif = :actif',
    ['ville' => 'Casablanca', 'actif' => 1]
);

// selectOne() -- retourne un seul objet
$total = DB::selectOne('SELECT SUM(total) as ca FROM commandes WHERE DATE(created_at) = ?', [today()]);
echo $total->ca;   // Chiffre d'affaires du jour

// ---- INSERT ----
DB::insert('INSERT INTO logs_activite (user_id, action, ip) VALUES (?, ?, ?)', [
    auth()->id(), 'connexion', request()->ip()
]);

// ---- UPDATE ----
$affectes = DB::update(
    'UPDATE commandes SET statut = ? WHERE created_at < ? AND statut = ?',
    ['expiree', now()->subHours(2), 'en_attente']
);
// Retourne le nombre de lignes affectees

// ---- DELETE ----
$supprimes = DB::delete('DELETE FROM tokens_expires WHERE expires_at < ?', [now()]);

// ---- STATEMENT (CREATE TABLE, ALTER, etc.) ----
DB::statement('ALTER TABLE restaurants ADD COLUMN note_google DECIMAL(3,2) NULL');

// ---- REQUETE BRUTE (sans binding) — uniquement pour du code de confiance ----
DB::unprepared('TRUNCATE TABLE cache');

3
Transactions — garantir la cohérence des données
DB::transaction() · beginTransaction() · commit() · rollBack()
ℹ Pourquoi les transactions sont critiques pour DariGo

Un client passe une commande : il faut (1) créer la commande ET (2) décrémenter le stock du plat. Si l'opération 2 échoue, la commande existe mais le stock est faux. Sans transaction : données incohérentes. Avec transaction : si une opération échoue, tout est annulé (rollback). Principe ACID : Atomicité, Cohérence, Isolation, Durabilité.

phpTransactions — création commande DariGo
// Methode 1 : DB::transaction() avec closure — rollback automatique si exception
$commande = DB::transaction(function () use ($request, $user) {

    // Operation 1 : creer la commande
    $commande = Commande::create([
        'client_id'     => $user->id,
        'restaurant_id' => $request->restaurant_id,
        'total'         => $request->total,
        'statut'        => 'en_attente',
    ]);

    // Operation 2 : decrements le stock de chaque plat commande
    foreach ($request->lignes as $ligne) {
        $plat = Plat::lockForUpdate()->find($ligne['plat_id']); // Verrou pessimiste

        if ($plat->stock < $ligne['quantite']) {
            throw new \Exception("Stock insuffisant pour : {$plat->nom}");
            // L'exception declenche le rollback automatique
        }

        $plat->decrement('stock', $ligne['quantite']);

        LigneCommande::create([
            'commande_id' => $commande->id,
            'plat_id'     => $plat->id,
            'quantite'    => $ligne['quantite'],
            'prix'        => $plat->prix,
        ]);
    }

    return $commande; // Retour de la closure = commit automatique
});

// Methode 2 : gestion manuelle (plus de controle)
DB::beginTransaction();
try {
    $commande = Commande::create([...]);
    Plat::where('id', $platId)->decrement('stock', $quantite);

    DB::commit();  // Valider
    return response()->json(['message' => 'Commande creee', 'id' => $commande->id], 201);
} catch (\Throwable $e) {
    DB::rollBack(); // Annuler TOUT
    \Log::error('Erreur creation commande', ['error' => $e->getMessage()]);
    return response()->json(['erreur' => 'Erreur lors de la creation de la commande'], 500);
}

// Transactions imbriquees (savepoints)
DB::transaction(function () {
    // Transaction principale
    Restaurant::create([...]);

    DB::transaction(function () {
        // Savepoint — si celle-ci echoue, seule cette partie est annulee
        Plat::create([...]);
    });
});

4
Débogage des requêtes SQL
enableQueryLog() · getQueryLog() · dd() · toSql() · Telescope
phpDébogage SQL — outils DariGo
// Activer le query log puis executer des requetes
DB::enableQueryLog();

$commandes = Commande::with('client', 'plats')
    ->where('statut', 'en_attente')
    ->get();

$log = DB::getQueryLog();
// Retourne un tableau avec : sql, bindings, time (ms)
// [ ['sql' => 'select * from...', 'bindings' => [...], 'time' => 12.5] ]

dd($log);   // Afficher et stopper

// toSql() — voir la requete sans l'executer
$sql = Commande::where('statut', 'livree')
    ->whereMonth('created_at', now()->month)
    ->toSql();
// 'select * from `commandes` where `statut` = ? and month(`created_at`) = ?'

// dump() — afficher sans stopper (continuer l'execution)
Commande::where('statut', 'en_attente')->dump()->get();

// Compter les requetes N+1 (Livewire Telescope ou Bar)
// Laravel Debugbar : composer require barryvdh/laravel-debugbar --dev
Une transaction DariGo exécute 3 opérations. La deuxième échoue avec une exception. Que se passe-t-il avec DB::transaction(closure) ?
Pourquoi faut-il utiliser DB::select('... WHERE id = ?', [$id]) plutôt que DB::select("... WHERE id = $id") ?
Partie 5 — Base de données Query Builder where · join · paginate
⏱ 22 min

Query Builder complet

Le générateur de requêtes fluide Laravel — sélection, conditions WHERE avancées, jointures (inner/left/right), GROUP BY, HAVING, pagination, CRUD, sous-requêtes et la méthode when() pour des requêtes conditionnelles.

✓ Ce que vous allez apprendre
  • Sélection : get(), first(), find(), value(), pluck(), chunk()
  • Conditions : where(), orWhere(), whereIn(), whereBetween(), whereNull(), whereDate()
  • Jointures : join(), leftJoin(), rightJoin()
  • Agrégation : groupBy(), having(), orderBy(), limit()
  • CRUD : insert(), update(), updateOrInsert(), delete()
  • Pagination : paginate(), simplePaginate(), cursorPaginate()
  • Requêtes conditionnelles : when()

1
Sélection et conditions WHERE
get() · first() · find() · where() · whereIn() · whereBetween() · whereDate()
phpQuery Builder — sélection DariGo
use Illuminate\Support\Facades\DB;

// ---- METHODES DE RECUPERATION ----
$restaurants = DB::table('restaurants')->get();              // Collection de stdClass
$restaurants = DB::table('restaurants')->get(['id', 'nom']); // Colonnes specifiques
$restaurant  = DB::table('restaurants')->first();            // Premier ou null
$restaurant  = DB::table('restaurants')->find(5);            // Par id ou null
$nom         = DB::table('restaurants')->value('nom');       // Valeur d'un seul champ
$noms        = DB::table('restaurants')->pluck('nom');       // Collection de noms
$noms        = DB::table('restaurants')->pluck('nom', 'id'); // [id => nom]
$count       = DB::table('restaurants')->count();
$max         = DB::table('commandes')->max('total');
$avg         = DB::table('commandes')->avg('total');
$sum         = DB::table('commandes')->sum('total');

// ---- CONDITIONS WHERE ----
// Operateurs : =, !=, <, <=, >, >=, like, not like, in, not in
DB::table('restaurants')->where('ville', 'Casablanca')->get();
DB::table('restaurants')->where('note', '>=', 4.0)->get();
DB::table('restaurants')->where('nom', 'like', '%darigo%')->get();

// WHERE multiples (AND)
DB::table('restaurants')
    ->where('ville', 'Casablanca')
    ->where('actif', true)
    ->where('note', '>=', 3.5)
    ->get();

// WHERE OR
DB::table('restaurants')
    ->where('ville', 'Casablanca')
    ->orWhere('ville', 'Rabat')
    ->get();

// WHERE groupes (parentheses)
DB::table('restaurants')
    ->where(function ($query) {
        $query->where('ville', 'Casablanca')
              ->orWhere('ville', 'Marrakech');
    })
    ->where('note', '>=', 4.0)
    ->get();
// SQL : WHERE (ville = 'Casablanca' OR ville = 'Marrakech') AND note >= 4.0

// WHERE IN / NOT IN
DB::table('commandes')->whereIn('statut', ['livree', 'confirmee'])->get();
DB::table('commandes')->whereNotIn('statut', ['annulee'])->get();

// WHERE NULL / NOT NULL
DB::table('commandes')->whereNull('livreur_id')->get();     // Sans livreur affecte
DB::table('livraisons')->whereNotNull('accepte_le')->get();

// WHERE BETWEEN
DB::table('commandes')->whereBetween('total', [50, 200])->get();
DB::table('plats')->whereBetween('prix', [20, 100])->get();

// WHERE DATE (Carbon/MySQL)
DB::table('commandes')->whereDate('created_at', today())->get();           // Aujourd'hui
DB::table('commandes')->whereMonth('created_at', now()->month)->get();     // Ce mois
DB::table('commandes')->whereYear('created_at', now()->year)->get();       // Cette annee
DB::table('commandes')->whereTime('created_at', '>=', '12:00:00')->get(); // Apres midi

2
Jointures, GROUP BY, HAVING et tri
join() · leftJoin() · groupBy() · having() · orderBy() · limit()
phpJointures et agrégations — statistiques DariGo
// ---- JOINTURES ----

// INNER JOIN — commandes avec le nom du client
$commandes = DB::table('commandes')
    ->join('users', 'commandes.client_id', '=', 'users.id')
    ->select('commandes.*', 'users.prenom', 'users.email')
    ->get();

// LEFT JOIN — tous les restaurants, avec ou sans commande
$stats = DB::table('restaurants')
    ->leftJoin('commandes', 'restaurants.id', '=', 'commandes.restaurant_id')
    ->select('restaurants.nom', DB::raw('COUNT(commandes.id) as nb_commandes'))
    ->groupBy('restaurants.id', 'restaurants.nom')
    ->orderBy('nb_commandes', 'DESC')
    ->get();

// Jointure multiple
$livraisons = DB::table('livraisons')
    ->join('commandes', 'livraisons.commande_id', '=', 'commandes.id')
    ->join('users as clients', 'commandes.client_id', '=', 'clients.id')
    ->join('users as livreurs', 'livraisons.livreur_id', '=', 'livreurs.id')
    ->select(
        'livraisons.statut',
        'clients.prenom as client',
        'livreurs.prenom as livreur',
        'commandes.total'
    )
    ->get();

// ---- GROUP BY + HAVING ----

// Chiffre d'affaires par restaurant (mois en cours)
$ca = DB::table('commandes')
    ->select('restaurant_id', DB::raw('SUM(total) as ca_mensuel'), DB::raw('COUNT(*) as nb_commandes'))
    ->whereMonth('created_at', now()->month)
    ->where('statut', 'livree')
    ->groupBy('restaurant_id')
    ->having('ca_mensuel', '>=', 1000)   // Restaurants avec CA > 1000 MAD
    ->orderBy('ca_mensuel', 'DESC')
    ->get();

// ---- TRI ET LIMITE ----
DB::table('restaurants')->orderBy('note', 'DESC')->limit(10)->get();  // Top 10
DB::table('restaurants')->latest('created_at')->first();              // Plus recent
DB::table('restaurants')->inRandomOrder()->first();                   // Aleatoire
DB::table('commandes')->skip(20)->take(10)->get();                    // Pagination manuelle

3
CRUD et requêtes conditionnelles when()
insert() · update() · updateOrInsert() · delete() · when() · chunk()
phpCRUD Query Builder — DariGo
// ---- INSERT ----
DB::table('logs')->insert([
    'user_id' => auth()->id(),
    'action'  => 'connexion',
    'ip'      => request()->ip(),
]);

// insertGetId() — recuperer l'id insere
$id = DB::table('livraisons')->insertGetId([
    'commande_id' => 15,
    'statut'      => 'disponible',
    'created_at'  => now(),
]);

// ---- UPDATE ----
$affected = DB::table('commandes')
    ->where('statut', 'en_attente')
    ->where('created_at', '<', now()->subHours(2))
    ->update(['statut' => 'expiree', 'updated_at' => now()]);

// increment() / decrement()
DB::table('plats')->where('id', 5)->decrement('stock', 2);
DB::table('restaurants')->where('id', 3)->increment('nb_commandes');

// updateOrInsert() — chercher et mettre a jour OU creer
DB::table('statistiques_journalieres')->updateOrInsert(
    ['restaurant_id' => 5, 'date' => today()],   // Conditions de recherche
    ['nb_commandes' => DB::raw('nb_commandes + 1'), 'updated_at' => now()]  // Donnees
);

// ---- DELETE ----
DB::table('tokens')->where('expires_at', '<', now())->delete();

// ---- when() — requeues conditionnelles ----
// PROBLEME : construire une requete selon les filtres de l'utilisateur
$ville     = request('ville');
$categorie = request('categorie');
$note_min  = request('note_min');

$restaurants = DB::table('restaurants')
    ->when($ville, function ($query, $ville) {
        // Ce bloc ne s'execute QUE si $ville est non-null/non-vide
        $query->where('ville', $ville);
    })
    ->when($categorie, function ($query, $categorie) {
        $query->where('categorie', $categorie);
    })
    ->when($note_min, function ($query, $note) {
        $query->where('note', '>=', $note);
    })
    ->when(request('tri') === 'note', function ($query) {
        $query->orderBy('note', 'DESC');
    }, function ($query) {
        $query->orderBy('created_at', 'DESC'); // Tri par defaut
    })
    ->paginate(12);

// ---- chunk() — traiter les grandes tables par morceaux ----
DB::table('commandes')->where('statut', 'livree')->chunk(100, function ($commandes) {
    foreach ($commandes as $commande) {
        // Traiter 100 commandes a la fois — evite les problemes de memoire
        $this->genererFacture($commande);
    }
});

// ---- Pagination ----
$restaurants = DB::table('restaurants')->paginate(12);   // Avec total (count)
$restaurants = DB::table('restaurants')->simplePaginate(12); // Sans total (plus rapide)

// Dans le Controller API : paginate() retourne toutes les metadata
// {"current_page":1,"data":[...],"last_page":5,"total":60,"per_page":12}
DB::table('restaurants')->get() retourne des objets stdClass. Restaurant::all() retourne des instances du modèle Restaurant. Quelle est la différence pratique ?
Une API reçoit des filtres optionnels : ville, categorie, note_min. Sans when(), comment ce code serait-il écrit ?
Partie 5 — Base de données Migrations · Schema up/down · FK · indexes
⏱ 20 min

Migrations & Schema Builder

Créer, modifier et versionner le schéma de la base de données avec les migrations Laravel — up()/down(), tous les types de colonnes, modificateurs (nullable, default, unique), clés étrangères, indexes, et les 6 commandes migrate essentielles.

✓ Ce que vous allez apprendre
  • Définition et utilité des migrations — versionner le schéma BDD comme du code
  • Structure up() et down() — créer et annuler
  • Tous les types de colonnes : string, integer, decimal, enum, json, timestamp...
  • Modificateurs : nullable(), default(), unique(), unsigned()
  • Clés étrangères : foreignId()->constrained() et cascadeOnDelete()
  • Modifier une table existante avec Schema::table()

1
Générer et comprendre les migrations
make:migration · up() · down() · Schema::create() · Schema::dropIfExists()
ℹ Pourquoi les migrations ?

Sans migrations : chaque développeur modifie sa BDD locale manuellement et le serveur de production a une structure différente. Avec migrations : la structure de la BDD est décrite en PHP versionné sur Git. php artisan migrate applique exactement les mêmes tables partout (local, CI/CD, production). Rollback possible : php artisan migrate:rollback.

bashGénérer les migrations DariGo
# Creer une migration (nom prefixe automatiquement par la date+heure)
php artisan make:migration create_restaurants_table
php artisan make:migration create_plats_table
php artisan make:migration create_commandes_table
php artisan make:migration ajouter_note_google_to_restaurants_table

# Avec table specifiee explicitement
php artisan make:migration ajouter_colonne_photo --table=restaurants

# Avec le modele (creer modele + migration + seeder + factory + controller)
php artisan make:model Restaurant -mscf --resource

2
Migrations DariGo — toutes les tables
Types colonnes · FK · cascade · indexes · table pivot
phpMigration restaurants — types de colonnes complets
Schema::create('restaurants', function (Blueprint $table) {
    $table->id();                                      // BIGINT UNSIGNED AUTO_INCREMENT PK
    $table->string('nom', 100);                        // VARCHAR(100) NOT NULL
    $table->string('slug', 120)->unique();             // VARCHAR + index UNIQUE
    $table->text('description')->nullable();           // TEXT NULL
    $table->string('ville', 50);
    $table->string('adresse');
    $table->string('telephone', 15)->nullable();
    $table->enum('categorie', ['tajine','couscous','pizza','burger','sushi','marocain']);
    $table->json('horaires')->nullable();              // JSON — tableau des horaires
    $table->decimal('note', 3, 2)->default(0.00);     // DECIMAL(3,2) — ex: 4.75
    $table->unsignedSmallInteger('nb_avis')->default(0);
    $table->boolean('actif')->default(true);
    $table->string('photo')->nullable();

    // Cle etrangere — proprietaire du restaurant
    $table->foreignId('proprietaire_id')              // BIGINT UNSIGNED
          ->constrained('users')                      // FK vers users.id
          ->restrictOnDelete();                       // Interdit supprimer un user avec restaurants

    $table->timestamps();                             // created_at + updated_at TIMESTAMP

    // Index pour les recherches frequentes
    $table->index(['ville', 'actif']);                // Index composite
    $table->index('note');                            // Index simple
});
phpMigration commandes — FK avec cascade
Schema::create('commandes', function (Blueprint $table) {
    $table->id();
    $table->foreignId('client_id')->constrained('users')->cascadeOnDelete();
    // cascadeOnDelete() : supprimer le client supprime ses commandes
    $table->foreignId('restaurant_id')->constrained()->restrictOnDelete();
    $table->foreignId('livreur_id')->nullable()->constrained('users')->nullOnDelete();
    // nullOnDelete() : si livreur supprime, livreur_id devient NULL

    $table->decimal('total', 8, 2);
    $table->enum('statut', ['en_attente','confirmee','en_preparation','prete','en_livraison','livree','annulee'])
          ->default('en_attente');
    $table->string('adresse_livraison');
    $table->text('notes')->nullable();
    $table->timestamp('acceptee_le')->nullable();
    $table->timestamp('livree_le')->nullable();
    $table->timestamps();

    $table->index(['client_id', 'statut']);
    $table->index('created_at');
});

// Table pivot commande_plat (many-to-many)
Schema::create('commande_plat', function (Blueprint $table) {
    $table->foreignId('commande_id')->constrained()->cascadeOnDelete();
    $table->foreignId('plat_id')->constrained()->restrictOnDelete();
    $table->unsignedTinyInteger('quantite')->default(1);
    $table->decimal('prix_unitaire', 8, 2);  // Prix au moment de la commande
    $table->primary(['commande_id', 'plat_id']); // Cle primaire composite
});
phpModifier une table existante — ajouter une colonne
// Fichier : ajouter_note_google_to_restaurants_table
return new class extends Migration
{
    public function up(): void
    {
        Schema::table('restaurants', function (Blueprint $table) {
            $table->decimal('note_google', 3, 2)->nullable()->after('note');
            $table->string('place_id_google', 50)->nullable()->after('note_google');
        });
    }

    public function down(): void
    {
        Schema::table('restaurants', function (Blueprint $table) {
            $table->dropColumn(['note_google', 'place_id_google']);
        });
    }
};
CommandeActionUsage
migrateExécuter les migrations non exécutéesStandard — tous environnements
migrate:statusAfficher l'état de chaque migrationDébogage — voir ce qui est appliqué
migrate:rollbackAnnuler le dernier batch de migrationsCorriger une migration en développement
migrate:rollback --step=3Annuler les 3 derniers batchsRevenir plusieurs étapes en arrière
migrate:freshSupprimer toutes les tables + remigrerDéveloppement local uniquement — DANGER prod
migrate:fresh --seedFresh + seedersReset complet de la BDD de test
migrate --pretendAfficher le SQL sans exécuterVérifier avant d'appliquer en production
La migration create_plats_table utilise foreignId('restaurant_id')->constrained(). Un restaurateur tente de supprimer son restaurant qui a encore 5 plats. Que se passe-t-il si la contrainte est restrictOnDelete() ?
Il faut ajouter une colonne note_google à la table restaurants déjà en production. Quelle est la bonne approche ?
Partie 5 — Base de données Seeders · Factories Faker · DatabaseSeeder
⏱ 18 min

Seeders & Factories

Peupler la base de données avec des données initiales (Seeders) et des données de test réalistes (Factories + Faker) — données marocaines pour DariGo, DatabaseSeeder, factory()->count()->create().

✓ Ce que vous allez apprendre
  • Différence données initiales (livrées avec l'app) vs données de test
  • Créer un Seeder avec php artisan make:seeder
  • Créer une Factory avec php artisan make:factory et Faker
  • Faker localisé pour le Maroc — noms, villes, téléphones marocains
  • DatabaseSeeder — orchestrer l'ordre des seeders
  • factory()->count(50)->create(), ->for(), ->has()

1
Seeders — données initiales DariGo
make:seeder · DB::table()->insert() · DatabaseSeeder · db:seed
bashGénérer les seeders
php artisan make:seeder VillesSeeder         # Donnees initiales — villes marocaines
php artisan make:seeder CategoriesSeeder     # Categories de restaurants
php artisan make:seeder AdminUserSeeder      # Compte admin initial
php artisan make:seeder RestaurantSeeder     # Restaurants de test
phpdatabase/seeders/VillesSeeder.php — données initiales
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class VillesSeeder extends Seeder
{
    public function run(): void
    {
        // truncate() vide la table avant d'inserer (evite les doublons)
        DB::table('villes')->truncate();

        DB::table('villes')->insert([
            ['nom' => 'Casablanca',  'region' => 'Casablanca-Settat',    'code_postal' => '20000'],
            ['nom' => 'Rabat',       'region' => 'Rabat-Salé-Kénitra',   'code_postal' => '10000'],
            ['nom' => 'Marrakech',   'region' => 'Marrakech-Safi',       'code_postal' => '40000'],
            ['nom' => 'Fès',         'region' => 'Fès-Meknès',           'code_postal' => '30000'],
            ['nom' => 'Tanger',      'region' => 'Tanger-Tétouan-Al Hoceima', 'code_postal' => '90000'],
            ['nom' => 'Agadir',      'region' => 'Souss-Massa',          'code_postal' => '80000'],
            ['nom' => 'Meknès',      'region' => 'Fès-Meknès',           'code_postal' => '50000'],
            ['nom' => 'Oujda',       'region' => 'Oriental',             'code_postal' => '60000'],
            ['nom' => 'Kénitra',     'region' => 'Rabat-Salé-Kénitra',   'code_postal' => '14000'],
            ['nom' => 'El Jadida',   'region' => 'Casablanca-Settat',    'code_postal' => '24000'],
        ]);

        $this->command->info('Villes marocaines insérées : ' . DB::table('villes')->count());
    }
}
phpdatabase/seeders/AdminUserSeeder.php
use App\Models\User;
use Illuminate\Support\Facades\Hash;

class AdminUserSeeder extends Seeder
{
    public function run(): void
    {
        // firstOrCreate() : creer uniquement si n'existe pas (idempotent)
        User::firstOrCreate(
            ['email' => 'admin@darigo.ma'],
            [
                'prenom'    => 'Admin',
                'nom'       => 'DariGo',
                'password'  => Hash::make('Admin2024!'),
                'role'      => 'admin',
                'ville'     => 'Casablanca',
                'telephone' => '0522000000',
                'email_verified_at' => now(),
            ]
        );

        $this->command->info('Compte admin crée : admin@darigo.ma / Admin2024!');
    }
}

2
Factories — données de test avec Faker marocain
make:factory · definition() · Faker · faker_locale · states
phpdatabase/factories/UserFactory.php — utilisateurs DariGo
<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;

class UserFactory extends Factory
{
    // Utiliser le Faker fr_MA (francais marocain) pour des donnees realistes
    protected $model = \App\Models\User::class;

    // Donnees marocaines statiques pour les noms et villes
    private static array $prenomsMarocains = [
        'Karim', 'Nadia', 'Youssef', 'Fatima', 'Mohamed', 'Leila',
        'Amine', 'Sara', 'Hassan', 'Kenza', 'Rachid', 'Imane',
    ];
    private static array $nomsMarocains = [
        'Alaoui', 'Benali', 'Chraibi', 'Idrissi', 'Mansouri', 'Tazi',
        'Bennani', 'El Fassi', 'Berrada', 'Filali', 'Guessous', 'Hajji',
    ];
    private static array $villesMarocaines = [
        'Casablanca', 'Rabat', 'Marrakech', 'Fès', 'Tanger', 'Agadir',
    ];

    public function definition(): array
    {
        $prenom = $this->faker->randomElement(self::$prenomsMarocains);
        $nom    = $this->faker->randomElement(self::$nomsMarocains);
        $ville  = $this->faker->randomElement(self::$villesMarocaines);

        return [
            'prenom'     => $prenom,
            'nom'        => $nom,
            'email'      => strtolower($prenom . '.' . $nom . $this->faker->unique()->randomNumber(3)) . '@gmail.com',
            'telephone'  => '06' . $this->faker->numerify('########'),  // 06XXXXXXXX
            'password'   => Hash::make('password'),
            'role'       => 'client',
            'ville'      => $ville,
            'email_verified_at' => now(),
        ];
    }

    // States — variantes de la factory
    public function restaurateur(): static
    {
        return $this->state(['role' => 'restaurateur']);
    }

    public function livreur(): static
    {
        return $this->state(['role' => 'livreur']);
    }

    public function admin(): static
    {
        return $this->state([
            'role'  => 'admin',
            'email' => 'admin@darigo.ma',
        ]);
    }
}
phpdatabase/factories/RestaurantFactory.php
class RestaurantFactory extends Factory
{
    protected $model = Restaurant::class;

    private static array $nomsRestaurants = [
        'Riad Al Baraka', 'Le Tajine d\'Or', 'Casa Blanca Grill',
        'Dar Zitoun', 'La Maison Berbere', 'Oasis Culinaire',
        'Le Couscous Royal', 'Marrakech Saveurs', 'Atlantic Grill',
    ];

    private static array $categories = [
        'tajine', 'couscous', 'pizza', 'burger', 'sushi', 'marocain',
    ];

    public function definition(): array
    {
        $nom  = $this->faker->randomElement(self::$nomsRestaurants);
        $slug = \Str::slug($nom) . '-' . $this->faker->unique()->randomNumber(4);

        return [
            'nom'            => $nom,
            'slug'           => $slug,
            'description'    => $this->faker->paragraph(2),
            'ville'          => $this->faker->randomElement(['Casablanca','Rabat','Marrakech','Fès']),
            'adresse'        => $this->faker->numberBetween(1, 200) . ' Rue ' . $this->faker->word(),
            'telephone'      => '05' . $this->faker->numerify('########'),
            'categorie'      => $this->faker->randomElement(self::$categories),
            'note'           => $this->faker->randomFloat(2, 2.0, 5.0),
            'nb_avis'        => $this->faker->numberBetween(0, 500),
            'actif'          => true,
            'proprietaire_id'=> User::factory()->restaurateur(),  // Creer un proprietaire
        ];
    }

    public function inactif(): static
    {
        return $this->state(['actif' => false]);
    }
}

3
DatabaseSeeder — orchestrer et exécuter
$this->call() · factory()->count()->create() · ->for() · ->has()
phpdatabase/seeders/DatabaseSeeder.php — DariGo complet
<?php

namespace Database\Seeders;

use App\Models\{Restaurant, User, Plat, Commande};
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        // ---- DONNEES INITIALES (livrees avec l'application) ----
        $this->call([
            VillesSeeder::class,       // Villes marocaines
            CategoriesSeeder::class,   // Categories de plats
            AdminUserSeeder::class,    // Compte admin
        ]);

        // ---- DONNEES DE TEST (developpement uniquement) ----
        if (app()->environment('local', 'testing')) {

            // 1. Creer des utilisateurs par role
            $clients      = User::factory(50)->create();                    // 50 clients
            $restaurateurs = User::factory(10)->restaurateur()->create();   // 10 restaurateurs
            $livreurs      = User::factory(20)->livreur()->create();       // 20 livreurs

            // 2. Creer des restaurants pour chaque restaurateur
            $restaurants = Restaurant::factory(30)
                ->recycle($restaurateurs)   // Reutiliser les restaurateurs existants
                ->create();

            // 3. Creer des plats pour chaque restaurant (5 a 15 plats)
            $restaurants->each(function ($restaurant) {
                Plat::factory(rand(5, 15))
                    ->for($restaurant)      // Lier au restaurant
                    ->create();
            });

            // 4. Creer des commandes pour les clients
            $clients->each(function ($client) use ($restaurants) {
                Commande::factory(rand(0, 5))
                    ->for($client, 'client')
                    ->for($restaurants->random(), 'restaurant')
                    ->create();
            });

            $this->command->info("Base de donnees peuplee :");
            $this->command->table(
                ['Table', 'Count'],
                [
                    ['Utilisateurs', User::count()],
                    ['Restaurants', Restaurant::count()],
                    ['Commandes', Commande::count()],
                ]
            );
        }
    }
}
bashExécuter les seeders
php artisan db:seed                          # Executer DatabaseSeeder
php artisan db:seed --class=VillesSeeder     # Seeder specifique
php artisan migrate:fresh --seed             # Reset BDD + tous les seeders
php artisan migrate:fresh --seed --seeder=AdminUserSeeder  # Reset + seeder specifique
php artisan db:seed est exécuté deux fois de suite sans truncate() dans le seeder. Que se passe-t-il ?
Partie 5 — Base de données Redis · Optimisation Cache · Sessions · Queues
⏱ 16 min

Redis & Optimisation

Redis comme BDD clé-valeur ultra-rapide pour le cache, les sessions et les queues Laravel — installation, commandes Redis::set/get/hset/incr, configuration Laravel, et stratégies d'optimisation pour l'API DariGo.

✓ Ce que vous allez apprendre
  • Redis — qu'est-ce que c'est, pourquoi c'est 100x plus rapide que MySQL
  • Installer Redis + predis/predis ou phpredis
  • Commandes Redis : SET/GET, HSET/HGET, INCR, EXPIRE, TTL
  • Redis pour le cache Laravel — CACHE_STORE=redis
  • Redis pour les sessions — SESSION_DRIVER=redis
  • Redis pour les queues — QUEUE_CONNECTION=redis

1
Redis — introduction et installation
Base clé-valeur · In-memory · Structures de données · Predis
ℹ Pourquoi Redis pour DariGo

MySQL : optimisé pour les données persistantes (transactions, relations). Mais pour lire la liste des restaurants 10 000 fois par heure, chaque lecture MySQL prend 5-50ms. Redis stocke les données en RAM : lecture en 0.1ms — 100x plus rapide. Redis n'est pas un remplacement de MySQL, mais un complément pour les données fréquemment lues et peu modifiées.

Structure RedisCommandes clésUsage DariGo
StringSET key val EX 300, GET keyCache d'un restaurant, compteur de vues
HashHSET key field val, HGET, HGETALLProfil utilisateur (plusieurs champs)
ListLPUSH key val, LRANGE key 0 -1File de jobs, historique de connexions
SetSADD key val, SMEMBERS keyRestaurants favoris d'un utilisateur
Sorted SetZADD key score val, ZRANGETop restaurants par note
bashInstallation Redis + Laravel
# Installer Redis (Ubuntu/Debian)
sudo apt install redis-server -y
redis-server --version    # Verifier

# macOS
brew install redis && brew services start redis

# Tester Redis en CLI
redis-cli ping            # PONG
redis-cli set test "DariGo fonctionne"
redis-cli get test        # "DariGo fonctionne"

# Installer le client PHP Redis
composer require predis/predis    # Pure PHP (toujours compatible)
# OU : installer l'extension phpredis (plus rapide, necessite compilation)

# Configurer .env
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

2
Facade Redis:: — opérations depuis Laravel
set() · get() · hset() · incr() · expire() · pipeline()
phpRedis:: — opérations DariGo
use Illuminate\Support\Facades\Redis;

// ---- STRINGS ----
Redis::set('darigo:restaurant:5', json_encode($restaurant));   // Stocker un objet
Redis::expire('darigo:restaurant:5', 300);                     // Expiration 5 minutes
// ou en une commande :
Redis::setex('darigo:restaurant:5', 300, json_encode($restaurant));

$data = Redis::get('darigo:restaurant:5');                     // Retourne la string JSON
$restaurant = json_decode($data, true);                        // Decoder

Redis::exists('darigo:restaurant:5');                          // true/false
Redis::ttl('darigo:restaurant:5');                             // Temps restant en secondes
Redis::del('darigo:restaurant:5');                             // Supprimer

// ---- HASHES — stocker un objet champ par champ ----
Redis::hset('darigo:user:42', 'prenom', 'Karim');
Redis::hset('darigo:user:42', 'ville', 'Casablanca');
Redis::hset('darigo:user:42', 'nb_commandes', 15);
Redis::expire('darigo:user:42', 3600);   // 1 heure

$prenom     = Redis::hget('darigo:user:42', 'prenom');    // 'Karim'
$profil     = Redis::hgetall('darigo:user:42');            // ['prenom'=>'Karim',...]

// ---- COMPTEURS — incrementer/decrementer ----
Redis::incr('darigo:stats:vues:restaurant:5');             // +1
Redis::incrby('darigo:stats:vues:restaurant:5', 5);        // +5
Redis::decr('darigo:stock:plat:12');                       // -1

// ---- LISTES ----
Redis::lpush('darigo:notifications:user:42', json_encode(['type' => 'livraison', 'msg' => 'Commande livree']));
$notifs = Redis::lrange('darigo:notifications:user:42', 0, 9); // 10 dernieres

// ---- PIPELINE — executer plusieurs commandes en une requete reseau ----
$results = Redis::pipeline(function ($pipe) {
    $pipe->set('darigo:test:1', 'valeur1');
    $pipe->set('darigo:test:2', 'valeur2');
    $pipe->incr('darigo:compteur');
});  // 3 commandes en 1 aller-retour reseau (vs 3 allers-retours)

3
Redis pour Cache, Sessions et Queues
.env CACHE_STORE · SESSION_DRIVER · QUEUE_CONNECTION · redis
bash.env production DariGo — tout via Redis
# Cache — stocker les resultats de requetes (restaurants, menus...)
CACHE_STORE=redis

# Sessions — stocker les sessions utilisateur (plus scalable que fichiers)
SESSION_DRIVER=redis

# Queues — jobs en arriere-plan (emails, notifications SMS...)
QUEUE_CONNECTION=redis
phpPattern Cache-Aside DariGo — optimisation complète
// ---- CACHE RESTAURANTS (Cache::remember) ----
public function index(Request $request): JsonResponse
{
    $ville     = $request->get('ville', 'Casablanca');
    $cacheKey  = "restaurants:{$ville}:" . md5(json_encode($request->all()));

    $restaurants = Cache::remember($cacheKey, 300, function () use ($request, $ville) {
        return Restaurant::with('plats')
            ->where('ville', $ville)
            ->where('actif', true)
            ->when($request->categorie, fn($q, $cat) => $q->where('categorie', $cat))
            ->orderBy('note', 'desc')
            ->paginate(12);
    });

    return response()->json($restaurants);
}

// ---- INVALIDATION CACHE quand un restaurant est modifie ----
// Dans RestaurantObserver::updated() ou dans le Controller update()
Cache::tags(['restaurants'])->flush();    // Redis uniquement (tags non supporte file)
// Ou : Cache::forget("restaurants:Casablanca:...");

// ---- SESSIONS REDIS ----
// Aucun changement de code — Session facade fonctionne identiquement
session()->put('panier', $panier);
session()->get('panier');

// ---- QUEUE REDIS ----
// Demarrer le worker Redis
// php artisan queue:work redis --queue=emails,default
// Supervision avec Supervisor en production (process manager)
✓ Comparaison des performances — avant/après Redis
IndicateurSans Redis (MySQL uniquement)Avec Redis
Temps de réponse GET /restaurants~80ms (MySQL)~2ms (Redis hit)
Charge MySQL à 1000 req/min1000 requêtes SQL/min~2 requêtes SQL/min (expirations)
Scalabilité sessions (plusieurs serveurs)Impossible (fichiers locaux)Native (Redis partagé)
Délai des queuesDB: poll toutes les 3sRedis: push instantané
Redis stocke les données en RAM. Que se passe-t-il si le serveur Redis redémarre et que la persistance n'est pas configurée ?
DariGo déploie 3 serveurs Laravel en load balancing. Sans Redis pour les sessions, qu'est-ce qui se passe si un utilisateur est dirigé vers un serveur différent à chaque requête ?
Partie 6 — Eloquent ORM Modèles · CRUD $fillable · Scopes
⏱ 22 min

Modèles Eloquent & CRUD

Créer et configurer les modèles Eloquent, conventions de nommage, mass assignment ($fillable/$guarded), opérations CRUD (all, find, create, update, delete), requêtes de récupération avancées et pagination.

✓ Ce que vous allez apprendre
  • Créer un modèle avec php artisan make:model -m — conventions de nommage
  • Configuration : $fillable, $guarded, $table, $primaryKey, $casts
  • Mass assignment — sécurité et vulnérabilités
  • CRUD Eloquent : all(), find(), findOrFail(), create(), update(), delete()
  • Requêtes avancées : where(), selectRaw(), groupBy(), having()
  • Pagination : paginate(), simplePaginate(), cursorPaginate()

1
Créer et configurer un modèle Eloquent
make:model · conventions · $fillable · $guarded · $casts · $table
ℹ Eloquent — principe de base

Chaque table MySQL correspond à un modèle Eloquent. Le modèle Restaurant correspond automatiquement à la table restaurants (nom au pluriel, minuscules). Eloquent encapsule les enregistrements en objets PHP — plus besoin d'écrire de SQL pour les opérations courantes.

bashGénérer les modèles DariGo
# Modele seul
php artisan make:model Restaurant

# Modele + migration + seeder + factory + controller resource (tout en une commande)
php artisan make:model Restaurant -mcsf --resource
# ou : php artisan make:model Restaurant --all

# Raccourcis : -m=migration, -c=controller, -s=seeder, -f=factory, -r=resource
phpapp/Models/Restaurant.php — modèle complet DariGo
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Restaurant extends Model
{
    use HasFactory, SoftDeletes;

    // -- Optionnel : si le nom de la table ne respecte pas la convention --
    // protected $table = 'restaurants';        // Laravel devine automatiquement
    // protected $primaryKey = 'id';            // Par defaut 'id'
    // public $timestamps = true;               // created_at + updated_at geres auto

    // -- Mass Assignment : liste blanche des colonnes autorisees --
    protected $fillable = [
        'nom',
        'slug',
        'description',
        'ville',
        'adresse',
        'telephone',
        'categorie',
        'horaires',
        'actif',
        'photo',
        'proprietaire_id',
    ];

    // -- Casts : conversion automatique des types --
    protected function casts(): array
    {
        return [
            'horaires'   => 'array',   // JSON <-> tableau PHP automatiquement
            'actif'      => 'boolean', // 0/1 <-> true/false
            'note'       => 'float',
            'created_at' => 'datetime',
        ];
    }

    // -- Attributs caches (jamais dans toArray/toJson) --
    protected $hidden = [
        'proprietaire_id', // Ne pas exposer dans l'API
    ];

    // -- Attributs toujours charges avec le modele --
    protected $appends = ['disponible'];
}
⚠ Mass Assignment — vulnérabilité à connaître

Si le modèle User a un champ role non listé dans $fillable, un attaquant ne peut pas l'injecter via User::create($request->all()). Sans $fillable ni $guarded : MassAssignmentException. Avec $guarded = [] : tout est autorisé — à utiliser avec prudence uniquement quand les données sont déjà validées par une Form Request.


2
CRUD Eloquent — toutes les opérations
all() · find() · findOrFail() · create() · update() · delete() · firstOrCreate()
phpCRUD Eloquent — référence complète DariGo
// ---- LIRE ----
$restaurants = Restaurant::all();                         // Tous les restaurants
$restaurant  = Restaurant::find(5);                       // Par id (null si absent)
$restaurant  = Restaurant::findOrFail(5);                 // Par id (404 si absent)
$restaurant  = Restaurant::where('slug', 'dar-zitoun')->first();
$restaurant  = Restaurant::where('slug', 'dar-zitoun')->firstOrFail();

// Selectionner des colonnes specifiques
$noms = Restaurant::select('id', 'nom', 'ville')->get();

// Conditions multiples
$restaurants = Restaurant::where('ville', 'Casablanca')
    ->where('actif', true)
    ->where('note', '>=', 4.0)
    ->orderBy('note', 'DESC')
    ->limit(10)
    ->get();

// ---- CREER ----
// Methode 1 : create() — mass assignment (fillable requis)
$restaurant = Restaurant::create([
    'nom'             => 'Dar Zitoun',
    'slug'            => 'dar-zitoun',
    'ville'           => 'Marrakech',
    'adresse'         => '14 Rue Moulay Ali, Medina',
    'categorie'       => 'marocain',
    'proprietaire_id' => auth()->id(),
]);

// Methode 2 : new + save() (utile pour initialiser puis modifier)
$restaurant = new Restaurant();
$restaurant->nom    = 'Dar Zitoun';
$restaurant->ville  = 'Marrakech';
$restaurant->save();
// $restaurant->id est maintenant disponible

// firstOrCreate() — creer seulement si n'existe pas
$restaurant = Restaurant::firstOrCreate(
    ['slug' => 'dar-zitoun'],                   // Conditions de recherche
    ['nom' => 'Dar Zitoun', 'ville' => 'Marrakech']  // Valeurs si creation
);

// updateOrCreate() — modifier si existe, creer sinon
Restaurant::updateOrCreate(
    ['slug' => 'dar-zitoun'],
    ['note' => 4.7, 'nb_avis' => 150]
);

// ---- MODIFIER ----
// Methode 1 : update() sur une instance
$restaurant = Restaurant::findOrFail(5);
$restaurant->update(['note' => 4.8, 'nb_avis' => 200]);

// Methode 2 : update() en masse (tous les restaurants de Casablanca)
Restaurant::where('ville', 'Casablanca')->update(['actif' => false]);

// Methode 3 : assigner puis save
$restaurant->note = 4.9;
$restaurant->save();

// fresh() — recharger depuis la BDD apres modification
$restaurant = $restaurant->fresh();

// ---- SUPPRIMER ----
$restaurant = Restaurant::findOrFail(5);
$restaurant->delete();    // Soft delete si SoftDeletes est activee

// Supprimer en masse
Restaurant::where('actif', false)->where('created_at', '<', now()->subYear())->delete();

// Restaurer un soft delete
Restaurant::withTrashed()->find(5)->restore();

// Supprimer definitivement (bypass soft delete)
Restaurant::withTrashed()->find(5)->forceDelete();

3
Requêtes Eloquent avancées et pagination
selectRaw() · groupBy() · having() · paginate() · withCount()
phpRequêtes avancées — statistiques DariGo
// Colonne calculee avec selectRaw
$plats = Plat::select('nom', 'prix')
    ->selectRaw('prix * 1.2 as prix_avec_tva')
    ->where('actif', true)
    ->get();

// Statistiques par ville
$stats = Restaurant::selectRaw('ville, COUNT(*) as nb_restaurants, AVG(note) as note_moyenne')
    ->where('actif', true)
    ->groupBy('ville')
    ->having('note_moyenne', '>=', 3.5)
    ->orderBy('nb_restaurants', 'DESC')
    ->get();
// Resultat : [['ville'=>'Casablanca', 'nb_restaurants'=>45, 'note_moyenne'=>4.2], ...]

// Top restaurants avec nb_commandes (withCount)
$topRestaurants = Restaurant::withCount('commandes')
    ->withAvg('commandes', 'total')
    ->where('actif', true)
    ->orderBy('commandes_count', 'DESC')
    ->limit(10)
    ->get();
// Chaque restaurant a : $restaurant->commandes_count, $restaurant->commandes_avg_total

// ---- PAGINATION ----
// paginate() — avec total count (pour afficher "Page 2/15")
$restaurants = Restaurant::where('actif', true)
    ->orderBy('note', 'DESC')
    ->paginate(12);
// JSON API : {"data":[...],"current_page":1,"last_page":15,"total":180,"per_page":12}

// simplePaginate() — sans total count (plus rapide sur grandes tables)
$restaurants = Restaurant::paginate(12);

// cursorPaginate() — pour les feeds infinis (pas de sauts de pages)
$restaurants = Restaurant::orderBy('id')->cursorPaginate(20);

// Pagination avec filtres (preserveQuery)
// GET /api/restaurants?ville=Casablanca&page=2
$restaurants = Restaurant::where('ville', request('ville'))
    ->paginate(12)
    ->withQueryString(); // Inclut ?ville=Casablanca dans les liens de pagination
Restaurant::findOrFail(999) est appelé et le restaurant #999 n'existe pas. Que se passe-t-il dans une route API avec la gestion globale des exceptions configurée ?
Un modèle utilise SoftDeletes. Restaurant::where('actif', false)->delete() est appelé. Que contient la colonne deleted_at de ces restaurants ?
Partie 6 — Eloquent ORM Relations Eager Loading · N+1
⏱ 24 min

Relations Eloquent

Toutes les relations Eloquent appliquées à DariGo — hasOne, hasMany, belongsTo, belongsToMany (table pivot, withPivot, attach/detach/sync), hasManyThrough, relations polymorphes, eager loading et résolution du problème N+1.

✓ Ce que vous allez apprendre
  • hasOne — un restaurant a un seul paramètre de livraison
  • hasMany / belongsTo — un restaurant a plusieurs plats
  • belongsToMany — commandes et plats via table pivot commande_plat
  • withPivot(), attach(), detach(), sync()
  • hasManyThrough — un client a plusieurs factures via ses commandes
  • Problème N+1 — détection et solution avec with()
  • has(), whereHas(), withCount()

1
hasOne et hasMany / belongsTo — DariGo
1-1 · 1-N · clé étrangère · méthode vs propriété
phpapp/Models/Restaurant.php — relations DariGo
class Restaurant extends Model
{
    // hasOne : un restaurant a un seul profil de livraison
    public function profilLivraison(): HasOne
    {
        return $this->hasOne(ProfilLivraison::class);
        // Laravel cherche profilLivraison.restaurant_id par convention
    }

    // hasMany : un restaurant a plusieurs plats
    public function plats(): HasMany
    {
        return $this->hasMany(Plat::class);
        // Laravel cherche plats.restaurant_id
    }

    // hasMany : un restaurant a plusieurs commandes
    public function commandes(): HasMany
    {
        return $this->hasMany(Commande::class);
    }

    // belongsTo : un restaurant appartient a un proprietaire (User)
    public function proprietaire(): BelongsTo
    {
        return $this->belongsTo(User::class, 'proprietaire_id');
        // 'proprietaire_id' = nom explicite de la FK (convention = user_id)
    }
}

class Plat extends Model
{
    // belongsTo : un plat appartient a un restaurant
    public function restaurant(): BelongsTo
    {
        return $this->belongsTo(Restaurant::class);
        // Laravel cherche plats.restaurant_id (convention automatique)
    }
}
phpUtiliser les relations — méthode vs propriété
$restaurant = Restaurant::find(5);

// Propriete (acces direct) — lazy loading (1 requete SQL supplementaire)
$plats = $restaurant->plats;          // Collection de Plat
$proprio = $restaurant->proprietaire; // Objet User

// Methode (relation comme query builder) — ajouter des contraintes
$platsActifs = $restaurant->plats()->where('actif', true)->orderBy('prix')->get();
$platsDispos = $restaurant->plats()->where('stock', '>', 0)->count();

// Ajouter via la relation (client_id/restaurant_id assigné automatiquement)
$restaurant->plats()->create([
    'nom'        => 'Pastilla au Poulet',
    'prix'       => 65.00,
    'stock'      => 20,
    'categorie'  => 'plat',
]);

// save() et saveMany()
$restaurant->plats()->save($plat);
$restaurant->plats()->saveMany([$plat1, $plat2]);

// Requetes sur les relations
Restaurant::has('plats')->get();                     // Restaurants avec au moins 1 plat
Restaurant::has('plats', '>=', 5)->get();            // Au moins 5 plats
Restaurant::whereHas('plats', function ($q) {        // Plats < 50 MAD
    $q->where('prix', '<', 50);
})->get();
Restaurant::withCount('plats')->get();               // + champ plats_count

2
belongsToMany — table pivot commande_plat
Table pivot · withPivot() · attach() · detach() · sync() · pivot->quantite
phpapp/Models/Commande.php — relation N-N avec table pivot
class Commande extends Model
{
    // Une commande a plusieurs plats, et un plat est dans plusieurs commandes
    public function plats(): BelongsToMany
    {
        return $this->belongsToMany(Plat::class, 'commande_plat')
            ->withPivot(['quantite', 'prix_unitaire'])  // Colonnes de la table pivot
            ->withTimestamps();                         // created_at/updated_at de la pivot
    }

    // Client de la commande
    public function client(): BelongsTo
    {
        return $this->belongsTo(User::class, 'client_id');
    }

    // Restaurant de la commande
    public function restaurant(): BelongsTo
    {
        return $this->belongsTo(Restaurant::class);
    }
}

class Plat extends Model
{
    public function commandes(): BelongsToMany
    {
        return $this->belongsToMany(Commande::class, 'commande_plat')
            ->withPivot(['quantite', 'prix_unitaire'])
            ->withTimestamps();
    }
}
phpManipuler la table pivot — attach/detach/sync
$commande = Commande::find(3);

// attach() — ajouter un plat a la commande avec donnees pivot
$commande->plats()->attach(12, [
    'quantite'      => 2,
    'prix_unitaire' => 65.00,
]);

// Ajouter plusieurs plats
$commande->plats()->attach([
    12 => ['quantite' => 2, 'prix_unitaire' => 65.00],
    15 => ['quantite' => 1, 'prix_unitaire' => 45.00],
]);

// detach() — retirer un plat
$commande->plats()->detach(12);
$commande->plats()->detach();     // Retirer TOUS les plats

// sync() — synchroniser (supprime les anciens, ajoute les nouveaux)
$commande->plats()->sync([
    12 => ['quantite' => 3, 'prix_unitaire' => 65.00],
    20 => ['quantite' => 1, 'prix_unitaire' => 55.00],
]);

// Lire les donnees pivot
foreach ($commande->plats as $plat) {
    echo $plat->nom . ' x' . $plat->pivot->quantite;
    echo ' = ' . ($plat->prix * $plat->pivot->quantite) . ' MAD';
}

// Calculer le total d'une commande
$total = $commande->plats->sum(function ($plat) {
    return $plat->prix * $plat->pivot->quantite;
});

// Filtrer sur les donnees pivot
$commande->plats()->wherePivot('quantite', '>', 1)->get();

// Liste des commandes de plus de 3 plats
Commande::has('plats', '>=', 3)->get();

// Liste des commandes avec des plats dont le prix > 100 MAD
Commande::whereHas('plats', function ($q) {
    $q->where('prix', '>', 100);
})->get();

3
hasManyThrough & problème N+1
hasManyThrough · N+1 · with() eager loading · lazy eager loading
phphasManyThrough — un client accède à tous ses plats commandés
class User extends Authenticatable
{
    // hasMany direct
    public function commandes(): HasMany
    {
        return $this->hasMany(Commande::class, 'client_id');
    }

    // hasManyThrough — tous les plats commandes par ce client (via ses commandes)
    // Client -> Commandes -> Plats (a travers la table pivot commande_plat)
    public function platsCommandes(): BelongsToMany
    {
        // Pour hasManyThrough sur many-to-many, passer par la relation
        return $this->hasManyThrough(Plat::class, Commande::class);
        // Ou via une requete personnalisee
    }

    // Exemple hasManyThrough classique : Client -> Commandes -> Livraisons
    public function livraisons(): HasManyThrough
    {
        return $this->hasManyThrough(Livraison::class, Commande::class);
        // User hasMany Commandes, Commandes hasMany Livraisons
        // => User hasManyThrough Livraisons via Commandes
    }
}

// Utilisation
$client   = User::find(7);
$livraisons = $client->livraisons;  // Toutes les livraisons de ce client
⚠ Problème N+1 — le piège le plus courant avec Eloquent

Si on affiche 50 restaurants avec leurs plats sans eager loading : 1 requête pour les restaurants + 50 requêtes pour les plats de chaque restaurant = 51 requêtes. Sur 100 restaurants : 101 requêtes. Solution : with('plats') = 2 requêtes quelle que soit la taille.

phpEager Loading — résoudre le problème N+1
// PROBLEME N+1 (ne pas faire)
$restaurants = Restaurant::all();   // 1 requete
foreach ($restaurants as $restaurant) {
    echo $restaurant->plats->count();  // 1 requete par restaurant = N requetes
}
// Total : 1 + N requetes (ex: 1 + 50 = 51)

// SOLUTION : with() — eager loading
$restaurants = Restaurant::with('plats')->get();   // 2 requetes au total
foreach ($restaurants as $restaurant) {
    echo $restaurant->plats->count();  // 0 requete supplementaire (en cache)
}

// Eager loading multiple
$restaurants = Restaurant::with(['plats', 'proprietaire', 'commandes'])->get();

// Eager loading avec contraintes
$restaurants = Restaurant::with([
    'plats' => function ($query) {
        $query->where('actif', true)->orderBy('prix');
    },
    'commandes' => function ($query) {
        $query->where('statut', 'livree')->latest()->limit(5);
    },
])->get();

// Eager loading imbrique (relations de relations)
$commandes = Commande::with('plats.restaurant')->get();
// Charge plats ET le restaurant de chaque plat

// Lazy eager loading (charger apres coup)
$restaurants = Restaurant::all();
$restaurants->load('plats');           // Charger apres
$restaurants->loadCount('commandes'); // Charger le count apres

// Detect N+1 en developpement
Model::preventLazyLoading(! app()->isProduction());
Un restaurateur modifie son menu : plats 3 et 7 restent, plat 5 est retiré, plat 9 est ajouté. Quelle méthode Eloquent effectue cette opération en une seule commande ?
Restaurant::with('plats') génère combien de requêtes SQL pour 100 restaurants ?
Partie 6 — Eloquent ORM Accessors · Mutators Scopes · API Resources
⏱ 20 min

Eloquent Avancé

Accessors et Mutators (attributs calculés et transformés), Casting ($casts), Scopes locaux et globaux, SoftDeletes, sérialisation (toArray()/toJson()) et API Resources pour formater les réponses JSON de l'API DariGo.

✓ Ce que vous allez apprendre
  • Accessors — attributs calculés en lecture (get{Attr}Attribute)
  • Mutators — transformer avant l'enregistrement (set{Attr}Attribute)
  • Casting $casts — JSON, boolean, encrypted, hashed
  • Scopes locaux — réutiliser des conditions (scopeActif())
  • Scopes globaux — conditions appliquées automatiquement
  • API Resources — formater les réponses JSON sans exposer la BDD

1
Accessors & Mutators — attributs calculés
get{Attr}Attribute · set{Attr}Attribute · Attribute::make() Laravel 9+
ℹ Accessor vs Mutator

Accessor : attribut calculé en lecture. $restaurant->nom_complet combine nom + ville sans colonne en BDD. Mutator : transformer avant l'écriture. $restaurant->nom = "dar zitoun" est automatiquement mis en majuscule avant save().

phpapp/Models/Restaurant.php — Accessors et Mutators
use Illuminate\Database\Eloquent\Casts\Attribute;

class Restaurant extends Model
{
    // Accessor : attribut calcule (pas de colonne en BDD)
    // $restaurant->nom_complet retourne "Dar Zitoun — Marrakech"
    protected function nomComplet(): Attribute
    {
        return Attribute::make(
            get: fn () => $this->nom . ' — ' . $this->ville,
        );
    }

    // Accessor + Mutator combines sur la meme propriete
    // $restaurant->nom = "dar zitoun" → stocke "Dar Zitoun" (ucwords)
    protected function nom(): Attribute
    {
        return Attribute::make(
            get: fn (string $value) => ucwords(strtolower($value)),  // Lecture : ucwords
            set: fn (string $value) => ucwords(strtolower($value)),  // Ecriture : ucwords
        );
    }

    // Accessor disponibilite (basee sur le champ actif + horaires)
    protected function estOuvert(): Attribute
    {
        return Attribute::make(
            get: function () {
                if (! $this->actif) return false;
                if (! $this->horaires) return true;

                $maintenant = now()->format('H:i');
                $jour = strtolower(now()->locale('fr')->isoFormat('dddd'));

                $horaire = $this->horaires[$jour] ?? null;
                if (! $horaire) return false;

                [$ouv, $ferm] = explode('-', $horaire);
                return $maintenant >= $ouv && $maintenant <= $ferm;
            }
        );
    }
}

// Utilisation
$restaurant = Restaurant::find(5);
echo $restaurant->nom_complet;    // "Dar Zitoun — Marrakech"
echo $restaurant->est_ouvert;     // true/false selon l'heure
$restaurant->nom = 'dar zitoun'; // Stocke "Dar Zitoun"
phpapp/Models/Plat.php — Accessor disponible
class Plat extends Model
{
    protected $appends = ['disponible', 'prix_formate'];

    // Accessible via $plat->disponible
    protected function disponible(): Attribute
    {
        return Attribute::make(
            get: fn () => $this->stock > 0 && $this->actif,
        );
    }

    // Formater le prix avec le symbole MAD
    protected function prixFormate(): Attribute
    {
        return Attribute::make(
            get: fn () => number_format($this->prix, 2) . ' MAD',
        );
    }
}

2
Scopes — réutiliser les conditions de requête
scopeActif() · Scope local · Scope global · Scope avec paramètre
ℹ Scopes — DRY pour les requêtes Eloquent

Sans Scope : ->where('actif', true) est répété dans chaque Controller. Avec un Scope local scopeActif() : Restaurant::actif()->get() partout. Le code de la condition n'est écrit qu'une fois, dans le modèle.

phpScopes — modèles DariGo
class Restaurant extends Model
{
    // ---- SCOPES LOCAUX ----

    // Restaurant::actif()->get()
    public function scopeActif(Builder $query): Builder
    {
        return $query->where('actif', true);
    }

    // Restaurant::dansVille('Casablanca')->get()
    public function scopeDansVille(Builder $query, string $ville): Builder
    {
        return $query->where('ville', $ville);
    }

    // Restaurant::bienNote(4.0)->get()
    public function scopeBienNote(Builder $query, float $seuil = 3.5): Builder
    {
        return $query->where('note', '>=', $seuil);
    }

    // Restaurant::deCategorie('tajine')->get()
    public function scopeDeCategorie(Builder $query, string $categorie): Builder
    {
        return $query->where('categorie', $categorie);
    }
}

class Plat extends Model
{
    // Plat::disponible()->get()
    public function scopeDisponible(Builder $query): Builder
    {
        return $query->where('actif', true)->where('stock', '>', 0);
    }

    // Plat::abordable(50)->get() — prix <= 50 MAD
    public function scopeAbordable(Builder $query, float $max = 100): Builder
    {
        return $query->where('prix', '<=', $max);
    }
}

// Utilisation enchainees — tres lisibles
$restaurants = Restaurant::actif()
    ->dansVille('Casablanca')
    ->bienNote(4.0)
    ->deCategorie('tajine')
    ->withCount('plats')
    ->paginate(12);

$plats = Plat::disponible()->abordable(80)->orderBy('prix')->get();

3
API Resources — formater les réponses JSON
make:resource · toArray() · ResourceCollection · $this->when()
ℹ Pourquoi les API Resources ?

Sans Resource : return response()->json($restaurant) expose toutes les colonnes de la BDD (y compris proprietaire_id, deleted_at...). Avec Resource : on contrôle exactement quels champs sont exposés, on peut ajouter des champs calculés, renommer des clés, conditionner les champs selon le rôle.

bashGénérer les API Resources DariGo
php artisan make:resource RestaurantResource
php artisan make:resource PlatResource
php artisan make:resource CommandeResource
php artisan make:resource RestaurantCollection  # Collection avec metadata
phpapp/Http/Resources/RestaurantResource.php
<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class RestaurantResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'          => $this->id,
            'nom'         => $this->nom,
            'slug'        => $this->slug,
            'description' => $this->description,
            'ville'       => $this->ville,
            'adresse'     => $this->adresse,
            'telephone'   => $this->telephone,
            'categorie'   => $this->categorie,
            'note'        => round($this->note, 1),
            'nb_avis'     => $this->nb_avis,
            'actif'       => $this->actif,
            'est_ouvert'  => $this->est_ouvert,  // Accessor

            // Inclure les plats seulement si charges (when loaded)
            'plats'  => PlatResource::collection($this->whenLoaded('plats')),
            'nb_plats' => $this->when(
                $this->relationLoaded('plats'),
                fn() => $this->plats->count()
            ),

            // Champs visibles uniquement par l'admin
            'proprietaire' => $this->when(
                auth()->user()?->role === 'admin',
                fn() => new UserResource($this->whenLoaded('proprietaire'))
            ),

            'created_at' => $this->created_at?->toDateString(),
        ];
    }
}

// Utilisation dans un Controller
public function show(Restaurant $restaurant): RestaurantResource
{
    $restaurant->load('plats');
    return new RestaurantResource($restaurant);
    // JSON : {"data": {"id":5, "nom":"Dar Zitoun", "plats":[...], ...}}
}

public function index(): AnonymousResourceCollection
{
    $restaurants = Restaurant::actif()->with('plats')->paginate(12);
    return RestaurantResource::collection($restaurants);
    // JSON avec pagination : {"data":[...], "links":{...}, "meta":{...}}
}
Dans une API Resource, $this->whenLoaded('plats') retourne quoi si les plats n'ont pas été chargés avec with('plats') ?
Un Scope local est défini : scopeActif($query). Comment l'appeler dans un Controller ?
Partie 7 — Tests PHPUnit Setup · Configuration TestCase · RefreshDatabase
⏱ 18 min

PHPUnit — Setup & Premiers tests

Configurer l'environnement de test Laravel — phpunit.xml, .env.testing, SQLite en mémoire, RefreshDatabase, setUp() et tearDown(), types de tests (Unit vs Feature), assertions de base et exécution.

✓ Ce que vous allez apprendre
  • Pourquoi tester — confiance, régression, documentation vivante
  • Unit vs Feature tests — quelle différence, quand choisir
  • Configurer phpunit.xml et .env.testing (BDD SQLite en mémoire)
  • Trait RefreshDatabase — BDD fraîche à chaque test
  • Méthodes setUp() et tearDown()
  • Assertions de base : assertTrue, assertEquals, assertNull
  • Exécuter les tests : php artisan test, filtres, couverture

1
Pourquoi tester — les 3 bénéfices concrets
Régression · Confiance · Documentation · ROI
ℹ Tests — investissement ou perte de temps ?

Sans tests : modifier le code de création de commande DariGo casse silencieusement le calcul du total — découvert en production par un client. Avec tests : php artisan test détecte la régression en 3 secondes avant le déploiement. Les tests sont une assurance, pas un luxe.

BénéficeSans testsAvec tests
RégressionUn bug cassé réapparaît sans qu'on le sacheDétecté immédiatement à chaque php artisan test
ConfianceDéployer en production = stressSi les tests passent, on peut déployer sereinement
DocumentationComportement attendu à deviner depuis le codeChaque test décrit un comportement attendu lisible
RefactoringDangereux — on ne sait pas ce qu'on casseSécurisé — les tests valident après chaque changement
TypeDossierCe qu'il testeExemple DariGo
Unit Testtests/Unit/Une classe ou méthode isolée — sans BDD, sans HTTPCalcul du total d'une commande, formatage du prix
Feature Testtests/Feature/Une fonctionnalité complète — avec BDD, HTTP, authPOST /api/commandes retourne 201 avec le bon JSON

2
Configuration — phpunit.xml et .env.testing
SQLite :memory: · CACHE_DRIVER=array · QUEUE_CONNECTION=sync
xmlphpunit.xml — configuration DariGo
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
         stopOnFailure="false">

  <testsuites>
    <testsuite name="Unit">
      <directory>tests/Unit</directory>
    </testsuite>
    <testsuite name="Feature">
      <directory>tests/Feature</directory>
    </testsuite>
  </testsuites>

  <source>
    <include>
      <directory>app</directory>
    </include>
  </source>

  <php>
    <!-- BDD SQLite en memoire : rapide, isolee, detruite apres les tests -->
    <env name="DB_CONNECTION" value="sqlite"/>
    <env name="DB_DATABASE" value=":memory:"/>

    <!-- Cache tableau : pas de Redis requis pour les tests -->
    <env name="CACHE_STORE" value="array"/>

    <!-- Queues synchrones : les jobs s'executent immediatement -->
    <env name="QUEUE_CONNECTION" value="sync"/>

    <!-- Mail : intercepter sans envoyer -->
    <env name="MAIL_MAILER" value="array"/>

    <!-- Bcrypt rapide pour les tests (4 rounds au lieu de 12) -->
    <env name="BCRYPT_ROUNDS" value="4"/>

    <!-- Sessions en tableau -->
    <env name="SESSION_DRIVER" value="array"/>
  </php>
</phpunit>
ℹ Pourquoi SQLite :memory: ?

SQLite en mémoire crée une BDD temporaire dans la RAM, sans fichier sur disque. Avantages : les migrations s'exécutent en quelques millisecondes (vs secondes avec MySQL), chaque suite de tests repart d'une BDD vide, et il n'y a aucun risque de polluer la BDD de développement. Inconvénient : les fonctions MySQL spécifiques (FULLTEXT, JSON_EXTRACT) ne sont pas disponibles.


3
Structure d'un test — setUp, tearDown, RefreshDatabase
make:test · setUp() · tearDown() · RefreshDatabase · DatabaseTransactions
bashGénérer des tests DariGo
php artisan make:test RestaurantTest           # Feature test
php artisan make:test CommandeTest            # Feature test
php artisan make:test CalculTotalTest --unit  # Unit test
phptests/Unit/CalculTotalTest.php — unit test
<?php

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;

class CalculTotalTest extends TestCase
{
    // Un Unit test n'etend pas Tests\TestCase mais PHPUnit\Framework\TestCase
    // Pas de BDD, pas de HTTP, pas de Laravel — ultra rapide

    public function test_calcul_total_commande(): void
    {
        $lignes = [
            ['prix' => 65.00, 'quantite' => 2],
            ['prix' => 45.00, 'quantite' => 1],
        ];

        $total = collect($lignes)->sum(fn($l) => $l['prix'] * $l['quantite']);

        $this->assertEquals(175.00, $total);
    }

    public function test_total_commande_vide(): void
    {
        $total = collect([])->sum(fn($l) => $l['prix'] * $l['quantite']);
        $this->assertEquals(0, $total);
    }
}
phptests/Feature/RestaurantTest.php — feature test complet
<?php

namespace Tests\Feature;

use App\Models\Restaurant;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class RestaurantTest extends TestCase
{
    use RefreshDatabase;
    // RefreshDatabase : reexecute toutes les migrations avant chaque test
    // La BDD repart de zero a chaque methode de test

    // Donnees partagees entre les methodes du test
    private User $admin;
    private User $client;
    private User $restaurateur;

    // setUp() : execute AVANT chaque methode de test
    protected function setUp(): void
    {
        parent::setUp(); // Toujours appeler parent::setUp()

        $this->admin        = User::factory()->create(['role' => 'admin']);
        $this->client       = User::factory()->create(['role' => 'client']);
        $this->restaurateur = User::factory()->create(['role' => 'restaurateur']);
    }

    // tearDown() : execute APRES chaque methode de test
    // (rarement necessaire avec RefreshDatabase)
    protected function tearDown(): void
    {
        // Nettoyer des fichiers, fermer des connexions...
        parent::tearDown();
    }

    /** @test */
    public function un_client_peut_voir_la_liste_des_restaurants(): void
    {
        Restaurant::factory(5)->create(['actif' => true]);

        $response = $this->actingAs($this->client, 'sanctum')
            ->getJson('/api/restaurants');

        $response->assertStatus(200)
                 ->assertJsonCount(5, 'data');
    }

    /** @test */
    public function un_restaurateur_peut_creer_un_restaurant(): void
    {
        $donnees = [
            'nom'       => 'Dar Zitoun Test',
            'ville'     => 'Marrakech',
            'adresse'   => '14 Rue Hassan II',
            'categorie' => 'marocain',
        ];

        $response = $this->actingAs($this->restaurateur, 'sanctum')
            ->postJson('/api/restaurants', $donnees);

        $response->assertStatus(201)
                 ->assertJsonFragment(['nom' => 'Dar Zitoun Test']);
    }

    /** @test */
    public function un_client_ne_peut_pas_creer_un_restaurant(): void
    {
        $response = $this->actingAs($this->client, 'sanctum')
            ->postJson('/api/restaurants', ['nom' => 'Faux restaurant']);

        $response->assertStatus(403);
    }
}

4
Exécuter les tests
php artisan test · --filter · --testsuite · --coverage · --parallel
bashCommandes d'exécution des tests
# Tous les tests
php artisan test

# Avec affichage detaille
php artisan test --verbose

# Filtrer par nom de methode
php artisan test --filter=un_client_peut_voir_la_liste

# Filtrer par classe
php artisan test --filter=RestaurantTest

# Suite specifique (Unit ou Feature)
php artisan test --testsuite=Feature
php artisan test --testsuite=Unit

# Stopper au premier echec
php artisan test --stop-on-failure

# En parallele (plus rapide sur les grands projets)
php artisan test --parallel

# Couverture de code (necessite Xdebug ou PCOV)
php artisan test --coverage
php artisan test --coverage --min=80   # Echoue si < 80% de couverture
Un test utilise RefreshDatabase. Les données créées dans setUp() sont-elles disponibles dans les deux méthodes de test qui suivent ?
Pourquoi BCRYPT_ROUNDS=4 est configuré dans phpunit.xml au lieu de 12 ?
Partie 7 — Tests PHPUnit Feature Tests · HTTP actingAs · assertJson
⏱ 22 min

Tests HTTP & Feature

Tester les routes API de DariGo — getJson/postJson/putJson/deleteJson, assertions HTTP (assertStatus, assertJson, assertJsonStructure), authentification avec actingAs(), et tester toutes les règles de validation.

✓ Ce que vous allez apprendre
  • Méthodes HTTP : getJson(), postJson(), putJson(), patchJson(), deleteJson()
  • Assertions HTTP : assertStatus(), assertOk(), assertCreated(), assertUnauthorized()
  • Assertions JSON : assertJson(), assertJsonFragment(), assertJsonStructure(), assertJsonCount()
  • Authentification : actingAs($user, 'sanctum')
  • Tester la validation : s'assurer que les erreurs 422 sont bien retournées
  • Tester les autorisations : 403 quand le mauvais rôle accède

1
Méthodes HTTP et assertions de statut
getJson · postJson · assertStatus · assertOk · assertCreated
phpMéthodes HTTP — référence DariGo
// Les methodes *Json ajoutent automatiquement :
// Accept: application/json
// Content-Type: application/json

$response = $this->getJson('/api/restaurants');
$response = $this->postJson('/api/restaurants', ['nom' => 'Test']);
$response = $this->putJson('/api/restaurants/5', ['nom' => 'Modifie']);
$response = $this->patchJson('/api/commandes/3/statut', ['statut' => 'confirmee']);
$response = $this->deleteJson('/api/restaurants/5');

// Assertions de statut HTTP
$response->assertStatus(200);          // Code exact
$response->assertOk();                 // 200
$response->assertCreated();            // 201
$response->assertNoContent();          // 204
$response->assertBadRequest();         // 400
$response->assertUnauthorized();       // 401
$response->assertForbidden();          // 403
$response->assertNotFound();           // 404
$response->assertUnprocessable();      // 422 (validation)
$response->assertServerError();        // 500

2
Assertions JSON — vérifier le contenu des réponses
assertJson · assertJsonFragment · assertJsonStructure · assertJsonCount
phpAssertions JSON — DariGo
$response = $this->actingAs($this->client, 'sanctum')
    ->postJson('/api/commandes', [
        'restaurant_id' => $this->restaurant->id,
        'lignes' => [
            ['plat_id' => $this->plat->id, 'quantite' => 2],
        ],
    ]);

// assertJson() : verifie que la reponse CONTIENT exactement ces paires cle/valeur
$response->assertJson([
    'message' => 'Commande creee',
    'statut'  => 'en_attente',
]);

// assertJsonFragment() : verifie qu'un fragment existe QUELQUE PART dans le JSON
// (plus souple — fonctionne dans des tableaux imbriques)
$response->assertJsonFragment(['statut' => 'en_attente']);

// assertJsonStructure() : verifie la STRUCTURE (cles) sans verifier les valeurs
$response->assertJsonStructure([
    'message',
    'commandeId',
    'total',
    'statut',
]);

// Structure imbriquee (plats avec sous-cles)
$this->getJson('/api/restaurants/' . $this->restaurant->id)
    ->assertJsonStructure([
        'data' => [
            'id', 'nom', 'ville',
            'plats' => [
                '*' => ['id', 'nom', 'prix', 'disponible']
            ],
        ],
    ]);

// assertJsonCount() : compter les elements d'un tableau JSON
$this->getJson('/api/restaurants')
    ->assertJsonCount(5, 'data');  // 5 restaurants dans data[]

// assertJsonPath() : verifier une valeur a un chemin precis
$response->assertJsonPath('data.nom', 'Dar Zitoun');
$response->assertJsonPath('data.plats.0.prix', 65.00);

// assertJsonMissing() : verifier qu'une cle N'EXISTE PAS
$response->assertJsonMissing(['proprietaire_id']); // Pas expose dans l'API

3
actingAs() — tester avec différents rôles
actingAs() · Sanctum · without middleware · rôles DariGo
phptests/Feature/CommandeTest.php — tests complets par rôle
<?php

namespace Tests\Feature;

use App\Models\{Commande, Plat, Restaurant, User};
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class CommandeTest extends TestCase
{
    use RefreshDatabase;

    private User $client;
    private User $restaurateur;
    private User $livreur;
    private User $admin;
    private Restaurant $restaurant;
    private Plat $plat;

    protected function setUp(): void
    {
        parent::setUp();
        $this->client       = User::factory()->create(['role' => 'client']);
        $this->restaurateur = User::factory()->create(['role' => 'restaurateur']);
        $this->livreur      = User::factory()->create(['role' => 'livreur']);
        $this->admin        = User::factory()->create(['role' => 'admin']);
        $this->restaurant   = Restaurant::factory()->create([
            'actif'           => true,
            'proprietaire_id' => $this->restaurateur->id,
        ]);
        $this->plat = Plat::factory()->create([
            'restaurant_id' => $this->restaurant->id,
            'prix'          => 65.00,
            'stock'         => 10,
            'actif'         => true,
        ]);
    }

    // ── Tests CRUD ────────────────────────────────────────

    /** @test */
    public function un_client_peut_creer_une_commande(): void
    {
        $response = $this->actingAs($this->client, 'sanctum')
            ->postJson('/api/commandes', [
                'restaurant_id' => $this->restaurant->id,
                'lignes'        => [
                    ['plat_id' => $this->plat->id, 'quantite' => 2],
                ],
            ]);

        $response->assertCreated()
                 ->assertJsonStructure(['commandeId', 'total', 'statut'])
                 ->assertJsonPath('statut', 'en_attente');

        // Verifier que le total est correct (2 * 65 MAD)
        $this->assertEquals(130.00, $response->json('total'));
    }

    /** @test */
    public function un_utilisateur_non_connecte_ne_peut_pas_commander(): void
    {
        $this->postJson('/api/commandes', [
            'restaurant_id' => $this->restaurant->id,
            'lignes'        => [['plat_id' => $this->plat->id, 'quantite' => 1]],
        ])->assertUnauthorized();
    }

    /** @test */
    public function un_livreur_ne_peut_pas_creer_une_commande(): void
    {
        $this->actingAs($this->livreur, 'sanctum')
            ->postJson('/api/commandes', [
                'restaurant_id' => $this->restaurant->id,
                'lignes'        => [['plat_id' => $this->plat->id, 'quantite' => 1]],
            ])->assertForbidden();
    }

    /** @test */
    public function un_client_voit_uniquement_ses_propres_commandes(): void
    {
        // Creer commandes pour deux clients differents
        $autreClient = User::factory()->create(['role' => 'client']);
        Commande::factory(3)->create(['client_id' => $this->client->id]);
        Commande::factory(2)->create(['client_id' => $autreClient->id]);

        $response = $this->actingAs($this->client, 'sanctum')
            ->getJson('/api/commandes');

        $response->assertOk()
                 ->assertJsonCount(3, 'commandes');
    }

    /** @test */
    public function impossible_de_commander_un_plat_hors_stock(): void
    {
        $this->plat->update(['stock' => 0]);  // Vider le stock

        $this->actingAs($this->client, 'sanctum')
            ->postJson('/api/commandes', [
                'restaurant_id' => $this->restaurant->id,
                'lignes'        => [['plat_id' => $this->plat->id, 'quantite' => 1]],
            ])->assertStatus(409)
              ->assertJsonFragment(['erreur' => 'Stock insuffisant']);
    }

    // ── Tests de validation ───────────────────────────────

    /** @test */
    public function la_validation_echoue_sans_restaurant_id(): void
    {
        $this->actingAs($this->client, 'sanctum')
            ->postJson('/api/commandes', [
                // restaurant_id manquant
                'lignes' => [['plat_id' => $this->plat->id, 'quantite' => 1]],
            ])->assertUnprocessable()
              ->assertJsonValidationErrors(['restaurant_id']);
    }

    /** @test */
    public function la_validation_echoue_avec_quantite_invalide(): void
    {
        $this->actingAs($this->client, 'sanctum')
            ->postJson('/api/commandes', [
                'restaurant_id' => $this->restaurant->id,
                'lignes'        => [['plat_id' => $this->plat->id, 'quantite' => 0]],
            ])->assertUnprocessable()
              ->assertJsonValidationErrors(['lignes.0.quantite']);
    }
}
assertJson(['statut' => 'en_attente']) vs assertJsonFragment(['statut' => 'en_attente']) — quelle est la différence ?
Un test appelle $this->actingAs($livreur, 'sanctum')->postJson('/api/restaurants', [...]). La route est protégée par middleware('role:restaurateur,admin'). Quel code HTTP est attendu ?
Partie 7 — Tests PHPUnit BDD · Mocking Mail::fake · Queue::fake
⏱ 20 min

Tests BDD & Mocking

Vérifier les états de la BDD après une action (assertDatabaseHas, assertDatabaseMissing), intercepter les mails, jobs et événements sans les envoyer (Mail::fake(), Queue::fake(), Event::fake()) et utiliser les Factories dans les tests.

✓ Ce que vous allez apprendre
  • assertDatabaseHas() — vérifier qu'un enregistrement existe en BDD
  • assertDatabaseMissing() — vérifier qu'un enregistrement n'existe plus
  • assertDatabaseCount() — compter les enregistrements
  • Mail::fake() — intercepter les emails sans les envoyer
  • Queue::fake() — vérifier qu'un job a bien été mis en queue
  • Event::fake() — vérifier qu'un événement a bien été déclenché
  • Factories dans les tests — states, sequences, relations

1
Assertions BDD — vérifier l'état de la base de données
assertDatabaseHas · assertDatabaseMissing · assertDatabaseCount · assertSoftDeleted
phpAssertions BDD — tests DariGo
/** @test */
public function register_cree_un_utilisateur_en_base(): void
{
    $this->postJson('/api/auth/register', [
        'prenom'                => 'Karim',
        'nom'                   => 'Alaoui',
        'email'                 => 'karim@darigo.ma',
        'telephone'             => '0612345678',
        'password'              => 'Karim2024!',
        'password_confirmation' => 'Karim2024!',
        'ville'                 => 'Casablanca',
    ])->assertCreated();

    // Verifier que l'utilisateur est en BDD
    $this->assertDatabaseHas('users', [
        'email' => 'karim@darigo.ma',
        'prenom' => 'Karim',
        'role'  => 'client',
    ]);

    // Verifier que le mot de passe N'EST PAS stocke en clair
    $this->assertDatabaseMissing('users', [
        'password' => 'Karim2024!',
    ]);
}

/** @test */
public function supprimer_un_restaurant_le_soft_delete(): void
{
    $restaurant = Restaurant::factory()->create();

    $this->actingAs($this->admin, 'sanctum')
        ->deleteJson('/api/restaurants/' . $restaurant->id)
        ->assertOk();

    // Soft delete : toujours en BDD mais avec deleted_at
    $this->assertSoftDeleted('restaurants', ['id' => $restaurant->id]);

    // assertDatabaseHas ne retrouve PAS les soft-deleted par defaut
    $this->assertDatabaseMissing('restaurants', [
        'id'         => $restaurant->id,
        'deleted_at' => null,
    ]);
}

/** @test */
public function creer_commande_decremente_le_stock(): void
{
    $this->plat->update(['stock' => 5]);

    $this->actingAs($this->client, 'sanctum')
        ->postJson('/api/commandes', [
            'restaurant_id' => $this->restaurant->id,
            'lignes'        => [['plat_id' => $this->plat->id, 'quantite' => 2]],
        ])->assertCreated();

    // Verifier que le stock a diminue
    $this->assertDatabaseHas('plats', [
        'id'    => $this->plat->id,
        'stock' => 3,  // 5 - 2 = 3
    ]);
}

/** @test */
public function logout_revoque_le_token_sanctum(): void
{
    $token = $this->client->createToken('test')->plainTextToken;

    $this->withToken($token)
        ->postJson('/api/auth/logout')
        ->assertOk();

    // Verifier que le token est supprime de la BDD
    $this->assertDatabaseCount('personal_access_tokens', 0);
}

/** @test */
public function la_pagination_retourne_le_bon_nombre(): void
{
    Restaurant::factory(15)->create(['actif' => true]);

    $response = $this->actingAs($this->client, 'sanctum')
        ->getJson('/api/restaurants?per_page=10');

    $response->assertOk()
             ->assertJsonCount(10, 'data')
             ->assertJsonPath('meta.total', 15)
             ->assertJsonPath('meta.last_page', 2);
}

2
Mocking — Mail::fake(), Queue::fake(), Event::fake()
Intercepter sans envoyer · assertSent · assertPushed · assertDispatched
ℹ Pourquoi Fake ?

Les tests ne doivent pas envoyer de vrais emails ni de vrais SMS — coûteux, lents et imprévisibles. Mail::fake() intercepte les emails en mémoire. On peut ensuite vérifier qu'un email a bien été envoyé sans jamais toucher un serveur SMTP. Même principe pour Queue::fake() et Event::fake().

phpMail::fake() — tester les emails DariGo
use App\Mail\ConfirmationCommandeMail;
use Illuminate\Support\Facades\Mail;

/** @test */
public function creer_commande_envoie_un_email_de_confirmation(): void
{
    Mail::fake();  // Intercepter tous les emails

    $response = $this->actingAs($this->client, 'sanctum')
        ->postJson('/api/commandes', [
            'restaurant_id' => $this->restaurant->id,
            'lignes'        => [['plat_id' => $this->plat->id, 'quantite' => 1]],
        ])->assertCreated();

    // Verifier qu'un email a ete envoye
    Mail::assertSent(ConfirmationCommandeMail::class);

    // Verifier le destinataire et le contenu
    Mail::assertSent(ConfirmationCommandeMail::class, function ($mail) {
        return $mail->hasTo($this->client->email);
    });

    // Verifier qu'UN SEUL email a ete envoye
    Mail::assertSentCount(1);

    // Verifier qu'aucun autre email n'a ete envoye
    Mail::assertNotSent(\App\Mail\AlerteAdminMail::class);
}
phpQueue::fake() et Event::fake() — DariGo
use App\Events\CommandeCreee;
use App\Jobs\TraiterPaiementLivraison;
use Illuminate\Support\Facades\{Event, Queue};

/** @test */
public function creer_commande_dispatche_le_bon_job(): void
{
    Queue::fake();  // Intercepter les jobs — ne pas les executer

    $this->actingAs($this->client, 'sanctum')
        ->postJson('/api/commandes', [
            'restaurant_id' => $this->restaurant->id,
            'lignes'        => [['plat_id' => $this->plat->id, 'quantite' => 1]],
        ])->assertCreated();

    // Verifier que le job a bien ete mis en queue
    Queue::assertPushed(TraiterPaiementLivraison::class);

    // Verifier le contenu du job
    Queue::assertPushed(TraiterPaiementLivraison::class, function ($job) {
        return $job->commande->client_id === $this->client->id;
    });

    // Verifier la file d'attente specifique
    Queue::assertPushedOn('paiements', TraiterPaiementLivraison::class);
}

/** @test */
public function creer_commande_declenche_levenement_commande_creee(): void
{
    Event::fake();  // Intercepter les evenements

    $this->actingAs($this->client, 'sanctum')
        ->postJson('/api/commandes', [
            'restaurant_id' => $this->restaurant->id,
            'lignes'        => [['plat_id' => $this->plat->id, 'quantite' => 1]],
        ])->assertCreated();

    // Verifier que l'evenement a ete dispatche
    Event::assertDispatched(CommandeCreee::class);

    // Verifier les donnees de l'evenement
    Event::assertDispatched(CommandeCreee::class, function ($event) {
        return $event->commande->client_id === $this->client->id;
    });

    // Verifier qu'aucun autre evenement n'a ete declenche
    Event::assertNotDispatched(\App\Events\CommandeAnnulee::class);
}

/** @test */
public function notification_est_envoyee_quand_statut_livraison_change(): void
{
    \Notification::fake();

    $commande = Commande::factory()->create(['client_id' => $this->client->id]);

    $this->actingAs($this->admin, 'sanctum')
        ->patchJson('/api/commandes/' . $commande->id . '/statut', ['statut' => 'livree'])
        ->assertOk();

    \Notification::assertSentTo(
        $this->client,
        \App\Notifications\CommandeLivreeNotification::class
    );
}

3
Factories dans les tests — states et relations
States · sequences · for() · has() · afterCreating()
phpFactories avancées dans les tests DariGo
// States — variantes semantiques
$admin       = User::factory()->admin()->create();
$restaurateur = User::factory()->restaurateur()->create();
$livreur     = User::factory()->livreur()->create();

// Plusieurs utilisateurs avec des roles differents en sequence
$users = User::factory()->count(3)->sequence(
    ['role' => 'client'],
    ['role' => 'restaurateur'],
    ['role' => 'livreur'],
)->create();

// Relations — creer un restaurant avec 5 plats
$restaurant = Restaurant::factory()
    ->has(Plat::factory()->count(5), 'plats')
    ->create();

// has() avec state
$restaurant = Restaurant::factory()
    ->has(Plat::factory()->count(3)->state(['actif' => false, 'stock' => 0]), 'plats')
    ->create();

// for() — creer un plat pour un restaurant existant
$plat = Plat::factory()
    ->for($this->restaurant)
    ->create(['prix' => 45.00, 'stock' => 20]);

// Creer 10 commandes avec leurs lignes
Commande::factory()
    ->count(10)
    ->for($this->client, 'client')
    ->for($this->restaurant, 'restaurant')
    ->create()
    ->each(function ($commande) {
        // Attacher des plats a chaque commande
        $commande->plats()->attach(
            $this->plat->id,
            ['quantite' => rand(1, 3), 'prix_unitaire' => $this->plat->prix]
        );
    });
Queue::fake() est appelé dans un test. Un job est dispatched. Est-ce que le job s'exécute réellement ?
La méthode assertDatabaseHas('users', ['password' => 'Karim2024!']) échoue après un register. Pourquoi ?
Partie 8 — CMS WordPress CMS · Panorama WordPress · Joomla · PrestaShop
⏱ 14 min

CMS — Définition & Panorama

Qu'est-ce qu'un CMS, comment il fonctionne (front office / back office), comparaison des CMS PHP majeurs (WordPress, Joomla, Drupal, PrestaShop) et quand choisir un CMS plutôt que Laravel.

✓ Ce que vous allez apprendre
  • Définition d'un CMS — séparation contenu / présentation
  • Architecture CMS — front office, back office, WYSIWYG
  • Comparatif WordPress / Joomla / Drupal / PrestaShop
  • CMS vs Framework Laravel — critères de choix
  • Part de marché et écosystème WordPress au Maroc

1
Définition et fonctionnement d'un CMS
CMS · Front Office · Back Office · WYSIWYG · Séparation contenu/forme
ℹ CMS — Content Management System

Un CMS est un logiciel qui permet de créer et de mettre à jour un site web sans écrire de code. La grande innovation des CMS est la séparation entre le contenu (textes, images, articles) et la forme (mise en page, couleurs, typographie). Le rédacteur se concentre sur le contenu, le designer sur l'apparence — sans que l'un bloque l'autre.

ComposantRôleAccesExemple WordPress
Front OfficeCe que voient les visiteurs — le site publicTout le mondehttps://monsite.ma/
Back OfficeInterface d'administration — gérer les contenusAdministrateurs et éditeurshttps://monsite.ma/wp-admin
WYSIWYGÉditeur visuel — What You See Is What You GetRédacteursGutenberg (WordPress)
Base de donnéesStockage des contenus, utilisateurs, paramètresSystèmeMySQL — tables wp_posts, wp_users...

Comment fonctionne un CMS ?

  1. L'administrateur se connecte au back office via le navigateur (pas d'application à installer)
  2. Il rédige un article via l'éditeur WYSIWYG — sans écrire de HTML
  3. Le CMS stocke le contenu dans MySQL
  4. Un visiteur demande la page — le CMS assemble le contenu et le thème graphique
  5. Le HTML final est envoyé au navigateur

2
Comparatif des CMS PHP majeurs
WordPress · Joomla · Drupal · PrestaShop · Part de marché
CMSLangagePart de marchéPoints fortsCas d'usage idéalAu Maroc
WordPressPHP43% du Web mondialCommunauté énorme, 60 000+ plugins, simpleBlog, site vitrine, magazine, e-commerce simpleTrès utilisé — agences, startups, médias
JoomlaPHP~2%Plus flexible que WordPress, ACL avancéPortails, intranets, sites communautairesEn déclin
DrupalPHP~1.3%Très robuste, sécurité entrepriseGouvernements, grandes organisationsSites gouvernementaux marocains
PrestaShopPHPLeader e-commerce francophoneE-commerce complet, multi-devises, multi-languesBoutiques en ligneTrès populaire — commerce marocain en ligne
ShopifyRuby/SaaS—Hébergé, zéro maintenanceE-commerce rapideCroissance rapide
ℹ Pourquoi WordPress domine

WordPress est open source, gratuit et dispose d'une communauté mondiale — plus de 60 000 plugins et 10 000 thèmes. Sa prise en main est rapide : un site professionnel peut être en ligne en quelques heures. Au Maroc, la majorité des agences web livrent leurs clients sur WordPress — sites vitrines pour PME casablancaises, blogs, magazines en ligne (Medias24, Hespress ont utilisé WordPress). Ses mises à jour automatiques intègrent des corrections de sécurité régulières.


3
CMS vs Framework Laravel — quand choisir quoi
Critères de choix · Time to market · Personnalisation · Maintenabilité
Élevées — PHP, BDD, API
CritèreCMS (WordPress)Framework (Laravel)
Time to marketTrès rapide — quelques heuresPlus long — tout à construire
PersonnalisationLimitée aux thèmes et pluginsTotale — contrôle absolu
Compétences requisesFaibles — interface graphique
Coût de maintenanceFaible — mises à jour autoÉlevé — développeur requis
PerformanceVariable selon les pluginsOptimisable précisément
SécuritéSurface d'attaque large (plugins tiers)Maîtrisée par le développeur
API RESTPossible (WP REST API) mais limitéNatif et optimal
Logique métier complexeDifficile — contournementsIdéal — code structuré
✓ Règle de décision simple

Choisir WordPress : site vitrine d'entreprise, blog, magazine, portfolio, e-commerce standard — contenu géré par un non-développeur, budget réduit, délai court. Choisir Laravel : plateforme DariGo, SaaS, application métier sur mesure, API consommée par mobile, logique complexe (commandes, paiements, rôles).

Une PME casablancaise veut un site vitrine avec 5 pages, une galerie photos et un formulaire de contact. Elle n'a pas de développeur interne. Quel choix est le plus pertinent ?
WordPress représente 43% du Web mondial. Quelle est la principale raison de cette domination ?
Partie 8 — CMS WordPress Installation · Admin BDD · Utilisateurs · Rôles
⏱ 16 min

Installation & Administration WordPress

Installer WordPress en local (XAMPP), créer la base de données MySQL, configurer wp-config.php, découvrir le tableau de bord et les tables BDD (wp_posts, wp_users...), gérer les utilisateurs et leurs rôles.

✓ Ce que vous allez apprendre
  • Télécharger WordPress et le placer dans le répertoire XAMPP htdocs/
  • Créer une base de données MySQL pour WordPress
  • Configurer wp-config.php — connexion BDD, clés de sécurité
  • Structure des tables WordPress — wp_posts, wp_users, wp_options...
  • Tableau de bord — navigation, rôles des utilisateurs
  • Rôles WordPress — Administrateur, Éditeur, Auteur, Contributeur, Abonné

1
Installation de WordPress en local — étapes complètes
XAMPP · htdocs/ · BDD MySQL · wp-config.php · Assistant

Étapes d'installation

  1. Télécharger WordPress : wordpress.org/download — archive ZIP (~20 Mo)
  2. Démarrer XAMPP : Apache + MySQL doivent être actifs (voyant vert)
  3. Placer WordPress dans htdocs/ : décompresser le ZIP dans C:\xampp\htdocs\mon-site-ma\
  4. Créer la BDD MySQL : phpMyAdmin (localhost/phpmyadmin) → Nouvelle base → monsite_db → utf8mb4_unicode_ci
  5. Lancer l'assistant : http://localhost/mon-site-ma → WordPress détecte l'absence de config et lance le wizard
  6. Renseigner les informations BDD : nom de la base, utilisateur (root), mot de passe (vide par défaut XAMPP), hôte (localhost)
  7. Configurer le site : titre, identifiant admin (éviter "admin" par sécurité), mot de passe fort, email
  8. Se connecter au back office : http://localhost/mon-site-ma/wp-admin
⚠ Sécurité — ne pas utiliser "admin" comme identifiant

Le nom d'utilisateur "admin" est le premier essaié lors d'une attaque par force brute. Utiliser un identifiant unique (karim_darigo) et un mot de passe généré par WordPress (le bouton "Générer" crée un mot de passe fort). Activer l'authentification à deux facteurs via plugin en production.

phpwp-config.php — configuration générée automatiquement
// Connexion BDD (genere par l'assistant)
define('DB_NAME',     'monsite_db');
define('DB_USER',     'root');
define('DB_PASSWORD', '');
define('DB_HOST',     'localhost');
define('DB_CHARSET',  'utf8mb4');

// Cles de securite — regenerer sur https://api.wordpress.org/secret-key/1.1/salt/
define('AUTH_KEY',         'votre-cle-unique-1');
define('SECURE_AUTH_KEY',  'votre-cle-unique-2');
// ... (8 cles au total)

// Prefixe des tables — changer pour la securite (eviter wp_ par defaut)
$table_prefix = 'ms_';  // ms_ au lieu de wp_

// Mode debug (false en production)
define('WP_DEBUG', false);

2
Structure interne — tables BDD WordPress
wp_posts · wp_users · wp_options · wp_postmeta · wp_usermeta
ℹ Architecture BDD WordPress — 12 tables par défaut

WordPress stocke tout dans MySQL. Comprendre la structure des tables permet de faire des requêtes avancées, de migrer des données et de débugger. Toutes les tables sont préfixées par wp_ (ou le préfixe personnalisé dans wp-config.php).

TableContenuDonnées typiques
wp_postsArticles, pages, médias, menuspost_title, post_content, post_status, post_type
wp_postmetaMétadonnées des posts (champs personnalisés)meta_key, meta_value
wp_usersComptes utilisateursuser_login, user_email, user_pass (haché)
wp_usermetaMétadonnées utilisateurs (rôles, préférences)wp_capabilities (rôle), wp_user_level
wp_optionsParamètres du site et des pluginsblogname, siteurl, active_plugins
wp_termsCatégories et étiquettesterm_id, name, slug
wp_term_taxonomyType de terme (category, post_tag)taxonomy, count
wp_commentsCommentaires des articlescomment_author, comment_content, comment_approved

3
Tableau de bord & rôles utilisateurs
Administrateur · Éditeur · Auteur · Contributeur · Abonné

Navigation dans le tableau de bord

Le tableau de bord est accessible via /wp-admin. La colonne de gauche contient le menu de navigation principal : Articles, Médias, Pages, Commentaires, Apparence, Extensions, Utilisateurs, Réglages.

RôleCapacités principalesCas d'usage DariGo Blog
AdministrateurTout — installer plugins, thèmes, gérer utilisateurs, paramètresL'équipe technique DariGo
ÉditeurGérer et publier TOUS les articles (y compris ceux des autres)Chef de rédaction du blog DariGo
AuteurPublier et gérer SES propres articles uniquementRédacteurs freelance
ContributeurRédiger ses articles mais ne peut PAS les publier (validation requise)Contributeurs occasionnels
AbonnéLire uniquement, accès au profilClients DariGo avec espace membres
Dans wp-config.php, on recommande de changer $table_prefix = 'wp_' en un préfixe personnalisé comme 'ms_'. Pourquoi ?
Un freelance rédige des articles pour DariGo Blog mais un chef de rédaction doit valider avant publication. Quel rôle WordPress lui attribuer ?
Partie 8 — CMS WordPress Articles · Pages Catégories · Médias
⏱ 14 min

Contenus & Fonctionnalités WordPress

Gérer les articles et les pages (différences, statuts, hiérarchie), catégories et étiquettes, bibliothèque de médias, menus de navigation et widgets — le tout depuis le tableau de bord.

✓ Ce que vous allez apprendre
  • Articles vs Pages — différences fondamentales et cas d'usage
  • Statuts de publication : Brouillon, En attente, Publié, Privé, Planifié
  • Catégories (obligatoires) et Étiquettes/Tags (optionnels)
  • Bibliothèque de médias — images, PDF, vidéos
  • Menus de navigation — créer, ordonner, emboîter
  • Widgets et zones de widgets

1
Articles vs Pages — différence fondamentale
Article · Page · Statuts · Hiérarchie · Permalien
CaractéristiqueArticle (Post)Page
Type de contenuChronologique — actualités, blogStatique — contenu permanent
Catégories/TagsOui — requis pour organiserNon — pas de taxonomie
HiérarchieNon — pas de parent/enfantOui — page parent > page enfant
Affiché dans le fluxOui — page d'accueil du blogNon — accès par URL directe
Templatetemplate single.phptemplate page.php ou template custom
Exemples DariGo Blog"Top 10 restaurants Casablanca", "Recette tajine""À propos", "Contact", "Conditions d'utilisation"
StatutVisibilitéUsage
BrouillonAdmins seulementArticle en cours de rédaction
En attente de relectureAdmins/ÉditeursSoumis par un Contributeur
PubliéTout le mondeEn ligne — visible par les visiteurs
PrivéAdmins/Éditeurs connectésContenu interne non public
PlanifiéPublié automatiquement à une date futurePublication planifiée à 8h du matin
CorbeillePersonne — supprimé (30 jours avant effaçage)Soft delete WordPress

2
Catégories, Étiquettes et Médias
Taxonomie · Slug · Bibliothèque médias · Texte alternatif

Catégories vs Étiquettes (Tags)

Catégories : classification principale, hiérarchique, obligatoire. Un article doit appartenir à au moins une catégorie. Exemple DariGo Blog : "Gastronomie marocaine", "Recettes", "Bons plans restaurants Casablanca".

Étiquettes (Tags) : mots-clés libres, non hiérarchiques, optionnels. Elles décrivent le contenu en détail. Exemple : "tajine", "couscous", "restaurant halal", "livraison Casablanca". Les tags améliorent le SEO et facilitent la navigation.

Bibliothèque de médias

Médias > Bibliothèque — tous les fichiers uploadés (images, PDF, vidéos). Les images sont stockées dans wp-content/uploads/année/mois/. WordPress génère automatiquement plusieurs tailles (thumbnail, medium, large, full). Le texte alternatif (alt) est indispensable pour l'accessibilité et le SEO — toujours le renseigner.


3
Menus de navigation et Widgets
Apparence · Menus · Widgets · Zones de widgets

Créer et configurer un menu

  1. Apparence > Menus > Créer un nouveau menu (ex : "Menu principal")
  2. Ajouter des éléments : Pages, Articles, Catégories, Liens personnalisés
  3. Ordonner par glisser-déposer
  4. Emboîter : glisser un élément sous un autre pour créer un sous-menu
  5. Assigner à un emplacement : "Menu principal", "Menu pied de page"
  6. Enregistrer le menu

Widgets

Les widgets sont des blocs de contenu placés dans les zones définies par le thème (barre latérale, pied de page...). Widgets courants : Recherche, Articles récents, Catégories, Archives, Texte libre, Réseaux sociaux. Apparence > Widgets pour les configurer.

L'équipe DariGo veut publier un article chaque lundi à 9h du matin automatiquement, sans être connectée. Quel statut WordPress utiliser ?
Partie 8 — CMS WordPress Thèmes · Child Theme Elementor · Personnalisation
⏱ 16 min

Thèmes & Personnalisation

Choisir et activer un thème WordPress, créer un Child Theme pour personnaliser sans perdre ses modifications lors des mises à jour, utiliser functions.php, comprendre la hiérarchie des templates et utiliser Elementor comme page builder.

✓ Ce que vous allez apprendre
  • Thème WordPress — définition, structure de fichiers, installer et activer
  • Hiérarchie des templates — quel fichier PHP est appelé pour quelle URL
  • Child Theme — pourquoi, comment créer, style.css + functions.php
  • functions.php — hooks, actions, filtres WordPress
  • Elementor — page builder visuel par glisser-déposer
  • Personnaliser l'apparence via le Personnalisateur WordPress

1
Thèmes WordPress — structure et installation
Thème · Apparence · Astra · Responsive · wp-content/themes/
ℹ Qu'est-ce qu'un thème WordPress ?

Un thème est un ensemble de fichiers PHP, CSS et JavaScript qui définit l'apparence du site. Il sépare la présentation du contenu. Changer de thème change l'apparence sans toucher aux articles ni aux pages. Les thèmes sont stockés dans wp-content/themes/. WordPress propose des milliers de thèmes gratuits sur wordpress.org/themes et des thèmes premium sur Themeforest, Elegant Themes...

textStructure minimale d'un thème WordPress
wp-content/themes/mon-theme/
├── style.css          # Obligatoire — en-tête du thème (nom, version, auteur)
├── index.php          # Obligatoire — template par défaut
├── functions.php      # Fonctions, hooks, scripts, styles
├── header.php         # En-tête du site (menu, logo)
├── footer.php         # Pied de page
├── sidebar.php        # Barre latérale
├── single.php         # Template d'un article
├── page.php           # Template d'une page
├── archive.php        # Template des archives (catégorie, date)
└── screenshot.png     # Apercu dans l'admin WordPress

Thème Astra — le plus populaire

Astra est le thème WordPress gratuit le plus installé (> 1 million d'installations actives, note 4.9/5). Il est ultra-léger (< 50 Ko), compatible avec Elementor et WooCommerce, et dispose d'une option Responsive intégrée. Pour l'installer : Apparence > Thèmes > Ajouter > Rechercher "Astra" > Installer > Activer.


2
Child Theme — personnaliser sans perdre ses modifications
Child Theme · style.css · functions.php · Mise à jour sécurisée
⚠ Ne jamais modifier un thème parent directement

Si on modifie directement les fichiers du thème Astra et qu'une mise à jour est disponible, toutes les modifications sont écrasées. Solution : créer un Child Theme. Le Child Theme hérite du parent mais ses fichiers ne sont jamais touchés par les mises à jour.

textwp-content/themes/astra-child/style.css
/*
Theme Name:   Astra Child DariGo
Template:     astra
Description:  Child theme d'Astra pour DariGo Blog
Author:       Equipe DariGo
Version:      1.0
*/

/* Personnalisations CSS ici — non ecrasees par les mises a jour d'Astra */

:root {
    --darigo-rouge: #e63946;
    --darigo-vert:  #2a9d8f;
}

.site-header {
    background-color: var(--darigo-rouge);
}

.entry-title a {
    color: var(--darigo-vert);
}

.btn-commander {
    background: var(--darigo-rouge);
    color: white;
    padding: 10px 20px;
    border-radius: 4px;
}
phpwp-content/themes/astra-child/functions.php
<?php
// Charger les styles du theme parent (Astra) ET du child theme
add_action('wp_enqueue_scripts', function () {
    // Charger le CSS du parent
    wp_enqueue_style(
        'astra-parent-style',
        get_template_directory_uri() . '/style.css'
    );

    // Charger le CSS du child (avec le parent comme dependance)
    wp_enqueue_style(
        'astra-child-style',
        get_stylesheet_directory_uri() . '/style.css',
        ['astra-parent-style'],
        wp_get_theme()->get('Version')
    );
});

// Ajouter une fonctionnalite : lien "Commander sur DariGo" apres chaque article
add_filter('the_content', function ($contenu) {
    if (is_single()) {
        $lien = get_post_meta(get_the_ID(), 'darigo_restaurant_url', true);
        if ($lien) {
            $contenu .= '<a class="btn-commander" href="' . esc_url($lien) . '">
                Commander sur DariGo
            </a>';
        }
    }
    return $contenu;
});

3
Elementor — page builder visuel
Drag & Drop · Sections · Colonnes · Widgets · Responsive

Elementor est le page builder le plus populaire de WordPress (> 10 millions d'installations). Il permet de créer des mises en page complexes par glisser-déposer, sans écrire de CSS. Compatible avec tous les thèmes et gratuit en version de base.

Concepts Elementor

  • Section : conteneur horizontal principal — 1, 2 ou 3 colonnes
  • Colonne : division vertical d'une section
  • Widget : élément de contenu (Titre, Image, Bouton, Formulaire, Galerie, Map...)
  • Style global : couleurs et typographie du site entière
  • Responsive : Elementor gère l'affichage mobile/tablette/desktop séparément

Installer et activer Elementor

Extensions > Ajouter > Rechercher "Elementor" > Installer > Activer. Ensuite, éditer une page > "Modifier avec Elementor". L'éditeur plein écran s'ouvre avec le panneau de widgets à gauche et la prévisualisation à droite.

L'équipe DariGo modifie le fichier single.php du thème Astra directement. Une mise à jour d'Astra est installée. Que se passe-t-il ?
Partie 8 — CMS WordPress Plugins · Extensions Déploiement · Hébergement
⏱ 16 min

Plugins & Déploiement

Installer et utiliser les plugins essentiels (SEO, sécurité, performance, formulaires), créer un plugin simple avec les hooks WordPress, et déployer un site WordPress sur un hébergeur marocain ou OVH.

✓ Ce que vous allez apprendre
  • Installer, activer et configurer un plugin depuis le répertoire WordPress
  • Plugins essentiels — Yoast SEO, Wordfence, WP Rocket, Contact Form 7
  • Structure d'un plugin simple — header, add_action, add_shortcode
  • Hébergement — mutualisé vs VPS, coûts au Maroc
  • Déploiement — FTP, cPanel, migration local → production
  • SSL, sauvegardes automatiques, sécurité de base

1
Plugins essentiels pour DariGo Blog
Yoast SEO · Wordfence · WP Rocket · Contact Form 7 · WooCommerce
PluginCatégorieRôleGratuit ?
Yoast SEOSEOOptimiser les balises meta, sitemap XML, analyse readabilityGratuit (Pro payant)
Wordfence SecuritySécuritéPare-feu, scan malware, blocage IP, authentification 2FAGratuit (Premium payant)
WP RocketPerformanceCache de pages, minification CSS/JS, lazy loading imagesPayant (~49$/an)
W3 Total CachePerformanceAlternative gratuite à WP RocketGratuit
Contact Form 7FormulairesCréer des formulaires de contact configurableGratuit
UpdraftPlusSauvegardeSauvegardes automatiques vers Google Drive, DropboxGratuit (Premium payant)
WooCommerceE-commerceTransformer WordPress en boutique en ligneGratuit
SmushMédiasCompression automatique des images sans perte de qualitéGratuit
⚠ Trop de plugins = problèmes

Chaque plugin ajoute du code PHP exécuté à chaque page. 50 plugins actifs = site lent et vulnérable aux conflits. Règle : installer uniquement les plugins nécessaires, vérifier la dernière mise à jour (< 1 an), le nombre d'installations actives (> 10 000) et les évaluations (> 4/5).


2
Créer un plugin simple — structure et hooks
add_action · add_filter · add_shortcode · wp_enqueue_scripts
phpwp-content/plugins/darigo-widget/darigo-widget.php
<?php
/**
 * Plugin Name:       DariGo Widget
 * Plugin URI:        https://darigo.ma/plugin
 * Description:       Affiche un widget "Commander sur DariGo" sur le blog
 * Version:           1.0.0
 * Author:            Equipe DariGo
 * Author URI:        https://darigo.ma
 * License:           GPL v2 or later
 * Text Domain:       darigo-widget
 */

// Securite : empecher l'acces direct au fichier PHP
if (! defined('ABSPATH')) {
    exit;
}

// HOOK : ajouter un shortcode [darigo_bouton url="https://app.darigo.ma/restaurants/5"]
add_shortcode('darigo_bouton', function ($atts) {
    $atts = shortcode_atts([
        'url'   => 'https://app.darigo.ma',
        'texte' => 'Commander sur DariGo',
    ], $atts);

    return sprintf(
        '<a href="%s" class="darigo-btn" target="_blank">%s</a>',
        esc_url($atts['url']),
        esc_html($atts['texte'])
    );
});

// HOOK : charger le CSS du plugin
add_action('wp_enqueue_scripts', function () {
    wp_enqueue_style(
        'darigo-widget-style',
        plugin_dir_url(__FILE__) . 'css/darigo-widget.css',
        [],
        '1.0.0'
    );
});

// HOOK : ajouter automatiquement le bouton apres chaque article de categorie "Restaurant"
add_filter('the_content', function ($contenu) {
    if (! is_single()) return $contenu;

    if (has_category('restaurant')) {
        $url = get_post_meta(get_the_ID(), '_darigo_url', true);
        if ($url) {
            $bouton = do_shortcode('[darigo_bouton url="' . esc_url($url) . '"]');
            $contenu .= '<div class="darigo-cta">' . $bouton . '</div>';
        }
    }

    return $contenu;
});

// Utilisation dans un article Blade ou page WordPress :
// [darigo_bouton url="https://app.darigo.ma/restaurants/5" texte="Voir le menu"]

3
Déploiement — local vers production
Hébergement · FTP · cPanel · Migration · SSL

Choisir un hébergement

TypePrix/moisAvantagesCas d'usage
Mutualisé25–80 MADSimple, support inclus, cPanel, SSL gratuit (Let's Encrypt)Blog DariGo, site vitrine PME
VPS80–400 MADRessources dédiées, contrôle total, performancesTrafic élevé, personnalisation
Hébergement WP managé200–800 MADOptimisé WordPress, backups auto, CDNSites à fort trafic
ℹ Hébergeurs au Maroc — options

Hébergeurs marocains : connectme.ma, maroc-host.com, planethoster.com (bureaux à Casablanca). Hébergeurs internationaux avec serveurs Europe : OVHcloud (France Centre), Hostinger (à partir de 2,99€/mois), o2switch (offre illimitée à ~7€/mois). Le certificat SSL (HTTPS) est gratuit via Let's Encrypt — à activer immédiatement.

textChecklist déploiement WordPress — local vers production
Avant le deploiement :
□  Exporter la BDD locale (phpMyAdmin > Exporter > SQL)
□  Compresser les fichiers WordPress en ZIP
□  Creer la BDD sur l'hebergeur (cPanel > MySQL Databases)
□  Creer un utilisateur MySQL et lui donner tous les droits sur la BDD

Transfert des fichiers :
□  Uploader le ZIP via FileZilla (FTP) vers public_html/
□  Decompresser le ZIP sur le serveur (via cPanel File Manager)
□  Importer la BDD (cPanel > phpMyAdmin > Importer > .sql)

Configuration :
□  Modifier wp-config.php : DB_NAME, DB_USER, DB_PASSWORD, DB_HOST
□  Mettre a jour les URLs BDD : wp_options > siteurl et home
□  OU utiliser le plugin Better Search Replace pour le remplacement
□  Activer SSL (cPanel > Let's Encrypt) et forcer HTTPS

Verification :
□  Tester toutes les pages
□  Verifier les formulaires
□  Tester sur mobile
□  Activer le plugin de sauvegarde automatique (UpdraftPlus)
Lors d'une migration de WordPress de localhost vers production, pourquoi faut-il mettre à jour les URLs dans la table wp_options ?
La fonction esc_url() est utilisée dans le plugin DariGo avant d'afficher une URL fournie par un utilisateur. Pourquoi ?
Projet Final — DariGo Laravel Architecture · Setup
⏱ 12 min

DariGo — Architecture & Setup Laravel

DariGo est une API REST complète de livraison de repas à domicile au Maroc, construite avec Laravel 11, Sanctum, Eloquent ORM et PHPUnit. Ce projet mobilise l'ensemble des compétences du cours — routage, modèles, sécurité, queues, tests et déploiement.

ℹ Qu'est-ce que DariGo ?

DariGo est une plateforme de livraison de repas à domicile — dari (maison en darija) + Go (livraison rapide). Un client commande chez un restaurant de Casablanca, Rabat ou Marrakech — un livreur reçoit la mission et livre. L'admin supervise tout. Ce projet est la version Laravel du back-end — une API REST pure, sans vues Blade, consommée par une app mobile.


1
Schéma BDD et architecture globale
5 tables · 4 rôles · API REST · Sanctum · Queues
textArchitecture DariGo — vue globale
APP MOBILE / POSTMAN
        |
        | HTTP + Bearer Token (Sanctum)
        v
    LARAVEL 11 API (darigo-api/)
    ├─ routes/api.php
    ├─ app/Http/Controllers/
    ├─ app/Models/         (Eloquent ORM)
    ├─ app/Policies/       (Gates & Policies)
    ├─ app/Jobs/           (Queues)
    ├─ app/Notifications/  (Email livreur)
    └─ tests/Feature/      (PHPUnit)
        |
        v
    MySQL 8 (darigo_db)
    ├─ users
    ├─ restaurants
    ├─ plats
    ├─ commandes
    └─ livraisons
TableModèleDescriptionRelations clés
usersUserTous les utilisateurs — 4 rôleshasMany restaurants, commandes, livraisons
restaurantsRestaurantRestaurants avec plats, ville, catégoriebelongsTo user (propriétaire), hasMany plats, commandes
platsPlatPlats avec prix, stock, disponibilitébelongsTo restaurant, belongsToMany commandes
commandesCommandeCommandes avec statuts, total, lignesbelongsTo user (client), restaurant; belongsToMany plats
livraisonsLivraisonMissions livreurs avec statutsbelongsTo commande, user (livreur)
RôlePermissions principales
clientVoir restaurants/plats, créer commandes, voir ses commandes
restaurateurGérer ses restaurants et ses plats, voir les commandes de ses restaurants
livreurVoir les livraisons disponibles, accepter une mission, mettre à jour le statut
adminTout — bypass automatique de toutes les policies

2
Setup initial du projet
composer create-project · Sanctum · .env · migrate · key:generate
bashCréation et configuration du projet
# 1 — Creer le projet
composer create-project laravel/laravel darigo-api
cd darigo-api

# 2 — Installer Sanctum (inclus dans Laravel 11, publier la config)
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"

# 3 — Configurer .env
# DB_DATABASE=darigo_db
# DB_USERNAME=root
# DB_PASSWORD=

# 4 — Creer la BDD
mysql -u root -p -e "CREATE DATABASE darigo_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"

# 5 — Generer la cle
php artisan key:generate

# 6 — Lancer les migrations par defaut
php artisan migrate

# 7 — Verifier
php artisan route:list  # Routes par defaut
php artisan serve       # http://localhost:8000
bashGenerer tous les fichiers du projet en une commande
# Modeles + migrations + factories + seeders + controllers
php artisan make:model User --migration             # Deja present
php artisan make:model Restaurant -mcsf --api
php artisan make:model Plat -mcsf --api
php artisan make:model Commande -mcsf --api
php artisan make:model Livraison -mcsf --api

# Policies
php artisan make:policy RestaurantPolicy --model=Restaurant
php artisan make:policy CommandePolicy --model=Commande
php artisan make:policy LivraisonPolicy --model=Livraison

# Job
php artisan make:job NotifierLivreurDisponible

# Notification
php artisan make:notification NouvelleCommandeDisponible

# Tests
php artisan make:test AuthTest
php artisan make:test RestaurantTest
php artisan make:test CommandeTest
php artisan make:test LivraisonTest
Projet Final — DariGo Laravel Étape 1 · Modèles & Relations
⏱ 22 min

Étape 1 — Migrations, Modèles & Factories

Créer les 5 migrations DariGo, configurer les 5 modèles Eloquent avec toutes leurs relations (hasOne, hasMany, belongsTo, belongsToMany, hasManyThrough) et générer les Factories avec des données marocaines réalistes.


1
Migrations DariGo — toutes les tables
phpMigration users — ajouter les champs DariGo
Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string('prenom', 50);
    $table->string('nom', 50);
    $table->string('email')->unique();
    $table->string('telephone', 15)->nullable();
    $table->string('password');
    $table->enum('role', ['client','restaurateur','livreur','admin'])->default('client');
    $table->string('ville', 50)->nullable();
    $table->string('photo')->nullable();
    $table->timestamp('email_verified_at')->nullable();
    $table->rememberToken();
    $table->timestamps();
});
phpMigrations restaurants, plats, commandes, livraisons
// restaurants
Schema::create('restaurants', function (Blueprint $table) {
    $table->id();
    $table->string('nom', 100);
    $table->string('slug', 120)->unique();
    $table->text('description')->nullable();
    $table->string('ville', 50);
    $table->string('adresse');
    $table->string('telephone', 15)->nullable();
    $table->enum('categorie', ['tajine','couscous','pizza','burger','sushi','marocain']);
    $table->json('horaires')->nullable();
    $table->decimal('note', 3, 2)->default(0.00);
    $table->unsignedSmallInteger('nb_avis')->default(0);
    $table->boolean('actif')->default(true);
    $table->string('photo')->nullable();
    $table->foreignId('proprietaire_id')->constrained('users')->restrictOnDelete();
    $table->timestamps();
    $table->index(['ville', 'actif', 'categorie']);
});

// plats
Schema::create('plats', function (Blueprint $table) {
    $table->id();
    $table->string('nom', 100);
    $table->text('description')->nullable();
    $table->decimal('prix', 8, 2);
    $table->enum('categorie', ['entree','plat','dessert','boisson'])->default('plat');
    $table->unsignedSmallInteger('stock')->default(999);
    $table->boolean('actif')->default(true);
    $table->string('photo')->nullable();
    $table->foreignId('restaurant_id')->constrained()->cascadeOnDelete();
    $table->timestamps();
});

// commandes
Schema::create('commandes', function (Blueprint $table) {
    $table->id();
    $table->foreignId('client_id')->constrained('users')->restrictOnDelete();
    $table->foreignId('restaurant_id')->constrained()->restrictOnDelete();
    $table->decimal('total', 8, 2);
    $table->enum('statut', ['en_attente','confirmee','en_preparation','prete','en_livraison','livree','annulee'])
          ->default('en_attente');
    $table->string('adresse_livraison');
    $table->string('ville_livraison', 50);
    $table->text('notes')->nullable();
    $table->timestamps();
    $table->index(['client_id', 'statut']);
});

// Table pivot commande_plat
Schema::create('commande_plat', function (Blueprint $table) {
    $table->foreignId('commande_id')->constrained()->cascadeOnDelete();
    $table->foreignId('plat_id')->constrained()->restrictOnDelete();
    $table->unsignedTinyInteger('quantite')->default(1);
    $table->decimal('prix_unitaire', 8, 2);   // prix au moment de la commande
    $table->string('nom_plat', 100);           // snapshot du nom
    $table->primary(['commande_id', 'plat_id']);
});

// livraisons
Schema::create('livraisons', function (Blueprint $table) {
    $table->id();
    $table->foreignId('commande_id')->constrained()->cascadeOnDelete();
    $table->foreignId('livreur_id')->nullable()->constrained('users')->nullOnDelete();
    $table->enum('statut', ['disponible','acceptee','en_route','livree','annulee'])
          ->default('disponible');
    $table->timestamp('acceptee_le')->nullable();
    $table->timestamp('livree_le')->nullable();
    $table->timestamps();
});

2
Modèles Eloquent — toutes les relations
phpapp/Models/User.php
class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    protected $fillable = ['prenom','nom','email','telephone','password','role','ville','photo'];
    protected $hidden   = ['password','remember_token'];
    protected function casts(): array {
        return ['email_verified_at' => 'datetime', 'password' => 'hashed'];
    }

    public function restaurants(): HasMany   { return $this->hasMany(Restaurant::class, 'proprietaire_id'); }
    public function commandes(): HasMany     { return $this->hasMany(Commande::class, 'client_id'); }
    public function livraisons(): HasMany    { return $this->hasMany(Livraison::class, 'livreur_id'); }
    // hasManyThrough : livraisons du client via ses commandes
    public function livraisonsClient(): HasManyThrough {
        return $this->hasManyThrough(Livraison::class, Commande::class, 'client_id', 'commande_id');
    }
    public function isAdmin(): bool         { return $this->role === 'admin'; }
    public function isRestaurateur(): bool  { return $this->role === 'restaurateur'; }
}
phpapp/Models/Restaurant.php
class Restaurant extends Model
{
    use HasFactory, SoftDeletes;
    protected $fillable = ['nom','slug','description','ville','adresse','telephone','categorie','horaires','actif','photo','proprietaire_id'];
    protected function casts(): array {
        return ['horaires' => 'array', 'actif' => 'boolean', 'note' => 'float'];
    }

    public function proprietaire(): BelongsTo { return $this->belongsTo(User::class, 'proprietaire_id'); }
    public function plats(): HasMany          { return $this->hasMany(Plat::class); }
    public function commandes(): HasMany      { return $this->hasMany(Commande::class); }

    // Scopes
    public function scopeActif($q)                    { return $q->where('actif', true); }
    public function scopeDansVille($q, string $ville)  { return $q->where('ville', $ville); }
    public function scopeBienNote($q, float $seuil = 3.5) { return $q->where('note', '>=', $seuil); }

    // Accessor : est-ce que le restaurant est ouvert maintenant ?
    protected function estOuvert(): Attribute {
        return Attribute::make(get: function () {
            if (!$this->actif || !$this->horaires) return $this->actif;
            $heure = now()->format('H:i');
            $jour  = strtolower(now()->locale('fr')->isoFormat('dddd'));
            $plage = $this->horaires[$jour] ?? null;
            if (!$plage) return false;
            [$ouv, $ferm] = explode('-', $plage);
            return $heure >= $ouv && $heure <= $ferm;
        });
    }
}
phpapp/Models/Commande.php
class Commande extends Model
{
    use HasFactory;
    protected $fillable = ['client_id','restaurant_id','total','statut','adresse_livraison','ville_livraison','notes'];

    public function client(): BelongsTo     { return $this->belongsTo(User::class, 'client_id'); }
    public function restaurant(): BelongsTo { return $this->belongsTo(Restaurant::class); }
    public function livraison(): HasOne     { return $this->hasOne(Livraison::class); }

    public function plats(): BelongsToMany {
        return $this->belongsToMany(Plat::class, 'commande_plat')
            ->withPivot(['quantite', 'prix_unitaire', 'nom_plat']);
    }

    // Scope
    public function scopeEnCours($q) {
        return $q->whereIn('statut', ['en_attente','confirmee','en_preparation','prete','en_livraison']);
    }
}

3
Factories & DatabaseSeeder
phpdatabase/seeders/DatabaseSeeder.php
public function run(): void
{
    // Admin
    User::firstOrCreate(['email' => 'admin@darigo.ma'], [
        'prenom' => 'Admin', 'nom' => 'DariGo',
        'password' => Hash::make('Admin2024!'),
        'role' => 'admin', 'ville' => 'Casablanca',
        'email_verified_at' => now(),
    ]);

    if (app()->environment('local', 'testing')) {
        $restaurateurs = User::factory(5)->restaurateur()->create();
        $livreurs      = User::factory(10)->livreur()->create();
        $clients       = User::factory(20)->create();

        $restaurants = Restaurant::factory(15)
            ->recycle($restaurateurs)->create();

        $restaurants->each(fn($r) =>
            Plat::factory(rand(6, 12))->for($r)->create()
        );

        $clients->each(function ($client) use ($restaurants) {
            Commande::factory(rand(1, 4))
                ->for($client, 'client')
                ->for($restaurants->random(), 'restaurant')
                ->create();
        });
    }
}
bashExécuter les migrations et les seeders
php artisan migrate:fresh --seed
# Output attendu :
# INFO  Running migrations.
# INFO  Seeding database.
# Admin DariGo cree : admin@darigo.ma
# Utilisateurs, restaurants et plats generes
Projet Final — DariGo Laravel Étape 2 · Authentification Sanctum
⏱ 18 min

Étape 2 — Authentification Sanctum

AuthController complet — register, login, logout, me. Middleware de vérification du rôle. Routes API avec groupes Sanctum. Tests PHPUnit de toutes les routes auth.


1
AuthController & Middleware rôle
phpapp/Http/Controllers/AuthController.php
<?php
namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\{JsonResponse, Request};
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rules\Password;

class AuthController extends Controller
{
    public function register(Request $request): JsonResponse
    {
        $data = $request->validate([
            'prenom'    => ['required', 'string', 'max:50'],
            'nom'       => ['required', 'string', 'max:50'],
            'email'     => ['required', 'email:rfc,dns', 'unique:users,email'],
            'telephone' => ['required', 'regex:/^(0[5-7][0-9]{8})$/'],
            'password'  => ['required', 'confirmed', Password::min(8)->mixedCase()->numbers()],
            'role'      => ['sometimes', 'in:client,restaurateur,livreur'],
            'ville'     => ['required', 'string'],
        ]);

        $user  = User::create([...$data, 'role' => $data['role'] ?? 'client']);
        $token = $user->createToken('darigo-' . $user->role, $this->abilities($user->role));

        return response()->json([
            'message' => 'Compte cree',
            'user'    => $user,
            'token'   => $token->plainTextToken,
        ], 201);
    }

    public function login(Request $request): JsonResponse
    {
        $request->validate([
            'email'    => ['required', 'email'],
            'password' => ['required'],
        ]);

        if (! Auth::attempt($request->only('email', 'password'))) {
            return response()->json(['message' => 'Identifiants incorrects'], 401);
        }

        $user  = Auth::user();
        $user->tokens()->where('name', 'darigo-' . $user->role)->delete();
        $token = $user->createToken('darigo-' . $user->role, $this->abilities($user->role));

        return response()->json([
            'message'   => 'Connexion reussie',
            'user'      => ['id' => $user->id, 'prenom' => $user->prenom, 'role' => $user->role, 'ville' => $user->ville],
            'token'     => $token->plainTextToken,
            'abilities' => $this->abilities($user->role),
        ]);
    }

    public function logout(Request $request): JsonResponse
    {
        $request->user()->currentAccessToken()->delete();
        return response()->json(['message' => 'Deconnecte']);
    }

    public function me(Request $request): JsonResponse
    {
        return response()->json(['user' => $request->user()]);
    }

    private function abilities(string $role): array
    {
        return match ($role) {
            'admin'        => ['*'],
            'restaurateur' => ['restaurants:manage', 'plats:manage', 'commandes:view'],
            'livreur'      => ['livraisons:view', 'livraisons:update'],
            default        => ['restaurants:view', 'plats:view', 'commandes:create', 'commandes:view'],
        };
    }
}
phpapp/Http/Middleware/CheckRole.php
public function handle(Request $request, Closure $next, string ...$roles): Response
{
    if (! $request->user() || ! in_array($request->user()->role, $roles)) {
        return response()->json([
            'message'      => 'Acces refuse',
            'roles_requis' => $roles,
        ], 403);
    }
    return $next($request);
}
phproutes/api.php — structure complète DariGo
// Auth public
Route::prefix('auth')->group(function () {
    Route::post('/register', [AuthController::class, 'register']);
    Route::post('/login',    [AuthController::class, 'login']);
});

// Protege par Sanctum
Route::middleware('auth:sanctum')->group(function () {
    Route::post('/auth/logout', [AuthController::class, 'logout']);
    Route::get('/auth/me',      [AuthController::class, 'me']);

    // Restaurants — public en lecture, protege en ecriture
    Route::get('/restaurants',             [RestaurantController::class, 'index']);
    Route::get('/restaurants/{restaurant}', [RestaurantController::class, 'show']);
    Route::middleware('role:restaurateur,admin')->group(function () {
        Route::post('/restaurants',                   [RestaurantController::class, 'store']);
        Route::put('/restaurants/{restaurant}',       [RestaurantController::class, 'update']);
        Route::delete('/restaurants/{restaurant}',    [RestaurantController::class, 'destroy']);
        Route::apiResource('restaurants.plats',       PlatController::class);
    });

    // Commandes — client cree, admin/resto voir tout
    Route::apiResource('commandes', CommandeController::class);
    Route::patch('/commandes/{commande}/statut', [CommandeController::class, 'updateStatut']);

    // Livraisons — livreurs uniquement
    Route::middleware('role:livreur,admin')->group(function () {
        Route::get('/livraisons',                         [LivraisonController::class, 'index']);
        Route::patch('/livraisons/{livraison}/accepter',  [LivraisonController::class, 'accepter']);
        Route::patch('/livraisons/{livraison}/statut',    [LivraisonController::class, 'updateStatut']);
    });
});

2
Tests PHPUnit — AuthTest
phptests/Feature/AuthTest.php
class AuthTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function register_cree_un_compte_et_retourne_un_token(): void
    {
        $this->postJson('/api/auth/register', [
            'prenom' => 'Karim', 'nom' => 'Alaoui',
            'email' => 'karim@darigo.ma', 'telephone' => '0612345678',
            'password' => 'Karim2024!', 'password_confirmation' => 'Karim2024!',
            'ville' => 'Casablanca',
        ])->assertCreated()
          ->assertJsonStructure(['token', 'user']);

        $this->assertDatabaseHas('users', ['email' => 'karim@darigo.ma', 'role' => 'client']);
        $this->assertDatabaseMissing('users', ['password' => 'Karim2024!']);
    }

    /** @test */
    public function login_retourne_token_et_abilities(): void
    {
        $user = User::factory()->create(['role' => 'livreur']);

        $this->postJson('/api/auth/login', [
            'email' => $user->email, 'password' => 'password',
        ])->assertOk()
          ->assertJsonStructure(['token', 'abilities'])
          ->assertJsonPath('user.role', 'livreur');
    }

    /** @test */
    public function login_invalide_retourne_401(): void
    {
        User::factory()->create(['email' => 'test@darigo.ma']);

        $this->postJson('/api/auth/login', [
            'email' => 'test@darigo.ma', 'password' => 'mauvais',
        ])->assertUnauthorized();
    }

    /** @test */
    public function logout_revoque_le_token(): void
    {
        $user  = User::factory()->create();
        $token = $user->createToken('test')->plainTextToken;

        $this->withToken($token)->postJson('/api/auth/logout')->assertOk();
        $this->assertDatabaseCount('personal_access_tokens', 0);

        // Token revoque : plus d'acces
        $this->withToken($token)->getJson('/api/auth/me')->assertUnauthorized();
    }
}
Projet Final — DariGo Laravel Étape 3 · API Restaurants & Plats
⏱ 20 min

Étape 3 — API Restaurants & Plats

RestaurantController et PlatController complets — CRUD avec Policy, pagination, filtres ville/catégorie/note, API Resource pour le formatage JSON, et scope disponible sur les plats.


1
RestaurantController & RestaurantPolicy
phpapp/Http/Controllers/RestaurantController.php
class RestaurantController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth:sanctum')->except(['index', 'show']);
        $this->authorizeResource(Restaurant::class, 'restaurant');
    }

    public function index(): JsonResponse
    {
        $restaurants = Restaurant::actif()
            ->when(request('ville'),      fn($q, $v) => $q->dansVille($v))
            ->when(request('categorie'),  fn($q, $c) => $q->where('categorie', $c))
            ->when(request('note_min'),   fn($q, $n) => $q->bienNote((float)$n))
            ->with(['proprietaire:id,prenom,nom'])
            ->withCount('plats')
            ->orderBy('note', 'DESC')
            ->paginate(request('par_page', 12));

        return response()->json($restaurants);
    }

    public function store(StoreRestaurantRequest $request): JsonResponse
    {
        $restaurant = Restaurant::create([
            ...$request->validated(),
            'slug'            => \Str::slug($request->nom) . '-' . \Str::random(5),
            'proprietaire_id' => auth()->id(),
        ]);

        return response()->json(['message' => 'Restaurant cree', 'restaurant' => $restaurant], 201);
    }

    public function show(Restaurant $restaurant): JsonResponse
    {
        return response()->json([
            'restaurant' => $restaurant->load(['plats' => fn($q) => $q->where('actif', true)]),
        ]);
    }

    public function update(UpdateRestaurantRequest $request, Restaurant $restaurant): JsonResponse
    {
        $restaurant->update($request->validated());
        return response()->json(['message' => 'Restaurant mis a jour', 'restaurant' => $restaurant->fresh()]);
    }

    public function destroy(Restaurant $restaurant): JsonResponse
    {
        $restaurant->delete();
        return response()->json(['message' => 'Restaurant supprime']);
    }
}
phpapp/Policies/RestaurantPolicy.php — avec before() admin
class RestaurantPolicy
{
    public function before(User $user, string $ability): ?bool
    {
        return $user->role === 'admin' ? true : null;
    }

    public function viewAny(User $user): bool  { return true; }
    public function view(User $user, Restaurant $restaurant): bool { return true; }
    public function create(User $user): bool   { return $user->role === 'restaurateur'; }
    public function update(User $user, Restaurant $restaurant): bool {
        return $user->id === $restaurant->proprietaire_id;
    }
    public function delete(User $user, Restaurant $restaurant): bool {
        return $user->id === $restaurant->proprietaire_id;
    }
}

2
PlatController — CRUD avec scope disponible
phpapp/Http/Controllers/PlatController.php
class PlatController extends Controller
{
    // GET /api/restaurants/{restaurant}/plats
    public function index(Restaurant $restaurant): JsonResponse
    {
        $plats = $restaurant->plats()
            ->when(request('disponible') === 'true', fn($q) => $q->disponible())
            ->when(request('categorie'), fn($q, $c) => $q->where('categorie', $c))
            ->orderBy('categorie')->orderBy('prix')
            ->get()
            ->map(fn($plat) => [
                ...$plat->toArray(),
                'disponible'    => $plat->stock > 0 && $plat->actif,
                'prix_formate'  => number_format($plat->prix, 2) . ' MAD',
            ]);

        return response()->json(['restaurant_id' => $restaurant->id, 'plats' => $plats]);
    }

    // POST /api/restaurants/{restaurant}/plats
    public function store(Request $request, Restaurant $restaurant): JsonResponse
    {
        $this->authorize('update', $restaurant);  // Meme droit que modifier le restaurant

        $data = $request->validate([
            'nom'         => ['required', 'string', 'max:100'],
            'description' => ['nullable', 'string'],
            'prix'        => ['required', 'numeric', 'min:0.01', 'max:9999'],
            'categorie'   => ['required', 'in:entree,plat,dessert,boisson'],
            'stock'       => ['required', 'integer', 'min:0'],
            'photo'       => ['nullable', 'image', 'max:2048'],
        ]);

        if ($request->hasFile('photo')) {
            $data['photo'] = $request->file('photo')->store('plats', 'public');
        }

        $plat = $restaurant->plats()->create($data);
        return response()->json(['message' => 'Plat ajoute', 'plat' => $plat], 201);
    }

    // PUT /api/restaurants/{restaurant}/plats/{plat}
    public function update(Request $request, Restaurant $restaurant, Plat $plat): JsonResponse
    {
        $this->authorize('update', $restaurant);

        $data = $request->validate([
            'nom'         => ['sometimes', 'string', 'max:100'],
            'prix'        => ['sometimes', 'numeric', 'min:0.01'],
            'stock'       => ['sometimes', 'integer', 'min:0'],
            'actif'       => ['sometimes', 'boolean'],
        ]);

        $plat->update($data);
        return response()->json(['message' => 'Plat mis a jour', 'plat' => $plat->fresh()]);
    }

    public function destroy(Restaurant $restaurant, Plat $plat): JsonResponse
    {
        $this->authorize('delete', $restaurant);
        $plat->delete();
        return response()->json(['message' => 'Plat supprime']);
    }
}
Projet Final — DariGo Laravel Étape 4 · API Commandes
⏱ 22 min

Étape 4 — API Commandes

Créer une commande avec validation du stock, dénormalisation des prix, machine à états pour les statuts, transaction BDD pour garantir la cohérence, et déclenchement de l'événement CommandeCreee.


1
CommandeController — store() avec transaction
phpapp/Http/Controllers/CommandeController.php
class CommandeController extends Controller
{
    public function store(StoreCommandeRequest $request): JsonResponse
    {
        $commande = DB::transaction(function () use ($request) {

            $total  = 0;
            $lignes = [];

            foreach ($request->lignes as $ligne) {
                $plat = Plat::lockForUpdate()->findOrFail($ligne['plat_id']);

                if ($plat->restaurant_id !== $request->restaurant_id) {
                    throw new \Exception("Plat {$plat->id} n'appartient pas a ce restaurant");
                }
                if (! $plat->actif || $plat->stock < $ligne['quantite']) {
                    abort(409, "Plat \"{$plat->nom}\" non disponible ou stock insuffisant");
                }

                $plat->decrement('stock', $ligne['quantite']);

                $sousTotal = $plat->prix * $ligne['quantite'];
                $total += $sousTotal;

                $lignes[$plat->id] = [
                    'quantite'      => $ligne['quantite'],
                    'prix_unitaire' => $plat->prix,
                    'nom_plat'      => $plat->nom,  // Denormalisation
                ];
            }

            $commande = Commande::create([
                'client_id'        => auth()->id(),
                'restaurant_id'    => $request->restaurant_id,
                'total'            => round($total, 2),
                'adresse_livraison'=> $request->adresse_livraison,
                'ville_livraison'  => $request->ville_livraison,
                'notes'            => $request->notes,
            ]);

            $commande->plats()->attach($lignes);

            return $commande;
        });

        // Declencher l'evenement (hors transaction)
        event(new CommandeCreee($commande));

        return response()->json([
            'message'    => 'Commande creee',
            'commande'   => $commande->load('plats'),
        ], 201);
    }

    public function index(): JsonResponse
    {
        $query = Commande::with(['restaurant:id,nom,ville', 'plats']);

        // Un client voit uniquement ses commandes
        if (auth()->user()->role === 'client') {
            $query->where('client_id', auth()->id());
        }
        // Un restaurateur voit les commandes de ses restaurants
        if (auth()->user()->role === 'restaurateur') {
            $restoIds = auth()->user()->restaurants->pluck('id');
            $query->whereIn('restaurant_id', $restoIds);
        }

        $commandes = $query->when(request('statut'), fn($q, $s) => $q->where('statut', $s))
            ->latest()->paginate(15);

        return response()->json($commandes);
    }

    public function updateStatut(Request $request, Commande $commande): JsonResponse
    {
        $this->authorize('updateStatut', $commande);

        $transitions = [
            'en_attente'     => ['confirmee', 'annulee'],
            'confirmee'      => ['en_preparation', 'annulee'],
            'en_preparation' => ['prete'],
            'prete'          => ['en_livraison'],
            'en_livraison'   => ['livree'],
        ];

        $statutsAutorises = $transitions[$commande->statut] ?? [];

        if (! in_array($request->statut, $statutsAutorises)) {
            return response()->json([
                'message'           => 'Transition de statut invalide',
                'statut_actuel'     => $commande->statut,
                'transitions_valides' => $statutsAutorises,
            ], 422);
        }

        $commande->update(['statut' => $request->statut]);
        return response()->json(['message' => 'Statut mis a jour', 'statut' => $request->statut]);
    }
}

2
Form Request StoreCommandeRequest
phpapp/Http/Requests/StoreCommandeRequest.php
class StoreCommandeRequest extends FormRequest
{
    public function authorize(): bool
    {
        return in_array(auth()->user()?->role, ['client', 'admin']);
    }

    public function rules(): array
    {
        return [
            'restaurant_id'    => ['required', 'exists:restaurants,id'],
            'adresse_livraison'=> ['required', 'string', 'max:255'],
            'ville_livraison'  => ['required', 'string'],
            'notes'            => ['nullable', 'string', 'max:500'],
            'lignes'           => ['required', 'array', 'min:1'],
            'lignes.*.plat_id' => ['required', 'exists:plats,id'],
            'lignes.*.quantite'=> ['required', 'integer', 'min:1', 'max:50'],
        ];
    }
}
Projet Final — DariGo Laravel Étape 5 · Livraisons & Queues
⏱ 20 min

Étape 5 — API Livraisons & Queues

LivraisonController — lister les missions disponibles, accepter une mission (updateOne atomique), mettre à jour le statut. Evénement CommandeCreee qui crée automatiquement une livraison et dispatche un Job pour notifier les livreurs disponibles.


1
Événement CommandeCreee — Listener & Job
phpapp/Events/CommandeCreee.php & Listener
// app/Events/CommandeCreee.php
class CommandeCreee
{
    use Dispatchable, SerializesModels;
    public function __construct(public readonly Commande $commande) {}
}

// app/Listeners/CreerLivraison.php
class CreerLivraison implements ShouldQueue
{
    public function handle(CommandeCreee $event): void
    {
        $livraison = Livraison::create([
            'commande_id' => $event->commande->id,
            'statut'      => 'disponible',
        ]);

        // Dispatcher le job de notification aux livreurs
        NotifierLivreurDisponible::dispatch($livraison)
            ->onQueue('notifications')
            ->delay(now()->addSeconds(10));
    }
}

// Enregistrement dans EventServiceProvider
protected $listen = [
    CommandeCreee::class => [CreerLivraison::class],
];
phpapp/Jobs/NotifierLivreurDisponible.php
class NotifierLivreurDisponible implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries   = 3;
    public int $timeout = 30;

    public function __construct(private readonly Livraison $livraison) {}

    public function handle(): void
    {
        $commande = $this->livraison->commande()->with('restaurant')->first();

        // Notifier les livreurs de la meme ville que le restaurant
        $livreurs = User::where('role', 'livreur')
            ->where('ville', $commande->restaurant->ville)
            ->get();

        Notification::send($livreurs, new NouvelleCommandeDisponible($this->livraison));
    }

    public function failed(\Throwable $e): void
    {
        \Log::error("Notification livreur echouee : " . $e->getMessage(), [
            'livraison_id' => $this->livraison->id,
        ]);
    }
}

2
LivraisonController — accepter et suivre
phpapp/Http/Controllers/LivraisonController.php
class LivraisonController extends Controller
{
    // GET /api/livraisons — missions disponibles pour le livreur
    public function index(): JsonResponse
    {
        $livraisons = Livraison::with(['commande.restaurant', 'commande.client:id,prenom'])
            ->when(auth()->user()->role === 'livreur', function ($q) {
                $q->where(function ($q) {
                    $q->where('statut', 'disponible')
                      ->orWhere('livreur_id', auth()->id());
                });
            })
            ->latest()->paginate(20);

        return response()->json($livraisons);
    }

    // PATCH /api/livraisons/{livraison}/accepter
    public function accepter(Livraison $livraison): JsonResponse
    {
        // Mise a jour atomique — eviter les race conditions
        $updated = Livraison::where('id', $livraison->id)
            ->where('statut', 'disponible')
            ->update([
                'livreur_id'  => auth()->id(),
                'statut'      => 'acceptee',
                'acceptee_le' => now(),
            ]);

        if (! $updated) {
            return response()->json([
                'message' => 'Mission deja acceptee ou non disponible',
            ], 409);
        }

        return response()->json([
            'message'   => 'Mission acceptee',
            'livraison' => $livraison->fresh(),
        ]);
    }

    // PATCH /api/livraisons/{livraison}/statut
    public function updateStatut(Request $request, Livraison $livraison): JsonResponse
    {
        $this->authorize('updateStatut', $livraison);

        $request->validate([
            'statut' => ['required', 'in:en_route,livree,annulee'],
        ]);

        $data = ['statut' => $request->statut];
        if ($request->statut === 'livree') {
            $data['livree_le'] = now();
            // Mettre aussi la commande a jour
            $livraison->commande->update(['statut' => 'livree']);
        }

        $livraison->update($data);
        return response()->json(['message' => 'Statut mis a jour', 'statut' => $request->statut]);
    }
}
Projet Final — DariGo Laravel Étape 6 · Tests PHPUnit
⏱ 20 min

Étape 6 — Tests PHPUnit complets

Suite de tests complète couvrant les routes Restaurants, Commandes et Livraisons — actingAs() par rôle, assertDatabaseHas, Queue::fake(), Event::fake(), couverture > 80%.


1
RestaurantTest & CommandeTest
phptests/Feature/RestaurantTest.php
class RestaurantTest extends TestCase
{
    use RefreshDatabase;

    private User $admin, $restaurateur, $client;
    private Restaurant $restaurant;

    protected function setUp(): void
    {
        parent::setUp();
        $this->admin        = User::factory()->admin()->create();
        $this->restaurateur = User::factory()->restaurateur()->create();
        $this->client       = User::factory()->create();
        $this->restaurant   = Restaurant::factory()->create([
            'actif' => true, 'proprietaire_id' => $this->restaurateur->id,
        ]);
    }

    /** @test */
    public function liste_restaurants_accessible_sans_auth(): void
    {
        Restaurant::factory(4)->create(['actif' => true]);
        $this->getJson('/api/restaurants')->assertOk()->assertJsonCount(5, 'data');
    }

    /** @test */
    public function filtre_par_ville_fonctionne(): void
    {
        Restaurant::factory(3)->create(['ville' => 'Rabat', 'actif' => true]);
        $this->getJson('/api/restaurants?ville=Rabat')
             ->assertOk()
             ->assertJsonCount(3, 'data');
    }

    /** @test */
    public function restaurateur_peut_creer_son_restaurant(): void
    {
        $response = $this->actingAs($this->restaurateur, 'sanctum')
            ->postJson('/api/restaurants', [
                'nom'       => 'Dar Zitoun',
                'ville'     => 'Marrakech',
                'adresse'   => '14 Rue Hassan II',
                'categorie' => 'marocain',
            ]);

        $response->assertCreated();
        $this->assertDatabaseHas('restaurants', ['nom' => 'Dar Zitoun', 'actif' => true]);
    }

    /** @test */
    public function client_ne_peut_pas_creer_restaurant(): void
    {
        $this->actingAs($this->client, 'sanctum')
            ->postJson('/api/restaurants', ['nom' => 'Test'])
            ->assertForbidden();
    }

    /** @test */
    public function restaurateur_ne_peut_pas_modifier_restaurant_dautrui(): void
    {
        $autreResto = Restaurant::factory()->create();

        $this->actingAs($this->restaurateur, 'sanctum')
            ->putJson('/api/restaurants/' . $autreResto->id, ['nom' => 'Pirate'])
            ->assertForbidden();
    }

    /** @test */
    public function admin_peut_supprimer_nimporte_quel_restaurant(): void
    {
        $this->actingAs($this->admin, 'sanctum')
            ->deleteJson('/api/restaurants/' . $this->restaurant->id)
            ->assertOk();

        $this->assertSoftDeleted('restaurants', ['id' => $this->restaurant->id]);
    }
}
phptests/Feature/CommandeTest.php — avec Event::fake et Queue::fake
class CommandeTest extends TestCase
{
    use RefreshDatabase;

    private User $client, $restaurateur;
    private Restaurant $restaurant;
    private Plat $plat;

    protected function setUp(): void
    {
        parent::setUp();
        $this->client       = User::factory()->create(['role' => 'client']);
        $this->restaurateur = User::factory()->restaurateur()->create();
        $this->restaurant   = Restaurant::factory()->create([
            'actif' => true, 'proprietaire_id' => $this->restaurateur->id,
        ]);
        $this->plat = Plat::factory()->create([
            'restaurant_id' => $this->restaurant->id,
            'prix' => 65.00, 'stock' => 10, 'actif' => true,
        ]);
    }

    /** @test */
    public function client_peut_creer_commande_et_evenement_est_declenche(): void
    {
        Event::fake();

        $response = $this->actingAs($this->client, 'sanctum')
            ->postJson('/api/commandes', [
                'restaurant_id'     => $this->restaurant->id,
                'adresse_livraison' => '14 Rue Hassan II',
                'ville_livraison'   => 'Casablanca',
                'lignes'            => [['plat_id' => $this->plat->id, 'quantite' => 2]],
            ]);

        $response->assertCreated()->assertJsonPath('commande.total', 130.0);

        $this->assertDatabaseHas('commandes', ['client_id' => $this->client->id, 'total' => 130.00]);
        $this->assertDatabaseHas('plats', ['id' => $this->plat->id, 'stock' => 8]);

        Event::assertDispatched(CommandeCreee::class);
    }

    /** @test */
    public function commande_refuse_si_stock_insuffisant(): void
    {
        $this->plat->update(['stock' => 1]);

        $this->actingAs($this->client, 'sanctum')
            ->postJson('/api/commandes', [
                'restaurant_id' => $this->restaurant->id,
                'adresse_livraison' => '14 Rue Moulay Ali',
                'ville_livraison'   => 'Casablanca',
                'lignes' => [['plat_id' => $this->plat->id, 'quantite' => 5]],
            ])->assertStatus(409);
    }

    /** @test */
    public function transition_statut_invalide_retourne_422(): void
    {
        $commande = Commande::factory()->create([
            'client_id' => $this->client->id,
            'restaurant_id' => $this->restaurant->id,
            'statut' => 'en_attente',
        ]);

        $this->actingAs($this->client, 'sanctum')
            ->patchJson('/api/commandes/' . $commande->id . '/statut', [
                'statut' => 'livree',  // Impossible depuis en_attente
            ])->assertUnprocessable();
    }

    /** @test */
    public function livraison_est_creee_quand_commande_est_creee(): void
    {
        $this->actingAs($this->client, 'sanctum')
            ->postJson('/api/commandes', [
                'restaurant_id'    => $this->restaurant->id,
                'adresse_livraison'=> '14 Rue Hassan',
                'ville_livraison'  => 'Casablanca',
                'lignes' => [['plat_id' => $this->plat->id, 'quantite' => 1]],
            ])->assertCreated();

        $this->assertDatabaseCount('livraisons', 1);
        $this->assertDatabaseHas('livraisons', ['statut' => 'disponible', 'livreur_id' => null]);
    }
}

2
Exécution et couverture
bashLancer tous les tests DariGo
# Tous les tests
php artisan test

# Avec couverture (necessite Xdebug)
php artisan test --coverage --min=80

# En parallele (plus rapide)
php artisan test --parallel

# Resultat attendu :
#   PASS  Tests\Feature\AuthTest        (4 tests)
#   PASS  Tests\Feature\RestaurantTest  (6 tests)
#   PASS  Tests\Feature\CommandeTest    (4 tests)
#   PASS  Tests\Feature\LivraisonTest   (3 tests)
#   Tests:  17 passed
#   Duration: 3.45s
Projet Final — DariGo Laravel Étape 7 · Déploiement
⏱ 16 min

Étape 7 — Déploiement production

Préparer l'API DariGo pour la production — .env production, php artisan optimize, config:cache, route:cache, Supervisor pour les queues, SSL et configuration Apache/Nginx.


1
Checklist de déploiement Laravel
bashSéquence de déploiement complète
# 1 — Cloner le projet sur le serveur
git clone https://github.com/equipe/darigo-api.git /var/www/darigo-api
cd /var/www/darigo-api

# 2 — Installer les dependances (sans dev)
composer install --no-dev --optimize-autoloader

# 3 — Configurer le .env production
cp .env.example .env
# Editer .env : APP_ENV=production, APP_DEBUG=false, DB_*, QUEUE_CONNECTION=redis...

# 4 — Generer la cle
php artisan key:generate

# 5 — Executer les migrations
php artisan migrate --force   # --force requis en production

# 6 — Creer le lien symbolique Storage
php artisan storage:link

# 7 — Optimiser (ESSENTIEL en production)
php artisan optimize           # cache config + routes + vues en une commande
# ou en detail :
php artisan config:cache       # Met en cache config/
php artisan route:cache        # Met en cache les routes (ATTENTION : plus de closures)
php artisan view:cache         # Pre-compile les vues Blade
php artisan event:cache        # Cache les listeners

# 8 — Permissions (Apache/Nginx doit pouvoir ecrire dans storage/)
chmod -R 775 storage bootstrap/cache
chown -R www-data:www-data storage bootstrap/cache
bash.env production DariGo — variables critiques
APP_NAME="DariGo API"
APP_ENV=production
APP_DEBUG=false       # JAMAIS true en production
APP_URL=https://api.darigo.ma

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=darigo_db
DB_USERNAME=darigo_user
DB_PASSWORD=MotDePasseForte2024!

CACHE_STORE=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis

REDIS_HOST=127.0.0.1
REDIS_PORT=6379

MAIL_MAILER=smtp
MAIL_HOST=smtp.sendgrid.net
MAIL_PORT=587
MAIL_USERNAME=apikey
MAIL_PASSWORD=SG.xxxxxxxxxxxx
MAIL_FROM_ADDRESS=noreply@darigo.ma

2
Supervisor — daemon pour les queues
Supervisor · queue:work · num_procs · autostart
ℹ Pourquoi Supervisor ?

php artisan queue:work est un processus qui s'arrête si le serveur redémarre. Supervisor est un gestionnaire de processus qui redémarre automatiquement les workers Laravel au redémarrage du serveur — garantissant que les notifications aux livreurs DariGo sont toujours traitées.

text/etc/supervisor/conf.d/darigo-worker.conf
[program:darigo-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/darigo-api/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2           ; 2 workers en parallele
redirect_stderr=true
stdout_logfile=/var/log/darigo-worker.log
stopwaitsecs=3600
bashCommandes Supervisor
sudo supervisorctl reread      # Lire les nouvelles configs
sudo supervisorctl update      # Appliquer les changements
sudo supervisorctl start darigo-worker:*
sudo supervisorctl status      # Verifier le statut

# Redemarrer les workers apres un deploiement
php artisan queue:restart      # Signe les workers de redemarrer proprement

3
Configuration Nginx & Bilan du cours
nginx/etc/nginx/sites-available/darigo-api
server {
    listen 80;
    server_name api.darigo.ma;
    return 301 https://$server_name$request_uri;  # Forcer HTTPS
}

server {
    listen 443 ssl http2;
    server_name api.darigo.ma;

    # Certificat SSL (Let's Encrypt via Certbot)
    ssl_certificate     /etc/letsencrypt/live/api.darigo.ma/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.darigo.ma/privkey.pem;

    root /var/www/darigo-api/public;   # Toujours pointer vers public/
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;  # Bloquer l'acces aux fichiers caches (.env, .git)
    }
}
✓ Bilan du projet — compétences mobilisées
ÉtapeCompétencePartie du cours
Architecture & SetupComposer, Artisan, .env, SanctumPartie 1
Migrations & ModèlesSchema Builder, Eloquent ORM, RelationsParties 5 & 6
Auth SanctumAPI Tokens, Abilities, Middleware rôlePartie 4
API Restaurants & PlatsControllers, Scopes, Policies, Form RequestParties 2, 4 & 6
API CommandesTransactions BDD, Events, Machine à étatsParties 3 & 5
Livraisons & QueuesJobs, Notifications, Listeners asynchronesPartie 3
Tests PHPUnitFeature tests, actingAs, fake(), assertDatabasePartie 7
Déploiementoptimize, Nginx, Supervisor, Redis, SSLToutes parties