Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
169 changes: 111 additions & 58 deletions README.md

Large diffs are not rendered by default.

25 changes: 18 additions & 7 deletions config/response-cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,32 @@
|--------------------------------------------------------------------------
|
| Here you may pass any cache store defined in your config/cache.php file.
| Please note that not all stores support tagging: For better control over
| and increased performance of your cache, we strongly recommend using a
| store with tag support, like Redis or APC.
| Tags work on every store. null uses your default cache store.
|
*/
'store' => env('RESPONSE_CACHE_STORE', 'file'),
'store' => env('RESPONSE_CACHE_STORE'),

/*
|--------------------------------------------------------------------------
| Tag Version Time to Live
|--------------------------------------------------------------------------
|
| Every tag has a version, which is replaced when the tag is flushed. This
| setting specifies how long versions are kept, in seconds. Once a version
| expires, all responses carrying that tag are treated as stale, so cached
| responses never outlive it.
|
*/
'tag_ttl' => env('RESPONSE_CACHE_TAG_TTL', 60 * 60 * 24 * 30),

/*
|--------------------------------------------------------------------------
| Default Tags
|--------------------------------------------------------------------------
|
| If your store supports tags, all responses cached will also be tagged
| with these tags by default. Additional tags may be used depending on the
| middleware and strategy.
| All responses cached will also be tagged with these tags by default, so
| flushing one of them clears every response. Additional tags may be used
| depending on the middleware and strategy.
|
*/
'tags' => ($tags = env('RESPONSE_CACHE_TAGS'))
Expand Down
14 changes: 9 additions & 5 deletions src/Commands/ClearCacheCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@

namespace Matchory\ResponseCache\Commands;

use BadMethodCallException;
use Illuminate\Console\Command;
use Matchory\ResponseCache\Repository;
use Matchory\ResponseCache\ResponseCache;
use Random\RandomException;
use RuntimeException;
use Symfony\Component\Console\Attribute\AsCommand;

/**
Expand All @@ -33,11 +34,14 @@ class ClearCacheCommand extends Command
protected $aliases = ['response-cache:flush'];

/**
* @throws BadMethodCallException
* @throws RandomException
* @throws RuntimeException
*/
public function handle(Repository $repository): int
public function handle(ResponseCache $responseCache): int
{
$repository->flush($this->argument('tags'));
$tags = $this->argument('tags');

$responseCache->flush($tags === [] ? null : $tags);

$this->components->info('Response cache flushed successfully.');

Expand Down
15 changes: 10 additions & 5 deletions src/Contracts/CacheStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,23 @@ interface CacheStrategy
*/
public function key(Request $request): string;

/**
* Determines, before a response exists, whether a request may be served
* from or stored in the cache.
*/
public function isCacheable(Request $request): bool;

/**
* Determines whether the response should be cached.
*/
public function shouldCache(Request $request, Response $response): bool;

/**
* Generates a list of tags for a request. If the cache is missed, this
* method will be invoked with the generated response passed, providing an
* opportunity to add tags dependent on the generated response, for example
* the ID of a customer associated to the request, or similar.
* Generates a list of tags for a request. Tags must be derivable from the
* request alone, as they are needed to look up a cached response before
* any response exists.
*
* @return string[]
*/
public function tags(Request $request, ?Response $response = null): array;
public function tags(Request $request): array;
}
10 changes: 6 additions & 4 deletions src/Facades/ResponseCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
/**
* Response Cache Facade
*
* @method static \Symfony\Component\HttpFoundation\Response|null get(\Illuminate\Http\Request $request, string[]|null $tags = null) Retrieves a cached response
* @method static bool has(\Illuminate\Http\Request $request, string[]|null $tags = null) Checks whether a response is cached
* @method static void put(\Illuminate\Http\Request $request, \Symfony\Component\HttpFoundation\Response $response, string[]|null $tags = null, int|null $ttl = null) Caches a response
* @method static bool isCacheable(\Illuminate\Http\Request $request) Checks whether a request may be cached
* @method static \Matchory\ResponseCache\Lookup lookup(\Illuminate\Http\Request $request, string[] $tags = []) Looks up a cached response
* @method static \Symfony\Component\HttpFoundation\Response|null get(\Illuminate\Http\Request $request, string[] $tags = []) Retrieves a cached response
* @method static \Matchory\ResponseCache\Snapshot|null snapshot(\Illuminate\Http\Request $request, string[] $tags = []) Captures the current tag versions
* @method static void put(\Illuminate\Http\Request $request, \Symfony\Component\HttpFoundation\Response $response, \Matchory\ResponseCache\Snapshot $snapshot, int|null $ttl = null) Caches a response
* @method static void flush(string[]|null $tags = null) Flushes the cache
* @method static void delete(string|string[] $uri, string[] $tags = []) Deletes one or more URIs
* @method static void delete(string|string[] $uri) Deletes one or more URIs
* @method static bool enabled() Checks whether the cache is enabled
*
* @bundle Matchory\ResponseCache
Expand Down
34 changes: 20 additions & 14 deletions src/Http/Middleware/CacheResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@

namespace Matchory\ResponseCache\Http\Middleware;

use BadMethodCallException;
use Closure;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Http\Request;
use Matchory\ResponseCache\Events\{Hit, Miss};
use Matchory\ResponseCache\ResponseCache;
use Psr\SimpleCache\InvalidArgumentException;
use Random\RandomException;
use Symfony\Component\HttpFoundation\Response;

use function array_shift;
Expand Down Expand Up @@ -46,15 +46,18 @@ public function __construct(
/**
* Handles requests.
*
* @throws BadMethodCallException
* @throws InvalidArgumentException
* @throws RandomException
*/
public function handle(
Request $request,
Closure $next,
string ...$args,
): Response {
if (! $this->responseCache->enabled()) {
if (
! $this->responseCache->enabled()
|| ! $this->responseCache->isCacheable($request)
) {
return $next($request);
}

Expand All @@ -65,25 +68,28 @@ public function handle(
: null;
$tags = $args;

if (
$this->responseCache->has($request, $tags)
&& ($response = $this->responseCache->get($request, $tags)) !== null
) {
// The lookup happens before the response is built, so its snapshot
// predates any flush that happens while it is being built
$lookup = $this->responseCache->lookup($request, $tags);

if ($lookup->response !== null) {
$this->eventDispatcher->dispatch(new Hit($request));

return $response;
return $lookup->response;
}

$response = $next($request);

$this->eventDispatcher->dispatch(new Miss($request));

$this->responseCache->put(
$request,
clone $response,
$tags,
$ttl,
);
if ($lookup->snapshot !== null) {
$this->responseCache->put(
$request,
clone $response,
$lookup->snapshot,
$ttl,
);
}

if (config('response-cache.cache_status_enabled')) {
$response->headers->set('Response-Cache-Status', 'miss');
Expand Down
30 changes: 30 additions & 0 deletions src/Lookup.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/**
* This file is part of response-cache, a Matchory library.
*
* @author Moritz Friedrich <moritz@matchory.com>
*/

declare(strict_types=1);

namespace Matchory\ResponseCache;

use Symfony\Component\HttpFoundation\Response;

/**
* Lookup
*
* The result of looking up a request in the cache: the response on a hit, or
* the snapshot to stamp the new entry with on a miss. The snapshot is `null` if
* the tag versions could not be established, in which case nothing is stored.
*
* @bundle Matchory\ResponseCache
*/
final readonly class Lookup
{
public function __construct(
public ?Response $response,
public ?Snapshot $snapshot,
) {}
}
Loading
Loading