Passed
Pull Request — master (#71)
by Sergei
11:11
created

RequirementsChecker::getByteSize()   B

Complexity

Conditions 10
Paths 10

Size

Total Lines 25
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 10.3375

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 20
c 1
b 0
f 0
nc 10
nop 1
dl 0
loc 25
ccs 17
cts 20
cp 0.85
crap 10.3375
rs 7.6666

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
declare(strict_types=1);
4
5
namespace Yiisoft\Requirements;
6
7
use function intval;
8
9
/**
10
 * YiiRequirementChecker allows checking, if current system meets the requirements for running the Yii application.
11
 * This class allows rendering of the check report for the web and console application interface.
12
 *
13
 * Example:
14
 *
15
 * ```php
16
 * require_once 'path/to/YiiRequirementChecker.php';
17
 * $requirementsChecker = new YiiRequirementChecker();
18
 * $requirements = [
19
 *     [
20
 *         'name' => 'PHP Some Extension',
21
 *         'mandatory' => true`,
22
 *         'condition' => extension_loaded('some_extension'),
23
 *         'by' => 'Some application feature',
24
 *         'memo' => 'PHP extension "some_extension" required',
25
 *     ],
26
 * ];
27
 * $requirementsChecker
28
 *     ->check($requirements)
29
 *     ->render();
30
 * ```
31
 *
32
 * If you wish to render the report with your own representation, use {@see getResult()} instead of {@see render()}.
33
 *
34
 * Requirement condition could be in format "eval:PHP expression".
35
 * In this case specified PHP expression will be evaluated in the context of this class instance.
36
 * For example:
37
 *
38
 * ```php
39
 * $requirements = [
40
 *     [
41
 *         'name' => 'Upload max file size',
42
 *         'condition' => 'eval:$this->checkUploadMaxFileSize("5M")',
43
 *     ],
44
 * ];
45
 * ```
46
 */
47
final class RequirementsChecker
48
{
49
    /**
50
     * @var array|null The check results, this property is for internal usage only.
51
     *
52
     * @psalm-var array{
53
     *     summary: array{
54
     *       total: int,
55
     *       errors: int,
56
     *       warnings: int,
57
     *     },
58
     *     requirements: array
59
     * }|null
60
     */
61
    public ?array $result = null;
62
63
    /**
64
     * Check the given requirements, collecting results into internal field.
65
     * This method can be invoked several times checking different requirement sets.
66
     * Use {@see getResult()} or {@see render()} to get the results.
67
     * @param array|string $requirements Requirements to be checked.
68
     * If an array, it is treated as the set of requirements;
69
     * If a string, it is treated as the path of the file, which contains the requirements;
70
     * @return $this Self instance.
71
     */
72 3
    public function check($requirements): self
73
    {
74 3
        if (is_string($requirements)) {
75
            /** @psalm-suppress UnresolvableInclude */
76
            $requirements = require $requirements;
77
        }
78 3
        if (!is_array($requirements)) {
79
            $this->usageError('Requirements must be an array, "' . gettype($requirements) . '" has been given!');
80
        }
81 3
        if (!isset($this->result)) {
82 3
            $this->result = [
83 3
                'summary' => [
84 3
                    'total' => 0,
85 3
                    'errors' => 0,
86 3
                    'warnings' => 0,
87 3
                ],
88 3
                'requirements' => [],
89 3
            ];
90
        }
91 3
        foreach ($requirements as $key => $rawRequirement) {
92 3
            if (!is_array($rawRequirement)) {
93
                $this->usageError(
94
                    'Requirement must be an array, "' . gettype($rawRequirement) . '" has been given!'
95
                );
96
            }
97 3
            $requirement = $this->normalizeRequirement($rawRequirement, $key);
98 3
            $this->result['summary']['total']++;
99 3
            if (!$requirement['condition']) {
100 2
                if ($requirement['mandatory']) {
101 2
                    $requirement['error'] = true;
102 2
                    $requirement['warning'] = true;
103 2
                    $this->result['summary']['errors']++;
104
                } else {
105 1
                    $requirement['error'] = false;
106 1
                    $requirement['warning'] = true;
107 2
                    $this->result['summary']['warnings']++;
108
                }
109
            } else {
110 3
                $requirement['error'] = false;
111 3
                $requirement['warning'] = false;
112
            }
113 3
            $this->result['requirements'][] = $requirement;
114
        }
115
116 3
        return $this;
117
    }
118
119
    /**
120
     * Return the check results.
121
     * @return array|null check results in format:
122
     *
123
     * ```php
124
     * [
125
     *     'summary' => [
126
     *         'total' => total number of checks,
127
     *         'errors' => number of errors,
128
     *         'warnings' => number of warnings,
129
     *     ],
130
     *     'requirements' => [
131
     *         [
132
     *             ...
133
     *             'error' => is there an error,
134
     *             'warning' => is there a warning,
135
     *         ],
136
     *         ...
137
     *     ],
138
     * ]
139
     * ```
140
     */
141 3
    public function getResult(): ?array
142
    {
143 3
        return $this->result ?? null;
144
    }
145
146
    /**
147
     * Renders the requirements check result.
148
     * The output will vary depending on if a script running from web or from console.
149
     */
150
    public function render(): void
151
    {
152
        if (!isset($this->result)) {
153
            $this->usageError('Nothing to render!');
154
        }
155
        $baseViewFilePath = __DIR__ . DIRECTORY_SEPARATOR . 'views';
156
        if (!empty($_SERVER['argv'])) {
157
            $viewFileName = $baseViewFilePath . DIRECTORY_SEPARATOR . 'console' . DIRECTORY_SEPARATOR . 'index.php';
158
        } else {
159
            $viewFileName = $baseViewFilePath . DIRECTORY_SEPARATOR . 'web' . DIRECTORY_SEPARATOR . 'index.php';
160
        }
161
        $this->renderViewFile($viewFileName, $this->result);
162
    }
163
164
    /**
165
     * Checks if the given PHP extension is available and its version matches the given one.
166
     * @param string $extensionName PHP extension name.
167
     * @param string $version Required PHP extension version.
168
     * @param string $compare Comparison operator, by default '>='
169
     * @return bool If PHP extension version matches.
170
     *
171
     * @psalm-param '!='|'<'|'<='|'<>'|'='|'=='|'>'|'>='|'eq'|'ge'|'gt'|'le'|'lt'|'ne' $compare
172
     */
173 1
    public function checkPhpExtensionVersion(string $extensionName, string $version, string $compare = '>='): bool
174
    {
175 1
        if (!extension_loaded($extensionName)) {
176 1
            return false;
177
        }
178 1
        $extensionVersion = phpversion($extensionName);
179 1
        if (empty($extensionVersion)) {
180
            return false;
181
        }
182 1
        if (strncasecmp($extensionVersion, 'PECL-', 5) === 0) {
183
            $extensionVersion = substr($extensionVersion, 5);
184
        }
185
186
        /** @var bool */
187 1
        return version_compare($extensionVersion, $version, $compare);
0 ignored issues
show
Bug Best Practice introduced by
The expression return version_compare($...on, $version, $compare) could return the type integer which is incompatible with the type-hinted return boolean. Consider adding an additional type-check to rule them out.
Loading history...
188
    }
189
190
    /**
191
     * Checks if PHP configuration option (from `php.ini`) is on.
192
     * @param string $name Configuration option name.
193
     * @return bool Whether option is on.
194
     */
195
    public function checkPhpIniOn(string $name): bool
196
    {
197
        $value = ini_get($name);
198
        if (empty($value)) {
199
            return false;
200
        }
201
202
        return ((int) $value === 1 || strtolower($value) === 'on');
203
    }
204
205
    /**
206
     * Checks if PHP configuration option (from `php.ini`) is off.
207
     * @param string $name Configuration option name.
208
     * @return bool Whether option is off.
209
     */
210
    public function checkPhpIniOff(string $name): bool
211
    {
212
        $value = ini_get($name);
213
        if (empty($value)) {
214
            return true;
215
        }
216
217
        return (strtolower($value) === 'off');
218
    }
219
220
    /**
221
     * Compare byte sizes of values given in the verbose representation,
222
     * like '5M', '15K' etc.
223
     * @param string $a First value.
224
     * @param string $b Second value.
225
     * @param string $compare Comparison operator, by default '>='.
226
     * @return bool Comparison result.
227
     */
228 5
    public function compareByteSize(string $a, string $b, string $compare = '>='): bool
229
    {
230 5
        $compareExpression = '(' . $this->getByteSize($a) . $compare . $this->getByteSize($b) . ')';
231
232
        /** @var bool */
233 5
        return $this->evaluateExpression($compareExpression);
234
    }
235
236
    /**
237
     * Gets the size in bytes from verbose size representation.
238
     * For example: '5K' => 5*1024
239
     * @param string $verboseSize Verbose size representation.
240
     * @return int Actual size in bytes.
241
     */
242 12
    public function getByteSize(string $verboseSize): int
243
    {
244 12
        if (empty($verboseSize)) {
245
            return 0;
246
        }
247 12
        if (is_numeric($verboseSize)) {
248 2
            return (int) $verboseSize;
249
        }
250 11
        $sizeUnit = trim($verboseSize, '0123456789');
251 11
        $size = trim(str_replace($sizeUnit, '', $verboseSize));
252 11
        if (!is_numeric($size)) {
253
            return 0;
254
        }
255 11
        switch (strtolower($sizeUnit)) {
256 11
            case 'kb':
257 10
            case 'k':
258 5
                return intval($size * 1024);
259 8
            case 'mb':
260 7
            case 'm':
261 6
                return intval($size * 1024 * 1024);
262 2
            case 'gb':
263 1
            case 'g':
264 2
                return intval($size * 1024 * 1024 * 1024);
265
            default:
266
                return 0;
267
        }
268
    }
269
270
    /**
271
     * Checks if upload max file size matches the given range.
272
     * @param string|null $min Verbose file size minimum required value, pass null to skip minimum check.
273
     * @param string|null $max Verbose file size maximum required value, pass null to skip maximum check.
274
     * @return bool True on success.
275
     */
276
    public function checkUploadMaxFileSize(?string $min = null, ?string $max = null): bool
277
    {
278
        $postMaxSize = ini_get('post_max_size');
279
        $uploadMaxFileSize = ini_get('upload_max_filesize');
280
        if ($min !== null) {
281
            $minCheckResult = $this->compareByteSize($postMaxSize, $min, '>=') && $this->compareByteSize($uploadMaxFileSize, $min, '>=');
282
        } else {
283
            $minCheckResult = true;
284
        }
285
        if ($max !== null) {
286
            $maxCheckResult = $this->compareByteSize($postMaxSize, $max, '<=') && $this->compareByteSize($uploadMaxFileSize, $max, '<=');
287
        } else {
288
            $maxCheckResult = true;
289
        }
290
291
        return ($minCheckResult && $maxCheckResult);
292
    }
293
294
    /**
295
     * Renders a view file.
296
     * This method includes the view file as a PHP script
297
     * and captures the display result if required.
298
     * @param string $_viewFile_ View file.
299
     * @param array|null $_data_ Data to be extracted and made available to the view file.
300
     * @param bool $_return_ Whether the rendering result should be returned as a string.
301
     * @return string The rendering result. Null if the rendering result is not required.
302
     */
303
    public function renderViewFile(string $_viewFile_, array $_data_ = null, bool $_return_ = false): ?string
304
    {
305
        // we use special variable names here to avoid conflict when extracting data
306
        if (is_array($_data_)) {
307
            extract($_data_, EXTR_PREFIX_SAME, 'data');
308
        } else {
309
            $data = $_data_;
310
        }
311
        if ($_return_) {
312
            ob_start();
313
            /**
314
             * @psalm-suppress InvalidArgument Need for compatibility with PHP 7.4
315
             */
316
            PHP_VERSION_ID >= 80000 ? ob_implicit_flush(false) : ob_implicit_flush(0);
317
318
            /** @psalm-suppress UnresolvableInclude */
319
            require $_viewFile_;
320
321
            return ob_get_clean();
322
        }
323
324
        /** @psalm-suppress UnresolvableInclude */
325
        require $_viewFile_;
326
327
        return null;
328
    }
329
330
    /**
331
     * Normalizes requirement ensuring it has correct format.
332
     * @param array $requirement Raw requirement.
333
     * @param int|string $requirementKey Requirement key in the list.
334
     * @return array Normalized requirement.
335
     */
336 3
    public function normalizeRequirement(array $requirement, $requirementKey = 0): array
337
    {
338 3
        if (!array_key_exists('condition', $requirement)) {
339
            $this->usageError("Requirement \"$requirementKey\" has no condition!");
340
        } else {
341 3
            $evalPrefix = 'eval:';
342 3
            if (is_string($requirement['condition']) && strpos($requirement['condition'], $evalPrefix) === 0) {
343 1
                $expression = substr($requirement['condition'], strlen($evalPrefix));
344 1
                $requirement['condition'] = $this->evaluateExpression($expression);
345
            }
346
        }
347 3
        if (!array_key_exists('name', $requirement)) {
348
            $requirement['name'] = is_numeric($requirementKey) ? 'Requirement #' . $requirementKey : $requirementKey;
349
        }
350 3
        if (!array_key_exists('mandatory', $requirement)) {
351
            if (array_key_exists('required', $requirement)) {
352
                $requirement['mandatory'] = $requirement['required'];
353
            } else {
354
                $requirement['mandatory'] = false;
355
            }
356
        }
357 3
        if (!array_key_exists('by', $requirement)) {
358
            $requirement['by'] = 'Unknown';
359
        }
360 3
        if (!array_key_exists('memo', $requirement)) {
361
            $requirement['memo'] = '';
362
        }
363
364 3
        return $requirement;
365
    }
366
367
    /**
368
     * Displays a usage error.
369
     * This method will then terminate the execution of the current application.
370
     * @param string $message the error message
371
     *
372
     * @psalm-return never
373
     */
374
    public function usageError(string $message): void
375
    {
376
        echo "Error: $message\n\n";
377
        exit(1);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
378
    }
379
380
    /**
381
     * Evaluates a PHP expression under the context of this class.
382
     * @param string $expression A PHP expression to be evaluated.
383
     * @return mixed The expression result.
384
     */
385 6
    public function evaluateExpression(string $expression)
386
    {
387 6
        return eval('return ' . $expression . ';');
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
388
    }
389
390
    /**
391
     * Returns the server information.
392
     * @return string server information.
393
     */
394
    public function getServerInfo(): string
395
    {
396
        return $_SERVER['SERVER_SOFTWARE'] ?? '';
397
    }
398
399
    /**
400
     * Returns the now date if possible in string representation.
401
     * @return string now date.
402
     */
403
    public function getNowDate(): string
404
    {
405
        return @strftime('%Y-%m-%d %H:%M', time());
0 ignored issues
show
Bug Best Practice introduced by
The expression return @strftime('%Y-%m-%d %H:%M', time()) could return the type false which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
406
    }
407
}
408