Completed
Push — 4.9 ( b3f7c3...779bac )
by Mikhail
02:00
created

LanguageGenerator   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 469
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 44
eloc 140
dl 0
loc 469
rs 8.8798
c 0
b 0
f 0

27 Methods

Rating   Name   Duplication   Size   Complexity  
A findByKey() 0 3 1
A findByKeyInRecycleBin() 0 3 1
A getKeyGenerator() 0 3 1
A addLangvar() 0 9 2
A removeByKey() 0 20 2
A purify() 0 4 1
A replaceLangvar() 0 30 4
A clearWhitespaces() 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 checkForEdited() 0 6 1
A __construct() 0 4 2
A setContent() 0 5 1
A create() 0 34 2
A replaceEol() 0 3 1
A appendRecycleBin() 0 8 2
A getRecycleBin() 0 3 1
A appendContent() 0 8 2
A setRecycleBin() 0 5 1
A getPath() 0 22 2
A setMediator() 0 3 1
A getKey() 0 3 1
A getTemplateFilename() 0 3 1
A checkLanguageSupport() 0 3 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 FILENAME
11
  * @property string $templatePath
12
  * @property string $language
13
  * @property string $recycleBin - buffer to which be removed all langvars from actual content
14
  * @property string $content
15
  * @property Config $config
16
  * @property AbstractMediator $mediator
17
  * @property array $codes
18
  * @property string $eol - end of line char
19
  * @todo add all $codes supported by cs-cart
20
  */
21
final class LanguageGenerator extends \generators\AbstractGenerator
22
{
23
    const FILENAME = 'var/langs/${lang}/addons/${addon}.po';
24
    private $templatePath = '';
25
    private $language;
26
    private $recycleBin = '';
27
    private $content = '';
28
    private $config;
29
    private $mediator;
30
    private static $codes = [
31
        'en' => ['pack-name' => 'English', 'country-code' => 'US'],
32
        'ru' => ['pack-name' => 'Russian', 'country-code' => 'RU']
33
    ];
34
    private static $eol = "\n";
35
36
    function __construct(Config $config, string $language = '')
37
    {
38
        $this->config   = $config;
39
        $this->language =  empty($language) ? $this->config->get('addon.default_language') : $language;
40
    }
41
42
    /**
43
     * @inheritdoc
44
     */
45
    public function getTemplateFilename(): string
46
    {
47
        return $this->templatePath;
48
    }
49
50
    public function setMediator(AbstractMediator $mediator): void
51
    {
52
        $this->mediator = $mediator;
53
    }
54
55
    public function getPath(): string
56
    {
57
        $addon_id = $this->config->get('addon.id');
58
59
        if (!$addon_id) {
60
            throw new \InvalidArgumentException('Addon id (name) not specified');
61
        }
62
63
        $path = $this->config->get('filesystem.output_path')
64
            . str_replace(
65
                [
66
                    '${lang}',
67
                    '${addon}'
68
                ],
69
                [
70
                    $this->language,
71
                    $addon_id
72
                ],
73
                static::FILENAME
74
            );
75
76
        return sanitize_filename($path);
77
    }
78
79
    /**
80
     * Check language for support
81
     * @param string $language
82
     *
83
     * @return bool
84
     */
85
    public static function checkLanguageSupport(string $language): bool
86
    {
87
        return array_key_exists($language, self::$codes);
88
    }
89
90
    /**
91
     * Replaces different style eol by one
92
     * 
93
     * @param string $content - content wich will be changed
94
     * 
95
     * @return string - content with one-style eol
96
     */
97
    public static function replaceEol(string $content): string
98
    {
99
        return preg_replace('~\r\n?~', self::$eol, $content);
100
    }
101
102
    /**
103
     * @inheritdoc
104
     *
105
     * @return LanguageGenerator
106
     */
107
    public function setContent(string $content)
108
    {
109
        $this->content = self::replaceEol($content);
110
111
        return $this;
112
    }
113
114
    /**
115
     * get recycleBin
116
     * 
117
     * @return string
118
     */
119
    public function getRecycleBin(): string
120
    {
121
        return self::purify($this->recycleBin);
122
    }
123
124
    /**
125
     * Set content to recycleBin
126
     *
127
     * @return LanguageGenerator
128
     */
129
    public function setRecycleBin(string $content)
130
    {
131
        $this->recycleBin = self::replaceEol($content);
132
133
        return $this;
134
    }
135
136
    /**
137
     * Append content to current generator content
138
     * @param string $content - content to append
139
     *
140
     * @return LanguageGenerator
141
     */
142
    public function appendContent(string $content)
143
    {
144
        $this->setContent(
145
            (empty($this->content) ? '' : self::setEndingNewLine($this->content) . PHP_EOL)
146
            . $content
147
        );
148
149
        return $this;
150
    }
151
152
    /**
153
     * Append content to recycleBin
154
     *
155
     * @return LanguageGenerator
156
     */
157
    public function appendRecycleBin(string $content)
158
    {
159
        $this->setRecycleBin(
160
            (empty($this->recycleBin) ? '' : self::setEndingNewLine($this->recycleBin) . PHP_EOL)
161
            . $content
162
        );
163
164
        return $this;
165
    }
166
167
    /**
168
     * create po heading structure
169
     * @throws \InvalidArgumentException if nor language param and addon default_language are specified
170
     *
171
     * @return LanguageGenerator
172
     */
173
    public function create()
174
    {
175
        $po_heading_template = <<<'EOD'
176
msgid ""
177
msgstr ""
178
"Language: ${code}\n"
179
"Content-Type: text/plain; charset=UTF-8\n"
180
"Pack-Name: ${pack-name}\n"
181
"Lang-Code: ${code}\n"
182
"Country-Code: ${country-code}\n"
183
EOD;
184
185
        if (!$this->language) {
186
            throw new \InvalidArgumentException('Nor language param and addon default_language are specified');
187
        }
188
189
        $language_information   = self::$codes[$this->language];
190
        $po_heading = str_replace(
191
            [
192
                '${code}',
193
                '${pack-name}',
194
                '${country-code}'
195
            ],
196
            [
197
                $this->language,
198
                $language_information['pack-name'],
199
                $language_information['country-code']
200
            ],
201
            $po_heading_template
202
        );
203
204
        $this->content = $po_heading;
205
206
        return $this;
207
    }
208
209
    /**
210
     * Constructs langvar full key code
211
     * @param string $type - example: Languages
212
     * @param string $arguments - parts of path for generating msgxtxt key
213
     * @todo validate subpath for containing only [a-z_\.]/i - throw exception if not - write tests
214
     *
215
     * @return string - return langvar string like Languages::email_marketing.subscription_confirmed
216
     */
217
    public static function getTranslationKey(string $type, ...$arguments): string
218
    {
219
        return self::getKeyGenerator($type)::generate(...$arguments);
220
    }
221
222
    private static function getKeyGenerator(string $type)
223
    {
224
        return '\\generators\\Language\\keyGenerators\\' . $type;
225
    }
226
227
    /**
228
     * Get langvar array from content
229
     * @param string $full_key - key for search like Languages::email_marketing.subscription_confirmed
230
     * @throws \InvalidArgumentException if $full_key is empty
231
     *
232
     * @return bool|array - [
233
     *  'msgctxt' => "Languages::payments.epdq.tbl_bgcolor",
234
     *  'msgid' =>  "Table background color",
235
     *  'msgstr' => "Table background color"
236
     * ]
237
     */
238
    public function findByKey(string $full_key)
239
    {
240
        return self::findByKeyIn($full_key, $this->content);
241
    }
242
243
    /**
244
     * Get langvar array from recycleBin
245
     * @param string $full_key - key for search like Languages::email_marketing.subscription_confirmed
246
     * @throws \InvalidArgumentException if $full_key is empty
247
     *
248
     * @return bool|array - [
249
     *  'msgctxt' => "Languages::payments.epdq.tbl_bgcolor",
250
     *  'msgid' =>  "Table background color",
251
     *  'msgstr' => "Table background color"
252
     * ]
253
     */
254
    public function findByKeyInRecycleBin(string $full_key)
255
    {
256
        return self::findByKeyIn($full_key, $this->recycleBin);
257
    }
258
259
    /**
260
     * Get langvar array from specified content
261
     * @param string $full_key - key for search like Languages::email_marketing.subscription_confirmed
262
     * @throws \InvalidArgumentException if $full_key is empty
263
     *
264
     * @return bool|array - [
265
     *  'msgctxt' => "Languages::payments.epdq.tbl_bgcolor",
266
     *  'msgid' =>  "Table background color",
267
     *  'msgstr' => "Table background color"
268
     * ]
269
     */
270
    public static function findByKeyIn(string $full_key, string $content)
271
    {
272
        if (!$full_key) {
273
            throw new \InvalidArgumentException('full_key cannot be empty');
274
        }
275
276
        $found_count = preg_match_all(
277
            '/(msgctxt\s+"(' . $full_key . ')")' . self::$eol . '+(msgid\s+"(.*)")' . self::$eol . '+(msgstr\s+"(.*)")/umi',
278
            $content,
279
            $matches
280
        );
281
282
        if ($found_count === 0 || $found_count === false) {
283
            return false;
284
        }
285
286
        return [
287
            'msgctxt' => $matches[2][0],
288
            'msgid' => $matches[4][0],
289
            'msgstr' => $matches[6][0]
290
        ];
291
    }
292
293
    /**
294
     * Fully remove langvar, that matches msgctxt (msgctxt)
295
     * @param string $msgctxt - msgctxt
296
     * @throws \InvalidArgumentException if $msgctxt is empty
297
     *
298
     * @return LanguageGenerator
299
     */
300
    public function removeByKey(string $msgctxt)
301
    {
302
        if (!$msgctxt) {
303
            throw new \InvalidArgumentException('msgctxt cannot be empty');
304
        }
305
306
        $recycle_bin = '';
307
        $new_content = preg_replace_callback(
308
            '/(msgctxt\s+"' . $msgctxt . '"' . self::$eol . '+msgid\s+".*"' . self::$eol . '+msgstr\s+".*")(' . self::$eol . '*)/umi',
309
            function($matches) use (&$recycle_bin) {
310
                $recycle_bin .= $matches[1] . $matches[2];
311
                return '';
312
            },
313
            $this->content
314
        );
315
316
        $this->setContent($new_content);
317
        $this->appendRecycleBin($recycle_bin);
318
319
        return $this;
320
    }
321
322
    /**
323
     * Fully removes all langvars with a specified id
324
     * 
325
     * @param string $id
326
     * 
327
     * @return LanguageGenerator
328
     */
329
    public function removeById(string $id)
330
    {
331
        if (!$id) {
332
            throw new \InvalidArgumentException('id cannot be empty');
333
        }
334
335
        $recycle_bin = '';
336
        $new_content = preg_replace_callback(
337
            '/(msgctxt\s+"[\w:._]+' . $this->config->get('addon.id') . '::' . $id . '[\w:._]*"' . self::$eol . '+msgid\s+".*"' . self::$eol . '+msgstr\s+".*")(' . self::$eol . '*)/umi',
338
            function($matches) use (&$recycle_bin) {
339
                $recycle_bin .= $matches[1] . $matches[2];
340
                return '';
341
            },
342
            $this->content
343
        );
344
345
        $this->setContent($new_content);
346
        $this->appendRecycleBin($recycle_bin);
347
348
        return $this;
349
    }
350
351
    /**
352
     * Check for ending line and add it if not found
353
     * @param string $content - multiline content
354
     *
355
     * @return string - multiline content with trailing new line
356
     */
357
    public static function setEndingNewLine(string $content): string
358
    {
359
        $output_arr = explode(self::$eol, $content);
360
361
        if (!empty(end($output_arr))) {
362
            $output_arr[] = '';
363
        } elseif (end($output_arr) === '' && prev($output_arr) === '') {
364
            array_pop($output_arr);
365
        }
366
367
        return implode(self::$eol, $output_arr);
368
    }
369
370
    /**
371
     * The file must end with an empty line.
372
     * @inheritdoc
373
     */
374
    public function toString(): string
375
    {
376
        return self::purify($this->content);
377
    }
378
379
    /**
380
     * replace langvar if already exists with same msgctxt
381
     * and create new if not
382
     * @todo add langvar right after removed
383
     * @param string $msgctxt
384
     * @param string $msgid
385
     * @param string $msgstr - optional, gets value of $msgid if empty
386
     *
387
     * @return LanguageGenerator
388
     */
389
    public function replaceLangvar(string $msgctxt, string $msgid, string $msgstr = '')
390
    {
391
        if (empty($msgctxt)) {
392
            throw new \InvalidArgumentException('msgctxt cannot be empty');
393
        }
394
395
        $saved_langvar = $this->findByKeyInRecycleBin($msgctxt);
396
397
        if ($saved_langvar) {
0 ignored issues
show
introduced by
$saved_langvar is a non-empty array, thus is always true.
Loading history...
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...
398
            list('msgctxt' => $msgctxt, 'msgid' => $msgid, 'msgstr' => $msgstr) = $saved_langvar;
399
            $langvar_lines = [
400
                "msgctxt \"$msgctxt\"",
401
                "msgid \"$msgid\"",
402
                "msgstr \"$msgstr\""
403
            ];
404
        } else {
405
            $msgstr_actual = $msgstr ?: $msgid;
406
407
            $langvar_lines = [
408
                "msgctxt \"$msgctxt\"",
409
                "msgid \"$msgid\"",
410
                "msgstr \"$msgstr_actual\""
411
            ];
412
413
            $this->removeByKey($msgctxt);
414
        }
415
        
416
        $this->appendContent(implode(PHP_EOL, $langvar_lines));
417
       
418
        return $this;
419
    }
420
421
    /**
422
     * Checks langvars for edited manualy
423
     * If msgctxt "SettingsOptions::sd_addon::name" has msgid "Name"
424
     * So it didn't modified manually
425
     * because Name created from name id
426
     * but if it was msgid "Vendor name" - it was modified
427
     * 
428
     * @return bool
429
     */
430
    public static function checkForEdited(string $msgctxt, string $msgid): bool
431
    {
432
        $msg_parts = explode('::', $msgctxt);
433
        $last_item = end($msg_parts);
434
435
        return strcmp(parse_to_readable($last_item), $msgid) !== 0;
436
    }
437
438
    /**
439
     * add langvar
440
     * @param string $msgctxt
441
     * @param string $msgid
442
     * @param string $msgstr - optional, gets value of $msgid if empty
443
     * @throws DuplicateException if langvar with such msgctxt already exists
444
     *
445
     * @return LanguageGenerator
446
     */
447
    public function addLangvar(string $msgctxt, string $msgid, string $msgstr = '')
448
    {
449
        if ($this->findByKey($msgctxt)) {
450
            throw new DuplicateException('langvar with same msgctxt already exists: ' . $msgctxt);
451
        }
452
453
        $this->replaceLangvar($msgctxt, $msgid, $msgstr);
454
455
        return $this;
456
    }
457
458
    /**
459
     * Clears multiple empty lines
460
     * 
461
     * @param string $content - content to be purified
462
     * 
463
     * @return string - purified content
464
     */
465
    public static function purify(string $content): string
466
    {
467
        return self::setEndingNewLine(
468
            self::clearWhitespaces($content)
469
        );
470
    }
471
472
    /**
473
     * Reduces multiple empty lines to one
474
     * 
475
     * @param string $content - content to be purified
476
     * 
477
     * @return string content without multiple whitespaces
478
     */
479
    public static function clearWhitespaces(string $content): string
480
    {
481
        return preg_replace('/(' . self::$eol . '{3,})/sm', str_repeat(self::$eol, 2), $content);
482
    }
483
484
    /**
485
     * @inheritdoc
486
     */
487
    public function getKey(): string
488
    {
489
        return parent::getKey() . ucfirst($this->language);
490
    }
491
492
    /**
493
     * @todo
494
     * add $this->defaultLanguageContent - this property may contents default language values like en/addons/addon.po
495
     * For what? we can duplicate it by $this->create() with Ru language then $this->duplicateFromDefault()
496
     * and now we have ru/addons/addon.po with same structure
497
     *
498
     * to be realized
499
     * $this->setDefaultContent(string $content): LanguageGenerator
500
     * $this->getDefaultContent(): string - results without po heading (which creates by $this->create()) - it can be cutted on setDefaultContent
501
     * $this->replaceLangvarIfNotEmpty(string $msgctxt, string $msgid, string $msgstr = ''): LanguageGenerator - replaceLangvar analog,
502
     *    but not fires if new msgid is empty
503
     *
504
     * $this->duplicateFromDefaut(): LanguageGenerator
505
     *
506
     * So instead of Generator->create()->addLangvar()->...->toString()
507
     * should be Generator->create()->setDefaultContent($file_content)->duplicateFromDefault()->toString()
508
     * now we have a copy of default language file and we can translate it
509
     *
510
     */
511
}
512