Skip to content

Commit 66bb6fd

Browse files
committed
Refactor architecture with interfaces, abstract classes, and tests
- Add ConverterInterface contract for converter implementations - Add GeonamesException with factory methods for domain-specific errors - Extract common logic into AbstractConverter and AbstractGazetteerConverter - Extract common downloader logic into AbstractDownloader - Simplify converter classes by inheriting from abstract base classes - Add comprehensive test coverage for converters and console commands - Update phpstan configuration for new classes
1 parent ff0f586 commit 66bb6fd

20 files changed

Lines changed: 2438 additions & 888 deletions

phpstan.neon.dist

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ parameters:
55
level: 4
66
paths:
77
- src
8+
excludePaths:
9+
# MongoDB converters are excluded because MongoDB is an optional dependency
10+
- src/Converter/MongoDBPostalCodeConverter.php
11+
- src/Converter/MongoDBGazetteerConverter.php
812
tmpDir: build/phpstan
9-
checkMissingIterableValueType: false
10-
treatPhpDocTypesAsCertain: false
13+
treatPhpDocTypesAsCertain: false

src/Console/Commands/DownloadGazetteerCommand.php

Lines changed: 192 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,68 @@
1313
use Symfony\Component\Console\Input\InputOption;
1414
use Symfony\Component\Console\Output\OutputInterface;
1515

16+
/**
17+
* Console command for downloading and converting GeoNames gazetteer data.
18+
*
19+
* This command downloads geographical feature data from the GeoNames database
20+
* and converts it to either JSON format or imports it directly into MongoDB.
21+
*
22+
* Feature classes available for filtering:
23+
* A - Administrative boundaries
24+
* H - Hydrographic features (streams, lakes)
25+
* L - Parks, areas
26+
* P - Populated places (cities, villages)
27+
* R - Roads, railroads
28+
* S - Spots, buildings, farms
29+
* T - Mountains, hills, rocks
30+
* U - Undersea features
31+
* V - Forest, heath
32+
*
33+
* Usage examples:
34+
* geonames:gazetteer:download TH # Download Thailand data
35+
* geonames:gazetteer:download all -c P # All populated places
36+
* geonames:gazetteer:download US -f mongodb # Import US data to MongoDB
37+
*/
1638
class DownloadGazetteerCommand extends Command
1739
{
40+
/**
41+
* Admin code files to clean up after processing.
42+
*/
43+
private const ADMIN_CODE_FILES = [
44+
'admin1CodesASCII.txt',
45+
'admin2Codes.txt',
46+
];
47+
48+
/**
49+
* The default command name.
50+
*
51+
* @var string
52+
*/
1853
protected static $defaultName = 'geonames:gazetteer:download';
1954

55+
/**
56+
* The default command description.
57+
*
58+
* @var string
59+
*/
2060
protected static $defaultDescription = 'Download and convert Geonames Gazetteer data';
2161

62+
/**
63+
* The gazetteer downloader instance.
64+
*/
2265
private GazetteerDownloader $downloader;
2366

67+
/**
68+
* The gazetteer converter instance.
69+
*/
2470
private GazetteerConverter $converter;
2571

72+
/**
73+
* Create a new download gazetteer command instance.
74+
*
75+
* @param GazetteerDownloader|null $downloader Optional downloader instance for testing
76+
* @param GazetteerConverter|null $converter Optional converter instance for testing
77+
*/
2678
public function __construct(?GazetteerDownloader $downloader = null, ?GazetteerConverter $converter = null)
2779
{
2880
parent::__construct();
@@ -31,6 +83,9 @@ public function __construct(?GazetteerDownloader $downloader = null, ?GazetteerC
3183
$this->converter = $converter ?? new GazetteerConverter;
3284
}
3385

86+
/**
87+
* Configure the command options and arguments.
88+
*/
3489
protected function configure(): void
3590
{
3691
$this
@@ -43,88 +98,171 @@ protected function configure(): void
4398
->addOption('mongodb-collection', null, InputOption::VALUE_REQUIRED, 'MongoDB collection name', 'gazetteer');
4499
}
45100

101+
/**
102+
* Execute the command to download and convert gazetteer data.
103+
*
104+
* @param InputInterface $input The console input interface
105+
* @param OutputInterface $output The console output interface
106+
* @return int The command exit code (SUCCESS or FAILURE)
107+
*/
46108
protected function execute(InputInterface $input, OutputInterface $output): int
47109
{
48110
$country = $input->getArgument('country') ?? 'all';
49111
$outputDir = $input->getOption('output');
50112
$format = $input->getOption('format');
51113

52-
// Create output directory if it doesn't exist
53-
if (! is_dir($outputDir)) {
54-
mkdir($outputDir, 0777, true);
55-
}
114+
$this->ensureOutputDirectoryExists($outputDir);
56115

57-
// Set output for progress bars
58116
$this->downloader->setOutput($output);
59117
$this->converter->setOutput($output);
60118

61119
$output->writeln('<info>Downloading Gazetteer data...</info>');
62120

63-
// Download the data
121+
$zipFile = $this->downloadData($country, $outputDir);
122+
123+
return $this->processData($input, $output, $zipFile, $format, $outputDir);
124+
}
125+
126+
/**
127+
* Ensure the output directory exists, creating it if necessary.
128+
*
129+
* @param string $outputDir The output directory path
130+
*/
131+
private function ensureOutputDirectoryExists(string $outputDir): void
132+
{
133+
if (! is_dir($outputDir)) {
134+
mkdir($outputDir, 0777, true);
135+
}
136+
}
137+
138+
/**
139+
* Download gazetteer data for the specified country.
140+
*
141+
* @param string $country The country code or 'all'
142+
* @param string $outputDir The output directory path
143+
* @return string The path to the downloaded ZIP file
144+
*/
145+
private function downloadData(string $country, string $outputDir): string
146+
{
64147
if ($country === 'all') {
65148
$this->downloader->downloadAll($outputDir);
66-
$zipFile = $outputDir.'/allCountries.zip';
67-
} else {
68-
$this->downloader->download($country, $outputDir);
69-
$zipFile = $outputDir.'/'.strtoupper($country).'.zip';
149+
150+
return $outputDir.'/allCountries.zip';
70151
}
71152

153+
$this->downloader->download($country, $outputDir);
154+
155+
return $outputDir.'/'.strtoupper($country).'.zip';
156+
}
157+
158+
/**
159+
* Process the downloaded data based on the output format.
160+
*
161+
* @param InputInterface $input The console input interface
162+
* @param OutputInterface $output The console output interface
163+
* @param string $zipFile The path to the ZIP file
164+
* @param string $format The output format (json or mongodb)
165+
* @param string $outputDir The output directory path
166+
* @return int The command exit code
167+
*/
168+
private function processData(
169+
InputInterface $input,
170+
OutputInterface $output,
171+
string $zipFile,
172+
string $format,
173+
string $outputDir
174+
): int {
72175
if ($format === 'json') {
73-
$output->writeln('<info>Converting to JSON format...</info>');
74-
$jsonFile = str_replace('.zip', '.json', $zipFile);
75-
$this->converter->convert($zipFile, $jsonFile, $outputDir);
176+
return $this->convertToJson($output, $zipFile, $outputDir);
177+
}
76178

77-
// Remove ZIP file after conversion
78-
unlink($zipFile);
179+
if ($format === 'mongodb') {
180+
return $this->importToMongoDB($input, $output, $zipFile, $outputDir);
181+
}
79182

80-
// Remove admin code files
81-
if (file_exists($outputDir.'/admin1CodesASCII.txt')) {
82-
unlink($outputDir.'/admin1CodesASCII.txt');
83-
}
84-
if (file_exists($outputDir.'/admin2Codes.txt')) {
85-
unlink($outputDir.'/admin2Codes.txt');
86-
}
183+
$output->writeln(sprintf('<error>Unsupported format: %s</error>', $format));
87184

88-
$output->writeln('<info>Data has been downloaded and converted successfully!</info>');
89-
$output->writeln(sprintf('<info>Output file: %s</info>', $jsonFile));
90-
} elseif ($format === 'mongodb') {
91-
$output->writeln('<info>Converting to MongoDB format...</info>');
185+
return Command::FAILURE;
186+
}
92187

93-
// Create MongoDB converter
94-
$mongodbUri = $input->getOption('mongodb-uri');
95-
$mongodbDb = $input->getOption('mongodb-db');
96-
$mongodbCollection = $input->getOption('mongodb-collection');
188+
/**
189+
* Convert the gazetteer data to JSON format.
190+
*
191+
* @param OutputInterface $output The console output interface
192+
* @param string $zipFile The path to the ZIP file
193+
* @param string $outputDir The output directory path
194+
* @return int The command exit code
195+
*/
196+
private function convertToJson(OutputInterface $output, string $zipFile, string $outputDir): int
197+
{
198+
$output->writeln('<info>Converting to JSON format...</info>');
199+
$jsonFile = str_replace('.zip', '.json', $zipFile);
200+
$this->converter->convertWithAdminCodes($zipFile, $jsonFile, $outputDir);
97201

98-
$mongoConverter = new MongoDBGazetteerConverter(
99-
$mongodbUri,
100-
$mongodbDb,
101-
$mongodbCollection
102-
);
103-
$mongoConverter->setOutput($output);
202+
$this->cleanup($zipFile, $outputDir);
104203

105-
// Convert and import to MongoDB
106-
$jsonFile = str_replace('.zip', '.json', $zipFile); // Dummy file name, not used
107-
$mongoConverter->convert($zipFile, $jsonFile, $outputDir);
204+
$output->writeln('<info>Data has been downloaded and converted successfully!</info>');
205+
$output->writeln(sprintf('<info>Output file: %s</info>', $jsonFile));
108206

109-
// Remove ZIP file after conversion
110-
unlink($zipFile);
207+
return Command::SUCCESS;
208+
}
111209

112-
// Remove admin code files
113-
if (file_exists($outputDir.'/admin1CodesASCII.txt')) {
114-
unlink($outputDir.'/admin1CodesASCII.txt');
115-
}
116-
if (file_exists($outputDir.'/admin2Codes.txt')) {
117-
unlink($outputDir.'/admin2Codes.txt');
118-
}
210+
/**
211+
* Import the gazetteer data to MongoDB.
212+
*
213+
* @param InputInterface $input The console input interface
214+
* @param OutputInterface $output The console output interface
215+
* @param string $zipFile The path to the ZIP file
216+
* @param string $outputDir The output directory path
217+
* @return int The command exit code
218+
*/
219+
private function importToMongoDB(
220+
InputInterface $input,
221+
OutputInterface $output,
222+
string $zipFile,
223+
string $outputDir
224+
): int {
225+
$output->writeln('<info>Converting to MongoDB format...</info>');
119226

120-
$output->writeln('<info>Data has been downloaded and imported to MongoDB successfully!</info>');
121-
$output->writeln(sprintf('<info>MongoDB: %s.%s</info>', $mongodbDb, $mongodbCollection));
122-
} else {
123-
$output->writeln(sprintf('<error>Unsupported format: %s</error>', $format));
227+
$mongodbUri = $input->getOption('mongodb-uri');
228+
$mongodbDb = $input->getOption('mongodb-db');
229+
$mongodbCollection = $input->getOption('mongodb-collection');
124230

125-
return Command::FAILURE;
126-
}
231+
$mongoConverter = new MongoDBGazetteerConverter(
232+
$mongodbUri,
233+
$mongodbDb,
234+
$mongodbCollection
235+
);
236+
$mongoConverter->setOutput($output);
237+
238+
$dummyOutputFile = str_replace('.zip', '.json', $zipFile);
239+
$mongoConverter->convertWithAdminCodes($zipFile, $dummyOutputFile, $outputDir);
240+
241+
$this->cleanup($zipFile, $outputDir);
242+
243+
$output->writeln('<info>Data has been downloaded and imported to MongoDB successfully!</info>');
244+
$output->writeln(sprintf('<info>MongoDB: %s.%s</info>', $mongodbDb, $mongodbCollection));
127245

128246
return Command::SUCCESS;
129247
}
248+
249+
/**
250+
* Clean up temporary files after processing.
251+
*
252+
* @param string $zipFile The ZIP file to remove
253+
* @param string $outputDir The output directory containing admin code files
254+
*/
255+
private function cleanup(string $zipFile, string $outputDir): void
256+
{
257+
if (file_exists($zipFile)) {
258+
unlink($zipFile);
259+
}
260+
261+
foreach (self::ADMIN_CODE_FILES as $file) {
262+
$filePath = $outputDir.'/'.$file;
263+
if (file_exists($filePath)) {
264+
unlink($filePath);
265+
}
266+
}
267+
}
130268
}

0 commit comments

Comments
 (0)