PHPackages                             vimatech/laravel-document-numbering - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. vimatech/laravel-document-numbering

ActiveLibrary[Payment Processing](/categories/payments)

vimatech/laravel-document-numbering
===================================

Sequential, gap-free, concurrency-safe document numbering for Laravel (invoices, quotes, credit notes).

v1.0.0(today)00MITPHPPHP ^8.3CI passing

Since Jun 26Pushed todayCompare

[ Source](https://github.com/vimatech-io/laravel-document-numbering)[ Packagist](https://packagist.org/packages/vimatech/laravel-document-numbering)[ Docs](https://github.com/vimatech-io/laravel-document-numbering)[ RSS](/packages/vimatech-laravel-document-numbering/feed)WikiDiscussions main Synced today

READMEChangelog (1)Dependencies (9)Versions (2)Used By (0)

Laravel Document Numbering
==========================

[](#laravel-document-numbering)

[![CI](https://github.com/vimatech-io/laravel-document-numbering/actions/workflows/ci.yml/badge.svg)](https://github.com/vimatech-io/laravel-document-numbering/actions/workflows/ci.yml)[![Latest Version on Packagist](https://camo.githubusercontent.com/24ebba4ed0a5ca44c00592b90e40ce10e796272706eb911d3912dde72fe9db2e/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f76696d61746563682f6c61726176656c2d646f63756d656e742d6e756d626572696e672e737667)](https://packagist.org/packages/vimatech/laravel-document-numbering)[![Total Downloads](https://camo.githubusercontent.com/e3c023f6fbfecf568257eec8ac0b2269e67455e765490e576dfb668418235687/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f76696d61746563682f6c61726176656c2d646f63756d656e742d6e756d626572696e672e737667)](https://packagist.org/packages/vimatech/laravel-document-numbering)[![License](https://camo.githubusercontent.com/8079ee3c3f56c8347e06463d4526d099db2d1eb632a750e1c652a8a53a823ab4/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f76696d61746563682f6c61726176656c2d646f63756d656e742d6e756d626572696e672e737667)](https://packagist.org/packages/vimatech/laravel-document-numbering)

**Sequential, gap-free, concurrency-safe document numbers for Laravel.**

Allocate legally-compliant numbers for invoices, quotes and credit notes — under concurrent requests, two callers can never take the same number or leave a hole in the sequence.

Why Laravel Document Numbering?
-------------------------------

[](#why-laravel-document-numbering)

Gap-free numbering is a **legal requirement** for invoices in most jurisdictions: the sequence may not skip values. Getting that right under load is harder than it looks. Most Laravel apps eventually need to answer:

- How do I guarantee invoice numbers never skip a value?
- How do two concurrent requests avoid taking the same number?
- Can each company/tenant keep its own independent sequence?
- How do I reset the counter every year or month?
- What happens to the number if the surrounding transaction rolls back?

Laravel Document Numbering provides a small, database-backed layer for exactly that — it leans on database transactions and row locks rather than hoping races never happen.

Feature Matrix
--------------

[](#feature-matrix)

FeatureSupportedConfigurable patterns (`INV-{YYYY}-{seq:5}`)✅Per-scope counters (company / tenant / branch)✅Period resets (`yearly`, `monthly`, `never`)✅Gap-free mode (transaction-bound)✅Fast-sequential mode✅Concurrency-safe (row locks)✅Eloquent trait, event &amp; facade✅Database-portable (MySQL, PostgreSQL, SQLite)✅Octane / FrankenPHP safe✅UI❌Requirements
------------

[](#requirements)

- PHP 8.3+
- Laravel 11, 12 or 13

Installation
------------

[](#installation)

```
composer require vimatech/laravel-document-numbering
```

Publish and run the migration:

```
php artisan vendor:publish --tag=numbering-migrations
php artisan migrate
```

Publish the config (optional):

```
php artisan vendor:publish --tag=numbering-config
```

Configuration
-------------

[](#configuration)

`config/numbering.php` defines a counter strategy per document type:

```
use Vimatech\DocumentNumbering\Enums\ResetPolicy;

return [
    'connection' => env('DOCUMENT_NUMBERING_CONNECTION'),
    'table' => 'document_number_sequences',

    // Times the allocation transaction is retried on a deadlock / locked error.
    'lock_attempts' => 5,

    'types' => [
        'invoice' => [
            'pattern' => 'INV-{YYYY}-{seq:5}',
            'reset' => ResetPolicy::Yearly,
            'gap_free' => true,
        ],
        'quote' => [
            'pattern' => 'QUO-{YYYY}-{seq:5}',
            'reset' => ResetPolicy::Yearly,
            'gap_free' => false,
        ],
        'credit_note' => [
            'pattern' => 'CN-{YY}{MM}-{seq:4}',
            'reset' => ResetPolicy::Monthly,
            'gap_free' => true,
        ],
    ],
];
```

### Pattern tokens

[](#pattern-tokens)

TokenMeaningExample`{YYYY}`4-digit year`2026``{YY}`2-digit year`26``{MM}`2-digit month`06``{seq:n}`sequence value, zero-padded to `n``00042`Everything outside a token is literal text. A sequence longer than its padding is **not** truncated (`{seq:3}` with value `12345` → `12345`).

### Reset policies

[](#reset-policies)

The reset policy decides when the counter restarts at `1`, by computing a *period key*. Two allocations share a counter only when their `(scope, type, period_key)` match.

PolicyPeriod key (for 2026-06)Restarts`ResetPolicy::Never``all`never`ResetPolicy::Yearly``2026`each year`ResetPolicy::Monthly``2026-06`each month> Tip: include the matching date token in the pattern (`{YYYY}` for yearly, `{YY}{MM}` for monthly) so reused sequence values stay unique across periods.

### Scopes

[](#scopes)

A **scope** is an arbitrary string that isolates counters — typically a company, tenant or branch id. `acme` and `globex` each get their own `INV-2026-00001`.

When you configure a scope column on a model (`$documentNumberScopeColumn`), that attribute **must be set before saving**. A missing value throws a `LogicException` rather than silently falling back to the global scope, which would otherwise mix one tenant's numbers into another's sequence.

Usage
-----

[](#usage)

### Via the facade

[](#via-the-facade)

```
use Vimatech\DocumentNumbering\Facades\Numbering;

$number = Numbering::for($companyId, 'invoice')->next(); // "INV-2026-00001"

// Preview the next value without consuming it (advisory under concurrency):
$preview = Numbering::for($companyId, 'invoice')->peek();
```

### Via the Eloquent trait

[](#via-the-eloquent-trait)

Add `HasDocumentNumber` to a model and tell it which type and scope to use. The number is assigned automatically on `creating`.

```
use Illuminate\Database\Eloquent\Model;
use Vimatech\DocumentNumbering\Concerns\HasDocumentNumber;

class Invoice extends Model
{
    use HasDocumentNumber;

    protected string $documentNumberType = 'invoice';
    protected string $documentNumberColumn = 'number';        // default: 'number'
    protected string $documentNumberScopeColumn = 'company_id'; // optional
}

$invoice = Invoice::create(['company_id' => 42]);
$invoice->number; // "INV-2026-00001"
```

You can replace the property hooks with method overrides for full control:

```
public function documentNumberType(): string { return 'invoice'; }
public function documentNumberScope(): string { return (string) $this->team_id; }
public function documentNumberColumn(): string { return 'reference'; }
```

For **gap-free** types the trait wraps the first `save()` in a database transaction, so the number allocation and the row `INSERT` commit together — if the insert fails, the number is released back into the sequence.

Complete Example
----------------

[](#complete-example)

```
use Illuminate\Database\Eloquent\Model;
use Vimatech\DocumentNumbering\Concerns\HasDocumentNumber;
use Vimatech\DocumentNumbering\Enums\ResetPolicy;

// 1. Configure the type (config/numbering.php)
'types' => [
    'invoice' => [
        'pattern'  => 'INV-{YYYY}-{seq:5}',
        'reset'    => ResetPolicy::Yearly,
        'gap_free' => true,
    ],
],

// 2. Add the trait to your model
class Invoice extends Model
{
    use HasDocumentNumber;

    protected string $documentNumberType = 'invoice';
    protected string $documentNumberScopeColumn = 'company_id';
}

// 3. Use it
$invoice = Invoice::create(['company_id' => 42]); // company 42
$invoice->number;                                 // "INV-2026-00001"

$next = Invoice::create(['company_id' => 42]);
$next->number;                                    // "INV-2026-00002"

$other = Invoice::create(['company_id' => 99]);   // different scope
$other->number;                                   // "INV-2026-00001"
```

Concurrency &amp; gap-free guarantees
-------------------------------------

[](#concurrency--gap-free-guarantees)

Each `(scope, type, period_key)` owns one row in `document_number_sequences`. Allocation runs inside a transaction and takes a `lockForUpdate()` lock on that row, so concurrent callers **serialise on the row** rather than racing the counter. The first allocation for a new period inserts the row at `0`; the unique index on `(scope, type, period_key)` makes a lost insert race harmless.

### `gap_free: true` (legally safe, default)

[](#gap_free-true-legally-safe-default)

The number is allocated **inside the caller's transaction**. The row lock is held until that transaction commits, so:

- no other allocation for the same sequence can proceed until you commit, and
- if you roll back, the increment is undone and the number is reused.

This is what invoices need. The cost is contention: the lock is held for the lifetime of the surrounding transaction, so keep those transactions short.

When using the facade directly, wrap your write and the allocation together:

```
DB::transaction(function () use ($companyId, $payload) {
    $number = Numbering::for($companyId, 'invoice')->next();

    Invoice::create([...$payload, 'number' => $number]);
});
```

The `HasDocumentNumber` trait does this wrapping for you.

### `gap_free: false` (fast sequential)

[](#gap_free-false-fast-sequential)

The number is committed as soon as it is allocated and is **not** returned on a later rollback, so gaps are possible. Choose this only for sequences where the law does not require gap-freeness (e.g. internal quote drafts) and throughput matters more than a perfectly dense sequence.

### FrankenPHP / Laravel Octane (worker mode)

[](#frankenphp--laravel-octane-worker-mode)

The package is safe under long-lived worker runtimes (FrankenPHP worker mode, Octane with Swoole/RoadRunner), where the application is booted once and reused across many requests. Specifically:

- **The `NumberingManager` is a stateless singleton.** It holds no per-request data: the document-type configuration is read from the config repository on demand, and the database connection is resolved *per call* through the connection resolver. It never caches a `Connection`/PDO handle, so a reconnect between requests (which Octane performs) is picked up automatically.
- **No accumulating static state.** Nothing grows in memory across requests.
- **The `HasDocumentNumber` trait registers its `creating` hook once** per worker, via Eloquent's standard trait-boot mechanism — there is no per-request re-registration or listener leak.
- **Correctness is unchanged.** Worker mode runs several workers concurrently, exactly like PHP-FPM. Gap-free safety still comes from the database row lock, which serialises allocation across all workers and processes.

No special configuration is required. If you register an event listener for `NumberAllocated`, follow the usual Octane guidance and avoid capturing request-scoped state in long-lived listeners.

### Database notes

[](#database-notes)

- **MySQL / PostgreSQL**: `lockForUpdate()` issues `SELECT ... FOR UPDATE`; row-level locks are held until commit. Recommended for production.
- **SQLite**: write transactions lock the database file, which serialises writers and provides the same guarantees with coarser granularity. Great for tests and small single-writer apps.

If the database aborts a statement while waiting for the lock (lock-wait timeout or deadlock), a `Vimatech\DocumentNumbering\Exceptions\SequenceLocked`exception is thrown and the transaction is rolled back — no number is consumed.

Events
------

[](#events)

`Vimatech\DocumentNumbering\Events\NumberAllocated` is dispatched after each allocation:

```
final class NumberAllocated
{
    public string $scope;
    public string $type;
    public string $periodKey;
    public int $sequence;
    public string $number;
}
```

For gap-free types this fires inside the caller's transaction, so a rollback also discards anything a listener did transactionally.

Testing
-------

[](#testing)

```
composer test       # Pest
composer format     # Pint
composer analyse    # PHPStan (level max)
```

The suite includes a concurrency test that forks multiple worker processes against a shared SQLite database and asserts that the allocated numbers contain **no duplicates and no gaps**.

Design Principles
-----------------

[](#design-principles)

- **Correctness first** — gap-free safety comes from database row locks, not optimistic hoping.
- **Backend-only, UI agnostic** — no controllers, no views, no opinions on your frontend.
- **No domain assumptions** — works for invoices, quotes, credit notes or any document type you define.
- **Laravel-native API** — a trait, an event, a facade and a config file.
- **Database-portable** — the same guarantees on MySQL, PostgreSQL and SQLite.
- **Worker-safe** — stateless singleton, no accumulating static state, ready for Octane and FrankenPHP.

Possible Future Extensions
--------------------------

[](#possible-future-extensions)

- Per-type custom formatters (callables)
- Daily reset policy
- Numbering audit log
- Filament integration

Future extensions may be released as separate packages to keep the core small and focused.

Contributing
------------

[](#contributing)

Contributions are welcome.

Please ensure:

- Tests pass (`composer test`)
- PHPStan passes (`composer analyse`)
- Code style is formatted with Pint (`composer format`)

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

Please review our [Security Policy](SECURITY.md) for reporting vulnerabilities.

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

Credits
-------

[](#credits)

Built and maintained by [Vimatech](https://vimatech.io). Created by [Adel Zemzemi](https://github.com/adelzemzemi).

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/3283664f06a0db1bfdbf282b2691363365c2f73569bcd99d63f5aaa52900ff55?d=identicon)[adelzemzemi](/maintainers/adelzemzemi)

---

Top Contributors

[![adelzemzemi](https://avatars.githubusercontent.com/u/272534830?v=4)](https://github.com/adelzemzemi "adelzemzemi (18 commits)")

---

Tags

laravelinvoicedocumentsequencenumberingvimatechgap-free

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/vimatech-laravel-document-numbering/health.svg)

```
[![Health](https://phpackages.com/badges/vimatech-laravel-document-numbering/health.svg)](https://phpackages.com/packages/vimatech-laravel-document-numbering)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.1M337](/packages/psalm-plugin-laravel)[larastan/larastan

Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel

6.4k51.0M7.6k](/packages/larastan-larastan)[laravel/horizon

Dashboard and code-driven configuration for Laravel queues.

4.1k91.3M280](/packages/laravel-horizon)[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.5k28.4M137](/packages/laravel-cashier)[laravel/ai

The official AI SDK for Laravel.

9782.1M162](/packages/laravel-ai)[spatie/laravel-health

Monitor the health of a Laravel application

87411.3M153](/packages/spatie-laravel-health)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
