From bb05ba0f170bb2f3a872c6d4a9c209ec00d0ba48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 14 Feb 2023 19:35:28 +0100 Subject: [PATCH] Feature: [docgen] create a service to generate a document from a template --- .../GeneratorDriver/RelatorioDriver.php | 1 + .../Service/Generator/Generator.php | 132 +++++++++++++++++ .../Service/Generator/GeneratorException.php | 18 +++ .../Generator/ObjectReadyException.php | 11 ++ .../RelatedEntityNotFoundException.php | 14 ++ .../Messenger/RequestGenerationMessage.php | 45 ++++++ .../Context/Generator/GeneratorTest.php | 134 ++++++++++++++++++ .../Entity/StoredObject.php | 77 +++++++--- 8 files changed, 415 insertions(+), 17 deletions(-) create mode 100644 src/Bundle/ChillDocGeneratorBundle/Service/Generator/Generator.php create mode 100644 src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorException.php create mode 100644 src/Bundle/ChillDocGeneratorBundle/Service/Generator/ObjectReadyException.php create mode 100644 src/Bundle/ChillDocGeneratorBundle/Service/Generator/RelatedEntityNotFoundException.php create mode 100644 src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationMessage.php create mode 100644 src/Bundle/ChillDocGeneratorBundle/tests/Service/Context/Generator/GeneratorTest.php diff --git a/src/Bundle/ChillDocGeneratorBundle/GeneratorDriver/RelatorioDriver.php b/src/Bundle/ChillDocGeneratorBundle/GeneratorDriver/RelatorioDriver.php index 9f879f7ff..0f62931b4 100644 --- a/src/Bundle/ChillDocGeneratorBundle/GeneratorDriver/RelatorioDriver.php +++ b/src/Bundle/ChillDocGeneratorBundle/GeneratorDriver/RelatorioDriver.php @@ -53,6 +53,7 @@ final class RelatorioDriver implements DriverInterface $response = $this->client->request('POST', $this->url, [ 'headers' => $form->getPreparedHeaders()->toArray(), 'body' => $form->bodyToIterable(), + 'timeout' => '300', ]); return $response->getContent(); diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Generator/Generator.php b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/Generator.php new file mode 100644 index 000000000..755e608ba --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/Generator.php @@ -0,0 +1,132 @@ +contextManager = $contextManager; + $this->driver = $driver; + $this->entityManager = $entityManager; + $this->logger = $logger; + $this->storedObjectManager = $storedObjectManager; + } + + /** + * @template T of File|null + * @template B of bool + * @param B $isTest + * @param (B is true ? T : null) $testFile + * @psalm-return (B is true ? string : null) + * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface|\Throwable + */ + public function generateDocFromTemplate( + DocGeneratorTemplate $template, + string $entityClassName, + int $entityId, + ?StoredObject $destinationStoredObject = null, + bool $isTest = false, + ?File $testFile = null + ): ?string { + if ($destinationStoredObject instanceof StoredObject && StoredObject::STATUS_PENDING !== $destinationStoredObject->getStatus()) { + throw new ObjectReadyException(); + } + + $context = $this->contextManager->getContextByDocGeneratorTemplate($template); + $contextGenerationData = ['test_file' => $testFile]; + + $entity = $this + ->entityManager + ->find($context->getEntityClass(), $entityId) + ; + + if (null === $entity) { + throw new RelatedEntityNotFoundException($entityClassName, $entityId); + } + + if ($isTest && ($testFile instanceof File)) { + $dataDecrypted = file_get_contents($testFile->getPathname()); + } else { + $dataDecrypted = $this->storedObjectManager->read($template->getFile()); + } + + try { + $generatedResource = $this + ->driver + ->generateFromString( + $dataDecrypted, + $template->getFile()->getType(), + $context->getData($template, $entity, $contextGenerationData), + $template->getFile()->getFilename() + ); + } catch (TemplateException $e) { + throw new GeneratorException($e->getErrors(), $e); + } + + if ($isTest) { + return $generatedResource; + } + + /** @var StoredObject $storedObject */ + $destinationStoredObject + ->setType($template->getFile()->getType()) + ->setFilename(sprintf('%s_odt', uniqid('doc_', true))) + ->setStatus(StoredObject::STATUS_READY) + ; + + $this->storedObjectManager->write($destinationStoredObject, $generatedResource); + + try { + $context + ->storeGenerated( + $template, + $destinationStoredObject, + $entity, + $contextGenerationData + ); + } catch (\Exception $e) { + $this + ->logger + ->error( + 'Unable to store the associated document to entity', + [ + 'entityClassName' => $entityClassName, + 'entityId' => $entityId, + 'contextKey' => $context->getName(), + ] + ); + + throw $e; + } + + $this->entityManager->flush(); + + return null; + } +} diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorException.php b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorException.php new file mode 100644 index 000000000..423d59dd4 --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorException.php @@ -0,0 +1,18 @@ + + */ + private array $errors; + + public function __construct(array $errors = [], \Throwable $previous = null) + { + $this->errors = $errors; + parent::__construct("Could not generate the document", 15252, + $previous); + } +} diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Generator/ObjectReadyException.php b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/ObjectReadyException.php new file mode 100644 index 000000000..85c82cf95 --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/ObjectReadyException.php @@ -0,0 +1,11 @@ +creatorId = $creator->getId(); + $this->templateId = $template->getId(); + $this->entityId = $entityId; + $this->entityClassName = $entityClassName; + } + + public function getCreatorId(): int + { + return $this->creatorId; + } + + public function getTemplateId(): int + { + return $this->templateId; + } + + public function getEntityId(): int + { + return $this->entityId; + } + + public function getEntityClassName(): string + { + return $this->entityClassName; + } +} diff --git a/src/Bundle/ChillDocGeneratorBundle/tests/Service/Context/Generator/GeneratorTest.php b/src/Bundle/ChillDocGeneratorBundle/tests/Service/Context/Generator/GeneratorTest.php new file mode 100644 index 000000000..2e68b443c --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/tests/Service/Context/Generator/GeneratorTest.php @@ -0,0 +1,134 @@ +setFile($templateStoredObject = (new StoredObject()) + ->setType('application/test')); + $destinationStoredObject = (new StoredObject())->setStatus(StoredObject::STATUS_PENDING); + $entity = new class {}; + $data = []; + + $context = $this->prophesize(DocGeneratorContextInterface::class); + $context->getData($template, $entity, Argument::type('array'))->willReturn($data); + $context->storeGenerated($template, $destinationStoredObject, $entity, Argument::type('array')) + ->shouldBeCalled(); + $context->getName()->willReturn('dummy_context'); + $context->getEntityClass()->willReturn('DummyClass'); + $context = $context->reveal(); + + $contextManagerInterface = $this->prophesize(ContextManagerInterface::class); + $contextManagerInterface->getContextByDocGeneratorTemplate($template) + ->willReturn($context); + + $driver = $this->prophesize(DriverInterface::class); + $driver->generateFromString('template', 'application/test', $data, Argument::any()) + ->willReturn('generated'); + + $entityManager = $this->prophesize(EntityManagerInterface::class); + $entityManager->find(Argument::type('string'), Argument::type('int')) + ->willReturn($entity); + $entityManager->flush()->shouldBeCalled(); + + $storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class); + $storedObjectManager->read($templateStoredObject)->willReturn('template'); + $storedObjectManager->write($destinationStoredObject, 'generated')->shouldBeCalled(); + + + $generator = new Generator( + $contextManagerInterface->reveal(), + $driver->reveal(), + $entityManager->reveal(), + new NullLogger(), + $storedObjectManager->reveal() + ); + + $generator->generateDocFromTemplate( + $template, + 'DummyEntity', + 1, + $destinationStoredObject + ); + } + + public function testPreventRegenerateDocument(): void + { + $this->expectException(ObjectReadyException::class); + + $generator = new Generator( + $this->prophesize(ContextManagerInterface::class)->reveal(), + $this->prophesize(DriverInterface::class)->reveal(), + $this->prophesize(EntityManagerInterface::class)->reveal(), + new NullLogger(), + $this->prophesize(StoredObjectManagerInterface::class)->reveal() + ); + + $template = (new DocGeneratorTemplate())->setFile($templateStoredObject = (new StoredObject()) + ->setType('application/test')); + $destinationStoredObject = (new StoredObject())->setStatus(StoredObject::STATUS_READY); + + $generator->generateDocFromTemplate( + $template, + 'DummyEntity', + 1, + $destinationStoredObject + ); + } + + public function testRelatedEntityNotFound(): void + { + $this->expectException(RelatedEntityNotFoundException::class); + + $template = (new DocGeneratorTemplate())->setFile($templateStoredObject = (new StoredObject()) + ->setType('application/test')); + $destinationStoredObject = (new StoredObject())->setStatus(StoredObject::STATUS_PENDING); + + $context = $this->prophesize(DocGeneratorContextInterface::class); + $context->getName()->willReturn('dummy_context'); + $context->getEntityClass()->willReturn('DummyClass'); + $context = $context->reveal(); + + $contextManagerInterface = $this->prophesize(ContextManagerInterface::class); + $contextManagerInterface->getContextByDocGeneratorTemplate($template) + ->willReturn($context); + + $entityManager = $this->prophesize(EntityManagerInterface::class); + $entityManager->find(Argument::type('string'), Argument::type('int')) + ->willReturn(null); + + $generator = new Generator( + $contextManagerInterface->reveal(), + $this->prophesize(DriverInterface::class)->reveal(), + $entityManager->reveal(), + new NullLogger(), + $this->prophesize(StoredObjectManagerInterface::class)->reveal() + ); + + $generator->generateDocFromTemplate( + $template, + 'DummyEntity', + 1, + $destinationStoredObject + ); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php b/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php index ec2b67cf2..21abc1f56 100644 --- a/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php +++ b/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php @@ -14,6 +14,9 @@ namespace Chill\DocStoreBundle\Entity; use ChampsLibres\AsyncUploaderBundle\Model\AsyncFileInterface; use ChampsLibres\AsyncUploaderBundle\Validator\Constraints\AsyncFileExists; use ChampsLibres\WopiLib\Contract\Entity\Document; +use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; +use Chill\MainBundle\Doctrine\Model\TrackCreationInterface; +use Chill\MainBundle\Doctrine\Model\TrackCreationTrait; use DateTime; use DateTimeInterface; use Doctrine\ORM\Mapping as ORM; @@ -30,8 +33,14 @@ use Symfony\Component\Serializer\Annotation as Serializer; * message="The file is not stored properly" * ) */ -class StoredObject implements AsyncFileInterface, Document +class StoredObject implements AsyncFileInterface, Document, TrackCreationInterface { + public const STATUS_READY = "ready"; + public const STATUS_PENDING = "pending"; + public const STATUS_FAILURE = "failure"; + + use TrackCreationTrait; + /** * @ORM\Column(type="datetime", name="creation_date") * @Serializer\Groups({"read", "write"}) @@ -48,7 +57,7 @@ class StoredObject implements AsyncFileInterface, Document * @ORM\Column(type="text") * @Serializer\Groups({"read", "write"}) */ - private $filename; + private string $filename = ''; /** * @ORM\Id @@ -56,7 +65,7 @@ class StoredObject implements AsyncFileInterface, Document * @ORM\Column(type="integer") * @Serializer\Groups({"read", "write"}) */ - private $id; + private ?int $id; /** * @var int[] @@ -89,28 +98,44 @@ class StoredObject implements AsyncFileInterface, Document */ private UuidInterface $uuid; - public function __construct() + /** + * @ORM\ManyToOne(targetEntity=DocGeneratorTemplate::class) + */ + private ?DocGeneratorTemplate $template; + + /** + * @ORM\Column(type="text", options={"default": "ready"}) + */ + private string $status; + + /** + * @param StoredObject::STATUS_* $status + */ + public function __construct(string $status = "ready") { - $this->creationDate = new DateTime(); $this->uuid = Uuid::uuid4(); + $this->status = $status; } + /** + * @Serializer\Groups({"read", "write"}) + */ public function getCreationDate(): DateTime { - return $this->creationDate; + return DateTime::createFromImmutable($this->createdAt); } - public function getDatas() + public function getDatas(): array { return $this->datas; } - public function getFilename() + public function getFilename(): string { return $this->filename; } - public function getId() + public function getId(): ?int { return $this->id; } @@ -133,6 +158,11 @@ class StoredObject implements AsyncFileInterface, Document return $this->getFilename(); } + public function getStatus(): string + { + return $this->status; + } + public function getTitle() { return $this->title; @@ -153,49 +183,62 @@ class StoredObject implements AsyncFileInterface, Document return (string) $this->uuid; } - public function setCreationDate(DateTime $creationDate) + /** + * @Serializer\Groups({"write"}) + */ + public function setCreationDate(DateTime $creationDate): self { - $this->creationDate = $creationDate; + $this->createdAt = \DateTimeImmutable::createFromMutable($creationDate); return $this; } - public function setDatas(?array $datas) + public function setDatas(?array $datas): self { $this->datas = (array) $datas; return $this; } - public function setFilename(?string $filename) + public function setFilename(?string $filename): self { $this->filename = (string) $filename; return $this; } - public function setIv(?array $iv) + public function setIv(?array $iv): self { $this->iv = (array) $iv; return $this; } - public function setKeyInfos(?array $keyInfos) + public function setKeyInfos(?array $keyInfos): self { $this->keyInfos = (array) $keyInfos; return $this; } - public function setTitle(?string $title) + /** + * @param StoredObject::STATUS_* $status + */ + public function setStatus(string $status): self + { + $this->status = $status; + + return $this; + } + + public function setTitle(?string $title): self { $this->title = (string) $title; return $this; } - public function setType(?string $type) + public function setType(?string $type): self { $this->type = (string) $type;