<?php
declare(strict_types=1);
namespace Metadata\Cache;
use Metadata\ClassMetadata;
class FileCache implements CacheInterface, ClearableCacheInterface
{
/**
* @var string
*/
private $dir;
public function __construct(string $dir)
{
if (!is_dir($dir) && false === @mkdir($dir, 0777, true)) {
throw new \InvalidArgumentException(sprintf('Can\'t create directory for cache at "%s"', $dir));
}
$this->dir = rtrim($dir, '\\/');
}
public function load(string $class): ?ClassMetadata
{
$path = $this->getCachePath($class);
if (!is_readable($path)) {
return null;
}
try {
$metadata = include $path;
if ($metadata instanceof ClassMetadata) {
return $metadata;
}
// if the file does not return anything, the return value is integer `1`.
} catch (\Error $e) {
// ignore corrupted cache
}
return null;
}
public function put(ClassMetadata $metadata): void
{
if (!is_writable($this->dir)) {
throw new \InvalidArgumentException(sprintf('The directory "%s" is not writable.', $this->dir));
}
$path = $this->getCachePath($metadata->name);
if (!is_writable(dirname($path))) {
throw new \RuntimeException(sprintf('Cache file "%s" is not writable.', $path));
}
$tmpFile = tempnam($this->dir, 'metadata-cache');
if (false === $tmpFile) {
$this->evict($metadata->name);
return;
}
$data = '<?php return unserialize(' . var_export(serialize($metadata), true) . ');';
$bytesWritten = file_put_contents($tmpFile, $data);
// use strlen and not mb_strlen. if there is utf8 in the code, it also writes more bytes.
if ($bytesWritten !== strlen($data)) {
@unlink($tmpFile);
// also evict the cache to not use an outdated version.
$this->evict($metadata->name);
return;
}
// Let's not break filesystems which do not support chmod.
@chmod($tmpFile, 0666 & ~umask());
$this->renameFile($tmpFile, $path);
}
/**
* Renames a file with fallback for windows
*/
private function renameFile(string $source, string $target): void
{
if (false === @rename($source, $target)) {
if (defined('PHP_WINDOWS_VERSION_BUILD')) {
if (false === copy($source, $target)) {
throw new \RuntimeException(sprintf('(WIN) Could not write new cache file to %s.', $target));
}
if (false === unlink($source)) {
throw new \RuntimeException(sprintf('(WIN) Could not delete temp cache file to %s.', $source));
}
} else {
throw new \RuntimeException(sprintf('Could not write new cache file to %s.', $target));
}
}
}
public function evict(string $class): void
{
$path = $this->getCachePath($class);
if (file_exists($path)) {
@unlink($path);
}
}
public function clear(): bool
{
$result = true;
$files = glob($this->dir . '/*.cache.php');
foreach ($files as $file) {
if (is_file($file)) {
$result = $result && @unlink($file);
}
}
return $result;
}
/**
* This function computes the cache file path.
*
* If anonymous class is to be cached, it contains invalid path characters that need to be removed/replaced
* Example of anonymous class name: class@anonymous\x00/app/src/Controller/DefaultController.php0x7f82a7e026ec
*/
private function getCachePath(string $key): string
{
$fileName = str_replace(['\\', "\0", '@', '/', '$', '{', '}', ':'], '-', $key);
return $this->dir . '/' . $fileName . '.cache.php';
}
}