Queues & Jobs

php artisan make:job ProcessVideoJob
php artisan make:job SendWelcomeEmail --sync  # synchrone pour le dev

# Lancer le worker
php artisan queue:work
php artisan queue:work redis --queue=emails,default  # priorité
php artisan queue:work --tries=3 --timeout=90
// app/Jobs/ProcessVideoJob.php
namespace App\Jobs;

use App\Models\Video;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class ProcessVideoJob implements ShouldQueue
{
    use Queueable, InteractsWithQueue, SerializesModels;

    // Nombre de tentatives
    public int $tries = 3;

    // Timeout en secondes
    public int $timeout = 120;

    // Délai entre les tentatives (backoff)
    public array $backoff = [60, 300, 600]; // 1min, 5min, 10min

    public function __construct(
        private readonly Video $video
    ) {}

    public function handle(): void {
        // Traitement lourd (transcodage, resize, etc.)
        VideoProcessor::transcode($this->video->path);
        $this->video->update(['status' => 'processed']);
    }

    // Appelé quand toutes les tentatives ont échoué
    public function failed(\Throwable $exception): void {
        $this->video->update(['status' => 'failed']);
        \Log::error("Video processing failed: {$exception->getMessage()}");
    }
}

// Dispatcher le job
ProcessVideoJob::dispatch($video);
ProcessVideoJob::dispatch($video)->onQueue('videos');
ProcessVideoJob::dispatch($video)->delay(now()->addMinutes(5));
ProcessVideoJob::dispatch($video)->onConnection('redis');

// Job chain (s'exécutent séquentiellement)
Bus::chain([
    new ProcessVideoJob($video),
    new GenerateThumbnailJob($video),
    new NotifyUploaderJob($video),
])->dispatch();

// Job batch (en parallèle, avec callback)
Bus::batch([
    new ProcessVideoJob($video1),
    new ProcessVideoJob($video2),
    new ProcessVideoJob($video3),
])->then(function (Batch $batch) {
    // Tous réussis
})->catch(function (Batch $batch, \Throwable $e) {
    // Au moins un a échoué
})->finally(function (Batch $batch) {
    // Toujours exécuté
})->dispatch();

Events & Listeners

php artisan make:event ArticlePublished
php artisan make:listener SendPublishedNotification --event=ArticlePublished
php artisan make:listener IndexArticleForSearch --event=ArticlePublished
// app/Events/ArticlePublished.php
namespace App\Events;

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

class ArticlePublished
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(
        public readonly Article $article
    ) {}
}

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

use App\Events\ArticlePublished;
use App\Notifications\ArticlePublishedNotification;
use Illuminate\Contracts\Queue\ShouldQueue;

class SendPublishedNotification implements ShouldQueue  // async
{
    public string $queue = 'notifications';

    public function handle(ArticlePublished $event): void {
        $event->article->author->notify(
            new ArticlePublishedNotification($event->article)
        );
    }
}

// Enregistrement dans EventServiceProvider (ou découverte auto)
// app/Providers/EventServiceProvider.php
protected $listen = [
    ArticlePublished::class => [
        SendPublishedNotification::class,
        IndexArticleForSearch::class,
    ],
];

// Dispatcher l'événement
ArticlePublished::dispatch($article);
event(new ArticlePublished($article)); // équivalent

Task Scheduling

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

Schedule::command('sitemap:generate')->daily();
Schedule::command('reports:weekly')->weeklyOn(1, '8:00');  // lundi 8h
Schedule::command('db:backup')->everyDay()->at('02:00');
Schedule::command('cache:clear')->hourly();

// Job en schedule
Schedule::job(new GenerateSitemapJob())->daily();

// Closure
Schedule::call(function () {
    Article::where('scheduled_at', '<=', now())
        ->update(['status' => 'published']);
})->everyMinute();

// Conditions
Schedule::command('stats:compute')
    ->daily()
    ->when(fn() => config('features.stats_enabled'))
    ->withoutOverlapping()   // ne lance pas si déjà en cours
    ->onOneServer()          // un seul serveur (Redis lock)
    ->runInBackground();     // ne bloque pas le scheduler
# Cron job à configurer sur le serveur (1 seule entrée suffit)
* * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1

# Ou avec le daemon Laravel (sans cron)
php artisan schedule:work

Notifications

php artisan make:notification OrderShipped
// app/Notifications/OrderShipped.php
namespace App\Notifications;

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

class OrderShipped extends Notification implements ShouldQueue
{
    use Queueable;

    public function __construct(
        private readonly Order $order
    ) {}

    // Canaux utilisés
    public function via(object $notifiable): array {
        return ['mail', 'database'];
    }

    // Mail
    public function toMail(object $notifiable): MailMessage {
        return (new MailMessage)
            ->subject('Votre commande #' . $this->order->id . ' a été expédiée')
            ->greeting('Bonjour ' . $notifiable->name . ',')
            ->line('Votre commande a été expédiée.')
            ->action('Suivre ma commande', url('/orders/' . $this->order->id))
            ->line('Merci pour votre confiance !');
    }

    // Base de données (table notifications)
    public function toDatabase(object $notifiable): array {
        return [
            'order_id'      => $this->order->id,
            'tracking_code' => $this->order->tracking_code,
        ];
    }
}

// Envoyer une notification
$user->notify(new OrderShipped($order));

// Notifications en masse
Notification::send($users, new OrderShipped($order));

// Lire les notifications
$user->notifications;            // toutes
$user->unreadNotifications;     // non lues
$user->notifications()->first()->markAsRead();

Cache

// Stocker et récupérer
Cache::put('key', 'value', now()->addMinutes(30));
Cache::put('key', 'value', 3600); // secondes

$value = Cache::get('key');
$value = Cache::get('key', 'default');
$value = Cache::get('key', fn() => 'computed default');

// remember — récupère ou calcule + stocke
$articles = Cache::remember('homepage.articles', 3600, function () {
    return Article::published()->latest()->take(10)->get();
});

// rememberForever — pas d'expiration
$settings = Cache::rememberForever('app.settings', fn() => Setting::all());

// Supprimer
Cache::forget('key');
Cache::flush();  // tout vider (attention en prod !)

// Vérifier
Cache::has('key');
Cache::missing('key');

// Tags (Redis/Memcached uniquement)
Cache::tags(['articles', 'user-1'])->put('key', $value, 3600);
Cache::tags(['articles'])->flush(); // invalider tout le tag

// Cache de requête Eloquent (scoped caching)
$count = Cache::remember('articles.published.count', 300, function () {
    return Article::published()->count();
});

// Incréments atomiques (compteurs)
Cache::increment('visitors');
Cache::increment('visitors', 5);
Cache::decrement('visitors');

Docker

# Dockerfile
FROM php:8.3-fpm-alpine

RUN apk add --no-cache \
    git curl libpng-dev libzip-dev zip unzip \
    && docker-php-ext-install pdo pdo_mysql zip gd opcache

# Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

WORKDIR /var/www/app

# Dépendances en premier (cache layer)
COPY composer.json composer.lock ./
RUN composer install --no-scripts --no-autoloader --prefer-dist

COPY . .
RUN composer dump-autoload --optimize

COPY docker/php/opcache.ini /usr/local/etc/php/conf.d/opcache.ini
RUN chown -R www-data:www-data /var/www/app/storage
# docker-compose.yml
services:
  app:
    build: .
    volumes:
      - .:/var/www/app
    depends_on: [mysql, redis]
    environment:
      - APP_ENV=local
      - DB_HOST=mysql
      - REDIS_HOST=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 --tries=3
    depends_on: [app, redis]

volumes:
  mysql_data:

Déploiement

# Script de déploiement (deploy.sh)

# 1. Passer en maintenance
php artisan down --retry=60

# 2. Récupérer le nouveau code
git pull origin main

# 3. Installer les dépendances (sans dev)
composer install --no-dev --optimize-autoloader

# 4. Migrations
php artisan migrate --force

# 5. Vider et reconstruire les caches
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache

# 6. Redémarrer les workers
php artisan queue:restart

# 7. Remettre en ligne
php artisan up
// .env en production
APP_ENV=production
APP_DEBUG=false
APP_URL=https://monapp.com

LOG_CHANNEL=stack
LOG_LEVEL=warning

SESSION_DRIVER=redis
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis

// config/logging.php — logs structurés (JSON) pour Papertrail/Logtail
'channels' => [
    'stack' => [
        'driver'   => 'stack',
        'channels' => ['daily', 'stderr'],
    ],
    'daily' => [
        'driver' => 'daily',
        'path'   => storage_path('logs/laravel.log'),
        'level'  => env('LOG_LEVEL', 'debug'),
        'days'   => 14,
    ],
];

Horizon & Telescope

# Horizon — dashboard pour les queues Redis
composer require laravel/horizon
php artisan horizon:install
php artisan horizon

# En production (Supervisor)
# /etc/supervisor/conf.d/horizon.conf
[program:laravel-horizon]
command=php /var/www/app/artisan horizon
autostart=true
autorestart=true
user=www-data

# Telescope — debugging dashboard (dev uniquement)
composer require laravel/telescope --dev
php artisan telescope:install
php artisan migrate
# → /telescope dans le navigateur
// config/horizon.php — configurer les workers par environnement
'environments' => [
    'production' => [
        'supervisor-1' => [
            'maxProcesses'  => 10,
            'balanceMaxShift' => 1,
            'balanceCooldown' => 3,
        ],
    ],
    'local' => [
        'supervisor-1' => [
            'maxProcesses' => 3,
        ],
    ],
],

// Métriques Horizon
// Horizon suit automatiquement :
// - Throughput (jobs/minute)
// - Temps d'exécution moyen
// - Jobs en attente / en cours / échoués
// Accessible sur /horizon (protégé par HorizonServiceProvider)
← Module 07 ▶ Mini-projet 🧠 QCM Projet Final →