Skip to content

Commit

Permalink
Adding the CloakedErrors exception
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod committed Nov 26, 2023
1 parent 566cb41 commit 5700507
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 115 deletions.
83 changes: 25 additions & 58 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,47 +100,58 @@ 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->isNotEmpty()) {
$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;
};

set_error_handler($errorHandler, $this->errorLevel->toBytes());
$result = ($this->closure)(...$arguments);
restore_error_handler();
try {
set_error_handler($errorHandler, $this->errorLevel->toBytes());
$result = ($this->closure)(...$arguments);
} finally {
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->exception;
return $this->errors;
}

public function errorLevel(): ErrorLevel
{
return $this->errorLevel;
}

public function errorsAreSilenced(): bool
Expand All @@ -152,49 +164,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);
}
}
63 changes: 43 additions & 20 deletions src/Error/CloakTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

namespace Bakame\Aide\Error;

use ErrorException;
use Exception;
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 +30,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 +42,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 +80,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 +112,37 @@ 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::assertStringContainsString('touch(): Unable to create file /foobar because', $errors->first()?->getMessage() ?? '');
self::assertSame('file(/foobar): Failed to open stream: No such file or directory', $errors->last()?->getMessage() ?? '');
}

#[Test]
public function it_does_not_interfer_with_exception(): void
{
$this->expectException(Exception::class);

$lambda = Cloak::warning(fn () => throw new Exception());
$lambda();
}
}
76 changes: 76 additions & 0 deletions src/Error/CloakedErrors.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace Bakame\Aide\Error;

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

use function array_unshift;
use function count;

/**
* @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 isNotEmpty(): bool
{
return !$this->isEmpty();
}

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;
}
}
9 changes: 4 additions & 5 deletions src/Error/ErrorLevel.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@

namespace Bakame\Aide\Error;

use OutOfBoundsException;

use OutOfRangeException;
use ValueError;

use function array_filter;
Expand Down Expand Up @@ -81,7 +80,7 @@ public function contains(self|int ...$levels): bool
{
foreach ($levels as $level) {
$level = $level instanceof self ? $level->toBytes() : $level;
if (0 !== ($level & $this->toBytes())) {
if (-1 === $this->value || 0 !== ($level & $this->value)) {
return true;
}
}
Expand Down Expand Up @@ -111,7 +110,7 @@ public function include(self|int ...$levels): self
$value = 0 === $this->value ? $levels[0] : $this->value;
foreach ($levels as $level) {
if (!in_array($level, self::LEVELS, true)) {
throw new OutOfBoundsException('The error reporting level value `'.$level.'` is invalid.');
throw new OutOfRangeException('The error reporting level value `'.$level.'` is invalid.');
}

$value &= $level;
Expand Down Expand Up @@ -139,7 +138,7 @@ public function ignore(self|int ...$levels): self
$value = $this->value;
foreach ($levels as $level) {
if (!in_array($level, self::LEVELS, true)) {
throw new OutOfBoundsException('The error reporting level value `'.$level.'` is invalid.');
throw new OutOfRangeException('The error reporting level value `'.$level.'` is invalid.');
}
$value &= ~$level;
}
Expand Down
Loading

0 comments on commit 5700507

Please sign in to comment.