eg-mohamed/referenceable
Referenceable is a Laravel package that adds customizable reference numbers to Eloquent models. It offers random, sequential, and template-based generation with placeholders like {YEAR} and {SEQ}, plus collision handling, validation, multi-tenancy, and Artisan commands.
Install and wire up (5 minutes):
composer require eg-mohamed/referenceable
php artisan referenceable:install # creates counter table + publishes config
The install command publishes config/referenceable.php. That file is the first thing to read — it is the real specification of the package; the README is a summary of it. Skim it before touching a model, because per-model properties only override the keys that exist there.
Add the column yourself. The package does not migrate your tables — it generates values, you own the schema:
Schema::table('orders', function (Blueprint $table) {
$table->string('reference')->nullable()->unique()->index();
});
Keep it nullable at the DB level during rollout if you already have rows; backfill later, then decide whether to tighten it.
First use case — invoices. Random strings are the default, but your first real need is almost always an accountant-friendly format. Start with the template strategy:
class Invoice extends Model
{
use HasReference;
protected $referenceStrategy = 'template';
protected $referenceTemplate = ['format' => '{PREFIX}{YEAR}{MONTH}{SEQ}', 'sequence_length' => 4];
protected $referencePrefix = 'INV';
}
// Invoice::create([...])->reference => INV2024030001
Verify immediately in Tinker: Invoice::create([...])->reference, then create two more in a row and confirm the sequence increments. Do this before you build anything on top of it — sequential/template behaviour is the part most likely to surprise you.
Where to look first, in order: published config → the HasReference trait source in vendor/eg-mohamed/referenceable/src/Traits/ → the artisan commands (php artisan referenceable --list). The trait source tells you the exact hook (creating vs saving), the method signatures for generateReference() / regenerateReference() / validateReference(), and whether an explicitly-passed reference is respected. You need to know that before writing seeders or tests.
Strategy selection is a product decision, not a config detail:
| Strategy | Use for | Avoid for |
|---|---|---|
random |
Public-facing IDs where you don't want volume leaked (support ticket refs, API-visible IDs) | Documents a human reads aloud or types from paper |
sequential |
Invoices, receipts, anything with legal/audit sequencing expectations | Anything sharded or multi-tenant without a tenant-aware format |
template |
Date-bucketed operational records — ORD202403-0001, TKT-24-03-00042 |
Cases where {SEQ} reset semantics are unclear to you yet |
Lookup and route binding. References exist to be the public identifier, so bind them:
public function getRouteKeyName(): string { return 'reference'; }
Or query directly: Order::findByReference('INV2024030001') returns null rather than throwing — good for a 404 handler, bad if you assumed firstOrFail semantics. Check the trait for whether it returns the model or a builder.
Draft → final regeneration. A very common real workflow is creating an order with a provisional reference, then issuing a formal one on confirmation:
$order->regenerateReference(save: true);
Guard it: only regenerate while the record is in a draft state, and never after the reference has appeared in an email, PDF, or webhook payload. Once a reference leaves your system it is immutable in practice.
Multi-tenancy. For per-company uniqueness:
protected $referenceUniquenessScope = 'tenant';
protected $referenceTenantColumn = 'company_id';
Make sure company_id is set before the creating event fires (i.e. in the same create() payload or in a creating listener registered earlier). If it's null at generation time, the uniqueness check silently degrades. Add the composite index the README suggests: $table->index(['company_id', 'reference']).
Backfilling an existing table. Use the artisan command with a dry run first, always:
php artisan referenceable:generate "App\Models\Order" --dry-run
php artisan referenceable:generate "App\Models\Order" --batch=500
This is the single highest-leverage command in the package — the alternative is a hand-written chunked each() loop that you'll get wrong on transaction boundaries. Run it on a production-sized copy of the DB, not just a dev seed.
Pre-generating references. For physical labels, pre-printed stationery, or bulk imports where the record doesn't exist yet, use the batch API rather than looping create():
$references = app(\MohamedSaid\ModelReference\ModelReference::class)->generateBatch(Order::class, 100);
Note the namespace — see Gotchas.
Validation of user-supplied references. If customers type a reference into a support form, use validateReference($input) for format checking before a DB hit, then findByReference for existence. Don't use validateReference() as your uniqueness guarantee; it validates shape, not existence.
Testing. The package generates values on model creation, which makes assertions non-deterministic. Two options: assert only the shape (assertMatchesRegularExpression('/^INV\d{12}$/', $invoice->reference)), or explicitly set the reference in the factory and verify the trait respects it. Establish which of those works in your first test — it's a five-minute check that saves an hour of flaky CI later.
The README has a namespace inconsistency. The Quick Start imports MohamedSaid\Referenceable\Traits\HasReference, but the batch-operations example imports MohamedSaid\ModelReference\ModelReference. The package was renamed from model-reference to referenceable in v2.0.3, and this is leftover drift. Resolve it from the actual vendor/ directory rather than from the docs, and don't copy class names out of the README into your codebase without confirming.
Mass assignment. Keep reference out of $fillable. If a client posts a reference field and it's fillable, you either let users dictate identifiers or you rely on the trait overwriting them — both are bad. Leave it unfillable and let the trait own the column.
Concurrency is real. Sequential generation under load will produce duplicate candidates if two requests read the same counter. The package advertises transactions and use_transactions in the performance block — make sure it's on, and make sure you have the unique index on the column so a collision fails loudly rather than silently duplicating. The collision_strategy and max_retries settings are your second line of defence, not your first.
excluded_characters fights length. The default character set excludes 01IOL to avoid transcription errors. Drop length to 4–5 and you've shrunk the keyspace substantially — combined with uniqueness_scope = 'model' on a high-volume table, you'll start burning retries. If you want short references, widen the character set or accept a prefix that carries the entropy.
cache_config with cache_ttl: 60. Confirm whether the TTL is minutes or seconds in the source before you tune it. More importantly: if model config is cached, editing protected $referenceTemplate on a model won't take effect until the cache expires or you clear it. During development of a new reference format, disable cache_config or you will spend twenty minutes convinced the package is broken. Same trap with php artisan config:cache — publish and edit the config before caching.
{SEQ} in templates shares the counter table. The sequential strategy reads model_reference_counters. If you use a template with {SEQ} and a separate model with the sequential strategy, verify they aren't contending for the same counter row. reset_frequency (daily/monthly/yearly) means the counter key is time-dependent — a backdated create() at 00:00:01 on the 1st may land in a fresh counter and produce ...-0001 while yesterday's records sit at ...-0850. That's correct behaviour, but it looks like data corruption to whoever's reconciling the month.
Never run --fix or --regenerate carelessly. referenceable:validate "App\Models\Order" --fix will rewrite references on existing records. Any reference already in a customer's inbox, a PDF, a bank statement, or an external system's database is now a dead link. Do the dry run, export the before/after mapping, and get sign-off. Treat these as migration commands, not maintenance commands.
Model-level vs global config precedence. Per the v1.0.1 changelog, properties are overridable by methods. If you need conditional logic (e.g. different prefixes per tenant or per year), that's your extension point — override the relevant method on the model rather than fighting the property system.
Debugging loop that works: php artisan referenceable:stats "App\Models\Order" --json for the shape of your data → validate (no --fix) for format violations → Tinker to create three records in a row. That sequence isolates config problems from collision problems from counter problems in about two minutes.
Adoption risk is the other gotcha. 87 stars, zero dependents, single maintainer, and a namespace rename inside the last few minor versions. Pin the version in composer.json ("eg-mohamed/referenceable": "^2.2"), keep the trait usage confined to a small number of models, and don't spread $referenceStrategy = 'template' across forty classes. If you ever need to replace it, the surface area is use HasReference, a handful of protected properties, and the column itself — that's a clean swap as long as you didn't let the config leak into business logic.
How can I help you explore Laravel packages today?