yeeraf / laravel-document-numberer
A Laravel package for create document running number
Package info
github.com/yeeraf/laravel-document-numberer
pkg:composer/yeeraf/laravel-document-numberer
Requires
- php: ^8.0
Requires (Dev)
- orchestra/testbench: ^6.0
- phpunit/phpunit: ^9.5
README
Laravel Document Running Number Generator
A Laravel package for generating sequential document numbers with a customisable prefix, suffix, and padding. Ideal for invoices, receipts, quotations, and any document that requires a unique running number.
Examples:
- INV-000001
- INV2101-0001
- REC-0000001
- QT #####1
Concurrent requests are safe — the package uses database row-level locking and a unique constraint to guarantee no two requests ever receive the same number.
Installation
composer require yeeraf/laravel-document-numberer
Then run the migration to create the document_numbers table:
php artisan migrate
Basic Usage
$documentNumberer = new \Yeeraf\DocumentNumberer\DocumentNumberer; $docNumber = $documentNumberer->generate(); // "2107000001"
By default the number is prefixed with the last 2 digits of the current year and the 2-digit month — e.g. for 20/07/2021 the prefix is 2107 — followed by a 6-digit zero-padded sequence starting at 1: 2107000001.
Options
name() — separate counters per document type
Use this when two document types share the same format but must have independent sequences.
// Invoice counter $documentNumberer = new \Yeeraf\DocumentNumberer\DocumentNumberer; $docNumber = $documentNumberer->name("invoice")->generate(); // "2107000001" // Receipt counter (independent from invoice) $documentNumberer = new \Yeeraf\DocumentNumberer\DocumentNumberer; $docNumber = $documentNumberer->name("receipt")->generate(); // "2107000001"
prefix() — custom prefix
$documentNumberer = new \Yeeraf\DocumentNumberer\DocumentNumberer; $docNumber = $documentNumberer->prefix("INV-")->generate(); // "INV-000001"
suffix() — custom suffix
$documentNumberer = new \Yeeraf\DocumentNumberer\DocumentNumberer; $docNumber = $documentNumberer->suffix("-X")->generate(); // "2107000001-X"
padLength(int) — number of digits in the sequence
$documentNumberer = new \Yeeraf\DocumentNumberer\DocumentNumberer; $docNumber = $documentNumberer->padLength(3)->generate(); // "2107001"
padString() — character used for padding
$documentNumberer = new \Yeeraf\DocumentNumberer\DocumentNumberer; $docNumber = $documentNumberer->padString("#")->generate(); // "2107#####1"
padType() — padding direction (left or right)
Default is left (digits appear on the right).
$documentNumberer = new \Yeeraf\DocumentNumberer\DocumentNumberer; $docNumber = $documentNumberer->padType('right')->generate(); // "2107100000"
teamId() — separate counters per team
Use in multi-tenant applications where each team needs its own independent sequence.
// Team 1 $documentNumberer = new \Yeeraf\DocumentNumberer\DocumentNumberer; $docNumber = $documentNumberer->teamId(1)->generate(); // "2107000001" // Team 2 (independent counter) $documentNumberer = new \Yeeraf\DocumentNumberer\DocumentNumberer; $docNumber = $documentNumberer->teamId(2)->generate(); // "2107000001"
autoExtend() — prevent the sequence from exceeding padLength
Default is true (the sequence grows beyond padLength automatically).
Set to false to throw an exception once the sequence would exceed the configured length:
$documentNumberer = new \Yeeraf\DocumentNumberer\DocumentNumberer; $docNumber = $documentNumberer ->padLength(3) ->autoExtend(false) ->generate(); // throws Exception("running number length go over pad length") once the counter exceeds 999
Combined Example
$documentNumberer = new \Yeeraf\DocumentNumberer\DocumentNumberer; $docNumber = $documentNumberer ->name("invoice") ->prefix("INV-") ->suffix("-X") ->padLength(4) ->padString("_") ->padType("left") ->teamId(1) ->generate(); // "INV-___1-X"
Concurrency
The package uses SELECT ... FOR UPDATE (row-level locking) together with a unique composite index on the document_numbers table to prevent race conditions when multiple requests call generate() simultaneously.
- Unique index — prevents duplicate rows from being created when two requests initialise the same counter at the same time.
- Deadlock retry — if MySQL raises a deadlock error, the transaction is automatically retried up to 3 times.
Note:
SELECT ... FOR UPDATEis supported by MySQL and PostgreSQL only. SQLite (used in the test suite) silently ignores this clause. Run the test suite against MySQL to validate concurrency behaviour.
License
The MIT License (MIT)