Passed
Push — feature/6 ( be06b0 )
by Raúl
02:00
created

Parser   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 393
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 393
rs 4.5454
c 0
b 0
f 0
wmc 59

14 Methods

Rating   Name   Duplication   Size   Complexity  
A parseHeaders() 0 16 3
C parseComment() 0 56 12
C parseProperty() 0 27 7
A __construct() 0 3 1
B parseMultiline() 0 18 6
C parse() 0 56 10
B isHeader() 0 48 6
A unquote() 0 3 1
A parseFile() 0 5 1
A getProperty() 0 5 1
B parseLine() 0 19 5
A parseString() 0 6 1
A shouldIgnoreLine() 0 3 2
A shouldCloseEntry() 0 6 3

How to fix   Complexity   

Complex Class

Complex classes like Parser 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 Parser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Sepia\PoParser;
4
5
use Sepia\PoParser\Catalog\Catalog;
6
use Sepia\PoParser\Catalog\CatalogArray;
7
use Sepia\PoParser\Catalog\EntryFactory;
8
use Sepia\PoParser\Catalog\Header;
9
use Sepia\PoParser\Catalog\HeaderFactory;
0 ignored issues
show
Bug introduced by
The type Sepia\PoParser\Catalog\HeaderFactory 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...
10
use Sepia\PoParser\Catalog\Headers;
11
use Sepia\PoParser\Exception\ParseException;
12
use Sepia\PoParser\SourceHandler\FileSystem;
13
use Sepia\PoParser\SourceHandler\SourceHandler;
14
use Sepia\PoParser\SourceHandler\StringSource;
15
16
/**
17
 *    Copyright (c) 2012 Raúl Ferràs [email protected]
18
 *    All rights reserved.
19
 *
20
 *    Redistribution and use in source and binary forms, with or without
21
 *    modification, are permitted provided that the following conditions
22
 *    are met:
23
 *    1. Redistributions of source code must retain the above copyright
24
 *       notice, this list of conditions and the following disclaimer.
25
 *    2. Redistributions in binary form must reproduce the above copyright
26
 *       notice, this list of conditions and the following disclaimer in the
27
 *       documentation and/or other materials provided with the distribution.
28
 *    3. Neither the name of copyright holders nor the names of its
29
 *       contributors may be used to endorse or promote products derived
30
 *       from this software without specific prior written permission.
31
 *
32
 *    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
33
 *    ''AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
34
 *    TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
35
 *    PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL COPYRIGHT HOLDERS OR CONTRIBUTORS
36
 *    BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
37
 *    CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
38
 *    SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
39
 *    INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
40
 *    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
41
 *    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
42
 *    POSSIBILITY OF SUCH DAMAGE.
43
 *
44
 * https://github.com/raulferras/PHP-po-parser
45
 *
46
 * Class to parse .po file and extract its strings.
47
 *
48
 * @version 5.0
49
 */
50
class Parser
51
{
52
    /** @var SourceHandler */
53
    protected $sourceHandler;
54
55
    /** @var int */
56
    protected $lineNumber;
57
58
    /** @var string */
59
    protected $property;
60
61
    /**
62
     * Reads and parses a string
63
     *
64
     * @param string $string po content
65
     *
66
     * @throws \Exception.
67
     * @return Parser
68
     */
69
    public static function parseString($string)
70
    {
71
        $parser = new Parser(new StringSource($string));
72
        $parser->parse();
73
74
        return $parser;
75
    }
76
77
    /**
78
     * Reads and parses a file
79
     *
80
     * @param string $filePath
81
     *
82
     * @throws \Exception.
83
     * @return Catalog
84
     */
85
    public static function parseFile($filePath)
86
    {
87
        $parser = new Parser(new FileSystem($filePath));
88
89
        return $parser->parse();
90
    }
91
92
    public function __construct(SourceHandler $sourceHandler)
93
    {
94
        $this->sourceHandler = $sourceHandler;
95
    }
96
97
    /**
98
     * Reads and parses strings of a .po file.
99
     *
100
     * @param SourceHandler . Optional
101
     *
102
     * @throws \Exception, \InvalidArgumentException, ParseException
103
     * @return Catalog
104
     */
105
    public function parse(Catalog $catalog = null)
106
    {
107
        $catalog = $catalog === null ? new CatalogArray() : $catalog;
108
        $this->lineNumber = 0;
109
        $entry = array();
110
        $this->property = null; // current property
111
112
        // Flags
113
        $headersFound = false;
114
115
        while (!$this->sourceHandler->ended()) {
116
            $line = trim($this->sourceHandler->getNextLine());
117
118
            if ($this->shouldIgnoreLine($line, $entry)) {
119
                $this->lineNumber++;
120
                continue;
121
            }
122
123
            if ($this->shouldCloseEntry($line, $entry)) {
124
                if (!$headersFound && $this->isHeader($entry)) {
125
                    $headersFound = true;
126
                    $catalog->setHeaders(
127
                        $this->parseHeaders($entry['msgstr'])
128
                    );
129
                } else {
130
                    $catalog->addEntry(EntryFactory::createFromArray($entry));
131
                }
132
133
                $entry = array();
134
                $this->property = null;
135
136
                if (empty($line)) {
137
                    $this->lineNumber++;
138
                    continue;
139
                }
140
            }
141
142
            $entry = $this->parseLine($line, $entry);
143
144
            $this->lineNumber++;
145
            continue;
146
        }
147
        $this->sourceHandler->close();
148
149
        // add final entry
150
        if (count($entry)) {
151
            if ($this->isHeader($entry)) {
152
                $catalog->addHeaders(
0 ignored issues
show
Bug introduced by
The method addHeaders() does not exist on Sepia\PoParser\Catalog\Catalog. ( Ignorable by Annotation )

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

152
                $catalog->/** @scrutinizer ignore-call */ 
153
                          addHeaders(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method addHeaders() does not exist on Sepia\PoParser\Catalog\CatalogArray. ( Ignorable by Annotation )

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

152
                $catalog->/** @scrutinizer ignore-call */ 
153
                          addHeaders(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
153
                    $this->parseHeaders($entry['msgstr'])
154
                );
155
            } else {
156
                $catalog->addEntry(EntryFactory::createFromArray($entry));
157
            }
158
        }
159
160
        return $catalog;
161
    }
162
163
    /**
164
     * @param string $line
165
     * @param array  $entry
166
     *
167
     * @return array
168
     * @throws ParseException
169
     */
170
    protected function parseLine($line, $entry)
171
    {
172
        $firstChar = strlen($line) > 0 ? $line[0] : '';
173
174
        switch ($firstChar) {
175
            case '#':
176
                $entry = $this->parseComment($line, $entry);
177
                break;
178
179
            case 'm':
180
                $entry = $this->parseProperty($line, $entry);
181
                break;
182
183
            case '"':
184
                $entry = $this->parseMultiline($line, $entry);
185
                break;
186
        }
187
188
        return $entry;
189
    }
190
191
    /**
192
     * @param string $line
193
     * @param array  $entry
194
     *
195
     * @return array
196
     * @throws ParseException
197
     */
198
    protected function parseProperty($line, array $entry)
199
    {
200
        list($key, $value) = $this->getProperty($line);
201
202
        if (!isset($entry[$key])) {
203
            $entry[$key] = '';
204
        }
205
206
        switch (true) {
207
            case $key === 'msgctxt':
208
            case $key === 'msgid':
209
            case $key === 'msgid_plural':
210
            case $key === 'msgstr':
211
                $entry[$key] .= $this->unquote($value);
212
                $this->property = $key;
213
                break;
214
215
            case strpos($key, 'msgstr[') !== false:
216
                $entry[$key] .= $this->unquote($value);
217
                $this->property = $key;
218
                break;
219
220
            default:
221
                throw new ParseException(sprintf('Could not parse %s at line %d', $key, $this->lineNumber));
222
        }
223
224
        return $entry;
225
    }
226
227
    /**
228
     * @param string $line
229
     * @param array  $entry
230
     *
231
     * @return array
232
     * @throws ParseException
233
     */
234
    protected function parseMultiline($line, $entry)
235
    {
236
        switch (true) {
237
            case $this->property === 'msgctxt':
238
            case $this->property === 'msgid':
239
            case $this->property === 'msgid_plural':
240
            case $this->property === 'msgstr':
241
            case strpos($this->property, 'msgstr[') !== false:
242
                $entry[$this->property] .= $this->unquote($line);
243
                break;
244
245
            default:
246
                throw new ParseException(
247
                    sprintf('Error parsing property %s as multiline.', $this->property)
248
                );
249
        }
250
251
        return $entry;
252
    }
253
254
    /**
255
     * @param string $line
256
     * @param array  $entry
257
     *
258
     * @return array
259
     * @throws ParseException
260
     */
261
    protected function parseComment($line, $entry)
262
    {
263
        $comment = trim(substr($line, 0, 2));
264
265
        switch ($comment) {
266
            case '#,':
267
                $line = trim(substr($line, 2));
268
                $entry['flags'] = preg_split('/,\s*/', $line);
269
                break;
270
271
            case '#.':
272
                $entry['ccomment'] = !isset($entry['ccomment']) ? array() : $entry['ccomment'];
273
                $entry['ccomment'][] = trim(substr($line, 2));
274
                break;
275
276
277
            case '#|':  // Previous string
278
            case '#~':  // Old entry
279
            case '#~|': // Previous string old
280
                $mode = array(
281
                    '#|' => 'previous',
282
                    '#~' => 'obsolete',
283
                    '#~|' => 'previous-obsolete'
284
                );
285
286
                $line = trim(substr($line, 2));
287
                $property = $mode[$comment];
288
                if ($property === 'previous') {
289
                    if (!isset($entry[$property])) {
290
                        $subEntry = array();
291
                    } else {
292
                        $subEntry = $entry[$property];
293
                    }
294
295
                    $subEntry = $this->parseLine($line, $subEntry);
296
                    //$subEntry = $this->parseProperty($line, $subEntry);
297
                    $entry[$property] = $subEntry;
298
                } else {
299
                    $entry = $this->parseLine($line, $entry);
300
                    $entry['obsolete'] = true;
301
                }
302
                break;
303
304
            // Reference
305
            case '#:':
306
                $entry['reference'][] = trim(substr($line, 2));
307
                break;
308
309
            case '#':
310
            default:
311
                $entry['tcomment'] = !isset($entry['tcomment']) ? array() : $entry['tcomment'];
312
                $entry['tcomment'][] = trim(substr($line, 1));
313
                break;
314
        }
315
316
        return $entry;
317
    }
318
319
    /**
320
     * @param string $msgstr
321
     *
322
     * @return Headers
323
     */
324
    protected function parseHeaders($msgstr)
325
    {
326
        $rawHeaders = array_filter(explode('\\n', $msgstr));
327
        $headers = array();
328
329
        foreach ($rawHeaders as $rawHeader) {
330
            $tokens = explode(':', $rawHeader, 2);
331
            if (count($tokens) !== 2) {
332
                continue;
333
            }
334
335
            list($id, $value) = $tokens;
336
            $headers[] = new Header($id, trim($value));
337
        }
338
339
        return new Headers($headers);
340
    }
341
342
    /**
343
     * @param string $line
344
     * @param array  $entry
345
     *
346
     * @return bool
347
     */
348
    protected function shouldIgnoreLine($line, array $entry)
349
    {
350
        return empty($line) && count($entry) === 0;
351
    }
352
353
    /**
354
     * @param string $line
355
     * @param array  $entry
356
     *
357
     * @return bool
358
     */
359
    protected function shouldCloseEntry($line, array $entry)
360
    {
361
        $tokens = $this->getProperty($line);
362
        $property = $tokens[0];
363
364
        return ($line === '' || ($property === 'msgid' && isset($entry['msgid'])));
365
    }
366
367
    /**
368
     * @param string $value
369
     * @return string
370
     */
371
    protected function unquote($value)
372
    {
373
        return preg_replace('/^\"|\"$/', '', $value);
374
    }
375
376
    /**
377
     * Checks if entry is a header by
378
     *
379
     * @param array $entry
380
     *
381
     * @return bool
382
     */
383
    protected function isHeader(array $entry)
384
    {
385
        if (empty($entry) || !isset($entry['msgstr'])) {
386
            return false;
387
        }
388
389
        if (!isset($entry['msgid']) || !empty($entry['msgid'])) {
390
            return false;
391
        }
392
393
        $standardHeaders = array(
394
            'Project-Id-Version:',
395
            'Report-Msgid-Bugs-To:',
396
            'POT-Creation-Date:',
397
            'PO-Revision-Date:',
398
            'Last-Translator:',
399
            'Language-Team:',
400
            'MIME-Version:',
401
            'Content-Type:',
402
            'Content-Transfer-Encoding:',
403
            'Plural-Forms:',
404
        );
405
406
        $headers = explode('\n', $entry['msgstr']);
407
        // Remove text after double colon
408
        $headers = array_map(
409
            function ($header) {
410
                $pattern = '/(.*?:)(.*)/i';
411
                $replace = '${1}';
412
                return preg_replace($pattern, $replace, $header);
413
            },
414
            $headers
415
        );
416
417
        if (count(array_intersect($standardHeaders, $headers)) > 0) {
418
            return true;
419
        }
420
421
        // If it does not contain any of the standard headers
422
        // Let's see if it contains any custom header.
423
        $customHeaders = array_filter(
424
            $headers,
425
            function ($header) {
426
                return preg_match('/^X\-(.*):/i', $header) === 1;
427
            }
428
        );
429
430
        return count($customHeaders) > 0;
431
    }
432
433
    /**
434
     * @param string $line
435
     *
436
     * @return array
437
     */
438
    protected function getProperty($line)
439
    {
440
        $tokens = preg_split('/\s+/ ', $line, 2);
441
442
        return $tokens;
443
    }
444
}
445