Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
c69a2e5
[JSON:API] Supports `JsonApiResource::toRelationships()`
crynobone Nov 3, 2025
cd01fa7
Apply fixes from StyleCI
StyleCIBot Nov 3, 2025
ec25825
wip
crynobone Nov 4, 2025
9a0a8fe
Apply fixes from StyleCI
StyleCIBot Nov 4, 2025
3ee71cc
wip
crynobone Nov 4, 2025
096ce53
Merge remote-tracking branch 'upstream/json-api-resource' into json-a…
crynobone Nov 4, 2025
5a3f43f
wip
crynobone Nov 4, 2025
6de34f6
Update JsonApiResource.php
taylorotwell Nov 4, 2025
04e0d2e
only include relationship if defined
taylorotwell Nov 4, 2025
a89fd69
formatting
taylorotwell Nov 4, 2025
b0eaf03
tweak how relations work
taylorotwell Nov 4, 2025
04bfaad
work on relationships
taylorotwell Nov 4, 2025
2479bae
fixes implementation
crynobone Nov 5, 2025
f2ffda0
Add `JsonApiRequest`
crynobone Nov 5, 2025
04fc800
Apply fixes from StyleCI
StyleCIBot Nov 5, 2025
2e56f97
wip
crynobone Nov 5, 2025
caf5c9c
Merge remote-tracking branch 'upstream/json-api-resource-relations' i…
crynobone Nov 5, 2025
334b9c2
wip
crynobone Nov 5, 2025
5930c7d
wip
crynobone Nov 5, 2025
0828655
wip
crynobone Nov 5, 2025
4a09643
formatting
taylorotwell Nov 5, 2025
2b7861e
wip
crynobone Nov 6, 2025
51f5f9f
Apply fixes from StyleCI
StyleCIBot Nov 6, 2025
227ef2a
wip
crynobone Nov 6, 2025
a7ec8d0
Apply fixes from StyleCI
StyleCIBot Nov 6, 2025
a949f45
wip
crynobone Nov 6, 2025
1fcd9a7
Apply fixes from StyleCI
StyleCIBot Nov 6, 2025
3cde253
Add `make:resource` with `--json-api` option
crynobone Nov 6, 2025
a9d041d
Apply suggestions from code review
crynobone Nov 6, 2025
f59070b
ensure request is always `JsonApiRequest`
crynobone Nov 7, 2025
d320641
wip
crynobone Nov 7, 2025
4ae7eb7
wip
crynobone Nov 7, 2025
2e6d941
Apply fixes from StyleCI
StyleCIBot Nov 7, 2025
64cacc9
wip
crynobone Nov 7, 2025
5748a9f
wip
crynobone Nov 7, 2025
acbde38
Apply fixes from StyleCI
StyleCIBot Nov 7, 2025
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
9 changes: 6 additions & 3 deletions src/Illuminate/Foundation/Console/ResourceMakeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,11 @@ public function handle()
*/
protected function getStub()
{
return $this->collection()
? $this->resolveStubPath('/stubs/resource-collection.stub')
: $this->resolveStubPath('/stubs/resource.stub');
return match (true) {
$this->collection() => $this->resolveStubPath('/stubs/resource-collection.stub'),
$this->option('json-api') => $this->resolveStubPath('/stubs/resource-json-api.stub'),
default => $this->resolveStubPath('/stubs/resource.stub'),
};
}

/**
Expand Down Expand Up @@ -101,6 +103,7 @@ protected function getOptions()
return [
['force', 'f', InputOption::VALUE_NONE, 'Create the class even if the resource already exists'],
['collection', 'c', InputOption::VALUE_NONE, 'Create a resource collection'],
['json-api', 'j', InputOption::VALUE_NONE, 'Create a JSON:API resource'],
];
}
}
19 changes: 19 additions & 0 deletions src/Illuminate/Foundation/Console/stubs/resource-json-api.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace {{ namespace }};

use Illuminate\Http\Request;
use Illuminate\Http\Resources\JsonApi\JsonApiResource;

class {{ class }} extends JsonApiResource
{
/**
* Transform the resource into an array.
*
* @return list<string>|array<string, mixed>
*/
public function toAttributes(Request $request): array
{
return parent::toAttributes($request);
}
}
16 changes: 13 additions & 3 deletions src/Illuminate/Http/Resources/Json/JsonResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ protected static function newCollection($resource)
public function resolve($request = null)
{
$data = $this->toAttributes(
$request ?: Container::getInstance()->make('request')
$request ?: $this->resolveRequestFromContainer()
);

if ($data instanceof Arrayable) {
Expand Down Expand Up @@ -230,6 +230,16 @@ public function withResponse(Request $request, JsonResponse $response)
//
}

/**
* Resolve the HTTP request instance from container.
*
* @return \Illuminate\Http\Request
*/
protected function resolveRequestFromContainer()
{
return Container::getInstance()->make('request');
}

/**
* Set the string that should wrap the outer-most resource array.
*
Expand Down Expand Up @@ -260,7 +270,7 @@ public static function withoutWrapping()
public function response($request = null)
{
return $this->toResponse(
$request ?: Container::getInstance()->make('request')
$request ?: $this->resolveRequestFromContainer()
);
}

Expand All @@ -282,7 +292,7 @@ public function toResponse($request)
*/
public function jsonSerialize(): array
{
return $this->resolve(Container::getInstance()->make('request'));
return $this->resolve($this->resolveRequestFromContainer());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

namespace Illuminate\Http\Resources\JsonApi;

use Illuminate\Container\Container;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class AnonymousResourceCollection extends \Illuminate\Http\Resources\Json\AnonymousResourceCollection
{
use Concerns\ResolvesJsonApiRequest;

/**
* Get any additional data that should be returned with the resource array.
*
Expand All @@ -20,7 +23,6 @@ public function with($request)
'included' => $this->collection
->map(fn ($resource) => $resource->resolveIncludedResources($request))
->flatten(depth: 1)
->uniqueStrict(fn ($relation): array => [$relation['id'], $relation['type']])
->all(),
...($implementation = JsonApiResource::$jsonApiInformation)
? ['jsonapi' => $implementation]
Expand Down Expand Up @@ -54,4 +56,27 @@ 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\SparseRequest
*/
#[\Override]
protected function resolveRequestFromContainer()
{
return $this->resolveJsonApiRequestFrom(Container::getInstance()->make('request'));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@

use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\AsPivot;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Http\Request;
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\MissingValue;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
Expand All @@ -32,13 +36,15 @@ trait ResolvesJsonApiElements
/**
* Resolves `data` for the resource.
*/
public function resolveResourceData(Request $request): array
public function resolveResourceData(JsonApiRequest $request): array
{
$resourceType = $this->resolveResourceType($request);

return [
'id' => $this->resolveResourceIdentifier($request),
'type' => $this->resolveResourceType($request),
'type' => $resourceType,
...(new Collection([
'attributes' => $this->resolveResourceAttributes($request),
'attributes' => $this->resolveResourceAttributes($request, $resourceType),
'relationships' => $this->resolveResourceRelationshipIdentifiers($request),
'links' => $this->resolveResourceLinks($request),
'meta' => $this->resolveResourceMetaInformation($request),
Expand All @@ -53,7 +59,7 @@ public function resolveResourceData(Request $request): array
*
* @throws ResourceIdentificationException
*/
protected function resolveResourceIdentifier(Request $request): string
protected function resolveResourceIdentifier(JsonApiRequest $request): string
{
if (! is_null($resourceId = $this->toId($request))) {
return $resourceId;
Expand All @@ -72,7 +78,7 @@ protected function resolveResourceIdentifier(Request $request): string
*
* @throws ResourceIdentificationException
*/
protected function resolveResourceType(Request $request): string
protected function resolveResourceType(JsonApiRequest $request): string
{
if (! is_null($resourceType = $this->toType($request))) {
return $resourceType;
Expand All @@ -91,7 +97,7 @@ protected function resolveResourceType(Request $request): string
*
* @throws \RuntimeException
*/
protected function resolveResourceAttributes(Request $request): array
protected function resolveResourceAttributes(JsonApiRequest $request, string $resourceType): array
{
$data = $this->toAttributes($request);

Expand All @@ -101,8 +107,11 @@ protected function resolveResourceAttributes(Request $request): array
$data = $data->jsonSerialize();
}

$sparseFieldset = $request->sparseFields($resourceType);

$data = (new Collection($data))
->mapWithKeys(fn ($value, $key) => is_int($key) ? [$value => $this->resource->{$value}] : [$key => $value])
->when(! empty($sparseFieldset), fn ($attributes) => $attributes->only($sparseFieldset))
->transform(fn ($value) => value($value, $request))
->all();

Expand All @@ -116,7 +125,7 @@ protected function resolveResourceAttributes(Request $request): array
*
* @throws \RuntimeException
*/
protected function resolveResourceRelationshipIdentifiers(Request $request): array
protected function resolveResourceRelationshipIdentifiers(JsonApiRequest $request): array
{
if (! $this->resource instanceof Model) {
return [];
Expand All @@ -125,85 +134,123 @@ protected function resolveResourceRelationshipIdentifiers(Request $request): arr
$this->compileResourceRelationships($request);

return [
...$this->loadedRelationshipIdentifiers,
...(new Collection($this->filter($this->loadedRelationshipIdentifiers)))
->map(function ($relation) {
return ! is_null($relation) ? $relation : ['data' => []];
})->all(),
];
}

/**
* Compile resource relationships.
*/
protected function compileResourceRelationships(Request $request): void
protected function compileResourceRelationships(JsonApiRequest $request): void
{
if ($this->loadedRelationshipsMap instanceof WeakMap) {
return;
}

$this->loadedRelationshipsMap = new WeakMap;
$sparseIncluded = $request->sparseIncluded();

$resourceRelationships = (new Collection($this->toRelationships($request)))
->mapWithKeys(fn ($value, $key) => is_int($key) ? [$value => fn () => $this->resource->{$value}] : [$key => $value])
->filter(fn ($value, $key) => in_array($key, $sparseIncluded));

$resourceRelationshipKeys = $resourceRelationships->keys();

$this->resource->loadMissing($resourceRelationshipKeys->all());

$this->loadedRelationshipIdentifiers = (new Collection($this->resource->getRelations()))
->mapWithKeys(function ($relations, $key) {
if ($relations instanceof Collection) {
if ($relations->isEmpty()) {
return [$key => ['data' => $relations]];
}
$this->loadedRelationshipsMap = new WeakMap;

$key = static::resourceTypeFromModel($relations->first());
$this->loadedRelationshipIdentifiers = $resourceRelationships->mapWithKeys(function ($relationResolver, $key) {
$relatedModels = value($relationResolver);

return [$key => ['data' => $relations->map(function ($relation) use ($key) {
return transform([$key, static::resourceIdFromModel($relation)], function ($uniqueKey) use ($relation) {
$this->loadedRelationshipsMap[$relation] = $uniqueKey;
// Relationship is a collection of models...
if ($relatedModels instanceof Collection) {
$relatedModels = $relatedModels->values();

return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]];
});
})]];
if ($relatedModels->isEmpty()) {
return [$key => ['data' => $relatedModels]];
}

return [$key => ['data' => transform(
[static::resourceTypeFromModel($relations), static::resourceIdFromModel($relations)],
function ($uniqueKey) use ($relations) {
$this->loadedRelationshipsMap[$relations] = $uniqueKey;
$relationship = $this->resource->{$key}();

$isUnique = ! $relationship instanceof BelongsToMany;

$key = static::resourceTypeFromModel($relatedModels->first());

return [$key => ['data' => $relatedModels->map(function ($relation) use ($key, $isUnique) {
return transform([$key, static::resourceIdFromModel($relation)], function ($uniqueKey) use ($relation, $isUnique) {
$this->loadedRelationshipsMap[$relation] = [...$uniqueKey, $isUnique];

return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]];
}
)]];
})->all();
});
})]];
}

// Relationship is a single model...
$relatedModel = $relatedModels;

if (is_null($relatedModel)) {
return [$key => null];
} elseif ($relatedModel instanceof Pivot ||
in_array(AsPivot::class, class_uses_recursive($relatedModel), true)) {
return [$key => new MissingValue];
}

return [$key => ['data' => [transform(
[static::resourceTypeFromModel($relatedModel), static::resourceIdFromModel($relatedModel)],
function ($uniqueKey) use ($relatedModel) {
$this->loadedRelationshipsMap[$relatedModel] = [...$uniqueKey, true];

return ['id' => $uniqueKey[1], 'type' => $uniqueKey[0]];
}
)]]];
})->all();
}

/**
* Resolves `included` for the resource.
*/
public function resolveIncludedResources(Request $request): array
public function resolveIncludedResources(JsonApiRequest $request): array
{
if (! $this->resource instanceof Model) {
return [];
}

$this->compileResourceRelationships($request);

$relations = new Collection;

foreach ($this->loadedRelationshipsMap as $relation => $uniqueKey) {
foreach ($this->loadedRelationshipsMap as $relation => $value) {
$resourceInstance = rescue(fn () => $relation->toResource(), new JsonApiResource($relation), false);

if (! $resourceInstance instanceof JsonApiResource &&
$resourceInstance instanceof JsonResource) {
$resourceInstance = new JsonApiResource($resourceInstance->resource);
}

[$type, $id, $isUnique] = $value;

$relations->push([
'id' => $uniqueKey[1],
'type' => $uniqueKey[0],
'id' => $id,
'type' => $type,
'_uniqueKey' => $isUnique === true ? [$id, $type] : [$id, $type, (string) Str::random()],
'attributes' => Arr::get($resourceInstance->resolve($request), 'data.attributes', []),
]);
}

return $relations->uniqueStrict(
fn ($relation): array => [$relation['id'], $relation['type']]
)->all();
return $relations->uniqueStrict(fn ($relation) => $relation['_uniqueKey'])
->map(fn ($relation) => Arr::except($relation, ['_uniqueKey']))
->all();
}

/**
* Resolve the links for the resource.
*
* @return array<string, mixed>
*/
protected function resolveResourceLinks(Request $request): array
protected function resolveResourceLinks(JsonApiRequest $request): array
{
return $this->toLinks($request);
}
Expand All @@ -213,7 +260,7 @@ protected function resolveResourceLinks(Request $request): array
*
* @return array<string, mixed>
*/
protected function resolveResourceMetaInformation(Request $request): array
protected function resolveResourceMetaInformation(JsonApiRequest $request): array
{
return $this->toMeta($request);
}
Expand Down
Loading