Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
 
3
declare(strict_types=1);
4
 
5
namespace OpenSpout\Writer\XLSX\Helper;
6
 
7
use DateTimeImmutable;
8
use OpenSpout\Common\Exception\IOException;
9
use OpenSpout\Common\Helper\Escaper\XLSX;
10
use OpenSpout\Common\Helper\FileSystemHelper as CommonFileSystemHelper;
11
use OpenSpout\Writer\Common\Entity\Sheet;
12
use OpenSpout\Writer\Common\Entity\Worksheet;
13
use OpenSpout\Writer\Common\Helper\CellHelper;
14
use OpenSpout\Writer\Common\Helper\FileSystemWithRootFolderHelperInterface;
15
use OpenSpout\Writer\Common\Helper\ZipHelper;
16
use OpenSpout\Writer\XLSX\Manager\Style\StyleManager;
17
use OpenSpout\Writer\XLSX\MergeCell;
18
use OpenSpout\Writer\XLSX\Options;
1441 ariadna 19
use OpenSpout\Writer\XLSX\Properties;
1 efrain 20
 
21
/**
22
 * @internal
23
 */
24
final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface
25
{
26
    public const RELS_FOLDER_NAME = '_rels';
27
    public const DRAWINGS_FOLDER_NAME = 'drawings';
28
    public const DOC_PROPS_FOLDER_NAME = 'docProps';
29
    public const XL_FOLDER_NAME = 'xl';
30
    public const WORKSHEETS_FOLDER_NAME = 'worksheets';
31
 
32
    public const RELS_FILE_NAME = '.rels';
33
    public const APP_XML_FILE_NAME = 'app.xml';
34
    public const CORE_XML_FILE_NAME = 'core.xml';
1441 ariadna 35
    public const CUSTOM_XML_FILE_NAME = 'custom.xml';
1 efrain 36
    public const CONTENT_TYPES_XML_FILE_NAME = '[Content_Types].xml';
37
    public const WORKBOOK_XML_FILE_NAME = 'workbook.xml';
38
    public const WORKBOOK_RELS_XML_FILE_NAME = 'workbook.xml.rels';
39
    public const STYLES_XML_FILE_NAME = 'styles.xml';
40
 
41
    private const SHEET_XML_FILE_HEADER = <<<'EOD'
42
        <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
43
        <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
44
        EOD;
45
 
46
    private readonly string $baseFolderRealPath;
47
    private readonly CommonFileSystemHelper $baseFileSystemHelper;
48
 
49
    /** @var ZipHelper Helper to perform tasks with Zip archive */
50
    private readonly ZipHelper $zipHelper;
51
 
1441 ariadna 52
    /** @var Properties document properties */
53
    private readonly Properties $properties;
1 efrain 54
 
55
    /** @var XLSX Used to escape XML data */
56
    private readonly XLSX $escaper;
57
 
58
    /** @var string Path to the root folder inside the temp folder where the files to create the XLSX will be stored */
59
    private string $rootFolder;
60
 
61
    /** @var string Path to the "_rels" folder inside the root folder */
62
    private string $relsFolder;
63
 
64
    /** @var string Path to the "docProps" folder inside the root folder */
65
    private string $docPropsFolder;
66
 
67
    /** @var string Path to the "xl" folder inside the root folder */
68
    private string $xlFolder;
69
 
70
    /** @var string Path to the "_rels" folder inside the "xl" folder */
71
    private string $xlRelsFolder;
72
 
73
    /** @var string Path to the "worksheets" folder inside the "xl" folder */
74
    private string $xlWorksheetsFolder;
75
 
76
    /** @var string Path to the temp folder, inside the root folder, where specific sheets content will be written to */
77
    private string $sheetsContentTempFolder;
78
 
79
    /**
1441 ariadna 80
     * @param string     $baseFolderPath The path of the base folder where all the I/O can occur
81
     * @param ZipHelper  $zipHelper      Helper to perform tasks with Zip archive
82
     * @param XLSX       $escaper        Used to escape XML data
83
     * @param Properties $properties     document properies
1 efrain 84
     */
1441 ariadna 85
    public function __construct(string $baseFolderPath, ZipHelper $zipHelper, XLSX $escaper, Properties $properties)
1 efrain 86
    {
87
        $this->baseFileSystemHelper = new CommonFileSystemHelper($baseFolderPath);
88
        $this->baseFolderRealPath = $this->baseFileSystemHelper->getBaseFolderRealPath();
89
        $this->zipHelper = $zipHelper;
90
        $this->escaper = $escaper;
1441 ariadna 91
        $this->properties = $properties;
1 efrain 92
    }
93
 
94
    public function createFolder(string $parentFolderPath, string $folderName): string
95
    {
96
        return $this->baseFileSystemHelper->createFolder($parentFolderPath, $folderName);
97
    }
98
 
99
    public function createFileWithContents(string $parentFolderPath, string $fileName, string $fileContents): string
100
    {
101
        return $this->baseFileSystemHelper->createFileWithContents($parentFolderPath, $fileName, $fileContents);
102
    }
103
 
104
    public function deleteFile(string $filePath): void
105
    {
106
        $this->baseFileSystemHelper->deleteFile($filePath);
107
    }
108
 
109
    public function deleteFolderRecursively(string $folderPath): void
110
    {
111
        $this->baseFileSystemHelper->deleteFolderRecursively($folderPath);
112
    }
113
 
114
    public function getRootFolder(): string
115
    {
116
        return $this->rootFolder;
117
    }
118
 
119
    public function getXlFolder(): string
120
    {
121
        return $this->xlFolder;
122
    }
123
 
124
    public function getXlWorksheetsFolder(): string
125
    {
126
        return $this->xlWorksheetsFolder;
127
    }
128
 
129
    public function getSheetsContentTempFolder(): string
130
    {
131
        return $this->sheetsContentTempFolder;
132
    }
133
 
134
    /**
135
     * Creates all the folders needed to create a XLSX file, as well as the files that won't change.
136
     *
137
     * @throws IOException If unable to create at least one of the base folders
138
     */
139
    public function createBaseFilesAndFolders(): void
140
    {
141
        $this
142
            ->createRootFolder()
143
            ->createRelsFolderAndFile()
144
            ->createDocPropsFolderAndFiles()
145
            ->createXlFolderAndSubFolders()
146
            ->createSheetsContentTempFolder()
147
        ;
148
    }
149
 
150
    /**
151
     * Creates the "[Content_Types].xml" file under the root folder.
152
     *
153
     * @param Worksheet[] $worksheets
154
     */
155
    public function createContentTypesFile(array $worksheets): self
156
    {
157
        $contentTypesXmlFileContents = <<<'EOD'
158
            <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
159
            <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
160
                <Default ContentType="application/xml" Extension="xml"/>
161
                <Default ContentType="application/vnd.openxmlformats-package.relationships+xml" Extension="rels"/>
162
                <Default ContentType="application/vnd.openxmlformats-officedocument.vmlDrawing" Extension="vml"/>
163
                <Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" PartName="/xl/workbook.xml"/>
164
            EOD;
165
 
166
        /** @var Worksheet $worksheet */
167
        foreach ($worksheets as $worksheet) {
168
            $contentTypesXmlFileContents .= '<Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" PartName="/xl/worksheets/sheet'.$worksheet->getId().'.xml"/>';
169
            $contentTypesXmlFileContents .= '<Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml" PartName="/xl/comments'.$worksheet->getId().'.xml" />';
170
        }
171
 
172
        $contentTypesXmlFileContents .= <<<'EOD'
1441 ariadna 173
            <Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml" PartName="/xl/styles.xml"/>
174
            <Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml" PartName="/xl/sharedStrings.xml"/>
175
            <Override ContentType="application/vnd.openxmlformats-package.core-properties+xml" PartName="/docProps/core.xml"/>
176
            <Override ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml" PartName="/docProps/app.xml"/>
177
            EOD;
178
 
179
        if ([] !== $this->properties->customProperties) {
180
            $contentTypesXmlFileContents .= <<<'EOD'
181
                <Override ContentType="application/vnd.openxmlformats-officedocument.custom-properties+xml" PartName="/docProps/custom.xml" />
182
                EOD;
183
        }
184
 
185
        $contentTypesXmlFileContents .= <<<'EOD'
1 efrain 186
            </Types>
187
            EOD;
188
 
189
        $this->createFileWithContents($this->rootFolder, self::CONTENT_TYPES_XML_FILE_NAME, $contentTypesXmlFileContents);
190
 
191
        return $this;
192
    }
193
 
194
    /**
195
     * Creates the "workbook.xml" file under the "xl" folder.
196
     *
197
     * @param Worksheet[] $worksheets
198
     */
1441 ariadna 199
    public function createWorkbookFile(Options $options, array $worksheets): self
1 efrain 200
    {
201
        $workbookXmlFileContents = <<<'EOD'
202
            <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
203
            <workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
1441 ariadna 204
            EOD;
205
 
206
        if (null !== $options->getWorkbookProtection()) {
207
            $workbookXmlFileContents .= $options->getWorkbookProtection()->getXml();
208
        }
209
 
210
        $workbookXmlFileContents .= <<<'EOD'
1 efrain 211
                <sheets>
212
            EOD;
213
 
214
        /** @var Worksheet $worksheet */
215
        foreach ($worksheets as $worksheet) {
216
            $worksheetName = $worksheet->getExternalSheet()->getName();
217
            $worksheetVisibility = $worksheet->getExternalSheet()->isVisible() ? 'visible' : 'hidden';
218
            $worksheetId = $worksheet->getId();
219
            $workbookXmlFileContents .= '<sheet name="'.$this->escaper->escape($worksheetName).'" sheetId="'.$worksheetId.'" r:id="rIdSheet'.$worksheetId.'" state="'.$worksheetVisibility.'"/>';
220
        }
221
 
222
        $workbookXmlFileContents .= <<<'EOD'
223
                </sheets>
224
            EOD;
225
 
226
        $definedNames = '';
227
 
228
        /** @var Worksheet $worksheet */
229
        foreach ($worksheets as $worksheet) {
230
            $sheet = $worksheet->getExternalSheet();
231
            if (null !== $autofilter = $sheet->getAutoFilter()) {
232
                $worksheetName = $sheet->getName();
1441 ariadna 233
                $name = \sprintf(
1 efrain 234
                    '\'%s\'!$%s$%s:$%s$%s',
235
                    $this->escaper->escape($worksheetName),
236
                    CellHelper::getColumnLettersFromColumnIndex($autofilter->fromColumnIndex),
237
                    $autofilter->fromRow,
238
                    CellHelper::getColumnLettersFromColumnIndex($autofilter->toColumnIndex),
239
                    $autofilter->toRow
240
                );
241
                $definedNames .= '<definedName function="false" hidden="true" localSheetId="'.$sheet->getIndex().'" name="_xlnm._FilterDatabase" vbProcedure="false">'.$name.'</definedName>';
242
            }
243
            if (null !== $printTitleRows = $sheet->getPrintTitleRows()) {
244
                $definedNames .= '<definedName name="_xlnm.Print_Titles" localSheetId="'.$sheet->getIndex().'">'.$this->escaper->escape($sheet->getName()).'!'.$printTitleRows.'</definedName>';
245
            }
246
        }
247
        if ('' !== $definedNames) {
248
            $workbookXmlFileContents .= '<definedNames>'.$definedNames.'</definedNames>';
249
        }
250
 
251
        $workbookXmlFileContents .= <<<'EOD'
252
            </workbook>
253
            EOD;
254
 
255
        $this->createFileWithContents($this->xlFolder, self::WORKBOOK_XML_FILE_NAME, $workbookXmlFileContents);
256
 
257
        return $this;
258
    }
259
 
260
    /**
261
     * Creates the "workbook.xml.res" file under the "xl/_res" folder.
262
     *
263
     * @param Worksheet[] $worksheets
264
     */
265
    public function createWorkbookRelsFile(array $worksheets): self
266
    {
267
        $workbookRelsXmlFileContents = <<<'EOD'
268
            <?xml version="1.0" encoding="UTF-8"?>
269
            <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
270
                <Relationship Id="rIdStyles" Target="styles.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"/>
271
                <Relationship Id="rIdSharedStrings" Target="sharedStrings.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"/>
272
            EOD;
273
 
274
        /** @var Worksheet $worksheet */
275
        foreach ($worksheets as $worksheet) {
276
            $worksheetId = $worksheet->getId();
277
            $workbookRelsXmlFileContents .= '<Relationship Id="rIdSheet'.$worksheetId.'" Target="worksheets/sheet'.$worksheetId.'.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"/>';
278
        }
279
 
280
        $workbookRelsXmlFileContents .= '</Relationships>';
281
 
282
        $this->createFileWithContents($this->xlRelsFolder, self::WORKBOOK_RELS_XML_FILE_NAME, $workbookRelsXmlFileContents);
283
 
284
        return $this;
285
    }
286
 
287
    /**
288
     * Create the "rels" file for a given worksheet. This contains relations to the comments.xml and drawing.vml files for this worksheet.
289
     *
290
     * @param Worksheet[] $worksheets
291
     */
292
    public function createWorksheetRelsFiles(array $worksheets): self
293
    {
294
        $this->createFolder($this->getXlWorksheetsFolder(), self::RELS_FOLDER_NAME);
295
 
296
        foreach ($worksheets as $worksheet) {
297
            $worksheetId = $worksheet->getId();
298
            $worksheetRelsContent = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
299
              <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
300
                <Relationship Id="rId_comments_vml1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" Target="../drawings/vmlDrawing'.$worksheetId.'.vml"/>
301
                <Relationship Id="rId_comments1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" Target="../comments'.$worksheetId.'.xml"/>
302
              </Relationships>';
303
 
304
            $folder = $this->getXlWorksheetsFolder().\DIRECTORY_SEPARATOR.'_rels';
305
            $filename = 'sheet'.$worksheetId.'.xml.rels';
306
 
307
            $this->createFileWithContents($folder, $filename, $worksheetRelsContent);
308
        }
309
 
310
        return $this;
311
    }
312
 
313
    /**
314
     * Creates the "styles.xml" file under the "xl" folder.
315
     */
316
    public function createStylesFile(StyleManager $styleManager): self
317
    {
318
        $stylesXmlFileContents = $styleManager->getStylesXMLFileContent();
319
        $this->createFileWithContents($this->xlFolder, self::STYLES_XML_FILE_NAME, $stylesXmlFileContents);
320
 
321
        return $this;
322
    }
323
 
324
    /**
325
     * Creates the "content.xml" file under the root folder.
326
     *
327
     * @param Worksheet[] $worksheets
328
     */
329
    public function createContentFiles(Options $options, array $worksheets): self
330
    {
331
        $allMergeCells = $options->getMergeCells();
332
        $pageSetup = $options->getPageSetup();
333
        foreach ($worksheets as $worksheet) {
334
            $contentXmlFilePath = $this->getXlWorksheetsFolder().\DIRECTORY_SEPARATOR.basename($worksheet->getFilePath());
335
            $worksheetFilePointer = fopen($contentXmlFilePath, 'w');
336
            \assert(false !== $worksheetFilePointer);
337
 
338
            $sheet = $worksheet->getExternalSheet();
339
            fwrite($worksheetFilePointer, self::SHEET_XML_FILE_HEADER);
340
 
341
            // AutoFilter tags
342
            if (null !== $autofilter = $sheet->getAutoFilter()) {
343
                if (isset($pageSetup) && $pageSetup->fitToPage) {
344
                    fwrite($worksheetFilePointer, '<sheetPr filterMode="false"><pageSetUpPr fitToPage="true"/></sheetPr>');
345
                } else {
346
                    fwrite($worksheetFilePointer, '<sheetPr filterMode="false"><pageSetUpPr fitToPage="false"/></sheetPr>');
347
                }
348
            } elseif (isset($pageSetup) && $pageSetup->fitToPage) {
349
                fwrite($worksheetFilePointer, '<sheetPr><pageSetUpPr fitToPage="true"/></sheetPr>');
350
            }
1441 ariadna 351
            $sheetRange = \sprintf(
352
                '%s%s:%s%s',
353
                CellHelper::getColumnLettersFromColumnIndex(0),
354
                1,
355
                CellHelper::getColumnLettersFromColumnIndex($worksheet->getMaxNumColumns() - 1),
356
                $worksheet->getLastWrittenRowIndex()
357
            );
358
            fwrite($worksheetFilePointer, \sprintf('<dimension ref="%s"/>', $sheetRange));
1 efrain 359
            if (null !== ($sheetView = $sheet->getSheetView())) {
360
                fwrite($worksheetFilePointer, '<sheetViews>'.$sheetView->getXml().'</sheetViews>');
361
            }
362
            fwrite($worksheetFilePointer, $this->getXMLFragmentForDefaultCellSizing($options));
363
            fwrite($worksheetFilePointer, $this->getXMLFragmentForColumnWidths($options, $sheet));
364
            fwrite($worksheetFilePointer, '<sheetData>');
365
 
366
            $worksheetFilePath = $worksheet->getFilePath();
367
            $this->copyFileContentsToTarget($worksheetFilePath, $worksheetFilePointer);
368
            fwrite($worksheetFilePointer, '</sheetData>');
369
 
1441 ariadna 370
            if (null !== $sheet->getSheetProtection()) {
371
                fwrite($worksheetFilePointer, $sheet->getSheetProtection()->getXml());
372
            }
373
 
1 efrain 374
            // AutoFilter tag
1441 ariadna 375
            if (null !== $autofilter) {
376
                $autoFilterRange = \sprintf(
377
                    '%s%s:%s%s',
378
                    CellHelper::getColumnLettersFromColumnIndex($autofilter->fromColumnIndex),
379
                    $autofilter->fromRow,
380
                    CellHelper::getColumnLettersFromColumnIndex($autofilter->toColumnIndex),
381
                    $autofilter->toRow
382
                );
383
                fwrite($worksheetFilePointer, \sprintf('<autoFilter ref="%s"/>', $autoFilterRange));
1 efrain 384
            }
385
 
386
            // create nodes for merge cells
387
            $mergeCells = array_filter(
388
                $allMergeCells,
389
                static fn (MergeCell $c) => $c->sheetIndex === $worksheet->getExternalSheet()->getIndex(),
390
            );
391
            if ([] !== $mergeCells) {
392
                $mergeCellString = '<mergeCells count="'.\count($mergeCells).'">';
393
                foreach ($mergeCells as $mergeCell) {
394
                    $topLeft = CellHelper::getColumnLettersFromColumnIndex($mergeCell->topLeftColumn).$mergeCell->topLeftRow;
395
                    $bottomRight = CellHelper::getColumnLettersFromColumnIndex($mergeCell->bottomRightColumn).$mergeCell->bottomRightRow;
1441 ariadna 396
                    $mergeCellString .= \sprintf(
1 efrain 397
                        '<mergeCell ref="%s:%s"/>',
398
                        $topLeft,
399
                        $bottomRight
400
                    );
401
                }
402
                $mergeCellString .= '</mergeCells>';
403
                fwrite($worksheetFilePointer, $mergeCellString);
404
            }
405
 
406
            $this->getXMLFragmentForPageMargin($worksheetFilePointer, $options);
407
 
408
            $this->getXMLFragmentForPageSetup($worksheetFilePointer, $options);
409
 
410
            $this->getXMLFragmentForHeaderFooter($worksheetFilePointer, $options);
411
 
412
            // Add the legacy drawing for comments
413
            fwrite($worksheetFilePointer, '<legacyDrawing r:id="rId_comments_vml1"/>');
414
 
415
            fwrite($worksheetFilePointer, '</worksheet>');
416
            fclose($worksheetFilePointer);
417
        }
418
 
419
        return $this;
420
    }
421
 
422
    /**
423
     * Deletes the temporary folder where sheets content was stored.
424
     */
425
    public function deleteWorksheetTempFolder(): self
426
    {
427
        $this->deleteFolderRecursively($this->sheetsContentTempFolder);
428
 
429
        return $this;
430
    }
431
 
432
    /**
433
     * Zips the root folder and streams the contents of the zip into the given stream.
434
     *
435
     * @param resource $streamPointer Pointer to the stream to copy the zip
436
     */
437
    public function zipRootFolderAndCopyToStream($streamPointer): void
438
    {
439
        $zip = $this->zipHelper->createZip($this->rootFolder);
440
 
441
        $zipFilePath = $this->zipHelper->getZipFilePath($zip);
442
 
443
        // In order to have the file's mime type detected properly, files need to be added
444
        // to the zip file in a particular order.
445
        // "[Content_Types].xml" then at least 2 files located in "xl" folder should be zipped first.
446
        $this->zipHelper->addFileToArchive($zip, $this->rootFolder, self::CONTENT_TYPES_XML_FILE_NAME);
447
        $this->zipHelper->addFileToArchive($zip, $this->rootFolder, self::XL_FOLDER_NAME.\DIRECTORY_SEPARATOR.self::WORKBOOK_XML_FILE_NAME);
448
        $this->zipHelper->addFileToArchive($zip, $this->rootFolder, self::XL_FOLDER_NAME.\DIRECTORY_SEPARATOR.self::STYLES_XML_FILE_NAME);
449
 
450
        $this->zipHelper->addFolderToArchive($zip, $this->rootFolder, ZipHelper::EXISTING_FILES_SKIP);
451
        $this->zipHelper->closeArchiveAndCopyToStream($zip, $streamPointer);
452
 
453
        // once the zip is copied, remove it
454
        $this->deleteFile($zipFilePath);
455
    }
456
 
457
    /**
458
     * @param resource $targetResource
459
     */
460
    private function getXMLFragmentForPageMargin($targetResource, Options $options): void
461
    {
462
        $pageMargin = $options->getPageMargin();
463
        if (null === $pageMargin) {
464
            return;
465
        }
466
 
467
        fwrite($targetResource, "<pageMargins top=\"{$pageMargin->top}\" right=\"{$pageMargin->right}\" bottom=\"{$pageMargin->bottom}\" left=\"{$pageMargin->left}\" header=\"{$pageMargin->header}\" footer=\"{$pageMargin->footer}\"/>");
468
    }
469
 
470
    /**
471
     * @param resource $targetResource
472
     */
473
    private function getXMLFragmentForHeaderFooter($targetResource, Options $options): void
474
    {
475
        $headerFooter = $options->getHeaderFooter();
476
        if (null === $headerFooter) {
477
            return;
478
        }
479
 
480
        $xml = '<headerFooter';
481
 
482
        if ($headerFooter->differentOddEven) {
483
            $xml .= " differentOddEven=\"{$headerFooter->differentOddEven}\"";
484
        }
485
 
486
        $xml .= '>';
487
 
488
        if (null !== $headerFooter->oddHeader) {
489
            $xml .= "<oddHeader>{$headerFooter->oddHeader}</oddHeader>";
490
        }
491
 
492
        if (null !== $headerFooter->oddFooter) {
493
            $xml .= "<oddFooter>{$headerFooter->oddFooter}</oddFooter>";
494
        }
495
 
496
        if ($headerFooter->differentOddEven) {
497
            if (null !== $headerFooter->evenHeader) {
498
                $xml .= "<evenHeader>{$headerFooter->evenHeader}</evenHeader>";
499
            }
500
 
501
            if (null !== $headerFooter->evenFooter) {
502
                $xml .= "<evenFooter>{$headerFooter->evenFooter}</evenFooter>";
503
            }
504
        }
505
 
506
        $xml .= '</headerFooter>';
507
 
508
        fwrite($targetResource, $xml);
509
    }
510
 
511
    /**
512
     * @param resource $targetResource
513
     */
514
    private function getXMLFragmentForPageSetup($targetResource, Options $options): void
515
    {
516
        $pageSetup = $options->getPageSetup();
517
        if (null === $pageSetup) {
518
            return;
519
        }
520
 
521
        $xml = '<pageSetup';
522
 
523
        if (null !== $pageSetup->pageOrientation) {
524
            $xml .= " orientation=\"{$pageSetup->pageOrientation->value}\"";
525
        }
526
 
527
        if (null !== $pageSetup->paperSize) {
528
            $xml .= " paperSize=\"{$pageSetup->paperSize->value}\"";
529
        }
530
 
531
        if (null !== $pageSetup->fitToHeight) {
532
            $xml .= " fitToHeight=\"{$pageSetup->fitToHeight}\"";
533
        }
534
 
535
        if (null !== $pageSetup->fitToWidth) {
536
            $xml .= " fitToWidth=\"{$pageSetup->fitToWidth}\"";
537
        }
538
 
539
        $xml .= '/>';
540
 
541
        fwrite($targetResource, $xml);
542
    }
543
 
544
    /**
545
     * Construct column width references xml to inject into worksheet xml file.
546
     */
547
    private function getXMLFragmentForColumnWidths(Options $options, Sheet $sheet): string
548
    {
549
        if ([] !== $sheet->getColumnWidths()) {
550
            $widths = $sheet->getColumnWidths();
551
        } elseif ([] !== $options->getColumnWidths()) {
552
            $widths = $options->getColumnWidths();
553
        } else {
554
            return '';
555
        }
556
 
557
        $xml = '<cols>';
558
 
559
        foreach ($widths as $columnWidth) {
560
            $xml .= '<col min="'.$columnWidth->start.'" max="'.$columnWidth->end.'" width="'.$columnWidth->width.'" customWidth="true"/>';
561
        }
562
        $xml .= '</cols>';
563
 
564
        return $xml;
565
    }
566
 
567
    /**
568
     * Constructs default row height and width xml to inject into worksheet xml file.
569
     */
570
    private function getXMLFragmentForDefaultCellSizing(Options $options): string
571
    {
572
        $rowHeightXml = null === $options->DEFAULT_ROW_HEIGHT ? '' : " defaultRowHeight=\"{$options->DEFAULT_ROW_HEIGHT}\"";
573
        $colWidthXml = null === $options->DEFAULT_COLUMN_WIDTH ? '' : " defaultColWidth=\"{$options->DEFAULT_COLUMN_WIDTH}\"";
574
        if ('' === $colWidthXml && '' === $rowHeightXml) {
575
            return '';
576
        }
577
        // Ensure that the required defaultRowHeight is set
578
        $rowHeightXml = '' === $rowHeightXml ? ' defaultRowHeight="0"' : $rowHeightXml;
579
 
580
        return "<sheetFormatPr{$colWidthXml}{$rowHeightXml}/>";
581
    }
582
 
583
    /**
584
     * Creates the folder that will be used as root.
585
     *
586
     * @throws IOException If unable to create the folder
587
     */
588
    private function createRootFolder(): self
589
    {
590
        $this->rootFolder = $this->createFolder($this->baseFolderRealPath, uniqid('xlsx', true));
591
 
592
        return $this;
593
    }
594
 
595
    /**
596
     * Creates the "_rels" folder under the root folder as well as the ".rels" file in it.
597
     *
598
     * @throws IOException If unable to create the folder or the ".rels" file
599
     */
600
    private function createRelsFolderAndFile(): self
601
    {
602
        $this->relsFolder = $this->createFolder($this->rootFolder, self::RELS_FOLDER_NAME);
603
 
604
        $this->createRelsFile();
605
 
606
        return $this;
607
    }
608
 
609
    /**
610
     * Creates the ".rels" file under the "_rels" folder (under root).
611
     *
612
     * @throws IOException If unable to create the file
613
     */
614
    private function createRelsFile(): self
615
    {
1441 ariadna 616
        $relationshipsXmlContents = <<<'EOD'
617
            <Relationship Id="rIdWorkbook" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
618
            <Relationship Id="rIdCore" Type="http://schemas.openxmlformats.org/officedocument/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
619
            <Relationship Id="rIdApp" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
620
            EOD;
621
 
622
        if ([] !== $this->properties->customProperties) {
623
            $relationshipsXmlContents .= <<<'EOD'
624
                <Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties" Target="docProps/custom.xml"/>
625
                EOD;
626
        }
627
 
628
        $relsFileContents = <<<EOD
1 efrain 629
            <?xml version="1.0" encoding="UTF-8"?>
1441 ariadna 630
            <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">{$relationshipsXmlContents}</Relationships>
1 efrain 631
            EOD;
632
 
633
        $this->createFileWithContents($this->relsFolder, self::RELS_FILE_NAME, $relsFileContents);
634
 
635
        return $this;
636
    }
637
 
638
    /**
639
     * Creates the "docProps" folder under the root folder as well as the "app.xml" and "core.xml" files in it.
640
     *
641
     * @throws IOException If unable to create the folder or one of the files
642
     */
643
    private function createDocPropsFolderAndFiles(): self
644
    {
645
        $this->docPropsFolder = $this->createFolder($this->rootFolder, self::DOC_PROPS_FOLDER_NAME);
646
 
647
        $this->createAppXmlFile();
648
        $this->createCoreXmlFile();
649
 
1441 ariadna 650
        if ([] !== $this->properties->customProperties) {
651
            $this->createCustomXmlFile();
652
        }
653
 
1 efrain 654
        return $this;
655
    }
656
 
657
    /**
658
     * Creates the "app.xml" file under the "docProps" folder.
659
     *
660
     * @throws IOException If unable to create the file
661
     */
662
    private function createAppXmlFile(): self
663
    {
664
        $appXmlFileContents = <<<EOD
665
            <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
666
            <Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">
1441 ariadna 667
                <Application>{$this->properties->application}</Application>
1 efrain 668
                <TotalTime>0</TotalTime>
669
            </Properties>
670
            EOD;
671
 
672
        $this->createFileWithContents($this->docPropsFolder, self::APP_XML_FILE_NAME, $appXmlFileContents);
673
 
674
        return $this;
675
    }
676
 
677
    /**
678
     * Creates the "core.xml" file under the "docProps" folder.
679
     *
680
     * @throws IOException If unable to create the file
681
     */
682
    private function createCoreXmlFile(): self
683
    {
684
        $createdDate = (new DateTimeImmutable())->format(DateTimeImmutable::W3C);
685
        $coreXmlFileContents = <<<EOD
686
            <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
687
            <cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
1441 ariadna 688
                <dc:title>{$this->properties->title}</dc:title>
689
                <dc:subject>{$this->properties->subject}</dc:subject>
690
                <dc:creator>{$this->properties->creator}</dc:creator>
691
                <cp:lastModifiedBy>{$this->properties->lastModifiedBy}</cp:lastModifiedBy>
692
                <cp:keywords>{$this->properties->keywords}</cp:keywords>
693
                <dc:description>{$this->properties->description}</dc:description>
694
                <cp:category>{$this->properties->category}</cp:category>
695
                <dc:language>{$this->properties->language}</dc:language>
1 efrain 696
                <dcterms:created xsi:type="dcterms:W3CDTF">{$createdDate}</dcterms:created>
697
                <dcterms:modified xsi:type="dcterms:W3CDTF">{$createdDate}</dcterms:modified>
698
                <cp:revision>0</cp:revision>
699
            </cp:coreProperties>
700
            EOD;
701
 
702
        $this->createFileWithContents($this->docPropsFolder, self::CORE_XML_FILE_NAME, $coreXmlFileContents);
703
 
704
        return $this;
705
    }
706
 
707
    /**
1441 ariadna 708
     * Creates the "custom.xml" file under the "docProps" folder.
709
     *
710
     * @throws IOException If unable to create the file
711
     */
712
    private function createCustomXmlFile(): self
713
    {
714
        /** The pid must increment for each property, starting with 2 */
715
        $pid = 2;
716
        $propertiesXmlContents = '';
717
 
718
        foreach ($this->properties->customProperties as $name => $value) {
719
            $propertiesXmlContents .= <<<EOD
720
                <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="{$pid}" name="{$name}"><vt:lpwstr>{$value}</vt:lpwstr></property>
721
                EOD;
722
 
723
            ++$pid;
724
        }
725
 
726
        $customXmlFileContents = <<<EOD
727
            <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
728
            <Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">{$propertiesXmlContents}</Properties>
729
            EOD;
730
 
731
        $this->createFileWithContents($this->docPropsFolder, self::CUSTOM_XML_FILE_NAME, $customXmlFileContents);
732
 
733
        return $this;
734
    }
735
 
736
    /**
1 efrain 737
     * Creates the "xl" folder under the root folder as well as its subfolders.
738
     *
739
     * @throws IOException If unable to create at least one of the folders
740
     */
741
    private function createXlFolderAndSubFolders(): self
742
    {
743
        $this->xlFolder = $this->createFolder($this->rootFolder, self::XL_FOLDER_NAME);
744
        $this->createXlRelsFolder();
745
        $this->createXlWorksheetsFolder();
746
        $this->createDrawingsFolder();
747
 
748
        return $this;
749
    }
750
 
751
    /**
752
     * Creates the temp folder where specific sheets content will be written to.
753
     * This folder is not part of the final ODS file and is only used to be able to jump between sheets.
754
     *
755
     * @throws IOException If unable to create the folder
756
     */
757
    private function createSheetsContentTempFolder(): self
758
    {
759
        $this->sheetsContentTempFolder = $this->createFolder($this->rootFolder, 'worksheets-temp');
760
 
761
        return $this;
762
    }
763
 
764
    /**
765
     * Creates the "_rels" folder under the "xl" folder.
766
     *
767
     * @throws IOException If unable to create the folder
768
     */
769
    private function createXlRelsFolder(): self
770
    {
771
        $this->xlRelsFolder = $this->createFolder($this->xlFolder, self::RELS_FOLDER_NAME);
772
 
773
        return $this;
774
    }
775
 
776
    /**
777
     * Creates the "drawings" folder under the "xl" folder.
778
     *
779
     * @throws IOException If unable to create the folder
780
     */
781
    private function createDrawingsFolder(): self
782
    {
783
        $this->createFolder($this->getXlFolder(), self::DRAWINGS_FOLDER_NAME);
784
 
785
        return $this;
786
    }
787
 
788
    /**
789
     * Creates the "worksheets" folder under the "xl" folder.
790
     *
791
     * @throws IOException If unable to create the folder
792
     */
793
    private function createXlWorksheetsFolder(): self
794
    {
795
        $this->xlWorksheetsFolder = $this->createFolder($this->xlFolder, self::WORKSHEETS_FOLDER_NAME);
796
 
797
        return $this;
798
    }
799
 
800
    /**
801
     * Streams the content of the file at the given path into the target resource.
802
     * Depending on which mode the target resource was created with, it will truncate then copy
803
     * or append the content to the target file.
804
     *
805
     * @param string   $sourceFilePath Path of the file whose content will be copied
806
     * @param resource $targetResource Target resource that will receive the content
807
     */
808
    private function copyFileContentsToTarget(string $sourceFilePath, $targetResource): void
809
    {
810
        $sourceHandle = fopen($sourceFilePath, 'r');
811
        \assert(false !== $sourceHandle);
812
        stream_copy_to_stream($sourceHandle, $targetResource);
813
        fclose($sourceHandle);
814
    }
815
}