Skip to content

Commit

Permalink
Improve Error Handling using the CloakedErrors exception
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod committed Nov 25, 2023
1 parent 566cb41 commit 0b95929
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 106 deletions.
74 changes: 19 additions & 55 deletions src/Error/Cloak.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -41,6 +41,7 @@ public function __construct(
}

$this->errorLevel = $errorLevel;
$this->errors = new CloakedErrors();
}

public static function throwOnError(): void
Expand Down Expand Up @@ -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;
};
Expand All @@ -118,28 +122,33 @@ 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) {
return $result;
}

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
Expand All @@ -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);
}
}
53 changes: 33 additions & 20 deletions src/Error/CloakTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

namespace Bakame\Aide\Error;

use ErrorException;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

use const E_ALL;
use const E_DEPRECATED;
use const E_NOTICE;
use const E_STRICT;
use const E_WARNING;

final class CloakTest extends TestCase
{
Expand All @@ -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]
Expand All @@ -41,35 +41,35 @@ 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
{
$lambda = Cloak::all(trigger_error(...));
$lambda('foo');

self::assertSame('foo', $lambda->lastError()?->getMessage());
self::assertSame('foo', $lambda->errors()->last()?->getMessage());
}

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(...));
Expand All @@ -79,20 +79,19 @@ public function testErrorTransformedIntoARuntimeException(): void
public function testErrorTransformedIntoAnInvalidArgumentException(): void
{
Cloak::throwOnError();
$this->expectException(ErrorException::class);
$this->expectException(CloakedErrors::class);

$touch = Cloak::all(touch(...));
$touch('/foo');
}

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
Expand All @@ -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());
}
}
68 changes: 68 additions & 0 deletions src/Error/CloakedErrors.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

namespace Bakame\Aide\Error;

use Countable;
use ErrorException;
use Iterator;
use IteratorAggregate;
use RuntimeException;

/**
* @implements IteratorAggregate<int, ErrorException>
*/
final class CloakedErrors extends RuntimeException implements Countable, IteratorAggregate
{
/** @var array<ErrorException> */
private array $errorExceptions;

public function __construct(string $message = '')
{
parent::__construct($message);
$this->errorExceptions = [];
}

public function count(): int
{
return count($this->errorExceptions);
}

/**
* @return Iterator<int, ErrorException>
*/
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;
}
}
Loading

0 comments on commit 0b95929

Please sign in to comment.