diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3f282b0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +max_line_length = 120 +tab_width = 4 + +[{*.ctp,*.engine,*.hphp,*.inc,*.install,*.module,*.php,*.php4,*.php5,*.phtml,*.profile,*.test,*.theme}] +max_line_length = 80 diff --git a/.gitignore b/.gitignore index 45d3b43..daa06c1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ vendor/ build/ .idea/ + +.phpunit.result.cache diff --git a/composer.json b/composer.json index 9ccef62..575c259 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,8 @@ "guzzlehttp/psr7": "^1.6", "php-http/client-implementation": "^1.0", "psr/http-client": "^1.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0", + "symfony/serializer": "^5.2" }, "require-dev": { "php-http/mock-client": "^1.3", diff --git a/src/Entity/Collection/CollectionRelLinkTrait.php b/src/Entity/Collection/CollectionRelLinkTrait.php new file mode 100644 index 0000000..6be628c --- /dev/null +++ b/src/Entity/Collection/CollectionRelLinkTrait.php @@ -0,0 +1,64 @@ +getRelLink('self'); + } + + public function getFirstLink() + { + return $this->getRelLink('first'); + } + + public function getPreviousLink() + { + return $this->getRelLink('prev'); + } + + public function getLastLink() + { + return $this->getRelLink('last'); + } + + public function getNextLink() + { + return $this->getRelLink('next'); + } + + /** + * Get the related link. + * + * @param string $link_name + * The name of the related link to retrieve. + * + * @return bool|\Psr\Http\Message\UriInterface + * The related URL or FALSE if not present. + */ + protected function getRelLink(string $link_name) + { + if (!isset($this->rawData->$link_name)) { + return false; + } + + $uri = new Uri($this->rawData->$link_name); + $uri = $uri->withPath($uri->getPath() . '.json'); + return $uri; + } +} diff --git a/src/Entity/Collection/EntityCollection.php b/src/Entity/Collection/EntityCollection.php index 8e4ce05..3c3442b 100644 --- a/src/Entity/Collection/EntityCollection.php +++ b/src/Entity/Collection/EntityCollection.php @@ -2,11 +2,14 @@ namespace Hussainweb\DrupalApi\Entity\Collection; -use GuzzleHttp\Psr7\Uri; +use Psr\Http\Client\ClientInterface; use Psr\Http\Message\ResponseInterface; +use Symfony\Component\Serializer\Encoder\JsonDecode; +use Symfony\Component\Serializer\Encoder\JsonEncoder; -abstract class EntityCollection implements \Iterator, \Countable +abstract class EntityCollection implements EntityCollectionInterface, \Countable { + use CollectionRelLinkTrait; private $iteratorPosition = 0; @@ -31,52 +34,22 @@ final public function __construct($data) */ public static function fromResponse(ResponseInterface $response) { - return new static(json_decode((string) $response->getBody())); - } - - public function getSelfLink() - { - return $this->getRelLink('self'); - } - - public function getFirstLink() - { - return $this->getRelLink('first'); - } - - public function getPreviousLink() - { - return $this->getRelLink('prev'); - } - - public function getNextLink() - { - return $this->getRelLink('next'); - } - - public function getLastLink() - { - return $this->getRelLink('last'); + return new static((new JsonDecode())->decode((string) $response->getBody(), JsonEncoder::FORMAT)); } /** - * Get the related link. + * Construct a paging entity collection from this collection. * - * @param string $link_name - * The name of the related link to retrieve. + * @param \Psr\Http\Client\ClientInterface $client + * The HTTP client used to fetch subsequent pages. * - * @return bool|\Psr\Http\Message\UriInterface - * The related URL or FALSE if not present. + * @see \Hussainweb\DrupalApi\Client::getEntity + * + * @return \Hussainweb\DrupalApi\Entity\Collection\PagingEntityCollection */ - protected function getRelLink($link_name) + public function toPagingEntityCollection(ClientInterface $client): PagingEntityCollection { - if (!isset($this->rawData->$link_name)) { - return false; - } - - $uri = new Uri($this->rawData->$link_name); - $uri = $uri->withPath($uri->getPath() . '.json'); - return $uri; + return new PagingEntityCollection($this->rawData, $client, $this->getListItemClass()); } /** @@ -115,7 +88,7 @@ public function key() /** * {@inheritdoc} */ - public function valid() + public function valid(): bool { return isset($this->rawData->list[$this->iteratorPosition]); } diff --git a/src/Entity/Collection/EntityCollectionInterface.php b/src/Entity/Collection/EntityCollectionInterface.php new file mode 100644 index 0000000..2ac6252 --- /dev/null +++ b/src/Entity/Collection/EntityCollectionInterface.php @@ -0,0 +1,63 @@ +rawData = $data; + $this->client = $client; + $this->listItemClass = $listItemClass; + } + + /** + * Construct the object from a HTTP Response. + * + * @param \Psr\Http\Message\ResponseInterface $response + * Response object to parse. + * @param \Psr\Http\Client\ClientInterface $client + * The HTTP client used to fetch subsequent pages. + * @param string $listItemClass + * The list item class FQCN to deserialize entities into. + * + * @return static + * The EntityCollection object for the response. + */ + public static function fromResponse( + ResponseInterface $response, + ClientInterface $client, + string $listItemClass + ): self { + return new static( + (new JsonDecode())->decode( + (string) $response->getBody(), + JsonEncoder::FORMAT + ), + $client, + $listItemClass + ); + } + + public function current() + { + return new $this->listItemClass( + $this->rawData->list[$this->iteratorPosition] + ); + } + + public function next() + { + ++$this->iteratorPosition; + } + + public function key() + { + return $this->iteratorPosition; + } + + public function valid(): bool + { + $valid = isset($this->rawData->list[$this->iteratorPosition]); + if (!$valid && $this->getNextLink()) { + $request = new Request((string) $this->getNextLink()); + $response = $this->client->sendRequest($request); + $this->rawData = (new JsonDecode())->decode( + (string) $response->getBody(), + JsonEncoder::FORMAT + ); + $this->iteratorPosition = 0; + $valid = true; + } + + return $valid; + } + + public function rewind() + { + // We do not allow rewinding as that could force us to go back and reload a + // previously loaded page. + } +} diff --git a/tests/Entity/Collection/PagingEntityCollectionTest.php b/tests/Entity/Collection/PagingEntityCollectionTest.php new file mode 100644 index 0000000..a171352 --- /dev/null +++ b/tests/Entity/Collection/PagingEntityCollectionTest.php @@ -0,0 +1,79 @@ +createMock(ClientInterface::class); + $page1 = json_decode(file_get_contents( + __DIR__ . '/../../fixtures/comment-collection-page-1.json' + )); + + // Modify the fixture so there's only two pages. + unset($page1->next); + $page1 = json_encode($page1); + + $client->expects($this->once())->method('sendRequest') + ->willReturn(new Response( + 200, + ['Content-Type' => 'application/json'], + $page1 + )); + $collection = new PagingEntityCollection($data, $client, Comment::class); + $count = 0; + + /** @var \Hussainweb\DrupalApi\Entity\Comment $comment */ + foreach ($collection as $comment) { + $count++; + $this->assertIsInt($comment->getId()); + } + + $this->assertEquals(10, $count); + } + + public function testFromClient() + { + $data = json_decode(file_get_contents(__DIR__ . '/../../fixtures/comment-collection-page-0.json')); + /** @var \Hussainweb\DrupalApi\Client|\PHPUnit\Framework\MockObject\MockObject $client */ + $client = $this->createMock(Client::class); + $page1 = json_decode(file_get_contents( + __DIR__ . '/../../fixtures/comment-collection-page-1.json' + )); + + // Modify the fixture so there's only two pages. + unset($page1->next); + $page1 = json_encode($page1); + + $comment_collection = new CommentCollection($data); + $client->expects($this->once())->method('getEntity') + ->willReturn($comment_collection); + + $request = new CommentCollectionRequest(); + $entity_collection = $client->getEntity($request); + + $http_client = $this->createMock(ClientInterface::class); + $http_client->expects($this->once())->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], $page1)); + $paging_collection = $entity_collection->toPagingEntityCollection($http_client); + $count = 0; + foreach ($paging_collection as $comment) { + $count++; + $this->assertInstanceOf(Comment::class, $comment); + } + + $this->assertEquals(10, $count); + } +} diff --git a/tests/fixtures/comment-collection-page-0.json b/tests/fixtures/comment-collection-page-0.json new file mode 100644 index 0000000..3a7a0cc --- /dev/null +++ b/tests/fixtures/comment-collection-page-0.json @@ -0,0 +1,143 @@ +{ + "self": "https://www.drupal.org/api-d7/comment?page=0&limit=5", + "first": "https://www.drupal.org/api-d7/comment?page=0&limit=5", + "last": "https://www.drupal.org/api-d7/comment?page=1720433&limit=5", + "next": "https://www.drupal.org/api-d7/comment?page=1&limit=5", + "list": [ + { + "comment_body": { + "value": "
We're currently working on Access '95 integration. But Access '97 will be one of the first features when we start work on 2.0!!!
", + "format": "1" + }, + "field_attribute_contribution_to": [], + "field_for_customer": [], + "field_attribute_as_volunteer": [], + "cid": "5076802", + "name": "JohnAlbin", + "homepage": "", + "subject": "", + "url": "https://www.drupal.org/project/vaporware/issues/1166082#comment-5076802", + "edit_url": "https://www.drupal.org/comment/edit/5076802", + "created": "876044700", + "node": { + "uri": "https://www.drupal.org/api-d7/node/1166082", + "id": "1166082", + "resource": "node" + }, + "author": { + "uri": "https://www.drupal.org/api-d7/user/32095", + "id": "32095", + "resource": "user" + }, + "feeds_item_guid": null, + "feeds_item_url": null, + "feed_nid": null + }, + { + "comment_body": { + "value": "PHP didn't like a reference to an undefined object under an array and crashed. Fixed it and works fine again.
", + "format": "1" + }, + "field_attribute_contribution_to": [], + "field_for_customer": [], + "field_attribute_as_volunteer": [], + "cid": "287017", + "name": "", + "homepage": "", + "subject": "", + "url": "https://www.drupal.org/project/drupal/issues/6#comment-287017", + "edit_url": "https://www.drupal.org/comment/edit/287017", + "created": "1008599295", + "node": { + "uri": "https://www.drupal.org/api-d7/node/6", + "id": "6", + "resource": "node" + }, + "feeds_item_guid": null, + "feeds_item_url": null, + "feed_nid": null + }, + { + "comment_body": { + "value": "Fixed.
", + "format": "1" + }, + "field_attribute_contribution_to": [], + "field_for_customer": [], + "field_attribute_as_volunteer": [], + "cid": "287019", + "name": "", + "homepage": "", + "subject": "", + "url": "https://www.drupal.org/project/drupal/issues/7#comment-287019", + "edit_url": "https://www.drupal.org/comment/edit/287019", + "created": "1008601237", + "node": { + "uri": "https://www.drupal.org/api-d7/node/7", + "id": "7", + "resource": "node" + }, + "feeds_item_guid": null, + "feeds_item_url": null, + "feed_nid": null + }, + { + "comment_body": { + "value": "This is a very useful function. Maybe it should also make the post private in the blog. In other words, maybe the post would only be visible to the author. Or maybe these are two different options.
\nI could see using this as a way to save a draft post, or if I want to remind myself of some things within the context of the host site.
\n- Joe
\n<!-- Feature creep... it's a healthy problem to have. -->
", + "format": "1" + }, + "field_attribute_contribution_to": [], + "field_for_customer": [], + "field_attribute_as_volunteer": [], + "cid": "287090", + "name": "j0e@www.drop.org", + "homepage": "", + "subject": "", + "url": "https://www.drupal.org/project/drupal/issues/9#comment-287090", + "edit_url": "https://www.drupal.org/comment/edit/287090", + "created": "1008635513", + "node": { + "uri": "https://www.drupal.org/api-d7/node/9", + "id": "9", + "resource": "node" + }, + "author": { + "uri": "https://www.drupal.org/api-d7/user/15", + "id": "15", + "resource": "user" + }, + "feeds_item_guid": null, + "feeds_item_url": null, + "feed_nid": null + }, + { + "comment_body": { + "value": "ax convinced me by referring to user privacy ... i've assigned this one to myself.
", + "format": "1" + }, + "field_attribute_contribution_to": [], + "field_for_customer": [], + "field_attribute_as_volunteer": [], + "cid": "287021", + "name": "moshe weitzman", + "homepage": "", + "subject": "", + "url": "https://www.drupal.org/project/drupal/issues/8#comment-287021", + "edit_url": "https://www.drupal.org/comment/edit/287021", + "created": "1008653348", + "node": { + "uri": "https://www.drupal.org/api-d7/node/8", + "id": "8", + "resource": "node" + }, + "author": { + "uri": "https://www.drupal.org/api-d7/user/23", + "id": "23", + "resource": "user" + }, + "feeds_item_guid": null, + "feeds_item_url": null, + "feed_nid": null + } + ] +} diff --git a/tests/fixtures/comment-collection-page-1.json b/tests/fixtures/comment-collection-page-1.json new file mode 100644 index 0000000..d078e1a --- /dev/null +++ b/tests/fixtures/comment-collection-page-1.json @@ -0,0 +1,144 @@ +{ + "self": "https://www.drupal.org/api-d7/comment?page=1&limit=5", + "first": "https://www.drupal.org/api-d7/comment?page=0&limit=5", + "last": "https://www.drupal.org/api-d7/comment?page=1720439&limit=5", + "prev": "https://www.drupal.org/api-d7/comment?page=0&limit=5", + "next": "https://www.drupal.org/api-d7/comment?page=2&limit=5", + "list": [ + { + "comment_body": { + "value": "not a bug, i guess - just the admin not having created a forum yet.
", + "format": "1" + }, + "field_attribute_contribution_to": [], + "field_for_customer": [], + "field_attribute_as_volunteer": [], + "cid": "287094", + "name": "ax", + "homepage": "", + "subject": "", + "url": "https://www.drupal.org/project/drupal/issues/11#comment-287094", + "edit_url": "https://www.drupal.org/comment/edit/287094", + "created": "1008677001", + "node": { + "uri": "https://www.drupal.org/api-d7/node/11", + "id": "11", + "resource": "node" + }, + "author": { + "uri": "https://www.drupal.org/api-d7/user/8", + "id": "8", + "resource": "user" + }, + "feeds_item_guid": null, + "feeds_item_url": null, + "feed_nid": null + }, + { + "comment_body": { + "value": "I think I fixed it. It was a typo: $edit[\"uid\"] should have read $node->uid.
", + "format": "1" + }, + "field_attribute_contribution_to": [], + "field_for_customer": [], + "field_attribute_as_volunteer": [], + "cid": "287097", + "name": "Dries", + "homepage": "", + "subject": "", + "url": "https://www.drupal.org/project/drupal/issues/16#comment-287097", + "edit_url": "https://www.drupal.org/comment/edit/287097", + "created": "1008709273", + "node": { + "uri": "https://www.drupal.org/api-d7/node/16", + "id": "16", + "resource": "node" + }, + "author": { + "uri": "https://www.drupal.org/api-d7/user/1", + "id": "1", + "resource": "user" + }, + "feeds_item_guid": null, + "feeds_item_url": null, + "feed_nid": null + }, + { + "comment_body": { + "value": "Fixed in CVS.
", + "format": "1" + }, + "field_attribute_contribution_to": [], + "field_for_customer": [], + "field_attribute_as_volunteer": [], + "cid": "287095", + "name": "", + "homepage": "", + "subject": "", + "url": "https://www.drupal.org/project/drupal/issues/15#comment-287095", + "edit_url": "https://www.drupal.org/comment/edit/287095", + "created": "1008862665", + "node": { + "uri": "https://www.drupal.org/api-d7/node/15", + "id": "15", + "resource": "node" + }, + "feeds_item_guid": null, + "feeds_item_url": null, + "feed_nid": null + }, + { + "comment_body": { + "value": "Marking as fixed. Ax: if this fixed it please close the bug.
", + "format": "1" + }, + "field_attribute_contribution_to": [], + "field_for_customer": [], + "field_attribute_as_volunteer": [], + "cid": "287098", + "name": "", + "homepage": "", + "subject": "", + "url": "https://www.drupal.org/project/drupal/issues/16#comment-287098", + "edit_url": "https://www.drupal.org/comment/edit/287098", + "created": "1008862728", + "node": { + "uri": "https://www.drupal.org/api-d7/node/16", + "id": "16", + "resource": "node" + }, + "feeds_item_guid": null, + "feeds_item_url": null, + "feed_nid": null + }, + { + "comment_body": { + "value": "this fixes the \"Submitted by anonymous\" issue - but the missing link(s) remain. i don't know if /you/ consider it a bug that the preview shows different (less) links than the actual, finished node (in the example of a blog: preview shows only \"update this blog\"; actual node - node.php?id=X - shows \"update this blog\" and \"add new comment\") - i /do/ consider it a bug, so i leave this open.
\nnote: other node types may have more, different and more \"important\" links than blogs.
\nfeel free to close this bug if you dont agree. or should this be made a separate report?
", + "format": "1" + }, + "field_attribute_contribution_to": [], + "field_for_customer": [], + "field_attribute_as_volunteer": [], + "cid": "287099", + "name": "ax", + "homepage": "", + "subject": "", + "url": "https://www.drupal.org/project/drupal/issues/16#comment-287099", + "edit_url": "https://www.drupal.org/comment/edit/287099", + "created": "1008865465", + "node": { + "uri": "https://www.drupal.org/api-d7/node/16", + "id": "16", + "resource": "node" + }, + "author": { + "uri": "https://www.drupal.org/api-d7/user/8", + "id": "8", + "resource": "user" + }, + "feeds_item_guid": null, + "feeds_item_url": null, + "feed_nid": null + } + ] +}