diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0bbb62a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +* text=auto + +/tests export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.php_cs export-ignore +/.travis.yml export-ignore +/phpunit.xml.dist export-ignore +/CHANGELOG.md export-ignore +/CONTRIBUTING.md export-ignore +/README.md export-ignore diff --git a/.travis.yml b/.travis.yml index bb6e5c8..37af9cc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: php php: + - 5.5.9 - 5.5 - 5.6 - 7.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 194e320..0110ce6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [0.4.1] - 2017-03-06 +### Fixed +- [#114](https://github.com/tobscure/json-api/pull/114): Do not call `getAttributes()` twice. ([@f3ath](https://github.com/f3ath)) + +## [0.4.0] - 2017-03-05 +### Added +- [#96](https://github.com/tobscure/json-api/pull/96): Improve invalid parameter exceptions with more information about what's invalid. ([@nubs](https://github.com/nubs)) + +### Changed +- [#106](https://github.com/tobscure/json-api/pull/106): Convert snake_case into camelCase when calling a relationship method. ([@ustrugany](https://github.com/ustrugany)) + +### Fixed +- [#94](https://github.com/tobscure/json-api/pull/94): Properly support empty to-one relationships. +- [#99](https://github.com/tobscure/json-api/pull/99): Omit attributes on resource identifier objects. ([@anthonyhendrickson](https://github.com/anthonyhendrickson)) +- [#103](https://github.com/tobscure/json-api/issues/103): Prevent resource relationships from being built twice per resource. +- [#110](https://github.com/tobscure/json-api/pull/110): Omit empty attributes on resources. ([@f3ath](https://github.com/f3ath)) + ## [0.3.0] - 2016-03-31 ### Added - [#79](https://github.com/tobscure/json-api/pull/79), [#81](https://github.com/tobscure/json-api/pull/81): Allow serializers to add links and metadata to resources. ([@bwaidelich](https://github.com/bwaidelich)) @@ -11,7 +28,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - [#85](https://github.com/tobscure/json-api/pull/85): Allow creation of relationships without data. ([@josephmcdermott](https://github.com/josephmcdermott)) ### Fixed -- [#65](https://github.com/tobscure/json-api/pull/65): Convert snake-case into camelCase when calling a relationship method. ([@avoelpel](https://github.com/avoelpel)) +- [#65](https://github.com/tobscure/json-api/pull/65): Convert kebab-case into camelCase when calling a relationship method. ([@avoelpel](https://github.com/avoelpel)) - [#70](https://github.com/tobscure/json-api/pull/70): Include related resources even if relationship is not listed in sparse fieldset. ([@Damith88](https://github.com/Damith88)) - [#72](https://github.com/tobscure/json-api/pull/72): Return `null` in `Parameters::getLimit` if no limit is set. ([@byCedric](https://github.com/byCedric)) - [46142e5](https://github.com/tobscure/json-api/commit/46142e5823da3bebbd9dfc38833af4d808a5e3f3): Prevent primary "data" resources from showing up again in the "included" array. ([@tobscure](https://github.com/tobscure)) @@ -39,6 +56,8 @@ Completely rewrite to improve all the things. ## 0.1.0 - 2015-08-07 - Initial release +[0.4.1]: https://github.com/tobscure/json-api/compare/v0.4.0...v0.4.1 +[0.4.0]: https://github.com/tobscure/json-api/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/tobscure/json-api/compare/v0.2.1...v0.3.0 [0.2.1]: https://github.com/tobscure/json-api/compare/v0.2.0...v0.2.1 [0.2.0]: https://github.com/tobscure/json-api/compare/v0.1.1...v0.2.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0ca2ed1..9a9f662 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,10 +6,9 @@ Contributions are welcome, and are accepted via pull requests. Please review the * Please follow the [PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) and [PHP-FIG Naming Conventions](https://github.com/php-fig/fig-standards/blob/master/bylaws/002-psr-naming-conventions.md). * Ensure that the current tests pass, and if you've added something new, add the tests where relevant. -* Remember that we follow [SemVer](http://semver.org). If you are changing the behaviour, or the public api, you may need to update the docs. -* Send a coherent commit history, making sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash](http://git-scm.com/book/en/Git-Tools-Rewriting-History) them before submitting. -* You may also need to [rebase](http://git-scm.com/book/en/Git-Branching-Rebasing) to avoid merge conflicts. - +* Remember that we follow [SemVer](http://semver.org). If you are changing the behavior, or the public api, you may need to update the docs. +* Send a coherent commit history, making sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash](https://git-scm.com/book/en/Git-Tools-Rewriting-History) them before submitting. +* You may also need to [rebase](https://git-scm.com/book/en/Git-Branching-Rebasing) to avoid merge conflicts. ## Running Tests @@ -21,7 +20,7 @@ First, install the dependencies: $ composer install ``` -Then run phpunit: +Then run PHPUnit: ```bash $ vendor/bin/phpunit @@ -29,4 +28,4 @@ $ vendor/bin/phpunit If the test suite passes on your local machine you should be good to go. -When you make a pull request, the tests will automatically be run again by [Travis CI](https://travis-ci.org/) on multiple php versions and hhvm. +When you make a pull request, the tests will automatically be run again by [Travis CI](https://travis-ci.org/) on multiple PHP versions and HHVM. diff --git a/README.md b/README.md index 200ee23..3feee89 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ Works with version 1.0 of the spec. +**⚠️ The project is no longer maintained.** Check out [tobyzerner/json-api-server](https://github.com/tobyzerner/json-api-server) instead. + ## Install via Composer: @@ -52,7 +54,7 @@ A JSON-API Document may contain one primary Element. The primary Element will be #### Sparse Fieldsets -You can specify which fields (attributes and relationships) to be included on an Element using the `fields` method. You must provide a multidimensional array organized by resource type: +You can specify which fields (attributes and relationships) are to be included on an Element using the `fields` method. You must provide a multidimensional array organized by resource type: ```php $collection->fields(['posts' => ['title', 'date']]); @@ -102,6 +104,15 @@ public function comments($post) } ``` +By default, the `AbstractSerializer` will convert relationship names from `kebab-case` and `snake_case` into a `camelCase` method name and call that on the serializer. If you wish to customize this behaviour, you may override the `getRelationship` method: + +```php +public function getRelationship($model, $name) +{ + // resolve Relationship called $name for $model +} +``` + ### Meta & Links The `Document`, `Resource`, and `Relationship` classes allow you to add meta information: diff --git a/composer.json b/composer.json index fbeebf8..3061728 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ } ], "require": { - "php": ">=5.5.9" + "php": "^5.5.9 || ^7.0" }, "require-dev": { "phpunit/phpunit": "^4.8 || ^5.0" diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 51bc73c..ae108af 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,6 +8,8 @@ convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" + failOnRisky="true" + failOnWarning="true" processIsolation="false" stopOnError="false" stopOnFailure="false" diff --git a/src/AbstractSerializer.php b/src/AbstractSerializer.php index 49a25c6..6e343cc 100644 --- a/src/AbstractSerializer.php +++ b/src/AbstractSerializer.php @@ -65,7 +65,7 @@ public function getMeta($model) /** * {@inheritdoc} * - * @throws LogicException + * @throws \LogicException */ public function getRelationship($model, $name) { @@ -85,9 +85,10 @@ public function getRelationship($model, $name) /** * Get the serializer method name for the given relationship. * - * kebab-case is converted into camelCase. + * snake_case and kebab-case are converted into camelCase. * * @param string $name + * * @return string */ private function getRelationshipMethodName($name) @@ -96,6 +97,10 @@ private function getRelationshipMethodName($name) $name = lcfirst(implode('', array_map('ucfirst', explode('-', $name)))); } + if (stripos($name, '_')) { + $name = lcfirst(implode('', array_map('ucfirst', explode('_', $name)))); + } + return $name; } } diff --git a/src/Collection.php b/src/Collection.php index c0ae153..4d6707b 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -22,7 +22,7 @@ class Collection implements ElementInterface * Create a new collection instance. * * @param mixed $data - * @param SerializerInterface $serializer + * @param \Tobscure\JsonApi\SerializerInterface $serializer */ public function __construct($data, SerializerInterface $serializer) { @@ -34,7 +34,8 @@ public function __construct($data, SerializerInterface $serializer) * * @param mixed $data * @param SerializerInterface $serializer - * @return Resource[] + * + * @return \Tobscure\JsonApi\Resource[] */ protected function buildResources($data, SerializerInterface $serializer) { @@ -63,6 +64,8 @@ public function getResources() * Set the resources array. * * @param array $resources + * + * @return void */ public function setResources($resources) { @@ -73,6 +76,7 @@ public function setResources($resources) * Request a relationship to be included for all resources. * * @param string|array $relationships + * * @return $this */ public function with($relationships) @@ -84,25 +88,11 @@ public function with($relationships) return $this; } - /** - * Request a relationship to be identified for all resources. - * - * @param string|array $relationships - * @return $this - */ - public function identify($relationships) - { - foreach ($this->resources as $resource) { - $resource->identify($relationships); - } - - return $this; - } - /** * Request a restricted set of fields. * * @param array|null $fields + * * @return $this */ public function fields($fields) diff --git a/src/Document.php b/src/Document.php index 9d1318e..aa350ea 100644 --- a/src/Document.php +++ b/src/Document.php @@ -18,6 +18,8 @@ class Document implements JsonSerializable use LinksTrait; use MetaTrait; + const MEDIA_TYPE = 'application/vnd.api+json'; + /** * The included array. * @@ -57,9 +59,10 @@ public function __construct(ElementInterface $data = null) /** * Get included resources. * - * @param ElementInterface $element + * @param \Tobscure\JsonApi\ElementInterface $element * @param bool $includeParent - * @return Resource[] + * + * @return \Tobscure\JsonApi\Resource[] */ protected function getIncluded(ElementInterface $element, $includeParent = false) { @@ -107,9 +110,10 @@ protected function getIncluded(ElementInterface $element, $includeParent = false } /** - * @param Resource[] $resources - * @param Resource $newResource - * @return Resource[] + * @param \Tobscure\JsonApi\Resource[] $resources + * @param \Tobscure\JsonApi\Resource $newResource + * + * @return \Tobscure\JsonApi\Resource[] */ protected function mergeResource(array $resources, Resource $newResource) { @@ -128,7 +132,8 @@ protected function mergeResource(array $resources, Resource $newResource) /** * Set the data object. * - * @param ElementInterface $element + * @param \Tobscure\JsonApi\ElementInterface $element + * * @return $this */ public function setData(ElementInterface $element) @@ -142,6 +147,7 @@ public function setData(ElementInterface $element) * Set the errors array. * * @param array $errors + * * @return $this */ public function setErrors($errors) @@ -155,6 +161,7 @@ public function setErrors($errors) * Set the jsonapi array. * * @param array $jsonapi + * * @return $this */ public function setJsonapi($jsonapi) diff --git a/src/ElementInterface.php b/src/ElementInterface.php index 6b0ac00..81b74ba 100644 --- a/src/ElementInterface.php +++ b/src/ElementInterface.php @@ -38,6 +38,7 @@ public function toIdentifier(); * Request a relationship to be included. * * @param string|array $relationships + * * @return $this */ public function with($relationships); @@ -46,6 +47,7 @@ public function with($relationships); * Request a restricted set of fields. * * @param array|null $fields + * * @return $this */ public function fields($fields); diff --git a/src/ErrorHandler.php b/src/ErrorHandler.php index 33243f0..1e3492d 100644 --- a/src/ErrorHandler.php +++ b/src/ErrorHandler.php @@ -20,7 +20,7 @@ class ErrorHandler /** * Stores the valid handlers. * - * @var ExceptionHandlerInterface[] + * @var \Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface[] */ private $handlers = []; @@ -28,8 +28,10 @@ class ErrorHandler * Handle the exception provided. * * @param Exception $e - * @return \Tobscure\JsonApi\Exception\Handler\ResponseBag + * * @throws RuntimeException + * + * @return \Tobscure\JsonApi\Exception\Handler\ResponseBag */ public function handle(Exception $e) { @@ -45,7 +47,9 @@ public function handle(Exception $e) /** * Register a new exception handler. * - * @param ExceptionHandlerInterface $handler + * @param \Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface $handler + * + * @return void */ public function registerHandler(ExceptionHandlerInterface $handler) { diff --git a/src/Exception/Handler/ExceptionHandlerInterface.php b/src/Exception/Handler/ExceptionHandlerInterface.php index 15ea0b4..4108fc0 100644 --- a/src/Exception/Handler/ExceptionHandlerInterface.php +++ b/src/Exception/Handler/ExceptionHandlerInterface.php @@ -19,7 +19,8 @@ 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 + * @param \Exception $e + * * @return bool */ public function manages(Exception $e); @@ -27,8 +28,9 @@ public function manages(Exception $e); /** * Handle the provided exception. * - * @param Exception $e - * @return ResponseBag + * @param \Exception $e + * + * @return \Tobscure\JsonApi\Exception\Handler\ResponseBag */ public function handle(Exception $e); } diff --git a/src/Exception/Handler/FallbackExceptionHandler.php b/src/Exception/Handler/FallbackExceptionHandler.php index 4d1fa96..2799d55 100644 --- a/src/Exception/Handler/FallbackExceptionHandler.php +++ b/src/Exception/Handler/FallbackExceptionHandler.php @@ -48,8 +48,9 @@ public function handle(Exception $e) } /** - * @param Exception $e + * @param \Exception $e * @param $status + * * @return array */ private function constructError(Exception $e, $status) diff --git a/src/Exception/Handler/InvalidParameterExceptionHandler.php b/src/Exception/Handler/InvalidParameterExceptionHandler.php index d4ff608..374020a 100644 --- a/src/Exception/Handler/InvalidParameterExceptionHandler.php +++ b/src/Exception/Handler/InvalidParameterExceptionHandler.php @@ -32,6 +32,16 @@ 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]); } } diff --git a/src/Exception/InvalidParameterException.php b/src/Exception/InvalidParameterException.php index b8fcf5e..72027c0 100644 --- a/src/Exception/InvalidParameterException.php +++ b/src/Exception/InvalidParameterException.php @@ -15,4 +15,28 @@ 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; + } } diff --git a/src/LinksTrait.php b/src/LinksTrait.php index 316790b..1f22171 100644 --- a/src/LinksTrait.php +++ b/src/LinksTrait.php @@ -34,6 +34,7 @@ public function getLinks() * Set the links. * * @param array $links + * * @return $this */ public function setLinks(array $links) @@ -48,6 +49,7 @@ public function setLinks(array $links) * * @param string $key * @param string $value + * * @return $this */ public function addLink($key, $value) @@ -65,6 +67,8 @@ public function addLink($key, $value) * @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) { @@ -95,6 +99,8 @@ public function addPaginationLinks($url, array $queryParams, $offset, $limit, $t * @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) { diff --git a/src/MetaTrait.php b/src/MetaTrait.php index 5202033..5572446 100644 --- a/src/MetaTrait.php +++ b/src/MetaTrait.php @@ -34,6 +34,7 @@ public function getMeta() * Set the meta data array. * * @param array $meta + * * @return $this */ public function setMeta(array $meta) @@ -48,6 +49,7 @@ public function setMeta(array $meta) * * @param string $key * @param string $value + * * @return $this */ public function addMeta($key, $value) diff --git a/src/Parameters.php b/src/Parameters.php index cfd6db6..0280e47 100644 --- a/src/Parameters.php +++ b/src/Parameters.php @@ -32,8 +32,10 @@ public function __construct(array $input) * Get the includes. * * @param array $available + * + * @throws \Tobscure\JsonApi\Exception\InvalidParameterException + * * @return array - * @throws InvalidParameterException */ public function getInclude(array $available = []) { @@ -43,7 +45,12 @@ public function getInclude(array $available = []) $invalid = array_diff($relationships, $available); if (count($invalid)) { - throw new InvalidParameterException('Invalid includes ['.implode(',', $invalid).']'); + throw new InvalidParameterException( + 'Invalid includes ['.implode(',', $invalid).']', + 1, + null, + 'include' + ); } return $relationships; @@ -56,8 +63,10 @@ public function getInclude(array $available = []) * Get number of offset. * * @param int|null $perPage + * + * @throws \Tobscure\JsonApi\Exception\InvalidParameterException + * * @return int - * @throws InvalidParameterException */ public function getOffset($perPage = null) { @@ -68,7 +77,7 @@ public function getOffset($perPage = null) $offset = (int) $this->getPage('offset'); if ($offset < 0) { - throw new InvalidParameterException('page[offset] must be >=0'); + throw new InvalidParameterException('page[offset] must be >=0', 2, null, 'page[offset]'); } return $offset; @@ -78,8 +87,10 @@ public function getOffset($perPage = null) * Calculate the offset based on the page[number] parameter. * * @param int $perPage + * + * @throws \Tobscure\JsonApi\Exception\InvalidParameterException + * * @return int - * @throws InvalidParameterException */ protected function getOffsetFromNumber($perPage) { @@ -96,6 +107,7 @@ protected function getOffsetFromNumber($perPage) * Get the limit. * * @param int|null $max + * * @return int|null */ public function getLimit($max = null) @@ -113,8 +125,10 @@ public function getLimit($max = null) * Get the sort. * * @param array $available + * + * @throws \Tobscure\JsonApi\Exception\InvalidParameterException + * * @return array - * @throws InvalidParameterException */ public function getSort(array $available = []) { @@ -137,7 +151,12 @@ public function getSort(array $available = []) $invalid = array_diff(array_keys($sort), $available); if (count($invalid)) { - throw new InvalidParameterException('Invalid sort fields ['.implode(',', $invalid).']'); + throw new InvalidParameterException( + 'Invalid sort fields ['.implode(',', $invalid).']', + 3, + null, + 'sort' + ); } } @@ -177,6 +196,7 @@ public function getFilter() * * @param string $key * @param null $default + * * @return mixed */ protected function getInput($key, $default = null) @@ -188,6 +208,7 @@ protected function getInput($key, $default = null) * Get the page. * * @param string $key + * * @return string */ protected function getPage($key) diff --git a/src/Relationship.php b/src/Relationship.php index bbad37d..29f7fc6 100644 --- a/src/Relationship.php +++ b/src/Relationship.php @@ -19,14 +19,14 @@ class Relationship /** * The data object. * - * @var ElementInterface|null + * @var \Tobscure\JsonApi\ElementInterface|null */ protected $data; /** * Create a new relationship. * - * @param ElementInterface|null $data + * @param \Tobscure\JsonApi\ElementInterface|null $data */ public function __construct(ElementInterface $data = null) { @@ -36,7 +36,7 @@ public function __construct(ElementInterface $data = null) /** * Get the data object. * - * @return ElementInterface|null + * @return \Tobscure\JsonApi\ElementInterface|null */ public function getData() { @@ -46,7 +46,8 @@ public function getData() /** * Set the data object. * - * @param ElementInterface|null $data + * @param \Tobscure\JsonApi\ElementInterface|null $data + * * @return $this */ public function setData($data) diff --git a/src/Resource.php b/src/Resource.php index b8cabd9..b52230b 100644 --- a/src/Resource.php +++ b/src/Resource.php @@ -22,7 +22,7 @@ class Resource implements ElementInterface protected $data; /** - * @var SerializerInterface + * @var \Tobscure\JsonApi\SerializerInterface */ protected $serializer; @@ -43,13 +43,18 @@ class Resource implements ElementInterface /** * An array of Resources that should be merged into this one. * - * @var Resource[] + * @var \Tobscure\JsonApi\Resource[] */ protected $merged = []; + /** + * @var \Tobscure\JsonApi\Relationship[] + */ + private $relationships; + /** * @param mixed $data - * @param SerializerInterface $serializer + * @param \Tobscure\JsonApi\SerializerInterface $serializer */ public function __construct($data, SerializerInterface $serializer) { @@ -72,7 +77,12 @@ public function toArray() { $array = $this->toIdentifier(); - $array['attributes'] = $this->getAttributes(); + if (! $this->isIdentifier()) { + $attributes = $this->getAttributes(); + if ($attributes) { + $array['attributes'] = $attributes; + } + } $relationships = $this->getRelationshipsAsArray(); @@ -123,6 +133,10 @@ public function isIdentifier() */ public function toIdentifier() { + if (! $this->data) { + return; + } + $array = [ 'type' => $this->getType(), 'id' => $this->getId() @@ -194,6 +208,7 @@ protected function getOwnFields() * to the requested fieldset. * * @param array $fields + * * @return array */ protected function filterFields(array $fields) @@ -209,6 +224,7 @@ protected function filterFields(array $fields) * Merge the attributes of merged resources into an array of attributes. * * @param array $attributes + * * @return array */ protected function mergeAttributes(array $attributes) @@ -223,7 +239,7 @@ protected function mergeAttributes(array $attributes) /** * Get the resource relationships. * - * @return Relationship[] + * @return \Tobscure\JsonApi\Relationship[] */ public function getRelationships() { @@ -235,7 +251,7 @@ public function getRelationships() /** * Get the resource relationships without considering requested ones. * - * @return Relationship[] + * @return \Tobscure\JsonApi\Relationship[] */ public function getUnfilteredRelationships() { @@ -259,10 +275,14 @@ public function getRelationshipsAsArray() /** * Get an array of built relationships. * - * @return Relationship[] + * @return \Tobscure\JsonApi\Relationship[] */ protected function buildRelationships() { + if (isset($this->relationships)) { + return $this->relationships; + } + $paths = Util::parseRelationshipPaths($this->includes); $relationships = []; @@ -280,7 +300,7 @@ protected function buildRelationships() } } - return $relationships; + return $this->relationships = $relationships; } /** @@ -288,6 +308,7 @@ protected function buildRelationships() * relationships. * * @param array $relationships + * * @return array */ protected function mergeRelationships(array $relationships) @@ -302,7 +323,8 @@ protected function mergeRelationships(array $relationships) /** * Convert the given array of Relationship objects into an array. * - * @param Relationship[] $relationships + * @param \Tobscure\JsonApi\Relationship[] $relationships + * * @return array */ protected function convertRelationshipsToArray(array $relationships) @@ -315,7 +337,9 @@ protected function convertRelationshipsToArray(array $relationships) /** * Merge a resource into this one. * - * @param Resource $resource + * @param \Tobscure\JsonApi\Resource $resource + * + * @return void */ public function merge(Resource $resource) { @@ -329,6 +353,8 @@ public function with($relationships) { $this->includes = array_unique(array_merge($this->includes, (array) $relationships)); + $this->relationships = null; + return $this; } @@ -352,6 +378,8 @@ public function getData() /** * @param mixed $data + * + * @return void */ public function setData($data) { @@ -359,7 +387,7 @@ public function setData($data) } /** - * @return SerializerInterface + * @return \Tobscure\JsonApi\SerializerInterface */ public function getSerializer() { @@ -367,7 +395,9 @@ public function getSerializer() } /** - * @param SerializerInterface $serializer + * @param \Tobscure\JsonApi\SerializerInterface $serializer + * + * @return void */ public function setSerializer(SerializerInterface $serializer) { diff --git a/src/SerializerInterface.php b/src/SerializerInterface.php index ddd43cb..96730fa 100644 --- a/src/SerializerInterface.php +++ b/src/SerializerInterface.php @@ -17,6 +17,7 @@ interface SerializerInterface * Get the type. * * @param mixed $model + * * @return string */ public function getType($model); @@ -25,6 +26,7 @@ public function getType($model); * Get the id. * * @param mixed $model + * * @return string */ public function getId($model); @@ -34,6 +36,7 @@ public function getId($model); * * @param mixed $model * @param array|null $fields + * * @return array */ public function getAttributes($model, array $fields = null); @@ -42,6 +45,7 @@ public function getAttributes($model, array $fields = null); * Get the links array. * * @param mixed $model + * * @return array */ public function getLinks($model); @@ -50,6 +54,7 @@ public function getLinks($model); * Get the meta. * * @param mixed $model + * * @return array */ public function getMeta($model); @@ -59,6 +64,7 @@ public function getMeta($model); * * @param mixed $model * @param string $name + * * @return \Tobscure\JsonApi\Relationship|null */ public function getRelationship($model, $name); diff --git a/src/Util.php b/src/Util.php index b5d6a28..cac77a3 100644 --- a/src/Util.php +++ b/src/Util.php @@ -26,6 +26,7 @@ class Util * ['user' => ['employer', 'employer.country'], 'comments' => []] * * @param array $paths + * * @return array */ public static function parseRelationshipPaths(array $paths) diff --git a/tests/AbstractSerializerTest.php b/tests/AbstractSerializerTest.php index f0210ba..0ddc86e 100644 --- a/tests/AbstractSerializerTest.php +++ b/tests/AbstractSerializerTest.php @@ -14,6 +14,7 @@ use Tobscure\JsonApi\AbstractSerializer; use Tobscure\JsonApi\Collection; use Tobscure\JsonApi\Relationship; +use Tobscure\JsonApi\Resource; class AbstractSerializerTest extends AbstractTestCase { @@ -41,6 +42,24 @@ public function testGetRelationshipReturnsRelationshipFromMethod() $this->assertTrue($relationship instanceof Relationship); } + public function testGetRelationshipReturnsRelationshipFromMethodUnderscored() + { + $serializer = new PostSerializer1; + + $relationship = $serializer->getRelationship(null, 'parent_post'); + + $this->assertTrue($relationship instanceof Relationship); + } + + public function testGetRelationshipReturnsRelationshipFromMethodKebabCase() + { + $serializer = new PostSerializer1; + + $relationship = $serializer->getRelationship(null, 'parent-post'); + + $this->assertTrue($relationship instanceof Relationship); + } + /** * @expectedException \LogicException */ @@ -68,6 +87,13 @@ public function comments($post) return new Relationship($element); } + public function parentPost($post) + { + $element = new Resource([], new self); + + return new Relationship($element); + } + public function invalid($post) { return 'invalid'; diff --git a/tests/DocumentTest.php b/tests/DocumentTest.php index 6007c58..e1c6f51 100644 --- a/tests/DocumentTest.php +++ b/tests/DocumentTest.php @@ -12,7 +12,9 @@ namespace Tobscure\Tests\JsonApi; use Tobscure\JsonApi\AbstractSerializer; +use Tobscure\JsonApi\Collection; use Tobscure\JsonApi\Document; +use Tobscure\JsonApi\Relationship; use Tobscure\JsonApi\Resource; /** @@ -40,6 +42,37 @@ public function testItCanBeSerializedToJson() { $this->assertEquals('[]', (string) new Document()); } + + public function testToArrayIncludesIncludedResources() + { + $comment = (object) ['id' => 1, 'foo' => 'bar']; + $post = (object) ['id' => 1, 'foo' => 'bar', 'comments' => [$comment]]; + + $resource = new Resource($post, new PostSerializer2); + $includedResource = new Resource($comment, new CommentSerializer2); + + $document = new Document($resource->with('comments')); + + $this->assertEquals([ + 'data' => $resource->toArray(), + 'included' => [ + $includedResource->toArray() + ] + ], $document->toArray()); + } + + public function testNoEmptyAttributes() + { + $post = (object) [ + 'id' => 1, + ]; + + $resource = new Resource($post, new PostSerializerEmptyAttributes2); + + $document = new Document($resource); + + $this->assertEquals('{"data":{"type":"posts","id":"1"}}', (string) $document, 'Attributes should be omitted'); + } } class PostSerializer2 extends AbstractSerializer @@ -50,4 +83,27 @@ public function getAttributes($post, array $fields = null) { return ['foo' => $post->foo]; } + + public function comments($post) + { + return new Relationship(new Collection($post->comments, new CommentSerializer2)); + } +} + +class PostSerializerEmptyAttributes2 extends PostSerializer2 +{ + public function getAttributes($post, array $fields = null) + { + return []; + } +} + +class CommentSerializer2 extends AbstractSerializer +{ + protected $type = 'comments'; + + public function getAttributes($comment, array $fields = null) + { + return ['foo' => $comment->foo]; + } } diff --git a/tests/Exception/Handler/InvalidParameterExceptionHandlerTest.php b/tests/Exception/Handler/InvalidParameterExceptionHandlerTest.php new file mode 100644 index 0000000..eb01ec2 --- /dev/null +++ b/tests/Exception/Handler/InvalidParameterExceptionHandlerTest.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tobscure\Tests\Exception\Handler; + +use Exception; +use Tobscure\JsonApi\Exception\Handler\InvalidParameterExceptionHandler; +use Tobscure\JsonApi\Exception\Handler\ResponseBag; +use Tobscure\JsonApi\Exception\InvalidParameterException; + +class InvalidParameterExceptionHandlerTest extends \PHPUnit_Framework_TestCase +{ + public function testHandlerCanManageInvalidParameterExceptions() + { + $handler = new InvalidParameterExceptionHandler(); + + $this->assertTrue($handler->manages(new InvalidParameterException)); + } + + public function testHandlerCanNotManageOtherExceptions() + { + $handler = new InvalidParameterExceptionHandler(); + + $this->assertFalse($handler->manages(new Exception)); + } + + public function testErrorHandling() + { + $handler = new InvalidParameterExceptionHandler(); + $response = $handler->handle(new InvalidParameterException('error', 1, null, 'include')); + + $this->assertInstanceOf(ResponseBag::class, $response); + $this->assertEquals(400, $response->getStatus()); + $this->assertEquals([['code' => 1, 'source' => ['parameter' => 'include']]], $response->getErrors()); + } +} diff --git a/tests/ParametersTest.php b/tests/ParametersTest.php index a2f0bbe..e38362b 100644 --- a/tests/ParametersTest.php +++ b/tests/ParametersTest.php @@ -34,6 +34,17 @@ public function testGetIncludeReturnsEmptyArray() $this->assertEquals([], $parameters->getInclude(['posts', 'images'])); } + /** + * @expectedException \Tobscure\JsonApi\Exception\InvalidParameterException + * @expectedExceptionCode 1 + */ + public function testGetIncludeWithUnallowedField() + { + $parameters = new Parameters(['include' => 'posts,images']); + + $parameters->getInclude(['posts']); + } + public function testGetSortReturnsArrayOfFieldToSortDirection() { $parameters = new Parameters(['sort' => 'firstname']); @@ -55,6 +66,17 @@ public function testGetSortDefaultsToEmptyArray() $this->assertEmpty($parameters->getSort()); } + /** + * @expectedException \Tobscure\JsonApi\Exception\InvalidParameterException + * @expectedExceptionCode 3 + */ + public function testGetSortWithUnallowedField() + { + $parameters = new Parameters(['sort' => 'firstname,lastname']); + + $parameters->getSort(['firstname']); + } + public function testGetOffsetParsesThePageOffset() { $parameters = new Parameters(['page' => ['offset' => 10]]); @@ -64,12 +86,13 @@ public function testGetOffsetParsesThePageOffset() /** * @expectedException \Tobscure\JsonApi\Exception\InvalidParameterException + * @expectedExceptionCode 2 */ public function testGetOffsetIsAtLeastZero() { $parameters = new Parameters(['page' => ['offset' => -5]]); - $this->assertEquals(0, $parameters->getOffset()); + $parameters->getOffset(); } public function testGetOffsetParsesThePageNumber() diff --git a/tests/ResourceTest.php b/tests/ResourceTest.php index b17ef78..63246ca 100644 --- a/tests/ResourceTest.php +++ b/tests/ResourceTest.php @@ -170,6 +170,44 @@ public function testMetaMergeWithSerializerLinks() ] ], $resource1->toArray()); } + + public function testEmptyToOneRelationships() + { + $post1 = (object) ['id' => '123', 'foo' => 'bar']; + + $resource1 = new Resource($post1, new PostSerializer4()); + $resource1->with('author'); + + $this->assertEquals([ + 'type' => 'posts', + 'id' => '123', + 'attributes' => [ + 'foo' => 'bar' + ], + 'relationships' => [ + 'author' => ['data' => null] + ] + ], $resource1->toArray()); + } + + public function testEmptyToManyRelationships() + { + $post1 = (object) ['id' => '123', 'foo' => 'bar']; + + $resource1 = new Resource($post1, new PostSerializer4()); + $resource1->with('likes'); + + $this->assertEquals([ + 'type' => 'posts', + 'id' => '123', + 'attributes' => [ + 'foo' => 'bar' + ], + 'relationships' => [ + 'likes' => ['data' => []] + ] + ], $resource1->toArray()); + } } class PostSerializer4 extends AbstractSerializer @@ -194,6 +232,16 @@ public function comments($post) { return new Relationship(new Collection($post->comments, new CommentSerializer)); } + + public function author($post) + { + return new Relationship(new Resource(null, new CommentSerializer)); + } + + public function likes($post) + { + return new Relationship(new Collection([], new CommentSerializer)); + } } class PostSerializer4WithLinksAndMeta extends PostSerializer4 {