Passed
Pull Request — 4 (#10235)
by Steve
06:33
created

VersionProvider::getModuleVersion()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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