Files

308 lines
9.5 KiB
PHP
Raw Permalink Normal View History

2026-03-29 15:28:24 +03:00
<?php
namespace App\Services;
use App\DTOs\GutendexBook;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class GutendexClient
{
protected string $baseUrl;
protected int $timeout;
protected int $retryMaxAttempts;
protected int $retryBackoff;
protected bool $cacheEnabled;
protected int $cacheTtl;
protected int $rateLimit;
public function __construct()
{
$this->baseUrl = config('gutendex.api_url', 'https://gutendex.com');
$this->timeout = config('gutendex.timeout', 30);
$this->retryMaxAttempts = config('gutendex.retry.max_attempts', 3);
$this->retryBackoff = config('gutendex.retry.backoff_multiplier', 2);
$this->cacheEnabled = config('gutendex.cache.enabled', true);
$this->cacheTtl = config('gutendex.cache.ttl', 86400);
$this->rateLimit = config('gutendex.rate_limit_delay', 1000);
}
/**
* Search for books with optional query and filters.
*
* @param string|null $query Search query
* @param int $page Page number
* @param array $filters Additional filters (language, topic, author, etc.)
* @return array API response with results
*/
public function searchBooks(?string $query = null, int $page = 1, array $filters = []): array
{
$params = array_merge(['page' => $page], $filters);
if ($query) {
$params['search'] = $query;
}
$cacheKey = 'gutendex:search:' . md5(json_encode($params));
if ($this->cacheEnabled && Cache::has($cacheKey)) {
Log::info('Gutendex: Cache hit for search', ['params' => $params]);
return Cache::get($cacheKey);
}
$response = $this->makeRequest('GET', '/books', $params);
if ($this->cacheEnabled && isset($response['results'])) {
Cache::put($cacheKey, $response, $this->cacheTtl);
}
return $response;
}
/**
* Get a specific book by its Gutenberg ID.
*
* @param int $gutenbergId The Gutenberg book ID
* @return GutendexBook|null
*/
public function getBook(int $gutenbergId): ?GutendexBook
{
$cacheKey = 'gutendex:book:' . $gutenbergId;
if ($this->cacheEnabled && Cache::has($cacheKey)) {
Log::info('Gutendex: Cache hit for book', ['id' => $gutenbergId]);
return Cache::get($cacheKey);
}
try {
$response = $this->makeRequest('GET', "/books/{$gutenbergId}");
if (empty($response)) {
return null;
}
$book = GutendexBook::fromApiResponse($response);
if ($this->cacheEnabled) {
Cache::put($cacheKey, $book, $this->cacheTtl);
}
return $book;
} catch (\Exception $e) {
Log::error('Gutendex: Failed to fetch book', [
'id' => $gutenbergId,
'error' => $e->getMessage(),
]);
return null;
}
}
/**
* Get books by language.
*
* @param string $language Language code (e.g., 'en', 'fr')
* @param int $page Page number
* @return array
*/
public function getBooksByLanguage(string $language = 'en', int $page = 1): array
{
return $this->searchBooks(null, $page, ['languages' => $language]);
}
/**
* Get books filtered by copyright status.
*
* @param string $copyright 'true', 'false', or 'null'
* @param int $page Page number
* @return array
*/
public function getBooksByCopyright(string $copyright = 'false', int $page = 1): array
{
return $this->searchBooks(null, $page, ['copyright' => $copyright]);
}
/**
* Fetch full text content from a given URL.
*
* @param string $textUrl URL to the full text
* @return string|null The text content or null on failure
*/
public function fetchFullText(string $textUrl): ?string
{
try {
Log::info('Gutendex: Fetching full text', ['url' => $textUrl]);
$response = Http::timeout($this->timeout * 2)
->retry($this->retryMaxAttempts, $this->retryBackoff * 1000)
->get($textUrl);
if ($response->successful()) {
$text = $response->body();
Log::info('Gutendex: Successfully fetched full text', [
'url' => $textUrl,
'size' => strlen($text),
]);
return $text;
}
Log::warning('Gutendex: Failed to fetch full text', [
'url' => $textUrl,
'status' => $response->status(),
]);
return null;
} catch (ConnectionException $e) {
Log::error('Gutendex: Connection error fetching full text', [
'url' => $textUrl,
'error' => $e->getMessage(),
]);
return null;
} catch (\Exception $e) {
Log::error('Gutendex: Error fetching full text', [
'url' => $textUrl,
'error' => $e->getMessage(),
]);
return null;
}
}
/**
* Get books with pagination info.
*
* @param array $filters Filters to apply
* @param int $page Page number
* @return array Contains 'books' (GutendexBook[]), 'count', 'next', 'previous'
*/
public function getBooksWithPagination(array $filters = [], int $page = 1): array
{
$response = $this->searchBooks(null, $page, $filters);
$books = array_map(
fn($bookData) => GutendexBook::fromApiResponse($bookData),
$response['results'] ?? []
);
return [
'books' => $books,
'count' => $response['count'] ?? 0,
'next' => $response['next'] ?? null,
'previous' => $response['previous'] ?? null,
];
}
/**
* Check if the Gutendex API is accessible.
*
* @return bool
*/
public function checkConnection(): bool
{
try {
$response = Http::timeout(10)->get($this->baseUrl . '/books?page=1');
return $response->successful();
} catch (\Exception $e) {
Log::error('Gutendex: Connection check failed', ['error' => $e->getMessage()]);
return false;
}
}
/**
* Make an HTTP request to the Gutendex API with retry logic.
*
* @param string $method HTTP method
* @param string $endpoint API endpoint
* @param array $params Query parameters
* @return array Response data
* @throws RequestException
*/
protected function makeRequest(string $method, string $endpoint, array $params = []): array
{
$url = $this->baseUrl . $endpoint;
$startTime = microtime(true);
try {
$response = Http::timeout($this->timeout)
->retry(
$this->retryMaxAttempts,
function ($exception, $request) {
// Exponential backoff
return $this->retryBackoff * 1000;
},
function ($exception, $request) {
// Retry on connection errors and 429 rate limit
if ($exception instanceof ConnectionException) {
return true;
}
if (method_exists($exception, 'response')) {
$response = $exception->response;
return $response && $response->status() === 429;
}
return false;
}
)
->acceptJson()
->$method($url, $params);
$duration = microtime(true) - $startTime;
if ($response->successful()) {
Log::info('Gutendex: API request successful', [
'method' => $method,
'endpoint' => $endpoint,
'duration' => round($duration, 2) . 's',
]);
// Add rate limit delay
if ($this->rateLimit > 0) {
usleep($this->rateLimit * 1000);
}
return $response->json();
}
Log::warning('Gutendex: API request failed', [
'method' => $method,
'endpoint' => $endpoint,
'status' => $response->status(),
'duration' => round($duration, 2) . 's',
]);
return [];
} catch (ConnectionException $e) {
Log::error('Gutendex: Connection error', [
'method' => $method,
'endpoint' => $endpoint,
'error' => $e->getMessage(),
]);
throw $e;
} catch (RequestException $e) {
$duration = microtime(true) - $startTime;
Log::error('Gutendex: Request exception', [
'method' => $method,
'endpoint' => $endpoint,
'error' => $e->getMessage(),
'duration' => round($duration, 2) . 's',
]);
throw $e;
}
}
/**
* Clear all cached Gutendex data.
*
* @return void
*/
public function clearCache(): void
{
Cache::flush();
Log::info('Gutendex: Cache cleared');
}
}