Sebastian De Deyne
Designer & developer at Spatie

Use Blink to execute something once and only once

Our Blink package is marketed as a caching solution to memoize data for the duration of a web request. Recently, we came upon another use case for the package: to execute something once and only once.

Blink looks just like Laravel’s cache. You can store things and retrieve them. The Blink cache is stored on a singleton object, so it’s destroyed at the end of the request (similar to Laravel’s array cache driver).

$orders = blink('orders', fn () => Order::all());

$orders = blink('orders');

Now for a different use case. Say we want to generate and store PDF invoices for an order. We want to regenerate them whenever an order or an order line changes. We can use Eloquent events to determine when to dispatch a job.

Order::saved(fn (Order $order) =>
    dispatch(new GenerateInvoicePdf($order))
);

OrderLine::saved(fn (OrderLine $order) =>
    dispatch(new GenerateInvoicePdf($orderLine->order))
);

If someone modifies an order and an order line in the same request, GenerateInvoicePdf will be dispatched twice, clogging the queue with unnecessary work.

Instead, we want to make sure the job only gets dispatched once. Here’s where Blink comes in. Instead of using Blink to store something, we’ll use it to execute something only once.

Order::saved(fn (Order $order) =>
    blink('generate-invoice-pdf:' . $order->id, fn () =>
        dispatch(new GenerateInvoicePdf($order))
    )
);

OrderLine::saved(fn (OrderLine $order) =>
    blink('generate-invoice-pdf:' . $orderLine->order->id, fn () =>
        dispatch(new GenerateInvoicePdf($orderLine->order))
    )
);

Now it doesn’t matter how many saved events are triggered; the job will only be dispatched once.

Cleaning things up, I like keeping cache calls to the same key in one place.

class Order extends Model
{
    public function generateInvoicePdf(): void
    {
        blink('generate-invoice-pdf:' . $this->id, fn () =>
            dispatch(new GenerateInvoicePdf($this))
        );
    }
}

Order::saved(fn (Order $order) =>
    $order->generateInvoicePdf()
);

OrderLine::saved(fn (OrderLine $order) =>
    $orderLine->order->generateInvoicePdf()
);

This won’t work when the environment is persisted across requests, like in a Horizon worker or Laravel Octane. In that case, you can fall store a should_regenerate boolean on the orders table and schedule a command to dispatch the GenerateInvoicePdf jobs.

If you enjoyed this post, you might be interested in my newsletter. I occasionally send a dispatch with personal stories, things I’ve been working on in the past month, and other interesting tidbits I come across online.