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) ]); } }