Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

use PhpCsFixer\Config;
use PhpCsFixer\Finder;
use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;

return (new Config())
->setParallelConfig(ParallelConfigFactory::detect())
->setRiskyAllowed(true)
->setRules([
'@Symfony' => true
])
->setFinder(
(new Finder())
->in(__DIR__)
)
;
12 changes: 9 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,29 @@
"license": "gpl-3.0-or-later",
"type": "library",
"require": {
"php": "^8.1"
"php": "^8.1",
"kkevindev/assert-return-value":"^1.11.0",
"symfony/http-client": "^6.4 || ^7.0 || ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^9.6",
"phpstan/phpstan": "^2.1",
"friendsofphp/php-cs-fixer": "^v3.89"
},

"autoload": {
"psr-4": {
"Kkevindev\\PostcodeTech\\": "src/"
}
},

"autoload-dev": {
"psr-4": {
"Kkevindev\\PostcodeTech\\Tests\\": "tests/"
}
},
"scripts": {
"test": "vendor/bin/phpunit",
"cs-fix": "vendor/bin/php-cs-fixer fix",
"phpstan": "vendor/bin/phpstan analyse",
"composer-validate": "composer validate --strict"
}
}
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ parameters:
level: 10
paths:
- src
- tests
91 changes: 45 additions & 46 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,75 +5,74 @@
use Kkevindev\PostcodeTech\Exceptions\HttpException;
use Kkevindev\PostcodeTech\Exceptions\PostcodeNotFoundException;
use Kkevindev\PostcodeTech\Exceptions\ValidationException;
use Kkevindev\PostcodeTech\Http\Request\Headers;
use Kkevindev\PostcodeTech\Http\Response\Response;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

/**
* @internal
*/
final class Client
{
/** @var string */
private const BASE_URI = 'https://postcode.tech';

private Headers $headers;

public function __construct(
private readonly string $token,
private readonly HttpClientInterface $httpClient,
) {
$this->headers = new Headers([
'Authorization' => sprintf('Bearer %s', $this->token),
]);
}

/**
* @throws PostcodeNotFoundException
* @throws ValidationException
* @throws HttpException
*
* @return array{
* street: string,
* city: string,
* }
*
* @throws PostcodeNotFoundException
* @throws ValidationException
* @throws HttpException
*/
public function get(string $postcode, int $number): array
{
$queryParameters = [
'postcode' => $postcode,
'number' => $number,
];
try {
$response = $this->httpClient->request(
'GET',
'https://postcode.tech/api/v1/postcode',
[
'auth_bearer' => $this->token,
'query' => [
'postcode' => $postcode,
'number' => $number,
],
],
);

$uri = sprintf(
'%s/%s?%s',
self::BASE_URI,
'api/v1/postcode',
http_build_query($queryParameters),
);
$statusCode = $response->getStatusCode();

$response = file_get_contents(
$uri,
false,
stream_context_create([
'http' => [
'method' => 'GET',
'header' => $this->headers->getHeaders(),
'ignore_errors' => true,
],
]),
);
$responseBody = $response->getContent(false);
} catch (TransportExceptionInterface|ClientExceptionInterface|RedirectionExceptionInterface|ServerExceptionInterface $exception) {
throw new HttpException($exception->getMessage(), previous: $exception);
}

if (!$response) {
throw new HttpException('No response received from the API.');
if (200 > $statusCode || 300 <= $statusCode) {
throw match ($statusCode) {
401 => new HttpException('Unauthorized', $responseBody),
404 => new PostcodeNotFoundException('No results found for the given postcode and number.', $responseBody),
422 => new ValidationException('The request data was invalid.', $responseBody),
default => new HttpException('An unknown error occurred while fetching the data from the API.', $responseBody),
};
}

// @todo refactor the magic '$http_response_header' to 'http_get_last_response_headers()' when PHP 8.4 is the lowest supported version.
$response = new Response($response, $http_response_header);
try {
$array = json_decode($responseBody, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $exception) {
throw new HttpException('The response body is not a valid JSON array.', $responseBody, $exception);
}

if (!$response->isSuccess()) {
match ($response->getHeaders()->getStatusCode()) {
404 => throw new PostcodeNotFoundException('No results found for the given postcode and number.', $response->getResponseBody()),
422 => throw new ValidationException('The request data was invalid.', $response->getResponseBody()),
default => throw new HttpException('An unknown error occurred while fetching the data from the API.', $response->getResponseBody()),
};
if (!is_array($array) || empty($array['street']) || !is_string($array['street']) || empty($array['city']) || !is_string($array['city'])) {
throw new HttpException('The response body did not contain the expected data.', $responseBody);
}

return $response->getResponseBodyAsArray();
return $array;
}
}
2 changes: 1 addition & 1 deletion src/Exceptions/HttpException.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

class HttpException extends \Exception
{
public function __construct(string $message = "", string $responseBody = "", ?\Throwable $previous = null)
public function __construct(string $message = '', string $responseBody = '', ?\Throwable $previous = null)
{
parent::__construct(sprintf('%s: %s', $message, $responseBody), previous: $previous);
}
Expand Down
39 changes: 0 additions & 39 deletions src/Http/Request/Headers.php

This file was deleted.

21 changes: 0 additions & 21 deletions src/Http/Response/Headers.php

This file was deleted.

62 changes: 0 additions & 62 deletions src/Http/Response/Response.php

This file was deleted.

3 changes: 2 additions & 1 deletion src/Postcode.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Kkevindev\PostcodeTech\Exceptions\HttpException;
use Kkevindev\PostcodeTech\Exceptions\PostcodeNotFoundException;
use Kkevindev\PostcodeTech\Exceptions\ValidationException;
use Symfony\Component\HttpClient\HttpClient;

class Postcode implements PostcodeInterface
{
Expand All @@ -23,7 +24,7 @@ protected function __construct(
*/
public static function search(string $postcode, int $number, string $token): self
{
$client = new Client($token);
$client = new Client($token, HttpClient::create());

$response = $client->get($postcode, $number);

Expand Down
55 changes: 55 additions & 0 deletions tests/AbstractClientTestCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace Kkevindev\PostcodeTech\Tests;

use Kkevindev\PostcodeTech\Client;
use Kkevindev\PostcodeTech\Exceptions\HttpException;
use Kkevindev\PostcodeTech\Exceptions\PostcodeNotFoundException;
use Kkevindev\PostcodeTech\Exceptions\ValidationException;
use PHPUnit\Framework\TestCase;

abstract class AbstractClientTestCase extends TestCase
{
abstract protected function getClient(string $token): Client;

public function testValid200ResponseReturnsResult(): void
{
$expectedStreet = 'Nieuwezijds Voorburgwal';
$expectedCity = 'Amsterdam';

$result = $this->getClient('demo')->get('1012 RJ', 147);

self::assertEquals($expectedStreet, $result['street']);
self::assertEquals($expectedCity, $result['city']);
}

public function test401ResponseThrowsException(): void
{
$this->expectException(HttpException::class);
$this->expectExceptionMessage(
<<<HTML
Unauthorized: {
"message": "Unauthorized"
}
HTML
);

$this->getClient('invalid-token')->get('0000AA', 401);
}

public function test404ResponseThrowsException(): void
{
$this->expectException(PostcodeNotFoundException::class);
$this->expectExceptionMessage('No results found for the given postcode and number.: {"message":"No result for this combination."}');

$this->getClient('demo')->get('0000AA', 404);
}

public function test422ResponseThrowsExceptionWhenMalformedInput(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('The request data was invalid.: {"message":"The given data was invalid.","errors":{"postcode":["Postcode should be formatted `1111AA` or `1111 AA`."]}}');

$this->getClient('demo')->get('X', 422);
}
}
Loading