Using Laravel's Model Events to Auto-increment Fields

When your auto-incrementing needs extend beyond the database defaults, reach for model events.

Auto-incrementing is easy… right? It certainly should be. Chances are your database of choice auto-increments by default on primary keys and by choice on any integer field you want. Ok cool. Article over?

Not even close. Sprinkle just the tiniest bit of complexity on top, and database defaults are out the door. What if you want to auto-increment but include an additional field as a parameter? Let’s take a real-world example: incrementing invoice numbers by company. Company A can have invoice number 1, and Company 2 can have invoice number 1. We can enforce uniqueness on the two fields, but can we auto-increment?

Sadly, common database solutions will just shake their heads at you here. You gotta roll your own solution, champ. Thankfully, with Laravel, rolling our own solution is a piece of cake.

Let’s stick with the invoice example. Let’s assume an invoice will always be attached to a company. What are some things we’d want from our auto-incrementing solution?

  • We want the app to take care of the logic for us, to rely on it the same way we’d rely on a MySql auto-increment or something similar. 

  • We want this logic to run whenever a new invoice is created or is about to be created. 

We might want other niceties, like prefixing 0s to very small invoice numbers. We’ll worry about those later. To satisfy the most important requirements, we will reach for model events.

Model events have been around in Laravel for a long time. They are essentially ways to hook into a model's lifecycle, from creation to deletion. We are going to hook into the creating event to handle our auto-incrementing.

You can listen and respond to these events with your own event classes, but we will use closures in this example. To utilize closures for responding to model events, define them in the models’ booted method:

namespace App\Models;

class Invoice extends Model
{
   protected static function booted():void
   {
       static::created(function (Invoice $invoice){
           //...
       })
   }
}

By defining a static created function, we will hook into any creation of this model, whether through a factory, in-app, or anywhere Eloquent is used to create an invoice. Pretty neat!

The closure we provide is going to receive a specific invoice. In this case, one that is about to be saved. We can interrupt this process to determine what the next invoice number should be:

namespace App\Models;

class Invoice extends Model
{
   protected static function booted():void
   {
       static::created(function (Invoice $invoice){
           // Get the last invoice for this company
           $lastInvoice = Invoice::where('company_id', '=', $invoice->company_id)
               ->select('invoice_number')
               ->last();

           // Handle no previous invoice
           if (!$lastInvoice) {
               $invoice->invoice_number = 1;
           } else {
               $invoice->invoice_number = $lastInvoice->invoice_number + 1;
           }
       })
   }
}

Technically, we are done. This little code will handle auto-incrementing for us, with a minuscule DB lookup per invoice created. Pretty great, right?

We can go a bit further, though. What if this is the only code we want to use to set the invoice number to avoid collisions? Let’s define a mutator for our invoices but prevent setting the invoice number altogether. 

namespace App\Models;

use Illuminate\Database\Eloquent\Casts\Attribute;

class Invoice extends Model
{
   // Event closure up here
   public function invoiceNumber(): Attribute
   {
       return Attribute::make(
           set: fn($value) => throw new \Exception('Invoice number is read-only')
       );
   }
}

Sweet, now only our auto-incrementing solution can set the invoice number. Or can it? We’ve broken it with this new code because we are trying to set the invoice number on the invoice model in our creating closure, which will trigger this error. Not to worry, we can bypass the setter by using the DB facade:

namespace App\Models;

class Invoice extends Model
{

   protected static function booted():void
   {
       static::created(function (Invoice $invoice){
           // Get the last invoice for this company
           $lastInvoice = Invoice::where('company_id', '=', $invoice->company_id)
               ->select('invoice_number')
               ->last();

           // Set up the query to update this invoice
           $query = DB::table('invoices')->where('id', '=', $invoice->id);
           // Handle no previous invoice
           if (!$lastInvoice) {
               $query->update(['invoice_number' => 1]);
           } else {
               $query->update(['invoice_number' => $lastInvoice->invoice_number +1]);
           }
       })
   }


   public function invoiceNumber(): Attribute
   {
       return Attribute::make(
           set: fn($value) => throw new \Exception('Invoice number is read-only')
       );
   }
}

Now, any attempt to use eloquent to set the invoice number will throw an error, but if we need to, we can bypass it with the DB facade. Nifty, but goes to show no solution is perfect. Documentation and training ensure unsuspecting developers do not break your business logic. 

If you wanted to hook into the creating event instead, you could change the update method to an insertion. Similarly, if you wanted to prefix the invoice numbers, as we mentioned earlier, you can define a get method in the accessor to prefix the integer with leading 0s. You would then need to bypass the accessor when incrementing the number in your event hook. Everything has tradeoffs, but with model events, you have a great foundation.

I hope this article was helpful. Happy developing out there!

Categories

  • Development

Tags:

  • Code
  • Laravel
  • Tutorial