Objectif
Ajouter des queues, events, notifications et cache Γ l'API du blog, puis la conteneuriser avec Docker (app + nginx + mysql + redis + queue worker).
| Concept | Mis en pratique |
|---|---|
| Job | SendWelcomeEmailJob dispatchΓ© Γ l'inscription |
| Event | ArticlePublished β listener NotifySubscribers |
| Cache | Liste articles en page d'accueil, invalidation Γ la publication |
| Schedule | Nettoyage articles brouillon de plus de 30 jours |
| Docker | docker-compose avec 5 services |
| DΓ©ploiement | Script deploy.sh + GitHub Actions CI |
Architecture Docker
ββββββββββββ ββββββββββββ ββββββββββββββββ
β nginx βββββΆβ app βββββΆβ mysql:8.0 β
β :8000 β β php-fpm β ββββββββββββββββ
ββββββββββββ β β ββββββββββββββββ
β βββββΆβ redis:alpineβ
ββββββββββββ ββββββββββββββββ
β
ββββββββββββ β
β queue βββββββββββββ
β worker β
ββββββββββββ
Consignes
- CrΓ©er
SendWelcomeEmailJob(queued) dispatchΓ© dansAuthController::register() - CrΓ©er l'event
ArticlePublished+ listenerNotifySubscribersListener(queued) - Dispatcher l'event quand
statuspasse Γ 'published' dans l'observer - ImplΓ©menter le cache de la liste d'articles (tag 'articles', TTL 3600)
- Invalider le cache au publish (dans le listener ou l'observer)
- Ajouter une commande Artisan
articles:cleanup-drafts --days=30 - Scheduler la commande
weeklydansroutes/console.php - 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: