Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
5c61f33
Initial plan
Copilot Nov 28, 2025
c29537f
Add middleware/interceptor system for request/response transformation
Copilot Nov 28, 2025
3d07422
Fix code review comments: improve docblock and JSON validation
Copilot Nov 28, 2025
621c234
Update src/Fetch/Interfaces/ClientHandler.php
Thavarshan Nov 29, 2025
4665e4d
Update src/Fetch/Concerns/SupportsMiddleware.php
Thavarshan Nov 29, 2025
9b25fa1
Update src/Fetch/Concerns/SupportsMiddleware.php
Thavarshan Nov 29, 2025
eba3dd5
Initial plan
Copilot Nov 28, 2025
d9122a1
Add connection pooling and HTTP/2 support implementation
Copilot Nov 28, 2025
5892023
Fix code review issues: separate curl multi options, fix URI in excep…
Copilot Nov 28, 2025
9428c55
Simplify metrics incrementing, improve curl options comment
Copilot Nov 28, 2025
95539e4
Fix code style issues: proper namespace imports and class element ord…
Copilot Nov 28, 2025
7ab3d2b
Update src/Fetch/Concerns/ManagesConnectionPool.php
Thavarshan Nov 28, 2025
6ef63be
Update src/Fetch/Concerns/ManagesConnectionPool.php
Thavarshan Nov 28, 2025
25061d0
Update src/Fetch/Pool/DnsCache.php
Thavarshan Nov 28, 2025
798891b
Update src/Fetch/Concerns/ManagesConnectionPool.php
Thavarshan Nov 28, 2025
56baf59
Update src/Fetch/Pool/HostConnectionPool.php
Thavarshan Nov 28, 2025
af63d60
Update src/Fetch/Concerns/ManagesConnectionPool.php
Thavarshan Nov 28, 2025
b094bb5
Update src/Fetch/Concerns/ManagesConnectionPool.php
Thavarshan Nov 28, 2025
5a167c4
Update src/Fetch/Pool/ConnectionPool.php
Thavarshan Nov 28, 2025
7e8c750
Address code review feedback: improve documentation and fix issues
Copilot Nov 28, 2025
b477660
Initial plan for RFC 7234 HTTP caching support
Copilot Nov 28, 2025
74d4128
Implement RFC 7234 HTTP caching support with memory and file backends
Copilot Nov 28, 2025
a556117
Fix code review issues: improve method signatures and remove deprecat…
Copilot Nov 28, 2025
e08995e
Fix code style issues: reorder class elements and remove superfluous …
Copilot Nov 28, 2025
e956c1a
Update src/Fetch/Cache/CacheKeyGenerator.php
Thavarshan Nov 28, 2025
4feb84c
Update src/Fetch/Cache/CacheInterface.php
Thavarshan Nov 28, 2025
06de2aa
Update src/Fetch/Cache/FileCache.php
Thavarshan Nov 28, 2025
bd31a7b
Update src/Fetch/Cache/FileCache.php
Thavarshan Nov 28, 2025
446203c
Update tests/Unit/CacheTest.php
Thavarshan Nov 28, 2025
dc4cdeb
Update src/Fetch/Concerns/ManagesCache.php
Thavarshan Nov 28, 2025
2a4042b
Fix TTL behavior consistency and use JSON serialization for security
Copilot Nov 28, 2025
8a233c7
Refactor cache interface and related tests; remove unused set method …
Thavarshan Nov 28, 2025
c0efed2
Address code review feedback: status codes consistency, race conditio…
Copilot Nov 28, 2025
e30c597
Refactor executeSyncRequestWithCache method to improve caching suppor…
Thavarshan Nov 28, 2025
51aa72c
Refactor cache interface and implementations to return boolean for se…
Thavarshan Nov 28, 2025
9bc7a75
Refactor middleware class definitions for improved readability
Thavarshan Nov 29, 2025
d971e7b
Address code review comments: improve stream handling, add validation…
Copilot Nov 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
bootstrap="vendor/autoload.php"
beStrictAboutTestsThatDoNotTestAnything="false"
colors="true"
processIsolation="false"
Expand Down
257 changes: 257 additions & 0 deletions src/Fetch/Cache/CacheControl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
<?php

declare(strict_types=1);

namespace Fetch\Cache;

use Psr\Http\Message\ResponseInterface;

/**
* Parses and handles Cache-Control headers according to RFC 7234.
*/
class CacheControl
{
/**
* Parsed Cache-Control directives.
*
* @var array<string, mixed>
*/
private array $directives = [];

/**
* Create a new CacheControl instance.
*
* @param array<string, mixed> $directives The parsed directives
*/
public function __construct(array $directives = [])
{
$this->directives = $directives;
}

/**
* Parse a Cache-Control header string.
*/
public static function parse(string $cacheControl): self
{
$directives = [];

foreach (explode(',', $cacheControl) as $directive) {
$directive = trim($directive);
if ($directive === '') {
continue;
}

$parts = explode('=', $directive, 2);
$name = strtolower(trim($parts[0]));
$value = isset($parts[1]) ? trim($parts[1], '"') : true;

// Convert numeric values
if (is_string($value) && is_numeric($value)) {
$value = (int) $value;
}

$directives[$name] = $value;
}

return new self($directives);
}

/**
* Parse Cache-Control from a response.
*/
public static function fromResponse(ResponseInterface $response): self
{
return self::parse($response->getHeaderLine('Cache-Control'));
}

/**
* Build a Cache-Control header string.
*
* @param array<string, mixed> $directives The directives to include
*/
public static function build(array $directives): string
{
$parts = [];

foreach ($directives as $name => $value) {
if ($value === true) {
$parts[] = $name;
} elseif ($value !== false && $value !== null) {
$parts[] = "{$name}={$value}";
}
}

return implode(', ', $parts);
}

/**
* Determine if the response should be cached.
*/
public function shouldCache(ResponseInterface $response, bool $isSharedCache = false): bool
{
// Don't cache if no-store is set
if ($this->hasNoStore()) {
return false;
}

// Don't cache private responses in shared cache
if ($isSharedCache && $this->isPrivate()) {
return false;
}

// Check response status code - using RFC 7234 recommended cacheable status codes
// This list matches the default cache_status_codes in ManagesCache trait
$status = $response->getStatusCode();
$cacheableStatuses = [200, 203, 204, 206, 300, 301, 404, 410];

if (! in_array($status, $cacheableStatuses, true)) {
return false;
}

return true;
}

/**
* Check if the response requires validation before being served.
*/
public function mustRevalidate(): bool
{
return $this->has('must-revalidate') || $this->has('proxy-revalidate');
}

/**
* Check if the response should not be cached.
*/
public function hasNoCache(): bool
{
return $this->has('no-cache');
}

/**
* Check if the response should not be stored at all.
*/
public function hasNoStore(): bool
{
return $this->has('no-store');
}

/**
* Check if the response is private.
*/
public function isPrivate(): bool
{
return $this->has('private');
}

/**
* Check if the response is public.
*/
public function isPublic(): bool
{
return $this->has('public');
}

/**
* Get the max-age directive value.
*/
public function getMaxAge(): ?int
{
return $this->getInt('max-age');
}

/**
* Get the s-maxage directive value (for shared caches).
*/
public function getSharedMaxAge(): ?int
{
return $this->getInt('s-maxage');
}

/**
* Get the stale-while-revalidate directive value.
*/
public function getStaleWhileRevalidate(): ?int
{
return $this->getInt('stale-while-revalidate');
}

/**
* Get the stale-if-error directive value.
*/
public function getStaleIfError(): ?int
{
return $this->getInt('stale-if-error');
}

/**
* Calculate the TTL for the response.
*
* @return int|null The TTL in seconds, or null if not cacheable
*/
public function getTtl(ResponseInterface $response, bool $isSharedCache = false): ?int
{
// For shared caches, s-maxage takes precedence
if ($isSharedCache) {
$sMaxAge = $this->getSharedMaxAge();
if ($sMaxAge !== null) {
return $sMaxAge;
}
}

// Check max-age directive
$maxAge = $this->getMaxAge();
if ($maxAge !== null) {
return $maxAge;
}

// Fall back to Expires header
$expires = $response->getHeaderLine('Expires');
if ($expires !== '') {
$expiresTime = strtotime($expires);
if ($expiresTime !== false) {
return max(0, $expiresTime - time());
}
}

return null;
}

/**
* Check if a directive exists.
*/
public function has(string $directive): bool
{
return isset($this->directives[$directive]);
}

/**
* Get a directive value.
*/
public function get(string $directive): mixed
{
return $this->directives[$directive] ?? null;
}

/**
* Get an integer directive value.
*/
public function getInt(string $directive): ?int
{
$value = $this->get($directive);
if ($value === null) {
return null;
}

return is_int($value) ? $value : (int) $value;
}

/**
* Get all directives.
*
* @return array<string, mixed>
*/
public function getDirectives(): array
{
return $this->directives;
}
}
57 changes: 57 additions & 0 deletions src/Fetch/Cache/CacheInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace Fetch\Cache;

/**
* Interface for cache backends used by the HTTP client.
*/
interface CacheInterface
{
/**
* Get a cached response by key.
*
* @param string $key The cache key
* @return CachedResponse|null The cached response, or null if not found
*/
public function get(string $key): ?CachedResponse;

/**
* Store a response in the cache.
*
* @param string $key The cache key
* @param CachedResponse $cachedResponse The response to cache
* @param int|null $ttl Time to live in seconds
* @return bool True if the item was stored, false otherwise
*/
public function set(string $key, CachedResponse $cachedResponse, ?int $ttl = null): bool;

/**
* Delete a cached response by key.
*
* @param string $key The cache key
* @return bool True if the item was deleted, false otherwise
*/
public function delete(string $key): bool;

/**
* Check if a key exists in the cache.
*
* @param string $key The cache key
* @return bool True if the key exists, false otherwise
*/
public function has(string $key): bool;

/**
* Clear all cached responses.
*/
public function clear(): void;

/**
* Remove expired entries from the cache.
*
* @return int The number of entries removed
*/
public function prune(): int;
}
Loading