feat: add postcodes:import command for loading ONSPD CSV
This commit is contained in:
104
app/Console/Commands/ImportPostcodes.php
Normal file
104
app/Console/Commands/ImportPostcodes.php
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Attributes\Description;
|
||||||
|
use Illuminate\Console\Attributes\Signature;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
#[Signature('postcodes:import {--file= : Path to ONSPD CSV file}')]
|
||||||
|
#[Description('Import UK postcodes (ONSPD) into the local postcodes and outcodes tables')]
|
||||||
|
final class ImportPostcodes extends Command
|
||||||
|
{
|
||||||
|
private const int CHUNK_SIZE = 1000;
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$file = $this->option('file');
|
||||||
|
|
||||||
|
if ($file === null || ! is_readable($file)) {
|
||||||
|
$this->error('--file is required and must be a readable path to an ONSPD CSV.');
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$handle = fopen($file, 'r');
|
||||||
|
|
||||||
|
if ($handle === false) {
|
||||||
|
throw new RuntimeException("Unable to open {$file}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$header = fgetcsv($handle);
|
||||||
|
|
||||||
|
if ($header === false) {
|
||||||
|
$this->error('CSV is empty.');
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$columns = array_change_key_case(array_flip($header), CASE_LOWER);
|
||||||
|
|
||||||
|
foreach (['pcd', 'lat', 'long'] as $required) {
|
||||||
|
if (! isset($columns[$required])) {
|
||||||
|
$this->error("Missing required column '{$required}'.");
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasDoterm = isset($columns['doterm']);
|
||||||
|
|
||||||
|
DB::table('postcodes')->truncate();
|
||||||
|
DB::table('outcodes')->truncate();
|
||||||
|
|
||||||
|
$buffer = [];
|
||||||
|
$imported = 0;
|
||||||
|
|
||||||
|
while (($row = fgetcsv($handle)) !== false) {
|
||||||
|
if ($hasDoterm && trim((string) ($row[$columns['doterm']] ?? '')) !== '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lat = trim((string) ($row[$columns['lat']] ?? ''));
|
||||||
|
$lng = trim((string) ($row[$columns['long']] ?? ''));
|
||||||
|
|
||||||
|
if ($lat === '' || $lng === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pcd = strtoupper(preg_replace('/\s+/', '', (string) $row[$columns['pcd']]));
|
||||||
|
|
||||||
|
if ($pcd === '' || strlen($pcd) < 5) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$buffer[] = [
|
||||||
|
'postcode' => $pcd,
|
||||||
|
'outcode' => substr($pcd, 0, strlen($pcd) - 3),
|
||||||
|
'lat' => (float) $lat,
|
||||||
|
'lng' => (float) $lng,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (count($buffer) >= self::CHUNK_SIZE) {
|
||||||
|
DB::table('postcodes')->insert($buffer);
|
||||||
|
$imported += count($buffer);
|
||||||
|
$buffer = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($buffer !== []) {
|
||||||
|
DB::table('postcodes')->insert($buffer);
|
||||||
|
$imported += count($buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
$this->info("Imported {$imported} postcodes.");
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
tests/Feature/Console/ImportPostcodesTest.php
Normal file
31
tests/Feature/Console/ImportPostcodesTest.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Postcode;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
function writeOnspdFixture(string $contents): string
|
||||||
|
{
|
||||||
|
$path = tempnam(sys_get_temp_dir(), 'onspd_').'.csv';
|
||||||
|
file_put_contents($path, $contents);
|
||||||
|
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('imports active postcodes from an ONSPD CSV', function (): void {
|
||||||
|
$csv = <<<'CSV'
|
||||||
|
pcd,pcds,doterm,lat,long
|
||||||
|
"SW1A1AA","SW1A 1AA","",51.501009,-0.141588
|
||||||
|
"M11AD","M1 1AD",,53.480957,-2.237428
|
||||||
|
CSV;
|
||||||
|
|
||||||
|
$path = writeOnspdFixture($csv);
|
||||||
|
|
||||||
|
$this->artisan('postcodes:import', ['--file' => $path])
|
||||||
|
->assertSuccessful();
|
||||||
|
|
||||||
|
expect(Postcode::count())->toBe(2)
|
||||||
|
->and(Postcode::find('SW1A1AA')->outcode)->toBe('SW1A')
|
||||||
|
->and(Postcode::find('M11AD')->outcode)->toBe('M1');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user