From 1c4e9f9dc4da78c937057c4ec7fcb75571ef431c Mon Sep 17 00:00:00 2001 From: Tomasz Kisiel <tkisiel5w4@yahoo.com> Date: Thu, 11 Nov 2021 04:36:08 +0100 Subject: [PATCH] Feat nested relations --- src/Tools/Utils.php | 29 ++- tests/Fixtures/TestPet.php | 14 ++ tests/Fixtures/TestPetApiResource.php | 32 +++ .../Fixtures/TestPetApiResourceCollection.php | 27 +++ tests/Fixtures/TestUser.php | 5 + tests/Fixtures/TestUserApiResource.php | 3 + .../Responses/UseApiResourceTagsTest.php | 211 ++++++++++++++++++ 7 files changed, 317 insertions(+), 4 deletions(-) create mode 100644 tests/Fixtures/TestPet.php create mode 100644 tests/Fixtures/TestPetApiResource.php create mode 100644 tests/Fixtures/TestPetApiResourceCollection.php diff --git a/src/Tools/Utils.php b/src/Tools/Utils.php index 96cddda0..006de9e3 100644 --- a/src/Tools/Utils.php +++ b/src/Tools/Utils.php @@ -2,10 +2,13 @@ namespace Knuckles\Scribe\Tools; +use _PHPStan_76800bfb5\Nette\PhpGenerator\PhpFile; use Closure; use DirectoryIterator; use Exception; use FastRoute\RouteParser\Std; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Routing\Route; use Illuminate\Support\Str; use Knuckles\Scribe\Exceptions\CouldntFindFactory; @@ -190,13 +193,31 @@ public static function getModelFactory(string $modelName, array $states = [], ar /** @var \Illuminate\Database\Eloquent\Factories\Factory $factory */ $factory = call_user_func_array([$modelName, 'factory'], []); foreach ($states as $state) { - $factory = $factory->$state(); + if (method_exists(get_class($factory), $state)) { + $factory = $factory->$state(); + } } foreach ($relations as $relation) { - // Eg "posts" relation becomes hasPosts() method - $methodName = "has$relation"; - $factory = $factory->$methodName(); + $relationChain = explode('.', $relation); + $relationVector = array_shift($relationChain); + + $relationModel = get_class((new $modelName())->{$relationVector}()->getModel()); + $relationType = get_class((new $modelName())->{$relationVector}()); + + $factoryChain = empty($relationChain) + ? call_user_func_array([$relationModel, 'factory'], []) + : Utils::getModelFactory($relationModel, $states, $relationChain); + + if ($relationType === BelongsToMany::class) { + $pivot = method_exists($factory, 'pivot' . $relationVector) + ? $factory->{'pivot' . $relationVector}() + : []; + + $factory = $factory->hasAttached($factoryChain, $pivot, $relationVector); + } else { + $factory = $factory->has($factoryChain, $relationVector); + } } } else { try { diff --git a/tests/Fixtures/TestPet.php b/tests/Fixtures/TestPet.php new file mode 100644 index 00000000..1b1e0b16 --- /dev/null +++ b/tests/Fixtures/TestPet.php @@ -0,0 +1,14 @@ +<?php + +namespace Knuckles\Scribe\Tests\Fixtures; + +use Illuminate\Database\Eloquent\Model; + +class TestPet extends Model +{ + + public function owners() + { + return $this->belongsToMany(TestUser::class)->withPivot('duration'); + } +} diff --git a/tests/Fixtures/TestPetApiResource.php b/tests/Fixtures/TestPetApiResource.php new file mode 100644 index 00000000..ef6e85ec --- /dev/null +++ b/tests/Fixtures/TestPetApiResource.php @@ -0,0 +1,32 @@ +<?php + +namespace Knuckles\Scribe\Tests\Fixtures; + +use Illuminate\Http\Resources\Json\JsonResource; + +class TestPetApiResource extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * + * @return array + */ + public function toArray($request) + { + $result = [ + 'id' => $this->id, + 'name' => $this->name, + 'species' => $this->species, + 'owners' => $this->whenLoaded('owners', function () { + return TestUserApiResource::collection($this->owners); + }), + 'ownership' => $this->whenPivotLoaded('pet_user', function () { + return $this->pivot; + }) + ]; + + return $result; + } +} diff --git a/tests/Fixtures/TestPetApiResourceCollection.php b/tests/Fixtures/TestPetApiResourceCollection.php new file mode 100644 index 00000000..a072cc90 --- /dev/null +++ b/tests/Fixtures/TestPetApiResourceCollection.php @@ -0,0 +1,27 @@ +<?php + +namespace Knuckles\Scribe\Tests\Fixtures; + +use Illuminate\Http\Resources\Json\ResourceCollection; + +class TestPetApiResourceCollection extends ResourceCollection +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * + * @return array + */ + public function toArray($request) + { + $data = [ + 'data' => $this->collection, + 'links' => [ + 'self' => 'link-value', + ], + ]; + + return $data; + } +} diff --git a/tests/Fixtures/TestUser.php b/tests/Fixtures/TestUser.php index 386dbe52..299ec76e 100644 --- a/tests/Fixtures/TestUser.php +++ b/tests/Fixtures/TestUser.php @@ -11,4 +11,9 @@ public function children() { return $this->hasMany(TestUser::class, 'parent_id'); } + + public function pets() + { + return $this->belongsToMany(TestPet::class)->withPivot('duration'); + } } diff --git a/tests/Fixtures/TestUserApiResource.php b/tests/Fixtures/TestUserApiResource.php index 2c0c8393..984f16ab 100644 --- a/tests/Fixtures/TestUserApiResource.php +++ b/tests/Fixtures/TestUserApiResource.php @@ -22,6 +22,9 @@ public function toArray($request) 'children' => $this->whenLoaded('children', function () { return TestUserApiResource::collection($this->children); }), + 'pets' => $this->whenLoaded('pets', function () { + return TestPetApiResource::collection($this->pets); + }), ]; if($request->route()->named('someone')) { diff --git a/tests/Strategies/Responses/UseApiResourceTagsTest.php b/tests/Strategies/Responses/UseApiResourceTagsTest.php index 1cb1a6bc..831c9bc4 100644 --- a/tests/Strategies/Responses/UseApiResourceTagsTest.php +++ b/tests/Strategies/Responses/UseApiResourceTagsTest.php @@ -8,6 +8,7 @@ use Knuckles\Scribe\ScribeServiceProvider; use Knuckles\Scribe\Tests\BaseLaravelTest; use Knuckles\Scribe\Tests\Fixtures\TestController; +use Knuckles\Scribe\Tests\Fixtures\TestPet; use Knuckles\Scribe\Tests\Fixtures\TestUser; use Knuckles\Scribe\Tools\DocumentationConfig; use Knuckles\Scribe\Tools\Utils; @@ -49,6 +50,13 @@ public function setUp(): void }); $factory->state(TestUser::class, 'state1', ["state1" => true]); $factory->state(TestUser::class, 'random-state', ["random-state" => true]); + $factory->define(TestPet::class, function () { + return [ + 'id' => 1, + 'name' => 'Mephistopheles', + 'species' => 'dog', + ]; + }); } /** @test */ @@ -221,6 +229,209 @@ public function loads_specified_relations_for_generated_model() ], $results); } + /** @test */ + public function loads_specified_nested_relations_for_generated_model() + { + $factory = app(\Illuminate\Database\Eloquent\Factory::class); + $factory->afterMaking(TestUser::class, function (TestUser $user, $faker) { + if ($user->id === 4) { + $child = Utils::getModelFactory(TestUser::class)->make(['id' => 5, 'parent_id' => 4]); + $user->setRelation('children', collect([$child])); + + $grandchild = Utils::getModelFactory(TestUser::class)->make(['id' => 6, 'parent_id' => 5]); + $child->setRelation('children', collect([$grandchild])); + } + }); + + $config = new DocumentationConfig([]); + + $route = new Route(['POST'], "/somethingRandom", ['uses' => [TestController::class, 'dummy']]); + + $strategy = new UseApiResourceTags($config); + $tags = [ + new Tag('apiResource', '\Knuckles\Scribe\Tests\Fixtures\TestUserApiResource'), + new Tag('apiResourceModel', '\Knuckles\Scribe\Tests\Fixtures\TestUser with=children.children'), + ]; + $results = $strategy->getApiResourceResponse($strategy->getApiResourceTag($tags), $tags, ExtractedEndpointData::fromRoute($route)); + + $this->assertArraySubset([ + [ + 'status' => 200, + 'content' => json_encode([ + 'data' => [ + 'id' => 4, + 'name' => 'Tested Again', + 'email' => 'a@b.com', + 'children' => [ + [ + 'id' => 5, + 'name' => 'Tested Again', + 'email' => 'a@b.com', + 'children' => [ + [ + 'id' => 6, + 'name' => 'Tested Again', + 'email' => 'a@b.com', + ] + ] + ], + ], + ], + ]), + ], + ], $results); + } + + /** @test */ + public function loads_specified_many_to_many_relations_for_generated_model() + { + $factory = app(\Illuminate\Database\Eloquent\Factory::class); + $factory->afterMaking(TestUser::class, function (TestUser $user, $faker) { + $pet = Utils::getModelFactory(TestPet::class)->make(['id' => 1]); + $user->setRelation('pets', collect([$pet])); + }); + + $config = new DocumentationConfig([]); + + $route = new Route(['POST'], "/somethingRandom", ['uses' => [TestController::class, 'dummy']]); + + $strategy = new UseApiResourceTags($config); + $tags = [ + new Tag('apiResource', '\Knuckles\Scribe\Tests\Fixtures\TestUserApiResource'), + new Tag('apiResourceModel', '\Knuckles\Scribe\Tests\Fixtures\TestUser with=pets'), + ]; + $results = $strategy->getApiResourceResponse($strategy->getApiResourceTag($tags), $tags, ExtractedEndpointData::fromRoute($route)); + + $this->assertArraySubset([ + [ + 'status' => 200, + 'content' => json_encode([ + 'data' => [ + 'id' => 4, + 'name' => 'Tested Again', + 'email' => 'a@b.com', + 'pets' => [ + [ + 'id' => 1, + 'name' => 'Mephistopheles', + 'species' => 'dog' + ], + ], + ], + ]), + ], + ], $results); + } + + /** @test */ + public function loads_specified_many_to_many_and_nested_relations_for_generated_model() + { + $factory = app(\Illuminate\Database\Eloquent\Factory::class); + $factory->afterMaking(TestUser::class, function (TestUser $user, $faker) { + if ($user->id === 4) { + $child = Utils::getModelFactory(TestUser::class)->make(['id' => 5, 'parent_id' => 4]); + $user->setRelation('children', collect([$child])); + + $pet = Utils::getModelFactory(TestPet::class)->make(['id' => 1]); + $child->setRelation('pets', collect([$pet])); + } + }); + + $config = new DocumentationConfig([]); + + $route = new Route(['POST'], "/somethingRandom", ['uses' => [TestController::class, 'dummy']]); + + $strategy = new UseApiResourceTags($config); + $tags = [ + new Tag('apiResource', '\Knuckles\Scribe\Tests\Fixtures\TestUserApiResource'), + new Tag('apiResourceModel', '\Knuckles\Scribe\Tests\Fixtures\TestUser with=children.pets'), + ]; + $results = $strategy->getApiResourceResponse($strategy->getApiResourceTag($tags), $tags, ExtractedEndpointData::fromRoute($route)); + + $this->assertArraySubset([ + [ + 'status' => 200, + 'content' => json_encode([ + 'data' => [ + 'id' => 4, + 'name' => 'Tested Again', + 'email' => 'a@b.com', + 'children' => [ + [ + 'id' => 5, + 'name' => 'Tested Again', + 'email' => 'a@b.com', + 'pets' => [ + [ + 'id' => 1, + 'name' => 'Mephistopheles', + 'species' => 'dog' + ], + ], + ] + ] + + ], + ]), + ], + ], $results); + } + + /** @test */ + public function loads_specified_many_to_many_relations_for_generated_model_with_pivot() + { + $factory = app(\Illuminate\Database\Eloquent\Factory::class); + $factory->afterMaking(TestUser::class, function (TestUser $user, $faker) { + $pet = Utils::getModelFactory(TestPet::class)->make(['id' => 1]); + + $pivot = $pet->newPivot($user, [ + 'pet_id' => $pet->id, + 'user_id' => $user->id, + 'duration' => 2 + ], 'pet_user', true); + + $pet->setRelation('pivot', $pivot); + + $user->setRelation('pets', collect([$pet])); + }); + + $config = new DocumentationConfig([]); + + $route = new Route(['POST'], "/somethingRandom", ['uses' => [TestController::class, 'dummy']]); + + $strategy = new UseApiResourceTags($config); + $tags = [ + new Tag('apiResource', '\Knuckles\Scribe\Tests\Fixtures\TestUserApiResource'), + new Tag('apiResourceModel', '\Knuckles\Scribe\Tests\Fixtures\TestUser with=pets'), + ]; + $results = $strategy->getApiResourceResponse($strategy->getApiResourceTag($tags), $tags, ExtractedEndpointData::fromRoute($route)); + + $this->assertArraySubset([ + [ + 'status' => 200, + 'content' => json_encode([ + 'data' => [ + 'id' => 4, + 'name' => 'Tested Again', + 'email' => 'a@b.com', + 'pets' => [ + [ + 'id' => 1, + 'name' => 'Mephistopheles', + 'species' => 'dog', + 'ownership' => [ + 'pet_id' => 1, + 'user_id' => 4, + 'duration' => 2 + ] + ], + ], + ], + ]), + ], + ], $results); + } + /** @test */ public function can_parse_apiresourcecollection_tags() {