├── LICENSE ├── README.md ├── _ide_helper.php ├── composer.json ├── config └── exception-notify.php ├── docs ├── bark.jpg ├── discord.jpg ├── jetbrains.png ├── lark.jpg ├── mail.jpg ├── ntfy.jpg ├── pushDeer.jpg ├── slack.jpg ├── telegram.jpg └── weWork.jpg └── src ├── Channels ├── AbstractChannel.php ├── Channel.php ├── DumpChannel.php ├── LogChannel.php ├── MailChannel.php ├── NotifyChannel.php └── StackChannel.php ├── Collectors ├── AbstractCollector.php ├── AbstractExceptionCollector.php ├── ApplicationCollector.php ├── ChoreCollector.php ├── ExceptionBasicCollector.php ├── ExceptionContextCollector.php ├── ExceptionTraceCollector.php ├── RequestBasicCollector.php ├── RequestFileCollector.php ├── RequestHeaderCollector.php ├── RequestPostCollector.php └── RequestQueryCollector.php ├── Commands ├── Concerns │ └── Configureable.php └── TestCommand.php ├── Contracts ├── ChannelContract.php ├── CollectorContract.php ├── ExceptionAwareContract.php └── ThrowableContract.php ├── Events ├── ReportedEvent.php └── ReportingEvent.php ├── ExceptionNotifyManager.php ├── ExceptionNotifyServiceProvider.php ├── Exceptions ├── InvalidArgumentException.php ├── InvalidConfigurationException.php └── RuntimeException.php ├── Facades └── ExceptionNotify.php ├── Jobs └── ReportExceptionJob.php ├── Mail └── ReportExceptionMail.php ├── Pipes ├── AddChorePipe.php ├── AddKeywordChorePipe.php ├── FixPrettyJsonPipe.php ├── LimitLengthPipe.php ├── SprintfHtmlPipe.php ├── SprintfMarkdownPipe.php └── SprintfPipe.php ├── Support ├── ExceptionContext.php ├── JsonFixer.php ├── Rectors │ ├── HydratePipeFuncCallToStaticCallRector.php │ └── ToInternalExceptionRector.php ├── Traits │ ├── AggregationTrait.php │ └── WithPipeArgs.php ├── Utils.php └── helpers.php └── Template.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021-2025 guanguans 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # laravel-exception-notify 2 | 3 |

usage

4 | 5 | > Monitor exception and report to the notification channels(Dump、Log、Mail、AnPush、Bark、Chanify、DingTalk、Discord、Gitter、GoogleChat、IGot、Lark、Mattermost、MicrosoftTeams、NowPush、Ntfy、Push、Pushback、PushBullet、PushDeer、PushMe、Pushover、PushPlus、QQ、RocketChat、ServerChan、ShowdocPush、SimplePush、Slack、Telegram、WeWork、WPush、XiZhi、YiFengChuanHua、Zulip). 6 | 7 | [![tests](https://github.com/guanguans/laravel-exception-notify/workflows/tests/badge.svg)](https://github.com/guanguans/laravel-exception-notify/actions) 8 | [![check & fix styling](https://github.com/guanguans/laravel-exception-notify/workflows/check%20&%20fix%20styling/badge.svg)](https://github.com/guanguans/laravel-exception-notify/actions) 9 | [![codecov](https://codecov.io/gh/guanguans/laravel-exception-notify/branch/main/graph/badge.svg?token=URGFAWS6S4)](https://codecov.io/gh/guanguans/laravel-exception-notify) 10 | [![Latest Stable Version](https://poser.pugx.org/guanguans/laravel-exception-notify/v)](https://packagist.org/packages/guanguans/laravel-exception-notify) 11 | [![GitHub release (with filter)](https://img.shields.io/github/v/release/guanguans/laravel-exception-notify)](https://github.com/guanguans/laravel-exception-notify/releases) 12 | [![Total Downloads](https://poser.pugx.org/guanguans/laravel-exception-notify/downloads)](https://packagist.org/packages/guanguans/laravel-exception-notify) 13 | [![License](https://poser.pugx.org/guanguans/laravel-exception-notify/license)](https://packagist.org/packages/guanguans/laravel-exception-notify) 14 | 15 | ## Related repositories 16 | 17 | * [https://github.com/guanguans/notify](https://github.com/guanguans/notify) 18 | * [https://github.com/guanguans/yii-log-target](https://github.com/guanguans/yii-log-target) 19 | 20 | ## Requirement 21 | 22 | * PHP >= 8.0 23 | 24 | ## Installation 25 | 26 | ```bash 27 | composer require guanguans/laravel-exception-notify --ansi -v 28 | ``` 29 | 30 | ## Configuration 31 | 32 | ### Publish files(optional) 33 | 34 | ```bash 35 | php artisan vendor:publish --provider="Guanguans\\LaravelExceptionNotify\\ExceptionNotifyServiceProvider" --ansi -v 36 | ``` 37 | 38 | ### Apply for channel authentication information 39 | 40 | * [Notify(30+)](https://github.com/guanguans/notify) 41 | * Dump(for debugging exception report) 42 | * Log 43 | * Mail 44 | 45 | ### Configure channels in the `config/exception-notify.php` and `.env` file 46 | 47 | ```dotenv 48 | EXCEPTION_NOTIFY_CHANNEL=stack 49 | EXCEPTION_NOTIFY_STACK_CHANNELS=log,slack,weWork 50 | EXCEPTION_NOTIFY_SLACK_WEBHOOK=https://hooks.slack.com/services/TPU9A9/B038KNUC0GY/6pKH3vfa3mjlUPcgLSjzR 51 | EXCEPTION_NOTIFY_WEWORK_TOKEN=73a3d5a3-ceff-4da8-bcf3-ff5891778 52 | ``` 53 | 54 | ## Usage 55 | 56 | ### Test whether exception can be monitored and reported to notification channel 57 | 58 | ```shell 59 | php artisan exception-notify:test --channel=dump --job-connection=sync 60 | php artisan exception-notify:test 61 | php artisan exception-notify:test -v 62 | ``` 63 | 64 | ### :camera_flash: Notification examples 65 | 66 |
67 | :monocle_face: details 68 | 69 | | discord | slack | telegram | 70 | |:----------------------------:|:------------------------:|:------------------------------:| 71 | | ![discord](docs/discord.jpg) | ![slack](docs/slack.jpg) | ![telegram](docs/telegram.jpg) | 72 | | lark | mail | weWork | 73 | | ![lark](docs/lark.jpg) | ![mail](docs/mail.jpg) | ![weWork](docs/weWork.jpg) | 74 | 75 |
76 | 77 | ### Skip report 78 | 79 | `app/Providers/AppServiceProvider.php` 80 | 81 | ```php 82 | Arr::first( 90 | [ 91 | \Symfony\Component\HttpKernel\Exception\HttpException::class, 92 | \Illuminate\Http\Exceptions\HttpResponseException::class, 93 | ], 94 | static fn (string $exception): bool => $throwable instanceof $exception 95 | )); 96 | } 97 | ``` 98 | 99 | ### Extend channel 100 | 101 | `app/Providers/AppServiceProvider.php` 102 | 103 | ```php 104 | 11 | * 12 | * For the full copyright and license information, please view 13 | * the LICENSE file that was distributed with this source code. 14 | * 15 | * @see https://github.com/guanguans/laravel-exception-notify 16 | */ 17 | 18 | namespace { 19 | class ExceptionNotify extends Guanguans\LaravelExceptionNotify\Facades\ExceptionNotify {} 20 | } 21 | 22 | namespace Illuminate\Support { 23 | /** 24 | * @mixin \Illuminate\Support\Str 25 | */ 26 | class Str {} 27 | 28 | /** 29 | * @mixin \Illuminate\Support\Stringable 30 | */ 31 | class Stringable {} 32 | } 33 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "guanguans/laravel-exception-notify", 3 | "description": "Monitor exception and report to the notification channels(Dump、Log、Mail、AnPush、Bark、Chanify、DingTalk、Discord、Gitter、GoogleChat、IGot、Lark、Mattermost、MicrosoftTeams、NowPush、Ntfy、Push、Pushback、PushBullet、PushDeer、PushMe、Pushover、PushPlus、QQ、RocketChat、ServerChan、ShowdocPush、SimplePush、Slack、Telegram、WeWork、WPush、XiZhi、YiFengChuanHua、Zulip).", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "bot", 8 | "dump", 9 | "email", 10 | "error", 11 | "exception", 12 | "laravel", 13 | "log", 14 | "logger", 15 | "mail", 16 | "monitor", 17 | "notification", 18 | "notifier", 19 | "notify", 20 | "push", 21 | "sdk", 22 | "QQ 频道机器人", 23 | "Server酱", 24 | "企业微信", 25 | "企业微信群机器人", 26 | "微信", 27 | "息知", 28 | "钉钉", 29 | "钉钉群机器人", 30 | "飞书", 31 | "飞书群机器人", 32 | "一封传话", 33 | "AnPush", 34 | "Bark", 35 | "Chanify", 36 | "DingTalk", 37 | "Discord", 38 | "Gitter", 39 | "GoogleChat", 40 | "IGot", 41 | "Lark", 42 | "Mattermost", 43 | "MicrosoftTeams", 44 | "NowPush", 45 | "Ntfy", 46 | "Push", 47 | "Pushback", 48 | "PushBullet", 49 | "PushDeer", 50 | "PushMe", 51 | "Pushover", 52 | "PushPlus", 53 | "QQ", 54 | "RocketChat", 55 | "ServerChan", 56 | "ShowdocPush", 57 | "SimplePush", 58 | "Slack", 59 | "Telegram", 60 | "WeWork", 61 | "WPush", 62 | "XiZhi", 63 | "YiFengChuanHua", 64 | "Zulip" 65 | ], 66 | "authors": [ 67 | { 68 | "name": "guanguans", 69 | "email": "ityaozm@gmail.com", 70 | "homepage": "https://www.guanguans.cn", 71 | "role": "developer" 72 | } 73 | ], 74 | "homepage": "https://github.com/guanguans/laravel-exception-notify", 75 | "support": { 76 | "issues": "https://github.com/guanguans/laravel-exception-notify/issues", 77 | "source": "https://github.com/guanguans/laravel-exception-notify" 78 | }, 79 | "funding": [ 80 | { 81 | "type": "wechat", 82 | "url": "https://www.guanguans.cn/images/wechat.jpeg" 83 | } 84 | ], 85 | "require": { 86 | "php": ">=8.0", 87 | "guanguans/notify": "^3.2", 88 | "laravel/framework": "^9.52 || ^10.0 || ^11.0 || ^12.0" 89 | }, 90 | "require-dev": { 91 | "adhocore/json-fixer": "^1.0", 92 | "azjezz/psl": "^1.9 || ^2.0 || ^3.0", 93 | "bamarni/composer-bin-plugin": "^1.8", 94 | "brainmaestro/composer-git-hooks": "^3.0", 95 | "composer/semver": "^3.4", 96 | "cweagans/composer-patches": "^1.7", 97 | "driftingly/rector-laravel": "^2.0", 98 | "ergebnis/composer-normalize": "^2.47", 99 | "ergebnis/json-printer": "^3.7", 100 | "ergebnis/rector-rules": "^1.4", 101 | "guanguans/ai-commit": "dev-main", 102 | "guanguans/monorepo-builder-worker": "^2.0", 103 | "laravel/facade-documenter": "dev-main", 104 | "mockery/mockery": "^1.6", 105 | "nette/utils": "^4.0", 106 | "orchestra/testbench": "^7.54 || ^8.0 || ^9.0 || ^10.0", 107 | "pestphp/pest": "^1.23 || ^2.0 || ^3.0", 108 | "pestphp/pest-plugin-faker": "^1.0 || ^2.0 || ^3.0", 109 | "pestphp/pest-plugin-laravel": "^1.4 || ^2.0 || ^3.0", 110 | "php-mock/php-mock-phpunit": "^2.13", 111 | "phpstan/extension-installer": "^1.4", 112 | "phpstan/phpstan": "^2.1", 113 | "phpstan/phpstan-deprecation-rules": "^2.0", 114 | "phpstan/phpstan-webmozart-assert": "^2.0", 115 | "rector/rector": "^2.0", 116 | "rector/swiss-knife": "^2.2", 117 | "rector/type-perfect": "^2.1", 118 | "shipmonk/composer-dependency-analyser": "^1.8", 119 | "shipmonk/phpstan-baseline-per-identifier": "^2.1", 120 | "spatie/pest-plugin-snapshots": "^1.1 || ^2.0", 121 | "spaze/phpstan-disallowed-calls": "^4.5", 122 | "symplify/phpstan-extensions": "^12.0", 123 | "symplify/phpstan-rules": "^14.6", 124 | "symplify/vendor-patches": "^11.4", 125 | "tomasvotruba/class-leak": "^2.0", 126 | "tomasvotruba/type-coverage": "^2.0", 127 | "yamadashy/phpstan-friendly-formatter": "^1.2" 128 | }, 129 | "repositories": { 130 | "facade-documenter": { 131 | "type": "vcs", 132 | "url": "git@github.com:laravel/facade-documenter.git" 133 | } 134 | }, 135 | "minimum-stability": "stable", 136 | "prefer-stable": true, 137 | "autoload": { 138 | "psr-4": { 139 | "Guanguans\\LaravelExceptionNotify\\": "src" 140 | }, 141 | "files": [ 142 | "src/Support/helpers.php" 143 | ] 144 | }, 145 | "autoload-dev": { 146 | "psr-4": { 147 | "Guanguans\\LaravelExceptionNotifyTests\\": "tests" 148 | } 149 | }, 150 | "config": { 151 | "allow-plugins": { 152 | "bamarni/composer-bin-plugin": true, 153 | "cweagans/composer-patches": true, 154 | "ergebnis/composer-normalize": true, 155 | "pestphp/pest-plugin": true, 156 | "phpstan/extension-installer": true 157 | }, 158 | "apcu-autoloader": true, 159 | "classmap-authoritative": false, 160 | "optimize-autoloader": true, 161 | "preferred-install": "dist", 162 | "sort-packages": true 163 | }, 164 | "extra": { 165 | "bamarni-bin": { 166 | "bin-links": true, 167 | "forward-command": true, 168 | "target-directory": "vendor-bin" 169 | }, 170 | "branch-alias": { 171 | "dev-main": "5.x-dev" 172 | }, 173 | "composer-normalize": { 174 | "indent-size": 4, 175 | "indent-style": "space" 176 | }, 177 | "hooks": { 178 | "post-merge": [ 179 | "composer checks" 180 | ], 181 | "pre-commit": [ 182 | "composer checks" 183 | ] 184 | }, 185 | "laravel": { 186 | "aliases": { 187 | "ExceptionNotify": "Guanguans\\LaravelExceptionNotify\\Facades\\ExceptionNotify" 188 | }, 189 | "providers": [ 190 | "Guanguans\\LaravelExceptionNotify\\ExceptionNotifyServiceProvider" 191 | ] 192 | } 193 | }, 194 | "scripts": { 195 | "post-install-cmd": [ 196 | "@cghooks add --ignore-lock", 197 | "@cghooks update" 198 | ], 199 | "post-update-cmd": [ 200 | "@cghooks update" 201 | ], 202 | "ai-commit": "@php ./vendor/bin/ai-commit commit --ansi", 203 | "ai-commit-bito": "@ai-commit --generator=bito_cli", 204 | "ai-commit-bito-no-verify": "@ai-commit-bito --no-verify", 205 | "ai-commit-github-copilot": "@ai-commit --generator=github_copilot_cli", 206 | "ai-commit-github-copilot-no-verify": "@ai-commit-github-copilot --no-verify", 207 | "ai-commit-github-models": "@ai-commit --generator=github_models_cli", 208 | "ai-commit-github-models-no-verify": "@ai-commit-github-models --no-verify", 209 | "ai-commit-no-verify": "@ai-commit --no-verify", 210 | "benchmark": "@php ./vendor/bin/phpbench run --report=aggregate --ansi -v", 211 | "cghooks": "@php ./vendor/bin/cghooks --ansi -v", 212 | "cghooks-ignore": "[ ! -f \"./vendor/bin/cghooks\" ] && exit 0 || php ./vendor/bin/cghooks --ansi -v", 213 | "checks": [ 214 | "@composer-normalize", 215 | "@composer-validate", 216 | "@yaml-lint", 217 | "@md-lint", 218 | "@facade-lint", 219 | "@style-lint", 220 | "@composer-dependency-analyser", 221 | "@test", 222 | "@phpstan", 223 | "@rector-dry-run" 224 | ], 225 | "class-leak": "@php ./vendor/bin/class-leak --ansi -v", 226 | "class-leak-check": "@class-leak check ./config ./src --skip-suffix=Pipe --skip-path=Support/Traits --skip-path=Channels --skip-type=\\Guanguans\\LaravelExceptionNotify\\Contracts\\Channel --skip-type=\\Guanguans\\LaravelExceptionNotify\\Contracts\\Collector --skip-type=Rector\\Rector\\AbstractRector", 227 | "composer-audit": "@composer audit --ansi -v", 228 | "composer-bin-all-update": "@composer bin all update --ansi -v", 229 | "composer-check-platform-reqs": "@composer check-platform-reqs --lock --ansi -v", 230 | "composer-dependency-analyser": "@php ./vendor/bin/composer-dependency-analyser --verbose", 231 | "composer-normalize": "@composer normalize --dry-run --diff --ansi -v", 232 | "composer-require-checker": "@php ./vendor/bin/composer-require-checker check --config-file=composer-require-checker.json composer.json --ansi -v", 233 | "composer-require-checker-json": "@php ./vendor/bin/composer-require-checker check --config-file=composer-require-checker.json composer.json --ansi -v --output=json | jq", 234 | "composer-unused": "@php ./vendor/bin/composer-unused --ansi -v", 235 | "composer-updater": "@php ./composer-updater --highest-php-binary=/opt/homebrew/opt/php@8.4/bin/php --ansi", 236 | "composer-updater-dry-run": "@composer-updater --dry-run", 237 | "composer-validate": "@composer validate --check-lock --strict --ansi -v", 238 | "facade-lint": "@facade-update --lint", 239 | "facade-update": [ 240 | "@putenvs", 241 | "$PHP81 -f ./vendor/bin/facade.php -- Guanguans\\\\LaravelExceptionNotify\\\\Facades\\\\ExceptionNotify" 242 | ], 243 | "lint": [ 244 | "@putenvs", 245 | "for DIR in .; do find $DIR -maxdepth 1 -type f -name '*.php' -type f ! -name 'xxx.php' -exec $PHP80 -l {} \\; 2>&1 | (! grep -v '^No syntax errors detected'); done", 246 | "for DIR in ./config ./src ./tests; do find $DIR -type f -name '*.php' -type f ! -name 'xxx.php' -exec $PHP80 -l {} \\; 2>&1 | (! grep -v '^No syntax errors detected'); done" 247 | ], 248 | "mark-finish": "printf '\\n!\\n!\\t\\033[0;32m%s\\033[0m\\n!\\n\\n' \"Finished\"", 249 | "mark-separate": "printf '\\n!\\n!\\t\\033[0;33m%s\\033[0m\\n!\\n\\n' \"----------------\"", 250 | "mark-start": "printf '\\n!\\n!\\t\\033[0;36m%s\\033[0m\\n!\\n\\n' \"Started\"", 251 | "md-fix": "@md-lint --fix", 252 | "md-lint": "lint-md --config .lintmdrc ./*.md ./.github/ ./docs/", 253 | "neon-lint": "@php ./vendor/bin/neon-lint *.neon", 254 | "normalized": "@composer normalize --diff --ansi -v", 255 | "normalized-dry-run": "@normalized --dry-run", 256 | "peck": "/opt/homebrew/opt/php@8.3/bin/php ./vendor/bin/peck check --path=src/ --config=peck.json --ansi -v", 257 | "peck-init": "@peck --init", 258 | "pest": "@php ./vendor/bin/pest --colors=always --min=90 --coverage", 259 | "pest-bail": "@pest --bail", 260 | "pest-coverage": "@pest --coverage-html=./.build/phpunit/ --coverage-clover=./.build/phpunit/clover.xml", 261 | "pest-disable-coverage-ignore": "@pest --disable-coverage-ignore", 262 | "pest-highest": [ 263 | "@putenvs", 264 | "$PHP83 ./vendor/bin/pest --coverage" 265 | ], 266 | "pest-migrate-configuration": "@pest --migrate-configuration", 267 | "pest-parallel": "@pest --parallel", 268 | "pest-profile": "@pest --profile", 269 | "pest-type-coverage": "@pest --type-coverage", 270 | "pest-update-snapshots": "@pest -d --update-snapshots", 271 | "phpmnd": "@php ./vendor/bin/phpmnd src --exclude-path=Support/Utils.php --hint --progress --ansi -v", 272 | "phpstan": "@php ./vendor/bin/phpstan analyse --ansi -v", 273 | "phpstan-baseline": "@phpstan --generate-baseline --allow-empty-baseline", 274 | "phpstan-split-baseline": [ 275 | "@phpstan --generate-baseline=baselines/loader.neon --allow-empty-baseline", 276 | "find baselines/ -type f -not -name loader.neon -delete", 277 | "@php ./vendor/bin/split-phpstan-baseline baselines/loader.neon" 278 | ], 279 | "post-merge": [ 280 | "composer install --ansi -v" 281 | ], 282 | "psalm": "@php ./vendor/bin/psalm", 283 | "psalm-baseline": "@psalm --update-baseline", 284 | "putenvs": [ 285 | "@putenv PHP73=/opt/homebrew/opt/php@7.3/bin/php", 286 | "@putenv PHP74=/opt/homebrew/opt/php@7.4/bin/php", 287 | "@putenv PHP80=/opt/homebrew/opt/php@8.0/bin/php", 288 | "@putenv PHP81=/opt/homebrew/opt/php@8.1/bin/php", 289 | "@putenv PHP82=/opt/homebrew/opt/php@8.2/bin/php", 290 | "@putenv PHP83=/opt/homebrew/opt/php@8.3/bin/php", 291 | "@putenv PHP84=/opt/homebrew/opt/php@8.4/bin/php" 292 | ], 293 | "rector": "@php ./vendor/bin/rector --ansi -v", 294 | "rector-clear-cache": "@rector --clear-cache", 295 | "rector-clear-cache-dry-run": "@rector-clear-cache --dry-run", 296 | "rector-custom-rule": "@rector custom-rule", 297 | "rector-detect-node": "@rector detect-node --loop", 298 | "rector-dry-run": "@rector --dry-run", 299 | "rector-list-rules": "@rector list-rules", 300 | "rector-setup-ci": "@rector setup-ci", 301 | "release": "@php ./vendor/bin/monorepo-builder release --ansi -v", 302 | "release-1.0.0-BETA1": "@release 1.0.0-BETA1", 303 | "release-1.0.0-BETA1-dry-run": "@release-1.0.0-BETA1 --dry-run", 304 | "release-major": "@release major", 305 | "release-major-dry-run": "@release-major --dry-run", 306 | "release-minor": "@release minor", 307 | "release-minor-dry-run": "@release-minor --dry-run", 308 | "release-patch": "@release patch", 309 | "release-patch-dry-run": "@release-patch --dry-run", 310 | "sk": "@php ./vendor/bin/swiss-knife --ansi -v", 311 | "sk-check-commented-code": "@sk check-commented-code ./config ./src --line-limit=5", 312 | "sk-check-conflicts": "@sk check-conflicts ./config ./src", 313 | "sk-finalize-classes": "@sk finalize-classes ./config ./src", 314 | "sk-finalize-classes-dry-run": "@sk-finalize-classes --dry-run", 315 | "sk-find-multi-classes": "@sk find-multi-classes ./config ./src", 316 | "sk-namespace-to-psr-4": "@sk namespace-to-psr-4 ./src --namespace-root=Guanguans\\LaravelExceptionNotify\\", 317 | "style-fix": "@php ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --show-progress=dots --diff --ansi -v", 318 | "style-lint": "@style-fix --diff --dry-run", 319 | "test": "@pest", 320 | "test-coverage": "@pest-coverage", 321 | "test-highest": "@pest-highest", 322 | "test-migrate-configuration": "@pest-migrate-configuration", 323 | "test-phpunit": "@php ./vendor/bin/phpunit --cache-result-file=./.build/phpunit/.phpunit.result.cache --coverage-text --ansi -v", 324 | "test-phpunit-coverage": "@test --coverage-html=./.build/phpunit/ --coverage-clover=clover.xml", 325 | "test-type-coverage": "@pest-type-coverage", 326 | "test-update-snapshots": "@pest-update-snapshots", 327 | "time-end": "@php -r 'date_default_timezone_set('\\''Asia/Shanghai'\\''); echo \"\\nTime: \".round(time() - (int) getenv('\\''START_TIME'\\'')).'\\'' seconds, Memory: '\\''.round(memory_get_peak_usage(true) / 1024 / 1024, 2).\" MB\\n\";'", 328 | "time-start": "@putenv START_TIME=$(date +%s);", 329 | "trufflehog": "trufflehog git https://github.com/guanguans/notify --only-verified", 330 | "yaml-lint": "@php ./vendor/bin/yaml-lint .github --ansi -v", 331 | "zh-fix": "@zh-lint --fix", 332 | "zh-lint": "zhlint {./,docs/,docs/**/}*-zh_CN.md" 333 | }, 334 | "scripts-aliases": { 335 | "normalized": [ 336 | "composer-normalize" 337 | ], 338 | "normalized-dry-run": [ 339 | "composer-normalize-dry-run" 340 | ], 341 | "pest": [ 342 | "test" 343 | ], 344 | "pest-bail": [ 345 | "test-bail" 346 | ], 347 | "pest-coverage": [ 348 | "test-coverage" 349 | ], 350 | "pest-disable-coverage-ignore": [ 351 | "test-disable-coverage-ignore" 352 | ], 353 | "pest-highest": [ 354 | "test-highest" 355 | ], 356 | "pest-migrate-configuration": [ 357 | "test-migrate-configuration" 358 | ], 359 | "pest-parallel": [ 360 | "test-parallel" 361 | ], 362 | "pest-profile": [ 363 | "test-profile" 364 | ], 365 | "pest-type-coverage": [ 366 | "test-type-coverage" 367 | ], 368 | "pest-update-snapshots": [ 369 | "test-update-snapshots" 370 | ] 371 | }, 372 | "$schema": "https://getcomposer.org/schema.json" 373 | } 374 | -------------------------------------------------------------------------------- /config/exception-notify.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | * 13 | * @see https://github.com/guanguans/laravel-exception-notify 14 | */ 15 | 16 | use Guanguans\LaravelExceptionNotify\Collectors; 17 | use Guanguans\LaravelExceptionNotify\Jobs\ReportExceptionJob; 18 | use Guanguans\LaravelExceptionNotify\Mail\ReportExceptionMail; 19 | use Guanguans\LaravelExceptionNotify\Pipes; 20 | use Guanguans\LaravelExceptionNotify\Template; 21 | use Guanguans\Notify; 22 | use function Guanguans\LaravelExceptionNotify\Support\env_explode; 23 | 24 | return [ 25 | /** 26 | * The switch of auto report exception. 27 | */ 28 | 'enabled' => (bool) env('EXCEPTION_NOTIFY_ENABLED', true), 29 | 30 | /** 31 | * The list of environment that report exception. 32 | */ 33 | 'environments' => env_explode('EXCEPTION_NOTIFY_ENVIRONMENTS', [ 34 | // 'local', 35 | // 'production', 36 | // 'testing', 37 | '*', 38 | ]), 39 | 40 | /** 41 | * The rate limiter of report same exception. 42 | */ 43 | 'rate_limiter' => [ 44 | 'cache_store' => env('EXCEPTION_NOTIFY_RATE_LIMITER_CACHE_STORE'), 45 | 'key_prefix' => env('EXCEPTION_NOTIFY_RATE_LIMITER_KEY_PREFIX', 'exception-notify:rate-limiter:'), 46 | 'max_attempts' => (int) env('EXCEPTION_NOTIFY_RATE_LIMITER_MAX_ATTEMPTS', config('app.debug') ? \PHP_INT_MAX : 1), 47 | 'decay_seconds' => (int) env('EXCEPTION_NOTIFY_RATE_LIMITER_DECAY_SECONDS', 300), 48 | ], 49 | 50 | /** 51 | * The job of report exception. 52 | */ 53 | 'job' => [ 54 | 'class' => ReportExceptionJob::class, 55 | 'connection' => env('EXCEPTION_NOTIFY_JOB_CONNECTION'), 56 | 'queue' => env('EXCEPTION_NOTIFY_JOB_QUEUE'), 57 | ], 58 | 59 | /** 60 | * The list of collector that report exception. 61 | */ 62 | 'collectors' => [ 63 | Collectors\ApplicationCollector::class, 64 | Collectors\ChoreCollector::class, 65 | Collectors\RequestBasicCollector::class, 66 | Collectors\ExceptionBasicCollector::class, 67 | Collectors\ExceptionContextCollector::class, 68 | Collectors\ExceptionTraceCollector::class, 69 | // Collectors\RequestHeaderCollector::class, 70 | // Collectors\RequestQueryCollector::class, 71 | // Collectors\RequestPostCollector::class, 72 | // Collectors\RequestFileCollector::class, 73 | ], 74 | 75 | /** 76 | * The title of report exception. 77 | */ 78 | 'title' => env('EXCEPTION_NOTIFY_TITLE', \sprintf('The %s application exception report', config('app.name'))), 79 | 80 | /** 81 | * The default channel of report exception. 82 | */ 83 | 'default' => env('EXCEPTION_NOTIFY_CHANNEL', 'stack'), 84 | 85 | /** 86 | * The list of channel that report exception. 87 | */ 88 | 'channels' => [ 89 | /** 90 | * @see \Guanguans\LaravelExceptionNotify\Channels\StackChannel 91 | */ 92 | 'stack' => [ 93 | 'driver' => 'stack', 94 | 'channels' => env_explode('EXCEPTION_NOTIFY_STACK_CHANNELS', [ 95 | // 'dump', 96 | 'log', 97 | // 'mail', 98 | // 'bark', 99 | // 'chanify', 100 | // 'dingTalk', 101 | // 'discord', 102 | // 'lark', 103 | // 'ntfy', 104 | // 'pushDeer', 105 | // 'slack', 106 | // 'telegram', 107 | // 'weWork', 108 | ]), 109 | ], 110 | 111 | /** 112 | * @see \Guanguans\LaravelExceptionNotify\Channels\DumpChannel 113 | */ 114 | 'dump' => [ 115 | 'driver' => 'dump', 116 | 'exit' => env('EXCEPTION_NOTIFY_DUMP_EXIT', false), 117 | ], 118 | 119 | /** 120 | * @see \Guanguans\LaravelExceptionNotify\Channels\LogChannel 121 | */ 122 | 'log' => [ 123 | 'driver' => 'log', 124 | 'channel' => env('EXCEPTION_NOTIFY_LOG_CHANNEL'), 125 | 'level' => env('EXCEPTION_NOTIFY_LOG_LEVEL', 'error'), 126 | ], 127 | 128 | /** 129 | * @see \Guanguans\LaravelExceptionNotify\Channels\MailChannel 130 | */ 131 | 'mail' => [ 132 | 'driver' => 'mail', 133 | 'mailer' => env('EXCEPTION_NOTIFY_MAIL_MAILER'), 134 | 'class' => ReportExceptionMail::class, 135 | 'title' => Template::TITLE, 136 | 'content' => Template::CONTENT, 137 | 'to' => [ 138 | 'address' => env_explode('EXCEPTION_NOTIFY_MAIL_TO_ADDRESS', [ 139 | 'your@example.mail', 140 | ]), 141 | ], 142 | 'pipes' => [ 143 | Pipes\SprintfHtmlPipe::class, 144 | ], 145 | ], 146 | 147 | /** 148 | * @see \Guanguans\LaravelExceptionNotify\Channels\NotifyChannel 149 | * @see \Guanguans\Notify\Foundation\Authenticators 150 | * @see \Guanguans\Notify\Foundation\Client 151 | * @see \Guanguans\Notify\Foundation\Message 152 | * @see https://github.com/guanguans/notify 153 | */ 154 | // 'foo' => [ 155 | // 'driver' => 'notify', 156 | // 'authenticator' => [ 157 | // 'class' => Notify\Foo\Authenticator::class, 158 | // 'parameter1' => '...', 159 | // // ... 160 | // ], 161 | // 'client' => [ 162 | // 'class' => Notify\Foo\Client::class, 163 | // /** @see \GuzzleHttp\RequestOptions */ 164 | // 'http_options' => [], 165 | // 'extender' => static fn (Notify\Foundation\Client $client) => $client->push( 166 | // GuzzleHttp\Middleware::log( 167 | // Illuminate\Support\Facades\Log::channel(), 168 | // new GuzzleHttp\MessageFormatter(GuzzleHttp\MessageFormatter::DEBUG), 169 | // 'debug' 170 | // ), 171 | // ), 172 | // ], 173 | // 'message' => [ 174 | // 'class' => Notify\Foo\Messages\Message::class, 175 | // 'options' => [ 176 | // 'option1' => Template::TITLE, 177 | // 'option2' => Template::CONTENT, 178 | // ], 179 | // ], 180 | // 'pipes' => [ 181 | // Pipes\LimitLengthPipe::with(1024), 182 | // ], 183 | // ], 184 | 185 | 'bark' => [ 186 | 'driver' => 'notify', 187 | 'authenticator' => [ 188 | 'class' => Notify\Bark\Authenticator::class, 189 | 'token' => env('EXCEPTION_NOTIFY_BARK_TOKEN'), 190 | ], 191 | 'client' => [ 192 | 'class' => Notify\Bark\Client::class, 193 | ], 194 | 'message' => [ 195 | 'class' => Notify\Bark\Messages\Message::class, 196 | 'options' => [ 197 | 'title' => Template::TITLE, 198 | 'body' => Template::CONTENT, 199 | ], 200 | ], 201 | 'pipes' => [ 202 | Pipes\LimitLengthPipe::with(4096), 203 | ], 204 | ], 205 | 206 | 'chanify' => [ 207 | 'driver' => 'notify', 208 | 'authenticator' => [ 209 | 'class' => Notify\Chanify\Authenticator::class, 210 | 'token' => env('EXCEPTION_NOTIFY_CHANIFY_TOKEN'), 211 | ], 212 | 'client' => [ 213 | 'class' => Notify\Chanify\Client::class, 214 | ], 215 | 'message' => [ 216 | 'class' => Notify\Chanify\Messages\TextMessage::class, 217 | 'options' => [ 218 | 'title' => Template::TITLE, 219 | 'text' => Template::CONTENT, 220 | ], 221 | ], 222 | 'pipes' => [ 223 | Pipes\LimitLengthPipe::with(1024), 224 | ], 225 | ], 226 | 227 | 'dingTalk' => [ 228 | 'driver' => 'notify', 229 | 'authenticator' => [ 230 | 'class' => Notify\DingTalk\Authenticator::class, 231 | 'token' => env('EXCEPTION_NOTIFY_DINGTALK_TOKEN'), 232 | 'secret' => env('EXCEPTION_NOTIFY_DINGTALK_SECRET'), 233 | ], 234 | 'client' => [ 235 | 'class' => Notify\DingTalk\Client::class, 236 | ], 237 | 'message' => [ 238 | 'class' => Notify\DingTalk\Messages\MarkdownMessage::class, 239 | 'options' => [ 240 | 'title' => Template::TITLE, 241 | 'text' => Template::CONTENT, 242 | ], 243 | ], 244 | 'pipes' => [ 245 | Pipes\AddKeywordChorePipe::with(env('EXCEPTION_NOTIFY_DINGTALK_KEYWORD')), 246 | Pipes\SprintfMarkdownPipe::class, 247 | Pipes\LimitLengthPipe::with(20000), 248 | ], 249 | ], 250 | 251 | 'discord' => [ 252 | 'driver' => 'notify', 253 | 'authenticator' => [ 254 | 'class' => Notify\Discord\Authenticator::class, 255 | 'webHook' => env('EXCEPTION_NOTIFY_DISCORD_WEBHOOK'), 256 | ], 257 | 'client' => [ 258 | 'class' => Notify\Discord\Client::class, 259 | ], 260 | 'message' => [ 261 | 'class' => Notify\Discord\Messages\Message::class, 262 | 'options' => [ 263 | 'content' => Template::CONTENT, 264 | ], 265 | ], 266 | 'pipes' => [ 267 | Pipes\LimitLengthPipe::with(2000), 268 | ], 269 | ], 270 | 271 | 'lark' => [ 272 | 'driver' => 'notify', 273 | 'authenticator' => [ 274 | 'class' => Notify\Lark\Authenticator::class, 275 | 'token' => env('EXCEPTION_NOTIFY_LARK_TOKEN'), 276 | 'secret' => env('EXCEPTION_NOTIFY_LARK_SECRET'), 277 | ], 278 | 'client' => [ 279 | 'class' => Notify\Lark\Client::class, 280 | ], 281 | 'message' => [ 282 | 'class' => Notify\Lark\Messages\TextMessage::class, 283 | 'options' => [ 284 | 'text' => Template::CONTENT, 285 | ], 286 | ], 287 | 'pipes' => [ 288 | Pipes\AddKeywordChorePipe::with(env('EXCEPTION_NOTIFY_LARK_KEYWORD')), 289 | Pipes\LimitLengthPipe::with(30720), 290 | ], 291 | ], 292 | 293 | 'ntfy' => [ 294 | 'driver' => 'notify', 295 | 'authenticator' => [ 296 | 'class' => Notify\Ntfy\Authenticator::class, 297 | 'usernameOrToken' => env('EXCEPTION_NOTIFY_NTFY_USERNAMEORTOKEN'), 298 | 'password' => env('EXCEPTION_NOTIFY_NTFY_PASSWORD'), 299 | ], 300 | 'client' => [ 301 | 'class' => Notify\Ntfy\Client::class, 302 | ], 303 | 'message' => [ 304 | 'class' => Notify\Ntfy\Messages\Message::class, 305 | 'options' => [ 306 | 'topic' => env('EXCEPTION_NOTIFY_NTFY_TOPIC', 'laravel-exception-notify'), 307 | 'title' => Template::TITLE, 308 | 'message' => Template::CONTENT, 309 | ], 310 | ], 311 | 'pipes' => [ 312 | Pipes\LimitLengthPipe::with(4096), 313 | ], 314 | ], 315 | 316 | 'pushDeer' => [ 317 | 'driver' => 'notify', 318 | 'authenticator' => [ 319 | 'class' => Notify\PushDeer\Authenticator::class, 320 | 'token' => env('EXCEPTION_NOTIFY_PUSHDEER_TOKEN'), 321 | ], 322 | 'client' => [ 323 | 'class' => Notify\PushDeer\Client::class, 324 | ], 325 | 'message' => [ 326 | 'class' => Notify\PushDeer\Messages\Message::class, 327 | 'options' => [ 328 | 'type' => 'markdown', 329 | 'text' => Template::TITLE, 330 | 'desp' => Template::CONTENT, 331 | ], 332 | ], 333 | 'pipes' => [ 334 | Pipes\SprintfMarkdownPipe::class, 335 | // Pipes\LimitLengthPipe::with(4096), 336 | ], 337 | ], 338 | 339 | 'slack' => [ 340 | 'driver' => 'notify', 341 | 'authenticator' => [ 342 | 'class' => Notify\Slack\Authenticator::class, 343 | 'webHook' => env('EXCEPTION_NOTIFY_SLACK_WEBHOOK'), 344 | ], 345 | 'client' => [ 346 | 'class' => Notify\Slack\Client::class, 347 | ], 348 | 'message' => [ 349 | 'class' => Notify\Slack\Messages\Message::class, 350 | 'options' => [ 351 | 'mrkdwn' => true, 352 | 'text' => Template::CONTENT, 353 | ], 354 | ], 355 | 'pipes' => [ 356 | Pipes\SprintfMarkdownPipe::class, 357 | // Pipes\LimitLengthPipe::with(10240), 358 | ], 359 | ], 360 | 361 | 'telegram' => [ 362 | 'driver' => 'notify', 363 | 'authenticator' => [ 364 | 'class' => Notify\Telegram\Authenticator::class, 365 | 'token' => env('EXCEPTION_NOTIFY_TELEGRAM_TOKEN'), 366 | ], 367 | 'client' => [ 368 | 'class' => Notify\Telegram\Client::class, 369 | ], 370 | 'message' => [ 371 | 'class' => Notify\Telegram\Messages\TextMessage::class, 372 | 'options' => [ 373 | 'chat_id' => env('EXCEPTION_NOTIFY_TELEGRAM_CHAT_ID'), 374 | 'text' => Template::CONTENT, 375 | ], 376 | ], 377 | 'pipes' => [ 378 | Pipes\LimitLengthPipe::with(4096), 379 | ], 380 | ], 381 | 382 | 'weWork' => [ 383 | 'driver' => 'notify', 384 | 'authenticator' => [ 385 | 'class' => Notify\WeWork\Authenticator::class, 386 | 'token' => env('EXCEPTION_NOTIFY_WEWORK_TOKEN'), 387 | ], 388 | 'client' => [ 389 | 'class' => Notify\WeWork\Client::class, 390 | ], 391 | 'message' => [ 392 | 'class' => Notify\WeWork\Messages\MarkdownMessage::class, 393 | 'options' => [ 394 | 'content' => Template::CONTENT, 395 | ], 396 | ], 397 | 'pipes' => [ 398 | Pipes\SprintfMarkdownPipe::class, 399 | Pipes\LimitLengthPipe::with(4096), 400 | ], 401 | ], 402 | ], 403 | ]; 404 | -------------------------------------------------------------------------------- /docs/bark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guanguans/laravel-exception-notify/5637cb6dd7fc4ae9806700e12ae1c2db363ca24b/docs/bark.jpg -------------------------------------------------------------------------------- /docs/discord.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guanguans/laravel-exception-notify/5637cb6dd7fc4ae9806700e12ae1c2db363ca24b/docs/discord.jpg -------------------------------------------------------------------------------- /docs/jetbrains.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guanguans/laravel-exception-notify/5637cb6dd7fc4ae9806700e12ae1c2db363ca24b/docs/jetbrains.png -------------------------------------------------------------------------------- /docs/lark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guanguans/laravel-exception-notify/5637cb6dd7fc4ae9806700e12ae1c2db363ca24b/docs/lark.jpg -------------------------------------------------------------------------------- /docs/mail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guanguans/laravel-exception-notify/5637cb6dd7fc4ae9806700e12ae1c2db363ca24b/docs/mail.jpg -------------------------------------------------------------------------------- /docs/ntfy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guanguans/laravel-exception-notify/5637cb6dd7fc4ae9806700e12ae1c2db363ca24b/docs/ntfy.jpg -------------------------------------------------------------------------------- /docs/pushDeer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guanguans/laravel-exception-notify/5637cb6dd7fc4ae9806700e12ae1c2db363ca24b/docs/pushDeer.jpg -------------------------------------------------------------------------------- /docs/slack.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guanguans/laravel-exception-notify/5637cb6dd7fc4ae9806700e12ae1c2db363ca24b/docs/slack.jpg -------------------------------------------------------------------------------- /docs/telegram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guanguans/laravel-exception-notify/5637cb6dd7fc4ae9806700e12ae1c2db363ca24b/docs/telegram.jpg -------------------------------------------------------------------------------- /docs/weWork.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guanguans/laravel-exception-notify/5637cb6dd7fc4ae9806700e12ae1c2db363ca24b/docs/weWork.jpg -------------------------------------------------------------------------------- /src/Channels/AbstractChannel.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Channels; 15 | 16 | use Guanguans\LaravelExceptionNotify\Contracts\ChannelContract; 17 | use Guanguans\LaravelExceptionNotify\Contracts\CollectorContract; 18 | use Guanguans\LaravelExceptionNotify\Contracts\ExceptionAwareContract; 19 | use Guanguans\LaravelExceptionNotify\Exceptions\InvalidConfigurationException; 20 | use Guanguans\LaravelExceptionNotify\Jobs\ReportExceptionJob; 21 | use Guanguans\LaravelExceptionNotify\Pipes\FixPrettyJsonPipe; 22 | use Guanguans\LaravelExceptionNotify\Support\Utils; 23 | use Illuminate\Config\Repository; 24 | use Illuminate\Contracts\Queue\ShouldQueue; 25 | use Illuminate\Pipeline\Pipeline; 26 | use Illuminate\Support\Arr; 27 | use Illuminate\Support\Collection; 28 | use Illuminate\Support\Facades\Validator; 29 | use Illuminate\Support\Stringable; 30 | use function Guanguans\LaravelExceptionNotify\Support\json_pretty_encode; 31 | use function Guanguans\LaravelExceptionNotify\Support\make; 32 | 33 | abstract class AbstractChannel implements ChannelContract 34 | { 35 | /** 36 | * @throws \Throwable 37 | */ 38 | public function __construct(protected Repository $configRepository) 39 | { 40 | $validator = Validator::make( 41 | $this->configRepository->all(), 42 | $rules = $this->rules(), 43 | $this->messages(), 44 | $this->attributes() + collect(Arr::dot($rules)) 45 | ->keys() 46 | ->mapWithKeys(fn (string $attribute): array => [ 47 | $attribute => str($this->getChannel())->append('.', $attribute)->toString(), 48 | ]) 49 | ->all() 50 | ); 51 | 52 | throw_if($validator->fails(), InvalidConfigurationException::fromValidator($validator)); 53 | } 54 | 55 | /** 56 | * @throws \ReflectionException 57 | */ 58 | public function report(\Throwable $throwable): void 59 | { 60 | $pendingDispatch = dispatch($this->makeJob($throwable)); 61 | 62 | if (Utils::isSyncJobConnection() && !app()->runningInConsole()) { 63 | $pendingDispatch->afterResponse(); 64 | } 65 | 66 | // unset($pendingDispatch); 67 | } 68 | 69 | protected function rules(): array 70 | { 71 | return [ 72 | '__channel' => 'required|string', 73 | 'driver' => 'required|string', 74 | 'collectors' => 'array', 75 | 'pipes' => 'array', 76 | ]; 77 | } 78 | 79 | protected function messages(): array 80 | { 81 | return []; 82 | } 83 | 84 | protected function attributes(): array 85 | { 86 | return []; 87 | } 88 | 89 | private function getChannel(): string 90 | { 91 | return $this->configRepository->get('__channel'); 92 | } 93 | 94 | /** 95 | * @throws \ReflectionException 96 | */ 97 | private function makeJob(\Throwable $throwable): ShouldQueue 98 | { 99 | return Utils::applyConfigurationToObject( 100 | make($configuration = config('exception-notify.job') + [ 101 | 'class' => ReportExceptionJob::class, 102 | 'channel' => $this->getChannel(), 103 | 'content' => $this->getContent($throwable), 104 | ]), 105 | $configuration 106 | ); 107 | } 108 | 109 | private function getContent(\Throwable $throwable): string 110 | { 111 | return (string) (new Pipeline(app())) 112 | ->send($this->getCollectors($throwable)) 113 | ->through($this->getPipes()) 114 | ->then(static fn (Collection $collectors): Stringable => str(json_pretty_encode($collectors->jsonSerialize()))); 115 | } 116 | 117 | private function getCollectors(\Throwable $throwable): Collection 118 | { 119 | return collect([ 120 | ...(array) config('exception-notify.collectors'), 121 | ...(array) $this->configRepository->get('collectors'), 122 | ])->mapWithKeys(static function (array|string $parameters, int|string $class) use ($throwable): array { 123 | if (!\is_array($parameters)) { 124 | [$parameters, $class] = [(array) $class, $parameters]; 125 | } 126 | 127 | /** @var CollectorContract $collectorContract */ 128 | $collectorContract = resolve($class, $parameters); 129 | $collectorContract instanceof ExceptionAwareContract and $collectorContract->setException($throwable); 130 | 131 | return [$collectorContract->name() => $collectorContract->collect()]; 132 | }); 133 | } 134 | 135 | private function getPipes(): array 136 | { 137 | return collect($this->configRepository->get('pipes'))->prepend(FixPrettyJsonPipe::class)->all(); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Channels/Channel.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Channels; 15 | 16 | use Guanguans\LaravelExceptionNotify\Contracts\ChannelContract; 17 | use Guanguans\LaravelExceptionNotify\Events\ReportedEvent; 18 | use Guanguans\LaravelExceptionNotify\Events\ReportingEvent; 19 | use Guanguans\LaravelExceptionNotify\Support\Traits\AggregationTrait; 20 | use Guanguans\LaravelExceptionNotify\Support\Utils; 21 | use Illuminate\Cache\RateLimiter; 22 | use Illuminate\Support\Facades\Cache; 23 | use Illuminate\Support\Facades\Event; 24 | use function Guanguans\LaravelExceptionNotify\Support\make; 25 | use function Guanguans\LaravelExceptionNotify\Support\rescue; 26 | 27 | /** 28 | * @see \Illuminate\Log\Logger 29 | * @see \Illuminate\Log\LogManager::createEmergencyLogger() 30 | * @see \Illuminate\Log\LogManager::get() 31 | * @see \Illuminate\Log\LogManager::stack() 32 | */ 33 | class Channel implements ChannelContract 34 | { 35 | use AggregationTrait; 36 | 37 | /** @var list */ 38 | private static array $skipCallbacks = []; 39 | 40 | public function __construct( 41 | private ChannelContract $channelContract 42 | ) {} 43 | 44 | public function report(\Throwable $throwable): void 45 | { 46 | rescue(function () use ($throwable): void { 47 | $this->shouldReport($throwable) and $this->channelContract->report($throwable); 48 | }); 49 | } 50 | 51 | public function reportContent(string $content): mixed 52 | { 53 | return rescue(function () use ($content): mixed { 54 | Event::dispatch(new ReportingEvent($this->channelContract, $content)); 55 | $result = $this->channelContract->reportContent($content); 56 | Event::dispatch(new ReportedEvent($this->channelContract, $result)); 57 | 58 | return $result; 59 | }); 60 | } 61 | 62 | public function reporting(mixed $listener): void 63 | { 64 | Event::listen(ReportingEvent::class, $listener); 65 | } 66 | 67 | public function reported(mixed $listener): void 68 | { 69 | Event::listen(ReportedEvent::class, $listener); 70 | } 71 | 72 | public static function skipWhen(\Closure $callback): void 73 | { 74 | self::$skipCallbacks[] = $callback; 75 | } 76 | 77 | public static function flush(): void 78 | { 79 | self::$skipCallbacks = []; 80 | } 81 | 82 | /** 83 | * @throws \ReflectionException 84 | */ 85 | public function shouldReport(\Throwable $throwable): bool 86 | { 87 | return !$this->shouldntReport($throwable); 88 | } 89 | 90 | /** 91 | * @throws \ReflectionException 92 | * 93 | * @see \Illuminate\Foundation\Exceptions\Handler::shouldntReport() 94 | */ 95 | private function shouldntReport(\Throwable $throwable): bool 96 | { 97 | return !app()->environment(config('exception-notify.environments')) 98 | || $this->shouldSkip($throwable) 99 | || !$this->attempt($throwable); 100 | } 101 | 102 | private function shouldSkip(\Throwable $throwable): bool 103 | { 104 | foreach (self::$skipCallbacks as $skipCallback) { 105 | if ($skipCallback($throwable)) { 106 | return true; 107 | } 108 | } 109 | 110 | return false; 111 | } 112 | 113 | /** 114 | * @see RateLimiter::attempt() 115 | * @see \Illuminate\Cache\RateLimiting\Limit 116 | * 117 | * @throws \ReflectionException 118 | */ 119 | private function attempt(\Throwable $throwable): bool 120 | { 121 | return $this->makeRateLimiter()->attempt( 122 | $this->fingerprintFor($throwable), 123 | config('exception-notify.rate_limiter.max_attempts'), 124 | static fn (): bool => true, 125 | config('exception-notify.rate_limiter.decay_seconds') 126 | ); 127 | } 128 | 129 | /** 130 | * @throws \ReflectionException 131 | */ 132 | private function makeRateLimiter(): RateLimiter 133 | { 134 | return Utils::applyConfigurationToObject( 135 | make($configuration = config('exception-notify.rate_limiter') + [ 136 | 'class' => RateLimiter::class, 137 | 'cache' => Cache::store(config('exception-notify.rate_limiter.cache_store')), 138 | ]), 139 | $configuration 140 | ); 141 | } 142 | 143 | /** 144 | * @see \Illuminate\Foundation\Exceptions\Handler::shouldntReport() 145 | * @see \Illuminate\Foundation\Exceptions\Handler::throttle() 146 | * @see \Illuminate\Foundation\Exceptions\Handler::throttleUsing() 147 | */ 148 | private function fingerprintFor(\Throwable $throwable): string 149 | { 150 | return config('exception-notify.rate_limiter.key_prefix').sha1( 151 | implode(':', [$throwable->getFile(), $throwable->getLine(), $throwable->getCode()]) 152 | ); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Channels/DumpChannel.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Channels; 15 | 16 | /** 17 | * @see \Symfony\Component\VarDumper\VarDumper::dump() 18 | */ 19 | class DumpChannel extends AbstractChannel 20 | { 21 | /** 22 | * @noinspection ForgottenDebugOutputInspection 23 | * @noinspection DebugFunctionUsageInspection 24 | */ 25 | public function reportContent(string $content): mixed 26 | { 27 | return $this->configRepository->get('exit', false) ? dd($content) : dump($content); 28 | } 29 | 30 | protected function rules(): array 31 | { 32 | return [ 33 | 'exit' => 'bool', 34 | ] + parent::rules(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Channels/LogChannel.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Channels; 15 | 16 | use Illuminate\Support\Facades\Log; 17 | 18 | /** 19 | * @see \Illuminate\Log\LogManager 20 | */ 21 | class LogChannel extends AbstractChannel 22 | { 23 | public function reportContent(string $content): mixed 24 | { 25 | Log::channel($this->configRepository->get('channel'))->log( 26 | $this->configRepository->get('level', 'error'), 27 | $content, 28 | $this->configRepository->get('context', []), 29 | ); 30 | 31 | return null; 32 | } 33 | 34 | protected function rules(): array 35 | { 36 | return [ 37 | 'channel' => 'nullable|string', 38 | 'level' => 'string', 39 | 'context' => 'array', 40 | ] + parent::rules(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Channels/MailChannel.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Channels; 15 | 16 | use Guanguans\LaravelExceptionNotify\Mail\ReportExceptionMail; 17 | use Guanguans\LaravelExceptionNotify\Support\Utils; 18 | use Illuminate\Mail\Mailable; 19 | use Illuminate\Mail\SentMessage; 20 | use Illuminate\Support\Facades\Mail; 21 | use function Guanguans\LaravelExceptionNotify\Support\make; 22 | 23 | /** 24 | * @see \Illuminate\Mail\MailManager 25 | * @see \Illuminate\Mail\Mailer 26 | * @see \Illuminate\Mail\PendingMail 27 | */ 28 | class MailChannel extends AbstractChannel 29 | { 30 | /** 31 | * @throws \ReflectionException 32 | */ 33 | public function reportContent(string $content): ?SentMessage 34 | { 35 | return Mail::mailer($this->configRepository->get('mailer'))->send($this->makeMail($content)); 36 | } 37 | 38 | protected function rules(): array 39 | { 40 | return [ 41 | 'mailer' => 'nullable|string', 42 | 'to' => 'required|array', 43 | ] + parent::rules(); 44 | } 45 | 46 | /** 47 | * @throws \ReflectionException 48 | */ 49 | private function makeMail(string $content): Mailable 50 | { 51 | return Utils::applyConfigurationToObject( 52 | make($configuration = Utils::applyContentToConfiguration( 53 | $this->configRepository->all() + ['class' => ReportExceptionMail::class], 54 | $content 55 | )), 56 | $configuration 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Channels/NotifyChannel.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Channels; 15 | 16 | use Guanguans\LaravelExceptionNotify\Support\Utils; 17 | use Guanguans\Notify\Foundation\Contracts\Authenticator; 18 | use Guanguans\Notify\Foundation\Contracts\Client; 19 | use Guanguans\Notify\Foundation\Message; 20 | use Psr\Http\Message\ResponseInterface; 21 | use function Guanguans\LaravelExceptionNotify\Support\make; 22 | 23 | /** 24 | * @see \Guanguans\Notify\Foundation\Authenticators 25 | * @see \Guanguans\Notify\Foundation\Client 26 | * @see \Guanguans\Notify\Foundation\Message 27 | */ 28 | class NotifyChannel extends AbstractChannel 29 | { 30 | /** 31 | * @throws \ReflectionException 32 | */ 33 | public function reportContent(string $content): ResponseInterface 34 | { 35 | return $this->makeClient()->send($this->makeMessage($content)); 36 | } 37 | 38 | protected function rules(): array 39 | { 40 | return [ 41 | 'authenticator' => 'required|array', 42 | 'authenticator.class' => 'required|string', 43 | 'client' => 'required|array', 44 | 'client.class' => 'required|string', 45 | 'message' => 'required|array', 46 | 'message.class' => 'required|string', 47 | ] + parent::rules(); 48 | } 49 | 50 | /** 51 | * @throws \ReflectionException 52 | */ 53 | private function makeClient(): Client 54 | { 55 | return Utils::applyConfigurationToObject( 56 | make($configuration = $this->configRepository->get('client') + [ 57 | 'authenticator' => $this->makeAuthenticator(), 58 | ]), 59 | $configuration 60 | ); 61 | } 62 | 63 | /** 64 | * @throws \ReflectionException 65 | */ 66 | private function makeAuthenticator(): Authenticator 67 | { 68 | return Utils::applyConfigurationToObject( 69 | make($configuration = $this->configRepository->get('authenticator')), 70 | $configuration 71 | ); 72 | } 73 | 74 | /** 75 | * @throws \ReflectionException 76 | */ 77 | private function makeMessage(string $content): Message 78 | { 79 | return Utils::applyConfigurationToObject( 80 | make($configuration = Utils::applyContentToConfiguration($this->configRepository->get('message'), $content)), 81 | $configuration 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Channels/StackChannel.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Channels; 15 | 16 | use Guanguans\LaravelExceptionNotify\Facades\ExceptionNotify; 17 | use function Guanguans\LaravelExceptionNotify\Support\rescue; 18 | 19 | /** 20 | * @see \Illuminate\Log\LogManager::createStackDriver() 21 | */ 22 | class StackChannel extends AbstractChannel 23 | { 24 | /** 25 | * @noinspection MissingParentCallInspection 26 | * @noinspection PhpMissingParentCallCommonInspection 27 | */ 28 | public function report(\Throwable $throwable): void 29 | { 30 | collect($this->configRepository->get('channels'))->each( 31 | static function (string $channel) use ($throwable): void { 32 | rescue(static fn (): mixed => ExceptionNotify::driver($channel)->report($throwable)); 33 | } 34 | ); 35 | } 36 | 37 | public function reportContent(string $content): array 38 | { 39 | return collect($this->configRepository->get('channels')) 40 | ->mapWithKeys( 41 | static fn (string $channel): array => [ 42 | $channel => rescue(static fn (): mixed => ExceptionNotify::driver($channel)->reportContent($content)), 43 | ] 44 | ) 45 | ->all(); 46 | } 47 | 48 | protected function rules(): array 49 | { 50 | return [ 51 | 'channels' => 'array', 52 | ] + parent::rules(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Collectors/AbstractCollector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Collectors; 15 | 16 | use Guanguans\LaravelExceptionNotify\Contracts\CollectorContract; 17 | 18 | /** 19 | * @see https://github.com/laravel/telescope/tree/5.x/src/Watchers 20 | * @see https://github.com/spatie/laravel-ray/tree/main/src/Watchers 21 | */ 22 | abstract class AbstractCollector implements CollectorContract 23 | { 24 | public function name(): string 25 | { 26 | return static::fallbackName(); 27 | } 28 | 29 | public static function fallbackName(): string 30 | { 31 | return str(class_basename(static::class)) 32 | ->beforeLast('Collector') 33 | ->headline() 34 | ->toString(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Collectors/AbstractExceptionCollector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Collectors; 15 | 16 | use Guanguans\LaravelExceptionNotify\Contracts\ExceptionAwareContract; 17 | use Symfony\Component\ErrorHandler\Exception\FlattenException; 18 | 19 | abstract class AbstractExceptionCollector extends AbstractCollector implements ExceptionAwareContract 20 | { 21 | protected \Throwable $exception; 22 | protected FlattenException $flattenException; 23 | 24 | public function setException(\Throwable $throwable): void 25 | { 26 | $this->exception = $throwable; 27 | $this->flattenException = FlattenException::createFromThrowable($throwable); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Collectors/ApplicationCollector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Collectors; 15 | 16 | use Illuminate\Container\Container; 17 | use Illuminate\Support\Carbon; 18 | 19 | class ApplicationCollector extends AbstractCollector 20 | { 21 | public function __construct( 22 | /** @var \Illuminate\Foundation\Application */ 23 | private Container $container 24 | ) {} 25 | 26 | /** 27 | * @see \Illuminate\Foundation\Console\AboutCommand::gatherApplicationInformation() 28 | */ 29 | public function collect(): array 30 | { 31 | return [ 32 | 'Time' => Carbon::now()->format('Y-m-d H:i:s'), 33 | 'Name' => config('app.name'), 34 | 'Version' => $this->container->version(), 35 | 'PHP Version' => \PHP_VERSION, 36 | 'Environment' => $this->container->environment(), 37 | 'In Console' => $this->container->runningInConsole(), 38 | 'Debug Mode' => $this->container->hasDebugModeEnabled(), 39 | 'Maintenance Mode' => $this->container->isDownForMaintenance(), 40 | 'Timezone' => config('app.timezone'), 41 | 'Locale' => config('app.locale'), 42 | ]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Collectors/ChoreCollector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Collectors; 15 | 16 | class ChoreCollector extends AbstractCollector 17 | { 18 | public function collect(): array 19 | { 20 | return []; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Collectors/ExceptionBasicCollector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Collectors; 15 | 16 | class ExceptionBasicCollector extends AbstractExceptionCollector 17 | { 18 | /** 19 | * @@see https://github.com/symfony/symfony/blob/7.1/src/Symfony/Component/Messenger/Transport/Serialization/Normalizer/FlattenExceptionNormalizer.php 20 | */ 21 | public function collect(): array 22 | { 23 | return [ 24 | 'Message' => $this->exception->getMessage(), 25 | 'Code' => $this->exception->getCode(), 26 | 'Class' => $this->exception::class, 27 | 'File' => $this->exception->getFile(), 28 | 'Line' => $this->exception->getLine(), 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Collectors/ExceptionContextCollector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Collectors; 15 | 16 | use Guanguans\LaravelExceptionNotify\Support\ExceptionContext; 17 | 18 | class ExceptionContextCollector extends AbstractExceptionCollector 19 | { 20 | public function __construct( 21 | private string $mark = '➤', 22 | private int $number = 5 23 | ) {} 24 | 25 | public function collect(): array 26 | { 27 | return ExceptionContext::getMarked($this->exception, $this->mark, $this->number); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Collectors/ExceptionTraceCollector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Collectors; 15 | 16 | use Illuminate\Support\Str; 17 | 18 | class ExceptionTraceCollector extends AbstractExceptionCollector 19 | { 20 | private array $except; 21 | 22 | public function __construct(?array $except = null) 23 | { 24 | $this->except = $except ?? ['vendor']; 25 | } 26 | 27 | public function collect(): array 28 | { 29 | return str($this->exception->getTraceAsString()) 30 | ->explode(\PHP_EOL) 31 | ->reject(fn (string $trace): bool => str($trace)->contains($this->except)) 32 | ->map(static fn (string $trace): string => Str::replaceFirst(base_path().\DIRECTORY_SEPARATOR, '', $trace)) 33 | ->unique(static fn (string $trace, int $index): string => Str::replaceFirst("#$index ", '', $trace)) 34 | ->all(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Collectors/RequestBasicCollector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Collectors; 15 | 16 | use Guanguans\LaravelExceptionNotify\Support\Utils; 17 | use Illuminate\Http\Request; 18 | 19 | class RequestBasicCollector extends AbstractCollector 20 | { 21 | public function __construct(private Request $request) {} 22 | 23 | public function collect(): array 24 | { 25 | return [ 26 | 'URL' => $this->request->url(), 27 | 'IP' => $this->request->ip(), 28 | 'Method' => $this->request->method(), 29 | 'Controller Action' => $this->request->route()?->getActionName(), 30 | 'Memory' => Utils::humanBytes(memory_get_peak_usage(true)), 31 | 'Duration' => blank( 32 | $startTime = \defined('LARAVEL_START') ? LARAVEL_START : $this->request->server('REQUEST_TIME_FLOAT') 33 | ) ? 'Unknown' : Utils::humanMilliseconds((microtime(true) - $startTime) * 1000), 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Collectors/RequestFileCollector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Collectors; 15 | 16 | use Guanguans\LaravelExceptionNotify\Support\Utils; 17 | use Illuminate\Http\Request; 18 | use Illuminate\Http\UploadedFile; 19 | 20 | class RequestFileCollector extends AbstractCollector 21 | { 22 | public function __construct(private Request $request) {} 23 | 24 | /** 25 | * @noinspection CallableParameterUseCaseInTypeContextInspection 26 | * 27 | * @see tests/FeatureTest.php 28 | * 29 | * ```json 30 | * { 31 | * "name": "images.jpeg", 32 | * "full_path": "images.jpeg", 33 | * "type": "image/jpeg", 34 | * "tmp_name": "/private/var/tmp/phpz7mx94", 35 | * "error": 0, 36 | * "size": 2075 37 | * } 38 | * ``` 39 | */ 40 | public function collect(): array 41 | { 42 | $files = $this->request->allFiles(); 43 | 44 | array_walk_recursive($files, static function (UploadedFile &$uploadedFile): void { 45 | $uploadedFile = [ 46 | 'name' => $uploadedFile->getClientOriginalName(), 47 | 'type' => $uploadedFile->getMimeType(), 48 | 'tmp_name' => $uploadedFile->getPathname(), 49 | // 'error' => $uploadedFile->getError(), 50 | 'error' => $uploadedFile->getErrorMessage(), 51 | 'size' => Utils::humanBytes($uploadedFile->getSize()), 52 | ]; 53 | }); 54 | 55 | return $files; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Collectors/RequestHeaderCollector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Collectors; 15 | 16 | use Illuminate\Http\Request; 17 | 18 | class RequestHeaderCollector extends AbstractCollector 19 | { 20 | private array $except; 21 | 22 | public function __construct( 23 | private Request $request, 24 | ?array $except = null 25 | ) { 26 | $this->except = $except ?? [ 27 | 'Authorization', 28 | 'Cookie', 29 | ]; 30 | } 31 | 32 | public function collect(): array 33 | { 34 | return collect($this->request->header()) 35 | ->reject(fn (array $header, string $key): bool => str($key)->is($this->except)) 36 | ->map(static fn (array $header): string => $header[0]) 37 | ->all(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Collectors/RequestPostCollector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Collectors; 15 | 16 | use Illuminate\Http\Request; 17 | 18 | class RequestPostCollector extends AbstractCollector 19 | { 20 | private array $masks; 21 | 22 | public function __construct( 23 | private Request $request, 24 | ?array $masks = null 25 | ) { 26 | $this->masks = $masks ?? [ 27 | 'password', 28 | '*password', 29 | 'password*', 30 | '*password*', 31 | ]; 32 | } 33 | 34 | public function collect(): array 35 | { 36 | return collect($this->request->post()) 37 | ->map(fn (mixed $value, string $key): mixed => str($key)->is($this->masks) ? '******' : $value) 38 | ->all(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Collectors/RequestQueryCollector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Collectors; 15 | 16 | use Illuminate\Http\Request; 17 | 18 | class RequestQueryCollector extends AbstractCollector 19 | { 20 | public function __construct(private Request $request) {} 21 | 22 | public function collect(): array 23 | { 24 | return $this->request->query(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Commands/Concerns/Configureable.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | * 13 | * @see https://github.com/guanguans/laravel-exception-notify 14 | */ 15 | 16 | namespace Guanguans\LaravelExceptionNotify\Commands\Concerns; 17 | 18 | use Illuminate\Support\Collection; 19 | use Symfony\Component\Console\Input\InputDefinition; 20 | use Symfony\Component\Console\Input\InputInterface; 21 | use Symfony\Component\Console\Input\InputOption; 22 | use Symfony\Component\Console\Output\OutputInterface; 23 | 24 | /** 25 | * @mixin \Illuminate\Console\Command 26 | */ 27 | trait Configureable 28 | { 29 | public function getDefinition(): InputDefinition 30 | { 31 | return tap(parent::getDefinition(), static function (InputDefinition $inputDefinition): void { 32 | $inputDefinition->addOption(new InputOption( 33 | 'configuration', 34 | null, 35 | InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 36 | 'Used to dynamically pass one or more configuration key-value pairs(e.g. `--configuration=app.name=guanguans` or `--configuration app.name=guanguans`).', 37 | )); 38 | }); 39 | } 40 | 41 | protected function initialize(InputInterface $input, OutputInterface $output): void 42 | { 43 | parent::initialize($input, $output); 44 | 45 | collect($this->option('configuration')) 46 | // ->dump() 47 | ->mapWithKeys(static function (string $configuration): array { 48 | \assert( 49 | str_contains($configuration, '='), 50 | "The configureable option [$configuration] must be formatted as key=value." 51 | ); 52 | 53 | [$key, $value] = explode('=', $configuration, 2); 54 | 55 | return [$key => $value]; 56 | }) 57 | ->tap(static fn (Collection $configuration) => config($configuration->all())); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Commands/TestCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Commands; 15 | 16 | use Guanguans\LaravelExceptionNotify\Commands\Concerns\Configureable; 17 | use Guanguans\LaravelExceptionNotify\ExceptionNotifyManager; 18 | use Guanguans\LaravelExceptionNotify\Exceptions\RuntimeException; 19 | use Guanguans\LaravelExceptionNotify\Support\Utils; 20 | use Guanguans\Notify\Foundation\Client; 21 | use Guanguans\Notify\Foundation\Response; 22 | use GuzzleHttp\MessageFormatter; 23 | use GuzzleHttp\Middleware; 24 | use Illuminate\Console\Command; 25 | use Illuminate\Support\Facades\Log; 26 | use Symfony\Component\Console\Input\InputInterface; 27 | use Symfony\Component\Console\Output\OutputInterface; 28 | 29 | class TestCommand extends Command 30 | { 31 | use Configureable { 32 | Configureable::initialize as configureableInitialize; 33 | } 34 | 35 | /** @noinspection ClassOverridesFieldOfSuperClassInspection */ 36 | protected $signature = <<<'SIGNATURE' 37 | exception-notify:test 38 | {--c|channel= : The channel of report exception} 39 | {--job-connection= : The connection of report exception job} 40 | SIGNATURE; 41 | 42 | /** @noinspection ClassOverridesFieldOfSuperClassInspection */ 43 | protected $description = 'Test whether exception can be monitored and reported to notification channel'; 44 | 45 | /** 46 | * @throws \ReflectionException 47 | */ 48 | public function handle(ExceptionNotifyManager $exceptionNotifyManager): int 49 | { 50 | if (!config($configurationKey = 'exception-notify.enabled')) { 51 | $this->components->warn(\sprintf( 52 | 'The value of this configuration [%s] is false, please configure it to true.', 53 | $this->warned($configurationKey) 54 | )); 55 | 56 | return self::INVALID; 57 | } 58 | 59 | $runtimeException = new RuntimeException('This is a test.'); 60 | 61 | if (!$exceptionNotifyManager->shouldReport($runtimeException)) { 62 | $this->components->warn(\sprintf( 63 | 'The exception [%s] should not be reported, please check the related configuration:', 64 | $this->warned($runtimeException::class), 65 | )); 66 | 67 | $this->components->bulletList([ 68 | 'exception-notify.environments', 69 | 'exception-notify.rate_limiter', 70 | ]); 71 | 72 | return self::INVALID; 73 | } 74 | 75 | try { 76 | throw $runtimeException; 77 | } finally { 78 | $this->laravel->terminating(function () use ($runtimeException): void { 79 | $this->components->warn(\sprintf( 80 | 'The exception [%s] has been thrown.', 81 | $this->warned($runtimeException::class) 82 | )); 83 | $this->components->warn(\sprintf( 84 | 'Please check whether the exception-notify channel [%s] has received an exception report.', 85 | $this->warned(config('exception-notify.default')) 86 | )); 87 | $this->components->warn(\sprintf( 88 | 'If not, please find the reason in the default log channel [%s].', 89 | $this->warned(config('logging.default')) 90 | )); 91 | 92 | if (!Utils::isSyncJobConnection()) { 93 | $this->components->warn(\sprintf( 94 | 'Or please ensure that the queue is working [%s].', 95 | $this->warned(\sprintf( 96 | 'php artisan queue:work %s --queue=%s --ansi -v', 97 | Utils::jobConnection(), 98 | Utils::jobQueue() 99 | )) 100 | )); 101 | } 102 | 103 | $this->components->info('Testing done.'); 104 | }); 105 | } 106 | } 107 | 108 | /** 109 | * @noinspection MethodVisibilityInspection 110 | * @noinspection PhpMissingParentCallCommonInspection 111 | */ 112 | protected function initialize(InputInterface $input, OutputInterface $output): void 113 | { 114 | $this->configureableInitialize($input, $output); 115 | 116 | $channel = $this->option('channel') and config()->set('exception-notify.default', $channel); 117 | $connection = $this->option('job-connection') and config()->set('exception-notify.job.connection', $connection); 118 | 119 | collect(config('exception-notify.channels'))->each(function (array $configuration, string $name): void { 120 | if ('notify' === ($configuration['driver'] ?? $name)) { 121 | config()->set( 122 | "exception-notify.channels.$name.client.extender", 123 | function (Client $client): Client { 124 | $client->push(Middleware::log(Log::channel(), new MessageFormatter(MessageFormatter::DEBUG), 'debug')); 125 | 126 | if ($this->output->isVerbose()) { 127 | $client->before( 128 | \Guanguans\Notify\Foundation\Middleware\Response::class, 129 | Middleware::mapResponse(static fn (Response $response): Response => $response->dump()), 130 | ); 131 | } 132 | 133 | return $client; 134 | } 135 | ); 136 | } 137 | }); 138 | } 139 | 140 | private function warned(string $string): string 141 | { 142 | return "$string"; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Contracts/ChannelContract.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Contracts; 15 | 16 | interface ChannelContract 17 | { 18 | public function report(\Throwable $throwable): void; 19 | 20 | public function reportContent(string $content): mixed; 21 | } 22 | -------------------------------------------------------------------------------- /src/Contracts/CollectorContract.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Contracts; 15 | 16 | interface CollectorContract 17 | { 18 | public function name(): string; 19 | 20 | public function collect(): array; 21 | } 22 | -------------------------------------------------------------------------------- /src/Contracts/ExceptionAwareContract.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Contracts; 15 | 16 | interface ExceptionAwareContract 17 | { 18 | public function setException(\Throwable $throwable): void; 19 | } 20 | -------------------------------------------------------------------------------- /src/Contracts/ThrowableContract.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Contracts; 15 | 16 | interface ThrowableContract extends \Throwable {} 17 | -------------------------------------------------------------------------------- /src/Events/ReportedEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Events; 15 | 16 | use Guanguans\LaravelExceptionNotify\Contracts\ChannelContract; 17 | 18 | class ReportedEvent 19 | { 20 | public function __construct( 21 | public ChannelContract $channelContract, 22 | public mixed $result 23 | ) {} 24 | } 25 | -------------------------------------------------------------------------------- /src/Events/ReportingEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Events; 15 | 16 | use Guanguans\LaravelExceptionNotify\Contracts\ChannelContract; 17 | 18 | class ReportingEvent 19 | { 20 | public function __construct( 21 | public ChannelContract $channelContract, 22 | public string $content 23 | ) {} 24 | } 25 | -------------------------------------------------------------------------------- /src/ExceptionNotifyManager.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | * 13 | * @see https://github.com/guanguans/laravel-exception-notify 14 | */ 15 | 16 | namespace Guanguans\LaravelExceptionNotify; 17 | 18 | use Guanguans\LaravelExceptionNotify\Channels\Channel; 19 | use Guanguans\LaravelExceptionNotify\Contracts\ChannelContract; 20 | use Guanguans\LaravelExceptionNotify\Exceptions\InvalidArgumentException; 21 | use Guanguans\LaravelExceptionNotify\Support\Traits\AggregationTrait; 22 | use Illuminate\Config\Repository; 23 | use Illuminate\Support\Manager; 24 | use function Guanguans\LaravelExceptionNotify\Support\rescue; 25 | 26 | /** 27 | * @property \Illuminate\Foundation\Application $container 28 | * 29 | * @method \Guanguans\LaravelExceptionNotify\Channels\Channel driver(?string $driver = null) 30 | * 31 | * @mixin \Guanguans\LaravelExceptionNotify\Channels\Channel 32 | */ 33 | class ExceptionNotifyManager extends Manager implements ChannelContract 34 | { 35 | use AggregationTrait { 36 | AggregationTrait::__call as macroCall; 37 | } 38 | 39 | public function __call(mixed $method, mixed $parameters): mixed 40 | { 41 | return static::hasMacro($method) 42 | ? $this->macroCall($method, $parameters) 43 | : parent::__call($method, $parameters); 44 | } 45 | 46 | public function channel(?string $channel = null): Channel 47 | { 48 | return $this->driver($channel); 49 | } 50 | 51 | public function report(\Throwable $throwable): void 52 | { 53 | rescue(function () use ($throwable): void { 54 | $this->driver()->report($throwable); 55 | }); 56 | } 57 | 58 | public function reportContent(string $content): mixed 59 | { 60 | return rescue(fn (): mixed => $this->driver()->reportContent($content)); 61 | } 62 | 63 | public function getDefaultDriver(): string 64 | { 65 | return config('exception-notify.default'); 66 | } 67 | 68 | /** 69 | * @noinspection MethodShouldBeFinalInspection 70 | * @noinspection MissingParentCallInspection 71 | * @noinspection PhpMissingParentCallCommonInspection 72 | */ 73 | protected function createDriver(mixed $driver): Channel 74 | { 75 | $channelContract = $this->createOriginalDriver($driver); 76 | 77 | return $channelContract instanceof Channel ? $channelContract : new Channel($channelContract); 78 | } 79 | 80 | private function createOriginalDriver(string $driver): ChannelContract 81 | { 82 | if (isset($this->customCreators[$driver])) { 83 | return $this->callCustomCreator($driver); 84 | } 85 | 86 | $configRepository = tap( 87 | new Repository($this->config->get("exception-notify.channels.$driver", [])), 88 | static fn (Repository $configRepository): mixed => $configRepository->set('__channel', $driver) 89 | ); 90 | 91 | $studlyName = str($configRepository->get('driver', $driver))->studly(); 92 | 93 | if (class_exists($class = "\\Guanguans\\LaravelExceptionNotify\\Channels\\{$studlyName}Channel")) { 94 | return new $class($configRepository); 95 | } 96 | 97 | throw new InvalidArgumentException("Driver [$driver] not supported."); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/ExceptionNotifyServiceProvider.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify; 15 | 16 | use Composer\InstalledVersions; 17 | use Guanguans\LaravelExceptionNotify\Commands\TestCommand; 18 | use Guanguans\LaravelExceptionNotify\Facades\ExceptionNotify; 19 | use Illuminate\Contracts\Debug\ExceptionHandler; 20 | use Illuminate\Foundation\Console\AboutCommand; 21 | use Illuminate\Support\Collection; 22 | use Illuminate\Support\ServiceProvider; 23 | use Illuminate\Support\Stringable; 24 | 25 | class ExceptionNotifyServiceProvider extends ServiceProvider 26 | { 27 | public array $singletons = [ 28 | ExceptionNotifyManager::class, 29 | TestCommand::class, 30 | ]; 31 | 32 | /** 33 | * @noinspection PhpMissingParentCallCommonInspection 34 | * 35 | * @throws \Illuminate\Contracts\Container\BindingResolutionException 36 | */ 37 | public function register(): void 38 | { 39 | $this 40 | ->setupConfig() 41 | ->registerAliases() 42 | ->registerReportUsing(); 43 | } 44 | 45 | public function boot(): void 46 | { 47 | $this->registerCommands(); 48 | } 49 | 50 | /** 51 | * @noinspection PhpMissingParentCallCommonInspection 52 | */ 53 | public function provides(): array 54 | { 55 | return [ 56 | $this->toAlias(ExceptionNotifyManager::class), 57 | $this->toAlias(TestCommand::class), 58 | ExceptionNotifyManager::class, 59 | TestCommand::class, 60 | ]; 61 | } 62 | 63 | private function setupConfig(): self 64 | { 65 | /** @noinspection RealpathInStreamContextInspection */ 66 | $source = realpath($raw = __DIR__.'/../config/exception-notify.php') ?: $raw; 67 | 68 | if ($this->app->runningInConsole()) { 69 | $this->publishes([$source => config_path('exception-notify.php')], 'laravel-exception-notify'); 70 | } 71 | 72 | $this->mergeConfigFrom($source, 'exception-notify'); 73 | 74 | return $this; 75 | } 76 | 77 | private function registerAliases(): self 78 | { 79 | foreach ($this->singletons as $singleton) { 80 | $this->alias($singleton); 81 | } 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * @noinspection PhpPossiblePolymorphicInvocationInspection 88 | * 89 | * @throws \Illuminate\Contracts\Container\BindingResolutionException 90 | */ 91 | private function registerReportUsing(): self 92 | { 93 | if ( 94 | config('exception-notify.enabled') 95 | && method_exists($exceptionHandler = $this->app->make(ExceptionHandler::class), 'reportable') 96 | ) { 97 | $exceptionHandler->reportable(static function (\Throwable $throwable): void { 98 | ExceptionNotify::report($throwable); 99 | }); 100 | } 101 | 102 | return $this; 103 | } 104 | 105 | private function registerCommands(): self 106 | { 107 | if ($this->app->runningInConsole()) { 108 | $this->commands(TestCommand::class); 109 | $this->addSectionToAboutCommand(); 110 | } 111 | 112 | return $this; 113 | } 114 | 115 | private function addSectionToAboutCommand(): void 116 | { 117 | AboutCommand::add( 118 | str($package = 'guanguans/laravel-exception-notify')->headline()->toString(), 119 | static fn (): array => collect(['Homepage' => "https://github.com/$package"]) 120 | ->when( 121 | class_exists(InstalledVersions::class), 122 | static fn (Collection $data): Collection => $data->put( 123 | 'Version', 124 | InstalledVersions::getPrettyVersion($package) 125 | ) 126 | ) 127 | ->all() 128 | ); 129 | } 130 | 131 | /** 132 | * @param class-string $class 133 | */ 134 | private function alias(string $class): self 135 | { 136 | $this->app->alias($class, $this->toAlias($class)); 137 | 138 | return $this; 139 | } 140 | 141 | /** 142 | * @param class-string $class 143 | */ 144 | private function toAlias(string $class): string 145 | { 146 | return str($class) 147 | ->replaceFirst(__NAMESPACE__, '') 148 | ->start('\\'.class_basename(ExceptionNotify::class)) 149 | ->replaceFirst('\\', '') 150 | ->explode('\\') 151 | ->map(static fn (string $name): Stringable => str($name)->snake('-')) 152 | ->implode('.'); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Exceptions; 15 | 16 | use Guanguans\LaravelExceptionNotify\Contracts\ThrowableContract; 17 | 18 | class InvalidArgumentException extends \InvalidArgumentException implements ThrowableContract {} 19 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidConfigurationException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Exceptions; 15 | 16 | use Illuminate\Validation\Validator; 17 | 18 | class InvalidConfigurationException extends InvalidArgumentException 19 | { 20 | public static function fromValidator(Validator $validator): self 21 | { 22 | return new self($validator->errors()->first()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Exceptions/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Exceptions; 15 | 16 | use Guanguans\LaravelExceptionNotify\Contracts\ThrowableContract; 17 | 18 | class RuntimeException extends \RuntimeException implements ThrowableContract {} 19 | -------------------------------------------------------------------------------- /src/Facades/ExceptionNotify.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Facades; 15 | 16 | use Guanguans\LaravelExceptionNotify\ExceptionNotifyManager; 17 | use Illuminate\Support\Facades\Facade; 18 | 19 | /** 20 | * @method static \Guanguans\LaravelExceptionNotify\Channels\Channel channel(string|null $channel = null) 21 | * @method static void report(\Throwable $throwable) 22 | * @method static mixed reportContent(string $content) 23 | * @method static string getDefaultDriver() 24 | * @method static mixed driver(string|null $driver = null) 25 | * @method static \Guanguans\LaravelExceptionNotify\ExceptionNotifyManager extend(string $driver, \Closure $callback) 26 | * @method static array getDrivers() 27 | * @method static \Illuminate\Contracts\Container\Container getContainer() 28 | * @method static \Guanguans\LaravelExceptionNotify\ExceptionNotifyManager setContainer(\Illuminate\Contracts\Container\Container $container) 29 | * @method static \Guanguans\LaravelExceptionNotify\ExceptionNotifyManager forgetDrivers() 30 | * @method static \Guanguans\LaravelExceptionNotify\ExceptionNotifyManager|mixed when(\Closure|mixed|null $value = null, callable|null $callback = null, callable|null $default = null) 31 | * @method static \Guanguans\LaravelExceptionNotify\ExceptionNotifyManager|mixed unless(\Closure|mixed|null $value = null, callable|null $callback = null, callable|null $default = null) 32 | * @method static void dd(mixed ...$args) 33 | * @method static \Guanguans\LaravelExceptionNotify\ExceptionNotifyManager dump(mixed ...$args) 34 | * @method static mixed withLocale(string $locale, \Closure $callback) 35 | * @method static void macro(string $name, object|callable $macro) 36 | * @method static void mixin(object $mixin, bool $replace = true) 37 | * @method static bool hasMacro(string $name) 38 | * @method static void flushMacros() 39 | * @method static mixed macroCall(string $method, array $parameters) 40 | * @method static \Guanguans\LaravelExceptionNotify\ExceptionNotifyManager|\Illuminate\Support\HigherOrderTapProxy tap(callable|null $callback = null) 41 | * @method static void reporting(mixed $listener) 42 | * @method static void reported(mixed $listener) 43 | * @method static void skipWhen(\Closure $callback) 44 | * @method static void flush() 45 | * @method static bool shouldReport(\Throwable $throwable) 46 | * 47 | * @see \Guanguans\LaravelExceptionNotify\ExceptionNotifyManager 48 | */ 49 | class ExceptionNotify extends Facade 50 | { 51 | /** 52 | * @noinspection PhpMissingParentCallCommonInspection 53 | */ 54 | protected static function getFacadeAccessor(): string 55 | { 56 | return ExceptionNotifyManager::class; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Jobs/ReportExceptionJob.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Jobs; 15 | 16 | use Guanguans\LaravelExceptionNotify\ExceptionNotifyManager; 17 | use Illuminate\Bus\Queueable; 18 | use Illuminate\Contracts\Queue\ShouldQueue; 19 | use Illuminate\Foundation\Bus\Dispatchable; 20 | use Illuminate\Queue\InteractsWithQueue; 21 | 22 | class ReportExceptionJob implements ShouldQueue 23 | { 24 | use Dispatchable; 25 | use InteractsWithQueue; 26 | use Queueable; 27 | 28 | public function __construct( 29 | private string $channel, 30 | private string $content 31 | ) {} 32 | 33 | public function handle(ExceptionNotifyManager $exceptionNotifyManager): void 34 | { 35 | $exceptionNotifyManager->driver($this->channel)->reportContent($this->content); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Mail/ReportExceptionMail.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Mail; 15 | 16 | use Illuminate\Mail\Mailable; 17 | 18 | class ReportExceptionMail extends Mailable 19 | { 20 | public function __construct( 21 | private string $title, 22 | private string $content 23 | ) {} 24 | 25 | public function build(): self 26 | { 27 | return $this 28 | ->subject($this->title) 29 | ->html($this->content); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Pipes/AddChorePipe.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Pipes; 15 | 16 | use Guanguans\LaravelExceptionNotify\Collectors\ChoreCollector; 17 | use Guanguans\LaravelExceptionNotify\Support\Traits\WithPipeArgs; 18 | use Illuminate\Support\Arr; 19 | use Illuminate\Support\Collection; 20 | use Illuminate\Support\Stringable; 21 | 22 | class AddChorePipe 23 | { 24 | use WithPipeArgs; 25 | 26 | /** 27 | * @param array-key $key 28 | */ 29 | public function handle(Collection $collectors, \Closure $next, mixed $value, mixed $key): Stringable 30 | { 31 | return $next(collect(Arr::add( 32 | $collectors->all(), 33 | str($key)->start(ChoreCollector::fallbackName().'.')->toString(), 34 | $value 35 | ))); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Pipes/AddKeywordChorePipe.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Pipes; 15 | 16 | use Guanguans\LaravelExceptionNotify\Support\Traits\WithPipeArgs; 17 | use Illuminate\Support\Collection; 18 | use Illuminate\Support\Stringable; 19 | 20 | class AddKeywordChorePipe extends AddChorePipe 21 | { 22 | use WithPipeArgs; 23 | 24 | public function handle(Collection $collectors, \Closure $next, mixed $value, mixed $key = 'Keyword'): Stringable 25 | { 26 | return parent::handle($collectors, $next, $value, $key); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Pipes/FixPrettyJsonPipe.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Pipes; 15 | 16 | use Guanguans\LaravelExceptionNotify\Support\JsonFixer; 17 | use Guanguans\LaravelExceptionNotify\Support\Traits\WithPipeArgs; 18 | use Illuminate\Support\Collection; 19 | use Illuminate\Support\Stringable; 20 | use function Guanguans\LaravelExceptionNotify\Support\json_pretty_encode; 21 | 22 | class FixPrettyJsonPipe 23 | { 24 | use WithPipeArgs; 25 | 26 | public function __construct(private JsonFixer $jsonFixer) {} 27 | 28 | public function handle(Collection $collectors, \Closure $next, string $missingValue = '"..."'): Stringable 29 | { 30 | $content = $next($collectors); 31 | 32 | try { 33 | $fixedReport = $this 34 | ->jsonFixer 35 | ->silent(false) 36 | ->missingValue($missingValue) 37 | ->fix((string) $content); 38 | 39 | return str(json_pretty_encode(json_decode($fixedReport, true, 512, \JSON_THROW_ON_ERROR))); 40 | } catch (\Throwable) { 41 | return $content; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Pipes/LimitLengthPipe.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Pipes; 15 | 16 | use Guanguans\LaravelExceptionNotify\Support\Traits\WithPipeArgs; 17 | use Illuminate\Support\Collection; 18 | use Illuminate\Support\Stringable; 19 | 20 | class LimitLengthPipe 21 | { 22 | use WithPipeArgs; 23 | 24 | /** 25 | * @noinspection RedundantDocCommentTagInspection 26 | * 27 | * @param \Closure(\Illuminate\Support\Collection): \Illuminate\Support\Stringable $next 28 | */ 29 | public function handle(Collection $collectors, \Closure $next, int $length, float $percentage = 0.9): Stringable 30 | { 31 | return $next($collectors)->substr(0, (int) ($length * $percentage)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Pipes/SprintfHtmlPipe.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Pipes; 15 | 16 | use Guanguans\LaravelExceptionNotify\Support\Traits\WithPipeArgs; 17 | use Illuminate\Support\Collection; 18 | use Illuminate\Support\Stringable; 19 | 20 | class SprintfHtmlPipe extends SprintfPipe 21 | { 22 | use WithPipeArgs; 23 | 24 | public function handle( 25 | Collection $collectors, 26 | \Closure $next, 27 | string $format = '
%s
' 28 | ): Stringable { 29 | return parent::handle($collectors, $next, $format); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Pipes/SprintfMarkdownPipe.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Pipes; 15 | 16 | use Guanguans\LaravelExceptionNotify\Support\Traits\WithPipeArgs; 17 | use Illuminate\Support\Collection; 18 | use Illuminate\Support\Stringable; 19 | 20 | class SprintfMarkdownPipe extends SprintfPipe 21 | { 22 | use WithPipeArgs; 23 | 24 | public function handle( 25 | Collection $collectors, 26 | \Closure $next, 27 | string $format = <<<'mark' 28 | ``` 29 | %s 30 | ``` 31 | mark 32 | ): Stringable { 33 | return parent::handle($collectors, $next, $format); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Pipes/SprintfPipe.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Pipes; 15 | 16 | use Guanguans\LaravelExceptionNotify\Support\Traits\WithPipeArgs; 17 | use Illuminate\Support\Collection; 18 | use Illuminate\Support\Stringable; 19 | 20 | class SprintfPipe 21 | { 22 | use WithPipeArgs; 23 | 24 | public function handle(Collection $collectors, \Closure $next, string $format): Stringable 25 | { 26 | return str(\sprintf($format, $next($collectors))); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Support/ExceptionContext.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Support; 15 | 16 | use Illuminate\Support\Collection; 17 | 18 | /** 19 | * @see https://github.com/laravel/telescope/blob/4.x/src/ExceptionContext.php 20 | */ 21 | class ExceptionContext 22 | { 23 | public static function getMarked(\Throwable $throwable, string $mark = '➤', int $number = 5): array 24 | { 25 | return collect(self::get($throwable, $number)) 26 | ->tap(static function (Collection $collection) use ( 27 | &$exceptionLine, 28 | $throwable, 29 | &$markedExceptionLine, 30 | $mark, 31 | &$maxLineLen 32 | ): void { 33 | $exceptionLine = $throwable->getLine(); 34 | $markedExceptionLine = "$mark $exceptionLine"; 35 | $maxLineLen = max( 36 | mb_strlen((string) array_key_last($collection->toArray())), 37 | mb_strlen($markedExceptionLine) 38 | ); 39 | }) 40 | ->mapWithKeys(static function (string $code, int $line) use ( 41 | $exceptionLine, 42 | $markedExceptionLine, 43 | $maxLineLen 44 | ): array { 45 | if ($line === $exceptionLine) { 46 | $line = $markedExceptionLine; 47 | } 48 | 49 | return [\sprintf("%{$maxLineLen}s", $line) => $code]; 50 | }) 51 | ->all(); 52 | } 53 | 54 | public static function get(\Throwable $throwable, int $number = 5): array 55 | { 56 | return self::getEval($throwable) ?? self::getFile($throwable, $number); 57 | } 58 | 59 | /** 60 | * @return null|array 61 | */ 62 | private static function getEval(\Throwable $throwable): ?array 63 | { 64 | return str($throwable->getFile())->contains($row = "eval()'d code") ? [$throwable->getLine() => $row] : null; 65 | } 66 | 67 | private static function getFile(\Throwable $throwable, int $number = 5): array 68 | { 69 | return collect(file($throwable->getFile())) 70 | ->slice($throwable->getLine() - $number, 2 * $number - 1) 71 | ->mapWithKeys(static fn (string $code, int $line): array => [$line + 1 => $code]) 72 | ->all(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Support/JsonFixer.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | * 13 | * @see https://github.com/guanguans/laravel-exception-notify 14 | */ 15 | 16 | namespace Guanguans\LaravelExceptionNotify\Support; 17 | 18 | use Guanguans\LaravelExceptionNotify\Exceptions\RuntimeException; 19 | 20 | /** 21 | * This file is modified from https://github.com/adhocore/php-json-fixer. 22 | */ 23 | final class JsonFixer 24 | { 25 | private array $stack = []; 26 | 27 | /** @var bool If current char is within a string */ 28 | private bool $inStr = false; 29 | 30 | /** @var bool Whether to throw Exception on failure */ 31 | private bool $silent = false; 32 | 33 | /** @var array The complementary pairs */ 34 | private array $pairs = [ 35 | '{' => '}', 36 | '[' => ']', 37 | '"' => '"', 38 | ]; 39 | 40 | /** @var int The last seen object `left brace` type position */ 41 | private int $objectPos = -1; 42 | 43 | /** @var int The last seen array `[` type position */ 44 | private int $arrayPos = -1; 45 | 46 | /** @var string Missing value. (Options: true, false, null) */ 47 | private string $missingValue = 'null'; 48 | 49 | /** 50 | * Set/unset silent mode. 51 | */ 52 | public function silent(bool $silent = true): self 53 | { 54 | $this->silent = $silent; 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * Set missing value. 61 | */ 62 | public function missingValue(string $value): self 63 | { 64 | $this->missingValue = $value; 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * Fix the truncated JSON. 71 | * 72 | * @throws \RuntimeException 73 | */ 74 | public function fix(string $json): string 75 | { 76 | [$head, $json, $tail] = $this->trim($json); 77 | 78 | if (empty($json) || $this->isValid($json)) { 79 | return $json; 80 | } 81 | 82 | if (null !== ($tmpJson = $this->quickFix($json))) { 83 | return $tmpJson; 84 | } 85 | 86 | $this->reset(); 87 | 88 | return $head.$this->doFix($json).$tail; 89 | } 90 | 91 | /* ------------------------- trait PadsJson ------------------------- */ 92 | public function pad(string $tmpJson): string 93 | { 94 | if (!$this->inStr) { 95 | $tmpJson = rtrim($tmpJson, ','); 96 | 97 | while (',' === $this->lastToken()) { 98 | $this->popToken(); 99 | } 100 | } 101 | 102 | $tmpJson = $this->padLiteral($tmpJson); 103 | $tmpJson = $this->padObject($tmpJson); 104 | 105 | return $this->padStack($tmpJson); 106 | } 107 | 108 | private function trim(string $json): array 109 | { 110 | preg_match('/^(\s*)(\S+)(\s*)$/', $json, $match); 111 | 112 | $match += ['', '', '', '']; 113 | $match[2] = trim($json); 114 | 115 | array_shift($match); 116 | 117 | return $match; 118 | } 119 | 120 | /** 121 | * @noinspection JsonEncodingApiUsageInspection 122 | */ 123 | private function isValid(string $json): bool 124 | { 125 | json_decode($json); 126 | 127 | return \JSON_ERROR_NONE === json_last_error(); 128 | } 129 | 130 | private function quickFix(string $json): ?string 131 | { 132 | if (isset($this->pairs[$json]) && 1 === \strlen($json)) { 133 | return $json.$this->pairs[$json]; 134 | } 135 | 136 | if ('"' !== $json[0]) { 137 | return $this->maybeLiteral($json); 138 | } 139 | 140 | return $this->padString($json); 141 | } 142 | 143 | private function reset(): void 144 | { 145 | $this->stack = []; 146 | $this->inStr = false; 147 | $this->objectPos = -1; 148 | $this->arrayPos = -1; 149 | } 150 | 151 | private function maybeLiteral(string $json): ?string 152 | { 153 | if (!\in_array($json[0], ['t', 'f', 'n'], true)) { 154 | return null; 155 | } 156 | 157 | foreach (['true', 'false', 'null'] as $literal) { 158 | if (str_starts_with($literal, $json)) { 159 | return $literal; 160 | } 161 | } 162 | 163 | // @codeCoverageIgnoreStart 164 | return null; 165 | // @codeCoverageIgnoreEnd 166 | } 167 | 168 | private function doFix(string $json): string 169 | { 170 | [$index, $char] = [-1, '']; 171 | 172 | while (isset($json[++$index])) { 173 | [$prev, $char] = [$char, $json[$index]]; 174 | 175 | if (!\in_array($char, [' ', "\n", "\r"], true)) { 176 | $this->stack($prev, $char, $index); 177 | } 178 | } 179 | 180 | return $this->fixOrFail($json); 181 | } 182 | 183 | private function stack(string $prev, string $char, int $index): void 184 | { 185 | if ($this->maybeStr($prev, $char, $index)) { 186 | return; 187 | } 188 | 189 | $last = $this->lastToken(); 190 | 191 | if (\in_array($last, [',', ':', '"'], true) && preg_match('/\"|\d|\{|\[|t|f|n/', $char)) { 192 | $this->popToken(); 193 | } 194 | 195 | if (\in_array($char, [',', ':', '[', '{'], true)) { 196 | $this->stack[$index] = $char; 197 | } 198 | 199 | $this->updatePos($char, $index); 200 | } 201 | 202 | private function lastToken(): mixed 203 | { 204 | return end($this->stack); 205 | } 206 | 207 | private function popToken(?string $token = null): mixed 208 | { 209 | // Last one 210 | if (null === $token) { 211 | return array_pop($this->stack); 212 | } 213 | 214 | $keys = array_reverse(array_keys($this->stack)); 215 | 216 | foreach ($keys as $key) { 217 | if ($this->stack[$key] === $token) { 218 | unset($this->stack[$key]); 219 | 220 | break; 221 | } 222 | } 223 | 224 | return null; 225 | } 226 | 227 | private function maybeStr(string $prev, string $char, int $index): bool 228 | { 229 | if ('\\' !== $prev && '"' === $char) { 230 | $this->inStr = !$this->inStr; 231 | } 232 | 233 | if ($this->inStr && '"' !== $this->lastToken()) { 234 | $this->stack[$index] = '"'; 235 | } 236 | 237 | return $this->inStr; 238 | } 239 | 240 | private function updatePos(string $char, int $index): void 241 | { 242 | if ('{' === $char) { 243 | $this->objectPos = $index; 244 | } elseif ('}' === $char) { 245 | $this->popToken('{'); 246 | $this->objectPos = -1; 247 | } elseif ('[' === $char) { 248 | $this->arrayPos = $index; 249 | } elseif (']' === $char) { 250 | $this->popToken('['); 251 | $this->arrayPos = -1; 252 | } 253 | } 254 | 255 | private function fixOrFail(string $json): string 256 | { 257 | $length = \strlen($json); 258 | $tmpJson = $this->pad($json); 259 | 260 | if ($this->isValid($tmpJson)) { 261 | return $tmpJson; 262 | } 263 | 264 | if ($this->silent) { 265 | return $json; 266 | } 267 | 268 | throw new RuntimeException(\sprintf('Could not fix JSON (tried padding `%s`)', substr($tmpJson, $length))); 269 | } 270 | 271 | private function padLiteral(string $tmpJson): string 272 | { 273 | if ($this->inStr) { 274 | return $tmpJson; 275 | } 276 | 277 | $match = preg_match('/(tr?u?e?|fa?l?s?e?|nu?l?l?)$/', $tmpJson, $matches); 278 | 279 | if (!$match || null === ($literal = $this->maybeLiteral($matches[1]))) { 280 | return $tmpJson; 281 | } 282 | 283 | return substr($tmpJson, 0, -\strlen($matches[1])).$literal; 284 | } 285 | 286 | private function padStack(string $tmpJson): string 287 | { 288 | foreach (array_reverse($this->stack, true) as $token) { 289 | if (isset($this->pairs[$token])) { 290 | $tmpJson .= $this->pairs[$token]; 291 | } 292 | } 293 | 294 | return $tmpJson; 295 | } 296 | 297 | private function padObject(string $tmpJson): string 298 | { 299 | if (!$this->objectNeedsPadding($tmpJson)) { 300 | return $tmpJson; 301 | } 302 | 303 | $part = substr($tmpJson, $this->objectPos + 1); 304 | 305 | if (preg_match('/(\s*\"[^"]+\"\s*:\s*[^,]+,?)+$/', $part)) { 306 | return $tmpJson; 307 | } 308 | 309 | if ($this->inStr) { 310 | $tmpJson .= '"'; 311 | } 312 | 313 | $tmpJson = $this->padIf($tmpJson); 314 | $tmpJson .= $this->missingValue; 315 | 316 | if ('"' === $this->lastToken()) { 317 | $this->popToken(); 318 | } 319 | 320 | return $tmpJson; 321 | } 322 | 323 | private function objectNeedsPadding(string $tmpJson): bool 324 | { 325 | $last = substr($tmpJson, -1); 326 | $empty = '{' === $last && !$this->inStr; 327 | 328 | return !$empty && $this->arrayPos < $this->objectPos; 329 | } 330 | 331 | private function padString(string $string): ?string 332 | { 333 | $last = substr($string, -1); 334 | $last2 = substr($string, -2); 335 | 336 | if ('\"' === $last2 || '"' !== $last) { 337 | return $string.'"'; 338 | } 339 | 340 | // @codeCoverageIgnoreStart 341 | return null; 342 | // @codeCoverageIgnoreEnd 343 | } 344 | 345 | private function padIf(string $string): string 346 | { 347 | if (!str_ends_with($string, ':')) { 348 | return $string.':'; 349 | } 350 | 351 | return $string; 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /src/Support/Rectors/HydratePipeFuncCallToStaticCallRector.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | * 13 | * @see https://github.com/guanguans/laravel-exception-notify 14 | */ 15 | 16 | namespace Guanguans\LaravelExceptionNotify\Support\Rectors; 17 | 18 | use PhpParser\Node; 19 | use PhpParser\Node\Expr\FuncCall; 20 | use Rector\PhpParser\Node\Value\ValueResolver; 21 | use Rector\Rector\AbstractRector; 22 | use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; 23 | use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; 24 | 25 | /** 26 | * @internal 27 | */ 28 | final class HydratePipeFuncCallToStaticCallRector extends AbstractRector 29 | { 30 | public function __construct(private ValueResolver $valueResolver) {} 31 | 32 | /** 33 | * @throws \Symplify\RuleDocGenerator\Exception\PoorDocumentationException 34 | */ 35 | public function getRuleDefinition(): RuleDefinition 36 | { 37 | return new RuleDefinition( 38 | 'Hydrate pipe func call to static call', 39 | [ 40 | new CodeSample( 41 | <<<'CODE_SAMPLE' 42 | hydrate_pipe(LimitLengthPipe::class, 4096); 43 | CODE_SAMPLE, 44 | <<<'CODE_SAMPLE' 45 | LimitLengthPipe::class::with(4096); 46 | CODE_SAMPLE, 47 | ), 48 | ], 49 | ); 50 | } 51 | 52 | public function getNodeTypes(): array 53 | { 54 | return [ 55 | FuncCall::class, 56 | ]; 57 | } 58 | 59 | /** 60 | * @param \PhpParser\Node\Expr\FuncCall $node 61 | */ 62 | public function refactor(Node $node): Node 63 | { 64 | if ($this->isName($node, 'Guanguans\LaravelExceptionNotify\Support\hydrate_pipe')) { 65 | $args = $node->getArgs(); 66 | 67 | return $this->nodeFactory->createStaticCall( 68 | $this->valueResolver->getValue($args[0]), 69 | 'with', 70 | \array_slice($args, 1) 71 | ); 72 | } 73 | 74 | return $node; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Support/Rectors/ToInternalExceptionRector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Support\Rectors; 15 | 16 | use PhpParser\Node; 17 | use PhpParser\Node\Expr\New_; 18 | use PhpParser\Node\Name; 19 | use Rector\Contract\Rector\ConfigurableRectorInterface; 20 | use Rector\Rector\AbstractRector; 21 | use Symplify\RuleDocGenerator\Exception\PoorDocumentationException; 22 | use Symplify\RuleDocGenerator\Exception\ShouldNotHappenException; 23 | use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample; 24 | use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; 25 | use Webmozart\Assert\Assert; 26 | 27 | /** 28 | * @internal 29 | */ 30 | final class ToInternalExceptionRector extends AbstractRector implements ConfigurableRectorInterface 31 | { 32 | private array $except = []; 33 | 34 | /** 35 | * @throws PoorDocumentationException 36 | * @throws ShouldNotHappenException 37 | */ 38 | public function getRuleDefinition(): RuleDefinition 39 | { 40 | return new RuleDefinition( 41 | 'To internal exception', 42 | [ 43 | new ConfiguredCodeSample( 44 | <<<'CODE_SAMPLE' 45 | throw new \InvalidArgumentException('on_headers must be callable'); 46 | CODE_SAMPLE, 47 | <<<'CODE_SAMPLE' 48 | throw new \Guanguans\LaravelExceptionNotify\Exceptions\InvalidArgumentException('on_headers must be callable'); 49 | CODE_SAMPLE, 50 | ['exceptionClassPattern' => 'exceptionClassPattern'], 51 | ), 52 | ], 53 | ); 54 | } 55 | 56 | public function configure(array $configuration): void 57 | { 58 | Assert::allStringNotEmpty($configuration); 59 | $this->except = array_merge($this->except, $configuration); 60 | } 61 | 62 | public function getNodeTypes(): array 63 | { 64 | return [ 65 | New_::class, 66 | ]; 67 | } 68 | 69 | /** 70 | * @param Node\Expr\New_ $node 71 | * 72 | * @throws \ReflectionException 73 | */ 74 | public function refactor(Node $node): ?Node 75 | { 76 | $class = $node->class; 77 | 78 | if ( 79 | !$class instanceof Name 80 | || str_starts_with($class->toString(), 'Guanguans\\LaravelExceptionNotify\\Exceptions\\') 81 | || !str_ends_with($class->toString(), 'Exception') 82 | || str($class->toString())->is($this->except) 83 | ) { 84 | return null; 85 | } 86 | 87 | $internalExceptionClass = "\\Guanguans\\LaravelExceptionNotify\\Exceptions\\{$class->getLast()}"; 88 | 89 | if (!class_exists($internalExceptionClass)) { 90 | $this->createInternalException($class); 91 | } 92 | 93 | $node->class = new Name($internalExceptionClass, $class->getAttributes()); 94 | 95 | return $node; 96 | } 97 | 98 | /** 99 | * @throws \ReflectionException 100 | */ 101 | private function createInternalException(Name $name): void 102 | { 103 | $externalExceptionClass = $name->toString(); 104 | $reflectionClass = new \ReflectionClass($externalExceptionClass); 105 | 106 | if ($reflectionClass->isFinal()) { 107 | return; 108 | } 109 | 110 | $file = __DIR__."/../../Exceptions/{$name->getLast()}.php"; 111 | 112 | /** @noinspection MkdirRaceConditionInspection */ 113 | is_dir($dir = \dirname($file)) or mkdir($dir, 0755, true); 114 | 115 | file_put_contents( 116 | $file, 117 | <<getLast()} extends \\$externalExceptionClass implements ThrowableContract {} 127 | 128 | PHP 129 | ); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Support/Traits/AggregationTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Support\Traits; 15 | 16 | use Guanguans\Notify\Foundation\Concerns\Dumpable; 17 | use Illuminate\Support\Traits\Conditionable; 18 | use Illuminate\Support\Traits\ForwardsCalls; 19 | use Illuminate\Support\Traits\Localizable; 20 | use Illuminate\Support\Traits\Macroable; 21 | use Illuminate\Support\Traits\Tappable; 22 | 23 | trait AggregationTrait 24 | { 25 | use Conditionable; 26 | use Dumpable; 27 | use ForwardsCalls; 28 | use Localizable; 29 | use Macroable; 30 | use Tappable; 31 | } 32 | -------------------------------------------------------------------------------- /src/Support/Traits/WithPipeArgs.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Support\Traits; 15 | 16 | trait WithPipeArgs 17 | { 18 | public static function with(mixed ...$args): string 19 | { 20 | return [] === $args ? static::class : static::class.':'.implode(',', $args); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Support/Utils.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Support; 15 | 16 | use Carbon\CarbonInterval; 17 | use Guanguans\LaravelExceptionNotify\Template; 18 | use Illuminate\Support\Collection; 19 | use Illuminate\Support\Str; 20 | 21 | class Utils 22 | { 23 | /** 24 | * @throws \ReflectionException 25 | */ 26 | public static function applyConfigurationToObject(object $object, array $configuration, ?array $except = null): object 27 | { 28 | return collect($configuration) 29 | ->except( 30 | $except ?? collect((new \ReflectionObject($object))->getConstructor()?->getParameters()) 31 | ->map(static fn (\ReflectionParameter $reflectionParameter): string => $reflectionParameter->getName()) 32 | ->push( 33 | '__channel', 34 | '__abstract', 35 | '__class', 36 | '__name', 37 | '_abstract', 38 | '_class', 39 | '_name', 40 | ) 41 | ->all() 42 | ) 43 | ->each(static function (mixed $value, string $key) use ($object): void { 44 | $hasApplied = false; 45 | 46 | // Apply configuration to object by method 47 | foreach ( 48 | [ 49 | static fn (string $key): string => $key, 50 | static fn (string $key): string => Str::camel($key), 51 | static fn (string $key): string => 'set'.Str::studly($key), 52 | static fn (string $key): string => 'on'.Str::studly($key), 53 | ] as $caster 54 | ) { 55 | if (method_exists($object, $method = $caster($key)) && \is_callable([$object, $method])) { 56 | $numberOfParameters = (new \ReflectionMethod($object, $method))->getNumberOfParameters(); 57 | 58 | if (0 === $numberOfParameters) { 59 | continue; 60 | } 61 | 62 | 1 === $numberOfParameters ? $object->{$method}($value) : app()->call([$object, $method], $value); 63 | $hasApplied = true; 64 | 65 | break; 66 | } 67 | } 68 | 69 | if ($hasApplied) { 70 | return; 71 | } 72 | 73 | // Apply configuration to object by property 74 | foreach ( 75 | [ 76 | static fn (string $key): string => $key, 77 | static fn (string $key): string => Str::camel($key), 78 | ] as $caster 79 | ) { 80 | if ( 81 | property_exists($object, $property = $caster($key)) 82 | && with(new \ReflectionProperty($object, $property))->isPublic() 83 | ) { 84 | $object->{$key} = $value; 85 | 86 | return; 87 | } 88 | } 89 | }) 90 | ->pipe(static function (Collection $configuration) use ($object): object { 91 | $extender = $configuration->get('extender'); 92 | 93 | if (!$extender) { 94 | return $object; 95 | } 96 | 97 | if (!\is_callable($extender)) { 98 | /** @var callable $extender */ 99 | $extender = make($extender); 100 | } 101 | 102 | return $extender($object); 103 | }); 104 | } 105 | 106 | public static function applyContentToConfiguration(array $configuration, string $content): array 107 | { 108 | array_walk_recursive($configuration, static function (mixed &$value) use ($content): void { 109 | \is_string($value) and $value = str_replace( 110 | [Template::TITLE, Template::CONTENT], 111 | [config('exception-notify.title'), $content], 112 | $value 113 | ); 114 | }); 115 | 116 | return $configuration; 117 | } 118 | 119 | /** 120 | * @see https://stackoverflow.com/a/23888858/1580028 121 | */ 122 | public static function humanBytes(int $bytes, int $decimals = 2): string 123 | { 124 | $size = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 125 | $factor = (int) floor((\strlen((string) $bytes) - 1) / 3); 126 | 127 | if (0 === $factor) { 128 | $decimals = 0; 129 | } 130 | 131 | return \sprintf("%.{$decimals}f %s", $bytes / (1024 ** $factor), $size[$factor]); 132 | } 133 | 134 | /** 135 | * @noinspection PhpUnhandledExceptionInspection 136 | */ 137 | public static function humanMilliseconds(float|int $milliseconds, array $syntax = []): string 138 | { 139 | \assert(0 < $milliseconds, 'The milliseconds must be greater than 0.'); 140 | 141 | return CarbonInterval::microseconds($milliseconds * 1000) 142 | ->cascade() 143 | ->forHumans($syntax + [ 144 | 'join' => ', ', 145 | 'locale' => 'en', 146 | // 'locale' => 'zh_CN', 147 | 'minimumUnit' => 'us', 148 | 'short' => true, 149 | ]); 150 | } 151 | 152 | public static function isSyncJobConnection(): bool 153 | { 154 | return 'sync' === self::jobConnection(); 155 | } 156 | 157 | public static function jobConnection(): string 158 | { 159 | return config('exception-notify.job.connection') ?? config('queue.default'); 160 | } 161 | 162 | public static function jobQueue(): ?string 163 | { 164 | return config('exception-notify.job.queue') ?? config(\sprintf('queue.connections.%s.queue', self::jobConnection())); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/Support/helpers.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify\Support; 15 | 16 | use Guanguans\LaravelExceptionNotify\Exceptions\InvalidArgumentException; 17 | use Illuminate\Support\Arr; 18 | use Illuminate\Support\Facades\Log; 19 | 20 | if (!\function_exists('Guanguans\LaravelExceptionNotify\Support\env_explode')) { 21 | /** 22 | * @noinspection LaravelFunctionsInspection 23 | */ 24 | function env_explode(string $key, mixed $default = null, string $delimiter = ',', int $limit = \PHP_INT_MAX): mixed 25 | { 26 | $env = env($key, $default); 27 | 28 | if (\is_string($env)) { 29 | return $env ? explode($delimiter, $env, $limit) : []; 30 | } 31 | 32 | return $env; 33 | } 34 | } 35 | 36 | if (!\function_exists('Guanguans\LaravelExceptionNotify\Support\json_pretty_encode')) { 37 | /** 38 | * @param int<1, 4194304> $depth 39 | * 40 | * @throws \JsonException 41 | */ 42 | function json_pretty_encode(mixed $value, int $options = 0, int $depth = 512): string 43 | { 44 | return json_encode( 45 | $value, 46 | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES | \JSON_THROW_ON_ERROR | \JSON_FORCE_OBJECT | $options, 47 | $depth 48 | ); 49 | } 50 | } 51 | 52 | if (!\function_exists('Guanguans\LaravelExceptionNotify\Support\make')) { 53 | /** 54 | * @see https://github.com/laravel/framework/blob/12.x/src/Illuminate/Foundation/helpers.php 55 | * @see https://github.com/yiisoft/yii2/blob/master/framework/BaseYii.php 56 | * 57 | * @template TClass of object 58 | * 59 | * @param array|class-string|string $name 60 | * @param array $parameters 61 | * 62 | * @return ($name is class-string ? TClass : mixed) 63 | */ 64 | function make(array|string $name, array $parameters = []): mixed 65 | { 66 | if (\is_string($name)) { 67 | return resolve($name, $parameters); 68 | } 69 | 70 | foreach ( 71 | $keys = [ 72 | '__abstract', 73 | '__class', 74 | '__name', 75 | '_abstract', 76 | '_class', 77 | '_name', 78 | 'abstract', 79 | 'class', 80 | 'name', 81 | ] as $key 82 | ) { 83 | if (isset($name[$key])) { 84 | return make($name[$key], $parameters + Arr::except($name, $key)); 85 | } 86 | } 87 | 88 | throw new InvalidArgumentException(\sprintf( 89 | 'The argument of abstract must be an array containing a `%s` element.', 90 | implode('` or `', $keys) 91 | )); 92 | } 93 | } 94 | 95 | if (!\function_exists('Guanguans\LaravelExceptionNotify\Support\rescue')) { 96 | /** 97 | * @param null|\Closure $rescue 98 | * @param bool|\Closure $log 99 | * 100 | * @see rescue() 101 | */ 102 | function rescue(callable $callback, mixed $rescue = null, mixed $log = true): mixed 103 | { 104 | try { 105 | return $callback(); 106 | } catch (\Throwable $throwable) { 107 | if (value($log, $throwable)) { 108 | Log::error($throwable->getMessage(), ['exception' => $throwable]); 109 | } 110 | 111 | return value($rescue, $throwable); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Template.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-exception-notify 12 | */ 13 | 14 | namespace Guanguans\LaravelExceptionNotify; 15 | 16 | interface Template 17 | { 18 | public const TITLE = '{title}'; 19 | public const CONTENT = '{content}'; 20 | } 21 | --------------------------------------------------------------------------------