Laravel - Eloquent - Dynamically defined relationship

前端 未结 4 716
广开言路
广开言路 2021-01-13 09:24

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

相关标签:
4条回答
  • 2021-01-13 09:33

    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.

    0 讨论(0)
  • 2021-01-13 09:37

    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];
                    });
                }
            }
        }
    
    0 讨论(0)
  • 2021-01-13 09:41

    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);
    });
    
    0 讨论(0)
  • 2021-01-13 09:57

    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'); 
    });
    
    0 讨论(0)
提交回复
热议问题