Passed
Push — master ( 3beee5...a29bda )
by Tobias
04:45
created

DocxMustache   C

Complexity

Total Complexity 58

Size/Duplication

Total Lines 443
Duplicated Lines 5.42 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 37
Bugs 26 Features 1
Metric Value
wmc 58
c 37
b 26
f 1
lcom 1
cbo 7
dl 24
loc 443
rs 6.3005

20 Methods

Rating   Name   Duplication   Size   Complexity  
A exctractOpenXmlFile() 0 5 1
A __construct() 0 19 1
A Execute() 0 5 1
A StoragePath() 0 4 1
A Log() 0 8 2
A CleanUpTmpDirs() 0 13 3
A GetTmpDir() 0 8 1
A CopyTmplate() 0 6 1
A ReadOpenXmlFile() 0 19 4
A SaveOpenXmlFile() 0 15 2
A SaveOpenXmlObjectToFile() 0 9 2
A ReadTeamplate() 0 19 1
A tempChangeProblematicChars() 0 11 2
B AddContentType() 0 27 5
A FetchReplaceableImages() 0 47 3
A RemoveReplaceImages() 0 15 4
C InsertImages() 0 48 9
B ImageReplacer() 0 32 2
C AnalyseImgUrlString() 24 72 10
A SaveAsPdf() 0 19 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like DocxMustache often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DocxMustache, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace WrkLst\DocxMustache;
4
5
use Exception;
6
use Illuminate\Support\Facades\Log;
7
8
//Custom DOCX template class to change content based on mustache templating engine.
9
class DocxMustache
10
{
11
    public $items;
12
    public $word_doc;
13
    public $template_file_name;
14
    public $template_file;
15
    public $local_path;
16
    public $storageDisk;
17
    public $storagePathPrefix;
18
    public $zipper;
19
    public $imageManipulation;
20
    public $verbose;
21
22
    public function __construct($items, $local_template_file)
23
    {
24
        $this->items = $items;
25
        $this->template_file_name = basename($local_template_file);
26
        $this->template_file = $local_template_file;
27
        $this->word_doc = false;
28
        $this->zipper = new \Chumper\Zipper\Zipper();
29
30
        //name of disk for storage
31
        $this->storageDisk = 'local';
32
33
        //prefix within your storage path
34
        $this->storagePathPrefix = 'app/';
35
36
        //if you use img urls that support manipulation via parameter
37
        $this->imageManipulation = ''; //'&w=1800';
38
39
        $this->verbose = false;
40
    }
41
42
    public function Execute()
43
    {
44
        $this->CopyTmplate();
45
        $this->ReadTeamplate();
46
    }
47
48
    /**
49
     * @param string $file
50
     */
51
    public function StoragePath($file)
52
    {
53
        return storage_path($file);
54
    }
55
56
    /**
57
     * @param string $msg
58
     */
59
    protected function Log($msg)
60
    {
61
        //introduce logging method here to keep track of process
62
        // can be overwritten in extended class to log with custom preocess logger
63
        if ($this->verbose) {
64
            Log::error($msg);
65
        }
66
    }
67
68
    public function CleanUpTmpDirs()
69
    {
70
        $now = time();
71
        $isExpired = ($now - (60 * 240));
72
        $disk = \Storage::disk($this->storageDisk);
73
        $all_dirs = $disk->directories($this->storagePathPrefix.'DocxMustache');
74
        foreach ($all_dirs as $dir) {
75
            //delete dirs older than 20min
76
            if ($disk->lastModified($dir) < $isExpired) {
77
                $disk->deleteDirectory($dir);
78
            }
79
        }
80
    }
81
82
    public function GetTmpDir()
83
    {
84
        $this->CleanUpTmpDirs();
85
        $path = $this->storagePathPrefix.'DocxMustache/'.uniqid($this->template_file).'/';
86
        \File::makeDirectory($this->StoragePath($path), 0775, true);
87
88
        return $path;
89
    }
90
91
    public function CopyTmplate()
92
    {
93
        $this->Log('Get Copy of Template');
94
        $this->local_path = $this->GetTmpDir();
95
        \Storage::disk($this->storageDisk)->copy($this->storagePathPrefix.$this->template_file, $this->local_path.$this->template_file_name);
96
    }
97
98
    protected function exctractOpenXmlFile($file)
99
    {
100
        $this->zipper->make($this->StoragePath($this->local_path.$this->template_file_name))
101
            ->extractTo($this->StoragePath($this->local_path), [$file], \Chumper\Zipper\Zipper::WHITELIST);
102
    }
103
104
    protected function ReadOpenXmlFile($file, $type = 'file')
105
    {
106
        $this->exctractOpenXmlFile($file);
107
        $this->tempChangeProblematicChars($file);
108
109
        if ($type == 'file') {
110
            if ($file_contents = \Storage::disk($this->storageDisk)->get($this->local_path.$file)) {
111
                return $file_contents;
112
            } else {
113
                throw new Exception('Cannot not read file '.$file);
114
            }
115
        } else {
116
            if ($xml_object = simplexml_load_file($this->StoragePath($this->local_path.$file))) {
117
                return $xml_object;
118
            } else {
119
                throw new Exception('Cannot load XML Object from file '.$file);
120
            }
121
        }
122
    }
123
124
    protected function SaveOpenXmlFile($file, $folder, $content)
125
    {
126
        $content = str_replace('%%KAUFUND%%','&',$content);
127
128
        \Storage::disk($this->storageDisk)
129
            ->put($this->local_path.$file, $content);
130
        //add new content to word doc
131
        if ($folder) {
132
            $this->zipper->folder($folder)
133
                ->add($this->StoragePath($this->local_path.$file));
134
        } else {
135
            $this->zipper
136
                ->add($this->StoragePath($this->local_path.$file));
137
        }
138
    }
139
140
    protected function SaveOpenXmlObjectToFile($xmlObject, $file, $folder)
141
    {
142
        if ($xmlString = $xmlObject->asXML()) {
143
            $this->SaveOpenXmlFile($file, $folder, $xmlString);
144
            $this->tempChangeProblematicChars($file, true);
145
        } else {
146
            throw new Exception('Cannot generate xml for '.$file);
147
        }
148
    }
149
150
    public function ReadTeamplate()
151
    {
152
        $this->Log('Analyze Template');
153
        //get the main document out of the docx archive
154
        $this->word_doc = $this->ReadOpenXmlFile('word/document.xml', 'file');
155
156
        $this->Log('Merge Data into Template');
157
158
        $this->word_doc = MustacheRender::render($this->items, $this->word_doc);
159
160
        $this->word_doc = HtmlConversion::convert($this->word_doc);
161
162
        $this->ImageReplacer();
163
164
        $this->Log('Compact Template with Data');
165
166
        $this->SaveOpenXmlFile('word/document.xml', 'word', $this->word_doc);
167
        $this->zipper->close();
168
    }
169
170
    protected function tempChangeProblematicChars($file, $reverse = false)
171
    {
172
        $content = \Storage::disk($this->storageDisk)
173
            ->get($this->local_path.$file);
174
        if(!$reverse)
175
            $content = str_replace('&','%%KAUFUND%%',$content);
176
        else
177
            $content = str_replace('%%KAUFUND%%','&',$content);
178
        \Storage::disk($this->storageDisk)
179
            ->put($this->local_path.$file, $content);
180
    }
181
182
    protected function AddContentType($imageCt = 'jpeg')
183
    {
184
        $ct_file = $this->ReadOpenXmlFile('[Content_Types].xml', 'object');
185
186
        if (! ($ct_file instanceof \Traversable)) {
187
            throw new Exception('Cannot traverse through [Content_Types].xml.');
188
        }
189
190
        //check if content type for jpg has been set
191
        $i = 0;
192
        $ct_already_set = false;
193
        foreach ($ct_file as $ct) {
194
            if ((string) $ct_file->Default[$i]['Extension'] == $imageCt) {
195
                $ct_already_set = true;
196
            }
197
            $i++;
198
        }
199
200
        //if content type for jpg has not been set, add it to xml
201
        // and save xml to file and add it to the archive
202
        if (! $ct_already_set) {
203
            $sxe = $ct_file->addChild('Default');
204
            $sxe->addAttribute('Extension', $imageCt);
205
            $sxe->addAttribute('ContentType', 'image/'.$imageCt);
206
            $this->SaveOpenXmlObjectToFile($ct_file, '[Content_Types].xml', false);
207
        }
208
    }
209
210
    protected function FetchReplaceableImages(&$main_file, $ns)
211
    {
212
        //set up basic arrays to keep track of imgs
213
        $imgs = [];
214
        $imgs_replaced = []; // so they can later be removed from media and relation file.
215
        $newIdCounter = 1;
216
217
        //iterate through all drawing containers of the xml document
218
        foreach ($main_file->xpath('//w:drawing') as $k=>$drawing) {
219
            //figure out if there is a URL saved in the description field of the img
220
            $img_url = $this->AnalyseImgUrlString($drawing->children($ns['wp'])->xpath('wp:docPr')[0]->attributes()['descr']);
221
            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->xpath('wp:docPr')[0]->attributes()['descr'] = $img_url['rest'];
222
223
            //if there is a url, save this img as a img to be replaced
224
            if ($img_url['valid']) {
225
                $ueid = 'wrklstId'.$newIdCounter;
226
                $wasId = (string) $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])->graphic->graphicData->children($ns['pic'])->pic->blipFill->children($ns['a'])->blip->attributes($ns['r'])['embed'];
227
228
                //get dimensions
229
                $cx = (int) $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])->graphic->graphicData->children($ns['pic'])->pic->spPr->children($ns['a'])->xfrm->ext->attributes()['cx'];
230
                $cy = (int) $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])->graphic->graphicData->children($ns['pic'])->pic->spPr->children($ns['a'])->xfrm->ext->attributes()['cy'];
231
232
                //remember img as being replaced
233
                $imgs_replaced[$wasId] = $wasId;
234
235
                //set new img id
236
                $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])->graphic->graphicData->children($ns['pic'])->pic->blipFill->children($ns['a'])->blip->attributes($ns['r'])['embed'] = $ueid;
237
238
                $imgs[] = [
239
                    'cx'     => (int) $cx,
240
                    'cy'     => (int) $cy,
241
                    'wasId'  => $wasId,
242
                    'id'     => $ueid,
243
                    'url'    => $img_url['url'],
244
                    'path'    => $img_url['path'],
245
                    'mode'    => $img_url['mode'],
246
                ];
247
248
                $newIdCounter++;
249
            }
250
        }
251
252
        return [
253
            'imgs'          => $imgs,
254
            'imgs_replaced' => $imgs_replaced,
255
        ];
256
    }
257
258
    protected function RemoveReplaceImages($imgs_replaced, &$rels_file)
259
    {
260
        //TODO: check if the same img is used at a different position int he file as well, as otherwise broken images are produced.
261
        //iterate through replaced images and clean rels files from them
262
        foreach ($imgs_replaced as $img_replaced) {
263
            $i = 0;
264
            foreach ($rels_file as $rel) {
265
                if ((string) $rel->attributes()['Id'] == $img_replaced) {
266
                    $this->zipper->remove('word/'.(string) $rel->attributes()['Target']);
267
                    unset($rels_file->Relationship[$i]);
268
                }
269
                $i++;
270
            }
271
        }
272
    }
273
274
    protected function InsertImages($ns, &$imgs, &$rels_file, &$main_file)
275
    {
276
        $docimage = new DocImage();
277
        $allowed_imgs = $docimage->AllowedContentTypeImages();
278
        $image_i = 1;
279
        //iterate through replacable images
280
        foreach ($imgs as $k=>$img) {
281
            $this->Log('Merge Images into Template - '.round($image_i / count($imgs) * 100).'%');
282
            //get file type of img and test it against supported imgs
283
            if ($imgageData = $docimage->GetImageFromUrl($img['mode'] == 'url' ? $img['url'] : $img['path'], $img['mode'] == 'url' ? $this->imageManipulation : '')) {
284
                $imgs[$k]['img_file_src'] = str_replace('wrklstId', 'wrklst_image', $img['id']).$allowed_imgs[$imgageData['mime']];
285
                $imgs[$k]['img_file_dest'] = str_replace('wrklstId', 'wrklst_image', $img['id']).'.jpeg';
286
287
                $resampled_img = $docimage->ResampleImage($this, $imgs, $k, $imgageData['data']);
288
289
                $sxe = $rels_file->addChild('Relationship');
290
                $sxe->addAttribute('Id', $img['id']);
291
                $sxe->addAttribute('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image');
292
                $sxe->addAttribute('Target', 'media/'.$imgs[$k]['img_file_dest']);
293
294
                foreach ($main_file->xpath('//w:drawing') as $k=>$drawing) {
295
                    if ($img['id'] == $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])
296
                        ->graphic->graphicData->children($ns['pic'])->pic->blipFill->children($ns['a'])
297
                        ->blip->attributes($ns['r'])['embed']) {
298
                        $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])
299
                            ->graphic->graphicData->children($ns['pic'])->pic->spPr->children($ns['a'])
300
                            ->xfrm->ext->attributes()['cx'] = $resampled_img['width_emus'];
301
                        $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])
302
                            ->graphic->graphicData->children($ns['pic'])->pic->spPr->children($ns['a'])
303
                            ->xfrm->ext->attributes()['cy'] = $resampled_img['height_emus'];
304
                        //anchor images
305
                        if (isset($main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->anchor)) {
306
                            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->anchor->extent->attributes()['cx'] = $resampled_img['width_emus'];
307
                            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->anchor->extent->attributes()['cy'] = $resampled_img['height_emus'];
308
                        }
309
                        //inline images
310
                        elseif (isset($main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->inline)) {
311
                            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->inline->extent->attributes()['cx'] = $resampled_img['width_emus'];
312
                            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->inline->extent->attributes()['cy'] = $resampled_img['height_emus'];
313
                        }
314
315
                        break;
316
                    }
317
                }
318
            }
319
            $image_i++;
320
        }
321
    }
322
323
    protected function ImageReplacer()
324
    {
325
        $this->Log('Merge Images into Template');
326
327
        //load main doc xml
328
        $main_file = simplexml_load_string($this->word_doc);
329
330
        //get all namespaces of the document
331
        $ns = $main_file->getNamespaces(true);
332
333
        $replaceableImage = $this->FetchReplaceableImages($main_file, $ns);
334
        $imgs = $replaceableImage['imgs'];
335
        $imgs_replaced = $replaceableImage['imgs_replaced'];
0 ignored issues
show
Unused Code introduced by
$imgs_replaced is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
336
337
        $rels_file = $this->ReadOpenXmlFile('word/_rels/document.xml.rels', 'object');
338
339
        //do not remove until it is checked if the same img is used at a different position int he file as well, as otherwise broken images are produced.
340
        //$this->RemoveReplaceImages($imgs_replaced, $rels_file);
0 ignored issues
show
Unused Code Comprehensibility introduced by
80% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
341
342
        //add jpg content type if not set
343
        $this->AddContentType('jpeg');
344
345
        $this->InsertImages($ns, $imgs, $rels_file, $main_file);
346
347
        $this->SaveOpenXmlObjectToFile($rels_file, 'word/_rels/document.xml.rels', 'word/_rels');
348
349
        if ($main_file_xml = $main_file->asXML()) {
350
            $this->word_doc = $main_file_xml;
351
        } else {
352
            throw new Exception('Cannot generate xml for word/document.xml.');
353
        }
354
    }
355
356
    /**
357
     * @param string $string
358
     */
359
    protected function AnalyseImgUrlString($string)
360
    {
361
        $string = (string) $string;
362
        $start = '[IMG-REPLACE]';
363
        $end = '[/IMG-REPLACE]';
364
        $start_local = '[LOCAL_IMG_REPLACE]';
365
        $end_local = '[/LOCAL_IMG_REPLACE]';
366
        $valid = false;
367
        $url = '';
368
        $path = '';
369
370
        if ($string != str_replace($start, '', $string) && $string == str_replace($start.$end, '', $string)) {
371
            $string = ' '.$string;
372
            $ini = strpos($string, $start);
373 View Code Duplication
            if ($ini == 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
374
                $url = '';
375
                $rest = $string;
376
            } else {
377
                $ini += strlen($start);
378
                $len = ((strpos($string, $end, $ini)) - $ini);
379
                $url = substr($string, $ini, $len);
380
381
                $ini = strpos($string, $start);
382
                $len = strpos($string, $end, $ini + strlen($start)) + strlen($end);
383
                $rest = substr($string, 0, $ini).substr($string, $len);
384
            }
385
386
            $valid = true;
387
388
            //TODO: create a better url validity check
389
            if (! trim(str_replace(['http', 'https', ':', ' '], '', $url)) || $url == str_replace('http', '', $url)) {
390
                $valid = false;
391
            }
392
            $mode = 'url';
393
        } elseif ($string != str_replace($start_local, '', $string) && $string == str_replace($start_local.$end_local, '', $string)) {
394
            $string = ' '.$string;
395
            $ini = strpos($string, $start_local);
396 View Code Duplication
            if ($ini == 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
397
                $path = '';
398
                $rest = $string;
399
            } else {
400
                $ini += strlen($start_local);
401
                $len = ((strpos($string, $end_local, $ini)) - $ini);
402
                $path = str_replace('..', '', substr($string, $ini, $len));
403
404
                $ini = strpos($string, $start_local);
405
                $len = strpos($string, $end_local, $ini + strlen($start)) + strlen($end_local);
406
                $rest = substr($string, 0, $ini).substr($string, $len);
407
            }
408
409
            $valid = true;
410
411
            //check if path starts with storage path
412
            if (! starts_with($path, storage_path())) {
413
                $valid = false;
414
            }
415
            $mode = 'path';
416
        } else {
417
            $mode = 'nothing';
418
            $url = '';
419
            $path = '';
420
            $rest = str_replace([$start, $end, $start_local, $end_local], '', $string);
421
        }
422
423
        return [
424
            'mode' => $mode,
425
            'url'  => trim($url),
426
            'path' => trim($path),
427
            'rest' => trim($rest),
428
            'valid' => $valid,
429
        ];
430
    }
431
432
    public function SaveAsPdf()
433
    {
434
        $this->Log('Converting DOCX to PDF');
435
        //convert to pdf with libre office
436
        $command = 'soffice --headless --convert-to pdf '.$this->StoragePath($this->local_path.$this->template_file_name).' --outdir '.$this->StoragePath($this->local_path);
437
        $process = new \Symfony\Component\Process\Process($command);
438
        $process->start();
439
        while ($process->isRunning()) {
440
            //wait until process is ready
441
        }
442
        // executes after the command finishes
443
        if (! $process->isSuccessful()) {
444
            throw new \Symfony\Component\Process\Exception\ProcessFailedException($process);
445
        } else {
446
            $path_parts = pathinfo($this->StoragePath($this->local_path.$this->template_file_name));
447
448
            return $this->StoragePath($this->local_path.$path_parts['filename'].'pdf');
449
        }
450
    }
451
}
452