Is it possible to set a model\'s relationship dynamically? For example, I have model Page
, and I want to add relationship banners()
to it without a
You have to have something in mind, an Eloquent relationship is a model of a relational database relatioship (i.e. MySQL). So, I came with two approaches.
The good
If you want to achieve a full-featured Eloquent relationship with indexes and foreing keys in the database, you probably want to alter the SQL tables dynamically.
For example, supossing you have all your models created and don't want to create them dynamically, you only have to alter the Page table, add a new field called "banner_id", index it and reference to "banner_id" field on Banner table.
Then you have to write down and support for the RDBMS you will work with. After that, you may want to include support for migrations. If it's the case, you may store in the database these table alterations for further rollbacks.
Now, for the Eloquent support part, you may look at Eloquent Model Class.
See that, for each kind of relation, you have a subyacent model (all can be found here, which is in fact what you are returning in relatioship methods:
public function hasMany($related, $foreignKey = null, $localKey = null)
{
$foreignKey = $foreignKey ?: $this->getForeignKey();
$instance = new $related;
$localKey = $localKey ?: $this->getKeyName();
return new HasMany($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey);
}
So you have to define a method in your model that accepts the type of relation and the model, creates a new HasMany (in case hasMany was the desired relationship) instance, and then returns it.
It's little bit complicated, and so you can use:
The easy
You can create a intermediate model (i.e. PageRelationship) that stores all the relationships between Page and other Models. A possible table schema could be:
+-------------+---------+------------------+-------------+
| relation_id | page_id | foreign_model_id | model_class |
+-------------+---------+------------------+-------------+
| 1 | 2 | 225 | Banner |
| 2 | 2 | 223 | Banner |
| 3 | 2 | 12 | Button |
+-------------+---------+------------------+-------------+
Then you can retrieve all dynamically relative models to a given Page. The problem here is that you don't actually have any real RDBMS relation between Models and Pages, so you may have to make multiple and heavy queries for loading related Models, and, what's worse, you have to manage yourself database consistency (i.e., deleting or updating the "225" Banner should also remove or update the row in page_relationship_table). Reverse relationships will be a headache too.
Conclusion
If the project is big, it depends on that, and you can't make a model that implements other models via inheritance or so, you should use the good approach. Otherwise, you should rethink you app design and then decide to choose or not second approach.
Just in case anyone is looking for a Laravel 8 answer:
Let's say I define my relationships in a single method of my model:
public function relationships()
{
return [
'user' => $this->belongsTo(User::class, 'user_id'),
];
}
Now, in my app service provider, I can use the resolveRelationUsing
method. I've done this by iterating through the models folder and checking all models which contain the aforementioned method:
foreach ((new Filesystem)->allFiles(app_path('Models')) as $file) {
$namespace = 'App\\Models\\' . str_replace(['/', '.php'], ['\\', ''], $file->getRelativePathname());
$class = app($namespace);
if (method_exists($class, 'relationships')) {
foreach ($class->relationships() as $key => $relationship) {
$class->resolveRelationUsing($key, function () use ($class, $key) {
return $class->relationships()[$key];
});
}
}
}
I've added a package for this i-rocky/eloquent-dynamic-relation
In case anyone still looking for a solution , here is one. If you think it's a bad idea, let me know.
trait HasDynamicRelation
{
/**
* Store the relations
*
* @var array
*/
private static $dynamic_relations = [];
/**
* Add a new relation
*
* @param $name
* @param $closure
*/
public static function addDynamicRelation($name, $closure)
{
static::$dynamic_relations[$name] = $closure;
}
/**
* Determine if a relation exists in dynamic relationships list
*
* @param $name
*
* @return bool
*/
public static function hasDynamicRelation($name)
{
return array_key_exists($name, static::$dynamic_relations);
}
/**
* If the key exists in relations then
* return call to relation or else
* return the call to the parent
*
* @param $name
*
* @return mixed
*/
public function __get($name)
{
if (static::hasDynamicRelation($name)) {
// check the cache first
if ($this->relationLoaded($name)) {
return $this->relations[$name];
}
// load the relationship
return $this->getRelationshipFromMethod($name);
}
return parent::__get($name);
}
/**
* If the method exists in relations then
* return the relation or else
* return the call to the parent
*
* @param $name
* @param $arguments
*
* @return mixed
*/
public function __call($name, $arguments)
{
if (static::hasDynamicRelation($name)) {
return call_user_func(static::$dynamic_relations[$name], $this);
}
return parent::__call($name, $arguments);
}
}
Add this trait in your model as following
class MyModel extends Model {
use HasDynamicRelation;
}
Now you can use the following method to add new relationships
MyModel::addDynamicRelation('some_relation', function(MyModel $model) {
return $model->hasMany(SomeRelatedModel::class);
});
you can use macro
call for your dynamic relation like this:
you should write this code in your service provider boot method.
\Illuminate\Database\Eloquent\Builder::macro('yourRelation', function () {
return $this->getModel()->belongsTo('class');
});