Skip to content

Commit cc889e6

Browse files
[12.x] Add resource helper functions to Model/Collections (#55107)
* Add resource helper functions * Fix doc-block style * Remove empty line * Make resource param optional and guess resource name * Use throw_unless instead of assert * Extract name guessing function * Add tests for resource helpers * Fix formatting * Fix namespace conflicts and add current page * Use more descriptive LogicException instead of Exception * Refactor to use traits and extend base collection instead of eloquent collection * Ensure trait method exists * formatting * formatting * formatting * adjust tests and logic * add testS * remove comment --------- Co-authored-by: Taylor Otwell <[email protected]>
1 parent d8dff6d commit cc889e6

12 files changed

+361
-3
lines changed

src/Illuminate/Collections/Collection.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use ArrayAccess;
66
use ArrayIterator;
77
use Illuminate\Contracts\Support\CanBeEscapedWhenCastToString;
8+
use Illuminate\Http\Resources\TransformsToResourceCollection;
89
use Illuminate\Support\Traits\EnumeratesValues;
910
use Illuminate\Support\Traits\Macroable;
1011
use InvalidArgumentException;
@@ -24,7 +25,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
2425
/**
2526
* @use \Illuminate\Support\Traits\EnumeratesValues<TKey, TValue>
2627
*/
27-
use EnumeratesValues, Macroable;
28+
use EnumeratesValues, Macroable, TransformsToResourceCollection;
2829

2930
/**
3031
* The items contained in the collection.

src/Illuminate/Database/Eloquent/Model.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Illuminate\Database\Eloquent\Relations\Concerns\AsPivot;
1818
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
1919
use Illuminate\Database\Eloquent\Relations\Pivot;
20+
use Illuminate\Http\Resources\TransformsToResource;
2021
use Illuminate\Support\Arr;
2122
use Illuminate\Support\Collection as BaseCollection;
2223
use Illuminate\Support\Str;
@@ -39,7 +40,8 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt
3940
Concerns\HidesAttributes,
4041
Concerns\GuardsAttributes,
4142
Concerns\PreventsCircularRecursion,
42-
ForwardsCalls;
43+
ForwardsCalls,
44+
TransformsToResource;
4345
/** @use HasCollection<\Illuminate\Database\Eloquent\Collection<array-key, static & self>> */
4446
use HasCollection;
4547

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
namespace Illuminate\Http\Resources;
4+
5+
use Illuminate\Http\Resources\Json\JsonResource;
6+
use Illuminate\Support\Str;
7+
use LogicException;
8+
9+
trait TransformsToResource
10+
{
11+
/**
12+
* Create a new resource object for the given resource.
13+
*
14+
* @param class-string<JsonResource>|null $resourceClass
15+
* @return JsonResource
16+
*
17+
* @throws \Throwable
18+
*/
19+
public function toResource(?string $resourceClass = null): JsonResource
20+
{
21+
if ($resourceClass === null) {
22+
return $this->guessResource();
23+
}
24+
25+
return $resourceClass::make($this);
26+
}
27+
28+
/**
29+
* Guess the resource class for the model.
30+
*
31+
* @return JsonResource
32+
*
33+
* @throws \Throwable
34+
*/
35+
protected function guessResource(): JsonResource
36+
{
37+
foreach (static::guessResourceName() as $resourceClass) {
38+
if (is_string($resourceClass) && class_exists($resourceClass)) {
39+
return $resourceClass::make($this);
40+
}
41+
}
42+
43+
throw new LogicException(sprintf('Failed to find resource class for model [%s].', get_class($this)));
44+
}
45+
46+
/**
47+
* Guess the resource class name for the model.
48+
*
49+
* @return array<class-string<JsonResource>>
50+
*/
51+
public static function guessResourceName(): array
52+
{
53+
$modelClass = static::class;
54+
55+
if (! Str::contains($modelClass, '\\Models\\')) {
56+
return [];
57+
}
58+
59+
$relativeNamespace = Str::after($modelClass, '\\Models\\');
60+
61+
$relativeNamespace = Str::contains($relativeNamespace, '\\')
62+
? Str::before($relativeNamespace, '\\'.class_basename($modelClass))
63+
: '';
64+
65+
$potentialResource = sprintf(
66+
'%s\\Http\\Resources\\%s%s',
67+
Str::before($modelClass, '\\Models'),
68+
strlen($relativeNamespace) > 0 ? $relativeNamespace.'\\' : '',
69+
class_basename($modelClass)
70+
);
71+
72+
return [$potentialResource.'Resource', $potentialResource];
73+
}
74+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
namespace Illuminate\Http\Resources;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
use Illuminate\Http\Resources\Json\JsonResource;
7+
use Illuminate\Http\Resources\Json\ResourceCollection;
8+
use LogicException;
9+
10+
trait TransformsToResourceCollection
11+
{
12+
/**
13+
* Create a new resource collection instance for the given resource.
14+
*
15+
* @param class-string<JsonResource>|null $resourceClass
16+
* @return ResourceCollection
17+
*
18+
* @throws \Throwable
19+
*/
20+
public function toResourceCollection(?string $resourceClass = null): ResourceCollection
21+
{
22+
if ($resourceClass === null) {
23+
return $this->guessResourceCollection();
24+
}
25+
26+
return $resourceClass::collection($this);
27+
}
28+
29+
/**
30+
* Guess the resource collection for the items.
31+
*
32+
* @return ResourceCollection
33+
*
34+
* @throws \Throwable
35+
*/
36+
protected function guessResourceCollection(): ResourceCollection
37+
{
38+
if ($this->isEmpty()) {
39+
return new ResourceCollection($this);
40+
}
41+
42+
$model = $this->items[0] ?? null;
43+
44+
throw_unless(is_object($model), LogicException::class, 'Resource collection guesser expects the collection to contain objects.');
45+
46+
/** @var class-string<Model> $className */
47+
$className = get_class($model);
48+
49+
throw_unless(method_exists($className, 'guessResourceName'), LogicException::class, sprintf('Expected class %s to implement guessResourceName method. Make sure the model uses the TransformsToResource trait.', $className));
50+
51+
foreach ($className::guessResourceName() as $resourceClass) {
52+
if (is_string($resourceClass) && class_exists($resourceClass)) {
53+
return $resourceClass::collection($this);
54+
}
55+
}
56+
57+
throw new LogicException(sprintf('Failed to find resource class for model [%s].', $className));
58+
}
59+
}

src/Illuminate/Pagination/AbstractPaginator.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Closure;
66
use Illuminate\Contracts\Support\CanBeEscapedWhenCastToString;
77
use Illuminate\Contracts\Support\Htmlable;
8+
use Illuminate\Http\Resources\TransformsToResourceCollection;
89
use Illuminate\Support\Arr;
910
use Illuminate\Support\Collection;
1011
use Illuminate\Support\Traits\ForwardsCalls;
@@ -21,7 +22,7 @@
2122
*/
2223
abstract class AbstractPaginator implements CanBeEscapedWhenCastToString, Htmlable, Stringable
2324
{
24-
use ForwardsCalls, Tappable;
25+
use ForwardsCalls, Tappable, TransformsToResourceCollection;
2526

2627
/**
2728
* All of the items being paginated.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Database;
4+
5+
use Illuminate\Database\Eloquent\Collection;
6+
use Illuminate\Database\Eloquent\Model;
7+
use Illuminate\Http\Resources\Json\JsonResource;
8+
use Illuminate\Tests\Database\Fixtures\Models\EloquentResourceCollectionTestModel;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class DatabaseEloquentResourceCollectionTest extends TestCase
12+
{
13+
public function testItCanTransformToExplicitResource()
14+
{
15+
$collection = new Collection([
16+
new EloquentResourceCollectionTestModel(),
17+
]);
18+
19+
$resource = $collection->toResourceCollection(EloquentResourceCollectionTestResource::class);
20+
21+
$this->assertInstanceOf(JsonResource::class, $resource);
22+
}
23+
24+
public function testItThrowsExceptionWhenResourceCannotBeFound()
25+
{
26+
$this->expectException(\LogicException::class);
27+
$this->expectExceptionMessage('Failed to find resource class for model [Illuminate\Tests\Database\Fixtures\Models\EloquentResourceCollectionTestModel].');
28+
29+
$collection = new Collection([
30+
new EloquentResourceCollectionTestModel(),
31+
]);
32+
$collection->toResourceCollection();
33+
}
34+
35+
public function testItCanGuessResourceWhenNotProvided()
36+
{
37+
$collection = new Collection([
38+
new EloquentResourceCollectionTestModel(),
39+
]);
40+
41+
class_alias(EloquentResourceCollectionTestResource::class, 'Illuminate\Tests\Database\Fixtures\Http\Resources\EloquentResourceCollectionTestModelResource');
42+
43+
$resource = $collection->toResourceCollection();
44+
45+
$this->assertInstanceOf(JsonResource::class, $resource);
46+
}
47+
}
48+
49+
class EloquentResourceCollectionTestResource extends JsonResource
50+
{
51+
//
52+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Database;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
use Illuminate\Http\Resources\Json\JsonResource;
7+
use Illuminate\Tests\Database\Fixtures\Models\EloquentResourceTestResourceModel;
8+
use Illuminate\Tests\Database\Fixtures\Models\EloquentResourceTestResourceModelWithGuessableResource;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class DatabaseEloquentResourceModelTest extends TestCase
12+
{
13+
public function testItCanTransformToExplicitResource()
14+
{
15+
$model = new EloquentResourceTestResourceModel();
16+
$resource = $model->toResource(EloquentResourceTestJsonResource::class);
17+
18+
$this->assertInstanceOf(EloquentResourceTestJsonResource::class, $resource);
19+
$this->assertSame($model, $resource->resource);
20+
}
21+
22+
public function testItThrowsExceptionWhenResourceCannotBeFound()
23+
{
24+
$this->expectException(\LogicException::class);
25+
$this->expectExceptionMessage('Failed to find resource class for model [Illuminate\Tests\Database\Fixtures\Models\EloquentResourceTestResourceModel].');
26+
27+
$model = new EloquentResourceTestResourceModel();
28+
$model->toResource();
29+
}
30+
31+
public function testItCanGuessResourceWhenNotProvided()
32+
{
33+
$model = new EloquentResourceTestResourceModelWithGuessableResource();
34+
35+
class_alias(EloquentResourceTestJsonResource::class, 'Illuminate\Tests\Database\Fixtures\Http\Resources\EloquentResourceTestResourceModelWithGuessableResourceResource');
36+
37+
$resource = $model->toResource();
38+
39+
$this->assertInstanceOf(EloquentResourceTestJsonResource::class, $resource);
40+
$this->assertSame($model, $resource->resource);
41+
}
42+
43+
public function testItCanGuessResourceWhenNotProvidedWithNonResourceSuffix()
44+
{
45+
$model = new EloquentResourceTestResourceModelWithGuessableResource();
46+
47+
class_alias(EloquentResourceTestJsonResource::class, 'Illuminate\Tests\Database\Fixtures\Http\Resources\EloquentResourceTestResourceModelWithGuessableResource');
48+
49+
$resource = $model->toResource();
50+
51+
$this->assertInstanceOf(EloquentResourceTestJsonResource::class, $resource);
52+
$this->assertSame($model, $resource->resource);
53+
}
54+
55+
public function testItCanGuessResourceName()
56+
{
57+
$model = new EloquentResourceTestResourceModel();
58+
$this->assertEquals([
59+
'Illuminate\Tests\Database\Fixtures\Http\Resources\EloquentResourceTestResourceModelResource',
60+
'Illuminate\Tests\Database\Fixtures\Http\Resources\EloquentResourceTestResourceModel'
61+
], $model::guessResourceName());
62+
}
63+
}
64+
65+
class EloquentResourceTestJsonResource extends JsonResource
66+
{
67+
//
68+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Database\Fixtures\Models;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
7+
class EloquentResourceCollectionTestModel extends Model
8+
{
9+
//
10+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Database\Fixtures\Models;
4+
5+
use Illuminate\Database\Eloquent\Concerns\HasUlids;
6+
use Illuminate\Database\Eloquent\Model;
7+
8+
class EloquentResourceTestResourceModel extends Model
9+
{
10+
//
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Database\Fixtures\Models;
4+
5+
use Illuminate\Database\Eloquent\Concerns\HasUlids;
6+
use Illuminate\Database\Eloquent\Model;
7+
8+
class EloquentResourceTestResourceModelWithGuessableResource extends Model
9+
{
10+
//
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Pagination\Fixtures\Models;
4+
5+
use Illuminate\Database\Eloquent\Concerns\HasUlids;
6+
use Illuminate\Database\Eloquent\Model;
7+
8+
class PaginatorResourceTestModel extends Model
9+
{
10+
//
11+
}

0 commit comments

Comments
 (0)