From 33bfbb544c9bacc12e50c9d285945c8a5af6c9cb Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Sat, 25 Nov 2023 09:02:16 +0100 Subject: [PATCH] Adding aide-error --- .php-cs-fixer.php | 2 +- composer.json | 2 +- config.subsplit-publish.json | 5 + .../.github/workflows/close-subsplit-prs.yaml | 2 +- src/Error/.gitattributes | 6 + src/Error/.github/FUNDING.yml | 1 + .../.github/workflows/close-subsplit-prs.yaml | 17 ++ src/Error/Cloak.php | 184 ++++++++++++++++++ src/Error/CloakTest.php | 146 ++++++++++++++ src/Error/LICENSE | 20 ++ src/Error/README.md | 176 +++++++++++++++++ src/Error/composer.json | 42 ++++ 12 files changed, 600 insertions(+), 3 deletions(-) create mode 100644 src/Error/.gitattributes create mode 100644 src/Error/.github/FUNDING.yml create mode 100644 src/Error/.github/workflows/close-subsplit-prs.yaml create mode 100644 src/Error/Cloak.php create mode 100644 src/Error/CloakTest.php create mode 100644 src/Error/LICENSE create mode 100644 src/Error/README.md create mode 100644 src/Error/composer.json diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 79865d7..1aaac2d 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -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, diff --git a/composer.json b/composer.json index a8ba39f..029bc72 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "bakame/aide", - "description": "Structured Field Values for HTTP manipulation in PHP", + "description": "Tools and Helper to ease PHP development", "type": "library", "keywords": ["helpers"], "license": "MIT", diff --git a/config.subsplit-publish.json b/config.subsplit-publish.json index 35035cd..062b2d9 100644 --- a/config.subsplit-publish.json +++ b/config.subsplit-publish.json @@ -4,6 +4,11 @@ "name": "aide-enum", "directory": "src/Enum", "target": "git@github.com:bakame-php/aide-enum.git" + }, + { + "name": "aide-error", + "directory": "src/Error", + "target": "git@github.com:bakame-php/aide-error.git" } ] } diff --git a/src/Enum/.github/workflows/close-subsplit-prs.yaml b/src/Enum/.github/workflows/close-subsplit-prs.yaml index b6f2bd9..4f80eef 100644 --- a/src/Enum/.github/workflows/close-subsplit-prs.yaml +++ b/src/Enum/.github/workflows/close-subsplit-prs.yaml @@ -9,7 +9,7 @@ jobs: - uses: frankdejonge/action-close-subsplit-pr@0.1.0 with: close_pr: 'yes' - target_branch_match: '^(?!master).+$' + target_branch_match: '^(?!main).+$' message: | Hi :wave:, diff --git a/src/Error/.gitattributes b/src/Error/.gitattributes new file mode 100644 index 0000000..adbaa59 --- /dev/null +++ b/src/Error/.gitattributes @@ -0,0 +1,6 @@ +* text=auto + +.github export-ignore +.gitattributes export-ignore +README.md export-ignore +**/*Test.php export-ignore diff --git a/src/Error/.github/FUNDING.yml b/src/Error/.github/FUNDING.yml new file mode 100644 index 0000000..70fdf4b --- /dev/null +++ b/src/Error/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [nyamsprod] diff --git a/src/Error/.github/workflows/close-subsplit-prs.yaml b/src/Error/.github/workflows/close-subsplit-prs.yaml new file mode 100644 index 0000000..4f80eef --- /dev/null +++ b/src/Error/.github/workflows/close-subsplit-prs.yaml @@ -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/action-close-subsplit-pr@0.1.0 + 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 diff --git a/src/Error/Cloak.php b/src/Error/Cloak.php new file mode 100644 index 0000000..8bc563f --- /dev/null +++ b/src/Error/Cloak.php @@ -0,0 +1,184 @@ +exception = null; + $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; + }; + + 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); + } +} diff --git a/src/Error/CloakTest.php b/src/Error/CloakTest.php new file mode 100644 index 0000000..d696062 --- /dev/null +++ b/src/Error/CloakTest.php @@ -0,0 +1,146 @@ +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::assertTrue($lambda->suppressDeprecated()); + } + + 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()); + } +} diff --git a/src/Error/LICENSE b/src/Error/LICENSE new file mode 100644 index 0000000..ea6b459 --- /dev/null +++ b/src/Error/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2023 ignace nyamagana butera + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/Error/README.md b/src/Error/README.md new file mode 100644 index 0000000..284c7f7 --- /dev/null +++ b/src/Error/README.md @@ -0,0 +1,176 @@ +# Bakame Aide for Errors + +A collection of class to avoid repeating the same methods on PHP's Enum. + +> [!CAUTION] +> Sub-split of Aide for Error. +> ⚠️ this is a sub-split, for pull requests and issues, visit: https://github.com/bakame-php/aide + +## Installation + +### Composer + +~~~ +composer require bakame-php/aide-error +~~~ + +### System Requirements + +You need: + +- **PHP >= 8.1** but the latest stable version of PHP is recommended + +## Usage + +Traditionnally to correctly handle errors with PHP's function you have two (2) options. Either +you use the `@` to suppres the error or you need to add some boilerplate code around `set_error_handler` + +The `Bakame\Aide\Error\Cloak` utility class helps remove that burden. + +```php +lastError(); //returns the last error as an \ErrorException; +} +```` + +You can control its behaviour on your global codebase + +```php +lastError(); +} +```` + +## Available properties and methods + +### 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 the instance will suppress user deprecated error +you can do the following: + +```php +$touch = Cloak::warning(touch(...)); +if ($touch->suppressWarning()) { + //tells wether or not E_WARNING is suppressed +} +``` + +The following methods are available. + +```php + [!NOTE] +> to respect PHP's default behaviour by default `Cloak` uses `Cloak::silenceError` + +### Named constructors + +To ease usage the following named constructors are added: + +```php +