diff --git a/src/Illuminate/Http/Resources/HasResource.php b/src/Illuminate/Http/Resources/HasResource.php new file mode 100644 index 000000000000..faa2ff25d35f --- /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|\Illuminate\Http\Resources\Json\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,95 @@ 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|null $callback + * @return void + */ + public static function guessResourceNamesUsing(callable $callback = null) + { + static::$resourceNameResolver = $callback; + } + + /** + * Specify the callback that should be invoked to get the namespace of the API resources. + * + * @param callable|null $callback + * @return void + */ + public static function resolveResourceNamespaceUsing(callable $callback = null) + { + static::$resourceNamespaceResolver = $callback; + } + + /** + * Get the namespace for the application's API resources. + * + * @return string + */ + public static function resolveResourceNamespace() + { + $resolver = static::$resourceNamespaceResolver ?: function () { + $appNamespace = static::appNamespace(); + + return $appNamespace.'Http\\Resources\\'; + }; + + return $resolver(); + } + + /** + * 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) { + $modelName = class_basename($modelName); + $resourceNamespace = static::resolveResourceNamespace(); + $resourceName = Str::endsWith($resourceNamespace, '\\') + ? $resourceNamespace.$modelName + : $resourceNamespace.'\\'.$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/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', + ]))->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 = class_basename($modelName); + + 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 testResourceClassCanBeDiscoveredBasedOnNamespaceResolution() + { + JsonResource::guessResourceNamesUsing(); + JsonResource::resolveResourceNamespaceUsing(function () { + return 'Illuminate\\Tests\\Integration\\Http\\Fixtures\\Resources\\'; + }); + + $resource = Post::resource(new Post([ + 'id' => 5, + 'title' => 'Test Title', + 'abstract' => 'Test abstract', + ])); + + $this->assertInstanceOf(\Illuminate\Tests\Integration\Http\Fixtures\Resources\PostResource::class, $resource); + 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([ @@ -771,6 +876,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([