diff --git a/README.md b/README.md index c1e636b..21dc5bb 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,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 Also there's tools to help processing of incoming requests: @@ -189,9 +189,10 @@ Also there's tools to help processing of incoming requests: - parse request options (include paths, sparse fieldsets, sort fields, pagination, filtering) - parse request documents for creating, updating and deleting resources and relationships -Next to custom extensions, the following [official extensions](https://jsonapi.org/extensions/) are included: +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/examples/bootstrap_examples.php b/examples/bootstrap_examples.php
index 40bc6cd..fc4793d 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 getOfficialLink() {
- return 'https://jsonapi.org/format/1.1/#profile-keywords';
+ return 'https://jsonapi.org/format/1.1/#extension-rules';
+ }
+
+ public function getNamespace() {
+ return 'version';
}
/**
- * 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(\DateTime::ISO8601);
}
+ if ($updated !== null) {
+ $timestamps['updated'] = $updated->format(\DateTime::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/index.html b/examples/index.html
index 8c1963c..1687b28 100644
--- a/examples/index.html
+++ b/examples/index.html
@@ -49,7 +49,9 @@ Misc
Content-Type: '.$contentType.''.PHP_EOL;
+
+$options = [
+ 'prettyPrint' => true,
+];
+echo ''.$document->toJson($options);
diff --git a/src/Document.php b/src/Document.php
index 5085e2f..cafa441 100644
--- a/src/Document.php
+++ b/src/Document.php
@@ -2,14 +2,17 @@
namespace alsvanzelf\jsonapi;
+use alsvanzelf\jsonapi\exceptions\DuplicateException;
use alsvanzelf\jsonapi\exceptions\Exception;
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\DocumentInterface;
+use alsvanzelf\jsonapi\interfaces\ExtensionInterface;
use alsvanzelf\jsonapi\interfaces\ProfileInterface;
use alsvanzelf\jsonapi\objects\JsonapiObject;
use alsvanzelf\jsonapi\objects\LinksObject;
@@ -19,7 +22,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';
@@ -37,6 +40,8 @@ abstract class Document implements DocumentInterface, \JsonSerializable {
protected $meta;
/** @var JsonapiObject */
protected $jsonapi;
+ /** @var ExtensionInterface[] */
+ protected $extensions = [];
/** @var ProfileInterface[] */
protected $profiles = [];
/** @var array */
@@ -190,6 +195,36 @@ 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
+ *
+ * @throws Exception if namespace uses illegal characters
+ * @throws DuplicateException if namespace conflicts with another applied extension
+ */
+ public function applyExtension(ExtensionInterface $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);
+ }
+ }
+
/**
* apply a profile which adds the link and sets a correct content-type
*
@@ -197,18 +232,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());
}
/**
@@ -219,7 +252,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();
@@ -273,7 +313,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/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 @@
+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/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/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 @@
+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 ad04da2..980b0b2 100644
--- a/src/objects/JsonapiObject.php
+++ b/src/objects/JsonapiObject.php
@@ -4,14 +4,21 @@
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;
+ /** @var ExtensionInterface[] */
+ protected $extensions = [];
+ /** @var ProfileInterface */
+ protected $profiles = [];
/** @var MetaObject */
protected $meta;
@@ -51,6 +58,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,12 +90,21 @@ 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;
}
if ($this->hasAtMembers()) {
return false;
}
+ if ($this->hasExtensionMembers()) {
+ return false;
+ }
return true;
}
@@ -83,11 +113,29 @@ 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;
}
+ 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();
}
diff --git a/src/objects/LinkObject.php b/src/objects/LinkObject.php
index 837875e..244d103 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;
@@ -161,6 +162,9 @@ public function isEmpty() {
if ($this->hasAtMembers()) {
return false;
}
+ if ($this->hasExtensionMembers()) {
+ return false;
+ }
return true;
}
@@ -169,7 +173,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/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 */
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 a06c33f..05aaa6b 100644
--- a/src/objects/ResourceIdentifierObject.php
+++ b/src/objects/ResourceIdentifierObject.php
@@ -5,13 +5,14 @@
use alsvanzelf\jsonapi\exceptions\Exception;
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\ResourceInterface;
use alsvanzelf\jsonapi\objects\MetaObject;
class ResourceIdentifierObject implements ObjectInterface, ResourceInterface {
- use AtMemberManager;
+ use AtMemberManager, ExtensionMemberManager;
/** @var string */
protected $type;
@@ -191,6 +192,9 @@ public function isEmpty() {
if ($this->hasAtMembers()) {
return false;
}
+ if ($this->hasExtensionMembers()) {
+ return false;
+ }
return true;
}
@@ -199,7 +203,7 @@ public function isEmpty() {
* @inheritDoc
*/
public function toArray() {
- $array = $this->getAtMembers();
+ $array = [];
$array['type'] = $this->type;
@@ -210,6 +214,13 @@ public function toArray() {
$array['lid'] = $this->lid;
}
+ 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();
}
diff --git a/tests/ConverterTest.php b/tests/ConverterTest.php
index 85839c9..f5c7d51 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,63 @@ 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', [], []));
}
- public function testMergeProfilesInContentType_WithProfileStringLink() {
+ /**
+ * @group Extensions
+ */
+ public function testPrepareContentType_WithExtensionStringLink() {
+ $extension = new TestExtension();
+ $extension->setOfficialLink('bar');
+
+ $this->assertSame('foo; ext="bar"', Converter::prepareContentType('foo', [$extension], []));
+ }
+
+ /**
+ * @group Profiles
+ */
+ public function testPrepareContentType_WithProfileStringLink() {
$profile = new TestProfile();
$profile->setOfficialLink('bar');
- $this->assertSame('foo;profile="bar", foo', 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", foo', Converter::mergeProfilesInContentType('foo', [$profile1, $profile2]));
+ $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]));
}
}
diff --git a/tests/DocumentTest.php b/tests/DocumentTest.php
index d363b8e..30043cb 100644
--- a/tests/DocumentTest.php
+++ b/tests/DocumentTest.php
@@ -2,10 +2,12 @@
namespace alsvanzelf\jsonapiTests;
+use alsvanzelf\jsonapi\exceptions\DuplicateException;
use alsvanzelf\jsonapi\exceptions\Exception;
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;
@@ -229,6 +231,71 @@ 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 Extensions
+ */
+ 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);
+ }
+
+ /**
+ * @group Extensions
+ */
+ 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
+ */
public function testApplyProfile_HappyPath() {
$profile = new TestProfile();
$profile->setOfficialLink('https://jsonapi.org');
@@ -238,12 +305,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 d81453f..ec00ccd 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;
@@ -74,6 +75,39 @@ public function testSendResponse_ContentTypeHeader() {
/**
* @runInSeparateProcess
+ * @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');
+
+ $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->setNamespace('two');
+ $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() {
if (extension_loaded('xdebug') === false) {
@@ -89,7 +123,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');
@@ -98,7 +132,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/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(\DateTime::ISO8601);
+ }
+ if ($updated !== null) {
+ $timestamps['updated'] = $updated->format(\DateTime::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/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
deleted file mode 100644
index 53daa9f..0000000
--- a/tests/example_output/example_profile/example_profile.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "jsonapi": {
- "version": "1.1"
- },
- "links": {
- "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..49d5808
--- /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": "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
new file mode 100644
index 0000000..16deba8
--- /dev/null
+++ b/tests/example_output/profile/profile.php
@@ -0,0 +1,19 @@
+applyProfile($profile);
+
+ $profile->setTimestamps($document, new \DateTime('2019-01-01T00:00:00+0000'), new \DateTime('2021-01-01T00:00:00+0000'));
+
+ return $document;
+ }
+}
diff --git a/tests/extensions/AtomicOperationsDocumentTest.php b/tests/extensions/AtomicOperationsDocumentTest.php
new file mode 100644
index 0000000..523ca22
--- /dev/null
+++ b/tests/extensions/AtomicOperationsDocumentTest.php
@@ -0,0 +1,51 @@
+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']);
+ }
+}
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;
+ }
+}
diff --git a/tests/helpers/ExtensionMemberManagerTest.php b/tests/helpers/ExtensionMemberManagerTest.php
new file mode 100644
index 0000000..92616bb
--- /dev/null
+++ b/tests/helpers/ExtensionMemberManagerTest.php
@@ -0,0 +1,70 @@
+setNamespace('test');
+
+ $this->assertFalse($helper->hasExtensionMembers());
+ $this->assertSame([], $helper->getExtensionMembers());
+
+ $helper->addExtensionMember($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();
+ $extension = new TestExtension();
+ $extension->setNamespace('test');
+
+ $helper->addExtensionMember($extension, 'test:foo', 'bar');
+
+ $array = $helper->getExtensionMembers();
+
+ $this->assertArrayHasKey('test:foo', $array);
+ }
+
+ public function testAddExtensionMember_WithObjectValue() {
+ $helper = new ExtensionMemberManager();
+ $extension = new TestExtension();
+ $extension->setNamespace('test');
+
+ $object = new \stdClass();
+ $object->bar = 'baz';
+
+ $helper->addExtensionMember($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();
+ $extension = new TestExtension();
+ $extension->setNamespace('test');
+
+ $this->expectException(InputException::class);
+
+ $helper->addExtensionMember($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());
+ }
+
+ /**
+ * @group Extensions
+ */
+ 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..882c708 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,43 @@ public function testIsEmpty_WithAtMembers() {
$this->assertFalse($jsonapiObject->isEmpty());
}
+
+ /**
+ * @group Extensions
+ */
+ public function testIsEmpty_WithExtensionLink() {
+ $jsonapiObject = new JsonapiObject($version=null);
+
+ $this->assertTrue($jsonapiObject->isEmpty());
+
+ $jsonapiObject->addExtension(new TestExtension());
+
+ $this->assertFalse($jsonapiObject->isEmpty());
+ }
+
+ /**
+ * @group Profiles
+ */
+ public function testIsEmpty_WithProfileLink() {
+ $jsonapiObject = new JsonapiObject($version=null);
+
+ $this->assertTrue($jsonapiObject->isEmpty());
+
+ $jsonapiObject->addProfile(new TestProfile());
+
+ $this->assertFalse($jsonapiObject->isEmpty());
+ }
+
+ /**
+ * @group Extensions
+ */
+ 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 ad41068..fd356c9 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 {
@@ -151,4 +152,17 @@ public function testIsEmpty_WithAtMembers() {
$this->assertFalse($linkObject->isEmpty());
}
+
+ /**
+ * @group Extensions
+ */
+ 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..fc4cb6a 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,27 @@ 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());
+ }
+
+ /**
+ * @group Extensions
+ */
+ 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..e5a99fb 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,19 @@ public function testIsEmpty_WithAtMembers() {
$this->assertFalse($relationshipObject->isEmpty());
}
+ /**
+ * @group Extensions
+ */
+ 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 edee0bd..b7eb1ea 100644
--- a/tests/objects/ResourceIdentifierObjectTest.php
+++ b/tests/objects/ResourceIdentifierObjectTest.php
@@ -5,6 +5,7 @@
use alsvanzelf\jsonapi\exceptions\Exception;
use alsvanzelf\jsonapi\exceptions\DuplicateException;
use alsvanzelf\jsonapi\objects\ResourceIdentifierObject;
+use alsvanzelf\jsonapiTests\extensions\TestExtension;
use PHPUnit\Framework\TestCase;
class ResourceIdentifierObjectTest extends TestCase {
@@ -174,4 +175,17 @@ public function testIsEmpty_WithAtMembers() {
$this->assertFalse($resourceIdentifierObject->isEmpty());
}
+
+ /**
+ * @group Extensions
+ */
+ public function testIsEmpty_WithExtensionMembers() {
+ $resourceIdentifierObject = new ResourceIdentifierObject();
+
+ $this->assertTrue($resourceIdentifierObject->isEmpty());
+
+ $resourceIdentifierObject->addExtensionMember(new TestExtension(), 'foo', 'bar');
+
+ $this->assertFalse($resourceIdentifierObject->isEmpty());
+ }
}
diff --git a/tests/profiles/CursorPaginationProfileTest.php b/tests/profiles/CursorPaginationProfileTest.php
index cd40ec4..4fb3db8 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']);