diff --git a/.gitignore b/.gitignore index 3dab634..92e81b9 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,10 @@ # Embedded web-server pid file /.web-server-pid + +# PhpStorm +/.idea + +###> friendsofphp/php-cs-fixer ### +/.php-cs-fixer.cache +###< friendsofphp/php-cs-fixer ### diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..94881a8 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,63 @@ +setUsingCache(true) + ->setCacheFile(__DIR__ . '/.php-cs-fixer.cache') + ->setRules([ + '@PSR2' => true, + 'align_multiline_comment' => true, + 'array_indentation' => true, + 'array_syntax' => ['syntax' => 'short'], + 'binary_operator_spaces' => true, + 'blank_line_before_statement' => ['statements' => ['return']], + 'cast_spaces' => true, + 'concat_space' => ['spacing' => 'one'], + 'constant_case' => true, + 'declare_equal_normalize' => true, + 'class_attributes_separation' => ['elements' => ['method' => 'one']], + 'lowercase_cast' => true, + 'method_argument_space' => true, + 'no_empty_statement' => true, + 'no_leading_import_slash' => true, + 'no_leading_namespace_whitespace' => true, + 'no_multiline_whitespace_around_double_arrow' => true, + 'no_unused_imports' => true, + 'no_whitespace_before_comma_in_array' => true, + 'no_whitespace_in_blank_line' => true, + 'multiline_whitespace_before_semicolons' => false, + 'ordered_imports' => true, + 'phpdoc_add_missing_param_annotation' => true, + 'phpdoc_align' => true, + 'phpdoc_annotation_without_dot' => true, + 'phpdoc_indent' => true, + 'phpdoc_no_empty_return' => true, + 'phpdoc_no_useless_inheritdoc' => true, + 'phpdoc_order' => true, + 'phpdoc_return_self_reference' => true, + 'phpdoc_scalar' => true, + 'phpdoc_separation' => true, + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_trim' => true, + 'phpdoc_trim_consecutive_blank_line_separation' => true, + 'phpdoc_types_order' => true, + 'phpdoc_var_annotation_correct_order' => true, + 'phpdoc_var_without_name' => true, + 'single_quote' => true, + 'short_scalar_cast' => true, + 'standardize_not_equals' => true, + 'ternary_operator_spaces' => true, + 'ternary_to_null_coalescing' => true, + 'trim_array_spaces' => true, + 'visibility_required' => ['elements' => ['property', 'method', 'const']], + 'yoda_style' => false, + ]) + ->setFinder( + PhpCsFixer\Finder::create() + ->in([__DIR__ . '/src']) + ->append([__FILE__]) + ->exclude([ + 'tests', + 'vendor', + 'var' + ]) + ); diff --git a/README.md b/README.md index 03d514e..731f313 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,109 @@ -# hawk.symfony -Symfony errors Catcher module for Hawk.so +# Hawk Symfony + +Symfony errors Catcher for [Hawk.so](https://hawk.so). + +## Setup + +1. [Register](https://garage.hawk.so/sign-up) an account, create a Project and get an Integration Token. + +2. Install SDK via [composer](https://getcomposer.org) to install the Catcher + +Catcher provides support for PHP 7.2 or later + +```bash +$ composer require codex-team/hawk.symfony +``` + +### Configuration + +Add the following authorization information to your `.env` file: + +```env +HAWK_TOKEN= +``` + +Create a configuration file at `config/packages/hawk.yaml` with the following content: + +```php +HawkBundle\HawkBundle::class => ['all' => true] +``` + +In the `config/packages/monolog.yaml` file, specify the handler settings under the appropriate section (`dev` or `prod`): + +```yaml +hawk: + type: service + id: HawkBundle\Monolog\Handler + level: error +``` + +### Adding User Information to Error Reports: + +```php +$this->catcher->setUser([ + 'name' => 'user name', + 'photo' => 'user photo', +]); + +$this->catcher->setContext([ + // Additional context information +]); +``` + +### Sending Exceptions Manually: +To manually send exceptions, initialize `__construct(\HawkBundle\Catcher $catcher)` class via dependency injection (DI), and use the following method: + +```php +$this->catcher->sendException($exception); +``` + +### Sending Custom Messages: + +You can also send custom messages using the `->sendMessage(...)` method: + +```php +$this->catcher->sendMessage( + 'your message', + [ + // Additional context information + ] +); +``` + +### Example: Sending Manually + +```php +private $catcher; + +public function __construct(\HawkBundle\Catcher $catcher) +{ + $this->catcher = $catcher; +} + +public function test() +{ + try { + // The code where you need to catch the error + } catch (\Exception $exception) { + $this->catcher->sendException($exception); + } +} +``` + +## Issues and improvements + +Feel free to ask questions or improve the project. + +## Links + +Repository: https://github.com/codex-team/hawk.symfony + +Report a bug: https://github.com/codex-team/hawk.symfony/issues + +Composer Package: https://packagist.org/packages/codex-team/hawk.symfony + +CodeX Team: https://codex.so + +## License + +MIT diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..1c1acc8 --- /dev/null +++ b/composer.json @@ -0,0 +1,34 @@ +{ + "name": "codex-team/hawk.symfony", + "description": "Symfony errors Catcher module for Hawk.so", + "keywords": ["hawk", "php", "error", "catcher", "monolog", "symfony"], + "type": "library", + "version": "0.0.1", + "license": "MIT", + "require": { + "php": "^7.2 || ^8.0", + "monolog/monolog": "^2.2 || ^3.0", + "guzzlehttp/guzzle": "^6.0 || ^7.0", + "codex-team/hawk.php": "^2.2.2", + "jean85/pretty-package-versions": "^1.5 || ^2.0", + "symfony/config": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/dependency-injection": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/http-kernel": "^4.4.20||^5.0.11||^6.0||^7.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.19||^3.40" + }, + "autoload": { + "psr-4": { + "HawkBundle\\": "src/" + } + }, + "extra": { + "symfony": { + "autoload": true + } + }, + "scripts": { + "csfix": "vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --using-cache=no --verbose" + } +} diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..02e1c19 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/Catcher.php b/src/Catcher.php new file mode 100644 index 0000000..fb9e55b --- /dev/null +++ b/src/Catcher.php @@ -0,0 +1,110 @@ +registerAddon(new Headers()); + + $this->handler = new Handler($options, $transport, $builder); + + $this->handler->registerErrorHandler(); + $this->handler->registerExceptionHandler(); + $this->handler->registerFatalHandler(); + } + + /** + * @param array $user + * + * @return $this + */ + public function setUser(array $user): self + { + $this->handler->setUser($user); + + return $this; + } + + /** + * @param array $context + * + * @return $this + */ + public function setContext(array $context): self + { + $this->handler->setContext($context); + + return $this; + } + + /** + * @param string $message + * @param array $context + */ + public function sendMessage(string $message, array $context = []): void + { + $this->handler->sendEvent([ + 'title' => $message, + 'context' => $context + ]); + } + + /** + * @param \Throwable $throwable + * @param array $context + */ + public function sendException(\Throwable $throwable, array $context = []) + { + $this->handler->handleException($throwable, $context); + } + + /** + * @param array $payload + */ + public function sendEvent(array $payload): void + { + $this->handler->sendEvent($payload); + } +} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php new file mode 100644 index 0000000..067e4fe --- /dev/null +++ b/src/DependencyInjection/Configuration.php @@ -0,0 +1,29 @@ +getRootNode(); + + $rootNode + ->children() + ->scalarNode('integration_token') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->end() + ; + + return $treeBuilder; + } +} diff --git a/src/DependencyInjection/HawkExtension.php b/src/DependencyInjection/HawkExtension.php new file mode 100644 index 0000000..bb82d2b --- /dev/null +++ b/src/DependencyInjection/HawkExtension.php @@ -0,0 +1,50 @@ +processConfiguration($configuration, $configs); + + // Load configuration files + $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); + $loader->load('services.yaml'); + + // Set parameters and register services + $container->setParameter('hawk.integration_token', $config['integration_token']); + + // Register TransportInterface + $container->register(GuzzlePromisesTransport::class) + ->setArgument('$client', new Reference(Client::class)); + + // Register Catcher + $container->register(Catcher::class) + ->setArgument('$options', ['integrationToken' => $config['integration_token']]) + ->setArgument('$transport', new Reference(GuzzlePromisesTransport::class)); + + // Register Monolog\Handler + $container->register(Handler::class) + ->setArgument('$catcher', new Reference(Catcher::class)) + ->setArgument('$level', Logger::ERROR) + ->addTag('monolog.handler'); + } +} diff --git a/src/HawkBundle.php b/src/HawkBundle.php new file mode 100644 index 0000000..2b79644 --- /dev/null +++ b/src/HawkBundle.php @@ -0,0 +1,17 @@ +catcher = $catcher; + } + + public function doWrite($record): void + { + $data = [ + 'level' => $record['level'], + 'title' => (new LineFormatter('%message%'))->format($record) + ]; + + if (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Throwable) { + $data['exception'] = $record['context']['exception']; + } + + $this->catcher->sendEvent($data); + } +} diff --git a/src/Monolog/HandlerTrait.php b/src/Monolog/HandlerTrait.php new file mode 100644 index 0000000..f6e4514 --- /dev/null +++ b/src/Monolog/HandlerTrait.php @@ -0,0 +1,34 @@ += 3) { + trait HandlerTrait + { + /** + * @param array|LogRecord $record + */ + abstract protected function doWrite($record): void; + + protected function write(LogRecord $record): void + { + $this->doWrite($record); + } + } +} else { + trait HandlerTrait + { + /** + * @param array|LogRecord $record + */ + abstract protected function doWrite($record): void; + + protected function write(array $record): void + { + $this->doWrite($record); + } + } +} diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml new file mode 100644 index 0000000..96be3ce --- /dev/null +++ b/src/Resources/config/services.yaml @@ -0,0 +1,7 @@ +services: + HawkBundle\Monolog\Handler: + public: false + arguments: + $level: 'error' + tags: + - { name: monolog.handler } \ No newline at end of file diff --git a/src/Transport/GuzzlePromisesTransport.php b/src/Transport/GuzzlePromisesTransport.php new file mode 100644 index 0000000..187fdf0 --- /dev/null +++ b/src/Transport/GuzzlePromisesTransport.php @@ -0,0 +1,62 @@ +client = $client; + } + + /** + * @inheritDoc + */ + public function getUrl(): string + { + return (new Options())->getUrl(); + } + + /** + * @inheritDoc + */ + public function send(Event $event) + { + $promise = $this->client->postAsync($this->getUrl(), [ + 'json' => $event->jsonSerialize(), + 'headers' => [ + 'Content-Type' => 'application/json' + ], + 'timeout' => 10 + ]); + + return $promise->then( + function ($response) { + return $response->getBody()->getContents(); + }, + function (RequestException $e) { + throw new \Exception('Failed to send event: ' . $e->getMessage(), 0, $e); + } + )->wait(); + } +}