src/Cache/CacheManager.php
<?php
declare(strict_types=1);
namespace PabloK\SupercacheBundle\Cache;
use PabloK\SupercacheBundle\Exceptions\FilesystemException;
use PabloK\SupercacheBundle\Exceptions\SecurityViolationException;
use PabloK\SupercacheBundle\Filesystem\Finder;
/**
* Performs cache-related operations on cached entries.
*/
class CacheManager
{
const UNCACHEABLE_DISABLED = -7;
const UNCACHEABLE_PRIVATE = -6;
const UNCACHEABLE_NO_STORE_POLICY = -5;
const UNCACHEABLE_QUERY = -4;
const UNCACHEABLE_CODE = -3;
const UNCACHEABLE_METHOD = -2;
const UNCACHEABLE_ROUTE = -1;
/**
* @var array Human readable values for UNCACHEABLE_* codes
*/
private static $readableUncachableExplanation = [
self::UNCACHEABLE_PRIVATE => 'private',
self::UNCACHEABLE_NO_STORE_POLICY => 'no-store-policy',
self::UNCACHEABLE_QUERY => 'query-string',
self::UNCACHEABLE_CODE => 'code',
self::UNCACHEABLE_METHOD => 'method',
self::UNCACHEABLE_ROUTE => 'route',
self::UNCACHEABLE_DISABLED => 'disabled',
];
/**
* @var Finder
*/
private $finder;
/**
* @param Finder $finder
*/
public function __construct(Finder $finder)
{
$this->finder = $finder;
}
/**
* Translates UNCACHEABLE_* reason codes into more human readable form.
*
* @param int $code Any code available by UNCACHEABLE_* constants
*
* @return string
*
* @throws \InvalidArgumentException Unknown code
*/
public static function getUncachableReasonFromCode($code)
{
if (!isset(self::$readableUncachableExplanation[$code])) {
throw new \InvalidArgumentException('Unknown code of ' . $code . ' specified');
}
return self::$readableUncachableExplanation[$code];
}
/**
* Checks if cache for given path exists.
*
* @param string $path Cache path, eg. /sandbox
*
* @return bool
*/
public function isExists($path)
{
$filePath = $path . \DIRECTORY_SEPARATOR . 'index.html';
return $this->finder->isReadable($filePath);
}
/**
* Provides list of all cached elements.
*
* @param null|string $parent Branch to start from. Eg. you can specify /sandbox and you'll get /sandbox,
* /sandbox/info but not /test or /test/sandbox.
*
* @return array<int, string>
*/
public function getEntriesList($parent = null)
{
$filesList = $this->finder->getFilesList();
$basePath = $this->finder->getRealCacheDir();
$basePathLength = \mb_strlen($basePath);
$entries = [];
foreach ($filesList as $e) {
$entry = \mb_substr($e->getPath(), $basePathLength);
if ('' === $entry) {
$entry = '/';
} elseif (\DIRECTORY_SEPARATOR !== '/') { //Why elseif and not if below? Simple - it's waste of time to call str_replace when $entry was empty before
$entry = \str_replace($entry, \DIRECTORY_SEPARATOR, '/'); //Did I mention I hate Windos?
}
if ($parent && 0 !== \strpos($entry, $parent)) {
continue;
}
$entries[] = $entry;
}
return $entries;
}
/**
* Deletes single cache entry.
*
* @param string $path Cache path, eg. /sandbox
*
* @return bool
*
* @throws \InvalidArgumentException Invalid cache patch specified. Patch which doesn't not exist but it's valid
* will not cause this exception.
*/
public function deleteEntry($path)
{
if (\DIRECTORY_SEPARATOR !== '/') { //Path will always have / (bcs it's http path), no matter on which OS
$path = \str_replace('/', \DIRECTORY_SEPARATOR, $path); //...but filesystem differ
}
$filePath = $path . \DIRECTORY_SEPARATOR . 'index.html'; //File path
if (!$this->finder->isReadable($filePath)) {
return false;
}
if (!$this->finder->deleteFile($filePath)) {
return false;
}
$this->finder->deleteDirectory($path); //This one can fail - if you try to delete entry /sandbox and url /sandbox/test is present /sandbox directory cannot be deleted
return true;
}
/**
* Deletes cache entry and all it's children.
* So if you request recursive removal of /sandbox paths /sandbox, /sandbox/info and /sandbox/test are going to be
* removed.
*
* @param string $path Cache path, eg. /sandbox
*
* @return bool
*
* @throws \RuntimeException For details please {@see Finder::deleteDirectoryRecursive()}
* @throws FilesystemException
*/
public function deleteEntryRecursive($path)
{
if (\DIRECTORY_SEPARATOR !== '/') {
$path = \str_replace('/', \DIRECTORY_SEPARATOR, $path);
}
if (!$this->finder->isReadable($path)) {
return false;
}
return $this->finder->deleteDirectoryRecursive($path);
}
/**
* Removes all cache entries.
* Alias for deleteEntryRecursive('/').
*
* @return bool
*
* @see deleteEntryRecursive()
*/
public function clear()
{
return $this->deleteEntryRecursive('/');
}
/**
* Tries to retrieve element from cache by it's path.
*
* @param ?string $type Contents of CacheElement::TYPE_* or null to get any type
*
* @return CacheElement|null
*/
public function getElement(string $path, ?string $type = null)
{
if (null === $type) {
$basePath = $path . '/';
$content = $this->finder->readFile($basePath . '/index.' . CacheElement::TYPE_HTML);
if (false !== $content) {
return new CacheElement($path, $content, CacheElement::TYPE_HTML);
}
$content = $this->finder->readFile($basePath . '/index.' . CacheElement::TYPE_JAVASCRIPT);
if (false !== $content) {
return new CacheElement($path, $content, CacheElement::TYPE_JAVASCRIPT);
}
$content = $this->finder->readFile($basePath . '/index.' . CacheElement::TYPE_BINARY);
if (false !== $content) {
return new CacheElement($path, $content, CacheElement::TYPE_BINARY);
}
} else {
$element = new CacheElement($path, '', $type); //This will also verify type
$content = $this->finder->readFile($path . '/index.' . CacheElement::TYPE_BINARY);
if (false !== $content) {
$element->updateContent($content);
}
return $element;
}
return null;
}
/**
* Saves entry content. If entry exists it will be updated, otherwise created.
*
* @param CacheElement $element
*
* @return bool
*
* @throws FilesystemException
* @throws SecurityViolationException Specified cache path was found to be dangerous (eg. /../../sandbox)
*/
public function saveElement(CacheElement $element)
{
$path = $element->getPath() . '/index.' . $element->getType(); //Type contains extension
return $this->finder
->writeFile(
$path,
$element->getContent(),
CacheType::TYPE_HTML === $element->getType()
);
}
}