mpj
mpj

Reputation: 5367

Get all relationships from Eloquent model

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

Upvotes: 14

Views: 35025

Answers (7)

Technolo Jesus
Technolo Jesus

Reputation: 312

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',
    ];

    // ...
}

Upvotes: 0

aimme
aimme

Reputation: 6763

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

Upvotes: 1

Алексей
Алексей

Reputation: 1

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

Upvotes: 0

Muhammad Dyas Yaskur
Muhammad Dyas Yaskur

Reputation: 8088

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

Upvotes: 0

kmuenkel
kmuenkel

Reputation: 2789

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

Upvotes: 4

Simon Schneider
Simon Schneider

Reputation: 1236

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 ^^

Upvotes: 6

Rob Gordijn
Rob Gordijn

Reputation: 6511

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

Upvotes: 10

Related Questions