From 4cc001a070cbfdff641e298b94d6f5741e6d08ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 2 Oct 2024 13:13:26 +0200 Subject: [PATCH] Enhance behaviour of duplicating storedObject to keep only the last "kept before conversion" version if any Enhance the duplication service to selectively handle versions tagged with "KEEP_BEFORE_CONVERSION". Modify StoredObject to support retrieval and checking of such versions. Add relevant test cases to validate this behavior. --- .../Entity/StoredObject.php | 71 ++++++++++++++++ .../Service/StoredObjectDuplicate.php | 13 ++- .../Tests/Entity/StoredObjectTest.php | 25 ++++++ .../Service/StoredObjectDuplicateTest.php | 82 +++++++++++++++++++ 4 files changed, 189 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php b/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php index c6d9e362c..b2ac22582 100644 --- a/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php +++ b/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php @@ -18,6 +18,8 @@ use Chill\MainBundle\Doctrine\Model\TrackCreationInterface; use Chill\MainBundle\Doctrine\Model\TrackCreationTrait; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Doctrine\Common\Collections\Order; +use Doctrine\Common\Collections\ReadableCollection; use Doctrine\Common\Collections\Selectable; use Doctrine\ORM\Mapping as ORM; use Ramsey\Uuid\Uuid; @@ -257,11 +259,39 @@ class StoredObject implements Document, TrackCreationInterface return $this->template; } + /** + * @return Selectable&Collection + */ public function getVersions(): Collection&Selectable { return $this->versions; } + /** + * Retrieves versions sorted by a given order. + * + * @param 'ASC'|'DESC' $order the sorting order, default is Order::Ascending + * + * @return readableCollection&Selectable The ordered collection of versions + */ + public function getVersionsOrdered(string $order = 'ASC'): ReadableCollection&Selectable + { + $versions = $this->getVersions()->toArray(); + + switch ($order) { + case 'ASC': + usort($versions, static fn (StoredObjectVersion $a, StoredObjectVersion $b) => $a->getVersion() <=> $b->getVersion()); + break; + case 'DESC': + usort($versions, static fn (StoredObjectVersion $a, StoredObjectVersion $b) => $b->getVersion() <=> $a->getVersion()); + break; + default: + throw new \UnexpectedValueException('Unknown order'); + } + + return new ArrayCollection($versions); + } + public function hasCurrentVersion(): bool { return null !== $this->getCurrentVersion(); @@ -272,6 +302,47 @@ class StoredObject implements Document, TrackCreationInterface return null !== $this->template; } + /** + * Checks if there is a version kept before conversion. + * + * @return bool true if a version is kept before conversion, false otherwise + */ + public function hasKeptBeforeConversionVersion(): bool + { + foreach ($this->getVersions() as $version) { + foreach ($version->getPointInTimes() as $pointInTime) { + if (StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION === $pointInTime->getReason()) { + return true; + } + } + } + + return false; + } + + /** + * Retrieves the last version of the stored object that was kept before conversion. + * + * This method iterates through the ordered versions and their respective points + * in time to find the most recent version that has a point in time with the reason + * 'KEEP_BEFORE_CONVERSION'. + * + * @return StoredObjectVersion|null the version that was kept before conversion, + * or null if not found + */ + public function getLastKeptBeforeConversionVersion(): ?StoredObjectVersion + { + foreach ($this->getVersionsOrdered('DESC') as $version) { + foreach ($version->getPointInTimes() as $pointInTime) { + if (StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION === $pointInTime->getReason()) { + return $version; + } + } + } + + return null; + } + public function setTemplate(?DocGeneratorTemplate $template): StoredObject { $this->template = $template; diff --git a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectDuplicate.php b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectDuplicate.php index b1c7e7a87..f49a147d5 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectDuplicate.php +++ b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectDuplicate.php @@ -22,9 +22,18 @@ class StoredObjectDuplicate { public function __construct(private readonly StoredObjectManagerInterface $storedObjectManager, private readonly LoggerInterface $logger) {} - public function duplicate(StoredObject|StoredObjectVersion $from): StoredObject + public function duplicate(StoredObject|StoredObjectVersion $from, bool $onlyLastKeptBeforeConversionVersion = true): StoredObject { - $fromVersion = $from instanceof StoredObjectVersion ? $from : $from->getCurrentVersion(); + $storedObject = $from instanceof StoredObjectVersion ? $from->getStoredObject() : $from; + + $fromVersion = match ($storedObject->hasKeptBeforeConversionVersion() && $onlyLastKeptBeforeConversionVersion) { + true => $from->getLastKeptBeforeConversionVersion(), + false => $storedObject->getCurrentVersion(), + }; + + if (null === $fromVersion) { + throw new \UnexpectedValueException('could not find a version to restore'); + } $oldContent = $this->storedObjectManager->read($fromVersion); diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Entity/StoredObjectTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Entity/StoredObjectTest.php index 5bcfed16f..b501fe3b6 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Entity/StoredObjectTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Entity/StoredObjectTest.php @@ -12,6 +12,8 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Tests\Entity; use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Entity\StoredObjectPointInTime; +use Chill\DocStoreBundle\Entity\StoredObjectPointInTimeReasonEnum; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; /** @@ -54,4 +56,27 @@ class StoredObjectTest extends KernelTestCase self::assertNotSame($firstVersion, $version); } + + public function testHasKeptBeforeConversionVersion(): void + { + $storedObject = new StoredObject(); + $version1 = $storedObject->registerVersion(); + + self::assertFalse($storedObject->hasKeptBeforeConversionVersion()); + + // add a point in time without the correct version + new StoredObjectPointInTime($version1, StoredObjectPointInTimeReasonEnum::KEEP_BY_USER); + + self::assertFalse($storedObject->hasKeptBeforeConversionVersion()); + self::assertNull($storedObject->getLastKeptBeforeConversionVersion()); + + new StoredObjectPointInTime($version1, StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION); + + self::assertTrue($storedObject->hasKeptBeforeConversionVersion()); + // add a second version + $version2 = $storedObject->registerVersion(); + new StoredObjectPointInTime($version2, StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION); + + self::assertSame($version2, $storedObject->getLastKeptBeforeConversionVersion()); + } } diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Service/StoredObjectDuplicateTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Service/StoredObjectDuplicateTest.php index 2e9da0bab..11cb00592 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Service/StoredObjectDuplicateTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Service/StoredObjectDuplicateTest.php @@ -12,9 +12,13 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Tests\Service; use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Entity\StoredObjectPointInTime; +use Chill\DocStoreBundle\Entity\StoredObjectPointInTimeReasonEnum; use Chill\DocStoreBundle\Service\StoredObjectDuplicate; use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; use PHPUnit\Framework\TestCase; +use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; use Psr\Log\NullLogger; /** @@ -24,9 +28,13 @@ use Psr\Log\NullLogger; */ class StoredObjectDuplicateTest extends TestCase { + use ProphecyTrait; + public function testDuplicateHappyScenario(): void { $storedObject = new StoredObject(); + // we create multiple version, we want the last to be duplicated + $storedObject->registerVersion(type: 'application/test'); $version = $storedObject->registerVersion(type: $type = 'application/test'); $manager = $this->createMock(StoredObjectManagerInterface::class); @@ -45,4 +53,78 @@ class StoredObjectDuplicateTest extends TestCase self::assertNotNull($actual->getCurrentVersion()->getCreatedFrom()); self::assertSame($version, $actual->getCurrentVersion()->getCreatedFrom()); } + + public function testDuplicateWithKeptVersion(): void + { + $storedObject = new StoredObject(); + // we create two versions for stored object + // the first one is "kept before conversion", and that one should + // be duplicated, not the second one + $version1 = $storedObject->registerVersion(type: $type = 'application/test'); + new StoredObjectPointInTime($version1, StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION); + $version2 = $storedObject->registerVersion(type: $type = 'application/test'); + + $manager = $this->prophesize(StoredObjectManagerInterface::class); + + // we create both possibilities for the method "read" + $manager->read($version1)->willReturn('1234'); + $manager->read($version2)->willReturn('4567'); + + // we create the write method, and check that it is called with the content from version1, not version2 + $manager->write(Argument::type(StoredObject::class), '1234', 'application/test') + ->shouldBeCalled() + ->will(function ($args) { + /** @var StoredObject $storedObject */ + $storedObject = $args[0]; // args are ordered by key, so the first one is the stored object... + $type = $args[2]; // and the last one is the string $type + + return $storedObject->registerVersion(type: $type); + }); + + // we create the service which will duplicate things + $storedObjectDuplicate = new StoredObjectDuplicate($manager->reveal(), new NullLogger()); + + $actual = $storedObjectDuplicate->duplicate($storedObject); + + self::assertNotNull($actual->getCurrentVersion()); + self::assertNotNull($actual->getCurrentVersion()->getCreatedFrom()); + self::assertSame($version1, $actual->getCurrentVersion()->getCreatedFrom()); + } + + public function testDuplicateWithKeptVersionButWeWantToDuplicateTheLastOne(): void + { + $storedObject = new StoredObject(); + // we create two versions for stored object + // the first one is "kept before conversion", and that one should + // be duplicated, not the second one + $version1 = $storedObject->registerVersion(type: $type = 'application/test'); + new StoredObjectPointInTime($version1, StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION); + $version2 = $storedObject->registerVersion(type: $type = 'application/test'); + + $manager = $this->prophesize(StoredObjectManagerInterface::class); + + // we create both possibilities for the method "read" + $manager->read($version1)->willReturn('1234'); + $manager->read($version2)->willReturn('4567'); + + // we create the write method, and check that it is called with the content from version1, not version2 + $manager->write(Argument::type(StoredObject::class), '4567', 'application/test') + ->shouldBeCalled() + ->will(function ($args) { + /** @var StoredObject $storedObject */ + $storedObject = $args[0]; // args are ordered by key, so the first one is the stored object... + $type = $args[2]; // and the last one is the string $type + + return $storedObject->registerVersion(type: $type); + }); + + // we create the service which will duplicate things + $storedObjectDuplicate = new StoredObjectDuplicate($manager->reveal(), new NullLogger()); + + $actual = $storedObjectDuplicate->duplicate($storedObject, false); + + self::assertNotNull($actual->getCurrentVersion()); + self::assertNotNull($actual->getCurrentVersion()->getCreatedFrom()); + self::assertSame($version2, $actual->getCurrentVersion()->getCreatedFrom()); + } }