From 7ec9e5c9ee80bb9569f30c6d37221e46071565b8 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sun, 17 Aug 2025 08:35:56 +0300 Subject: [PATCH 1/6] Add `UserExceptionInterface` --- CHANGELOG.md | 1 + src/Exception/UserException.php | 5 ++--- src/Exception/UserExceptionInterface.php | 15 +++++++++++++++ templates/production.php | 4 ++-- 4 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 src/Exception/UserExceptionInterface.php diff --git a/CHANGELOG.md b/CHANGELOG.md index ce738ec..d0f1d51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Enh #150: Cleanup templates, remove legacy code (@vjik) - New #151: Add `$traceLink` parameter to `HtmlRenderer` to allow linking to trace files (@vjik) +- New #152: Add `UserExceptionInterface` to mark user exceptions (@vjik) ## 4.1.0 April 18, 2025 diff --git a/src/Exception/UserException.php b/src/Exception/UserException.php index b6ded57..dc1738d 100644 --- a/src/Exception/UserException.php +++ b/src/Exception/UserException.php @@ -7,11 +7,10 @@ use Exception; /** - * UserException is the base class for exceptions that are meant to be shown to end users. - * Such exceptions are often caused by mistakes of end users. + * `UserException` represents an exception that is meant to be shown to end users. * * @final */ -class UserException extends Exception +class UserException extends Exception implements UserExceptionInterface { } diff --git a/src/Exception/UserExceptionInterface.php b/src/Exception/UserExceptionInterface.php new file mode 100644 index 0000000..3182a1f --- /dev/null +++ b/src/Exception/UserExceptionInterface.php @@ -0,0 +1,15 @@ +getThrowableName($throwable); $message = $throwable->getMessage(); } else { From 42d78d5c79f7ec97fa99dfd16477e608ff779268 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sun, 17 Aug 2025 08:37:38 +0300 Subject: [PATCH 2/6] fix --- CHANGELOG.md | 2 +- tests/Factory/ThrowableResponseFactoryTest.php | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0f1d51..7d7d7a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - Enh #150: Cleanup templates, remove legacy code (@vjik) - New #151: Add `$traceLink` parameter to `HtmlRenderer` to allow linking to trace files (@vjik) -- New #152: Add `UserExceptionInterface` to mark user exceptions (@vjik) +- New #153: Add `UserExceptionInterface` to mark user exceptions (@vjik) ## 4.1.0 April 18, 2025 diff --git a/tests/Factory/ThrowableResponseFactoryTest.php b/tests/Factory/ThrowableResponseFactoryTest.php index 6f6a239..0c9ae08 100644 --- a/tests/Factory/ThrowableResponseFactoryTest.php +++ b/tests/Factory/ThrowableResponseFactoryTest.php @@ -32,12 +32,8 @@ public function testHandleWithHeadRequestMethod(): void $this->createThrowable(), $this->createServerRequest('HEAD', ['Accept' => ['test/html']]) ); - $response - ->getBody() - ->rewind(); - $content = $response - ->getBody() - ->getContents(); + $response->getBody()->rewind(); + $content = $response->getBody()->getContents(); $this->assertEmpty($content); $this->assertSame([HeaderRenderer::DEFAULT_ERROR_MESSAGE], $response->getHeader('X-Error-Message')); From 58f1837e48bf3e6e9ff2d1ba571dc515b29b3116 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sun, 17 Aug 2025 15:50:05 +0300 Subject: [PATCH 3/6] Refactor to attribute --- CHANGELOG.md | 2 +- src/Exception/UserException.php | 25 +++++++++++- src/Exception/UserExceptionInterface.php | 15 -------- templates/production.php | 4 +- .../UserException/NotFoundException.php | 13 +++++++ .../UserException/UserExceptionTest.php | 38 +++++++++++++++++++ 6 files changed, 77 insertions(+), 20 deletions(-) delete mode 100644 src/Exception/UserExceptionInterface.php create mode 100644 tests/Exception/UserException/NotFoundException.php create mode 100644 tests/Exception/UserException/UserExceptionTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d7d7a3..abefd0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - Enh #150: Cleanup templates, remove legacy code (@vjik) - New #151: Add `$traceLink` parameter to `HtmlRenderer` to allow linking to trace files (@vjik) -- New #153: Add `UserExceptionInterface` to mark user exceptions (@vjik) +- New #153: Introduce `UserException` attribute to mark user exceptions (@vjik) ## 4.1.0 April 18, 2025 diff --git a/src/Exception/UserException.php b/src/Exception/UserException.php index dc1738d..8529530 100644 --- a/src/Exception/UserException.php +++ b/src/Exception/UserException.php @@ -4,13 +4,34 @@ namespace Yiisoft\ErrorHandler\Exception; +use Attribute; use Exception; +use ReflectionClass; +use Throwable; + +use function count; /** - * `UserException` represents an exception that is meant to be shown to end users. + * `UserException` is an exception anв class attribute that indicates + * the exception message is safe to display to end users. + * + * Usage: + * - throw directly (`throw new UserException(...)`) for explicit user-facing errors; + * - annotate any exception class with the `#[UserException]` attribute + * to mark its messages as user-facing without extending this class. * * @final */ -class UserException extends Exception implements UserExceptionInterface +#[Attribute(Attribute::TARGET_CLASS)] +class UserException extends Exception { + public static function is(Throwable $throwable): bool + { + if ($throwable instanceof self) { + return true; + } + + $attributes = (new ReflectionClass($throwable))->getAttributes(self::class); + return count($attributes) > 0; + } } diff --git a/src/Exception/UserExceptionInterface.php b/src/Exception/UserExceptionInterface.php deleted file mode 100644 index 3182a1f..0000000 --- a/src/Exception/UserExceptionInterface.php +++ /dev/null @@ -1,15 +0,0 @@ -getThrowableName($throwable); $message = $throwable->getMessage(); } else { diff --git a/tests/Exception/UserException/NotFoundException.php b/tests/Exception/UserException/NotFoundException.php new file mode 100644 index 0000000..15731b7 --- /dev/null +++ b/tests/Exception/UserException/NotFoundException.php @@ -0,0 +1,13 @@ +getMessage()); + assertInstanceOf(Exception::class, $exception); + } + + public static function dataIs(): iterable + { + yield [true, new UserException()]; + yield [false, new Exception()]; + yield [true, new NotFoundException()]; + } + + #[DataProvider('dataIs')] + public function testIs(bool $expected, Throwable $exception): void + { + assertSame($expected, UserException::is($exception)); + } +} From 98ab936ab89829f10b5bac420d0cf5e419c11e35 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sun, 17 Aug 2025 16:25:56 +0300 Subject: [PATCH 4/6] Update src/Exception/UserException.php Co-authored-by: Aleksei Gagarin --- src/Exception/UserException.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Exception/UserException.php b/src/Exception/UserException.php index 8529530..5e896e4 100644 --- a/src/Exception/UserException.php +++ b/src/Exception/UserException.php @@ -12,7 +12,7 @@ use function count; /** - * `UserException` is an exception anв class attribute that indicates + * `UserException` is an exception and class attribute that indicates * the exception message is safe to display to end users. * * Usage: From bbb6a71c0e0a3900f73479d744e1f2bfb6408b7c Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Mon, 18 Aug 2025 10:38:49 +0300 Subject: [PATCH 5/6] Update src/Exception/UserException.php Co-authored-by: Alexander Makarov --- src/Exception/UserException.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Exception/UserException.php b/src/Exception/UserException.php index 5e896e4..6e2cb24 100644 --- a/src/Exception/UserException.php +++ b/src/Exception/UserException.php @@ -12,7 +12,7 @@ use function count; /** - * `UserException` is an exception and class attribute that indicates + * `UserException` is an exception and a class attribute that indicates * the exception message is safe to display to end users. * * Usage: From 07b3ba3c726130ebe781c7ecfc5e498f8843b7e9 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Mon, 18 Aug 2025 14:45:57 +0300 Subject: [PATCH 6/6] rename method --- src/Exception/UserException.php | 2 +- templates/production.php | 2 +- tests/Exception/UserException/UserExceptionTest.php | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Exception/UserException.php b/src/Exception/UserException.php index 6e2cb24..8c78b1e 100644 --- a/src/Exception/UserException.php +++ b/src/Exception/UserException.php @@ -25,7 +25,7 @@ #[Attribute(Attribute::TARGET_CLASS)] class UserException extends Exception { - public static function is(Throwable $throwable): bool + public static function isUserException(Throwable $throwable): bool { if ($throwable instanceof self) { return true; diff --git a/templates/production.php b/templates/production.php index 3686f8e..6551f7b 100644 --- a/templates/production.php +++ b/templates/production.php @@ -9,7 +9,7 @@ * @var HtmlRenderer $this */ -if (UserException::is($throwable)) { +if (UserException::isUserException($throwable)) { $name = $this->getThrowableName($throwable); $message = $throwable->getMessage(); } else { diff --git a/tests/Exception/UserException/UserExceptionTest.php b/tests/Exception/UserException/UserExceptionTest.php index f9cd494..ce5fa39 100644 --- a/tests/Exception/UserException/UserExceptionTest.php +++ b/tests/Exception/UserException/UserExceptionTest.php @@ -23,16 +23,16 @@ public function testUserExceptionInstance(): void assertInstanceOf(Exception::class, $exception); } - public static function dataIs(): iterable + public static function dataIsUserException(): iterable { yield [true, new UserException()]; yield [false, new Exception()]; yield [true, new NotFoundException()]; } - #[DataProvider('dataIs')] - public function testIs(bool $expected, Throwable $exception): void + #[DataProvider('dataIsUserException')] + public function testIsUserException(bool $expected, Throwable $exception): void { - assertSame($expected, UserException::is($exception)); + assertSame($expected, UserException::isUserException($exception)); } }