From 1dc2e2c2bb62ffd73fb5d31a0780dbcfce2e3a3c Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:25:25 +0400 Subject: [PATCH] [#598] Log deprecations in the error handler instead of throwing them ErrorHandler::handleError() threw an ErrorException for every reported error, so a deprecation, which only warns about a future PHP version, stopped the request. Idiorm's query logging passes a NULL bound value to PDO::quote(), which PHP 8.1 deprecates, so a debug-mode query with a NULL parameter failed. A reported E_DEPRECATED or E_USER_DEPRECATED is now logged as a warning with its file and line, and the script goes on. Other errors still throw. --- src/Tracer/ErrorHandler.php | 10 +++ .../Adapters/Idiorm/IdiormDbalTest.php | 24 ++++++ tests/Unit/Tracer/ErrorHandlerTest.php | 80 +++++++++++++++++++ 3 files changed, 114 insertions(+) diff --git a/src/Tracer/ErrorHandler.php b/src/Tracer/ErrorHandler.php index 5aebf951..bef92a4d 100644 --- a/src/Tracer/ErrorHandler.php +++ b/src/Tracer/ErrorHandler.php @@ -63,6 +63,8 @@ public function setCliOutput(OutputInterface $output): void } /** + * Turns a reported PHP error into an exception. A deprecation only warns about a future PHP version, + * often from code that is not ours, so it is logged and the script goes on. * @throws ErrorException */ public function handleError(int $severity, string $message, string $file, int $line): bool @@ -71,6 +73,14 @@ public function handleError(int $severity, string $message, string $file, int $l return false; } + if ($severity === E_DEPRECATED || $severity === E_USER_DEPRECATED) { + if ($this->logger !== null) { + $this->logger->warning($message . ' in ' . $file . ':' . $line); + } + + return true; + } + throw new ErrorException($message, 0, $severity, $file, $line); } diff --git a/tests/Unit/Database/Adapters/Idiorm/IdiormDbalTest.php b/tests/Unit/Database/Adapters/Idiorm/IdiormDbalTest.php index 9dcda684..40fc6372 100644 --- a/tests/Unit/Database/Adapters/Idiorm/IdiormDbalTest.php +++ b/tests/Unit/Database/Adapters/Idiorm/IdiormDbalTest.php @@ -3,6 +3,7 @@ namespace Quantum\Tests\Unit\Database\Adapters\Idiorm; use Quantum\Database\Adapters\Idiorm\IdiormDbal; +use Quantum\Tracer\ErrorHandler; use Quantum\App\Enums\AppType; class IdiormDbalTest extends IdiormDbalTestCase @@ -51,6 +52,29 @@ public function testIdiormLogsNoQueriesWhenDebugIsTheTextFalseFromEnv(): void $this->assertFalse($this->getBaseConfig()['logging']); } + public function testIdiormRunsAQueryWithANullParameterWhileDeprecationsAreReported(): void + { + IdiormDbal::execute('CREATE TABLE nullable_values (id INTEGER PRIMARY KEY, name TEXT)'); + + (new ErrorHandler())->setup(logger()); + + $oldLevel = error_reporting(E_ALL); + + try { + IdiormDbal::execute('INSERT INTO nullable_values (name) VALUES (?)', [null]); + + $this->assertSame([['name' => null]], IdiormDbal::query('SELECT name FROM nullable_values')); + + $warnings = array_column(logger()->getAdapter()->getMessages(), 'warning'); + + $this->assertNotEmpty(array_filter($warnings, fn ($message) => str_contains((string) $message, 'PDO::quote()'))); + } finally { + error_reporting($oldLevel); + restore_error_handler(); + restore_exception_handler(); + } + } + /** * @return array */ diff --git a/tests/Unit/Tracer/ErrorHandlerTest.php b/tests/Unit/Tracer/ErrorHandlerTest.php index 9ff87fe8..6ef78251 100644 --- a/tests/Unit/Tracer/ErrorHandlerTest.php +++ b/tests/Unit/Tracer/ErrorHandlerTest.php @@ -91,6 +91,55 @@ public function testHandleErrorReturnsFalseForSuppressedErrors(): void } } + /** + * @dataProvider deprecationSeverityProvider + */ + public function testHandleErrorLogsADeprecationInsteadOfThrowing(int $severity): void + { + $logged = $this->captureWarnings(); + + $oldLevel = error_reporting(E_ALL); + + try { + $result = $this->errorHandler->handleError($severity, 'Feature is deprecated', '/app/file.php', 12); + + $this->assertTrue($result); + $this->assertSame(['Feature is deprecated in /app/file.php:12'], $logged()); + } finally { + error_reporting($oldLevel); + } + } + + /** + * @dataProvider deprecationSeverityProvider + */ + public function testHandleErrorIgnoresADeprecationWhenThereIsNoLogger(int $severity): void + { + $this->setPrivateProperty($this->errorHandler, 'logger', null); + + $oldLevel = error_reporting(E_ALL); + + try { + $this->assertTrue($this->errorHandler->handleError($severity, 'Feature is deprecated', __FILE__, __LINE__)); + } finally { + error_reporting($oldLevel); + } + } + + public function testHandleErrorLeavesAnUnreportedDeprecationToPhp(): void + { + $logged = $this->captureWarnings(); + + $oldLevel = error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED); + + try { + $this->assertFalse($this->errorHandler->handleError(E_DEPRECATED, 'Feature is deprecated', __FILE__, __LINE__)); + $this->assertSame([], $logged()); + } finally { + error_reporting($oldLevel); + } + } + public function testHandleExceptionCliWritesExceptionMessage(): void { $this->useAppType(AppType::CONSOLE); @@ -218,6 +267,17 @@ public function testLogErrorFallsBackToErrorMethodForUnknownLevel(): void $this->assertTrue(true); } + /** + * @return array + */ + public function deprecationSeverityProvider(): array + { + return [ + 'deprecated' => [E_DEPRECATED], + 'user-deprecated' => [E_USER_DEPRECATED], + ]; + } + /** * @dataProvider errorTypeProvider */ @@ -242,4 +302,24 @@ public function errorTypeProvider(): array ]; } + /** + * Gives the handler a logger that records its warnings, and returns a function that lists them + * @return callable(): array + */ + private function captureWarnings(): callable + { + $logged = []; + + $logger = Mockery::mock(Logger::class); + $logger->shouldReceive('warning')->andReturnUsing(function (string $message) use (&$logged): void { + $logged[] = $message; + }); + + $this->setPrivateProperty($this->errorHandler, 'logger', $logger); + + return function () use (&$logged): array { + return $logged; + }; + } + }