Objectif

Ajouter des queues, events, notifications et cache Γ  l'API du blog, puis la conteneuriser avec Docker (app + nginx + mysql + redis + queue worker).

ConceptMis en pratique
JobSendWelcomeEmailJob dispatchΓ© Γ  l'inscription
EventArticlePublished β†’ listener NotifySubscribers
CacheListe articles en page d'accueil, invalidation Γ  la publication
ScheduleNettoyage articles brouillon de plus de 30 jours
Dockerdocker-compose avec 5 services
DΓ©ploiementScript deploy.sh + GitHub Actions CI

Architecture Docker

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  nginx   │───▢│   app    │───▢│  mysql:8.0   β”‚
β”‚ :8000    β”‚    β”‚ php-fpm  β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚          β”‚    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                β”‚          │───▢│  redis:alpineβ”‚
                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                       β”‚
                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”‚
                β”‚  queue   β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                β”‚  worker  β”‚
                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Consignes

  1. CrΓ©er SendWelcomeEmailJob (queued) dispatchΓ© dans AuthController::register()
  2. CrΓ©er l'event ArticlePublished + listener NotifySubscribersListener (queued)
  3. Dispatcher l'event quand status passe Γ  'published' dans l'observer
  4. ImplΓ©menter le cache de la liste d'articles (tag 'articles', TTL 3600)
  5. Invalider le cache au publish (dans le listener ou l'observer)
  6. Ajouter une commande Artisan articles:cleanup-drafts --days=30
  7. Scheduler la commande weekly dans routes/console.php
  8. CrΓ©er le Dockerfile + docker-compose.yml

Solution commentΓ©e

// app/Jobs/SendWelcomeEmailJob.php
namespace App\Jobs;

use App\Mail\WelcomeMail;
use App\Models\User;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;

class SendWelcomeEmailJob implements ShouldQueue
{
    use Queueable;

    public int $tries = 3;

    public function __construct(
        private readonly User $user
    ) {}

    public function handle(): void {
        \Mail::to($this->user->email)->send(new WelcomeMail($this->user));
    }

    public function failed(\Throwable $e): void {
        \Log::error("Welcome email failed for user {$this->user->id}: {$e->getMessage()}");
    }
}

// Dans AuthController::register()
// SendWelcomeEmailJob::dispatch($user)->onQueue('emails');
// app/Events/ArticlePublished.php
namespace App\Events;

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

class ArticlePublished
{
    use Dispatchable, SerializesModels;
    public function __construct(public readonly Article $article) {}
}

// app/Listeners/NotifySubscribersListener.php
namespace App\Listeners;

use App\Events\ArticlePublished;
use App\Models\User;
use App\Notifications\NewArticleNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Cache;

class NotifySubscribersListener implements ShouldQueue
{
    public string $queue = 'notifications';

    public function handle(ArticlePublished $event): void {
        // Invalider le cache
        Cache::tags(['articles'])->flush();

        // Notifier les abonnΓ©s (ex: tous les users avec role != guest)
        User::where('subscribed', true)->each(function (User $user) use ($event) {
            $user->notify(new NewArticleNotification($event->article));
        });
    }
}

// Dans ArticleObserver::updating()
// if ($article->isDirty('status') && $article->status === 'published') {
//     ArticlePublished::dispatch($article);
// }
// Cache dans ArticleController::index()
public function index(Request $request) {
    $cacheKey = 'articles.' . md5(json_encode($request->only(['status', 'page', 'per_page'])));

    $articles = Cache::tags(['articles'])->remember($cacheKey, 3600, function () use ($request) {
        return Article::with(['author', 'category', 'tags'])
            ->published()
            ->when($request->category_id, fn($q) => $q->where('category_id', $request->category_id))
            ->latest('published_at')
            ->paginate($request->per_page ?? 15);
    });

    return ArticleResource::collection($articles);
}

// routes/console.php (Laravel 11)
use Illuminate\Support\Facades\Schedule;

Schedule::command('articles:cleanup-drafts --days=30')->weekly();

// app/Console/Commands/CleanupDraftsCommand.php
namespace App\Console\Commands;

use App\Models\Article;
use Illuminate\Console\Command;

class CleanupDraftsCommand extends Command
{
    protected $signature = 'articles:cleanup-drafts {--days=30 : Γ‚ge minimum en jours}';
    protected $description = 'Supprime les articles brouillon inactifs';

    public function handle(): int {
        $days    = (int) $this->option('days');
        $deleted = Article::where('status', 'draft')
            ->where('updated_at', '<', now()->subDays($days))
            ->delete();

        $this->info("{$deleted} articles brouillon supprimΓ©s (> {$days} jours).");
        return self::SUCCESS;
    }
}
# docker-compose.yml
services:
  app:
    build: .
    volumes: [.:/var/www/app]
    environment:
      DB_HOST: mysql
      REDIS_HOST: redis
      QUEUE_CONNECTION: redis
      CACHE_DRIVER: redis
    depends_on: [mysql, redis]

  nginx:
    image: nginx:alpine
    ports: ['8000:80']
    volumes:
      - .:/var/www/app
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on: [app]

  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: laravel
    volumes: [mysql_data:/var/lib/mysql]

  redis:
    image: redis:alpine

  queue:
    build: .
    command: php artisan queue:work redis --queue=emails,notifications,default --tries=3
    restart: unless-stopped
    depends_on: [app, redis]

volumes:
  mysql_data:
← Cours Module 08 🧠 QCM Projet Final β†’