diff --git a/src/Error/Cloak.php b/src/Error/Cloak.php index 9ade2b6..bf52e82 100644 --- a/src/Error/Cloak.php +++ b/src/Error/Cloak.php @@ -27,7 +27,7 @@ class Cloak public const THROW = 2; protected static bool $useException = false; - protected ?ErrorException $exception = null; + protected CloakedErrors $errors; protected readonly ErrorLevel $errorLevel; public function __construct( @@ -41,6 +41,7 @@ public function __construct( } $this->errorLevel = $errorLevel; + $this->errors = new CloakedErrors(); } public static function throwOnError(): void @@ -99,17 +100,20 @@ public static function all(Closure $closure, int $onError = self::FOLLOW_ENV): s } /** - * @throws ErrorException + * @throws CloakedErrors */ public function __invoke(mixed ...$arguments): mixed { - $this->exception = null; + if (!$this->errors->isEmpty()) { + $this->errors = new CloakedErrors(); + } + $errorHandler = function (int $errno, string $errstr, string $errfile, int $errline): bool { if (0 === (error_reporting() & $errno)) { return false; } - $this->exception = new ErrorException($errstr, 0, $errno, $errfile, $errline); + $this->errors->unshift(new ErrorException($errstr, 0, $errno, $errfile, $errline)); return true; }; @@ -118,12 +122,12 @@ public function __invoke(mixed ...$arguments): mixed $result = ($this->closure)(...$arguments); restore_error_handler(); - if (null === $this->exception) { /* @phpstan-ignore-line */ + if ($this->errors->isEmpty()) { return $result; } - if (self::THROW === $this->onError) { /* @phpstan-ignore-line */ - throw $this->exception; + if (self::THROW === $this->onError) { + throw $this->errors; } if (self::SILENT === $this->onError) { @@ -131,15 +135,20 @@ public function __invoke(mixed ...$arguments): mixed } if (true === self::$useException) { - throw $this->exception; + throw $this->errors; } return $result; } - public function lastError(): ?ErrorException + public function errors(): CloakedErrors + { + return $this->errors; + } + + public function errorLevel(): ErrorLevel { - return $this->exception; + return $this->errorLevel; } public function errorsAreSilenced(): bool @@ -152,49 +161,4 @@ public function errorsAreThrown(): bool return self::THROW === $this->onError || (self::SILENT !== $this->onError && true === self::$useException); } - - public function includeAll(): bool - { - return $this->include(E_ALL); - } - - public function includeWarning(): bool - { - return $this->include(E_WARNING); - } - - public function includeNotice(): bool - { - return $this->include(E_NOTICE); - } - - public function includeDeprecated(): bool - { - return $this->include(E_DEPRECATED); - } - - public function includeStrict(): bool - { - return $this->include(E_STRICT); - } - - public function includeUserWarning(): bool - { - return $this->include(E_USER_WARNING); - } - - public function includeUserNotice(): bool - { - return $this->include(E_USER_NOTICE); - } - - public function includeUserDeprecated(): bool - { - return $this->include(E_USER_DEPRECATED); - } - - public function include(ErrorLevel|int $errorLevel): bool - { - return $this->errorLevel->contains($errorLevel); - } } diff --git a/src/Error/CloakTest.php b/src/Error/CloakTest.php index 209eb09..177a819 100644 --- a/src/Error/CloakTest.php +++ b/src/Error/CloakTest.php @@ -4,7 +4,6 @@ namespace Bakame\Aide\Error; -use ErrorException; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; @@ -12,6 +11,7 @@ use const E_DEPRECATED; use const E_NOTICE; use const E_STRICT; +use const E_WARNING; final class CloakTest extends TestCase { @@ -29,9 +29,9 @@ public function it_returns_information_about_its_error_reporting_level(): void $res = $lambda('/foo'); self::assertFalse($res); - self::assertTrue($lambda->includeWarning()); - self::assertFalse($lambda->includeNotice()); - self::assertInstanceOf(ErrorException::class, $lambda->lastError()); + self::assertTrue($lambda->errorLevel()->contains(E_WARNING)); + self::assertFalse($lambda->errorLevel()->contains(E_NOTICE)); + self::assertCount(1, $lambda->errors()); } #[Test] @@ -41,14 +41,14 @@ public function it_will_include_nothing_in_case_of_success(): void $res = $lambda('foo'); self::assertSame('FOO', $res); - self::assertNull($lambda->lastError()); + self::assertCount(0, $lambda->errors()); } public function testGetErrorReporting(): void { $lambda = Cloak::deprecated(strtoupper(...)); - self::assertTrue($lambda->includeDeprecated()); + self::assertTrue($lambda->errorLevel()->contains(E_DEPRECATED)); } public function testCapturesTriggeredError(): void @@ -56,7 +56,7 @@ public function testCapturesTriggeredError(): void $lambda = Cloak::all(trigger_error(...)); $lambda('foo'); - self::assertSame('foo', $lambda->lastError()?->getMessage()); + self::assertSame('foo', $lambda->errors()->last()?->getMessage()); } public function testCapturesSilencedError(): void @@ -64,12 +64,12 @@ public function testCapturesSilencedError(): void $lambda = Cloak::warning(fn (string $x) => @trigger_error($x)); $lambda('foo'); - self::assertNull($lambda->lastError()); + self::assertTrue($lambda->errors()->isEmpty()); } public function testErrorTransformedIntoARuntimeException(): void { - $this->expectException(ErrorException::class); + $this->expectException(CloakedErrors::class); Cloak::throwOnError(); $touch = Cloak::warning(touch(...)); @@ -79,7 +79,7 @@ public function testErrorTransformedIntoARuntimeException(): void public function testErrorTransformedIntoAnInvalidArgumentException(): void { Cloak::throwOnError(); - $this->expectException(ErrorException::class); + $this->expectException(CloakedErrors::class); $touch = Cloak::all(touch(...)); $touch('/foo'); @@ -87,12 +87,11 @@ public function testErrorTransformedIntoAnInvalidArgumentException(): void public function testSpecificBehaviourOverrideGeneralErrorSetting(): void { - Cloak::throwOnError(); + $this->expectNotToPerformAssertions(); + Cloak::throwOnError(); $touch = Cloak::all(touch(...), Cloak::SILENT); $touch('/foo'); - - self::assertInstanceOf(ErrorException::class, $touch->lastError()); } public function testCaptureNothingThrowNoException(): void @@ -112,14 +111,28 @@ public function it_can_detect_the_level_to_include(): void E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED ); - self::assertTrue($touch->includeAll()); - self::assertFalse($touch->includeStrict()); - self::assertFalse($touch->includeDeprecated()); - self::assertFalse($touch->includeNotice()); - self::assertTrue($touch->includeUserNotice()); - self::assertTrue($touch->includeUserDeprecated()); - self::assertTrue($touch->includeUserWarning()); + $errorLevel = $touch->errorLevel(); + + self::assertFalse($errorLevel->contains(E_NOTICE)); self::assertTrue($touch->errorsAreThrown()); self::assertFalse($touch->errorsAreSilenced()); } + + #[Test] + public function it_can_collection_all_errors(): void + { + $closure = function (string $path): array|false { + touch($path); + + return file($path); + }; + + $lambda = Cloak::warning($closure); + $res = $lambda('/foobar'); + $errors = $lambda->errors(); + self::assertFalse($res); + self::assertCount(2, $errors); + self::assertSame('touch(): Unable to create file /foobar because Read-only file system', $errors->first()?->getMessage()); + self::assertSame('file(/foobar): Failed to open stream: No such file or directory', $errors->last()?->getMessage()); + } } diff --git a/src/Error/CloakedErrors.php b/src/Error/CloakedErrors.php new file mode 100644 index 0000000..a51e11c --- /dev/null +++ b/src/Error/CloakedErrors.php @@ -0,0 +1,68 @@ + + */ +final class CloakedErrors extends RuntimeException implements Countable, IteratorAggregate +{ + /** @var array */ + private array $errorExceptions; + + public function __construct(string $message = '') + { + parent::__construct($message); + $this->errorExceptions = []; + } + + public function count(): int + { + return count($this->errorExceptions); + } + + /** + * @return Iterator + */ + public function getIterator(): Iterator + { + yield from $this->errorExceptions; + } + + public function unshift(ErrorException $exception): void + { + array_unshift($this->errorExceptions, $exception); + } + + public function isEmpty(): bool + { + return [] === $this->errorExceptions; + } + + public function first(): ?ErrorException + { + return $this->get(-1); + } + + public function last(): ?ErrorException + { + return $this->get(0); + } + + public function get(int $offset): ?ErrorException + { + if ($offset < 0) { + $offset += count($this->errorExceptions); + } + + return $this->errorExceptions[$offset] ?? null; + } +} diff --git a/src/Error/README.md b/src/Error/README.md index f8cff40..6588f32 100644 --- a/src/Error/README.md +++ b/src/Error/README.md @@ -39,15 +39,17 @@ $res = @touch('/foo'); // bad and not recommended set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true); $res = touch('/foo'); restore_error_handler(); -// better but you lost some information is case of error -// and having to write this everytime as it is overkill +// better but you lost some information in case of error +// having to write this everytime is overkill //using Cloak $touch = Cloak::all(touch(...)); $res = $lambda('/foo'); -$lambda->lastError(); -// returns the last error as an \ErrorException -// if an error occurred or `null` on success +$lambda->errors(); //returns a CloakedErrors +// returns a CloakedErrors instance +// the instance is empty on success +// otherwise contains all the \ErrorExceptions +// generated during the closure execution ```` You can control its behaviour on your global codebase @@ -56,12 +58,13 @@ You can control its behaviour on your global codebase lastError(); + // errors are still available via the `errors` methpd + // but throwing will not happen + $touch->errors(); } ```` @@ -86,37 +89,35 @@ if (!$touch = Cloak::warning(touch(...), Cloak::SILENT)) { ### Accessing the Error Reporting Level Once instantiated, you can always access the error reporting level via -the `suppress*` methods. For instance if you need to know if a -specific error will be suppressed you can do the following: +the `errorLevel` method. For instance if you need to know if a +specific error is included you can do the following: ```php $touch = Cloak::all(touch(...)); -$touch->includeWarning(); //tells if the E_WARNING is included or not +$touch->errorLevel()->contains(E_WARNING); //tells if the E_WARNING is included or not ``` -The following methods are available. +### Accessing the error -```php -errors(); // $errors is a CloakedErrors instance +$errors->isEmpty(); //true if the execution generated 0 ErrorException; false otherwise +foreach ($errors as $error) { + $error; //ErrorException instances ordered from the newest one -> oldest one. +} +$errors->first(); // the oldest ErrorException +$errors->last(); // the newest ErrorException ``` -### Accessing the error - -To access the last error store in the instance you need to call the `Cloak::lastError` method. -If no error occurred during the last execution of the class the method will return `null`, -otherwise you will get an `\ErrorException` class containing all the detail about the -last error. - ### Controlling when to throw or not your errors. The class general behaviour is controlled by two (2) static method: