This commit is contained in:
2026-05-01 23:40:14 +08:00
commit b8f599a617
3867 changed files with 478663 additions and 0 deletions
@@ -0,0 +1,87 @@
<?php
namespace Illuminate\Http\Resources\JsonApi;
use Illuminate\Container\Container;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection as BaseAnonymousResourceCollection;
use Illuminate\Support\Arr;
class AnonymousResourceCollection extends BaseAnonymousResourceCollection
{
use Concerns\ResolvesJsonApiRequest;
/**
* Get any additional data that should be returned with the resource array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
#[\Override]
public function with($request)
{
return array_filter([
'included' => $this->collection
->map(fn ($resource) => $resource->resolveIncludedResourceObjects($request))
->flatten(depth: 1)
->uniqueStrict('_uniqueKey')
->map(fn ($included) => Arr::except($included, ['_uniqueKey']))
->values()
->all(),
...($implementation = JsonApiResource::$jsonApiInformation)
? ['jsonapi' => $implementation]
: [],
]);
}
/**
* Transform the resource into a JSON array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
#[\Override]
public function toAttributes(Request $request)
{
return $this->collection
->map(fn ($resource) => $resource->resolveResourceData($request))
->all();
}
/**
* Customize the outgoing response for the resource.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Http\JsonResponse $response
* @return void
*/
#[\Override]
public function withResponse(Request $request, JsonResponse $response): void
{
$response->header('Content-Type', 'application/vnd.api+json');
}
/**
* Create an HTTP response that represents the object.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
#[\Override]
public function toResponse($request)
{
return parent::toResponse($this->resolveJsonApiRequestFrom($request));
}
/**
* Resolve the HTTP request instance from container.
*
* @return \Illuminate\Http\Resources\JsonApi\JsonApiRequest
*/
#[\Override]
protected function resolveRequestFromContainer()
{
return $this->resolveJsonApiRequestFrom(Container::getInstance()->make('request'));
}
}
@@ -0,0 +1,438 @@
<?php
namespace Illuminate\Http\Resources\JsonApi\Concerns;
use Generator;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\Concerns\AsPivot;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Resources\JsonApi\Exceptions\ResourceIdentificationException;
use Illuminate\Http\Resources\JsonApi\JsonApiRequest;
use Illuminate\Http\Resources\JsonApi\JsonApiResource;
use Illuminate\Http\Resources\JsonApi\RelationResolver;
use Illuminate\Http\Resources\MissingValue;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use JsonSerializable;
use WeakMap;
trait ResolvesJsonApiElements
{
/**
* Determine whether resources respect inclusions and fields from the request.
*/
protected bool $usesRequestQueryString = true;
/**
* Determine whether included relationship for the resource from eager loaded relationship.
*/
protected bool $includesPreviouslyLoadedRelationships = false;
/**
* Cached loaded relationships map.
*
* @var array<int, array{0: \Illuminate\Http\Resources\JsonApi\JsonApiResource, 1: string, 2: string, 3: bool}|null
*/
public $loadedRelationshipsMap;
/**
* Cached loaded relationships identifiers.
*/
protected array $loadedRelationshipIdentifiers = [];
/**
* The maximum relationship depth.
*/
public static int $maxRelationshipDepth = 5;
/**
* Specify the maximum relationship depth.
*
* @param non-negative-int $depth
*/
public static function maxRelationshipDepth(int $depth): void
{
static::$maxRelationshipDepth = max(0, $depth);
}
/**
* Resolves `data` for the resource.
*/
protected function resolveResourceObject(JsonApiRequest $request): array
{
$resourceType = $this->resolveResourceType($request);
return [
'id' => $this->resolveResourceIdentifier($request),
'type' => $resourceType,
...(new Collection([
'attributes' => $this->resolveResourceAttributes($request, $resourceType),
'relationships' => $this->resolveResourceRelationshipIdentifiers($request),
'links' => $this->resolveResourceLinks($request),
'meta' => $this->resolveResourceMetaInformation($request),
]))->filter()->map(fn ($value) => (object) $value),
];
}
/**
* Resolve the resource's identifier.
*
* @return string
*
* @throws ResourceIdentificationException
*/
public function resolveResourceIdentifier(JsonApiRequest $request): string
{
if (! is_null($resourceId = $this->toId($request))) {
return (string) $resourceId;
}
if (! ($this->resource instanceof Model || method_exists($this->resource, 'getKey'))) {
throw ResourceIdentificationException::attemptingToDetermineIdFor($this);
}
return (string) $this->resource->getKey();
}
/**
* Resolve the resource's type.
*
* @throws ResourceIdentificationException
*/
public function resolveResourceType(JsonApiRequest $request): string
{
if (! is_null($resourceType = $this->toType($request))) {
return $resourceType;
}
if (static::class !== JsonApiResource::class) {
return Str::of(static::class)->classBasename()->basename('Resource')->snake()->pluralStudly();
}
if (! $this->resource instanceof Model) {
throw ResourceIdentificationException::attemptingToDetermineTypeFor($this);
}
$modelClassName = $this->resource::class;
$morphMap = Relation::getMorphAlias($modelClassName);
return Str::of(
$morphMap !== $modelClassName ? $morphMap : class_basename($modelClassName)
)->snake()->pluralStudly();
}
/**
* Resolve the resource's attributes.
*
* @throws \RuntimeException
*/
protected function resolveResourceAttributes(JsonApiRequest $request, string $resourceType): array
{
$data = $this->toAttributes($request);
if ($data instanceof Arrayable) {
$data = $data->toArray();
} elseif ($data instanceof JsonSerializable) {
$data = $data->jsonSerialize();
}
$usesSparseFieldset = $this->usesRequestQueryString && $request->hasSparseFieldset($resourceType);
$sparseFieldset = $usesSparseFieldset ? $request->sparseFields($resourceType) : [];
$data = (new Collection($data))
->mapWithKeys(fn ($value, $key) => is_int($key) ? [$value => $this->resource->{$value}] : [$key => $value])
->when($usesSparseFieldset, fn ($attributes) => $attributes->only($sparseFieldset))
->transform(fn ($value) => value($value, $request))
->all();
return $this->filter($data);
}
/**
* Resolves `relationships` for the resource's data object.
*
* @return array
*
* @throws \RuntimeException
*/
protected function resolveResourceRelationshipIdentifiers(JsonApiRequest $request): array
{
if (! $this->resource instanceof Model) {
return [];
}
$this->compileResourceRelationships($request);
return [
...(new Collection($this->filter($this->loadedRelationshipIdentifiers)))
->map(function ($relation) {
return ! is_null($relation) ? $relation : ['data' => null];
})->all(),
];
}
/**
* Compile resource relationships.
*/
protected function compileResourceRelationships(JsonApiRequest $request): void
{
if (! is_null($this->loadedRelationshipsMap)) {
return;
}
$sparseIncluded = match (true) {
$this->includesPreviouslyLoadedRelationships => array_keys($this->resource->getRelations()),
default => $request->sparseIncluded(),
};
$resourceRelationships = (new Collection($this->toRelationships($request)))
->transform(fn ($value, $key) => is_int($key) ? new RelationResolver($value) : new RelationResolver($key, $value))
->mapWithKeys(fn ($relationResolver) => [$relationResolver->relationName => $relationResolver])
->filter(fn ($value, $key) => in_array($key, $sparseIncluded));
$resourceRelationshipKeys = $resourceRelationships->keys();
$this->resource->loadMissing($resourceRelationshipKeys->all() ?? []);
$this->loadedRelationshipsMap = [];
$this->loadedRelationshipIdentifiers = (new LazyCollection(function () use ($request, $resourceRelationships) {
foreach ($resourceRelationships as $relationName => $relationResolver) {
$relatedModels = $relationResolver->handle($this->resource);
if (! is_null($relatedModels) && $this->includesPreviouslyLoadedRelationships === false) {
if (! empty($relations = $request->sparseIncluded($relationName))) {
$relatedModels->loadMissing($relations);
}
}
yield from $this->compileResourceRelationshipUsingResolver(
$request,
$this->resource,
$relationResolver,
$relatedModels,
);
}
}))->all();
}
/**
* Compile resource relations.
*/
protected function compileResourceRelationshipUsingResolver(
JsonApiRequest $request,
mixed $resource,
RelationResolver $relationResolver,
Collection|Model|null $relatedModels
): Generator {
$relationName = $relationResolver->relationName;
$resourceClass = $relationResolver->resourceClass();
// Relationship is a collection of models...
if ($relatedModels instanceof Collection) {
$relatedModels = $relatedModels->values();
if ($relatedModels->isEmpty()) {
yield $relationName => ['data' => $relatedModels];
return;
}
$relationship = $resource->{$relationName}();
$isUnique = ! $relationship instanceof BelongsToMany;
yield $relationName => ['data' => $relatedModels->map(function ($relatedModel) use ($request, $resourceClass, $isUnique) {
$relatedResource = rescue(fn () => $relatedModel->toResource($resourceClass), new JsonApiResource($relatedModel));
return transform(
[$relatedResource->resolveResourceType($request), $relatedResource->resolveResourceIdentifier($request)],
function ($uniqueKey) use ($request, $relatedModel, $relatedResource, $isUnique) {
$this->loadedRelationshipsMap[] = [$relatedResource, ...$uniqueKey, $isUnique];
$this->compileIncludedNestedRelationshipsMap($request, $relatedModel, $relatedResource);
return [
'id' => $uniqueKey[1],
'type' => $uniqueKey[0],
];
}
);
})->all()];
return;
}
// Relationship is a single model...
$relatedModel = $relatedModels;
if (is_null($relatedModel)) {
yield $relationName => null;
return;
} elseif ($relatedModel instanceof Pivot ||
isset(class_uses_recursive($relatedModel)[AsPivot::class])) {
yield $relationName => new MissingValue;
return;
}
$relatedResource = rescue(fn () => $relatedModel->toResource($resourceClass), new JsonApiResource($relatedModel));
yield $relationName => ['data' => transform(
[$relatedResource->resolveResourceType($request), $relatedResource->resolveResourceIdentifier($request)],
function ($uniqueKey) use ($relatedModel, $relatedResource, $request) {
$this->loadedRelationshipsMap[] = [$relatedResource, ...$uniqueKey, true];
$this->compileIncludedNestedRelationshipsMap($request, $relatedModel, $relatedResource);
return [
'id' => $uniqueKey[1],
'type' => $uniqueKey[0],
];
}
)];
}
/**
* Compile included relationships map.
*/
protected function compileIncludedNestedRelationshipsMap(JsonApiRequest $request, Model $relation, JsonApiResource $resource): void
{
(new Collection($resource->toRelationships($request)))
->transform(fn ($value, $key) => is_int($key) ? new RelationResolver($value) : new RelationResolver($key, $value))
->mapWithKeys(fn ($relationResolver) => [$relationResolver->relationName => $relationResolver])
->filter(fn ($value, $key) => in_array($key, array_keys($relation->getRelations())))
->each(function ($relationResolver, $key) use ($relation, $request) {
$this->compileResourceRelationshipUsingResolver($request, $relation, $relationResolver, $relation->getRelation($key));
});
}
/**
* Resolves `included` for the resource.
*/
public function resolveIncludedResourceObjects(JsonApiRequest $request): Collection
{
if (! $this->resource instanceof Model) {
return new Collection;
}
$this->compileResourceRelationships($request);
$relations = new Collection;
$index = 0;
// Track visited objects by instance + type to prevent infinite loops from circular
// references created by "chaperone()". We use object instances rather than type
// and ID for any possible cases like BelongsToMany with different pivot data.
// We'll track types to allow the same models with different resource types.
$visitedObjects = new WeakMap;
$visitedObjects[$this->resource] = [
$this->resolveResourceType($request) => true,
];
while ($index < count($this->loadedRelationshipsMap)) {
[$resourceInstance, $type, $id, $isUnique] = $this->loadedRelationshipsMap[$index];
$underlyingResource = $resourceInstance->resource;
if (is_object($underlyingResource)) {
if (isset($visitedObjects[$underlyingResource][$type])) {
$index++;
continue;
}
$visitedObjects[$underlyingResource] ??= [];
$visitedObjects[$underlyingResource][$type] = true;
}
if (! $resourceInstance instanceof JsonApiResource &&
$resourceInstance instanceof JsonResource) {
$resourceInstance = new JsonApiResource($resourceInstance->resource);
}
$relationsData = $resourceInstance
->includePreviouslyLoadedRelationships()
->resolve($request);
array_push($this->loadedRelationshipsMap, ...($resourceInstance->loadedRelationshipsMap ?? []));
$relations->push(array_filter([
'id' => $id,
'type' => $type,
'_uniqueKey' => implode(':', $isUnique === true ? [$id, $type] : [$id, $type, (string) Str::random()]),
'attributes' => Arr::get($relationsData, 'data.attributes'),
'relationships' => Arr::get($relationsData, 'data.relationships'),
'links' => Arr::get($relationsData, 'data.links'),
'meta' => Arr::get($relationsData, 'data.meta'),
]));
$index++;
}
return $relations;
}
/**
* Resolve the links for the resource.
*
* @return array<string, mixed>
*/
protected function resolveResourceLinks(JsonApiRequest $request): array
{
return $this->toLinks($request);
}
/**
* Resolve the meta information for the resource.
*
* @return array<string, mixed>
*/
protected function resolveResourceMetaInformation(JsonApiRequest $request): array
{
return $this->toMeta($request);
}
/**
* Indicate that relationship loading should respect the request's "includes" query string.
*
* @return $this
*/
public function respectFieldsAndIncludesInQueryString(bool $value = true)
{
$this->usesRequestQueryString = $value;
return $this;
}
/**
* Indicate that relationship loading should not rely on the request's "includes" query string.
*
* @return $this
*/
public function ignoreFieldsAndIncludesInQueryString()
{
return $this->respectFieldsAndIncludesInQueryString(false);
}
/**
* Determine relationship should include loaded relationships.
*
* @return $this
*/
public function includePreviouslyLoadedRelationships()
{
$this->includesPreviouslyLoadedRelationships = true;
return $this;
}
}
@@ -0,0 +1,21 @@
<?php
namespace Illuminate\Http\Resources\JsonApi\Concerns;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\JsonApi\JsonApiRequest;
trait ResolvesJsonApiRequest
{
/**
* Resolve a JSON API request instance from the given HTTP request.
*
* @return \Illuminate\Http\Resources\JsonApi\JsonApiRequest
*/
protected function resolveJsonApiRequestFrom(Request $request)
{
return $request instanceof JsonApiRequest
? $request
: JsonApiRequest::createFrom($request);
}
}
@@ -0,0 +1,38 @@
<?php
namespace Illuminate\Http\Resources\JsonApi\Exceptions;
use RuntimeException;
class ResourceIdentificationException extends RuntimeException
{
/**
* Create an exception indicating we were unable to determine the resource ID for the given resource.
*
* @param mixed $resource
* @return self
*/
public static function attemptingToDetermineIdFor($resource)
{
$resourceType = is_object($resource) ? $resource::class : gettype($resource);
return new self(sprintf(
'Unable to resolve resource object ID for [%s].', $resourceType
));
}
/**
* Create an exception indicating we were unable to determine the resource type for the given resource.
*
* @param mixed $resource
* @return self
*/
public static function attemptingToDetermineTypeFor($resource)
{
$resourceType = is_object($resource) ? $resource::class : gettype($resource);
return new self(sprintf(
'Unable to resolve resource object type for [%s].', $resourceType
));
}
}
@@ -0,0 +1,87 @@
<?php
namespace Illuminate\Http\Resources\JsonApi;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
class JsonApiRequest extends Request
{
/**
* Cached sparse fieldset.
*/
protected ?array $cachedSparseFields = null;
/**
* Cached sparse included.
*/
protected ?array $cachedSparseIncluded = null;
/**
* Get the request's included fields.
*/
public function sparseFields(string $key): array
{
if (is_null($this->cachedSparseFields)) {
$this->cachedSparseFields = (new Collection($this->array('fields')))
->transform(fn ($fieldsets) => empty($fieldsets) ? [] : explode(',', $fieldsets))
->all();
}
return $this->cachedSparseFields[$key] ?? [];
}
/**
* Determine if a sparse fieldset was provided for the given resource type.
*/
public function hasSparseFieldset(string $key): bool
{
if (is_null($this->cachedSparseFields)) {
$this->sparseFields($key);
}
return array_key_exists($key, $this->cachedSparseFields);
}
/**
* Get the request's included relationships.
*/
public function sparseIncluded(?string $key = null): ?array
{
if (is_null($this->cachedSparseIncluded)) {
$included = (string) $this->string('include', '');
$this->cachedSparseIncluded = (new Collection(empty($included) ? [] : explode(',', $included)))
->transform(function ($item) {
$with = null;
if (str_contains($item, '.')) {
[$relation, $with] = explode('.', $item, 2);
} else {
$relation = $item;
}
return ['relation' => $relation, 'with' => $with];
})->mapToGroups(fn ($item) => [$item['relation'] => $item['with']])
->toArray();
}
if (is_null($key)) {
return array_keys($this->cachedSparseIncluded);
}
return transform($this->cachedSparseIncluded[$key] ?? null, function ($value) {
return Collection::wrap($value)
->transform(function ($item) {
if (! is_string($item) || $item === '') {
return null;
}
$item = implode('.', Arr::take(explode('.', $item), JsonApiResource::$maxRelationshipDepth - 1));
return ! empty($item) ? $item : null;
})->filter()->all();
}) ?? [];
}
}
@@ -0,0 +1,255 @@
<?php
namespace Illuminate\Http\Resources\JsonApi;
use BadMethodCallException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Arr;
class JsonApiResource extends JsonResource
{
use Concerns\ResolvesJsonApiElements,
Concerns\ResolvesJsonApiRequest;
/**
* The "data" wrapper that should be applied.
*
* @var string|null
*/
public static $wrap = 'data';
/**
* The resource's "version" for JSON:API.
*
* @var array{version?: string, ext?: array, profile?: array, meta?: array}
*/
public static $jsonApiInformation = [];
/**
* The resource's "links" for JSON:API.
*/
protected array $jsonApiLinks = [];
/**
* The resource's "meta" for JSON:API.
*/
protected array $jsonApiMeta = [];
/**
* Set the JSON:API version for the request.
*
* @return void
*/
public static function configure(?string $version = null, array $ext = [], array $profile = [], array $meta = [])
{
static::$jsonApiInformation = array_filter([
'version' => $version,
'ext' => $ext,
'profile' => $profile,
'meta' => $meta,
]);
}
/**
* Get the resource's ID.
*
* @return string|null
*/
public function toId(Request $request)
{
return null;
}
/**
* Get the resource's type.
*
* @return string|null
*/
public function toType(Request $request)
{
return null;
}
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\Support\Arrayable|\JsonSerializable|array
*/
#[\Override]
public function toAttributes(Request $request)
{
if (property_exists($this, 'attributes')) {
return $this->attributes;
}
return $this->toArray($request);
}
/**
* Get the resource's relationships.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\Support\Arrayable|array
*/
public function toRelationships(Request $request)
{
if (property_exists($this, 'relationships')) {
return $this->relationships;
}
return [];
}
/**
* Get the resource's links.
*
* @return array
*/
public function toLinks(Request $request)
{
return $this->jsonApiLinks;
}
/**
* Get the resource's meta information.
*
* @return array
*/
public function toMeta(Request $request)
{
return $this->jsonApiMeta;
}
/**
* Get any additional data that should be returned with the resource array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
#[\Override]
public function with($request)
{
return array_filter([
'included' => $this->resolveIncludedResourceObjects($request)
->uniqueStrict('_uniqueKey')
->map(fn ($included) => Arr::except($included, ['_uniqueKey']))
->values()
->all(),
...($implementation = static::$jsonApiInformation)
? ['jsonapi' => $implementation]
: [],
]);
}
/**
* Resolve the resource to an array.
*
* @param \Illuminate\Http\Request|null $request
* @return array
*/
#[\Override]
public function resolve($request = null)
{
return [
'data' => $this->resolveResourceData($this->resolveJsonApiRequestFrom($request ?? $this->resolveRequestFromContainer())),
];
}
/**
* Resolve the resource data to an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
#[\Override]
public function resolveResourceData(Request $request)
{
return $this->resolveResourceObject($request);
}
/**
* Customize the outgoing response for the resource.
*/
#[\Override]
public function withResponse(Request $request, JsonResponse $response): void
{
$response->header('Content-Type', 'application/vnd.api+json');
}
/**
* Create an HTTP response that represents the object.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
#[\Override]
public function toResponse($request)
{
return parent::toResponse($this->resolveJsonApiRequestFrom($request));
}
/**
* Resolve the HTTP request instance from container.
*
* @return \Illuminate\Http\Resources\JsonApi\JsonApiRequest
*/
#[\Override]
protected function resolveRequestFromContainer()
{
return $this->resolveJsonApiRequestFrom(parent::resolveRequestFromContainer());
}
/**
* Create a new resource collection instance.
*
* @param mixed $resource
* @return \Illuminate\Http\Resources\JsonApi\AnonymousResourceCollection
*/
#[\Override]
protected static function newCollection($resource)
{
return new AnonymousResourceCollection($resource, static::class);
}
/**
* Set the string that should wrap the outer-most resource array.
*
* @param string $value
* @return never
*
* @throws \RuntimeException
*/
#[\Override]
public static function wrap($value)
{
throw new BadMethodCallException(sprintf('Using %s() method is not allowed.', __METHOD__));
}
/**
* Disable wrapping of the outer-most resource array.
*
* @return never
*/
#[\Override]
public static function withoutWrapping()
{
throw new BadMethodCallException(sprintf('Using %s() method is not allowed.', __METHOD__));
}
/**
* Flush the resource's global state.
*
* @return void
*/
#[\Override]
public static function flushState()
{
parent::flushState();
static::$jsonApiInformation = [];
static::$maxRelationshipDepth = 5;
}
}
@@ -0,0 +1,62 @@
<?php
namespace Illuminate\Http\Resources\JsonApi;
use Closure;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
/**
* @internal
*/
class RelationResolver
{
/**
* The relation resolver.
*
* @var \Closure(mixed):(\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model|null)
*/
public Closure $relationResolver;
/**
* The relation resource class.
*
* @var class-string<\Illuminate\Http\Resources\JsonApi\JsonApiResource>|null
*/
public ?string $relationResourceClass = null;
/**
* Construct a new resource relationship resolver.
*
* @param \Closure(mixed):(\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model|null)|class-string<\Illuminate\Http\Resources\JsonApi\JsonApiResource>|null $resolver
*/
public function __construct(public string $relationName, Closure|string|null $resolver = null)
{
$this->relationResolver = match (true) {
$resolver instanceof Closure => $resolver,
default => fn ($resource) => $resource->getRelation($this->relationName),
};
if (is_string($resolver) && class_exists($resolver)) {
$this->relationResourceClass = $resolver;
}
}
/**
* Resolve the relation for a resource.
*/
public function handle(mixed $resource): Collection|Model|null
{
return value($this->relationResolver, $resource);
}
/**
* Get the resource class.
*
* @return class-string<\Illuminate\Http\Resources\JsonApi\JsonApiResource>|null
*/
public function resourceClass(): ?string
{
return $this->relationResourceClass;
}
}