238 lines
6.4 KiB
PHP
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());
|
|
}
|
|
}
|