🆙 Add cms i using 🆙

This commit is contained in:
Remco
2025-11-25 22:42:56 +01:00
parent 94704e0925
commit d44196149e
35591 changed files with 3601123 additions and 0 deletions
@@ -0,0 +1,718 @@
# Changelog
All notable changes to `laravel-ray` will be documented in this file
## 1.43.0 - 2025-11-24
### What's Changed
* Add more context to exceptions by @freekmurze in https://github.com/spatie/laravel-ray/pull/396
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.42.0...1.43.0
## 1.42.0 - 2025-11-20
### What's Changed
* Bump stefanzweifel/git-auto-commit-action from 6 to 7 by @dependabot[bot] in https://github.com/spatie/laravel-ray/pull/392
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.41.0...1.42.0
## 1.41.0 - 2025-10-16
### What's Changed
* Fix PHP 8.5 deprecations by @IonBazan in https://github.com/spatie/laravel-ray/pull/393
### New Contributors
* @IonBazan made their first contribution in https://github.com/spatie/laravel-ray/pull/393
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.40.3...1.41.0
## 1.40.3 - 2025-10-13
### What's Changed
* Bump shivammathur/setup-php from 2.32.0 to 2.33.0 by @dependabot[bot] in https://github.com/spatie/laravel-ray/pull/378
* Bump dependabot/fetch-metadata from 2.3.0 to 2.4.0 by @dependabot[bot] in https://github.com/spatie/laravel-ray/pull/379
* Bump shivammathur/setup-php from 2.33.0 to 2.34.1 by @dependabot[bot] in https://github.com/spatie/laravel-ray/pull/381
* Bump shivammathur/setup-php from 2.34.1 to 2.35.1 by @dependabot[bot] in https://github.com/spatie/laravel-ray/pull/383
* Bump shivammathur/setup-php from 2.35.1 to 2.35.2 by @dependabot[bot] in https://github.com/spatie/laravel-ray/pull/384
* Bump shivammathur/setup-php from 2.35.2 to 2.35.3 by @dependabot[bot] in https://github.com/spatie/laravel-ray/pull/386
* Bump shivammathur/setup-php from 2.35.3 to 2.35.4 by @dependabot[bot] in https://github.com/spatie/laravel-ray/pull/387
* Fix unnecessary consecutive spaces in comment by @msng in https://github.com/spatie/laravel-ray/pull/388
* Update issue template by @AlexVanderbist in https://github.com/spatie/laravel-ray/pull/389
* Bump shivammathur/setup-php from 2.35.4 to 2.35.5 by @dependabot[bot] in https://github.com/spatie/laravel-ray/pull/390
* Fix no-op Invador frame check in OriginFactory::getFrame() by @webpresencekyle in https://github.com/spatie/laravel-ray/pull/391
* Bump actions/checkout from 4 to 5 by @dependabot[bot] in https://github.com/spatie/laravel-ray/pull/385
* Bump stefanzweifel/git-auto-commit-action from 5 to 6 by @dependabot[bot] in https://github.com/spatie/laravel-ray/pull/382
### New Contributors
* @msng made their first contribution in https://github.com/spatie/laravel-ray/pull/388
* @webpresencekyle made their first contribution in https://github.com/spatie/laravel-ray/pull/391
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.40.2...1.40.3
## 1.40.2 - 2025-03-27
### What's Changed
* Respect custom path for cached views by @alies-dev in https://github.com/spatie/laravel-ray/pull/376
### New Contributors
* @alies-dev made their first contribution in https://github.com/spatie/laravel-ray/pull/376
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.40.1...1.40.2
## 1.40.1 - 2025-03-14
### What's Changed
* Fix issue #374 recent changes to mailwatcher has broken mailraw by @timvandijck in https://github.com/spatie/laravel-ray/pull/375
### New Contributors
* @timvandijck made their first contribution in https://github.com/spatie/laravel-ray/pull/375
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.40.0...1.40.1
## 1.40.0 - 2025-03-08
### What's Changed
* Allow to show emails (not just when log driver) by @glorand in https://github.com/spatie/laravel-ray/pull/373
### New Contributors
* @glorand made their first contribution in https://github.com/spatie/laravel-ray/pull/373
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.39.1...1.40.0
## 1.39.1 - 2025-02-05
### What's Changed
* Update Dependencies by @sweptsquash in https://github.com/spatie/laravel-ray/pull/369
* Bump shivammathur/setup-php from 2.31.1 to 2.32.0 by @dependabot in https://github.com/spatie/laravel-ray/pull/370
* Bump dependabot/fetch-metadata from 2.2.0 to 2.3.0 by @dependabot in https://github.com/spatie/laravel-ray/pull/371
* Supports Laravel 12 by @crynobone in https://github.com/spatie/laravel-ray/pull/372
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.39.0...1.39.1
## 1.39.0 - 2024-12-11
### What's Changed
* Update the Rector dependency to version ^2.0.0-rc2 by @martio in https://github.com/spatie/laravel-ray/pull/368
* Install `rector/rector` during first run of `php artisan ray:clean` instead of requiring `rector/rector` by @crynobone in https://github.com/spatie/laravel-ray/pull/363
* Add conditional query watcher and convenience methods for update, delete, insert and select queries by @patrickomeara in https://github.com/spatie/laravel-ray/pull/359
### New Contributors
* @martio made their first contribution in https://github.com/spatie/laravel-ray/pull/368
* @patrickomeara made their first contribution in https://github.com/spatie/laravel-ray/pull/359
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.38.0...1.39.0
## 1.37.1 - 2024-07-15
### What's Changed
* Add the xray directive to show all available variables in a Blade view.
* Bump dependabot/fetch-metadata from 2.1.0 to 2.2.0 by @dependabot in https://github.com/spatie/laravel-ray/pull/356
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.37.0...1.37.1
## 1.37.0 - 2024-07-03
### What's Changed
* Add 'measure' directive to RayServiceProvider by @iurigustavo in https://github.com/spatie/laravel-ray/pull/355
### New Contributors
* @iurigustavo made their first contribution in https://github.com/spatie/laravel-ray/pull/355
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.36.3...1.37.0
## 1.36.3 - 2024-07-02
### What's Changed
* Bump shivammathur/setup-php from 2.30.4 to 2.30.5 by @dependabot in https://github.com/spatie/laravel-ray/pull/350
* Bump shivammathur/setup-php from 2.30.5 to 2.31.0 by @dependabot in https://github.com/spatie/laravel-ray/pull/353
* Allow zbateson/mail-mime-parser ^3.0 by @JamesFreeman in https://github.com/spatie/laravel-ray/pull/354
### New Contributors
* @JamesFreeman made their first contribution in https://github.com/spatie/laravel-ray/pull/354
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.36.2...1.36.3
## 1.36.2 - 2024-05-02
### What's Changed
- fix signed URLs in logged mailables
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.36.1...1.36.2
## 1.36.1 - 2024-04-12
### What's Changed
* Bump shivammathur/setup-php from 2.30.1 to 2.30.2 by @dependabot in https://github.com/spatie/laravel-ray/pull/338
* Make implicit nullable param to explicit (PHP 8.4 compatibility) by @GromNaN in https://github.com/spatie/laravel-ray/pull/340
* Update branch alias for version 1.x by @GromNaN in https://github.com/spatie/laravel-ray/pull/341
### New Contributors
* @GromNaN made their first contribution in https://github.com/spatie/laravel-ray/pull/340
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.36.0...1.36.1
## 1.36.0 - 2024-03-29
### What's Changed
* Bump shivammathur/setup-php from 2.29.0 to 2.30.0 by @dependabot in https://github.com/spatie/laravel-ray/pull/334
* Bump shivammathur/setup-php from 2.30.0 to 2.30.1 by @dependabot in https://github.com/spatie/laravel-ray/pull/335
* Bump dependabot/fetch-metadata from 1.6.0 to 2.0.0 by @dependabot in https://github.com/spatie/laravel-ray/pull/336
* Bump ramsey/composer-install from 2 to 3 by @dependabot in https://github.com/spatie/laravel-ray/pull/333
* Support context by @freekmurze in https://github.com/spatie/laravel-ray/pull/337
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.35.1...1.36.0
## 1.35.1 - 2024-02-13
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.35.0...1.35.1
## 1.35.0 - 2024-02-03
### What's Changed
* Supports Laravel 11 by @crynobone in https://github.com/spatie/laravel-ray/pull/329
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.34.0...1.35.0
## 1.34.0 - 2024-01-25
### What's Changed
* Adds an artisan command to remove ray calls from your codebase.
* Bump stefanzweifel/git-auto-commit-action from 4 to 5 by @dependabot in https://github.com/spatie/laravel-ray/pull/321
* Bump shivammathur/setup-php from 2.28.0 to 2.29.0 by @dependabot in https://github.com/spatie/laravel-ray/pull/330
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.33.1...1.34.0
## 1.33.1 - 2024-01-04
- Allow symphony stopwatch 7
## 1.33.0 - 2023-09-04
### What's Changed
- Bump shivammathur/setup-php from 2.25.4 to 2.25.5 by @dependabot in https://github.com/spatie/laravel-ray/pull/309
- Add ability to return the results of callable by @grantholle in https://github.com/spatie/laravel-ray/pull/314
### New Contributors
- @grantholle made their first contribution in https://github.com/spatie/laravel-ray/pull/314
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.32.6...1.33.0
## 1.32.6 - 2023-07-19
### What's Changed
- Bump dependabot/fetch-metadata from 1.5.1 to 1.6.0 by @dependabot in https://github.com/spatie/laravel-ray/pull/305
- feat: support raw sql by @innocenzi in https://github.com/spatie/laravel-ray/pull/306
### New Contributors
- @innocenzi made their first contribution in https://github.com/spatie/laravel-ray/pull/306
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.32.5...1.32.6
## 1.32.5 - 2023-06-23
- fix displaying Mailables
## 1.32.4 - 2023-03-23
### What's Changed
- Avoid making DB connection unless necessary by @crynobone in https://github.com/spatie/laravel-ray/pull/295
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.32.3...1.32.4
## 1.32.3 - 2023-03-03
- display correct origin when using `invade`
## 1.32.2 - 2023-02-06
### What's Changed
- Bump shivammathur/setup-php from 2.23.0 to 2.24.0 by @dependabot in https://github.com/spatie/laravel-ray/pull/291
- Bump dependabot/fetch-metadata from 1.3.5 to 1.3.6 by @dependabot in https://github.com/spatie/laravel-ray/pull/292
- Add context to ApplicationLogPayload by @bilfeldt in https://github.com/spatie/laravel-ray/pull/293
### New Contributors
- @bilfeldt made their first contribution in https://github.com/spatie/laravel-ray/pull/293
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.32.1...1.32.2
## 1.32.1 - 2023-01-26
### What's Changed
- Make DB connection optional by @lentex in https://github.com/spatie/laravel-ray/pull/290
### New Contributors
- @lentex made their first contribution in https://github.com/spatie/laravel-ray/pull/290
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.32.0...1.32.1
## 1.32.0 - 2023-01-11
- add support for Laravel 10
## 1.31.0 - 2022-09-20
### What's Changed
- Added in comment to docblock for linux docker users by @jaetoole in https://github.com/spatie/laravel-ray/pull/271
- @ray blade directive completion for Laravel Idea(PhpStorm) by @adelf in https://github.com/spatie/laravel-ray/pull/273
### New Contributors
- @jaetoole made their first contribution in https://github.com/spatie/laravel-ray/pull/271
- @adelf made their first contribution in https://github.com/spatie/laravel-ray/pull/273
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.30.0...1.31.0
## 1.30.0 - 2022-07-29
### What's Changed
- Add `send_deprecated_notices_to_ray` to config stub by @squatto in https://github.com/spatie/laravel-ray/pull/267
- Feat: Slow query configuration by @fullstackfool in https://github.com/spatie/laravel-ray/pull/269
### New Contributors
- @fullstackfool made their first contribution in https://github.com/spatie/laravel-ray/pull/269
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.29.7...1.30.0
## 1.29.7 - 2022-05-27
## What's Changed
- Fixes https://github.com/spatie/laravel-ray/issues/250 by @dfox288 in https://github.com/spatie/laravel-ray/pull/251
## New Contributors
- @dfox288 made their first contribution in https://github.com/spatie/laravel-ray/pull/251
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.29.6...1.29.7
## 1.29.6 - 2022-04-15
## What's Changed
- ignore php 8.1 deprecation notices by @Nielsvanpach in https://github.com/spatie/laravel-ray/pull/247
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.29.5...1.29.6
## 1.29.5 - 2022-04-05
## What's Changed
- Fix undefined payload when using queue driver other than sync by @stein-j in https://github.com/spatie/laravel-ray/pull/245
## New Contributors
- @stein-j made their first contribution in https://github.com/spatie/laravel-ray/pull/245
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.29.4...1.29.5
## 1.29.4 - 2022-02-22
## What's Changed
- check if ApplicationLogPayload can be loaded by @ThomasEnssner in https://github.com/spatie/laravel-ray/pull/242
## New Contributors
- @ThomasEnssner made their first contribution in https://github.com/spatie/laravel-ray/pull/242
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.29.3...1.29.4
## 1.29.3 - 2022-02-15
- correctly display mailables that are written to the log in Laravel 9
## 1.29.2 - 2022-02-13
## What's Changed
- Fix deprecated by @TiiFuchs in https://github.com/spatie/laravel-ray/pull/240
## New Contributors
- @TiiFuchs made their first contribution in https://github.com/spatie/laravel-ray/pull/240
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.29.1...1.29.2
## 1.29.1 - 2022-02-09
- moved dependency
## 1.29.0 - 2022-01-13
- automatically set project name
## 1.28.0 - 2022-01-11
1.28.0
- allow Laravel 9
## 1.28.0 - 2022-01-11
- allow Laravel 9
## 1.27.2 - 2021-12-27
- Fix: make sure there is always a `VarDumper` handler registered to output to HTML or CLI
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.27.1...1.27.2
## 1.27.1 - 2021-12-27
## What's Changed
- Register `DumpRecorder` only once and keep original handler connected by @AlexVanderbist in https://github.com/spatie/laravel-ray/pull/233
## New Contributors
- @AlexVanderbist made their first contribution in https://github.com/spatie/laravel-ray/pull/233
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.27.0...1.27.1
## 1.27.0 - 2021-12-26
## What's Changed
- Slow Query Logging by @patinthehat in https://github.com/spatie/laravel-ray/pull/232
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.26.5...1.27.0
## 1.26.5 - 2021-12-21
## What's Changed
- add support for Symfony 6 by @Nielsvanpach in https://github.com/spatie/laravel-ray/pull/231
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.26.4...1.26.5
## 1.26.4 - 2021-12-10
## What's Changed
- Added DeprecatedNoticeWatcher that piggy backs off of the Application… by @JuanRangel in https://github.com/spatie/laravel-ray/pull/229
## New Contributors
- @JuanRangel made their first contribution in https://github.com/spatie/laravel-ray/pull/229
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.26.3...1.26.4
## 1.26.3 - 2021-11-22
## What's Changed
- Fix typo in ray.php docblock by @iDiegoNL in https://github.com/spatie/laravel-ray/pull/227
## New Contributors
- @iDiegoNL made their first contribution in https://github.com/spatie/laravel-ray/pull/227
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.26.2...1.26.3
## 1.26.2 - 2021-11-15
## What's Changed
- Check if Laravel has been bound with `Facade\FlareClient\Flare` by @crynobone in https://github.com/spatie/laravel-ray/pull/224
**Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.26.1...1.26.2
## 1.26.1 - 2021-10-01
- fix #217 error with duplicate queries log (#220)
## 1.26.0 - 2021-09-27
- feature duplicate queries (#216)
## 1.25.2 - 2021-09-10
- enhance metadata instead of overriding it in when sending a request (#215)
## 1.25.1 - 2021-09-07
- add support for zbateson/mail-mime-parser v2 (#214)
## 1.25.0 - 2021-08-27
- add tags to cache payload (#210)
## 1.24.2 - 2021-07-23
- fix origin of query builder ray calls (now for real)
## 1.24.1 - 2021-07-23
- fix origin of query builder ray calls
## 1.24.0 - 2021-07-23
- add `ray` macro on query builder
## 1.23.0 - 2021-06-24
- allow multiple mailables ([#204](https://github.com/spatie/laravel-ray/pull/204))
## 1.22.0 - 2021-06-24
- when a query exception occurs, the query itself will also be sent to Ray.
## 1.21.0 - 2021-06-22
- add `countQueries`
## 1.20.2 - 2021-06-21
- fix `mailable` when using `Mail::fake`
## 1.20.1 - 2021-06-15
- fix origin of stringable
## 1.20.0 - 2021-06-15
- add support for stringables
## 1.19.1 - 2021-06-11
- better HTTP Client logging (#201)
## 1.19.0 - 2021-06-04
- add http logging methods
## 1.18.0 - 2021-03-23
- colorize high severity messages (#197)
## 1.17.4 - 2021-04-30
- check if an exception is passed before log dumping
## 1.17.3 - 2021-04-29
- the package won't send dumps to Ray when dump sending is disabled
## 1.17.2 - 2021-04-06
- Laravel Octane Compatibility (#178)
## 1.17.1 - 2021-03-14
- send exceptions by default
## 1.17.0 - 2021-03-13
- enable/disable sending exceptions to Ray (#173)
## 1.16.0 - 2021-03-12
- allow using `env()` when config is not available (#172)
## 1.15.1 - 2021-03-10
- fix handling of null logs (#171)
## 1.15.0 - 2021-03-09
- add `env` method
## 1.14.0 - 2021-03-04
- add support for hostname
## 1.13.0 - 2021-02-22
- add exception watcher
## 1.12.6 - 2021-02-10
- replace spaces with underscores in `env()` calls (#154)
## 1.12.5 - 2021-02-10
- fix "Package spatie/laravel-ray is not installed" exception (#156)
## 1.12.4 - 2021-02-10
- handle edge case where ray proxy would not be set
## 1.12.3 - 2021-02-08
- chain colours on `show*` methods (#149)
## 1.12.2 - 2021-02-07
- ignore errors caused by using `storage_path`
## 1.12.1 - 2021-02-05
- register watchers on boot (#138)
## 1.12.0 - 2021-02-03
- remove enabled methods (#132)
## 1.11.2 - 2021-02-02
- do not blow up when using `Mail::fake()`
## 1.11.1 - 2021-02-01
- update config file
## 1.11.0 - 2021-01-31
- add view requests
- add view cache
## 1.10.1 - 2021-01-31
- display logged exceptions
## 1.10.0 - 2021-01-29
- add view methods
## 1.9.3 - 2021-01-28
- internals cleanup
## 1.9.2 - 2021-01-28
- improve dependencies
## 1.9.1 - 2021-01-25
- improve service provider
## 1.9.0 - 2021-01-22
- add `showJobs`
## 1.8.0 - 2021-01-19
- the package will now select the best payload type when passing something to `ray()`
## 1.7.1 - 2021-01-17
- lower dependencies
## 1.7.0 - 2021-01-15
- make `model` more flexible
## 1.6.1 - 2021-01-15
- better support for logged mailables
## 1.6.0 - 2021-01-15
- add `markdown` method
## 1.5.2 - 2021-01-13
- fix headers on response payload
## 1.5.1 - 2021-01-13
- make the test response macro chainable
## 1.5.0 - 2021-01-13
- add `testResponse` method
## 1.4.0 - 2021-01-13
- let the `model` call accepts multiple models.
## 1.3.6 - 2021-01-13
- update `str_replace()` calls in `ray:publish-config` with `env()` usage (#82)
## 1.3.5 - 2021-01-12
- improve recognizing mails in logs
## 1.3.4 - 2021-01-09
- add `env()` vars for each Laravel config setting (#55)
## 1.3.3 - 2021-01-09
- add `enabled()` and `disabled()` methods (#54)
## 1.3.2 - 2021-01-09
- fix frame for `rd` function
## 1.3.1 - 2021-01-09
- fix broken `queries()`-method (#51)
## 1.3.0 - 2021-01-08
- Add `PublishConfigCommand`
## 1.2.0 - 2021-01-08
- add support for `local_path` and `remote_path` settings
## 1.1.0 - 2021-01-07
- add support for Lumen (#22)
## 1.0.3 - 20201-01-07
- fix incompatibilities on Windows (#20)
- fix host settings (#14)
## 1.0.2 - 2021-01-07
- fix deps
## 1.0.1 - 2021-01-07
- fix deps
## 1.0.0 - 2021-01-07
- initial release
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Spatie bvba <info@spatie.be>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -0,0 +1,49 @@
# Debug with Ray to fix problems faster
[![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-ray.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-ray)
![Tests](https://github.com/spatie/laravel-ray/workflows/Tests/badge.svg)
[![PHPStan](https://github.com/spatie/laravel-ray/actions/workflows/phpstan.yml/badge.svg)](https://github.com/spatie/laravel-ray/actions/workflows/phpstan.yml)
[![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-ray.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-ray)
This package can be installed in any PHP application to send messages to [the Ray app](https://myray.app).
[<img src="https://github-ads.s3.eu-central-1.amazonaws.com/laravel-ray.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/laravel-ray)
The desktop app:
- can be used in WordPress, Laravel, PHP, JavaScript function
- shows you models, mails, queries, ... IN Laravel
- helps you to debug locally or via SSH
- lets you measure performance & set breakpoints
## Documentation
You can find the full documentation on [our documentation site](https://spatie.be/docs/ray).
## Testing
``` bash
composer test
```
## Changelog
Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
## Contributing
Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details.
## Security Vulnerabilities
Please review [our security policy](../../security/policy) on how to report security vulnerabilities.
## Credits
- [Freek Van der Herten](https://github.com/freekmurze)
- [All Contributors](../../contributors)
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
@@ -0,0 +1,87 @@
{
"name": "spatie/laravel-ray",
"description": "Easily debug Laravel apps",
"license": "MIT",
"keywords": [
"spatie",
"laravel-ray"
],
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
}
],
"homepage": "https://github.com/spatie/laravel-ray",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/spatie"
},
{
"type": "other",
"url": "https://spatie.be/open-source/support-us"
}
],
"require": {
"php": "^7.4|^8.0",
"ext-json": "*",
"composer-runtime-api": "^2.2",
"illuminate/contracts": "^7.20|^8.19|^9.0|^10.0|^11.0|^12.0",
"illuminate/database": "^7.20|^8.19|^9.0|^10.0|^11.0|^12.0",
"illuminate/queue": "^7.20|^8.19|^9.0|^10.0|^11.0|^12.0",
"illuminate/support": "^7.20|^8.19|^9.0|^10.0|^11.0|^12.0",
"spatie/backtrace": "^1.7.1",
"spatie/ray": "^1.44.0",
"symfony/stopwatch": "4.2|^5.1|^6.0|^7.0|^8.0",
"zbateson/mail-mime-parser": "^1.3.1|^2.0|^3.0"
},
"require-dev": {
"guzzlehttp/guzzle": "^7.3",
"laravel/framework": "^7.20|^8.19|^9.0|^10.0|^11.0|^12.0",
"laravel/pint": "^1.25",
"orchestra/testbench-core": "^5.0|^6.0|^7.0|^8.0|^9.0|^10.0",
"pestphp/pest": "^1.22|^2.0|^3.0|^4.0",
"phpstan/phpstan": "^1.10.57|^2.0.2",
"phpunit/phpunit": "^9.3|^10.1|^11.0.10|^12.4",
"rector/rector": "^0.19.2|^1.0.1|^2.0.0",
"spatie/pest-plugin-snapshots": "^1.1|^2.0",
"symfony/var-dumper": "^4.2|^5.1|^6.0|^7.0.3|^8.0"
},
"minimum-stability": "dev",
"prefer-stable": true,
"autoload": {
"psr-4": {
"Spatie\\LaravelRay\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Spatie\\LaravelRay\\Tests\\": "tests"
}
},
"config": {
"allow-plugins": {
"pestphp/pest-plugin": true
},
"sort-packages": true
},
"extra": {
"branch-alias": {
"dev-main": "1.x-dev"
},
"laravel": {
"providers": [
"Spatie\\LaravelRay\\RayServiceProvider"
]
}
},
"scripts": {
"analyse": "vendor/bin/phpstan",
"format": "vendor/bin/pint",
"test": "vendor/bin/pest",
"test-coverage": "vendor/bin/pest --coverage"
}
}
@@ -0,0 +1,12 @@
{
"$schema": "https://laravel-ide.com/schema/laravel-ide-v2.json",
"blade": {
"directives": [
{
"name": "ray",
"prefix": "<?php ray(",
"suffix": "); ?>"
}
]
}
}
@@ -0,0 +1,91 @@
parameters:
ignoreErrors:
-
message: "#^Call to an undefined method Spatie\\\\LaravelRay\\\\OriginFactory\\:\\:returnTinkerFrame\\(\\)\\.$#"
count: 1
path: src/OriginFactory.php
-
message: "#^Variable \\$foundFrame in PHPDoc tag @var does not exist\\.$#"
count: 2
path: src/OriginFactory.php
-
message: "#^Call to method createReport\\(\\) on an unknown class Facade\\\\FlareClient\\\\Flare\\.$#"
count: 1
path: src/Watchers/ExceptionWatcher.php
-
message: "#^Call to method createReport\\(\\) on an unknown class Spatie\\\\FlareClient\\\\Flare\\.$#"
count: 1
path: src/Watchers/ExceptionWatcher.php
-
message: "#^Call to method trim\\(\\) on an unknown class Facade\\\\FlareClient\\\\Truncation\\\\ReportTrimmer\\.$#"
count: 1
path: src/Watchers/ExceptionWatcher.php
-
message: "#^Call to method trim\\(\\) on an unknown class Spatie\\\\FlareClient\\\\Truncation\\\\ReportTrimmer\\.$#"
count: 1
path: src/Watchers/ExceptionWatcher.php
-
message: "#^Class Facade\\\\FlareClient\\\\Flare not found\\.$#"
count: 2
path: src/Watchers/ExceptionWatcher.php
-
message: "#^Class Spatie\\\\FlareClient\\\\Flare not found\\.$#"
count: 2
path: src/Watchers/ExceptionWatcher.php
-
message: "#^Instantiated class Facade\\\\FlareClient\\\\Truncation\\\\ReportTrimmer not found\\.$#"
count: 1
path: src/Watchers/ExceptionWatcher.php
-
message: "#^Instantiated class Spatie\\\\FlareClient\\\\Truncation\\\\ReportTrimmer not found\\.$#"
count: 1
path: src/Watchers/ExceptionWatcher.php
-
message: "#^PHPDoc tag @var for variable \\$flare contains unknown class Facade\\\\FlareClient\\\\Flare\\.$#"
count: 1
path: src/Watchers/ExceptionWatcher.php
-
message: "#^Access to an undefined property Illuminate\\\\Support\\\\Optional::\\$file.$#"
count: 1
path: src/OriginFactory.php
-
message: "#^Access to an undefined property Illuminate\\\\Support\\\\Optional::\\$lineNumber.$#"
count: 1
path: src/OriginFactory.php
-
message: "#^Call to an undefined method Illuminate\\\\Support\\\\Optional::applyCalledMethods\\(\\).$#"
count: 11
path: src/
-
message: "#^Call to an undefined method Illuminate\\\\Support\\\\Optional::getActionName\\(\\).$#"
count: 1
path: src/Watchers/RequestWatcher.php
-
message: "#^Call to an undefined method Illuminate\\\\Support\\\\Optional::gatherMiddleware\\(\\).$#"
count: 1
path: src/Watchers/RequestWatcher.php
-
message: "#^Access to protected property Illuminate\\\\Support\\\\Collection::\\$items.$#"
count: 2
path: src/RayServiceProvider.php
-
message: "#^Access to protected property Illuminate\\\\Support\\\\Stringable::\\$value.$#"
count: 2
path: src/RayServiceProvider.php
@@ -0,0 +1,16 @@
includes:
- phpstan-baseline.neon
parameters:
level: 2
paths:
- src
ignoreErrors:
- '#^Call to method \w+\(\) on an unknown class Spatie\\YiiRay\\Ray\.$#'
- '#^Call to method \w+\(\) on an unknown class Spatie\\WordPressRay\\Ray\.$#'
- '#^Call to method \w+\(\) on an unknown class Spatie\\RayBundle\\Ray\.$#'
- '#^Access to an undefined property Spatie\\Ray\\Settings\\Settings\:\:\$\w+\.$#'
-
message: '#Unsafe usage of new static\(\).#'
reportUnmatched: false
@@ -0,0 +1,47 @@
<?php
namespace Spatie\LaravelRay\Commands;
use Composer\InstalledVersions;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Facades\Process;
use Spatie\LaravelRay\Support\Composer;
class CleanRayCommand extends Command
{
protected $signature = 'ray:clean';
protected $description = 'Remove all Ray calls from your codebase.';
public function handle(Filesystem $files)
{
$directories = [
'app',
'config',
'database',
'public',
'resources',
'routes',
'tests',
];
if (! InstalledVersions::isInstalled('rector/rector')) {
(new Composer($files, defined('TESTBENCH_WORKING_PATH') ? TESTBENCH_WORKING_PATH : base_path()))
->requirePackages(['rector/rector'], true, $this->output);
}
$this->withProgressBar($directories, function ($directory) {
$result = Process::run('./vendor/bin/remove-ray.sh '.$directory);
if (! $result->successful()) {
$this->error($result->errorOutput());
return;
}
});
$this->newLine(2);
$this->info('All Ray calls have been removed from your codebase.');
}
}
@@ -0,0 +1,49 @@
<?php
namespace Spatie\LaravelRay\Commands;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
class PublishConfigCommand extends Command
{
protected $signature = 'ray:publish-config {--homestead : Indicates that Homestead is being used}
{--docker : Indicates that Docker is being used}';
protected $description = 'Create the Laravel Ray config file in project root.';
public function handle()
{
if ((new Filesystem)->exists('ray.php')) {
$this->error('ray.php already exists in the project root');
return;
}
copy(__DIR__.'/../../stub/ray.php', base_path('ray.php'));
if ($this->option('docker')) {
file_put_contents(
base_path('ray.php'),
str_replace(
"'host' => env('RAY_HOST', 'localhost')",
"'host' => env('RAY_HOST', 'host.docker.internal')",
file_get_contents(base_path('ray.php'))
)
);
}
if ($this->option('homestead')) {
file_put_contents(
base_path('ray.php'),
str_replace(
"'host' => env('RAY_HOST', 'localhost')",
"'host' => env('RAY_HOST', '10.0.2.2')",
file_get_contents(base_path('ray.php'))
)
);
}
$this->info('`ray.php` created in the project base directory');
}
}
@@ -0,0 +1,98 @@
<?php
namespace Spatie\LaravelRay\DumpRecorder;
use Illuminate\Contracts\Container\Container;
use ReflectionMethod;
use ReflectionProperty;
use Spatie\LaravelRay\Ray;
use Symfony\Component\VarDumper\VarDumper;
class DumpRecorder
{
/** @var array */
protected $dumps = [];
/** @var \Illuminate\Contracts\Container\Container */
protected $app;
protected static $registeredHandler = false;
protected static $runningLaravel9 = null;
public function __construct(Container $app)
{
$this->app = $app;
if (static::$runningLaravel9 === null) {
static::$runningLaravel9 = version_compare(app()->version(), '9.0.0', '>=');
}
}
public function register(): self
{
$multiDumpHandler = new MultiDumpHandler;
$this->app->singleton(MultiDumpHandler::class, function () use ($multiDumpHandler) {
return $multiDumpHandler;
});
if (! static::$registeredHandler || static::$runningLaravel9) {
static::$registeredHandler = true;
$multiDumpHandler->resetHandlers();
$this->ensureOriginalHandlerExists();
$originalHandler = VarDumper::setHandler(function ($dumpedVariable) use ($multiDumpHandler) {
$multiDumpHandler->dump($dumpedVariable);
});
if ($originalHandler) {
$multiDumpHandler->addHandler($originalHandler);
}
$multiDumpHandler->addHandler(function ($dumpedVariable) {
if ($this->shouldDump()) {
app(Ray::class)->send($dumpedVariable);
}
});
}
return $this;
}
protected function shouldDump(): bool
{
/** @var Ray $ray */
$ray = app(Ray::class);
return $ray->settings->send_dumps_to_ray;
}
/**
* Only the `VarDumper` knows how to create the orignal HTML or CLI VarDumper.
* Using reflection and the private VarDumper::register() method we can force it
* to create and register a new VarDumper::$handler before we'll overwrite it.
* Of course, we only need to do this if there isn't a registered VarDumper::$handler.
*
* @throws \ReflectionException
*/
protected function ensureOriginalHandlerExists(): void
{
$reflectionProperty = new ReflectionProperty(VarDumper::class, 'handler');
if (PHP_VERSION_ID < 80100) {
$reflectionProperty->setAccessible(true);
}
$handler = $reflectionProperty->getValue();
if (! $handler) {
// No handler registered yet, so we'll force VarDumper to create one.
$reflectionMethod = new ReflectionMethod(VarDumper::class, 'register');
if (PHP_VERSION_ID < 80100) {
$reflectionMethod->setAccessible(true);
}
$reflectionMethod->invoke(null);
}
}
}
@@ -0,0 +1,28 @@
<?php
namespace Spatie\LaravelRay\DumpRecorder;
class MultiDumpHandler
{
/** @var array */
protected $handlers = [];
public function dump($value)
{
foreach ($this->handlers as $handler) {
$handler($value);
}
}
public function addHandler(?callable $callable = null): self
{
$this->handlers[] = $callable;
return $this;
}
public function resetHandlers(): void
{
$this->handlers = [];
}
}
@@ -0,0 +1,260 @@
<?php
namespace Spatie\LaravelRay;
use Illuminate\Cache\CacheManager;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Database\Query\Builder;
use Illuminate\Events\Dispatcher;
use Illuminate\Log\Logger;
use Illuminate\Log\LogManager;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Facade;
use Illuminate\Support\Str;
use Illuminate\Support\Stringable;
use Spatie\Backtrace\Backtrace;
use Spatie\Backtrace\Frame;
use Spatie\LaravelRay\DumpRecorder\DumpRecorder;
use Spatie\LaravelRay\Watchers\CacheWatcher;
use Spatie\LaravelRay\Watchers\QueryWatcher;
use Spatie\LaravelRay\Watchers\ViewWatcher;
use Spatie\Ray\Origin\Origin;
use Spatie\Ray\Ray;
use Spatie\Ray\Support\Invador;
class OriginFactory
{
public function getOrigin(): Origin
{
$frame = $this->getFrame();
return new Origin(
optional($frame)->file,
optional($frame)->lineNumber,
);
}
protected function getFrame(): ?Frame
{
$frames = collect(Backtrace::create()->frames())->reverse();
$indexOfRay = $frames
->search(function (Frame $frame) {
if ($frame->class === Ray::class) {
return true;
}
if (Str::startsWith($frame->file, __DIR__)) {
return true;
}
return false;
});
/** @var Frame|null $rayFrame */
$rayFrame = $frames[$indexOfRay] ?? null;
$rayFunctionFrame = $frames[$indexOfRay + 2] ?? null;
/** @var Frame|null $originFrame */
$originFrame = $frames[$indexOfRay + 1] ?? null;
if ($originFrame && Str::endsWith($originFrame->file, Ray::makePathOsSafe('ray/src/helpers.php'))) {
$framesAbove = 2;
if ($rayFunctionFrame && $rayFunctionFrame->method === 'rd') {
$framesAbove = 3;
}
$originFrame = $frames[$indexOfRay + $framesAbove] ?? null;
}
if (! $rayFrame) {
return null;
}
if ($rayFrame->class === Stringable::class) {
return $this->findFrameForStringableMacro($frames, $indexOfRay);
}
if ($rayFrame->class === Collection::class && Str::startsWith($rayFrame->method, 'Spatie\LaravelRay')) {
return $this->findFrameForCollectionMacro($frames, $indexOfRay);
}
if ($rayFrame->class === QueryWatcher::class) {
return $this->findFrameForQuery($frames);
}
if ($rayFrame->class === ViewWatcher::class) {
return $this->findFrameForView($frames, $indexOfRay);
}
if ($rayFrame->class === DumpRecorder::class) {
return $this->findFrameForDump($frames);
}
if ($rayFrame->class === CacheWatcher::class) {
return $this->findFrameForCache($frames);
}
if ($originFrame->class === Dispatcher::class) {
return $this->findFrameForEvent($frames);
}
if ($originFrame->class === Builder::class) {
return $this->findFrameForQueryBuilder($frames);
}
if (Str::endsWith($originFrame->file, Ray::makePathOsSafe('/vendor/psy/psysh/src/ExecutionLoopClosure.php'))) {
$this->returnTinkerFrame();
}
try {
if (Str::startsWith($originFrame->file, config('view.compiled'))) {
return $this->replaceCompiledViewPathWithOriginalViewPath($originFrame);
}
} catch (BindingResolutionException $exception) {
// ignore errors caused by using `storage_path`
}
if ($originFrame->class === Invador::class) {
return $frames[$indexOfRay + 2];
}
return $originFrame;
}
protected function findFrameForStringableMacro(Collection $frames, int $indexOfFoundFrame): ?Frame
{
return $frames[$indexOfFoundFrame + 2];
}
protected function findFrameForCollectionMacro(Collection $frames, int $indexOfFoundFrame): ?Frame
{
return $frames[$indexOfFoundFrame + 2];
}
protected function findFrameForQuery(Collection $frames): ?Frame
{
$indexOfLastDatabaseCall = $frames
->filter(function (Frame $frame) {
return ! is_null($frame->class);
})
->search(function (Frame $frame) {
return Str::startsWith($frame->class, 'Illuminate\Database');
});
return $frames[$indexOfLastDatabaseCall + 1] ?? null;
}
protected function findFrameForQueryBuilder(Collection $frames): ?Frame
{
$indexOfLastDatabaseCall = $frames
->filter(function (Frame $frame) {
return ! is_null($frame->class);
})
->search(function (Frame $frame) {
return Str::startsWith($frame->class, 'Illuminate\Database');
});
return $frames[$indexOfLastDatabaseCall + 1] ?? null;
}
protected function findFrameForView(Collection $frames, int $indexOfRayFrame): ?Frame
{
return $frames[$indexOfRayFrame + 6] ?? null;
}
protected function findFrameForDump(Collection $frames): ?Frame
{
$indexOfDumpCall = $frames
->search(function (Frame $frame) {
if (! is_null($frame->class)) {
return false;
}
return in_array($frame->method, ['dump', 'dd']);
});
return $frames[$indexOfDumpCall + 1] ?? null;
}
protected function findFrameForEvent(Collection $frames): ?Frame
{
$indexOfLoggerCall = $frames
->search(function (Frame $frame) {
return $frame->class === Logger::class;
});
if ($indexOfLoggerCall) {
return $this->findFrameForLog($frames, $indexOfLoggerCall);
}
$indexOfEventDispatcherCall = $frames
->search(function (Frame $frame) {
return ($frame->class === Dispatcher::class) && $frame->method === 'dispatch';
});
/** @var Frame $foundFrame */
if ($foundFrame = $frames[$indexOfEventDispatcherCall + 2]) {
if (Str::endsWith($foundFrame->file, Ray::makePathOsSafe('/Illuminate/Foundation/Events/Dispatchable.php'))) {
$foundFrame = $frames[$indexOfEventDispatcherCall + 3];
}
}
return $foundFrame ?? null;
}
protected function findFrameForLog(Collection $frames, int $indexOfLoggerCall): ?Frame
{
/** @var Frame $foundFrame */
if ($foundFrame = $frames[$indexOfLoggerCall + 1]) {
if ($foundFrame->class === LogManager::class) {
$foundFrame = $frames[$indexOfLoggerCall + 2];
if ($foundFrame->class = Facade::class) {
$foundFrame = $frames[$indexOfLoggerCall + 3];
}
if (Str::endsWith($foundFrame->file, Ray::makePathOsSafe('/Illuminate/Foundation/helpers.php'))) {
$foundFrame = $frames[$indexOfLoggerCall + 3];
}
}
}
return $foundFrame ?? null;
}
public function findFrameForCache(Collection $frames): ?Frame
{
$index = $frames->search(function (Frame $frame) {
return $frame->class === CacheManager::class;
});
while (Str::startsWith($frames[$index]->class, 'Illuminate')) {
$index++;
}
return $frames[$index] ?? null;
}
protected function replaceCompiledViewPathWithOriginalViewPath(Frame $frame): Frame
{
if (! file_exists($frame->file)) {
return $frame;
}
$fileContents = file_get_contents($frame->file);
$originalViewPath = trim(Str::between($fileContents, '/**PATH', 'ENDPATH**/'));
if (! file_exists($originalViewPath)) {
return $frame;
}
$frame->file = $originalViewPath;
$frame->lineNumber = 1;
return $frame;
}
}
@@ -0,0 +1,58 @@
<?php
namespace Spatie\LaravelRay\Payloads;
use Spatie\Ray\ArgumentConverter;
use Spatie\Ray\Payloads\Payload;
class CachePayload extends Payload
{
/** @var string */
protected $type;
/** @var string[] */
protected $tags;
/** @var string */
protected $key;
/** @var mixed */
protected $value;
/** @var int|null */
protected $expirationInSeconds;
public function __construct(string $type, string $key, $tags, $value = null, ?int $expirationInSeconds = null)
{
$this->type = $type;
$this->key = $key;
$this->tags = is_array($tags) ? $tags : [$tags];
$this->value = $value;
$this->expirationInSeconds = $expirationInSeconds;
}
public function getType(): string
{
return 'table';
}
public function getContent(): array
{
$values = array_filter([
'Event' => '<code>'.$this->type.'</code>',
'Key' => $this->key,
'Value' => ArgumentConverter::convertToPrimitive($this->value),
'Tags' => count($this->tags) ? ArgumentConverter::convertToPrimitive($this->tags) : null,
'Expiration in seconds' => $this->expirationInSeconds,
]);
return [
'values' => $values,
'label' => 'Cache',
];
}
}
@@ -0,0 +1,107 @@
<?php
namespace Spatie\LaravelRay\Payloads;
use Dotenv\Dotenv;
use Illuminate\Support\Env;
use Spatie\Ray\ArgumentConverter;
use Spatie\Ray\Payloads\Payload;
class EnvironmentPayload extends Payload
{
/** @var array */
protected $values;
/** @var string */
protected $path;
/** @var string */
protected $filename;
/**
* @param string[]|array|null $onlyShowNames
*/
public function __construct(?array $onlyShowNames = null, ?string $filename = null)
{
$filename = $filename ?? app()->environmentFilePath();
$this->path = dirname($filename);
$this->filename = basename($filename);
$this->values = $this->getDotEnvValues($onlyShowNames);
}
public function getType(): string
{
return 'table';
}
public function getContent(): array
{
$values = array_map(function ($value) {
$value = $this->decorateSpecialValues($value);
return ArgumentConverter::convertToPrimitive($value);
}, $this->values);
return [
'values' => $values,
'label' => '.env',
];
}
protected function decorateSpecialValues($value)
{
if ($value === '') {
return '<div class="text-gray-400">(empty)</div>';
}
if ($value === 'null' || $value === 'NULL') {
return '<div class="text-gray-400">NULL</div>';
}
if ($value === 'true' || $value === 'false') {
$color = $value === 'true' ? 'green' : 'red';
return "<div class=\"text-{$color}-600\">{$value}</div>";
}
if (preg_match('~^https?://~', $value) === 1) {
return "<a href=\"{$value}\" class=\"text-blue-600 hover:underline\">{$value}</a>";
}
if (strpos($value, 'base64:') === 0) {
return "<div class=\"text-gray-400\">{$value}</div>";
}
// ip addresses
if (preg_match('~(\d{1,3}\.){3}\d{1,3}~', $value) === 1) {
return "<div href=\"{$value}\" class=\"text-indigo-700\">{$value}</div>";
}
return $value;
}
protected function loadDotEnv(): array
{
return Dotenv::create(
Env::getRepository(),
$this->path,
$this->filename
)->safeLoad();
}
protected function getDotEnvValues(?array $filterNames): array
{
$values = $this->loadDotEnv();
if (! $filterNames) {
return $values;
}
return array_filter($values, function ($value) use ($filterNames) {
return in_array($value, $filterNames, true);
}, ARRAY_FILTER_USE_KEY);
}
}
@@ -0,0 +1,42 @@
<?php
namespace Spatie\LaravelRay\Payloads;
use Spatie\Ray\ArgumentConverter;
use Spatie\Ray\Payloads\Payload;
class EventPayload extends Payload
{
/** @var string */
protected $eventName;
/** @var object|mixed|null */
protected $event = null;
/** @var array */
protected $payload = [];
public function __construct(string $eventName, array $payload)
{
$this->eventName = $eventName;
class_exists($eventName)
? $this->event = $payload[0]
: $this->payload = $payload;
}
public function getType(): string
{
return 'event';
}
public function getContent(): array
{
return [
'name' => $this->eventName,
'event' => $this->event ? ArgumentConverter::convertToPrimitive($this->event) : null,
'payload' => count($this->payload) ? ArgumentConverter::convertToPrimitive($this->payload) : null,
'class_based_event' => ! is_null($this->event),
];
}
}
@@ -0,0 +1,51 @@
<?php
namespace Spatie\LaravelRay\Payloads;
use Illuminate\Database\Events\QueryExecuted;
use Spatie\Ray\Payloads\Payload;
class ExecutedQueryPayload extends Payload
{
/** @var \Illuminate\Database\Events\QueryExecuted */
protected $query;
public function __construct(QueryExecuted $query)
{
$this->query = $query;
}
public function getType(): string
{
return 'executed_query';
}
public function getContent(): array
{
$grammar = $this->query->connection->getQueryGrammar();
$properties = method_exists($grammar, 'substituteBindingsIntoRawSql') ? [
'sql' => $grammar->substituteBindingsIntoRawSql(
$this->query->sql,
$this->query->connection->prepareBindings($this->query->bindings)
),
] : [
'sql' => $this->query->sql,
'bindings' => $this->query->bindings,
];
if ($this->hasAllProperties()) {
$properties = array_merge($properties, [
'connection_name' => $this->query->connectionName,
'time' => $this->query->time,
]);
}
return $properties;
}
protected function hasAllProperties(): bool
{
return ! is_null($this->query->time);
}
}
@@ -0,0 +1,48 @@
<?php
namespace Spatie\LaravelRay\Payloads;
use Illuminate\Queue\Jobs\Job;
use Spatie\Ray\ArgumentConverter;
use Spatie\Ray\Payloads\Payload;
class JobEventPayload extends Payload
{
/** @var object */
protected $event;
/** @var object|mixed */
protected $job;
/** @var \Throwable|null */
protected $exception = null;
public function __construct(object $event)
{
$this->event = $event;
// Some queue drivers use an intermediate job with the orignal job stored inside.
// For other drivers, the job is not altered, and it can be used directly.
$this->job = $event->job instanceof Job
? unserialize($event->job->payload()['data']['command'])
: $this->job = $event->job;
if (property_exists($event, 'exception')) {
$this->exception = $event->exception ?? null;
}
}
public function getType(): string
{
return 'job_event';
}
public function getContent(): array
{
return [
'event_name' => class_basename($this->event),
'job' => $this->job ? ArgumentConverter::convertToPrimitive($this->job) : null,
'exception' => $this->exception ? ArgumentConverter::convertToPrimitive($this->exception) : null,
];
}
}
@@ -0,0 +1,123 @@
<?php
namespace Spatie\LaravelRay\Payloads;
use Spatie\Ray\Payloads\Payload;
use ZBateson\MailMimeParser\Header\AddressHeader;
use ZBateson\MailMimeParser\Header\HeaderConsts;
use ZBateson\MailMimeParser\Header\Part\AddressPart;
use ZBateson\MailMimeParser\IMessage;
use ZBateson\MailMimeParser\MailMimeParser;
class LoggedMailPayload extends Payload
{
/** @var string */
protected $html = '';
/** @var array */
protected $from;
/** @var string|null */
protected $subject;
/** @var array */
protected $to;
/** @var array */
protected $cc;
/** @var array */
protected $bcc;
public static function forLoggedMail(string $loggedMail): self
{
$parser = new MailMimeParser;
$message = $parser->parse($loggedMail, true);
// get the part in $loggedMail that starts with <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0
$content = self::getMailContent($loggedMail, $message);
return new self(
$content,
self::convertHeaderToPersons($message->getHeader(HeaderConsts::FROM)),
$message->getHeaderValue(HeaderConsts::SUBJECT),
self::convertHeaderToPersons($message->getHeader(HeaderConsts::TO)),
self::convertHeaderToPersons($message->getHeader(HeaderConsts::CC)),
self::convertHeaderToPersons($message->getHeader(HeaderConsts::BCC)),
);
}
public function __construct(
string $html,
array $from = [],
?string $subject = null,
array $to = [],
array $cc = [],
array $bcc = []
) {
$this->html = $html;
$this->from = $from;
$this->subject = $subject;
$this->to = $to;
$this->cc = $cc;
$this->bcc = $bcc;
}
protected static function getMailContent(string $loggedMail, IMessage $message): string
{
$startOfHtml = strpos($loggedMail, '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0', true);
if (! $startOfHtml) {
return $message->getContent() ?? $message->getHtmlContent() ?? '';
}
return substr($loggedMail, $startOfHtml) ?? '';
}
public function getType(): string
{
return 'mailable';
}
public function getContent(): array
{
return [
'html' => $this->sanitizeHtml($this->html),
'subject' => $this->subject,
'from' => $this->from,
'to' => $this->to,
'cc' => $this->cc,
'bcc' => $this->bcc,
];
}
protected function sanitizeHtml(string $html): string
{
$needle = 'Content-Type: text/html; charset=utf-8 Content-Transfer-Encoding: quoted-printable';
if (strpos($html, $needle) !== false) {
$html = substr($html, strpos($html, $needle));
}
return $html;
}
protected static function convertHeaderToPersons(?AddressHeader $header): array
{
if ($header === null) {
return [];
}
return array_map(
function (AddressPart $address) {
return [
'name' => $address->getName(),
'email' => $address->getEmail(),
];
},
$header->getAddresses()
);
}
}
@@ -0,0 +1,78 @@
<?php
namespace Spatie\LaravelRay\Payloads;
use Illuminate\Mail\Mailable;
use Spatie\Ray\Payloads\Payload;
use Throwable;
class MailablePayload extends Payload
{
/** @var string */
protected $html = '';
/** @var \Illuminate\Mail\Mailable|null */
protected $mailable = null;
public static function forMailable(Mailable $mailable)
{
return new self(self::renderMailable($mailable), $mailable);
}
public function __construct(string $html, ?Mailable $mailable = null)
{
$this->html = $html;
$this->mailable = $mailable;
}
public function getType(): string
{
return 'mailable';
}
public function getContent(): array
{
$content = [
'html' => $this->html,
'from' => [],
'to' => [],
'cc' => [],
'bcc' => [],
];
if ($this->mailable) {
$content = array_merge($content, [
'mailable_class' => get_class($this->mailable),
'from' => $this->convertToPersons($this->mailable->from),
'subject' => $this->mailable->subject,
'to' => $this->convertToPersons($this->mailable->to),
'cc' => $this->convertToPersons($this->mailable->cc),
'bcc' => $this->convertToPersons($this->mailable->bcc),
]);
}
return $content;
}
protected static function renderMailable(Mailable $mailable): string
{
try {
return $mailable->render();
} catch (Throwable $exception) {
return "Mailable could not be rendered because {$exception->getMessage()}";
}
}
protected function convertToPersons(array $persons): array
{
return collect($persons)
->map(function (array $person) {
return [
'email' => $person['address'],
'name' => $person['name'] ?? '',
];
})
->toArray();
}
}
@@ -0,0 +1,75 @@
<?php
namespace Spatie\LaravelRay\Payloads;
use League\CommonMark\GithubFlavoredMarkdownConverter;
use Spatie\Ray\Payloads\Payload;
class MarkdownPayload extends Payload
{
/** @var string */
protected $markdown;
public function __construct(string $markdown)
{
$this->markdown = $markdown;
}
public function getType(): string
{
return 'custom';
}
public function getContent(): array
{
return [
'content' => $this->markdownToHtml($this->markdown),
'label' => 'Markdown',
];
}
protected function markdownToHtml(string $markdown): string
{
$converter = new GithubFlavoredMarkdownConverter([
'renderer' => [
'block_separator' => "<br>\n",
],
'html_input' => 'allow',
'allow_unsafe_links' => false,
]);
$html = $converter->convertToHtml($markdown);
$html = $this->processCodeBlocks($html);
$html = $this->processHeaderTags($html);
$css = $this->getCustomStyles();
return trim("{$css}{$html}");
}
protected function getCustomStyles(): string
{
// render links as underlined
return '<style>a { text-decoration:underline!important; }</style>';
}
protected function processCodeBlocks($html): string
{
// format code blocks background color, padding, and display width; the background
// color changes based on light or dark app theme.
return str_replace('<pre><code', '<pre class="w-100 bg-gray-200 dark:bg-gray-800 p-5"><code', $html);
}
protected function processHeaderTags($html): string
{
// render headers with the correct format and size as divs
$html = str_replace(['<h1>', '<h2>', '<h3>', '<h4>'], [
'<div class="w-100 block" style="font-size:2.1em!important;">',
'<div class="w-100 block" style="font-size:1.8em!important;">',
'<div class="w-100 block" style="font-size:1.5em!important;">',
'<div class="w-100 block" style="font-size:1.2em!important;">',
], $html);
// replace closing header tags with closing div tags
return preg_replace('~</h[1-4]>~', '</div>', $html);
}
}
@@ -0,0 +1,43 @@
<?php
namespace Spatie\LaravelRay\Payloads;
use Illuminate\Database\Eloquent\Model;
use Spatie\Ray\ArgumentConverter;
use Spatie\Ray\Payloads\Payload;
class ModelPayload extends Payload
{
/** @var \Illuminate\Database\Eloquent\Model|null */
protected $model;
public function __construct(?Model $model)
{
$this->model = $model;
}
public function getType(): string
{
return 'eloquent_model';
}
public function getContent(): array
{
if (! $this->model) {
return [];
}
$content = [
'class_name' => get_class($this->model),
'attributes' => ArgumentConverter::convertToPrimitive($this->model->attributesToArray()),
];
$relations = $this->model->relationsToArray();
if (count($relations)) {
$content['relations'] = ArgumentConverter::convertToPrimitive($relations);
}
return $content;
}
}
@@ -0,0 +1,36 @@
<?php
namespace Spatie\LaravelRay\Payloads;
use Illuminate\Database\Query\Builder;
use Spatie\Ray\Payloads\Payload;
class QueryPayload extends Payload
{
/** @var \Illuminate\Database\Query\Builder */
protected $query;
public function __construct(Builder $query)
{
$this->query = $query;
}
public function getType(): string
{
return 'executed_query';
}
public function getContent(): array
{
if (method_exists($this->query, 'toRawSql')) {
return [
'sql' => $this->query->toRawSql(),
];
}
return [
'sql' => $this->query->toSql(),
'bindings' => $this->query->getBindings(),
];
}
}
@@ -0,0 +1,70 @@
<?php
namespace Spatie\LaravelRay\Payloads;
use Illuminate\Testing\TestResponse;
use Spatie\Ray\ArgumentConverter;
use Spatie\Ray\Payloads\Payload;
class ResponsePayload extends Payload
{
/** @var int */
protected $statusCode;
/** @var array */
protected $headers;
/** @var string|null */
protected $content;
/** @var array|null */
protected $json;
public static function fromTestResponse(TestResponse $testResponse): self
{
return new self(
$testResponse->getStatusCode(),
$testResponse->headers->all(),
$testResponse->content(),
$json = rescue(function () use ($testResponse) {
return $testResponse->json();
}, null, false)
);
}
public function __construct(int $statusCode, array $headers, string $content, ?array $json = null)
{
$this->statusCode = $statusCode;
$this->headers = $this->normalizeHeaders($headers);
$this->content = $content;
$this->json = $json;
}
public function getType(): string
{
return 'response';
}
public function getContent(): array
{
return [
'status_code' => $this->statusCode,
'headers' => ArgumentConverter::convertToPrimitive($this->headers),
'content' => $this->content,
'json' => ArgumentConverter::convertToPrimitive($this->json),
];
}
protected function normalizeHeaders(array $headers): array
{
return collect($headers)
->map(function (array $values) {
return $values[0] ?? null;
})
->filter()
->toArray();
}
}
@@ -0,0 +1,53 @@
<?php
namespace Spatie\LaravelRay\Payloads;
use Illuminate\Support\Str;
use Illuminate\View\View;
use Spatie\Ray\ArgumentConverter;
use Spatie\Ray\Payloads\Payload;
class ViewPayload extends Payload
{
/** @var \Illuminate\View\View */
protected $view;
public function __construct(View $view)
{
$this->view = $view;
}
public function getType(): string
{
return 'view';
}
public function getContent(): array
{
return [
'view_path' => $this->view->getPath(),
'view_path_relative_to_project_root' => Str::after($this->pathRelativeToProjectRoot($this->view), '/'),
'data' => ArgumentConverter::convertToPrimitive($this->getData($this->view)),
];
}
protected function pathRelativeToProjectRoot(View $view): string
{
$path = $view->getPath();
if (Str::startsWith($path, base_path())) {
$path = substr($path, strlen(base_path()));
}
return $path;
}
protected function getData(View $view): array
{
return collect($view->getData())
->filter(function ($value, $key) {
return ! in_array($key, ['app', '__env', 'obLevel', 'errors']);
})
->toArray();
}
}
@@ -0,0 +1,646 @@
<?php
namespace Spatie\LaravelRay;
use Closure;
use Composer\InstalledVersions;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Database\QueryException;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\MailManager;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Context;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Testing\Fakes\MailFake;
use Illuminate\Testing\TestResponse;
use Illuminate\View\View;
use ReflectionFunction;
use Spatie\LaravelRay\Payloads\EnvironmentPayload;
use Spatie\LaravelRay\Payloads\ExecutedQueryPayload;
use Spatie\LaravelRay\Payloads\LoggedMailPayload;
use Spatie\LaravelRay\Payloads\MailablePayload;
use Spatie\LaravelRay\Payloads\MarkdownPayload;
use Spatie\LaravelRay\Payloads\ModelPayload;
use Spatie\LaravelRay\Payloads\ResponsePayload;
use Spatie\LaravelRay\Payloads\ViewPayload;
use Spatie\LaravelRay\Watchers\CacheWatcher;
use Spatie\LaravelRay\Watchers\ConditionalQueryWatcher;
use Spatie\LaravelRay\Watchers\DeleteQueryWatcher;
use Spatie\LaravelRay\Watchers\DuplicateQueryWatcher;
use Spatie\LaravelRay\Watchers\EventWatcher;
use Spatie\LaravelRay\Watchers\ExceptionWatcher;
use Spatie\LaravelRay\Watchers\HttpClientWatcher;
use Spatie\LaravelRay\Watchers\InsertQueryWatcher;
use Spatie\LaravelRay\Watchers\JobWatcher;
use Spatie\LaravelRay\Watchers\MailWatcher;
use Spatie\LaravelRay\Watchers\QueryWatcher;
use Spatie\LaravelRay\Watchers\RequestWatcher;
use Spatie\LaravelRay\Watchers\SelectQueryWatcher;
use Spatie\LaravelRay\Watchers\SlowQueryWatcher;
use Spatie\LaravelRay\Watchers\UpdateQueryWatcher;
use Spatie\LaravelRay\Watchers\ViewWatcher;
use Spatie\LaravelRay\Watchers\Watcher;
use Spatie\Ray\Client;
use Spatie\Ray\Payloads\ExceptionPayload;
use Spatie\Ray\Ray as BaseRay;
use Spatie\Ray\Settings\Settings;
use Throwable;
class Ray extends BaseRay
{
public function __construct(Settings $settings, ?Client $client = null, ?string $uuid = null)
{
// persist the enabled setting across multiple instantiations
$enabled = static::$enabled;
parent::__construct($settings, $client, $uuid);
static::$enabled = $enabled;
}
public function loggedMail(string $loggedMail): self
{
$payload = LoggedMailPayload::forLoggedMail($loggedMail);
$this->sendRequest($payload);
return $this;
}
public function mailable(Mailable ...$mailables): self
{
$shouldRestoreFake = false;
if (get_class(app(MailManager::class)) === MailFake::class) {
$shouldRestoreFake = true;
Mail::swap(new MailManager(app()));
}
if ($shouldRestoreFake) {
Mail::fake();
}
$payloads = array_map(function (Mailable $mailable) {
return MailablePayload::forMailable($mailable);
}, $mailables);
$this->sendRequest($payloads);
return $this;
}
/**
* @param null $callable
* @return \Spatie\LaravelRay\Ray
*/
public function showMails($callable = null)
{
$watcher = app(MailWatcher::class);
$watcher->enable();
return $this->handleWatcherCallable($watcher, $callable);
}
public function stopShowingMails(): self
{
app(MailWatcher::class)->disable();
return $this;
}
/**
* @param array|string ...$keys
* @return $this
*/
public function context(...$keys): self
{
if (! class_exists(Context::class)) {
return $this;
}
if (isset($keys[0]) && is_array($keys[0])) {
$keys = $keys[0];
}
$context = count($keys)
? Context::only($keys)
: Context::all();
$this
->send($context)
->label('Context');
return $this;
}
/**
* @param array|string ...$keys
* @return $this
*/
public function hiddenContext(...$keys): self
{
if (! class_exists(Context::class)) {
return $this;
}
if (isset($keys[0]) && is_array($keys[0])) {
$keys = $keys[0];
}
$hiddenContext = count($keys)
? Context::onlyHidden($keys)
: Context::allHidden();
$this
->send($hiddenContext)
->label('Hidden Context');
return $this;
}
/**
* @param Model|iterable ...$model
*/
public function model(...$model): self
{
$models = [];
foreach ($model as $passedModel) {
if (is_null($passedModel)) {
$models[] = null;
continue;
}
if ($passedModel instanceof Model) {
$models[] = $passedModel;
continue;
}
if (is_iterable($model)) {
foreach ($passedModel as $item) {
$models[] = $item;
continue;
}
}
}
$payloads = array_map(function (?Model $model) {
return new ModelPayload($model);
}, $models);
foreach ($payloads as $payload) {
ray()->sendRequest($payload);
}
return $this;
}
/**
* @param Model|iterable $models
*/
public function models($models): self
{
return $this->model($models);
}
public function markdown(string $markdown): self
{
$payload = new MarkdownPayload($markdown);
$this->sendRequest($payload);
return $this;
}
/**
* @param string[]|array|null $onlyShowNames
*/
public function env(?array $onlyShowNames = null, ?string $filename = null): self
{
$filename ??= app()->environmentFilePath();
$payload = new EnvironmentPayload($onlyShowNames, $filename);
$this->sendRequest($payload);
return $this;
}
/**
* @param null $callable
* @return \Spatie\LaravelRay\Ray
*/
public function showEvents($callable = null)
{
$watcher = app(EventWatcher::class);
return $this->handleWatcherCallable($watcher, $callable);
}
public function events($callable = null)
{
return $this->showEvents($callable);
}
public function stopShowingEvents(): self
{
/** @var \Spatie\LaravelRay\Watchers\EventWatcher $eventWatcher */
$eventWatcher = app(EventWatcher::class);
$eventWatcher->disable();
return $this;
}
public function showExceptions(): self
{
/** @var \Spatie\LaravelRay\Watchers\ExceptionWatcher $exceptionWatcher */
$exceptionWatcher = app(ExceptionWatcher::class);
$exceptionWatcher->enable();
return $this;
}
public function stopShowingExceptions(): self
{
/** @var \Spatie\LaravelRay\Watchers\ExceptionWatcher $exceptionWatcher */
$exceptionWatcher = app(ExceptionWatcher::class);
$exceptionWatcher->disable();
return $this;
}
/**
* @param null $callable
* @return \Spatie\LaravelRay\Ray
*/
public function showJobs($callable = null)
{
$watcher = app(JobWatcher::class);
return $this->handleWatcherCallable($watcher, $callable);
}
/**
* @param null $callable
* @return \Spatie\LaravelRay\Ray
*/
public function showCache($callable = null)
{
$watcher = app(CacheWatcher::class);
return $this->handleWatcherCallable($watcher, $callable);
}
public function stopShowingCache(): self
{
app(CacheWatcher::class)->disable();
return $this;
}
public function jobs($callable = null)
{
return $this->showJobs($callable);
}
public function stopShowingJobs(): self
{
app(JobWatcher::class)->disable();
return $this;
}
public function view(View $view): self
{
$payload = new ViewPayload($view);
return $this->sendRequest($payload);
}
/**
* @param null $callable
* @return \Spatie\LaravelRay\Ray
*/
public function showViews($callable = null)
{
$watcher = app(ViewWatcher::class);
return $this->handleWatcherCallable($watcher, $callable);
}
public function views($callable = null)
{
return $this->showViews($callable);
}
public function stopShowingViews(): self
{
app(ViewWatcher::class)->disable();
return $this;
}
/**
* @param null $callable
* @return \Spatie\LaravelRay\Ray
*/
public function showQueries($callable = null)
{
$watcher = app(QueryWatcher::class);
return $this->handleWatcherCallable($watcher, $callable);
}
public function countQueries(callable $callable)
{
/** @var QueryWatcher $watcher */
$watcher = app(QueryWatcher::class);
$watcher->keepExecutedQueries();
if (! $watcher->enabled()) {
$watcher->doNotSendIndividualQueries();
}
$output = $this->handleWatcherCallable($watcher, $callable);
$executedQueryStatistics = collect($watcher->getExecutedQueries())
->pipe(function (Collection $queries) {
return [
'Count' => $queries->count(),
'Total time' => $queries->sum(function (QueryExecuted $query) {
return $query->time;
}),
];
});
$executedQueryStatistics['Total time'] .= ' ms';
$watcher
->stopKeepingAndClearExecutedQueries()
->sendIndividualQueries();
$this->table($executedQueryStatistics, 'Queries');
return $output;
}
public function queries($callable = null)
{
return $this->showQueries($callable);
}
public function stopShowingQueries(): self
{
app(QueryWatcher::class)->disable();
return $this;
}
public function slowQueries($milliseconds = 500, $callable = null)
{
return $this->showSlowQueries($milliseconds, $callable);
}
public function showSlowQueries($milliseconds = 500, $callable = null)
{
$watcher = app(SlowQueryWatcher::class)
->setMinimumTimeInMilliseconds($milliseconds);
return $this->handleWatcherCallable($watcher, $callable);
}
public function stopShowingSlowQueries(): self
{
app(SlowQueryWatcher::class)->disable();
return $this;
}
/**
* @param null $callable
* @return \Spatie\LaravelRay\Ray
*/
public function showDuplicateQueries($callable = null)
{
$watcher = app(DuplicateQueryWatcher::class);
return $this->handleWatcherCallable($watcher, $callable);
}
public function stopShowingDuplicateQueries(): self
{
app(DuplicateQueryWatcher::class)->disable();
return $this;
}
public function showConditionalQueries(Closure $condition, $callable = null, $name = 'default')
{
$watcher = ConditionalQueryWatcher::buildWatcherForName($condition, $name);
return $this->handleWatcherCallable($watcher, $callable);
}
public function stopShowingConditionalQueries($name = 'default'): self
{
app(ConditionalQueryWatcher::abstractName($name))->disable();
return $this;
}
public function showUpdateQueries($callable = null)
{
$watcher = app(UpdateQueryWatcher::class);
return $this->handleWatcherCallable($watcher, $callable);
}
public function stopShowingUpdateQueries(): self
{
app(UpdateQueryWatcher::class)->disable();
return $this;
}
public function showDeleteQueries($callable = null)
{
$watcher = app(DeleteQueryWatcher::class);
return $this->handleWatcherCallable($watcher, $callable);
}
public function stopShowingDeleteQueries(): self
{
app(DeleteQueryWatcher::class)->disable();
return $this;
}
public function showInsertQueries($callable = null)
{
$watcher = app(InsertQueryWatcher::class);
return $this->handleWatcherCallable($watcher, $callable);
}
public function stopShowingInsertQueries(): self
{
app(InsertQueryWatcher::class)->disable();
return $this;
}
public function showSelectQueries($callable = null)
{
$watcher = app(SelectQueryWatcher::class);
return $this->handleWatcherCallable($watcher, $callable);
}
public function stopShowingSelectQueries(): self
{
app(SelectQueryWatcher::class)->disable();
return $this;
}
/**
* @param null $callable
* @return \Spatie\LaravelRay\Ray
*/
public function showRequests($callable = null)
{
$watcher = app(RequestWatcher::class);
return $this->handleWatcherCallable($watcher, $callable);
}
public function requests($callable = null)
{
return $this->showRequests($callable);
}
public function stopShowingRequests(): self
{
$this->requestWatcher()->disable();
return $this;
}
/**
* @param null $callable
* @return \Spatie\LaravelRay\Ray
*/
public function showHttpClientRequests($callable = null)
{
if (! HttpClientWatcher::supportedByLaravelVersion()) {
$this->send('Http logging is not available in your Laravel version')->red();
return $this;
}
$watcher = app(HttpClientWatcher::class);
return $this->handleWatcherCallable($watcher, $callable);
}
public function httpClientRequests($callable = null)
{
return $this->showHttpClientRequests($callable);
}
public function stopShowingHttpClientRequests(): self
{
app(HttpClientWatcher::class)->disable();
return $this;
}
protected function handleWatcherCallable(Watcher $watcher, ?Closure $callable = null)
{
$rayProxy = new RayProxy;
$wasEnabled = $watcher->enabled();
$watcher->enable();
if ($rayProxy) {
$watcher->setRayProxy($rayProxy);
}
if ($callable) {
$output = $callable();
if (! $wasEnabled) {
$watcher->disable();
}
if ((new ReflectionFunction($callable))->hasReturnType()) {
return $output;
}
}
return $rayProxy;
}
public function testResponse(TestResponse $testResponse)
{
$payload = ResponsePayload::fromTestResponse($testResponse);
$this->sendRequest($payload);
}
protected function requestWatcher(): RequestWatcher
{
return app(RequestWatcher::class);
}
public function exception(Throwable $exception, array $meta = [])
{
$payloads[] = new ExceptionPayload($exception, $meta);
if ($exception instanceof QueryException) {
$executedQuery = new QueryExecuted($exception->getSql(), $exception->getBindings(), null, DB::connection(config('database.default')));
$payloads[] = new ExecutedQueryPayload($executedQuery);
}
$this->sendRequest($payloads)->red();
return $this;
}
/**
* @param \Spatie\Ray\Payloads\Payload|\Spatie\Ray\Payloads\Payload[] $payloads
*
* @throws \Exception
*/
public function sendRequest($payloads, array $meta = []): BaseRay
{
if (! $this->enabled()) {
return $this;
}
$meta['laravel_version'] = app()->version();
if (class_exists(InstalledVersions::class)) {
try {
$meta['laravel_ray_package_version'] = InstalledVersions::getVersion('spatie/laravel-ray');
} catch (\Exception $e) {
$meta['laravel_ray_package_version'] = '0.0.0';
}
}
return BaseRay::sendRequest($payloads, $meta);
}
}
@@ -0,0 +1,23 @@
<?php
namespace Spatie\LaravelRay;
use Spatie\Ray\Ray;
class RayProxy
{
/** @var array */
protected $methodsCalled = [];
public function __call($method, $arguments)
{
$this->methodsCalled[] = compact('method', 'arguments');
}
public function applyCalledMethods(Ray $ray)
{
foreach ($this->methodsCalled as $methodCalled) {
call_user_func_array([$ray, $methodCalled['method']], $methodCalled['arguments']);
}
}
}
@@ -0,0 +1,280 @@
<?php
namespace Spatie\LaravelRay;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\Builder;
use Illuminate\Mail\Mailable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Stringable;
use Illuminate\Testing\TestResponse;
use Illuminate\View\Compilers\BladeCompiler;
use Spatie\LaravelRay\Commands\CleanRayCommand;
use Spatie\LaravelRay\Commands\PublishConfigCommand;
use Spatie\LaravelRay\Payloads\MailablePayload;
use Spatie\LaravelRay\Payloads\ModelPayload;
use Spatie\LaravelRay\Payloads\QueryPayload;
use Spatie\LaravelRay\Watchers\ApplicationLogWatcher;
use Spatie\LaravelRay\Watchers\CacheWatcher;
use Spatie\LaravelRay\Watchers\DeleteQueryWatcher;
use Spatie\LaravelRay\Watchers\DeprecatedNoticeWatcher;
use Spatie\LaravelRay\Watchers\DumpWatcher;
use Spatie\LaravelRay\Watchers\DuplicateQueryWatcher;
use Spatie\LaravelRay\Watchers\EventWatcher;
use Spatie\LaravelRay\Watchers\ExceptionWatcher;
use Spatie\LaravelRay\Watchers\HttpClientWatcher;
use Spatie\LaravelRay\Watchers\InsertQueryWatcher;
use Spatie\LaravelRay\Watchers\JobWatcher;
use Spatie\LaravelRay\Watchers\MailWatcher;
use Spatie\LaravelRay\Watchers\QueryWatcher;
use Spatie\LaravelRay\Watchers\RequestWatcher;
use Spatie\LaravelRay\Watchers\SelectQueryWatcher;
use Spatie\LaravelRay\Watchers\SlowQueryWatcher;
use Spatie\LaravelRay\Watchers\UpdateQueryWatcher;
use Spatie\LaravelRay\Watchers\ViewWatcher;
use Spatie\Ray\Client;
use Spatie\Ray\PayloadFactory;
use Spatie\Ray\Payloads\Payload;
use Spatie\Ray\Settings\Settings;
use Spatie\Ray\Settings\SettingsFactory;
class RayServiceProvider extends ServiceProvider
{
public function register()
{
$this
->registerCommands()
->registerSettings()
->setProjectName()
->registerBindings()
->registerWatchers()
->registerMacros()
->registerBindings()
->registerBladeDirectives()
->registerPayloadFinder();
}
public function boot()
{
$this->bootWatchers();
}
protected function registerCommands(): self
{
$this->commands(PublishConfigCommand::class);
$this->commands(CleanRayCommand::class);
return $this;
}
protected function registerSettings(): self
{
$this->app->singleton(Settings::class, function ($app) {
$settings = SettingsFactory::createFromConfigFile($app->configPath());
return $settings->setDefaultSettings([
'enable' => env('RAY_ENABLED', ! app()->environment('production')),
'send_cache_to_ray' => env('SEND_CACHE_TO_RAY', false),
'send_dumps_to_ray' => env('SEND_DUMPS_TO_RAY', true),
'send_jobs_to_ray' => env('SEND_JOBS_TO_RAY', false),
'send_mails_to_ray' => env('SEND_MAILS_TO_RAY', true),
'send_log_calls_to_ray' => env('SEND_LOG_CALLS_TO_RAY', true),
'send_queries_to_ray' => env('SEND_QUERIES_TO_RAY', false),
'send_duplicate_queries_to_ray' => env('SEND_DUPLICATE_QUERIES_TO_RAY', false),
'send_slow_queries_to_ray' => env('SEND_SLOW_QUERIES_TO_RAY', false),
'send_update_queries_to_ray' => env('SEND_UPDATE_QUERIES_TO_RAY', false),
'send_insert_queries_to_ray' => env('SEND_INSERT_QUERIES_TO_RAY', false),
'send_delete_queries_to_ray' => env('SEND_DELETE_QUERIES_TO_RAY', false),
'send_select_queries_to_ray' => env('SEND_SELECT_QUERIES_TO_RAY', false),
'send_requests_to_ray' => env('SEND_REQUESTS_TO_RAY', false),
'send_http_client_requests_to_ray' => env('SEND_HTTP_CLIENT_REQUESTS_TO_RAY', false),
'send_views_to_ray' => env('SEND_VIEWS_TO_RAY', false),
'send_exceptions_to_ray' => env('SEND_EXCEPTIONS_TO_RAY', true),
'send_deprecated_notices_to_ray' => env('SEND_DEPRECATED_NOTICES_TO_RAY', false),
]);
});
return $this;
}
public function setProjectName(): self
{
if (Ray::$projectName === '') {
$projectName = config('app.name');
if ($projectName !== 'Laravel') {
ray()->project($projectName);
}
}
return $this;
}
protected function registerBindings(): self
{
$settings = app(Settings::class);
$this->app->bind(Client::class, function () use ($settings) {
return new Client($settings->port, $settings->host);
});
$this->app->bind(Ray::class, function () {
$client = app(Client::class);
$settings = app(Settings::class);
$ray = new Ray($settings, $client);
if (! $settings->enable) {
$ray->disable();
}
return $ray;
});
Payload::$originFactoryClass = OriginFactory::class;
return $this;
}
protected function registerWatchers(): self
{
$watchers = [
ExceptionWatcher::class,
MailWatcher::class,
ApplicationLogWatcher::class,
JobWatcher::class,
EventWatcher::class,
DumpWatcher::class,
QueryWatcher::class,
DuplicateQueryWatcher::class,
SlowQueryWatcher::class,
InsertQueryWatcher::class,
SelectQueryWatcher::class,
UpdateQueryWatcher::class,
DeleteQueryWatcher::class,
ViewWatcher::class,
CacheWatcher::class,
RequestWatcher::class,
HttpClientWatcher::class,
DeprecatedNoticeWatcher::class,
];
collect($watchers)
->each(function (string $watcherClass) {
$this->app->singleton($watcherClass);
});
return $this;
}
protected function bootWatchers(): self
{
$watchers = [
ExceptionWatcher::class,
MailWatcher::class,
ApplicationLogWatcher::class,
JobWatcher::class,
EventWatcher::class,
DumpWatcher::class,
QueryWatcher::class,
DuplicateQueryWatcher::class,
SlowQueryWatcher::class,
InsertQueryWatcher::class,
SelectQueryWatcher::class,
UpdateQueryWatcher::class,
DeleteQueryWatcher::class,
ViewWatcher::class,
CacheWatcher::class,
RequestWatcher::class,
HttpClientWatcher::class,
DeprecatedNoticeWatcher::class,
MailWatcher::class,
];
collect($watchers)
->each(function (string $watcherClass) {
/** @var \Spatie\LaravelRay\Watchers\Watcher $watcher */
$watcher = app($watcherClass);
$watcher->register();
});
return $this;
}
protected function registerMacros(): self
{
Collection::macro('ray', function (string $description = '') {
$description === ''
? ray($this->items)
: ray($description, $this->items);
return $this;
});
TestResponse::macro('ray', function () {
ray()->testResponse($this);
return $this;
});
Stringable::macro('ray', function (string $description = '') {
$description === ''
? ray($this->value)
: ray($description, $this->value);
return $this;
});
Builder::macro('ray', function () {
$payload = new QueryPayload($this);
ray()->sendRequest($payload);
return $this;
});
return $this;
}
protected function registerBladeDirectives(): self
{
if (! $this->app->has('blade.compiler')) {
return $this;
}
$this->callAfterResolving('blade.compiler', function (BladeCompiler $bladeCompiler) {
Blade::directive('ray', function ($expression) {
return "<?php ray($expression); ?>";
});
Blade::directive('measure', function () {
return '<?php ray()->measure() ?>';
});
Blade::directive('xray', function () {
return '<?php ray($__data)?>';
});
});
return $this;
}
protected function registerPayloadFinder(): self
{
PayloadFactory::registerPayloadFinder(function ($argument) {
if ($argument instanceof Model) {
return new ModelPayload($argument);
}
if ($argument instanceof Mailable) {
return MailablePayload::forMailable($argument);
}
return null;
});
return $this;
}
}
@@ -0,0 +1,38 @@
<?php
namespace Spatie\LaravelRay\Support;
use Closure;
use Symfony\Component\Console\Output\OutputInterface;
class Composer extends \Illuminate\Support\Composer
{
/**
* Install the given Composer packages into the application.
*
* Override this method for `illuminate/support` 10 and below.
*
* @param array<int, string> $packages
* @param string|null $composerBinary
* @return bool
*/
public function requirePackages(array $packages, bool $dev = false, Closure|OutputInterface|null $output = null, $composerBinary = null)
{
$command = collect([
...$this->findComposer($composerBinary),
'require',
...$packages,
])
->when($dev, function ($command) {
$command->push('--dev');
})->all();
return $this->getProcess($command, ['COMPOSER_MEMORY_LIMIT' => '-1'])
->run(
$output instanceof OutputInterface
? function ($type, $line) use ($output) {
$output->write(' '.$line);
} : $output
) === 0;
}
}
@@ -0,0 +1,82 @@
<?php
namespace Spatie\LaravelRay\Watchers;
use Illuminate\Log\Events\MessageLogged;
use Illuminate\Support\Facades\Event;
use Spatie\LaravelRay\Ray;
use Spatie\Ray\Payloads\ApplicationLogPayload;
class ApplicationLogWatcher extends Watcher
{
public function register(): void
{
/** @var \Spatie\LaravelRay\Ray $ray */
$ray = app(Ray::class);
$this->enabled = $ray->settings->send_log_calls_to_ray;
Event::listen(MessageLogged::class, function (MessageLogged $message) {
if (! $this->shouldLogMessage($message)) {
return;
}
if (! class_exists('Spatie\Ray\Payloads\ApplicationLogPayload')) {
return;
}
$payload = new ApplicationLogPayload($message->message, $message->context);
/** @var Ray $ray */
$ray = app(Ray::class);
$ray->sendRequest($payload);
switch ($message->level) {
case 'error':
case 'critical':
case 'alert':
case 'emergency':
$ray->color('red');
break;
case 'warning':
$ray->color('orange');
break;
}
});
}
protected function shouldLogMessage(MessageLogged $message): bool
{
if (! $this->enabled()) {
return false;
}
if (is_null($message->message)) {
return false;
}
/** @var Ray $ray */
$ray = app(Ray::class);
if (! $ray->settings->send_log_calls_to_ray) {
return false;
}
if ((new ExceptionWatcher)->concernsException($message)) {
return false;
}
if ((new MailWatcher)->concernsLoggedMail($message)) {
return false;
}
if ((new DeprecatedNoticeWatcher)->concernsDeprecatedNotice($message)) {
return false;
}
return true;
}
}
@@ -0,0 +1,83 @@
<?php
namespace Spatie\LaravelRay\Watchers;
use Illuminate\Cache\Events\CacheHit;
use Illuminate\Cache\Events\CacheMissed;
use Illuminate\Cache\Events\KeyForgotten;
use Illuminate\Cache\Events\KeyWritten;
use Spatie\LaravelRay\Payloads\CachePayload;
use Spatie\LaravelRay\Ray;
use Spatie\Ray\Settings\Settings;
class CacheWatcher extends Watcher
{
public function register(): void
{
$settings = app(Settings::class);
$this->enabled = $settings->send_cache_to_ray;
app('events')->listen(CacheHit::class, function (CacheHit $event) {
if (! $this->enabled()) {
return;
}
$payload = new CachePayload('Hit', $event->key, $event->tags, $event->value);
$ray = $this->ray()->sendRequest($payload);
optional($this->rayProxy)->applyCalledMethods($ray);
});
app('events')->listen(CacheMissed::class, function (CacheMissed $event) {
if (! $this->enabled()) {
return;
}
$payload = new CachePayload('Missed', $event->key, $event->tags);
$this->ray()->sendRequest($payload);
});
app('events')->listen(KeyWritten::class, function (KeyWritten $event) {
if (! $this->enabled()) {
return;
}
$payload = new CachePayload(
'Key written',
$event->key,
$event->tags,
$event->value,
$this->formatExpiration($event),
);
$this->ray()->sendRequest($payload);
});
app('events')->listen(KeyForgotten::class, function (KeyForgotten $event) {
if (! $this->enabled()) {
return;
}
$payload = new CachePayload(
'Key forgotten',
$event->key,
$event->tags
);
$this->ray()->sendRequest($payload);
});
}
protected function formatExpiration(KeyWritten $event): ?int
{
return $event->seconds;
}
public function ray(): Ray
{
return app(Ray::class);
}
}
@@ -0,0 +1,63 @@
<?php
namespace Spatie\LaravelRay\Watchers;
use BadMethodCallException;
use Closure;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\Event;
use Spatie\LaravelRay\Payloads\ExecutedQueryPayload;
use Spatie\LaravelRay\Ray;
class ConditionalQueryWatcher extends QueryWatcher
{
protected $conditionalCallback;
public static function buildWatcherForName(Closure $condition, $name)
{
$watcher = new static;
$watcher->setConditionalCallback($condition);
return app()->instance(static::abstractName($name), $watcher);
}
public static function abstractName(string $name)
{
return static::class.':'.$name;
}
public function setConditionalCallback($conditionalCallback)
{
$this->conditionalCallback = $conditionalCallback;
$this->listen();
}
public function register(): void
{
throw new BadMethodCallException('ConditionalQueryWatcher cannot be registered. Only its child classes.');
}
public function listen(): void
{
Event::listen(QueryExecuted::class, function (QueryExecuted $query) {
if (! $this->enabled()) {
return;
}
if (! $this->conditionalCallback) {
return;
}
$ray = app(Ray::class);
if (($this->conditionalCallback)($query)) {
$payload = new ExecutedQueryPayload($query);
$ray->sendRequest($payload);
}
optional($this->rayProxy)->applyCalledMethods($ray);
});
}
}
@@ -0,0 +1,21 @@
<?php
namespace Spatie\LaravelRay\Watchers;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Str;
use Spatie\Ray\Settings\Settings;
class DeleteQueryWatcher extends ConditionalQueryWatcher
{
public function register(): void
{
$settings = app(Settings::class);
$this->enabled = $settings->send_delete_queries_to_ray ?? false;
$this->setConditionalCallback(function (QueryExecuted $query) {
return Str::startsWith(strtolower($query->sql), 'delete');
});
}
}
@@ -0,0 +1,27 @@
<?php
namespace Spatie\LaravelRay\Watchers;
use Illuminate\Log\Events\MessageLogged;
use Illuminate\Support\Str;
use Spatie\Ray\Settings\Settings;
class DeprecatedNoticeWatcher extends Watcher
{
public function register(): void
{
//
}
public function concernsDeprecatedNotice(MessageLogged $messageLogged): bool
{
$settings = app(Settings::class);
$this->enabled = $settings->send_deprecated_notices_to_ray;
if ($this->enabled()) {
return false;
}
return Str::contains($messageLogged->message, ['deprecated', 'Deprecated', '[\ReturnTypeWillChange]']);
}
}
@@ -0,0 +1,18 @@
<?php
namespace Spatie\LaravelRay\Watchers;
use Spatie\LaravelRay\DumpRecorder\DumpRecorder;
use Spatie\Ray\Settings\Settings;
class DumpWatcher extends Watcher
{
public function register(): void
{
$settings = app(Settings::class);
$this->enabled = $settings->send_dumps_to_ray;
app(DumpRecorder::class)->register();
}
}
@@ -0,0 +1,84 @@
<?php
namespace Spatie\LaravelRay\Watchers;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Str;
use Spatie\LaravelRay\Payloads\ExecutedQueryPayload;
use Spatie\LaravelRay\Ray;
use Spatie\Ray\Settings\Settings;
class DuplicateQueryWatcher extends Watcher
{
/** @var string[] */
protected $executedQueries = [];
public function register(): void
{
$settings = app(Settings::class);
$this->enabled = $settings->send_duplicate_queries_to_ray;
Event::listen(QueryExecuted::class, function (QueryExecuted $query) {
if (! $this->enabled()) {
return;
}
$sql = Str::replaceArray('?', $this->cleanupBindings($query->bindings), $query->sql);
$duplicated = in_array($sql, $this->executedQueries);
$this->executedQueries[] = $sql;
if (! $duplicated) {
return;
}
$payload = new ExecutedQueryPayload($query);
$ray = app(Ray::class)->sendRequest($payload);
optional($this->rayProxy)->applyCalledMethods($ray);
});
}
private function cleanupBindings(array $bindings): array
{
return array_map(function ($binding) {
if ($binding instanceof \DateTimeInterface) {
return $binding->format('Y-m-d H:i:s');
}
return $binding;
}, $bindings);
}
public function enable(): Watcher
{
if (app()->bound('db')) {
collect(DB::getConnections())->each(function ($connection) {
$connection->enableQueryLog();
});
}
parent::enable();
return $this;
}
public function getExecutedQueries(): array
{
return $this->executedQueries;
}
public function disable(): Watcher
{
DB::disableQueryLog();
parent::disable();
return $this;
}
}
@@ -0,0 +1,25 @@
<?php
namespace Spatie\LaravelRay\Watchers;
use Illuminate\Support\Facades\Event;
use Spatie\LaravelRay\Payloads\EventPayload;
use Spatie\LaravelRay\Ray;
class EventWatcher extends Watcher
{
public function register(): void
{
Event::listen('*', function (string $eventName, array $arguments) {
if (! $this->enabled()) {
return;
}
$payload = new EventPayload($eventName, $arguments);
$ray = app(Ray::class)->sendRequest($payload);
optional($this->rayProxy)->applyCalledMethods($ray);
});
}
}
@@ -0,0 +1,145 @@
<?php
namespace Spatie\LaravelRay\Watchers;
use Closure;
use Exception;
use Facade\FlareClient\Flare as FacadeFlare;
use Facade\FlareClient\Truncation\ReportTrimmer as FacadeReportTrimmer;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Log\Events\MessageLogged;
use Illuminate\Support\Facades\Event;
use Spatie\FlareClient\Flare;
use Spatie\FlareClient\Truncation\ReportTrimmer;
use Spatie\LaravelRay\Ray;
use Spatie\Ray\Settings\Settings;
use Throwable;
class ExceptionWatcher extends Watcher
{
public function register(): void
{
$settings = app(Settings::class);
$this->enabled = $settings->send_exceptions_to_ray;
Event::listen(MessageLogged::class, function (MessageLogged $message) {
if (! $this->enabled()) {
return;
}
if (! $this->concernsException($message)) {
return;
}
$exception = $message->context['exception'];
$meta = [];
if ($flareReport = $this->getFlareReport($exception)) {
$meta['flare_report'] = $flareReport;
}
$exceptionContext = $this->getRequestAndRouteContext();
$meta = array_merge($meta, $exceptionContext);
/** @var Ray $ray */
$ray = app(Ray::class);
$ray->exception(
$exception,
$meta,
);
});
}
public function concernsException(MessageLogged $messageLogged): bool
{
if (! isset($messageLogged->context['exception'])) {
return false;
}
if (! $messageLogged->context['exception'] instanceof Exception) {
return false;
}
return true;
}
public function getFlareReport(Throwable $exception): ?array
{
if (app()->bound(Flare::class)) {
$flare = app(Flare::class);
$report = $flare->createReport($exception);
return (new ReportTrimmer)->trim($report->toArray());
}
if (app()->bound(FacadeFlare::class)) {
/** @var \Facade\FlareClient\Flare $flare */
$flare = app(FacadeFlare::class);
$report = $flare->createReport($exception);
return (new FacadeReportTrimmer)->trim($report->toArray());
}
return null;
}
protected function getRequestAndRouteContext(): array
{
return [
'request_headers' => $this->getRequestHeaders(),
'application_route' => $this->getApplicationRouteContext(),
'application_route_parameters' => $this->getApplicationRouteParameters(),
];
}
/**
* Get the request's headers.
*
* @return array<string, string>
*/
protected function getRequestHeaders(): array
{
return array_map(function (array $header) {
return implode(', ', $header);
}, request()->headers->all());
}
/**
* Get the application's route context.
*
* @return array<string, string>
*/
protected function getApplicationRouteContext(): array
{
$route = request()->route();
return $route ? array_filter([
'controller' => $route->getActionName(),
'route name' => $route->getName() ?: null,
'middleware' => implode(', ', array_map(function ($middleware) {
return $middleware instanceof Closure ? 'Closure' : $middleware;
}, $route->gatherMiddleware())),
]) : [];
}
/**
* Get the application's route parameters context.
*/
protected function getApplicationRouteParameters(): ?string
{
$route = request()->route();
$parameters = $route ? $route->parameters() : null;
return $parameters ? json_encode(array_map(
fn ($value) => $value instanceof Model ? $value->withoutRelations() : $value,
$parameters
), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) : null;
}
}
@@ -0,0 +1,99 @@
<?php
namespace Spatie\LaravelRay\Watchers;
use Illuminate\Http\Client\Events\RequestSending;
use Illuminate\Http\Client\Events\ResponseReceived;
use Illuminate\Http\Client\Request;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Event;
use Spatie\LaravelRay\Ray;
use Spatie\Ray\Payloads\TablePayload;
use Spatie\Ray\Settings\Settings;
class HttpClientWatcher extends Watcher
{
public function register(): void
{
if (! static::supportedByLaravelVersion()) {
return;
}
$settings = app(Settings::class);
$this->enabled = $settings->send_http_client_requests_to_ray;
Event::listen(RequestSending::class, function (RequestSending $event) {
if (! $this->enabled()) {
return;
}
$ray = $this->handleRequest($event->request);
optional($this->rayProxy)->applyCalledMethods($ray);
});
Event::listen(ResponseReceived::class, function (ResponseReceived $event) {
if (! $this->enabled()) {
return;
}
$ray = $this->handleResponse($event->request, $event->response);
optional($this->rayProxy)->applyCalledMethods($ray);
});
}
protected function handleRequest(Request $request)
{
$payload = new TablePayload([
'Method' => $request->method(),
'URL' => $request->url(),
'Headers' => $request->headers(),
'Data' => $request->data(),
'Body' => $request->body(),
'Type' => $this->getRequestType($request),
], 'Http');
return app(Ray::class)->sendRequest($payload);
}
protected function getRequestType(Request $request)
{
if ($request->isJson()) {
return 'Json';
}
if ($request->isMultipart()) {
return 'Multipart';
}
return 'Form';
}
protected function handleResponse(Request $request, Response $response)
{
$payload = new TablePayload([
'URL' => $request->url(),
'Real Request' => ! empty($response->handlerStats()),
'Success' => $response->successful(),
'Status' => $response->status(),
'Headers' => $response->headers(),
'Body' => rescue(function () use ($response) {
return $response->json();
}, $response->body(), false),
'Cookies' => $response->cookies(),
'Size' => $response->handlerStats()['size_download'] ?? null,
'Connection time' => $response->handlerStats()['connect_time'] ?? null,
'Duration' => $response->handlerStats()['total_time'] ?? null,
'Request Size' => $response->handlerStats()['request_size'] ?? null,
], 'Http');
return app(Ray::class)->sendRequest($payload);
}
public static function supportedByLaravelVersion()
{
return version_compare(app()->version(), '8.46.0', '>=');
}
}
@@ -0,0 +1,21 @@
<?php
namespace Spatie\LaravelRay\Watchers;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Str;
use Spatie\Ray\Settings\Settings;
class InsertQueryWatcher extends ConditionalQueryWatcher
{
public function register(): void
{
$settings = app(Settings::class);
$this->enabled = $settings->send_insert_queries_to_ray ?? false;
$this->setConditionalCallback(function (QueryExecuted $query) {
return Str::startsWith(strtolower($query->sql), 'insert');
});
}
}
@@ -0,0 +1,39 @@
<?php
namespace Spatie\LaravelRay\Watchers;
use Illuminate\Queue\Events\JobFailed;
use Illuminate\Queue\Events\JobProcessed;
use Illuminate\Queue\Events\JobProcessing;
use Illuminate\Queue\Events\JobQueued;
use Illuminate\Support\Facades\Event;
use Spatie\LaravelRay\Payloads\JobEventPayload;
use Spatie\LaravelRay\Ray;
use Spatie\Ray\Settings\Settings;
class JobWatcher extends Watcher
{
public function register(): void
{
$settings = app(Settings::class);
$this->enabled = $settings->send_jobs_to_ray;
Event::listen([
JobQueued::class,
JobProcessing::class,
JobProcessed::class,
JobFailed::class,
], function (object $event) {
if (! $this->enabled()) {
return;
}
$payload = new JobEventPayload($event);
$ray = app(Ray::class)->sendRequest($payload);
optional($this->rayProxy)->applyCalledMethods($ray);
});
}
}
@@ -0,0 +1,80 @@
<?php
namespace Spatie\LaravelRay\Watchers;
use Illuminate\Log\Events\MessageLogged;
use Illuminate\Mail\Events\MessageSending;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Str;
use Spatie\LaravelRay\Payloads\MailablePayload;
use Spatie\LaravelRay\Ray;
use Spatie\Ray\Settings\Settings;
class MailWatcher extends Watcher
{
public function register(): void
{
$settings = app(Settings::class);
if ($settings->send_mails_to_ray ?? true) {
$this->enable();
}
$this->supportsMessageSendingEvent()
? $this->registerMessageSendingEventListener()
: $this->listenForLoggedMails();
}
protected function registerMessageSendingEventListener(): void
{
Event::listen([
MessageSending::class,
], function (MessageSending $event) {
if (! $this->enabled()) {
return;
}
$payload = new MailablePayload($event->message->getHtmlBody() ?? $event->message->getTextBody());
$ray = app(Ray::class)->sendRequest($payload);
optional($this->rayProxy)->applyCalledMethods($ray);
});
}
public function listenForLoggedMails(): void
{
Event::listen(MessageLogged::class, function (MessageLogged $messageLogged) {
if (! $this->enabled()) {
return;
}
if (! $this->concernsLoggedMail($messageLogged)) {
return;
}
/** @var Ray $ray */
$ray = app(Ray::class);
$ray->loggedMail($messageLogged->message);
});
}
public function concernsLoggedMail(MessageLogged $messageLogged): bool
{
if (! Str::contains($messageLogged->message, 'Message-ID')) {
return false;
}
if (! Str::contains($messageLogged->message, 'To:')) {
return false;
}
return true;
}
public function supportsMessageSendingEvent(): bool
{
return version_compare(app()->version(), '11.0.0', '>=');
}
}
@@ -0,0 +1,106 @@
<?php
namespace Spatie\LaravelRay\Watchers;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Spatie\LaravelRay\Payloads\ExecutedQueryPayload;
use Spatie\LaravelRay\Ray;
use Spatie\Ray\Settings\Settings;
class QueryWatcher extends Watcher
{
/** @var QueryExecuted[] */
protected $executedQueries = [];
/** @var bool */
protected $keepExecutedQueries = false;
/** @var bool */
protected $sendIndividualQueries = true;
public function register(): void
{
$settings = app(Settings::class);
$this->enabled = $settings->send_queries_to_ray;
Event::listen(QueryExecuted::class, function (QueryExecuted $query) {
if (! $this->enabled()) {
return;
}
if ($this->keepExecutedQueries) {
$this->executedQueries[] = $query;
}
if (! $this->sendIndividualQueries) {
return;
}
$payload = new ExecutedQueryPayload($query);
$ray = app(Ray::class)->sendRequest($payload);
optional($this->rayProxy)->applyCalledMethods($ray);
});
}
public function enable(): Watcher
{
if (app()->bound('db')) {
collect(DB::getConnections())->each(function ($connection) {
$connection->enableQueryLog();
});
}
parent::enable();
return $this;
}
public function keepExecutedQueries(): self
{
$this->keepExecutedQueries = true;
return $this;
}
public function getExecutedQueries(): array
{
return $this->executedQueries;
}
public function sendIndividualQueries(): self
{
$this->sendIndividualQueries = true;
return $this;
}
public function doNotSendIndividualQueries(): self
{
$this->sendIndividualQueries = false;
return $this;
}
public function stopKeepingAndClearExecutedQueries(): self
{
$this->keepExecutedQueries = false;
$this->executedQueries = [];
return $this;
}
public function disable(): Watcher
{
DB::disableQueryLog();
parent::disable();
return $this;
}
}
@@ -0,0 +1,133 @@
<?php
namespace Spatie\LaravelRay\Watchers;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Http\Events\RequestHandled;
use Illuminate\Http\Request;
use Illuminate\Http\Response as IlluminateResponse;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Str;
use Illuminate\View\View;
use Spatie\LaravelRay\Ray;
use Spatie\Ray\Payloads\TablePayload;
use Spatie\Ray\Settings\Settings;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
class RequestWatcher extends Watcher
{
public function register(): void
{
$settings = app(Settings::class);
$this->enabled = $settings->send_requests_to_ray;
Event::listen(RequestHandled::class, function (RequestHandled $event) {
if (! $this->enabled()) {
return;
}
$ray = $this->handleRequest($event->request, $event->response);
optional($this->rayProxy)->applyCalledMethods($ray);
});
}
protected function handleRequest(Request $request, Response $response): Ray
{
$startTime = defined('LARAVEL_START')
? LARAVEL_START
: $request->server('REQUEST_TIME_FLOAT');
$headers = collect($request->headers->all())
->map(function (array $header) {
return $header[0];
})
->toArray();
$session = $request->hasSession()
? $request->session()->all()
: [];
$payload = new TablePayload([
'IP Address' => $request->ip(),
'URI' => str_replace($request->root(), '', $request->fullUrl()) ?: '/',
'Method' => $request->method(),
'Controller action' => optional($request->route())->getActionName(),
'Middleware' => array_values(optional($request->route())->gatherMiddleware() ?? []),
'Headers' => $headers,
'Payload' => $this->payload($request),
'Session' => $session,
'Response code' => $response->getStatusCode(),
'Response' => $this->response($response),
'Duration' => $startTime ? floor((microtime(true) - $startTime) * 1000) : null,
'Memory' => round(memory_get_peak_usage(true) / 1024 / 1024, 1),
], 'Request');
return app(Ray::class)->sendRequest($payload);
}
private function payload(Request $request)
{
$files = $request->files->all();
array_walk_recursive($files, function (&$file) {
$file = [
'name' => $file->getClientOriginalName(),
'size' => $file->isFile() ? ($file->getSize() / 1000).'KB' : '0',
];
});
return array_replace_recursive($request->input(), $files);
}
protected function response(Response $response)
{
$content = $response->getContent();
if (is_string($content)) {
if (is_array(json_decode($content, true)) &&
json_last_error() === JSON_ERROR_NONE) {
return json_decode($content, true);
}
if (Str::startsWith(strtolower($response->headers->get('Content-Type')), 'text/plain')) {
return $content;
}
}
if ($response instanceof RedirectResponse) {
return 'Redirected to '.$response->getTargetUrl();
}
if ($response instanceof IlluminateResponse && $response->getOriginalContent() instanceof View) {
return [
'view' => $response->getOriginalContent()->getPath(),
'data' => $this->extractDataFromView($response->getOriginalContent()),
];
}
return 'HTML Response';
}
protected function extractDataFromView($view)
{
return collect($view->getData())
->map(function ($value) {
if ($value instanceof Model) {
return $value->toArray();
}
if (is_object($value)) {
return [
'class' => get_class($value),
'properties' => json_decode(json_encode($value), true),
];
}
return json_decode(json_encode($value), true);
})
->toArray();
}
}
@@ -0,0 +1,21 @@
<?php
namespace Spatie\LaravelRay\Watchers;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Str;
use Spatie\Ray\Settings\Settings;
class SelectQueryWatcher extends ConditionalQueryWatcher
{
public function register(): void
{
$settings = app(Settings::class);
$this->enabled = $settings->send_select_queries_to_ray ?? false;
$this->setConditionalCallback(function (QueryExecuted $query) {
return Str::startsWith(strtolower($query->sql), 'select');
});
}
}
@@ -0,0 +1,30 @@
<?php
namespace Spatie\LaravelRay\Watchers;
use Illuminate\Database\Events\QueryExecuted;
use Spatie\Ray\Settings\Settings;
class SlowQueryWatcher extends ConditionalQueryWatcher
{
protected $minimumTimeInMs = 500;
public function register(): void
{
$settings = app(Settings::class);
$this->enabled = $settings->send_slow_queries_to_ray ?? false;
$this->minimumTimeInMs = $settings->slow_query_threshold_in_ms ?? $this->minimumTimeInMs;
$this->setConditionalCallback(function (QueryExecuted $query) {
return $query->time >= $this->minimumTimeInMs;
});
}
public function setMinimumTimeInMilliseconds(float $milliseconds): self
{
$this->minimumTimeInMs = $milliseconds;
return $this;
}
}
@@ -0,0 +1,21 @@
<?php
namespace Spatie\LaravelRay\Watchers;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Str;
use Spatie\Ray\Settings\Settings;
class UpdateQueryWatcher extends ConditionalQueryWatcher
{
public function register(): void
{
$settings = app(Settings::class);
$this->enabled = $settings->send_update_queries_to_ray ?? false;
$this->setConditionalCallback(function (QueryExecuted $query) {
return Str::startsWith(strtolower($query->sql), 'update');
});
}
}
@@ -0,0 +1,30 @@
<?php
namespace Spatie\LaravelRay\Watchers;
use Illuminate\Support\Facades\Event;
use Spatie\LaravelRay\Ray;
use Spatie\Ray\Settings\Settings;
class ViewWatcher extends Watcher
{
public function register(): void
{
$settings = app(Settings::class);
$this->enabled = $settings->send_views_to_ray;
Event::listen('composing:*', function ($event, $data) {
if (! $this->enabled()) {
return;
}
/** @var \Illuminate\View\View $view */
$view = $data[0];
$ray = app(Ray::class)->view($view);
optional($this->rayProxy)->applyCalledMethods($ray);
});
}
}
@@ -0,0 +1,42 @@
<?php
namespace Spatie\LaravelRay\Watchers;
use Spatie\LaravelRay\RayProxy;
abstract class Watcher
{
/** @var bool */
protected $enabled = false;
/** @var \Spatie\LaravelRay\RayProxy|null */
protected $rayProxy;
abstract public function register(): void;
public function enabled(): bool
{
return $this->enabled;
}
public function enable(): Watcher
{
$this->enabled = true;
return $this;
}
public function disable(): Watcher
{
$this->enabled = false;
return $this;
}
public function setRayProxy(RayProxy $rayProxy): Watcher
{
$this->rayProxy = $rayProxy;
return $this;
}
}
@@ -0,0 +1,133 @@
<?php
return [
/*
* This setting controls whether data should be sent to Ray.
*
* By default, `ray()` will only transmit data in non-production environments.
*/
'enable' => env('RAY_ENABLED', true),
/*
* When enabled, all cache events will automatically be sent to Ray.
*/
'send_cache_to_ray' => env('SEND_CACHE_TO_RAY', false),
/*
* When enabled, all things passed to `dump` or `dd`
* will be sent to Ray as well.
*/
'send_dumps_to_ray' => env('SEND_DUMPS_TO_RAY', true),
/*
* When enabled all job events will automatically be sent to Ray.
*/
'send_jobs_to_ray' => env('SEND_JOBS_TO_RAY', false),
/*
* When enabled all mails will automatically be sent to Ray.
*/
'send_mails_to_ray' => env('SEND_MAILS_TO_RAY', true),
/*
* When enabled, all things logged to the application log
* will be sent to Ray as well.
*/
'send_log_calls_to_ray' => env('SEND_LOG_CALLS_TO_RAY', true),
/*
* When enabled, all queries will automatically be sent to Ray.
*/
'send_queries_to_ray' => env('SEND_QUERIES_TO_RAY', false),
/**
* When enabled, all duplicate queries will automatically be sent to Ray.
*/
'send_duplicate_queries_to_ray' => env('SEND_DUPLICATE_QUERIES_TO_RAY', false),
/*
* When enabled, slow queries will automatically be sent to Ray.
*/
'send_slow_queries_to_ray' => env('SEND_SLOW_QUERIES_TO_RAY', false),
/**
* Queries that are longer than this number of milliseconds will be regarded as slow.
*/
'slow_query_threshold_in_ms' => env('RAY_SLOW_QUERY_THRESHOLD_IN_MS', 500),
/*
* When enabled, all update queries will automatically be sent to Ray.
*/
'send_update_queries_to_ray' => env('SEND_UPDATE_QUERIES_TO_RAY', false),
/*
* When enabled, all insert queries will automatically be sent to Ray.
*/
'send_insert_queries_to_ray' => env('SEND_INSERT_QUERIES_TO_RAY', false),
/*
* When enabled, all delete queries will automatically be sent to Ray.
*/
'send_delete_queries_to_ray' => env('SEND_DELETE_QUERIES_TO_RAY', false),
/*
* When enabled, all select queries will automatically be sent to Ray.
*/
'send_select_queries_to_ray' => env('SEND_SELECT_QUERIES_TO_RAY', false),
/*
* When enabled, all requests made to this app will automatically be sent to Ray.
*/
'send_requests_to_ray' => env('SEND_REQUESTS_TO_RAY', false),
/**
* When enabled, all Http Client requests made by this app will be automatically sent to Ray.
*/
'send_http_client_requests_to_ray' => env('SEND_HTTP_CLIENT_REQUESTS_TO_RAY', false),
/*
* When enabled, all views that are rendered automatically be sent to Ray.
*/
'send_views_to_ray' => env('SEND_VIEWS_TO_RAY', false),
/*
* When enabled, all exceptions will be automatically sent to Ray.
*/
'send_exceptions_to_ray' => env('SEND_EXCEPTIONS_TO_RAY', true),
/*
* When enabled, all deprecation notices will be automatically sent to Ray.
*/
'send_deprecated_notices_to_ray' => env('SEND_DEPRECATED_NOTICES_TO_RAY', false),
/*
* The host used to communicate with the Ray app.
* When using Docker on Mac or Windows, you can replace localhost with 'host.docker.internal'
* When using Docker on Linux, you can replace localhost with '172.17.0.1'
* When using Homestead with the VirtualBox provider, you can replace localhost with '10.0.2.2'
* When using Homestead with the Parallels provider, you can replace localhost with '10.211.55.2'
*/
'host' => env('RAY_HOST', 'localhost'),
/*
* The port number used to communicate with the Ray app.
*/
'port' => env('RAY_PORT', 23517),
/*
* Absolute base path for your sites or projects in Homestead,
* Vagrant, Docker, or another remote development server.
*/
'remote_path' => env('RAY_REMOTE_PATH', null),
/*
* Absolute base path for your sites or projects on your local
* computer where your IDE or code editor is running on.
*/
'local_path' => env('RAY_LOCAL_PATH', null),
/*
* When this setting is enabled, the package will not try to format values sent to Ray.
*/
'always_send_raw_values' => false,
];