Files
2026-04-03 00:23:01 +03:00

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()]);
}
}