407 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			407 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| 
 | |
| namespace libphonenumber\buildtools;
 | |
| 
 | |
| use Symfony\Component\Console\Helper\ProgressHelper;
 | |
| use Symfony\Component\Console\Output\OutputInterface;
 | |
| 
 | |
| class GeneratePhonePrefixData
 | |
| {
 | |
|     const NANPA_COUNTRY_CODE = 1;
 | |
|     const DATA_FILE_EXTENSION = '.txt';
 | |
|     const GENERATION_COMMENT = <<<'EOT'
 | |
| /**
 | |
|  * This file is automatically @generated by {@link GeneratePhonePrefixData}.
 | |
|  * Please don't modify it directly.
 | |
|  */
 | |
| 
 | |
| 
 | |
| EOT;
 | |
| 
 | |
|     public $inputDir;
 | |
|     private $filesToIgnore = array('.', '..', '.svn', '.git');
 | |
|     private $outputDir;
 | |
|     private $englishMaps = array();
 | |
|     /**
 | |
|      * @var OutputInterface
 | |
|      */
 | |
|     private static $consoleOutput;
 | |
| 
 | |
|     public function start($inputDir, $outputDir, OutputInterface $consoleOutput, ProgressHelper $progress)
 | |
|     {
 | |
|         $this->inputDir = $inputDir;
 | |
|         $this->outputDir = $outputDir;
 | |
|         self::$consoleOutput = $consoleOutput;
 | |
| 
 | |
|         $inputOutputMappings = $this->createInputOutputMappings();
 | |
|         $availableDataFiles = array();
 | |
| 
 | |
|         $progress->start($consoleOutput, count($inputOutputMappings));
 | |
|         foreach ($inputOutputMappings as $textFile => $outputFiles) {
 | |
|             $mappings = $this->readMappingsFromFile($textFile);
 | |
| 
 | |
|             $language = $this->getLanguageFromTextFile($textFile);
 | |
| 
 | |
|             $this->removeEmptyEnglishMappings($mappings, $language);
 | |
|             $this->makeDataFallbackToEnglish($textFile, $mappings);
 | |
|             $mappingForFiles = $this->splitMap($mappings, $outputFiles);
 | |
| 
 | |
|             foreach ($mappingForFiles as $outputFile => $value) {
 | |
|                 $this->writeMappingFile($language, $outputFile, $value);
 | |
|                 $this->addConfigurationMapping($availableDataFiles, $language, $outputFile);
 | |
|             }
 | |
|             $progress->advance();
 | |
|         }
 | |
| 
 | |
|         $this->writeConfigMap($availableDataFiles);
 | |
|         $progress->finish();
 | |
|     }
 | |
| 
 | |
|     private function createInputOutputMappings()
 | |
|     {
 | |
|         $topLevel = scandir($this->inputDir);
 | |
| 
 | |
|         $mappings = array();
 | |
| 
 | |
|         foreach ($topLevel as $languageDirectory) {
 | |
|             if (in_array($languageDirectory, $this->filesToIgnore)) {
 | |
|                 continue;
 | |
|             }
 | |
| 
 | |
|             $fileLocation = $this->inputDir . DIRECTORY_SEPARATOR . $languageDirectory;
 | |
| 
 | |
|             if (is_dir($fileLocation)) {
 | |
|                 // Will contain files
 | |
| 
 | |
|                 $countryCodeFiles = scandir($fileLocation);
 | |
| 
 | |
|                 foreach ($countryCodeFiles as $countryCodeFileName) {
 | |
|                     if (in_array($countryCodeFileName, $this->filesToIgnore)) {
 | |
|                         continue;
 | |
|                     }
 | |
| 
 | |
| 
 | |
|                     $outputFiles = $this->createOutputFileNames(
 | |
|                         $countryCodeFileName,
 | |
|                         $this->getCountryCodeFromTextFileName($countryCodeFileName),
 | |
|                         $languageDirectory
 | |
|                     );
 | |
| 
 | |
|                     $mappings[$languageDirectory . DIRECTORY_SEPARATOR . $countryCodeFileName] = $outputFiles;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return $mappings;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Method used by {@code #createInputOutputMappings()} to generate the list of output binary files
 | |
|      * from the provided input text file. For the data files expected to be large (currently only
 | |
|      * NANPA is supported), this method generates a list containing one output file for each area
 | |
|      * code. Otherwise, a single file is added to the list.
 | |
|      */
 | |
| 
 | |
|     private function createOutputFileNames($file, $countryCode, $language)
 | |
|     {
 | |
|         $outputFiles = array();
 | |
| 
 | |
|         if ($countryCode == self::NANPA_COUNTRY_CODE) {
 | |
|             // Fetch the 4-digit prefixes stored in the file.
 | |
|             $phonePrefixes = array();
 | |
| 
 | |
|             $this->parseTextFile(
 | |
|                 $this->getFilePathFromLanguageAndCountryCode($language, $countryCode),
 | |
|                 function ($prefix, $location) use (&$phonePrefixes) {
 | |
|                     $shortPrefix = substr($prefix, 0, 4);
 | |
|                     if (!in_array($shortPrefix, $phonePrefixes)) {
 | |
|                         $phonePrefixes[] = $shortPrefix;
 | |
|                     }
 | |
|                 }
 | |
|             );
 | |
| 
 | |
|             foreach ($phonePrefixes as $prefix) {
 | |
|                 $outputFiles[] = $this->generateFilename($prefix, $language);
 | |
|             }
 | |
|         } elseif ($countryCode == 86) {
 | |
| 
 | |
|             /*
 | |
|              * Reduce memory usage for China numbers
 | |
|              * @see https://github.com/giggsey/libphonenumber-for-php/issues/44
 | |
|              */
 | |
| 
 | |
|             // Fetch the 5-digit prefixes stored in the file.
 | |
|             $phonePrefixes = array();
 | |
| 
 | |
|             $this->parseTextFile(
 | |
|                 $this->getFilePathFromLanguageAndCountryCode($language, $countryCode),
 | |
|                 function ($prefix, $location) use (&$phonePrefixes) {
 | |
|                     $shortPrefix = substr($prefix, 0, 5);
 | |
|                     if (!in_array($shortPrefix, $phonePrefixes)) {
 | |
|                         $phonePrefixes[] = $shortPrefix;
 | |
|                     }
 | |
|                 }
 | |
|             );
 | |
| 
 | |
|             foreach ($phonePrefixes as $prefix) {
 | |
|                 $outputFiles[] = $this->generateFilename($prefix, $language);
 | |
|             }
 | |
|         } else {
 | |
|             $outputFiles[] = $this->generateFilename($countryCode, $language);
 | |
|         }
 | |
| 
 | |
|         return $outputFiles;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Reads phone prefix data from the provides file path and invokes the given handler for each
 | |
|      * mapping read.
 | |
|      *
 | |
|      * @param $filePath
 | |
|      * @param $handler
 | |
|      * @return array
 | |
|      * @throws \InvalidArgumentException
 | |
|      */
 | |
|     private function parseTextFile($filePath, \Closure $handler)
 | |
|     {
 | |
| 
 | |
|         if (!file_exists($filePath) || !is_readable($filePath)) {
 | |
|             throw new \InvalidArgumentException("File '{$filePath}' does not exist");
 | |
|         }
 | |
| 
 | |
|         $data = file($filePath);
 | |
| 
 | |
|         $countryData = array();
 | |
| 
 | |
|         foreach ($data as $line) {
 | |
|             // Remove \n
 | |
|             $line = str_replace("\n", "", $line);
 | |
|             $line = str_replace("\r", "", $line);
 | |
|             $line = trim($line);
 | |
| 
 | |
|             if (strlen($line) == 0 || substr($line, 0, 1) == '#') {
 | |
|                 continue;
 | |
|             }
 | |
|             if (strpos($line, '|')) {
 | |
|                 // Valid line
 | |
|                 $parts = explode('|', $line);
 | |
| 
 | |
| 
 | |
|                 $prefix = $parts[0];
 | |
|                 $location = $parts[1];
 | |
| 
 | |
|                 $handler($prefix, $location);
 | |
|             }
 | |
| 
 | |
|         }
 | |
| 
 | |
|         return $countryData;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     private function getFilePathFromLanguageAndCountryCode($language, $code)
 | |
|     {
 | |
|         return $this->getFilePath($language . DIRECTORY_SEPARATOR . $code . self::DATA_FILE_EXTENSION);
 | |
|     }
 | |
| 
 | |
|     private function getFilePath($fileName)
 | |
|     {
 | |
|         $path = $this->inputDir . $fileName;
 | |
| 
 | |
|         return $path;
 | |
|     }
 | |
| 
 | |
|     private function generateFilename($prefix, $language)
 | |
|     {
 | |
|         return $language . DIRECTORY_SEPARATOR . $prefix . self::DATA_FILE_EXTENSION;
 | |
|     }
 | |
| 
 | |
|     private function getCountryCodeFromTextFileName($countryCodeFileName)
 | |
|     {
 | |
|         return str_replace(self::DATA_FILE_EXTENSION, '', $countryCodeFileName);
 | |
|     }
 | |
| 
 | |
|     private function readMappingsFromFile($inputFile)
 | |
|     {
 | |
|         $areaCodeMap = array();
 | |
| 
 | |
|         $this->parseTextFile(
 | |
|             $this->inputDir . $inputFile,
 | |
|             function ($prefix, $location) use (&$areaCodeMap) {
 | |
|                 $areaCodeMap[$prefix] = $location;
 | |
|             }
 | |
|         );
 | |
| 
 | |
|         return $areaCodeMap;
 | |
|     }
 | |
| 
 | |
|     private function getLanguageFromTextFile($textFile)
 | |
|     {
 | |
|         $parts = explode(DIRECTORY_SEPARATOR, $textFile);
 | |
| 
 | |
|         return $parts[0];
 | |
|     }
 | |
| 
 | |
|     private function removeEmptyEnglishMappings(&$mappings, $language)
 | |
|     {
 | |
|         if ($language != "en") {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         foreach ($mappings as $k => $v) {
 | |
|             if ($v == "") {
 | |
|                 unset($mappings[$k]);
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Compress the provided mappings according to the English data file if any.
 | |
|      * @param string $textFile
 | |
|      * @param array $mappings
 | |
|      */
 | |
|     private function makeDataFallbackToEnglish($textFile, &$mappings)
 | |
|     {
 | |
|         $englishPath = $this->getEnglishDataPath($textFile);
 | |
| 
 | |
|         if ($textFile == $englishPath || !file_exists($this->getFilePath($englishPath))) {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         $countryCode = substr($textFile, 3, 2);
 | |
| 
 | |
|         if (!array_key_exists($countryCode, $this->englishMaps)) {
 | |
|             $englishMap = $this->readMappingsFromFile($englishPath);
 | |
| 
 | |
|             $this->englishMaps[$countryCode] = $englishMap;
 | |
|         }
 | |
| 
 | |
|         $this->compressAccordingToEnglishData($this->englishMaps[$countryCode], $mappings);
 | |
|     }
 | |
| 
 | |
|     private function getEnglishDataPath($textFile)
 | |
|     {
 | |
|         return "en" . DIRECTORY_SEPARATOR . substr($textFile, 3, 2) . self::DATA_FILE_EXTENSION;
 | |
|     }
 | |
| 
 | |
|     private function compressAccordingToEnglishData($englishMap, &$nonEnglishMap)
 | |
|     {
 | |
|         foreach ($nonEnglishMap as $prefix => $value) {
 | |
| 
 | |
|             if (array_key_exists($prefix, $englishMap)) {
 | |
|                 $englishDescription = $englishMap[$prefix];
 | |
|                 if ($englishDescription == $value) {
 | |
|                     if (!$this->hasOverlappingPrefix($prefix, $nonEnglishMap)) {
 | |
|                         unset($nonEnglishMap[$prefix]);
 | |
|                     } else {
 | |
|                         $nonEnglishMap[$prefix] = "";
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     private function hasOverlappingPrefix($number, $mappings)
 | |
|     {
 | |
|         while (strlen($number) > 0) {
 | |
|             $number = substr($number, 0, -1);
 | |
| 
 | |
|             if (array_key_exists($number, $mappings)) {
 | |
|                 return true;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     private function splitMap($mappings, $outputFiles)
 | |
|     {
 | |
|         $mappingForFiles = array();
 | |
| 
 | |
|         foreach ($mappings as $prefix => $location) {
 | |
|             $targetFile = null;
 | |
| 
 | |
|             foreach ($outputFiles as $k => $outputFile) {
 | |
|                 $outputFilePrefix = $this->getPhonePrefixLanguagePairFromFilename($outputFile)->prefix;
 | |
|                 if (self::startsWith($prefix, $outputFilePrefix)) {
 | |
|                     $targetFile = $outputFilePrefix;
 | |
|                     break;
 | |
|                 }
 | |
|             }
 | |
| 
 | |
| 
 | |
|             if (!array_key_exists($targetFile, $mappingForFiles)) {
 | |
|                 $mappingForFiles[$targetFile] = array();
 | |
|             }
 | |
|             $mappingForFiles[$targetFile][$prefix] = $location;
 | |
|         }
 | |
| 
 | |
|         return $mappingForFiles;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Extracts the phone prefix and the language code contained in the provided file name.
 | |
|      */
 | |
|     private function getPhonePrefixLanguagePairFromFilename($outputFile)
 | |
|     {
 | |
|         $parts = explode(DIRECTORY_SEPARATOR, $outputFile);
 | |
| 
 | |
|         $returnObj = new \stdClass();
 | |
|         $returnObj->language = $parts[0];
 | |
| 
 | |
|         $returnObj->prefix = $this->getCountryCodeFromTextFileName($parts[1]);
 | |
| 
 | |
|         return $returnObj;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      *
 | |
|      * @link http://stackoverflow.com/a/834355/403165
 | |
|      * @param $haystack
 | |
|      * @param $needle
 | |
|      * @return bool
 | |
|      */
 | |
|     private static function startsWith($haystack, $needle)
 | |
|     {
 | |
|         return !strncmp($haystack, $needle, strlen($needle));
 | |
|     }
 | |
| 
 | |
|     private function writeMappingFile($language, $outputFile, $data)
 | |
|     {
 | |
| 
 | |
|         if (!file_exists($this->outputDir . $language)) {
 | |
|             mkdir($this->outputDir . $language);
 | |
|         }
 | |
| 
 | |
|         $phpSource = '<?php' . PHP_EOL
 | |
|             . self::GENERATION_COMMENT
 | |
|             . 'return ' . var_export($data, true) . ';'
 | |
|             . PHP_EOL;
 | |
| 
 | |
|         $outputPath = $this->outputDir . $language . DIRECTORY_SEPARATOR . $outputFile . '.php';
 | |
| 
 | |
|         file_put_contents($outputPath, $phpSource);
 | |
|     }
 | |
| 
 | |
|     public function addConfigurationMapping(&$availableDataFiles, $language, $prefix)
 | |
|     {
 | |
|         if (!array_key_exists($language, $availableDataFiles)) {
 | |
|             $availableDataFiles[$language] = array();
 | |
|         }
 | |
| 
 | |
|         $availableDataFiles[$language][] = $prefix;
 | |
|     }
 | |
| 
 | |
|     private function writeConfigMap($availableDataFiles)
 | |
|     {
 | |
|         $phpSource = '<?php' . PHP_EOL
 | |
|             . self::GENERATION_COMMENT
 | |
|             . 'return ' . var_export($availableDataFiles, true) . ';'
 | |
|             . PHP_EOL;
 | |
| 
 | |
|         $outputPath = $this->outputDir . 'Map.php';
 | |
| 
 | |
|         file_put_contents($outputPath, $phpSource);
 | |
|     }
 | |
| }
 | 
