Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
35 changes: 35 additions & 0 deletions src/core/src/Database/Eloquent/Attributes/UseResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Hypervel\Database\Eloquent\Attributes;

use Attribute;

/**
* Declare the resource class for a model using an attribute.
*
* When placed on a model class that uses the TransformsToResource trait,
* the specified resource will be used when calling the model's toResource() method.
*
* @example
* ```php
* #[UseResource(PostResource::class)]
* class Post extends Model {}
*
* // Now $post->toResource() will use PostResource
* ```
*/
#[Attribute(Attribute::TARGET_CLASS)]
class UseResource
{
/**
* Create a new attribute instance.
*
* @param class-string<\Hypervel\Http\Resources\Json\JsonResource> $class
*/
public function __construct(
public string $class,
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Hypervel\Database\Eloquent\Attributes;

use Attribute;

/**
* Declare the resource collection class for a model using an attribute.
*
* When placed on a model class, collections of this model will use the specified
* resource collection class when calling toResourceCollection().
*
* @example
* ```php
* #[UseResourceCollection(PostCollection::class)]
* class Post extends Model {}
*
* // Now Post::all()->toResourceCollection() will use PostCollection
* ```
*/
#[Attribute(Attribute::TARGET_CLASS)]
class UseResourceCollection
{
/**
* Create a new attribute instance.
*
* @param class-string<\Hypervel\Http\Resources\Json\ResourceCollection> $class
*/
public function __construct(
public string $class,
) {
}
}
3 changes: 3 additions & 0 deletions src/core/src/Database/Eloquent/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Hyperf\Collection\Enumerable;
use Hyperf\Database\Model\Collection as BaseCollection;
use Hypervel\Support\Collection as SupportCollection;
use Hypervel\Support\Traits\TransformsToResourceCollection;

/**
* @template TKey of array-key
Expand Down Expand Up @@ -39,6 +40,8 @@
*/
class Collection extends BaseCollection
{
use TransformsToResourceCollection;

/**
* @template TFindDefault
*
Expand Down
100 changes: 100 additions & 0 deletions src/core/src/Database/Eloquent/Concerns/TransformsToResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

declare(strict_types=1);

namespace Hypervel\Database\Eloquent\Concerns;

use Hyperf\Stringable\Str;
use Hypervel\Database\Eloquent\Attributes\UseResource;
use Hypervel\Http\Resources\Json\JsonResource;
use LogicException;
use ReflectionClass;

/**
* Provides the ability to transform a model to a JSON resource.
*/
trait TransformsToResource
{
/**
* Create a new resource object for the given resource.
*
* @param null|class-string<\Hypervel\Http\Resources\Json\JsonResource> $resourceClass
*/
public function toResource(?string $resourceClass = null): JsonResource
{
if ($resourceClass === null) {
return $this->guessResource();
}

return $resourceClass::make($this);
}

/**
* Guess the resource class for the model.
*/
protected function guessResource(): JsonResource
{
$resourceClass = $this->resolveResourceFromAttribute(static::class);

if ($resourceClass !== null && class_exists($resourceClass)) {
return $resourceClass::make($this);
}

foreach (static::guessResourceName() as $resourceClass) {
/* @phpstan-ignore-next-line function.alreadyNarrowedType */
if (is_string($resourceClass) && class_exists($resourceClass)) {
return $resourceClass::make($this);
}
}

throw new LogicException(sprintf('Failed to find resource class for model [%s].', get_class($this)));
}

/**
* Guess the resource class name for the model.
*
* @return array<int, class-string<\Hypervel\Http\Resources\Json\JsonResource>>
*/
public static function guessResourceName(): array
{
$modelClass = static::class;

if (! Str::contains($modelClass, '\Models\\')) {
return [];
}

$relativeNamespace = Str::after($modelClass, '\Models\\');

$relativeNamespace = Str::contains($relativeNamespace, '\\')
? Str::before($relativeNamespace, '\\' . class_basename($modelClass))
: '';

$potentialResource = sprintf(
'%s\Http\Resources\%s%s',
Str::before($modelClass, '\Models'),
strlen($relativeNamespace) > 0 ? $relativeNamespace . '\\' : '',
class_basename($modelClass)
);

return [$potentialResource . 'Resource', $potentialResource];
}

/**
* Get the resource class from the UseResource attribute.
*
* @param class-string $class
* @return null|class-string<\Hypervel\Http\Resources\Json\JsonResource>
*/
protected function resolveResourceFromAttribute(string $class): ?string
{
if (! class_exists($class)) {
return null;
}

$attributes = (new ReflectionClass($class))->getAttributes(UseResource::class);

return $attributes !== []
? $attributes[0]->newInstance()->class
: null;
}
}
2 changes: 2 additions & 0 deletions src/core/src/Database/Eloquent/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Hypervel\Database\Eloquent\Concerns\HasObservers;
use Hypervel\Database\Eloquent\Concerns\HasRelations;
use Hypervel\Database\Eloquent\Concerns\HasRelationships;
use Hypervel\Database\Eloquent\Concerns\TransformsToResource;
use Hypervel\Database\Eloquent\Relations\Pivot;
use Hypervel\Router\Contracts\UrlRoutable;
use Psr\EventDispatcher\EventDispatcherInterface;
Expand Down Expand Up @@ -75,6 +76,7 @@ abstract class Model extends BaseModel implements UrlRoutable, HasBroadcastChann
use HasObservers;
use HasRelations;
use HasRelationships;
use TransformsToResource;

protected ?string $connection = null;

Expand Down
1 change: 1 addition & 0 deletions src/horizon/src/ProcessInspector.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public function monitoring(): array
->pluck('pid')
->pipe(function (Collection $processes) {
foreach ($processes as $process) {
/** @var string $process */
$processes = $processes->merge($this->exec->run('pgrep -P ' . (string) $process));
}

Expand Down
22 changes: 19 additions & 3 deletions src/http/src/Resources/Json/AnonymousResourceCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,24 @@

namespace Hypervel\Http\Resources\Json;

use Hyperf\Resource\Json\AnonymousResourceCollection as BaseAnonymousResourceCollection;

class AnonymousResourceCollection extends BaseAnonymousResourceCollection
/**
* Anonymous resource collection for wrapping arbitrary collections.
*
* This class extends ResourceCollection to ensure proper type hierarchy
* within Hypervel's resource system.
*/
class AnonymousResourceCollection extends ResourceCollection
{
/**
* Create a new anonymous resource collection.
*
* @param mixed $resource the resource being collected
* @param string $collects the name of the resource being collected
*/
public function __construct(mixed $resource, string $collects)
{
$this->collects = $collects;

parent::__construct($resource);
}
}
2 changes: 2 additions & 0 deletions src/support/src/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Hypervel\Support;

use Hyperf\Collection\Collection as BaseCollection;
use Hypervel\Support\Traits\TransformsToResourceCollection;

/**
* @template TKey of array-key
Expand All @@ -14,4 +15,5 @@
*/
class Collection extends BaseCollection
{
use TransformsToResourceCollection;
}
125 changes: 125 additions & 0 deletions src/support/src/Traits/TransformsToResourceCollection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php

declare(strict_types=1);

namespace Hypervel\Support\Traits;

use Hypervel\Database\Eloquent\Attributes\UseResource;
use Hypervel\Database\Eloquent\Attributes\UseResourceCollection;
use Hypervel\Http\Resources\Json\ResourceCollection;
use LogicException;
use ReflectionClass;
use Throwable;

/**
* Provides the ability to transform a collection to a resource collection.
*/
trait TransformsToResourceCollection
{
/**
* Create a new resource collection instance for the given resource.
*
* @param null|class-string<\Hypervel\Http\Resources\Json\JsonResource> $resourceClass
* @throws Throwable
*/
public function toResourceCollection(?string $resourceClass = null): ResourceCollection
{
if ($resourceClass === null) {
return $this->guessResourceCollection();
}

return $resourceClass::collection($this);
}

/**
* Guess the resource collection for the items.
*
* @throws Throwable
*/
protected function guessResourceCollection(): ResourceCollection
{
if ($this->isEmpty()) {
return new ResourceCollection($this);
}

$model = $this->items[0] ?? null;

throw_unless(is_object($model), LogicException::class, 'Resource collection guesser expects the collection to contain objects.');

/** @var class-string $className */
$className = get_class($model);

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)
);

$useResourceCollection = $this->resolveResourceCollectionFromAttribute($className);

if ($useResourceCollection !== null && class_exists($useResourceCollection)) {
return new $useResourceCollection($this);
}

$useResource = $this->resolveResourceFromAttribute($className);

if ($useResource !== null && class_exists($useResource)) {
return $useResource::collection($this);
}

$resourceClasses = $className::guessResourceName();

foreach ($resourceClasses as $resourceClass) {
$resourceCollection = $resourceClass . 'Collection';
if (class_exists($resourceCollection)) {
return new $resourceCollection($this);
}
}

foreach ($resourceClasses as $resourceClass) {
if (is_string($resourceClass) && class_exists($resourceClass)) {
return $resourceClass::collection($this);
}
}

throw new LogicException(sprintf('Failed to find resource class for model [%s].', $className));
}

/**
* Get the resource class from the UseResource attribute.
*
* @param class-string $class
* @return null|class-string<\Hypervel\Http\Resources\Json\JsonResource>
*/
protected function resolveResourceFromAttribute(string $class): ?string
{
if (! class_exists($class)) {
return null;
}

$attributes = (new ReflectionClass($class))->getAttributes(UseResource::class);

return $attributes !== []
? $attributes[0]->newInstance()->class
: null;
}

/**
* Get the resource collection class from the UseResourceCollection attribute.
*
* @param class-string $class
* @return null|class-string<\Hypervel\Http\Resources\Json\ResourceCollection>
*/
protected function resolveResourceCollectionFromAttribute(string $class): ?string
{
if (! class_exists($class)) {
return null;
}

$attributes = (new ReflectionClass($class))->getAttributes(UseResourceCollection::class);

return $attributes !== []
? $attributes[0]->newInstance()->class
: null;
}
}
Loading