From cb5fabd14d61ca091d6fd9a3e675d3c69b5706bc Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Tue, 13 Aug 2019 22:56:07 +0200 Subject: [PATCH 01/42] aliases are removed from the 1.1 spec --- examples/bootstrap_examples.php | 15 ++-- examples/example_profile.php | 4 +- src/Document.php | 10 +-- src/helpers/Converter.php | 4 +- src/helpers/ProfileAliasManager.php | 80 ----------------- src/interfaces/ProfileInterface.php | 52 ----------- src/objects/ProfileLinkObject.php | 47 ---------- src/profiles/CursorPaginationProfile.php | 30 ++++--- tests/ConverterTest.php | 14 +-- tests/DocumentTest.php | 30 +------ tests/SeparateProcessTest.php | 5 +- .../example_output/ExampleVersionProfile.php | 17 +--- .../example_profile/example_profile.json | 9 +- .../example_profile/example_profile.php | 2 +- tests/helpers/ProfileAliasManagerTest.php | 87 ------------------- ...TestableNonAbstractProfileAliasManager.php | 39 --------- .../profiles/CursorPaginationProfileTest.php | 84 +++++++++--------- tests/profiles/TestProfile.php | 14 ++- 18 files changed, 85 insertions(+), 458 deletions(-) delete mode 100644 src/helpers/ProfileAliasManager.php delete mode 100644 src/objects/ProfileLinkObject.php delete mode 100644 tests/helpers/ProfileAliasManagerTest.php delete mode 100644 tests/helpers/TestableNonAbstractProfileAliasManager.php diff --git a/examples/bootstrap_examples.php b/examples/bootstrap_examples.php index 2832970..40bc6cd 100644 --- a/examples/bootstrap_examples.php +++ b/examples/bootstrap_examples.php @@ -2,7 +2,6 @@ use alsvanzelf\jsonapi\Document; use alsvanzelf\jsonapi\ResourceDocument; -use alsvanzelf\jsonapi\helpers\ProfileAliasManager; use alsvanzelf\jsonapi\interfaces\ProfileInterface; use alsvanzelf\jsonapi\interfaces\ResourceInterface; @@ -102,17 +101,13 @@ function getCurrentLocation() { } } -class ExampleVersionProfile extends ProfileAliasManager implements ProfileInterface { +class ExampleVersionProfile implements ProfileInterface { /** - * the required methods (next to extending ProfileAliasManager) + * the required method */ public function getOfficialLink() { - return 'https://jsonapi.org/format/1.1/#profile-keywords-and-aliases'; - } - - public function getOfficialKeywords() { - return ['version']; + return 'https://jsonapi.org/format/1.1/#profile-keywords'; } /** @@ -121,10 +116,10 @@ public function getOfficialKeywords() { public function setVersion(ResourceInterface $resource, $version) { if ($resource instanceof ResourceDocument) { - $resource->addMeta($this->getKeyword('version'), $version, $level=Document::LEVEL_RESOURCE); + $resource->addMeta('version', $version, $level=Document::LEVEL_RESOURCE); } else { - $resource->addMeta($this->getKeyword('version'), $version); + $resource->addMeta('version', $version); } } } diff --git a/examples/example_profile.php b/examples/example_profile.php index 66cef95..ac12aa1 100644 --- a/examples/example_profile.php +++ b/examples/example_profile.php @@ -6,11 +6,9 @@ /** * use a profile as extension to the document - * - * allowing to define aliases for the keywords to solve conflicts between different profiles */ -$profile = new ExampleVersionProfile(['version' => 'ref']); +$profile = new ExampleVersionProfile(); $document = new ResourceDocument('user', 42); $document->applyProfile($profile); diff --git a/src/Document.php b/src/Document.php index 2967cd2..836990f 100644 --- a/src/Document.php +++ b/src/Document.php @@ -12,10 +12,8 @@ use alsvanzelf\jsonapi\interfaces\DocumentInterface; use alsvanzelf\jsonapi\interfaces\ProfileInterface; use alsvanzelf\jsonapi\objects\JsonapiObject; -use alsvanzelf\jsonapi\objects\LinkObject; use alsvanzelf\jsonapi\objects\LinksObject; use alsvanzelf\jsonapi\objects\MetaObject; -use alsvanzelf\jsonapi\objects\ProfileLinkObject; /** * @see ResourceDocument, CollectionDocument, ErrorsDocument or MetaDocument @@ -196,13 +194,7 @@ public function applyProfile(ProfileInterface $profile) { $this->setLinksObject(new LinksObject()); } - $link = $profile->getAliasedLink(); - if ($link instanceof LinkObject) { - $this->links->appendLinkObject('profile', $link); - } - else { - $this->links->append('profile', $link); - } + $this->links->append('profile', $profile->getOfficialLink()); } /** diff --git a/src/helpers/Converter.php b/src/helpers/Converter.php index aa66da9..0d16caf 100644 --- a/src/helpers/Converter.php +++ b/src/helpers/Converter.php @@ -4,7 +4,6 @@ use alsvanzelf\jsonapi\interfaces\ObjectInterface; use alsvanzelf\jsonapi\interfaces\ProfileInterface; -use alsvanzelf\jsonapi\objects\LinkObject; /** * @internal @@ -48,8 +47,7 @@ public static function mergeProfilesInContentType($contentType, array $profiles) $profileLinks = []; foreach ($profiles as $profile) { - $link = $profile->getAliasedLink(); - $profileLinks[] = ($link instanceof LinkObject) ? $link->toArray()['href'] : $link; + $profileLinks[] = $profile->getOfficialLink(); } $profileLinks = implode(' ', $profileLinks); diff --git a/src/helpers/ProfileAliasManager.php b/src/helpers/ProfileAliasManager.php deleted file mode 100644 index 1ef0075..0000000 --- a/src/helpers/ProfileAliasManager.php +++ /dev/null @@ -1,80 +0,0 @@ -getOfficialKeywords(); - if ($officialKeywords === []) { - return; - } - - $this->keywordMapping = array_combine($officialKeywords, $officialKeywords); - if ($aliases === []) { - return; - } - - foreach ($aliases as $keyword => $alias) { - if ($alias === $keyword) { - throw new InputException('an alias should be different from its keyword'); - } - if (in_array($keyword, $officialKeywords, $strict=true) === false) { - throw new InputException('unknown keyword "'.$keyword.'" to alias'); - } - Validator::checkMemberName($alias); - - $this->keywordMapping[$keyword] = $alias; - } - - $this->aliasMapping = $aliases; - } - - /** - * @inheritDoc - */ - public function getKeyword($keyword) { - if (isset($this->keywordMapping[$keyword]) === false) { - throw new InputException('unknown keyword "'.$keyword.'"'); - } - - return $this->keywordMapping[$keyword]; - } - - /** - * @inheritDoc - */ - abstract public function getOfficialKeywords(); - - /** - * @inheritDoc - */ - abstract public function getOfficialLink(); - - /** - * @inheritDoc - */ - public function getAliasedLink() { - if ($this->aliasMapping === []) { - return $this->getOfficialLink(); - } - - return new ProfileLinkObject($this->getOfficialLink(), $this->aliasMapping); - } -} diff --git a/src/interfaces/ProfileInterface.php b/src/interfaces/ProfileInterface.php index 3e6df08..b4a6f2f 100644 --- a/src/interfaces/ProfileInterface.php +++ b/src/interfaces/ProfileInterface.php @@ -2,48 +2,7 @@ namespace alsvanzelf\jsonapi\interfaces; -use alsvanzelf\jsonapi\exceptions\InputException; -use alsvanzelf\jsonapi\objects\LinkObject; - -/** - * @see ProfileAliasManager which implement most of the methods - */ interface ProfileInterface { - /** - * get a profile with its aliases to keywords of the profile - * - * having this in the constructor makes sure the aliases are used from the start - * - * @param array $aliases optional mapping keywords to aliases - * - * @throws InputException if the alias is not different from the keyword - * @throws InputException if the keyword is not known to the profile - * @throws InputException if the alias is not a valid member name - */ - public function __construct(array $aliases=[]); - - /** - * get the keyword or current alias based on the official keyword from the profile - * - * e.g. for a profile defining an official keyword 'version', this would return 'version' - * or if ->alias('version', 'v') was called before, this would return 'v' - * - * @param string $keyword - * @return string - * - * @throws InputException if the keyword is not known to the profile - */ - public function getKeyword($keyword); - - /** - * returns an array of official keywords this profile defines - * - * @internal - * - * @return string[] - */ - public function getOfficialKeywords(); - /** * the unique link identifying and describing the profile * @@ -52,15 +11,4 @@ public function getOfficialKeywords(); * @return string */ public function getOfficialLink(); - - /** - * get the official link, or a LinkObject with the link and its aliases - * - * optionally also contains the aliases applied - * - * @internal - * - * @return LinkObject|string - */ - public function getAliasedLink(); } diff --git a/src/objects/ProfileLinkObject.php b/src/objects/ProfileLinkObject.php deleted file mode 100644 index 4652a53..0000000 --- a/src/objects/ProfileLinkObject.php +++ /dev/null @@ -1,47 +0,0 @@ -aliases = $aliases; - } - - /** - * human api - */ - - /** - * spec api - */ - - /** - * ObjectInterface - */ - - /** - * @inheritDoc - */ - public function toArray() { - $array = parent::toArray(); - - if ($this->aliases !== []) { - $array['aliases'] = $this->aliases; - } - - return $array; - } -} diff --git a/src/profiles/CursorPaginationProfile.php b/src/profiles/CursorPaginationProfile.php index 1700f92..77d9fc2 100644 --- a/src/profiles/CursorPaginationProfile.php +++ b/src/profiles/CursorPaginationProfile.php @@ -4,7 +4,6 @@ use alsvanzelf\jsonapi\Document; use alsvanzelf\jsonapi\ResourceDocument; -use alsvanzelf\jsonapi\helpers\ProfileAliasManager; use alsvanzelf\jsonapi\interfaces\PaginableInterface; use alsvanzelf\jsonapi\interfaces\ProfileInterface; use alsvanzelf\jsonapi\interfaces\ResourceInterface; @@ -44,7 +43,7 @@ * - {@see get*ErrorObject} to generate ErrorObjects for specific error cases * - {@see generatePreviousLink} {@see generateNextLink} to apply the links manually */ -class CursorPaginationProfile extends ProfileAliasManager implements ProfileInterface { +class CursorPaginationProfile implements ProfileInterface { /** * human api */ @@ -115,7 +114,7 @@ public function setCount(PaginableInterface $paginable, $exactTotal=null, $bestG * @return string */ public function generatePreviousLink($baseOrCurrentUrl, $beforeCursor) { - return $this->setQueryParameter($baseOrCurrentUrl, $this->getKeyword('page').'[before]', $beforeCursor); + return $this->setQueryParameter($baseOrCurrentUrl, 'page[before]', $beforeCursor); } /** @@ -126,7 +125,7 @@ public function generatePreviousLink($baseOrCurrentUrl, $beforeCursor) { * @return string */ public function generateNextLink($baseOrCurrentUrl, $afterCursor) { - return $this->setQueryParameter($baseOrCurrentUrl, $this->getKeyword('page').'[after]', $afterCursor); + return $this->setQueryParameter($baseOrCurrentUrl, 'page[after]', $afterCursor); } /** @@ -192,10 +191,10 @@ public function setItemMeta(ResourceInterface $resource, $cursor) { ]; if ($resource instanceof ResourceDocument) { - $resource->addMeta($this->getKeyword('page'), $metadata, $level=Document::LEVEL_RESOURCE); + $resource->addMeta('page', $metadata, $level=Document::LEVEL_RESOURCE); } else { - $resource->addMeta($this->getKeyword('page'), $metadata); + $resource->addMeta('page', $metadata); } } @@ -229,7 +228,7 @@ public function setPaginationMeta(PaginableInterface $paginable, $exactTotal=nul $metadata['rangeTruncated'] = $rangeIsTruncated; } - $paginable->addMeta($this->getKeyword('page'), $metadata); + $paginable->addMeta('page', $metadata); } /** @@ -280,9 +279,9 @@ public function getUnsupportedSortErrorObject($genericTitle=null, $specificDetai public function getMaxPageSizeExceededErrorObject($maxSize, $genericTitle=null, $specificDetails=null) { $errorObject = new ErrorObject('Max page size exceeded'); $errorObject->appendTypeLink('https://jsonapi.org/profiles/ethanresnick/cursor-pagination/max-size-exceeded'); - $errorObject->blameQueryParameter($this->getKeyword('page').'[size]'); + $errorObject->blameQueryParameter('page[size]'); $errorObject->setHttpStatusCode(400); - $errorObject->addMeta($this->getKeyword('page'), $value=['maxSize' => $maxSize]); + $errorObject->addMeta('page', $value=['maxSize' => $maxSize]); if ($genericTitle !== null) { $errorObject->setHumanExplanation($genericTitle, $specificDetails); @@ -302,7 +301,7 @@ public function getMaxPageSizeExceededErrorObject($maxSize, $genericTitle=null, * - /errors/0/title optional * - /errors/0/detail optional * - * @param int $queryParameter e.g. 'sort' or 'page[size]', aliasing should already be done using {@see getKeyword} + * @param int $queryParameter e.g. 'sort' or 'page[size]' * @param string $typeLink optional * @param string $genericTitle optional, e.g. 'Invalid Parameter.' * @param string $specificDetails optional, e.g. 'page[size] must be a positive integer; got 0' @@ -394,9 +393,14 @@ public function getOfficialLink() { } /** - * @inheritDoc + * returns the keyword without aliasing + * + * @deprecated since aliasing was removed from the profiles spec + * + * @param string $keyword + * @return string */ - public function getOfficialKeywords() { - return ['page']; + public function getKeyword($keyword) { + return $keyword; } } diff --git a/tests/ConverterTest.php b/tests/ConverterTest.php index ad33478..85839c9 100644 --- a/tests/ConverterTest.php +++ b/tests/ConverterTest.php @@ -4,7 +4,6 @@ use alsvanzelf\jsonapi\helpers\Converter; use alsvanzelf\jsonapi\objects\AttributesObject; -use alsvanzelf\jsonapi\objects\LinkObject; use alsvanzelf\jsonapiTests\profiles\TestProfile; use PHPUnit\Framework\TestCase; @@ -70,24 +69,17 @@ public function testMergeProfilesInContentType_HappyPath() { public function testMergeProfilesInContentType_WithProfileStringLink() { $profile = new TestProfile(); - $profile->setAliasedLink('bar'); - - $this->assertSame('foo;profile="bar", foo', Converter::mergeProfilesInContentType('foo', [$profile])); - } - - public function testMergeProfilesInContentType_WithProfileObjectLink() { - $profile = new TestProfile(); - $profile->setAliasedLink(new LinkObject('bar')); + $profile->setOfficialLink('bar'); $this->assertSame('foo;profile="bar", foo', Converter::mergeProfilesInContentType('foo', [$profile])); } public function testMergeProfilesInContentType_WithMultipleProfiles() { $profile1 = new TestProfile(); - $profile1->setAliasedLink('bar'); + $profile1->setOfficialLink('bar'); $profile2 = new TestProfile(); - $profile2->setAliasedLink(new LinkObject('baz')); + $profile2->setOfficialLink('baz'); $this->assertSame('foo;profile="bar baz", foo', Converter::mergeProfilesInContentType('foo', [$profile1, $profile2])); } diff --git a/tests/DocumentTest.php b/tests/DocumentTest.php index 2cd0916..61e9936 100644 --- a/tests/DocumentTest.php +++ b/tests/DocumentTest.php @@ -5,7 +5,6 @@ use alsvanzelf\jsonapi\exceptions\Exception; use alsvanzelf\jsonapi\exceptions\InputException; use alsvanzelf\jsonapi\objects\LinkObject; -use alsvanzelf\jsonapi\objects\ProfileLinkObject; use alsvanzelf\jsonapiTests\TestableNonAbstractDocument as Document; use alsvanzelf\jsonapiTests\profiles\TestProfile; use PHPUnit\Framework\TestCase; @@ -180,7 +179,7 @@ public function testAddLinkObject_HappyPath() { public function testApplyProfile_HappyPath() { $profile = new TestProfile(); - $profile->setAliasedLink('https://jsonapi.org'); + $profile->setOfficialLink('https://jsonapi.org'); $document = new Document(); $document->applyProfile($profile); @@ -195,33 +194,6 @@ public function testApplyProfile_HappyPath() { $this->assertSame('https://jsonapi.org', $array['links']['profile'][0]); } - public function testApplyProfile_WithLinkObject() { - $profile = new TestProfile(); - $profile->setAliasedLink(new ProfileLinkObject('https://jsonapi.org', $aliases=['foo' => 'bar'], $meta=['baz' => 'baf'])); - - $document = new Document(); - $document->applyProfile($profile); - - $array = $document->toArray(); - - $this->assertArrayHasKey('links', $array); - $this->assertCount(1, $array['links']); - $this->assertArrayHasKey('profile', $array['links']); - $this->assertCount(1, $array['links']['profile']); - $this->assertArrayHasKey(0, $array['links']['profile']); - $this->assertCount(3, $array['links']['profile'][0]); - $this->assertArrayHasKey('href', $array['links']['profile'][0]); - $this->assertArrayHasKey('aliases', $array['links']['profile'][0]); - $this->assertArrayHasKey('meta', $array['links']['profile'][0]); - $this->assertSame('https://jsonapi.org', $array['links']['profile'][0]['href']); - $this->assertCount(1, $array['links']['profile'][0]['aliases']); - $this->assertArrayHasKey('foo', $array['links']['profile'][0]['aliases']); - $this->assertSame('bar', $array['links']['profile'][0]['aliases']['foo']); - $this->assertCount(1, $array['links']['profile'][0]['meta']); - $this->assertArrayHasKey('baz', $array['links']['profile'][0]['meta']); - $this->assertSame('baf', $array['links']['profile'][0]['meta']['baz']); - } - public function testToJson_HappyPath() { $document = new Document(); diff --git a/tests/SeparateProcessTest.php b/tests/SeparateProcessTest.php index dc8c9e1..4c7938f 100644 --- a/tests/SeparateProcessTest.php +++ b/tests/SeparateProcessTest.php @@ -2,7 +2,6 @@ namespace alsvanzelf\jsonapiTests; -use alsvanzelf\jsonapi\objects\ProfileLinkObject; use alsvanzelf\jsonapiTests\TestableNonAbstractDocument as Document; use alsvanzelf\jsonapiTests\profiles\TestProfile; use PHPUnit\Framework\TestCase; @@ -74,7 +73,7 @@ public function testSendResponse_ContentTypeHeader() { */ public function testSendResponse_ContentTypeHeaderWithProfiles() { $profile = new TestProfile(); - $profile->setAliasedLink('https://jsonapi.org'); + $profile->setOfficialLink('https://jsonapi.org'); $document = new Document(); $document->applyProfile($profile); @@ -85,7 +84,7 @@ public function testSendResponse_ContentTypeHeaderWithProfiles() { $this->assertSame(['Content-Type: '.Document::CONTENT_TYPE_OFFICIAL.';profile="https://jsonapi.org", '.Document::CONTENT_TYPE_OFFICIAL], xdebug_get_headers()); $profile = new TestProfile(); - $profile->setAliasedLink('https://jsonapi.org/2'); + $profile->setOfficialLink('https://jsonapi.org/2'); $document->applyProfile($profile); ob_start(); diff --git a/tests/example_output/ExampleVersionProfile.php b/tests/example_output/ExampleVersionProfile.php index 824a2e6..266be3f 100644 --- a/tests/example_output/ExampleVersionProfile.php +++ b/tests/example_output/ExampleVersionProfile.php @@ -4,21 +4,12 @@ use alsvanzelf\jsonapi\Document; use alsvanzelf\jsonapi\ResourceDocument; -use alsvanzelf\jsonapi\helpers\ProfileAliasManager; use alsvanzelf\jsonapi\interfaces\ProfileInterface; use alsvanzelf\jsonapi\interfaces\ResourceInterface; -class ExampleVersionProfile extends ProfileAliasManager implements ProfileInterface { - /** - * the required methods (next to extending ProfileAliasManager) - */ - +class ExampleVersionProfile implements ProfileInterface { public function getOfficialLink() { - return 'https://jsonapi.org/format/1.1/#profile-keywords-and-aliases'; - } - - public function getOfficialKeywords() { - return ['version']; + return 'https://jsonapi.org/format/1.1/#profile-keywords'; } /** @@ -27,10 +18,10 @@ public function getOfficialKeywords() { public function setVersion(ResourceInterface $resource, $version) { if ($resource instanceof ResourceDocument) { - $resource->addMeta($this->getKeyword('version'), $version, $level=Document::LEVEL_RESOURCE); + $resource->addMeta('version', $version, $level=Document::LEVEL_RESOURCE); } else { - $resource->addMeta($this->getKeyword('version'), $version); + $resource->addMeta('version', $version); } } } diff --git a/tests/example_output/example_profile/example_profile.json b/tests/example_output/example_profile/example_profile.json index e7ee9ff..53daa9f 100644 --- a/tests/example_output/example_profile/example_profile.json +++ b/tests/example_output/example_profile/example_profile.json @@ -4,19 +4,14 @@ }, "links": { "profile": [ - { - "href": "https://jsonapi.org/format/1.1/#profile-keywords-and-aliases", - "aliases": { - "version": "ref" - } - } + "https://jsonapi.org/format/1.1/#profile-keywords" ] }, "data": { "type": "user", "id": "42", "meta": { - "ref": "2019" + "version": "2019" } } } diff --git a/tests/example_output/example_profile/example_profile.php b/tests/example_output/example_profile/example_profile.php index c881f25..65b3bb4 100644 --- a/tests/example_output/example_profile/example_profile.php +++ b/tests/example_output/example_profile/example_profile.php @@ -7,7 +7,7 @@ class example_profile { public static function createJsonapiDocument() { - $profile = new ExampleVersionProfile(['version' => 'ref']); + $profile = new ExampleVersionProfile(); $document = new ResourceDocument('user', 42); $document->applyProfile($profile); diff --git a/tests/helpers/ProfileAliasManagerTest.php b/tests/helpers/ProfileAliasManagerTest.php deleted file mode 100644 index c1a1fed..0000000 --- a/tests/helpers/ProfileAliasManagerTest.php +++ /dev/null @@ -1,87 +0,0 @@ -assertSame([], $profileAliasManager->getAliasMapping()); - $this->assertSame(['foo' => 'foo', 'bar' => 'bar'], $profileAliasManager->getKeywordMapping()); - } - - public function testConstructor_WithAliases() { - $profileAliasManager = new ProfileAliasManager(['bar' => 'baz']); - - $this->assertSame(['bar' => 'baz'], $profileAliasManager->getAliasMapping()); - $this->assertSame(['foo' => 'foo', 'bar' => 'baz'], $profileAliasManager->getKeywordMapping()); - } - - public function testConstructor_WithoutOfficialKeywords() { - $profileAliasManager = new ProfileAliasManager_WithoutKeywords(); - - $this->assertSame([], $profileAliasManager->getAliasMapping()); - $this->assertSame([], $profileAliasManager->getKeywordMapping()); - } - - public function testConstructor_NonAdjustedAliases() { - $this->expectException(InputException::class); - - new ProfileAliasManager(['foo' => 'foo']); - } - - public function testConstructor_NonExistingKeyword() { - $this->expectException(InputException::class); - - new ProfileAliasManager(['baz' => 'bar']); - } - - public function testGetKeyword_HappyPath() { - $profileAliasManager = new ProfileAliasManager(['bar' => 'baz']); - - $this->assertSame('foo', $profileAliasManager->getKeyword('foo')); - $this->assertSame('baz', $profileAliasManager->getKeyword('bar')); - } - - public function testGetKeyword_NonExistingKeyword() { - $profileAliasManager = new ProfileAliasManager(); - - $this->expectException(InputException::class); - - $profileAliasManager->getKeyword('baz'); - } - - public function testGetAliasedLink_HappyPath() { - $profileAliasManager = new ProfileAliasManager(); - - if (method_exists($this, 'assertIsString')) { - $this->assertIsString($profileAliasManager->getAliasedLink()); - } - else { - $this->assertInternalType('string', $profileAliasManager->getAliasedLink()); - } - $this->assertSame('https://jsonapi.org', $profileAliasManager->getAliasedLink()); - } - - public function testGetAliasedLink_ObjectWithAliases() { - $profileAliasManager = new ProfileAliasManager(['bar' => 'baz']); - - $this->assertInstanceOf(ProfileLinkObject::class, $profileAliasManager->getAliasedLink()); - - $array = $profileAliasManager->getAliasedLink()->toArray(); - - $this->assertArrayHasKey('href', $array); - $this->assertSame('https://jsonapi.org', $array['href']); - - $this->assertArrayHasKey('aliases', $array); - $this->assertCount(1, $array['aliases']); - $this->assertArrayHasKey('bar', $array['aliases']); - $this->assertSame('baz', $array['aliases']['bar']); - } -} diff --git a/tests/helpers/TestableNonAbstractProfileAliasManager.php b/tests/helpers/TestableNonAbstractProfileAliasManager.php deleted file mode 100644 index e73d7dd..0000000 --- a/tests/helpers/TestableNonAbstractProfileAliasManager.php +++ /dev/null @@ -1,39 +0,0 @@ -setAccessible(true); - - return $aliasMapping->getValue($this); - } - - public function getKeywordMapping() { - $keywordMapping = new \ReflectionProperty(ProfileAliasManager::class, 'keywordMapping'); - $keywordMapping->setAccessible(true); - - return $keywordMapping->getValue($this); - } - - public function getOfficialKeywords() { - return ['foo', 'bar']; - } - - public function getOfficialLink() { - return 'https://jsonapi.org'; - } -} - -class TestableNonAbstractProfileAliasManager_WithoutKeywords extends TestableNonAbstractProfileAliasManager { - public function getOfficialKeywords() { - return []; - } -} diff --git a/tests/profiles/CursorPaginationProfileTest.php b/tests/profiles/CursorPaginationProfileTest.php index 183df74..533f43a 100644 --- a/tests/profiles/CursorPaginationProfileTest.php +++ b/tests/profiles/CursorPaginationProfileTest.php @@ -14,9 +14,9 @@ */ class CursorPaginationProfileTest extends TestCase { public function testSetLinks_HappyPath() { - $profile = new CursorPaginationProfile(['page' => 'pagination']); + $profile = new CursorPaginationProfile(); $collection = new CollectionDocument(); - $baseOrCurrentUrl = '/people?'.$profile->getKeyword('page').'[size]=10'; + $baseOrCurrentUrl = '/people?page[size]=10'; $firstCursor = 'bar'; $lastCursor = 'foo'; @@ -30,12 +30,12 @@ public function testSetLinks_HappyPath() { $this->assertArrayHasKey('next', $array['links']); $this->assertArrayHasKey('href', $array['links']['prev']); $this->assertArrayHasKey('href', $array['links']['next']); - $this->assertSame('/people?'.$profile->getKeyword('page').'[size]=10&'.$profile->getKeyword('page').'[before]='.$firstCursor, $array['links']['prev']['href']); - $this->assertSame('/people?'.$profile->getKeyword('page').'[size]=10&'.$profile->getKeyword('page').'[after]='.$lastCursor, $array['links']['next']['href']); + $this->assertSame('/people?page[size]=10&page[before]='.$firstCursor, $array['links']['prev']['href']); + $this->assertSame('/people?page[size]=10&page[after]='.$lastCursor, $array['links']['next']['href']); } public function test_WithRelationship() { - $profile = new CursorPaginationProfile(['page' => 'pagination']); + $profile = new CursorPaginationProfile(); $document = new ResourceDocument('test', 1); $person1 = new ResourceObject('person', 1); @@ -45,7 +45,7 @@ public function test_WithRelationship() { $profile->setCursor($person2, 'arthur'); $profile->setCursor($person42, 'zaphod'); - $baseOrCurrentUrl = '/people?'.$profile->getKeyword('page').'[size]=10'; + $baseOrCurrentUrl = '/people?page[size]=10'; $firstCursor = 'ford'; $lastCursor = 'zaphod'; $exactTotal = 3; @@ -67,22 +67,22 @@ public function test_WithRelationship() { $this->assertArrayHasKey('meta', $array['data']['relationships']['people']); $this->assertArrayHasKey('prev', $array['data']['relationships']['people']['links']); $this->assertArrayHasKey('next', $array['data']['relationships']['people']['links']); - $this->assertArrayHasKey('pagination', $array['data']['relationships']['people']['meta']); + $this->assertArrayHasKey('page', $array['data']['relationships']['people']['meta']); $this->assertArrayHasKey('href', $array['data']['relationships']['people']['links']['prev']); $this->assertArrayHasKey('href', $array['data']['relationships']['people']['links']['next']); - $this->assertArrayHasKey('total', $array['data']['relationships']['people']['meta']['pagination']); - $this->assertArrayHasKey('estimatedTotal', $array['data']['relationships']['people']['meta']['pagination']); - $this->assertArrayHasKey('bestGuess', $array['data']['relationships']['people']['meta']['pagination']['estimatedTotal']); + $this->assertArrayHasKey('total', $array['data']['relationships']['people']['meta']['page']); + $this->assertArrayHasKey('estimatedTotal', $array['data']['relationships']['people']['meta']['page']); + $this->assertArrayHasKey('bestGuess', $array['data']['relationships']['people']['meta']['page']['estimatedTotal']); $this->assertCount(3, $array['data']['relationships']['people']['data']); $this->assertArrayHasKey('meta', $array['data']['relationships']['people']['data'][0]); - $this->assertArrayHasKey('pagination', $array['data']['relationships']['people']['data'][0]['meta']); - $this->assertArrayHasKey('cursor', $array['data']['relationships']['people']['data'][0]['meta']['pagination']); + $this->assertArrayHasKey('page', $array['data']['relationships']['people']['data'][0]['meta']); + $this->assertArrayHasKey('cursor', $array['data']['relationships']['people']['data'][0]['meta']['page']); } public function testSetLinksFirstPage_HappyPath() { - $profile = new CursorPaginationProfile(['page' => 'pagination']); + $profile = new CursorPaginationProfile(); $collection = new CollectionDocument(); - $baseOrCurrentUrl = '/people?'.$profile->getKeyword('page').'[size]=10'; + $baseOrCurrentUrl = '/people?page[size]=10'; $lastCursor = 'foo'; $profile->setLinksFirstPage($collection, $baseOrCurrentUrl, $lastCursor); @@ -95,13 +95,13 @@ public function testSetLinksFirstPage_HappyPath() { $this->assertArrayHasKey('next', $array['links']); $this->assertNull($array['links']['prev']); $this->assertArrayHasKey('href', $array['links']['next']); - $this->assertSame('/people?'.$profile->getKeyword('page').'[size]=10&'.$profile->getKeyword('page').'[after]='.$lastCursor, $array['links']['next']['href']); + $this->assertSame('/people?page[size]=10&page[after]='.$lastCursor, $array['links']['next']['href']); } public function testSetLinksLastPage_HappyPath() { - $profile = new CursorPaginationProfile(['page' => 'pagination']); + $profile = new CursorPaginationProfile(); $collection = new CollectionDocument(); - $baseOrCurrentUrl = '/people?'.$profile->getKeyword('page').'[size]=10'; + $baseOrCurrentUrl = '/people?page[size]=10'; $firstCursor = 'bar'; $profile->setLinksLastPage($collection, $baseOrCurrentUrl, $firstCursor); @@ -114,11 +114,11 @@ public function testSetLinksLastPage_HappyPath() { $this->assertArrayHasKey('next', $array['links']); $this->assertArrayHasKey('href', $array['links']['prev']); $this->assertNull($array['links']['next']); - $this->assertSame('/people?'.$profile->getKeyword('page').'[size]=10&'.$profile->getKeyword('page').'[before]='.$firstCursor, $array['links']['prev']['href']); + $this->assertSame('/people?page[size]=10&page[before]='.$firstCursor, $array['links']['prev']['href']); } public function testSetCursor() { - $profile = new CursorPaginationProfile(['page' => 'pagination']); + $profile = new CursorPaginationProfile(); $resourceDocument = new ResourceDocument('user', 42); $profile->setCursor($resourceDocument, 'foo'); @@ -127,13 +127,13 @@ public function testSetCursor() { $this->assertArrayHasKey('data', $array); $this->assertArrayHasKey('meta', $array['data']); - $this->assertArrayHasKey('pagination', $array['data']['meta']); - $this->assertArrayHasKey('cursor', $array['data']['meta']['pagination']); - $this->assertSame('foo', $array['data']['meta']['pagination']['cursor']); + $this->assertArrayHasKey('page', $array['data']['meta']); + $this->assertArrayHasKey('cursor', $array['data']['meta']['page']); + $this->assertSame('foo', $array['data']['meta']['page']['cursor']); } public function testSetPaginationLinkObjectsExplicitlyEmpty_HapptPath() { - $profile = new CursorPaginationProfile(['page' => 'pagination']); + $profile = new CursorPaginationProfile(); $collection = new CollectionDocument(); $profile->setPaginationLinkObjectsExplicitlyEmpty($collection); @@ -149,7 +149,7 @@ public function testSetPaginationLinkObjectsExplicitlyEmpty_HapptPath() { } public function testSetPaginationMeta() { - $profile = new CursorPaginationProfile(['page' => 'pagination']); + $profile = new CursorPaginationProfile(); $collection = new CollectionDocument(); $exactTotal = 42; $bestGuessTotal = 100; @@ -160,18 +160,18 @@ public function testSetPaginationMeta() { $array = $collection->toArray(); $this->assertArrayHasKey('meta', $array); - $this->assertArrayHasKey('pagination', $array['meta']); - $this->assertArrayHasKey('total', $array['meta']['pagination']); - $this->assertArrayHasKey('estimatedTotal', $array['meta']['pagination']); - $this->assertArrayHasKey('bestGuess', $array['meta']['pagination']['estimatedTotal']); - $this->assertArrayHasKey('rangeTruncated', $array['meta']['pagination']); - $this->assertSame(42, $array['meta']['pagination']['total']); - $this->assertSame(100, $array['meta']['pagination']['estimatedTotal']['bestGuess']); - $this->assertSame(true, $array['meta']['pagination']['rangeTruncated']); + $this->assertArrayHasKey('page', $array['meta']); + $this->assertArrayHasKey('total', $array['meta']['page']); + $this->assertArrayHasKey('estimatedTotal', $array['meta']['page']); + $this->assertArrayHasKey('bestGuess', $array['meta']['page']['estimatedTotal']); + $this->assertArrayHasKey('rangeTruncated', $array['meta']['page']); + $this->assertSame(42, $array['meta']['page']['total']); + $this->assertSame(100, $array['meta']['page']['estimatedTotal']['bestGuess']); + $this->assertSame(true, $array['meta']['page']['rangeTruncated']); } public function testGetUnsupportedSortErrorObject_HappyPath() { - $profile = new CursorPaginationProfile(['page' => 'pagination']); + $profile = new CursorPaginationProfile(); $genericTitle = 'foo'; $specificDetails = 'bar'; @@ -197,7 +197,7 @@ public function testGetUnsupportedSortErrorObject_HappyPath() { } public function testGetMaxPageSizeExceededErrorObject_HappyPath() { - $profile = new CursorPaginationProfile(['page' => 'pagination']); + $profile = new CursorPaginationProfile(); $maxSize = 42; $genericTitle = 'foo'; $specificDetails = 'bar'; @@ -215,21 +215,21 @@ public function testGetMaxPageSizeExceededErrorObject_HappyPath() { $this->assertArrayHasKey('source', $array); $this->assertArrayHasKey('parameter', $array['source']); $this->assertArrayHasKey('meta', $array); - $this->assertArrayHasKey('pagination', $array['meta']); - $this->assertArrayHasKey('maxSize', $array['meta']['pagination']); + $this->assertArrayHasKey('page', $array['meta']); + $this->assertArrayHasKey('maxSize', $array['meta']['page']); $this->assertCount(1, $array['links']['type']); $this->assertSame('400', $array['status']); $this->assertSame('Max page size exceeded', $array['code']); $this->assertSame($genericTitle, $array['title']); $this->assertSame($specificDetails, $array['detail']); - $this->assertSame('pagination[size]', $array['source']['parameter']); + $this->assertSame('page[size]', $array['source']['parameter']); $this->assertSame('https://jsonapi.org/profiles/ethanresnick/cursor-pagination/max-size-exceeded', $array['links']['type'][0]); - $this->assertSame(42, $array['meta']['pagination']['maxSize']); + $this->assertSame(42, $array['meta']['page']['maxSize']); } public function testGetInvalidParameterValueErrorObject_HappyPath() { - $profile = new CursorPaginationProfile(['page' => 'pagination']); - $queryParameter = 'pagination[size]'; + $profile = new CursorPaginationProfile(); + $queryParameter = 'page[size]'; $typeLink = 'https://jsonapi.org'; $genericTitle = 'foo'; $specificDetails = 'bar'; @@ -251,12 +251,12 @@ public function testGetInvalidParameterValueErrorObject_HappyPath() { $this->assertSame('Invalid parameter value', $array['code']); $this->assertSame($genericTitle, $array['title']); $this->assertSame($specificDetails, $array['detail']); - $this->assertSame('pagination[size]', $array['source']['parameter']); + $this->assertSame('page[size]', $array['source']['parameter']); $this->assertSame('https://jsonapi.org', $array['links']['type'][0]); } public function testGetRangePaginationNotSupportedErrorObject_HappyPath() { - $profile = new CursorPaginationProfile(['page' => 'pagination']); + $profile = new CursorPaginationProfile(); $genericTitle = 'foo'; $specificDetails = 'bar'; diff --git a/tests/profiles/TestProfile.php b/tests/profiles/TestProfile.php index 91fe146..331e1a8 100644 --- a/tests/profiles/TestProfile.php +++ b/tests/profiles/TestProfile.php @@ -5,17 +5,13 @@ use alsvanzelf\jsonapi\interfaces\ProfileInterface; class TestProfile implements ProfileInterface { - private $aliasedLink; + private $officialLink; - public function setAliasedLink($aliasedLink) { - $this->aliasedLink = $aliasedLink; + public function setOfficialLink($officialLink) { + $this->officialLink = $officialLink; } - public function __construct(array $aliases=[]) {} - public function getKeyword($keyword) {} - public function getOfficialKeywords() {} - public function getOfficialLink() {} - public function getAliasedLink() { - return $this->aliasedLink; + public function getOfficialLink() { + return $this->officialLink; } } From 47d867fc06a1fbc37b30d46ca9407efed1d9cb04 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Tue, 20 Oct 2020 22:26:01 +0200 Subject: [PATCH 02/42] allow adding local id if there is no normal id --- src/helpers/Validator.php | 1 + src/objects/ResourceIdentifierObject.php | 25 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/helpers/Validator.php b/src/helpers/Validator.php index 6e814d7..12d26fd 100644 --- a/src/helpers/Validator.php +++ b/src/helpers/Validator.php @@ -12,6 +12,7 @@ class Validator { const OBJECT_CONTAINER_TYPE = 'type'; const OBJECT_CONTAINER_ID = 'id'; + const OBJECT_CONTAINER_LID = 'lid'; const OBJECT_CONTAINER_ATTRIBUTES = 'attributes'; const OBJECT_CONTAINER_RELATIONSHIPS = 'relationships'; diff --git a/src/objects/ResourceIdentifierObject.php b/src/objects/ResourceIdentifierObject.php index fe32d91..cf68344 100644 --- a/src/objects/ResourceIdentifierObject.php +++ b/src/objects/ResourceIdentifierObject.php @@ -16,6 +16,8 @@ class ResourceIdentifierObject implements ObjectInterface, ResourceInterface { protected $type; /** @var string */ protected $id; + /** @var string */ + protected $lid; /** @var MetaObject */ protected $meta; /** @var Validator */ @@ -42,6 +44,7 @@ public function __construct($type=null, $id=null) { // always mark as used, as these keys are reserved $this->validator->claimUsedFields($fieldNames=['type'], Validator::OBJECT_CONTAINER_TYPE); $this->validator->claimUsedFields($fieldNames=['id'], Validator::OBJECT_CONTAINER_ID); + $this->validator->claimUsedFields($fieldNames=['lid'], Validator::OBJECT_CONTAINER_LID); } /** @@ -73,11 +76,30 @@ public function setType($type) { /** * @param string|int $id will be casted to a string + * + * @throws DuplicateException if localId is already set */ public function setId($id) { + if ($this->lid !== null) { + throw new DuplicateException('id is not allowed when localId is already set'); + } + $this->id = (string) $id; } + /** + * @param string|int $localId will be casted to a string + * + * @throws DuplicateException if normal id is already set + */ + public function setLocalId($localId) { + if ($this->id !== null) { + throw new DuplicateException('localId is not allowed when id is already set'); + } + + $this->lid = (string) $localId; + } + /** * @param MetaObject $metaObject */ @@ -179,6 +201,9 @@ public function toArray() { if ($this->id !== null) { $array['id'] = $this->id; } + elseif ($this->lid !== null) { + $array['lid'] = $this->lid; + } if ($this->meta !== null && $this->meta->isEmpty() === false) { $array['meta'] = $this->meta->toArray(); From f6481f4c2b6e765bf321e1d26dee9f4cdd8cddd1 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Tue, 20 Oct 2020 22:26:22 +0200 Subject: [PATCH 03/42] make sure to pass along the local id when we normally pass the id --- src/objects/ResourceIdentifierObject.php | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/objects/ResourceIdentifierObject.php b/src/objects/ResourceIdentifierObject.php index cf68344..03a43ca 100644 --- a/src/objects/ResourceIdentifierObject.php +++ b/src/objects/ResourceIdentifierObject.php @@ -118,7 +118,7 @@ public function setMetaObject(MetaObject $metaObject) { * @return ResourceIdentifierObject */ public static function fromResourceObject(ResourceObject $resourceObject) { - $resourceIdentifierObject = new self($resourceObject->type, $resourceObject->id); + $resourceIdentifierObject = new self($resourceObject->type, $resourceObject->primaryId()); if ($resourceObject->meta !== null) { $resourceIdentifierObject->setMetaObject($resourceObject->meta); @@ -149,7 +149,7 @@ public function equals(ResourceInterface $resource) { * @return boolean */ public function hasIdentification() { - return ($this->type !== null && $this->id !== null); + return ($this->type !== null && $this->primaryId() !== null); } /** @@ -166,7 +166,7 @@ public function getIdentificationKey() { throw new Exception('resource has no identification yet'); } - return $this->type.'|'.$this->id; + return $this->type.'|'.$this->primaryId(); } /** @@ -177,7 +177,7 @@ public function getIdentificationKey() { * @inheritDoc */ public function isEmpty() { - if ($this->type !== null || $this->id !== null) { + if ($this->type !== null || $this->primaryId() !== null) { return false; } if ($this->meta !== null && $this->meta->isEmpty() === false) { @@ -222,4 +222,16 @@ public function toArray() { public function getResource($identifierOnly=false) { return $this; } + + /** + * @internal + */ + + private function primaryId() { + if ($this->lid !== null) { + return $this->lid; + } + + return $this->id; + } } From defb86d5f27710f018bfb10f6caf3b2937bbf728 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Tue, 20 Oct 2020 22:28:55 +0200 Subject: [PATCH 04/42] easily set a json schema or other describing link to the document --- src/Document.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Document.php b/src/Document.php index 2967cd2..935199b 100644 --- a/src/Document.php +++ b/src/Document.php @@ -122,6 +122,20 @@ public function setSelfLink($href, array $meta=[], $level=Document::LEVEL_ROOT) $this->addLink('self', $href, $meta, $level); } + /** + * set a link describing the current document + * + * for example this could link to an OpenAPI or JSON Schema document + * + * @note according to the spec, this can only be set to Document::LEVEL_ROOT + * + * @param string $href + * @param array $meta optional, if given a LinkObject is added, otherwise a link string is added + */ + public function setDescribedByLink($href, array $meta=[]) { + $this->addLink('describedby', $href, $meta, $level=Document::LEVEL_ROOT); + } + /** * @param string $key * @param mixed $value From e886ae42ca5d5b94a12fb6431ef2363be094fee5 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Tue, 20 Oct 2020 22:31:06 +0200 Subject: [PATCH 05/42] support the new link 'native-meta' properties --- src/objects/LinkObject.php | 84 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/src/objects/LinkObject.php b/src/objects/LinkObject.php index 1eab36d..bc23cb4 100644 --- a/src/objects/LinkObject.php +++ b/src/objects/LinkObject.php @@ -11,6 +11,16 @@ class LinkObject implements ObjectInterface { /** @var string */ protected $href; + /** @var string */ + protected $rel; + /** @var LinkObject */ + protected $describedby; + /** @var string */ + protected $title; + /** @var string */ + protected $type; + /** @var string[] */ + protected $hreflang; /** @var MetaObject */ protected $meta; @@ -31,6 +41,25 @@ public function __construct($href=null, array $meta=[]) { * human api */ + /** + * @param string $href + */ + public function setDescribedBy($href) { + $this->setDescribedByLinkObject(new LinkObject($href)); + } + + /** + * @param string $language + */ + public function addLanguage($language) { + if ($this->hreflang === null) { + $this->setHreflang($language); + } + else { + $this->setHreflang(...$this->hreflang, $language); + } + } + /** * @param string $key * @param mixed $value @@ -54,6 +83,41 @@ public function setHref($href) { $this->href = $href; } + /** + * @param string $relationType + */ + public function setRelationType($relationType) { + $this->rel = $relationType; + } + + /** + * @param LinkObject $describedBy + */ + public function setDescribedByLinkObject(LinkObject $describedBy) { + $this->describedby = $describedBy; + } + + /** + * @param string $friendlyTitle + */ + public function setHumanTitle($humanTitle) { + $this->title = $humanTitle; + } + + /** + * @param string $mediaType + */ + public function setMediaType($mediaType) { + $this->type = $mediaType; + } + + /** + * @param string ...$hreflang + */ + public function setHreflang(...$hreflang) { + $this->hreflang = $hreflang; + } + /** * @param MetaObject $metaObject */ @@ -90,6 +154,26 @@ public function toArray() { $array['href'] = $this->href; + if ($this->rel) { + $array['rel'] = $this->rel; + } + if ($this->title) { + $array['title'] = $this->title; + } + if ($this->type) { + $array['type'] = $this->type; + } + if ($this->hreflang) { + if (count($this->hreflang) === 1) { + $array['hreflang'] = $this->hreflang[0]; + } + else { + $array['hreflang'] = $this->hreflang; + } + } + if ($this->describedby !== null && $this->describedby->isEmpty() === false) { + $array['describedby'] = $this->describedby->toArray(); + } if ($this->meta !== null && $this->meta->isEmpty() === false) { $array['meta'] = $this->meta->toArray(); } From b13f3269d1636c7e040a436b61b98fb6107aebe5 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sat, 24 Oct 2020 11:40:40 +0200 Subject: [PATCH 06/42] fix type checking --- src/objects/LinkObject.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/objects/LinkObject.php b/src/objects/LinkObject.php index bc23cb4..63fd793 100644 --- a/src/objects/LinkObject.php +++ b/src/objects/LinkObject.php @@ -163,7 +163,7 @@ public function toArray() { if ($this->type) { $array['type'] = $this->type; } - if ($this->hreflang) { + if ($this->hreflang !== []) { if (count($this->hreflang) === 1) { $array['hreflang'] = $this->hreflang[0]; } From 17eea59fe6a0187bfd5701e79d26ab648d937ac0 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Mon, 5 Apr 2021 11:06:06 +0200 Subject: [PATCH 07/42] add interface for extensions --- src/interfaces/ExtensionInterface.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/interfaces/ExtensionInterface.php diff --git a/src/interfaces/ExtensionInterface.php b/src/interfaces/ExtensionInterface.php new file mode 100644 index 0000000..3c07497 --- /dev/null +++ b/src/interfaces/ExtensionInterface.php @@ -0,0 +1,21 @@ + Date: Mon, 5 Apr 2021 11:28:55 +0200 Subject: [PATCH 08/42] add extension and profiles links in the jsonapi object and content-type --- src/Document.php | 32 ++++++++++++++++++++++----- src/helpers/Converter.php | 41 +++++++++++++++++++++++++---------- src/objects/JsonapiObject.php | 38 ++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 17 deletions(-) diff --git a/src/Document.php b/src/Document.php index 836990f..e07ad33 100644 --- a/src/Document.php +++ b/src/Document.php @@ -10,6 +10,7 @@ use alsvanzelf\jsonapi\helpers\LinksManager; use alsvanzelf\jsonapi\helpers\Validator; use alsvanzelf\jsonapi\interfaces\DocumentInterface; +use alsvanzelf\jsonapi\interfaces\ExtensionInterface; use alsvanzelf\jsonapi\interfaces\ProfileInterface; use alsvanzelf\jsonapi\objects\JsonapiObject; use alsvanzelf\jsonapi\objects\LinksObject; @@ -37,6 +38,8 @@ abstract class Document implements DocumentInterface, \JsonSerializable { protected $meta; /** @var JsonapiObject */ protected $jsonapi; + /** @var ExtensionInterface[] */ + protected $extensions = []; /** @var ProfileInterface[] */ protected $profiles = []; /** @var array */ @@ -176,6 +179,25 @@ public function unsetJsonapiObject() { $this->jsonapi = null; } + /** + * apply a extension which adds the link and sets a correct content-type + * + * note that the rules from the extension are not automatically enforced + * applying the rules, and applying them correctly, is manual + * however the $extension could have custom methods to help + * + * @see https://jsonapi.org/extensions/#extensions + * + * @param ExtensionInterface $extension + */ + public function applyExtension(ExtensionInterface $extension) { + $this->extensions[] = $extension; + + if ($this->jsonapi !== null) { + $this->jsonapi->addExtension($extension); + } + } + /** * apply a profile which adds the link and sets a correct content-type * @@ -183,18 +205,16 @@ public function unsetJsonapiObject() { * applying the rules, and applying them correctly, is manual * however the $profile could have custom methods to help * - * @see https://jsonapi.org/format/1.1/#profiles + * @see https://jsonapi.org/extensions/#profiles * * @param ProfileInterface $profile */ public function applyProfile(ProfileInterface $profile) { $this->profiles[] = $profile; - if ($this->links === null) { - $this->setLinksObject(new LinksObject()); + if ($this->jsonapi !== null) { + $this->jsonapi->addProfile($profile); } - - $this->links->append('profile', $profile->getOfficialLink()); } /** @@ -259,7 +279,7 @@ public function sendResponse(array $options=[]) { http_response_code($this->httpStatusCode); - $contentType = Converter::mergeProfilesInContentType($options['contentType'], $this->profiles); + $contentType = Converter::prepareContentType($options['contentType'], $this->extensions, $this->profiles); header('Content-Type: '.$contentType); echo $json; diff --git a/src/helpers/Converter.php b/src/helpers/Converter.php index 0d16caf..7d77267 100644 --- a/src/helpers/Converter.php +++ b/src/helpers/Converter.php @@ -2,6 +2,7 @@ namespace alsvanzelf\jsonapi\helpers; +use alsvanzelf\jsonapi\interfaces\ExtensionInterface; use alsvanzelf\jsonapi\interfaces\ObjectInterface; use alsvanzelf\jsonapi\interfaces\ProfileInterface; @@ -34,23 +35,41 @@ public static function camelCaseToWords($camelCase) { } /** - * generates the value for a content type header, with profiles merged in if available + * generates the value for a content type header, with extensions and profiles merged in if available * - * @param string $contentType - * @param ProfileInterface[] $profiles + * @param string $contentType + * @param ExtensionInterface[] $extensions + * @param ProfileInterface[] $profiles * @return string */ - public static function mergeProfilesInContentType($contentType, array $profiles) { - if ($profiles === []) { - return $contentType; + public static function prepareContentType($contentType, array $extensions, array $profiles) { + if ($extensions !== []) { + $extensionLinks = []; + foreach ($extensions as $extension) { + $extensionLinks[] = $extension->getOfficialLink(); + } + $extensionLinks = implode(' ', $extensionLinks); + + $contentType .= '; ext="'.$extensionLinks.'"'; } - $profileLinks = []; - foreach ($profiles as $profile) { - $profileLinks[] = $profile->getOfficialLink(); + if ($profiles !== []) { + $profileLinks = []; + foreach ($profiles as $profile) { + $profileLinks[] = $profile->getOfficialLink(); + } + $profileLinks = implode(' ', $profileLinks); + + $contentType .= '; profile="'.$profileLinks.'"'; } - $profileLinks = implode(' ', $profileLinks); - return $contentType.';profile="'.$profileLinks.'", '.$contentType; + return $contentType; + } + + /** + * @deprecated {@see prepareContentType()} + */ + public static function mergeProfilesInContentType($contentType, array $profiles) { + return self::prepareContentType($contentType, $extensions=[], $profiles); } } diff --git a/src/objects/JsonapiObject.php b/src/objects/JsonapiObject.php index ad04da2..a774864 100644 --- a/src/objects/JsonapiObject.php +++ b/src/objects/JsonapiObject.php @@ -4,7 +4,9 @@ use alsvanzelf\jsonapi\Document; use alsvanzelf\jsonapi\helpers\AtMemberManager; +use alsvanzelf\jsonapi\interfaces\ExtensionInterface; use alsvanzelf\jsonapi\interfaces\ObjectInterface; +use alsvanzelf\jsonapi\interfaces\ProfileInterface; use alsvanzelf\jsonapi\objects\MetaObject; class JsonapiObject implements ObjectInterface { @@ -12,6 +14,10 @@ class JsonapiObject implements ObjectInterface { /** @var string */ protected $version; + /** @var ExtensionInterface[] */ + protected $extensions = []; + /** @var ProfileInterface */ + protected $profiles = []; /** @var MetaObject */ protected $meta; @@ -51,6 +57,20 @@ public function setVersion($version) { $this->version = $version; } + /** + * @param ExtensionInterface $extension + */ + public function addExtension(ExtensionInterface $extension) { + $this->extensions[] = $extension; + } + + /** + * @param ProfileInterface $profile + */ + public function addProfile(ProfileInterface $profile) { + $this->profiles[] = $profile; + } + /** * @param MetaObject $metaObject */ @@ -69,6 +89,12 @@ public function isEmpty() { if ($this->version !== null) { return false; } + if ($this->extensions !== []) { + return false; + } + if ($this->profiles !== []) { + return false; + } if ($this->meta !== null && $this->meta->isEmpty() === false) { return false; } @@ -88,6 +114,18 @@ public function toArray() { if ($this->version !== null) { $array['version'] = $this->version; } + if ($this->extensions !== []) { + $array['ext'] = []; + foreach ($this->extensions as $extension) { + $array['ext'][] = $extension->getOfficialLink(); + } + } + if ($this->profiles !== []) { + $array['profile'] = []; + foreach ($this->profiles as $profile) { + $array['profile'][] = $profile->getOfficialLink(); + } + } if ($this->meta !== null && $this->meta->isEmpty() === false) { $array['meta'] = $this->meta->toArray(); } From d9988dbeb114dd7ecc26032a59104c5565f53f68 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Mon, 5 Apr 2021 11:44:21 +0200 Subject: [PATCH 09/42] allow to set extension members --- src/Document.php | 12 ++++- src/helpers/ExtensionMemberManager.php | 63 ++++++++++++++++++++++++ src/objects/AttributesObject.php | 26 ++++++++-- src/objects/ErrorObject.php | 14 +++++- src/objects/JsonapiObject.php | 14 +++++- src/objects/LinkObject.php | 15 +++++- src/objects/LinksObject.php | 24 +++++++-- src/objects/MetaObject.php | 26 ++++++++-- src/objects/RelationshipObject.php | 15 +++++- src/objects/RelationshipsObject.php | 24 +++++++-- src/objects/ResourceIdentifierObject.php | 15 +++++- 11 files changed, 224 insertions(+), 24 deletions(-) create mode 100644 src/helpers/ExtensionMemberManager.php diff --git a/src/Document.php b/src/Document.php index e07ad33..f11ca25 100644 --- a/src/Document.php +++ b/src/Document.php @@ -6,6 +6,7 @@ use alsvanzelf\jsonapi\exceptions\InputException; use alsvanzelf\jsonapi\helpers\AtMemberManager; use alsvanzelf\jsonapi\helpers\Converter; +use alsvanzelf\jsonapi\helpers\ExtensionMemberManager; use alsvanzelf\jsonapi\helpers\HttpStatusCodeManager; use alsvanzelf\jsonapi\helpers\LinksManager; use alsvanzelf\jsonapi\helpers\Validator; @@ -20,7 +21,7 @@ * @see ResourceDocument, CollectionDocument, ErrorsDocument or MetaDocument */ abstract class Document implements DocumentInterface, \JsonSerializable { - use AtMemberManager, HttpStatusCodeManager, LinksManager; + use AtMemberManager, ExtensionMemberManager, HttpStatusCodeManager, LinksManager; const JSONAPI_VERSION_1_0 = '1.0'; const JSONAPI_VERSION_1_1 = '1.1'; @@ -225,7 +226,14 @@ public function applyProfile(ProfileInterface $profile) { * @inheritDoc */ public function toArray() { - $array = $this->getAtMembers(); + $array = []; + + if ($this->hasAtMembers()) { + $array = array_merge($array, $this->getAtMembers()); + } + if ($this->hasExtensionMembers()) { + $array = array_merge($array, $this->getExtensionMembers()); + } if ($this->jsonapi !== null && $this->jsonapi->isEmpty() === false) { $array['jsonapi'] = $this->jsonapi->toArray(); diff --git a/src/helpers/ExtensionMemberManager.php b/src/helpers/ExtensionMemberManager.php new file mode 100644 index 0000000..4261628 --- /dev/null +++ b/src/helpers/ExtensionMemberManager.php @@ -0,0 +1,63 @@ +getNamespace(); + + if (strpos($key, $namespace.':') === 0) { + $key = substr($key, strlen($namespace.':')); + } + + Validator::checkMemberName($key); + + if (is_object($value)) { + $value = Converter::objectToArray($value); + } + + $this->extensionMembers[$namespace.':'.$key] = $value; + } + + /** + * internal api + */ + + /** + * @internal + * + * @return boolean + */ + public function hasExtensionMembers() { + return ($this->extensionMembers !== []); + } + + /** + * @internal + * + * @return array + */ + public function getExtensionMembers() { + return $this->extensionMembers; + } +} diff --git a/src/objects/AttributesObject.php b/src/objects/AttributesObject.php index 5cbc5b1..867c45a 100644 --- a/src/objects/AttributesObject.php +++ b/src/objects/AttributesObject.php @@ -4,11 +4,12 @@ use alsvanzelf\jsonapi\helpers\AtMemberManager; use alsvanzelf\jsonapi\helpers\Converter; +use alsvanzelf\jsonapi\helpers\ExtensionMemberManager; use alsvanzelf\jsonapi\helpers\Validator; use alsvanzelf\jsonapi\interfaces\ObjectInterface; class AttributesObject implements ObjectInterface { - use AtMemberManager; + use AtMemberManager, ExtensionMemberManager; /** @var array */ protected $attributes = []; @@ -85,13 +86,32 @@ public function getKeys() { * @inheritDoc */ public function isEmpty() { - return ($this->attributes === [] && $this->hasAtMembers() === false); + if ($this->attributes !== []) { + return false; + } + if ($this->hasAtMembers()) { + return false; + } + if ($this->hasExtensionMembers()) { + return false; + } + + return true; } /** * @inheritDoc */ public function toArray() { - return array_merge($this->getAtMembers(), $this->attributes); + $array = []; + + if ($this->hasAtMembers()) { + $array = array_merge($array, $this->getAtMembers()); + } + if ($this->hasExtensionMembers()) { + $array = array_merge($array, $this->getExtensionMembers()); + } + + return array_merge($array, $this->attributes); } } diff --git a/src/objects/ErrorObject.php b/src/objects/ErrorObject.php index 1473b04..9f0eade 100644 --- a/src/objects/ErrorObject.php +++ b/src/objects/ErrorObject.php @@ -6,13 +6,14 @@ use alsvanzelf\jsonapi\exceptions\InputException; use alsvanzelf\jsonapi\helpers\AtMemberManager; use alsvanzelf\jsonapi\helpers\Converter; +use alsvanzelf\jsonapi\helpers\ExtensionMemberManager; use alsvanzelf\jsonapi\helpers\HttpStatusCodeManager; use alsvanzelf\jsonapi\helpers\LinksManager; use alsvanzelf\jsonapi\helpers\Validator; use alsvanzelf\jsonapi\interfaces\ObjectInterface; class ErrorObject implements ObjectInterface { - use AtMemberManager, HttpStatusCodeManager, LinksManager; + use AtMemberManager, ExtensionMemberManager, HttpStatusCodeManager, LinksManager; /** @var string */ protected $id; @@ -288,6 +289,9 @@ public function isEmpty() { if ($this->hasAtMembers()) { return false; } + if ($this->hasExtensionMembers()) { + return false; + } return true; } @@ -296,8 +300,14 @@ public function isEmpty() { * @inheritDoc */ public function toArray() { - $array = $this->getAtMembers(); + $array = []; + if ($this->hasAtMembers()) { + $array = array_merge($array, $this->getAtMembers()); + } + if ($this->hasExtensionMembers()) { + $array = array_merge($array, $this->getExtensionMembers()); + } if ($this->id !== null) { $array['id'] = $this->id; } diff --git a/src/objects/JsonapiObject.php b/src/objects/JsonapiObject.php index a774864..980b0b2 100644 --- a/src/objects/JsonapiObject.php +++ b/src/objects/JsonapiObject.php @@ -4,13 +4,14 @@ use alsvanzelf\jsonapi\Document; use alsvanzelf\jsonapi\helpers\AtMemberManager; +use alsvanzelf\jsonapi\helpers\ExtensionMemberManager; use alsvanzelf\jsonapi\interfaces\ExtensionInterface; use alsvanzelf\jsonapi\interfaces\ObjectInterface; use alsvanzelf\jsonapi\interfaces\ProfileInterface; use alsvanzelf\jsonapi\objects\MetaObject; class JsonapiObject implements ObjectInterface { - use AtMemberManager; + use AtMemberManager, ExtensionMemberManager; /** @var string */ protected $version; @@ -101,6 +102,9 @@ public function isEmpty() { if ($this->hasAtMembers()) { return false; } + if ($this->hasExtensionMembers()) { + return false; + } return true; } @@ -109,8 +113,14 @@ public function isEmpty() { * @inheritDoc */ public function toArray() { - $array = $this->getAtMembers(); + $array = []; + if ($this->hasAtMembers()) { + $array = array_merge($array, $this->getAtMembers()); + } + if ($this->hasExtensionMembers()) { + $array = array_merge($array, $this->getExtensionMembers()); + } if ($this->version !== null) { $array['version'] = $this->version; } diff --git a/src/objects/LinkObject.php b/src/objects/LinkObject.php index 1eab36d..b1d05d4 100644 --- a/src/objects/LinkObject.php +++ b/src/objects/LinkObject.php @@ -3,11 +3,12 @@ namespace alsvanzelf\jsonapi\objects; use alsvanzelf\jsonapi\helpers\AtMemberManager; +use alsvanzelf\jsonapi\helpers\ExtensionMemberManager; use alsvanzelf\jsonapi\interfaces\ObjectInterface; use alsvanzelf\jsonapi\objects\MetaObject; class LinkObject implements ObjectInterface { - use AtMemberManager; + use AtMemberManager, ExtensionMemberManager; /** @var string */ protected $href; @@ -78,6 +79,9 @@ public function isEmpty() { if ($this->hasAtMembers()) { return false; } + if ($this->hasExtensionMembers()) { + return false; + } return true; } @@ -86,7 +90,14 @@ public function isEmpty() { * @inheritDoc */ public function toArray() { - $array = $this->getAtMembers(); + $array = []; + + if ($this->hasAtMembers()) { + $array = array_merge($array, $this->getAtMembers()); + } + if ($this->hasExtensionMembers()) { + $array = array_merge($array, $this->getExtensionMembers()); + } $array['href'] = $this->href; diff --git a/src/objects/LinksObject.php b/src/objects/LinksObject.php index 9e6347b..67e9640 100644 --- a/src/objects/LinksObject.php +++ b/src/objects/LinksObject.php @@ -5,13 +5,14 @@ use alsvanzelf\jsonapi\exceptions\DuplicateException; use alsvanzelf\jsonapi\helpers\AtMemberManager; use alsvanzelf\jsonapi\helpers\Converter; +use alsvanzelf\jsonapi\helpers\ExtensionMemberManager; use alsvanzelf\jsonapi\helpers\Validator; use alsvanzelf\jsonapi\interfaces\ObjectInterface; use alsvanzelf\jsonapi\objects\LinkObject; use alsvanzelf\jsonapi\objects\LinksArray; class LinksObject implements ObjectInterface { - use AtMemberManager; + use AtMemberManager, ExtensionMemberManager; /** @var array with string|LinkObject */ protected $links = []; @@ -161,14 +162,31 @@ public function appendLinkObject($key, LinkObject $linkObject) { * @inheritDoc */ public function isEmpty() { - return ($this->links === [] && $this->hasAtMembers() === false); + if ($this->links !== []) { + return false; + } + if ($this->hasAtMembers()) { + return false; + } + if ($this->hasExtensionMembers()) { + return false; + } + + return true; } /** * @inheritDoc */ public function toArray() { - $array = $this->getAtMembers(); + $array = []; + + if ($this->hasAtMembers()) { + $array = array_merge($array, $this->getAtMembers()); + } + if ($this->hasExtensionMembers()) { + $array = array_merge($array, $this->getExtensionMembers()); + } foreach ($this->links as $key => $link) { if ($link instanceof LinkObject && $link->isEmpty() === false) { diff --git a/src/objects/MetaObject.php b/src/objects/MetaObject.php index f31eb28..03e9e1e 100644 --- a/src/objects/MetaObject.php +++ b/src/objects/MetaObject.php @@ -4,11 +4,12 @@ use alsvanzelf\jsonapi\helpers\AtMemberManager; use alsvanzelf\jsonapi\helpers\Converter; +use alsvanzelf\jsonapi\helpers\ExtensionMemberManager; use alsvanzelf\jsonapi\helpers\Validator; use alsvanzelf\jsonapi\interfaces\ObjectInterface; class MetaObject implements ObjectInterface { - use AtMemberManager; + use AtMemberManager, ExtensionMemberManager; /** @var array */ protected $meta = []; @@ -67,13 +68,32 @@ public function add($key, $value) { * @inheritDoc */ public function isEmpty() { - return ($this->meta === [] && $this->hasAtMembers() === false); + if ($this->meta !== []) { + return false; + } + if ($this->hasAtMembers()) { + return false; + } + if ($this->hasExtensionMembers()) { + return false; + } + + return true; } /** * @inheritDoc */ public function toArray() { - return array_merge($this->getAtMembers(), $this->meta); + $array = []; + + if ($this->hasAtMembers()) { + $array = array_merge($array, $this->getAtMembers()); + } + if ($this->hasExtensionMembers()) { + $array = array_merge($array, $this->getExtensionMembers()); + } + + return array_merge($array, $this->meta); } } diff --git a/src/objects/RelationshipObject.php b/src/objects/RelationshipObject.php index ea62561..7644cd5 100644 --- a/src/objects/RelationshipObject.php +++ b/src/objects/RelationshipObject.php @@ -5,6 +5,7 @@ use alsvanzelf\jsonapi\CollectionDocument; use alsvanzelf\jsonapi\exceptions\InputException; use alsvanzelf\jsonapi\helpers\AtMemberManager; +use alsvanzelf\jsonapi\helpers\ExtensionMemberManager; use alsvanzelf\jsonapi\helpers\LinksManager; use alsvanzelf\jsonapi\interfaces\ObjectInterface; use alsvanzelf\jsonapi\interfaces\PaginableInterface; @@ -15,7 +16,7 @@ use alsvanzelf\jsonapi\objects\ResourceObject; class RelationshipObject implements ObjectInterface, PaginableInterface, RecursiveResourceContainerInterface { - use AtMemberManager, LinksManager; + use AtMemberManager, ExtensionMemberManager, LinksManager; const TO_ONE = 'one'; const TO_MANY = 'many'; @@ -273,6 +274,9 @@ public function isEmpty() { if ($this->hasAtMembers()) { return false; } + if ($this->hasExtensionMembers()) { + return false; + } return true; } @@ -281,7 +285,14 @@ public function isEmpty() { * @inheritDoc */ public function toArray() { - $array = $this->getAtMembers(); + $array = []; + + if ($this->hasAtMembers()) { + $array = array_merge($array, $this->getAtMembers()); + } + if ($this->hasExtensionMembers()) { + $array = array_merge($array, $this->getExtensionMembers()); + } if ($this->links !== null && $this->links->isEmpty() === false) { $array['links'] = $this->links->toArray(); diff --git a/src/objects/RelationshipsObject.php b/src/objects/RelationshipsObject.php index 6764aa3..c0bf3e0 100644 --- a/src/objects/RelationshipsObject.php +++ b/src/objects/RelationshipsObject.php @@ -4,6 +4,7 @@ use alsvanzelf\jsonapi\exceptions\DuplicateException; use alsvanzelf\jsonapi\helpers\AtMemberManager; +use alsvanzelf\jsonapi\helpers\ExtensionMemberManager; use alsvanzelf\jsonapi\helpers\Validator; use alsvanzelf\jsonapi\interfaces\ObjectInterface; use alsvanzelf\jsonapi\interfaces\RecursiveResourceContainerInterface; @@ -12,7 +13,7 @@ use alsvanzelf\jsonapi\objects\ResourceObject; class RelationshipsObject implements ObjectInterface, RecursiveResourceContainerInterface { - use AtMemberManager; + use AtMemberManager, ExtensionMemberManager; /** @var RelationshipObject[] */ protected $relationships = []; @@ -77,14 +78,31 @@ public function getKeys() { * @inheritDoc */ public function isEmpty() { - return ($this->relationships === [] && $this->hasAtMembers() === false); + if ($this->relationships !== []) { + return false; + } + if ($this->hasAtMembers()) { + return false; + } + if ($this->hasExtensionMembers()) { + return false; + } + + return true; } /** * @inheritDoc */ public function toArray() { - $array = $this->getAtMembers(); + $array = []; + + if ($this->hasAtMembers()) { + $array = array_merge($array, $this->getAtMembers()); + } + if ($this->hasExtensionMembers()) { + $array = array_merge($array, $this->getExtensionMembers()); + } foreach ($this->relationships as $key => $relationshipObject) { $array[$key] = $relationshipObject->toArray(); diff --git a/src/objects/ResourceIdentifierObject.php b/src/objects/ResourceIdentifierObject.php index fe32d91..00bf6d2 100644 --- a/src/objects/ResourceIdentifierObject.php +++ b/src/objects/ResourceIdentifierObject.php @@ -4,13 +4,14 @@ use alsvanzelf\jsonapi\exceptions\Exception; use alsvanzelf\jsonapi\helpers\AtMemberManager; +use alsvanzelf\jsonapi\helpers\ExtensionMemberManager; use alsvanzelf\jsonapi\helpers\Validator; use alsvanzelf\jsonapi\interfaces\ObjectInterface; use alsvanzelf\jsonapi\interfaces\ResourceInterface; use alsvanzelf\jsonapi\objects\MetaObject; class ResourceIdentifierObject implements ObjectInterface, ResourceInterface { - use AtMemberManager; + use AtMemberManager, ExtensionMemberManager; /** @var string */ protected $type; @@ -164,6 +165,9 @@ public function isEmpty() { if ($this->hasAtMembers()) { return false; } + if ($this->hasExtensionMembers()) { + return false; + } return true; } @@ -172,7 +176,7 @@ public function isEmpty() { * @inheritDoc */ public function toArray() { - $array = $this->getAtMembers(); + $array = []; $array['type'] = $this->type; @@ -180,6 +184,13 @@ public function toArray() { $array['id'] = $this->id; } + if ($this->hasAtMembers()) { + $array = array_merge($array, $this->getAtMembers()); + } + if ($this->hasExtensionMembers()) { + $array = array_merge($array, $this->getExtensionMembers()); + } + if ($this->meta !== null && $this->meta->isEmpty() === false) { $array['meta'] = $this->meta->toArray(); } From 8c79241da2c77a4d3365ef5f66e296009b7cdf8f Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Mon, 5 Apr 2021 11:46:36 +0200 Subject: [PATCH 10/42] adjust example code for profiles and add for extensions --- examples/bootstrap_examples.php | 45 +++++++++++++++++++++++++++++---- examples/example_profile.php | 30 ---------------------- examples/extension.php | 37 +++++++++++++++++++++++++++ examples/profile.php | 39 ++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+), 35 deletions(-) delete mode 100644 examples/example_profile.php create mode 100644 examples/extension.php create mode 100644 examples/profile.php diff --git a/examples/bootstrap_examples.php b/examples/bootstrap_examples.php index 40bc6cd..406f47c 100644 --- a/examples/bootstrap_examples.php +++ b/examples/bootstrap_examples.php @@ -2,6 +2,7 @@ use alsvanzelf\jsonapi\Document; use alsvanzelf\jsonapi\ResourceDocument; +use alsvanzelf\jsonapi\interfaces\ExtensionInterface; use alsvanzelf\jsonapi\interfaces\ProfileInterface; use alsvanzelf\jsonapi\interfaces\ResourceInterface; @@ -101,25 +102,59 @@ function getCurrentLocation() { } } -class ExampleVersionProfile implements ProfileInterface { +class ExampleVersionExtension implements ExtensionInterface { /** * the required method */ + public function getNamespace() { + return 'version'; + } + public function getOfficialLink() { - return 'https://jsonapi.org/format/1.1/#profile-keywords'; + return 'https://jsonapi.org/format/1.1/#extension-rules'; } /** - * optionally helpers for the specific profile + * optionally helpers for the specific extension */ public function setVersion(ResourceInterface $resource, $version) { if ($resource instanceof ResourceDocument) { - $resource->addMeta('version', $version, $level=Document::LEVEL_RESOURCE); + $resource->getResource()->addExtensionMember($this, 'id', $version); } else { - $resource->addMeta('version', $version); + $resource->addExtensionMember($this, 'id', $version); + } + } +} + +class ExampleTimestampsProfile implements ProfileInterface { + /** + * the required method + */ + + public function getOfficialLink() { + return 'https://jsonapi.org/recommendations/#authoring-profiles'; + } + + /** + * optionally helpers for the specific profile + */ + + public function setTimestamps(ResourceInterface $resource, \DateTimeInterface $created=null, \DateTimeInterface $updated=null) { + if ($resource instanceof ResourceIdentifierObject) { + throw new Exception('cannot add attributes to identifier objects'); + } + + $timestamps = []; + if ($created !== null) { + $timestamps['created'] = $created->format(\DateTimeInterface::ISO8601); } + if ($updated !== null) { + $timestamps['updated'] = $updated->format(\DateTimeInterface::ISO8601); + } + + $resource->add('timestamps', $timestamps); } } diff --git a/examples/example_profile.php b/examples/example_profile.php deleted file mode 100644 index ac12aa1..0000000 --- a/examples/example_profile.php +++ /dev/null @@ -1,30 +0,0 @@ -applyProfile($profile); - -/** - * you can apply the rules of the profile manually - * or use methods of the profile if provided - */ - -$profile->setVersion($document, '2019'); - -/** - * get the json - */ - -$options = [ - 'prettyPrint' => true, -]; -echo '
'.$document->toJson($options);
diff --git a/examples/extension.php b/examples/extension.php
new file mode 100644
index 0000000..74738bf
--- /dev/null
+++ b/examples/extension.php
@@ -0,0 +1,37 @@
+applyExtension($extension);
+
+$document->add('foo', 'bar');
+
+/**
+ * you can apply the rules of the extension manually
+ * or use methods of the extension if provided
+ */
+
+$extension->setVersion($document, '2019');
+
+/**
+ * get the json
+ */
+
+$contentType = Converter::prepareContentType(Document::CONTENT_TYPE_OFFICIAL, [$extension], []);
+echo 'Content-Type: '.$contentType.''.PHP_EOL;
+
+$options = [
+	'prettyPrint' => true,
+];
+echo '
'.$document->toJson($options);
diff --git a/examples/profile.php b/examples/profile.php
new file mode 100644
index 0000000..8b070b6
--- /dev/null
+++ b/examples/profile.php
@@ -0,0 +1,39 @@
+applyProfile($profile);
+
+$document->add('foo', 'bar');
+
+/**
+ * you can apply the rules of the profile manually
+ * or use methods of the profile if provided
+ */
+
+$created = new \DateTime('-1 year');
+$updated = new \DateTime('-1 month');
+$profile->setTimestamps($document, $created, $updated);
+
+/**
+ * get the json
+ */
+
+$contentType = Converter::prepareContentType(Document::CONTENT_TYPE_OFFICIAL, [], [$profile]);
+echo 'Content-Type: '.$contentType.''.PHP_EOL;
+
+$options = [
+	'prettyPrint' => true,
+];
+echo '
'.$document->toJson($options);

From ab8a2d291347f87b264137d850b904ca1619530b Mon Sep 17 00:00:00 2001
From: Lode Claassen 
Date: Mon, 5 Apr 2021 11:51:16 +0200
Subject: [PATCH 11/42] adjust tests

---
 tests/ConverterTest.php                             |  4 ++--
 tests/DocumentTest.php                              | 13 +++++++------
 tests/SeparateProcessTest.php                       |  4 ++--
 .../at_members_everywhere.json                      |  8 ++++----
 .../cursor_pagination_profile.json                  |  8 ++++----
 .../example_profile/example_profile.json            |  4 +---
 6 files changed, 20 insertions(+), 21 deletions(-)

diff --git a/tests/ConverterTest.php b/tests/ConverterTest.php
index 85839c9..254aa09 100644
--- a/tests/ConverterTest.php
+++ b/tests/ConverterTest.php
@@ -71,7 +71,7 @@ public function testMergeProfilesInContentType_WithProfileStringLink() {
 		$profile = new TestProfile();
 		$profile->setOfficialLink('bar');
 		
-		$this->assertSame('foo;profile="bar", foo', Converter::mergeProfilesInContentType('foo', [$profile]));
+		$this->assertSame('foo; profile="bar"', Converter::mergeProfilesInContentType('foo', [$profile]));
 	}
 	
 	public function testMergeProfilesInContentType_WithMultipleProfiles() {
@@ -81,7 +81,7 @@ public function testMergeProfilesInContentType_WithMultipleProfiles() {
 		$profile2 = new TestProfile();
 		$profile2->setOfficialLink('baz');
 		
-		$this->assertSame('foo;profile="bar baz", foo', Converter::mergeProfilesInContentType('foo', [$profile1, $profile2]));
+		$this->assertSame('foo; profile="bar baz"', Converter::mergeProfilesInContentType('foo', [$profile1, $profile2]));
 	}
 }
 
diff --git a/tests/DocumentTest.php b/tests/DocumentTest.php
index 61e9936..0817d7f 100644
--- a/tests/DocumentTest.php
+++ b/tests/DocumentTest.php
@@ -186,12 +186,13 @@ public function testApplyProfile_HappyPath() {
 		
 		$array = $document->toArray();
 		
-		$this->assertArrayHasKey('links', $array);
-		$this->assertCount(1, $array['links']);
-		$this->assertArrayHasKey('profile', $array['links']);
-		$this->assertCount(1, $array['links']['profile']);
-		$this->assertArrayHasKey(0, $array['links']['profile']);
-		$this->assertSame('https://jsonapi.org', $array['links']['profile'][0]);
+		$this->assertArrayHasKey('jsonapi', $array);
+		$this->assertCount(2, $array['jsonapi']);
+		$this->assertSame('1.1', $array['jsonapi']['version']);
+		$this->assertArrayHasKey('profile', $array['jsonapi']);
+		$this->assertCount(1, $array['jsonapi']['profile']);
+		$this->assertArrayHasKey(0, $array['jsonapi']['profile']);
+		$this->assertSame('https://jsonapi.org', $array['jsonapi']['profile'][0]);
 	}
 	
 	public function testToJson_HappyPath() {
diff --git a/tests/SeparateProcessTest.php b/tests/SeparateProcessTest.php
index 4c7938f..6f725a9 100644
--- a/tests/SeparateProcessTest.php
+++ b/tests/SeparateProcessTest.php
@@ -81,7 +81,7 @@ public function testSendResponse_ContentTypeHeaderWithProfiles() {
 		ob_start();
 		$document->sendResponse();
 		ob_end_clean();
-		$this->assertSame(['Content-Type: '.Document::CONTENT_TYPE_OFFICIAL.';profile="https://jsonapi.org", '.Document::CONTENT_TYPE_OFFICIAL], xdebug_get_headers());
+		$this->assertSame(['Content-Type: '.Document::CONTENT_TYPE_OFFICIAL.'; profile="https://jsonapi.org"'], xdebug_get_headers());
 		
 		$profile = new TestProfile();
 		$profile->setOfficialLink('https://jsonapi.org/2');
@@ -90,7 +90,7 @@ public function testSendResponse_ContentTypeHeaderWithProfiles() {
 		ob_start();
 		$document->sendResponse();
 		ob_end_clean();
-		$this->assertSame(['Content-Type: '.Document::CONTENT_TYPE_OFFICIAL.';profile="https://jsonapi.org https://jsonapi.org/2", '.Document::CONTENT_TYPE_OFFICIAL], xdebug_get_headers());
+		$this->assertSame(['Content-Type: '.Document::CONTENT_TYPE_OFFICIAL.'; profile="https://jsonapi.org https://jsonapi.org/2"'], xdebug_get_headers());
 	}
 	
 	/**
diff --git a/tests/example_output/at_members_everywhere/at_members_everywhere.json b/tests/example_output/at_members_everywhere/at_members_everywhere.json
index feb9aea..76638c7 100644
--- a/tests/example_output/at_members_everywhere/at_members_everywhere.json
+++ b/tests/example_output/at_members_everywhere/at_members_everywhere.json
@@ -21,9 +21,9 @@
 		"@context": "/meta/@context"
 	},
 	"data": {
-		"@context": "/data/@context",
 		"type": "user",
 		"id": "42",
+		"@context": "/data/@context",
 		"meta": {
 			"@context": "/data/meta/@context"
 		},
@@ -48,9 +48,9 @@
 			"bar": {
 				"@context": "/data/relationships/bar/@context",
 				"data": {
-					"@context": "/data/relationships/bar/data/@context",
 					"type": "user",
 					"id": "2",
+					"@context": "/data/relationships/bar/data/@context",
 					"meta": {
 						"@context": "/data/relationships/bar/data/meta/@context"
 					}
@@ -70,17 +70,17 @@
 	},
 	"included": [
 		{
-			"@context": "/included/0/@context",
 			"type": "user",
 			"id": "1",
+			"@context": "/included/0/@context",
 			"attributes": {
 				"@context": "/included/0/attributes/@context"
 			}
 		},
 		{
-			"@context": "/included/1/@context",
 			"type": "user",
 			"id": "3",
+			"@context": "/included/1/@context",
 			"relationships": {
 				"@context": "/included/1/relationships/@context"
 			}
diff --git a/tests/example_output/cursor_pagination_profile/cursor_pagination_profile.json b/tests/example_output/cursor_pagination_profile/cursor_pagination_profile.json
index cb84fda..02b81ad 100644
--- a/tests/example_output/cursor_pagination_profile/cursor_pagination_profile.json
+++ b/tests/example_output/cursor_pagination_profile/cursor_pagination_profile.json
@@ -1,11 +1,11 @@
 {
 	"jsonapi": {
-		"version": "1.1"
-	},
-	"links": {
+		"version": "1.1",
 		"profile": [
 			"https://jsonapi.org/profiles/ethanresnick/cursor-pagination/"
-		],
+		]
+	},
+	"links": {
 		"prev": null,
 		"next": {
 			"href": "/users?sort=42&page[size]=10&page[after]=zaphod"
diff --git a/tests/example_output/example_profile/example_profile.json b/tests/example_output/example_profile/example_profile.json
index 53daa9f..468d581 100644
--- a/tests/example_output/example_profile/example_profile.json
+++ b/tests/example_output/example_profile/example_profile.json
@@ -1,8 +1,6 @@
 {
 	"jsonapi": {
-		"version": "1.1"
-	},
-	"links": {
+		"version": "1.1",
 		"profile": [
 			"https://jsonapi.org/format/1.1/#profile-keywords"
 		]

From f69059b06e43c2ef7e00b627a32881bde188a54b Mon Sep 17 00:00:00 2001
From: Lode Claassen 
Date: Mon, 5 Apr 2021 12:25:05 +0200
Subject: [PATCH 12/42] adjust and add more tests

---
 examples/bootstrap_examples.php               |   8 +-
 .../ExampleEverywhereExtension.php            |  15 ++
 .../ExampleTimestampsProfile.php              |  29 +++
 .../ExampleVersionExtension.php               |  26 +++
 .../example_output/ExampleVersionProfile.php  |  27 ---
 .../example_profile/example_profile.json      |  15 --
 .../example_profile/example_profile.php       |  19 --
 tests/example_output/extension/extension.json |  13 ++
 tests/example_output/extension/extension.php  |  19 ++
 .../extension_members_everywhere.json         |  89 +++++++++
 .../extension_members_everywhere.php          | 173 ++++++++++++++++++
 tests/example_output/profile/profile.json     |  18 ++
 tests/example_output/profile/profile.php      |  19 ++
 13 files changed, 405 insertions(+), 65 deletions(-)
 create mode 100644 tests/example_output/ExampleEverywhereExtension.php
 create mode 100644 tests/example_output/ExampleTimestampsProfile.php
 create mode 100644 tests/example_output/ExampleVersionExtension.php
 delete mode 100644 tests/example_output/ExampleVersionProfile.php
 delete mode 100644 tests/example_output/example_profile/example_profile.json
 delete mode 100644 tests/example_output/example_profile/example_profile.php
 create mode 100644 tests/example_output/extension/extension.json
 create mode 100644 tests/example_output/extension/extension.php
 create mode 100644 tests/example_output/extension_members_everywhere/extension_members_everywhere.json
 create mode 100644 tests/example_output/extension_members_everywhere/extension_members_everywhere.php
 create mode 100644 tests/example_output/profile/profile.json
 create mode 100644 tests/example_output/profile/profile.php

diff --git a/examples/bootstrap_examples.php b/examples/bootstrap_examples.php
index 406f47c..8e8f712 100644
--- a/examples/bootstrap_examples.php
+++ b/examples/bootstrap_examples.php
@@ -107,14 +107,14 @@ class ExampleVersionExtension implements ExtensionInterface {
 	 * the required method
 	 */
 	
-	public function getNamespace() {
-		return 'version';
-	}
-	
 	public function getOfficialLink() {
 		return 'https://jsonapi.org/format/1.1/#extension-rules';
 	}
 	
+	public function getNamespace() {
+		return 'version';
+	}
+	
 	/**
 	 * optionally helpers for the specific extension
 	 */
diff --git a/tests/example_output/ExampleEverywhereExtension.php b/tests/example_output/ExampleEverywhereExtension.php
new file mode 100644
index 0000000..885e26b
--- /dev/null
+++ b/tests/example_output/ExampleEverywhereExtension.php
@@ -0,0 +1,15 @@
+format(\DateTimeInterface::ISO8601);
+		}
+		if ($updated !== null) {
+			$timestamps['updated'] = $updated->format(\DateTimeInterface::ISO8601);
+		}
+		
+		$resource->add('timestamps', $timestamps);
+	}
+}
diff --git a/tests/example_output/ExampleVersionExtension.php b/tests/example_output/ExampleVersionExtension.php
new file mode 100644
index 0000000..bde454a
--- /dev/null
+++ b/tests/example_output/ExampleVersionExtension.php
@@ -0,0 +1,26 @@
+getResource()->addExtensionMember($this, 'id', $version);
+		}
+		else {
+			$resource->addExtensionMember($this, 'id', $version);
+		}
+	}
+}
diff --git a/tests/example_output/ExampleVersionProfile.php b/tests/example_output/ExampleVersionProfile.php
deleted file mode 100644
index 266be3f..0000000
--- a/tests/example_output/ExampleVersionProfile.php
+++ /dev/null
@@ -1,27 +0,0 @@
-addMeta('version', $version, $level=Document::LEVEL_RESOURCE);
-		}
-		else {
-			$resource->addMeta('version', $version);
-		}
-	}
-}
diff --git a/tests/example_output/example_profile/example_profile.json b/tests/example_output/example_profile/example_profile.json
deleted file mode 100644
index 468d581..0000000
--- a/tests/example_output/example_profile/example_profile.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
-	"jsonapi": {
-		"version": "1.1",
-		"profile": [
-			"https://jsonapi.org/format/1.1/#profile-keywords"
-		]
-	},
-	"data": {
-		"type": "user",
-		"id": "42",
-		"meta": {
-			"version": "2019"
-		}
-	}
-}
diff --git a/tests/example_output/example_profile/example_profile.php b/tests/example_output/example_profile/example_profile.php
deleted file mode 100644
index 65b3bb4..0000000
--- a/tests/example_output/example_profile/example_profile.php
+++ /dev/null
@@ -1,19 +0,0 @@
-applyProfile($profile);
-		
-		$profile->setVersion($document, '2019');
-		
-		return $document;
-	}
-}
diff --git a/tests/example_output/extension/extension.json b/tests/example_output/extension/extension.json
new file mode 100644
index 0000000..4b857c0
--- /dev/null
+++ b/tests/example_output/extension/extension.json
@@ -0,0 +1,13 @@
+{
+	"jsonapi": {
+		"version": "1.1",
+		"ext": [
+			"https://jsonapi.org/format/1.1/#extension-rules"
+		]
+	},
+	"data": {
+		"type": "user",
+		"id": "42",
+		"version:id": "2019"
+	}
+}
diff --git a/tests/example_output/extension/extension.php b/tests/example_output/extension/extension.php
new file mode 100644
index 0000000..4535c63
--- /dev/null
+++ b/tests/example_output/extension/extension.php
@@ -0,0 +1,19 @@
+applyExtension($extension);
+		
+		$extension->setVersion($document, '2019');
+		
+		return $document;
+	}
+}
diff --git a/tests/example_output/extension_members_everywhere/extension_members_everywhere.json b/tests/example_output/extension_members_everywhere/extension_members_everywhere.json
new file mode 100644
index 0000000..f41faa3
--- /dev/null
+++ b/tests/example_output/extension_members_everywhere/extension_members_everywhere.json
@@ -0,0 +1,89 @@
+{
+	"everywhere:key": "/key",
+	"jsonapi": {
+		"everywhere:key": "/jsonapi/key",
+		"version": "1.1",
+		"meta": {
+			"everywhere:key": "/jsonapi/meta/key"
+		}
+	},
+	"links": {
+		"everywhere:key": "/links/key",
+		"foo": {
+			"everywhere:key": "/links/foo/key",
+			"href": "https://jsonapi.org",
+			"meta": {
+				"everywhere:key": "/links/foo/meta/key"
+			}
+		}
+	},
+	"meta": {
+		"everywhere:key": "/meta/key"
+	},
+	"data": {
+		"type": "user",
+		"id": "42",
+		"everywhere:key": "/data/key",
+		"meta": {
+			"everywhere:key": "/data/meta/key"
+		},
+		"attributes": {
+			"everywhere:key": "/data/attributes/key"
+		},
+		"relationships": {
+			"everywhere:key": "/data/relationships/key",
+			"foo": {
+				"everywhere:key": "/data/relationships/foo/key",
+				"links": {
+					"everywhere:key": "/data/relationships/foo/links/key"
+				},
+				"data": {
+					"type": "user",
+					"id": "1"
+				},
+				"meta": {
+					"everywhere:key": "/data/relationships/foo/meta/key"
+				}
+			},
+			"bar": {
+				"everywhere:key": "/data/relationships/bar/key",
+				"data": {
+					"type": "user",
+					"id": "2",
+					"everywhere:key": "/data/relationships/bar/data/key",
+					"meta": {
+						"everywhere:key": "/data/relationships/bar/data/meta/key"
+					}
+				}
+			}
+		},
+		"links": {
+			"everywhere:key": "/data/links/key",
+			"foo": {
+				"everywhere:key": "/data/links/foo/key",
+				"href": "https://jsonapi.org",
+				"meta": {
+					"everywhere:key": "/data/links/foo/meta/key"
+				}
+			}
+		}
+	},
+	"included": [
+		{
+			"type": "user",
+			"id": "1",
+			"everywhere:key": "/included/0/key",
+			"attributes": {
+				"everywhere:key": "/included/0/attributes/key"
+			}
+		},
+		{
+			"type": "user",
+			"id": "3",
+			"everywhere:key": "/included/1/key",
+			"relationships": {
+				"everywhere:key": "/included/1/relationships/key"
+			}
+		}
+	]
+}
diff --git a/tests/example_output/extension_members_everywhere/extension_members_everywhere.php b/tests/example_output/extension_members_everywhere/extension_members_everywhere.php
new file mode 100644
index 0000000..f7747f2
--- /dev/null
+++ b/tests/example_output/extension_members_everywhere/extension_members_everywhere.php
@@ -0,0 +1,173 @@
+applyExtension($extension);
+		
+		/**
+		 * root
+		 */
+		
+		$document->addExtensionMember($extension, 'key', '/key');
+		
+		/**
+		 * jsonapi
+		 */
+		
+		$metaObject = new MetaObject();
+		$metaObject->addExtensionMember($extension, 'key', '/jsonapi/meta/key');
+		
+		$jsonapiObject = new JsonapiObject();
+		$jsonapiObject->addExtensionMember($extension, 'key', '/jsonapi/key');
+		$jsonapiObject->setMetaObject($metaObject);
+		$document->setJsonapiObject($jsonapiObject);
+		
+		/**
+		 * links
+		 */
+		
+		$metaObject = new MetaObject();
+		$metaObject->addExtensionMember($extension, 'key', '/links/foo/meta/key');
+		
+		$linkObject = new LinkObject('https://jsonapi.org');
+		$linkObject->addExtensionMember($extension, 'key', '/links/foo/key');
+		$linkObject->setMetaObject($metaObject);
+		
+		$linksObject = new LinksObject();
+		$linksObject->addExtensionMember($extension, 'key', '/links/key');
+		$linksObject->addLinkObject('foo', $linkObject);
+		$document->setLinksObject($linksObject);
+		
+		/**
+		 * meta
+		 */
+		
+		$metaObject = new MetaObject();
+		$metaObject->addExtensionMember($extension, 'key', '/meta/key');
+		$document->setMetaObject($metaObject);
+		
+		/**
+		 * resource
+		 */
+		
+		/**
+		 * resource - relationships
+		 * 
+		 * @todo make it work to have extension members in both the identifier and the resource parts
+		 *       e.g. it is missing in the data of the first relationship (`data.relationships.foo.data.key`)
+		 *       whereas it does appear in the second relationship (`data.relationships.bar.data.key`)
+		 * @see https://github.com/json-api/json-api/issues/1367
+		 */
+		
+		$relationshipsObject = new RelationshipsObject();
+		$relationshipsObject->addExtensionMember($extension, 'key', '/data/relationships/key');
+		
+		$attributesObject = new AttributesObject();
+		$attributesObject->addExtensionMember($extension, 'key', '/included/0/attributes/key');
+		
+		$resourceObject = new ResourceObject('user', 1);
+		$resourceObject->addExtensionMember($extension, 'key', '/included/0/key');
+		$resourceObject->setAttributesObject($attributesObject);
+		
+		$linksObject = new LinksObject();
+		$linksObject->addExtensionMember($extension, 'key', '/data/relationships/foo/links/key');
+		
+		$metaObject = new MetaObject();
+		$metaObject->addExtensionMember($extension, 'key', '/data/relationships/foo/meta/key');
+		
+		$relationshipObject = new RelationshipObject(RelationshipObject::TO_ONE);
+		$relationshipObject->addExtensionMember($extension, 'key', '/data/relationships/foo/key');
+		$relationshipObject->setResource($resourceObject);
+		$relationshipObject->setLinksObject($linksObject);
+		$relationshipObject->setMetaObject($metaObject);
+		
+		$relationshipsObject->addRelationshipObject('foo', $relationshipObject);
+		
+		$metaObject = new MetaObject();
+		$metaObject->addExtensionMember($extension, 'key', '/data/relationships/bar/data/meta/key');
+		
+		$resourceIdentifierObject = new ResourceIdentifierObject('user', 2);
+		$resourceIdentifierObject->addExtensionMember($extension, 'key', '/data/relationships/bar/data/key');
+		$resourceIdentifierObject->setMetaObject($metaObject);
+		
+		$relationshipObject = new RelationshipObject(RelationshipObject::TO_ONE);
+		$relationshipObject->addExtensionMember($extension, 'key', '/data/relationships/bar/key');
+		$relationshipObject->setResource($resourceIdentifierObject);
+		
+		$relationshipsObject->addRelationshipObject('bar', $relationshipObject);
+		
+		/**
+		 * resource - attributes
+		 */
+		
+		$attributesObject = new AttributesObject();
+		$attributesObject->addExtensionMember($extension, 'key', '/data/attributes/key');
+		
+		/**
+		 * resource - links
+		 */
+		
+		$metaObject = new MetaObject();
+		$metaObject->addExtensionMember($extension, 'key', '/data/links/foo/meta/key');
+		
+		$linkObject = new LinkObject('https://jsonapi.org');
+		$linkObject->addExtensionMember($extension, 'key', '/data/links/foo/key');
+		$linkObject->setMetaObject($metaObject);
+		
+		$linksObject = new LinksObject();
+		$linksObject->addExtensionMember($extension, 'key', '/data/links/key');
+		$linksObject->addLinkObject('foo', $linkObject);
+		
+		/**
+		 * resource - meta
+		 */
+		
+		$metaObject = new MetaObject();
+		$metaObject->addExtensionMember($extension, 'key', '/data/meta/key');
+		
+		/**
+		 * resource - resource
+		 */
+		
+		$resourceObject = new ResourceObject('user', 42);
+		$resourceObject->addExtensionMember($extension, 'key', '/data/key');
+		$resourceObject->setAttributesObject($attributesObject);
+		$resourceObject->setLinksObject($linksObject);
+		$resourceObject->setMetaObject($metaObject);
+		$resourceObject->setRelationshipsObject($relationshipsObject);
+		
+		$document->setPrimaryResource($resourceObject);
+		
+		/**
+		 * included
+		 */
+		
+		$relationshipsObject = new RelationshipsObject();
+		$relationshipsObject->addExtensionMember($extension, 'key', '/included/1/relationships/key');
+		
+		$resourceObject = new ResourceObject('user', 3);
+		$resourceObject->addExtensionMember($extension, 'key', '/included/1/key');
+		$resourceObject->setRelationshipsObject($relationshipsObject);
+		
+		$document->addIncludedResourceObject($resourceObject);
+		
+		return $document;
+	}
+}
diff --git a/tests/example_output/profile/profile.json b/tests/example_output/profile/profile.json
new file mode 100644
index 0000000..9163f43
--- /dev/null
+++ b/tests/example_output/profile/profile.json
@@ -0,0 +1,18 @@
+{
+	"jsonapi": {
+		"version": "1.1",
+		"profile": [
+			"https://jsonapi.org/recommendations/#authoring-profiles"
+		]
+	},
+	"data": {
+		"type": "user",
+		"id": "42",
+		"attributes": {
+			"timestamps": {
+				"created": "2021-04-05T20:19:00+0000",
+				"updated": "2021-04-05T20:21:00+0000"
+			}
+		}
+	}
+}
diff --git a/tests/example_output/profile/profile.php b/tests/example_output/profile/profile.php
new file mode 100644
index 0000000..678219b
--- /dev/null
+++ b/tests/example_output/profile/profile.php
@@ -0,0 +1,19 @@
+applyProfile($profile);
+		
+		$profile->setTimestamps($document, new \DateTime('2019'), new \DateTime('2021'));
+		
+		return $document;
+	}
+}

From 7722c1721ee64f8cdc1b62606e73fb841087c588 Mon Sep 17 00:00:00 2001
From: Lode Claassen 
Date: Mon, 5 Apr 2021 12:56:10 +0200
Subject: [PATCH 13/42] add generic tests around extensions

---
 tests/ConverterTest.php            | 40 +++++++++++++++++++++++++-----
 tests/DocumentTest.php             | 29 ++++++++++++++++++++++
 tests/SeparateProcessTest.php      | 28 +++++++++++++++++++++
 tests/extensions/TestExtension.php | 26 +++++++++++++++++++
 4 files changed, 117 insertions(+), 6 deletions(-)
 create mode 100644 tests/extensions/TestExtension.php

diff --git a/tests/ConverterTest.php b/tests/ConverterTest.php
index 254aa09..6959393 100644
--- a/tests/ConverterTest.php
+++ b/tests/ConverterTest.php
@@ -4,6 +4,7 @@
 
 use alsvanzelf\jsonapi\helpers\Converter;
 use alsvanzelf\jsonapi\objects\AttributesObject;
+use alsvanzelf\jsonapiTests\extensions\TestExtension;
 use alsvanzelf\jsonapiTests\profiles\TestProfile;
 use PHPUnit\Framework\TestCase;
 
@@ -63,25 +64,52 @@ public function dataProviderCamelCaseToWords_HappyPath() {
 		];
 	}
 	
-	public function testMergeProfilesInContentType_HappyPath() {
-		$this->assertSame('foo', Converter::mergeProfilesInContentType('foo', []));
+	/**
+	 * @group Extensions
+	 * @group Profiles
+	 */
+	public function testPrepareContentType_HappyPath() {
+		$this->assertSame('foo', Converter::prepareContentType('foo', [], []));
+	}
+	
+	/**
+	 * @group Extensions
+	 */
+	public function testPrepareContentType_WithExtensionStringLink() {
+		$extension = new TestExtension();
+		$extension->setOfficialLink('bar');
+		
+		$this->assertSame('foo; ext="bar"', Converter::prepareContentType('foo', [$extension], []));
 	}
 	
-	public function testMergeProfilesInContentType_WithProfileStringLink() {
+	/**
+	 * @group Profiles
+	 */
+	public function testPrepareContentType_WithProfileStringLink() {
 		$profile = new TestProfile();
 		$profile->setOfficialLink('bar');
 		
-		$this->assertSame('foo; profile="bar"', Converter::mergeProfilesInContentType('foo', [$profile]));
+		$this->assertSame('foo; profile="bar"', Converter::prepareContentType('foo', [], [$profile]));
 	}
 	
-	public function testMergeProfilesInContentType_WithMultipleProfiles() {
+	/**
+	 * @group Extensions
+	 * @group Profiles
+	 */
+	public function testPrepareContentType_WithMultipleExtensionsAndProfiles() {
+		$extension1 = new TestExtension();
+		$extension1->setOfficialLink('bar');
+		
+		$extension2 = new TestExtension();
+		$extension2->setOfficialLink('baz');
+		
 		$profile1 = new TestProfile();
 		$profile1->setOfficialLink('bar');
 		
 		$profile2 = new TestProfile();
 		$profile2->setOfficialLink('baz');
 		
-		$this->assertSame('foo; profile="bar baz"', Converter::mergeProfilesInContentType('foo', [$profile1, $profile2]));
+		$this->assertSame('foo; ext="bar baz"; profile="bar baz"', Converter::prepareContentType('foo', [$extension1, $extension2], [$profile1, $profile2]));
 	}
 }
 
diff --git a/tests/DocumentTest.php b/tests/DocumentTest.php
index 0817d7f..6a715b9 100644
--- a/tests/DocumentTest.php
+++ b/tests/DocumentTest.php
@@ -6,6 +6,7 @@
 use alsvanzelf\jsonapi\exceptions\InputException;
 use alsvanzelf\jsonapi\objects\LinkObject;
 use alsvanzelf\jsonapiTests\TestableNonAbstractDocument as Document;
+use alsvanzelf\jsonapiTests\extensions\TestExtension;
 use alsvanzelf\jsonapiTests\profiles\TestProfile;
 use PHPUnit\Framework\TestCase;
 
@@ -177,6 +178,34 @@ public function testAddLinkObject_HappyPath() {
 		$this->assertSame('https://jsonapi.org', $array['links']['foo']['href']);
 	}
 	
+	/**
+	 * @group Extensions
+	 */
+	public function testApplyExtension_HappyPath() {
+		$extension = new TestExtension();
+		$extension->setNamespace('test');
+		$extension->setOfficialLink('https://jsonapi.org');
+		
+		$document = new Document();
+		$document->applyExtension($extension);
+		$document->addExtensionMember($extension, 'foo', 'bar');
+		
+		$array = $document->toArray();
+		
+		$this->assertArrayHasKey('jsonapi', $array);
+		$this->assertCount(2, $array['jsonapi']);
+		$this->assertSame('1.1', $array['jsonapi']['version']);
+		$this->assertArrayHasKey('ext', $array['jsonapi']);
+		$this->assertCount(1, $array['jsonapi']['ext']);
+		$this->assertArrayHasKey(0, $array['jsonapi']['ext']);
+		$this->assertSame('https://jsonapi.org', $array['jsonapi']['ext'][0]);
+		$this->assertArrayHasKey('test:foo', $array);
+		$this->assertSame('bar', $array['test:foo']);
+	}
+	
+	/**
+	 * @group Profiles
+	 */
 	public function testApplyProfile_HappyPath() {
 		$profile = new TestProfile();
 		$profile->setOfficialLink('https://jsonapi.org');
diff --git a/tests/SeparateProcessTest.php b/tests/SeparateProcessTest.php
index 6f725a9..8680809 100644
--- a/tests/SeparateProcessTest.php
+++ b/tests/SeparateProcessTest.php
@@ -3,6 +3,7 @@
 namespace alsvanzelf\jsonapiTests;
 
 use alsvanzelf\jsonapiTests\TestableNonAbstractDocument as Document;
+use alsvanzelf\jsonapiTests\extensions\TestExtension;
 use alsvanzelf\jsonapiTests\profiles\TestProfile;
 use PHPUnit\Framework\TestCase;
 
@@ -70,6 +71,33 @@ public function testSendResponse_ContentTypeHeader() {
 	
 	/**
 	 * @runInSeparateProcess
+	 * @group Extensions
+	 */
+	public function testSendResponse_ContentTypeHeaderWithExtensions() {
+		$extension = new TestExtension();
+		$extension->setOfficialLink('https://jsonapi.org');
+		
+		$document = new Document();
+		$document->applyExtension($extension);
+		
+		ob_start();
+		$document->sendResponse();
+		ob_end_clean();
+		$this->assertSame(['Content-Type: '.Document::CONTENT_TYPE_OFFICIAL.'; ext="https://jsonapi.org"'], xdebug_get_headers());
+		
+		$extension = new TestExtension();
+		$extension->setOfficialLink('https://jsonapi.org/2');
+		$document->applyExtension($extension);
+		
+		ob_start();
+		$document->sendResponse();
+		ob_end_clean();
+		$this->assertSame(['Content-Type: '.Document::CONTENT_TYPE_OFFICIAL.'; ext="https://jsonapi.org https://jsonapi.org/2"'], xdebug_get_headers());
+	}
+	
+	/**
+	 * @runInSeparateProcess
+	 * @group Profiles
 	 */
 	public function testSendResponse_ContentTypeHeaderWithProfiles() {
 		$profile = new TestProfile();
diff --git a/tests/extensions/TestExtension.php b/tests/extensions/TestExtension.php
new file mode 100644
index 0000000..9b57f9e
--- /dev/null
+++ b/tests/extensions/TestExtension.php
@@ -0,0 +1,26 @@
+namespace = $namespace;
+	}
+	
+	public function setOfficialLink($officialLink) {
+		$this->officialLink = $officialLink;
+	}
+	
+	public function getNamespace() {
+		return $this->namespace;
+	}
+	
+	public function getOfficialLink() {
+		return $this->officialLink;
+	}
+}

From 5eaf3f5fb6217fb35797575da990645ac3fd2e8e Mon Sep 17 00:00:00 2001
From: Lode Claassen 
Date: Mon, 5 Apr 2021 15:21:43 +0200
Subject: [PATCH 14/42] add ready-to-use atomic operations extension

---
 README.md                                     |  5 +-
 examples/atomic_operations_extension.php      | 33 +++++++++++
 src/extensions/AtomicOperationsDocument.php   | 57 +++++++++++++++++++
 src/extensions/AtomicOperationsExtension.php  | 32 +++++++++++
 .../AtomicOperationsDocumentTest.php          | 51 +++++++++++++++++
 5 files changed, 176 insertions(+), 2 deletions(-)
 create mode 100644 examples/atomic_operations_extension.php
 create mode 100644 src/extensions/AtomicOperationsDocument.php
 create mode 100644 src/extensions/AtomicOperationsExtension.php
 create mode 100644 tests/extensions/AtomicOperationsDocumentTest.php

diff --git a/README.md b/README.md
index 09e675c..a0b2039 100644
--- a/README.md
+++ b/README.md
@@ -150,9 +150,10 @@ It has support for generating & sending documents with:
 - v1.1 extensions via profiles
 - v1.1 @-members for JSON-LD and others
 
-Next to custom extensions, the following [official extensions](https://jsonapi.org/extensions/) are included:
+Next to custom extensions/profiles, the following [official extensions/profiles](https://jsonapi.org/extensions/) are included:
 
-- Cursor Pagination ([example code](/examples/cursor_pagination_profile.php), [specification](https://jsonapi.org/profiles/ethanresnick/cursor-pagination/))
+- Atomic Operations extension ([example code](/examples/atomic_operations_extension.php), [specification](https://jsonapi.org/ext/atomic/))
+- Cursor Pagination profile ([example code](/examples/cursor_pagination_profile.php), [specification](https://jsonapi.org/profiles/ethanresnick/cursor-pagination/))
 
 Plans for the future include:
 
diff --git a/examples/atomic_operations_extension.php b/examples/atomic_operations_extension.php
new file mode 100644
index 0000000..4e97c6d
--- /dev/null
+++ b/examples/atomic_operations_extension.php
@@ -0,0 +1,33 @@
+add('name',  'Ford');
+$user2->add('name',  'Arthur');
+$user42->add('name', 'Zaphod');
+
+$document->addResults($user1);
+$document->addResults($user2);
+$document->addResults($user42);
+
+/**
+ * get the json
+ */
+
+$options = [
+	'prettyPrint' => true,
+];
+echo '
'.$document->toJson($options);
diff --git a/src/extensions/AtomicOperationsDocument.php b/src/extensions/AtomicOperationsDocument.php
new file mode 100644
index 0000000..1599fd8
--- /dev/null
+++ b/src/extensions/AtomicOperationsDocument.php
@@ -0,0 +1,57 @@
+extension = new AtomicOperationsExtension();
+		$this->applyExtension($this->extension);
+	}
+	
+	/**
+	 * add resources as results of the operations
+	 * 
+	 * @param ResourceInterface[] ...$resources
+	 */
+	public function addResults(ResourceInterface ...$resources) {
+		$this->results = array_merge($this->results, $resources);
+	}
+	
+	/**
+	 * DocumentInterface
+	 */
+	
+	/**
+	 * @inheritDoc
+	 */
+	public function toArray() {
+		$results = [];
+		foreach ($this->results as $result) {
+			$results[] = [
+				'data' => $result->getResource()->toArray(),
+			];
+		}
+		
+		$this->addExtensionMember($this->extension, 'results', $results);
+		
+		return parent::toArray();
+	}
+}
diff --git a/src/extensions/AtomicOperationsExtension.php b/src/extensions/AtomicOperationsExtension.php
new file mode 100644
index 0000000..b94cfe6
--- /dev/null
+++ b/src/extensions/AtomicOperationsExtension.php
@@ -0,0 +1,32 @@
+add('name', 'Ford');
+		$resource2->add('name', 'Arthur');
+		$resource3->add('name', 'Zaphod');
+		$document->addResults($resource1, $resource2, $resource3);
+		
+		$array = $document->toArray();
+		
+		$this->assertArrayHasKey('jsonapi', $array);
+		$this->assertArrayHasKey('ext', $array['jsonapi']);
+		$this->assertCount(1, $array['jsonapi']['ext']);
+		$this->assertSame((new AtomicOperationsExtension())->getOfficialLink(), $array['jsonapi']['ext'][0]);
+		
+		$this->assertArrayHasKey('atomic:results', $array);
+		$this->assertCount(3, $array['atomic:results']);
+		$this->assertSame(['data' => $resource1->toArray()], $array['atomic:results'][0]);
+		$this->assertSame(['data' => $resource2->toArray()], $array['atomic:results'][1]);
+		$this->assertSame(['data' => $resource3->toArray()], $array['atomic:results'][2]);
+	}
+	
+	public function testSetResults_EmptySuccessResults() {
+		$document = new AtomicOperationsDocument();
+		$array    = $document->toArray();
+		
+		$this->assertArrayHasKey('jsonapi', $array);
+		$this->assertArrayHasKey('ext', $array['jsonapi']);
+		$this->assertCount(1, $array['jsonapi']['ext']);
+		$this->assertSame((new AtomicOperationsExtension())->getOfficialLink(), $array['jsonapi']['ext'][0]);
+		
+		$this->assertArrayHasKey('atomic:results', $array);
+		$this->assertCount(0, $array['atomic:results']);
+	}
+}

From eca7637e10f2fb522a08c5a467192d578091c9ca Mon Sep 17 00:00:00 2001
From: Lode Claassen 
Date: Mon, 5 Apr 2021 19:27:59 +0200
Subject: [PATCH 15/42] test profile link appears in jsonapi object

---
 .../profiles/CursorPaginationProfileTest.php  | 42 +++++++++++++++++++
 1 file changed, 42 insertions(+)

diff --git a/tests/profiles/CursorPaginationProfileTest.php b/tests/profiles/CursorPaginationProfileTest.php
index 533f43a..415986e 100644
--- a/tests/profiles/CursorPaginationProfileTest.php
+++ b/tests/profiles/CursorPaginationProfileTest.php
@@ -20,10 +20,16 @@ public function testSetLinks_HappyPath() {
 		$firstCursor      = 'bar';
 		$lastCursor       = 'foo';
 		
+		$collection->applyProfile($profile);
 		$profile->setLinks($collection, $baseOrCurrentUrl, $firstCursor, $lastCursor);
 		
 		$array = $collection->toArray();
 		
+		$this->assertArrayHasKey('jsonapi', $array);
+		$this->assertArrayHasKey('profile', $array['jsonapi']);
+		$this->assertCount(1, $array['jsonapi']['profile']);
+		$this->assertSame($profile->getOfficialLink(), $array['jsonapi']['profile'][0]);
+		
 		$this->assertArrayHasKey('links', $array);
 		$this->assertCount(2, $array['links']);
 		$this->assertArrayHasKey('prev', $array['links']);
@@ -37,6 +43,7 @@ public function testSetLinks_HappyPath() {
 	public function test_WithRelationship() {
 		$profile  = new CursorPaginationProfile();
 		$document = new ResourceDocument('test', 1);
+		$document->applyProfile($profile);
 		
 		$person1  = new ResourceObject('person', 1);
 		$person2  = new ResourceObject('person', 2);
@@ -59,6 +66,11 @@ public function test_WithRelationship() {
 		
 		$array = $document->toArray();
 		
+		$this->assertArrayHasKey('jsonapi', $array);
+		$this->assertArrayHasKey('profile', $array['jsonapi']);
+		$this->assertCount(1, $array['jsonapi']['profile']);
+		$this->assertSame($profile->getOfficialLink(), $array['jsonapi']['profile'][0]);
+		
 		$this->assertArrayHasKey('data', $array);
 		$this->assertArrayHasKey('relationships', $array['data']);
 		$this->assertArrayHasKey('people', $array['data']['relationships']);
@@ -85,10 +97,16 @@ public function testSetLinksFirstPage_HappyPath() {
 		$baseOrCurrentUrl = '/people?page[size]=10';
 		$lastCursor       = 'foo';
 		
+		$collection->applyProfile($profile);
 		$profile->setLinksFirstPage($collection, $baseOrCurrentUrl, $lastCursor);
 		
 		$array = $collection->toArray();
 		
+		$this->assertArrayHasKey('jsonapi', $array);
+		$this->assertArrayHasKey('profile', $array['jsonapi']);
+		$this->assertCount(1, $array['jsonapi']['profile']);
+		$this->assertSame($profile->getOfficialLink(), $array['jsonapi']['profile'][0]);
+		
 		$this->assertArrayHasKey('links', $array);
 		$this->assertCount(2, $array['links']);
 		$this->assertArrayHasKey('prev', $array['links']);
@@ -104,10 +122,16 @@ public function testSetLinksLastPage_HappyPath() {
 		$baseOrCurrentUrl = '/people?page[size]=10';
 		$firstCursor      = 'bar';
 		
+		$collection->applyProfile($profile);
 		$profile->setLinksLastPage($collection, $baseOrCurrentUrl, $firstCursor);
 		
 		$array = $collection->toArray();
 		
+		$this->assertArrayHasKey('jsonapi', $array);
+		$this->assertArrayHasKey('profile', $array['jsonapi']);
+		$this->assertCount(1, $array['jsonapi']['profile']);
+		$this->assertSame($profile->getOfficialLink(), $array['jsonapi']['profile'][0]);
+		
 		$this->assertArrayHasKey('links', $array);
 		$this->assertCount(2, $array['links']);
 		$this->assertArrayHasKey('prev', $array['links']);
@@ -121,10 +145,16 @@ public function testSetCursor() {
 		$profile          = new CursorPaginationProfile();
 		$resourceDocument = new ResourceDocument('user', 42);
 		
+		$resourceDocument->applyProfile($profile);
 		$profile->setCursor($resourceDocument, 'foo');
 		
 		$array = $resourceDocument->toArray();
 		
+		$this->assertArrayHasKey('jsonapi', $array);
+		$this->assertArrayHasKey('profile', $array['jsonapi']);
+		$this->assertCount(1, $array['jsonapi']['profile']);
+		$this->assertSame($profile->getOfficialLink(), $array['jsonapi']['profile'][0]);
+		
 		$this->assertArrayHasKey('data', $array);
 		$this->assertArrayHasKey('meta', $array['data']);
 		$this->assertArrayHasKey('page', $array['data']['meta']);
@@ -136,10 +166,16 @@ public function testSetPaginationLinkObjectsExplicitlyEmpty_HapptPath() {
 		$profile    = new CursorPaginationProfile();
 		$collection = new CollectionDocument();
 		
+		$collection->applyProfile($profile);
 		$profile->setPaginationLinkObjectsExplicitlyEmpty($collection);
 		
 		$array = $collection->toArray();
 		
+		$this->assertArrayHasKey('jsonapi', $array);
+		$this->assertArrayHasKey('profile', $array['jsonapi']);
+		$this->assertCount(1, $array['jsonapi']['profile']);
+		$this->assertSame($profile->getOfficialLink(), $array['jsonapi']['profile'][0]);
+		
 		$this->assertArrayHasKey('links', $array);
 		$this->assertCount(2, $array['links']);
 		$this->assertArrayHasKey('prev', $array['links']);
@@ -155,10 +191,16 @@ public function testSetPaginationMeta() {
 		$bestGuessTotal   = 100;
 		$rangeIsTruncated = true;
 		
+		$collection->applyProfile($profile);
 		$profile->setPaginationMeta($collection, $exactTotal, $bestGuessTotal, $rangeIsTruncated);
 		
 		$array = $collection->toArray();
 		
+		$this->assertArrayHasKey('jsonapi', $array);
+		$this->assertArrayHasKey('profile', $array['jsonapi']);
+		$this->assertCount(1, $array['jsonapi']['profile']);
+		$this->assertSame($profile->getOfficialLink(), $array['jsonapi']['profile'][0]);
+		
 		$this->assertArrayHasKey('meta', $array);
 		$this->assertArrayHasKey('page', $array['meta']);
 		$this->assertArrayHasKey('total', $array['meta']['page']);

From 0a9f38d0f1a1da22305781b1947221993f679091 Mon Sep 17 00:00:00 2001
From: Lode Claassen 
Date: Mon, 5 Apr 2021 19:37:42 +0200
Subject: [PATCH 16/42] try to fix pre-php7.2 date constants

---
 examples/bootstrap_examples.php                   | 4 ++--
 tests/example_output/ExampleTimestampsProfile.php | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/examples/bootstrap_examples.php b/examples/bootstrap_examples.php
index 8e8f712..fc4793d 100644
--- a/examples/bootstrap_examples.php
+++ b/examples/bootstrap_examples.php
@@ -149,10 +149,10 @@ public function setTimestamps(ResourceInterface $resource, \DateTimeInterface $c
 		
 		$timestamps = [];
 		if ($created !== null) {
-			$timestamps['created'] = $created->format(\DateTimeInterface::ISO8601);
+			$timestamps['created'] = $created->format(\DateTime::ISO8601);
 		}
 		if ($updated !== null) {
-			$timestamps['updated'] = $updated->format(\DateTimeInterface::ISO8601);
+			$timestamps['updated'] = $updated->format(\DateTime::ISO8601);
 		}
 		
 		$resource->add('timestamps', $timestamps);
diff --git a/tests/example_output/ExampleTimestampsProfile.php b/tests/example_output/ExampleTimestampsProfile.php
index c774e9c..56b07f5 100644
--- a/tests/example_output/ExampleTimestampsProfile.php
+++ b/tests/example_output/ExampleTimestampsProfile.php
@@ -18,10 +18,10 @@ public function setTimestamps(ResourceInterface $resource, \DateTimeInterface $c
 		
 		$timestamps = [];
 		if ($created !== null) {
-			$timestamps['created'] = $created->format(\DateTimeInterface::ISO8601);
+			$timestamps['created'] = $created->format(\DateTime::ISO8601);
 		}
 		if ($updated !== null) {
-			$timestamps['updated'] = $updated->format(\DateTimeInterface::ISO8601);
+			$timestamps['updated'] = $updated->format(\DateTime::ISO8601);
 		}
 		
 		$resource->add('timestamps', $timestamps);

From a980a462914dcd48306c3feaef7bd09c31312330 Mon Sep 17 00:00:00 2001
From: Lode Claassen 
Date: Mon, 5 Apr 2021 19:45:05 +0200
Subject: [PATCH 17/42] cleanup

---
 README.md                  | 2 +-
 examples/index.html        | 4 +++-
 src/objects/LinksArray.php | 1 -
 3 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index a0b2039..7fa377a 100644
--- a/README.md
+++ b/README.md
@@ -147,7 +147,7 @@ It has support for generating & sending documents with:
 - resource collections
 - to-one and to-many relationships
 - errors (easily turning exceptions into jsonapi output)
-- v1.1 extensions via profiles
+- v1.1 extensions and profiles
 - v1.1 @-members for JSON-LD and others
 
 Next to custom extensions/profiles, the following [official extensions/profiles](https://jsonapi.org/extensions/) are included:
diff --git a/examples/index.html b/examples/index.html
index 45100ac..b3a9da3 100644
--- a/examples/index.html
+++ b/examples/index.html
@@ -48,7 +48,9 @@ 

Misc

  • Null values if explicitly not available
  • Meta-only use-cases
  • Status-only
  • -
  • Example profile
  • +
  • Example extension
  • +
  • Atomic operations extension
  • +
  • Example profile
  • Cursor pagination profile
  • Different ways to output
  • diff --git a/src/objects/LinksArray.php b/src/objects/LinksArray.php index d390e01..d4ae042 100644 --- a/src/objects/LinksArray.php +++ b/src/objects/LinksArray.php @@ -9,7 +9,6 @@ /** * an array of links (strings and LinkObjects), used for: * - type links in an ErrorObject - * - profile links at root level */ class LinksArray implements ObjectInterface { /** @var array with string|LinkObject */ From a513d3d682e0bc2c86febecd7c883862d098c101 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Mon, 5 Apr 2021 23:21:15 +0200 Subject: [PATCH 18/42] improve test coverage --- tests/ConverterTest.php | 11 +++ tests/helpers/ExtensionMemberManagerTest.php | 67 +++++++++++++++++++ ...TestableNonTraitExtensionMemberManager.php | 12 ++++ tests/objects/ErrorObjectTest.php | 20 ++++++ tests/objects/JsonapiObjectTest.php | 32 +++++++++ tests/objects/LinkObjectTest.php | 11 +++ tests/objects/MetaObjectTest.php | 21 ++++++ tests/objects/RelationshipObjectTest.php | 11 +++ .../objects/ResourceIdentifierObjectTest.php | 11 +++ 9 files changed, 196 insertions(+) create mode 100644 tests/helpers/ExtensionMemberManagerTest.php create mode 100644 tests/helpers/TestableNonTraitExtensionMemberManager.php diff --git a/tests/ConverterTest.php b/tests/ConverterTest.php index 6959393..f5c7d51 100644 --- a/tests/ConverterTest.php +++ b/tests/ConverterTest.php @@ -111,6 +111,17 @@ public function testPrepareContentType_WithMultipleExtensionsAndProfiles() { $this->assertSame('foo; ext="bar baz"; profile="bar baz"', Converter::prepareContentType('foo', [$extension1, $extension2], [$profile1, $profile2])); } + + /** + * test method while it is part of the interface + * @group Profiles + */ + public function testMergeProfilesInContentType_HappyPath() { + $profile = new TestProfile(); + $profile->setOfficialLink('bar'); + + $this->assertSame('foo; profile="bar"', Converter::mergeProfilesInContentType('foo', [$profile])); + } } class TestObject { diff --git a/tests/helpers/ExtensionMemberManagerTest.php b/tests/helpers/ExtensionMemberManagerTest.php new file mode 100644 index 0000000..bbd728c --- /dev/null +++ b/tests/helpers/ExtensionMemberManagerTest.php @@ -0,0 +1,67 @@ +extension = new TestExtension(); + $this->extension->setNamespace('test'); + $this->extension->setOfficialLink('https://example.org'); + } + + public function testAddExtensionMember_HappyPath() { + $helper = new ExtensionMemberManager(); + + $this->assertFalse($helper->hasExtensionMembers()); + $this->assertSame([], $helper->getExtensionMembers()); + + $helper->addExtensionMember($this->extension, 'foo', 'bar'); + + $array = $helper->getExtensionMembers(); + + $this->assertTrue($helper->hasExtensionMembers()); + $this->assertCount(1, $array); + $this->assertArrayHasKey('test:foo', $array); + $this->assertSame('bar', $array['test:foo']); + } + + public function testAddExtensionMember_WithNamespacePrefixed() { + $helper = new ExtensionMemberManager(); + + $helper->addExtensionMember($this->extension, 'test:foo', 'bar'); + + $array = $helper->getExtensionMembers(); + + $this->assertArrayHasKey('test:foo', $array); + } + + public function testAddExtensionMember_WithObjectValue() { + $helper = new ExtensionMemberManager(); + + $object = new \stdClass(); + $object->bar = 'baz'; + + $helper->addExtensionMember($this->extension, 'foo', $object); + + $array = $helper->getExtensionMembers(); + + $this->assertArrayHasKey('test:foo', $array); + $this->assertArrayHasKey('bar', $array['test:foo']); + $this->assertSame('baz', $array['test:foo']['bar']); + } + + public function testAddExtensionMember_InvalidNamespaceOrCharacter() { + $helper = new ExtensionMemberManager(); + + $this->expectException(InputException::class); + + $helper->addExtensionMember($this->extension, 'foo:bar', 'baz'); + } +} diff --git a/tests/helpers/TestableNonTraitExtensionMemberManager.php b/tests/helpers/TestableNonTraitExtensionMemberManager.php new file mode 100644 index 0000000..516f103 --- /dev/null +++ b/tests/helpers/TestableNonTraitExtensionMemberManager.php @@ -0,0 +1,12 @@ +addAtMember('context', 'test'); $this->assertFalse($errorObject->isEmpty()); + + $errorObject = new ErrorObject(); + $errorObject->addExtensionMember(new TestExtension(), 'foo', 'bar'); + $this->assertFalse($errorObject->isEmpty()); + } + + public function testToArray_WithExtensionMembers() { + $errorObject = new ErrorObject(); + $extension = new TestExtension(); + $extension->setNamespace('test'); + + $this->assertSame([], $errorObject->toArray()); + + $errorObject->addExtensionMember($extension, 'foo', 'bar'); + + $array = $errorObject->toArray(); + + $this->assertArrayHasKey('test:foo', $array); + $this->assertSame('bar', $array['test:foo']); } } diff --git a/tests/objects/JsonapiObjectTest.php b/tests/objects/JsonapiObjectTest.php index 0807a19..c866d95 100644 --- a/tests/objects/JsonapiObjectTest.php +++ b/tests/objects/JsonapiObjectTest.php @@ -3,6 +3,8 @@ namespace alsvanzelf\jsonapiTests\objects; use alsvanzelf\jsonapi\objects\JsonapiObject; +use alsvanzelf\jsonapiTests\extensions\TestExtension; +use alsvanzelf\jsonapiTests\profiles\TestProfile; use PHPUnit\Framework\TestCase; class JsonapiObjectTest extends TestCase { @@ -31,4 +33,34 @@ public function testIsEmpty_WithAtMembers() { $this->assertFalse($jsonapiObject->isEmpty()); } + + public function testIsEmpty_WithExtensionLink() { + $jsonapiObject = new JsonapiObject($version=null); + + $this->assertTrue($jsonapiObject->isEmpty()); + + $jsonapiObject->addExtension(new TestExtension()); + + $this->assertFalse($jsonapiObject->isEmpty()); + } + + public function testIsEmpty_WithProfileLink() { + $jsonapiObject = new JsonapiObject($version=null); + + $this->assertTrue($jsonapiObject->isEmpty()); + + $jsonapiObject->addProfile(new TestProfile()); + + $this->assertFalse($jsonapiObject->isEmpty()); + } + + public function testIsEmpty_WithExtensionMembers() { + $jsonapiObject = new JsonapiObject($version=null); + + $this->assertTrue($jsonapiObject->isEmpty()); + + $jsonapiObject->addExtensionMember(new TestExtension(), 'foo', 'bar'); + + $this->assertFalse($jsonapiObject->isEmpty()); + } } diff --git a/tests/objects/LinkObjectTest.php b/tests/objects/LinkObjectTest.php index bdf7e89..6a14c57 100644 --- a/tests/objects/LinkObjectTest.php +++ b/tests/objects/LinkObjectTest.php @@ -3,6 +3,7 @@ namespace alsvanzelf\jsonapiTests\objects; use alsvanzelf\jsonapi\objects\LinkObject; +use alsvanzelf\jsonapiTests\extensions\TestExtension; use PHPUnit\Framework\TestCase; class LinkObjectTest extends TestCase { @@ -31,4 +32,14 @@ public function testIsEmpty_WithAtMembers() { $this->assertFalse($linkObject->isEmpty()); } + + public function testIsEmpty_WithExtensionMembers() { + $linkObject = new LinkObject(); + + $this->assertTrue($linkObject->isEmpty()); + + $linkObject->addExtensionMember(new TestExtension(), 'foo', 'bar'); + + $this->assertFalse($linkObject->isEmpty()); + } } diff --git a/tests/objects/MetaObjectTest.php b/tests/objects/MetaObjectTest.php index 5014060..57e29c2 100644 --- a/tests/objects/MetaObjectTest.php +++ b/tests/objects/MetaObjectTest.php @@ -3,6 +3,7 @@ namespace alsvanzelf\jsonapiTests\objects; use alsvanzelf\jsonapi\objects\MetaObject; +use alsvanzelf\jsonapiTests\extensions\TestExtension; use PHPUnit\Framework\TestCase; class MetaObjectTest extends TestCase { @@ -18,4 +19,24 @@ public function testFromObject_HappyPath() { $this->assertArrayHasKey('foo', $array); $this->assertSame('bar', $array['foo']); } + + public function testIsEmpty_WithAtMembers() { + $metaObject = new MetaObject(); + + $this->assertTrue($metaObject->isEmpty()); + + $metaObject->addAtMember('context', 'test'); + + $this->assertFalse($metaObject->isEmpty()); + } + + public function testIsEmpty_WithExtensionMembers() { + $metaObject = new MetaObject(); + + $this->assertTrue($metaObject->isEmpty()); + + $metaObject->addExtensionMember(new TestExtension(), 'foo', 'bar'); + + $this->assertFalse($metaObject->isEmpty()); + } } diff --git a/tests/objects/RelationshipObjectTest.php b/tests/objects/RelationshipObjectTest.php index 346480a..3f5aeed 100644 --- a/tests/objects/RelationshipObjectTest.php +++ b/tests/objects/RelationshipObjectTest.php @@ -9,6 +9,7 @@ use alsvanzelf\jsonapi\objects\RelationshipObject; use alsvanzelf\jsonapi\objects\ResourceIdentifierObject; use alsvanzelf\jsonapi\objects\ResourceObject; +use alsvanzelf\jsonapiTests\extensions\TestExtension; use PHPUnit\Framework\TestCase; class RelationshipObjectTest extends TestCase { @@ -315,6 +316,16 @@ public function testIsEmpty_WithAtMembers() { $this->assertFalse($relationshipObject->isEmpty()); } + public function testIsEmpty_WithExtensionMembers() { + $relationshipObject = new RelationshipObject(RelationshipObject::TO_ONE); + + $this->assertTrue($relationshipObject->isEmpty()); + + $relationshipObject->addExtensionMember(new TestExtension(), 'foo', 'bar'); + + $this->assertFalse($relationshipObject->isEmpty()); + } + private function validateToOneRelationshipArray(array $array) { $this->assertNotEmpty($array); $this->assertArrayHasKey('data', $array); diff --git a/tests/objects/ResourceIdentifierObjectTest.php b/tests/objects/ResourceIdentifierObjectTest.php index 2508479..bfc956a 100644 --- a/tests/objects/ResourceIdentifierObjectTest.php +++ b/tests/objects/ResourceIdentifierObjectTest.php @@ -4,6 +4,7 @@ use alsvanzelf\jsonapi\exceptions\Exception; use alsvanzelf\jsonapi\objects\ResourceIdentifierObject; +use alsvanzelf\jsonapiTests\extensions\TestExtension; use PHPUnit\Framework\TestCase; class ResourceIdentifierObjectTest extends TestCase { @@ -98,4 +99,14 @@ public function testIsEmpty_WithAtMembers() { $this->assertFalse($resourceIdentifierObject->isEmpty()); } + + public function testIsEmpty_WithExtensionMembers() { + $resourceIdentifierObject = new ResourceIdentifierObject(); + + $this->assertTrue($resourceIdentifierObject->isEmpty()); + + $resourceIdentifierObject->addExtensionMember(new TestExtension(), 'foo', 'bar'); + + $this->assertFalse($resourceIdentifierObject->isEmpty()); + } } From e7fc8ff4b5288b0785c09ccd89d8a1ff7748e9e9 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Mon, 5 Apr 2021 23:22:54 +0200 Subject: [PATCH 19/42] improve test coverage --- tests/profiles/CursorPaginationProfileTest.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/profiles/CursorPaginationProfileTest.php b/tests/profiles/CursorPaginationProfileTest.php index 533f43a..cd40ec4 100644 --- a/tests/profiles/CursorPaginationProfileTest.php +++ b/tests/profiles/CursorPaginationProfileTest.php @@ -305,4 +305,15 @@ public function testSetQueryParameter_EncodedUrl() { $this->assertSame('/people?sort=x&page%5Bsize%5D=10&page%5Bafter%5D=bar', $newUrl); } + + /** + * test method while it is part of the interface + */ + public function testGetKeyword_HappyPath() { + $profile = new CursorPaginationProfile(); + + $keyword = $profile->getKeyword('page'); + + $this->assertSame('page', $keyword); + } } From 214a51724dcfe3af1251f64c621af5c21b4fdf8d Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Mon, 5 Apr 2021 23:50:19 +0200 Subject: [PATCH 20/42] prevent using setUp as it has a BC break between php7.1 and 7.2 --- tests/helpers/ExtensionMemberManagerTest.php | 32 ++++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/helpers/ExtensionMemberManagerTest.php b/tests/helpers/ExtensionMemberManagerTest.php index bbd728c..dfef301 100644 --- a/tests/helpers/ExtensionMemberManagerTest.php +++ b/tests/helpers/ExtensionMemberManagerTest.php @@ -8,21 +8,15 @@ use PHPUnit\Framework\TestCase; class ExtensionMemberManagerTest extends TestCase { - protected $extension; - - protected function setUp() { - $this->extension = new TestExtension(); - $this->extension->setNamespace('test'); - $this->extension->setOfficialLink('https://example.org'); - } - public function testAddExtensionMember_HappyPath() { - $helper = new ExtensionMemberManager(); + $helper = new ExtensionMemberManager(); + $extension = new TestExtension(); + $extension->setNamespace('test'); $this->assertFalse($helper->hasExtensionMembers()); $this->assertSame([], $helper->getExtensionMembers()); - $helper->addExtensionMember($this->extension, 'foo', 'bar'); + $helper->addExtensionMember($extension, 'foo', 'bar'); $array = $helper->getExtensionMembers(); @@ -33,9 +27,11 @@ public function testAddExtensionMember_HappyPath() { } public function testAddExtensionMember_WithNamespacePrefixed() { - $helper = new ExtensionMemberManager(); + $helper = new ExtensionMemberManager(); + $extension = new TestExtension(); + $extension->setNamespace('test'); - $helper->addExtensionMember($this->extension, 'test:foo', 'bar'); + $helper->addExtensionMember($extension, 'test:foo', 'bar'); $array = $helper->getExtensionMembers(); @@ -43,12 +39,14 @@ public function testAddExtensionMember_WithNamespacePrefixed() { } public function testAddExtensionMember_WithObjectValue() { - $helper = new ExtensionMemberManager(); + $helper = new ExtensionMemberManager(); + $extension = new TestExtension(); + $extension->setNamespace('test'); $object = new \stdClass(); $object->bar = 'baz'; - $helper->addExtensionMember($this->extension, 'foo', $object); + $helper->addExtensionMember($extension, 'foo', $object); $array = $helper->getExtensionMembers(); @@ -58,10 +56,12 @@ public function testAddExtensionMember_WithObjectValue() { } public function testAddExtensionMember_InvalidNamespaceOrCharacter() { - $helper = new ExtensionMemberManager(); + $helper = new ExtensionMemberManager(); + $extension = new TestExtension(); + $extension->setNamespace('test'); $this->expectException(InputException::class); - $helper->addExtensionMember($this->extension, 'foo:bar', 'baz'); + $helper->addExtensionMember($extension, 'foo:bar', 'baz'); } } From 867a2247abd9fab7936f7c4fddc87211559e2ed2 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Tue, 6 Apr 2021 00:11:52 +0200 Subject: [PATCH 21/42] check for valid extension namespaces --- src/Document.php | 14 +++++++++++++- tests/DocumentTest.php | 32 ++++++++++++++++++++++++++++++++ tests/SeparateProcessTest.php | 2 ++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/Document.php b/src/Document.php index f11ca25..fddfab5 100644 --- a/src/Document.php +++ b/src/Document.php @@ -2,6 +2,7 @@ namespace alsvanzelf\jsonapi; +use alsvanzelf\jsonapi\exceptions\DuplicateException; use alsvanzelf\jsonapi\exceptions\Exception; use alsvanzelf\jsonapi\exceptions\InputException; use alsvanzelf\jsonapi\helpers\AtMemberManager; @@ -190,9 +191,20 @@ public function unsetJsonapiObject() { * @see https://jsonapi.org/extensions/#extensions * * @param ExtensionInterface $extension + * + * @throws Exception if namespace uses illegal characters + * @throws DuplicateException if namespace conflicts with another applied extension */ public function applyExtension(ExtensionInterface $extension) { - $this->extensions[] = $extension; + $namespace = $extension->getNamespace(); + if (strlen($namespace) < 1 || preg_match('{[^a-zA-Z0-9]}', $namespace) === 1) { + throw new Exception('invalid namespace "'.$namespace.'"'); + } + if (isset($this->extensions[$namespace])) { + throw new DuplicateException('an extension with namespace "'.$namespace.'" is already applied'); + } + + $this->extensions[$namespace] = $extension; if ($this->jsonapi !== null) { $this->jsonapi->addExtension($extension); diff --git a/tests/DocumentTest.php b/tests/DocumentTest.php index 6a715b9..b7e984d 100644 --- a/tests/DocumentTest.php +++ b/tests/DocumentTest.php @@ -2,6 +2,7 @@ namespace alsvanzelf\jsonapiTests; +use alsvanzelf\jsonapi\exceptions\DuplicateException; use alsvanzelf\jsonapi\exceptions\Exception; use alsvanzelf\jsonapi\exceptions\InputException; use alsvanzelf\jsonapi\objects\LinkObject; @@ -203,6 +204,37 @@ public function testApplyExtension_HappyPath() { $this->assertSame('bar', $array['test:foo']); } + public function testApplyExtension_InvalidNamespace() { + $document = new Document(); + $extension = new TestExtension(); + $extension->setNamespace('foo-bar'); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('invalid namespace "foo-bar"'); + + $document->applyExtension($extension); + } + + public function testApplyExtension_ConflictingNamespace() { + $document = new Document(); + + $extension1 = new TestExtension(); + $extension1->setNamespace('foo'); + $document->applyExtension($extension1); + + $extension2 = new TestExtension(); + $extension2->setNamespace('bar'); + $document->applyExtension($extension2); + + $extension3 = new TestExtension(); + $extension3->setNamespace('foo'); + + $this->expectException(DuplicateException::class); + $this->expectExceptionMessage('an extension with namespace "foo" is already applied'); + + $document->applyExtension($extension3); + } + /** * @group Profiles */ diff --git a/tests/SeparateProcessTest.php b/tests/SeparateProcessTest.php index 8680809..90f2b16 100644 --- a/tests/SeparateProcessTest.php +++ b/tests/SeparateProcessTest.php @@ -75,6 +75,7 @@ public function testSendResponse_ContentTypeHeader() { */ public function testSendResponse_ContentTypeHeaderWithExtensions() { $extension = new TestExtension(); + $extension->setNamespace('one'); $extension->setOfficialLink('https://jsonapi.org'); $document = new Document(); @@ -86,6 +87,7 @@ public function testSendResponse_ContentTypeHeaderWithExtensions() { $this->assertSame(['Content-Type: '.Document::CONTENT_TYPE_OFFICIAL.'; ext="https://jsonapi.org"'], xdebug_get_headers()); $extension = new TestExtension(); + $extension->setNamespace('two'); $extension->setOfficialLink('https://jsonapi.org/2'); $document->applyExtension($extension); From 95295216d72235e8473369868b99756071a125d1 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Fri, 14 May 2021 17:03:45 +0200 Subject: [PATCH 22/42] explain when to use local id --- src/helpers/Validator.php | 2 +- src/objects/ResourceIdentifierObject.php | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/helpers/Validator.php b/src/helpers/Validator.php index 12d26fd..692d473 100644 --- a/src/helpers/Validator.php +++ b/src/helpers/Validator.php @@ -85,7 +85,7 @@ public function clearUsedFields($objectContainerToClear) { */ public function claimUsedResourceIdentifier(ResourceInterface $resource) { if ($resource->getResource()->hasIdentification() === false) { - throw new InputException('can not validate resource without identifier, set type and id first'); + throw new InputException('can not validate resource without identifier, set type and id/lid first'); } $resourceKey = $resource->getResource()->getIdentificationKey(); diff --git a/src/objects/ResourceIdentifierObject.php b/src/objects/ResourceIdentifierObject.php index 03a43ca..fb9869b 100644 --- a/src/objects/ResourceIdentifierObject.php +++ b/src/objects/ResourceIdentifierObject.php @@ -88,6 +88,10 @@ public function setId($id) { } /** + * set a local id to connect resources to each other when created on the client + * + * @note this should not be used to send back from the server to the client + * * @param string|int $localId will be casted to a string * * @throws DuplicateException if normal id is already set From a39626031d129986c94b442346b0f383254af513 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Fri, 14 May 2021 17:04:35 +0200 Subject: [PATCH 23/42] test using local ids --- src/objects/ResourceIdentifierObject.php | 1 + .../objects/ResourceIdentifierObjectTest.php | 76 +++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/src/objects/ResourceIdentifierObject.php b/src/objects/ResourceIdentifierObject.php index fb9869b..a06c33f 100644 --- a/src/objects/ResourceIdentifierObject.php +++ b/src/objects/ResourceIdentifierObject.php @@ -3,6 +3,7 @@ namespace alsvanzelf\jsonapi\objects; use alsvanzelf\jsonapi\exceptions\Exception; +use alsvanzelf\jsonapi\exceptions\DuplicateException; use alsvanzelf\jsonapi\helpers\AtMemberManager; use alsvanzelf\jsonapi\helpers\Validator; use alsvanzelf\jsonapi\interfaces\ObjectInterface; diff --git a/tests/objects/ResourceIdentifierObjectTest.php b/tests/objects/ResourceIdentifierObjectTest.php index 2508479..edee0bd 100644 --- a/tests/objects/ResourceIdentifierObjectTest.php +++ b/tests/objects/ResourceIdentifierObjectTest.php @@ -3,10 +3,55 @@ namespace alsvanzelf\jsonapiTests\objects; use alsvanzelf\jsonapi\exceptions\Exception; +use alsvanzelf\jsonapi\exceptions\DuplicateException; use alsvanzelf\jsonapi\objects\ResourceIdentifierObject; use PHPUnit\Framework\TestCase; class ResourceIdentifierObjectTest extends TestCase { + public function testSetId_HappyPath() { + $resourceIdentifierObject = new ResourceIdentifierObject(); + $resourceIdentifierObject->setType('test'); + $resourceIdentifierObject->setId('1'); + + $array = $resourceIdentifierObject->toArray(); + + $this->assertArrayHasKey('id', $array); + $this->assertArrayNotHasKey('lid', $array); + $this->assertSame('1', $array['id']); + } + + public function testSetId_WithLocalIdAlreadySet() { + $resourceIdentifierObject = new ResourceIdentifierObject(); + $resourceIdentifierObject->setType('test'); + $resourceIdentifierObject->setLocalId('uuid-1'); + + $this->expectException(DuplicateException::class); + + $resourceIdentifierObject->setId('1'); + } + + public function testSetLocalId_HappyPath() { + $resourceIdentifierObject = new ResourceIdentifierObject(); + $resourceIdentifierObject->setType('test'); + $resourceIdentifierObject->setLocalId('uuid-1'); + + $array = $resourceIdentifierObject->toArray(); + + $this->assertArrayHasKey('lid', $array); + $this->assertArrayNotHasKey('id', $array); + $this->assertSame('uuid-1', $array['lid']); + } + + public function testSetLocalId_WithIdAlreadySet() { + $resourceIdentifierObject = new ResourceIdentifierObject(); + $resourceIdentifierObject->setType('test'); + $resourceIdentifierObject->setId('1'); + + $this->expectException(DuplicateException::class); + + $resourceIdentifierObject->setLocalId('uuid-1'); + } + public function testEquals_HappyPath() { $one = new ResourceIdentifierObject('test', 1); $two = new ResourceIdentifierObject('test', 2); @@ -25,6 +70,19 @@ public function testEquals_WithoutIdentification() { $one->equals($two); } + public function testEquals_WithLocalId() { + $one = new ResourceIdentifierObject('test'); + $two = new ResourceIdentifierObject('test'); + $new = new ResourceIdentifierObject('test'); + + $one->setLocalId('uuid-1'); + $two->setLocalId('uuid-2'); + $new->setLocalId('uuid-1'); + + $this->assertFalse($one->equals($two)); + $this->assertTrue($one->equals($new)); + } + public function testGetIdentificationKey_HappyPath() { $resourceIdentifierObject = new ResourceIdentifierObject('user', 42); @@ -32,6 +90,7 @@ public function testGetIdentificationKey_HappyPath() { $this->assertArrayHasKey('type', $array); $this->assertArrayHasKey('id', $array); + $this->assertArrayNotHasKey('lid', $array); $this->assertSame('user', $array['type']); $this->assertSame('42', $array['id']); $this->assertTrue($resourceIdentifierObject->hasIdentification()); @@ -59,6 +118,23 @@ public function testGetIdentificationKey_SetAfterwards() { $this->assertSame('user|42', $resourceIdentifierObject->getIdentificationKey()); } + public function testGetIdentificationKey_WithLocalId() { + $resourceIdentifierObject = new ResourceIdentifierObject(); + + $resourceIdentifierObject->setType('user'); + $resourceIdentifierObject->setLocalId('uuid-42'); + + $array = $resourceIdentifierObject->toArray(); + + $this->assertArrayHasKey('type', $array); + $this->assertArrayHasKey('lid', $array); + $this->assertArrayNotHasKey('id', $array); + $this->assertSame('user', $array['type']); + $this->assertSame('uuid-42', $array['lid']); + $this->assertTrue($resourceIdentifierObject->hasIdentification()); + $this->assertSame('user|uuid-42', $resourceIdentifierObject->getIdentificationKey()); + } + public function testGetIdentificationKey_NoIdentification() { $resourceIdentifierObject = new ResourceIdentifierObject(); From 65ebda71f29373b37a08d42c75ebae5795fbd5c5 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Fri, 14 May 2021 17:17:17 +0200 Subject: [PATCH 24/42] test described by root link --- tests/DocumentTest.php | 52 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/DocumentTest.php b/tests/DocumentTest.php index 2cd0916..e92c875 100644 --- a/tests/DocumentTest.php +++ b/tests/DocumentTest.php @@ -98,6 +98,58 @@ public function testAddLink_BlocksUnknownLevel() { $document->addLink('foo', 'https://jsonapi.org', $meta=[], $level='foo'); } + public function testSetSelfLink_HappyPath() { + $document = new Document(); + + $array = $document->toArray(); + $this->assertArrayNotHasKey('links', $array); + + $document->setSelfLink('https://jsonapi.org/foo'); + + $array = $document->toArray(); + $this->assertArrayHasKey('links', $array); + $this->assertCount(1, $array['links']); + $this->assertArrayHasKey('self', $array['links']); + $this->assertSame('https://jsonapi.org/foo', $array['links']['self']); + } + + public function testSetDescribedByLink_HappyPath() { + $document = new Document(); + $document->setDescribedByLink('https://jsonapi.org/format', ['version' => '1.1']); + + $array = $document->toArray(); + + $this->assertCount(1, $array['links']); + if (method_exists($this, 'assertIsArray')) { + $this->assertIsArray($array['links']['describedby']); + } + else { + $this->assertInternalType('array', $array['links']['describedby']); + } + $this->assertCount(2, $array['links']['describedby']); + $this->assertArrayHasKey('href', $array['links']['describedby']); + $this->assertArrayHasKey('meta', $array['links']['describedby']); + $this->assertSame('https://jsonapi.org/format', $array['links']['describedby']['href']); + $this->assertCount(1, $array['links']['describedby']['meta']); + $this->assertArrayHasKey('version', $array['links']['describedby']['meta']); + $this->assertSame('1.1', $array['links']['describedby']['meta']['version']); + } + + public function testSetDescribedByLink_WithMeta() { + $document = new Document(); + + $array = $document->toArray(); + $this->assertArrayNotHasKey('links', $array); + + $document->setDescribedByLink('https://jsonapi.org/format'); + + $array = $document->toArray(); + $this->assertArrayHasKey('links', $array); + $this->assertCount(1, $array['links']); + $this->assertArrayHasKey('describedby', $array['links']); + $this->assertSame('https://jsonapi.org/format', $array['links']['describedby']); + } + public function testAddMeta_HappyPath() { $document = new Document(); From 3c3f011024244bf6ea22ea48126d7410c8c27ba9 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sat, 15 May 2021 12:49:21 +0200 Subject: [PATCH 25/42] fix adding link languages --- src/objects/LinkObject.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/objects/LinkObject.php b/src/objects/LinkObject.php index 63fd793..08efd81 100644 --- a/src/objects/LinkObject.php +++ b/src/objects/LinkObject.php @@ -20,7 +20,7 @@ class LinkObject implements ObjectInterface { /** @var string */ protected $type; /** @var string[] */ - protected $hreflang; + protected $hreflang = []; /** @var MetaObject */ protected $meta; @@ -52,11 +52,11 @@ public function setDescribedBy($href) { * @param string $language */ public function addLanguage($language) { - if ($this->hreflang === null) { + if ($this->hreflang === []) { $this->setHreflang($language); } else { - $this->setHreflang(...$this->hreflang, $language); + $this->setHreflang(...array_merge($this->hreflang, [$language])); } } From 7e53ef2b7a01cfc4fbea4f74a88b28b55ccb3f98 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sat, 15 May 2021 12:49:40 +0200 Subject: [PATCH 26/42] test emptyness --- src/objects/LinkObject.php | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/objects/LinkObject.php b/src/objects/LinkObject.php index 08efd81..f7025a7 100644 --- a/src/objects/LinkObject.php +++ b/src/objects/LinkObject.php @@ -136,6 +136,21 @@ public function isEmpty() { if ($this->href !== null) { return false; } + if ($this->rel !== null) { + return false; + } + if ($this->title !== null) { + return false; + } + if ($this->type !== null) { + return false; + } + if ($this->hreflang !== []) { + return false; + } + if ($this->describedby !== null && $this->describedby->isEmpty() === false) { + return false; + } if ($this->meta !== null && $this->meta->isEmpty() === false) { return false; } @@ -154,13 +169,13 @@ public function toArray() { $array['href'] = $this->href; - if ($this->rel) { + if ($this->rel !== null) { $array['rel'] = $this->rel; } - if ($this->title) { + if ($this->title !== null) { $array['title'] = $this->title; } - if ($this->type) { + if ($this->type !== null) { $array['type'] = $this->type; } if ($this->hreflang !== []) { From 03f34f50cf5037eb05e3fd244650cdb3f2d1c2e2 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sat, 15 May 2021 12:49:48 +0200 Subject: [PATCH 27/42] add some future validation ideas --- src/objects/LinkObject.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/objects/LinkObject.php b/src/objects/LinkObject.php index f7025a7..837875e 100644 --- a/src/objects/LinkObject.php +++ b/src/objects/LinkObject.php @@ -84,6 +84,8 @@ public function setHref($href) { } /** + * @todo validate according to https://tools.ietf.org/html/rfc8288#section-2.1 + * * @param string $relationType */ public function setRelationType($relationType) { @@ -112,6 +114,8 @@ public function setMediaType($mediaType) { } /** + * @todo validate according to https://tools.ietf.org/html/rfc5646 + * * @param string ...$hreflang */ public function setHreflang(...$hreflang) { From 353ef77344138c5e78e000657eab1d326b96325e Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sat, 15 May 2021 12:49:54 +0200 Subject: [PATCH 28/42] test the new link properties --- tests/objects/LinkObjectTest.php | 120 +++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/tests/objects/LinkObjectTest.php b/tests/objects/LinkObjectTest.php index bdf7e89..ad41068 100644 --- a/tests/objects/LinkObjectTest.php +++ b/tests/objects/LinkObjectTest.php @@ -6,6 +6,49 @@ use PHPUnit\Framework\TestCase; class LinkObjectTest extends TestCase { + public function testSetDescribedBy_HappyPath() { + $linkObject = new LinkObject(); + + $this->assertTrue($linkObject->isEmpty()); + + $linkObject->setDescribedBy('https://jsonapi.org'); + + $this->assertFalse($linkObject->isEmpty()); + + $array = $linkObject->toArray(); + + $this->assertArrayHasKey('describedby', $array); + $this->assertArrayHasKey('href', $array['describedby']); + $this->assertSame('https://jsonapi.org', $array['describedby']['href']); + } + + public function testAddLanguage_HappyPath() { + $linkObject = new LinkObject(); + + $this->assertTrue($linkObject->isEmpty()); + + $linkObject->addLanguage('nl-NL'); + + $this->assertFalse($linkObject->isEmpty()); + + $array = $linkObject->toArray(); + + $this->assertArrayHasKey('hreflang', $array); + $this->assertSame('nl-NL', $array['hreflang']); + } + + public function testAddLanguage_Multiple() { + $linkObject = new LinkObject(); + + $linkObject->addLanguage('nl-NL'); + $array = $linkObject->toArray(); + $this->assertSame('nl-NL', $array['hreflang']); + + $linkObject->addLanguage('en-US'); + $array = $linkObject->toArray(); + $this->assertSame(['nl-NL', 'en-US'], $array['hreflang']); + } + public function testAddMeta_HappyPath() { $linkObject = new LinkObject(); @@ -22,6 +65,83 @@ public function testAddMeta_HappyPath() { $this->assertSame('bar', $array['meta']['foo']); } + public function testSetRelationType_HappyPath() { + $linkObject = new LinkObject(); + + $this->assertTrue($linkObject->isEmpty()); + + $linkObject->setRelationType('external'); + + $this->assertFalse($linkObject->isEmpty()); + + $array = $linkObject->toArray(); + + $this->assertArrayHasKey('rel', $array); + $this->assertSame('external', $array['rel']); + } + + public function testSetDescribedByLinkObject_HappyPath() { + $linkObject = new LinkObject(); + + $this->assertTrue($linkObject->isEmpty()); + + $describedBy = new LinkObject('https://jsonapi.org'); + $linkObject->setDescribedByLinkObject($describedBy); + + $this->assertFalse($linkObject->isEmpty()); + + $array = $linkObject->toArray(); + + $this->assertArrayHasKey('describedby', $array); + $this->assertArrayHasKey('href', $array['describedby']); + $this->assertSame('https://jsonapi.org', $array['describedby']['href']); + } + + public function testSetHumanTitle_HappyPath() { + $linkObject = new LinkObject(); + + $this->assertTrue($linkObject->isEmpty()); + + $linkObject->setHumanTitle('A link'); + + $this->assertFalse($linkObject->isEmpty()); + + $array = $linkObject->toArray(); + + $this->assertArrayHasKey('title', $array); + $this->assertSame('A link', $array['title']); + } + + public function testSetMediaType_HappyPath() { + $linkObject = new LinkObject(); + + $this->assertTrue($linkObject->isEmpty()); + + $linkObject->setMediaType('text/html'); + + $this->assertFalse($linkObject->isEmpty()); + + $array = $linkObject->toArray(); + + $this->assertArrayHasKey('type', $array); + $this->assertSame('text/html', $array['type']); + } + + public function testSetHreflang_HappyPath() { + $linkObject = new LinkObject(); + + $this->assertTrue($linkObject->isEmpty()); + + $linkObject->setHreflang('nl-NL', 'en-US'); + + $this->assertFalse($linkObject->isEmpty()); + + $array = $linkObject->toArray(); + + $this->assertArrayHasKey('hreflang', $array); + $this->assertSame(['nl-NL', 'en-US'], $array['hreflang']); + } + public function testIsEmpty_WithAtMembers() { $linkObject = new LinkObject(); From 51a9d275e2999a7327c586dda6e6ef2ef6e394fc Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sat, 15 May 2021 12:58:33 +0200 Subject: [PATCH 29/42] be explicit about time --- tests/example_output/profile/profile.json | 4 ++-- tests/example_output/profile/profile.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/example_output/profile/profile.json b/tests/example_output/profile/profile.json index 9163f43..49d5808 100644 --- a/tests/example_output/profile/profile.json +++ b/tests/example_output/profile/profile.json @@ -10,8 +10,8 @@ "id": "42", "attributes": { "timestamps": { - "created": "2021-04-05T20:19:00+0000", - "updated": "2021-04-05T20:21:00+0000" + "created": "2019-01-01T00:00:00+0000", + "updated": "2021-01-01T00:00:00+0000" } } } diff --git a/tests/example_output/profile/profile.php b/tests/example_output/profile/profile.php index 678219b..16deba8 100644 --- a/tests/example_output/profile/profile.php +++ b/tests/example_output/profile/profile.php @@ -12,7 +12,7 @@ public static function createJsonapiDocument() { $document = new ResourceDocument('user', 42); $document->applyProfile($profile); - $profile->setTimestamps($document, new \DateTime('2019'), new \DateTime('2021')); + $profile->setTimestamps($document, new \DateTime('2019-01-01T00:00:00+0000'), new \DateTime('2021-01-01T00:00:00+0000')); return $document; } From 107c3315457d37708c4162de4aa2663fe13806e1 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sat, 15 May 2021 15:00:06 +0200 Subject: [PATCH 30/42] annotate new tests --- tests/DocumentTest.php | 6 ++++++ tests/SeparateProcessTest.php | 4 ++++ tests/helpers/ExtensionMemberManagerTest.php | 3 +++ tests/objects/ErrorObjectTest.php | 3 +++ tests/objects/JsonapiObjectTest.php | 9 +++++++++ tests/objects/LinkObjectTest.php | 3 +++ tests/objects/MetaObjectTest.php | 3 +++ tests/objects/RelationshipObjectTest.php | 3 +++ tests/objects/ResourceIdentifierObjectTest.php | 3 +++ 9 files changed, 37 insertions(+) diff --git a/tests/DocumentTest.php b/tests/DocumentTest.php index b7e984d..c90ae6b 100644 --- a/tests/DocumentTest.php +++ b/tests/DocumentTest.php @@ -204,6 +204,9 @@ public function testApplyExtension_HappyPath() { $this->assertSame('bar', $array['test:foo']); } + /** + * @group Extensions + */ public function testApplyExtension_InvalidNamespace() { $document = new Document(); $extension = new TestExtension(); @@ -215,6 +218,9 @@ public function testApplyExtension_InvalidNamespace() { $document->applyExtension($extension); } + /** + * @group Extensions + */ public function testApplyExtension_ConflictingNamespace() { $document = new Document(); diff --git a/tests/SeparateProcessTest.php b/tests/SeparateProcessTest.php index 722d2bc..ec00ccd 100644 --- a/tests/SeparateProcessTest.php +++ b/tests/SeparateProcessTest.php @@ -78,6 +78,10 @@ public function testSendResponse_ContentTypeHeader() { * @group Extensions */ public function testSendResponse_ContentTypeHeaderWithExtensions() { + if (extension_loaded('xdebug') === false) { + $this->markTestSkipped('can not run without xdebug'); + } + $extension = new TestExtension(); $extension->setNamespace('one'); $extension->setOfficialLink('https://jsonapi.org'); diff --git a/tests/helpers/ExtensionMemberManagerTest.php b/tests/helpers/ExtensionMemberManagerTest.php index dfef301..92616bb 100644 --- a/tests/helpers/ExtensionMemberManagerTest.php +++ b/tests/helpers/ExtensionMemberManagerTest.php @@ -7,6 +7,9 @@ use alsvanzelf\jsonapiTests\extensions\TestExtension; use PHPUnit\Framework\TestCase; +/** + * @group Extensions + */ class ExtensionMemberManagerTest extends TestCase { public function testAddExtensionMember_HappyPath() { $helper = new ExtensionMemberManager(); diff --git a/tests/objects/ErrorObjectTest.php b/tests/objects/ErrorObjectTest.php index ee0629a..7046e31 100644 --- a/tests/objects/ErrorObjectTest.php +++ b/tests/objects/ErrorObjectTest.php @@ -159,6 +159,9 @@ public function testIsEmpty_All() { $this->assertFalse($errorObject->isEmpty()); } + /** + * @group Extensions + */ public function testToArray_WithExtensionMembers() { $errorObject = new ErrorObject(); $extension = new TestExtension(); diff --git a/tests/objects/JsonapiObjectTest.php b/tests/objects/JsonapiObjectTest.php index c866d95..882c708 100644 --- a/tests/objects/JsonapiObjectTest.php +++ b/tests/objects/JsonapiObjectTest.php @@ -34,6 +34,9 @@ public function testIsEmpty_WithAtMembers() { $this->assertFalse($jsonapiObject->isEmpty()); } + /** + * @group Extensions + */ public function testIsEmpty_WithExtensionLink() { $jsonapiObject = new JsonapiObject($version=null); @@ -44,6 +47,9 @@ public function testIsEmpty_WithExtensionLink() { $this->assertFalse($jsonapiObject->isEmpty()); } + /** + * @group Profiles + */ public function testIsEmpty_WithProfileLink() { $jsonapiObject = new JsonapiObject($version=null); @@ -54,6 +60,9 @@ public function testIsEmpty_WithProfileLink() { $this->assertFalse($jsonapiObject->isEmpty()); } + /** + * @group Extensions + */ public function testIsEmpty_WithExtensionMembers() { $jsonapiObject = new JsonapiObject($version=null); diff --git a/tests/objects/LinkObjectTest.php b/tests/objects/LinkObjectTest.php index 6a14c57..941bfbc 100644 --- a/tests/objects/LinkObjectTest.php +++ b/tests/objects/LinkObjectTest.php @@ -33,6 +33,9 @@ public function testIsEmpty_WithAtMembers() { $this->assertFalse($linkObject->isEmpty()); } + /** + * @group Extensions + */ public function testIsEmpty_WithExtensionMembers() { $linkObject = new LinkObject(); diff --git a/tests/objects/MetaObjectTest.php b/tests/objects/MetaObjectTest.php index 57e29c2..fc4cb6a 100644 --- a/tests/objects/MetaObjectTest.php +++ b/tests/objects/MetaObjectTest.php @@ -30,6 +30,9 @@ public function testIsEmpty_WithAtMembers() { $this->assertFalse($metaObject->isEmpty()); } + /** + * @group Extensions + */ public function testIsEmpty_WithExtensionMembers() { $metaObject = new MetaObject(); diff --git a/tests/objects/RelationshipObjectTest.php b/tests/objects/RelationshipObjectTest.php index 3f5aeed..e5a99fb 100644 --- a/tests/objects/RelationshipObjectTest.php +++ b/tests/objects/RelationshipObjectTest.php @@ -316,6 +316,9 @@ public function testIsEmpty_WithAtMembers() { $this->assertFalse($relationshipObject->isEmpty()); } + /** + * @group Extensions + */ public function testIsEmpty_WithExtensionMembers() { $relationshipObject = new RelationshipObject(RelationshipObject::TO_ONE); diff --git a/tests/objects/ResourceIdentifierObjectTest.php b/tests/objects/ResourceIdentifierObjectTest.php index bfc956a..dd196a7 100644 --- a/tests/objects/ResourceIdentifierObjectTest.php +++ b/tests/objects/ResourceIdentifierObjectTest.php @@ -100,6 +100,9 @@ public function testIsEmpty_WithAtMembers() { $this->assertFalse($resourceIdentifierObject->isEmpty()); } + /** + * @group Extensions + */ public function testIsEmpty_WithExtensionMembers() { $resourceIdentifierObject = new ResourceIdentifierObject(); From c01a21ea7f4a2d9de43bf1426f4b024f6d973a76 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sun, 16 May 2021 14:54:59 +0200 Subject: [PATCH 31/42] mark all methods using link arrays as deprecated --- src/helpers/LinksManager.php | 6 ++++++ src/objects/ErrorObject.php | 12 ++++++++++++ src/objects/LinksObject.php | 6 ++++++ tests/helpers/LinksManagerTest.php | 6 ++++++ tests/objects/LinksArrayTest.php | 3 +++ tests/objects/LinksObjectTest.php | 6 ++++++ 6 files changed, 39 insertions(+) diff --git a/src/helpers/LinksManager.php b/src/helpers/LinksManager.php index 97c1c7f..37bd14c 100644 --- a/src/helpers/LinksManager.php +++ b/src/helpers/LinksManager.php @@ -29,6 +29,8 @@ public function addLink($key, $href, array $meta=[]) { /** * append a link to a key with an array of links * + * @deprecated array links are not supported anymore {@see addLink()} + * * @param string $key * @param string $href * @param array $meta optional, if given a LinkObject is added, otherwise a link string is added @@ -56,6 +58,8 @@ public function addLinkObject($key, LinkObject $linkObject) { /** * set a key containing a LinksArray * + * @deprecated array links are not supported anymore {@see addLinkObject()} + * * @param string $key * @param LinksArray $linksArray */ @@ -67,6 +71,8 @@ public function addLinksArray($key, LinksArray $linksArray) { /** * append a LinkObject to a key with a LinksArray * + * @deprecated array links are not supported anymore {@see addLinkObject()} + * * @param string $key * @param LinkObject $linkObject */ diff --git a/src/objects/ErrorObject.php b/src/objects/ErrorObject.php index 9f0eade..25a5c0c 100644 --- a/src/objects/ErrorObject.php +++ b/src/objects/ErrorObject.php @@ -152,9 +152,21 @@ public function setAboutLink($href, array $meta=[]) { $this->addLink('about', $href, $meta); } + /** + * set the link of the generic type of this error, explained in a human-friendly way + * + * @param string $href + * @param array $meta optional, if given a LinkObject is added, otherwise a link string is added + */ + public function setTypeLink($href, array $meta=[]) { + $this->addLink('type', $href, $meta); + } + /** * append a link of the generic type of this error, explained in a human-friendly way * + * @deprecated array links are not supported anymore {@see setTypeLink()} + * * @param string $href * @param array $meta optional, if given a LinkObject is added, otherwise a link string is added */ diff --git a/src/objects/LinksObject.php b/src/objects/LinksObject.php index 67e9640..0d9c659 100644 --- a/src/objects/LinksObject.php +++ b/src/objects/LinksObject.php @@ -64,6 +64,8 @@ public function add($key, $href, array $meta=[]) { * * @see LinksArray for use cases * + * @deprecated array links are not supported anymore {@see add()} + * * @param string $key * @param string $href * @param array $meta optional, if given a LinkObject is added, otherwise a link string is added @@ -120,6 +122,8 @@ public function addLinkObject($key, LinkObject $linkObject) { } /** + * @deprecated array links are not supported anymore {@see addLinkObject()} + * * @param string $key * @param LinksArray $linksArray * @@ -136,6 +140,8 @@ public function addLinksArray($key, LinksArray $linksArray) { } /** + * @deprecated array links are not supported anymore {@see addLinkObject()} + * * @param string $key * @param LinkObject $linkObject * diff --git a/tests/helpers/LinksManagerTest.php b/tests/helpers/LinksManagerTest.php index 980f253..14bf66b 100644 --- a/tests/helpers/LinksManagerTest.php +++ b/tests/helpers/LinksManagerTest.php @@ -82,6 +82,9 @@ public function testAddLinkObject_HappyPath() { $this->assertSame('https://jsonapi.org', $array['foo']['href']); } + /** + * @deprecated array links are not supported anymore + */ public function testAddLinksArray_HappyPath() { $linksArray = new LinksArray(); $linksArray->add('https://jsonapi.org'); @@ -98,6 +101,9 @@ public function testAddLinksArray_HappyPath() { $this->assertSame('https://jsonapi.org', $array['foo'][0]); } + /** + * @deprecated array links are not supported anymore + */ public function testAppendLinkObject_HappyPath() { $linksManager = new LinksManager(); $linksManager->addLinksArray('foo', LinksArray::fromArray(['https://jsonapi.org/1'])); diff --git a/tests/objects/LinksArrayTest.php b/tests/objects/LinksArrayTest.php index 5b9945c..f0d0c7d 100644 --- a/tests/objects/LinksArrayTest.php +++ b/tests/objects/LinksArrayTest.php @@ -5,6 +5,9 @@ use alsvanzelf\jsonapi\objects\LinksArray; use PHPUnit\Framework\TestCase; +/** + * @deprecated array links are not supported anymore + */ class LinksArrayTest extends TestCase { public function testFromObject_HappyPath() { $object = new \stdClass(); diff --git a/tests/objects/LinksObjectTest.php b/tests/objects/LinksObjectTest.php index ac7b166..ac2995f 100644 --- a/tests/objects/LinksObjectTest.php +++ b/tests/objects/LinksObjectTest.php @@ -109,6 +109,9 @@ public function testAddLinkObject_ExistingKey() { $linksObject->addLinkObject($key='foo', $linkObject); } + /** + * @deprecated array links are not supported anymore + */ public function testAddLinksArray_HappyPath() { $linksObject = new LinksObject(); $linksObject->addLinksArray('foo', LinksArray::fromArray(['https://jsonapi.org'])); @@ -122,6 +125,9 @@ public function testAddLinksArray_HappyPath() { $this->assertSame('https://jsonapi.org', $array['foo'][0]); } + /** + * @deprecated array links are not supported anymore + */ public function testAddLinksArray_BlocksReusingNonArray() { $linksObject = new LinksObject(); $linksObject->add('foo', 'https://jsonapi.org'); From c168e95aaa1f8c8efcd3568bf49fae1119ec5882 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sun, 16 May 2021 14:55:32 +0200 Subject: [PATCH 32/42] adjust tests --- examples/errors_all_options.php | 2 +- src/objects/ErrorObject.php | 2 +- src/profiles/CursorPaginationProfile.php | 8 ++++---- .../errors_all_options/errors_all_options.json | 16 ++++++---------- .../errors_all_options/errors_all_options.php | 2 +- tests/profiles/CursorPaginationProfileTest.php | 12 ++++-------- 6 files changed, 17 insertions(+), 25 deletions(-) diff --git a/examples/errors_all_options.php b/examples/errors_all_options.php index 5be9ab7..4f628cc 100644 --- a/examples/errors_all_options.php +++ b/examples/errors_all_options.php @@ -38,7 +38,7 @@ $errorSpecApi->setHumanTitle($genericTitle='Too much options'); $errorSpecApi->setHumanDetails($specificDetails='Please, choose a bit less. Consult your ...'); $errorSpecApi->setAboutLink($specificAboutLink='https://www.example.com/explanation.html', ['foo'=>'bar']); -$errorSpecApi->appendTypeLink($genericTypeLink='https://www.example.com/documentation.html', ['foo'=>'bar']); +$errorSpecApi->setTypeLink($genericTypeLink='https://www.example.com/documentation.html', ['foo'=>'bar']); /** * prepare multiple error objects for the errors response diff --git a/src/objects/ErrorObject.php b/src/objects/ErrorObject.php index 25a5c0c..e0c6646 100644 --- a/src/objects/ErrorObject.php +++ b/src/objects/ErrorObject.php @@ -138,7 +138,7 @@ public function setHumanExplanation($genericTitle, $specificDetails=null, $speci $this->setAboutLink($specificAboutLink); } if ($genericTypeLink !== null) { - $this->appendTypeLink($genericTypeLink); + $this->setTypeLink($genericTypeLink); } } diff --git a/src/profiles/CursorPaginationProfile.php b/src/profiles/CursorPaginationProfile.php index 77d9fc2..e95f523 100644 --- a/src/profiles/CursorPaginationProfile.php +++ b/src/profiles/CursorPaginationProfile.php @@ -248,7 +248,7 @@ public function setPaginationMeta(PaginableInterface $paginable, $exactTotal=nul */ public function getUnsupportedSortErrorObject($genericTitle=null, $specificDetails=null) { $errorObject = new ErrorObject('Unsupported sort'); - $errorObject->appendTypeLink('https://jsonapi.org/profiles/ethanresnick/cursor-pagination/unsupported-sort'); + $errorObject->setTypeLink('https://jsonapi.org/profiles/ethanresnick/cursor-pagination/unsupported-sort'); $errorObject->blameQueryParameter('sort'); $errorObject->setHttpStatusCode(400); @@ -278,7 +278,7 @@ public function getUnsupportedSortErrorObject($genericTitle=null, $specificDetai */ public function getMaxPageSizeExceededErrorObject($maxSize, $genericTitle=null, $specificDetails=null) { $errorObject = new ErrorObject('Max page size exceeded'); - $errorObject->appendTypeLink('https://jsonapi.org/profiles/ethanresnick/cursor-pagination/max-size-exceeded'); + $errorObject->setTypeLink('https://jsonapi.org/profiles/ethanresnick/cursor-pagination/max-size-exceeded'); $errorObject->blameQueryParameter('page[size]'); $errorObject->setHttpStatusCode(400); $errorObject->addMeta('page', $value=['maxSize' => $maxSize]); @@ -313,7 +313,7 @@ public function getInvalidParameterValueErrorObject($queryParameter, $typeLink=n $errorObject->setHttpStatusCode(400); if ($typeLink !== null) { - $errorObject->appendTypeLink($typeLink); + $errorObject->setTypeLink($typeLink); } if ($genericTitle !== null) { @@ -337,7 +337,7 @@ public function getInvalidParameterValueErrorObject($queryParameter, $typeLink=n */ public function getRangePaginationNotSupportedErrorObject($genericTitle=null, $specificDetails=null) { $errorObject = new ErrorObject('Range pagination not supported'); - $errorObject->appendTypeLink('https://jsonapi.org/profiles/ethanresnick/cursor-pagination/range-pagination-not-supported'); + $errorObject->setTypeLink('https://jsonapi.org/profiles/ethanresnick/cursor-pagination/range-pagination-not-supported'); $errorObject->setHttpStatusCode(400); if ($genericTitle !== null) { diff --git a/tests/example_output/errors_all_options/errors_all_options.json b/tests/example_output/errors_all_options/errors_all_options.json index b0cd4a3..11c8e33 100644 --- a/tests/example_output/errors_all_options/errors_all_options.json +++ b/tests/example_output/errors_all_options/errors_all_options.json @@ -17,9 +17,7 @@ "detail": "Please, choose a bit less. Consult your ...", "links": { "about": "https://www.example.com/explanation.html", - "type": [ - "https://www.example.com/documentation.html" - ] + "type": "https://www.example.com/documentation.html" } }, { @@ -35,14 +33,12 @@ "foo": "bar" } }, - "type": [ - { - "href": "https://www.example.com/documentation.html", - "meta": { - "foo": "bar" - } + "type": { + "href": "https://www.example.com/documentation.html", + "meta": { + "foo": "bar" } - ] + } }, "source": { "pointer": "/data/attributes/title", diff --git a/tests/example_output/errors_all_options/errors_all_options.php b/tests/example_output/errors_all_options/errors_all_options.php index 7ca6e96..f0a4076 100644 --- a/tests/example_output/errors_all_options/errors_all_options.php +++ b/tests/example_output/errors_all_options/errors_all_options.php @@ -19,7 +19,7 @@ public static function createJsonapiDocument() { $errorSpecApi->setHumanTitle($genericTitle='Too much options'); $errorSpecApi->setHumanDetails($specificDetails='Please, choose a bit less. Consult your ...'); $errorSpecApi->setAboutLink($specificAboutLink='https://www.example.com/explanation.html', ['foo'=>'bar']); - $errorSpecApi->appendTypeLink($genericTypeLink='https://www.example.com/documentation.html', ['foo'=>'bar']); + $errorSpecApi->setTypeLink($genericTypeLink='https://www.example.com/documentation.html', ['foo'=>'bar']); $metaObject = new \stdClass(); $metaObject->property = 'value'; diff --git a/tests/profiles/CursorPaginationProfileTest.php b/tests/profiles/CursorPaginationProfileTest.php index 4fb3db8..97704d3 100644 --- a/tests/profiles/CursorPaginationProfileTest.php +++ b/tests/profiles/CursorPaginationProfileTest.php @@ -229,12 +229,11 @@ public function testGetUnsupportedSortErrorObject_HappyPath() { $this->assertArrayHasKey('type', $array['links']); $this->assertArrayHasKey('source', $array); $this->assertArrayHasKey('parameter', $array['source']); - $this->assertCount(1, $array['links']['type']); $this->assertSame('400', $array['status']); $this->assertSame('Unsupported sort', $array['code']); $this->assertSame($genericTitle, $array['title']); $this->assertSame($specificDetails, $array['detail']); - $this->assertSame('https://jsonapi.org/profiles/ethanresnick/cursor-pagination/unsupported-sort', $array['links']['type'][0]); + $this->assertSame('https://jsonapi.org/profiles/ethanresnick/cursor-pagination/unsupported-sort', $array['links']['type']); $this->assertSame('sort', $array['source']['parameter']); } @@ -259,13 +258,12 @@ public function testGetMaxPageSizeExceededErrorObject_HappyPath() { $this->assertArrayHasKey('meta', $array); $this->assertArrayHasKey('page', $array['meta']); $this->assertArrayHasKey('maxSize', $array['meta']['page']); - $this->assertCount(1, $array['links']['type']); $this->assertSame('400', $array['status']); $this->assertSame('Max page size exceeded', $array['code']); $this->assertSame($genericTitle, $array['title']); $this->assertSame($specificDetails, $array['detail']); $this->assertSame('page[size]', $array['source']['parameter']); - $this->assertSame('https://jsonapi.org/profiles/ethanresnick/cursor-pagination/max-size-exceeded', $array['links']['type'][0]); + $this->assertSame('https://jsonapi.org/profiles/ethanresnick/cursor-pagination/max-size-exceeded', $array['links']['type']); $this->assertSame(42, $array['meta']['page']['maxSize']); } @@ -288,13 +286,12 @@ public function testGetInvalidParameterValueErrorObject_HappyPath() { $this->assertArrayHasKey('type', $array['links']); $this->assertArrayHasKey('source', $array); $this->assertArrayHasKey('parameter', $array['source']); - $this->assertCount(1, $array['links']['type']); $this->assertSame('400', $array['status']); $this->assertSame('Invalid parameter value', $array['code']); $this->assertSame($genericTitle, $array['title']); $this->assertSame($specificDetails, $array['detail']); $this->assertSame('page[size]', $array['source']['parameter']); - $this->assertSame('https://jsonapi.org', $array['links']['type'][0]); + $this->assertSame('https://jsonapi.org', $array['links']['type']); } public function testGetRangePaginationNotSupportedErrorObject_HappyPath() { @@ -312,12 +309,11 @@ public function testGetRangePaginationNotSupportedErrorObject_HappyPath() { $this->assertArrayHasKey('detail', $array); $this->assertArrayHasKey('links', $array); $this->assertArrayHasKey('type', $array['links']); - $this->assertCount(1, $array['links']['type']); $this->assertSame('400', $array['status']); $this->assertSame('Range pagination not supported', $array['code']); $this->assertSame($genericTitle, $array['title']); $this->assertSame($specificDetails, $array['detail']); - $this->assertSame('https://jsonapi.org/profiles/ethanresnick/cursor-pagination/range-pagination-not-supported', $array['links']['type'][0]); + $this->assertSame('https://jsonapi.org/profiles/ethanresnick/cursor-pagination/range-pagination-not-supported', $array['links']['type']); } public function testSetQueryParameter_HappyPath() { From 96471988370c3c37378058b28e53efba5dbd18e4 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sun, 16 May 2021 14:55:40 +0200 Subject: [PATCH 33/42] keep code coverage for the deprecated method --- tests/objects/ErrorObjectTest.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/objects/ErrorObjectTest.php b/tests/objects/ErrorObjectTest.php index 7046e31..98a3125 100644 --- a/tests/objects/ErrorObjectTest.php +++ b/tests/objects/ErrorObjectTest.php @@ -114,6 +114,24 @@ public function testFromException_BlocksNonException() { ErrorObject::fromException(new \stdClass()); } + /** + * @deprecated array links are not supported anymore + */ + public function testAppendTypeLink_HappyPath() { + $errorObject = new ErrorObject(); + $this->assertTrue($errorObject->isEmpty()); + + $errorObject->appendTypeLink('https://jsonapi.org'); + + $this->assertFalse($errorObject->isEmpty()); + + $array = $errorObject->toArray(); + + $this->assertArrayHasKey('links', $array); + $this->assertArrayHasKey('type', $array['links']); + $this->assertSame(['https://jsonapi.org'], $array['links']['type']); + } + public function testIsEmpty_All() { $errorObject = new ErrorObject(); $this->assertTrue($errorObject->isEmpty()); From 11a2fc74934983932cc695fefb1a01d4fa21a4b2 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sun, 16 May 2021 15:49:30 +0200 Subject: [PATCH 34/42] CS --- src/helpers/LinksManager.php | 6 +++--- src/objects/ErrorObject.php | 2 +- src/objects/LinksObject.php | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/helpers/LinksManager.php b/src/helpers/LinksManager.php index 37bd14c..ab234a4 100644 --- a/src/helpers/LinksManager.php +++ b/src/helpers/LinksManager.php @@ -29,7 +29,7 @@ public function addLink($key, $href, array $meta=[]) { /** * append a link to a key with an array of links * - * @deprecated array links are not supported anymore {@see addLink()} + * @deprecated array links are not supported anymore {@see ->addLink()} * * @param string $key * @param string $href @@ -58,7 +58,7 @@ public function addLinkObject($key, LinkObject $linkObject) { /** * set a key containing a LinksArray * - * @deprecated array links are not supported anymore {@see addLinkObject()} + * @deprecated array links are not supported anymore {@see ->addLinkObject()} * * @param string $key * @param LinksArray $linksArray @@ -71,7 +71,7 @@ public function addLinksArray($key, LinksArray $linksArray) { /** * append a LinkObject to a key with a LinksArray * - * @deprecated array links are not supported anymore {@see addLinkObject()} + * @deprecated array links are not supported anymore {@see ->addLinkObject()} * * @param string $key * @param LinkObject $linkObject diff --git a/src/objects/ErrorObject.php b/src/objects/ErrorObject.php index e0c6646..3d40d78 100644 --- a/src/objects/ErrorObject.php +++ b/src/objects/ErrorObject.php @@ -165,7 +165,7 @@ public function setTypeLink($href, array $meta=[]) { /** * append a link of the generic type of this error, explained in a human-friendly way * - * @deprecated array links are not supported anymore {@see setTypeLink()} + * @deprecated array links are not supported anymore {@see ->setTypeLink()} * * @param string $href * @param array $meta optional, if given a LinkObject is added, otherwise a link string is added diff --git a/src/objects/LinksObject.php b/src/objects/LinksObject.php index 0d9c659..1e759ee 100644 --- a/src/objects/LinksObject.php +++ b/src/objects/LinksObject.php @@ -64,7 +64,7 @@ public function add($key, $href, array $meta=[]) { * * @see LinksArray for use cases * - * @deprecated array links are not supported anymore {@see add()} + * @deprecated array links are not supported anymore {@see ->add()} * * @param string $key * @param string $href @@ -122,7 +122,7 @@ public function addLinkObject($key, LinkObject $linkObject) { } /** - * @deprecated array links are not supported anymore {@see addLinkObject()} + * @deprecated array links are not supported anymore {@see ->addLinkObject()} * * @param string $key * @param LinksArray $linksArray @@ -140,7 +140,7 @@ public function addLinksArray($key, LinksArray $linksArray) { } /** - * @deprecated array links are not supported anymore {@see addLinkObject()} + * @deprecated array links are not supported anymore {@see ->addLinkObject()} * * @param string $key * @param LinkObject $linkObject From 9c64d6c3d23e56e6fd4a15fd1ebc1b555febcb01 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sun, 16 May 2021 15:53:43 +0200 Subject: [PATCH 35/42] test From e02f98611e900c5b8e1b8336b13d6d61b2e718ce Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sun, 16 May 2021 16:40:57 +0200 Subject: [PATCH 36/42] add the extension/profile headers to the root self link --- src/Document.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Document.php b/src/Document.php index cafa441..372a4a1 100644 --- a/src/Document.php +++ b/src/Document.php @@ -15,6 +15,7 @@ use alsvanzelf\jsonapi\interfaces\ExtensionInterface; use alsvanzelf\jsonapi\interfaces\ProfileInterface; use alsvanzelf\jsonapi\objects\JsonapiObject; +use alsvanzelf\jsonapi\objects\LinkObject; use alsvanzelf\jsonapi\objects\LinksObject; use alsvanzelf\jsonapi\objects\MetaObject; @@ -117,12 +118,24 @@ public function addLink($key, $href, array $meta=[], $level=Document::LEVEL_ROOT /** * set the self link on the document * + * @note a LinkObject is added when extensions or profiles are applied + * * @param string $href * @param array $meta optional, if given a LinkObject is added, otherwise a link string is added * @param string $level one of the Document::LEVEL_* constants, optional, defaults to Document::LEVEL_ROOT */ public function setSelfLink($href, array $meta=[], $level=Document::LEVEL_ROOT) { - $this->addLink('self', $href, $meta, $level); + if ($level === Document::LEVEL_ROOT && ($this->extensions !== [] || $this->profiles !== [])) { + $contentType = Converter::prepareContentType(Document::CONTENT_TYPE_OFFICIAL, $this->extensions, $this->profiles); + + $linkObject = new LinkObject($href, $meta); + $linkObject->setMediaType($contentType); + + $this->addLinkObject('self', $linkObject); + } + else { + $this->addLink('self', $href, $meta, $level); + } } /** From 9c3143b26a807872a76b518ec6243355110151ed Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sun, 16 May 2021 16:42:27 +0200 Subject: [PATCH 37/42] improve tests around applying extensions and profiles --- tests/DocumentTest.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/DocumentTest.php b/tests/DocumentTest.php index 30043cb..fd2bbc6 100644 --- a/tests/DocumentTest.php +++ b/tests/DocumentTest.php @@ -237,7 +237,7 @@ public function testAddLinkObject_HappyPath() { public function testApplyExtension_HappyPath() { $extension = new TestExtension(); $extension->setNamespace('test'); - $extension->setOfficialLink('https://jsonapi.org'); + $extension->setOfficialLink('https://jsonapi.org/extension'); $document = new Document(); $document->applyExtension($extension); @@ -245,13 +245,14 @@ public function testApplyExtension_HappyPath() { $array = $document->toArray(); + $this->assertCount(2, $array); $this->assertArrayHasKey('jsonapi', $array); $this->assertCount(2, $array['jsonapi']); $this->assertSame('1.1', $array['jsonapi']['version']); $this->assertArrayHasKey('ext', $array['jsonapi']); $this->assertCount(1, $array['jsonapi']['ext']); $this->assertArrayHasKey(0, $array['jsonapi']['ext']); - $this->assertSame('https://jsonapi.org', $array['jsonapi']['ext'][0]); + $this->assertSame('https://jsonapi.org/extension', $array['jsonapi']['ext'][0]); $this->assertArrayHasKey('test:foo', $array); $this->assertSame('bar', $array['test:foo']); } @@ -298,20 +299,21 @@ public function testApplyExtension_ConflictingNamespace() { */ public function testApplyProfile_HappyPath() { $profile = new TestProfile(); - $profile->setOfficialLink('https://jsonapi.org'); + $profile->setOfficialLink('https://jsonapi.org/profile'); $document = new Document(); $document->applyProfile($profile); $array = $document->toArray(); + $this->assertCount(1, $array); $this->assertArrayHasKey('jsonapi', $array); $this->assertCount(2, $array['jsonapi']); $this->assertSame('1.1', $array['jsonapi']['version']); $this->assertArrayHasKey('profile', $array['jsonapi']); $this->assertCount(1, $array['jsonapi']['profile']); $this->assertArrayHasKey(0, $array['jsonapi']['profile']); - $this->assertSame('https://jsonapi.org', $array['jsonapi']['profile'][0]); + $this->assertSame('https://jsonapi.org/profile', $array['jsonapi']['profile'][0]); } public function testToJson_HappyPath() { From 9afcda0501107fbb77a7d333f1817c00f4f507ee Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sun, 16 May 2021 16:43:01 +0200 Subject: [PATCH 38/42] test whether the extended self link works --- tests/DocumentTest.php | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/DocumentTest.php b/tests/DocumentTest.php index fd2bbc6..020a70a 100644 --- a/tests/DocumentTest.php +++ b/tests/DocumentTest.php @@ -242,10 +242,11 @@ public function testApplyExtension_HappyPath() { $document = new Document(); $document->applyExtension($extension); $document->addExtensionMember($extension, 'foo', 'bar'); + $document->setSelfLink('https://jsonapi.org/foo'); $array = $document->toArray(); - $this->assertCount(2, $array); + $this->assertCount(3, $array); $this->assertArrayHasKey('jsonapi', $array); $this->assertCount(2, $array['jsonapi']); $this->assertSame('1.1', $array['jsonapi']['version']); @@ -255,6 +256,14 @@ public function testApplyExtension_HappyPath() { $this->assertSame('https://jsonapi.org/extension', $array['jsonapi']['ext'][0]); $this->assertArrayHasKey('test:foo', $array); $this->assertSame('bar', $array['test:foo']); + $this->assertArrayHasKey('links', $array); + $this->assertCount(1, $array['links']); + $this->assertArrayHasKey('self', $array['links']); + $this->assertCount(2, $array['links']['self']); + $this->assertArrayHasKey('href', $array['links']['self']); + $this->assertArrayHasKey('type', $array['links']['self']); + $this->assertSame('https://jsonapi.org/foo', $array['links']['self']['href']); + $this->assertSame('application/vnd.api+json; ext="https://jsonapi.org/extension"', $array['links']['self']['type']); } /** @@ -303,10 +312,11 @@ public function testApplyProfile_HappyPath() { $document = new Document(); $document->applyProfile($profile); + $document->setSelfLink('https://jsonapi.org/foo'); $array = $document->toArray(); - $this->assertCount(1, $array); + $this->assertCount(2, $array); $this->assertArrayHasKey('jsonapi', $array); $this->assertCount(2, $array['jsonapi']); $this->assertSame('1.1', $array['jsonapi']['version']); @@ -314,6 +324,14 @@ public function testApplyProfile_HappyPath() { $this->assertCount(1, $array['jsonapi']['profile']); $this->assertArrayHasKey(0, $array['jsonapi']['profile']); $this->assertSame('https://jsonapi.org/profile', $array['jsonapi']['profile'][0]); + $this->assertArrayHasKey('links', $array); + $this->assertCount(1, $array['links']); + $this->assertArrayHasKey('self', $array['links']); + $this->assertCount(2, $array['links']['self']); + $this->assertArrayHasKey('href', $array['links']['self']); + $this->assertArrayHasKey('type', $array['links']['self']); + $this->assertSame('https://jsonapi.org/foo', $array['links']['self']['href']); + $this->assertSame('application/vnd.api+json; profile="https://jsonapi.org/profile"', $array['links']['self']['type']); } public function testToJson_HappyPath() { From d3573221297fa2021e246d55383d8528fdedb58a Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sun, 16 May 2021 16:45:12 +0200 Subject: [PATCH 39/42] re-use links initialization of the link manager --- src/Document.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Document.php b/src/Document.php index 372a4a1..ddb75d9 100644 --- a/src/Document.php +++ b/src/Document.php @@ -23,7 +23,9 @@ * @see ResourceDocument, CollectionDocument, ErrorsDocument or MetaDocument */ abstract class Document implements DocumentInterface, \JsonSerializable { - use AtMemberManager, ExtensionMemberManager, HttpStatusCodeManager, LinksManager; + use AtMemberManager, ExtensionMemberManager, HttpStatusCodeManager, LinksManager { + LinksManager::addLink as linkManagerAddLink; + } const JSONAPI_VERSION_1_0 = '1.0'; const JSONAPI_VERSION_1_1 = '1.1'; @@ -98,11 +100,7 @@ public function __construct() { */ public function addLink($key, $href, array $meta=[], $level=Document::LEVEL_ROOT) { if ($level === Document::LEVEL_ROOT) { - if ($this->links === null) { - $this->setLinksObject(new LinksObject()); - } - - $this->links->add($key, $href, $meta); + $this->linkManagerAddLink($key, $href, $meta); } elseif ($level === Document::LEVEL_JSONAPI) { throw new InputException('level "jsonapi" can not be used for links'); From 30e6fb359ac24d618217ec9e165a4daa23ac28ce Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sun, 22 Aug 2021 22:51:32 +0200 Subject: [PATCH 40/42] test extentions link for atomic operations in self link --- tests/extensions/AtomicOperationsDocumentTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/extensions/AtomicOperationsDocumentTest.php b/tests/extensions/AtomicOperationsDocumentTest.php index 523ca22..e1bac4f 100644 --- a/tests/extensions/AtomicOperationsDocumentTest.php +++ b/tests/extensions/AtomicOperationsDocumentTest.php @@ -21,6 +21,7 @@ public function testSetResults_HappyPath() { $resource2->add('name', 'Arthur'); $resource3->add('name', 'Zaphod'); $document->addResults($resource1, $resource2, $resource3); + $document->setSelfLink('https://example.org/operations'); $array = $document->toArray(); @@ -29,6 +30,13 @@ public function testSetResults_HappyPath() { $this->assertCount(1, $array['jsonapi']['ext']); $this->assertSame((new AtomicOperationsExtension())->getOfficialLink(), $array['jsonapi']['ext'][0]); + $this->assertArrayHasKey('links', $array); + $this->assertArrayHasKey('self', $array['links']); + $this->assertArrayHasKey('href', $array['links']['self']); + $this->assertArrayHasKey('type', $array['links']['self']); + $this->assertSame('https://example.org/operations', $array['links']['self']['href']); + $this->assertSame('application/vnd.api+json; ext="'.(new AtomicOperationsExtension())->getOfficialLink().'"', $array['links']['self']['type']); + $this->assertArrayHasKey('atomic:results', $array); $this->assertCount(3, $array['atomic:results']); $this->assertSame(['data' => $resource1->toArray()], $array['atomic:results'][0]); From bd98dd07a7f626f2893325ed58359da681d7ad4c Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sun, 22 Aug 2021 23:03:50 +0200 Subject: [PATCH 41/42] make it easy to set localids on resource documents as well --- src/ResourceDocument.php | 7 +++++++ tests/ResourceDocumentTest.php | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/ResourceDocument.php b/src/ResourceDocument.php index a94c16e..8db9d09 100644 --- a/src/ResourceDocument.php +++ b/src/ResourceDocument.php @@ -179,6 +179,13 @@ public function setId($id) { $this->resource->setId($id); } + /** + * @param string|int $localId will be casted to a string + */ + public function setLocalId($localId) { + $this->resource->setLocalId($localId); + } + /** * @param AttributesObject $attributesObject * @param array $options optional {@see ResourceObject::$defaults} diff --git a/tests/ResourceDocumentTest.php b/tests/ResourceDocumentTest.php index 8c352f2..36bcf7e 100644 --- a/tests/ResourceDocumentTest.php +++ b/tests/ResourceDocumentTest.php @@ -132,6 +132,19 @@ public function testAddMeta_RecreateJsonapiObject() { $this->assertSame('jsonapi', $array['jsonapi']['meta']['baz']); } + public function testSetLocalId_HappyPath() { + $document = new ResourceDocument(); + $document->setType('user'); + $document->setLocalId('42'); + + $array = $document->toArray(); + + $this->assertArrayHasKey('data', $array); + $this->assertArrayHasKey('lid', $array['data']); + $this->assertArrayNotHasKey('id', $array['data']); + $this->assertSame('42', $array['data']['lid']); + } + public function testAddRelationshipObject_WithIncluded() { $resourceObject = new ResourceObject('user', 42); $resourceObject->add('foo', 'bar'); From b08b5e3500d7b5d9e6811ebafc99d5d9e8f4fb15 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sun, 22 Aug 2021 23:42:55 +0200 Subject: [PATCH 42/42] make it easy to fetch local ids from request documents --- src/helpers/RequestParser.php | 14 ++++++++++++++ tests/helpers/RequestParserTest.php | 25 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/helpers/RequestParser.php b/src/helpers/RequestParser.php index 5ae8f25..5209fda 100644 --- a/src/helpers/RequestParser.php +++ b/src/helpers/RequestParser.php @@ -248,6 +248,20 @@ public function getFilter() { return $this->queryParameters['filter']; } + /** + * @return boolean + */ + public function hasLocalId() { + return (isset($this->document['data']['lid'])); + } + + /** + * @return string + */ + public function getLocalId() { + return $this->document['data']['lid']; + } + /** * @param string $attributeName * @return boolean diff --git a/tests/helpers/RequestParserTest.php b/tests/helpers/RequestParserTest.php index 6410c78..fd17061 100644 --- a/tests/helpers/RequestParserTest.php +++ b/tests/helpers/RequestParserTest.php @@ -336,6 +336,31 @@ public function testGetFilter() { $this->assertSame(['foo' => 'bar'], $requestParser->getFilter()); } + public function testHasLocalId() { + $document = [ + 'data' => [ + 'id' => 'foo', + ], + ]; + $requestParser = new RequestParser($selfLink='', $quaryParameters=[], $document); + + $this->assertArrayHasKey('data', $requestParser->getDocument()); + $this->assertArrayHasKey('id', $requestParser->getDocument()['data']); + $this->assertFalse($requestParser->hasLocalId()); + + $document = [ + 'data' => [ + 'lid' => 'foo', + ], + ]; + $requestParser = new RequestParser($selfLink='', $quaryParameters=[], $document); + + $this->assertArrayHasKey('data', $requestParser->getDocument()); + $this->assertArrayNotHasKey('id', $requestParser->getDocument()['data']); + $this->assertTrue($requestParser->hasLocalId()); + $this->assertSame('foo', $requestParser->getLocalId()); + } + public function testHasAttribute() { $requestParser = new RequestParser(); $this->assertFalse($requestParser->hasAttribute('foo'));