Get all relationships from Eloquent model

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

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


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