diff --git a/.gitignore b/.gitignore index c9dec9a..d540119 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ /vendor/ .php_cs.cache composer.lock +.devcontainer +.php-cs-fixer.cache +.phpunit.result.cache diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..19600b5 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,80 @@ +in(__DIR__); + +return (new PhpCsFixer\Config()) + ->setRules([ + '@PSR12' => true, + 'array_syntax' => [ + 'syntax' => 'short', + ], + 'binary_operator_spaces' => [ + 'operators' => [ + '=>' => 'align', + '=' => 'align', + ], + ], + 'concat_space' => [ + 'spacing' => 'one', + ], + 'declare_strict_types' => true, + 'type_declaration_spaces' => true, + 'single_line_comment_style' => [ + 'comment_types' => ['hash'], + ], + 'lowercase_cast' => true, + 'class_attributes_separation' => [ + 'elements' => ['method' => 'one', 'property' => 'one', 'const' => 'one'], + ], + 'native_function_casing' => true, + 'new_with_parentheses' => true, + 'no_alias_functions' => true, + 'no_blank_lines_after_class_opening' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_empty_comment' => true, + 'no_empty_phpdoc' => true, + 'no_empty_statement' => true, + 'no_extra_blank_lines' => true, + 'no_leading_import_slash' => true, + 'no_leading_namespace_whitespace' => true, + 'no_multiline_whitespace_around_double_arrow' => true, + 'multiline_whitespace_before_semicolons' => false, + 'no_short_bool_cast' => true, + 'no_singleline_whitespace_before_semicolons' => true, + 'no_spaces_around_offset' => true, + 'no_trailing_comma_in_singleline' => true, + 'no_unreachable_default_argument_value' => true, + 'no_unused_imports' => true, + 'no_useless_else' => true, + 'no_useless_return' => true, + 'no_whitespace_before_comma_in_array' => true, + 'object_operator_without_whitespace' => true, + 'ordered_imports' => true, + 'phpdoc_align' => true, + 'phpdoc_indent' => true, + 'general_phpdoc_tag_rename' => true, + 'phpdoc_inline_tag_normalizer' => true, + 'phpdoc_tag_type' => true, + 'phpdoc_order' => true, + 'phpdoc_scalar' => true, + 'phpdoc_separation' => true, + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_summary' => true, + 'phpdoc_to_comment' => true, + 'phpdoc_trim' => true, + 'phpdoc_types' => true, + 'self_accessor' => true, + 'short_scalar_cast' => true, + 'single_quote' => true, + 'space_after_semicolon' => true, + 'standardize_not_equals' => true, + 'trailing_comma_in_multiline' => [ + 'elements' => ['arrays'], + ], + 'trim_array_spaces' => true, + 'unary_operator_spaces' => true, + 'whitespace_after_comma_in_array' => true, + ]) + ->setRiskyAllowed(true) + ->setUsingCache(true) + ->setFinder($finder); diff --git a/.php_cs b/.php_cs deleted file mode 100644 index 75675d6..0000000 --- a/.php_cs +++ /dev/null @@ -1,19 +0,0 @@ -in(__DIR__); - -return Symfony\CS\Config\Config::create() - ->level(Symfony\CS\FixerInterface::PSR2_LEVEL) - ->fixers([ - 'unused_use', - 'new_with_braces', - 'phpdoc_scalar', - 'phpdoc_params', - 'multiline_array_trailing_comma', - 'phpdoc_trim', - 'return', - ]) - ->setUsingCache(true) - ->setUsingLinter(true) - ->finder($finder) -; diff --git a/.travis.yml b/.travis.yml index 5a289b0..8b35064 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,10 @@ language: php php: - - 5.6 - - 7.0 - - hhvm + - 8.1 + - 8.2 + - 8.3 + - 8.4 cache: directories: @@ -20,6 +21,4 @@ script: - composer test matrix: - fast_finish: true - allow_failures: - - php: hhvm + fast_finish: true \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c86b990..67db6e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [2.0.0] - 2025-08-13 +### Changed + * Minimum supported PHP version is now `8.1`. + ## [1.0.0] - 2016-09-09 ### Changed * Moved `TomPHP\ExceptionConstructorTools\ExceptionConstructorTools` to diff --git a/composer.json b/composer.json index a106242..505bdac 100644 --- a/composer.json +++ b/composer.json @@ -14,12 +14,14 @@ } ], "require": { - "php": "^5.6|^7.0" + "php": ">=8.1" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^1.10", - "phpunit/phpunit": "^5.5", - "squizlabs/php_codesniffer": "*" + "friendsofphp/php-cs-fixer": "^3.85", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^9.6 || ^7.5 || ^11.5", + "rector/rector": "^2.1", + "squizlabs/php_codesniffer": "^3.9" }, "autoload": { "psr-4": { @@ -44,8 +46,18 @@ "phpcs --standard=psr2 src tests", "php-cs-fixer fix --dry-run --verbose" ], + "analyse": [ + "phpstan analyse --memory-limit=1G" + ], + "rector": [ + "rector process src tests --ansi" + ], + "rector:dry": [ + "rector process src tests --dry-run --ansi" + ], "test": [ "@cs:check", + "@analyse", "phpunit --colors=always" ] } diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..a8ef0a1 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,8 @@ +parameters: + level: max + + paths: + - src/ + - tests/ + + treatPhpDocTypesAsCertain: false \ No newline at end of file diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..3e3ae3f --- /dev/null +++ b/rector.php @@ -0,0 +1,34 @@ +withPaths([ + __DIR__ . '/src', + __DIR__ . '/tests', + ]) + // Ensure Rector knows we are targeting PHP 8.1+ syntax and features + ->withPhpSets(php81: true) + ->withPreparedSets( + typeDeclarations: true, // Add param/return/var types where possible + earlyReturn: true, // Replace nested ifs with early returns + strictBooleans: true, // Use strict bool checks + phpunitCodeQuality: true, // Modernise PHPUnit usage + deadCode: true // Remove unused code, vars, imports, etc. + ) + ->withSets([ + LevelSetList::UP_TO_PHP_81, // Enforces all rules up to PHP 8.1 + SetList::CODE_QUALITY, + SetList::CODING_STYLE, + SetList::DEAD_CODE, + SetList::NAMING, // Enforce clear & consistent naming + SetList::TYPE_DECLARATION, // Add missing type declarations aggressively + SetList::PRIVATIZATION, // Make props/methods private if possible + SetList::STRICT_BOOLEANS, // Strict boolean expressions + ])->withPHPStanConfigs([ + __DIR__ . '/phpstan.neon.dist', + ]); diff --git a/src/ExceptionConstructorTools.php b/src/ExceptionConstructorTools.php index 987e7db..0446973 100644 --- a/src/ExceptionConstructorTools.php +++ b/src/ExceptionConstructorTools.php @@ -1,5 +1,7 @@ $params The sprintf parameters for the message. + * @param int $code Numeric exception code. + * @param \Exception $exception The previous exception. */ protected static function create( - $message, + string $message, array $params = [], - $code = 0, - \Exception $previous = null - ) { - return new static(sprintf($message, ...$params), $code, $previous); + int $code = 0, + ?\Exception $exception = null + ): static { + $class = static::class; + $reflectionClass = new \ReflectionClass($class); + return $reflectionClass->newInstance(sprintf($message, ...$params), $code, $exception); } /** * Returns a string representation of the type of a variable. - * - * @param mixed $variable - * - * @return string */ - protected static function typeToString($variable) + protected static function typeToString(mixed $variable): string { return is_object($variable) - ? get_class($variable) + ? $variable::class : '[' . gettype($variable) . ']'; } /** * Returns a string representation of the value. - * - * @param mixed $value - * - * @return string */ - protected static function valueToString($value) + protected static function valueToString(mixed $value): string { - switch (gettype($value)) { - case 'string': - return '"' . addslashes($value) . '"'; - case 'boolean': - return $value ? 'true' : 'false'; - default: - return $value; + if (!is_string($value) + && !is_numeric($value) + && !is_bool($value) + && !$value instanceof \Stringable + ) { + throw new \InvalidArgumentException("Value isn't stringable"); } + + return match (gettype($value)) { + 'string' => '"' . addslashes($value) . '"', + 'boolean' => $value ? 'true' : 'false', + default => (string) $value, + }; } /** * Returns the list as a formatted string. * - * @param string[] $list - * - * @return string + * @param array $list */ - protected static function listToString(array $list) + protected static function listToString(array $list): string { - if (empty($list)) { + if ($list === []) { return '[]'; } - $list = array_map(['static', 'valueToString'], $list); + $list = array_map(fn ($item): string => static::valueToString($item), $list); return '[' . implode(', ', $list) . ']'; } diff --git a/tests/support/ExampleException.php b/tests/support/ExampleException.php index 656eca9..339831b 100644 --- a/tests/support/ExampleException.php +++ b/tests/support/ExampleException.php @@ -1,5 +1,7 @@ |bool|float|int|string|null $param + */ + public static function fromFormatString(string $format, array|bool|float|int|string|null $param): static { - return self::create($format, [$param]); + if (!is_array($param)) { + $param = [$param]; + } + + return self::create($format, $param); } - public static function fromCode($code) + public static function fromCode(int $code): static { return self::create('', [], $code); } - public static function fromPreviousException($previous) + public static function fromPreviousException(?\Exception $exception): static { - return self::create('', [], 0, $previous); + return self::create('', [], 0, $exception); } - public static function withTypeInMessage($param) + public static function withTypeInMessage(mixed $param): static { return self::create(self::typeToString($param)); } - public static function withValueInMessage($value) + public static function withValueInMessage(mixed $value): static { return self::create(self::valueToString($value)); } - public static function withListInMessage($param) + /** + * @param array $param + */ + public static function withListInMessage(array $param): static { return self::create(self::listToString($param)); } diff --git a/tests/support/ExampleExtendedException.php b/tests/support/ExampleExtendedException.php index 91a5697..3dfbbd1 100644 --- a/tests/support/ExampleExtendedException.php +++ b/tests/support/ExampleExtendedException.php @@ -1,5 +1,7 @@ assertSame('example message', $exception->getMessage()); + $this->assertSame('example message', $exampleException->getMessage()); } - public function testItCanAddAnExceptionCode() + public function testItCanAddAnExceptionCode(): void { - $exception = ExampleException::fromCode(909); + $exampleException = ExampleException::fromCode(909); - $this->assertSame(909, $exception->getCode()); + $this->assertSame(909, $exampleException->getCode()); } - public function testItCanAddAPreviousException() + public function testItCanAddAPreviousException(): void { - $previous = new \RuntimeException(); + $runtimeException = new \RuntimeException(); - $exception = ExampleException::fromPreviousException($previous); + $exampleException = ExampleException::fromPreviousException($runtimeException); - $this->assertSame($previous, $exception->getPrevious()); + $this->assertSame($runtimeException, $exampleException->getPrevious()); } - public function testItUsesLateStaticBindings() + public function testItUsesLateStaticBindings(): void { - $exception = ExampleExtendedException::fromFormatString('', []); + $exampleExtendedException = ExampleExtendedException::fromFormatString('', []); - $this->assertInstanceOf('tests\support\ExampleExtendedException', $exception); + $this->assertInstanceOf('tests\support\ExampleExtendedException', $exampleExtendedException); } - public function testItConvertsABuiltInTypeToAMessage() + public function testItConvertsABuiltInTypeToAMessage(): void { - $exception = ExampleException::withTypeInMessage(99); + $exampleException = ExampleException::withTypeInMessage(99); - $this->assertSame('[integer]', $exception->getMessage()); + $this->assertSame('[integer]', $exampleException->getMessage()); } - public function testItConvertsAnObjectToAClassNameMessage() + public function testItConvertsAnObjectToAClassNameMessage(): void { - $exception = ExampleException::withTypeInMessage(new \stdClass()); + $exampleException = ExampleException::withTypeInMessage(new \stdClass()); - $this->assertSame('stdClass', $exception->getMessage()); + $this->assertSame('stdClass', $exampleException->getMessage()); } - public function testItConvertsAStringValueToAMessage() + public function testItConvertsAStringValueToAMessage(): void { - $exception = ExampleException::withValueInMessage('value'); + $exampleException = ExampleException::withValueInMessage('value'); - $this->assertSame('"value"', $exception->getMessage()); + $this->assertSame('"value"', $exampleException->getMessage()); } - public function testItConvertsAStringWithQuotesValueToAMessage() + public function testItConvertsAStringWithQuotesValueToAMessage(): void { - $exception = ExampleException::withValueInMessage('"value"'); + $exampleException = ExampleException::withValueInMessage('"value"'); - $this->assertSame('"\"value\""', $exception->getMessage()); + $this->assertSame('"\"value\""', $exampleException->getMessage()); } - public function testItConvertsATrueValueToAMessage() + public function testItConvertsATrueValueToAMessage(): void { - $exception = ExampleException::withValueInMessage(true); + $exampleException = ExampleException::withValueInMessage(true); - $this->assertSame('true', $exception->getMessage()); + $this->assertSame('true', $exampleException->getMessage()); } - public function testItConvertsAFalseValueToAMessage() + public function testItConvertsAFalseValueToAMessage(): void { - $exception = ExampleException::withValueInMessage(false); + $exampleException = ExampleException::withValueInMessage(false); - $this->assertSame('false', $exception->getMessage()); + $this->assertSame('false', $exampleException->getMessage()); } - public function testItConvertsAnIntValueToAMessage() + public function testItConvertsAnIntValueToAMessage(): void { - $exception = ExampleException::withValueInMessage(12); + $exampleException = ExampleException::withValueInMessage(12); - $this->assertSame('12', $exception->getMessage()); + $this->assertSame('12', $exampleException->getMessage()); } - public function testItConvertsAListOfStringsToAMessage() + public function testItConvertsAListOfStringsToAMessage(): void { - $exception = ExampleException::withListInMessage(['a', 'b', 'c']); + $exampleException = ExampleException::withListInMessage(['a', 'b', 'c']); - $this->assertSame('["a", "b", "c"]', $exception->getMessage()); + $this->assertSame('["a", "b", "c"]', $exampleException->getMessage()); } - public function testItConvertsAnEmptyListToAMessage() + public function testItConvertsAnEmptyListToAMessage(): void { - $exception = ExampleException::withListInMessage([]); + $exampleException = ExampleException::withListInMessage([]); - $this->assertSame('[]', $exception->getMessage()); + $this->assertSame('[]', $exampleException->getMessage()); } - public function testItConvertsAListOfIntsToAMessage() + public function testItConvertsAListOfIntsToAMessage(): void { - $exception = ExampleException::withListInMessage([1, 2, 3]); + $exampleException = ExampleException::withListInMessage([1, 2, 3]); - $this->assertSame('[1, 2, 3]', $exception->getMessage()); + $this->assertSame('[1, 2, 3]', $exampleException->getMessage()); } }