Laravel Pending Object Pattern

post-thumb

The Pending Object pattern plays a key role in Laravel, as it is utilized almost in all its aspects. It offers an exceptional developer experience (DX) for artisans.

What is a Pending Object?

Have you ever wondered what transpires when you utilize the Mail::to method?

1Mail::to($request->user())
2	->send(new OrderShipped($order));

The to() method here does not yield a Mail object. Rather, it results in a PendingMail object.

 1namespace Illuminate\Mail;
 2
 3class Mailer
 4{
 5	public function to($users, $name = null)
 6	{
 7		if (! is_null($name) && is_string($users)) {
 8		    $users = new Address($users, $name);
 9		}
10
11		return (new PendingMail($this))->to($users);
12	}
13}

The advantage of this approach is that each Mail::to will have its exclusive pending object where you can invoke several methods to modify any aspect related to this specific mail object you’ve initiated.

 1namespace Illuminate\Mail;
 2
 3class PendingMail
 4{
 5    public function __construct(MailerContract $mailer)
 6    public function locale($locale)
 7    public function to($users)
 8    public function cc($users)
 9    public function bcc($users)
10    public function send(MailableContract $mailable)
11    public function queue(MailableContract $mailable)
12    public function later($delay, MailableContract $mailable)
13}

So, if we investigate what the cc() method is doing:

 1/**
 2 * Set the recipients of the message.
 3 *
 4 * @param  mixed  $users
 5 * @return $this
 6 */
 7public function cc($users)
 8{
 9    $this->cc = $users;
10
11    return $this;
12}

It seems to resemble a Data Transfer Object (DTO), in which you use setters and getters for data exchange between your application’s layers. However, a significant distinction in Laravel’s core approach is that Pending Objects are actionable. This principle is apparent in methods such as send and queue.

1public function send(MailableContract $mailable)
2{
3    return $this->mailer->send($this->fill($mailable));
4}
5
6public function queue(MailableContract $mailable)
7{
8    return $this->mailer->queue($this->fill($mailable));
9}

Core Pending Objects In Laravel 10

there exists an array of Pending Objects for you to explore and comprehend their functionality:

  • 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

The Application of the Pending Object

Now, let’s attempt constructing a Pending Action for a CSV exporter.

1$users = User::all()->toArray();
2
3CsvExporter::from($users)
4	->columns(['email', 'username'])
5	->noHeaders()
6	->download()

This example demonstrates a CSV exporter and how a Pending Object can assist us in constructing this CSV file. First, we’ll create the CsvExporter class.

 1namespace App\Services\Exporter;
 2
 3class CsvExporter
 4{
 5    public function from(array $data): PendingCsvExport
 6    {
 7        return new PendingCsvExport($data, $this);
 8    }
 9
10    public function generate(array $data, array $columns, string $delimiter = ',', bool $includeHeaders = true): string
11    {
12        $output = fopen('php://temp', 'r+');
13
14        if ($includeHeaders && !empty($data) && !empty($columns)) {
15            fputcsv($output, $columns, $delimiter);
16        }
17
18        foreach ($data as $row) {
19            $selectedData = [];
20            foreach ($columns as $column) {
21                $selectedData[] = $row[$column] ?? null;
22            }
23            fputcsv($output, $selectedData, $delimiter);
24        }
25
26        rewind($output);
27        $csvContent = stream_get_contents($output);
28        fclose($output);
29
30        return $csvContent;
31    }
32}

Beyond the generate method, I’ve chosen a straightforward approach to demonstrate and create a real CSV export function. However, you can opt for a package specifically designed for this task if you wish.

Next, let’s establish the PendingCSVExport object.

 1namespace App\Services\Exporter;
 2
 3use Illuminate\Support\Facades\Response;
 4
 5class PendingCsvExport
 6{
 7    protected array $data;
 8    protected array $columns = [];
 9    protected bool $includeHeaders = true;
10    protected string $delimiter = ',';
11    protected CsvExporter $exporter;
12
13    public function __construct(array $data, CsvExporter $exporter)
14    {
15        $this->data = $data;
16        $this->exporter = $exporter;
17    }
18
19    public function columns(array $columns)
20    {
21        $this->columns = $columns;
22        return $this;
23    }
24
25    public function noHeaders()
26    {
27        $this->includeHeaders = false;
28        return $this;
29    }
30
31    public function delimiter(string $delimiter)
32    {
33        $this->delimiter = $delimiter;
34        return $this;
35    }
36
37    public function download($filename = 'export.csv')
38    {
39        $content = $this->exporter->generate($this->data, $this->columns, $this->delimiter, $this->includeHeaders);
40
41        return Response::make($content, 200, [
42            'Content-Type' => 'text/csv',
43            'Content-Disposition' => 'attachment; filename="' . $filename . '"',
44        ]);
45    }
46}

See here how our PendingObject holds some properties about the CSV layout and data. Then a single action method download. Later on you can add more actions like stream, queue, and mail. To either queue the export and send it as a mail. Or to just generate the CSV and mail it directly to the user.

Automatic Execution

Have you ever considered the mechanism behind dispatch jobs operating in numerous ways?

1ProcessPodcast::dispatch();
2
3ProcessPodcast::dispatch()->onQueue('emails');

Observing how dispatch merely dispatches the job, but also, if you chain a method like onQueue, it takes that into account and still dispatches the job, you might wonder about the driver behind this operation. The answer lies in the magic __destruct method.

 1public function __destruct()
 2{
 3    if (! $this->shouldDispatch()) {
 4        return;
 5    } elseif ($this->afterResponse) {
 6        app(Dispatcher::class)->dispatchAfterResponse($this->job);
 7    } else {
 8        app(Dispatcher::class)->dispatch($this->job);
 9    }
10}

So what actually occurs here is that when you write SomeJob::dispatch(), it only returns a PendingObject. Subsequently, PHP invokes the __destruct method when it commences the garbage collection process (you can read more about it from the PHP.NET Documentation ) Laravel leverages this technique to conveniently self-execute the pending object, eliminating the need for you to trigger a concluding method such as ->run() or ->send().

And that wraps up our discussion on the Pending Object pattern.

Happy Coding!

comments powered by Disqus

You May Also Like