Laravel - Eloquent - Dynamically defined relationship

10,808

Solution 1

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);
});

Solution 2

As of laravel 7, dynamic relationship is officially supported. You can use the Model::resolveRelationUsing() method.

https://laravel.com/docs/7.x/eloquent-relationships#dynamic-relationships

Solution 3

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.

Solution 4

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'); 
});
Share:
10,808
Giedrius
Author by

Giedrius

Updated on July 29, 2022

Comments

  • Giedrius
    Giedrius almost 2 years

    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 actually changing its file? So does something like this exist:

    Page::createRelationship('banners', function(){
        $this->hasMany('banners');
    });
    

    Or something similar? As they are fetched using the magic methods anyway, perhaps I can add the relationship dynamically?

    Thanks!