Laravel: Паттерн Pending Object

Источник: «Laravel Pending Object Pattern»
Паттерн Pending Object играет ключевую роль в Laravel, поскольку используется практически во всех его аспектах. Он обеспечивает исключительный опыт разработчика (DX).

Что такое 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, которые можно изучить и понять их функциональность:

Применение 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.

Счастливого кодинга!

Дополнительные материалы

Предыдущая Статья

Как избежать CSS-сдвигов макета связанных с ch

Следующая Статья

firstOrCreate() vs createOrFirst()