暂存
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Http\Resources\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class Collects
|
||||
{
|
||||
/**
|
||||
* Create a new attribute instance.
|
||||
*
|
||||
* @param string $class
|
||||
*/
|
||||
public function __construct(public string $class)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Http\Resources\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class PreserveKeys
|
||||
{
|
||||
/**
|
||||
* Create a new attribute instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Attributes\Collects;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Pagination\AbstractCursorPaginator;
|
||||
use Illuminate\Pagination\AbstractPaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use LogicException;
|
||||
use ReflectionClass;
|
||||
use Traversable;
|
||||
|
||||
trait CollectsResources
|
||||
{
|
||||
/**
|
||||
* The cached Collects attribute values.
|
||||
*
|
||||
* @var array<class-string, class-string|false>
|
||||
*/
|
||||
protected static $cachedCollectsAttributes = [];
|
||||
|
||||
/**
|
||||
* Map the given collection resource into its individual resources.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @return mixed
|
||||
*/
|
||||
protected function collectResource($resource)
|
||||
{
|
||||
if ($resource instanceof MissingValue) {
|
||||
return $resource;
|
||||
}
|
||||
|
||||
if (is_array($resource)) {
|
||||
$resource = new Collection($resource);
|
||||
}
|
||||
|
||||
$collects = $this->collects();
|
||||
|
||||
$this->collection = $collects && ! $resource->first() instanceof $collects
|
||||
? $resource->mapInto($collects)
|
||||
: $resource->toBase();
|
||||
|
||||
return ($resource instanceof AbstractPaginator || $resource instanceof AbstractCursorPaginator)
|
||||
? $resource->setCollection($this->collection)
|
||||
: $this->collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resource that this resource collects.
|
||||
*
|
||||
* @return class-string<\Illuminate\Http\Resources\Json\JsonResource>|null
|
||||
*
|
||||
* @throws \LogicException
|
||||
*/
|
||||
protected function collects()
|
||||
{
|
||||
$collects = null;
|
||||
|
||||
if (! array_key_exists(static::class, static::$cachedCollectsAttributes)) {
|
||||
$attribute = (new ReflectionClass($this))->getAttributes(Collects::class);
|
||||
|
||||
static::$cachedCollectsAttributes[static::class] = $attribute !== []
|
||||
? $attribute[0]->newInstance()->class
|
||||
: false;
|
||||
}
|
||||
|
||||
if (static::$cachedCollectsAttributes[static::class]) {
|
||||
$collects = static::$cachedCollectsAttributes[static::class];
|
||||
} elseif ($this->collects) {
|
||||
$collects = $this->collects;
|
||||
} elseif (str_ends_with(class_basename($this), 'Collection') &&
|
||||
(class_exists($class = Str::replaceLast('Collection', '', get_class($this))) ||
|
||||
class_exists($class = Str::replaceLast('Collection', 'Resource', get_class($this))))) {
|
||||
$collects = $class;
|
||||
}
|
||||
|
||||
if (! $collects || is_a($collects, JsonResource::class, true)) {
|
||||
return $collects;
|
||||
}
|
||||
|
||||
throw new LogicException('Resource collections must collect instances of '.JsonResource::class.'.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON serialization options that should be applied to the resource response.
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
public function jsonOptions()
|
||||
{
|
||||
$collects = $this->collects();
|
||||
|
||||
if (! $collects) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (new ReflectionClass($collects))
|
||||
->newInstanceWithoutConstructor()
|
||||
->jsonOptions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an iterator for the resource collection.
|
||||
*
|
||||
* @return \ArrayIterator
|
||||
*/
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
return $this->collection->getIterator();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Attributes\PreserveKeys;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Stringable;
|
||||
use ReflectionClass;
|
||||
|
||||
trait ConditionallyLoadsAttributes
|
||||
{
|
||||
/**
|
||||
* The cached preserve keys attribute values.
|
||||
*
|
||||
* @var array<class-string, bool>
|
||||
*/
|
||||
protected static $cachedPreserveKeysAttributes = [];
|
||||
|
||||
/**
|
||||
* Filter the given data, removing any optional values.
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
protected function filter($data)
|
||||
{
|
||||
$index = -1;
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
$index++;
|
||||
|
||||
if (is_array($value)) {
|
||||
$data[$key] = $this->filter($value);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_numeric($key) && $value instanceof MergeValue) {
|
||||
return $this->mergeData(
|
||||
$data, $index, $this->filter($value->data),
|
||||
array_values($value->data) === $value->data
|
||||
);
|
||||
}
|
||||
|
||||
if ($value instanceof self && is_null($value->resource)) {
|
||||
$data[$key] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->removeMissingValues($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the given data in at the given index.
|
||||
*
|
||||
* @param array $data
|
||||
* @param int $index
|
||||
* @param array $merge
|
||||
* @param bool $numericKeys
|
||||
* @return array
|
||||
*/
|
||||
protected function mergeData($data, $index, $merge, $numericKeys)
|
||||
{
|
||||
if ($numericKeys) {
|
||||
return $this->removeMissingValues(array_merge(
|
||||
array_merge(array_slice($data, 0, $index, true), $merge),
|
||||
$this->filter(array_values(array_slice($data, $index + 1, null, true)))
|
||||
));
|
||||
}
|
||||
|
||||
return $this->removeMissingValues(array_slice($data, 0, $index, true) +
|
||||
$merge +
|
||||
$this->filter(array_slice($data, $index + 1, null, true)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the missing values from the filtered data.
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
protected function removeMissingValues($data)
|
||||
{
|
||||
$numericKeys = true;
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if (($value instanceof PotentiallyMissing && $value->isMissing()) ||
|
||||
($value instanceof self &&
|
||||
$value->resource instanceof PotentiallyMissing &&
|
||||
$value->isMissing())) {
|
||||
unset($data[$key]);
|
||||
} else {
|
||||
$numericKeys = $numericKeys && is_numeric($key);
|
||||
}
|
||||
}
|
||||
|
||||
if (! array_key_exists(static::class, static::$cachedPreserveKeysAttributes)) {
|
||||
static::$cachedPreserveKeysAttributes[static::class] = (new ReflectionClass($this))->getAttributes(PreserveKeys::class) !== [];
|
||||
}
|
||||
|
||||
if (static::$cachedPreserveKeysAttributes[static::class]) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
if (property_exists($this, 'preserveKeys') && $this->preserveKeys === true) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
return $numericKeys ? array_values($data) : $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a value if the given "condition" is truthy.
|
||||
*
|
||||
* @param bool $condition
|
||||
* @param mixed $value
|
||||
* @param mixed $default
|
||||
* @return \Illuminate\Http\Resources\MissingValue|mixed
|
||||
*/
|
||||
protected function when($condition, $value, $default = new MissingValue)
|
||||
{
|
||||
if ($condition) {
|
||||
return value($value);
|
||||
}
|
||||
|
||||
return func_num_args() === 3 ? value($default) : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a value if the given "condition" is falsy.
|
||||
*
|
||||
* @param bool $condition
|
||||
* @param mixed $value
|
||||
* @param mixed $default
|
||||
* @return \Illuminate\Http\Resources\MissingValue|mixed
|
||||
*/
|
||||
public function unless($condition, $value, $default = new MissingValue)
|
||||
{
|
||||
$arguments = func_num_args() === 2 ? [$value] : [$value, $default];
|
||||
|
||||
return $this->when(! $condition, ...$arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge a value into the array.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return \Illuminate\Http\Resources\MergeValue|mixed
|
||||
*/
|
||||
protected function merge($value)
|
||||
{
|
||||
return $this->mergeWhen(true, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge a value if the given condition is truthy.
|
||||
*
|
||||
* @param bool $condition
|
||||
* @param mixed $value
|
||||
* @param mixed $default
|
||||
* @return \Illuminate\Http\Resources\MergeValue|mixed
|
||||
*/
|
||||
protected function mergeWhen($condition, $value, $default = new MissingValue)
|
||||
{
|
||||
if ($condition) {
|
||||
return new MergeValue(value($value));
|
||||
}
|
||||
|
||||
return func_num_args() === 3 ? new MergeValue(value($default)) : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge a value unless the given condition is truthy.
|
||||
*
|
||||
* @param bool $condition
|
||||
* @param mixed $value
|
||||
* @param mixed $default
|
||||
* @return \Illuminate\Http\Resources\MergeValue|mixed
|
||||
*/
|
||||
protected function mergeUnless($condition, $value, $default = new MissingValue)
|
||||
{
|
||||
$arguments = func_num_args() === 2 ? [$value] : [$value, $default];
|
||||
|
||||
return $this->mergeWhen(! $condition, ...$arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the given attributes.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @return \Illuminate\Http\Resources\MergeValue
|
||||
*/
|
||||
protected function attributes($attributes)
|
||||
{
|
||||
return new MergeValue(
|
||||
Arr::only($this->resource->toArray(), $attributes)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an attribute if it exists on the resource.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param mixed $value
|
||||
* @param mixed $default
|
||||
* @return \Illuminate\Http\Resources\MissingValue|mixed
|
||||
*/
|
||||
public function whenHas($attribute, $value = null, $default = new MissingValue)
|
||||
{
|
||||
if (! array_key_exists($attribute, $this->resource->getAttributes())) {
|
||||
return value($default);
|
||||
}
|
||||
|
||||
return func_num_args() === 1
|
||||
? $this->resource->{$attribute}
|
||||
: value($value, $this->resource->{$attribute});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a model attribute if it is null.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param mixed $default
|
||||
* @return \Illuminate\Http\Resources\MissingValue|mixed
|
||||
*/
|
||||
protected function whenNull($value, $default = new MissingValue)
|
||||
{
|
||||
$arguments = func_num_args() == 1 ? [$value] : [$value, $default];
|
||||
|
||||
return $this->when(is_null($value), ...$arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a model attribute if it is not null.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param mixed $default
|
||||
* @return \Illuminate\Http\Resources\MissingValue|mixed
|
||||
*/
|
||||
protected function whenNotNull($value, $default = new MissingValue)
|
||||
{
|
||||
$arguments = func_num_args() == 1 ? [$value] : [$value, $default];
|
||||
|
||||
return $this->when(! is_null($value), ...$arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an accessor when it has been appended.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param mixed $value
|
||||
* @param mixed $default
|
||||
* @return \Illuminate\Http\Resources\MissingValue|mixed
|
||||
*/
|
||||
protected function whenAppended($attribute, $value = null, $default = new MissingValue)
|
||||
{
|
||||
if ($this->resource->hasAppended($attribute)) {
|
||||
return func_num_args() >= 2 ? value($value) : $this->resource->$attribute;
|
||||
}
|
||||
|
||||
return func_num_args() === 3 ? value($default) : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a relationship if it has been loaded.
|
||||
*
|
||||
* @param string $relationship
|
||||
* @param mixed $value
|
||||
* @param mixed $default
|
||||
* @return \Illuminate\Http\Resources\MissingValue|mixed
|
||||
*/
|
||||
protected function whenLoaded($relationship, $value = null, $default = new MissingValue)
|
||||
{
|
||||
if (! $this->resource->relationLoaded($relationship)) {
|
||||
return value($default);
|
||||
}
|
||||
|
||||
$loadedValue = $this->resource->{$relationship};
|
||||
|
||||
if (func_num_args() === 1) {
|
||||
return $loadedValue;
|
||||
}
|
||||
|
||||
if ($loadedValue === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
$value = value(...);
|
||||
}
|
||||
|
||||
return value($value, $loadedValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a relationship count if it exists.
|
||||
*
|
||||
* @param string $relationship
|
||||
* @param mixed $value
|
||||
* @param mixed $default
|
||||
* @return \Illuminate\Http\Resources\MissingValue|mixed
|
||||
*/
|
||||
public function whenCounted($relationship, $value = null, $default = new MissingValue)
|
||||
{
|
||||
$attribute = (new Stringable($relationship))->snake()->finish('_count')->value();
|
||||
|
||||
if (! array_key_exists($attribute, $this->resource->getAttributes())) {
|
||||
return value($default);
|
||||
}
|
||||
|
||||
if (func_num_args() === 1) {
|
||||
return $this->resource->{$attribute};
|
||||
}
|
||||
|
||||
if ($this->resource->{$attribute} === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
$value = value(...);
|
||||
}
|
||||
|
||||
return value($value, $this->resource->{$attribute});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a relationship aggregated value if it exists.
|
||||
*
|
||||
* @param string $relationship
|
||||
* @param string $column
|
||||
* @param string $aggregate
|
||||
* @param mixed $value
|
||||
* @param mixed $default
|
||||
* @return \Illuminate\Http\Resources\MissingValue|mixed
|
||||
*/
|
||||
public function whenAggregated($relationship, $column, $aggregate, $value = null, $default = new MissingValue)
|
||||
{
|
||||
$attribute = (new Stringable($relationship))->snake()->append('_')->append($aggregate)->append('_')->finish($column)->value();
|
||||
|
||||
if (! array_key_exists($attribute, $this->resource->getAttributes())) {
|
||||
return value($default);
|
||||
}
|
||||
|
||||
if (func_num_args() === 3) {
|
||||
return $this->resource->{$attribute};
|
||||
}
|
||||
|
||||
if ($this->resource->{$attribute} === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
$value = value(...);
|
||||
}
|
||||
|
||||
return value($value, $this->resource->{$attribute});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a relationship existence check if it exists.
|
||||
*
|
||||
* @param string $relationship
|
||||
* @param mixed $value
|
||||
* @param mixed $default
|
||||
* @return \Illuminate\Http\Resources\MissingValue|mixed
|
||||
*/
|
||||
public function whenExistsLoaded($relationship, $value = null, $default = new MissingValue)
|
||||
{
|
||||
$attribute = (new Stringable($relationship))->snake()->finish('_exists')->value();
|
||||
|
||||
if (! array_key_exists($attribute, $this->resource->getAttributes())) {
|
||||
return value($default);
|
||||
}
|
||||
|
||||
if (func_num_args() === 1) {
|
||||
return $this->resource->{$attribute};
|
||||
}
|
||||
|
||||
if ($this->resource->{$attribute} === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
return value($value, $this->resource->{$attribute});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a callback if the given pivot table has been loaded.
|
||||
*
|
||||
* @param string $table
|
||||
* @param mixed $value
|
||||
* @param mixed $default
|
||||
* @return \Illuminate\Http\Resources\MissingValue|mixed
|
||||
*/
|
||||
protected function whenPivotLoaded($table, $value, $default = new MissingValue)
|
||||
{
|
||||
return $this->whenPivotLoadedAs('pivot', ...func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a callback if the given pivot table with a custom accessor has been loaded.
|
||||
*
|
||||
* @param string $accessor
|
||||
* @param string $table
|
||||
* @param mixed $value
|
||||
* @param mixed $default
|
||||
* @return \Illuminate\Http\Resources\MissingValue|mixed
|
||||
*/
|
||||
protected function whenPivotLoadedAs($accessor, $table, $value, $default = new MissingValue)
|
||||
{
|
||||
return $this->when(
|
||||
$this->hasPivotLoadedAs($accessor, $table),
|
||||
$value,
|
||||
$default,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the resource has the specified pivot table loaded.
|
||||
*
|
||||
* @param string $table
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasPivotLoaded($table)
|
||||
{
|
||||
return $this->hasPivotLoadedAs('pivot', $table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the resource has the specified pivot table loaded with a custom accessor.
|
||||
*
|
||||
* @param string $accessor
|
||||
* @param string $table
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasPivotLoadedAs($accessor, $table)
|
||||
{
|
||||
return isset($this->resource->$accessor) &&
|
||||
($this->resource->$accessor instanceof $table ||
|
||||
$this->resource->$accessor->getTable() === $table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the given value if it is present.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param callable $callback
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
protected function transform($value, callable $callback, $default = new MissingValue)
|
||||
{
|
||||
return transform(
|
||||
$value, $callback, $default
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Http\Resources;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Support\Traits\ForwardsCalls;
|
||||
use Illuminate\Support\Traits\Macroable;
|
||||
|
||||
trait DelegatesToResource
|
||||
{
|
||||
use ForwardsCalls, Macroable {
|
||||
__call as macroCall;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the resource's route key.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRouteKey()
|
||||
{
|
||||
return $this->resource->getRouteKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the route key for the resource.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRouteKeyName()
|
||||
{
|
||||
return $this->resource->getRouteKeyName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the model for a bound value.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param string|null $field
|
||||
* @return void
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function resolveRouteBinding($value, $field = null)
|
||||
{
|
||||
throw new Exception('Resources may not be implicitly resolved from route bindings.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the model for a bound value.
|
||||
*
|
||||
* @param string $childType
|
||||
* @param mixed $value
|
||||
* @param string|null $field
|
||||
* @return void
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function resolveChildRouteBinding($childType, $value, $field = null)
|
||||
{
|
||||
throw new Exception('Resources may not be implicitly resolved from child route bindings.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given attribute exists.
|
||||
*
|
||||
* @param mixed $offset
|
||||
* @return bool
|
||||
*/
|
||||
public function offsetExists($offset): bool
|
||||
{
|
||||
return isset($this->resource[$offset]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value for a given offset.
|
||||
*
|
||||
* @param mixed $offset
|
||||
* @return mixed
|
||||
*/
|
||||
public function offsetGet($offset): mixed
|
||||
{
|
||||
return $this->resource[$offset];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value for a given offset.
|
||||
*
|
||||
* @param mixed $offset
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
public function offsetSet($offset, $value): void
|
||||
{
|
||||
$this->resource[$offset] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset the value for a given offset.
|
||||
*
|
||||
* @param mixed $offset
|
||||
* @return void
|
||||
*/
|
||||
public function offsetUnset($offset): void
|
||||
{
|
||||
unset($this->resource[$offset]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an attribute exists on the resource.
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function __isset($key)
|
||||
{
|
||||
return isset($this->resource->{$key});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset an attribute on the resource.
|
||||
*
|
||||
* @param string $key
|
||||
* @return void
|
||||
*/
|
||||
public function __unset($key)
|
||||
{
|
||||
unset($this->resource->{$key});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically get properties from the underlying resource.
|
||||
*
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get($key)
|
||||
{
|
||||
return $this->resource->{$key};
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically pass method calls to the underlying resource.
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $parameters
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call($method, $parameters)
|
||||
{
|
||||
if (static::hasMacro($method)) {
|
||||
return $this->macroCall($method, $parameters);
|
||||
}
|
||||
|
||||
return $this->forwardCallTo($this->resource, $method, $parameters);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Http\Resources\Json;
|
||||
|
||||
class AnonymousResourceCollection extends ResourceCollection
|
||||
{
|
||||
/**
|
||||
* The name of the resource being collected.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $collects;
|
||||
|
||||
/**
|
||||
* Indicates if the collection keys should be preserved.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $preserveKeys = false;
|
||||
|
||||
/**
|
||||
* Create a new anonymous resource collection.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @param string $collects
|
||||
*/
|
||||
public function __construct($resource, $collects)
|
||||
{
|
||||
$this->collects = $collects;
|
||||
|
||||
parent::__construct($resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the collection keys should be preserved.
|
||||
*/
|
||||
public function preserveKeys(bool $value = true): static
|
||||
{
|
||||
$this->preserveKeys = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Http\Resources\Json;
|
||||
|
||||
use ArrayAccess;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Routing\UrlRoutable;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use Illuminate\Contracts\Support\Responsable;
|
||||
use Illuminate\Database\Eloquent\JsonEncodingException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Attributes\PreserveKeys;
|
||||
use Illuminate\Http\Resources\ConditionallyLoadsAttributes;
|
||||
use Illuminate\Http\Resources\DelegatesToResource;
|
||||
use JsonException;
|
||||
use JsonSerializable;
|
||||
use ReflectionClass;
|
||||
|
||||
class JsonResource implements ArrayAccess, JsonSerializable, Responsable, UrlRoutable
|
||||
{
|
||||
use ConditionallyLoadsAttributes, DelegatesToResource;
|
||||
|
||||
/**
|
||||
* The resource instance.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
public $resource;
|
||||
|
||||
/**
|
||||
* The additional data that should be added to the top-level resource array.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $with = [];
|
||||
|
||||
/**
|
||||
* The additional metadata that should be added to the resource response.
|
||||
*
|
||||
* Added during response construction by the developer.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $additional = [];
|
||||
|
||||
/**
|
||||
* The "data" wrapper that should be applied.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public static $wrap = 'data';
|
||||
|
||||
/**
|
||||
* Whether to force wrapping even if the $wrap key exists in underlying resource data.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public static bool $forceWrapping = false;
|
||||
|
||||
/**
|
||||
* Create a new resource instance.
|
||||
*
|
||||
* @param mixed $resource
|
||||
*/
|
||||
public function __construct($resource)
|
||||
{
|
||||
$this->resource = $resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new resource instance.
|
||||
*
|
||||
* @param mixed ...$parameters
|
||||
* @return static
|
||||
*/
|
||||
public static function make(...$parameters)
|
||||
{
|
||||
return new static(...$parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new anonymous resource collection.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
|
||||
*/
|
||||
public static function collection($resource)
|
||||
{
|
||||
return tap(static::newCollection($resource), function ($collection) {
|
||||
if (! array_key_exists(static::class, static::$cachedPreserveKeysAttributes)) {
|
||||
static::$cachedPreserveKeysAttributes[static::class] = (new ReflectionClass(static::class))->getAttributes(PreserveKeys::class) !== [];
|
||||
}
|
||||
|
||||
if (static::$cachedPreserveKeysAttributes[static::class]) {
|
||||
$collection->preserveKeys = true;
|
||||
} elseif (property_exists(static::class, 'preserveKeys')) {
|
||||
$collection->preserveKeys = (new static([]))->preserveKeys === true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new resource collection instance.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
|
||||
*/
|
||||
protected static function newCollection($resource)
|
||||
{
|
||||
return new AnonymousResourceCollection($resource, static::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the resource to an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request|null $request
|
||||
* @return array
|
||||
*/
|
||||
public function resolve($request = null)
|
||||
{
|
||||
$data = $this->resolveResourceData(
|
||||
$request ?: $this->resolveRequestFromContainer()
|
||||
);
|
||||
|
||||
if ($data instanceof Arrayable) {
|
||||
$data = $data->toArray();
|
||||
} elseif ($data instanceof JsonSerializable) {
|
||||
$data = $data->jsonSerialize();
|
||||
}
|
||||
|
||||
return $this->filter((array) $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
|
||||
*/
|
||||
public function toAttributes(Request $request)
|
||||
{
|
||||
if (property_exists($this, 'attributes')) {
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
return $this->toArray($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the resource data to an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function resolveResourceData(Request $request)
|
||||
{
|
||||
return $this->toAttributes($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
|
||||
*/
|
||||
public function toArray(Request $request)
|
||||
{
|
||||
if (is_null($this->resource)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return is_array($this->resource)
|
||||
? $this->resource
|
||||
: $this->resource->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the resource to JSON.
|
||||
*
|
||||
* @param int $options
|
||||
* @return string
|
||||
*
|
||||
* @throws \Illuminate\Database\Eloquent\JsonEncodingException
|
||||
*/
|
||||
public function toJson($options = 0)
|
||||
{
|
||||
try {
|
||||
$json = json_encode($this->jsonSerialize(), $options | JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException $e) {
|
||||
throw JsonEncodingException::forResource($this, $e->getMessage());
|
||||
}
|
||||
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the resource to pretty print formatted JSON.
|
||||
*
|
||||
* @param int $options
|
||||
* @return string
|
||||
*
|
||||
* @throws \Illuminate\Database\Eloquent\JsonEncodingException
|
||||
*/
|
||||
public function toPrettyJson(int $options = 0)
|
||||
{
|
||||
return $this->toJson(JSON_PRETTY_PRINT | $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get any additional data that should be returned with the resource array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function with(Request $request)
|
||||
{
|
||||
return $this->with;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add additional metadata to the resource response.
|
||||
*
|
||||
* @param array $data
|
||||
* @return $this
|
||||
*/
|
||||
public function additional(array $data)
|
||||
{
|
||||
$this->additional = $data;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON serialization options that should be applied to the resource response.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function jsonOptions()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customize the response for a request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Illuminate\Http\JsonResponse $response
|
||||
* @return void
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @param string $value
|
||||
* @return void
|
||||
*/
|
||||
public static function wrap($value)
|
||||
{
|
||||
static::$wrap = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable wrapping of the outer-most resource array.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function withoutWrapping()
|
||||
{
|
||||
static::$wrap = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the resource into an HTTP response.
|
||||
*
|
||||
* @param \Illuminate\Http\Request|null $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function response($request = null)
|
||||
{
|
||||
return $this->toResponse(
|
||||
$request ?: $this->resolveRequestFromContainer()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTTP response that represents the object.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function toResponse($request)
|
||||
{
|
||||
return (new ResourceResponse($this))->toResponse($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the resource for JSON serialization.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->resolve($this->resolveRequestFromContainer());
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush the resource's global state.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function flushState()
|
||||
{
|
||||
static::$wrap = 'data';
|
||||
static::$forceWrapping = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Http\Resources\Json;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class PaginatedResourceResponse extends ResourceResponse
|
||||
{
|
||||
/**
|
||||
* Create an HTTP response that represents the object.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function toResponse($request)
|
||||
{
|
||||
return tap(response()->json(
|
||||
$this->wrap(
|
||||
$this->resource->resolve($request),
|
||||
array_merge_recursive(
|
||||
$this->paginationInformation($request),
|
||||
$this->resource->with($request),
|
||||
$this->resource->additional
|
||||
)
|
||||
),
|
||||
$this->calculateStatus(),
|
||||
[],
|
||||
$this->resource->jsonOptions()
|
||||
), function ($response) use ($request) {
|
||||
$response->original = $this->resource->resource->map(function ($item) {
|
||||
if (is_array($item)) {
|
||||
return Arr::get($item, 'resource');
|
||||
} elseif (is_object($item)) {
|
||||
return $item->resource ?? null;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
$this->resource->withResponse($request, $response);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the pagination information to the response.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array
|
||||
*/
|
||||
protected function paginationInformation($request)
|
||||
{
|
||||
$paginated = $this->resource->resource->toArray();
|
||||
|
||||
$default = [
|
||||
'links' => $this->paginationLinks($paginated),
|
||||
'meta' => $this->meta($paginated),
|
||||
];
|
||||
|
||||
if (method_exists($this->resource, 'paginationInformation') ||
|
||||
$this->resource->hasMacro('paginationInformation')) {
|
||||
return $this->resource->paginationInformation($request, $paginated, $default);
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pagination links for the response.
|
||||
*
|
||||
* @param array $paginated
|
||||
* @return array
|
||||
*/
|
||||
protected function paginationLinks($paginated)
|
||||
{
|
||||
return [
|
||||
'first' => $paginated['first_page_url'] ?? null,
|
||||
'last' => $paginated['last_page_url'] ?? null,
|
||||
'prev' => $paginated['prev_page_url'] ?? null,
|
||||
'next' => $paginated['next_page_url'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather the metadata for the response.
|
||||
*
|
||||
* @param array $paginated
|
||||
* @return array
|
||||
*/
|
||||
protected function meta($paginated)
|
||||
{
|
||||
return Arr::except($paginated, [
|
||||
'data',
|
||||
'first_page_url',
|
||||
'last_page_url',
|
||||
'prev_page_url',
|
||||
'next_page_url',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Http\Resources\Json;
|
||||
|
||||
use Countable;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\CollectsResources;
|
||||
use Illuminate\Pagination\AbstractCursorPaginator;
|
||||
use Illuminate\Pagination\AbstractPaginator;
|
||||
use IteratorAggregate;
|
||||
|
||||
class ResourceCollection extends JsonResource implements Countable, IteratorAggregate
|
||||
{
|
||||
use CollectsResources;
|
||||
|
||||
/**
|
||||
* The resource that this resource collects.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $collects;
|
||||
|
||||
/**
|
||||
* The mapped collection instance.
|
||||
*
|
||||
* @var \Illuminate\Support\Collection|null
|
||||
*/
|
||||
public $collection;
|
||||
|
||||
/**
|
||||
* Indicates if all existing request query parameters should be added to pagination links.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $preserveAllQueryParameters = false;
|
||||
|
||||
/**
|
||||
* The query parameters that should be added to the pagination links.
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
protected $queryParameters;
|
||||
|
||||
/**
|
||||
* Create a new resource instance.
|
||||
*
|
||||
* @param mixed $resource
|
||||
*/
|
||||
public function __construct($resource)
|
||||
{
|
||||
parent::__construct($resource);
|
||||
|
||||
$this->resource = $this->collectResource($resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that all current query parameters should be appended to pagination links.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function preserveQuery()
|
||||
{
|
||||
$this->preserveAllQueryParameters = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the query string parameters that should be present on pagination links.
|
||||
*
|
||||
* @param array $query
|
||||
* @return $this
|
||||
*/
|
||||
public function withQuery(array $query)
|
||||
{
|
||||
$this->preserveAllQueryParameters = false;
|
||||
|
||||
$this->queryParameters = $query;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the count of items in the resource collection.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return $this->collection->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the resource into a JSON array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
|
||||
*/
|
||||
#[\Override]
|
||||
public function toArray(Request $request)
|
||||
{
|
||||
if ($this->collection->first() instanceof JsonResource) {
|
||||
return $this->collection->map->resolve($request)->all();
|
||||
}
|
||||
|
||||
return $this->collection->map->toArray($request)->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTTP response that represents the object.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function toResponse($request)
|
||||
{
|
||||
if ($this->resource instanceof AbstractPaginator || $this->resource instanceof AbstractCursorPaginator) {
|
||||
return $this->preparePaginatedResponse($request);
|
||||
}
|
||||
|
||||
return parent::toResponse($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a paginate-aware HTTP response.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
protected function preparePaginatedResponse($request)
|
||||
{
|
||||
if ($this->preserveAllQueryParameters) {
|
||||
$this->resource->appends($request->query());
|
||||
} elseif (! is_null($this->queryParameters)) {
|
||||
$this->resource->appends($this->queryParameters);
|
||||
}
|
||||
|
||||
return (new PaginatedResourceResponse($this))->toResponse($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Http\Resources\Json;
|
||||
|
||||
use Illuminate\Contracts\Support\Responsable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class ResourceResponse implements Responsable
|
||||
{
|
||||
/**
|
||||
* The underlying resource.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
public $resource;
|
||||
|
||||
/**
|
||||
* Create a new resource response.
|
||||
*
|
||||
* @param mixed $resource
|
||||
*/
|
||||
public function __construct($resource)
|
||||
{
|
||||
$this->resource = $resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTTP response that represents the object.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function toResponse($request)
|
||||
{
|
||||
return tap(response()->json(
|
||||
$this->wrap(
|
||||
$this->resource->resolve($request),
|
||||
$this->resource->with($request),
|
||||
$this->resource->additional
|
||||
),
|
||||
$this->calculateStatus(),
|
||||
[],
|
||||
$this->resource->jsonOptions()
|
||||
), function ($response) use ($request) {
|
||||
$response->original = $this->resource->resource;
|
||||
|
||||
$this->resource->withResponse($request, $response);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap the given data if necessary.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection|array $data
|
||||
* @param array $with
|
||||
* @param array $additional
|
||||
* @return array
|
||||
*/
|
||||
protected function wrap($data, $with = [], $additional = [])
|
||||
{
|
||||
if ($data instanceof Collection) {
|
||||
$data = $data->all();
|
||||
}
|
||||
|
||||
if ($this->haveDefaultWrapperAndDataIsUnwrapped($data)) {
|
||||
$data = [$this->wrapper() => $data];
|
||||
} elseif ($this->haveAdditionalInformationAndDataIsUnwrapped($data, $with, $additional)) {
|
||||
$data = [($this->wrapper() ?? 'data') => $data];
|
||||
}
|
||||
|
||||
return array_merge_recursive($data, $with, $additional);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if we have a default wrapper and the given data is unwrapped.
|
||||
*
|
||||
* @param array $data
|
||||
* @return bool
|
||||
*/
|
||||
protected function haveDefaultWrapperAndDataIsUnwrapped($data)
|
||||
{
|
||||
if ($this->resource instanceof JsonResource && $this->resource::$forceWrapping) {
|
||||
return $this->wrapper() !== null;
|
||||
}
|
||||
|
||||
return $this->wrapper() && ! array_key_exists($this->wrapper(), $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if "with" data has been added and our data is unwrapped.
|
||||
*
|
||||
* @param array $data
|
||||
* @param array $with
|
||||
* @param array $additional
|
||||
* @return bool
|
||||
*/
|
||||
protected function haveAdditionalInformationAndDataIsUnwrapped($data, $with, $additional)
|
||||
{
|
||||
return (! empty($with) || ! empty($additional)) &&
|
||||
(! $this->wrapper() ||
|
||||
! array_key_exists($this->wrapper(), $data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default data wrapper for the resource.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function wrapper()
|
||||
{
|
||||
return get_class($this->resource)::$wrap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the appropriate status code for the response.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function calculateStatus()
|
||||
{
|
||||
return $this->resource->resource instanceof Model &&
|
||||
$this->resource->resource->wasRecentlyCreated ? 201 : 200;
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
+438
@@ -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);
|
||||
}
|
||||
}
|
||||
+38
@@ -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;
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Http\Resources;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use JsonSerializable;
|
||||
|
||||
class MergeValue
|
||||
{
|
||||
/**
|
||||
* The data to be merged.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $data;
|
||||
|
||||
/**
|
||||
* Create a new merge value instance.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection|\JsonSerializable|array $data
|
||||
*/
|
||||
public function __construct($data)
|
||||
{
|
||||
$this->data = match (true) {
|
||||
$data instanceof Collection => $data->all(),
|
||||
$data instanceof JsonSerializable => $data->jsonSerialize(),
|
||||
default => $data,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Http\Resources;
|
||||
|
||||
class MissingValue implements PotentiallyMissing
|
||||
{
|
||||
/**
|
||||
* Determine if the object should be considered "missing".
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isMissing()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Http\Resources;
|
||||
|
||||
interface PotentiallyMissing
|
||||
{
|
||||
/**
|
||||
* Determine if the object should be considered "missing".
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isMissing();
|
||||
}
|
||||
Reference in New Issue
Block a user