├── .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 | [](https://coveralls.io/r/emgiezet/errbitPHP)
5 | [](https://travis-ci.org/emgiezet/errbitPHP)
6 | [](https://www.versioneye.com/user/projects/5249e725632bac0a4900b2bf)
7 | [](https://packagist.org/packages/emgiezet/errbit-php)
8 | [](https://insight.symfony.com/projects/a0c405fb-8ee9-40e9-acf1-eee084fc35a6)
9 | [](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 | [](http://www.slideshare.net/MaxMaecki/meetphp-11-huston-we-have-an-airbrake)
23 |
24 | ## ChangeLog
25 | Check the:
26 |
27 | []
28 | []
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 |