소스 검색

Merge branch 'v8-6215/remplacer/wkhtlkox-par-phpdocx' into develop

Olivier Massot 1 년 전
부모
커밋
2f8cb77d9d

+ 3 - 3
composer.json

@@ -8,7 +8,7 @@
     },
     {
       "type": "vcs",
-      "url": "ssh://git@gitlab.2iopenservice.com/opentalent/phpdocx.git"
+      "url": "ssh://git@gitlab.2iopenservice.com/opentalent/phpdocx-15.5.git"
     }
   ],
   "require": {
@@ -23,6 +23,7 @@
     "doctrine/doctrine-bundle": "^2.1",
     "doctrine/doctrine-migrations-bundle": "^3.0",
     "doctrine/orm": "^2.17",
+    "dompdf/dompdf": "^3.0",
     "egulias/email-validator": "^3.0",
     "jbouzekri/phumbor-bundle": "^3.1.0",
     "knplabs/knp-gaufrette-bundle": "^0.8.0",
@@ -113,8 +114,7 @@
       "App\\": "src/"
     },
     "classmap": [
-      "vendor/opentalent/phpdocx/Classes/Phpdocx/Create/CreateDocx.php",
-      "vendor/opentalent/phpdocx/Classes/Phpdocx/Create/CreateDocxFromTemplate.php"
+      "vendor/opentalent/phpdocx/Classes/Phpdocx"
     ]
   },
   "autoload-dev": {

+ 2 - 2
config/packages/knp_gaufrette.yaml

@@ -4,10 +4,10 @@ knp_gaufrette:
   adapters:
     storage:
       local:
-        directory: '%kernel.project_dir%/storage'
+        directory: '%kernel.project_dir%/var/files/storage'
         create: true
   filesystems:
     storage:
       adapter: storage
 
-  stream_wrapper: ~
+  stream_wrapper: ~

+ 11 - 2
config/services.yaml

@@ -21,8 +21,10 @@ services:
             $persistProcessor: '@api_platform.doctrine.orm.state.persist_processor'
             $removeProcessor: '@api_platform.doctrine.orm.state.remove_processor'
             $opentalentNoReplyEmailAddress: 'noreply@opentalent.fr'
-            $legacyBaseUrl: '%env(PUBLIC_API_LEG_BASE_URL)%'
-            $baseUrl: '%env(PUBLIC_API_BASE_URL)%'
+            $legacyBaseUrl: '%env(API_LEG_BASE_URL)%'
+            $publicLegacyBaseUrl: '%env(PUBLIC_API_LEG_BASE_URL)%'
+            $baseUrl: '%env(API_BASE_URL)%'
+            $publicBaseUrl: '%env(PUBLIC_API_BASE_URL)%'
 
     # makes classes in src/ available to be used as services
     # this creates a service per class whose id is the fully-qualified class name
@@ -48,6 +50,13 @@ services:
     App\Service\Network\Utils:
         public: true
 
+    Phpdocx\Create\CreateDocx:
+        class: Phpdocx\Create\CreateDocx
+
+#    App\Service\Export\Encoder\PdfEncoder:
+#        arguments:
+#            $phpDocx: '@Phpdocx\Create\CreateDocx'
+
     App\Service\Organization\Utils:
         public: true
 

+ 22 - 9
src/Service/Export/Encoder/DocXEncoder.php

@@ -5,34 +5,47 @@ declare(strict_types=1);
 namespace App\Service\Export\Encoder;
 
 use App\Enum\Export\ExportFormatEnum;
+use App\Service\Utils\FileUtils;
 use Phpdocx\Create\CreateDocx;
+use Throwable;
 
 /**
  * Encode HTML to docx format.
  */
 class DocXEncoder implements EncoderInterface
 {
+    public function __construct(
+        private readonly CreateDocx $phpDocx,
+        private readonly FileUtils $fileUtils
+    ) {}
+
     public function support(string $format): bool
     {
         return $format === ExportFormatEnum::DOCX->value;
     }
 
     //  TODO: resolve Phpstan errors
+
     /**
      * Encode the given HTML content into docX, and
      * return the encoded content.
      *
-     * @param array<mixed> $options
+     * @param array<string, mixed> $options
+     * @throws Throwable
      */
     public function encode(string $html, array $options = []): string
     {
-        //    $docx = new CreateDocx();
-        //    $docx->embedHTML($html);
-        //    $tempFile = tempnam(sys_get_temp_dir(), 'docx');
-        //    $docx->createDocx($tempFile);
-        //    $content = file_get_contents($tempFile);
-        //    unlink($tempFile);
-        //    return $content;
-        return '';
+        $tempFilename = $this->fileUtils->getTempFilename('docx');
+
+        // @see https://www.phpdocx.com/documentation/introduction/html-to-word-PHP#
+        $this->phpDocx->embedHTML($html);
+
+        try {
+            $this->phpDocx->createDocx($tempFilename);
+
+            return $this->fileUtils->getFileContent($tempFilename);
+        } finally {
+            $this->fileUtils->unlinkIfExist($tempFilename);
+        }
     }
 }

+ 24 - 35
src/Service/Export/Encoder/PdfEncoder.php

@@ -5,32 +5,21 @@ declare(strict_types=1);
 namespace App\Service\Export\Encoder;
 
 use App\Enum\Export\ExportFormatEnum;
-use Knp\Snappy\Pdf;
+use App\Service\Utils\Path;
+use Dompdf\Dompdf;
+use Dompdf\Options;
 
 /**
  * Encode HTML to PDF.
  */
 class PdfEncoder implements EncoderInterface
 {
-    /**
-     * Default encoding options.
-     *
-     * @see https://wkhtmltopdf.org/libwkhtmltox/
-     *
-     * @var array<mixed>
-     */
-    private array $defaultOptions = [
-        'margin-top' => 35,
-        'margin-right' => 10,
-        'margin-bottom' => 15,
-        'margin-left' => 15,
-        'header-spacing' => 5,
-        'enable-local-file-access' => true,
-    ];
-
-    public function __construct(
-        private readonly Pdf $knpSnappy,
-    ) {
+    protected Options $domPdfOptions;
+    protected Dompdf $dompdf;
+
+    public function __construct() {
+        $this->domPdfOptions = new Options();
+        $this->dompdf = new Dompdf();
     }
 
     public function support(string $format): bool
@@ -39,25 +28,25 @@ class PdfEncoder implements EncoderInterface
     }
 
     /**
-     * Default encoding options.
+     * Converts the provided HTML content into a PDF document.
      *
-     * @return array<mixed>
-     */
-    public function getDefaultOptions()
-    {
-        return $this->defaultOptions;
-    }
-
-    /**
-     * Encode the given HTML content into PDF, and
-     * return the encoded content.
+     * @param string $html The HTML content to be converted to PDF.
+     * @param array<string, mixed> $options Optional configuration settings for the PDF generation
+     *                                      @see https://github.com/dompdf/dompdf/blob/master/src/Options.php
      *
-     * @param array<mixed> $options
+     * @return string The generated PDF content as a string.
      */
     public function encode(string $html, array $options = []): string
     {
-        $options = array_merge($this->getDefaultOptions(), $options);
-
-        return $this->knpSnappy->getOutputFromHtml($html, $options);
+        $this->domPdfOptions->setIsRemoteEnabled(true);
+        $this->domPdfOptions->setChroot(Path::getProjectDir() . '/public');
+        $this->domPdfOptions->setDefaultPaperOrientation('portrait');
+        $this->domPdfOptions->setDefaultPaperSize('A4');
+        $this->domPdfOptions->set($options);
+
+        $this->dompdf->setOptions($this->domPdfOptions);
+        $this->dompdf->loadHtml($html);
+        $this->dompdf->render();
+        return $this->dompdf->output();
     }
 }

+ 5 - 1
src/Service/File/Storage/ApiLegacyStorage.php

@@ -20,6 +20,7 @@ class ApiLegacyStorage implements FileStorageInterface
         protected readonly DataManager $dataManager,
         protected readonly UrlBuilder $urlBuilder,
         protected readonly string $legacyBaseUrl,
+        protected readonly string $publicLegacyBaseUrl
     ) {
     }
 
@@ -45,7 +46,10 @@ class ApiLegacyStorage implements FileStorageInterface
     {
         $url = sprintf('api/files/%s/download/%s?relativePath=1', $file->getId(), $size);
 
-        return UrlBuilder::concat($this->legacyBaseUrl, [$this->apiLegacyRequestService->getContent($url)], []);
+        // L'url interne est l'équivalent d'un chemin relatif dans ce cas
+        $baseUrl = $relativePath ? $this->legacyBaseUrl : $this->publicLegacyBaseUrl;
+
+        return UrlBuilder::concat($baseUrl, [$this->apiLegacyRequestService->getContent($url)], []);
     }
 
     public function support(File $file): bool

+ 1 - 1
src/Service/Twig/AssetsExtension.php

@@ -57,6 +57,6 @@ class AssetsExtension extends AbstractExtension
      */
     public function fileImagePath(File $file, string $size): string
     {
-        return $this->fileManager->getImageUrl($file, $size, true);
+        return ltrim($this->fileManager->getImageUrl($file, $size, true), '/');
     }
 }

+ 33 - 0
src/Service/Utils/FileUtils.php

@@ -6,6 +6,7 @@ namespace App\Service\Utils;
 
 use App\Entity\Core\File;
 use Mimey\MimeTypes;
+use RuntimeException;
 
 class FileUtils
 {
@@ -45,4 +46,36 @@ class FileUtils
 
         return boolval(preg_match('#^image#', $mimetype));
     }
+
+    /**
+     * Génère un nom de fichier temporaire situé dans le répertoire var/tmp,
+     * avec l'extension et le préfixe donnés.
+     *
+     * @param string $ext
+     * @param string $prefix
+     * @return string
+     * @throws RuntimeException
+     */
+    public function getTempFilename(string $ext = 'tmp', string $prefix = ''): string
+    {
+        if (empty($ext)) {
+            throw new RuntimeException('Extension can not be empty');
+        }
+        $tempDir = Path::getProjectDir() . '/var/tmp';
+        if (!is_dir($tempDir)) {
+            mkdir($tempDir);
+        }
+        return $tempDir . '/' . $prefix . uniqid() . '.' . $ext;
+    }
+
+    public function unlinkIfExist(string $path): void
+    {
+        if (file_exists($path)) {
+            unlink($path);
+        }
+    }
+
+    public function getFileContent(string $path): string {
+        return file_get_contents($path);
+    }
 }

+ 3 - 2
src/Service/Utils/UrlBuilder.php

@@ -94,9 +94,10 @@ class UrlBuilder
     /**
      * Retourne l'URL relative sans le scheme et l'host.
      */
-    public function getRelativeUrl(string $path): string
+    public function getRelativeUrl(string $url): string
     {
-        return UrlGenerator::getRelativePath($this->baseUrl, $path);
+        $parts = parse_url($url);
+        return ($parts['path'] ?? '') . (isset($parts['query']) ? '?'.$parts['query'] : '');
     }
 
     /**

+ 29 - 27
templates/export/licence_cmf.html.twig

@@ -6,16 +6,14 @@
 
         {% block style %}
         html {
-            width: 21cm;
-            height: 100%;
+            word-wrap: break-word;
         }
 
         @page {
-            margin: 180px 50px;
+            margin: 130px 50px 30px 50px;
         }
 
         body {
-            height: 100%;
         }
 
         .Style1 {
@@ -70,8 +68,8 @@
 
         #year_head {
             position: absolute;
-            bottom: 10px;
-            left: 100px;
+            top: 30px;
+            left: 90px;
             color: #9d1348;
             font-size: 25px;
             font-weight: bold;
@@ -84,7 +82,7 @@
 
         #year_card {
             position: absolute;
-            bottom: 75px;
+            bottom: 94px;
             left: 55px;
             color: #9d1348;
             font-size: 14px;
@@ -127,7 +125,7 @@
             z-index: 2;
         }
 
-        #scissor{
+        #scissor {
             position: absolute;
             top: 0;
             left: 15px;
@@ -158,7 +156,6 @@
             integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN"
             crossorigin="anonymous"
     />
-
     <title>Licence CMF</title>
 </head>
 
@@ -166,21 +163,23 @@
 {% block content %}
     {% for licence in model.licences %}
         <page data-iri="{{ licence.id }}">
-            <table width="793" border="0" cellspacing="0" cellpadding="0">
+            <table width="500" border="0" cellspacing="0" cellpadding="0">
                 <tbody>
                 <tr>
-                    <td width="693">
-                        <table width="680" border="0" align="center" cellpadding="0" cellspacing="0">
+                    <td width="500">
+                        <table width="500" align="center" border="0" cellpadding="0" cellspacing="0">
                             <tbody>
                             <tr>
-                                <td width="340" class="relative">
-                                    <img src="{{ absPath('images/cmf_licence.png') }}"
+                                <td width="250" class="relative">
+                                    <img src="{{ 'images/cmf_licence.png' }}"
                                             width="170" height="86"/>
-                                    <span id="year_head">{{ licence.year }}</span>
+                                    <span id="year_head">
+                                        {{ licence.year }}
+                                    </span>
                                 </td>
-                                <td width="340">
+                                <td width="250">
                                     <div align="right">
-                                        <img src="{{ absPath('images/cmf-reseau.png') }}"
+                                        <img src="{{ 'images/cmf-reseau.png' }}"
                                                 width="200" height="86"/>
                                     </div>
                                 </td>
@@ -192,7 +191,7 @@
                 <tr>
                     <td>
                         <p class="Style7">
-                            {% if licence.personGender %}{{ (licence.personGender ~ '_LONG') | trans }}{% endif %}
+                           {% if licence.personGender %}{{ (licence.personGender.value ~ '_LONG') | trans }}{% endif %}
                             {{ licence.personLastName }} {{ licence.personFirstName }}
                             ,</p>
 
@@ -234,10 +233,10 @@
                     </tr>
                     <tr class="up">
                         {% if licence.isOrganizationLicence %}
-                            <td width="80" id="avatar">
+                            <td height="82" width="80" id="avatar">
                                 <div align="center">
                                     {% if(licence.logo is null) %}
-                                        <img src="{{ absPath('images/picto_face.png') }}"
+                                        <img src="{{ 'images/picto_face.png' }}"
                                              width="85"
                                              height="82"/>
                                     {% else %}
@@ -257,11 +256,11 @@
                                 </span>
                             </td>
                         {% else %}
-                            <td width="80" id="avatar">
+                            <td height="26" width="80" id="avatar">
                                 <div align="center">
                                     {% if(licence.personAvatar is null) %}
                                         <img
-                                                src="{{ absPath('images/picto_face.png') }}"
+                                                src="{{ 'images/picto_face.png' }}"
                                                 width="85"
                                                 height="82"/>
                                     {% else %}
@@ -283,10 +282,10 @@
                     </tr>
 
                     <tr class="bottom">
-                        <td width="70" valign="middle"
+                        <td height="45" width="70" valign="middle"
                             style="vertical-align: top;">
                             <div align="center">
-                                <img src="{{ absPath('images/cmf_licence.png') }}"
+                                <img src="{{ 'images/cmf_licence.png' }}"
                                      height="45"/>
                                 <span id="year_card">{{ licence.year }}</span>
                             </div>
@@ -299,7 +298,7 @@
                                 <p> ou flashez ce code</p></span>
                             </div>
                         </td>
-                        <td width="70" align="right" valign="middle" id="qrCode">
+                        <td height="65" width="70" align="right" valign="middle" id="qrCode">
                             {% if(licence.qrCode) %}
                                 <img style="margin-right: 10px;"
                                      src="{{ fileImagePath(licence.qrCode, 'sm') }}"
@@ -310,7 +309,8 @@
                     </tr>
 
                     <tr>
-                        <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 height="26" 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>
 
@@ -322,7 +322,7 @@
             </div>
 
 
-            <table width="793" border="0" cellspacing="0" cellpadding="0">
+            <table width="500" border="0" cellspacing="0" cellpadding="0">
                 <tbody>
                 <tr>
                     <td></td>
@@ -338,7 +338,9 @@
                 </tr>
                 </tbody>
             </table>
+            {% if model.licences|length > 1 %}
             <div class="page_break"></div>
+            {% endif %}
         </page>
     {% endfor %}
 {% endblock content %}

+ 105 - 0
tests/Unit/Service/Export/Encoder/DocXEncoderTest.php

@@ -0,0 +1,105 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Tests\Unit\Service\Export\Encoder;
+
+use App\Service\Export\Encoder\DocXEncoder;
+use App\Service\Utils\FileUtils;
+use Phpdocx\Create\CreateDocx;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+
+class DocXEncoderTest extends TestCase
+{
+    private MockObject | CreateDocx $phpDocx;
+    private MockObject | FileUtils $fileUtils;
+
+    public function setUp(): void {
+        $this->phpDocx = $this->getMockBuilder(CreateDocx::class)->disableOriginalConstructor()->getMock();
+        $this->fileUtils = $this->getMockBuilder(FileUtils::class)->disableOriginalConstructor()->getMock();
+    }
+
+    public function testSupport(): void
+    {
+        $encoder = $this->getMockBuilder(DocXEncoder::class)
+            ->setConstructorArgs([$this->phpDocx, $this->fileUtils])
+            ->setMethodsExcept(['support'])
+            ->getMock();
+
+        $this->assertTrue($encoder->support('docx'));
+        $this->assertFalse($encoder->support('txt'));
+    }
+
+    public function testEncode(): void {
+        $encoder = $this->getMockBuilder(DocXEncoder::class)
+            ->setConstructorArgs([$this->phpDocx, $this->fileUtils])
+            ->setMethodsExcept(['encode'])
+            ->getMock();
+
+        $this->fileUtils
+            ->expects(self::once())
+            ->method('getTempFilename')
+            ->with('docx')
+            ->willReturn('tmp/temp.docx');
+
+        $this->phpDocx
+            ->expects($this->once())
+            ->method('embedHtml')
+            ->with('<div>content</div>');
+
+        $this->phpDocx
+            ->expects(self::once())
+            ->method('createDocx')
+            ->with('tmp/temp.docx');
+
+        $this->fileUtils
+            ->expects(self::once())
+            ->method('getFileContent')
+            ->with('tmp/temp.docx')
+            ->willReturn('%%encoded%%');
+
+        $this->fileUtils
+            ->expects(self::once())
+            ->method('unlinkIfExist')
+            ->with('tmp/temp.docx');
+
+        $this->assertEquals(
+            '%%encoded%%',
+            $encoder->encode('<div>content</div>')
+        );
+    }
+
+    public function testEncodeWithError(): void {
+        $encoder = $this->getMockBuilder(DocXEncoder::class)
+            ->setConstructorArgs([$this->phpDocx, $this->fileUtils])
+            ->setMethodsExcept(['encode'])
+            ->getMock();
+
+        $this->fileUtils
+            ->method('getTempFilename')
+            ->with('docx')
+            ->willReturn('tmp/temp.docx');
+
+        $this->phpDocx
+            ->method('embedHtml')
+            ->with('<div>content</div>');
+
+        $this->phpDocx
+            ->method('createDocx')
+            ->with('tmp/temp.docx');
+
+        $this->fileUtils
+            ->method('getFileContent')
+            ->willThrowException(new \Exception('error'));
+
+        $this->fileUtils
+            ->expects(self::once())
+            ->method('unlinkIfExist')
+            ->with('tmp/temp.docx');
+
+        $this->expectException(\Exception::class);
+        $this->expectExceptionMessage('error');
+
+        $encoder->encode('<div>content</div>');
+    }
+}

+ 30 - 29
tests/Unit/Service/Export/Encoder/PdfEncoderTest.php

@@ -3,26 +3,32 @@
 namespace App\Tests\Unit\Service\Export\Encoder;
 
 use App\Service\Export\Encoder\PdfEncoder;
+use App\Service\Utils\Path;
+use Dompdf\Dompdf;
+use Dompdf\Options;
 use Knp\Snappy\Pdf;
 use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
 
-class PdfEncoderTest extends TestCase
-{
-    private MockObject|Pdf $knpSnappy;
+class TestablePdfEncoder extends PdfEncoder {
+    public function setDomPdfOptions(Options $options): void {
+        $this->domPdfOptions = $options;
+    }
 
-    public function setUp(): void
-    {
-        $this->knpSnappy = $this->getMockBuilder(Pdf::class)->getMock();
+    public function setDomPdf(Dompdf $dompdf): void {
+        $this->dompdf = $dompdf;
     }
+}
 
+class PdfEncoderTest extends TestCase
+{
     /**
      * @see PdfEncoder::support()
      */
     public function testSupport(): void
     {
         $encoder = $this->getMockBuilder(PdfEncoder::class)
-            ->setConstructorArgs([$this->knpSnappy])
+            ->disableOriginalConstructor()
             ->setMethodsExcept(['support'])
             ->getMock();
 
@@ -30,36 +36,31 @@ class PdfEncoderTest extends TestCase
         $this->assertFalse($encoder->support('txt'));
     }
 
-    /**
-     * @see PdfEncoder::getDefaultOptions()
-     */
-    public function testGetDefaultOptions(): void
-    {
-        $encoder = $this->getMockBuilder(PdfEncoder::class)
-            ->setConstructorArgs([$this->knpSnappy])
-            ->setMethodsExcept(['getDefaultOptions'])
-            ->getMock();
-
-        $this->assertIsArray($encoder->getDefaultOptions());
-    }
-
     /**
      * @see PdfEncoder::encode()
      */
     public function testEncode(): void
     {
-        $encoder = $this->getMockBuilder(PdfEncoder::class)
-            ->setConstructorArgs([$this->knpSnappy])
-            ->setMethodsExcept(['encode'])
+        $encoder = $this->getMockBuilder(TestablePdfEncoder::class)
+            ->disableOriginalConstructor()
+            ->setMethodsExcept(['encode', 'setDomPdfOptions', 'setDomPdf'])
             ->getMock();
 
-        $encoder->method('getDefaultOptions')->willReturn(['defaultOption' => 1]);
+        $domPdfOptions = $this->getMockBuilder(Options::class)->disableOriginalConstructor()->getMock();
+        $domPdf = $this->getMockBuilder(Dompdf::class)->disableOriginalConstructor()->getMock();
+        $encoder->setDomPdfOptions($domPdfOptions);
+        $encoder->setDomPdf($domPdf);
+
+        $domPdfOptions->expects(self::once())->method('setIsRemoteEnabled')->with(true);
+        $domPdfOptions->expects(self::once())->method('setChroot')->with(Path::getProjectDir() . '/public');
+        $domPdfOptions->expects(self::once())->method('setDefaultPaperOrientation')->with('portrait');
+        $domPdfOptions->expects(self::once())->method('setDefaultPaperSize')->with('A4');
+        $domPdfOptions->expects(self::once())->method('set')->with(['additionalOption' => 2]);
 
-        $this->knpSnappy
-            ->expects(self::once())
-            ->method('getOutputFromHtml')
-            ->with('<div>content</div>', ['defaultOption' => 1, 'additionalOption' => 2])
-            ->willReturn('%%encoded%%');
+        $domPdf->expects(self::once())->method('setOptions')->with($domPdfOptions);
+        $domPdf->expects(self::once())->method('loadHtml')->with('<div>content</div>');
+        $domPdf->expects(self::once())->method('render');
+        $domPdf->expects(self::once())->method('output')->willReturn('%%encoded%%');
 
         $this->assertEquals(
             '%%encoded%%',

+ 1 - 1
tests/Unit/Service/File/Storage/ApiLegacyStorageTest.php

@@ -42,7 +42,7 @@ class ApiLegacyStorageTest extends TestCase
 
         $apiLegacyStorage = $this
             ->getMockBuilder(ApiLegacyStorage::class)
-            ->setConstructorArgs([$apiLegacyRequestService, $dataManager, $urlBuilder, 'url'])
+            ->setConstructorArgs([$apiLegacyRequestService, $dataManager, $urlBuilder, 'url', 'publicUrl'])
             ->setMethodsExcept(['read'])
             ->getMock();