diff --git a/app/Services/ApiClient.php b/app/Services/ApiClient.php index 84242f3..0da6c09 100644 --- a/app/Services/ApiClient.php +++ b/app/Services/ApiClient.php @@ -2,22 +2,34 @@ namespace App\Services; use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Log; +use Illuminate\Http\Client\PendingRequest; +use Illuminate\Http\Client\RequestException; +use Closure; +use App\Http\Responses\ApiResponse; class ApiClient { protected string $baseUrl; protected string $token; - public function __construct(string $baseUrl = null,string $token = null) + protected int $timeout = 10; + protected int $connectTimeout = 5; + protected int $retryTimes = 0; + protected int $retryDelay = 100; + + protected ?Closure $fallbackHandler = null; + protected bool $enableLogging = true; + + public function __construct(string $baseUrl = '', string $token = '') { - $this->baseUrl = $baseUrl; + $this->baseUrl = rtrim($baseUrl, '/'); $this->token = $token; } - - public function set(string $baseUrl = null,string $token = null):self + + public function setBaseUrl(string $url): self { - $this->baseUrl = $baseUrl; - $this->token = $token; + $this->baseUrl = rtrim($url, '/'); return $this; } @@ -27,39 +39,147 @@ class ApiClient return $this; } - public function setBaseUrl(string $url): self + public function setTimeout(int $seconds): self { - $this->baseUrl = rtrim($url, '/'); + $this->timeout = $seconds; return $this; } - public function withDefaultHeaders(): \Illuminate\Http\Client\PendingRequest + public function setConnectTimeout(int $seconds): self { - return Http::withHeaders([ - 'Accept' => 'application/json', - 'Authorization' => 'Bearer ' . $this->token, - ]); + $this->connectTimeout = $seconds; + return $this; } + public function setRetry(int $times, int $delayMs = 100): self + { + $this->retryTimes = $times; + $this->retryDelay = $delayMs; + return $this; + } + + public function setFallbackHandler(?Closure $handler): self + { + $this->fallbackHandler = $handler; + return $this; + } + + public function enableLogging(bool $value = true): self + { + $this->enableLogging = $value; + return $this; + } + + protected function withDefaultHeaders(): PendingRequest + { + $request = Http::connectTimeout($this->connectTimeout) + ->timeout($this->timeout) + ->withHeaders([ + 'Accept' => 'application/json', + 'Authorization' => 'Bearer ' . $this->token, + ]); + + if ($this->retryTimes > 0) { + $request = $request->retry($this->retryTimes, $this->retryDelay, function ($exception, $request) { + if ($this->enableLogging) { + Log::warning('API retrying...', [ + 'url' => $request->url(), + 'exception' => $exception->getMessage(), + ]); + } + return true; + }); + } + + return $request; + } + + public function requestWithCatch(callable $fn) +{ + try { + return $fn(); + } catch (RequestException $e) { + if ($this->enableLogging) { + Log::error('API Request Failed', [ + 'message' => $e->getMessage(), + 'url' => $e->request?->url(), + 'code' => $e->getCode(), + ]); + } + + if ($this->fallbackHandler instanceof \Closure) { + return call_user_func($this->fallbackHandler, $e); + } + + // 智能 fallback,直接寫在這裡 + $status = $e->response?->status(); + + if ($status >= 500) { + return ApiResponse::error('伺服器錯誤,請稍後再試','SERVER_ERROR',503); + } + + if ($status === 404) { + return ApiResponse::error('遠端 API 路徑不存在','API_NOT_FOUND',404); + } + + if ($status === 401 || $status === 403) { + return ApiResponse::unauthorized(); + } + + if ($e->getCode() === 0) { + return ApiResponse::error('無法連線遠端 API(可能超時或 DNS 錯誤)','CONNECTION_ERROR',504); + } + + return ApiResponse::error('API 請求失敗: ' . $e->getMessage(), 'HTTP_CLIENT_ERROR',$status ?? 400); + } +} + public function get(string $endpoint, array $query = []) { - return $this->withDefaultHeaders()->get("{$this->baseUrl}{$endpoint}", $query); + return $this->requestWithCatch(fn () => + $this->withDefaultHeaders()->get("{$this->baseUrl}{$endpoint}", $query) + ); } public function post(string $endpoint, array $data = []) { - return $this->withDefaultHeaders()->post("{$this->baseUrl}{$endpoint}", $data); + return $this->requestWithCatch(fn () => + $this->withDefaultHeaders()->post("{$this->baseUrl}{$endpoint}", $data) + ); + } + + public function put(string $endpoint, array $data = []) + { + return $this->requestWithCatch(fn () => + $this->withDefaultHeaders()->put("{$this->baseUrl}{$endpoint}", $data) + ); + } + + public function patch(string $endpoint, array $data = []) + { + return $this->requestWithCatch(fn () => + $this->withDefaultHeaders()->patch("{$this->baseUrl}{$endpoint}", $data) + ); + } + + public function delete(string $endpoint, array $data = []) + { + return $this->requestWithCatch(fn () => + $this->withDefaultHeaders()->delete("{$this->baseUrl}{$endpoint}", $data) + ); } public function upload(string $endpoint, array $files = [], array $data = []) { - $request = $this->withDefaultHeaders(); + return $this->requestWithCatch(function () use ($endpoint, $files, $data) { + $request = $this->withDefaultHeaders(); - foreach ($files as $key => $filePath) { - $filename = basename($filePath); - $request = $request->attach($key, fopen($filePath, 'r'), $filename); - } + foreach ($files as $key => $filePath) { + $filename = basename($filePath); + $request = $request->attach($key, fopen($filePath, 'r'), $filename); + } - return $request->withoutVerifying()->post("{$this->baseUrl}{$endpoint}", $data); + return $request->withoutVerifying()->post("{$this->baseUrl}{$endpoint}", $data); + }); } } \ No newline at end of file