Passed
Branch master (59633e)
by Alain
02:02
created

PHPFeature::getRequiredVersion()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1
Metric Value
dl 0
loc 7
ccs 2
cts 2
cp 1
rs 9.4285
cc 1
eloc 2
nc 1
nop 2
crap 1
1
<?php
2
/**
3
 * PHPFeature Class
4
 *
5
 * @package   phpfeature
6
 * @author    Alain Schlesser <[email protected]>
7
 * @license   GPL-2.0+
8
 * @link      http://www.brightnucleus.com/
9
 * @copyright 2016 Alain Schlesser, Bright Nucleus
10
 */
11
12
/**
13
 * Class PHPFeature
14
 *
15
 * @since  0.1.0
16
 *
17
 * @author Alain Schlesser <[email protected]>
18
 */
19
class PHPFeature implements FeatureInterface
20
{
21
22
    /**
23
     * RegEx pattern that matches the comparison string.
24
     *
25
     * @since 0.1.0
26
     *
27
     * @var string
28
     */
29
    const COMPARISON_PATTERN = '/^(?:(<=|lt|<|le|>=|gt|>|ge|=|==|eq|!=|<>|ne))([0-9].*)$/';
30
31
    /**
32
     * Reference to the Configuration object.
33
     *
34
     * @since 0.1.0
35
     *
36
     * @var ConfigInterface
37
     */
38
    protected $config;
39
40
    /**
41
     * Reference to the Version object.
42
     *
43
     * @since 0.1.0
44
     *
45
     * @var SemanticVersion
46
     */
47
    protected $version;
48
49
    /**
50
     * Instantiate a PHPFeature object.
51
     *
52
     * @since 0.1.0
53
     *
54
     * @param SemanticVersion|string|int|null $phpVersion  Version of PHP to
55
     *                                                     check the features
56
     *                                                     for.
57
     * @param ConfigInterface|null            $config      Configuration that
58
     *                                                     contains the known
59
     *                                                     features.
60
     * @throws RuntimeException If the PHP version could not be validated.
61
     */
62 46
    public function __construct($phpVersion = null, ConfigInterface $config = null)
63
    {
64
65
        // TODO: Better way to bootstrap this while still allowing DI?
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
66 46
        if ( ! $config) {
67 46
            $config = new Config(include(__DIR__ . '/../config/known_features.php'));
68
        }
69
70 46
        $this->config = $config;
71
72 46
        if (null === $phpVersion) {
73
            $phpVersion = phpversion();
74
        }
75
76 46
        if (is_integer($phpVersion)) {
77
            $phpVersion = (string)$phpVersion;
78
        }
79
80 46
        if (is_string($phpVersion)) {
81 46
            $phpVersion = new SemanticVersion($phpVersion, true);
82
        }
83
84 46
        $this->version = $phpVersion;
85 46
    }
86
87
    /**
88
     * Check whether a feature or a collection of features is supported.
89
     *
90
     * Accepts either a string or an array of strings. Returns true if all the
91
     * passed-in features are supported, or false if at least one of them is
92
     * not.
93
     *
94
     * @since 0.1.0
95
     *
96
     * @param string|array $features    What features to check the support of.
97
     * @return bool
98
     * @throws InvalidArgumentException If the wrong type of argument is passed
99
     *                                  in.
100
     * @throws RuntimeException         If a requirement could not be parsed.
101
     */
102 23
    public function isSupported($features)
103
    {
104
105 23
        if (is_string($features)) {
106 21
            $features = array($features);
107
        }
108
109 23
        if ( ! is_array($features)) {
110
            throw new InvalidArgumentException(sprintf(
111
                'Wrong type of argument passed in to is_supported(): "%1$s".',
112
                gettype($features)
113
            ));
114
        }
115
116 23
        $isSupported = true;
117
118 23 View Code Duplication
        while ($isSupported && count($features) > 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
119 23
            $feature = array_pop($features);
120 23
            $isSupported &= (bool)$this->checkSupport($feature);
121
        }
122
123 23
        return (bool)$isSupported;
124
    }
125
126
    /**
127
     * Get the minimum required version that supports all of the requested
128
     * features.
129
     *
130
     * Accepts either a string or an array of strings. Returns a
131
     * SemanticVersion object for the version number that is known to support
132
     * all the passed-in features, or false if at least one of
133
     * them is not supported by any known version.
134
     *
135
     * @since 0.2.0
136
     *
137
     * @param string|array $features    What features to check the support of.
138
     * @return SemanticVersion|bool
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use SemanticVersion|false.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
139
     * @throws InvalidArgumentException If the wrong type of argument is passed
140
     *                                  in.
141
     * @throws RuntimeException         If a requirement could not be parsed.
142
     */
143 23
    public function getMinimumRequired($features)
144
    {
145
146 23
        if (is_string($features)) {
147 21
            $features = array($features);
148
        }
149
150 23
        if ( ! is_array($features)) {
151
            throw new InvalidArgumentException(sprintf(
152
                'Wrong type of argument passed in to get_minimum_required(): "%1$s".',
153
                gettype($features)
154
            ));
155
        }
156
157 23
        $minimumRequired = '0.0.0';
158 23
        $isSupported     = true;
159
160 23 View Code Duplication
        while (count($features) > 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
161 23
            $feature = array_pop($features);
162 23
            $isSupported &= (bool)$this->checkSupport($feature, $minimumRequired);
163
        }
164
165 23
        return $minimumRequired !== '0.0.0' ? new SemanticVersion($minimumRequired, true) : false;
166
    }
167
168
    /**
169
     * Check whether a single feature is supported.
170
     *
171
     * @since 0.1.0
172
     *
173
     * @param string $feature         The feature to check.
174
     * @param string $minimumRequired Optional. Minimum required version that
0 ignored issues
show
Documentation introduced by
Should the type for parameter $minimumRequired not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
175
     *                                supports all features.
176
     * @return bool
177
     * @throws RuntimeException If the requirement could not be parsed.
178
     */
179 46
    protected function checkSupport($feature, &$minimumRequired = null)
180
    {
181
182 46
        if ( ! $this->config->hasKey($feature)) {
183 2
            return false;
184
        }
185
186 44
        $requirements = $this->config->getKey($feature);
187
188 44
        if ( ! is_array($requirements)) {
189 44
            $requirements = array($requirements);
190
        }
191
192 44
        $isSupported = true;
193
194 44
        while (($isSupported || $minimumRequired) && count($requirements) > 0) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $minimumRequired of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
195 44
            $requirement = array_pop($requirements);
196 44
            $isSupported &= (bool)$this->checkRequirement($requirement, $minimumRequired);
197
        }
198
199 44
        return (bool)$isSupported;
200
    }
201
202
    /**
203
     * Check whether a single requirement is met.
204
     *
205
     * @since 0.1.0
206
     *
207
     * @param string $requirement     A requirement that is composed of an
208
     *                                operator and a version milestone.
209
     * @param string $minimumRequired Optional. Minimum required version that
0 ignored issues
show
Documentation introduced by
Should the type for parameter $minimumRequired not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
210
     *                                supports all features.
211
     * @return bool
212
     * @throws RuntimeException If the requirement could not be parsed.
213
     */
214 44
    protected function checkRequirement($requirement, &$minimumRequired = null)
215
    {
216
217 44
        $requirement = trim($requirement);
218 44
        $pattern     = self::COMPARISON_PATTERN;
219
220 44
        $arguments = array();
221 44
        $result    = preg_match($pattern, $requirement, $arguments);
222
223 44
        if ( ! $result || ! isset($arguments[1]) || ! isset($arguments[2])) {
224
            throw new RuntimeException(sprintf(
225
                'Could not parse the requirement "%1$s".',
226
                (string)$requirement
227
            ));
228
        }
229
230 44
        $operator  = isset($arguments[1]) ? (string)$arguments[1] : '>=';
231 44
        $milestone = isset($arguments[2]) ? (string)$arguments[2] : '0.0.0';
232
233 44
        $isSupported = (bool)version_compare($this->version->getVersion(), $milestone, $operator);
234
235 44
        if ($minimumRequired) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $minimumRequired of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
236 22
            $requiredVersion = $this->getRequiredVersion($milestone, $operator);
237 22
            if (version_compare($requiredVersion, $minimumRequired, '>')) {
238 22
                $minimumRequired = $requiredVersion;
239
            }
240
        }
241
242 44
        return $isSupported;
243
    }
244
245
    /**
246
     * Get the required version for a single requirement.
247
     *
248
     * @since 0.2.0
249
     *
250
     * @param string $milestone A version milestone that is used to define the
251
     *                          requirement.
252
     * @param string $operator  An operator that gets applied to the milestone.
253
     *                          Possible values: '<=', 'lt', '<', 'le', '>=',
254
     *                          'gt', '>', 'ge', '=', '==', 'eq', '!=', '<>',
255
     *                          'ne'
256
     * @return string
257
     */
258 22
    protected function getRequiredVersion($milestone, $operator)
0 ignored issues
show
Unused Code introduced by
The parameter $operator is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
259
    {
260
261
        // TODO: Algorithm is still missing, the `$operator` is simply ignored
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
262
        // and the pure `$milestone` is returned.
263 22
        return $milestone;
264
    }
265
}
266