Laravel: Паттерн Pending Object
Что такое Pending Object
Задумывались ли вы когда-нибудь, что происходит при использовании метода Mail::to
?
Mail::to($request->user())
->send(new OrderShipped($order));
Метод to()
здесь не выдаёт объект Mail
. Скорее, он приводит к объекту PendingMail
.
namespace Illuminate\Mail;
class Mailer
{
public function to($users, $name = null)
{
if (! is_null($name) && is_string($users)) {
$users = new Address($users, $name);
}
return (new PendingMail($this))->to($users);
}
}
Преимущество такого подхода заключается в том, что каждый Mail::to
будет иметь свой эксклюзивный отложенный объект, в котором можно вызывать несколько методов для изменения любого аспекта, связанного с этим конкретным почтовым объектом, который вы инициировали.
namespace Illuminate\Mail;
class PendingMail
{
public function __construct(MailerContract $mailer)
public function locale($locale)
public function to($users)
public function cc($users)
public function bcc($users)
public function send(MailableContract $mailable)
public function queue(MailableContract $mailable)
public function later($delay, MailableContract $mailable)
}
Итак, если мы исследуем, что делает метод cc()
:
/**
* Set the recipients of the message.
*
* @param mixed $users
* @return $this
*/
public function cc($users)
{
$this->cc = $users;
return $this;
}
Внешне он напоминает Data Transfer Object (DTO), в котором для обмена данными между уровнями приложения используются сеттеры и геттеры. Однако существенным отличием основного подхода Laravel является то, что Pending Objects являются объектами действия. Этот принцип проявляется в таких методах, как send
и queue
.
public function send(MailableContract $mailable)
{
return $this->mailer->send($this->fill($mailable));
}
public function queue(MailableContract $mailable)
{
return $this->mailer->queue($this->fill($mailable));
}
Основные Pending Object в Laravel 10
Существует множество объектов Pending Objects, которые можно изучить и понять их функциональность:
Illuminate/Database/Eloquent/PendingHasThroughRelationship
Illuminate/Broadcasting/PendingBroadcast
Illuminate/Mail/PendingMail
Illuminate/Foundation/Bus/PendingChain
Illuminate/Foundation/Bus/PendingDispatch
Illuminate/Foundation/Bus/PendingClosureDispatch
Illuminate/Bus/PendingBatch
Illuminate/Testing/PendingCommand
Illuminate/Support/Testing/Fakes/PendingBatchFake
Illuminate/Support/Testing/Fakes/PendingMailFake
Illuminate/Support/Testing/Fakes/PendingChainFake
Illuminate/Http/Client/PendingRequest
Illuminate/Routing/PendingResourceRegistration
Illuminate/Routing/PendingSingletonResourceRegistration
Illuminate/Process/PendingProcess
Применение Pending Object
Теперь попробуем сконструировать Pending Action для экспортёра CSV.
$users = User::all()->toArray();
CsvExporter::from($users)
->columns(['email', 'username'])
->noHeaders()
->download()
Этот пример демонстрирует работу экспортёра CSV и то, как объект Pending Object может помочь нам в создании CSV файла. Сначала мы создадим класс CsvExporter
.
namespace App\Services\Exporter;
class CsvExporter
{
public function from(array $data): PendingCsvExport
{
return new PendingCsvExport($data, $this);
}
public function generate(array $data, array $columns, string $delimiter = ',', bool $includeHeaders = true): string
{
$output = fopen('php://temp', 'r+');
if ($includeHeaders && !empty($data) && !empty($columns)) {
fputcsv($output, $columns, $delimiter);
}
foreach ($data as $row) {
$selectedData = [];
foreach ($columns as $column) {
$selectedData[] = $row[$column] ?? null;
}
fputcsv($output, $selectedData, $delimiter);
}
rewind($output);
$csvContent = stream_get_contents($output);
fclose($output);
return $csvContent;
}
Помимо метода генерации, я выбрал прямой подход для демонстрации и создания реальной функции экспорта CSV. Однако при желании можно выбрать пакет, специально предназначенный для решения этой задачи.
Далее создадим объект PendingCSVExport
.
namespace App\Services\Exporter;
use Illuminate\Support\Facades\Response;
class PendingCsvExport
{
protected array $data;
protected array $columns = [];
protected bool $includeHeaders = true;
protected string $delimiter = ',';
protected CsvExporter $exporter;
public function __construct(array $data, CsvExporter $exporter)
{
$this->data = $data;
$this->exporter = $exporter;
}
public function columns(array $columns)
{
$this->columns = $columns;
return $this;
}
public function noHeaders()
{
$this->includeHeaders = false;
return $this;
}
public function delimiter(string $delimiter)
{
$this->delimiter = $delimiter;
return $this;
}
public function download($filename = 'export.csv')
{
$content = $this->exporter->generate($this->data, $this->columns, $this->delimiter, $this->includeHeaders);
return Response::make($content, 200, [
'Content-Type' => 'text/csv',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
]);
}
}
Здесь видно, как наш объект PendingObject хранит некоторые свойства макета CSV и данных. Затем используется метод download
с одним действием. В дальнейшем можно добавить другие действия, такие как stream
, queue
и mail
. Чтобы поставить экспорт в очередь и отправить его по почте. Или просто сгенерировать CSV и отправить его непосредственно пользователю.
Автоматическое выполнение
Задумывались ли вы когда-нибудь о механизме, благодаря которому работают диспетчерские службы?
ProcessPodcast::dispatch();
ProcessPodcast::dispatch()->onQueue('emails');
Наблюдая за тем, как dispatch
просто отправляет задание, но при этом, если создать цепочку методов типа onQueue
, он учитывает это и все равно отправляет задание, можно задаться вопросом о драйвере, стоящем за этой операцией. Ответ кроется в волшебном методе __destruct
.
public function __destruct()
{
if (! $this->shouldDispatch()) {
return;
} elseif ($this->afterResponse) {
app(Dispatcher::class)->dispatchAfterResponse($this->job);
} else {
app(Dispatcher::class)->dispatch($this->job);
}
}
Таким образом, в действительности происходит так: когда вы пишете SomeJob::dispatch()
, она возвращает только объект PendingObject. Впоследствии PHP вызывает метод __destruct
, когда начинает процесс сборки мусора (подробнее об этом можно прочитать в документации PHP.NET). Laravel использует эту технику для удобного автоматического выполнения отложенного объекта, избавляя вас от необходимости запускать завершающий метод, такой как ->run()
или ->send()
.
На этом мы завершаем рассмотрение паттерна Pending Object.
Счастливого кодинга!