diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 05e7b40..3584e6c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,4 +31,4 @@ jobs: os: >- ['ubuntu-latest', 'windows-latest'] php: >- - ['8.1', '8.2', '8.3'] + ['8.1', '8.2', '8.3', '8.4'] diff --git a/.github/workflows/composer-require-checker.yml b/.github/workflows/composer-require-checker.yml index a857bce..a93390b 100644 --- a/.github/workflows/composer-require-checker.yml +++ b/.github/workflows/composer-require-checker.yml @@ -31,4 +31,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.1', '8.2', '8.3'] + ['8.1', '8.2', '8.3', '8.4'] diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml index 457772a..5d6931d 100644 --- a/.github/workflows/rector.yml +++ b/.github/workflows/rector.yml @@ -21,4 +21,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.3'] + ['8.4'] diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index e33eca8..d03874d 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -29,4 +29,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.1', '8.2', '8.3'] + ['8.1', '8.2', '8.3', '8.4'] diff --git a/CHANGELOG.md b/CHANGELOG.md index d294eec..1ccf578 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,9 @@ class (@olegbaturin) - Chg #137: Add separate parameters for each of `HtmlRenderer` settings in constructor. Mark `$settings` parameter as deprecated (@vjik) -- Enh #138: Raise the minimum PHP version to 8.1 and minor refactoring (@vjik) +- Enh #138, #139: Raise the minimum PHP version to 8.1 and minor refactoring (@vjik) +- Chg #139: Change PHP constraint in `composer.json` to `~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0` (@vjik) +- Bug #139: Explicitly mark nullable parameters (@vjik) ## 3.3.0 July 11, 2024 diff --git a/composer.json b/composer.json index 02d53c4..0ec520e 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ } ], "require": { - "php": "^8.1", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", "ext-dom": "*", "ext-mbstring": "*", "alexkart/curl-builder": "^1.0", @@ -47,14 +47,14 @@ "yiisoft/injector": "^1.0" }, "require-dev": { + "bamarni/composer-bin-plugin": "^1.8", "httpsoft/http-message": "^1.1.6", - "maglnet/composer-require-checker": "^4.7.1", "phpunit/phpunit": "^10.5.44", "psr/event-dispatcher": "^1.0", "rector/rector": "^2.0.7", "roave/infection-static-analysis-plugin": "^1.35", "spatie/phpunit-watcher": "^1.24", - "vimeo/psalm": "^5.26.1|^6", + "vimeo/psalm": "^5.26.1|^6.2", "yiisoft/di": "^1.3", "yiisoft/test-support": "^3.0.1" }, @@ -69,6 +69,11 @@ } }, "extra": { + "bamarni-bin": { + "bin-links": true, + "target-directory": "tools", + "forward-command": true + }, "config-plugin-options": { "source-directory": "config" }, @@ -80,8 +85,9 @@ "sort-packages": true, "bump-after-update": "dev", "allow-plugins": { - "infection/extension-installer": true, - "composer/package-versions-deprecated": true + "bamarni/composer-bin-plugin": true, + "composer/package-versions-deprecated": true, + "infection/extension-installer": true } }, "scripts": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ea84752..22ae7b7 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -9,6 +9,7 @@ executionOrder="random" failOnRisky="true" failOnWarning="true" + failOnDeprecation="true" stopOnFailure="false" colors="true" displayDetailsOnPhpunitDeprecations="true" diff --git a/rector.php b/rector.php index 75ff290..6589abb 100644 --- a/rector.php +++ b/rector.php @@ -6,6 +6,7 @@ use Rector\Config\RectorConfig; use Rector\Php71\Rector\FuncCall\RemoveExtraParametersRector; use Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector; +use Rector\Php81\Rector\ClassMethod\NewInInitializerRector; use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector; return RectorConfig::configure() @@ -21,4 +22,5 @@ ClosureToArrowFunctionRector::class, NullToStrictStringFuncCallArgRector::class, RemoveExtraParametersRector::class, + NewInInitializerRector::class, ]); diff --git a/src/ErrorHandler.php b/src/ErrorHandler.php index b3d9ae0..f9c4aa0 100644 --- a/src/ErrorHandler.php +++ b/src/ErrorHandler.php @@ -62,8 +62,8 @@ public function __construct( */ public function handle( Throwable $t, - ThrowableRendererInterface $renderer = null, - ServerRequestInterface $request = null + ?ThrowableRendererInterface $renderer = null, + ?ServerRequestInterface $request = null ): ErrorData { $renderer ??= $this->defaultRenderer; @@ -192,6 +192,9 @@ private function initializeOnce(): void }); if (!(PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg')) { + /** + * @var string + */ $this->workingDirectory = getcwd(); } diff --git a/src/Exception/ErrorException.php b/src/Exception/ErrorException.php index d2b98f1..11c1465 100644 --- a/src/Exception/ErrorException.php +++ b/src/Exception/ErrorException.php @@ -33,14 +33,14 @@ class ErrorException extends \ErrorException implements FriendlyExceptionInterfa E_USER_ERROR => 'PHP User Error', E_USER_WARNING => 'PHP User Warning', E_USER_NOTICE => 'PHP User Notice', - E_STRICT => 'PHP Strict Warning', + 2048 => 'PHP Strict Warning', // E_STRICT E_RECOVERABLE_ERROR => 'PHP Recoverable Error', E_DEPRECATED => 'PHP Deprecated Warning', E_USER_DEPRECATED => 'PHP User Deprecated Warning', ]; /** @psalm-param DebugBacktraceType $backtrace */ - public function __construct(string $message = '', int $code = 0, int $severity = 1, string $filename = __FILE__, int $line = __LINE__, Exception $previous = null, private readonly array $backtrace = []) + public function __construct(string $message = '', int $code = 0, int $severity = 1, string $filename = __FILE__, int $line = __LINE__, ?Exception $previous = null, private readonly array $backtrace = []) { parent::__construct($message, $code, $severity, $filename, $line, $previous); $this->addXDebugTraceToFatalIfAvailable(); @@ -149,6 +149,6 @@ private function isXdebugStackAvailable(): bool } // Xdebug 3 and later, proper mode is required - return str_contains(ini_get('xdebug.mode'), 'develop'); + return str_contains((string) \ini_get('xdebug.mode'), 'develop'); } } diff --git a/src/Factory/ThrowableResponseFactory.php b/src/Factory/ThrowableResponseFactory.php index dd9db73..6daf9ac 100644 --- a/src/Factory/ThrowableResponseFactory.php +++ b/src/Factory/ThrowableResponseFactory.php @@ -53,10 +53,10 @@ final class ThrowableResponseFactory implements ThrowableResponseFactoryInterfac private ?string $contentType = null; public function __construct( - private ResponseFactoryInterface $responseFactory, - private ErrorHandler $errorHandler, - private ContainerInterface $container, - HeadersProvider $headersProvider = null, + private readonly ResponseFactoryInterface $responseFactory, + private readonly ErrorHandler $errorHandler, + private readonly ContainerInterface $container, + ?HeadersProvider $headersProvider = null, ) { $this->headersProvider = $headersProvider ?? new HeadersProvider(); } diff --git a/src/Renderer/HeaderRenderer.php b/src/Renderer/HeaderRenderer.php index 0270024..385551b 100644 --- a/src/Renderer/HeaderRenderer.php +++ b/src/Renderer/HeaderRenderer.php @@ -14,12 +14,12 @@ */ final class HeaderRenderer implements ThrowableRendererInterface { - public function render(Throwable $t, ServerRequestInterface $request = null): ErrorData + public function render(Throwable $t, ?ServerRequestInterface $request = null): ErrorData { return new ErrorData('', ['X-Error-Message' => self::DEFAULT_ERROR_MESSAGE]); } - public function renderVerbose(Throwable $t, ServerRequestInterface $request = null): ErrorData + public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData { return new ErrorData('', [ 'X-Error-Type' => $t::class, diff --git a/src/Renderer/HtmlRenderer.php b/src/Renderer/HtmlRenderer.php index 9449187..9e8fc75 100644 --- a/src/Renderer/HtmlRenderer.php +++ b/src/Renderer/HtmlRenderer.php @@ -156,7 +156,7 @@ public function __construct( ?? null; } - public function render(Throwable $t, ServerRequestInterface $request = null): ErrorData + public function render(Throwable $t, ?ServerRequestInterface $request = null): ErrorData { return new ErrorData($this->renderTemplate($this->template, [ 'request' => $request, @@ -164,7 +164,7 @@ public function render(Throwable $t, ServerRequestInterface $request = null): Er ])); } - public function renderVerbose(Throwable $t, ServerRequestInterface $request = null): ErrorData + public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData { return new ErrorData($this->renderTemplate($this->verboseTemplate, [ 'request' => $request, @@ -530,7 +530,7 @@ private function renderTemplate(string $path, array $parameters): string try { /** @psalm-suppress PossiblyNullFunctionCall */ $renderer->bindTo($this)($path, $parameters); - return ob_get_clean(); + return (string) ob_get_clean(); } catch (Throwable $e) { while (ob_get_level() > $obInitialLevel) { if (!@ob_end_clean()) { diff --git a/src/Renderer/JsonRenderer.php b/src/Renderer/JsonRenderer.php index ef53296..028084e 100644 --- a/src/Renderer/JsonRenderer.php +++ b/src/Renderer/JsonRenderer.php @@ -16,7 +16,7 @@ */ final class JsonRenderer implements ThrowableRendererInterface { - public function render(Throwable $t, ServerRequestInterface $request = null): ErrorData + public function render(Throwable $t, ?ServerRequestInterface $request = null): ErrorData { return new ErrorData( json_encode( @@ -28,7 +28,7 @@ public function render(Throwable $t, ServerRequestInterface $request = null): Er ); } - public function renderVerbose(Throwable $t, ServerRequestInterface $request = null): ErrorData + public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData { return new ErrorData( json_encode( @@ -40,7 +40,7 @@ public function renderVerbose(Throwable $t, ServerRequestInterface $request = nu 'line' => $t->getLine(), 'trace' => $t->getTrace(), ], - JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE | JSON_PARTIAL_OUTPUT_ON_ERROR + JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE | JSON_PARTIAL_OUTPUT_ON_ERROR ) ); } diff --git a/src/Renderer/PlainTextRenderer.php b/src/Renderer/PlainTextRenderer.php index 76fc39a..1f9ddd5 100644 --- a/src/Renderer/PlainTextRenderer.php +++ b/src/Renderer/PlainTextRenderer.php @@ -14,12 +14,12 @@ */ final class PlainTextRenderer implements ThrowableRendererInterface { - public function render(Throwable $t, ServerRequestInterface $request = null): ErrorData + public function render(Throwable $t, ?ServerRequestInterface $request = null): ErrorData { return new ErrorData(self::DEFAULT_ERROR_MESSAGE); } - public function renderVerbose(Throwable $t, ServerRequestInterface $request = null): ErrorData + public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData { return new ErrorData( self::throwableToString($t) diff --git a/src/Renderer/XmlRenderer.php b/src/Renderer/XmlRenderer.php index 2bd63f2..e7af2cd 100644 --- a/src/Renderer/XmlRenderer.php +++ b/src/Renderer/XmlRenderer.php @@ -16,7 +16,7 @@ */ final class XmlRenderer implements ThrowableRendererInterface { - public function render(Throwable $t, ServerRequestInterface $request = null): ErrorData + public function render(Throwable $t, ?ServerRequestInterface $request = null): ErrorData { $content = ''; $content .= "\n\n"; @@ -25,7 +25,7 @@ public function render(Throwable $t, ServerRequestInterface $request = null): Er return new ErrorData($content); } - public function renderVerbose(Throwable $t, ServerRequestInterface $request = null): ErrorData + public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData { $content = ''; $content .= "\n\n"; diff --git a/src/ThrowableRendererInterface.php b/src/ThrowableRendererInterface.php index a409a04..b6f4886 100644 --- a/src/ThrowableRendererInterface.php +++ b/src/ThrowableRendererInterface.php @@ -23,7 +23,7 @@ interface ThrowableRendererInterface * @param ServerRequestInterface|null $request * @return ErrorData */ - public function render(Throwable $t, ServerRequestInterface $request = null): ErrorData; + public function render(Throwable $t, ?ServerRequestInterface $request = null): ErrorData; /** * Returns error data suitable for adding it to response in development environment. @@ -31,5 +31,5 @@ public function render(Throwable $t, ServerRequestInterface $request = null): Er * @param ServerRequestInterface|null $request * @return ErrorData */ - public function renderVerbose(Throwable $t, ServerRequestInterface $request = null): ErrorData; + public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData; } diff --git a/tests/Factory/ThrowableResponseFactoryTest.php b/tests/Factory/ThrowableResponseFactoryTest.php index df40fe2..6f6a239 100644 --- a/tests/Factory/ThrowableResponseFactoryTest.php +++ b/tests/Factory/ThrowableResponseFactoryTest.php @@ -233,7 +233,7 @@ public function testAddedHeaders(): void } private function createThrowableResponseFactory( - HeadersProvider $provider = null, + ?HeadersProvider $provider = null, ): ThrowableResponseFactoryInterface { $container = new SimpleContainer([], fn (string $className): object => new $className()); return new ThrowableResponseFactory( diff --git a/tests/Renderer/HtmlRendererTest.php b/tests/Renderer/HtmlRendererTest.php index aa8026b..6b1308e 100644 --- a/tests/Renderer/HtmlRendererTest.php +++ b/tests/Renderer/HtmlRendererTest.php @@ -7,6 +7,7 @@ use Exception; use HttpSoft\Message\Uri; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\WithoutErrorHandler; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ServerRequestInterface; use ReflectionClass; @@ -131,9 +132,16 @@ public function testRenderCallStack(): void ); } + #[WithoutErrorHandler] public function testRenderCallStackItemIfFileIsNotExistAndLineMoreZero(): void { - $this->assertEmpty($this->invokeMethod(new HtmlRenderer(), 'renderCallStackItem', [ + $errorMessage = null; + set_error_handler( + static function (int $code, string $message) use (&$errorMessage) { + $errorMessage = $message; + } + ); + $result = $this->invokeMethod(new HtmlRenderer(), 'renderCallStackItem', [ 'file' => 'not-exist', 'line' => 1, 'class' => null, @@ -142,7 +150,11 @@ public function testRenderCallStackItemIfFileIsNotExistAndLineMoreZero(): void 'index' => 1, 'isVendorFile' => false, 'reflectionParameters' => [], - ])); + ]); + restore_error_handler(); + + $this->assertSame('', $result); + $this->assertSame('file(not-exist): Failed to open stream: No such file or directory', $errorMessage); } public function testRenderRequest(): void diff --git a/tools/.gitignore b/tools/.gitignore new file mode 100644 index 0000000..cf452dc --- /dev/null +++ b/tools/.gitignore @@ -0,0 +1,2 @@ +/*/vendor +/*/composer.lock diff --git a/tools/composer-require-checker/composer.json b/tools/composer-require-checker/composer.json new file mode 100644 index 0000000..35b552f --- /dev/null +++ b/tools/composer-require-checker/composer.json @@ -0,0 +1,8 @@ +{ + "require-dev": { + "maglnet/composer-require-checker": "^4.7.1" + }, + "config": { + "bump-after-update": "dev" + } +}