diff --git a/README.md b/README.md
index 09e675c..b599848 100644
--- a/README.md
+++ b/README.md
@@ -150,14 +150,19 @@ It has support for generating & sending documents with:
- v1.1 extensions via profiles
- v1.1 @-members for JSON-LD and others
+Also there's tools to help processing of incoming requests:
+
+- parse request options (include paths, sparse fieldsets, sort fields, pagination, filtering)
+- parse request documents for creating, updating and deleting resources and relationships
+
Next to custom extensions, the following [official extensions](https://jsonapi.org/extensions/) are included:
- Cursor Pagination ([example code](/examples/cursor_pagination_profile.php), [specification](https://jsonapi.org/profiles/ethanresnick/cursor-pagination/))
Plans for the future include:
-- parse request options: sparse fields, sorting, pagination, filtering ([#44](https://github.com/lode/jsonapi/issues/44))
-- parse requests for creating, updating and deleting resources and relationships ([#5](https://github.com/lode/jsonapi/issues/5))
+- validate request options ([#58](https://github.com/lode/jsonapi/issues/58))
+- validate request documents ([#57](https://github.com/lode/jsonapi/issues/57))
## Contributing
diff --git a/composer.json b/composer.json
index aeb7cc6..be3a040 100644
--- a/composer.json
+++ b/composer.json
@@ -22,6 +22,10 @@
}
},
"require-dev": {
- "phpunit/phpunit": "^5.7|^6.5|^7.5|^8.0"
+ "phpunit/phpunit": "^5.7|^6.5|^7.5|^8.0",
+ "psr/http-message": "^1.0"
+ },
+ "suggests": {
+ "psr/http-message": "Allows constructing requests from Psr RequestInterface"
}
}
diff --git a/composer.lock b/composer.lock
index 1b61b3a..95b64c8 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "893b23bd2e3e1e2a4f08f72b322de18d",
+ "content-hash": "713fd198af3cd27832a25efabe345697",
"packages": [],
"packages-dev": [
{
@@ -703,8 +703,62 @@
"mock",
"xunit"
],
+ "abandoned": true,
"time": "2017-06-30T09:13:00+00:00"
},
+ {
+ "name": "psr/http-message",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
+ "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/master"
+ },
+ "time": "2016-08-06T14:39:51+00:00"
+ },
{
"name": "sebastian/code-unit-reverse-lookup",
"version": "1.0.1",
@@ -1395,5 +1449,6 @@
"platform": {
"php": ">=5.6"
},
- "platform-dev": []
+ "platform-dev": [],
+ "plugin-api-version": "2.0.0"
}
diff --git a/examples/index.html b/examples/index.html
index 45100ac..8c1963c 100644
--- a/examples/index.html
+++ b/examples/index.html
@@ -45,6 +45,7 @@
Relationship responses
Misc
+ - Processing query parameters and request documents
- Null values if explicitly not available
- Meta-only use-cases
- Status-only
diff --git a/examples/request_superglobals.php b/examples/request_superglobals.php
new file mode 100644
index 0000000..fff5992
--- /dev/null
+++ b/examples/request_superglobals.php
@@ -0,0 +1,88 @@
+ 'ship,ship.wing',
+ 'fields' => [
+ 'user' => 'name,location',
+ ],
+ 'sort' => 'name,-location',
+ 'page' => [
+ 'number' => '2',
+ 'size' => '10',
+ ],
+ 'filter' => '42',
+];
+$_POST = [
+ 'data' => [
+ 'type' => 'user',
+ 'id' => '42',
+ 'attributes' => [
+ 'name' => 'Foo',
+ ],
+ 'relationships' => [
+ 'ship' => [
+ 'data' => [
+ 'type' => 'ship',
+ 'id' => '42',
+ ],
+ ],
+ ],
+ ],
+ 'meta' => [
+ 'lock' => true,
+ ],
+];
+
+$_SERVER['REQUEST_SCHEME'] = 'https';
+$_SERVER['HTTP_HOST'] = 'example.org';
+$_SERVER['REQUEST_URI'] = '/user/42?'.http_build_query($_GET);
+$_SERVER['CONTENT_TYPE'] = Document::CONTENT_TYPE_OFFICIAL;
+
+/**
+ * parsing the request
+ *
+ * if you have a PSR request object you can use `$requestParser = RequestParser::fromPsrRequest($request);`
+ */
+$requestParser = RequestParser::fromSuperglobals();
+
+/**
+ * now you can check for certain query parameters and document values in an easy way
+ */
+
+// useful for filling a self link in responses
+var_dump($requestParser->getSelfLink());
+
+// useful for determining how to process the request (list/get/create/update)
+var_dump($requestParser->hasIncludePaths());
+var_dump($requestParser->hasSparseFieldset('user'));
+var_dump($requestParser->hasSortFields());
+var_dump($requestParser->hasPagination());
+var_dump($requestParser->hasFilter());
+
+// these methods often return arrays where comma separated query parameter values are processed for ease of use
+var_dump($requestParser->getIncludePaths());
+var_dump($requestParser->getSparseFieldset('user'));
+var_dump($requestParser->getSortFields());
+var_dump($requestParser->getPagination());
+var_dump($requestParser->getFilter());
+
+// use for determinging whether keys were given without having to dive deep into the POST data yourself
+var_dump($requestParser->hasAttribute('name'));
+var_dump($requestParser->hasRelationship('ship'));
+var_dump($requestParser->hasMeta('lock'));
+
+// get the raw data from the document, this doesn't (yet) return specific objects
+var_dump($requestParser->getAttribute('name'));
+var_dump($requestParser->getRelationship('ship'));
+var_dump($requestParser->getMeta('lock'));
+
+// get the full document for custom processing
+var_dump($requestParser->getDocument());
diff --git a/src/helpers/RequestParser.php b/src/helpers/RequestParser.php
new file mode 100644
index 0000000..156c1d4
--- /dev/null
+++ b/src/helpers/RequestParser.php
@@ -0,0 +1,325 @@
+ true,
+
+ /**
+ * reformat the sort query parameter paths to separate the sort order
+ * this allows easier processing of sort orders and field names
+ */
+ 'useAnnotatedSortFields' => true,
+ ];
+ /** @var string */
+ private $selfLink = '';
+ /** @var array */
+ private $queryParameters = [];
+ /** @var array */
+ private $document = [];
+
+ /**
+ * @param string $selfLink the uri used to make this request {@see getSelfLink()}
+ * @param array $queryParameters all query parameters defined by the specification
+ * @param array $document the request jsonapi document
+ */
+ public function __construct($selfLink='', array $queryParameters=[], array $document=[]) {
+ $this->selfLink = $selfLink;
+ $this->queryParameters = $queryParameters;
+ $this->document = $document;
+ }
+
+ /**
+ * @return self
+ */
+ public static function fromSuperglobals() {
+ $selfLink = $_SERVER['REQUEST_SCHEME'].'://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
+
+ $queryParameters = $_GET;
+
+ $document = $_POST;
+ if ($document === []) {
+ $documentIsJsonapi = (strpos($_SERVER['CONTENT_TYPE'], Document::CONTENT_TYPE_OFFICIAL) !== false);
+ $documentIsJson = (strpos($_SERVER['CONTENT_TYPE'], Document::CONTENT_TYPE_DEBUG) !== false);
+
+ if ($documentIsJsonapi || $documentIsJson) {
+ $document = json_decode(file_get_contents('php://input'), true);
+
+ if ($document === null) {
+ $document = [];
+ }
+ }
+ }
+
+ return new self($selfLink, $queryParameters, $document);
+ }
+
+ /**
+ * @param ServerRequestInterface|RequestInterface $request
+ * @return self
+ */
+ public static function fromPsrRequest(RequestInterface $request) {
+ $selfLink = (string) $request->getUri();
+
+ if ($request instanceof ServerRequestInterface) {
+ $queryParameters = $request->getQueryParams();
+ }
+ else {
+ $queryParameters = [];
+ parse_str($request->getUri()->getQuery(), $queryParameters);
+ }
+
+ if ($request->getBody()->getContents() === '') {
+ $document = [];
+ }
+ else {
+ $document = json_decode($request->getBody()->getContents(), true);
+
+ if ($document === null) {
+ $document = [];
+ }
+ }
+
+ return new self($selfLink, $queryParameters, $document);
+ }
+
+ /**
+ * the full link used to make this request
+ *
+ * this is not a bare self link of a resource and includes query parameters if used
+ *
+ * @return string
+ */
+ public function getSelfLink() {
+ return $this->selfLink;
+ }
+
+ /**
+ * @return boolean
+ */
+ public function hasIncludePaths() {
+ return isset($this->queryParameters['include']);
+ }
+
+ /**
+ * returns a nested array based on the path, or the raw paths
+ *
+ * the nested format allows easier processing on each step of the chain
+ * the raw format allows for custom processing
+ *
+ * @param array $options optional {@see RequestParser::$defaults}
+ * @return string[]|array
+ */
+ public function getIncludePaths(array $options=[]) {
+ $includePaths = explode(',', $this->queryParameters['include']);
+
+ $options = array_merge(self::$defaults, $options);
+ if ($options['useNestedIncludePaths'] === false) {
+ return $includePaths;
+ }
+
+ $restructured = [];
+ foreach ($includePaths as $path) {
+ $steps = explode('.', $path);
+
+ $wrapped = [];
+ while ($steps !== []) {
+ $lastStep = array_pop($steps);
+ $wrapped = [$lastStep => $wrapped];
+ }
+
+ $restructured = array_merge($restructured, $wrapped);
+ }
+
+ return $restructured;
+ }
+
+ /**
+ * @param string $type
+ * @return boolean
+ */
+ public function hasSparseFieldset($type) {
+ return isset($this->queryParameters['fields'][$type]);
+ }
+
+ /**
+ * @param string $type
+ * @return string[]
+ */
+ public function getSparseFieldset($type) {
+ if ($this->queryParameters['fields'][$type] === '') {
+ return [];
+ }
+
+ return explode(',', $this->queryParameters['fields'][$type]);
+ }
+
+ /**
+ * @return boolean
+ */
+ public function hasSortFields() {
+ return isset($this->queryParameters['sort']);
+ }
+
+ /**
+ * returns an array with sort order annotations, or the raw sort fields with minus signs
+ *
+ * the annotated format allows easier processing of sort orders and field names
+ * the raw format allows for custom processing
+ *
+ * @todo return some kind of SortFieldObject
+ *
+ * @param array $options optional {@see RequestParser::$defaults}
+ * @return string[]|array {
+ * @var string $field the sort field, without any minus sign for descending sort order
+ * @var string $order one of the RequestParser::SORT_* constants
+ * }
+ */
+ public function getSortFields(array $options=[]) {
+ $fields = explode(',', $this->queryParameters['sort']);
+
+ $options = array_merge(self::$defaults, $options);
+ if ($options['useAnnotatedSortFields'] === false) {
+ return $fields;
+ }
+
+ $sort = [];
+ foreach ($fields as $field) {
+ $order = RequestParser::SORT_ASCENDING;
+
+ if (strpos($field, '-') === 0) {
+ $field = substr($field, 1);
+ $order = RequestParser::SORT_DESCENDING;
+ }
+
+ $sort[] = [
+ 'field' => $field,
+ 'order' => $order,
+ ];
+ }
+
+ return $sort;
+ }
+
+ /**
+ * @return boolean
+ */
+ public function hasPagination() {
+ return isset($this->queryParameters['page']);
+ }
+
+ /**
+ * @todo return some kind of PaginatorObject which recognizes the strategy of pagination used
+ * e.g. page-based, offset-based, cursor-based, or unknown
+ *
+ * @return array
+ */
+ public function getPagination() {
+ return $this->queryParameters['page'];
+ }
+
+ /**
+ * @return boolean
+ */
+ public function hasFilter() {
+ return isset($this->queryParameters['filter']);
+ }
+
+ /**
+ * @return array
+ */
+ public function getFilter() {
+ return $this->queryParameters['filter'];
+ }
+
+ /**
+ * @param string $attributeName
+ * @return boolean
+ */
+ public function hasAttribute($attributeName) {
+ if (isset($this->document['data']['attributes']) === false) {
+ return false;
+ }
+ if (array_key_exists($attributeName, $this->document['data']['attributes']) === false) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @param string $attributeName
+ * @return mixed
+ */
+ public function getAttribute($attributeName) {
+ return $this->document['data']['attributes'][$attributeName];
+ }
+
+ /**
+ * @param string $relationshipName
+ * @return boolean
+ */
+ public function hasRelationship($relationshipName) {
+ if (isset($this->document['data']['relationships']) === false) {
+ return false;
+ }
+ if (array_key_exists($relationshipName, $this->document['data']['relationships']) === false) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @todo return some kind of read-only ResourceIdentifierObject
+ *
+ * @param string $relationshipName
+ * @return array
+ */
+ public function getRelationship($relationshipName) {
+ return $this->document['data']['relationships'][$relationshipName];
+ }
+
+ /**
+ * @param string $metaKey
+ * @return boolean
+ */
+ public function hasMeta($metaKey) {
+ if (isset($this->document['meta']) === false) {
+ return false;
+ }
+ if (array_key_exists($metaKey, $this->document['meta']) === false) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @param string $metaKey
+ * @return mixed
+ */
+ public function getMeta($metaKey) {
+ return $this->document['meta'][$metaKey];
+ }
+
+ /**
+ * @return array
+ */
+ public function getDocument() {
+ return $this->document;
+ }
+}
diff --git a/tests/helpers/RequestParserTest.php b/tests/helpers/RequestParserTest.php
new file mode 100644
index 0000000..1a45059
--- /dev/null
+++ b/tests/helpers/RequestParserTest.php
@@ -0,0 +1,421 @@
+ 'ship,ship.wing',
+ 'fields' => [
+ 'user' => 'name,location',
+ ],
+ 'sort' => 'name,-location',
+ 'page' => [
+ 'number' => '2',
+ 'size' => '10',
+ ],
+ 'filter' => '42',
+ ];
+
+ $_SERVER['REQUEST_SCHEME'] = 'https';
+ $_SERVER['HTTP_HOST'] = 'example.org';
+ $_SERVER['REQUEST_URI'] = '/user/42?'.http_build_query($_GET);
+ $_SERVER['CONTENT_TYPE'] = Document::CONTENT_TYPE_OFFICIAL;
+
+ $_POST = [
+ 'data' => [
+ 'type' => 'user',
+ 'id' => '42',
+ 'attributes' => [
+ 'name' => 'Foo',
+ ],
+ 'relationships' => [
+ 'ship' => [
+ 'data' => [
+ 'type' => 'ship',
+ 'id' => '42',
+ ],
+ ],
+ ],
+ ],
+ 'meta' => [
+ 'lock' => true,
+ ],
+ ];
+
+ $requestParser = RequestParser::fromSuperglobals();
+
+ $this->assertSame('https://example.org/user/42?'.http_build_query($_GET), $requestParser->getSelfLink());
+
+ $this->assertTrue($requestParser->hasIncludePaths());
+ $this->assertTrue($requestParser->hasSparseFieldset('user'));
+ $this->assertTrue($requestParser->hasSortFields());
+ $this->assertTrue($requestParser->hasPagination());
+ $this->assertTrue($requestParser->hasFilter());
+
+ $this->assertSame(['ship' => ['wing' => []]], $requestParser->getIncludePaths());
+ $this->assertSame(['name', 'location'], $requestParser->getSparseFieldset('user'));
+ $this->assertSame([['field' => 'name', 'order' => RequestParser::SORT_ASCENDING], ['field' => 'location', 'order' => RequestParser::SORT_DESCENDING]], $requestParser->getSortFields());
+ $this->assertSame(['number' => '2', 'size' => '10'], $requestParser->getPagination());
+ $this->assertSame('42', $requestParser->getFilter());
+
+ $this->assertTrue($requestParser->hasAttribute('name'));
+ $this->assertTrue($requestParser->hasRelationship('ship'));
+ $this->assertTrue($requestParser->hasMeta('lock'));
+
+ $this->assertSame('Foo', $requestParser->getAttribute('name'));
+ $this->assertSame(['data' => ['type' => 'ship', 'id' => '42']], $requestParser->getRelationship('ship'));
+ $this->assertSame(true, $requestParser->getMeta('lock'));
+
+ $this->assertSame($_POST, $requestParser->getDocument());
+ }
+
+ public function testFromSuperglobals_WithPhpInputStream() {
+ $_SERVER['REQUEST_SCHEME'] = 'https';
+ $_SERVER['HTTP_HOST'] = 'example.org';
+ $_SERVER['REQUEST_URI'] = '/';
+ $_SERVER['CONTENT_TYPE'] = Document::CONTENT_TYPE_OFFICIAL;
+
+ $_GET = [];
+ $_POST = [];
+
+ $requestParser = RequestParser::fromSuperglobals();
+
+ $this->assertSame([], $requestParser->getDocument());
+ }
+
+ public function testFromPsrRequest_WithRequestInterface() {
+ $queryParameters = [
+ 'include' => 'ship,ship.wing',
+ 'fields' => [
+ 'user' => 'name,location',
+ ],
+ 'sort' => 'name,-location',
+ 'page' => [
+ 'number' => '2',
+ 'size' => '10',
+ ],
+ 'filter' => '42',
+ ];
+ $selfLink = 'https://example.org/user/42?'.http_build_query($queryParameters);
+ $document = [
+ 'data' => [
+ 'type' => 'user',
+ 'id' => '42',
+ 'attributes' => [
+ 'name' => 'Foo',
+ ],
+ 'relationships' => [
+ 'ship' => [
+ 'data' => [
+ 'type' => 'ship',
+ 'id' => '42',
+ ],
+ ],
+ ],
+ ],
+ 'meta' => [
+ 'lock' => true,
+ ],
+ ];
+
+ $request = new TestableNonInterfaceRequestInterface($selfLink, $queryParameters, $document);
+ $requestParser = RequestParser::fromPsrRequest($request);
+
+ $this->assertSame('https://example.org/user/42?'.http_build_query($queryParameters), $requestParser->getSelfLink());
+
+ $this->assertTrue($requestParser->hasIncludePaths());
+ $this->assertTrue($requestParser->hasSparseFieldset('user'));
+ $this->assertTrue($requestParser->hasSortFields());
+ $this->assertTrue($requestParser->hasPagination());
+ $this->assertTrue($requestParser->hasFilter());
+
+ $this->assertSame(['ship' => ['wing' => []]], $requestParser->getIncludePaths());
+ $this->assertSame(['name', 'location'], $requestParser->getSparseFieldset('user'));
+ $this->assertSame([['field' => 'name', 'order' => RequestParser::SORT_ASCENDING], ['field' => 'location', 'order' => RequestParser::SORT_DESCENDING]], $requestParser->getSortFields());
+ $this->assertSame(['number' => '2', 'size' => '10'], $requestParser->getPagination());
+ $this->assertSame('42', $requestParser->getFilter());
+
+ $this->assertTrue($requestParser->hasAttribute('name'));
+ $this->assertTrue($requestParser->hasRelationship('ship'));
+ $this->assertTrue($requestParser->hasMeta('lock'));
+
+ $this->assertSame('Foo', $requestParser->getAttribute('name'));
+ $this->assertSame(['data' => ['type' => 'ship', 'id' => '42']], $requestParser->getRelationship('ship'));
+ $this->assertSame(true, $requestParser->getMeta('lock'));
+
+ $this->assertSame($document, $requestParser->getDocument());
+ }
+
+ public function testFromPsrRequest_WithEmptyDocument() {
+ $selfLink = '';
+ $queryParameters = [];
+ $document = null;
+
+ $request = new TestableNonInterfaceRequestInterface($selfLink, $queryParameters, $document);
+ $requestParser = RequestParser::fromPsrRequest($request);
+
+ $this->assertSame([], $requestParser->getDocument());
+ }
+
+ public function testFromPsrRequest_WithServerRequestInterface() {
+ $queryParameters = [
+ 'sort' => 'name,-location',
+ ];
+ $selfLink = 'https://example.org/user/42?'.http_build_query($queryParameters);
+ $document = [];
+
+ $request = new TestableNonInterfaceServerRequestInterface($selfLink, $queryParameters, $document);
+ $requestParser = RequestParser::fromPsrRequest($request);
+
+ $this->assertSame('https://example.org/user/42?'.http_build_query($queryParameters), $requestParser->getSelfLink());
+ $this->assertTrue($requestParser->hasSortFields());
+ $this->assertSame([['field' => 'name', 'order' => RequestParser::SORT_ASCENDING], ['field' => 'location', 'order' => RequestParser::SORT_DESCENDING]], $requestParser->getSortFields());
+ }
+
+ public function testGetSelfLink() {
+ $requestParser = new RequestParser('https://example.org/');
+ $this->assertSame('https://example.org/', $requestParser->getSelfLink());
+
+ $queryParameters = ['foo' => 'bar'];
+ $selfLink = 'https://example.org/user/42?'.http_build_query($queryParameters);
+
+ $requestParser = new RequestParser($selfLink, $queryParameters);
+ $this->assertSame($selfLink, $requestParser->getSelfLink());
+ }
+
+ public function testHasIncludePaths() {
+ $requestParser = new RequestParser();
+ $this->assertFalse($requestParser->hasIncludePaths());
+
+ $queryParameters = ['include' => 'foo,bar,baz.baf'];
+ $requestParser = new RequestParser($selfLink='', $queryParameters);
+ $this->assertTrue($requestParser->hasIncludePaths());
+ }
+
+ public function testGetIncludePaths_Reformatted() {
+ $queryParameters = ['include' => 'foo,bar,baz.baf'];
+ $requestParser = new RequestParser($selfLink='', $queryParameters);
+ $this->assertSame(['foo' => [], 'bar' => [], 'baz' => ['baf' => []]], $requestParser->getIncludePaths());
+ }
+
+ public function testGetIncludePaths_Raw() {
+ $queryParameters = ['include' => 'foo,bar,baz.baf'];
+ $requestParser = new RequestParser($selfLink='', $queryParameters);
+ $options = ['useNestedIncludePaths' => false];
+ $this->assertSame(['foo', 'bar', 'baz.baf'], $requestParser->getIncludePaths($options));
+ }
+
+ public function testHasSparseFieldset() {
+ $requestParser = new RequestParser();
+ $this->assertFalse($requestParser->hasSparseFieldset('foo'));
+
+ $queryParameters = ['fields' => ['foo' => 'bar']];
+ $requestParser = new RequestParser($selfLink='', $queryParameters);
+ $this->assertTrue($requestParser->hasSparseFieldset('foo'));
+ }
+
+ public function testGetSparseFieldset() {
+ $queryParameters = ['fields' => ['foo' => 'bar,baz']];
+ $requestParser = new RequestParser($selfLink='', $queryParameters);
+ $this->assertSame(['bar', 'baz'], $requestParser->getSparseFieldset('foo'));
+
+ $queryParameters = ['fields' => ['foo' => '']];
+ $requestParser = new RequestParser($selfLink='', $queryParameters);
+ $this->assertSame([], $requestParser->getSparseFieldset('foo'));
+ }
+
+ public function testHasSortFields() {
+ $requestParser = new RequestParser();
+ $this->assertFalse($requestParser->hasSortFields());
+
+ $queryParameters = ['sort' => 'foo'];
+ $requestParser = new RequestParser($selfLink='', $queryParameters);
+ $this->assertTrue($requestParser->hasSortFields());
+ }
+
+ public function testGetSortFields_Reformatted() {
+ $queryParameters = ['sort' => 'foo'];
+ $requestParser = new RequestParser($selfLink='', $queryParameters);
+ $this->assertSame([['field' => 'foo', 'order' => RequestParser::SORT_ASCENDING]], $requestParser->getSortFields());
+
+ $queryParameters = ['sort' => '-bar'];
+ $requestParser = new RequestParser($selfLink='', $queryParameters);
+ $this->assertSame([['field' => 'bar', 'order' => RequestParser::SORT_DESCENDING]], $requestParser->getSortFields());
+
+ $queryParameters = ['sort' => 'foo,-bar'];
+ $requestParser = new RequestParser($selfLink='', $queryParameters);
+ $this->assertSame([['field' => 'foo', 'order' => RequestParser::SORT_ASCENDING], ['field' => 'bar', 'order' => RequestParser::SORT_DESCENDING]], $requestParser->getSortFields());
+ }
+
+ public function testGetSortFields_Raw() {
+ $queryParameters = ['sort' => 'foo,-bar'];
+ $requestParser = new RequestParser($selfLink='', $queryParameters);
+ $options = ['useAnnotatedSortFields' => false];
+ $this->assertSame(['foo', '-bar'], $requestParser->getSortFields($options));
+ }
+
+ public function testHasPagination() {
+ $requestParser = new RequestParser();
+ $this->assertFalse($requestParser->hasPagination());
+
+ $queryParameters = ['page' => ['number' => '2', 'size' => '10']];
+ $requestParser = new RequestParser($selfLink='', $queryParameters);
+ $this->assertTrue($requestParser->hasPagination());
+ }
+
+ public function testGetPagination() {
+ $queryParameters = ['page' => ['number' => '2', 'size' => '10']];
+ $requestParser = new RequestParser($selfLink='', $queryParameters);
+ $this->assertSame(['number' => '2', 'size' => '10'], $requestParser->getPagination());
+ }
+
+ public function testHasFilter() {
+ $requestParser = new RequestParser();
+ $this->assertFalse($requestParser->hasFilter());
+
+ $queryParameters = ['filter' => 'foo'];
+ $requestParser = new RequestParser($selfLink='', $queryParameters);
+ $this->assertTrue($requestParser->hasFilter());
+ }
+
+ public function testGetFilter() {
+ $queryParameters = ['filter' => 'foo'];
+ $requestParser = new RequestParser($selfLink='', $queryParameters);
+ $this->assertSame('foo', $requestParser->getFilter());
+
+ $queryParameters = ['filter' => ['foo' => 'bar']];
+ $requestParser = new RequestParser($selfLink='', $queryParameters);
+ $this->assertSame(['foo' => 'bar'], $requestParser->getFilter());
+ }
+
+ public function testHasAttribute() {
+ $requestParser = new RequestParser();
+ $this->assertFalse($requestParser->hasAttribute('foo'));
+ $this->assertFalse($requestParser->hasAttribute('bar'));
+
+ $document = [
+ 'data' => [
+ 'attributes' => [
+ 'foo' => 'bar',
+ ],
+ ],
+ ];
+
+ $requestParser = new RequestParser($selfLink='', $quaryParameters=[], $document);
+ $this->assertTrue($requestParser->hasAttribute('foo'));
+ $this->assertFalse($requestParser->hasAttribute('bar'));
+ }
+
+ public function testGetAttribute() {
+ $document = [
+ 'data' => [
+ 'attributes' => [
+ 'foo' => 'bar',
+ ],
+ ],
+ ];
+
+ $requestParser = new RequestParser($selfLink='', $quaryParameters=[], $document);
+ $this->assertSame('bar', $requestParser->getAttribute('foo'));
+ }
+
+ public function testHasRelationship() {
+ $requestParser = new RequestParser();
+ $this->assertFalse($requestParser->hasRelationship('foo'));
+ $this->assertFalse($requestParser->hasRelationship('bar'));
+
+ $document = [
+ 'data' => [
+ 'relationships' => [
+ 'foo' => [
+ 'data' => [
+ 'type' => 'bar',
+ 'id' => '42',
+ ],
+ ],
+ ],
+ ],
+ ];
+
+ $requestParser = new RequestParser($selfLink='', $quaryParameters=[], $document);
+ $this->assertTrue($requestParser->hasRelationship('foo'));
+ $this->assertFalse($requestParser->hasRelationship('bar'));
+ }
+
+ public function testGetRelationship() {
+ $document = [
+ 'data' => [
+ 'relationships' => [
+ 'foo' => [
+ 'data' => [
+ 'type' => 'bar',
+ 'id' => '42',
+ ],
+ ],
+ ],
+ ],
+ ];
+
+ $requestParser = new RequestParser($selfLink='', $quaryParameters=[], $document);
+ $this->assertSame(['data' => ['type' => 'bar', 'id' => '42']], $requestParser->getRelationship('foo'));
+ }
+
+ public function testHasMeta() {
+ $requestParser = new RequestParser();
+ $this->assertFalse($requestParser->hasMeta('foo'));
+ $this->assertFalse($requestParser->hasMeta('bar'));
+
+ $document = [
+ 'meta' => [
+ 'foo' => 'bar',
+ ],
+ ];
+
+ $requestParser = new RequestParser($selfLink='', $quaryParameters=[], $document);
+ $this->assertTrue($requestParser->hasMeta('foo'));
+ $this->assertFalse($requestParser->hasMeta('bar'));
+ }
+
+ public function testGetMeta() {
+ $document = [
+ 'meta' => [
+ 'foo' => 'bar',
+ ],
+ ];
+
+ $requestParser = new RequestParser($selfLink='', $quaryParameters=[], $document);
+ $this->assertSame('bar', $requestParser->getMeta('foo'));
+ }
+
+ public function testGetDocument() {
+ $document = [
+ 'data' => [
+ 'attributes' => [
+ 'foo' => 'bar',
+ ],
+ 'relationships' => [
+ 'foo' => [
+ 'data' => [
+ 'type' => 'bar',
+ 'id' => '42',
+ ],
+ ],
+ ],
+ ],
+ 'meta' => [
+ 'foo' => 'bar',
+ ],
+ 'foo' => 'bar',
+ ];
+
+ $requestParser = new RequestParser($selfLink='', $quaryParameters=[], $document);
+ $this->assertSame($document, $requestParser->getDocument());
+ }
+}
diff --git a/tests/helpers/TestableNonInterfaceRequestInterface.php b/tests/helpers/TestableNonInterfaceRequestInterface.php
new file mode 100644
index 0000000..97a332f
--- /dev/null
+++ b/tests/helpers/TestableNonInterfaceRequestInterface.php
@@ -0,0 +1,56 @@
+selfLink = $selfLink;
+ $this->queryParameters = $queryParameters;
+ $this->document = $document;
+ }
+
+ /**
+ * RequestInterface
+ */
+
+ public function getUri() {
+ return new TestableNonInterfaceUriInterface($this->selfLink, $this->queryParameters);
+ }
+
+ // not used in current implementation
+ public function getRequestTarget() {}
+ public function withRequestTarget($requestTarget) {}
+ public function getMethod() {}
+ public function withMethod($method) {}
+ public function withUri(UriInterface $uri, $preserveHost = false) {}
+
+ /**
+ * MessageInterface
+ */
+
+ public function getBody() {
+ return new TestableNonInterfaceStreamInterface($this->document);
+ }
+
+ // not used in current implementation
+ public function getProtocolVersion() {}
+ public function withProtocolVersion($version) {}
+ public function getHeaders() {}
+ public function hasHeader($name) {}
+ public function getHeader($name) {}
+ public function getHeaderLine($name) {}
+ public function withHeader($name, $value) {}
+ public function withAddedHeader($name, $value) {}
+ public function withoutHeader($name) {}
+ public function withBody(StreamInterface $body) {}
+}
diff --git a/tests/helpers/TestableNonInterfaceServerRequestInterface.php b/tests/helpers/TestableNonInterfaceServerRequestInterface.php
new file mode 100644
index 0000000..017a12f
--- /dev/null
+++ b/tests/helpers/TestableNonInterfaceServerRequestInterface.php
@@ -0,0 +1,30 @@
+queryParameters;
+ }
+
+ // not used in current implementation
+ public function getServerParams() {}
+ public function getCookieParams() {}
+ public function withCookieParams(array $cookies) {}
+ public function withQueryParams(array $query) {}
+ public function getUploadedFiles() {}
+ public function withUploadedFiles(array $uploadedFiles) {}
+ public function getParsedBody() {}
+ public function withParsedBody($data) {}
+ public function getAttributes() {}
+ public function getAttribute($name, $default = null) {}
+ public function withAttribute($name, $value) {}
+ public function withoutAttribute($name) {}
+}
diff --git a/tests/helpers/TestableNonInterfaceStreamInterface.php b/tests/helpers/TestableNonInterfaceStreamInterface.php
new file mode 100644
index 0000000..d573988
--- /dev/null
+++ b/tests/helpers/TestableNonInterfaceStreamInterface.php
@@ -0,0 +1,41 @@
+document = $document;
+ }
+
+ /**
+ * StreamInterface
+ */
+
+ public function getContents() {
+ if ($this->document === null) {
+ return '';
+ }
+
+ return (string) json_encode($this->document);
+ }
+
+ // not used in current implementation
+ public function __toString() {}
+ public function close() {}
+ public function detach() {}
+ public function getSize() {}
+ public function tell() {}
+ public function eof() {}
+ public function isSeekable() {}
+ public function seek($offset, $whence = SEEK_SET) {}
+ public function rewind() {}
+ public function isWritable() {}
+ public function write($string) {}
+ public function isReadable() {}
+ public function read($length) {}
+ public function getMetadata($key = null) {}
+}
diff --git a/tests/helpers/TestableNonInterfaceUriInterface.php b/tests/helpers/TestableNonInterfaceUriInterface.php
new file mode 100644
index 0000000..5a1dd89
--- /dev/null
+++ b/tests/helpers/TestableNonInterfaceUriInterface.php
@@ -0,0 +1,43 @@
+selfLink = $selfLink;
+ $this->queryParameters = $queryParameters;
+ }
+
+ /**
+ * UriInterface
+ */
+
+ public function getQuery() {
+ return http_build_query($this->queryParameters);
+ }
+
+ public function __toString() {
+ return $this->selfLink;
+ }
+
+ // not used in current implementation
+ public function getScheme() {}
+ public function getAuthority() {}
+ public function getUserInfo() {}
+ public function getHost() {}
+ public function getPort() {}
+ public function getPath() {}
+ public function getFragment() {}
+ public function withScheme($scheme) {}
+ public function withUserInfo($user, $password = null) {}
+ public function withHost($host) {}
+ public function withPort($port) {}
+ public function withPath($path) {}
+ public function withQuery($query) {}
+ public function withFragment($fragment) {}
+}