• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ICanBoogie / Storage / 11646577464

02 Nov 2024 11:22PM UTC coverage: 82.778%. First build
11646577464

push

github

olvlvl
Test against PHP 8.4.0RC3

147 of 178 new or added lines in 13 files covered. (82.58%)

149 of 180 relevant lines covered (82.78%)

6.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.28
/lib/FileStorage.php
1
<?php
2

3
/*
4
 * This file is part of the ICanBoogie package.
5
 *
6
 * (c) Olivier Laviale <olivier.laviale@gmail.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
namespace ICanBoogie\Storage;
13

14
use ArrayAccess;
15
use DirectoryIterator;
16
use Exception;
17
use ICanBoogie\Storage\FileStorage\Adapter;
18
use ICanBoogie\Storage\FileStorage\Adapter\SerializeAdapter;
19
use ICanBoogie\Storage\FileStorage\Iterator;
20
use RegexIterator;
21
use Traversable;
22

23
/**
24
 * A storage using the file system.
25
 */
26
final class FileStorage implements Storage, ArrayAccess
27
{
28
    use Storage\ArrayAccess;
29
    use Storage\ClearWithIterator;
30

31
    private static bool $release_after = false;
32

33
    /**
34
     * Absolute path to the storage directory.
35
     */
36
    private string $path;
37

38
    private Adapter $adapter;
39

40
    /**
41
     * @param string $path Absolute path to the storage directory.
42
     */
43
    public function __construct(string $path, ?Adapter $adapter = null)
44
    {
45
        $this->path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
15✔
46
        $this->adapter = $adapter ?? new SerializeAdapter();
15✔
47

48
        if (self::$release_after === null) {
15✔
NEW
49
            self::$release_after = !(str_starts_with(PHP_OS, 'WIN'));
×
50
        }
51
    }
52

53
    /**
54
     * @inheritdoc
55
     */
56
    public function exists(string $key): bool
57
    {
58
        $pathname = $this->format_pathname($key);
14✔
59
        $ttl_mark = $this->format_pathname_with_ttl($pathname);
14✔
60

61
        if (file_exists($ttl_mark) && fileatime($ttl_mark) < time() || !file_exists($pathname)) {
14✔
62
            return false;
7✔
63
        }
64

65
        return file_exists($pathname);
11✔
66
    }
67

68
    /**
69
     * @inheritdoc
70
     */
71
    public function retrieve(string $key): mixed
72
    {
73
        if (!$this->exists($key)) {
13✔
74
            return null;
4✔
75
        }
76

77
        return $this->read($this->format_pathname($key));
11✔
78
    }
79

80
    /**
81
     * @inheritdoc
82
     *
83
     * @throws Exception when a file operation fails.
84
     */
85
    public function store(string $key, mixed $value, ?int $ttl = null): void
86
    {
87
        $this->check_writable();
13✔
88

89
        $pathname = $this->format_pathname($key);
13✔
90
        $ttl_mark = $this->format_pathname_with_ttl($pathname);
13✔
91

92
        if ($ttl) {
13✔
93
            $future = time() + $ttl;
1✔
94

95
            touch($ttl_mark, $future, $future);
1✔
96
        } elseif (file_exists($ttl_mark)) {
12✔
NEW
97
            unlink($ttl_mark);
×
98
        }
99

100
        if ($value === true) {
13✔
NEW
101
            touch($pathname);
×
102

NEW
103
            return;
×
104
        }
105

106
        if ($value === null) {
13✔
107
            $this->eliminate($key);
1✔
108

109
            return;
1✔
110
        }
111

112
        set_error_handler(function () {
12✔
113
        });
12✔
114

115
        try {
116
            $this->safe_store($pathname, $value);
12✔
117
        } finally {
118
            restore_error_handler();
12✔
119
        }
120
    }
121

122
    /**
123
     * @inheritdoc
124
     */
125
    public function eliminate(string $key): void
126
    {
127
        $pathname = $this->format_pathname($key);
4✔
128

129
        if (!file_exists($pathname)) {
4✔
130
            return;
1✔
131
        }
132

133
        unlink($pathname);
3✔
134
    }
135

136
    /**
137
     * Normalizes a key into a valid filename.
138
     */
139
    private function normalize_key(string $key): string
140
    {
141
        return str_replace('/', '--', $key);
15✔
142
    }
143

144
    /**
145
     * Formats a key into an absolute pathname.
146
     */
147
    private function format_pathname(string $key): string
148
    {
149
        return $this->path . $this->normalize_key($key);
15✔
150
    }
151

152
    /**
153
     * Formats a pathname with a TTL extension.
154
     */
155
    private function format_pathname_with_ttl(string $pathname): string
156
    {
157
        return $pathname . '.ttl';
15✔
158
    }
159

160
    private function read(string $pathname): mixed
161
    {
162
        return $this->adapter->read($pathname);
11✔
163
    }
164

165
    private function write(string $pathname, mixed $value): void
166
    {
167
        $this->adapter->write($pathname, $value);
12✔
168
    }
169

170
    /**
171
     * Safely store the value.
172
     *
173
     * @throws Exception if an error occurs.
174
     */
175
    private function safe_store(string $pathname, mixed $value): void
176
    {
177
        $dir = dirname($pathname);
12✔
178
        $uniqid = uniqid(mt_rand(), true);
12✔
179
        $tmp_pathname = $dir . '/var-' . $uniqid;
12✔
180
        $garbage_pathname = $dir . '/garbage-var-' . $uniqid;
12✔
181

182
        #
183
        # We lock the file create/update, but we write the data in a temporary file, which is then
184
        # renamed once the data is written.
185
        #
186

187
        $fh = fopen($pathname, 'a+');
12✔
188

189
        if (!$fh) {
12✔
NEW
190
            throw new Exception("Unable to open $pathname.");
×
191
        }
192

193
        if (self::$release_after && !flock($fh, LOCK_EX)) {
12✔
NEW
194
            throw new Exception("Unable to get to exclusive lock on $pathname.");
×
195
        }
196

197
        $this->write($tmp_pathname, $value);
12✔
198

199
        #
200
        # Windows, this is for you
201
        #
202
        if (!self::$release_after) {
12✔
203
            fclose($fh);
12✔
204
        }
205

206
        if (!rename($pathname, $garbage_pathname)) {
12✔
NEW
207
            throw new Exception("Unable to rename $pathname as $garbage_pathname.");
×
208
        }
209

210
        if (!rename($tmp_pathname, $pathname)) {
12✔
NEW
211
            throw new Exception("Unable to rename $tmp_pathname as $pathname.");
×
212
        }
213

214
        if (!unlink($garbage_pathname)) {
12✔
NEW
215
            throw new Exception("Unable to delete $garbage_pathname.");
×
216
        }
217

218
        #
219
        # Unix, this is for you
220
        #
221
        if (self::$release_after) {
12✔
NEW
222
            flock($fh, LOCK_UN);
×
NEW
223
            fclose($fh);
×
224
        }
225
    }
226

227
    /**
228
     * @inheritdoc
229
     */
230
    public function getIterator(): Traversable
231
    {
232
        if (!is_dir($this->path)) {
2✔
NEW
233
            return;
×
234
        }
235

236
        $iterator = new DirectoryIterator($this->path);
2✔
237

238
        foreach ($iterator as $file) {
2✔
239
            if ($file->isDot() || $file->isDir()) {
2✔
240
                continue;
2✔
241
            }
242

243
            yield $file->getFilename();
2✔
244
        }
245
    }
246

247
    /**
248
     * Returns an iterator for the keys matching a specified regex.
249
     */
250
    public function matching(string $regex): iterable
251
    {
NEW
252
        return new Iterator(new RegexIterator(new DirectoryIterator($this->path), $regex));
×
253
    }
254

255
    private ?bool $is_writable = null;
256

257
    /**
258
     * Checks whether the storage directory is writable.
259
     *
260
     * @throws Exception when the storage directory is not writable.
261
     */
262
    public function check_writable(): bool
263
    {
264
        if ($this->is_writable) {
13✔
265
            return true;
2✔
266
        }
267

268
        $path = $this->path;
13✔
269

270
        if (!file_exists($path)) {
13✔
271
            set_error_handler(function () {
13✔
272
            });
13✔
273
            mkdir($path, 0705, true);
13✔
274
            restore_error_handler();
13✔
275
        }
276

277
        if (!is_writable($path)) {
13✔
NEW
278
            throw new Exception("The directory $path is not writable.");
×
279
        }
280

281
        return $this->is_writable = true;
13✔
282
    }
283
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc