├── .coveralls.yml ├── .github └── workflows │ └── php.yml ├── .gitignore ├── Dockerfile ├── Entrypoint.sh ├── README.md ├── Resources ├── doc │ ├── changlelog.md │ ├── full_config.md │ ├── kohana3_integration.md │ └── symfony2_integration.md └── xsd │ ├── XSD.xml │ ├── airbrake_2_2.xml │ └── hoptoad_2_0.xsd ├── build └── logs │ └── .gitkeep ├── composer.json ├── docker-compose.yml ├── phpunit.xml.dist ├── psalm.xml ├── src └── Errbit │ ├── Errbit.php │ ├── Errors │ ├── BaseError.php │ ├── Error.php │ ├── ErrorInterface.php │ ├── Fatal.php │ ├── Notice.php │ └── Warning.php │ ├── Exception │ ├── ConfigurationException.php │ ├── Exception.php │ └── Notice.php │ ├── Handlers │ └── ErrorHandlers.php │ ├── Utils │ ├── Converter.php │ └── XmlBuilder.php │ └── Writer │ ├── AbstractWriter.php │ ├── GuzzleWriter.php │ ├── SocketWriter.php │ └── WriterInterface.php └── tests ├── Integration └── Errbit │ └── Tests │ └── IntegrationTest.php └── Unit └── Errbit └── Tests ├── ErrbitTest.php ├── Errors ├── ErrorTest.php ├── FatalTest.php ├── NoticeTest.php └── WarningTest.php ├── Exception ├── ExceptionTest.php └── NoticeTest.php ├── Handlers └── ErrorHandlersTest.php ├── Utils ├── ConverterTest.php └── XmlBuilderTest.php ├── Writer └── GuzzleWriterTest.php └── bootstrap.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | # .coveralls.yml example configuration 2 | 3 | coverage_clover: build/logs/clover.xml 4 | json_path: build/logs/coveralls-upload.json 5 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: PHP Composer 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ${{ matrix.operating-system }} 15 | strategy: 16 | matrix: 17 | operating-system: [ 'ubuntu-latest', 'windows-latest', 'macos-latest' ] 18 | php-versions: [ '8.1', '8.2', '8.3' ] 19 | phpunit-versions: [ 'latest' ] 20 | include: 21 | - operating-system: 'ubuntu-latest' 22 | php-versions: '8.0' 23 | phpunit-versions: 9 24 | steps: 25 | - name: Setup PHP 26 | uses: shivammathur/setup-php@v2 27 | with: 28 | php-version: ${{ matrix.php-versions }} 29 | extensions: mbstring, intl 30 | ini-values: post_max_size=256M, max_execution_time=180 31 | coverage: xdebug 32 | tools: php-cs-fixer, phpunit:${{ matrix.phpunit-versions }} 33 | - uses: actions/checkout@v3 34 | - name: Check PHP Version 35 | run: php -v 36 | - name: Validate composer.json and composer.lock 37 | run: composer validate --strict 38 | - name: Cache Composer packages 39 | id: composer-cache 40 | uses: actions/cache@v3 41 | with: 42 | path: vendor 43 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 44 | restore-keys: | 45 | ${{ runner.os }}-php- 46 | - name: Install dependencies 47 | run: composer install --prefer-dist --no-progress 48 | 49 | # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit" 50 | # Docs: https://getcomposer.org/doc/articles/scripts.md 51 | - name: Run test suite 52 | run: composer run-script test 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/* 2 | bin 3 | .buildpath 4 | .settings 5 | .project 6 | test_result 7 | build 8 | results 9 | composer.phar 10 | composer.lock 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.1-fpm-alpine 2 | 3 | RUN apk add --update \ 4 | git \ 5 | autoconf \ 6 | libtool \ 7 | bash \ 8 | g++ \ 9 | vim \ 10 | make 11 | 12 | RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer 13 | 14 | # @see https://hub.docker.com/_/php 15 | 16 | ARG php_ini_dir="/usr/local/etc/php" 17 | 18 | RUN mv "$php_ini_dir/php.ini-production" "$PHP_INI_DIR/php.ini" 19 | 20 | # @see https://xdebug.org/docs/compat 21 | # Xdebug 3.1 22 | 23 | ARG xdebug_client_host="127.0.0.1" 24 | ARG xdebug_client_port=9003 25 | 26 | RUN echo $php_ini_dir 27 | 28 | RUN apk update 29 | RUN apk add --upgrade php81-pecl-xdebug \ 30 | && echo "zend_extension=/usr/lib/php81/modules/xdebug.so" > $php_ini_dir/conf.d/99-xdebug.ini \ 31 | && echo "xdebug.client_port=$xdebug_client_port" >> $php_ini_dir/conf.d/99-xdebug.ini \ 32 | && echo "xdebug.client_host=$xdebug_client_host" >> $php_ini_dir/conf.d/99-xdebug.ini \ 33 | && echo "xdebug.mode=debug,develop,coverage" >> $php_ini_dir/conf.d/99-xdebug.ini 34 | 35 | WORKDIR /app 36 | -------------------------------------------------------------------------------- /Entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | composer config -g http-basic.gitlab.com gitlab-ci-token ${GITLAB_TOKEN} 4 | composer install \ 5 | --working-dir /app \ 6 | --prefer-dist 7 | 8 | php-fpm 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Errbit & Airbrake Client for PHP 2 | 3 | 4 | [![Coverage Status](https://coveralls.io/repos/emgiezet/errbitPHP/badge.png)](https://coveralls.io/r/emgiezet/errbitPHP) 5 | [![Build Status](https://travis-ci.org/emgiezet/errbitPHP.png?branch=master)](https://travis-ci.org/emgiezet/errbitPHP) 6 | [![Dependency Status](https://www.versioneye.com/user/projects/5249e725632bac0a4900b2bf/badge.png)](https://www.versioneye.com/user/projects/5249e725632bac0a4900b2bf) 7 | [![Latest Stable Version](https://poser.pugx.org/emgiezet/errbit-php/v/stable.png)](https://packagist.org/packages/emgiezet/errbit-php) 8 | [![SymfonyInsight](https://insight.symfony.com/projects/a0c405fb-8ee9-40e9-acf1-eee084fc35a6/mini.svg)](https://insight.symfony.com/projects/a0c405fb-8ee9-40e9-acf1-eee084fc35a6) 9 | [![Join the chat at https://gitter.im/emgiezet/errbitPHP](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/emgiezet/errbitPHP?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 10 | 11 | This is a full-featured client to add integration with [Errbit](https://github.com/errbit/errbit) (or Airbrake) 12 | to any PHP 8.0 and 8.1 application. 13 | 14 | Original idea and source has no support for php namespaces. 15 | Moreover it has a bug and with newest errbit version the xml has not supported chars. 16 | 17 | 18 | ## What is for? 19 | Handling your errors and passing them to the Error Retention tool called [Errbit](https://github.com/errbit/errbit). It's a free alternative of sentry.io or airbrake.io. 20 | Check the presentation below! 21 | 22 | [![Huston whe have an Airbrake](http://image.slidesharecdn.com/hustonwehaveanairbrake-131125152637-phpapp02/95/slide-1-638.jpg?1385415083)](http://www.slideshare.net/MaxMaecki/meetphp-11-huston-we-have-an-airbrake) 23 | 24 | ## ChangeLog 25 | Check the: 26 | 27 | [![Full change log here](Resources/doc/changlelog.md)] 28 | [![Releases](https://github.com/emgiezet/errbitPHP/releases)] 29 | 30 | ## Installation 31 | 32 | ### Composer Way 33 | For php 5.3 34 | ```json 35 | require: { 36 | ... 37 | "emgiezet/errbit-php": "1.*" 38 | } 39 | ``` 40 | For php 8.0+ 41 | ```json 42 | require: { 43 | ... 44 | "emgiezet/errbit-php": "2.*" 45 | } 46 | ``` 47 | 48 | ## Usage 49 | 50 | To setup an Errbit instance you need to configure it with an array of parameters. 51 | Only two of them are mandatory. 52 | 53 | ``` php 54 | use Errbit\Errbit; 55 | 56 | Errbit::instance() 57 | ->configure(array( 58 | 'api_key' => 'YOUR API KEY', 59 | 'host' => 'YOUR ERRBIT HOST, OR api.airbrake.io FOR AIRBRAKE' 60 | )) 61 | ->start(); 62 | ``` 63 | 64 | View the [full configuration](https://github.com/emgiezet/errbitPHP/blob/master/Resources/doc/full_config.md). 65 | 66 | This will register error handlers: 67 | 68 | ``` php 69 | set_error_handler(); 70 | set_exception_handler(); 71 | register_shutdown_function(); 72 | ``` 73 | 74 | And log all the errors intercepted by handlers to your errbit. 75 | 76 | If you want to notify an exception manually, you can call `notify()` without calling a `start()`. That way you can avoid registering the handlers. 77 | 78 | ``` php 79 | use Errbit\Errbit; 80 | 81 | try { 82 | somethingErrorProne(); 83 | } catch (Exception $e) { 84 | Errbit::instance()->notify( 85 | $e, 86 | array('controller'=>'UsersController', 'action'=>'show') 87 | ); 88 | } 89 | ``` 90 | 91 | ## Using your own error handler 92 | 93 | If you don't want Errbit to install its own error handlers and prefer to use 94 | your own, you can just leave out the call to `start()`, then wherever you 95 | catch an Exception (note the errors *must* be converted to Exceptions), simply 96 | call 97 | 98 | ``` php 99 | use Errbit\Errbit; 100 | Errbit::instance()->notify($exception); 101 | ``` 102 | 103 | With this type of use. Library will not handle the errors collected by: 104 | 105 | ``` php 106 | set_error_handler(); 107 | register_shutdown_function(); 108 | ``` 109 | 110 | ## Using only some of the default handlers 111 | 112 | There are three error handlers installed by Errbit: exception, error and fatal. 113 | 114 | By default all three are used. If you want to use your own for some handlers, 115 | but not for others, pass the list into the `start()` method. 116 | 117 | ``` php 118 | use Errbit\Errbit; 119 | Errbit::instance()->start(array('error', 'fatal')); // using our own exception handler 120 | ``` 121 | 122 | ## Symfony2 Integration 123 | 124 | See the [documentation](https://github.com/emgiezet/errbitPHP/blob/master/Resources/doc/symfony2_integration.md) for symfony2 integration. 125 | 126 | ## Kohana 3.3 Integration 127 | 128 | check out the [kohana-errbit](https://github.com/kwn/kohana-errbit) for kohana 3.3 integration. 129 | 130 | ## Symfony 1.4 Integration 131 | 132 | No namespaces in php 5.2 so this library can't be used. 133 | Go to [filipc/sfErrbitPlugin](https://github.com/filipc/sfErrbitPlugin) and monitor your legacy 1.4 applications. 134 | 135 | 136 | 137 | ## License & Copyright 138 | 139 | Copyright © mmx3.pl 2013 Licensed under the MIT license. Based on idea of git://github.com/flippa/errbit-php.git but rewritten in 90%. 140 | 141 | ## Contributors 142 | 143 | https://github.com/emgiezet/errbitPHP/graphs/contributors 144 | 145 | Rest of the contributors: 146 | Author: [emgiezet](https://github.com/emgiezet/) 147 | [Contributors page](https://github.com/emgiezet/errbitPHP/graphs/contributors) 148 | -------------------------------------------------------------------------------- /Resources/doc/changlelog.md: -------------------------------------------------------------------------------- 1 | # v2.0.0 2 | ## New features 3 | - Major rewrite to php8.0 4 | - New composer dependencies 5 | - new Writer based on Guzzle HttpClient. works in sync and async. 6 | 7 | ## Deprecations: 8 | - dropped php5.3 support 9 | - 10 | 11 | 12 | # v1.1.1 13 | Last version with support of php5.3+ 14 | -------------------------------------------------------------------------------- /Resources/doc/full_config.md: -------------------------------------------------------------------------------- 1 | ## Full config of errbitPHP 2 | 3 | 4 | ``` php 5 | use Errbit\Errbit; 6 | 7 | Errbit::instance() 8 | ->configure(array( 9 | 'api_key' => 'YOUR API KEY', 10 | 'host' => 'YOUR ERRBIT HOST, OR api.airbrake.io FOR AIRBRAKE', 11 | 'port' => 80, // optional 12 | 'secure' => false, // optional 13 | 'project_root' => '/your/project/root', // optional 14 | 'environment_name' => 'production', // optional 15 | 'params_filters' => array('/password/', '/card_number/'), // optional 16 | 'backtrace_filters' => array('#/some/long/path#' => '') // optional 17 | 'connect_timeout' => 3 // optional 18 | 'write_timeout' => 3 // optional 19 | 'skipped_exceptions' => array() // optional 20 | )) 21 | ->start(); 22 | ``` 23 | -------------------------------------------------------------------------------- /Resources/doc/kohana3_integration.md: -------------------------------------------------------------------------------- 1 | # Kohana Integration 2 | 3 | ### Work in progress. 4 | -------------------------------------------------------------------------------- /Resources/doc/symfony2_integration.md: -------------------------------------------------------------------------------- 1 | # Symfony2 Integration 2 | 3 | ## Composer 4 | Add to your `composer.json` 5 | 6 | ```json 7 | require: { 8 | ... 9 | "emgiezet/errbit-php": "dev-master" 10 | } 11 | ``` 12 | Bring the action! 13 | 14 | ``` 15 | $composer.phar install 16 | ``` 17 | 18 | ## Exception Listener 19 | 20 | ```php 21 | enableLog = $errbitParams['errbit_enable_log']; 43 | Errbit::instance()->configure($errbitParams); 44 | } 45 | 46 | /** 47 | * Handle exception method 48 | * 49 | * @param GetResponseForExceptionEvent $event 50 | */ 51 | public function onKernelException(GetResponseForExceptionEvent $event) 52 | { 53 | if ($this->enableLog) { 54 | // get exeption and send to errbit 55 | Errbit::instance()->notify($event->getException()); 56 | } 57 | } 58 | } 59 | ``` 60 | 61 | ## Service Definition 62 | 63 | ```yaml 64 | 65 | services: 66 | acme_demo.event_listener.errbit_exception_listener: 67 | class: Acme\DemoBundle\EventListener\ErrbitExceptionListener 68 | arguments: [%errbit%] 69 | tags: 70 | - { name: kernel.event_listener, event: kernel.exception, method: onKernelException } 71 | 72 | ``` 73 | 74 | Yay We're nearly there! 75 | 76 | ## Config 77 | 78 | ### config.yml 79 | ```yaml 80 | parameters: 81 | errbit: 82 | errbit_enable_log: %errbit_enable_log% 83 | api_key: %errbit_api_key% 84 | host: errbit.yourhosthere.com 85 | port: 80 86 | environment_name: production 87 | skipped_exceptions: [] # optional list of exceptions FQDN 88 | ``` 89 | ### parameters.yml(.ini) 90 | 91 | ```yaml 92 | #parameters.yml 93 | parameters: 94 | errbit_enable_log : true 95 | errbit_api_key : yourApiKeyHere 96 | ``` 97 | 98 | ```ini 99 | ; parameters.ini 100 | [parameters] 101 | errbit_enable_log=true 102 | errbit_api_key=yourApiKeyHere 103 | ``` 104 | 105 | ## If you have some problems here. 106 | 107 | Try the full integration example at: [Symfony2 ErrbitPHP Sandbox](https://github.com/emgiezet/symfony2-errbit) 108 | -------------------------------------------------------------------------------- /Resources/xsd/XSD.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /Resources/xsd/airbrake_2_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /Resources/xsd/hoptoad_2_0.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /build/logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emgiezet/errbit-php/f9a48157bd8fd3eb007441e35164d877e9f13dd7/build/logs/.gitkeep -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "emgiezet/errbit-php", 3 | "type": "library", 4 | "description": "errbit/airbrake integration with php with psr-2", 5 | "keywords": [ 6 | "errbit", 7 | "errbit php", 8 | "error tracking", 9 | "airbrake" 10 | ], 11 | "homepage": "https://github.com/emgiezet/errbitPHP", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "flippa" 16 | }, 17 | { 18 | "name": "emgiezet" 19 | }, 20 | { 21 | "name": "karolsojko" 22 | }, 23 | { 24 | "name": "deathowl" 25 | } 26 | ], 27 | "support": { 28 | "issues": "https://github.com/emgiezet/errbitPHP/issues" 29 | }, 30 | "require": { 31 | "php": "^8.0||8.1||8.2||^8.3", 32 | "guzzlehttp/guzzle": "^7.5.0", 33 | "ext-simplexml": "*" 34 | }, 35 | "require-dev": { 36 | "rector/rector": "^0.15.10", 37 | "mockery/mockery": "1.5.1", 38 | "phpunit/phpunit": "9.4.4", 39 | "php-coveralls/php-coveralls": "^2.5", 40 | "vimeo/psalm": "^5.6", 41 | "phpstan/phpstan": "^1.9" 42 | }, 43 | "replace": { 44 | "nodrew/php-airbrake": "dev-master", 45 | "flippa-official/errbit-php": "dev-master" 46 | }, 47 | "autoload": { 48 | "psr-0": { 49 | "Errbit\\": "src/", 50 | "Unit\\Errbit\\": "tests/Unit", 51 | "Integration\\Errbit\\": "tests/Integration" 52 | } 53 | }, 54 | "config": { 55 | "bin-dir": "bin" 56 | }, 57 | "extra": { 58 | "branch-alias": { 59 | "dev-master": "2.0.x-dev" 60 | } 61 | }, 62 | "scripts": { 63 | "test": "bin/phpunit --testsuite UnitTests --coverage-xml ./build/logs/clover.xml", 64 | "coveralls": "bin/php-coveralls --coverage_clover=./build/logs/clover.xml -v" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | errbit: 4 | image: errbit/errbit:latest 5 | ports: 6 | - "8080:8080" 7 | depends_on: 8 | - mongo 9 | environment: 10 | - PORT=8080 11 | - RACK_ENV=production 12 | - MONGO_URL=mongodb://mongo:27017/errbit 13 | - ERRBIT_ADMIN_EMAIL=errbit-php@errbit.php 14 | - ERRBIT_ADMIN_PASSWORD=errbit-php 15 | - ERRBIT_ADMIN_USER=errbit-php 16 | mongo: 17 | image: mongo:4.1 18 | ports: 19 | - "27017" 20 | errbit.php.service: 21 | container_name: "errbit.php.service" 22 | build: 23 | context: . 24 | volumes: 25 | - ./:/app 26 | entrypoint: ["/bin/sh", "/app/Entrypoint.sh"] 27 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./src 6 | 7 | 8 | 9 | 10 | ./tests/Unit 11 | 12 | 13 | ./tests/Integration/ 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Errbit/Errbit.php: -------------------------------------------------------------------------------- 1 | configure(array( ... ))->start(); 21 | * 22 | * @example Notify an Exception manually 23 | * Errbit::instance()->notify($exception); 24 | * 25 | */ 26 | class Errbit 27 | { 28 | private static ?\Errbit\Errbit $instance = null; 29 | 30 | /** 31 | * @var WriterInterface 32 | */ 33 | protected WriterInterface $writer; 34 | 35 | /** 36 | * Get a singleton instance of the client. 37 | * 38 | * This is the intended way to access the Errbit client. 39 | * 40 | * @return Errbit a singleton 41 | */ 42 | public static function instance() 43 | { 44 | if (!isset(self::$instance)) { 45 | self::$instance = new self(); 46 | } 47 | 48 | return self::$instance; 49 | } 50 | 51 | public const VERSION = '2.0.1'; 52 | public const API_VERSION = '2.2'; 53 | public const PROJECT_NAME = 'errbit-php'; 54 | public const PROJECT_URL = 'https://github.com/emgiezet/errbit-php'; 55 | private array $observers = []; 56 | 57 | /** 58 | * Initialize a new client with the given config. 59 | * 60 | * This is made public for flexibility, though it is not expected you 61 | * should use it. 62 | * 63 | * @param array $config the configuration for the API 64 | */ 65 | public function __construct(private array $config = []) 66 | { 67 | } 68 | 69 | public function setWriter(WriterInterface $writer): void 70 | { 71 | $this->writer = $writer; 72 | } 73 | 74 | /** 75 | * @param $callback 76 | * @return $this 77 | * @throws \Errbit\Exception\Exception 78 | */ 79 | public function onNotify($callback): static 80 | { 81 | if (!is_callable($callback)) { 82 | throw new Exception('Notify callback must be callable'); 83 | } 84 | 85 | $this->observers[] = $callback; 86 | 87 | return $this; 88 | } 89 | 90 | /** 91 | * Set the full configuration for the client. 92 | * 93 | * The only required keys are `api_key' and `host', but other supported 94 | * options are: 95 | * 96 | * - api_key 97 | * - host 98 | * - port 99 | * - secure 100 | * - project_root 101 | * - environment_name 102 | * - url 103 | * - controller 104 | * - action 105 | * - session_data 106 | * - parameters 107 | * - cgi_data 108 | * - params_filters 109 | * - backtrace_filters 110 | * 111 | * @param array $config 112 | * 113 | * @return static the current instance of the client 114 | * @throws \Errbit\Exception\ConfigurationException 115 | */ 116 | public function configure(array $config = []): static 117 | { 118 | $this->config = array_merge($this->config, $config); 119 | $this->checkConfig(); 120 | 121 | return $this; 122 | } 123 | 124 | /** 125 | * @param array $handlers 126 | * 127 | * @return $this 128 | * @throws \Errbit\Exception\Exception 129 | */ 130 | public function start(array $handlers = ['exception', 'error', 'fatal']): static 131 | { 132 | $this->checkConfig(); 133 | ErrorHandlers::register($this, $handlers); 134 | 135 | return $this; 136 | } 137 | 138 | /** 139 | * Notify an individual exception manually. 140 | * 141 | * @param \Errbit\Errors\ErrorInterface $exception 142 | * @param array $options 143 | * 144 | * @return static [Errbit] the current instance 145 | * @throws \Errbit\Exception\ConfigurationException 146 | */ 147 | public function notify(ErrorInterface $exception, array $options = []): static 148 | { 149 | $this->checkConfig(); 150 | $config = array_merge($this->config, $options); 151 | 152 | if ($this->shouldNotify($exception, $config['skipped_exceptions'])) { 153 | $this->getWriter()->write($exception, $config); 154 | $this->notifyObservers($exception, $config); 155 | } 156 | 157 | return $this; 158 | } 159 | 160 | /** 161 | * @param \Errbit\Errors\ErrorInterface $exception 162 | * @param array $skippedExceptions 163 | * 164 | * @return bool 165 | */ 166 | protected function shouldNotify(ErrorInterface $exception, array $skippedExceptions): bool 167 | { 168 | foreach ($skippedExceptions as $skippedException) { 169 | if ($exception instanceof $skippedException) { 170 | return false; 171 | } 172 | } 173 | foreach ($this->config['ignore_user_agent'] as $ua) { 174 | if (str_contains($_SERVER['HTTP_USER_AGENT'],$ua) ) { 175 | return false; 176 | } 177 | } 178 | 179 | return true; 180 | } 181 | 182 | /** 183 | * @param \Errbit\Errors\ErrorInterface $exception 184 | * @param array $config 185 | * 186 | * @return void 187 | */ 188 | protected function notifyObservers(ErrorInterface $exception, array $config): void 189 | { 190 | foreach ($this->observers as $observer) { 191 | $observer($exception, $config); 192 | } 193 | } 194 | 195 | /** 196 | * @return \Errbit\Writer\WriterInterface 197 | */ 198 | protected function getWriter(): WriterInterface 199 | { 200 | if (empty($this->writer)) { 201 | $defaultWriter = new $this->config['default_writer']; 202 | $this->writer = $defaultWriter; 203 | } 204 | 205 | return $this->writer; 206 | } 207 | 208 | /** 209 | * Config checker 210 | * 211 | * @throws ConfigurationException 212 | * @return void 213 | */ 214 | private function checkConfig(): void 215 | { 216 | if (empty($this->config['api_key'])) { 217 | throw new ConfigurationException("`api_key' must be configured"); 218 | } 219 | 220 | if (empty($this->config['host'])) { 221 | throw new ConfigurationException("`host' must be configured"); 222 | } 223 | 224 | if (empty($this->config['port'])) { 225 | $this->config['port'] = !empty($this->config['secure']) ? 443 : 80; 226 | } 227 | 228 | if (!isset($this->config['secure'])) { 229 | $this->config['secure'] = ($this->config['port'] == 443); 230 | } 231 | 232 | if (empty($this->config['hostname'])) { 233 | $this->config['hostname'] = gethostname() ?: ''; 234 | } 235 | 236 | if (empty($this->config['project_root'])) { 237 | $this->config['project_root'] = __DIR__; 238 | } 239 | 240 | if (empty($this->config['environment_name'])) { 241 | $this->config['environment_name'] = 'development'; 242 | } 243 | 244 | if (!isset($this->config['params_filters'])) { 245 | $this->config['params_filters'] = ['/password/']; 246 | } 247 | 248 | if (!isset($this->config['connect_timeout'])) { 249 | $this->config['connect_timeout'] = 3; 250 | } 251 | 252 | if (!isset($this->config['write_timeout'])) { 253 | $this->config['write_timeout'] = 3; 254 | } 255 | 256 | if (!isset($this->config['backtrace_filters'])) { 257 | $this->config['backtrace_filters'] = [sprintf('/^%s/', preg_quote((string) $this->config['project_root'], '/')) => '[PROJECT_ROOT]']; 258 | } 259 | 260 | if (!isset($this->config['skipped_exceptions'])) { 261 | $this->config['skipped_exceptions'] = []; 262 | } 263 | 264 | if (!isset($this->config['default_writer'])) { 265 | $this->config['default_writer'] = \Errbit\Writer\SocketWriter::class; 266 | } 267 | 268 | if (!isset($this->config['agent'])) { 269 | $this->config['agent'] = 'errbitPHP'; 270 | } 271 | if (!isset($this->config['async'])) { 272 | $this->config['async'] = false; 273 | } 274 | if (!isset($this->config['ignore_user_agent'])) { 275 | $this->config['ignore_user_agent'] = []; 276 | } 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/Errbit/Errors/BaseError.php: -------------------------------------------------------------------------------- 1 | message; 30 | } 31 | /** 32 | * Line getter 33 | * 34 | * @return integer the number of line 35 | */ 36 | public function getLine(): int 37 | { 38 | return $this->line; 39 | } 40 | /** 41 | * File getter 42 | * 43 | * @return string name of the file 44 | */ 45 | public function getFile(): string 46 | { 47 | return $this->file; 48 | } 49 | 50 | /** 51 | * @return array 52 | */ 53 | public function getTrace(): array 54 | { 55 | return $this->trace; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Errbit/Errors/Error.php: -------------------------------------------------------------------------------- 1 | $line, 'file' => $file, 'function' => '']] 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Errbit/Errors/Notice.php: -------------------------------------------------------------------------------- 1 | options = array_merge( 33 | [ 34 | 'url' => $this->buildRequestUrl(), 35 | 'parameters' => !empty($_REQUEST) ? $_REQUEST : [], 36 | 'session_data' => !empty($_SESSION) ? $_SESSION : [], 37 | 'cgi_data' => !empty($_SERVER) ? $_SERVER : [], 38 | ], 39 | $options 40 | ); 41 | 42 | $this->filterData(); 43 | } 44 | 45 | /** 46 | * Building request url 47 | * 48 | * @return string url 49 | * 50 | */ 51 | private function buildRequestUrl(): ?string 52 | { 53 | if (!empty($_SERVER['REQUEST_URI'])) { 54 | return sprintf( 55 | '%s://%s%s%s', 56 | $this->guessProtocol(), 57 | $this->guessHost(), 58 | $this->guessPort(), 59 | $_SERVER['REQUEST_URI'] 60 | ); 61 | } 62 | 63 | return null; 64 | } 65 | 66 | /** 67 | * Protocol guesser 68 | * 69 | * @return string http or https protocol 70 | */ 71 | private function guessProtocol(): string 72 | { 73 | if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) { 74 | return $_SERVER['HTTP_X_FORWARDED_PROTO']; 75 | } elseif (!empty($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443) { 76 | return 'https'; 77 | } else { 78 | return 'http'; 79 | } 80 | } 81 | 82 | /** 83 | * Host guesser 84 | * 85 | * @return string servername 86 | */ 87 | private function guessHost(): string 88 | { 89 | if (!empty($_SERVER['HTTP_HOST'])) { 90 | return $_SERVER['HTTP_HOST']; 91 | } elseif (!empty($_SERVER['SERVER_NAME'])) { 92 | return $_SERVER['SERVER_NAME']; 93 | } else { 94 | return '127.0.0.1'; 95 | } 96 | } 97 | 98 | /** 99 | * Port guesser 100 | * 101 | * @return string port 102 | */ 103 | private function guessPort(): string 104 | { 105 | if (!empty($_SERVER['SERVER_PORT']) && !in_array( 106 | $_SERVER['SERVER_PORT'], 107 | [80, 443] 108 | )) { 109 | return sprintf(':%d', $_SERVER['SERVER_PORT']); 110 | } 111 | 112 | return '80'; 113 | } 114 | 115 | /** 116 | * Filtering data 117 | * 118 | * @return void 119 | */ 120 | private function filterData(): void 121 | { 122 | if (empty($this->options['params_filters'])) { 123 | return; 124 | } 125 | 126 | foreach (['parameters', 'session_data', 'cgi_data'] as $name) { 127 | $this->filterParams($name); 128 | } 129 | } 130 | 131 | /** 132 | * Filtering params 133 | * 134 | * @param string $name param name 135 | * 136 | * @return void 137 | */ 138 | private function filterParams(string $name): void 139 | { 140 | if (empty($this->options[$name])) { 141 | return; 142 | } 143 | 144 | if (is_array($this->options['params_filters'])) { 145 | foreach ($this->options['params_filters'] as $pattern) { 146 | foreach ($this->options[$name] as $key => $value) { 147 | 148 | if (preg_match($pattern, (string)$key)) { 149 | $this->options[$name][$key] = '[FILTERED]'; 150 | } 151 | } 152 | } 153 | } 154 | } 155 | 156 | // -- Private Methods 157 | 158 | /** 159 | * Convenience method to instantiate a new notice. 160 | * 161 | * @param mixed $exception - Exception 162 | * @param array $options - array of options 163 | * 164 | * @return Notice 165 | */ 166 | public static function forException( 167 | mixed $exception, 168 | array $options = [] 169 | ): Notice { 170 | return new self($exception, $options); 171 | } 172 | 173 | /** 174 | * Build the full XML document for the notice. 175 | * 176 | * @return string the XML 177 | */ 178 | public function asXml(): string 179 | { 180 | $exception = $this->exception; 181 | $options = $this->options; 182 | $builder = new XmlBuilder(); 183 | $self = $this; 184 | 185 | return $builder->tag( 186 | 'notice', 187 | '', 188 | ['version' => Errbit::API_VERSION], 189 | function (XmlBuilder $notice) use ($exception, $options, $self) { 190 | $notice->tag('api-key', $options['api_key']); 191 | $notice->tag( 192 | 'notifier', 193 | '', 194 | [], 195 | function (XmlBuilder $notifier) { 196 | $notifier->tag('name', Errbit::PROJECT_NAME); 197 | $notifier->tag('version', Errbit::VERSION); 198 | $notifier->tag('url', Errbit::PROJECT_URL); 199 | } 200 | ); 201 | 202 | $notice->tag( 203 | 'error', 204 | '', 205 | [], 206 | function (XmlBuilder $error) use ($exception, $self) { 207 | $class = Notice::className($exception); 208 | $error->tag('class', $self->filterTrace($class)); 209 | $error->tag( 210 | 'message', 211 | $self->filterTrace( 212 | sprintf( 213 | '%s: %s', 214 | $class, 215 | $exception->getMessage() 216 | ) 217 | ) 218 | ); 219 | $error->tag( 220 | 'backtrace', 221 | '', 222 | [], 223 | function (XmlBuilder $backtrace) use ( 224 | $exception, 225 | $self 226 | ) { 227 | $trace = $exception->getTrace(); 228 | 229 | $file1 = $exception->getFile(); 230 | $backtrace->tag( 231 | 'line', 232 | '', 233 | [ 234 | 'number' => $exception->getLine(), 235 | 'file' => !empty($file1) ? $self->filterTrace( 236 | $file1 237 | ) : '', 238 | 'method' => "", 239 | ] 240 | ); 241 | 242 | // if there is no trace we should add an empty element 243 | if (empty($trace)) { 244 | $backtrace->tag( 245 | 'line', 246 | '', 247 | [ 248 | 'number' => '', 249 | 'file' => '', 250 | 'method' => '', 251 | ] 252 | ); 253 | } else { 254 | foreach ($trace as $frame) { 255 | $backtrace->tag( 256 | 'line', 257 | '', 258 | [ 259 | 'number' => $frame['line'] ?? 0, 260 | 'file' => isset($frame['file']) ? 261 | $self->filterTrace( 262 | $frame['file'] 263 | ) : '', 264 | 'method' => $self->filterTrace( 265 | $self->formatMethod($frame) 266 | ), 267 | ] 268 | ); 269 | } 270 | } 271 | } 272 | ); 273 | } 274 | ); 275 | 276 | if (!empty($options['url']) 277 | || !empty($options['controller']) 278 | || !empty($options['action']) 279 | || !empty($options['parameters']) 280 | || !empty($options['session_data']) 281 | || !empty($options['cgi_data']) 282 | ) { 283 | $notice->tag( 284 | 'request', 285 | '', 286 | [], 287 | function (XmlBuilder $request) use ($options) { 288 | $request->tag( 289 | 'url', 290 | !empty($options['url']) ? $options['url'] : '' 291 | ); 292 | $request->tag( 293 | 'component', 294 | !empty($options['controller']) ? $options['controller'] : '' 295 | ); 296 | $request->tag( 297 | 'action', 298 | !empty($options['action']) ? $options['action'] : '' 299 | ); 300 | if (!empty($options['parameters'])) { 301 | $request->tag( 302 | 'params', 303 | '', 304 | [], 305 | function (XmlBuilder $params) use ($options 306 | ) { 307 | Notice::xmlVarsFor( 308 | $params, 309 | $options['parameters'] 310 | ); 311 | } 312 | ); 313 | } 314 | 315 | if (!empty($options['session_data'])) { 316 | $request->tag( 317 | 'session', 318 | '', 319 | [], 320 | function (XmlBuilder $session) use ($options 321 | ) { 322 | Notice::xmlVarsFor( 323 | $session, 324 | $options['session_data'] 325 | ); 326 | } 327 | ); 328 | } 329 | 330 | if (!empty($options['cgi_data'])) { 331 | $request->tag( 332 | 'cgi-data', 333 | '', 334 | [], 335 | function (XmlBuilder $cgiData) use ($options 336 | ) { 337 | Notice::xmlVarsFor( 338 | $cgiData, 339 | $options['cgi_data'] 340 | ); 341 | } 342 | ); 343 | } 344 | } 345 | ); 346 | } 347 | 348 | if (!empty($options['user'])) { 349 | $notice->tag( 350 | 'user-attributes', 351 | '', 352 | [], 353 | function (XmlBuilder $user) use ($options) { 354 | Notice::xmlVarsFor($user, $options['user']); 355 | } 356 | ); 357 | } 358 | 359 | $notice->tag( 360 | 'server-environment', 361 | '', 362 | [], 363 | function (XmlBuilder $env) use ($options) { 364 | $env->tag('project-root', $options['project_root']); 365 | $env->tag( 366 | 'environment-name', 367 | $options['environment_name'] 368 | ); 369 | } 370 | ); 371 | } 372 | )->asXml(); 373 | } 374 | 375 | /** 376 | * Get a human readable class name for the Exception. 377 | * 378 | * Native PHP errors are named accordingly. 379 | * 380 | * @param object $exception - the exception object 381 | * 382 | * @return string - the name to display 383 | */ 384 | public static function className(object $exception): string 385 | { 386 | $shortClassname = self::parseClassname($exception::class); 387 | switch ($shortClassname['classname']) { 388 | case 'Notice': 389 | return 'Notice'; 390 | case 'Warning': 391 | return 'Warning'; 392 | case 'Error': 393 | return 'Error'; 394 | case 'Fatal': 395 | return 'Fatal Error'; 396 | default: 397 | return $shortClassname['classname']; 398 | } 399 | } 400 | 401 | /** 402 | * Parses class name to namespace and class name. 403 | * 404 | * @param string $name Name of class 405 | * 406 | * @return (string|string[])[] 407 | * 408 | * @psalm-return array{namespace: list, classname: string} 409 | */ 410 | private static function parseClassname(string $name): array 411 | { 412 | return [ 413 | 'namespace' => array_slice(explode('\\', $name), 0, -1), 414 | 'classname' => implode('', array_slice(explode('\\', $name), -1)), 415 | ]; 416 | } 417 | 418 | /** 419 | * Perform search/replace filters on a backtrace entry. 420 | * 421 | * @param string $str the entry from the backtrace 422 | * 423 | * @return string the filtered entry 424 | */ 425 | public function filterTrace(string $str): string 426 | { 427 | 428 | if (empty($this->options['backtrace_filters']) || !is_array( 429 | $this->options['backtrace_filters'] 430 | )) { 431 | return $str; 432 | } 433 | 434 | foreach ($this->options['backtrace_filters'] as $pattern => $replacement) { 435 | $str = preg_replace($pattern, (string)$replacement, $str); 436 | } 437 | 438 | return $str; 439 | } 440 | 441 | /** 442 | * Extract a human-readable method/function name from the given stack frame. 443 | * 444 | * @param array $frame - a single entry for the backtrace 445 | * 446 | * @return string - the name of the method/function 447 | */ 448 | public static function formatMethod(array $frame): string 449 | { 450 | if (!empty($frame['class']) && !empty($frame['type']) && !empty($frame['function'])) { 451 | return sprintf( 452 | '%s%s%s()', 453 | $frame['class'], 454 | $frame['type'], 455 | $frame['function'] 456 | ); 457 | } else { 458 | return sprintf( 459 | '%s()', 460 | !empty($frame['function']) ? $frame['function'] : '' 461 | ); 462 | } 463 | } 464 | 465 | /** 466 | * Recursively build an list of the all the vars in the given array. 467 | * 468 | * @param \Errbit\Utils\XmlBuilder $builder the builder instance to set the 469 | * data into 470 | * @param array $array the stack frame entry 471 | * 472 | * @return void 473 | */ 474 | public static function xmlVarsFor(XmlBuilder $builder, array $array): void 475 | { 476 | 477 | foreach ($array as $key => $value) { 478 | if (is_object($value)) { 479 | 480 | $hash = spl_object_hash($value); 481 | 482 | $value = (array)$value; 483 | } else { 484 | $hash = null; 485 | } 486 | 487 | if (is_array($value)) { 488 | if (null === $hash || !in_array($hash, self::$hashArray)) { 489 | self::$hashArray[] = $hash; 490 | $builder->tag( 491 | 'var', 492 | '', 493 | ['key' => $key], 494 | function ($var) use ($value) { 495 | Notice::xmlVarsFor($var, $value); 496 | }, 497 | true 498 | ); 499 | } else { 500 | $builder->tag( 501 | 'var', 502 | '*** RECURSION ***', 503 | ['key' => $key] 504 | ); 505 | } 506 | 507 | } else { 508 | $builder->tag('var', $value, ['key' => $key]); 509 | } 510 | } 511 | } 512 | } 513 | -------------------------------------------------------------------------------- /src/Errbit/Handlers/ErrorHandlers.php: -------------------------------------------------------------------------------- 1 | install($handlers); 40 | $this->converter = Converter::createDefault(); 41 | } 42 | 43 | // -- Handlers 44 | 45 | /** 46 | * on Error 47 | * 48 | * @param integer $code error code 49 | * @param string $message error message 50 | * @param string $file error file 51 | * @param int $line 52 | * 53 | * @throws \Errbit\Exception\Exception 54 | */ 55 | public function onError(int $code, string $message, string $file, int $line): void 56 | { 57 | $exception = $this->converter->convert($code, $message, $file, 58 | $line, debug_backtrace()); 59 | $this->errbit->notify($exception); 60 | } 61 | 62 | /** 63 | * On exception 64 | * 65 | * @throws \Exception 66 | */ 67 | public function onException(\Exception $exception): void 68 | { 69 | $error = $this->converter->convert($exception->getCode(), $exception->getMessage(), $exception->getFile(), 70 | $exception->getLine(), debug_backtrace()); 71 | $this->errbit->notify($error); 72 | } 73 | 74 | /** 75 | * On shut down 76 | * 77 | * @throws \Errbit\Exception\Exception 78 | */ 79 | public function onShutdown(): void 80 | { 81 | if (($error = error_get_last()) && $error['type'] & error_reporting()) { 82 | $this->errbit->notify(new Fatal($error['message'], $error['file'], $error['line'])); 83 | } 84 | } 85 | 86 | // -- Private Methods 87 | 88 | /** 89 | * Installer 90 | * 91 | * @param array $handlers 92 | */ 93 | private function install(array $handlers): void 94 | { 95 | if (in_array('error', $handlers, true)) { 96 | set_error_handler([$this, 'onError'], error_reporting()); 97 | } 98 | 99 | if (in_array('exception', $handlers, true)) { 100 | set_exception_handler([$this, 'onException']); 101 | } 102 | 103 | if (in_array('fatal', $handlers, true)) { 104 | register_shutdown_function([$this, 'onShutdown']); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Errbit/Utils/Converter.php: -------------------------------------------------------------------------------- 1 | new Notice($message, $line, $file, $backtrace), 28 | E_WARNING, E_USER_WARNING => new Warning($message, $line, $file, $backtrace), 29 | E_RECOVERABLE_ERROR, E_ERROR, E_CORE_ERROR => new Fatal($message, $line, $file), 30 | default => new Error($message, $line, $file, $backtrace), 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Errbit/Utils/XmlBuilder.php: -------------------------------------------------------------------------------- 1 | tag('product', function($product) { 19 | * $product->tag('name', 'Plush Puppy Toy'); 20 | * $product->tag('price', '$8', array('currency' => 'USD')); 21 | * $product->tag('discount', function($discount) { 22 | * $discount->tag('percent', 20); 23 | * $discount->tag('name', '20% off promotion'); 24 | * }); 25 | * }) 26 | * ->asXml(); 27 | */ 28 | class XmlBuilder 29 | { 30 | private \SimpleXMLElement $_xml; 31 | /** 32 | * Instantiate a new XmlBuilder. 33 | * 34 | * @param SimpleXMLElement $xml the parent node (only used internally) 35 | */ 36 | public function __construct(?\SimpleXMLElement $xml = null) 37 | { 38 | $this->_xml = $xml ?: new \SimpleXMLElement('<__ErrbitXMLBuilder__/>'); 39 | } 40 | 41 | /** 42 | * Insert a tag into the XML. 43 | * 44 | * @param string $name the name of the tag, required. 45 | * @param string $value the text value of the element, optional 46 | * @param array $attributes an array of attributes for the tag, optional 47 | * @param Callable $callback a callback to receive an XmlBuilder for the new tag, optional 48 | * 49 | * @return XmlBuilder a builder for the inserted tag 50 | */ 51 | /** 52 | * Insert a tag into the XML. 53 | * 54 | * @param string $name the name of the tag, required. 55 | * @param string $value the text value of the element, optional 56 | * @param array $attributes an array of attributes for the tag, optional 57 | * @param Callable $callback a callback to receive an XmlBuilder for the new tag, optional 58 | * 59 | * @return XmlBuilder a builder for the inserted tag 60 | */ 61 | public function tag($name, $value = '', $attributes = [], $callback = null, bool $getLastChild = false) 62 | { 63 | 64 | $idx = is_countable($this->_xml->$name) ? count($this->_xml->$name) : 0; 65 | 66 | if (is_object($value)) { 67 | $value = "[" . $value::class . "]"; 68 | } else { 69 | $value = (string) $value; 70 | } 71 | 72 | $this->_xml->{$name}[$idx] = $value; 73 | 74 | foreach ($attributes as $attr => $v) { 75 | $this->_xml->{$name}[$idx][$attr] = $v; 76 | } 77 | $node = new self($this->_xml->$name); 78 | if ($getLastChild) { 79 | $array = $this->_xml->xpath($name."[last()]"); 80 | $xml = array_shift($array); 81 | $node = new self($xml); 82 | } 83 | 84 | if ($callback) { 85 | $callback($node); 86 | } 87 | 88 | return $node; 89 | } 90 | 91 | /** 92 | * Add an attribute to the current element. 93 | * 94 | * @param String $name the name of the attribute 95 | * @param String $value the value of the attribute 96 | * 97 | * @return static the current builder 98 | */ 99 | public function attribute($name, $value): static 100 | { 101 | $this->_xml[$name] = $value; 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * Return this XmlBuilder as a string of XML. 108 | * 109 | * @return string the XML of the document 110 | */ 111 | public function asXml(): string 112 | { 113 | return self::utf8ForXML($this->_xml->asXML()); 114 | } 115 | 116 | /** 117 | * Util to converts special chars to be valid with xml 118 | * 119 | * @param string $string xml string to converte the special chars 120 | * 121 | * @return string escaped string 122 | */ 123 | public static function utf8ForXML($string) 124 | { 125 | return preg_replace('/[^\x{0009}\x{000a}\x{000d}\x{0020}-\x{D7FF}\x{E000}-\x{FFFD}]+/u', ' ', $string); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Errbit/Writer/AbstractWriter.php: -------------------------------------------------------------------------------- 1 | asXml(); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/Errbit/Writer/GuzzleWriter.php: -------------------------------------------------------------------------------- 1 | asyncWrite($exception, $config); 49 | } 50 | return $this->synchronousWrite($exception, $config); 51 | 52 | } 53 | 54 | /** 55 | * 56 | * @throws \GuzzleHttp\Exception\GuzzleException 57 | */ 58 | public function synchronousWrite(ErrorInterface $exception, array $config): ResponseInterface 59 | { 60 | $uri = $this->buildConnectionScheme($config); 61 | $body = $this->buildNoticeFor($exception, $config); 62 | 63 | return $this->client->post( 64 | uri: $uri.self::NOTICES_PATH, 65 | options: [ 66 | 'body' =>$body, 67 | 'connect_timout' => $config['connect_timeout'], 68 | 'headers'=>[ 69 | 'Content-Type'=>'text/xml', 70 | 'Accept'=>'text/xml, application/xml' 71 | ] 72 | ] 73 | ); 74 | } 75 | 76 | public function asyncWrite(ErrorInterface $exception, array $config): PromiseInterface 77 | { 78 | $uri = $this->buildConnectionScheme($config); 79 | $promise = $this->client->postAsync( 80 | $uri.self::NOTICES_PATH, 81 | [ 82 | 'body' =>$this->buildNoticeFor($exception, $config), 83 | 'connect_timout' => $config['connect_timeout'], 84 | 'headers'=>[ 85 | 'Content-Type'=>'text/xml', 86 | 'Accept'=>'text/xml, application/xml' 87 | ] 88 | ] 89 | ); 90 | return $promise; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Errbit/Writer/SocketWriter.php: -------------------------------------------------------------------------------- 1 | buildConnectionScheme($config), 30 | (integer) $config['port'], 31 | $errno, 32 | $errstr, 33 | $config['connect_timeout'] 34 | ); 35 | 36 | if ($socket) { 37 | stream_set_timeout($socket, $config['write_timeout']); 38 | $payLoad = $this->buildPayload($exception, $config); 39 | if (strlen((string) $payLoad) > 7000 && $config['async']) { 40 | $messageId = uniqid('', true); 41 | $chunks = str_split((string) $payLoad, 7000); 42 | foreach ($chunks as $idx => $chunk) { 43 | $packet = ['messageid' => $messageId, 'data' => $chunk]; 44 | if ($idx == count($chunks)-1) { 45 | $packet['last'] = true; 46 | } 47 | $fragment = json_encode($packet, JSON_THROW_ON_ERROR); 48 | fwrite($socket, $fragment); 49 | } 50 | } else { 51 | fwrite($socket, (string) $payLoad); 52 | 53 | /** 54 | * If errbit is behind a proxy, then we need read characters to make sure 55 | * that request got to errbit successfully. 56 | * 57 | * Proxies usually do not make request to endpoints if client quits connection before 58 | * proxy even gets the chance to create connection to endpoint 59 | */ 60 | if ($this->charactersToRead !== false) { 61 | while (!feof($socket)) { 62 | $character = fread($socket, $this->charactersToRead); 63 | break; 64 | } 65 | } 66 | } 67 | 68 | fclose($socket); 69 | } 70 | } 71 | 72 | /** 73 | * @param \Errbit\Errors\ErrorInterface $exception 74 | * @param array $config 75 | * 76 | * @return string 77 | */ 78 | protected function buildPayload(ErrorInterface $exception, array $config): string 79 | { 80 | return $this->addHttpHeadersIfNeeded( 81 | $this->buildNoticeFor($exception, $config), 82 | $config 83 | ); 84 | } 85 | 86 | /** 87 | * @param string $body 88 | * @param array $config 89 | * 90 | * @return string 91 | */ 92 | protected function addHttpHeadersIfNeeded(string $body, array $config): string 93 | { 94 | if ($config['async'] ?? false) { 95 | return $body; 96 | } else { 97 | return sprintf( 98 | "%s\r\n\r\n%s", 99 | implode( 100 | "\r\n", 101 | [sprintf('POST %s HTTP/1.1', self::NOTICES_PATH), sprintf('Host: %s', $config['host']), sprintf('User-Agent: %s', $config['agent']), sprintf('Content-Type: %s', 'text/xml'), sprintf('Accept: %s', 'text/xml, application/xml'), sprintf('Content-Length: %d', strlen((string) $body)), sprintf('Connection: %s', 'close')] 102 | ), 103 | $body 104 | ); 105 | } 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/Errbit/Writer/WriterInterface.php: -------------------------------------------------------------------------------- 1 | '127.0.0.1', 23 | 'port'=>'8080', 24 | 'secure'=>false, 25 | 'api_key'=>'fa7619c7bfe2b9725992a495eea61f0f' 26 | ]; 27 | $errbit= new Errbit($config); 28 | $handler = new \Errbit\Handlers\ErrorHandlers($errbit, ['exception', 'error', ['fatal', 'lol', 'doink']]); 29 | $caught = []; 30 | try { 31 | $handler->onError($error, 'Errbit Test: '.$error, __FILE__, 666); 32 | } catch ( \Exception $e) { 33 | $caught[] = $e->getMessage(); 34 | } 35 | $this->assertEmpty($caught, 'Exceptions are thrown during errbit notice: '.print_r($caught,true)); 36 | 37 | } 38 | 39 | /** 40 | * @dataProvider dataProviderErrorTypes 41 | * 42 | * @return void 43 | */ 44 | public function testGuzzleWriterIntegrationTest(int $error) 45 | { 46 | $config = [ 47 | 'host'=>'127.0.0.1', 48 | 'port'=>'8080', 49 | 'secure'=>false, 50 | 'api_key'=>'fa7619c7bfe2b9725992a495eea61f0f' 51 | ]; 52 | $errbit= new Errbit($config); 53 | $client = new Client(['base_uri'=>$config['host']]); 54 | $writer = new GuzzleWriter($client); 55 | $errbit->setWriter($writer); 56 | $handler = new \Errbit\Handlers\ErrorHandlers($errbit, ['exception', 'error', ['fatal', 'lol', 'doink']]); 57 | $caught = []; 58 | try { 59 | $handler->onError($error, 'Errbit Test: '.$error, __FILE__, 666); 60 | } catch ( \Exception $e) { 61 | $caught[] = $e->getMessage(); 62 | } 63 | $this->assertEmpty($caught, 'Exceptions are thrown during errbit notice: '.print_r($caught,true)); 64 | 65 | } 66 | 67 | /** 68 | * @dataProvider dataProviderErrorTypes 69 | * 70 | * @return void 71 | */ 72 | public function testGuzzleWriterAsyncIntegrationTest(int $error) 73 | { 74 | $config = [ 75 | 'host'=>'127.0.0.1', 76 | 'port'=>'8080', 77 | 'secure'=>false, 78 | 'api_key'=>'fa7619c7bfe2b9725992a495eea61f0f', 79 | 'async'=>true 80 | ]; 81 | $errbit= new Errbit($config); 82 | $client = new Client(['base_uri'=>$config['host']]); 83 | $writer = new GuzzleWriter($client); 84 | $errbit->setWriter($writer); 85 | $handler = new \Errbit\Handlers\ErrorHandlers($errbit, ['exception', 'error', ['fatal', 'lol', 'doink']]); 86 | $caught = []; 87 | try { 88 | $handler->onError($error, 'Errbit Test: '.$error, __FILE__, 666); 89 | } catch ( \Exception $e) { 90 | $caught[] = $e->getMessage(); 91 | } 92 | $this->assertEmpty($caught, 'Exceptions are thrown during errbit notice: '.print_r($caught,true)); 93 | 94 | } 95 | 96 | public function dataProviderErrorTypes(): array 97 | { 98 | return [ 99 | [E_NOTICE], 100 | [E_USER_NOTICE], 101 | [E_WARNING], 102 | [E_USER_WARNING], 103 | [E_ERROR], 104 | [E_USER_ERROR] 105 | ]; 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /tests/Unit/Errbit/Tests/ErrbitTest.php: -------------------------------------------------------------------------------- 1 | 'test', 'host' => 'test', 'skipped_exceptions' => ['BadMethodCallException']]; 19 | $this->errbit = new Errbit($config); 20 | } 21 | 22 | /** 23 | * @test 24 | */ 25 | public function shouldPassExceptionsToWriter() 26 | { 27 | $exception = Mockery::mock(Error::class); 28 | 29 | $writer = Mockery::mock(\Errbit\Writer\WriterInterface::class); 30 | $writer->shouldReceive('write')->with($exception, Mockery::any()); 31 | $this->errbit->setWriter($writer); 32 | 33 | $return = $this->errbit->notify($exception); 34 | $this->assertIsObject($return); 35 | } 36 | 37 | /** 38 | * @test 39 | */ 40 | public function shouldIgnoreSkippedExceptions() 41 | { 42 | $this->errbit->configure(['skipped_exceptions'=>[Notice::class]]); 43 | $exception = new Notice('Notice test', 123,'test.php', ['test']); 44 | //don't write because this Notice should be ignored 45 | $writer = Mockery::mock(\Errbit\Writer\WriterInterface::class)->shouldNotReceive('write')->getMock(); 46 | $this->errbit->setWriter($writer); 47 | $return = $this->errbit->notify($exception, []); 48 | $this->assertIsObject($return); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/Unit/Errbit/Tests/Errors/ErrorTest.php: -------------------------------------------------------------------------------- 1 | markTestIncomplete('This test has not been implemented yet.'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Unit/Errbit/Tests/Errors/FatalTest.php: -------------------------------------------------------------------------------- 1 | getMessage(); 17 | $this->assertEquals('test', $msg, 'Message of base error missmatch'); 18 | 19 | $line = $object->getLine(); 20 | $this->assertEquals(12, $line, 'Line no mismatch'); 21 | 22 | $file = $object->getFile(); 23 | $this->assertEquals(__FILE__, $file, 'File missmatch'); 24 | 25 | $trace = $object->getTrace(); 26 | 27 | $actualTrace = [['line' => 12, 'file' => __FILE__, 'function' => '']]; 28 | $this->assertEquals($actualTrace, $trace, 'trace missmatch'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Unit/Errbit/Tests/Errors/NoticeTest.php: -------------------------------------------------------------------------------- 1 | markTestIncomplete('This test has not been implemented yet.'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/Unit/Errbit/Tests/Errors/WarningTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('readTemp')->times(3)->andReturn(10, 12, 14); 16 | $this->markTestIncomplete('This test has not been implemented yet.'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Unit/Errbit/Tests/Exception/ExceptionTest.php: -------------------------------------------------------------------------------- 1 | markTestIncomplete('This test has not been implemented yet.'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/Unit/Errbit/Tests/Exception/NoticeTest.php: -------------------------------------------------------------------------------- 1 | markTestIncomplete('This test has not been implemented yet.'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/Unit/Errbit/Tests/Handlers/ErrorHandlersTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('readTemp')->times(3)->andReturn(10, 12, 14); 20 | 21 | $config = ['api_key'=>'9fa28ccc56ed3aae882d25a9cee5695a', 'host'=>'127.0.0.1', 'port' => '8080', 'secure' => false]; 22 | $errbit= new Errbit($config); 23 | 24 | $writerMock = \Mockery::mock(SocketWriter::class); 25 | $writerMock->shouldReceive('write'); 26 | $errbit->setWriter($writerMock); 27 | $handler = new ErrorHandlers($errbit, ['exception', 'error', 'fatal', 'lol', 'doink']); 28 | 29 | $errors = [E_NOTICE, E_USER_NOTICE, E_WARNING, E_USER_WARNING, E_ERROR, E_USER_ERROR]; 30 | $catched = []; 31 | try { 32 | foreach ($errors as $error) { 33 | $handler->onError($error, 'Errbit Test: '.$error, __FILE__, 666); 34 | } 35 | } catch ( \Exception $e) { 36 | $catched[] = $e->getMessage(); 37 | } 38 | $this->assertTrue(count($catched) === 0, 'Exceptions are thrown during errbit notice'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Unit/Errbit/Tests/Utils/ConverterTest.php: -------------------------------------------------------------------------------- 1 | object = Converter::createDefault(); 28 | } 29 | 30 | public function testNotice() 31 | { 32 | $notice = $this->object->convert(E_NOTICE, "TestNotice", "test.php", 8, [""]); 33 | $expected = new Notice("TestNotice", 8, "test.php", [""]); 34 | $this->assertEquals($notice, $expected); 35 | } 36 | 37 | public function testUserNotice() 38 | { 39 | 40 | $notice = $this->object->convert(E_USER_NOTICE, "TestNotice", "test.php", 8, [""]); 41 | $expected = new Notice("TestNotice", 8, "test.php", [""]); 42 | $this->assertEquals($notice, $expected); 43 | } 44 | 45 | public function testFatalError() 46 | { 47 | $fatal = $this->object->convert(E_ERROR, "TestError", "test.php", 8, [""]); 48 | $expected = new Fatal("TestError", 8, "test.php"); 49 | $this->assertEquals($fatal, $expected); 50 | } 51 | 52 | public function testCatchableFatalError() 53 | { 54 | $notice = $this->object->convert(E_RECOVERABLE_ERROR, "TestError", "test.php", 8, [""]); 55 | $expected = new Fatal("TestError", 8, "test.php"); 56 | $this->assertEquals($notice, $expected); 57 | } 58 | 59 | 60 | public function testUserError() 61 | { 62 | $notice = $this->object->convert(E_USER_ERROR, "TestError", "test.php", 8, [""]); 63 | $expected = new Error("TestError", 8, "test.php", [""]); 64 | $this->assertEquals($notice, $expected); 65 | } 66 | 67 | public function testWarning() 68 | { 69 | $notice = $this->object->convert(E_WARNING, "TestWarning", "test.php", 8, [""]); 70 | $expected = new Warning("TestWarning", 8, "test.php", [""]); 71 | $this->assertEquals($notice, $expected); 72 | } 73 | 74 | public function testUserWarning() 75 | { 76 | $notice = $this->object->convert(E_USER_WARNING, "TestWarning", "test.php", 8, [""]); 77 | $expected = new Warning("TestWarning", 8, "test.php", [""]); 78 | $this->assertEquals($notice, $expected); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/Unit/Errbit/Tests/Utils/XmlBuilderTest.php: -------------------------------------------------------------------------------- 1 | config = ['api_key'=>'9fa28ccc56ed3aae882d25a9cee5695a', 'host' => 'errbit.redexperts.net', 'port' => '80', 'secure' => '443', 'project_root' => 'test', 'environment_name' => 'test', 'url' => 'test', 'controller' => 'test', 'action' => 'test', 'session_data' => ['test'], 'parameters' => ['test', 'doink'], 'cgi_data' => ['test'], 'params_filters' => ['test'=>'/test/'], 'backtrace_filters' => 'test']; 26 | } 27 | 28 | /** 29 | * @return void 30 | */ 31 | public function testBase() 32 | { 33 | 34 | $notice = new Notice(new \Exception(), $this->config); 35 | 36 | $xml = $notice->asXml(); 37 | 38 | $dom = new \DOMDocument(); 39 | $dom->loadXML($xml); 40 | 41 | $valid = $dom->schemaValidate(__DIR__.'/../../../../../Resources/xsd/hoptoad_2_0.xsd'); 42 | $this->assertTrue($valid, 'Not Valid XSD'); 43 | 44 | } 45 | 46 | /** 47 | * @return void 48 | */ 49 | public function testShouldNotFollowRecursion() 50 | { 51 | 52 | $foo = new \StdClass; 53 | $bar = new \StdClass; 54 | $foo->bar = $bar; 55 | $bar->foo = $foo; 56 | $vars = ['foo' => $foo, 'bar' => $bar]; 57 | 58 | $this->config['session_data'] = [$vars]; 59 | 60 | $notice = new Notice(new \Exception(), $this->config); 61 | 62 | $xml = $notice->asXml(); 63 | 64 | $dom = new \DOMDocument(); 65 | $dom->loadXML($xml); 66 | 67 | $valid = $dom->schemaValidate(__DIR__.'/../../../../../Resources/xsd/XSD.xml'); 68 | $this->assertTrue($valid, 'Not Valid XSD'); 69 | } 70 | 71 | /** 72 | * @return void 73 | */ 74 | public function testSimpleObjectInXml() 75 | { 76 | $foo = new \StdClass; 77 | 78 | $foo->first = "First"; 79 | $foo->second = "Second"; 80 | $foo->third = ["1", "2"]; 81 | 82 | $this->config['session_data'] = [$foo]; 83 | 84 | $notice = new Notice(new \Exception(), $this->config); 85 | 86 | $xml = $notice->asXml(); 87 | 88 | $dom = new \DOMDocument(); 89 | $dom->loadXML($xml); 90 | 91 | $valid = $dom->schemaValidate(__DIR__.'/../../../../../Resources/xsd/XSD.xml'); 92 | $this->assertTrue($valid, 'Not Valid XSD'); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/Unit/Errbit/Tests/Writer/GuzzleWriterTest.php: -------------------------------------------------------------------------------- 1 | '127.0.0.1', 25 | 'port'=>'8080', 26 | 'secure'=>false, 27 | 'api_key'=>'fa7619c7bfe2b9725992a495eea61f0f' 28 | ]; 29 | $errbit= new Errbit($config); 30 | $clientMock = \Mockery::mock(Client::class); 31 | $clientMock->shouldReceive('post')->once(); 32 | $writer = new GuzzleWriter($clientMock); 33 | $errbit->setWriter($writer); 34 | $handler = new \Errbit\Handlers\ErrorHandlers($errbit, ['exception', 'error', ['fatal', 'lol', 'doink']]); 35 | $catched = []; 36 | try { 37 | $handler->onError($error, 'Errbit Test: '.$error, __FILE__, 666); 38 | } catch ( \Exception $e) { 39 | $catched[] = $e->getMessage(); 40 | } 41 | $this->assertEmpty($catched, 'Exceptions are thrown during errbit notice: '.print_r($catched,1)); 42 | 43 | } 44 | 45 | public function dataProviderTestWrite(): array 46 | { 47 | return [ 48 | [E_NOTICE], 49 | [E_USER_NOTICE], 50 | [E_WARNING], 51 | [E_USER_WARNING], 52 | [E_ERROR], 53 | [E_USER_ERROR] 54 | ]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Unit/Errbit/Tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |