12

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!

9
  • use polymorphic relationships and traits that will accomplish what you want have a look at that in the laravel documentations Commented Sep 12, 2016 at 11:39
  • If I'm not wrong you still have to define those in the original Page model? I am looking for a way to add that relationship without the need of editing the file Commented Sep 12, 2016 at 11:42
  • I can see there is a setRelation method on the Model class, however, it is not static, so callable only on instance. Is there something similar I can use so it automatically sets the relationship on all created instanced? Commented Sep 12, 2016 at 12:29
  • if thats the case cant you do a new static itll work the same way as if it was a static for your purpose Commented Sep 12, 2016 at 13:08
  • Yes, I could do it for an instance. However, I need to define the relationship in a whole Model's scope, so whenever a new instance is created, $model->banners() is already accessible. So far I can't see a solution for this, so working on a solution myself. Commented Sep 12, 2016 at 13:18

5 Answers 5

19

Update: It's available in laravel now using Model::resolveRelationUsing() since Laravel 7.

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);
});
Sign up to request clarification or add additional context in comments.

3 Comments

I ended up doing the same for my project, worked very well! Good job and thanks.
This functionality is now included since Laravel 8.
I think this is good but in some cases, you don't have access to the model MyModel because it lives outside, that's why I think the best approach is to create a trait in the package and use it in your model.
4

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

Comments

2

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

1 Comment

This is great. You do however lose the ability to query the relationship with an accessor. i.e Model::first()->yourRelation you instead have to use Model::first()->yourRelation()->get()
1

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.

5 Comments

Thanks for the extensive answer. I can see how either of the solutions could be used, but if I'm not mistaken both would require the Page model class to be edited? As my system is completely modular, I am trying to find a way of modules extending other modules without over-complicating the Models themselves.
You have to modify it to make it capable of handling the dynamic relationship functionality, as it isn't native implemented, but you don't have to change it later. The right approach of a modular system is having a inheritance model structure, so the Banner model extends PageElement model, and you build the relationship between Page and PageElement. Anyways, making things that modular will complicate your models.
I will either have my own extension of the Model class that gets extended by all models to have the custom functionality, or perhaps add the dynamic relations functionality into laravel framework and submit pull request, in case more people would find that useful. Will post an answer once I found a definite solution.
Ok. Can you tell me more about the modular functionality that you want and why you need it? I'm thinking in a couple of situations that can bring you to that design needs and all of them got a solution with version update or code edit.
Basically all functionality on the system will be through modules. Every instance of the system we deploy will have different modules enabled, as well as some changes to some modules. All the modules will be version controlled on the master system, however, there will be options to edit them per-instance. The modules will be developed by multiple developers, so I am trying to find a solution, that would allow me installing different modules under different systems with the least knock-on effect on the rest of the system.
1

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

1 Comment

Hello @kdjion have you found any performance issues with this solution? I would like to implement something similar but with a lot of tables/models and would like to know if the system could become slower.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.