Skip to content

Instantly share code, notes, and snippets.

@atomjoy
Last active June 21, 2025 08:23
Show Gist options
  • Save atomjoy/252a10278c5c8c01b410716e6d1fcd34 to your computer and use it in GitHub Desktop.
Save atomjoy/252a10278c5c8c01b410716e6d1fcd34 to your computer and use it in GitHub Desktop.
Kolejkowanie wiadomości email w Laravel (queue, jobs, events).

Queue Mail Jobs Laravel

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.

Dodaj zadanie do kolejki

$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'));

Wiadomości email w kolejce

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 [];
	}
}

Utwórz zadanie (Jobs)

php artisan make:job LunchEmailJob

Przykład zadania

<?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;
	}
}

Limitowanie wysyłania

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;
		});
	}
}

RateLimiter w AppServiceProvider

/**
 * Bootstrap services.
 */
public function boot(): void
{
	// Rate limit email job
	RateLimiter::for('send-email-job', function ($job) {
		return Limit::perMinute(60);
	});
}

Zdarzenia w kolejkach

<?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);
				}
			}
		});
	}
}

Klasa details dla wiadomości

<?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
	){}
}

Widok wiadomości email

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>

Konfiguracja Redis

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

Uruchom workera kolejki z cron

# 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment