madebyclowd/laravel-auto-sequence

Automatically generate sequential invoice/order numbers (like INV-2026-0001) for Laravel Eloquent models. Safe from duplicates even under heavy concurrent traffic.

Maintainers

Package info

github.com/madebyclowd/laravel-auto-sequence

pkg:composer/madebyclowd/laravel-auto-sequence

Transparency log

Statistics

Installs: 61

Dependents: 0

Suggesters: 0

Stars: 9

Open Issues: 0

v1.0.3 2026-07-15 02:15 UTC

This package is auto-updated.

Last update: 2026-07-15 02:16:14 UTC


README

Latest Version on Packagist Total Downloads License

Automatically generate custom, sequential numbers (like INV-2026-0001, ORD-999, etc.) for your Laravel models.

It is completely automatic and guarantees zero duplicate numbers and no accidental gaps, even when thousands of users are placing orders or creating invoices at the exact same millisecond.

Why not just use the normal auto-increment id?

Using a model's default database id (1, 2, 3...) is fine for relationships, but a bad idea to show to your users because:

  1. It leaks data: Your customers will know exactly how many invoices or orders you have processed.
  2. It is unpredictable: It might start at 1 in your local testing database but start at 14,000 on production.
  3. It is boring: You can't customize it to include the current year, a department code, or dynamic prefixes (e.g., you can't easily turn id = 5 into INV-2026-NY-0005).

This package solves this by generating a separate, beautifully formatted number column for you (like invoice_number), while ensuring two simultaneous database queries never try to claim the exact same number.

Features

  • Concurrency Safety: Utilizes pessimistic database locking (SELECT ... FOR UPDATE) or Redis-based distributed locks to prevent duplicate number generation.
  • Hi/Lo Pre-Allocation Caching: Optionally allocates sequence numbers in blocks (e.g., 50 at a time) and increments them in-memory to reduce database lock contention.
  • Composite Key Partitioning: Segregates counters using composite primary keys ['module', 'type_code', 'period', 'scope'].
  • Period Resets: Automatically resets counters on date boundaries (daily, weekly, monthly, yearly) or custom fiscal periods.
  • Dynamic Rules & Scopes: Resolves type prefixes from model relations (e.g. $invoice->branch->code) and scopes sequences by organizational units (e.g. $invoice->tenant_id).
  • Flexible Format Placeholders: Supports template placeholders like date tokens ({YYYY}, {MM}, {date:d-M-Y}), dynamic model attributes ({attribute:customer_code}), and random strings ({rand:8}).
  • Verification & Repair Commands: Includes Artisan commands to audit model records, detect counter drift, and repair sequence state.

Requirements

  • PHP 8.2+
  • Laravel 11, 12, or 13 (illuminate/support and illuminate/database)

Table of Contents

Quick Start

1. Install the package:

composer require madebyclowd/laravel-auto-sequence

2. Run the setup wizard — it asks a couple of yes/no questions and creates the database tables the package needs (you can just press Enter to accept the sensible defaults):

php artisan sequence:install

3. Add a number column to the model you want to sequence (a plain string column, nullable or with a default of null), then wire up the model:

use MadeByClowd\AutoSequence\Contracts\AutoSequence;
use MadeByClowd\AutoSequence\Traits\HasSequenceNumber;
use Illuminate\Database\Eloquent\Model;

class Invoice extends Model implements AutoSequence
{
    use HasSequenceNumber;

    public function getSequenceConfig(): array
    {
        return [
            'number' => [
                'module' => 'invoice',       // groups this sequence's counter
                'type_code' => 'INV',        // the prefix in the generated number
                'period' => 'monthly',       // counter resets every month
                'format_template' => '{type_code}-{YYYY}-{MM}-{seq:5}',
            ],
        ];
    }
}

4. Create a record like normal — the column fills itself in:

$invoice = Invoice::create([...]); // no need to set 'number' yourself
echo $invoice->number; // "INV-2026-06-00001"

That's it — every new Invoice now gets a safe, formatted, auto-incrementing number. Read on for the full set of options.

Basic Usage

1. Implement and Configure Your Model

Add the AutoSequence contract and use the HasSequenceNumber trait on your Eloquent model, then return one array entry per column you want to auto-fill from getSequenceConfig():

use MadeByClowd\AutoSequence\Contracts\AutoSequence;
use MadeByClowd\AutoSequence\Traits\HasSequenceNumber;
use Illuminate\Database\Eloquent\Model;

class Invoice extends Model implements AutoSequence
{
    use HasSequenceNumber;

    /**
     * Return the sequence configurations for the model.
     */
    public function getSequenceConfig(): array
    {
        return [
            'number' => [
                'module' => 'invoice',
                'type_code' => 'INV',
                'period' => 'monthly', // daily, weekly, monthly, yearly, never (or custom date formats)
                'format_template' => '{type_code}-{YYYY}-{MM}-{seq:5}', // Outputs: INV-2026-06-00001
                'pad_length' => 5,
            ]
        ];
    }
}

Only module and format_template are required — everything else falls back to a sensible default (see Configuration and Additional Configuration Options).

2. Manual Override Protection

If you manually assign a value to the sequenced attribute before saving, the package will respect it and skip generation:

$invoice = new Invoice();
$invoice->number = 'MANUAL-999';
$invoice->save(); // Bypasses sequence generator, keeping 'MANUAL-999'

Format Template Tokens

format_template is a plain string with placeholders. Mix and match any of these:

Token Produces Example
{type_code} The resolved type code/prefix INV
{seq:N} The incrementing counter, zero-padded to N digits {seq:5}00042
{YYYY} / {MM} / {DD} Current year / month / day 2026, 06, 16
{date:FORMAT} Any PHP date format {date:d-M-Y}16-Jun-2026
{period} The raw value returned by a custom period callable FY2026
{attribute:column} A live attribute from the model being saved (dot-notation for relations) {attribute:branch.company.code}
{rand:N} A random alphanumeric string of length N {rand:8}aZ3kQ9pL

Example combining several: '{type_code}-{YYYY}-{attribute:branch.code}-{seq:5}'.

Advanced Usage

1. Dynamic Type Code Resolution (type_relation)

Resolve the type code prefix from a model relationship dynamically (e.g., Bali branch DPS vs Jakarta branch JKT):

'number' => [
    'module' => 'invoice',
    'type_relation' => 'branch', // Calls $model->branch->code
    'default_type' => 'HQ',       // Fallback if relation is missing
    'period' => 'yearly',
    'format_template' => '{type_code}-{YYYY}-{seq:5}',
]

To use a relationship column other than code:

'type_relation' => [
    'relation' => 'category',
    'column' => 'id_code',
]

2. Multi-Tenant Scoping (scope)

Isolate sequence pools across tenants or branches by specifying a scoping model attribute:

'number' => [
    'module' => 'invoice',
    'type_code' => 'INV',
    'scope' => 'tenant_id', // Evaluates $model->tenant_id dynamically to separate counters
    'format_template' => '{type_code}-{YYYY}-{seq:5}',
]

3. Custom Reset Callables (Fiscal Calendar)

Provide a closure or a custom class string to partition sequences by custom dates (e.g., fiscal years starting in April):

'number' => [
    'module' => 'invoice',
    'type_code' => 'INV',
    'period' => function ($model) {
        $createdAt = $model->created_at ?? now();
        $year = $createdAt->month >= 4 ? $createdAt->year : $createdAt->year - 1;
        return "FY{$year}";
    },
    'format_template' => '{type_code}-{period}-{seq:5}',
]

4. Custom PHP Date Formatting ({date:FORMAT})

Format the template using standard PHP date character parameters:

'format_template' => '{type_code}-{date:d-M-Y}-{seq:5}' // INV-16-Jun-2026-00001

5. Multi-Column Sequences

Generate sequences for multiple attributes on a single model:

public function getSequenceConfig(): array
{
    return [
        'invoice_number' => [
            'module' => 'invoice',
            'type_code' => 'INV',
            'format_template' => '{type_code}-{YYYY}-{seq:5}',
        ],
        'internal_ref' => [
            'module' => 'internal',
            'type_code' => 'REF',
            'period' => 'never',
            'format_template' => '{type_code}-{seq:8}',
        ]
    ];
}

6. Closure Format Templates & Nested Relation Placeholders

Customize your formatting templates dynamically using closures or fetch nested relation attributes using dot-notation:

'number' => [
    'module' => 'invoice',
    'type_code' => 'INV',
    // 1. Fetching a nested relationship attribute:
    'format_template' => 'INV-{attribute:branch.company.code}-{seq:5}', 
    
    // 2. Closure-based template:
    'format_template' => function ($model) {
        return 'INV-' . ($model->is_priority ? 'URGENT' : 'NORMAL') . '-{seq:5}';
    }
]

7. Additional Configuration Options

The following additional configuration options are supported within the model sequence settings:

'number' => [
    'module' => 'invoice',
    'type_code' => 'INV',
    'format_template' => '{type_code}-{seq:5}',
    
    // Set a custom starting value (defaults to 1)
    'start_value' => 1000, 
    
    // Set a custom increment step size (defaults to 1)
    'step' => 2, 
    
    // Set a maximum limit. Throws a SequenceExhaustedException if exceeded
    'max_value' => 99999, 
    
    // Enforce sequence integrity. Throws a AutoSequenceException if a manual value is set before saving
    'allow_manual' => false, 
    
    // Enable D365 continuous sequence (recycles deleted numbers automatically)
    'continuous' => true,
    
    // Database connection override for this specific sequence
    'connection' => 'tenant_db_connection', 
]

8. Soft Deletes & Concurrency Best Practices

To ensure data integrity and prevent service disruption in high-volume production systems, the package implements the following behaviors:

  • Soft Deletes Protection: If your Eloquent model uses Laravel's SoftDeletes trait, deleting a record (soft delete) will not trigger sequence number recycling. The sequence number remains reserved on the soft-deleted record in the database. This allows restoring the model via $model->restore() without causing duplicate sequence collisions. The sequence number is only recycled when the model is permanently deleted via $model->forceDelete().
  • Preventing PHP-FPM Thread Exhaustion: Under the database locking driver, the package automatically sets session-level or local transaction-level lock wait timeouts on the database connection (using MySQL's innodb_lock_wait_timeout, Postgres's lock_timeout, SQL Server's LOCK_TIMEOUT, or SQLite's busy_timeout). If a transaction holds a sequence lock for too long, subsequent requests fail fast with a SequenceLockException after the configured timeout (default 5s) instead of blocking indefinitely, preventing worker exhaustion and gateway 502/504 errors.
  • Pre-Allocation & Transaction Modes: High-performance pre-allocation (pre_allocation.enabled => true) cannot be used together with the 'gapless' transaction mode. In 'gapless' mode, a rolled-back transaction would roll back the database counter but keep the pre-allocated block in memory/cache, resulting in duplicate sequence collisions. To use pre-allocation, set transaction_mode => 'gap_tolerant'.

Manual Generation (Facade)

Inject sequence values programmatically (e.g. in custom jobs, observers, or seeds):

use MadeByClowd\AutoSequence\Facades\Sequence;

// Fetch and increment next sequence value (with optional connection, start value, step, continuous, max value)
$number = Sequence::generate(
    'order', 
    'SO', 
    '202606', 
    '{type_code}-{YYYY}-{seq:5}', 
    5, 
    'tenant_1',
    null,       // $model (optional)
    null,       // $connection (optional override)
    1,          // $startValue (optional, default 1)
    1,          // $step (optional, default 1)
    false,      // $continuous (optional, default false)
    99999       // $maxValue (optional, default null)
);

// Recycle a sequence number manually (inserts it back into sequence_recycled table)
Sequence::recycle('order', 'SO', '202606', 'tenant_1', 105);

// Get current value without incrementing
$current = Sequence::getCurrent('order', 'SO', '202606', 'tenant_1');

// Reset or offset the counter
Sequence::reset('order', 'SO', '202606', 'tenant_1', 100);

Artisan Commands

List Sequences

Display a table of all active sequence counters in the database:

php artisan sequence:list
php artisan sequence:list --module=invoice

Reset Counters

Reset or set a specific sequence counter manually:

php artisan sequence:reset invoice INV --value=100

Verify and Repair

Scan actual model tables for sequence column values, identify any counter drift, and optionally align the database sequence counters to prevent key collisions:

php artisan sequence:verify "App\Models\Invoice" number --type=INV --module=invoice
# To automatically repair:
php artisan sequence:verify "App\Models\Invoice" number --type=INV --module=invoice --repair

Configuration (config/auto-sequence.php)

Publishing configuration gives you full architectural control:

return [
    'table' => 'sequences',
    'recycled_table' => 'sequence_recycled',
    'connection' => null,

    // Concurrency Locking Strategy
    'locking' => [
        'driver' => 'database',   // 'database' (Pessimistic lock), 'cache' (Atomic lock), or 'none'
        'cache_store' => null,    // cache connection name for atomic locks
        'timeout' => 5,           // seconds to block waiting for a lock (applies native DB lock timeout for MySQL, Postgres, SQL Server, SQLite)
        'retry_interval' => 100,  // milliseconds between retry attempts (applies if locking driver is cache)
    ],

    // Transaction Mode:
    // 'gapless': increments within model transaction (rolls back on failure; no gaps)
    // 'gap_tolerant': increments in isolated transaction (commits immediately; minimizes lock duration)
    // Note: 'gapless' is incompatible with pre-allocation.
    'transaction_mode' => 'gapless',

    // Hi/Lo Pre-Allocation Caching
    'pre_allocation' => [
        'enabled' => false,
        'block_size' => 50, // Grab 50 numbers at a time
        'store' => null,    // dedicated cache store name (e.g. 'redis') to prevent gaps from LRU eviction/flushes
    ],

    // Audit Tracking
    'audit' => [
        'enabled' => false, // Toggle created_by / updated_by tracking columns
        'user_model' => 'App\Models\User',
        'created_by_column' => 'created_by',
        'updated_by_column' => 'updated_by',
        'user_id_type' => 'bigInteger', // Options: 'bigInteger', 'uuid', 'ulid', 'string'
    ],
];

You don't have to publish this file at all — every key above has a working default, so skip it unless you actually need to change something.

Troubleshooting / FAQ

My column stays null after Model::create(). Make sure the model implements AutoSequence (not just uses the trait) and that you didn't manually set the column to a non-empty value before saving — see Manual Override Protection.

I get a lock/timeout error under load. You're likely on the 'cache' locking driver with a store that doesn't support atomic locks. file and array cache drivers don't support concurrent locks properly — switch to redis, memcached, or the 'database' driver (see locking.driver in Configuration).

I enabled pre_allocation and got a configuration exception. Pre-allocation requires transaction_mode => 'gap_tolerant' — it's incompatible with the default 'gapless' mode. See Pre-Allocation & Transaction Modes.

A restored (soft-deleted) record didn't get its number recycled — is that a bug? No — that's intentional. Numbers are only recycled on forceDelete(), never on a soft delete, so a later restore() can't collide with a number already handed out in the meantime.

How do I fix a counter that's out of sync with my actual data? Run php artisan sequence:verify {model} {column} --repair — see Verify and Repair.

Can I use this with UUID/ULID primary keys? Yes — the sequence counter lives in its own table (sequences), keyed by module/type_code/period/scope, completely independent of your model's primary key type.

License

The MIT License (MIT). Please see the LICENSE file for more information.