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,21 @@
The MIT License (MIT)
Copyright (c) 2014-2016 Toby Zerner <toby.zerner@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

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