diff --git a/src/HasJsonRelationships.php b/src/HasJsonRelationships.php index e6b70cf..aea32ce 100644 --- a/src/HasJsonRelationships.php +++ b/src/HasJsonRelationships.php @@ -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; @@ -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; @@ -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) @@ -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); + } } diff --git a/src/JsonKey.php b/src/JsonKey.php new file mode 100644 index 0000000..58a20ec --- /dev/null +++ b/src/JsonKey.php @@ -0,0 +1,16 @@ +column; + } +} diff --git a/tests/Concatenation/HasManyThroughJsonTest.php b/tests/Concatenation/HasManyThroughJsonTest.php new file mode 100644 index 0000000..56b0de3 --- /dev/null +++ b/tests/Concatenation/HasManyThroughJsonTest.php @@ -0,0 +1,173 @@ +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()); + } +}