Laravel Pending Object Pattern
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!