├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src └── Checker.php ├── tests └── ChekerTest.php └── usage └── usage.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | indent_style = space 12 | indent_size = 4 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .phpunit.result.cache 3 | composer.lock 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Miraz Mac 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![PHP Requirements Checker](https://i.postimg.cc/PxhHbVsY/pcr.png) 2 | 3 | # PHP Requirements Checker 4 | A PHP library to check the current environment against a set of defined requirements. Currently it supports checking for PHP version, OS, extensions, php.ini values, functions, classes, apache modules and local files and folders. 5 | 6 | ### Install via composer 7 | 8 | ```shell 9 | composer require mirazmac/php-requirements-checker 10 | ``` 11 | 12 | ### Manual Install 13 | 14 | Download the latest release. Extract and require **src/Checker.php** in your code. But it's highly recommended to use [Composer](http://getcomposer.org). 15 | 16 | ```php 17 | require 'src/Checker.php'; 18 | ``` 19 | 20 | 21 | 22 | ## Usage 23 | 24 | ```php 25 | use MirazMac\Requirements\Checker; 26 | 27 | $checker = new Checker; 28 | 29 | // Define requirements 30 | $checker->requirePhpVersion('>=5.6') 31 | ->requirePhpExtensions(['ffmpeg', 'mbstring']) 32 | ->requireFunctions(['random_bytes']) 33 | ->requireFile('../composer.json', Checker::CHECK_FILE_EXISTS) 34 | ->requireDirectory('../src', Checker::CHECK_IS_READABLE) 35 | ->requireIniValues([ 36 | 'allow_url_fopen' => true, 37 | 'short_open_tag' => true, 38 | 'memory_limit' => '>=64M', 39 | ]); 40 | 41 | // Runs the check and returns parsed requirements as an array 42 | // Contains parsed requirements with state of the current values 43 | // and their comparison result 44 | $output = $checker->check(); 45 | 46 | // Should be called after running check() to see if requirements has met or not 47 | $satisfied = $checker->isSatisfied(); 48 | 49 | if ($satisfied) { 50 | echo "Requirements are met."; 51 | } else { 52 | echo join(', ', $checker->getErrors()); 53 | } 54 | 55 | ``` 56 | 57 | ## Supported Requirement Checks 58 | 59 | Every supported requirements check begins with the word **require**. They return the class instance that means they're chain-able. These are the supported checks: 60 | 61 | ### requirePhpVersion(string $version); 62 | You can check if current PHP version matches your desired version using this method. The parameter ``$version`` should be a string containing your desired PHP version. Comparison operators can be prepended at the very beginning of the string. 63 | 64 | ```php 65 | $checker->requirePhpVersion('7.0.0'); 66 | 67 | // Note the comparison operator 68 | // Supports comparison operators: <, >, =, >= 69 | $checker->requirePhpVersion('>=7.0.0'); 70 | ``` 71 | 72 | ### requireOS(string $os); 73 | You can check if current OS matches with your desired operating system. The parameter ``$os`` must have one of the following values: 74 | ``Checker::OS_UNIX``, ``Checker::OS_DOS`` 75 | 76 | ```php 77 | $checker->requireOS(Checker::OS_UNIX); 78 | ``` 79 | 80 | 81 | ### requireIniValues(array $values) 82 | Use this to validate a set of php.ini config values to compare against your provided values. The parameter ``$values`` should be an array as key => value fashion, where the key would contain the php.ini config var and the value should be the desired value. Like ``requirePhpVersion();`` comparison operators can be prepended at the very beginning of the value. 83 | To keep things simple and neat, use ``boolean`` instead of using ``On/1/Off/0`` for the check. 84 | 85 | ```php 86 | $checker->requireIniValues([ 87 | // Will check if file_uploads is enabled or not 88 | // Notice the usage of boolean instead of On/Off/1/0 89 | 'file_uploads' => true, 90 | 91 | // Note the comparison operator > before the desired value 92 | // This means the library will check if post_max_size is greater than 2M or not 93 | 'post_max_size' => '>2M', 94 | 95 | // Set a value to `NULL` to just skip the check for that value 96 | // Useful when you don't wanna compare but want to fetch the 97 | // current value on the parsed requirements array 98 | 'safe_mode' => null, 99 | ]); 100 | ``` 101 | 102 | ### requirePhpExtensions(array $extensions) 103 | To make sure provided extensions are loaded. Parameter ``$extenstions`` should be an array with the extension names. 104 | 105 | ```php 106 | $checker->requirePhpExtensions([ 107 | 'openssl', 'mbstring', 'curl' 108 | ]); 109 | ``` 110 | 111 | ### requireFunctions(array $functions) 112 | To make sure provided functions are loaded. Parameter ``$functions`` should be an array with the function names. 113 | 114 | ```php 115 | $checker->requireFunctions([ 116 | 'apcu_fetch', 'mb_substr', 'curl_init' 117 | ]); 118 | ``` 119 | 120 | ### requireClasses(array $classes) 121 | To make sure provided classes are loaded. Parameter ``$classes`` should be an array with the class names (namespaced or global namespace will be used). 122 | 123 | ```php 124 | $checker->requireClasses([ 125 | 'PDO', 'finfo', 'stdClass' 126 | ]); 127 | ``` 128 | 129 | ### requireApacheModules(array $modules) 130 | To make sure provided modules are loaded. Parameter ``$modules`` should be an array with the module names. 131 | NOTE: This check will only run if current server is Apache. 132 | 133 | ```php 134 | $checker->requireApacheModules([ 135 | 'mod_rewrite', 'mod_mime' 136 | ]); 137 | ``` 138 | 139 | ### requireFile(string $path, string $check = self::CHECK_FILE_EXISTS) 140 | To check permissions and existence of certain files and directories. The parameter ``$path`` should be path to any file or directory. 141 | The parameter ``$check`` is the check name that would be performed on the path. The supported values are: 142 | * ``Checker::CHECK_IS_FILE`` - Runs ``is_file()`` on the path. 143 | 144 | * ``Checker::CHECK_IS_DIR`` Runs ``is_dir()`` on the path. 145 | 146 | * ``Checker::CHECK_IS_READABLE`` Runs ``is_readable()`` on the path. 147 | 148 | * ``Checker::CHECK_IS_WRITABLE`` Runs ``is_writable()`` on the path. 149 | 150 | * ``Checker::CHECK_FILE_EXISTS`` Runs ``file_exists()`` on the path. 151 | 152 | NOTE: ``requireDirectory()`` is an alias of this method. 153 | 154 | ```php 155 | $checker->requireFile('app/config.ini', Checker::CHECK_IS_FILE) 156 | ->requireFile('app/cache', Checker::CHECK_IS_WRITABLE); 157 | ->requireDirectory('app/cache', Checker::CHECK_IS_DIR); 158 | ``` 159 | 160 | ## Todos 161 | * Write tests 162 | * Write extended docs 163 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mirazmac/php-requirements-checker", 3 | "description": "A quick library to check the current environment against a set of defined requirements. It supports checking for PHP version, OS, Extensions, PHP INI values, Functions, Classes, Apache Modules and local Files and Folders.", 4 | "type": "library", 5 | "require": { 6 | "php": ">=5.4" 7 | }, 8 | "license": "MIT", 9 | "authors": [ 10 | { 11 | "name": "MirazMac", 12 | "email": "mirazmac@gmail.com" 13 | } 14 | ], 15 | "require-dev": { 16 | "phpunit/phpunit": "^4" 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "MirazMac\\Requirements\\": "src/" 21 | } 22 | }, 23 | "scripts": { 24 | "phpunit": "vendor/bin/phpunit" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./src/ 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | tests/ 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Checker.php: -------------------------------------------------------------------------------- 1 | 13 | * @link https://mirazmac.com Author Homepage 14 | * @version 0.1 15 | * @license LICENSE The MIT License 16 | * @package MirazMac\Requirements 17 | */ 18 | class Checker 19 | { 20 | /** 21 | * Identifier for is_file() check 22 | */ 23 | const CHECK_IS_FILE = 'is_file'; 24 | 25 | /** 26 | * Identifier for is_dir() check 27 | */ 28 | const CHECK_IS_DIR = 'is_dir'; 29 | 30 | /** 31 | * Identifier for is_readable() check 32 | */ 33 | const CHECK_IS_READABLE = 'is_readable'; 34 | 35 | /** 36 | * Identifier for is_writable() check 37 | */ 38 | const CHECK_IS_WRITABLE = 'is_writable'; 39 | 40 | /** 41 | * Identifier for file_exists() check 42 | */ 43 | const CHECK_FILE_EXISTS = 'file_exists'; 44 | 45 | /** 46 | * Identifier for Unix 47 | */ 48 | const OS_UNIX = 'UNIX'; 49 | 50 | /** 51 | * Identifier for Windows 52 | */ 53 | const OS_DOS = 'DOS'; 54 | 55 | /** 56 | * Requirements as an array 57 | * 58 | * @var array 59 | */ 60 | protected $requirements; 61 | 62 | /** 63 | * Parsed state of the requirements 64 | * 65 | * @var array 66 | */ 67 | protected $parsedRequirements; 68 | 69 | /** 70 | * Stores if current requirements criteria is satisfied or not 71 | * 72 | * @var boolean 73 | */ 74 | protected $satisfied = true; 75 | 76 | /** 77 | * Basic locales for comparison operators 78 | * 79 | * @var array 80 | */ 81 | protected $locale = [ 82 | '>' => 'greater than', 83 | '=' => 'equal to', 84 | '<' => 'lower than', 85 | '>=' => 'greater than or equal to', 86 | '=<' => 'lower than or equal to', 87 | ]; 88 | 89 | /** 90 | * Array of the total errors 91 | * 92 | * @var array 93 | */ 94 | protected $errors = []; 95 | 96 | /** 97 | * Constructor 98 | */ 99 | public function __construct() 100 | { 101 | $this->resetRequirements(); 102 | } 103 | 104 | /** 105 | * Perform checks on the passed requirements 106 | * 107 | * @return array The parsed requirements 108 | */ 109 | public function check() 110 | { 111 | // Reset any previous run 112 | $this->errors = []; 113 | 114 | $this->parsedRequirements['system'] = $this->validateSystemRequirement(); 115 | $this->parsedRequirements['extensions'] = $this->validateExtensionRequirement(); 116 | $this->parsedRequirements['apache_modules'] = $this->validateApacheModuleRequirement(); 117 | $this->parsedRequirements['functions'] = $this->validateFunctionRequirement(); 118 | $this->parsedRequirements['classes'] = $this->validateClassRequirement(); 119 | $this->parsedRequirements['ini_values'] = $this->validateIniRequirement(); 120 | $this->parsedRequirements['files'] = $this->validateFileRequirement(); 121 | 122 | return $this->parsedRequirements; 123 | } 124 | 125 | /** 126 | * Resets the requirements to default 127 | * 128 | * @return Checker 129 | */ 130 | public function resetRequirements() 131 | { 132 | $default = [ 133 | 'system' => ['php_version' => null, 'os' => null], 134 | 'ini_values' => [], 135 | 'files' => [], 136 | 'extensions' => [], 137 | 'classes' => [], 138 | 'functions' => [], 139 | 'apache_modules' => [], 140 | ]; 141 | 142 | $this->requirements = $default; 143 | $this->parsedRequirements = $default; 144 | 145 | return $this; 146 | } 147 | 148 | /** 149 | * Return the requirements added currently 150 | * 151 | * @return array 152 | */ 153 | public function getRequirements() 154 | { 155 | return $this->requirements; 156 | } 157 | 158 | /** 159 | * Returns if the last requirement check was satisfying or not 160 | * 161 | * @return boolean 162 | */ 163 | public function isSatisfied() 164 | { 165 | return $this->satisfied; 166 | } 167 | 168 | /** 169 | * Returns all the errors as array 170 | * 171 | * @return array 172 | */ 173 | public function getErrors() 174 | { 175 | return $this->errors; 176 | } 177 | 178 | /** 179 | * Validates only the PHP INI requirements 180 | * 181 | * @return array 182 | */ 183 | public function validateIniRequirement() 184 | { 185 | $values = []; 186 | 187 | foreach ($this->requirements['ini_values'] as $key => $value) { 188 | $setting = ini_get($key); 189 | 190 | if ($setting == 'On' || $setting == '1') { 191 | $setting = true; 192 | } elseif ($setting == 'Off' || $setting == '' || $setting == '0') { 193 | $setting = false; 194 | } 195 | 196 | $data = $this->getParsedStructure(); 197 | $data['preferred'] = $value; 198 | $data['current'] = $setting; 199 | 200 | // So you only want to show the value? 201 | // No validation? Fine. 202 | if (is_null($value)) { 203 | $data['satisfied'] = true; 204 | $values[$key] = $data; 205 | continue; 206 | } 207 | 208 | if (is_bool($value)) { 209 | $data['preferred'] = $value ? 'On' : 'Off'; 210 | $data['current'] = $setting ? 'On' : 'Off'; 211 | if ($value) { 212 | $data['satisfied'] = $setting; 213 | } else { 214 | $data['satisfied'] = !$setting; 215 | } 216 | 217 | if (!$data['satisfied']) { 218 | $this->satisfied = false; 219 | $data['message'] = "The php.ini setting `{$key}` should be {$data['preferred']}, currently it's set to {$data['current']}"; 220 | $this->errors[] = $data['message']; 221 | } 222 | 223 | $values[$key] = $data; 224 | continue; 225 | } 226 | 227 | 228 | $parsed = $this->parseComparisonString($value, '='); 229 | 230 | $value = $parsed['plain']; 231 | $operator = $parsed['operator']; 232 | 233 | $newcfg = $this->returnBytes($setting); 234 | $newval = $this->returnBytes($value); 235 | 236 | // Acknowledge '-1'(unlimited) values 237 | // @see https://github.com/MirazMac/php-requirements-checker/issues/2 238 | $data['satisfied'] = ($setting == '-1') ? true : $this->looseComparison($newcfg, $operator, $newval); 239 | 240 | if (!$data['satisfied']) { 241 | $this->satisfied = false; 242 | $data['message'] = "The php.ini setting `{$key}` should be {$this->locale[$operator]} {$value}. Currently it's set to {$setting}"; 243 | $this->errors[] = $data['message']; 244 | } 245 | 246 | $values[$key] = $data; 247 | } 248 | 249 | return $values; 250 | } 251 | 252 | /** 253 | * Validates only the classes requirement 254 | * 255 | * @return array 256 | */ 257 | public function validateClassRequirement() 258 | { 259 | $values = []; 260 | 261 | foreach ($this->requirements['classes'] as $key => $class) { 262 | $data = $this->getParsedStructure(); 263 | $data['preferred'] = $class; 264 | $data['current'] = false; 265 | 266 | 267 | $satisfied = false; 268 | 269 | foreach (explode('|', $class) as $className) { 270 | if (class_exists($this->ensureNamespace($className))) { 271 | $satisfied = true; 272 | break; 273 | } 274 | } 275 | 276 | if ($satisfied) { 277 | $data['satisfied'] = true; 278 | $data['current'] = true; 279 | } else { 280 | $this->satisfied = false; 281 | $data['message'] = "Class `{$class}` is not defined"; 282 | $this->errors[] = $data['message']; 283 | } 284 | 285 | $values[$class] = $data; 286 | } 287 | 288 | return $values; 289 | } 290 | 291 | /** 292 | * Validates apache module requirements 293 | * 294 | * @return array 295 | */ 296 | public function validateApacheModuleRequirement() 297 | { 298 | $values = []; 299 | 300 | // Run Only in apache servers 301 | if (!function_exists('apache_get_modules')) { 302 | return $values; 303 | } 304 | 305 | $modules = apache_get_modules(); 306 | 307 | foreach ($this->requirements['apache_modules'] as $key => $module) { 308 | $structure = $this->getParsedStructure(); 309 | $structure['preferred'] = $module; 310 | $structure['current'] = true; 311 | $structure['satisfied'] = true; 312 | 313 | $satisfied = false; 314 | 315 | foreach (explode('|', $module) as $moduleName) { 316 | if (in_array($moduleName, $modules)) { 317 | $satisfied = true; 318 | break; 319 | } 320 | } 321 | 322 | if (!$satisfied) { 323 | $structure['satisfied'] = false; 324 | $structure['current'] = false; 325 | $this->satisfied = false; 326 | $structure['message'] = "Apache module `{$module}` is not loaded"; 327 | $this->errors[] = $structure['message']; 328 | } 329 | 330 | $values[$module] = $structure; 331 | } 332 | 333 | return $values; 334 | } 335 | 336 | /** 337 | * Validates only the functions requirement 338 | * 339 | * @return array 340 | */ 341 | public function validateFunctionRequirement() 342 | { 343 | $values = []; 344 | 345 | foreach ($this->requirements['functions'] as $key => $func) { 346 | $data = $this->getParsedStructure(); 347 | $data['preferred'] = $func; 348 | $data['current'] = false; 349 | 350 | $satisfied = false; 351 | 352 | foreach (explode('|', $func) as $function) { 353 | if (function_exists($function)) { 354 | $satisfied = true; 355 | break; 356 | } 357 | } 358 | 359 | if ($satisfied) { 360 | $data['satisfied'] = true; 361 | $data['current'] = true; 362 | } else { 363 | $this->satisfied = false; 364 | $data['message'] = "PHP function `{$func}()` is not defined"; 365 | $this->errors[] = $data['message']; 366 | } 367 | 368 | $values[$func] = $data; 369 | } 370 | 371 | return $values; 372 | } 373 | 374 | /** 375 | * Validates only the extensions requirement 376 | * 377 | * @return array 378 | */ 379 | public function validateExtensionRequirement() 380 | { 381 | $values = []; 382 | 383 | foreach ($this->requirements['extensions'] as $key => $ext) { 384 | $data = $this->getParsedStructure(); 385 | $data['preferred'] = $ext; 386 | $data['current'] = false; 387 | 388 | $satisfied = false; 389 | 390 | foreach (explode('|', $ext) as $extension) { 391 | if (extension_loaded($extension)) { 392 | $satisfied = true; 393 | break; 394 | } 395 | } 396 | 397 | if ($satisfied) { 398 | $data['satisfied'] = true; 399 | $data['current'] = true; 400 | } else { 401 | $this->satisfied = false; 402 | $data['message'] = "PHP extension {$ext} is not loaded"; 403 | $this->errors[] = $data['message']; 404 | } 405 | 406 | $values[$ext] = $data; 407 | } 408 | 409 | return $values; 410 | } 411 | 412 | /** 413 | * Validate only the system requirements, includes the php version, OS and apache version 414 | * 415 | * @return array 416 | */ 417 | public function validateSystemRequirement() 418 | { 419 | $values = []; 420 | 421 | if ($this->requirements['system']['php_version']) { 422 | $structure = $this->getParsedStructure(); 423 | $structure['current'] = \PHP_VERSION; 424 | $structure['preferred'] = implode(' & ', $this->requirements['system']['php_version']); 425 | 426 | // Check all parts 427 | $result = true; 428 | $count = 0; 429 | foreach ($this->requirements['system']['php_version'] as $php_requirement) 430 | { 431 | $count++; 432 | $parsed = $this->parseComparisonString($php_requirement, '>='); 433 | $result = ($result AND version_compare(\PHP_VERSION, $parsed['plain'], $parsed['operator'])); 434 | } 435 | 436 | $this->satisfied = $result; 437 | 438 | if (!$result) { 439 | $structure['message'] = sprintf( 440 | 'PHP version must %1$s %2$s', 441 | $count == 1 ? 'be '.$this->locale[$parsed['operator']] : 'match', 442 | $structure['preferred'] 443 | ); 444 | $this->errors[] = $structure['message']; 445 | } 446 | 447 | $values['php_version'] = $structure; 448 | } 449 | 450 | 451 | // Now the OS 452 | if ($this->requirements['system']['os']) { 453 | $structureOS = $this->getParsedStructure(); 454 | $os = \DIRECTORY_SEPARATOR === '\\' ? static::OS_DOS : static::OS_UNIX; 455 | $structureOS['satisfied'] = true; 456 | $structureOS['preferred'] = $this->requirements['system']['os']; 457 | $structureOS['current'] = $os; 458 | 459 | if ($os !== $structureOS['preferred']) { 460 | $structureOS['satisfied'] = false; 461 | $this->satisfied = false; 462 | $structureOS['message'] = "The operating system must be {$structureOS['preferred']}, currently we are on a {$os} system"; 463 | $this->errors[] = $structureOS['message']; 464 | } 465 | 466 | $values['os'] = $structureOS; 467 | } 468 | 469 | 470 | return $values; 471 | } 472 | 473 | /** 474 | * Validates only the file requirements 475 | * 476 | * @return array 477 | */ 478 | public function validateFileRequirement() 479 | { 480 | $values = []; 481 | 482 | foreach ($this->requirements['files'] as $file => $checks) { 483 | $structure = $this->getParsedStructure(); 484 | $file = $this->unixPath($file); 485 | $structure['path'] = $file; 486 | $type = 'path'; 487 | $exists = file_exists($file); 488 | 489 | if (is_file($file)) { 490 | $type = 'file'; 491 | } elseif (is_dir($file)) { 492 | $type = 'directory'; 493 | } 494 | 495 | foreach ($checks as $check) { 496 | $data = $structure; 497 | $data['preferred'] = $check; 498 | $data['satisfied'] = (bool) $check($file); 499 | $data['current'] = $data['satisfied']; 500 | 501 | if (!$data['satisfied']) { 502 | $this->satisfied = false; 503 | 504 | switch ($check) { 505 | case static::CHECK_IS_DIR: 506 | $data['message'] = "The path `{$file}` must be a directory"; 507 | 508 | if (!$exists) { 509 | $data['message'] .= ", but the path doesn't even exist"; 510 | } elseif ($type === 'file') { 511 | $data['message'] .= ', but the path is a file'; 512 | } 513 | $data['current'] = "No directory"; 514 | break; 515 | case static::CHECK_IS_FILE: 516 | $data['message'] = "The path `{$file}` must be a file"; 517 | $data['current'] = "No file"; 518 | 519 | if (!$exists) { 520 | $data['message'] .= ", but the path doesn't even exist"; 521 | } elseif ($type === 'directory') { 522 | $data['message'] .= ', but the path is a directory'; 523 | } 524 | break; 525 | case static::CHECK_FILE_EXISTS: 526 | $data['message'] = "The path `{$file}` doesn't exist"; 527 | $data['current'] = "Doesn't exist"; 528 | break; 529 | case static::CHECK_IS_READABLE: 530 | case static::CHECK_IS_WRITABLE: 531 | $humanFriendly = str_replace('is_', '', $check); 532 | $data['current'] = "Not {$humanFriendly}"; 533 | $data['message'] = "The path `{$file}` must be {$humanFriendly}"; 534 | 535 | if (!$exists) { 536 | $data['message'] .= ", but the path doesn't even exist"; 537 | } 538 | break; 539 | } 540 | 541 | $this->errors[] = $data['message']; 542 | } 543 | $values[] = $data; 544 | } 545 | } 546 | 547 | return $values; 548 | } 549 | 550 | /** 551 | * Require a certain OS 552 | * 553 | * @param string $os The OS would pass the test 554 | * Possible values: DOS, WIN 555 | * @return Checker 556 | */ 557 | public function requireOS($os) 558 | { 559 | $this->requirements['system']['os'] = $os; 560 | return $this; 561 | } 562 | 563 | /** 564 | * Require php.ini config values 565 | * 566 | * @param array $values As key => value format 567 | * @return Checker 568 | */ 569 | public function requireIniValues(array $values) 570 | { 571 | foreach ($values as $key => $value) { 572 | $this->requirements['ini_values'][$key] = $value; 573 | } 574 | 575 | return $this; 576 | } 577 | 578 | /** 579 | * Set required PHP version 580 | * 581 | * @param string|array $version The required PHP version(s). Use array to specify a range, e.g. ['>=7.2', '<7.3'] 582 | * @return Checker 583 | */ 584 | public function requirePhpVersion($version) 585 | { 586 | $this->requirements['system']['php_version'] = $version; 587 | return $this; 588 | } 589 | 590 | /** 591 | * Add required extensions 592 | * 593 | * @param array|string $extensions The exact name(s) of the extension as they appear of the phpinfo() page 594 | * @return Checker 595 | */ 596 | public function requirePhpExtensions(array $extensions) 597 | { 598 | foreach ($extensions as $ext) { 599 | $this->requirements['extensions'][$ext] = $ext; 600 | } 601 | 602 | return $this; 603 | } 604 | 605 | /** 606 | * Add required functions(s) 607 | * 608 | * @param array $functions Required function(s) name 609 | * @return Checker 610 | */ 611 | public function requireFunctions(array $functions) 612 | { 613 | foreach ($functions as $func) { 614 | $this->requirements['functions'][$func] = $func; 615 | } 616 | 617 | return $this; 618 | } 619 | 620 | /** 621 | * Add required classe(es) 622 | * 623 | * @param array $classes Required class(es) name 624 | * @return Checker 625 | */ 626 | public function requireClasses(array $classes) 627 | { 628 | foreach ($classes as $class) { 629 | $this->requirements['classes'][$class] = $class; 630 | } 631 | 632 | return $this; 633 | } 634 | 635 | /** 636 | * Require list of apache modules (check will be performed only if server is apache) 637 | * 638 | * @param array $modules 639 | * @return Checker 640 | */ 641 | public function requireApacheModules(array $modules) 642 | { 643 | foreach ($modules as $module) { 644 | $this->requirements['apache_modules'][$module] = $module; 645 | } 646 | 647 | return $this; 648 | } 649 | 650 | /** 651 | * Require a file or folder with appropriate check 652 | * 653 | * @param string $path Path to the file/directory 654 | * @param string $check Any of the supported checks, defaults to file_exists 655 | * @return Checker 656 | */ 657 | public function requireFile($path, $check = self::CHECK_FILE_EXISTS) 658 | { 659 | $supportedChecks = ['is_file', 'is_dir', 'is_readable', 'is_writable', 'file_exists']; 660 | 661 | if (!in_array($check, $supportedChecks)) { 662 | throw new \InvalidArgumentException("No such check is supported!"); 663 | } 664 | 665 | $this->requirements['files'][$path][] = $check; 666 | 667 | return $this; 668 | } 669 | 670 | /** 671 | * Alias of $this->requireFile() for people who are extra concerned about verbosity 672 | * 673 | * @param string $path Path to the file/directory 674 | * @param string $check Any of the supported checks, defaults to file_exists 675 | * @return Checker 676 | */ 677 | public function requireDirectory($path, $check = self::CHECK_FILE_EXISTS) 678 | { 679 | return $this->requireFile($path, $check); 680 | } 681 | 682 | /** 683 | * Remove extensions requirements 684 | * 685 | * @param array $keys extensions to remove as an array 686 | * @return Checker 687 | */ 688 | public function removeExtensionsRequirement(array $keys) 689 | { 690 | foreach ($keys as $key) { 691 | unset($this->requirements['extensions'][$key]); 692 | } 693 | 694 | return $this; 695 | } 696 | 697 | /** 698 | * Remove apache modules requirements 699 | * 700 | * @param array $keys apache modules to remove as an array 701 | * @return Checker 702 | */ 703 | public function removeApacheModulesRequirement(array $keys) 704 | { 705 | foreach ($keys as $key) { 706 | unset($this->requirements['apache_modules'][$key]); 707 | } 708 | 709 | return $this; 710 | } 711 | 712 | /** 713 | * Remove classes requirements 714 | * 715 | * @param array $keys classes to remove as an array 716 | * @return Checker 717 | */ 718 | public function removeClassesRequirement(array $keys) 719 | { 720 | foreach ($keys as $key) { 721 | unset($this->requirements['classes'][$key]); 722 | } 723 | 724 | return $this; 725 | } 726 | 727 | 728 | /** 729 | * Remove functions requirements 730 | * 731 | * @param array $keys functions to remove as an array 732 | * @return Checker 733 | */ 734 | public function removeFunctionsRequirement(array $keys) 735 | { 736 | foreach ($keys as $key) { 737 | unset($this->requirements['functions'][$key]); 738 | } 739 | 740 | return $this; 741 | } 742 | 743 | /** 744 | * Remove INI requirements 745 | * 746 | * @param array $keys Keys as an array 747 | * @return Checker 748 | */ 749 | public function removeIniValuesRequirement(array $keys) 750 | { 751 | foreach ($keys as $key) { 752 | unset($this->requirements['ini_values'][$key]); 753 | } 754 | 755 | return $this; 756 | } 757 | 758 | /** 759 | * Remove a file/directory requirement 760 | * 761 | * @param string $path The file path that you added earlier using self::requireFile() 762 | * @param string|null The check name to remove, set this to null/empty and 763 | * the entire path will be removed with all the checks. 764 | * @return Checker 765 | */ 766 | public function removeFilesRequirement($path, $check = null) 767 | { 768 | // Make sure the path is added 769 | if (!isset($this->requirements['files'][$path])) { 770 | return $this; 771 | } 772 | 773 | // No check passed so just remove the file entirely 774 | if (!$check) { 775 | unset($this->requirements['files'][$path]); 776 | return $this; 777 | } 778 | 779 | $newVals = $this->arrayRemoveValue($this->requirements['files'][$path], [$check]); 780 | 781 | $this->requirements['files'][$path] = $newVals; 782 | 783 | return $this; 784 | } 785 | 786 | /** 787 | * Remove currently set OS requirements 788 | * 789 | * @return Checker 790 | */ 791 | public function removeOsRequirement() 792 | { 793 | $this->requirements['system']['os'] = null; 794 | return $this; 795 | } 796 | 797 | /** 798 | * Remove PHP version requirement 799 | * 800 | * @return Checker 801 | */ 802 | public function removePhpVersionRequirement() 803 | { 804 | $this->requirements['system']['php_version'] = null; 805 | 806 | return $this; 807 | } 808 | 809 | /** 810 | * Remove specific value from one-dimensional array 811 | * 812 | * @param array $array The array to remove values from 813 | * @param array $removals List of values to remove 814 | * 815 | * @return array 816 | */ 817 | public function arrayRemoveValue(array $array, array $removals) 818 | { 819 | return array_diff($array, $removals); 820 | } 821 | 822 | /** 823 | * Transform any file path to unix style path 824 | * 825 | * @param string $string 826 | * @return string 827 | */ 828 | public function unixPath($string) 829 | { 830 | return str_replace('\\', '/', $string); 831 | } 832 | 833 | /** 834 | * Checks if a string is PHP style namespaced using backslash or not 835 | * 836 | * @param string $string 837 | * @return boolean 838 | */ 839 | public function ensureNamespace($string) 840 | { 841 | // Already namespaced 842 | if (strpos($string, '\\') !== false) { 843 | return $string; 844 | } 845 | 846 | // Append a global namespace 847 | return '\\' . $string; 848 | } 849 | 850 | /** 851 | * Parse a string that has comparison operator prepended to it 852 | * 853 | * @param string $string 854 | * @param string $default 855 | * @return array As operator => the comparison operator if exists 856 | * plain => the string without the operator 857 | **/ 858 | public function parseComparisonString($string, $default) 859 | { 860 | $comparison = substr($string, 0, 1); 861 | $comparison2 = substr($string, 0, 2); 862 | 863 | $operator = $default; 864 | $withoutVersion = $string; 865 | 866 | if ($comparison2 == '>=' || $comparison2 == '<=') { 867 | $operator = $comparison2; 868 | $withoutVersion = substr($string, 2); 869 | } elseif ($comparison == '>' || $comparison == '<' || $comparison == '=') { 870 | $operator = $comparison; 871 | $withoutVersion = substr($string, 1); 872 | } 873 | 874 | return [ 875 | 'operator' => $operator, 876 | 'plain' => $withoutVersion, 877 | ]; 878 | } 879 | 880 | /** 881 | * Loosely compare two variables with the comparison operator provided 882 | * 883 | * @param mixed $var1 884 | * @param string $op 885 | * @param mixed $var2 886 | * @return boolean 887 | */ 888 | public function looseComparison($var1, $op, $var2) 889 | { 890 | switch ($op) { 891 | case "=": 892 | return $var1 == $var2; 893 | case "!=": 894 | return $var1 != $var2; 895 | case ">=": 896 | return $var1 >= $var2; 897 | case "<=": 898 | return $var1 <= $var2; 899 | case ">": 900 | return $var1 > $var2; 901 | case "<": 902 | return $var1 < $var2; 903 | default: 904 | return true; 905 | } 906 | } 907 | 908 | /** 909 | * Returns bytes from php.ini string values 910 | * 911 | * @param string $val 912 | * @return integer 913 | */ 914 | public function returnBytes($val) 915 | { 916 | $val = strtolower(trim($val)); 917 | if (substr($val, -1) == 'b') { 918 | $val = substr($val, 0, -1); 919 | } 920 | $last = substr($val, -1); 921 | 922 | $val = intval($val); 923 | switch ($last) { 924 | case 'g': 925 | case 'gb': 926 | $val *= 1024; 927 | // no break 928 | case 'm': 929 | case 'mb': 930 | $val *= 1024; 931 | // no break 932 | case 'k': 933 | case 'kb': 934 | $val *= 1024; 935 | } 936 | 937 | return $val; 938 | } 939 | 940 | /** 941 | * Returns base structure for the values 942 | * 943 | * @return array 944 | */ 945 | protected function getParsedStructure() 946 | { 947 | return [ 948 | 'satisfied' => false, 949 | 'preferred' => null, 950 | 'current' => null, 951 | 'message' => null, 952 | ]; 953 | } 954 | } 955 | -------------------------------------------------------------------------------- /tests/ChekerTest.php: -------------------------------------------------------------------------------- 1 | = 9 | */ 10 | class CheckerTest extends \PHPUnit_Framework_TestCase 11 | { 12 | protected $checker; 13 | 14 | public function setUp() 15 | { 16 | $this->checker = new Checker; 17 | } 18 | 19 | public function tearDown() 20 | { 21 | $this->checker = null; 22 | } 23 | 24 | public function testOSFail() 25 | { 26 | $this->checker->resetRequirements(); 27 | 28 | if (DIRECTORY_SEPARATOR === '/') { 29 | $os = $this->checker::OS_DOS; 30 | } else { 31 | $os = $this->checker::OS_UNIX; 32 | } 33 | 34 | $this->checker 35 | ->requireOS($os) 36 | ->check(); 37 | 38 | $this->assertEquals(false, $this->checker->isSatisfied()); 39 | } 40 | 41 | public function testOSPass() 42 | { 43 | $this->checker->resetRequirements(); 44 | 45 | if (DIRECTORY_SEPARATOR === '/') { 46 | $os = $this->checker::OS_UNIX; 47 | } else { 48 | $os = $this->checker::OS_DOS; 49 | } 50 | 51 | $this->checker 52 | ->requireOS($os) 53 | ->check(); 54 | 55 | $this->assertEquals(true, $this->checker->isSatisfied()); 56 | } 57 | 58 | public function testFunctionsPass() 59 | { 60 | $this->checker->resetRequirements(); 61 | 62 | $this->checker 63 | ->requireFunctions(['sprintf', 'strtolower', 'ucfirst|ucwords']) 64 | ->check(); 65 | 66 | $this->assertEquals(true, $this->checker->isSatisfied()); 67 | } 68 | 69 | public function testFunctionsFail() 70 | { 71 | $this->checker->resetRequirements(); 72 | 73 | $this->checker 74 | ->requireFunctions([rand() . uniqid(), uniqid() . rand()]) 75 | ->check(); 76 | 77 | $this->assertEquals(false, $this->checker->isSatisfied()); 78 | } 79 | 80 | public function testClassesPass() 81 | { 82 | $this->checker->resetRequirements(); 83 | 84 | $this->checker 85 | ->requireClasses(['Directory', 'Exception|RuntimeException']) 86 | ->check(); 87 | 88 | $this->assertEquals(true, $this->checker->isSatisfied()); 89 | } 90 | 91 | public function testClassesFail() 92 | { 93 | $this->checker->resetRequirements(); 94 | 95 | $this->checker 96 | ->requireFunctions([rand() . uniqid(), uniqid() . rand()]) 97 | ->check(); 98 | 99 | $this->assertEquals(false, $this->checker->isSatisfied()); 100 | } 101 | 102 | public function testUnlimitedIniValuePass() 103 | { 104 | ini_set('memory_limit', -1); 105 | 106 | $this->checker->resetRequirements(); 107 | $this->checker->requireIniValues([ 108 | 'memory_limit' => '>=64M', 109 | ]); 110 | 111 | $this->assertEquals(true, $this->checker->isSatisfied()); 112 | } 113 | 114 | 115 | /** 116 | * @dataProvider phpVersionPassData 117 | */ 118 | public function testPHPVersionPass($version) 119 | { 120 | $this->checker->resetRequirements(); 121 | 122 | $this->checker 123 | ->requirePhpVersion($version) 124 | ->check(); 125 | 126 | $this->assertEquals(true, $this->checker->isSatisfied()); 127 | } 128 | 129 | /** 130 | * @dataProvider phpVersionFailData 131 | */ 132 | public function testPHPVersionFail($version) 133 | { 134 | $this->checker->resetRequirements(); 135 | 136 | $this->checker 137 | ->requirePhpVersion($version) 138 | ->check(); 139 | 140 | $this->assertEquals(false, $this->checker->isSatisfied()); 141 | } 142 | 143 | public function testIniPass() 144 | { 145 | $this->checker->resetRequirements(); 146 | 147 | $setting = ini_get('file_uploads'); 148 | if ($setting == 'On' || $setting == '1') { 149 | $setting = true; 150 | } elseif ($setting == 'Off' || $setting == '' || $setting == '0') { 151 | $setting = false; 152 | } 153 | 154 | $this->checker 155 | ->requireIniValues([ 156 | 'file_uploads' => $setting 157 | ]) 158 | ->check(); 159 | 160 | 161 | $this->assertEquals(true, $this->checker->isSatisfied()); 162 | } 163 | 164 | public function testIniFail() 165 | { 166 | $this->checker->resetRequirements(); 167 | 168 | $setting = ini_get('file_uploads'); 169 | if ($setting == 'On' || $setting == '1') { 170 | $setting = true; 171 | } elseif ($setting == 'Off' || $setting == '' || $setting == '0') { 172 | $setting = false; 173 | } 174 | 175 | $this->checker 176 | ->requireIniValues([ 177 | 'file_uploads' => !$setting 178 | ]) 179 | ->check(); 180 | 181 | 182 | $this->assertEquals(false, $this->checker->isSatisfied()); 183 | } 184 | 185 | public function phpVersionFailData() 186 | { 187 | $phpversion = (float) phpversion(); 188 | 189 | // Higher than current 190 | $data[] = $phpversion + 1; 191 | // Lower than current 192 | $data[] = $phpversion - 1; 193 | 194 | // comparison operators 195 | $data[] = '>' . ($phpversion + 1); 196 | $data[] = '>=' . ($phpversion + 2); 197 | $data[] = '<' . $phpversion; 198 | $data[] = '>=' . $phpversion; 199 | $data[] = '=<' . ($phpversion - 1); 200 | 201 | return [$data]; 202 | } 203 | 204 | public function phpVersionPassData() 205 | { 206 | $phpversion = (float) phpversion(); 207 | 208 | $data[] = '=' . phpversion(); 209 | 210 | // comparison operators 211 | $data[] = '>' . ($phpversion - 1); 212 | $data[] = '>=' . $phpversion; 213 | $data[] = '<' . ($phpversion + 1); 214 | $data[] = '>=' . $phpversion; 215 | $data[] = '=<' . ($phpversion + 1); 216 | 217 | return [$data]; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /usage/usage.php: -------------------------------------------------------------------------------- 1 | requirePhpVersion('>=5.6') 18 | ->requirePhpExtensions(['pdo', 'mbstring']) 19 | ->requireFunctions(['random_bytes']) 20 | ->requireFile('../composer.json', Checker::CHECK_FILE_EXISTS) 21 | ->requireDirectory('../src', Checker::CHECK_IS_READABLE) 22 | ->requireIniValues([ 23 | 'allow_url_fopen' => true, 24 | 'short_open_tag' => true, 25 | 'memory_limit' => '>=64M', 26 | ]); 27 | 28 | // Runs the check and returns parsed requirements as an array 29 | // Contains parsed requirements with state of the current values and their comparison result 30 | $output = $checker->check(); 31 | 32 | 33 | // Should be called after running check() to see if requirements has met or not 34 | $satisfied = $checker->isSatisfied(); 35 | 36 | if ($satisfied) { 37 | echo "Requirements are met."; 38 | } else { 39 | echo join(', ', $checker->getErrors()); 40 | } 41 | --------------------------------------------------------------------------------