diff --git a/README.md b/README.md index b8237c1..c593ec4 100644 --- a/README.md +++ b/README.md @@ -724,15 +724,29 @@ use Victorycodedev\MetaapiCloudPhpSdk\Exceptions\MetaApiException; try { $account = $metaapi->accounts()->readById('account-id'); } catch (MetaApiException $exception) { - echo $exception->getMessage(); + echo $exception->message(); echo $exception->statusCode(); - echo $exception->errorName(); + echo $exception->id(); + echo $exception->error(); print_r($exception->details()); + print_r($exception->toArray()); print_r($exception->response()); } ``` +`MetaApiException` reflects MetaApi's standard REST error model: + +```php +$exception->id(); // Error id +$exception->error(); // Error name, e.g. ValidationError +$exception->message(); // User-friendly error description +$exception->details(); // Additional validation details +$exception->toArray(); // id, error, message and details +``` + +The existing `errorId()`, `errorName()` and `getMessage()` methods remain available for backward compatibility. HTTP response errors and transport failures from custom Guzzle clients are normalized to `MetaApiException`; the original transport exception is available through `getPrevious()`. + ## Testing ```bash diff --git a/src/Exceptions/MetaApiException.php b/src/Exceptions/MetaApiException.php index dae52e1..6b3182a 100644 --- a/src/Exceptions/MetaApiException.php +++ b/src/Exceptions/MetaApiException.php @@ -34,19 +34,34 @@ public function headers(): array } public function errorId(): ?int + { + return $this->id(); + } + + public function errorName(): ?string + { + return $this->error(); + } + + public function id(): ?int { return is_array($this->response) && isset($this->response['id']) ? (int) $this->response['id'] : null; } - public function errorName(): ?string + public function error(): ?string { return is_array($this->response) && isset($this->response['error']) ? (string) $this->response['error'] : null; } + public function message(): string + { + return $this->getMessage(); + } + public function details(): mixed { return is_array($this->response) && array_key_exists('details', $this->response) @@ -58,4 +73,14 @@ public function retryAfter(): ?string { return $this->headers['Retry-After'][0] ?? $this->headers['retry-after'][0] ?? null; } + + public function toArray(): array + { + return [ + 'id' => $this->id(), + 'error' => $this->error(), + 'message' => $this->message(), + 'details' => $this->details(), + ]; + } } diff --git a/src/Http.php b/src/Http.php index 582bf4a..0f7df3d 100644 --- a/src/Http.php +++ b/src/Http.php @@ -4,6 +4,8 @@ use GuzzleHttp\Client; use GuzzleHttp\ClientInterface; +use GuzzleHttp\Exception\GuzzleException; +use GuzzleHttp\Exception\RequestException; use Psr\Http\Message\ResponseInterface; use Victorycodedev\MetaapiCloudPhpSdk\Exceptions\MetaApiException; use Victorycodedev\MetaapiCloudPhpSdk\Responses\ActionResponse; @@ -70,10 +72,10 @@ public function upload(string $uri, string $filePath, string $fieldName = 'file' public function request(string $verb, string $uri, array $options = []): array|string|null { $options = $this->normalizeOptions($options); - $response = $this->client->request($verb, $this->resolveUri($uri), $options); + $response = $this->send($verb, $uri, $options); if (!$this->isSuccessful($response)) { - return $this->handleError($response); + $this->handleError($response); } $body = (string) $response->getBody(); @@ -98,10 +100,10 @@ public function request(string $verb, string $uri, array $options = []): array|s public function requestAction(string $verb, string $uri, array $options = []): ActionResponse { $options = $this->normalizeOptions($options); - $response = $this->client->request($verb, $this->resolveUri($uri), $options); + $response = $this->send($verb, $uri, $options); if (!$this->isSuccessful($response)) { - return $this->handleError($response); + $this->handleError($response); } return new ActionResponse( @@ -135,6 +137,7 @@ public function handleError(ResponseInterface $response): void private function normalizeOptions(array $options): array { + $options['http_errors'] = false; $options['headers'] = array_merge( $this->defaultHeaders($options), $options['headers'] ?? [] @@ -153,6 +156,30 @@ private function normalizeOptions(array $options): array return $options; } + private function send(string $verb, string $uri, array $options): ResponseInterface + { + try { + return $this->client->request($verb, $this->resolveUri($uri), $options); + } catch (RequestException $exception) { + if ($exception->hasResponse()) { + $this->handleError($exception->getResponse()); + } + + throw $this->transportException($exception); + } catch (GuzzleException $exception) { + throw $this->transportException($exception); + } + } + + private function transportException(GuzzleException $exception): MetaApiException + { + return new MetaApiException( + $exception->getMessage() ?: 'Unable to connect to MetaApi', + (int) $exception->getCode(), + previous: $exception + ); + } + private function defaultHeaders(array $options): array { $headers = [ diff --git a/tests/HttpTest.php b/tests/HttpTest.php index 6d14174..b8e7340 100644 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -1,6 +1,10 @@ errorName())->toBe('ValidationError'); expect($exception->details())->toBe(['code' => 'E_AUTH']); expect($exception->getMessage())->toBe('Invalid account'); + expect($exception->id())->toBe(1); + expect($exception->error())->toBe('ValidationError'); + expect($exception->message())->toBe('Invalid account'); + expect($exception->toArray())->toBe([ + 'id' => 1, + 'error' => 'ValidationError', + 'message' => 'Invalid account', + 'details' => ['code' => 'E_AUTH'], + ]); return; } @@ -124,3 +137,43 @@ expect(fn () => $http->get('/users/current/accounts')) ->toThrow(MetaApiException::class, 'Too many requests'); }); + +it('converts response errors from custom clients into MetaApiException', function (): void { + $mock = new MockHandler([ + new Response(400, [], '{"id":2,"error":"ValidationError","message":"Invalid server"}'), + ]); + $client = new Client([ + 'handler' => HandlerStack::create($mock), + 'http_errors' => true, + ]); + $http = new \Victorycodedev\MetaapiCloudPhpSdk\Http('test-token', 'https://example.test', $client); + + expect(fn () => $http->get('/users/current/accounts')) + ->toThrow(MetaApiException::class, 'Invalid server'); +}); + +it('wraps transport failures in MetaApiException', function (): void { + $request = new Request('GET', 'https://example.test/users/current/accounts'); + $transportError = new ConnectException('Connection failed', $request); + $mock = new MockHandler([$transportError]); + $client = new Client(['handler' => HandlerStack::create($mock)]); + $http = new \Victorycodedev\MetaapiCloudPhpSdk\Http('test-token', 'https://example.test', $client); + + try { + $http->get('/users/current/accounts'); + } catch (MetaApiException $exception) { + expect($exception->message())->toBe('Connection failed'); + expect($exception->statusCode())->toBe(0); + expect($exception->getPrevious())->toBe($transportError); + expect($exception->toArray())->toBe([ + 'id' => null, + 'error' => null, + 'message' => 'Connection failed', + 'details' => null, + ]); + + return; + } + + $this->fail('Expected MetaApiException to be thrown.'); +});