From 17eea59fe6a0187bfd5701e79d26ab648d937ac0 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Mon, 5 Apr 2021 11:06:06 +0200 Subject: [PATCH 01/16] 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 02/16] 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 03/16] 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 04/16] 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 05/16] 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 06/16] 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 07/16] 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 08/16] 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 09/16] 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 10/16] 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 11/16] 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 12/16] 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 214a51724dcfe3af1251f64c621af5c21b4fdf8d Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Mon, 5 Apr 2021 23:50:19 +0200 Subject: [PATCH 13/16] 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 14/16] 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 51a9d275e2999a7327c586dda6e6ef2ef6e394fc Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sat, 15 May 2021 12:58:33 +0200 Subject: [PATCH 15/16] 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 16/16] 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();