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

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

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/Illuminate/Database/Eloquent/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -742,9 +742,13 @@ public function get($columns = ['*'])
$models = $builder->eagerLoadRelations($models);
}

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

if (Model::isAutoloadingRelationsGlobally()) {
$collection->enableRelationAutoload();
}

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

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

/**
* Load a relationship path with types if it is not already eager loaded.
*
* @return void
*/
public function loadMissingRelationWithTypes(array $path)
{
[$name, $class] = array_shift($path);

$this->filter(fn ($model) => ! is_null($model) && ! $model->relationLoaded($name) && $model::class === $class)
->load($name);

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

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

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

(new static($models))->loadMissingRelationWithTypes($path);
}

/**
* Load a relationship path if it is not already eager loaded.
*
Expand Down Expand Up @@ -314,6 +339,22 @@ public function loadMorphCount($relation, $relations)
return $this;
}

/**
* Enable relation autoload for the collection.
*
* @return $this
*/
public function enableRelationAutoload()
{
$callback = fn ($path) => $this->loadMissingRelationWithTypes($path);

$this
->filter(fn ($model) => ! $model->hasRelationAutoloadCallback())
->each(fn ($model) => $model->usingRelationAutoloadCallback($this, $callback));
litvinchuk marked this conversation as resolved.
Show resolved Hide resolved

return $this;
}

/**
* Determine if a key exists in the collection.
*
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 @@ -549,6 +549,10 @@ public function getRelationValue($key)
return;
}

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

if ($this->preventsLazyLoading) {
$this->handleLazyLoadingViolation($key);
}
Expand Down
142 changes: 142 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,20 @@ trait HasRelationships
*/
protected $touches = [];

/**
* The relationship autoload callback.
*
* @var ?Closure
*/
protected $relationAutoloadCallback = null;

/**
* The relationship autoload context.
*
* @var ?Collection
*/
protected $relationAutoloadContext = null;

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

/**
* Set relation autoload callback for model and its relations.
*
* @param mixed $context
* @param Closure $callback
* @return $this
*/
public function usingRelationAutoloadCallback($context, Closure $callback)
{
$this->relationAutoloadContext = $context;
$this->relationAutoloadCallback = $callback;

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

return $this;
}

/**
* Get relation autoload context.
*
* @return mixed
*/
public function getRelationAutoloadContext()
{
return $this->relationAutoloadContext;
}

/**
* Enable relation autoload for model and its relations if not already enabled.
*
* @return $this
*/
public function enableRelationAutoload()
{
if ($this->hasRelationAutoloadCallback()) {
return $this;
}

$collection = new Collection([$this]);

$this->usingRelationAutoloadCallback(
$collection,
fn ($path) => $collection->loadMissingRelationWithTypes($path)
);

return $this;
}

/**
* Check if relation autoload callback is set.
*
* @return bool
*/
public function hasRelationAutoloadCallback()
{
return ! is_null($this->relationAutoloadCallback);
}

/**
* Trigger relation autoload callback and check if relation is loaded.
*
* @param string $key
* @return bool
*/
protected function handleRelationAutoload($key)
{
if (! $this->hasRelationAutoloadCallback()) {
return false;
}

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

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

/**
* Trigger relation autoload callback.
*
* @param string $key
* @param array $keys
* @return void
*/
protected function triggerRelationAutoloadCallback($key, $keys)
{
call_user_func(
$this->relationAutoloadCallback,
array_merge([[$key, get_class($this)]], $keys)
);
}

/**
* Apply relation autoload callback to value.
*
* @param string $key
* @param mixed $values
* @return void
*/
protected function applyRelationAutoloadCallbackToValue($key, $values)
{
if (! $this->hasRelationAutoloadCallback() || ! $values) {
return;
}

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

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

$callback = fn (array $keys) => $this->triggerRelationAutoloadCallback($key, $keys);

foreach ($values as $item) {
$context = $item->getRelationAutoloadContext();

// check if relation autoload contexts are different
// to avoid circular relation autoload
if (is_null($context) || $context !== $this->relationAutoloadContext) {
$item->usingRelationAutoloadCallback($this->relationAutoloadContext, $callback);
}
}
}

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

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

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 @@ -173,6 +173,13 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt
*/
protected static $modelsShouldPreventLazyLoading = false;

/**
* Indicates whether relations should be automatically loaded on all models.
*
* @var bool
*/
protected static $modelsShouldGlobalAutoloadRelations = false;

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

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

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

/**
* Determine if relations autoload is enabled.
*
* @return bool
*/
public static function isAutoloadingRelationsGlobally()
{
return static::$modelsShouldGlobalAutoloadRelations;
}

/**
* 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