Files
kupshop/bundles/KupShop/KupShopBundle/Util/CachingStreamedResponse.php
2025-08-02 16:30:27 +02:00

238 lines
6.4 KiB
PHP

<?php
/**
* Created by PhpStorm.
* User: ondra
* Date: 18.12.17
* Time: 8:25.
*/
namespace KupShop\KupShopBundle\Util;
use KupShop\KupShopBundle\Util\System\PathFinder;
use Symfony\Component\HttpFoundation\StreamedResponse;
class CachingStreamedResponse extends StreamedResponse
{
protected $ttl = 86400;
protected $cacheName;
protected $skipCache = false;
protected $isExpensive = false;
protected $isLimited = false;
private $fileHandle;
protected $chunkSize = 4096;
/** @var int */
protected $action;
// load cached file
public const ACTION_USE_CACHE = 1;
// generate content and create cache file
public const ACTION_REFRESH = 2;
// generate content without creating cache file
public const ACTION_SKIP_CACHE = 3;
/** @var int */
protected $timestamp;
public function setTtl(int $seconds): static
{
$this->ttl = $seconds;
return $this;
}
public function setCacheName($cacheName)
{
$this->cacheName = $cacheName;
return $this;
}
public function setSkipCache($skipCache)
{
$this->skipCache = $skipCache;
return $this;
}
public function setExpensive(bool $isExpensive = true)
{
$this->isExpensive = $isExpensive;
}
public function setIsLimited(bool $isLimited): void
{
$this->isLimited = $isLimited;
}
/**
* @return CachingStreamedResponse
*/
public function setChunkSize(int $chunkSize)
{
$this->chunkSize = $chunkSize;
return $this;
}
public function initialize()
{
if (!$this->cacheName) {
throw new \LogicException('CachingStreamedResponse::$cacheName must be set!');
}
$this->action = static::ACTION_REFRESH;
if ($this->skipCache) {
if ($this->isExpensive && !$this->isLimited) {
$this->action = static::ACTION_REFRESH;
} else {
$this->action = static::ACTION_SKIP_CACHE;
}
} elseif ($this->isExpensive && file_exists($this->getCacheName())) {
$this->action = static::ACTION_USE_CACHE;
} else {
if (file_exists($this->getCacheName()) && (time() - filemtime($this->getCacheName())) < $this->ttl) {
$this->action = static::ACTION_USE_CACHE;
} elseif (file_exists($this->getLockFileName()) && !flock(fopen($this->getLockFileName(), 'r'), LOCK_SH | LOCK_NB)) {
// Fixes bug with stale flock on lock file
// If lock is stale, delete it and refresh
if ((time() - filemtime($this->getLockFileName())) < 3600 * 24) {
$this->action = static::ACTION_USE_CACHE; // if lock is not stale, set ACTION_USE_CACHE no matter the existence of the cache file
} else {
unlink($this->getLockFileName());
}
}
}
if ($this->action === static::ACTION_USE_CACHE) {
if (file_exists($this->getCacheName())) {
// use cached file creation timestamp for Last-Modified header
$timestamp = filemtime($this->getCacheName());
$this->headers->set('Content-Length', filesize($this->getCacheName()));
} else {
// trying to use cache without cache file => probably subsequent request because it couldn't flock lock file
$this->setStatusCode(429);
$this->sendHeaders();
exit;
}
} else {
$this->timestamp = $timestamp = time();
}
$this->headers->set('Last-Modified', gmdate('D, d M Y H:i:s ', $timestamp).'GMT');
}
/**
* @throws \Throwable
*/
public function sendContent(): static
{
if (!$this->action) {
throw new \LogicException('Call CachingStreamedResponse::initialize() before sendContent()!');
}
if ($this->streamed) {
return $this;
}
if ($this->action === static::ACTION_USE_CACHE) {
$this->readCacheFile();
return $this;
} elseif ($this->action === static::ACTION_SKIP_CACHE) {
return parent::sendContent();
}
$flock_file = fopen($this->getLockFileName(), 'wb');
flock($flock_file, LOCK_EX);
ob_start([$this, 'write'], $this->chunkSize);
try {
parent::sendContent();
} catch (\Throwable $exception) {
ob_end_clean();
$raven = getRaven();
$raven->captureException($exception);
if (!isDevelopment() && file_exists($this->getCacheName())) {
$this->readCacheFile();
return $this;
} else {
throw $exception;
}
}
ob_end_flush();
rename($this->getNewCacheName(), $this->getCacheName());
// adjust modification timestamp
// so Last-Modified has the same value as when the cached file is used
touch($this->getCacheName(), $this->timestamp);
flock($flock_file, LOCK_UN);
fclose($flock_file);
@unlink($this->getLockFileName());
return $this;
}
public function write($buffer, $phase)
{
if ($phase & PHP_OUTPUT_HANDLER_START) {
$this->fileHandle = fopen($this->getNewCacheName(), 'wb');
}
if ($this->fileHandle) {
fwrite($this->fileHandle, $buffer);
}
if ($phase & PHP_OUTPUT_HANDLER_END) {
fflush($this->fileHandle);
fclose($this->fileHandle);
$this->fileHandle = null;
}
return $buffer;
}
public function getCacheName()
{
$pathFinder = PathFinder::getService();
return $pathFinder->getTmpDir().$this->cacheName;
}
private function getNewCacheName()
{
return $this->getCacheName().'_new';
}
private function getLockFileName()
{
return $this->getCacheName().'.lock';
}
private function readCacheFile()
{
$this->streamed = true;
if (!file_exists($this->getCacheName())) {
$this->streamed = false;
$this->action = CachingStreamedResponse::ACTION_SKIP_CACHE;
return $this->sendContent(); // fallback to ACTION_SKIP_CACHE in case final cache file is not present
}
return readfile($this->getCacheName());
}
}