devaspid/laravel-id-generator

Concurrency-safe document and identifier number generation for Laravel.

Maintainers

Package info

github.com/dev-asp-id/laravel-id-generator

pkg:composer/devaspid/laravel-id-generator

Transparency log

Statistics

Installs: 9

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

1.0.1 2026-07-14 04:35 UTC

This package is auto-updated.

Last update: 2026-07-14 04:40:00 UTC


README

Generate business document numbers such as invoices, transactions, procurement records, and purchase orders without scanning their transaction tables.

The package keeps only mutable counter state in id_sequences; formatting and reset behaviour remain version-controlled in config/id-generator.php. Counters are incremented inside a database transaction with a row lock, making the generator safe for concurrent requests.

Try Demo

Click

Features

  • Laravel 12 and 13 support, PHP 8.3+
  • One compact state row per configured generator
  • Atomic, concurrency-safe sequence increments
  • Global, yearly, monthly, and daily scopes
  • Prefix, suffix, left padding, and date placeholders
  • id-generator:sync command to create missing sequence rows
  • No scheduler and no lookup of invoice/transaction tables

Requirements

  • PHP 8.3 or newer
  • Laravel 12 or Laravel 13
  • A database connection that supports transactions and row locks in production

Installation

Install the package:

composer require devaspid/laravel-id-generator

Publish its configuration and migration, then migrate:

php artisan vendor:publish --tag=id-generator-config
php artisan vendor:publish --tag=id-generator-migrations
php artisan migrate

Define your generators in config/id-generator.php, then create their state rows:

php artisan id-generator:sync

Run id-generator:sync on every environment after adding a generator. It creates missing rows only; it never resets or overwrites an existing counter.

Configuration

<?php

return [
    'invoice' => [
        'prefix' => 'INV-{Y}{m}-',
        'padding' => 6,
        'scope' => 'monthly',
    ],

    'transaction' => [
        'prefix' => 'TRX-',
        'padding' => 8,
        'scope' => 'never',
    ],

    'procurement' => [
        'prefix' => 'PRC-{Y}-',
        'suffix' => '-ID',
        'padding' => 5,
        'scope' => 'yearly',
    ],

    'purchase-order' => [
        'prefix' => 'PO-',
        'padding' => 5,
        'scope' => 'daily',
    ],
];
Option Required Default Description
prefix No '' Text before the sequence number.
suffix No '' Text after the sequence number.
padding No 0 Minimum number width, padded on the left with zeroes.
scope No never Counter lifecycle: never, yearly, monthly, or daily.

Supported placeholders in prefix and suffix are {Y}, {m}, {d}, and {His}. For example, INV-{Y}{m}- becomes INV-202607- in July 2026.

Placeholder Output Description
{Y} 2026 Four digit year
{y} 26 Two digit year
{F} July Full month name
{M} Jul Short month name
{m} 07 Month with leading zero
{n} 7 Month without leading zero
{l} Monday Full day name
{D} Mon Short day name
{d} 09 Day with leading zero
{j} 9 Day without leading zero
{H} 14 Hour
{i} 35 Minute
{s} 22 Second
{His} 143522 HHMMSS

Usage

Configure your generator in config/id-generator.php.

return [

    'invoice' => [
        'prefix' => 'INV-{Y}{m}-',
        'padding' => 6,
        'scope' => 'monthly',
    ],

];

Generate the next document number.

use Devaspid\LaravelIdGenerator\Facades\IdGenerator;

$invoice->number = IdGenerator::generate('invoice');

Output:

INV-202607-000001

Using with Eloquent Model Events

A common approach is generating the document number automatically when a model is created.

use Devaspid\LaravelIdGenerator\Facades\IdGenerator;

class Invoice extends Model
{
    protected static function booted(): void
    {
        static::creating(function (Invoice $invoice) {

            if (blank($invoice->number)) {
                $invoice->number = IdGenerator::generate('invoice');
            }

        });
    }
}

Generating Different Document Types

Every configured generator is referenced by its configuration key.

$invoice->number = IdGenerator::generate('invoice');

$transaction->reference = IdGenerator::generate('transaction');

$procurement->number = IdGenerator::generate('procurement');

$purchaseOrder->number = IdGenerator::generate('purchase-order');

Using Inside a Database Transaction

When a document number should only exist if the business transaction succeeds, generate it inside the same database transaction.

use Illuminate\Support\Facades\DB;
use Devaspid\LaravelIdGenerator\Facades\IdGenerator;

DB::transaction(function () use ($request) {

    $invoice = new Invoice($request->validated());

    $invoice->number = IdGenerator::generate('invoice');

    $invoice->save();

});

Manual Generation

You can generate a document number anywhere in your application.

use Devaspid\LaravelIdGenerator\Facades\IdGenerator;

$number = IdGenerator::generate('invoice');

Using the Auto Discovered Alias

Laravel automatically registers the facade alias, allowing the shorter syntax.

$number = IdGenerator::generate('invoice');

Example Configuration

return [

    'invoice' => [
        'prefix' => 'INV-{Y}{m}-',
        'padding' => 6,
        'scope' => 'monthly',
    ],

    'transaction' => [
        'prefix' => 'TRX-',
        'padding' => 8,
        'scope' => 'never',
    ],

    'procurement' => [
        'prefix' => 'PRC-{Y}-',
        'suffix' => '-ID',
        'padding' => 5,
        'scope' => 'yearly',
    ],

    'purchase-order' => [
        'prefix' => 'PO-',
        'padding' => 5,
        'scope' => 'daily',
    ],

];

Generated values:

Invoice:
INV-202607-000001

Transaction:
TRX-00000001

Procurement:
PRC-2026-00001-ID

Purchase Order:
PO-00001

Scope and reset behaviour

Reset behaviour is driven by a scope, not a scheduler and not the date of the last invoice. The database stores current and last_scope in one row for each generator.

For a monthly invoice generator, a July state might be current = 1523 and last_scope = '2026-07'. The first generation in August detects the new scope while holding the row lock, changes the state to current = 0 and last_scope = '2026-08', then increments to 1. No business table is scanned, and no extra sequence rows are created for idle periods.

never resolves to a global scope and never resets. yearly, monthly, and daily resolve respectively to YYYY, YYYY-MM, and YYYY-MM-DD.

Concurrency

generate() starts a database transaction, locks the single row for the requested generator (SELECT ... FOR UPDATE), evaluates its scope, increments the counter, and saves it before committing. Concurrent requests for the same generator wait briefly for that row instead of receiving the same number. Different generators use different rows and can proceed independently.

Use a unique database index on your document-number column as a final application-level integrity safeguard.

API reference

string IdGenerator::generate(string $name)

Throws InvalidGeneratorConfiguration if the generator is absent or invalid, and SequenceNotFound if id-generator:sync has not created its state row.

Upgrade guide

Before upgrading this package, commit your composer.lock, application config, and database backup. Run Composer's update, review the package release notes, then run:

php artisan id-generator:sync

The command is safe to rerun and preserves current and last_scope. Do not truncate id_sequences in a live system unless restarting every document sequence is intentional.

FAQ

Why not scan the invoice table for its highest number?

Scanning grows more expensive as the table grows and has a race condition: two concurrent requests can read the same maximum. This package locks one small state row instead.

Does monthly reset require a scheduler?

No. The first generation in a new period resets atomically. Months with no documents create no data and require no work.

Will a failed transaction leave a number gap?

When generate() runs inside the same outer transaction as the document insert, a rollback rolls back both changes. Database engines and other application workflows may still create gaps in some circumstances; document sequences should generally be unique and ordered, not assumed gapless.

Can I add custom scopes?

The ScopeResolverRegistry is intentionally separated from the manager so a future release can expose custom resolver registration without changing storage or generation semantics.

License

This package is open-sourced software licensed under the MIT license.

Support the Project

If you find this project useful, consider giving it a ⭐ on GitHub.

Maintained with ❤️ by devASPId.

🌐 https://asp.web.id

🤝 https://asp.web.id/volunteer