Skip to content

Commit 85f8bf9

Browse files
author
Mohamed Khaled
committed
Implement exception hierarchy reorganization with static factory methods
1 parent 67b60dc commit 85f8bf9

File tree

14 files changed

+315
-61
lines changed

14 files changed

+315
-61
lines changed

src/AiClient.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
namespace WordPress\AiClient;
66

77
use WordPress\AiClient\Builders\PromptBuilder;
8-
use WordPress\AiClient\Exceptions\InvalidArgumentException;
9-
use WordPress\AiClient\Exceptions\RuntimeException;
8+
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
9+
use WordPress\AiClient\Common\Exception\RuntimeException;
1010
use WordPress\AiClient\ProviderImplementations\Anthropic\AnthropicProvider;
1111
use WordPress\AiClient\ProviderImplementations\Google\GoogleProvider;
1212
use WordPress\AiClient\ProviderImplementations\OpenAi\OpenAiProvider;

src/Exceptions/InvalidArgumentException.php renamed to src/Common/Exception/InvalidArgumentException.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
declare(strict_types=1);
44

5-
namespace WordPress\AiClient\Exceptions;
5+
namespace WordPress\AiClient\Common\Exception;
6+
7+
use WordPress\AiClient\Exceptions\AiClientExceptionInterface;
68

79
/**
810
* Exception thrown when an invalid argument is provided.
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
declare(strict_types=1);
44

5-
namespace WordPress\AiClient\Exceptions;
5+
namespace WordPress\AiClient\Common\Exception;
6+
7+
use WordPress\AiClient\Exceptions\AiClientExceptionInterface;
68

79
/**
810
* Exception thrown for runtime errors.

src/Exceptions/NetworkException.php

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/Exceptions/RequestException.php

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/Messages/DTO/Message.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
namespace WordPress\AiClient\Messages\DTO;
66

7-
use InvalidArgumentException;
87
use WordPress\AiClient\Common\AbstractDataTransferObject;
8+
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
99
use WordPress\AiClient\Messages\Enums\MessageRoleEnum;
1010

1111
/**
@@ -188,7 +188,7 @@ final public static function fromArray(array $array): self
188188
return new ModelMessage($parts);
189189
} else {
190190
// Only USER and MODEL roles are supported
191-
throw new \InvalidArgumentException('Invalid message role: ' . $role->value);
191+
throw new InvalidArgumentException('Invalid message role: ' . $role->value);
192192
}
193193
}
194194
}

src/ProviderImplementations/OpenAi/OpenAiModelMetadataDirectory.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace WordPress\AiClient\ProviderImplementations\OpenAi;
66

77
use RuntimeException;
8+
use WordPress\AiClient\Providers\Http\Exception\ResponseException;
89
use WordPress\AiClient\Files\Enums\FileTypeEnum;
910
use WordPress\AiClient\Files\Enums\MediaOrientationEnum;
1011
use WordPress\AiClient\Messages\Enums\ModalityEnum;
@@ -53,9 +54,7 @@ protected function parseResponseToModelMetadataList(Response $response): array
5354
/** @var ModelsResponseData $responseData */
5455
$responseData = $response->getData();
5556
if (!isset($responseData['data']) || !$responseData['data']) {
56-
throw new RuntimeException(
57-
'Unexpected API response: Missing the data key.'
58-
);
57+
throw ResponseException::fromMissingData('OpenAI', 'data');
5958
}
6059

6160
// Unfortunately, the OpenAI API does not return model capabilities, so we have to hardcode them here.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WordPress\AiClient\Providers\Http\Exception;
6+
7+
use WordPress\AiClient\Common\Exception\RuntimeException;
8+
9+
/**
10+
* Exception thrown for network-related errors.
11+
*
12+
* This includes HTTP transport errors, connection failures,
13+
* timeouts, and other network-related issues.
14+
*
15+
* @since 0.2.0
16+
*/
17+
class NetworkException extends RuntimeException
18+
{
19+
/**
20+
* Creates a NetworkException for connection failures.
21+
*
22+
* @since 0.2.0
23+
*
24+
* @param string $uri The URI that failed to connect.
25+
* @param string $reason The reason for connection failure.
26+
* @param \Throwable|null $previous The underlying network exception.
27+
* @return self
28+
*/
29+
public static function fromConnectionFailure(string $uri, string $reason = 'Connection failed', ?\Throwable $previous = null): self
30+
{
31+
$message = sprintf('Network connection failed for %s: %s', $uri, $reason);
32+
33+
return new self($message, 0, $previous);
34+
}
35+
36+
/**
37+
* Creates a NetworkException for timeout errors.
38+
*
39+
* @since 0.2.0
40+
*
41+
* @param string $uri The URI that timed out.
42+
* @param string $timeoutType Type of timeout (e.g., 'connection', 'read', 'request').
43+
* @param int|null $timeoutSeconds The timeout duration if known.
44+
* @param \Throwable|null $previous The underlying timeout exception.
45+
* @return self
46+
*/
47+
public static function fromTimeout(string $uri, string $timeoutType = 'request', ?int $timeoutSeconds = null, ?\Throwable $previous = null): self
48+
{
49+
$message = sprintf('Network %s timeout for %s', $timeoutType, $uri);
50+
if ($timeoutSeconds !== null) {
51+
$message .= sprintf(' (after %d seconds)', $timeoutSeconds);
52+
}
53+
54+
return new self($message, 0, $previous);
55+
}
56+
57+
/**
58+
* Creates a NetworkException from a PSR-18 network exception.
59+
*
60+
* @since 0.2.0
61+
*
62+
* @param string $uri The URI that was being requested.
63+
* @param \Throwable $networkException The PSR-18 network exception.
64+
* @return self
65+
*/
66+
public static function fromPsr18NetworkException(string $uri, \Throwable $networkException): self
67+
{
68+
$message = sprintf(
69+
'Network error occurred while sending request to %s: %s',
70+
$uri,
71+
$networkException->getMessage()
72+
);
73+
74+
return new self($message, 0, $networkException);
75+
}
76+
77+
/**
78+
* Creates a NetworkException for DNS resolution failures.
79+
*
80+
* @since 0.2.0
81+
*
82+
* @param string $hostname The hostname that failed to resolve.
83+
* @param \Throwable|null $previous The underlying DNS exception.
84+
* @return self
85+
*/
86+
public static function fromDnsFailure(string $hostname, ?\Throwable $previous = null): self
87+
{
88+
$message = sprintf('Failed to resolve hostname: %s', $hostname);
89+
90+
return new self($message, 0, $previous);
91+
}
92+
93+
/**
94+
* Creates a NetworkException for SSL/TLS errors.
95+
*
96+
* @since 0.2.0
97+
*
98+
* @param string $uri The URI with SSL/TLS issues.
99+
* @param string $sslError Description of the SSL/TLS error.
100+
* @param \Throwable|null $previous The underlying SSL exception.
101+
* @return self
102+
*/
103+
public static function fromSslError(string $uri, string $sslError, ?\Throwable $previous = null): self
104+
{
105+
$message = sprintf('SSL/TLS error for %s: %s', $uri, $sslError);
106+
107+
return new self($message, 0, $previous);
108+
}
109+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WordPress\AiClient\Providers\Http\Exception;
6+
7+
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
8+
use WordPress\AiClient\Providers\Http\DTO\Response;
9+
10+
/**
11+
* Exception thrown for AI API request errors due to bad request data.
12+
*
13+
* This includes malformed requests, invalid parameters, and scenarios
14+
* where the API responds with a 400 Bad Request status code indicating
15+
* that our code didn't catch an invalid argument but the API did.
16+
*
17+
* @since 0.2.0
18+
*/
19+
class RequestException extends InvalidArgumentException
20+
{
21+
/**
22+
* Creates a RequestException for invalid API parameters.
23+
*
24+
* @since 0.2.0
25+
*
26+
* @param string $apiName The name of the API/provider.
27+
* @param string $paramName The parameter that was invalid.
28+
* @param string $message Additional error message.
29+
* @return self
30+
*/
31+
public static function fromInvalidParam(string $apiName, string $paramName, string $message = ''): self
32+
{
33+
$errorMessage = sprintf('Invalid parameter "%s" for %s API', $paramName, $apiName);
34+
if ($message !== '') {
35+
$errorMessage .= ': ' . $message;
36+
}
37+
38+
return new self($errorMessage);
39+
}
40+
41+
/**
42+
* Creates a RequestException from a 400 Bad Request API response.
43+
*
44+
* @since 0.2.0
45+
*
46+
* @param string $apiName The name of the API/provider.
47+
* @param Response $response The HTTP response containing the error.
48+
* @return self
49+
*/
50+
public static function fromBadRequestResponse(string $apiName, Response $response): self
51+
{
52+
$body = $response->getBody();
53+
$errorDetail = $body ? substr($body, 0, 200) : 'Invalid request parameters';
54+
55+
$message = sprintf(
56+
'Bad request to %s API (400): %s',
57+
$apiName,
58+
$errorDetail
59+
);
60+
61+
return new self($message);
62+
}
63+
64+
/**
65+
* Creates a RequestException from a bad request to a specific URI.
66+
*
67+
* @since 0.2.0
68+
*
69+
* @param string $uri The URI that was requested.
70+
* @param string $errorDetail Details about what made the request bad.
71+
* @return self
72+
*/
73+
public static function fromBadRequestToUri(string $uri, string $errorDetail = 'Invalid request parameters'): self
74+
{
75+
return new self(sprintf('Bad request to %s (400): %s', $uri, $errorDetail));
76+
}
77+
}

src/Providers/Http/Exception/ResponseException.php

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,94 @@
44

55
namespace WordPress\AiClient\Providers\Http\Exception;
66

7-
use WordPress\AiClient\Exceptions\RequestException;
7+
use WordPress\AiClient\Common\Exception\RuntimeException;
8+
use WordPress\AiClient\Providers\Http\DTO\Response;
89

910
/**
1011
* Exception class for HTTP response errors.
1112
*
13+
* This is used when response data is unexpected or malformed,
14+
* typically indicating that a provider changed in ways our code
15+
* is not aware of or when parsing response data fails.
16+
*
1217
* @since 0.1.0
1318
*/
14-
class ResponseException extends RequestException
19+
class ResponseException extends RuntimeException
1520
{
21+
/**
22+
* Creates a ResponseException for missing expected data.
23+
*
24+
* @since 0.2.0
25+
*
26+
* @param string $apiName The name of the API/provider.
27+
* @param string $fieldName The field that was expected but missing.
28+
* @param string $context Additional context about where the field was expected.
29+
* @return self
30+
*/
31+
public static function fromMissingData(string $apiName, string $fieldName, string $context = ''): self
32+
{
33+
$message = sprintf('Unexpected %s API response: Missing the "%s" key', $apiName, $fieldName);
34+
if ($context !== '') {
35+
$message .= ' in ' . $context;
36+
}
37+
$message .= '.';
38+
39+
return new self($message);
40+
}
41+
42+
/**
43+
* Creates a ResponseException for unexpected API response structure.
44+
*
45+
* @since 0.2.0
46+
*
47+
* @param string $apiName The name of the API/provider.
48+
* @param string $expected What structure was expected.
49+
* @param string $actual What was actually received.
50+
* @return self
51+
*/
52+
public static function fromUnexpectedStructure(string $apiName, string $expected, string $actual = 'unknown'): self
53+
{
54+
return new self(sprintf(
55+
'Unexpected %s API response structure. Expected: %s, Got: %s',
56+
$apiName,
57+
$expected,
58+
$actual
59+
));
60+
}
61+
62+
/**
63+
* Creates a ResponseException for malformed response data.
64+
*
65+
* @since 0.2.0
66+
*
67+
* @param string $apiName The name of the API/provider.
68+
* @param string $reason Why the response is considered malformed.
69+
* @param Response|null $response The response object if available.
70+
* @return self
71+
*/
72+
public static function fromMalformedResponse(string $apiName, string $reason, ?Response $response = null): self
73+
{
74+
$message = sprintf('Malformed %s API response: %s', $apiName, $reason);
75+
76+
$statusCode = $response ? $response->getStatusCode() : 0;
77+
78+
return new self($message, $statusCode);
79+
}
80+
81+
/**
82+
* Creates a ResponseException from response parsing failure.
83+
*
84+
* @since 0.2.0
85+
*
86+
* @param string $apiName The name of the API/provider.
87+
* @param string $dataType The type of data that failed to parse.
88+
* @param \Throwable|null $previous The previous exception that caused parsing to fail.
89+
* @return self
90+
*/
91+
public static function fromParsingFailure(string $apiName, string $dataType, ?\Throwable $previous = null): self
92+
{
93+
$message = sprintf('Failed to parse %s from %s API response', $dataType, $apiName);
94+
95+
return new self($message, 0, $previous);
96+
}
1697
}

0 commit comments

Comments
 (0)