statusCode; } public function apiCode(): string { return $this->apiCode; } } class hetzner_cloud_client { private const BASE_URL = 'https://api.hetzner.cloud/v1'; public function __construct(private readonly string $token, private readonly int $timeoutSeconds = 8) { if (trim($token) === '') { throw new RuntimeException('Hetzner Cloud API token is required.'); } } public function getLoadBalancer(int|string $id): array { return $this->request('GET', '/load_balancers/' . rawurlencode((string)$id))['load_balancer'] ?? []; } public function addIpTarget(int|string $loadBalancerId, string $ip): array { return $this->request('POST', '/load_balancers/' . rawurlencode((string)$loadBalancerId) . '/actions/add_target', [ 'type' => 'ip', 'ip' => ['ip' => $ip], ]); } public function removeIpTarget(int|string $loadBalancerId, string $ip): array { return $this->request('POST', '/load_balancers/' . rawurlencode((string)$loadBalancerId) . '/actions/remove_target', [ 'type' => 'ip', 'ip' => ['ip' => $ip], ]); } public function addService(int|string $loadBalancerId, string $protocol, int $listenPort, int $destinationPort): array { return $this->request('POST', '/load_balancers/' . rawurlencode((string)$loadBalancerId) . '/actions/add_service', [ 'protocol' => strtolower($protocol), 'listen_port' => $listenPort, 'destination_port' => $destinationPort, 'proxyprotocol' => false, ]); } private function request(string $method, string $path, ?array $payload = null): array { $curl = curl_init(self::BASE_URL . $path); if ($curl === false) { throw new RuntimeException('Could not initialize Hetzner Cloud API request.'); } $headers = [ 'Accept: application/json', 'Authorization: Bearer ' . trim($this->token), ]; curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_CUSTOMREQUEST, strtoupper($method)); curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, min(3, $this->timeoutSeconds)); curl_setopt($curl, CURLOPT_TIMEOUT, max(1, $this->timeoutSeconds)); curl_setopt($curl, CURLOPT_NOSIGNAL, true); curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); if ($payload !== null) { $body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($body === false) { throw new RuntimeException('Could not encode Hetzner Cloud API payload.'); } $headers[] = 'Content-Type: application/json'; curl_setopt($curl, CURLOPT_POSTFIELDS, $body); } curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); $raw = curl_exec($curl); $error = curl_error($curl); $status = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE); curl_close($curl); if ($raw === false) { throw new RuntimeException('Hetzner Cloud API request failed: ' . $error); } $decoded = trim((string)$raw) === '' ? [] : json_decode((string)$raw, true); if (!is_array($decoded)) { $decoded = ['raw' => (string)$raw]; } if ($status < 200 || $status >= 300) { $errorPayload = is_array($decoded['error'] ?? null) ? $decoded['error'] : []; $apiCode = (string)($errorPayload['code'] ?? $decoded['code'] ?? ''); $message = (string)($errorPayload['message'] ?? $decoded['message'] ?? ('HTTP ' . $status)); throw new hetzner_cloud_api_exception('Hetzner Cloud API request failed: ' . $message, $status, $apiCode); } return $decoded; } }