Completed
Pull Request — master (#2252)
by ྅༻ Ǭɀħ
02:22
created

PO::export_entry()   D

Complexity

Conditions 11
Paths 129

Size

Total Lines 39
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 28
nc 129
nop 1
dl 0
loc 39
rs 4.9629
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * This file is part of the POMO package.
4
 *
5
 * @copyright 2014 POMO
6
 * @license GPL
7
 */
8
9
namespace POMO;
10
11
use POMO\Translations\GettextTranslations;
12
use POMO\Translations\EntryTranslations;
13
14
ini_set('auto_detect_line_endings', 1);
15
16
/**
17
 * Class for working with PO files
18
 */
19
class PO extends GettextTranslations
20
{
21
    const MAX_LINE_LEN = 79;
22
23
    public $comments_before_headers = '';
24
25
    /**
26
     * Exports headers to a PO entry
27
     *
28
     * @return string msgid/msgstr PO entry for this PO file headers, doesn't
29
     *                contain newline at the end
30
     */
31
    public function export_headers()
32
    {
33
        $header_string = '';
34
        foreach ($this->headers as $header => $value) {
35
            $header_string.= "$header: $value\n";
36
        }
37
        $poified = PO::poify($header_string);
38
        if ($this->comments_before_headers) {
39
            $before_headers = PO::prepend_each_line(rtrim(
40
                $this->comments_before_headers)."\n", '# '
41
            );
42
        } else {
43
            $before_headers = '';
44
        }
45
46
        return rtrim("{$before_headers}msgid \"\"\nmsgstr $poified");
47
    }
48
49
    /**
50
     * Exports all entries to PO format
51
     *
52
     * @return string sequence of mgsgid/msgstr PO strings, doesn't containt
53
     *                newline at the end
54
     */
55
    public function export_entries()
56
    {
57
        //TODO: sorting
58
        return implode("\n\n", array_map(
59
            array(__NAMESPACE__.'\PO', 'export_entry'),
60
            $this->entries)
61
        );
62
    }
63
64
    /**
65
     * Exports the whole PO file as a string
66
     *
67
     * @param  bool   $include_headers whether to include the headers in the
68
     *                                 export
69
     * @return string ready for inclusion in PO file string for headers and all
70
     *                                the enrtries
71
     */
72
    public function export($include_headers = true)
73
    {
74
        $res = '';
75
        if ($include_headers) {
76
            $res .= $this->export_headers();
77
            $res .= "\n\n";
78
        }
79
        $res .= $this->export_entries();
80
81
        return $res;
82
    }
83
84
    /**
85
     * Same as {@link export}, but writes the result to a file
86
     *
87
     * @param  string $filename        where to write the PO string
88
     * @param  bool   $include_headers whether to include tje headers in the
89
     *                                 export
90
     * @return bool   true on success, false on error
91
     */
92
    public function export_to_file($filename, $include_headers = true)
93
    {
94
        $fh = fopen($filename, 'w');
95
        if (false === $fh) {
96
            return false;
97
        }
98
        $export = $this->export($include_headers);
99
        $res = fwrite($fh, $export);
100
        if (false === $res) {
101
            return false;
102
        }
103
104
        return fclose($fh);
105
    }
106
107
    /**
108
     * Text to include as a comment before the start of the PO contents
109
     *
110
     * Doesn't need to include # in the beginning of lines, these are added
111
     * automatically
112
     *
113
     * @param string $text Comment text
114
     */
115
    public function set_comment_before_headers($text)
116
    {
117
        $this->comments_before_headers = $text;
118
    }
119
120
    /**
121
     * Formats a string in PO-style
122
     *
123
     * @param  string $string the string to format
124
     * @return string the poified string
125
     */
126
    public static function poify($string)
127
    {
128
        $quote = '"';
129
        $slash = '\\';
130
        $newline = "\n";
131
132
        $replaces = array(
133
            "$slash"    => "$slash$slash",
134
            "$quote"    => "$slash$quote",
135
            "\t"        => '\t',
136
        );
137
138
        $string = str_replace(
139
            array_keys($replaces),
140
            array_values($replaces),
141
            $string
142
        );
143
144
        $po = $quote.implode(
145
            "${slash}n$quote$newline$quote",
146
            explode($newline, $string)
147
        ).$quote;
148
        // add empty string on first line for readbility
149
        if (false !== strpos($string, $newline) &&
150
                (substr_count($string, $newline) > 1 ||
151
                !($newline === substr($string, -strlen($newline))))) {
152
            $po = "$quote$quote$newline$po";
153
        }
154
        // remove empty strings
155
        $po = str_replace("$newline$quote$quote", '', $po);
156
157
        return $po;
158
    }
159
160
    /**
161
     * Gives back the original string from a PO-formatted string
162
     *
163
     * @param  string $string PO-formatted string
164
     * @return string enascaped string
165
     */
166
    public static function unpoify($string)
167
    {
168
        $escapes = array('t' => "\t", 'n' => "\n", '\\' => '\\');
169
        $lines = array_map('trim', explode("\n", $string));
170
        $lines = array_map(array(__NAMESPACE__.'\PO', 'trim_quotes'), $lines);
171
        $unpoified = '';
172
        $previous_is_backslash = false;
173
        foreach ($lines as $line) {
174
            preg_match_all('/./u', $line, $chars);
175
            $chars = $chars[0];
176
            foreach ($chars as $char) {
177
                if (!$previous_is_backslash) {
178
                    if ('\\' == $char) {
179
                        $previous_is_backslash = true;
180
                    } else {
181
                        $unpoified .= $char;
182
                    }
183
                } else {
184
                    $previous_is_backslash = false;
185
                    $unpoified .= isset($escapes[$char]) ?
186
                        $escapes[$char] :
187
                        $char;
188
                }
189
            }
190
        }
191
192
        return $unpoified;
193
    }
194
195
    /**
196
     * Inserts $with in the beginning of every new line of $string and
197
     * returns the modified string
198
     *
199
     * @param  string $string prepend lines in this string
200
     * @param  string $with   prepend lines with this string
201
     * @return string The modified string
202
     */
203
    public static function prepend_each_line($string, $with)
204
    {
205
        $php_with = var_export($with, true);
206
        $lines = explode("\n", $string);
207
        // do not prepend the string on the last empty line, artefact by explode
208
        if ("\n" == substr($string, -1)) {
209
            unset($lines[count($lines) - 1]);
210
        }
211
        $res = implode("\n", array_map(
212
                create_function('$x', "return $php_with.\$x;"),
213
                $lines
214
            ));
215
        // give back the empty line, we ignored above
216
        if ("\n" == substr($string, -1)) {
217
            $res .= "\n";
218
        }
219
220
        return $res;
221
    }
222
223
    /**
224
     * Prepare a text as a comment -- wraps the lines and prepends #
225
     * and a special character to each line
226
     *
227
     * @param  string $text the comment text
228
     * @param  string $char character to denote a special PO comment,
229
     *                      like :, default is a space
230
     * @return string The modified string
231
     */
232
    private static function comment_block($text, $char=' ')
233
    {
234
        $text = wordwrap($text, self::MAX_LINE_LEN - 3);
235
236
        return self::prepend_each_line($text, "#$char ");
237
    }
238
239
    /**
240
     * Builds a string from the entry for inclusion in PO file
241
     *
242
     * @param  object      &$entry the entry to convert to po string
243
     * @return string|bool PO-style formatted string for the entry or
244
     *                            false if the entry is empty
245
     */
246
    public static function export_entry(&$entry)
247
    {
248
        if (is_null($entry->singular)) {
249
            return false;
250
        }
251
        $po = array();
252
        if (!empty($entry->translator_comments)) {
253
            $po[] = self::comment_block($entry->translator_comments);
254
        }
255
        if (!empty($entry->extracted_comments)) {
256
            $po[] = self::comment_block($entry->extracted_comments, '.');
257
        }
258
        if (!empty($entry->references)) {
259
            $po[] = self::comment_block(implode(' ', $entry->references), ':');
260
        }
261
        if (!empty($entry->flags)) {
262
            $po[] = self::comment_block(implode(", ", $entry->flags), ',');
263
        }
264
        if (!is_null($entry->context)) {
265
            $po[] = 'msgctxt '.self::poify($entry->context);
266
        }
267
        $po[] = 'msgid '.self::poify($entry->singular);
268
        if (!$entry->is_plural) {
269
            $translation = empty($entry->translations) ?
270
                '' :
271
                $entry->translations[0];
272
            $po[] = 'msgstr '.self::poify($translation);
273
        } else {
274
            $po[] = 'msgid_plural '.self::poify($entry->plural);
275
            $translations = empty($entry->translations) ?
276
                array('', '') :
277
                $entry->translations;
278
            foreach ($translations as $i => $translation) {
279
                $po[] = "msgstr[$i] ".self::poify($translation);
280
            }
281
        }
282
283
        return implode("\n", $po);
284
    }
285
286
    public function import_from_file($filename)
287
    {
288
        $f = fopen($filename, 'r');
289
        if (!$f) {
290
            return false;
291
        }
292
        $lineno = 0;
293
        while (true) {
294
            $res = $this->read_entry($f, $lineno);
295
            if (!$res) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $res 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...
296
                break;
297
            }
298
            if ($res['entry']->singular == '') {
299
                $this->set_headers(
300
                    $this->make_headers($res['entry']->translations[0])
301
                );
302
            } else {
303
                $this->add_entry($res['entry']);
304
            }
305
        }
306
        PO::read_line($f, 'clear');
307
        if (false === $res) {
0 ignored issues
show
Bug introduced by
The variable $res does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
308
            return false;
309
        }
310
        if (! $this->headers && ! $this->entries) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->headers 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...
Bug Best Practice introduced by
The expression $this->entries 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...
311
            return false;
312
        }
313
314
        return true;
315
    }
316
317
    public function read_entry($f, $lineno = 0)
318
    {
319
        $entry = new EntryTranslations();
320
        // where were we in the last step
321
        // can be: comment, msgctxt, msgid, msgid_plural, msgstr, msgstr_plural
322
        $context = '';
323
        $msgstr_index = 0;
324
        $is_final = create_function('$context', 'return $context == "msgstr" || $context == "msgstr_plural";');
325
        while (true) {
326
            $lineno++;
327
            $line = PO::read_line($f);
328
            if (!$line) {
329
                if (feof($f)) {
330
                    if ($is_final($context)) {
331
                        break;
332
                    } elseif (!$context) {// we haven't read a line and eof came
333
334
                        return null;
335
                    } else {
336
                        return false;
337
                    }
338
                } else {
339
                    return false;
340
                }
341
            }
342
            if ($line == "\n") {
343
                continue;
344
            }
345
346
            $line = trim($line);
347
            if (preg_match('/^#/', $line, $m)) {
348
                // the comment is the start of a new entry
349
                if ($is_final($context)) {
350
                    PO::read_line($f, 'put-back');
351
                    $lineno--;
352
                    break;
353
                }
354
                // comments have to be at the beginning
355
                if ($context && $context != 'comment') {
356
                    return false;
357
                }
358
                // add comment
359
                $this->add_comment_to_entry($entry, $line);
360
            } elseif (preg_match('/^msgctxt\s+(".*")/', $line, $m)) {
361
                if ($is_final($context)) {
362
                    PO::read_line($f, 'put-back');
363
                    $lineno--;
364
                    break;
365
                }
366
                if ($context && $context != 'comment') {
367
                    return false;
368
                }
369
                $context = 'msgctxt';
370
                $entry->context .= PO::unpoify($m[1]);
371
            } elseif (preg_match('/^msgid\s+(".*")/', $line, $m)) {
372
                if ($is_final($context)) {
373
                    PO::read_line($f, 'put-back');
374
                    $lineno--;
375
                    break;
376
                }
377
                if ($context &&
378
                    $context != 'msgctxt' &&
379
                    $context != 'comment') {
380
                    return false;
381
                }
382
                $context = 'msgid';
383
                $entry->singular .= PO::unpoify($m[1]);
384
            } elseif (preg_match('/^msgid_plural\s+(".*")/', $line, $m)) {
385
                if ($context != 'msgid') {
386
                    return false;
387
                }
388
                $context = 'msgid_plural';
389
                $entry->is_plural = true;
390
                $entry->plural .= PO::unpoify($m[1]);
391
            } elseif (preg_match('/^msgstr\s+(".*")/', $line, $m)) {
392
                if ($context != 'msgid') {
393
                    return false;
394
                }
395
                $context = 'msgstr';
396
                $entry->translations = array(PO::unpoify($m[1]));
397
            } elseif (preg_match('/^msgstr\[(\d+)\]\s+(".*")/', $line, $m)) {
398
                if ($context != 'msgid_plural' && $context != 'msgstr_plural') {
399
                    return false;
400
                }
401
                $context = 'msgstr_plural';
402
                $msgstr_index = $m[1];
403
                $entry->translations[$m[1]] = PO::unpoify($m[2]);
404
            } elseif (preg_match('/^".*"$/', $line)) {
405
                $unpoified = PO::unpoify($line);
406
                switch ($context) {
407
                    case 'msgid':
408
                        $entry->singular .= $unpoified;
409
                        break;
410
                    case 'msgctxt':
411
                        $entry->context .= $unpoified;
412
                        break;
413
                    case 'msgid_plural':
414
                        $entry->plural .= $unpoified;
415
                        break;
416
                    case 'msgstr':
417
                        $entry->translations[0] .= $unpoified;
418
                        break;
419
                    case 'msgstr_plural':
420
                        $entry->translations[$msgstr_index] .= $unpoified;
421
                        break;
422
                    default:
423
                        return false;
424
                }
425
            } else {
426
                return false;
427
            }
428
        }
429
        if (array() == array_filter(
430
                $entry->translations,
431
                create_function('$t', 'return $t || "0" === $t;'))
432
            ) {
433
            $entry->translations = array();
434
        }
435
436
        return array('entry' => $entry, 'lineno' => $lineno);
437
    }
438
439
    public static function read_line($f, $action = 'read')
440
    {
441
        static $last_line = '';
442
        static $use_last_line = false;
443
        if ('clear' == $action) {
444
            $last_line = '';
445
446
            return true;
447
        }
448
        if ('put-back' == $action) {
449
            $use_last_line = true;
450
451
            return true;
452
        }
453
        $line = $use_last_line ? $last_line : fgets($f);
454
        $line = ( "\r\n" == substr( $line, -2 ) ) ?
455
            rtrim( $line, "\r\n" ) . "\n" :
456
            $line;
457
        $last_line = $line;
458
        $use_last_line = false;
459
460
        return $line;
461
    }
462
463
    public function add_comment_to_entry(&$entry, $po_comment_line)
464
    {
465
        $first_two = substr($po_comment_line, 0, 2);
466
        $comment = trim(substr($po_comment_line, 2));
467
        if ('#:' == $first_two) {
468
            $entry->references = array_merge(
469
                $entry->references,
470
                preg_split('/\s+/', $comment)
471
            );
472
        } elseif ('#.' == $first_two) {
473
            $entry->extracted_comments = trim(
474
                $entry->extracted_comments . "\n" . $comment
475
            );
476
        } elseif ('#,' == $first_two) {
477
            $entry->flags = array_merge(
478
                $entry->flags,
479
                preg_split('/,\s*/', $comment)
480
            );
481
        } else {
482
            $entry->translator_comments = trim(
483
                $entry->translator_comments . "\n" . $comment
484
            );
485
        }
486
    }
487
488
    public static function trim_quotes($s)
489
    {
490
        if ( substr($s, 0, 1) == '"') {
491
            $s = substr($s, 1);
492
        }
493
        if ( substr($s, -1, 1) == '"') {
494
            $s = substr($s, 0, -1);
495
        }
496
497
        return $s;
498
    }
499
}
500