Passed
Pull Request — v1 (#368)
by Ankit
02:30
created

FileStore::lock()   A

Complexity

Conditions 3
Paths 6

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 12
nc 6
nop 3
dl 0
loc 21
rs 9.8666
c 0
b 0
f 0
1
<?php
2
3
namespace TusPhp\Cache;
4
5
use TusPhp\File;
6
use Carbon\Carbon;
7
use TusPhp\Config;
8
9
class FileStore extends AbstractCache
10
{
11
    /** @var int */
12
    public const LOCK_NONE = 0;
13
14
    /** @var string */
15
    protected $cacheDir;
16
17
    /** @var string */
18
    protected $cacheFile;
19
20
    /**
21
     * FileStore constructor.
22
     *
23
     * @param string|null $cacheDir
24
     * @param string|null $cacheFile
25
     */
26
    public function __construct(string $cacheDir = null, string $cacheFile = null)
27
    {
28
        $cacheDir  = $cacheDir ?? Config::get('file.dir');
29
        $cacheFile = $cacheFile ?? Config::get('file.name');
30
31
        $this->setCacheDir($cacheDir);
32
        $this->setCacheFile($cacheFile);
33
    }
34
35
    /**
36
     * Set cache dir.
37
     *
38
     * @param string $path
39
     *
40
     * @return self
41
     */
42
    public function setCacheDir(string $path) : self
43
    {
44
        $this->cacheDir = $path;
45
46
        return $this;
47
    }
48
49
    /**
50
     * Get cache dir.
51
     *
52
     * @return string
53
     */
54
    public function getCacheDir() : string
55
    {
56
        return $this->cacheDir;
57
    }
58
59
    /**
60
     * Set cache file.
61
     *
62
     * @param string $file
63
     *
64
     * @return self
65
     */
66
    public function setCacheFile(string $file) : self
67
    {
68
        $this->cacheFile = $file;
69
70
        return $this;
71
    }
72
73
    /**
74
     * Get cache file.
75
     *
76
     * @return string
77
     */
78
    public function getCacheFile() : string
79
    {
80
        return $this->cacheDir . $this->cacheFile;
81
    }
82
83
    /**
84
     * Create cache dir if not exists.
85
     *
86
     * @return void
87
     */
88
    protected function createCacheDir()
89
    {
90
        if ( ! file_exists($this->cacheDir)) {
91
            mkdir($this->cacheDir);
92
        }
93
    }
94
95
    /**
96
     * Create a cache file.
97
     *
98
     * @return void
99
     */
100
    protected function createCacheFile()
101
    {
102
        $this->createCacheDir();
103
104
        $cacheFilePath = $this->getCacheFile();
105
106
        if ( ! file_exists($cacheFilePath)) {
107
            touch($cacheFilePath);
108
        }
109
    }
110
111
    /**
112
     * {@inheritDoc}
113
     */
114
    public function get(string $key, bool $withExpired = false)
115
    {
116
        $key      = $this->getActualCacheKey($key);
117
        $contents = $this->getCacheContents();
118
119
        if (empty($contents[$key])) {
120
            return null;
121
        }
122
123
        if ($withExpired) {
124
            return $contents[$key];
125
        }
126
127
        return $this->isValid($key) ? $contents[$key] : null;
128
    }
129
130
    /**
131
     * @param string        $path
132
     * @param int           $type
133
     * @param callable|null $cb
134
     *
135
     * @return mixed
136
     */
137
    protected function lock(string $path, int $type = LOCK_SH, callable $cb = null)
138
    {
139
        $out    = false;
140
        $handle = @fopen($path, File::READ_BINARY);
141
142
        if (false === $handle) {
143
            return $out;
144
        }
145
146
        try {
147
            if (flock($handle, $type)) {
148
                clearstatcache(true, $path);
149
150
                $out = $cb($handle);
151
            }
152
        } finally {
153
            flock($handle, LOCK_UN);
154
            fclose($handle);
155
        }
156
157
        return $out;
158
    }
159
160
    /**
161
     * Get contents of a file with shared access.
162
     *
163
     * @param string $path
164
     *
165
     * @return string
166
     */
167
    public function sharedGet(string $path) : string
168
    {
169
        return $this->lock($path, LOCK_SH, function ($handle) use ($path) {
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->lock($path...ion(...) { /* ... */ }) could return the type false which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
170
            return fread($handle, filesize($path) ?: 1);
171
        });
172
    }
173
174
    /**
175
     * Write the contents of a file with exclusive lock.
176
     *
177
     * @param string $path
178
     * @param string $contents
179
     * @param int    $lock
180
     *
181
     * @return int|false
182
     */
183
    public function put(string $path, string $contents, int $lock = LOCK_EX)
184
    {
185
        return file_put_contents($path, $contents, $lock);
186
    }
187
188
    /**
189
     * {@inheritDoc}
190
     */
191
    public function set(string $key, $value)
192
    {
193
        $cacheKey  = $this->getActualCacheKey($key);
194
        $cacheFile = $this->getCacheFile();
195
196
        if ( ! file_exists($cacheFile) || ! $this->isValid($cacheKey)) {
197
            $this->createCacheFile();
198
        }
199
200
        return $this->lock($cacheFile, LOCK_EX, function ($handle) use ($cacheKey, $cacheFile, $value) {
201
            $contents = fread($handle, filesize($cacheFile) ?: 1) ?? [];
202
            $contents = json_decode($contents, true) ?? [];
0 ignored issues
show
Bug introduced by
It seems like $contents can also be of type array; however, parameter $json of json_decode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

202
            $contents = json_decode(/** @scrutinizer ignore-type */ $contents, true) ?? [];
Loading history...
203
204
            if ( ! empty($contents[$cacheKey]) && \is_array($value)) {
205
                $contents[$cacheKey] = $value + $contents[$cacheKey];
206
            } else {
207
                $contents[$cacheKey] = $value;
208
            }
209
210
            return $this->put($cacheFile, json_encode($contents), self::LOCK_NONE);
211
        });
212
    }
213
214
    /**
215
     * {@inheritDoc}
216
     */
217
    public function delete(string $key) : bool
218
    {
219
        $cacheKey = $this->getActualCacheKey($key);
220
        $contents = $this->getCacheContents();
221
222
        if (isset($contents[$cacheKey])) {
223
            unset($contents[$cacheKey]);
224
225
            return false !== $this->put($this->getCacheFile(), json_encode($contents));
226
        }
227
228
        return false;
229
    }
230
231
    /**
232
     * {@inheritDoc}
233
     */
234
    public function keys() : array
235
    {
236
        $contents = $this->getCacheContents();
237
238
        if (\is_array($contents)) {
239
            return array_keys($contents);
240
        }
241
242
        return [];
243
    }
244
245
    /**
246
     * Check if cache is still valid.
247
     *
248
     * @param string $key
249
     *
250
     * @return bool
251
     */
252
    public function isValid(string $key) : bool
253
    {
254
        $key  = $this->getActualCacheKey($key);
255
        $meta = $this->getCacheContents()[$key] ?? [];
256
257
        if (empty($meta['expires_at'])) {
258
            return false;
259
        }
260
261
        return Carbon::now() < Carbon::createFromFormat(self::RFC_7231, $meta['expires_at']);
262
    }
263
264
    /**
265
     * Get cache contents.
266
     *
267
     * @return array|bool
268
     */
269
    public function getCacheContents()
270
    {
271
        $cacheFile = $this->getCacheFile();
272
273
        if ( ! file_exists($cacheFile)) {
274
            return false;
275
        }
276
277
        return json_decode($this->sharedGet($cacheFile), true) ?? [];
278
    }
279
280
    /**
281
     * Get actual cache key with prefix.
282
     *
283
     * @param string $key
284
     *
285
     * @return string
286
     */
287
    public function getActualCacheKey(string $key) : string
288
    {
289
        $prefix = $this->getPrefix();
290
291
        if (false === strpos($key, $prefix)) {
292
            $key = $prefix . $key;
293
        }
294
295
        return $key;
296
    }
297
}
298