Get all relationships from Eloquent model
Asked Answered
C

7

14

Having one Eloquent model, is it possible to get all its relationships and their type at runtime?

I've tried taking a look at ReflectionClass, but I couldn't find anything useful for this scenario.

For example, if we have the classic Post model, is there a way to extract relationships like this?

- belongsTo: User
- belongsToMany: Tag
Cheryle answered 2/12, 2013 at 18:2 Comment(0)
S
10

To accomplish this, you will have you know the names of the methods within the model - and they can vary a lot ;)

Thoughts:

  • if you got a pattern in the method, like relUser / relTag, you can filter them out

  • or loop over all public methods, see if a Relation object pops up (bad idea)

  • you can define a protected $relationMethods (note: Laravel already uses $relations) which holds an array with method.

After calling Post->User() you will receive a BelongsTo or 1 of the other objects from the Relation family, so you can do you listing for the type of relation.

[edit: after comments]

If the models are equipped with a protected $with = array(...); then you are able to look into the loaded relations with $Model->getRelations() after a record is loaded. This is not possible when no record is loaded, since the relations aren't touched yet.

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

But currently it doesn't show up in the api at laravel.com/api - this is because we got newer version

Spindlelegs answered 2/12, 2013 at 21:20 Comment(5)
The point behind this question is to make a package for other developers which needs to know about relationships. I don't want to force them to modify their classes just to adapt to the package. If there's no other option, I will end up using something similar to the internal $relations, as you pointed out.Cheryle
If the models are equipped with a protected $with = array(...); then you are able to look into the loaded relations with $Model->getRelations() after a record is loaded. This is not possible when no record is loaded, since the relations aren't touched yet.Spindlelegs
Where is getRelations() documented? I can only see getRelation($relation), but you have to pass the relationship name.Cheryle
it is in the Model.php (/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php) - But it is strange that is doesn't show up in the api at laravel.com/api - maybe I got a newer version.Spindlelegs
You're right, I got it too. If you add that to the answer I can mark it as valid.Cheryle
A
6

Like Rob stated. It is a bad idea to loop through every public method and check out if a relation is returned.

Barryvdh uses a Regex based approach in his very popular Laravel-ide-helper: https://github.com/barryvdh/laravel-ide-helper/blob/master/src/Console/ModelsCommand.php

You just have to filter the properties you receive after calling getPropertiesFromMethods like this (untested example):

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

Is there a cleaner way of doing that without touching the Models?

I think we have to wait for PHP7 (Return Type Reflections) or for a new Reflection Service from Taylor ^^

Aesthete answered 10/9, 2015 at 13:48 Comment(0)
R
4

I've been working on the same thing lately, and I don't think it can effectively be done without Reflection. But this is a little resource-intensive, so I've applied some caching. One check that's needed is to verify the return type, and pre-php7, that can only be done by actually executing each method. So I've also applied some logic that reduces the number of likely candidates before running that check.

/**
 * 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;
}
Rebate answered 27/12, 2016 at 18:55 Comment(0)
A
1

I know its bit late, but I have been visiting this question multiple times so thought to share my observations to help those who visits this question in future.

Here is the method i used to extract the relationships from an eloquent model class.

 /**
 * 
 * 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;
}

If you dd() or dump() the returned $relations for the App/Post model, The output will be something like this

^ 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"
  ]
]
Apperceive answered 8/9, 2022 at 15:22 Comment(0)
V
0

I have the same needs on my project. My solution is using get_class function to check type of relation. example:

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

example result:

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"
  ]
]

I need it for duplicate an model item including the relation

Viewfinder answered 23/11, 2020 at 15:48 Comment(0)
A
0
composer require adideas/laravel-get-relationship-eloquent-model

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

Laravel get relationship all eloquent models!

You don't need to know the names of the methods in the model to do this. Having one or many Eloquent models, thanks to this package, you can get all of its relationships and their type at runtime

Alfred answered 3/5, 2021 at 14:47 Comment(1)
Welcome to Stack Overflow! Please be careful with linking to your own library, you don't want to be seen as a spammer. You should make clear in your answer that you're affiliated with that library.Brahma
C
0

Or just make a map of what relations it has and save your processing power for something more important.

Example map:


class Continent extends Model
{
    // ...

    public static array $relationships = [
        'countries',
        'historicalSites',
    ];

    // ...
}
Criticism answered 17/4, 2024 at 13:52 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.