Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: extract ResponseCache class for Web Page Caching #7644

Merged
merged 13 commits into from
Jul 5, 2023
4 changes: 4 additions & 0 deletions deptrac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,10 @@ parameters:
- Cache
skip_violations:
# Individual class exemptions
CodeIgniter\Cache\ResponseCache:
- CodeIgniter\HTTP\CLIRequest
- CodeIgniter\HTTP\IncomingRequest
- CodeIgniter\HTTP\ResponseInterface
CodeIgniter\Entity\Cast\URICast:
- CodeIgniter\HTTP\URI
CodeIgniter\Log\Handlers\ChromeLoggerHandler:
Expand Down
10 changes: 0 additions & 10 deletions phpstan-baseline.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,6 @@ parameters:
count: 1
path: system/Cache/Handlers/RedisHandler.php

-
message: "#^Call to an undefined method CodeIgniter\\\\HTTP\\\\Request\\:\\:getPost\\(\\)\\.$#"
count: 1
path: system/CodeIgniter.php

-
message: "#^Call to an undefined method CodeIgniter\\\\HTTP\\\\Request\\:\\:setLocale\\(\\)\\.$#"
count: 1
path: system/CodeIgniter.php

-
message: "#^Property CodeIgniter\\\\Database\\\\BaseBuilder\\:\\:\\$db \\(CodeIgniter\\\\Database\\\\BaseConnection\\) in empty\\(\\) is not falsy\\.$#"
count: 1
Expand Down
149 changes: 149 additions & 0 deletions system/Cache/ResponseCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\Cache;

use CodeIgniter\HTTP\CLIRequest;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Cache as CacheConfig;
use Exception;

/**
* Web Page Caching
*/
class ResponseCache
MGatner marked this conversation as resolved.
Show resolved Hide resolved
{
/**
* Whether to take the URL query string into consideration when generating
* output cache files. Valid options are:
*
* false = Disabled
* true = Enabled, take all query parameters into account.
* Please be aware that this may result in numerous cache
* files generated for the same page over and over again.
* array('q') = Enabled, but only take into account the specified list
* of query parameters.
*
* @var bool|string[]
*/
protected $cacheQueryString = false;

/**
* Cache time to live.
*
* @var int seconds
*/
protected int $ttl = 0;

protected CacheInterface $cache;

public function __construct(CacheConfig $config, CacheInterface $cache)
{
$this->cacheQueryString = $config->cacheQueryString;
$this->cache = $cache;
}

/**
* @return $this
*/
public function setTtl(int $ttl)
{
$this->ttl = $ttl;

return $this;
}

/**
* Generates the cache key to use from the current request.
*
* @param CLIRequest|IncomingRequest $request
*
* @internal for testing purposes only
*/
public function generateCacheKey($request): string
{
if ($request instanceof CLIRequest) {
return md5($request->getPath());
}

$uri = clone $request->getUri();

$query = $this->cacheQueryString
? $uri->getQuery(is_array($this->cacheQueryString) ? ['only' => $this->cacheQueryString] : [])
: '';

return md5($uri->setFragment('')->setQuery($query));
}

/**
* Caches the response.
*
* @param CLIRequest|IncomingRequest $request
*/
public function make($request, ResponseInterface $response): bool
{
if ($this->ttl === 0) {
return true;
}

$headers = [];

foreach ($response->headers() as $header) {
$headers[$header->getName()] = $header->getValueLine();
}

return $this->cache->save(
$this->generateCacheKey($request),
serialize(['headers' => $headers, 'output' => $response->getBody()]),
$this->ttl
);
}

/**
* Gets the cached response for the request.
*
* @param CLIRequest|IncomingRequest $request
*/
public function get($request, ResponseInterface $response): ?ResponseInterface
{
if ($cachedResponse = $this->cache->get($this->generateCacheKey($request))) {
$cachedResponse = unserialize($cachedResponse);

if (
! is_array($cachedResponse)
|| ! isset($cachedResponse['output'])
|| ! isset($cachedResponse['headers'])
) {
throw new Exception('Error unserializing page cache');
}

$headers = $cachedResponse['headers'];
$output = $cachedResponse['output'];

// Clear all default headers
foreach (array_keys($response->headers()) as $key) {
$response->removeHeader($key);
}

// Set cached headers
foreach ($headers as $name => $value) {
$response->setHeader($name, $value);
}

$response->setBody($output);

return $response;
}

return null;
}
}
52 changes: 26 additions & 26 deletions system/CodeIgniter.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace CodeIgniter;

use Closure;
use CodeIgniter\Cache\ResponseCache;
use CodeIgniter\Debug\Timer;
use CodeIgniter\Events\Events;
use CodeIgniter\Exceptions\FrameworkException;
Expand Down Expand Up @@ -84,7 +85,7 @@ class CodeIgniter
/**
* Current request.
*
* @var CLIRequest|IncomingRequest|Request|null
* @var CLIRequest|IncomingRequest|null
*/
protected $request;

Expand Down Expand Up @@ -127,6 +128,8 @@ class CodeIgniter
* Cache expiration time
*
* @var int seconds
*
* @deprecated 4.4.0 Moved to ResponseCache::$ttl. No longer used.
*/
protected static $cacheTTL = 0;

Expand Down Expand Up @@ -175,13 +178,20 @@ class CodeIgniter
*/
protected int $bufferLevel;

/**
* Web Page Caching
*/
protected ResponseCache $pageCache;

/**
* Constructor.
*/
public function __construct(App $config)
{
$this->startTime = microtime(true);
$this->config = $config;

$this->pageCache = Services::responsecache();
}

/**
Expand Down Expand Up @@ -330,7 +340,7 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon
);
}

static::$cacheTTL = 0;
$this->pageCache->setTtl(0);
$this->bufferLevel = ob_get_level();

$this->startBenchmark();
Expand Down Expand Up @@ -463,7 +473,7 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache
return $possibleResponse;
}

if ($possibleResponse instanceof Request) {
if ($possibleResponse instanceof IncomingRequest || $possibleResponse instanceof CLIRequest) {
$this->request = $possibleResponse;
}
}
Expand Down Expand Up @@ -517,9 +527,7 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache
// Cache it without the performance metrics replaced
// so that we can have live speed updates along the way.
// Must be run after filters to preserve the Response headers.
if (static::$cacheTTL > 0) {
$this->cachePage($cacheConfig);
}
$this->pageCache->make($this->request, $this->response);

// Update the performance metrics
$body = $this->response->getBody();
Expand Down Expand Up @@ -603,9 +611,11 @@ protected function startBenchmark()
* Sets a Request object to be used for this request.
* Used when running certain tests.
*
* @param CLIRequest|IncomingRequest $request
*
* @return $this
*/
public function setRequest(Request $request)
public function setRequest($request)
{
$this->request = $request;

Expand Down Expand Up @@ -674,27 +684,11 @@ protected function forceSecureAccess($duration = 31_536_000)
*/
public function displayCache(Cache $config)
{
if ($cachedResponse = cache()->get($this->generateCacheName($config))) {
$cachedResponse = unserialize($cachedResponse);
if (! is_array($cachedResponse) || ! isset($cachedResponse['output']) || ! isset($cachedResponse['headers'])) {
throw new Exception('Error unserializing page cache');
}

$headers = $cachedResponse['headers'];
$output = $cachedResponse['output'];

// Clear all default headers
foreach (array_keys($this->response->headers()) as $key) {
$this->response->removeHeader($key);
}

// Set cached headers
foreach ($headers as $name => $value) {
$this->response->setHeader($name, $value);
}
if ($cachedResponse = $this->pageCache->get($this->request, $this->response)) {
$this->response = $cachedResponse;

$this->totalTime = $this->benchmark->getElapsedTime('total_execution');
$output = $this->displayPerformanceMetrics($output);
$output = $this->displayPerformanceMetrics($cachedResponse->getBody());
$this->response->setBody($output);

return $this->response;
Expand All @@ -705,6 +699,8 @@ public function displayCache(Cache $config)

/**
* Tells the app that the final output should be cached.
*
* @deprecated 4.4.0 Moved to ResponseCache::setTtl(). to No longer used.
*/
public static function cache(int $time)
{
Expand All @@ -716,6 +712,8 @@ public static function cache(int $time)
* full-page caching for very high performance.
*
* @return bool
*
* @deprecated 4.4.0 No longer used.
*/
public function cachePage(Cache $config)
{
Expand All @@ -741,6 +739,8 @@ public function getPerformanceStats(): array

/**
* Generates the cache name to use for our full-page caching.
*
* @deprecated 4.4.0 No longer used.
*/
protected function generateCacheName(Cache $config): string
{
Expand Down
2 changes: 2 additions & 0 deletions system/Config/BaseService.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use CodeIgniter\Autoloader\Autoloader;
use CodeIgniter\Autoloader\FileLocator;
use CodeIgniter\Cache\CacheInterface;
use CodeIgniter\Cache\ResponseCache;
use CodeIgniter\CLI\Commands;
use CodeIgniter\CodeIgniter;
use CodeIgniter\Database\ConnectionInterface;
Expand Down Expand Up @@ -117,6 +118,7 @@
* @method static View renderer($viewPath = null, ConfigView $config = null, $getShared = true)
* @method static IncomingRequest|CLIRequest request(App $config = null, $getShared = true)
* @method static ResponseInterface response(App $config = null, $getShared = true)
* @method static ResponseCache responsecache(?Cache $config = null, ?CacheInterface $cache = null, bool $getShared = true)
* @method static Router router(RouteCollectionInterface $routes = null, Request $request = null, $getShared = true)
* @method static RouteCollection routes($getShared = true)
* @method static Security security(App $config = null, $getShared = true)
Expand Down
18 changes: 18 additions & 0 deletions system/Config/Services.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use CodeIgniter\Cache\CacheFactory;
use CodeIgniter\Cache\CacheInterface;
use CodeIgniter\Cache\ResponseCache;
use CodeIgniter\CLI\Commands;
use CodeIgniter\CodeIgniter;
use CodeIgniter\Database\ConnectionInterface;
Expand Down Expand Up @@ -438,6 +439,23 @@ public static function negotiator(?RequestInterface $request = null, bool $getSh
return new Negotiate($request);
}

/**
* Return the ResponseCache.
*
* @return ResponseCache
*/
public static function responsecache(?Cache $config = null, ?CacheInterface $cache = null, bool $getShared = true)
{
if ($getShared) {
return static::getSharedInstance('responsecache', $config, $cache);
}

$config ??= config(Cache::class);
$cache ??= AppServices::cache();

return new ResponseCache($config, $cache);
}

/**
* Return the appropriate pagination handler.
*
Expand Down
7 changes: 4 additions & 3 deletions system/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,13 @@ protected function forceHTTPS(int $duration = 31_536_000)
}

/**
* Provides a simple way to tie into the main CodeIgniter class and
* tell it how long to cache the current page for.
* How long to cache the current page for.
*
* @params int $time time to live in seconds.
*/
protected function cachePage(int $time)
{
CodeIgniter::cache($time);
Services::responsecache()->setTtl($time);
}

/**
Expand Down
Loading