Passed
Push — feat_cache_clear ( 757990...58de91 )
by Arnaud
03:48
created

Cache::clearByPattern()   A

Complexity

Conditions 5
Paths 12

Size

Total Lines 24
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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