When working with invoices, you need to deal with serial numbers that look like ABC-000001
. Do you know how to auto-generate them in Laravel? This tutorial will cover a few ways to do this.
For our example, we will use a simple invoice DB table with the following columns:
id
user_id
due_date
amount
serial
- Full serial number like ABC-1
serial_number
- Serial number like 1
serial_series
- Serial series like ABC
Here's how that looks in our migration:
Migration
Schema::create('invoices', function (Blueprint $table) { $table->id(); $table->foreignIdFor(User::class)->constrained(); $table->date('due_date'); $table->integer('amount'); $table->string('serial')->nullable(); $table->string('serial_series'); $table->integer('serial_number')->nullable(); $table->timestamps();});
This makes our Model look like this:
app/Models/Invoice.php
use Illuminate\Database\Eloquent\Casts\Attribute;use Illuminate\Database\Eloquent\Factories\HasFactory;use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\BelongsTo; class Invoice extends Model{ use HasFactory; protected $fillable = [ 'user_id', 'due_date', 'amount', 'serial', 'serial_number', 'serial_series', ]; protected function amount(): Attribute { return Attribute::make( get: fn($value) => $value / 100, set: fn($value) => $value * 100, ); } public function user(): BelongsTo { return $this->belongsTo(User::class); }}
Let's create the Controller methods to save a new invoice and auto-generate the serial number.
This is the form, with series options coming from the config.
app/Http/Controllers/InvoiceController.php
use App\Http\Requests\StoreInvoiceRequest;use App\Models\Invoice;use App\Models\User; class InvoiceController extends Controller{ public function create() { $users = User::pluck('name', 'id'); $invoiceSeries = config('invoiceSettings.availableInvoiceSeries'); return view('invoices.create', [ 'users' => $users, 'invoiceSeries' => $invoiceSeries, ]); }}
These are the config values:
config/invoiceSettings.php
return [ 'availableInvoiceSeries' => [ 'ABC', 'DAF', 'GHI', 'UKS' ],];
And here's the form in Blade file:
resources/views/invoices/create.blade.php
// ... layout with Tailwind/Breeze <form action="{{ route('invoice.store') }}" method="POST"> @csrf <div class="mb-4"> <label for="user_id" class="block text-gray-700 text-sm font-bold mb-2">User</label> <select id="user_id" name="user_id" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"> <option value="">Select User</option> @foreach($users as $id => $name) <option value="{{ $id }}" @selected(old('user_id') == $id)>{{ $name }}</option> @endforeach </select> @error('user_id') <div class="text-red-500 mt-2 text-sm"> {{ $message }} </div> @enderror </div> <div class="mb-4"> <label for="serial_series" class="block text-gray-700 text-sm font-bold mb-2">Invoice Series</label> <select id="serial_series" name="serial_series" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"> @foreach($invoiceSeries as $seriesCode) <option value="{{ $seriesCode }}" @selected(old('serial_series') == $seriesCode)>{{ $seriesCode }}</option> @endforeach </select> @error('serial_series') <div class="text-red-500 mt-2 text-sm"> {{ $message }} </div> @enderror </div> <div class="mb-4"> <label for="due_date" class="block text-gray-700 text-sm font-bold mb-2">Due Date</label> <input type="date" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="due_date" name="due_date" value="{{ old('due_date') }}"> @error('due_date') <div class="text-red-500 mt-2 text-sm"> {{ $message }} </div> @enderror </div> <div class="mb-4"> <label for="amount" class="block text-gray-700 text-sm font-bold mb-2">Amount</label> <input type="text" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="amount" name="amount" value="{{ old('amount') }}"> @error('amount') <div class="text-red-500 mt-2 text-sm"> {{ $message }} </div> @enderror </div> <div class="mb-4"> <button type="submit" class="bg-blue-500 text-white px-4 py-3 rounded font-medium w-full">Create Invoice </button> </div> </form>
Next, we'll look at how to save the new invoice and generate the serial numbers differently.
The first option is to generate the serial number when the invoice is created. This is the simplest option, and it looks like this:
app/Http/Controllers/InvoiceController.php
// ... public function store(StoreInvoiceRequest $request): RedirectResponse{ $data = $request->validated(); $data['serial_number'] = (Invoice::where('serial_series', $data['serial_series'])->max('serial_number') ?? 0) + 1; $data['serial'] = $data['serial_series'] . '-' . $data['serial_number']; Invoice::create($data); return redirect()->route('invoice.index');} // ...
And while this works, there are better options in my eyes.
You must remember to add this code to every place you create an invoice if there are multiple places, like an API Controller.
Code in Repository
Another option for generating an invoice number is to use Observers:
app/Models/Invoice.php
// ... protected static function booted(): void{ parent::booted(); self::creating(static function (Invoice $invoice) { $invoice->serial_number = (Invoice::where('serial_series', $invoice->serial_series)->max('serial_number') ?? 0) + 1; $invoice->serial = $invoice->serial_series . '-' . $invoice->serial_number; });} // ...
This allows us to drop a big piece of code from our Controller:
app/Http/Controllers/InvoiceController.php
// ... public function store(StoreInvoiceRequest $request): RedirectResponse{ $data = $request->validated(); $data['serial_number'] = (Invoice::where('serial_series', $data['serial_series'])->max('serial_number') ?? 0) + 1; $data['serial'] = $data['serial_series'] . '-' . $data['serial_number']; Invoice::create($data); Invoice::create($request->validated()); return redirect()->route('invoice.index');} // ...
We are making it much cleaner and providing an option to create an invoice in any way we want without worrying about the serial number.
Code in Repository
It's important to mention that Observers can be a separate file, too. They don't have to be defined in our Models and can be used as a dedicated Observer for a Model.
php artisan make:observer InvoiceObserver --model=Invoice
Here's how that looks:
app/Observers/InvoiceObserver.php
use App\Models\Invoice; class InvoiceObserver{ public function creating(Invoice $invoice): void { $invoice->serial_number = (Invoice::where('serial_series', $invoice->serial_series)->max('serial_number') ?? 0) + 1; $invoice->serial = $invoice->serial_series . '-' . $invoice->serial_number; }}
Of course, this has to be registered:
app/Models/Invoice.php
use App\Observers\InvoiceObserver;use Illuminate\Database\Eloquent\Attributes\ObservedBy; #[ObservedBy([InvoiceObserver::class])]class Invoice extends Model { // ...}
And that's it! Now, we have a dedicated Observer for our Invoice Model.
Code in Repository
Sadly, this observer way still has a flaw: we can accidentally make duplicate serial numbers. This is not good, as any accountant will give you a hard time.
Let's take a look at another option.
Our third option includes a solution to the duplicate serial number problem - jobs and a unique index! They run in the background and can be re-run if something goes wrong. Here's how it looks:
We need to ensure we have a unique index in our migration.
Migration
Schema::create('invoices', function (Blueprint $table) { $table->id(); $table->foreignIdFor(User::class)->constrained(); $table->date('due_date'); $table->integer('amount'); $table->string('serial')->nullable(); $table->string('serial_series'); $table->integer('serial_number')->nullable(); $table->timestamps(); $table->unique(['serial_series', 'serial_number']);});
Then, we can create our Job that will generate the serial number and re-run if the serial number is already taken:
app/Jobs/GenerateInvoiceNumber.php
// ...private Invoice $invoice; public function __construct(int $invoiceID){ $this->onQueue('invoiceNumbersQueue'); $this->invoice = Invoice::findOrFail($invoiceID);} public function handle(): void{ $this->invoice->serial_number = (Invoice::where('serial_series', $this->invoice->serial_series)->max('serial_number') ?? 0) + 1; $this->invoice->serial = $this->invoice->serial_series . '-' . $this->invoice->serial_number; $this->invoice->save();}// ...
And then we can dispatch this Job in our controller:
app/Http/Controllers/InvoiceController.php
use App\Jobs\GenerateInvoiceNumberJob; // ...public function store(StoreInvoiceRequest $request): RedirectResponse{ $invoice = Invoice::create($request->validated()); dispatch(new GenerateInvoiceNumberJob($invoice->id)); return redirect()->route('invoice.index');}// ...
As a last step, we should make sure that we are running the queue worker:
php artisan queue:work --queue=invoiceNumbersQueue
Running a specific queue is important as we want invoices to have their worker processing only invoices and not other jobs. This way, you would expect your jobs to run one after another rather than in parallel, which could cause duplicates.
Code in Repository
As our last option, we can combine two examples here - Observers and Jobs. This will solve our problem of invoice creation from anywhere and still use a dedicated queue for invoice numbers that we can re-run if something goes wrong. Here's how it looks:
The first thing we need to do is to create a Job:
app/Jobs/GenerateInvoiceNumber.php
// ...private Invoice $invoice; public function __construct(int $invoiceID){ $this->onQueue('invoiceNumbersQueue'); $this->invoice = Invoice::findOrFail($invoiceID);} public function handle(): void{ $this->invoice->serial_number = (Invoice::where('serial_series', $this->invoice->serial_series)->max('serial_number') ?? 0) + 1; $this->invoice->serial = $this->invoice->serial_series . '-' . $this->invoice->serial_number; $this->invoice->save();}// ...
Then it's all about the Observer (for this example, we will use Model Observer, but you can use a dedicated Observer if you want):
app/Models/Invoice.php
use App\Jobs\GenerateInvoiceNumberJob; // ... protected static function booted(){ parent::booted(); self::created(static function (Invoice $invoice) { dispatch(new GenerateInvoiceNumberJob($invoice->id)); });}// ...
Once you create an invoice, it will automatically schedule a job to generate the serial number. It does not matter where you will create it.
Code in Repository
It's very common to have leading zeros in your serial numbers. For example, you might want to have ABC-001
instead of ABC-1
. This is very easy to do like this:
app/Jobs/GenerateInvoiceNumberJob.php
public function handle(): void{ // ... $this->invoice->serial = $this->invoice->serial_series . '-' . $this->invoice->serial_number; $this->invoice->serial = $this->invoice->serial_series . '-' . str_pad($this->invoice->serial_number, 5, '0', STR_PAD_LEFT); // ...}
Or in short, here's how the str_pad()
works:
str_pad('1', 5, '0', STR_PAD_LEFT); // 00001
1
- The string we want to pad5
- The total length of the string we want to have0
- The character we want to use to pad the stringSTR_PAD_LEFT
- The side we want to pad the string on (left, right, both)That's it! Now you have a serial number with leading zeros.