From 93a3cb429480e84aec5ba74aa733fa02ad57bd81 Mon Sep 17 00:00:00 2001 From: Andrey Kozlov Date: Mon, 13 Feb 2023 14:51:23 +0400 Subject: [PATCH 1/3] Add include traversing methods to ApiResource --- .../Resources/TransformRelationToResource.php | 100 ++++++++++++++++-- 1 file changed, 91 insertions(+), 9 deletions(-) diff --git a/src/Http/Resources/TransformRelationToResource.php b/src/Http/Resources/TransformRelationToResource.php index 54b7ba4..f5ee45b 100644 --- a/src/Http/Resources/TransformRelationToResource.php +++ b/src/Http/Resources/TransformRelationToResource.php @@ -5,18 +5,24 @@ use Cronqvist\Api\Services\Helpers\GuessForModel; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; +use Illuminate\Http\Resources\Json\ResourceCollection; +use Illuminate\Http\Resources\MissingValue; use Illuminate\Support\Str; +use Spatie\QueryBuilder\QueryBuilderRequest; trait TransformRelationToResource { use GuessForModel; + private ApiResource $parentResource; + private string $requestedAs; + /** * Override the mapping of a Model with its Resource * * @var array */ - protected $modelResourceMap = []; + protected array $modelResourceMap = []; /** * Transform all loaded relations with its own related resource class @@ -41,11 +47,13 @@ protected function transformRelations($data) * Transform a relation with its own related resource class * * @param string $relation - * @return mixed + * @return ApiResource|ResourceCollection|mixed */ - protected function transformRelation($relation) + protected function transformRelation(string $relation) { - if(empty($relation)) return $relation; + if(empty($relation)) { + return $relation; + } if($relation instanceof Model) { if($resource = $this->getResourceClassFor($relation)) { @@ -53,6 +61,7 @@ protected function transformRelation($relation) } } else if($relation instanceof Collection) { if($relation->count() && $resource = $this->getResourceClassFor($relation->first())) { + /** @var ApiResource $resource */ return $resource::collection($relation); } } @@ -62,10 +71,10 @@ protected function transformRelation($relation) /** * Get the resource class based on the model, if it exist. * - * @param \Illuminate\Database\Eloquent\Model $model + * @param Model $model * @return string */ - protected function getResourceClassFor(Model $model) + protected function getResourceClassFor(Model $model): ?string { $modelClass = get_class($model); if(isset($this->modelResourceMap[$modelClass])) { @@ -79,12 +88,85 @@ protected function getResourceClassFor(Model $model) * Retrieve a relationship if it has been loaded and map it to its Resource class. * * @param string $relationship - * @return \Cronqvist\Api\Http\Resources\ApiResource|\Illuminate\Http\Resources\MissingValue + * @return ApiResource|ResourceCollection|MissingValue|mixed */ - protected function whenLoadedToResource($relationship) + protected function whenLoadedToResource(string $relationship) { return $this->whenLoaded($relationship, function() use($relationship) { - return $this->transformRelation($this->resource->getRelation($relationship)); + $resource = $this->transformRelation($this->resource->getRelation($relationship)); + if($resource instanceof ApiResource) { + $resource->setParent($this, $relationship); + } else if($resource instanceof ResourceCollection) { + $resource->each(fn(ApiResource $res) => $res->setParent($this, $relationship)); + } + return $resource; + }); + } + + public function setParent(ApiResource $parentResource, string $requestedAs):void { + $this->parentResource = $parentResource; + $this->requestedAs = $requestedAs; + } + + protected function includePath(): array + { + $stack=[]; + $resource = $this; + do { + $stack[] = $resource->requestedAs; + } while($resource = $resource->parentResource); + array_pop($stack); //remove root resource + return array_reverse($stack); + } + + protected function globalIncludes(): \Illuminate\Support\Collection + { + return QueryBuilderRequest::createFrom(request())->includes(); + } + + protected function localIncludes(): array + { + $result = []; + $path = $this->includePath(); + $pathLength = count($path); + foreach ($this->globalIncludes() as $include){ + $include = explode('.',$include); + for ($matches=0; $matches<$pathLength; $matches++) { + $pathPart = $path[$matches]; + if($pathPart !== $include[$matches]){ + break; + } + } + $res = array_slice($include, $matches); + if(count($res) && !in_array($res, $result, true)){ + $result[] = $res; + } + } + return $result; + } + protected function directIncludes(): array + { + return array_unique(array_map(static fn($e)=>$e[0],$this->localIncludes())); + } + + protected function whenIncluded($relationship, $callback) + { + if(in_array($relationship, $this->directIncludes(), true)) { + return $callback(); + } + return new MissingValue; + } + + protected function whenIncludedToResource($relationship) + { + return $this->whenIncluded($relationship, function() use($relationship) { + $resource = $this->transformRelation($this->resource->getRelation($relationship)); + if($resource instanceof ApiResource) { + $resource->setParent($this, $relationship); + } else if($resource instanceof ResourceCollection) { + $resource->each(fn($res) => $res->setParent($this, $relationship)); + } + return $resource; }); } } From 2d608abef0cf57a92fb25a7bd1457fe279544b68 Mon Sep 17 00:00:00 2001 From: Andrey Kozlov Date: Fri, 17 Feb 2023 13:16:14 +0400 Subject: [PATCH 2/3] Fix type check --- src/Http/Resources/TransformRelationToResource.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Http/Resources/TransformRelationToResource.php b/src/Http/Resources/TransformRelationToResource.php index f5ee45b..c6f7f25 100644 --- a/src/Http/Resources/TransformRelationToResource.php +++ b/src/Http/Resources/TransformRelationToResource.php @@ -46,10 +46,10 @@ protected function transformRelations($data) /** * Transform a relation with its own related resource class * - * @param string $relation + * @param Model|Collection $relation * @return ApiResource|ResourceCollection|mixed */ - protected function transformRelation(string $relation) + protected function transformRelation($relation) { if(empty($relation)) { return $relation; From ddbba2dc160650b39a40cda6b2161682720a980d Mon Sep 17 00:00:00 2001 From: Andrey Kozlov Date: Thu, 23 Feb 2023 10:44:43 +0400 Subject: [PATCH 3/3] Allow inclusion of dummy relationships These serve no purpose other than being a placeholder for whenIncluded, allowing us to have simple logic in resource rather than having to define complex relationships in models --- .../Resources/TransformRelationToResource.php | 4 ++-- src/Services/QueryBuilder/ApiIncludes.php | 18 ++++++++++++++++++ .../QueryBuilder/Includes/IncludedDummy.php | 14 ++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 src/Services/QueryBuilder/ApiIncludes.php create mode 100644 src/Services/QueryBuilder/Includes/IncludedDummy.php diff --git a/src/Http/Resources/TransformRelationToResource.php b/src/Http/Resources/TransformRelationToResource.php index c6f7f25..af16874 100644 --- a/src/Http/Resources/TransformRelationToResource.php +++ b/src/Http/Resources/TransformRelationToResource.php @@ -14,8 +14,8 @@ trait TransformRelationToResource { use GuessForModel; - private ApiResource $parentResource; - private string $requestedAs; + private ?ApiResource $parentResource = null; + private ?string $requestedAs = null; /** * Override the mapping of a Model with its Resource diff --git a/src/Services/QueryBuilder/ApiIncludes.php b/src/Services/QueryBuilder/ApiIncludes.php new file mode 100644 index 0000000..a5db66b --- /dev/null +++ b/src/Services/QueryBuilder/ApiIncludes.php @@ -0,0 +1,18 @@ +