clock-work

This commit is contained in:
noor
2023-04-24 17:39:09 +05:30
committed by RafficMohammed
parent cf4bec91a6
commit 1eea7ff15e
178 changed files with 13169 additions and 123 deletions

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Providers;
use Clockwork\Support\Laravel\ClockworkMiddleware;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider;
class QueryListenProvider extends ServiceProvider
{
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
DB::listen(function ($query) {
\Clockwork::info($query->sql, [$query->time]);
});
$this->app['router']->aliasMiddleware('clockwork', ClockworkMiddleware::class);
}
}

View File

@@ -35,6 +35,10 @@
{ {
"type": "vcs", "type": "vcs",
"url": "https://github.com/sandesh556/pdf-laravel5.git" "url": "https://github.com/sandesh556/pdf-laravel5.git"
},
{
"type": "vcs",
"url": "https://github.com/ladybirdweb/clockwork.git"
} }
], ],
"require": { "require": {
@@ -70,7 +74,8 @@
"unisharp/laravel-ckeditor": "dev-master", "unisharp/laravel-ckeditor": "dev-master",
"thomaswelton/laravel-gravatar": "dev-master", "thomaswelton/laravel-gravatar": "dev-master",
"symfony/mailgun-mailer": "^6.2", "symfony/mailgun-mailer": "^6.2",
"symfony/http-client": "^6.2" "symfony/http-client": "^6.2",
"itsgoingd/clockwork": "dev-master"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^9.5.10", "phpunit/phpunit": "^9.5.10",
@@ -81,9 +86,10 @@
"nunomaduro/collision": "^6.3", "nunomaduro/collision": "^6.3",
"mockery/mockery": "^1.4.4", "mockery/mockery": "^1.4.4",
"fakerphp/faker": "^1.9.1", "fakerphp/faker": "^1.9.1",
"barryvdh/laravel-debugbar": "^3.7", "barryvdh/laravel-debugbar": "^3.8",
"spatie/laravel-ignition": "^1.4", "spatie/laravel-ignition": "^1.4",
"laravel/pint": "^1.4" "laravel/pint": "^1.4",
"beyondcode/laravel-query-detector": "^1.7"
}, },
"autoload": { "autoload": {
"classmap": [ "classmap": [

158
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "de4b83b369c705c49ee38aa8f3984384", "content-hash": "ec936e44c27edb7acd9fdd54c2266a10",
"packages": [ "packages": [
{ {
"name": "aws/aws-crt-php", "name": "aws/aws-crt-php",
@@ -2558,6 +2558,73 @@
], ],
"time": "2022-05-21T17:30:32+00:00" "time": "2022-05-21T17:30:32+00:00"
}, },
{
"name": "itsgoingd/clockwork",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/ladybirdweb/clockwork.git",
"reference": "e29be8125ce999947d60986dcd8e606c04c012ab"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ladybirdweb/clockwork/zipball/e29be8125ce999947d60986dcd8e606c04c012ab",
"reference": "e29be8125ce999947d60986dcd8e606c04c012ab",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": ">=5.6"
},
"default-branch": true,
"type": "library",
"extra": {
"laravel": {
"providers": [
"Clockwork\\Support\\Laravel\\ClockworkServiceProvider"
],
"aliases": {
"Clockwork": "Clockwork\\Support\\Laravel\\Facade"
}
}
},
"autoload": {
"psr-4": {
"Clockwork\\": "Clockwork/"
}
},
"license": [
"MIT"
],
"authors": [
{
"name": "itsgoingd",
"email": "itsgoingd@luzer.sk",
"homepage": "https://twitter.com/itsgoingd"
}
],
"description": "php dev tools in your browser",
"homepage": "https://underground.works/clockwork",
"keywords": [
"debugging",
"devtools",
"laravel",
"logging",
"lumen",
"profiling",
"slim"
],
"support": {
"source": "https://github.com/ladybirdweb/clockwork/tree/master"
},
"funding": [
{
"type": "github",
"url": "https://github.com/itsgoingd"
}
],
"time": "2023-04-14T10:39:18+00:00"
},
{ {
"name": "laravel/framework", "name": "laravel/framework",
"version": "v9.52.0", "version": "v9.52.0",
@@ -10063,30 +10130,30 @@
"packages-dev": [ "packages-dev": [
{ {
"name": "barryvdh/laravel-debugbar", "name": "barryvdh/laravel-debugbar",
"version": "v3.7.0", "version": "v3.8.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/barryvdh/laravel-debugbar.git", "url": "https://github.com/barryvdh/laravel-debugbar.git",
"reference": "3372ed65e6d2039d663ed19aa699956f9d346271" "reference": "eb01216141e62433178c52b0cbdb785b45bae871"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/3372ed65e6d2039d663ed19aa699956f9d346271", "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/eb01216141e62433178c52b0cbdb785b45bae871",
"reference": "3372ed65e6d2039d663ed19aa699956f9d346271", "reference": "eb01216141e62433178c52b0cbdb785b45bae871",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"illuminate/routing": "^7|^8|^9", "illuminate/routing": "^9|^10",
"illuminate/session": "^7|^8|^9", "illuminate/session": "^9|^10",
"illuminate/support": "^7|^8|^9", "illuminate/support": "^9|^10",
"maximebf/debugbar": "^1.17.2", "maximebf/debugbar": "^1.17.2",
"php": ">=7.2.5", "php": "^8.0",
"symfony/finder": "^5|^6" "symfony/finder": "^6"
}, },
"require-dev": { "require-dev": {
"mockery/mockery": "^1.3.3", "mockery/mockery": "^1.3.3",
"orchestra/testbench-dusk": "^5|^6|^7", "orchestra/testbench-dusk": "^5|^6|^7|^8",
"phpunit/phpunit": "^8.5|^9.0", "phpunit/phpunit": "^8.5.30|^9.0",
"squizlabs/php_codesniffer": "^3.5" "squizlabs/php_codesniffer": "^3.5"
}, },
"type": "library", "type": "library",
@@ -10131,7 +10198,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/barryvdh/laravel-debugbar/issues", "issues": "https://github.com/barryvdh/laravel-debugbar/issues",
"source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.7.0" "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.8.0"
}, },
"funding": [ "funding": [
{ {
@@ -10143,7 +10210,67 @@
"type": "github" "type": "github"
} }
], ],
"time": "2022-07-11T09:26:42+00:00" "time": "2023-02-04T15:47:28+00:00"
},
{
"name": "beyondcode/laravel-query-detector",
"version": "1.7.0",
"source": {
"type": "git",
"url": "https://github.com/beyondcode/laravel-query-detector.git",
"reference": "40c7e168fcf7eeb80d8e96f7922e05ab194269c8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/beyondcode/laravel-query-detector/zipball/40c7e168fcf7eeb80d8e96f7922e05ab194269c8",
"reference": "40c7e168fcf7eeb80d8e96f7922e05ab194269c8",
"shasum": ""
},
"require": {
"illuminate/support": "^5.5 || ^6.0 || ^7.0 || ^8.0 || ^9.0|^10.0",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"laravel/legacy-factories": "^1.0",
"orchestra/testbench": "^3.0 || ^4.0 || ^5.0 || ^6.0|^8.0",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"BeyondCode\\QueryDetector\\QueryDetectorServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"BeyondCode\\QueryDetector\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Marcel Pociot",
"email": "marcel@beyondco.de",
"homepage": "https://beyondcode.de",
"role": "Developer"
}
],
"description": "Laravel N+1 Query Detector",
"homepage": "https://github.com/beyondcode/laravel-query-detector",
"keywords": [
"beyondcode",
"laravel-query-detector"
],
"support": {
"issues": "https://github.com/beyondcode/laravel-query-detector/issues",
"source": "https://github.com/beyondcode/laravel-query-detector/tree/1.7.0"
},
"time": "2023-02-15T10:37:22+00:00"
}, },
{ {
"name": "doctrine/instantiator", "name": "doctrine/instantiator",
@@ -13321,7 +13448,8 @@
"chumper/zipper": 20, "chumper/zipper": 20,
"brozot/laravel-fcm": 20, "brozot/laravel-fcm": 20,
"unisharp/laravel-ckeditor": 20, "unisharp/laravel-ckeditor": 20,
"thomaswelton/laravel-gravatar": 20 "thomaswelton/laravel-gravatar": 20,
"itsgoingd/clockwork": 20
}, },
"prefer-stable": false, "prefer-stable": false,
"prefer-lowest": false, "prefer-lowest": false,

View File

@@ -186,6 +186,8 @@ return [
\Yajra\DataTables\DataTablesServiceProvider::class, \Yajra\DataTables\DataTablesServiceProvider::class,
\Bugsnag\BugsnagLaravel\BugsnagServiceProvider::class, \Bugsnag\BugsnagLaravel\BugsnagServiceProvider::class,
Maatwebsite\Excel\ExcelServiceProvider::class, Maatwebsite\Excel\ExcelServiceProvider::class,
App\Providers\QueryListenProvider::class
], ],
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

417
config/clockwork.php Normal file
View File

@@ -0,0 +1,417 @@
<?php
return [
/*
|------------------------------------------------------------------------------------------------------------------
| Enable Clockwork
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork is enabled by default only when your application is in debug mode. Here you can explicitly enable or
| disable Clockwork. When disabled, no data is collected and the api and web ui are inactive.
|
*/
'enable' => env('CLOCKWORK_ENABLE', null),
'middlewares'=>['web','auth','roles'],
/*
|------------------------------------------------------------------------------------------------------------------
| Features
|------------------------------------------------------------------------------------------------------------------
|
| You can enable or disable various Clockwork features here. Some features have additional settings (eg. slow query
| threshold for database queries).
|
*/
'features' => [
// Cache usage stats and cache queries including results
'cache' => [
'enabled' => env('CLOCKWORK_CACHE_ENABLED', true),
// Collect cache queries
'collect_queries' => env('CLOCKWORK_CACHE_QUERIES', true),
// Collect values from cache queries (high performance impact with a very high number of queries)
'collect_values' => env('CLOCKWORK_CACHE_COLLECT_VALUES', false)
],
// Database usage stats and queries
'database' => [
'enabled' => env('CLOCKWORK_DATABASE_ENABLED', true),
// Collect database queries (high performance impact with a very high number of queries)
'collect_queries' => env('CLOCKWORK_DATABASE_COLLECT_QUERIES', true),
// Collect details of models updates (high performance impact with a lot of model updates)
'collect_models_actions' => env('CLOCKWORK_DATABASE_COLLECT_MODELS_ACTIONS', true),
// Collect details of retrieved models (very high performance impact with a lot of models retrieved)
'collect_models_retrieved' => env('CLOCKWORK_DATABASE_COLLECT_MODELS_RETRIEVED', false),
// Query execution time threshold in milliseconds after which the query will be marked as slow
'slow_threshold' => env('CLOCKWORK_DATABASE_SLOW_THRESHOLD'),
// Collect only slow database queries
'slow_only' => env('CLOCKWORK_DATABASE_SLOW_ONLY', false),
// Detect and report duplicate queries
'detect_duplicate_queries' => env('CLOCKWORK_DATABASE_DETECT_DUPLICATE_QUERIES', false)
],
// Dispatched events
'events' => [
'enabled' => env('CLOCKWORK_EVENTS_ENABLED', true),
// Ignored events (framework events are ignored by default)
'ignored_events' => [
// App\Events\UserRegistered::class,
// 'user.registered'
],
],
// Laravel log (you can still log directly to Clockwork with laravel log disabled)
'log' => [
'enabled' => env('CLOCKWORK_LOG_ENABLED', true)
],
// Sent notifications
'notifications' => [
'enabled' => env('CLOCKWORK_NOTIFICATIONS_ENABLED', true),
],
// Performance metrics
'performance' => [
// Allow collecting of client metrics. Requires separate clockwork-browser npm package.
'client_metrics' => env('CLOCKWORK_PERFORMANCE_CLIENT_METRICS', true)
],
// Dispatched queue jobs
'queue' => [
'enabled' => env('CLOCKWORK_QUEUE_ENABLED', true)
],
// Redis commands
'redis' => [
'enabled' => env('CLOCKWORK_REDIS_ENABLED', true)
],
// Routes list
'routes' => [
'enabled' => env('CLOCKWORK_ROUTES_ENABLED', false),
// Collect only routes from particular namespaces (only application routes by default)
'only_namespaces' => [ 'App' ]
],
// Rendered views
'views' => [
'enabled' => env('CLOCKWORK_VIEWS_ENABLED', true),
// Collect views including view data (high performance impact with a high number of views)
'collect_data' => env('CLOCKWORK_VIEWS_COLLECT_DATA', false),
// Use Twig profiler instead of Laravel events for apps using laravel-twigbridge (more precise, but does
// not support collecting view data)
'use_twig_profiler' => env('CLOCKWORK_VIEWS_USE_TWIG_PROFILER', false)
]
],
/*
|------------------------------------------------------------------------------------------------------------------
| Enable web UI
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork comes with a web UI accessible via http://your.app/clockwork. Here you can enable or disable this
| feature. You can also set a custom path for the web UI.
|
*/
'web' => env('CLOCKWORK_WEB', true),
/*
|------------------------------------------------------------------------------------------------------------------
| Enable toolbar
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork can show a toolbar with basic metrics on all responses. Here you can enable or disable this feature.
| Requires a separate clockwork-browser npm library.
| For installation instructions see https://underground.works/clockwork/#docs-viewing-data
|
*/
'toolbar' => env('CLOCKWORK_TOOLBAR', true),
/*
|------------------------------------------------------------------------------------------------------------------
| HTTP requests collection
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork collects data about HTTP requests to your app. Here you can choose which requests should be collected.
|
*/
'requests' => [
// With on-demand mode enabled, Clockwork will only profile requests when the browser extension is open or you
// manually pass a "clockwork-profile" cookie or get/post data key.
// Optionally you can specify a "secret" that has to be passed as the value to enable profiling.
'on_demand' => env('CLOCKWORK_REQUESTS_ON_DEMAND', false),
// Collect only errors (requests with HTTP 4xx and 5xx responses)
'errors_only' => env('CLOCKWORK_REQUESTS_ERRORS_ONLY', false),
// Response time threshold in milliseconds after which the request will be marked as slow
'slow_threshold' => env('CLOCKWORK_REQUESTS_SLOW_THRESHOLD'),
// Collect only slow requests
'slow_only' => env('CLOCKWORK_REQUESTS_SLOW_ONLY', false),
// Sample the collected requests (e.g. set to 100 to collect only 1 in 100 requests)
'sample' => env('CLOCKWORK_REQUESTS_SAMPLE', false),
// List of URIs that should not be collected
'except' => [
'/horizon/.*', // Laravel Horizon requests
'/telescope/.*', // Laravel Telescope requests
'/_debugbar/.*', // Laravel DebugBar requests
],
// List of URIs that should be collected, any other URI will not be collected if not empty
'only' => [
// '/api/.*'
],
// Don't collect OPTIONS requests, mostly used in the CSRF pre-flight requests and are rarely of interest
'except_preflight' => env('CLOCKWORK_REQUESTS_EXCEPT_PREFLIGHT', true)
],
/*
|------------------------------------------------------------------------------------------------------------------
| Artisan commands collection
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork can collect data about executed artisan commands. Here you can enable and configure which commands
| should be collected.
|
*/
'artisan' => [
// Enable or disable collection of executed Artisan commands
'collect' => env('CLOCKWORK_ARTISAN_COLLECT', false),
// List of commands that should not be collected (built-in commands are not collected by default)
'except' => [
// 'inspire'
],
// List of commands that should be collected, any other command will not be collected if not empty
'only' => [
// 'inspire'
],
// Enable or disable collection of command output
'collect_output' => env('CLOCKWORK_ARTISAN_COLLECT_OUTPUT', false),
// Enable or disable collection of built-in Laravel commands
'except_laravel_commands' => env('CLOCKWORK_ARTISAN_EXCEPT_LARAVEL_COMMANDS', true)
],
/*
|------------------------------------------------------------------------------------------------------------------
| Queue jobs collection
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork can collect data about executed queue jobs. Here you can enable and configure which queue jobs should
| be collected.
|
*/
'queue' => [
// Enable or disable collection of executed queue jobs
'collect' => env('CLOCKWORK_QUEUE_COLLECT', false),
// List of queue jobs that should not be collected
'except' => [
// App\Jobs\ExpensiveJob::class
],
// List of queue jobs that should be collected, any other queue job will not be collected if not empty
'only' => [
// App\Jobs\BuggyJob::class
]
],
/*
|------------------------------------------------------------------------------------------------------------------
| Tests collection
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork can collect data about executed tests. Here you can enable and configure which tests should be
| collected.
|
*/
'tests' => [
// Enable or disable collection of ran tests
'collect' => env('CLOCKWORK_TESTS_COLLECT', false),
// List of tests that should not be collected
'except' => [
// Tests\Unit\ExampleTest::class
]
],
/*
|------------------------------------------------------------------------------------------------------------------
| Enable data collection when Clockwork is disabled
|------------------------------------------------------------------------------------------------------------------
|
| You can enable this setting to collect data even when Clockwork is disabled, e.g. for future analysis.
|
*/
'collect_data_always' => env('CLOCKWORK_COLLECT_DATA_ALWAYS', false),
/*
|------------------------------------------------------------------------------------------------------------------
| Metadata storage
|------------------------------------------------------------------------------------------------------------------
|
| Configure how is the metadata collected by Clockwork stored. Two options are available:
| - files - A simple fast storage implementation storing data in one-per-request files.
| - sql - Stores requests in a sql database. Supports MySQL, PostgreSQL and SQLite. Requires PDO.
|
*/
'storage' => env('CLOCKWORK_STORAGE', 'files'),
// Path where the Clockwork metadata is stored
'storage_files_path' => env('CLOCKWORK_STORAGE_FILES_PATH', storage_path('clockwork')),
// Compress the metadata files using gzip, trading a little bit of performance for lower disk usage
'storage_files_compress' => env('CLOCKWORK_STORAGE_FILES_COMPRESS', false),
// SQL database to use, can be a name of database configured in database.php or a path to a SQLite file
'storage_sql_database' => env('CLOCKWORK_STORAGE_SQL_DATABASE', storage_path('clockwork.sqlite')),
// SQL table name to use, the table is automatically created and updated when needed
'storage_sql_table' => env('CLOCKWORK_STORAGE_SQL_TABLE', 'clockwork'),
// Maximum lifetime of collected metadata in minutes, older requests will automatically be deleted, false to disable
'storage_expiration' => env('CLOCKWORK_STORAGE_EXPIRATION', 60 * 24 * 7),
/*
|------------------------------------------------------------------------------------------------------------------
| Authentication
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork can be configured to require authentication before allowing access to the collected data. This might be
| useful when the application is publicly accessible. Setting to true will enable a simple authentication with a
| pre-configured password. You can also pass a class name of a custom implementation.
|
*/
'authentication' => env('CLOCKWORK_AUTHENTICATION', false),
// Password for the simple authentication
'authentication_password' => env('CLOCKWORK_AUTHENTICATION_PASSWORD', 'VerySecretPassword'),
/*
|------------------------------------------------------------------------------------------------------------------
| Stack traces collection
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork can collect stack traces for log messages and certain data like database queries. Here you can set
| whether to collect stack traces, limit the number of collected frames and set further configuration. Collecting
| long stack traces considerably increases metadata size.
|
*/
'stack_traces' => [
// Enable or disable collecting of stack traces
'enabled' => env('CLOCKWORK_STACK_TRACES_ENABLED', true),
// Limit the number of frames to be collected
'limit' => env('CLOCKWORK_STACK_TRACES_LIMIT', 10),
// List of vendor names to skip when determining caller, common vendors are automatically added
'skip_vendors' => [
// 'phpunit'
],
// List of namespaces to skip when determining caller
'skip_namespaces' => [
// 'Laravel'
],
// List of class names to skip when determining caller
'skip_classes' => [
// App\CustomLog::class
]
],
/*
|------------------------------------------------------------------------------------------------------------------
| Serialization
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork serializes the collected data to json for storage and transfer. Here you can configure certain aspects
| of serialization. Serialization has a large effect on the cpu time and memory usage.
|
*/
// Maximum depth of serialized multi-level arrays and objects
'serialization_depth' => env('CLOCKWORK_SERIALIZATION_DEPTH', 10),
// A list of classes that will never be serialized (e.g. a common service container class)
'serialization_blackbox' => [
\Illuminate\Container\Container::class,
\Illuminate\Foundation\Application::class,
\Laravel\Lumen\Application::class
],
/*
|------------------------------------------------------------------------------------------------------------------
| Register helpers
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork comes with a "clock" global helper function. You can use this helper to quickly log something and to
| access the Clockwork instance.
|
*/
'register_helpers' => env('CLOCKWORK_REGISTER_HELPERS', true),
/*
|------------------------------------------------------------------------------------------------------------------
| Send headers for AJAX request
|------------------------------------------------------------------------------------------------------------------
|
| When trying to collect data, the AJAX method can sometimes fail if it is missing required headers. For example, an
| API might require a version number using Accept headers to route the HTTP request to the correct codebase.
|
*/
'headers' => [
// 'Accept' => 'application/vnd.com.whatever.v1+json',
],
/*
|------------------------------------------------------------------------------------------------------------------
| Server timing
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork supports the W3C Server Timing specification, which allows for collecting a simple performance metrics
| in a cross-browser way. E.g. in Chrome, your app, database and timeline event timings will be shown in the Dev
| Tools network tab. This setting specifies the max number of timeline events that will be sent. Setting to false
| will disable the feature.
|
*/
'server_timing' => env('CLOCKWORK_SERVER_TIMING', 10)
];

69
config/querydetector.php Normal file
View File

@@ -0,0 +1,69 @@
<?php
return [
/*
* Enable or disable the query detection.
* If this is set to "null", the app.debug config value will be used.
*/
'enabled' => env('QUERY_DETECTOR_ENABLED', null),
/*
* Threshold level for the N+1 query detection. If a relation query will be
* executed more then this amount, the detector will notify you about it.
*/
'threshold' => (int) env('QUERY_DETECTOR_THRESHOLD', 1),
/*
* Here you can whitelist model relations.
*
* Right now, you need to define the model relation both as the class name and the attribute name on the model.
* So if an "Author" model would have a "posts" relation that points to a "Post" class, you need to add both
* the "posts" attribute and the "Post::class", since the relation can get resolved in multiple ways.
*/
'except' => [
//Author::class => [
// Post::class,
// 'posts',
//]
],
/*
* Here you can set a specific log channel to write to
* in case you are trying to isolate queries or have a lot
* going on in the laravel.log. Defaults to laravel.log though.
*/
'log_channel' => env('QUERY_DETECTOR_LOG_CHANNEL', 'daily'),
/*
* Define the output format that you want to use. Multiple classes are supported.
* Available options are:
*
* Alert:
* Displays an alert on the website
* \BeyondCode\QueryDetector\Outputs\Alert::class
*
* Console:
* Writes the N+1 queries into your browsers console log
* \BeyondCode\QueryDetector\Outputs\Console::class
*
* Clockwork: (make sure you have the itsgoingd/clockwork package installed)
* Writes the N+1 queries warnings to Clockwork log
* \BeyondCode\QueryDetector\Outputs\Clockwork::class
*
* Debugbar: (make sure you have the barryvdh/laravel-debugbar package installed)
* Writes the N+1 queries into a custom messages collector of Debugbar
* \BeyondCode\QueryDetector\Outputs\Debugbar::class
*
* JSON:
* Writes the N+1 queries into the response body of your JSON responses
* \BeyondCode\QueryDetector\Outputs\Json::class
*
* Log:
* Writes the N+1 queries into the Laravel.log file
* \BeyondCode\QueryDetector\Outputs\Log::class
*/
'output' => [
\BeyondCode\QueryDetector\Outputs\Alert::class,
\BeyondCode\QueryDetector\Outputs\Log::class,
]
];

View File

@@ -1415,6 +1415,7 @@ return [
*/ */
'error-debug' => 'Error logs and debugging', 'error-debug' => 'Error logs and debugging',
'debug-options' => 'Debugging options', 'debug-options' => 'Debugging options',
'clock-work' =>'Clock work',
'view-logs' => 'View error logs', 'view-logs' => 'View error logs',
'not-authorised-error-debug' => 'You are not authorised to access the URL', 'not-authorised-error-debug' => 'You are not authorised to access the URL',
'error-debug-settings' => 'Error and debugging settings', 'error-debug-settings' => 'Error and debugging settings',

View File

@@ -540,13 +540,27 @@
</a> </a>
</div> </div>
<div class="text-center text-sm">{!! Lang::get('lang.debug-options') !!}</div> <div class="text-center text-sm">{!! Lang::get('lang.debug-options') !!}</div>
</div></div>
@if(Config::get('app.debug'))
<div class="col-md-2 col-sm-6">
<div class="settingiconblue">
<div class="settingdivblue">
<a href="{{ url('clockwork/app') }}">
<span class="fa-stack fa-2x">
<i class="fa fa-server fa-stack-1x"></i>
</span>
</a>
</div>
<div class="text-center text-sm">{!!Lang::get('lang.clock-work')!!}</div>
</div>
</div>
@endif
</div> </div>
</div> </div>
</div> </div>
<!-- /.row -->
</div>
<!-- ./box-body -->
</div>
<div class="card card-light"> <div class="card card-light">

View File

@@ -10,17 +10,17 @@
} }
], ],
"require": { "require": {
"php": ">=7.2.5", "php": "^8.0",
"maximebf/debugbar": "^1.17.2", "maximebf/debugbar": "^1.17.2",
"illuminate/routing": "^7|^8|^9", "illuminate/routing": "^9|^10",
"illuminate/session": "^7|^8|^9", "illuminate/session": "^9|^10",
"illuminate/support": "^7|^8|^9", "illuminate/support": "^9|^10",
"symfony/finder": "^5|^6" "symfony/finder": "^6"
}, },
"require-dev": { "require-dev": {
"mockery/mockery": "^1.3.3", "mockery/mockery": "^1.3.3",
"orchestra/testbench-dusk": "^5|^6|^7", "orchestra/testbench-dusk": "^5|^6|^7|^8",
"phpunit/phpunit": "^8.5|^9.0", "phpunit/phpunit": "^8.5.30|^9.0",
"squizlabs/php_codesniffer": "^3.5" "squizlabs/php_codesniffer": "^3.5"
}, },
"autoload": { "autoload": {
@@ -52,8 +52,8 @@
} }
}, },
"scripts": { "scripts": {
"check-style": "phpcs -p --standard=PSR12 config/ src/ tests/", "check-style": "phpcs -p --standard=PSR12 config/ src/ tests/ --ignore=src/Resources/* ",
"fix-style": "phpcbf -p --standard=PSR12 config/ src/ tests/", "fix-style": "phpcbf -p --standard=PSR12 config/ src/ tests/ --ignore=src/Resources*",
"test": "phpunit" "test": "phpunit"
} }
} }

View File

@@ -92,7 +92,7 @@ return [
| Vendor files are included by default, but can be set to false. | Vendor files are included by default, but can be set to false.
| This can also be set to 'js' or 'css', to only include javascript or css vendor files. | This can also be set to 'js' or 'css', to only include javascript or css vendor files.
| Vendor files are for css: font-awesome (including fonts) and highlight.js (css files) | Vendor files are for css: font-awesome (including fonts) and highlight.js (css files)
| and for js: jquery and and highlight.js | and for js: jquery and highlight.js
| So if you want syntax highlighting, set it to true. | So if you want syntax highlighting, set it to true.
| jQuery is set to not conflict with existing jQuery scripts. | jQuery is set to not conflict with existing jQuery scripts.
| |
@@ -198,7 +198,8 @@ return [
'types' => ['SELECT'], // Deprecated setting, is always only SELECT 'types' => ['SELECT'], // Deprecated setting, is always only SELECT
], ],
'hints' => false, // Show hints for common mistakes 'hints' => false, // Show hints for common mistakes
'show_copy' => false, // Show copy button next to the query 'show_copy' => false, // Show copy button next to the query,
'slow_threshold' => false, // Only track queries that last longer than this time in ms
], ],
'mail' => [ 'mail' => [
'full_log' => false, 'full_log' => false,
@@ -206,6 +207,7 @@ return [
'views' => [ 'views' => [
'timeline' => false, // Add the views to the timeline (Experimental) 'timeline' => false, // Add the views to the timeline (Experimental)
'data' => false, //Note: Can slow down the application, because the data can be quite large.. 'data' => false, //Note: Can slow down the application, because the data can be quite large..
'exclude_paths' => [], // Add the paths which you don't want to appear in the views
], ],
'route' => [ 'route' => [
'label' => true, // show complete route on bar 'label' => true, // show complete route on bar

View File

@@ -1,4 +1,4 @@
## Laravel Debugbar ## Debugbar for Laravel
![Unit Tests](https://github.com/barryvdh/laravel-debugbar/workflows/Unit%20Tests/badge.svg) ![Unit Tests](https://github.com/barryvdh/laravel-debugbar/workflows/Unit%20Tests/badge.svg)
[![Packagist License](https://poser.pugx.org/barryvdh/laravel-debugbar/license.png)](http://choosealicense.com/licenses/mit/) [![Packagist License](https://poser.pugx.org/barryvdh/laravel-debugbar/license.png)](http://choosealicense.com/licenses/mit/)
[![Latest Stable Version](https://poser.pugx.org/barryvdh/laravel-debugbar/version.png)](https://packagist.org/packages/barryvdh/laravel-debugbar) [![Latest Stable Version](https://poser.pugx.org/barryvdh/laravel-debugbar/version.png)](https://packagist.org/packages/barryvdh/laravel-debugbar)

View File

@@ -134,7 +134,7 @@ class QueryCollector extends PDOCollector
$pdo = null; $pdo = null;
try { try {
$pdo = $connection->getPdo(); $pdo = $connection->getPdo();
} catch (\Exception $e) { } catch (\Throwable $e) {
// ignore error for non-pdo laravel drivers // ignore error for non-pdo laravel drivers
} }
$bindings = $connection->prepareBindings($bindings); $bindings = $connection->prepareBindings($bindings);
@@ -511,6 +511,41 @@ class QueryCollector extends PDOCollector
'type' => 'explain', 'type' => 'explain',
]; ];
} }
} elseif ($query['driver'] === 'sqlite') {
$vmi = '<table style="margin:-5px -11px !important;width: 100% !important">';
$vmi .= "<thead><tr>
<td>Address</td>
<td>Opcode</td>
<td>P1</td>
<td>P2</td>
<td>P3</td>
<td>P4</td>
<td>P5</td>
<td>Comment</td>
</tr></thead>";
foreach ($query['explain'] as $explain) {
$vmi .= "<tr>
<td>{$explain->addr}</td>
<td>{$explain->opcode}</td>
<td>{$explain->p1}</td>
<td>{$explain->p2}</td>
<td>{$explain->p3}</td>
<td>{$explain->p4}</td>
<td>{$explain->p5}</td>
<td>{$explain->comment}</td>
</tr>";
}
$vmi .= '</table>';
$statements[] = [
'sql' => " - EXPLAIN:",
'type' => 'explain',
'params' => [
'Virtual Machine Instructions' => $vmi,
]
];
} else { } else {
foreach ($query['explain'] as $explain) { foreach ($query['explain'] as $explain) {
$statements[] = [ $statements[] = [

View File

@@ -5,24 +5,51 @@ namespace Barryvdh\Debugbar\DataCollector;
use Barryvdh\Debugbar\DataFormatter\SimpleFormatter; use Barryvdh\Debugbar\DataFormatter\SimpleFormatter;
use DebugBar\Bridge\Twig\TwigCollector; use DebugBar\Bridge\Twig\TwigCollector;
use Illuminate\View\View; use Illuminate\View\View;
use Symfony\Component\VarDumper\Cloner\VarCloner; use InvalidArgumentException;
class ViewCollector extends TwigCollector class ViewCollector extends TwigCollector
{ {
protected $name;
protected $templates = []; protected $templates = [];
protected $collect_data; protected $collect_data;
protected $exclude_paths;
/**
* A list of known editor strings.
*
* @var array
*/
protected $editors = [
'sublime' => 'subl://open?url=file://%file&line=%line',
'textmate' => 'txmt://open?url=file://%file&line=%line',
'emacs' => 'emacs://open?url=file://%file&line=%line',
'macvim' => 'mvim://open/?url=file://%file&line=%line',
'phpstorm' => 'phpstorm://open?file=%file&line=%line',
'idea' => 'idea://open?file=%file&line=%line',
'vscode' => 'vscode://file/%file:%line',
'vscode-insiders' => 'vscode-insiders://file/%file:%line',
'vscode-remote' => 'vscode://vscode-remote/%file:%line',
'vscode-insiders-remote' => 'vscode-insiders://vscode-remote/%file:%line',
'vscodium' => 'vscodium://file/%file:%line',
'nova' => 'nova://core/open/file?filename=%file&line=%line',
'xdebug' => 'xdebug://%file@%line',
'atom' => 'atom://core/open/file?filename=%file&line=%line',
'espresso' => 'x-espresso://open?filepath=%file&lines=%line',
'netbeans' => 'netbeans://open/?f=%file:%line',
];
/** /**
* Create a ViewCollector * Create a ViewCollector
* *
* @param bool $collectData Collects view data when tru * @param bool $collectData Collects view data when tru
* @param string[] $excludePaths Paths to exclude from collection
*/ */
public function __construct($collectData = true) public function __construct($collectData = true, $excludePaths = [])
{ {
$this->setDataFormatter(new SimpleFormatter()); $this->setDataFormatter(new SimpleFormatter());
$this->collect_data = $collectData; $this->collect_data = $collectData;
$this->name = 'views';
$this->templates = []; $this->templates = [];
$this->exclude_paths = $excludePaths;
} }
public function getName() public function getName()
@@ -35,7 +62,7 @@ class ViewCollector extends TwigCollector
return [ return [
'views' => [ 'views' => [
'icon' => 'leaf', 'icon' => 'leaf',
'widget' => 'PhpDebugBar.Widgets.TemplatesWidget', 'widget' => 'PhpDebugBar.Widgets.LaravelViewTemplatesWidget',
'map' => 'views', 'map' => 'views',
'default' => '[]' 'default' => '[]'
], ],
@@ -46,6 +73,36 @@ class ViewCollector extends TwigCollector
]; ];
} }
/**
* Get the editor href for a given file and line, if available.
*
* @param string $filePath
* @param int $line
*
* @throws InvalidArgumentException If editor resolver does not return a string
*
* @return null|string
*/
protected function getEditorHref($filePath, $line)
{
if (empty(config('debugbar.editor'))) {
return null;
}
if (empty($this->editors[config('debugbar.editor')])) {
throw new InvalidArgumentException(
'Unknown editor identifier: ' . config('debugbar.editor') . '. Known editors:' .
implode(', ', array_keys($this->editors))
);
}
$filePath = $this->replaceSitesPath($filePath);
$url = str_replace(['%file', '%line'], [$filePath, $line], $this->editors[config('debugbar.editor')]);
return $url;
}
/** /**
* Add a View instance to the Collector * Add a View instance to the Collector
* *
@@ -71,6 +128,12 @@ class ViewCollector extends TwigCollector
$path = ''; $path = '';
} }
foreach ($this->exclude_paths as $excludePath) {
if (strpos($path, $excludePath) !== false) {
return;
}
}
if (!$this->collect_data) { if (!$this->collect_data) {
$params = array_keys($view->getData()); $params = array_keys($view->getData());
} else { } else {
@@ -86,6 +149,7 @@ class ViewCollector extends TwigCollector
'param_count' => count($params), 'param_count' => count($params),
'params' => $params, 'params' => $params,
'type' => $type, 'type' => $type,
'editorLink' => $this->getEditorHref($view->getPath(), 0),
]; ];
if ($this->getXdebugLink($path)) { if ($this->getXdebugLink($path)) {
@@ -104,4 +168,16 @@ class ViewCollector extends TwigCollector
'templates' => $templates, 'templates' => $templates,
]; ];
} }
/**
* Replace remote path
*
* @param string $filePath
*
* @return string
*/
protected function replaceSitesPath($filePath)
{
return str_replace(config('debugbar.remote_sites_path'), config('debugbar.local_sites_path'), $filePath);
}
} }

View File

@@ -4,6 +4,7 @@ namespace Barryvdh\Debugbar\DataFormatter;
use DebugBar\DataFormatter\DataFormatter; use DebugBar\DataFormatter\DataFormatter;
#[\AllowDynamicProperties]
class QueryFormatter extends DataFormatter class QueryFormatter extends DataFormatter
{ {
/** /**

View File

@@ -9,6 +9,7 @@ use DebugBar\DataFormatter\DataFormatter;
* *
* @see https://github.com/symfony/symfony/blob/v3.4.4/src/Symfony/Component/HttpKernel/DataCollector/Util/ValueExporter.php * @see https://github.com/symfony/symfony/blob/v3.4.4/src/Symfony/Component/HttpKernel/DataCollector/Util/ValueExporter.php
*/ */
#[\AllowDynamicProperties]
class SimpleFormatter extends DataFormatter class SimpleFormatter extends DataFormatter
{ {
/** /**

View File

@@ -23,6 +23,7 @@ class JavascriptRenderer extends BaseJavascriptRenderer
$this->cssVendors['fontawesome'] = __DIR__ . '/Resources/vendor/font-awesome/style.css'; $this->cssVendors['fontawesome'] = __DIR__ . '/Resources/vendor/font-awesome/style.css';
$this->jsFiles['laravel-sql'] = __DIR__ . '/Resources/sqlqueries/widget.js'; $this->jsFiles['laravel-sql'] = __DIR__ . '/Resources/sqlqueries/widget.js';
$this->jsFiles['laravel-cache'] = __DIR__ . '/Resources/cache/widget.js'; $this->jsFiles['laravel-cache'] = __DIR__ . '/Resources/cache/widget.js';
$this->jsFiles['laravel-view'] = __DIR__ . '/Resources/templates/widget.js';
$theme = config('debugbar.theme', 'auto'); $theme = config('debugbar.theme', 'auto');
switch ($theme) { switch ($theme) {

View File

@@ -34,6 +34,7 @@ use DebugBar\DebugBar;
use DebugBar\Storage\PdoStorage; use DebugBar\Storage\PdoStorage;
use DebugBar\Storage\RedisStorage; use DebugBar\Storage\RedisStorage;
use Exception; use Exception;
use Throwable;
use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Foundation\Application;
use Illuminate\Session\SessionManager; use Illuminate\Session\SessionManager;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@@ -204,7 +205,8 @@ class LaravelDebugbar extends DebugBar
if ($this->shouldCollect('views', true) && isset($this->app['events'])) { if ($this->shouldCollect('views', true) && isset($this->app['events'])) {
try { try {
$collectData = $this->app['config']->get('debugbar.options.views.data', true); $collectData = $this->app['config']->get('debugbar.options.views.data', true);
$this->addCollector(new ViewCollector($collectData)); $excludePaths = $this->app['config']->get('debugbar.options.views.exclude_paths', []);
$this->addCollector(new ViewCollector($collectData, $excludePaths));
$this->app['events']->listen( $this->app['events']->listen(
'composing:*', 'composing:*',
function ($view, $data = []) use ($debugbar) { function ($view, $data = []) use ($debugbar) {
@@ -258,7 +260,7 @@ class LaravelDebugbar extends DebugBar
try { try {
$logMessage = (string) $message; $logMessage = (string) $message;
if (mb_check_encoding($logMessage, 'UTF-8')) { if (mb_check_encoding($logMessage, 'UTF-8')) {
$logMessage .= (!empty($context) ? ' ' . json_encode($context) : ''); $logMessage .= (!empty($context) ? ' ' . json_encode($context, JSON_PRETTY_PRINT) : '');
} else { } else {
$logMessage = "[INVALID UTF-8 DATA]"; $logMessage = "[INVALID UTF-8 DATA]";
} }
@@ -635,7 +637,7 @@ class LaravelDebugbar extends DebugBar
/** /**
* Adds an exception to be profiled in the debug bar * Adds an exception to be profiled in the debug bar
* *
* @param Exception $e * @param Throwable $e
*/ */
public function addThrowable($e) public function addThrowable($e)
{ {

View File

@@ -24,6 +24,8 @@ div.phpdebugbar-openhandler,
div.phpdebugbar div.phpdebugbar-header > div > *, div.phpdebugbar div.phpdebugbar-header > div > *,
div.phpdebugbar ul.phpdebugbar-widgets-timeline li span.phpdebugbar-widgets-label, div.phpdebugbar ul.phpdebugbar-widgets-timeline li span.phpdebugbar-widgets-label,
div.phpdebugbar ul.phpdebugbar-widgets-timeline li span.phpdebugbar-widgets-collector, div.phpdebugbar ul.phpdebugbar-widgets-timeline li span.phpdebugbar-widgets-collector,
div.phpdebugbar ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item,
div.phpdebugbar ul.phpdebugbar-widgets-list li span.phpdebugbar-widgets-label,
div.phpdebugbar code.phpdebugbar-widgets-sql span.hljs-keyword, div.phpdebugbar code.phpdebugbar-widgets-sql span.hljs-keyword,
div.phpdebugbar-openhandler .phpdebugbar-openhandler-header, div.phpdebugbar-openhandler .phpdebugbar-openhandler-header,
div.phpdebugbar-openhandler .phpdebugbar-openhandler-header a { div.phpdebugbar-openhandler .phpdebugbar-openhandler-header a {

View File

@@ -13,6 +13,7 @@ div.phpdebugbar {
div.phpdebugbar * { div.phpdebugbar * {
direction: ltr; direction: ltr;
font-size: initial;
text-align: left; text-align: left;
} }

View File

@@ -0,0 +1,98 @@
(function($) {
var csscls = PhpDebugBar.utils.makecsscls('phpdebugbar-widgets-');
/**
* Widget for the displaying templates data
*
* Options:
* - data
*/
var TemplatesWidget = PhpDebugBar.Widgets.LaravelViewTemplatesWidget = PhpDebugBar.Widget.extend({
className: csscls('templates'),
render: function() {
this.$status = $('<div />').addClass(csscls('status')).appendTo(this.$el);
this.$list = new PhpDebugBar.Widgets.ListWidget({ itemRenderer: function(li, tpl) {
$('<span />').addClass(csscls('name')).text(tpl.name).appendTo(li);
if (typeof tpl.editorLink !== 'undefined' && tpl.editorLink !== null) {
$('<a href="' + tpl.editorLink + '"></a>')
.addClass(csscls('editor-link'))
.on('click', function (event) {
event.stopPropagation();
})
.appendTo(li);
}
if (typeof tpl.xdebug_link !== 'undefined' && tpl.xdebug_link !== null) {
if (tpl.xdebug_link.ajax) {
$('<a title="' + tpl.xdebug_link.url + '"></a>').on('click', function () {
$.ajax(tpl.xdebug_link.url);
}).addClass(csscls('editor-link')).appendTo(li);
} else {
$('<a href="' + tpl.xdebug_link.url + '"></a>').addClass(csscls('editor-link')).appendTo(li);
}
}
if (tpl.render_time_str) {
$('<span title="Render time" />').addClass(csscls('render-time')).text(tpl.render_time_str).appendTo(li);
}
if (tpl.memory_str) {
$('<span title="Memory usage" />').addClass(csscls('memory')).text(tpl.memory_str).appendTo(li);
}
if (typeof(tpl.param_count) != 'undefined') {
$('<span title="Parameter count" />').addClass(csscls('param-count')).text(tpl.param_count).appendTo(li);
}
if (typeof(tpl.type) != 'undefined' && tpl.type) {
$('<span title="Type" />').addClass(csscls('type')).text(tpl.type).appendTo(li);
}
if (tpl.params && !$.isEmptyObject(tpl.params)) {
var table = $('<table><tr><th colspan="2">Params</th></tr></table>').addClass(csscls('params')).appendTo(li);
for (var key in tpl.params) {
if (typeof tpl.params[key] !== 'function') {
table.append('<tr><td class="' + csscls('name') + '">' + key + '</td><td class="' + csscls('value') +
'"><pre><code>' + tpl.params[key] + '</code></pre></td></tr>');
}
}
li.css('cursor', 'pointer').click(function() {
if (table.is(':visible')) {
table.hide();
} else {
table.show();
}
});
}
}});
this.$list.$el.appendTo(this.$el);
this.$callgraph = $('<div />').addClass(csscls('callgraph')).appendTo(this.$el);
this.bindAttr('data', function(data) {
this.$list.set('data', data.templates);
this.$status.empty();
this.$callgraph.empty();
var sentence = data.sentence || "templates were rendered";
$('<span />').text(data.nb_templates + " " + sentence).appendTo(this.$status);
if (data.accumulated_render_time_str) {
this.$status.append($('<span title="Accumulated render time" />').addClass(csscls('render-time')).text(data.accumulated_render_time_str));
}
if (data.memory_usage_str) {
this.$status.append($('<span title="Memory usage" />').addClass(csscls('memory')).text(data.memory_usage_str));
}
if (data.nb_blocks > 0) {
$('<div />').text(data.nb_blocks + " blocks were rendered").appendTo(this.$status);
}
if (data.nb_macros > 0) {
$('<div />').text(data.nb_macros + " macros were rendered").appendTo(this.$status);
}
if (typeof data.callgraph !== 'undefined') {
this.$callgraph.html(data.callgraph);
}
});
}
});
})(PhpDebugBar.$);

View File

@@ -10,7 +10,7 @@ use Twig_SimpleFunction;
/** /**
* Access Laravels auth class in your Twig templates. * Access Laravels auth class in your Twig templates.
*/ */
class Debug extends Twig_Extension class Debug extends Extension
{ {
/** /**
* @var \Barryvdh\Debugbar\LaravelDebugbar * @var \Barryvdh\Debugbar\LaravelDebugbar
@@ -44,8 +44,15 @@ class Debug extends Twig_Extension
*/ */
public function getFunctions() public function getFunctions()
{ {
// Maintain compatibility with Twig 2 and 3.
$simpleFunction = 'Twig_SimpleFunction';
if (!class_exists($simpleFunction)) {
$simpleFunction = '\Twig\TwigFunction';
}
return [ return [
new Twig_SimpleFunction( new $simpleFunction(
'debug', 'debug',
[$this, 'debug'], [$this, 'debug'],
['needs_context' => true, 'needs_environment' => true] ['needs_context' => true, 'needs_environment' => true]
@@ -57,10 +64,10 @@ class Debug extends Twig_Extension
* Based on Twig_Extension_Debug / twig_var_dump * Based on Twig_Extension_Debug / twig_var_dump
* (c) 2011 Fabien Potencier * (c) 2011 Fabien Potencier
* *
* @param Twig_Environment $env * @param \Twig_Environment|\Twig\Environment $env
* @param $context * @param $context
*/ */
public function debug(Twig_Environment $env, $context) public function debug($env, $context)
{ {
if (!$env->isDebug() || !$this->debugbar) { if (!$env->isDebug() || !$this->debugbar) {
return; return;

View File

@@ -3,14 +3,11 @@
namespace Barryvdh\Debugbar\Twig\Extension; namespace Barryvdh\Debugbar\Twig\Extension;
use DebugBar\DataFormatter\DataFormatterInterface; use DebugBar\DataFormatter\DataFormatterInterface;
use Twig_Environment;
use Twig_Extension;
use Twig_SimpleFunction;
/** /**
* Dump variables using the DataFormatter * Dump variables using the DataFormatter
*/ */
class Dump extends Twig_Extension class Dump extends Extension
{ {
/** /**
* @var \DebugBar\DataFormatter\DataFormatter * @var \DebugBar\DataFormatter\DataFormatter
@@ -40,8 +37,15 @@ class Dump extends Twig_Extension
*/ */
public function getFunctions() public function getFunctions()
{ {
// Maintain compatibility with Twig 2 and 3.
$simpleFunction = '\Twig_SimpleFunction';
if (!class_exists($simpleFunction)) {
$simpleFunction = '\Twig\TwigFunction';
}
return [ return [
new Twig_SimpleFunction( new $simpleFunction(
'dump', 'dump',
[$this, 'dump'], [$this, 'dump'],
['is_safe' => ['html'], 'needs_context' => true, 'needs_environment' => true] ['is_safe' => ['html'], 'needs_context' => true, 'needs_environment' => true]
@@ -53,12 +57,12 @@ class Dump extends Twig_Extension
* Based on Twig_Extension_Debug / twig_var_dump * Based on Twig_Extension_Debug / twig_var_dump
* (c) 2011 Fabien Potencier * (c) 2011 Fabien Potencier
* *
* @param Twig_Environment $env * @param \Twig_Environment|\Twig\Environment $env
* @param $context * @param $context
* *
* @return string * @return string
*/ */
public function dump(Twig_Environment $env, $context) public function dump($env, $context)
{ {
$output = ''; $output = '';

View File

@@ -0,0 +1,14 @@
<?php
namespace Barryvdh\Debugbar\Twig\Extension;
// Maintain compatibility with Twig 2 and 3.
if (class_exists('\Twig_Extension')) {
abstract class Extension extends \Twig_Extension
{
}
} else {
abstract class Extension extends \Twig\Extension\AbstractExtension
{
}
}

View File

@@ -4,13 +4,12 @@ namespace Barryvdh\Debugbar\Twig\Extension;
use Barryvdh\Debugbar\Twig\TokenParser\StopwatchTokenParser; use Barryvdh\Debugbar\Twig\TokenParser\StopwatchTokenParser;
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Twig_Extension;
/** /**
* Access Laravels auth class in your Twig templates. * Access Laravels auth class in your Twig templates.
* Based on Symfony\Bridge\Twig\Extension\StopwatchExtension * Based on Symfony\Bridge\Twig\Extension\StopwatchExtension
*/ */
class Stopwatch extends Twig_Extension class Stopwatch extends Extension
{ {
/** /**
* @var \Barryvdh\Debugbar\LaravelDebugbar * @var \Barryvdh\Debugbar\LaravelDebugbar

View File

@@ -0,0 +1,14 @@
<?php
namespace Barryvdh\Debugbar\Twig\Node;
// Maintain compatibility with Twig 2 and 3.
if (class_exists('\Twig_Node')) {
abstract class Node extends \Twig_Node
{
}
} else {
abstract class Node extends \Twig\Node\Node
{
}
}

View File

@@ -7,20 +7,37 @@ namespace Barryvdh\Debugbar\Twig\Node;
* *
* @author Wouter J <wouter@wouterj.nl> * @author Wouter J <wouter@wouterj.nl>
*/ */
class StopwatchNode extends \Twig_Node class StopwatchNode extends Node
{ {
/**
* @param \Twig_NodeInterface|\Twig\Node\Node $name
* @param $body
* @param \Twig_Node_Expression_AssignName|\Twig\Node\Expression\AssignNameExpression $var
* @param $lineno
* @param $tag
*/
public function __construct( public function __construct(
\Twig_NodeInterface $name, $name,
$body, $body,
\Twig_Node_Expression_AssignName $var, $var,
$lineno = 0, $lineno = 0,
$tag = null $tag = null
) { ) {
parent::__construct(['body' => $body, 'name' => $name, 'var' => $var], [], $lineno, $tag); parent::__construct(['body' => $body, 'name' => $name, 'var' => $var], [], $lineno, $tag);
} }
public function compile(\Twig_Compiler $compiler) /**
* @param \Twig_Compiler|\Twig\Compiler $env
* @return void
*/
public function compile($compiler)
{ {
// Maintain compatibility with Twig 2 and 3.
$extension = \Barryvdh\Debugbar\Twig\Extension\Stopwatch::class;
if (class_exists('\Twig_Node')) {
$extension = 'stopwatch';
}
$compiler $compiler
->addDebugInfo($this) ->addDebugInfo($this)
->write('') ->write('')
@@ -28,11 +45,11 @@ class StopwatchNode extends \Twig_Node
->raw(' = ') ->raw(' = ')
->subcompile($this->getNode('name')) ->subcompile($this->getNode('name'))
->write(";\n") ->write(";\n")
->write("\$this->env->getExtension('stopwatch')->getDebugbar()->startMeasure(") ->write(sprintf("\$this->env->getExtension('%s')->getDebugbar()->startMeasure(", $extension))
->subcompile($this->getNode('var')) ->subcompile($this->getNode('var'))
->raw(");\n") ->raw(");\n")
->subcompile($this->getNode('body')) ->subcompile($this->getNode('body'))
->write("\$this->env->getExtension('stopwatch')->getDebugbar()->stopMeasure(") ->write(sprintf("\$this->env->getExtension('%s')->getDebugbar()->stopMeasure(", $extension))
->subcompile($this->getNode('var')) ->subcompile($this->getNode('var'))
->raw(");\n"); ->raw(");\n");
} }

View File

@@ -9,7 +9,7 @@ use Barryvdh\Debugbar\Twig\Node\StopwatchNode;
* *
* @author Wouter J <wouter@wouterj.nl> * @author Wouter J <wouter@wouterj.nl>
*/ */
class StopwatchTokenParser extends \Twig_TokenParser class StopwatchTokenParser extends TokenParser
{ {
protected $debugbarAvailable; protected $debugbarAvailable;
@@ -18,7 +18,10 @@ class StopwatchTokenParser extends \Twig_TokenParser
$this->debugbarAvailable = $debugbarAvailable; $this->debugbarAvailable = $debugbarAvailable;
} }
public function parse(\Twig_Token $token) /**
* @param \Twig_Token|\Twig\Token $token
*/
public function parse($token)
{ {
$lineno = $token->getLine(); $lineno = $token->getLine();
$stream = $this->parser->getStream(); $stream = $this->parser->getStream();
@@ -26,17 +29,31 @@ class StopwatchTokenParser extends \Twig_TokenParser
// {% stopwatch 'bar' %} // {% stopwatch 'bar' %}
$name = $this->parser->getExpressionParser()->parseExpression(); $name = $this->parser->getExpressionParser()->parseExpression();
$stream->expect(\Twig_Token::BLOCK_END_TYPE); // Maintain compatibility with Twig 2 and 3.
if (class_exists("\Twig_Token")) {
$blockEndType = \Twig_Token::BLOCK_END_TYPE;
} else {
$blockEndType = \Twig\Token::BLOCK_END_TYPE;
}
$stream->expect($blockEndType);
// {% endstopwatch %} // {% endstopwatch %}
$body = $this->parser->subparse([$this, 'decideStopwatchEnd'], true); $body = $this->parser->subparse([$this, 'decideStopwatchEnd'], true);
$stream->expect(\Twig_Token::BLOCK_END_TYPE); $stream->expect($blockEndType);
// Maintain compatibility with Twig 2 and 3.
if (class_exists("\Twig_Node_Expression_AssignName")) {
$assignNameExpression = new \Twig_Node_Expression_AssignName($this->parser->getVarName(), $token->getLine());
} else {
$assignNameExpression = new \Twig\Node\Expression\AssignNameExpression($this->parser->getVarName(), $token->getLine());
}
if ($this->debugbarAvailable) { if ($this->debugbarAvailable) {
return new StopwatchNode( return new StopwatchNode(
$name, $name,
$body, $body,
new \Twig_Node_Expression_AssignName($this->parser->getVarName(), $token->getLine()), $assignNameExpression,
$lineno, $lineno,
$this->getTag() $this->getTag()
); );
@@ -50,7 +67,10 @@ class StopwatchTokenParser extends \Twig_TokenParser
return 'stopwatch'; return 'stopwatch';
} }
public function decideStopwatchEnd(\Twig_Token $token) /**
* @param \Twig_Token|\Twig\Token $token
*/
public function decideStopwatchEnd($token)
{ {
return $token->test('endstopwatch'); return $token->test('endstopwatch');
} }

View File

@@ -0,0 +1,14 @@
<?php
namespace Barryvdh\Debugbar\Twig\TokenParser;
// Maintain compatibility with Twig 2 and 3.
if (class_exists('\Twig_TokenParser')) {
abstract class TokenParser extends \Twig_TokenParser
{
}
} else {
abstract class TokenParser extends \Twig\TokenParser\AbstractTokenParser
{
}
}

View File

@@ -0,0 +1,4 @@
preset: laravel
disabled:
- single_class_element_per_statement

View File

@@ -0,0 +1,7 @@
# Changelog
All notable changes to `laravel-query-detector` will be documented in this file
## 1.0.0 - 201X-XX-XX
- initial release

View File

@@ -0,0 +1,55 @@
# Contributing
Contributions are **welcome** and will be fully **credited**.
Please read and understand the contribution guide before creating an issue or pull request.
## Etiquette
This project is open source, and as such, the maintainers give their free time to build and maintain the source code
held within. They make the code freely available in the hope that it will be of use to other developers. It would be
extremely unfair for them to suffer abuse or anger for their hard work.
Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the
world that developers are civilized and selfless people.
It's the duty of the maintainer to ensure that all submissions to the project are of sufficient
quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used.
## Viability
When requesting or submitting new features, first consider whether it might be useful to others. Open
source projects are used by many developers, who may have entirely different needs to your own. Think about
whether or not your feature is likely to be used by other users of the project.
## Procedure
Before filing an issue:
- Attempt to replicate the problem, to ensure that it wasn't a coincidental incident.
- Check to make sure your feature suggestion isn't already present within the project.
- Check the pull requests tab to ensure that the bug doesn't have a fix in progress.
- Check the pull requests tab to ensure that the feature isn't already in progress.
Before submitting a pull request:
- Check the codebase to ensure that your feature doesn't already exist.
- Check the pull requests to ensure that another person hasn't already submitted the feature or fix.
## Requirements
If the project maintainer has any additional requirements, you will find them listed here.
- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer).
- **Add tests!** - Your patch won't be accepted if it doesn't have tests.
- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date.
- **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option.
- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting.
**Happy coding**!

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Beyond Code GmbH <hello@beyondco.de>
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.

View File

@@ -0,0 +1,53 @@
# Laravel N+1 Query Detector
[![Latest Version on Packagist](https://img.shields.io/packagist/v/beyondcode/laravel-query-detector.svg?style=flat-square)](https://packagist.org/packages/beyondcode/laravel-query-detector)
[![Build Status](https://img.shields.io/travis/beyondcode/laravel-query-detector/master.svg?style=flat-square)](https://travis-ci.org/beyondcode/laravel-query-detector)
[![Quality Score](https://img.shields.io/scrutinizer/g/beyondcode/laravel-query-detector.svg?style=flat-square)](https://scrutinizer-ci.com/g/beyondcode/laravel-query-detector)
[![Total Downloads](https://img.shields.io/packagist/dt/beyondcode/laravel-query-detector.svg?style=flat-square)](https://packagist.org/packages/beyondcode/laravel-query-detector)
The Laravel N+1 query detector helps you to increase your application's performance by reducing the number of queries it executes. This package monitors your queries in real-time, while you develop your application and notify you when you should add eager loading (N+1 queries).
![Example alert](https://beyondco.de/github/n+1/alert.png)
## Installation
You can install the package via composer:
```bash
composer require beyondcode/laravel-query-detector --dev
```
The package will automatically register itself.
## Documentation
You can find the documentation on our [website](http://beyondco.de/docs/laravel-query-detector).
### Testing
``` bash
composer test
```
### Changelog
Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently.
## Contributing
Please see [CONTRIBUTING](CONTRIBUTING.md) for details.
### Security
If you discover any security related issues, please email marcel@beyondco.de instead of using the issue tracker.
## Credits
- [Marcel Pociot](https://github.com/mpociot)
- [All Contributors](../../contributors)
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

View File

@@ -0,0 +1,51 @@
{
"name": "beyondcode/laravel-query-detector",
"description": "Laravel N+1 Query Detector",
"keywords": [
"beyondcode",
"laravel-query-detector"
],
"homepage": "https://github.com/beyondcode/laravel-query-detector",
"license": "MIT",
"authors": [
{
"name": "Marcel Pociot",
"email": "marcel@beyondco.de",
"homepage": "https://beyondcode.de",
"role": "Developer"
}
],
"require": {
"php": "^7.1 || ^8.0",
"illuminate/support": "^5.5 || ^6.0 || ^7.0 || ^8.0 || ^9.0|^10.0"
},
"require-dev": {
"laravel/legacy-factories": "^1.0",
"orchestra/testbench": "^3.0 || ^4.0 || ^5.0 || ^6.0|^8.0",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
},
"autoload": {
"psr-4": {
"BeyondCode\\QueryDetector\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"BeyondCode\\QueryDetector\\Tests\\": "tests"
}
},
"scripts": {
"test": "vendor/bin/phpunit",
"test-coverage": "vendor/bin/phpunit --coverage-html coverage"
},
"config": {
"sort-packages": true
},
"extra": {
"laravel": {
"providers": [
"BeyondCode\\QueryDetector\\QueryDetectorServiceProvider"
]
}
}
}

View File

@@ -0,0 +1,69 @@
<?php
return [
/*
* Enable or disable the query detection.
* If this is set to "null", the app.debug config value will be used.
*/
'enabled' => env('QUERY_DETECTOR_ENABLED', null),
/*
* Threshold level for the N+1 query detection. If a relation query will be
* executed more then this amount, the detector will notify you about it.
*/
'threshold' => (int) env('QUERY_DETECTOR_THRESHOLD', 1),
/*
* Here you can whitelist model relations.
*
* Right now, you need to define the model relation both as the class name and the attribute name on the model.
* So if an "Author" model would have a "posts" relation that points to a "Post" class, you need to add both
* the "posts" attribute and the "Post::class", since the relation can get resolved in multiple ways.
*/
'except' => [
//Author::class => [
// Post::class,
// 'posts',
//]
],
/*
* Here you can set a specific log channel to write to
* in case you are trying to isolate queries or have a lot
* going on in the laravel.log. Defaults to laravel.log though.
*/
'log_channel' => env('QUERY_DETECTOR_LOG_CHANNEL', 'daily'),
/*
* Define the output format that you want to use. Multiple classes are supported.
* Available options are:
*
* Alert:
* Displays an alert on the website
* \BeyondCode\QueryDetector\Outputs\Alert::class
*
* Console:
* Writes the N+1 queries into your browsers console log
* \BeyondCode\QueryDetector\Outputs\Console::class
*
* Clockwork: (make sure you have the itsgoingd/clockwork package installed)
* Writes the N+1 queries warnings to Clockwork log
* \BeyondCode\QueryDetector\Outputs\Clockwork::class
*
* Debugbar: (make sure you have the barryvdh/laravel-debugbar package installed)
* Writes the N+1 queries into a custom messages collector of Debugbar
* \BeyondCode\QueryDetector\Outputs\Debugbar::class
*
* JSON:
* Writes the N+1 queries into the response body of your JSON responses
* \BeyondCode\QueryDetector\Outputs\Json::class
*
* Log:
* Writes the N+1 queries into the Laravel.log file
* \BeyondCode\QueryDetector\Outputs\Log::class
*/
'output' => [
\BeyondCode\QueryDetector\Outputs\Alert::class,
\BeyondCode\QueryDetector\Outputs\Log::class,
]
];

View File

@@ -0,0 +1,4 @@
---
packageName: Laravel Query Detector
githubUrl: https://github.com/beyondcode/laravel-query-detector
---

View File

@@ -0,0 +1,17 @@
---
title: Installation
order: 1
---
# Laravel N+1 Query Detector
The Laravel N+1 query detector helps you to increase your application's performance by reducing the number of queries it executes. This package monitors your queries in real-time, while you develop your application and notify you when you should add eager loading (N+1 queries).
# Installation
You can install the package via composer:
```
composer require beyondcode/laravel-query-detector --dev
```
The package will automatically register itself.

View File

@@ -0,0 +1,92 @@
---
title: Usage
order: 2
---
## Usage
If you run your application in the `debug` mode, the query monitor will be automatically active. So there is nothing you have to do.
By default, this package will display an `alert()` message to notify you about an N+1 query found in the current request.
If you rather want this information to be written to your `laravel.log` file, written to your browser's console log as a warning or listed in a new tab for the [Laravel Debugbar (barryvdh/laravel-debugbar)](https://github.com/barryvdh/laravel-debugbar), you can publish the configuration and change the output behaviour (see example below).
You can publish the package's configuration using this command:
```bash
php artisan vendor:publish --provider="BeyondCode\QueryDetector\QueryDetectorServiceProvider"
```
This will add the `querydetector.php` file in your config directory with the following contents:
```php
return [
/*
* Enable or disable the query detection.
* If this is set to "null", the app.debug config value will be used.
*/
'enabled' => env('QUERY_DETECTOR_ENABLED', null),
/*
* Threshold level for the N+1 query detection. If a relation query will be
* executed more then this amount, the detector will notify you about it.
*/
'threshold' => (int) env('QUERY_DETECTOR_THRESHOLD', 1),
/*
* Here you can whitelist model relations.
*
* Right now, you need to define the model relation both as the class name and the attribute name on the model.
* So if an "Author" model would have a "posts" relation that points to a "Post" class, you need to add both
* the "posts" attribute and the "Post::class", since the relation can get resolved in multiple ways.
*/
'except' => [
//Author::class => [
// Post::class,
// 'posts',
//]
],
/*
* Define the output format that you want to use. Multiple classes are supported.
* Available options are:
*
* Alert:
* Displays an alert on the website
* \BeyondCode\QueryDetector\Outputs\Alert::class
*
* Console:
* Writes the N+1 queries into your browsers console log
* \BeyondCode\QueryDetector\Outputs\Console::class
*
* Clockwork: (make sure you have the itsgoingd/clockwork package installed)
* Writes the N+1 queries warnings to Clockwork log
* \BeyondCode\QueryDetector\Outputs\Clockwork::class
*
* Debugbar: (make sure you have the barryvdh/laravel-debugbar package installed)
* Writes the N+1 queries into a custom messages collector of Debugbar
* \BeyondCode\QueryDetector\Outputs\Debugbar::class
*
* JSON:
* Writes the N+1 queries into the response body of your JSON responses
* \BeyondCode\QueryDetector\Outputs\Json::class
*
* Log:
* Writes the N+1 queries into the Laravel.log file
* \BeyondCode\QueryDetector\Outputs\Log::class
*/
'output' => [
\BeyondCode\QueryDetector\Outputs\Log::class,
\BeyondCode\QueryDetector\Outputs\Alert::class,
]
];
```
If you use **Lumen**, you need to copy the config file manually and register the Lumen Service Provider in `bootstrap/app.php` file
```php
$app->register(\BeyondCode\QueryDetector\LumenQueryDetectorServiceProvider::class);
```
If you need additional logic to run when the package detects unoptimized queries, you can listen to the `\BeyondCode\QueryDetector\Events\QueryDetected` event and write a listener to run your own handler. (e.g. send warning to Sentry/Bugsnag, send Slack notification, etc.)

View File

@@ -0,0 +1,26 @@
<?php
namespace BeyondCode\QueryDetector\Events;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
class QueryDetected {
use SerializesModels;
/** @var Collection */
protected $queries;
public function __construct(Collection $queries)
{
$this->queries = $queries;
}
/**
* @return Collection
*/
public function getQueries()
{
return $this->queries;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace BeyondCode\QueryDetector;
use Illuminate\Support\ServiceProvider;
class LumenQueryDetectorServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->configure('querydetector');
$this->mergeConfigFrom(__DIR__ . '/../config/config.php', 'querydetector');
$this->app->middleware([
QueryDetectorMiddleware::class
]);
$this->app->singleton(QueryDetector::class);
$this->app->alias(QueryDetector::class, 'querydetector');
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace BeyondCode\QueryDetector\Outputs;
use Illuminate\Support\Collection;
use Symfony\Component\HttpFoundation\Response;
class Alert implements Output
{
public function boot()
{
//
}
public function output(Collection $detectedQueries, Response $response)
{
if (stripos($response->headers->get('Content-Type'), 'text/html') !== 0 || $response->isRedirection()) {
return;
}
$content = $response->getContent();
$outputContent = $this->getOutputContent($detectedQueries);
$pos = strripos($content, '</body>');
if (false !== $pos) {
$content = substr($content, 0, $pos) . $outputContent . substr($content, $pos);
} else {
$content = $content . $outputContent;
}
// Update the new content and reset the content length
$response->setContent($content);
$response->headers->remove('Content-Length');
}
protected function getOutputContent(Collection $detectedQueries)
{
$output = '<script type="text/javascript">';
$output .= "alert('Found the following N+1 queries in this request:\\n\\n";
foreach ($detectedQueries as $detectedQuery) {
$output .= "Model: ".addslashes($detectedQuery['model'])." => Relation: ".addslashes($detectedQuery['relation']);
$output .= " - You should add \"with(\'".addslashes($detectedQuery['relation'])."\')\" to eager-load this relation.";
$output .= "\\n";
}
$output .= "')";
$output .= '</script>';
return $output;
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace BeyondCode\QueryDetector\Outputs;
use Illuminate\Support\Collection;
use Symfony\Component\HttpFoundation\Response;
class Clockwork implements Output
{
public function boot()
{
//
}
public function output(Collection $detectedQueries, Response $response)
{
clock()->warning("{$detectedQueries->count()} N+1 queries detected:", $detectedQueries->toArray());
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace BeyondCode\QueryDetector\Outputs;
use Illuminate\Support\Collection;
use Symfony\Component\HttpFoundation\Response;
class Console implements Output
{
public function boot()
{
//
}
public function output(Collection $detectedQueries, Response $response)
{
if (stripos($response->headers->get('Content-Type'), 'text/html') !== 0 || $response->isRedirection()) {
return;
}
$content = $response->getContent();
$outputContent = $this->getOutputContent($detectedQueries);
$pos = strripos($content, '</body>');
if (false !== $pos) {
$content = substr($content, 0, $pos) . $outputContent . substr($content, $pos);
} else {
$content = $content . $outputContent;
}
// Update the new content and reset the content length
$response->setContent($content);
$response->headers->remove('Content-Length');
}
protected function getOutputContent(Collection $detectedQueries)
{
$output = '<script type="text/javascript">';
$output .= "console.warn('Found the following N+1 queries in this request:\\n\\n";
foreach ($detectedQueries as $detectedQuery) {
$output .= "Model: ".addslashes($detectedQuery['model'])." => Relation: ".addslashes($detectedQuery['relation']);
$output .= " - You should add \"with(\'".$detectedQuery['relation']."\')\" to eager-load this relation.";
$output .= "\\n\\n";
$output .= "Model: ".addslashes($detectedQuery['model'])."\\n";
$output .= "Relation: ".$detectedQuery['relation']."\\n";
$output .= "Num-Called: ".$detectedQuery['count']."\\n";
$output .= "\\n";
$output .= 'Call-Stack:\\n';
foreach ($detectedQuery['sources'] as $source) {
$output .= "#$source->index $source->name:$source->line\\n";
}
}
$output .= "')";
$output .= '</script>';
return $output;
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace BeyondCode\QueryDetector\Outputs;
use Illuminate\Support\Collection;
use Symfony\Component\HttpFoundation\Response;
use Barryvdh\Debugbar\Facade as LaravelDebugbar;
use DebugBar\DataCollector\MessagesCollector;
class Debugbar implements Output
{
protected $collector;
public function boot()
{
$this->collector = new MessagesCollector('N+1 Queries');
if (!LaravelDebugbar::hasCollector($this->collector->getName())) {
LaravelDebugbar::addCollector($this->collector);
}
}
public function output(Collection $detectedQueries, Response $response)
{
foreach ($detectedQueries as $detectedQuery) {
$this->collector->addMessage(sprintf('Model: %s => Relation: %s - You should add `with(%s)` to eager-load this relation.',
$detectedQuery['model'],
$detectedQuery['relation'],
$detectedQuery['relation']
));
}
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace BeyondCode\QueryDetector\Outputs;
use Illuminate\Support\Collection;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Http\JsonResponse;
class Json implements Output
{
public function boot()
{
//
}
public function output(Collection $detectedQueries, Response $response)
{
if ($response instanceof JsonResponse) {
$data = $response->getData(true);
if (! is_array($data)){
$data = [ $data ];
}
$data['warning_queries'] = $detectedQueries;
$response->setData($data);
}
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace BeyondCode\QueryDetector\Outputs;
use Illuminate\Support\Facades\Log as LaravelLog;
use Illuminate\Support\Collection;
use Symfony\Component\HttpFoundation\Response;
class Log implements Output
{
public function boot()
{
//
}
public function output(Collection $detectedQueries, Response $response)
{
$this->log('Detected N+1 Query');
foreach ($detectedQueries as $detectedQuery) {
$logOutput = 'Model: '.$detectedQuery['model'] . PHP_EOL;
$logOutput .= 'Relation: '.$detectedQuery['relation'] . PHP_EOL;
$logOutput .= 'Num-Called: '.$detectedQuery['count'] . PHP_EOL;
$logOutput .= 'Call-Stack:' . PHP_EOL;
foreach ($detectedQuery['sources'] as $source) {
$logOutput .= '#'.$source->index.' '.$source->name.':'.$source->line . PHP_EOL;
}
$this->log($logOutput);
}
}
private function log(string $message)
{
LaravelLog::channel(config('querydetector.log_channel'))->info($message);
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace BeyondCode\QueryDetector\Outputs;
use Illuminate\Support\Collection;
use Symfony\Component\HttpFoundation\Response;
interface Output
{
public function boot();
public function output(Collection $detectedQueries, Response $response);
}

View File

@@ -0,0 +1,216 @@
<?php
namespace BeyondCode\QueryDetector;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Database\Eloquent\Builder;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Database\Eloquent\Relations\Relation;
use BeyondCode\QueryDetector\Events\QueryDetected;
class QueryDetector
{
/** @var Collection */
private $queries;
public function __construct()
{
$this->queries = Collection::make();
}
public function boot()
{
DB::listen(function($query) {
$backtrace = collect(debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 50));
$this->logQuery($query, $backtrace);
});
foreach ($this->getOutputTypes() as $outputType) {
app()->singleton($outputType);
app($outputType)->boot();
}
}
public function isEnabled(): bool
{
$configEnabled = value(config('querydetector.enabled'));
if ($configEnabled === null) {
$configEnabled = config('app.debug');
}
return $configEnabled;
}
public function logQuery($query, Collection $backtrace)
{
$modelTrace = $backtrace->first(function ($trace) {
return Arr::get($trace, 'object') instanceof Builder;
});
// The query is coming from an Eloquent model
if (! is_null($modelTrace)) {
/*
* Relations get resolved by either calling the "getRelationValue" method on the model,
* or if the class itself is a Relation.
*/
$relation = $backtrace->first(function ($trace) {
return Arr::get($trace, 'function') === 'getRelationValue' || Arr::get($trace, 'class') === Relation::class ;
});
// We try to access a relation
if (is_array($relation) && isset($relation['object'])) {
if ($relation['class'] === Relation::class) {
$model = get_class($relation['object']->getParent());
$relationName = get_class($relation['object']->getRelated());
$relatedModel = $relationName;
} else {
$model = get_class($relation['object']);
$relationName = $relation['args'][0];
$relatedModel = $relationName;
}
$sources = $this->findSource($backtrace);
$key = md5($query->sql . $model . $relationName . $sources[0]->name . $sources[0]->line);
$count = Arr::get($this->queries, $key.'.count', 0);
$time = Arr::get($this->queries, $key.'.time', 0);
$this->queries[$key] = [
'count' => ++$count,
'time' => $time + $query->time,
'query' => $query->sql,
'model' => $model,
'relatedModel' => $relatedModel,
'relation' => $relationName,
'sources' => $sources
];
}
}
}
protected function findSource($stack)
{
$sources = [];
foreach ($stack as $index => $trace) {
$sources[] = $this->parseTrace($index, $trace);
}
return array_values(array_filter($sources));
}
public function parseTrace($index, array $trace)
{
$frame = (object) [
'index' => $index,
'name' => null,
'line' => isset($trace['line']) ? $trace['line'] : '?',
];
if (isset($trace['class']) &&
isset($trace['file']) &&
!$this->fileIsInExcludedPath($trace['file'])
) {
$frame->name = $this->normalizeFilename($trace['file']);
return $frame;
}
return false;
}
/**
* Check if the given file is to be excluded from analysis
*
* @param string $file
* @return bool
*/
protected function fileIsInExcludedPath($file)
{
$excludedPaths = [
'/vendor/laravel/framework/src/Illuminate/Database',
'/vendor/laravel/framework/src/Illuminate/Events',
];
$normalizedPath = str_replace('\\', '/', $file);
foreach ($excludedPaths as $excludedPath) {
if (strpos($normalizedPath, $excludedPath) !== false) {
return true;
}
}
return false;
}
/**
* Shorten the path by removing the relative links and base dir
*
* @param string $path
* @return string
*/
protected function normalizeFilename($path): string
{
if (file_exists($path)) {
$path = realpath($path);
}
return str_replace(base_path(), '', $path);
}
public function getDetectedQueries(): Collection
{
$exceptions = config('querydetector.except', []);
$queries = $this->queries
->values();
foreach ($exceptions as $parentModel => $relations) {
foreach ($relations as $relation) {
$queries = $queries->reject(function ($query) use ($relation, $parentModel) {
return $query['model'] === $parentModel && $query['relatedModel'] === $relation;
});
}
}
$queries = $queries->where('count', '>', config('querydetector.threshold', 1))->values();
if ($queries->isNotEmpty()) {
event(new QueryDetected($queries));
}
return $queries;
}
protected function getOutputTypes()
{
$outputTypes = config('querydetector.output');
if (! is_array($outputTypes)) {
$outputTypes = [$outputTypes];
}
return $outputTypes;
}
protected function applyOutput(Response $response)
{
foreach ($this->getOutputTypes() as $type) {
app($type)->output($this->getDetectedQueries(), $response);
}
}
public function output($request, $response)
{
if ($this->getDetectedQueries()->isNotEmpty()) {
$this->applyOutput($response);
}
return $response;
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace BeyondCode\QueryDetector;
use Closure;
class QueryDetectorMiddleware
{
/** @var QueryDetector */
private $detector;
public function __construct(QueryDetector $detector)
{
$this->detector = $detector;
}
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if (! $this->detector->isEnabled()) {
return $next($request);
}
$this->detector->boot();
/** @var \Illuminate\Http\Response $response */
$response = $next($request);
// Modify the response to add the Debugbar
$this->detector->output($request, $response);
return $response;
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace BeyondCode\QueryDetector;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Support\ServiceProvider;
class QueryDetectorServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*/
public function boot()
{
if ($this->app->runningInConsole()) {
$this->publishes([
__DIR__.'/../config/config.php' => config_path('querydetector.php'),
], 'config');
}
$this->registerMiddleware(QueryDetectorMiddleware::class);
}
/**
* Register the application services.
*/
public function register()
{
$this->app->singleton(QueryDetector::class);
$this->app->alias(QueryDetector::class, 'querydetector');
$this->mergeConfigFrom(__DIR__.'/../config/config.php', 'querydetector');
}
/**
* Register the middleware
*
* @param string $middleware
*/
protected function registerMiddleware($middleware)
{
$kernel = $this->app[Kernel::class];
$kernel->pushMiddleware($middleware);
}
}

View File

@@ -42,9 +42,6 @@ namespace Composer\Autoload;
*/ */
class ClassLoader class ClassLoader
{ {
/** @var \Closure(string):void */
private static $includeFile;
/** @var ?string */ /** @var ?string */
private $vendorDir; private $vendorDir;
@@ -109,7 +106,6 @@ class ClassLoader
public function __construct($vendorDir = null) public function __construct($vendorDir = null)
{ {
$this->vendorDir = $vendorDir; $this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
} }
/** /**
@@ -429,7 +425,7 @@ class ClassLoader
public function loadClass($class) public function loadClass($class)
{ {
if ($file = $this->findFile($class)) { if ($file = $this->findFile($class)) {
(self::$includeFile)($file); includeFile($file);
return true; return true;
} }
@@ -559,23 +555,18 @@ class ClassLoader
return false; return false;
} }
}
private static function initializeIncludeClosure(): void
{ /**
if (self::$includeFile !== null) { * Scope isolated include.
return; *
} * Prevents access to $this/self from included files.
*
/** * @param string $file
* Scope isolated include. * @return void
* * @private
* Prevents access to $this/self from included files. */
* function includeFile($file)
* @param string $file {
* @return void include $file;
*/
self::$includeFile = static function($file) {
include $file;
};
}
} }

View File

@@ -146,12 +146,14 @@ return array(
'Composer\\CaBundle\\' => array($vendorDir . '/composer/ca-bundle/src'), 'Composer\\CaBundle\\' => array($vendorDir . '/composer/ca-bundle/src'),
'Complex\\' => array($vendorDir . '/markbaker/complex/classes/src'), 'Complex\\' => array($vendorDir . '/markbaker/complex/classes/src'),
'Collective\\Html\\' => array($vendorDir . '/laravelcollective/html/src'), 'Collective\\Html\\' => array($vendorDir . '/laravelcollective/html/src'),
'Clockwork\\' => array($vendorDir . '/itsgoingd/clockwork/Clockwork'),
'Chumper\\Zipper\\' => array($vendorDir . '/chumper/zipper/src/Chumper/Zipper'), 'Chumper\\Zipper\\' => array($vendorDir . '/chumper/zipper/src/Chumper/Zipper'),
'Carbon\\' => array($vendorDir . '/nesbot/carbon/src/Carbon'), 'Carbon\\' => array($vendorDir . '/nesbot/carbon/src/Carbon'),
'Bugsnag\\PsrLogger\\' => array($vendorDir . '/bugsnag/bugsnag-psr-logger/src'), 'Bugsnag\\PsrLogger\\' => array($vendorDir . '/bugsnag/bugsnag-psr-logger/src'),
'Bugsnag\\BugsnagLaravel\\' => array($vendorDir . '/bugsnag/bugsnag-laravel/src'), 'Bugsnag\\BugsnagLaravel\\' => array($vendorDir . '/bugsnag/bugsnag-laravel/src'),
'Bugsnag\\' => array($vendorDir . '/bugsnag/bugsnag/src'), 'Bugsnag\\' => array($vendorDir . '/bugsnag/bugsnag/src'),
'Brick\\Math\\' => array($vendorDir . '/brick/math/src'), 'Brick\\Math\\' => array($vendorDir . '/brick/math/src'),
'BeyondCode\\QueryDetector\\' => array($vendorDir . '/beyondcode/laravel-query-detector/src'),
'Barryvdh\\Debugbar\\' => array($vendorDir . '/barryvdh/laravel-debugbar/src'), 'Barryvdh\\Debugbar\\' => array($vendorDir . '/barryvdh/laravel-debugbar/src'),
'Aws\\' => array($vendorDir . '/aws/aws-sdk-php/src'), 'Aws\\' => array($vendorDir . '/aws/aws-sdk-php/src'),
'App\\' => array($baseDir . '/app', $vendorDir . '/laravel/pint/app'), 'App\\' => array($baseDir . '/app', $vendorDir . '/laravel/pint/app'),

View File

@@ -33,18 +33,25 @@ class ComposerAutoloaderInit10c1836cea18dd9470bc2e97275d9d56
$loader->register(true); $loader->register(true);
$filesToLoad = \Composer\Autoload\ComposerStaticInit10c1836cea18dd9470bc2e97275d9d56::$files; $includeFiles = \Composer\Autoload\ComposerStaticInit10c1836cea18dd9470bc2e97275d9d56::$files;
$requireFile = static function ($fileIdentifier, $file) { foreach ($includeFiles as $fileIdentifier => $file) {
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { composerRequire10c1836cea18dd9470bc2e97275d9d56($fileIdentifier, $file);
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
};
foreach ($filesToLoad as $fileIdentifier => $file) {
($requireFile)($fileIdentifier, $file);
} }
return $loader; return $loader;
} }
} }
/**
* @param string $fileIdentifier
* @param string $file
* @return void
*/
function composerRequire10c1836cea18dd9470bc2e97275d9d56($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}

View File

@@ -250,6 +250,7 @@ class ComposerStaticInit10c1836cea18dd9470bc2e97275d9d56
'Composer\\CaBundle\\' => 18, 'Composer\\CaBundle\\' => 18,
'Complex\\' => 8, 'Complex\\' => 8,
'Collective\\Html\\' => 16, 'Collective\\Html\\' => 16,
'Clockwork\\' => 10,
'Chumper\\Zipper\\' => 15, 'Chumper\\Zipper\\' => 15,
'Carbon\\' => 7, 'Carbon\\' => 7,
), ),
@@ -259,6 +260,7 @@ class ComposerStaticInit10c1836cea18dd9470bc2e97275d9d56
'Bugsnag\\BugsnagLaravel\\' => 23, 'Bugsnag\\BugsnagLaravel\\' => 23,
'Bugsnag\\' => 8, 'Bugsnag\\' => 8,
'Brick\\Math\\' => 11, 'Brick\\Math\\' => 11,
'BeyondCode\\QueryDetector\\' => 25,
'Barryvdh\\Debugbar\\' => 18, 'Barryvdh\\Debugbar\\' => 18,
), ),
'A' => 'A' =>
@@ -836,6 +838,10 @@ class ComposerStaticInit10c1836cea18dd9470bc2e97275d9d56
array ( array (
0 => __DIR__ . '/..' . '/laravelcollective/html/src', 0 => __DIR__ . '/..' . '/laravelcollective/html/src',
), ),
'Clockwork\\' =>
array (
0 => __DIR__ . '/..' . '/itsgoingd/clockwork/Clockwork',
),
'Chumper\\Zipper\\' => 'Chumper\\Zipper\\' =>
array ( array (
0 => __DIR__ . '/..' . '/chumper/zipper/src/Chumper/Zipper', 0 => __DIR__ . '/..' . '/chumper/zipper/src/Chumper/Zipper',
@@ -860,6 +866,10 @@ class ComposerStaticInit10c1836cea18dd9470bc2e97275d9d56
array ( array (
0 => __DIR__ . '/..' . '/brick/math/src', 0 => __DIR__ . '/..' . '/brick/math/src',
), ),
'BeyondCode\\QueryDetector\\' =>
array (
0 => __DIR__ . '/..' . '/beyondcode/laravel-query-detector/src',
),
'Barryvdh\\Debugbar\\' => 'Barryvdh\\Debugbar\\' =>
array ( array (
0 => __DIR__ . '/..' . '/barryvdh/laravel-debugbar/src', 0 => __DIR__ . '/..' . '/barryvdh/laravel-debugbar/src',

View File

@@ -152,34 +152,34 @@
}, },
{ {
"name": "barryvdh/laravel-debugbar", "name": "barryvdh/laravel-debugbar",
"version": "v3.7.0", "version": "v3.8.0",
"version_normalized": "3.7.0.0", "version_normalized": "3.8.0.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/barryvdh/laravel-debugbar.git", "url": "https://github.com/barryvdh/laravel-debugbar.git",
"reference": "3372ed65e6d2039d663ed19aa699956f9d346271" "reference": "eb01216141e62433178c52b0cbdb785b45bae871"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/3372ed65e6d2039d663ed19aa699956f9d346271", "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/eb01216141e62433178c52b0cbdb785b45bae871",
"reference": "3372ed65e6d2039d663ed19aa699956f9d346271", "reference": "eb01216141e62433178c52b0cbdb785b45bae871",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"illuminate/routing": "^7|^8|^9", "illuminate/routing": "^9|^10",
"illuminate/session": "^7|^8|^9", "illuminate/session": "^9|^10",
"illuminate/support": "^7|^8|^9", "illuminate/support": "^9|^10",
"maximebf/debugbar": "^1.17.2", "maximebf/debugbar": "^1.17.2",
"php": ">=7.2.5", "php": "^8.0",
"symfony/finder": "^5|^6" "symfony/finder": "^6"
}, },
"require-dev": { "require-dev": {
"mockery/mockery": "^1.3.3", "mockery/mockery": "^1.3.3",
"orchestra/testbench-dusk": "^5|^6|^7", "orchestra/testbench-dusk": "^5|^6|^7|^8",
"phpunit/phpunit": "^8.5|^9.0", "phpunit/phpunit": "^8.5.30|^9.0",
"squizlabs/php_codesniffer": "^3.5" "squizlabs/php_codesniffer": "^3.5"
}, },
"time": "2022-07-11T09:26:42+00:00", "time": "2023-02-04T15:47:28+00:00",
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
@@ -223,7 +223,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/barryvdh/laravel-debugbar/issues", "issues": "https://github.com/barryvdh/laravel-debugbar/issues",
"source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.7.0" "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.8.0"
}, },
"funding": [ "funding": [
{ {
@@ -237,6 +237,69 @@
], ],
"install-path": "../barryvdh/laravel-debugbar" "install-path": "../barryvdh/laravel-debugbar"
}, },
{
"name": "beyondcode/laravel-query-detector",
"version": "1.7.0",
"version_normalized": "1.7.0.0",
"source": {
"type": "git",
"url": "https://github.com/beyondcode/laravel-query-detector.git",
"reference": "40c7e168fcf7eeb80d8e96f7922e05ab194269c8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/beyondcode/laravel-query-detector/zipball/40c7e168fcf7eeb80d8e96f7922e05ab194269c8",
"reference": "40c7e168fcf7eeb80d8e96f7922e05ab194269c8",
"shasum": ""
},
"require": {
"illuminate/support": "^5.5 || ^6.0 || ^7.0 || ^8.0 || ^9.0|^10.0",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"laravel/legacy-factories": "^1.0",
"orchestra/testbench": "^3.0 || ^4.0 || ^5.0 || ^6.0|^8.0",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
},
"time": "2023-02-15T10:37:22+00:00",
"type": "library",
"extra": {
"laravel": {
"providers": [
"BeyondCode\\QueryDetector\\QueryDetectorServiceProvider"
]
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"BeyondCode\\QueryDetector\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Marcel Pociot",
"email": "marcel@beyondco.de",
"homepage": "https://beyondcode.de",
"role": "Developer"
}
],
"description": "Laravel N+1 Query Detector",
"homepage": "https://github.com/beyondcode/laravel-query-detector",
"keywords": [
"beyondcode",
"laravel-query-detector"
],
"support": {
"issues": "https://github.com/beyondcode/laravel-query-detector/issues",
"source": "https://github.com/beyondcode/laravel-query-detector/tree/1.7.0"
},
"install-path": "../beyondcode/laravel-query-detector"
},
{ {
"name": "brick/math", "name": "brick/math",
"version": "0.10.2", "version": "0.10.2",
@@ -3019,6 +3082,76 @@
], ],
"install-path": "../intervention/image" "install-path": "../intervention/image"
}, },
{
"name": "itsgoingd/clockwork",
"version": "dev-master",
"version_normalized": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/ladybirdweb/clockwork.git",
"reference": "e29be8125ce999947d60986dcd8e606c04c012ab"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ladybirdweb/clockwork/zipball/e29be8125ce999947d60986dcd8e606c04c012ab",
"reference": "e29be8125ce999947d60986dcd8e606c04c012ab",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": ">=5.6"
},
"time": "2023-04-14T10:39:18+00:00",
"default-branch": true,
"type": "library",
"extra": {
"laravel": {
"providers": [
"Clockwork\\Support\\Laravel\\ClockworkServiceProvider"
],
"aliases": {
"Clockwork": "Clockwork\\Support\\Laravel\\Facade"
}
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Clockwork\\": "Clockwork/"
}
},
"license": [
"MIT"
],
"authors": [
{
"name": "itsgoingd",
"email": "itsgoingd@luzer.sk",
"homepage": "https://twitter.com/itsgoingd"
}
],
"description": "php dev tools in your browser",
"homepage": "https://underground.works/clockwork",
"keywords": [
"debugging",
"devtools",
"laravel",
"logging",
"lumen",
"profiling",
"slim"
],
"support": {
"source": "https://github.com/ladybirdweb/clockwork/tree/master"
},
"funding": [
{
"type": "github",
"url": "https://github.com/itsgoingd"
}
],
"install-path": "../itsgoingd/clockwork"
},
{ {
"name": "laravel/dusk", "name": "laravel/dusk",
"version": "v6.25.2", "version": "v6.25.2",
@@ -13877,6 +14010,7 @@
"dev": true, "dev": true,
"dev-package-names": [ "dev-package-names": [
"barryvdh/laravel-debugbar", "barryvdh/laravel-debugbar",
"beyondcode/laravel-query-detector",
"doctrine/instantiator", "doctrine/instantiator",
"fakerphp/faker", "fakerphp/faker",
"filp/whoops", "filp/whoops",

View File

@@ -3,7 +3,7 @@
'name' => 'laravel/laravel', 'name' => 'laravel/laravel',
'pretty_version' => 'dev-master', 'pretty_version' => 'dev-master',
'version' => 'dev-master', 'version' => 'dev-master',
'reference' => '2381fd7cf5b871b9db0df601f4fdd55c221c4f20', 'reference' => 'c25c1d94b8fa38df50e8d839730bf32cb534b8cb',
'type' => 'project', 'type' => 'project',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),
@@ -29,14 +29,23 @@
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'barryvdh/laravel-debugbar' => array( 'barryvdh/laravel-debugbar' => array(
'pretty_version' => 'v3.7.0', 'pretty_version' => 'v3.8.0',
'version' => '3.7.0.0', 'version' => '3.8.0.0',
'reference' => '3372ed65e6d2039d663ed19aa699956f9d346271', 'reference' => 'eb01216141e62433178c52b0cbdb785b45bae871',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../barryvdh/laravel-debugbar', 'install_path' => __DIR__ . '/../barryvdh/laravel-debugbar',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => true, 'dev_requirement' => true,
), ),
'beyondcode/laravel-query-detector' => array(
'pretty_version' => '1.7.0',
'version' => '1.7.0.0',
'reference' => '40c7e168fcf7eeb80d8e96f7922e05ab194269c8',
'type' => 'library',
'install_path' => __DIR__ . '/../beyondcode/laravel-query-detector',
'aliases' => array(),
'dev_requirement' => true,
),
'brick/math' => array( 'brick/math' => array(
'pretty_version' => '0.10.2', 'pretty_version' => '0.10.2',
'version' => '0.10.2.0', 'version' => '0.10.2.0',
@@ -588,6 +597,17 @@
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'itsgoingd/clockwork' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => 'e29be8125ce999947d60986dcd8e606c04c012ab',
'type' => 'library',
'install_path' => __DIR__ . '/../itsgoingd/clockwork',
'aliases' => array(
0 => '9999999-dev',
),
'dev_requirement' => false,
),
'kodova/hamcrest-php' => array( 'kodova/hamcrest-php' => array(
'dev_requirement' => true, 'dev_requirement' => true,
'replaced' => array( 'replaced' => array(
@@ -615,7 +635,7 @@
'laravel/laravel' => array( 'laravel/laravel' => array(
'pretty_version' => 'dev-master', 'pretty_version' => 'dev-master',
'version' => 'dev-master', 'version' => 'dev-master',
'reference' => '2381fd7cf5b871b9db0df601f4fdd55c221c4f20', 'reference' => 'c25c1d94b8fa38df50e8d839730bf32cb534b8cb',
'type' => 'project', 'type' => 'project',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),
@@ -1247,8 +1267,8 @@
'psr/log-implementation' => array( 'psr/log-implementation' => array(
'dev_requirement' => false, 'dev_requirement' => false,
'provided' => array( 'provided' => array(
0 => '1.0|2.0|3.0', 0 => '1.0.0 || 2.0.0 || 3.0.0',
1 => '1.0.0 || 2.0.0 || 3.0.0', 1 => '1.0|2.0|3.0',
), ),
), ),
'psr/simple-cache' => array( 'psr/simple-cache' => array(

View File

@@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = tab
[*.php]
insert_final_newline = true

View File

@@ -0,0 +1 @@
.github/ export-ignore

706
vendor/itsgoingd/clockwork/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,706 @@
5.1.12
- improved Timeline event run method to stop the event in case of an exception (implemented by UlrichEckhardt, thanks!)
- fixed some deprecation warnings on PHP 8.2 (implemented by faytekin, thanks!)
- fixed some deprecation warnings on PHP 8.1 (implemented by villermen, thanks!)
5.1.11
- fixed crash when resolving authenticated user in Laravel without using Eloquent (reported by m-thalmann-athesia, thanks!)
5.1.10
- fixed crash when resolving authenticated user in Laravel (reported by LucaRed, thanks!)
5.1.9
- added support for Eloquent strict mode (reported by Sergiobop, thanks!)
5.1.8
- updated list of built-in Laravel commands to ignore when collecting commands and included Horizon commands
- fixed collecting of Laravel queue jobs when used with Horizon
- fixed collecting of authanticated user name when the User model includes name() method (implemented by devfrey, thanks!)
5.1.7
- added support for authentiaction in the Vanilla integration
- added support for compressed Xdebug profiles
- improved collecting of Laravel Artisan commands to support abbreviated commands (implemented by mike-peters90, thanks!)
- fixed doubled backslashes in collected Laravel database query bindings (reported by pys1992, thanks!)
- fixed compatibility with PostgreSQL in SQL storage (implemented by screw, thanks!)
- fixed possible crash during file storage cleanup when used with Laravel Octane (reported by flexchar, thanks!)
- fixed infinite loop when collecting queries in Doctrine 3.x (reported by N-M, thanks!)
5.1.6
- added Monolog 2.x compatible handler (idea by mahagr, thanks!)
- improved log to handle all Throwable classes as exceptions (idea by EdmondDantes, thanks!)
- fixed support for capturing console output in Laravel 9 (reported by mikerockett, thanks!)
5.1.5
- removed support for psr/log
- fixed some typos (implemented by fridzema, thanks!)
*BREAKING*
- `Clockwork\Request\Log` no longer implements the PSR log interface, it is unlikely you are using this class directly
5.1.4
- added Laravel 9 support
- added support for manually registering Clockwork middleware in Laravel
- fixed some failing tests might not been collected in Laravel (reported by ajcastro, thanks!)
- fixed not respecting the collect tests setting in Laravel (reported by SimBig, thanks!)
- fixed some deprecation warnings on PHP 8.1 (implemented by usernotnull, thanks!)
5.1.3
- added PSR to the default filtered namespaces from stack traces in the Laravel integration
- fixed not being able to log non-string values when using psr/log >=2.0 (reported by Wit3, thanks!)
- fixed some deprecation warnings on PHP 8.1 (reported by Pinnokkio, thanks!)
- fixed wrong redirect when accessing web ui with an url including a trailing slash (implemented by ssnepenthe, thanks!)
- fixed update-token could be leaked via the Clockwork rest api (implemented by ssnepenthe, thanks!)
5.1.2
- fixed some deprecation warnings on PHP 8.1 (reported by Codomic, thanks!)
5.1.1
- added support for psr/log 2.0 (used in recent Laravel versions) (implemented by byGits, thanks!)
- improved timeline api event run method to return the return value of passed closure
- improved collecting Laravel database queries to not quote integers (implemented by thisiskj, thanks!)
- improved toolbar details link to always be absolute and work with subdirectories (reported by superDuperCyberTechno, thanks!)
- fixed some deprecation warnings on PHP 8.1 (implemented by gharlan, thanks!)
- fixed collecting Laravel database queries to produce correct queries when bindings contain question marks (reported by woshixiaobai, thanks!)
- fixed filtering collected and recorded requests by closure (implemented by ssnepenthe, thanks!)
- fixed some inconsistencies in the Clockwork metadata api
- fixed some web UI assets being server with wrong mime-types (implemented by ssnepenthe, thanks!)
- fixed missing method on storage interface and missing default parameter value in sql storage (implemented by ssnepenthe, thanks!)
*BREAKING*
- timeline api event run method now returns the return value of passed closure instead of the event instance
5.1
- added initial support for Laravel Octane
- added support for Web UI in the vanilla integration
- added support for collecting Laravel cache queries without values (implemented by akalongman, thanks!)
- added ability to filter Laravel routes from particular namespaces (idea by hailwood, thanks!)
- improved collecting of request URL to include full URL including the query string
- improved Clockwork Browser payload to include Web UI path
- updated Clockwork App (5.1)
- fixed logging falsy values via Clockwork::log (reported by Karmalakas, thanks!)
- fixed PHP 8 incompatibility when processing some Laravel notifications (implemented by nanaya, thanks!)
- fixed request body being collected even when already parsed into POST data
- fixed collecting request URLs with non-standard ports
5.0.8
- fixed crash when collecting Laravel mailables built via MailMessage (implemented by cbl, thanks!)
- fixed crash when collecting artisan command in Lumen (reported by 2Attack, thanks!)
- fixed crash when collecting database queries in Laravel with connection implementation not using PDO (implemented by lenssoft, thanks!)
- fixed crash when HTTP request body contains valid json which does not contain array (eg. a number) (reported by Mradxz, thanks!)
- fixed collected jobs dispatched from other jobs not having a correct parent job set (implemented by josvar, thanks!)
5.0.7
- changed delay listening to events until the app is booted (improves comatibility with some other packages)
- changed default settings to enable toolbar (separately installed component)
- changed default except requests filter to include debugbar api (implemented by edgardmessias, thanks!)
- fixed wrong type-hint for the timeline event run method (reported by hferradj, thanks!)
- fixed on-demand mode not working in Laravel (reported by yemenifree, thanks!)
- fixed crash when collecting Laravel notifications with recipient names (reported by iainheng, thanks!)
- fixed possible crashes and other issues when collecting Laravel notifications (reported by beekmanbv, thanks!)
- fixed crash when creating runnable queries in DBAL data source (implemented by N-M, thanks!)
5.0.6
- fixed vanilla integration overriding other cookies when used with a PSR-7 response (reported by leemason, thanks!)
5.0.5
- added support for toolbar in the vanilla integration (idea by reeslo, thanks!)
- added support for client metrics in the vanilla integration
- improved PSR-7 support in the vanilla integration
- fixed toolbar might not work when not collecting database models
- fixed crash collecting Slack and Nexmo notifications (reported by abalozz, thanks!)
- fixed timeline api usage not being updated in the Slim integration leading to crash (reported by jiaojie1989, implemented by seanhamlin, thanks!)
- fixed api path being interpreted as regex in the vanilla integration (implemented by pqr, thanks!)
- fixed Symfony storage not being updated for latest storage api (implemented by auchanhub, thanks!)
5.0.4
- fixed Lumen integration crash (implemented by alexbegoon, thanks!)
- fixed PHP 5.6 incompatibility (implemented by sanis, thanks!)
5.0.3
- fixed PHP 8.0 incompatibility in log (implemented by mtorromeo, thanks!)
5.0.2
- fixed data sources not being initialized for extended data requests (reported by tmishutin, thanks!)
- fixed inconsistent handling of time and duration arguments in various Request::add* methods (reported by mahagr, thanks!)
- updated Clockwork App (5.0.2)
5.0.1
- fixed performance issues related to collecting stack traces for Eloquent models actions (reported by mstaack, thanks!)
- fixed collecting database and unsupported Laravel notifications (implemented by YannikFirre, thanks!)
- fixed log and timeline sorting leading to invalid metadata format
- updated Clockwork App (5.0.1)
5.0
- added collecting of client-metrics and web-vitals
- added collecting of Eloquent models actions and retrieved, created, updated and deleted models counts
- added collecting of Laravel notifications
- added reworked timeline api
- added configurable web ui path (default changed to /clockwork)
- added toolbar support
- added on-demand mode (with optional secret)
- added option to collect error requests only (requests with 4xx and 5xx responses)
- added option to specify slow threshold and collect slow requests only
- added option to sample collected requests (collect only 1 in x requests)
- added option to collect only specified urls
- added option to not collect pre-flight requests (enabled by default)
- added option to filter collected and recorded requests by closure
- added Laravel controller timeline event
- added support for updating existing requests
- added Slim 4 support
- updated to Clockwork App 5.0
- improved reworked the central Clockwork class api
- improved requests recording to use a terminate callback
- improved global log instance to live on the request instance
- improved global timeline instance to live on the request instance
- improved Symfony routes registration to register web ui paths only when enabled
- improved SQL storage to be more compatible with different PDO error modes
- improved Clockwork rest api with only/except filters
- improved handling of corrupted index records in file storage
- improved cleaned up the code-base, added and improved comments, use modern php features
- removed Laravel total, initialization, boot and run timeline events
- removed legacy clockwork.controller events
- removed duplicate file/line information from collected metadata
- fixed authentication route not being registered when web ui is disabled
- fixed database queries not being collected for queue jobs
- fixed multi-line database queries not being counted properly (implemented by edgardmessias, thanks!)
- fixed StackFrame not processing Windows paths correctly
*BREAKING*
- multiple changes to the Laravel config file, please review and re-publish
- minimal required PHP version is now 5.6 (previously 5.5)
- the timeline api was reworked, please see documentation for details
- the global log instance was moved to request instance, please see documentation for details
- the central Clockwork class api was reworked, old api is available but deprecated
- changed Slim middleware namespaces
4.1.8
- fixed handling of index file locking failures in file storage (reported by mahagr, thanks!)
4.1.7
- fixed a rare crash in Eloquent duplicate queries detection (reported by mstaack, thanks!)
- fixed code-style in the Laravel config (implemented by fgilio, thanks!)
4.1.6
- added support for filtering collected requests by method to Laravel integration (options requests filtered by default) (idea by mortenscheel, thanks!)
- added support for filtering collected requests by uri and method to vanilla integration
- fixed handling of failed file operations on index file in file storage (reported by staabm, thanks!)
4.1.5
- fixed crash on initialization in Lumen apps using queue (reported by gramparallelo, thanks!)
4.1.4
- added support for a time property to the Request:add* apis, defaults to "current time - duration"
- fixed crash when collecting console commands with array arguments or options in the Laravel integration (implemented by mortenscheel, thanks!)
- fixed default storage directory being one level too deep in vanilla integration
4.1.3
- fixed file storage not unlocking index when cleanup has nothing to clean (implemented by Nacoma, thanks!)
4.1.2
- fixed interaction when making HTTP requests in feature tests when collecting tests in Laravel
- updated to Clockwork App 4.1.1
4.1.1
- added ext-json to composer.json require section (idea by staabm, thanks!)
- fixed Clockwork being initialized too soon in Laravel integration leading to possible crashes (reported by tminich, thanks!)
4.1
- added support for command type requests with command specific metadata (commandName, commandArguments, commandArgumentsDefaults, commandOptions, commandOptionsDefaults, commandExitCode, commandOutput)
- added support for collecting executed artisan commands in Laravel integration
- added support for queue-job type requests with queue-job specific metadata (jobName, jobDescription, jobStatus, jobPayload, jobQueue, jobConnection, jobOptions)
- added support for collecting executed queue-jobs in Laravel integration (also supports Laravel Horizon)
- added support for test type requests with test specific metadata (testName, testStatus, testStatusMessage, testAsserts)
- added support for collecting test runs in Laravel integration using PHPunit
- added support for disabling collection of view data when collecting rendered views (new default is to collect views without data)
- added Twig data source using the built-in Twig profiler to collect more precise Twig profiling data
- added support for setting parent requests on requests
- improved collecting of database queries, cache queries, dispatched queue jobs and redis commands to also collect time
- improved the data sources filters api to allow multiple filter types
- improved collecting of Laravel views to use a separate data source
- improved Eloquent data source to have an additional "early" filter applied before the query is added to query counts
- improved Eloquent data source now passes raw stack trace as second argument to filters
- improved Laravel data source to work when response is not provided
- improved Laravel events data source to include Laravel namespace in the default ignored events
- improved Laravel views data source to strip view data prefixed with __
- improved PHP data source to not set request time for cli commands
- improved serializer to omit data below depth limit, support debugInfo, jsonSerialize and toArray methods (partially implemented by mahagr, thanks!)
- improved log to allow overriding serializer settings via context, no longer enabled toString by default
- improved Request class now has pre-populated request time on creation
- improved StackTrace helper with limit option, last method, fixed filter output keys
- improved Lumen queue and redis feature detection
- improved vanilla integration to allow manually sending the headers early (implemented by tminich, thanks!)
- fixed Symfony support, added support for latest Symfony 5.x and 4.x (reported by llaville, thanks!)
- removed dark theme for the web UI setting (now configurable in the Clockwork app itself)
- updated to Clockwork App 4.1
*BREAKING*
- multiple new settings were added to the Laravel config file
- DataSourceInterface::reset method was added, default empty implementation is provided in the base DataSource class
- LaravelDataSource constructor arguments changed to reflect removing the views collecting support
4.0.17
- improved performance and memory usage when doing file storage cleanup (reported by ikkez, thanks!)
- fixed crash after running file storage cleanup
- fixed typo in clockwork:clean argument description
4.0.16
- fixed Laravel middleware being registered too late, causing "collect data always" setting to not work (reported by Youniteus, thanks!)
4.0.15
- fixed cleanup not working with file storage (implemented by LucidTaZ, thanks!)
4.0.14
- fixed compatibility with Laravel 5.4 and earlier when resolving authenticated user
4.0.13
- fixed stack traces processing not handling call_user_func frames properly leading to wrong traces (reported by marcus-at-localhost, thanks!)
- fixed wrong stack traces skip namespaces defaults leading to wrong traces
- fixed vanilla integration config file missing and no longer used settings
4.0.12
- added a simple index file locking to the file storage
- improved handling of invalid index data in the file storage (reported by nsbucky and tkaven, thanks!)
- fixed Laravel data source crash when running without auth service (implemented by DrBenton, thanks!)
4.0.11
- updated web UI (Clockwork App 4.0.6)
4.0.10
- fixed wrong file:line for log messages (requires enabled stack traces atm)
4.0.9
- fixed duplicate queries detection reporting all relationship queries instead of only duplicates (reported by robclancy, thanks!)
- improved the default .gitignore for metadata storage to ignore compressed metadata as well (implemented by clugg, thanks!)
4.0.8
- updated web UI (Clockwork App 4.0.5)
4.0.7
- updated web UI (Clockwork App 4.0.4)
4.0.6
- fixed possible crash in LaravelDataSource when resolving authenticated user in non-standard auth implementations (4.0 regression) (implemented by zarunet, thanks!)
- fixed StackTrace::filter calling array_filter with swapped arguments (implemented by villermen, thanks!)
- fixed PHP 5.x incompatibility tenaming the Storage\Search empty and notEmpty methods to isEmpty and isNotEmpty (reported by eduardodgarciac, thanks!)
- updated web UI (Clockwork App 4.0.3)
4.0.5
- fixed multiple issues causing FileStorage cleanup to not delete old metadata or crash (partially implemented by jaumesala, reported by SerafimArts, thanks!)
- updated web UI (Clockwork App 4.0.2)
4.0.4
- fixed web UI not working (4.0.2 regression) (reported by williamqian and lachlankrautz, thanks!)
4.0.3
- fixed crash when using SQL storage (reported by sebastiaanluca, thanks!)
4.0.2
- updated web UI (Clockwork App 4.0.1)
4.0.1
- fixed Lumen support (reported by Owlnofeathers, thanks!)
4.0
- added "features" configuration
- added requests search (extended storage api)
- added collecting request body data (idea by lkloon123, thanks!)
- added collecting of dispatched queue jobs
- added collecting Redis commands (idea by tillkruss, thanks!)
- added collecting of database query stats separate from queries
- added collecting of executed middleware
- added ability to specify slow database query threshold
- added ability to collect only slow database queries
- added ability to disable collecting of database queries keeping database stats
- added ability to disable collecting of cache queries keeping cache stats
- added duplicate (N+1) database query detection (inspired by beyondcode/laravel-query-detector, thanks!)
- added configuration to limit number of collected frames for stack traces (defaults to 10)
- added configuration to specify skipped vendors, namespaces and files for stack traces
- added index file to file storage
- added support for compression in file storage
- added new filters api to data sources
- improved file and sql storage to support search api
- improved symfony storage to work with file storage changes
- improved log api to allow passing custom stack traces in context
- improved refactored and cleaned up Laravel service provider
- improved Lumen integration to share more code with Laravel integration
- improved refactored sql storage a bit
- improved timeline api, description is now optional and defaults to event name when calling startEvent (idea by robclancy, thanks!)
- updated web UI
- fixed regexp in vanilla integration Clockwork REST api processing
- removed storage filter support (replaced by features configuration)
- BREAKING configuration format changes, please re-deploy if using customized Clockwork config
- NOTE metadata files from previous versions will need to be manually removed on upgrade
3.1.4
- improved DBALDataSource to work with custom types (thanks villermen)
3.1.3
- updated LaravelCacheDataSource to support Laravel 5.8
3.1.2
- fixed missing use statement in vanilla integration (thanks micc83)
3.1.1
- exposed the Request::setAuthenticatedUser method on the main Clockwork class
- fixed possible crash in LaravelDataSource when resolving authenticated user in non-standard auth implementations (thanks freshleafmedia, motia)
3.1
- added new integration for vanilla PHP (thanks martbean)
- added support for collecting authenticated user info
- added bunch of helper methods for adding data like database queries or events to Clockwork
- added serializer options to the config files
- updated web UI to match latest Chrome version
- improved collecting of exceptions
- improved filtered uris implementation in Laravel to no longer have any performance overhead (thanks marcusbetts)
- improved compatibility with Laravel Telescope
- fixed numeric keys being lost on serialization of arrays (thanks ametad)
- fixed serialization of parent class private properties
- fixed a possible crash when resolving stack traces (thanks mbardelmeijer)
- deprecated Clockwork::subrequest method in favor of Clockwork::addSubrequest
3.0.2
- fixed infinite redirect if dark web theme is enabled on Laravel or Lumen <5.5 (thanks pixelskribe)
3.0.1
- improved LaravelDataSource to not collect views data if it is filtered (by default)
3.0
- updated web UI to match latest Chrome version
- added new api for user-data (custom tabs in Clockwork app)
- added support for authentication (thanks xiaohuilam)
- added support for collecting stack traces for log messages, queries, etc. (thanks sisve)
- added new api for recording subrequests (thanks L3o-pold)
- added Symfony integration beta
- added Xdebug profiler support
- added collecting of full URLs for requests
- added collecting of peak memory usage
- added ability to use dark theme for the web UI
- added new extend-api to data soruces for extending data when it's being sent to the application
- improved data serialization implementation - handles recursion, unlimited depth, type metadata, clear marking for protected and private properties
- improved data serialization with configurable defaults, limit and blackboxing of classes
- improved handling of binary bindings in EloquentDataSource (thanks sergio91pt and coderNeos)
- improved stack traces collection to resolve original view names
- BREAKING improved Laravel integration to type-hint contracts instead of concrete implementations (thanks robclancy)
- improved default configuration to not collect data for Laravel Horizon requests (thanks fgilio)
- improved LaravelDataSource view data collecting to remove Laravel Twigbridge metadata
- changed Laravel integration to register middleware in the boot method instead of register (thanks dionysiosarvanitis)
- changed Laravel and Lumen integrations to use a single shared Log instance
- fixed Clockwork HTTP API returning empty object instead of null if request was not found
- fixed Clockwork routes not returning 404 when disabled on runtime with route cache enabled (thanks joskfg)
- BREAKING dropped Laravel 4 support
- BREAKING dropped PHP 5.4 support, requires PHP 5.5
2.2.5
- changed SQL storage schema URI column type from VARCHAR to TEXT (thanks sumidatx)
- fixed possible crash in file storage cleanup if the file was already deleted (thanks bcalik)
- fixed event handling in Eloquent data source compatibility with some 3rd party packages (thanks erikgaal)
2.2.4
- drop support for collecting Laravel controller middleware (as this can have unexpected side-effects) (thanks phh)
2.2.3
- improved Server-Timing now uses the new header format (thanks kohenkatz)
- fixed Laravel crash when gathering middleware if the controller class doesn't exist
2.2.2
- fixed compatibility with Laravel 5.2 (thanks peppeocchi)
2.2.1
- fixed Laravel 4.x support once again (thanks bcalik)
2.2
- added support for collecting route middleware (thanks Vercoutere)
- added support for collecting routes and middleware in newer Lumen versions
- updated Web UI to match Clockwork Chrome 2.2
- improved Laravel support to register most event handlers only when collecting data
- fixed Lumen middleware not being registered automatically (thanks lucian-dragomir)
- fixed published Lumen config not being loaded
2.1.1
- fixed Laravel 4.x support (added legacy version of the config file) (thanks bcalik)
2.1
- updated Web UI to match Clockwork Chrome 2.1
- improved Laravel support to load the default config and use env variables in the default config
- improved Lumen support to use the standard config subsystem instead of directly accessing env variables (thanks davoaust, SunMar)
- improved reliability of storing metadata in some cases (by using JSON_PARTIAL_OUTPUT_ON_ERROR when supported)
- fixed wrong mime-type for javascript assets in Web UI causing it to not work in some browsers (thanks sleavitt)
- fixed path checking in Web UI causing it to not work on Windows (thanks Malezha)
- fixed parameters conversion in DBALDataSource (thanks andrzejenne)
2.0.4
- improved mkdir error handling in FileStorage (thanks FBnil)
- fixed crash in LaravelEventsDataSource when firing events with associative array as payload
2.0.3
- fixed Clockwork now working when used with Laravel route cache
2.0.2
- fixed crash on attempt to clean up file storage if the project contains Clockwork 1.x metadata
2.0.1
- fixed Web UI not working in Firefox
2.0
- added Web UI
- added new Laravel cache data source
- added new Laravel events data source
- added new more robust metadata storage API
- added automatic metadata cleanup (defaults to 1 week)
- added better metadata serialization including class names for objects
- added PostgreSQL compatibility for the SQL storage (thanks oldskool73)
- added Slim 3 middleware (thanks sperrichon)
- added PSR message data source (thanks sperrichon)
- added Doctrine DBAL data source (thanks sperrichon)
- changed Clockwork request ids now use dashes instead of dots (thanks Tibbelit)
- changed Laravel and Lumen integrations to no longer log data for console commands
- changed simplified the clock Laravel helper (thanks Jergus Lejko)
- fixed wrong version data logged in SQL storage
- removed PHP 5.3 support, code style changes
- removed CodeIgniter support
- removed ability to register additional data sources via Clockwork config
UPGRADING
- update the required Clockwork version to ^2.0 in your composer.json
- PHP 5.3 - no longer supported, you can continue using the latest 1.x version
- CodeIgniter - no longer supported, you can continue using the latest 1.x version
- Slim 2 - update the imported namespace from Clockwork\Support\Slim to Clockwork\Support\Slim\Legacy
- ability to register additional data sources via Clockwork config was removed, please call app('clockwork')->addDataSource(...) in your own service provider
1.14.5
- fixed incompatibility with Laravel 4.1 an 4.2 (introduced in 1.14.3)
1.14.4
- added support for Lumen 5.5 (thanks nebez)
1.14.3
- added support for Laravel 5.5 package auto-discovery (thanks Omranic)
- added automatic registration of the Laravel middleware (no need to edit your Http/Kernel.php anymore, existing installations don't need to be changed)
- updated Laravel artisan clockwork:clean command for Laravel 5.5 (thanks rosswilson252)
- fixed crash when retrieving all requests from Sql storage (thanks pies)
1.14.2
- fixed missing imports in Doctrine data source (thanks jenssegers)
1.14.1
- fixed collecting Eloquent queries when using PDO_ODBC driver for real (thanks abhimanyu003)
1.14
- added support for Server-Timing headers (thanks Garbee)
- fixed compatibility with Lumen 5.4 (thanks Dimasdanz)
- fixed collecting Eloquent queries with bindings containing backslashes (thanks fitztrev)
- fixed collecting Eloquent queries when using PDO_ODBC driver (thanks abhimanyu003)
- fixed collecting Doctrine queries with array bindings (thanks RolfJanssen)
- replaced Doctrine bindings preparation code with more complete version from laravel-doctrine
- fixed PHP 5.3 compatibility
1.13.1
- fixed compatibility with Lumen 5.4 (thanks meanevo)
1.13
- added support for Laravel 5.4 (thanks KKSzymanowski)
- improved Laravel "clock" helper function now takes multiple arguments to be logged at once (eg. `clock($foo, $bar, $baz)`)
1.12
- added collecting of caller file name and line number for queries and model name (Laravel 4.2+) for ORM queries to the Eloquent data source (thanks OmarMakled and fitztrev for the idea)
- added collecting of context, caller file name and line number to the logger (thanks crissi for the idea)
- fixed crash in Lumen data source when running unit tests with simulated requests on Lumen
- fixed compatibility with Laravel 4.0
1.11.2
- switched to PSR-4 autoloading
- fixed Swift data source crash when sending email with no from/to address specified (thanks marksecurelogin)
1.11.1
- added support for DateTimeImmutable in Doctrine data source (thanks morfin)
- fixed not being able to log null values via the "clock" helper function
- fixed Laravel 4.2-dev not being properly detected as 4.2 release (thanks DemianD)
1.11
- added support for Lumen 5.2 (thanks lukeed)
- added "clock" helper function
- fixed data sources being initialized too late (thanks morfin)
- fixed code style in Doctrine data source
- removed Laravel log dependency from Doctrine data source
- NOTE laravel-doctrine provides ootb support for Clockwork, you should use this instead of included Doctrine data source with Laravel
1.10.1
- fixed collecting of database queries in Laravel 5.2 (thanks sebastiandedeyne)
1.10
- added Laravel 5.2 support (thanks jonphipps)
- improved file storage to allow configuring directory permissions (thanks patrick-radius)
- fixed interaction with PHPUnit in Lumen (thanks troyharvey)
- removed "router dispatch" timeline event for now (due to Laravel 5.2 changes)
1.9
- added Lumen support (thanks dawiyo)
- added aliases for all Clockwork parts so they can be resolved by the IoC container in Laravel and Lumen
- fixed Laravel framework initialisation, booting and running timeline events not being recorded properly (thanks HipsterJazzbo, sisve)
- fixed how Laravel clockwork:clean artisan command is registered (thanks freekmurze)
- removed Lumen framework initialisation, booting and running timeline events as they are not supported by Lumen
1.8.1
- fixed SQL data storage initialization if PDO is set to throw exception on error (thanks YOzaz)
1.8
- added SQL data storage implementation
- added new config options for data storage for Laravel (please re-publish the config file)
- fixed not being able to use the Larvel route caching when using Clockwork (thanks Garbee, kylestev, cbakker86)
1.7
- added support for Laravel 5 (thanks Garbee, slovenianGooner)
- improved support for Laravel 4.1 and 4.2, Clockwork data is now available for error responses
- added Doctrine data source (thanks matiux)
- fixed compatibility with some old PHP 5.3 versions (thanks hailwood)
- updated Laravel data source to capture the context for log messages (thanks hermanzhu)
1.6
- improved Eloquent data source to support multiple databases (thanks ingro)
- improved compatibility with Laravel apps not using database
- improved compatibility with various CodeIngiter installations
- fixed a bug where log messages and timeline data might not be sorted correctly
- fixed missing static keyword in CodeIgniter hook (thanks noevidenz)
- changed Timeline::endEvent behavior to return false instead of throwing exception when called for non-existing event
1.5
- improved Slim support to use DI container to share Clockwork instance instead of config
- improved Slim support now adds all messages logged via Slim's log interface to Clockwork log as well
- improved CodeIgniter support to make Clockwork available through the CI app (tnx BradEstey)
- fixed Laravel support breaking flash messages (tnx hannesvdvreken)
- fixed CodeIgniter support PSR-0 autoloading and other improvements (tnx pwhelan)
- fixed file storage warning when recursive data is collected
1.4.4
- changed Laravel support to disable permanent data collection by default (tnx jenssegers)
- improved Laravel support to return Clockwork data with proper Content-Type (tnx maximebeaudoin)
- fixed CodeIgniter support compatibility with PHP 5.3 (tnx BradEstey)
1.4.3
- fixed incorrect requests ids being generated depending on set locale
1.4.2
- fixed Laravel support compatibility with PHP 5.3
1.4.1
- fixed Laravel support compatibility with PHP 5.3
1.4
- added support for collecting emails and views data
- added support for CodeIgniter 2.1 (tnx pwhelan)
- added data source and plugin for collecting emails data from Swift mailer
- added support for collecting emails and views data from Laravel
- added --age argument to Laravel artisan clockwork::clean command, specifies how old the request data must be to be deleted (in hours)
- improved Laravel service provider
- fixed compatibility with latest Laravel 4.1
1.3
NOTE: Clockwork\Request\Log::log method arguments have been changed from log($message, $level) to log($level, $message), levels are now specified via Psr\Log\LogLevel class, it's recommended to use shortcut methods for various levels (emergency, alert, critical, error, warning, notice, info and debug($message))
- clockwork log class now implements PSR logger interface, updated Laravel and Monolog support to use all available log levels
- clockwork log now accepts objects and arrays as input and logs their json representation
- added support for specifying additional headers on metadata requests (Laravel) (tnx philsturgeon)
1.2
- added support for Laravel 4.1
- added facade for Laravel
- added ability to disable collecting data about requests to specified URIs in Laravel
- added clockwork:clean artisan command for cleaning request metadata for Laravel
- added an easy way to add timeline events and log records via main Clockwork class
- added support for Slim apps running in subdirs (requires Clockwork Chrome 1.1+)
- file storage now creates default gitignore file for the request data when creating the storage dir
- fixed a few bugs which might cause request data to not appear in Chrome extension
- fixed a few bugs that could lead to PHP errors/exceptions
1.1
- added support for Laravel 4 apps running in subdirs (requires Clockwork Chrome 1.1+)
- added data-protocol version to the request data
- updated Laravel 4 service provider to work with Clockwork Web
- fixed a bug where Clockwork would break Laravel 4 apps not using database
- fixed a bug where calling Timeline::endEvent after Timeline::finalize caused exception to be thrown
- fixed a bug where using certain filters would store incorrect data
0.9.1
- added support for application routes (ootb support for Laravel 4 only atm)
- added configuration file for Laravel 4
- added support for filtering stored data in Storage
- added library version constant Clockwork::VERSION

View File

@@ -0,0 +1,13 @@
<?php namespace Clockwork\Authentication;
interface AuthenticatorInterface
{
const REQUIRES_USERNAME = 'username';
const REQUIRES_PASSWORD = 'password';
public function attempt(array $credentials);
public function check($token);
public function requires();
}

View File

@@ -0,0 +1,19 @@
<?php namespace Clockwork\Authentication;
class NullAuthenticator implements AuthenticatorInterface
{
public function attempt(array $credentials)
{
return true;
}
public function check($token)
{
return true;
}
public function requires()
{
return [];
}
}

View File

@@ -0,0 +1,34 @@
<?php namespace Clockwork\Authentication;
class SimpleAuthenticator implements AuthenticatorInterface
{
protected $password;
public function __construct($password)
{
$this->password = $password;
}
public function attempt(array $credentials)
{
if (! isset($credentials['password'])) {
return false;
}
if (! hash_equals($credentials['password'], $this->password)) {
return false;
}
return password_hash($this->password, \PASSWORD_DEFAULT);
}
public function check($token)
{
return password_verify($this->password, $token);
}
public function requires()
{
return [ AuthenticatorInterface::REQUIRES_PASSWORD ];
}
}

View File

@@ -0,0 +1,283 @@
<?php namespace Clockwork;
use Clockwork\Authentication\AuthenticatorInterface;
use Clockwork\Authentication\NullAuthenticator;
use Clockwork\DataSource\DataSourceInterface;
use Clockwork\Helpers\Serializer;
use Clockwork\Request\Log;
use Clockwork\Request\Request;
use Clockwork\Request\RequestType;
use Clockwork\Request\ShouldCollect;
use Clockwork\Request\ShouldRecord;
use Clockwork\Storage\StorageInterface;
use Closure;
// A central class implementing the core flow of the library
class Clockwork
{
// Clockwork library version
const VERSION = '5.1.12';
// Array of data sources, these objects collect metadata for the current application run
protected $dataSources = [];
// Request object, data structure which stores metadata about the current application run
protected $request;
// Storage object, provides implementation for storing and retrieving request objects
protected $storage;
// Authenticator implementation, authenticates requests for clockwork metadata
protected $authenticator;
// An object specifying the rules for collecting requests
protected $shouldCollect;
// An object specifying the rules for recording requests
protected $shouldRecord;
// Create a new Clockwork instance with default request object, a storage implementation has to be additionally set
public function __construct()
{
$this->request = new Request;
$this->authenticator = new NullAuthenticator;
$this->shouldCollect = new ShouldCollect;
$this->shouldRecord = new ShouldRecord;
}
// Add a new data source
public function addDataSource(DataSourceInterface $dataSource)
{
$this->dataSources[] = $dataSource;
return $this;
}
// Resolve the current request, sending it through all data sources, finalizing log and timeline
public function resolveRequest()
{
foreach ($this->dataSources as $dataSource) {
$dataSource->resolve($this->request);
}
$this->request->log()->sort();
$this->request->timeline()->finalize($this->request->time);
return $this;
}
// Resolve the current request as a "command" type request with command-specific data
public function resolveAsCommand($name, $exitCode = null, $arguments = [], $options = [], $argumentsDefaults = [], $optionsDefaults = [], $output = null)
{
$this->resolveRequest();
$this->request->type = RequestType::COMMAND;
$this->request->commandName = $name;
$this->request->commandArguments = $arguments;
$this->request->commandArgumentsDefaults = $argumentsDefaults;
$this->request->commandOptions = $options;
$this->request->commandOptionsDefaults = $optionsDefaults;
$this->request->commandExitCode = $exitCode;
$this->request->commandOutput = $output;
return $this;
}
// Resolve the current request as a "queue-job" type request with queue-job-specific data
public function resolveAsQueueJob($name, $description = null, $status = 'processed', $payload = [], $queue = null, $connection = null, $options = [])
{
$this->resolveRequest();
$this->request->type = RequestType::QUEUE_JOB;
$this->request->jobName = $name;
$this->request->jobDescription = $description;
$this->request->jobStatus = $status;
$this->request->jobPayload = (new Serializer)->normalize($payload);
$this->request->jobQueue = $queue;
$this->request->jobConnection = $connection;
$this->request->jobOptions = (new Serializer)->normalizeEach($options);
return $this;
}
// Resolve the current request as a "test" type request with test-specific data, accepts test name, status, status
// message in case of failure and array of ran asserts
public function resolveAsTest($name, $status = 'passed', $statusMessage = null, $asserts = [])
{
$this->resolveRequest();
$this->request->type = RequestType::TEST;
$this->request->testName = $name;
$this->request->testStatus = $status;
$this->request->testStatusMessage = $statusMessage;
foreach ($asserts as $assert) {
$this->request->addTestAssert($assert['name'], $assert['arguments'], $assert['passed'], $assert['trace']);
}
return $this;
}
// Extends the request with an additional data form all data sources, which is not required for normal use
public function extendRequest(Request $request = null)
{
foreach ($this->dataSources as $dataSource) {
$dataSource->extend($request ?: $this->request);
}
return $this;
}
// Store the current request via configured storage implementation
public function storeRequest()
{
return $this->storage->store($this->request);
}
// Reset all data sources to an empty state, clearing any collected data
public function reset()
{
foreach ($this->dataSources as $dataSource) {
$dataSource->reset();
}
return $this;
}
// Get or set the current request instance
public function request(Request $request = null)
{
if (! $request) return $this->request;
$this->request = $request;
return $this;
}
// Get the log instance for the current request or log a new message
public function log($level = null, $message = null, array $context = [])
{
if ($level) {
return $this->request->log()->log($level, $message, $context);
}
return $this->request->log();
}
// Get the timeline instance for the current request
public function timeline()
{
return $this->request->timeline();
}
// Shortcut to create a new event on the current timeline instance
public function event($description, $data = [])
{
return $this->request->timeline()->event($description, $data);
}
// Configure which requests should be collected, can be called with arrey of options, a custom closure or with no
// arguments for a fluent configuration api
public function shouldCollect($shouldCollect = null)
{
if ($shouldCollect instanceof Closure) return $this->shouldCollect->callback($shouldCollect);
if (is_array($shouldCollect)) return $this->shouldCollect->merge($shouldCollect);
return $this->shouldCollect;
}
// Configure which requests should be recorded, can be called with arrey of options, a custom closure or with no
// arguments for a fluent configuration api
public function shouldRecord($shouldRecord = null)
{
if ($shouldRecord instanceof Closure) return $this->shouldRecord->callback($shouldRecord);
if (is_array($shouldRecord)) return $this->shouldRecord->merge($shouldRecord);
return $this->shouldRecord;
}
// Get or set all data sources at once
public function dataSources($dataSources = null)
{
if (! $dataSources) return $this->dataSources;
$this->dataSources = $dataSources;
return $this;
}
// Get or set a storage implementation
public function storage(StorageInterface $storage = null)
{
if (! $storage) return $this->storage;
$this->storage = $storage;
return $this;
}
// Get or set an authenticator implementation
public function authenticator(AuthenticatorInterface $authenticator = null)
{
if (! $authenticator) return $this->authenticator;
$this->authenticator = $authenticator;
return $this;
}
// Forward any other method calls to the current request and log instances
public function __call($method, $args)
{
if (in_array($method, [ 'emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug' ])) {
return $this->request->log()->$method(...$args);
}
return $this->request->$method(...$args);
}
// DEPRECATED The following apis are deprecated and will be removed in a future version
// Get all added data sources
public function getDataSources()
{
return $this->dataSources;
}
// Get the current request instance
public function getRequest()
{
return $this->request;
}
// Set the current request instance
public function setRequest(Request $request)
{
$this->request = $request;
return $this;
}
// Get a storage implementation
public function getStorage()
{
return $this->storage;
}
// Set a storage implementation
public function setStorage(StorageInterface $storage)
{
$this->storage = $storage;
return $this;
}
// Get an authenticator implementation
public function getAuthenticator()
{
return $this->authenticator;
}
// Set an authenticator implementation
public function setAuthenticator(AuthenticatorInterface $authenticator)
{
$this->authenticator = $authenticator;
return $this;
}
}

View File

@@ -0,0 +1,64 @@
<?php namespace Clockwork\DataSource\Concerns;
use Clockwork\Helpers\StackFilter;
use Clockwork\Helpers\StackTrace;
use Clockwork\Request\Log;
use Clockwork\Request\Request;
// Duplicate (N+1) queries detection for EloquentDataSource, inspired by the beyondcode/laravel-query-detector package
// by Marcel Pociot (https://github.com/beyondcode/laravel-query-detector)
trait EloquentDetectDuplicateQueries
{
protected $duplicateQueries = [];
protected function appendDuplicateQueriesWarnings(Request $request)
{
$log = new Log;
foreach ($this->duplicateQueries as $query) {
if ($query['count'] <= 1) continue;
$log->warning(
"N+1 queries: {$query['model']}::{$query['relation']} loaded {$query['count']} times.",
[ 'performance' => true, 'trace' => $query['trace'] ]
);
}
$request->log()->merge($log);
}
protected function detectDuplicateQuery(StackTrace $trace)
{
$relationFrame = $trace->first(function ($frame) {
return $frame->function == 'getRelationValue'
|| $frame->class == \Illuminate\Database\Eloquent\Relations\Relation::class;
});
if (! $relationFrame || ! $relationFrame->object) return;
if ($relationFrame->class == \Illuminate\Database\Eloquent\Relations\Relation::class) {
$model = get_class($relationFrame->object->getParent());
$relation = get_class($relationFrame->object->getRelated());
} else {
$model = get_class($relationFrame->object);
$relation = $relationFrame->args[0];
}
$shortTrace = $trace->skip(StackFilter::make()
->isNotVendor([ 'itsgoingd', 'laravel', 'illuminate' ])
->isNotNamespace([ 'Clockwork', 'Illuminate' ]));
$hash = implode('-', [ $model, $relation, $shortTrace->first()->file, $shortTrace->first()->line ]);
if (! isset($this->duplicateQueries[$hash])) {
$this->duplicateQueries[$hash] = [
'count' => 0,
'model' => $model,
'relation' => $relation,
'trace' => $trace
];
}
$this->duplicateQueries[$hash]['count']++;
}
}

View File

@@ -0,0 +1,191 @@
<?php namespace Clockwork\DataSource;
use Clockwork\Request\Request;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Logging\LoggerChain;
use Doctrine\DBAL\Logging\SQLLogger;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;
// Data source for DBAL, provides database queries
class DBALDataSource extends DataSource implements SQLLogger
{
// Array of collected queries
protected $queries = [];
// Current running query
protected $query = null;
// DBAL connection
protected $connection;
// DBAL connection name
protected $connectionName;
// Create a new data source instance, takes a DBAL connection instance as an argument
public function __construct(Connection $connection)
{
$this->connection = $connection;
$this->connectionName = $this->connection->getDatabase();
$configuration = $this->connection->getConfiguration();
$currentLogger = $configuration->getSQLLogger();
if ($currentLogger === null) {
$configuration->setSQLLogger($this);
} else {
$loggerChain = new LoggerChain;
$loggerChain->addLogger($currentLogger);
$loggerChain->addLogger($this);
$configuration->setSQLLogger($loggerChain);
}
}
// Adds executed database queries to the request
public function resolve(Request $request)
{
$request->databaseQueries = array_merge($request->databaseQueries, $this->queries);
return $request;
}
// Reset the data source to an empty state, clearing any collected data
public function reset()
{
$this->queries = [];
$this->query = null;
}
// DBAL SQLLogger event
public function startQuery($sql, array $params = null, array $types = null)
{
$this->query = [
'query' => $sql,
'params' => $params,
'types' => $types,
'time' => microtime(true)
];
}
// DBAL SQLLogger event
public function stopQuery()
{
$this->registerQuery($this->query);
$this->query = null;
}
// Collect an executed database query
protected function registerQuery($query)
{
$query = [
'query' => $this->createRunnableQuery($query['query'], $query['params'], $query['types']),
'bindings' => $query['params'],
'duration' => (microtime(true) - $query['time']) * 1000,
'connection' => $this->connectionName,
'time' => $query['time']
];
if ($this->passesFilters([ $query ])) {
$this->queries[] = $query;
}
}
// Takes a query, an array of params and types as arguments, returns runnable query with upper-cased keywords
protected function createRunnableQuery($query, $params, $types)
{
// add params to query
$query = $this->replaceParams($this->connection->getDatabasePlatform(), $query, $params, $types);
// highlight keywords
$keywords = [
'select', 'insert', 'update', 'delete', 'into', 'values', 'set', 'where', 'from', 'limit', 'is', 'null',
'having', 'group by', 'order by', 'asc', 'desc'
];
$regexp = '/\b' . implode('\b|\b', $keywords) . '\b/i';
return preg_replace_callback($regexp, function ($match) { return strtoupper($match[0]); }, $query);
}
/**
* Source at laravel-doctrine/orm LaravelDoctrine\ORM\Loggers\Formatters\ReplaceQueryParams::format().
*
* @param AbstractPlatform $platform
* @param string $sql
* @param array|null $params
* @param array|null $types
*
*
* @return string
*/
public function replaceParams($platform, $sql, array $params = null, array $types = null)
{
if (is_array($params)) {
foreach ($params as $key => $param) {
$type = isset($types[$key]) ? $types[$key] : null; // Originally used null coalescing
$param = $this->convertParam($platform, $param, $type);
$sql = preg_replace('/\?/', "$param", $sql, 1);
}
}
return $sql;
}
/**
* Source at laravel-doctrine/orm LaravelDoctrine\ORM\Loggers\Formatters\ReplaceQueryParams::convertParam().
*
* @param mixed $param
*
* @throws \Exception
* @return string
*/
protected function convertParam($platform, $param, $type = null)
{
if (is_object($param)) {
if (!method_exists($param, '__toString')) {
if ($param instanceof \DateTimeInterface) {
$param = $param->format('Y-m-d H:i:s');
} elseif (Type::hasType($type)) {
$type = Type::getType($type);
$param = $type->convertToDatabaseValue($param, $platform);
} else {
throw new \Exception('Given query param is an instance of ' . get_class($param) . ' and could not be converted to a string');
}
}
} elseif (is_array($param)) {
if ($this->isNestedArray($param)) {
$param = json_encode($param, JSON_UNESCAPED_UNICODE);
} else {
$param = implode(
', ',
array_map(
function ($part) {
return '"' . (string) $part . '"';
},
$param
)
);
return '(' . $param . ')';
}
} else {
$param = htmlspecialchars((string) $param); // Originally used the e() Laravel helper
}
return '"' . (string) $param . '"';
}
/**
* Source at laravel-doctrine/orm LaravelDoctrine\ORM\Loggers\Formatters\ReplaceQueryParams::isNestedArray().
*
* @param array $array
* @return bool
*/
private function isNestedArray(array $array)
{
foreach ($array as $key => $value) {
if (is_array($value)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,67 @@
<?php namespace Clockwork\DataSource;
use Clockwork\Request\Request;
// Base data source class
class DataSource implements DataSourceInterface
{
// Array of filter functions
protected $filters = [];
// Adds collected data to the request and returns it, to be implemented by extending classes
public function resolve(Request $request)
{
return $request;
}
// Extends the request with an additional data, which is not required for normal use
public function extend(Request $request)
{
return $request;
}
// Reset the data source to an empty state, clearing any collected data
public function reset()
{
}
// Register a new filter
public function addFilter(\Closure $filter, $type = 'default')
{
$this->filters[$type] = isset($this->filters[$type])
? array_merge($this->filters[$type], [ $filter ]) : [ $filter ];
return $this;
}
// Clear all registered filters
public function clearFilters()
{
$this->filters = [];
return $this;
}
// Returns boolean whether the filterable passes all registered filters
protected function passesFilters($args, $type = 'default')
{
$filters = isset($this->filters[$type]) ? $this->filters[$type] : [];
foreach ($filters as $filter) {
if (! $filter(...$args)) return false;
}
return true;
}
// Censors passwords in an array, identified by key containing "pass" substring
public function removePasswords(array $data)
{
$keys = array_keys($data);
$values = array_map(function ($value, $key) {
return strpos($key, 'pass') !== false ? '*removed*' : $value;
}, $data, $keys);
return array_combine($keys, $values);
}
}

View File

@@ -0,0 +1,16 @@
<?php namespace Clockwork\DataSource;
use Clockwork\Request\Request;
// Data source interface, all data sources must implement this interface
interface DataSourceInterface
{
// Adds collected data to the request and returns it
public function resolve(Request $request);
// Extends the request with an additional data, which is not required for normal use
public function extend(Request $request);
// Reset the data source to an empty state, clearing any collected data
public function reset();
}

View File

@@ -0,0 +1,12 @@
<?php namespace Clockwork\DataSource;
use Doctrine\ORM\EntityManager;
// Data source for Doctrine, provides database queries
class DoctrineDataSource extends DBALDataSource
{
public function __construct(EntityManager $enm)
{
parent::__construct($enm->getConnection());
}
}

View File

@@ -0,0 +1,329 @@
<?php namespace Clockwork\DataSource;
use Clockwork\Helpers\Serializer;
use Clockwork\Helpers\StackTrace;
use Clockwork\Request\Request;
use Clockwork\Support\Laravel\Eloquent\ResolveModelLegacyScope;
use Clockwork\Support\Laravel\Eloquent\ResolveModelScope;
use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Contracts\Events\Dispatcher as EventDispatcher;
// Data source for Eloquent (Laravel ORM), provides database queries, stats, model actions and counts
class EloquentDataSource extends DataSource
{
use Concerns\EloquentDetectDuplicateQueries;
// Database manager instance
protected $databaseManager;
// Event dispatcher instance
protected $eventDispatcher;
// Array of collected queries
protected $queries = [];
// Query counts by type
protected $count = [
'total' => 0, 'slow' => 0, 'select' => 0, 'insert' => 0, 'update' => 0, 'delete' => 0, 'other' => 0
];
// Collected models actions
protected $modelsActions = [];
// Model action counts by model, eg. [ 'retrieved' => [ User::class => 1 ] ]
protected $modelsCount = [
'retrieved' => [], 'created' => [], 'updated' => [], 'deleted' => []
];
// Whether we are collecting database queries or stats only
protected $collectQueries = true;
// Whether we are collecting models actions or stats only
protected $collectModelsActions = true;
// Whether we are collecting retrieved models as well when collecting models actions
protected $collectModelsRetrieved = false;
// Query execution time threshold in ms after which the query is marked as slow
protected $slowThreshold;
// Enable duplicate queries detection
protected $detectDuplicateQueries = false;
// Model name to associate with the next executed query, used to map queries to models
public $nextQueryModel;
// Create a new data source instance, takes a database manager, an event dispatcher as arguments and additional
// options as arguments
public function __construct(ConnectionResolverInterface $databaseManager, EventDispatcher $eventDispatcher, $collectQueries = true, $slowThreshold = null, $slowOnly = false, $detectDuplicateQueries = false, $collectModelsActions = true, $collectModelsRetrieved = false)
{
$this->databaseManager = $databaseManager;
$this->eventDispatcher = $eventDispatcher;
$this->collectQueries = $collectQueries;
$this->slowThreshold = $slowThreshold;
$this->detectDuplicateQueries = $detectDuplicateQueries;
$this->collectModelsActions = $collectModelsActions;
$this->collectModelsRetrieved = $collectModelsRetrieved;
if ($slowOnly) $this->addFilter(function ($query) { return $query['duration'] > $this->slowThreshold; });
}
// Adds ran database queries, query counts, models actions and models counts to the request
public function resolve(Request $request)
{
$request->databaseQueries = array_merge($request->databaseQueries, $this->queries);
$request->databaseQueriesCount += $this->count['total'];
$request->databaseSlowQueries += $this->count['slow'];
$request->databaseSelects += $this->count['select'];
$request->databaseInserts += $this->count['insert'];
$request->databaseUpdates += $this->count['update'];
$request->databaseDeletes += $this->count['delete'];
$request->databaseOthers += $this->count['other'];
$request->modelsActions = array_merge($request->modelsActions, $this->modelsActions);
$request->modelsRetrieved = $this->modelsCount['retrieved'];
$request->modelsCreated = $this->modelsCount['created'];
$request->modelsUpdated = $this->modelsCount['updated'];
$request->modelsDeleted = $this->modelsCount['deleted'];
$this->appendDuplicateQueriesWarnings($request);
return $request;
}
// Reset the data source to an empty state, clearing any collected data
public function reset()
{
$this->queries = [];
$this->count = [
'total' => 0, 'slow' => 0, 'select' => 0, 'insert' => 0, 'update' => 0, 'delete' => 0, 'other' => 0
];
$this->modelsActions = [];
$this->modelsCount = [
'retrieved' => [], 'created' => [], 'updated' => [], 'deleted' => []
];
$this->nextQueryModel = null;
}
// Start listening to Eloquent events
public function listenToEvents()
{
if ($scope = $this->getModelResolvingScope()) {
$this->eventDispatcher->listen('eloquent.booted: *', function ($model, $data = null) use ($scope) {
if (is_string($model) && is_array($data)) { // Laravel 5.4 wildcard event
$model = reset($data);
}
$model->addGlobalScope($scope);
});
}
if (class_exists(\Illuminate\Database\Events\QueryExecuted::class)) {
// Laravel 5.2 and up
$this->eventDispatcher->listen(\Illuminate\Database\Events\QueryExecuted::class, function ($event) {
$this->registerQuery($event);
});
} else {
// Laravel 5.0 to 5.1
$this->eventDispatcher->listen('illuminate.query', function ($event) {
$this->registerLegacyQuery($event);
});
}
// register all event listeners individually so we don't have to regex the event type and support Laravel <5.4
$this->listenToModelEvent('retrieved');
$this->listenToModelEvent('created');
$this->listenToModelEvent('updated');
$this->listenToModelEvent('deleted');
}
// Register a listener collecting model events of specified type
protected function listenToModelEvent($event)
{
$this->eventDispatcher->listen("eloquent.{$event}: *", function ($model, $data = null) use ($event) {
if (is_string($model) && is_array($data)) { // Laravel 5.4 wildcard event
$model = reset($data);
}
$this->collectModelEvent($event, $model);
});
}
// Collect an executed database query
protected function registerQuery($event)
{
$trace = StackTrace::get([ 'arguments' => $this->detectDuplicateQueries ])->resolveViewName();
if ($this->detectDuplicateQueries) $this->detectDuplicateQuery($trace);
$query = [
'query' => $this->createRunnableQuery($event->sql, $event->bindings, $event->connectionName),
'duration' => $event->time,
'connection' => $event->connectionName,
'time' => microtime(true) - $event->time / 1000,
'trace' => (new Serializer)->trace($trace),
'model' => $this->nextQueryModel,
'tags' => $this->slowThreshold !== null && $event->time > $this->slowThreshold ? [ 'slow' ] : []
];
$this->nextQueryModel = null;
if (! $this->passesFilters([ $query, $trace ], 'early')) return;
$this->incrementQueryCount($query);
if (! $this->collectQueries || ! $this->passesFilters([ $query, $trace ])) return;
$this->queries[] = $query;
}
// Collect an executed database query (pre Laravel 5.2)
protected function registerLegacyQuery($sql, $bindings, $time, $connection)
{
return $this->registerQuery((object) [
'sql' => $sql,
'bindings' => $bindings,
'time' => $time,
'connectionName' => $connection
]);
}
// Collect a model event and update stats
protected function collectModelEvent($event, $model)
{
$lastQuery = ($queryCount = count($this->queries)) ? $this->queries[$queryCount - 1] : null;
$action = [
'model' => $modelClass = get_class($model),
'key' => $this->getModelKey($model),
'action' => $event,
'attributes' => $this->collectModelsRetrieved && $event == 'retrieved' ? $model->getOriginal() : [],
'changes' => $this->collectModelsActions ? $model->getChanges() : [],
'time' => microtime(true) / 1000,
'query' => $lastQuery ? $lastQuery['query'] : null,
'duration' => $lastQuery ? $lastQuery['duration'] : null,
'connection' => $lastQuery ? $lastQuery['connection'] : null,
'trace' => null,
'tags' => []
];
if ($lastQuery) $this->queries[$queryCount - 1]['model'] = $modelClass;
if (! $this->passesFilters([ $action ], 'models-early')) return;
$this->incrementModelsCount($action['action'], $action['model']);
if (! $this->collectModelsActions) return;
if (! $this->collectModelsRetrieved && $event == 'retrieved') return;
if (! $this->passesFilters([ $action ], 'models')) return;
$action['trace'] = (new Serializer)->trace(StackTrace::get()->resolveViewName());
$this->modelsActions[] = $action;
}
// Takes a query, an array of bindings and the connection as arguments, returns runnable query with upper-cased keywords
protected function createRunnableQuery($query, $bindings, $connection)
{
// add bindings to query
$bindings = $this->databaseManager->connection($connection)->prepareBindings($bindings);
$index = 0;
$query = preg_replace_callback('/\?/', function ($matches) use ($bindings, $connection, &$index) {
$binding = $this->quoteBinding($bindings[$index++], $connection);
// convert binary bindings to hexadecimal representation
if (! preg_match('//u', (string) $binding)) $binding = '0x' . bin2hex($binding);
// escape backslashes in the binding (preg_replace requires to do so)
return (string) $binding;
}, $query, count($bindings));
// highlight keywords
$keywords = [
'select', 'insert', 'update', 'delete', 'into', 'values', 'set', 'where', 'from', 'limit', 'is', 'null',
'having', 'group by', 'order by', 'asc', 'desc'
];
$regexp = '/\b' . implode('\b|\b', $keywords) . '\b/i';
return preg_replace_callback($regexp, function ($match) { return strtoupper($match[0]); }, $query);
}
// Takes a query binding and a connection name, returns a quoted binding value
protected function quoteBinding($binding, $connection)
{
$connection = $this->databaseManager->connection($connection);
if (! method_exists($connection, 'getPdo')) return;
$pdo = $connection->getPdo();
if ($pdo === null) return;
if ($pdo->getAttribute(\PDO::ATTR_DRIVER_NAME) === 'odbc') {
// PDO_ODBC driver doesn't support the quote method, apply simple MSSQL style quoting instead
return "'" . str_replace("'", "''", $binding) . "'";
}
return is_string($binding) ? $pdo->quote($binding) : $binding;
}
// Increment query counts for collected query
protected function incrementQueryCount($query)
{
$sql = ltrim($query['query']);
$this->count['total']++;
if (preg_match('/^select\b/i', $sql)) {
$this->count['select']++;
} elseif (preg_match('/^insert\b/i', $sql)) {
$this->count['insert']++;
} elseif (preg_match('/^update\b/i', $sql)) {
$this->count['update']++;
} elseif (preg_match('/^delete\b/i', $sql)) {
$this->count['delete']++;
} else {
$this->count['other']++;
}
if (in_array('slow', $query['tags'])) {
$this->count['slow']++;
}
}
// Increment model counts for collected model action
protected function incrementModelsCount($action, $model)
{
if (! isset($this->modelsCount[$action][$model])) {
$this->modelsCount[$action][$model] = 0;
}
$this->modelsCount[$action][$model]++;
}
// Returns model resolving scope for the installed Laravel version
protected function getModelResolvingScope()
{
if (interface_exists(\Illuminate\Database\Eloquent\ScopeInterface::class)) {
// Laravel 5.0 to 5.1
return new ResolveModelLegacyScope($this);
}
return new ResolveModelScope($this);
}
// Returns model key without crashing when using Eloquent strict mode and it's not loaded
protected function getModelKey($model)
{
try {
return $model->getKey();
} catch (\Illuminate\Database\Eloquent\MissingAttributeException $e) {}
}
}

View File

@@ -0,0 +1,138 @@
<?php namespace Clockwork\DataSource;
use Clockwork\Helpers\Serializer;
use Clockwork\Helpers\StackTrace;
use Clockwork\Request\Request;
use Illuminate\Contracts\Events\Dispatcher as EventDispatcher;
// Data source for Laravel cache component, provides cache queries and stats
class LaravelCacheDataSource extends DataSource
{
// Event dispatcher instance
protected $eventDispatcher;
// Executed cache queries
protected $queries = [];
// Query counts by type
protected $count = [
'read' => 0, 'hit' => 0, 'write' => 0, 'delete' => 0
];
// Whether we are collecting cache queries or stats only
protected $collectQueries = true;
// Whether we are collecting values from cache queries
protected $collectValues = true;
// Create a new data source instance, takes an event dispatcher and additional options as argument
public function __construct(EventDispatcher $eventDispatcher, $collectQueries = true, $collectValues = true)
{
$this->eventDispatcher = $eventDispatcher;
$this->collectQueries = $collectQueries;
$this->collectValues = $collectValues;
}
// Adds cache queries and stats to the request
public function resolve(Request $request)
{
$request->cacheQueries = array_merge($request->cacheQueries, $this->queries);
$request->cacheReads += $this->count['read'];
$request->cacheHits += $this->count['hit'];
$request->cacheWrites += $this->count['write'];
$request->cacheDeletes += $this->count['delete'];
return $request;
}
// Reset the data source to an empty state, clearing any collected data
public function reset()
{
$this->queries = [];
$this->count = [
'read' => 0, 'hit' => 0, 'write' => 0, 'delete' => 0
];
}
// Start listening to cache events
public function listenToEvents()
{
if (class_exists(\Illuminate\Cache\Events\CacheHit::class)) {
$this->eventDispatcher->listen(\Illuminate\Cache\Events\CacheHit::class, function ($event) {
$this->registerQuery([ 'type' => 'hit', 'key' => $event->key, 'value' => $event->value ]);
});
$this->eventDispatcher->listen(\Illuminate\Cache\Events\CacheMissed::class, function ($event) {
$this->registerQuery([ 'type' => 'miss', 'key' => $event->key ]);
});
$this->eventDispatcher->listen(\Illuminate\Cache\Events\KeyWritten::class, function ($event) {
$this->registerQuery([
'type' => 'write', 'key' => $event->key, 'value' => $event->value,
'expiration' => property_exists($event, 'seconds') ? $event->seconds : $event->minutes * 60
]);
});
$this->eventDispatcher->listen(\Illuminate\Cache\Events\KeyForgotten::class, function ($event) {
$this->registerQuery([ 'type' => 'delete', 'key' => $event->key ]);
});
} else {
// legacy Laravel 5.1 style events
$this->eventDispatcher->listen('cache.hit', function ($key, $value) {
$this->registerQuery([ 'type' => 'hit', 'key' => $key, 'value' => $value ]);
});
$this->eventDispatcher->listen('cache.missed', function ($key) {
$this->registerQuery([ 'type' => 'miss', 'key' => $key ]);
});
$this->eventDispatcher->listen('cache.write', function ($key, $value, $minutes) {
$this->registerQuery([
'type' => 'write', 'key' => $key, 'value' => $value, 'expiration' => $minutes * 60
]);
});
$this->eventDispatcher->listen('cache.delete', function ($key) {
$this->registerQuery([ 'type' => 'delete', 'key' => $key ]);
});
}
}
// Collect an executed query
protected function registerQuery(array $query)
{
$trace = StackTrace::get()->resolveViewName();
$record = [
'type' => $query['type'],
'key' => $query['key'],
'time' => microtime(true),
'connection' => null,
'trace' => (new Serializer)->trace($trace)
];
if ($this->collectValues && isset($query['value'])) {
$record['value'] = (new Serializer)->normalize($query['value']);
}
$this->incrementQueryCount($record);
if ($this->collectQueries && $this->passesFilters([ $record ])) {
$this->queries[] = $record;
}
}
// Increment query counts for collected query
protected function incrementQueryCount($query)
{
if ($query['type'] == 'write') {
$this->count['write']++;
} elseif ($query['type'] == 'delete') {
$this->count['delete']++;
} else {
$this->count['read']++;
if ($query['type'] == 'hit') {
$this->count['hit']++;
}
}
}
}

View File

@@ -0,0 +1,223 @@
<?php namespace Clockwork\DataSource;
use Clockwork\DataSource\DataSource;
use Clockwork\Helpers\Serializer;
use Clockwork\Request\Log;
use Clockwork\Request\Request;
use Illuminate\Contracts\Foundation\Application;
use Symfony\Component\HttpFoundation\Response;
// Data source for Laravel framework, provides application log, request and response information
class LaravelDataSource extends DataSource
{
// Laravel application instance
protected $app;
// Laravel response instance
protected $response;
// Whether we should collect log messages
protected $collectLog = true;
// Whether we should collect routes
protected $collectRoutes = false;
// Only collect routes from following list of namespaces (collect all if empty)
protected $routesOnlyNamespaces = [];
// Clockwork log instance
protected $log;
// Create a new data source, takes Laravel application instance and additional options as an arguments
public function __construct(Application $app, $collectLog = true, $collectRoutes = false, $routesOnlyNamespaces = true)
{
$this->app = $app;
$this->collectLog = $collectLog;
$this->collectRoutes = $collectRoutes;
$this->routesOnlyNamespaces = $routesOnlyNamespaces;
$this->log = new Log;
}
// Adds request, response information, middleware, routes, session data, user and log entries to the request
public function resolve(Request $request)
{
$request->method = $this->getRequestMethod();
$request->url = $this->getRequestUrl();
$request->uri = $this->getRequestUri();
$request->controller = $this->getController();
$request->headers = $this->getRequestHeaders();
$request->responseStatus = $this->getResponseStatus();
$request->middleware = $this->getMiddleware();
$request->routes = $this->getRoutes();
$request->sessionData = $this->getSessionData();
$this->resolveAuthenticatedUser($request);
$request->log()->merge($this->log);
return $request;
}
// Reset the data source to an empty state, clearing any collected data
public function reset()
{
$this->log = new Log;
}
// Set Laravel application instance for the current request
public function setApplication(Application $app)
{
$this->app = $app;
return $this;
}
// Set Laravel response instance for the current request
public function setResponse(Response $response)
{
$this->response = $response;
return $this;
}
// Listen for the log events
public function listenToEvents()
{
if (! $this->collectLog) return;
if (class_exists(\Illuminate\Log\Events\MessageLogged::class)) {
// Laravel 5.4
$this->app['events']->listen(\Illuminate\Log\Events\MessageLogged::class, function ($event) {
$this->log->log($event->level, $event->message, $event->context);
});
} else {
// Laravel 5.0 to 5.3
$this->app['events']->listen('illuminate.log', function ($level, $message, $context) {
$this->log->log($level, $message, $context);
});
}
}
// Get a textual representation of the current route's controller
protected function getController()
{
$router = $this->app['router'];
$route = $router->current();
$controller = $route ? $route->getActionName() : null;
if ($controller instanceof \Closure) {
$controller = 'anonymous function';
} elseif (is_object($controller)) {
$controller = 'instance of ' . get_class($controller);
} elseif (is_array($controller) && count($controller) == 2) {
if (is_object($controller[0])) {
$controller = get_class($controller[0]) . '->' . $controller[1];
} else {
$controller = $controller[0] . '::' . $controller[1];
}
} elseif (! is_string($controller)) {
$controller = null;
}
return $controller;
}
// Get the request headers
protected function getRequestHeaders()
{
return $this->app['request']->headers->all();
}
// Get the request method
protected function getRequestMethod()
{
return $this->app['request']->getMethod();
}
// Get the request URL
protected function getRequestUrl()
{
return $this->app['request']->fullUrl();
}
// Get the request URI
protected function getRequestUri()
{
return $this->app['request']->getRequestUri();
}
// Get the response status code
protected function getResponseStatus()
{
return $this->response ? $this->response->getStatusCode() : null;
}
// Get an array of middleware for the matched route
protected function getMiddleware()
{
$route = $this->app['router']->current();
if (! $route) return;
return method_exists($route, 'gatherMiddleware') ? $route->gatherMiddleware() : $route->middleware();
}
// Get an array of application routes
protected function getRoutes()
{
if (! $this->collectRoutes) return [];
return array_values(array_filter(array_map(function ($route) {
$action = $route->getActionName() ?: 'anonymous function';
$namespace = strpos($action, '\\') !== false ? explode('\\', $action)[0] : null;
if (count($this->routesOnlyNamespaces) && ! in_array($namespace, $this->routesOnlyNamespaces)) return;
return [
'method' => implode(', ', $route->methods()),
'uri' => $route->uri(),
'name' => $route->getName(),
'action' => $action,
'middleware' => $route->middleware(),
'before' => method_exists($route, 'beforeFilters') ? implode(', ', array_keys($route->beforeFilters())) : '',
'after' => method_exists($route, 'afterFilters') ? implode(', ', array_keys($route->afterFilters())) : ''
];
}, $this->app['router']->getRoutes()->getRoutes())));
}
// Get the session data (normalized with removed passwords)
protected function getSessionData()
{
if (! isset($this->app['session'])) return [];
return $this->removePasswords((new Serializer)->normalizeEach($this->app['session']->all()));
}
// Add authenticated user data to the request
protected function resolveAuthenticatedUser(Request $request)
{
if (! isset($this->app['auth'])) return;
if (! ($user = $this->app['auth']->user())) return;
if ($user instanceof \Illuminate\Database\Eloquent\Model) {
// retrieve attributes in this awkward way to make sure we don't trigger exceptions with Eloquent strict mode on
$keyName = method_exists($user, 'getAuthIdentifierName') ? $user->getAuthIdentifierName() : $user->getKeyName();
$user = $user->getAttributes();
$userId = isset($user[$keyName]) ? $user[$keyName] : null;
$userEmail = isset($user['email']) ? $user['email'] : $userId;
$userName = isset($user['name']) ? $user['name'] : null;
} else {
$userId = $user->getAuthIdentifier();
$userEmail = isset($user->email) ? $user->email : $userId;
$userName = isset($user->name) ? $user->name : null;
}
$request->setAuthenticatedUser($userEmail, $userId, [
'email' => $userEmail,
'name' => $userName
]);
}
}

View File

@@ -0,0 +1,143 @@
<?php namespace Clockwork\DataSource;
use Clockwork\Helpers\Serializer;
use Clockwork\Helpers\StackTrace;
use Clockwork\Request\Request;
use Illuminate\Contracts\Events\Dispatcher;
// Data source for Laravel events component, provides fired events
class LaravelEventsDataSource extends DataSource
{
// Event dispatcher instance
protected $dispatcher;
// Fired events
protected $events = [];
// Whether framework events should be collected
protected $ignoredEvents = false;
// Create a new data source instance, takes an event dispatcher and additional options as arguments
public function __construct(Dispatcher $dispatcher, $ignoredEvents = [])
{
$this->dispatcher = $dispatcher;
$this->ignoredEvents = is_array($ignoredEvents)
? array_merge($ignoredEvents, $this->defaultIgnoredEvents()) : [];
}
// Adds fired events to the request
public function resolve(Request $request)
{
$request->events = array_merge($request->events, $this->events);
return $request;
}
// Reset the data source to an empty state, clearing any collected data
public function reset()
{
$this->events = [];
}
// Start listening to the events
public function listenToEvents()
{
$this->dispatcher->listen('*', function ($event = null, $data = null) {
if (method_exists($this->dispatcher, 'firing')) { // Laravel 5.0 - 5.3
$data = func_get_args();
$event = $this->dispatcher->firing();
}
$this->registerEvent($event, $data);
});
}
// Collect a fired event, prepares data for serialization and resolves registered listeners
protected function registerEvent($event, array $data)
{
if (! $this->shouldCollect($event)) return;
$trace = StackTrace::get()->resolveViewName();
$event = [
'event' => $event,
'data' => (new Serializer)->normalize(count($data) == 1 && isset($data[0]) ? $data[0] : $data),
'time' => microtime(true),
'listeners' => $this->findListenersFor($event),
'trace' => (new Serializer)->trace($trace)
];
if ($this->passesFilters([ $event ])) {
$this->events[] = $event;
}
}
// Returns registered listeners for the specified event
protected function findListenersFor($event)
{
$listener = $this->dispatcher->getListeners($event)[0];
return array_filter(array_map(function ($listener) {
if ($listener instanceof \Closure) {
// Laravel 5.4+ (and earlier versions in some cases) wrap the listener into a closure,
// attempt to resolve the original listener
$use = (new \ReflectionFunction($listener))->getStaticVariables();
$listener = isset($use['listener']) ? $use['listener'] : $listener;
}
if (is_string($listener)) {
return $listener;
} elseif (is_array($listener) && count($listener) == 2) {
if (is_object($listener[0])) {
return get_class($listener[0]) . '@' . $listener[1];
} else {
return $listener[0] . '::' . $listener[1];
}
} elseif ($listener instanceof \Closure) {
$listener = new \ReflectionFunction($listener);
if (strpos($listener->getNamespaceName(), 'Clockwork\\') === 0) { // skip our own listeners
return;
}
$filename = str_replace(base_path(), '', $listener->getFileName());
$startLine = $listener->getStartLine();
$endLine = $listener->getEndLine();
return "Closure ({$filename}:{$startLine}-{$endLine})";
}
}, $this->dispatcher->getListeners($event)));
}
// Returns whether the event should be collected (depending on ignored events)
protected function shouldCollect($event)
{
return ! preg_match('/^(?:' . implode('|', $this->ignoredEvents) . ')$/', $event);
}
// Returns default ignored events (framework-specific events)
protected function defaultIgnoredEvents()
{
return [
'Illuminate\\\\.+',
'Laravel\\\\.+',
'auth\.(?:attempt|login|logout)',
'artisan\.start',
'bootstrapped:.+',
'composing:.+',
'creating:.+',
'illuminate\.query',
'connection\..+',
'eloquent\..+',
'kernel\.handled',
'illuminate\.log',
'mailer\.sending',
'router\.(?:before|after|matched)',
'router.filter:.+',
'locale\.changed',
'clockwork\..+'
];
}
}

View File

@@ -0,0 +1,250 @@
<?php namespace Clockwork\DataSource;
use Clockwork\Helpers\Serializer;
use Clockwork\Helpers\StackTrace;
use Clockwork\Request\Request;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Events\MessageSending;
use Illuminate\Mail\Events\MessageSent;
use Illuminate\Notifications\Events\NotificationSending;
use Illuminate\Notifications\Events\NotificationSent;
// Data source for Laravel notifications and mail components, provides sent notifications and emails
class LaravelNotificationsDataSource extends DataSource
{
// Event dispatcher instance
protected $dispatcher;
// Sent notifications
protected $notifications = [];
// Last collected notification
protected $lastNotification;
// Create a new data source instance, takes an event dispatcher as argument
public function __construct(Dispatcher $dispatcher)
{
$this->dispatcher = $dispatcher;
}
// Add sent notifications to the request
public function resolve(Request $request)
{
$request->notifications = array_merge($request->notifications, $this->notifications);
return $request;
}
// Reset the data source to an empty state, clearing any collected data
public function reset()
{
$this->notifications = [];
}
// Listen to the email and notification events
public function listenToEvents()
{
$this->dispatcher->listen(MessageSending::class, function ($event) { $this->sendingMessage($event); });
$this->dispatcher->listen(MessageSent::class, function ($event) { $this->sentMessage($event); });
$this->dispatcher->listen(NotificationSending::class, function ($event) { $this->sendingNotification($event); });
$this->dispatcher->listen(NotificationSent::class, function ($event) { $this->sentNotification($event); });
}
// Collect a sent email
protected function sendingMessage($event)
{
$trace = StackTrace::get()->resolveViewName();
$mailable = ($frame = $trace->first(function ($frame) { return is_subclass_of($frame->object, Mailable::class); }))
? $frame->object : null;
$notification = (object) [
'subject' => $event->message->getSubject(),
'from' => $this->messageAddressToString($event->message->getFrom()),
'to' => $this->messageAddressToString($event->message->getTo()),
'content' => $this->messageBody($event->message),
'type' => 'mail',
'data' => [
'cc' => $this->messageAddressToString($event->message->getCc()),
'bcc' => $this->messageAddressToString($event->message->getBcc()),
'replyTo' => $this->messageAddressToString($event->message->getReplyTo()),
'mailable' => (new Serializer)->normalize($mailable)
],
'time' => microtime(true),
'trace' => (new Serializer)->trace($trace)
];
if ($this->updateLastNotification($notification)) return;
if ($this->passesFilters([ $notification ])) {
$this->notifications[] = $this->lastNotification = $notification;
} else {
$this->lastNotification = null;
}
}
// Update last notification with time taken to send it
protected function sentMessage($event)
{
if ($this->lastNotification) {
$this->lastNotification->duration = (microtime(true) - $this->lastNotification->time) * 1000;
}
}
// Collect a sent notification
protected function sendingNotification($event)
{
$trace = StackTrace::get()->resolveViewName();
$channelSpecific = $this->resolveChannelSpecific($event);
$notification = (object) [
'subject' => $channelSpecific['subject'],
'from' => $channelSpecific['from'],
'to' => $channelSpecific['to'],
'content' => $channelSpecific['content'],
'type' => $event->channel,
'data' => array_merge($channelSpecific['data'], [
'notification' => (new Serializer)->normalize($event->notification),
'notifiable' => (new Serializer)->normalize($event->notifiable)
]),
'time' => microtime(true),
'trace' => (new Serializer)->trace($trace)
];
if ($this->passesFilters([ $notification ])) {
$this->notifications[] = $this->lastNotification = $notification;
} else {
$this->lastNotification = null;
}
}
// Update last notification with time taken to send it and response
protected function sentNotification($event)
{
if ($this->lastNotification) {
$this->lastNotification->duration = (microtime(true) - $this->lastNotification->time) * 1000;
$this->lastNotification->data['response'] = $event->response;
}
}
// Update last sent email notification with additional data from the message sent event
protected function updateLastNotification($notification)
{
if (! $this->lastNotification) return false;
if ($this->lastNotification->to !== $notification->to) return false;
$this->lastNotification->subject = $notification->subject;
$this->lastNotification->from = $notification->from;
$this->lastNotification->to = $notification->to;
$this->lastNotification->content = $notification->content;
$this->lastNotification->data = array_merge($this->lastNotification->data, $notification->data);
return true;
}
// Resolve notification channel specific data
protected function resolveChannelSpecific($event)
{
if (method_exists($event->notification, 'toMail')) {
$channelSpecific = $this->resolveMailChannelSpecific($event, $event->notification->toMail($event->notifiable));
} elseif (method_exists($event->notification, 'toSlack')) {
$channelSpecific = $this->resolveSlackChannelSpecific($event, $event->notification->toSlack($event->notifiable));
} elseif (method_exists($event->notification, 'toNexmo')) {
$channelSpecific = $this->resolveNexmoChannelSpecific($event, $event->notification->toNexmo($event->notifiable));
} elseif (method_exists($event->notification, 'toBroadcast')) {
$channelSpecific = [ 'data' => [ 'data' => (new Serializer)->normalize($event->notification->toBroadcast($event->notifiable)) ] ];
} elseif (method_exists($event->notification, 'toArray')) {
$channelSpecific = [ 'data' => [ 'data' => (new Serializer)->normalize($event->notification->toArray($event->notifiable)) ] ];
} else {
$channelSpecific = [];
}
return array_merge(
[ 'subject' => null, 'from' => null, 'to' => null, 'content' => null, 'data' => [] ], $channelSpecific
);
}
// Resolve mail notification channel specific data
protected function resolveMailChannelSpecific($event, $message)
{
return [
'subject' => $message->subject ?: get_class($event->notification),
'from' => $this->notificationAddressToString($message->from),
'to' => $this->notificationAddressToString($event->notifiable->routeNotificationFor('mail', $event->notification)),
'data' => [
'cc' => $this->notificationAddressToString($message->cc),
'bcc' => $this->notificationAddressToString($message->bcc),
'replyTo' => $this->notificationAddressToString($message->replyTo)
]
];
}
// Resolve Slack notification channel specific data
protected function resolveSlackChannelSpecific($event, $message)
{
return [
'subject' => get_class($event->notification),
'from' => $message->username,
'to' => $message->channel,
'content' => $message->content
];
}
// Resolve Nexmo notification channel specific data
protected function resolveNexmoChannelSpecific($event, $message)
{
return [
'subject' => get_class($event->notification),
'from' => $message->from,
'to' => $event->notifiable->routeNotificationFor('nexmo', $event->notification),
'content' => $message->content
];
}
protected function messageAddressToString($address)
{
if (! $address) return;
return array_map(function ($address, $key) {
// Laravel 8 or earlier
if (! ($address instanceof \Symfony\Component\Mime\Address)) {
return $address ? "{$address} <{$key}>" : $key;
}
// Laravel 9 or later
return $address->toString();
}, $address, array_keys($address));
}
protected function messageBody($message)
{
// Laravel 8 or earlier
if (! ($message instanceof \Symfony\Component\Mime\Email)) {
return $message->getBody();
}
// Laravel 9 or later
return $message->getHtmlBody() ?: $message->getTextBody();
}
protected function notificationAddressToString($address)
{
if (! $address) return;
if (! is_array($address)) $address = [ $address ];
return array_map(function ($address) {
if (! is_array($address)) return $address;
$email = isset($address['address']) ? $address['address'] : $address[0];
$name = isset($address['name']) ? $address['name'] : $address[1];
return $name ? "{$name} <{$email}>" : $email;
}, $address);
}
}

View File

@@ -0,0 +1,90 @@
<?php namespace Clockwork\DataSource;
use Clockwork\Helpers\Serializer;
use Clockwork\Helpers\StackTrace;
use Clockwork\Request\Request;
use Illuminate\Queue\Queue;
// Data source for Laravel queue component, provides dispatched queue jobs
class LaravelQueueDataSource extends DataSource
{
// Queue instance
protected $queue;
// Dispatched queue jobs
protected $jobs = [];
// Clockwork ID of the current request
protected $currentRequestId;
// Create a new data source instance, takes a queue as an argument
public function __construct(Queue $queue)
{
$this->queue = $queue;
}
// Adds dispatched queue jobs to the request
public function resolve(Request $request)
{
$request->queueJobs = array_merge($request->queueJobs, $this->getJobs());
return $request;
}
// Reset the data source to an empty state, clearing any collected data
public function reset()
{
$this->jobs = [];
}
// Listen to the queue events
public function listenToEvents()
{
$this->queue->createPayloadUsing(function ($connection, $queue, $payload) {
$this->registerJob([
'id' => $id = (new Request)->id,
'connection' => $connection,
'queue' => $queue,
'name' => $payload['displayName'],
'data' => isset($payload['data']['command']) ? $payload['data']['command'] : null,
'maxTries' => $payload['maxTries'],
'timeout' => $payload['timeout'],
'time' => microtime(true)
]);
return [ 'clockwork_id' => $id, 'clockwork_parent_id' => $this->currentRequestId ];
});
}
// Set Clockwork ID of the current request
public function setCurrentRequestId($requestId)
{
$this->currentRequestId = $requestId;
return $this;
}
// Collect a dispatched queue job
protected function registerJob(array $job)
{
$trace = StackTrace::get()->resolveViewName();
$job = array_merge($job, [
'trace' => (new Serializer)->trace($trace)
]);
if ($this->passesFilters([ $job ])) {
$this->jobs[] = $job;
}
}
// Get an array of dispatched queue jobs commands
protected function getJobs()
{
return array_map(function ($query) {
return array_merge($query, [
'data' => isset($query['data']) ? (new Serializer)->normalize($query['data']) : null
]);
}, $this->jobs);
}
}

View File

@@ -0,0 +1,86 @@
<?php namespace Clockwork\DataSource;
use Clockwork\Helpers\Serializer;
use Clockwork\Helpers\StackTrace;
use Clockwork\Request\Request;
use Illuminate\Contracts\Events\Dispatcher as EventDispatcher;
// Data source for Laravel redis component, provides redis commands
class LaravelRedisDataSource extends DataSource
{
// Event dispatcher instance
protected $eventDispatcher;
// Executed redis commands
protected $commands = [];
// Whether to skip Redis commands originating from Laravel cache Redis store
protected $skipCacheCommands = true;
// Create a new data source instance, takes an event dispatcher and additional options as arguments
public function __construct(EventDispatcher $eventDispatcher, $skipCacheCommands = true)
{
$this->eventDispatcher = $eventDispatcher;
$this->skipCacheCommands = $skipCacheCommands;
if ($this->skipCacheCommands) {
$this->addFilter(function ($command, $trace) {
return ! $trace->first(function ($frame) { return $frame->class == 'Illuminate\Cache\RedisStore'; });
});
}
}
// Adds redis commands to the request
public function resolve(Request $request)
{
$request->redisCommands = array_merge($request->redisCommands, $this->getCommands());
return $request;
}
// Reset the data source to an empty state, clearing any collected data
public function reset()
{
$this->commands = [];
}
// Listen to the cache events
public function listenToEvents()
{
$this->eventDispatcher->listen(\Illuminate\Redis\Events\CommandExecuted::class, function ($event) {
$this->registerCommand([
'command' => $event->command,
'parameters' => $event->parameters,
'duration' => $event->time,
'connection' => $event->connectionName,
'time' => microtime(true) - $event->time / 1000
]);
});
}
// Collect an executed command
protected function registerCommand(array $command)
{
$trace = StackTrace::get()->resolveViewName();
$command = array_merge($command, [
'trace' => (new Serializer)->trace($trace)
]);
if ($this->passesFilters([ $command, $trace ])) {
$this->commands[] = $command;
}
}
// Get an array of executed redis commands
protected function getCommands()
{
return array_map(function ($query) {
return array_merge($query, [
'parameters' => isset($query['parameters']) ? (new Serializer)->normalize($query['parameters']) : null
]);
}, $this->commands);
}
}

View File

@@ -0,0 +1,71 @@
<?php namespace Clockwork\DataSource;
use Clockwork\DataSource\DataSource;
use Clockwork\Helpers\Serializer;
use Clockwork\Request\Request;
use Clockwork\Request\Timeline\Timeline;
use Illuminate\Contracts\Events\Dispatcher;
// Data source for Laravel views component, provides rendered views
class LaravelViewsDataSource extends DataSource
{
// Event dispatcher
protected $dispatcher;
// Timeline data structure for collected views
protected $views;
// Whether we should collect view data
protected $collectData = false;
// Create a new data source instance, takes an event dispatcher as argument
public function __construct(Dispatcher $dispatcher, $collectData = false)
{
$this->dispatcher = $dispatcher;
$this->collectData = $collectData;
$this->views = new Timeline;
}
// Adds rendered views to the request
public function resolve(Request $request)
{
$request->viewsData = array_merge($request->viewsData, $this->views->finalize());
return $request;
}
// Reset the data source to an empty state, clearing any collected data
public function reset()
{
$this->views = new Timeline;
}
// Listen to the views events
public function listenToEvents()
{
$this->dispatcher->listen('composing:*', function ($view, $data = null) {
if (is_string($view) && is_array($data)) { // Laravel 5.4 wildcard event
$view = $data[0];
}
$data = array_filter(
$this->collectData ? $view->getData() : [],
function ($v, $k) { return strpos($k, '__') !== 0; },
\ARRAY_FILTER_USE_BOTH
);
$this->views->event('Rendering a view', [
'name' => 'view ' . $view->getName(),
'start' => $time = microtime(true),
'end' => $time,
'data' => [
'name' => $view->getName(),
'data' => (new Serializer)->normalize($data)
]
]);
});
}
}

View File

@@ -0,0 +1,201 @@
<?php namespace Clockwork\DataSource;
use Clockwork\DataSource\DataSource;
use Clockwork\Helpers\Serializer;
use Clockwork\Request\Log;
use Clockwork\Request\Request;
use Laravel\Lumen\Application;
use Symfony\Component\HttpFoundation\Response;
// Data source for Lumen framework, provides application log, request and response information
class LumenDataSource extends DataSource
{
// Lumen application instance
protected $app;
// Lumen response instance
protected $response;
// Whether we should collect log messages
protected $collectLog = true;
// Whether we should collect routes
protected $collectRoutes = false;
// Clockwork log instance
protected $log;
// Create a new data source, takes Lumen application instance and additional options as arguments
public function __construct(Application $app, $collectLog = true, $collectRoutes = false)
{
$this->app = $app;
$this->collectLog = $collectLog;
$this->collectRoutes = $collectRoutes;
$this->log = new Log;
}
// Adds request, response information, middleware, routes, session data, user and log entries to the request
public function resolve(Request $request)
{
$request->method = $this->getRequestMethod();
$request->uri = $this->getRequestUri();
$request->controller = $this->getController();
$request->headers = $this->getRequestHeaders();
$request->responseStatus = $this->getResponseStatus();
$request->routes = $this->getRoutes();
$request->sessionData = $this->getSessionData();
$this->resolveAuthenticatedUser($request);
$request->log()->merge($this->log);
return $request;
}
// Reset the data source to an empty state, clearing any collected data
public function reset()
{
$this->log = new Log;
}
// Set Lumen response instance for the current request
public function setResponse(Response $response)
{
$this->response = $response;
return $this;
}
// Listen for the log events
public function listenToEvents()
{
if (! $this->collectLog) return;
if (class_exists(\Illuminate\Log\Events\MessageLogged::class)) {
// Lumen 5.4
$this->app['events']->listen(\Illuminate\Log\Events\MessageLogged::class, function ($event) {
$this->log->log($event->level, $event->message, $event->context);
});
} else {
// Lumen 5.0 to 5.3
$this->app['events']->listen('illuminate.log', function ($level, $message, $context) {
$this->log->log($level, $message, $context);
});
}
}
// Get a textual representation of current route's controller
protected function getController()
{
$routes = method_exists($this->app, 'getRoutes') ? $this->app->getRoutes() : [];
$method = $this->getRequestMethod();
$pathInfo = $this->getPathInfo();
if (isset($routes[$method.$pathInfo]['action']['uses'])) {
$controller = $routes[$method.$pathInfo]['action']['uses'];
} elseif (isset($routes[$method.$pathInfo]['action'][0])) {
$controller = $routes[$method.$pathInfo]['action'][0];
} else {
$controller = null;
}
if ($controller instanceof \Closure) {
$controller = 'anonymous function';
} elseif (is_object($controller)) {
$controller = 'instance of ' . get_class($controller);
} elseif (! is_string($controller)) {
$controller = null;
}
return $controller;
}
// Get the request headers
protected function getRequestHeaders()
{
return $this->app['request']->headers->all();
}
// Get the request method
protected function getRequestMethod()
{
if ($this->app->bound('request')) {
return $this->app['request']->getMethod();
} elseif (isset($_POST['_method'])) {
return strtoupper($_POST['_method']);
} else {
return $_SERVER['REQUEST_METHOD'];
}
}
// Get the request URI
protected function getRequestUri()
{
return $this->app['request']->getRequestUri();
}
// Get the response status code
protected function getResponseStatus()
{
return $this->response ? $this->response->getStatusCode() : null;
}
// Get an array of application routes
protected function getRoutes()
{
if (! $this->collectRoutes) return [];
if (isset($this->app->router)) {
$routes = array_values($this->app->router->getRoutes());
} elseif (method_exists($this->app, 'getRoutes')) {
$routes = array_values($this->app->getRoutes());
} else {
$routes = [];
}
return array_map(function ($route) {
return [
'method' => $route['method'],
'uri' => $route['uri'],
'name' => isset($route['action']['as']) ? $route['action']['as'] : null,
'action' => isset($route['action']['uses']) && is_string($route['action']['uses']) ? $route['action']['uses'] : 'anonymous function',
'middleware' => isset($route['action']['middleware']) ? $route['action']['middleware'] : null,
];
}, $routes);
}
// Get the session data (normalized with passwords removed)
protected function getSessionData()
{
if (! isset($this->app['session'])) return [];
return $this->removePasswords((new Serializer)->normalizeEach($this->app['session']->all()));
}
// Add authenticated user data to the request
protected function resolveAuthenticatedUser(Request $request)
{
if (! isset($this->app['auth'])) return;
if (! ($user = $this->app['auth']->user())) return;
if (! isset($user->email) || ! isset($user->id)) return;
$request->setAuthenticatedUser($user->email, $user->id, [
'email' => $user->email,
'name' => isset($user->name) ? $user->name : null
]);
}
// Get the request path info
protected function getPathInfo()
{
if ($this->app->bound('request')) {
return $this->app['request']->getPathInfo();
} else {
$query = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '';
return '/' . trim(str_replace("?{$query}", '', $_SERVER['REQUEST_URI']), '/');
}
}
}

View File

@@ -0,0 +1,37 @@
<?php namespace Clockwork\DataSource;
use Clockwork\DataSource\DataSource;
use Clockwork\Request\Log;
use Clockwork\Request\Request;
use Clockwork\Support\Monolog\Handler\ClockworkHandler;
use Monolog\Logger as Monolog;
// Data source for Monolog, provides application log
class MonologDataSource extends DataSource
{
// Clockwork log instance
protected $log;
// Create a new data source, takes Monolog instance as an argument
public function __construct(Monolog $monolog)
{
$this->log = new Log;
$monolog->pushHandler(new ClockworkHandler($this->log));
}
// Adds log entries to the request
public function resolve(Request $request)
{
$request->log()->merge($this->log);
return $request;
}
// Reset the data source to an empty state, clearing any collected data
public function reset()
{
$this->log = new Log;
}
}

View File

@@ -0,0 +1,155 @@
<?php namespace Clockwork\DataSource;
use Clockwork\DataSource\DataSource;
use Clockwork\Helpers\Serializer;
use Clockwork\Request\Request;
// Data source providing data obtainable in vanilla PHP
class PhpDataSource extends DataSource
{
// Adds request, response information, session data and peak memory usage to the request
public function resolve(Request $request)
{
$request->time = PHP_SAPI !== 'cli' ? $this->getRequestTime() : $request->time;
$request->method = $this->getRequestMethod();
$request->url = $this->getRequestUrl();
$request->uri = $this->getRequestUri();
$request->headers = $this->getRequestHeaders();
$request->getData = $this->getGetData();
$request->postData = $this->getPostData();
$request->requestData = $this->getRequestData();
$request->sessionData = $this->getSessionData();
$request->cookies = $this->getCookies();
$request->responseStatus = $this->getResponseStatus();
$request->responseTime = $this->getResponseTime();
$request->memoryUsage = $this->getMemoryUsage();
return $request;
}
// Get the request cookies (normalized with passwords removed)
protected function getCookies()
{
return $this->removePasswords((new Serializer)->normalizeEach($_COOKIE));
}
// Get the request GET data (normalized with passwords removed)
protected function getGetData()
{
return $this->removePasswords((new Serializer)->normalizeEach($_GET));
}
// Get the request POST data (normalized with passwords removed)
protected function getPostData()
{
return $this->removePasswords((new Serializer)->normalizeEach($_POST));
}
// Get the request body data (attempt to parse as json, normalized with passwords removed)
protected function getRequestData()
{
// The data will already be parsed into POST data by PHP in case of application/x-www-form-urlencoded requests
if (count($_POST)) return;
$requestData = file_get_contents('php://input');
$requestJsonData = json_decode($requestData, true);
return is_array($requestJsonData)
? $this->removePasswords((new Serializer)->normalizeEach($requestJsonData))
: $requestData;
}
// Get the request headers
protected function getRequestHeaders()
{
$headers = [];
foreach ($_SERVER as $key => $value) {
if (substr($key, 0, 5) !== 'HTTP_') continue;
$header = substr($key, 5);
$header = str_replace('_', ' ', $header);
$header = ucwords(strtolower($header));
$header = str_replace(' ', '-', $header);
if (! isset($headers[$header])) {
$headers[$header] = [ $value ];
} else {
$headers[$header][] = $value;
}
}
ksort($headers);
return $headers;
}
// Get the request method
protected function getRequestMethod()
{
if (isset($_SERVER['REQUEST_METHOD'])) {
return $_SERVER['REQUEST_METHOD'];
}
}
// Get the response time
protected function getRequestTime()
{
if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
return $_SERVER['REQUEST_TIME_FLOAT'];
}
}
// Get the request URL
protected function getRequestUrl()
{
$https = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on';
$host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : null;
$addr = isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : null;
$port = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : null;
$uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : null;
$scheme = $https ? 'https' : 'http';
$host = $host ?: $addr;
$port = (! $https && $port != 80 || $https && $port != 443) ? ":{$port}" : '';
// remove port number from the host
$host = $host ? preg_replace('/:\d+$/', '', trim($host)) : null;
return "{$scheme}://{$host}{$port}{$uri}";
}
// Get the request URI
protected function getRequestUri()
{
if (isset($_SERVER['REQUEST_URI'])) {
return $_SERVER['REQUEST_URI'];
}
}
// Get the response status code
protected function getResponseStatus()
{
return http_response_code();
}
// Get the response time (current time, assuming most of the application code has already run at this point)
protected function getResponseTime()
{
return microtime(true);
}
// Get the session data (normalized with passwords removed)
protected function getSessionData()
{
if (! isset($_SESSION)) return [];
return $this->removePasswords((new Serializer)->normalizeEach($_SESSION));
}
// Get the peak memory usage in bytes
protected function getMemoryUsage()
{
return memory_get_peak_usage(true);
}
}

View File

@@ -0,0 +1,96 @@
<?php namespace Clockwork\DataSource;
use Clockwork\DataSource\DataSource;
use Clockwork\Helpers\Serializer;
use Clockwork\Request\Request;
use Psr\Http\Message\ServerRequestInterface as PsrRequest;
use Psr\Http\Message\ResponseInterface as PsrResponse;
// Data source providing data obtainable from the PSR-7 request and response interfaces
class PsrMessageDataSource extends DataSource
{
// PSR Messages
protected $psrRequest;
protected $psrResponse;
// Create a new data source, takes PSR-7 request and response as arguments
public function __construct(PsrRequest $psrRequest = null, PsrResponse $psrResponse = null)
{
$this->psrRequest = $psrRequest;
$this->psrResponse = $psrResponse;
}
// Adds request and response information to the request
public function resolve(Request $request)
{
if ($this->psrRequest) {
$request->method = $this->psrRequest->getMethod();
$request->uri = $this->getRequestUri();
$request->headers = $this->getRequestHeaders();
$request->getData = $this->sanitize($this->psrRequest->getQueryParams());
$request->postData = $this->sanitize($this->psrRequest->getParsedBody());
$request->cookies = $this->sanitize($this->psrRequest->getCookieParams());
$request->time = $this->getRequestTime();
}
if ($this->psrResponse !== null) {
$request->responseStatus = $this->psrResponse->getStatusCode();
$request->responseTime = $this->getResponseTime();
}
return $request;
}
// Normalize items in the array and remove passwords
protected function sanitize($data)
{
return is_array($data) ? $this->removePasswords((new Serializer)->normalizeEach($data)) : $data;
}
// Get the response time, fetching it from ServerParams
protected function getRequestTime()
{
$env = $this->psrRequest->getServerParams();
if (isset($env['REQUEST_TIME_FLOAT'])) {
return $env['REQUEST_TIME_FLOAT'];
}
}
// Get the response time (current time, assuming most of the application code has already run at this point)
protected function getResponseTime()
{
return microtime(true);
}
// Get the request headers
protected function getRequestHeaders()
{
$headers = [];
foreach ($this->psrRequest->getHeaders() as $header => $values) {
if (strtoupper(substr($header, 0, 5)) === 'HTTP_') {
$header = substr($header, 5);
}
$header = str_replace('_', ' ', $header);
$header = ucwords(strtolower($header));
$header = str_replace(' ', '-', $header);
$headers[$header] = $values;
}
ksort($headers);
return $headers;
}
// Get the request URI
protected function getRequestUri()
{
$uri = $this->psrRequest->getUri();
return $uri->getPath() . ($uri->getQuery() ? '?' . $uri->getQuery() : '');
}
}

View File

@@ -0,0 +1,104 @@
<?php namespace Clockwork\DataSource;
use Clockwork\DataSource\DataSource;
use Clockwork\Request\Request;
use Slim\Slim;
// Data source for Slim 2 framework, provides controller, request and response information
class SlimDataSource extends DataSource
{
// Slim instance
protected $slim;
// Create a new data source, takes Slim instance as an argument
public function __construct(Slim $slim)
{
$this->slim = $slim;
}
// Adds request and response information to the request
public function resolve(Request $request)
{
$request->method = $this->getRequestMethod();
$request->uri = $this->getRequestUri();
$request->controller = $this->getController();
$request->headers = $this->getRequestHeaders();
$request->responseStatus = $this->getResponseStatus();
return $request;
}
// Get a textual representation of current route's controller
protected function getController()
{
$matchedRoutes = $this->slim->router()->getMatchedRoutes(
$this->slim->request()->getMethod(), $this->slim->request()->getResourceUri()
);
if (! count($matchedRoutes)) return;
$controller = end($matchedRoutes)->getCallable();
if ($controller instanceof \Closure) {
$controller = 'anonymous function';
} elseif (is_object($controller)) {
$controller = 'instance of ' . get_class($controller);
} elseif (is_array($controller) && count($controller) == 2) {
if (is_object($controller[0])) {
$controller = get_class($controller[0]) . '->' . $controller[1];
} else {
$controller = $controller[0] . '::' . $controller[1];
}
} elseif (! is_string($controller)) {
$controller = null;
}
return $controller;
}
// Get the request headers
protected function getRequestHeaders()
{
$headers = [];
foreach ($_SERVER as $key => $value) {
if (substr($key, 0, 5) !== 'HTTP_') continue;
$header = substr($key, 5);
$header = str_replace('_', ' ', $header);
$header = ucwords(strtolower($header));
$header = str_replace(' ', '-', $header);
$value = $this->slim->request()->headers($header, $value);
if (! isset($headers[$header])) {
$headers[$header] = [ $value ];
} else {
$headers[$header][] = $value;
}
}
ksort($headers);
return $headers;
}
// Get the request method
protected function getRequestMethod()
{
return $this->slim->request()->getMethod();
}
// Get the request URI
protected function getRequestUri()
{
return $this->slim->request()->getPathInfo();
}
// Get the response status code
protected function getResponseStatus()
{
return $this->slim->response()->status();
}
}

View File

@@ -0,0 +1,45 @@
<?php namespace Clockwork\DataSource;
use Clockwork\Request\Request;
use Clockwork\Request\Timeline\Timeline;
use Clockwork\Support\Swift\SwiftPluginClockworkTimeline;
use Swift_Mailer;
// Data source for Swift mailer, provides sent emails
class SwiftDataSource extends DataSource
{
// Swift instance
protected $swift;
// Clockwork timeline instance
protected $timeline;
// Create a new data source, takes a Swift instance as an argument
public function __construct(Swift_Mailer $swift)
{
$this->swift = $swift;
$this->timeline = new Timeline;
}
// Listen to the email events
public function listenToEvents()
{
$this->swift->registerPlugin(new SwiftPluginClockworkTimeline($this->timeline));
}
// Adds send emails to the request
public function resolve(Request $request)
{
$request->emailsData = array_merge($request->emailsData, $this->timeline->finalize());
return $request;
}
// Reset the data source to an empty state, clearing any collected data
public function reset()
{
$this->timeline = new Timeline;
}
}

View File

@@ -0,0 +1,40 @@
<?php namespace Clockwork\DataSource;
use Clockwork\Request\Request;
use Clockwork\Support\Twig\ProfilerClockworkDumper;
use Twig_Environment;
use Twig_Extension_Profiler;
use Twig_Profiler_Profile;
// Data source for Twig, provides rendered views
class TwigDataSource extends DataSource
{
// Twig environment instance
protected $twig;
// Twig profile instance
protected $profile;
// Create a new data source, takes Twig instance as an argument
public function __construct(Twig_Environment $twig)
{
$this->twig = $twig;
}
// Register the Twig profiler extension
public function listenToEvents()
{
$this->twig->addExtension(new Twig_Extension_Profiler($this->profile = new Twig_Profiler_Profile));
}
// Adds rendered views to the request
public function resolve(Request $request)
{
$timeline = (new ProfilerClockworkDumper)->dump($this->profile);
$request->viewsData = array_merge($request->viewsData, $timeline->finalize());
return $request;
}
}

View File

@@ -0,0 +1,31 @@
<?php namespace Clockwork\DataSource;
use Clockwork\Request\Request;
// Data source for Xdebug, provides profiling data
class XdebugDataSource extends DataSource
{
// Adds profiling data path to the request
public function resolve(Request $request)
{
$request->xdebug = [ 'profile' => xdebug_get_profiler_filename() ];
return $request;
}
// Extends the request with full profiling data
public function extend(Request $request)
{
$profile = isset($request->xdebug['profile']) ? $request->xdebug['profile'] : null;
if ($profile && ! preg_match('/\.php$/', $profile) && is_readable($profile)) {
$request->xdebug['profileData'] = file_get_contents($profile);
if (preg_match('/\.gz$/', $profile)) {
$request->xdebug['profileData'] = gzdecode($request->xdebug['profileData']);
}
}
return $request;
}
}

View File

@@ -0,0 +1,36 @@
<?php namespace Clockwork\Helpers\Concerns;
use Clockwork\Helpers\StackFrame;
// Replaces the first stack frame rendering a Laravel view with a duplicate with a resolved original view path (instead
// of the compiled view path)
trait ResolvesViewName
{
public function resolveViewName()
{
$viewFrame = $this->first(function ($frame) {
return $frame->shortPath ? preg_match('#^/storage/framework/views/[a-z0-9]+\.php$#', $frame->shortPath) : false;
});
if (! $viewFrame) return $this;
$renderFrame = $this->first(function ($frame) {
return $frame->call == 'Illuminate\View\View->getContents()'
&& $frame->object instanceof \Illuminate\View\View;
});
if (! $renderFrame) return $this;
$resolvedViewFrame = new StackFrame(
[ 'file' => $renderFrame->object->getPath(), 'line' => $viewFrame->line ],
$this->basePath,
$this->vendorPath
);
return $this->copy(array_merge(
array_slice($this->frames, 0, array_search($viewFrame, $this->frames)),
[ $resolvedViewFrame ],
array_slice($this->frames, array_search($viewFrame, $this->frames) + 2)
));
}
}

View File

@@ -0,0 +1,139 @@
<?php namespace Clockwork\Helpers;
// Prepares various types of data for serialization
class Serializer
{
// Serialized objects cache by object hash
protected $cache = [];
// Options for the current instance
protected $options = [];
// Default options for new instances
protected static $defaults = [
'blackbox' => [
\Illuminate\Container\Container::class,
\Illuminate\Foundation\Application::class,
\Laravel\Lumen\Application::class
],
'limit' => 10,
'toArray' => false,
'toString' => false,
'debugInfo' => true,
'jsonSerialize' => false,
'traces' => true,
'tracesFilter' => null,
'tracesSkip' => null,
'tracesLimit' => null
];
// Create a new instance optionally with options overriding defaults
public function __construct(array $options = [])
{
$this->options = $options + static::$defaults;
}
// Set default options for all new instances
public static function defaults(array $defaults)
{
static::$defaults = $defaults + static::$defaults;
}
// Prepares the passed data to be ready for serialization, takes any kind of data to normalize as the first
// argument, other arguments are used internally in recursion
public function normalize($data, $context = null, $limit = null)
{
if ($context === null) $context = [ 'references' => [] ];
if ($limit === null) $limit = $this->options['limit'];
if (is_array($data)) {
if ($limit === 0) return [ '__type__' => 'array', '__omitted__' => 'limit' ];
return [ '__type__' => 'array' ] + $this->normalizeEach($data, $context, $limit - 1);
} elseif (is_object($data)) {
if ($data instanceof \Closure) return [ '__type__' => 'anonymous function' ];
$className = get_class($data);
$objectHash = spl_object_hash($data);
if ($limit === 0) return [ '__class__' => $className, '__omitted__' => 'limit' ];
if (isset($context['references'][$objectHash])) return [ '__type__' => 'recursion' ];
$context['references'][$objectHash] = true;
if (isset($this->cache[$objectHash])) return $this->cache[$objectHash];
if ($this->options['blackbox'] && in_array($className, $this->options['blackbox'])) {
return $this->cache[$objectHash] = [ '__class__' => $className, '__omitted__' => 'blackbox' ];
} elseif ($this->options['toString'] && method_exists($data, '__toString')) {
return $this->cache[$objectHash] = (string) $data;
}
if ($this->options['debugInfo'] && method_exists($data, '__debugInfo')) {
$data = (array) $data->__debugInfo();
} elseif ($this->options['jsonSerialize'] && method_exists($data, 'jsonSerialize')) {
$data = (array) $data->jsonSerialize();
} elseif ($this->options['toArray'] && method_exists($data, 'toArray')) {
$data = (array) $data->toArray();
} else {
$data = (array) $data;
}
$data = array_combine(
array_map(function ($key) {
// replace null-byte prefixes of protected and private properties used by php with * (protected)
// and ~ (private)
return preg_replace('/^\0.+?\0/', '~', str_replace("\0*\0", '*', $key));
}, array_keys($data)),
$this->normalizeEach($data, $context, $limit - 1)
);
return $this->cache[$objectHash] = [ '__class__' => $className ] + $data;
} elseif (is_resource($data)) {
return [ '__type__' => 'resource' ];
}
return $data;
}
// Normalize each member of an array (doesn't add metadata for top level)
public function normalizeEach($data, $context = null, $limit = null) {
return array_map(function ($item) use ($context, $limit) {
return $this->normalize($item, $context, $limit);
}, $data);
}
// Normalize a stack trace instance
public function trace(StackTrace $trace)
{
if (! $this->options['traces']) return null;
if ($this->options['tracesFilter']) $trace = $trace->filter($this->options['tracesFilter']);
if ($this->options['tracesSkip']) $trace = $trace->skip($this->options['tracesSkip']);
if ($this->options['tracesLimit']) $trace = $trace->limit($this->options['tracesLimit']);
return array_map(function ($frame) {
return [
'call' => $frame->call,
'file' => $frame->file,
'line' => $frame->line,
'isVendor' => (bool) $frame->vendor
];
}, $trace->frames());
}
// Normalize an exception instance
public function exception(/* Throwable */ $exception)
{
return [
'type' => get_class($exception),
'message' => $exception->getMessage(),
'code' => $exception->getCode(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => (new Serializer([ 'tracesLimit' => false ]))->trace(StackTrace::from($exception->getTrace())),
'previous' => $exception->getPrevious() ? $this->exception($exception->getPrevious()) : null
];
}
}

View File

@@ -0,0 +1,45 @@
<?php namespace Clockwork\Helpers;
use Clockwork\Request\Request;
// Generates Server-Timing header value
class ServerTiming
{
// Performance metrics to include
protected $metrics = [];
// Add a performance metric
public function add($metric, $value, $description)
{
$this->metrics[] = [ 'metric' => $metric, 'value' => $value, 'description' => $description ];
return $this;
}
// Generate the header value
public function value()
{
return implode(', ', array_map(function ($metric) {
return "{$metric['metric']}; dur={$metric['value']}; desc=\"{$metric['description']}\"";
}, $this->metrics));
}
// Create a new instance from a Clockwork request
public static function fromRequest(Request $request, $eventsCount = 10)
{
$header = new static;
$header->add('app', $request->getResponseDuration(), 'Application');
if ($request->getDatabaseDuration()) {
$header->add('db', $request->getDatabaseDuration(), 'Database');
}
// add timeline events limited to a set number so the header doesn't get too large
foreach (array_slice($request->timeline()->events, 0, $eventsCount) as $i => $event) {
$header->add("timeline-event-{$i}", $event->duration(), $event->description);
}
return $header;
}
}

View File

@@ -0,0 +1,148 @@
<?php namespace Clockwork\Helpers;
// Filter stack traces
class StackFilter
{
protected $classes = [];
protected $notClasses = [];
protected $files = [];
protected $notFiles = [];
protected $functions = [];
protected $notFunctions = [];
protected $namespaces = [];
protected $notNamespaces = [];
protected $vendors = [];
protected $notVendors = [];
public static function make()
{
return new static;
}
public function isClass($classes)
{
$this->classes = array_merge($this->classes, is_array($classes) ? $classes : [ $classes ]);
return $this;
}
public function isNotClass($classes)
{
$this->notClasses = array_merge($this->notClasses, is_array($classes) ? $classes : [ $classes ]);
return $this;
}
public function isFile($files)
{
$this->files = array_merge($this->files, is_array($files) ? $files : [ $files ]);
return $this;
}
public function isNotFile($files)
{
$this->notFiles = array_merge($this->notFiles, is_array($files) ? $files : [ $files ]);
return $this;
}
public function isFunction($functions)
{
$this->functions = array_merge($this->functions, is_array($functions) ? $functions : [ $functions ]);
return $this;
}
public function isNotFunction($functions)
{
$this->notFunctions = array_merge($this->notFunctions, is_array($functions) ? $functions : [ $functions ]);
return $this;
}
public function isNamespace($namespaces)
{
$this->namespaces = array_merge($this->namespaces, is_array($namespaces) ? $namespaces : [ $namespaces ]);
return $this;
}
public function isNotNamespace($namespaces)
{
$this->notNamespaces = array_merge($this->notNamespaces, is_array($namespaces) ? $namespaces : [ $namespaces ]);
return $this;
}
public function isVendor($vendors)
{
$this->vendors = array_merge($this->vendors, is_array($vendors) ? $vendors : [ $vendors ]);
return $this;
}
public function isNotVendor($vendors)
{
$this->notVendors = array_merge($this->notVendors, is_array($vendors) ? $vendors : [ $vendors ]);
return $this;
}
// Apply the filter to a stack frame
public function filter(StackFrame $frame)
{
return $this->matchesClass($frame)
&& $this->matchesFile($frame)
&& $this->matchesFunction($frame)
&& $this->matchesNamespace($frame)
&& $this->matchesVendor($frame);
}
// Return a closure calling this filter
public function closure()
{
return function ($frame) { return $this->filter($frame); };
}
protected function matchesClass(StackFrame $frame)
{
if (count($this->classes) && ! in_array($frame->class, $this->classes)) return false;
if (count($this->notClasses) && in_array($frame->class, $this->notClasses)) return false;
return true;
}
protected function matchesFile(StackFrame $frame)
{
if (count($this->files) && ! in_array($frame->file, $this->files)) return false;
if (count($this->notFiles) && in_array($frame->file, $this->notFiles)) return false;
return true;
}
protected function matchesFunction(StackFrame $frame)
{
if (count($this->functions) && ! in_array($frame->function, $this->functions)) return false;
if (count($this->notFunctions) && in_array($frame->function, $this->notFunctions)) return false;
return true;
}
protected function matchesNamespace(StackFrame $frame)
{
foreach ($this->notNamespaces as $namespace) {
if ($frame->class !== null && strpos($frame->class, "{$namespace}\\") !== false) return false;
}
if (! count($this->namespaces)) return true;
foreach ($this->namespaces as $namespace) {
if ($frame->class !== null && strpos($frame->class, "{$namespace}\\") !== false) return true;
}
return false;
}
protected function matchesVendor(StackFrame $frame)
{
if (count($this->vendors) && ! in_array($frame->vendor, $this->vendors)) return false;
if (count($this->notVendors) && in_array($frame->vendor, $this->notVendors)) return false;
return true;
}
}

View File

@@ -0,0 +1,38 @@
<?php namespace Clockwork\Helpers;
// A single frame of a stack trace
class StackFrame
{
public $call;
public $function;
public $line;
public $file;
public $class;
public $object;
public $type;
public $args = [];
public $shortPath;
public $vendor;
public function __construct(array $data = [], $basePath = '', $vendorPath = '')
{
foreach ($data as $key => $value) {
$this->$key = $value;
}
$this->call = $this->formatCall();
$this->shortPath = $this->file ? str_replace($basePath, '', $this->file) : null;
$this->vendor = ($this->file && strpos($this->file, $vendorPath) === 0)
? explode(DIRECTORY_SEPARATOR, str_replace($vendorPath, '', $this->file))[0] : null;
}
protected function formatCall()
{
if ($this->class) {
return "{$this->class}{$this->type}{$this->function}()";
} else {
return "{$this->function}()";
}
}
}

View File

@@ -0,0 +1,127 @@
<?php namespace Clockwork\Helpers;
// A stack trace
class StackTrace
{
use Concerns\ResolvesViewName;
protected $frames;
protected $basePath;
protected $vendorPath;
// Capture a new stack trace, accepts an array of options, "arguments" to include arguments in the trace and "limit"
// to limit the trace length
public static function get($options = [])
{
$backtraceOptions = isset($options['arguments'])
? DEBUG_BACKTRACE_PROVIDE_OBJECT : DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS;
$limit = isset($options['limit']) ? $options['limit'] : 0;
return static::from(debug_backtrace($backtraceOptions, $limit));
}
// Create a stack trace from an existing debug_backtrace output
public static function from(array $trace)
{
$basePath = static::resolveBasePath();
$vendorPath = static::resolveVendorPath();
return new static(array_map(function ($frame, $index) use ($basePath, $vendorPath, $trace) {
return new StackFrame(
static::fixCallUserFuncFrame($frame, $trace, $index), $basePath, $vendorPath
);
}, $trace, array_keys($trace)), $basePath, $vendorPath);
}
public function __construct(array $frames, $basePath, $vendorPath)
{
$this->frames = $frames;
$this->basePath = $basePath;
$this->vendorPath = $vendorPath;
}
// Get all frames
public function frames()
{
return $this->frames;
}
// Get the first frame, optionally filtered by a stack filter or a closure
public function first($filter = null)
{
if (! $filter) return reset($this->frames);
if ($filter instanceof StackFilter) $filter = $filter->closure();
foreach ($this->frames as $frame) {
if ($filter($frame)) return $frame;
}
}
// Get the last frame, optionally filtered by a stack filter or a closure
public function last($filter = null)
{
if (! $filter) return $this->frames[count($this->frames) - 1];
if ($filter instanceof StackFilter) $filter = $filter->closure();
foreach (array_reverse($this->frames) as $frame) {
if ($filter($frame)) return $frame;
}
}
// Get trace filtered by a stack filter or a closure
public function filter($filter = null)
{
if ($filter instanceof StackFilter) $filter = $filter->closure();
return $this->copy(array_values(array_filter($this->frames, $filter)));
}
// Get trace skipping a number of frames or frames matching a stack filter or a closure
public function skip($count = null)
{
if ($count instanceof StackFilter) $count = $count->closure();
if ($count instanceof \Closure) $count = array_search($this->first($count), $this->frames);
return $this->copy(array_slice($this->frames, $count));
}
// Get trace with a number of frames from the top
public function limit($count = null)
{
return $this->copy(array_slice($this->frames, 0, $count));
}
// Get a copy of the trace
public function copy($frames = null)
{
return new static($frames ?: $this->frames, $this->basePath, $this->vendorPath);
}
protected static function resolveBasePath()
{
return substr(__DIR__, 0, strpos(__DIR__, DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR));
}
protected static function resolveVendorPath()
{
return static::resolveBasePath() . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR;
}
// Fixes call_user_func stack frames missing file and line
protected static function fixCallUserFuncFrame($frame, array $trace, $index)
{
if (isset($frame['file'])) return $frame;
$nextFrame = isset($trace[$index + 1]) ? $trace[$index + 1] : null;
if (! $nextFrame || ! in_array($nextFrame['function'], [ 'call_user_func', 'call_user_func_array' ])) return $frame;
$frame['file'] = $nextFrame['file'];
$frame['line'] = $nextFrame['line'];
return $frame;
}
}

View File

@@ -0,0 +1,20 @@
<?php namespace Clockwork\Request;
// Incoming HTTP request
class IncomingRequest
{
// Method
public $method;
// URI
public $uri;
// GET and POST data
public $input = [];
// Cookies
public $cookies = [];
public function __construct(array $data = [])
{
foreach ($data as $key => $val) $this->$key = $val;
}
}

View File

@@ -0,0 +1,125 @@
<?php namespace Clockwork\Request;
use Clockwork\Helpers\Serializer;
use Clockwork\Helpers\StackTrace;
use Clockwork\Helpers\StackFilter;
// Data structure representing a log with timestamped messages
class Log
{
// Array of logged messages
public $messages = [];
// Create a new log, optionally with existing messages
public function __construct($messages = [])
{
$this->messages = $messages;
}
// Log a new message, with a level and context, context can be used to override serializer defaults,
// $context['trace'] = true can be used to force collecting a stack trace
public function log($level = LogLevel::INFO, $message = null, array $context = [])
{
$trace = $this->hasTrace($context) ? $context['trace'] : StackTrace::get()->resolveViewName();
$this->messages[] = [
'message' => (new Serializer($context))->normalize($message),
'exception' => $this->formatException($context),
'context' => $this->formatContext($context),
'level' => $level,
'time' => microtime(true),
'trace' => (new Serializer(! empty($context['trace']) ? [ 'traces' => true ] : []))->trace($trace)
];
}
public function emergency($message, array $context = [])
{
$this->log(LogLevel::EMERGENCY, $message, $context);
}
public function alert($message, array $context = [])
{
$this->log(LogLevel::ALERT, $message, $context);
}
public function critical($message, array $context = [])
{
$this->log(LogLevel::CRITICAL, $message, $context);
}
public function error($message, array $context = [])
{
$this->log(LogLevel::ERROR, $message, $context);
}
public function warning($message, array $context = [])
{
$this->log(LogLevel::WARNING, $message, $context);
}
public function notice($message, array $context = [])
{
$this->log(LogLevel::NOTICE, $message, $context);
}
public function info($message, array $context = [])
{
$this->log(LogLevel::INFO, $message, $context);
}
public function debug($message, array $context = [])
{
$this->log(LogLevel::DEBUG, $message, $context);
}
// Merge another log instance into the current log
public function merge(Log $log)
{
$this->messages = array_merge($this->messages, $log->messages);
return $this;
}
// Sort the log messages by timestamp
public function sort()
{
usort($this->messages, function ($a, $b) { return $a['time'] * 1000 - $b['time'] * 1000; });
}
// Get all messages as an array
public function toArray()
{
return $this->messages;
}
// Format message context, removes exception and trace if we are serializing them
protected function formatContext($context)
{
if ($this->hasException($context)) unset($context['exception']);
if ($this->hasTrace($context)) unset($context['trace']);
return (new Serializer)->normalize($context);
}
// Format exception if present in the context
protected function formatException($context)
{
if ($this->hasException($context)) {
return (new Serializer)->exception($context['exception']);
}
}
// Check if context has serializable trace
protected function hasTrace($context)
{
return ! empty($context['trace']) && $context['trace'] instanceof StackTrace && empty($context['raw']);
}
// Check if context has serializable exception
protected function hasException($context)
{
return ! empty($context['exception'])
&& ($context['exception'] instanceof \Throwable || $context['exception'] instanceof \Exception)
&& empty($context['raw']);
}
}

View File

@@ -0,0 +1,13 @@
<?php namespace Clockwork\Request;
class LogLevel
{
const EMERGENCY = 'emergency';
const ALERT = 'alert';
const CRITICAL = 'critical';
const ERROR = 'error';
const WARNING = 'warning';
const NOTICE = 'notice';
const INFO = 'info';
const DEBUG = 'debug';
}

View File

@@ -0,0 +1,591 @@
<?php namespace Clockwork\Request;
use Clockwork\Helpers\Serializer;
// Data structure representing a single application request
class Request
{
// Unique request ID
public $id;
// Metadata version
public $version = 1;
// Request type (request, command, queue-job or test)
public $type = 'request';
// Request time
public $time;
// Request method
public $method;
// Request URL
public $url;
// Request URI
public $uri;
// Request headers
public $headers = [];
// Textual representation of the executed controller
public $controller;
// Request GET data
public $getData = [];
// Request POST data
public $postData = [];
// Request body data
public $requestData = [];
// Session data array
public $sessionData = [];
// Authenticated user
public $authenticatedUser;
// Request cookies
public $cookies = [];
// Response time
public $responseTime;
// Response processing time
public $responseDuration;
// Response status code
public $responseStatus;
// Peak memory usage in bytes
public $memoryUsage;
// Executed middleware
public $middleware = [];
// Database queries
public $databaseQueries = [];
// Database queries count
public $databaseQueriesCount;
// Database slow queries count
public $databaseSlowQueries;
// Database query counts of a particular type (selects, inserts, updates, deletes, others)
public $databaseSelects;
public $databaseInserts;
public $databaseUpdates;
public $databaseDeletes;
public $databaseOthers;
public $databaseDuration;
// Cache queries
public $cacheQueries = [];
// Cache query counts of a particular type (reads, hits, writes, deletes)
public $cacheReads;
public $cacheHits;
public $cacheWrites;
public $cacheDeletes;
// Cache queries execution time
public $cacheTime;
// Model actions
public $modelsActions = [];
// Model action counts by model
public $modelsRetrieved = [];
public $modelsCreated = [];
public $modelsUpdated = [];
public $modelsDeleted = [];
// Redis commands
public $redisCommands = [];
// Dispatched queue jobs
public $queueJobs = [];
// Timeline events
public $timelineData = [];
// Log messages
public $log = [];
// Fired events
public $events = [];
// Application routes
public $routes = [];
// Sent notifications
public $notifications = [];
// Sent emails (legacy property replaced by notifications)
public $emailsData = [];
// Rendered views
public $viewsData = [];
// Custom user data
public $userData = [];
// Subrequests
public $subrequests = [];
// Xebug profiler data
public $xdebug = [];
// Command name
public $commandName;
// Command arguments passed in
public $commandArguments = [];
// Command arguments defaults
public $commandArgumentsDefaults = [];
// Command options passed in
public $commandOptions = [];
// Command options defaults
public $commandOptionsDefaults = [];
// Command exit code
public $commandExitCode;
// Command output
public $commandOutput;
// Queue job name
public $jobName;
// Queue job description
public $jobDescription;
// Queue job status
public $jobStatus;
// Queue job payload
public $jobPayload = [];
// Queue job queue name
public $jobQueue;
// Queue job connection name
public $jobConnection;
// Queue job additional options
public $jobOptions = [];
// Test name
public $testName;
// Test status
public $testStatus;
// Test status message (eg. in case of failure)
public $testStatusMessage;
// Ran test asserts
public $testAsserts = [];
// Client-side performance metrics in the form of [ metric => value ]
public $clientMetrics = [];
// Web vitals in the form of [ vital => value ]
public $webVitals = [];
// Parent request
public $parent;
// Token to update this request data
public $updateToken;
// Log instance for the current request
protected $currentLog;
// Timeline instance for the current request
protected $currentTimeline;
// Array of property values to override collected values from data sources
protected $overrides = [];
// Create a new request, if optional data array argument is provided, it will be used to populate the request object,
// otherwise an empty request with current time, autogenerated ID and update token will be created
public function __construct(array $data = [])
{
$this->id = isset($data['id']) ? $data['id'] : $this->generateRequestId();
$this->time = microtime(true);
$this->updateToken = isset($data['updateToken']) ? $data['updateToken'] : $this->generateUpdateToken();
foreach ($data as $key => $val) $this->$key = $val;
$this->currentLog = new Log($this->log);
$this->currentTimeline = new Timeline\Timeline($this->timelineData);
}
// Compute the sum of durations of all database queries
public function getDatabaseDuration()
{
return array_reduce($this->databaseQueries, function ($total, $query) {
return isset($query['duration']) ? $total + $query['duration'] : $total;
}, 0);
}
// Compute response duration in milliseconds
public function getResponseDuration()
{
return ($this->responseTime - $this->time) * 1000;
}
// Get all request data as an array
public function toArray()
{
return [
'id' => $this->id,
'version' => $this->version,
'type' => $this->type,
'time' => $this->time,
'method' => $this->method,
'url' => $this->url,
'uri' => $this->uri,
'headers' => $this->headers,
'controller' => $this->controller,
'getData' => $this->getData,
'postData' => $this->postData,
'requestData' => $this->requestData,
'sessionData' => $this->sessionData,
'authenticatedUser' => $this->authenticatedUser,
'cookies' => $this->cookies,
'responseTime' => $this->responseTime,
'responseStatus' => $this->responseStatus,
'responseDuration' => $this->responseDuration ?: $this->getResponseDuration(),
'memoryUsage' => $this->memoryUsage,
'middleware' => $this->middleware,
'databaseQueries' => $this->databaseQueries,
'databaseQueriesCount' => $this->databaseQueriesCount,
'databaseSlowQueries' => $this->databaseSlowQueries,
'databaseSelects' => $this->databaseSelects,
'databaseInserts' => $this->databaseInserts,
'databaseUpdates' => $this->databaseUpdates,
'databaseDeletes' => $this->databaseDeletes,
'databaseOthers' => $this->databaseOthers,
'databaseDuration' => $this->getDatabaseDuration(),
'cacheQueries' => $this->cacheQueries,
'cacheReads' => $this->cacheReads,
'cacheHits' => $this->cacheHits,
'cacheWrites' => $this->cacheWrites,
'cacheDeletes' => $this->cacheDeletes,
'cacheTime' => $this->cacheTime,
'modelsActions' => $this->modelsActions,
'modelsRetrieved' => $this->modelsRetrieved,
'modelsCreated' => $this->modelsCreated,
'modelsUpdated' => $this->modelsUpdated,
'modelsDeleted' => $this->modelsDeleted,
'redisCommands' => $this->redisCommands,
'queueJobs' => $this->queueJobs,
'timelineData' => $this->timeline()->toArray(),
'log' => $this->log()->toArray(),
'events' => $this->events,
'routes' => $this->routes,
'notifications' => $this->notifications,
'emailsData' => $this->emailsData,
'viewsData' => $this->viewsData,
'userData' => array_map(function ($data) {
return $data instanceof UserData ? $data->toArray() : $data;
}, $this->userData),
'subrequests' => $this->subrequests,
'xdebug' => $this->xdebug,
'commandName' => $this->commandName,
'commandArguments' => $this->commandArguments,
'commandArgumentsDefaults' => $this->commandArgumentsDefaults,
'commandOptions' => $this->commandOptions,
'commandOptionsDefaults' => $this->commandOptionsDefaults,
'commandExitCode' => $this->commandExitCode,
'commandOutput' => $this->commandOutput,
'jobName' => $this->jobName,
'jobDescription' => $this->jobDescription,
'jobStatus' => $this->jobStatus,
'jobPayload' => $this->jobPayload,
'jobQueue' => $this->jobQueue,
'jobConnection' => $this->jobConnection,
'jobOptions' => $this->jobOptions,
'testName' => $this->testName,
'testStatus' => $this->testStatus,
'testStatusMessage' => $this->testStatusMessage,
'testAsserts' => $this->testAsserts,
'clientMetrics' => $this->clientMetrics,
'webVitals' => $this->webVitals,
'parent' => $this->parent,
'updateToken' => $this->updateToken
];
}
// Get all request data as a JSON string
public function toJson()
{
return json_encode($this->toArray(), \JSON_PARTIAL_OUTPUT_ON_ERROR);
}
// Return request data except specified keys as an array
public function except($keys)
{
return array_filter($this->toArray(), function ($value, $key) use ($keys) {
return ! in_array($key, $keys);
}, ARRAY_FILTER_USE_BOTH);
}
// Return only request data with specified keys as an array
public function only($keys)
{
return array_filter($this->toArray(), function ($value, $key) use ($keys) {
return in_array($key, $keys);
}, ARRAY_FILTER_USE_BOTH);
}
// Return log instance for the current request
public function log()
{
return $this->currentLog;
}
// Return timeline instance for the current request
public function timeline()
{
return $this->currentTimeline;
}
// Add a new overridden property
public function override($property, $value)
{
$this->overrides[$property] = $value;
return $this;
}
// Get or set all overrides at once
public function overrides($overrides = null)
{
if (! $overrides) return $this->overrides;
$this->overrides = $overrides;
return $this;
}
// Add database query, takes query, bindings, duration (in ms) and additional data - connection (connection name),
// time (when was the query executed), file (caller file name), line (caller line number), trace (serialized trace),
// model (associated ORM model)
public function addDatabaseQuery($query, $bindings = [], $duration = null, $data = [])
{
$this->databaseQueries[] = [
'query' => $query,
'bindings' => (new Serializer)->normalize($bindings),
'duration' => $duration,
'connection' => isset($data['connection']) ? $data['connection'] : null,
'time' => isset($data['time']) ? $data['time'] : microtime(true) - ($duration ?: 0) / 1000,
'file' => isset($data['file']) ? $data['file'] : null,
'line' => isset($data['line']) ? $data['line'] : null,
'trace' => isset($data['trace']) ? $data['trace'] : null,
'model' => isset($data['model']) ? $data['model'] : null,
'tags' => array_merge(
isset($data['tags']) ? $data['tags'] : [], isset($data['slow']) ? [ 'slow' ] : []
)
];
}
// Add model action, takes model, action and additional data - key, attributes, changes, time (when was the action
// executed), query, duration (in ms), connection (connection name), trace (serialized trace), file (caller file
// name), line (caller line number), tags
public function addModelAction($model, $action, $data = [])
{
$this->modelActions[] = [
'model' => $model,
'key' => isset($data['key']) ? $data['key'] : null,
'action' => $action,
'attributes' => isset($data['attributes']) ? $data['attributes'] : [],
'changes' => isset($data['changes']) ? $data['changes'] : [],
'duration' => $duration = isset($data['duration']) ? $data['duration'] : null,
'time' => isset($data['time']) ? $data['time'] : microtime(true) - ($duration ?: 0) / 1000,
'query' => isset($data['query']) ? $data['query'] : null,
'connection' => isset($data['connection']) ? $data['connection'] : null,
'trace' => isset($data['trace']) ? $data['trace'] : null,
'file' => isset($data['file']) ? $data['file'] : null,
'line' => isset($data['line']) ? $data['line'] : null,
'tags' => isset($data['tags']) ? $data['tags'] : []
];
}
// Add cache query, takes type, key, value, duration (in ms) and additional data - connection (connection name),
// time (when was the query executed), file (caller file name), line (caller line number), trace (serialized trace),
// expiration
public function addCacheQuery($type, $key, $value = null, $duration = null, $data = [])
{
$this->cacheQueries[] = [
'type' => $type,
'key' => $key,
'value' => (new Serializer)->normalize($value),
'duration' => $duration,
'connection' => isset($data['connection']) ? $data['connection'] : null,
'time' => isset($data['time']) ? $data['time'] : microtime(true) - ($duration ?: 0) / 1000,
'file' => isset($data['file']) ? $data['file'] : null,
'line' => isset($data['line']) ? $data['line'] : null,
'trace' => isset($data['trace']) ? $data['trace'] : null,
'expiration' => isset($data['expiration']) ? $data['expiration'] : null
];
}
// Add event, takes event name, data, time and additional data - listeners, duration (in ms), file (caller file
// name), line (caller line number), trace (serialized trace)
public function addEvent($event, $eventData = null, $time = null, $data = [])
{
$this->events[] = [
'event' => $event,
'data' => (new Serializer)->normalize($eventData),
'duration' => $duration = isset($data['duration']) ? $data['duration'] : null,
'time' => $time ? $time : microtime(true) - ($duration ?: 0) / 1000,
'listeners' => isset($data['listeners']) ? $data['listeners'] : null,
'file' => isset($data['file']) ? $data['file'] : null,
'line' => isset($data['line']) ? $data['line'] : null,
'trace' => isset($data['trace']) ? $data['trace'] : null
];
}
// Add route, takes method, uri, action and additional data - name, middleware, before (before filters), after
// (after filters)
public function addRoute($method, $uri, $action, $data = [])
{
$this->routes[] = [
'method' => $method,
'uri' => $uri,
'action' => $action,
'name' => isset($data['name']) ? $data['name'] : null,
'middleware' => isset($data['middleware']) ? $data['middleware'] : null,
'before' => isset($data['before']) ? $data['before'] : null,
'after' => isset($data['after']) ? $data['after'] : null
];
}
// Add sent notifucation, takes subject, recipient, sender, and additional data - time, duration, type, content, data
public function addNotification($subject, $to, $from = null, $data = [])
{
$this->notifications[] = [
'subject' => $subject,
'from' => $from,
'to' => $to,
'content' => isset($data['content']) ? $data['content'] : null,
'type' => isset($data['type']) ? $data['type'] : null,
'data' => isset($data['data']) ? $data['data'] : [],
'duration' => $duration = isset($data['duration']) ? $data['duration'] : null,
'time' => isset($data['time']) ? $data['time'] : microtime(true) - ($duration ?: 0) / 1000,
'trace' => isset($data['trace']) ? $data['trace'] : null,
'file' => isset($data['file']) ? $data['file'] : null,
'line' => isset($data['line']) ? $data['line'] : null
];
}
// Add sent email, takes subject, recipient address, sender address, array of headers, and additional data - time
// (when was the email sent), duration (sending time in ms)
public function addEmail($subject, $to, $from = null, $headers = [], $data = [])
{
$this->emailsData[] = [
'start' => isset($data['time']) ? $data['time'] : null,
'end' => isset($data['time'], $data['duration']) ? $data['time'] + $data['duration'] / 1000 : null,
'duration' => isset($data['duration']) ? $data['duration'] : null,
'description' => 'Sending an email message',
'data' => [
'subject' => $subject,
'to' => $to,
'from' => $from,
'headers' => (new Serializer)->normalize($headers)
]
];
}
// Add view, takes view name, view data and additional data - time (when was the view rendered), duration (sending
// time in ms)
public function addView($name, $viewData = [], $data = [])
{
$this->viewsData[] = [
'start' => isset($data['time']) ? $data['time'] : null,
'end' => isset($data['time'], $data['duration']) ? $data['time'] + $data['duration'] / 1000 : null,
'duration' => isset($data['duration']) ? $data['duration'] : null,
'description' => 'Rendering a view',
'data' => [
'name' => $name,
'data' => (new Serializer)->normalize($viewData)
]
];
}
// Add executed subrequest, takes the requested url, subrequest Clockwork ID and additional data - path if non-default
public function addSubrequest($url, $id, $data = [])
{
$this->subrequests[] = [
'url' => $url,
'id' => $id,
'path' => isset($data['path']) ? $data['path'] : null
];
}
// Set the authenticated user, takes a username, an id and additional data - email and name
public function setAuthenticatedUser($username, $id = null, $data = [])
{
$this->authenticatedUser = [
'id' => $id,
'username' => $username,
'email' => isset($data['email']) ? $data['email'] : null,
'name' => isset($data['name']) ? $data['name'] : null
];
}
// Set parent request, takes the request id and additional options - url and path if non-default
public function setParent($id, $data = [])
{
$this->parent = [
'id' => $id,
'url' => isset($data['url']) ? $data['url'] : null,
'path' => isset($data['path']) ? $data['path'] : null
];
}
// Add custom user data
public function userData($key = null)
{
if ($key && isset($this->userData[$key])) {
return $this->userData[$key];
}
$userData = (new UserData)->title($key);
return $key ? $this->userData[$key] = $userData : $this->userData[] = $userData;
}
// Add a ran test assert, takes the assert name, arguments, whether it passed and trace as arguments
public function addTestAssert($name, $arguments = null, $passed = true, $trace = null)
{
$this->testAsserts[] = [
'name' => $name,
'arguments' => (new Serializer)->normalize($arguments),
'trace' => $trace,
'passed' => $passed
];
}
// Generate unique request ID in the form of <current time>-<random number>
protected function generateRequestId()
{
return str_replace('.', '-', sprintf('%.4F', microtime(true))) . '-' . mt_rand();
}
// Generate a random update token
protected function generateUpdateToken()
{
$length = 8;
$bytes = function_exists('random_bytes') ? random_bytes($length) : openssl_random_pseudo_bytes($length);
return substr(bin2hex($bytes), 0, $length);
}
}

View File

@@ -0,0 +1,10 @@
<?php namespace Clockwork\Request;
// Supported request types
class RequestType
{
const REQUEST = 'request';
const COMMAND = 'command';
const QUEUE_JOB = 'queue-job';
const TEST = 'test';
}

View File

@@ -0,0 +1,120 @@
<?php namespace Clockwork\Request;
// Filter incoming requests before collecting data
class ShouldCollect
{
// Enable on-demand mode, boolean or the secret value
protected $onDemand = false;
// Enable sampling, chance to be sampled (eg. 100 to collect 1 in 100 requests)
protected $sample = false;
// List of URIs that should not be collected, can contain regexes
protected $except = [];
// List of URIs that should only be collected, can contain regexes (only used if non-empty)
protected $only = [];
// Disable collection of OPTIONS method requests (most commonly used for CORS pre-flight requests)
protected $exceptPreflight = false;
// Custom filter callback
protected $callback;
// Append one or more except URIs
public function except($uris)
{
$this->except = array_merge($this->except, is_array($uris) ? $uris : [ $uris ]);
return $this;
}
// Append one or more only URIs
public function only($uris)
{
$this->only = array_merge($this->only, is_array($uris) ? $uris : [ $uris ]);
return $this;
}
// Merge multiple settings from array
public function merge(array $data = [])
{
foreach ($data as $key => $val) $this->$key = $val;
}
// Apply the filter to an incoming request
public function filter(IncomingRequest $request)
{
return $this->passOnDemand($request)
&& $this->passSampling()
&& $this->passExcept($request)
&& $this->passOnly($request)
&& $this->passExceptPreflight($request)
&& $this->passCallback($request);
}
protected function passOnDemand(IncomingRequest $request)
{
if (! $this->onDemand) return true;
if ($this->onDemand !== true) {
$input = isset($request->input['clockwork-profile']) ? $request->input['clockwork-profile'] : '';
$cookie = isset($request->cookies['clockwork-profile']) ? $request->cookies['clockwork-profile'] : '';
return hash_equals($this->onDemand, $input) || hash_equals($this->onDemand, $cookie);
}
return isset($request->input['clockwork-profile']) || isset($request->cookies['clockwork-profile']);
}
protected function passSampling()
{
if (! $this->sample) return true;
return mt_rand(0, $this->sample) == $this->sample;
}
protected function passExcept(IncomingRequest $request)
{
if (! count($this->except)) return true;
foreach ($this->except as $pattern) {
if (preg_match('#' . str_replace('#', '\#', $pattern) . '#', $request->uri)) return false;
}
return true;
}
protected function passOnly(IncomingRequest $request)
{
if (! count($this->only)) return true;
foreach ($this->only as $pattern) {
if (preg_match('#' . str_replace('#', '\#', $pattern) . '#', $request->uri)) return true;
}
return false;
}
protected function passExceptPreflight(IncomingRequest $request)
{
if (! $this->exceptPreflight) return true;
return strtoupper($request->method) != 'OPTIONS';
}
protected function passCallback(IncomingRequest $request)
{
if (! $this->callback) return true;
return call_user_func($this->callback, $request);
}
public function __call($method, $parameters)
{
if (! count($parameters)) return $this->$method;
$this->$method = count($parameters) ? $parameters[0] : true;
return $this;
}
}

View File

@@ -0,0 +1,58 @@
<?php namespace Clockwork\Request;
// Filter requests before recording
class ShouldRecord
{
// Enable collecting of errors only (requests with 4xx or 5xx responses)
protected $errorsOnly = false;
// Enable collecting of slow requests only, slow response time threshold in ms
protected $slowOnly = false;
// Custom filter callback
protected $callback;
// Merge multiple settings from array
public function merge(array $data = [])
{
foreach ($data as $key => $val) $this->$key = $val;
}
// Apply the filter to a request
public function filter(Request $request)
{
return $this->passErrorsOnly($request)
&& $this->passSlowOnly($request)
&& $this->passCallback($request);
}
protected function passErrorsOnly(Request $request)
{
if (! $this->errorsOnly) return true;
return 400 <= $request->responseStatus && $request->responseStatus <= 599;
}
protected function passSlowOnly(Request $request)
{
if (! $this->slowOnly) return true;
return $request->getResponseDuration() >= $this->slowOnly;
}
protected function passCallback(Request $request)
{
if (! $this->callback) return true;
return call_user_func($this->callback, $request);
}
// Fluent API
public function __call($method, $parameters)
{
if (! count($parameters)) return $this->$method;
$this->$method = count($parameters) ? $parameters[0] : true;
return $this;
}
}

View File

@@ -0,0 +1,105 @@
<?php namespace Clockwork\Request\Timeline;
// Data structure representing a single timeline event with fluent API
class Event
{
// Event description
public $description;
// Unique event name
public $name;
// Start time
public $start;
// End time
public $end;
// Color (blue, red, green, purple, grey)
public $color;
// Additional event data
public $data;
public function __construct($description, $data = [])
{
$this->description = $description;
$this->name = isset($data['name']) ? $data['name'] : $description;
$this->start = isset($data['start']) ? $data['start'] : null;
$this->end = isset($data['end']) ? $data['end'] : null;
$this->color = isset($data['color']) ? $data['color'] : null;
$this->data = isset($data['data']) ? $data['data'] : null;
}
// Begin the event at current time
public function begin()
{
$this->start = microtime(true);
return $this;
}
// End the event at current time
public function end()
{
$this->end = microtime(true);
return $this;
}
// Begin the event, execute the passed in closure and end the event, returns the closure return value
public function run(\Closure $closure, ...$args)
{
$this->begin();
try {
return $closure(...$args);
} finally {
$this->end();
}
}
// Set or retrieve event duration (in ms), event can be defined with both start and end time or just a single time and duration
public function duration($duration = null)
{
if (! $duration) return ($this->start && $this->end) ? ($this->end - $this->start) * 1000 : 0;
if ($this->start) $this->end = $this->start + $duration / 1000;
if ($this->end) $this->start = $this->end - $duration / 1000;
return $this;
}
// Finalize the event, ends the event, fills in start time if empty and limits the start and end time
public function finalize($start = null, $end = null)
{
$end = $end ?: microtime(true);
$this->start = $this->start ?: $start;
$this->end = $this->end ?: $end;
if ($this->start < $start) $this->start = $start;
if ($this->end > $end) $this->end = $end;
}
// Fluent API
public function __call($method, $parameters)
{
if (! count($parameters)) return $this->$method;
$this->$method = $parameters[0];
return $this;
}
// Return an array representation of the event
public function toArray()
{
return [
'description' => $this->description,
'start' => $this->start,
'end' => $this->end,
'duration' => $this->duration(),
'color' => $this->color,
'data' => $this->data
];
}
}

View File

@@ -0,0 +1,72 @@
<?php namespace Clockwork\Request\Timeline;
// Data structure representing collection of time-based events
class Timeline
{
// Timeline events
public $events = [];
// Create a new timeline, optionally with existing events
public function __construct($events = [])
{
foreach ($events as $event) {
$this->create($event['description'], $event);
}
}
// Find or create a new event, takes description and optional data - name, start, end, duration, color, data
public function event($description, $data = [])
{
$name = isset($data['name']) ? $data['name'] : $description;
if ($event = $this->find($name)) return $event;
return $this->create($description, $data);
}
// Create a new event, takes description and optional data - name, start, end, duration, color, data
public function create($description, $data = [])
{
return $this->events[] = new Event($description, $data);
}
// Find event by name
public function find($name)
{
foreach ($this->events as $event) {
if ($event->name == $name) return $event;
}
}
// Merge another timeline instance into the current timeline
public function merge(Timeline $timeline)
{
$this->events = array_merge($this->events, $timeline->events);
return $this;
}
// Finalize timeline, ends all events, sorts them and returns as an array
public function finalize($start = null, $end = null)
{
foreach ($this->events as $event) {
$event->finalize($start, $end);
}
$this->sort();
return $this->toArray();
}
// Sort the timeline events by start time
public function sort()
{
usort($this->events, function ($a, $b) { return $a->start * 1000 - $b->start * 1000; });
}
// Return events as an array
public function toArray()
{
return array_map(function ($event) { return $event->toArray(); }, $this->events);
}
}

View File

@@ -0,0 +1,52 @@
<?php namespace Clockwork\Request;
// Data structure representing custom user data
class UserData
{
// Data items
protected $data = [];
// Data title
protected $title;
// Add generic user data
public function data(array $data, $key = null)
{
if ($key !== null) {
return $this->data[$key] = new UserDataItem($data);
}
return $this->data[] = new UserDataItem($data);
}
// Add user data shown as counters
public function counters(array $data)
{
return $this->data($data)
->showAs('counters');
}
// Add user data shown as table
public function table($title, array $data)
{
return $this->data($data)
->showAs('table')
->title($title);
}
// Set data title
public function title($title)
{
$this->title = $title;
return $this;
}
// Transform data and all contents to a serializable array with metadata
public function toArray()
{
return array_merge(
array_map(function ($data) { return $data->toArray(); }, $this->data),
[ '__meta' => array_filter([ 'title' => $this->title ]) ]
);
}
}

Some files were not shown because too many files have changed in this diff Show More