187 lines
5.2 KiB
PHP
187 lines
5.2 KiB
PHP
<?php
|
|
|
|
namespace App\Services\JobApi\Clients;
|
|
|
|
use App\Models\JobSource;
|
|
use App\Services\JobApi\DTO\JobData;
|
|
use App\Services\JobApi\Exceptions\ApiException;
|
|
use App\Services\JobApi\Exceptions\RateLimitException;
|
|
use App\Services\JobApi\Transformers\JobTransformer;
|
|
use Illuminate\Http\Client\PendingRequest;
|
|
use Illuminate\Http\Client\Response;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
abstract class JobApiClient
|
|
{
|
|
protected PendingRequest $client;
|
|
protected JobTransformer $transformer;
|
|
protected int $maxRetries = 3;
|
|
protected int $retryDelay = 1000; // milliseconds
|
|
|
|
public function __construct(
|
|
protected JobSource $source
|
|
) {
|
|
$this->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<JobData> 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()]);
|
|
}
|
|
}
|