diff --git a/.gitattributes b/.gitattributes
index 82f8b45..ea2ff46 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -5,3 +5,5 @@
/phpcs.xml.dist export-ignore
/phpstan.neon export-ignore
/phpunit.xml.dist export-ignore
+/.php-cs-fixer.dist.php export-ignore
+/bump-version.sh export-ignore
diff --git a/.gitignore b/.gitignore
index 992d660..141ec2e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,5 @@ vendor
.idea
composer.lock
.phpunit.result.cache
-.phpunit.cache
\ No newline at end of file
+.phpunit.cache
+.php-cs-fixer.cache
diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
new file mode 100644
index 0000000..25d63ed
--- /dev/null
+++ b/.php-cs-fixer.dist.php
@@ -0,0 +1,25 @@
+in(__DIR__)
+ ->append([__FILE__]);
+
+return (new PhpCsFixer\Config())
+ ->setRules([
+ '@PER-CS' => true,
+ 'no_empty_phpdoc' => true,
+ 'no_superfluous_phpdoc_tags' => [
+ 'allow_mixed' => true,
+ ],
+ 'no_unused_imports' => true,
+ 'not_operator_with_successor_space' => true,
+ 'phpdoc_trim' => true,
+ 'phpdoc_trim_consecutive_blank_line_separation' => true,
+ 'trailing_comma_in_multiline' => [
+ 'after_heredoc' => true,
+ // https://cs.symfony.com/doc/rules/control_structure/trailing_comma_in_multiline.html
+ // only enable for the elements that are safe to use with PHP 7.4+
+ 'elements' => ['arguments', 'arrays'],
+ ],
+ ])
+ ->setFinder($finder);
diff --git a/README.md b/README.md
index 2c26564..12cea95 100644
--- a/README.md
+++ b/README.md
@@ -99,6 +99,51 @@ $connection->setClientId('your-client-id')
});
```
+### Transports
+
+By default, the SDK will create a `Transport` with the `TransportFactory` if you do not supply one.
+
+To create your own `Transport` you have to create a class which implements `Sendy\Api\Http\Tranport\TransportInterface`.
+
+An example for the Laravel framework could look like this
+
+```php
+use Illuminate\Foundation\Application;
+use Illuminate\Support\Arr;
+use Illuminate\Support\Facades\Http;
+use Sendy\Api\Exceptions\TransportException;
+use Sendy\Api\Http\Request;
+use Sendy\Api\Http\Response;
+use Sendy\Api\Http\Transport\TransportInterface
+
+class LaravelTransport implements TransportInterface
+{
+ public function send(Request $request): Response
+ {
+ $headers = $request->getHeaders();
+ $contentType = Arr::pull($headers, 'Content-Type', 'application/json');
+
+ try {
+ $response = Http::withHeaders($headers)
+ ->withBody($request->getBody(), $contentType)
+ ->withMethod($request->getMethod())
+ ->withUrl($request->getUrl())
+ ->send();
+ } catch (\Throwable $e) {
+ throw new TransportException($e->getMessage(), $e->getCode(), $e);
+ }
+
+ return new Response($response->status(), $response->headers(), $response->body());
+ }
+
+ public function getUserAgent() : string
+ {
+ return 'LaravelHttpClient/' . Application::VERSION;
+ }
+}
+
+```
+
### Endpoints
The endpoints in the API documentation are mapped to the resource as defined in the Resources directory. Please consult
diff --git a/bump_version.sh b/bump_version.sh
new file mode 100755
index 0000000..8872e6e
--- /dev/null
+++ b/bump_version.sh
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+# Print the current version of the project and bump it to the given version.
+
+set -e
+
+current_version=$(echo "echo Connection::VERSION . PHP_EOL;" | cat src/Connection.php - | php)
+echo "Current version: $current_version"
+
+if [[ -z "$1" ]]
+then
+ echo "To bump the version, provide the new version number as an argument."
+ exit 1
+fi
+
+# Remove the 'v' prefix if it exists
+new_version=${1#v}
+
+echo "New version: $new_version"
+
+if ! [[ "$new_version" =~ ^[0-9]+\.[0-9]+\.[0-9](-[a-z]+\.[0-9]+)?$ ]]
+then
+ echo "Invalid version format. Please use semantic versioning (https://semver.org/)."
+ exit 1
+fi
+
+echo "Bumping version to: $new_version"
+
+perl -pi -e "s/^ public const VERSION = .*/ public const VERSION = '$new_version';/" src/Connection.php
+
+echo
+echo "To release the new version, first, commit the changes:"
+echo " git add --all"
+echo " git commit -m "$new_version""
+echo " git push"
+echo
+echo "Once the commit is pushed to the master branch, create a release on GitHub to distribute the new version:"
+echo " https://github.com/sendynl/php-sdk/releases/new?tag=v$new_version"
diff --git a/composer.json b/composer.json
index f353291..4e20523 100644
--- a/composer.json
+++ b/composer.json
@@ -27,8 +27,7 @@
},
"require": {
"php": ">=7.4.0",
- "ext-json": "*",
- "guzzlehttp/guzzle": "~6.0|~7.0"
+ "ext-json": "*"
},
"config": {
"platform": {
@@ -38,12 +37,15 @@
"require-dev": {
"phpunit/phpunit": "^9.0",
"phpstan/phpstan": "^1",
- "squizlabs/php_codesniffer": "^3.7",
- "mockery/mockery": "^1.5"
+ "mockery/mockery": "^1.5",
+ "php-cs-fixer/shim": "^3.87",
+ "psr/http-client": "^1.0",
+ "psr/http-factory": "^1.1"
},
"scripts": {
- "lint": "vendor/bin/phpcs",
- "analyze": "vendor/bin/phpstan --xdebug",
+ "lint": "vendor/bin/php-cs-fixer fix --dry-run --diff",
+ "fix": "vendor/bin/php-cs-fixer fix",
+ "analyze": "vendor/bin/phpstan",
"test": "vendor/bin/phpunit"
}
}
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
deleted file mode 100644
index 9ce187d..0000000
--- a/phpcs.xml.dist
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
- The KeenDelivery Coding Standards
-
- src
- tests
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/ApiException.php b/src/ApiException.php
index e66c9cd..afc7555 100644
--- a/src/ApiException.php
+++ b/src/ApiException.php
@@ -2,29 +2,8 @@
namespace Sendy\Api;
-final class ApiException extends \Exception
-{
- /** @var array */
- private array $errors = [];
-
- /**
- * @param string $message
- * @param int $code
- * @param \Throwable|null $previous
- * @param string[][] $errors
- */
- public function __construct(string $message = "", int $code = 0, ?\Throwable $previous = null, array $errors = [])
- {
- $this->errors = $errors;
-
- parent::__construct($message, $code, $previous);
- }
-
- /**
- * @return array
- */
- public function getErrors(): array
- {
- return $this->errors;
- }
-}
+/**
+ * @deprecated This interface exists for backwards compatibility and may be removed in a future version.
+ * @internal
+ */
+interface ApiException extends \Throwable {}
diff --git a/src/Connection.php b/src/Connection.php
index dc23ae8..b4c9cee 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -2,15 +2,12 @@
namespace Sendy\Api;
-use GuzzleHttp\Client;
-use GuzzleHttp\Exception\BadResponseException;
-use GuzzleHttp\Exception\GuzzleException;
-use GuzzleHttp\Exception\ServerException;
-use GuzzleHttp\Psr7\Message;
-use GuzzleHttp\Psr7\Request;
-use Psr\Http\Message\ResponseInterface;
-use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
+use Sendy\Api\Exceptions\SendyException;
+use Sendy\Api\Http\Request;
+use Sendy\Api\Http\Response;
+use Sendy\Api\Http\Transport\TransportFactory;
+use Sendy\Api\Http\Transport\TransportInterface;
use Sendy\Api\Resources\Resource;
/**
@@ -26,7 +23,9 @@
*/
class Connection
{
- private const BASE_URL = 'https://app.sendy.nl';
+ public const VERSION = '3.0.0';
+
+ public const BASE_URL = 'https://app.sendy.nl';
private const API_URL = '/api';
@@ -34,10 +33,7 @@ class Connection
private const TOKEN_URL = '/oauth/token';
- private const VERSION = '1.0.2';
-
- /** @var Client|null */
- private ?Client $client = null;
+ private ?TransportInterface $transport = null;
/** @var string The Client ID as UUID */
private string $clientId;
@@ -66,10 +62,9 @@ class Connection
/** @var mixed */
private $state = null;
- /** @var callable(Client) */
+ /** @var callable($this) */
private $tokenUpdateCallback;
- /** @var bool */
private bool $oauthClient = false;
public ?Meta $meta;
@@ -77,46 +72,22 @@ class Connection
public ?RateLimits $rateLimits;
/**
- * @return Client
+ * @var array>
*/
- public function getClient(): Client
- {
- if ($this->client instanceof Client) {
- return $this->client;
- }
+ public array $sendyHeaders = [];
- $userAgent = sprintf("Sendy/%s PHP/%s", self::VERSION, phpversion());
-
- if ($this->isOauthClient()) {
- $userAgent .= ' OAuth/2.0';
- }
-
- $userAgent .= " {$this->userAgentAppendix}";
-
- $this->client = new Client([
- 'http_errors' => true,
- 'expect' => false,
- 'base_uri' => self::BASE_URL,
- 'headers' => [
- 'User-Agent' => trim($userAgent),
- ]
- ]);
-
- return $this->client;
+ public function getTransport(): TransportInterface
+ {
+ return $this->transport ??= TransportFactory::create();
}
- /**
- * @param Client $client
- */
- public function setClient(Client $client): void
+ public function setTransport(TransportInterface $transport): Connection
{
- $this->client = $client;
+ $this->transport = $transport;
+
+ return $this;
}
- /**
- * @param string $userAgentAppendix
- * @return Connection
- */
public function setUserAgentAppendix(string $userAgentAppendix): Connection
{
$this->userAgentAppendix = $userAgentAppendix;
@@ -124,10 +95,6 @@ public function setUserAgentAppendix(string $userAgentAppendix): Connection
return $this;
}
- /**
- * @param string $clientId
- * @return Connection
- */
public function setClientId(string $clientId): Connection
{
$this->clientId = $clientId;
@@ -135,10 +102,6 @@ public function setClientId(string $clientId): Connection
return $this;
}
- /**
- * @param string $clientSecret
- * @return Connection
- */
public function setClientSecret(string $clientSecret): Connection
{
$this->clientSecret = $clientSecret;
@@ -146,10 +109,6 @@ public function setClientSecret(string $clientSecret): Connection
return $this;
}
- /**
- * @param string $authorizationCode
- * @return Connection
- */
public function setAuthorizationCode(string $authorizationCode): Connection
{
$this->authorizationCode = $authorizationCode;
@@ -157,18 +116,11 @@ public function setAuthorizationCode(string $authorizationCode): Connection
return $this;
}
- /**
- * @return string
- */
public function getAccessToken(): string
{
return $this->accessToken;
}
- /**
- * @param string $accessToken
- * @return Connection
- */
public function setAccessToken(string $accessToken): Connection
{
$this->accessToken = $accessToken;
@@ -176,18 +128,11 @@ public function setAccessToken(string $accessToken): Connection
return $this;
}
- /**
- * @return int
- */
public function getTokenExpires(): int
{
return $this->tokenExpires;
}
- /**
- * @param int $tokenExpires
- * @return Connection
- */
public function setTokenExpires(int $tokenExpires): Connection
{
$this->tokenExpires = $tokenExpires;
@@ -195,18 +140,11 @@ public function setTokenExpires(int $tokenExpires): Connection
return $this;
}
- /**
- * @return string
- */
public function getRefreshToken(): string
{
return $this->refreshToken;
}
- /**
- * @param string $refreshToken
- * @return Connection
- */
public function setRefreshToken(string $refreshToken): Connection
{
$this->refreshToken = $refreshToken;
@@ -214,10 +152,6 @@ public function setRefreshToken(string $refreshToken): Connection
return $this;
}
- /**
- * @param string $redirectUrl
- * @return Connection
- */
public function setRedirectUrl(string $redirectUrl): Connection
{
$this->redirectUrl = $redirectUrl;
@@ -227,19 +161,14 @@ public function setRedirectUrl(string $redirectUrl): Connection
/**
* @param mixed|null $state
- * @return Connection
*/
- public function setState($state)
+ public function setState($state): Connection
{
$this->state = $state;
return $this;
}
- /**
- * @param callable $tokenUpdateCallback
- * @return Connection
- */
public function setTokenUpdateCallback(callable $tokenUpdateCallback): Connection
{
$this->tokenUpdateCallback = $tokenUpdateCallback;
@@ -249,31 +178,22 @@ public function setTokenUpdateCallback(callable $tokenUpdateCallback): Connectio
/**
* Build the URL to authorize the application
- *
- * @return string
*/
public function getAuthorizationUrl(): string
{
return self::BASE_URL . self::AUTH_URL . '?' . http_build_query([
- 'client_id' => $this->clientId,
- 'redirect_uri' => $this->redirectUrl,
- 'response_type' => 'code',
- 'state' => $this->state,
- ]);
+ 'client_id' => $this->clientId,
+ 'redirect_uri' => $this->redirectUrl,
+ 'response_type' => 'code',
+ 'state' => $this->state,
+ ]);
}
- /**
- * @return bool
- */
public function isOauthClient(): bool
{
return $this->oauthClient;
}
- /**
- * @param bool $oauthClient
- * @return Connection
- */
public function setOauthClient(bool $oauthClient): Connection
{
$this->oauthClient = $oauthClient;
@@ -281,6 +201,9 @@ public function setOauthClient(bool $oauthClient): Connection
return $this;
}
+ /**
+ * @throws SendyException
+ */
public function checkOrAcquireAccessToken(): void
{
if (empty($this->accessToken) || ($this->tokenHasExpired() && $this->isOauthClient())) {
@@ -299,74 +222,67 @@ public function tokenHasExpired(): bool
return $this->tokenExpires - 10 < time();
}
+ /**
+ * @throws SendyException
+ */
private function acquireAccessToken(): void
{
- try {
- if (empty($this->refreshToken)) {
- $parameters = [
- 'redirect_uri' => $this->redirectUrl,
- 'grant_type' => 'authorization_code',
- 'client_id' => $this->clientId,
- 'client_secret' => $this->clientSecret,
- 'code' => $this->authorizationCode,
- ];
- } else {
- $parameters = [
- 'refresh_token' => $this->refreshToken,
- 'grant_type' => 'refresh_token',
- 'client_id' => $this->clientId,
- 'client_secret' => $this->clientSecret,
- ];
- }
-
- $response = $this->getClient()->post(self::BASE_URL . self::TOKEN_URL, ['form_params' => $parameters]);
-
- Message::rewindBody($response);
-
- $responseBody = $response->getBody()->getContents();
+ if (empty($this->refreshToken)) {
+ $parameters = [
+ 'redirect_uri' => $this->redirectUrl,
+ 'grant_type' => 'authorization_code',
+ 'client_id' => $this->clientId,
+ 'client_secret' => $this->clientSecret,
+ 'code' => $this->authorizationCode,
+ ];
+ } else {
+ $parameters = [
+ 'refresh_token' => $this->refreshToken,
+ 'grant_type' => 'refresh_token',
+ 'client_id' => $this->clientId,
+ 'client_secret' => $this->clientSecret,
+ ];
+ }
- $body = json_decode($responseBody, true);
+ $body = $this->performRequest(
+ $this->createRequest('POST', self::BASE_URL . self::TOKEN_URL, json_encode($parameters)),
+ false,
+ );
- if (json_last_error() === JSON_ERROR_NONE) {
- $this->accessToken = $body['access_token'];
- $this->refreshToken = $body['refresh_token'];
- $this->tokenExpires = time() + $body['expires_in'];
+ $this->accessToken = $body['access_token'];
+ $this->refreshToken = $body['refresh_token'];
+ $this->tokenExpires = time() + $body['expires_in'];
- if (is_callable($this->tokenUpdateCallback)) {
- call_user_func($this->tokenUpdateCallback, $this);
- }
- } else {
- throw new ApiException('Could not acquire tokens, json decode failed. Got response: ' . $responseBody);
- }
- } catch (BadResponseException $e) {
- throw new ApiException('Something went wrong. Got: ' . $e->getMessage(), 0, $e);
+ if (is_callable($this->tokenUpdateCallback)) {
+ call_user_func($this->tokenUpdateCallback, $this);
}
}
/**
- * @param string $method
- * @param string $endpoint
- * @param null|StreamInterface|resource|string $body
- * @param array|null $params
- * @param array|null $headers
- * @return Request
+ * @param array $params
+ * @param array $headers
*/
- private function createRequest(
+ public function createRequest(
string $method,
string $endpoint,
- $body = null,
- ?array $params = null,
- ?array $headers = null
+ ?string $body = null,
+ array $params = [],
+ array $headers = []
): Request {
+ $userAgent = sprintf("SendySDK/%s PHP/%s", self::VERSION, phpversion());
+
+ if ($this->isOauthClient()) {
+ $userAgent .= ' OAuth/2.0';
+ }
+
+ $userAgent .= " {$this->getTransport()->getUserAgent()} {$this->userAgentAppendix}";
+
$headers = array_merge($headers, [
- 'Accept' => 'application/json',
+ 'Accept' => 'application/json',
'Content-Type' => 'application/json',
+ 'User-Agent' => trim($userAgent),
]);
- $this->checkOrAcquireAccessToken();
-
- $headers['Authorization'] = "Bearer {$this->accessToken}";
-
if (! empty($params)) {
$endpoint .= strpos($endpoint, '?') === false ? '?' : '&';
$endpoint .= http_build_query($params);
@@ -380,8 +296,7 @@ private function createRequest(
* @param array $params
* @param array $headers
* @return array>
- * @throws ApiException
- * @throws GuzzleException
+ * @throws SendyException
*/
public function get($url, array $params = [], array $headers = []): array
{
@@ -398,14 +313,13 @@ public function get($url, array $params = [], array $headers = []): array
* @param array $params
* @param array $headers
* @return array>
- * @throws ApiException
- * @throws GuzzleException
+ * @throws SendyException
*/
public function post($url, ?array $body = null, array $params = [], array $headers = []): array
{
$url = self::API_URL . $url;
- if (!is_null($body)) {
+ if (! is_null($body)) {
$body = json_encode($body);
}
@@ -420,8 +334,7 @@ public function post($url, ?array $body = null, array $params = [], array $heade
* @param array> $params
* @param array> $headers
* @return array>
- * @throws ApiException
- * @throws GuzzleException
+ * @throws SendyException
*/
public function put($url, array $body = [], array $params = [], array $headers = []): array
{
@@ -436,7 +349,7 @@ public function put($url, array $body = [], array $params = [], array $headers =
/**
* @param UriInterface|string $url
* @return array>
- * @throws ApiException|\GuzzleHttp\Exception\GuzzleException
+ * @throws SendyException
*/
public function delete($url): array
{
@@ -448,44 +361,40 @@ public function delete($url): array
}
/**
- * @param Request $request
- * @return mixed[]|\mixed[][]|\string[][]
- * @throws ApiException
- * @throws GuzzleException
+ * @return array>
+ * @throws SendyException
*/
- private function performRequest(Request $request): array
+ private function performRequest(Request $request, bool $checkAccessToken = true): array
{
- try {
- $response = $this->getClient()->send($request);
+ if ($checkAccessToken) {
+ $this->checkOrAcquireAccessToken();
- return $this->parseResponse($response);
- } catch (\Exception $e) {
- $this->parseException($e);
+ $request->setHeader('Authorization', "Bearer {$this->accessToken}");
}
+
+ $response = $this->getTransport()->send($request);
+
+ return $this->parseResponse($response, $request);
}
/**
- * @param ResponseInterface $response
* @return array>
- * @throws ApiException
+ * @throws SendyException
*/
- public function parseResponse(ResponseInterface $response): array
+ public function parseResponse(Response $response, Request $request): array
{
$this->extractRateLimits($response);
+ $this->extractSendyHeaders($response);
+
+ if ($exception = $response->toException($request)) {
+ throw $exception;
+ }
if ($response->getStatusCode() === 204) {
return [];
}
- Message::rewindBody($response);
-
- $responseBody = $response->getBody()->getContents();
-
- $json = json_decode($responseBody, true);
-
- if (json_last_error() !== JSON_ERROR_NONE) {
- throw new ApiException("Json decode failed. Got: " . $responseBody);
- }
+ $json = $response->getDecodedBody();
if (array_key_exists('data', $json)) {
if (array_key_exists('meta', $json)) {
@@ -500,52 +409,25 @@ public function parseResponse(ResponseInterface $response): array
return $json;
}
- /**
- * @param \Exception $e
- * @return void
- * @throws ApiException
- */
- public function parseException(\Exception $e): void
+ private function extractRateLimits(Response $response): void
{
- if (! $e instanceof BadResponseException) {
- throw new ApiException($e->getMessage(), 0, $e);
- }
-
- $this->extractRateLimits($e->getResponse());
-
- if ($e instanceof ServerException) {
- throw new ApiException($e->getMessage(), $e->getResponse()->getStatusCode());
- }
-
- $response = $e->getResponse();
-
- Message::rewindBody($response);
-
- $responseBody = $response->getBody()->getContents();
-
- $json = json_decode($responseBody, true);
-
- if (json_last_error() !== JSON_ERROR_NONE) {
- throw new ApiException("Json decode failed. Got: " . $responseBody);
- }
-
- if (array_key_exists('errors', $json)) {
- throw new ApiException($json['message'], 0, $e, $json['errors']);
- }
-
- throw new ApiException($json['message']);
+ $this->rateLimits = RateLimits::buildFromResponse($response);
}
- private function extractRateLimits(ResponseInterface $response): void
+ /**
+ * Extract the x-sendy-* headers from the response.
+ */
+ private function extractSendyHeaders(Response $response): void
{
- $this->rateLimits = RateLimits::buildFromResponse($response);
+ $this->sendyHeaders = array_filter(
+ $response->getHeaders(),
+ fn(string $key) => substr($key, 0, 8) === 'x-sendy-',
+ ARRAY_FILTER_USE_KEY,
+ );
}
/**
* Magic method to fetch the resource object
- *
- * @param string $resource
- * @return Resource
*/
public function __get(string $resource): Resource
{
diff --git a/src/Exceptions/ClientException.php b/src/Exceptions/ClientException.php
new file mode 100644
index 0000000..5dd9e67
--- /dev/null
+++ b/src/Exceptions/ClientException.php
@@ -0,0 +1,8 @@
+> */
+ private array $errors = [];
+
+ /**
+ * @param array> $errors
+ */
+ final public function __construct(
+ string $message = '',
+ int $code = 0,
+ ?\Throwable $previous = null,
+ array $errors = []
+ ) {
+ $this->errors = $errors;
+
+ parent::__construct($message, $code, $previous);
+ }
+
+ /**
+ * @return array>
+ */
+ public function getErrors(): array
+ {
+ return $this->errors;
+ }
+}
diff --git a/src/Exceptions/HttpException.php b/src/Exceptions/HttpException.php
new file mode 100644
index 0000000..3fbfdd7
--- /dev/null
+++ b/src/Exceptions/HttpException.php
@@ -0,0 +1,46 @@
+getSummary(),
+ $response->getStatusCode(),
+ null,
+ $response->getErrors(),
+ );
+
+ $exception->request = $request;
+ $exception->response = $response;
+
+ return $exception;
+ }
+
+ public function getRequest(): Request
+ {
+ return $this->request;
+ }
+
+ public function getResponse(): Response
+ {
+ return $this->response;
+ }
+}
diff --git a/src/Exceptions/JsonException.php b/src/Exceptions/JsonException.php
new file mode 100644
index 0000000..323d7dd
--- /dev/null
+++ b/src/Exceptions/JsonException.php
@@ -0,0 +1,14 @@
+>
+ */
+ public function getErrors(): array;
+}
diff --git a/src/Exceptions/ServerException.php b/src/Exceptions/ServerException.php
new file mode 100644
index 0000000..e950db4
--- /dev/null
+++ b/src/Exceptions/ServerException.php
@@ -0,0 +1,8 @@
+
+ */
+ private array $headers;
+
+ private ?string $body;
+
+ /**
+ * @param array $headers
+ */
+ public function __construct(
+ string $method,
+ string $url,
+ array $headers = [],
+ ?string $body = null
+ ) {
+ $this->method = strtoupper($method);
+ $this->url = $url;
+ $this->headers = array_change_key_case($headers, CASE_LOWER);
+ $this->body = $body;
+ }
+
+ public function getMethod(): string
+ {
+ return $this->method;
+ }
+
+ public function getUrl(): string
+ {
+ if (substr($this->url, 0, 1) === '/') {
+ return Connection::BASE_URL . $this->url;
+ }
+
+ return $this->url;
+ }
+
+ /**
+ * @return array
+ */
+ public function getHeaders(): array
+ {
+ return $this->headers;
+ }
+
+ public function getBody(): ?string
+ {
+ return $this->body;
+ }
+
+ public function setHeader(string $name, string $value): void
+ {
+ $this->headers[strtolower($name)] = $value;
+ }
+}
diff --git a/src/Http/Response.php b/src/Http/Response.php
new file mode 100644
index 0000000..0717c9e
--- /dev/null
+++ b/src/Http/Response.php
@@ -0,0 +1,180 @@
+ 'Continue',
+ 101 => 'Switching Protocols',
+ 102 => 'Processing',
+ 200 => 'OK',
+ 201 => 'Created',
+ 202 => 'Accepted',
+ 203 => 'Non-Authoritative Information',
+ 204 => 'No Content',
+ 205 => 'Reset Content',
+ 206 => 'Partial Content',
+ 207 => 'Multi-status',
+ 208 => 'Already Reported',
+ 300 => 'Multiple Choices',
+ 301 => 'Moved Permanently',
+ 302 => 'Found',
+ 303 => 'See Other',
+ 304 => 'Not Modified',
+ 305 => 'Use Proxy',
+ 306 => 'Switch Proxy',
+ 307 => 'Temporary Redirect',
+ 308 => 'Permanent Redirect',
+ 400 => 'Bad Request',
+ 401 => 'Unauthorized',
+ 402 => 'Payment Required',
+ 403 => 'Forbidden',
+ 404 => 'Not Found',
+ 405 => 'Method Not Allowed',
+ 406 => 'Not Acceptable',
+ 407 => 'Proxy Authentication Required',
+ 408 => 'Request Time-out',
+ 409 => 'Conflict',
+ 410 => 'Gone',
+ 411 => 'Length Required',
+ 412 => 'Precondition Failed',
+ 413 => 'Request Entity Too Large',
+ 414 => 'Request-URI Too Large',
+ 415 => 'Unsupported Media Type',
+ 416 => 'Requested range not satisfiable',
+ 417 => 'Expectation Failed',
+ 418 => 'I\'m a teapot',
+ 422 => 'Unprocessable Entity',
+ 423 => 'Locked',
+ 424 => 'Failed Dependency',
+ 425 => 'Unordered Collection',
+ 426 => 'Upgrade Required',
+ 428 => 'Precondition Required',
+ 429 => 'Too Many Requests',
+ 431 => 'Request Header Fields Too Large',
+ 451 => 'Unavailable For Legal Reasons',
+ 500 => 'Internal Server Error',
+ 501 => 'Not Implemented',
+ 502 => 'Bad Gateway',
+ 503 => 'Service Unavailable',
+ 504 => 'Gateway Time-out',
+ 505 => 'HTTP Version not supported',
+ 506 => 'Variant Also Negotiates',
+ 507 => 'Insufficient Storage',
+ 508 => 'Loop Detected',
+ 510 => 'Not Extended',
+ 511 => 'Network Authentication Required',
+ ];
+
+ private int $statusCode;
+
+ /**
+ * @var array>
+ */
+ private array $headers;
+
+ private string $body;
+
+ /**
+ * @param array|string> $headers
+ */
+ public function __construct(int $statusCode, array $headers, string $body)
+ {
+ $this->statusCode = $statusCode;
+ $this->headers = array_map(
+ fn($value) => is_array($value) ? $value : [$value],
+ array_change_key_case($headers, CASE_LOWER),
+ );
+ $this->body = $body;
+ }
+
+ public function getStatusCode(): int
+ {
+ return $this->statusCode;
+ }
+
+ /**
+ * @return array>
+ */
+ public function getHeaders(): array
+ {
+ return $this->headers;
+ }
+
+ public function getBody(): string
+ {
+ return $this->body;
+ }
+
+ /**
+ * Decode the JSON body of the response.
+ *
+ * @return array
+ * @throws JsonException If the body is not valid JSON.
+ */
+ public function getDecodedBody(): array
+ {
+ try {
+ return json_decode($this->body, true, 512, JSON_THROW_ON_ERROR);
+ } catch (\JsonException $e) {
+ throw new JsonException("Json decode failed. Got: {$this->body}", $this->statusCode, $e);
+ }
+ }
+
+ /**
+ * Get a summary of the response, suitable for use in exception messages.
+ */
+ public function getSummary(): string
+ {
+ $summary = $this->statusCode . ' - ' . (self::PHRASES[$this->statusCode] ?? 'Unknown Status');
+ $decodedBody = json_decode($this->body, true);
+
+ if (isset($decodedBody['error_description'], $decodedBody['hint'])) {
+ $summary .= ": {$decodedBody['error_description']} ({$decodedBody['hint']})";
+ } elseif (isset($decodedBody['message'])) {
+ $summary .= ": {$decodedBody['message']}";
+ }
+
+ return $summary;
+ }
+
+ /**
+ * Extract errors from the response body.
+ *
+ * @return array>
+ */
+ public function getErrors(): array
+ {
+ $data = json_decode($this->body, true);
+
+ return $data['errors'] ?? [];
+ }
+
+ public function toException(Request $request): ?HttpException
+ {
+ if ($this->statusCode === 422) {
+ return ValidationException::fromRequestAndResponse(
+ $request,
+ $this,
+ $this->getDecodedBody()['message'] ?? 'Validation failed',
+ );
+ }
+
+ if ($this->statusCode >= 400 && $this->statusCode < 500) {
+ return ClientException::fromRequestAndResponse($request, $this);
+ }
+
+ if ($this->statusCode >= 500) {
+ return ServerException::fromRequestAndResponse($request, $this);
+ }
+
+ return null;
+ }
+}
diff --git a/src/Http/Transport/CurlTransport.php b/src/Http/Transport/CurlTransport.php
new file mode 100644
index 0000000..9e87e78
--- /dev/null
+++ b/src/Http/Transport/CurlTransport.php
@@ -0,0 +1,92 @@
+getUrl());
+ curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, $request->getMethod());
+ curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($curlHandle, CURLOPT_HEADER, true);
+ curl_setopt($curlHandle, CURLOPT_HTTPHEADER, $this->formatHeaders($request->getHeaders()));
+
+ if ($body = $request->getBody()) {
+ curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $body);
+ }
+
+ $response = curl_exec($curlHandle);
+
+ if ($response === false) {
+ $error = curl_error($curlHandle);
+
+ throw new TransportException('cURL error: ' . $error);
+ }
+
+ $headerSize = curl_getinfo($curlHandle, CURLINFO_HEADER_SIZE);
+ $headers = substr($response, 0, $headerSize);
+ $body = substr($response, $headerSize);
+ $statusCode = curl_getinfo($curlHandle, CURLINFO_RESPONSE_CODE);
+
+ return new Response($statusCode, $this->parseHeaders($headers), $body);
+ } finally {
+ curl_close($curlHandle);
+ }
+ }
+
+ public function getUserAgent(): string
+ {
+ if (! extension_loaded('curl')) {
+ return 'curl';
+ }
+
+ return 'curl/' . curl_version()['version'];
+ }
+
+ /**
+ * Formats headers for cURL.
+ *
+ * @param array $headers
+ * @return list
+ */
+ private function formatHeaders(array $headers): array
+ {
+ $formatted = [];
+ foreach ($headers as $name => $value) {
+ $formatted[] = $name . ': ' . $value;
+ }
+ return $formatted;
+ }
+
+ /**
+ * Parses the raw header string into an associative array.
+ *
+ * @return array>
+ */
+ private function parseHeaders(string $rawHeaders): array
+ {
+ $headers = [];
+ $lines = explode("\r\n", $rawHeaders);
+
+ foreach ($lines as $line) {
+ if (strpos($line, ':') !== false) {
+ [$name, $value] = explode(': ', $line, 2);
+ $headers[strtolower($name)][] = $value;
+ }
+ }
+
+ return $headers;
+ }
+}
diff --git a/src/Http/Transport/MockTransport.php b/src/Http/Transport/MockTransport.php
new file mode 100644
index 0000000..15ea0b5
--- /dev/null
+++ b/src/Http/Transport/MockTransport.php
@@ -0,0 +1,38 @@
+
+ */
+ private array $requests = [];
+
+ public function __construct(?Response $response = null)
+ {
+ $this->response = $response ?? new Response(200, [], json_encode(['success' => true]));
+ }
+
+ public function send(Request $request): Response
+ {
+ $this->requests[] = $request;
+
+ return $this->response;
+ }
+
+ public function getUserAgent(): string
+ {
+ return 'MockTransport/1.0';
+ }
+
+ public function getLastRequest(): ?Request
+ {
+ return end($this->requests) ?: null;
+ }
+}
diff --git a/src/Http/Transport/Psr18Transport.php b/src/Http/Transport/Psr18Transport.php
new file mode 100644
index 0000000..fc75bd5
--- /dev/null
+++ b/src/Http/Transport/Psr18Transport.php
@@ -0,0 +1,89 @@
+client = $client;
+ $this->requestFactory = $requestFactory;
+ $this->streamFactory = $streamFactory;
+ $this->uriFactory = $uriFactory;
+ $this->userAgent = $userAgent;
+ }
+
+ public function send(Request $request): Response
+ {
+ $psrRequest = $this->requestFactory->createRequest(
+ $request->getMethod(),
+ $this->uriFactory->createUri($request->getUrl()),
+ );
+
+ foreach ($request->getHeaders() as $name => $value) {
+ $psrRequest = $psrRequest->withHeader($name, $value);
+ }
+
+ if ($body = $request->getBody()) {
+ $psrRequest = $psrRequest->withBody(
+ $this->streamFactory->createStream($body),
+ );
+ }
+
+ try {
+ $psrResponse = $this->client->sendRequest($psrRequest);
+ } catch (\Throwable $e) {
+ throw new TransportException($e->getMessage(), $e->getCode(), $e);
+ }
+
+ return new Response(
+ $psrResponse->getStatusCode(),
+ $psrResponse->getHeaders(),
+ (string) $psrResponse->getBody(),
+ );
+ }
+
+ public function getClient(): ClientInterface
+ {
+ return $this->client;
+ }
+
+ public function getRequestFactory(): RequestFactoryInterface
+ {
+ return $this->requestFactory;
+ }
+
+ public function getStreamFactory(): StreamFactoryInterface
+ {
+ return $this->streamFactory;
+ }
+
+ public function getUriFactory(): UriFactoryInterface
+ {
+ return $this->uriFactory;
+ }
+
+ public function getUserAgent(): string
+ {
+ return $this->userAgent;
+ }
+}
diff --git a/src/Http/Transport/TransportFactory.php b/src/Http/Transport/TransportFactory.php
new file mode 100644
index 0000000..5a1d2c3
--- /dev/null
+++ b/src/Http/Transport/TransportFactory.php
@@ -0,0 +1,91 @@
+ $meta
- * @return Meta
*/
public static function buildFromResponse(array $meta): Meta
{
@@ -58,7 +48,7 @@ public static function buildFromResponse(array $meta): Meta
$meta['path'],
$meta['per_page'],
$meta['to'],
- $meta['total']
+ $meta['total'],
);
}
}
diff --git a/src/RateLimits.php b/src/RateLimits.php
index 661d954..82000fc 100644
--- a/src/RateLimits.php
+++ b/src/RateLimits.php
@@ -2,7 +2,7 @@
namespace Sendy\Api;
-use Psr\Http\Message\ResponseInterface;
+use Sendy\Api\Http\Response;
final class RateLimits
{
@@ -14,12 +14,6 @@ final class RateLimits
public int $reset;
- /**
- * @param int $retryAfter
- * @param int $limit
- * @param int $remaining
- * @param int $reset
- */
public function __construct(int $retryAfter, int $limit, int $remaining, int $reset)
{
$this->retryAfter = $retryAfter;
@@ -28,13 +22,15 @@ public function __construct(int $retryAfter, int $limit, int $remaining, int $re
$this->reset = $reset;
}
- public static function buildFromResponse(ResponseInterface $response): RateLimits
+ public static function buildFromResponse(Response $response): RateLimits
{
+ $headers = $response->getHeaders();
+
return new self(
- (int) implode("", $response->getHeader('Retry-After')),
- (int) implode("", $response->getHeader('X-RateLimit-Limit')),
- (int) implode("", $response->getHeader('X-RateLimit-Remaining')),
- (int) implode("", $response->getHeader('X-RateLimit-Reset'))
+ (int) ($headers['retry-after'][0] ?? 0),
+ (int) ($headers['x-ratelimit-limit'][0] ?? 0),
+ (int) ($headers['x-ratelimit-remaining'][0] ?? 0),
+ (int) ($headers['x-ratelimit-reset'][0] ?? 0),
);
}
}
diff --git a/src/Resources/Carrier.php b/src/Resources/Carrier.php
index 150d3eb..b9983d2 100644
--- a/src/Resources/Carrier.php
+++ b/src/Resources/Carrier.php
@@ -2,8 +2,7 @@
namespace Sendy\Api\Resources;
-use GuzzleHttp\Exception\GuzzleException;
-use Sendy\Api\ApiException;
+use Sendy\Api\Exceptions\SendyException;
final class Carrier extends Resource
{
@@ -13,8 +12,7 @@ final class Carrier extends Resource
* Display all carriers in a list.
*
* @return array>
- * @throws GuzzleException
- * @throws ApiException
+ * @throws SendyException
* @link https://app.sendy.nl/api/docs#tag/Carriers/operation/api.carriers.index
*/
public function list(): array
@@ -29,8 +27,7 @@ public function list(): array
*
* @param int $id The id of the carrier
* @return array>
- * @throws ApiException
- * @throws GuzzleException
+ * @throws SendyException
* @link https://app.sendy.nl/api/docs#tag/Carriers/operation/api.carriers.show
*/
public function get(int $id): array
diff --git a/src/Resources/Label.php b/src/Resources/Label.php
index e19625d..df8274b 100644
--- a/src/Resources/Label.php
+++ b/src/Resources/Label.php
@@ -2,8 +2,7 @@
namespace Sendy\Api\Resources;
-use GuzzleHttp\Exception\GuzzleException;
-use Sendy\Api\ApiException;
+use Sendy\Api\Exceptions\SendyException;
final class Label extends Resource
{
@@ -17,8 +16,7 @@ final class Label extends Resource
* @param null|'top-left'|'top-right'|'bottom-left'|'bottom-right' $startLocation Where to start combining the
* labels. Only used when $paperType is set to A4.
* @return array>
- * @throws GuzzleException
- * @throws ApiException
+ * @throws SendyException
* @link https://app.sendy.nl/api/docs#tag/Documents/operation/api.labels.index
*/
public function get(array $shipmentIds, ?string $paperType = null, ?string $startLocation = null): array
diff --git a/src/Resources/Me.php b/src/Resources/Me.php
index a86411f..1860e4d 100644
--- a/src/Resources/Me.php
+++ b/src/Resources/Me.php
@@ -2,6 +2,8 @@
namespace Sendy\Api\Resources;
+use Sendy\Api\Exceptions\SendyException;
+
final class Me extends Resource
{
/**
@@ -11,6 +13,7 @@ final class Me extends Resource
*
* @link https://app.sendy.nl/api/docs#tag/User/operation/api.me
* @return array>
+ * @throws SendyException
*/
public function get(): array
{
diff --git a/src/Resources/Parcelshop.php b/src/Resources/Parcelshop.php
index e13eecc..f45f31f 100644
--- a/src/Resources/Parcelshop.php
+++ b/src/Resources/Parcelshop.php
@@ -2,10 +2,9 @@
namespace Sendy\Api\Resources;
-use GuzzleHttp\Exception\GuzzleException;
-use Sendy\Api\ApiException;
+use Sendy\Api\Exceptions\SendyException;
-class Parcelshop extends Resource
+final class Parcelshop extends Resource
{
/**
* List parcel shops
@@ -18,8 +17,7 @@ class Parcelshop extends Resource
* @param string $country The country code of the location.
* @param string|null $postalCode The postal code of the location.
* @return array>
- * @throws GuzzleException
- * @throws ApiException
+ * @throws SendyException
* @link https://app.sendy.nl/api/docs#tag/Parcel-shops
*/
public function list(
diff --git a/src/Resources/Service.php b/src/Resources/Service.php
index 4f69d8a..950cdff 100644
--- a/src/Resources/Service.php
+++ b/src/Resources/Service.php
@@ -2,10 +2,9 @@
namespace Sendy\Api\Resources;
-use GuzzleHttp\Exception\GuzzleException;
-use Sendy\Api\ApiException;
+use Sendy\Api\Exceptions\SendyException;
-class Service extends Resource
+final class Service extends Resource
{
/**
* List services associated with a carrier
@@ -14,8 +13,7 @@ class Service extends Resource
*
* @param int $carrierId The id of the carrier
* @return array>
- * @throws GuzzleException
- * @throws ApiException
+ * @throws SendyException
* @link https://app.sendy.nl/api/docs#tag/Services/operation/api.carriers.services.index
*/
public function list(int $carrierId): array
diff --git a/src/Resources/Shipment.php b/src/Resources/Shipment.php
index 4999adc..8d143cb 100644
--- a/src/Resources/Shipment.php
+++ b/src/Resources/Shipment.php
@@ -2,8 +2,7 @@
namespace Sendy\Api\Resources;
-use GuzzleHttp\Exception\GuzzleException;
-use Sendy\Api\ApiException;
+use Sendy\Api\Exceptions\SendyException;
use Sendy\Api\Meta;
final class Shipment extends Resource
@@ -15,8 +14,7 @@ final class Shipment extends Resource
*
* @param int $page The page number to fetch
* @return array>
- * @throws GuzzleException
- * @throws ApiException
+ * @throws SendyException
* @link https://app.sendy.nl/api/docs#tag/Shipments/operation/api.shipments.index
* @see Meta
*/
@@ -32,8 +30,7 @@ public function list(int $page = 1): array
*
* @param string $id The UUID of the shipment
* @return array>
- * @throws GuzzleException
- * @throws ApiException
+ * @throws SendyException
* @link https://app.sendy.nl/api/docs#tag/Shipments/operation/api.shipments.show
*/
public function get(string $id): array
@@ -49,8 +46,7 @@ public function get(string $id): array
* @param string $id The UUID of the shipment
* @param array> $data
* @return array>
- * @throws GuzzleException
- * @throws ApiException
+ * @throws SendyException
*/
public function update(string $id, array $data): array
{
@@ -68,8 +64,7 @@ public function update(string $id, array $data): array
*
* @param string $id The UUID of the shipment
* @return array>
- * @throws GuzzleException
- * @throws ApiException
+ * @throws SendyException
* @link https://app.sendy.nl/api/docs#tag/Shipments/operation/api.shipments.destroy
*/
public function delete(string $id): array
@@ -85,8 +80,7 @@ public function delete(string $id): array
* @param array> $data
* @param bool $generateDirectly Should the shipment be generated right away. This will increase the response time.
* @return array>
- * @throws ApiException
- * @throws GuzzleException
+ * @throws SendyException
* @link https://app.sendy.nl/api/docs#tag/Shipments/operation/api.shipments.preference
* @see ShippingPreference
*/
@@ -100,8 +94,7 @@ public function createFromPreference(array $data, bool $generateDirectly = true)
*
* @param array> $data
* @return array>
- * @throws ApiException
- * @throws GuzzleException
+ * @throws SendyException
* @link https://app.sendy.nl/api/docs#tag/Shipments/operation/api.shipments.smart-rule
*/
public function createWithSmartRules(array $data): array
@@ -117,8 +110,7 @@ public function createWithSmartRules(array $data): array
* @param string $id The UUID of the shipment
* @param bool $asynchronous Whether the shipping label should be generated asynchronously
* @return array>
- * @throws ApiException
- * @throws GuzzleException
+ * @throws SendyException
* @link https://app.sendy.nl/api/docs#tag/Shipments/operation/api.shipments.generate
*/
public function generate(string $id, bool $asynchronous = true): array
@@ -133,8 +125,7 @@ public function generate(string $id, bool $asynchronous = true): array
*
* @param string $id The UUID of the shipment
* @return array>
- * @throws ApiException
- * @throws GuzzleException
+ * @throws SendyException
* @link https://app.sendy.nl/api/docs#tag/Documents/operation/api.shipments.labels.index
*/
public function labels(string $id): array
@@ -147,10 +138,8 @@ public function labels(string $id): array
*
* Get a PDF with the export documents for a specific shipment
*
- * @param string $id
* @return array>
- * @throws ApiException
- * @throws GuzzleException
+ * @throws SendyException
* @link https://app.sendy.nl/api/docs#tag/Documents/operation/api.shipments.documents.index
*/
public function documents(string $id): array
diff --git a/src/Resources/ShippingPreference.php b/src/Resources/ShippingPreference.php
index 6c0f6f7..5b984e3 100644
--- a/src/Resources/ShippingPreference.php
+++ b/src/Resources/ShippingPreference.php
@@ -2,10 +2,9 @@
namespace Sendy\Api\Resources;
-use GuzzleHttp\Exception\GuzzleException;
-use Sendy\Api\ApiException;
+use Sendy\Api\Exceptions\SendyException;
-class ShippingPreference extends Resource
+final class ShippingPreference extends Resource
{
/**
* List all shipping preferences
@@ -14,8 +13,7 @@ class ShippingPreference extends Resource
*
* @link https://app.sendy.nl/api/docs#tag/Shipping-preferences
* @return array>
- * @throws GuzzleException
- * @throws ApiException
+ * @throws SendyException
*/
public function list(): array
{
diff --git a/src/Resources/Shop.php b/src/Resources/Shop.php
index fed9a03..a5646f3 100644
--- a/src/Resources/Shop.php
+++ b/src/Resources/Shop.php
@@ -2,10 +2,9 @@
namespace Sendy\Api\Resources;
-use GuzzleHttp\Exception\GuzzleException;
-use Sendy\Api\ApiException;
+use Sendy\Api\Exceptions\SendyException;
-class Shop extends Resource
+final class Shop extends Resource
{
/**
* List all shops
@@ -14,8 +13,7 @@ class Shop extends Resource
*
* @link https://app.sendy.nl/api/docs#tag/Shops/operation/api.shops.index
* @return array>
- * @throws GuzzleException
- * @throws ApiException
+ * @throws SendyException
*/
public function list(): array
{
@@ -28,10 +26,8 @@ public function list(): array
* Get a specific shop by its UUID
*
* @link https://app.sendy.nl/api/docs#tag/Shops/operation/api.shops.show
- * @param string $id
* @return array>
- * @throws ApiException
- * @throws GuzzleException
+ * @throws SendyException
*/
public function get(string $id): array
{
diff --git a/src/Resources/Webhook.php b/src/Resources/Webhook.php
index 8e690ba..2d51792 100644
--- a/src/Resources/Webhook.php
+++ b/src/Resources/Webhook.php
@@ -2,14 +2,15 @@
namespace Sendy\Api\Resources;
+use Sendy\Api\Exceptions\SendyException;
+
final class Webhook extends Resource
{
/**
* List all webhooks
*
+ * @throws SendyException
* @return array>
- * @throws \GuzzleHttp\Exception\GuzzleException
- * @throws \Sendy\Api\ApiException
* @link https://app.sendy.nl/api/docs#tag/Webhooks/operation/api.webhooks.index
*/
public function list(): array
@@ -21,9 +22,9 @@ public function list(): array
* Create a new webhook
*
* @param array> $data
+ *
+ * @throws SendyException
* @return array>
- * @throws \GuzzleHttp\Exception\GuzzleException
- * @throws \Sendy\Api\ApiException
* @link https://app.sendy.nl/api/docs#tag/Webhooks/operation/api.webhooks.store
*/
public function create(array $data): array
@@ -35,9 +36,9 @@ public function create(array $data): array
* Delete a webhook
*
* @param string $id The ID of the webhook
+ *
+ * @throws SendyException
* @return array
- * @throws \GuzzleHttp\Exception\GuzzleException
- * @throws \Sendy\Api\ApiException
*/
public function delete(string $id): array
{
@@ -49,9 +50,9 @@ public function delete(string $id): array
*
* @param string $id The id of the webhook to be updated
* @param array> $data
+ *
+ * @throws SendyException
* @return array>
- * @throws \GuzzleHttp\Exception\GuzzleException
- * @throws \Sendy\Api\ApiException
*/
public function update(string $id, array $data): array
{
diff --git a/tests/ConnectionTest.php b/tests/ConnectionTest.php
index 1014666..8d4b75c 100644
--- a/tests/ConnectionTest.php
+++ b/tests/ConnectionTest.php
@@ -2,17 +2,13 @@
namespace Sendy\Api\Tests;
-use GuzzleHttp\Client;
-use GuzzleHttp\Exception\ClientException;
-use GuzzleHttp\Exception\ServerException;
-use GuzzleHttp\Handler\MockHandler;
-use GuzzleHttp\HandlerStack;
-use GuzzleHttp\Middleware;
-use GuzzleHttp\Psr7\Request;
-use GuzzleHttp\Psr7\Response;
+use PHPUnit\Framework\TestCase;
use Sendy\Api\ApiException;
use Sendy\Api\Connection;
-use PHPUnit\Framework\TestCase;
+use Sendy\Api\Http\Request;
+use Sendy\Api\Http\Response;
+use Sendy\Api\Http\Transport\MockTransport;
+use Sendy\Api\Http\Transport\TransportFactory;
use Sendy\Api\Meta;
use Sendy\Api\Resources\Me;
@@ -20,27 +16,27 @@ class ConnectionTest extends TestCase
{
public function testUserAgentIsSet(): void
{
- $connection = new Connection();
+ $phpVersion = phpversion();
+ $curlVersion = curl_version()['version'];
+ $connection = $this->createConnection();
$this->assertEquals(
- sprintf('Sendy/1.0.2 PHP/%s', phpversion()),
- $connection->getClient()->getConfig('headers')['User-Agent']
+ "SendySDK/3.0.0 PHP/{$phpVersion} curl/{$curlVersion}",
+ $connection->createRequest('GET', '/')->getHeaders()['user-agent'],
);
- $connection = new Connection();
+ $connection = $this->createConnection();
$connection->setUserAgentAppendix('WooCommerce/6.2');
-
$this->assertEquals(
- sprintf('Sendy/1.0.2 PHP/%s WooCommerce/6.2', phpversion()),
- $connection->getClient()->getConfig('headers')['User-Agent']
+ "SendySDK/3.0.0 PHP/{$phpVersion} curl/{$curlVersion} WooCommerce/6.2",
+ $connection->createRequest('GET', '/')->getHeaders()['user-agent'],
);
- $connection = new Connection();
+ $connection = $this->createConnection();
$connection->setOauthClient(true);
-
$this->assertEquals(
- sprintf('Sendy/1.0.2 PHP/%s OAuth/2.0', phpversion()),
- $connection->getClient()->getConfig('headers')['User-Agent']
+ "SendySDK/3.0.0 PHP/{$phpVersion} OAuth/2.0 curl/{$curlVersion}",
+ $connection->createRequest('GET', '/')->getHeaders()['user-agent'],
);
}
@@ -48,11 +44,11 @@ public function testTokenExpires(): void
{
$connection = new Connection();
- $connection->setTokenExpires(time() - 3600);
+ $connection->setTokenExpires(time() - 600);
$this->assertTrue($connection->tokenHasExpired());
- $connection->setTokenExpires(time() + 60);
+ $connection->setTokenExpires(time() + 600);
$this->assertFalse($connection->tokenHasExpired());
}
@@ -86,7 +82,7 @@ public function testAuthorizationUrlIsBuilt(): void
// phpcs:disable
$this->assertEquals(
'https://app.sendy.nl/oauth/authorize?client_id=client-id&redirect_uri=https%3A%2F%2Fexample.com&response_type=code&state=state',
- $connection->getAuthorizationUrl()
+ $connection->getAuthorizationUrl(),
);
// phpcs:enable
}
@@ -95,9 +91,9 @@ public function testParseResponseReturnsEmptyArrayWhenResponseHasNoContent(): vo
{
$connection = new Connection();
- $response = new Response(204);
+ $response = new Response(204, [], '');
- $this->assertEquals([], $connection->parseResponse($response));
+ $this->assertEquals([], $connection->parseResponse($response, new Request('GET', '/foo')));
}
public function testParseResponseThrowsApiExceptionWithInvalidJson(): void
@@ -109,7 +105,7 @@ public function testParseResponseThrowsApiExceptionWithInvalidJson(): void
$this->expectException(ApiException::class);
$this->expectExceptionMessage('Json decode failed. Got: InvalidJson');
- $connection->parseResponse($response);
+ $connection->parseResponse($response, new Request('GET', '/foo'));
}
public function testParseResponseExtractsMeta(): void
@@ -125,13 +121,13 @@ public function testParseResponseExtractsMeta(): void
'path' => '/foo/bar',
'per_page' => 25,
'to' => 25,
- 'total' => 27
+ 'total' => 27,
],
];
$response = new Response(200, [], json_encode($responseBody));
- $this->assertEquals([], $connection->parseResponse($response));
+ $this->assertEquals([], $connection->parseResponse($response, new Request('GET', '/foo')));
$this->assertInstanceOf(Meta::class, $connection->meta);
}
@@ -142,12 +138,12 @@ public function testParseResponseUnwrapsData(): void
$responseBody = [
'data' => [
'foo' => 'bar',
- ]
+ ],
];
$response = new Response(200, [], json_encode($responseBody));
- $this->assertEquals(['foo' => 'bar'], $connection->parseResponse($response));
+ $this->assertEquals(['foo' => 'bar'], $connection->parseResponse($response, new Request('GET', '/foo')));
$responseBody = [
'foo' => 'bar',
@@ -155,105 +151,22 @@ public function testParseResponseUnwrapsData(): void
$response = new Response(200, [], json_encode($responseBody));
- $this->assertEquals(['foo' => 'bar'], $connection->parseResponse($response));
- }
-
- public function testParseExceptionHandlesExceptions(): void
- {
- $exception = new \Exception('RandomException');
-
- $connection = new Connection();
-
- $this->expectException(ApiException::class);
- $this->expectExceptionMessage('RandomException');
-
- $connection->parseException($exception);
- }
-
- public function testParseExceptionHandlesServerExceptions(): void
- {
- $exception = new ServerException('Server exception', new Request('GET', '/'), new Response(500));
-
- $connection = new Connection();
-
- $this->expectException(ApiException::class);
- $this->expectExceptionMessage('Server exception');
-
- $connection->parseException($exception);
- }
-
- public function testParseExceptionHandlesInvalidJson(): void
- {
- $exception = new ClientException('Foo', new Request('GET', '/'), new Response(422, [], 'InvalidJson'));
-
- $connection = new Connection();
-
- $this->expectException(ApiException::class);
- $this->expectExceptionMessage('Json decode failed. Got: InvalidJson');
-
- $connection->parseException($exception);
- }
-
- public function testParseExceptionHandlesErrorsMessages(): void
- {
- $exception = new ClientException(
- 'Foo',
- new Request('GET', '/'),
- new Response(422, [], json_encode(['message' => 'Error message']))
- );
-
- $connection = new Connection();
-
- $this->expectException(ApiException::class);
- $this->expectExceptionMessage('Error message');
-
- $connection->parseException($exception);
- }
-
- public function testParseExceptionSetsErrors(): void
- {
- $exception = new ClientException(
- 'Foo',
- new Request('GET', '/'),
- new Response(422, [], json_encode(['message' => 'Error message', 'errors' => ['First', 'Second']]))
- );
-
- $connection = new Connection();
-
- try {
- $connection->parseException($exception);
- } catch (ApiException $e) {
- $this->assertSame(['First', 'Second'], $e->getErrors());
- }
-
- $exception = new ClientException(
- 'Foo',
- new Request('GET', '/'),
- new Response(422, [], json_encode(['message' => 'Error message']))
- );
-
- try {
- $connection->parseException($exception);
- } catch (ApiException $e) {
- $this->assertSame([], $e->getErrors());
- }
+ $this->assertEquals(['foo' => 'bar'], $connection->parseResponse($response, new Request('GET', '/foo')));
}
public function testTokensAreAcquiredWithAuthorizationCode(): void
{
$connection = new Connection();
- $mockHandler = new MockHandler([
+ $transport = new MockTransport(
new Response(200, [], json_encode([
'access_token' => 'FromAuthCode',
'refresh_token' => 'RefreshToken',
'expires_in' => 3600,
- ]))
- ]);
-
- $client = new Client(['handler' => HandlerStack::create($mockHandler)]);
+ ])),
+ );
- $connection->setClient($client);
+ $connection->setTransport($transport);
$connection->setClientId('clientId');
$connection->setRedirectUrl('https://www.example.com/');
@@ -266,24 +179,22 @@ public function testTokensAreAcquiredWithAuthorizationCode(): void
$this->assertEquals('RefreshToken', $connection->getRefreshToken());
$this->assertEquals(time() + 3600, $connection->getTokenExpires());
- $this->assertEquals('https://app.sendy.nl/oauth/token', (string) $mockHandler->getLastRequest()->getUri());
+ $this->assertEquals('https://app.sendy.nl/oauth/token', $transport->getLastRequest()->getUrl());
}
public function testTokensAreAcquiredWithRefreshToken(): void
{
$connection = new Connection();
- $mockHandler = new MockHandler([
+ $transport = new MockTransport(
new Response(200, [], json_encode([
'access_token' => 'NewAccessToken',
'refresh_token' => 'NewRefreshToken',
'expires_in' => 3600,
- ]))
- ]);
-
- $client = new Client(['handler' => HandlerStack::create($mockHandler)]);
+ ])),
+ );
- $connection->setClient($client);
+ $connection->setTransport($transport);
$connection->setClientId('clientId');
$connection->setClientSecret('clientSecret');
@@ -297,7 +208,7 @@ public function testTokensAreAcquiredWithRefreshToken(): void
$this->assertEquals(
'https://app.sendy.nl/oauth/token',
- (string) $mockHandler->getLastRequest()->getUri()
+ $transport->getLastRequest()->getUrl(),
);
}
@@ -305,17 +216,15 @@ public function testTokenUpdateCallbackIsCalled(): void
{
$connection = new Connection();
- $mockHandler = new MockHandler([
+ $transport = new MockTransport(
new Response(200, [], json_encode([
'access_token' => 'NewAccessToken',
'refresh_token' => 'NewRefreshToken',
'expires_in' => 3600,
- ]))
- ]);
-
- $client = new Client(['handler' => HandlerStack::create($mockHandler)]);
+ ])),
+ );
- $connection->setClient($client);
+ $connection->setTransport($transport);
$connection->setClientId('clientId');
$connection->setClientSecret('clientSecret');
@@ -335,28 +244,63 @@ public function testGetRequestIsBuiltAndSent(): void
$connection = new Connection();
$connection->setAccessToken('PersonalAccessToken');
- $mockHandler = new MockHandler([
+ $transport = new MockTransport(
new Response(200, [], json_encode(['foo' => 'bar'])),
+ );
+
+ $connection->setTransport($transport);
+
+ $this->assertEquals(['foo' => 'bar'], $connection->get('/foo'));
+ $this->assertEquals('https://app.sendy.nl/api/foo', $transport->getLastRequest()->getUrl());
+ $this->assertEquals('GET', $transport->getLastRequest()->getMethod());
+ }
+
+ public function testGetRequestWithQueryParametersIsBuiltAndSent(): void
+ {
+ $connection = new Connection();
+ $connection->setAccessToken('PersonalAccessToken');
+
+ $transport = new MockTransport(
new Response(200, [], json_encode(['foo' => 'bar'])),
- new Response(500, [], 'Something went wrong'),
- ]);
+ );
- $client = new Client(['handler' => HandlerStack::create($mockHandler)]);
+ $connection->setTransport($transport);
- $connection->setClient($client);
+ $this->assertEquals(['foo' => 'bar'], $connection->get('/foo', ['baz' => 'foo']));
+ $this->assertEquals('https://app.sendy.nl/api/foo?baz=foo', $transport->getLastRequest()->getUrl());
+ $this->assertEquals('GET', $transport->getLastRequest()->getMethod());
+ }
- $this->assertEquals(['foo' => 'bar'], $connection->get('/foo'));
+ public function testGetRequestWith4xxResponseThrowsClientException(): void
+ {
+ $connection = new Connection();
+ $connection->setAccessToken('PersonalAccessToken');
- $this->assertEquals('/api/foo', (string) $mockHandler->getLastRequest()->getUri());
- $this->assertEquals('GET', $mockHandler->getLastRequest()->getMethod());
+ $transport = new MockTransport(
+ new Response(418, [], '{}'),
+ );
- $connection->get('/foo', ['baz' => 'foo']);
+ $connection->setTransport($transport);
- $this->assertEquals('/api/foo?baz=foo', (string) $mockHandler->getLastRequest()->getUri());
- $this->assertEquals('GET', $mockHandler->getLastRequest()->getMethod());
+ $this->expectException(\Sendy\Api\Exceptions\ClientException::class);
+ $this->expectExceptionMessage('418 - I\'m a teapot');
+ $this->expectExceptionCode(418);
+ $connection->get('/brew-coffee');
+ }
- $this->expectException(ApiException::class);
- $this->expectExceptionMessage('Something went wrong');
+ public function testGetRequestWith5xxResponseThrowsServerException(): void
+ {
+ $connection = new Connection();
+ $connection->setAccessToken('PersonalAccessToken');
+
+ $transport = new MockTransport(
+ new Response(500, [], '{"message": "Something went wrong"}'),
+ );
+
+ $connection->setTransport($transport);
+
+ $this->expectException(\Sendy\Api\Exceptions\ServerException::class);
+ $this->expectExceptionMessage('500 - Internal Server Error: Something went wrong');
$this->expectExceptionCode(500);
$connection->get('/foo');
@@ -367,18 +311,16 @@ public function testDeleteRequestIsBuiltAndSent(): void
$connection = new Connection();
$connection->setAccessToken('PersonalAccessToken');
- $mockHandler = new MockHandler([
+ $transport = new MockTransport(
new Response(204, [], json_encode(['foo' => 'bar'])),
- ]);
-
- $client = new Client(['handler' => HandlerStack::create($mockHandler)]);
+ );
- $connection->setClient($client);
+ $connection->setTransport($transport);
$this->assertEquals([], $connection->delete('/bar'));
- $this->assertEquals('/api/bar', (string) $mockHandler->getLastRequest()->getUri());
- $this->assertEquals('DELETE', $mockHandler->getLastRequest()->getMethod());
+ $this->assertEquals('https://app.sendy.nl/api/bar', $transport->getLastRequest()->getUrl());
+ $this->assertEquals('DELETE', $transport->getLastRequest()->getMethod());
}
public function testPostRequestIsBuiltAndSent(): void
@@ -386,19 +328,17 @@ public function testPostRequestIsBuiltAndSent(): void
$connection = new Connection();
$connection->setAccessToken('PersonalAccessToken');
- $mockHandler = new MockHandler([
+ $transport = new MockTransport(
new Response(201, [], json_encode(['foo' => 'bar'])),
- ]);
-
- $client = new Client(['handler' => HandlerStack::create($mockHandler)]);
+ );
- $connection->setClient($client);
+ $connection->setTransport($transport);
$this->assertEquals(['foo' => 'bar'], $connection->post('/foo', ['request' => 'body']));
- $this->assertEquals('/api/foo', (string) $mockHandler->getLastRequest()->getUri());
- $this->assertEquals('POST', $mockHandler->getLastRequest()->getMethod());
- $this->assertEquals('{"request":"body"}', $mockHandler->getLastRequest()->getBody()->getContents());
+ $this->assertEquals('https://app.sendy.nl/api/foo', $transport->getLastRequest()->getUrl());
+ $this->assertEquals('POST', $transport->getLastRequest()->getMethod());
+ $this->assertEquals('{"request":"body"}', $transport->getLastRequest()->getBody());
}
public function testPutRequestIsBuiltAndSent(): void
@@ -406,18 +346,24 @@ public function testPutRequestIsBuiltAndSent(): void
$connection = new Connection();
$connection->setAccessToken('PersonalAccessToken');
- $mockHandler = new MockHandler([
+ $mockTransport = new MockTransport(
new Response(201, [], json_encode(['foo' => 'bar'])),
- ]);
-
- $client = new Client(['handler' => HandlerStack::create($mockHandler)]);
+ );
- $connection->setClient($client);
+ $connection->setTransport($mockTransport);
$this->assertEquals(['foo' => 'bar'], $connection->put('/foo', ['request' => 'body']));
- $this->assertEquals('/api/foo', (string) $mockHandler->getLastRequest()->getUri());
- $this->assertEquals('PUT', $mockHandler->getLastRequest()->getMethod());
- $this->assertEquals('{"request":"body"}', $mockHandler->getLastRequest()->getBody()->getContents());
+ $this->assertEquals('https://app.sendy.nl/api/foo', $mockTransport->getLastRequest()->getUrl());
+ $this->assertEquals('PUT', $mockTransport->getLastRequest()->getMethod());
+ $this->assertEquals('{"request":"body"}', $mockTransport->getLastRequest()->getBody());
+ }
+
+ private function createConnection(): Connection
+ {
+ $connection = new Connection();
+ $connection->setTransport(TransportFactory::createCurlTransport());
+
+ return $connection;
}
}
diff --git a/tests/RateLimitsTest.php b/tests/RateLimitsTest.php
index 624b66c..8bd081e 100644
--- a/tests/RateLimitsTest.php
+++ b/tests/RateLimitsTest.php
@@ -2,7 +2,7 @@
namespace Sendy\Api\Tests;
-use GuzzleHttp\Psr7\Response;
+use Sendy\Api\Http\Response;
use Sendy\Api\RateLimits;
use PHPUnit\Framework\TestCase;
@@ -17,7 +17,8 @@ public function testBuildFromResponseBuildsRateLimitsObject(): void
'X-RateLimit-Limit' => '180',
'X-RateLimit-Remaining' => '179',
'X-RateLimit-Reset' => '1681381136',
- ]
+ ],
+ '',
);
$this->assertInstanceOf(RateLimits::class, RateLimits::buildFromResponse($response));
diff --git a/tests/Resources/CarrierTest.php b/tests/Resources/CarrierTest.php
index 1a8de9e..8486bff 100644
--- a/tests/Resources/CarrierTest.php
+++ b/tests/Resources/CarrierTest.php
@@ -2,13 +2,10 @@
namespace Sendy\Api\Tests\Resources;
-use GuzzleHttp\Client;
-use GuzzleHttp\Handler\MockHandler;
-use GuzzleHttp\HandlerStack;
-use GuzzleHttp\Psr7\Response;
-use Sendy\Api\Connection;
-use Sendy\Api\Resources\Carrier;
use PHPUnit\Framework\TestCase;
+use Sendy\Api\Http\Response;
+use Sendy\Api\Http\Transport\MockTransport;
+use Sendy\Api\Resources\Carrier;
use Sendy\Api\Tests\TestsEndpoints;
class CarrierTest extends TestCase
@@ -17,29 +14,29 @@ class CarrierTest extends TestCase
public function testList(): void
{
- $handler = new MockHandler([
+ $transport = new MockTransport(
new Response(200, [], json_encode([])),
- ]);
+ );
- $resource = new Carrier($this->buildConnectionWithMockHandler($handler));
+ $resource = new Carrier($this->buildConnectionWithMockTransport($transport));
$this->assertEquals([], $resource->list());
- $this->assertEquals('/api/carriers', (string) $handler->getLastRequest()->getUri());
- $this->assertEquals('GET', $handler->getLastRequest()->getMethod());
+ $this->assertEquals('https://app.sendy.nl/api/carriers', $transport->getLastRequest()->getUrl());
+ $this->assertEquals('GET', $transport->getLastRequest()->getMethod());
}
public function testGet(): void
{
- $handler = new MockHandler([
+ $transport = new MockTransport(
new Response(200, [], json_encode([])),
- ]);
+ );
- $resource = new Carrier($this->buildConnectionWithMockHandler($handler));
+ $resource = new Carrier($this->buildConnectionWithMockTransport($transport));
$this->assertEquals([], $resource->get(1));
- $this->assertEquals('/api/carriers/1', (string) $handler->getLastRequest()->getUri());
- $this->assertEquals('GET', $handler->getLastRequest()->getMethod());
+ $this->assertEquals('https://app.sendy.nl/api/carriers/1', $transport->getLastRequest()->getUrl());
+ $this->assertEquals('GET', $transport->getLastRequest()->getMethod());
}
}
diff --git a/tests/Resources/LabelTest.php b/tests/Resources/LabelTest.php
index 75c1a60..9296566 100644
--- a/tests/Resources/LabelTest.php
+++ b/tests/Resources/LabelTest.php
@@ -2,11 +2,10 @@
namespace Sendy\Api\Tests\Resources;
-use GuzzleHttp\Handler\MockHandler;
-use GuzzleHttp\Psr7\Response;
-use Sendy\Api\Resources\Carrier;
-use Sendy\Api\Resources\Label;
use PHPUnit\Framework\TestCase;
+use Sendy\Api\Http\Response;
+use Sendy\Api\Http\Transport\MockTransport;
+use Sendy\Api\Resources\Label;
use Sendy\Api\Tests\TestsEndpoints;
class LabelTest extends TestCase
@@ -15,37 +14,37 @@ class LabelTest extends TestCase
public function testGet(): void
{
- $handler = new MockHandler([
+ $transport = new MockTransport(
new Response(200, [], json_encode([])),
- ]);
+ );
- $resource = new Label($this->buildConnectionWithMockHandler($handler));
+ $resource = new Label($this->buildConnectionWithMockTransport($transport));
$this->assertEquals([], $resource->get(['123456']));
$this->assertEquals(
- '/api/labels?ids%5B0%5D=123456',
- (string) $handler->getLastRequest()->getUri()
+ 'https://app.sendy.nl/api/labels?ids%5B0%5D=123456',
+ $transport->getLastRequest()->getUrl(),
);
- $this->assertEquals('GET', $handler->getLastRequest()->getMethod());
+ $this->assertEquals('GET', $transport->getLastRequest()->getMethod());
}
public function testParametersAreSetInURL(): void
{
- $handler = new MockHandler([
+ $transport = new MockTransport(
new Response(200, [], json_encode([])),
- ]);
+ );
- $resource = new Label($this->buildConnectionWithMockHandler($handler));
+ $resource = new Label($this->buildConnectionWithMockTransport($transport));
$this->assertEquals([], $resource->get(['123456', 'A4', 'top-left']));
$this->assertEquals(
- '/api/labels?ids%5B0%5D=123456&ids%5B1%5D=A4&ids%5B2%5D=top-left',
- (string) $handler->getLastRequest()->getUri()
+ 'https://app.sendy.nl/api/labels?ids%5B0%5D=123456&ids%5B1%5D=A4&ids%5B2%5D=top-left',
+ $transport->getLastRequest()->getUrl(),
);
- $this->assertEquals('GET', $handler->getLastRequest()->getMethod());
+ $this->assertEquals('GET', $transport->getLastRequest()->getMethod());
}
}
diff --git a/tests/Resources/MeTest.php b/tests/Resources/MeTest.php
index 7bd71a5..6d6f974 100644
--- a/tests/Resources/MeTest.php
+++ b/tests/Resources/MeTest.php
@@ -2,11 +2,10 @@
namespace Sendy\Api\Tests\Resources;
-use GuzzleHttp\Handler\MockHandler;
-use GuzzleHttp\Psr7\Response;
-use Sendy\Api\Resources\Label;
-use Sendy\Api\Resources\Me;
use PHPUnit\Framework\TestCase;
+use Sendy\Api\Http\Response;
+use Sendy\Api\Http\Transport\MockTransport;
+use Sendy\Api\Resources\Me;
use Sendy\Api\Tests\TestsEndpoints;
class MeTest extends TestCase
@@ -15,15 +14,15 @@ class MeTest extends TestCase
public function testGet(): void
{
- $handler = new MockHandler([
+ $transport = new MockTransport(
new Response(200, [], json_encode([])),
- ]);
+ );
- $resource = new Me($this->buildConnectionWithMockHandler($handler));
+ $resource = new Me($this->buildConnectionWithMockTransport($transport));
$this->assertEquals([], $resource->get());
- $this->assertEquals('/api/me', (string) $handler->getLastRequest()->getUri());
- $this->assertEquals('GET', $handler->getLastRequest()->getMethod());
+ $this->assertEquals('https://app.sendy.nl/api/me', $transport->getLastRequest()->getUrl());
+ $this->assertEquals('GET', $transport->getLastRequest()->getMethod());
}
}
diff --git a/tests/Resources/ParcelshopTest.php b/tests/Resources/ParcelshopTest.php
index 912b05f..3ebfccc 100644
--- a/tests/Resources/ParcelshopTest.php
+++ b/tests/Resources/ParcelshopTest.php
@@ -2,10 +2,10 @@
namespace Sendy\Api\Tests\Resources;
-use GuzzleHttp\Handler\MockHandler;
-use GuzzleHttp\Psr7\Response;
-use Sendy\Api\Resources\Parcelshop;
use PHPUnit\Framework\TestCase;
+use Sendy\Api\Http\Response;
+use Sendy\Api\Http\Transport\MockTransport;
+use Sendy\Api\Resources\Parcelshop;
use Sendy\Api\Tests\TestsEndpoints;
class ParcelshopTest extends TestCase
@@ -14,18 +14,19 @@ class ParcelshopTest extends TestCase
public function testList(): void
{
- $handler = new MockHandler([
+ $transport = new MockTransport(
new Response(200, [], json_encode([])),
- ]);
+ );
- $resource = new Parcelshop($this->buildConnectionWithMockHandler($handler));
+ $resource = new Parcelshop($this->buildConnectionWithMockTransport($transport));
$this->assertEquals([], $resource->list(['DHL'], 52.040588, 5.564890, 'NL', '3905KW'));
- $this->assertEquals('GET', $handler->getLastRequest()->getMethod());
+ $this->assertEquals('GET', $transport->getLastRequest()->getMethod());
$this->assertEquals(
- '/api/parcel_shops?carriers%5B0%5D=DHL&latitude=52.040588&longitude=5.56489&country=NL&postal_code=3905KW',
- (string) $handler->getLastRequest()->getUri()
+ 'https://app.sendy.nl/api/parcel_shops'
+ . '?carriers%5B0%5D=DHL&latitude=52.040588&longitude=5.56489&country=NL&postal_code=3905KW',
+ $transport->getLastRequest()->getUrl(),
);
}
}
diff --git a/tests/Resources/ServiceTest.php b/tests/Resources/ServiceTest.php
index 49d1feb..08b7224 100644
--- a/tests/Resources/ServiceTest.php
+++ b/tests/Resources/ServiceTest.php
@@ -2,8 +2,8 @@
namespace Sendy\Api\Tests\Resources;
-use GuzzleHttp\Handler\MockHandler;
-use GuzzleHttp\Psr7\Response;
+use Sendy\Api\Http\Response;
+use Sendy\Api\Http\Transport\MockTransport;
use Sendy\Api\Resources\Service;
use PHPUnit\Framework\TestCase;
use Sendy\Api\Tests\TestsEndpoints;
@@ -14,15 +14,15 @@ class ServiceTest extends TestCase
public function testList(): void
{
- $handler = new MockHandler([
+ $transport = new MockTransport(
new Response(200, [], json_encode([])),
- ]);
+ );
- $resource = new Service($this->buildConnectionWithMockHandler($handler));
+ $resource = new Service($this->buildConnectionWithMockTransport($transport));
$this->assertEquals([], $resource->list(1337));
- $this->assertEquals('/api/carriers/1337/services', (string) $handler->getLastRequest()->getUri());
- $this->assertEquals('GET', $handler->getLastRequest()->getMethod());
+ $this->assertEquals('https://app.sendy.nl/api/carriers/1337/services', $transport->getLastRequest()->getUrl());
+ $this->assertEquals('GET', $transport->getLastRequest()->getMethod());
}
}
diff --git a/tests/Resources/ShipmentTest.php b/tests/Resources/ShipmentTest.php
index 9a1d840..8fb31f8 100644
--- a/tests/Resources/ShipmentTest.php
+++ b/tests/Resources/ShipmentTest.php
@@ -2,10 +2,10 @@
namespace Sendy\Api\Tests\Resources;
-use GuzzleHttp\Handler\MockHandler;
-use GuzzleHttp\Psr7\Response;
-use Sendy\Api\Resources\Shipment;
use PHPUnit\Framework\TestCase;
+use Sendy\Api\Http\Response;
+use Sendy\Api\Http\Transport\MockTransport;
+use Sendy\Api\Resources\Shipment;
use Sendy\Api\Tests\TestsEndpoints;
class ShipmentTest extends TestCase
@@ -14,148 +14,167 @@ class ShipmentTest extends TestCase
public function testList(): void
{
- $handler = new MockHandler([
+ $transport = new MockTransport(
new Response(200, [], json_encode([])),
- ]);
+ );
- $resource = new Shipment($this->buildConnectionWithMockHandler($handler));
+ $resource = new Shipment($this->buildConnectionWithMockTransport($transport));
$this->assertEquals([], $resource->list());
- $this->assertEquals('/api/shipments?page=1', (string)$handler->getLastRequest()->getUri());
- $this->assertEquals('GET', $handler->getLastRequest()->getMethod());
+ $this->assertEquals('https://app.sendy.nl/api/shipments?page=1', $transport->getLastRequest()->getUrl());
+ $this->assertEquals('GET', $transport->getLastRequest()->getMethod());
}
public function testGet(): void
{
- $handler = new MockHandler([
+ $transport = new MockTransport(
new Response(200, [], json_encode([])),
- ]);
+ );
- $resource = new Shipment($this->buildConnectionWithMockHandler($handler));
+ $resource = new Shipment($this->buildConnectionWithMockTransport($transport));
$this->assertEquals([], $resource->get('1337'));
- $this->assertEquals('/api/shipments/1337', (string)$handler->getLastRequest()->getUri());
- $this->assertEquals('GET', $handler->getLastRequest()->getMethod());
+ $this->assertEquals('https://app.sendy.nl/api/shipments/1337', $transport->getLastRequest()->getUrl());
+ $this->assertEquals('GET', $transport->getLastRequest()->getMethod());
}
public function testUpdate(): void
{
- $handler = new MockHandler([
+ $transport = new MockTransport(
new Response(200, [], json_encode([])),
- ]);
+ );
- $resource = new Shipment($this->buildConnectionWithMockHandler($handler));
+ $resource = new Shipment($this->buildConnectionWithMockTransport($transport));
$this->assertEquals([], $resource->update('1337', ['foo' => 'bar']));
- $this->assertEquals('/api/shipments/1337', (string)$handler->getLastRequest()->getUri());
- $this->assertEquals('PUT', $handler->getLastRequest()->getMethod());
- $this->assertEquals('{"foo":"bar"}', $handler->getLastRequest()->getBody()->getContents());
+ $this->assertEquals('https://app.sendy.nl/api/shipments/1337', $transport->getLastRequest()->getUrl());
+ $this->assertEquals('PUT', $transport->getLastRequest()->getMethod());
+ $this->assertEquals('{"foo":"bar"}', $transport->getLastRequest()->getBody());
}
public function testDelete(): void
{
- $handler = new MockHandler([
- new Response(204),
- ]);
+ $transport = new MockTransport(
+ new Response(204, [], ''),
+ );
- $resource = new Shipment($this->buildConnectionWithMockHandler($handler));
+ $resource = new Shipment($this->buildConnectionWithMockTransport($transport));
$this->assertEquals([], $resource->delete('1337'));
- $this->assertEquals('/api/shipments/1337', (string)$handler->getLastRequest()->getUri());
- $this->assertEquals('DELETE', $handler->getLastRequest()->getMethod());
+ $this->assertEquals('https://app.sendy.nl/api/shipments/1337', $transport->getLastRequest()->getUrl());
+ $this->assertEquals('DELETE', $transport->getLastRequest()->getMethod());
}
public function testCreateFromPreference(): void
{
- $handler = new MockHandler([
- new Response(200, [], '{}'),
- new Response(200, [], '{}'),
- ]);
+ $transport = new MockTransport(
+ new Response(200, [], json_encode([])),
+ );
- $resource = new Shipment($this->buildConnectionWithMockHandler($handler));
+ $resource = new Shipment($this->buildConnectionWithMockTransport($transport));
$this->assertEquals([], $resource->createFromPreference(['foo' => 'bar'], false));
$this->assertEquals(
- '/api/shipments/preference?generateDirectly=0',
- (string)$handler->getLastRequest()->getUri()
+ 'https://app.sendy.nl/api/shipments/preference?generateDirectly=0',
+ $transport->getLastRequest()->getUrl(),
+ );
+ $this->assertEquals('POST', $transport->getLastRequest()->getMethod());
+ $this->assertEquals('{"foo":"bar"}', $transport->getLastRequest()->getBody());
+ }
+
+ public function testCreateAndGenerateFromPreference(): void
+ {
+ $transport = new MockTransport(
+ new Response(200, [], json_encode([])),
);
- $this->assertEquals('POST', $handler->getLastRequest()->getMethod());
- $this->assertEquals('{"foo":"bar"}', $handler->getLastRequest()->getBody()->getContents());
+
+ $resource = new Shipment($this->buildConnectionWithMockTransport($transport));
$this->assertEquals([], $resource->createFromPreference(['foo' => 'bar']));
$this->assertEquals(
- '/api/shipments/preference?generateDirectly=1',
- (string)$handler->getLastRequest()->getUri()
+ 'https://app.sendy.nl/api/shipments/preference?generateDirectly=1',
+ $transport->getLastRequest()->getUrl(),
);
}
public function testCreateWithSmartRules(): void
{
- $handler = new MockHandler([
+ $transport = new MockTransport(
new Response(200, [], json_encode(['foo' => 'bar'])),
- ]);
+ );
- $resource = new Shipment($this->buildConnectionWithMockHandler($handler));
+ $resource = new Shipment($this->buildConnectionWithMockTransport($transport));
- $resource->createWithSmartRules(['foo' => 'bar']);
+ $this->assertEquals(['foo' => 'bar'], $resource->createWithSmartRules(['foo' => 'bar']));
- $this->assertEquals('/api/shipments/smart-rule', (string)$handler->getLastRequest()->getUri());
- $this->assertEquals('POST', $handler->getLastRequest()->getMethod());
- $this->assertEquals('{"foo":"bar"}', $handler->getLastRequest()->getBody()->getContents());
+ $this->assertEquals('https://app.sendy.nl/api/shipments/smart-rule', $transport->getLastRequest()->getUrl());
+ $this->assertEquals('POST', $transport->getLastRequest()->getMethod());
+ $this->assertEquals('{"foo":"bar"}', $transport->getLastRequest()->getBody());
}
-
- public function testGenerate(): void
+ public function testGenerateAsynchronous(): void
{
- $handler = new MockHandler([
- new Response(200, [], '{}'),
+ $transport = new MockTransport(
new Response(200, [], '{}'),
- ]);
+ );
- $resource = new Shipment($this->buildConnectionWithMockHandler($handler));
+ $resource = new Shipment($this->buildConnectionWithMockTransport($transport));
$this->assertEquals([], $resource->generate('1337'));
- $this->assertEquals('/api/shipments/1337/generate', (string)$handler->getLastRequest()->getUri());
- $this->assertEquals('POST', $handler->getLastRequest()->getMethod());
- $this->assertEquals('{"asynchronous":true}', $handler->getLastRequest()->getBody()->getContents());
+ $this->assertEquals('https://app.sendy.nl/api/shipments/1337/generate', $transport->getLastRequest()->getUrl());
+ $this->assertEquals('POST', $transport->getLastRequest()->getMethod());
+ $this->assertEquals('{"asynchronous":true}', $transport->getLastRequest()->getBody());
+ }
+
+ public function testGenerateSynchronous(): void
+ {
+ $transport = new MockTransport(
+ new Response(200, [], '{}'),
+ );
+
+ $resource = new Shipment($this->buildConnectionWithMockTransport($transport));
$this->assertEquals([], $resource->generate('1337', false));
- $this->assertEquals('/api/shipments/1337/generate', (string)$handler->getLastRequest()->getUri());
- $this->assertEquals('{"asynchronous":false}', $handler->getLastRequest()->getBody()->getContents());
+ $this->assertEquals('https://app.sendy.nl/api/shipments/1337/generate', $transport->getLastRequest()->getUrl());
+ $this->assertEquals('POST', $transport->getLastRequest()->getMethod());
+ $this->assertEquals('{"asynchronous":false}', $transport->getLastRequest()->getBody());
}
public function testLabels(): void
{
- $handler = new MockHandler([
+ $transport = new MockTransport(
new Response(200, [], json_encode([])),
- ]);
+ );
- $resource = new Shipment($this->buildConnectionWithMockHandler($handler));
+ $resource = new Shipment($this->buildConnectionWithMockTransport($transport));
$this->assertEquals([], $resource->labels('1337'));
- $this->assertEquals('/api/shipments/1337/labels', (string)$handler->getLastRequest()->getUri());
- $this->assertEquals('GET', $handler->getLastRequest()->getMethod());
+ $this->assertEquals('https://app.sendy.nl/api/shipments/1337/labels', $transport->getLastRequest()->getUrl());
+ $this->assertEquals('GET', $transport->getLastRequest()->getMethod());
}
public function testDocuments(): void
{
- $handler = new MockHandler([
+ $transport = new MockTransport(
new Response(200, [], json_encode([])),
- ]);
+ );
- $resource = new Shipment($this->buildConnectionWithMockHandler($handler));
+ $resource = new Shipment($this->buildConnectionWithMockTransport($transport));
$this->assertEquals([], $resource->documents('1337'));
- $this->assertEquals('/api/shipments/1337/documents', (string)$handler->getLastRequest()->getUri());
- $this->assertEquals('GET', $handler->getLastRequest()->getMethod());
+ $this->assertEquals(
+ 'https://app.sendy.nl/api/shipments/1337/documents',
+ $transport->getLastRequest()->getUrl(),
+ );
+ $this->assertEquals('GET', $transport->getLastRequest()->getMethod());
}
}
diff --git a/tests/Resources/ShippingPreferenceTest.php b/tests/Resources/ShippingPreferenceTest.php
index 2015acc..3fecbd1 100644
--- a/tests/Resources/ShippingPreferenceTest.php
+++ b/tests/Resources/ShippingPreferenceTest.php
@@ -2,11 +2,10 @@
namespace Sendy\Api\Tests\Resources;
-use GuzzleHttp\Handler\MockHandler;
-use GuzzleHttp\Psr7\Response;
-use Sendy\Api\Resources\ShippingPreference;
use PHPUnit\Framework\TestCase;
-use Sendy\Api\Resources\Shop;
+use Sendy\Api\Http\Response;
+use Sendy\Api\Http\Transport\MockTransport;
+use Sendy\Api\Resources\ShippingPreference;
use Sendy\Api\Tests\TestsEndpoints;
class ShippingPreferenceTest extends TestCase
@@ -15,15 +14,15 @@ class ShippingPreferenceTest extends TestCase
public function testList(): void
{
- $handler = new MockHandler([
+ $transport = new MockTransport(
new Response(200, [], json_encode([])),
- ]);
+ );
- $resource = new ShippingPreference($this->buildConnectionWithMockHandler($handler));
+ $resource = new ShippingPreference($this->buildConnectionWithMockTransport($transport));
$this->assertEquals([], $resource->list());
- $this->assertEquals('/api/shipping_preferences', (string) $handler->getLastRequest()->getUri());
- $this->assertEquals('GET', $handler->getLastRequest()->getMethod());
+ $this->assertEquals('https://app.sendy.nl/api/shipping_preferences', $transport->getLastRequest()->getUrl());
+ $this->assertEquals('GET', $transport->getLastRequest()->getMethod());
}
}
diff --git a/tests/Resources/ShopTest.php b/tests/Resources/ShopTest.php
index ab4d59d..903226b 100644
--- a/tests/Resources/ShopTest.php
+++ b/tests/Resources/ShopTest.php
@@ -2,10 +2,10 @@
namespace Sendy\Api\Tests\Resources;
-use GuzzleHttp\Handler\MockHandler;
-use GuzzleHttp\Psr7\Response;
-use Sendy\Api\Resources\Shop;
use PHPUnit\Framework\TestCase;
+use Sendy\Api\Http\Response;
+use Sendy\Api\Http\Transport\MockTransport;
+use Sendy\Api\Resources\Shop;
use Sendy\Api\Tests\TestsEndpoints;
class ShopTest extends TestCase
@@ -14,29 +14,29 @@ class ShopTest extends TestCase
public function testList(): void
{
- $handler = new MockHandler([
+ $transport = new MockTransport(
new Response(200, [], json_encode([])),
- ]);
+ );
- $resource = new Shop($this->buildConnectionWithMockHandler($handler));
+ $resource = new Shop($this->buildConnectionWithMockTransport($transport));
$this->assertEquals([], $resource->list());
- $this->assertEquals('/api/shops', (string) $handler->getLastRequest()->getUri());
- $this->assertEquals('GET', $handler->getLastRequest()->getMethod());
+ $this->assertEquals('https://app.sendy.nl/api/shops', $transport->getLastRequest()->getUrl());
+ $this->assertEquals('GET', $transport->getLastRequest()->getMethod());
}
public function testGet(): void
{
- $handler = new MockHandler([
+ $transport = new MockTransport(
new Response(200, [], json_encode([])),
- ]);
+ );
- $resource = new Shop($this->buildConnectionWithMockHandler($handler));
+ $resource = new Shop($this->buildConnectionWithMockTransport($transport));
$this->assertEquals([], $resource->get('1337'));
- $this->assertEquals('/api/shops/1337', (string) $handler->getLastRequest()->getUri());
- $this->assertEquals('GET', $handler->getLastRequest()->getMethod());
+ $this->assertEquals('https://app.sendy.nl/api/shops/1337', $transport->getLastRequest()->getUrl());
+ $this->assertEquals('GET', $transport->getLastRequest()->getMethod());
}
}
diff --git a/tests/Resources/WebhookTest.php b/tests/Resources/WebhookTest.php
index 0fc3e1e..5aeba9d 100644
--- a/tests/Resources/WebhookTest.php
+++ b/tests/Resources/WebhookTest.php
@@ -2,10 +2,10 @@
namespace Sendy\Api\Tests\Resources;
-use GuzzleHttp\Handler\MockHandler;
-use GuzzleHttp\Psr7\Response;
-use Sendy\Api\Resources\Webhook;
use PHPUnit\Framework\TestCase;
+use Sendy\Api\Http\Response;
+use Sendy\Api\Http\Transport\MockTransport;
+use Sendy\Api\Resources\Webhook;
use Sendy\Api\Tests\TestsEndpoints;
class WebhookTest extends TestCase
@@ -14,47 +14,47 @@ class WebhookTest extends TestCase
public function testList(): void
{
- $handler = new MockHandler([
+ $transport = new MockTransport(
new Response(200, [], json_encode([])),
- ]);
+ );
- $resource = new Webhook($this->buildConnectionWithMockHandler($handler));
+ $resource = new Webhook($this->buildConnectionWithMockTransport($transport));
$this->assertEquals([], $resource->list());
- $this->assertEquals('/api/webhooks', (string)$handler->getLastRequest()->getUri());
- $this->assertEquals('GET', $handler->getLastRequest()->getMethod());
+ $this->assertEquals('https://app.sendy.nl/api/webhooks', $transport->getLastRequest()->getUrl());
+ $this->assertEquals('GET', $transport->getLastRequest()->getMethod());
}
public function testDelete(): void
{
- $handler = new MockHandler([
- new Response(204),
- ]);
+ $transport = new MockTransport(
+ new Response(204, [], ''),
+ );
- $resource = new Webhook($this->buildConnectionWithMockHandler($handler));
+ $resource = new Webhook($this->buildConnectionWithMockTransport($transport));
- $resource->delete('webhook-id');
+ $this->assertEquals([], $resource->delete('webhook-id'));
- $this->assertEquals('/api/webhooks/webhook-id', $handler->getLastRequest()->getUri());
- $this->assertEquals('DELETE', $handler->getLastRequest()->getMethod());
+ $this->assertEquals('https://app.sendy.nl/api/webhooks/webhook-id', $transport->getLastRequest()->getUrl());
+ $this->assertEquals('DELETE', $transport->getLastRequest()->getMethod());
}
public function testCreate(): void
{
- $handler = new MockHandler([
+ $transport = new MockTransport(
new Response(201, [], json_encode([
'data' => [
'id' => 'webhook-id',
'url' => 'https://example.com/webhook',
'events' => [
'shipment.generated',
- ]
- ]
+ ],
+ ],
])),
- ]);
+ );
- $resource = new Webhook($this->buildConnectionWithMockHandler($handler));
+ $resource = new Webhook($this->buildConnectionWithMockTransport($transport));
$resource->create([
'url' => 'https://example.com/webhook',
@@ -63,29 +63,29 @@ public function testCreate(): void
],
]);
- $this->assertEquals('/api/webhooks', (string)$handler->getLastRequest()->getUri());
- $this->assertEquals('POST', $handler->getLastRequest()->getMethod());
+ $this->assertEquals('https://app.sendy.nl/api/webhooks', $transport->getLastRequest()->getUrl());
+ $this->assertEquals('POST', $transport->getLastRequest()->getMethod());
$this->assertEquals(
'{"url":"https:\/\/example.com\/webhook","events":["shipments.generated"]}',
- $handler->getLastRequest()->getBody()->getContents()
+ $transport->getLastRequest()->getBody(),
);
}
public function testUpdate(): void
{
- $handler = new MockHandler([
+ $transport = new MockTransport(
new Response(201, [], json_encode([
'data' => [
'id' => 'webhook-id',
'url' => 'https://example.com/updated-webhook',
'events' => [
'shipment.generated',
- ]
- ]
+ ],
+ ],
])),
- ]);
+ );
- $resource = new Webhook($this->buildConnectionWithMockHandler($handler));
+ $resource = new Webhook($this->buildConnectionWithMockTransport($transport));
$resource->update('webhook-id', [
'url' => 'https://example.com/updated-webhook',
@@ -94,11 +94,11 @@ public function testUpdate(): void
],
]);
- $this->assertEquals('/api/webhooks/webhook-id', $handler->getLastRequest()->getUri());
- $this->assertEquals('PUT', $handler->getLastRequest()->getMethod());
+ $this->assertEquals('https://app.sendy.nl/api/webhooks/webhook-id', $transport->getLastRequest()->getUrl());
+ $this->assertEquals('PUT', $transport->getLastRequest()->getMethod());
$this->assertEquals(
'{"url":"https:\/\/example.com\/updated-webhook","events":["shipment.generated"]}',
- $handler->getLastRequest()->getBody()->getContents()
+ $transport->getLastRequest()->getBody(),
);
}
}
diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php
new file mode 100644
index 0000000..ee5193e
--- /dev/null
+++ b/tests/ResponseTest.php
@@ -0,0 +1,54 @@
+toException(new Request('GET', '/foo'));
+
+ $this->assertInstanceOf(ServerException::class, $exception);
+ $this->assertSame(500, $exception->getCode());
+ $this->assertSame('500 - Internal Server Error', $exception->getMessage());
+ }
+
+ public function testToExceptionHandlesInvalidJson(): void
+ {
+ $response = new Response(422, [], 'InvalidJson');
+
+ $this->expectException(ApiException::class);
+ $this->expectExceptionMessage('Json decode failed. Got: InvalidJson');
+
+ $response->toException(new Request('GET', '/foo'));
+ }
+
+ public function testToExceptionHandlesValidationMessages(): void
+ {
+ $response = new Response(422, [], json_encode(['message' => 'Error message']));
+
+ $exception = $response->toException(new Request('GET', '/foo'));
+
+ $this->assertInstanceOf(ValidationException::class, $exception);
+ $this->assertSame(422, $exception->getCode());
+ $this->assertSame('Error message', $exception->getMessage());
+ }
+
+ public function testToExceptionSetsErrors(): void
+ {
+ $response = new Response(422, [], json_encode(['message' => 'Error message', 'errors' => ['First', 'Second']]));
+ $this->assertSame(['First', 'Second'], $response->toException(new Request('GET', '/foo'))->getErrors());
+
+ $response = new Response(422, [], json_encode(['message' => 'Error message']));
+ $this->assertSame([], $response->toException(new Request('GET', '/foo'))->getErrors());
+ }
+}
diff --git a/tests/TestsEndpoints.php b/tests/TestsEndpoints.php
index b7a269b..b083bb6 100644
--- a/tests/TestsEndpoints.php
+++ b/tests/TestsEndpoints.php
@@ -2,22 +2,16 @@
namespace Sendy\Api\Tests;
-use GuzzleHttp\Client;
-use GuzzleHttp\Handler\MockHandler;
-use GuzzleHttp\HandlerStack;
-use GuzzleHttp\Psr7\Response;
use Sendy\Api\Connection;
+use Sendy\Api\Http\Transport\MockTransport;
trait TestsEndpoints
{
- public function buildConnectionWithMockHandler(MockHandler $handler): Connection
+ public function buildConnectionWithMockTransport(MockTransport $transport): Connection
{
$connection = new Connection();
$connection->setAccessToken('PersonalAccessToken');
-
- $client = new Client(['handler' => HandlerStack::create($handler)]);
-
- $connection->setClient($client);
+ $connection->setTransport($transport);
return $connection;
}