client = $this->buildHttpClient(); $this->transformer = $this->getTransformer(); } /** * Build the HTTP client with base configuration */ protected function buildHttpClient(): PendingRequest { $client = Http::timeout(30) ->retry($this->maxRetries, $this->retryDelay, function ($exception, $request) { // Retry on server errors (5xx) but not on client errors (4xx) if ($exception instanceof \Illuminate\Http\Client\RequestException) { $statusCode = $exception->response?->status(); return $statusCode >= 500 && $statusCode < 600; } return false; }) ->withHeaders($this->getHeaders()); if ($this->source->api_key) { $client = $this->attachAuthentication($client); } return $client; } /** * Get the transformer instance for this API */ abstract protected function getTransformer(): JobTransformer; /** * Get default headers for API requests */ protected function getHeaders(): array { return [ 'Accept' => 'application/json', 'User-Agent' => 'JobifyGarden/1.0', ]; } /** * Attach authentication to the HTTP client */ abstract protected function attachAuthentication(PendingRequest $client): PendingRequest; /** * Fetch jobs from the API * * @param array $params Query parameters (keywords, location, page, etc.) * @return array Array of standardized job data */ abstract public function fetchJobs(array $params = []): array; /** * Make an API request with error handling and logging */ protected function makeRequest(string $method, string $endpoint, array $params = []): Response { $url = $this->buildUrl($endpoint); Log::info("API Request: {$this->source->name}", [ 'method' => $method, 'url' => $url, 'params' => $params, ]); try { $response = match (strtoupper($method)) { 'GET' => $this->client->get($url, $params), 'POST' => $this->client->post($url, $params), default => throw new ApiException( "Unsupported HTTP method: {$method}", $this->source->name ), }; if ($response->failed()) { $this->handleFailedResponse($response); } Log::info("API Response: {$this->source->name}", [ 'status' => $response->status(), 'url' => $url, ]); return $response; } catch (\Illuminate\Http\Client\ConnectionException $e) { throw new ApiException( "Connection failed to {$this->source->name}", $this->source->name, ['error' => $e->getMessage()], 0, $e ); } } /** * Build full URL from endpoint */ protected function buildUrl(string $endpoint): string { $baseUrl = rtrim($this->source->api_url, '/'); $endpoint = ltrim($endpoint, '/'); return "{$baseUrl}/{$endpoint}"; } /** * Handle failed API responses */ protected function handleFailedResponse(Response $response): void { $statusCode = $response->status(); if ($statusCode === 429) { $retryAfter = $response->header('Retry-After'); throw new RateLimitException( $this->source->name, $retryAfter ? (int) $retryAfter : null, ['response' => $response->body()] ); } throw new ApiException( "API request failed with status {$statusCode}", $this->source->name, [ 'status' => $statusCode, 'response' => $response->body(), ], $statusCode ); } /** * Get the source name */ public function getSourceName(): string { return $this->source->name; } /** * Check if the API is available */ public function isAvailable(): bool { return $this->source->is_active; } /** * Update the last fetched timestamp */ protected function updateLastFetched(): void { $this->source->update(['last_fetched_at' => now()]); } }