Issues (44)

build/GeneratePhonePrefixData.php (2 issues)

Severity
1
<?php
2
3
namespace libphonenumber\buildtools;
4
5
use Symfony\Component\Console\Helper\ProgressBar;
6
use Symfony\Component\Console\Output\OutputInterface;
7
8
/**
9
 * Class GeneratePhonePrefixData
10
 * @package libphonenumber\buildtools
11
 * @internal
12
 */
13
class GeneratePhonePrefixData
14
{
15
    const NANPA_COUNTRY_CODE = 1;
16
    const DATA_FILE_EXTENSION = '.txt';
17
    const GENERATION_COMMENT = <<<'EOT'
18
/**
19
 * This file has been @generated by a phing task by {@link GeneratePhonePrefixData}.
20
 * See [README.md](README.md#generating-data) for more information.
21
 *
22
 * Pull requests changing data in these files will not be accepted. See the
23
 * [FAQ in the README](README.md#problems-with-invalid-numbers] on how to make
24
 * metadata changes.
25
 *
26
 * Do not modify this file directly!
27
 */
28
29
30
EOT;
31
32
    public $inputDir;
33
    private $filesToIgnore = array('.', '..', '.svn', '.git');
34
    private $outputDir;
35
    private $englishMaps = array();
36
    private $prefixesToExpand = array(
37
        861 => 5,
38
        12 => 2,
39
        13 => 2,
40
        14 => 2,
41
        15 => 2,
42
        16 => 2,
43
        17 => 2,
44
        18 => 2,
45
        19 => 2,
46
    );
47
48
49
    public function start($inputDir, $outputDir, OutputInterface $consoleOutput, $expandCountries)
50
    {
51
        $this->inputDir = $inputDir;
52
        $this->outputDir = $outputDir;
53
54
        $inputOutputMappings = $this->createInputOutputMappings($expandCountries);
55
        $availableDataFiles = array();
56
57
        $progress = new ProgressBar($consoleOutput, \count($inputOutputMappings));
58
59
        $progress->start();
60
        foreach ($inputOutputMappings as $textFile => $outputFiles) {
61
            $mappings = $this->readMappingsFromFile($textFile);
62
63
            $language = $this->getLanguageFromTextFile($textFile);
64
65
            $this->removeEmptyEnglishMappings($mappings, $language);
66
            $this->makeDataFallbackToEnglish($textFile, $mappings);
67
            $mappingForFiles = $this->splitMap($mappings, $outputFiles);
68
69
            foreach ($mappingForFiles as $outputFile => $value) {
70
                $this->writeMappingFile($language, $outputFile, $value);
71
                $this->addConfigurationMapping($availableDataFiles, $language, $outputFile);
72
            }
73
            $progress->advance();
74
        }
75
76
        $this->writeConfigMap($availableDataFiles);
77
        $progress->finish();
78
    }
79
80
    private function createInputOutputMappings($expandCountries)
81
    {
82
        $topLevel = \scandir($this->inputDir);
83
84
        $mappings = array();
85
86
        foreach ($topLevel as $languageDirectory) {
87
            if (\in_array($languageDirectory, $this->filesToIgnore)) {
88
                continue;
89
            }
90
91
            $fileLocation = $this->inputDir . DIRECTORY_SEPARATOR . $languageDirectory;
92
93
            if (\is_dir($fileLocation)) {
94
                // Will contain files
95
96
                $countryCodeFiles = \scandir($fileLocation);
97
98
                foreach ($countryCodeFiles as $countryCodeFileName) {
99
                    if (\in_array($countryCodeFileName, $this->filesToIgnore)) {
100
                        continue;
101
                    }
102
103
104
                    $outputFiles = $this->createOutputFileNames(
105
                        $countryCodeFileName,
106
                        $this->getCountryCodeFromTextFileName($countryCodeFileName),
107
                        $languageDirectory,
108
                        $expandCountries
109
                    );
110
111
                    $mappings[$languageDirectory . DIRECTORY_SEPARATOR . $countryCodeFileName] = $outputFiles;
112
                }
113
            }
114
        }
115
116
        return $mappings;
117
    }
118
119
    /**
120
     * Method used by {@code #createInputOutputMappings()} to generate the list of output binary files
121
     * from the provided input text file. For the data files expected to be large (currently only
122
     * NANPA is supported), this method generates a list containing one output file for each area
123
     * code. Otherwise, a single file is added to the list.
124
     * @param string $file
125
     * @param string $countryCode
126
     * @param string $language
127
     * @param bool $expandCountries
128
     * @return array
129
     */
130
    private function createOutputFileNames($file, $countryCode, $language, $expandCountries)
0 ignored issues
show
The parameter $file is not used and could be removed. ( Ignorable by Annotation )

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

130
    private function createOutputFileNames(/** @scrutinizer ignore-unused */ $file, $countryCode, $language, $expandCountries)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
131
    {
132
        $outputFiles = array();
133
134
        if ($expandCountries === false) {
135
            $outputFiles[] = $this->generateFilename($countryCode, $language);
136
            return $outputFiles;
137
        }
138
139
        /*
140
         * Reduce memory usage for China numbers
141
         * @see https://github.com/giggsey/libphonenumber-for-php/issues/44
142
         *
143
         * Analytics of the data suggests that the following prefixes need expanding:
144
         *  - 861 (to 5 chars)
145
         */
146
        $phonePrefixes = array();
147
        $prefixesToExpand = $this->prefixesToExpand;
148
149
        $this->parseTextFile(
150
            $this->getFilePathFromLanguageAndCountryCode($language, $countryCode),
151
            function ($prefix, $location) use (&$phonePrefixes, $prefixesToExpand, $countryCode) {
0 ignored issues
show
The parameter $location is not used and could be removed. ( Ignorable by Annotation )

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

151
            function ($prefix, /** @scrutinizer ignore-unused */ $location) use (&$phonePrefixes, $prefixesToExpand, $countryCode) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
152
                $length = \strlen($countryCode);
153
                foreach ($prefixesToExpand as $p => $l) {
154
                    if (GeneratePhonePrefixData::startsWith($prefix, $p)) {
155
                        // Allow later entries to overwrite initial ones
156
                        $length = $l;
157
                    }
158
                }
159
160
                $shortPrefix = \substr($prefix, 0, $length);
161
                if (!\in_array($shortPrefix, $phonePrefixes)) {
162
                    $phonePrefixes[] = $shortPrefix;
163
                }
164
            }
165
        );
166
167
        foreach ($phonePrefixes as $prefix) {
168
            $outputFiles[] = $this->generateFilename($prefix, $language);
169
        }
170
171
        return $outputFiles;
172
    }
173
174
    /**
175
     * Reads phone prefix data from the provides file path and invokes the given handler for each
176
     * mapping read.
177
     *
178
     * @param string $filePath
179
     * @param \Closure $handler
180
     * @return array
181
     * @throws \InvalidArgumentException
182
     */
183
    private function parseTextFile($filePath, \Closure $handler)
184
    {
185
        if (!\file_exists($filePath) || !\is_readable($filePath)) {
186
            throw new \InvalidArgumentException("File '{$filePath}' does not exist");
187
        }
188
189
        $data = \file($filePath);
190
191
        $countryData = array();
192
193
        foreach ($data as $line) {
194
            // Remove \n
195
            $line = \str_replace(array("\n", "\r"), '', $line);
196
            $line = \trim($line);
197
198
            if (\strlen($line) == 0 || \substr($line, 0, 1) == '#') {
199
                continue;
200
            }
201
            if (\strpos($line, '|')) {
202
                // Valid line
203
                $parts = \explode('|', $line);
204
205
206
                $prefix = $parts[0];
207
                $location = $parts[1];
208
209
                $handler($prefix, $location);
210
            }
211
        }
212
213
        return $countryData;
214
    }
215
216
    /**
217
     * @param string $language
218
     * @param string $code
219
     * @return string
220
     */
221
    private function getFilePathFromLanguageAndCountryCode($language, $code)
222
    {
223
        return $this->getFilePath($language . DIRECTORY_SEPARATOR . $code . self::DATA_FILE_EXTENSION);
224
    }
225
226
    /**
227
     * @param string $fileName
228
     * @return string
229
     */
230
    private function getFilePath($fileName)
231
    {
232
        $path = $this->inputDir . $fileName;
233
234
        return $path;
235
    }
236
237
    /**
238
     * @param string $prefix
239
     * @param string $language
240
     * @return string
241
     */
242
    private function generateFilename($prefix, $language)
243
    {
244
        return $language . DIRECTORY_SEPARATOR . $prefix . self::DATA_FILE_EXTENSION;
245
    }
246
247
    /**
248
     * @param string $countryCodeFileName
249
     * @return string
250
     */
251
    private function getCountryCodeFromTextFileName($countryCodeFileName)
252
    {
253
        return \str_replace(self::DATA_FILE_EXTENSION, '', $countryCodeFileName);
254
    }
255
256
    /**
257
     * @param string $inputFile
258
     * @return array
259
     */
260
    private function readMappingsFromFile($inputFile)
261
    {
262
        $areaCodeMap = array();
263
264
        $this->parseTextFile(
265
            $this->inputDir . $inputFile,
266
            function ($prefix, $location) use (&$areaCodeMap) {
267
                $areaCodeMap[$prefix] = $location;
268
            }
269
        );
270
271
        return $areaCodeMap;
272
    }
273
274
    /**
275
     * @param string $textFile
276
     * @return mixed
277
     */
278
    private function getLanguageFromTextFile($textFile)
279
    {
280
        $parts = \explode(DIRECTORY_SEPARATOR, $textFile);
281
282
        return $parts[0];
283
    }
284
285
    /**
286
     * @param array $mappings
287
     * @param string $language
288
     */
289
    private function removeEmptyEnglishMappings(&$mappings, $language)
290
    {
291
        if ($language != 'en') {
292
            return;
293
        }
294
295
        foreach ($mappings as $k => $v) {
296
            if ($v == '') {
297
                unset($mappings[$k]);
298
            }
299
        }
300
    }
301
302
    /**
303
     * Compress the provided mappings according to the English data file if any.
304
     * @param string $textFile
305
     * @param array $mappings
306
     */
307
    private function makeDataFallbackToEnglish($textFile, &$mappings)
308
    {
309
        $englishPath = $this->getEnglishDataPath($textFile);
310
311
        if ($textFile == $englishPath || !\file_exists($this->getFilePath($englishPath))) {
312
            return;
313
        }
314
315
        $countryCode = \substr($textFile, 3, 2);
316
317
        if (!\array_key_exists($countryCode, $this->englishMaps)) {
318
            $englishMap = $this->readMappingsFromFile($englishPath);
319
320
            $this->englishMaps[$countryCode] = $englishMap;
321
        }
322
323
        $this->compressAccordingToEnglishData($this->englishMaps[$countryCode], $mappings);
324
    }
325
326
    private function getEnglishDataPath($textFile)
327
    {
328
        return 'en' . DIRECTORY_SEPARATOR . \substr($textFile, 3);
329
    }
330
331
    private function compressAccordingToEnglishData($englishMap, &$nonEnglishMap)
332
    {
333
        foreach ($nonEnglishMap as $prefix => $value) {
334
            if (\array_key_exists($prefix, $englishMap)) {
335
                $englishDescription = $englishMap[$prefix];
336
                if ($englishDescription == $value) {
337
                    if (!$this->hasOverlappingPrefix($prefix, $nonEnglishMap)) {
338
                        unset($nonEnglishMap[$prefix]);
339
                    } else {
340
                        $nonEnglishMap[$prefix] = '';
341
                    }
342
                }
343
            }
344
        }
345
    }
346
347
    private function hasOverlappingPrefix($number, $mappings)
348
    {
349
        while (\strlen($number) > 0) {
350
            $number = \substr($number, 0, -1);
351
352
            if (\array_key_exists($number, $mappings)) {
353
                return true;
354
            }
355
        }
356
357
        return false;
358
    }
359
360
    private function splitMap($mappings, $outputFiles)
361
    {
362
        $mappingForFiles = array();
363
364
        foreach ($mappings as $prefix => $location) {
365
            $targetFile = null;
366
367
            foreach ($outputFiles as $k => $outputFile) {
368
                $outputFilePrefix = $this->getPhonePrefixLanguagePairFromFilename($outputFile)->prefix;
369
                if (self::startsWith($prefix, $outputFilePrefix)) {
370
                    $targetFile = $outputFilePrefix;
371
                    break;
372
                }
373
            }
374
375
            if (!\array_key_exists($targetFile, $mappingForFiles)) {
376
                $mappingForFiles[$targetFile] = array();
377
            }
378
            $mappingForFiles[$targetFile][$prefix] = $location;
379
        }
380
381
        return $mappingForFiles;
382
    }
383
384
    /**
385
     * Extracts the phone prefix and the language code contained in the provided file name.
386
     */
387
    private function getPhonePrefixLanguagePairFromFilename($outputFile)
388
    {
389
        $parts = \explode(DIRECTORY_SEPARATOR, $outputFile);
390
391
        $returnObj = new \stdClass();
392
        $returnObj->language = $parts[0];
393
394
        $returnObj->prefix = $this->getCountryCodeFromTextFileName($parts[1]);
395
396
        return $returnObj;
397
    }
398
399
    /**
400
     *
401
     * @link http://stackoverflow.com/a/834355/403165
402
     * @param $haystack
403
     * @param $needle
404
     * @return bool
405
     */
406
    private static function startsWith($haystack, $needle)
407
    {
408
        return !\strncmp($haystack, $needle, \strlen($needle));
409
    }
410
411
    private function writeMappingFile($language, $outputFile, $data)
412
    {
413
        if (!\file_exists($this->outputDir . $language)) {
414
            \mkdir($this->outputDir . $language);
415
        }
416
417
        $phpSource = '<?php' . PHP_EOL
418
            . self::GENERATION_COMMENT
419
            . 'return ' . \var_export($data, true) . ';'
420
            . PHP_EOL;
421
422
        $outputPath = $this->outputDir . $language . DIRECTORY_SEPARATOR . $outputFile . '.php';
423
424
        \file_put_contents($outputPath, $phpSource);
425
    }
426
427
    public function addConfigurationMapping(&$availableDataFiles, $language, $prefix)
428
    {
429
        if (!\array_key_exists($language, $availableDataFiles)) {
430
            $availableDataFiles[$language] = array();
431
        }
432
433
        $availableDataFiles[$language][] = $prefix;
434
    }
435
436
    private function writeConfigMap($availableDataFiles)
437
    {
438
        $phpSource = '<?php' . PHP_EOL
439
            . self::GENERATION_COMMENT
440
            . 'return ' . \var_export($availableDataFiles, true) . ';'
441
            . PHP_EOL;
442
443
        $outputPath = $this->outputDir . 'Map.php';
444
445
        \file_put_contents($outputPath, $phpSource);
446
    }
447
}
448