Passed
Push — master ( 68ba73...03763a )
by Bingo
06:35
created

PhpDocxTemplate   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 380
Duplicated Lines 0 %

Test Coverage

Coverage 92.21%

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 143
dl 0
loc 380
rs 10
c 1
b 0
f 1
ccs 142
cts 154
cp 0.9221
wmc 23

21 Methods

Rating   Name   Duplication   Size   Complexity  
A getDocx() 0 3 1
A hMergeTc() 0 19 2
A xmlToString() 0 4 1
A vMergeTc() 0 6 1
A updateXml() 0 3 1
A writeXml() 0 3 1
A vMerge() 0 9 1
A getXml() 0 3 1
A colspan() 0 9 1
A cleanTags() 0 6 1
A stripTags() 0 3 1
A __construct() 0 7 1
A withGridspan() 0 5 1
A withoutGridspan() 0 7 1
A patchXml() 0 71 2
A cellbg() 0 9 1
A close() 0 3 1
A renderXml() 0 22 1
A save() 0 4 1
A render() 0 5 1
A buildXml() 0 6 1
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 7
    public function __construct(string $path)
37
    {
38 7
        $this->docx = new DocxDocument($path);
39 7
        $this->crcToNewMedia = [];
40 7
        $this->crcToNewEmbedded = [];
41 7
        $this->picToReplace = [];
42 7
        $this->picMap = [];
43 7
    }
44
45
    /**
46
     * Convert DOM to string
47
     *
48
     * @param DOMDocument $dom - DOM to be converted
49
     *
50
     * @return string
51
     */
52 5
    public function xmlToString(DOMDocument $dom): string
53
    {
54
        //return $el->ownerDocument->saveXML($el);
55 5
        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 4
    public function getXml(): string
74
    {
75 4
        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 2
    private function updateXml(DOMDocument $xml): void
92
    {
93 2
        $this->docx->updateDOMDocument($xml);
94 2
    }
95
96
    /**
97
     * Patch initial xml
98
     *
99
     * @param string $xml - initial xml
100
     */
101 4
    public function patchXml(string $xml): string
102
    {
103 4
        $xml = preg_replace('/(?<={)(<[^>]*>)+(?=[\{%])|(?<=[%\}])(<[^>]*>)+(?=\})/mu', '', $xml);
104 4
        $xml = preg_replace_callback(
105 4
            '/{%(?:(?!%}).)*|{{(?:(?!}}).)*/mu',
106 4
            array(get_class($this), 'stripTags'),
107 4
            $xml
108
        );
109 4
        $xml = preg_replace_callback(
110 4
            '/(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*colspan\s+([^%]*)\s*%}(.*?<\/w:tc>)/mu',
111 4
            array(get_class($this), 'colspan'),
112 4
            $xml
113
        );
114 4
        $xml = preg_replace_callback(
115 4
            '/(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*cellbg\s+([^%]*)\s*%}(.*?<\/w:tc>)/mu',
116 4
            array(get_class($this), 'cellbg'),
117 4
            $xml
118
        );
119
        // avoid {{r and {%r tags to strip MS xml tags too far
120
        // ensure space preservation when splitting
121 4
        $xml = preg_replace(
122 4
            '/<w:t>((?:(?!<w:t>).)*)({{r\s.*?}}|{%r\s.*?%})/mu',
123 4
            '<w:t xml:space="preserve">${1}${2}',
124 4
            $xml
125
        );
126 4
        $xml = preg_replace(
127 4
            '/({{r\s.*?}}|{%r\s.*?%})/mu',
128 4
            '</w:t></w:r><w:r><w:t xml:space="preserve">${1}</w:t></w:r><w:r><w:t xml:space="preserve">',
129 4
            $xml
130
        );
131
        
132
        // replace into xml code the row/paragraph/run containing
133
        // {%y xxx %} or {{y xxx}} template tag
134
        // by {% xxx %} or {{ xx }} without any surronding <w:y> tags
135 4
        $tokens = ['tr', 'tc', 'p', 'r'];
136 4
        foreach ($tokens as $token) {
137 4
            $regex = '/';
138 4
            $regex .= sprintf(
139 4
                '<w:%ss[ >](?:(?!<w:%ss[ >]).)*({%%|{{)%ss ([^}%%]*(?:%%}|}})).*?<\/w:%ss>',
140 4
                $token,
141 4
                $token,
142 4
                $token,
143 4
                $token
144
            );
145 4
            $regex .= '/mu';
146 4
            $xml = preg_replace(
147 4
                $regex,
148 4
                '${1} ${2}',
149 4
                $xml
150
            );
151
        }
152
153 4
        $xml = preg_replace_callback(
154 4
            '/<w:tc[ >](?:(?!<w:tc[ >]).)*?{%\s*vm\s*%}.*?<\/w:tc[ >]/mu',
155 4
            array(get_class($this), 'vMergeTc'),
156 4
            $xml
157
        );
158
159 4
        $xml = preg_replace_callback(
160 4
            '/<w:tc[ >](?:(?!<w:tc[ >]).)*?{%\s*hm\s*%}.*?<\/w:tc[ >]/mu',
161 4
            array(get_class($this), 'hMergeTc'),
162 4
            $xml
163
        );
164
165 4
        $xml = preg_replace_callback(
166 4
            '/(?<=\{[\{%])(.*?)(?=[\}%]})/mu',
167 4
            array(get_class($this), 'cleanTags'),
168 4
            $xml
169
        );
170
171 4
        return $xml;
172
    }
173
174
    /**
175
     * Strip tags from matches
176
     *
177
     * @param array $matches - matches
178
     *
179
     * @return string
180
     */
181 4
    private function stripTags(array $matches): string
182
    {
183 4
        return preg_replace('/<\/w:t>.*?(<w:t>|<w:t [^>]*>)/mu', '', $matches[0]);
184
    }
185
186
    /**
187
     * Parse colspan
188
     *
189
     * @param array $matches - matches
190
     *
191
     * @return string
192
     */
193 1
    private function colspan(array $matches): string
194
    {
195 1
        $cellXml = $matches[1] . $matches[3];
196 1
        $cellXml = preg_replace('/<w:r[ >](?:(?!<w:r[ >]).)*<w:t><\/w:t>.*?<\/w:r>/mu', '', $cellXml);
197 1
        $cellXml = preg_replace('/<w:gridSpan[^\/]*\/>/mu', '', $cellXml, 1);
198 1
        return preg_replace(
199 1
            '/(<w:tcPr[^>]*>)/mu',
200 1
            sprintf('${1}<w:gridSpan w:val="{{%s}}"/>', $matches[2]),
201 1
            $cellXml
202
        );
203
    }
204
205
    /**
206
     * Parse cellbg
207
     *
208
     * @param array $matches - matches
209
     *
210
     * @return string
211
     */
212 1
    private function cellbg(array $matches): string
213
    {
214 1
        $cellXml = $matches[1] . $matches[3];
215 1
        $cellXml = preg_replace('/<w:r[ >](?:(?!<w:r[ >]).)*<w:t><\/w:t>.*?<\/w:r>/mu', '', $cellXml);
216 1
        $cellXml = preg_replace('/<w:shd[^\/]*\/>/mu', '', $cellXml, 1);
217 1
        return preg_replace(
218 1
            '/(<w:tcPr[^>]*>)/mu',
219 1
            sprintf('${1}<w:shd w:val="clear" w:color="auto" w:fill="{{%s}}"/>', $matches[2]),
220 1
            $cellXml
221
        );
222
    }
223
224
    /**
225
     * Parse vm
226
     *
227
     * @param array $matches - matches
228
     *
229
     * @return string
230
     */
231 1
    private function vMergeTc(array $matches): string
232
    {
233 1
        return preg_replace_callback(
234 1
            '/(<\/w:tcPr[ >].*?<w:t(?:.*?)>)(.*?)(?:{%\s*vm\s*%})(.*?)(<\/w:t>)/mu',
235 1
            array(get_class($this), 'vMerge'),
236 1
            $matches[0]
237
        );
238
    }
239
240
    /**
241
     * Continue parsing vm
242
     *
243
     * @param array $matches - matches
244
     *
245
     * @return string
246
     */
247 1
    private function vMerge(array $matches): string
248
    {
249
        return '<w:vMerge w:val="{% if loop.first %}restart{% else %}continue{% endif %}"/>' .
250 1
            $matches[1] .  // Everything between ``</w:tcPr>`` and ``<w:t>``.
251 1
            "{% if loop.first %}" .
252 1
            $matches[2] .  // Everything before ``{% vm %}``.
253 1
            $matches[3] .  // Everything after ``{% vm %}``.
254 1
            "{% endif %}" .
255 1
            $matches[4];  // ``</w:t>``.
256
    }
257
258
    /**
259
     * Parse hm
260
     *
261
     * @param array $matches - matches
262
     *
263
     * @return string
264
     */
265 1
    private function hMergeTc(array $matches): string
266
    {
267 1
        $xmlToPatch = $matches[0];
268 1
        if (strpos($xmlToPatch, 'w:gridSpan') !== false) {
269
            $xmlToPatch = preg_replace_callback(
270
                '/(w:gridSpan w:val=")(\d+)(")/mu',
271
                array(get_class($this), 'withGridspan'),
272
                $xmlToPatch
273
            );
274
            $xmlToPatch = preg_replace('/{%\s*hm\s*%}/mu', '', $xmlToPatch);
275
        } else {
276 1
            $xmlToPatch = preg_replace_callback(
277 1
                '/(<\/w:tcPr[ >].*?<w:t(?:.*?)>)(.*?)(?:{%\s*hm\s*%})(.*?)(<\/w:t>)/mu',
278 1
                array(get_class($this), 'withoutGridspan'),
279 1
                $xmlToPatch
280
            );
281
        }
282
283 1
        return "{% if loop.first %}" . $xmlToPatch . "{% endif %}";
284
    }
285
286
    private function withGridspan(array $matches): string
287
    {
288
        return $matches[1] . // ``w:gridSpan w:val="``.
289
            '{{ ' . $matches[2] . ' * loop.length }}' . // Content of ``w:val``, multiplied by loop length.
290
            $matches[3];  // Closing quotation mark.
291
    }
292
293 1
    private function withoutGridspan(array $matches): string
294
    {
295
        return '<w:gridSpan w:val="{{ loop.length }}"/>' .
296 1
            $matches[1] . // Everything between ``</w:tcPr>`` and ``<w:t>``.
297 1
            $matches[2] . // Everything before ``{% hm %}``.
298 1
            $matches[3] . // Everything after ``{% hm %}``.
299 1
            $matches[4]; // ``</w:t>``.
300
    }
301
302
    /**
303
     * Clean tags in matches
304
     *
305
     * @param array $matches - matches
306
     *
307
     * @return string
308
     */
309 4
    private function cleanTags(array $matches): string
310
    {
311 4
        return str_replace(
312 4
            ["&#8216;", '&lt;', '&gt;', '“', '”', "‘", "’"],
313 4
            ["'", '<', '>', '"', '"', "'", "'"],
314 4
            $matches[0]
315
        );
316
    }
317
318
    /**
319
     * Render xml
320
     *
321
     * @param string $srcXml - source xml
322
     * @param array $context - data to be rendered
323
     *
324
     * @return string
325
     */
326 3
    private function renderXml(string $srcXml, array $context): string
327
    {
328 3
        $srcXml = str_replace('<w:p>', "\n<w:p>", $srcXml);
329
        
330 3
        $template = new Environment(new ArrayLoader([
331 3
            'index' => $srcXml,
332
        ]));
333 3
        $dstXml = $template->render('index', $context);
334
335 3
        $dstXml = str_replace(
336 3
            ["\n<w:p>", "{_{", '}_}', '{_%', '%_}'],
337 3
            ['<w:p>', "{{", '}}', '{%', '%}'],
338 3
            $dstXml
339
        );
340
341
        // fix xml after rendering
342 3
        $dstXml = preg_replace(
343 3
            '/<w:p [^>]*>(?:(<w:pPr><w:ind [^>]*\/><\/w:pPr>)?)<w:r [^>]*>(?:<w:t\/>|<w:t [^>]*\/>)<\/w:r><\/w:p>/mu',
344 3
            '',
345 3
            $dstXml
346
        );
347 3
        return $dstXml;
348
    }
349
350
    /**
351
     * Build xml
352
     *
353
     * @param array $context - data to be rendered
354
     *
355
     * @return string
356
     */
357 3
    public function buildXml(array $context): string
358
    {
359 3
        $xml = $this->getXml();
360 3
        $xml = $this->patchXml($xml);
361 3
        $xml = $this->renderXml($xml, $context);
362 3
        return $xml;
363
    }
364
365
    /**
366
     * Render document
367
     *
368
     * @param array $context - data to be rendered
369
     */
370 2
    public function render(array $context): void
371
    {
372 2
        $xmlSrc = $this->buildXml($context);
373 2
        $newXml = $this->docx->fixTables($xmlSrc);
374 2
        $this->updateXml($newXml);
375 2
    }
376
377
    /**
378
     * Save document
379
     *
380
     * @param string $path - target path
381
     */
382 1
    public function save(string $path): void
383
    {
384
        //$this->preProcessing();
385 1
        $this->docx->save($path);
386
        //$this->postProcessing($path);
387 1
    }
388
389
    /**
390
     * Clean everything after rendering
391
     */
392 5
    public function close(): void
393
    {
394 5
        $this->docx->close();
395 5
    }
396
}
397