Merge remote-tracking branch 'origin/master' into issue715_household_move_email

This commit is contained in:
2023-03-06 12:20:20 +01:00
84 changed files with 1810 additions and 540 deletions

View File

@@ -1,29 +1,19 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocGeneratorBundle\Service\Generator;
use Chill\DocGeneratorBundle\Context\ContextManagerInterface;
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface;
use Chill\DocGeneratorBundle\GeneratorDriver\Exception\TemplateException;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\File\File;
use Throwable;
class Generator
class Generator implements GeneratorInterface
{
private ContextManagerInterface $contextManager;
@@ -35,6 +25,8 @@ class Generator
private StoredObjectManagerInterface $storedObjectManager;
private const LOG_PREFIX = '[docgen generator] ';
public function __construct(
ContextManagerInterface $contextManager,
DriverInterface $driver,
@@ -52,49 +44,69 @@ class Generator
/**
* @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
* @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
int $entityId,
array $contextGenerationDataNormalized,
?StoredObject $destinationStoredObject = null,
bool $isTest = false,
?File $testFile = null
): ?string {
if ($destinationStoredObject instanceof StoredObject && StoredObject::STATUS_PENDING !== $destinationStoredObject->getStatus()) {
$this->logger->info(self::LOG_PREFIX.'Aborting generation of an already generated document');
throw new ObjectReadyException();
}
$this->logger->info(self::LOG_PREFIX.'Starting generation of a document', [
'entity_id' => $entityId,
'destination_stored_object' => $destinationStoredObject === null ? null : $destinationStoredObject->getId()
]);
$context = $this->contextManager->getContextByDocGeneratorTemplate($template);
$contextGenerationData = ['test_file' => $testFile];
$entity = $this
->entityManager
->find($context->getEntityClass(), $entityId);
->find($context->getEntityClass(), $entityId)
;
if (null === $entity) {
throw new RelatedEntityNotFoundException($entityClassName, $entityId);
throw new RelatedEntityNotFoundException($template->getEntity(), $entityId);
}
$contextGenerationDataNormalized = array_merge(
$contextGenerationDataNormalized,
$context instanceof DocGeneratorContextWithPublicFormInterface ?
$context->contextGenerationDataDenormalize($template, $entity, $contextGenerationDataNormalized)
: []
);
$data = $context->getData($template, $entity, $contextGenerationDataNormalized);
$destinationStoredObjectId = $destinationStoredObject instanceof StoredObject ? $destinationStoredObject->getId() : null;
$this->entityManager->clear();
gc_collect_cycles();
if (null !== $destinationStoredObjectId) {
$destinationStoredObject = $this->entityManager->find(StoredObject::class, $destinationStoredObjectId);
}
if ($isTest && ($testFile instanceof File)) {
$dataDecrypted = file_get_contents($testFile->getPathname());
$templateDecrypted = file_get_contents($testFile->getPathname());
} else {
$dataDecrypted = $this->storedObjectManager->read($template->getFile());
$templateDecrypted = $this->storedObjectManager->read($template->getFile());
}
try {
$generatedResource = $this
->driver
->generateFromString(
$dataDecrypted,
$templateDecrypted,
$template->getFile()->getType(),
$context->getData($template, $entity, $contextGenerationData),
$data,
$template->getFile()->getFilename()
);
} catch (TemplateException $e) {
@@ -102,6 +114,11 @@ class Generator
}
if ($isTest) {
$this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [
'is_test' => true,
'entity_id' => $entityId,
'destination_stored_object' => $destinationStoredObject === null ? null : $destinationStoredObject->getId()
]);
return $generatedResource;
}
@@ -109,35 +126,18 @@ class Generator
$destinationStoredObject
->setType($template->getFile()->getType())
->setFilename(sprintf('%s_odt', uniqid('doc_', true)))
->setStatus(StoredObject::STATUS_READY);
->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();
$this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [
'entity_id' => $entityId,
'destination_stored_object' => $destinationStoredObject->getId(),
]);
return null;
}
}

View File

@@ -30,4 +30,12 @@ class GeneratorException extends RuntimeException
$previous
);
}
/**
* @return array
*/
public function getErrors(): array
{
return $this->errors;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Chill\DocGeneratorBundle\Service\Generator;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocStoreBundle\Entity\StoredObject;
use Symfony\Component\HttpFoundation\File\File;
interface GeneratorInterface
{
/**
* @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,
int $entityId,
array $contextGenerationDataNormalized,
?StoredObject $destinationStoredObject = null,
bool $isTest = false,
?File $testFile = null
): ?string;
}

View File

@@ -0,0 +1,156 @@
<?php
namespace Chill\DocGeneratorBundle\Service\Messenger;
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
use Chill\DocGeneratorBundle\Service\Generator\GeneratorException;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Contracts\Translation\TranslatorInterface;
final class OnGenerationFails implements EventSubscriberInterface
{
private DocGeneratorTemplateRepository $docGeneratorTemplateRepository;
private EntityManagerInterface $entityManager;
private LoggerInterface $logger;
private MailerInterface $mailer;
private StoredObjectRepository $storedObjectRepository;
private TranslatorInterface $translator;
private UserRepositoryInterface $userRepository;
const LOG_PREFIX = '[docgen failed] ';
/**
* @param DocGeneratorTemplateRepository $docGeneratorTemplateRepository
* @param EntityManagerInterface $entityManager
* @param LoggerInterface $logger
* @param MailerInterface $mailer
* @param StoredObjectRepository $storedObjectRepository
* @param TranslatorInterface $translator
* @param UserRepositoryInterface $userRepository
*/
public function __construct(
DocGeneratorTemplateRepository $docGeneratorTemplateRepository,
EntityManagerInterface $entityManager,
LoggerInterface $logger,
MailerInterface $mailer,
StoredObjectRepository $storedObjectRepository,
TranslatorInterface $translator,
UserRepositoryInterface $userRepository
) {
$this->docGeneratorTemplateRepository = $docGeneratorTemplateRepository;
$this->entityManager = $entityManager;
$this->logger = $logger;
$this->mailer = $mailer;
$this->storedObjectRepository = $storedObjectRepository;
$this->translator = $translator;
$this->userRepository = $userRepository;
}
public static function getSubscribedEvents()
{
return [
WorkerMessageFailedEvent::class => 'onMessageFailed'
];
}
public function onMessageFailed(WorkerMessageFailedEvent $event): void
{
if ($event->willRetry()) {
return;
}
if (!$event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) {
return;
}
/** @var \Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage $message */
$message = $event->getEnvelope()->getMessage();
$this->logger->error(self::LOG_PREFIX.'Docgen failed', [
'stored_object_id' => $message->getDestinationStoredObjectId(),
'entity_id' => $message->getEntityId(),
'template_id' => $message->getTemplateId(),
'creator_id' => $message->getCreatorId(),
'throwable_class' => get_class($event->getThrowable()),
]);
$this->markObjectAsFailed($message);
$this->warnCreator($message, $event);
}
private function markObjectAsFailed(RequestGenerationMessage $message): void
{
$object = $this->storedObjectRepository->find($message->getDestinationStoredObjectId());
if (null === $object) {
$this->logger->error(self::LOG_PREFIX.'Stored object not found', ['stored_object_id', $message->getDestinationStoredObjectId()]);
}
$object->setStatus(StoredObject::STATUS_FAILURE);
$this->entityManager->flush();
}
private function warnCreator(RequestGenerationMessage $message, WorkerMessageFailedEvent $event): void
{
if (null === $creatorId = $message->getCreatorId()) {
$this->logger->info(self::LOG_PREFIX.'creator id is null');
return;
}
if (null === $creator = $this->userRepository->find($creatorId)) {
$this->logger->error(self::LOG_PREFIX.'Creator not found with given id', ['creator_id', $creatorId]);
return;
}
if (null === $creator->getEmail() || '' === $creator->getEmail()) {
$this->logger->info(self::LOG_PREFIX.'Creator does not have any email', ['user' => $creator->getUsernameCanonical()]);
return;
}
// if the exception is not a GeneratorException, we try the previous one...
$throwable = $event->getThrowable();
if (!$throwable instanceof GeneratorException) {
$throwable = $throwable->getPrevious();
}
if ($throwable instanceof GeneratorException) {
$errors = $throwable->getErrors();
} else {
$errors = [$throwable->getTraceAsString()];
}
if (null === $template = $this->docGeneratorTemplateRepository->find($message->getTemplateId())) {
$this->logger->info(self::LOG_PREFIX.'Template not found', ['template_id' => $message->getTemplateId()]);
return;
}
$email = (new TemplatedEmail())
->to($creator->getEmail())
->subject($this->translator->trans('docgen.failure_email.The generation of a document failed'))
->textTemplate('@ChillDocGenerator/Email/on_generation_failed_email.txt.twig')
->context([
'errors' => $errors,
'template' => $template,
'creator' => $creator,
'stored_object_id' => $message->getDestinationStoredObjectId(),
]);
$this->mailer->send($email);
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Chill\DocGeneratorBundle\Service\Messenger;
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
use Chill\DocGeneratorBundle\Service\Generator\Generator;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
/**
* Handle the request of document generation
*/
class RequestGenerationHandler implements MessageHandlerInterface
{
private StoredObjectRepository $storedObjectRepository;
private DocGeneratorTemplateRepository $docGeneratorTemplateRepository;
private EntityManagerInterface $entityManager;
private Generator $generator;
public const AUTHORIZED_TRIALS = 5;
public function __construct(
DocGeneratorTemplateRepository $docGeneratorTemplateRepository,
EntityManagerInterface $entityManager,
Generator $generator,
StoredObjectRepository $storedObjectRepository
) {
$this->docGeneratorTemplateRepository = $docGeneratorTemplateRepository;
$this->entityManager = $entityManager;
$this->generator = $generator;
$this->storedObjectRepository = $storedObjectRepository;
}
public function __invoke(RequestGenerationMessage $message)
{
if (null === $template = $this->docGeneratorTemplateRepository->find($message->getTemplateId())) {
throw new \RuntimeException('template not found: ' . $message->getTemplateId());
}
if (null === $destinationStoredObject = $this->storedObjectRepository->find($message->getDestinationStoredObjectId())) {
throw new \RuntimeException('destination stored object not found : ' . $message->getDestinationStoredObjectId());
}
if ($destinationStoredObject->getGenerationTrialsCounter() >= self::AUTHORIZED_TRIALS) {
throw new UnrecoverableMessageHandlingException('maximum number of retry reached');
}
$destinationStoredObject->addGenerationTrial();
$this->entityManager->createQuery('UPDATE '.StoredObject::class.' s SET s.generationTrialsCounter = s.generationTrialsCounter + 1 WHERE s.id = :id')
->setParameter('id', $destinationStoredObject->getId())
->execute();
$this->generator->generateDocFromTemplate(
$template,
$message->getEntityId(),
$message->getContextGenerationData(),
$destinationStoredObject
);
}
}

View File

@@ -1,35 +1,35 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocGeneratorBundle\Service\Messenger;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Entity\User;
class RequestGenerationMessage
{
private int $creatorId;
private int $creatorId;
private string $entityClassName;
private int $templateId;
private int $entityId;
private int $entityId;
private int $templateId;
private int $destinationStoredObjectId;
public function __construct(User $creator, DocGeneratorTemplate $template, int $entityId, string $entityClassName)
{
private array $contextGenerationData;
public function __construct(
User $creator,
DocGeneratorTemplate $template,
int $entityId,
StoredObject $destinationStoredObject,
array $contextGenerationData
) {
$this->creatorId = $creator->getId();
$this->templateId = $template->getId();
$this->entityId = $entityId;
$this->entityClassName = $entityClassName;
$this->destinationStoredObjectId = $destinationStoredObject->getId();
$this->contextGenerationData = $contextGenerationData;
}
public function getCreatorId(): int
@@ -37,9 +37,14 @@ class RequestGenerationMessage
return $this->creatorId;
}
public function getEntityClassName(): string
public function getDestinationStoredObjectId(): int
{
return $this->entityClassName;
return $this->destinationStoredObjectId;
}
public function getTemplateId(): int
{
return $this->templateId;
}
public function getEntityId(): int
@@ -47,8 +52,8 @@ class RequestGenerationMessage
return $this->entityId;
}
public function getTemplateId(): int
public function getContextGenerationData(): array
{
return $this->templateId;
return $this->contextGenerationData;
}
}