Passed
Pull Request — master (#97)
by Alexander
12:27
created

FileCache   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 123
Duplicated Lines 0 %

Test Coverage

Coverage 72.34%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 50
dl 0
loc 123
ccs 34
cts 47
cp 0.7234
rs 10
c 4
b 0
f 0
wmc 24

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 3
A renameFile() 0 13 5
A evict() 0 5 2
A getCachePath() 0 5 1
A clear() 0 11 4
A load() 0 18 4
A put() 0 33 5
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Metadata\Cache;
6
7
use Metadata\ClassMetadata;
8
9
class FileCache implements CacheInterface, ClearableCacheInterface
10
{
11
    /**
12
     * @var string
13
     */
14
    private $dir;
15
16 4
    public function __construct(string $dir)
17
    {
18 4
        if (!is_dir($dir) && false === @mkdir($dir, 0777, true)) {
19
            throw new \InvalidArgumentException(sprintf('Can\'t create directory for cache at "%s"', $dir));
20
        }
21
22 4
        $this->dir = rtrim($dir, '\\/');
23 4
    }
24
25 4
    public function load(string $class): ?ClassMetadata
26
    {
27 4
        $path = $this->getCachePath($class);
28 4
        if (!file_exists($path)) {
29 2
            return null;
30
        }
31
32
        try {
33 4
            $metadata = include $path;
34 3
            if ($metadata instanceof ClassMetadata) {
35 3
                return $metadata;
36
            }
37
            // if the file does not return anything, the return value is integer `1`.
38
        } catch (\ParseError $e) {
39 1
            // ignore corrupted cache
40
        }
41
42
        return null;
43 2
    }
44
45
    public function put(ClassMetadata $metadata): void
46 2
    {
47
        if (!is_writable($this->dir)) {
48 2
            throw new \InvalidArgumentException(sprintf('The directory "%s" is not writable.', $this->dir));
49
        }
50
51
        $path = $this->getCachePath($metadata->name);
52 2
        if (!is_writable(dirname($path))) {
53
            throw new \RuntimeException(sprintf('Cache file "%s" is not writable.', $path));
54 2
        }
55 2
56
        $tmpFile = tempnam($this->dir, 'metadata-cache');
57
        if (false === $tmpFile) {
58
            $this->evict($metadata->name);
59
60
            return;
61 2
        }
62 2
63
        $data = '<?php return unserialize(' . var_export(serialize($metadata), true) . ');';
64 2
        $bytesWritten = file_put_contents($tmpFile, $data);
65
        // use strlen and not mb_strlen. if there is utf8 in the code, it also writes more bytes.
66
        if ($bytesWritten !== strlen($data)) {
67
            @unlink($tmpFile);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

67
            /** @scrutinizer ignore-unhandled */ @unlink($tmpFile);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
68
            // also evict the cache to not use an outdated version.
69
            $this->evict($metadata->name);
70
71
            return;
72
        }
73 2
74
        // Let's not break filesystems which do not support chmod.
75 2
        @chmod($tmpFile, 0666 & ~umask());
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chmod(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

75
        /** @scrutinizer ignore-unhandled */ @chmod($tmpFile, 0666 & ~umask());

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
76 2
77
        $this->renameFile($tmpFile, $path);
78
    }
79
80
    /**
81 2
     * Renames a file with fallback for windows
82
     */
83 2
    private function renameFile(string $source, string $target): void
84
    {
85
        if (false === @rename($source, $target)) {
86
            if (defined('PHP_WINDOWS_VERSION_BUILD')) {
87
                if (false === copy($source, $target)) {
88
                    throw new \RuntimeException(sprintf('(WIN) Could not write new cache file to %s.', $target));
89
                }
90
91
                if (false === unlink($source)) {
92
                    throw new \RuntimeException(sprintf('(WIN) Could not delete temp cache file to %s.', $source));
93
                }
94
            } else {
95
                throw new \RuntimeException(sprintf('Could not write new cache file to %s.', $target));
96 2
            }
97
        }
98 2
    }
99
100 2
    public function evict(string $class): void
101 2
    {
102 2
        $path = $this->getCachePath($class);
103
        if (file_exists($path)) {
104 2
            @unlink($path);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

104
            /** @scrutinizer ignore-unhandled */ @unlink($path);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
105
        }
106
    }
107
108
    public function clear(): bool
109
    {
110 4
        $result = true;
111
        $files = glob($this->dir . '/*.cache.php');
112 4
        foreach ($files as $file) {
113
            if (is_file($file)) {
114
                $result = $result && @unlink($file);
115
            }
116
        }
117
118
        return $result;
119
    }
120
121
    /**
122
     * This function computes the cache file path.
123
     *
124
     * If anonymous class is to be cached, it contains invalid path characters that need to be removed/replaced
125
     * Example of anonymous class name: class@anonymous\x00/app/src/Controller/DefaultController.php0x7f82a7e026ec
126
     */
127
    private function getCachePath(string $key): string
128
    {
129
        $fileName = str_replace(['\\', "\0", '@', '/', '$', '{', '}', ':'], '-', $key);
130
131
        return $this->dir . '/' . $fileName . '.cache.php';
132
    }
133
}
134