Passed
Push — master ( 3806bb...143923 )
by Arnaud
05:33
created

Cache   A

Complexity

Total Complexity 37

Size/Duplication

Total Lines 277
Duplicated Lines 0 %

Test Coverage

Coverage 52.59%

Importance

Changes 3
Bugs 1 Features 1
Metric Value
eloc 108
c 3
b 1
f 1
dl 0
loc 277
ccs 61
cts 116
cp 0.5259
rs 9.44
wmc 37

17 Methods

Rating   Name   Duplication   Size   Complexity  
A prepareKey() 0 9 1
A clearByPattern() 0 27 6
A createKeyFromAsset() 0 11 2
A delete() 0 13 2
A createKeyFromPath() 0 7 2
A set() 0 17 2
A has() 0 8 2
A prune() 0 17 4
A getFilePathname() 0 3 1
A getMultiple() 0 3 1
A duration() 0 13 4
A get() 0 22 4
A deleteMultiple() 0 3 1
A setMultiple() 0 3 1
A clear() 0 11 2
A createKeyFromString() 0 3 1
A __construct() 0 7 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Cecil.
7
 *
8
 * Copyright (c) Arnaud Ligny <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Cecil\Assets;
15
16
use Cecil\Builder;
17
use Cecil\Collection\Page\Page;
18
use Cecil\Config;
19
use Cecil\Exception\RuntimeException;
20
use Cecil\Util;
21
use Psr\SimpleCache\CacheInterface;
22
23
class Cache implements CacheInterface
24
{
25
    /** @var Builder */
26
    protected $builder;
27
28
    /** @var Config */
29
    protected $config;
30
31
    /** @var string */
32
    protected $pool;
33
34
    /** @var string */
35
    protected $cacheDir;
36
37
    /** @var int */
38
    protected $duration;
39
40 1
    public function __construct(Builder $builder, string $pool = '')
41
    {
42 1
        $this->builder = $builder;
43 1
        $this->config = $builder->getConfig();
44 1
        $this->pool = $pool;
45 1
        $this->cacheDir = Util::joinFile($this->config->getCachePath(), $pool);
46 1
        $this->duration = 31536000; // 1 year
47
    }
48
49
    /**
50
     * {@inheritdoc}
51
     */
52 1
    public function get($key, $default = null): mixed
53
    {
54
        try {
55 1
            $key = $this->prepareKey($key);
56 1
            if (false === $content = Util\File::fileGetContents($this->getFilePathname($key))) {
57
                return $default;
58
            }
59 1
            $data = unserialize($content);
60
61
            // check expiration
62 1
            if ($data['expiration'] <= time()) {
63
                $this->delete($key);
64
65 1
                return $default;
66
            }
67
        } catch (\Exception $e) {
68
            $this->builder->getLogger()->error($e->getMessage());
69
70
            return $default;
71
        }
72
73 1
        return $data['value'];
74
    }
75
76
    /**
77
     * {@inheritdoc}
78
     */
79 1
    public function set($key, $value, $ttl = null): bool
80
    {
81
        try {
82 1
            $key = $this->prepareKey($key);
83 1
            $data = serialize([
84 1
                'value'      => $value,
85 1
                'expiration' => time() + $this->duration($ttl),
86 1
            ]);
87 1
            $this->prune($key);
88 1
            Util\File::getFS()->dumpFile($this->getFilePathname($key), $data);
89
        } catch (\Exception $e) {
90
            $this->builder->getLogger()->error($e->getMessage());
91
92
            return false;
93
        }
94
95 1
        return true;
96
    }
97
98
    /**
99
     * {@inheritdoc}
100
     */
101
    public function delete($key): bool
102
    {
103
        try {
104
            $key = $this->prepareKey($key);
105
            Util\File::getFS()->remove($this->getFilePathname($key));
106
            $this->prune($key);
107
        } catch (\Exception $e) {
108
            $this->builder->getLogger()->error($e->getMessage());
109
110
            return false;
111
        }
112
113
        return true;
114
    }
115
116
    /**
117
     * {@inheritdoc}
118
     */
119
    public function clear(): bool
120
    {
121
        try {
122
            Util\File::getFS()->remove($this->cacheDir);
123
        } catch (\Exception $e) {
124
            $this->builder->getLogger()->error($e->getMessage());
125
126
            return false;
127
        }
128
129
        return true;
130
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135
    public function getMultiple($keys, $default = null): iterable
136
    {
137
        throw new \Exception(\sprintf('%s::%s not yet implemented.', __CLASS__, __FUNCTION__));
138
    }
139
140
    /**
141
     * {@inheritdoc}
142
     */
143
    public function setMultiple($values, $ttl = null): bool
144
    {
145
        throw new \Exception(\sprintf('%s::%s not yet implemented.', __CLASS__, __FUNCTION__));
146
    }
147
148
    /**
149
     * {@inheritdoc}
150
     */
151
    public function deleteMultiple($keys): bool
152
    {
153
        throw new \Exception(\sprintf('%s::%s not yet implemented.', __CLASS__, __FUNCTION__));
154
    }
155
156
    /**
157
     * {@inheritdoc}
158
     */
159 1
    public function has($key): bool
160
    {
161 1
        $key = $this->prepareKey($key);
162 1
        if (!Util\File::getFS()->exists($this->getFilePathname($key))) {
163 1
            return false;
164
        }
165
166 1
        return true;
167
    }
168
169
    /**
170
     * Creates key with the MD5 hash of a string.
171
     */
172 1
    public function createKeyFromString(string $value): string
173
    {
174 1
        return hash('md5', $value);
175
    }
176
177
    /**
178
     * Creates key from a file: "$relativePath__MD5".
179
     *
180
     * @throws RuntimeException
181
     */
182 1
    public function createKeyFromPath(string $path, string $relativePath): string
183
    {
184 1
        if (false === $content = Util\File::fileGetContents($path)) {
185
            throw new RuntimeException(\sprintf('Can\'t create cache key for "%s".', $path));
186
        }
187
188 1
        return $this->prepareKey(\sprintf('%s__%s', $relativePath, $this->createKeyFromString($content)));
189
    }
190
191
    /**
192
     * Creates key from an Asset source: "$filename_$ext_$tag__VERSION__MD5".
193
     */
194 1
    public function createKeyFromAsset(Asset $asset, ?array $tags = null): string
195
    {
196 1
        $tags = implode('_', $tags ?? []);
197
198 1
        return $this->prepareKey(\sprintf(
199 1
            '%s%s%s__%s__%s',
200 1
            $asset['filename'],
201 1
            "_{$asset['ext']}",
202 1
            $tags ? "_$tags" : '',
203 1
            $this->builder->getVersion(),
204 1
            $this->createKeyFromString($asset['content_source'] ?? '')
205 1
        ));
206
    }
207
208
    /**
209
     * Clear cache by pattern.
210
     */
211
    public function clearByPattern(string $pattern): int
212
    {
213
        try {
214
            if (!Util\File::getFS()->exists($this->cacheDir)) {
215
                throw new RuntimeException(\sprintf('Can\'t remove cache directory "%s".', $this->cacheDir));
216
            }
217
            $fileCount = 0;
218
            $iterator = new \RecursiveIteratorIterator(
219
                new \RecursiveDirectoryIterator($this->cacheDir),
220
                \RecursiveIteratorIterator::SELF_FIRST
221
            );
222
            foreach ($iterator as $file) {
223
                if ($file->isFile()) {
224
                    if (preg_match('/' . $pattern . '/i', $file->getPathname())) {
225
                        Util\File::getFS()->remove($file->getPathname());
226
                        $fileCount++;
227
                        $this->builder->getLogger()->debug(\sprintf('Cache file "%s" removed', $file->getPathname()));
228
                    }
229
                }
230
            }
231
        } catch (\Exception $e) {
232
            $this->builder->getLogger()->error($e->getMessage());
233
234
            return 0;
235
        }
236
237
        return $fileCount;
238
    }
239
240
    /**
241
     * Returns cache file pathname from key.
242
     */
243 1
    private function getFilePathname(string $key): string
244
    {
245 1
        return Util::joinFile($this->cacheDir, \sprintf('%s.ser', $key));
246
    }
247
248
    /**
249
     * Removes previous cache files.
250
     */
251 1
    private function prune(string $key): bool
252
    {
253
        try {
254 1
            $keyAsArray = explode('__', $this->prepareKey($key), -1);
255 1
            if (!empty($keyAsArray)) {
256 1
                $pattern = Util::joinFile($this->cacheDir, $keyAsArray[0]) . '*';
257 1
                foreach (glob($pattern) as $filename) {
258 1
                    Util\File::getFS()->remove($filename);
259
                }
260
            }
261
        } catch (\Exception $e) {
262
            $this->builder->getLogger()->error($e->getMessage());
263
264
            return false;
265
        }
266
267 1
        return true;
268
    }
269
270
    /**
271
     * $key must be a valid string.
272
     */
273 1
    private function prepareKey(string $key): string
274
    {
275 1
        $key = str_replace(['https://', 'http://'], '', $key);
276 1
        $key = Page::slugify($key);
277 1
        $key = trim($key, '/');
278 1
        $key = str_replace(['\\', '/'], ['-', '-'], $key);
279 1
        $key = substr($key, 0, 200); // Maximum filename length in NTFS?
280
281 1
        return $key;
282
    }
283
284
    /**
285
     * Convert the various expressions of a TTL value into duration in seconds.
286
     */
287 1
    protected function duration(\DateInterval|int|null $ttl): int
288
    {
289 1
        if ($ttl === null) {
290 1
            return $this->duration;
291
        }
292
        if (is_int($ttl)) {
293
            return $ttl;
294
        }
295
        if ($ttl instanceof \DateInterval) {
1 ignored issue
show
introduced by
$ttl is always a sub-type of DateInterval.
Loading history...
296
            return (int)$ttl->format('%s');
297
        }
298
299
        throw new \InvalidArgumentException('TTL values must be one of null, int, \DateInterval');
300
    }
301
}
302