Search by

boneybone / laravel-serial-number

rizalfakhri

Concurrency-safe, pattern-based running/serial number generator for Laravel.

Package info

gitlab.com/nazarsyafiq89/bb-laravel-serial-number

Issues

pkg:composer/boneybone/laravel-serial-number

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

v1.0.0 2026-10-05 12:10 UTC

This package is not auto-updated.

Last update: 2026-11-11 10:46:24 UTC


README

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

OptionDescription
patternTokens: {SEQ}, {SEQ:n}, {YEAR}, {YY}, {MONTH}, {DAY}, plus any custom tokens.
prefix / suffixAlternative to pattern: becomes prefix{SEQ}suffix.
lengthDefault zero-padding for {SEQ} (config serial-number.length, default 6).
tokensCustom token values. Empty values throw, so you never get /IPR/2026/....
scopeAppended to the counter key. Empty values throw.
resetnever (default), yearly, monthly or daily.
dateThe date used for both the reset period and the date tokens (default now()).
uniquefn (string $candidate): bool. Rejected candidates are skipped.
max_attemptsMaximum number of skips before SerialNumberExhausted is thrown (default 50).
seedStarting 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.