Passed
Push — v1 ( 1a9a08...c21198 )
by Ankit
03:08
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
            $contents = fread($handle, filesize($path) ?: 1);
171
172
            if (false === $contents) {
173
                return '';
174
            }
175
176
            return $contents;
177
        });
178
    }
179
180
    /**
181
     * Write the contents of a file with exclusive lock.
182
     *
183
     * @param string $path
184
     * @param string $contents
185
     * @param int    $lock
186
     *
187
     * @return int|false
188
     */
189
    public function put(string $path, string $contents, int $lock = LOCK_EX)
190
    {
191
        return file_put_contents($path, $contents, $lock);
192
    }
193
194
    /**
195
     * {@inheritDoc}
196
     */
197
    public function set(string $key, $value)
198
    {
199
        $cacheKey  = $this->getActualCacheKey($key);
200
        $cacheFile = $this->getCacheFile();
201
202
        if ( ! file_exists($cacheFile) || ! $this->isValid($cacheKey)) {
203
            $this->createCacheFile();
204
        }
205
206
        return $this->lock($cacheFile, LOCK_EX, function ($handle) use ($cacheKey, $cacheFile, $value) {
207
            $contents = fread($handle, filesize($cacheFile) ?: 1) ?? '';
208
            $contents = json_decode($contents, true) ?? [];
209
210
            if ( ! empty($contents[$cacheKey]) && \is_array($value)) {
211
                $contents[$cacheKey] = $value + $contents[$cacheKey];
212
            } else {
213
                $contents[$cacheKey] = $value;
214
            }
215
216
            return $this->put($cacheFile, json_encode($contents), self::LOCK_NONE);
217
        });
218
    }
219
220
    /**
221
     * {@inheritDoc}
222
     */
223
    public function delete(string $key) : bool
224
    {
225
        $cacheKey = $this->getActualCacheKey($key);
226
        $contents = $this->getCacheContents();
227
228
        if (isset($contents[$cacheKey])) {
229
            unset($contents[$cacheKey]);
230
231
            return false !== $this->put($this->getCacheFile(), json_encode($contents));
232
        }
233
234
        return false;
235
    }
236
237
    /**
238
     * {@inheritDoc}
239
     */
240
    public function keys() : array
241
    {
242
        $contents = $this->getCacheContents();
243
244
        if (\is_array($contents)) {
245
            return array_keys($contents);
246
        }
247
248
        return [];
249
    }
250
251
    /**
252
     * Check if cache is still valid.
253
     *
254
     * @param string $key
255
     *
256
     * @return bool
257
     */
258
    public function isValid(string $key) : bool
259
    {
260
        $key  = $this->getActualCacheKey($key);
261
        $meta = $this->getCacheContents()[$key] ?? [];
262
263
        if (empty($meta['expires_at'])) {
264
            return false;
265
        }
266
267
        return Carbon::now() < Carbon::createFromFormat(self::RFC_7231, $meta['expires_at']);
268
    }
269
270
    /**
271
     * Get cache contents.
272
     *
273
     * @return array|bool
274
     */
275
    public function getCacheContents()
276
    {
277
        $cacheFile = $this->getCacheFile();
278
279
        if ( ! file_exists($cacheFile)) {
280
            return false;
281
        }
282
283
        return json_decode($this->sharedGet($cacheFile), true) ?? [];
284
    }
285
286
    /**
287
     * Get actual cache key with prefix.
288
     *
289
     * @param string $key
290
     *
291
     * @return string
292
     */
293
    public function getActualCacheKey(string $key) : string
294
    {
295
        $prefix = $this->getPrefix();
296
297
        if (false === strpos($key, $prefix)) {
298
            $key = $prefix . $key;
299
        }
300
301
        return $key;
302
    }
303
}
304