Passed
Push — master ( c052ad...705b31 )
by Bingo
02:51
created

PhpDocxTemplate::setImageValue()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

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

484
    public function setImageValue($search, $replace, /** @scrutinizer ignore-unused */ ?int $limit = null): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
485
    {
486 1
        $this->docx->setImageValue($search, $replace, $limit = null);
0 ignored issues
show
Unused Code introduced by
The call to PhpDocxTemplate\DocxDocument::setImageValue() has too many arguments starting with $limit = null. ( Ignorable by Annotation )

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

486
        $this->docx->/** @scrutinizer ignore-call */ 
487
                     setImageValue($search, $replace, $limit = null);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
487 1
    }
488
}
489