laraveleloquentbelongs-to

Why does Laravel load belongsTo withDefault values even if there is relation set?


I have a model, Project, that I am eager loading relations in the Http/Controller.

The problem is that the withDefault closure on the belongsTo Relation in the model always fires, regardless of if there is an actual relation to eager load, resulting in many pointless queries and strange side effects.

This doesn't seem like it should be expected behaviour - is there any way to stop it calling withDefault() unless it actually needs to?

To clarify:

return $project->loadMissing('address');

From the model:

    public function address(): BelongsTo
    {
        return $this
            ->belongsTo(Address::class, 'address_id')
            ->withDefault(function() {
                // This will always trigger
            });
    }

withDefault triggers always, regardless of the state of the actual relation.


Solution

  • Looks like it is the default behavior.

    The callback function inside withDefault function will get executed every time Laravel sets the BelongsTo relation of a model.

    Take a look at the initRelation function of laravel's core BelongsTo class

    public function initRelation(array $models, $relation)
    {
        foreach ($models as $model) {
            $model->setRelation($relation, $this->getDefaultFor($model));
        }
    
        return $models;
    }
    

    And if you look at the getDefaultFor and withDefault functions here

    /**
     * Return a new model instance in case the relationship does not exist.
     *
     * @param  \Closure|array|bool  $callback
     * @return $this
     */
    public function withDefault($callback = true)
    {
        $this->withDefault = $callback;
    
        return $this;
    }
    
    /**
     * Get the default value for this relation.
     *
     * @param  \Illuminate\Database\Eloquent\Model  $parent
     * @return \Illuminate\Database\Eloquent\Model|null
     */
    protected function getDefaultFor(Model $parent)
    {
        if (! $this->withDefault) {
            return;
        }
    
        $instance = $this->newRelatedInstanceFor($parent);
    
        if (is_callable($this->withDefault)) {
            return call_user_func($this->withDefault, $instance, $parent) ?: $instance;
        }
    
        if (is_array($this->withDefault)) {
            $instance->forceFill($this->withDefault);
        }
    
        return $instance;
    }
    

    You will observe that if $this->withDefault is a callable, it will always be executed; there isn't a direct way to conditionally trigger its execution.

    I would recommend avoiding running heavy tasks like database queries inside that function, as it could lead to performance problems, especially when processing a large amount of data.