Passed
Push — master ( 8f1d03...3f48a5 )
by Bingo
02:51
created

DocxDocument   F

Complexity

Total Complexity 118

Size/Duplication

Total Lines 676
Duplicated Lines 0 %

Test Coverage

Coverage 68.4%

Importance

Changes 6
Bugs 2 Features 3
Metric Value
eloc 296
c 6
b 2
f 3
dl 0
loc 676
ccs 210
cts 307
cp 0.684
rs 2
wmc 118

28 Methods

Rating   Name   Duplication   Size   Complexity  
A getHeaderName() 0 3 1
A addImageToRelations() 0 53 5
A setValueForPart() 0 6 1
A readPartWithRels() 0 9 2
A fixBrokenMacros() 0 8 1
A getRelationsName() 0 3 1
A getMainPartName() 0 12 2
F fixTables() 0 103 29
D prepareImageAttrs() 0 57 17
B chooseImageDimension() 0 21 7
A getDOMDocument() 0 6 1
A getNextRelationsIndex() 0 12 3
A getVariablesForPart() 0 5 1
B getImageArgs() 0 34 8
A savePartWithRels() 0 9 3
A getCellLen() 0 17 6
A getDocumentContentTypesName() 0 3 1
A ensureMacroCompleted() 0 6 3
A close() 0 6 2
A getDocumentMainPart() 0 3 1
A save() 0 30 4
A __construct() 0 9 2
A extract() 0 16 3
A rrmdir() 0 15 6
A getFooterName() 0 3 1
A updateDOMDocument() 0 4 1
A getImageTemplate() 0 3 1
A fixImageWidthHeightRatio() 0 13 5

How to fix   Complexity   

Complex Class

Complex classes like DocxDocument often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DocxDocument, and based on these observations, apply Extract Interface, too.

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
28
    /**
29
     * Construct an instance of Document
30
     *
31
     * @param string $path - path to the document
32
     *
33
     * @throws Exception
34
     */
35 11
    public function __construct(string $path)
36
    {
37 11
        if (file_exists($path)) {
38 11
            $this->path = $path;
39 11
            $this->tmpDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid("", true) . date("His");
40 11
            $this->zipClass = new ZipArchive();
41 11
            $this->extract();
42
        } else {
43
            throw new Exception("The template " . $path . " was not found!");
44
        }
45 11
    }
46
47
    /**
48
     * Extract (unzip) document contents
49
     */
50 11
    private function extract(): void
51
    {
52 11
        if (file_exists($this->tmpDir) && is_dir($this->tmpDir)) {
53
            $this->rrmdir($this->tmpDir);
54
        }
55
56 11
        mkdir($this->tmpDir);
57
58 11
        $this->zipClass->open($this->path);
59 11
        $this->zipClass->extractTo($this->tmpDir);
60
61 11
        $this->tempDocumentMainPart = $this->readPartWithRels($this->getMainPartName());
62
63 11
        $this->tempDocumentContentTypes = $this->zipClass->getFromName($this->getDocumentContentTypesName());
64
65 11
        $this->document = file_get_contents(sprintf('%s%sword%sdocument.xml', $this->tmpDir, DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR));
66 11
    }
67
68
    /**
69
     * Get document main part
70
     *
71
     * @return string
72
     */
73 1
    public function getDocumentMainPart(): string
74
    {
75 1
        return $this->tempDocumentMainPart;
76
    }
77
78
    /**
79
     * Get the name of main part document (method from PhpOffice\PhpWord)
80
     *
81
     * @return string
82
     */
83 11
    public function getMainPartName(): string
84
    {
85 11
        $contentTypes = $this->zipClass->getFromName('[Content_Types].xml');
86
87
        $pattern = '~PartName="\/(word\/document.*?\.xml)" ' .
88
                   'ContentType="application\/vnd\.openxmlformats-officedocument' .
89 11
                   '\.wordprocessingml\.document\.main\+xml"~';
90
91 11
        $matches = [];
92 11
        preg_match($pattern, $contentTypes, $matches);
93
94 11
        return array_key_exists(1, $matches) ? $matches[1] : sprintf('word%sdocument.xml', DIRECTORY_SEPARATOR);
95
    }
96
97
    /**
98
     * @return string
99
     */
100 11
    private function getDocumentContentTypesName(): string
101
    {
102 11
        return '[Content_Types].xml';
103
    }
104
105
    /**
106
     * Read document part (method from PhpOffice\PhpWord)
107
     *
108
     * @param string $fileName
109
     *
110
     * @return string
111
     */
112 11
    private function readPartWithRels(string $fileName): string
113
    {
114 11
        $relsFileName = $this->getRelationsName($fileName);
115 11
        $partRelations = $this->zipClass->getFromName($relsFileName);
116 11
        if ($partRelations !== false) {
117 11
            $this->tempDocumentRelations[$fileName] = $partRelations;
118
        }
119
120 11
        return $this->fixBrokenMacros($this->zipClass->getFromName($fileName));
121
    }
122
123
    /**
124
     * Get the name of the relations file for document part (method from PhpOffice\PhpWord)
125
     *
126
     * @param string $documentPartName
127
     *
128
     * @return string
129
     */
130 11
    private function getRelationsName(string $documentPartName): string
131
    {
132 11
        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

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

397
            $imgName = 'image_' . $rid . '_' . /** @scrutinizer ignore-type */ pathinfo($partFileName, PATHINFO_FILENAME) . '.' . $imgExt;
Loading history...
398 1
            $this->tempDocumentNewImages[$imgPath] = $imgName;
399
400 1
            $targetDir = sprintf('%s%sword%smedia', $this->tmpDir, DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR);
401 1
            if (!file_exists($targetDir)) {
402
                mkdir($targetDir, 0777, true);
403
            }
404 1
            copy($imgPath, sprintf('%s%s%s', $targetDir, DIRECTORY_SEPARATOR, $imgName));
405
406
            // setup type for image
407 1
            $xmlImageType = str_replace(array('{IMG}', '{EXT}'), array($imgName, $imgExt), $typeTpl);
408 1
            $this->tempDocumentContentTypes = str_replace('</Types>', $xmlImageType, $this->tempDocumentContentTypes) . '</Types>';
409
        }
410
411 1
        $xmlImageRelation = str_replace(array('{RID}', '{IMG}'), array($rid, $imgName), $relationTpl);
412
413 1
        if (!isset($this->tempDocumentRelations[$partFileName])) {
414
            // create new relations file
415
            $this->tempDocumentRelations[$partFileName] = $newRelationsTpl;
416
            // and add it to content types
417
            $xmlRelationsType = str_replace('{RELS}', $this->getRelationsName($partFileName), $newRelationsTypeTpl);
418
            $this->tempDocumentContentTypes = str_replace('</Types>', $xmlRelationsType, $this->tempDocumentContentTypes) . '</Types>';
419
        }
420
421
        // add image to relations
422 1
        $this->tempDocumentRelations[$partFileName] = str_replace('</Relationships>', $xmlImageRelation, $this->tempDocumentRelations[$partFileName]) . '</Relationships>';
423 1
    }
424
425 1
    public function getImageTemplate(): string
426
    {
427 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">';
428
    }
429
430
    /**
431
     * Find and replace macros in the given XML section.
432
     *
433
     * @param mixed $search
434
     * @param mixed $replace
435
     * @param string $documentPartXML
436
     *
437
     * @return string
438
     */
439
    protected function setValueForPart($search, $replace, string $documentPartXML): string
440
    {
441
        // Note: we can't use the same function for both cases here, because of performance considerations.
442
        $regExpEscaper = new RegExp();
443
444
        return preg_replace($regExpEscaper->escape($search), $replace, $documentPartXML);
445
    }
446
447
    /**
448
     * Get document.xml contents as DOMDocument
449
     *
450
     * @return DOMDocument
451
     */
452 8
    public function getDOMDocument(): DOMDocument
453
    {
454 8
        $dom = new DOMDocument();
455
456 8
        $dom->loadXML($this->document);
457 8
        return $dom;
458
    }
459
460
    /**
461
     * Update document.xml contents
462
     *
463
     * @param DOMDocument $dom - new contents
464
     */
465 6
    public function updateDOMDocument(DOMDocument $dom): void
466
    {
467 6
        $this->document = $dom->saveXml();
468 6
        file_put_contents(sprintf('%s%sword%sdocument.xml', $this->tmpDir, DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR), $this->document);
469 6
    }
470
471
    /**
472
     * Fix table corruption
473
     *
474
     * @param string $xml - xml to fix
475
     *
476
     * @return DOMDocument
477
     */
478 6
    public function fixTables(string $xml): DOMDocument
479
    {
480 6
        $dom = new DOMDocument();
481 6
        $dom->loadXML($xml);
482 6
        $tables = $dom->getElementsByTagName('tbl');
483 6
        foreach ($tables as $table) {
484 3
            $columns = [];
485 3
            $columnsLen = 0;
486 3
            $toAdd = 0;
487 3
            $tableGrid = null;
488 3
            foreach ($table->childNodes as $el) {
489 3
                if ($el->nodeName == 'w:tblGrid') {
490 3
                    $tableGrid = $el;
491 3
                    foreach ($el->childNodes as $col) {
492 3
                        if ($col->nodeName == 'w:gridCol') {
493 3
                            $columns[] = $col;
494 3
                            $columnsLen += 1;
495
                        }
496
                    }
497 3
                } elseif ($el->nodeName == 'w:tr') {
498 3
                    $cellsLen = 0;
499 3
                    foreach ($el->childNodes as $col) {
500 3
                        if ($col->nodeName == 'w:tc') {
501 3
                            $cellsLen += 1;
502
                        }
503
                    }
504 3
                    if (($columnsLen + $toAdd) < $cellsLen) {
505
                        $toAdd = $cellsLen - $columnsLen;
506
                    }
507
                }
508
            }
509
510
            // add columns, if necessary
511 3
            if (!is_null($tableGrid) && $toAdd > 0) {
512
                $width = 0;
513
                foreach ($columns as $col) {
514
                    if (!is_null($col->getAttribute('w:w'))) {
515
                        $width += $col->getAttribute('w:w');
516
                    }
517
                }
518
                if ($width > 0) {
519
                    $oldAverage = $width / $columnsLen;
520
                    $newAverage = round($width / ($columnsLen + $toAdd));
521
                    foreach ($columns as $col) {
522
                        $col->setAttribute('w:w', round($col->getAttribute('w:w') * $newAverage / $oldAverage));
523
                    }
524
                    while ($toAdd > 0) {
525
                        $newCol = $dom->createElement("w:gridCol");
526
                        $newCol->setAttribute('w:w', $newAverage);
527
                        $tableGrid->appendChild($newCol);
528
                        $toAdd -= 1;
529
                    }
530
                }
531
            }
532
533
            // remove columns, if necessary
534 3
            $columns = [];
535 3
            foreach ($tableGrid->childNodes as $col) {
536 3
                if ($col->nodeName == 'w:gridCol') {
537 3
                    $columns[] = $col;
538
                }
539
            }
540 3
            $columnsLen = count($columns);
541
542 3
            $cellsLen = 0;
543 3
            $cellsLenMax = 0;
544 3
            foreach ($table->childNodes as $el) {
545 3
                if ($el->nodeName == 'w:tr') {
546 3
                    $cells = [];
547 3
                    foreach ($el->childNodes as $col) {
548 3
                        if ($col->nodeName == 'w:tc') {
549 3
                            $cells[] = $col;
550
                        }
551
                    }
552 3
                    $cellsLen = $this->getCellLen($cells);
553 3
                    $cellsLenMax = max($cellsLenMax, $cellsLen);
554
                }
555
            }
556 3
            $toRemove = $cellsLen - $cellsLenMax;
557 3
            if ($toRemove > 0) {
558
                $removedWidth = 0.0;
559
                for ($i = $columnsLen - 1; ($i + 1) >= $toRemove; $i -= 1) {
560
                    $extraCol = $columns[$i];
561
                    $removedWidth += $extraCol->getAttribute('w:w');
562
                    $tableGrid->removeChild($extraCol);
563
                }
564
565
                $columnsLeft = [];
566
                foreach ($tableGrid->childNodes as $col) {
567
                    if ($col->nodeName == 'w:gridCol') {
568
                        $columnsLeft[] = $col;
569
                    }
570
                }
571
                $extraSpace = 0;
572
                if (count($columnsLeft) > 0) {
573
                    $extraSpace = $removedWidth / count($columnsLeft);
574
                }
575
                foreach ($columnsLeft as $col) {
576
                    $col->setAttribute('w:w', round($col->getAttribute('w:w') + $extraSpace));
577
                }
578
            }
579
        }
580 6
        return $dom;
581
    }
582
583
    /**
584
     * Get total cells length
585
     *
586
     * @param array $cells - cells
587
     *
588
     * @return int
589
     */
590 3
    private function getCellLen(array $cells): int
591
    {
592 3
        $total = 0;
593 3
        foreach ($cells as $cell) {
594 3
            foreach ($cell->childNodes as $tc) {
595 3
                if ($tc->nodeName == 'w:tcPr') {
596 3
                    foreach ($tc->childNodes as $span) {
597 3
                        if ($span->nodeName == 'w:gridSpan') {
598 1
                            $total += intval($span->getAttribute('w:val'));
599 1
                            break;
600
                        }
601
                    }
602 3
                    break;
603
                }
604
            }
605
        }
606 3
        return $total + 1;
607
    }
608
609
    /**
610
     * @param string $fileName
611
     */
612 2
    protected function savePartWithRels(string $fileName): void
613
    {
614 2
        if (isset($this->tempDocumentRelations[$fileName])) {
615 2
            $relsFileName = $this->getRelationsName($fileName);
616 2
            $targetDir = dirname($this->tmpDir . DIRECTORY_SEPARATOR . $relsFileName);
617 2
            if (!file_exists($targetDir)) {
618
                mkdir($targetDir, 0777, true);
619
            }
620 2
            file_put_contents($this->tmpDir . DIRECTORY_SEPARATOR . $relsFileName, $this->tempDocumentRelations[$fileName]);
621
        }
622 2
    }
623
624
    /**
625
     * Save the document to the target path
626
     *
627
     * @param string $path - target path
628
     */
629 2
    public function save(string $path): void
630
    {
631 2
        $rootPath = realpath($this->tmpDir);
632
633 2
        $zip = new ZipArchive();
634 2
        $zip->open($path, ZipArchive::CREATE | ZipArchive::OVERWRITE);
635
636 2
        $this->savePartWithRels($this->getMainPartName());
637 2
        file_put_contents($this->tmpDir . DIRECTORY_SEPARATOR . $this->getDocumentContentTypesName(), $this->tempDocumentContentTypes);
638
639 2
        $files = new RecursiveIteratorIterator(
640 2
            new RecursiveDirectoryIterator($rootPath),
641 2
            RecursiveIteratorIterator::LEAVES_ONLY
642
        );
643
644 2
        foreach ($files as $name => $file) {
645 2
            if (!$file->isDir()) {
646 2
                $filePath = $file->getRealPath();
647 2
                $relativePath = substr($filePath, strlen($rootPath) + 1);
648 2
                $zip->addFile($filePath, $relativePath);
649
            }
650
        }
651
652 2
        $zip->close();
653
654 2
        if (isset($this->zipClass)) {
655 2
            $this->zipClass->close();
656
        }
657
658 2
        $this->rrmdir($this->tmpDir);
659 2
    }
660
661
    /**
662
     * Remove recursively directory
663
     *
664
     * @param string $dir - target directory
665
     */
666 7
    private function rrmdir(string $dir): void
667
    {
668 7
        $objects = scandir($dir);
669 7
        if (is_array($objects)) {
0 ignored issues
show
introduced by
The condition is_array($objects) is always true.
Loading history...
670 7
            foreach ($objects as $object) {
671 7
                if ($object != "." && $object != "..") {
672 7
                    if (filetype($dir . DIRECTORY_SEPARATOR . $object) == "dir") {
673 7
                        $this->rrmdir($dir . DIRECTORY_SEPARATOR . $object);
674
                    } else {
675 7
                        unlink($dir . DIRECTORY_SEPARATOR . $object);
676
                    }
677
                }
678
            }
679 7
            reset($objects);
680 7
            rmdir($dir);
681
        }
682 7
    }
683
684
    /**
685
     * Close document
686
     */
687 5
    public function close(): void
688
    {
689 5
        if (isset($this->zipClass)) {
690 5
            $this->zipClass->close();
691
        }
692 5
        $this->rrmdir($this->tmpDir);
693 5
    }
694
}
695