PhpDocxTemplate::patchXmlChunk()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 79
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 41
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 51
nc 2
nop 1
dl 0
loc 79
rs 9.069
c 0
b 0
f 0
ccs 41
cts 41
cp 1
crap 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace PhpDocxTemplate;
4
5
use DOMDocument;
6
use Twig\Extension\ExtensionInterface;
7
use Twig\Loader\ArrayLoader;
8
use Twig\Environment;
9
use PhpDocxTemplate\Twig\Impl\{
10
    ImageExtension,
11
    ImageRenderer,
12
    QrCodeExtension,
13
    QrCodeRenderer
14
};
15
16
/**
17
 * Class PhpDocxTemplate
18
 *
19
 * @package PhpDocxTemplate
20
 */
21
class PhpDocxTemplate
22
{
23
    private const NEWLINE_XML = '</w:t><w:br/><w:t xml:space="preserve">';
24
    private const NEWPARAGRAPH_XML = '</w:t></w:r></w:p><w:p><w:r><w:t xml:space="preserve">';
25
    private const TAB_XML = '</w:t></w:r><w:r><w:tab/></w:r><w:r><w:t xml:space="preserve">';
26
    private const PAGE_BREAK = '</w:t><w:br w:type="page"/><w:t xml:space="preserve">';
27
28
    private const HEADER_URI = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header";
29
    private const FOOTER_URI = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer";
30
31
    private $docx;
32
    private $crcToNewMedia;
33
    private $crcToNewEmbedded;
34
    private $picToReplace;
35
    private $picMap;
36
37
    private $twigExtensions = [];
38
39
    /**
40
     * Construct an instance of PhpDocxTemplate
41
     *
42
     * @param string $path - path to the template
43
     */
44 12
    public function __construct(string $path)
45
    {
46 12
        $this->docx = new DocxDocument($path);
47 12
        $this->crcToNewMedia = [];
48 12
        $this->crcToNewEmbedded = [];
49 12
        $this->picToReplace = [];
50 12
        $this->picMap = [];
51 12
    }
52
53
    /**
54
     * Convert DOM to string
55
     *
56
     * @param DOMDocument $dom - DOM to be converted
57
     *
58
     * @return string
59
     */
60 10
    public function xmlToString(DOMDocument $dom): string
61
    {
62
        //return $el->ownerDocument->saveXML($el);
63 10
        return $dom->saveXML();
64
    }
65
66
    /**
67
     * Get document wrapper
68
     *
69
     * @return DocxDocument
70
     */
71 3
    public function getDocx(): DocxDocument
72
    {
73 3
        return $this->docx;
74
    }
75
76
    /**
77
     * Convert document.xml contents as string
78
     *
79
     * @return string
80
     */
81 9
    public function getXml(): string
82
    {
83 9
        return $this->xmlToString($this->docx->getDOMDocument());
84
    }
85
86
    /**
87
     * Write document.xml contents to file
88
     */
89
    private function writeXml(string $path): void
0 ignored issues
show
Unused Code introduced by
The method writeXml() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
90
    {
91
        file_put_contents($path, $this->getXml());
92
    }
93
94
    /**
95
     * Update document.xml contents to file
96
     *
97
     * @param DOMDocument $xml - new contents
98
     */
99 7
    private function updateXml(DOMDocument $xml): void
100
    {
101 7
        $this->docx->updateDOMDocument($xml);
102 7
    }
103
104 8
    public function patchXml(string $xml): string
105
    {
106 8
        $matches = [];
107
108 8
        preg_match('/^.*?(<w:body>)/s', $xml, $matches);
109
110 8
        $beforeXml = $matches[0];
111
112 8
        preg_match('/(<\/w:body>).*?$/s', $xml, $matches);
113
114 8
        $afterXml = $matches[0];
115
116 8
        $dom = new DOMDocument();
117 8
        $dom->loadXML($xml);
118
119 8
        $elBody = $dom->getElementsByTagName('body')->item(0);
120
121 8
        $chunkXml = '';
122
123 8
        for ($itemIdx = 0; $itemIdx < $elBody->childNodes->count(); $itemIdx++) {
124 8
            $el = $elBody->childNodes->item($itemIdx);
125
126 8
            $chunkXml .= $this->patchXmlChunk($el->ownerDocument->saveXML($el));
0 ignored issues
show
Bug introduced by
The method saveXML() does not exist on null. ( Ignorable by Annotation )

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

126
            $chunkXml .= $this->patchXmlChunk($el->ownerDocument->/** @scrutinizer ignore-call */ saveXML($el));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
127
        }
128
129 8
        return sprintf('%s%s%s', $beforeXml, $chunkXml, $afterXml);
130
    }
131
132
    /**
133
     * Patch initial xml
134
     *
135
     * @param string $xml - initial xml
136
     */
137 9
    public function patchXmlChunk(string $xml): string
138
    {
139 9
        $xml = preg_replace('/(?<={)(<[^>]*>)+(?=[\{%\#])|(?<=[%\}\#])(<[^>]*>)+(?=\})/mu', '', $xml);
140 9
        $xml = preg_replace_callback(
141 9
            '/{%(?:(?!%}).)*|{#(?:(?!#}).)*|{{(?:(?!}}).)*/mu',
142 9
            array(get_class($this), 'stripTags'),
143
            $xml
144
        );
145 9
        $xml = preg_replace_callback(
146 9
            '/(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*colspan\s+([^%]*)\s*%}(.*?<\/w:tc>)/mu',
147 9
            array(get_class($this), 'colspan'),
148
            $xml
149
        );
150 9
        $xml = preg_replace_callback(
151 9
            '/(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*cellbg\s+([^%]*)\s*%}(.*?<\/w:tc>)/mu',
152 9
            array(get_class($this), 'cellbg'),
153
            $xml
154
        );
155
        // avoid {{r and {%r tags to strip MS xml tags too far
156
        // ensure space preservation when splitting
157 9
        $xml = preg_replace(
158 9
            '/<w:t>((?:(?!<w:t>).)*)({{r\s.*?}}|{%r\s.*?%})/mu',
159 9
            '<w:t xml:space="preserve">${1}${2}',
160
            $xml
161
        );
162 9
        $xml = preg_replace(
163 9
            '/({{r\s.*?}}|{%r\s.*?%})/mu',
164 9
            '</w:t></w:r><w:r><w:t xml:space="preserve">${1}</w:t></w:r><w:r><w:t xml:space="preserve">',
165
            $xml
166
        );
167
168
        // {%- will merge with previous paragraph text
169 9
        $xml = preg_replace(
170 9
            '/<\/w:t>(?:(?!<\/w:t>).)*?{%-/mu',
171 9
            '{%',
172
            $xml
173
        );
174
175
        // -%} will merge with next paragraph text
176 9
        $xml = preg_replace(
177 9
            '/-%}(?:(?!<w:t[ >]).)*?<w:t[^>]*?>/mu',
178 9
            '%}',
179
            $xml
180
        );
181
182
        // replace into xml code the row/paragraph/run containing
183
        // {%y xxx %} or {{y xxx}} template tag
184
        // by {% xxx %} or {{ xx }} without any surronding <w:y> tags
185 9
        $tokens = ['tr', 'tc', 'p', 'r'];
186 9
        foreach ($tokens as $token) {
187 9
            $regex = '/';
188 9
            $regex .= str_replace("%s", $token, '<w:%s[ >](?:(?!<w:%s[ >]).)*({%|{{)%s ([^}%]*(?:%}|}})).*?<\/w:%s>');
189 9
            $regex .= '/mu';
190 9
            $xml = preg_replace(
191 9
                $regex,
192 9
                '${1} ${2}',
193
                $xml
194
            );
195
        }
196
197 9
        $xml = preg_replace_callback(
198 9
            '/<w:tc[ >](?:(?!<w:tc[ >]).)*?{%\s*vm\s*%}.*?<\/w:tc[ >]/mu',
199 9
            array(get_class($this), 'vMergeTc'),
200
            $xml
201
        );
202
203 9
        $xml = preg_replace_callback(
204 9
            '/<w:tc[ >](?:(?!<w:tc[ >]).)*?{%\s*hm\s*%}.*?<\/w:tc[ >]/mu',
205 9
            array(get_class($this), 'hMergeTc'),
206
            $xml
207
        );
208
209 9
        $xml = preg_replace_callback(
210 9
            '/(?<=\{[\{%])(.*?)(?=[\}%]})/mu',
211 9
            array(get_class($this), 'cleanTags'),
212
            $xml
213
        );
214
215 9
        return $xml;
216
    }
217
218 8
    private function resolveListing(string $xml): string
219
    {
220 8
        return preg_replace_callback(
221 8
            '/<w:p\b(?:[^>]*)?>.*?<\/w:p>/mus',
222 8
            array(get_class($this), 'resolveParagraph'),
223
            $xml
224
        );
225
    }
226
227 8
    private function resolveParagraph(array $matches): string
228
    {
229 8
        preg_match("/<w:pPr>.*<\/w:pPr>/mus", $matches[0], $paragraphProperties);
230
231 8
        return preg_replace_callback(
232 8
            '/<w:r\b(?:[^>]*)?>.*?<\/w:r>/mus',
233
            function ($m) use ($paragraphProperties) {
234 8
                return $this->resolveRun($paragraphProperties[0] ?? '', $m);
235 8
            },
236 8
            $matches[0]
237
        );
238
    }
239
240 8
    private function resolveRun(string $paragraphProperties, array $matches): string
241
    {
242 8
        preg_match("/<w:rPr>.*<\/w:rPr>/mus", $matches[0], $runProperties);
243
244 8
        return preg_replace_callback(
245 8
            '/<w:t\b(?:[^>]*)?>.*?<\/w:t>/mus',
246
            function ($m) use ($paragraphProperties, $runProperties) {
247 8
                return $this->resolveText($paragraphProperties, $runProperties[0] ?? '', $m);
248 8
            },
249 8
            $matches[0]
250
        );
251
    }
252
253 8
    private function resolveText(string $paragraphProperties, string $runProperties, array $matches): string
254
    {
255 8
        $xml = str_replace(
256 8
            "\t",
257 8
            sprintf("</w:t></w:r>" .
258
                "<w:r>%s<w:tab/></w:r>" .
259 8
                "<w:r>%s<w:t xml:space=\"preserve\">", $runProperties, $runProperties),
260 8
            $matches[0]
261
        );
262
263 8
        $xml = str_replace(
264 8
            "\a",
265 8
            sprintf("</w:t></w:r></w:p>" .
266 8
                "<w:p>%s<w:r>%s<w:t xml:space=\"preserve\">", $paragraphProperties, $runProperties),
267
            $xml
268
        );
269
270 8
        $xml = str_replace("\n", sprintf("</w:t>" .
271
            "</w:r>" .
272
            "</w:p>" .
273
            "<w:p>%s" .
274
            "<w:r>%s" .
275 8
            "<w:t xml:space=\"preserve\">", $paragraphProperties, $runProperties), $xml);
276
277 8
        $xml = str_replace(
278 8
            "\f",
279 8
            sprintf("</w:t></w:r></w:p>" .
280
                "<w:p><w:r><w:br w:type=\"page\"/></w:r></w:p>" .
281 8
                "<w:p>%s<w:r>%s<w:t xml:space=\"preserve\">", $paragraphProperties, $runProperties),
282
            $xml
283
        );
284
285 8
        return $xml;
286
    }
287
288
    /**
289
     * Strip tags from matches
290
     *
291
     * @param array $matches - matches
292
     *
293
     * @return string
294
     */
295 9
    private static function stripTags(array $matches): string
296
    {
297 9
        return preg_replace('/<\/w:t>.*?(<w:t>|<w:t [^>]*>)/mu', '', $matches[0]);
298
    }
299
300
    /**
301
     * Parse colspan
302
     *
303
     * @param array $matches - matches
304
     *
305
     * @return string
306
     */
307 1
    private static function colspan(array $matches): string
308
    {
309 1
        $cellXml = $matches[1] . $matches[3];
310 1
        $cellXml = preg_replace('/<w:r[ >](?:(?!<w:r[ >]).)*<w:t><\/w:t>.*?<\/w:r>/mu', '', $cellXml);
311 1
        $cellXml = preg_replace('/<w:gridSpan[^\/]*\/>/mu', '', $cellXml, 1);
312 1
        return preg_replace(
313 1
            '/(<w:tcPr[^>]*>)/mu',
314 1
            sprintf('${1}<w:gridSpan w:val="{{%s}}"/>', $matches[2]),
315
            $cellXml
316
        );
317
    }
318
319
    /**
320
     * Parse cellbg
321
     *
322
     * @param array $matches - matches
323
     *
324
     * @return string
325
     */
326 1
    private function cellbg(array $matches): string
327
    {
328 1
        $cellXml = $matches[1] . $matches[3];
329 1
        $cellXml = preg_replace('/<w:r[ >](?:(?!<w:r[ >]).)*<w:t><\/w:t>.*?<\/w:r>/mu', '', $cellXml);
330 1
        $cellXml = preg_replace('/<w:shd[^\/]*\/>/mu', '', $cellXml, 1);
331 1
        return preg_replace(
332 1
            '/(<w:tcPr[^>]*>)/mu',
333 1
            sprintf('${1}<w:shd w:val="clear" w:color="auto" w:fill="{{%s}}"/>', $matches[2]),
334
            $cellXml
335
        );
336
    }
337
338
    /**
339
     * Parse vm
340
     *
341
     * @param array $matches - matches
342
     *
343
     * @return string
344
     */
345 1
    private function vMergeTc(array $matches): string
346
    {
347 1
        return preg_replace_callback(
348 1
            '/(<\/w:tcPr[ >].*?<w:t(?:.*?)>)(.*?)(?:{%\s*vm\s*%})(.*?)(<\/w:t>)/mu',
349 1
            array(get_class($this), 'vMerge'),
350 1
            $matches[0]
351
        );
352
    }
353
354
    /**
355
     * Continue parsing vm
356
     *
357
     * @param array $matches - matches
358
     *
359
     * @return string
360
     */
361 1
    private function vMerge(array $matches): string
362
    {
363
        return '<w:vMerge w:val="{% if loop.first %}restart{% else %}continue{% endif %}"/>' .
364 1
            $matches[1] .  // Everything between ``</w:tcPr>`` and ``<w:t>``.
365 1
            "{% if loop.first %}" .
366 1
            $matches[2] .  // Everything before ``{% vm %}``.
367 1
            $matches[3] .  // Everything after ``{% vm %}``.
368 1
            "{% endif %}" .
369 1
            $matches[4];  // ``</w:t>``.
370
    }
371
372
    /**
373
     * Parse hm
374
     *
375
     * @param array $matches - matches
376
     *
377
     * @return string
378
     */
379 1
    private function hMergeTc(array $matches): string
380
    {
381 1
        $xmlToPatch = $matches[0];
382 1
        if (strpos($xmlToPatch, 'w:gridSpan') !== false) {
383
            $xmlToPatch = preg_replace_callback(
384
                '/(w:gridSpan w:val=")(\d+)(")/mu',
385
                array(get_class($this), 'withGridspan'),
386
                $xmlToPatch
387
            );
388
            $xmlToPatch = preg_replace('/{%\s*hm\s*%}/mu', '', $xmlToPatch);
389
        } else {
390 1
            $xmlToPatch = preg_replace_callback(
391 1
                '/(<\/w:tcPr[ >].*?<w:t(?:.*?)>)(.*?)(?:{%\s*hm\s*%})(.*?)(<\/w:t>)/mu',
392 1
                array(get_class($this), 'withoutGridspan'),
393
                $xmlToPatch
394
            );
395
        }
396
397 1
        return "{% if loop.first %}" . $xmlToPatch . "{% endif %}";
398
    }
399
400
    private function withGridspan(array $matches): string
401
    {
402
        return $matches[1] . // ``w:gridSpan w:val="``.
403
            '{{ ' . $matches[2] . ' * loop.length }}' . // Content of ``w:val``, multiplied by loop length.
404
            $matches[3];  // Closing quotation mark.
405
    }
406
407 1
    private function withoutGridspan(array $matches): string
408
    {
409
        return '<w:gridSpan w:val="{{ loop.length }}"/>' .
410 1
            $matches[1] . // Everything between ``</w:tcPr>`` and ``<w:t>``.
411 1
            $matches[2] . // Everything before ``{% hm %}``.
412 1
            $matches[3] . // Everything after ``{% hm %}``.
413 1
            $matches[4]; // ``</w:t>``.
414
    }
415
416
    /**
417
     * Clean tags in matches
418
     *
419
     * @param array $matches - matches
420
     *
421
     * @return string
422
     */
423 9
    private function cleanTags(array $matches): string
424
    {
425 9
        return str_replace(
426 9
            ["&#8216;", '&lt;', '&gt;', '“', '”', "‘", "’"],
427 9
            ["'", '<', '>', '"', '"', "'", "'"],
428 9
            $matches[0]
429
        );
430
    }
431
432
    /**
433
     * Добавить пользовательское расширение
434
     *
435
     * @param ExtensionInterface $ext
436
     */
437
    public function addTwigExtensions(ExtensionInterface $ext): void
438
    {
439
        $this->twigExtensions[] = $ext;
440
    }
441
442
    /**
443
     * Render xml
444
     *
445
     * @param string $srcXml - source xml
446
     * @param array $context - data to be rendered
447
     *
448
     * @return string
449
     */
450 8
    private function renderXml(string $srcXml, array $context): string
451
    {
452 8
        $srcXml = str_replace('<w:p>', "\n<w:p>", $srcXml);
453
454 8
        $template = new Environment(new ArrayLoader([
455 8
            'index' => $srcXml,
456
        ]));
457
458
        /**
459
         * Пользовательские расширения
460
         */
461 8
        foreach ($this->twigExtensions as $ext) {
462
            $template->addExtension($ext);
463
        }
464
465 8
        $ext = new ImageExtension();
466 8
        $ext->setRenderer(
467 8
            new ImageRenderer($this)
468
        );
469 8
        $template->addExtension($ext);
470
471
472 8
        $ext = new QrCodeExtension();
473 8
        $ext->setRenderer(
474 8
            new QrCodeRenderer($this)
475
        );
476 8
        $template->addExtension($ext);
477
478 8
        $dstXml = $template->render('index', $context);
479
480 8
        $dstXml = str_replace(
481 8
            ["\n<w:p>", "{_{", '}_}', '{_%', '%_}'],
482 8
            ['<w:p>', "{{", '}}', '{%', '%}'],
483
            $dstXml
484
        );
485
486
        // fix xml after rendering
487 8
        $dstXml = preg_replace(
488 8
            '/<w:p [^>]*>(?:<w:r [^>]*><w:t [^>]*>\s*<\/w:t><\/w:r>)?(?:<w:pPr><w:ind w:left="360"\/>' .
489 8
            '<\/w:pPr>)?<w:r [^>]*>(?:<w:t\/>|<w:t [^>]*><\/w:t>|<w:t [^>]*\/>|<w:t><\/w:t>)<\/w:r><\/w:p>/mu',
490 8
            '',
491
            $dstXml
492
        );
493
494 8
        $dstXml = $this->resolveListing($dstXml);
495
496 8
        return $dstXml;
497
    }
498
499
    /**
500
     * Build xml
501
     *
502
     * @param array $context - data to be rendered
503
     *
504
     * @return string
505
     */
506 8
    public function buildXml(array $context): string
507
    {
508 8
        $xml = $this->getXml();
509 8
        $xml = $this->patchXml($xml);
510 8
        $xml = $this->renderXml($xml, $context);
511 8
        return $xml;
512
    }
513
514
    /**
515
     * Render document
516
     *
517
     * @param array $context - data to be rendered
518
     */
519 7
    public function render(array $context): void
520
    {
521 7
        $xmlSrc = $this->buildXml($context);
522 7
        $newXml = $this->docx->fixTables($xmlSrc);
523 7
        $this->updateXml($newXml);
524
525 7
        $this->renderHeaders($context);
526 7
        $this->renderFooters($context);
527 7
    }
528
529
    /**
530
     * Save document
531
     *
532
     * @param string $path - target path
533
     */
534 3
    public function save(string $path): void
535
    {
536
        //$this->preProcessing();
537 3
        $this->docx->save($path);
538
        //$this->postProcessing($path);
539 3
    }
540
541 7
    public function renderHeaders(array $context): void
542
    {
543
        $this->docx->setHeaders(array_map(function ($header) use ($context) {
544 1
            return $this->renderXml($header, $context);
545 7
        }, $this->docx->getHeaders()));
546 7
    }
547
548 7
    public function renderFooters(array $context): void
549
    {
550
        $this->docx->setFooters(array_map(function ($footer) use ($context) {
551 1
            return $this->renderXml($footer, $context);
552 7
        }, $this->docx->getFooters()));
553 7
    }
554
555
    /**
556
     * Clean everything after rendering
557
     */
558 5
    public function close(): void
559
    {
560 5
        $this->docx->close();
561 5
    }
562
}
563