{"id":47608602,"url":"https://github.com/rajeshmk/laravel-sequence","last_synced_at":"2026-04-01T19:45:03.150Z","repository":{"id":65153283,"uuid":"584137279","full_name":"rajeshmk/laravel-sequence","owner":"rajeshmk","description":"Generate transaction-safe sequential numbers in Laravel with grouping, formatting, and model integration.","archived":false,"fork":false,"pushed_at":"2026-03-18T17:44:38.000Z","size":37,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-03-19T01:01:22.994Z","etag":null,"topics":["auto-number","concurrency","custom-id","database","eloquent","generator","invoice-number","laravel","laravel-package","laravel-plugin","order-number","php","php8","sequence","sequence-number","serial-number"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rajeshmk.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-01-01T14:35:09.000Z","updated_at":"2026-03-18T15:57:25.000Z","dependencies_parsed_at":"2026-03-19T01:01:26.501Z","dependency_job_id":null,"html_url":"https://github.com/rajeshmk/laravel-sequence","commit_stats":null,"previous_names":["vocolabs/laravel-roll-number","rajeshmk/laravel-sequence"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/rajeshmk/laravel-sequence","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rajeshmk%2Flaravel-sequence","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rajeshmk%2Flaravel-sequence/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rajeshmk%2Flaravel-sequence/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rajeshmk%2Flaravel-sequence/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rajeshmk","download_url":"https://codeload.github.com/rajeshmk/laravel-sequence/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rajeshmk%2Flaravel-sequence/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31291206,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-01T13:12:26.723Z","status":"ssl_error","status_checked_at":"2026-04-01T13:12:25.102Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["auto-number","concurrency","custom-id","database","eloquent","generator","invoice-number","laravel","laravel-package","laravel-plugin","order-number","php","php8","sequence","sequence-number","serial-number"],"created_at":"2026-04-01T19:45:01.459Z","updated_at":"2026-04-01T19:45:03.143Z","avatar_url":"https://github.com/rajeshmk.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Laravel Sequence Numbers\n\nConcurrency-safe Laravel sequence numbers with database transactions, row-level locking, grouping, prefixes, and Eloquent model integration.\n\n[![Latest Stable Version](https://poser.pugx.org/hatchyu/laravel-sequence/v)](https://packagist.org/packages/hatchyu/laravel-sequence)\n[![Total Downloads](https://poser.pugx.org/hatchyu/laravel-sequence/downloads)](https://packagist.org/packages/hatchyu/laravel-sequence)\n[![License](https://poser.pugx.org/hatchyu/laravel-sequence/license)](https://packagist.org/packages/hatchyu/laravel-sequence)\n[![PHP](https://img.shields.io/badge/PHP-%5E8.3-777BB4?logo=php)](https://packagist.org/packages/hatchyu/laravel-sequence)\n[![Laravel](https://img.shields.io/badge/Laravel-10%20%7C%2011%20%7C%2012%20%7C%2013-FF2D20?logo=laravel)](https://packagist.org/packages/hatchyu/laravel-sequence)\n\n\u003e **The Problem:** Generating sequential, human-readable numbers—like `INV-0001` or `ORD-2026-001`—is surprisingly difficult in a highly concurrent Laravel application. Relying on simple database counts or `max()` queries inevitably leads to race conditions, duplicate numbers, and database query crashes.\n\u003e\n\u003e **The Solution:** A concurrency-safe sequence number generator for Laravel using database transactions and row-level locking. It guarantees perfectly incremental, gapless numbers even under extreme server load. Effortlessly create formatted sequences with customized prefixes and zero-padding, isolate counters by dynamic groups (like per-tenant or per-year), and auto-assign them to your Eloquent models using a convenient `HasSequence` trait.\n\n**Quick summary:** use the `sequence()` helper inside a DB transaction to generate concurrency-safe sequence numbers, or add the `HasSequence` trait to auto-assign them on Eloquent model `creating`.\n\n**Important constraint:** This package intentionally requires a database transaction to guarantee correctness under concurrency.\n\n## How It Works\n\n- The package stores the current counter in a `sequences` table keyed by `(name, group_by)`.\n- When you call `-\u003enext()`, it locks the matching row with `SELECT ... FOR UPDATE` inside your transaction.\n- It increments `last_number` safely, then returns the formatted value with any prefix, padding, or custom format applied.\n- `groupBy()` changes which counter row is used, so you can keep separate sequences per tenant, branch, year, day, or any other scope.\n- Unlike `max() + 1` or counting rows, this approach is designed to stay correct under concurrency.\n\n## When To Use This Package\n\n- You need human-readable sequential numbers such as invoices, orders, customer codes, or batch IDs.\n- You need sequence generation to remain correct under concurrency in a Laravel application.\n- You want grouped counters such as per tenant, per branch, per year, or per day.\n- You want formatted output like prefixes, zero-padding, or custom templates while keeping the numeric counter safe.\n- You want sequence assignment integrated with Eloquent model creation through `HasSequence`.\n\n## When Not To Use This Package\n\n- You cannot wrap the create flow in a database transaction.\n- You do not need strict sequential behavior and a UUID or random identifier would work better.\n- You are fine with gaps, duplicates, or eventual consistency from approaches like `max() + 1`.\n- Your database engine or table setup does not support transaction-safe row-level locking.\n\n## Requirements\n\n- PHP: `^8.3`\n- Laravel: `^10 || ^11 || ^12 || ^13`\n- Database: uses row-level locking (`SELECT ... FOR UPDATE`) inside transactions to ensure safe concurrent increments.\n\n## Installation\n\nInstall via Composer:\n\n```bash\ncomposer require hatchyu/laravel-sequence\n```\n\nRun your migrations (the package auto-loads its migrations via the service provider):\n\n```bash\nphp artisan migrate\n```\n\nOptional: publish the config file if you want to customize table/connection/model:\n\n```bash\nphp artisan vendor:publish --tag=config --provider=\"Hatchyu\\\\Sequence\\\\SequenceServiceProvider\"\n```\n\nOptional: publish the migration if you want to customize the table name or columns:\n\n```bash\nphp artisan vendor:publish --tag=sequence-migrations --provider=\"Hatchyu\\\\Sequence\\\\SequenceServiceProvider\"\n```\n\n## Tables\n\nThe package creates a `sequences` table which stores the current `last_number` per `(name, group_by)` tuple. The `group_by` column is a deterministic string token generated from the configured `groupBy` values.\n\nIf you use a custom model via `config('sequence.model')`, it must extend Eloquent `Model` and be backed by a table that contains `name`, `group_by`, and `last_number` columns. The package writes those attributes via `forceFill()`, so `fillable` is not required.\n\nIf you publish and customize the migration, keep the unique index on `(name, group_by)` and a numeric `last_number` column — those are required for correctness under concurrency.\n\n## Usage\n\nImportant: Sequence generation must run inside a DB transaction; the package will throw an exception if called outside one.\nThis package intentionally requires a database transaction to guarantee correctness under concurrency.\nUse `-\u003enext()` to reserve and return the next value.\n\n### Mental model\n\n- `prefix()` and `format()` control only how the final sequence value is displayed.\n- `groupBy()` determines which counter row is used, so it controls when numbering resets.\n- `padLength()` zero-pads the numeric part on the left, similar to PHP `str_pad(..., STR_PAD_LEFT)`.\n\n### 1) Simple sequential numbers\n\nGenerate an incrementing sequence (\"1\", \"2\", \"3\", ...):\n\n```php\nuse Illuminate\\Support\\Facades\\DB;\n\n$value = DB::transaction(function () {\n    return sequence('sequence_number')-\u003enext();\n});\n\n// returns \"1\", then \"2\", etc.\n```\n\n### 2) Prefix and pad length\n\nProvide a prefix and a pad numeric length (padded with zeros):\n\n```php\n$value = DB::transaction(function () {\n    return sequence('category_code')\n        -\u003eprefix('C')\n        -\u003epadLength(3)\n        -\u003enext(); // \"C001\"\n});\n```\n\nYou can combine any prefix string with an integer `padLength`.\n\n`padLength` behaves like PHP `str_pad(..., STR_PAD_LEFT)`: the numeric part is left-padded with `0` up to the requested length, and if the number is already longer than that length, it is returned unchanged.\n\n### 2b) Custom increment step\n\nBy default the package increments by `1`. If you need `1, 6, 11, ...` or any other step size, use `step()`:\n\n```php\n$first = DB::transaction(fn () =\u003e sequence('batch')-\u003estep(5)-\u003enext());  // \"1\"\n$second = DB::transaction(fn () =\u003e sequence('batch')-\u003estep(5)-\u003enext()); // \"6\"\n$third = DB::transaction(fn () =\u003e sequence('batch')-\u003estep(5)-\u003enext());  // \"11\"\n```\n\nThe first generated value still starts from the configured minimum. The step is applied to each subsequent reservation.\n\n### 3) Dynamic parts in the output (e.g. year + sequence)\n\nIf you want codes like `202601`, `202602`, ... you can pass dynamic prefix values (for example `date('Y')`) and a suitable pad length:\n\n```php\n$value = DB::transaction(function () {\n    return sequence('batch_code')\n        -\u003eprefix(date('Y'))\n        -\u003epadLength(2)\n        -\u003enext();\n});\n```\n\nThis only prefixes the generated number with the current year. It does not create a separate counter per year.\n\nFor example, if the underlying sequence keeps incrementing across years, you might see values like `202601`, `202602`, ... and later `2027100`, `2027101`, ...\n\n`prefix()` and `format()` only control how the final sequence value is displayed. They do not create a new counter or reset numbering.\n\n`groupBy()` determines which counter row is used. Use it whenever numbering should reset by year, month, tenant, branch, or any other grouping key.\n\nFor common date-based scopes, you can also use the convenience helpers `groupByYear()`, `groupByMonth()`, and `groupByDay()`.\n\nIf you want the number to reset for each new year, month, branch, or tenant, use grouping as well:\n\n```php\n$value = DB::transaction(function () {\n    return sequence('batch_code')\n        -\u003eprefix(date('Y'))\n        -\u003epadLength(2)\n        -\u003egroupByYear()\n        -\u003enext();\n});\n\n// 202601, 202602, ... then 202701, 202702, ...\n```\n\n### 3b) Custom format templates\n\nIf you want a full template such as `INV/20260318/0001`, use `format()` and place a `?` where the sequence number should go:\n\n```php\n$value = DB::transaction(function () {\n    return sequence('invoice')\n        -\u003eformat('INV/' . date('Ymd') . '/?')\n        -\u003epadLength(4)\n        -\u003enext();\n});\n```\n\nThe `?` placeholder is replaced with the generated number after padding is applied.\n\n### 3c) Custom format callbacks\n\nIf you need full control over the final output, `format()` also accepts a callback. The callback receives the already padded numeric portion and must return the final sequence string:\n\n```php\nuse Illuminate\\Support\\Str;\n\n$value = DB::transaction(function () {\n    return sequence('tickets')\n        -\u003epadLength(4)\n        -\u003eformat(fn (string $number): string =\u003e \"TIC-{$number}-\" . Str::random(3))\n        -\u003enext();\n});\n```\n\nThis is useful when you need dynamic suffixes, more advanced string composition, or formatting that does not fit a single `?` placeholder template.\n\nLike `prefix()`, `format()` only changes how the final value is displayed. It does not create a separate counter by itself.\n\nWithout grouping, the date part in the formatted output can change while the underlying counter continues increasing. For example, you might see `INV/20260318/0001`, `INV/20260318/0002`, and later `INV/20260319/0100`, `INV/20260319/0101`, ...\n\nIf you also want the counter to reset per day, combine the format with grouping:\n\n```php\n$value = DB::transaction(function () {\n    return sequence('invoice')\n        -\u003egroupByDay()\n        -\u003eformat('INV/' . date('Ymd') . '/?')\n        -\u003epadLength(4)\n        -\u003enext();\n});\n// INV/20260318/0001, INV/20260318/0002, then INV/20260319/0001, INV/20260319/0002, ...\n```\n\n### 4) Grouped sequences (per parent, per branch, etc.)\n\nSometimes you want separate counters per group of values (branch, year, tenant, etc.). The package supports grouping by multiple keys or models.\n\nImportant: `groupBy()` changes which counter row is used. It does not automatically add those values to the output string. If you want the group key to also appear in the generated value, include it in the prefix or format template yourself.\n\nExample: reset sequence per branch and year:\n\n```php\n// When generating directly with multiple group keys\nDB::transaction(function () use ($branchId, $year) {\n    return sequence('customer_code')\n        -\u003egroupBy($branchId, $year)\n        -\u003enext();\n});\n\n// When used via HasSequence, configure grouping in SequenceConfig (example below)\n```\n\nNotes:\n\n- You can pass persisted Eloquent models inside `groupBy($modelA, $modelB)`.\n- Models must exist in the database before being used for grouping.\n- If you prefer a more expressive name when grouping by parent models, use `belongsTo($modelA, $modelB)`. It behaves exactly like `groupBy()`.\n\nExample with `belongsTo()`:\n\n```php\nDB::transaction(function () use ($tenant, $branch) {\n    return sequence('tenant_branch_invoice')\n        -\u003ebelongsTo($tenant, $branch)\n        -\u003epadLength(4)\n        -\u003enext();\n});\n```\n\n### Common recipes\n\nYearly reset with the year shown in the output:\n\n```php\nDB::transaction(fn () =\u003e sequence('batch_code_grouped_by_year')\n    -\u003eprefix(date('Y'))\n    -\u003epadLength(2)\n    -\u003egroupByYear()\n    -\u003enext()\n);\n// 202601, 202602, ... then 202701, 202702, ...\n```\n\nSeparate counters by multiple grouping keys (tenant, branch, year):\n\n```php\nDB::transaction(function () {\n    return sequence('invoice_tenant_branch_year_wise')\n        -\u003epadLength(2)\n        -\u003egroupBy(1, 2, date('Y'))\n        -\u003enext();\n});\n```\n\nInvoice number with a date in the output and a per-day reset:\n\n```php\nDB::transaction(fn () =\u003e sequence('daily_invoice')\n    -\u003egroupByDay()\n    -\u003eformat('INV/' . date('Ymd') . '/?')\n    -\u003epadLength(4)\n    -\u003enext()\n);\n// INV/20260318/0001, INV/20260318/0002, then INV/20260319/0001\n```\n\n### 5) Auto-assign on Eloquent models (`HasSequence`)\n\nAdd the `HasSequence` trait to your model and provide a `sequenceColumns()` method that returns a `SequenceColumnCollection`. This supports one or many columns. Example:\n\n```php\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Hatchyu\\Sequence\\Traits\\HasSequence;\nuse Hatchyu\\Sequence\\Support\\SequenceConfig;\nuse Hatchyu\\Sequence\\Support\\SequenceColumnCollection;\n\nclass CustomerProfile extends Model\n{\n    use HasSequence;\n\n    protected function sequenceColumns(): SequenceColumnCollection\n    {\n        return SequenceColumnCollection::collection()\n            -\u003ecolumn(\n                'customer_code',\n                SequenceConfig::create()\n                    -\u003eprefix('CU')\n                    -\u003epadLength(3)\n                    // optional: make sequence per-branch (or per branch+year, etc.)\n                    -\u003egroupBy($this-\u003ebranch_id)\n            );\n    }\n}\n```\n\nBehavior notes for trait usage:\n\n- The sequence type name is derived from the model table name + column name (used as the `name` key in `sequences`).\n- Generation runs during the model `creating` hook — your create flow must be in a DB transaction, otherwise generation will throw.\n- This package intentionally requires a database transaction to guarantee correctness under concurrency.\n- If the column already has a non-empty value, the trait will not overwrite it.\n\nExample with a custom format on a model column:\n\n```php\nprotected function sequenceColumns(): SequenceColumnCollection\n{\n    return SequenceColumnCollection::collection()\n        -\u003ecolumn(\n            'invoice_number',\n            SequenceConfig::create()\n                -\u003egroupByDay()\n                -\u003eformat('INV/' . date('Ymd') . '/?')\n                -\u003epadLength(4)\n        );\n}\n```\n\nMultiple columns example:\n\n```php\nprotected function sequenceColumns(): SequenceColumnCollection\n{\n    return SequenceColumnCollection::collection()\n        -\u003ecolumn(\n            'admission_number',\n            SequenceConfig::create()\n                -\u003eprefix('ADM')\n                -\u003epadLength(3)\n        )\n        -\u003ecolumn(\n            'attendance_number',\n            SequenceConfig::create()\n                -\u003egroupBy($this-\u003etenantId(), $this-\u003eacademic_year, $this-\u003eclass_id)\n        );\n}\n```\n\n## API reference\n\n- Helper: `sequence(string $name)` — returns a `NextSequence` instance.\n- Call `-\u003egroupBy(...$keys)` on the returned object to scope the counter by multiple values or models.\n- Call `-\u003ebelongsTo(...$models)` — an expressive alias for `groupBy()` when scoping counters by parent Eloquent models.\n- Convenience helpers: `-\u003egroupByYear()`, `-\u003egroupByMonth()`, and `-\u003egroupByDay()` for common date-based counter scopes.\n- Call `-\u003estep(int $amount)` to define a custom increment step (default `1`).\n- Call `-\u003eprefix(string $prefix)` to prepend a static or dynamic string.\n- Call `-\u003epadLength(int $length)` to zero-pad the numeric part on the left.\n- Call `-\u003eformat(string|Closure $format)` to use either a `?` placeholder template or a callback that returns the final output string.\n- Call `-\u003erange(int $min, ?int $max = null)` to set min/max bounds directly on the fluent sequence builder.\n- Call `-\u003ebounded(int $min, int $max)` to set a bounded range that throws on overflow.\n- Call `-\u003ecyclingRange(int $min, int $max)` to set a bounded range that cycles back to `min`.\n- Call `-\u003ecycle()` to wrap to `min` when `max` is reached.\n- Call `-\u003ethrowOnOverflow()` to explicitly keep the default overflow behavior.\n- Call `-\u003econfig(fn (SequenceConfig $config) =\u003e ...)` when you want to configure the underlying `SequenceConfig` explicitly or reuse the same style as `HasSequence`.\n- `SequenceConfig::groupByYear()` / `groupByMonth()` / `groupByDay()` — convenience helpers for date-based group scopes.\n- `SequenceConfig::step(int $amount)` — configure a custom increment step.\n- `SequenceConfig::prefix(string $prefix)` — configure a prefix.\n- `SequenceConfig::padLength(int $length)` — configure zero-padding for the numeric part.\n- `SequenceConfig::format(string|Closure $format)` — configure either a custom output template with `?` or a callback formatter that receives the padded numeric part.\n- `SequenceConfig::range(int $min, ?int $max = null)` — set min/max bounds.\n- `SequenceConfig::bounded(int $min, int $max)` — range + throw on overflow.\n- `SequenceConfig::cyclingRange(int $min, int $max)` — range + cycle on overflow.\n- `SequenceConfig::cycle()` — wrap to min when max is reached.\n- `SequenceConfig::throwOnOverflow()` — throw when max is reached (default).\n- Call `-\u003enext(): string` to reserve and return the next sequence value.\n\nExample:\n\n```php\n$next = sequence('orders')\n    -\u003eprefix('ORD-')\n    -\u003epadLength(6)\n    -\u003egroupBy($customerId, date('Y'))\n    -\u003enext();\n```\n\nExample with direct fluent range configuration:\n\n```php\n$next = sequence('range_test')\n    -\u003epadLength(2)\n    -\u003erange(1, 7)\n    -\u003egroupByYear()\n    -\u003enext();\n```\n\nExample with config callback:\n\n```php\nuse Hatchyu\\Sequence\\Support\\SequenceConfig;\n\n$next = sequence('range_test')\n    -\u003epadLength(2)\n    -\u003econfig(function (SequenceConfig $config) {\n        $config-\u003erange(1, 7)\n            -\u003egroupByYear();\n    })\n    -\u003enext();\n```\n\nNote: `config()` is optional. The fluent `sequence()` builder forwards configuration methods to the underlying `SequenceConfig`, so you can chain methods like `range()`, `bounded()`, `groupBy()`, or `format()` directly. Use `config()` when you prefer an explicit callback or need to work with `SequenceConfig` itself.\n\n## Concurrency and transactions\n\n- Always call generation inside `DB::transaction()`.\n- The package uses `SELECT ... FOR UPDATE` to lock the row that stores `last_number` for a given `(name, group_by)`.\n- Keep transactions short to reduce lock contention.\n- If you configured a custom connection in `config/sequence.php`, make sure to use that same connection for the surrounding transaction.\n- When using `HasSequence`, the package will use the model's connection by default (unless `sequence.connection` is configured).\n- When using the `sequence()` helper, the package uses `sequence.connection` if set; otherwise it uses the default connection. Use `DB::connection('name')-\u003etransaction(...)` to match.\n- Ensure your database engine supports row-level locking in transactions (e.g., InnoDB on MySQL).\n\n## Range and overflow\n\nThe package supports min/max ranges via `range()`, `bounded()`, and `cyclingRange()` on the fluent `sequence()` builder, or the same methods on `SequenceConfig` when configuring model sequences.\n\n- `range($min, $max)` sets the allowed range (inclusive). The default overflow behavior is to throw a `SequenceOverflowException` when `max` is reached.\n- `bounded($min, $max)` is a convenience wrapper that sets the range and keeps the default \"fail\" overflow behavior.\n- `cyclingRange($min, $max)` wraps back to `min` when `max` is reached.\n \nNotes:\n- `min` is inclusive and can be `0`. `max` is inclusive and must be at least `1`.\n- If the next number exceeds the pad length, it is returned as-is (no truncation).\n- Grouping affects the counter scope, not the rendered output.\n\nExample (throw on overflow):\n\n```php\nDB::transaction(fn () =\u003e sequence('orders')\n    -\u003eprefix('ORD-')\n    -\u003epadLength(4)\n    -\u003ebounded(1, 9999)\n    -\u003enext()\n);\n// ORD-0001, ORD-0002, ... ORD-9999, then throws SequenceOverflowException\n```\n\nExample (cycle back to min):\n\n```php\nDB::transaction(fn () =\u003e sequence('sessions')\n    -\u003ecyclingRange(1, 10)\n    -\u003enext()\n);\n// 1, 2, 3, ... 10, 1, 2, ...\n```\n\nSee the error handling section below for a `SequenceOverflowException` catch example. Consult the config API in `src/Support/SequenceConfig.php` for exact methods and options.\n\n## Customization (Config)\n\nAfter publishing the config file, you can customize:\n\n- `table`: The name of the sequence counters table.\n- `connection`: The database connection used by the sequence model (use the same connection for your surrounding transaction).\n- `model`: The Eloquent model class for sequences. Must extend `Model` and use a table with `name`, `group_by`, and `last_number` columns.\n- `strict_mode`: When enabled (default), validates name and group key lengths and throws clear exceptions before hitting DB errors.\n\n## Events\n\nThe package dispatches a `Hatchyu\\Sequence\\Events\\SequenceAssigned` event whenever a number is reserved:\n\n```php\nuse Hatchyu\\Sequence\\Events\\SequenceAssigned;\nuse Illuminate\\Support\\Facades\\Event;\n\nEvent::listen(SequenceAssigned::class, function (SequenceAssigned $event) {\n    // $event-\u003ename, $event-\u003erawNumber, $event-\u003esequenceNumber, $event-\u003egroupByKey\n});\n```\n\nEvent payload fields:\n\n- `name`: internal sequence name stored in the `sequences` table\n- `rawNumber`: numeric counter value before formatting\n- `sequenceNumber`: final returned string after prefix/padding/formatting\n- `groupByKey`: resolved group token used to scope the counter\n\n## Testing\n\nThe package includes comprehensive unit tests and concurrency regression tests.\n\n### Unit Tests\n\nRun the test suite with Pest:\n\n```bash\n./vendor/bin/pest\n```\n\nKey test files:\n- `tests/Unit/NextSequenceTest.php` - Tests transaction enforcement, sequential increments, overflow, cycling, and custom formatting\n- `tests/Unit/SequenceConfigTest.php` - Tests configuration creation and validation\n- `tests/Unit/SequenceAssignedEventTest.php` - Tests event object construction\n- `tests/Unit/SequenceExceptionTest.php` - Tests exception code assignments\n\nTest coverage includes:\n- Configuration creation with prefix and pad length\n- Validation of negative pad length (throws exception)\n- Validation of grouping by non-persisted models (throws exception)\n- Transaction enforcement\n- Sequential increments\n- Custom increment steps\n- Range overflow behavior\n- Cycling behavior\n- Custom format templates\n- Custom format callbacks\n- Event object construction\n\n### Concurrency Testing\n\nFor concurrency testing, use the regression script that simulates parallel processes:\n\n```bash\nphp scripts/regression_concurrency.php\n```\n\nThis script:\n- Creates multiple worker processes that generate sequence numbers simultaneously\n- Verifies no duplicate numbers are generated\n- Tests the concurrency safety of the package\n\n### Testing Best Practices\n\n- Always wrap sequence number generation in `DB::transaction()` in tests\n- Use database fixtures for consistent test data\n- Test both simple sequences and grouped sequences\n- Verify event dispatching in integration tests\n\nExample test:\n\n```php\nuse Illuminate\\Foundation\\Testing\\RefreshDatabase;\nuse Hatchyu\\Sequence\\Events\\SequenceAssigned;\n\nuses(RefreshDatabase::class);\n\nit('generates sequential sequence numbers', function () {\n    $value1 = DB::transaction(fn() =\u003e sequence('test')-\u003enext());\n    $value2 = DB::transaction(fn() =\u003e sequence('test')-\u003enext());\n\n    expect($value1)-\u003etoBe('1');\n    expect($value2)-\u003etoBe('2');\n});\n\nit('dispatches sequence number assigned event', function () {\n    Event::fake();\n\n    DB::transaction(fn() =\u003e sequence('test')-\u003enext());\n\n    Event::assertDispatched(SequenceAssigned::class);\n});\n```\n\n## Error handling \u0026 troubleshooting\n\nThe package throws `Hatchyu\\Sequence\\Exceptions\\SequenceException` (a `RuntimeException`) and a few grouped subclasses with specific error codes:\n\n- `SequenceValidationException` — invalid names or group-by tokens\n  - `CODE_NAME_REQUIRED` (400) — sequence name is empty\n  - `CODE_NAME_TOO_LONG` (401) — sequence name exceeds length limit\n  - `CODE_GROUP_BY_TOKEN_TOO_LONG` (402) — group-by token exceeds length limit\n\n- `SequenceTransactionException` — missing DB transaction\n  - `CODE_TRANSACTION_NOT_INITIATED` (300) — no active transaction\n  - `CODE_TRANSACTION_NOT_INITIATED_ON_CONNECTION` (301) — no active transaction on specific connection\n\n- `SequenceConfigException` — invalid configuration values\n  - `CODE_PAD_LENGTH_NEGATIVE` (100) — pad length is negative\n  - `CODE_MIN_NEGATIVE` (101) — min value is negative\n  - `CODE_MAX_TOO_SMALL` (102) — max value is less than 1\n  - `CODE_MAX_LESS_THAN_MIN` (103) — max value is less than min value\n  - `CODE_INVALID_MODEL_CLASS` (104) — configured model class is invalid\n  - `CODE_FORMAT_PLACEHOLDER_MISSING` (105) — format template does not contain `?`\n\n- `SequenceModelException` — invalid or unsaved models\n  - `CODE_MODEL_KEY_MUST_BE_STRING` (200) — model key is not a string\n  - `CODE_MODEL_MUST_BE_PERSISTED` (201) — model must be saved before grouping\n\n- `SequenceOverflowException` — max reached while overflow strategy is `FAIL`\n  - `CODE_SEQUENCE_OVERFLOW` (500) — sequence max reached\n\nYou can catch either the base class or a specific subclass depending on how granular you want the handling to be. Each exception includes a specific error code for programmatic handling.\n\nExample:\n\n```php\nuse Hatchyu\\Sequence\\Exceptions\\SequenceException;\nuse Hatchyu\\Sequence\\Exceptions\\SequenceOverflowException;\nuse Hatchyu\\Sequence\\Exceptions\\SequenceTransactionException;\n\ntry {\n    DB::transaction(fn () =\u003e sequence('orders')-\u003enext());\n} catch (SequenceOverflowException $e) {\n    // max reached and overflow strategy is FAIL\n    throw $e;\n} catch (SequenceTransactionException $e) {\n    if ($e-\u003egetCode() === SequenceTransactionException::CODE_TRANSACTION_NOT_INITIATED) {\n        // handle missing transaction specifically\n    }\n    throw $e;\n} catch (SequenceException $e) {\n    // handle any other sequence error\n    throw $e;\n}\n```\n\n### Common troubleshooting\n\n- \"Not in transaction\" exception: ensure `sequence()` runs inside `DB::transaction()`.\n- Duplicate numbers under concurrency: check that your DB supports `SELECT ... FOR UPDATE` on the used engine and that transactions are used.\n- Counter did not reset for a new year/month/day: add `groupBy(...)`; changing only prefix or format does not create a new counter scope.\n- Group key not visible in the generated string: add it to the prefix or `format()` output yourself; `groupBy()` only scopes the counter.\n\n## FAQ\n\n### Does this package require a database transaction?\n\nYes. This package intentionally requires a database transaction to guarantee correctness under concurrency. It will throw a `SequenceTransactionException` if sequence generation runs outside an active transaction.\n\n### Why not just use `max() + 1`?\n\nBecause `max() + 1` is not safe under concurrency. Two requests can read the same current maximum and generate the same next value. This package avoids that by using row-level locking inside a transaction.\n\n### Does `prefix()` or `format()` reset the counter?\n\nNo. `prefix()` and `format()` only change the rendered output. If you need separate counters per year, tenant, branch, or day, use `groupBy(...)` or the date grouping helpers.\n\n### Can I use this with Eloquent models?\n\nYes. Add the `HasSequence` trait and return a `SequenceColumnCollection` from `sequenceColumns()`. Your model creation flow still needs to run inside a database transaction.\n\n### Will this work for per-year or per-day numbering?\n\nYes, if you scope the counter with grouping. Use `groupByYear()`, `groupByMonth()`, `groupByDay()`, or `groupBy(...)` with your own keys.\n\n## Development\n\n### Running Tests\n\n```bash\n# Run all tests\n./vendor/bin/pest\n\n# Run specific test file\n./vendor/bin/pest tests/Unit/SequenceConfigTest.php\n\n# Run with coverage\n./vendor/bin/pest --coverage\n```\n\nNote: the current test suite exercises the core generation logic against SQLite and includes standalone regression scripts. For production release confidence on MySQL or PostgreSQL, it is still a good idea to run one integration pass in an application that uses your target driver.\n\n### Regression Scripts\n\nThe `scripts/` directory contains regression testing scripts:\n\n```bash\n# Test first number generation\nphp scripts/regression_first_number.php\n\n# Test concurrency with multiple processes (requires pcntl extension)\nphp scripts/regression_concurrency.php\n```\n\n## Contribution\n\nContributions are welcome — open issues or PRs.\n\n## License\n\nMIT. See `LICENSE`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frajeshmk%2Flaravel-sequence","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frajeshmk%2Flaravel-sequence","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frajeshmk%2Flaravel-sequence/lists"}