Overview
In Bagisto, one package must never edit another package’s models. Swap Eloquent Models instead, and the override lives outside core code.
Our multi-vendor marketplace ships dozens of packages. Each owns its models, yet any of them can be replaced without a patched file.
The trick is three small pieces: an empty contract, a registry, and a proxy that resolves the class at the moment it is called.
Editing a model in place does not scale
A relation has to name a class. The obvious move is to name the concrete model and get on with the feature.
|
1 2 3 4 |
// Every relation names one concrete model, forever. $this->belongsTo(Seller::class, 'marketplace_seller_id'); // Extending Seller now means editing the package that declared it. |
That line becomes the only truth. A plugin that needs a richer seller has nowhere to stand.
Swap Eloquent Models with a contract and a proxy
Every model implements an empty interface. Packages depend on the interface, so the class behind it stays free to change.
A registry maps each contract to whichever model is registered right now.
|
1 2 3 4 5 |
// contract => concrete, remembered for the whole request $this->models[$abstract] = $concrete; // type-hint the interface, receive the swapped model $this->app->alias($concrete, $abstract); |
The container alias is the quiet half. Type-hint the interface anywhere and Laravel hands back the replacement.
A proxy then forwards every static call to whatever the registry currently holds.
|
1 2 3 4 |
// Swap Eloquent Models by resolving the target class on every static call. $target = $concord->model($this->contract); return call_user_func($target.'::'.$method, ...$parameters); |
That forwarding is plain static overloading, so the proxy needs no knowledge of the model at all.
Relations call SellerProxy::modelClass() rather than naming a class, so the answer is computed per call and never baked in.
|
1 2 |
// Resolved at call time, so a registered override is picked up for free. $this->belongsTo(SellerProxy::modelClass(), 'marketplace_seller_id'); |
Register your own model in a module of your own, and every relation, query and route binding follows it at once.
What to watch when you Swap Eloquent Models
A proxy is not a model. It cannot be instantiated or type-hinted, so the contract is what you inject.
The registry refuses any replacement that does not extend or implement the contract it is registered against.
Registering a model also resets that proxy’s cached instance, which is why service provider order matters.
Static analysis loses the trail, so proxies carry method doc blocks purely to keep the IDE and our earlier reflection notes honest.
Final Thought
Swap Eloquent Models once and extensibility stops being a fork. A package ships a default, an integrator ships a replacement.
Name the interface, register the class, resolve it late. That is the entire idea, and it costs one empty file per model.