从Eloquent模型中获取所有关系


Get all relationships from Eloquent model

拥有一个Eloquent模型,是否可以在运行时获取其所有关系及其类型?

我试着看了ReflectionClass,但找不到任何对这个场景有用的东西。

例如,如果我们有经典的Post模型,有没有一种方法可以提取这样的关系?

- belongsTo: User
- belongsToMany: Tag

要做到这一点,您必须知道模型中方法的名称,它们可能会有很大的变化;)

想法:

  • 如果你在方法中得到了一个模式,比如relUser/relTag,你可以过滤掉

  • 或者循环所有公共方法,看看是否弹出Relation对象(坏主意)

  • 您可以定义一个protected $relationMethods(注意:Laravel已经使用了$relations),它包含一个带有方法的数组。

调用Post->User()后,您将收到一个BelongsTo或一个来自Relation族的其他对象,因此您可以列出关系类型。

[编辑:评论后]

如果模型配备了受保护的$with = array(...);,则在加载记录后,您可以查看与$Model->getRelations()的加载关系。当没有加载记录时,这是不可能的,因为关系还没有被触及。

getRelations()/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php

但目前它没有出现在laravel.com/api的api中,这是因为我们有更新的版本

就像Rob说的那样。循环遍历每个公共方法并检查是否返回了关系是个坏主意。

Barryvdh在他非常受欢迎的Laravel-ide助手中使用了基于Regex的方法:https://github.com/barryvdh/laravel-ide-helper/blob/master/src/Console/ModelsCommand.php

您只需要在调用getPropertiesFromMethods后过滤您接收到的属性,如下所示(未经测试的示例):

class classSniffer{
    private $properties = [];
    //...
    public function getPropertiesFromMethods($model){
        //the copied code from the class above (ModelsCommand@getPropertiesFromMethods)
    }
    public function getRelationsFrom($model){
        $this->getPropertiesFromMethods($model);
        $relations = [];
        foreach($this->properties as $name => $property){
            $type = $property;
            $isRelation = strstr($property[$type], 'Illuminate'Database'Eloquent'Relations');
            if($isRelation){
                $relations[$name] = $property;
            }            
        }
        return $relations;
    }
}

有没有一种更清洁的方法可以做到这一点而不接触模型?

我认为我们必须等待PHP7(返回类型反射)或Taylor ^^的新反射服务

我最近一直在做同样的事情,我认为没有反射就无法有效地完成。但这有点资源密集型,所以我应用了一些缓存。需要进行的一项检查是验证返回类型和php7之前的版本,这只能通过实际执行每个方法来完成。因此,在进行检查之前,我还应用了一些逻辑来减少可能的候选人数量。

/**
 * Identify all relationships for a given model
 *
 * @param   object  $model  Model
 * @param   string  $heritage   A flag that indicates whether parent and/or child relationships should be included
 * @return  array
 */
public function getAllRelations('Illuminate'Database'Eloquent'Model $model = null, $heritage = 'all')
{
    $model = $model ?: $this;
    $modelName = get_class($model);
    $types = ['children' => 'Has', 'parents' => 'Belongs', 'all' => ''];
    $heritage = in_array($heritage, array_keys($types)) ? $heritage : 'all';
    if ('Illuminate'Support'Facades'Cache::has($modelName."_{$heritage}_relations")) {
        return 'Illuminate'Support'Facades'Cache::get($modelName."_{$heritage}_relations"); 
    }
    $reflectionClass = new 'ReflectionClass($model);
    $traits = $reflectionClass->getTraits();    // Use this to omit trait methods
    $traitMethodNames = [];
    foreach ($traits as $name => $trait) {
        $traitMethods = $trait->getMethods();
        foreach ($traitMethods as $traitMethod) {
            $traitMethodNames[] = $traitMethod->getName();
        }
    }
    // Checking the return value actually requires executing the method.  So use this to avoid infinite recursion.
    $currentMethod = collect(explode('::', __METHOD__))->last();
    $filter = $types[$heritage];
    $methods = $reflectionClass->getMethods('ReflectionMethod::IS_PUBLIC);  // The method must be public
    $methods = collect($methods)->filter(function ($method) use ($modelName, $traitMethodNames, $currentMethod) {
        $methodName = $method->getName();
        if (!in_array($methodName, $traitMethodNames)   //The method must not originate in a trait
            && strpos($methodName, '__') !== 0  //It must not be a magic method
            && $method->class === $modelName    //It must be in the self scope and not inherited
            && !$method->isStatic() //It must be in the this scope and not static
            && $methodName != $currentMethod    //It must not be an override of this one
        ) {
            $parameters = (new 'ReflectionMethod($modelName, $methodName))->getParameters();
            return collect($parameters)->filter(function ($parameter) {
                return !$parameter->isOptional();   // The method must have no required parameters
            })->isEmpty();  // If required parameters exist, this will be false and omit this method
        }
        return false;
    })->mapWithKeys(function ($method) use ($model, $filter) {
        $methodName = $method->getName();
        $relation = $model->$methodName();  //Must return a Relation child. This is why we only want to do this once
        if (is_subclass_of($relation, 'Illuminate'Database'Eloquent'Relations'Relation::class)) {
            $type = (new 'ReflectionClass($relation))->getShortName();  //If relation is of the desired heritage
            if (!$filter || strpos($type, $filter) === 0) {
                return [$methodName => get_class($relation->getRelated())]; // ['relationName'=>'relatedModelClass']
            }
        }
        return false;   // Remove elements reflecting methods that do not have the desired return type
    })->toArray();
    'Illuminate'Support'Facades'Cache::forever($modelName."_{$heritage}_relations", $methods);
    return $methods;
}

我知道这有点晚了,但我已经多次访问这个问题,所以我想分享我的观察结果,以帮助那些将来访问这个问题的人。

这是我用来从一个雄辩的模型类中提取关系的方法。

 /**
 * 
 * Returns all the relationship methods defined
 * in the provided model class with related 
 * model class and relation function name
 *
 * @param string $modelClass exampe: App'Models'Post
 * @return array $relattions array containing information about relationships
 */
protected function getModelRelationshipMethods(string $modelClass)
{
    //can define this at class level
    $relationshipMethods = [
        'hasMany',
        'hasOne',
        'belongsTo',
        'belongsToMany',
     ];
    $reflector = new ReflectionClass($modelClass);
    $path = $reflector->getFileName();
    //lines of the file
    $lines = file($path);
    $methods = $reflector->getMethods();
    $relations = [];
    foreach ($methods as $method) {
        //if its a concrete class method            
        if ($method->class == $modelClass) {
            $start = $method->getStartLine();
            $end = $method->getEndLine();
            //loop through lines of the method
            for($i = $start-1; $i<=$end-1; $i++) {
                // look for text between -> and ( assuming that its on one line
                preg_match('~'->(.*?)'(~', $lines[$i], $matches);
                // if there is a match
                if (count($matches)) {
                    //loop to check if the found text is in relationshipMethods list
                    foreach ($matches as $match) {
                        // if so add it to the output array
                        if (in_array($match, $relationshipMethods)) {
                            $relations[] = [
                                //function name of the relation definition
                                'method_name' => $method->name,
                                //type of relation
                                'relation' => $match,
                                //related Class name
                                'related' => (preg_match('/'.$match.''((.*?),/', $lines[$i], $related) == 1) ? $related[1] : null,
                            ];
                        }
                    }
                }
            }
        }
    }
    
    return $relations;
}

如果您对App/Post模型返回的$relationships进行dd()或dump(),则输出将类似于以下

^ array:3 [
  0 => array:3 [
    "method_name" => "user"
    "relation" => "belongsTo"
    "related" => "User::class"
  ]
  1 => array:3 [
    "method_name" => "tag"
    "relation" => "belongsToMany"
    "related" => "Tag::class"
  ]
  2 => array:3 [
    "method_name" => "comments"
    "relation" => "hasMany"
    "related" => "Comment::class"
  ]
]

我的项目也有同样的需求。我的解决方案是使用get_class函数来检查关系的类型。示例:

 $invoice = App'Models'Invoice::with('customer', 'products', 'invoiceProducts', 'invoiceProduct')->latest()->first();
    foreach ($invoice->getRelations() as $relation => $items) {
        $model = get_class($invoice->{$relation}());
        $type  = explode('''', $model);
        $type  = $type[count($type) - 1];
        $relations[] = ['name' => $relation, 'type' => $type];
    }
    dd($relations);

示例结果:

array:4 [▼
  0 => array:2 [▼
    "name" => "customer"
    "type" => "BelongsTo"
  ]
  1 => array:2 [▼
    "name" => "products"
    "type" => "BelongsToMany"
  ]
  2 => array:2 [▼
    "name" => "invoiceProducts"
    "type" => "HasMany"
  ]
  3 => array:2 [▼
    "name" => "invoiceProduct"
    "type" => "HasOne"
  ]
]

我需要它来复制一个模型项,包括关系

composer require adideas/laravel-get-relationship-eloquent-model

https://packagist.org/packages/adideas/laravel-get-relationship-eloquent-model

拉拉威尔得到关系所有雄辩的模型!

这样做不需要知道模型中方法的名称。有了这个包,您可以在运行时获得一个或多个Eloquent模型的所有关系及其类型