json-api-server/src/Serializer.php

270 lines
8.4 KiB
PHP

<?php
namespace Tobyz\JsonApiServer;
use DateTime;
use DateTimeInterface;
use JsonApiPhp\JsonApi;
use Psr\Http\Message\ServerRequestInterface as Request;
use Tobyz\JsonApiServer\Adapter\AdapterInterface;
class Serializer
{
protected $api;
protected $request;
protected $map = [];
protected $primary = [];
public function __construct(Api $api, Request $request)
{
$this->api = $api;
$this->request = $request;
}
public function add(ResourceType $resource, $model, array $include)
{
$data = $this->addToMap($resource, $model, $include);
$this->primary[] = $data['type'].':'.$data['id'];
}
private function addToMap(ResourceType $resource, $model, array $include)
{
$adapter = $resource->getAdapter();
$schema = $resource->getSchema();
$data = [
'type' => $type = $resource->getType(),
'id' => $adapter->getId($model),
'fields' => [],
'links' => [],
'meta' => []
];
$resourceUrl = $this->api->getBaseUrl().'/'.$data['type'].'/'.$data['id'];
$fields = $schema->fields;
$queryParams = $this->request->getQueryParams();
if (isset($queryParams['fields'][$type])) {
$fields = array_intersect_key($fields, array_flip(explode(',', $queryParams['fields'][$type])));
}
ksort($fields);
$key = $data['type'].':'.$data['id'];
foreach ($fields as $name => $field) {
if (isset($this->map[$key]['fields'][$name])) {
continue;
}
if (! ($field->isVisible)($this->request, $model)) {
continue;
}
if ($field instanceof Schema\Attribute) {
$value = $this->attribute($field, $model, $adapter);
} elseif ($field instanceof Schema\Relationship) {
$isIncluded = isset($include[$name]);
$isLinkage = ($field->linkage)($this->request);
if (! $isIncluded && ! $isLinkage) {
$value = $this->emptyRelationship($field, $resourceUrl);
} elseif ($field instanceof Schema\HasOne) {
$value = $this->toOne($field, $model, $adapter, $isIncluded, $isLinkage, $include[$name] ?? [], $resourceUrl);
} elseif ($field instanceof Schema\HasMany) {
$value = $this->toMany($field, $model, $adapter, $isIncluded, $isLinkage, $include[$name] ?? [], $resourceUrl);
}
}
$data['fields'][$name] = $value;
}
$data['links']['self'] = new JsonApi\Link\SelfLink($resourceUrl);
ksort($schema->meta);
foreach ($schema->meta as $name => $meta) {
$data['meta'][$name] = new JsonApi\Meta($meta->name, ($meta->value)($this->request, $model));
}
$this->merge($data);
return $data;
}
private function attribute(Schema\Attribute $field, $model, AdapterInterface $adapter): JsonApi\Attribute
{
if ($field->getter) {
$value = ($field->getter)($this->request, $model);
} else {
$value = $adapter->getAttribute($model, $field);
}
if ($value instanceof DateTimeInterface) {
$value = $value->format(DateTime::RFC3339);
}
return new JsonApi\Attribute($field->name, $value);
}
private function toOne(Schema\HasOne $field, $model, AdapterInterface $adapter, bool $isIncluded, bool $isLinkage, array $include, string $resourceUrl)
{
$links = $this->getRelationshipLinks($field, $resourceUrl);
$value = $isIncluded ? ($field->getter ? ($field->getter)($this->request, $model) : $adapter->getHasOne($model, $field)) : ($isLinkage && $field->loadable ? $adapter->getHasOneId($model, $field) : null);
if (! $value) {
return new JsonApi\ToNull(
$field->name,
...$links
);
}
if ($isIncluded) {
$identifier = $this->addRelated($field, $value, $include);
} else {
$identifier = $this->relatedResourceIdentifier($field, $value);
}
return new JsonApi\ToOne(
$field->name,
$identifier,
...$links
);
}
private function toMany(Schema\HasMany $field, $model, AdapterInterface $adapter, bool $isIncluded, bool $isLinkage, array $include, string $resourceUrl)
{
if ($field->getter) {
$value = ($field->getter)($this->request, $model);
} else {
$value = ($isLinkage || $isIncluded) ? $adapter->getHasMany($model, $field) : null;
}
$identifiers = [];
if ($isIncluded) {
foreach ($value as $relatedModel) {
$identifiers[] = $this->addRelated($field, $relatedModel, $include);
}
} else {
foreach ($value as $relatedModel) {
$identifiers[] = $this->relatedResourceIdentifier($field, $relatedModel);
}
}
return new JsonApi\ToMany(
$field->name,
new JsonApi\ResourceIdentifierCollection(...$identifiers),
...$this->getRelationshipLinks($field, $resourceUrl)
);
}
private function emptyRelationship(Schema\Relationship $field, string $resourceUrl): JsonApi\EmptyRelationship
{
return new JsonApi\EmptyRelationship(
$field->name,
...$this->getRelationshipLinks($field, $resourceUrl)
);
}
private function getRelationshipLinks(Schema\Relationship $field, string $resourceUrl): array
{
if (! $field->hasLinks) {
return [];
}
return [
new JsonApi\Link\SelfLink($resourceUrl.'/relationships/'.$field->name),
new JsonApi\Link\RelatedLink($resourceUrl.'/'.$field->name)
];
}
private function addRelated(Schema\Relationship $field, $model, array $include): JsonApi\ResourceIdentifier
{
$relatedResource = $field->resource ? $this->api->getResource($field->resource) : $this->resourceForModel($model);
return $this->resourceIdentifier(
$this->addToMap($relatedResource, $model, $include)
);
}
private function resourceForModel($model)
{
foreach ($this->api->getResources() as $resource) {
if ($resource->getAdapter()->handles($model)) {
return $resource;
}
}
throw new \RuntimeException('No resource defined to handle model of type '.get_class($model));
}
private function merge($data): void
{
$key = $data['type'].':'.$data['id'];
if (isset($this->map[$key])) {
$this->map[$key]['fields'] = array_merge($this->map[$key]['fields'], $data['fields']);
$this->map[$key]['links'] = array_merge($this->map[$key]['links'], $data['links']);
$this->map[$key]['meta'] = array_merge($this->map[$key]['meta'], $data['meta']);
} else {
$this->map[$key] = $data;
}
}
public function primary(): array
{
$primary = array_values(array_intersect_key($this->map, array_flip($this->primary)));
return $this->resourceObjects($primary);
}
public function included(): array
{
$included = array_values(array_diff_key($this->map, array_flip($this->primary)));
return $this->resourceObjects($included);
}
private function resourceObjects(array $items): array
{
return array_map(function ($data) {
return $this->resourceObject($data);
}, $items);
}
private function resourceObject(array $data): JsonApi\ResourceObject
{
return new JsonApi\ResourceObject(
$data['type'],
$data['id'],
...array_values($data['fields']),
...array_values($data['links']),
...array_values($data['meta'])
);
}
private function resourceIdentifier(array $data): JsonApi\ResourceIdentifier
{
return new JsonApi\ResourceIdentifier(
$data['type'],
$data['id']
);
}
private function relatedResourceIdentifier(Schema\Relationship $field, $model)
{
$relatedResource = $this->api->getResource($field->resource);
return $this->resourceIdentifier([
'type' => $field->resource,
'id' => $relatedResource->getAdapter()->getId($model)
]);
}
}