Skip to content

Commit

Permalink
Adding aide-error
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod committed Nov 25, 2023
1 parent 425c1a3 commit 718b91e
Show file tree
Hide file tree
Showing 11 changed files with 598 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .php-cs-fixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
'import_constants' => true,
'import_functions' => true,
],
'new_with_braces' => true,
'new_with_parentheses' => true,
'no_blank_lines_after_phpdoc' => true,
'no_empty_phpdoc' => true,
'no_empty_comment' => true,
Expand Down
5 changes: 5 additions & 0 deletions config.subsplit-publish.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
"name": "aide-enum",
"directory": "src/Enum",
"target": "[email protected]:bakame-php/aide-enum.git"
},
{
"name": "aide-error",
"directory": "src/Error",
"target": "[email protected]:bakame-php/aide-error.git"
}
]
}
2 changes: 1 addition & 1 deletion src/Enum/.github/workflows/close-subsplit-prs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
- uses: frankdejonge/[email protected]
with:
close_pr: 'yes'
target_branch_match: '^(?!master).+$'
target_branch_match: '^(?!main).+$'
message: |
Hi :wave:,
Expand Down
6 changes: 6 additions & 0 deletions src/Error/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
* text=auto

.github export-ignore
.gitattributes export-ignore
README.md export-ignore
**/*Test.php export-ignore
1 change: 1 addition & 0 deletions src/Error/.github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
github: [nyamsprod]
17 changes: 17 additions & 0 deletions src/Error/.github/workflows/close-subsplit-prs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
on:
schedule:
- cron: '30 7 * * *'
jobs:
close_subsplit_prs:
runs-on: ubuntu-latest
name: Close sub-split PRs
steps:
- uses: frankdejonge/[email protected]
with:
close_pr: 'yes'
target_branch_match: '^(?!main).+$'
message: |
Hi :wave:,
Thank you for contributing to Aide. Unfortunately, you've sent a PR to a read-only sub-split repository.
All pull requests should be directed towards: https://github.com/bakame-php/aide
184 changes: 184 additions & 0 deletions src/Error/Cloak.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
<?php

declare(strict_types=1);

namespace Bakame\Aide\Error;

use Closure;
use ErrorException;

use const E_ALL;
use const E_DEPRECATED;
use const E_NOTICE;
use const E_STRICT;
use const E_USER_DEPRECATED;
use const E_USER_NOTICE;
use const E_USER_WARNING;
use const E_WARNING;

class Cloak
{
public const FORCE_NOTHING = 0;
public const SILENCE_ERROR = 1;
public const THROW_ON_ERROR = 2;

protected static bool $useException = false;
protected ?ErrorException $exception = null;

public function __construct(
protected readonly Closure $closure,
public readonly int $errorLevel = E_WARNING,
public readonly int $behaviour = self::FORCE_NOTHING
) {
}

public static function throwOnError(): void
{
self::$useException = true;
}

public static function silenceError(): void
{
self::$useException = false;
}

public static function warning(Closure $closure, int $behaviour = self::FORCE_NOTHING): self
{
return new self($closure, E_WARNING, $behaviour);
}

public static function notice(Closure $closure, int $behaviour = self::FORCE_NOTHING): self
{
return new self($closure, E_NOTICE, $behaviour);
}

public static function deprecated(Closure $closure, int $behaviour = self::FORCE_NOTHING): self
{
return new self($closure, E_DEPRECATED, $behaviour);
}

public static function strict(Closure $closure, int $behaviour = self::FORCE_NOTHING): self
{
return new self($closure, E_STRICT, $behaviour);
}

public static function userWarning(Closure $closure, int $behaviour = self::FORCE_NOTHING): self
{
return new self($closure, E_USER_WARNING, $behaviour);
}

public static function userNotice(Closure $closure, int $behaviour = self::FORCE_NOTHING): self
{
return new self($closure, E_USER_NOTICE, $behaviour);
}

public static function userDeprecated(Closure $closure, int $behaviour = self::FORCE_NOTHING): self
{
return new self($closure, E_USER_DEPRECATED, $behaviour);
}

public static function all(Closure $closure, int $behaviour = self::FORCE_NOTHING): self
{
return new self($closure, E_ALL, $behaviour);
}

/**
* @throws ErrorException
*/
public function __invoke(mixed ...$arguments): mixed
{
$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);

return true;
};

$this->exception = null;
set_error_handler($errorHandler, $this->errorLevel);
$result = ($this->closure)(...$arguments);
restore_error_handler();

if (null === $this->exception) { /* @phpstan-ignore-line */
return $result;
}

if (self::THROW_ON_ERROR === $this->behaviour) { /* @phpstan-ignore-line */
throw $this->exception;
}

if (self::SILENCE_ERROR === $this->behaviour) {
return $result;
}

if (true === self::$useException) {
throw $this->exception;
}

return $result;
}

public function lastError(): ?ErrorException
{
return $this->exception;
}

public function errorsAreSilenced(): bool
{
return !$this->errorsAreThrown();
}

public function errorsAreThrown(): bool
{
return self::THROW_ON_ERROR === $this->behaviour
|| (self::SILENCE_ERROR !== $this->behaviour && true === self::$useException);
}

public function suppressAll(): bool
{
return $this->suppress(E_ALL);
}

public function suppressWarning(): bool
{
return $this->suppress(E_WARNING);
}

public function suppressNotice(): bool
{
return $this->suppress(E_NOTICE);
}

public function suppressDeprecated(): bool
{
return $this->suppress(E_DEPRECATED);
}

public function suppressStrict(): bool
{
return $this->suppress(E_STRICT);
}

public function suppressUserWarning(): bool
{
return $this->suppress(E_USER_WARNING);
}

public function suppressUserNotice(): bool
{
return $this->suppress(E_USER_NOTICE);
}

public function suppressUserDeprecated(): bool
{
return $this->suppress(E_USER_DEPRECATED);
}

public function suppress(int $errorLevel): bool
{
return 0 !== ($errorLevel & $this->errorLevel);
}
}
145 changes: 145 additions & 0 deletions src/Error/CloakTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

declare(strict_types=1);

namespace Bakame\Aide\Error;

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

use const E_DEPRECATED;
use const E_WARNING;

final class CloakTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();

Cloak::silenceError();
}

#[Test]
public function it_returns_information_about_its_error_reporting_level(): void
{
$lambda = Cloak::warning(touch(...));
$res = $lambda('/foo');

self::assertFalse($res);
self::assertSame(E_WARNING, $lambda->errorLevel);
self::assertTrue($lambda->suppressWarning());
self::assertFalse($lambda->suppressNotice());
self::assertInstanceOf(ErrorException::class, $lambda->lastError());
}

#[Test]
public function it_will_suppress_nothing_in_case_of_success(): void
{
$lambda = Cloak::userWarning(strtoupper(...));
$res = $lambda('foo');

self::assertSame('FOO', $res);
self::assertNull($lambda->lastError());
}

public function testGetErrorReporting(): void
{
$lambda = Cloak::deprecated(strtoupper(...));

self::assertSame(E_DEPRECATED, $lambda->errorLevel);
}

public function testCapturesTriggeredError(): void
{
$lambda = Cloak::all(trigger_error(...));
$lambda('foo');

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

public function testCapturesSilencedError(): void
{
$lambda = Cloak::notice(function (string $x) {
@trigger_error($x);
});
$lambda('foo');

self::assertNull($lambda->lastError());
}

public function testObjectDoesntInteractWithExistingErrorHandlers(): void
{
$count = 0;
set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) use (&$count): bool {
$count++;

return true;
});
trigger_error('foo');
self::assertSame(1, $count);

$lambda = Cloak::warning(trigger_error(...));
$lambda('foo');
self::assertSame(1, $count); /* @phpstan-ignore-line */

trigger_error('foo');
self::assertSame(2, $count); /* @phpstan-ignore-line */
}

public function testErrorTransformedIntoARuntimeException(): void
{
$this->expectException(ErrorException::class);

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

public function testErrorTransformedIntoAnInvalidArgumentException(): void
{
Cloak::throwOnError();
$this->expectException(ErrorException::class);

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

public function testSpecificBehaviourOverrideGeneralErrorSetting(): void
{
Cloak::throwOnError();

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

self::assertInstanceOf(ErrorException::class, $touch->lastError());
}

public function testCaptureNothingThrowNoException(): void
{
Cloak::throwOnError();
$strtoupper = Cloak::strict(strtoupper(...));

self::assertSame('FOO', $strtoupper('foo'));
}

#[Test]
public function it_can_detect_the_level_to_suppress(): void
{
$touch = new Cloak(
touch(...),
E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED,
Cloak::THROW_ON_ERROR
);

self::assertTrue($touch->suppressAll());
self::assertFalse($touch->suppressStrict());
self::assertFalse($touch->suppressDeprecated());
self::assertFalse($touch->suppressNotice());
self::assertTrue($touch->suppressUserNotice());
self::assertTrue($touch->suppressUserDeprecated());
self::assertTrue($touch->suppressUserWarning());
self::assertTrue($touch->errorsAreThrown());
self::assertFalse($touch->errorsAreSilenced());
}
}
Loading

0 comments on commit 718b91e

Please sign in to comment.