Looking to hire Laravel developers? Try LaraJobs

laravel-id-generator maintained by devaspid

Description
Concurrency-safe document and identifier number generation for Laravel.
Last update
2026/07/11 16:05 (dev-main)
License
Links
Downloads
0

Comments
comments powered by Disqus

Laravel ID Generator

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.

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.

Usage

Use the facade wherever a document number is needed:

use Devaspid\LaravelIdGenerator\Facades\IdGenerator;

$invoice->number = IdGenerator::generate('invoice');
// INV-202607-000001

Generate the other configured numbers in exactly the same way:

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

The Laravel alias IdGenerator is also auto-discovered:

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

Call the generator and save the document in the same application-level database transaction when the number must not be consumed after a failed business write:

DB::transaction(function () use ($request) {
    $invoice = new Invoice($request->validated());
    $invoice->number = IdGenerator::generate('invoice');
    $invoice->save();
});

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.