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()
     {