Passed
Push — master ( 86d2a6...05cb1a )
by Bingo
03:06
created

PhpDocxTemplate::patchXml()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 85
Code Lines 56

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 57
CRAP Score 2

Importance

Changes 2
Bugs 0 Features 2
Metric Value
cc 2
eloc 56
c 2
b 0
f 2
nc 2
nop 1
dl 0
loc 85
ccs 57
cts 57
cp 1
crap 2
rs 8.9599

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 DOMElement;
7
use Twig\Loader\ArrayLoader;
8
use Twig\Environment;
9
10
/**
11
 * Class PhpDocxTemplate
12
 *
13
 * @package PhpDocxTemplate
14
 */
15
class PhpDocxTemplate
16
{
17
    private const NEWLINE_XML = '</w:t><w:br/><w:t xml:space="preserve">';
18
    private const NEWPARAGRAPH_XML = '</w:t></w:r></w:p><w:p><w:r><w:t xml:space="preserve">';
19
    private const TAB_XML = '</w:t></w:r><w:r><w:tab/></w:r><w:r><w:t xml:space="preserve">';
20
    private const PAGE_BREAK = '</w:t><w:br w:type="page"/><w:t xml:space="preserve">';
21
22
    private const HEADER_URI = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header";
23
    private const FOOTER_URI = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer";
24
25
    private $docx;
26
    private $crcToNewMedia;
27
    private $crcToNewEmbedded;
28
    private $picToReplace;
29
    private $picMap;
30
31
    /**
32
     * Construct an instance of PhpDocxTemplate
33
     *
34
     * @param string $path - path to the template
35
     */
36 9
    public function __construct(string $path)
37
    {
38 9
        $this->docx = new DocxDocument($path);
39 9
        $this->crcToNewMedia = [];
40 9
        $this->crcToNewEmbedded = [];
41 9
        $this->picToReplace = [];
42 9
        $this->picMap = [];
43 9
    }
44
45
    /**
46
     * Convert DOM to string
47
     *
48
     * @param DOMDocument $dom - DOM to be converted
49
     *
50
     * @return string
51
     */
52 7
    public function xmlToString(DOMDocument $dom): string
53
    {
54
        //return $el->ownerDocument->saveXML($el);
55 7
        return $dom->saveXML();
56
    }
57
58
    /**
59
     * Get document wrapper
60
     *
61
     * @return DocxDocument
62
     */
63 1
    public function getDocx(): DocxDocument
64
    {
65 1
        return $this->docx;
66
    }
67
68
    /**
69
     * Convert document.xml contents as string
70
     *
71
     * @return string
72
     */
73 6
    public function getXml(): string
74
    {
75 6
        return $this->xmlToString($this->docx->getDOMDocument());
76
    }
77
78
    /**
79
     * Write document.xml contents to file
80
     */
81
    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...
82
    {
83
        file_put_contents($path, $this->getXml());
84
    }
85
86
    /**
87
     * Update document.xml contents to file
88
     *
89
     * @param DOMDocument $xml - new contents
90
     */
91 4
    private function updateXml(DOMDocument $xml): void
92
    {
93 4
        $this->docx->updateDOMDocument($xml);
94 4
    }
95
96
    /**
97
     * Patch initial xml
98
     *
99
     * @param string $xml - initial xml
100
     */
101 6
    public function patchXml(string $xml): string
102
    {
103 6
        $xml = preg_replace('/(?<={)(<[^>]*>)+(?=[\{%])|(?<=[%\}])(<[^>]*>)+(?=\})/mu', '', $xml);
104 6
        $xml = preg_replace_callback(
105 6
            '/{%(?:(?!%}).)*|{{(?:(?!}}).)*/mu',
106 6
            array(get_class($this), 'stripTags'),
107 6
            $xml
108
        );
109 6
        $xml = preg_replace_callback(
110 6
            '/(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*colspan\s+([^%]*)\s*%}(.*?<\/w:tc>)/mu',
111 6
            array(get_class($this), 'colspan'),
112 6
            $xml
113
        );
114 6
        $xml = preg_replace_callback(
115 6
            '/(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*cellbg\s+([^%]*)\s*%}(.*?<\/w:tc>)/mu',
116 6
            array(get_class($this), 'cellbg'),
117 6
            $xml
118
        );
119
        // avoid {{r and {%r tags to strip MS xml tags too far
120
        // ensure space preservation when splitting
121 6
        $xml = preg_replace(
122 6
            '/<w:t>((?:(?!<w:t>).)*)({{r\s.*?}}|{%r\s.*?%})/mu',
123 6
            '<w:t xml:space="preserve">${1}${2}',
124 6
            $xml
125
        );
126 6
        $xml = preg_replace(
127 6
            '/({{r\s.*?}}|{%r\s.*?%})/mu',
128 6
            '</w:t></w:r><w:r><w:t xml:space="preserve">${1}</w:t></w:r><w:r><w:t xml:space="preserve">',
129 6
            $xml
130
        );
131
132
        // {%- will merge with previous paragraph text
133 6
        $xml = preg_replace(
134 6
            '/<\/w:t>(?:(?!<\/w:t>).)*?{%-/mu',
135 6
            '{%',
136 6
            $xml
137
        );
138
139
        // -%} will merge with next paragraph text
140 6
        $xml = preg_replace(
141 6
            '/-%}(?:(?!<w:t[ >]).)*?<w:t[^>]*?>/mu',
142 6
            '%}',
143 6
            $xml
144
        );      
145
146
        // replace into xml code the row/paragraph/run containing
147
        // {%y xxx %} or {{y xxx}} template tag
148
        // by {% xxx %} or {{ xx }} without any surronding <w:y> tags
149 6
        $tokens = ['tr', 'tc', 'p', 'r'];
150 6
        foreach ($tokens as $token) {
151 6
            $regex = '/';
152 6
            $regex .= sprintf(
153 6
                '<w:%ss[ >](?:(?!<w:%ss[ >]).)*({%%|{{)%ss ([^}%%]*(?:%%}|}})).*?<\/w:%ss>',
154 6
                $token,
155 6
                $token,
156 6
                $token,
157 6
                $token
158
            );
159 6
            $regex .= '/mu';
160 6
            $xml = preg_replace(
161 6
                $regex,
162 6
                '${1} ${2}',
163 6
                $xml
164
            );
165
        }
166
167 6
        $xml = preg_replace_callback(
168 6
            '/<w:tc[ >](?:(?!<w:tc[ >]).)*?{%\s*vm\s*%}.*?<\/w:tc[ >]/mu',
169 6
            array(get_class($this), 'vMergeTc'),
170 6
            $xml
171
        );
172
173 6
        $xml = preg_replace_callback(
174 6
            '/<w:tc[ >](?:(?!<w:tc[ >]).)*?{%\s*hm\s*%}.*?<\/w:tc[ >]/mu',
175 6
            array(get_class($this), 'hMergeTc'),
176 6
            $xml
177
        );
178
179 6
        $xml = preg_replace_callback(
180 6
            '/(?<=\{[\{%])(.*?)(?=[\}%]})/mu',
181 6
            array(get_class($this), 'cleanTags'),
182 6
            $xml
183
        );
184
185 6
        return $xml;
186
    }
187
188 5
    private function resolveListing(string $xml): string
189
    {
190 5
        return preg_replace_callback(
191 5
            '/<w:p\b(?:[^>]*)?>.*?<\/w:p>/mus',
192 5
            array(get_class($this), 'resolveParagraph'),
193 5
            $xml
194
        );
195
    }
196
197 5
    private function resolveParagraph(array $matches): string
198
    {
199 5
        preg_match("/<w:pPr>.*<\/w:pPr>/mus", $matches[0], $paragraphProperties);
200
201 5
        return preg_replace_callback(
202 5
            '/<w:r\b(?:[^>]*)?>.*?<\/w:r>/mus',
203
            function ($m) use ($paragraphProperties) {
204 5
                return $this->resolveRun($paragraphProperties[0] ?? '', $m);
205 5
            },
206 5
            $matches[0]
207
        );
208
    }
209
210 5
    private function resolveRun(string $paragraphProperties, array $matches): string
211
    {
212 5
        preg_match("/<w:rPr>.*<\/w:rPr>/mus", $matches[0], $runProperties);
213
214 5
        return preg_replace_callback(
215 5
            '/<w:t\b(?:[^>]*)?>.*?<\/w:t>/mus',
216
            function ($m) use ($paragraphProperties, $runProperties) {
217 5
                return $this->resolveText($paragraphProperties, $runProperties[0] ?? '', $m);
218 5
            },
219 5
            $matches[0]
220
        );
221
    }
222
223 5
    private function resolveText(string $paragraphProperties, string $runProperties, array $matches): string
224
    {
225 5
        $xml = str_replace(
226 5
            "\t",
227 5
            sprintf("</w:t></w:r>" .
228
                "<w:r>%s<w:tab/></w:r>" .
229 5
                "<w:r>%s<w:t xml:space=\"preserve\">", $runProperties, $runProperties),
230 5
            $matches[0]
231
        );
232
233 5
        $xml = str_replace(
234 5
            "\a",
235 5
            sprintf("</w:t></w:r></w:p>" .
236 5
                "<w:p>%s<w:r>%s<w:t xml:space=\"preserve\">", $paragraphProperties, $runProperties),
237 5
            $xml
238
        );
239
240 5
        $xml = str_replace("\n", sprintf("</w:t>" .
241
            "</w:r>" .
242
            "</w:p>" .
243
            "<w:p>%s" .
244
            "<w:r>%s" .
245 5
            "<w:t xml:space=\"preserve\">", $paragraphProperties, $runProperties), $xml);
246
247 5
        $xml = str_replace(
248 5
            "\f",
249 5
            sprintf("</w:t></w:r></w:p>" .
250
                "<w:p><w:r><w:br w:type=\"page\"/></w:r></w:p>" .
251 5
                "<w:p>%s<w:r>%s<w:t xml:space=\"preserve\">", $paragraphProperties, $runProperties),
252 5
            $xml
253
        );
254
255 5
        return $xml;
256
    }
257
258
    /**
259
     * Strip tags from matches
260
     *
261
     * @param array $matches - matches
262
     *
263
     * @return string
264
     */
265 6
    private function stripTags(array $matches): string
266
    {
267 6
        return preg_replace('/<\/w:t>.*?(<w:t>|<w:t [^>]*>)/mu', '', $matches[0]);
268
    }
269
270
    /**
271
     * Parse colspan
272
     *
273
     * @param array $matches - matches
274
     *
275
     * @return string
276
     */
277 1
    private function colspan(array $matches): string
278
    {
279 1
        $cellXml = $matches[1] . $matches[3];
280 1
        $cellXml = preg_replace('/<w:r[ >](?:(?!<w:r[ >]).)*<w:t><\/w:t>.*?<\/w:r>/mu', '', $cellXml);
281 1
        $cellXml = preg_replace('/<w:gridSpan[^\/]*\/>/mu', '', $cellXml, 1);
282 1
        return preg_replace(
283 1
            '/(<w:tcPr[^>]*>)/mu',
284 1
            sprintf('${1}<w:gridSpan w:val="{{%s}}"/>', $matches[2]),
285 1
            $cellXml
286
        );
287
    }
288
289
    /**
290
     * Parse cellbg
291
     *
292
     * @param array $matches - matches
293
     *
294
     * @return string
295
     */
296 1
    private function cellbg(array $matches): string
297
    {
298 1
        $cellXml = $matches[1] . $matches[3];
299 1
        $cellXml = preg_replace('/<w:r[ >](?:(?!<w:r[ >]).)*<w:t><\/w:t>.*?<\/w:r>/mu', '', $cellXml);
300 1
        $cellXml = preg_replace('/<w:shd[^\/]*\/>/mu', '', $cellXml, 1);
301 1
        return preg_replace(
302 1
            '/(<w:tcPr[^>]*>)/mu',
303 1
            sprintf('${1}<w:shd w:val="clear" w:color="auto" w:fill="{{%s}}"/>', $matches[2]),
304 1
            $cellXml
305
        );
306
    }
307
308
    /**
309
     * Parse vm
310
     *
311
     * @param array $matches - matches
312
     *
313
     * @return string
314
     */
315 1
    private function vMergeTc(array $matches): string
316
    {
317 1
        return preg_replace_callback(
318 1
            '/(<\/w:tcPr[ >].*?<w:t(?:.*?)>)(.*?)(?:{%\s*vm\s*%})(.*?)(<\/w:t>)/mu',
319 1
            array(get_class($this), 'vMerge'),
320 1
            $matches[0]
321
        );
322
    }
323
324
    /**
325
     * Continue parsing vm
326
     *
327
     * @param array $matches - matches
328
     *
329
     * @return string
330
     */
331 1
    private function vMerge(array $matches): string
332
    {
333
        return '<w:vMerge w:val="{% if loop.first %}restart{% else %}continue{% endif %}"/>' .
334 1
            $matches[1] .  // Everything between ``</w:tcPr>`` and ``<w:t>``.
335 1
            "{% if loop.first %}" .
336 1
            $matches[2] .  // Everything before ``{% vm %}``.
337 1
            $matches[3] .  // Everything after ``{% vm %}``.
338 1
            "{% endif %}" .
339 1
            $matches[4];  // ``</w:t>``.
340
    }
341
342
    /**
343
     * Parse hm
344
     *
345
     * @param array $matches - matches
346
     *
347
     * @return string
348
     */
349 1
    private function hMergeTc(array $matches): string
350
    {
351 1
        $xmlToPatch = $matches[0];
352 1
        if (strpos($xmlToPatch, 'w:gridSpan') !== false) {
353
            $xmlToPatch = preg_replace_callback(
354
                '/(w:gridSpan w:val=")(\d+)(")/mu',
355
                array(get_class($this), 'withGridspan'),
356
                $xmlToPatch
357
            );
358
            $xmlToPatch = preg_replace('/{%\s*hm\s*%}/mu', '', $xmlToPatch);
359
        } else {
360 1
            $xmlToPatch = preg_replace_callback(
361 1
                '/(<\/w:tcPr[ >].*?<w:t(?:.*?)>)(.*?)(?:{%\s*hm\s*%})(.*?)(<\/w:t>)/mu',
362 1
                array(get_class($this), 'withoutGridspan'),
363 1
                $xmlToPatch
364
            );
365
        }
366
367 1
        return "{% if loop.first %}" . $xmlToPatch . "{% endif %}";
368
    }
369
370
    private function withGridspan(array $matches): string
371
    {
372
        return $matches[1] . // ``w:gridSpan w:val="``.
373
            '{{ ' . $matches[2] . ' * loop.length }}' . // Content of ``w:val``, multiplied by loop length.
374
            $matches[3];  // Closing quotation mark.
375
    }
376
377 1
    private function withoutGridspan(array $matches): string
378
    {
379
        return '<w:gridSpan w:val="{{ loop.length }}"/>' .
380 1
            $matches[1] . // Everything between ``</w:tcPr>`` and ``<w:t>``.
381 1
            $matches[2] . // Everything before ``{% hm %}``.
382 1
            $matches[3] . // Everything after ``{% hm %}``.
383 1
            $matches[4]; // ``</w:t>``.
384
    }
385
386
    /**
387
     * Clean tags in matches
388
     *
389
     * @param array $matches - matches
390
     *
391
     * @return string
392
     */
393 6
    private function cleanTags(array $matches): string
394
    {
395 6
        return str_replace(
396 6
            ["&#8216;", '&lt;', '&gt;', '“', '”', "‘", "’"],
397 6
            ["'", '<', '>', '"', '"', "'", "'"],
398 6
            $matches[0]
399
        );
400
    }
401
402
    /**
403
     * Render xml
404
     *
405
     * @param string $srcXml - source xml
406
     * @param array $context - data to be rendered
407
     *
408
     * @return string
409
     */
410 5
    private function renderXml(string $srcXml, array $context): string
411
    {
412 5
        $srcXml = str_replace('<w:p>', "\n<w:p>", $srcXml);
413
414 5
        $template = new Environment(new ArrayLoader([
415 5
            'index' => $srcXml,
416
        ]));
417 5
        $dstXml = $template->render('index', $context);
418
419 5
        $dstXml = str_replace(
420 5
            ["\n<w:p>", "{_{", '}_}', '{_%', '%_}'],
421 5
            ['<w:p>', "{{", '}}', '{%', '%}'],
422 5
            $dstXml
423
        );
424
425
        // fix xml after rendering
426 5
        $dstXml = preg_replace(
427
            '/<w:p [^>]*>(?:<w:r [^>]*><w:t [^>]*>\s*<\/w:t><\/w:r>)?(?:<w:pPr><w:ind w:left="360"\/>' .
428 5
            '<\/w:pPr>)?<w:r [^>]*>(?:<w:t\/>|<w:t [^>]*><\/w:t>|<w:t [^>]*\/>|<w:t><\/w:t>)<\/w:r><\/w:p>/mu',
429 5
            '',
430 5
            $dstXml
431
        );
432
433 5
        $dstXml = $this->resolveListing($dstXml);
434
435 5
        return $dstXml;
436
    }
437
438
    /**
439
     * Build xml
440
     *
441
     * @param array $context - data to be rendered
442
     *
443
     * @return string
444
     */
445 5
    public function buildXml(array $context): string
446
    {
447 5
        $xml = $this->getXml();
448 5
        $xml = $this->patchXml($xml);
449 5
        $xml = $this->renderXml($xml, $context);
450 5
        return $xml;
451
    }
452
453
    /**
454
     * Render document
455
     *
456
     * @param array $context - data to be rendered
457
     */
458 4
    public function render(array $context): void
459
    {
460 4
        $xmlSrc = $this->buildXml($context);
461 4
        $newXml = $this->docx->fixTables($xmlSrc);
462 4
        $this->updateXml($newXml);
463 4
    }
464
465
    /**
466
     * Save document
467
     *
468
     * @param string $path - target path
469
     */
470 1
    public function save(string $path): void
471
    {
472
        //$this->preProcessing();
473 1
        $this->docx->save($path);
474
        //$this->postProcessing($path);
475 1
    }
476
477
    /**
478
     * Clean everything after rendering
479
     */
480 5
    public function close(): void
481
    {
482 5
        $this->docx->close();
483 5
    }
484
}
485