Passed
Push — cache ( 9fab09...a37f4f )
by Arnaud
03:59
created

Cache::prepareKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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