Skip to content

Commit 890ba8d

Browse files
authored
Merge pull request #330 from binaryfire/feature/use-resource-attributes
feat: Add `#[UseResource]` and `#[UseResourceCollection]` attributes
2 parents 6015417 + 50d4a0b commit 890ba8d

File tree

12 files changed

+604
-3
lines changed

12 files changed

+604
-3
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hypervel\Database\Eloquent\Attributes;
6+
7+
use Attribute;
8+
9+
/**
10+
* Declare the resource class for a model using an attribute.
11+
*
12+
* When placed on a model class that uses the TransformsToResource trait,
13+
* the specified resource will be used when calling the model's toResource() method.
14+
*
15+
* @example
16+
* ```php
17+
* #[UseResource(PostResource::class)]
18+
* class Post extends Model {}
19+
*
20+
* // Now $post->toResource() will use PostResource
21+
* ```
22+
*/
23+
#[Attribute(Attribute::TARGET_CLASS)]
24+
class UseResource
25+
{
26+
/**
27+
* Create a new attribute instance.
28+
*
29+
* @param class-string<\Hypervel\Http\Resources\Json\JsonResource> $class
30+
*/
31+
public function __construct(
32+
public string $class,
33+
) {
34+
}
35+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hypervel\Database\Eloquent\Attributes;
6+
7+
use Attribute;
8+
9+
/**
10+
* Declare the resource collection class for a model using an attribute.
11+
*
12+
* When placed on a model class, collections of this model will use the specified
13+
* resource collection class when calling toResourceCollection().
14+
*
15+
* @example
16+
* ```php
17+
* #[UseResourceCollection(PostCollection::class)]
18+
* class Post extends Model {}
19+
*
20+
* // Now Post::all()->toResourceCollection() will use PostCollection
21+
* ```
22+
*/
23+
#[Attribute(Attribute::TARGET_CLASS)]
24+
class UseResourceCollection
25+
{
26+
/**
27+
* Create a new attribute instance.
28+
*
29+
* @param class-string<\Hypervel\Http\Resources\Json\ResourceCollection> $class
30+
*/
31+
public function __construct(
32+
public string $class,
33+
) {
34+
}
35+
}

src/core/src/Database/Eloquent/Collection.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Hyperf\Collection\Enumerable;
88
use Hyperf\Database\Model\Collection as BaseCollection;
99
use Hypervel\Support\Collection as SupportCollection;
10+
use Hypervel\Support\Traits\TransformsToResourceCollection;
1011

1112
/**
1213
* @template TKey of array-key
@@ -39,6 +40,8 @@
3940
*/
4041
class Collection extends BaseCollection
4142
{
43+
use TransformsToResourceCollection;
44+
4245
/**
4346
* @template TFindDefault
4447
*
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hypervel\Database\Eloquent\Concerns;
6+
7+
use Hyperf\Stringable\Str;
8+
use Hypervel\Database\Eloquent\Attributes\UseResource;
9+
use Hypervel\Http\Resources\Json\JsonResource;
10+
use LogicException;
11+
use ReflectionClass;
12+
13+
/**
14+
* Provides the ability to transform a model to a JSON resource.
15+
*/
16+
trait TransformsToResource
17+
{
18+
/**
19+
* Create a new resource object for the given resource.
20+
*
21+
* @param null|class-string<\Hypervel\Http\Resources\Json\JsonResource> $resourceClass
22+
*/
23+
public function toResource(?string $resourceClass = null): JsonResource
24+
{
25+
if ($resourceClass === null) {
26+
return $this->guessResource();
27+
}
28+
29+
return $resourceClass::make($this);
30+
}
31+
32+
/**
33+
* Guess the resource class for the model.
34+
*/
35+
protected function guessResource(): JsonResource
36+
{
37+
$resourceClass = $this->resolveResourceFromAttribute(static::class);
38+
39+
if ($resourceClass !== null && class_exists($resourceClass)) {
40+
return $resourceClass::make($this);
41+
}
42+
43+
foreach (static::guessResourceName() as $resourceClass) {
44+
/* @phpstan-ignore-next-line function.alreadyNarrowedType */
45+
if (is_string($resourceClass) && class_exists($resourceClass)) {
46+
return $resourceClass::make($this);
47+
}
48+
}
49+
50+
throw new LogicException(sprintf('Failed to find resource class for model [%s].', get_class($this)));
51+
}
52+
53+
/**
54+
* Guess the resource class name for the model.
55+
*
56+
* @return array<int, class-string<\Hypervel\Http\Resources\Json\JsonResource>>
57+
*/
58+
public static function guessResourceName(): array
59+
{
60+
$modelClass = static::class;
61+
62+
if (! Str::contains($modelClass, '\Models\\')) {
63+
return [];
64+
}
65+
66+
$relativeNamespace = Str::after($modelClass, '\Models\\');
67+
68+
$relativeNamespace = Str::contains($relativeNamespace, '\\')
69+
? Str::before($relativeNamespace, '\\' . class_basename($modelClass))
70+
: '';
71+
72+
$potentialResource = sprintf(
73+
'%s\Http\Resources\%s%s',
74+
Str::before($modelClass, '\Models'),
75+
strlen($relativeNamespace) > 0 ? $relativeNamespace . '\\' : '',
76+
class_basename($modelClass)
77+
);
78+
79+
return [$potentialResource . 'Resource', $potentialResource];
80+
}
81+
82+
/**
83+
* Get the resource class from the UseResource attribute.
84+
*
85+
* @param class-string $class
86+
* @return null|class-string<\Hypervel\Http\Resources\Json\JsonResource>
87+
*/
88+
protected function resolveResourceFromAttribute(string $class): ?string
89+
{
90+
if (! class_exists($class)) {
91+
return null;
92+
}
93+
94+
$attributes = (new ReflectionClass($class))->getAttributes(UseResource::class);
95+
96+
return $attributes !== []
97+
? $attributes[0]->newInstance()->class
98+
: null;
99+
}
100+
}

src/core/src/Database/Eloquent/Model.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Hypervel\Database\Eloquent\Concerns\HasObservers;
1616
use Hypervel\Database\Eloquent\Concerns\HasRelations;
1717
use Hypervel\Database\Eloquent\Concerns\HasRelationships;
18+
use Hypervel\Database\Eloquent\Concerns\TransformsToResource;
1819
use Hypervel\Database\Eloquent\Relations\Pivot;
1920
use Hypervel\Router\Contracts\UrlRoutable;
2021
use Psr\EventDispatcher\EventDispatcherInterface;
@@ -75,6 +76,7 @@ abstract class Model extends BaseModel implements UrlRoutable, HasBroadcastChann
7576
use HasObservers;
7677
use HasRelations;
7778
use HasRelationships;
79+
use TransformsToResource;
7880

7981
protected ?string $connection = null;
8082

src/horizon/src/ProcessInspector.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public function monitoring(): array
4949
->pluck('pid')
5050
->pipe(function (Collection $processes) {
5151
foreach ($processes as $process) {
52+
/** @var string $process */
5253
$processes = $processes->merge($this->exec->run('pgrep -P ' . (string) $process));
5354
}
5455

src/http/src/Resources/Json/AnonymousResourceCollection.php

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,24 @@
44

55
namespace Hypervel\Http\Resources\Json;
66

7-
use Hyperf\Resource\Json\AnonymousResourceCollection as BaseAnonymousResourceCollection;
8-
9-
class AnonymousResourceCollection extends BaseAnonymousResourceCollection
7+
/**
8+
* Anonymous resource collection for wrapping arbitrary collections.
9+
*
10+
* This class extends ResourceCollection to ensure proper type hierarchy
11+
* within Hypervel's resource system.
12+
*/
13+
class AnonymousResourceCollection extends ResourceCollection
1014
{
15+
/**
16+
* Create a new anonymous resource collection.
17+
*
18+
* @param mixed $resource the resource being collected
19+
* @param string $collects the name of the resource being collected
20+
*/
21+
public function __construct(mixed $resource, string $collects)
22+
{
23+
$this->collects = $collects;
24+
25+
parent::__construct($resource);
26+
}
1127
}

src/support/src/Collection.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Hypervel\Support;
66

77
use Hyperf\Collection\Collection as BaseCollection;
8+
use Hypervel\Support\Traits\TransformsToResourceCollection;
89

910
/**
1011
* @template TKey of array-key
@@ -14,4 +15,5 @@
1415
*/
1516
class Collection extends BaseCollection
1617
{
18+
use TransformsToResourceCollection;
1719
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hypervel\Support\Traits;
6+
7+
use Hypervel\Database\Eloquent\Attributes\UseResource;
8+
use Hypervel\Database\Eloquent\Attributes\UseResourceCollection;
9+
use Hypervel\Http\Resources\Json\ResourceCollection;
10+
use LogicException;
11+
use ReflectionClass;
12+
use Throwable;
13+
14+
/**
15+
* Provides the ability to transform a collection to a resource collection.
16+
*/
17+
trait TransformsToResourceCollection
18+
{
19+
/**
20+
* Create a new resource collection instance for the given resource.
21+
*
22+
* @param null|class-string<\Hypervel\Http\Resources\Json\JsonResource> $resourceClass
23+
* @throws Throwable
24+
*/
25+
public function toResourceCollection(?string $resourceClass = null): ResourceCollection
26+
{
27+
if ($resourceClass === null) {
28+
return $this->guessResourceCollection();
29+
}
30+
31+
return $resourceClass::collection($this);
32+
}
33+
34+
/**
35+
* Guess the resource collection for the items.
36+
*
37+
* @throws Throwable
38+
*/
39+
protected function guessResourceCollection(): ResourceCollection
40+
{
41+
if ($this->isEmpty()) {
42+
return new ResourceCollection($this);
43+
}
44+
45+
$model = $this->items[0] ?? null;
46+
47+
throw_unless(is_object($model), LogicException::class, 'Resource collection guesser expects the collection to contain objects.');
48+
49+
/** @var class-string $className */
50+
$className = get_class($model);
51+
52+
throw_unless(
53+
method_exists($className, 'guessResourceName'),
54+
LogicException::class,
55+
sprintf('Expected class %s to implement guessResourceName method. Make sure the model uses the TransformsToResource trait.', $className)
56+
);
57+
58+
$useResourceCollection = $this->resolveResourceCollectionFromAttribute($className);
59+
60+
if ($useResourceCollection !== null && class_exists($useResourceCollection)) {
61+
return new $useResourceCollection($this);
62+
}
63+
64+
$useResource = $this->resolveResourceFromAttribute($className);
65+
66+
if ($useResource !== null && class_exists($useResource)) {
67+
return $useResource::collection($this);
68+
}
69+
70+
$resourceClasses = $className::guessResourceName();
71+
72+
foreach ($resourceClasses as $resourceClass) {
73+
$resourceCollection = $resourceClass . 'Collection';
74+
if (class_exists($resourceCollection)) {
75+
return new $resourceCollection($this);
76+
}
77+
}
78+
79+
foreach ($resourceClasses as $resourceClass) {
80+
if (is_string($resourceClass) && class_exists($resourceClass)) {
81+
return $resourceClass::collection($this);
82+
}
83+
}
84+
85+
throw new LogicException(sprintf('Failed to find resource class for model [%s].', $className));
86+
}
87+
88+
/**
89+
* Get the resource class from the UseResource attribute.
90+
*
91+
* @param class-string $class
92+
* @return null|class-string<\Hypervel\Http\Resources\Json\JsonResource>
93+
*/
94+
protected function resolveResourceFromAttribute(string $class): ?string
95+
{
96+
if (! class_exists($class)) {
97+
return null;
98+
}
99+
100+
$attributes = (new ReflectionClass($class))->getAttributes(UseResource::class);
101+
102+
return $attributes !== []
103+
? $attributes[0]->newInstance()->class
104+
: null;
105+
}
106+
107+
/**
108+
* Get the resource collection class from the UseResourceCollection attribute.
109+
*
110+
* @param class-string $class
111+
* @return null|class-string<\Hypervel\Http\Resources\Json\ResourceCollection>
112+
*/
113+
protected function resolveResourceCollectionFromAttribute(string $class): ?string
114+
{
115+
if (! class_exists($class)) {
116+
return null;
117+
}
118+
119+
$attributes = (new ReflectionClass($class))->getAttributes(UseResourceCollection::class);
120+
121+
return $attributes !== []
122+
? $attributes[0]->newInstance()->class
123+
: null;
124+
}
125+
}

0 commit comments

Comments
 (0)