From a5e5aef66ad4a8478d80751959a2d8eefb1818b8 Mon Sep 17 00:00:00 2001 From: Sergiy Litvinchuk Date: Sun, 24 Nov 2024 21:44:49 +0200 Subject: [PATCH 01/13] Add eloquent relation autoload feature --- src/Illuminate/Database/Eloquent/Builder.php | 10 +- .../Database/Eloquent/Collection.php | 41 ++++ .../Eloquent/Concerns/HasAttributes.php | 4 + .../Eloquent/Concerns/HasRelationships.php | 142 ++++++++++++++ src/Illuminate/Database/Eloquent/Model.php | 28 +++ .../EloquentModelRelationAutoloadTest.php | 185 ++++++++++++++++++ 6 files changed, 407 insertions(+), 3 deletions(-) create mode 100644 tests/Integration/Database/EloquentModelRelationAutoloadTest.php diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 4e22b9ae9fa2..9acdf7b1d85e 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -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::alwaysAutoloadsRelations()) { + $collection->enableRelationAutoload(); + } + + return $this->applyAfterQueryCallbacks($collection); } /** diff --git a/src/Illuminate/Database/Eloquent/Collection.php b/src/Illuminate/Database/Eloquent/Collection.php index d030a3bc93e9..1447ac68cc32 100755 --- a/src/Illuminate/Database/Eloquent/Collection.php +++ b/src/Illuminate/Database/Eloquent/Collection.php @@ -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) + { + list($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. * @@ -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)); + + return $this; + } + /** * Determine if a key exists in the collection. * diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index 8c06e658e47e..810c2e28d399 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -551,6 +551,10 @@ public function getRelationValue($key) return; } + if ($this->handleRelationAutoload($key)) { + return $this->relations[$key]; + } + if ($this->preventsLazyLoading) { $this->handleLazyLoadingViolation($key); } diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php index df89155e9382..4367b6ea08c6 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php @@ -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. * @@ -92,6 +106,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. * @@ -988,6 +1128,8 @@ public function setRelation($relation, $value) { $this->relations[$relation] = $value; + $this->applyRelationAutoloadCallbackToValue($relation, $value); + return $this; } diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index f730ec3cd24c..2bc9a46e7d07 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -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. + * + * @var bool + */ + protected static $modelsShouldAlwaysAutoloadRelations = false; + /** * The callback that is responsible for handling lazy loading violations. * @@ -446,6 +453,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 alwaysAutoloadRelations($value = true) + { + static::$modelsShouldAlwaysAutoloadRelations = $value; + } + /** * Register a callback that is responsible for handling lazy loading violations. * @@ -2231,6 +2249,16 @@ public static function preventsLazyLoading() return static::$modelsShouldPreventLazyLoading; } + /** + * Determine if relations autoload is enabled. + * + * @return bool + */ + public static function alwaysAutoloadsRelations() + { + return static::$modelsShouldAlwaysAutoloadRelations; + } + /** * Determine if discarding guarded attribute fills is disabled. * diff --git a/tests/Integration/Database/EloquentModelRelationAutoloadTest.php b/tests/Integration/Database/EloquentModelRelationAutoloadTest.php new file mode 100644 index 000000000000..3b88bb8f7285 --- /dev/null +++ b/tests/Integration/Database/EloquentModelRelationAutoloadTest.php @@ -0,0 +1,185 @@ +increments('id'); + }); + + Schema::create('videos', function (Blueprint $table) { + $table->increments('id'); + }); + + Schema::create('comments', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('parent_id')->nullable(); + $table->morphs('commentable'); + }); + + Schema::create('likes', function (Blueprint $table) { + $table->increments('id'); + $table->morphs('likeable'); + }); + } + + public function testRelationAutoload() + { + $post1 = Post::create(); + $comment1 = $post1->comments()->create(['parent_id' => null]); + $comment2 = $post1->comments()->create(['parent_id' => $comment1->id]); + $comment2->likes()->create(); + $comment2->likes()->create(); + + $post2 = Post::create(); + $comment3 = $post2->comments()->create(['parent_id' => null]); + $comment3->likes()->create(); + + $posts = Post::get(); + + DB::enableQueryLog(); + + $likes = []; + + $posts->enableRelationAutoload(); + + foreach ($posts as $post) { + foreach ($post->comments as $comment) { + $likes = array_merge($likes, $comment->likes->all()); + } + } + + $this->assertCount(2, DB::getQueryLog()); + $this->assertCount(3, $likes); + $this->assertTrue($posts[0]->comments[0]->relationLoaded('likes')); + } + + public function testRelationAutoloadVariousNestedMorphRelations() + { + tap(Post::create(), function ($post) { + $post->likes()->create(); + $post->comments()->create(); + tap($post->comments()->create(), function ($comment) { + $comment->likes()->create(); + $comment->likes()->create(); + }); + }); + + tap(Post::create(), function ($post) { + $post->likes()->create(); + tap($post->comments()->create(), function ($comment) { + $comment->likes()->create(); + }); + }); + + tap(Video::create(), function ($video) { + tap($video->comments()->create(), function ($comment) { + $comment->likes()->create(); + }); + }); + + tap(Video::create(), function ($video) { + tap($video->comments()->create(), function ($comment) { + $comment->likes()->create(); + }); + }); + + Post::alwaysAutoloadRelations(); + + $likes = Like::get(); + + DB::enableQueryLog(); + + $videos = []; + $videoLike = null; + +// $likes->enableRelationAutoload(); + + foreach ($likes as $like) { + $likeable = $like->likeable; + + if (($likeable instanceof Comment) && ($likeable->commentable instanceof Video)) { + $videos[] = $likeable->commentable; + $videoLike = $like; + } + } + + $this->assertCount(4, DB::getQueryLog()); + $this->assertCount(2, $videos); + $this->assertTrue($videoLike->relationLoaded('likeable')); + $this->assertTrue($videoLike->likeable->relationLoaded('commentable')); + } +} + +class Comment extends Model +{ + public $timestamps = false; + + protected $guarded = []; + + public function parent() + { + return $this->belongsTo(self::class); + } + + public function likes() + { + return $this->morphMany(Like::class, 'likeable'); + } + + public function commentable() + { + return $this->morphTo(); + } +} + +class Post extends Model +{ + public $timestamps = false; + + public function comments() + { + return $this->morphMany(Comment::class, 'commentable'); + } + + public function likes() + { + return $this->morphMany(Like::class, 'likeable'); + } +} + +class Video extends Model +{ + public $timestamps = false; + + public function comments() + { + return $this->morphMany(Comment::class, 'commentable'); + } + + public function likes() + { + return $this->morphMany(Like::class, 'likeable'); + } +} + +class Like extends Model +{ + public $timestamps = false; + + protected $guarded = []; + + public function likeable() + { + return $this->morphTo(); + } +} From 137518c24e2af9377a3008d7ae9b7e2784a7908b Mon Sep 17 00:00:00 2001 From: Sergiy Litvinchuk Date: Sun, 24 Nov 2024 22:21:53 +0200 Subject: [PATCH 02/13] update tests --- .../Database/EloquentModelRelationAutoloadTest.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/Integration/Database/EloquentModelRelationAutoloadTest.php b/tests/Integration/Database/EloquentModelRelationAutoloadTest.php index 3b88bb8f7285..5bb92bdd7731 100644 --- a/tests/Integration/Database/EloquentModelRelationAutoloadTest.php +++ b/tests/Integration/Database/EloquentModelRelationAutoloadTest.php @@ -93,8 +93,6 @@ public function testRelationAutoloadVariousNestedMorphRelations() }); }); - Post::alwaysAutoloadRelations(); - $likes = Like::get(); DB::enableQueryLog(); @@ -102,7 +100,7 @@ public function testRelationAutoloadVariousNestedMorphRelations() $videos = []; $videoLike = null; -// $likes->enableRelationAutoload(); + $likes->enableRelationAutoload(); foreach ($likes as $like) { $likeable = $like->likeable; From 37cff238962e3f4faa9507f9adb69e32a94c9899 Mon Sep 17 00:00:00 2001 From: Sergiy Litvinchuk Date: Sun, 24 Nov 2024 22:40:37 +0200 Subject: [PATCH 03/13] update tests --- .../DatabaseEloquentBelongsToManyWithCastedAttributesTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Database/DatabaseEloquentBelongsToManyWithCastedAttributesTest.php b/tests/Database/DatabaseEloquentBelongsToManyWithCastedAttributesTest.php index c7cab6453dfb..08f1bd45a56e 100644 --- a/tests/Database/DatabaseEloquentBelongsToManyWithCastedAttributesTest.php +++ b/tests/Database/DatabaseEloquentBelongsToManyWithCastedAttributesTest.php @@ -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(); @@ -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(); From fcd037e16f677fdee5200af300cf72dd4d6e2a3d Mon Sep 17 00:00:00 2001 From: Sergiy Litvinchuk Date: Sun, 24 Nov 2024 22:43:43 +0200 Subject: [PATCH 04/13] fix cs --- src/Illuminate/Database/Eloquent/Collection.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Collection.php b/src/Illuminate/Database/Eloquent/Collection.php index 1447ac68cc32..30c327a80997 100755 --- a/src/Illuminate/Database/Eloquent/Collection.php +++ b/src/Illuminate/Database/Eloquent/Collection.php @@ -255,7 +255,7 @@ public function loadMissing($relations) */ public function loadMissingRelationWithTypes(array $path) { - list($name, $class) = array_shift($path); + [$name, $class] = array_shift($path); $this->filter(fn ($model) => ! is_null($model) && ! $model->relationLoaded($name) && $model::class === $class) ->load($name); @@ -342,7 +342,7 @@ public function loadMorphCount($relation, $relations) /** * Enable relation autoload for the collection. * - * @return $this + * @return $this */ public function enableRelationAutoload() { From ca43c036b5c686c778a09db229d95f8f0a07a3ee Mon Sep 17 00:00:00 2001 From: Sergiy Litvinchuk Date: Sun, 24 Nov 2024 22:47:12 +0200 Subject: [PATCH 05/13] fix cs --- .../Eloquent/Concerns/HasRelationships.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php index 4367b6ea08c6..8000cbf4b6d3 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php @@ -109,8 +109,8 @@ public static function resolveRelationUsing($name, Closure $callback) /** * Set relation autoload callback for model and its relations. * - * @param mixed $context - * @param Closure $callback + * @param mixed $context + * @param Closure $callback * @return $this */ public function usingRelationAutoloadCallback($context, Closure $callback) @@ -169,7 +169,7 @@ public function hasRelationAutoloadCallback() /** * Trigger relation autoload callback and check if relation is loaded. * - * @param string $key + * @param string $key * @return bool */ protected function handleRelationAutoload($key) @@ -186,8 +186,8 @@ protected function handleRelationAutoload($key) /** * Trigger relation autoload callback. * - * @param string $key - * @param array $keys + * @param string $key + * @param array $keys * @return void */ protected function triggerRelationAutoloadCallback($key, $keys) @@ -201,8 +201,8 @@ protected function triggerRelationAutoloadCallback($key, $keys) /** * Apply relation autoload callback to value. * - * @param string $key - * @param mixed $values + * @param string $key + * @param mixed $values * @return void */ protected function applyRelationAutoloadCallbackToValue($key, $values) From 7afdf2f159a42a984edc5ea2de9c625e20333f5e Mon Sep 17 00:00:00 2001 From: Sergiy Litvinchuk Date: Mon, 25 Nov 2024 23:39:05 +0200 Subject: [PATCH 06/13] Rename alwaysAutoloadRelations method to globalAutoloadRelations --- src/Illuminate/Database/Eloquent/Builder.php | 2 +- src/Illuminate/Database/Eloquent/Model.php | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 9acdf7b1d85e..c5933b6a4483 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -856,7 +856,7 @@ public function get($columns = ['*']) $collection = $builder->getModel()->newCollection($models); - if (Model::alwaysAutoloadsRelations()) { + if (Model::isAutoloadingRelationsGlobally()) { $collection->enableRelationAutoload(); } diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 2bc9a46e7d07..2ddc64fc37dd 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -183,7 +183,7 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt * * @var bool */ - protected static $modelsShouldAlwaysAutoloadRelations = false; + protected static $modelsShouldGlobalAutoloadRelations = false; /** * The callback that is responsible for handling lazy loading violations. @@ -459,9 +459,9 @@ public static function preventLazyLoading($value = true) * @param bool $value * @return void */ - public static function alwaysAutoloadRelations($value = true) + public static function globalAutoloadRelations($value = true) { - static::$modelsShouldAlwaysAutoloadRelations = $value; + static::$modelsShouldGlobalAutoloadRelations = $value; } /** @@ -2254,9 +2254,9 @@ public static function preventsLazyLoading() * * @return bool */ - public static function alwaysAutoloadsRelations() + public static function isAutoloadingRelationsGlobally() { - return static::$modelsShouldAlwaysAutoloadRelations; + return static::$modelsShouldGlobalAutoloadRelations; } /** From 32618a00e55a1554d1a8436b9ae646eb6fea0ea6 Mon Sep 17 00:00:00 2001 From: Sergiy Litvinchuk Date: Tue, 26 Nov 2024 11:29:38 +0200 Subject: [PATCH 07/13] Rename enableRelationAutoload method to withRelationAutoload --- src/Illuminate/Database/Eloquent/Builder.php | 2 +- src/Illuminate/Database/Eloquent/Collection.php | 2 +- .../Database/Eloquent/Concerns/HasRelationships.php | 2 +- .../Database/EloquentModelRelationAutoloadTest.php | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index c5933b6a4483..753bea8c8625 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -857,7 +857,7 @@ public function get($columns = ['*']) $collection = $builder->getModel()->newCollection($models); if (Model::isAutoloadingRelationsGlobally()) { - $collection->enableRelationAutoload(); + $collection->withRelationAutoload(); } return $this->applyAfterQueryCallbacks($collection); diff --git a/src/Illuminate/Database/Eloquent/Collection.php b/src/Illuminate/Database/Eloquent/Collection.php index 30c327a80997..4c4ef9d1e771 100755 --- a/src/Illuminate/Database/Eloquent/Collection.php +++ b/src/Illuminate/Database/Eloquent/Collection.php @@ -344,7 +344,7 @@ public function loadMorphCount($relation, $relations) * * @return $this */ - public function enableRelationAutoload() + public function withRelationAutoload() { $callback = fn ($path) => $this->loadMissingRelationWithTypes($path); diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php index 8000cbf4b6d3..af104ee4883b 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php @@ -140,7 +140,7 @@ public function getRelationAutoloadContext() * * @return $this */ - public function enableRelationAutoload() + public function withRelationAutoload() { if ($this->hasRelationAutoloadCallback()) { return $this; diff --git a/tests/Integration/Database/EloquentModelRelationAutoloadTest.php b/tests/Integration/Database/EloquentModelRelationAutoloadTest.php index 5bb92bdd7731..7891eb30201d 100644 --- a/tests/Integration/Database/EloquentModelRelationAutoloadTest.php +++ b/tests/Integration/Database/EloquentModelRelationAutoloadTest.php @@ -50,7 +50,7 @@ public function testRelationAutoload() $likes = []; - $posts->enableRelationAutoload(); + $posts->withRelationAutoload(); foreach ($posts as $post) { foreach ($post->comments as $comment) { @@ -100,7 +100,7 @@ public function testRelationAutoloadVariousNestedMorphRelations() $videos = []; $videoLike = null; - $likes->enableRelationAutoload(); + $likes->withRelationAutoload(); foreach ($likes as $like) { $likeable = $like->likeable; From cb47bebacedb41475fa0bf387be4a1c65b416bca Mon Sep 17 00:00:00 2001 From: Sergiy Litvinchuk Date: Wed, 27 Nov 2024 18:32:59 +0200 Subject: [PATCH 08/13] Optimize withRelationAutoload method --- src/Illuminate/Database/Eloquent/Collection.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Collection.php b/src/Illuminate/Database/Eloquent/Collection.php index 4c4ef9d1e771..6ebeddadcc84 100755 --- a/src/Illuminate/Database/Eloquent/Collection.php +++ b/src/Illuminate/Database/Eloquent/Collection.php @@ -348,9 +348,8 @@ public function withRelationAutoload() { $callback = fn ($path) => $this->loadMissingRelationWithTypes($path); - $this - ->filter(fn ($model) => ! $model->hasRelationAutoloadCallback()) - ->each(fn ($model) => $model->usingRelationAutoloadCallback($this, $callback)); + $this->each(fn ($model) => $model->hasRelationAutoloadCallback() + || $model->usingRelationAutoloadCallback($this, $callback)); return $this; } From af6ccd9c042311ec9d5b073c90fc1a6d406c834e Mon Sep 17 00:00:00 2001 From: Sergiy Litvinchuk Date: Mon, 30 Dec 2024 13:42:26 +0200 Subject: [PATCH 09/13] Simplified circular relation autoload detection --- .../Database/Eloquent/Collection.php | 2 +- .../Eloquent/Concerns/HasRelationships.php | 36 +++++-------------- 2 files changed, 9 insertions(+), 29 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Collection.php b/src/Illuminate/Database/Eloquent/Collection.php index 6ebeddadcc84..2e43d4411e80 100755 --- a/src/Illuminate/Database/Eloquent/Collection.php +++ b/src/Illuminate/Database/Eloquent/Collection.php @@ -349,7 +349,7 @@ public function withRelationAutoload() $callback = fn ($path) => $this->loadMissingRelationWithTypes($path); $this->each(fn ($model) => $model->hasRelationAutoloadCallback() - || $model->usingRelationAutoloadCallback($this, $callback)); + || $model->usingRelationAutoloadCallback($callback)); return $this; } diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php index af104ee4883b..0ccb497e25ae 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php @@ -46,13 +46,6 @@ trait HasRelationships */ protected $relationAutoloadCallback = null; - /** - * The relationship autoload context. - * - * @var ?Collection - */ - protected $relationAutoloadContext = null; - /** * The many to many relationship methods. * @@ -109,32 +102,21 @@ public static function resolveRelationUsing($name, Closure $callback) /** * Set relation autoload callback for model and its relations. * - * @param mixed $context * @param Closure $callback + * @param mixed $context * @return $this */ - public function usingRelationAutoloadCallback($context, Closure $callback) + public function usingRelationAutoloadCallback(Closure $callback, $context = null) { - $this->relationAutoloadContext = $context; $this->relationAutoloadCallback = $callback; foreach ($this->relations as $key => $value) { - $this->applyRelationAutoloadCallbackToValue($key, $value); + $this->applyRelationAutoloadCallbackToValue($key, $value, $context); } 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. * @@ -149,7 +131,6 @@ public function withRelationAutoload() $collection = new Collection([$this]); $this->usingRelationAutoloadCallback( - $collection, fn ($path) => $collection->loadMissingRelationWithTypes($path) ); @@ -203,9 +184,10 @@ protected function triggerRelationAutoloadCallback($key, $keys) * * @param string $key * @param mixed $values + * @param mixed $context * @return void */ - protected function applyRelationAutoloadCallbackToValue($key, $values) + protected function applyRelationAutoloadCallbackToValue($key, $values, $context = null) { if (! $this->hasRelationAutoloadCallback() || ! $values) { return; @@ -222,12 +204,10 @@ protected function applyRelationAutoloadCallbackToValue($key, $values) $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); + if (is_null($context) || $context !== $item) { + $item->usingRelationAutoloadCallback($callback, $context); } } } @@ -1128,7 +1108,7 @@ public function setRelation($relation, $value) { $this->relations[$relation] = $value; - $this->applyRelationAutoloadCallbackToValue($relation, $value); + $this->applyRelationAutoloadCallbackToValue($relation, $value, $this); return $this; } From ba3a29adf333321868aef6fa46dc418c04633928 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 2 Apr 2025 16:16:22 -0500 Subject: [PATCH 10/13] formatting --- src/Illuminate/Database/Eloquent/Builder.php | 2 +- .../Database/Eloquent/Collection.php | 53 +++++++------ .../Eloquent/Concerns/HasAttributes.php | 2 +- .../Eloquent/Concerns/HasRelationships.php | 78 +++++++------------ .../EloquentModelRelationAutoloadTest.php | 4 +- 5 files changed, 62 insertions(+), 77 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 753bea8c8625..81941443fe77 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -857,7 +857,7 @@ public function get($columns = ['*']) $collection = $builder->getModel()->newCollection($models); if (Model::isAutoloadingRelationsGlobally()) { - $collection->withRelationAutoload(); + $collection->primeRelationshipAutoloading(); } return $this->applyAfterQueryCallbacks($collection); diff --git a/src/Illuminate/Database/Eloquent/Collection.php b/src/Illuminate/Database/Eloquent/Collection.php index 2e43d4411e80..8704c249e83d 100755 --- a/src/Illuminate/Database/Eloquent/Collection.php +++ b/src/Illuminate/Database/Eloquent/Collection.php @@ -249,28 +249,32 @@ public function loadMissing($relations) } /** - * Load a relationship path with types if it is not already eager loaded. + * Load a relationship path for models of the given type if it is not already eager loaded. * + * @param array $tuples * @return void */ - public function loadMissingRelationWithTypes(array $path) + public function loadMissingRelationsViaRelationAndClassTuples(array $tuples) { - [$name, $class] = array_shift($path); + [$relation, $class] = array_shift($tuples); - $this->filter(fn ($model) => ! is_null($model) && ! $model->relationLoaded($name) && $model::class === $class) - ->load($name); + $this->filter(function ($model) use ($relation, $class) { + return ! is_null($model) && + ! $model->relationLoaded($relation) && + $model::class === $class; + })->load($relation); - if (empty($path)) { + if (empty($tuples)) { return; } - $models = $this->pluck($name)->whereNotNull(); + $models = $this->pluck($relation)->whereNotNull(); if ($models->first() instanceof BaseCollection) { $models = $models->collapse(); } - (new static($models))->loadMissingRelationWithTypes($path); + (new static($models))->loadMissingRelationsViaRelationAndClassTuples($tuples); } /** @@ -339,21 +343,6 @@ public function loadMorphCount($relation, $relations) return $this; } - /** - * Enable relation autoload for the collection. - * - * @return $this - */ - public function withRelationAutoload() - { - $callback = fn ($path) => $this->loadMissingRelationWithTypes($path); - - $this->each(fn ($model) => $model->hasRelationAutoloadCallback() - || $model->usingRelationAutoloadCallback($callback)); - - return $this; - } - /** * Determine if a key exists in the collection. * @@ -761,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 primeRelationshipAutoloading() + { + $callback = fn ($tuples) => $this->loadMissingRelationsViaRelationAndClassTuples($tuples); + + foreach ($this as $model) { + if (! $model->hasRelationAutoloadCallback()) { + $model->autoloadRelationsUsing($callback); + } + } + + return $this; + } + /** * Get the type of the entities being queued. * diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index 810c2e28d399..0d0fc454bf0b 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -551,7 +551,7 @@ public function getRelationValue($key) return; } - if ($this->handleRelationAutoload($key)) { + if ($this->attemptToAutoloadRelation($key)) { return $this->relations[$key]; } diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php index 0ccb497e25ae..b169d9af27fa 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php @@ -40,9 +40,9 @@ trait HasRelationships protected $touches = []; /** - * The relationship autoload callback. + * The relationship autoloader callback. * - * @var ?Closure + * @var \Closure|null */ protected $relationAutoloadCallback = null; @@ -100,45 +100,25 @@ public static function resolveRelationUsing($name, Closure $callback) } /** - * Set relation autoload callback for model and its relations. + * Define an automatic relationship autoloader callback for this model and its relations. * - * @param Closure $callback + * @param \Closure $callback * @param mixed $context * @return $this */ - public function usingRelationAutoloadCallback(Closure $callback, $context = null) + public function autoloadRelationsUsing(Closure $callback, $context = null) { $this->relationAutoloadCallback = $callback; foreach ($this->relations as $key => $value) { - $this->applyRelationAutoloadCallbackToValue($key, $value, $context); - } - - return $this; - } - - /** - * Enable relation autoload for model and its relations if not already enabled. - * - * @return $this - */ - public function withRelationAutoload() - { - if ($this->hasRelationAutoloadCallback()) { - return $this; + $this->propagateRelationAutoloadCallbackToRelation($key, $value, $context); } - $collection = new Collection([$this]); - - $this->usingRelationAutoloadCallback( - fn ($path) => $collection->loadMissingRelationWithTypes($path) - ); - return $this; } /** - * Check if relation autoload callback is set. + * Determine if a relationship autoloader callback has been defined. * * @return bool */ @@ -148,66 +128,64 @@ public function hasRelationAutoloadCallback() } /** - * Trigger relation autoload callback and check if relation is loaded. + * Attempt to autoload the given relationship using the autoload callback. * * @param string $key * @return bool */ - protected function handleRelationAutoload($key) + protected function attemptToAutoloadRelation($key) { if (! $this->hasRelationAutoloadCallback()) { return false; } - $this->triggerRelationAutoloadCallback($key, []); + $this->invokeRelationAutoloadCallbackFor($key, []); return $this->relationLoaded($key); } /** - * Trigger relation autoload callback. + * Invoke the relationship autoloader callback for the given relationships. * * @param string $key - * @param array $keys + * @param array $tuples * @return void */ - protected function triggerRelationAutoloadCallback($key, $keys) + protected function invokeRelationAutoloadCallbackFor($key, $tuples) { - call_user_func( - $this->relationAutoloadCallback, - array_merge([[$key, get_class($this)]], $keys) - ); + $tuples = array_merge([[$key, get_class($this)]], $tuples); + + call_user_func($this->relationAutoloadCallback, $tuples); } /** - * Apply relation autoload callback to value. + * Propagate the relationship autoloader callback to the given related models. * * @param string $key * @param mixed $values * @param mixed $context * @return void */ - protected function applyRelationAutoloadCallbackToValue($key, $values, $context = null) + protected function propagateRelationAutoloadCallbackToRelation($key, $models, $context = null) { - if (! $this->hasRelationAutoloadCallback() || ! $values) { + if (! $this->hasRelationAutoloadCallback() || ! $models) { return; } - if ($values instanceof Model) { - $values = [$values]; + if ($models instanceof Model) { + $models = [$models]; } - if (! is_iterable($values)) { + if (! is_iterable($models)) { return; } - $callback = fn (array $keys) => $this->triggerRelationAutoloadCallback($key, $keys); + $callback = fn (array $tuples) => $this->invokeRelationAutoloadCallbackFor($key, $tuples); - foreach ($values as $item) { - // check if relation autoload contexts are different - // to avoid circular relation autoload - if (is_null($context) || $context !== $item) { - $item->usingRelationAutoloadCallback($callback, $context); + 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); } } } @@ -1108,7 +1086,7 @@ public function setRelation($relation, $value) { $this->relations[$relation] = $value; - $this->applyRelationAutoloadCallbackToValue($relation, $value, $this); + $this->propagateRelationAutoloadCallbackToRelation($relation, $value, $this); return $this; } diff --git a/tests/Integration/Database/EloquentModelRelationAutoloadTest.php b/tests/Integration/Database/EloquentModelRelationAutoloadTest.php index 7891eb30201d..3cdcb21e681a 100644 --- a/tests/Integration/Database/EloquentModelRelationAutoloadTest.php +++ b/tests/Integration/Database/EloquentModelRelationAutoloadTest.php @@ -50,7 +50,7 @@ public function testRelationAutoload() $likes = []; - $posts->withRelationAutoload(); + $posts->primeRelationshipAutoloading(); foreach ($posts as $post) { foreach ($post->comments as $comment) { @@ -100,7 +100,7 @@ public function testRelationAutoloadVariousNestedMorphRelations() $videos = []; $videoLike = null; - $likes->withRelationAutoload(); + $likes->primeRelationshipAutoloading(); foreach ($likes as $like) { $likeable = $like->likeable; From cedcc1e84af7989c032b58b074a12b399e25b339 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 2 Apr 2025 16:31:18 -0500 Subject: [PATCH 11/13] formatting --- src/Illuminate/Database/Eloquent/Builder.php | 2 +- src/Illuminate/Database/Eloquent/Collection.php | 2 +- .../Database/EloquentModelRelationAutoloadTest.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 81941443fe77..896b9c38ad00 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -857,7 +857,7 @@ public function get($columns = ['*']) $collection = $builder->getModel()->newCollection($models); if (Model::isAutoloadingRelationsGlobally()) { - $collection->primeRelationshipAutoloading(); + $collection->withRelationshipAutoloading(); } return $this->applyAfterQueryCallbacks($collection); diff --git a/src/Illuminate/Database/Eloquent/Collection.php b/src/Illuminate/Database/Eloquent/Collection.php index 8704c249e83d..927d33c2551a 100755 --- a/src/Illuminate/Database/Eloquent/Collection.php +++ b/src/Illuminate/Database/Eloquent/Collection.php @@ -755,7 +755,7 @@ protected function duplicateComparator($strict) * * @return $this */ - public function primeRelationshipAutoloading() + public function withRelationshipAutoloading() { $callback = fn ($tuples) => $this->loadMissingRelationsViaRelationAndClassTuples($tuples); diff --git a/tests/Integration/Database/EloquentModelRelationAutoloadTest.php b/tests/Integration/Database/EloquentModelRelationAutoloadTest.php index 3cdcb21e681a..8f80c5bb5149 100644 --- a/tests/Integration/Database/EloquentModelRelationAutoloadTest.php +++ b/tests/Integration/Database/EloquentModelRelationAutoloadTest.php @@ -50,7 +50,7 @@ public function testRelationAutoload() $likes = []; - $posts->primeRelationshipAutoloading(); + $posts->withRelationshipAutoloading(); foreach ($posts as $post) { foreach ($post->comments as $comment) { @@ -100,7 +100,7 @@ public function testRelationAutoloadVariousNestedMorphRelations() $videos = []; $videoLike = null; - $likes->primeRelationshipAutoloading(); + $likes->withRelationshipAutoloading(); foreach ($likes as $like) { $likeable = $like->likeable; From fdcb00126a762b5872c31c6124afea50baeca2e1 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 2 Apr 2025 17:15:21 -0500 Subject: [PATCH 12/13] formatting --- .../Eloquent/Concerns/HasRelationships.php | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php index b169d9af27fa..2f30de88a2b1 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php @@ -99,6 +99,16 @@ 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. * @@ -117,16 +127,6 @@ public function autoloadRelationsUsing(Closure $callback, $context = null) return $this; } - /** - * Determine if a relationship autoloader callback has been defined. - * - * @return bool - */ - public function hasRelationAutoloadCallback() - { - return ! is_null($this->relationAutoloadCallback); - } - /** * Attempt to autoload the given relationship using the autoload callback. * From a346fc89d2a02132676b815b5dd630ccb514f6cf Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 8 Apr 2025 13:25:29 -0500 Subject: [PATCH 13/13] formatting --- src/Illuminate/Database/Eloquent/Builder.php | 2 +- src/Illuminate/Database/Eloquent/Collection.php | 8 ++++---- src/Illuminate/Database/Eloquent/Model.php | 16 ++++++++-------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 896b9c38ad00..73163988aec8 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -856,7 +856,7 @@ public function get($columns = ['*']) $collection = $builder->getModel()->newCollection($models); - if (Model::isAutoloadingRelationsGlobally()) { + if (Model::isAutomaticallyEagerLoadingRelationships()) { $collection->withRelationshipAutoloading(); } diff --git a/src/Illuminate/Database/Eloquent/Collection.php b/src/Illuminate/Database/Eloquent/Collection.php index 927d33c2551a..ba15202828e1 100755 --- a/src/Illuminate/Database/Eloquent/Collection.php +++ b/src/Illuminate/Database/Eloquent/Collection.php @@ -251,10 +251,10 @@ public function loadMissing($relations) /** * Load a relationship path for models of the given type if it is not already eager loaded. * - * @param array $tuples + * @param array> $tuples * @return void */ - public function loadMissingRelationsViaRelationAndClassTuples(array $tuples) + public function loadMissingRelationshipChain(array $tuples) { [$relation, $class] = array_shift($tuples); @@ -274,7 +274,7 @@ public function loadMissingRelationsViaRelationAndClassTuples(array $tuples) $models = $models->collapse(); } - (new static($models))->loadMissingRelationsViaRelationAndClassTuples($tuples); + (new static($models))->loadMissingRelationshipChain($tuples); } /** @@ -757,7 +757,7 @@ protected function duplicateComparator($strict) */ public function withRelationshipAutoloading() { - $callback = fn ($tuples) => $this->loadMissingRelationsViaRelationAndClassTuples($tuples); + $callback = fn ($tuples) => $this->loadMissingRelationshipChain($tuples); foreach ($this as $model) { if (! $model->hasRelationAutoloadCallback()) { diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 2ddc64fc37dd..206da672fc92 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -179,11 +179,11 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt protected static $modelsShouldPreventLazyLoading = false; /** - * Indicates whether relations should be automatically loaded on all models. + * Indicates whether relations should be automatically loaded on all models when they are accessed. * * @var bool */ - protected static $modelsShouldGlobalAutoloadRelations = false; + protected static $modelsShouldAutomaticallyEagerLoadRelationships = false; /** * The callback that is responsible for handling lazy loading violations. @@ -454,14 +454,14 @@ public static function preventLazyLoading($value = true) } /** - * Determine if model relationships should be automatically loaded. + * Determine if model relationships should be automatically eager loaded when accessed. * * @param bool $value * @return void */ - public static function globalAutoloadRelations($value = true) + public static function automaticallyEagerLoadRelationships($value = true) { - static::$modelsShouldGlobalAutoloadRelations = $value; + static::$modelsShouldAutomaticallyEagerLoadRelationships = $value; } /** @@ -2250,13 +2250,13 @@ public static function preventsLazyLoading() } /** - * Determine if relations autoload is enabled. + * Determine if relationships are being automatically eager loaded when accessed. * * @return bool */ - public static function isAutoloadingRelationsGlobally() + public static function isAutomaticallyEagerLoadingRelationships() { - return static::$modelsShouldGlobalAutoloadRelations; + return static::$modelsShouldAutomaticallyEagerLoadRelationships; } /** pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy