Test Setup Failed
Branch master (59633e)
by Alain
02:10
created

PHPFeature::getRequiredVersion()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 7
rs 9.4285
cc 1
eloc 2
nc 1
nop 2
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
    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
        if ( ! $config) {
67
            $config = new Config(include(__DIR__ . '/../config/known_features.php'));
68
        }
69
70
        $this->config = $config;
71
72
        if (null === $phpVersion) {
73
            $phpVersion = phpversion();
74
        }
75
76
        if (is_integer($phpVersion)) {
77
            $phpVersion = (string)$phpVersion;
78
        }
79
80
        if (is_string($phpVersion)) {
81
            $phpVersion = new SemanticVersion($phpVersion, true);
82
        }
83
84
        $this->version = $phpVersion;
85
    }
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
    public function isSupported($features)
103
    {
104
105
        if (is_string($features)) {
106
            $features = array($features);
107
        }
108
109
        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
        $isSupported = true;
117
118 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
            $feature = array_pop($features);
120
            $isSupported &= (bool)$this->checkSupport($feature);
121
        }
122
123
        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
    public function getMinimumRequired($features)
144
    {
145
146
        if (is_string($features)) {
147
            $features = array($features);
148
        }
149
150
        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
        $minimumRequired = '0.0.0';
158
        $isSupported     = true;
159
160 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
            $feature = array_pop($features);
162
            $isSupported &= (bool)$this->checkSupport($feature, $minimumRequired);
163
        }
164
165
        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
    protected function checkSupport($feature, &$minimumRequired = null)
180
    {
181
182
        if ( ! $this->config->hasKey($feature)) {
183
            return false;
184
        }
185
186
        $requirements = $this->config->getKey($feature);
187
188
        if ( ! is_array($requirements)) {
189
            $requirements = array($requirements);
190
        }
191
192
        $isSupported = true;
193
194
        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
            $requirement = array_pop($requirements);
196
            $isSupported &= (bool)$this->checkRequirement($requirement, $minimumRequired);
197
        }
198
199
        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
    protected function checkRequirement($requirement, &$minimumRequired = null)
215
    {
216
217
        $requirement = trim($requirement);
218
        $pattern     = self::COMPARISON_PATTERN;
219
220
        $arguments = array();
221
        $result    = preg_match($pattern, $requirement, $arguments);
222
223
        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
        $operator  = isset($arguments[1]) ? (string)$arguments[1] : '>=';
231
        $milestone = isset($arguments[2]) ? (string)$arguments[2] : '0.0.0';
232
233
        $isSupported = (bool)version_compare($this->version->getVersion(), $milestone, $operator);
234
235
        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
            $requiredVersion = $this->getRequiredVersion($milestone, $operator);
237
            if (version_compare($requiredVersion, $minimumRequired, '>')) {
238
                $minimumRequired = $requiredVersion;
239
            }
240
        }
241
242
        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
    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
        return $milestone;
264
    }
265
}
266