Passed
Push — master ( 679987...7f4fee )
by Bingo
03:22
created

DocxDocument   F

Complexity

Total Complexity 128

Size/Duplication

Total Lines 754
Duplicated Lines 0 %

Test Coverage

Coverage 76.07%

Importance

Changes 4
Bugs 2 Features 2
Metric Value
eloc 346
c 4
b 2
f 2
dl 0
loc 754
ccs 267
cts 351
cp 0.7607
rs 2
wmc 128

27 Methods

Rating   Name   Duplication   Size   Complexity  
A getHeaderName() 0 3 1
A readPartWithRels() 0 9 2
A fixBrokenMacros() 0 8 1
A getRelationsName() 0 3 1
A getMainPartName() 0 12 2
A getDocumentContentTypesName() 0 3 1
A getDocumentMainPart() 0 3 1
A __construct() 0 9 2
A extract() 0 29 5
A getFooterName() 0 3 1
A getNextRelationsIndex() 0 12 3
A ensureMacroCompleted() 0 6 3
A addImageToRelations() 0 49 4
A setValueForPart() 0 9 2
F fixTables() 0 103 29
C prepareImageAttrs() 0 53 14
A chooseImageDimension() 0 17 6
A getDOMDocument() 0 5 1
C setImageValue() 0 65 13
A getVariablesForPart() 0 5 1
B getImageArgs() 0 34 8
A getCellLen() 0 17 6
A close() 0 6 2
A save() 0 27 4
A rrmdir() 0 15 6
A updateDOMDocument() 0 4 1
B fixImageWidthHeightRatio() 0 35 8

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 DOMElement;
7
use Exception;
8
use ZipArchive;
9
use RecursiveIteratorIterator;
10
use RecursiveDirectoryIterator;
11
use PhpDocxTemplate\Escaper\RegExp;
12
13
/**
14
 * Class DocxDocument
15
 *
16
 * @package PhpDocxTemplate
17
 */
18
class DocxDocument
19
{
20
    private const MAXIMUM_REPLACEMENTS_DEFAULT = -1;
21
    private $path;
22
    private $tmpDir;
23
    private $document;
24
    private $zipClass;
25
    private $tempDocumentMainPart;
26
    private $tempDocumentHeaders = [];
27
    private $tempDocumentFooters = [];
28
    private $tempDocumentRelations = [];
29
    private $tempDocumentContentTypes = '';
30
    private $tempDocumentNewImages = [];
31
32
    /**
33
     * Construct an instance of Document
34
     *
35
     * @param string $path - path to the document
36
     *
37
     * @throws Exception
38
     */
39 11
    public function __construct(string $path)
40
    {
41 11
        if (file_exists($path)) {
42 11
            $this->path = $path;
43 11
            $this->tmpDir = sys_get_temp_dir() . "/" . uniqid("", true) . date("His");
44 11
            $this->zipClass = new ZipArchive();
45 11
            $this->extract();
46
        } else {
47
            throw new Exception("The template " . $path . " was not found!");
48
        }
49 11
    }
50
51
    /**
52
     * Extract (unzip) document contents
53
     */
54 11
    private function extract(): void
55
    {
56 11
        if (file_exists($this->tmpDir) && is_dir($this->tmpDir)) {
57
            $this->rrmdir($this->tmpDir);
58
        }
59
60 11
        mkdir($this->tmpDir);
61
62 11
        $this->zipClass->open($this->path);
63 11
        $this->zipClass->extractTo($this->tmpDir);
64
65 11
        $index = 1;
66 11
        while (false !== $this->zipClass->locateName($this->getHeaderName($index))) {
67
            $this->tempDocumentHeaders[$index] = $this->readPartWithRels($this->getHeaderName($index));
68
            $index += 1;
69
        }
70 11
        $index = 1;
71 11
        while (false !== $this->zipClass->locateName($this->getFooterName($index))) {
72
            $this->tempDocumentFooters[$index] = $this->readPartWithRels($this->getFooterName($index));
73
            $index += 1;
74
        }
75
76 11
        $this->tempDocumentMainPart = $this->readPartWithRels($this->getMainPartName());
77
78 11
        $this->tempDocumentContentTypes = $this->zipClass->getFromName($this->getDocumentContentTypesName());
79
80
        //$this->zipClass->close();
81
82 11
        $this->document = file_get_contents($this->tmpDir . "/word/document.xml");
83 11
    }
84
85
    /**
86
     * Get document main part
87
     *
88
     * @return string
89
     */
90 1
    public function getDocumentMainPart(): string
91
    {
92 1
        return $this->tempDocumentMainPart;
93
    }
94
95
    /**
96
     * Get the name of main part document (method from PhpOffice\PhpWord)
97
     *
98
     * @return string
99
     */
100 11
    private function getMainPartName(): string
101
    {
102 11
        $contentTypes = $this->zipClass->getFromName('[Content_Types].xml');
103
104
        $pattern = '~PartName="\/(word\/document.*?\.xml)" ' .
105
                   'ContentType="application\/vnd\.openxmlformats-officedocument' .
106 11
                   '\.wordprocessingml\.document\.main\+xml"~';
107
108 11
        $matches = [];
109 11
        preg_match($pattern, $contentTypes, $matches);
110
111 11
        return array_key_exists(1, $matches) ? $matches[1] : 'word/document.xml';
112
    }
113
114
    /**
115
     * @return string
116
     */
117 11
    private function getDocumentContentTypesName(): string
118
    {
119 11
        return '[Content_Types].xml';
120
    }
121
122
    /**
123
     * Read document part (method from PhpOffice\PhpWord)
124
     *
125
     * @param string $fileName
126
     *
127
     * @return string
128
     */
129 11
    private function readPartWithRels(string $fileName): string
130
    {
131 11
        $relsFileName = $this->getRelationsName($fileName);
132 11
        $partRelations = $this->zipClass->getFromName($relsFileName);
133 11
        if ($partRelations !== false) {
134 11
            $this->tempDocumentRelations[$fileName] = $partRelations;
135
        }
136
137 11
        return $this->fixBrokenMacros($this->zipClass->getFromName($fileName));
138
    }
139
140
    /**
141
     * Get the name of the relations file for document part (method from PhpOffice\PhpWord)
142
     *
143
     * @param string $documentPartName
144
     *
145
     * @return string
146
     */
147 11
    private function getRelationsName(string $documentPartName): string
148
    {
149 11
        return 'word/_rels/' . pathinfo($documentPartName, PATHINFO_BASENAME) . '.rels';
0 ignored issues
show
Bug introduced by
Are you sure pathinfo($documentPartNa...late\PATHINFO_BASENAME) 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

149
        return 'word/_rels/' . /** @scrutinizer ignore-type */ pathinfo($documentPartName, PATHINFO_BASENAME) . '.rels';
Loading history...
150
    }
151
152 1
    private function getNextRelationsIndex(string $documentPartName): int
153
    {
154 1
        if (isset($this->tempDocumentRelations[$documentPartName])) {
155 1
            $candidate = substr_count($this->tempDocumentRelations[$documentPartName], '<Relationship');
156 1
            while (strpos($this->tempDocumentRelations[$documentPartName], 'Id="rId' . $candidate . '"') !== false) {
157
                $candidate++;
158
            }
159
160 1
            return $candidate;
161
        }
162
163
        return 1;
164
    }
165
166
    /**
167
     * Finds parts of broken macros and sticks them together (method from PhpOffice\PhpWord)
168
     *
169
     * @param string $documentPart
170
     *
171
     * @return string
172
     */
173 11
    private function fixBrokenMacros(string $documentPart): string
174
    {
175 11
        return preg_replace_callback(
176 11
            '/\$(?:\{|[^{$]*\>\{)[^}$]*\}/U',
177
            function ($match) {
178
                return strip_tags($match[0]);
179 11
            },
180 11
            $documentPart
181
        );
182
    }
183
184
    /**
185
     * @param string $macro
186
     *
187
     * @return string
188
     */
189 1
    protected static function ensureMacroCompleted(string $macro): string
190
    {
191 1
        if (substr($macro, 0, 2) !== '{{' && substr($macro, -1) !== '}}') {
192 1
            $macro = '{{' . $macro . '}}';
193
        }
194 1
        return $macro;
195
    }
196
197
    /**
198
     * Get the name of the header file for $index.
199
     *
200
     * @param int $index
201
     *
202
     * @return string
203
     */
204 11
    private function getHeaderName(int $index): string
205
    {
206 11
        return sprintf('word/header%d.xml', $index);
207
    }
208
209
    /**
210
     * Get the name of the footer file for $index.
211
     *
212
     * @param int $index
213
     *
214
     * @return string
215
     */
216 11
    private function getFooterName(int $index): string
217
    {
218 11
        return sprintf('word/footer%d.xml', $index);
219
    }
220
221
    /**
222
     * Find all variables in $documentPartXML.
223
     *
224
     * @param string $documentPartXML
225
     *
226
     * @return string[]
227
     */
228 1
    private function getVariablesForPart(string $documentPartXML): array
229
    {
230 1
        $matches = array();
231 1
        preg_match_all('/\{\{(.*?)\}\}/i', $documentPartXML, $matches);
232 1
        return $matches[1];
233
    }
234
235 1
    private function getImageArgs(string $varNameWithArgs): array
236
    {
237 1
        $varElements = explode(':', $varNameWithArgs);
238 1
        array_shift($varElements); // first element is name of variable => remove it
239
240 1
        $varInlineArgs = array();
241
        // size format documentation: https://msdn.microsoft.com/en-us/library/documentformat.openxml.vml.shape%28v=office.14%29.aspx?f=255&MSPPError=-2147217396
242 1
        foreach ($varElements as $argIdx => $varArg) {
243
            if (strpos($varArg, '=')) { // arg=value
244
                list($argName, $argValue) = explode('=', $varArg, 2);
245
                $argName = strtolower($argName);
246
                if ($argName == 'size') {
247
                    list($varInlineArgs['width'], $varInlineArgs['height']) = explode('x', $argValue, 2);
248
                } else {
249
                    $varInlineArgs[strtolower($argName)] = $argValue;
250
                }
251
            } elseif (preg_match('/^([0-9]*[a-z%]{0,2}|auto)x([0-9]*[a-z%]{0,2}|auto)$/i', $varArg)) { // 60x40
252
                list($varInlineArgs['width'], $varInlineArgs['height']) = explode('x', $varArg, 2);
253
            } else { // :60:40:f
254
                switch ($argIdx) {
255
                    case 0:
256
                        $varInlineArgs['width'] = $varArg;
257
                        break;
258
                    case 1:
259
                        $varInlineArgs['height'] = $varArg;
260
                        break;
261
                    case 2:
262
                        $varInlineArgs['ratio'] = $varArg;
263
                        break;
264
                }
265
            }
266
        }
267
268 1
        return $varInlineArgs;
269
    }
270
271
    /**
272
     * @param mixed $replaceImage
273
     * @param array $varInlineArgs
274
     *
275
     * @return array
276
     */
277 1
    private function prepareImageAttrs($replaceImage, array $varInlineArgs): array
278
    {
279
        // get image path and size
280 1
        $width = null;
281 1
        $height = null;
282 1
        $ratio = null;
283
284
        // a closure can be passed as replacement value which after resolving, can contain the replacement info for the image
285
        // use case: only when a image if found, the replacement tags can be generated
286 1
        if (is_callable($replaceImage)) {
287
            $replaceImage = $replaceImage();
288
        }
289
290 1
        if (is_array($replaceImage) && isset($replaceImage['path'])) {
291 1
            $imgPath = $replaceImage['path'];
292 1
            if (isset($replaceImage['width'])) {
293 1
                $width = $replaceImage['width'];
294
            }
295 1
            if (isset($replaceImage['height'])) {
296 1
                $height = $replaceImage['height'];
297
            }
298 1
            if (isset($replaceImage['ratio'])) {
299 1
                $ratio = $replaceImage['ratio'];
300
            }
301
        } else {
302
            $imgPath = $replaceImage;
303
        }
304
305 1
        $width = $this->chooseImageDimension($width, isset($varInlineArgs['width']) ? $varInlineArgs['width'] : null, 115);
306 1
        $height = $this->chooseImageDimension($height, isset($varInlineArgs['height']) ? $varInlineArgs['height'] : null, 70);
307
308 1
        $imageData = @getimagesize($imgPath);
309 1
        if (!is_array($imageData)) {
310
            throw new Exception(sprintf('Invalid image: %s', $imgPath));
311
        }
312 1
        list($actualWidth, $actualHeight, $imageType) = $imageData;
313
314
        // fix aspect ratio (by default)
315 1
        if (is_null($ratio) && isset($varInlineArgs['ratio'])) {
316
            $ratio = $varInlineArgs['ratio'];
317
        }
318 1
        if (is_null($ratio) || !in_array(strtolower($ratio), array('', '-', 'f', 'false'))) {
319 1
            $this->fixImageWidthHeightRatio($width, $height, $actualWidth, $actualHeight);
320
        }
321
322
        $imageAttrs = array(
323 1
            'src'    => $imgPath,
324 1
            'mime'   => image_type_to_mime_type($imageType),
325 1
            'width'  => $width,
326 1
            'height' => $height,
327
        );
328
329 1
        return $imageAttrs;
330
    }
331
332
    /**
333
     * @param mixed $width
334
     * @param mixed $height
335
     * @param int $actualWidth
336
     * @param int $actualHeight
337
     */
338 1
    private function fixImageWidthHeightRatio(&$width, &$height, int $actualWidth, int $actualHeight): void
339
    {
340 1
        $imageRatio = $actualWidth / $actualHeight;
341
342 1
        if (($width === '') && ($height === '')) { // defined size are empty
343
            $width = $actualWidth . 'px';
344
            $height = $actualHeight . 'px';
345 1
        } elseif ($width === '') { // defined width is empty
346
            $heightFloat = (float) $height;
347
            $widthFloat = $heightFloat * $imageRatio;
348
            $matches = array();
349
            preg_match("/\d([a-z%]+)$/", $height, $matches);
350
            $width = $widthFloat . $matches[1];
351 1
        } elseif ($height === '') { // defined height is empty
352
            $widthFloat = (float) $width;
353
            $heightFloat = $widthFloat / $imageRatio;
354
            $matches = array();
355
            preg_match("/\d([a-z%]+)$/", $width, $matches);
356
            $height = $heightFloat . $matches[1];
357
        } else { // we have defined size, but we need also check it aspect ratio
358 1
            $widthMatches = array();
359 1
            preg_match("/\d([a-z%]+)$/", $width, $widthMatches);
360 1
            $heightMatches = array();
361 1
            preg_match("/\d([a-z%]+)$/", $height, $heightMatches);
362
            // try to fix only if dimensions are same
363 1
            if ($widthMatches[1] == $heightMatches[1]) {
364 1
                $dimention = $widthMatches[1];
365 1
                $widthFloat = (float) $width;
366 1
                $heightFloat = (float) $height;
367 1
                $definedRatio = $widthFloat / $heightFloat;
368
369 1
                if ($imageRatio > $definedRatio) { // image wider than defined box
370
                    $height = ($widthFloat / $imageRatio) . $dimention;
371 1
                } elseif ($imageRatio < $definedRatio) { // image higher than defined box
372
                    $width = ($heightFloat * $imageRatio) . $dimention;
373
                }
374
            }
375
        }
376 1
    }
377
378 1
    private function chooseImageDimension(?int $baseValue, ?int $inlineValue, int $defaultValue): string
379
    {
380 1
        $value = $baseValue;
381 1
        if (is_null($value) && isset($inlineValue)) {
382
            $value = $inlineValue;
383
        }
384 1
        if (!preg_match('/^([0-9]*(cm|mm|in|pt|pc|px|%|em|ex|)|auto)$/i', $value)) {
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type null; however, parameter $subject of preg_match() does only seem to accept 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

384
        if (!preg_match('/^([0-9]*(cm|mm|in|pt|pc|px|%|em|ex|)|auto)$/i', /** @scrutinizer ignore-type */ $value)) {
Loading history...
385
            $value = null;
386
        }
387 1
        if (is_null($value)) {
388
            $value = $defaultValue;
389
        }
390 1
        if (is_numeric($value)) {
0 ignored issues
show
introduced by
The condition is_numeric($value) is always true.
Loading history...
391 1
            $value .= 'px';
392
        }
393
394 1
        return $value;
395
    }
396
397 1
    private function addImageToRelations(string $partFileName, string $rid, string $imgPath, string $imageMimeType): void
398
    {
399
        // define templates
400 1
        $typeTpl = '<Override PartName="/word/media/{IMG}" ContentType="image/{EXT}"/>';
401 1
        $relationTpl = '<Relationship Id="{RID}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/{IMG}"/>';
402 1
        $newRelationsTpl = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . "\n" . '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>';
403 1
        $newRelationsTypeTpl = '<Override PartName="/{RELS}" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>';
404
        $extTransform = array(
405 1
            'image/jpeg' => 'jpeg',
406
            'image/png'  => 'png',
407
            'image/bmp'  => 'bmp',
408
            'image/gif'  => 'gif',
409
        );
410
411
        // get image embed name
412 1
        if (isset($this->tempDocumentNewImages[$imgPath])) {
413
            $imgName = $this->tempDocumentNewImages[$imgPath];
414
        } else {
415
            // transform extension
416 1
            if (isset($extTransform[$imageMimeType])) {
417 1
                $imgExt = $extTransform[$imageMimeType];
418
            } else {
419
                throw new Exception("Unsupported image type $imageMimeType");
420
            }
421
422
            // add image to document
423 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

423
            $imgName = 'image_' . $rid . '_' . /** @scrutinizer ignore-type */ pathinfo($partFileName, PATHINFO_FILENAME) . '.' . $imgExt;
Loading history...
424
            //$this->zipClass->pclzipAddFile($imgPath, 'word/media/' . $imgName);
425 1
            $this->zipClass->addFile($imgPath, 'word/media/' . $imgName);
426
427 1
            $this->tempDocumentNewImages[$imgPath] = $imgName;
428
429
            // setup type for image
430 1
            $xmlImageType = str_replace(array('{IMG}', '{EXT}'), array($imgName, $imgExt), $typeTpl);
431 1
            $this->tempDocumentContentTypes = str_replace('</Types>', $xmlImageType, $this->tempDocumentContentTypes) . '</Types>';
432
        }
433
434 1
        $xmlImageRelation = str_replace(array('{RID}', '{IMG}'), array($rid, $imgName), $relationTpl);
435
436 1
        if (!isset($this->tempDocumentRelations[$partFileName])) {
437
            // create new relations file
438
            $this->tempDocumentRelations[$partFileName] = $newRelationsTpl;
439
            // and add it to content types
440
            $xmlRelationsType = str_replace('{RELS}', $this->getRelationsName($partFileName), $newRelationsTypeTpl);
441
            $this->tempDocumentContentTypes = str_replace('</Types>', $xmlRelationsType, $this->tempDocumentContentTypes) . '</Types>';
442
        }
443
444
        // add image to relations
445 1
        $this->tempDocumentRelations[$partFileName] = str_replace('</Relationships>', $xmlImageRelation, $this->tempDocumentRelations[$partFileName]) . '</Relationships>';
446 1
    }
447
448
    /**
449
     * @param mixed $search
450
     * @param mixed $replace Path to image, or array("path" => xx, "width" => yy, "height" => zz)
451
     * @param int $limit
452
     */
453 1
    public function setImageValue($search, $replace, ?int $limit = self::MAXIMUM_REPLACEMENTS_DEFAULT): void
454
    {
455
        // prepare $search_replace
456 1
        if (!is_array($search)) {
457
            $search = array($search);
458
        }
459
460 1
        $replacesList = array();
461 1
        if (!is_array($replace) || isset($replace['path'])) {
462
            $replacesList[] = $replace;
463
        } else {
464 1
            $replacesList = array_values($replace);
465
        }
466
467 1
        $searchReplace = array();
468 1
        foreach ($search as $searchIdx => $searchString) {
469 1
            $searchReplace[$searchString] = isset($replacesList[$searchIdx]) ? $replacesList[$searchIdx] : $replacesList[0];
470
        }
471
472
        // collect document parts
473
        $searchParts = array(
474 1
            $this->getMainPartName() => &$this->tempDocumentMainPart,
475
        );
476 1
        foreach (array_keys($this->tempDocumentHeaders) as $headerIndex) {
477
            $searchParts[$this->getHeaderName($headerIndex)] = &$this->tempDocumentHeaders[$headerIndex];
478
        }
479 1
        foreach (array_keys($this->tempDocumentFooters) as $headerIndex) {
480
            $searchParts[$this->getFooterName($headerIndex)] = &$this->tempDocumentFooters[$headerIndex];
481
        }
482
483
        // define templates
484
        // result can be verified via "Open XML SDK 2.5 Productivity Tool" (http://www.microsoft.com/en-us/download/details.aspx?id=30425)
485 1
        $imgTpl = '<w:pict><v:shape type="#_x0000_t75" style="width:{WIDTH};height:{HEIGHT}" stroked="f"><v:imagedata r:id="{RID}" o:title=""/></v:shape></w:pict>';
486
487 1
        foreach ($searchParts as $partFileName => &$partContent) {
488 1
            $partVariables = $this->getVariablesForPart($partContent);
489
490 1
            foreach ($searchReplace as $searchString => $replaceImage) {
491
                $varsToReplace = array_filter($partVariables, function ($partVar) use ($searchString) {
492 1
                    return ($partVar == $searchString) || preg_match('/^' . preg_quote($searchString) . ':/', $partVar);
493 1
                });
494
495 1
                foreach ($varsToReplace as $varNameWithArgs) {
496 1
                    $varInlineArgs = $this->getImageArgs($varNameWithArgs);
497 1
                    $preparedImageAttrs = $this->prepareImageAttrs($replaceImage, $varInlineArgs);
498 1
                    $imgPath = $preparedImageAttrs['src'];
499
500
                    // get image index
501 1
                    $imgIndex = $this->getNextRelationsIndex($partFileName);
502 1
                    $rid = 'rId' . $imgIndex;
503
504
                    // replace preparations
505 1
                    $this->addImageToRelations($partFileName, $rid, $imgPath, $preparedImageAttrs['mime']);
506 1
                    $xmlImage = str_replace(array('{RID}', '{WIDTH}', '{HEIGHT}'), array($rid, $preparedImageAttrs['width'], $preparedImageAttrs['height']), $imgTpl);
507
508
                    // replace variable
509 1
                    $varNameWithArgsFixed = self::ensureMacroCompleted($varNameWithArgs);
510 1
                    $matches = array();
511 1
                    if (preg_match('/(<[^<]+>)([^<]*)(' . preg_quote($varNameWithArgsFixed) . ')([^>]*)(<[^>]+>)/Uu', $partContent, $matches)) {
512 1
                        $wholeTag = $matches[0];
513 1
                        array_shift($matches);
514 1
                        list($openTag, $prefix, , $postfix, $closeTag) = $matches;
515 1
                        $replaceXml = $openTag . $prefix . $closeTag . $xmlImage . $openTag . $postfix . $closeTag;
516
                        // replace on each iteration, because in one tag we can have 2+ inline variables => before proceed next variable we need to change $partContent
517 1
                        $partContent = $this->setValueForPart($wholeTag, $replaceXml, $partContent, $limit);
518
                    }
519
                }
520
            }
521
        }
522 1
    }
523
524
    /**
525
     * Find and replace macros in the given XML section.
526
     *
527
     * @param mixed $search
528
     * @param mixed $replace
529
     * @param string $documentPartXML
530
     * @param int $limit
531
     *
532
     * @return string
533
     */
534 1
    protected function setValueForPart($search, $replace, string $documentPartXML, ?int $limit): string
535
    {
536
        // Note: we can't use the same function for both cases here, because of performance considerations.
537 1
        if (self::MAXIMUM_REPLACEMENTS_DEFAULT === $limit) {
538
            return str_replace($search, $replace, $documentPartXML);
539
        }
540 1
        $regExpEscaper = new RegExp();
541
542 1
        return preg_replace($regExpEscaper->escape($search), $replace, $documentPartXML, $limit);
0 ignored issues
show
Bug introduced by
It seems like $limit can also be of type null; however, parameter $limit of preg_replace() does only seem to accept integer, 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

542
        return preg_replace($regExpEscaper->escape($search), $replace, $documentPartXML, /** @scrutinizer ignore-type */ $limit);
Loading history...
543
    }
544
545
    /**
546
     * Get document.xml contents as DOMDocument
547
     *
548
     * @return DOMDocument
549
     */
550 8
    public function getDOMDocument(): DOMDocument
551
    {
552 8
        $dom = new DOMDocument();
553 8
        $dom->loadXML($this->document);
554 8
        return $dom;
555
    }
556
557
    /**
558
     * Update document.xml contents
559
     *
560
     * @param DOMDocument $dom - new contents
561
     */
562 6
    public function updateDOMDocument(DOMDocument $dom): void
563
    {
564 6
        $this->document = $dom->saveXml();
565 6
        file_put_contents($this->tmpDir . "/word/document.xml", $this->document);
566 6
    }
567
568
    /**
569
     * Fix table corruption
570
     *
571
     * @param string $xml - xml to fix
572
     *
573
     * @return DOMDocument
574
     */
575 6
    public function fixTables(string $xml): DOMDocument
576
    {
577 6
        $dom = new DOMDocument();
578 6
        $dom->loadXML($xml);
579 6
        $tables = $dom->getElementsByTagName('tbl');
580 6
        foreach ($tables as $table) {
581 2
            $columns = [];
582 2
            $columnsLen = 0;
583 2
            $toAdd = 0;
584 2
            $tableGrid = null;
585 2
            foreach ($table->childNodes as $el) {
586 2
                if ($el->nodeName == 'w:tblGrid') {
587 2
                    $tableGrid = $el;
588 2
                    foreach ($el->childNodes as $col) {
589 2
                        if ($col->nodeName == 'w:gridCol') {
590 2
                            $columns[] = $col;
591 2
                            $columnsLen += 1;
592
                        }
593
                    }
594 2
                } elseif ($el->nodeName == 'w:tr') {
595 2
                    $cellsLen = 0;
596 2
                    foreach ($el->childNodes as $col) {
597 2
                        if ($col->nodeName == 'w:tc') {
598 2
                            $cellsLen += 1;
599
                        }
600
                    }
601 2
                    if (($columnsLen + $toAdd) < $cellsLen) {
602 2
                        $toAdd = $cellsLen - $columnsLen;
603
                    }
604
                }
605
            }
606
607
            // add columns, if necessary
608 2
            if (!is_null($tableGrid) && $toAdd > 0) {
609
                $width = 0;
610
                foreach ($columns as $col) {
611
                    if (!is_null($col->getAttribute('w:w'))) {
612
                        $width += $col->getAttribute('w:w');
613
                    }
614
                }
615
                if ($width > 0) {
616
                    $oldAverage = $width / $columnsLen;
617
                    $newAverage = round($width / ($columnsLen + $toAdd));
618
                    foreach ($columns as $col) {
619
                        $col->setAttribute('w:w', round($col->getAttribute('w:w') * $newAverage / $oldAverage));
620
                    }
621
                    while ($toAdd > 0) {
622
                        $newCol = $dom->createElement("w:gridCol");
623
                        $newCol->setAttribute('w:w', $newAverage);
624
                        $tableGrid->appendChild($newCol);
625
                        $toAdd -= 1;
626
                    }
627
                }
628
            }
629
630
            // remove columns, if necessary
631 2
            $columns = [];
632 2
            foreach ($tableGrid->childNodes as $col) {
633 2
                if ($col->nodeName == 'w:gridCol') {
634 2
                    $columns[] = $col;
635
                }
636
            }
637 2
            $columnsLen = count($columns);
638
639 2
            $cellsLen = 0;
640 2
            $cellsLenMax = 0;
641 2
            foreach ($table->childNodes as $el) {
642 2
                if ($el->nodeName == 'w:tr') {
643 2
                    $cells = [];
644 2
                    foreach ($el->childNodes as $col) {
645 2
                        if ($col->nodeName == 'w:tc') {
646 2
                            $cells[] = $col;
647
                        }
648
                    }
649 2
                    $cellsLen = $this->getCellLen($cells);
650 2
                    $cellsLenMax = max($cellsLenMax, $cellsLen);
651
                }
652
            }
653 2
            $toRemove = $cellsLen - $cellsLenMax;
654 2
            if ($toRemove > 0) {
655
                $removedWidth = 0.0;
656
                for ($i = $columnsLen - 1; ($i + 1) >= $toRemove; $i -= 1) {
657
                    $extraCol = $columns[$i];
658
                    $removedWidth += $extraCol->getAttribute('w:w');
659
                    $tableGrid->removeChild($extraCol);
660
                }
661
662
                $columnsLeft = [];
663
                foreach ($tableGrid->childNodes as $col) {
664
                    if ($col->nodeName == 'w:gridCol') {
665
                        $columnsLeft[] = $col;
666
                    }
667
                }
668
                $extraSpace = 0;
669
                if (count($columnsLeft) > 0) {
670
                    $extraSpace = $removedWidth / count($columnsLeft);
671
                }
672
                foreach ($columnsLeft as $col) {
673 2
                    $col->setAttribute('w:w', round($col->getAttribute('w:w') + $extraSpace));
674
                }
675
            }
676
        }
677 6
        return $dom;
678
    }
679
680
    /**
681
     * Get total cells length
682
     *
683
     * @param array $cells - cells
684
     *
685
     * @return int
686
     */
687 2
    private function getCellLen(array $cells): int
688
    {
689 2
        $total = 0;
690 2
        foreach ($cells as $cell) {
691 2
            foreach ($cell->childNodes as $tc) {
692 2
                if ($tc->nodeName == 'w:tcPr') {
693 2
                    foreach ($tc->childNodes as $span) {
694 2
                        if ($span->nodeName == 'w:gridSpan') {
695 1
                            $total += intval($span->getAttribute('w:val'));
696 2
                            break;
697
                        }
698
                    }
699 2
                    break;
700
                }
701
            }
702
        }
703 2
        return $total + 1;
704
    }
705
706
    /**
707
     * Save the document to the target path
708
     *
709
     * @param string $path - target path
710
     */
711 2
    public function save(string $path): void
712
    {
713 2
        $rootPath = realpath($this->tmpDir);
714
715 2
        $zip = new ZipArchive();
716 2
        $zip->open($path, ZipArchive::CREATE | ZipArchive::OVERWRITE);
717
718 2
        $files = new RecursiveIteratorIterator(
719 2
            new RecursiveDirectoryIterator($rootPath),
720 2
            RecursiveIteratorIterator::LEAVES_ONLY
721
        );
722
723 2
        foreach ($files as $name => $file) {
724 2
            if (!$file->isDir()) {
725 2
                $filePath = $file->getRealPath();
726 2
                $relativePath = substr($filePath, strlen($rootPath) + 1);
727 2
                $zip->addFile($filePath, $relativePath);
728
            }
729
        }
730
731 2
        $zip->close();
732
733 2
        if (isset($this->zipClass)) {
734 2
            $this->zipClass->close();
735
        }
736
737 2
        $this->rrmdir($this->tmpDir);
738 2
    }
739
740
    /**
741
     * Remove recursively directory
742
     *
743
     * @param string $dir - target directory
744
     */
745 7
    private function rrmdir(string $dir): void
746
    {
747 7
        $objects = scandir($dir);
748 7
        if (is_array($objects)) {
0 ignored issues
show
introduced by
The condition is_array($objects) is always true.
Loading history...
749 7
            foreach ($objects as $object) {
750 7
                if ($object != "." && $object != "..") {
751 7
                    if (filetype($dir . "/" . $object) == "dir") {
752 7
                        $this->rrmdir($dir . "/" . $object);
753
                    } else {
754 7
                        unlink($dir . "/" . $object);
755
                    }
756
                }
757
            }
758 7
            reset($objects);
759 7
            rmdir($dir);
760
        }
761 7
    }
762
763
    /**
764
     * Close document
765
     */
766 5
    public function close(): void
767
    {
768 5
        if (isset($this->zipClass)) {
769 5
            $this->zipClass->close();
770
        }
771 5
        $this->rrmdir($this->tmpDir);
772 5
    }
773
}
774