Using contracts (interfaces) for model relations instead of concrete classes #55975
Replies: 1 comment 6 replies
-
Intriguing topic 👍🏻
Doesn't sound too bad, I'd say 🤔
Because this is to be registered in a service container? Technically, one could probably also register this in namespace Package\Models;
interface IHasPackageUser
{
/**
* @return class-string<\Illuminate\Database\Eloquent\Model>
*/
public static function userRelationClass(): string;
} namespace Package\Models;
trait HasPackageUser
{
public function bootHasPackageUser()
{
self::resolveRelationUsing('user', function (SomeModel $model) {
return $model->belongsTo(self::userRelationClass());
});
}
} namespace App\Models;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Package\Models\HasPackageUser;
use Package\Models\IHasPackageUser;
class Account extends Model implements IHasPackageUser
{
use HasPackageUser;
public static function userRelationClass(): string
{
return User::class;
}
}); |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Often when creating packages we define models that have relations, either with other models in the package, or with models that are part of the application, for example a common one is a user relationship:
This all works fine for simple models, but starts to get messy when you want to override the model with your own implementation.
A common pattern i've seen in packages to allow changing implementations is to create a basic model in the package, and then allow config to override it, for example:
Again this works, except we have lost type hinting, but it feels weird (i'm aware this could just be me) that the implementation is determined by config, especially in the typed world of static analysis tools we have now.
So, in my head, something like this would be useful:
and then the contract is resolved to the actual concrete model class by the application.
But resolving the contract is where it starts to get a bit more tricky, I think my ideal would have been to bind it to the service container in the application service provider, and have it resolved from there, something like:
but I dont think that eloquent has access to the service container in that way (and probably shouldnt?), so we would need to pass in a resolver function to eloquent to help it resolve them.
This brought me to the
resolveRelationUsing
function (https://laravel.com/docs/12.x/eloquent-relationships#dynamic-relationships) that already exists and works:But it looks like its discoutraged(?) and it again feels a bit weird to move the relationship definition out of the package and into the application.
I also thought about using
morphMap()
and it works fine for morphed models but not other related types. I did a quick hack locally and added:to the
belongsTo
method in theHasRelationships.php
concern, and it worked fine with:but obviously thats not usable as is and would need a few changes to the core eloquent code, which im happy to try and PR if thats what people would like.
Which brings me here, and now I have a few thoughts/questions:
Beta Was this translation helpful? Give feedback.
All reactions