├── LICENSE ├── composer.json └── src ├── AbstractSerializer.php ├── Collection.php ├── Document.php ├── ElementInterface.php ├── ErrorHandler.php ├── Exception ├── Handler │ ├── ExceptionHandlerInterface.php │ ├── FallbackExceptionHandler.php │ ├── InvalidParameterExceptionHandler.php │ └── ResponseBag.php └── InvalidParameterException.php ├── LinksTrait.php ├── MetaTrait.php ├── Parameters.php ├── Relationship.php ├── Resource.php ├── SerializerInterface.php └── Util.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2016 Toby Zerner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tobscure/json-api", 3 | "description": "JSON-API responses in PHP", 4 | "keywords": ["json", "api", "standard", "jsonapi"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Toby Zerner", 9 | "email": "toby.zerner@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": "^5.5.9 || ^7.0" 14 | }, 15 | "require-dev": { 16 | "phpunit/phpunit": "^4.8 || ^5.0" 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "Tobscure\\JsonApi\\": "src/" 21 | } 22 | }, 23 | "autoload-dev": { 24 | "psr-4": { 25 | "Tobscure\\Tests\\JsonApi\\": "tests/" 26 | } 27 | }, 28 | "extra": { 29 | "branch-alias": { 30 | "dev-master": "1.0-dev" 31 | } 32 | }, 33 | "minimum-stability": "dev", 34 | "prefer-stable": true 35 | } 36 | -------------------------------------------------------------------------------- /src/AbstractSerializer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Tobscure\JsonApi; 13 | 14 | use LogicException; 15 | 16 | abstract class AbstractSerializer implements SerializerInterface 17 | { 18 | /** 19 | * The type. 20 | * 21 | * @var string 22 | */ 23 | protected $type; 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function getType($model) 29 | { 30 | return $this->type; 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function getId($model) 37 | { 38 | return $model->id; 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function getAttributes($model, array $fields = null) 45 | { 46 | return []; 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function getLinks($model) 53 | { 54 | return []; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function getMeta($model) 61 | { 62 | return []; 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | * 68 | * @throws \LogicException 69 | */ 70 | public function getRelationship($model, $name) 71 | { 72 | $method = $this->getRelationshipMethodName($name); 73 | 74 | if (method_exists($this, $method)) { 75 | $relationship = $this->$method($model); 76 | 77 | if ($relationship !== null && ! ($relationship instanceof Relationship)) { 78 | throw new LogicException('Relationship method must return null or an instance of Tobscure\JsonApi\Relationship'); 79 | } 80 | 81 | return $relationship; 82 | } 83 | } 84 | 85 | /** 86 | * Get the serializer method name for the given relationship. 87 | * 88 | * snake_case and kebab-case are converted into camelCase. 89 | * 90 | * @param string $name 91 | * 92 | * @return string 93 | */ 94 | private function getRelationshipMethodName($name) 95 | { 96 | if (stripos($name, '-')) { 97 | $name = lcfirst(implode('', array_map('ucfirst', explode('-', $name)))); 98 | } 99 | 100 | if (stripos($name, '_')) { 101 | $name = lcfirst(implode('', array_map('ucfirst', explode('_', $name)))); 102 | } 103 | 104 | return $name; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Collection.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Tobscure\JsonApi; 13 | 14 | class Collection implements ElementInterface 15 | { 16 | /** 17 | * @var array 18 | */ 19 | protected $resources = []; 20 | 21 | /** 22 | * Create a new collection instance. 23 | * 24 | * @param mixed $data 25 | * @param \Tobscure\JsonApi\SerializerInterface $serializer 26 | */ 27 | public function __construct($data, SerializerInterface $serializer) 28 | { 29 | $this->resources = $this->buildResources($data, $serializer); 30 | } 31 | 32 | /** 33 | * Convert an array of raw data to Resource objects. 34 | * 35 | * @param mixed $data 36 | * @param SerializerInterface $serializer 37 | * 38 | * @return \Tobscure\JsonApi\Resource[] 39 | */ 40 | protected function buildResources($data, SerializerInterface $serializer) 41 | { 42 | $resources = []; 43 | 44 | foreach ($data as $resource) { 45 | if (! ($resource instanceof Resource)) { 46 | $resource = new Resource($resource, $serializer); 47 | } 48 | 49 | $resources[] = $resource; 50 | } 51 | 52 | return $resources; 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function getResources() 59 | { 60 | return $this->resources; 61 | } 62 | 63 | /** 64 | * Set the resources array. 65 | * 66 | * @param array $resources 67 | * 68 | * @return void 69 | */ 70 | public function setResources($resources) 71 | { 72 | $this->resources = $resources; 73 | } 74 | 75 | /** 76 | * Request a relationship to be included for all resources. 77 | * 78 | * @param string|array $relationships 79 | * 80 | * @return $this 81 | */ 82 | public function with($relationships) 83 | { 84 | foreach ($this->resources as $resource) { 85 | $resource->with($relationships); 86 | } 87 | 88 | return $this; 89 | } 90 | 91 | /** 92 | * Request a restricted set of fields. 93 | * 94 | * @param array|null $fields 95 | * 96 | * @return $this 97 | */ 98 | public function fields($fields) 99 | { 100 | foreach ($this->resources as $resource) { 101 | $resource->fields($fields); 102 | } 103 | 104 | return $this; 105 | } 106 | 107 | /** 108 | * {@inheritdoc} 109 | */ 110 | public function toArray() 111 | { 112 | return array_map(function (Resource $resource) { 113 | return $resource->toArray(); 114 | }, $this->resources); 115 | } 116 | 117 | /** 118 | * {@inheritdoc} 119 | */ 120 | public function toIdentifier() 121 | { 122 | return array_map(function (Resource $resource) { 123 | return $resource->toIdentifier(); 124 | }, $this->resources); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Document.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Tobscure\JsonApi; 13 | 14 | use JsonSerializable; 15 | 16 | class Document implements JsonSerializable 17 | { 18 | use LinksTrait; 19 | use MetaTrait; 20 | 21 | const MEDIA_TYPE = 'application/vnd.api+json'; 22 | 23 | /** 24 | * The included array. 25 | * 26 | * @var array 27 | */ 28 | protected $included = []; 29 | 30 | /** 31 | * The errors array. 32 | * 33 | * @var array 34 | */ 35 | protected $errors; 36 | 37 | /** 38 | * The jsonapi array. 39 | * 40 | * @var array 41 | */ 42 | protected $jsonapi; 43 | 44 | /** 45 | * The data object. 46 | * 47 | * @var ElementInterface 48 | */ 49 | protected $data; 50 | 51 | /** 52 | * @param ElementInterface $data 53 | */ 54 | public function __construct(ElementInterface $data = null) 55 | { 56 | $this->data = $data; 57 | } 58 | 59 | /** 60 | * Get included resources. 61 | * 62 | * @param \Tobscure\JsonApi\ElementInterface $element 63 | * @param bool $includeParent 64 | * 65 | * @return \Tobscure\JsonApi\Resource[] 66 | */ 67 | protected function getIncluded(ElementInterface $element, $includeParent = false) 68 | { 69 | $included = []; 70 | 71 | foreach ($element->getResources() as $resource) { 72 | if ($resource->isIdentifier()) { 73 | continue; 74 | } 75 | 76 | if ($includeParent) { 77 | $included = $this->mergeResource($included, $resource); 78 | } else { 79 | $type = $resource->getType(); 80 | $id = $resource->getId(); 81 | } 82 | 83 | foreach ($resource->getUnfilteredRelationships() as $relationship) { 84 | $includedElement = $relationship->getData(); 85 | 86 | if (! $includedElement instanceof ElementInterface) { 87 | continue; 88 | } 89 | 90 | foreach ($this->getIncluded($includedElement, true) as $child) { 91 | // If this resource is the same as the top-level "data" 92 | // resource, then we don't want it to show up again in the 93 | // "included" array. 94 | if (! $includeParent && $child->getType() === $type && $child->getId() === $id) { 95 | continue; 96 | } 97 | 98 | $included = $this->mergeResource($included, $child); 99 | } 100 | } 101 | } 102 | 103 | $flattened = []; 104 | 105 | array_walk_recursive($included, function ($a) use (&$flattened) { 106 | $flattened[] = $a; 107 | }); 108 | 109 | return $flattened; 110 | } 111 | 112 | /** 113 | * @param \Tobscure\JsonApi\Resource[] $resources 114 | * @param \Tobscure\JsonApi\Resource $newResource 115 | * 116 | * @return \Tobscure\JsonApi\Resource[] 117 | */ 118 | protected function mergeResource(array $resources, Resource $newResource) 119 | { 120 | $type = $newResource->getType(); 121 | $id = $newResource->getId(); 122 | 123 | if (isset($resources[$type][$id])) { 124 | $resources[$type][$id]->merge($newResource); 125 | } else { 126 | $resources[$type][$id] = $newResource; 127 | } 128 | 129 | return $resources; 130 | } 131 | 132 | /** 133 | * Set the data object. 134 | * 135 | * @param \Tobscure\JsonApi\ElementInterface $element 136 | * 137 | * @return $this 138 | */ 139 | public function setData(ElementInterface $element) 140 | { 141 | $this->data = $element; 142 | 143 | return $this; 144 | } 145 | 146 | /** 147 | * Set the errors array. 148 | * 149 | * @param array $errors 150 | * 151 | * @return $this 152 | */ 153 | public function setErrors($errors) 154 | { 155 | $this->errors = $errors; 156 | 157 | return $this; 158 | } 159 | 160 | /** 161 | * Set the jsonapi array. 162 | * 163 | * @param array $jsonapi 164 | * 165 | * @return $this 166 | */ 167 | public function setJsonapi($jsonapi) 168 | { 169 | $this->jsonapi = $jsonapi; 170 | 171 | return $this; 172 | } 173 | 174 | /** 175 | * Map everything to arrays. 176 | * 177 | * @return array 178 | */ 179 | public function toArray() 180 | { 181 | $document = []; 182 | 183 | if (! empty($this->links)) { 184 | $document['links'] = $this->links; 185 | } 186 | 187 | if (! empty($this->data)) { 188 | $document['data'] = $this->data->toArray(); 189 | 190 | $resources = $this->getIncluded($this->data); 191 | 192 | if (count($resources)) { 193 | $document['included'] = array_map(function (Resource $resource) { 194 | return $resource->toArray(); 195 | }, $resources); 196 | } 197 | } 198 | 199 | if (! empty($this->meta)) { 200 | $document['meta'] = $this->meta; 201 | } 202 | 203 | if (! empty($this->errors)) { 204 | $document['errors'] = $this->errors; 205 | } 206 | 207 | if (! empty($this->jsonapi)) { 208 | $document['jsonapi'] = $this->jsonapi; 209 | } 210 | 211 | return $document; 212 | } 213 | 214 | /** 215 | * Map to string. 216 | * 217 | * @return string 218 | */ 219 | public function __toString() 220 | { 221 | return json_encode($this->toArray()); 222 | } 223 | 224 | /** 225 | * Serialize for JSON usage. 226 | * 227 | * @return array 228 | */ 229 | public function jsonSerialize() 230 | { 231 | return $this->toArray(); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/ElementInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Tobscure\JsonApi; 13 | 14 | interface ElementInterface 15 | { 16 | /** 17 | * Get the resources array. 18 | * 19 | * @return array 20 | */ 21 | public function getResources(); 22 | 23 | /** 24 | * Map to a "resource object" array. 25 | * 26 | * @return array 27 | */ 28 | public function toArray(); 29 | 30 | /** 31 | * Map to a "resource object identifier" array. 32 | * 33 | * @return array 34 | */ 35 | public function toIdentifier(); 36 | 37 | /** 38 | * Request a relationship to be included. 39 | * 40 | * @param string|array $relationships 41 | * 42 | * @return $this 43 | */ 44 | public function with($relationships); 45 | 46 | /** 47 | * Request a restricted set of fields. 48 | * 49 | * @param array|null $fields 50 | * 51 | * @return $this 52 | */ 53 | public function fields($fields); 54 | } 55 | -------------------------------------------------------------------------------- /src/ErrorHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Tobscure\JsonApi; 13 | 14 | use Exception; 15 | use RuntimeException; 16 | use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface; 17 | 18 | class ErrorHandler 19 | { 20 | /** 21 | * Stores the valid handlers. 22 | * 23 | * @var \Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface[] 24 | */ 25 | private $handlers = []; 26 | 27 | /** 28 | * Handle the exception provided. 29 | * 30 | * @param Exception $e 31 | * 32 | * @throws RuntimeException 33 | * 34 | * @return \Tobscure\JsonApi\Exception\Handler\ResponseBag 35 | */ 36 | public function handle(Exception $e) 37 | { 38 | foreach ($this->handlers as $handler) { 39 | if ($handler->manages($e)) { 40 | return $handler->handle($e); 41 | } 42 | } 43 | 44 | throw new RuntimeException('Exception handler for '.get_class($e).' not found.'); 45 | } 46 | 47 | /** 48 | * Register a new exception handler. 49 | * 50 | * @param \Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface $handler 51 | * 52 | * @return void 53 | */ 54 | public function registerHandler(ExceptionHandlerInterface $handler) 55 | { 56 | $this->handlers[] = $handler; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Exception/Handler/ExceptionHandlerInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Tobscure\JsonApi\Exception\Handler; 13 | 14 | use Exception; 15 | 16 | interface ExceptionHandlerInterface 17 | { 18 | /** 19 | * If the exception handler is able to format a response for the provided exception, 20 | * then the implementation should return true. 21 | * 22 | * @param \Exception $e 23 | * 24 | * @return bool 25 | */ 26 | public function manages(Exception $e); 27 | 28 | /** 29 | * Handle the provided exception. 30 | * 31 | * @param \Exception $e 32 | * 33 | * @return \Tobscure\JsonApi\Exception\Handler\ResponseBag 34 | */ 35 | public function handle(Exception $e); 36 | } 37 | -------------------------------------------------------------------------------- /src/Exception/Handler/FallbackExceptionHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Tobscure\JsonApi\Exception\Handler; 13 | 14 | use Exception; 15 | 16 | class FallbackExceptionHandler implements ExceptionHandlerInterface 17 | { 18 | /** 19 | * @var bool 20 | */ 21 | private $debug; 22 | 23 | /** 24 | * @param bool $debug 25 | */ 26 | public function __construct($debug) 27 | { 28 | $this->debug = $debug; 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function manages(Exception $e) 35 | { 36 | return true; 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function handle(Exception $e) 43 | { 44 | $status = 500; 45 | $error = $this->constructError($e, $status); 46 | 47 | return new ResponseBag($status, [$error]); 48 | } 49 | 50 | /** 51 | * @param \Exception $e 52 | * @param $status 53 | * 54 | * @return array 55 | */ 56 | private function constructError(Exception $e, $status) 57 | { 58 | $error = ['code' => $status, 'title' => 'Internal server error']; 59 | 60 | if ($this->debug) { 61 | $error['detail'] = (string) $e; 62 | } 63 | 64 | return $error; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Exception/Handler/InvalidParameterExceptionHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Tobscure\JsonApi\Exception\Handler; 13 | 14 | use Exception; 15 | use Tobscure\JsonApi\Exception\InvalidParameterException; 16 | 17 | class InvalidParameterExceptionHandler implements ExceptionHandlerInterface 18 | { 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function manages(Exception $e) 23 | { 24 | return $e instanceof InvalidParameterException; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function handle(Exception $e) 31 | { 32 | $status = 400; 33 | $error = []; 34 | 35 | $code = $e->getCode(); 36 | if ($code) { 37 | $error['code'] = $code; 38 | } 39 | 40 | $invalidParameter = $e->getInvalidParameter(); 41 | if ($invalidParameter) { 42 | $error['source'] = ['parameter' => $invalidParameter]; 43 | } 44 | 45 | return new ResponseBag($status, [$error]); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Exception/Handler/ResponseBag.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Tobscure\JsonApi\Exception\Handler; 13 | 14 | /** 15 | * DTO to manage JSON error response handling. 16 | */ 17 | class ResponseBag 18 | { 19 | private $status; 20 | private $errors; 21 | 22 | /** 23 | * @param int $status 24 | * @param array $errors 25 | */ 26 | public function __construct($status, array $errors) 27 | { 28 | $this->status = $status; 29 | $this->errors = $errors; 30 | } 31 | 32 | /** 33 | * @return array 34 | */ 35 | public function getErrors() 36 | { 37 | return $this->errors; 38 | } 39 | 40 | /** 41 | * @return int 42 | */ 43 | public function getStatus() 44 | { 45 | return $this->status; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Exception/InvalidParameterException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Tobscure\JsonApi\Exception; 13 | 14 | use Exception; 15 | 16 | class InvalidParameterException extends Exception 17 | { 18 | /** 19 | * @var string The parameter that caused this exception. 20 | */ 21 | private $invalidParameter; 22 | 23 | /** 24 | * {@inheritdoc} 25 | * 26 | * @param string $invalidParameter The parameter that caused this exception. 27 | */ 28 | public function __construct($message = '', $code = 0, $previous = null, $invalidParameter = '') 29 | { 30 | parent::__construct($message, $code, $previous); 31 | 32 | $this->invalidParameter = $invalidParameter; 33 | } 34 | 35 | /** 36 | * @return string The parameter that caused this exception. 37 | */ 38 | public function getInvalidParameter() 39 | { 40 | return $this->invalidParameter; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/LinksTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Tobscure\JsonApi; 13 | 14 | trait LinksTrait 15 | { 16 | /** 17 | * The links array. 18 | * 19 | * @var array 20 | */ 21 | protected $links; 22 | 23 | /** 24 | * Get the links. 25 | * 26 | * @return array 27 | */ 28 | public function getLinks() 29 | { 30 | return $this->links; 31 | } 32 | 33 | /** 34 | * Set the links. 35 | * 36 | * @param array $links 37 | * 38 | * @return $this 39 | */ 40 | public function setLinks(array $links) 41 | { 42 | $this->links = $links; 43 | 44 | return $this; 45 | } 46 | 47 | /** 48 | * Add a link. 49 | * 50 | * @param string $key 51 | * @param string $value 52 | * 53 | * @return $this 54 | */ 55 | public function addLink($key, $value) 56 | { 57 | $this->links[$key] = $value; 58 | 59 | return $this; 60 | } 61 | 62 | /** 63 | * Add pagination links (first, prev, next, and last). 64 | * 65 | * @param string $url The base URL for pagination links. 66 | * @param array $queryParams The query params provided in the request. 67 | * @param int $offset The current offset. 68 | * @param int $limit The current limit. 69 | * @param int|null $total The total number of results, or null if unknown. 70 | * 71 | * @return void 72 | */ 73 | public function addPaginationLinks($url, array $queryParams, $offset, $limit, $total = null) 74 | { 75 | if (isset($queryParams['page']['number'])) { 76 | $offset = floor($offset / $limit) * $limit; 77 | } 78 | 79 | $this->addPaginationLink('first', $url, $queryParams, 0, $limit); 80 | 81 | if ($offset > 0) { 82 | $this->addPaginationLink('prev', $url, $queryParams, max(0, $offset - $limit), $limit); 83 | } 84 | 85 | if ($total === null || $offset + $limit < $total) { 86 | $this->addPaginationLink('next', $url, $queryParams, $offset + $limit, $limit); 87 | } 88 | 89 | if ($total) { 90 | $this->addPaginationLink('last', $url, $queryParams, floor(($total - 1) / $limit) * $limit, $limit); 91 | } 92 | } 93 | 94 | /** 95 | * Add a pagination link. 96 | * 97 | * @param string $name The name of the link. 98 | * @param string $url The base URL for pagination links. 99 | * @param array $queryParams The query params provided in the request. 100 | * @param int $offset The offset to link to. 101 | * @param int $limit The current limit. 102 | * 103 | * @return void 104 | */ 105 | protected function addPaginationLink($name, $url, array $queryParams, $offset, $limit) 106 | { 107 | if (! isset($queryParams['page']) || ! is_array($queryParams['page'])) { 108 | $queryParams['page'] = []; 109 | } 110 | 111 | $page = &$queryParams['page']; 112 | 113 | if (isset($page['number'])) { 114 | $page['number'] = floor($offset / $limit) + 1; 115 | 116 | if ($page['number'] <= 1) { 117 | unset($page['number']); 118 | } 119 | } else { 120 | $page['offset'] = $offset; 121 | 122 | if ($page['offset'] <= 0) { 123 | unset($page['offset']); 124 | } 125 | } 126 | 127 | if (isset($page['limit'])) { 128 | $page['limit'] = $limit; 129 | } 130 | 131 | $queryString = http_build_query($queryParams); 132 | 133 | $this->addLink($name, $url.($queryString ? '?'.$queryString : '')); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/MetaTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Tobscure\JsonApi; 13 | 14 | trait MetaTrait 15 | { 16 | /** 17 | * The meta data array. 18 | * 19 | * @var array 20 | */ 21 | protected $meta; 22 | 23 | /** 24 | * Get the meta. 25 | * 26 | * @return array 27 | */ 28 | public function getMeta() 29 | { 30 | return $this->meta; 31 | } 32 | 33 | /** 34 | * Set the meta data array. 35 | * 36 | * @param array $meta 37 | * 38 | * @return $this 39 | */ 40 | public function setMeta(array $meta) 41 | { 42 | $this->meta = $meta; 43 | 44 | return $this; 45 | } 46 | 47 | /** 48 | * Add meta data. 49 | * 50 | * @param string $key 51 | * @param string $value 52 | * 53 | * @return $this 54 | */ 55 | public function addMeta($key, $value) 56 | { 57 | $this->meta[$key] = $value; 58 | 59 | return $this; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Parameters.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Tobscure\JsonApi; 13 | 14 | use Tobscure\JsonApi\Exception\InvalidParameterException; 15 | 16 | class Parameters 17 | { 18 | /** 19 | * @var array 20 | */ 21 | protected $input; 22 | 23 | /** 24 | * @param array $input 25 | */ 26 | public function __construct(array $input) 27 | { 28 | $this->input = $input; 29 | } 30 | 31 | /** 32 | * Get the includes. 33 | * 34 | * @param array $available 35 | * 36 | * @throws \Tobscure\JsonApi\Exception\InvalidParameterException 37 | * 38 | * @return array 39 | */ 40 | public function getInclude(array $available = []) 41 | { 42 | if ($include = $this->getInput('include')) { 43 | $relationships = explode(',', $include); 44 | 45 | $invalid = array_diff($relationships, $available); 46 | 47 | if (count($invalid)) { 48 | throw new InvalidParameterException( 49 | 'Invalid includes ['.implode(',', $invalid).']', 50 | 1, 51 | null, 52 | 'include' 53 | ); 54 | } 55 | 56 | return $relationships; 57 | } 58 | 59 | return []; 60 | } 61 | 62 | /** 63 | * Get number of offset. 64 | * 65 | * @param int|null $perPage 66 | * 67 | * @throws \Tobscure\JsonApi\Exception\InvalidParameterException 68 | * 69 | * @return int 70 | */ 71 | public function getOffset($perPage = null) 72 | { 73 | if ($perPage && ($offset = $this->getOffsetFromNumber($perPage))) { 74 | return $offset; 75 | } 76 | 77 | $offset = (int) $this->getPage('offset'); 78 | 79 | if ($offset < 0) { 80 | throw new InvalidParameterException('page[offset] must be >=0', 2, null, 'page[offset]'); 81 | } 82 | 83 | return $offset; 84 | } 85 | 86 | /** 87 | * Calculate the offset based on the page[number] parameter. 88 | * 89 | * @param int $perPage 90 | * 91 | * @throws \Tobscure\JsonApi\Exception\InvalidParameterException 92 | * 93 | * @return int 94 | */ 95 | protected function getOffsetFromNumber($perPage) 96 | { 97 | $page = (int) $this->getPage('number'); 98 | 99 | if ($page <= 1) { 100 | return 0; 101 | } 102 | 103 | return ($page - 1) * $perPage; 104 | } 105 | 106 | /** 107 | * Get the limit. 108 | * 109 | * @param int|null $max 110 | * 111 | * @return int|null 112 | */ 113 | public function getLimit($max = null) 114 | { 115 | $limit = $this->getPage('limit') ?: $this->getPage('size') ?: null; 116 | 117 | if ($limit && $max) { 118 | $limit = min($max, $limit); 119 | } 120 | 121 | return $limit; 122 | } 123 | 124 | /** 125 | * Get the sort. 126 | * 127 | * @param array $available 128 | * 129 | * @throws \Tobscure\JsonApi\Exception\InvalidParameterException 130 | * 131 | * @return array 132 | */ 133 | public function getSort(array $available = []) 134 | { 135 | $sort = []; 136 | 137 | if ($input = $this->getInput('sort')) { 138 | $fields = explode(',', $input); 139 | 140 | foreach ($fields as $field) { 141 | if (substr($field, 0, 1) === '-') { 142 | $field = substr($field, 1); 143 | $order = 'desc'; 144 | } else { 145 | $order = 'asc'; 146 | } 147 | 148 | $sort[$field] = $order; 149 | } 150 | 151 | $invalid = array_diff(array_keys($sort), $available); 152 | 153 | if (count($invalid)) { 154 | throw new InvalidParameterException( 155 | 'Invalid sort fields ['.implode(',', $invalid).']', 156 | 3, 157 | null, 158 | 'sort' 159 | ); 160 | } 161 | } 162 | 163 | return $sort; 164 | } 165 | 166 | /** 167 | * Get the fields requested for inclusion. 168 | * 169 | * @return array 170 | */ 171 | public function getFields() 172 | { 173 | $fields = $this->getInput('fields'); 174 | 175 | if (! is_array($fields)) { 176 | return []; 177 | } 178 | 179 | return array_map(function ($fields) { 180 | return explode(',', $fields); 181 | }, $fields); 182 | } 183 | 184 | /** 185 | * Get a filter item. 186 | * 187 | * @return mixed 188 | */ 189 | public function getFilter() 190 | { 191 | return $this->getInput('filter'); 192 | } 193 | 194 | /** 195 | * Get an input item. 196 | * 197 | * @param string $key 198 | * @param null $default 199 | * 200 | * @return mixed 201 | */ 202 | protected function getInput($key, $default = null) 203 | { 204 | return isset($this->input[$key]) ? $this->input[$key] : $default; 205 | } 206 | 207 | /** 208 | * Get the page. 209 | * 210 | * @param string $key 211 | * 212 | * @return string 213 | */ 214 | protected function getPage($key) 215 | { 216 | $page = $this->getInput('page'); 217 | 218 | return isset($page[$key]) ? $page[$key] : ''; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/Relationship.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Tobscure\JsonApi; 13 | 14 | class Relationship 15 | { 16 | use LinksTrait; 17 | use MetaTrait; 18 | 19 | /** 20 | * The data object. 21 | * 22 | * @var \Tobscure\JsonApi\ElementInterface|null 23 | */ 24 | protected $data; 25 | 26 | /** 27 | * Create a new relationship. 28 | * 29 | * @param \Tobscure\JsonApi\ElementInterface|null $data 30 | */ 31 | public function __construct(ElementInterface $data = null) 32 | { 33 | $this->data = $data; 34 | } 35 | 36 | /** 37 | * Get the data object. 38 | * 39 | * @return \Tobscure\JsonApi\ElementInterface|null 40 | */ 41 | public function getData() 42 | { 43 | return $this->data; 44 | } 45 | 46 | /** 47 | * Set the data object. 48 | * 49 | * @param \Tobscure\JsonApi\ElementInterface|null $data 50 | * 51 | * @return $this 52 | */ 53 | public function setData($data) 54 | { 55 | $this->data = $data; 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * Map everything to an array. 62 | * 63 | * @return array 64 | */ 65 | public function toArray() 66 | { 67 | $array = []; 68 | 69 | if (! empty($this->data)) { 70 | $array['data'] = $this->data->toIdentifier(); 71 | } 72 | 73 | if (! empty($this->meta)) { 74 | $array['meta'] = $this->meta; 75 | } 76 | 77 | if (! empty($this->links)) { 78 | $array['links'] = $this->links; 79 | } 80 | 81 | return $array; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Resource.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Tobscure\JsonApi; 13 | 14 | class Resource implements ElementInterface 15 | { 16 | use LinksTrait; 17 | use MetaTrait; 18 | 19 | /** 20 | * @var mixed 21 | */ 22 | protected $data; 23 | 24 | /** 25 | * @var \Tobscure\JsonApi\SerializerInterface 26 | */ 27 | protected $serializer; 28 | 29 | /** 30 | * A list of relationships to include. 31 | * 32 | * @var array 33 | */ 34 | protected $includes = []; 35 | 36 | /** 37 | * A list of fields to restrict to. 38 | * 39 | * @var array|null 40 | */ 41 | protected $fields; 42 | 43 | /** 44 | * An array of Resources that should be merged into this one. 45 | * 46 | * @var \Tobscure\JsonApi\Resource[] 47 | */ 48 | protected $merged = []; 49 | 50 | /** 51 | * @var \Tobscure\JsonApi\Relationship[] 52 | */ 53 | private $relationships; 54 | 55 | /** 56 | * @param mixed $data 57 | * @param \Tobscure\JsonApi\SerializerInterface $serializer 58 | */ 59 | public function __construct($data, SerializerInterface $serializer) 60 | { 61 | $this->data = $data; 62 | $this->serializer = $serializer; 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | */ 68 | public function getResources() 69 | { 70 | return [$this]; 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | public function toArray() 77 | { 78 | $array = $this->toIdentifier(); 79 | 80 | if (! $this->isIdentifier()) { 81 | $attributes = $this->getAttributes(); 82 | if ($attributes) { 83 | $array['attributes'] = $attributes; 84 | } 85 | } 86 | 87 | $relationships = $this->getRelationshipsAsArray(); 88 | 89 | if (count($relationships)) { 90 | $array['relationships'] = $relationships; 91 | } 92 | 93 | $links = []; 94 | if (! empty($this->links)) { 95 | $links = $this->links; 96 | } 97 | $serializerLinks = $this->serializer->getLinks($this->data); 98 | if (! empty($serializerLinks)) { 99 | $links = array_merge($serializerLinks, $links); 100 | } 101 | if (! empty($links)) { 102 | $array['links'] = $links; 103 | } 104 | 105 | $meta = []; 106 | if (! empty($this->meta)) { 107 | $meta = $this->meta; 108 | } 109 | $serializerMeta = $this->serializer->getMeta($this->data); 110 | if (! empty($serializerMeta)) { 111 | $meta = array_merge($serializerMeta, $meta); 112 | } 113 | if (! empty($meta)) { 114 | $array['meta'] = $meta; 115 | } 116 | 117 | return $array; 118 | } 119 | 120 | /** 121 | * Check whether or not this resource is an identifier (i.e. does it have 122 | * any data attached?). 123 | * 124 | * @return bool 125 | */ 126 | public function isIdentifier() 127 | { 128 | return ! is_object($this->data) && ! is_array($this->data); 129 | } 130 | 131 | /** 132 | * {@inheritdoc} 133 | */ 134 | public function toIdentifier() 135 | { 136 | if (! $this->data) { 137 | return; 138 | } 139 | 140 | $array = [ 141 | 'type' => $this->getType(), 142 | 'id' => $this->getId() 143 | ]; 144 | 145 | if (! empty($this->meta)) { 146 | $array['meta'] = $this->meta; 147 | } 148 | 149 | return $array; 150 | } 151 | 152 | /** 153 | * Get the resource type. 154 | * 155 | * @return string 156 | */ 157 | public function getType() 158 | { 159 | return $this->serializer->getType($this->data); 160 | } 161 | 162 | /** 163 | * Get the resource ID. 164 | * 165 | * @return string 166 | */ 167 | public function getId() 168 | { 169 | if (! is_object($this->data) && ! is_array($this->data)) { 170 | return (string) $this->data; 171 | } 172 | 173 | return (string) $this->serializer->getId($this->data); 174 | } 175 | 176 | /** 177 | * Get the resource attributes. 178 | * 179 | * @return array 180 | */ 181 | public function getAttributes() 182 | { 183 | $attributes = (array) $this->serializer->getAttributes($this->data, $this->getOwnFields()); 184 | 185 | $attributes = $this->filterFields($attributes); 186 | 187 | $attributes = $this->mergeAttributes($attributes); 188 | 189 | return $attributes; 190 | } 191 | 192 | /** 193 | * Get the requested fields for this resource type. 194 | * 195 | * @return array|null 196 | */ 197 | protected function getOwnFields() 198 | { 199 | $type = $this->getType(); 200 | 201 | if (isset($this->fields[$type])) { 202 | return $this->fields[$type]; 203 | } 204 | } 205 | 206 | /** 207 | * Filter the given fields array (attributes or relationships) according 208 | * to the requested fieldset. 209 | * 210 | * @param array $fields 211 | * 212 | * @return array 213 | */ 214 | protected function filterFields(array $fields) 215 | { 216 | if ($requested = $this->getOwnFields()) { 217 | $fields = array_intersect_key($fields, array_flip($requested)); 218 | } 219 | 220 | return $fields; 221 | } 222 | 223 | /** 224 | * Merge the attributes of merged resources into an array of attributes. 225 | * 226 | * @param array $attributes 227 | * 228 | * @return array 229 | */ 230 | protected function mergeAttributes(array $attributes) 231 | { 232 | foreach ($this->merged as $resource) { 233 | $attributes = array_replace_recursive($attributes, $resource->getAttributes()); 234 | } 235 | 236 | return $attributes; 237 | } 238 | 239 | /** 240 | * Get the resource relationships. 241 | * 242 | * @return \Tobscure\JsonApi\Relationship[] 243 | */ 244 | public function getRelationships() 245 | { 246 | $relationships = $this->buildRelationships(); 247 | 248 | return $this->filterFields($relationships); 249 | } 250 | 251 | /** 252 | * Get the resource relationships without considering requested ones. 253 | * 254 | * @return \Tobscure\JsonApi\Relationship[] 255 | */ 256 | public function getUnfilteredRelationships() 257 | { 258 | return $this->buildRelationships(); 259 | } 260 | 261 | /** 262 | * Get the resource relationships as an array. 263 | * 264 | * @return array 265 | */ 266 | public function getRelationshipsAsArray() 267 | { 268 | $relationships = $this->getRelationships(); 269 | 270 | $relationships = $this->convertRelationshipsToArray($relationships); 271 | 272 | return $this->mergeRelationships($relationships); 273 | } 274 | 275 | /** 276 | * Get an array of built relationships. 277 | * 278 | * @return \Tobscure\JsonApi\Relationship[] 279 | */ 280 | protected function buildRelationships() 281 | { 282 | if (isset($this->relationships)) { 283 | return $this->relationships; 284 | } 285 | 286 | $paths = Util::parseRelationshipPaths($this->includes); 287 | 288 | $relationships = []; 289 | 290 | foreach ($paths as $name => $nested) { 291 | $relationship = $this->serializer->getRelationship($this->data, $name); 292 | 293 | if ($relationship) { 294 | $relationshipData = $relationship->getData(); 295 | if ($relationshipData instanceof ElementInterface) { 296 | $relationshipData->with($nested)->fields($this->fields); 297 | } 298 | 299 | $relationships[$name] = $relationship; 300 | } 301 | } 302 | 303 | return $this->relationships = $relationships; 304 | } 305 | 306 | /** 307 | * Merge the relationships of merged resources into an array of 308 | * relationships. 309 | * 310 | * @param array $relationships 311 | * 312 | * @return array 313 | */ 314 | protected function mergeRelationships(array $relationships) 315 | { 316 | foreach ($this->merged as $resource) { 317 | $relationships = array_replace_recursive($relationships, $resource->getRelationshipsAsArray()); 318 | } 319 | 320 | return $relationships; 321 | } 322 | 323 | /** 324 | * Convert the given array of Relationship objects into an array. 325 | * 326 | * @param \Tobscure\JsonApi\Relationship[] $relationships 327 | * 328 | * @return array 329 | */ 330 | protected function convertRelationshipsToArray(array $relationships) 331 | { 332 | return array_map(function (Relationship $relationship) { 333 | return $relationship->toArray(); 334 | }, $relationships); 335 | } 336 | 337 | /** 338 | * Merge a resource into this one. 339 | * 340 | * @param \Tobscure\JsonApi\Resource $resource 341 | * 342 | * @return void 343 | */ 344 | public function merge(Resource $resource) 345 | { 346 | $this->merged[] = $resource; 347 | } 348 | 349 | /** 350 | * {@inheritdoc} 351 | */ 352 | public function with($relationships) 353 | { 354 | $this->includes = array_unique(array_merge($this->includes, (array) $relationships)); 355 | 356 | $this->relationships = null; 357 | 358 | return $this; 359 | } 360 | 361 | /** 362 | * {@inheritdoc} 363 | */ 364 | public function fields($fields) 365 | { 366 | $this->fields = $fields; 367 | 368 | return $this; 369 | } 370 | 371 | /** 372 | * @return mixed 373 | */ 374 | public function getData() 375 | { 376 | return $this->data; 377 | } 378 | 379 | /** 380 | * @param mixed $data 381 | * 382 | * @return void 383 | */ 384 | public function setData($data) 385 | { 386 | $this->data = $data; 387 | } 388 | 389 | /** 390 | * @return \Tobscure\JsonApi\SerializerInterface 391 | */ 392 | public function getSerializer() 393 | { 394 | return $this->serializer; 395 | } 396 | 397 | /** 398 | * @param \Tobscure\JsonApi\SerializerInterface $serializer 399 | * 400 | * @return void 401 | */ 402 | public function setSerializer(SerializerInterface $serializer) 403 | { 404 | $this->serializer = $serializer; 405 | } 406 | } 407 | -------------------------------------------------------------------------------- /src/SerializerInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Tobscure\JsonApi; 13 | 14 | interface SerializerInterface 15 | { 16 | /** 17 | * Get the type. 18 | * 19 | * @param mixed $model 20 | * 21 | * @return string 22 | */ 23 | public function getType($model); 24 | 25 | /** 26 | * Get the id. 27 | * 28 | * @param mixed $model 29 | * 30 | * @return string 31 | */ 32 | public function getId($model); 33 | 34 | /** 35 | * Get the attributes array. 36 | * 37 | * @param mixed $model 38 | * @param array|null $fields 39 | * 40 | * @return array 41 | */ 42 | public function getAttributes($model, array $fields = null); 43 | 44 | /** 45 | * Get the links array. 46 | * 47 | * @param mixed $model 48 | * 49 | * @return array 50 | */ 51 | public function getLinks($model); 52 | 53 | /** 54 | * Get the meta. 55 | * 56 | * @param mixed $model 57 | * 58 | * @return array 59 | */ 60 | public function getMeta($model); 61 | 62 | /** 63 | * Get a relationship. 64 | * 65 | * @param mixed $model 66 | * @param string $name 67 | * 68 | * @return \Tobscure\JsonApi\Relationship|null 69 | */ 70 | public function getRelationship($model, $name); 71 | } 72 | -------------------------------------------------------------------------------- /src/Util.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Tobscure\JsonApi; 13 | 14 | class Util 15 | { 16 | /** 17 | * Parse relationship paths. 18 | * 19 | * Given a flat array of relationship paths like: 20 | * 21 | * ['user', 'user.employer', 'user.employer.country', 'comments'] 22 | * 23 | * create a nested array of relationship paths one-level deep that can 24 | * be passed on to other serializers: 25 | * 26 | * ['user' => ['employer', 'employer.country'], 'comments' => []] 27 | * 28 | * @param array $paths 29 | * 30 | * @return array 31 | */ 32 | public static function parseRelationshipPaths(array $paths) 33 | { 34 | $tree = []; 35 | 36 | foreach ($paths as $path) { 37 | list($primary, $nested) = array_pad(explode('.', $path, 2), 2, null); 38 | 39 | if (! isset($tree[$primary])) { 40 | $tree[$primary] = []; 41 | } 42 | 43 | if ($nested) { 44 | $tree[$primary][] = $nested; 45 | } 46 | } 47 | 48 | return $tree; 49 | } 50 | } 51 | --------------------------------------------------------------------------------