Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat nested relations #364

Merged
merged 2 commits into from
Nov 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions src/Tools/Utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
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;
Expand Down Expand Up @@ -190,13 +192,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)) {
shalvah marked this conversation as resolved.
Show resolved Hide resolved
$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 {
Expand Down
14 changes: 14 additions & 0 deletions tests/Fixtures/TestPet.php
Original file line number Diff line number Diff line change
@@ -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');
}
}
32 changes: 32 additions & 0 deletions tests/Fixtures/TestPetApiResource.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
27 changes: 27 additions & 0 deletions tests/Fixtures/TestPetApiResourceCollection.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
5 changes: 5 additions & 0 deletions tests/Fixtures/TestUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,9 @@ public function children()
{
return $this->hasMany(TestUser::class, 'parent_id');
}

public function pets()
{
return $this->belongsToMany(TestPet::class)->withPivot('duration');
}
}
3 changes: 3 additions & 0 deletions tests/Fixtures/TestUserApiResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')) {
Expand Down
211 changes: 211 additions & 0 deletions tests/Strategies/Responses/UseApiResourceTagsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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()
{
Expand Down