feat(provider): add health-probe contract and supporting value objects
Add getName(), ping(), and probe() to ProviderInterface. Introduce ProbeTrait to classify HTTP/transport exceptions into typed error codes, ProviderStatus as the result value object, and ProviderStatusType / ProviderErrorCode as backing enums. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||||
|
|
||||||
|
trait ProbeTrait
|
||||||
|
{
|
||||||
|
public function probe(): ProviderStatus
|
||||||
|
{
|
||||||
|
if (!$this->isConfigured()) {
|
||||||
|
return new ProviderStatus($this->getName(), ProviderStatusType::NotConfigured);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->ping();
|
||||||
|
|
||||||
|
return new ProviderStatus($this->getName(), ProviderStatusType::Ok);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return $this->statusFromException($e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function statusFromException(\Throwable $e): ProviderStatus
|
||||||
|
{
|
||||||
|
[$error, $message] = $this->classifyException($e);
|
||||||
|
|
||||||
|
return new ProviderStatus($this->getName(), ProviderStatusType::Error, $error, $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{ProviderErrorCode, string} */
|
||||||
|
private function classifyException(\Throwable $e): array
|
||||||
|
{
|
||||||
|
if ($e instanceof TransportExceptionInterface) {
|
||||||
|
return [ProviderErrorCode::UrlUnreachable, 'Could not reach the server: ' . $e->getMessage()];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($e instanceof HttpExceptionInterface) {
|
||||||
|
$code = $e->getResponse()->getStatusCode();
|
||||||
|
|
||||||
|
return match (true) {
|
||||||
|
$code === 401 => [ProviderErrorCode::AuthFailed, 'Invalid or expired token — verify your credentials'],
|
||||||
|
$code === 403 => [ProviderErrorCode::AuthFailed, 'Access denied — token lacks the required scopes'],
|
||||||
|
$code === 404 => [ProviderErrorCode::UrlUnreachable, 'Endpoint not found — check the configured URL'],
|
||||||
|
default => [ProviderErrorCode::Unknown, "HTTP {$code}: " . $e->getMessage()],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
$msg = $e->getMessage();
|
||||||
|
$lower = strtolower($msg);
|
||||||
|
$error = match (true) {
|
||||||
|
str_contains($msg, 'not found') || str_contains($msg, 'Could not resolve') => ProviderErrorCode::UserNotFound,
|
||||||
|
str_contains($msg, 'GraphQL error')
|
||||||
|
&& (str_contains($lower, 'unauthorized') || str_contains($lower, 'bad credentials')) => ProviderErrorCode::AuthFailed,
|
||||||
|
default => ProviderErrorCode::Unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
return [$error, $msg];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
enum ProviderErrorCode: string
|
||||||
|
{
|
||||||
|
case AuthFailed = 'auth_failed';
|
||||||
|
case UrlUnreachable = 'url_unreachable';
|
||||||
|
case UserNotFound = 'user_not_found';
|
||||||
|
case Unknown = 'unknown';
|
||||||
|
}
|
||||||
@@ -10,4 +10,10 @@ interface ProviderInterface
|
|||||||
public function fetch(): array;
|
public function fetch(): array;
|
||||||
|
|
||||||
public function isConfigured(): bool;
|
public function isConfigured(): bool;
|
||||||
|
|
||||||
|
public function getName(): string;
|
||||||
|
|
||||||
|
public function probe(): ProviderStatus;
|
||||||
|
|
||||||
|
public function ping(): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
final class ProviderStatus
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly string $name,
|
||||||
|
public readonly ProviderStatusType $status,
|
||||||
|
public readonly ?ProviderErrorCode $error = null,
|
||||||
|
public readonly ?string $message = null,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** @return array<string, string> */
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
$data = ['status' => $this->status->value];
|
||||||
|
if ($this->error !== null) {
|
||||||
|
$data['error'] = $this->error->value;
|
||||||
|
}
|
||||||
|
if ($this->message !== null) {
|
||||||
|
$data['message'] = $this->message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
enum ProviderStatusType: string
|
||||||
|
{
|
||||||
|
case Ok = 'ok';
|
||||||
|
case Error = 'error';
|
||||||
|
case NotConfigured = 'not_configured';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user