Unveiling the magic behind Laravel's PendingDispatch

There's an interesting snippet demonstrated by Taylor Otwell at Laracon EU 2019 that dispatches a few queue jobs. It looks seemingly easy to understand, but, of course, the devil is in the details.

Let's try to dispatch a queue job on our own:

<?php

// Bunch of imports...

$post = Post::factory()
    ->create();

BroadcastToSubscribers::dispatch($post);

Looks pretty straightforward - a new Post model instance is created and a BroadcastToSubscribers queue job is dispatched.

Let's make our task a bit more complex by specifying a queue connection name:

<?php

// Bunch of imports...

$post = Post::factory()
    ->create();

BroadcastToSubscribers::dispatch($post)
    ->onConnection('my-custom-connection');

There are no method calls like finish or enqueue to indicate that we finished the dispatch configuration, so does Laravel know that we call the onConnection method after calling the dispatch method?

We can dump the result of the dispatch method call to see that we have an instance of the Illuminate\Foundation\Bus\PendingDispatch class, which doesn't have any methods to manually finish the queue job dispatch process.

Upon closer inspection, however, we can see that PendingDispatch has the __destruct method, that seemingly does all the heavy lifting.

__destruct is a PHP magic method, that is automatically invoked as soon as all object references are dropped, effectively acting as a destructor.

You may be familiar with destructors already, as they are also used in other languages:

PendingDispatch utilizes the __destruct method to automatically dispatch queue jobs at the end of the current scope:

<?php

// Bunch of imports...

$post = Post::factory()
    ->create();

$pd = BroadcastToSubscribers::dispatch($post) // PendingDispatch starts its lifecycle
    ->onConnection('my-custom-connection');

// End of scope, time to call __destruct and dispatch the queue job

We can utilize such a technique to write something on our own.

Here is a transaction guard implementation1 that will commit a database transaction as soon as the execution scope reaches its end:

<?php

use Illuminate\Database\ConnectionInterface;

class TransactionGuard
{
    protected bool $executed = false;

    protected ConnectionInterface $connection;

    public function __construct(ConnectionInterface $connection)
    {
        $this->connection = $connection;

        // Start a new transaction using the default database connection
        $this->connection->beginTransaction();
    }

    public function commit()
    {
        // We need to check if the active transaction was either committed or rolled back
        // because we don't want to run the logic twice
        if (!$this->executed) {
            $this->connection->commit();
        }

        $this->executed = true;
    }

    public function rollback()
    {
        if (!$this->executed) {
            $this->connection->rollback();
        }

        $this->executed = true;
    }

    public function __destruct()
    {
        $this->commit();
    }
}

Usage (example source):

<?php

Route::get('/test', function (TransactionGuard $guard) {
    DB::update('update users set votes = 1');

    DB::delete('delete from posts');
});

In this case TransactionGuard will commit the active database transaction at the end of the request execution scope.

1 - guard terminology is taken from Rust's MutexGuard, RwLock{Read, Write}Guard and similar structs.