Passed
Push — master ( 7ef79d...b0e99a )
by Alain
03:47
created

PHPFeature::getRequiredVersion()   C

Complexity

Conditions 13
Paths 24

Size

Total Lines 27
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 84.2968

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 27
ccs 5
cts 20
cp 0.25
rs 5.1234
cc 13
eloc 21
nc 24
nop 2
crap 84.2968

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * PHPFeature Class.
4
 *
5
 * @package   brightnucleus/phpfeature
6
 * @author    Alain Schlesser <[email protected]>
7
 * @license   MIT
8
 * @link      http://www.brightnucleus.com/
9
 * @copyright 2016 Alain Schlesser, Bright Nucleus
10
 */
11
12
use BrightNucleus_Config as Config;
13
use BrightNucleus_ConfigInterface as ConfigInterface;
14
use PHPFeature_SemanticVersion as SemanticVersion;
15
16
/**
17
 * Class PHPFeature.
18
 *
19
 * The main PHP Feature implementation of the `FeatureInterface`.
20
 *
21
 * @since  0.1.0
22
 *
23
 * @author Alain Schlesser <[email protected]>
24
 */
25
class PHPFeature implements FeatureInterface
26
{
27
28
    /**
29
     * RegEx pattern that matches the comparison string.
30
     *
31
     * @since 0.1.0
32
     *
33
     * @var string
34
     */
35
    const COMPARISON_PATTERN = '/^(?:(<=|lt|<|le|>=|gt|>|ge|=|==|eq|!=|<>|ne))([0-9].*)$/';
36
37
    /**
38
     * Reference to the Configuration object.
39
     *
40
     * @since 0.1.0
41
     *
42
     * @var ConfigInterface
43
     */
44
    protected $config;
45
46
    /**
47
     * Reference to the Version object.
48
     *
49
     * @since 0.1.0
50
     *
51
     * @var SemanticVersion
52
     */
53
    protected $version;
54
55
    /**
56
     * Reference to the PHP releases.
57
     *
58
     * @since 0.2.4
59
     *
60
     * @var PHPReleases
61
     */
62
    protected $releases;
63
64
    /**
65
     * Instantiate a PHPFeature object.
66
     *
67
     * @since 0.1.0
68
     *
69
     * @param SemanticVersion|string|int|null $phpVersion Version of PHP to check the features for.
70
     * @param ConfigInterface|null            $config     Configuration that contains the known features.
71
     *
72
     * @throws RuntimeException If the PHP version could not be validated.
73
     */
74 46
    public function __construct($phpVersion = null, ConfigInterface $config = null)
75
    {
76
77
        // 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...
78 46
        if ( ! $config) {
79 46
            $config = new Config(include(dirname(__FILE__) . '/../config/known_features.php'));
80
        }
81
82 46
        $this->config = $config;
83
84 46
        if (null === $phpVersion) {
85
            $phpVersion = phpversion();
86
        }
87
88 46
        if (is_int($phpVersion)) {
89
            $phpVersion = (string)$phpVersion;
90
        }
91
92 46
        if (is_string($phpVersion)) {
93 46
            $phpVersion = new SemanticVersion($phpVersion, true);
94
        }
95
96 46
        $this->version = $phpVersion;
97 46
    }
98
99
    /**
100
     * Check whether a feature or a collection of features is supported.
101
     *
102
     * Accepts either a string or an array of strings. Returns true if all the passed-in features are supported, or
103
     * false if at least one of them is not.
104
     *
105
     * @since 0.1.0
106
     *
107
     * @param string|array $features What features to check the support of.
108
     *
109
     * @return bool Whether the set of features as a whole is supported.
110
     * @throws InvalidArgumentException If the wrong type of argument is passed in.
111
     * @throws RuntimeException         If a requirement could not be parsed.
112
     */
113 23
    public function isSupported($features)
114
    {
115
116 23
        if (is_string($features)) {
117 21
            $features = array($features);
118
        }
119
120 23
        if ( ! is_array($features)) {
121
            throw new InvalidArgumentException(sprintf(
122
                'Wrong type of argument passed in to is_supported(): "%1$s".',
123
                gettype($features)
124
            ));
125
        }
126
127 23
        $isSupported = true;
128
129 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...
130 23
            $feature = array_pop($features);
131 23
            $isSupported &= (bool)$this->checkSupport($feature);
132
        }
133
134 23
        return (bool)$isSupported;
135
    }
136
137
    /**
138
     * Get the minimum required version that supports all of the requested features.
139
     *
140
     * Accepts either a string or an array of strings. Returns a SemanticVersion object for the version number that is
141
     * known to support all the passed-in features, or false if at least one of them is not supported by any known
142
     * version.
143
     *
144
     * @since 0.2.0
145
     *
146
     * @param string|array $features What features to check the support of.
147
     *
148
     * @return SemanticVersion|false SemanticVersion object for the version number that is known to support all the
149
     *                               passed-in features, false if none.
150
     * @throws InvalidArgumentException If the wrong type of argument is passed in.
151
     * @throws RuntimeException         If a requirement could not be parsed.
152
     */
153 23
    public function getMinimumRequired($features)
154
    {
155
156 23
        if (is_string($features)) {
157 21
            $features = array($features);
158
        }
159
160 23
        if ( ! is_array($features)) {
161
            throw new InvalidArgumentException(sprintf(
162
                'Wrong type of argument passed in to get_minimum_required(): "%1$s".',
163
                gettype($features)
164
            ));
165
        }
166
167 23
        $minimumRequired = '0.0.0';
168 23
        $isSupported     = true;
169
170 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...
171 23
            $feature = array_pop($features);
172 23
            $isSupported &= (bool)$this->checkSupport($feature, $minimumRequired);
173
        }
174
175 23
        return $minimumRequired !== '0.0.0' ? new SemanticVersion($minimumRequired, true) : false;
176
    }
177
178
    /**
179
     * Check whether a single feature is supported.
180
     *
181
     * @since 0.1.0
182
     *
183
     * @param string      $feature         The feature to check.
184
     * @param string|null $minimumRequired Optional. Minimum required version that supports all features.
185
     *
186
     * @return bool Whether the requested feature is supported.
187
     * @throws RuntimeException If the requirement could not be parsed.
188
     */
189 46
    protected function checkSupport($feature, &$minimumRequired = null)
190
    {
191
192 46
        if ( ! $this->config->hasKey($feature)) {
193 2
            return false;
194
        }
195
196 44
        $requirements = (array)$this->config->getKey($feature);
197
198 44
        $isSupported = true;
199
200 44
        while (($isSupported || null !== $minimumRequired) && count($requirements) > 0) {
201 44
            $requirement = array_pop($requirements);
202 44
            $isSupported &= (bool)$this->checkRequirement($requirement, $minimumRequired);
203
        }
204
205 44
        return (bool)$isSupported;
206
    }
207
208
    /**
209
     * Check whether a single requirement is met.
210
     *
211
     * @since 0.1.0
212
     *
213
     * @param string      $requirement     A requirement that is composed of an operator and a version milestone.
214
     * @param string|null $minimumRequired Optional. Minimum required version that supports all features.
215
     *
216
     * @return bool Whether the requirement is met.
217
     * @throws RuntimeException If the requirement could not be parsed.
218
     */
219 44
    protected function checkRequirement($requirement, &$minimumRequired = null)
220
    {
221
222 44
        $requirement = trim($requirement);
223 44
        $pattern     = self::COMPARISON_PATTERN;
224
225 44
        $arguments = array();
226 44
        $result    = preg_match($pattern, $requirement, $arguments);
227
228 44
        if ( ! $result || ! isset($arguments[1]) || ! isset($arguments[2])) {
229
            throw new RuntimeException(sprintf(
230
                'Could not parse the requirement "%1$s".',
231
                (string)$requirement
232
            ));
233
        }
234
235 44
        $operator  = isset($arguments[1]) ? (string)$arguments[1] : '>=';
236 44
        $milestone = isset($arguments[2]) ? (string)$arguments[2] : '0.0.0';
237
238 44
        $isSupported = (bool)version_compare($this->version->getVersion(), $milestone, $operator);
239
240 44
        if (null !== $minimumRequired) {
241 22
            $requiredVersion = $this->getRequiredVersion($milestone, $operator);
242 22
            if (version_compare($requiredVersion, $minimumRequired, '>')) {
243 22
                $minimumRequired = $requiredVersion;
244
            }
245
        }
246
247 44
        return $isSupported;
248
    }
249
250
    /**
251
     * Get the required version for a single requirement.
252
     *
253
     * @todo  The entire algorithm is only an approximation. A 5.2 SemVer library is needed.
0 ignored issues
show
Coding Style introduced by
Comment refers to a TODO task

This check looks TODO comments that have been left in the code.

``TODO``s show that something is left unfinished and should be attended to.

Loading history...
254
     *
255
     * @since 0.2.0
256
     *
257
     * @param string $milestone A version milestone that is used to define the requirement.
258
     * @param string $operator  An operator that gets applied to the milestone.
259
     *                          Possible values: '<=', 'lt', '<', 'le', '>=', 'gt', '>', 'ge', '=', '==', 'eq', '!=',
260
     *                          '<>', 'ne'
261
     *
262
     * @return string Version string that meets a single requirement.
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
263
     * @throws RuntimeException If the requirement could not be satisfied.
264
     * @throws RuntimeException If the NotEqual is used.
265
     */
266 22
    protected function getRequiredVersion($milestone, $operator)
267
    {
268 22
        if (null === $this->releases) {
269 22
            $this->releases = new PHPReleases();
270
        }
271
272
        switch ($operator) {
273 22
            case '>':
274
            case 'gt':
275
                return $this->getGreaterThanVersion($milestone);
276
            case '<':
277
            case 'lt':
278
                return $this->getLesserThanVersion($milestone);
279
            case '>=':
280
            case 'ge':
281 22
                return $this->getGreaterEqualVersion($milestone);
282
            case '<=':
283
            case 'le':
284
                return $this->getLesserEqualVersion($milestone);
285
            case '!=':
286
            case '<>':
287
            case 'ne':
288
                throw new RuntimeException('NotEqual operator is not implemented.');
289
        }
290
291
        return $milestone;
292
    }
293
294
    /**
295
     * Get a version greater than the milestone.
296
     *
297
     * @since 0.2.4
298
     *
299
     * @param string $milestone A version milestone that is used to define the requirement.
300
     *
301
     * @return string Version number that meets the requirement.
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
302
     * @throws RuntimeException If the requirement could not be satisfied.
303
     */
304 View Code Duplication
    protected function getGreaterThanVersion($milestone)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
305
    {
306
        $data = $this->releases->getAll();
307
        foreach ($data as $version => $date) {
308
            if (version_compare($version, $milestone, '>')) {
309
                return $version;
310
            }
311
        }
312
313
        throw new RuntimeException('Could not satisfy version requirements.');
314
    }
315
316
    /**
317
     * Get a version lesser than the milestone.
318
     *
319
     * @since 0.2.4
320
     *
321
     * @param string $milestone A version milestone that is used to define the requirement.
322
     *
323
     * @return string Version number that meets the requirement.
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
324
     * @throws RuntimeException If the requirement could not be satisfied.
325
     */
326 View Code Duplication
    protected function getLesserThanVersion($milestone)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
327
    {
328
        $data = $this->releases->getAll();
329
        foreach ($data as $version => $date) {
330
            if (version_compare($version, $milestone, '<')) {
331
                return $version;
332
            }
333
        }
334
335
        throw new RuntimeException('Could not satisfy version requirements.');
336
    }
337
338
    /**
339
     * Get a version greater or equal than the milestone.
340
     *
341
     * @since 0.2.4
342
     *
343
     * @param string $milestone A version milestone that is used to define the requirement.
344
     *
345
     * @return string Version number that meets the requirement.
0 ignored issues
show
Documentation introduced by
Should the return type not be string|integer?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
346
     */
347 22
    protected function getGreaterEqualVersion($milestone)
348
    {
349 22
        if ($this->releases->exists($milestone)) {
350 22
            return $milestone;
351
        }
352
353
        return $this->getGreaterThanVersion($milestone);
354
    }
355
356
    /**
357
     * Get a version lesser or equal than the milestone.
358
     *
359
     * @since 0.2.4
360
     *
361
     * @param string $milestone A version milestone that is used to define the requirement.
362
     *
363
     * @return string Version number that meets the requirement.
0 ignored issues
show
Documentation introduced by
Should the return type not be string|integer?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
364
     */
365
    protected function getLesserEqualVersion($milestone)
366
    {
367
        if ($this->releases->exists($milestone)) {
368
            return $milestone;
369
        }
370
371
        return $this->getLesserThanVersion($milestone);
372
    }
373
}
374