Proyectos de Subversion Moodle

Rev

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