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

[8.x] Add trait for models to detect their api resources and add some resource sugar #35515

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 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
41 changes: 41 additions & 0 deletions src/Illuminate/Http/Resources/HasResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace Illuminate\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

trait HasResource
{
/**
* Get a new resource instance for the given resource(s)
*
* @param mixed ...$parameters
* @return \Illuminate\Http\Resources\Json\JsonResource
*/
public static function resource(...$parameters)
{
return static::newResource(...$parameters)
?: JsonResource::resourceForModel(get_called_class(), ...$parameters);
}

/**
* Create a new resource instance for the model.
*
* @param static|null $model
* @return \Illuminate\Http\Resources\Json\JsonResource
*/
protected static function newResource($model = null)
{
//
}

/**
* Get the resource representation of the model
*
* @return \Illuminate\Http\Resources\Json\JsonResource
*/
public function toResource()
{
return static::resource($this);
}
}
117 changes: 116 additions & 1 deletion src/Illuminate/Http/Resources/Json/JsonResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Responsable;
use Illuminate\Database\Eloquent\JsonEncodingException;
use Illuminate\Foundation\Application;
use Illuminate\Http\Resources\ConditionallyLoadsAttributes;
use Illuminate\Http\Resources\DelegatesToResource;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use JsonSerializable;
use Throwable;

class JsonResource implements ArrayAccess, JsonSerializable, Responsable, UrlRoutable
{
Expand Down Expand Up @@ -46,13 +50,27 @@ class JsonResource implements ArrayAccess, JsonSerializable, Responsable, UrlRou
*/
public static $wrap = 'data';

/**
* The default namespace where factories reside.
*
* @var string
*/
protected static $namespace = 'App\\Http\\Resources\\';
grantholle marked this conversation as resolved.
Show resolved Hide resolved

/**
* The resource name resolver.
*
* @var callable
*/
protected static $resourceNameResolver;

/**
* Create a new resource instance.
*
* @param mixed $resource
* @return void
*/
public function __construct($resource)
public function __construct($resource = null)
{
$this->resource = $resource;
}
Expand All @@ -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);
}

Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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\\';
grantholle marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
3 changes: 3 additions & 0 deletions tests/Integration/Http/Fixtures/Post.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
87 changes: 87 additions & 0 deletions tests/Integration/Http/ResourceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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([
Expand Down Expand Up @@ -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([
Expand Down