From 27b1652297212bbe1f8ae32217c7292ca5f7f2ba Mon Sep 17 00:00:00 2001 From: Jonas Staudenmeir Date: Thu, 10 May 2018 00:52:31 +0200 Subject: [PATCH] Add Collection::loadMissing() --- .../Database/Eloquent/Collection.php | 58 +++++++++- src/Illuminate/Database/Eloquent/Model.php | 6 +- .../EloquentCollectionLoadMissingTest.php | 104 ++++++++++++++++++ .../Database/EloquentModelLoadMissingTest.php | 66 +++++++++++ 4 files changed, 230 insertions(+), 4 deletions(-) create mode 100644 tests/Integration/Database/EloquentCollectionLoadMissingTest.php create mode 100644 tests/Integration/Database/EloquentModelLoadMissingTest.php diff --git a/src/Illuminate/Database/Eloquent/Collection.php b/src/Illuminate/Database/Eloquent/Collection.php index f359223ab728..e867314bd3a5 100755 --- a/src/Illuminate/Database/Eloquent/Collection.php +++ b/src/Illuminate/Database/Eloquent/Collection.php @@ -44,7 +44,7 @@ public function find($key, $default = null) /** * Load a set of relationships onto the collection. * - * @param mixed $relations + * @param array|string $relations * @return $this */ public function load($relations) @@ -86,6 +86,62 @@ public function loadMorph($relation, $relations) return $this; } + /** + * Load a set of relationships onto the collection if they are not already eager loaded. + * + * @param array|string $relations + * @return $this + */ + public function loadMissing($relations) + { + if (is_string($relations)) { + $relations = func_get_args(); + } + + foreach ($relations as $relation) { + $this->loadMissingRelation($this, explode('.', $relation)); + } + + return $this; + } + + /** + * Load a relationship path if it is not already eager loaded. + * + * @param \Illuminate\Database\Eloquent\Collection $models + * @param array $path + * @return void + */ + protected function loadMissingRelation(Collection $models, array $path) + { + // Get the first relationship. + $relation = array_shift($path); + + // Handle relationships with specific columns. + $name = explode(':', $relation)[0]; + + // Load the relationship where missing. + $models->filter(function ($model) use ($name) { + return ! is_null($model) && ! $model->relationLoaded($name); + })->load($relation); + + // End the recursion. + if (empty($path)) { + return; + } + + // Get the models for the next level. + $models = $models->pluck($name); + + // Handle *-many relationships. + if ($models->first() instanceof BaseCollection) { + $models = $models->collapse(); + }; + + // Load the remaining path. + $this->loadMissingRelation(new static($models), $path); + } + /** * Add an item to the collection. * diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index a3098eb58369..8190f1178be0 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -405,9 +405,9 @@ public function loadMissing($relations) { $relations = is_string($relations) ? func_get_args() : $relations; - return $this->load(array_filter($relations, function ($relation) { - return ! $this->relationLoaded($relation); - })); + $this->newCollection([$this])->loadMissing($relations); + + return $this; } /** diff --git a/tests/Integration/Database/EloquentCollectionLoadMissingTest.php b/tests/Integration/Database/EloquentCollectionLoadMissingTest.php new file mode 100644 index 000000000000..3ebfaccecf04 --- /dev/null +++ b/tests/Integration/Database/EloquentCollectionLoadMissingTest.php @@ -0,0 +1,104 @@ +increments('id'); + }); + + Schema::create('posts', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('user_id'); + }); + + Schema::create('comments', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('parent_id')->nullable(); + $table->unsignedInteger('post_id'); + }); + + Schema::create('revisions', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('comment_id'); + }); + + User::create(); + + Post::create(['user_id' => 1]); + + Comment::create(['parent_id' => null, 'post_id' => 1]); + Comment::create(['parent_id' => 1, 'post_id' => 1]); + + Revision::create(['comment_id' => 1]); + } + + public function testLoadMissing() + { + $posts = Post::with('comments', 'user')->get(); + + \DB::enableQueryLog(); + + $posts->loadMissing('comments.parent:id.revisions', 'user:id'); + + $this->assertCount(2, \DB::getQueryLog()); + $this->assertTrue($posts[0]->comments[0]->relationLoaded('parent')); + $this->assertTrue($posts[0]->comments[1]->parent->relationLoaded('revisions')); + $this->assertFalse(array_key_exists('post_id', $posts[0]->comments[1]->parent->getAttributes())); + } +} + +class Comment extends Model +{ + public $timestamps = false; + + protected $guarded = ['id']; + + public function parent() { + return $this->belongsTo(Comment::class); + } + + public function revisions() { + return $this->hasMany(Revision::class); + } +} + +class Post extends Model +{ + public $timestamps = false; + + protected $guarded = ['id']; + + public function comments() { + return $this->hasMany(Comment::class); + } + + public function user() { + return $this->belongsTo(User::class); + } +} + +class Revision extends Model +{ + public $timestamps = false; + + protected $guarded = ['id']; +} + +class User extends Model +{ + public $timestamps = false; +} diff --git a/tests/Integration/Database/EloquentModelLoadMissingTest.php b/tests/Integration/Database/EloquentModelLoadMissingTest.php new file mode 100644 index 000000000000..43c4ad10552c --- /dev/null +++ b/tests/Integration/Database/EloquentModelLoadMissingTest.php @@ -0,0 +1,66 @@ +increments('id'); + }); + + Schema::create('comments', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('parent_id')->nullable(); + $table->unsignedInteger('post_id'); + }); + + Post::create(); + + Comment::create(['parent_id' => null, 'post_id' => 1]); + Comment::create(['parent_id' => 1, 'post_id' => 1]); + } + + public function testLoadMissing() + { + $post = Post::with('comments')->first(); + + \DB::enableQueryLog(); + + $post->loadMissing('comments.parent'); + + $this->assertCount(1, \DB::getQueryLog()); + $this->assertTrue($post->comments[0]->relationLoaded('parent')); + } +} + +class Comment extends Model +{ + public $timestamps = false; + + protected $guarded = ['id']; + + public function parent() { + return $this->belongsTo(Comment::class); + } +} + +class Post extends Model +{ + public $timestamps = false; + + public function comments() { + return $this->hasMany(Comment::class); + } +}