apiUrl = $config->getRequired('WHMCS_API_URL'); $this->identifier = $config->getRequired('WHMCS_API_IDENTIFIER'); $this->secret = $config->getRequired('WHMCS_API_SECRET'); } /** * Call a WHMCS API action with optional parameters. * * Retries up to 3 times with exponential backoff on failure. * * @return array The decoded JSON response. * * @throws RuntimeException On final failure after all retries. */ public function call(string $action, array $params = []): array { $postFields = array_merge($params, [ 'identifier' => $this->identifier, 'secret' => $this->secret, 'action' => $action, 'responsetype' => 'json', ]); $this->logger->debug("WHMCS API call: {$action}", [ 'params' => array_diff_key($params, ['identifier' => 1, 'secret' => 1]), ]); $lastException = null; for ($attempt = 0; $attempt < self::MAX_RETRIES; $attempt++) { try { $result = $this->doRequest($postFields); $this->logger->debug("WHMCS API response: {$action}", [ 'result' => $result['result'] ?? 'unknown', ]); return $result; } catch (RuntimeException $e) { $lastException = $e; $backoff = self::BACKOFF_SECONDS[$attempt] ?? 4; $retryMessage = sprintf( 'WHMCS API call failed (attempt %d/%d): %s, retrying in %ds', $attempt + 1, self::MAX_RETRIES, $e->getMessage(), $backoff, ); $this->logger->warning($retryMessage); if ($attempt < self::MAX_RETRIES - 1) { sleep($backoff); } } } throw new RuntimeException( "WHMCS API call '{$action}' failed after " . self::MAX_RETRIES . " attempts: " . $lastException->getMessage(), 0, $lastException, ); } /** * Execute the cURL request to the WHMCS API. * * @return array * * @throws RuntimeException On cURL or JSON decode error. */ private function doRequest(array $postFields): array { $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $this->apiUrl, CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query($postFields), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 60, CURLOPT_CONNECTTIMEOUT => 15, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, CURLOPT_HTTPHEADER => [ 'Content-Type: application/x-www-form-urlencoded', 'Accept: application/json', ], ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlError = curl_error($ch); $curlErrno = curl_errno($ch); curl_close($ch); if ($curlErrno !== 0) { throw new RuntimeException("cURL error ({$curlErrno}): {$curlError}"); } if ($httpCode < 200 || $httpCode >= 300) { throw new RuntimeException("HTTP {$httpCode} response from WHMCS API"); } if (! is_string($response) || $response === '') { throw new RuntimeException('Empty response from WHMCS API'); } $decoded = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new RuntimeException('Failed to decode WHMCS API response: ' . json_last_error_msg()); } if (isset($decoded['result']) && $decoded['result'] === 'error') { throw new RuntimeException('WHMCS API error: ' . ($decoded['message'] ?? 'Unknown error')); } return $decoded; } }