First commit
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.DS_Store
|
||||||
|
.vscode
|
||||||
|
/vendor
|
||||||
|
composer.lock
|
||||||
|
.phpunit.result.cache
|
||||||
38
composer.json
Normal file
38
composer.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "pcm/geocode-bundle",
|
||||||
|
"description": "Geocode postcodes and entities",
|
||||||
|
"type": "symfony-bundle",
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Bradley Goode",
|
||||||
|
"email": "bg@pcmsystems.co.uk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Matt Feeney",
|
||||||
|
"email": "mf@pcmsystems.co.uk"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^8.1.0",
|
||||||
|
"symfony/http-client": "^6.1",
|
||||||
|
"symfony/dependency-injection": "^6.1"
|
||||||
|
},
|
||||||
|
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^9.5",
|
||||||
|
"symfony/phpunit-bridge": "^6.1"
|
||||||
|
},
|
||||||
|
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Pcm\\GeocodeBundle\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Pcm\\GeocodeBundle\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
phpunit.xml.dist
Normal file
27
phpunit.xml.dist
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd"
|
||||||
|
backupGlobals="false"
|
||||||
|
colors="true"
|
||||||
|
bootstrap="./vendor/autoload.php"
|
||||||
|
>
|
||||||
|
<php>
|
||||||
|
<ini name="error_reporting" value="-1" />
|
||||||
|
<ini name="intl.default_locale" value="en" />
|
||||||
|
<ini name="intl.error_level" value="0" />
|
||||||
|
<ini name="memory_limit" value="-1" />
|
||||||
|
</php>
|
||||||
|
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Test suite">
|
||||||
|
<directory suffix="Test.php">./tests</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
|
||||||
|
<filter>
|
||||||
|
<whitelist>
|
||||||
|
<directory>./src</directory>
|
||||||
|
</whitelist>
|
||||||
|
</filter>
|
||||||
|
</phpunit>
|
||||||
32
src/Entity/GeocodeData.php
Normal file
32
src/Entity/GeocodeData.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pcm\GeocodeBundle\Entity;
|
||||||
|
|
||||||
|
class GeocodeData
|
||||||
|
{
|
||||||
|
private float $latitude;
|
||||||
|
|
||||||
|
private float $longitude;
|
||||||
|
|
||||||
|
public function setLatitude(float $latitude): void
|
||||||
|
{
|
||||||
|
$this->latitude = $latitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLongitude(float $longitude): void
|
||||||
|
{
|
||||||
|
$this->longitude = $longitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLatitude(): float
|
||||||
|
{
|
||||||
|
return $this->getLatitude();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLongitude(): float
|
||||||
|
{
|
||||||
|
return $this->getLongitude();
|
||||||
|
}
|
||||||
|
}
|
||||||
66
src/Geocoder.php
Normal file
66
src/Geocoder.php
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pcm\GeocodeBundle;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Pcm\GeocodeBundle\Entity\GeocodeData;
|
||||||
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||||
|
|
||||||
|
class Geocoder
|
||||||
|
{
|
||||||
|
private const API_URL = "https://nominatim.openstreetmap.org/search";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $postcode
|
||||||
|
* @return GeocodeData
|
||||||
|
*/
|
||||||
|
public function geocodePostcode(string $postcode): GeocodeData
|
||||||
|
{
|
||||||
|
$client = $this->getHttpClient();
|
||||||
|
$response = $this->makeApiRequest($client, $postcode);
|
||||||
|
$data = $response->toArray();
|
||||||
|
$this->throwIfNoResponseData($data);
|
||||||
|
|
||||||
|
return $this->createGeocodeDataObject($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getHttpClient(): HttpClientInterface
|
||||||
|
{
|
||||||
|
return HttpClient::create([
|
||||||
|
'headers' => ["User-Agent" => "Geocode"]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeApiRequest(HttpClientInterface $client, string $postcode): ResponseInterface
|
||||||
|
{
|
||||||
|
return $client->request(
|
||||||
|
method: 'GET',
|
||||||
|
url: self::API_URL,
|
||||||
|
options: [
|
||||||
|
'query' => [
|
||||||
|
'format' => 'json',
|
||||||
|
'postalcode' => $postcode
|
||||||
|
]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function throwIfNoResponseData(array $data): void
|
||||||
|
{
|
||||||
|
if (empty($data))
|
||||||
|
throw new Exception("No data was received from API response! Were the arguments valid?");
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createGeocodeDataObject(array $data): GeocodeData
|
||||||
|
{
|
||||||
|
$geocodeData = new GeocodeData();
|
||||||
|
$geocodeData->setLatitude((float) $data[0]['lat']);
|
||||||
|
$geocodeData->setLongitude((float) $data[0]['lon']);
|
||||||
|
|
||||||
|
return $geocodeData;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/Interface/MappableInterface.php
Normal file
18
src/Interface/MappableInterface.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pcm\GeocodeBundle\Interface;
|
||||||
|
|
||||||
|
interface MappableInterface
|
||||||
|
{
|
||||||
|
public function getLatitude(): ?float;
|
||||||
|
|
||||||
|
public function getLongitude(): ?float;
|
||||||
|
|
||||||
|
public function setLatitude(float $lat): self;
|
||||||
|
|
||||||
|
public function setLongitude(float $lon): self;
|
||||||
|
|
||||||
|
public function isGeocoded(): bool;
|
||||||
|
}
|
||||||
9
src/PcmGeocodeBundle.php
Normal file
9
src/PcmGeocodeBundle.php
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pcm\GeocodeBundle;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
|
||||||
|
|
||||||
|
class PcmGeocodeBundle extends AbstractBundle {}
|
||||||
41
src/Trait/MappableTrait.php
Normal file
41
src/Trait/MappableTrait.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pcm\GeocodeBundle\Trait;
|
||||||
|
|
||||||
|
trait MappableTrait
|
||||||
|
{
|
||||||
|
private ?float $latitude = null;
|
||||||
|
|
||||||
|
private ?float $longitude = null;
|
||||||
|
|
||||||
|
public function getLatitude(): ?float
|
||||||
|
{
|
||||||
|
return $this->latitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLongitude(): ?float
|
||||||
|
{
|
||||||
|
return $this->longitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLatitude(float $lat): self
|
||||||
|
{
|
||||||
|
$this->latitude = $lat;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLongitude(float $lon): self
|
||||||
|
{
|
||||||
|
$this->longitude = $lon;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isGeocoded(): bool
|
||||||
|
{
|
||||||
|
return $this->getLatitude() && $this->getLongitude();
|
||||||
|
}
|
||||||
|
}
|
||||||
55
tests/Service/GeocodeTest.php
Normal file
55
tests/Service/GeocodeTest.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\tests\Service;
|
||||||
|
|
||||||
|
use Pcm\GeocodeBundle\Entity\GeocodeData;
|
||||||
|
use Pcm\GeocodeBundle\Geocoder;
|
||||||
|
use Pcm\GeocodeBundle\Interface\MappableInterface;
|
||||||
|
use Pcm\GeocodeBundle\Trait\MappableTrait;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class GeocodeTest extends TestCase
|
||||||
|
{
|
||||||
|
// Buckingham Palace
|
||||||
|
private const POSTCODE = 'SW1A 1AA';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Pcm\GeocodeBundle\Geocoder
|
||||||
|
*/
|
||||||
|
private Geocoder $geocoder;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->geocoder = new Geocoder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGeocodeInstance(): void
|
||||||
|
{
|
||||||
|
$this->assertInstanceOf(\Pcm\GeocodeBundle\Geocoder::class, $this->geocoder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGeocodePostcodeThrowsOnInvalidInput(): void
|
||||||
|
{
|
||||||
|
sleep(1);
|
||||||
|
$this->expectException(\Exception::class);
|
||||||
|
$this->geocoder->geocodePostcode('aaaaaaaa');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGeocodePostcodeReturnsGeocodeObject(): void
|
||||||
|
{
|
||||||
|
sleep(1);
|
||||||
|
$result = $this->geocoder->geocodePostcode(self::POSTCODE);
|
||||||
|
$this->assertInstanceOf(GeocodeData::class, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getMappableEntity(): MappableInterface
|
||||||
|
{
|
||||||
|
return new class implements MappableInterface
|
||||||
|
{
|
||||||
|
use MappableTrait;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
86
tests/Trait/MappableTraitTest.php
Normal file
86
tests/Trait/MappableTraitTest.php
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\tests\Trait;
|
||||||
|
|
||||||
|
use Pcm\GeocodeBundle\Interface\MappableInterface;
|
||||||
|
use Pcm\GeocodeBundle\Trait\MappableTrait;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class MappableTraitTest extends TestCase
|
||||||
|
{
|
||||||
|
private const FLOAT = 123.456;
|
||||||
|
|
||||||
|
private MappableInterface $obj;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->obj = $this->getTraitObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetLatitude(): void
|
||||||
|
{
|
||||||
|
$this->assertInstanceOf(MappableInterface::class, $this->obj->setLatitude(self::FLOAT));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetLatitudeReturnsNull(): void
|
||||||
|
{
|
||||||
|
$this->assertNull($this->obj->getLatitude());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetLatitude(): void
|
||||||
|
{
|
||||||
|
$this->obj->setLatitude(self::FLOAT);
|
||||||
|
$this->assertSame(self::FLOAT, $this->obj->getLatitude());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetLongitude(): void
|
||||||
|
{
|
||||||
|
$this->assertInstanceOf(MappableInterface::class, $this->obj->setLongitude(self::FLOAT));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetLongitudeReturnsNull(): void
|
||||||
|
{
|
||||||
|
$this->assertNull($this->obj->getLongitude());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetLongitude(): void
|
||||||
|
{
|
||||||
|
$this->obj->setLongitude(self::FLOAT);
|
||||||
|
$this->assertSame(self::FLOAT, $this->obj->getLongitude());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsGeocodedReturnsFalse(): void
|
||||||
|
{
|
||||||
|
$this->assertFalse($this->obj->isGeocoded());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsGeocodedReturnsFalseIfLatIsSet(): void
|
||||||
|
{
|
||||||
|
$this->obj->setLatitude(self::FLOAT);
|
||||||
|
$this->assertFalse($this->obj->isGeocoded());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsGeocodedReturnsFalseIfLonIsSet(): void
|
||||||
|
{
|
||||||
|
$this->obj->setLongitude(self::FLOAT);
|
||||||
|
$this->assertFalse($this->obj->isGeocoded());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsGeocodedReturnsTrueIfLatAndLonAreSet(): void
|
||||||
|
{
|
||||||
|
$this->obj->setLatitude(self::FLOAT);
|
||||||
|
$this->obj->setLongitude(self::FLOAT);
|
||||||
|
$this->assertTrue($this->obj->isGeocoded());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getTraitObject(): MappableInterface
|
||||||
|
{
|
||||||
|
return new class implements MappableInterface
|
||||||
|
{
|
||||||
|
use MappableTrait;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user