DocxDocument::rrmdir()   A
last analyzed

Complexity

Conditions 6
Paths 5

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 6
eloc 10
c 2
b 0
f 1
nc 5
nop 1
dl 0
loc 15
ccs 10
cts 10
cp 1
crap 6
rs 9.2222
1
<?php
2
3
namespace PhpDocxTemplate;
4
5
use DOMDocument;
6
use Exception;
7
use RecursiveIteratorIterator;
8
use RecursiveDirectoryIterator;
9
use PhpDocxTemplate\Escaper\RegExp;
10
use ZipArchive;
11
12
/**
13
 * Class DocxDocument
14
 *
15
 * @package PhpDocxTemplate
16
 */
17
class DocxDocument
18
{
19
    private $path;
20
    private $tmpDir;
21
    private $document;
22
    private $zipClass;
23
    private $tempDocumentMainPart;
24
    private $tempDocumentRelations = [];
25
    private $tempDocumentContentTypes = '';
26
    private $tempDocumentNewImages = [];
27
    private $tempDocumentHeaders = [];
28
    private $tempDocumentFooters = [];
29
30
    /**
31
     * Construct an instance of Document
32
     *
33
     * @param string $path - path to the document
34
     *
35
     * @throws Exception
36
     */
37 12
    public function __construct(string $path)
38
    {
39 12
        if (file_exists($path)) {
40 12
            $this->path = $path;
41 12
            $this->tmpDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid("", true) . date("His");
42 12
            $this->zipClass = new ZipArchive();
43 12
            $this->extract();
44
        } else {
45
            throw new Exception("The template " . $path . " was not found!");
46
        }
47 12
    }
48
49
    /**
50
     * Extract (unzip) document contents
51
     */
52 12
    private function extract(): void
53
    {
54 12
        if (file_exists($this->tmpDir) && is_dir($this->tmpDir)) {
55
            $this->rrmdir($this->tmpDir);
56
        }
57
58 12
        mkdir($this->tmpDir);
59
60 12
        $this->zipClass->open($this->path);
61 12
        $this->zipClass->extractTo($this->tmpDir);
62
63 12
        $index = 1;
64 12
        while (false !== $this->zipClass->locateName($this->getHeaderName($index))) {
65 1
            $this->tempDocumentHeaders[$index] = $this->readPartWithRels($this->getHeaderName($index));
66 1
            $index++;
67
        }
68 12
        $index = 1;
69 12
        while (false !== $this->zipClass->locateName($this->getFooterName($index))) {
70 1
            $this->tempDocumentFooters[$index] = $this->readPartWithRels($this->getFooterName($index));
71 1
            $index++;
72
        }
73
74 12
        $this->tempDocumentMainPart = $this->readPartWithRels($this->getMainPartName());
75
76 12
        $this->tempDocumentContentTypes = $this->zipClass->getFromName($this->getDocumentContentTypesName());
77
78 12
        $this->document = file_get_contents(sprintf('%s%sword%sdocument.xml', $this->tmpDir, DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR));
79 12
    }
80
81
    /**
82
     * Get document main part
83
     *
84
     * @return string
85
     */
86 1
    public function getDocumentMainPart(): string
87
    {
88 1
        return $this->tempDocumentMainPart;
89
    }
90
91
    /**
92
     * @return array
93
     */
94 7
    public function getHeaders(): array
95
    {
96 7
        return $this->tempDocumentHeaders;
97
    }
98
99
    /**
100
     * @return array
101
     */
102 7
    public function getFooters(): array
103
    {
104 7
        return $this->tempDocumentFooters;
105
    }
106
107 7
    public function setHeaders(array $headers): void
108
    {
109 7
        $this->tempDocumentHeaders = $headers;
110 7
    }
111
112 7
    public function setFooters(array $footers): void
113
    {
114 7
        $this->tempDocumentFooters = $footers;
115 7
    }
116
117
    /**
118
     * Get the name of main part document (method from PhpOffice\PhpWord)
119
     *
120
     * @return string
121
     */
122 12
    public function getMainPartName(): string
123
    {
124 12
        $contentTypes = $this->zipClass->getFromName('[Content_Types].xml');
125
126
        $pattern = '~PartName="\/(word\/document.*?\.xml)" ' .
127
                   'ContentType="application\/vnd\.openxmlformats-officedocument' .
128 12
                   '\.wordprocessingml\.document\.main\+xml"~';
129
130 12
        $matches = [];
131 12
        preg_match($pattern, $contentTypes, $matches);
132
133 12
        return array_key_exists(1, $matches) ? $matches[1] : sprintf('word%sdocument.xml', DIRECTORY_SEPARATOR);
134
    }
135
136
    /**
137
     * @return string
138
     */
139 12
    private function getDocumentContentTypesName(): string
140
    {
141 12
        return '[Content_Types].xml';
142
    }
143
144
    /**
145
     * Read document part (method from PhpOffice\PhpWord)
146
     *
147
     * @param string $fileName
148
     *
149
     * @return string
150
     */
151 12
    private function readPartWithRels(string $fileName): string
152
    {
153 12
        $relsFileName = $this->getRelationsName($fileName);
154 12
        $partRelations = $this->zipClass->getFromName($relsFileName);
155 12
        if ($partRelations !== false) {
156 12
            $this->tempDocumentRelations[$fileName] = $partRelations;
157
        }
158
159 12
        return $this->fixBrokenMacros($this->zipClass->getFromName($fileName));
160
    }
161
162
    /**
163
     * Get the name of the relations file for document part (method from PhpOffice\PhpWord)
164
     *
165
     * @param string $documentPartName
166
     *
167
     * @return string
168
     */
169 12
    private function getRelationsName(string $documentPartName): string
170
    {
171 12
        return sprintf('word%s_rels%s%s.rels', DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR, pathinfo($documentPartName, PATHINFO_BASENAME));
0 ignored issues
show
Bug introduced by
It seems like pathinfo($documentPartNa...late\PATHINFO_BASENAME) can also be of type array; however, parameter $values of sprintf() does only seem to accept double|integer|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

171
        return sprintf('word%s_rels%s%s.rels', DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR, /** @scrutinizer ignore-type */ pathinfo($documentPartName, PATHINFO_BASENAME));
Loading history...
172
    }
173
174 1
    public function getNextRelationsIndex(string $documentPartName): int
175
    {
176 1
        if (isset($this->tempDocumentRelations[$documentPartName])) {
177 1
            $candidate = substr_count($this->tempDocumentRelations[$documentPartName], '<Relationship');
178 1
            while (strpos($this->tempDocumentRelations[$documentPartName], 'Id="rId' . $candidate . '"') !== false) {
179
                $candidate++;
180
            }
181
182 1
            return $candidate;
183
        }
184
185
        return 1;
186
    }
187
188
    /**
189
     * Finds parts of broken macros and sticks them together (method from PhpOffice\PhpWord)
190
     *
191
     * @param string $documentPart
192
     *
193
     * @return string
194
     */
195 12
    private function fixBrokenMacros(string $documentPart): string
196
    {
197 12
        return preg_replace_callback(
198 12
            '/\$(?:\{|[^{$]*\>\{)[^}$]*\}/U',
199
            function ($match) {
200
                return strip_tags($match[0]);
201 12
            },
202
            $documentPart
203
        );
204
    }
205
206
    /**
207
     * @param string $macro
208
     *
209
     * @return string
210
     */
211
    protected static function ensureMacroCompleted(string $macro): string
212
    {
213
        if (substr($macro, 0, 2) !== '{{' && substr($macro, -1) !== '}}') {
214
            $macro = '{{' . $macro . '}}';
215
        }
216
        return $macro;
217
    }
218
219
    /**
220
     * Get the name of the header file for $index.
221
     *
222
     * @param int $index
223
     *
224
     * @return string
225
     */
226 12
    private function getHeaderName(int $index): string
227
    {
228 12
        return sprintf('word%sheader%d.xml', DIRECTORY_SEPARATOR, $index);
229
    }
230
231
    /**
232
     * Get the name of the footer file for $index.
233
     *
234
     * @param int $index
235
     *
236
     * @return string
237
     */
238 12
    private function getFooterName(int $index): string
239
    {
240 12
        return sprintf('word%sfooter%d.xml', DIRECTORY_SEPARATOR, $index);
241
    }
242
243
    /**
244
     * Find all variables in $documentPartXML.
245
     *
246
     * @param string $documentPartXML
247
     *
248
     * @return string[]
249
     */
250
    private function getVariablesForPart(string $documentPartXML): array
251
    {
252
        $matches = array();
253
        preg_match_all('/\{\{(.*?)\}\}/i', $documentPartXML, $matches);
254
        return $matches[1];
255
    }
256
257
    private function getImageArgs(string $varNameWithArgs): array
258
    {
259
        $varElements = explode(':', $varNameWithArgs);
260
        array_shift($varElements); // first element is name of variable => remove it
261
262
        $varInlineArgs = array();
263
        // size format documentation: https://msdn.microsoft.com/en-us/library/documentformat.openxml.vml.shape%28v=office.14%29.aspx?f=255&MSPPError=-2147217396
264
        foreach ($varElements as $argIdx => $varArg) {
265
            if (strpos($varArg, '=')) { // arg=value
266
                list($argName, $argValue) = explode('=', $varArg, 2);
267
                $argName = strtolower($argName);
268
                if ($argName == 'size') {
269
                    list($varInlineArgs['width'], $varInlineArgs['height']) = explode('x', $argValue, 2);
270
                } else {
271
                    $varInlineArgs[strtolower($argName)] = $argValue;
272
                }
273
            } elseif (preg_match('/^([0-9]*[a-z%]{0,2}|auto)x([0-9]*[a-z%]{0,2}|auto)$/i', $varArg)) { // 60x40
274
                list($varInlineArgs['width'], $varInlineArgs['height']) = explode('x', $varArg, 2);
275
            } else { // :60:40:f
276
                switch ($argIdx) {
277
                    case 0:
278
                        $varInlineArgs['width'] = $varArg;
279
                        break;
280
                    case 1:
281
                        $varInlineArgs['height'] = $varArg;
282
                        break;
283
                    case 2:
284
                        $varInlineArgs['ratio'] = $varArg;
285
                        break;
286
                }
287
            }
288
        }
289
290
        return $varInlineArgs;
291
    }
292
293
    /**
294
     * @param mixed $replaceImage
295
     * @param array $varInlineArgs
296
     *
297
     * @return array
298
     */
299 1
    public function prepareImageAttrs($replaceImage, array $varInlineArgs = []): array
300
    {
301
        // get image path and size
302 1
        $width = null;
303 1
        $height = null;
304 1
        $unit = null;
305 1
        $ratio = null;
306
307
        // a closure can be passed as replacement value which after resolving, can contain the replacement info for the image
308
        // use case: only when a image if found, the replacement tags can be generated
309 1
        if (is_callable($replaceImage)) {
310
            $replaceImage = $replaceImage();
311
        }
312
313 1
        if (is_array($replaceImage) && isset($replaceImage['path'])) {
314 1
            $imgPath = $replaceImage['path'];
315 1
            if (isset($replaceImage['width'])) {
316 1
                $width = $replaceImage['width'];
317
            }
318 1
            if (isset($replaceImage['height'])) {
319 1
                $height = $replaceImage['height'];
320
            }
321 1
            if (isset($replaceImage['unit'])) {
322 1
                $unit = $replaceImage['unit'];
323
            }
324 1
            if (isset($replaceImage['ratio'])) {
325 1
                $ratio = $replaceImage['ratio'];
326
            }
327
        } else {
328
            $imgPath = $replaceImage;
329
        }
330
331 1
        $width = $this->chooseImageDimension($width, $unit ? $unit : 'px', isset($varInlineArgs['width']) ? $varInlineArgs['width'] : null, 115);
332 1
        $height = $this->chooseImageDimension($height, $unit ? $unit : 'px', isset($varInlineArgs['height']) ? $varInlineArgs['height'] : null, 70);
333
334 1
        $imageData = @getimagesize($imgPath);
335 1
        if (!is_array($imageData)) {
336
            throw new Exception(sprintf('Invalid image: %s', $imgPath));
337
        }
338 1
        list($actualWidth, $actualHeight, $imageType) = $imageData;
339
340
        // fix aspect ratio (by default)
341 1
        if (is_null($ratio) && isset($varInlineArgs['ratio'])) {
342
            $ratio = $varInlineArgs['ratio'];
343
        }
344 1
        if (is_null($ratio) || !in_array(strtolower($ratio), array('', '-', 'f', 'false'))) {
345 1
            $this->fixImageWidthHeightRatio($width, $height, $actualWidth, $actualHeight);
346
        }
347
348
        $imageAttrs = array(
349 1
            'src' => $imgPath,
350 1
            'mime' => image_type_to_mime_type($imageType),
351 1
            'width' => $width * 9525,
352 1
            'height' => $height * 9525,
353
        );
354
355 1
        return $imageAttrs;
356
    }
357
358
    /**
359
     * @param mixed $width
360
     * @param mixed $height
361
     * @param int $actualWidth
362
     * @param int $actualHeight
363
     */
364 1
    private function fixImageWidthHeightRatio(&$width, &$height, int $actualWidth, int $actualHeight): void
365
    {
366 1
        $imageRatio = $actualWidth / $actualHeight;
367
368 1
        if (($width === '') && ($height === '')) { // defined size are empty
369
            $width = $actualWidth;
370
            $height = $actualHeight;
371 1
        } elseif ($width === '') { // defined width is empty
372
            $heightFloat = (float)$height;
373
            $width = $heightFloat * $imageRatio;
374 1
        } elseif ($height === '') { // defined height is empty
375
            $widthFloat = (float)$width;
376
            $height = $widthFloat / $imageRatio;
377
        }
378 1
    }
379
380
    /**
381
     * @param mixed $baseValue
382
     * @param string $unit
383
     * @param int|null $inlineValue
384
     * @param int $defaultValue
385
     */
386 1
    private function chooseImageDimension($baseValue, string $unit, ?int $inlineValue, int $defaultValue): string
387
    {
388 1
        $value = $baseValue;
389 1
        if (is_null($value) && isset($inlineValue)) {
390
            $value = $inlineValue;
391
        }
392 1
        if (is_null($value)) {
393
            $value = $defaultValue;
394
        }
395 1
        switch ($unit) {
396 1
            case 'mm':
397
                $value = $value * 3.8; // 1mm = 3.8px
398
                break;
399 1
            case 'pt':
400
                $value = $value / 3 * 4; // 1pt = 4/3 px
401
                break;
402 1
            case 'pc':
403
                $value = $value * 16; // 1px = 16px
404
                break;
405
        }
406 1
        return $value;
407
    }
408
409 1
    public function addImageToRelations(string $partFileName, string $rid, string $imgPath, string $imageMimeType): void
410
    {
411
        // define templates
412 1
        $typeTpl = '<Override PartName="/word/media/{IMG}" ContentType="image/{EXT}"/>';
413 1
        $relationTpl = '<Relationship Id="{RID}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/{IMG}"/>';
414 1
        $newRelationsTpl = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . "\n" . '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>';
415 1
        $newRelationsTypeTpl = '<Override PartName="/{RELS}" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>';
416
        $extTransform = array(
417 1
            'image/jpeg' => 'jpeg',
418
            'image/png'  => 'png',
419
            'image/bmp'  => 'bmp',
420
            'image/gif'  => 'gif',
421
        );
422
        //tempDocumentRelations
423
424
        // get image embed name
425 1
        if (isset($this->tempDocumentNewImages[$imgPath])) {
426
            $imgName = $this->tempDocumentNewImages[$imgPath];
427
        } else {
428
            // transform extension
429 1
            if (isset($extTransform[$imageMimeType])) {
430 1
                $imgExt = $extTransform[$imageMimeType];
431
            } else {
432
                throw new Exception("Unsupported image type $imageMimeType");
433
            }
434
435
            // add image to document
436 1
            $imgName = 'image_' . $rid . '_' . pathinfo($partFileName, PATHINFO_FILENAME) . '.' . $imgExt;
0 ignored issues
show
Bug introduced by
Are you sure pathinfo($partFileName, ...late\PATHINFO_FILENAME) of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

436
            $imgName = 'image_' . $rid . '_' . /** @scrutinizer ignore-type */ pathinfo($partFileName, PATHINFO_FILENAME) . '.' . $imgExt;
Loading history...
437 1
            $this->tempDocumentNewImages[$imgPath] = $imgName;
438
439 1
            $targetDir = sprintf('%s%sword%smedia', $this->tmpDir, DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR);
440 1
            if (!file_exists($targetDir)) {
441
                mkdir($targetDir, 0777, true);
442
            }
443 1
            copy($imgPath, sprintf('%s%s%s', $targetDir, DIRECTORY_SEPARATOR, $imgName));
444
445
            // setup type for image
446 1
            $xmlImageType = str_replace(array('{IMG}', '{EXT}'), array($imgName, $imgExt), $typeTpl);
447 1
            $this->tempDocumentContentTypes = str_replace('</Types>', $xmlImageType, $this->tempDocumentContentTypes) . '</Types>';
448
        }
449
450 1
        $xmlImageRelation = str_replace(array('{RID}', '{IMG}'), array($rid, $imgName), $relationTpl);
451
452 1
        if (!isset($this->tempDocumentRelations[$partFileName])) {
453
            // create new relations file
454
            $this->tempDocumentRelations[$partFileName] = $newRelationsTpl;
455
            // and add it to content types
456
            $xmlRelationsType = str_replace('{RELS}', $this->getRelationsName($partFileName), $newRelationsTypeTpl);
457
            $this->tempDocumentContentTypes = str_replace('</Types>', $xmlRelationsType, $this->tempDocumentContentTypes) . '</Types>';
458
        }
459
460
        // add image to relations
461 1
        $this->tempDocumentRelations[$partFileName] = str_replace('</Relationships>', $xmlImageRelation, $this->tempDocumentRelations[$partFileName]) . '</Relationships>';
462 1
    }
463
464 1
    public function getImageTemplate(): string
465
    {
466 1
        return '</w:t></w:r><w:r><w:drawing><wp:inline xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"> <wp:extent cx="{WIDTH}" cy="{HEIGHT}"/> <wp:docPr id="{IMAGEID}" name=""/> <wp:cNvGraphicFramePr> <a:graphicFrameLocks noChangeAspect="1"/> </wp:cNvGraphicFramePr> <a:graphic> <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture"> <pic:pic> <pic:nvPicPr> <pic:cNvPr id="{IMAGEID}" name=""/> <pic:cNvPicPr/> </pic:nvPicPr> <pic:blipFill> <a:blip r:embed="rId{IMAGEID}"/> <a:stretch> <a:fillRect/> </a:stretch> </pic:blipFill> <pic:spPr> <a:xfrm> <a:off x="0" y="0"/> <a:ext cx="{WIDTH}" cy="{HEIGHT}"/> </a:xfrm> <a:prstGeom prst="rect"> <a:avLst/> </a:prstGeom> </pic:spPr> </pic:pic> </a:graphicData> </a:graphic> </wp:inline> </w:drawing></w:r><w:r><w:t xml:space="preserve">';
467
    }
468
469
    /**
470
     * Find and replace macros in the given XML section.
471
     *
472
     * @param mixed $search
473
     * @param mixed $replace
474
     * @param string $documentPartXML
475
     *
476
     * @return string
477
     */
478
    protected function setValueForPart($search, $replace, string $documentPartXML): string
479
    {
480
        // Note: we can't use the same function for both cases here, because of performance considerations.
481
        $regExpEscaper = new RegExp();
482
483
        return preg_replace($regExpEscaper->escape($search), $replace, $documentPartXML);
484
    }
485
486
    /**
487
     * Get document.xml contents as DOMDocument
488
     *
489
     * @return DOMDocument
490
     */
491 9
    public function getDOMDocument(): DOMDocument
492
    {
493 9
        $dom = new DOMDocument();
494
495 9
        $dom->loadXML($this->document);
496 9
        return $dom;
497
    }
498
499
    /**
500
     * Update document.xml contents
501
     *
502
     * @param DOMDocument $dom - new contents
503
     */
504 7
    public function updateDOMDocument(DOMDocument $dom): void
505
    {
506 7
        $this->document = $dom->saveXml();
507 7
        file_put_contents(sprintf('%s%sword%sdocument.xml', $this->tmpDir, DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR), $this->document);
508 7
    }
509
510
    /**
511
     * Fix table corruption
512
     *
513
     * @param string $xml - xml to fix
514
     *
515
     * @return DOMDocument
516
     */
517 7
    public function fixTables(string $xml): DOMDocument
518
    {
519 7
        $dom = new DOMDocument();
520 7
        $dom->loadXML($xml);
521 7
        $tables = $dom->getElementsByTagName('tbl');
522 7
        foreach ($tables as $table) {
523 3
            $columns = [];
524 3
            $columnsLen = 0;
525 3
            $toAdd = 0;
526 3
            $tableGrid = null;
527 3
            foreach ($table->childNodes as $el) {
528 3
                if ($el->nodeName == 'w:tblGrid') {
529 3
                    $tableGrid = $el;
530 3
                    foreach ($el->childNodes as $col) {
531 3
                        if ($col->nodeName == 'w:gridCol') {
532 3
                            $columns[] = $col;
533 3
                            $columnsLen += 1;
534
                        }
535
                    }
536 3
                } elseif ($el->nodeName == 'w:tr') {
537 3
                    $cellsLen = 0;
538 3
                    foreach ($el->childNodes as $col) {
539 3
                        if ($col->nodeName == 'w:tc') {
540 3
                            $cellsLen += 1;
541
                        }
542
                    }
543 3
                    if (($columnsLen + $toAdd) < $cellsLen) {
544
                        $toAdd = $cellsLen - $columnsLen;
545
                    }
546
                }
547
            }
548
549
            // add columns, if necessary
550 3
            if (!is_null($tableGrid) && $toAdd > 0) {
551
                $width = 0;
552
                foreach ($columns as $col) {
553
                    if (!is_null($col->getAttribute('w:w'))) {
554
                        $width += $col->getAttribute('w:w');
555
                    }
556
                }
557
                if ($width > 0) {
558
                    $oldAverage = $width / $columnsLen;
559
                    $newAverage = round($width / ($columnsLen + $toAdd));
560
                    foreach ($columns as $col) {
561
                        $col->setAttribute('w:w', round($col->getAttribute('w:w') * $newAverage / $oldAverage));
562
                    }
563
                    while ($toAdd > 0) {
564
                        $newCol = $dom->createElement("w:gridCol");
565
                        $newCol->setAttribute('w:w', $newAverage);
566
                        $tableGrid->appendChild($newCol);
567
                        $toAdd -= 1;
568
                    }
569
                }
570
            }
571
572
            // remove columns, if necessary
573 3
            $columns = [];
574 3
            foreach ($tableGrid->childNodes as $col) {
575 3
                if ($col->nodeName == 'w:gridCol') {
576 3
                    $columns[] = $col;
577
                }
578
            }
579 3
            $columnsLen = count($columns);
580
581 3
            $cellsLen = 0;
582 3
            $cellsLenMax = 0;
583 3
            foreach ($table->childNodes as $el) {
584 3
                if ($el->nodeName == 'w:tr') {
585 3
                    $cells = [];
586 3
                    foreach ($el->childNodes as $col) {
587 3
                        if ($col->nodeName == 'w:tc') {
588 3
                            $cells[] = $col;
589
                        }
590
                    }
591 3
                    $cellsLen = $this->getCellLen($cells);
592 3
                    $cellsLenMax = max($cellsLenMax, $cellsLen);
593
                }
594
            }
595 3
            $toRemove = $cellsLen - $cellsLenMax;
596 3
            if ($toRemove > 0) {
597
                $removedWidth = 0.0;
598
                for ($i = $columnsLen - 1; ($i + 1) >= $toRemove; $i -= 1) {
599
                    $extraCol = $columns[$i];
600
                    $removedWidth += $extraCol->getAttribute('w:w');
601
                    $tableGrid->removeChild($extraCol);
602
                }
603
604
                $columnsLeft = [];
605
                foreach ($tableGrid->childNodes as $col) {
606
                    if ($col->nodeName == 'w:gridCol') {
607
                        $columnsLeft[] = $col;
608
                    }
609
                }
610
                $extraSpace = 0;
611
                if (count($columnsLeft) > 0) {
612
                    $extraSpace = $removedWidth / count($columnsLeft);
613
                }
614
                foreach ($columnsLeft as $col) {
615
                    $col->setAttribute('w:w', round($col->getAttribute('w:w') + $extraSpace));
616
                }
617
            }
618
        }
619 7
        return $dom;
620
    }
621
622
    /**
623
     * Get total cells length
624
     *
625
     * @param array $cells - cells
626
     *
627
     * @return int
628
     */
629 3
    private function getCellLen(array $cells): int
630
    {
631 3
        $total = 0;
632 3
        foreach ($cells as $cell) {
633 3
            foreach ($cell->childNodes as $tc) {
634 3
                if ($tc->nodeName == 'w:tcPr') {
635 3
                    foreach ($tc->childNodes as $span) {
636 3
                        if ($span->nodeName == 'w:gridSpan') {
637 1
                            $total += intval($span->getAttribute('w:val'));
638 1
                            break;
639
                        }
640
                    }
641 3
                    break;
642
                }
643
            }
644
        }
645 3
        return $total + 1;
646
    }
647
648
    /**
649
     * @param string $fileName
650
     */
651 3
    protected function savePartWithRels(string $fileName): void
652
    {
653 3
        if (isset($this->tempDocumentRelations[$fileName])) {
654 3
            $relsFileName = $this->getRelationsName($fileName);
655 3
            $targetDir = dirname($this->tmpDir . DIRECTORY_SEPARATOR . $relsFileName);
656 3
            if (!file_exists($targetDir)) {
657
                mkdir($targetDir, 0777, true);
658
            }
659 3
            file_put_contents($this->tmpDir . DIRECTORY_SEPARATOR . $relsFileName, $this->tempDocumentRelations[$fileName]);
660
        }
661 3
    }
662
663
    /**
664
     * Save the document to the target path
665
     *
666
     * @param string $path - target path
667
     */
668 3
    public function save(string $path): void
669
    {
670 3
        $rootPath = realpath($this->tmpDir);
671
672 3
        $zip = new ZipArchive();
673 3
        $zip->open($path, ZipArchive::CREATE | ZipArchive::OVERWRITE);
674
675 3
        $this->savePartWithRels($this->getMainPartName());
676 3
        file_put_contents($this->tmpDir . DIRECTORY_SEPARATOR . $this->getDocumentContentTypesName(), $this->tempDocumentContentTypes);
677
678 3
        foreach ($this->tempDocumentHeaders as $index => $xml) {
679 1
            file_put_contents($this->tmpDir . DIRECTORY_SEPARATOR . $this->getHeaderName($index), $xml);
680
        }
681 3
        foreach ($this->tempDocumentFooters as $index => $xml) {
682 1
            file_put_contents($this->tmpDir . DIRECTORY_SEPARATOR . $this->getFooterName($index), $xml);
683
        }
684
685 3
        $files = new RecursiveIteratorIterator(
686 3
            new RecursiveDirectoryIterator($rootPath),
687 3
            RecursiveIteratorIterator::LEAVES_ONLY
688
        );
689
690 3
        foreach ($files as $name => $file) {
691 3
            if (!$file->isDir()) {
692 3
                $filePath = $file->getRealPath();
693 3
                $relativePath = substr($filePath, strlen($rootPath) + 1);
694 3
                $zip->addFile($filePath, $relativePath);
695
            }
696
        }
697
698 3
        $zip->close();
699
700 3
        if (isset($this->zipClass)) {
701 3
            $this->zipClass->close();
702
        }
703
704 3
        $this->rrmdir($this->tmpDir);
705 3
    }
706
707
    /**
708
     * Remove recursively directory
709
     *
710
     * @param string $dir - target directory
711
     */
712 8
    private function rrmdir(string $dir): void
713
    {
714 8
        $objects = scandir($dir);
715 8
        if (is_array($objects)) {
0 ignored issues
show
introduced by
The condition is_array($objects) is always true.
Loading history...
716 8
            foreach ($objects as $object) {
717 8
                if ($object != "." && $object != "..") {
718 8
                    if (filetype($dir . DIRECTORY_SEPARATOR . $object) == "dir") {
719 8
                        $this->rrmdir($dir . DIRECTORY_SEPARATOR . $object);
720
                    } else {
721 8
                        unlink($dir . DIRECTORY_SEPARATOR . $object);
722
                    }
723
                }
724
            }
725 8
            reset($objects);
726 8
            rmdir($dir);
727
        }
728 8
    }
729
730
    /**
731
     * Close document
732
     */
733 5
    public function close(): void
734
    {
735 5
        if (isset($this->zipClass)) {
736 5
            $this->zipClass->close();
737
        }
738 5
        $this->rrmdir($this->tmpDir);
739 5
    }
740
}
741