CacheProcessFactory::createProcesses()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
c 1
b 0
f 0
dl 0
loc 14
rs 10
cc 2
nc 2
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Churn\Process;
6
7
use Churn\Event\Event\AfterAnalysis as AfterAnalysisEvent;
8
use Churn\Event\Event\AfterFileAnalysis as AfterFileAnalysisEvent;
9
use Churn\Event\Subscriber\AfterAnalysis;
10
use Churn\Event\Subscriber\AfterFileAnalysis;
11
use Churn\File\File;
12
use Churn\File\FileHelper;
13
use InvalidArgumentException;
14
use Throwable;
15
16
/**
17
 * @internal
18
 */
19
final class CacheProcessFactory implements AfterAnalysis, AfterFileAnalysis, ProcessFactory
20
{
21
    /**
22
     * @var string The cache file path.
23
     */
24
    private $cachePath;
25
26
    /**
27
     * @var ProcessFactory Inner process factory.
28
     */
29
    private $processFactory;
30
31
    /**
32
     * @var array<string, array<scalar>> The cached data.
33
     */
34
    private $cache;
35
36
    /**
37
     * @param string $cachePath The cache file path.
38
     * @param ProcessFactory $processFactory Inner process factory.
39
     * @throws InvalidArgumentException If the path is invalid.
40
     */
41
    public function __construct(string $cachePath, ProcessFactory $processFactory)
42
    {
43
        try {
44
            FileHelper::ensureFileIsWritable($cachePath);
45
        } catch (Throwable $e) {
46
            $message = 'Invalid cache file path: ' . $e->getMessage();
47
48
            throw new InvalidArgumentException($message, 0, $e);
49
        }
50
51
        $this->cachePath = $cachePath;
52
        $this->processFactory = $processFactory;
53
        $this->cache = $this->loadCache($cachePath);
54
    }
55
56
    /**
57
     * @param File $file File that the processes will execute on.
58
     * @return iterable<ProcessInterface> The list of processes to execute.
59
     */
60
    #[\Override]
61
    public function createProcesses(File $file): iterable
62
    {
63
        if (!$this->isCached($file)) {
64
            return $this->processFactory->createProcesses($file);
65
        }
66
67
        $key = $file->getFullPath();
68
69
        $countChanges = (int) $this->cache[$key][1];
70
        $cyclomaticComplexity = (int) $this->cache[$key][2];
71
        $this->cache[$key][3] = true;
72
73
        return [new PredefinedProcess($file, $countChanges, $cyclomaticComplexity)];
74
    }
75
76
    /**
77
     * @param AfterAnalysisEvent $event The event triggered when the analysis is done.
78
     */
79
    #[\Override]
80
    public function onAfterAnalysis(AfterAnalysisEvent $event): void
81
    {
82
        $this->writeCache();
83
    }
84
85
    /**
86
     * @param AfterFileAnalysisEvent $event The event triggered when the analysis of a file is done.
87
     */
88
    #[\Override]
89
    public function onAfterFileAnalysis(AfterFileAnalysisEvent $event): void
90
    {
91
        $this->addToCache(
92
            $event->getFilePath(),
93
            $event->getNumberOfChanges(),
94
            $event->getCyclomaticComplexity()
95
        );
96
    }
97
98
    /**
99
     * @param string $path The absolute path of the file.
100
     * @param integer $nbChanges The number of times the file has been changed.
101
     * @param integer $complexity The cyclomatic complexity of the file.
102
     */
103
    private function addToCache(string $path, int $nbChanges, int $complexity): void
104
    {
105
        $this->cache[$path][0] = $this->cache[$path][0] ?? \md5_file($path);
106
        $this->cache[$path][1] = $nbChanges;
107
        $this->cache[$path][2] = $complexity;
108
        $this->cache[$path][3] = true;
109
    }
110
111
    /**
112
     * Write the cache in its file.
113
     */
114
    private function writeCache(): void
115
    {
116
        $data = [];
117
118
        foreach ($this->cache as $path => $values) {
119
            if (!(bool) $values[3]) {
120
                continue;
121
            }
122
123
            unset($values[3]);
124
            $data[] = \implode(',', \array_merge([$path], $values));
125
        }
126
127
        \file_put_contents($this->cachePath, \implode("\n", $data));
128
    }
129
130
    /**
131
     * @param File $file The file to process.
132
     */
133
    private function isCached(File $file): bool
134
    {
135
        $key = $file->getFullPath();
136
137
        if (!isset($this->cache[$key])) {
138
            return false;
139
        }
140
141
        $md5 = $this->cache[$key][0];
142
        $this->cache[$key][0] = $newMd5 = \md5_file($file->getFullPath());
143
144
        return $md5 === $newMd5;
145
    }
146
147
    /**
148
     * @param string $cachePath Cache file path.
149
     * @return array<string, array<scalar>>
150
     */
151
    private function loadCache(string $cachePath): array
152
    {
153
        if (!\is_file($cachePath)) {
154
            return [];
155
        }
156
157
        $rows = \file($cachePath);
158
        if (false === $rows) {
159
            return [];
160
        }
161
162
        $cache = [];
163
164
        foreach ($rows as $row) {
165
            $data = \explode(',', $row);
166
            $cache[$data[0]] = \array_slice($data, 1);
167
            $cache[$data[0]][3] = false;
168
        }
169
170
        return $cache;
171
    }
172
}
173