first commit

This commit is contained in:
2025-06-17 11:53:18 +02:00
commit 9f0f7ba12b
8804 changed files with 1369176 additions and 0 deletions

View File

@ -0,0 +1,106 @@
<?php
/*
* This file is part of JSON-API.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tobscure\JsonApi;
use LogicException;
abstract class AbstractSerializer implements SerializerInterface
{
/**
* The type.
*
* @var string
*/
protected $type;
/**
* {@inheritdoc}
*/
public function getType($model)
{
return $this->type;
}
/**
* {@inheritdoc}
*/
public function getId($model)
{
return $model->id;
}
/**
* {@inheritdoc}
*/
public function getAttributes($model, array $fields = null)
{
return [];
}
/**
* {@inheritdoc}
*/
public function getLinks($model)
{
return [];
}
/**
* {@inheritdoc}
*/
public function getMeta($model)
{
return [];
}
/**
* {@inheritdoc}
*
* @throws \LogicException
*/
public function getRelationship($model, $name)
{
$method = $this->getRelationshipMethodName($name);
if (method_exists($this, $method)) {
$relationship = $this->$method($model);
if ($relationship !== null && ! ($relationship instanceof Relationship)) {
throw new LogicException('Relationship method must return null or an instance of Tobscure\JsonApi\Relationship');
}
return $relationship;
}
}
/**
* Get the serializer method name for the given relationship.
*
* snake_case and kebab-case are converted into camelCase.
*
* @param string $name
*
* @return string
*/
private function getRelationshipMethodName($name)
{
if (stripos($name, '-')) {
$name = lcfirst(implode('', array_map('ucfirst', explode('-', $name))));
}
if (stripos($name, '_')) {
$name = lcfirst(implode('', array_map('ucfirst', explode('_', $name))));
}
return $name;
}
}

View File

@ -0,0 +1,126 @@
<?php
/*
* This file is part of JSON-API.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tobscure\JsonApi;
class Collection implements ElementInterface
{
/**
* @var array
*/
protected $resources = [];
/**
* Create a new collection instance.
*
* @param mixed $data
* @param \Tobscure\JsonApi\SerializerInterface $serializer
*/
public function __construct($data, SerializerInterface $serializer)
{
$this->resources = $this->buildResources($data, $serializer);
}
/**
* Convert an array of raw data to Resource objects.
*
* @param mixed $data
* @param SerializerInterface $serializer
*
* @return \Tobscure\JsonApi\Resource[]
*/
protected function buildResources($data, SerializerInterface $serializer)
{
$resources = [];
foreach ($data as $resource) {
if (! ($resource instanceof Resource)) {
$resource = new Resource($resource, $serializer);
}
$resources[] = $resource;
}
return $resources;
}
/**
* {@inheritdoc}
*/
public function getResources()
{
return $this->resources;
}
/**
* Set the resources array.
*
* @param array $resources
*
* @return void
*/
public function setResources($resources)
{
$this->resources = $resources;
}
/**
* Request a relationship to be included for all resources.
*
* @param string|array $relationships
*
* @return $this
*/
public function with($relationships)
{
foreach ($this->resources as $resource) {
$resource->with($relationships);
}
return $this;
}
/**
* Request a restricted set of fields.
*
* @param array|null $fields
*
* @return $this
*/
public function fields($fields)
{
foreach ($this->resources as $resource) {
$resource->fields($fields);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function toArray()
{
return array_map(function (Resource $resource) {
return $resource->toArray();
}, $this->resources);
}
/**
* {@inheritdoc}
*/
public function toIdentifier()
{
return array_map(function (Resource $resource) {
return $resource->toIdentifier();
}, $this->resources);
}
}

View File

@ -0,0 +1,234 @@
<?php
/*
* This file is part of JSON-API.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tobscure\JsonApi;
use JsonSerializable;
class Document implements JsonSerializable
{
use LinksTrait;
use MetaTrait;
const MEDIA_TYPE = 'application/vnd.api+json';
/**
* The included array.
*
* @var array
*/
protected $included = [];
/**
* The errors array.
*
* @var array
*/
protected $errors;
/**
* The jsonapi array.
*
* @var array
*/
protected $jsonapi;
/**
* The data object.
*
* @var ElementInterface
*/
protected $data;
/**
* @param ElementInterface $data
*/
public function __construct(ElementInterface $data = null)
{
$this->data = $data;
}
/**
* Get included resources.
*
* @param \Tobscure\JsonApi\ElementInterface $element
* @param bool $includeParent
*
* @return \Tobscure\JsonApi\Resource[]
*/
protected function getIncluded(ElementInterface $element, $includeParent = false)
{
$included = [];
foreach ($element->getResources() as $resource) {
if ($resource->isIdentifier()) {
continue;
}
if ($includeParent) {
$included = $this->mergeResource($included, $resource);
} else {
$type = $resource->getType();
$id = $resource->getId();
}
foreach ($resource->getUnfilteredRelationships() as $relationship) {
$includedElement = $relationship->getData();
if (! $includedElement instanceof ElementInterface) {
continue;
}
foreach ($this->getIncluded($includedElement, true) as $child) {
// If this resource is the same as the top-level "data"
// resource, then we don't want it to show up again in the
// "included" array.
if (! $includeParent && $child->getType() === $type && $child->getId() === $id) {
continue;
}
$included = $this->mergeResource($included, $child);
}
}
}
$flattened = [];
array_walk_recursive($included, function ($a) use (&$flattened) {
$flattened[] = $a;
});
return $flattened;
}
/**
* @param \Tobscure\JsonApi\Resource[] $resources
* @param \Tobscure\JsonApi\Resource $newResource
*
* @return \Tobscure\JsonApi\Resource[]
*/
protected function mergeResource(array $resources, Resource $newResource)
{
$type = $newResource->getType();
$id = $newResource->getId();
if (isset($resources[$type][$id])) {
$resources[$type][$id]->merge($newResource);
} else {
$resources[$type][$id] = $newResource;
}
return $resources;
}
/**
* Set the data object.
*
* @param \Tobscure\JsonApi\ElementInterface $element
*
* @return $this
*/
public function setData(ElementInterface $element)
{
$this->data = $element;
return $this;
}
/**
* Set the errors array.
*
* @param array $errors
*
* @return $this
*/
public function setErrors($errors)
{
$this->errors = $errors;
return $this;
}
/**
* Set the jsonapi array.
*
* @param array $jsonapi
*
* @return $this
*/
public function setJsonapi($jsonapi)
{
$this->jsonapi = $jsonapi;
return $this;
}
/**
* Map everything to arrays.
*
* @return array
*/
public function toArray()
{
$document = [];
if (! empty($this->links)) {
$document['links'] = $this->links;
}
if (! empty($this->data)) {
$document['data'] = $this->data->toArray();
$resources = $this->getIncluded($this->data);
if (count($resources)) {
$document['included'] = array_map(function (Resource $resource) {
return $resource->toArray();
}, $resources);
}
}
if (! empty($this->meta)) {
$document['meta'] = $this->meta;
}
if (! empty($this->errors)) {
$document['errors'] = $this->errors;
}
if (! empty($this->jsonapi)) {
$document['jsonapi'] = $this->jsonapi;
}
return $document;
}
/**
* Map to string.
*
* @return string
*/
public function __toString()
{
return json_encode($this->toArray());
}
/**
* Serialize for JSON usage.
*
* @return array
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->toArray();
}
}

View File

@ -0,0 +1,54 @@
<?php
/*
* This file is part of JSON-API.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tobscure\JsonApi;
interface ElementInterface
{
/**
* Get the resources array.
*
* @return array
*/
public function getResources();
/**
* Map to a "resource object" array.
*
* @return array
*/
public function toArray();
/**
* Map to a "resource object identifier" array.
*
* @return array
*/
public function toIdentifier();
/**
* Request a relationship to be included.
*
* @param string|array $relationships
*
* @return $this
*/
public function with($relationships);
/**
* Request a restricted set of fields.
*
* @param array|null $fields
*
* @return $this
*/
public function fields($fields);
}

View File

@ -0,0 +1,58 @@
<?php
/*
* This file is part of JSON-API.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tobscure\JsonApi;
use Exception;
use RuntimeException;
use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface;
class ErrorHandler
{
/**
* Stores the valid handlers.
*
* @var \Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface[]
*/
private $handlers = [];
/**
* Handle the exception provided.
*
* @param Exception $e
*
* @throws RuntimeException
*
* @return \Tobscure\JsonApi\Exception\Handler\ResponseBag
*/
public function handle(Exception $e)
{
foreach ($this->handlers as $handler) {
if ($handler->manages($e)) {
return $handler->handle($e);
}
}
throw new RuntimeException('Exception handler for '.get_class($e).' not found.');
}
/**
* Register a new exception handler.
*
* @param \Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface $handler
*
* @return void
*/
public function registerHandler(ExceptionHandlerInterface $handler)
{
$this->handlers[] = $handler;
}
}

View File

@ -0,0 +1,36 @@
<?php
/*
* This file is part of JSON-API.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tobscure\JsonApi\Exception\Handler;
use Exception;
interface ExceptionHandlerInterface
{
/**
* If the exception handler is able to format a response for the provided exception,
* then the implementation should return true.
*
* @param \Exception $e
*
* @return bool
*/
public function manages(Exception $e);
/**
* Handle the provided exception.
*
* @param \Exception $e
*
* @return \Tobscure\JsonApi\Exception\Handler\ResponseBag
*/
public function handle(Exception $e);
}

View File

@ -0,0 +1,66 @@
<?php
/*
* This file is part of JSON-API.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tobscure\JsonApi\Exception\Handler;
use Exception;
class FallbackExceptionHandler implements ExceptionHandlerInterface
{
/**
* @var bool
*/
private $debug;
/**
* @param bool $debug
*/
public function __construct($debug)
{
$this->debug = $debug;
}
/**
* {@inheritdoc}
*/
public function manages(Exception $e)
{
return true;
}
/**
* {@inheritdoc}
*/
public function handle(Exception $e)
{
$status = 500;
$error = $this->constructError($e, $status);
return new ResponseBag($status, [$error]);
}
/**
* @param \Exception $e
* @param $status
*
* @return array
*/
private function constructError(Exception $e, $status)
{
$error = ['code' => $status, 'title' => 'Internal server error'];
if ($this->debug) {
$error['detail'] = (string) $e;
}
return $error;
}
}

View File

@ -0,0 +1,47 @@
<?php
/*
* This file is part of JSON-API.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tobscure\JsonApi\Exception\Handler;
use Exception;
use Tobscure\JsonApi\Exception\InvalidParameterException;
class InvalidParameterExceptionHandler implements ExceptionHandlerInterface
{
/**
* {@inheritdoc}
*/
public function manages(Exception $e)
{
return $e instanceof InvalidParameterException;
}
/**
* {@inheritdoc}
*/
public function handle(Exception $e)
{
$status = 400;
$error = [];
$code = $e->getCode();
if ($code) {
$error['code'] = $code;
}
$invalidParameter = $e->getInvalidParameter();
if ($invalidParameter) {
$error['source'] = ['parameter' => $invalidParameter];
}
return new ResponseBag($status, [$error]);
}
}

View File

@ -0,0 +1,47 @@
<?php
/*
* This file is part of JSON-API.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tobscure\JsonApi\Exception\Handler;
/**
* DTO to manage JSON error response handling.
*/
class ResponseBag
{
private $status;
private $errors;
/**
* @param int $status
* @param array $errors
*/
public function __construct($status, array $errors)
{
$this->status = $status;
$this->errors = $errors;
}
/**
* @return array
*/
public function getErrors()
{
return $this->errors;
}
/**
* @return int
*/
public function getStatus()
{
return $this->status;
}
}

View File

@ -0,0 +1,42 @@
<?php
/*
* This file is part of JSON-API.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tobscure\JsonApi\Exception;
use Exception;
class InvalidParameterException extends Exception
{
/**
* @var string The parameter that caused this exception.
*/
private $invalidParameter;
/**
* {@inheritdoc}
*
* @param string $invalidParameter The parameter that caused this exception.
*/
public function __construct($message = '', $code = 0, $previous = null, $invalidParameter = '')
{
parent::__construct($message, $code, $previous);
$this->invalidParameter = $invalidParameter;
}
/**
* @return string The parameter that caused this exception.
*/
public function getInvalidParameter()
{
return $this->invalidParameter;
}
}

View File

@ -0,0 +1,135 @@
<?php
/*
* This file is part of JSON-API.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tobscure\JsonApi;
trait LinksTrait
{
/**
* The links array.
*
* @var array
*/
protected $links;
/**
* Get the links.
*
* @return array
*/
public function getLinks()
{
return $this->links;
}
/**
* Set the links.
*
* @param array $links
*
* @return $this
*/
public function setLinks(array $links)
{
$this->links = $links;
return $this;
}
/**
* Add a link.
*
* @param string $key
* @param string $value
*
* @return $this
*/
public function addLink($key, $value)
{
$this->links[$key] = $value;
return $this;
}
/**
* Add pagination links (first, prev, next, and last).
*
* @param string $url The base URL for pagination links.
* @param array $queryParams The query params provided in the request.
* @param int $offset The current offset.
* @param int $limit The current limit.
* @param int|null $total The total number of results, or null if unknown.
*
* @return void
*/
public function addPaginationLinks($url, array $queryParams, $offset, $limit, $total = null)
{
if (isset($queryParams['page']['number'])) {
$offset = floor($offset / $limit) * $limit;
}
$this->addPaginationLink('first', $url, $queryParams, 0, $limit);
if ($offset > 0) {
$this->addPaginationLink('prev', $url, $queryParams, max(0, $offset - $limit), $limit);
}
if ($total === null || $offset + $limit < $total) {
$this->addPaginationLink('next', $url, $queryParams, $offset + $limit, $limit);
}
if ($total) {
$this->addPaginationLink('last', $url, $queryParams, floor(($total - 1) / $limit) * $limit, $limit);
}
}
/**
* Add a pagination link.
*
* @param string $name The name of the link.
* @param string $url The base URL for pagination links.
* @param array $queryParams The query params provided in the request.
* @param int $offset The offset to link to.
* @param int $limit The current limit.
*
* @return void
*/
protected function addPaginationLink($name, $url, array $queryParams, $offset, $limit)
{
if (! isset($queryParams['page']) || ! is_array($queryParams['page'])) {
$queryParams['page'] = [];
}
$page = &$queryParams['page'];
if (isset($page['number'])) {
$page['number'] = floor($offset / $limit) + 1;
if ($page['number'] <= 1) {
unset($page['number']);
}
} else {
$page['offset'] = $offset;
if ($page['offset'] <= 0) {
unset($page['offset']);
}
}
if (isset($page['limit'])) {
$page['limit'] = $limit;
}
$queryString = http_build_query($queryParams);
$this->addLink($name, $url.($queryString ? '?'.$queryString : ''));
}
}

View File

@ -0,0 +1,61 @@
<?php
/*
* This file is part of JSON-API.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tobscure\JsonApi;
trait MetaTrait
{
/**
* The meta data array.
*
* @var array
*/
protected $meta;
/**
* Get the meta.
*
* @return array
*/
public function getMeta()
{
return $this->meta;
}
/**
* Set the meta data array.
*
* @param array $meta
*
* @return $this
*/
public function setMeta(array $meta)
{
$this->meta = $meta;
return $this;
}
/**
* Add meta data.
*
* @param string $key
* @param string $value
*
* @return $this
*/
public function addMeta($key, $value)
{
$this->meta[$key] = $value;
return $this;
}
}

View File

@ -0,0 +1,220 @@
<?php
/*
* This file is part of JSON-API.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tobscure\JsonApi;
use Tobscure\JsonApi\Exception\InvalidParameterException;
class Parameters
{
/**
* @var array
*/
protected $input;
/**
* @param array $input
*/
public function __construct(array $input)
{
$this->input = $input;
}
/**
* Get the includes.
*
* @param array $available
*
* @throws \Tobscure\JsonApi\Exception\InvalidParameterException
*
* @return array
*/
public function getInclude(array $available = [])
{
if ($include = $this->getInput('include')) {
$relationships = explode(',', $include);
$invalid = array_diff($relationships, $available);
if (count($invalid)) {
throw new InvalidParameterException(
'Invalid includes ['.implode(',', $invalid).']',
1,
null,
'include'
);
}
return $relationships;
}
return [];
}
/**
* Get number of offset.
*
* @param int|null $perPage
*
* @throws \Tobscure\JsonApi\Exception\InvalidParameterException
*
* @return int
*/
public function getOffset($perPage = null)
{
if ($perPage && ($offset = $this->getOffsetFromNumber($perPage))) {
return $offset;
}
$offset = (int) $this->getPage('offset');
if ($offset < 0) {
throw new InvalidParameterException('page[offset] must be >=0', 2, null, 'page[offset]');
}
return $offset;
}
/**
* Calculate the offset based on the page[number] parameter.
*
* @param int $perPage
*
* @throws \Tobscure\JsonApi\Exception\InvalidParameterException
*
* @return int
*/
protected function getOffsetFromNumber($perPage)
{
$page = (int) $this->getPage('number');
if ($page <= 1) {
return 0;
}
return ($page - 1) * $perPage;
}
/**
* Get the limit.
*
* @param int|null $max
*
* @return int|null
*/
public function getLimit($max = null)
{
$limit = $this->getPage('limit') ?: $this->getPage('size') ?: null;
if ($limit && $max) {
$limit = min($max, $limit);
}
return $limit;
}
/**
* Get the sort.
*
* @param array $available
*
* @throws \Tobscure\JsonApi\Exception\InvalidParameterException
*
* @return array
*/
public function getSort(array $available = [])
{
$sort = [];
if ($input = $this->getInput('sort')) {
$fields = explode(',', $input);
foreach ($fields as $field) {
if (substr($field, 0, 1) === '-') {
$field = substr($field, 1);
$order = 'desc';
} else {
$order = 'asc';
}
$sort[$field] = $order;
}
$invalid = array_diff(array_keys($sort), $available);
if (count($invalid)) {
throw new InvalidParameterException(
'Invalid sort fields ['.implode(',', $invalid).']',
3,
null,
'sort'
);
}
}
return $sort;
}
/**
* Get the fields requested for inclusion.
*
* @return array
*/
public function getFields()
{
$fields = $this->getInput('fields');
if (! is_array($fields)) {
return [];
}
return array_map(function ($fields) {
return explode(',', $fields);
}, $fields);
}
/**
* Get a filter item.
*
* @return mixed
*/
public function getFilter()
{
return $this->getInput('filter');
}
/**
* Get an input item.
*
* @param string $key
* @param null $default
*
* @return mixed
*/
protected function getInput($key, $default = null)
{
return isset($this->input[$key]) ? $this->input[$key] : $default;
}
/**
* Get the page.
*
* @param string $key
*
* @return string
*/
protected function getPage($key)
{
$page = $this->getInput('page');
return isset($page[$key]) ? $page[$key] : '';
}
}

View File

@ -0,0 +1,83 @@
<?php
/*
* This file is part of JSON-API.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tobscure\JsonApi;
class Relationship
{
use LinksTrait;
use MetaTrait;
/**
* The data object.
*
* @var \Tobscure\JsonApi\ElementInterface|null
*/
protected $data;
/**
* Create a new relationship.
*
* @param \Tobscure\JsonApi\ElementInterface|null $data
*/
public function __construct(ElementInterface $data = null)
{
$this->data = $data;
}
/**
* Get the data object.
*
* @return \Tobscure\JsonApi\ElementInterface|null
*/
public function getData()
{
return $this->data;
}
/**
* Set the data object.
*
* @param \Tobscure\JsonApi\ElementInterface|null $data
*
* @return $this
*/
public function setData($data)
{
$this->data = $data;
return $this;
}
/**
* Map everything to an array.
*
* @return array
*/
public function toArray()
{
$array = [];
if (! empty($this->data)) {
$array['data'] = $this->data->toIdentifier();
}
if (! empty($this->meta)) {
$array['meta'] = $this->meta;
}
if (! empty($this->links)) {
$array['links'] = $this->links;
}
return $array;
}
}

View File

@ -0,0 +1,406 @@
<?php
/*
* This file is part of JSON-API.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tobscure\JsonApi;
class Resource implements ElementInterface
{
use LinksTrait;
use MetaTrait;
/**
* @var mixed
*/
protected $data;
/**
* @var \Tobscure\JsonApi\SerializerInterface
*/
protected $serializer;
/**
* A list of relationships to include.
*
* @var array
*/
protected $includes = [];
/**
* A list of fields to restrict to.
*
* @var array|null
*/
protected $fields;
/**
* An array of Resources that should be merged into this one.
*
* @var \Tobscure\JsonApi\Resource[]
*/
protected $merged = [];
/**
* @var \Tobscure\JsonApi\Relationship[]
*/
private $relationships;
/**
* @param mixed $data
* @param \Tobscure\JsonApi\SerializerInterface $serializer
*/
public function __construct($data, SerializerInterface $serializer)
{
$this->data = $data;
$this->serializer = $serializer;
}
/**
* {@inheritdoc}
*/
public function getResources()
{
return [$this];
}
/**
* {@inheritdoc}
*/
public function toArray()
{
$array = $this->toIdentifier();
if (! $this->isIdentifier()) {
$attributes = $this->getAttributes();
if ($attributes) {
$array['attributes'] = $attributes;
}
}
$relationships = $this->getRelationshipsAsArray();
if (count($relationships)) {
$array['relationships'] = $relationships;
}
$links = [];
if (! empty($this->links)) {
$links = $this->links;
}
$serializerLinks = $this->serializer->getLinks($this->data);
if (! empty($serializerLinks)) {
$links = array_merge($serializerLinks, $links);
}
if (! empty($links)) {
$array['links'] = $links;
}
$meta = [];
if (! empty($this->meta)) {
$meta = $this->meta;
}
$serializerMeta = $this->serializer->getMeta($this->data);
if (! empty($serializerMeta)) {
$meta = array_merge($serializerMeta, $meta);
}
if (! empty($meta)) {
$array['meta'] = $meta;
}
return $array;
}
/**
* Check whether or not this resource is an identifier (i.e. does it have
* any data attached?).
*
* @return bool
*/
public function isIdentifier()
{
return ! is_object($this->data) && ! is_array($this->data);
}
/**
* {@inheritdoc}
*/
public function toIdentifier()
{
if (! $this->data) {
return;
}
$array = [
'type' => $this->getType(),
'id' => $this->getId()
];
if (! empty($this->meta)) {
$array['meta'] = $this->meta;
}
return $array;
}
/**
* Get the resource type.
*
* @return string
*/
public function getType()
{
return $this->serializer->getType($this->data);
}
/**
* Get the resource ID.
*
* @return string
*/
public function getId()
{
if (! is_object($this->data) && ! is_array($this->data)) {
return (string) $this->data;
}
return (string) $this->serializer->getId($this->data);
}
/**
* Get the resource attributes.
*
* @return array
*/
public function getAttributes()
{
$attributes = (array) $this->serializer->getAttributes($this->data, $this->getOwnFields());
$attributes = $this->filterFields($attributes);
$attributes = $this->mergeAttributes($attributes);
return $attributes;
}
/**
* Get the requested fields for this resource type.
*
* @return array|null
*/
protected function getOwnFields()
{
$type = $this->getType();
if (isset($this->fields[$type])) {
return $this->fields[$type];
}
}
/**
* Filter the given fields array (attributes or relationships) according
* to the requested fieldset.
*
* @param array $fields
*
* @return array
*/
protected function filterFields(array $fields)
{
if ($requested = $this->getOwnFields()) {
$fields = array_intersect_key($fields, array_flip($requested));
}
return $fields;
}
/**
* Merge the attributes of merged resources into an array of attributes.
*
* @param array $attributes
*
* @return array
*/
protected function mergeAttributes(array $attributes)
{
foreach ($this->merged as $resource) {
$attributes = array_replace_recursive($attributes, $resource->getAttributes());
}
return $attributes;
}
/**
* Get the resource relationships.
*
* @return \Tobscure\JsonApi\Relationship[]
*/
public function getRelationships()
{
$relationships = $this->buildRelationships();
return $this->filterFields($relationships);
}
/**
* Get the resource relationships without considering requested ones.
*
* @return \Tobscure\JsonApi\Relationship[]
*/
public function getUnfilteredRelationships()
{
return $this->buildRelationships();
}
/**
* Get the resource relationships as an array.
*
* @return array
*/
public function getRelationshipsAsArray()
{
$relationships = $this->getRelationships();
$relationships = $this->convertRelationshipsToArray($relationships);
return $this->mergeRelationships($relationships);
}
/**
* Get an array of built relationships.
*
* @return \Tobscure\JsonApi\Relationship[]
*/
protected function buildRelationships()
{
if (isset($this->relationships)) {
return $this->relationships;
}
$paths = Util::parseRelationshipPaths($this->includes);
$relationships = [];
foreach ($paths as $name => $nested) {
$relationship = $this->serializer->getRelationship($this->data, $name);
if ($relationship) {
$relationshipData = $relationship->getData();
if ($relationshipData instanceof ElementInterface) {
$relationshipData->with($nested)->fields($this->fields);
}
$relationships[$name] = $relationship;
}
}
return $this->relationships = $relationships;
}
/**
* Merge the relationships of merged resources into an array of
* relationships.
*
* @param array $relationships
*
* @return array
*/
protected function mergeRelationships(array $relationships)
{
foreach ($this->merged as $resource) {
$relationships = array_replace_recursive($relationships, $resource->getRelationshipsAsArray());
}
return $relationships;
}
/**
* Convert the given array of Relationship objects into an array.
*
* @param \Tobscure\JsonApi\Relationship[] $relationships
*
* @return array
*/
protected function convertRelationshipsToArray(array $relationships)
{
return array_map(function (Relationship $relationship) {
return $relationship->toArray();
}, $relationships);
}
/**
* Merge a resource into this one.
*
* @param \Tobscure\JsonApi\Resource $resource
*
* @return void
*/
public function merge(Resource $resource)
{
$this->merged[] = $resource;
}
/**
* {@inheritdoc}
*/
public function with($relationships)
{
$this->includes = array_unique(array_merge($this->includes, (array) $relationships));
$this->relationships = null;
return $this;
}
/**
* {@inheritdoc}
*/
public function fields($fields)
{
$this->fields = $fields;
return $this;
}
/**
* @return mixed
*/
public function getData()
{
return $this->data;
}
/**
* @param mixed $data
*
* @return void
*/
public function setData($data)
{
$this->data = $data;
}
/**
* @return \Tobscure\JsonApi\SerializerInterface
*/
public function getSerializer()
{
return $this->serializer;
}
/**
* @param \Tobscure\JsonApi\SerializerInterface $serializer
*
* @return void
*/
public function setSerializer(SerializerInterface $serializer)
{
$this->serializer = $serializer;
}
}

View File

@ -0,0 +1,71 @@
<?php
/*
* This file is part of JSON-API.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tobscure\JsonApi;
interface SerializerInterface
{
/**
* Get the type.
*
* @param mixed $model
*
* @return string
*/
public function getType($model);
/**
* Get the id.
*
* @param mixed $model
*
* @return string
*/
public function getId($model);
/**
* Get the attributes array.
*
* @param mixed $model
* @param array|null $fields
*
* @return array
*/
public function getAttributes($model, array $fields = null);
/**
* Get the links array.
*
* @param mixed $model
*
* @return array
*/
public function getLinks($model);
/**
* Get the meta.
*
* @param mixed $model
*
* @return array
*/
public function getMeta($model);
/**
* Get a relationship.
*
* @param mixed $model
* @param string $name
*
* @return \Tobscure\JsonApi\Relationship|null
*/
public function getRelationship($model, $name);
}

View File

@ -0,0 +1,50 @@
<?php
/*
* This file is part of JSON-API.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tobscure\JsonApi;
class Util
{
/**
* Parse relationship paths.
*
* Given a flat array of relationship paths like:
*
* ['user', 'user.employer', 'user.employer.country', 'comments']
*
* create a nested array of relationship paths one-level deep that can
* be passed on to other serializers:
*
* ['user' => ['employer', 'employer.country'], 'comments' => []]
*
* @param array $paths
*
* @return array
*/
public static function parseRelationshipPaths(array $paths)
{
$tree = [];
foreach ($paths as $path) {
list($primary, $nested) = array_pad(explode('.', $path, 2), 2, null);
if (! isset($tree[$primary])) {
$tree[$primary] = [];
}
if ($nested) {
$tree[$primary][] = $nested;
}
}
return $tree;
}
}