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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 26 additions & 1 deletion src/Exceptions/MetaApiException.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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(),
];
}
}
35 changes: 31 additions & 4 deletions src/Http.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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(
Expand Down Expand Up @@ -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'] ?? []
Expand All @@ -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 = [
Expand Down
53 changes: 53 additions & 0 deletions tests/HttpTest.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
<?php

use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Victorycodedev\MetaapiCloudPhpSdk\Exceptions\MetaApiException;
use Victorycodedev\MetaapiCloudPhpSdk\Responses\ActionResponse;
Expand Down Expand Up @@ -109,6 +113,15 @@
expect($exception->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;
}
Expand All @@ -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.');
});
Loading