From 8832e6b07762e0181d22d01506d64a13b6a2ecf0 Mon Sep 17 00:00:00 2001 From: Grant Holle Date: Mon, 7 Dec 2020 14:33:01 +0800 Subject: [PATCH 1/6] Add trait for models to detect their api resources and add some sugar around creating resources --- src/Illuminate/Http/Resources/HasResource.php | 41 ++++++ .../Http/Resources/Json/JsonResource.php | 117 +++++++++++++++++- tests/Integration/Http/Fixtures/Post.php | 3 + tests/Integration/Http/ResourceTest.php | 87 +++++++++++++ 4 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 src/Illuminate/Http/Resources/HasResource.php diff --git a/src/Illuminate/Http/Resources/HasResource.php b/src/Illuminate/Http/Resources/HasResource.php new file mode 100644 index 000000000000..8712e7081b83 --- /dev/null +++ b/src/Illuminate/Http/Resources/HasResource.php @@ -0,0 +1,41 @@ +resource = $resource; } @@ -65,6 +83,10 @@ public function __construct($resource) */ public static function make(...$parameters) { + if (($parameters[0] ?? null) instanceof Collection) { + return static::collection($parameters[0]); + } + return new static(...$parameters); } @@ -164,6 +186,23 @@ public function additional(array $data) return $this; } + /** + * Sets the resource for a model or collection of models + * + * @param mixed $resource + * @return $this|AnonymousResourceCollection + */ + public function for($resource) + { + if ($resource instanceof Collection) { + return static::collection($resource); + } + + $this->resource = $resource; + + return $this; + } + /** * Customize the response for a request. * @@ -230,4 +269,80 @@ public function jsonSerialize() { return $this->resolve(Container::getInstance()->make('request')); } + + /** + * Get a new resource instance for the given model name. + * + * @param string $modelName + * @param mixed ...$parameters + * @return static + */ + public static function resourceForModel(string $modelName, ...$parameters) + { + $resource = static::resolveResourceName($modelName); + + return $resource::make(...$parameters); + } + + /** + * Specify the callback that should be invoked to guess factory names based on dynamic relationship names. + * + * @param callable $callback + * @return void + */ + public static function guessResourceNamesUsing(callable $callback) + { + static::$resourceNameResolver = $callback; + } + + /** + * Specify the default namespace that contains the application's API resources. + * + * @param string $namespace + * @return void + */ + public static function useNamespace(string $namespace) + { + static::$namespace = $namespace; + } + + /** + * Get the resource name for the given model name. + * + * @param string $modelName + * @return string + */ + public static function resolveResourceName(string $modelName) + { + $resolver = static::$resourceNameResolver ?: function (string $modelName) { + $appNamespace = static::appNamespace(); + + $modelName = Str::startsWith($modelName, $appNamespace.'Models\\') + ? Str::after($modelName, $appNamespace.'Models\\') + : Str::after($modelName, $appNamespace); + $resourceName = static::$namespace.$modelName; + + return class_exists($resourceName) + ? $resourceName + : $resourceName.'Resource'; + }; + + return $resolver($modelName); + } + + /** + * Get the application namespace for the application. + * + * @return string + */ + protected static function appNamespace() + { + try { + return Container::getInstance() + ->make(Application::class) + ->getNamespace(); + } catch (Throwable $e) { + return 'App\\'; + } + } } diff --git a/tests/Integration/Http/Fixtures/Post.php b/tests/Integration/Http/Fixtures/Post.php index 2eb1df88986e..b8a949830123 100644 --- a/tests/Integration/Http/Fixtures/Post.php +++ b/tests/Integration/Http/Fixtures/Post.php @@ -3,9 +3,12 @@ namespace Illuminate\Tests\Integration\Http\Fixtures; use Illuminate\Database\Eloquent\Model; +use Illuminate\Http\Resources\HasResource; class Post extends Model { + use HasResource; + /** * The attributes that aren't mass assignable. * diff --git a/tests/Integration/Http/ResourceTest.php b/tests/Integration/Http/ResourceTest.php index 2553fd49c1e8..d4ecb49dccb2 100644 --- a/tests/Integration/Http/ResourceTest.php +++ b/tests/Integration/Http/ResourceTest.php @@ -9,6 +9,7 @@ use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Route; +use Illuminate\Support\Str; use Illuminate\Tests\Integration\Http\Fixtures\Author; use Illuminate\Tests\Integration\Http\Fixtures\AuthorResourceWithOptionalRelationship; use Illuminate\Tests\Integration\Http\Fixtures\EmptyPostCollectionResource; @@ -58,6 +59,68 @@ public function testResourcesMayBeConvertedToJson() ]); } + public function testResourcesMayBeConvertedToJsonUsingToResourceTraitFunction() + { + JsonResource::guessResourceNamesUsing(function ($modelName) { + $namespace = 'Illuminate\\Tests\\Integration\\Http\\Fixtures\\'; + $modelName = Str::after($modelName, $namespace); + + return $namespace.$modelName.'Resource'; + }); + + Route::get('/', function () { + return (new Post([ + 'id' => 5, + 'title' => 'Test Title', + 'abstract' => 'Test abstract', + ]))->toResource(); + }); + + $response = $this->withoutExceptionHandling()->get( + '/', ['Accept' => 'application/json'] + ); + + $response->assertStatus(200); + + $response->assertJson([ + 'data' => [ + 'id' => 5, + 'title' => 'Test Title', + ], + ]); + } + + public function testResourcesMayBeConvertedToJsonUsingForResourceFunction() + { + JsonResource::guessResourceNamesUsing(function ($modelName) { + $namespace = 'Illuminate\\Tests\\Integration\\Http\\Fixtures\\'; + $modelName = Str::after($modelName, $namespace); + + return $namespace.$modelName.'Resource'; + }); + + Route::get('/', function () { + return Post::resource()->for(new Post([ + 'id' => 5, + 'title' => 'Test Title', + 'abstract' => 'Test abstract', + ])); + }); + + $response = $this->withoutExceptionHandling()->get( + '/', ['Accept' => 'application/json'] + ); + + $response->assertStatus(200); + + $response->assertJson([ + 'data' => [ + 'id' => 5, + 'title' => 'Test Title', + ], + ]); + } + public function testResourcesMayBeConvertedToJsonWithToJsonMethod() { $resource = new PostResource(new Post([ @@ -771,6 +834,30 @@ public function testOriginalOnResponseIsCollectionOfModelWhenCollectionResource( }); } + public function testCollectionCanBeConvertedToResourceWhenUsingResourceForFunction() + { + JsonResource::guessResourceNamesUsing(function ($modelName) { + return PostResource::class; + }); + + $createdPosts = collect([ + new Post(['id' => 5, 'title' => 'Test Title']), + new Post(['id' => 6, 'title' => 'Test Title 2']), + ]); + + Route::get('/', function () use ($createdPosts) { + return Post::resource()->for($createdPosts); + }); + + $response = $this->withoutExceptionHandling()->get( + '/', ['Accept' => 'application/json'] + ); + + $createdPosts->each(function ($post) use ($response) { + $this->assertTrue($response->getOriginalContent()->contains($post->toResource())); + }); + } + public function testCollectionResourcesAreCountable() { $posts = collect([ From d12ce2e389c0044d4cb536cd23ec0b7829063157 Mon Sep 17 00:00:00 2001 From: Grant Holle Date: Tue, 8 Dec 2020 08:39:00 +0800 Subject: [PATCH 2/6] Fix doc blocks --- src/Illuminate/Http/Resources/HasResource.php | 8 ++++---- src/Illuminate/Http/Resources/Json/JsonResource.php | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Http/Resources/HasResource.php b/src/Illuminate/Http/Resources/HasResource.php index 8712e7081b83..fa1cc8f5e249 100644 --- a/src/Illuminate/Http/Resources/HasResource.php +++ b/src/Illuminate/Http/Resources/HasResource.php @@ -10,7 +10,7 @@ trait HasResource * Get a new resource instance for the given resource(s) * * @param mixed ...$parameters - * @return JsonResource + * @return \Illuminate\Http\Resources\Json\JsonResource */ public static function resource(...$parameters) { @@ -21,8 +21,8 @@ public static function resource(...$parameters) /** * Create a new resource instance for the model. * - * @param static $model - * @return JsonResource + * @param static|null $model + * @return \Illuminate\Http\Resources\Json\JsonResource */ protected static function newResource($model = null) { @@ -32,7 +32,7 @@ protected static function newResource($model = null) /** * Get the resource representation of the model * - * @return JsonResource + * @return \Illuminate\Http\Resources\Json\JsonResource */ public function toResource() { diff --git a/src/Illuminate/Http/Resources/Json/JsonResource.php b/src/Illuminate/Http/Resources/Json/JsonResource.php index 866e8dd8ab5f..1c5709d49f70 100644 --- a/src/Illuminate/Http/Resources/Json/JsonResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonResource.php @@ -190,7 +190,7 @@ public function additional(array $data) * Sets the resource for a model or collection of models * * @param mixed $resource - * @return $this|AnonymousResourceCollection + * @return $this|\Illuminate\Http\Resources\Json\AnonymousResourceCollection */ public function for($resource) { From 874109759e6d4815c14fffeaf4574b3cf7a45858 Mon Sep 17 00:00:00 2001 From: Grant Holle Date: Tue, 8 Dec 2020 22:31:18 +0800 Subject: [PATCH 3/6] Clean up how the application resource namespace is resolved --- .../Http/Resources/Json/JsonResource.php | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/src/Illuminate/Http/Resources/Json/JsonResource.php b/src/Illuminate/Http/Resources/Json/JsonResource.php index 1c5709d49f70..ddfc88fe79a0 100644 --- a/src/Illuminate/Http/Resources/Json/JsonResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonResource.php @@ -51,18 +51,18 @@ class JsonResource implements ArrayAccess, JsonSerializable, Responsable, UrlRou public static $wrap = 'data'; /** - * The default namespace where factories reside. + * The resource name resolver. * - * @var string + * @var callable */ - protected static $namespace = 'App\\Http\\Resources\\'; + protected static $resourceNameResolver; /** - * The resource name resolver. + * The resource namespace resolver. * * @var callable */ - protected static $resourceNameResolver; + protected static $resourceNamespaceResolver; /** * Create a new resource instance. @@ -296,14 +296,30 @@ public static function guessResourceNamesUsing(callable $callback) } /** - * Specify the default namespace that contains the application's API resources. + * Specify the callback that should be invoked to get the namespace of the API resources. * - * @param string $namespace + * @param callable $callback * @return void */ - public static function useNamespace(string $namespace) + public static function resolveResourceNamespaceUsing(callable $callback) + { + static::$resourceNamespaceResolver = $callback; + } + + /** + * Get the namespace for the application's API resources. + * + * @return string + */ + public static function resolveResourceNamespace() { - static::$namespace = $namespace; + $resolver = static::$resourceNamespaceResolver ?: function () { + $appNamespace = static::appNamespace(); + + return $appNamespace.'Http\\Resources\\'; + }; + + return $resolver(); } /** @@ -315,12 +331,11 @@ public static function useNamespace(string $namespace) public static function resolveResourceName(string $modelName) { $resolver = static::$resourceNameResolver ?: function (string $modelName) { - $appNamespace = static::appNamespace(); - - $modelName = Str::startsWith($modelName, $appNamespace.'Models\\') - ? Str::after($modelName, $appNamespace.'Models\\') - : Str::after($modelName, $appNamespace); - $resourceName = static::$namespace.$modelName; + $modelName = class_basename($modelName); + $resourceNamespace = static::resolveResourceNamespace(); + $resourceName = Str::endsWith($resourceNamespace, '\\') + ? $resourceNamespace.$modelName + : $resourceNamespace.'\\'.$modelName; return class_exists($resourceName) ? $resourceName From 60d65217db4130f6bfcfcc615c5556dc14596809 Mon Sep 17 00:00:00 2001 From: Grant Holle Date: Mon, 14 Dec 2020 09:02:50 +0800 Subject: [PATCH 4/6] Increase test coverage for namespace resolution --- src/Illuminate/Http/Resources/HasResource.php | 4 ++-- .../Http/Fixtures/Resources/PostResource.php | 9 +++++++++ tests/Integration/Http/ResourceTest.php | 17 ++++++++++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 tests/Integration/Http/Fixtures/Resources/PostResource.php diff --git a/src/Illuminate/Http/Resources/HasResource.php b/src/Illuminate/Http/Resources/HasResource.php index fa1cc8f5e249..faa2ff25d35f 100644 --- a/src/Illuminate/Http/Resources/HasResource.php +++ b/src/Illuminate/Http/Resources/HasResource.php @@ -7,7 +7,7 @@ trait HasResource { /** - * Get a new resource instance for the given resource(s) + * Get a new resource instance for the given resource(s). * * @param mixed ...$parameters * @return \Illuminate\Http\Resources\Json\JsonResource @@ -30,7 +30,7 @@ protected static function newResource($model = null) } /** - * Get the resource representation of the model + * Get the resource representation of the model. * * @return \Illuminate\Http\Resources\Json\JsonResource */ diff --git a/tests/Integration/Http/Fixtures/Resources/PostResource.php b/tests/Integration/Http/Fixtures/Resources/PostResource.php new file mode 100644 index 000000000000..df90d50558a5 --- /dev/null +++ b/tests/Integration/Http/Fixtures/Resources/PostResource.php @@ -0,0 +1,9 @@ + 5, + 'title' => 'Test Title', + 'abstract' => 'Test abstract', + ])); + + $this->assertInstanceOf(\Illuminate\Tests\Integration\Http\Fixtures\Resources\PostResource::class, $resource); + } + public function testResourcesMayBeConvertedToJsonWithToJsonMethod() { $resource = new PostResource(new Post([ From 13e36b2f9fbbc49d59eacc435824503937131a95 Mon Sep 17 00:00:00 2001 From: Grant Holle Date: Mon, 14 Dec 2020 09:19:46 +0800 Subject: [PATCH 5/6] Allow resolutions to be reset --- src/Illuminate/Http/Resources/Json/JsonResource.php | 8 ++++---- tests/Integration/Http/ResourceTest.php | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Http/Resources/Json/JsonResource.php b/src/Illuminate/Http/Resources/Json/JsonResource.php index ddfc88fe79a0..23d124b11691 100644 --- a/src/Illuminate/Http/Resources/Json/JsonResource.php +++ b/src/Illuminate/Http/Resources/Json/JsonResource.php @@ -287,10 +287,10 @@ public static function resourceForModel(string $modelName, ...$parameters) /** * Specify the callback that should be invoked to guess factory names based on dynamic relationship names. * - * @param callable $callback + * @param callable|null $callback * @return void */ - public static function guessResourceNamesUsing(callable $callback) + public static function guessResourceNamesUsing(callable $callback = null) { static::$resourceNameResolver = $callback; } @@ -298,10 +298,10 @@ public static function guessResourceNamesUsing(callable $callback) /** * Specify the callback that should be invoked to get the namespace of the API resources. * - * @param callable $callback + * @param callable|null $callback * @return void */ - public static function resolveResourceNamespaceUsing(callable $callback) + public static function resolveResourceNamespaceUsing(callable $callback = null) { static::$resourceNamespaceResolver = $callback; } diff --git a/tests/Integration/Http/ResourceTest.php b/tests/Integration/Http/ResourceTest.php index 5132374488a8..b958e57dcbb1 100644 --- a/tests/Integration/Http/ResourceTest.php +++ b/tests/Integration/Http/ResourceTest.php @@ -123,6 +123,7 @@ public function testResourcesMayBeConvertedToJsonUsingForResourceFunction() public function testResourceClassCanBeDiscoveredBasedOnNamespaceResolution() { + JsonResource::guessResourceNamesUsing(); JsonResource::resolveResourceNamespaceUsing(function () { return 'Illuminate\\Tests\\Integration\\Http\\Fixtures\\Resources\\'; }); @@ -134,6 +135,7 @@ public function testResourceClassCanBeDiscoveredBasedOnNamespaceResolution() ])); $this->assertInstanceOf(\Illuminate\Tests\Integration\Http\Fixtures\Resources\PostResource::class, $resource); + JsonResource::resolveResourceNamespaceUsing(); } public function testResourcesMayBeConvertedToJsonWithToJsonMethod() From 92326802d73bcfa40b061f35046adeade1723526 Mon Sep 17 00:00:00 2001 From: Grant Holle Date: Mon, 14 Dec 2020 09:28:31 +0800 Subject: [PATCH 6/6] Add test for passing collection directly to resource call --- tests/Integration/Http/ResourceTest.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/Integration/Http/ResourceTest.php b/tests/Integration/Http/ResourceTest.php index b958e57dcbb1..2feffca3c3b7 100644 --- a/tests/Integration/Http/ResourceTest.php +++ b/tests/Integration/Http/ResourceTest.php @@ -3,6 +3,7 @@ namespace Illuminate\Tests\Integration\Http; use Illuminate\Http\Resources\ConditionallyLoadsAttributes; +use Illuminate\Http\Resources\Json\AnonymousResourceCollection; use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\MergeValue; use Illuminate\Http\Resources\MissingValue; @@ -138,6 +139,30 @@ public function testResourceClassCanBeDiscoveredBasedOnNamespaceResolution() JsonResource::resolveResourceNamespaceUsing(); } + public function testResourceClassCanBeDiscoveredBasedOnNamespaceResolutionForACollection() + { + JsonResource::guessResourceNamesUsing(); + JsonResource::resolveResourceNamespaceUsing(function () { + return 'Illuminate\\Tests\\Integration\\Http\\Fixtures\\Resources\\'; + }); + + $resource = Post::resource(collect([ + new Post([ + 'id' => 5, + 'title' => 'Test Title', + 'abstract' => 'Test abstract', + ]), + new Post([ + 'id' => 6, + 'title' => 'Test Title 2', + 'abstract' => 'Test abstract 2', + ]), + ])); + + $this->assertInstanceOf(AnonymousResourceCollection::class, $resource); + JsonResource::resolveResourceNamespaceUsing(); + } + public function testResourcesMayBeConvertedToJsonWithToJsonMethod() { $resource = new PostResource(new Post([