├── .editorconfig ├── .gitignore ├── .scrutinizer.yml ├── LICENSE ├── README.md ├── UPGRADE_1_TO_2.md ├── composer.json ├── composer.lock ├── examples ├── atomic_operations_extension.php ├── bootstrap_examples.php ├── collection.php ├── collection_canonical.php ├── cursor_pagination_profile.php ├── errors_all_options.php ├── errors_exception_native.php ├── extension.php ├── index.html ├── meta_only.php ├── null_values.php ├── output.php ├── profile.php ├── relationship_to_many_document.php ├── relationship_to_one_document.php ├── relationships.php ├── request_superglobals.php ├── resource_human_api.php ├── resource_links.php ├── resource_nested_relations.php ├── resource_spec_api.php └── status_only.php ├── phpunit.xml ├── phpunitWithCodeCoverage.xml ├── script ├── test.php └── test_with_coverage.php ├── src ├── CollectionDocument.php ├── DataDocument.php ├── Document.php ├── ErrorsDocument.php ├── MetaDocument.php ├── ResourceDocument.php ├── base.php ├── collection.php ├── error.php ├── errors.php ├── exception.php ├── exceptions │ ├── DuplicateException.php │ ├── Exception.php │ └── InputException.php ├── extensions │ ├── AtomicOperationsDocument.php │ └── AtomicOperationsExtension.php ├── helpers │ ├── AtMemberManager.php │ ├── Converter.php │ ├── ExtensionMemberManager.php │ ├── HttpStatusCodeManager.php │ ├── LinksManager.php │ ├── RequestParser.php │ └── Validator.php ├── interfaces │ ├── DocumentInterface.php │ ├── ExtensionInterface.php │ ├── ObjectInterface.php │ ├── PaginableInterface.php │ ├── ProfileInterface.php │ ├── RecursiveResourceContainerInterface.php │ ├── ResourceContainerInterface.php │ └── ResourceInterface.php ├── objects │ ├── AttributesObject.php │ ├── ErrorObject.php │ ├── JsonapiObject.php │ ├── LinkObject.php │ ├── LinksArray.php │ ├── LinksObject.php │ ├── MetaObject.php │ ├── RelationshipObject.php │ ├── RelationshipsObject.php │ ├── ResourceIdentifierObject.php │ └── ResourceObject.php ├── profiles │ └── CursorPaginationProfile.php ├── resource.php └── response.php └── tests ├── CollectionDocumentTest.php ├── ConverterTest.php ├── DocumentTest.php ├── ErrorsDocumentTest.php ├── ExampleOutputTest.php ├── MetaDocumentTest.php ├── ResourceDocumentTest.php ├── SeparateProcessTest.php ├── TestableNonAbstractDocument.php ├── ValidatorTest.php ├── bootstrap_tests.php ├── example_output ├── ExampleEverywhereExtension.php ├── ExampleTimestampsProfile.php ├── ExampleUser.php ├── ExampleVersionExtension.php ├── at_members_everywhere │ ├── at_members_everywhere.json │ └── at_members_everywhere.php ├── at_members_in_errors │ ├── at_members_in_errors.json │ └── at_members_in_errors.php ├── collection │ ├── collection.json │ └── collection.php ├── collection_canonical │ ├── collection_canonical.json │ └── collection_canonical.php ├── cursor_pagination_profile │ ├── cursor_pagination_profile.json │ └── cursor_pagination_profile.php ├── errors_all_options │ ├── errors_all_options.json │ └── errors_all_options.php ├── errors_exception_native │ ├── errors_exception_native.json │ └── errors_exception_native.php ├── extension │ ├── extension.json │ └── extension.php ├── extension_members_everywhere │ ├── extension_members_everywhere.json │ └── extension_members_everywhere.php ├── meta_only │ ├── meta_only.json │ └── meta_only.php ├── null_values │ ├── null_values.json │ └── null_values.php ├── profile │ ├── profile.json │ └── profile.php ├── relationship_to_many_document │ ├── relationship_to_many_document.json │ └── relationship_to_many_document.php ├── relationship_to_one_document │ ├── relationship_to_one_document.json │ └── relationship_to_one_document.php ├── relationships │ ├── relationships.json │ └── relationships.php ├── resource_document_identifier_only │ ├── resource_document_identifier_only.json │ └── resource_document_identifier_only.php ├── resource_human_api │ ├── resource_human_api.json │ └── resource_human_api.php ├── resource_links │ ├── resource_links.json │ └── resource_links.php ├── resource_nested_relations │ ├── resource_nested_relations.json │ └── resource_nested_relations.php ├── resource_spec_api │ ├── resource_spec_api.json │ └── resource_spec_api.php └── status_only │ ├── status_only.json │ └── status_only.php ├── extensions ├── AtomicOperationsDocumentTest.php └── TestExtension.php ├── helpers ├── AtMemberManagerTest.php ├── ExtensionMemberManagerTest.php ├── HttpStatusCodeManagerTest.php ├── LinksManagerTest.php ├── RequestParserTest.php ├── TestableNonInterfaceRequestInterface.php ├── TestableNonInterfaceServerRequestInterface.php ├── TestableNonInterfaceStreamInterface.php ├── TestableNonInterfaceUriInterface.php ├── TestableNonTraitAtMemberManager.php ├── TestableNonTraitExtensionMemberManager.php ├── TestableNonTraitHttpStatusCodeManager.php └── TestableNonTraitLinksManager.php ├── objects ├── AttributesObjectTest.php ├── ErrorObjectTest.php ├── JsonapiObjectTest.php ├── LinkObjectTest.php ├── LinksArrayTest.php ├── LinksObjectTest.php ├── MetaObjectTest.php ├── RelationshipObjectTest.php ├── RelationshipsObjectTest.php ├── ResourceIdentifierObjectTest.php └── ResourceObjectTest.php └── profiles ├── CursorPaginationProfileTest.php └── TestProfile.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # @see http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = tab 7 | indent_size = 4 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = false 11 | insert_final_newline = true 12 | 13 | [*.yml] 14 | indent_style = space 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | tests/report/ 3 | .phpunit.result.cache 4 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | build: 2 | environment: 3 | php: 8.1 4 | nodes: 5 | coverage: 6 | environment: 7 | php: 8 | ini: 9 | "xdebug.mode": coverage 10 | tests: 11 | override: 12 | - command: php ./script/test_with_coverage.php 13 | coverage: 14 | file: tests/report/clover.xml 15 | format: clover 16 | analysis: 17 | tests: 18 | override: 19 | - php-scrutinizer-run 20 | 21 | filter: 22 | paths: 23 | - src/ 24 | excluded_paths: 25 | - src/base.php 26 | - src/collection.php 27 | - src/error.php 28 | - src/errors.php 29 | - src/exception.php 30 | - src/resource.php 31 | - src/response.php 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Lode Claassen 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 | 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alsvanzelf/jsonapi", 3 | "type": "library", 4 | "description": "Human-friendly library to implement JSON:API without needing to know the specification.", 5 | "keywords": ["jsonapi", "json-api", "api", "json"], 6 | "homepage": "https://github.com/lode/jsonapi", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Lode Claassen", 11 | "email": "lode@alsvanzelf.nl", 12 | "homepage": "https://www.lodeclaassen.nl/" 13 | } 14 | ], 15 | "require": { 16 | "php": ">=5.6", 17 | "ext-json": "*" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "alsvanzelf\\jsonapi\\": "src/", 22 | "alsvanzelf\\jsonapiTests\\": "tests/" 23 | } 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "*", 27 | "psr/http-message": "^1.0" 28 | }, 29 | "suggest": { 30 | "psr/http-message": "Allows constructing requests from Psr RequestInterface" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/atomic_operations_extension.php: -------------------------------------------------------------------------------- 1 | add('name', 'Ford'); 19 | $user2->add('name', 'Arthur'); 20 | $user42->add('name', 'Zaphod'); 21 | 22 | $document->addResults($user1); 23 | $document->addResults($user2); 24 | $document->addResults($user42); 25 | 26 | /** 27 | * get the json 28 | */ 29 | 30 | $options = [ 31 | 'prettyPrint' => true, 32 | ]; 33 | echo '
'.$document->toJson($options); 34 | -------------------------------------------------------------------------------- /examples/bootstrap_examples.php: -------------------------------------------------------------------------------- 1 | [ 17 | 1 => [ 18 | 'title' => 'JSON:API paints my bikeshed!', 19 | 'authorId' => 9, 20 | ], 21 | ], 22 | 'comments' => [ 23 | 5 => [ 24 | 'body' => 'First!', 25 | 'authorId' => 2, 26 | ], 27 | 12 => [ 28 | 'body' => 'I like XML better', 29 | 'authorId' => 9, 30 | ], 31 | ], 32 | 'people' => [ 33 | 9 => [ 34 | 'firstName' => 'Dan', 35 | 'lastName' => 'Gebhardt', 36 | 'twitter' => 'dgeb', 37 | ], 38 | ], 39 | 'user' => [ 40 | 1 => [ 41 | 'name' => 'Ford Prefect', 42 | 'heads' => 1, 43 | ], 44 | 2 => [ 45 | 'name' => 'Arthur Dent', 46 | 'heads' => '1, but not always there', 47 | ], 48 | 42 => [ 49 | 'name' => 'Zaphod Beeblebrox', 50 | 'heads' => 2, 51 | ], 52 | ], 53 | ]; 54 | 55 | public static function getRecord($type, $id) { 56 | if (!isset(self::$records[$type][$id])) { 57 | throw new Exception('sorry, we have a limited dataset'); 58 | } 59 | 60 | return self::$records[$type][$id]; 61 | } 62 | 63 | public static function getEntity($type, $id) { 64 | $record = self::getRecord($type, $id); 65 | 66 | $user = new ExampleUser($id); 67 | foreach ($record as $key => $value) { 68 | $user->$key = $value; 69 | } 70 | 71 | return $user; 72 | } 73 | 74 | public static function findRecords($type) { 75 | return self::$records[$type]; 76 | } 77 | 78 | public static function findEntities($type) { 79 | $records = self::findRecords($type); 80 | $entities = []; 81 | 82 | foreach ($records as $id => $record) { 83 | $entities[$id] = self::getEntity($type, $id); 84 | } 85 | 86 | return $entities; 87 | } 88 | } 89 | 90 | class ExampleUser { 91 | public $id; 92 | public $name; 93 | public $heads; 94 | public $unknown; 95 | 96 | public function __construct($id) { 97 | $this->id = $id; 98 | } 99 | 100 | function getCurrentLocation() { 101 | return 'Earth'; 102 | } 103 | } 104 | 105 | class ExampleVersionExtension implements ExtensionInterface { 106 | /** 107 | * the required method 108 | */ 109 | 110 | public function getOfficialLink() { 111 | return 'https://jsonapi.org/format/1.1/#extension-rules'; 112 | } 113 | 114 | public function getNamespace() { 115 | return 'version'; 116 | } 117 | 118 | /** 119 | * optionally helpers for the specific extension 120 | */ 121 | 122 | public function setVersion(ResourceInterface $resource, $version) { 123 | if ($resource instanceof ResourceDocument) { 124 | $resource->getResource()->addExtensionMember($this, 'id', $version); 125 | } 126 | else { 127 | $resource->addExtensionMember($this, 'id', $version); 128 | } 129 | } 130 | } 131 | 132 | class ExampleTimestampsProfile implements ProfileInterface { 133 | /** 134 | * the required method 135 | */ 136 | 137 | public function getOfficialLink() { 138 | return 'https://jsonapi.org/recommendations/#authoring-profiles'; 139 | } 140 | 141 | /** 142 | * optionally helpers for the specific profile 143 | */ 144 | 145 | public function setTimestamps(ResourceInterface $resource, \DateTimeInterface $created=null, \DateTimeInterface $updated=null) { 146 | if ($resource instanceof ResourceIdentifierObject) { 147 | throw new Exception('cannot add attributes to identifier objects'); 148 | } 149 | 150 | $timestamps = []; 151 | if ($created !== null) { 152 | $timestamps['created'] = $created->format(\DateTime::ISO8601); 153 | } 154 | if ($updated !== null) { 155 | $timestamps['updated'] = $updated->format(\DateTime::ISO8601); 156 | } 157 | 158 | $resource->add('timestamps', $timestamps); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /examples/collection.php: -------------------------------------------------------------------------------- 1 | id); 18 | 19 | if ($user->id == 42) { 20 | $ship = new ResourceObject('ship', 5); 21 | $ship->add('name', 'Heart of Gold'); 22 | $resource->addRelationship('ship', $ship); 23 | } 24 | 25 | $collection[] = $resource; 26 | } 27 | 28 | $document = CollectionDocument::fromResources(...$collection); 29 | 30 | /** 31 | * get the json 32 | */ 33 | 34 | $options = [ 35 | 'prettyPrint' => true, 36 | ]; 37 | echo ''.$document->toJson($options); 38 | -------------------------------------------------------------------------------- /examples/collection_canonical.php: -------------------------------------------------------------------------------- 1 | $articleRecord) { 21 | $authorId = $articleRecord['authorId']; 22 | 23 | $author = ResourceObject::fromArray($peopleRecords[$authorId], 'people', $authorId); 24 | $author->setSelfLink('http://example.com/people/'.$authorId); 25 | $authorRelationshipLinks = [ 26 | 'self' => 'http://example.com/articles/'.$articleId.'/relationships/author', 27 | 'related' => 'http://example.com/articles/'.$articleId.'/author', 28 | ]; 29 | 30 | $comments = []; 31 | foreach ($commentRecords as $commentId => $commentRecord) { 32 | $comment = ResourceObject::fromArray($commentRecord, 'comments', $commentId); 33 | $comment->add('body', $commentRecord['body']); 34 | $comment->addRelationship('author', new ResourceObject('people', $commentRecord['authorId'])); 35 | $comment->setSelfLink('http://example.com/comments/'.$commentId); 36 | 37 | $comments[] = $comment; 38 | } 39 | 40 | $commentsRelationshipLinks = [ 41 | 'self' => 'http://example.com/articles/'.$articleId.'/relationships/comments', 42 | 'related' => 'http://example.com/articles/'.$articleId.'/comments', 43 | ]; 44 | 45 | $article = new ResourceObject('articles', $articleId); 46 | $article->add('title', $articleRecord['title']); 47 | $article->setSelfLink('http://example.com/articles/'.$articleId); 48 | $article->addRelationship('author', $author, $authorRelationshipLinks); 49 | $article->addRelationship('comments', $comments, $commentsRelationshipLinks); 50 | 51 | $document->addResource($article); 52 | } 53 | 54 | $document->setSelfLink('http://example.com/articles'); 55 | $document->setPaginationLinks($previous=null, $next='http://example.com/articles?page[offset]=2', $first=null, $last='http://example.com/articles?page[offset]=10'); 56 | $document->unsetJsonapiObject(); 57 | 58 | /** 59 | * get the json 60 | */ 61 | 62 | $options = [ 63 | 'prettyPrint' => true, 64 | ]; 65 | echo ''.$document->toJson($options); 66 | -------------------------------------------------------------------------------- /examples/cursor_pagination_profile.php: -------------------------------------------------------------------------------- 1 | setCursor($user1, 'ford'); 20 | $profile->setCursor($user2, 'arthur'); 21 | $profile->setCursor($user42, 'zaphod'); 22 | 23 | $document = CollectionDocument::fromResources($user1, $user2, $user42); 24 | $document->applyProfile($profile); 25 | 26 | $profile->setCount($document, $exactTotal=3, $bestGuessTotal=10); 27 | $profile->setLinksFirstPage($document, $currentUrl='/users?sort=42&page[size]=10', $lastCursor='zaphod'); 28 | 29 | /** 30 | * get the json 31 | */ 32 | 33 | $options = [ 34 | 'prettyPrint' => true, 35 | ]; 36 | echo ''.$document->toJson($options); 37 | -------------------------------------------------------------------------------- /examples/errors_all_options.php: -------------------------------------------------------------------------------- 1 | blameJsonPointer($pointer='/data/attributes/title'); 18 | $errorSpecApi->blameQueryParameter($parameter='filter'); 19 | $errorSpecApi->blameHeader($headerName='X-Foo'); 20 | 21 | // an identifier useful for helpdesk purposes 22 | $errorSpecApi->setUniqueIdentifier($id=42); 23 | 24 | // add meta data as you would on a normal json response 25 | $errorSpecApi->addMeta($key='foo', $value='bar'); 26 | 27 | // or as object 28 | $metaObject = new \stdClass(); 29 | $metaObject->property = 'value'; 30 | $errorSpecApi->addMeta($key='object', $metaObject); 31 | 32 | // the http status code 33 | // @note it is better to set this on the jsonapi\errors object .. 34 | // .. as only a single one can be consumed by the browser 35 | $errorSpecApi->setHttpStatusCode($httpStatusCode=404); 36 | 37 | // if not set during construction, set them here 38 | $errorSpecApi->setApplicationCode($genericCode='Invalid input'); 39 | $errorSpecApi->setHumanTitle($genericTitle='Too much options'); 40 | $errorSpecApi->setHumanDetails($specificDetails='Please, choose a bit less. Consult your ...'); 41 | $errorSpecApi->setAboutLink($specificAboutLink='https://www.example.com/explanation.html', ['foo'=>'bar']); 42 | $errorSpecApi->setTypeLink($genericTypeLink='https://www.example.com/documentation.html', ['foo'=>'bar']); 43 | 44 | /** 45 | * prepare multiple error objects for the errors response 46 | */ 47 | 48 | $anotherError = new ErrorObject('kiss', 'Error objects can be small and simple as well.'); 49 | $previousException = new Exception('something went wrong!', 501); 50 | $someException = new Exception('please don\'t throw things', 503, $previousException); 51 | 52 | /** 53 | * building up the json response 54 | * 55 | * you can pass the $error object to the constructor .. 56 | * .. or add multiple errors via ->addErrorObject() or ->addException() 57 | * 58 | * @note exceptions will expose the exception message, code, file, line and trace 59 | * also the code is used as http status code if valid 60 | * 61 | * further you can force another http status code than what's in the errors 62 | */ 63 | 64 | $document = new ErrorsDocument($errorHumanApi); 65 | 66 | $document->addErrorObject($errorSpecApi); 67 | $document->addErrorObject($anotherError); 68 | $document->addException($someException); 69 | $document->add($genericCode='Authentication error', $genericTitle='Not logged in'); 70 | $document->addLink('redirect', '/login', ['label'=>'Log in']); 71 | 72 | $document->setHttpStatusCode(400); 73 | 74 | /** 75 | * sending the response 76 | */ 77 | 78 | $options = [ 79 | 'prettyPrint' => true, 80 | ]; 81 | echo ''.$document->toJson($options); 82 | -------------------------------------------------------------------------------- /examples/errors_exception_native.php: -------------------------------------------------------------------------------- 1 | true, 21 | 'includeExceptionPrevious' => true, 22 | ]; 23 | $document = ErrorsDocument::fromException($e, $options); 24 | 25 | $options = [ 26 | 'prettyPrint' => true, 27 | ]; 28 | echo ''.$document->toJson($options); 29 | } 30 | -------------------------------------------------------------------------------- /examples/extension.php: -------------------------------------------------------------------------------- 1 | applyExtension($extension); 17 | 18 | $document->add('foo', 'bar'); 19 | 20 | /** 21 | * you can apply the rules of the extension manually 22 | * or use methods of the extension if provided 23 | */ 24 | 25 | $extension->setVersion($document, '2019'); 26 | 27 | /** 28 | * get the json 29 | */ 30 | 31 | $contentType = Converter::prepareContentType(Document::CONTENT_TYPE_OFFICIAL, [$extension], []); 32 | echo 'Content-Type: '.$contentType.'
'.PHP_EOL; 33 | 34 | $options = [ 35 | 'prettyPrint' => true, 36 | ]; 37 | echo ''.$document->toJson($options); 38 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |json:api examples 5 | 15 | 16 | 17 |json:api examples
18 | 19 |Single resources
20 |
'.$document->toJson($options); 25 | -------------------------------------------------------------------------------- /examples/null_values.php: -------------------------------------------------------------------------------- 1 | add('foo', null); 17 | $document->addMeta('foo', null); 18 | 19 | // show a specific link is not available 20 | $document->addLink('foo', null); 21 | $document->addLinkObject('bar', new LinkObject()); 22 | 23 | // show a relationship is not set 24 | $document->addRelationship('bar', null); 25 | $document->addRelationshipObject('baz', new RelationshipObject(RelationshipObject::TO_ONE)); 26 | $document->addRelationshipObject('baf', new RelationshipObject(RelationshipObject::TO_MANY)); 27 | 28 | /** 29 | * sending the response 30 | */ 31 | 32 | $options = [ 33 | 'prettyPrint' => true, 34 | ]; 35 | echo ''.$document->toJson($options); 36 | -------------------------------------------------------------------------------- /examples/output.php: -------------------------------------------------------------------------------- 1 | add('foo', 'bar'); 10 | 11 | /** 12 | * get the array 13 | */ 14 | 15 | echo 'Get the array
'; 16 | echo '$document->toArray();'; 17 | echo ''.var_export($document->toArray(), true).''; 18 | 19 | /** 20 | * get the json 21 | */ 22 | 23 | $options = ['prettyPrint' => true]; 24 | echo 'Get the json
'; 25 | echo '$document->toJson();'; 26 | echo ''.var_export($document->toJson($options), true).''; 27 | 28 | /** 29 | * use own json_encode 30 | */ 31 | 32 | $options = ['prettyPrint' => true]; 33 | echo 'Use own
'; 34 | echo 'json_encode()
json_encode($document, JSON_PRETTY_PRINT);'; 35 | echo ''.var_export(json_encode($document, JSON_PRETTY_PRINT), true).''; 36 | 37 | /** 38 | * get custom json (for a non-spec array) 39 | */ 40 | 41 | $customArray = $document->toArray(); 42 | $customArray['custom'] = 'foo'; 43 | 44 | $options = ['prettyPrint' => true, 'array' => $customArray]; 45 | echo 'Get custom json
'; 46 | echo ''; 47 | echo '$customArray = $document->toArray();'.PHP_EOL; 48 | echo '$customArray[\'custom\'] = \'foo\';'.PHP_EOL; 49 | echo '$options = [\'array\' => $customArray];'.PHP_EOL; 50 | echo '$document->toJson($options);'.PHP_EOL; 51 | echo ''; 52 | echo ''.var_export($document->toJson($options), true).''; 53 | 54 | /** 55 | * get jsonp with callback 56 | */ 57 | 58 | $options = ['prettyPrint' => true, 'jsonpCallback' => 'callback']; 59 | echo 'Get jsonp with callback
'; 60 | echo ''; 61 | echo '$options = [\'jsonpCallback\' => \'callback\'];'.PHP_EOL; 62 | echo '$document->toJson($options);'.PHP_EOL; 63 | echo ''; 64 | echo ''.var_export($document->toJson($options), true).''; 65 | 66 | /** 67 | * send json response 68 | */ 69 | 70 | $options = ['prettyPrint' => true, 'contentType' => 'text/html']; 71 | echo 'Send json response
'; 72 | echo '$document->sendResponse();'; 73 | echo ''; 74 | $document->sendResponse($options); 75 | echo ''; 76 | echo 'Also sends http status code ('.$document->getHttpStatusCode().') and headers: [Content-Type: '.Document::CONTENT_TYPE_OFFICIAL.']
'; 77 | -------------------------------------------------------------------------------- /examples/profile.php: -------------------------------------------------------------------------------- 1 | applyProfile($profile); 17 | 18 | $document->add('foo', 'bar'); 19 | 20 | /** 21 | * you can apply the rules of the profile manually 22 | * or use methods of the profile if provided 23 | */ 24 | 25 | $created = new \DateTime('-1 year'); 26 | $updated = new \DateTime('-1 month'); 27 | $profile->setTimestamps($document, $created, $updated); 28 | 29 | /** 30 | * get the json 31 | */ 32 | 33 | $contentType = Converter::prepareContentType(Document::CONTENT_TYPE_OFFICIAL, [], [$profile]); 34 | echo 'Content-Type: '.$contentType.'
'.PHP_EOL; 35 | 36 | $options = [ 37 | 'prettyPrint' => true, 38 | ]; 39 | echo ''.$document->toJson($options); 40 | -------------------------------------------------------------------------------- /examples/relationship_to_many_document.php: -------------------------------------------------------------------------------- 1 | add('tags', 2); 13 | $relationshipDocument->add('tags', 3); 14 | 15 | $relationshipDocument->setSelfLink('/articles/1/relationship/tags'); 16 | $relationshipDocument->addLink('related', '/articles/1/tags'); 17 | 18 | /** 19 | * sending the response 20 | */ 21 | 22 | $options = [ 23 | 'prettyPrint' => true, 24 | ]; 25 | echo ''.$relationshipDocument->toJson($options); 26 | -------------------------------------------------------------------------------- /examples/relationship_to_one_document.php: -------------------------------------------------------------------------------- 1 | setSelfLink('/articles/1/relationship/author', $meta=[], $level=Document::LEVEL_ROOT); 15 | $relationshipDocument->addLink('related', '/articles/1/author'); 16 | 17 | /** 18 | * sending the response 19 | */ 20 | 21 | $options = [ 22 | 'prettyPrint' => true, 23 | ]; 24 | echo ''.$relationshipDocument->toJson($options); 25 | -------------------------------------------------------------------------------- /examples/relationships.php: -------------------------------------------------------------------------------- 1 | add('foo', 'bar'); 18 | 19 | $ship2Resource = new ResourceObject('ship', 42); 20 | $ship2Resource->add('bar', 'baz'); 21 | 22 | $friend1Resource = new ResourceObject('user', 24); 23 | $friend1Resource->add('foo', 'bar'); 24 | 25 | $friend2Resource = new ResourceObject('user', 42); 26 | $friend2Resource->add('bar', 'baz'); 27 | 28 | $dockResource = new ResourceObject('dock', 3); 29 | $dockResource->add('bar', 'baf'); 30 | 31 | /** 32 | * to-one relationship 33 | */ 34 | 35 | $document->addRelationship('included-ship', $ship1Resource); 36 | 37 | /** 38 | * to-one relationship, without included resource 39 | */ 40 | 41 | $options = ['includeContainedResources' => false]; 42 | $document->addRelationship('excluded-ship', $ship2Resource, $links=[], $meta=[], $options); 43 | 44 | /** 45 | * to-many relationship, one-by-one 46 | */ 47 | 48 | $relationshipObject = new RelationshipObject($type=RelationshipObject::TO_MANY); 49 | $relationshipObject->addResource($friend1Resource); 50 | $relationshipObject->addResource($friend2Resource); 51 | 52 | $document->addRelationshipObject('one-by-one-friends', $relationshipObject); 53 | 54 | /** 55 | * to-many relationship, all-at-once 56 | */ 57 | 58 | $friends = new CollectionDocument(); 59 | $friends->addResource($friend1Resource); 60 | $friends->addResource($friend2Resource); 61 | 62 | $document->addRelationship('included-friends', $friends); 63 | 64 | /** 65 | * to-many relationship, different types 66 | */ 67 | 68 | $relationshipObject = new RelationshipObject($type=RelationshipObject::TO_MANY); 69 | $relationshipObject->addResource($ship1Resource); 70 | $relationshipObject->addResource($dockResource); 71 | 72 | $document->addRelationshipObject('one-by-one-neighbours', $relationshipObject); 73 | 74 | /** 75 | * custom 76 | */ 77 | $custom_relation = [ 78 | 'data' => ['cus' => 'tom'], 79 | ]; 80 | $jsonapi->add_relation('custom', $custom_relation); 81 | 82 | /** 83 | * sending the response 84 | */ 85 | 86 | $options = [ 87 | 'prettyPrint' => true, 88 | ]; 89 | echo ''.$document->toJson($options); 90 | -------------------------------------------------------------------------------- /examples/request_superglobals.php: -------------------------------------------------------------------------------- 1 | 'ship,ship.wing', 13 | 'fields' => [ 14 | 'user' => 'name,location', 15 | ], 16 | 'sort' => 'name,-location', 17 | 'page' => [ 18 | 'number' => '2', 19 | 'size' => '10', 20 | ], 21 | 'filter' => '42', 22 | ]; 23 | $_POST = [ 24 | 'data' => [ 25 | 'type' => 'user', 26 | 'id' => '42', 27 | 'attributes' => [ 28 | 'name' => 'Foo', 29 | ], 30 | 'relationships' => [ 31 | 'ship' => [ 32 | 'data' => [ 33 | 'type' => 'ship', 34 | 'id' => '42', 35 | ], 36 | ], 37 | ], 38 | ], 39 | 'meta' => [ 40 | 'lock' => true, 41 | ], 42 | ]; 43 | 44 | $_SERVER['REQUEST_SCHEME'] = 'https'; 45 | $_SERVER['HTTP_HOST'] = 'example.org'; 46 | $_SERVER['REQUEST_URI'] = '/user/42?'.http_build_query($_GET); 47 | $_SERVER['CONTENT_TYPE'] = Document::CONTENT_TYPE_OFFICIAL; 48 | 49 | /** 50 | * parsing the request 51 | * 52 | * if you have a PSR request object you can use `$requestParser = RequestParser::fromPsrRequest($request);` 53 | */ 54 | $requestParser = RequestParser::fromSuperglobals(); 55 | 56 | /** 57 | * now you can check for certain query parameters and document values in an easy way 58 | */ 59 | 60 | // useful for filling a self link in responses 61 | var_dump($requestParser->getSelfLink()); 62 | 63 | // useful for determining how to process the request (list/get/create/update) 64 | var_dump($requestParser->hasIncludePaths()); 65 | var_dump($requestParser->hasSparseFieldset('user')); 66 | var_dump($requestParser->hasSortFields()); 67 | var_dump($requestParser->hasPagination()); 68 | var_dump($requestParser->hasFilter()); 69 | 70 | // these methods often return arrays where comma separated query parameter values are processed for ease of use 71 | var_dump($requestParser->getIncludePaths()); 72 | var_dump($requestParser->getSparseFieldset('user')); 73 | var_dump($requestParser->getSortFields()); 74 | var_dump($requestParser->getPagination()); 75 | var_dump($requestParser->getFilter()); 76 | 77 | // use for determinging whether keys were given without having to dive deep into the POST data yourself 78 | var_dump($requestParser->hasAttribute('name')); 79 | var_dump($requestParser->hasRelationship('ship')); 80 | var_dump($requestParser->hasMeta('lock')); 81 | 82 | // get the raw data from the document, this doesn't (yet) return specific objects 83 | var_dump($requestParser->getAttribute('name')); 84 | var_dump($requestParser->getRelationship('ship')); 85 | var_dump($requestParser->getMeta('lock')); 86 | 87 | // get the full document for custom processing 88 | var_dump($requestParser->getDocument()); 89 | -------------------------------------------------------------------------------- /examples/resource_human_api.php: -------------------------------------------------------------------------------- 1 | id); 24 | $document->add('location', $user1->getCurrentLocation()); 25 | $document->addLink('homepage', 'https://jsonapi.org'); 26 | $document->addMeta('difference', 'is in the code to generate this'); 27 | 28 | $relation = ResourceDocument::fromObject($user42, $type='user', $user42->id); 29 | $document->addRelationship('friend', $relation); 30 | 31 | /** 32 | * get the json 33 | * 34 | * using $document->toJson() here for example purposes 35 | * use $document->sendResponse() to send directly 36 | */ 37 | 38 | $options = [ 39 | 'prettyPrint' => true, 40 | ]; 41 | echo ''.$document->toJson($options).''; 42 | -------------------------------------------------------------------------------- /examples/resource_links.php: -------------------------------------------------------------------------------- 1 | id); 15 | 16 | $selfResourceMeta = ['level' => Document::LEVEL_RESOURCE]; 17 | $partnerMeta = ['level' => Document::LEVEL_RESOURCE]; 18 | $redirectMeta = ['level' => Document::LEVEL_ROOT]; 19 | 20 | $document->setSelfLink('/user/42', $selfResourceMeta); 21 | $document->addLink('partner', '/user/1', $partnerMeta, $level=Document::LEVEL_RESOURCE); 22 | $document->addLink('redirect', '/login', $redirectMeta, $level=Document::LEVEL_ROOT); 23 | 24 | /** 25 | * sending the response 26 | */ 27 | 28 | $options = [ 29 | 'prettyPrint' => true, 30 | ]; 31 | echo ''.$document->toJson($options); 32 | -------------------------------------------------------------------------------- /examples/resource_nested_relations.php: -------------------------------------------------------------------------------- 1 | add('color', 'orange'); 16 | 17 | $wing = new ResourceObject('wing', 1); 18 | $wing->add('side', 'top'); 19 | $wing->addRelationship('flap', $flap); 20 | 21 | $ship = new ResourceObject('ship', 5); 22 | $ship->add('name', 'Heart of Gold'); 23 | $ship->addRelationship('wing', $wing); 24 | 25 | /** 26 | * building up the json response 27 | */ 28 | 29 | $document = ResourceDocument::fromObject($userEntity, $type='user', $userEntity->id); 30 | $document->addRelationship('ship', $ship); 31 | 32 | /** 33 | * sending the response 34 | */ 35 | 36 | $options = [ 37 | 'prettyPrint' => true, 38 | ]; 39 | echo ''.$document->toJson($options); 40 | -------------------------------------------------------------------------------- /examples/resource_spec_api.php: -------------------------------------------------------------------------------- 1 | add('name', $user1->name); 24 | $attributes1->add('heads', $user1->heads); 25 | $attributes1->add('unknown', $user1->unknown); 26 | $attributes1->add('location', $user1->getCurrentLocation()); 27 | 28 | $attributes42 = new AttributesObject(); 29 | $attributes42->add('name', $user42->name); 30 | $attributes42->add('heads', $user42->heads); 31 | $attributes42->add('unknown', $user42->unknown); 32 | 33 | $links = new LinksObject(); 34 | $links->addLinkString('homepage', 'https://jsonapi.org'); 35 | 36 | $meta = new MetaObject(); 37 | $meta->add('difference', 'is in the code to generate this'); 38 | 39 | $resource = new ResourceObject(); 40 | $resource->setId($user42->id); 41 | $resource->setType('user'); 42 | $resource->setAttributesObject($attributes42); 43 | 44 | $relationship = new RelationshipObject(RelationshipObject::TO_ONE); 45 | $relationship->setResource($resource); 46 | $relationships = new RelationshipsObject(); 47 | $relationships->addRelationshipObject('friend', $relationship); 48 | 49 | $document = new ResourceDocument(); 50 | $document->setId($user1->id); 51 | $document->setType('user'); 52 | $document->setAttributesObject($attributes1); 53 | $document->setLinksObject($links); 54 | $document->setMetaObject($meta); 55 | $document->setRelationshipsObject($relationships); 56 | 57 | /** 58 | * get the json 59 | * 60 | * using $document->toJson() here for example purposes 61 | * use $document->sendResponse() to send directly 62 | */ 63 | 64 | $options = [ 65 | 'prettyPrint' => true, 66 | ]; 67 | echo ''.$document->toJson($options).''; 68 | -------------------------------------------------------------------------------- /examples/status_only.php: -------------------------------------------------------------------------------- 1 | setHttpStatusCode(201); 13 | $document->sendResponse(); 14 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 |3 | 7 | -------------------------------------------------------------------------------- /phpunitWithCodeCoverage.xml: -------------------------------------------------------------------------------- 1 | 2 |4 | 6 |tests 5 |3 | 27 | -------------------------------------------------------------------------------- /script/test.php: -------------------------------------------------------------------------------- 1 | true, 28 | ]; 29 | 30 | /** 31 | * human api 32 | */ 33 | 34 | /** 35 | * generate a CollectionDocument from one or multiple resources 36 | * 37 | * adds included resources if found inside the resource's relationships, use {@see ->addResource()} to change that behavior 38 | * 39 | * @param ResourceInterface ...$resources 40 | * @return CollectionDocument 41 | */ 42 | public static function fromResources(ResourceInterface ...$resources) { 43 | $collectionDocument = new self(); 44 | 45 | foreach ($resources as $resource) { 46 | $collectionDocument->addResource($resource); 47 | } 48 | 49 | return $collectionDocument; 50 | } 51 | 52 | /** 53 | * @param string $type 54 | * @param string|int $id 55 | * @param array $attributes optional, if given a ResourceObject is added, otherwise a ResourceIdentifierObject is added 56 | */ 57 | public function add($type, $id, array $attributes=[]) { 58 | if ($attributes === []) { 59 | $this->addResource(new ResourceIdentifierObject($type, $id)); 60 | } 61 | else { 62 | $this->addResource(ResourceObject::fromArray($attributes, $type, $id)); 63 | } 64 | } 65 | 66 | /** 67 | * @inheritDoc 68 | */ 69 | public function setPaginationLinks($previousHref=null, $nextHref=null, $firstHref=null, $lastHref=null) { 70 | if ($previousHref !== null) { 71 | $this->addLink('prev', $previousHref); 72 | } 73 | if ($nextHref !== null) { 74 | $this->addLink('next', $nextHref); 75 | } 76 | if ($firstHref !== null) { 77 | $this->addLink('first', $firstHref); 78 | } 79 | if ($lastHref !== null) { 80 | $this->addLink('last', $lastHref); 81 | } 82 | } 83 | 84 | /** 85 | * spec api 86 | */ 87 | 88 | /** 89 | * add a resource to the collection 90 | * 91 | * adds included resources if found inside the resource's relationships, unless $options['includeContainedResources'] is set to false 92 | * 93 | * @param ResourceInterface $resource 94 | * @param array $options optional {@see CollectionDocument::$defaults} 95 | * 96 | * @throws InputException if the resource is empty 97 | */ 98 | public function addResource(ResourceInterface $resource, array $options=[]) { 99 | if ($resource->getResource()->isEmpty()) { 100 | throw new InputException('does not make sense to add empty resources to a collection'); 101 | } 102 | 103 | $options = array_merge(self::$defaults, $options); 104 | 105 | $this->validator->claimUsedResourceIdentifier($resource); 106 | 107 | $this->resources[] = $resource; 108 | 109 | if ($options['includeContainedResources'] && $resource instanceof RecursiveResourceContainerInterface) { 110 | $this->addIncludedResourceObject(...$resource->getNestedContainedResourceObjects()); 111 | } 112 | } 113 | 114 | /** 115 | * DocumentInterface 116 | */ 117 | 118 | /** 119 | * @inheritDoc 120 | */ 121 | public function toArray() { 122 | $array = parent::toArray(); 123 | 124 | $array['data'] = []; 125 | foreach ($this->resources as $resource) { 126 | $array['data'][] = $resource->getResource()->toArray(); 127 | } 128 | 129 | return $array; 130 | } 131 | 132 | /** 133 | * ResourceContainerInterface 134 | */ 135 | 136 | /** 137 | * @inheritDoc 138 | */ 139 | public function getContainedResources() { 140 | return $this->resources; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/DataDocument.php: -------------------------------------------------------------------------------- 1 | validator = new Validator(); 23 | } 24 | 25 | /** 26 | * human api 27 | */ 28 | 29 | /** 30 | * spec api 31 | */ 32 | 33 | /** 34 | * mainly used when an `included` query parameter is passed 35 | * and resources are requested separate from what is standard for a response 36 | * 37 | * @param ResourceObject ...$resourceObjects 38 | */ 39 | public function addIncludedResourceObject(ResourceObject ...$resourceObjects) { 40 | foreach ($resourceObjects as $resourceObject) { 41 | try { 42 | $this->validator->claimUsedResourceIdentifier($resourceObject); 43 | } 44 | catch (DuplicateException $e) { 45 | // silently skip duplicates 46 | continue; 47 | } 48 | 49 | $this->includedResources[] = $resourceObject; 50 | } 51 | } 52 | 53 | /** 54 | * internal api 55 | */ 56 | 57 | /** 58 | * DocumentInterface 59 | */ 60 | 61 | /** 62 | * @inheritDoc 63 | */ 64 | public function toArray() { 65 | $array = parent::toArray(); 66 | 67 | $array['data'] = null; 68 | 69 | if ($this->includedResources !== []) { 70 | $array['included'] = []; 71 | foreach ($this->includedResources as $resource) { 72 | $array['included'][] = $resource->toArray(); 73 | } 74 | } 75 | 76 | return $array; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/ErrorsDocument.php: -------------------------------------------------------------------------------- 1 | true, 24 | 25 | /** 26 | * add previous exceptions as separate errors when adding exceptions 27 | */ 28 | 'includeExceptionPrevious' => true, 29 | ]; 30 | 31 | /** 32 | * @param ErrorObject $errorObject optional 33 | */ 34 | public function __construct(ErrorObject $errorObject=null) { 35 | parent::__construct(); 36 | 37 | if ($errorObject !== null) { 38 | $this->addErrorObject($errorObject); 39 | } 40 | } 41 | 42 | /** 43 | * human api 44 | */ 45 | 46 | /** 47 | * @param \Exception|\Throwable $exception 48 | * @param array $options optional {@see ErrorsDocument::$defaults} 49 | * @return ErrorsDocument 50 | * 51 | * @throws InputException if $exception is not \Exception or \Throwable 52 | */ 53 | public static function fromException($exception, array $options=[]) { 54 | if ($exception instanceof \Exception === false && $exception instanceof \Throwable === false) { 55 | throw new InputException('input is not a real exception in php5 or php7'); 56 | } 57 | 58 | $options = array_merge(self::$defaults, $options); 59 | 60 | $errorsDocument = new self(); 61 | $errorsDocument->addException($exception, $options); 62 | 63 | return $errorsDocument; 64 | } 65 | 66 | /** 67 | * add an ErrorObject for the given $exception 68 | * 69 | * recursively adds multiple ErrorObjects if $exception carries a ->getPrevious() 70 | * 71 | * @param \Exception|\Throwable $exception 72 | * @param array $options optional {@see ErrorsDocument::$defaults} 73 | * 74 | * @throws InputException if $exception is not \Exception or \Throwable 75 | */ 76 | public function addException($exception, array $options=[]) { 77 | if ($exception instanceof \Exception === false && $exception instanceof \Throwable === false) { 78 | throw new InputException('input is not a real exception in php5 or php7'); 79 | } 80 | 81 | $options = array_merge(self::$defaults, $options); 82 | 83 | $this->addErrorObject(ErrorObject::fromException($exception, $options)); 84 | 85 | if ($options['includeExceptionPrevious']) { 86 | $exception = $exception->getPrevious(); 87 | while ($exception !== null) { 88 | $this->addException($exception, $options); 89 | $exception = $exception->getPrevious(); 90 | } 91 | } 92 | } 93 | 94 | /** 95 | * @param string|int $genericCode developer-friendly code of the generic type of error 96 | * @param string $genericTitle human-friendly title of the generic type of error 97 | * @param string $specificDetails optional, human-friendly explanation of the specific error 98 | * @param string $specificAboutLink optional, human-friendly explanation of the specific error 99 | * @param string $genericTypeLink optional, human-friendly explanation of the generic type of error 100 | */ 101 | public function add($genericCode, $genericTitle, $specificDetails=null, $specificAboutLink=null, $genericTypeLink=null) { 102 | $errorObject = new ErrorObject($genericCode, $genericTitle, $specificDetails, $specificAboutLink, $genericTypeLink); 103 | 104 | $this->addErrorObject($errorObject); 105 | } 106 | 107 | /** 108 | * spec api 109 | */ 110 | 111 | /** 112 | * @note also defines the http status code of the document if the ErrorObject has it defined 113 | * 114 | * @param ErrorObject $errorObject 115 | */ 116 | public function addErrorObject(ErrorObject $errorObject) { 117 | $this->errors[] = $errorObject; 118 | 119 | if ($errorObject->hasHttpStatusCode()) { 120 | $this->setHttpStatusCode($this->determineHttpStatusCode($errorObject->getHttpStatusCode())); 121 | } 122 | } 123 | 124 | /** 125 | * DocumentInterface 126 | */ 127 | 128 | /** 129 | * @inheritDoc 130 | */ 131 | public function toArray() { 132 | $array = parent::toArray(); 133 | 134 | $array['errors'] = []; 135 | foreach ($this->errors as $error) { 136 | if ($error->isEmpty()) { 137 | continue; 138 | } 139 | 140 | $array['errors'][] = $error->toArray(); 141 | } 142 | 143 | return $array; 144 | } 145 | 146 | /** 147 | * internal api 148 | */ 149 | 150 | /** 151 | * @internal 152 | * 153 | * @param string|int $httpStatusCode 154 | * @return int 155 | */ 156 | protected function determineHttpStatusCode($httpStatusCode) { 157 | // add the new code 158 | $category = substr($httpStatusCode, 0, 1); 159 | $this->httpStatusCodes[$category][$httpStatusCode] = true; 160 | 161 | $advisedStatusCode = $httpStatusCode; 162 | 163 | // when there's multiple, give preference to 5xx errors 164 | if (isset($this->httpStatusCodes['5']) && isset($this->httpStatusCodes['4'])) { 165 | // use a generic one 166 | $advisedStatusCode = 500; 167 | } 168 | elseif (isset($this->httpStatusCodes['5'])) { 169 | if (count($this->httpStatusCodes['5']) === 1) { 170 | $advisedStatusCode = key($this->httpStatusCodes['5']); 171 | } 172 | else { 173 | // use a generic one 174 | $advisedStatusCode = 500; 175 | } 176 | } 177 | elseif (isset($this->httpStatusCodes['4'])) { 178 | if (count($this->httpStatusCodes['4']) === 1) { 179 | $advisedStatusCode = key($this->httpStatusCodes['4']); 180 | } 181 | else { 182 | // use a generic one 183 | $advisedStatusCode = 400; 184 | } 185 | } 186 | 187 | return (int) $advisedStatusCode; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/MetaDocument.php: -------------------------------------------------------------------------------- 1 | setMetaObject(MetaObject::fromArray($meta)); 25 | 26 | return $metaDocument; 27 | } 28 | 29 | /** 30 | * @param object $meta 31 | * @return MetaDocument 32 | */ 33 | public static function fromObject($meta) { 34 | $array = Converter::objectToArray($meta); 35 | 36 | return self::fromArray($array); 37 | } 38 | 39 | /** 40 | * wrapper for Document::addMeta() to the primary data of this document available via `add()` 41 | * 42 | * @param string $key 43 | * @param mixed $value 44 | * @param string $level one of the Document::LEVEL_* constants, optional, defaults to Document::LEVEL_ROOT 45 | */ 46 | public function add($key, $value, $level=Document::LEVEL_ROOT) { 47 | parent::addMeta($key, $value, $level); 48 | } 49 | 50 | /** 51 | * spec api 52 | */ 53 | 54 | /** 55 | * DocumentInterface 56 | */ 57 | 58 | /** 59 | * @inheritDoc 60 | */ 61 | public function toArray() { 62 | $array = parent::toArray(); 63 | 64 | // force meta to be set, and be an object when converting to json 65 | if (isset($array['meta']) === false) { 66 | $array['meta'] = new \stdClass(); 67 | } 68 | 69 | return $array; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/base.php: -------------------------------------------------------------------------------- 1 | get_array()) 49 | * - outputs exception details for errors (@see errors->add_exception()) 50 | * 51 | * @note the effects marked with an asterisk (*) are automatically turned on .. 52 | * .. when requested by a human developer (request with an accept header w/o json) 53 | */ 54 | public static $debug = null; 55 | 56 | /** 57 | * the root of the application using jsonapi 58 | * this is currently used to shorten filename of exception traces .. 59 | * .. and thus only used when ::$debug is set to true 60 | */ 61 | public static $appRoot = __DIR__.'/../../../../'; 62 | 63 | /** 64 | * base constructor for all objects 65 | * 66 | * few things are arranged here: 67 | * - determines ::$debug based on the display_errors directive 68 | */ 69 | public function __construct() { 70 | // set debug mode based on display_errors 71 | if (is_null(self::$debug)) { 72 | self::$debug = (bool)ini_get('display_errors'); 73 | } 74 | 75 | self::$appRoot = realpath(self::$appRoot).'/'; 76 | } 77 | 78 | /** 79 | * converting an object to an array 80 | * 81 | * @param object $object by default, its public properties are used 82 | * if it is a \alsvanzelf\jsonapi\resource, its ->get_array() is used 83 | * @return array 84 | */ 85 | protected static function convert_object_to_array($object) { 86 | if (is_object($object) == false) { 87 | throw new \Exception('can only convert objects'); 88 | } 89 | 90 | if ($object instanceof \alsvanzelf\jsonapi\resource) { 91 | return $object->get_array(); 92 | } 93 | 94 | return get_object_vars($object); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/collection.php: -------------------------------------------------------------------------------- 1 | fill_collection() or ->add_resource() 10 | * - self link @see ->set_self_link() 11 | * - output @see ->send_response() or ->get_json() 12 | * 13 | * extra elements 14 | * - meta data @see ->add_meta() or ->fill_meta() 15 | * - included although possible, you should set those via the resource 16 | * 17 | * @deprecated {@see CollectionDocument} 18 | */ 19 | class collection extends response { 20 | 21 | /** 22 | * internal data containers 23 | */ 24 | protected $primary_type = null; 25 | protected $primary_collection = array(); 26 | protected $primary_resource_objects = array(); 27 | 28 | /** 29 | * creates a new collection 30 | * 31 | * @param string $type typically the name of the endpoint or database table 32 | */ 33 | public function __construct($type=null) { 34 | parent::__construct(); 35 | 36 | $this->primary_type = $type; 37 | } 38 | 39 | /** 40 | * get the primary type as set via the constructor 41 | * 42 | * @return string|null 43 | */ 44 | public function get_type() { 45 | return $this->primary_type; 46 | } 47 | 48 | /** 49 | * generates an array for the whole response body 50 | * 51 | * @see jsonapi.org/format 52 | * 53 | * @return array, containing: 54 | * - links 55 | * - data [] 56 | * - {everything from the resource's data-key} 57 | * - included {from the resource's included-key} 58 | * - meta 59 | */ 60 | public function get_array() { 61 | $response = array(); 62 | 63 | // links 64 | if ($this->links) { 65 | $response['links'] = $this->links; 66 | } 67 | 68 | // primary data 69 | $response['data'] = $this->primary_collection; 70 | 71 | // included resources 72 | if ($this->included_data) { 73 | $response['included'] = array_values($this->included_data); 74 | } 75 | 76 | // meta data 77 | if ($this->meta_data) { 78 | $response['meta'] = $this->meta_data; 79 | } 80 | 81 | return $response; 82 | } 83 | 84 | /** 85 | * returns the primary resource objects 86 | * this is used by a resource to add a collection or resource relations 87 | * 88 | * @return array 89 | */ 90 | public function get_resources() { 91 | return $this->primary_resource_objects; 92 | } 93 | 94 | /** 95 | * adds a resource to the primary collection 96 | * this will end up in response.data[] 97 | * 98 | * @note only data and meta(root-level) of a resource are used 99 | * that is its type, id, attributes, relations, links, meta(data-level) 100 | * and meta(root-level) is added to response.meta[] 101 | * further, its included resources are separately added to response.included[] 102 | * 103 | * @see jsonapi\resource 104 | * @see ->fill_collection() for adding a whole array of resources directly 105 | * 106 | * @param \alsvanzelf\jsonapi\resource $resource 107 | * @return void 108 | */ 109 | public function add_resource(\alsvanzelf\jsonapi\resource $resource) { 110 | $resource_array = $resource->get_array(); 111 | 112 | $included_resources = $resource->get_included_resources(); 113 | if (!empty($included_resources)) { 114 | $this->fill_included_resources($included_resources); 115 | } 116 | 117 | // root-level meta-data 118 | if (!empty($resource_array['meta'])) { 119 | $this->fill_meta($resource_array['meta']); 120 | } 121 | 122 | $this->primary_collection[] = $resource_array['data']; 123 | 124 | // make a backup of the actual resource, to pass on as a collection for a relation 125 | $this->primary_resource_objects[] = $resource; 126 | } 127 | 128 | /** 129 | * fills the primary collection with resources 130 | * this will end up in response.data[] 131 | * 132 | * @see ->add_resource() 133 | * 134 | * @param array $resources 135 | * @return void 136 | */ 137 | public function fill_collection($resources) { 138 | foreach ($resources as $resource) { 139 | $this->add_resource($resource); 140 | } 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /src/exception.php: -------------------------------------------------------------------------------- 1 | send_response()) output a errors collection response 8 | * 9 | * @note throwing the exception alone doesn't give you json output 10 | * 11 | * @deprecated {@see ErrorsDocument::fromException()} 12 | */ 13 | class exception extends \Exception { 14 | 15 | /** 16 | * internal data containers 17 | */ 18 | protected $friendly_message; 19 | protected $about_link; 20 | 21 | /** 22 | * custom exception for usage by jsonapi projects 23 | * when echo'd, sends a jsonapi\errors response with the exception in it 24 | * 25 | * can be thrown as a normal exception, optionally with two extra parameters 26 | * 27 | * @param string $message 28 | * @param integer $code optional, defaults to 500 29 | * if using one of the predefined ones in jsonapi\response::STATUS_* 30 | * sends out those as http status 31 | * @param Exception $previous 32 | * @param string $friendly_message optional, which message to output to clients 33 | * the exception $message is hidden unless base::$debug is true 34 | * @param string $about_link optional, a url to send clients to for more explanation 35 | * i.e. a link to the api documentation 36 | */ 37 | public function __construct($message='', $code=0, $previous=null, $friendly_message=null, $about_link=null) { 38 | // exception is the only class not extending base 39 | new base(); 40 | 41 | parent::__construct($message, $code, $previous); 42 | 43 | if ($friendly_message) { 44 | $this->set_friendly_message($friendly_message); 45 | } 46 | if ($about_link) { 47 | $this->set_about_link($about_link); 48 | } 49 | } 50 | 51 | /** 52 | * sets a main user facing message 53 | * 54 | * @see error->set_friendly_message() 55 | */ 56 | public function set_friendly_message($friendly_message) { 57 | $this->friendly_message = $friendly_message; 58 | } 59 | 60 | /** 61 | * sets a link which can help in solving the problem 62 | * 63 | * @see error->set_about_link() 64 | */ 65 | public function set_about_link($about_link) { 66 | $this->about_link = $about_link; 67 | } 68 | 69 | /** 70 | * sends out the json response of an jsonapi\errors object to the browser 71 | * 72 | * @see errors->send_response() 73 | */ 74 | public function send_response($content_type=null, $encode_options=null, $response=null, $jsonp_callback=null) { 75 | $jsonapi = new errors($this, $this->friendly_message, $this->about_link); 76 | $jsonapi->send_response($content_type, $encode_options, $response, $jsonp_callback); 77 | exit; // sanity check 78 | } 79 | 80 | /** 81 | * alias for ->send_response() 82 | * 83 | * @deprecated as this causes hard to debug issues .. 84 | * .. when exceptions are called as a by-effect of this function 85 | * 86 | * @return string empty for sake of correctness 87 | * as ->send_response() already echo's the json and terminates script execution 88 | */ 89 | public function __toString() { 90 | if (base::$debug) { 91 | trigger_error('toString conversion of exception is deprecated, use ->send_response() instead', E_USER_DEPRECATED); 92 | } 93 | 94 | $this->send_response(); 95 | return ''; 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/exceptions/DuplicateException.php: -------------------------------------------------------------------------------- 1 | extension = new AtomicOperationsExtension(); 26 | $this->applyExtension($this->extension); 27 | } 28 | 29 | /** 30 | * add resources as results of the operations 31 | * 32 | * @param ResourceInterface[] ...$resources 33 | */ 34 | public function addResults(ResourceInterface ...$resources) { 35 | $this->results = array_merge($this->results, $resources); 36 | } 37 | 38 | /** 39 | * DocumentInterface 40 | */ 41 | 42 | /** 43 | * @inheritDoc 44 | */ 45 | public function toArray() { 46 | $results = []; 47 | foreach ($this->results as $result) { 48 | $results[] = [ 49 | 'data' => $result->getResource()->toArray(), 50 | ]; 51 | } 52 | 53 | $this->addExtensionMember($this->extension, 'results', $results); 54 | 55 | return parent::toArray(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/extensions/AtomicOperationsExtension.php: -------------------------------------------------------------------------------- 1 | atMembers['@'.$key] = $value; 36 | } 37 | 38 | /** 39 | * internal api 40 | */ 41 | 42 | /** 43 | * @internal 44 | * 45 | * @return boolean 46 | */ 47 | public function hasAtMembers() { 48 | return ($this->atMembers !== []); 49 | } 50 | 51 | /** 52 | * @internal 53 | * 54 | * @return array 55 | */ 56 | public function getAtMembers() { 57 | return $this->atMembers; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/helpers/Converter.php: -------------------------------------------------------------------------------- 1 | toArray(); 20 | } 21 | 22 | return get_object_vars($object); 23 | } 24 | 25 | /** 26 | * @see https://stackoverflow.com/questions/7593969/regex-to-split-camelcase-or-titlecase-advanced/7599674#7599674 27 | * 28 | * @param string $camelCase 29 | * @return string 30 | */ 31 | public static function camelCaseToWords($camelCase) { 32 | $parts = preg_split('/(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/', $camelCase); 33 | 34 | return implode(' ', $parts); 35 | } 36 | 37 | /** 38 | * generates the value for a content type header, with extensions and profiles merged in if available 39 | * 40 | * @param string $contentType 41 | * @param ExtensionInterface[] $extensions 42 | * @param ProfileInterface[] $profiles 43 | * @return string 44 | */ 45 | public static function prepareContentType($contentType, array $extensions, array $profiles) { 46 | if ($extensions !== []) { 47 | $extensionLinks = []; 48 | foreach ($extensions as $extension) { 49 | $extensionLinks[] = $extension->getOfficialLink(); 50 | } 51 | $extensionLinks = implode(' ', $extensionLinks); 52 | 53 | $contentType .= '; ext="'.$extensionLinks.'"'; 54 | } 55 | 56 | if ($profiles !== []) { 57 | $profileLinks = []; 58 | foreach ($profiles as $profile) { 59 | $profileLinks[] = $profile->getOfficialLink(); 60 | } 61 | $profileLinks = implode(' ', $profileLinks); 62 | 63 | $contentType .= '; profile="'.$profileLinks.'"'; 64 | } 65 | 66 | return $contentType; 67 | } 68 | 69 | /** 70 | * @deprecated {@see prepareContentType()} 71 | */ 72 | public static function mergeProfilesInContentType($contentType, array $profiles) { 73 | return self::prepareContentType($contentType, $extensions=[], $profiles); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/helpers/ExtensionMemberManager.php: -------------------------------------------------------------------------------- 1 | getNamespace(); 28 | 29 | if (strpos($key, $namespace.':') === 0) { 30 | $key = substr($key, strlen($namespace.':')); 31 | } 32 | 33 | Validator::checkMemberName($key); 34 | 35 | if (is_object($value)) { 36 | $value = Converter::objectToArray($value); 37 | } 38 | 39 | $this->extensionMembers[$namespace.':'.$key] = $value; 40 | } 41 | 42 | /** 43 | * internal api 44 | */ 45 | 46 | /** 47 | * @internal 48 | * 49 | * @return boolean 50 | */ 51 | public function hasExtensionMembers() { 52 | return ($this->extensionMembers !== []); 53 | } 54 | 55 | /** 56 | * @internal 57 | * 58 | * @return array 59 | */ 60 | public function getExtensionMembers() { 61 | return $this->extensionMembers; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/helpers/HttpStatusCodeManager.php: -------------------------------------------------------------------------------- 1 | httpStatusCode = $httpStatusCode; 27 | } 28 | 29 | /** 30 | * internal api 31 | */ 32 | 33 | /** 34 | * @internal 35 | * 36 | * @return boolean 37 | */ 38 | public function hasHttpStatusCode() { 39 | return ($this->httpStatusCode !== null); 40 | } 41 | 42 | /** 43 | * @internal 44 | * 45 | * @return int 46 | */ 47 | public function getHttpStatusCode() { 48 | return $this->httpStatusCode; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/helpers/LinksManager.php: -------------------------------------------------------------------------------- 1 | ensureLinksObject(); 26 | $this->links->add($key, $href, $meta); 27 | } 28 | 29 | /** 30 | * append a link to a key with an array of links 31 | * 32 | * @deprecated array links are not supported anymore {@see ->addLink()} 33 | * 34 | * @param string $key 35 | * @param string $href 36 | * @param array $meta optional, if given a LinkObject is added, otherwise a link string is added 37 | */ 38 | public function appendLink($key, $href, array $meta=[]) { 39 | $this->ensureLinksObject(); 40 | $this->links->append($key, $href, $meta); 41 | } 42 | 43 | /** 44 | * spec api 45 | */ 46 | 47 | /** 48 | * set a key containing a LinkObject 49 | * 50 | * @param string $key 51 | * @param LinkObject $linkObject 52 | */ 53 | public function addLinkObject($key, LinkObject $linkObject) { 54 | $this->ensureLinksObject(); 55 | $this->links->addLinkObject($key, $linkObject); 56 | } 57 | 58 | /** 59 | * set a key containing a LinksArray 60 | * 61 | * @deprecated array links are not supported anymore {@see ->addLinkObject()} 62 | * 63 | * @param string $key 64 | * @param LinksArray $linksArray 65 | */ 66 | public function addLinksArray($key, LinksArray $linksArray) { 67 | $this->ensureLinksObject(); 68 | $this->links->addLinksArray($key, $linksArray); 69 | } 70 | 71 | /** 72 | * append a LinkObject to a key with a LinksArray 73 | * 74 | * @deprecated array links are not supported anymore {@see ->addLinkObject()} 75 | * 76 | * @param string $key 77 | * @param LinkObject $linkObject 78 | */ 79 | public function appendLinkObject($key, LinkObject $linkObject) { 80 | $this->ensureLinksObject(); 81 | $this->links->appendLinkObject($key, $linkObject); 82 | } 83 | 84 | /** 85 | * set a LinksObject containing all links 86 | * 87 | * @param LinksObject $linksObject 88 | */ 89 | public function setLinksObject(LinksObject $linksObject) { 90 | $this->links = $linksObject; 91 | } 92 | 93 | /** 94 | * internal api 95 | */ 96 | 97 | /** 98 | * @internal 99 | */ 100 | private function ensureLinksObject() { 101 | if ($this->links === null) { 102 | $this->setLinksObject(new LinksObject()); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/helpers/Validator.php: -------------------------------------------------------------------------------- 1 | true, 31 | ]; 32 | 33 | /** 34 | * block if already existing in another object, otherwise just overwrite 35 | * 36 | * @see https://jsonapi.org/format/1.1/#document-resource-object-fields 37 | * 38 | * @param string[] $fieldName 39 | * @param string $objectContainer one of the Validator::OBJECT_CONTAINER_* constants 40 | * @param array $options optional {@see Validator::$defaults} 41 | * 42 | * @throws DuplicateException 43 | */ 44 | public function claimUsedFields(array $fieldNames, $objectContainer, array $options=[]) { 45 | $options = array_merge(self::$defaults, $options); 46 | 47 | foreach ($fieldNames as $fieldName) { 48 | if (isset($this->usedFields[$fieldName]) === false) { 49 | $this->usedFields[$fieldName] = $objectContainer; 50 | continue; 51 | } 52 | if ($this->usedFields[$fieldName] === $objectContainer) { 53 | continue; 54 | } 55 | 56 | /** 57 | * @note this is not allowed by the specification 58 | */ 59 | if ($this->usedFields[$fieldName] === Validator::OBJECT_CONTAINER_TYPE && $options['enforceTypeFieldNamespace'] === false) { 60 | continue; 61 | } 62 | 63 | throw new DuplicateException('field name "'.$fieldName.'" already in use at "data.'.$this->usedFields[$fieldName].'"'); 64 | } 65 | } 66 | 67 | /** 68 | * @param string $objectContainer one of the Validator::OBJECT_CONTAINER_* constants 69 | */ 70 | public function clearUsedFields($objectContainerToClear) { 71 | foreach ($this->usedFields as $fieldName => $containerFound) { 72 | if ($containerFound !== $objectContainerToClear) { 73 | continue; 74 | } 75 | 76 | unset($this->usedFields[$fieldName]); 77 | } 78 | } 79 | 80 | /** 81 | * @param ResourceInterface $resource 82 | * 83 | * @throws InputException if no type or id has been set on the resource 84 | * @throws DuplicateException if the combination of type and id has been set before 85 | */ 86 | public function claimUsedResourceIdentifier(ResourceInterface $resource) { 87 | if ($resource->getResource()->hasIdentification() === false) { 88 | throw new InputException('can not validate resource without identifier, set type and id/lid first'); 89 | } 90 | 91 | $resourceKey = $resource->getResource()->getIdentificationKey(); 92 | if (isset($this->usedResourceIdentifiers[$resourceKey]) === false) { 93 | $this->usedResourceIdentifiers[$resourceKey] = true; 94 | return; 95 | } 96 | 97 | throw new DuplicateException('can not have multiple resources with the same identification'); 98 | } 99 | 100 | /** 101 | * @see https://jsonapi.org/format/1.1/#document-member-names 102 | * 103 | * @todo allow non-url safe chars 104 | * 105 | * @param string $memberName 106 | * 107 | * @throws InputException 108 | */ 109 | public static function checkMemberName($memberName) { 110 | $globallyAllowedCharacters = 'a-zA-Z0-9'; 111 | $generallyAllowedCharacters = $globallyAllowedCharacters.'_-'; 112 | 113 | $regex = '{^ 114 | ( 115 | ['.$globallyAllowedCharacters.'] 116 | 117 | | 118 | 119 | ['.$globallyAllowedCharacters.'] 120 | ['.$generallyAllowedCharacters.']* 121 | ['.$globallyAllowedCharacters.'] 122 | ) 123 | $}x'; 124 | 125 | if (preg_match($regex, $memberName) === 1) { 126 | return; 127 | } 128 | 129 | throw new InputException('invalid member name "'.$memberName.'"'); 130 | } 131 | 132 | /** 133 | * @param string|int $httpStatusCode 134 | * @return boolean 135 | */ 136 | public static function checkHttpStatusCode($httpStatusCode) { 137 | $httpStatusCode = (int) $httpStatusCode; 138 | 139 | if ($httpStatusCode < 100) { 140 | return false; 141 | } 142 | if ($httpStatusCode >= 600) { 143 | return false; 144 | } 145 | 146 | return true; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/interfaces/DocumentInterface.php: -------------------------------------------------------------------------------- 1 | toJson()} 10 | * 11 | * @return array 12 | */ 13 | public function toArray(); 14 | 15 | /** 16 | * generate json with the contents of the document, used by {@see ->sendResponse()} 17 | * 18 | * @param array $options optional 19 | * @return string json 20 | * 21 | * @throws Exception if generating json fails 22 | */ 23 | public function toJson(array $options=[]); 24 | 25 | /** 26 | * send jsonapi response to the browser 27 | * 28 | * @note will set http status code and content type, and echo json 29 | * 30 | * @param array $options optional 31 | */ 32 | public function sendResponse(array $options=[]); 33 | } 34 | -------------------------------------------------------------------------------- /src/interfaces/ExtensionInterface.php: -------------------------------------------------------------------------------- 1 | $value) { 34 | $attributesObject->add($key, $value); 35 | } 36 | 37 | return $attributesObject; 38 | } 39 | 40 | /** 41 | * @param object $attributes 42 | * @return AttributesObject 43 | */ 44 | public static function fromObject($attributes) { 45 | $array = Converter::objectToArray($attributes); 46 | 47 | return self::fromArray($array); 48 | } 49 | 50 | /** 51 | * spec api 52 | */ 53 | 54 | /** 55 | * @param string $key 56 | * @param mixed $value 57 | */ 58 | public function add($key, $value) { 59 | Validator::checkMemberName($key); 60 | 61 | if (is_object($value)) { 62 | $value = Converter::objectToArray($value); 63 | } 64 | 65 | $this->attributes[$key] = $value; 66 | } 67 | 68 | /** 69 | * internal api 70 | */ 71 | 72 | /** 73 | * @internal 74 | * 75 | * @return string[] 76 | */ 77 | public function getKeys() { 78 | return array_keys($this->attributes); 79 | } 80 | 81 | /** 82 | * ObjectInterface 83 | */ 84 | 85 | /** 86 | * @inheritDoc 87 | */ 88 | public function isEmpty() { 89 | if ($this->attributes !== []) { 90 | return false; 91 | } 92 | if ($this->hasAtMembers()) { 93 | return false; 94 | } 95 | if ($this->hasExtensionMembers()) { 96 | return false; 97 | } 98 | 99 | return true; 100 | } 101 | 102 | /** 103 | * @inheritDoc 104 | */ 105 | public function toArray() { 106 | $array = []; 107 | 108 | if ($this->hasAtMembers()) { 109 | $array = array_merge($array, $this->getAtMembers()); 110 | } 111 | if ($this->hasExtensionMembers()) { 112 | $array = array_merge($array, $this->getExtensionMembers()); 113 | } 114 | 115 | return array_merge($array, $this->attributes); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/objects/JsonapiObject.php: -------------------------------------------------------------------------------- 1 | setVersion($version); 31 | } 32 | } 33 | 34 | /** 35 | * human api 36 | */ 37 | 38 | /** 39 | * @param string $key 40 | * @param mixed $value 41 | */ 42 | public function addMeta($key, $value) { 43 | if ($this->meta === null) { 44 | $this->setMetaObject(new MetaObject()); 45 | } 46 | 47 | $this->meta->add($key, $value); 48 | } 49 | 50 | /** 51 | * spec api 52 | */ 53 | 54 | /** 55 | * @param string $version 56 | */ 57 | public function setVersion($version) { 58 | $this->version = $version; 59 | } 60 | 61 | /** 62 | * @param ExtensionInterface $extension 63 | */ 64 | public function addExtension(ExtensionInterface $extension) { 65 | $this->extensions[] = $extension; 66 | } 67 | 68 | /** 69 | * @param ProfileInterface $profile 70 | */ 71 | public function addProfile(ProfileInterface $profile) { 72 | $this->profiles[] = $profile; 73 | } 74 | 75 | /** 76 | * @param MetaObject $metaObject 77 | */ 78 | public function setMetaObject(MetaObject $metaObject) { 79 | $this->meta = $metaObject; 80 | } 81 | 82 | /** 83 | * ObjectInterface 84 | */ 85 | 86 | /** 87 | * @inheritDoc 88 | */ 89 | public function isEmpty() { 90 | if ($this->version !== null) { 91 | return false; 92 | } 93 | if ($this->extensions !== []) { 94 | return false; 95 | } 96 | if ($this->profiles !== []) { 97 | return false; 98 | } 99 | if ($this->meta !== null && $this->meta->isEmpty() === false) { 100 | return false; 101 | } 102 | if ($this->hasAtMembers()) { 103 | return false; 104 | } 105 | if ($this->hasExtensionMembers()) { 106 | return false; 107 | } 108 | 109 | return true; 110 | } 111 | 112 | /** 113 | * @inheritDoc 114 | */ 115 | public function toArray() { 116 | $array = []; 117 | 118 | if ($this->hasAtMembers()) { 119 | $array = array_merge($array, $this->getAtMembers()); 120 | } 121 | if ($this->hasExtensionMembers()) { 122 | $array = array_merge($array, $this->getExtensionMembers()); 123 | } 124 | if ($this->version !== null) { 125 | $array['version'] = $this->version; 126 | } 127 | if ($this->extensions !== []) { 128 | $array['ext'] = []; 129 | foreach ($this->extensions as $extension) { 130 | $array['ext'][] = $extension->getOfficialLink(); 131 | } 132 | } 133 | if ($this->profiles !== []) { 134 | $array['profile'] = []; 135 | foreach ($this->profiles as $profile) { 136 | $array['profile'][] = $profile->getOfficialLink(); 137 | } 138 | } 139 | if ($this->meta !== null && $this->meta->isEmpty() === false) { 140 | $array['meta'] = $this->meta->toArray(); 141 | } 142 | 143 | return $array; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/objects/LinkObject.php: -------------------------------------------------------------------------------- 1 | setHref($href); 35 | } 36 | if ($meta !== []) { 37 | $this->setMetaObject(MetaObject::fromArray($meta)); 38 | } 39 | } 40 | 41 | /** 42 | * human api 43 | */ 44 | 45 | /** 46 | * @param string $href 47 | */ 48 | public function setDescribedBy($href) { 49 | $this->setDescribedByLinkObject(new LinkObject($href)); 50 | } 51 | 52 | /** 53 | * @param string $language 54 | */ 55 | public function addLanguage($language) { 56 | if ($this->hreflang === []) { 57 | $this->setHreflang($language); 58 | } 59 | else { 60 | $this->setHreflang(...array_merge($this->hreflang, [$language])); 61 | } 62 | } 63 | 64 | /** 65 | * @param string $key 66 | * @param mixed $value 67 | */ 68 | public function addMeta($key, $value) { 69 | if ($this->meta === null) { 70 | $this->setMetaObject(new MetaObject()); 71 | } 72 | 73 | $this->meta->add($key, $value); 74 | } 75 | 76 | /** 77 | * spec api 78 | */ 79 | 80 | /** 81 | * @param string $href 82 | */ 83 | public function setHref($href) { 84 | $this->href = $href; 85 | } 86 | 87 | /** 88 | * @todo validate according to https://tools.ietf.org/html/rfc8288#section-2.1 89 | * 90 | * @param string $relationType 91 | */ 92 | public function setRelationType($relationType) { 93 | $this->rel = $relationType; 94 | } 95 | 96 | /** 97 | * @param LinkObject $describedBy 98 | */ 99 | public function setDescribedByLinkObject(LinkObject $describedBy) { 100 | $this->describedby = $describedBy; 101 | } 102 | 103 | /** 104 | * @param string $friendlyTitle 105 | */ 106 | public function setHumanTitle($humanTitle) { 107 | $this->title = $humanTitle; 108 | } 109 | 110 | /** 111 | * @param string $mediaType 112 | */ 113 | public function setMediaType($mediaType) { 114 | $this->type = $mediaType; 115 | } 116 | 117 | /** 118 | * @todo validate according to https://tools.ietf.org/html/rfc5646 119 | * 120 | * @param string ...$hreflang 121 | */ 122 | public function setHreflang(...$hreflang) { 123 | $this->hreflang = $hreflang; 124 | } 125 | 126 | /** 127 | * @param MetaObject $metaObject 128 | */ 129 | public function setMetaObject(MetaObject $metaObject) { 130 | $this->meta = $metaObject; 131 | } 132 | 133 | /** 134 | * ObjectInterface 135 | */ 136 | 137 | /** 138 | * @inheritDoc 139 | */ 140 | public function isEmpty() { 141 | if ($this->href !== null) { 142 | return false; 143 | } 144 | if ($this->rel !== null) { 145 | return false; 146 | } 147 | if ($this->title !== null) { 148 | return false; 149 | } 150 | if ($this->type !== null) { 151 | return false; 152 | } 153 | if ($this->hreflang !== []) { 154 | return false; 155 | } 156 | if ($this->describedby !== null && $this->describedby->isEmpty() === false) { 157 | return false; 158 | } 159 | if ($this->meta !== null && $this->meta->isEmpty() === false) { 160 | return false; 161 | } 162 | if ($this->hasAtMembers()) { 163 | return false; 164 | } 165 | if ($this->hasExtensionMembers()) { 166 | return false; 167 | } 168 | 169 | return true; 170 | } 171 | 172 | /** 173 | * @inheritDoc 174 | */ 175 | public function toArray() { 176 | $array = []; 177 | 178 | if ($this->hasAtMembers()) { 179 | $array = array_merge($array, $this->getAtMembers()); 180 | } 181 | if ($this->hasExtensionMembers()) { 182 | $array = array_merge($array, $this->getExtensionMembers()); 183 | } 184 | 185 | $array['href'] = $this->href; 186 | 187 | if ($this->rel !== null) { 188 | $array['rel'] = $this->rel; 189 | } 190 | if ($this->title !== null) { 191 | $array['title'] = $this->title; 192 | } 193 | if ($this->type !== null) { 194 | $array['type'] = $this->type; 195 | } 196 | if ($this->hreflang !== []) { 197 | if (count($this->hreflang) === 1) { 198 | $array['hreflang'] = $this->hreflang[0]; 199 | } 200 | else { 201 | $array['hreflang'] = $this->hreflang; 202 | } 203 | } 204 | if ($this->describedby !== null && $this->describedby->isEmpty() === false) { 205 | $array['describedby'] = $this->describedby->toArray(); 206 | } 207 | if ($this->meta !== null && $this->meta->isEmpty() === false) { 208 | $array['meta'] = $this->meta->toArray(); 209 | } 210 | 211 | return $array; 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/objects/LinksArray.php: -------------------------------------------------------------------------------- 1 | add($href); 30 | } 31 | 32 | return $linksArray; 33 | } 34 | 35 | /** 36 | * @param object $hrefs 37 | * @return LinksArray 38 | */ 39 | public static function fromObject($hrefs) { 40 | $array = Converter::objectToArray($hrefs); 41 | 42 | return self::fromArray($array); 43 | } 44 | 45 | /** 46 | * @param string $href 47 | * @param array $meta optional, if given a LinkObject is added, otherwise a link string is added 48 | */ 49 | public function add($href, array $meta=[]) { 50 | if ($meta === []) { 51 | $this->addLinkString($href); 52 | } 53 | else { 54 | $this->addLinkObject(new LinkObject($href, $meta)); 55 | } 56 | } 57 | 58 | /** 59 | * spec api 60 | */ 61 | 62 | /** 63 | * @param string $href 64 | */ 65 | public function addLinkString($href) { 66 | $this->links[] = $href; 67 | } 68 | 69 | /** 70 | * @param LinkObject $linkObject 71 | */ 72 | public function addLinkObject(LinkObject $linkObject) { 73 | $this->links[] = $linkObject; 74 | } 75 | 76 | /** 77 | * ObjectInterface 78 | */ 79 | 80 | /** 81 | * @inheritDoc 82 | */ 83 | public function isEmpty() { 84 | return ($this->links === []); 85 | } 86 | 87 | /** 88 | * @inheritDoc 89 | */ 90 | public function toArray() { 91 | $array = []; 92 | 93 | foreach ($this->links as $link) { 94 | if ($link instanceof LinkObject && $link->isEmpty() === false) { 95 | $array[] = $link->toArray(); 96 | } 97 | elseif (is_string($link)) { 98 | $array[] = $link; 99 | } 100 | } 101 | 102 | return $array; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/objects/LinksObject.php: -------------------------------------------------------------------------------- 1 | $href) { 32 | $linksObject->add($key, $href); 33 | } 34 | 35 | return $linksObject; 36 | } 37 | 38 | /** 39 | * @param object $links 40 | * @return LinksObject 41 | */ 42 | public static function fromObject($links) { 43 | $array = Converter::objectToArray($links); 44 | 45 | return self::fromArray($array); 46 | } 47 | 48 | /** 49 | * @param string $key 50 | * @param string $href 51 | * @param array $meta optional, if given a LinkObject is added, otherwise a link string is added 52 | */ 53 | public function add($key, $href, array $meta=[]) { 54 | if ($meta === []) { 55 | $this->addLinkString($key, $href); 56 | } 57 | else { 58 | $this->addLinkObject($key, new LinkObject($href, $meta)); 59 | } 60 | } 61 | 62 | /** 63 | * appends a link to an array of links under a specific key 64 | * 65 | * @see LinksArray for use cases 66 | * 67 | * @deprecated array links are not supported anymore {@see ->add()} 68 | * 69 | * @param string $key 70 | * @param string $href 71 | * @param array $meta optional, if given a LinkObject is added, otherwise a link string is added 72 | * 73 | * @throws DuplicateException if another link is already using that $key but is not an array 74 | */ 75 | public function append($key, $href, array $meta=[]) { 76 | Validator::checkMemberName($key); 77 | 78 | if (isset($this->links[$key]) === false) { 79 | $this->addLinksArray($key, new LinksArray()); 80 | } 81 | elseif ($this->links[$key] instanceof LinksArray === false) { 82 | throw new DuplicateException('can not add to key "'.$key.'", it is not an array of links'); 83 | } 84 | 85 | $this->links[$key]->add($href, $meta); 86 | } 87 | 88 | /** 89 | * spec api 90 | */ 91 | 92 | /** 93 | * @param string $key 94 | * @param string $href 95 | * 96 | * @throws DuplicateException if another link is already using that $key 97 | */ 98 | public function addLinkString($key, $href) { 99 | Validator::checkMemberName($key); 100 | 101 | if (isset($this->links[$key])) { 102 | throw new DuplicateException('link with key "'.$key.'" already set'); 103 | } 104 | 105 | $this->links[$key] = $href; 106 | } 107 | 108 | /** 109 | * @param string $key 110 | * @param LinkObject $linkObject 111 | * 112 | * @throws DuplicateException if another link is already using that $key 113 | */ 114 | public function addLinkObject($key, LinkObject $linkObject) { 115 | Validator::checkMemberName($key); 116 | 117 | if (isset($this->links[$key])) { 118 | throw new DuplicateException('link with key "'.$key.'" already set'); 119 | } 120 | 121 | $this->links[$key] = $linkObject; 122 | } 123 | 124 | /** 125 | * @deprecated array links are not supported anymore {@see ->addLinkObject()} 126 | * 127 | * @param string $key 128 | * @param LinksArray $linksArray 129 | * 130 | * @throws DuplicateException if another link is already using that $key 131 | */ 132 | public function addLinksArray($key, LinksArray $linksArray) { 133 | Validator::checkMemberName($key); 134 | 135 | if (isset($this->links[$key])) { 136 | throw new DuplicateException('link with key "'.$key.'" already set'); 137 | } 138 | 139 | $this->links[$key] = $linksArray; 140 | } 141 | 142 | /** 143 | * @deprecated array links are not supported anymore {@see ->addLinkObject()} 144 | * 145 | * @param string $key 146 | * @param LinkObject $linkObject 147 | * 148 | * @throws DuplicateException if another link is already using that $key but is not an array 149 | */ 150 | public function appendLinkObject($key, LinkObject $linkObject) { 151 | Validator::checkMemberName($key); 152 | 153 | if (isset($this->links[$key]) === false) { 154 | $this->addLinksArray($key, new LinksArray()); 155 | } 156 | elseif ($this->links[$key] instanceof LinksArray === false) { 157 | throw new DuplicateException('can not add to key "'.$key.'", it is not an array of links'); 158 | } 159 | 160 | $this->links[$key]->addLinkObject($linkObject); 161 | } 162 | 163 | /** 164 | * ObjectInterface 165 | */ 166 | 167 | /** 168 | * @inheritDoc 169 | */ 170 | public function isEmpty() { 171 | if ($this->links !== []) { 172 | return false; 173 | } 174 | if ($this->hasAtMembers()) { 175 | return false; 176 | } 177 | if ($this->hasExtensionMembers()) { 178 | return false; 179 | } 180 | 181 | return true; 182 | } 183 | 184 | /** 185 | * @inheritDoc 186 | */ 187 | public function toArray() { 188 | $array = []; 189 | 190 | if ($this->hasAtMembers()) { 191 | $array = array_merge($array, $this->getAtMembers()); 192 | } 193 | if ($this->hasExtensionMembers()) { 194 | $array = array_merge($array, $this->getExtensionMembers()); 195 | } 196 | 197 | foreach ($this->links as $key => $link) { 198 | if ($link instanceof LinkObject && $link->isEmpty() === false) { 199 | $array[$key] = $link->toArray(); 200 | } 201 | elseif ($link instanceof LinksArray && $link->isEmpty() === false) { 202 | $array[$key] = $link->toArray(); 203 | } 204 | elseif ($link instanceof LinkObject && $link->isEmpty()) { 205 | $array[$key] = null; 206 | } 207 | else { // string or null 208 | $array[$key] = $link; 209 | } 210 | } 211 | 212 | return $array; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/objects/MetaObject.php: -------------------------------------------------------------------------------- 1 | $value) { 29 | $metaObject->add($key, $value); 30 | } 31 | 32 | return $metaObject; 33 | } 34 | 35 | /** 36 | * @param object $meta 37 | * @return MetaObject 38 | */ 39 | public static function fromObject($meta) { 40 | $array = Converter::objectToArray($meta); 41 | 42 | return self::fromArray($array); 43 | } 44 | 45 | /** 46 | * spec api 47 | */ 48 | 49 | /** 50 | * @param string $key 51 | * @param mixed $value 52 | */ 53 | public function add($key, $value) { 54 | Validator::checkMemberName($key); 55 | 56 | if (is_object($value)) { 57 | $value = Converter::objectToArray($value); 58 | } 59 | 60 | $this->meta[$key] = $value; 61 | } 62 | 63 | /** 64 | * ObjectInterface 65 | */ 66 | 67 | /** 68 | * @inheritDoc 69 | */ 70 | public function isEmpty() { 71 | if ($this->meta !== []) { 72 | return false; 73 | } 74 | if ($this->hasAtMembers()) { 75 | return false; 76 | } 77 | if ($this->hasExtensionMembers()) { 78 | return false; 79 | } 80 | 81 | return true; 82 | } 83 | 84 | /** 85 | * @inheritDoc 86 | */ 87 | public function toArray() { 88 | $array = []; 89 | 90 | if ($this->hasAtMembers()) { 91 | $array = array_merge($array, $this->getAtMembers()); 92 | } 93 | if ($this->hasExtensionMembers()) { 94 | $array = array_merge($array, $this->getExtensionMembers()); 95 | } 96 | 97 | return array_merge($array, $this->meta); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/objects/RelationshipsObject.php: -------------------------------------------------------------------------------- 1 | addRelationshipObject($key, $relationshipObject); 36 | 37 | return $relationshipObject; 38 | } 39 | 40 | /** 41 | * spec api 42 | */ 43 | 44 | /** 45 | * @param string $key 46 | * @param RelationshipObject $relationshipObject 47 | * 48 | * @throws DuplicateException if another relationship is already using that $key 49 | */ 50 | public function addRelationshipObject($key, RelationshipObject $relationshipObject) { 51 | Validator::checkMemberName($key); 52 | 53 | if (isset($this->relationships[$key])) { 54 | throw new DuplicateException('relationship with key "'.$key.'" already set'); 55 | } 56 | 57 | $this->relationships[$key] = $relationshipObject; 58 | } 59 | 60 | /** 61 | * internal api 62 | */ 63 | 64 | /** 65 | * @internal 66 | * 67 | * @return string[] 68 | */ 69 | public function getKeys() { 70 | return array_keys($this->relationships); 71 | } 72 | 73 | /** 74 | * ObjectInterface 75 | */ 76 | 77 | /** 78 | * @inheritDoc 79 | */ 80 | public function isEmpty() { 81 | if ($this->relationships !== []) { 82 | return false; 83 | } 84 | if ($this->hasAtMembers()) { 85 | return false; 86 | } 87 | if ($this->hasExtensionMembers()) { 88 | return false; 89 | } 90 | 91 | return true; 92 | } 93 | 94 | /** 95 | * @inheritDoc 96 | */ 97 | public function toArray() { 98 | $array = []; 99 | 100 | if ($this->hasAtMembers()) { 101 | $array = array_merge($array, $this->getAtMembers()); 102 | } 103 | if ($this->hasExtensionMembers()) { 104 | $array = array_merge($array, $this->getExtensionMembers()); 105 | } 106 | 107 | foreach ($this->relationships as $key => $relationshipObject) { 108 | $array[$key] = $relationshipObject->toArray(); 109 | } 110 | 111 | return $array; 112 | } 113 | 114 | /** 115 | * RecursiveResourceContainerInterface 116 | */ 117 | 118 | /** 119 | * @inheritDoc 120 | */ 121 | public function getNestedContainedResourceObjects() { 122 | $resourceObjects = []; 123 | 124 | foreach ($this->relationships as $relationship) { 125 | $resourceObjects = array_merge($resourceObjects, $relationship->getNestedContainedResourceObjects()); 126 | } 127 | 128 | return $resourceObjects; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /tests/ConverterTest.php: -------------------------------------------------------------------------------- 1 | foo = 'bar'; 15 | $object->baz = 42; 16 | 17 | $array = Converter::objectToArray($object); 18 | 19 | $this->assertCount(2, $array); 20 | $this->assertArrayHasKey('foo', $array); 21 | $this->assertArrayHasKey('baz', $array); 22 | $this->assertSame('bar', $array['foo']); 23 | $this->assertSame(42, $array['baz']); 24 | } 25 | 26 | public function testObjectToArray_MethodsAndPrivateProperties() { 27 | $object = new TestObject(); 28 | $array = Converter::objectToArray($object); 29 | 30 | $this->assertCount(2, $array); 31 | $this->assertArrayHasKey('foo', $array); 32 | $this->assertArrayHasKey('baz', $array); 33 | $this->assertArrayNotHasKey('secret', $array); 34 | $this->assertArrayNotHasKey('method', $array); 35 | } 36 | 37 | public function testObjectToArray_FromInternalObject() { 38 | $values = ['foo'=>'bar', 'baz'=>42]; 39 | $attributesObject = AttributesObject::fromArray($values); 40 | 41 | $array = Converter::objectToArray($attributesObject); 42 | 43 | $this->assertCount(2, $array); 44 | $this->assertArrayHasKey('foo', $array); 45 | $this->assertArrayHasKey('baz', $array); 46 | $this->assertSame('bar', $array['foo']); 47 | $this->assertSame(42, $array['baz']); 48 | } 49 | 50 | /** 51 | * @dataProvider dataProviderCamelCaseToWords_HappyPath 52 | */ 53 | public function testCamelCaseToWords_HappyPath($camelCase, $expectedOutput) { 54 | $this->assertSame($expectedOutput, Converter::camelCaseToWords($camelCase)); 55 | } 56 | 57 | public function dataProviderCamelCaseToWords_HappyPath() { 58 | return [ 59 | ['value', 'value'], 60 | ['camelValue', 'camel Value'], 61 | ['TitleValue', 'Title Value'], 62 | ['VALUE', 'VALUE'], 63 | ['eclipseRCPExt', 'eclipse RCP Ext'], 64 | ]; 65 | } 66 | 67 | /** 68 | * @group Extensions 69 | * @group Profiles 70 | */ 71 | public function testPrepareContentType_HappyPath() { 72 | $this->assertSame('foo', Converter::prepareContentType('foo', [], [])); 73 | } 74 | 75 | /** 76 | * @group Extensions 77 | */ 78 | public function testPrepareContentType_WithExtensionStringLink() { 79 | $extension = new TestExtension(); 80 | $extension->setOfficialLink('bar'); 81 | 82 | $this->assertSame('foo; ext="bar"', Converter::prepareContentType('foo', [$extension], [])); 83 | } 84 | 85 | /** 86 | * @group Profiles 87 | */ 88 | public function testPrepareContentType_WithProfileStringLink() { 89 | $profile = new TestProfile(); 90 | $profile->setOfficialLink('bar'); 91 | 92 | $this->assertSame('foo; profile="bar"', Converter::prepareContentType('foo', [], [$profile])); 93 | } 94 | 95 | /** 96 | * @group Extensions 97 | * @group Profiles 98 | */ 99 | public function testPrepareContentType_WithMultipleExtensionsAndProfiles() { 100 | $extension1 = new TestExtension(); 101 | $extension1->setOfficialLink('bar'); 102 | 103 | $extension2 = new TestExtension(); 104 | $extension2->setOfficialLink('baz'); 105 | 106 | $profile1 = new TestProfile(); 107 | $profile1->setOfficialLink('bar'); 108 | 109 | $profile2 = new TestProfile(); 110 | $profile2->setOfficialLink('baz'); 111 | 112 | $this->assertSame('foo; ext="bar baz"; profile="bar baz"', Converter::prepareContentType('foo', [$extension1, $extension2], [$profile1, $profile2])); 113 | } 114 | 115 | /** 116 | * test method while it is part of the interface 117 | * @group Profiles 118 | */ 119 | public function testMergeProfilesInContentType_HappyPath() { 120 | $profile = new TestProfile(); 121 | $profile->setOfficialLink('bar'); 122 | 123 | $this->assertSame('foo; profile="bar"', Converter::mergeProfilesInContentType('foo', [$profile])); 124 | } 125 | } 126 | 127 | class TestObject { 128 | public $foo = 'bar'; 129 | public $baz = 42; 130 | private $secret = 'value'; 131 | public function method() {} 132 | } 133 | -------------------------------------------------------------------------------- /tests/ExampleOutputTest.php: -------------------------------------------------------------------------------- 1 | true, 13 | ]; 14 | 15 | /** 16 | * @dataProvider dataProviderTestOutput 17 | */ 18 | public function testOutput($generator, $expectedJson, array $options=[], $testName=null) { 19 | $options = array_merge(self::$defaults, $options); 20 | 21 | $document = $generator::createJsonapiDocument(); 22 | $actualJson = $document->toJson($options); 23 | 24 | // adhere to editorconfig 25 | $actualJson = str_replace(' ', "\t", $actualJson).PHP_EOL; 26 | 27 | // create new cases 28 | $actualJsonPath = __DIR__.'/example_output/'.$testName.'/'.$testName.'.json'; 29 | if ($expectedJson === null && file_exists($actualJsonPath) === false) { 30 | file_put_contents($actualJsonPath, $actualJson); 31 | $this->markTestSkipped('no stored json to test against, try again'); 32 | return; 33 | } 34 | 35 | $this->assertSame($expectedJson, $actualJson); 36 | } 37 | 38 | public function dataProviderTestOutput() { 39 | $directories = glob(__DIR__.'/example_output/*', GLOB_ONLYDIR); 40 | 41 | $testCases = []; 42 | foreach ($directories as $directory) { 43 | $testName = basename($directory); 44 | $className = '\\alsvanzelf\\jsonapiTests\\example_output\\'.$testName.'\\'.$testName; 45 | 46 | require $directory.'/'.$testName.'.php'; 47 | 48 | $generator = new $className; 49 | $expectedJson = null; 50 | $options = []; 51 | 52 | if (file_exists($directory.'/'.$testName.'.json')) { 53 | $expectedJson = file_get_contents($directory.'/'.$testName.'.json'); 54 | } 55 | if (file_exists($directory.'/options.txt')) { 56 | $options = json_decode(file_get_contents($directory.'/options.txt'), true); 57 | } 58 | 59 | $testCases[$testName] = [$generator, $expectedJson, $options, $testName]; 60 | } 61 | 62 | return $testCases; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/MetaDocumentTest.php: -------------------------------------------------------------------------------- 1 | toArray(); 13 | $this->assertArrayHasKey('meta', $array); 14 | 15 | // verify meta is an object, not an array 16 | $json = $document->toJson(); 17 | $this->assertSame('{"jsonapi":{"version":"1.1"},"meta":{}}', $json); 18 | } 19 | 20 | public function testFromArray_HappyPath() { 21 | $document = MetaDocument::fromArray(['foo' => 'bar']); 22 | 23 | $array = $document->toArray(); 24 | 25 | $this->assertArrayHasKey('meta', $array); 26 | $this->assertCount(1, $array['meta']); 27 | $this->assertArrayHasKey('foo', $array['meta']); 28 | $this->assertSame('bar', $array['meta']['foo']); 29 | } 30 | 31 | public function testFromObject_HappyPath() { 32 | $object = new \stdClass(); 33 | $object->foo = 'bar'; 34 | 35 | $document = MetaDocument::fromObject($object); 36 | 37 | $array = $document->toArray(); 38 | 39 | $this->assertArrayHasKey('meta', $array); 40 | $this->assertCount(1, $array['meta']); 41 | $this->assertArrayHasKey('foo', $array['meta']); 42 | $this->assertSame('bar', $array['meta']['foo']); 43 | } 44 | 45 | public function testAddMeta_HappyPath() { 46 | $document = new MetaDocument(); 47 | $document->addMeta('foo', 'bar'); 48 | 49 | $array = $document->toArray(); 50 | 51 | $this->assertArrayHasKey('meta', $array); 52 | $this->assertCount(1, $array['meta']); 53 | $this->assertArrayHasKey('foo', $array['meta']); 54 | $this->assertSame('bar', $array['meta']['foo']); 55 | } 56 | 57 | public function testAdd_HappyPath() { 58 | $document = new MetaDocument(); 59 | $document->add('foo', 'bar'); 60 | 61 | $array = $document->toArray(); 62 | 63 | $this->assertArrayHasKey('meta', $array); 64 | $this->assertCount(1, $array['meta']); 65 | $this->assertArrayHasKey('foo', $array['meta']); 66 | $this->assertSame('bar', $array['meta']['foo']); 67 | 68 | $this->assertCount(2, $array); 69 | $this->assertArrayNotHasKey('data', $array); 70 | $this->assertArrayHasKey('jsonapi', $array); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/SeparateProcessTest.php: -------------------------------------------------------------------------------- 1 | sendResponse(); 22 | $output = ob_get_clean(); 23 | 24 | $this->assertSame('{"jsonapi":{"version":"1.1"}}', $output); 25 | } 26 | 27 | /** 28 | * @runInSeparateProcess 29 | */ 30 | public function testSendResponse_NoContent() { 31 | $document = new Document(); 32 | $document->setHttpStatusCode(204); 33 | 34 | ob_start(); 35 | $document->sendResponse(); 36 | $output = ob_get_clean(); 37 | 38 | $this->assertSame('', $output); 39 | $this->assertSame(204, http_response_code()); 40 | } 41 | 42 | /** 43 | * @runInSeparateProcess 44 | */ 45 | public function testSendResponse_ContentTypeHeader() { 46 | if (extension_loaded('xdebug') === false) { 47 | $this->markTestSkipped('can not run without xdebug'); 48 | } 49 | 50 | $document = new Document(); 51 | 52 | ob_start(); 53 | $document->sendResponse(); 54 | ob_end_clean(); 55 | $this->assertSame(['Content-Type: '.Document::CONTENT_TYPE_OFFICIAL], xdebug_get_headers()); 56 | 57 | $options = ['contentType' => Document::CONTENT_TYPE_OFFICIAL]; 58 | ob_start(); 59 | $document->sendResponse($options); 60 | ob_end_clean(); 61 | $this->assertSame(['Content-Type: '.Document::CONTENT_TYPE_OFFICIAL], xdebug_get_headers()); 62 | 63 | $options = ['contentType' => Document::CONTENT_TYPE_DEBUG]; 64 | ob_start(); 65 | $document->sendResponse($options); 66 | ob_end_clean(); 67 | $this->assertSame(['Content-Type: '.Document::CONTENT_TYPE_DEBUG], xdebug_get_headers()); 68 | 69 | $options = ['contentType' => Document::CONTENT_TYPE_JSONP]; 70 | ob_start(); 71 | $document->sendResponse($options); 72 | ob_end_clean(); 73 | $this->assertSame(['Content-Type: '.Document::CONTENT_TYPE_JSONP], xdebug_get_headers()); 74 | } 75 | 76 | /** 77 | * @runInSeparateProcess 78 | * @group Extensions 79 | */ 80 | public function testSendResponse_ContentTypeHeaderWithExtensions() { 81 | if (extension_loaded('xdebug') === false) { 82 | $this->markTestSkipped('can not run without xdebug'); 83 | } 84 | 85 | $extension = new TestExtension(); 86 | $extension->setNamespace('one'); 87 | $extension->setOfficialLink('https://jsonapi.org'); 88 | 89 | $document = new Document(); 90 | $document->applyExtension($extension); 91 | 92 | ob_start(); 93 | $document->sendResponse(); 94 | ob_end_clean(); 95 | $this->assertSame(['Content-Type: '.Document::CONTENT_TYPE_OFFICIAL.'; ext="https://jsonapi.org"'], xdebug_get_headers()); 96 | 97 | $extension = new TestExtension(); 98 | $extension->setNamespace('two'); 99 | $extension->setOfficialLink('https://jsonapi.org/2'); 100 | $document->applyExtension($extension); 101 | 102 | ob_start(); 103 | $document->sendResponse(); 104 | ob_end_clean(); 105 | $this->assertSame(['Content-Type: '.Document::CONTENT_TYPE_OFFICIAL.'; ext="https://jsonapi.org https://jsonapi.org/2"'], xdebug_get_headers()); 106 | } 107 | 108 | /** 109 | * @runInSeparateProcess 110 | * @group Profiles 111 | */ 112 | public function testSendResponse_ContentTypeHeaderWithProfiles() { 113 | if (extension_loaded('xdebug') === false) { 114 | $this->markTestSkipped('can not run without xdebug'); 115 | } 116 | 117 | $profile = new TestProfile(); 118 | $profile->setOfficialLink('https://jsonapi.org'); 119 | 120 | $document = new Document(); 121 | $document->applyProfile($profile); 122 | 123 | ob_start(); 124 | $document->sendResponse(); 125 | ob_end_clean(); 126 | $this->assertSame(['Content-Type: '.Document::CONTENT_TYPE_OFFICIAL.'; profile="https://jsonapi.org"'], xdebug_get_headers()); 127 | 128 | $profile = new TestProfile(); 129 | $profile->setOfficialLink('https://jsonapi.org/2'); 130 | $document->applyProfile($profile); 131 | 132 | ob_start(); 133 | $document->sendResponse(); 134 | ob_end_clean(); 135 | $this->assertSame(['Content-Type: '.Document::CONTENT_TYPE_OFFICIAL.'; profile="https://jsonapi.org https://jsonapi.org/2"'], xdebug_get_headers()); 136 | } 137 | 138 | /** 139 | * @runInSeparateProcess 140 | */ 141 | public function testSendResponse_StatusCodeHeader() { 142 | $document = new Document(); 143 | 144 | ob_start(); 145 | $document->sendResponse(); 146 | ob_end_clean(); 147 | $this->assertSame(200, http_response_code()); 148 | 149 | $document->setHttpStatusCode(201); 150 | ob_start(); 151 | $document->sendResponse(); 152 | ob_end_clean(); 153 | $this->assertSame(201, http_response_code()); 154 | 155 | $document->setHttpStatusCode(422); 156 | ob_start(); 157 | $document->sendResponse(); 158 | ob_end_clean(); 159 | $this->assertSame(422, http_response_code()); 160 | 161 | $document->setHttpStatusCode(503); 162 | ob_start(); 163 | $document->sendResponse(); 164 | ob_end_clean(); 165 | $this->assertSame(503, http_response_code()); 166 | } 167 | 168 | /** 169 | * @runInSeparateProcess 170 | */ 171 | public function testSendResponse_CustomJson() { 172 | $document = new Document(); 173 | $options = ['json' => '{"foo":42}']; 174 | 175 | ob_start(); 176 | $document->sendResponse($options); 177 | $output = ob_get_clean(); 178 | 179 | $this->assertSame('{"foo":42}', $output); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /tests/TestableNonAbstractDocument.php: -------------------------------------------------------------------------------- 1 | format(\DateTime::ISO8601); 22 | } 23 | if ($updated !== null) { 24 | $timestamps['updated'] = $updated->format(\DateTime::ISO8601); 25 | } 26 | 27 | $resource->add('timestamps', $timestamps); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/example_output/ExampleUser.php: -------------------------------------------------------------------------------- 1 | id = $id; 13 | } 14 | 15 | function getCurrentLocation() { 16 | return 'Earth'; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/example_output/ExampleVersionExtension.php: -------------------------------------------------------------------------------- 1 | getResource()->addExtensionMember($this, 'id', $version); 21 | } 22 | else { 23 | $resource->addExtensionMember($this, 'id', $version); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/example_output/at_members_everywhere/at_members_everywhere.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "/@context", 3 | "jsonapi": { 4 | "@context": "/jsonapi/@context", 5 | "version": "1.1", 6 | "meta": { 7 | "@context": "/jsonapi/meta/@context" 8 | } 9 | }, 10 | "links": { 11 | "@context": "/links/@context", 12 | "foo": { 13 | "@context": "/links/foo/@context", 14 | "href": "https://jsonapi.org", 15 | "meta": { 16 | "@context": "/links/foo/meta/@context" 17 | } 18 | } 19 | }, 20 | "meta": { 21 | "@context": "/meta/@context" 22 | }, 23 | "data": { 24 | "type": "user", 25 | "id": "42", 26 | "@context": "/data/@context", 27 | "meta": { 28 | "@context": "/data/meta/@context" 29 | }, 30 | "attributes": { 31 | "@context": "/data/attributes/@context" 32 | }, 33 | "relationships": { 34 | "@context": "/data/relationships/@context", 35 | "foo": { 36 | "@context": "/data/relationships/foo/@context", 37 | "links": { 38 | "@context": "/data/relationships/foo/links/@context" 39 | }, 40 | "data": { 41 | "type": "user", 42 | "id": "1" 43 | }, 44 | "meta": { 45 | "@context": "/data/relationships/foo/meta/@context" 46 | } 47 | }, 48 | "bar": { 49 | "@context": "/data/relationships/bar/@context", 50 | "data": { 51 | "type": "user", 52 | "id": "2", 53 | "@context": "/data/relationships/bar/data/@context", 54 | "meta": { 55 | "@context": "/data/relationships/bar/data/meta/@context" 56 | } 57 | } 58 | } 59 | }, 60 | "links": { 61 | "@context": "/data/links/@context", 62 | "foo": { 63 | "@context": "/data/links/foo/@context", 64 | "href": "https://jsonapi.org", 65 | "meta": { 66 | "@context": "/data/links/foo/meta/@context" 67 | } 68 | } 69 | } 70 | }, 71 | "included": [ 72 | { 73 | "type": "user", 74 | "id": "1", 75 | "@context": "/included/0/@context", 76 | "attributes": { 77 | "@context": "/included/0/attributes/@context" 78 | } 79 | }, 80 | { 81 | "type": "user", 82 | "id": "3", 83 | "@context": "/included/1/@context", 84 | "relationships": { 85 | "@context": "/included/1/relationships/@context" 86 | } 87 | } 88 | ] 89 | } 90 | -------------------------------------------------------------------------------- /tests/example_output/at_members_in_errors/at_members_in_errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "test", 3 | "jsonapi": { 4 | "@context": "test", 5 | "version": "1.1" 6 | }, 7 | "errors": [ 8 | { 9 | "@context": "test", 10 | "code": "generic code", 11 | "title": "generic title", 12 | "detail": "specific details", 13 | "links": { 14 | "@context": "test", 15 | "foo": { 16 | "@context": "test", 17 | "href": "https://jsonapi.org", 18 | "meta": { 19 | "@context": "test" 20 | } 21 | } 22 | }, 23 | "source": { 24 | "foo": "bar" 25 | }, 26 | "meta": { 27 | "@context": "test" 28 | } 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /tests/example_output/at_members_in_errors/at_members_in_errors.php: -------------------------------------------------------------------------------- 1 | addAtMember('context', 'test'); 20 | 21 | /** 22 | * jsonapi 23 | */ 24 | 25 | $jsonapiObject = new JsonapiObject(); 26 | $jsonapiObject->addAtMember('context', 'test'); 27 | $document->setJsonapiObject($jsonapiObject); 28 | 29 | /** 30 | * error 31 | */ 32 | 33 | $errorObject = new ErrorObject('generic code', 'generic title', 'specific details'); 34 | $errorObject->addAtMember('context', 'test'); 35 | 36 | /** 37 | * error - source 38 | * 39 | * @todo make it work to have @-members next to the sources 40 | * if we need it in relationship identifiers, it is worth adding it here as well 41 | * @see https://github.com/json-api/json-api/issues/1367 42 | */ 43 | 44 | $errorObject->addSource('foo', 'bar'); 45 | 46 | /** 47 | * error - links 48 | */ 49 | 50 | $metaObject = new MetaObject(); 51 | $metaObject->addAtMember('context', 'test'); 52 | 53 | $linkObject = new LinkObject('https://jsonapi.org'); 54 | $linkObject->addAtMember('context', 'test'); 55 | $linkObject->setMetaObject($metaObject); 56 | 57 | $linksObject = new LinksObject(); 58 | $linksObject->addAtMember('context', 'test'); 59 | $linksObject->addLinkObject('foo', $linkObject); 60 | $errorObject->setLinksObject($linksObject); 61 | 62 | /** 63 | * error - meta 64 | */ 65 | 66 | $metaObject = new MetaObject(); 67 | $metaObject->addAtMember('context', 'test'); 68 | $errorObject->setMetaObject($metaObject); 69 | 70 | $document->addErrorObject($errorObject); 71 | 72 | return $document; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/example_output/collection/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonapi": { 3 | "version": "1.1" 4 | }, 5 | "data": [ 6 | { 7 | "type": "user", 8 | "id": "1", 9 | "attributes": { 10 | "name": "Ford Prefect", 11 | "heads": 1, 12 | "unknown": null 13 | } 14 | }, 15 | { 16 | "type": "user", 17 | "id": "2", 18 | "attributes": { 19 | "name": "Arthur Dent", 20 | "heads": "1, but not always there", 21 | "unknown": null 22 | } 23 | }, 24 | { 25 | "type": "user", 26 | "id": "42", 27 | "attributes": { 28 | "name": "Zaphod Beeblebrox", 29 | "heads": 2, 30 | "unknown": null 31 | }, 32 | "relationships": { 33 | "ship": { 34 | "data": { 35 | "type": "ship", 36 | "id": "5" 37 | } 38 | } 39 | } 40 | } 41 | ], 42 | "included": [ 43 | { 44 | "type": "ship", 45 | "id": "5", 46 | "attributes": { 47 | "name": "Heart of Gold" 48 | } 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /tests/example_output/collection/collection.php: -------------------------------------------------------------------------------- 1 | name = 'Ford Prefect'; 13 | $user1->heads = 1; 14 | 15 | $user2 = new ExampleUser(2); 16 | $user2->name = 'Arthur Dent'; 17 | $user2->heads = '1, but not always there'; 18 | 19 | $user42 = new ExampleUser(42); 20 | $user42->name = 'Zaphod Beeblebrox'; 21 | $user42->heads = 2; 22 | 23 | $users = [$user1, $user2, $user42]; 24 | 25 | $collection = []; 26 | 27 | foreach ($users as $user) { 28 | $resource = ResourceObject::fromObject($user, $type='user', $user->id); 29 | 30 | if ($user->id == 42) { 31 | $ship = new ResourceObject('ship', 5); 32 | $ship->add('name', 'Heart of Gold'); 33 | $resource->addRelationship('ship', $ship); 34 | } 35 | 36 | $collection[] = $resource; 37 | } 38 | 39 | $document = CollectionDocument::fromResources(...$collection); 40 | 41 | return $document; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/example_output/collection_canonical/collection_canonical.json: -------------------------------------------------------------------------------- 1 | { 2 | "links": { 3 | "self": "http://example.com/articles", 4 | "next": "http://example.com/articles?page[offset]=2", 5 | "last": "http://example.com/articles?page[offset]=10" 6 | }, 7 | "data": [ 8 | { 9 | "type": "articles", 10 | "id": "1", 11 | "attributes": { 12 | "title": "JSON:API paints my bikeshed!" 13 | }, 14 | "relationships": { 15 | "author": { 16 | "links": { 17 | "self": "http://example.com/articles/1/relationships/author", 18 | "related": "http://example.com/articles/1/author" 19 | }, 20 | "data": { 21 | "type": "people", 22 | "id": "9" 23 | } 24 | }, 25 | "comments": { 26 | "links": { 27 | "self": "http://example.com/articles/1/relationships/comments", 28 | "related": "http://example.com/articles/1/comments" 29 | }, 30 | "data": [ 31 | { 32 | "type": "comments", 33 | "id": "5" 34 | }, 35 | { 36 | "type": "comments", 37 | "id": "12" 38 | } 39 | ] 40 | } 41 | }, 42 | "links": { 43 | "self": "http://example.com/articles/1" 44 | } 45 | } 46 | ], 47 | "included": [ 48 | { 49 | "type": "people", 50 | "id": "9", 51 | "attributes": { 52 | "firstName": "Dan", 53 | "lastName": "Gebhardt", 54 | "twitter": "dgeb" 55 | }, 56 | "links": { 57 | "self": "http://example.com/people/9" 58 | } 59 | }, 60 | { 61 | "type": "comments", 62 | "id": "5", 63 | "attributes": { 64 | "body": "First!", 65 | "authorId": 2 66 | }, 67 | "relationships": { 68 | "author": { 69 | "data": { 70 | "type": "people", 71 | "id": "2" 72 | } 73 | } 74 | }, 75 | "links": { 76 | "self": "http://example.com/comments/5" 77 | } 78 | }, 79 | { 80 | "type": "comments", 81 | "id": "12", 82 | "attributes": { 83 | "body": "I like XML better", 84 | "authorId": 9 85 | }, 86 | "relationships": { 87 | "author": { 88 | "data": { 89 | "type": "people", 90 | "id": "9" 91 | } 92 | } 93 | }, 94 | "links": { 95 | "self": "http://example.com/comments/12" 96 | } 97 | } 98 | ] 99 | } 100 | -------------------------------------------------------------------------------- /tests/example_output/collection_canonical/collection_canonical.php: -------------------------------------------------------------------------------- 1 | [ 12 | 'title' => 'JSON:API paints my bikeshed!', 13 | 'authorId' => 9, 14 | ], 15 | ]; 16 | $commentRecords = [ 17 | 5 => [ 18 | 'body' => 'First!', 19 | 'authorId' => 2, 20 | ], 21 | 12 => [ 22 | 'body' => 'I like XML better', 23 | 'authorId' => 9, 24 | ], 25 | ]; 26 | $peopleRecords = [ 27 | 9 => [ 28 | 'firstName' => 'Dan', 29 | 'lastName' => 'Gebhardt', 30 | 'twitter' => 'dgeb', 31 | ], 32 | ]; 33 | 34 | $document = new CollectionDocument(); 35 | 36 | foreach ($articleRecords as $articleId => $articleRecord) { 37 | $authorId = $articleRecord['authorId']; 38 | 39 | $author = ResourceObject::fromArray($peopleRecords[$authorId], 'people', $authorId); 40 | $author->setSelfLink('http://example.com/people/'.$authorId); 41 | $authorRelationshipLinks = [ 42 | 'self' => 'http://example.com/articles/'.$articleId.'/relationships/author', 43 | 'related' => 'http://example.com/articles/'.$articleId.'/author', 44 | ]; 45 | 46 | $comments = []; 47 | foreach ($commentRecords as $commentId => $commentRecord) { 48 | $comment = ResourceObject::fromArray($commentRecord, 'comments', $commentId); 49 | $comment->add('body', $commentRecord['body']); 50 | $comment->addRelationship('author', new ResourceObject('people', $commentRecord['authorId'])); 51 | $comment->setSelfLink('http://example.com/comments/'.$commentId); 52 | 53 | $comments[] = $comment; 54 | } 55 | 56 | $commentsRelationshipLinks = [ 57 | 'self' => 'http://example.com/articles/'.$articleId.'/relationships/comments', 58 | 'related' => 'http://example.com/articles/'.$articleId.'/comments', 59 | ]; 60 | 61 | $article = new ResourceObject('articles', $articleId); 62 | $article->add('title', $articleRecord['title']); 63 | $article->setSelfLink('http://example.com/articles/'.$articleId); 64 | $article->addRelationship('author', $author, $authorRelationshipLinks); 65 | $article->addRelationship('comments', $comments, $commentsRelationshipLinks); 66 | 67 | $document->addResource($article); 68 | } 69 | 70 | $document->setSelfLink('http://example.com/articles'); 71 | $document->setPaginationLinks($previous=null, $next='http://example.com/articles?page[offset]=2', $first=null, $last='http://example.com/articles?page[offset]=10'); 72 | $document->unsetJsonapiObject(); 73 | 74 | return $document; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/example_output/cursor_pagination_profile/cursor_pagination_profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonapi": { 3 | "version": "1.1", 4 | "profile": [ 5 | "https://jsonapi.org/profiles/ethanresnick/cursor-pagination/" 6 | ] 7 | }, 8 | "links": { 9 | "prev": null, 10 | "next": { 11 | "href": "/users?sort=42&page[size]=10&page[after]=zaphod" 12 | } 13 | }, 14 | "meta": { 15 | "page": { 16 | "total": 3, 17 | "estimatedTotal": { 18 | "bestGuess": 10 19 | } 20 | } 21 | }, 22 | "data": [ 23 | { 24 | "type": "user", 25 | "id": "1", 26 | "meta": { 27 | "page": { 28 | "cursor": "ford" 29 | } 30 | } 31 | }, 32 | { 33 | "type": "user", 34 | "id": "2", 35 | "meta": { 36 | "page": { 37 | "cursor": "arthur" 38 | } 39 | } 40 | }, 41 | { 42 | "type": "user", 43 | "id": "42", 44 | "meta": { 45 | "page": { 46 | "cursor": "zaphod" 47 | } 48 | } 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /tests/example_output/cursor_pagination_profile/cursor_pagination_profile.php: -------------------------------------------------------------------------------- 1 | setCursor($user1, 'ford'); 18 | $profile->setCursor($user2, 'arthur'); 19 | $profile->setCursor($user42, 'zaphod'); 20 | 21 | $document = CollectionDocument::fromResources($user1, $user2, $user42); 22 | $document->applyProfile($profile); 23 | 24 | $profile->setCount($document, $exactTotal=3, $bestGuessTotal=10); 25 | $profile->setLinksFirstPage($document, $currentUrl='/users?sort=42&page[size]=10', $lastCursor='zaphod'); 26 | 27 | return $document; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/example_output/errors_all_options/errors_all_options.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonapi": { 3 | "version": "1.1" 4 | }, 5 | "links": { 6 | "redirect": { 7 | "href": "/login", 8 | "meta": { 9 | "label": "Log in" 10 | } 11 | } 12 | }, 13 | "errors": [ 14 | { 15 | "code": "Invalid input", 16 | "title": "Too much options", 17 | "detail": "Please, choose a bit less. Consult your ...", 18 | "links": { 19 | "about": "https://www.example.com/explanation.html", 20 | "type": "https://www.example.com/documentation.html" 21 | } 22 | }, 23 | { 24 | "id": 42, 25 | "status": "404", 26 | "code": "Invalid input", 27 | "title": "Too much options", 28 | "detail": "Please, choose a bit less. Consult your ...", 29 | "links": { 30 | "about": { 31 | "href": "https://www.example.com/explanation.html", 32 | "meta": { 33 | "foo": "bar" 34 | } 35 | }, 36 | "type": { 37 | "href": "https://www.example.com/documentation.html", 38 | "meta": { 39 | "foo": "bar" 40 | } 41 | } 42 | }, 43 | "source": { 44 | "pointer": "/data/attributes/title", 45 | "parameter": "filter", 46 | "header": "X-Foo" 47 | }, 48 | "meta": { 49 | "foo": "bar", 50 | "object": { 51 | "property": "value" 52 | } 53 | } 54 | }, 55 | { 56 | "code": "kiss", 57 | "title": "Error objects can be small and simple as well." 58 | }, 59 | { 60 | "status": "500", 61 | "code": "Exception", 62 | "meta": { 63 | "type": "Exception", 64 | "message": "please don't throw things", 65 | "code": 500, 66 | "file": "/errors_all_options.php", 67 | "line": 31 68 | } 69 | }, 70 | { 71 | "code": "Exception", 72 | "meta": { 73 | "type": "Exception", 74 | "message": "something went wrong!", 75 | "code": 0, 76 | "file": "/errors_all_options.php", 77 | "line": 30 78 | } 79 | }, 80 | { 81 | "code": "Authentication error", 82 | "title": "Not logged in" 83 | } 84 | ] 85 | } 86 | -------------------------------------------------------------------------------- /tests/example_output/errors_all_options/errors_all_options.php: -------------------------------------------------------------------------------- 1 | blameJsonPointer($pointer='/data/attributes/title'); 14 | $errorSpecApi->blameQueryParameter($parameter='filter'); 15 | $errorSpecApi->blameHeader($headerName='X-Foo'); 16 | $errorSpecApi->setUniqueIdentifier($id=42); 17 | $errorSpecApi->addMeta($key='foo', $value='bar'); 18 | $errorSpecApi->setHttpStatusCode($httpStatusCode=404); 19 | $errorSpecApi->setApplicationCode($genericCode='Invalid input'); 20 | $errorSpecApi->setHumanTitle($genericTitle='Too much options'); 21 | $errorSpecApi->setHumanDetails($specificDetails='Please, choose a bit less. Consult your ...'); 22 | $errorSpecApi->setAboutLink($specificAboutLink='https://www.example.com/explanation.html', ['foo'=>'bar']); 23 | $errorSpecApi->setTypeLink($genericTypeLink='https://www.example.com/documentation.html', ['foo'=>'bar']); 24 | 25 | $metaObject = new \stdClass(); 26 | $metaObject->property = 'value'; 27 | $errorSpecApi->addMeta($key='object', $metaObject); 28 | 29 | $anotherError = new ErrorObject('kiss', 'Error objects can be small and simple as well.'); 30 | $previousException = new \Exception('something went wrong!'); 31 | $someException = new \Exception('please don\'t throw things', 500, $previousException); 32 | 33 | $document = new ErrorsDocument($errorHumanApi); 34 | $document->addErrorObject($errorSpecApi); 35 | $document->addErrorObject($anotherError); 36 | $document->addException($someException, $options=['includeExceptionTrace' => false, 'stripExceptionBasePath' => __DIR__]); 37 | $document->add($genericCode='Authentication error', $genericTitle='Not logged in'); 38 | $document->addLink('redirect', '/login', ['label'=>'Log in']); 39 | $document->setHttpStatusCode(400); 40 | 41 | return $document; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/example_output/errors_exception_native/errors_exception_native.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonapi": { 3 | "version": "1.1" 4 | }, 5 | "errors": [ 6 | { 7 | "status": "404", 8 | "code": "Exception", 9 | "meta": { 10 | "type": "Exception", 11 | "message": "unknown user", 12 | "code": 404, 13 | "file": "/errors_exception_native.php", 14 | "line": 9 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tests/example_output/errors_exception_native/errors_exception_native.php: -------------------------------------------------------------------------------- 1 | false, 12 | 'includeExceptionPrevious' => true, 13 | 'stripExceptionBasePath' => __DIR__, 14 | ]; 15 | $document = ErrorsDocument::fromException($exception, $options); 16 | 17 | return $document; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/example_output/extension/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonapi": { 3 | "version": "1.1", 4 | "ext": [ 5 | "https://jsonapi.org/format/1.1/#extension-rules" 6 | ] 7 | }, 8 | "data": { 9 | "type": "user", 10 | "id": "42", 11 | "version:id": "2019" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/example_output/extension/extension.php: -------------------------------------------------------------------------------- 1 | applyExtension($extension); 14 | 15 | $extension->setVersion($document, '2019'); 16 | 17 | return $document; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/example_output/extension_members_everywhere/extension_members_everywhere.json: -------------------------------------------------------------------------------- 1 | { 2 | "everywhere:key": "/key", 3 | "jsonapi": { 4 | "everywhere:key": "/jsonapi/key", 5 | "version": "1.1", 6 | "meta": { 7 | "everywhere:key": "/jsonapi/meta/key" 8 | } 9 | }, 10 | "links": { 11 | "everywhere:key": "/links/key", 12 | "foo": { 13 | "everywhere:key": "/links/foo/key", 14 | "href": "https://jsonapi.org", 15 | "meta": { 16 | "everywhere:key": "/links/foo/meta/key" 17 | } 18 | } 19 | }, 20 | "meta": { 21 | "everywhere:key": "/meta/key" 22 | }, 23 | "data": { 24 | "type": "user", 25 | "id": "42", 26 | "everywhere:key": "/data/key", 27 | "meta": { 28 | "everywhere:key": "/data/meta/key" 29 | }, 30 | "attributes": { 31 | "everywhere:key": "/data/attributes/key" 32 | }, 33 | "relationships": { 34 | "everywhere:key": "/data/relationships/key", 35 | "foo": { 36 | "everywhere:key": "/data/relationships/foo/key", 37 | "links": { 38 | "everywhere:key": "/data/relationships/foo/links/key" 39 | }, 40 | "data": { 41 | "type": "user", 42 | "id": "1" 43 | }, 44 | "meta": { 45 | "everywhere:key": "/data/relationships/foo/meta/key" 46 | } 47 | }, 48 | "bar": { 49 | "everywhere:key": "/data/relationships/bar/key", 50 | "data": { 51 | "type": "user", 52 | "id": "2", 53 | "everywhere:key": "/data/relationships/bar/data/key", 54 | "meta": { 55 | "everywhere:key": "/data/relationships/bar/data/meta/key" 56 | } 57 | } 58 | } 59 | }, 60 | "links": { 61 | "everywhere:key": "/data/links/key", 62 | "foo": { 63 | "everywhere:key": "/data/links/foo/key", 64 | "href": "https://jsonapi.org", 65 | "meta": { 66 | "everywhere:key": "/data/links/foo/meta/key" 67 | } 68 | } 69 | } 70 | }, 71 | "included": [ 72 | { 73 | "type": "user", 74 | "id": "1", 75 | "everywhere:key": "/included/0/key", 76 | "attributes": { 77 | "everywhere:key": "/included/0/attributes/key" 78 | } 79 | }, 80 | { 81 | "type": "user", 82 | "id": "3", 83 | "everywhere:key": "/included/1/key", 84 | "relationships": { 85 | "everywhere:key": "/included/1/relationships/key" 86 | } 87 | } 88 | ] 89 | } 90 | -------------------------------------------------------------------------------- /tests/example_output/meta_only/meta_only.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonapi": { 3 | "version": "1.1" 4 | }, 5 | "meta": { 6 | "foo": "bar" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/example_output/meta_only/meta_only.php: -------------------------------------------------------------------------------- 1 | addMeta('foo', 'bar'); 11 | 12 | return $document; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/example_output/null_values/null_values.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonapi": { 3 | "version": "1.1" 4 | }, 5 | "links": { 6 | "foo": null, 7 | "bar": null 8 | }, 9 | "meta": { 10 | "foo": null 11 | }, 12 | "data": { 13 | "type": "user", 14 | "id": "42", 15 | "attributes": { 16 | "foo": null 17 | }, 18 | "relationships": { 19 | "bar": { 20 | "data": null 21 | }, 22 | "baz": { 23 | "data": null 24 | }, 25 | "baf": { 26 | "data": [] 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/example_output/null_values/null_values.php: -------------------------------------------------------------------------------- 1 | add('foo', null); 14 | $document->addMeta('foo', null); 15 | 16 | $document->addLink('foo', null); 17 | $document->addLinkObject('bar', new LinkObject()); 18 | 19 | $document->addRelationship('bar', null); 20 | $document->addRelationshipObject('baz', new RelationshipObject(RelationshipObject::TO_ONE)); 21 | $document->addRelationshipObject('baf', new RelationshipObject(RelationshipObject::TO_MANY)); 22 | 23 | return $document; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/example_output/profile/profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonapi": { 3 | "version": "1.1", 4 | "profile": [ 5 | "https://jsonapi.org/recommendations/#authoring-profiles" 6 | ] 7 | }, 8 | "data": { 9 | "type": "user", 10 | "id": "42", 11 | "attributes": { 12 | "timestamps": { 13 | "created": "2019-01-01T00:00:00+0000", 14 | "updated": "2021-01-01T00:00:00+0000" 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/example_output/profile/profile.php: -------------------------------------------------------------------------------- 1 | applyProfile($profile); 14 | 15 | $profile->setTimestamps($document, new \DateTime('2019-01-01T00:00:00+0000'), new \DateTime('2021-01-01T00:00:00+0000')); 16 | 17 | return $document; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/example_output/relationship_to_many_document/relationship_to_many_document.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonapi": { 3 | "version": "1.1" 4 | }, 5 | "links": { 6 | "self": "/articles/1/relationship/tags", 7 | "related": "/articles/1/tags" 8 | }, 9 | "data": [ 10 | { 11 | "type": "tags", 12 | "id": "2" 13 | }, 14 | { 15 | "type": "tags", 16 | "id": "3" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tests/example_output/relationship_to_many_document/relationship_to_many_document.php: -------------------------------------------------------------------------------- 1 | add('tags', 2); 11 | $document->add('tags', 3); 12 | 13 | $document->setSelfLink('/articles/1/relationship/tags'); 14 | $document->addLink('related', '/articles/1/tags'); 15 | 16 | return $document; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/example_output/relationship_to_one_document/relationship_to_one_document.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonapi": { 3 | "version": "1.1" 4 | }, 5 | "links": { 6 | "self": "/articles/1/relationship/author", 7 | "related": "/articles/1/author" 8 | }, 9 | "data": { 10 | "type": "author", 11 | "id": "12" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/example_output/relationship_to_one_document/relationship_to_one_document.php: -------------------------------------------------------------------------------- 1 | setSelfLink('/articles/1/relationship/author', $meta=[], $level=Document::LEVEL_ROOT); 13 | $document->addLink('related', '/articles/1/author'); 14 | 15 | return $document; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/example_output/relationships/relationships.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonapi": { 3 | "version": "1.1" 4 | }, 5 | "data": { 6 | "type": "user", 7 | "id": "1", 8 | "relationships": { 9 | "included-ship": { 10 | "data": { 11 | "type": "ship", 12 | "id": "24" 13 | } 14 | }, 15 | "excluded-ship": { 16 | "data": { 17 | "type": "ship", 18 | "id": "42" 19 | } 20 | }, 21 | "one-by-one-friends": { 22 | "data": [ 23 | { 24 | "type": "user", 25 | "id": "24" 26 | }, 27 | { 28 | "type": "user", 29 | "id": "42" 30 | } 31 | ] 32 | }, 33 | "included-friends": { 34 | "data": [ 35 | { 36 | "type": "user", 37 | "id": "24" 38 | }, 39 | { 40 | "type": "user", 41 | "id": "42" 42 | } 43 | ] 44 | }, 45 | "one-by-one-neighbours": { 46 | "data": [ 47 | { 48 | "type": "ship", 49 | "id": "24" 50 | }, 51 | { 52 | "type": "dock", 53 | "id": "3" 54 | } 55 | ] 56 | } 57 | } 58 | }, 59 | "included": [ 60 | { 61 | "type": "ship", 62 | "id": "24", 63 | "attributes": { 64 | "foo": "bar" 65 | } 66 | }, 67 | { 68 | "type": "user", 69 | "id": "24", 70 | "attributes": { 71 | "foo": "bar" 72 | } 73 | }, 74 | { 75 | "type": "user", 76 | "id": "42", 77 | "attributes": { 78 | "bar": "baz" 79 | } 80 | }, 81 | { 82 | "type": "dock", 83 | "id": "3", 84 | "attributes": { 85 | "bar": "baf" 86 | } 87 | } 88 | ] 89 | } 90 | -------------------------------------------------------------------------------- /tests/example_output/relationships/relationships.php: -------------------------------------------------------------------------------- 1 | add('foo', 'bar'); 16 | 17 | $ship2Resource = new ResourceObject('ship', 42); 18 | $ship2Resource->add('bar', 'baz'); 19 | 20 | $friend1Resource = new ResourceObject('user', 24); 21 | $friend1Resource->add('foo', 'bar'); 22 | 23 | $friend2Resource = new ResourceObject('user', 42); 24 | $friend2Resource->add('bar', 'baz'); 25 | 26 | $dockResource = new ResourceObject('dock', 3); 27 | $dockResource->add('bar', 'baf'); 28 | 29 | /** 30 | * to-one relationship 31 | */ 32 | 33 | $document->addRelationship('included-ship', $ship1Resource); 34 | 35 | /** 36 | * to-one relationship, without included resource 37 | */ 38 | 39 | $options = ['includeContainedResources' => false]; 40 | $document->addRelationship('excluded-ship', $ship2Resource, $links=[], $meta=[], $options); 41 | 42 | /** 43 | * to-many relationship, one-by-one 44 | */ 45 | 46 | $relationshipObject = new RelationshipObject($type=RelationshipObject::TO_MANY); 47 | $relationshipObject->addResource($friend1Resource); 48 | $relationshipObject->addResource($friend2Resource); 49 | 50 | $document->addRelationshipObject('one-by-one-friends', $relationshipObject); 51 | 52 | /** 53 | * to-many relationship, all-at-once 54 | */ 55 | 56 | $friends = new CollectionDocument(); 57 | $friends->addResource($friend1Resource); 58 | $friends->addResource($friend2Resource); 59 | 60 | $document->addRelationship('included-friends', $friends); 61 | 62 | /** 63 | * to-many relationship, different types 64 | */ 65 | 66 | $relationshipObject = new RelationshipObject($type=RelationshipObject::TO_MANY); 67 | $relationshipObject->addResource($ship1Resource); 68 | $relationshipObject->addResource($dockResource); 69 | 70 | $document->addRelationshipObject('one-by-one-neighbours', $relationshipObject); 71 | 72 | return $document; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/example_output/resource_document_identifier_only/resource_document_identifier_only.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonapi": { 3 | "version": "1.1" 4 | }, 5 | "data": { 6 | "type": "user", 7 | "id": "42" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/example_output/resource_document_identifier_only/resource_document_identifier_only.php: -------------------------------------------------------------------------------- 1 | name = 'Ford Prefect'; 12 | $user1->heads = 1; 13 | 14 | $user42 = new ExampleUser(42); 15 | $user42->name = 'Zaphod Beeblebrox'; 16 | $user42->heads = 2; 17 | 18 | $document = ResourceDocument::fromObject($user1, $type='user', $user1->id); 19 | $document->add('location', $user1->getCurrentLocation()); 20 | $document->addLink('homepage', 'https://jsonapi.org'); 21 | $document->addMeta('difference', 'is in the code to generate this'); 22 | 23 | $relation = ResourceDocument::fromObject($user42, $type='user', $user42->id); 24 | $document->addRelationship('friend', $relation); 25 | 26 | return $document; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/example_output/resource_links/resource_links.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonapi": { 3 | "version": "1.1" 4 | }, 5 | "links": { 6 | "redirect": { 7 | "href": "/login", 8 | "meta": { 9 | "level": "root" 10 | } 11 | } 12 | }, 13 | "data": { 14 | "type": "user", 15 | "id": "42", 16 | "attributes": { 17 | "name": "Zaphod Beeblebrox", 18 | "heads": 2, 19 | "unknown": null 20 | }, 21 | "links": { 22 | "self": { 23 | "href": "/user/42", 24 | "meta": { 25 | "level": "resource" 26 | } 27 | }, 28 | "partner": { 29 | "href": "/user/1", 30 | "meta": { 31 | "level": "resource" 32 | } 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/example_output/resource_links/resource_links.php: -------------------------------------------------------------------------------- 1 | name = 'Zaphod Beeblebrox'; 13 | $user42->heads = 2; 14 | 15 | $document = ResourceDocument::fromObject($user42, $type='user', $user42->id); 16 | 17 | $selfResourceMeta = ['level' => Document::LEVEL_RESOURCE]; 18 | $partnerMeta = ['level' => Document::LEVEL_RESOURCE]; 19 | $redirectMeta = ['level' => Document::LEVEL_ROOT]; 20 | 21 | $document->setSelfLink('/user/42', $selfResourceMeta); 22 | $document->addLink('partner', '/user/1', $partnerMeta, $level=Document::LEVEL_RESOURCE); 23 | $document->addLink('redirect', '/login', $redirectMeta, $level=Document::LEVEL_ROOT); 24 | 25 | return $document; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/example_output/resource_nested_relations/resource_nested_relations.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonapi": { 3 | "version": "1.1" 4 | }, 5 | "data": { 6 | "type": "user", 7 | "id": "42", 8 | "attributes": { 9 | "name": "Zaphod Beeblebrox", 10 | "heads": 2, 11 | "unknown": null 12 | }, 13 | "relationships": { 14 | "ship": { 15 | "data": { 16 | "type": "ship", 17 | "id": "5" 18 | } 19 | } 20 | } 21 | }, 22 | "included": [ 23 | { 24 | "type": "ship", 25 | "id": "5", 26 | "attributes": { 27 | "name": "Heart of Gold" 28 | }, 29 | "relationships": { 30 | "wing": { 31 | "data": { 32 | "type": "wing", 33 | "id": "1" 34 | } 35 | } 36 | } 37 | }, 38 | { 39 | "type": "wing", 40 | "id": "1", 41 | "attributes": { 42 | "side": "top" 43 | }, 44 | "relationships": { 45 | "flap": { 46 | "data": { 47 | "type": "flap", 48 | "id": "1" 49 | } 50 | } 51 | } 52 | }, 53 | { 54 | "type": "flap", 55 | "id": "1", 56 | "attributes": { 57 | "color": "orange" 58 | } 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /tests/example_output/resource_nested_relations/resource_nested_relations.php: -------------------------------------------------------------------------------- 1 | name = 'Zaphod Beeblebrox'; 13 | $user42->heads = 2; 14 | 15 | $flap = new ResourceObject('flap', 1); 16 | $flap->add('color', 'orange'); 17 | 18 | $wing = new ResourceObject('wing', 1); 19 | $wing->add('side', 'top'); 20 | $wing->addRelationship('flap', $flap); 21 | 22 | $ship = new ResourceObject('ship', 5); 23 | $ship->add('name', 'Heart of Gold'); 24 | $ship->addRelationship('wing', $wing); 25 | 26 | /** 27 | * building up the json response 28 | */ 29 | 30 | $document = ResourceDocument::fromObject($user42, $type='user', $user42->id); 31 | $document->addRelationship('ship', $ship); 32 | 33 | return $document; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/example_output/resource_spec_api/resource_spec_api.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonapi": { 3 | "version": "1.1" 4 | }, 5 | "links": { 6 | "homepage": "https://jsonapi.org" 7 | }, 8 | "meta": { 9 | "difference": "is in the code to generate this" 10 | }, 11 | "data": { 12 | "type": "user", 13 | "id": "1", 14 | "attributes": { 15 | "name": "Ford Prefect", 16 | "heads": 1, 17 | "unknown": null, 18 | "location": "Earth" 19 | }, 20 | "relationships": { 21 | "friend": { 22 | "data": { 23 | "type": "user", 24 | "id": "42" 25 | } 26 | } 27 | } 28 | }, 29 | "included": [ 30 | { 31 | "type": "user", 32 | "id": "42", 33 | "attributes": { 34 | "name": "Zaphod Beeblebrox", 35 | "heads": 2, 36 | "unknown": null 37 | } 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /tests/example_output/resource_spec_api/resource_spec_api.php: -------------------------------------------------------------------------------- 1 | name = 'Ford Prefect'; 18 | $user1->heads = 1; 19 | 20 | $user42 = new ExampleUser(42); 21 | $user42->name = 'Zaphod Beeblebrox'; 22 | $user42->heads = 2; 23 | 24 | $attributes1 = new AttributesObject(); 25 | $attributes1->add('name', $user1->name); 26 | $attributes1->add('heads', $user1->heads); 27 | $attributes1->add('unknown', $user1->unknown); 28 | $attributes1->add('location', $user1->getCurrentLocation()); 29 | 30 | $attributes42 = new AttributesObject(); 31 | $attributes42->add('name', $user42->name); 32 | $attributes42->add('heads', $user42->heads); 33 | $attributes42->add('unknown', $user42->unknown); 34 | 35 | $links = new LinksObject(); 36 | $links->addLinkString('homepage', 'https://jsonapi.org'); 37 | 38 | $meta = new MetaObject(); 39 | $meta->add('difference', 'is in the code to generate this'); 40 | 41 | $resource = new ResourceObject(); 42 | $resource->setId($user42->id); 43 | $resource->setType('user'); 44 | $resource->setAttributesObject($attributes42); 45 | 46 | $relationship = new RelationshipObject(RelationshipObject::TO_ONE); 47 | $relationship->setResource($resource); 48 | $relationships = new RelationshipsObject(); 49 | $relationships->addRelationshipObject('friend', $relationship); 50 | 51 | $document = new ResourceDocument(); 52 | $document->setId($user1->id); 53 | $document->setType('user'); 54 | $document->setAttributesObject($attributes1); 55 | $document->setLinksObject($links); 56 | $document->setMetaObject($meta); 57 | $document->setRelationshipsObject($relationships); 58 | 59 | return $document; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/example_output/status_only/status_only.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonapi": { 3 | "version": "1.1" 4 | }, 5 | "meta": {} 6 | } 7 | -------------------------------------------------------------------------------- /tests/example_output/status_only/status_only.php: -------------------------------------------------------------------------------- 1 | setHttpStatusCode(201); 11 | 12 | return $document; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/extensions/AtomicOperationsDocumentTest.php: -------------------------------------------------------------------------------- 1 | add('name', 'Ford'); 21 | $resource2->add('name', 'Arthur'); 22 | $resource3->add('name', 'Zaphod'); 23 | $document->addResults($resource1, $resource2, $resource3); 24 | $document->setSelfLink('https://example.org/operations'); 25 | 26 | $array = $document->toArray(); 27 | 28 | $this->assertArrayHasKey('jsonapi', $array); 29 | $this->assertArrayHasKey('ext', $array['jsonapi']); 30 | $this->assertCount(1, $array['jsonapi']['ext']); 31 | $this->assertSame((new AtomicOperationsExtension())->getOfficialLink(), $array['jsonapi']['ext'][0]); 32 | 33 | $this->assertArrayHasKey('links', $array); 34 | $this->assertArrayHasKey('self', $array['links']); 35 | $this->assertArrayHasKey('href', $array['links']['self']); 36 | $this->assertArrayHasKey('type', $array['links']['self']); 37 | $this->assertSame('https://example.org/operations', $array['links']['self']['href']); 38 | $this->assertSame('application/vnd.api+json; ext="'.(new AtomicOperationsExtension())->getOfficialLink().'"', $array['links']['self']['type']); 39 | 40 | $this->assertArrayHasKey('atomic:results', $array); 41 | $this->assertCount(3, $array['atomic:results']); 42 | $this->assertSame(['data' => $resource1->toArray()], $array['atomic:results'][0]); 43 | $this->assertSame(['data' => $resource2->toArray()], $array['atomic:results'][1]); 44 | $this->assertSame(['data' => $resource3->toArray()], $array['atomic:results'][2]); 45 | } 46 | 47 | public function testSetResults_EmptySuccessResults() { 48 | $document = new AtomicOperationsDocument(); 49 | $array = $document->toArray(); 50 | 51 | $this->assertArrayHasKey('jsonapi', $array); 52 | $this->assertArrayHasKey('ext', $array['jsonapi']); 53 | $this->assertCount(1, $array['jsonapi']['ext']); 54 | $this->assertSame((new AtomicOperationsExtension())->getOfficialLink(), $array['jsonapi']['ext'][0]); 55 | 56 | $this->assertArrayHasKey('atomic:results', $array); 57 | $this->assertCount(0, $array['atomic:results']); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/extensions/TestExtension.php: -------------------------------------------------------------------------------- 1 | namespace = $namespace; 13 | } 14 | 15 | public function setOfficialLink($officialLink) { 16 | $this->officialLink = $officialLink; 17 | } 18 | 19 | public function getNamespace() { 20 | return $this->namespace; 21 | } 22 | 23 | public function getOfficialLink() { 24 | return $this->officialLink; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/helpers/AtMemberManagerTest.php: -------------------------------------------------------------------------------- 1 | assertFalse($helper->hasAtMembers()); 14 | $this->assertSame([], $helper->getAtMembers()); 15 | 16 | $helper->addAtMember('@foo', 'bar'); 17 | 18 | $array = $helper->getAtMembers(); 19 | 20 | $this->assertTrue($helper->hasAtMembers()); 21 | $this->assertCount(1, $array); 22 | $this->assertArrayHasKey('@foo', $array); 23 | $this->assertSame('bar', $array['@foo']); 24 | } 25 | 26 | public function testAddAtMember_WithoutAtSign() { 27 | $helper = new AtMemberManager(); 28 | 29 | $helper->addAtMember('foo', 'bar'); 30 | 31 | $array = $helper->getAtMembers(); 32 | 33 | $this->assertArrayHasKey('@foo', $array); 34 | } 35 | 36 | public function testAddAtMember_WithObjectValue() { 37 | $helper = new AtMemberManager(); 38 | 39 | $object = new \stdClass(); 40 | $object->bar = 'baz'; 41 | 42 | $helper->addAtMember('foo', $object); 43 | 44 | $array = $helper->getAtMembers(); 45 | 46 | $this->assertArrayHasKey('@foo', $array); 47 | $this->assertArrayHasKey('bar', $array['@foo']); 48 | $this->assertSame('baz', $array['@foo']['bar']); 49 | } 50 | 51 | public function testAddAtMember_InvalidDoubleAt() { 52 | $helper = new AtMemberManager(); 53 | 54 | $this->expectException(InputException::class); 55 | 56 | $helper->addAtMember('@@foo', 'bar'); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/helpers/ExtensionMemberManagerTest.php: -------------------------------------------------------------------------------- 1 | setNamespace('test'); 18 | 19 | $this->assertFalse($helper->hasExtensionMembers()); 20 | $this->assertSame([], $helper->getExtensionMembers()); 21 | 22 | $helper->addExtensionMember($extension, 'foo', 'bar'); 23 | 24 | $array = $helper->getExtensionMembers(); 25 | 26 | $this->assertTrue($helper->hasExtensionMembers()); 27 | $this->assertCount(1, $array); 28 | $this->assertArrayHasKey('test:foo', $array); 29 | $this->assertSame('bar', $array['test:foo']); 30 | } 31 | 32 | public function testAddExtensionMember_WithNamespacePrefixed() { 33 | $helper = new ExtensionMemberManager(); 34 | $extension = new TestExtension(); 35 | $extension->setNamespace('test'); 36 | 37 | $helper->addExtensionMember($extension, 'test:foo', 'bar'); 38 | 39 | $array = $helper->getExtensionMembers(); 40 | 41 | $this->assertArrayHasKey('test:foo', $array); 42 | } 43 | 44 | public function testAddExtensionMember_WithObjectValue() { 45 | $helper = new ExtensionMemberManager(); 46 | $extension = new TestExtension(); 47 | $extension->setNamespace('test'); 48 | 49 | $object = new \stdClass(); 50 | $object->bar = 'baz'; 51 | 52 | $helper->addExtensionMember($extension, 'foo', $object); 53 | 54 | $array = $helper->getExtensionMembers(); 55 | 56 | $this->assertArrayHasKey('test:foo', $array); 57 | $this->assertArrayHasKey('bar', $array['test:foo']); 58 | $this->assertSame('baz', $array['test:foo']['bar']); 59 | } 60 | 61 | public function testAddExtensionMember_InvalidNamespaceOrCharacter() { 62 | $helper = new ExtensionMemberManager(); 63 | $extension = new TestExtension(); 64 | $extension->setNamespace('test'); 65 | 66 | $this->expectException(InputException::class); 67 | 68 | $helper->addExtensionMember($extension, 'foo:bar', 'baz'); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/helpers/HttpStatusCodeManagerTest.php: -------------------------------------------------------------------------------- 1 | assertFalse($helper->hasHttpStatusCode()); 14 | $this->assertNull($helper->getHttpStatusCode()); 15 | 16 | $helper->setHttpStatusCode(204); 17 | 18 | $this->assertTrue($helper->hasHttpStatusCode()); 19 | $this->assertSame(204, $helper->getHttpStatusCode()); 20 | } 21 | 22 | public function testSetHttpStatusCode_InvalidForHttp() { 23 | $helper = new HttpStatusCodeManager(); 24 | 25 | $this->expectException(InputException::class); 26 | 27 | $helper->setHttpStatusCode(42); 28 | } 29 | 30 | public function testSetHttpStatusCode_AllowsYetUnknownHttpCodes() { 31 | $helper = new HttpStatusCodeManager(); 32 | 33 | $helper->setHttpStatusCode(299); 34 | 35 | $this->assertTrue($helper->hasHttpStatusCode()); 36 | $this->assertSame(299, $helper->getHttpStatusCode()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/helpers/LinksManagerTest.php: -------------------------------------------------------------------------------- 1 | addLink('foo', 'https://jsonapi.org'); 15 | 16 | $array = $linksManager->toArray(); 17 | 18 | $this->assertCount(1, $array); 19 | $this->assertArrayHasKey('foo', $array); 20 | $this->assertSame('https://jsonapi.org', $array['foo']); 21 | } 22 | 23 | public function testAppendLink_HappyPath() { 24 | $linksManager = new LinksManager(); 25 | $linksManager->appendLink('foo', 'https://jsonapi.org'); 26 | 27 | $array = $linksManager->toArray(); 28 | 29 | $this->assertCount(1, $array); 30 | $this->assertArrayHasKey('foo', $array); 31 | $this->assertCount(1, $array['foo']); 32 | $this->assertArrayHasKey(0, $array['foo']); 33 | $this->assertSame('https://jsonapi.org', $array['foo'][0]); 34 | } 35 | 36 | public function testAppendLink_WithMeta() { 37 | $linksManager = new LinksManager(); 38 | $linksManager->appendLink('foo', 'https://jsonapi.org', ['bar' => 'baz']); 39 | 40 | $array = $linksManager->toArray(); 41 | 42 | $this->assertCount(1, $array); 43 | $this->assertArrayHasKey('foo', $array); 44 | $this->assertCount(1, $array['foo']); 45 | $this->assertArrayHasKey(0, $array['foo']); 46 | $this->assertArrayHasKey('href', $array['foo'][0]); 47 | $this->assertArrayHasKey('meta', $array['foo'][0]); 48 | $this->assertArrayHasKey('bar', $array['foo'][0]['meta']); 49 | $this->assertSame('https://jsonapi.org', $array['foo'][0]['href']); 50 | $this->assertSame('baz', $array['foo'][0]['meta']['bar']); 51 | } 52 | 53 | public function testAppendLink_MultipleLinks() { 54 | $linksManager = new LinksManager(); 55 | $linksManager->appendLink('foo', 'https://jsonapi.org', ['bar' => 'baz']); 56 | $linksManager->appendLink('foo', 'https://jsonapi.org/2'); 57 | 58 | $array = $linksManager->toArray(); 59 | 60 | $this->assertCount(1, $array); 61 | $this->assertArrayHasKey('foo', $array); 62 | $this->assertCount(2, $array['foo']); 63 | $this->assertArrayHasKey(0, $array['foo']); 64 | $this->assertArrayHasKey(1, $array['foo']); 65 | $this->assertArrayHasKey('href', $array['foo'][0]); 66 | $this->assertArrayHasKey('meta', $array['foo'][0]); 67 | $this->assertArrayHasKey('bar', $array['foo'][0]['meta']); 68 | $this->assertSame('https://jsonapi.org', $array['foo'][0]['href']); 69 | $this->assertSame('baz', $array['foo'][0]['meta']['bar']); 70 | $this->assertSame('https://jsonapi.org/2', $array['foo'][1]); 71 | } 72 | 73 | public function testAddLinkObject_HappyPath() { 74 | $linksManager = new LinksManager(); 75 | $linksManager->addLinkObject('foo', new LinkObject('https://jsonapi.org')); 76 | 77 | $array = $linksManager->toArray(); 78 | 79 | $this->assertCount(1, $array); 80 | $this->assertArrayHasKey('foo', $array); 81 | $this->assertArrayHasKey('href', $array['foo']); 82 | $this->assertSame('https://jsonapi.org', $array['foo']['href']); 83 | } 84 | 85 | /** 86 | * @deprecated array links are not supported anymore 87 | */ 88 | public function testAddLinksArray_HappyPath() { 89 | $linksArray = new LinksArray(); 90 | $linksArray->add('https://jsonapi.org'); 91 | 92 | $linksManager = new LinksManager(); 93 | $linksManager->addLinksArray('foo', $linksArray); 94 | 95 | $array = $linksManager->toArray(); 96 | 97 | $this->assertCount(1, $array); 98 | $this->assertArrayHasKey('foo', $array); 99 | $this->assertCount(1, $array['foo']); 100 | $this->assertArrayHasKey(0, $array['foo']); 101 | $this->assertSame('https://jsonapi.org', $array['foo'][0]); 102 | } 103 | 104 | /** 105 | * @deprecated array links are not supported anymore 106 | */ 107 | public function testAppendLinkObject_HappyPath() { 108 | $linksManager = new LinksManager(); 109 | $linksManager->addLinksArray('foo', LinksArray::fromArray(['https://jsonapi.org/1'])); 110 | $linksManager->appendLinkObject('foo', new LinkObject('https://jsonapi.org/2')); 111 | $linksManager->appendLinkObject('foo', new LinkObject('https://jsonapi.org/3')); 112 | $linksManager->appendLinkObject('bar', new LinkObject('https://jsonapi.org/4')); 113 | 114 | $array = $linksManager->toArray(); 115 | 116 | $this->assertCount(2, $array); 117 | $this->assertArrayHasKey('foo', $array); 118 | $this->assertArrayHasKey('bar', $array); 119 | $this->assertCount(3, $array['foo']); 120 | $this->assertCount(1, $array['bar']); 121 | $this->assertArrayHasKey(0, $array['foo']); 122 | $this->assertArrayHasKey(1, $array['foo']); 123 | $this->assertArrayHasKey(2, $array['foo']); 124 | $this->assertArrayHasKey(0, $array['bar']); 125 | $this->assertArrayHasKey('href', $array['foo'][1]); 126 | $this->assertArrayHasKey('href', $array['foo'][2]); 127 | $this->assertArrayHasKey('href', $array['bar'][0]); 128 | $this->assertSame('https://jsonapi.org/1', $array['foo'][0]); 129 | $this->assertSame('https://jsonapi.org/2', $array['foo'][1]['href']); 130 | $this->assertSame('https://jsonapi.org/3', $array['foo'][2]['href']); 131 | $this->assertSame('https://jsonapi.org/4', $array['bar'][0]['href']); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /tests/helpers/TestableNonInterfaceRequestInterface.php: -------------------------------------------------------------------------------- 1 | selfLink = $selfLink; 18 | $this->queryParameters = $queryParameters; 19 | $this->document = $document; 20 | } 21 | 22 | /** 23 | * RequestInterface 24 | */ 25 | 26 | public function getUri() { 27 | return new TestableNonInterfaceUriInterface($this->selfLink, $this->queryParameters); 28 | } 29 | 30 | // not used in current implementation 31 | public function getRequestTarget() {} 32 | public function withRequestTarget($requestTarget) {} 33 | public function getMethod() {} 34 | public function withMethod($method) {} 35 | public function withUri(UriInterface $uri, $preserveHost = false) {} 36 | 37 | /** 38 | * MessageInterface 39 | */ 40 | 41 | public function getBody() { 42 | return new TestableNonInterfaceStreamInterface($this->document); 43 | } 44 | 45 | // not used in current implementation 46 | public function getProtocolVersion() {} 47 | public function withProtocolVersion($version) {} 48 | public function getHeaders() {} 49 | public function hasHeader($name) {} 50 | public function getHeader($name) {} 51 | public function getHeaderLine($name) {} 52 | public function withHeader($name, $value) {} 53 | public function withAddedHeader($name, $value) {} 54 | public function withoutHeader($name) {} 55 | public function withBody(StreamInterface $body) {} 56 | } 57 | -------------------------------------------------------------------------------- /tests/helpers/TestableNonInterfaceServerRequestInterface.php: -------------------------------------------------------------------------------- 1 | queryParameters; 15 | } 16 | 17 | // not used in current implementation 18 | public function getServerParams() {} 19 | public function getCookieParams() {} 20 | public function withCookieParams(array $cookies) {} 21 | public function withQueryParams(array $query) {} 22 | public function getUploadedFiles() {} 23 | public function withUploadedFiles(array $uploadedFiles) {} 24 | public function getParsedBody() {} 25 | public function withParsedBody($data) {} 26 | public function getAttributes() {} 27 | public function getAttribute($name, $default = null) {} 28 | public function withAttribute($name, $value) {} 29 | public function withoutAttribute($name) {} 30 | } 31 | -------------------------------------------------------------------------------- /tests/helpers/TestableNonInterfaceStreamInterface.php: -------------------------------------------------------------------------------- 1 | document = $document; 12 | } 13 | 14 | /** 15 | * StreamInterface 16 | */ 17 | 18 | public function getContents() { 19 | if ($this->document === null) { 20 | return ''; 21 | } 22 | 23 | return (string) json_encode($this->document); 24 | } 25 | 26 | // not used in current implementation 27 | public function __toString() {} 28 | public function close() {} 29 | public function detach() {} 30 | public function getSize() {} 31 | public function tell() {} 32 | public function eof() {} 33 | public function isSeekable() {} 34 | public function seek($offset, $whence = SEEK_SET) {} 35 | public function rewind() {} 36 | public function isWritable() {} 37 | public function write($string) {} 38 | public function isReadable() {} 39 | public function read($length) {} 40 | public function getMetadata($key = null) {} 41 | } 42 | -------------------------------------------------------------------------------- /tests/helpers/TestableNonInterfaceUriInterface.php: -------------------------------------------------------------------------------- 1 | selfLink = $selfLink; 13 | $this->queryParameters = $queryParameters; 14 | } 15 | 16 | /** 17 | * UriInterface 18 | */ 19 | 20 | public function getQuery() { 21 | return http_build_query($this->queryParameters); 22 | } 23 | 24 | public function __toString() { 25 | return $this->selfLink; 26 | } 27 | 28 | // not used in current implementation 29 | public function getScheme() {} 30 | public function getAuthority() {} 31 | public function getUserInfo() {} 32 | public function getHost() {} 33 | public function getPort() {} 34 | public function getPath() {} 35 | public function getFragment() {} 36 | public function withScheme($scheme) {} 37 | public function withUserInfo($user, $password = null) {} 38 | public function withHost($host) {} 39 | public function withPort($port) {} 40 | public function withPath($path) {} 41 | public function withQuery($query) {} 42 | public function withFragment($fragment) {} 43 | } 44 | -------------------------------------------------------------------------------- /tests/helpers/TestableNonTraitAtMemberManager.php: -------------------------------------------------------------------------------- 1 | links === null) { 15 | return []; 16 | } 17 | 18 | return $this->links->toArray(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/objects/AttributesObjectTest.php: -------------------------------------------------------------------------------- 1 | foo = 'bar'; 14 | 15 | $attributesObject = AttributesObject::fromObject($object); 16 | 17 | $array = $attributesObject->toArray(); 18 | 19 | $this->assertCount(1, $array); 20 | $this->assertArrayHasKey('foo', $array); 21 | $this->assertSame('bar', $array['foo']); 22 | } 23 | 24 | public function testAdd_HappyPath() { 25 | $attributesObject = new AttributesObject(); 26 | $attributesObject->add('foo', 'bar'); 27 | 28 | $array = $attributesObject->toArray(); 29 | 30 | $this->assertCount(1, $array); 31 | $this->assertArrayHasKey('foo', $array); 32 | $this->assertSame('bar', $array['foo']); 33 | } 34 | 35 | public function testAdd_WithObject() { 36 | $object = new \stdClass(); 37 | $object->bar = 'baz'; 38 | 39 | $attributesObject = new AttributesObject(); 40 | $attributesObject->add('foo', $object); 41 | 42 | $array = $attributesObject->toArray(); 43 | 44 | $this->assertCount(1, $array); 45 | $this->assertArrayHasKey('foo', $array); 46 | 47 | $this->assertCount(1, $array['foo']); 48 | $this->assertArrayHasKey('bar', $array['foo']); 49 | $this->assertSame('baz', $array['foo']['bar']); 50 | } 51 | 52 | /** 53 | * @group Extensions 54 | */ 55 | public function testAdd_BlocksExtensionMembersViaRegularAdd() { 56 | $attributesObject = new AttributesObject(); 57 | $extension = new TestExtension(); 58 | $extension->setNamespace('test'); 59 | 60 | $this->assertSame([], $attributesObject->toArray()); 61 | 62 | $this->expectException(InputException::class); 63 | $this->expectExceptionMessage('invalid member name "test:foo"'); 64 | 65 | $attributesObject->add('test:foo', 'bar'); 66 | } 67 | 68 | /** 69 | * @group Extensions 70 | */ 71 | public function testAddExtensionMember_HappyPath() { 72 | $attributesObject = new AttributesObject(); 73 | $extension = new TestExtension(); 74 | $extension->setNamespace('test'); 75 | 76 | $this->assertSame([], $attributesObject->toArray()); 77 | 78 | $attributesObject->addExtensionMember($extension, 'foo', 'bar'); 79 | 80 | $array = $attributesObject->toArray(); 81 | 82 | $this->assertArrayHasKey('test:foo', $array); 83 | $this->assertSame('bar', $array['test:foo']); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/objects/JsonapiObjectTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($jsonapiObject->isEmpty()); 15 | 16 | $jsonapiObject->addMeta('foo', 'bar'); 17 | 18 | $this->assertFalse($jsonapiObject->isEmpty()); 19 | 20 | $array = $jsonapiObject->toArray(); 21 | 22 | $this->assertArrayHasKey('meta', $array); 23 | $this->assertArrayHasKey('foo', $array['meta']); 24 | $this->assertSame('bar', $array['meta']['foo']); 25 | } 26 | 27 | public function testIsEmpty_WithAtMembers() { 28 | $jsonapiObject = new JsonapiObject($version=null); 29 | 30 | $this->assertTrue($jsonapiObject->isEmpty()); 31 | 32 | $jsonapiObject->addAtMember('context', 'test'); 33 | 34 | $this->assertFalse($jsonapiObject->isEmpty()); 35 | } 36 | 37 | /** 38 | * @group Extensions 39 | */ 40 | public function testIsEmpty_WithExtensionLink() { 41 | $jsonapiObject = new JsonapiObject($version=null); 42 | 43 | $this->assertTrue($jsonapiObject->isEmpty()); 44 | 45 | $jsonapiObject->addExtension(new TestExtension()); 46 | 47 | $this->assertFalse($jsonapiObject->isEmpty()); 48 | } 49 | 50 | /** 51 | * @group Profiles 52 | */ 53 | public function testIsEmpty_WithProfileLink() { 54 | $jsonapiObject = new JsonapiObject($version=null); 55 | 56 | $this->assertTrue($jsonapiObject->isEmpty()); 57 | 58 | $jsonapiObject->addProfile(new TestProfile()); 59 | 60 | $this->assertFalse($jsonapiObject->isEmpty()); 61 | } 62 | 63 | /** 64 | * @group Extensions 65 | */ 66 | public function testIsEmpty_WithExtensionMembers() { 67 | $jsonapiObject = new JsonapiObject($version=null); 68 | 69 | $this->assertTrue($jsonapiObject->isEmpty()); 70 | 71 | $jsonapiObject->addExtensionMember(new TestExtension(), 'foo', 'bar'); 72 | 73 | $this->assertFalse($jsonapiObject->isEmpty()); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/objects/LinkObjectTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($linkObject->isEmpty()); 14 | 15 | $linkObject->setDescribedBy('https://jsonapi.org'); 16 | 17 | $this->assertFalse($linkObject->isEmpty()); 18 | 19 | $array = $linkObject->toArray(); 20 | 21 | $this->assertArrayHasKey('describedby', $array); 22 | $this->assertArrayHasKey('href', $array['describedby']); 23 | $this->assertSame('https://jsonapi.org', $array['describedby']['href']); 24 | } 25 | 26 | public function testAddLanguage_HappyPath() { 27 | $linkObject = new LinkObject(); 28 | 29 | $this->assertTrue($linkObject->isEmpty()); 30 | 31 | $linkObject->addLanguage('nl-NL'); 32 | 33 | $this->assertFalse($linkObject->isEmpty()); 34 | 35 | $array = $linkObject->toArray(); 36 | 37 | $this->assertArrayHasKey('hreflang', $array); 38 | $this->assertSame('nl-NL', $array['hreflang']); 39 | } 40 | 41 | public function testAddLanguage_Multiple() { 42 | $linkObject = new LinkObject(); 43 | 44 | $linkObject->addLanguage('nl-NL'); 45 | $array = $linkObject->toArray(); 46 | $this->assertSame('nl-NL', $array['hreflang']); 47 | 48 | $linkObject->addLanguage('en-US'); 49 | $array = $linkObject->toArray(); 50 | $this->assertSame(['nl-NL', 'en-US'], $array['hreflang']); 51 | } 52 | 53 | public function testAddMeta_HappyPath() { 54 | $linkObject = new LinkObject(); 55 | 56 | $this->assertTrue($linkObject->isEmpty()); 57 | 58 | $linkObject->addMeta('foo', 'bar'); 59 | 60 | $this->assertFalse($linkObject->isEmpty()); 61 | 62 | $array = $linkObject->toArray(); 63 | 64 | $this->assertArrayHasKey('meta', $array); 65 | $this->assertArrayHasKey('foo', $array['meta']); 66 | $this->assertSame('bar', $array['meta']['foo']); 67 | } 68 | 69 | public function testSetRelationType_HappyPath() { 70 | $linkObject = new LinkObject(); 71 | 72 | $this->assertTrue($linkObject->isEmpty()); 73 | 74 | $linkObject->setRelationType('external'); 75 | 76 | $this->assertFalse($linkObject->isEmpty()); 77 | 78 | $array = $linkObject->toArray(); 79 | 80 | $this->assertArrayHasKey('rel', $array); 81 | $this->assertSame('external', $array['rel']); 82 | } 83 | 84 | public function testSetDescribedByLinkObject_HappyPath() { 85 | $linkObject = new LinkObject(); 86 | 87 | $this->assertTrue($linkObject->isEmpty()); 88 | 89 | $describedBy = new LinkObject('https://jsonapi.org'); 90 | $linkObject->setDescribedByLinkObject($describedBy); 91 | 92 | $this->assertFalse($linkObject->isEmpty()); 93 | 94 | $array = $linkObject->toArray(); 95 | 96 | $this->assertArrayHasKey('describedby', $array); 97 | $this->assertArrayHasKey('href', $array['describedby']); 98 | $this->assertSame('https://jsonapi.org', $array['describedby']['href']); 99 | } 100 | 101 | public function testSetHumanTitle_HappyPath() { 102 | $linkObject = new LinkObject(); 103 | 104 | $this->assertTrue($linkObject->isEmpty()); 105 | 106 | $linkObject->setHumanTitle('A link'); 107 | 108 | $this->assertFalse($linkObject->isEmpty()); 109 | 110 | $array = $linkObject->toArray(); 111 | 112 | $this->assertArrayHasKey('title', $array); 113 | $this->assertSame('A link', $array['title']); 114 | } 115 | 116 | public function testSetMediaType_HappyPath() { 117 | $linkObject = new LinkObject(); 118 | 119 | $this->assertTrue($linkObject->isEmpty()); 120 | 121 | $linkObject->setMediaType('text/html'); 122 | 123 | $this->assertFalse($linkObject->isEmpty()); 124 | 125 | $array = $linkObject->toArray(); 126 | 127 | $this->assertArrayHasKey('type', $array); 128 | $this->assertSame('text/html', $array['type']); 129 | } 130 | 131 | public function testSetHreflang_HappyPath() { 132 | $linkObject = new LinkObject(); 133 | 134 | $this->assertTrue($linkObject->isEmpty()); 135 | 136 | $linkObject->setHreflang('nl-NL', 'en-US'); 137 | 138 | $this->assertFalse($linkObject->isEmpty()); 139 | 140 | $array = $linkObject->toArray(); 141 | 142 | $this->assertArrayHasKey('hreflang', $array); 143 | $this->assertSame(['nl-NL', 'en-US'], $array['hreflang']); 144 | } 145 | 146 | public function testIsEmpty_WithAtMembers() { 147 | $linkObject = new LinkObject(); 148 | 149 | $this->assertTrue($linkObject->isEmpty()); 150 | 151 | $linkObject->addAtMember('context', 'test'); 152 | 153 | $this->assertFalse($linkObject->isEmpty()); 154 | } 155 | 156 | /** 157 | * @group Extensions 158 | */ 159 | public function testIsEmpty_WithExtensionMembers() { 160 | $linkObject = new LinkObject(); 161 | 162 | $this->assertTrue($linkObject->isEmpty()); 163 | 164 | $linkObject->addExtensionMember(new TestExtension(), 'foo', 'bar'); 165 | 166 | $this->assertFalse($linkObject->isEmpty()); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /tests/objects/LinksArrayTest.php: -------------------------------------------------------------------------------- 1 | foo = 'https://jsonapi.org'; 15 | 16 | $linksArray = LinksArray::fromObject($object); 17 | 18 | $array = $linksArray->toArray(); 19 | 20 | $this->assertCount(1, $array); 21 | $this->assertArrayHasKey(0, $array); 22 | $this->assertSame('https://jsonapi.org', $array[0]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/objects/MetaObjectTest.php: -------------------------------------------------------------------------------- 1 | foo = 'bar'; 13 | 14 | $metaObject = MetaObject::fromObject($object); 15 | 16 | $array = $metaObject->toArray(); 17 | 18 | $this->assertCount(1, $array); 19 | $this->assertArrayHasKey('foo', $array); 20 | $this->assertSame('bar', $array['foo']); 21 | } 22 | 23 | public function testIsEmpty_WithAtMembers() { 24 | $metaObject = new MetaObject(); 25 | 26 | $this->assertTrue($metaObject->isEmpty()); 27 | 28 | $metaObject->addAtMember('context', 'test'); 29 | 30 | $this->assertFalse($metaObject->isEmpty()); 31 | } 32 | 33 | /** 34 | * @group Extensions 35 | */ 36 | public function testIsEmpty_WithExtensionMembers() { 37 | $metaObject = new MetaObject(); 38 | 39 | $this->assertTrue($metaObject->isEmpty()); 40 | 41 | $metaObject->addExtensionMember(new TestExtension(), 'foo', 'bar'); 42 | 43 | $this->assertFalse($metaObject->isEmpty()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/objects/RelationshipsObjectTest.php: -------------------------------------------------------------------------------- 1 | add('foo', new ResourceObject('user', 42)); 16 | 17 | $array = $relationshipsObject->toArray(); 18 | 19 | $this->assertCount(1, $array); 20 | $this->assertArrayHasKey('foo', $array); 21 | $this->assertArrayHasKey('data', $array['foo']); 22 | $this->assertArrayHasKey('type', $array['foo']['data']); 23 | $this->assertArrayHasKey('id', $array['foo']['data']); 24 | $this->assertSame('user', $array['foo']['data']['type']); 25 | $this->assertSame('42', $array['foo']['data']['id']); 26 | } 27 | 28 | public function testAddRelationshipObject_HappyPath() { 29 | $relationshipObject = RelationshipObject::fromAnything(new ResourceObject('user', 42)); 30 | 31 | $relationshipsObject = new RelationshipsObject(); 32 | $relationshipsObject->addRelationshipObject($key='foo', $relationshipObject); 33 | 34 | $array = $relationshipsObject->toArray(); 35 | 36 | $this->assertCount(1, $array); 37 | $this->assertArrayHasKey('foo', $array); 38 | $this->assertArrayHasKey('data', $array['foo']); 39 | $this->assertArrayHasKey('type', $array['foo']['data']); 40 | $this->assertArrayHasKey('id', $array['foo']['data']); 41 | $this->assertSame('user', $array['foo']['data']['type']); 42 | $this->assertSame('42', $array['foo']['data']['id']); 43 | } 44 | 45 | public function testAddRelationshipObject_WithPredefinedKey() { 46 | $relationshipObject = RelationshipObject::fromAnything(new ResourceObject('user', 42)); 47 | 48 | $relationshipsObject = new RelationshipsObject(); 49 | $relationshipsObject->addRelationshipObject('foo', $relationshipObject); 50 | 51 | $array = $relationshipsObject->toArray(); 52 | 53 | $this->assertCount(1, $array); 54 | $this->assertArrayHasKey('foo', $array); 55 | $this->assertArrayHasKey('data', $array['foo']); 56 | $this->assertArrayHasKey('type', $array['foo']['data']); 57 | $this->assertArrayHasKey('id', $array['foo']['data']); 58 | $this->assertSame('user', $array['foo']['data']['type']); 59 | $this->assertSame('42', $array['foo']['data']['id']); 60 | } 61 | 62 | public function testAddRelationshipObject_InvalidKey() { 63 | $relationshipObject = RelationshipObject::fromAnything(new ResourceObject('user', 42)); 64 | $relationshipsObject = new RelationshipsObject(); 65 | 66 | $this->expectException(InputException::class); 67 | 68 | $relationshipsObject->addRelationshipObject($key='-foo', $relationshipObject); 69 | } 70 | 71 | public function testAddRelationshipObject_MultipleRelationships() { 72 | $relationshipObject = RelationshipObject::fromAnything(new ResourceObject('user', 42)); 73 | $relationshipsObject = new RelationshipsObject(); 74 | 75 | $relationshipsObject->addRelationshipObject($key='foo', $relationshipObject); 76 | $relationshipsObject->addRelationshipObject($key='bar', $relationshipObject); 77 | 78 | $array = $relationshipsObject->toArray(); 79 | 80 | $this->assertCount(2, $array); 81 | $this->assertArrayHasKey('foo', $array); 82 | $this->assertArrayHasKey('bar', $array); 83 | } 84 | 85 | public function testAddRelationshipObject_MultipleReusingKeys() { 86 | $relationshipObject = RelationshipObject::fromAnything(new ResourceObject('user', 42)); 87 | $relationshipsObject = new RelationshipsObject(); 88 | 89 | $relationshipsObject->addRelationshipObject($key='foo', $relationshipObject); 90 | 91 | $this->expectException(DuplicateException::class); 92 | 93 | $relationshipsObject->addRelationshipObject($key='foo', $relationshipObject); 94 | } 95 | 96 | public function testToArray_EmptyRelationship() { 97 | $relationshipObject = new RelationshipObject(RelationshipObject::TO_ONE); 98 | $relationshipsObject = new RelationshipsObject(); 99 | 100 | $relationshipsObject->addRelationshipObject($key='foo', $relationshipObject); 101 | 102 | $array = $relationshipsObject->toArray(); 103 | 104 | $this->assertFalse($relationshipsObject->isEmpty()); 105 | $this->assertCount(1, $array); 106 | $this->assertArrayHasKey('foo', $array); 107 | $this->assertArrayHasKey('data', $array['foo']); 108 | $this->assertNull($array['foo']['data']); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tests/profiles/TestProfile.php: -------------------------------------------------------------------------------- 1 | officialLink = $officialLink; 12 | } 13 | 14 | public function getOfficialLink() { 15 | return $this->officialLink; 16 | } 17 | } 18 | --------------------------------------------------------------------------------4 | 6 |tests 5 |7 | 26 |8 | 10 |src 9 |11 | 20 |src/interfaces 12 |src/base.php 13 |src/collection.php 14 |src/error.php 15 |src/errors.php 16 |src/exception.php 17 |src/resource.php 18 |src/response.php 19 |21 | 22 |23 | 24 | 25 |