From af86338a6ce95f0ccce6e849592e8ebe1173adaa Mon Sep 17 00:00:00 2001 From: Artem Stepin Date: Fri, 1 Mar 2024 21:07:42 +0100 Subject: [PATCH] Retrieve older content versions (#10) * Added function to retrieve older content versions --- README.md | 19 ++++ src/Api/Content.php | 44 +++++++-- src/Entity/AbstractContent.php | 23 +++++ src/Entity/ContentHistory.php | 133 +++++++++++++++++++++++++++ src/Entity/User.php | 136 ++++++++++++++++++++++++++++ src/Entity/UserPicture.php | 78 ++++++++++++++++ tests/ConfluenceClientTest.php | 77 ++++++++++++++++ tests/Entity/ContentHistoryTest.php | 33 +++++++ 8 files changed, 534 insertions(+), 9 deletions(-) create mode 100644 src/Entity/ContentHistory.php create mode 100644 src/Entity/User.php create mode 100644 src/Entity/UserPicture.php create mode 100644 tests/ConfluenceClientTest.php create mode 100644 tests/Entity/ContentHistoryTest.php diff --git a/README.md b/README.md index 1157eed..2b1b107 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,14 @@ $createdPage = $searchResults->getResultAt(0); $resultContent = $client->content()->get(1234567890); ``` +#### Fetch old versions of a page or comment by content id +```php +/* @var $client CloudPlayDev\ConfluenceClient\ConfluenceClient */ + +//Get a page or comment in a specific version +$resultContentInVersion2 = $client->content()->get(1234567890, 2); +``` + #### Fetch page descendants ```php use CloudPlayDev\ConfluenceClient\Api\Content; @@ -76,6 +84,17 @@ use CloudPlayDev\ConfluenceClient\Api\Content; $childContent = $client->content()->children($page, Content::CONTENT_TYPE_PAGE); //\CloudPlayDev\ConfluenceClient\Entity\ContentSearchResult ``` +#### Fetch content history +```php +use CloudPlayDev\ConfluenceClient\Api\Content; +/* @var $client CloudPlayDev\ConfluenceClient\ConfluenceClient */ +/* @var $page CloudPlayDev\ConfluenceClient\Entity\ContentPage */ + +//get child content +$pageId = 2323232323; +$historyData = $client->content()->history($pageId); //\CloudPlayDev\ConfluenceClient\Entity\ContentSearchResult +``` + ### Manipulating content #### Create new page diff --git a/src/Api/Content.php b/src/Api/Content.php index 0806489..b188421 100644 --- a/src/Api/Content.php +++ b/src/Api/Content.php @@ -4,8 +4,10 @@ namespace CloudPlayDev\ConfluenceClient\Api; use CloudPlayDev\ConfluenceClient\Entity\AbstractContent; +use CloudPlayDev\ConfluenceClient\Entity\ContentHistory; use CloudPlayDev\ConfluenceClient\Entity\ContentSearchResult; use CloudPlayDev\ConfluenceClient\Entity\ContentBody; +use CloudPlayDev\ConfluenceClient\Entity\Hydratable; use CloudPlayDev\ConfluenceClient\Exception\ConfluencePhpClientException; use Http\Client\Exception as HttpClientException; use JsonException; @@ -43,19 +45,28 @@ class Content extends AbstractApi * default value for expand query parameter */ private const DEFAULT_EXPAND = 'space,version,body.storage,container'; + private const DEFAULT_HISTORY_EXPAND = 'content,content.space,content.version,content.body.storage,content.container'; /** * @see https://docs.atlassian.com/atlassian-confluence/REST/6.6.0/#content-getContent - * @param int $contentId - * @return AbstractContent|null * @throws ConfluencePhpClientException + * @throws HttpClientException + * @throws JsonException */ - public function get(int $contentId): ?AbstractContent + public function get(int $contentId, ?int $version = null): ?AbstractContent { - $response = $this->httpGet( - self::getRestfulUri('content', $contentId), - ['expand' => self::DEFAULT_EXPAND] - ); + $fetchUri = ['content', $contentId]; + $parameter = ['expand' => self::DEFAULT_EXPAND]; + + if ($version !== null) { + $fetchUri[] = 'version'; + $fetchUri[] = $version; + + $parameter = ['expand' => self::DEFAULT_HISTORY_EXPAND]; + } + + $response = $this->httpGet(self::getRestfulUri(...$fetchUri), $parameter); + return $this->hydrateResponse($response, AbstractContent::class); } @@ -69,7 +80,7 @@ public function get(int $contentId): ?AbstractContent public function find(array $searchParameter): ContentSearchResult { $allowedSearchParameter = ['title', 'spaceKey', 'type', 'id']; - $queryParameter = array_filter($searchParameter, static function(string $searchKey) use ($allowedSearchParameter) { + $queryParameter = array_filter($searchParameter, static function (string $searchKey) use ($allowedSearchParameter) { return in_array($searchKey, $allowedSearchParameter, true); }, ARRAY_FILTER_USE_KEY); @@ -138,7 +149,7 @@ public function create(AbstractContent $content): AbstractContent ]; if (count($content->getAncestors()) > 0) { - $ancestorsData = array_map(static function(int $id) { + $ancestorsData = array_map(static function (int $id) { return ['id' => $id]; }, $content->getAncestors()); @@ -236,4 +247,19 @@ public function convert(ContentBody $convertBody, string $to = 'view', ?Abstract ); } + + /** + * Returns the history of a particular piece of content, sorted by version number in descending order. + * + * @param int $contentId + * @return Hydratable + * @throws HttpClientException + */ + public function history(int $contentId): Hydratable + { + return $this->hydrateResponse( + $this->httpGet(self::getRestfulUri('content', $contentId, 'history')), + ContentHistory::class + ); + } } diff --git a/src/Entity/AbstractContent.php b/src/Entity/AbstractContent.php index 69ca58f..a9d9232 100644 --- a/src/Entity/AbstractContent.php +++ b/src/Entity/AbstractContent.php @@ -32,6 +32,8 @@ abstract class AbstractContent implements Hydratable private ?int $containerId = null; private string $containerType = Content::CONTENT_TYPE_PAGE; + private bool $isLatest = true; + /** * @return string */ @@ -271,6 +273,11 @@ public function addAncestor(int $id): self */ public static function load(array $data): self { + /* handle older content versions */ + if(isset($data['content'], $data['when'])) { + return self::load($data['content']); + } + Assert::true(isset($data['id'], $data['type'], $data['title'], @@ -302,8 +309,24 @@ public static function load(array $data): self $content->setContent((string)$data['body']['storage']['value']); } + if(isset($data['status'])) { + Assert::string($data['status']); + $content->setLatest($data['status'] === 'current'); + } + return $content; } + public function isLatest(): bool + { + return $this->isLatest; + } + + protected function setLatest(bool $isLatest): AbstractContent + { + $this->isLatest = $isLatest; + return $this; + } + } diff --git a/src/Entity/ContentHistory.php b/src/Entity/ContentHistory.php new file mode 100644 index 0000000..28f93b9 --- /dev/null +++ b/src/Entity/ContentHistory.php @@ -0,0 +1,133 @@ +setLatest($data['latest']); + } + + $contentHistory->setCreatedDate(self::getDateTimeFromString($data['createdDate'])); + $contentHistory->setCreatedBy(User::load($data['createdBy'])); + $contentHistory->setUpdatedBy(User::load($data['lastUpdated']['by'])); + + $contentHistory->setUpdatedDate(self::getDateTimeFromString($data['lastUpdated']['when'])); + + $contentHistory->setLastVersionNumber($data['lastUpdated']['number']); + + return $contentHistory; + } + + /** + * @throws HydrationException + */ + private static function getDateTimeFromString(string $dateString): DateTimeInterface + { + $dateTimeImmutable = DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.vZ', $dateString); + if($dateTimeImmutable === false) { + throw new HydrationException('Invalid date string: ' . $dateString); + } + + return $dateTimeImmutable; + } + + private function setLatest(bool $latest): ContentHistory + { + $this->isLatest = $latest; + return $this; + } + + private function setCreatedDate(DateTimeInterface $createFromFormat): ContentHistory + { + $this->createdDate = $createFromFormat; + return $this; + } + + private function setCreatedBy(User $user): ContentHistory + { + $this->createdBy = $user; + return $this; + } + + private function setUpdatedBy(User $user): ContentHistory + { + $this->updatedBy = $user; + return $this; + } + + public function setUpdatedDate(DateTimeInterface $updatedDate): ContentHistory + { + $this->updatedDate = $updatedDate; + return $this; + } + + public function getUpdatedDate(): DateTimeInterface + { + return $this->updatedDate; + } + + public function getCreatedDate(): DateTimeInterface + { + return $this->createdDate; + } + + public function isLatest(): bool + { + return $this->isLatest; + } + + public function getCreatedBy(): User + { + return $this->createdBy; + } + + public function getUpdatedBy(): User + { + return $this->updatedBy; + } + + public function getLastVersionNumber(): int + { + return $this->lastVersionNumber; + } + + public function setLastVersionNumber(int $lastVersionNumber): void + { + $this->lastVersionNumber = $lastVersionNumber; + } + + +} diff --git a/src/Entity/User.php b/src/Entity/User.php new file mode 100644 index 0000000..8413919 --- /dev/null +++ b/src/Entity/User.php @@ -0,0 +1,136 @@ +setType($data['type']); + $user->setAccountId($data['accountId']); + $user->setAccountType($data['accountType']); + $user->setEmail($data['email']); + $user->setPublicName($data['publicName']); + $user->setDisplayName($data['displayName']); + $user->setIsExternalCollaborator($data['isExternalCollaborator']); + + $user->setProfilePicture(UserPicture::load($data['profilePicture'])); + + return $user; + } + + public function getType(): string + { + return $this->type; + } + + public function setType(string $type): User + { + $this->type = $type; + return $this; + } + + public function getAccountId(): string + { + return $this->accountId; + } + + public function setAccountId(string $accountId): User + { + $this->accountId = $accountId; + return $this; + } + + public function getAccountType(): string + { + return $this->accountType; + } + + public function setAccountType(string $accountType): User + { + $this->accountType = $accountType; + return $this; + } + + public function getEmail(): string + { + return $this->email; + } + + public function setEmail(string $email): User + { + $this->email = $email; + return $this; + } + + public function getPublicName(): string + { + return $this->publicName; + } + + public function setPublicName(string $publicName): User + { + $this->publicName = $publicName; + return $this; + } + + public function getProfilePicture(): UserPicture + { + return $this->profilePicture; + } + + public function setProfilePicture(UserPicture $profilePicture): User + { + $this->profilePicture = $profilePicture; + return $this; + } + + public function getDisplayName(): string + { + return $this->displayName; + } + + public function setDisplayName(string $displayName): User + { + $this->displayName = $displayName; + return $this; + } + + public function isExternalCollaborator(): bool + { + return $this->isExternalCollaborator; + } + + public function setIsExternalCollaborator(bool $isExternalCollaborator): User + { + $this->isExternalCollaborator = $isExternalCollaborator; + return $this; + } + + + + + +} diff --git a/src/Entity/UserPicture.php b/src/Entity/UserPicture.php new file mode 100644 index 0000000..847b8f8 --- /dev/null +++ b/src/Entity/UserPicture.php @@ -0,0 +1,78 @@ +setPath($data['path']); + $userPicture->setWidth($data['width']); + $userPicture->setHeight($data['height']); + $userPicture->setIsDefault($data['isDefault']); + + + return $userPicture; + } + + private function setPath(string $path): UserPicture + { + $this->path = $path; + return $this; + } + + private function setWidth(int $width): UserPicture + { + $this->width = $width; + return $this; + } + + private function setHeight(int $height): UserPicture + { + $this->height = $height; + return $this; + } + + private function setIsDefault(bool $isDefault): UserPicture + { + $this->isDefault = $isDefault; + return $this; + } + + public function getPath(): string + { + return $this->path; + } + + public function getWidth(): int + { + return $this->width; + } + + public function getHeight(): int + { + return $this->height; + } + + public function isDefault(): bool + { + return $this->isDefault; + } + + +} diff --git a/tests/ConfluenceClientTest.php b/tests/ConfluenceClientTest.php new file mode 100644 index 0000000..4c34c2a --- /dev/null +++ b/tests/ConfluenceClientTest.php @@ -0,0 +1,77 @@ +createMock(Builder::class); + $builder->expects($this->once()) + ->method('getHttpClient'); + + $client = new ConfluenceClient('https://example.com', $builder); + $client->getHttpClient(); + } + + public function testCanUseBasicAuth(): void + { + $builder = $this->createMock(Builder::class); + $builder->expects($this->atLeast(2)) + ->method('addPlugin'); + + $builder->expects($this->atLeast(2)) + ->method('removePlugin'); + + $client = new ConfluenceClient('https://example.com', $builder); + $client->authenticateBasicAuth('username', 'password'); + } + + public function testCanUseAuth(): void + { + $builder = $this->createMock(Builder::class); + $builder->expects($this->atLeast(2)) + ->method('addPlugin'); + + $builder->expects($this->atLeast(2)) + ->method('removePlugin'); + + $client = new ConfluenceClient('https://example.com', $builder); + $client->authenticate('token'); + } + + public function testCanUseUsernameAndPasswortInUri(): void + { + $builder = $this->createMock(Builder::class); + $builder->expects($this->exactly(4)) + ->method('addPlugin'); + + $builder->expects($this->exactly(2)) + ->method('removePlugin'); + + $urlFactory = $this->createMock(UriFactoryInterface::class); + $urlFactory->method('createUri')->willReturn(new Uri('https://username:password@example.com/somepath/')); + + $builder->method('getUriFactory')->willReturn($urlFactory); + + new ConfluenceClient('https://username:password@example.com', $builder); + } + + public function testCanGetContent(): void + { + $builder = $this->createMock(Builder::class); + + $client = new ConfluenceClient('https://example.com', $builder); + $content = $client->content(); + + self::assertInstanceOf(Content::class, $content); + } +} diff --git a/tests/Entity/ContentHistoryTest.php b/tests/Entity/ContentHistoryTest.php new file mode 100644 index 0000000..31022ca --- /dev/null +++ b/tests/Entity/ContentHistoryTest.php @@ -0,0 +1,33 @@ +getCreatedDate()); + self::assertInstanceOf(\DateTimeImmutable::class, $contentHistory->getUpdatedDate()); + self::assertTrue($contentHistory->isLatest()); + + self::assertSame('atlassian', $contentHistory->getCreatedBy()->getAccountType()); + self::assertSame('myusername', $contentHistory->getCreatedBy()->getPublicName()); + self::assertSame('myusername', $contentHistory->getUpdatedBy()->getPublicName()); + self::assertSame(3, $contentHistory->getLastVersionNumber()); + + + + } +}