Load all relationships for a model

后端 未结 7 838
名媛妹妹
名媛妹妹 2021-01-01 23:58

Usually to eager load a relationship I would do something like this:

Model::with(\'foo\', \'bar\', \'baz\')...

A solution might be to set

相关标签:
7条回答
  • 2021-01-02 00:20

    No it's not, at least not without some additional work, because your model doesn't know which relations it supports until they are actually loaded.

    I had this problem in one of my own Laravel packages. There is no way to get a list of the relations of a model with Laravel. It's pretty obvious though if you look at how they are defined. Simple functions which return a Relation object. You can't even get the return type of a function with php's reflection classes, so there is no way to distinguish between a relation function and any other function.

    What you can do to make it easier is defining a function that adds all the relationships. To do this you can use eloquents query scopes (Thanks to Jarek Tkaczyk for mentioning it in the comments).

    public function scopeWithAll($query) 
    {
        $query->with('foo', 'bar', 'baz');
    }
    

    Using scopes instead of static functions allows you to not only use your function directly on the model but for example also when chaining query builder methods like where in any order:

    Model::where('something', 'Lorem ipsum dolor')->withAll()->where('somethingelse', '>', 10)->get();
    

    Alternatives to get supported relations

    Although Laravel does not support something like that out of the box you can allways add it yourself.

    Annotations

    I used annotations to determine if a function is a relation or not in my package mentioned above. Annotations are not officially part of php but a lot of people use doc blocks to simulate them. Laravel 5 is going to use annotations in its route definitions too so I figuered it not to be bad practice in this case. The advantage is, that you don't need to maintain a seperate list of supported relations.

    Add an annotation to each of your relations:

    /**
     * @Relation
     */
    public function foo() 
    {
        return $this->belongsTo('Foo');
    }
    

    And write a function that parses the doc blocks of all methods in the model and returns the name. You can do this in a model or in a parent class:

    public static function getSupportedRelations() 
    {
        $relations = [];
        $reflextionClass = new ReflectionClass(get_called_class());
    
        foreach($reflextionClass->getMethods() as $method) 
        {
            $doc = $method->getDocComment();
    
            if($doc && strpos($doc, '@Relation') !== false) 
            {
                $relations[] = $method->getName();
            }
        }
    
        return $relations;
    }
    

    And then just use them in your withAll function:

    public function scopeWithAll($query) 
    {
        $query->with($this->getSupportedRelations());
    }
    

    Some like annotations in php and some don't. I like it for this simple use case.

    Array of supported relations

    You can also maintain an array of all the supported relations. This however needs you to always sync it with the available relations which, especially if there are multiple developers involved, is not allways that easy.

    protected $supportedRelations = ['foo','bar', 'baz'];
    

    And then just use them in your withAll function:

    public function scopeWithAll($query) 
    {
        return $query->with($this->supportedRelations);
    }
    

    You can of course also override with like lukasgeiter mentioned in his answer. This seems cleaner than using withAll. If you use annotations or a config array however is a matter of opinion.

    0 讨论(0)
  • 2021-01-02 00:22

    I wouldn't use static methods like suggested since... it's Eloquent ;) Just leverage what it already offers - a scope.

    Of course it won't do it for you (the main question), however this is definitely the way to go:

    // SomeModel
    public function scopeWithAll($query)
    {
        $query->with([ ... all relations here ... ]); 
        // or store them in protected variable - whatever you prefer
        // the latter would be the way if you want to have the method
        // in your BaseModel. Then simply define it as [] there and use:
        // $query->with($this->allRelations); 
    }
    

    This way you're free to use this as you like:

    // static-like
    SomeModel::withAll()->get();
    
    // dynamically on the eloquent Builder
    SomeModel::query()->withAll()->get();
    SomeModel::where('something', 'some value')->withAll()->get();
    

    Also, in fact you can let Eloquent do it for you, just like Doctrine does - using doctrine/annotations and DocBlocks. You could do something like this:

    // SomeModel
    
    /**
     * @Eloquent\Relation
     */
    public function someRelation()
    {
      return $this->hasMany(..);
    }
    

    It's a bit too long story to include it here, so learn how it works: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/annotations-reference.html

    0 讨论(0)
  • 2021-01-02 00:26

    Since i've met with a similar problem, and found a good solution that isn't described here and doesn't require filling some custom arrays or whatever, i'll post it for the future.

    What i do, is first create a trait, called RelationsManager:

    trait RelationsManager
    {
        protected static $relationsList = [];
    
        protected static $relationsInitialized = false;
    
        protected static $relationClasses = [
            HasOne::class,
            HasMany::class,
            BelongsTo::class,
            BelongsToMany::class
        ];
    
        public static function getAllRelations($type = null) : array
        {
            if (!self::$relationsInitialized) {
                self::initAllRelations();
            }
    
            return $type ? (self::$relationsList[$type] ?? []) : self::$relationsList;
        }
    
        protected static function initAllRelations()
        {
            self::$relationsInitialized = true;
    
            $reflect = new ReflectionClass(static::class);
    
            foreach($reflect->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
                /** @var ReflectionMethod $method */
                if ($method->hasReturnType() && in_array((string)$method->getReturnType(), self::$relationClasses)) {
                    self::$relationsList[(string)$method->getReturnType()][] = $method->getName();
                }
            }
        }
    
        public static function withAll() : Builder
        {
            $relations = array_flatten(static::getAllRelations());
    
            return $relations ? self::with($relations) : self::query();
        }
    }
    

    Now you can use it with any class, like -

    class Project extends Model
    {
        use RelationsManager;
    
        //... some relations
    
    }
    

    and then when you need to fetch them from the database:

    $projects = Project::withAll()->get();
    

    Some notes - my example relation classes list doesn't include morph relations, so if you want to get them as well - you need to add them to $relationClasses variable. Also, this solution only works with PHP 7.

    0 讨论(0)
  • 2021-01-02 00:26

    You could attempt to detect the methods specific to your model using reflection, such as:

    $base_methods = get_class_methods('Illuminate\Database\Eloquent\Model');
    $model_methods = get_class_methods(get_class($entry));
    $maybe_relations = array_diff($model_methods, $base_methods);
    
    dd($maybe_relations);
    

    Then attempt to load each in a well-controlled try/catch. The Model class of Laravel has a load and a loadMissing methods for eager loading.

    See the api reference.

    0 讨论(0)
  • 2021-01-02 00:36

    You can't have a dynamic loading of relationships for a certain model. you need to tell the model which relations to support.

    0 讨论(0)
  • 2021-01-02 00:40

    There's no way to know what all the relations are without specifying them yourself. How the other answers posted are good, but I wanted to add a few things.

    Base Model

    I kind of have the feeling that you want to do this in multiple models, so at first I'd create a BaseModel if you haven't already.

    class BaseModel extends Eloquent {
        public $allRelations = array();
    }
    

    "Config" array

    Instead of hard coding the relationships into a method I suggest you use a member variable. As you can see above I already added $allRelations. Be aware that you can't name it $relations since Laravel already uses that internally.

    Override with()

    Since you wanted with(*) you can do that too. Add this to the BaseModel

    public static function with($relations){
        $instance = new static;
        if($relations == '*'){
            $relations = $instance->allRelations;
        }
        else if(is_string($relations)){
            $relations = func_get_args();
        }
        return $instance->newQuery()->with($relations);
    }
    

    (By the way, some parts of this function come from the original Model class)

    Usage

    class MyModel extends BaseModel {
        public $allRelations = array('foo', 'bar');
    }
    
    MyModel::with('*')->get();
    
    0 讨论(0)
提交回复
热议问题