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()); } }