Skip to content

Commit

Permalink
Add HasManyThroughJson relation
Browse files Browse the repository at this point in the history
  • Loading branch information
staudenmeir committed Nov 18, 2022
1 parent bce0d20 commit 2b2ec91
Show file tree
Hide file tree
Showing 3 changed files with 330 additions and 1 deletion.
142 changes: 141 additions & 1 deletion src/HasJsonRelationships.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Staudenmeir\EloquentJsonRelations;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
Expand All @@ -11,6 +12,7 @@
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use RuntimeException;
use Staudenmeir\EloquentJsonRelations\Relations\BelongsToJson;
use Staudenmeir\EloquentJsonRelations\Relations\HasManyJson;
use Staudenmeir\EloquentJsonRelations\Relations\Postgres\BelongsTo as BelongsToPostgres;
Expand Down Expand Up @@ -43,7 +45,7 @@ public function getAttribute($key)
/**
* Get an attribute from the $attributes array.
*
* @param string $key
* @param string $key
* @return mixed
*/
public function getAttributeFromArray($key)
Expand Down Expand Up @@ -295,4 +297,142 @@ protected function newHasManyJson(Builder $query, Model $parent, $foreignKey, $l
{
return new HasManyJson($query, $parent, $foreignKey, $localKey);
}

/**
* Define has-many-through JSON relationship.
*
* @param string $related
* @param string $through
* @param string|\Staudenmeir\EloquentJsonRelations\JsonKey $firstKey
* @param string|null $secondKey
* @param string|null $localKey
* @param string|\Staudenmeir\EloquentJsonRelations\JsonKey|null $secondLocalKey
* @return \Staudenmeir\EloquentHasManyDeep\HasManyDeep
*/
public function hasManyThroughJson(
string $related,
string $through,
string|JsonKey $firstKey,
string $secondKey = null,
string $localKey = null,
string|JsonKey $secondLocalKey = null
) {
$relationships = [];

$through = new $through();

if ($firstKey instanceof JsonKey) {
$relationships[] = $this->hasManyJson($through, $firstKey, $localKey);

$relationships[] = $through->hasMany($related, $secondKey, $secondLocalKey);
} else {
if (!method_exists($through, 'belongsToJson')) {
//@codeCoverageIgnoreStart
$message = 'Please add the HasJsonRelationships trait to the ' . $through::class . ' model.';

throw new RuntimeException($message);
// @codeCoverageIgnoreEnd
}

$relationships[] = $this->hasMany($through, $firstKey, $localKey);

$relationships[] = $through->belongsToJson($related, $secondLocalKey, $secondKey);
}

$hasManyThroughJson = $this->newHasManyThroughJson($relationships);

$jsonKey = $firstKey instanceof JsonKey ? $firstKey : $secondLocalKey;

if (str_contains($jsonKey, '[]->')) {
$this->addHasManyThroughJsonPivotRelationship($hasManyThroughJson, $relationships, $through);
}

return $hasManyThroughJson;
}

/**
* Add the pivot relationship to the has-many-through JSON relationship.
*
* @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep $hasManyThroughJson
* @param \Illuminate\Database\Eloquent\Relations\Relation[] $relationships
* @param \Illuminate\Database\Eloquent\Model $through
* @return void
*/
protected function addHasManyThroughJsonPivotRelationship(
$hasManyThroughJson,
array $relationships,
Model $through
): void {
if ($relationships[0] instanceof HasManyJson) {
/** @var \Staudenmeir\EloquentJsonRelations\Relations\HasManyJson $hasManyJson */
$hasManyJson = $relationships[0];

$postGetCallback = function (Collection $models) use ($hasManyJson, $relationships) {
if (isset($models[0]->laravel_through_key)) {
$hasManyJson->hydratePivotRelation(
$models,
$this,
fn (Model $model) => json_decode($model->laravel_through_key, true)
);
}
};

$localKey = $this->{$hasManyJson->getLocalKeyName()};

if (!is_null($localKey)) {
$hasManyThroughJson->withPostGetCallbacks([$postGetCallback]);
}

$hasManyThroughJson->withCustomEagerMatchingCallback(
function (array $models, Collection $results, string $relation) use ($hasManyJson, $hasManyThroughJson) {
foreach ($models as $model) {
$hasManyJson->hydratePivotRelation(
$model->$relation,
$model,
fn (Model $model) => json_decode($model->laravel_through_key, true)
);
}

return $models;
}
);
} else {
/** @var \Staudenmeir\EloquentJsonRelations\Relations\BelongsToJson $belongsToJson */
$belongsToJson = $relationships[1];

$path = $belongsToJson->getForeignKeyPath();

$postProcessor = function (Model $model, array $attributes) use ($belongsToJson, $path) {
$records = json_decode($attributes[$path], true);

return $belongsToJson->pivotAttributes($model, $model, $records);
};

$hasManyThroughJson->withPivot(
$through->getTable(),
[$path],
accessor: 'pivot',
postProcessor: $postProcessor
);
}
}

/**
* Instantiate a new HasManyThroughJson relationship.
*
* @param \Illuminate\Database\Eloquent\Relations\Relation[] $relationships
* @return \Staudenmeir\EloquentHasManyDeep\HasManyDeep
*/
protected function newHasManyThroughJson(array $relationships)
{
if (!method_exists($this, 'hasManyDeepFromRelations')) {
//@codeCoverageIgnoreStart
$message = 'Please install staudenmeir/eloquent-has-many-deep and add the HasRelationships trait to this model.';

throw new RuntimeException($message);
// @codeCoverageIgnoreEnd
}

return $this->hasManyDeepFromRelations($relationships);
}
}
16 changes: 16 additions & 0 deletions src/JsonKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Staudenmeir\EloquentJsonRelations;

class JsonKey
{
public function __construct(protected string $column)
{
//
}

public function __toString(): string
{
return $this->column;
}
}
173 changes: 173 additions & 0 deletions tests/Concatenation/HasManyThroughJsonTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
<?php

namespace Tests\Concatenation;

use Illuminate\Database\Capsule\Manager as DB;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Tests\Models\Role;
use Tests\Models\Project;
use Tests\TestCase;

class HasManyThroughJsonTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();

if (DB::connection()->getDriverName() === 'sqlite') {
$this->markTestSkipped();
}
}

public function testLazyLoading()
{
$projects = Role::find(2)->projects;

$this->assertEquals([71, 73], $projects->pluck('id')->all());
}

public function testLazyLoadingWithObjects()
{
if (DB::connection()->getDriverName() === 'sqlsrv') {
$this->markTestSkipped();
}

$projects = Role::find(2)->projects2;

$this->assertEquals([71, 73], $projects->pluck('id')->all());
$pivot = $projects[0]->pivot;
$this->assertInstanceOf(Pivot::class, $pivot);
$this->assertTrue($pivot->exists);
$this->assertEquals(['role' => ['active' => false]], $pivot->getAttributes());
}

public function testLazyLoadingWithReverseRelationship()
{
$roles = Project::find(71)->roles;

$this->assertEquals([1, 2], $roles->pluck('id')->all());
}

public function testLazyLoadingWithReverseRelationshipAndObjects()
{
if (DB::connection()->getDriverName() === 'sqlsrv') {
$this->markTestSkipped();
}

$roles = Project::find(71)->roles2;

$this->assertEquals([1, 2], $roles->pluck('id')->all());
$pivot = $roles[0]->pivot;
$this->assertInstanceOf(Pivot::class, $pivot);
$this->assertTrue($pivot->exists);
$this->assertEquals(['role' => ['active' => true]], $pivot->getAttributes());
}

public function testEmptyLazyLoading()
{
$projects = (new Role())->projects()->get();

$this->assertEmpty($projects);
}

public function testEmptyLazyLoadingWithReverseRelationship()
{
DB::enableQueryLog();

$roles = (new Project())->roles;

$this->assertInstanceOf(Collection::class, $roles);
$this->assertEmpty(DB::getQueryLog());
}

public function testEagerLoading()
{
$roles = Role::with('projects')->get();

$this->assertEquals([71], $roles[0]->projects->pluck('id')->all());
$this->assertEquals([71, 73], $roles[1]->projects->pluck('id')->all());
$this->assertEquals([73], $roles[2]->projects->pluck('id')->all());
$this->assertEquals([], $roles[3]->projects->pluck('id')->all());
}

public function testEagerLoadingWithObjects()
{
if (DB::connection()->getDriverName() === 'sqlsrv') {
$this->markTestSkipped();
}

$roles = Role::with('projects2')->get();

$this->assertEquals([71], $roles[0]->projects2->pluck('id')->all());
$this->assertEquals([71, 73], $roles[1]->projects2->pluck('id')->all());
$this->assertEquals([73], $roles[2]->projects2->pluck('id')->all());
$this->assertEquals([], $roles[3]->projects2->pluck('id')->all());
$pivot = $roles[1]->projects2[0]->pivot;
$this->assertInstanceOf(Pivot::class, $pivot);
$this->assertTrue($pivot->exists);
$this->assertEquals(['role' => ['active' => false]], $pivot->getAttributes());
}

public function testEagerLoadingWithReverseRelationship()
{
$projects = Project::with('roles')->get();

$this->assertEquals([1, 2], $projects[0]->roles->pluck('id')->all());
$this->assertEquals([], $projects[1]->roles->pluck('id')->all());
$this->assertEquals([2, 3], $projects[2]->roles->pluck('id')->all());
}

public function testEagerLoadingWithReverseRelationshipAndObjects()
{
if (DB::connection()->getDriverName() === 'sqlsrv') {
$this->markTestSkipped();
}

$projects = Project::with('roles2')->get();

$this->assertEquals([1, 2], $projects[0]->roles2->pluck('id')->all());
$this->assertEquals([], $projects[1]->roles2->pluck('id')->all());
$this->assertEquals([2, 3], $projects[2]->roles2->pluck('id')->all());
$pivot = $projects[0]->roles2[0]->pivot;
$this->assertInstanceOf(Pivot::class, $pivot);
$this->assertTrue($pivot->exists);
$this->assertEquals(['role' => ['active' => true]], $pivot->getAttributes());
}

public function testExistenceQuery()
{
$roles = Role::has('projects')->get();

$this->assertEquals([1, 2, 3], $roles->pluck('id')->all());
}

public function testExistenceQueryWithObjects()
{
if (DB::connection()->getDriverName() === 'sqlsrv') {
$this->markTestSkipped();
}

$roles = Role::has('projects2')->get();

$this->assertEquals([1, 2, 3], $roles->pluck('id')->all());
}

public function testExistenceQueryWithReverseRelationship()
{
$projects = Project::has('roles')->get();

$this->assertEquals([71, 73], $projects->pluck('id')->all());
}

public function testExistenceQueryWithReverseRelationshipAndObjects()
{
if (DB::connection()->getDriverName() === 'sqlsrv') {
$this->markTestSkipped();
}

$projects = Project::has('roles2')->get();

$this->assertEquals([71, 73], $projects->pluck('id')->all());
}
}

0 comments on commit 2b2ec91

Please sign in to comment.