Test Failed
Pull Request — master (#12)
by
unknown
02:50
created

PhpDocxTemplate::renderXml()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 47
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 2

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 2
eloc 25
c 2
b 0
f 1
nc 2
nop 2
dl 0
loc 47
ccs 17
cts 17
cp 1
crap 2
rs 9.52
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 12
     * @param string $path - path to the template
43
     */
44 12
    public function __construct(string $path)
45 12
    {
46 12
        $this->docx = new DocxDocument($path);
47 12
        $this->crcToNewMedia = [];
48 12
        $this->crcToNewEmbedded = [];
49 12
        $this->picToReplace = [];
50
        $this->picMap = [];
51
    }
52
53
    /**
54
     * Convert DOM to string
55
     *
56
     * @param DOMDocument $dom - DOM to be converted
57
     *
58 10
     * @return string
59
     */
60
    public function xmlToString(DOMDocument $dom): string
61 10
    {
62
        //return $el->ownerDocument->saveXML($el);
63
        return $dom->saveXML();
64
    }
65
66
    /**
67
     * Get document wrapper
68
     *
69 3
     * @return DocxDocument
70
     */
71 3
    public function getDocx(): DocxDocument
72
    {
73
        return $this->docx;
74
    }
75
76
    /**
77
     * Convert document.xml contents as string
78
     *
79 9
     * @return string
80
     */
81 9
    public function getXml(): string
82
    {
83
        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 7
     * @param DOMDocument $xml - new contents
98
     */
99 7
    private function updateXml(DOMDocument $xml): void
100 7
    {
101
        $this->docx->updateDOMDocument($xml);
102
    }
103
104
    public function patchXml(string $xml): string
105
    {
106
        $matches = [];
107 9
108
        preg_match('/^.*?(<w:body>)/s', $xml, $matches);
109 9
110 9
        $beforeXml = $matches[0];
111 9
112 9
        preg_match('/(<\/w:body>).*?$/s', $xml, $matches);
113
114
        $afterXml = $matches[0];
115 9
116 9
        $dom = new DOMDocument();
117 9
        $dom->loadXML($xml);
118
119
        $elBody = $dom->getElementsByTagName('body')->item(0);
120 9
121 9
        $chunkXml = '';
122 9
123
        for ($itemIdx = 0; $itemIdx < $elBody->childNodes->count(); $itemIdx++) {
124
            $el = $elBody->childNodes->item($itemIdx);
125
126
            $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 9
        }
128 9
129 9
        return sprintf('%s%s%s', $beforeXml, $chunkXml, $afterXml);
130
    }
131
132 9
    /**
133 9
     * Patch initial xml
134 9
     *
135
     * @param string $xml - initial xml
136
     */
137
    public function patchXmlChunk(string $xml): string
138
    {
139 9
        $xml = preg_replace('/(?<={)(<[^>]*>)+(?=[\{%\#])|(?<=[%\}\#])(<[^>]*>)+(?=\})/mu', '', $xml);
140 9
        $xml = preg_replace_callback(
141 9
            '/{%(?:(?!%}).)*|{#(?:(?!#}).)*|{{(?:(?!}}).)*/mu',
142
            array(get_class($this), 'stripTags'),
143
            $xml
144
        );
145
        $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 9
            $xml
149
        );
150
        $xml = preg_replace_callback(
151
            '/(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*cellbg\s+([^%]*)\s*%}(.*?<\/w:tc>)/mu',
152
            array(get_class($this), 'cellbg'),
153
            $xml
154
        );
155 9
        // avoid {{r and {%r tags to strip MS xml tags too far
156 9
        // 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 9
            $xml
161 9
        );
162 9
        $xml = preg_replace(
163
            '/({{r\s.*?}}|{%r\s.*?%})/mu',
164
            '</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 9
168 9
        // {%- will merge with previous paragraph text
169 9
        $xml = preg_replace(
170
            '/<\/w:t>(?:(?!<\/w:t>).)*?{%-/mu',
171
            '{%',
172
            $xml
173 9
        );
174 9
175 9
        // -%} will merge with next paragraph text
176
        $xml = preg_replace(
177
            '/-%}(?:(?!<w:t[ >]).)*?<w:t[^>]*?>/mu',
178
            '%}',
179 9
            $xml
180 9
        );
181 9
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
        foreach ($tokens as $token) {
187
            $regex = '/';
188 8
            $regex .= str_replace("%s", $token, '<w:%s[ >](?:(?!<w:%s[ >]).)*({%|{{)%s ([^}%]*(?:%}|}})).*?<\/w:%s>');
189
            $regex .= '/mu';
190 8
            $xml = preg_replace(
191 8
                $regex,
192 8
                '${1} ${2}',
193
                $xml
194
            );
195
        }
196
197 8
        $xml = preg_replace_callback(
198
            '/<w:tc[ >](?:(?!<w:tc[ >]).)*?{%\s*vm\s*%}.*?<\/w:tc[ >]/mu',
199 8
            array(get_class($this), 'vMergeTc'),
200
            $xml
201 8
        );
202 8
203
        $xml = preg_replace_callback(
204 8
            '/<w:tc[ >](?:(?!<w:tc[ >]).)*?{%\s*hm\s*%}.*?<\/w:tc[ >]/mu',
205 8
            array(get_class($this), 'hMergeTc'),
206 8
            $xml
207
        );
208
209
        $xml = preg_replace_callback(
210 8
            '/(?<=\{[\{%])(.*?)(?=[\}%]})/mu',
211
            array(get_class($this), 'cleanTags'),
212 8
            $xml
213
        );
214 8
215 8
        return $xml;
216
    }
217 8
218 8
    private function resolveListing(string $xml): string
219 8
    {
220
        return preg_replace_callback(
221
            '/<w:p\b(?:[^>]*)?>.*?<\/w:p>/mus',
222
            array(get_class($this), 'resolveParagraph'),
223 8
            $xml
224
        );
225 8
    }
226 8
227 8
    private function resolveParagraph(array $matches): string
228
    {
229 8
        preg_match("/<w:pPr>.*<\/w:pPr>/mus", $matches[0], $paragraphProperties);
230 8
231
        return preg_replace_callback(
232
            '/<w:r\b(?:[^>]*)?>.*?<\/w:r>/mus',
233 8
            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
        preg_match("/<w:rPr>.*<\/w:rPr>/mus", $matches[0], $runProperties);
243
244
        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 8
    }
252
253
    private function resolveText(string $paragraphProperties, string $runProperties, array $matches): string
254
    {
255 8
        $xml = str_replace(
256
            "\t",
257
            sprintf("</w:t></w:r>" .
258
                "<w:r>%s<w:tab/></w:r>" .
259
                "<w:r>%s<w:t xml:space=\"preserve\">", $runProperties, $runProperties),
260
            $matches[0]
261
        );
262
263
        $xml = str_replace(
264
            "\a",
265 9
            sprintf("</w:t></w:r></w:p>" .
266
                "<w:p>%s<w:r>%s<w:t xml:space=\"preserve\">", $paragraphProperties, $runProperties),
267 9
            $xml
268
        );
269
270
        $xml = str_replace("\n", sprintf("</w:t>" .
271
            "</w:r>" .
272
            "</w:p>" .
273
            "<w:p>%s" .
274
            "<w:r>%s" .
275
            "<w:t xml:space=\"preserve\">", $paragraphProperties, $runProperties), $xml);
276
277 1
        $xml = str_replace(
278
            "\f",
279 1
            sprintf("</w:t></w:r></w:p>" .
280 1
                "<w:p><w:r><w:br w:type=\"page\"/></w:r></w:p>" .
281 1
                "<w:p>%s<w:r>%s<w:t xml:space=\"preserve\">", $paragraphProperties, $runProperties),
282 1
            $xml
283 1
        );
284 1
285
        return $xml;
286
    }
287
288
    /**
289
     * Strip tags from matches
290
     *
291
     * @param array $matches - matches
292
     *
293
     * @return string
294
     */
295
    private static function stripTags(array $matches): string
296 1
    {
297
        return preg_replace('/<\/w:t>.*?(<w:t>|<w:t [^>]*>)/mu', '', $matches[0]);
298 1
    }
299 1
300 1
    /**
301 1
     * Parse colspan
302 1
     *
303 1
     * @param array $matches - matches
304
     *
305
     * @return string
306
     */
307
    private static function colspan(array $matches): string
308
    {
309
        $cellXml = $matches[1] . $matches[3];
310
        $cellXml = preg_replace('/<w:r[ >](?:(?!<w:r[ >]).)*<w:t><\/w:t>.*?<\/w:r>/mu', '', $cellXml);
311
        $cellXml = preg_replace('/<w:gridSpan[^\/]*\/>/mu', '', $cellXml, 1);
312
        return preg_replace(
313
            '/(<w:tcPr[^>]*>)/mu',
314
            sprintf('${1}<w:gridSpan w:val="{{%s}}"/>', $matches[2]),
315 1
            $cellXml
316
        );
317 1
    }
318 1
319 1
    /**
320 1
     * Parse cellbg
321
     *
322
     * @param array $matches - matches
323
     *
324
     * @return string
325
     */
326
    private function cellbg(array $matches): string
327
    {
328
        $cellXml = $matches[1] . $matches[3];
329
        $cellXml = preg_replace('/<w:r[ >](?:(?!<w:r[ >]).)*<w:t><\/w:t>.*?<\/w:r>/mu', '', $cellXml);
330
        $cellXml = preg_replace('/<w:shd[^\/]*\/>/mu', '', $cellXml, 1);
331 1
        return preg_replace(
332
            '/(<w:tcPr[^>]*>)/mu',
333
            sprintf('${1}<w:shd w:val="clear" w:color="auto" w:fill="{{%s}}"/>', $matches[2]),
334 1
            $cellXml
335 1
        );
336 1
    }
337 1
338 1
    /**
339 1
     * Parse vm
340
     *
341
     * @param array $matches - matches
342
     *
343
     * @return string
344
     */
345
    private function vMergeTc(array $matches): string
346
    {
347
        return preg_replace_callback(
348
            '/(<\/w:tcPr[ >].*?<w:t(?:.*?)>)(.*?)(?:{%\s*vm\s*%})(.*?)(<\/w:t>)/mu',
349 1
            array(get_class($this), 'vMerge'),
350
            $matches[0]
351 1
        );
352 1
    }
353
354
    /**
355
     * Continue parsing vm
356
     *
357
     * @param array $matches - matches
358
     *
359
     * @return string
360 1
     */
361 1
    private function vMerge(array $matches): string
362 1
    {
363
        return '<w:vMerge w:val="{% if loop.first %}restart{% else %}continue{% endif %}"/>' .
364
            $matches[1] .  // Everything between ``</w:tcPr>`` and ``<w:t>``.
365
            "{% if loop.first %}" .
366
            $matches[2] .  // Everything before ``{% vm %}``.
367 1
            $matches[3] .  // Everything after ``{% vm %}``.
368
            "{% endif %}" .
369
            $matches[4];  // ``</w:t>``.
370
    }
371
372
    /**
373
     * Parse hm
374
     *
375
     * @param array $matches - matches
376
     *
377 1
     * @return string
378
     */
379
    private function hMergeTc(array $matches): string
380 1
    {
381 1
        $xmlToPatch = $matches[0];
382 1
        if (strpos($xmlToPatch, 'w:gridSpan') !== false) {
383 1
            $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
            $xmlToPatch = preg_replace_callback(
391
                '/(<\/w:tcPr[ >].*?<w:t(?:.*?)>)(.*?)(?:{%\s*hm\s*%})(.*?)(<\/w:t>)/mu',
392
                array(get_class($this), 'withoutGridspan'),
393 9
                $xmlToPatch
394
            );
395 9
        }
396 9
397 9
        return "{% if loop.first %}" . $xmlToPatch . "{% endif %}";
398 9
    }
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
    private function withoutGridspan(array $matches): string
408
    {
409
        return '<w:gridSpan w:val="{{ loop.length }}"/>' .
410 8
            $matches[1] . // Everything between ``</w:tcPr>`` and ``<w:t>``.
411
            $matches[2] . // Everything before ``{% hm %}``.
412 8
            $matches[3] . // Everything after ``{% hm %}``.
413
            $matches[4]; // ``</w:t>``.
414 8
    }
415 8
416
    /**
417
     * Clean tags in matches
418
     *
419 8
     * @param array $matches - matches
420 8
     *
421 8
     * @return string
422
     */
423 8
    private function cleanTags(array $matches): string
424
    {
425
        return str_replace(
426 8
            ["&#8216;", '&lt;', '&gt;', '“', '”', "‘", "’"],
427 8
            ["'", '<', '>', '"', '"', "'", "'"],
428 8
            $matches[0]
429
        );
430 8
    }
431
432 8
    /**
433
     * Добавить пользовательское расширение
434 8
     *
435 8
     * @param ExtensionInterface $ext
436 8
     */
437
    public function addTwigExtensions(ExtensionInterface $ext): void
438
    {
439
        $this->twigExtensions[] = $ext;
440
    }
441 8
442 8
    /**
443 8
     * Render xml
444 8
     *
445
     * @param string $srcXml - source xml
446
     * @param array $context - data to be rendered
447
     *
448 8
     * @return string
449
     */
450 8
    private function renderXml(string $srcXml, array $context): string
451
    {
452
        $srcXml = str_replace('<w:p>', "\n<w:p>", $srcXml);
453
454
        $template = new Environment(new ArrayLoader([
455
            'index' => $srcXml,
456
        ]));
457
458
        /**
459
         * Пользовательские расширения
460 8
         */
461
        foreach ($this->twigExtensions as $ext) {
462 8
            $template->addExtension($ext);
463 8
        }
464 8
465 8
        $ext = new ImageExtension();
466
        $ext->setRenderer(
467
            new ImageRenderer($this)
468
        );
469
        $template->addExtension($ext);
470
471
472
        $ext = new QrCodeExtension();
473 7
        $ext->setRenderer(
474
            new QrCodeRenderer($this)
475 7
        );
476 7
        $template->addExtension($ext);
477 7
478
        $dstXml = $template->render('index', $context);
479 7
480 7
        $dstXml = str_replace(
481 7
            ["\n<w:p>", "{_{", '}_}', '{_%', '%_}'],
482
            ['<w:p>', "{{", '}}', '{%', '%}'],
483
            $dstXml
484
        );
485
486
        // fix xml after rendering
487
        $dstXml = preg_replace(
488 3
            '/<w:p [^>]*>(?:<w:r [^>]*><w:t [^>]*>\s*<\/w:t><\/w:r>)?(?:<w:pPr><w:ind w:left="360"\/>' .
489
            '<\/w:pPr>)?<w:r [^>]*>(?:<w:t\/>|<w:t [^>]*><\/w:t>|<w:t [^>]*\/>|<w:t><\/w:t>)<\/w:r><\/w:p>/mu',
490
            '',
491 3
            $dstXml
492
        );
493 3
494
        $dstXml = $this->resolveListing($dstXml);
495 7
496
        return $dstXml;
497
    }
498 1
499 7
    /**
500 7
     * Build xml
501
     *
502 7
     * @param array $context - data to be rendered
503
     *
504
     * @return string
505 1
     */
506 7
    public function buildXml(array $context): string
507 7
    {
508
        $xml = $this->getXml();
509
        $xml = $this->patchXml($xml);
510
        $xml = $this->renderXml($xml, $context);
511
        return $xml;
512 5
    }
513
514 5
    /**
515 5
     * Render document
516
     *
517
     * @param array $context - data to be rendered
518
     */
519
    public function render(array $context): void
520
    {
521
        $xmlSrc = $this->buildXml($context);
522
        $newXml = $this->docx->fixTables($xmlSrc);
523
        $this->updateXml($newXml);
524
525
        $this->renderHeaders($context);
526
        $this->renderFooters($context);
527
    }
528
529
    /**
530
     * Save document
531
     *
532
     * @param string $path - target path
533
     */
534
    public function save(string $path): void
535
    {
536
        //$this->preProcessing();
537
        $this->docx->save($path);
538
        //$this->postProcessing($path);
539
    }
540
541
    public function renderHeaders(array $context): void
542
    {
543
        $this->docx->setHeaders(array_map(function ($header) use ($context) {
544
            return $this->renderXml($header, $context);
545
        }, $this->docx->getHeaders()));
546
    }
547
548
    public function renderFooters(array $context): void
549
    {
550
        $this->docx->setFooters(array_map(function ($footer) use ($context) {
551
            return $this->renderXml($footer, $context);
552
        }, $this->docx->getFooters()));
553
    }
554
555
    /**
556
     * Clean everything after rendering
557
     */
558
    public function close(): void
559
    {
560
        $this->docx->close();
561
    }
562
}
563