diff --git a/composer.json b/composer.json index 92cac5c..5478a5b 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,8 @@ "nexusphp/clock": "self.version", "nexusphp/collection": "self.version", "nexusphp/option": "self.version", - "nexusphp/phpstan-nexus": "self.version" + "nexusphp/phpstan-nexus": "self.version", + "nexusphp/suppression": "self.version" }, "provide": { "psr/clock-implementation": "1.0" diff --git a/src/Nexus/Suppression/LICENSE b/src/Nexus/Suppression/LICENSE new file mode 100644 index 0000000..ce701d4 --- /dev/null +++ b/src/Nexus/Suppression/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 John Paul E. Balandan, CPA + +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/Nexus/Suppression/README.md b/src/Nexus/Suppression/README.md new file mode 100644 index 0000000..8ce4264 --- /dev/null +++ b/src/Nexus/Suppression/README.md @@ -0,0 +1,54 @@ +# Nexus Suppression + +Provides abstractions around error management, steering usages away from the error +suppression operator (`@`). This way we make the intent clearer (e.g., suppressing +errors vs handling them explicitly). + +## Installation + + composer require nexusphp/suppression + +## Getting Started + +Instead of using the error suppression operator, which is considered bad practice, +you can either wrap the expression in a closure then pass it to either `Silencer::box()` +or `Silencer::suppress()`. + +### `Silencer::box()` + +You will typically use the `box()` method of `Silencer` when you want to execute an +error-prone operation, get its result, and get also its error message. + +```php +// instead of: +$result = @mkdir('tests/Suppression'); + +// use: +[$result, $message] = (new Silencer())->box(fn(): bool => mkdir('tests/Suppression')); + +var_dump($result); // bool(false) +var_dump($message); // string(11) "File exists" + +``` + +The `box` method accepts a `Closure` that returns the result of the operation. The method then +return this result plus the error message, if any. If no error occurred, error message is `null`. + +### `Silencer::suppress()` + +You may use the `suppress()` method when you don't want the error message but just want to +retrieve the operation's result without the error, if any. The syntax is similar to `box()`: +just pass a `Closure` returning the result of the operation. + +## License + +Nexus Suppression is licensed under the [MIT License][1]. + +## Resources + +* [Report issues][2] and [send pull requests][3] in the [main Nexus repository][4] + +[1]: LICENSE +[2]: https://github.com/NexusPHP/framework/issues +[3]: https://github.com/NexusPHP/framework/pulls +[4]: https://github.com/NexusPHP/framework diff --git a/src/Nexus/Suppression/Silencer.php b/src/Nexus/Suppression/Silencer.php new file mode 100644 index 0000000..a2b7b89 --- /dev/null +++ b/src/Nexus/Suppression/Silencer.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Nexus\Suppression; + +final class Silencer +{ + /** + * @template T + * + * @param (\Closure(): T) $func + * + * @return array{T, null|string} + */ + public function box(\Closure $func): array + { + $message = null; + + set_error_handler(static function (int $errno, string $errstr) use (&$message): bool { + $message = $errstr; + + if (str_contains($message, '): ')) { + $message = substr($message, (int) strpos($message, '): ') + 3); + } + + return true; + }); + + try { + return [$func(), $message]; + } finally { + restore_error_handler(); + } + } + + /** + * @template T + * + * @param (\Closure(): T) $func + * + * @return T + */ + public function suppress(\Closure $func): mixed + { + $prevErrorLevel = error_reporting(0); + + try { + return $func(); + } finally { + error_reporting($prevErrorLevel); + } + } +} diff --git a/src/Nexus/Suppression/composer.json b/src/Nexus/Suppression/composer.json new file mode 100644 index 0000000..a16e576 --- /dev/null +++ b/src/Nexus/Suppression/composer.json @@ -0,0 +1,35 @@ +{ + "name": "nexusphp/suppression", + "description": "Provides abstractions around error management.", + "license": "MIT", + "type": "library", + "keywords": [ + "nexus", + "suppression" + ], + "authors": [ + { + "name": "John Paul E. Balandan, CPA", + "email": "paulbalandan@gmail.com" + } + ], + "support": { + "issues": "https://github.com/NexusPHP/framework/issues", + "source": "https://github.com/NexusPHP/framework" + }, + "require": { + "php": "^8.3" + }, + "minimum-stability": "dev", + "prefer-stable": true, + "autoload": { + "psr-4": { + "Nexus\\Suppression\\": "" + } + }, + "config": { + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true + } +} diff --git a/tests/Suppression/SilencerTest.php b/tests/Suppression/SilencerTest.php new file mode 100644 index 0000000..c45f87f --- /dev/null +++ b/tests/Suppression/SilencerTest.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Nexus\Tests\Suppression; + +use Nexus\Suppression\Silencer; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\TestCase; + +/** + * @internal + */ +#[CoversClass(Silencer::class)] +#[Group('unit-test')] +final class SilencerTest extends TestCase +{ + public function testSilencerBox(): void + { + [$result, $message] = (new Silencer())->box( + static fn(): false|string => file_get_contents('non-existent-file.txt'), + ); + self::assertFalse($result); + self::assertSame('Failed to open stream: No such file or directory', $message); + + [$result, $message] = (new Silencer())->box( + static fn(): false|string => file_get_contents(__FILE__), + ); + self::assertIsString($result); + self::assertNull($message); + } + + public function testSilencerSuppress(): void + { + $prevErrorLevel = error_reporting(); + + $result = (new Silencer())->suppress(static function (): int { + trigger_error('Test', E_USER_WARNING); + + return 30; + }); + self::assertSame(30, $result); + + self::assertSame($prevErrorLevel, error_reporting()); + } +}