diff --git a/CHANGELOG.md b/CHANGELOG.md index b20f30b..1eea29e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,17 @@ ADDED: -- New accessor and mutator methods for `WoohooLabs\Yang\JsonApi\Request\ResourceObject`: `id()`, `setId()`, `type()`, `setType()`, +- [#13](https://github.com/woohoolabs/yang/issues/13): `DocumentHydratorInterface` and `ClassDocumentHydrator` in order to fix some issues with the `HydratorInterface` and `ClassHydrator` +- [#12](https://github.com/woohoolabs/yang/issues/13): New accessor and mutator methods for `WoohooLabs\Yang\JsonApi\Request\ResourceObject`: `id()`, `setId()`, `type()`, `setType()`, `attributes()`, `relationships()` CHANGED: +DEPRECATED: + +- `HydratorInterface`: use the `DocumentHydratorInterface` instead +- `ClassHydrator`: use the `ClassDocumentHydrator` instead + REMOVED: FIXED: diff --git a/README.md b/README.md index 2dc46b2..26ee4ae 100644 --- a/README.md +++ b/README.md @@ -504,16 +504,16 @@ foreach ($dogResource->relationship("owners")->resources() as $ownerResource) { } ``` -This is the situation when using a hydrator can help you. Currently, Yang has only one hydrator, the `ClassHydrator` which - if the +This is the situation when using a hydrator can help you. Currently, Yang has only one hydrator, the `ClassDocumentHydrator` which - if the response was successful - maps the specified document to an `stdClass` along with all the resource attributes and relationships. -It means that errors, links, meta data won't be present in the returned object! However, relationships are very easy to +It means that errors, links, meta data won't be present in the returned object. However, relationships are very easy to access now. -Let's use the document from the last example for demonstrating the power of hydrators: +Let's use the document from the last example for demonstrating the power of hydrators: ```php -$hydrator = new ClassHydrator(); -$dog = $hydrator->hydrate($response->document()); +$hydrator = new ClassDocumentHydrator(); +$dog = $hydrator->hydrateSingleResource($response->document()); ``` That's all you need to do in order to create the same `$dog` object as in the first example! Now, you can display its properties: @@ -531,20 +531,27 @@ foreach ($dog->owners as $owner) { } ``` -> Note: The method `ClassHydrator::hydrate()` returns an empty `stdClass` if the document doesn't have any primary data, -it returns an `stdClass` if the document has a single primary resource, and it returns an array of `stdObject`s if the -document contains multiple primary resources. +> Note: The method `ClassDocumentHydrator::hydrateSingleResource()` returns an empty `stdClass` if the document doesn't +have any primary data or if the primary data is a collection. Otherwise - when the primary data is a single resource - +an `stdObject` along with all the attributes and relationships is returned. -You may also use the `ClassHydrator::hydrateCollection()` method for retrieving many dogs: +Additionally, you may use the `ClassHydrator::hydrateCollection()` method for retrieving many dogs: ```php -$hydrator = new ClassHydrator(); +$hydrator = new ClassDocumentHydrator(); $dogs = $hydrator->hydrateCollection($response->document()); ``` -> Note: The method `ClassHydrator::hydrateCollection()` returns an empty array if the document doesn't have any primary data, -it returns an array with only one `stdClass` if the document has a single primary resource, and it returns an array of -`stdObject`s if the document contains multiple primary resources. +> Note: The method `ClassHydrator::hydrateCollection()` returns an empty array if the document doesn't have any primary data +or when the primary data is a single resource. Otherwise - when the primary data is a collection of resources - an array +of `stdObject`s along with all the attributes and relationship is returned. + +Furthermore, there is a `hydrate()` method available for you when you don't care if the primary data is a single resource +or a collection of resources. + +> Note: The method `ClassDocumentHydrator::hydrate()` returns an empty array if the document doesn't have any primary data. +It returns an array containing a single `stdClass` if the primary data is a single resource. Otherwise - the primary data +is a collection of resources - an array of `stdObject`s is returned. ## Advanced Usage diff --git a/src/JsonApi/Hydrator/ClassDocumentHydrator.php b/src/JsonApi/Hydrator/ClassDocumentHydrator.php new file mode 100644 index 0000000..104de8c --- /dev/null +++ b/src/JsonApi/Hydrator/ClassDocumentHydrator.php @@ -0,0 +1,132 @@ +hasAnyPrimaryResources() === false) { + return []; + } + + if ($document->isSingleResourceDocument()) { + return [$this->hydratePrimaryResource($document)]; + } + + return $this->hydratePrimaryResources($document); + } + + public function hydrateSingleResource(Document $document): stdClass + { + if ($document->isSingleResourceDocument() === false) { + return new stdClass(); + } + + if ($document->hasAnyPrimaryResources() === false) { + return new stdClass(); + } + + return $this->hydratePrimaryResource($document); + } + + /** + * @return stdClass[] + */ + public function hydrateCollection(Document $document): iterable + { + if ($document->hasAnyPrimaryResources() === false) { + return []; + } + + if ($document->isSingleResourceDocument()) { + return []; + } + + return $this->hydratePrimaryResources($document); + } + + private function hydratePrimaryResources(Document $document): array + { + $result = []; + $resourceMap = []; + + foreach ($document->primaryResources() as $primaryResource) { + $result[] = $this->hydrateResource($primaryResource, $document, $resourceMap); + } + + return $result; + } + + private function hydratePrimaryResource(Document $document): stdClass + { + $resourceMap = []; + + return $this->hydrateResource($document->primaryResource(), $document, $resourceMap); + } + + /** + * @param stdClass[] $resourceMap + */ + private function hydrateResource(ResourceObject $resource, Document $document, array &$resourceMap): stdClass + { + // Fill basic attributes of the resource + $result = new stdClass(); + $result->type = $resource->type(); + $result->id = $resource->id(); + foreach ($resource->attributes() as $attribute => $value) { + $result->{$attribute} = $value; + } + + //Save resource to the identity map + $this->saveObjectToMap($result, $resourceMap); + + //Fill relationships + foreach ($resource->relationships() as $name => $relationship) { + foreach ($relationship->resourceLinks() as $link) { + $object = $this->getObjectFromMap($link["type"], $link["id"], $resourceMap); + + if ($object === null && $document->hasIncludedResource($link["type"], $link["id"])) { + $relatedResource = $document->resource($link["type"], $link["id"]); + $object = $this->hydrateResource($relatedResource, $document, $resourceMap); + } + + if ($object === null) { + continue; + } + + if ($relationship->isToOneRelationship()) { + $result->{$name} = $object; + } else { + $result->{$name}[] = $object; + } + } + } + + return $result; + } + + /** + * @param stdClass[] $resourceMap + */ + private function getObjectFromMap(string $type, string $id, array $resourceMap): ?stdClass + { + return $resourceMap[$type . "-" . $id] ?? null; + } + + /** + * @param stdClass[] $resourceMap + */ + private function saveObjectToMap(stdClass $object, array &$resourceMap): void + { + $resourceMap[$object->type . "-" . $object->id] = $object; + } +} diff --git a/src/JsonApi/Hydrator/ClassHydrator.php b/src/JsonApi/Hydrator/ClassHydrator.php index 5075521..6a8421e 100644 --- a/src/JsonApi/Hydrator/ClassHydrator.php +++ b/src/JsonApi/Hydrator/ClassHydrator.php @@ -7,6 +7,9 @@ use WoohooLabs\Yang\JsonApi\Schema\Document; use WoohooLabs\Yang\JsonApi\Schema\Resource\ResourceObject; +/** + * @deprecated Use the ClassDocumentHydrator instead. + */ final class ClassHydrator implements HydratorInterface { /** diff --git a/src/JsonApi/Hydrator/DocumentHydratorInterface.php b/src/JsonApi/Hydrator/DocumentHydratorInterface.php new file mode 100644 index 0000000..01ab518 --- /dev/null +++ b/src/JsonApi/Hydrator/DocumentHydratorInterface.php @@ -0,0 +1,31 @@ + + */ + public function hydrate(Document $document): iterable; + + /** + * Hydrates a document when its primary data is a single resource. + * + * @return mixed + */ + public function hydrateSingleResource(Document $document); + + /** + * Hydrates a document when its primary data is a collection of resources. + * + * @return iterable + */ + public function hydrateCollection(Document $document): iterable; +} diff --git a/src/JsonApi/Hydrator/HydratorInterface.php b/src/JsonApi/Hydrator/HydratorInterface.php index b195030..93f7bee 100644 --- a/src/JsonApi/Hydrator/HydratorInterface.php +++ b/src/JsonApi/Hydrator/HydratorInterface.php @@ -5,6 +5,9 @@ use WoohooLabs\Yang\JsonApi\Schema\Document; +/** + * @deprecated Use the DocumentHydratorInterface instead. + */ interface HydratorInterface { /** diff --git a/tests/JsonApi/Hydrator/ClassDocumentHydratorTest.php b/tests/JsonApi/Hydrator/ClassDocumentHydratorTest.php new file mode 100644 index 0000000..327229e --- /dev/null +++ b/tests/JsonApi/Hydrator/ClassDocumentHydratorTest.php @@ -0,0 +1,531 @@ + [], + ]; + + $document = Document::fromArray($document); + $hydrator = new ClassDocumentHydrator(); + $object = $hydrator->hydrate($document); + + $this->assertEquals([], $object); + } + + /** + * @test + */ + public function hydrateWithSingleResourceTypeAndId() + { + $document = [ + "data" => [ + "type" => "a", + "id" => "1", + ], + ]; + + $document = Document::fromArray($document); + $hydrator = new ClassDocumentHydrator(); + $objects = $hydrator->hydrate($document); + + $this->assertCount(1, $objects); + + $this->assertAttributeSame("a", "type", $objects[0]); + $this->assertAttributeSame("1", "id", $objects[0]); + } + + /** + * @test + */ + public function hydrateWithSingleResourceAttributes() + { + $document = [ + "data" => [ + "attributes" => [ + "a" => "A", + "b" => "B", + "c" => "C", + ], + ], + ]; + + $document = Document::fromArray($document); + $hydrator = new ClassDocumentHydrator(); + $objects = $hydrator->hydrate($document); + + $this->assertCount(1, $objects); + + $this->assertAttributeSame("A", "a", $objects[0]); + $this->assertAttributeSame("B", "b", $objects[0]); + $this->assertAttributeSame("C", "c", $objects[0]); + } + + /** + * @test + */ + public function hydrateWithCollectionAttributes() + { + $document = [ + "data" => [ + [ + "type" => "a", + "id" => "1", + "attributes" => [ + "a" => "A", + "b" => "B", + "c" => "C", + ], + ], + [ + "type" => "a", + "id" => "0", + "attributes" => [ + "a" => "D", + "b" => "E", + "c" => "F", + ], + ], + ], + ]; + + $document = Document::fromArray($document); + $hydrator = new ClassDocumentHydrator(); + $objects = $hydrator->hydrate($document); + + $this->assertCount(2, $objects); + + $this->assertAttributeSame("1", "id", $objects[0]); + $this->assertAttributeSame("A", "a", $objects[0]); + $this->assertAttributeSame("B", "b", $objects[0]); + $this->assertAttributeSame("C", "c", $objects[0]); + + $this->assertAttributeSame("0", "id", $objects[1]); + $this->assertAttributeSame("D", "a", $objects[1]); + $this->assertAttributeSame("E", "b", $objects[1]); + $this->assertAttributeSame("F", "c", $objects[1]); + } + + /** + * @test + */ + public function hydrateWithSingleResourceNotIncludedToOneRelationship() + { + $document = [ + "data" => [ + "relationships" => [ + "x" => [ + "data" => [ + "type" => "a", + "id" => "1", + ], + ], + ], + ], + ]; + + $document = Document::fromArray($document); + $hydrator = new ClassDocumentHydrator(); + $objects = $hydrator->hydrate($document); + + $this->assertCount(1, $objects); + $this->assertObjectNotHasAttribute("x", $objects[0]); + } + + /** + * @test + */ + public function hydrateWithSingleResourceIncludedToOneRelationship() + { + $document = [ + "data" => [ + "type" => "a", + "id" => "1", + "relationships" => [ + "x" => [ + "data" => [ + "type" => "b", + "id" => "0", + ], + ], + ], + ], + "included" => [ + [ + "type" => "b", + "id" => "0", + "attributes" => [ + "a" => "A", + "b" => "B", + ], + ], + ], + ]; + + $document = Document::fromArray($document); + $hydrator = new ClassDocumentHydrator(); + $objects = $hydrator->hydrate($document); + + $this->assertCount(1, $objects); + $this->assertObjectHasAttribute("x", $objects[0]); + $this->assertAttributeSame("b", "type", $objects[0]->x); + $this->assertAttributeSame("0", "id", $objects[0]->x); + $this->assertAttributeSame("A", "a", $objects[0]->x); + } + + /** + * @test + */ + public function hydrateWithToManyRelationship() + { + $document = [ + "data" => [ + "type" => "a", + "id" => "1", + "relationships" => [ + "x" => [ + "data" => [ + [ + "type" => "b", + "id" => "1", + ], + [ + "type" => "b", + "id" => "2", + ], + [ + "type" => "b", + "id" => "3", + ], + ], + ], + ], + ], + "included" => [ + [ + "type" => "b", + "id" => "1", + "attributes" => [ + "a" => "A", + "b" => "B", + ], + ], + [ + "type" => "b", + "id" => "2", + "attributes" => [ + "a" => "C", + "b" => "D", + ], + ], + ], + ]; + + $document = Document::fromArray($document); + $hydrator = new ClassDocumentHydrator(); + $objects = $hydrator->hydrate($document); + + $this->assertCount(1, $objects); + $this->assertCount(2, $objects[0]->x); + + $this->assertAttributeSame("b", "type", $objects[0]->x[0]); + $this->assertAttributeSame("1", "id", $objects[0]->x[0]); + $this->assertAttributeSame("A", "a", $objects[0]->x[0]); + $this->assertAttributeSame("B", "b", $objects[0]->x[0]); + + $this->assertAttributeSame("b", "type", $objects[0]->x[1]); + $this->assertAttributeSame("2", "id", $objects[0]->x[1]); + $this->assertAttributeSame("C", "a", $objects[0]->x[1]); + $this->assertAttributeSame("D", "b", $objects[0]->x[1]); + } + + /** + * @test + */ + public function hydrateWithSingleResourceNestedRelationships() + { + $document = [ + "data" => [ + "type" => "a", + "id" => "1", + "relationships" => [ + "x" => [ + "data" => [ + "type" => "b", + "id" => "1", + ], + ], + ], + ], + "included" => [ + [ + "type" => "b", + "id" => "1", + "relationships" => [ + "y" => [ + "data" => [ + "type" => "b", + "id" => "2", + ], + ], + ], + ], + [ + "type" => "b", + "id" => "2", + "attributes" => [ + "a" => "C", + "b" => "D", + ], + ], + ], + ]; + + $document = Document::fromArray($document); + $hydrator = new ClassDocumentHydrator(); + $objects = $hydrator->hydrate($document); + + $this->assertCount(1, $objects); + + $this->assertObjectHasAttribute("x", $objects[0]); + $this->assertObjectHasAttribute("y", $objects[0]->x); + + $this->assertAttributeSame("b", "type", $objects[0]->x->y); + $this->assertAttributeSame("2", "id", $objects[0]->x->y); + $this->assertAttributeSame("C", "a", $objects[0]->x->y); + $this->assertAttributeSame("D", "b", $objects[0]->x->y); + } + + /** + * @test + */ + public function hydrateWithSingleResourceRecursiveRelationships() + { + $document = [ + "data" => [ + "type" => "a", + "id" => "1", + "attributes" => [ + "a" => "A", + "b" => "B", + ], + "relationships" => [ + "x" => [ + "data" => [ + "type" => "b", + "id" => "1", + ], + ], + ], + ], + "included" => [ + [ + "type" => "b", + "id" => "1", + "relationships" => [ + "y" => [ + "data" => [ + "type" => "b", + "id" => "2", + ], + ], + ], + ], + [ + "type" => "b", + "id" => "2", + "relationships" => [ + "z" => [ + "data" => [ + "type" => "a", + "id" => "1", + ], + ], + ], + ], + ], + ]; + + $document = Document::fromArray($document); + $hydrator = new ClassDocumentHydrator(); + $objects = $hydrator->hydrate($document); + + $this->assertCount(1, $objects); + + $this->assertObjectHasAttribute("x", $objects[0]); + $this->assertObjectHasAttribute("y", $objects[0]->x); + $this->assertObjectHasAttribute("z", $objects[0]->x->y); + $this->assertSame($objects[0], $objects[0]->x->y->z); + + $this->assertAttributeSame("a", "type", $objects[0]->x->y->z); + $this->assertAttributeSame("1", "id", $objects[0]->x->y->z); + $this->assertAttributeSame("A", "a", $objects[0]->x->y->z); + $this->assertAttributeSame("B", "b", $objects[0]->x->y->z); + } + + /** + * @test + */ + public function hydrateSingleResourceWhenCollectionEmpty() + { + $document = [ + "data" => [], + ]; + + $document = Document::fromArray($document); + $hydrator = new ClassDocumentHydrator(); + $object = $hydrator->hydrateSingleResource($document); + + $this->assertEquals(new stdClass(), $object); + } + + /** + * @test + */ + public function hydrateSingleResourceWhenCollection() + { + $document = [ + "data" => [ + [ + "type" => "a", + "id" => "1", + ], + ], + ]; + + $document = Document::fromArray($document); + $hydrator = new ClassDocumentHydrator(); + $object = $hydrator->hydrateSingleResource($document); + + $this->assertEquals(new stdClass(), $object); + } + + /** + * @test + */ + public function hydrateSingleResourceWhenSingleResourceEmpty() + { + $document = [ + "data" => null, + ]; + + $document = Document::fromArray($document); + $hydrator = new ClassDocumentHydrator(); + $object = $hydrator->hydrateSingleResource($document); + + $this->assertEquals(new stdClass(), $object); + } + + /** + * @test + */ + public function hydrateSingleResourceWhenSingleResource() + { + $document = [ + "data" => [ + "type" => "a", + "id" => "1", + ], + ]; + + $document = Document::fromArray($document); + $hydrator = new ClassDocumentHydrator(); + $object = $hydrator->hydrateSingleResource($document); + + $this->assertEquals("a", $object->type); + $this->assertEquals("1", $object->id); + } + + /** + * @test + */ + public function hydrateCollectionWhenEmpty() + { + $document = [ + "data" => [], + ]; + + $document = Document::fromArray($document); + $hydrator = new ClassDocumentHydrator(); + $collection = $hydrator->hydrateCollection($document); + + $this->assertCount(0, $collection); + } + + /** + * @test + */ + public function hydrateCollectionWhenEmptySingleResource() + { + $document = [ + "data" => null, + ]; + + $document = Document::fromArray($document); + $hydrator = new ClassDocumentHydrator(); + $collection = $hydrator->hydrateCollection($document); + + $this->assertCount(0, $collection); + } + + /** + * @test + */ + public function hydrateCollectionWhenSingleResource() + { + $document = [ + "data" => [ + "type" => "a", + "id" => "1", + ], + ]; + + $document = Document::fromArray($document); + $hydrator = new ClassDocumentHydrator(); + $collection = $hydrator->hydrateCollection($document); + + $this->assertSame([], $collection); + } + + /** + * @test + */ + public function hydrateCollectionMultipleResources() + { + $document = [ + "data" => [ + [ + "type" => "a", + "id" => "1", + ], + [ + "type" => "a", + "id" => "2", + ], + ], + ]; + + $document = Document::fromArray($document); + $hydrator = new ClassDocumentHydrator(); + $collection = $hydrator->hydrateCollection($document); + + $this->assertCount(2, $collection); + $this->assertAttributeSame("a", "type", $collection[0]); + $this->assertAttributeSame("1", "id", $collection[0]); + $this->assertAttributeSame("a", "type", $collection[1]); + $this->assertAttributeSame("2", "id", $collection[1]); + } +}