Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Auto Sequence Laravel Package

madebyclowd/laravel-auto-sequence

Laravel Auto Sequence generates unique, sequential numbers for Eloquent models, ideal for invoice, order, and ticket IDs. Configure prefixes, suffixes, padding, and reset periods with simple model traits.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a narrow, single-purpose package: it assigns gap-tolerant or gapless sequential document numbers (INV-2026-06-00001) to Eloquent models with concurrency safety. It is not an ID generator, not a UUID alternative, and not a general "numbering everything" framework. Treat it as a replacement for the hand-rolled SELECT MAX(number) + 1 that every Laravel dev has written at least once and later regretted under load.

Day-one setup is three commands, and you only need the first.

composer require madebyclowd/laravel-auto-sequence
php artisan sequence:install   # publishes config + migrations, then migrates

If you're in a monorepo or a CI pipeline where publishing is done by a deploy step, split it:

php artisan vendor:publish --tag=auto-sequence-config
php artisan vendor:publish --tag=auto-sequence-migrations

Where to look first. Open config/auto-sequence.php immediately after install — you need to make three decisions before writing any model code:

  1. locking.driver — database (advisory/session locks, fine for single-DB apps) vs cache. If you pick cache, stop and verify your cache store supports locks (redis, memcached, database). file and array do not, and the failure mode is silent duplicate numbers under concurrency, not an exception.
  2. transaction_mode — gap_tolerant (default, fast) vs gapless (slower, holds the sequence row longer). Pick gap_tolerant unless an auditor is literally standing behind you.
  3. pre_allocation.enabled — leave it off until you have a measured throughput problem. It complicates everything (see Gotchas).

Your first use case should be the boring one: invoices you already have, where the number is currently generated in an observer or a booted() hook. Migrate one model.

// app/Models/Invoice.php
use Illuminate\Database\Eloquent\Model;
use MadeByClowd\AutoSequence\Contracts\Sequenceable;
use MadeByClowd\AutoSequence\Traits\HasSequenceNumber;

class Invoice extends Model implements Sequenceable
{
    use HasSequenceNumber;

    public function getSequenceConfig(): array
    {
        return [
            'number' => [
                'module'          => 'invoice',
                'type_code'       => 'INV',
                'period'          => 'monthly',
                'format_template' => '{type_code}-{YYYY}-{MM}-{seq:5}',
                'pad_length'      => 5,
            ],
        ];
    }
}

Column must be number (the array key is the column name), nullable, and unique-indexed. Both the contract and the trait are required — the trait without the contract is a no-op, and that's the #1 "it silently does nothing" report.

Immediately after the first save, run:

php artisan sequence:list --module=invoice

If your row appears there with the right period bucket and counter, the wiring is correct. If not, fix that before touching anything else.


Implementation Patterns

The one-row-per-config default is right 90% of the time

Most developers start by trying to be clever — a service class, a custom generator, a job. Don't. getSequenceConfig() returning a static array plus the trait gets you: concurrency-safe allocation, retry on collision, manual-override protection, and a DB-backed counter you can audit. That's the whole value proposition.

Scope first if you're multi-tenant, or you will rewrite later

Retrofitting tenant isolation onto an existing sequence table means backfilling counters and reconciling duplicates. If there is any chance a second tenant, branch, or legal entity appears, add scope now:

'scope' => 'tenant_id',

This resolves $model->tenant_id at save time and partitions the counter pool. The composite primary key is ['module', 'type_code', 'period', 'scope'] — that's the mental model to hold: four columns, one counter row each.

Prefer type_relation over hardcoded prefixes

The pattern that pays for itself fastest is deriving the prefix from a relationship rather than hardcoding it, because business stakeholders change document prefixes more often than they change anything else in the schema:

'type_relation' => ['relation' => 'branch', 'column' => 'code'],
'default_type'  => 'HQ',

default_type is not optional in spirit — if the relation is null (e.g. a draft created before the branch is assigned) and you haven't set it, you'll get an exception at the worst possible moment. Always set it.

Periods: use the callable only when finance actually demands it

The 'period' => 'monthly' shorthand covers most cases. Custom callables — fiscal years starting April 1st, 4-4-5 retail calendars — are supported and are genuinely useful, but they are also the source of the nastiest drift bugs, because the period string is the partition key. If you change the callable's logic mid-year, existing counters are orphaned and numbering restarts. Version your fiscal logic, and don't touch it inside an open period.

'period' => function ($model) {
    $d = $model->created_at ?? now();
    $fy = $d->month >= 4 ? $d->year : $d->year - 1;
    return "FY{$fy}";
},

Multiple sequences on one model is a real feature, use it

getSequenceConfig() returning multiple keys is the correct answer for invoice_number + internal_ref + credit_note_ref rather than three separate generator calls in a service. Each key is independent, so you can mix periods (monthly for the customer-facing number, never for the internal ref) in a single model.

Reach for the Facade, not the trait, outside the model lifecycle

The trait fires on model save. Anything else — bulk imports, backfilling, an API endpoint that reserves a number before the record exists, a queued job that creates 50k rows — should use the facade:

use MadeByClowd\AutoSequence\Facades\Sequence;

$number = Sequence::generate(
    'order', 'SO', '202606', '{type_code}-{YYYY}-{seq:5}', 5, 'tenant_1'
);

The long positional signature is unpleasant; wrap it in a small App\Support\Numbering helper with named-argument delegation so the rest of your codebase doesn't repeat nine positional parameters. That wrapper is the single highest-leverage thing you can build on top of this package.

Operational tooling is the actual day-to-day win

sequence:verify is the command you'll run most, usually after a data import or a manual fix gone wrong:

php artisan sequence:verify "App\Models\Invoice" number           # detect drift
php artisan sequence:verify "App\Models\Invoice" number --repair  # fast-forward counter

Note --repair only moves the counter forward to match the highest real number. It does not delete orphaned numbers or resolve duplicate rows — if you imported invoices numbered INV-2026-06-00007 while the counter sat at 3, repair sets the counter to 7, and numbers 4–6 are simply never issued. That's correct behaviour for gap-tolerant mode and a problem for gapless mode.

Test against the real trait, not a mock

Because the counter lives in the DB and the behaviour is transactional, mocking the facade gives you false confidence. Use RefreshDatabase, create a handful of models concurrently-ish (or just in a loop in separate transactions), and assert uniqueness. Set load_migrations appropriately in your test config — the package honours load_migrations => false inside your own test suite too, which is what you want if your app ships its own replacement migration.


Gotchas and Tips

Manual override protection is silent, and it will confuse someone. If the sequence column already has a non-null value on save, the package skips generation entirely rather than throwing. This is by design (allow_manual defaults to permitting it; set 'allow_manual' => false to throw instead). Day-to-day consequence: a factory or seeder that does 'number' => null is fine, but 'number' => '' (empty string) or a draft value will silently produce an unnumbered row. Grep your factories and seeders for the column name.

allow_manual => false should be your default in production configs. The failure mode of the default is a silently-unnumbered invoice that reaches a customer; the failure mode of false is a loud exception in a test. Choose the loud one.

Gapless and pre-allocation are mutually exclusive, and the check happens at runtime. Enabling pre_allocation.enabled => true while transaction_mode is 'gapless' throws a configuration exception. Worse, if you enable pre-allocation and use the default cache store, cache eviction or a cache:clear produces silent gaps in the sequence — precisely the thing gapless mode exists to prevent. If you enable pre-allocation, pin a dedicated non-evicting store:

'pre_allocation' => [
    'enabled' => true,
    'store'   => 'redis-noevict',
],

Direct DB edits while pre-allocation is on require a cache flush. The pool is cached under auto-sequence_pool:module:type:period:scope. php artisan sequence:reset handles this; a raw UPDATE sequences SET ... does not, and you'll spend an hour wondering why the counter "didn't take." Don't hand-edit the table — use the facade or the artisan command.

Cache lock drivers: verify, don't assume. config('cache.default') might be redis in prod and file in local. Under file, the lock driver degrades and you get duplicate numbers only under actual concurrency — which means your local tests pass and your Black Friday checkout does not. Assert the driver in a test or in a service provider's boot().

Database lock timeouts are short (5s default) and fail fast. The database locking driver sets a native lock wait timeout rather than blocking PHP. If you generate sequences inside a long-running transaction that also touches the sequence table, you'll get acquisition exceptions under load. Keep sequence-generating work in short, isolated transactions and never generate inside a loop that holds a transaction open across HTTP calls.

Soft deletes don't recycle — and that surprises people. continuous => true recycles numbers only on forceDelete(). Soft-deleted models keep their number reserved so a restore can't collide. If your business genuinely wants recycling on soft delete, you'll need forceDelete() or a manual Sequence::recycle(...) in an observer, plus a policy decision about what recycled invoice numbers mean to your accountant. (They usually mean "audit finding.")

continuous => true is a two-edged sword. Recycled numbers mean INV-2026-06-00008 can refer to two different invoices over time if your retention/logs assume immutability. Don't enable it on anything externally referenced — invoices, orders, receipts. It's fine for internal refs.

Composite key awareness when debugging with raw SQL. Always query all four columns:

SELECT * FROM sequences
WHERE module='invoice' AND type_code='INV' AND period='202606' AND scope='tenant_1';

Querying on a prefix of the composite key bypasses the index and, if you're doing it inside a transaction while other writers are active, worsens lock contention. Also note the fourth column is your scope value — if scope isn't configured, it's an empty/default value, not NULL, and that inconsistency bites people writing dashboards.

Changing format_template doesn't renumber anything. It only affects future allocations. That's correct, but stakeholders sometimes assume otherwise. Decide format changes at a period boundary and communicate that.

sequence:reset sets the next number, off by one from intuition. php artisan sequence:reset invoice INV --value=500 means the next generated number is 501. Every team gets this wrong exactly once, in production, on a Friday.

Maturity caveat. With 16 stars and a recent single-maintainer release cadence, budget for reading the source when something surprises you — the surface is small enough (trait, contract, facade, one table) that this is genuinely feasible, and it's the fastest path to understanding exactly what transaction_mode and pre_allocation do under your specific DB. Pin the version in composer.json and read the changelog before upgrading; a numbering package is infrastructure, and you do not want an unattended minor bump changing allocation semantics.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource