From 71bfb38028ed519119ab58aab7397c205f1b0446 Mon Sep 17 00:00:00 2001 From: ArthurErlich Date: Sat, 30 May 2026 14:13:07 +0200 Subject: [PATCH] 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 --- src/Service/ProbeTrait.php | 63 ++++++++++++++++++++++++++++++ src/Service/ProviderErrorCode.php | 13 ++++++ src/Service/ProviderInterface.php | 6 +++ src/Service/ProviderStatus.php | 29 ++++++++++++++ src/Service/ProviderStatusType.php | 12 ++++++ 5 files changed, 123 insertions(+) create mode 100644 src/Service/ProbeTrait.php create mode 100644 src/Service/ProviderErrorCode.php create mode 100644 src/Service/ProviderStatus.php create mode 100644 src/Service/ProviderStatusType.php diff --git a/src/Service/ProbeTrait.php b/src/Service/ProbeTrait.php new file mode 100644 index 0000000..af86c33 --- /dev/null +++ b/src/Service/ProbeTrait.php @@ -0,0 +1,63 @@ +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]; + } +} diff --git a/src/Service/ProviderErrorCode.php b/src/Service/ProviderErrorCode.php new file mode 100644 index 0000000..d9c484c --- /dev/null +++ b/src/Service/ProviderErrorCode.php @@ -0,0 +1,13 @@ + */ + 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; + } +} diff --git a/src/Service/ProviderStatusType.php b/src/Service/ProviderStatusType.php new file mode 100644 index 0000000..29031e1 --- /dev/null +++ b/src/Service/ProviderStatusType.php @@ -0,0 +1,12 @@ +