Passed
Branch 4.9 (cb955a)
by Mikhail
01:30
created

LanguageGenerator   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 463
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 42
eloc 139
dl 0
loc 463
c 0
b 0
f 0
rs 9.0399

26 Methods

Rating   Name   Duplication   Size   Complexity  
A findByKey() 0 3 1
A __construct() 0 3 1
A setContent() 0 5 1
A create() 0 36 2
A findByKeyInRecycleBin() 0 3 1
A replaceEol() 0 3 1
A getKeyGenerator() 0 3 1
A appendRecycleBin() 0 8 2
A addLangvar() 0 9 2
A removeByKey() 0 20 2
A getRecycleBin() 0 3 1
A purify() 0 4 1
A appendContent() 0 8 2
A replaceLangvar() 0 30 4
A setRecycleBin() 0 5 1
A clearWhitespaces() 0 3 1
A getPath() 0 23 2
A setMediator() 0 3 1
A getTemplateFilename() 0 3 1
A getTranslationKey() 0 3 1
A removeById() 0 20 2
A toString() 0 3 1
A findByKeyIn() 0 20 4
A setEndingNewLine() 0 11 4
A checkLanguageSupport() 0 3 1
A checkForEdited() 0 6 1

How to fix   Complexity   

Complex Class

Complex classes like LanguageGenerator 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.

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 LanguageGenerator, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace generators\Language;
4
5
use Config;
6
use generators\Language\exceptions\DuplicateException;
7
use mediators\AbstractMediator;
8
9
/**
10
  * @property string $pathTemplate
11
  * @property string $templatePath
12
  * @property string $recycleBin - buffer to which be removed all langvars from actual content
13
  * @property string $content
14
  * @property Config $config
15
  * @property AbstractMediator $mediator
16
  * @property array $codes
17
  * @property string $eol - end of line char
18
  * @todo add all $codes supported by cs-cart
19
  */
20
final class LanguageGenerator extends \generators\AbstractGenerator
21
{
22
    // readonly
23
    private $pathTemplate = 'var/langs/${lang}/addons/${addon}.po';
24
    private $templatePath = '';
25
    private $recycleBin = '';
26
    private $content = '';
27
    private $config;
28
    private $mediator;
29
    private static $codes = [
30
        'en' => ['pack-name' => 'English', 'country-code' => 'US'],
31
        'ru' => ['pack-name' => 'Russian', 'country-code' => 'RU']
32
    ];
33
    private static $eol = "\n";
34
35
    function __construct(Config $config)
36
    {
37
        $this->config = $config;
38
    }
39
40
    /**
41
     * @inheritdoc
42
     */
43
    public function getTemplateFilename(): string
44
    {
45
        return $this->templatePath;
46
    }
47
48
    public function setMediator(AbstractMediator $mediator): void
49
    {
50
        $this->mediator = $mediator;
51
    }
52
53
    public function getPath(): string
54
    {
55
        $addon_id = $this->config->getOr('addon', 'addon.id');
56
57
        if (!$addon_id) {
58
            throw new \InvalidArgumentException('Addon id (name) not specified');
59
        }
60
61
        $path = $this->config->get('path')
62
            . $this->config->get('filesystem.output_path_relative')
63
            . str_replace(
64
                [
65
                    '${lang}',
66
                    '${addon}'
67
                ],
68
                [
69
                    $this->config->getOr('lang', 'addon.default_language'),
70
                    $addon_id
71
                ],
72
                $this->pathTemplate
73
            );
74
75
        return get_absolute_path($path);
76
    }
77
78
    /**
79
     * Check language for support
80
     * @param string $language
81
     *
82
     * @return bool
83
     */
84
    public static function checkLanguageSupport(string $language): bool
85
    {
86
        return array_key_exists($language, self::$codes);
87
    }
88
89
    /**
90
     * Replaces different style eol by one
91
     * 
92
     * @param string $content - content wich will be changed
93
     * 
94
     * @return string - content with one-style eol
95
     */
96
    public static function replaceEol(string $content): string
97
    {
98
        return preg_replace('~\r\n?~', self::$eol, $content);
99
    }
100
101
    /**
102
     * @inheritdoc
103
     *
104
     * @return LanguageGenerator
105
     */
106
    public function setContent(string $content)
107
    {
108
        $this->content = self::replaceEol($content);
109
110
        return $this;
111
    }
112
113
    /**
114
     * get recycleBin
115
     * 
116
     * @return string
117
     */
118
    public function getRecycleBin(): string
119
    {
120
        return self::purify($this->recycleBin);
121
    }
122
123
    /**
124
     * Set content to recycleBin
125
     *
126
     * @return LanguageGenerator
127
     */
128
    public function setRecycleBin(string $content)
129
    {
130
        $this->recycleBin = self::replaceEol($content);
131
132
        return $this;
133
    }
134
135
    /**
136
     * Append content to current generator content
137
     * @param string $content - content to append
138
     *
139
     * @return LanguageGenerator
140
     */
141
    public function appendContent(string $content)
142
    {
143
        $this->setContent(
144
            (empty($this->content) ? '' : self::setEndingNewLine($this->content) . PHP_EOL)
145
            . $content
146
        );
147
148
        return $this;
149
    }
150
151
    /**
152
     * Append content to recycleBin
153
     *
154
     * @return LanguageGenerator
155
     */
156
    public function appendRecycleBin(string $content)
157
    {
158
        $this->setRecycleBin(
159
            (empty($this->recycleBin) ? '' : self::setEndingNewLine($this->recycleBin) . PHP_EOL)
160
            . $content
161
        );
162
163
        return $this;
164
    }
165
166
    /**
167
     * create po heading structure
168
     * @throws \InvalidArgumentException if nor language param and addon default_language are specified
169
     *
170
     * @return LanguageGenerator
171
     */
172
    public function create()
173
    {
174
        $po_heading_template = <<<'EOD'
175
msgid ""
176
msgstr ""
177
"Language: ${code}\n"
178
"Content-Type: text/plain; charset=UTF-8\n"
179
"Pack-Name: ${pack-name}\n"
180
"Lang-Code: ${code}\n"
181
"Country-Code: ${country-code}\n"
182
EOD;
183
184
        $language_code          = $this->config->getOr('language', 'addon.default_language');
185
186
        if (!$language_code) {
187
            throw new \InvalidArgumentException('Nor language param and addon default_language are specified');
188
        }
189
190
        $language_information   = self::$codes[$language_code];
191
        $po_heading = str_replace(
192
            [
193
                '${code}',
194
                '${pack-name}',
195
                '${country-code}'
196
            ],
197
            [
198
                $language_code,
199
                $language_information['pack-name'],
200
                $language_information['country-code']
201
            ],
202
            $po_heading_template
203
        );
204
205
        $this->content = $po_heading;
206
207
        return $this;
208
    }
209
210
    /**
211
     * Constructs langvar full key code
212
     * @param string $type - example: Languages
213
     * @param string $arguments - parts of path for generating msgxtxt key
214
     * @todo validate subpath for containing only [a-z_\.]/i - throw exception if not - write tests
215
     *
216
     * @return string - return langvar string like Languages::email_marketing.subscription_confirmed
217
     */
218
    public static function getTranslationKey(string $type, ...$arguments): string
219
    {
220
        return self::getKeyGenerator($type)::generate(...$arguments);
221
    }
222
223
    private static function getKeyGenerator(string $type)
224
    {
225
        return '\\generators\\Language\\keyGenerators\\' . $type;
226
    }
227
228
    /**
229
     * Get langvar array from content
230
     * @param string $full_key - key for search like Languages::email_marketing.subscription_confirmed
231
     * @throws \InvalidArgumentException if $full_key is empty
232
     *
233
     * @return bool|array - [
234
     *  'msgctxt' => "Languages::payments.epdq.tbl_bgcolor",
235
     *  'msgid' =>  "Table background color",
236
     *  'msgstr' => "Table background color"
237
     * ]
238
     */
239
    public function findByKey(string $full_key)
240
    {
241
        return self::findByKeyIn($full_key, $this->content);
242
    }
243
244
    /**
245
     * Get langvar array from recycleBin
246
     * @param string $full_key - key for search like Languages::email_marketing.subscription_confirmed
247
     * @throws \InvalidArgumentException if $full_key is empty
248
     *
249
     * @return bool|array - [
250
     *  'msgctxt' => "Languages::payments.epdq.tbl_bgcolor",
251
     *  'msgid' =>  "Table background color",
252
     *  'msgstr' => "Table background color"
253
     * ]
254
     */
255
    public function findByKeyInRecycleBin(string $full_key)
256
    {
257
        return self::findByKeyIn($full_key, $this->recycleBin);
258
    }
259
260
    /**
261
     * Get langvar array from specified content
262
     * @param string $full_key - key for search like Languages::email_marketing.subscription_confirmed
263
     * @throws \InvalidArgumentException if $full_key is empty
264
     *
265
     * @return bool|array - [
266
     *  'msgctxt' => "Languages::payments.epdq.tbl_bgcolor",
267
     *  'msgid' =>  "Table background color",
268
     *  'msgstr' => "Table background color"
269
     * ]
270
     */
271
    public static function findByKeyIn(string $full_key, string $content)
272
    {
273
        if (!$full_key) {
274
            throw new \InvalidArgumentException('full_key cannot be empty');
275
        }
276
277
        $found_count = preg_match_all(
278
            '/(msgctxt\s+"(' . $full_key . ')")' . self::$eol . '+(msgid\s+"(.*)")' . self::$eol . '+(msgstr\s+"(.*)")/umi',
279
            $content,
280
            $matches
281
        );
282
283
        if ($found_count === 0 || $found_count === false) {
284
            return false;
285
        }
286
287
        return [
288
            'msgctxt' => $matches[2][0],
289
            'msgid' => $matches[4][0],
290
            'msgstr' => $matches[6][0]
291
        ];
292
    }
293
294
    /**
295
     * Fully remove langvar, that matches msgctxt (msgctxt)
296
     * @param string $msgctxt - msgctxt
297
     * @throws \InvalidArgumentException if $msgctxt is empty
298
     *
299
     * @return LanguageGenerator
300
     */
301
    public function removeByKey(string $msgctxt)
302
    {
303
        if (!$msgctxt) {
304
            throw new \InvalidArgumentException('msgctxt cannot be empty');
305
        }
306
307
        $recycle_bin = '';
308
        $new_content = preg_replace_callback(
309
            '/(msgctxt\s+"' . $msgctxt . '"' . self::$eol . '+msgid\s+".*"' . self::$eol . '+msgstr\s+".*")(' . self::$eol . '*)/umi',
310
            function($matches) use (&$recycle_bin) {
311
                $recycle_bin .= $matches[1] . $matches[2];
312
                return '';
313
            },
314
            $this->content
315
        );
316
317
        $this->setContent($new_content);
318
        $this->appendRecycleBin($recycle_bin);
319
320
        return $this;
321
    }
322
323
    /**
324
     * Fully removes all langvars with a specified id
325
     * 
326
     * @param string $id
327
     * 
328
     * @return LanguageGenerator
329
     */
330
    public function removeById(string $id)
331
    {
332
        if (!$id) {
333
            throw new \InvalidArgumentException('id cannot be empty');
334
        }
335
336
        $recycle_bin = '';
337
        $new_content = preg_replace_callback(
338
            '/(msgctxt\s+"[\w:._]+' . $this->config->get('addon.id') . '::' . $id . '[\w:._]*"' . self::$eol . '+msgid\s+".*"' . self::$eol . '+msgstr\s+".*")(' . self::$eol . '*)/umi',
339
            function($matches) use (&$recycle_bin) {
340
                $recycle_bin .= $matches[1] . $matches[2];
341
                return '';
342
            },
343
            $this->content
344
        );
345
346
        $this->setContent($new_content);
347
        $this->appendRecycleBin($recycle_bin);
348
349
        return $this;
350
    }
351
352
    /**
353
     * Check for ending line and add it if not found
354
     * @param string $content - multiline content
355
     *
356
     * @return string - multiline content with trailing new line
357
     */
358
    public static function setEndingNewLine(string $content): string
359
    {
360
        $output_arr = explode(self::$eol, $content);
361
362
        if (!empty(end($output_arr))) {
363
            $output_arr[] = '';
364
        } elseif (end($output_arr) === '' && prev($output_arr) === '') {
365
            array_pop($output_arr);
366
        }
367
368
        return implode(self::$eol, $output_arr);
369
    }
370
371
    /**
372
     * The file must end with an empty line.
373
     * @inheritdoc
374
     */
375
    public function toString(): string
376
    {
377
        return self::purify($this->content);
378
    }
379
380
    /**
381
     * replace langvar if already exists with same msgctxt
382
     * and create new if not
383
     * @todo add langvar right after removed
384
     * @param string $msgctxt
385
     * @param string $msgid
386
     * @param string msgstr - optional, gets value of $msgid if empty
0 ignored issues
show
Bug introduced by
The type generators\Language\msgstr was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
387
     *
388
     * @return LanguageGenerator
389
     */
390
    public function replaceLangvar(string $msgctxt, string $msgid, string $msgstr = '')
391
    {
392
        if (empty($msgctxt)) {
393
            throw new \InvalidArgumentException('msgctxt cannot be empty');
394
        }
395
396
        $saved_langvar = $this->findByKeyInRecycleBin($msgctxt);
397
398
        if ($saved_langvar) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $saved_langvar of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
introduced by
$saved_langvar is a non-empty array, thus is always true.
Loading history...
399
            list('msgctxt' => $msgctxt, 'msgid' => $msgid, 'msgstr' => $msgstr) = $saved_langvar;
400
            $langvar_lines = [
401
                "msgctxt \"$msgctxt\"",
402
                "msgid \"$msgid\"",
403
                "msgstr \"$msgstr\""
404
            ];
405
        } else {
406
            $msgstr_actual = $msgstr ?: $msgid;
407
408
            $langvar_lines = [
409
                "msgctxt \"$msgctxt\"",
410
                "msgid \"$msgid\"",
411
                "msgstr \"$msgstr_actual\""
412
            ];
413
414
            $this->removeByKey($msgctxt);
415
        }
416
        
417
        $this->appendContent(implode(PHP_EOL, $langvar_lines));
418
       
419
        return $this;
420
    }
421
422
    /**
423
     * Checks langvars for edited manualy
424
     * If msgctxt "SettingsOptions::sd_addon::name" has msgid "Name"
425
     * So it didn't modified manually
426
     * because Name created from name id
427
     * but if it was msgid "Vendor name" - it was modified
428
     * 
429
     * @return bool
430
     */
431
    public static function checkForEdited(string $msgctxt, string $msgid): bool
432
    {
433
        $msg_parts = explode('::', $msgctxt);
434
        $last_item = end($msg_parts);
435
436
        return strcmp(parse_to_readable($last_item), $msgid) !== 0;
437
    }
438
439
    /**
440
     * add langvar
441
     * @param string $msgctxt
442
     * @param string $msgid
443
     * @param string msgstr - optional, gets value of $msgid if empty
444
     * @throws DuplicateException if langvar with such msgctxt already exists
445
     *
446
     * @return LanguageGenerator
447
     */
448
    public function addLangvar(string $msgctxt, string $msgid, string $msgstr = '')
449
    {
450
        if ($this->findByKey($msgctxt)) {
451
            throw new DuplicateException('langvar with same msgctxt already exists: ' . $msgctxt);
452
        }
453
454
        $this->replaceLangvar($msgctxt, $msgid, $msgstr);
455
456
        return $this;
457
    }
458
459
    /**
460
     * Clears multiple empty lines
461
     * 
462
     * @param string $content - content to be purified
463
     * 
464
     * @return string - purified content
465
     */
466
    public static function purify(string $content): string
467
    {
468
        return self::setEndingNewLine(
469
            self::clearWhitespaces($content)
470
        );
471
    }
472
473
    /**
474
     * Reduces multiple empty lines to one
475
     * 
476
     * @param string $content - content to be purified
477
     * 
478
     * @return string content without multiple whitespaces
479
     */
480
    public static function clearWhitespaces(string $content): string
481
    {
482
        return preg_replace('/(' . self::$eol . '{3,})/sm', str_repeat(self::$eol, 2), $content);
483
    }
484
485
    /**
486
     * @todo
487
     * add $this->defaultLanguageContent - this property may contents default language values like en/addons/addon.po
488
     * For what? we can duplicate it by $this->create() with Ru language then $this->duplicateFromDefault()
489
     * and now we have ru/addons/addon.po with same structure
490
     *
491
     * to be realized
492
     * $this->setDefaultContent(string $content): LanguageGenerator
493
     * $this->getDefaultContent(): string - results without po heading (which creates by $this->create()) - it can be cutted on setDefaultContent
494
     * $this->replaceLangvarIfNotEmpty(string $msgctxt, string $msgid, string $msgstr = ''): LanguageGenerator - replaceLangvar analog,
495
     *    but not fires if new msgid is empty
496
     *
497
     * $this->duplicateFromDefaut(): LanguageGenerator
498
     *
499
     * So instead of Generator->create()->addLangvar()->...->toString()
500
     * should be Generator->create()->setDefaultContent($file_content)->duplicateFromDefault()->toString()
501
     * now we have a copy of default language file and we can translate it
502
     *
503
     */
504
}
505