Skip to content

Commit

Permalink
Merge pull request #7644 from kenjis/refactor-PageCache
Browse files Browse the repository at this point in the history
refactor: extract ResponseCache class for Web Page Caching
  • Loading branch information
kenjis authored Jul 5, 2023
2 parents 8d9b176 + 7890156 commit 0d15487
Show file tree
Hide file tree
Showing 11 changed files with 503 additions and 51 deletions.
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 <[email protected]>
*
* 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
*/
final class ResponseCache
{
/**
* 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[]
*/
private $cacheQueryString = false;

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

private 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

0 comments on commit 0d15487

Please sign in to comment.