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

RequirementsChecker::renderViewFile()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 25
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
eloc 11
c 2
b 0
f 0
nc 6
nop 3
dl 0
loc 25
ccs 0
cts 11
cp 0
crap 20
rs 9.9
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