json-api-server/src/Handler/Concerns/IncludesData.php

139 lines
4.2 KiB
PHP

<?php
namespace Tobyz\JsonApiServer\Handler\Concerns;
use Closure;
use Psr\Http\Message\ServerRequestInterface as Request;
use function Tobyz\JsonApiServer\evaluate;
use Tobyz\JsonApiServer\Exception\BadRequestException;
use Tobyz\JsonApiServer\ResourceType;
use Tobyz\JsonApiServer\Schema\Relationship;
use function Tobyz\JsonApiServer\run_callbacks;
trait IncludesData
{
private function getInclude(Request $request): array
{
$queryParams = $request->getQueryParams();
if (! empty($queryParams['include'])) {
$include = $this->parseInclude($queryParams['include']);
$this->validateInclude($this->resource, $include);
return $include;
}
return [];
}
private function parseInclude(string $include): array
{
$tree = [];
foreach (explode(',', $include) as $path) {
$keys = explode('.', $path);
$array = &$tree;
foreach ($keys as $key) {
if (! isset($array[$key])) {
$array[$key] = [];
}
$array = &$array[$key];
}
}
return $tree;
}
private function validateInclude(ResourceType $resource, array $include, string $path = '')
{
$fields = $resource->getSchema()->getFields();
foreach ($include as $name => $nested) {
if (! isset($fields[$name])
|| ! $fields[$name] instanceof Relationship
|| ! $fields[$name]->isIncludable()
) {
throw new BadRequestException("Invalid include [{$path}{$name}]", 'include');
}
if ($type = $fields[$name]->getType()) {
$relatedResource = $this->api->getResource($type);
$this->validateInclude($relatedResource, $nested, $name.'.');
} elseif ($nested) {
throw new BadRequestException("Invalid include [{$path}{$name}.*]", 'include');
}
}
}
private function buildRelationshipTrails(ResourceType $resource, array $include): array
{
$fields = $resource->getSchema()->getFields();
$trails = [];
foreach ($include as $name => $nested) {
$relationship = $fields[$name];
if ($relationship->getLoadable()) {
$trails[] = [$relationship];
}
if ($type = $fields[$name]->getType()) {
$relatedResource = $this->api->getResource($type);
$trails = array_merge(
$trails,
array_map(
function ($trail) use ($relationship) {
return array_merge([$relationship], $trail);
},
$this->buildRelationshipTrails($relatedResource, $nested)
)
);
}
}
return $trails;
}
private function loadRelationships(array $models, array $include, Request $request)
{
$adapter = $this->resource->getAdapter();
$fields = $this->resource->getSchema()->getFields();
$trails = $this->buildRelationshipTrails($this->resource, $include);
$loaded = [];
foreach ($trails as $relationships) {
$relationship = end($relationships);
if (($load = $relationship->getLoadable()) instanceof Closure) {
$load($models, $relationships, false, $request);
} else {
$scope = function ($query) use ($request, $relationship) {
run_callbacks($relationship->getScopes(), [$query, $request]);
};
$adapter->load($models, $relationships, $scope);
}
$loaded[] = $relationships[0];
}
foreach ($fields as $name => $field) {
if (! $field instanceof Relationship || ! evaluate($field->getLinkage(), [$request]) || ! $field->getLoadable() || in_array($field, $loaded)) {
continue;
}
if (($load = $field->getLoadable()) instanceof Closure) {
$load($models, true);
} else {
$adapter->loadIds($models, $field);
}
}
}
}