Passed
Push — master ( d8d42d...3806bb )
by Arnaud
06:01
created

Cache::createKeyFromAsset()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

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