Passed
Push — master ( b0e99a...74682a )
by Alain
02:34
created

PHPFeature::getLesserEqualVersion()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 8

Duplication

Lines 15
Ratio 100 %

Code Coverage

Tests 7
CRAP Score 4.0312

Importance

Changes 2
Bugs 0 Features 2
Metric Value
c 2
b 0
f 2
dl 15
loc 15
ccs 7
cts 8
cp 0.875
rs 9.2
cc 4
eloc 8
nc 4
nop 1
crap 4.0312
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 66
    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 66
        if ( ! $config) {
79 46
            $config = new Config(include(dirname(__FILE__) . '/../config/known_features.php'));
80
        }
81
82 66
        $this->config = $config;
83
84 66
        if (null === $phpVersion) {
85
            $phpVersion = phpversion();
86
        }
87
88 66
        if (is_int($phpVersion)) {
89
            $phpVersion = (string)$phpVersion;
90
        }
91
92 66
        if (is_string($phpVersion)) {
93 66
            $phpVersion = new SemanticVersion($phpVersion, true);
94
        }
95
96 66
        $this->version = $phpVersion;
97 66
    }
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 43
    public function getMinimumRequired($features)
154
    {
155
156 43
        if (is_string($features)) {
157 41
            $features = array($features);
158
        }
159
160 43
        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 43
        $minimumRequired = '0.0.0';
168 43
        $isSupported     = true;
169
170 43 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 43
            $feature = array_pop($features);
172 43
            $isSupported &= (bool)$this->checkSupport($feature, $minimumRequired);
173
        }
174
175 43
        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 66
    protected function checkSupport($feature, &$minimumRequired = null)
190
    {
191
192 66
        if ( ! $this->config->hasKey($feature)) {
193 2
            return false;
194
        }
195
196 64
        $requirements = (array)$this->config->getKey($feature);
197
198 64
        $isSupported = true;
199
200 64
        while (($isSupported || null !== $minimumRequired) && count($requirements) > 0) {
201 64
            $requirement = array_pop($requirements);
202 64
            $isSupported &= (bool)$this->checkRequirement($requirement, $minimumRequired);
203
        }
204
205 64
        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 64
    protected function checkRequirement($requirement, &$minimumRequired = null)
220
    {
221
222 64
        $requirement = trim($requirement);
223 64
        $pattern     = self::COMPARISON_PATTERN;
224
225 64
        $arguments = array();
226 64
        $result    = preg_match($pattern, $requirement, $arguments);
227
228 64
        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 64
        $operator  = isset($arguments[1]) ? (string)$arguments[1] : '>=';
236 64
        $milestone = isset($arguments[2]) ? (string)$arguments[2] : '0.0.0';
237
238 64
        $isSupported = (bool)version_compare($this->version->getVersion(), $milestone, $operator);
239
240 64
        if (null !== $minimumRequired) {
241 42
            $requiredVersion = $this->getRequiredVersion($milestone, $operator);
242 42
            if (version_compare($requiredVersion, $minimumRequired, '>')) {
243 42
                $minimumRequired = $requiredVersion;
244
            }
245
        }
246
247 64
        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 42
    protected function getRequiredVersion($milestone, $operator)
267
    {
268 42
        if (null === $this->releases) {
269 42
            $this->releases = new PHPReleases();
270
        }
271
272
        switch ($operator) {
273 42
            case '>':
274
            case 'gt':
275 4
                return $this->getGreaterThanVersion($milestone);
276
            case '<':
277
            case 'lt':
278 6
                return $this->getLesserThanVersion($milestone);
279
            case '>=':
280
            case 'ge':
281 26
                return $this->getGreaterEqualVersion($milestone);
282
            case '<=':
283
            case 'le':
284 6
                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 4 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 4
        $data = $this->releases->getAll();
307 4
        foreach ($data as $version => $date) {
308 4
            if (version_compare($version, $milestone, '>')) {
309 4
                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 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...
324
     * @throws RuntimeException If the requirement could not be satisfied.
325
     */
326 6 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 6
        if (version_compare($this->version->getVersion(), $milestone, '<')) {
329 4
            return $this->version->getVersion();
330
        }
331 2
        $data = array_reverse($this->releases->getAll());
332 2
        foreach ($data as $version => $date) {
333 2
            if (version_compare($version, $milestone, '<')) {
334 2
                return $version;
335
            }
336
        }
337
338
        throw new RuntimeException('Could not satisfy version requirements.');
339
    }
340
341
    /**
342
     * Get a version greater or equal than the milestone.
343
     *
344
     * @since 0.2.4
345
     *
346
     * @param string $milestone A version milestone that is used to define the requirement.
347
     *
348
     * @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...
349
     */
350 26 View Code Duplication
    protected function getGreaterEqualVersion($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...
351
    {
352 26
        if ($this->releases->exists($milestone)) {
353 25
            return $milestone;
354
        }
355
356 1
        $data = $this->releases->getAll();
357 1
        foreach ($data as $version => $date) {
358 1
            if (version_compare($version, $milestone, '>=')) {
359 1
                return $version;
360
            }
361
        }
362
363
        throw new RuntimeException('Could not satisfy version requirements.');
364
    }
365
366
    /**
367
     * Get a version lesser or equal than the milestone.
368
     *
369
     * @since 0.2.4
370
     *
371
     * @param string $milestone A version milestone that is used to define the requirement.
372
     *
373
     * @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...
374
     */
375 6 View Code Duplication
    protected function getLesserEqualVersion($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...
376
    {
377 6
        if (version_compare($this->version->getVersion(), $milestone, '<=')) {
378 4
            return $this->version->getVersion();
379
        }
380
381 2
        $data = array_reverse($this->releases->getAll());
382 2
        foreach ($data as $version => $date) {
383 2
            if (version_compare($version, $milestone, '<=')) {
384 2
                return $version;
385
            }
386
        }
387
388
        throw new RuntimeException('Could not satisfy version requirements.');
389
    }
390
}
391