Passed
Push — master ( 1822aa...297bad )
by Bingo
04:41
created

DocxDocument::getRelationsName()   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 1
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 Exception;
8
use ZipArchive;
9
use RecursiveIteratorIterator;
10
use RecursiveDirectoryIterator;
11
12
/**
13
 * Class DocxDocument
14
 *
15
 * @package PhpDocxTemplate
16
 */
17
class DocxDocument
18
{
19
    private $path;
20
    private $tmpDir;
21
    private $document;
22
    private $zipClass;
23
    private $tempDocumentMainPart;
24
    private $tempDocumentRelations = [];
25
26
    /**
27
     * Construct an instance of Document
28
     *
29
     * @param string $path - path to the document
30
     *
31
     * @throws Exception
32
     */
33 10
    public function __construct(string $path)
34
    {
35 10
        if (file_exists($path)) {
36 10
            $this->path = $path;
37 10
            $this->tmpDir = sys_get_temp_dir() . "/" . uniqid("", true) . date("His");
38 10
            $this->zipClass = new ZipArchive();
39 10
            $this->extract();
40
        } else {
41
            throw new Exception("The template " . $path . " was not found!");
42
        }
43 10
    }
44
45
    /**
46
     * Extract (unzip) document contents
47
     */
48 10
    private function extract(): void
49
    {
50 10
        if (file_exists($this->tmpDir) && is_dir($this->tmpDir)) {
51
            $this->rrmdir($this->tmpDir);
52
        }
53
54 10
        mkdir($this->tmpDir);
55
56 10
        $this->zipClass->open($this->path);
57 10
        $this->zipClass->extractTo($this->tmpDir);
58 10
        $this->tempDocumentMainPart = $this->readPartWithRels($this->getMainPartName());
59 10
        $this->zipClass->close();
60
61 10
        $this->document = file_get_contents($this->tmpDir . "/word/document.xml");
62 10
    }
63
64
    /**
65
     * Get document main part
66
     *
67
     * @return string
68
     */
69 1
    public function getDocumentMainPart(): string
70
    {
71 1
        return $this->tempDocumentMainPart;
72
    }
73
74
    /**
75
     * Get the name of main part document (method from PhpOffice\PhpWord)
76
     *
77
     * @return string
78
     */
79 10
    private function getMainPartName(): string
80
    {
81 10
        $contentTypes = $this->zipClass->getFromName('[Content_Types].xml');
82
83
        $pattern = '~PartName="\/(word\/document.*?\.xml)" ' .
84
                   'ContentType="application\/vnd\.openxmlformats-officedocument' .
85 10
                   '\.wordprocessingml\.document\.main\+xml"~';
86
87 10
        $matches = [];
88 10
        preg_match($pattern, $contentTypes, $matches);
89
90 10
        return array_key_exists(1, $matches) ? $matches[1] : 'word/document.xml';
91
    }
92
93
    /**
94
     * Read document part (method from PhpOffice\PhpWord)
95
     *
96
     * @param string $fileName
97
     *
98
     * @return string
99
     */
100 10
    private function readPartWithRels(string $fileName): string
101
    {
102 10
        $relsFileName = $this->getRelationsName($fileName);
103 10
        $partRelations = $this->zipClass->getFromName($relsFileName);
104 10
        if ($partRelations !== false) {
105 10
            $this->tempDocumentRelations[$fileName] = $partRelations;
106
        }
107
108 10
        return $this->fixBrokenMacros($this->zipClass->getFromName($fileName));
109
    }
110
111
    /**
112
     * Get the name of the relations file for document part (method from PhpOffice\PhpWord)
113
     *
114
     * @param string $documentPartName
115
     *
116
     * @return string
117
     */
118 10
    private function getRelationsName(string $documentPartName): string
119
    {
120 10
        return 'word/_rels/' . pathinfo($documentPartName, PATHINFO_BASENAME) . '.rels';
0 ignored issues
show
Bug introduced by
Are you sure pathinfo($documentPartNa...late\PATHINFO_BASENAME) of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

120
        return 'word/_rels/' . /** @scrutinizer ignore-type */ pathinfo($documentPartName, PATHINFO_BASENAME) . '.rels';
Loading history...
121
    }
122
123
124
    /**
125
     * Finds parts of broken macros and sticks them together (method from PhpOffice\PhpWord)
126
     *
127
     * @param string $documentPart
128
     *
129
     * @return string
130
     */
131 10
    private function fixBrokenMacros(string $documentPart): string
132
    {
133 10
        return preg_replace_callback(
134 10
            '/\$(?:\{|[^{$]*\>\{)[^}$]*\}/U',
135
            function ($match) {
136
                return strip_tags($match[0]);
137 10
            },
138 10
            $documentPart
139
        );
140
    }
141
142
    /**
143
     * Get document.xml contents as DOMDocument
144
     *
145
     * @return DOMDocument
146
     */
147 7
    public function getDOMDocument(): DOMDocument
148
    {
149 7
        $dom = new DOMDocument();
150 7
        $dom->loadXML($this->document);
151 7
        return $dom;
152
    }
153
154
    /**
155
     * Update document.xml contents
156
     *
157
     * @param DOMDocument $dom - new contents
158
     */
159 5
    public function updateDOMDocument(DOMDocument $dom): void
160
    {
161 5
        $this->document = $dom->saveXml();
162 5
        file_put_contents($this->tmpDir . "/word/document.xml", $this->document);
163 5
    }
164
165
    /**
166
     * Fix table corruption
167
     *
168
     * @param string $xml - xml to fix
169
     *
170
     * @return DOMDocument
171
     */
172 5
    public function fixTables(string $xml): DOMDocument
173
    {
174 5
        $dom = new DOMDocument();
175 5
        $dom->loadXML($xml);
176 5
        $tables = $dom->getElementsByTagName('tbl');
177 5
        foreach ($tables as $table) {
178 2
            $columns = [];
179 2
            $columnsLen = 0;
180 2
            $toAdd = 0;
181 2
            $tableGrid = null;
182 2
            foreach ($table->childNodes as $el) {
183 2
                if ($el->nodeName == 'w:tblGrid') {
184 2
                    $tableGrid = $el;
185 2
                    foreach ($el->childNodes as $col) {
186 2
                        if ($col->nodeName == 'w:gridCol') {
187 2
                            $columns[] = $col;
188 2
                            $columnsLen += 1;
189
                        }
190
                    }
191 2
                } elseif ($el->nodeName == 'w:tr') {
192 2
                    $cellsLen = 0;
193 2
                    foreach ($el->childNodes as $col) {
194 2
                        if ($col->nodeName == 'w:tc') {
195 2
                            $cellsLen += 1;
196
                        }
197
                    }
198 2
                    if (($columnsLen + $toAdd) < $cellsLen) {
199 2
                        $toAdd = $cellsLen - $columnsLen;
200
                    }
201
                }
202
            }
203
204
            // add columns, if necessary
205 2
            if (!is_null($tableGrid) && $toAdd > 0) {
206
                $width = 0;
207
                foreach ($columns as $col) {
208
                    if (!is_null($col->getAttribute('w:w'))) {
209
                        $width += $col->getAttribute('w:w');
210
                    }
211
                }
212
                if ($width > 0) {
213
                    $oldAverage = $width / $columnsLen;
214
                    $newAverage = round($width / ($columnsLen + $toAdd));
215
                    foreach ($columns as $col) {
216
                        $col->setAttribute('w:w', round($col->getAttribute('w:w') * $newAverage / $oldAverage));
217
                    }
218
                    while ($toAdd > 0) {
219
                        $newCol = $dom->createElement("w:gridCol");
220
                        $newCol->setAttribute('w:w', $newAverage);
221
                        $tableGrid->appendChild($newCol);
222
                        $toAdd -= 1;
223
                    }
224
                }
225
            }
226
227
            // remove columns, if necessary
228 2
            $columns = [];
229 2
            foreach ($tableGrid->childNodes as $col) {
230 2
                if ($col->nodeName == 'w:gridCol') {
231 2
                    $columns[] = $col;
232
                }
233
            }
234 2
            $columnsLen = count($columns);
235
236 2
            $cellsLen = 0;
237 2
            $cellsLenMax = 0;
238 2
            foreach ($table->childNodes as $el) {
239 2
                if ($el->nodeName == 'w:tr') {
240 2
                    $cells = [];
241 2
                    foreach ($el->childNodes as $col) {
242 2
                        if ($col->nodeName == 'w:tc') {
243 2
                            $cells[] = $col;
244
                        }
245
                    }
246 2
                    $cellsLen = $this->getCellLen($cells);
247 2
                    $cellsLenMax = max($cellsLenMax, $cellsLen);
248
                }
249
            }
250 2
            $toRemove = $cellsLen - $cellsLenMax;
251 2
            if ($toRemove > 0) {
252
                $removedWidth = 0.0;
253
                for ($i = $columnsLen - 1; ($i + 1) >= $toRemove; $i -= 1) {
254
                    $extraCol = $columns[$i];
255
                    $removedWidth += $extraCol->getAttribute('w:w');
256
                    $tableGrid->removeChild($extraCol);
257
                }
258
259
                $columnsLeft = [];
260
                foreach ($tableGrid->childNodes as $col) {
261
                    if ($col->nodeName == 'w:gridCol') {
262
                        $columnsLeft[] = $col;
263
                    }
264
                }
265
                $extraSpace = 0;
266
                if (count($columnsLeft) > 0) {
267
                    $extraSpace = $removedWidth / count($columnsLeft);
268
                }
269
                foreach ($columnsLeft as $col) {
270 2
                    $col->setAttribute('w:w', round($col->getAttribute('w:w') + $extraSpace));
271
                }
272
            }
273
        }
274 5
        return $dom;
275
    }
276
277
    /**
278
     * Get total cells length
279
     *
280
     * @param array $cells - cells
281
     *
282
     * @return int
283
     */
284 2
    private function getCellLen(array $cells): int
285
    {
286 2
        $total = 0;
287 2
        foreach ($cells as $cell) {
288 2
            foreach ($cell->childNodes as $tc) {
289 2
                if ($tc->nodeName == 'w:tcPr') {
290 2
                    foreach ($tc->childNodes as $span) {
291 2
                        if ($span->nodeName == 'w:gridSpan') {
292 1
                            $total += intval($span->getAttribute('w:val'));
293 2
                            break;
294
                        }
295
                    }
296 2
                    break;
297
                }
298
            }
299
        }
300 2
        return $total + 1;
301
    }
302
303
    /**
304
     * Save the document to the target path
305
     *
306
     * @param string $path - target path
307
     */
308 1
    public function save(string $path): void
309
    {
310 1
        $rootPath = realpath($this->tmpDir);
311
312 1
        $zip = new ZipArchive();
313 1
        $zip->open($path, ZipArchive::CREATE | ZipArchive::OVERWRITE);
314
315 1
        $files = new RecursiveIteratorIterator(
316 1
            new RecursiveDirectoryIterator($rootPath),
317 1
            RecursiveIteratorIterator::LEAVES_ONLY
318
        );
319
320 1
        foreach ($files as $name => $file) {
321 1
            if (!$file->isDir()) {
322 1
                $filePath = $file->getRealPath();
323 1
                $relativePath = substr($filePath, strlen($rootPath) + 1);
324 1
                $zip->addFile($filePath, $relativePath);
325
            }
326
        }
327
328 1
        $zip->close();
329
330 1
        $this->rrmdir($this->tmpDir);
331 1
    }
332
333
    /**
334
     * Remove recursively directory
335
     *
336
     * @param string $dir - target directory
337
     */
338 6
    private function rrmdir(string $dir): void
339
    {
340 6
        $objects = scandir($dir);
341 6
        if (is_array($objects)) {
0 ignored issues
show
introduced by
The condition is_array($objects) is always true.
Loading history...
342 6
            foreach ($objects as $object) {
343 6
                if ($object != "." && $object != "..") {
344 6
                    if (filetype($dir . "/" . $object) == "dir") {
345 6
                        $this->rrmdir($dir . "/" . $object);
346
                    } else {
347 6
                        unlink($dir . "/" . $object);
348
                    }
349
                }
350
            }
351 6
            reset($objects);
352 6
            rmdir($dir);
353
        }
354 6
    }
355
356
    /**
357
     * Close document
358
     */
359 5
    public function close(): void
360
    {
361 5
        $this->rrmdir($this->tmpDir);
362 5
    }
363
}
364