diff --git a/README.md b/README.md index 240866b..e23a406 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Supports Laravel 5.5.29+. * [MorphToMany](#morphtomany) * [MorphedByMany](#morphedbymany) * [Intermediate and Pivot Data](#intermediate-and-pivot-data) + * [HasOneDeep](#hasonedeep) Using the [documentation example](https://laravel.com/docs/eloquent-relationships#has-many-through) with an additional level: `Country` → has many → `User` → has many → `Post` → has many → `Comment` @@ -288,4 +289,21 @@ public function permissions() foreach ($user->permissions as $permission) { // $permission->pivot->expires_at } +``` + +### HasOneDeep + +Use the `HasOneDeep` relationship if you only want to retrieve a single related instance: + +```php +class Country extends Model +{ + use \Staudenmeir\EloquentHasManyDeep\HasRelationships; + + public function latestComment() + { + return $this->hasOneDeep('App\Comment', ['App\User', 'App\Post']) + ->latest('comments.created_at'); + } +} ``` \ No newline at end of file diff --git a/src/HasOneDeep.php b/src/HasOneDeep.php new file mode 100644 index 0000000..d434e62 --- /dev/null +++ b/src/HasOneDeep.php @@ -0,0 +1,72 @@ +first() ?: $this->getDefaultFor(end($this->throughParents)); + } + + /** + * Initialize the relation on a set of models. + * + * @param array $models + * @param string $relation + * @return array + */ + public function initRelation(array $models, $relation) + { + foreach ($models as $model) { + $model->setRelation($relation, $this->getDefaultFor($model)); + } + + return $models; + } + + /** + * Match the eagerly loaded results to their parents. + * + * @param array $models + * @param \Illuminate\Database\Eloquent\Collection $results + * @param string $relation + * @return array + */ + public function match(array $models, Collection $results, $relation) + { + $dictionary = $this->buildDictionary($results); + + foreach ($models as $model) { + if (isset($dictionary[$key = $model->getAttribute($this->localKey)])) { + $model->setRelation( + $relation, reset($dictionary[$key]) + ); + } + } + + return $models; + } + + /** + * Make a new related instance for the given model. + * + * @param \Illuminate\Database\Eloquent\Model $parent + * @return \Illuminate\Database\Eloquent\Model + */ + public function newRelatedInstanceFor(Model $parent) + { + return $this->related->newInstance(); + } +} diff --git a/src/HasRelationships.php b/src/HasRelationships.php index c72aea9..68c1011 100644 --- a/src/HasRelationships.php +++ b/src/HasRelationships.php @@ -19,6 +19,34 @@ trait HasRelationships * @return \Staudenmeir\EloquentHasManyDeep\HasManyDeep */ public function hasManyDeep($related, array $through, array $foreignKeys = [], array $localKeys = []) + { + return $this->newHasManyDeep(...$this->hasOneOrManyDeep($related, $through, $foreignKeys, $localKeys)); + } + + /** + * Define a has-one-deep relationship. + * + * @param string $related + * @param array $through + * @param array $foreignKeys + * @param array $localKeys + * @return \Staudenmeir\EloquentHasManyDeep\HasOneDeep + */ + public function hasOneDeep($related, array $through, array $foreignKeys = [], array $localKeys = []) + { + return $this->newHasOneDeep(...$this->hasOneOrManyDeep($related, $through, $foreignKeys, $localKeys)); + } + + /** + * Prepare a has-one-deep or has-many-deep relationship. + * + * @param string $related + * @param array $through + * @param array $foreignKeys + * @param array $localKeys + * @return array + */ + protected function hasOneOrManyDeep($related, array $through, array $foreignKeys, array $localKeys) { $relatedInstance = $this->newRelatedInstance($related); @@ -44,7 +72,7 @@ public function hasManyDeep($related, array $through, array $foreignKeys = [], a } } - return $this->newHasManyDeep($relatedInstance->newQuery(), $this, $throughParents, $foreignKeys, $localKeys); + return [$relatedInstance->newQuery(), $this, $throughParents, $foreignKeys, $localKeys]; } /** @@ -61,4 +89,19 @@ protected function newHasManyDeep(Builder $query, Model $farParent, array $throu { return new HasManyDeep($query, $farParent, $throughParents, $foreignKeys, $localKeys); } + + /** + * Instantiate a new HasOneDeep relationship. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Model $farParent + * @param \Illuminate\Database\Eloquent\Model[] $throughParents + * @param array $foreignKeys + * @param array $localKeys + * @return \Staudenmeir\EloquentHasManyDeep\HasOneDeep + */ + protected function newHasOneDeep(Builder $query, Model $farParent, array $throughParents, array $foreignKeys, array $localKeys) + { + return new HasOneDeep($query, $farParent, $throughParents, $foreignKeys, $localKeys); + } } diff --git a/tests/HasOneDeepTest.php b/tests/HasOneDeepTest.php new file mode 100644 index 0000000..3f66098 --- /dev/null +++ b/tests/HasOneDeepTest.php @@ -0,0 +1,46 @@ +comment; + + $this->assertEquals(1, $comment->comment_pk); + $sql = 'select "comments".*, "users"."country_country_pk" from "comments"' + .' inner join "posts" on "posts"."post_pk" = "comments"."post_post_pk"' + .' inner join "users" on "users"."user_pk" = "posts"."user_user_pk"' + .' where "users"."deleted_at" is null and "users"."country_country_pk" = ? limit 1'; + $this->assertEquals($sql, Capsule::getQueryLog()[1]['query']); + $this->assertEquals([1], Capsule::getQueryLog()[1]['bindings']); + } + + public function testDefault() + { + $comment = Country::find(2)->comment; + + $this->assertInstanceOf(Comment::class, $comment); + $this->assertFalse($comment->exists); + } + + public function testEagerLoading() + { + $countries = Country::with('comment')->get(); + + $this->assertEquals(1, $countries[0]->comment->comment_pk); + $this->assertInstanceOf(Comment::class, $countries[1]->comment); + $this->assertFalse($countries[1]->comment->exists); + $sql = 'select "comments".*, "users"."country_country_pk" from "comments"' + .' inner join "posts" on "posts"."post_pk" = "comments"."post_post_pk"' + .' inner join "users" on "users"."user_pk" = "posts"."user_user_pk"' + .' where "users"."deleted_at" is null and "users"."country_country_pk" in (?, ?)'; + $this->assertEquals($sql, Capsule::getQueryLog()[1]['query']); + $this->assertEquals([1, 2], Capsule::getQueryLog()[1]['bindings']); + } +} diff --git a/tests/Models/Country.php b/tests/Models/Country.php index b69a626..6dc12cc 100644 --- a/tests/Models/Country.php +++ b/tests/Models/Country.php @@ -6,6 +6,11 @@ class Country extends Model { protected $primaryKey = 'country_pk'; + public function comment() + { + return $this->hasOneDeep(Comment::class, [User::class, Post::class])->withDefault(); + } + public function comments() { return $this->hasManyDeep(Comment::class, [User::class, Post::class]);