Parcourir la source

exports: implement messenger and refactoring

Olivier Massot il y a 3 ans
Parent
commit
e2b5b36ec8

+ 2 - 1
composer.json

@@ -14,6 +14,7 @@
         "api-platform/core": "^2.6",
         "blackfire/php-sdk": "^1.23",
         "composer/package-versions-deprecated": "^1.11",
+        "doctrine/dbal": "^2.6",
         "doctrine/doctrine-bundle": "^2.1",
         "doctrine/doctrine-migrations-bundle": "^3.0",
         "doctrine/orm": "^2.9",
@@ -36,7 +37,7 @@
         "symfony/framework-bundle": "5.3.*",
         "symfony/http-client": "5.3.*",
         "symfony/intl": "5.3.*",
-        "symfony/monolog-bundle": "^3.0",
+        "symfony/monolog-bundle": "^3.7",
         "symfony/property-access": "5.3.*",
         "symfony/property-info": "5.3.*",
         "symfony/security-bundle": "5.3.*",

+ 18 - 20
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "262167aec7d8f28fda7d705a1a0df1d3",
+    "content-hash": "2cdc6dae0bb5067c949e28897d4d2931",
     "packages": [
         {
             "name": "api-platform/core",
@@ -784,38 +784,35 @@
         },
         {
             "name": "doctrine/dbal",
-            "version": "3.2.0",
+            "version": "2.13.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/dbal.git",
-                "reference": "5d54f63541d7bed1156cb5c9b79274ced61890e4"
+                "reference": "6e22f6012b42d7932674857989fcf184e9e9b1c3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/dbal/zipball/5d54f63541d7bed1156cb5c9b79274ced61890e4",
-                "reference": "5d54f63541d7bed1156cb5c9b79274ced61890e4",
+                "url": "https://api.github.com/repos/doctrine/dbal/zipball/6e22f6012b42d7932674857989fcf184e9e9b1c3",
+                "reference": "6e22f6012b42d7932674857989fcf184e9e9b1c3",
                 "shasum": ""
             },
             "require": {
-                "composer/package-versions-deprecated": "^1.11.99",
-                "doctrine/cache": "^1.11|^2.0",
+                "doctrine/cache": "^1.0|^2.0",
                 "doctrine/deprecations": "^0.5.3",
                 "doctrine/event-manager": "^1.0",
-                "php": "^7.3 || ^8.0",
-                "psr/cache": "^1|^2|^3",
-                "psr/log": "^1|^2|^3"
+                "ext-pdo": "*",
+                "php": "^7.1 || ^8"
             },
             "require-dev": {
                 "doctrine/coding-standard": "9.0.0",
                 "jetbrains/phpstorm-stubs": "2021.1",
-                "phpstan/phpstan": "1.2.0",
-                "phpstan/phpstan-strict-rules": "^1.1",
-                "phpunit/phpunit": "9.5.10",
+                "phpstan/phpstan": "1.3.0",
+                "phpunit/phpunit": "^7.5.20|^8.5|9.5.11",
                 "psalm/plugin-phpunit": "0.16.1",
-                "squizlabs/php_codesniffer": "3.6.1",
-                "symfony/cache": "^5.2|^6.0",
-                "symfony/console": "^2.0.5|^3.0|^4.0|^5.0|^6.0",
-                "vimeo/psalm": "4.13.0"
+                "squizlabs/php_codesniffer": "3.6.2",
+                "symfony/cache": "^4.4",
+                "symfony/console": "^2.0.5|^3.0|^4.0|^5.0",
+                "vimeo/psalm": "4.16.1"
             },
             "suggest": {
                 "symfony/console": "For helpful console commands such as SQL execution and import of files."
@@ -826,7 +823,7 @@
             "type": "library",
             "autoload": {
                 "psr-4": {
-                    "Doctrine\\DBAL\\": "src"
+                    "Doctrine\\DBAL\\": "lib/Doctrine/DBAL"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
@@ -869,13 +866,14 @@
                 "queryobject",
                 "sasql",
                 "sql",
+                "sqlanywhere",
                 "sqlite",
                 "sqlserver",
                 "sqlsrv"
             ],
             "support": {
                 "issues": "https://github.com/doctrine/dbal/issues",
-                "source": "https://github.com/doctrine/dbal/tree/3.2.0"
+                "source": "https://github.com/doctrine/dbal/tree/2.13.7"
             },
             "funding": [
                 {
@@ -891,7 +889,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-11-26T21:00:12+00:00"
+            "time": "2022-01-06T09:08:04+00:00"
         },
         {
             "name": "doctrine/deprecations",

+ 1 - 1
config/packages/doctrine.yaml

@@ -35,4 +35,4 @@ doctrine:
                         prefix: 'App\Entity'
                         alias: App
             adminassos:
-                connection: adminassos
+                connection: adminassos

+ 2 - 2
config/packages/messenger.yaml

@@ -6,9 +6,9 @@ framework:
         transports:
             # https://symfony.com/doc/current/messenger.html#transport-configuration
             async: '%env(MESSENGER_TRANSPORT_DSN)%'
-            failed: 'doctrine://default?queue_name=failed'
+            failed: '%env(MESSENGER_TRANSPORT_DSN)%?queue_name=failed'
             sync: 'sync://'
 
         routing:
             # Route your messages to the transports
-            'App\Message\ExportRequestMessage': async
+            'App\Message\Command\Export': async

+ 4 - 4
config/services.yaml

@@ -44,13 +44,13 @@ services:
         App\Service\Export\Encoder\EncoderInterface:
             tags: ['app.encoder']
 
-    App\Doctrine\Access\HandleCurrentAccessExtension:
+    App\Service\ServiceIterator\CurrentAccessExtensionIterator:
         - !tagged_iterator app.extensions.access
-    App\Service\Access\HandleOptionalsRoles:
+    App\Service\ServiceIterator\OptionalsRolesIterator:
         - !tagged_iterator app.optionalsroles
-    App\Service\Export\ExporterHandler:
+    App\Service\ServiceIterator\ExporterIterator:
         - !tagged_iterator app.exporter
-    App\Service\Export\Encoder\EncoderHandler:
+    App\Service\ServiceIterator\EncoderIterator:
         - !tagged_iterator app.encoder
 
     #########################################

+ 31 - 9
src/ApiResources/Export/ExportRequest.php

@@ -31,10 +31,16 @@ abstract class ExportRequest
     protected string $format;
 
     /**
-     * The access requesting this export
-     * @var Access|null
+     * The id of the access requesting this export
+     * @var int|null
      */
-    protected ?Access $requester = null;
+    protected ?int $requesterId = null;
+
+    /**
+     * Should the export be asynchrone
+     * @var bool
+     */
+    protected bool $async = true;
 
     /**
      * @return int
@@ -61,18 +67,34 @@ abstract class ExportRequest
     }
 
     /**
-     * @return Access|null
+     * @return int|null
+     */
+    public function getRequesterId(): ?int
+    {
+        return $this->requesterId;
+    }
+
+    /**
+     * @param int|null $requesterId
+     */
+    public function setRequesterId(?int $requesterId): void
+    {
+        $this->requesterId = $requesterId;
+    }
+
+    /**
+     * @return bool
      */
-    public function getRequester(): ?Access
+    public function isAsync(): bool
     {
-        return $this->requester;
+        return $this->async;
     }
 
     /**
-     * @param Access $requester
+     * @param bool $async
      */
-    public function setRequester(Access $requester): void
+    public function setAsync(bool $async): void
     {
-        $this->requester = $requester;
+        $this->async = $async;
     }
 }

+ 23 - 9
src/DataPersister/Export/LicenceCmf/ExportRequestDataPersister.php

@@ -6,17 +6,19 @@ namespace App\DataPersister\Export\LicenceCmf;
 use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
 use App\ApiResources\Export\ExportRequest;
 use App\Entity\Access\Access;
-use App\Entity\Core\File;
-use App\Service\Export\ExporterHandler;
+use App\Message\Command\Export;
+use App\Service\ServiceIterator\ExporterIterator;
 use Exception;
-use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\Messenger\MessageBusInterface;
 use Symfony\Component\Security\Core\Security;
+use Symfony\Component\HttpFoundation\Response;
 
 class ExportRequestDataPersister implements ContextAwareDataPersisterInterface
 {
     public function __construct(
         private Security $security,
-        private ExporterHandler $handler
+        private MessageBusInterface $messageBus,
+        private ExporterIterator $handler
     ) {}
 
     public function supports($data, array $context = []): bool
@@ -27,18 +29,30 @@ class ExportRequestDataPersister implements ContextAwareDataPersisterInterface
     /**
      * @param $exportRequest ExportRequest Une requête d'export
      * @param array $context
+     * @return Response
      * @throws Exception
      */
-    public function persist($exportRequest, array $context = [])
+    public function persist($exportRequest, array $context = []): Response
     {
         /** @var Access $access */
         $access = $this->security->getUser();
-        $exportRequest->setRequester($access);
+        $exportRequest->setRequesterId($access->getId());
 
-        $exportService = $this->handler->getServiceFor($exportRequest);
-        $exportService->export($exportRequest);
+        if ($exportRequest->isAsync()) {
 
-        return new JsonResponse(['ok']);
+            // Send the export request to Messenger (@see App\Message\Handler\ExportHandler)
+            $this->messageBus->dispatch(
+                new Export($exportRequest)
+            );
+            return new Response(null, 204);
+
+        } else {
+
+            $exportService = $this->handler->getExporterFor($exportRequest);
+            $file = $exportService->export($exportRequest);
+
+            return new Response('File generated: ' . $file->getId(), 200);
+        }
     }
 
     /**

+ 32 - 0
src/Message/Command/Export.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace App\Message\Command;
+
+use App\ApiResources\Export\ExportRequest;
+
+/**
+ * Transmission d'une ExportRequest au service d'export associé
+ */
+class Export
+{
+    public function __construct(
+        private ExportRequest $exportRequest
+    )
+    {}
+
+    /**
+     * @return ExportRequest
+     */
+    public function getExportRequest(): ExportRequest
+    {
+        return $this->exportRequest;
+    }
+
+    /**
+     * @param ExportRequest $exportRequest
+     */
+    public function setExportRequest(ExportRequest $exportRequest): void
+    {
+        $this->exportRequest = $exportRequest;
+    }
+}

+ 0 - 11
src/Message/ExportRequestMessage.php

@@ -1,11 +0,0 @@
-<?php
-
-namespace App\Message;
-
-/**
- * Transmission d'une ExportRequest à son service d'export
- */
-class ExportRequestMessage
-{
-
-}

+ 21 - 0
src/Message/Handler/ExportHandler.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace App\Message\Handler;
+
+use App\Message\Command\Export;
+use App\Service\ServiceIterator\ExporterIterator;
+use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
+
+class ExportHandler implements MessageHandlerInterface
+{
+    public function __construct(
+        private ExporterIterator $handler
+    ) {}
+
+    public function __invoke(Export $export)
+    {
+        $exportRequest = $export->getExportRequest();
+        $exportService = $this->handler->getExporterFor($exportRequest);
+        $exportService->export($exportRequest);
+    }
+}

+ 57 - 14
src/Service/Export/BaseExporter.php

@@ -5,27 +5,44 @@ namespace App\Service\Export;
 use App\ApiResources\Export\ExportRequest;
 use App\Entity\Core\File;
 use App\Enum\Export\ExportFormatEnum;
-use App\Service\Export\Encoder\EncoderHandler;
+use App\Repository\Access\AccessRepository;
 use App\Service\Export\Model\ExportModelInterface;
+use App\Service\ServiceIterator\EncoderIterator;
 use App\Service\Storage\TemporaryFileStorage;
+use App\Service\Utils\StringsUtils;
 use Doctrine\ORM\EntityManagerInterface;
 use Exception;
+use Psr\Log\LoggerInterface;
+use Symfony\Contracts\Service\Attribute\Required;
 use Twig\Environment;
 
 /**
  * Classe de base des services d'export
  */
-class BaseExporter implements ExporterInterface
+abstract class BaseExporter implements ExporterInterface
 {
-    const TEMPLATE = '@templates/export/licence_cmf.html.twig';
-
-    public function __construct(
-        private Environment $twig,
-        private EncoderHandler $encoderHandler,
-        private EntityManagerInterface $entityManager,
-        private TemporaryFileStorage $storage
-    )
-    {}
+    const TEMPLATE = '';
+
+    // dependencies injections
+    protected AccessRepository $accessRepository;
+    protected Environment $twig;
+    protected EncoderIterator $encoderHandler;
+    protected EntityManagerInterface $entityManager;
+    protected TemporaryFileStorage $storage;
+    protected LoggerInterface $logger;
+
+    #[Required]
+    public function setAccessRepository(AccessRepository $accessRepository) { $this->accessRepository = $accessRepository; }
+    #[Required]
+    public function setTwig(Environment $twig) { $this->twig = $twig; }
+    #[Required]
+    public function setEncoderHandler(EncoderIterator $encoderHandler) { $this->encoderHandler = $encoderHandler; }
+    #[Required]
+    public function setEntityManager(EntityManagerInterface $entityManager) { $this->entityManager = $entityManager; }
+    #[Required]
+    public function setStorage(TemporaryFileStorage $storage) { $this->storage = $storage; }
+    #[Required]
+    public function setLogger(LoggerInterface $logger) { $this->logger = $logger; }
 
     public function support(ExportRequest $exportRequest): bool
     {
@@ -62,11 +79,15 @@ class BaseExporter implements ExporterInterface
         // Met à jour l'enregistrement du fichier en base
         // <-- [refactoring] cette partie pourrait être faite en amont du service
         $file = new File();
-        $file->setOrganization($exportRequest->getRequester()->getOrganization());
+
+        $requesterId = $exportRequest->getRequesterId();
+        $organization = $this->accessRepository->find($requesterId)->getOrganization();
+
+        $file->setOrganization($organization);
         $file->setVisibility('NOBODY');
         $file->setFolder('DOCUMENTS');
         $file->setCreateDate(new \DateTime());
-        $file->setCreatedBy($exportRequest->getRequester()->getId());
+        $file->setCreatedBy($requesterId);
         // -->
         // <-- [refactoring] cette partie doit être faite après la création du fichier (storage ? service ?)
         $file->setType('LICENCE_CMF');
@@ -95,6 +116,28 @@ class BaseExporter implements ExporterInterface
         throw new Exception('not implemented error');
     }
 
+    /**
+     * Retourne le nom par défaut de cet export,
+     * utilisé pour trouver le template twig ou encore pour nommer
+     * le fichier exporté.
+     *
+     * @return string
+     */
+    protected function getBasename(): string
+    {
+        return StringsUtils::camelToSnake(
+            preg_replace('/^([\w\d]+)Exporter$/', '$1', self::class, 1)
+        );
+    }
+
+    /**
+     * Return the path of the twig template for this export
+     * @return string
+     */
+    protected function getTemplatePath(): string {
+        return '@templates/export/' . $this->getBasename() . '.html.twig';
+    }
+
     /**
      * Fait le render du template twig à partir du modèle de données
      *
@@ -137,7 +180,7 @@ class BaseExporter implements ExporterInterface
      */
     protected function getFileBasename(ExportModelInterface $model): string
     {
-        return strtolower($model::class);
+        return $this->getBasename();
     }
 
     /**

+ 7 - 11
src/Service/Export/LicenceCmfExporter.php

@@ -5,7 +5,7 @@ namespace App\Service\Export;
 
 use App\ApiResources\Export\ExportRequest;
 use App\ApiResources\Export\LicenceCmf\LicenceCmfOrganizationER;
-use App\Service\Export\Encoder\EncoderHandler;
+use App\Service\Export\Encoder\EncoderIterator;
 use App\Service\Export\Model\ExportModelInterface;
 use App\Service\Export\Model\LicenceCmf;
 use App\Enum\Access\FunctionEnum;
@@ -15,6 +15,7 @@ use App\Service\Export\Model\LicenceCmfCollection;
 use App\Service\Storage\TemporaryFileStorage;
 use App\Service\Storage\UploadStorage;
 use Doctrine\ORM\EntityManagerInterface;
+use Psr\Log\LoggerInterface;
 use Twig\Environment;
 
 /**
@@ -22,6 +23,8 @@ use Twig\Environment;
  */
 class LicenceCmfExporter extends BaseExporter implements ExporterInterface
 {
+    const TEMPLATE = '@templates/export/licence_cmf.html.twig';
+
     const CMF_ID = 12097;
 
     /**
@@ -31,17 +34,10 @@ class LicenceCmfExporter extends BaseExporter implements ExporterInterface
     const LICENCE_CMF_COLOR = [0 => '931572', 1 => 'C2981A', 2 =>  '003882', 3 =>  '27AAE1', 4 =>  '2BB673'];
 
     public function __construct(
-        private Environment $twig,
-        private EncoderHandler $encoderHandler,
-        private EntityManagerInterface $entityManager,
-        private TemporaryFileStorage $storage,
         private OrganizationRepository $organizationRepository,
-        private AccessRepository $accessRepository,
-        private UploadStorage $uploadStorage
+        private UploadStorage $uploadStorage,
     )
-    {
-        parent::__construct($this->twig, $this->encoderHandler, $this->entityManager, $this->storage);
-    }
+    {}
 
     public function support($exportRequest): bool
     {
@@ -50,7 +46,7 @@ class LicenceCmfExporter extends BaseExporter implements ExporterInterface
 
     function buildModel(ExportRequest $exportRequest): LicenceCmfCollection
     {
-        $organization = $exportRequest->getRequester()->getOrganization();
+        $organization = $this->accessRepository->find($exportRequest->getRequesterId())->getOrganization();
 
         $licenceCmf = new LicenceCmf();
         $licenceCmf->setId($organization->getId());

+ 8 - 8
src/Service/Export/Model/LicenceCmf.php

@@ -61,33 +61,33 @@ class LicenceCmf implements ExportModelInterface
 
     /**
      * Gender of the licence owner
-     * @var int
+     * @var int|null
      */
-    private int $personId;
+    private ?int $personId = null;
 
     /**
      * Gender of the licence owner
      * @var string
      */
-    private string $personGender;
+    private string $personGender = '';
 
     /**
      * First name of the licence owner
      * @var string
      */
-    private string $personFirstName;
+    private string $personFirstName = '';
 
     /**
      * Name of the licence owner
      * @var string
      */
-    private string $personLastName;
+    private string $personLastName = '';
 
     /**
      * Avatar of the person
      * @var string|null
      */
-    private ?string $personAvatarUri;
+    private ?string $personAvatarUri = '';
 
     /**
      * @return int
@@ -242,9 +242,9 @@ class LicenceCmf implements ExportModelInterface
     }
 
     /**
-     * @param int $personId
+     * @param int|null $personId
      */
-    public function setPersonId(int $personId): void
+    public function setPersonId(?int $personId): void
     {
         $this->personId = $personId;
     }

+ 2 - 2
src/Doctrine/Access/HandleCurrentAccessExtension.php → src/Service/ServiceIterator/CurrentAccessExtensionIterator.php

@@ -5,7 +5,7 @@ namespace App\Doctrine\Access;
 
 use Doctrine\ORM\QueryBuilder;
 
-class HandleCurrentAccessExtension{
+class CurrentAccessExtensionIterator{
     public function __construct(private iterable $extensions)
     { }
 
@@ -16,4 +16,4 @@ class HandleCurrentAccessExtension{
                 return $extension->addWhere($queryBuilder);
         }
     }
-}
+}

+ 4 - 3
src/Service/Export/Encoder/EncoderHandler.php → src/Service/ServiceIterator/EncoderIterator.php

@@ -1,14 +1,15 @@
 <?php
 
-namespace App\Service\Export\Encoder;
+namespace App\Service\ServiceIterator;
 
 use App\ApiResources\Export\ExportRequest;
+use App\Service\Export\Encoder\EncoderInterface;
 use Exception;
 
 /**
  * Permet d'itérer sur les services d'encodage
  */
-class EncoderHandler
+class EncoderIterator
 {
     /**
      * Pour l'injection des services, voir config/services.yaml, section 'TAG Services'
@@ -22,7 +23,7 @@ class EncoderHandler
      * Itère sur les services d'encodage disponibles et
      * retourne le premier qui supporte ce type de requête.
      *
-     * @param ExportRequest $exportRequest
+     * @param string $format
      * @return EncoderInterface
      * @throws Exception
      */

+ 4 - 3
src/Service/Export/ExporterHandler.php → src/Service/ServiceIterator/ExporterIterator.php

@@ -1,14 +1,15 @@
 <?php
 
-namespace App\Service\Export;
+namespace App\Service\ServiceIterator;
 
 use App\ApiResources\Export\ExportRequest;
+use App\Service\Export\ExporterInterface;
 use Exception;
 
 /**
  * Permet d'itérer sur les services d'export
  */
-class ExporterHandler
+class ExporterIterator
 {
     /**
      * Pour l'injection des services, voir config/services.yaml, section 'TAG Services'
@@ -26,7 +27,7 @@ class ExporterHandler
      * @return ExporterInterface
      * @throws Exception
      */
-    public function getServiceFor(ExportRequest $exportRequest): ExporterInterface
+    public function getExporterFor(ExportRequest $exportRequest): ExporterInterface
     {
         /** @var ExporterInterface $exportService */
         foreach ($this->exportServices as $exportService){

+ 2 - 2
src/Service/Access/HandleOptionalsRoles.php → src/Service/ServiceIterator/OptionalsRolesIterator.php

@@ -5,7 +5,7 @@ namespace App\Service\Access;
 
 use App\Entity\Access\Access;
 
-class HandleOptionalsRoles{
+class OptionalsRolesIterator{
 
     public function __construct(private iterable $optionalsRoles)
     { }
@@ -19,4 +19,4 @@ class HandleOptionalsRoles{
         }
         return $roles;
     }
-}
+}

+ 19 - 1
src/Service/Utils/StringsUtils.php

@@ -20,4 +20,22 @@ class StringsUtils
     public static function unquote(string $str): string {
         return str_replace("'", "", $str);
     }
-}
+
+    /**
+     * Convert CamelCase formatted string into snake_case
+     * @see https://stackoverflow.com/a/40514305/4279120
+     *
+     * @param string $string
+     * @param string $sep
+     * @return string
+     */
+    public static function camelToSnake(string $string, string $sep = "_"): string {
+        return strtolower(
+            preg_replace(
+            '/(?<=\d)(?=[A-Za-z])|(?<=[A-Za-z])(?=\d)|(?<=[a-z])(?=[A-Z])/',
+            $sep,
+            $string
+            )
+        );
+    }
+}