Browse Source

Prevent converted notices and warnings from halting execution; fixes #4

2.1.0
Dustin Wilson 1 year ago
parent
commit
d23f850efa
  1. 1
      .gitignore
  2. 27
      README.md
  3. 3
      composer.json
  4. 78
      composer.lock
  5. 32
      lib/Catcher.php
  6. 7
      tests/cases/TestCatcher.php
  7. 17
      tests/cases/TestPlainTextHandler.php

1
.gitignore

@ -1,6 +1,7 @@
# Catcher-specific
/test*.html
/test*.php
/test/
/test/*
# General

27
README.md

@ -1,6 +1,7 @@
[a]: https://php.net/manual/en/book.dom.php
[b]: https://github.com/Seldaek/monolog
[c]: https://github.com/symfony/yaml
[d]: https://www.php.net/manual/en/function.pcntl-fork.php
# Catcher
@ -59,6 +60,26 @@ Catcher comes built-in with the following handlers:
* `JSONHandler` – Outputs errors in a JSON format mostly representative of how errors are stored internally by Catcher handlers; it is provided as an example. The decision to make it like this was made because errors often need to be represented according to particular requirements or even a specification, and we cannot possibly support them all. `JSONHandler`, however, can be easily extended to suit individual project needs.
* `PlainTextHandler` – Outputs errors cleanly in plain text meant mostly for command line use and also provides for logging
### A Note About Warnings & Notices ###
As described in the summary paragraph at the beginning of this document, Catcher by default converts all warnings, notices, etc. to `Throwable`s and then proceeds to throw them. Normally, when throwing that halts execution no matter what, but with Catcher that is not the case.
```php
$catcher = new Catcher();
try {
trigger_error(\E_USER_WARNING, 'Ook!');
} catch (\Throwable $t) {
echo $t->message();
}
```
Output:
```
Ook!
```
This is accomplished internally because of [`pcntl_fork`][d]. The throw is done in a separate fork which causes that fork to exit after the `Throwable` is handled while the main process is allowed to continue. `pcntl_fork` is a POSIX function and therefore is only available for use in CLI UNIX environments; this means that it will work neither in Windows nor in Web environments. We also understand this might be undesirable behavior to some, so turning this off is as simple as setting `Catcher::$forking` to false.
## Documentation
@ -71,6 +92,7 @@ namespace MensBeam\Foundation;
use Mensbeam\Foundation\Catcher\Handler;
class Catcher {
public bool $forking = true;
public bool $preventExit = false;
public bool $throwErrors = true;
@ -91,6 +113,7 @@ class Catcher {
#### Properties
_forking_: When set to true Catcher will throw converted notices, warnings, etc. in a fork, allowing for execution to continue afterwards
_preventExit_: When set to true Catcher won't exit at all even after fatal errors or exceptions
_throwErrors_: When set to true Catcher will convert errors to throwables
@ -347,7 +370,7 @@ The default handlers, especially `PlainTextHandler`, are set up to handle most t
```php
namespace Your\Namespace\Goes\Here;
use Mensbeam\Foundation\Catcher\Handler,
use MensBeam\Foundation\Catcher\Handler,
Symfony\Component\Yaml\Yaml;
@ -389,7 +412,7 @@ class YamlHandler extends Handler {
This theoretical class uses the [`symfony/yaml`][c] package in Composer and exposes a few of its options as options for the Handler. Using this class would be very similar to the examples provided at the beginning of this document:
```php
use Mensbeam\Foundation\Catcher,
use MensBeam\Foundation\Catcher,
Your\Namespace\Goes\Here\YamlHandler;
$catcher = new Catcher(new YamlHandler([

3
composer.json

@ -19,7 +19,8 @@
"psr/log": "^3.0"
},
"suggest": {
"ext-dom": "For HTMLHandler"
"ext-dom": "For HTMLHandler",
"ext-pcntl": "For allowing catching of notices, warnings, etc."
},
"require-dev": {
"ext-dom": "*",

78
composer.lock

@ -60,30 +60,30 @@
"packages-dev": [
{
"name": "doctrine/instantiator",
"version": "1.4.1",
"version": "1.5.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/instantiator.git",
"reference": "10dcfce151b967d20fde1b34ae6640712c3891bc"
"reference": "0a0fa9780f5d4e507415a065172d26a98d02047b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc",
"reference": "10dcfce151b967d20fde1b34ae6640712c3891bc",
"url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b",
"reference": "0a0fa9780f5d4e507415a065172d26a98d02047b",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"doctrine/coding-standard": "^9",
"doctrine/coding-standard": "^9 || ^11",
"ext-pdo": "*",
"ext-phar": "*",
"phpbench/phpbench": "^0.16 || ^1",
"phpstan/phpstan": "^1.4",
"phpstan/phpstan-phpunit": "^1",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
"vimeo/psalm": "^4.22"
"vimeo/psalm": "^4.30 || ^5.4"
},
"type": "library",
"autoload": {
@ -110,7 +110,7 @@
],
"support": {
"issues": "https://github.com/doctrine/instantiator/issues",
"source": "https://github.com/doctrine/instantiator/tree/1.4.1"
"source": "https://github.com/doctrine/instantiator/tree/1.5.0"
},
"funding": [
{
@ -126,7 +126,7 @@
"type": "tidelift"
}
],
"time": "2022-03-03T08:28:38+00:00"
"time": "2022-12-30T00:15:36+00:00"
},
{
"name": "eloquent/phony",
@ -617,16 +617,16 @@
},
{
"name": "nikic/php-parser",
"version": "v4.15.1",
"version": "v4.15.2",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
"reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900"
"reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/0ef6c55a3f47f89d7a374e6f835197a0b5fcf900",
"reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc",
"reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc",
"shasum": ""
},
"require": {
@ -667,9 +667,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v4.15.1"
"source": "https://github.com/nikic/PHP-Parser/tree/v4.15.2"
},
"time": "2022-09-04T07:30:47+00:00"
"time": "2022-11-12T15:38:23+00:00"
},
{
"name": "phar-io/manifest",
@ -784,16 +784,16 @@
},
{
"name": "phpunit/php-code-coverage",
"version": "9.2.18",
"version": "9.2.23",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "12fddc491826940cf9b7e88ad9664cf51f0f6d0a"
"reference": "9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/12fddc491826940cf9b7e88ad9664cf51f0f6d0a",
"reference": "12fddc491826940cf9b7e88ad9664cf51f0f6d0a",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c",
"reference": "9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c",
"shasum": ""
},
"require": {
@ -849,7 +849,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.18"
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.23"
},
"funding": [
{
@ -857,7 +857,7 @@
"type": "github"
}
],
"time": "2022-10-27T13:35:33+00:00"
"time": "2022-12-28T12:41:10+00:00"
},
{
"name": "phpunit/php-file-iterator",
@ -1102,16 +1102,16 @@
},
{
"name": "phpunit/phpunit",
"version": "9.5.26",
"version": "9.5.27",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "851867efcbb6a1b992ec515c71cdcf20d895e9d2"
"reference": "a2bc7ffdca99f92d959b3f2270529334030bba38"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/851867efcbb6a1b992ec515c71cdcf20d895e9d2",
"reference": "851867efcbb6a1b992ec515c71cdcf20d895e9d2",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a2bc7ffdca99f92d959b3f2270529334030bba38",
"reference": "a2bc7ffdca99f92d959b3f2270529334030bba38",
"shasum": ""
},
"require": {
@ -1184,7 +1184,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.26"
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.27"
},
"funding": [
{
@ -1200,7 +1200,7 @@
"type": "tidelift"
}
],
"time": "2022-10-28T06:00:21+00:00"
"time": "2022-12-09T07:31:23+00:00"
},
{
"name": "psr/http-message",
@ -2221,16 +2221,16 @@
},
{
"name": "symfony/css-selector",
"version": "v5.4.11",
"version": "v5.4.17",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
"reference": "c1681789f059ab756001052164726ae88512ae3d"
"reference": "052ef49b660f9ad2a3adb311c555c9bc11ba61f4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/c1681789f059ab756001052164726ae88512ae3d",
"reference": "c1681789f059ab756001052164726ae88512ae3d",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/052ef49b660f9ad2a3adb311c555c9bc11ba61f4",
"reference": "052ef49b660f9ad2a3adb311c555c9bc11ba61f4",
"shasum": ""
},
"require": {
@ -2267,7 +2267,7 @@
"description": "Converts CSS selectors to XPath expressions",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/css-selector/tree/v5.4.11"
"source": "https://github.com/symfony/css-selector/tree/v5.4.17"
},
"funding": [
{
@ -2283,7 +2283,7 @@
"type": "tidelift"
}
],
"time": "2022-06-27T16:58:25+00:00"
"time": "2022-12-23T11:40:44+00:00"
},
{
"name": "symfony/polyfill-ctype",
@ -2369,16 +2369,16 @@
},
{
"name": "symfony/polyfill-php80",
"version": "v1.26.0",
"version": "v1.27.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace"
"reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
"reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
"shasum": ""
},
"require": {
@ -2387,7 +2387,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.26-dev"
"dev-main": "1.27-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -2432,7 +2432,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0"
"source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0"
},
"funding": [
{
@ -2448,7 +2448,7 @@
"type": "tidelift"
}
],
"time": "2022-05-10T07:21:04+00:00"
"time": "2022-11-03T14:55:06+00:00"
},
{
"name": "symfony/yaml",

32
lib/Catcher.php

@ -16,6 +16,8 @@ use MensBeam\Foundation\Catcher\{
class Catcher {
/** Fork when throwing non-exiting errors, if available */
public bool $forking = true;
/** When set to true Catcher won't exit when instructed */
public bool $preventExit = false;
/** When set to true Catcher will throw errors as throwables */
@ -158,11 +160,35 @@ class Catcher {
if ($code !== 0 && error_reporting()) {
$error = new Error($message, $code, $file, $line);
if ($this->throwErrors) {
throw $error;
} else {
$this->handleThrowable($error);
// The point of this library is to allow treating of errors as if they were
// exceptions but instead have things like warnings, notices, etc. not stop
// execution. You normally can't have it both ways. So, what's going on here is
// that if the error wouldn't normally stop execution the newly-created Error
// throwable is thrown in a fork instead, allowing execution to resume in the
// parent process.
if (
$this->forking &&
\PHP_SAPI === 'cli' &&
function_exists('pcntl_fork') &&
!in_array($code, [ \E_ERROR, \E_PARSE, \E_CORE_ERROR, \E_COMPILE_ERROR, \E_USER_ERROR ])
) {
$pid = pcntl_fork();
if ($pid === -1) {
// This can't be covered unless I can somehow fake a misconfigured system
throw new \Exception(message: 'Could not create fork to throw Error', previous: $error); // @codeCoverageIgnore
} elseif (!$pid) {
// This can't be covered because it happens in the fork
throw $error; // @codeCoverageIgnore
}
pcntl_wait($status);
return true;
} else {
throw $error;
}
}
$this->handleThrowable($error);
return true;
}

7
tests/cases/TestCatcher.php

@ -378,6 +378,13 @@ class TestCatcher extends \PHPUnit\Framework\TestCase {
$h3->dispatch->called();
$c->throwErrors = true;
try {
trigger_error('Ook!', \E_USER_WARNING);
} catch (\Throwable $t) {
$this->assertInstanceOf(Error::class, $t);
$this->assertSame(\E_USER_WARNING, $t->getCode());
}
try {
trigger_error('Ook!', \E_USER_ERROR);
} catch (\Throwable $t) {

17
tests/cases/TestPlainTextHandler.php

@ -53,6 +53,23 @@ class TestPlainTextHandler extends \PHPUnit\Framework\TestCase {
ob_end_clean();
$l->critical->called();
$c = new ThrowableController(new \Exception(message: 'Ook!', previous: new \Error(message: 'Eek!', previous: new Error(message: 'Ack!', code: \E_USER_ERROR))));
$l = Phony::mock(LoggerInterface::class);
$h = new PlainTextHandler([
'logger' => $l->get(),
'silent' => true
]);
$o = $h->handle($c);
$this->assertSame(Handler::CONTINUE, $o['controlCode']);
$this->assertSame(Handler::SILENT | Handler::NOW, $o['outputCode']);
$this->assertTrue(isset($o['previous']));
ob_start();
$h->dispatch();
ob_end_clean();
$l->critical->called();
}

Loading…
Cancel
Save