Skip to content
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