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)