1<?php
2
3/*
4 * This file is part of Twig.
5 *
6 * (c) Fabien Potencier
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Twig\Cache;
13
14/**
15 * Implements a cache on the filesystem.
16 *
17 * @author Andrew Tch <andrew@noop.lv>
18 */
19class FilesystemCache implements CacheInterface, RemovableCacheInterface
20{
21    public const FORCE_BYTECODE_INVALIDATION = 1;
22
23    private $directory;
24    private $options;
25
26    public function __construct(string $directory, int $options = 0)
27    {
28        $this->directory = rtrim($directory, '\/').'/';
29        $this->options = $options;
30    }
31
32    public function generateKey(string $name, string $className): string
33    {
34        $hash = hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $className);
35
36        return $this->directory.$hash[0].$hash[1].'/'.$hash.'.php';
37    }
38
39    public function load(string $key): void
40    {
41        if (is_file($key)) {
42            @include_once $key;
43        }
44    }
45
46    public function write(string $key, string $content): void
47    {
48        $dir = \dirname($key);
49        if (!is_dir($dir)) {
50            if (false === @mkdir($dir, 0777, true)) {
51                clearstatcache(true, $dir);
52                if (!is_dir($dir)) {
53                    throw new \RuntimeException(\sprintf('Unable to create the cache directory (%s).', $dir));
54                }
55            }
56        } elseif (!is_writable($dir)) {
57            throw new \RuntimeException(\sprintf('Unable to write in the cache directory (%s).', $dir));
58        }
59
60        $tmpFile = tempnam($dir, basename($key));
61        if (false !== @file_put_contents($tmpFile, $content) && @rename($tmpFile, $key)) {
62            @chmod($key, 0666 & ~umask());
63
64            if (self::FORCE_BYTECODE_INVALIDATION == ($this->options & self::FORCE_BYTECODE_INVALIDATION)) {
65                // Compile cached file into bytecode cache
66                if (\function_exists('opcache_invalidate') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) {
67                    @opcache_invalidate($key, true);
68                } elseif (\function_exists('apc_compile_file')) {
69                    apc_compile_file($key);
70                }
71            }
72
73            return;
74        }
75
76        throw new \RuntimeException(\sprintf('Failed to write cache file "%s".', $key));
77    }
78
79    public function remove(string $name, string $cls): void
80    {
81        $key = $this->generateKey($name, $cls);
82        if (!@unlink($key) && file_exists($key)) {
83            throw new \RuntimeException(\sprintf('Failed to delete cache file "%s".', $key));
84        }
85    }
86
87    public function getTimestamp(string $key): int
88    {
89        if (!is_file($key)) {
90            return 0;
91        }
92
93        return (int) @filemtime($key);
94    }
95}
96