FileCache::read()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace ICanBoogie\CLDR\Cache;
4
5
use Exception;
6
use ICanBoogie\CLDR\Cache;
7
use Throwable;
8
9
use function dirname;
10
use function fclose;
11
use function file_exists;
12
use function file_put_contents;
13
use function flock;
14
use function fopen;
15
use function is_writable;
16
use function mt_rand;
17
use function rename;
18
use function restore_error_handler;
19
use function rtrim;
20
use function set_error_handler;
21
use function str_replace;
22
use function uniqid;
23
use function unlink;
24
25
/**
26
 * A storage using the file system.
27
 */
28
final class FileCache implements Cache
29
{
30
    /**
31
     * Recommended name for the cache directory.
32
     */
33
    public const RECOMMENDED_DIR = '.cldr-cache';
34
35
    private static bool $release_after;
36
37
    /**
38
     * Absolute path to the storage directory.
39
     */
40
    private string $path;
41
42
    /**
43
     * @param string $path Absolute path to the storage directory.
44
     */
45
    public function __construct(string $path)
46
    {
47
        $this->path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
48
49
        self::$release_after ??= !(str_starts_with(PHP_OS, 'WIN'));
50
    }
51
52
    public function get(string $path): ?array
53
    {
54
        $pathname = $this->format_absolute_pathname($path);
55
56
        if (!file_exists($pathname)) {
57
            return null;
58
        }
59
60
        return $this->read($pathname);
61
    }
62
63
    /**
64
     * @throws Throwable
65
     */
66
    public function set(string $path, array $data): void
67
    {
68
        $this->assert_writable();
69
70
        $pathname = $this->format_absolute_pathname($path);
71
72
        // @phpstan-ignore-next-line
73
        set_error_handler(fn() => null);
74
75
        try {
76
            $this->safe_set($pathname, $data);
77
        } finally {
78
            restore_error_handler();
79
        }
80
    }
81
82
    private function format_absolute_pathname(string $path): string
83
    {
84
        return $this->path . str_replace('/', '--', $path) . '.php';
85
    }
86
87
    /**
88
     * @phpstan-ignore-next-line
89
     */
90
    private function read(string $pathname): array
91
    {
92
        return require $pathname;
93
    }
94
95
    /**
96
     * @phpstan-ignore-next-line
97
     */
98
    private function write(string $pathname, array $data): void
99
    {
100
        $var = var_export($data, true);
101
        $content = <<<PHP
102
        <?php return $var;
103
        PHP;
104
105
        file_put_contents($pathname, $content);
106
    }
107
108
    /**
109
     * Safely set the value.
110
     *
111
     * @throws Exception
112
     *
113
     * @phpstan-ignore-next-line
114
     */
115
    private function safe_set(string $pathname, array $data): void
116
    {
117
        $dir = dirname($pathname);
118
        $uniqid = uniqid((string)mt_rand(), true);
119
        $tmp_pathname = $dir . '/var-' . $uniqid;
120
        $garbage_pathname = $dir . '/garbage-var-' . $uniqid;
121
122
        #
123
        # We lock the file create/update, but we write the data in a temporary file, which is then
124
        # renamed once the data is written.
125
        #
126
127
        $fh = fopen($pathname, 'a+');
128
129
        if (!$fh) {
0 ignored issues
show
introduced by
$fh is of type resource, thus it always evaluated to false.
Loading history...
130
            throw new Exception("Unable to open $pathname.");
131
        }
132
133
        if (self::$release_after && !flock($fh, LOCK_EX)) {
134
            throw new Exception("Unable to get to exclusive lock on $pathname.");
135
        }
136
137
        $this->write($tmp_pathname, $data);
138
139
        #
140
        # Windows, this is for you
141
        #
142
        if (!self::$release_after) {
143
            fclose($fh);
144
        }
145
146
        if (!rename($pathname, $garbage_pathname)) {
147
            throw new Exception("Unable to rename $pathname as $garbage_pathname.");
148
        }
149
150
        if (!rename($tmp_pathname, $pathname)) {
151
            throw new Exception("Unable to rename $tmp_pathname as $pathname.");
152
        }
153
154
        if (!unlink($garbage_pathname)) {
155
            throw new Exception("Unable to delete $garbage_pathname.");
156
        }
157
158
        #
159
        # Unix, this is for you
160
        #
161
        if (self::$release_after) {
162
            flock($fh, LOCK_UN);
163
            fclose($fh);
164
        }
165
    }
166
167
    /**
168
     * Checks whether the storage directory is writable.
169
     *
170
     * @throws Exception when the storage directory is not writable.
171
     */
172
    private function assert_writable(): void
173
    {
174
        $path = $this->path;
175
176
        if (!is_writable($path)) {
177
            throw new Exception("The directory $path is not writable.");
178
        }
179
    }
180
}
181