diff --git a/src/Document.php b/src/Document.php index fdec9ce..a102132 100644 --- a/src/Document.php +++ b/src/Document.php @@ -15,7 +15,13 @@ use JsonApiPhp\JsonApi\Document\LinksTrait; use JsonApiPhp\JsonApi\Document\Meta; use JsonApiPhp\JsonApi\Document\MetaTrait; -use JsonApiPhp\JsonApi\Document\Resource\ResourceInterface; +use JsonApiPhp\JsonApi\Document\PrimaryData\MultiIdentifierData; +use JsonApiPhp\JsonApi\Document\PrimaryData\MultiResourceData; +use JsonApiPhp\JsonApi\Document\PrimaryData\NullData; +use JsonApiPhp\JsonApi\Document\PrimaryData\PrimaryDataInterface; +use JsonApiPhp\JsonApi\Document\PrimaryData\SingleIdentifierData; +use JsonApiPhp\JsonApi\Document\PrimaryData\SingleResourceData; +use JsonApiPhp\JsonApi\Document\Resource\ResourceIdentifier; use JsonApiPhp\JsonApi\Document\Resource\ResourceObject; class Document implements \JsonSerializable @@ -26,11 +32,23 @@ class Document implements \JsonSerializable use LinksTrait; use MetaTrait; + /** + * @var PrimaryDataInterface + */ private $data; + + /** + * @var Error[] + */ private $errors; + private $api; + + /** + * @var ResourceObject[] + */ private $included; - private $is_sparse = false; + private $sparse = false; private function __construct() { @@ -50,17 +68,38 @@ public static function fromErrors(Error ...$errors): self return $doc; } - public static function fromResource(ResourceInterface $data): self + public static function fromResource(ResourceObject $resource): self + { + $doc = new self; + $doc->data = new SingleResourceData($resource); + return $doc; + } + + public static function fromResources(ResourceObject ...$resources): self + { + $doc = new self; + $doc->data = new MultiResourceData(...$resources); + return $doc; + } + + public static function fromIdentifier(ResourceIdentifier $identifier) + { + $doc = new self; + $doc->data = new SingleIdentifierData($identifier); + return $doc; + } + + public static function fromIdentifiers(ResourceIdentifier... $identifiers) { $doc = new self; - $doc->data = $data; + $doc->data = new MultiIdentifierData(...$identifiers); return $doc; } - public static function fromResources(ResourceInterface ...$data): self + public static function nullDocument() { $doc = new self; - $doc->data = $data; + $doc->data = new NullData(); return $doc; } @@ -69,19 +108,27 @@ public function setApiVersion(string $version = self::DEFAULT_API_VERSION) $this->api['version'] = $version; } - public function setApiMeta(array $meta) + public function setApiMeta(Meta $meta) { $this->api['meta'] = $meta; } - public function setIncluded(ResourceObject ...$included) + public function setIncluded(ResourceObject ...$resources) { - $this->included = $included; + if (null === $this->data) { + throw new \LogicException('Document with no data cannot contain included resources'); + } + foreach ($resources as $resource) { + if (isset($this->included[(string) $resource->toIdentifier()])) { + throw new \LogicException("Resource {$resource->toIdentifier()} is already included"); + } + $this->included[(string) $resource->toIdentifier()] = $resource; + } } public function markSparse() { - $this->is_sparse = true; + $this->sparse = true; } public function jsonSerialize() @@ -94,7 +141,7 @@ public function jsonSerialize() 'meta' => $this->meta, 'jsonapi' => $this->api, 'links' => $this->links, - 'included' => $this->included, + 'included' => $this->included ? array_values($this->included) : null, ], function ($v) { return null !== $v; @@ -104,47 +151,19 @@ function ($v) { private function enforceFullLinkage() { - if ($this->is_sparse || empty($this->included)) { + if ($this->sparse || empty($this->included)) { return; } - foreach ($this->included as $included_resource) { - if ($this->hasLinkTo($included_resource) || $this->anotherIncludedResourceIdentifies($included_resource)) { + foreach ($this->included as $included) { + if ($this->data->hasLinkTo($included)) { continue; } - throw new \LogicException("Full linkage is required for $included_resource"); - } - } - - private function anotherIncludedResourceIdentifies(ResourceObject $resource): bool - { - /** @var ResourceObject $included_resource */ - foreach ($this->included as $included_resource) { - if ($included_resource !== $resource && $included_resource->identifies($resource)) { - return true; - } - } - return false; - } - - private function hasLinkTo(ResourceObject $resource): bool - { - /** @var ResourceInterface $my_resource */ - foreach ($this->toResources() as $my_resource) { - if ($my_resource->identifies($resource)) { - return true; - } - } - return false; - } - - private function toResources(): \Iterator - { - if ($this->data instanceof ResourceInterface) { - yield $this->data; - } elseif (is_array($this->data)) { - foreach ($this->data as $datum) { - yield $datum; + foreach ($this->included as $anotherIncluded) { + if ($anotherIncluded->identifies($included)) { + continue 2; + } } + throw new \LogicException("Full linkage is required for {$included->toIdentifier()}"); } } } diff --git a/src/Document/PrimaryData/MultiIdentifierData.php b/src/Document/PrimaryData/MultiIdentifierData.php new file mode 100644 index 0000000..93d7c1a --- /dev/null +++ b/src/Document/PrimaryData/MultiIdentifierData.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace JsonApiPhp\JsonApi\Document\PrimaryData; + +use JsonApiPhp\JsonApi\Document\Resource\ResourceIdentifier; +use JsonApiPhp\JsonApi\Document\Resource\ResourceObject; + +final class MultiIdentifierData implements PrimaryDataInterface +{ + private $identifiers; + + public function __construct(ResourceIdentifier ...$identifiers) + { + $this->identifiers = $identifiers; + } + + public function hasLinkTo(ResourceObject $resource): bool + { + foreach ($this->identifiers as $identifier) { + if ($identifier->identifies($resource)) { + return true; + } + } + return false; + } + + public function jsonSerialize() + { + return $this->identifiers; + } +} diff --git a/src/Document/PrimaryData/MultiResourceData.php b/src/Document/PrimaryData/MultiResourceData.php new file mode 100644 index 0000000..0e63fd8 --- /dev/null +++ b/src/Document/PrimaryData/MultiResourceData.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace JsonApiPhp\JsonApi\Document\PrimaryData; + +use JsonApiPhp\JsonApi\Document\Resource\ResourceObject; + +final class MultiResourceData implements PrimaryDataInterface +{ + private $resources; + + public function __construct(ResourceObject ...$resources) + { + $this->resources = $resources; + } + + public function hasLinkTo(ResourceObject $resource): bool + { + foreach ($this->resources as $myResource) { + if ($myResource->identifies($resource)) { + return true; + } + } + return false; + } + + public function jsonSerialize() + { + return $this->resources; + } +} diff --git a/src/Document/PrimaryData/NullData.php b/src/Document/PrimaryData/NullData.php new file mode 100644 index 0000000..68882da --- /dev/null +++ b/src/Document/PrimaryData/NullData.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace JsonApiPhp\JsonApi\Document\PrimaryData; + +use JsonApiPhp\JsonApi\Document\Resource\ResourceObject; + +final class NullData implements PrimaryDataInterface +{ + public function hasLinkTo(ResourceObject $resource): bool + { + return false; + } + + public function jsonSerialize() + { + return null; + } +} diff --git a/src/Document/PrimaryData/PrimaryDataInterface.php b/src/Document/PrimaryData/PrimaryDataInterface.php new file mode 100644 index 0000000..89967c5 --- /dev/null +++ b/src/Document/PrimaryData/PrimaryDataInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace JsonApiPhp\JsonApi\Document\PrimaryData; + +use JsonApiPhp\JsonApi\Document\Resource\ResourceObject; + +interface PrimaryDataInterface extends \JsonSerializable +{ + public function hasLinkTo(ResourceObject $resource): bool; + + public function jsonSerialize(); +} diff --git a/src/Document/PrimaryData/SingleIdentifierData.php b/src/Document/PrimaryData/SingleIdentifierData.php new file mode 100644 index 0000000..9f8959b --- /dev/null +++ b/src/Document/PrimaryData/SingleIdentifierData.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace JsonApiPhp\JsonApi\Document\PrimaryData; + +use JsonApiPhp\JsonApi\Document\Resource\ResourceIdentifier; +use JsonApiPhp\JsonApi\Document\Resource\ResourceObject; + +final class SingleIdentifierData implements PrimaryDataInterface +{ + private $identifier; + + public function __construct(ResourceIdentifier $identifier) + { + $this->identifier = $identifier; + } + + public function hasLinkTo(ResourceObject $resource): bool + { + return $this->identifier->identifies($resource); + } + + public function jsonSerialize() + { + return $this->identifier; + } +} diff --git a/src/Document/PrimaryData/SingleResourceData.php b/src/Document/PrimaryData/SingleResourceData.php new file mode 100644 index 0000000..1ff8446 --- /dev/null +++ b/src/Document/PrimaryData/SingleResourceData.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace JsonApiPhp\JsonApi\Document\PrimaryData; + +use JsonApiPhp\JsonApi\Document\Resource\ResourceObject; + +final class SingleResourceData implements PrimaryDataInterface +{ + private $resource; + + public function __construct(ResourceObject $resource) + { + $this->resource = $resource; + } + + public function hasLinkTo(ResourceObject $resource): bool + { + return $this->resource->identifies($resource); + } + + public function jsonSerialize() + { + return $this->resource; + } +} diff --git a/src/Document/Resource/Linkage/LinkageInterface.php b/src/Document/Resource/Linkage/LinkageInterface.php new file mode 100644 index 0000000..7340c67 --- /dev/null +++ b/src/Document/Resource/Linkage/LinkageInterface.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace JsonApiPhp\JsonApi\Document\Resource\Linkage; + +use JsonApiPhp\JsonApi\Document\Resource\ResourceObject; + +interface LinkageInterface extends \JsonSerializable +{ + public function isLinkedTo(ResourceObject $resource): bool; + + public function jsonSerialize(); +} diff --git a/src/Document/Resource/Linkage/MultiLinkage.php b/src/Document/Resource/Linkage/MultiLinkage.php new file mode 100644 index 0000000..b732335 --- /dev/null +++ b/src/Document/Resource/Linkage/MultiLinkage.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace JsonApiPhp\JsonApi\Document\Resource\Linkage; + +use JsonApiPhp\JsonApi\Document\Resource\ResourceIdentifier; +use JsonApiPhp\JsonApi\Document\Resource\ResourceObject; + +final class MultiLinkage implements LinkageInterface +{ + private $identifiers; + + public function __construct(ResourceIdentifier ...$identifiers) + { + $this->identifiers = $identifiers; + } + + public function isLinkedTo(ResourceObject $resource): bool + { + foreach ($this->identifiers as $identifier) { + if ($identifier->identifies($resource)) { + return true; + } + } + return false; + } + + public function jsonSerialize() + { + return $this->identifiers; + } +} diff --git a/src/Document/Resource/NullResource.php b/src/Document/Resource/Linkage/NullLinkage.php similarity index 56% rename from src/Document/Resource/NullResource.php rename to src/Document/Resource/Linkage/NullLinkage.php index f30976d..e6fde43 100644 --- a/src/Document/Resource/NullResource.php +++ b/src/Document/Resource/Linkage/NullLinkage.php @@ -9,25 +9,19 @@ */ declare(strict_types=1); -namespace JsonApiPhp\JsonApi\Document\Resource; +namespace JsonApiPhp\JsonApi\Document\Resource\Linkage; -final class NullResource implements ResourceInterface -{ - public function jsonSerialize() - { - return null; - } +use JsonApiPhp\JsonApi\Document\Resource\ResourceObject; - public function identifies(ResourceInterface $resource): bool +final class NullLinkage implements LinkageInterface +{ + public function isLinkedTo(ResourceObject $resource): bool { return false; } - /** - * @deprecated to be removed in 1.0 - */ - public function __toString(): string + public function jsonSerialize() { - return 'null'; + return null; } } diff --git a/src/Document/Resource/Linkage/SingleLinkage.php b/src/Document/Resource/Linkage/SingleLinkage.php new file mode 100644 index 0000000..1ed15ec --- /dev/null +++ b/src/Document/Resource/Linkage/SingleLinkage.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +declare(strict_types=1); + +namespace JsonApiPhp\JsonApi\Document\Resource\Linkage; + +use JsonApiPhp\JsonApi\Document\Resource\ResourceIdentifier; +use JsonApiPhp\JsonApi\Document\Resource\ResourceObject; + +final class SingleLinkage implements LinkageInterface +{ + private $identifier; + + public function __construct(ResourceIdentifier $identifier) + { + $this->identifier = $identifier; + } + + public function isLinkedTo(ResourceObject $resource): bool + { + return $this->identifier->identifies($resource); + } + + public function jsonSerialize() + { + return $this->identifier; + } +} diff --git a/src/Document/Resource/Relationship/Linkage.php b/src/Document/Resource/Relationship/Linkage.php deleted file mode 100644 index 54c5b76..0000000 --- a/src/Document/Resource/Relationship/Linkage.php +++ /dev/null @@ -1,76 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace JsonApiPhp\JsonApi\Document\Resource\Relationship; - -use JsonApiPhp\JsonApi\Document\Resource\ResourceIdentifier; -use JsonApiPhp\JsonApi\Document\Resource\ResourceInterface; - -final class Linkage implements \JsonSerializable -{ - private $data; - - private function __construct() - { - } - - public static function nullLinkage(): self - { - return new self; - } - - public static function emptyArrayLinkage(): self - { - $linkage = new self; - $linkage->data = []; - return $linkage; - } - - public static function fromSingleIdentifier(ResourceIdentifier $data): self - { - $linkage = new self; - $linkage->data = $data; - return $linkage; - } - - public static function fromManyIdentifiers(ResourceIdentifier ...$data): self - { - $linkage = new self; - $linkage->data = $data; - return $linkage; - } - - public function isLinkedTo(ResourceInterface $resource): bool - { - foreach ($this->toLinkages() as $linkage) { - if ($linkage->identifies($resource)) { - return true; - } - } - return false; - } - - private function toLinkages(): \Generator - { - if ($this->data instanceof ResourceIdentifier) { - yield $this->data; - } elseif (is_array($this->data)) { - foreach ($this->data as $resource) { - yield $resource; - } - } - } - - public function jsonSerialize() - { - return $this->data; - } -} diff --git a/src/Document/Resource/Relationship/Relationship.php b/src/Document/Resource/Relationship/Relationship.php index 84b47e6..6c151db 100644 --- a/src/Document/Resource/Relationship/Relationship.php +++ b/src/Document/Resource/Relationship/Relationship.php @@ -14,7 +14,8 @@ use JsonApiPhp\JsonApi\Document\LinksTrait; use JsonApiPhp\JsonApi\Document\Meta; use JsonApiPhp\JsonApi\Document\MetaTrait; -use JsonApiPhp\JsonApi\Document\Resource\ResourceInterface; +use JsonApiPhp\JsonApi\Document\Resource\Linkage\LinkageInterface; +use JsonApiPhp\JsonApi\Document\Resource\ResourceObject; final class Relationship implements \JsonSerializable { @@ -22,7 +23,7 @@ final class Relationship implements \JsonSerializable use MetaTrait; /** - * @var Linkage + * @var LinkageInterface */ private $linkage = null; @@ -51,14 +52,14 @@ public static function fromRelatedLink(string $link, array $meta = null): self return $r; } - public static function fromLinkage(Linkage $linkage): self + public static function fromLinkage(LinkageInterface $linkage): self { $r = new self; $r->linkage = $linkage; return $r; } - public function hasLinkageTo(ResourceInterface $resource): bool + public function hasLinkageTo(ResourceObject $resource): bool { return ($this->linkage && $this->linkage->isLinkedTo($resource)); } diff --git a/src/Document/Resource/ResourceIdentifier.php b/src/Document/Resource/ResourceIdentifier.php index 9d28075..3b67bd8 100644 --- a/src/Document/Resource/ResourceIdentifier.php +++ b/src/Document/Resource/ResourceIdentifier.php @@ -12,22 +12,18 @@ namespace JsonApiPhp\JsonApi\Document\Resource; use JsonApiPhp\JsonApi\Document\Meta; -use JsonApiPhp\JsonApi\Document\MetaTrait; -class ResourceIdentifier implements ResourceInterface +class ResourceIdentifier implements \JsonSerializable { - use MetaTrait; + private $type; + private $id; + private $meta; - protected $type; - protected $id; - - public function __construct(string $type, string $id = null, Meta $meta = null) + public function __construct(string $type, string $id, Meta $meta = null) { $this->type = $type; $this->id = $id; - if ($meta) { - $this->setMeta($meta); - } + $this->meta = $meta; } public function jsonSerialize() @@ -44,19 +40,18 @@ function ($v) { ); } - /** - * @deprecated to be removed in 1.0 - */ + public function identifies(ResourceObject $resource): bool + { + return $resource->toIdentifier()->equals($this); + } + public function __toString(): string { - return sprintf("%s:%s", $this->type, $this->id ?: 'null'); + return "$this->type:$this->id"; } - public function identifies(ResourceInterface $resource): bool + private function equals(ResourceIdentifier $that) { - return $resource instanceof self - && $this->type === $resource->type - && $this->id !== null - && $this->id === $resource->id; + return $this->type === $that->type && $this->id === $that->id; } } diff --git a/src/Document/Resource/ResourceInterface.php b/src/Document/Resource/ResourceInterface.php deleted file mode 100644 index d8b558d..0000000 --- a/src/Document/Resource/ResourceInterface.php +++ /dev/null @@ -1,17 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace JsonApiPhp\JsonApi\Document\Resource; - -interface ResourceInterface extends \JsonSerializable -{ - public function identifies(ResourceInterface $resource): bool; -} diff --git a/src/Document/Resource/ResourceObject.php b/src/Document/Resource/ResourceObject.php index ad21ba0..9276a84 100644 --- a/src/Document/Resource/ResourceObject.php +++ b/src/Document/Resource/ResourceObject.php @@ -12,15 +12,34 @@ namespace JsonApiPhp\JsonApi\Document\Resource; use JsonApiPhp\JsonApi\Document\LinksTrait; +use JsonApiPhp\JsonApi\Document\Meta; use JsonApiPhp\JsonApi\Document\Resource\Relationship\Relationship; -class ResourceObject extends ResourceIdentifier +class ResourceObject implements \JsonSerializable { use LinksTrait; + private $type; + private $id; + private $meta; private $attributes; + + /** + * @var Relationship[] + */ private $relationships; + public function __construct(string $type, string $id = null) + { + $this->type = $type; + $this->id = $id; + } + + public function setMeta(Meta $meta) + { + $this->meta = $meta; + } + public function setAttribute(string $name, $value) { if ($this->isReservedName($name)) { @@ -43,7 +62,7 @@ public function setRelationship(string $name, Relationship $relationship) $this->relationships[$name] = $relationship; } - public function toId(): ResourceIdentifier + public function toIdentifier(): ResourceIdentifier { return new ResourceIdentifier($this->type, $this->id); } @@ -65,10 +84,9 @@ function ($v) { ); } - public function identifies(ResourceInterface $resource): bool + public function identifies(ResourceObject $resource): bool { if ($this->relationships) { - /** @var Relationship $relationship */ foreach ($this->relationships as $relationship) { if ($relationship->hasLinkageTo($resource)) { return true; @@ -78,10 +96,6 @@ public function identifies(ResourceInterface $resource): bool return false; } - /** - * @param string $name - * @return bool - */ private function isReservedName(string $name): bool { return in_array($name, ['id', 'type']); diff --git a/test/BaseTestCase.php b/test/BaseTestCase.php index cc7a5a4..3c30197 100644 --- a/test/BaseTestCase.php +++ b/test/BaseTestCase.php @@ -17,6 +17,10 @@ abstract class BaseTestCase extends TestCase { public static function assertEncodesTo(string $expected, $obj, string $message = '') { - self::assertEquals(json_encode(json_decode($expected)), json_encode($obj), $message); + self::assertEquals( + json_decode($expected), + json_decode(json_encode($obj, JSON_UNESCAPED_SLASHES)), + $message + ); } } diff --git a/test/Document/CompoundDocumentTest.php b/test/Document/CompoundDocumentTest.php index 2b3ef27..1c6794a 100644 --- a/test/Document/CompoundDocumentTest.php +++ b/test/Document/CompoundDocumentTest.php @@ -12,9 +12,11 @@ namespace JsonApiPhp\JsonApi\Test\Document; use JsonApiPhp\JsonApi\Document; -use JsonApiPhp\JsonApi\Document\Resource\NullResource; -use JsonApiPhp\JsonApi\Document\Resource\Relationship\Linkage; +use JsonApiPhp\JsonApi\Document\Meta; +use JsonApiPhp\JsonApi\Document\Resource\Linkage\MultiLinkage; +use JsonApiPhp\JsonApi\Document\Resource\Linkage\SingleLinkage; use JsonApiPhp\JsonApi\Document\Resource\Relationship\Relationship; +use JsonApiPhp\JsonApi\Document\Resource\ResourceIdentifier; use JsonApiPhp\JsonApi\Document\Resource\ResourceObject; use JsonApiPhp\JsonApi\Test\BaseTestCase; @@ -41,65 +43,122 @@ class CompoundDocumentTest extends BaseTestCase { /** - * In a compound document, all included resources MUST be represented as an array of resource objects - * in a top-level included member. + * Let's begin with the example from the official docs @link http://jsonapi.org/format/#document-compound-documents */ - public function testIncludedResourcesRepresentedAsArray() + public function testOfficialDocsExample() { - $apple = new ResourceObject('apples', '1'); - $apple->setAttribute('color', 'red'); - $orange = new ResourceObject('oranges', '1'); - $orange->setAttribute('color', 'orange'); - $basket = new ResourceObject('baskets', '1'); - $basket->setRelationship( - 'fruits', - Relationship::fromLinkage( - Linkage::fromManyIdentifiers( - $apple->toId(), - $orange->toId() - ) - ) + $dan = new ResourceObject('people', '9'); + $dan->setAttribute('first-name', 'Dan'); + $dan->setAttribute('last-name', 'Gebhardt'); + $dan->setAttribute('twitter', 'dgeb'); + $dan->setLink('self', 'http://example.com/people/9'); + + $comment05 = new ResourceObject('comments', '5'); + $comment05->setAttribute('body', 'First!'); + $comment05->setLink('self', 'http://example.com/comments/5'); + $comment05->setRelationship( + 'author', + Relationship::fromLinkage(new SingleLinkage(new ResourceIdentifier('people', '2'))) ); - $doc = Document::fromResource($basket); - $doc->setIncluded($apple, $orange); + + $comment12 = new ResourceObject('comments', '12'); + $comment12->setAttribute('body', 'I like XML better'); + $comment12->setLink('self', 'http://example.com/comments/12'); + $comment12->setRelationship( + 'author', + Relationship::fromLinkage(new SingleLinkage($dan->toIdentifier())) + ); + + $author = Relationship::fromLinkage(new SingleLinkage($dan->toIdentifier())); + $author->setLink('self', 'http://example.com/articles/1/relationships/author'); + $author->setLink('related', 'http://example.com/articles/1/author'); + + $comments = Relationship::fromLinkage(new MultiLinkage($comment05->toIdentifier(), $comment12->toIdentifier())); + $comments->setLink('self', 'http://example.com/articles/1/relationships/comments'); + $comments->setLink('related', 'http://example.com/articles/1/comments'); + + $article = new ResourceObject('articles', '1'); + $article->setAttribute('title', 'JSON API paints my bikeshed!'); + $article->setLink('self', 'http://example.com/articles/1'); + $article->setRelationship('author', $author); + $article->setRelationship('comments', $comments); + + $doc = Document::fromResources($article); + $doc->setIncluded($dan, $comment05, $comment12); + $this->assertEncodesTo( ' { - "data": { - "type": "baskets", - "id": "1", - "relationships": { - "fruits": { - "data": [ - { - "type": "apples", - "id": "1" - }, - { - "type": "oranges", - "id": "1" - } - ] - } - } + "data": [{ + "type": "articles", + "id": "1", + "attributes": { + "title": "JSON API paints my bikeshed!" }, - "included": [ - { - "type": "apples", - "id": "1", - "attributes": { - "color": "red" - } + "links": { + "self": "http://example.com/articles/1" + }, + "relationships": { + "author": { + "links": { + "self": "http://example.com/articles/1/relationships/author", + "related": "http://example.com/articles/1/author" }, - { - "type": "oranges", - "id": "1", - "attributes": { - "color": "orange" - } - } - ] - } + "data": { "type": "people", "id": "9" } + }, + "comments": { + "links": { + "self": "http://example.com/articles/1/relationships/comments", + "related": "http://example.com/articles/1/comments" + }, + "data": [ + { "type": "comments", "id": "5" }, + { "type": "comments", "id": "12" } + ] + } + } + }], + "included": [{ + "type": "people", + "id": "9", + "attributes": { + "first-name": "Dan", + "last-name": "Gebhardt", + "twitter": "dgeb" + }, + "links": { + "self": "http://example.com/people/9" + } + }, { + "type": "comments", + "id": "5", + "attributes": { + "body": "First!" + }, + "relationships": { + "author": { + "data": { "type": "people", "id": "2" } + } + }, + "links": { + "self": "http://example.com/comments/5" + } + }, { + "type": "comments", + "id": "12", + "attributes": { + "body": "I like XML better" + }, + "relationships": { + "author": { + "data": { "type": "people", "id": "9" } + } + }, + "links": { + "self": "http://example.com/comments/12" + } + }] + } ', $doc ); @@ -111,7 +170,7 @@ public function testIncludedResourcesRepresentedAsArray() */ public function testFullLinkageIsRequired() { - $doc = Document::fromResource(new NullResource); + $doc = Document::nullDocument(); $doc->setIncluded(new ResourceObject('apples', '1')); json_encode($doc); } @@ -121,7 +180,7 @@ public function testFullLinkageIsRequired() */ public function testFullLinkageIsNotRequiredIfSparse() { - $doc = Document::fromResource(new NullResource); + $doc = Document::nullDocument(); $doc->markSparse(); $doc->setIncluded(new ResourceObject('apples', '1')); $this->assertEncodesTo( @@ -140,34 +199,85 @@ public function testFullLinkageIsNotRequiredIfSparse() ); } + /** + * Compound documents require “full linkage”, meaning that every included resource MUST be identified + * by at least one resource identifier object in the same document. + * These resource identifier objects could either be primary data or represent resource linkage + * contained within primary or included resources. + */ public function testIncludedResourceMayBeIdentifiedByPrimaryData() { $apple = new ResourceObject('apples', '1'); $apple->setAttribute('color', 'red'); - $doc = Document::fromResource($apple->toId()); + $doc = Document::fromIdentifier($apple->toIdentifier()); $doc->setIncluded($apple); $this->assertJson(json_encode($doc)); } - public function testIncludedResourceMayBeIdentifiedByAnotherIncludedResource() + public function testIncludedResourceMayBeIdentifiedByLinkageInPrimaryData() + { + $author = new ResourceObject('people', '9'); + $author->setAttribute('first-name', 'Dan'); + + $article = new ResourceObject('articles', '1'); + $article->setAttribute('title', 'JSON API paints my bikeshed!'); + $article->setRelationship( + 'author', + Relationship::fromLinkage(new SingleLinkage($author->toIdentifier())) + ); + + $doc = Document::fromResource($article); + $doc->setIncluded($author); + $this->assertJson(json_encode($doc)); + } + + public function testIncludedResourceMayBeIdentifiedByAnotherLinkedResource() + { + $writer = new ResourceObject('writers', '3'); + $writer->setAttribute('name', 'Eric Evans'); + + $book = new ResourceObject('books', '2'); + $book->setAttribute('name', 'Domain Driven Design'); + $book->setRelationship( + 'author', + Relationship::fromLinkage(new SingleLinkage($writer->toIdentifier())) + ); + + $cart = new ResourceObject('shopping-carts', '1'); + $cart->setRelationship( + 'contents', + Relationship::fromLinkage(new MultiLinkage($book->toIdentifier())) + ); + + $this->assertTrue($book->identifies($writer)); + + $doc = Document::fromResource($cart); + $doc->setIncluded($book, $writer); + $this->assertJson(json_encode($doc)); + } + + /** + * A compound document MUST NOT include more than one resource object for each type and id pair. + * @expectedException \LogicException + * @expectedExceptionMessage Resource apples:1 is already included + */ + public function testCanNotBeManyIncludedResourcesWithEqualIdentifiers() { - /** - * BasketID identifies included BasketObject - * BasketObject identifies included AppleObject - */ $apple = new ResourceObject('apples', '1'); $apple->setAttribute('color', 'red'); - $basket = new ResourceObject('basket', '1'); - $basket->setRelationship( - 'fruits', - Relationship::fromLinkage( - Linkage::fromManyIdentifiers( - $apple->toId() - ) - ) - ); - $doc = Document::fromResource($basket->toId()); - $doc->setIncluded($apple, $basket); + $doc = Document::fromIdentifier($apple->toIdentifier()); + $doc->setIncluded($apple, $apple); $this->assertJson(json_encode($doc)); } + + /** + * If a document does not contain a top-level data key, the included member MUST NOT be present either. + * @expectedException \LogicException + * @expectedExceptionMessage Document with no data cannot contain included resources + */ + public function testIncludedMustOnlyBePresentWithData() + { + $doc = Document::fromMeta(Meta::fromArray(['foo' => 'bar'])); + $doc->setIncluded(new ResourceObject('apples', '1')); + } } diff --git a/test/Document/DocumentTest.php b/test/Document/DocumentTest.php index e7d5255..e1cc757 100644 --- a/test/Document/DocumentTest.php +++ b/test/Document/DocumentTest.php @@ -14,7 +14,6 @@ use JsonApiPhp\JsonApi\Document; use JsonApiPhp\JsonApi\Document\Error; use JsonApiPhp\JsonApi\Document\Meta; -use JsonApiPhp\JsonApi\Document\Resource\NullResource; use JsonApiPhp\JsonApi\Document\Resource\ResourceIdentifier; use JsonApiPhp\JsonApi\Document\Resource\ResourceObject; use JsonApiPhp\JsonApi\Test\BaseTestCase; @@ -91,7 +90,7 @@ public function testDocumentMayContainJustData() "data": null } ', - Document::fromResource(new NullResource), + Document::nullDocument(), 'The simplest document possible contains null' ); @@ -104,7 +103,7 @@ public function testDocumentMayContainJustData() } } ', - Document::fromResource(new ResourceIdentifier('books', 'abc123')), + Document::fromIdentifier(new ResourceIdentifier('books', 'abc123')), 'Resource identifier can be used as primary data' ); @@ -141,7 +140,7 @@ public function testDocumentMayContainJustData() ] } ', - Document::fromResources( + Document::fromIdentifiers( new ResourceIdentifier('books', '12'), new ResourceIdentifier('carrots', '42') ), @@ -157,11 +156,11 @@ public function testDocumentMayContainJustData() */ public function testDocumentCanHaveExtraProperties() { - $doc = Document::fromResource( + $doc = Document::fromIdentifier( new ResourceIdentifier('apples', '42') ); $doc->setApiVersion('1.0'); - $doc->setApiMeta(['a' => 'b']); + $doc->setApiMeta(Meta::fromArray(['a' => 'b'])); $doc->setMeta(Meta::fromArray(['test' => 'test'])); $doc->setLink('self', 'http://example.com/self'); $doc->setLink('related', 'http://example.com/rel', ['foo' => 'bar']); diff --git a/test/Document/Resource/Relationship/LinkageTest.php b/test/Document/Resource/Relationship/LinkageTest.php index e71cb9e..0c7b8d9 100644 --- a/test/Document/Resource/Relationship/LinkageTest.php +++ b/test/Document/Resource/Relationship/LinkageTest.php @@ -11,9 +11,12 @@ namespace JsonApiPhp\JsonApi\Test\Document\Resource\Relationship; -use JsonApiPhp\JsonApi\Document\Resource\NullResource; +use JsonApiPhp\JsonApi\Document\Resource\Linkage\MultiLinkage; +use JsonApiPhp\JsonApi\Document\Resource\Linkage\NullLinkage; +use JsonApiPhp\JsonApi\Document\Resource\Linkage\SingleLinkage; use JsonApiPhp\JsonApi\Document\Resource\Relationship\Linkage; use JsonApiPhp\JsonApi\Document\Resource\ResourceIdentifier; +use JsonApiPhp\JsonApi\Document\Resource\ResourceObject; use JsonApiPhp\JsonApi\Test\BaseTestCase; /** @@ -40,7 +43,7 @@ public function testCanCreateNullLinkage() { $this->assertEncodesTo( 'null', - Linkage::nullLinkage() + new NullLinkage() ); } @@ -48,7 +51,7 @@ public function testCanCreateEmptyArrayLinkage() { $this->assertEncodesTo( '[]', - Linkage::emptyArrayLinkage() + new MultiLinkage() ); } @@ -61,7 +64,7 @@ public function testCanCreateFromSingleResourceId() "id": "abc" } ', - Linkage::fromSingleIdentifier(new ResourceIdentifier('books', 'abc')) + new SingleLinkage(new ResourceIdentifier('books', 'abc')) ); } @@ -80,7 +83,7 @@ public function testCanCreateFromArrayOfResourceIds() } ] ', - Linkage::fromManyIdentifiers( + new MultiLinkage( new ResourceIdentifier('books', 'abc'), new ResourceIdentifier('squirrels', '123') ) @@ -89,24 +92,22 @@ public function testCanCreateFromArrayOfResourceIds() public function testNullLinkageIsLinkedToNothing() { - $apple = new ResourceIdentifier('apples', '1'); - $this->assertFalse(Linkage::nullLinkage()->isLinkedTo($apple)); - $this->assertFalse(Linkage::nullLinkage()->isLinkedTo(new NullResource)); + $apple = new ResourceObject('apples', '1'); + $this->assertFalse((new NullLinkage())->isLinkedTo($apple)); } public function testEmptyArrayLinkageIsLinkedToNothing() { - $apple = new ResourceIdentifier('apples', '1'); - $this->assertFalse(Linkage::emptyArrayLinkage()->isLinkedTo($apple)); - $this->assertFalse(Linkage::emptyArrayLinkage()->isLinkedTo(new NullResource)); + $apple = new ResourceObject('apples', '1'); + $this->assertFalse((new MultiLinkage())->isLinkedTo($apple)); } public function testSingleLinkageIsLinkedOnlyToItself() { - $apple = new ResourceIdentifier('apples', '1'); - $orange = new ResourceIdentifier('oranges', '1'); + $apple = new ResourceObject('apples', '1'); + $orange = new ResourceObject('oranges', '1'); - $linkage = Linkage::fromSingleIdentifier($apple); + $linkage = new SingleLinkage($apple->toIdentifier()); $this->assertTrue($linkage->isLinkedTo($apple)); $this->assertFalse($linkage->isLinkedTo($orange)); @@ -114,11 +115,11 @@ public function testSingleLinkageIsLinkedOnlyToItself() public function testMultiLinkageIsLinkedOnlyToItsMembers() { - $apple = new ResourceIdentifier('apples', '1'); - $orange = new ResourceIdentifier('oranges', '1'); - $banana = new ResourceIdentifier('bananas', '1'); + $apple = new ResourceObject('apples', '1'); + $orange = new ResourceObject('oranges', '1'); + $banana = new ResourceObject('bananas', '1'); - $linkage = Linkage::fromManyIdentifiers($apple, $orange); + $linkage = new MultiLinkage($apple->toIdentifier(), $orange->toIdentifier()); $this->assertTrue($linkage->isLinkedTo($apple)); $this->assertTrue($linkage->isLinkedTo($orange)); diff --git a/test/Document/Resource/Relationship/RelationshipTest.php b/test/Document/Resource/Relationship/RelationshipTest.php index 45ad99e..436384a 100644 --- a/test/Document/Resource/Relationship/RelationshipTest.php +++ b/test/Document/Resource/Relationship/RelationshipTest.php @@ -12,6 +12,7 @@ namespace JsonApiPhp\JsonApi\Test\Document\Resource\Relationship; use JsonApiPhp\JsonApi\Document\Meta; +use JsonApiPhp\JsonApi\Document\Resource\Linkage\NullLinkage; use JsonApiPhp\JsonApi\Document\Resource\Relationship\Linkage; use JsonApiPhp\JsonApi\Document\Resource\Relationship\Relationship; use JsonApiPhp\JsonApi\Test\BaseTestCase; @@ -84,7 +85,7 @@ public function testCanCreateFromLinkage() "data": null } ', - Relationship::fromLinkage(Linkage::nullLinkage()) + Relationship::fromLinkage(new NullLinkage()) ); } diff --git a/test/Document/Resource/ResourceTest.php b/test/Document/Resource/ResourceTest.php index 97c0779..c1b238d 100644 --- a/test/Document/Resource/ResourceTest.php +++ b/test/Document/Resource/ResourceTest.php @@ -39,7 +39,7 @@ public function resourceProvider() return [ [ '{"type": "books"}', - new ResourceIdentifier('books'), + new ResourceObject('books'), ], [ '{"type":"books","id":"42abc"}', diff --git a/test/IntegrationTest.php b/test/IntegrationTest.php index 211ed25..6eee1e5 100644 --- a/test/IntegrationTest.php +++ b/test/IntegrationTest.php @@ -12,7 +12,7 @@ namespace JsonApiPhp\JsonApi\Test; use JsonApiPhp\JsonApi\Document; -use JsonApiPhp\JsonApi\Document\Resource\Relationship\Linkage; +use JsonApiPhp\JsonApi\Document\Resource\Linkage\SingleLinkage; use JsonApiPhp\JsonApi\Document\Resource\Relationship\Relationship; use JsonApiPhp\JsonApi\Document\Resource\ResourceIdentifier; use JsonApiPhp\JsonApi\Document\Resource\ResourceObject; @@ -48,7 +48,7 @@ public function testFromTheReadmeFile() $articles = new ResourceObject('articles', '1'); $author = Relationship::fromLinkage( - Linkage::fromSingleIdentifier( + new SingleLinkage( new ResourceIdentifier('people', '9') ) );