Skip to content

Commit

Permalink
Add support for nested wrapped resources
Browse files Browse the repository at this point in the history
  • Loading branch information
lexdewilligen committed Mar 12, 2021
1 parent bbd037a commit 4fecfb9
Show file tree
Hide file tree
Showing 9 changed files with 376 additions and 6 deletions.
18 changes: 18 additions & 0 deletions src/AnonymousResourceCollection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace AgilePixels\ResourceAbilities;

class AnonymousResourceCollection extends ResourceCollection
{
/**
* Create a new anonymous resource collection.
*
* @param mixed $resource
* @param string $collects
* @return void
*/
public function __construct($resource, public $collects)
{
parent::__construct($resource);
}
}
4 changes: 2 additions & 2 deletions src/HasRelationships.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

namespace AgilePixels\ResourceAbilities;

use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use AgilePixels\ResourceAbilities\ResourceCollection;
use Illuminate\Http\Resources\Json\JsonResource;

trait HasRelationships
{
public static function collectionWhenLoaded(string $relationship, JsonResource $jsonResource): AnonymousResourceCollection
public static function collectionWhenLoaded(string $relationship, JsonResource $jsonResource): ResourceCollection
{
return static::collection($jsonResource->whenLoaded($relationship));
}
Expand Down
16 changes: 16 additions & 0 deletions src/JsonResource/ProcessesAbilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use AgilePixels\ResourceAbilities\AbilityResource;
use AgilePixels\ResourceAbilities\Collection;
use AgilePixels\ResourceAbilities\HasAbilities as ModelHasAbilities;
use AgilePixels\ResourceAbilities\AnonymousResourceCollection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Resources\MissingValue;

Expand All @@ -30,4 +31,19 @@ public static function collectionAbilities(Collection | MissingValue $resource,
$resource instanceof Collection ? $resource->getWithAllAbilities() : true,
)->add($ability, $parameters, $serializer);
}

/**
* Create a new anonymous resource collection.
*
* @param mixed $resource
* @return AnonymousResourceCollection
*/
public static function collection($resource): AnonymousResourceCollection
{
return tap(new AnonymousResourceCollection($resource, static::class), function ($collection) {
if (property_exists(static::class, 'preserveKeys')) {
$collection->preserveKeys = (new static([]))->preserveKeys === true;
}
});
}
}
83 changes: 83 additions & 0 deletions src/PaginatedResourceResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

namespace AgilePixels\ResourceAbilities;

use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;

class PaginatedResourceResponse extends ResourceResponse
{
/**
* Create an HTTP response that represents the object. We make sure the returned array from the resolve() method
* isn't wrapped again since we moved the wrapping logic to the resource instead of this response class.
*
* @param Request $request
* @return JsonResponse
*/
public function toResponse($request): JsonResponse
{
return tap(response()->json(
array_merge_recursive(
$this->resource->resolve($request),
$this->paginationInformation($request),
),
$this->calculateStatus()
), function ($response) use ($request) {
$response->original = $this->resource->resource->map(function ($item) {
return is_array($item) ? Arr::get($item, 'resource') : $item->resource;
});

$this->resource->withResponse($request, $response);
});
}

/**
* Add the pagination information to the response.
*
* @param Request $request
* @return array
*/
protected function paginationInformation(Request $request): array
{
$paginated = $this->resource->resource->toArray();

return [
'links' => $this->paginationLinks($paginated),
'meta' => $this->meta($paginated),
];
}

/**
* Get the pagination links for the response.
*
* @param array $paginated
* @return array
*/
protected function paginationLinks(array $paginated): array
{
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 meta data for the response.
*
* @param array $paginated
* @return array
*/
protected function meta(array $paginated): array
{
return Arr::except($paginated, [
'data',
'first_page_url',
'last_page_url',
'prev_page_url',
'next_page_url',
]);
}
}
135 changes: 135 additions & 0 deletions src/ResourceCollection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?php

namespace AgilePixels\ResourceAbilities;

use Illuminate\Container\Container;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Pagination\AbstractPaginator;
use Illuminate\Support\Collection;
use Illuminate\Http\Resources\Json\ResourceCollection as BaseResourceCollection;
use JsonSerializable;

class ResourceCollection extends BaseResourceCollection
{
/**
* Wrap the given data if necessary. This is the same wrapping that Laravel normally does in the
* ResourceResponse class. However, we make sure the wrapping is done in here so that nested resource
* collections are also wrapped instead of only the first JsonResource using the toResponse method.
*
* @param array $data
* @param array $with
* @param array $additional
* @return array
*/
protected function wrapData(array $data, array $with = [], array $additional = []): array
{
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(array $data): bool
{
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(array $data, array $with, array $additional): bool
{
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(): string
{
return static::$wrap;
}

/**
* Resolve the resource to an array.
*
* @param Request|null $request
* @return array
*/
public function resolve($request = null)
{
$data = static::toArray(
$request = $request ?: Container::getInstance()->make('request')
);

if ($data instanceof Arrayable) {
$data = $data->toArray();
} elseif ($data instanceof JsonSerializable) {
$data = $data->jsonSerialize();
}

$data = $this->filter((array) $data);

return $this->wrapData(
$data,
$this->with($request),
$this->additional
);
}

/**
* Create an HTTP response that represents the object.
*
* @param Request $request
* @return JsonResponse
*/
public function toResponse($request): JsonResponse
{
if ($this->resource instanceof AbstractPaginator) {
return $this->preparePaginatedResponse($request);
}

return (new ResourceResponse($this))->toResponse($request);
}

/**
* Create a paginate-aware HTTP response.
*
* @param Request $request
* @return JsonResponse
*/
protected function preparePaginatedResponse($request): JsonResponse
{
if ($this->preserveAllQueryParameters) {
$this->resource->appends($request->query());
} elseif (! is_null($this->queryParameters)) {
$this->resource->appends($this->queryParameters);
}

return (new PaginatedResourceResponse($this))->toResponse($request);
}
}
29 changes: 29 additions & 0 deletions src/ResourceResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace AgilePixels\ResourceAbilities;

use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceResponse as Response;

class ResourceResponse extends Response
{
/**
* Create an HTTP response that represents the object. We make sure the returned array from the resolve() method
* isn't wrapped again since we moved the wrapping logic to the resource instead of this response class.
*
* @param Request $request
* @return JsonResponse
*/
public function toResponse($request): JsonResponse
{
return tap(response()->json(
$this->resource->resolve($request),
$this->calculateStatus()
), function ($response) use ($request) {
$response->original = $this->resource->resource;

$this->resource->withResponse($request, $response);
});
}
}
8 changes: 5 additions & 3 deletions tests/HasRelationshipsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,11 @@ public function toArray($request)
$this->get('/resource')->assertExactJson([
'data' => [
'users' => [
[
'id' => 1,
'name' => 'Test User',
'data' => [
[
'id' => 1,
'name' => 'Test User',
],
],
],
],
Expand Down
Loading

0 comments on commit 4fecfb9

Please sign in to comment.