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/PendingHasThroughRelationshipIlluminate/Broadcasting/PendingBroadcastIlluminate/Mail/PendingMailIlluminate/Foundation/Bus/PendingChainIlluminate/Foundation/Bus/PendingDispatchIlluminate/Foundation/Bus/PendingClosureDispatchIlluminate/Bus/PendingBatchIlluminate/Testing/PendingCommandIlluminate/Support/Testing/Fakes/PendingBatchFakeIlluminate/Support/Testing/Fakes/PendingMailFakeIlluminate/Support/Testing/Fakes/PendingChainFakeIlluminate/Http/Client/PendingRequestIlluminate/Routing/PendingResourceRegistrationIlluminate/Routing/PendingSingletonResourceRegistrationIlluminate/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.
Счастливого кодинга!