Looking to hire Laravel developers? Try LaraJobs

laravel-serial-number maintained by boneybone

Description
Concurrency-safe, pattern-based running/serial number generator for Laravel.
Last update
2026/11/11 11:17 (dev-main)
License
Links
Downloads
0

Comments
comments powered by Disqus

boneybone/laravel-serial-number

A running/serial number generator for Laravel that is safe under concurrent requests and driven by patterns, e.g. ACME/IPR/2026/00042.

Install

composer require boneybone/laravel-serial-number:^1.0
php artisan vendor:publish --tag=serial-number-migrations
php artisan migrate

Optional: php artisan vendor:publish --tag=serial-number-config.

Releases are tagged (v1.0.0, …), so composer update only brings releases, never work in progress on main.

Apps that installed it from the private GitLab repository can remove the laravel-serial-number entry from repositories in their composer.json; the next composer update gets it from Packagist.

Usage

Facade

use Boneybone\SerialNumber\Facades\SerialNumber;

SerialNumber::next('invoice', ['prefix' => 'INV-', 'length' => 5]);          // INV-00001
SerialNumber::next('pr', [
    'pattern' => '{COMPANY}/IPR/{YEAR}/{SEQ:5}',
    'tokens'  => ['company' => 'ACME'],
    'scope'   => $companyId,   // separate counter per company
    'reset'   => 'yearly',     // never | yearly | monthly | daily
]);                                                                         // ACME/IPR/2026/00001
SerialNumber::preview('invoice', ['prefix' => 'INV-', 'length' => 5]);     // INV-00002 (not consumed)

Model trait

use Boneybone\SerialNumber\Concerns\HasSerialNumber;

class PurchaseOrder extends Model
{
    use HasSerialNumber;

    public function serialNumberColumn(): string
    {
        return 'po_number';
    }

    public function serialNumberOptions(): array
    {
        return [
            'pattern' => '{COMPANY}/PO/{YEAR}/{SEQ:5}',
            'tokens' => ['company' => $this->company?->name],
            'scope' => $this->company_id,
            'reset' => 'yearly',
        ];
    }

    // Optional: assign automatically on create
    public function shouldAutoAssignSerialNumber(): bool
    {
        return true;
    }
}

$po->assignSerialNumber()->save();   // or $po->generateSerialNumber()

The trait checks the model's column (all global scopes ignored) and skips numbers that already exist.

Options

Option Description
pattern Tokens: {SEQ}, {SEQ:n}, {YEAR}, {YY}, {MONTH}, {DAY}, plus any custom tokens.
prefix / suffix Alternative to pattern: becomes prefix{SEQ}suffix.
length Default zero-padding for {SEQ} (config serial-number.length, default 6).
tokens Custom token values. Empty values throw, so you never get /IPR/2026/....
scope Appended to the counter key. Empty values throw.
reset never (default), yearly, monthly or daily.
date The date used for both the reset period and the date tokens (default now()).
unique fn (string $candidate): bool. Rejected candidates are skipped.
max_attempts Maximum number of skips before SerialNumberExhausted is thrown (default 50).
seed Starting last_number (int or callable) when a counter is first created, e.g. the max existing number.

The counter key is {key}[:{scope}][:{period}], e.g. purchase_order:3:2026.

Guarantees and notes

  • A new counter row is created atomically (insertOrIgnore) and then locked with SELECT ... FOR UPDATE, so concurrent requests never share a number. SQLite ignores row locks.
  • Gapless numbering: call it inside your own DB::transaction() that also saves the record. If the save fails, the counter increment rolls back too, and the row lock is held until your transaction commits.
  • Add a unique index on the business column (e.g. po_number) as a final safety net at the database level.
  • The SerialNumberGenerated event is dispatched after the transaction commits.

Developing the package

To try changes in an app before pushing them, point the app at your local checkout:

composer config repositories.laravel-serial-number '{"type": "path", "url": "../laravel-serial-number", "options": {"symlink": true}}'
composer require "boneybone/laravel-serial-number:*@dev"

Remove that repositories entry and switch back to ^1.0 before committing the app.

Testing

composer test

Releasing

  1. Merge into main.
  2. Tag it and push the tag: git tag -a v1.1.0 -m "v1.1.0" && git push origin v1.1.0.
  3. The GitLab push mirror copies main and the tag to the public repository Packagist reads from. Apps get it with composer update boneybone/laravel-serial-number.