Skip to content

Commit

Permalink
Improve public API
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod committed Nov 16, 2024
1 parent fbd5bbd commit 6a22e17
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 33 deletions.
39 changes: 24 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This package contains classes used to parse, validate and manipulate the [Cache-

## Usage

Unless explicitly stated, all the classes discribed hereafter are immutable.
Unless explicitly stated, all the classes described hereafter are immutable.

### Parsing

Expand All @@ -27,14 +27,23 @@ $cacheStatus = Field::fromHttpValue($headerLine, $statusCode);
### Field Container

The `Field` class is a container whose members are handled request cache information as they are added by the various
server. The class implements PHP's `IteratorAggregate`, `ArrayAccess`, `Countable` and `Stringable` interface.
servers. The class implements PHP's `IteratorAggregate`, `ArrayAccess`, `Countable` and `Stringable` interface.

```php
echo $cacheStatus; // returns 'ReverseProxyCache; hit, ForwardProxyCache; fwd=uri-miss; collapsed; stored';
echo $cacheStatus[1]; // returns 'ForwardProxyCache; fwd=uri-miss; collapsed; stored';
$cacheStatus[1]; // returns a HandledRequestCache instance
count($cacheStatus); // returns 2
```

You can also determine if a specific handled cache request exist either by supplying the cache index or its server identifier

```php
$cacheStatus->contains(Token::fromString('foobar')); // returns false
$cacheStatus->indexOf(Token::fromString('foobar')); // returns null
$cacheStatus->contains(Token::fromString('ReverseProxyCache')); // returns true
$cacheStatus->indexOf(Token::fromString('ReverseProxyCache')); // returns 0
```

As per the RFC the `closestToOrigin` and `closestToUser` methods give you access to the caches closest to the
origin server and the one closest to the client (user).

Expand All @@ -43,21 +52,20 @@ $cacheClosestToTheOrigin = $cacheStatus->closestToOrigin(); // the handled reque
$cacheClosestToTheClient = $cacheStatus->closestToUser(); // the handled request cache closest to the user
```

### The Handled Request Cache object

Both methods return `null` if the cache does not exist or a `HandledRequestCache` instance.


### The Handled Request Cache object

```php
$cacheClosestToTheOrigin = $cacheStatus->closestToOrigin(); // the handled request cache closest to the origin server
$cacheClosestToTheClient = $cacheStatus->closestToUser(); // the handled request cache closest to the user
$cacheClosestToTheOrigin->hit; // return true
$cacheClosestToTheOrigin->forward; // return null
$cacheClosestToTheClient->hit; // return false
$cacheClosestToTheClient->forward->reason; // return ForwardReason::UriMiss
$cacheClosestToTheClient->forward->statusCode; // return 304
```

A `HandledRequestCache` instance contains information about the cache and how it handled the current message.
A `HandledRequestCache` instance contains information about the cache and how it was handled for the current message.
In particular, in compliance with the RFC, if the `forward` property is present you will get extra information
regarding the reason why the cache was forwarded.

Expand Down Expand Up @@ -102,17 +110,18 @@ echo $response->getHeaderLine(Field::NAME);
**While we used PSR-7 ResponseInterface, The package parsing and serializing methods can use any HTTP abstraction package or PHP `$_SERVER` array.**

```php
$cacheStatus = Field::fromSapiServer($_SERVER, 'HTTP_CACHE_STATUS');
$cacheStatus = Field::fromSapiServer($_SERVER, Field::SAPI_NAME);
$newCacheStatus = $cacheStatus->push('BrowserCache; fwd=uri-miss');

header(Field::NAME.': '.$newCacheStatus);
```

In this last example we use PHP native function to parse and add the correct header to the HTTP response.
In this last example we use PHP native function to parse and add the correct header to the PHP emitted HTTP response.

## Structured Field
## Structured Fields

This Header field is compliant with the HTTP Structured Field RFC. As such we take advantage of that fact by using
the [HTTP Structured Fields for PHP](https://github.com/bakame-php/http-structured-fields) v2.0, which remove all the
boilerplate needed for such header to be parsable and manipulable in PHP while staying compliant with all the different
RFC.
Because the Header field is compliant with the HTTP Structured Field RFC. We can easily parse it **but** were are also
validating it against its specific RFC rules. To do so the package is dependent on
the [HTTP Structured Fields for PHP](https://github.com/bakame-php/http-structured-fields) v2.0 package, which remove all the
boilerplate needed for such header to be parsed, validated and manipulated in PHP while staying compliant with all the different
RFCs.
57 changes: 41 additions & 16 deletions src/Field.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ class Field implements ArrayAccess, IteratorAggregate, Countable, StructuredFiel
/** @var array<HandledRequestCache> */
private array $caches;

public function __construct(HandledRequestCache|Stringable|string ...$caches)
public function __construct(HandledRequestCache|Item|StructuredFieldProvider|Stringable|string ...$caches)
{
$this->caches = array_map(fn (HandledRequestCache|Stringable|string $value) => match (true) {
$this->caches = array_map(fn (StructuredFieldProvider|Item|Stringable|string $value) => match (true) {
$value instanceof HandledRequestCache => $value,
$value instanceof StructuredFieldProvider,
$value instanceof Item => HandledRequestCache::fromStructuredField($value),
default => HandledRequestCache::fromHttpValue($value),
}, $caches);
}
Expand All @@ -45,25 +47,35 @@ public function __construct(HandledRequestCache|Stringable|string ...$caches)
* Returns an instance from PHP SAPI.
*
* @param array<string, string> $server
*
* @throws Exception If the field is not found
*/
public static function fromSapi(array $server = [], string $name = self::SAPI_NAME): self
{
return self::fromHttpValue($server[$name] ?? '');
if (!array_key_exists($name, $server)) {
throw new Exception('The field `'.$name.'` is not present.');
}

return self::fromHttpValue($server[$name]);
}

/**
* Returns an instance from a Header Line and the optional response status code.
*/
public static function fromHttpValue(Stringable|string $httpHeaderLine = '', ?int $statusCode = null): self
public static function fromHttpValue(OuterList|StructuredFieldProvider|Stringable|string $list = '', ?int $statusCode = null): self
{
return self::fromStructuredField(OuterList::fromHttpValue($httpHeaderLine), $statusCode);
}
if ($list instanceof StructuredFieldProvider) {
$className = $list::class;
$list = $list->toStructuredField();
if (!$list instanceof OuterList) {
throw new Exception('The structured field provider `'.$className.'` must return an '.OuterList::class.' data type.');
}
}

if (!$list instanceof OuterList) {
$list = OuterList::fromHttpValue($list);
}

/**
* Returns an instance from a Structured Field List and the optional response status code.
*/
private static function fromStructuredField(OuterList $list, ?int $statusCode = null): self
{
return new self(...$list->map(
fn (Item|InnerList $item, int $offset): HandledRequestCache => match (true) {
$item instanceof Item => HandledRequestCache::fromStructuredField($item, $statusCode),
Expand Down Expand Up @@ -188,14 +200,27 @@ public function has(int ...$indexes): bool
*/
public function contains(Token|string $serverIdentifier): bool
{
$validate = fn (Token|string $token): bool => $token instanceof Token ? $token->equals($serverIdentifier) : $token === $serverIdentifier;
foreach ($this->caches as $member) {
return null !== $this->indexOf($serverIdentifier);
}

/**
* Returns the index for a Handled request cache based on the provided server identifier.
*
* Return the index or null if the index does not exist.
*/
public function indexOf(Token|string $serverIdentifier): ?int
{
$validate = fn (Token|string $token): bool => match (true) {
$token instanceof Token => $token->equals($serverIdentifier),
default => $token === $serverIdentifier,
};
foreach ($this->caches as $offset => $member) {
if ($validate($member->servedBy)) {
return true;
return $offset;
}
}

return false;
return null;
}

/**
Expand Down Expand Up @@ -227,7 +252,7 @@ public function offsetSet(mixed $offset, mixed $value): void
/**
* Append a new handled request cache at the end of the field.
*/
public function push(HandledRequestCache|Stringable|string ...$values): self
public function push(HandledRequestCache|Item|StructuredFieldProvider|Stringable|string ...$values): self
{
return match ($values) {
[] => $this,
Expand Down
10 changes: 9 additions & 1 deletion src/HandledRequestCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,20 @@ public static function fromHttpValue(Stringable|string $value, ?int $statusCode
/**
* Returns an instance from a Structured Field Item and the optional response status code.
*/
public static function fromStructuredField(Item $item, ?int $statusCode = null): self
public static function fromStructuredField(StructuredFieldProvider|Item $item, ?int $statusCode = null): self
{
if (null !== $statusCode && ($statusCode < 100 || $statusCode > 599)) {
throw new Exception('The default forward status code must be a valid HTTP status code when present.');
}

if ($item instanceof StructuredFieldProvider) {
$className = $item::class;
$item = $item->toStructuredField();
if (!$item instanceof Item) {
throw new Exception('The structured field provider `'.$className.'` must return an '.Item::class.' data type.');
}
}

$validation = self::validator()->validate($item);
if ($validation->isFailed()) {
throw new Exception('The submitted item is an invalid handled request cache status', previous: $validation->errors->toException());
Expand Down
3 changes: 2 additions & 1 deletion src/HandledRequestCacheTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ public function parsing_a_response_header(): void
self::assertFalse($fieldList->contains('Foobar'));
self::assertTrue($fieldList->contains(Token::fromString('BrowserCache')));
self::assertFalse($fieldList->contains('BrowserCache'));

self::assertSame(2, $fieldList->indexOf(Token::fromString('BrowserCache')));
self::assertNull($fieldList->indexOf('foobar'));
self::assertTrue($closestToOrigin->hit);
self::assertFalse($intermediary->hit);
self::assertFalse($closestToClient->hit);
Expand Down

0 comments on commit 6a22e17

Please sign in to comment.