Browse Source

Initial commit

main
Dustin Wilson 6 months ago
commit
7e912be190
  1. 77
      .gitignore
  2. 4
      AUTHORS
  3. 22
      LICENSE
  4. 87
      README.md
  5. 32
      composer.json
  6. 2764
      composer.lock
  7. 66
      lib/AWSSNSHandler.php
  8. 38
      test
  9. 18
      tests/bootstrap.php
  10. 126
      tests/cases/TestAWSSNSHandler.php
  11. 11
      tests/lib/Error.php
  12. 31
      tests/lib/ErrorHandlingTestCase.php
  13. 29
      tests/lib/FailLogger.php
  14. 66
      tests/lib/TestingHandler.php
  15. 22
      tests/phpunit.xml

77
.gitignore

@ -0,0 +1,77 @@
# Catcher-specific
/test*.html
/test*.php
/test/
/test/*
# General
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
/vendor/
/vendor-bin/*/vendor
/tests/html5lib-tests
/tests/.phpunit.cache
/tests/coverage
cachegrind.out.*

4
AUTHORS

@ -0,0 +1,4 @@
Project leads
-------------
Dustin Wilson https://dustinwilson.com/
J. King https://jkingweb.ca/

22
LICENSE

@ -0,0 +1,22 @@
Copyright (c) 2022 Dustin Wilson, J. King
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.

87
README.md

@ -0,0 +1,87 @@
[a]: https://code.mensbeam.com/MensBeam/Catcher
[b]: https://packagist.org/packages/aws/aws-sdk-php
[c]: https://github.com/symfony/yaml
[d]: https://www.php.net/manual/en/function.pcntl-fork.php
[e]: https://www.php.net/manual/en/function.print-r.php
[f]: https://github.com/symfony/var-exporter
[g]: https://github.com/php-fig/log
# AWSSNSHandler #
_AWSSNSHandler_ is a Throwable handler for use in [_Catcher_][a], a Throwable and error handling library for PHP. It sends throwables and errors to Amazon SNS topics. Right now _AWSSNSHandler_ only supports sending to email topics. SMS messages aren't supported at this time.
## Requirements ##
* PHP >= 8.1
* [mensbeam/catcher][a] ^2.1.0
* [aws/aws-sdk-php][b] ^3.283
## Installation ##
```shell
composer require mensbeam/catcher-awssnshandler
```
## Usage ##
For most use cases this library requires no configuration and little effort to integrate into non-complex environments:
```php
use MensBeam\Catcher,
MensBeam\Catcher\AWSSNSHandler,
Aws\Sns\SnsClient;
$client = new SnsClient([
'version' => 'latest',
'region' => 'us-west-2',
'credentials' => [
'key' => 'AKIAFIMBZAFZZQL42RMH',
'secret' => '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'
]
]);
$catcher = new Catcher(new AWSSNSHandler($client, 'arn:aws:sns:us-west-2:701867229025:ook_eek'));
```
That's it. It will automatically register Catcher as an exception, error, and shutdown handler and use `AWSSNSHandler` as its sole handler. Like other _Catcher_ handlers, _AWSSNSHandler_ can be configured with a logger. When logging it behaves identically to _JSONHandler_. See the [_Catcher_][a] documentation for more info on how to configure a logger.
## Documentation ##
### MensBeam\Catcher\AWSSNSHandler ###
```php
namespace MensBeam\Catcher;
use Aws\Sns\SnsClient;
class AWSSNSHandler extends JSONHandler {
protected SnsClient $client;
protected string $topicARN;
public function __construct(SnsClient $client, string $topicARN, array $options = []);
public function getClient(): SnsClient;
public function setClient(SnsClient $client): void;
public function getTopicARN(): string;
public function setTopicARN(string $topicARN): void;
}
```
#### MensBeam\Catcher\AWSSNSHandler::getClient ####
Returns the `Aws\Sns\SnsClient` the handler uses
#### MensBeam\Catcher\AWSSNSHandler::getTopicARN ####
Returns the AWS SNS topic ARN the handler sends messages to
#### MensBeam\Catcher\AWSSNSHandler::setClient ####
Replaces the `Aws\Sns\SnsClient` the handler uses with one specified
#### MensBeam\Catcher\AWSSNSHandler::setTopicARN ####
Replaces the AWS SNS topic ARN the handler sends messages to with one specified

32
composer.json

@ -0,0 +1,32 @@
{
"name": "mensbeam/catcher-awsnshandler",
"description": "Amazon AWS SNS Message Handler for MensBeam's Catcher",
"type": "library",
"license": "MIT",
"autoload": {
"psr-4": {
"MensBeam\\Catcher\\": "lib/"
}
},
"autoload-dev": {
"psr-4": {
"MensBeam\\Catcher\\AWSSNSHandler\\Test\\": "tests/lib/"
}
},
"authors": [
{
"name": "Dustin Wilson",
"email": "dustin@dustinwilson.com"
}
],
"require": {
"php": ">=8.1",
"mensbeam/catcher": "^2.1.2",
"aws/aws-sdk-php": "^3.283"
},
"require-dev": {
"phpunit/phpunit": "^10",
"phake/phake": "^4.4",
"psr/log": "^3.0"
}
}

2764
composer.lock

File diff suppressed because it is too large

66
lib/AWSSNSHandler.php

@ -0,0 +1,66 @@
<?php
/**
* @license MIT
* Copyright 2022 Dustin Wilson, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\Catcher;
use Aws\Sns\SnsClient;
class AWSSNSHandler extends JSONHandler {
protected SnsClient $client;
protected string $topicARN;
public function __construct(SnsClient $client, string $topicARN, array $options = []) {
$this->client = $client;
$this->topicARN = $topicARN;
parent::__construct($options);
}
public function getClient(): SnsClient {
return $this->client;
}
public function getTopicARN(): string {
return $this->topicARN;
}
public function setClient(SnsClient $client): void {
$this->client = $client;
}
public function setTopicARN(string $topicARN): void {
$this->topicARN = $topicARN;
}
protected function handleCallback(array $output): array {
return $output;
}
protected function invokeCallback(): void {
foreach ($this->outputBuffer as $o) {
if (($o['code'] & self::OUTPUT) === 0) {
if ($o['code'] & self::LOG) {
$this->serializeOutputThrowable($o);
}
continue;
}
$this->client->publish([
'Message' => $this->serializeOutputThrowable($o),
'TopicArn' => $this->topicARN
]);
}
}
}

38
test

@ -0,0 +1,38 @@
#!/usr/bin/env php
<?php
/**
* @license MIT
* Copyright 2022 Dustin Wilson, et al.
* See LICENSE and AUTHORS files for details
*/
$dir = ini_get('extension_dir');
$php = escapeshellarg(\PHP_BINARY);
$code = escapeshellarg(__DIR__ . '/lib');
array_shift($argv);
foreach ($argv as $k => $v) {
if (in_array($v, ['--coverage', '--coverage-html'])) {
$argv[$k] = '--coverage-html tests/coverage';
}
}
$cmd = [
$php,
'-d opcache.enable_cli=0',
];
if (!extension_loaded('xdebug')) {
$cmd[] = '-d zend_extension=xdebug.so';
}
$cmd = implode(' ', [
...$cmd,
'-d xdebug.mode=coverage,develop,trace',
escapeshellarg(__DIR__ . '/vendor/bin/phpunit'),
'--configuration tests/phpunit.xml',
...$argv,
'--display-deprecations'
]);
passthru($cmd);

18
tests/bootstrap.php

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace MensBeam\Catcher\Test;
ini_set('memory_limit', '2G');
ini_set('zend.assertions', '1');
ini_set('assert.exception', 'true');
error_reporting(\E_ALL);
define('CWD', dirname(__DIR__));
require_once CWD . '/vendor/autoload.php';
if (function_exists('xdebug_set_filter')) {
if (defined('XDEBUG_PATH_INCLUDE')) {
xdebug_set_filter(\XDEBUG_FILTER_CODE_COVERAGE, \XDEBUG_PATH_INCLUDE, [ CWD . '/lib/' ]);
} else {
xdebug_set_filter(\XDEBUG_FILTER_CODE_COVERAGE, \XDEBUG_PATH_WHITELIST, [ CWD . '/lib/' ]);
}
}

126
tests/cases/TestAWSSNSHandler.php

@ -0,0 +1,126 @@
<?php
/**
* @license MIT
* Copyright 2022 Dustin Wilson, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\Catcher\Test;
use MensBeam\Catcher\{
Error,
Handler,
ThrowableController,
AWSSNSHandler
};
use Aws\Sns\SnsClient,
Psr\Log\LoggerInterface,
Phake;
/** @covers \MensBeam\Catcher\AWSSNSHandler */
class TestAWSSNSHandler extends \PHPUnit\Framework\TestCase {
protected ?SnsClient $client = null;
protected ?Handler $handler = null;
public function setUp(): void {
parent::setUp();
$this->client = Phake::mock(SnsClient::class);
Phake::when($this->client)->publish()->thenReturn(true);
$this->handler = new AWSSNSHandler($this->client, 'ook', [
'outputBacktrace' => true,
'silent' => true
]);
}
public function testGettingSettingProps() {
$c = $this->handler->getClient();
$this->assertTrue($c instanceof SnsClient);
$c2 = Phake::mock(SnsClient::class);
Phake::when($c2)->publish()->thenReturn(true);
$this->handler->setClient($c2);
$this->assertNotEquals($c, $this->handler->getClient());
$this->assertTrue($this->handler->getClient() instanceof SnsClient);
$this->assertSame('ook', $this->handler->getTopicARN());
$this->handler->setTopicARN('eek');
$this->assertSame('eek', $this->handler->getTopicARN());
}
/** @dataProvider provideInvocationTests */
public function testInvocation(\Throwable $throwable, bool $silent, bool $log, ?string $logMethodName, ?array $ignore, int $line): void {
$this->handler->setOption('outputToStderr', false);
if (!$silent) {
$this->handler->setOption('silent', false);
}
if ($log) {
$l = Phake::mock(LoggerInterface::class);
$this->handler->setOption('logger', $l);
}
if ($ignore !== null) {
$this->handler->setOption('ignore', $ignore);
}
$o = $this->handler->handle(new ThrowableController($throwable));
$c = $o['class'] ?? null;
$h = $this->handler;
$h();
$u = $h->getLastOutputThrowable();
if ($ignore === null) {
$this->assertNotNull($u);
$this->assertEquals($c, $u['class']);
$this->assertEquals(__FILE__, $u['file']);
$this->assertEquals($line, $u['line']);
if (!$silent) {
Phake::verify($this->client, Phake::times(1))->publish;
}
} else {
$this->assertNull($u);
}
if ($log) {
Phake::verify($l, Phake::times((int)(count($ignore ?? []) === 0)))->$logMethodName;
}
}
public static function provideHandlingTests(): iterable {
$options = [
[ new \Exception('Ook!'), Handler::BUBBLES | Handler::EXIT, [ 'forceExit' => true ] ],
[ new \Error('Ook!'), Handler::BUBBLES ],
[ new Error('Ook!', \E_ERROR, '/dev/null', 42, new \Error('Eek!')), Handler::BUBBLES | Handler::NOW, [ 'forceOutputNow' => true ] ],
[ new \Exception('Ook!'), Handler::BUBBLES, [ 'logger' => Phake::mock(LoggerInterface::class), 'logWhenSilent' => false ] ],
[ new \Error('Ook!'), Handler::BUBBLES | Handler::LOG, [ 'forceOutputNow' => true, 'logger' => Phake::mock(LoggerInterface::class) ] ]
];
foreach ($options as $o) {
$o[1] |= Handler::NOW;
yield $o;
}
}
public static function provideInvocationTests(): iterable {
$options = [
[ new \Exception('Ook!'), false, true, 'critical', null ],
[ new \Exception('Ook!'), false, true, 'critical', [ \Exception::class ] ],
[ new \Error('Ook!'), true, false, null, null ],
[ new \Error('Ook!'), true, false, null, [ \Error::class ] ],
[ new Error('Ook!', \E_ERROR, __FILE__, __LINE__), false, true, 'error', null ],
[ new Error('Ook!', \E_ERROR, __FILE__, __LINE__), false, true, 'error', [ \E_ERROR ] ],
[ new \Exception(message: 'Ook!', previous: new \Error(message: 'Eek!', previous: new \ParseError('Ack!'))), true, true, 'critical', null ],
[ new \Exception(message: 'Ook!', previous: new \Error(message: 'Eek!', previous: new \ParseError('Ack!'))), true, true, 'critical', [ \Exception::class ] ]
];
$l = count($options);
foreach ($options as $k => $o) {
yield [ ...$o, __LINE__ - 4 - $l + $k ];
}
}
}

11
tests/lib/Error.php

@ -0,0 +1,11 @@
<?php
/**
* @license MIT
* Copyright 2022 Dustin Wilson, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\Catcher\Test;
class Error extends \Error {}

31
tests/lib/ErrorHandlingTestCase.php

@ -0,0 +1,31 @@
<?php
/**
* @license MIT
* Copyright 2022 Dustin Wilson, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\Catcher\Test;
class ErrorHandlingTestCase extends \PHPUnit\Framework\TestCase {
protected ?Error $lastError = null;
public function setUp(): void {
set_error_handler([ $this, 'handleError' ]);
}
public function tearDown(): void {
restore_error_handler();
}
public function handleError(int $code, string $message, string $file, int $line): void {
$e = new Error($message, $code);
$this->lastError = $e;
if (in_array($code, [ \E_ERROR, \E_PARSE, \E_CORE_ERROR, \E_COMPILE_ERROR, \E_USER_ERROR, \E_RECOVERABLE_ERROR ])) {
throw $e;
}
}
}

29
tests/lib/FailLogger.php

@ -0,0 +1,29 @@
<?php
/**
* @license MIT
* Copyright 2022 Dustin Wilson, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\Catcher\Test;
use org\bovigo\vfs\vfsStream,
Psr\Log\LoggerInterface,
Psr\Log\LoggerTrait;
class FailLogger implements LoggerInterface {
use LoggerTrait;
public function log($level, string|\Stringable $message, array $context = []): void {
$v = vfsStream::setup('ook');
$d = vfsStream::newDirectory('ook', 0777)->at($v);
file_put_contents($d->url(), $message);
echo "Ook!\n";
}
public function error(string|\Stringable $message, array $context = []): void {
trigger_error('Ook!', \E_USER_ERROR);
}
}

66
tests/lib/TestingHandler.php

@ -0,0 +1,66 @@
<?php
/**
* @license MIT
* Copyright 2022 Dustin Wilson, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\Catcher\Test;
use MensBeam\Catcher\Handler;
class TestingHandler extends Handler {
public array $output = [];
protected ?string $_name = null;
// Could just use silent option instead, but we need to test Handler::SILENT
protected bool $_print = false;
// When printing output, print it as JSON
protected bool $_printJSON = true;
protected function handleCallback(array $output): array {
$output['code'] = (\PHP_SAPI === 'cli') ? $output['code'] | self::NOW : $output['code'];
return $output;
}
protected function invokeCallback(): void {
foreach ($this->outputBuffer as $o) {
if ($o['code'] & self::LOG) {
$this->log($o['controller']->getThrowable(), json_encode([
'class' => $o['class'],
'code' => $o['code'],
'file' => $o['file'],
'line' => $o['line'],
'message' => $o['message']
]));
}
if (($o['code'] & self::OUTPUT) === 0) {
continue;
}
$o = $this->cleanOutputThrowable($o);
if ($this->_print) {
if (!$this->_printJSON) {
$oo = '';
foreach ($o['frames'] as $f) {
if (!isset($f['args'])) {
continue;
}
$oo .= $this->serializeArgs($f['args']) . "\n";
}
$oo = rtrim($oo);
} else {
$oo = json_encode($o, \JSON_THROW_ON_ERROR);
}
$this->print($oo);
}
$this->output[] = $o;
}
}
}

22
tests/phpunit.xml

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.2/phpunit.xsd"
beStrictAboutOutputDuringTests="true"
beStrictAboutTestsThatDoNotTestAnything="true"
bootstrap="bootstrap.php"
cacheDirectory=".phpunit.cache"
colors="true"
executionOrder="defects"
requireCoverageMetadata="true"
>
<testsuites>
<testsuite name="Main">
<directory prefix="Test" suffix=".php">./cases</directory>
</testsuite>
</testsuites>
<coverage/>
<source>
<include>
<directory suffix=".php">../lib</directory>
</include>
</source>
</phpunit>
Loading…
Cancel
Save