Passed
Push — 4 ( e05142...78dcfd )
by Maxime
08:15 queued 12s
created

VersionProvider::getCache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Core\Manifest;
4
5
use InvalidArgumentException;
6
use SilverStripe\Core\Config\Config;
7
use Psr\SimpleCache\CacheInterface;
8
use SilverStripe\Core\Config\Configurable;
9
use SilverStripe\Core\Injector\Injectable;
10
use SilverStripe\Core\Injector\Injector;
11
12
/**
13
 * The version provider will look up configured modules and examine the composer.lock file
14
 * to find the current version installed for each. This is used for the logo title in the CMS
15
 * via {@link LeftAndMain::CMSVersion()}
16
 *
17
 * Example configuration:
18
 *
19
 * <code>
20
 * SilverStripe\Core\Manifest\VersionProvider:
21
 *   modules:
22
 *     # package/name: Package Title
23
 *     silverstripe/framework: Framework
24
 *     silverstripe/cms: CMS
25
 * </code>
26
 */
27
class VersionProvider
28
{
29
    use Configurable;
30
    use Injectable;
31
32
    /**
33
     * @var array
34
     */
35
    private static $modules = [
0 ignored issues
show
introduced by
The private property $modules is not used, and could be removed.
Loading history...
36
        'silverstripe/framework' => 'Framework',
37
        'silverstripe/recipe-core' => 'Core Recipe',
38
    ];
39
40
    /**
41
     * Gets a comma delimited string of package titles and versions
42
     *
43
     * @return string
44
     */
45
    public function getVersion()
46
    {
47
        $key = sprintf('%s-%s', $this->getComposerLockPath(), 'all');
48
        $version = $this->getCachedValue($key);
49
        if ($version) {
50
            return $version;
51
        }
52
        $modules = $this->getModules();
53
        $lockModules = $this->getModuleVersionFromComposer(array_keys($modules));
54
        $moduleVersions = [];
55
        foreach ($modules as $module => $title) {
56
            if (!array_key_exists($module, $lockModules)) {
57
                continue;
58
            }
59
            $version = $lockModules[$module];
60
            $moduleVersions[$module] = [$title, $version];
61
        }
62
        $moduleVersions = $this->filterModules($moduleVersions);
63
        $ret = [];
64
        foreach ($moduleVersions as $module => $value) {
65
            list($title, $version) = $value;
66
            $ret[] = "$title: $version";
67
        }
68
        $version = implode(', ', $ret);
69
        if ($version) {
70
            $this->setCacheValue($key, $version);
71
        }
72
        return $version;
73
    }
74
75
    /**
76
     * Get the version of a specific module
77
     *
78
     * @param string $module - e.g. silverstripe/framework
79
     * @return string - e.g. 4.11
80
     */
81
    public function getModuleVersion(string $module): string
82
    {
83
        $key = sprintf('%s-%s', $this->getComposerLockPath(), $module);
84
        $version = $this->getCachedValue($key);
85
        if ($version) {
86
            return $version;
87
        }
88
        $version = $this->getModuleVersionFromComposer([$module])[$module] ?? '';
89
        if ($version) {
90
            $this->setCacheValue($key, $version);
91
        }
92
        return $version;
93
    }
94
95
    /**
96
     * @return CacheInterface
97
     */
98
    private function getCache(): CacheInterface
99
    {
100
        return Injector::inst()->get(CacheInterface::class . '.VersionProvider');
101
    }
102
103
    /**
104
     * @param string $key
105
     *
106
     * @return string
107
     */
108
    private function getCachedValue(string $key): string
109
    {
110
        $cache = $this->getCache();
111
        try {
112
            if ($cache->has($key)) {
113
                return $cache->get($key);
114
            }
115
        } catch (InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
116
        }
117
        return '';
118
    }
119
120
    /**
121
     * @param string $key
122
     * @param string $value
123
     */
124
    private function setCacheValue(string $key, string $value): void
125
    {
126
        $cache = $this->getCache();
127
        try {
128
            $cache->set($key, $value);
129
        } catch (InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
130
        }
131
    }
132
133
    /**
134
     * Filter modules to only use the last module from a git repo, for example
135
     *
136
     * [
137
     *   silverstripe/framework => ['Framework', 1.1.1'],
138
     *   silverstripe/cms => ['CMS', 2.2.2'],
139
     *   silverstripe/recipe-cms => ['CMS Recipe', '3.3.3'],
140
     *   cwp/cwp-core => ['CWP', '4.4.4']
141
     * ]
142
     * =>
143
     * [
144
     *   silverstripe/recipe-cms => ['CMS Recipe', '3.3.3'],
145
     *   cwp/cwp-core => ['CWP', '4.4.4']
146
     * ]
147
     *
148
     * @param array $modules
149
     * @return array
150
     */
151
    private function filterModules(array $modules)
152
    {
153
        $accountModule = [];
154
        foreach ($modules as $module => $value) {
155
            if (!preg_match('#^([a-z0-9\-]+)/([a-z0-9\-]+)$#', $module, $m)) {
156
                continue;
157
            }
158
            $account = $m[1];
159
            $accountModule[$account] = [$module, $value];
160
        }
161
        $ret = [];
162
        foreach ($accountModule as $account => $arr) {
163
            list($module, $value) = $arr;
164
            $ret[$module] = $value;
165
        }
166
        return $ret;
167
    }
168
169
    /**
170
     * Gets the configured core modules to use for the SilverStripe application version
171
     *
172
     * @return array
173
     */
174
    public function getModules()
175
    {
176
        $modules = Config::inst()->get(self::class, 'modules');
177
        return !empty($modules) ? $modules : ['silverstripe/framework' => 'Framework'];
178
    }
179
180
    /**
181
     * Tries to obtain version number from composer.lock if it exists
182
     *
183
     * @param array $modules
184
     * @return array
185
     */
186
    public function getModuleVersionFromComposer($modules = [])
187
    {
188
        $versions = [];
189
        $lockData = $this->getComposerLock();
190
        if ($lockData && !empty($lockData['packages'])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $lockData of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
191
            foreach ($lockData['packages'] as $package) {
192
                if (in_array($package['name'], $modules) && isset($package['version'])) {
193
                    $versions[$package['name']] = $package['version'];
194
                }
195
            }
196
        }
197
        return $versions;
198
    }
199
200
    /**
201
     * Load composer.lock's contents and return it
202
     *
203
     * @param bool $cache
204
     * @return array
205
     */
206
    protected function getComposerLock($cache = true)
207
    {
208
        $composerLockPath = $this->getComposerLockPath();
209
        if (!file_exists($composerLockPath)) {
210
            return [];
211
        }
212
213
        $lockData = [];
214
        $jsonData = file_get_contents($composerLockPath);
215
216
        if ($cache) {
217
            $cache = Injector::inst()->get(CacheInterface::class . '.VersionProvider_composerlock');
218
            $cacheKey = md5($jsonData);
219
            if ($versions = $cache->get($cacheKey)) {
220
                $lockData = json_decode($versions, true);
221
            }
222
        }
223
224
        if (empty($lockData) && $jsonData) {
225
            $lockData = json_decode($jsonData, true);
226
227
            if ($cache) {
228
                $cache->set($cacheKey, $jsonData);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $cacheKey does not seem to be defined for all execution paths leading up to this point.
Loading history...
229
            }
230
        }
231
232
        return $lockData;
233
    }
234
235
    /**
236
     * @return string
237
     */
238
    protected function getComposerLockPath(): string
239
    {
240
        return BASE_PATH . '/composer.lock';
241
    }
242
}
243