Skip to content

Commit

Permalink
Add HasOneDeep relationship
Browse files Browse the repository at this point in the history
  • Loading branch information
staudenmeir committed Nov 2, 2018
1 parent 1eb508c commit b188e07
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 1 deletion.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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');
}
}
```
72 changes: 72 additions & 0 deletions src/HasOneDeep.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

namespace Staudenmeir\EloquentHasManyDeep;

use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Concerns\SupportsDefaultModels;

class HasOneDeep extends HasManyDeep
{
use SupportsDefaultModels;

/**
* Get the results of the relationship.
*
* @return mixed
*/
public function getResults()
{
return $this->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();
}
}
45 changes: 44 additions & 1 deletion src/HasRelationships.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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];
}

/**
Expand All @@ -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);
}
}
46 changes: 46 additions & 0 deletions tests/HasOneDeepTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace Tests;

use Illuminate\Database\Capsule\Manager as Capsule;
use Tests\Models\Comment;
use Tests\Models\Country;

class HasOneDeepTest extends TestCase
{
public function testLazyLoading()
{
$comment = Country::first()->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']);
}
}
5 changes: 5 additions & 0 deletions tests/Models/Country.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down

0 comments on commit b188e07

Please sign in to comment.