diff --git a/composer.json b/composer.json index 242dd97..2b9ea40 100755 --- a/composer.json +++ b/composer.json @@ -18,7 +18,8 @@ "require": { "php": ">=5.4.0", "php-http/httplug": "^1.1", - "guzzlehttp/psr7": "^1.3" + "guzzlehttp/psr7": "^1.3", + "ext-json": "*" }, "require-dev": { "phpunit/phpunit": "^4.3.5|^5.0", diff --git a/lib/FH/PostcodeAPI/Client.php b/lib/FH/PostcodeAPI/Client.php index af080e8..0f18a6b 100755 --- a/lib/FH/PostcodeAPI/Client.php +++ b/lib/FH/PostcodeAPI/Client.php @@ -4,11 +4,14 @@ use FH\PostcodeAPI\Exception\CouldNotParseResponseException; use FH\PostcodeAPI\Exception\InvalidApiKeyException; +use FH\PostcodeAPI\Exception\InvalidUrlException; use FH\PostcodeAPI\Exception\ServerErrorException; use GuzzleHttp\Psr7\Request; +use Http\Client\Exception; use Http\Client\HttpClient; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; +use stdClass; /** * Client library for postcodeapi.nu 2.0 web service. @@ -50,7 +53,11 @@ public function __construct(HttpClient $httpClient, $url = null) * @param string|null $number * @param int $from * - * @return \stdClass + * @return stdClass + * @throws CouldNotParseResponseException + * @throws Exception + * @throws InvalidApiKeyException + * @throws ServerErrorException */ public function getAddresses($postcode = null, $number = null, $from = 0) { @@ -64,7 +71,11 @@ public function getAddresses($postcode = null, $number = null, $from = 0) /** * @param string $id * - * @return \stdClass + * @return stdClass + * @throws CouldNotParseResponseException + * @throws Exception + * @throws InvalidApiKeyException + * @throws ServerErrorException */ public function getAddress($id) { @@ -74,7 +85,11 @@ public function getAddress($id) /** * @param string $postcode * - * @return \stdClass + * @return stdClass + * @throws CouldNotParseResponseException + * @throws Exception + * @throws InvalidApiKeyException + * @throws ServerErrorException */ public function getPostcodeDataByPostcode($postcode) { @@ -86,7 +101,11 @@ public function getPostcodeDataByPostcode($postcode) * @param string $longitude * @param string $sort * - * @return \stdClass + * @return stdClass + * @throws CouldNotParseResponseException + * @throws Exception + * @throws InvalidApiKeyException + * @throws ServerErrorException */ public function getPostcodesByCoordinates($latitude, $longitude, $sort = self::POSTCODES_SORT_DISTANCE) { @@ -99,18 +118,41 @@ public function getPostcodesByCoordinates($latitude, $longitude, $sort = self::P ]); } + /** + * Sends request to API using url and returns parsed response. + * Useful for pagination link. + * + * @param $url + * @return stdClass + * @throws CouldNotParseResponseException + * @throws Exception + * @throws InvalidApiKeyException + * @throws ServerErrorException + * @throws InvalidUrlException + */ + public function request($url) + { + $this->validateUrl($url); + $request = $this->createHttpGetRequest($url); + $response = $this->httpClient->sendRequest($request); + + return $this->parseResponse($response, $request); + } + /** * @param string $path * @param array $params * - * @return \stdClass + * @return stdClass * - * @throws RequestException + * @throws CouldNotParseResponseException + * @throws InvalidApiKeyException + * @throws ServerErrorException + * @throws Exception */ private function get($path, array $params = []) { $request = $this->createHttpGetRequest($this->buildUrl($path), $params); - $response = $this->httpClient->sendRequest($request); return $this->parseResponse($response, $request); @@ -126,10 +168,8 @@ private function buildUrl($path) } /** - * @param string $method * @param string $url - * @param array $queryParams - * + * @param array $params * @return Request */ private function createHttpGetRequest($url, array $params = []) @@ -142,9 +182,12 @@ private function createHttpGetRequest($url, array $params = []) /** * @param ResponseInterface $response * - * @return \stdClass + * @param RequestInterface $request + * @return stdClass * * @throws CouldNotParseResponseException + * @throws InvalidApiKeyException + * @throws ServerErrorException */ private function parseResponse(ResponseInterface $response, RequestInterface $request) { @@ -165,4 +208,23 @@ private function parseResponse(ResponseInterface $response, RequestInterface $re return $result; } + + /** + * @param string $url + * @throws InvalidUrlException + */ + private function validateUrl($url) + { + if (filter_var($url, FILTER_VALIDATE_URL) === false) { + throw new InvalidUrlException($url); + } + + $urlComponentsToCompare = [PHP_URL_HOST, PHP_URL_SCHEME]; + + foreach ($urlComponentsToCompare as $urlComponent) { + if (parse_url($url, $urlComponent) !== parse_url($this->url, $urlComponent)) { + throw new InvalidUrlException($url); + } + } + } } diff --git a/lib/FH/PostcodeAPI/Exception/InvalidUrlException.php b/lib/FH/PostcodeAPI/Exception/InvalidUrlException.php new file mode 100644 index 0000000..82ab179 --- /dev/null +++ b/lib/FH/PostcodeAPI/Exception/InvalidUrlException.php @@ -0,0 +1,32 @@ + + */ +class InvalidUrlException extends \Exception implements PostcodeApiExceptionInterface +{ + /** + * @var string + */ + private $url; + + /** + * @param string $url + */ + public function __construct($url) + { + parent::__construct("Invalid url provided '$url'"); + + $this->url = $url; + } + + /** + * @return string + */ + public function getUrl() + { + return $this->url; + } +} diff --git a/tests/FH/PostcodeAPI/ClientTest.php b/tests/FH/PostcodeAPI/ClientTest.php index 86dc356..bc45675 100755 --- a/tests/FH/PostcodeAPI/ClientTest.php +++ b/tests/FH/PostcodeAPI/ClientTest.php @@ -3,12 +3,19 @@ namespace FH\PostcodeAPI\Test; use FH\PostcodeAPI\Client; +use FH\PostcodeAPI\Exception\CouldNotParseResponseException; +use FH\PostcodeAPI\Exception\InvalidApiKeyException; +use FH\PostcodeAPI\Exception\InvalidUrlException; +use FH\PostcodeAPI\Exception\ServerErrorException; use GuzzleHttp\Psr7\Response; +use Http\Client\Exception; +use PHPUnit_Framework_TestCase; +use stdClass; /** * @author Gijs Nieuwenhuis */ -final class ClientTest extends \PHPUnit_Framework_TestCase +final class ClientTest extends PHPUnit_Framework_TestCase { /** @var string */ const POSTCODE_PATTERN = '/^[\d]{4}[\w]{2}$/i'; @@ -31,8 +38,16 @@ final class ClientTest extends \PHPUnit_Framework_TestCase /** @var string */ const FRESHHEADS_ADDRESS_ID = '0855200000061001'; + /** @var string */ + const FRESHHEADS_VALID_URL = 'https://api.postcodeapi.nu/v2/addresses/?postcode=4904ZR&from%5Bpostcode%5D=4904ZR&from%5Bid%5D=0826200000012452&from%5Bnumber%5D=95'; + /** * @expectedException FH\PostcodeAPI\Exception\InvalidApiKeyException + * + * @throws CouldNotParseResponseException + * @throws Exception + * @throws InvalidApiKeyException + * @throws ServerErrorException */ public function testRequestExceptionIsThrownWhenUsingAnInvalidApiKey() { @@ -43,6 +58,12 @@ public function testRequestExceptionIsThrownWhenUsingAnInvalidApiKey() $client->getAddresses(); } + /** + * @throws InvalidApiKeyException + * @throws ServerErrorException + * @throws CouldNotParseResponseException + * @throws Exception + */ public function testListResourceReturnsAllAddressesWhenNoParamsAreSupplied() { $client = $this->createClient( @@ -60,6 +81,12 @@ public function testListResourceReturnsAllAddressesWhenNoParamsAreSupplied() $this->applyAddressFieldAreSetAndOfTheCorrectTypeAssertions($addresses[0]); } + /** + * @throws CouldNotParseResponseException + * @throws Exception + * @throws InvalidApiKeyException + * @throws ServerErrorException + */ public function testListResourceReturnsExpectedAddressWhenPostcodeAndNumberAreSupplied() { $client = $this->createClient( @@ -80,6 +107,12 @@ public function testListResourceReturnsExpectedAddressWhenPostcodeAndNumberAreSu $this->applyIsFreshheadsAddressAssertions($firstAddress); } + /** + * @throws CouldNotParseResponseException + * @throws Exception + * @throws InvalidApiKeyException + * @throws ServerErrorException + */ public function testExpectedAddressInformationIsReturnedFromDetailResource() { $client = $this->createClient( @@ -94,8 +127,13 @@ public function testExpectedAddressInformationIsReturnedFromDetailResource() /** * @expectedException FH\PostcodeAPI\Exception\ServerErrorException + * + * @throws CouldNotParseResponseException + * @throws Exception + * @throws InvalidApiKeyException + * @throws ServerErrorException */ - public function testClientThrowsExceptionWhenInvalidInputIsSupplied() + public function testExpectedAddress() { $client = $this->createClient( $this->loadMockResponse('failed_list_with_invalid_postalcode_and_number') @@ -104,6 +142,50 @@ public function testClientThrowsExceptionWhenInvalidInputIsSupplied() $client->getAddresses('invalid_postcode', 'invalid_number'); } + /** + * @dataProvider invalidUrlsProvider + * @expectedException FH\PostcodeAPI\Exception\InvalidUrlException + * + * @throws CouldNotParseResponseException + * @throws Exception + * @throws InvalidApiKeyException + * @throws ServerErrorException + * @throws InvalidUrlException + */ + public function testClientThrowsExceptionWhenInvalidUrlIsSupplied($invalidUrl) + { + $client = $this->createClient( + $this->loadMockResponse('successful_list_without_filtering') + ); + + $client->request($invalidUrl); + } + + /** + * @throws CouldNotParseResponseException + * @throws Exception + * @throws InvalidApiKeyException + * @throws ServerErrorException + * @throws InvalidUrlException + */ + public function testExpectedNoExceptionsWhenValidNextLinkIsSupplied() + { + $client = $this->createClient( + $this->loadMockResponse('successful_list_without_filtering') + ); + + $client->request(self::FRESHHEADS_VALID_URL); + } + + public function invalidUrlsProvider() + { + return [ + ['invalid_url'], + ['https://api.postcodeapi.com/invalid-host'], + ['http://api.postcodeapi.nu/invalid-schema'], + ]; + } + /** * @param string $name * @@ -115,9 +197,9 @@ private function loadMockResponse($name) } /** - * @param \stdClass $address + * @param stdClass $address */ - private function applyIsFreshheadsAddressAssertions(\stdClass $address) + private function applyIsFreshheadsAddressAssertions(stdClass $address) { static::assertSame(strtoupper($address->postcode), self::FRESHHEADS_POSTCODE, 'Incoming postcode did not match the expected postcode'); static::assertSame((string)$address->number, (string)self::FRESHHEADS_NUMBER, 'Incoming number did not match the expected number'); @@ -139,18 +221,18 @@ private function applyIsFreshheadsAddressAssertions(\stdClass $address) } /** - * @param \stdClass $response + * @param stdClass $response */ - private function applyAssertsToMakeSureAddressesArrayIsAvailableInResponse(\stdClass $response) + private function applyAssertsToMakeSureAddressesArrayIsAvailableInResponse(stdClass $response) { static::assertTrue(isset($response->_embedded->addresses)); static::assertTrue(is_array($response->_embedded->addresses)); } /** - * @param \stdClass $address + * @param stdClass $address */ - private function applyAddressFieldAreSetAndOfTheCorrectTypeAssertions(\stdClass $address) + private function applyAddressFieldAreSetAndOfTheCorrectTypeAssertions(stdClass $address) { // only test the availability of the most import fields and their values @@ -176,8 +258,7 @@ private function applyAddressFieldAreSetAndOfTheCorrectTypeAssertions(\stdClass } /** - * @param string $mockedResponses - * + * @param Response|string $mockedResponse * @return Client */ private function createClient(Response $mockedResponse)