Browse Source

working synchrone version

Olivier Massot 3 years ago
parent
commit
87a60d6180

+ 1 - 0
composer.json

@@ -26,6 +26,7 @@
         "nelmio/cors-bundle": "^2.1",
         "odolbeau/phone-number-bundle": "^3.1",
         "phpdocumentor/reflection-docblock": "^5.2",
+        "ramsey/uuid": "^4.2",
         "symfony/asset": "5.3.*",
         "symfony/console": "5.3.*",
         "symfony/doctrine-messenger": "5.3.*",

+ 238 - 1
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": "2da3b25e52397e18f617f240f9e924b3",
+    "content-hash": "65f8b5efccc1da398d50d0f44d1cf767",
     "packages": [
         {
             "name": "api-platform/core",
@@ -243,6 +243,66 @@
             },
             "time": "2021-11-17T15:47:39+00:00"
         },
+        {
+            "name": "brick/math",
+            "version": "0.9.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/brick/math.git",
+                "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/brick/math/zipball/ca57d18f028f84f777b2168cd1911b0dee2343ae",
+                "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "php": "^7.1 || ^8.0"
+            },
+            "require-dev": {
+                "php-coveralls/php-coveralls": "^2.2",
+                "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.0",
+                "vimeo/psalm": "4.9.2"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Brick\\Math\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "Arbitrary-precision arithmetic library",
+            "keywords": [
+                "Arbitrary-precision",
+                "BigInteger",
+                "BigRational",
+                "arithmetic",
+                "bigdecimal",
+                "bignum",
+                "brick",
+                "math"
+            ],
+            "support": {
+                "issues": "https://github.com/brick/math/issues",
+                "source": "https://github.com/brick/math/tree/0.9.3"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/BenMorel",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/brick/math",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-08-15T20:50:18+00:00"
+        },
         {
             "name": "composer/ca-bundle",
             "version": "1.3.1",
@@ -3433,6 +3493,183 @@
             },
             "time": "2021-05-03T11:20:27+00:00"
         },
+        {
+            "name": "ramsey/collection",
+            "version": "1.2.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ramsey/collection.git",
+                "reference": "cccc74ee5e328031b15640b51056ee8d3bb66c0a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ramsey/collection/zipball/cccc74ee5e328031b15640b51056ee8d3bb66c0a",
+                "reference": "cccc74ee5e328031b15640b51056ee8d3bb66c0a",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.3 || ^8",
+                "symfony/polyfill-php81": "^1.23"
+            },
+            "require-dev": {
+                "captainhook/captainhook": "^5.3",
+                "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
+                "ergebnis/composer-normalize": "^2.6",
+                "fakerphp/faker": "^1.5",
+                "hamcrest/hamcrest-php": "^2",
+                "jangregor/phpstan-prophecy": "^0.8",
+                "mockery/mockery": "^1.3",
+                "phpspec/prophecy-phpunit": "^2.0",
+                "phpstan/extension-installer": "^1",
+                "phpstan/phpstan": "^0.12.32",
+                "phpstan/phpstan-mockery": "^0.12.5",
+                "phpstan/phpstan-phpunit": "^0.12.11",
+                "phpunit/phpunit": "^8.5 || ^9",
+                "psy/psysh": "^0.10.4",
+                "slevomat/coding-standard": "^6.3",
+                "squizlabs/php_codesniffer": "^3.5",
+                "vimeo/psalm": "^4.4"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Ramsey\\Collection\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Ben Ramsey",
+                    "email": "ben@benramsey.com",
+                    "homepage": "https://benramsey.com"
+                }
+            ],
+            "description": "A PHP library for representing and manipulating collections.",
+            "keywords": [
+                "array",
+                "collection",
+                "hash",
+                "map",
+                "queue",
+                "set"
+            ],
+            "support": {
+                "issues": "https://github.com/ramsey/collection/issues",
+                "source": "https://github.com/ramsey/collection/tree/1.2.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/ramsey",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/ramsey/collection",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-10-10T03:01:02+00:00"
+        },
+        {
+            "name": "ramsey/uuid",
+            "version": "4.2.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ramsey/uuid.git",
+                "reference": "fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ramsey/uuid/zipball/fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df",
+                "reference": "fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df",
+                "shasum": ""
+            },
+            "require": {
+                "brick/math": "^0.8 || ^0.9",
+                "ext-json": "*",
+                "php": "^7.2 || ^8.0",
+                "ramsey/collection": "^1.0",
+                "symfony/polyfill-ctype": "^1.8",
+                "symfony/polyfill-php80": "^1.14"
+            },
+            "replace": {
+                "rhumsaa/uuid": "self.version"
+            },
+            "require-dev": {
+                "captainhook/captainhook": "^5.10",
+                "captainhook/plugin-composer": "^5.3",
+                "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
+                "doctrine/annotations": "^1.8",
+                "ergebnis/composer-normalize": "^2.15",
+                "mockery/mockery": "^1.3",
+                "moontoast/math": "^1.1",
+                "paragonie/random-lib": "^2",
+                "php-mock/php-mock": "^2.2",
+                "php-mock/php-mock-mockery": "^1.3",
+                "php-parallel-lint/php-parallel-lint": "^1.1",
+                "phpbench/phpbench": "^1.0",
+                "phpstan/extension-installer": "^1.0",
+                "phpstan/phpstan": "^0.12",
+                "phpstan/phpstan-mockery": "^0.12",
+                "phpstan/phpstan-phpunit": "^0.12",
+                "phpunit/phpunit": "^8.5 || ^9",
+                "slevomat/coding-standard": "^7.0",
+                "squizlabs/php_codesniffer": "^3.5",
+                "vimeo/psalm": "^4.9"
+            },
+            "suggest": {
+                "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.",
+                "ext-ctype": "Enables faster processing of character classification using ctype functions.",
+                "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.",
+                "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.",
+                "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter",
+                "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type."
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "4.x-dev"
+                },
+                "captainhook": {
+                    "force-install": true
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Ramsey\\Uuid\\": "src/"
+                },
+                "files": [
+                    "src/functions.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).",
+            "keywords": [
+                "guid",
+                "identifier",
+                "uuid"
+            ],
+            "support": {
+                "issues": "https://github.com/ramsey/uuid/issues",
+                "source": "https://github.com/ramsey/uuid/tree/4.2.3"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/ramsey",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-09-25T23:10:38+00:00"
+        },
         {
             "name": "ruflin/elastica",
             "version": "3.2.4",

+ 1 - 0
config/bundles.php

@@ -15,4 +15,5 @@ return [
     Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
     FOS\ElasticaBundle\FOSElasticaBundle::class => ['all' => true],
     Knp\Bundle\SnappyBundle\KnpSnappyBundle::class => ['all' => true],
+    Knp\Bundle\GaufretteBundle\KnpGaufretteBundle::class => ['all' => true],
 ];

+ 11 - 0
config/packages/knp_gaufrette.yaml

@@ -0,0 +1,11 @@
+# @see https://github.com/KnpLabs/KnpGaufretteBundle
+knp_gaufrette:
+  adapters:
+    temp:
+      local:
+        directory: '%kernel.project_dir%/var/files/temp'
+        create: true
+  filesystems:
+    temp:
+      adapter: temp
+      alias: temp

+ 3 - 0
config/services.yaml

@@ -9,6 +9,7 @@ services:
         autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
         bind:
             $opentalentConfig: '%kernel.project_dir%%env(OPENTALENT_CONFIG)%'
+            $internalFilesUploadUri: '%env(INTERNAL_FILES_DOWNLOAD_URI)%'
 
     # makes classes in src/ available to be used as services
     # this creates a service per class whose id is the fully-qualified class name
@@ -29,6 +30,8 @@ services:
     App\Service\Organization\Utils:
         public: true
 
+    Gaufrette\Filesystem: '@knp_gaufrette.filesystem_map'
+
     #########################################
     ##  TAG Services ##
     _instanceof:

+ 8 - 0
src/ApiResources/Export/ExportRequestInterface.php

@@ -0,0 +1,8 @@
+<?php
+
+namespace App\ApiResources\Export;
+
+class ExportRequestInterface
+{
+
+}

+ 11 - 0
src/ApiResources/Export/LicenceCmf/LicenceCmfOrganizationER.php

@@ -32,4 +32,15 @@ class LicenceCmfOrganizationER extends ExportRequest
      */
     #[Assert\EqualTo('pdf')]
     protected string $format = 'pdf';
+
+    /**
+     * Retourne l'année de la licence
+     * (toujours l'année courante, sauf dans le cas des tests où on pourra mocker cette méthode)
+     *
+     * @return int
+     */
+    public function getYear(): int
+    {
+        return (int)date('Y');
+    }
 }

+ 4 - 0
src/DataPersister/Export/LicenceCmf/ExportRequestDataPersister.php

@@ -6,8 +6,10 @@ 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 Exception;
+use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\Security\Core\Security;
 
 class ExportRequestDataPersister implements ContextAwareDataPersisterInterface
@@ -35,6 +37,8 @@ class ExportRequestDataPersister implements ContextAwareDataPersisterInterface
 
         $exportService = $this->handler->getServiceFor($exportRequest);
         $exportService->export($exportRequest);
+
+        return new JsonResponse(['ok']);
     }
 
     /**

+ 316 - 0
src/Entity/Core/File.php

@@ -3,8 +3,10 @@ declare(strict_types=1);
 
 namespace App\Entity\Core;
 
+use App\Entity\Organization\Organization;
 use App\Entity\Person\Person;
 use App\Repository\Core\FileRepository;
+use DateTime;
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\Common\Collections\Collection;
 use Doctrine\ORM\Mapping as ORM;
@@ -18,18 +20,140 @@ class File
     #[ORM\GeneratedValue]
     private ?int $id = null;
 
+    /**
+     * Propriétaire du fichier
+     *
+     * @var Person
+     */
+    #[ORM\ManyToOne]
+    private Person $person;
+
+    /**
+     * Organisation propriétaire du fichier
+     * @var Organization
+     */
+    #[ORM\ManyToOne]
+    private Organization $organization;
+
+    /**
+     * Slug du fichier (i.e. le chemin d'accès relatif)
+     * @var string
+     */
     #[ORM\Column(length: 255)]
     private string $slug;
 
+    /**
+     * Chemin d'accès du fichier
+     * @var string
+     */
     #[ORM\Column(length: 255)]
     private string $path;
 
+    /**
+     * Nom du fichier
+     * @var string
+     */
     #[ORM\Column(length: 255)]
     private string $name;
 
+    /**
+     * Mimetype du fichier
+     * @var string|null
+     */
     #[ORM\Column(length: 255, nullable: true)]
     private ?string $mimeType = null;
 
+    /**
+     * Visibilité du fichier (tout le monde, personne, l'organisation seulement...)
+     * @var string
+     */
+    #[ORM\Column(length: 24, options: ['default' => 'NOBODY'])]
+    private string $visibility = 'NOBODY';
+
+    /**
+     * Configuration particulière associée au fichier (exemple: image recadrée)
+     * @var string|null
+     */
+    #[ORM\Column(type: 'text', nullable: true)]
+    private ?string $config;
+
+    /**
+     * Dossier contenant le fichier
+     * @var string
+     */
+    #[ORM\Column(length: 24)]
+    private string $folder;
+
+    /**
+     * Type de document (uploaded, mail, bill...etc)
+     * @var string
+     */
+    #[ORM\Column(length: 50, options: ['default' => 'NONE'])]
+    private string $type = "NONE";
+
+    /**
+     * Taille du document en octets
+     * @var int|null
+     */
+    #[ORM\Column]
+    private ?int $size;
+
+    /**
+     * Un fichier est temporaire par exemple s'il a été généré et est stocké pour être téléchargé dans la foulée
+     * Les fichiers temporaires peuvent être supprimés sans risque, à l'inverse des fichiers uploadés par les
+     * utilisateurs par exemple.
+     *
+     * @var boolean
+     */
+    #[ORM\Column(options: ['default' => false])]
+    private bool $isTemporaryFile = false;
+
+    /**
+     * Date de création du fichier
+     * @var DateTime
+     */
+    #[ORM\Column]
+    private DateTime $createDate;
+
+    /**
+     * Id de l'access ayant créé ce fichier
+     * @var int|null
+     */
+    #[ORM\Column]
+    private ?int $createdBy;
+
+    /**
+     * Date de dernière mise à jour du fichier
+     * @var DateTime
+     */
+    #[ORM\Column]
+    private DateTime $updateDate;
+
+    /**
+     * Id de l'access ayant mis à jour ce fichier le dernier
+     * @var int|null
+     */
+    #[ORM\Column]
+    private ?int $updatedBy;
+
+//    #[ORM\Column]
+//    private ?int $eventReport_id;
+//
+//    #[ORM\Column]
+//    private ?\DateTime $availabilityDate;
+//
+//    #[ORM\Column]
+//    private ?int $documentWish_id;
+//
+//    #[ORM\Column]
+//    private ?int $onlineRegistrationSetting_id;
+//
+//    #[ORM\Column]
+//    private ?int $templateSystem_id;
+//
+//    #[ORM\Column]
+//    private ?int $work_id;
+
     #[ORM\OneToMany(mappedBy: 'image', targetEntity: Person::class, orphanRemoval: true)]
     private Collection $personImages;
 
@@ -43,6 +167,38 @@ class File
         return $this->id;
     }
 
+    /**
+     * @return Person
+     */
+    public function getPerson(): Person
+    {
+        return $this->person;
+    }
+
+    /**
+     * @param Person $person
+     */
+    public function setPerson(Person $person): void
+    {
+        $this->person = $person;
+    }
+
+    /**
+     * @return Organization
+     */
+    public function getOrganization(): Organization
+    {
+        return $this->organization;
+    }
+
+    /**
+     * @param Organization $organization
+     */
+    public function setOrganization(Organization $organization): void
+    {
+        $this->organization = $organization;
+    }
+
     public function getSlug(): string
     {
         return $this->slug;
@@ -114,4 +270,164 @@ class File
 
         return $this;
     }
+
+    /**
+     * @return string
+     */
+    public function getVisibility(): string
+    {
+        return $this->visibility;
+    }
+
+    /**
+     * @param string $visibility
+     */
+    public function setVisibility(string $visibility): void
+    {
+        $this->visibility = $visibility;
+    }
+
+    /**
+     * @return string|null
+     */
+    public function getConfig(): ?string
+    {
+        return $this->config;
+    }
+
+    /**
+     * @param string|null $config
+     */
+    public function setConfig(?string $config): void
+    {
+        $this->config = $config;
+    }
+
+    /**
+     * @return string
+     */
+    public function getFolder(): string
+    {
+        return $this->folder;
+    }
+
+    /**
+     * @param string $folder
+     */
+    public function setFolder(string $folder): void
+    {
+        $this->folder = $folder;
+    }
+
+    /**
+     * @return string
+     */
+    public function getType(): string
+    {
+        return $this->type;
+    }
+
+    /**
+     * @param string $type
+     */
+    public function setType(string $type): void
+    {
+        $this->type = $type;
+    }
+
+    /**
+     * @return int|null
+     */
+    public function getSize(): ?int
+    {
+        return $this->size;
+    }
+
+    /**
+     * @param int|null $size
+     */
+    public function setSize(?int $size): void
+    {
+        $this->size = $size;
+    }
+
+    /**
+     * @return int
+     */
+    public function getIsTemporary(): int
+    {
+        return $this->isTemporary;
+    }
+
+    /**
+     * @param int $isTemporary
+     */
+    public function setIsTemporary(int $isTemporary): void
+    {
+        $this->isTemporary = $isTemporary;
+    }
+
+    /**
+     * @return DateTime
+     */
+    public function getCreateDate(): DateTime
+    {
+        return $this->createDate;
+    }
+
+    /**
+     * @param DateTime $createDate
+     */
+    public function setCreateDate(DateTime $createDate): void
+    {
+        $this->createDate = $createDate;
+    }
+
+    /**
+     * @return int|null
+     */
+    public function getCreatedBy(): ?int
+    {
+        return $this->createdBy;
+    }
+
+    /**
+     * @param int|null $createdBy
+     */
+    public function setCreatedBy(?int $createdBy): void
+    {
+        $this->createdBy = $createdBy;
+    }
+
+    /**
+     * @return DateTime
+     */
+    public function getUpdateDate(): DateTime
+    {
+        return $this->updateDate;
+    }
+
+    /**
+     * @param DateTime $updateDate
+     */
+    public function setUpdateDate(DateTime $updateDate): void
+    {
+        $this->updateDate = $updateDate;
+    }
+
+    /**
+     * @return int|null
+     */
+    public function getUpdatedBy(): ?int
+    {
+        return $this->updatedBy;
+    }
+
+    /**
+     * @param int|null $updatedBy
+     */
+    public function setUpdatedBy(?int $updatedBy): void
+    {
+        $this->updatedBy = $updatedBy;
+    }
 }

+ 22 - 0
src/Enum/Export/ExportFormatEnum.php

@@ -14,4 +14,26 @@ class ExportFormatEnum extends Enum
     private const CSV ='csv';
     private const TXT = 'txt';
     private const XLSX = 'xlsx';
+    private const XML = 'xml';
+
+    /** @var array */
+    protected static array $mimeType = [
+        self::PDF => 'application/pdf',
+        self::CSV => 'text/csv',
+        self::TXT => 'text/plain',
+        self::XLSX => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+        self::XML => 'application/xml'
+    ];
+
+    /**
+     * @param  string $formatShortName
+     * @return string
+     */
+    public static function getMimeType($formatShortName)
+    {
+        if (!isset(static::$mimeType[$formatShortName])) {
+            return "Unknown format ($formatShortName)";
+        }
+        return static::$mimeType[$formatShortName];
+    }
 }

+ 153 - 0
src/Service/Export/BaseExporter.php

@@ -0,0 +1,153 @@
+<?php
+
+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\Service\Export\Model\ExportModelInterface;
+use App\Service\Storage\TemporaryFileStorage;
+use Doctrine\ORM\EntityManagerInterface;
+use Exception;
+use Twig\Environment;
+
+/**
+ * Classe de base des Exporter
+ */
+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
+    )
+    {}
+
+    public function support(ExportRequest $exportRequest): bool
+    {
+        return false;
+    }
+
+    /**
+     * Exécute l'opération d'export correspondant à la requête passée
+     * en paramètre
+     *
+     * @param ExportRequest $exportRequest
+     * @return File
+     * @throws Exception
+     */
+    public function export(ExportRequest $exportRequest)
+    {
+        // Génère le modèle à partir de l'exportRequest
+        $model = $this->buildModel($exportRequest);
+
+        // Génère le html à partir du template et du service
+        $html = $this->render($model);
+
+        // Encode le html au format voulu
+        $content = $this->encode($html, $exportRequest->getFormat());
+
+        // Créé le fichier dans le storage adapté
+        $filename = $this->getFileBasename($model);
+        if (!preg_match('/^.+\.' . $exportRequest->getFormat() . '$/i', $filename)) {
+            $filename .= '.' . $exportRequest->getFormat();
+        }
+
+        $path = $this->store($filename, $content);
+
+        // 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());
+        $file->setVisibility('NOBODY');
+        $file->setFolder('DOCUMENTS');
+        $file->setCreateDate(new \DateTime());
+        $file->setCreatedBy($exportRequest->getRequester()->getId());
+        // -->
+        // <-- [refactoring] cette partie doit être faite après la création du fichier (storage ? service ?)
+        $file->setType('LICENCE_CMF');
+        $file->setMimeType(ExportFormatEnum::getMimeType($exportRequest->getFormat()));
+        $file->setName($filename);
+        $file->setPath($path);
+        $file->setSlug($path);
+        // -->
+
+        $this->entityManager->persist($file);
+        $this->entityManager->flush();
+
+        // Retourne l'objet File ainsi créé
+        return $file;
+    }
+
+    /**
+     * Construit le modèle de données qui servira au render du template
+     *
+     * @param ExportRequest $exportRequest
+     * @return ExportModelInterface
+     * @throws Exception
+     */
+    function buildModel(ExportRequest $exportRequest): ExportModelInterface
+    {
+        throw new Exception('not implemented error');
+    }
+
+    /**
+     * Fait le render du template twig à partir du modèle de données
+     *
+     * @param ExportModelInterface $model
+     * @return string Rendu HTML
+     * @throws Exception
+     */
+    function render(ExportModelInterface $model): string
+    {
+        try {
+            return $this->twig->render(
+                '@templates/export/licence_cmf.html.twig',
+                ['model' => $model]
+            );
+        }
+        catch (\Twig\Error\LoaderError | \Twig\Error\RuntimeError | \Twig\Error\SyntaxError $e) {
+            throw new \Exception('error during template rendering : ' . $e);
+        }
+    }
+
+    /**
+     * Encode le html au format demandé
+     *
+     * @param string $html
+     * @param string $format @see ExportFormatEnum
+     * @return string
+     * @throws Exception
+     */
+    protected function encode(string $html, string $format): string
+    {
+        $encoder = $this->encoderHandler->getEncoderFor($format);
+        return $encoder->encode($html);
+    }
+
+    /**
+     * Retourne le nom du fichier exporté
+     *
+     * @param ExportModelInterface $model
+     * @return string
+     */
+    protected function getFileBasename(ExportModelInterface $model): string
+    {
+        return strtolower($model::class);
+    }
+
+    /**
+     * Créé le fichier
+     *
+     * @return mixed
+     * @throws Exception
+     */
+    function store(string $name, string $content): string
+    {
+        return $this->storage->write($name, $content);
+    }
+}

+ 2 - 2
src/Service/Export/Encoder/EncoderHandler.php

@@ -26,11 +26,11 @@ class EncoderHandler
      * @return EncoderInterface
      * @throws Exception
      */
-    public function getEncoderFor(ExportRequest $exportRequest): EncoderInterface
+    public function getEncoderFor(string $format): EncoderInterface
     {
         /** @var EncoderInterface $encoder */
         foreach ($this->encoders as $encoder){
-            if($encoder->support($exportRequest))
+            if($encoder->support($format))
                 return $encoder;
         }
         throw new Exception('no encoder found for this export request');

+ 1 - 1
src/Service/Export/Encoder/EncoderInterface.php

@@ -6,7 +6,7 @@ use App\ApiResources\Export\ExportRequest;
 
 interface EncoderInterface
 {
-    public function support(ExportRequest $exportRequest): bool;
+    public function support(string $format): bool;
 
     public function encode(string $html, array $options = []);
 

+ 2 - 3
src/Service/Export/Encoder/PdfEncoder.php

@@ -3,7 +3,6 @@ declare(strict_types=1);
 
 namespace App\Service\Export\Encoder;
 
-use App\ApiResources\Export\ExportRequest;
 use App\Enum\Export\ExportFormatEnum;
 use \Knp\Snappy\Pdf;
 
@@ -31,8 +30,8 @@ class PdfEncoder implements EncoderInterface
         private Pdf $knpSnappy
     ) {}
 
-    public function support(ExportRequest $exportRequest): bool {
-        return $exportRequest->getFormat() === ExportFormatEnum::PDF()->getValue();
+    public function support(string $format): bool {
+        return $format === ExportFormatEnum::PDF()->getValue();
     }
 
     /**

+ 3 - 2
src/Service/Export/ExporterInterface.php

@@ -12,10 +12,11 @@ interface ExporterInterface
 {
     /**
      * Le service supporte-t-il ce type d'ExportRequest
+     *
      * @param ExportRequest $exportRequest
      * @return boolean
      */
-    public function support($exportRequest): bool;
+    public function support(ExportRequest $exportRequest): bool;
 
     /**
      * Exécute l'opération d'export correspondant à la requête passée
@@ -23,5 +24,5 @@ interface ExporterInterface
      *
      * @param ExportRequest $exportRequest
      */
-    public function export($exportRequest);
+    public function export(ExportRequest $exportRequest);
 }

+ 43 - 44
src/Service/Export/LicenceCmfExporter.php

@@ -6,18 +6,21 @@ 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\Model\ExportModelInterface;
 use App\Service\Export\Model\LicenceCmf;
 use App\Enum\Access\FunctionEnum;
 use App\Repository\Access\AccessRepository;
 use App\Repository\Organization\OrganizationRepository;
+use App\Service\Export\Model\LicenceCmfCollection;
 use App\Service\Storage\TemporaryFileStorage;
-use App\Service\Utils\Path;
+use App\Service\Storage\UploadStorage;
+use Doctrine\ORM\EntityManagerInterface;
 use Twig\Environment;
 
 /**
  * Exporte la licence CMF de la structure ou du ou des access, au format demandé
  */
-class LicenceCmfExporter implements ExporterInterface
+class LicenceCmfExporter extends BaseExporter implements ExporterInterface
 {
     const CMF_ID = 12097;
 
@@ -28,86 +31,82 @@ class LicenceCmfExporter 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 Environment $twig,
-        private EncoderHandler $encoderHandler
+        private UploadStorage $uploadStorage
     )
-    {}
+    {
+        parent::__construct($this->twig, $this->encoderHandler, $this->entityManager, $this->storage);
+    }
 
     public function support($exportRequest): bool
     {
         return $exportRequest instanceof LicenceCmfOrganizationER;
     }
 
-    /**
-     *
-     */
-    public function export($exportRequest)
+    function buildModel(ExportRequest $exportRequest): LicenceCmfCollection
     {
         $organization = $exportRequest->getRequester()->getOrganization();
-        $currentYear = (int)date('Y');
 
-        $model = new LicenceCmf();
-        $model->setId($organization->getId());
-        $model->setYear($currentYear);
-        $model->setOrganizationName($organization->getName());
-        $model->setOrganizationIdentifier($organization->getIdentifier());
+        $licenceCmf = new LicenceCmf();
+        $licenceCmf->setId($organization->getId());
+        $licenceCmf->setYear($exportRequest->getYear());
+        $licenceCmf->setIsOrganizationLicence( $exportRequest instanceof LicenceCmfOrganizationER);
+        $licenceCmf->setOrganizationName($organization->getName());
+        $licenceCmf->setOrganizationIdentifier($organization->getIdentifier());
 
         $parentFederation = $organization->getNetworkOrganizations()[0]->getParent();
-        $model->setFederationName($parentFederation->getName());
+        $licenceCmf->setFederationName($parentFederation->getName());
 
-        $model->setColor(
-            $this->getLicenceColor($currentYear)
+        $licenceCmf->setColor(
+            $this->getLicenceColor($exportRequest->getYear())
         );
 
         $logoId = $organization->getLogo()?->getId();
         if ($logoId) {
-            $model->setLogoUri(
-                rtrim($_SERVER['INTERNAL_FILES_DOWNLOAD_URI'], '/') . '/' . $logoId
+            $licenceCmf->setLogoUri(
+                $this->uploadStorage->getUri($logoId)
             );
         }
 
         $presidents = $this->accessRepository->findByOrganizationAndMission($organization, FunctionEnum::PRESIDENT()->getValue());
         if (count($presidents) > 0) {
             $president = $presidents[0]->getPerson();
-            $model->setPersonId($president->getId());
-            $model->setPersonGender($president->getGender());
-            $model->setPersonFirstName($president->getGivenName());
-            $model->setPersonLastName($president->getName());
+            $licenceCmf->setPersonId($president->getId());
+            $licenceCmf->setPersonGender($president->getGender());
+            $licenceCmf->setPersonFirstName($president->getGivenName());
+            $licenceCmf->setPersonLastName($president->getName());
         }
 
         $cmf = $this->organizationRepository->find(self::CMF_ID);
         $qrCodeId = $cmf->getParameters()?->getQrCode()?->getId();
         if ($qrCodeId) {
-            $model->setQrCodeUri(
-                rtrim($_SERVER['INTERNAL_FILES_DOWNLOAD_URI'], '/') . '/' . $qrCodeId
+            $licenceCmf->setQrCodeUri(
+                $this->uploadStorage->getUri($qrCodeId)
             );
         }
 
-        $html = $this->twig->render(
-            '@templates/export/licence_cmf.html.twig',
-            [
-                'models' => [$model],
-                'isOrganizationLicence' => true
-            ]
-        );
-
-        $filename = 'licence_cmf_' . $currentYear . '.pdf';
-        $tempDir = TemporaryFileStorage::makeStorageDirFor($organization->getId());
-        $tempPath = Path::join($tempDir, $filename);
-
-        if (file_exists($tempPath)) {
-            unlink($tempPath);
-        }
-
-        $encoder = $this->encoderHandler->getEncoderFor($exportRequest);
-        $encoder->encodeToFile($html, $tempPath);
+        $model = new LicenceCmfCollection();
+        $model->setLicences([$licenceCmf]);
+        return $model;
+    }
 
+    /**
+     * @param LicenceCmfCollection $model
+     * @return string
+     */
+    protected function getFileBasename(ExportModelInterface $model): string
+    {
+        return 'licence_cmf_' . $model->getLicences()[0]->getYear() . '.pdf';
     }
 
     /**
      * Retourne la couleur de licence pour l'année donnée
+     *
      * @param int $year
      * @return string
      */

+ 5 - 0
src/Service/Export/Model/ExportModelInterface.php

@@ -0,0 +1,5 @@
+<?php
+
+namespace App\Service\Export\Model;
+
+interface ExportModelInterface {}

+ 23 - 1
src/Service/Export/Model/LicenceCmf.php

@@ -3,7 +3,7 @@ declare(strict_types=1);
 
 namespace App\Service\Export\Model;
 
-class LicenceCmf
+class LicenceCmf implements ExportModelInterface
 {
     /**
      * An id for the licence
@@ -17,6 +17,12 @@ class LicenceCmf
      */
     private int $year;
 
+    /**
+     * Is this the licence of an organization and not of a person?
+     * @var bool
+     */
+    private bool $isOrganizationLicence;
+
     /**
      * Name of the organization
      * @var string
@@ -115,6 +121,22 @@ class LicenceCmf
         $this->year = $year;
     }
 
+    /**
+     * @return bool
+     */
+    public function isOrganizationLicence(): bool
+    {
+        return $this->isOrganizationLicence;
+    }
+
+    /**
+     * @param bool $isOrganizationLicence
+     */
+    public function setIsOrganizationLicence(bool $isOrganizationLicence): void
+    {
+        $this->isOrganizationLicence = $isOrganizationLicence;
+    }
+
     /**
      * @return string
      */

+ 37 - 0
src/Service/Export/Model/LicenceCmfCollection.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace App\Service\Export\Model;
+
+class LicenceCmfCollection implements ExportModelInterface
+{
+    /**
+     * Les différentes licences contenues dans la collection
+     *
+     * @var array
+     */
+    private array $licences;
+
+    /**
+     * @return array
+     */
+    public function getLicences(): array
+    {
+        return $this->licences;
+    }
+
+    /**
+     * @param array $licences
+     */
+    public function setLicences(array $licences): void
+    {
+        $this->licences = $licences;
+    }
+
+    /**
+     * @param LicenceCmf $licence
+     */
+    public function addLicence(LicenceCmf $licence): void
+    {
+        $this->licences[] = $licence;
+    }
+}

+ 8 - 4
src/Service/Storage/FileStorage.php

@@ -4,15 +4,19 @@ declare(strict_types=1);
 namespace App\Service\Storage;
 
 use App\Service\Utils\Path;
+use Knp\Bundle\GaufretteBundle\FilesystemMap;
 
 /**
  * Base class for file storage
  */
 abstract class FileStorage
 {
-    protected static function getStorageBaseDir(): string {
-        return Path::join(Path::getAppDir(), 'var', 'files');
-    }
-
+    public function __construct(
+        protected FilesystemMap $filesystem
+    )
+    {}
 
+    protected function getStorageBaseDir(): string {
+        return Path::join(Path::getProjectDir(), 'var', 'files');
+    }
 }

+ 16 - 29
src/Service/Storage/TemporaryFileStorage.php

@@ -4,6 +4,8 @@ declare(strict_types=1);
 namespace App\Service\Storage;
 
 use App\Service\Utils\Path;
+use Exception;
+use Ramsey\Uuid\Uuid;
 
 /**
  * Gère le stockage des fichiers temporaires, comme les documents générés par les utilisateurs
@@ -11,43 +13,28 @@ use App\Service\Utils\Path;
  */
 class TemporaryFileStorage extends FileStorage
 {
-    protected static function getStorageBaseDir(): string {
-        return Path::join(parent::getStorageBaseDir(), 'temp');
+    protected function getRelativeStorageBaseDir(): string {
+        return 'temp';
     }
 
-    /**
-     * Return the path of the temporary storage dir for the given organization and access
-     *
-     * @param int $organizationId
-     * @param int|null $accessId
-     * @return string
-     */
-    protected static function getStorageDirFor(int $organizationId, ?int $accessId = null): string
-    {
-        return Path::join(
-            self::getStorageBaseDir(),
-            date('ymd'),
-            (string)$organizationId,
-            (string)$accessId ?? ''
-        );
+    protected function getStorageBaseDir(): string {
+        // TODO: remplacer par une reference à config/packages/knp_gaufrette.yaml
+        return Path::join(parent::getStorageBaseDir(), $this->getRelativeStorageBaseDir());
     }
 
     /**
-     * Recursively make the temporary storage dir for the given organization and access
-     * and return its path
-     *
-     * (If the directory already exists, simply returns its path)
+     * Write the given content to a temporary file
      *
-     * @param int $organizationId
-     * @param int|null $accessId
+     * @param string $filename
+     * @param string $content
      * @return string
+     * @throws Exception
      */
-    public static function makeStorageDirFor(int $organizationId, ?int $accessId = null): string
+    public function write(string $filename, string $content): string
     {
-        $dirPath = self::getStorageDirFor($organizationId, $accessId);
-        if (!file_exists($dirPath)) {
-            mkdir($dirPath, 0775, true);
-        }
-        return $dirPath;
+        $filePath = Path::join(Uuid::uuid4(), $filename);
+        $this->filesystem->get('temp')->getAdapter()->write($filePath, $content);
+
+        return Path::join('temp', $filePath);
     }
 }

+ 22 - 0
src/Service/Storage/UploadStorage.php

@@ -1,8 +1,30 @@
 <?php
+declare(strict_types=1);
 
 namespace App\Service\Storage;
 
+/**
+ * Gère l'upload et le téléchargement de fichiers par les utilisateurs
+ *
+ * Pour la durée de la migration vers Symfony 5, la gestion des fichiers est déléguée à l'ancienne API
+ *
+ */
 class UploadStorage
 {
+    public function __construct(private string $internalFilesUploadUri)
+    {}
+
+    private function getBaseDownloadUri(): string {
+        return $this->internalFilesUploadUri;
+    }
+
+    private static function getUploadUri(): string
+    {
+        return '';
+    }
+
+    public function getUri(int $fileId): string {
+        return rtrim(self::getBaseDownloadUri(), '/') . '/' . $fileId;
+    }
 
 }

+ 1 - 1
src/Service/Utils/Path.php

@@ -13,7 +13,7 @@ class Path
      *
      * @return string
      */
-    public static function getAppDir(): string
+    public static function getProjectDir(): string
     {
         return dirname(__file__, 4);
     }

+ 9 - 0
symfony.lock

@@ -19,6 +19,9 @@
     "blackfire/php-sdk": {
         "version": "v1.23.0"
     },
+    "brick/math": {
+        "version": "0.9.3"
+    },
     "composer/ca-bundle": {
         "version": "1.2.8"
     },
@@ -239,6 +242,12 @@
     "psr/log": {
         "version": "1.1.3"
     },
+    "ramsey/collection": {
+        "version": "1.2.2"
+    },
+    "ramsey/uuid": {
+        "version": "4.2.3"
+    },
     "ruflin/elastica": {
         "version": "3.2"
     },

+ 25 - 25
templates/export/licence_cmf.html.twig

@@ -166,8 +166,8 @@
 <body>
 
 {% block content %}
-    {% for model in models %}
-        <page data-iri="{{ model.id }}">
+    {% for licence in model.licences %}
+        <page data-iri="{{ licence.id }}">
             <table width="793" border="0" cellspacing="0" cellpadding="0">
                 <tbody>
                 <tr>
@@ -178,7 +178,7 @@
                                 <td width="340" class="relative">
                                     <img src="{{ asset('static/cmf_licence.png') }}"
                                             width="170" height="86"/>
-                                    <span id="year_head">{{ model.year }}</span>
+                                    <span id="year_head">{{ licence.year }}</span>
                                 </td>
                                 <td width="340">
                                     <div align="right">
@@ -195,12 +195,12 @@
                     <td><p class="Style7"></p>
                         <p class="Style7"></p>
                         <p class="Style7">
-                            {{ (model.personGender ~ '_long')| trans }} {{ model.personLastName }} {{ model.personFirstName }}
+                            {{ (licence.personGender ~ '_long')| trans }} {{ licence.personLastName }} {{ licence.personFirstName }}
                             ,</p>
 
                         <p class="Style7"></p>
                         <p class="Style8">Vous trouverez ci-joint votre <strong>Licence CMF pour
-                                l’année {{ model.year }}</strong>.
+                                l’année {{ licence.year }}</strong>.
                         </p>
                         <p class="Style8">Vous pouvez : </p>
                         <ul class="Style8">
@@ -217,7 +217,7 @@
                         </p>
                         <p class="Style8">
                             Nous vous rappelons que la Licence CMF, personnelle et unique, <br/>
-                            est <strong>valable jusqu’au 31 décembre de l’année {{ model.year }}</strong>.
+                            est <strong>valable jusqu’au 31 décembre de l’année {{ licence.year }}</strong>.
                         </p>
                         <p class="Style8"></p>
                         <hr/>
@@ -230,20 +230,20 @@
                 <table class="card_dimension" align="center" cellpadding="0" cellspacing="0">
                     <tbody>
                     <tr>
-                        <td height="26" colspan="3" align="center" valign="bottom" bgcolor="{{ model.color }}">
+                        <td height="26" colspan="3" align="center" valign="bottom" bgcolor="{{ licence.color }}">
                             <div align="center"><span class="Style1">Licence CMF</span></div>
                         </td>
                     </tr>
                     <tr class="up">
-                        {% if isOrganizationLicence %}
+                        {% if licence.isOrganizationLicence %}
                             <td width="80" id="avatar">
                                 <div align="center">
-                                    {% if(model.logoUri is null) %}
+                                    {% if(licence.logoUri is null) %}
                                         <img src="{{ asset('public/static/picto_face.png') }}"
                                              width="85"
                                              height="82"/>
                                     {% else %}
-                                        <img src="{{ model.logoUri }}"
+                                        <img src="{{ licence.logoUri }}"
                                              width="85"
                                              height="82"/>
                                     {% endif %}
@@ -251,34 +251,34 @@
                             </td>
                             <td colspan="2">
                                 <span class="Style2">
-                                    <p>{{ model.personLastName }} {{ model.personFirstName }}</p>
-                                    <p>{{ model.organizationName }}</p>
-                                    <p>{{ model.federationName }}</p>
-                                    <p>N° : {{ model.organizationIdentifier }}</p>
-                                    <p>Licence valable jusqu’au 31/12/{{ model.year }}</p>
+                                    <p>{{ licence.personLastName }} {{ licence.personFirstName }}</p>
+                                    <p>{{ licence.organizationName }}</p>
+                                    <p>{{ licence.federationName }}</p>
+                                    <p>N° : {{ licence.organizationIdentifier }}</p>
+                                    <p>Licence valable jusqu’au 31/12/{{ licence.year }}</p>
                                 </span>
                             </td>
                         {% else %}
                             <td width="80" id="avatar">
                                 <div align="center">
-                                    {% if(model.personAvatarUri is null) %}
+                                    {% if(licence.personAvatarUri is null) %}
                                         <img
                                                 src="{{ asset('public/static/picto_face.png') }}"
                                                 width="85"
                                                 height="82"/>
                                     {% else %}
                                         <img class="avatar"
-                                             src="{{ asset(model.personAvatarUri) }}"/>
+                                             src="{{ asset(licence.personAvatarUri) }}"/>
                                     {% endif %}
                                 </div>
                             </td>
                             <td colspan="2">
                                 <span class="Style2">
-                                   <p>{{ model.personLastName }} {{ model.personFirstName }}</p>
-                                   <p>{{ model.organizationName }}</p>
+                                   <p>{{ licence.personLastName }} {{ licence.personFirstName }}</p>
+                                   <p>{{ licence.organizationName }}</p>
                                    <p>?</p>
-                                  <p>N° : {{ model.organizationIdentifier }}-{{ model.personId }}</p>
-                                <p>Licence valable jusqu’au 31/12/{{ model.year }}</p>
+                                  <p>N° : {{ licence.organizationIdentifier }}-{{ licence.personId }}</p>
+                                <p>Licence valable jusqu’au 31/12/{{ licence.year }}</p>
                                 </span>
                             </td>
                         {% endif %}
@@ -290,7 +290,7 @@
                             <div align="center">
                                 <img src="{{ asset('public/static/cmf_licence.png') }}"
                                      height="45"/>
-                                <span id="year_card">{{ model.year }}</span>
+                                <span id="year_card">{{ licence.year }}</span>
                             </div>
                         </td>
                         <td width="140" align="right" valign="middle">
@@ -302,9 +302,9 @@
                             </div>
                         </td>
                         <td width="70" align="right" valign="middle" id="qrCode">
-                            {% if(model.qrCodeUri is not null) %}
+                            {% if(licence.qrCodeUri is not null) %}
                                 <img style="margin-right: 10px;"
-                                     src="{{ asset(model.qrCodeUri) }}"
+                                     src="{{ asset(licence.qrCodeUri) }}"
                                      alt=""
                                      width="65" height="65"/>
                             {% endif %}
@@ -312,7 +312,7 @@
                     </tr>
 
                     <tr>
-                        <td colspan="3" align="center" bgcolor="{{ model.color }}"><span class="Style3">CMF ● cmf@cmf-musique.org ● 01 55 58 22 82 ● www.cmf-musique.org</span>
+                        <td colspan="3" align="center" bgcolor="{{ licence.color }}"><span class="Style3">CMF ● cmf@cmf-musique.org ● 01 55 58 22 82 ● www.cmf-musique.org</span>
                         </td>
                     </tr>