Laravel pozwala na łatwe tworzenie zadań w kolejce, które mogą być przetwarzane w tle. Przenosząc czasochłonne zadania do kolejki, Twoja aplikacja może odpowiadać na żądania internetowe szybciej i zapewniać lepsze wrażenia użytkownika.
$details = [
'email' => '[email protected]',
'subject' => 'Welcome Email',
'title' => 'Email title goes here',
'message' => 'Email message goes here',
'link_url' => null,
'image_url' => null,
];
# Mail to queue
Mail::onQueue('lunch')->to($details['email'])->queue(new LunchEmail(new LunchEmailMessage(...$details)));
# Mail with ShouldQueue
Mail::onQueue('lunch')->to($details['email'])->send(new LunchEmail(new LunchEmailMessage(...$details)))
# Jobs
LunchEmailJob::dispatch($details)->onQueue('lunch');
dispatch((new LunchEmailJob($details))->onQueue('lunch'));
Po zaimplementowaniu kontraktu ShouldQueue wiadomość będą zawsze dodawane do kolejki 'default'. Nazwę kolejki zmieniamy w konstruktorze ($this->queue='nazwa_kolejki') lub dodając metodę Mail::onQueue('nazwa_kolejki').
<?php
namespace App\Mail;
use App\Mail\Messages\LunchEmailMessage;
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 LunchEmail extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
public $details = null;
public function __construct(LunchEmailMessage $details)
{
// Paramaetry wiadomości przekazywane do View (jako $details)
$this->details = $details;
// Ustawiamy nazwę kolejki, bez tego jeżeli w Job w metodzie handler()
// wywołamy Mail::to()->send() bez metody onQueue('lunch') przy ponownym
// wykonaniu Job ($tries > 0) nazwa kolejki zostanie zmieniona na default.
$this->queue = 'lunch';
}
public function envelope(): Envelope
{
return new Envelope(
subject: $this->details->subject,
);
}
public function content(): Content
{
return new Content(
view: 'emails.lunch.default',
);
}
public function attachments(): array
{
return [];
}
}
php artisan make:job LunchEmailJob
<?php
namespace App\Jobs;
use Throwable;
use App\Mail\LunchEmail;
use App\Mail\Messages\LunchEmailMessage;
use App\Jobs\Middleware\RateLimitedWithRedis;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Mail;
use Illuminate\Queue\Middleware\RateLimited;
class LunchEmailJob implements ShouldQueue
{
use Queueable;
protected $details;
public $tries = 0;
public $backoff = 0;
public $timeout = 60;
public $priority = 'high';
public function __construct(LunchEmailMessage $details, $queue = 'lunch')
{
// Wiadomość email
$this->details = $details;
// Wymuś nazwę kolejki, można dodać ->onQueue('lunch') na klasie Job lub tutaj.
$this->queue = $queue;
}
public function handle(): void
{
// Dodaj nazwę kolejki do klasy wiadomości email lub w konstruktorze LunchEmail
Mail::to($this->details->email)->send((new LunchEmail($this->details))->onQueue($this->queue));
// Działa też z onQueue() lub dodaj w konstruktorze nazwę kolejki dla mailable,
// zapobiega to zmianie nazwy kolejki na default po powtórnym wykonaniu zadania
Mail::onQueue('lunch')->to($this->details->email)->send(new LunchEmail($this->details));
}
public function middleware(): array
{
return [
// Send 25 every 1 sec
new RateLimitedWithRedis(25, 1),
// Send 60 per minute (z SericeProvider)
// new RateLimited('send-email-job'),
];
}
public function failed(?Throwable $exception): void
{
// Send user notification of failure, etc...
}
public function getDetails()
{
return $this->details;
}
}
Zainstaluj pakiet do obsługi Redisa dodaj konfigurację w .env i odkomentuj RateLimitedWithRedis w middleware() klasy LunchEmailJob.
<?php
namespace App\Jobs\Middleware;
use Closure;
use Illuminate\Support\Facades\Redis;
class RateLimitedWithRedis
{
public function __construct(
protected $maxAttempts = 25,
protected $decaySeconds = 1,
protected $key = 'rate_limit_send_email_job'
){}
/**
* Process the queued job.
*
* @param \Closure(object): void $next
*/
public function handle(object $job, Closure $next): void
{
// $key = "rate_limit_" . $job->payload()['uuid'];
Redis::throttle($this->key)
->block(0)->allow($this->maxAttempts)->every($this->decaySeconds)
->then(function () use ($job, $next) {
// Lock obtained...
$next($job);
}, function () use ($job) {
// Could not obtain lock...
$job->release($this->decaySeconds);
return;
});
}
}
/**
* Bootstrap services.
*/
public function boot(): void
{
// Rate limit email job
RateLimiter::for('send-email-job', function ($job) {
return Limit::perMinute(60);
});
}
<?php
namespace App\Providers;
use DateTime;
use App\Models\LogEmail;
use App\Jobs\LunchEmailJob;
use App\Mail\Messages\LunchEmailMessage;
use Illuminate\Mail\SendQueuedMailable;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\ServiceProvider;
use Illuminate\Queue\Events\JobProcessed;
use Illuminate\Queue\Events\JobProcessing;
class LunchQueueProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
//
}
/**
* Bootstrap services.
*/
public function boot(): void
{
// Rate limit email job
RateLimiter::for('send-email-job', function ($job) {
return Limit::perMinute(60);
});
// Queue events
Queue::before(function (JobProcessing $event) {
// $event->connectionName
// $event->job->payload()
});
Queue::after(function (JobProcessed $event) {
// $event->connectionName
// $event->job->payload()
// Get object
$data = unserialize($event->job->payload()['data']['command']);
// Log jobs
if ($data instanceof LunchEmailJob) {
// $data = $data->getDetails();
// Log here ...
}
// Log sent emails to table and file
if ($data instanceof SendQueuedMailable) {
try {
$message = $data->mailable->details;
$queue_name = 'default';
// Lunch emails
if ($message instanceof LunchEmailMessage) {
$queue_name = 'lunch';
}
// Add to table
LogEmail::create([
'email' => $message->email,
'subject' => $message->subject,
'message' => $message->message,
'queue' => $queue_name,
]);
// Save to log
Log::build([
'driver' => 'single',
'path' => storage_path('logs/queue_lunch.log'),
])->info("EMAIL_SENT", [
'time' => (new DateTime('now'))->format('H:i:s.v'),
'queue' => $queue_name,
'message' => $message,
]);
} catch (\Throwable $e) {
report($e);
}
}
});
}
}
<?php
namespace App\Mail\Messages;
class LunchEmailMessage
{
function __construct(
public $email,
public $subject,
public $title,
public $message,
public $link_url = null,
public $image_url = null
){}
}
emails/lunch/default.blade.php
<!DOCTYPE html>
<html>
<head>
<title>{{ $details->subject ?? '' }}</title>
<style>
a.lunch_email_button {
text-decoration: none;
padding: 15px 25px;
color: #fff !important;
background: #5c5 !important;
border-radius: 50px;
font-weight: bold;
}
</style>
</head>
<body>
<center>
<a href="{{ $details->link_url ?? request()->getSchemeAndHttpHost() }}">
<img src="{{ $details->image_url ?? 'https://images.unsplash.com/photo-1473093295043-cdd812d0e601' }}" width="100%">
</a>
</center>
<h1>{{ $details->title ?? '' }}</h1>
<p>{{ $details->message ?? '' }}</p>
<center>
<a href="{{ $details->link_url ?? request()->getSchemeAndHttpHost() }}" class="lunch_email_button">
{{ __('See more') }}
</a>
</center>
<strong>{{ __('Hava a nice day!') }}</strong>
</body>
</html>
Dodaj tylko gdy używasz rate limiterów z Redis w middleware.
# Install
composer require predis/predis
# .Env Redis
REDIS_CLUSTER=predis
REDIS_CLIENT=predis
REDIS_PASSWORD=
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
# Terminal
php artisan queue:work -v --queue=default,emails,lunch --max-time=3600 --sleep=5 --memory=128
# Cron
crontab -e
# Queue restart every hour
0 * * * * /usr/bin/php /path-to-your-project/artisan queue:work --queue=default,emails,lunch --max-time=3600 --sleep=5 > /dev/null
# Queue restart every minute
* * * * * /usr/bin/php /path-to-your-project/artisan queue:work --queue=default,emails,lunch --max-time=60 --sleep=5 > /dev/null
# Schedule restart every minute
* * * * * /usr/bin/php /path-to-your-project/artisan schedule:run >> /dev/null 2>&1