brokeyourbike/has-source-model
A lightweight Laravel trait that gives any Eloquent model a polymorphic "source" relationship, letting you track and retrieve the record or model a row originated from with minimal setup.
This package gives an Eloquent model a polymorphic "source" relation — the record it was derived from (an Invoice sourced from an Order, a Refund sourced from a Payment). Day-to-day it is a thin trait plus a couple of columns, so onboarding is quick.
^2.x unless you can read the diff:
composer require brokeyourbike/has-source-model-laravel
source_id / source_type morph columns; add them yourself with $table->morphs('source') if the package doesn't):
php artisan vendor:publish --tag="has-source-model-migrations"
php artisan migrate
use BrokeYourBike\HasSourceModel\HasSourceModel;
class Invoice extends Model
{
use HasSourceModel;
}
$invoice = Invoice::create([...]);
$invoice->source()->associate($order)->save();
$invoice->source; // Order instance
$invoice->sourceModel(); // same, via the trait helper (verify name in the source)
Where to look first: the trait itself (src/HasSourceModel.php) and its migration stub. The surface area is small, and the docblock/README will tell you the exact relation and helper names — the v3 release notes do not.
Treat source as write-once, read-often. Associate it in the same transaction as the model's creation, then never mutate it. This keeps provenance trustworthy and avoids needing to reason about re-pointing a record at a different origin.
DB::transaction(function () use ($order) {
$invoice = Invoice::create([...]);
$invoice->source()->associate($order)->save();
});
Prefer associate() over mass assignment. Accepting source_type / source_id from request input lets a client dictate the morph class — a classic injection vector. Resolve the source model server-side and associate it explicitly.
Enforce a morph map before you have data. Polymorphic columns store FQCNs by default, which bloats rows and couples your schema to namespaces:
// AppServiceProvider::boot()
Relation::enforceMorphMap([
'order' => \App\Models\Order::class,
'payment' => \App\Models\Payment::class,
]);
Do this before the first production write, or plan a backfill.
Eager load, always. source is a morphTo, so looping over a collection without with('source') produces one query per distinct type per row:
Invoice::with('source')->whereDate('created_at', today())->get();
For heterogeneous sources, constrain per type so you don't hydrate more than you need:
Invoice::with(['source' => function (MorphTo $morphTo) {
$morphTo->morphWith([
Order::class => ['customer'],
Payment::class => ['gateway'],
]);
}])->get();
Walk the provenance chain. Derived-from relations compose naturally into an audit trail. Wrap the traversal so callers don't reimplement the loop:
// app/Models/Concerns/... or a small service
function lineage(Model $model): array
{
$chain = [$model];
while ($model->source) {
$chain[] = $model = $model->source;
}
return $chain; // refund -> payment -> order
}
Filter by source in list/report queries. If v3's "reference" feature ships a scope (e.g. whereSource()), use it rather than hand-writing where('source_type', ...)->where('source_id', ...). Verify the generated SQL once — morph scopes are easy to get subtly wrong when a morph map is active.
Factories and tests. ->for($order, 'source') works out of the box for a morph relation and is the cleanest way to build fixtures; write one test asserting both that the relation resolves and that the stored source_type is your morph alias, not the FQCN. That single test catches the most common regression in this area.
Use it as an integration seam. Provenance is genuinely useful in events and listeners: on InvoicePaid, resolve $invoice->source and dispatch downstream work to the originating aggregate rather than duplicating foreign keys on every table.
cb25541, 25e561f, and 4580864, diff the trait and any migration stub against your installed 2.x, and check whether stored source_type values or the relation/helper names changed. Treat it as a schema-affecting release until proven otherwise.null from source. Backfill in a transaction and verify with a count query before and after.source may already be taken. Check for an existing source column, accessor, or relation on the model — Laravel will not warn you, and the trait will lose to a local attribute. Rename via the package's config if it exposes one.$model->source?->... and decide explicitly whether source is required for new records (a creating hook or a validation rule beats hoping).morphTo — deleting an Order leaves invoices pointing at a dead ID. Either forbid deleting sourced parents, or accept nulls and handle them in the UI ($invoice->source()->exists() before display).source_id is non-null, confirm source_type matches a morph-map alias (not the FQCN or the reverse), then confirm the class still exists. Relation::getMorphedModel($type) in tinker resolves this in seconds.morphTo plus your own scope is usually cheaper than patching around it."brokeyourbike/has-source-model-laravel": "^3.0" with a composer.lock committed; this package moves in majors without detailed notes, so let CI — not a fresh composer update — decide when you cross a version boundary.How can I help you explore Laravel packages today?