Test Failed
Push — master ( 29e5e7...76f94d )
by Bingo
09:54 queued 06:47
created

PhpDocxTemplate::resolveListing()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 4
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 6
rs 10
ccs 4
cts 4
cp 1
crap 1
1
<?php
2
3
namespace PhpDocxTemplate;
4
5
use DOMDocument;
6
use Twig\Loader\ArrayLoader;
7
use Twig\Environment;
8
use PhpDocxTemplate\Twig\Impl\{
9
    ImageExtension,
10
    ImageRenderer,
11
    QrCodeExtension,
12
    QrCodeRenderer
13
};
14
15
/**
16
 * Class PhpDocxTemplate
17
 *
18
 * @package PhpDocxTemplate
19
 */
20
class PhpDocxTemplate
21
{
22
    private const NEWLINE_XML = '</w:t><w:br/><w:t xml:space="preserve">';
23
    private const NEWPARAGRAPH_XML = '</w:t></w:r></w:p><w:p><w:r><w:t xml:space="preserve">';
24
    private const TAB_XML = '</w:t></w:r><w:r><w:tab/></w:r><w:r><w:t xml:space="preserve">';
25
    private const PAGE_BREAK = '</w:t><w:br w:type="page"/><w:t xml:space="preserve">';
26
27
    private const HEADER_URI = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header";
28
    private const FOOTER_URI = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer";
29
30
    private $docx;
31
    private $crcToNewMedia;
32
    private $crcToNewEmbedded;
33
    private $picToReplace;
34
    private $picMap;
35
36
    /**
37
     * Construct an instance of PhpDocxTemplate
38
     *
39
     * @param string $path - path to the template
40
     */
41
    public function __construct(string $path)
42 12
    {
43
        $this->docx = new DocxDocument($path);
44 12
        $this->crcToNewMedia = [];
45 12
        $this->crcToNewEmbedded = [];
46 12
        $this->picToReplace = [];
47 12
        $this->picMap = [];
48 12
    }
49 12
50
    /**
51
     * Convert DOM to string
52
     *
53
     * @param DOMDocument $dom - DOM to be converted
54
     *
55
     * @return string
56
     */
57
    public function xmlToString(DOMDocument $dom): string
58 10
    {
59
        //return $el->ownerDocument->saveXML($el);
60
        return $dom->saveXML();
61 10
    }
62
63
    /**
64
     * Get document wrapper
65
     *
66
     * @return DocxDocument
67
     */
68
    public function getDocx(): DocxDocument
69 3
    {
70
        return $this->docx;
71 3
    }
72
73
    /**
74
     * Convert document.xml contents as string
75
     *
76
     * @return string
77
     */
78
    public function getXml(): string
79 9
    {
80
        return $this->xmlToString($this->docx->getDOMDocument());
81 9
    }
82
83
    /**
84
     * Write document.xml contents to file
85
     */
86
    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...
87
    {
88
        file_put_contents($path, $this->getXml());
89
    }
90
91
    /**
92
     * Update document.xml contents to file
93
     *
94
     * @param DOMDocument $xml - new contents
95
     */
96
    private function updateXml(DOMDocument $xml): void
97 7
    {
98
        $this->docx->updateDOMDocument($xml);
99 7
    }
100 7
101
    public function patchXml(string $xml): string
102
    {
103
        $matches = [];
104
105
        preg_match('/^.*?(<w:body>)/s', $xml, $matches);
106
107 9
        $beforeXml = $matches[0];
108
109 9
        preg_match('/(<\/w:body>).*?$/s', $xml, $matches);
110 9
111 9
        $afterXml = $matches[0];
112 9
113
        $dom = new DOMDocument();
114
        $dom->loadXML($xml);
115 9
116 9
        $elBody = $dom->getElementsByTagName('body')->item(0);
117 9
118
        $chunkXml = '';
119
120 9
        for ($itemIdx = 0; $itemIdx < $elBody->childNodes->count(); $itemIdx++) {
121 9
            $el = $elBody->childNodes->item($itemIdx);
122 9
123
            $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

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