Skip to content

Commit afa8da2

Browse files
vjiksamdark
andauthored
Add HttpCacheMiddleware (#6)
Co-authored-by: Alexander Makarov <sam@rmcreative.ru>
1 parent b2d05c9 commit afa8da2

25 files changed

Lines changed: 1292 additions & 0 deletions
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\HttpMiddleware\HttpCache\CacheControlProvider;
6+
7+
use Psr\Http\Message\ServerRequestInterface;
8+
9+
/**
10+
* Interface for `Cache-Control` header value providers. Given a request it generates a header value.
11+
*
12+
* @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control
13+
*/
14+
interface CacheControlProviderInterface
15+
{
16+
/**
17+
* Returns a cache control header value for the given server request.
18+
*
19+
* @param ServerRequestInterface $request The server request for which to generate the cache control value.
20+
* @return string|null The cache control header value or null if no cache control is applicable.
21+
*/
22+
public function get(ServerRequestInterface $request): ?string;
23+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\HttpMiddleware\HttpCache\CacheControlProvider;
6+
7+
use Psr\Http\Message\ServerRequestInterface;
8+
9+
/**
10+
* Returns a predefined cache control header value regardless of request.
11+
*/
12+
final class ConstantCacheControlProvider implements CacheControlProviderInterface
13+
{
14+
public const DEFAULT_VALUE = 'public, max-age=3600';
15+
16+
/**
17+
* @param string $value The cache control header value to return.
18+
*/
19+
public function __construct(
20+
private readonly string $value = self::DEFAULT_VALUE,
21+
) {
22+
}
23+
24+
public function get(ServerRequestInterface $request): ?string
25+
{
26+
return $this->value;
27+
}
28+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\HttpMiddleware\HttpCache\CacheControlProvider;
6+
7+
use Psr\Http\Message\ServerRequestInterface;
8+
9+
/**
10+
* Returns `null` regardless of request.
11+
* It can be used when cache control functionality is not required.
12+
*/
13+
final class NullCacheControlProvider implements CacheControlProviderInterface
14+
{
15+
public function get(ServerRequestInterface $request): ?string
16+
{
17+
return null;
18+
}
19+
}

src/HttpCache/ETag.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\HttpMiddleware\HttpCache;
6+
7+
/**
8+
* Represents an ETag (Entity Tag) used for HTTP caching.
9+
*
10+
* An ETag is a unique identifier assigned to a specific version of a resource.
11+
* It is used by clients to determine if the resource has changed since the last request.
12+
*/
13+
final class ETag
14+
{
15+
/**
16+
* @param string $seed The seed value used to generate the ETag.
17+
* @param bool $weak Indicates whether the ETag is weak (if the content is semantically equal, but not byte-equal).
18+
*/
19+
public function __construct(
20+
public readonly string $seed,
21+
public readonly bool $weak = false,
22+
) {
23+
}
24+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\HttpMiddleware\HttpCache\ETagGenerator;
6+
7+
/**
8+
* Generates a string ETag value using a callable function.
9+
*
10+
* @psalm-type TCallable = callable(string): string
11+
*/
12+
final class CallableETagGenerator implements ETagGeneratorInterface
13+
{
14+
/**
15+
* @var callable
16+
* @psalm-var TCallable
17+
*/
18+
private $callable;
19+
20+
/**
21+
* @param callable $callable A callable function that takes a string seed and returns a string ETag value.
22+
*
23+
* @psalm-param TCallable $callable
24+
*/
25+
public function __construct(callable $callable)
26+
{
27+
$this->callable = $callable;
28+
}
29+
30+
public function generate(string $seed): string
31+
{
32+
return ($this->callable)($seed);
33+
}
34+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\HttpMiddleware\HttpCache\ETagGenerator;
6+
7+
/**
8+
* Generates a string ETag value using a PHP native function {@see base64_encode()} and {@see sha1()}.
9+
*/
10+
final class DefaultETagGenerator implements ETagGeneratorInterface
11+
{
12+
public function generate(string $seed): string
13+
{
14+
return rtrim(base64_encode(sha1($seed, true)), '=');
15+
}
16+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\HttpMiddleware\HttpCache\ETagGenerator;
6+
7+
/**
8+
* Interface defines a method to generate a string ETag value based on a given seed string.
9+
*/
10+
interface ETagGeneratorInterface
11+
{
12+
/**
13+
* Generates a string ETag value based on the provided seed.
14+
*
15+
* @param string $seed The seed value used to generate the ETag.
16+
* @return string The generated string ETag value.
17+
*/
18+
public function generate(string $seed): string;
19+
}

src/HttpCache/ETagHeader.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\HttpMiddleware\HttpCache;
6+
7+
use Yiisoft\HttpMiddleware\HttpCache\ETagGenerator\ETagGeneratorInterface;
8+
9+
/**
10+
* @internal
11+
*/
12+
final class ETagHeader
13+
{
14+
private ?string $value = null;
15+
16+
public function __construct(
17+
private readonly ETag $eTag,
18+
private readonly ETagGeneratorInterface $generator,
19+
) {
20+
}
21+
22+
/**
23+
* Returns the raw ETag value generated from the seed.
24+
*
25+
* @return string The raw ETag value.
26+
*/
27+
public function rawValue(): string
28+
{
29+
return $this->value ??= $this->generator->generate($this->eTag->seed);
30+
}
31+
32+
/**
33+
* Returns the ETag value formatted for use in HTTP headers.
34+
*
35+
* The value is enclosed in double quotes and prefixed with 'W/' if the ETag is weak.
36+
*
37+
* @return string The formatted ETag header value.
38+
*/
39+
public function headerValue(): string
40+
{
41+
$value = '"' . $this->rawValue() . '"';
42+
if ($this->eTag->weak) {
43+
$value = 'W/' . $value;
44+
}
45+
return $value;
46+
}
47+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\HttpMiddleware\HttpCache\ETagProvider;
6+
7+
use Psr\Http\Message\ServerRequestInterface;
8+
use Yiisoft\HttpMiddleware\HttpCache\ETag;
9+
10+
/**
11+
* Obtains {@see ETag} for a given server request.
12+
*/
13+
interface ETagProviderInterface
14+
{
15+
/**
16+
* Returns an {@see ETag} instance for the given server request.
17+
*
18+
* @param ServerRequestInterface $request The server request for which to generate the ETag.
19+
* @return ETag|null Instance of {@see ETag} or null if no ETag can be generated for the request.
20+
*/
21+
public function get(ServerRequestInterface $request): ?ETag;
22+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\HttpMiddleware\HttpCache\ETagProvider;
6+
7+
use Psr\Http\Message\ServerRequestInterface;
8+
use Yiisoft\HttpMiddleware\HttpCache\ETag;
9+
10+
/**
11+
* Returns `null` ETag for all requests.
12+
* It can be used when ETag functionality is not required.
13+
*/
14+
final class NullETagProvider implements ETagProviderInterface
15+
{
16+
public function get(ServerRequestInterface $request): ?ETag
17+
{
18+
return null;
19+
}
20+
}

0 commit comments

Comments
 (0)