Skip to content

[12.x] Added Automatic Relation Loading (Eager Loading) Feature #53655

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Apr 8, 2025
10 changes: 7 additions & 3 deletions src/Illuminate/Database/Eloquent/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -854,9 +854,13 @@ public function get($columns = ['*'])
$models = $builder->eagerLoadRelations($models);
}

return $this->applyAfterQueryCallbacks(
$builder->getModel()->newCollection($models)
);
$collection = $builder->getModel()->newCollection($models);

if (Model::isAutomaticallyEagerLoadingRelationships()) {
$collection->withRelationshipAutoloading();
}

return $this->applyAfterQueryCallbacks($collection);
}

/**
Expand Down
47 changes: 47 additions & 0 deletions src/Illuminate/Database/Eloquent/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,35 @@ public function loadMissing($relations)
return $this;
}

/**
* Load a relationship path for models of the given type if it is not already eager loaded.
*
* @param array<int, <string, class-string>> $tuples
* @return void
*/
public function loadMissingRelationshipChain(array $tuples)
{
[$relation, $class] = array_shift($tuples);

$this->filter(function ($model) use ($relation, $class) {
return ! is_null($model) &&
! $model->relationLoaded($relation) &&
$model::class === $class;
})->load($relation);

if (empty($tuples)) {
return;
}

$models = $this->pluck($relation)->whereNotNull();

if ($models->first() instanceof BaseCollection) {
$models = $models->collapse();
}

(new static($models))->loadMissingRelationshipChain($tuples);
}

/**
* Load a relationship path if it is not already eager loaded.
*
Expand Down Expand Up @@ -721,6 +750,24 @@ protected function duplicateComparator($strict)
return fn ($a, $b) => $a->is($b);
}

/**
* Enable relationship autoloading for all models in this collection.
*
* @return $this
*/
public function withRelationshipAutoloading()
{
$callback = fn ($tuples) => $this->loadMissingRelationshipChain($tuples);

foreach ($this as $model) {
if (! $model->hasRelationAutoloadCallback()) {
$model->autoloadRelationsUsing($callback);
}
}

return $this;
}

/**
* Get the type of the entities being queued.
*
Expand Down
4 changes: 4 additions & 0 deletions src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,10 @@ public function getRelationValue($key)
return;
}

if ($this->attemptToAutoloadRelation($key)) {
return $this->relations[$key];
}

if ($this->preventsLazyLoading) {
$this->handleLazyLoadingViolation($key);
}
Expand Down
100 changes: 100 additions & 0 deletions src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ trait HasRelationships
*/
protected $touches = [];

/**
* The relationship autoloader callback.
*
* @var \Closure|null
*/
protected $relationAutoloadCallback = null;

/**
* The many to many relationship methods.
*
Expand Down Expand Up @@ -92,6 +99,97 @@ public static function resolveRelationUsing($name, Closure $callback)
);
}

/**
* Determine if a relationship autoloader callback has been defined.
*
* @return bool
*/
public function hasRelationAutoloadCallback()
{
return ! is_null($this->relationAutoloadCallback);
}

/**
* Define an automatic relationship autoloader callback for this model and its relations.
*
* @param \Closure $callback
* @param mixed $context
* @return $this
*/
public function autoloadRelationsUsing(Closure $callback, $context = null)
{
$this->relationAutoloadCallback = $callback;

foreach ($this->relations as $key => $value) {
$this->propagateRelationAutoloadCallbackToRelation($key, $value, $context);
}

return $this;
}

/**
* Attempt to autoload the given relationship using the autoload callback.
*
* @param string $key
* @return bool
*/
protected function attemptToAutoloadRelation($key)
{
if (! $this->hasRelationAutoloadCallback()) {
return false;
}

$this->invokeRelationAutoloadCallbackFor($key, []);

return $this->relationLoaded($key);
}

/**
* Invoke the relationship autoloader callback for the given relationships.
*
* @param string $key
* @param array $tuples
* @return void
*/
protected function invokeRelationAutoloadCallbackFor($key, $tuples)
{
$tuples = array_merge([[$key, get_class($this)]], $tuples);

call_user_func($this->relationAutoloadCallback, $tuples);
}

/**
* Propagate the relationship autoloader callback to the given related models.
*
* @param string $key
* @param mixed $values
* @param mixed $context
* @return void
*/
protected function propagateRelationAutoloadCallbackToRelation($key, $models, $context = null)
{
if (! $this->hasRelationAutoloadCallback() || ! $models) {
return;
}

if ($models instanceof Model) {
$models = [$models];
}

if (! is_iterable($models)) {
return;
}

$callback = fn (array $tuples) => $this->invokeRelationAutoloadCallbackFor($key, $tuples);

foreach ($models as $model) {
// Check if relation autoload contexts are different to avoid circular relation autoload...
if (is_null($context) || $context !== $model) {
$model->autoloadRelationsUsing($callback, $context);
}
}
}

/**
* Define a one-to-one relationship.
*
Expand Down Expand Up @@ -988,6 +1086,8 @@ public function setRelation($relation, $value)
{
$this->relations[$relation] = $value;

$this->propagateRelationAutoloadCallbackToRelation($relation, $value, $this);

return $this;
}

Expand Down
28 changes: 28 additions & 0 deletions src/Illuminate/Database/Eloquent/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,13 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt
*/
protected static $modelsShouldPreventLazyLoading = false;

/**
* Indicates whether relations should be automatically loaded on all models when they are accessed.
*
* @var bool
*/
protected static $modelsShouldAutomaticallyEagerLoadRelationships = false;

/**
* The callback that is responsible for handling lazy loading violations.
*
Expand Down Expand Up @@ -446,6 +453,17 @@ public static function preventLazyLoading($value = true)
static::$modelsShouldPreventLazyLoading = $value;
}

/**
* Determine if model relationships should be automatically eager loaded when accessed.
*
* @param bool $value
* @return void
*/
public static function automaticallyEagerLoadRelationships($value = true)
{
static::$modelsShouldAutomaticallyEagerLoadRelationships = $value;
}

/**
* Register a callback that is responsible for handling lazy loading violations.
*
Expand Down Expand Up @@ -2231,6 +2249,16 @@ public static function preventsLazyLoading()
return static::$modelsShouldPreventLazyLoading;
}

/**
* Determine if relationships are being automatically eager loaded when accessed.
*
* @return bool
*/
public static function isAutomaticallyEagerLoadingRelationships()
{
return static::$modelsShouldAutomaticallyEagerLoadRelationships;
}

/**
* Determine if discarding guarded attribute fills is disabled.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public function testModelsAreProperlyMatchedToParents()
$model1->shouldReceive('getAttribute')->with('foo')->passthru();
$model1->shouldReceive('hasGetMutator')->andReturn(false);
$model1->shouldReceive('hasAttributeMutator')->andReturn(false);
$model1->shouldReceive('hasRelationAutoloadCallback')->andReturn(false);
$model1->shouldReceive('getCasts')->andReturn([]);
$model1->shouldReceive('getRelationValue', 'relationLoaded', 'relationResolver', 'setRelation', 'isRelation')->passthru();

Expand All @@ -36,6 +37,7 @@ public function testModelsAreProperlyMatchedToParents()
$model2->shouldReceive('getAttribute')->with('foo')->passthru();
$model2->shouldReceive('hasGetMutator')->andReturn(false);
$model2->shouldReceive('hasAttributeMutator')->andReturn(false);
$model2->shouldReceive('hasRelationAutoloadCallback')->andReturn(false);
$model2->shouldReceive('getCasts')->andReturn([]);
$model2->shouldReceive('getRelationValue', 'relationLoaded', 'relationResolver', 'setRelation', 'isRelation')->passthru();

Expand Down
Loading
Loading