├── .gitattributes
├── Clockwork
├── Web
│ ├── public
│ │ ├── img
│ │ │ ├── icons
│ │ │ │ ├── favicon-16x16.png
│ │ │ │ ├── favicon-32x32.png
│ │ │ │ ├── apple-touch-icon.png
│ │ │ │ ├── apple-touch-icon-60x60.png
│ │ │ │ ├── apple-touch-icon-76x76.png
│ │ │ │ ├── apple-touch-icon-120x120.png
│ │ │ │ ├── apple-touch-icon-152x152.png
│ │ │ │ └── apple-touch-icon-180x180.png
│ │ │ ├── appearance-auto-icon.png
│ │ │ ├── appearance-dark-icon.png
│ │ │ ├── appearance-light-icon.png
│ │ │ └── whats-new
│ │ │ │ ├── 5.0
│ │ │ │ ├── toolbar.png
│ │ │ │ ├── models-tab.png
│ │ │ │ ├── timeline.png
│ │ │ │ ├── clockwork-5.png
│ │ │ │ ├── client-metrics.png
│ │ │ │ └── notifications-tab.png
│ │ │ │ └── 5.1
│ │ │ │ └── database-queries.png
│ │ ├── manifest.json
│ │ └── index.html
│ └── Web.php
├── Support
│ ├── Symfony
│ │ ├── Resources
│ │ │ └── config
│ │ │ │ ├── routing
│ │ │ │ └── clockwork.php
│ │ │ │ └── clockwork.php
│ │ ├── ClockworkBundle.php
│ │ ├── ClockworkExtension.php
│ │ ├── ClockworkConfiguration.php
│ │ ├── ClockworkFactory.php
│ │ ├── ClockworkListener.php
│ │ ├── ClockworkLoader.php
│ │ ├── ClockworkController.php
│ │ └── ClockworkSupport.php
│ ├── Laravel
│ │ ├── Facade.php
│ │ ├── helpers.php
│ │ ├── Eloquent
│ │ │ ├── ResolveModelScope.php
│ │ │ └── ResolveModelLegacyScope.php
│ │ ├── ClockworkMiddleware.php
│ │ ├── ClockworkCleanCommand.php
│ │ ├── Console
│ │ │ ├── CapturingOldFormatter.php
│ │ │ ├── CapturingFormatter.php
│ │ │ └── CapturingLegacyFormatter.php
│ │ ├── ClockworkController.php
│ │ └── Tests
│ │ │ ├── UsesClockwork.php
│ │ │ └── ClockworkExtension.php
│ ├── Vanilla
│ │ ├── helpers.php
│ │ ├── iframe.html.php
│ │ └── ClockworkMiddleware.php
│ ├── Monolog
│ │ ├── Monolog
│ │ │ └── ClockworkHandler.php
│ │ ├── Monolog2
│ │ │ └── ClockworkHandler.php
│ │ └── Handler
│ │ │ └── ClockworkHandler.php
│ ├── Slim
│ │ ├── Old
│ │ │ ├── ClockworkLogWriter.php
│ │ │ └── ClockworkMiddleware.php
│ │ ├── Legacy
│ │ │ └── ClockworkMiddleware.php
│ │ └── ClockworkMiddleware.php
│ ├── Lumen
│ │ ├── ClockworkMiddleware.php
│ │ ├── ClockworkSupport.php
│ │ ├── Controller.php
│ │ └── ClockworkServiceProvider.php
│ ├── Twig
│ │ └── ProfilerClockworkDumper.php
│ └── Swift
│ │ └── SwiftPluginClockworkTimeline.php
├── Request
│ ├── RequestType.php
│ ├── LogLevel.php
│ ├── IncomingRequest.php
│ ├── UserData.php
│ ├── UserDataItem.php
│ ├── ShouldRecord.php
│ ├── Timeline
│ │ ├── Timeline.php
│ │ └── Event.php
│ ├── ShouldCollect.php
│ └── Log.php
├── Storage
│ ├── Storage.php
│ ├── StorageInterface.php
│ ├── SymfonyStorage.php
│ ├── Search.php
│ └── SqlSearch.php
├── Authentication
│ ├── AuthenticatorInterface.php
│ ├── NullAuthenticator.php
│ └── SimpleAuthenticator.php
├── DataSource
│ ├── DoctrineDataSource.php
│ ├── DataSourceInterface.php
│ ├── XdebugDataSource.php
│ ├── MonologDataSource.php
│ ├── SwiftDataSource.php
│ ├── TwigDataSource.php
│ ├── DataSource.php
│ ├── LaravelViewsDataSource.php
│ ├── Concerns
│ │ └── EloquentDetectDuplicateQueries.php
│ ├── LaravelQueueDataSource.php
│ ├── LaravelRedisDataSource.php
│ ├── PsrMessageDataSource.php
│ ├── SlimDataSource.php
│ ├── LaravelEventsDataSource.php
│ ├── LaravelCacheDataSource.php
│ ├── PhpDataSource.php
│ ├── LaravelHttpClientDataSource.php
│ ├── DBALDataSource.php
│ ├── GuzzleDataSource.php
│ ├── LumenDataSource.php
│ └── LaravelDataSource.php
├── Helpers
│ ├── StackFrame.php
│ ├── Concerns
│ │ └── ResolvesViewName.php
│ ├── ServerTiming.php
│ ├── StackTrace.php
│ ├── StackFilter.php
│ └── Serializer.php
└── Clockwork.php
├── .editorconfig
├── LICENSE
├── composer.json
└── README.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | .github/ export-ignore
--------------------------------------------------------------------------------
/Clockwork/Web/public/img/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/Clockwork/Web/public/img/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/Clockwork/Web/public/img/appearance-auto-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/appearance-auto-icon.png
--------------------------------------------------------------------------------
/Clockwork/Web/public/img/appearance-dark-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/appearance-dark-icon.png
--------------------------------------------------------------------------------
/Clockwork/Web/public/img/appearance-light-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/appearance-light-icon.png
--------------------------------------------------------------------------------
/Clockwork/Web/public/img/whats-new/5.0/toolbar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/whats-new/5.0/toolbar.png
--------------------------------------------------------------------------------
/Clockwork/Web/public/img/icons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/icons/apple-touch-icon.png
--------------------------------------------------------------------------------
/Clockwork/Web/public/img/whats-new/5.0/models-tab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/whats-new/5.0/models-tab.png
--------------------------------------------------------------------------------
/Clockwork/Web/public/img/whats-new/5.0/timeline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/whats-new/5.0/timeline.png
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_style = tab
7 |
8 | [*.php]
9 | insert_final_newline = true
10 |
--------------------------------------------------------------------------------
/Clockwork/Web/public/img/whats-new/5.0/clockwork-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/whats-new/5.0/clockwork-5.png
--------------------------------------------------------------------------------
/Clockwork/Web/public/img/icons/apple-touch-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/icons/apple-touch-icon-60x60.png
--------------------------------------------------------------------------------
/Clockwork/Web/public/img/icons/apple-touch-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/icons/apple-touch-icon-76x76.png
--------------------------------------------------------------------------------
/Clockwork/Web/public/img/whats-new/5.0/client-metrics.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/whats-new/5.0/client-metrics.png
--------------------------------------------------------------------------------
/Clockwork/Web/public/img/icons/apple-touch-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/icons/apple-touch-icon-120x120.png
--------------------------------------------------------------------------------
/Clockwork/Web/public/img/icons/apple-touch-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/icons/apple-touch-icon-152x152.png
--------------------------------------------------------------------------------
/Clockwork/Web/public/img/icons/apple-touch-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/icons/apple-touch-icon-180x180.png
--------------------------------------------------------------------------------
/Clockwork/Web/public/img/whats-new/5.0/notifications-tab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/whats-new/5.0/notifications-tab.png
--------------------------------------------------------------------------------
/Clockwork/Web/public/img/whats-new/5.1/database-queries.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quindecim-p/clockwork/HEAD/Clockwork/Web/public/img/whats-new/5.1/database-queries.png
--------------------------------------------------------------------------------
/Clockwork/Support/Symfony/Resources/config/routing/clockwork.php:
--------------------------------------------------------------------------------
1 | import('.', 'clockwork');
7 | };
8 |
--------------------------------------------------------------------------------
/Clockwork/Request/RequestType.php:
--------------------------------------------------------------------------------
1 | getConnection());
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Clockwork/Request/LogLevel.php:
--------------------------------------------------------------------------------
1 | debug($argument);
13 | }
14 |
15 | return reset($arguments);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Clockwork/Web/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Clockwork
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/Clockwork/Support/Vanilla/helpers.php:
--------------------------------------------------------------------------------
1 | dataSource = $dataSource;
16 | }
17 |
18 | public function apply(Builder $builder, Model $model)
19 | {
20 | $this->dataSource->nextQueryModel = get_class($model);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Clockwork/Support/Monolog/Monolog/ClockworkHandler.php:
--------------------------------------------------------------------------------
1 | clockworkLog = $clockworkLog;
18 | }
19 |
20 | protected function write(array $record)
21 | {
22 | $this->clockworkLog->log($record['level'], $record['message']);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Clockwork/Support/Monolog/Monolog2/ClockworkHandler.php:
--------------------------------------------------------------------------------
1 | clockworkLog = $clockworkLog;
18 | }
19 |
20 | protected function write(array $record): void
21 | {
22 | $this->clockworkLog->log($record['level'], $record['message']);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Clockwork/Support/Laravel/Eloquent/ResolveModelLegacyScope.php:
--------------------------------------------------------------------------------
1 | dataSource = $dataSource;
16 | }
17 |
18 | public function apply(Builder $builder, Model $model)
19 | {
20 | $this->dataSource->nextQueryModel = get_class($model);
21 | }
22 |
23 | public function remove(Builder $builder, Model $model)
24 | {
25 | // nothing to do here
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Clockwork/Support/Monolog/Handler/ClockworkHandler.php:
--------------------------------------------------------------------------------
1 | clockworkLog = $clockworkLog;
19 | }
20 |
21 | protected function write(array $record)
22 | {
23 | $this->clockworkLog->log($record['level'], $record['message']);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Clockwork/Authentication/SimpleAuthenticator.php:
--------------------------------------------------------------------------------
1 | password = $password;
10 | }
11 |
12 | public function attempt(array $credentials)
13 | {
14 | if (! isset($credentials['password'])) {
15 | return false;
16 | }
17 |
18 | if (! hash_equals($credentials['password'], $this->password)) {
19 | return false;
20 | }
21 |
22 | return password_hash($this->password, \PASSWORD_DEFAULT);
23 | }
24 |
25 | public function check($token)
26 | {
27 | return password_verify($this->password, $token);
28 | }
29 |
30 | public function requires()
31 | {
32 | return [ AuthenticatorInterface::REQUIRES_PASSWORD ];
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Clockwork/Support/Symfony/ClockworkExtension.php:
--------------------------------------------------------------------------------
1 | load('clockwork.php');
14 |
15 | $container->getDefinition(ClockworkSupport::class)->replaceArgument('$config', $config);
16 | }
17 |
18 | public function getConfiguration(array $config, ContainerBuilder $container)
19 | {
20 | return new ClockworkConfiguration($container->getParameter('kernel.debug'));
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Clockwork/DataSource/XdebugDataSource.php:
--------------------------------------------------------------------------------
1 | xdebug = [ 'profile' => xdebug_get_profiler_filename() ];
12 |
13 | return $request;
14 | }
15 |
16 | // Extends the request with full profiling data
17 | public function extend(Request $request)
18 | {
19 | $profile = isset($request->xdebug['profile']) ? $request->xdebug['profile'] : null;
20 |
21 | if ($profile && ! preg_match('/\.php$/', $profile) && is_readable($profile)) {
22 | $request->xdebug['profileData'] = file_get_contents($profile);
23 |
24 | if (preg_match('/\.gz$/', $profile)) {
25 | $request->xdebug['profileData'] = gzdecode($request->xdebug['profileData']);
26 | }
27 | }
28 |
29 | return $request;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Clockwork/Storage/StorageInterface.php:
--------------------------------------------------------------------------------
1 | $value) {
20 | $this->$key = $value;
21 | }
22 |
23 | $this->call = $this->formatCall();
24 |
25 | $this->shortPath = $this->file ? str_replace($basePath, '', $this->file) : null;
26 | $this->vendor = ($this->file && strpos($this->file, $vendorPath) === 0)
27 | ? explode(DIRECTORY_SEPARATOR, str_replace($vendorPath, '', $this->file))[0] : null;
28 | }
29 |
30 | protected function formatCall()
31 | {
32 | if ($this->class) {
33 | return "{$this->class}{$this->type}{$this->function}()";
34 | } else {
35 | return "{$this->function}()";
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Clockwork/Support/Slim/Old/ClockworkLogWriter.php:
--------------------------------------------------------------------------------
1 | 'emergency',
14 | 2 => 'alert',
15 | 3 => 'critical',
16 | 4 => 'error',
17 | 5 => 'warning',
18 | 6 => 'notice',
19 | 7 => 'info',
20 | 8 => 'debug'
21 | ];
22 |
23 | public function __construct(Clockwork $clockwork, $originalLogWriter)
24 | {
25 | $this->clockwork = $clockwork;
26 | $this->originalLogWriter = $originalLogWriter;
27 | }
28 |
29 | public function write($message, $level = null)
30 | {
31 | $this->clockwork->log($this->getPsrLevel($level), $message);
32 |
33 | if ($this->originalLogWriter) {
34 | return $this->originalLogWriter->write($message, $level);
35 | }
36 | }
37 |
38 | protected function getPsrLevel($level)
39 | {
40 | return isset($this->logLevels[$level]) ? $this->logLevels[$level] : $level;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Clockwork/Support/Lumen/ClockworkMiddleware.php:
--------------------------------------------------------------------------------
1 | app = $app;
16 | }
17 |
18 | // Handle an incoming request
19 | public function handle($request, \Closure $next)
20 | {
21 | $this->app['clockwork']->event('Controller')->begin();
22 |
23 | try {
24 | $response = $next($request);
25 | } catch (\Exception $e) {
26 | $this->app[ExceptionHandler::class]->report($e);
27 | $response = $this->app[ExceptionHandler::class]->render($request, $e);
28 | }
29 |
30 | return $this->app['clockwork.support']->processRequest($request, $response);
31 | }
32 |
33 | // Record the current request after a response is sent
34 | public function terminate()
35 | {
36 | $this->app['clockwork.support']->recordRequest();
37 | }
38 | }
--------------------------------------------------------------------------------
/Clockwork/Web/Web.php:
--------------------------------------------------------------------------------
1 | resolveAssetPath($path);
10 |
11 | if (! $path) return;
12 |
13 | switch (pathinfo($path, PATHINFO_EXTENSION)) {
14 | case 'css': $mime = 'text/css'; break;
15 | case 'js': $mime = 'application/javascript'; break;
16 | case 'json': $mime = 'application/json'; break;
17 | case 'png': $mime = 'image/png'; break;
18 | default: $mime = 'text/html'; break;
19 | }
20 |
21 | return [
22 | 'path' => $path,
23 | 'mime' => $mime
24 | ];
25 | }
26 |
27 | // Resolves absolute path of the asset, protects from accessing files outside Clockwork public dir
28 | protected function resolveAssetPath($path)
29 | {
30 | $publicPath = realpath(__DIR__ . '/public');
31 |
32 | $path = realpath("$publicPath/{$path}");
33 |
34 | return strpos($path, $publicPath) === 0 ? $path : false;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Clockwork/Support/Symfony/ClockworkConfiguration.php:
--------------------------------------------------------------------------------
1 | debug = $debug;
14 | }
15 |
16 | public function getConfigTreeBuilder()
17 | {
18 | return $this->getConfigRoot()
19 | ->children()
20 | ->booleanNode('enable')->defaultValue($this->debug)->end()
21 | ->variableNode('web')->defaultValue(true)->end()
22 | ->booleanNode('authentication')->defaultValue(false)->end()
23 | ->scalarNode('authentication_password')->defaultValue('VerySecretPassword')->end()
24 | ->end()
25 | ->end();
26 | }
27 |
28 | protected function getConfigRoot()
29 | {
30 | if (Kernel::VERSION_ID < 40300) {
31 | return (new TreeBuilder)->root('clockwork');
32 | }
33 |
34 | return (new TreeBuilder('clockwork'))->getRootNode();
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Clockwork/Support/Laravel/ClockworkMiddleware.php:
--------------------------------------------------------------------------------
1 | app = $app;
16 | }
17 |
18 | // Handle an incoming request
19 | public function handle($request, \Closure $next)
20 | {
21 | $this->app['clockwork']->event('Controller')->begin();
22 |
23 | try {
24 | $response = $next($request);
25 | } catch (\Exception $e) {
26 | $this->app[ExceptionHandler::class]->report($e);
27 | $response = $this->app[ExceptionHandler::class]->render($request, $e);
28 | }
29 |
30 | return $this->app['clockwork.support']->processRequest($request, $response);
31 | }
32 |
33 | // Record the current request after a response is sent
34 | public function terminate()
35 | {
36 | $this->app['clockwork.support']->recordRequest();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2013 Miroslav Rigler
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Clockwork/DataSource/MonologDataSource.php:
--------------------------------------------------------------------------------
1 | log = new Log;
20 |
21 | $handler = Logger::API == 1 ? new LegacyClockworkHandler($this->log) : new ClockworkHandler($this->log);
22 |
23 | $monolog->pushHandler($handler);
24 | }
25 |
26 | // Adds log entries to the request
27 | public function resolve(Request $request)
28 | {
29 | $request->log()->merge($this->log);
30 |
31 | return $request;
32 | }
33 |
34 | // Reset the data source to an empty state, clearing any collected data
35 | public function reset()
36 | {
37 | $this->log = new Log;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Clockwork/Support/Symfony/ClockworkFactory.php:
--------------------------------------------------------------------------------
1 | container = $container;
17 | $this->profiler = $profiler;
18 | }
19 |
20 | public function clockwork()
21 | {
22 | return (new Clockwork)
23 | ->authenticator($this->container->get('clockwork.authenticator'))
24 | ->storage($this->container->get('clockwork.storage'));
25 | }
26 |
27 | public function clockworkAuthenticator()
28 | {
29 | return $this->container->get('clockwork.support')->makeAuthenticator();
30 | }
31 |
32 | public function clockworkStorage()
33 | {
34 | return new SymfonyStorage(
35 | $this->profiler, substr($this->container->getParameter('profiler.storage.dsn'), 5)
36 | );
37 | }
38 |
39 | public function clockworkSupport($config)
40 | {
41 | return new ClockworkSupport($this->container, $config);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Clockwork/Request/IncomingRequest.php:
--------------------------------------------------------------------------------
1 | $val) $this->$key = $val;
25 | }
26 |
27 | // Returns a header value, or default if not set
28 | public function header($key, $default = null)
29 | {
30 | return isset($this->headers[$key]) ? $this->headers[$key] : $default;
31 | }
32 |
33 | // Returns an input value, or default if not set
34 | public function input($key, $default = null)
35 | {
36 | return isset($this->input[$key]) ? $this->input[$key] : $default;
37 | }
38 |
39 | // Returns true, if HTTP host is one of the common domains used for local development
40 | public function hasLocalHost()
41 | {
42 | $segments = explode('.', $this->host);
43 | $tld = $segments[count($segments) - 1];
44 |
45 | return $this->host == '127.0.0.1'
46 | || in_array($tld, [ 'localhost', 'local', 'test', 'wip' ]);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Clockwork/DataSource/SwiftDataSource.php:
--------------------------------------------------------------------------------
1 | swift = $swift;
22 |
23 | $this->timeline = new Timeline;
24 | }
25 |
26 | // Listen to the email events
27 | public function listenToEvents()
28 | {
29 | $this->swift->registerPlugin(new SwiftPluginClockworkTimeline($this->timeline));
30 | }
31 |
32 | // Adds send emails to the request
33 | public function resolve(Request $request)
34 | {
35 | $request->emailsData = array_merge($request->emailsData, $this->timeline->finalize());
36 |
37 | return $request;
38 | }
39 |
40 | // Reset the data source to an empty state, clearing any collected data
41 | public function reset()
42 | {
43 | $this->timeline = new Timeline;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Clockwork/Helpers/Concerns/ResolvesViewName.php:
--------------------------------------------------------------------------------
1 | first(function ($frame) {
12 | return $frame->shortPath ? preg_match('#^/storage/framework/views/[a-z0-9]+\.php$#', $frame->shortPath) : false;
13 | });
14 |
15 | if (! $viewFrame) return $this;
16 |
17 | $renderFrame = $this->first(function ($frame) {
18 | return $frame->call == 'Illuminate\View\View->getContents()'
19 | && $frame->object instanceof \Illuminate\View\View;
20 | });
21 |
22 | if (! $renderFrame) return $this;
23 |
24 | $resolvedViewFrame = new StackFrame(
25 | [ 'file' => $renderFrame->object->getPath(), 'line' => $viewFrame->line ],
26 | $this->basePath,
27 | $this->vendorPath
28 | );
29 |
30 | return $this->copy(array_merge(
31 | array_slice($this->frames, 0, array_search($viewFrame, $this->frames)),
32 | [ $resolvedViewFrame ],
33 | array_slice($this->frames, array_search($viewFrame, $this->frames) + 2)
34 | ));
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Clockwork/Request/UserData.php:
--------------------------------------------------------------------------------
1 | data[$key] = new UserDataItem($data);
17 | }
18 |
19 | return $this->data[] = new UserDataItem($data);
20 | }
21 |
22 | // Add user data shown as counters
23 | public function counters(array $data)
24 | {
25 | return $this->data($data)
26 | ->showAs('counters');
27 | }
28 |
29 | // Add user data shown as table
30 | public function table($title, array $data)
31 | {
32 | return $this->data($data)
33 | ->showAs('table')
34 | ->title($title);
35 | }
36 |
37 | // Set data title
38 | public function title($title)
39 | {
40 | $this->title = $title;
41 | return $this;
42 | }
43 |
44 | // Transform data and all contents to a serializable array with metadata
45 | public function toArray()
46 | {
47 | return array_merge(
48 | array_map(function ($data) { return $data->toArray(); }, $this->data),
49 | [ '__meta' => array_filter([ 'title' => $this->title ]) ]
50 | );
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Clockwork/DataSource/TwigDataSource.php:
--------------------------------------------------------------------------------
1 | twig = $twig;
24 | }
25 |
26 | // Register the Twig profiler extension
27 | public function listenToEvents()
28 | {
29 | if (class_exists(ProfilerExtension::class)) {
30 | $this->twig->addExtension(new ProfilerExtension(($this->profile = new Profile())));
31 | } else {
32 | $this->twig->addExtension(new Twig_Extension_Profiler($this->profile = new Twig_Profiler_Profile));
33 | }
34 | }
35 |
36 | // Adds rendered views to the request
37 | public function resolve(Request $request)
38 | {
39 | $timeline = (new ProfilerClockworkDumper)->dump($this->profile);
40 |
41 | $request->viewsData = array_merge($request->viewsData, $timeline->finalize());
42 |
43 | return $request;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Clockwork/Support/Twig/ProfilerClockworkDumper.php:
--------------------------------------------------------------------------------
1 | dumpProfile($profile, $timeline);
16 |
17 | return $timeline;
18 | }
19 |
20 | public function dumpProfile($profile, Timeline $timeline, $parent = null)
21 | {
22 | $id = $this->lastId++;
23 |
24 | if ($profile->isRoot()) {
25 | $name = $profile->getName();
26 | } elseif ($profile->isTemplate()) {
27 | $name = basename($profile->getTemplate());
28 | } else {
29 | $name = basename($profile->getTemplate()) . '::' . $profile->getType() . '(' . $profile->getName() . ')';
30 | }
31 |
32 | foreach ($profile as $p) {
33 | $this->dumpProfile($p, $timeline, $id);
34 | }
35 |
36 | $data = $profile->__serialize();
37 |
38 | $timeline->event($name, [
39 | 'name' => $id,
40 | 'start' => isset($data[3]['wt']) ? $data[3]['wt'] : null,
41 | 'end' => isset($data[4]['wt']) ? $data[4]['wt'] : null,
42 | 'data' => [
43 | 'data' => [],
44 | 'memoryUsage' => isset($data[4]['mu']) ? $data[4]['mu'] : null,
45 | 'parent' => $parent
46 | ]
47 | ]);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Clockwork/Helpers/ServerTiming.php:
--------------------------------------------------------------------------------
1 | metrics[] = [ 'metric' => $metric, 'value' => $value, 'description' => $description ];
15 |
16 | return $this;
17 | }
18 |
19 | // Generate the header value
20 | public function value()
21 | {
22 | return implode(', ', array_map(function ($metric) {
23 | return "{$metric['metric']}; dur={$metric['value']}; desc=\"{$metric['description']}\"";
24 | }, $this->metrics));
25 | }
26 |
27 | // Create a new instance from a Clockwork request
28 | public static function fromRequest(Request $request, $eventsCount = 10)
29 | {
30 | $header = new static;
31 |
32 | $header->add('app', $request->getResponseDuration(), 'Application');
33 |
34 | if ($request->getDatabaseDuration()) {
35 | $header->add('db', $request->getDatabaseDuration(), 'Database');
36 | }
37 |
38 | // add timeline events limited to a set number so the header doesn't get too large
39 | foreach (array_slice($request->timeline()->events, 0, $eventsCount) as $i => $event) {
40 | $header->add("timeline-event-{$i}", $event->duration(), $event->description);
41 | }
42 |
43 | return $header;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Clockwork/Support/Laravel/ClockworkCleanCommand.php:
--------------------------------------------------------------------------------
1 | option('all')) {
33 | $this->laravel['config']->set('clockwork.storage_expiration', 0);
34 | } elseif ($expiration = $this->option('expiration')) {
35 | $this->laravel['config']->set('clockwork.storage_expiration', $expiration);
36 | }
37 |
38 | $this->laravel['clockwork.support']->makeStorage()->cleanup($force = true);
39 |
40 | $this->info('Metadata cleaned successfully.');
41 | }
42 |
43 | // Compatibility for the old Laravel versions
44 | public function fire()
45 | {
46 | return $this->handle();
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Clockwork/Request/UserDataItem.php:
--------------------------------------------------------------------------------
1 | data = $data;
21 | }
22 |
23 | // Set how the item should be presented ("counters" or "table")
24 | public function showAs($showAs)
25 | {
26 | $this->showAs = $showAs;
27 | return $this;
28 | }
29 |
30 | // Set data title (shown as table title in the official app)
31 | public function title($title)
32 | {
33 | $this->title = $title;
34 | return $this;
35 | }
36 |
37 | // Set a map of human-readable labels for the data contents
38 | public function labels($labels)
39 | {
40 | $this->labels = $labels;
41 | return $this;
42 | }
43 |
44 | // Transform contents to a serializable array with metadata
45 | public function toArray()
46 | {
47 | return array_merge($this->data, [
48 | '__meta' => array_filter([
49 | 'showAs' => $this->showAs,
50 | 'title' => $this->title,
51 | 'labels' => $this->labels
52 | ])
53 | ]);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Clockwork/Support/Symfony/ClockworkListener.php:
--------------------------------------------------------------------------------
1 | clockwork = $clockwork;
18 | $this->profiler = $profiler;
19 | }
20 |
21 | public function onKernelRequest(KernelEvent $event)
22 | {
23 | if (preg_match('#/__clockwork(.*)#', $event->getRequest()->getPathInfo())) {
24 | $this->profiler->disable();
25 | }
26 | }
27 |
28 | public function onKernelResponse(KernelEvent $event)
29 | {
30 | if (! $this->clockwork->isEnabled()) return;
31 |
32 | $response = $event->getResponse();
33 |
34 | if (! $response->headers->has('X-Debug-Token')) return;
35 |
36 | $response->headers->set('X-Clockwork-Id', $response->headers->get('X-Debug-Token'));
37 | $response->headers->set('X-Clockwork-Version', Clockwork::VERSION);
38 | }
39 |
40 | public static function getSubscribedEvents()
41 | {
42 | return [
43 | KernelEvents::REQUEST => [ 'onKernelRequest', 512 ],
44 | KernelEvents::RESPONSE => [ 'onKernelResponse', -128 ]
45 | ];
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "itsgoingd/clockwork",
3 | "description": "php dev tools in your browser",
4 | "keywords": ["debugging", "profiling", "logging", "laravel", "lumen", "slim", "devtools"],
5 | "homepage": "https://underground.works/clockwork",
6 | "license": "MIT",
7 | "authors": [
8 | {
9 | "name": "itsgoingd",
10 | "email": "itsgoingd@luzer.sk",
11 | "homepage": "https://twitter.com/itsgoingd"
12 | }
13 | ],
14 | "require": {
15 | "php": ">=5.6",
16 | "ext-json": "*"
17 | },
18 | "suggest": {
19 | "ext-pdo": "Needed in order to use a SQL database for metadata storage",
20 | "ext-pdo_sqlite": "Needed in order to use a SQLite for metadata storage",
21 | "ext-pdo_mysql": "Needed in order to use MySQL for metadata storage",
22 | "ext-pdo_postgres": "Needed in order to use Postgres for metadata storage",
23 | "ext-redis": "Needed in order to use Redis for metadata storage",
24 | "php-http/discovery": "Vanilla integration - required for the middleware zero-configuration setup"
25 | },
26 | "autoload": {
27 | "psr-4": {
28 | "Clockwork\\": "Clockwork/"
29 | }
30 | },
31 | "extra": {
32 | "laravel": {
33 | "providers": [
34 | "Clockwork\\Support\\Laravel\\ClockworkServiceProvider"
35 | ],
36 | "aliases": {
37 | "Clockwork": "Clockwork\\Support\\Laravel\\Facade"
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Clockwork/Request/ShouldRecord.php:
--------------------------------------------------------------------------------
1 | $val) $this->$key = $val;
18 | }
19 |
20 | // Apply the filter to a request
21 | public function filter(Request $request)
22 | {
23 | return $this->passErrorsOnly($request)
24 | && $this->passSlowOnly($request)
25 | && $this->passCallback($request);
26 | }
27 |
28 | protected function passErrorsOnly(Request $request)
29 | {
30 | if (! $this->errorsOnly) return true;
31 |
32 | return 400 <= $request->responseStatus && $request->responseStatus <= 599;
33 | }
34 |
35 | protected function passSlowOnly(Request $request)
36 | {
37 | if (! $this->slowOnly) return true;
38 |
39 | return $request->getResponseDuration() >= $this->slowOnly;
40 | }
41 |
42 | protected function passCallback(Request $request)
43 | {
44 | if (! $this->callback) return true;
45 |
46 | return call_user_func($this->callback, $request);
47 | }
48 |
49 | // Fluent API
50 | public function __call($method, $parameters)
51 | {
52 | if (! count($parameters)) return $this->$method;
53 |
54 | $this->$method = count($parameters) ? $parameters[0] : true;
55 |
56 | return $this;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Clockwork/Support/Vanilla/iframe.html.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Clockwork
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
23 |
24 |
25 |
26 |
27 |
36 |
37 |
--------------------------------------------------------------------------------
/Clockwork/Storage/SymfonyStorage.php:
--------------------------------------------------------------------------------
1 | profiler = $profiler;
21 | $this->path = $path;
22 | }
23 |
24 | // Store request, no-op since this is read-only storage implementation
25 | public function store(Request $request, $skipIndex = false)
26 | {
27 | return;
28 | }
29 |
30 | // Cleanup old requests, no-op since this is read-only storage implementation
31 | public function cleanup($force = false)
32 | {
33 | return;
34 | }
35 |
36 | protected function loadRequest($token)
37 | {
38 | return ($profile = $this->profiler->loadProfile($token)) ? (new ProfileTransformer)->transform($profile) : null;
39 | }
40 |
41 | // Open index file, optionally move file pointer to the end
42 | protected function openIndex($position = 'start', $lock = null, $force = null)
43 | {
44 | $this->indexHandle = fopen("{$this->path}/index.csv", 'r');
45 |
46 | if ($position == 'end') fseek($this->indexHandle, 0, SEEK_END);
47 | }
48 |
49 | protected function makeRequestFromIndex($record)
50 | {
51 | return new Request(array_combine(
52 | [ 'id', 'method', 'uri', 'time', 'parent', 'responseStatus' ],
53 | [ $record[0], $record[2], $record[3], $record[4], $record[5], $record[6] ]
54 | ));
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Clockwork/Support/Symfony/ClockworkLoader.php:
--------------------------------------------------------------------------------
1 | support = $support;
14 | }
15 |
16 | public function load($resource, $type = null)
17 | {
18 | $routes = new RouteCollection();
19 |
20 | $routes->add('clockwork', new Route('/__clockwork/{id}/{direction}/{count}', [
21 | '_controller' => [ ClockworkController::class, 'getData' ],
22 | 'direction' => null,
23 | 'count' => null
24 | ], [ 'id' => '(?!(app|auth))([a-z0-9-]+|latest)', 'direction' => '(next|previous)', 'count' => '\d+' ]));
25 |
26 | $routes->add('clockwork.auth', new Route('/__clockwork/auth', [
27 | '_controller' => [ ClockworkController::class, 'authenticate' ]
28 | ]));
29 |
30 | if (! $this->support->isWebEnabled()) return $routes;
31 |
32 | foreach ($this->support->webPaths() as $path) {
33 | $routes->add("clockwork.webRedirect.{$path}", new Route("{$path}", [
34 | '_controller' => [ ClockworkController::class, 'webRedirect' ]
35 | ]));
36 |
37 | $routes->add("clockwork.webIndex.{$path}", new Route("{$path}/app", [
38 | '_controller' => [ ClockworkController::class, 'webIndex' ]
39 | ]));
40 |
41 | $routes->add("clockwork.webAsset.{$path}", new Route("{$path}/{path}", [
42 | '_controller' => [ ClockworkController::class, 'webAsset' ]
43 | ], [ 'path' => '.+' ]));
44 | }
45 |
46 | return $routes;
47 | }
48 |
49 | public function supports($resource, $type = null)
50 | {
51 | return $type == 'clockwork';
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Clockwork/Support/Laravel/Console/CapturingOldFormatter.php:
--------------------------------------------------------------------------------
1 | formatter = $formatter;
16 | }
17 |
18 | public function capturedOutput()
19 | {
20 | $capturedOutput = $this->capturedOutput;
21 |
22 | $this->capturedOutput = null;
23 |
24 | return $capturedOutput;
25 | }
26 |
27 | public function setDecorated($decorated)
28 | {
29 | return $this->formatter->setDecorated($decorated);
30 | }
31 |
32 | public function isDecorated()
33 | {
34 | return $this->formatter->isDecorated();
35 | }
36 |
37 | public function setStyle($name, OutputFormatterStyleInterface $style)
38 | {
39 | return $this->formatter->setStyle($name, $style);
40 | }
41 |
42 | public function hasStyle($name)
43 | {
44 | return $this->formatter->hasStyle($name);
45 | }
46 |
47 | public function getStyle($name)
48 | {
49 | return $this->formatter->getStyle($name);
50 | }
51 |
52 | public function format($message)
53 | {
54 | $formatted = $this->formatter->format($message);
55 |
56 | $this->capturedOutput .= $formatted;
57 |
58 | return $formatted;
59 | }
60 |
61 | public function __call($method, $args)
62 | {
63 | return $this->formatter->$method(...$args);
64 | }
65 |
66 | public function __clone()
67 | {
68 | $this->formatter = clone $this->formatter;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Clockwork/Support/Swift/SwiftPluginClockworkTimeline.php:
--------------------------------------------------------------------------------
1 | timeline = $timeline;
17 | }
18 |
19 | // Invoked immediately before a message is sent
20 | public function beforeSendPerformed(Swift_Events_SendEvent $evt)
21 | {
22 | $message = $evt->getMessage();
23 |
24 | $headers = [];
25 | foreach ($message->getHeaders()->getAll() as $header) {
26 | $headers[$header->getFieldName()] = $header->getFieldBody();
27 | }
28 |
29 | $this->timeline->event('Sending an email message', [
30 | 'name' => 'email ' . $message->getId(),
31 | 'start' => $time = microtime(true),
32 | 'data' => [
33 | 'from' => $this->addressToString($message->getFrom()),
34 | 'to' => $this->addressToString($message->getTo()),
35 | 'subject' => $message->getSubject(),
36 | 'headers' => $headers
37 | ]
38 | ]);
39 | }
40 |
41 | // Invoked immediately after a message is sent
42 | public function sendPerformed(Swift_Events_SendEvent $evt)
43 | {
44 | $message = $evt->getMessage();
45 |
46 | $this->timeline->event('email ' . $message->getId())->end();
47 | }
48 |
49 | protected function addressToString($address)
50 | {
51 | if (! $address) return;
52 |
53 | foreach ($address as $email => $name) {
54 | $address[$email] = $name ? "$name <$email>" : $email;
55 | }
56 |
57 | return implode(', ', $address);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Clockwork/Support/Laravel/Console/CapturingFormatter.php:
--------------------------------------------------------------------------------
1 | formatter = $formatter;
16 | }
17 |
18 | public function capturedOutput()
19 | {
20 | $capturedOutput = $this->capturedOutput;
21 |
22 | $this->capturedOutput = null;
23 |
24 | return $capturedOutput;
25 | }
26 |
27 | public function setDecorated(bool $decorated): void
28 | {
29 | $this->formatter->setDecorated($decorated);
30 | }
31 |
32 | public function isDecorated(): bool
33 | {
34 | return $this->formatter->isDecorated();
35 | }
36 |
37 | public function setStyle(string $name, OutputFormatterStyleInterface $style): void
38 | {
39 | $this->formatter->setStyle($name, $style);
40 | }
41 |
42 | public function hasStyle(string $name): bool
43 | {
44 | return $this->formatter->hasStyle($name);
45 | }
46 |
47 | public function getStyle(string $name): OutputFormatterStyleInterface
48 | {
49 | return $this->formatter->getStyle($name);
50 | }
51 |
52 | public function format(?string $message): ?string
53 | {
54 | $formatted = $this->formatter->format($message);
55 |
56 | $this->capturedOutput .= $formatted;
57 |
58 | return $formatted;
59 | }
60 |
61 | public function __call($method, $args)
62 | {
63 | return $this->formatter->$method(...$args);
64 | }
65 |
66 | public function __clone()
67 | {
68 | $this->formatter = clone $this->formatter;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Clockwork/Support/Laravel/Console/CapturingLegacyFormatter.php:
--------------------------------------------------------------------------------
1 | formatter = $formatter;
16 | }
17 |
18 | public function capturedOutput()
19 | {
20 | $capturedOutput = $this->capturedOutput;
21 |
22 | $this->capturedOutput = null;
23 |
24 | return $capturedOutput;
25 | }
26 |
27 | public function setDecorated(bool $decorated)
28 | {
29 | return $this->formatter->setDecorated($decorated);
30 | }
31 |
32 | public function isDecorated(): bool
33 | {
34 | return $this->formatter->isDecorated();
35 | }
36 |
37 | public function setStyle(string $name, OutputFormatterStyleInterface $style)
38 | {
39 | return $this->formatter->setStyle($name, $style);
40 | }
41 |
42 | public function hasStyle(string $name): bool
43 | {
44 | return $this->formatter->hasStyle($name);
45 | }
46 |
47 | public function getStyle(string $name): OutputFormatterStyleInterface
48 | {
49 | return $this->formatter->getStyle($name);
50 | }
51 |
52 | public function format(?string $message): ?string
53 | {
54 | $formatted = $this->formatter->format($message);
55 |
56 | $this->capturedOutput .= $formatted;
57 |
58 | return $formatted;
59 | }
60 |
61 | public function __call($method, $args)
62 | {
63 | return $this->formatter->$method(...$args);
64 | }
65 |
66 | public function __clone()
67 | {
68 | $this->formatter = clone $this->formatter;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Clockwork/DataSource/DataSource.php:
--------------------------------------------------------------------------------
1 | filters[$type] = isset($this->filters[$type])
32 | ? array_merge($this->filters[$type], [ $filter ]) : [ $filter ];
33 |
34 | return $this;
35 | }
36 |
37 | // Clear all registered filters
38 | public function clearFilters()
39 | {
40 | $this->filters = [];
41 |
42 | return $this;
43 | }
44 |
45 | // Returns boolean whether the filterable passes all registered filters
46 | protected function passesFilters($args, $type = 'default')
47 | {
48 | $filters = isset($this->filters[$type]) ? $this->filters[$type] : [];
49 |
50 | foreach ($filters as $filter) {
51 | if (! $filter(...$args)) return false;
52 | }
53 |
54 | return true;
55 | }
56 |
57 | // Censors passwords in an array, identified by key containing "pass" substring
58 | public function removePasswords(array $data)
59 | {
60 | $keys = array_keys($data);
61 | $values = array_map(function ($value, $key) {
62 | return strpos($key, 'pass') !== false ? '*removed*' : $value;
63 | }, $data, $keys);
64 |
65 | return array_combine($keys, $values);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Clockwork/Support/Symfony/Resources/config/clockwork.php:
--------------------------------------------------------------------------------
1 | register(Clockwork\Support\Symfony\ClockworkFactory::class)
8 | ->setArgument('$container', new Reference('service_container'))
9 | ->setArgument('$profiler', new Reference('profiler'));
10 |
11 | $container->register(Clockwork\Clockwork::class)
12 | ->setFactory([ new Reference(ClockworkFactory::class), 'clockwork' ])
13 | ->setPublic(true);
14 |
15 | $container->register(Clockwork\Authentication\AuthenticatorInterface::class)
16 | ->setFactory([ new Reference(ClockworkFactory::class), 'clockworkAuthenticator' ])
17 | ->setPublic(true);
18 |
19 | $container->register(Clockwork\Storage\StorageInterface::class)
20 | ->setFactory([ new Reference(ClockworkFactory::class), 'clockworkStorage' ])
21 | ->setPublic(true);
22 |
23 | $container->register(Clockwork\Support\Symfony\ClockworkSupport::class)
24 | ->setArgument('$config', [])
25 | ->setFactory([ new Reference(ClockworkFactory::class), 'clockworkSupport' ])
26 | ->setPublic(true);
27 |
28 | $container->autowire(Clockwork\Support\Symfony\ClockworkController::class)
29 | ->setAutoconfigured(true);
30 |
31 | $container->autowire(Clockwork\Support\Symfony\ClockworkListener::class)
32 | ->setArgument('$profiler', new Reference('profiler'))
33 | ->addTag('kernel.event_subscriber');
34 |
35 | $container->autowire(Clockwork\Support\Symfony\ClockworkLoader::class)
36 | ->addTag('routing.loader');
37 |
38 | $container->setAlias('clockwork', Clockwork\Clockwork::class)->setPublic('true');
39 | $container->setAlias('clockwork.authenticator', Clockwork\Authentication\AuthenticatorInterface::class)->setPublic('true');
40 | $container->setAlias('clockwork.storage', Clockwork\Storage\StorageInterface::class)->setPublic('true');
41 | $container->setAlias('clockwork.support', Clockwork\Support\Symfony\ClockworkSupport::class)->setPublic('true');
42 |
--------------------------------------------------------------------------------
/Clockwork/Support/Symfony/ClockworkController.php:
--------------------------------------------------------------------------------
1 | clockwork = $clockwork;
17 | $this->support = $support;
18 | }
19 |
20 | public function authenticate(Request $request)
21 | {
22 | $this->ensureClockworkIsEnabled();
23 |
24 | $token = $this->clockwork->authenticator()->attempt($request->request->all());
25 |
26 | return new JsonResponse([ 'token' => $token ], $token ? 200 : 403);
27 | }
28 |
29 | public function getData(Request $request, $id = null, $direction = null, $count = null)
30 | {
31 | $this->ensureClockworkIsEnabled();
32 |
33 | return $this->support->getData($request, $id, $direction, $count);
34 | }
35 |
36 | public function webIndex(Request $request)
37 | {
38 | $this->ensureClockworkIsEnabled();
39 | $this->ensureClockworkWebIsEnabled();
40 |
41 | return $this->support->getWebAsset('index.html');
42 | }
43 |
44 | public function webAsset($path)
45 | {
46 | $this->ensureClockworkIsEnabled();
47 | $this->ensureClockworkWebIsEnabled();
48 |
49 | return $this->support->getWebAsset($path);
50 | }
51 |
52 | public function webRedirect(Request $request)
53 | {
54 | $this->ensureClockworkIsEnabled();
55 | $this->ensureClockworkWebIsEnabled();
56 |
57 | $path = $this->support->webPaths()[0];
58 |
59 | return $this->redirectToRoute("clockwork.webIndex.{$path}");
60 | }
61 |
62 | protected function ensureClockworkIsEnabled()
63 | {
64 | if (! $this->support->isEnabled()) throw $this->createNotFoundException();
65 | }
66 |
67 | protected function ensureClockworkWebIsEnabled()
68 | {
69 | if (! $this->support->isWebEnabled()) throw $this->createNotFoundException();
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Clockwork/DataSource/LaravelViewsDataSource.php:
--------------------------------------------------------------------------------
1 | dispatcher = $dispatcher;
26 |
27 | $this->collectData = $collectData;
28 |
29 | $this->views = new Timeline;
30 | }
31 |
32 | // Adds rendered views to the request
33 | public function resolve(Request $request)
34 | {
35 | $request->viewsData = array_merge($request->viewsData, $this->views->finalize());
36 |
37 | return $request;
38 | }
39 |
40 | // Reset the data source to an empty state, clearing any collected data
41 | public function reset()
42 | {
43 | $this->views = new Timeline;
44 | }
45 |
46 | // Listen to the views events
47 | public function listenToEvents()
48 | {
49 | $this->dispatcher->listen('composing:*', function ($view, $data = null) {
50 | if (is_string($view) && is_array($data)) { // Laravel 5.4 wildcard event
51 | $view = $data[0];
52 | }
53 |
54 | $data = array_filter(
55 | $this->collectData ? $view->getData() : [],
56 | function ($v, $k) { return strpos($k, '__') !== 0; },
57 | \ARRAY_FILTER_USE_BOTH
58 | );
59 |
60 | $this->views->event('Rendering a view', [
61 | 'name' => 'view ' . $view->getName(),
62 | 'start' => $time = microtime(true),
63 | 'end' => $time,
64 | 'data' => [
65 | 'name' => $view->getName(),
66 | 'data' => (new Serializer)->normalize($data)
67 | ]
68 | ]);
69 | });
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Clockwork/Request/Timeline/Timeline.php:
--------------------------------------------------------------------------------
1 | create($event['description'], $event);
14 | }
15 | }
16 |
17 | // Find or create a new event, takes description and optional data - name, start, end, duration, color, data
18 | public function event($description, $data = [])
19 | {
20 | $name = isset($data['name']) ? $data['name'] : $description;
21 |
22 | if ($event = $this->find($name)) return $event;
23 |
24 | return $this->create($description, $data);
25 | }
26 |
27 | // Create a new event, takes description and optional data - name, start, end, duration, color, data
28 | public function create($description, $data = [])
29 | {
30 | return $this->events[] = new Event($description, $data);
31 | }
32 |
33 | // Find event by name
34 | public function find($name)
35 | {
36 | foreach ($this->events as $event) {
37 | if ($event->name == $name) return $event;
38 | }
39 | }
40 |
41 | // Merge another timeline instance into the current timeline
42 | public function merge(Timeline $timeline)
43 | {
44 | $this->events = array_merge($this->events, $timeline->events);
45 |
46 | return $this;
47 | }
48 |
49 | // Finalize timeline, ends all events, sorts them and returns as an array
50 | public function finalize($start = null, $end = null)
51 | {
52 | foreach ($this->events as $event) {
53 | $event->finalize($start, $end);
54 | }
55 |
56 | $this->sort();
57 |
58 | return $this->toArray();
59 | }
60 |
61 | // Sort the timeline events by start time
62 | public function sort()
63 | {
64 | usort($this->events, function ($a, $b) { return $a->start * 1000 - $b->start * 1000; });
65 | }
66 |
67 | // Return events as an array
68 | public function toArray()
69 | {
70 | return array_map(function ($event) { return $event->toArray(); }, $this->events);
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Clockwork/Support/Lumen/ClockworkSupport.php:
--------------------------------------------------------------------------------
1 | app = $app;
17 | }
18 |
19 | // Resolves the framework data source from the container
20 | protected function frameworkDataSource()
21 | {
22 | return $this->app['clockwork.lumen'];
23 | }
24 |
25 | // Process an http request and response, resolves the request, sets Clockwork headers and cookies
26 | public function process($request, $response)
27 | {
28 | if (! $response instanceof Response) {
29 | $response = new Response((string) $response);
30 | }
31 |
32 | return parent::process($request, $response);
33 | }
34 |
35 | // Set response on the framework data source
36 | protected function setResponse($response)
37 | {
38 | $this->app['clockwork.lumen']->setResponse($response);
39 | }
40 |
41 | // Check whether Clockwork is enabled
42 | public function isEnabled()
43 | {
44 | return $this->getConfig('enable')
45 | || $this->getConfig('enable') === null && env('APP_DEBUG', false);
46 | }
47 |
48 | // Check whether a particular feature is available
49 | public function isFeatureAvailable($feature)
50 | {
51 | if ($feature == 'database') {
52 | return $this->app->bound('db') && $this->app['config']->get('database.default');
53 | } elseif ($feature == 'emails') {
54 | return $this->app->bound('mailer');
55 | } elseif ($feature == 'redis') {
56 | return $this->app->bound('redis') && method_exists(\Illuminate\Redis\RedisManager::class, 'enableEvents');
57 | } elseif ($feature == 'queue') {
58 | return $this->app->bound('queue') && method_exists(\Illuminate\Queue\Queue::class, 'createPayloadUsing');
59 | } elseif ($feature == 'xdebug') {
60 | return in_array('xdebug', get_loaded_extensions());
61 | }
62 |
63 | return true;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Clockwork/DataSource/Concerns/EloquentDetectDuplicateQueries.php:
--------------------------------------------------------------------------------
1 | duplicateQueries as $query) {
19 | if ($query['count'] <= 1) continue;
20 |
21 | $log->warning(
22 | "N+1 queries: {$query['model']}::{$query['relation']} loaded {$query['count']} times.",
23 | [ 'performance' => true, 'trace' => $query['trace'] ]
24 | );
25 | }
26 |
27 | $request->log()->merge($log);
28 | }
29 |
30 | protected function detectDuplicateQuery(StackTrace $trace)
31 | {
32 | $relationFrame = $trace->first(function ($frame) {
33 | return $frame->function == 'getRelationValue'
34 | || $frame->class == \Illuminate\Database\Eloquent\Relations\Relation::class;
35 | });
36 |
37 | if (! $relationFrame || ! $relationFrame->object) return;
38 |
39 | if ($relationFrame->class == \Illuminate\Database\Eloquent\Relations\Relation::class) {
40 | $model = get_class($relationFrame->object->getParent());
41 | $relation = get_class($relationFrame->object->getRelated());
42 | } else {
43 | $model = get_class($relationFrame->object);
44 | $relation = $relationFrame->args[0];
45 | }
46 |
47 | $shortTrace = $trace->skip(StackFilter::make()
48 | ->isNotVendor([ 'itsgoingd', 'laravel', 'illuminate' ])
49 | ->isNotNamespace([ 'Clockwork', 'Illuminate' ]));
50 |
51 | $hash = implode('-', [ $model, $relation, $shortTrace->first()->file, $shortTrace->first()->line ]);
52 |
53 | if (! isset($this->duplicateQueries[$hash])) {
54 | $this->duplicateQueries[$hash] = [
55 | 'count' => 0,
56 | 'model' => $model,
57 | 'relation' => $relation,
58 | 'trace' => $trace
59 | ];
60 | }
61 |
62 | $this->duplicateQueries[$hash]['count']++;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Clockwork/DataSource/LaravelQueueDataSource.php:
--------------------------------------------------------------------------------
1 | queue = $queue;
25 | }
26 |
27 | // Adds dispatched queue jobs to the request
28 | public function resolve(Request $request)
29 | {
30 | $request->queueJobs = array_merge($request->queueJobs, $this->getJobs());
31 |
32 | return $request;
33 | }
34 |
35 | // Reset the data source to an empty state, clearing any collected data
36 | public function reset()
37 | {
38 | $this->jobs = [];
39 | }
40 |
41 | // Listen to the queue events
42 | public function listenToEvents()
43 | {
44 | $this->queue->createPayloadUsing(function ($connection, $queue, $payload) {
45 | $this->registerJob([
46 | 'id' => $id = (new Request)->id,
47 | 'connection' => $connection,
48 | 'queue' => $queue,
49 | 'name' => $payload['displayName'],
50 | 'data' => isset($payload['data']['command']) ? $payload['data']['command'] : null,
51 | 'maxTries' => $payload['maxTries'],
52 | 'timeout' => $payload['timeout'],
53 | 'time' => microtime(true)
54 | ]);
55 |
56 | return [ 'clockwork_id' => $id, 'clockwork_parent_id' => $this->currentRequestId ];
57 | });
58 | }
59 |
60 | // Set Clockwork ID of the current request
61 | public function setCurrentRequestId($requestId)
62 | {
63 | $this->currentRequestId = $requestId;
64 | return $this;
65 | }
66 |
67 | // Collect a dispatched queue job
68 | protected function registerJob(array $job)
69 | {
70 | $trace = StackTrace::get()->resolveViewName();
71 |
72 | $job = array_merge($job, [
73 | 'trace' => (new Serializer)->trace($trace)
74 | ]);
75 |
76 | if ($this->passesFilters([ $job ])) {
77 | $this->jobs[] = $job;
78 | }
79 | }
80 |
81 | // Get an array of dispatched queue jobs commands
82 | protected function getJobs()
83 | {
84 | return array_map(function ($query) {
85 | return array_merge($query, [
86 | 'data' => isset($query['data']) ? (new Serializer)->normalize($query['data']) : null
87 | ]);
88 | }, $this->jobs);
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Clockwork/DataSource/LaravelRedisDataSource.php:
--------------------------------------------------------------------------------
1 | eventDispatcher = $eventDispatcher;
25 |
26 | $this->skipCacheCommands = $skipCacheCommands;
27 |
28 | if ($this->skipCacheCommands) {
29 | $this->addFilter(function ($command, $trace) {
30 | return ! $trace->first(function ($frame) { return $frame->class == 'Illuminate\Cache\RedisStore'; });
31 | });
32 | }
33 | }
34 |
35 | // Adds redis commands to the request
36 | public function resolve(Request $request)
37 | {
38 | $request->redisCommands = array_merge($request->redisCommands, $this->getCommands());
39 |
40 | return $request;
41 | }
42 |
43 | // Reset the data source to an empty state, clearing any collected data
44 | public function reset()
45 | {
46 | $this->commands = [];
47 | }
48 |
49 | // Listen to the cache events
50 | public function listenToEvents()
51 | {
52 | $this->eventDispatcher->listen(\Illuminate\Redis\Events\CommandExecuted::class, function ($event) {
53 | $this->registerCommand([
54 | 'command' => $event->command,
55 | 'parameters' => $event->parameters,
56 | 'duration' => $event->time,
57 | 'connection' => $event->connectionName,
58 | 'time' => microtime(true) - $event->time / 1000
59 | ]);
60 | });
61 | }
62 |
63 | // Collect an executed command
64 | protected function registerCommand(array $command)
65 | {
66 | $trace = StackTrace::get()->resolveViewName();
67 |
68 | $command = array_merge($command, [
69 | 'trace' => (new Serializer)->trace($trace)
70 | ]);
71 |
72 | if ($this->passesFilters([ $command, $trace ])) {
73 | $this->commands[] = $command;
74 | }
75 | }
76 |
77 | // Get an array of executed redis commands
78 | protected function getCommands()
79 | {
80 | return array_map(function ($query) {
81 | return array_merge($query, [
82 | 'parameters' => isset($query['parameters']) ? (new Serializer)->normalize($query['parameters']) : null
83 | ]);
84 | }, $this->commands);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/Clockwork/Support/Lumen/Controller.php:
--------------------------------------------------------------------------------
1 | clockwork = $clockwork;
22 | $this->clockworkSupport = $clockworkSupport;
23 | }
24 |
25 | // Authantication endpoint
26 | public function authenticate(Request $request)
27 | {
28 | $this->ensureClockworkIsEnabled();
29 |
30 | $token = $this->clockwork->authenticator()->attempt(
31 | $request->only([ 'username', 'password' ])
32 | );
33 |
34 | return new JsonResponse([ 'token' => $token ], $token ? 200 : 403);
35 | }
36 |
37 | // Metadata retrieving endpoint
38 | public function getData(Request $request, $id = null, $direction = null, $count = null)
39 | {
40 | $this->ensureClockworkIsEnabled();
41 |
42 | return $this->clockworkSupport->getData(
43 | $id, $direction, $count, $request->only([ 'only', 'except' ])
44 | );
45 | }
46 |
47 | // Extended metadata retrieving endpoint
48 | public function getExtendedData(Request $request, $id = null)
49 | {
50 | $this->ensureClockworkIsEnabled();
51 |
52 | return $this->clockworkSupport->getExtendedData(
53 | $id, $request->only([ 'only', 'except' ])
54 | );
55 | }
56 |
57 | // Metadata updating endpoint
58 | public function updateData(Request $request, $id = null)
59 | {
60 | $this->ensureClockworkIsEnabled();
61 |
62 | return $this->clockworkSupport->updateData($id, $request->json()->all());
63 | }
64 |
65 | // App index
66 | public function webIndex(Request $request)
67 | {
68 | $this->ensureClockworkIsEnabled();
69 |
70 | return $this->clockworkSupport->getWebAsset('index.html');
71 | }
72 |
73 | // App assets serving
74 | public function webAsset($path)
75 | {
76 | $this->ensureClockworkIsEnabled();
77 |
78 | return $this->clockworkSupport->getWebAsset($path);
79 | }
80 |
81 | // App redirect (/clockwork -> /clockwork/app)
82 | public function webRedirect(Request $request)
83 | {
84 | $this->ensureClockworkIsEnabled();
85 |
86 | return new RedirectResponse('/' . $request->path() . '/app');
87 | }
88 |
89 | // Ensure Clockwork is still enabled at this point and stop Telescope recording if present
90 | protected function ensureClockworkIsEnabled()
91 | {
92 | if (class_exists(Telescope::class)) Telescope::stopRecording();
93 |
94 | if (! $this->clockworkSupport->isEnabled()) abort(404);
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/Clockwork/Support/Laravel/ClockworkController.php:
--------------------------------------------------------------------------------
1 | ensureClockworkIsEnabled($clockworkSupport);
19 |
20 | $token = $clockwork->authenticator()->attempt(
21 | $request->only([ 'username', 'password' ])
22 | );
23 |
24 | return new JsonResponse([ 'token' => $token ], $token ? 200 : 403);
25 | }
26 |
27 | // Metadata retrieving endpoint
28 | public function getData(ClockworkSupport $clockworkSupport, Request $request, $id = null, $direction = null, $count = null)
29 | {
30 | $this->ensureClockworkIsEnabled($clockworkSupport);
31 |
32 | return $clockworkSupport->getData(
33 | $id, $direction, $count, $request->only([ 'only', 'except' ])
34 | );
35 | }
36 |
37 | // Extended metadata retrieving endpoint
38 | public function getExtendedData(ClockworkSupport $clockworkSupport, Request $request, $id = null)
39 | {
40 | $this->ensureClockworkIsEnabled($clockworkSupport);
41 |
42 | return $clockworkSupport->getExtendedData(
43 | $id, $request->only([ 'only', 'except' ])
44 | );
45 | }
46 |
47 | // Metadata updating endpoint
48 | public function updateData(ClockworkSupport $clockworkSupport, Request $request, $id = null)
49 | {
50 | $this->ensureClockworkIsEnabled($clockworkSupport);
51 |
52 | return $clockworkSupport->updateData($id, $request->json()->all());
53 | }
54 |
55 | // App index
56 | public function webIndex(ClockworkSupport $clockworkSupport)
57 | {
58 | $this->ensureClockworkIsEnabled($clockworkSupport);
59 |
60 | return $clockworkSupport->getWebAsset('index.html');
61 | }
62 |
63 | // App assets serving
64 | public function webAsset(ClockworkSupport $clockworkSupport, $path)
65 | {
66 | $this->ensureClockworkIsEnabled($clockworkSupport);
67 |
68 | return $clockworkSupport->getWebAsset($path);
69 | }
70 |
71 | // App redirect (/clockwork -> /clockwork/app)
72 | public function webRedirect(ClockworkSupport $clockworkSupport, Request $request)
73 | {
74 | $this->ensureClockworkIsEnabled($clockworkSupport);
75 |
76 | return new RedirectResponse('/' . $request->path() . '/app');
77 | }
78 |
79 | // Ensure Clockwork is still enabled at this point and stop Telescope recording if present
80 | protected function ensureClockworkIsEnabled(ClockworkSupport $clockworkSupport)
81 | {
82 | if (class_exists(Telescope::class)) Telescope::stopRecording();
83 |
84 | if (! $clockworkSupport->isEnabled()) abort(404);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/Clockwork/Support/Laravel/Tests/UsesClockwork.php:
--------------------------------------------------------------------------------
1 | []
16 | ];
17 |
18 | // Set up Clockwork in this test case, should be called from the PHPUnit setUp method
19 | protected function setUpClockwork()
20 | {
21 | if (! $this->app->make('clockwork.support')->isCollectingTests()) return;
22 |
23 | $this->beforeApplicationDestroyed(function () {
24 | if ($this->app->make('clockwork.support')->isTestFiltered($this->toString())) return;
25 |
26 | $this->app->make('clockwork')
27 | ->resolveAsTest(
28 | $this->toString(),
29 | $this->resolveClockworkStatus(),
30 | $this->getStatusMessage(),
31 | $this->resolveClockworkAsserts()
32 | )
33 | ->storeRequest();
34 | });
35 | }
36 |
37 | // Resolve Clockwork test status
38 | protected function resolveClockworkStatus()
39 | {
40 | $status = $this->getStatus();
41 |
42 | $statuses = [
43 | BaseTestRunner::STATUS_UNKNOWN => 'unknown',
44 | BaseTestRunner::STATUS_PASSED => 'passed',
45 | BaseTestRunner::STATUS_SKIPPED => 'skipped',
46 | BaseTestRunner::STATUS_INCOMPLETE => 'incomplete',
47 | BaseTestRunner::STATUS_FAILURE => 'failed',
48 | BaseTestRunner::STATUS_ERROR => 'error',
49 | BaseTestRunner::STATUS_RISKY => 'passed',
50 | BaseTestRunner::STATUS_WARNING => 'warning'
51 | ];
52 |
53 | return isset($statuses[$status]) ? $statuses[$status] : null;
54 | }
55 |
56 | // Resolve executed asserts
57 | protected function resolveClockworkAsserts()
58 | {
59 | $asserts = static::$clockwork['asserts'];
60 |
61 | if ($this->getStatus() == BaseTestRunner::STATUS_FAILURE && count($asserts)) {
62 | $asserts[count($asserts) - 1]['passed'] = false;
63 | }
64 |
65 | static::$clockwork['asserts'] = [];
66 |
67 | return $asserts;
68 | }
69 |
70 | // Overload the main PHPUnit assert method to collect executed asserts
71 | public static function assertThat($value, Constraint $constraint, string $message = ''): void
72 | {
73 | $trace = StackTrace::get([ 'arguments' => true, 'limit' => 10 ]);
74 |
75 | $assertFrame = $trace->filter(function ($frame) { return strpos($frame->function, 'assert') === 0; })->last();
76 | $trace = $trace->skip(StackFilter::make()->isNotVendor([ 'itsgoingd', 'phpunit' ]))->limit(3);
77 |
78 | static::$clockwork['asserts'][] = [
79 | 'name' => $assertFrame->function,
80 | 'arguments' => $assertFrame->args,
81 | 'trace' => (new Serializer)->trace($trace),
82 | 'passed' => true
83 | ];
84 |
85 | parent::assertThat($value, $constraint, $message);
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Clockwork/Request/Timeline/Event.php:
--------------------------------------------------------------------------------
1 | description = $description;
24 | $this->name = isset($data['name']) ? $data['name'] : $description;
25 |
26 | $this->start = isset($data['start']) ? $data['start'] : null;
27 | $this->end = isset($data['end']) ? $data['end'] : null;
28 |
29 | $this->color = isset($data['color']) ? $data['color'] : null;
30 | $this->data = isset($data['data']) ? $data['data'] : null;
31 | }
32 |
33 | // Begin the event at current time
34 | public function begin()
35 | {
36 | $this->start = microtime(true);
37 |
38 | return $this;
39 | }
40 |
41 | // End the event at current time
42 | public function end()
43 | {
44 | $this->end = microtime(true);
45 |
46 | return $this;
47 | }
48 |
49 | // Begin the event, execute the passed in closure and end the event, returns the closure return value
50 | public function run(\Closure $closure, ...$args)
51 | {
52 | $this->begin();
53 | try {
54 | return $closure(...$args);
55 | } finally {
56 | $this->end();
57 | }
58 | }
59 |
60 | // Set or retrieve event duration (in ms), event can be defined with both start and end time or just a single time and duration
61 | public function duration($duration = null)
62 | {
63 | if (! $duration) return ($this->start && $this->end) ? ($this->end - $this->start) * 1000 : 0;
64 |
65 | if ($this->start) $this->end = $this->start + $duration / 1000;
66 | if ($this->end) $this->start = $this->end - $duration / 1000;
67 |
68 | return $this;
69 | }
70 |
71 | // Finalize the event, ends the event, fills in start time if empty and limits the start and end time
72 | public function finalize($start = null, $end = null)
73 | {
74 | $end = $end ?: microtime(true);
75 |
76 | $this->start = $this->start ?: $start;
77 | $this->end = $this->end ?: $end;
78 |
79 | if ($this->start < $start) $this->start = $start;
80 | if ($this->end > $end) $this->end = $end;
81 | }
82 |
83 | // Fluent API
84 | public function __call($method, $parameters)
85 | {
86 | if (! count($parameters)) return $this->$method;
87 |
88 | $this->$method = $parameters[0];
89 |
90 | return $this;
91 | }
92 |
93 | // Return an array representation of the event
94 | public function toArray()
95 | {
96 | return [
97 | 'description' => $this->description,
98 | 'start' => $this->start,
99 | 'end' => $this->end,
100 | 'duration' => $this->duration(),
101 | 'color' => $this->color,
102 | 'data' => $this->data
103 | ];
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/Clockwork/DataSource/PsrMessageDataSource.php:
--------------------------------------------------------------------------------
1 | psrRequest = $psrRequest;
21 | $this->psrResponse = $psrResponse;
22 | }
23 |
24 | // Adds request and response information to the request
25 | public function resolve(Request $request)
26 | {
27 | if ($this->psrRequest) {
28 | $request->method = $this->psrRequest->getMethod();
29 | $request->uri = $this->getRequestUri();
30 | $request->headers = $this->getRequestHeaders();
31 | $request->getData = $this->sanitize($this->psrRequest->getQueryParams());
32 | $request->postData = $this->sanitize($this->psrRequest->getParsedBody());
33 | $request->cookies = $this->sanitize($this->psrRequest->getCookieParams());
34 | $request->time = $this->getRequestTime();
35 | }
36 |
37 | if ($this->psrResponse !== null) {
38 | $request->responseStatus = $this->psrResponse->getStatusCode();
39 | $request->responseTime = $this->getResponseTime();
40 | }
41 |
42 | return $request;
43 | }
44 |
45 | // Normalize items in the array and remove passwords
46 | protected function sanitize($data)
47 | {
48 | return is_array($data) ? $this->removePasswords((new Serializer)->normalizeEach($data)) : $data;
49 | }
50 |
51 | // Get the response time, fetching it from ServerParams
52 | protected function getRequestTime()
53 | {
54 | $env = $this->psrRequest->getServerParams();
55 |
56 | if (isset($env['REQUEST_TIME_FLOAT'])) {
57 | return $env['REQUEST_TIME_FLOAT'];
58 | }
59 | }
60 |
61 | // Get the response time (current time, assuming most of the application code has already run at this point)
62 | protected function getResponseTime()
63 | {
64 | return microtime(true);
65 | }
66 |
67 | // Get the request headers
68 | protected function getRequestHeaders()
69 | {
70 | $headers = [];
71 |
72 | foreach ($this->psrRequest->getHeaders() as $header => $values) {
73 | if (strtoupper(substr($header, 0, 5)) === 'HTTP_') {
74 | $header = substr($header, 5);
75 | }
76 |
77 | $header = str_replace('_', ' ', $header);
78 | $header = ucwords(strtolower($header));
79 | $header = str_replace(' ', '-', $header);
80 |
81 | $headers[$header] = $values;
82 | }
83 |
84 | ksort($headers);
85 |
86 | return $headers;
87 | }
88 |
89 | // Get the request URI
90 | protected function getRequestUri()
91 | {
92 | $uri = $this->psrRequest->getUri();
93 |
94 | return $uri->getPath() . ($uri->getQuery() ? '?' . $uri->getQuery() : '');
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/Clockwork/Support/Slim/Old/ClockworkMiddleware.php:
--------------------------------------------------------------------------------
1 | storagePathOrClockwork = $storagePathOrClockwork;
19 | }
20 |
21 | public function call()
22 | {
23 | $this->app->container->singleton('clockwork', function () {
24 | if ($this->storagePathOrClockwork instanceof Clockwork) {
25 | return $this->storagePathOrClockwork;
26 | }
27 |
28 | $clockwork = new Clockwork();
29 |
30 | $clockwork->addDataSource(new PhpDataSource())
31 | ->addDataSource(new SlimDataSource($this->app))
32 | ->storage(new FileStorage($this->storagePathOrClockwork));
33 |
34 | return $clockwork;
35 | });
36 |
37 | $originalLogWriter = $this->app->getLog()->getWriter();
38 | $clockworkLogWriter = new ClockworkLogWriter($this->app->clockwork, $originalLogWriter);
39 |
40 | $this->app->getLog()->setWriter($clockworkLogWriter);
41 |
42 | $clockworkDataUri = '#/__clockwork(?:/(?([0-9-]+|latest)))?(?:/(?(?:previous|next)))?(?:/(?\d+))?#';
43 | if ($this->app->config('debug') && preg_match($clockworkDataUri, $this->app->request()->getPathInfo(), $matches)) {
44 | $matches = array_merge([ 'direction' => null, 'count' => null ], $matches);
45 | return $this->retrieveRequest($matches['id'], $matches['direction'], $matches['count']);
46 | }
47 |
48 | try {
49 | $this->next->call();
50 | $this->logRequest();
51 | } catch (Exception $e) {
52 | $this->logRequest();
53 | throw $e;
54 | }
55 | }
56 |
57 | public function retrieveRequest($id = null, $direction = null, $count = null)
58 | {
59 | $storage = $this->app->clockwork->storage();
60 |
61 | if ($direction == 'previous') {
62 | $data = $storage->previous($id, $count);
63 | } elseif ($direction == 'next') {
64 | $data = $storage->next($id, $count);
65 | } elseif ($id == 'latest') {
66 | $data = $storage->latest();
67 | } else {
68 | $data = $storage->find($id);
69 | }
70 |
71 | echo json_encode($data, \JSON_PARTIAL_OUTPUT_ON_ERROR);
72 | }
73 |
74 | protected function logRequest()
75 | {
76 | $this->app->clockwork->resolveRequest();
77 | $this->app->clockwork->storeRequest();
78 |
79 | if ($this->app->config('debug')) {
80 | $this->app->response()->header('X-Clockwork-Id', $this->app->clockwork->request()->id);
81 | $this->app->response()->header('X-Clockwork-Version', Clockwork::VERSION);
82 |
83 | $env = $this->app->environment();
84 | if ($env['SCRIPT_NAME']) {
85 | $this->app->response()->header('X-Clockwork-Path', $env['SCRIPT_NAME'] . '/__clockwork/');
86 | }
87 |
88 | $request = $this->app->clockwork->request();
89 | $this->app->response()->header('Server-Timing', ServerTiming::fromRequest($request)->value());
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/Clockwork/DataSource/SlimDataSource.php:
--------------------------------------------------------------------------------
1 | slim = $slim;
18 | }
19 |
20 | // Adds request and response information to the request
21 | public function resolve(Request $request)
22 | {
23 | $request->method = $this->getRequestMethod();
24 | $request->uri = $this->getRequestUri();
25 | $request->controller = $this->getController();
26 | $request->headers = $this->getRequestHeaders();
27 | $request->responseStatus = $this->getResponseStatus();
28 |
29 | return $request;
30 | }
31 |
32 | // Get a textual representation of current route's controller
33 | protected function getController()
34 | {
35 | $matchedRoutes = $this->slim->router()->getMatchedRoutes(
36 | $this->slim->request()->getMethod(), $this->slim->request()->getResourceUri()
37 | );
38 |
39 | if (! count($matchedRoutes)) return;
40 |
41 | $controller = end($matchedRoutes)->getCallable();
42 |
43 | if ($controller instanceof \Closure) {
44 | $controller = 'anonymous function';
45 | } elseif (is_object($controller)) {
46 | $controller = 'instance of ' . get_class($controller);
47 | } elseif (is_array($controller) && count($controller) == 2) {
48 | if (is_object($controller[0])) {
49 | $controller = get_class($controller[0]) . '->' . $controller[1];
50 | } else {
51 | $controller = $controller[0] . '::' . $controller[1];
52 | }
53 | } elseif (! is_string($controller)) {
54 | $controller = null;
55 | }
56 |
57 | return $controller;
58 | }
59 |
60 | // Get the request headers
61 | protected function getRequestHeaders()
62 | {
63 | $headers = [];
64 |
65 | foreach ($_SERVER as $key => $value) {
66 | if (substr($key, 0, 5) !== 'HTTP_') continue;
67 |
68 | $header = substr($key, 5);
69 | $header = str_replace('_', ' ', $header);
70 | $header = ucwords(strtolower($header));
71 | $header = str_replace(' ', '-', $header);
72 |
73 | $value = $this->slim->request()->headers($header, $value);
74 |
75 | if (! isset($headers[$header])) {
76 | $headers[$header] = [ $value ];
77 | } else {
78 | $headers[$header][] = $value;
79 | }
80 | }
81 |
82 | ksort($headers);
83 |
84 | return $headers;
85 | }
86 |
87 | // Get the request method
88 | protected function getRequestMethod()
89 | {
90 | return $this->slim->request()->getMethod();
91 | }
92 |
93 | // Get the request URI
94 | protected function getRequestUri()
95 | {
96 | return $this->slim->request()->getPathInfo();
97 | }
98 |
99 | // Get the response status code
100 | protected function getResponseStatus()
101 | {
102 | return $this->slim->response()->status();
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/Clockwork/Support/Symfony/ClockworkSupport.php:
--------------------------------------------------------------------------------
1 | container = $container;
22 | $this->config = $config;
23 | }
24 |
25 | public function getConfig($key, $default = null)
26 | {
27 | return isset($this->config[$key]) ? $this->config[$key] : $default;
28 | }
29 |
30 | public function getData(Request $request, $id = null, $direction = null, $count = null)
31 | {
32 | $authenticator = $this->container->get('clockwork')->authenticator();
33 | $storage = $this->container->get('clockwork')->storage();
34 |
35 | $authenticated = $authenticator->check($request->headers->get('X-Clockwork-Auth'));
36 |
37 | if ($authenticated !== true) {
38 | return new JsonResponse([ 'message' => $authenticated, 'requires' => $authenticator->requires() ], 403);
39 | }
40 |
41 | if ($direction == 'previous') {
42 | $data = $storage->previous($id, $count, Search::fromRequest($request->query->all()));
43 | } elseif ($direction == 'next') {
44 | $data = $storage->next($id, $count, Search::fromRequest($request->query->all()));
45 | } elseif ($id == 'latest') {
46 | $data = $storage->latest(Search::fromRequest($request->query->all()));
47 | } else {
48 | $data = $storage->find($id);
49 | }
50 |
51 | $data = is_array($data)
52 | ? array_map(function ($request) { return $request->toArray(); }, $data)
53 | : $data->toArray();
54 |
55 | return new JsonResponse($data);
56 | }
57 |
58 | public function getWebAsset($path)
59 | {
60 | $web = new Web;
61 |
62 | if ($asset = $web->asset($path)) {
63 | return new BinaryFileResponse($asset['path'], 200, [ 'Content-Type' => $asset['mime'] ]);
64 | } else {
65 | throw new NotFoundHttpException;
66 | }
67 | }
68 |
69 | public function makeAuthenticator()
70 | {
71 | $authenticator = $this->getConfig('authentication');
72 |
73 | if (is_string($authenticator)) {
74 | return $this->container->get($authenticator);
75 | } elseif ($authenticator) {
76 | return new SimpleAuthenticator($this->getConfig('authentication_password'));
77 | } else {
78 | return new NullAuthenticator;
79 | }
80 | }
81 |
82 | public function isEnabled()
83 | {
84 | return $this->getConfig('enable', false);
85 | }
86 |
87 | public function isWebEnabled()
88 | {
89 | return $this->getConfig('web', true);
90 | }
91 |
92 | public function webPaths()
93 | {
94 | $path = $this->getConfig('web', true);
95 |
96 | if (is_string($path)) return [ trim($path, '/') ];
97 |
98 | return [ 'clockwork', '__clockwork' ];
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/Clockwork/Support/Lumen/ClockworkServiceProvider.php:
--------------------------------------------------------------------------------
1 | app->configure('clockwork');
15 | $this->mergeConfigFrom(__DIR__ . '/../Laravel/config/clockwork.php', 'clockwork');
16 | }
17 |
18 | // Register Clockwork components
19 | protected function registerClockwork()
20 | {
21 | parent::registerClockwork();
22 |
23 | $this->app->singleton('clockwork.support', function ($app) {
24 | return new ClockworkSupport($app);
25 | });
26 |
27 | if ($this->isRunningWithFacades() && ! class_exists('Clockwork')) {
28 | class_alias(\Clockwork\Support\Laravel\Facade::class, 'Clockwork');
29 | }
30 | }
31 |
32 | // Register Clockwork data sources
33 | protected function registerDataSources()
34 | {
35 | parent::registerDataSources();
36 |
37 | $this->app->singleton('clockwork.lumen', function ($app) {
38 | return (new LumenDataSource(
39 | $app,
40 | $app['clockwork.support']->isFeatureEnabled('log'),
41 | $app['clockwork.support']->isFeatureEnabled('views'),
42 | $app['clockwork.support']->isFeatureEnabled('routes')
43 | ));
44 | });
45 | }
46 |
47 | // Register Clockwork components aliases for type hinting
48 | protected function registerAliases()
49 | {
50 | parent::registerAliases();
51 |
52 | $this->app->alias('clockwork.lumen', LumenDataSource::class);
53 | }
54 |
55 | // Register event listeners
56 | protected function registerEventListeners()
57 | {
58 | $this->app['clockwork.support']->addDataSources()->listenToEvents();
59 | }
60 |
61 | // Register Clockwork middleware
62 | public function registerMiddleware()
63 | {
64 | $this->app->middleware([ ClockworkMiddleware::class ]);
65 | }
66 |
67 | // Register Clockwork REST api routes
68 | public function registerRoutes()
69 | {
70 | $router = isset($this->app->router) ? $this->app->router : $this->app;
71 |
72 | $router->get('/__clockwork/{id:(?:[0-9-]+|latest)}/extended', 'Clockwork\Support\Lumen\Controller@getExtendedData');
73 | $router->get('/__clockwork/{id:(?:[0-9-]+|latest)}[/{direction:(?:next|previous)}[/{count:\d+}]]', 'Clockwork\Support\Lumen\Controller@getData');
74 | $router->put('/__clockwork/{id}', 'Clockwork\Support\Lumen\Controller@updateData');
75 | $router->post('/__clockwork/auth', 'Clockwork\Support\Lumen\Controller@authenticate');
76 | }
77 |
78 | // Register Clockwork app routes
79 | public function registerWebRoutes()
80 | {
81 | $router = isset($this->app->router) ? $this->app->router : $this->app;
82 |
83 | $this->app['clockwork.support']->webPaths()->each(function ($path) use ($router) {
84 | $router->get("{$path}", 'Clockwork\Support\Lumen\Controller@webRedirect');
85 | $router->get("{$path}/app", 'Clockwork\Support\Lumen\Controller@webIndex');
86 | $router->get("{$path}/{path:.+}", 'Clockwork\Support\Lumen\Controller@webAsset');
87 | });
88 | }
89 |
90 | // Check whether we are running with facades enabled
91 | protected function isRunningWithFacades()
92 | {
93 | return Facade::getFacadeApplication() !== null;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/Clockwork/Support/Laravel/Tests/ClockworkExtension.php:
--------------------------------------------------------------------------------
1 | registerSubscribers(
22 | new class implements Event\Test\PreparedSubscriber {
23 | public function notify($event): void { ClockworkExtension::$asserts = []; }
24 | },
25 | new class implements Event\Test\ErroredSubscriber {
26 | public function notify($event): void { ClockworkExtension::recordTest('error', $event->throwable()->message()); }
27 | },
28 | new class implements Event\Test\FailedSubscriber {
29 | public function notify($event): void { ClockworkExtension::recordTest('failed', $event->throwable()->message()); }
30 | },
31 | new class implements Event\Test\MarkedIncompleteSubscriber {
32 | public function notify($event): void { ClockworkExtension::recordTest('incomplete', $event->throwable()->message()); }
33 | },
34 | new class implements Event\Test\PassedSubscriber {
35 | public function notify($event): void { ClockworkExtension::recordTest('passed'); }
36 | },
37 | new class implements Event\Test\SkippedSubscriber {
38 | public function notify($event): void { ClockworkExtension::recordTest('skipped', $event->message()); }
39 | },
40 | new class implements Event\Test\AssertionSucceededSubscriber {
41 | public function notify($event): void { ClockworkExtension::recordAssertion(true); }
42 | },
43 | new class implements Event\Test\AssertionFailedSubscriber {
44 | public function notify($event): void { ClockworkExtension::recordAssertion(false); }
45 | }
46 | );
47 | }
48 |
49 | public static function recordTest($status, $message = null)
50 | {
51 | $trace = StackTrace::get([ 'arguments' => false, 'limit' => 10 ]);
52 | $testFrame = $trace->filter(function ($frame) { return $frame->object instanceof \PHPUnit\Framework\TestCase; })->last();
53 |
54 | if (! $testFrame) return;
55 |
56 | $testInstance = $testFrame->object;
57 |
58 | $reflectionClass = new \ReflectionClass($testInstance);
59 |
60 | if (! $reflectionClass->hasProperty('app')) return;
61 |
62 | $reflectionProperty = $reflectionClass->getProperty('app');
63 | $reflectionProperty->setAccessible(true);
64 |
65 | $app = $reflectionProperty->getValue($testInstance);
66 |
67 | if (! $app->make('clockwork.support')->isCollectingTests()) return;
68 | if ($app->make('clockwork.support')->isTestFiltered($testInstance->toString())) return;
69 |
70 | $app->make('clockwork')
71 | ->resolveAsTest(
72 | str_replace('__pest_evaluable_', '', $testInstance->toString()),
73 | $status,
74 | $message,
75 | ClockworkExtension::$asserts
76 | )
77 | ->storeRequest();
78 | }
79 |
80 | public static function recordAssertion($passed = true)
81 | {
82 | $trace = StackTrace::get([ 'arguments' => true, 'limit' => 10 ]);
83 | $assertFrame = $trace->filter(function ($frame) { return strpos($frame->function, 'assert') === 0; })->last();
84 |
85 | $trace = $trace->skip(StackFilter::make()->isNotVendor([ 'itsgoingd', 'phpunit' ]))->limit(3);
86 |
87 | static::$asserts[] = [
88 | 'name' => $assertFrame->function,
89 | 'arguments' => $assertFrame->args,
90 | 'trace' => (new Serializer)->trace($trace),
91 | 'passed' => $passed
92 | ];
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/Clockwork/Request/ShouldCollect.php:
--------------------------------------------------------------------------------
1 | except = array_merge($this->except, is_array($uris) ? $uris : [ $uris ]);
26 |
27 | return $this;
28 | }
29 |
30 | // Append one or more only URIs
31 | public function only($uris)
32 | {
33 | $this->only = array_merge($this->only, is_array($uris) ? $uris : [ $uris ]);
34 |
35 | return $this;
36 | }
37 |
38 | // Merge multiple settings from array
39 | public function merge(array $data = [])
40 | {
41 | foreach ($data as $key => $val) $this->$key = $val;
42 | }
43 |
44 | // Apply the filter to an incoming request
45 | public function filter(IncomingRequest $request)
46 | {
47 | return $this->passOnDemand($request)
48 | && $this->passSampling()
49 | && $this->passExcept($request)
50 | && $this->passOnly($request)
51 | && $this->passExceptPreflight($request)
52 | && $this->passCallback($request);
53 | }
54 |
55 | protected function passOnDemand(IncomingRequest $request)
56 | {
57 | if (! $this->onDemand) return true;
58 |
59 | if ($this->onDemand !== true) {
60 | $input = isset($request->input['clockwork-profile']) ? $request->input['clockwork-profile'] : '';
61 | $cookie = isset($request->cookies['clockwork-profile']) ? $request->cookies['clockwork-profile'] : '';
62 |
63 | return hash_equals($this->onDemand, $input) || hash_equals($this->onDemand, $cookie);
64 | }
65 |
66 | return isset($request->input['clockwork-profile']) || isset($request->cookies['clockwork-profile']);
67 | }
68 |
69 | protected function passSampling()
70 | {
71 | if (! $this->sample) return true;
72 |
73 | return mt_rand(0, $this->sample) == $this->sample;
74 | }
75 |
76 | protected function passExcept(IncomingRequest $request)
77 | {
78 | if (! count($this->except)) return true;
79 |
80 | foreach ($this->except as $pattern) {
81 | if (preg_match('#' . str_replace('#', '\#', $pattern) . '#', $request->uri)) return false;
82 | }
83 |
84 | return true;
85 | }
86 |
87 | protected function passOnly(IncomingRequest $request)
88 | {
89 | if (! count($this->only)) return true;
90 |
91 | foreach ($this->only as $pattern) {
92 | if (preg_match('#' . str_replace('#', '\#', $pattern) . '#', $request->uri)) return true;
93 | }
94 |
95 | return false;
96 | }
97 |
98 | protected function passExceptPreflight(IncomingRequest $request)
99 | {
100 | if (! $this->exceptPreflight) return true;
101 |
102 | return strtoupper($request->method) != 'OPTIONS';
103 | }
104 |
105 | protected function passCallback(IncomingRequest $request)
106 | {
107 | if (! $this->callback) return true;
108 |
109 | return call_user_func($this->callback, $request);
110 | }
111 |
112 | public function __call($method, $parameters)
113 | {
114 | if (! count($parameters)) return $this->$method;
115 |
116 | $this->$method = count($parameters) ? $parameters[0] : true;
117 |
118 | return $this;
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/Clockwork/Support/Vanilla/ClockworkMiddleware.php:
--------------------------------------------------------------------------------
1 | clockwork = $clockwork;
26 | }
27 |
28 | // Returns a new middleware instance with a default singleton Clockwork instance, takes an additional configuration as argument
29 | public static function init($config = [])
30 | {
31 | return new static(Clockwork::init($config));
32 | }
33 |
34 | // Sets a PSR-17 compatible response factory. When using the middleware with routing enabled, response factory must be manually set
35 | // or the php-http/discovery has to be intalled for zero-configuration use
36 | public function withResponseFactory(ResponseFactoryInterface $responseFactory)
37 | {
38 | $this->responseFactory = $responseFactory;
39 | return $this;
40 | }
41 |
42 | // Disables routing handling in the middleware. When disabled additional manual configuration of the application router is required.
43 | public function withoutRouting()
44 | {
45 | $this->handleRouting = false;
46 | return $this;
47 | }
48 |
49 | // Process the middleware
50 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) :ResponseInterface
51 | {
52 | $request = $request->withAttribute('clockwork', $this->clockwork);
53 |
54 | $this->clockwork->event('Controller')->begin();
55 |
56 | if ($response = $this->handleApiRequest($request)) return $response;
57 | if ($response = $this->handleWebRequest($request)) return $response;
58 |
59 | $response = $handler->handle($request);
60 |
61 | return $this->clockwork->usePsrMessage($request, $response)->requestProcessed();
62 | }
63 |
64 | // Handle a Clockwork REST api request if routing is enabled
65 | protected function handleApiRequest(ServerRequestInterface $request)
66 | {
67 | $path = $this->clockwork->getConfig()['api'];
68 |
69 | if (! $this->handleRouting) return;
70 | if (! preg_match("#{$path}.*#", $request->getUri()->getPath())) return;
71 |
72 | return $this->clockwork->usePsrMessage($request, $this->prepareResponse())->handleMetadata();
73 | }
74 |
75 | // Handle a Clockwork Web interface request if routing is enabled
76 | protected function handleWebRequest(ServerRequestInterface $request)
77 | {
78 | $path = is_string($this->clockwork->getConfig()['web']['enable']) ? $this->clockwork->getConfig()['web']['enable'] : '/clockwork';
79 |
80 | if (! $this->handleRouting) return;
81 | if ($request->getUri()->getPath() != $path) return;
82 |
83 | return $this->clockwork->usePsrMessage($request, $this->prepareResponse())->returnWeb();
84 | }
85 |
86 | protected function prepareResponse()
87 | {
88 | if (! $this->responseFactory && ! class_exists(Psr17Factory::class)) {
89 | throw new \Exception('The Clockwork vanilla middleware requires a response factory or the php-http/discovery package to be installed.');
90 | }
91 |
92 | return ($this->responseFactory ?: new Psr17Factory)->createResponse();
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/Clockwork/Request/Log.php:
--------------------------------------------------------------------------------
1 | messages = $messages;
17 | }
18 |
19 | // Log a new message, with a level and context, context can be used to override serializer defaults,
20 | // $context['trace'] = true can be used to force collecting a stack trace
21 | public function log($level = LogLevel::INFO, $message = null, array $context = [])
22 | {
23 | $trace = $this->hasTrace($context) ? $context['trace'] : StackTrace::get()->resolveViewName();
24 |
25 | $this->messages[] = [
26 | 'message' => (new Serializer($context))->normalize($message),
27 | 'exception' => $this->formatException($context),
28 | 'context' => $this->formatContext($context),
29 | 'level' => $level,
30 | 'time' => microtime(true),
31 | 'trace' => (new Serializer(! empty($context['trace']) ? [ 'traces' => true ] : []))->trace($trace)
32 | ];
33 | }
34 |
35 | public function emergency($message, array $context = [])
36 | {
37 | $this->log(LogLevel::EMERGENCY, $message, $context);
38 | }
39 |
40 | public function alert($message, array $context = [])
41 | {
42 | $this->log(LogLevel::ALERT, $message, $context);
43 | }
44 |
45 | public function critical($message, array $context = [])
46 | {
47 | $this->log(LogLevel::CRITICAL, $message, $context);
48 | }
49 |
50 | public function error($message, array $context = [])
51 | {
52 | $this->log(LogLevel::ERROR, $message, $context);
53 | }
54 |
55 | public function warning($message, array $context = [])
56 | {
57 | $this->log(LogLevel::WARNING, $message, $context);
58 | }
59 |
60 | public function notice($message, array $context = [])
61 | {
62 | $this->log(LogLevel::NOTICE, $message, $context);
63 | }
64 |
65 | public function info($message, array $context = [])
66 | {
67 | $this->log(LogLevel::INFO, $message, $context);
68 | }
69 |
70 | public function debug($message, array $context = [])
71 | {
72 | $this->log(LogLevel::DEBUG, $message, $context);
73 | }
74 |
75 | // Merge another log instance into the current log
76 | public function merge(Log $log)
77 | {
78 | $this->messages = array_merge($this->messages, $log->messages);
79 |
80 | return $this;
81 | }
82 |
83 | // Sort the log messages by timestamp
84 | public function sort()
85 | {
86 | usort($this->messages, function ($a, $b) { return $a['time'] * 1000 - $b['time'] * 1000; });
87 | }
88 |
89 | // Get all messages as an array
90 | public function toArray()
91 | {
92 | return $this->messages;
93 | }
94 |
95 | // Format message context, removes exception and trace if we are serializing them
96 | protected function formatContext($context)
97 | {
98 | if ($this->hasException($context)) unset($context['exception']);
99 | if ($this->hasTrace($context)) unset($context['trace']);
100 |
101 | return (new Serializer)->normalize($context);
102 | }
103 |
104 | // Format exception if present in the context
105 | protected function formatException($context)
106 | {
107 | if ($this->hasException($context)) {
108 | return (new Serializer)->exception($context['exception']);
109 | }
110 | }
111 |
112 | // Check if context has serializable trace
113 | protected function hasTrace($context)
114 | {
115 | return ! empty($context['trace']) && $context['trace'] instanceof StackTrace && empty($context['raw']);
116 | }
117 |
118 | // Check if context has serializable exception
119 | protected function hasException($context)
120 | {
121 | return ! empty($context['exception'])
122 | && ($context['exception'] instanceof \Throwable || $context['exception'] instanceof \Exception)
123 | && empty($context['raw']);
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/Clockwork/Support/Slim/Legacy/ClockworkMiddleware.php:
--------------------------------------------------------------------------------
1 | clockwork = $storagePathOrClockwork instanceof Clockwork
21 | ? $storagePathOrClockwork : $this->createDefaultClockwork($storagePathOrClockwork);
22 | $this->startTime = $startTime ?: microtime(true);
23 | }
24 |
25 | public function __invoke(Request $request, Response $response, callable $next)
26 | {
27 | return $this->process($request, $response, $next);
28 | }
29 |
30 | public function process(Request $request, Response $response, callable $next)
31 | {
32 | $authUri = '#/__clockwork/auth#';
33 | if (preg_match($authUri, $request->getUri()->getPath(), $matches)) {
34 | return $this->authenticate($response, $request);
35 | }
36 |
37 | $clockworkDataUri = '#/__clockwork(?:/(?([0-9-]+|latest)))?(?:/(?(?:previous|next)))?(?:/(?\d+))?#';
38 | if (preg_match($clockworkDataUri, $request->getUri()->getPath(), $matches)) {
39 | $matches = array_merge([ 'id' => null, 'direction' => null, 'count' => null ], $matches);
40 | return $this->retrieveRequest($response, $request, $matches['id'], $matches['direction'], $matches['count']);
41 | }
42 |
43 | $response = $next($request, $response);
44 |
45 | return $this->logRequest($request, $response);
46 | }
47 |
48 | protected function authenticate(Response $response, Request $request)
49 | {
50 | $token = $this->clockwork->authenticator()->attempt($request->getParsedBody());
51 |
52 | return $response->withJson([ 'token' => $token ])->withStatus($token ? 200 : 403);
53 | }
54 |
55 | protected function retrieveRequest(Response $response, Request $request, $id, $direction, $count)
56 | {
57 | $authenticator = $this->clockwork->authenticator();
58 | $storage = $this->clockwork->storage();
59 |
60 | $authenticated = $authenticator->check(current($request->getHeader('X-Clockwork-Auth')));
61 |
62 | if ($authenticated !== true) {
63 | return $response
64 | ->withJson([ 'message' => $authenticated, 'requires' => $authenticator->requires() ])
65 | ->withStatus(403);
66 | }
67 |
68 | if ($direction == 'previous') {
69 | $data = $storage->previous($id, $count);
70 | } elseif ($direction == 'next') {
71 | $data = $storage->next($id, $count);
72 | } elseif ($id == 'latest') {
73 | $data = $storage->latest();
74 | } else {
75 | $data = $storage->find($id);
76 | }
77 |
78 | return $response
79 | ->withHeader('Content-Type', 'application/json')
80 | ->withJson($data);
81 | }
82 |
83 | protected function logRequest(Request $request, Response $response)
84 | {
85 | $this->clockwork->timeline()->finalize($this->startTime);
86 | $this->clockwork->addDataSource(new PsrMessageDataSource($request, $response));
87 |
88 | $this->clockwork->resolveRequest();
89 | $this->clockwork->storeRequest();
90 |
91 | $clockworkRequest = $this->clockwork->request();
92 |
93 | $response = $response
94 | ->withHeader('X-Clockwork-Id', $clockworkRequest->id)
95 | ->withHeader('X-Clockwork-Version', Clockwork::VERSION);
96 |
97 | if ($basePath = $request->getUri()->getBasePath()) {
98 | $response = $response->withHeader('X-Clockwork-Path', $basePath);
99 | }
100 |
101 | return $response->withHeader('Server-Timing', ServerTiming::fromRequest($clockworkRequest)->value());
102 | }
103 |
104 | protected function createDefaultClockwork($storagePath)
105 | {
106 | $clockwork = new Clockwork();
107 |
108 | $clockwork->storage(new FileStorage($storagePath));
109 | $clockwork->authenticator(new NullAuthenticator);
110 |
111 | return $clockwork;
112 | }
113 | }
--------------------------------------------------------------------------------
/Clockwork/Helpers/StackTrace.php:
--------------------------------------------------------------------------------
1 | frames = $frames;
40 | $this->basePath = $basePath;
41 | $this->vendorPath = $vendorPath;
42 | }
43 |
44 | // Get all frames
45 | public function frames()
46 | {
47 | return $this->frames;
48 | }
49 |
50 | // Get the first frame, optionally filtered by a stack filter or a closure
51 | public function first($filter = null)
52 | {
53 | if (! $filter) return reset($this->frames);
54 |
55 | if ($filter instanceof StackFilter) $filter = $filter->closure();
56 |
57 | foreach ($this->frames as $frame) {
58 | if ($filter($frame)) return $frame;
59 | }
60 | }
61 |
62 | // Get the last frame, optionally filtered by a stack filter or a closure
63 | public function last($filter = null)
64 | {
65 | if (! $filter) return $this->frames[count($this->frames) - 1];
66 |
67 | if ($filter instanceof StackFilter) $filter = $filter->closure();
68 |
69 | foreach (array_reverse($this->frames) as $frame) {
70 | if ($filter($frame)) return $frame;
71 | }
72 | }
73 |
74 | // Get trace filtered by a stack filter or a closure
75 | public function filter($filter = null)
76 | {
77 | if ($filter instanceof StackFilter) $filter = $filter->closure();
78 |
79 | return $this->copy(array_values(array_filter($this->frames, $filter)));
80 | }
81 |
82 | // Get trace skipping a number of frames or frames matching a stack filter or a closure
83 | public function skip($count = null)
84 | {
85 | if ($count instanceof StackFilter) $count = $count->closure();
86 | if ($count instanceof \Closure) $count = array_search($this->first($count), $this->frames);
87 |
88 | return $this->copy(array_slice($this->frames, $count));
89 | }
90 |
91 | // Get trace with a number of frames from the top
92 | public function limit($count = null)
93 | {
94 | return $this->copy(array_slice($this->frames, 0, $count));
95 | }
96 |
97 | // Get a copy of the trace
98 | public function copy($frames = null)
99 | {
100 | return new static($frames ?: $this->frames, $this->basePath, $this->vendorPath);
101 | }
102 |
103 | protected static function resolveBasePath()
104 | {
105 | return substr(__DIR__, 0, strpos(__DIR__, DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR));
106 | }
107 |
108 | protected static function resolveVendorPath()
109 | {
110 | return static::resolveBasePath() . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR;
111 | }
112 |
113 | // Fixes call_user_func stack frames missing file and line
114 | protected static function fixCallUserFuncFrame($frame, array $trace, $index)
115 | {
116 | if (isset($frame['file'])) return $frame;
117 |
118 | $nextFrame = isset($trace[$index + 1]) ? $trace[$index + 1] : null;
119 |
120 | if (! $nextFrame || ! in_array($nextFrame['function'], [ 'call_user_func', 'call_user_func_array' ])) return $frame;
121 |
122 | $frame['file'] = $nextFrame['file'];
123 | $frame['line'] = $nextFrame['line'];
124 |
125 | return $frame;
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/Clockwork/Support/Slim/ClockworkMiddleware.php:
--------------------------------------------------------------------------------
1 | app = $app;
22 | $this->clockwork = $storagePathOrClockwork instanceof Clockwork
23 | ? $storagePathOrClockwork : $this->createDefaultClockwork($storagePathOrClockwork);
24 | $this->startTime = $startTime ?: microtime(true);
25 | }
26 |
27 | public function __invoke(Request $request, RequestHandler $handler)
28 | {
29 | return $this->process($request, $handler);
30 | }
31 |
32 | public function process(Request $request, RequestHandler $handler)
33 | {
34 | $authUri = '#/__clockwork/auth#';
35 | if (preg_match($authUri, $request->getUri()->getPath(), $matches)) {
36 | return $this->authenticate($request);
37 | }
38 |
39 | $clockworkDataUri = '#/__clockwork(?:/(?([0-9-]+|latest)))?(?:/(?(?:previous|next)))?(?:/(?\d+))?#';
40 | if (preg_match($clockworkDataUri, $request->getUri()->getPath(), $matches)) {
41 | $matches = array_merge([ 'id' => null, 'direction' => null, 'count' => null ], $matches);
42 | return $this->retrieveRequest($request, $matches['id'], $matches['direction'], $matches['count']);
43 | }
44 |
45 | $response = $handler->handle($request);
46 |
47 | return $this->logRequest($request, $response);
48 | }
49 |
50 | protected function authenticate(Request $request)
51 | {
52 | $token = $this->clockwork->authenticator()->attempt($request->getParsedBody());
53 |
54 | return $this->jsonResponse([ 'token' => $token ], $token ? 200 : 403);
55 | }
56 |
57 | protected function retrieveRequest(Request $request, $id, $direction, $count)
58 | {
59 | $authenticator = $this->clockwork->authenticator();
60 | $storage = $this->clockwork->storage();
61 |
62 | $authenticated = $authenticator->check(current($request->getHeader('X-Clockwork-Auth')));
63 |
64 | if ($authenticated !== true) {
65 | return $this->jsonResponse([ 'message' => $authenticated, 'requires' => $authenticator->requires() ], 403);
66 | }
67 |
68 | if ($direction == 'previous') {
69 | $data = $storage->previous($id, $count);
70 | } elseif ($direction == 'next') {
71 | $data = $storage->next($id, $count);
72 | } elseif ($id == 'latest') {
73 | $data = $storage->latest();
74 | } else {
75 | $data = $storage->find($id);
76 | }
77 |
78 | return $this->jsonResponse($data);
79 | }
80 |
81 | protected function logRequest(Request $request, $response)
82 | {
83 | $this->clockwork->timeline()->finalize($this->startTime);
84 | $this->clockwork->addDataSource(new PsrMessageDataSource($request, $response));
85 |
86 | $this->clockwork->resolveRequest();
87 | $this->clockwork->storeRequest();
88 |
89 | $clockworkRequest = $this->clockwork->request();
90 |
91 | $response = $response
92 | ->withHeader('X-Clockwork-Id', $clockworkRequest->id)
93 | ->withHeader('X-Clockwork-Version', Clockwork::VERSION);
94 |
95 | if ($basePath = $this->app->getBasePath()) {
96 | $response = $response->withHeader('X-Clockwork-Path', "$basePath/__clockwork/");
97 | }
98 |
99 | return $response->withHeader('Server-Timing', ServerTiming::fromRequest($clockworkRequest)->value());
100 | }
101 |
102 | protected function createDefaultClockwork($storagePath)
103 | {
104 | $clockwork = new Clockwork();
105 |
106 | $clockwork->storage(new FileStorage($storagePath));
107 | $clockwork->authenticator(new NullAuthenticator);
108 |
109 | return $clockwork;
110 | }
111 |
112 | protected function jsonResponse($data, $status = 200)
113 | {
114 | $response = $this->app->getResponseFactory()
115 | ->createResponse($status)
116 | ->withHeader('Content-Type', 'application/json');
117 |
118 | $response->getBody()->write(json_encode($data));
119 |
120 | return $response;
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/Clockwork/Helpers/StackFilter.php:
--------------------------------------------------------------------------------
1 | classes = array_merge($this->classes, is_array($classes) ? $classes : [ $classes ]);
29 | return $this;
30 | }
31 |
32 | public function isNotClass($classes)
33 | {
34 | $this->notClasses = array_merge($this->notClasses, is_array($classes) ? $classes : [ $classes ]);
35 | return $this;
36 | }
37 |
38 | public function isFile($files)
39 | {
40 | $this->files = array_merge($this->files, is_array($files) ? $files : [ $files ]);
41 | return $this;
42 | }
43 |
44 | public function isNotFile($files)
45 | {
46 | $this->notFiles = array_merge($this->notFiles, is_array($files) ? $files : [ $files ]);
47 | return $this;
48 | }
49 |
50 | public function isFunction($functions)
51 | {
52 | $this->functions = array_merge($this->functions, is_array($functions) ? $functions : [ $functions ]);
53 | return $this;
54 | }
55 |
56 | public function isNotFunction($functions)
57 | {
58 | $this->notFunctions = array_merge($this->notFunctions, is_array($functions) ? $functions : [ $functions ]);
59 | return $this;
60 | }
61 |
62 | public function isNamespace($namespaces)
63 | {
64 | $this->namespaces = array_merge($this->namespaces, is_array($namespaces) ? $namespaces : [ $namespaces ]);
65 | return $this;
66 | }
67 |
68 | public function isNotNamespace($namespaces)
69 | {
70 | $this->notNamespaces = array_merge($this->notNamespaces, is_array($namespaces) ? $namespaces : [ $namespaces ]);
71 | return $this;
72 | }
73 |
74 | public function isVendor($vendors)
75 | {
76 | $this->vendors = array_merge($this->vendors, is_array($vendors) ? $vendors : [ $vendors ]);
77 | return $this;
78 | }
79 |
80 | public function isNotVendor($vendors)
81 | {
82 | $this->notVendors = array_merge($this->notVendors, is_array($vendors) ? $vendors : [ $vendors ]);
83 | return $this;
84 | }
85 |
86 | // Apply the filter to a stack frame
87 | public function filter(StackFrame $frame)
88 | {
89 | return $this->matchesClass($frame)
90 | && $this->matchesFile($frame)
91 | && $this->matchesFunction($frame)
92 | && $this->matchesNamespace($frame)
93 | && $this->matchesVendor($frame);
94 | }
95 |
96 | // Return a closure calling this filter
97 | public function closure()
98 | {
99 | return function ($frame) { return $this->filter($frame); };
100 | }
101 |
102 | protected function matchesClass(StackFrame $frame)
103 | {
104 | if (count($this->classes) && ! in_array($frame->class, $this->classes)) return false;
105 | if (count($this->notClasses) && in_array($frame->class, $this->notClasses)) return false;
106 |
107 | return true;
108 | }
109 |
110 | protected function matchesFile(StackFrame $frame)
111 | {
112 | if (count($this->files) && ! in_array($frame->file, $this->files)) return false;
113 | if (count($this->notFiles) && in_array($frame->file, $this->notFiles)) return false;
114 |
115 | return true;
116 | }
117 |
118 | protected function matchesFunction(StackFrame $frame)
119 | {
120 | if (count($this->functions) && ! in_array($frame->function, $this->functions)) return false;
121 | if (count($this->notFunctions) && in_array($frame->function, $this->notFunctions)) return false;
122 |
123 | return true;
124 | }
125 |
126 | protected function matchesNamespace(StackFrame $frame)
127 | {
128 | foreach ($this->notNamespaces as $namespace) {
129 | if ($frame->class !== null && strpos($frame->class, "{$namespace}\\") !== false) return false;
130 | }
131 |
132 | if (! count($this->namespaces)) return true;
133 |
134 | foreach ($this->namespaces as $namespace) {
135 | if ($frame->class !== null && strpos($frame->class, "{$namespace}\\") !== false) return true;
136 | }
137 |
138 | return false;
139 | }
140 |
141 | protected function matchesVendor(StackFrame $frame)
142 | {
143 | if (count($this->vendors) && ! in_array($frame->vendor, $this->vendors)) return false;
144 | if (count($this->notVendors) && in_array($frame->vendor, $this->notVendors)) return false;
145 |
146 | return true;
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/Clockwork/DataSource/LaravelEventsDataSource.php:
--------------------------------------------------------------------------------
1 | dispatcher = $dispatcher;
25 |
26 | $this->ignoredEvents = is_array($ignoredEvents)
27 | ? array_merge($ignoredEvents, $this->defaultIgnoredEvents()) : [];
28 | }
29 |
30 | // Adds fired events to the request
31 | public function resolve(Request $request)
32 | {
33 | $request->events = array_merge($request->events, $this->events);
34 |
35 | return $request;
36 | }
37 |
38 | // Reset the data source to an empty state, clearing any collected data
39 | public function reset()
40 | {
41 | $this->events = [];
42 | }
43 |
44 | // Start listening to the events
45 | public function listenToEvents()
46 | {
47 | $this->dispatcher->listen('*', function ($event = null, $data = null) {
48 | if (method_exists($this->dispatcher, 'firing')) { // Laravel 5.0 - 5.3
49 | $data = func_get_args();
50 | $event = $this->dispatcher->firing();
51 | }
52 |
53 | $this->registerEvent($event, $data);
54 | });
55 | }
56 |
57 | // Collect a fired event, prepares data for serialization and resolves registered listeners
58 | protected function registerEvent($event, array $data)
59 | {
60 | if (! $this->shouldCollect($event)) return;
61 |
62 | $trace = StackTrace::get()->resolveViewName();
63 |
64 | $event = [
65 | 'event' => $event,
66 | 'data' => (new Serializer)->normalize(count($data) == 1 && isset($data[0]) ? $data[0] : $data),
67 | 'time' => microtime(true),
68 | 'listeners' => $this->findListenersFor($event),
69 | 'trace' => (new Serializer)->trace($trace)
70 | ];
71 |
72 | if ($this->passesFilters([ $event ])) {
73 | $this->events[] = $event;
74 | }
75 | }
76 |
77 | // Returns registered listeners for the specified event
78 | protected function findListenersFor($event)
79 | {
80 | $listener = $this->dispatcher->getListeners($event)[0];
81 |
82 | return array_filter(array_map(function ($listener) {
83 | if ($listener instanceof \Closure) {
84 | // Laravel 5.4+ (and earlier versions in some cases) wrap the listener into a closure,
85 | // attempt to resolve the original listener
86 | $use = (new \ReflectionFunction($listener))->getStaticVariables();
87 | $listener = isset($use['listener']) ? $use['listener'] : $listener;
88 | }
89 |
90 | if (is_string($listener)) {
91 | return $listener;
92 | } elseif (is_array($listener) && count($listener) == 2) {
93 | if (is_object($listener[0])) {
94 | return get_class($listener[0]) . '@' . $listener[1];
95 | } else {
96 | return $listener[0] . '::' . $listener[1];
97 | }
98 | } elseif ($listener instanceof \Closure) {
99 | $listener = new \ReflectionFunction($listener);
100 |
101 | if (strpos($listener->getNamespaceName(), 'Clockwork\\') === 0) { // skip our own listeners
102 | return;
103 | }
104 |
105 | $filename = str_replace(base_path(), '', $listener->getFileName());
106 | $startLine = $listener->getStartLine();
107 | $endLine = $listener->getEndLine();
108 |
109 | return "Closure ({$filename}:{$startLine}-{$endLine})";
110 | }
111 | }, $this->dispatcher->getListeners($event)));
112 | }
113 |
114 | // Returns whether the event should be collected (depending on ignored events)
115 | protected function shouldCollect($event)
116 | {
117 | return ! preg_match('/^(?:' . implode('|', $this->ignoredEvents) . ')$/', $event);
118 | }
119 |
120 | // Returns default ignored events (framework-specific events)
121 | protected function defaultIgnoredEvents()
122 | {
123 | return [
124 | 'Illuminate\\\\.+',
125 | 'Laravel\\\\.+',
126 | 'auth\.(?:attempt|login|logout)',
127 | 'artisan\.start',
128 | 'bootstrapped:.+',
129 | 'composing:.+',
130 | 'creating:.+',
131 | 'illuminate\.query',
132 | 'connection\..+',
133 | 'eloquent\..+',
134 | 'kernel\.handled',
135 | 'illuminate\.log',
136 | 'mailer\.sending',
137 | 'router\.(?:before|after|matched)',
138 | 'router.filter:.+',
139 | 'locale\.changed',
140 | 'clockwork\..+'
141 | ];
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/Clockwork/DataSource/LaravelCacheDataSource.php:
--------------------------------------------------------------------------------
1 | 0, 'hit' => 0, 'write' => 0, 'delete' => 0
21 | ];
22 |
23 | // Whether we are collecting cache queries or stats only
24 | protected $collectQueries = true;
25 |
26 | // Whether we are collecting values from cache queries
27 | protected $collectValues = true;
28 |
29 | // Create a new data source instance, takes an event dispatcher and additional options as argument
30 | public function __construct(EventDispatcher $eventDispatcher, $collectQueries = true, $collectValues = true)
31 | {
32 | $this->eventDispatcher = $eventDispatcher;
33 |
34 | $this->collectQueries = $collectQueries;
35 | $this->collectValues = $collectValues;
36 | }
37 |
38 | // Adds cache queries and stats to the request
39 | public function resolve(Request $request)
40 | {
41 | $request->cacheQueries = array_merge($request->cacheQueries, $this->queries);
42 |
43 | $request->cacheReads += $this->count['read'];
44 | $request->cacheHits += $this->count['hit'];
45 | $request->cacheWrites += $this->count['write'];
46 | $request->cacheDeletes += $this->count['delete'];
47 |
48 | return $request;
49 | }
50 |
51 | // Reset the data source to an empty state, clearing any collected data
52 | public function reset()
53 | {
54 | $this->queries = [];
55 |
56 | $this->count = [
57 | 'read' => 0, 'hit' => 0, 'write' => 0, 'delete' => 0
58 | ];
59 | }
60 |
61 | // Start listening to cache events
62 | public function listenToEvents()
63 | {
64 | if (class_exists(\Illuminate\Cache\Events\CacheHit::class)) {
65 | $this->eventDispatcher->listen(\Illuminate\Cache\Events\CacheHit::class, function ($event) {
66 | $this->registerQuery([ 'type' => 'hit', 'key' => $event->key, 'value' => $event->value ]);
67 | });
68 | $this->eventDispatcher->listen(\Illuminate\Cache\Events\CacheMissed::class, function ($event) {
69 | $this->registerQuery([ 'type' => 'miss', 'key' => $event->key ]);
70 | });
71 | $this->eventDispatcher->listen(\Illuminate\Cache\Events\KeyWritten::class, function ($event) {
72 | $this->registerQuery([
73 | 'type' => 'write', 'key' => $event->key, 'value' => $event->value,
74 | 'expiration' => property_exists($event, 'seconds') ? $event->seconds : $event->minutes * 60
75 | ]);
76 | });
77 | $this->eventDispatcher->listen(\Illuminate\Cache\Events\KeyForgotten::class, function ($event) {
78 | $this->registerQuery([ 'type' => 'delete', 'key' => $event->key ]);
79 | });
80 | } else {
81 | // legacy Laravel 5.1 style events
82 | $this->eventDispatcher->listen('cache.hit', function ($key, $value) {
83 | $this->registerQuery([ 'type' => 'hit', 'key' => $key, 'value' => $value ]);
84 | });
85 | $this->eventDispatcher->listen('cache.missed', function ($key) {
86 | $this->registerQuery([ 'type' => 'miss', 'key' => $key ]);
87 | });
88 | $this->eventDispatcher->listen('cache.write', function ($key, $value, $minutes) {
89 | $this->registerQuery([
90 | 'type' => 'write', 'key' => $key, 'value' => $value, 'expiration' => $minutes * 60
91 | ]);
92 | });
93 | $this->eventDispatcher->listen('cache.delete', function ($key) {
94 | $this->registerQuery([ 'type' => 'delete', 'key' => $key ]);
95 | });
96 | }
97 | }
98 |
99 | // Collect an executed query
100 | protected function registerQuery(array $query)
101 | {
102 | $trace = StackTrace::get()->resolveViewName();
103 |
104 | $record = [
105 | 'type' => $query['type'],
106 | 'key' => $query['key'],
107 | 'expiration' => isset($query['expiration']) ? $query['expiration'] : null,
108 | 'time' => microtime(true),
109 | 'connection' => null,
110 | 'trace' => (new Serializer)->trace($trace)
111 | ];
112 |
113 | if ($this->collectValues && isset($query['value'])) {
114 | $record['value'] = (new Serializer)->normalize($query['value']);
115 | }
116 |
117 | $this->incrementQueryCount($record);
118 |
119 | if ($this->collectQueries && $this->passesFilters([ $record ])) {
120 | $this->queries[] = $record;
121 | }
122 | }
123 |
124 | // Increment query counts for collected query
125 | protected function incrementQueryCount($query)
126 | {
127 | if ($query['type'] == 'write') {
128 | $this->count['write']++;
129 | } elseif ($query['type'] == 'delete') {
130 | $this->count['delete']++;
131 | } else {
132 | $this->count['read']++;
133 |
134 | if ($query['type'] == 'hit') {
135 | $this->count['hit']++;
136 | }
137 | }
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/Clockwork/DataSource/PhpDataSource.php:
--------------------------------------------------------------------------------
1 | time = PHP_SAPI !== 'cli' ? $this->getRequestTime() : $request->time;
14 | $request->method = $this->getRequestMethod();
15 | $request->url = $this->getRequestUrl();
16 | $request->uri = $this->getRequestUri();
17 | $request->headers = $this->getRequestHeaders();
18 | $request->getData = $this->getGetData();
19 | $request->postData = $this->getPostData();
20 | $request->requestData = $this->getRequestData();
21 | $request->sessionData = $this->getSessionData();
22 | $request->cookies = $this->getCookies();
23 | $request->responseStatus = $this->getResponseStatus();
24 | $request->responseTime = $this->getResponseTime();
25 | $request->memoryUsage = $this->getMemoryUsage();
26 |
27 | return $request;
28 | }
29 |
30 | // Get the request cookies (normalized with passwords removed)
31 | protected function getCookies()
32 | {
33 | return $this->removePasswords((new Serializer)->normalizeEach($_COOKIE));
34 | }
35 |
36 | // Get the request GET data (normalized with passwords removed)
37 | protected function getGetData()
38 | {
39 | return $this->removePasswords((new Serializer)->normalizeEach($_GET));
40 | }
41 |
42 | // Get the request POST data (normalized with passwords removed)
43 | protected function getPostData()
44 | {
45 | return $this->removePasswords((new Serializer)->normalizeEach($_POST));
46 | }
47 |
48 | // Get the request body data (attempt to parse as json, normalized with passwords removed)
49 | protected function getRequestData()
50 | {
51 | // The data will already be parsed into POST data by PHP in case of application/x-www-form-urlencoded requests
52 | if (count($_POST)) return;
53 |
54 | $requestData = file_get_contents('php://input');
55 | $requestJsonData = json_decode($requestData, true);
56 |
57 | return is_array($requestJsonData)
58 | ? $this->removePasswords((new Serializer)->normalizeEach($requestJsonData))
59 | : $requestData;
60 | }
61 |
62 | // Get the request headers
63 | protected function getRequestHeaders()
64 | {
65 | $headers = [];
66 |
67 | foreach ($_SERVER as $key => $value) {
68 | if (substr($key, 0, 5) !== 'HTTP_') continue;
69 |
70 | $header = substr($key, 5);
71 | $header = str_replace('_', ' ', $header);
72 | $header = ucwords(strtolower($header));
73 | $header = str_replace(' ', '-', $header);
74 |
75 | if (! isset($headers[$header])) {
76 | $headers[$header] = [ $value ];
77 | } else {
78 | $headers[$header][] = $value;
79 | }
80 | }
81 |
82 | ksort($headers);
83 |
84 | return $headers;
85 | }
86 |
87 | // Get the request method
88 | protected function getRequestMethod()
89 | {
90 | if (isset($_SERVER['REQUEST_METHOD'])) {
91 | return $_SERVER['REQUEST_METHOD'];
92 | }
93 | }
94 |
95 | // Get the response time
96 | protected function getRequestTime()
97 | {
98 | if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
99 | return $_SERVER['REQUEST_TIME_FLOAT'];
100 | }
101 | }
102 |
103 | // Get the request URL
104 | protected function getRequestUrl()
105 | {
106 | $https = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on';
107 | $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : null;
108 | $addr = isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : null;
109 | $port = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : null;
110 | $uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : null;
111 |
112 | $scheme = $https ? 'https' : 'http';
113 | $host = $host ?: $addr;
114 | $port = (! $https && $port != 80 || $https && $port != 443) ? ":{$port}" : '';
115 |
116 | // remove port number from the host
117 | $host = $host ? preg_replace('/:\d+$/', '', trim($host)) : null;
118 |
119 | return "{$scheme}://{$host}{$port}{$uri}";
120 | }
121 |
122 | // Get the request URI
123 | protected function getRequestUri()
124 | {
125 | if (isset($_SERVER['REQUEST_URI'])) {
126 | return $_SERVER['REQUEST_URI'];
127 | }
128 | }
129 |
130 | // Get the response status code
131 | protected function getResponseStatus()
132 | {
133 | return http_response_code();
134 | }
135 |
136 | // Get the response time (current time, assuming most of the application code has already run at this point)
137 | protected function getResponseTime()
138 | {
139 | return microtime(true);
140 | }
141 |
142 | // Get the session data (normalized with passwords removed)
143 | protected function getSessionData()
144 | {
145 | if (! isset($_SESSION)) return [];
146 |
147 | return $this->removePasswords((new Serializer)->normalizeEach($_SESSION));
148 | }
149 |
150 | // Get the peak memory usage in bytes
151 | protected function getMemoryUsage()
152 | {
153 | return memory_get_peak_usage(true);
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/Clockwork/Helpers/Serializer.php:
--------------------------------------------------------------------------------
1 | [
15 | \Illuminate\Container\Container::class,
16 | \Illuminate\Foundation\Application::class,
17 | \Laravel\Lumen\Application::class
18 | ],
19 | 'limit' => 10,
20 | 'toArray' => false,
21 | 'toString' => false,
22 | 'debugInfo' => true,
23 | 'jsonSerialize' => false,
24 | 'traces' => true,
25 | 'tracesFilter' => null,
26 | 'tracesSkip' => null,
27 | 'tracesLimit' => null
28 | ];
29 |
30 | // Create a new instance optionally with options overriding defaults
31 | public function __construct(array $options = [])
32 | {
33 | $this->options = $options + static::$defaults;
34 | }
35 |
36 | // Set default options for all new instances
37 | public static function defaults(array $defaults)
38 | {
39 | static::$defaults = $defaults + static::$defaults;
40 | }
41 |
42 | // Prepares the passed data to be ready for serialization, takes any kind of data to normalize as the first
43 | // argument, other arguments are used internally in recursion
44 | public function normalize($data, $context = null, $limit = null)
45 | {
46 | if ($context === null) $context = [ 'references' => [] ];
47 | if ($limit === null) $limit = $this->options['limit'];
48 |
49 | if (is_array($data)) {
50 | if ($limit === 0) return [ '__type__' => 'array', '__omitted__' => 'limit' ];
51 |
52 | return [ '__type__' => 'array' ] + $this->normalizeEach($data, $context, $limit - 1);
53 | } elseif (is_object($data)) {
54 | if ($data instanceof \Closure) return [ '__type__' => 'anonymous function' ];
55 |
56 | $className = get_class($data);
57 | $objectHash = spl_object_hash($data);
58 |
59 | if ($className === '__PHP_Incomplete_Class') return [ '__class__' => $className ];
60 |
61 | if ($limit === 0) return [ '__class__' => $className, '__omitted__' => 'limit' ];
62 |
63 | if (isset($context['references'][$objectHash])) return [ '__type__' => 'recursion' ];
64 |
65 | $context['references'][$objectHash] = true;
66 |
67 | if (isset($this->cache[$objectHash])) return $this->cache[$objectHash];
68 |
69 | if ($this->options['blackbox'] && in_array($className, $this->options['blackbox'])) {
70 | return $this->cache[$objectHash] = [ '__class__' => $className, '__omitted__' => 'blackbox' ];
71 | } elseif ($this->options['toString'] && method_exists($data, '__toString')) {
72 | return $this->cache[$objectHash] = (string) $data;
73 | }
74 |
75 | if ($this->options['debugInfo'] && method_exists($data, '__debugInfo')) {
76 | $data = (array) $data->__debugInfo();
77 | } elseif ($this->options['jsonSerialize'] && method_exists($data, 'jsonSerialize')) {
78 | $data = (array) $data->jsonSerialize();
79 | } elseif ($this->options['toArray'] && method_exists($data, 'toArray')) {
80 | $data = (array) $data->toArray();
81 | } else {
82 | $data = (array) $data;
83 | }
84 |
85 | $data = array_combine(
86 | array_map(function ($key) {
87 | // replace null-byte prefixes of protected and private properties used by php with * (protected)
88 | // and ~ (private)
89 | return preg_replace('/^\0.+?\0/', '~', str_replace("\0*\0", '*', $key));
90 | }, array_keys($data)),
91 | $this->normalizeEach($data, $context, $limit - 1)
92 | );
93 |
94 | return $this->cache[$objectHash] = [ '__class__' => $className ] + $data;
95 | } elseif (is_resource($data)) {
96 | return [ '__type__' => 'resource' ];
97 | }
98 |
99 | return $data;
100 | }
101 |
102 | // Normalize each member of an array (doesn't add metadata for top level)
103 | public function normalizeEach($data, $context = null, $limit = null) {
104 | return array_map(function ($item) use ($context, $limit) {
105 | return $this->normalize($item, $context, $limit);
106 | }, $data);
107 | }
108 |
109 | // Normalize a stack trace instance
110 | public function trace(StackTrace $trace)
111 | {
112 | if (! $this->options['traces']) return null;
113 |
114 | if ($this->options['tracesFilter']) $trace = $trace->filter($this->options['tracesFilter']);
115 | if ($this->options['tracesSkip']) $trace = $trace->skip($this->options['tracesSkip']);
116 | if ($this->options['tracesLimit']) $trace = $trace->limit($this->options['tracesLimit']);
117 |
118 | return array_map(function ($frame) {
119 | return [
120 | 'call' => $frame->call,
121 | 'file' => $frame->file,
122 | 'line' => $frame->line,
123 | 'isVendor' => (bool) $frame->vendor
124 | ];
125 | }, $trace->frames());
126 | }
127 |
128 | // Normalize an exception instance
129 | public function exception(/* Throwable */ $exception)
130 | {
131 | return [
132 | 'type' => get_class($exception),
133 | 'message' => $exception->getMessage(),
134 | 'code' => $exception->getCode(),
135 | 'file' => $exception->getFile(),
136 | 'line' => $exception->getLine(),
137 | 'trace' => (new Serializer([ 'tracesLimit' => false ]))->trace(StackTrace::from($exception->getTrace())),
138 | 'previous' => $exception->getPrevious() ? $this->exception($exception->getPrevious()) : null
139 | ];
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/Clockwork/DataSource/LaravelHttpClientDataSource.php:
--------------------------------------------------------------------------------
1 | dispatcher = $dispatcher;
32 |
33 | $this->collectContent = $collectContent;
34 | $this->collectRawContent = $collectRawContent;
35 | }
36 |
37 | // Add sent notifications to the request
38 | public function resolve(Request $request)
39 | {
40 | $request->httpRequests = array_merge($request->httpRequests, $this->requests);
41 |
42 | return $request;
43 | }
44 |
45 | // Reset the data source to an empty state, clearing any collected data
46 | public function reset()
47 | {
48 | $this->requests = [];
49 | $this->executingRequests = [];
50 | }
51 |
52 | // Listen to the email and notification events
53 | public function listenToEvents()
54 | {
55 | $this->dispatcher->listen(ConnectionFailed::class, function ($event) { $this->connectionFailed($event); });
56 | $this->dispatcher->listen(RequestSending::class, function ($event) { $this->sendingRequest($event); });
57 | $this->dispatcher->listen(ResponseReceived::class, function ($event) { $this->responseReceived($event); });
58 | }
59 |
60 | // Collect an executing request
61 | protected function sendingRequest(RequestSending $event)
62 | {
63 | $trace = StackTrace::get()->resolveViewName();
64 |
65 | $request = (object) [
66 | 'request' => (object) [
67 | 'method' => $event->request->method(),
68 | 'url' => $this->removeAuthFromUrl($event->request->url()),
69 | 'headers' => $event->request->headers(),
70 | 'content' => $this->collectContent ? $event->request->data() : null,
71 | 'body' => $this->collectRawContent ? $event->request->body() : null
72 | ],
73 | 'response' => null,
74 | 'stats' => null,
75 | 'error' => null,
76 | 'time' => microtime(true),
77 | 'trace' => (new Serializer)->trace($trace)
78 | ];
79 |
80 | if ($this->passesFilters([ $request ])) {
81 | $this->requests[] = $this->executingRequests[spl_object_hash($event->request)] = $request;
82 | }
83 | }
84 |
85 | // Update last request with response details and time taken
86 | protected function responseReceived($event)
87 | {
88 | if (! isset($this->executingRequests[spl_object_hash($event->request)])) return;
89 |
90 | $request = $this->executingRequests[spl_object_hash($event->request)];
91 | $stats = $event->response->handlerStats();
92 |
93 | $request->duration = (microtime(true) - $request->time) * 1000;
94 | $request->response = (object) [
95 | 'status' => $event->response->status(),
96 | 'headers' => $event->response->headers(),
97 | 'content' => $this->collectContent ? $event->response->json() : null,
98 | 'body' => $this->collectRawContent ? $event->response->body() : null
99 | ];
100 | $request->stats = (object) [
101 | 'timing' => isset($stats['total_time_us']) ? (object) [
102 | 'lookup' => $stats['namelookup_time_us'] / 1000,
103 | 'connect' => ($stats['pretransfer_time_us'] - $stats['namelookup_time_us']) / 1000,
104 | 'waiting' => ($stats['starttransfer_time_us'] - $stats['pretransfer_time_us']) / 1000,
105 | 'transfer' => ($stats['total_time_us'] - $stats['starttransfer_time_us']) / 1000
106 | ] : null,
107 | 'size' => (object) [
108 | 'upload' => isset($stats['size_upload']) ? $stats['size_upload'] : null,
109 | 'download' => isset($stats['size_download']) ? $stats['size_download'] : null
110 | ],
111 | 'speed' => (object) [
112 | 'upload' => isset($stats['speed_upload']) ? $stats['speed_upload'] : null,
113 | 'download' => isset($stats['speed_download']) ? $stats['speed_download'] : null
114 | ],
115 | 'hosts' => (object) [
116 | 'local' => isset($stats['local_ip']) ? [ 'ip' => $stats['local_ip'], 'port' => $stats['local_port'] ] : null,
117 | 'remote' => isset($stats['primary_ip']) ? [ 'ip' => $stats['primary_ip'], 'port' => $stats['primary_port'] ] : null
118 | ],
119 | 'version' => isset($stats['http_version']) ? $stats['http_version'] : null
120 | ];
121 |
122 | unset($this->executingRequests[spl_object_hash($event->request)]);
123 | }
124 |
125 | // Update last request with error when connection fails
126 | protected function connectionFailed($event)
127 | {
128 | if (! isset($this->executingRequests[spl_object_hash($event->request)])) return;
129 |
130 | $request = $this->executingRequests[spl_object_hash($event->request)];
131 |
132 | $request->duration = (microtime(true) - $request->time) * 1000;
133 | $request->error = 'connection-failed';
134 |
135 | unset($this->executingRequests[spl_object_hash($event->request)]);
136 | }
137 |
138 | // Removes username and password from the URL
139 | protected function removeAuthFromUrl($url)
140 | {
141 | return preg_replace('#^(.+?://)(.+?@)(.*)$#', '$1$3', $url);
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/Clockwork/Storage/Search.php:
--------------------------------------------------------------------------------
1 | $condition = isset($search[$condition]) ? $search[$condition] : [];
27 | }
28 |
29 | foreach ([ 'stopOnFirstMismatch' ] as $option) {
30 | $this->$option = isset($options[$option]) ? $options[$option] : $this->$option;
31 | }
32 |
33 | $this->method = array_map('strtolower', $this->method);
34 | }
35 |
36 | // Create a new instance from request input
37 | public static function fromRequest($data = [])
38 | {
39 | return new static($data);
40 | }
41 |
42 | // Check whether the request matches current search parameters
43 | public function matches(Request $request)
44 | {
45 | if ($request->type == RequestType::COMMAND) {
46 | return $this->matchesCommand($request);
47 | } elseif ($request->type == RequestType::QUEUE_JOB) {
48 | return $this->matchesQueueJob($request);
49 | } elseif ($request->type == RequestType::TEST) {
50 | return $this->matchesTest($request);
51 | } else {
52 | return $this->matchesRequest($request);
53 | }
54 | }
55 |
56 | // Check whether a request type request matches
57 | protected function matchesRequest(Request $request)
58 | {
59 | return $this->matchesString($this->type, RequestType::REQUEST)
60 | && $this->matchesString($this->uri, $request->uri)
61 | && $this->matchesString($this->controller, $request->controller)
62 | && $this->matchesExact($this->method, strtolower($request->method))
63 | && $this->matchesNumber($this->status, $request->responseStatus)
64 | && $this->matchesNumber($this->time, $request->responseDuration)
65 | && $this->matchesDate($this->received, $request->time);
66 | }
67 |
68 | // Check whether a command type request matches
69 | protected function matchesCommand(Request $request)
70 | {
71 | return $this->matchesString($this->type, RequestType::COMMAND)
72 | && $this->matchesString($this->name, $request->commandName)
73 | && $this->matchesNumber($this->status, $request->commandExitCode)
74 | && $this->matchesNumber($this->time, $request->responseDuration)
75 | && $this->matchesDate($this->received, $request->time);
76 | }
77 |
78 | // Check whether a queue-job type request matches
79 | protected function matchesQueueJob(Request $request)
80 | {
81 | return $this->matchesString($this->type, RequestType::QUEUE_JOB)
82 | && $this->matchesString($this->name, $request->jobName)
83 | && $this->matchesString($this->status, $request->jobStatus)
84 | && $this->matchesNumber($this->time, $request->responseDuration)
85 | && $this->matchesDate($this->received, $request->time);
86 | }
87 |
88 | // Check whether a test type request matches
89 | protected function matchesTest(Request $request)
90 | {
91 | return $this->matchesString($this->type, RequestType::TEST)
92 | && $this->matchesString($this->name, $request->testName)
93 | && $this->matchesString($this->status, $request->testStatus)
94 | && $this->matchesNumber($this->time, $request->responseDuration)
95 | && $this->matchesDate($this->received, $request->time);
96 | }
97 |
98 | // Check if there are no search parameters specified
99 | public function isEmpty()
100 | {
101 | return ! count($this->uri) && ! count($this->controller) && ! count($this->method) && ! count($this->status)
102 | && ! count($this->time) && ! count($this->received) && ! count($this->name) && ! count($this->type);
103 | }
104 |
105 | // Check if there are some search parameters specified
106 | public function isNotEmpty()
107 | {
108 | return ! $this->isEmpty();
109 | }
110 |
111 | // Check if the value matches date type search parameter
112 | protected function matchesDate($inputs, $value)
113 | {
114 | if (! count($inputs)) return true;
115 |
116 | foreach ($inputs as $input) {
117 | if (preg_match('/^<(.+)$/', $input, $match)) {
118 | if ($value < strtotime($match[1])) return true;
119 | } elseif (preg_match('/^>(.+)$/', $input, $match)) {
120 | if ($value > strtotime($match[1])) return true;
121 | }
122 | }
123 |
124 | return false;
125 | }
126 |
127 | // Check if the value matches exact type search parameter
128 | protected function matchesExact($inputs, $value)
129 | {
130 | if (! count($inputs)) return true;
131 |
132 | return in_array($value, $inputs);
133 | }
134 |
135 | // Check if the value matches number type search parameter
136 | protected function matchesNumber($inputs, $value)
137 | {
138 | if (! count($inputs)) return true;
139 |
140 | foreach ($inputs as $input) {
141 | if (preg_match('/^<(\d+(?:\.\d+)?)$/', $input, $match)) {
142 | if ($value < $match[1]) return true;
143 | } elseif (preg_match('/^>(\d+(?:\.\d+)?)$/', $input, $match)) {
144 | if ($value > $match[1]) return true;
145 | } elseif (preg_match('/^(\d+(?:\.\d+)?)-(\d+(?:\.\d+)?)$/', $input, $match)) {
146 | if ($match[1] < $value && $value < $match[2]) return true;
147 | } else {
148 | if ($value == $input) return true;
149 | }
150 | }
151 |
152 | return false;
153 | }
154 |
155 | // Check if the value matches string type search parameter
156 | protected function matchesString($inputs, $value)
157 | {
158 | if (! count($inputs)) return true;
159 |
160 | foreach ($inputs as $input) {
161 | if (strpos($value, $input) !== false) return true;
162 | }
163 |
164 | return false;
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/Clockwork/DataSource/DBALDataSource.php:
--------------------------------------------------------------------------------
1 | connection = $connection;
30 | $this->connectionName = $this->connection->getDatabase();
31 |
32 | $configuration = $this->connection->getConfiguration();
33 | $currentLogger = $configuration->getSQLLogger();
34 |
35 | if ($currentLogger === null) {
36 | $configuration->setSQLLogger($this);
37 | } else {
38 | $loggerChain = new LoggerChain;
39 | $loggerChain->addLogger($currentLogger);
40 | $loggerChain->addLogger($this);
41 |
42 | $configuration->setSQLLogger($loggerChain);
43 | }
44 | }
45 |
46 | // Adds executed database queries to the request
47 | public function resolve(Request $request)
48 | {
49 | $request->databaseQueries = array_merge($request->databaseQueries, $this->queries);
50 |
51 | return $request;
52 | }
53 |
54 | // Reset the data source to an empty state, clearing any collected data
55 | public function reset()
56 | {
57 | $this->queries = [];
58 | $this->query = null;
59 | }
60 |
61 | // DBAL SQLLogger event
62 | public function startQuery($sql, array $params = null, array $types = null)
63 | {
64 | $this->query = [
65 | 'query' => $sql,
66 | 'params' => $params,
67 | 'types' => $types,
68 | 'time' => microtime(true)
69 | ];
70 | }
71 |
72 | // DBAL SQLLogger event
73 | public function stopQuery()
74 | {
75 | $this->registerQuery($this->query);
76 | $this->query = null;
77 | }
78 |
79 | // Collect an executed database query
80 | protected function registerQuery($query)
81 | {
82 | $query = [
83 | 'query' => $this->createRunnableQuery($query['query'], $query['params'], $query['types']),
84 | 'bindings' => $query['params'],
85 | 'duration' => (microtime(true) - $query['time']) * 1000,
86 | 'connection' => $this->connectionName,
87 | 'time' => $query['time']
88 | ];
89 |
90 | if ($this->passesFilters([ $query ])) {
91 | $this->queries[] = $query;
92 | }
93 | }
94 |
95 | // Takes a query, an array of params and types as arguments, returns runnable query with upper-cased keywords
96 | protected function createRunnableQuery($query, $params, $types)
97 | {
98 | // add params to query
99 | $query = $this->replaceParams($this->connection->getDatabasePlatform(), $query, $params, $types);
100 |
101 | // highlight keywords
102 | $keywords = [
103 | 'select', 'insert', 'update', 'delete', 'into', 'values', 'set', 'where', 'from', 'limit', 'is', 'null',
104 | 'having', 'group by', 'order by', 'asc', 'desc'
105 | ];
106 | $regexp = '/\b' . implode('\b|\b', $keywords) . '\b/i';
107 |
108 | return preg_replace_callback($regexp, function ($match) { return strtoupper($match[0]); }, $query);
109 | }
110 |
111 | /**
112 | * Source at laravel-doctrine/orm LaravelDoctrine\ORM\Loggers\Formatters\ReplaceQueryParams::format().
113 | *
114 | * @param AbstractPlatform $platform
115 | * @param string $sql
116 | * @param array|null $params
117 | * @param array|null $types
118 | *
119 | *
120 | * @return string
121 | */
122 | public function replaceParams($platform, $sql, array $params = null, array $types = null)
123 | {
124 | if (is_array($params)) {
125 | foreach ($params as $key => $param) {
126 | $type = isset($types[$key]) ? $types[$key] : null; // Originally used null coalescing
127 | $param = $this->convertParam($platform, $param, $type);
128 | $sql = preg_replace('/\?/', "$param", $sql, 1);
129 | }
130 | }
131 | return $sql;
132 | }
133 |
134 | /**
135 | * Source at laravel-doctrine/orm LaravelDoctrine\ORM\Loggers\Formatters\ReplaceQueryParams::convertParam().
136 | *
137 | * @param mixed $param
138 | *
139 | * @throws \Exception
140 | * @return string
141 | */
142 | protected function convertParam($platform, $param, $type = null)
143 | {
144 | if (is_object($param)) {
145 | if (!method_exists($param, '__toString')) {
146 | if ($param instanceof \DateTimeInterface) {
147 | $param = $param->format('Y-m-d H:i:s');
148 | } elseif (Type::hasType($type)) {
149 | $type = Type::getType($type);
150 | $param = $type->convertToDatabaseValue($param, $platform);
151 | } else {
152 | throw new \Exception('Given query param is an instance of ' . get_class($param) . ' and could not be converted to a string');
153 | }
154 | }
155 | } elseif (is_array($param)) {
156 | if ($this->isNestedArray($param)) {
157 | $param = json_encode($param, JSON_UNESCAPED_UNICODE);
158 | } else {
159 | $param = implode(
160 | ', ',
161 | array_map(
162 | function ($part) {
163 | return '"' . (string) $part . '"';
164 | },
165 | $param
166 | )
167 | );
168 | return '(' . $param . ')';
169 | }
170 | } else {
171 | $param = htmlspecialchars((string) $param); // Originally used the e() Laravel helper
172 | }
173 | return '"' . (string) $param . '"';
174 | }
175 |
176 | /**
177 | * Source at laravel-doctrine/orm LaravelDoctrine\ORM\Loggers\Formatters\ReplaceQueryParams::isNestedArray().
178 | *
179 | * @param array $array
180 | * @return bool
181 | */
182 | private function isNestedArray(array $array)
183 | {
184 | foreach ($array as $key => $value) {
185 | if (is_array($value)) {
186 | return true;
187 | }
188 | }
189 | return false;
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/Clockwork/Storage/SqlSearch.php:
--------------------------------------------------------------------------------
1 | pdo = $pdo;
26 |
27 | list($this->conditions, $this->bindings) = $this->resolveConditions();
28 |
29 | $this->buildQuery();
30 | }
31 |
32 | // Creates a new instance from a base Search class instance
33 | public static function fromBase(Search $search = null, PDO $pdo = null)
34 | {
35 | return new static((array) $search, $pdo);
36 | }
37 |
38 | // Add an additional where condition, takes the SQL condition and array of bindings
39 | public function addCondition($condition, $bindings = [])
40 | {
41 | $this->conditions = array_merge([ $condition ], $this->conditions);
42 | $this->bindings = array_merge($bindings, $this->bindings);
43 | $this->buildQuery();
44 |
45 | return $this;
46 | }
47 |
48 | // Resolve SQL conditions and bindings based on the search parameters
49 | protected function resolveConditions()
50 | {
51 | if ($this->isEmpty()) return [ [], [] ];
52 |
53 | $conditions = array_filter([
54 | $this->resolveStringCondition([ 'type' ], $this->type),
55 | $this->resolveStringCondition([ 'uri', 'commandName', 'jobName', 'testName' ], array_merge($this->uri, $this->name)),
56 | $this->resolveStringCondition([ 'controller' ], $this->controller),
57 | $this->resolveExactCondition('method', $this->method),
58 | $this->resolveNumberCondition([ 'responseStatus', 'commandExitCode', 'jobStatus', 'testStatus' ], $this->status),
59 | $this->resolveNumberCondition([ 'responseDuration' ], $this->time),
60 | $this->resolveDateCondition([ 'time' ], $this->received)
61 | ]);
62 |
63 | $sql = array_map(function ($condition) { return $condition[0]; }, $conditions);
64 | $bindings = array_reduce($conditions, function ($bindings, $condition) {
65 | return array_merge($bindings, $condition[1]);
66 | }, []);
67 |
68 | return [ $sql, $bindings ];
69 | }
70 |
71 | // Resolve a date type condition and bindings
72 | protected function resolveDateCondition($fields, $inputs)
73 | {
74 | if (! count($inputs)) return null;
75 |
76 | $bindings = [];
77 | $conditions = implode(' OR ', array_map(function ($field) use ($inputs, &$bindings) {
78 | return implode(' OR ', array_map(function ($input, $index) use ($field, &$bindings) {
79 | if (preg_match('/^<(.+)$/', $input, $match)) {
80 | $bindings["{$field}{$index}"] = $match[1];
81 | return $this->quote($field) . " < :{$field}{$index}";
82 | } elseif (preg_match('/^>(.+)$/', $input, $match)) {
83 | $bindings["{$field}{$index}"] = $match[1];
84 | return $this->quote($field). " > :{$field}{$index}";
85 | }
86 | }, $inputs, array_keys($inputs)));
87 | }, $fields));
88 |
89 | return [ "({$conditions})", $bindings ];
90 | }
91 |
92 | // Resolve an exact type condition and bindings
93 | protected function resolveExactCondition($field, $inputs)
94 | {
95 | if (! count($inputs)) return null;
96 |
97 | $bindings = [];
98 | $values = implode(', ', array_map(function ($input, $index) use ($field, &$bindings) {
99 | $bindings["{$field}{$index}"] = $input;
100 | return ":{$field}{$index}";
101 | }, $inputs, array_keys($inputs)));
102 |
103 | return [ $this->quote($field) . " IN ({$values})", $bindings ];
104 | }
105 |
106 | // Resolve a number type condition and bindings
107 | protected function resolveNumberCondition($fields, $inputs)
108 | {
109 | if (! count($inputs)) return null;
110 |
111 | $bindings = [];
112 | $conditions = implode(' OR ', array_map(function ($field) use ($inputs, &$bindings) {
113 | return implode(' OR ', array_map(function ($input, $index) use ($field, &$bindings) {
114 | if (preg_match('/^<(\d+(?:\.\d+)?)$/', $input, $match)) {
115 | $bindings["{$field}{$index}"] = $match[1];
116 | return $this->quote($field) . " < :{$field}{$index}";
117 | } elseif (preg_match('/^>(\d+(?:\.\d+)?)$/', $input, $match)) {
118 | $bindings["{$field}{$index}"] = $match[1];
119 | return $this->quote($field) . " > :{$field}{$index}";
120 | } elseif (preg_match('/^(\d+(?:\.\d+)?)-(\d+(?:\.\d+)?)$/', $input, $match)) {
121 | $bindings["{$field}{$index}from"] = $match[1];
122 | $bindings["{$field}{$index}to"] = $match[2];
123 | $quotedField = $this->quote($field);
124 | return "({$quotedField} > :{$field}{$index}from AND {$quotedField} < :{$field}{$index}to)";
125 | } else {
126 | $bindings["{$field}{$index}"] = $input;
127 | return $this->quote($field) . " = :{$field}{$index}";
128 | }
129 | }, $inputs, array_keys($inputs)));
130 | }, $fields));
131 |
132 | return [ "({$conditions})", $bindings ];
133 | }
134 |
135 | // Resolve a string type condition and bindings
136 | protected function resolveStringCondition($fields, $inputs)
137 | {
138 | if (! count($inputs)) return null;
139 |
140 | $bindings = [];
141 | $conditions = implode(' OR ', array_map(function ($field) use ($inputs, &$bindings) {
142 | return implode(' OR ', array_map(function ($input, $index) use ($field, &$bindings) {
143 | $bindings["{$field}{$index}"] = $input;
144 | return $this->quote($field) . " LIKE :{$field}{$index}";
145 | }, $inputs, array_keys($inputs)));
146 | }, $fields));
147 |
148 | return [ "({$conditions})", $bindings ];
149 | }
150 |
151 | // Build the where part of the SQL query
152 | protected function buildQuery()
153 | {
154 | $this->query = count($this->conditions) ? 'WHERE ' . implode(' AND ', $this->conditions) : '';
155 | }
156 |
157 | // Quotes SQL identifier name properly for the current database
158 | protected function quote($identifier)
159 | {
160 | return $this->pdo && $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) == 'mysql' ? "`{$identifier}`" : "\"{$identifier}\"";
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/Clockwork/DataSource/GuzzleDataSource.php:
--------------------------------------------------------------------------------
1 | collectContent = $collectContent;
30 | $this->collectRawContent = $collectRawContent;
31 | }
32 |
33 | // Returns a new Guzzle instance, pre-configured with Clockwork support
34 | public function instance(array $config = [])
35 | {
36 | return new Client($this->configure($config));
37 | }
38 |
39 | // Updates Guzzle configuration array with Clockwork support
40 | public function configure(array $config = [])
41 | {
42 | $handler = isset($config['handler']) ? $config['handler'] : HandlerStack::create();
43 |
44 | $handler->push($this);
45 |
46 | $config['handler'] = $handler;
47 |
48 | return $config;
49 | }
50 |
51 | // Add sent notifications to the request
52 | public function resolve(Request $request)
53 | {
54 | $request->httpRequests = array_merge($request->httpRequests, $this->requests);
55 |
56 | return $request;
57 | }
58 |
59 | // Reset the data source to an empty state, clearing any collected data
60 | public function reset()
61 | {
62 | $this->requests = [];
63 | $this->executingRequests = [];
64 | }
65 |
66 | // Guzzle middleware implemenation, that does the requests logging itself
67 | public function __invoke(callable $handler): callable
68 | {
69 | return function(RequestInterface $request, array $options) use ($handler): PromiseInterface {
70 | $time = microtime(true);
71 | $stats = null;
72 |
73 | $originalOnStats = isset($options['on_stats']) ? $options['on_stats'] : null;
74 | $options['on_stats'] = function (TransferStats $transferStats) use (&$stats, $originalOnStats) {
75 | $stats = $transferStats->getHandlerStats();
76 | if ($originalOnStats) $originalOnStats($transferStats);
77 | };
78 |
79 | return $handler($request, $options)
80 | ->then(function(ResponseInterface $response) use ($request, $time, $stats) {
81 | $this->collectRequest($request, $response, $time, $stats);
82 |
83 | return $response;
84 | }, function(GuzzleException $exception) use ($request, $time, $stats) {
85 | $response = $exception instanceof RequestException ? $exception->getResponse() : null;
86 | $this->collectRequest($request, $response, $time, $stats);
87 |
88 | throw $exception;
89 | });
90 | };
91 | }
92 |
93 | // Collect a request-response pair
94 | protected function collectRequest($request, $response = null, $startTime = null, $stats = null)
95 | {
96 | $trace = StackTrace::get();
97 |
98 | $request = (object) [
99 | 'request' => (object) [
100 | 'method' => $request->getMethod(),
101 | 'url' => $this->removeAuthFromUrl((string) $request->getUri()),
102 | 'headers' => $request->getHeaders(),
103 | 'content' => $this->collectContent ? $this->resolveRequestContent($request) : null,
104 | 'body' => $this->collectRawContent ? (string) $request->getBody() : null
105 | ],
106 | 'response' => (object) [
107 | 'status' => (int) $response->getStatusCode(),
108 | 'headers' => $response->getHeaders(),
109 | 'content' => $this->collectContent ? json_decode((string) $response->getBody(), true) : null,
110 | 'body' => $this->collectRawContent ? (string) $response->getBody() : null
111 | ],
112 | 'stats' => $stats ? (object) [
113 | 'timing' => isset($stats['total_time_us']) ? (object) [
114 | 'lookup' => $stats['namelookup_time_us'] / 1000,
115 | 'connect' => ($stats['pretransfer_time_us'] - $stats['namelookup_time_us']) / 1000,
116 | 'waiting' => ($stats['starttransfer_time_us'] - $stats['pretransfer_time_us']) / 1000,
117 | 'transfer' => ($stats['total_time_us'] - $stats['starttransfer_time_us']) / 1000
118 | ] : null,
119 | 'size' => (object) [
120 | 'upload' => isset($stats['size_upload']) ? $stats['size_upload'] : null,
121 | 'download' => isset($stats['size_download']) ? $stats['size_download'] : null
122 | ],
123 | 'speed' => (object) [
124 | 'upload' => isset($stats['speed_upload']) ? $stats['speed_upload'] : null,
125 | 'download' => isset($stats['speed_download']) ? $stats['speed_download'] : null
126 | ],
127 | 'hosts' => (object) [
128 | 'local' => isset($stats['local_ip']) ? [ 'ip' => $stats['local_ip'], 'port' => $stats['local_port'] ] : null,
129 | 'remote' => isset($stats['primary_ip']) ? [ 'ip' => $stats['primary_ip'], 'port' => $stats['primary_port'] ] : null
130 | ],
131 | 'version' => isset($stats['http_version']) ? $stats['http_version'] : null
132 | ] : null,
133 | 'error' => null,
134 | 'time' => $startTime,
135 | 'duration' => (microtime(true) - $startTime) * 1000,
136 | 'trace' => (new Serializer)->trace($trace)
137 | ];
138 |
139 | if ($this->passesFilters([ $request ])) {
140 | $this->requests[] = $request;
141 | }
142 | }
143 |
144 | // Resolve request content, with support for form data and json requests
145 | protected function resolveRequestContent($request)
146 | {
147 | $body = (string) $request->getBody();
148 | $headers = $request->getHeaders();
149 |
150 | if (isset($headers['Content-Type']) && $headers['Content-Type'][0] == 'application/x-www-form-urlencoded') {
151 | parse_str($body, $parameters);
152 | return $parameters;
153 | } elseif (isset($headers['Content-Type']) && strpos($headers['Content-Type'][0], 'json') !== false) {
154 | return json_decode($body, true);
155 | }
156 |
157 | return [];
158 | }
159 |
160 | // Removes username and password from the URL
161 | protected function removeAuthFromUrl($url)
162 | {
163 | return preg_replace('#^(.+?://)(.+?@)(.*)$#', '$1$3', $url);
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/Clockwork/DataSource/LumenDataSource.php:
--------------------------------------------------------------------------------
1 | app = $app;
33 |
34 | $this->collectLog = $collectLog;
35 | $this->collectRoutes = $collectRoutes;
36 |
37 | $this->log = new Log;
38 | }
39 |
40 | // Adds request, response information, middleware, routes, session data, user and log entries to the request
41 | public function resolve(Request $request)
42 | {
43 | $request->method = $this->getRequestMethod();
44 | $request->uri = $this->getRequestUri();
45 | $request->controller = $this->getController();
46 | $request->headers = $this->getRequestHeaders();
47 | $request->responseStatus = $this->getResponseStatus();
48 | $request->routes = $this->getRoutes();
49 | $request->sessionData = $this->getSessionData();
50 |
51 | $this->resolveAuthenticatedUser($request);
52 |
53 | $request->log()->merge($this->log);
54 |
55 | return $request;
56 | }
57 |
58 | // Reset the data source to an empty state, clearing any collected data
59 | public function reset()
60 | {
61 | $this->log = new Log;
62 | }
63 |
64 | // Set Lumen response instance for the current request
65 | public function setResponse(Response $response)
66 | {
67 | $this->response = $response;
68 | return $this;
69 | }
70 |
71 | // Listen for the log events
72 | public function listenToEvents()
73 | {
74 | if (! $this->collectLog) return;
75 |
76 | if (class_exists(\Illuminate\Log\Events\MessageLogged::class)) {
77 | // Lumen 5.4
78 | $this->app['events']->listen(\Illuminate\Log\Events\MessageLogged::class, function ($event) {
79 | $this->log->log($event->level, $event->message, $event->context);
80 | });
81 | } else {
82 | // Lumen 5.0 to 5.3
83 | $this->app['events']->listen('illuminate.log', function ($level, $message, $context) {
84 | $this->log->log($level, $message, $context);
85 | });
86 | }
87 | }
88 |
89 | // Get a textual representation of current route's controller
90 | protected function getController()
91 | {
92 | $routes = method_exists($this->app, 'getRoutes') ? $this->app->getRoutes() : [];
93 |
94 | $method = $this->getRequestMethod();
95 | $pathInfo = $this->getPathInfo();
96 |
97 | if (isset($routes[$method.$pathInfo]['action']['uses'])) {
98 | $controller = $routes[$method.$pathInfo]['action']['uses'];
99 | } elseif (isset($routes[$method.$pathInfo]['action'][0])) {
100 | $controller = $routes[$method.$pathInfo]['action'][0];
101 | } else {
102 | $controller = null;
103 | }
104 |
105 | if ($controller instanceof \Closure) {
106 | $controller = 'anonymous function';
107 | } elseif (is_object($controller)) {
108 | $controller = 'instance of ' . get_class($controller);
109 | } elseif (! is_string($controller)) {
110 | $controller = null;
111 | }
112 |
113 | return $controller;
114 | }
115 |
116 | // Get the request headers
117 | protected function getRequestHeaders()
118 | {
119 | return $this->app['request']->headers->all();
120 | }
121 |
122 | // Get the request method
123 | protected function getRequestMethod()
124 | {
125 | if ($this->app->bound('request')) {
126 | return $this->app['request']->getMethod();
127 | } elseif (isset($_POST['_method'])) {
128 | return strtoupper($_POST['_method']);
129 | } else {
130 | return $_SERVER['REQUEST_METHOD'];
131 | }
132 | }
133 |
134 | // Get the request URI
135 | protected function getRequestUri()
136 | {
137 | return $this->app['request']->getRequestUri();
138 | }
139 |
140 | // Get the response status code
141 | protected function getResponseStatus()
142 | {
143 | return $this->response ? $this->response->getStatusCode() : null;
144 | }
145 |
146 | // Get an array of application routes
147 | protected function getRoutes()
148 | {
149 | if (! $this->collectRoutes) return [];
150 |
151 | if (isset($this->app->router)) {
152 | $routes = array_values($this->app->router->getRoutes());
153 | } elseif (method_exists($this->app, 'getRoutes')) {
154 | $routes = array_values($this->app->getRoutes());
155 | } else {
156 | $routes = [];
157 | }
158 |
159 | return array_map(function ($route) {
160 | return [
161 | 'method' => $route['method'],
162 | 'uri' => $route['uri'],
163 | 'name' => isset($route['action']['as']) ? $route['action']['as'] : null,
164 | 'action' => isset($route['action']['uses']) && is_string($route['action']['uses']) ? $route['action']['uses'] : 'anonymous function',
165 | 'middleware' => isset($route['action']['middleware']) ? $route['action']['middleware'] : null,
166 | ];
167 | }, $routes);
168 | }
169 |
170 | // Get the session data (normalized with passwords removed)
171 | protected function getSessionData()
172 | {
173 | if (! isset($this->app['session'])) return [];
174 |
175 | return $this->removePasswords((new Serializer)->normalizeEach($this->app['session']->all()));
176 | }
177 |
178 | // Add authenticated user data to the request
179 | protected function resolveAuthenticatedUser(Request $request)
180 | {
181 | if (! isset($this->app['auth'])) return;
182 | if (! ($user = $this->app['auth']->user())) return;
183 | if (! isset($user->email) || ! isset($user->id)) return;
184 |
185 | $request->setAuthenticatedUser($user->email, $user->id, [
186 | 'email' => $user->email,
187 | 'name' => isset($user->name) ? $user->name : null
188 | ]);
189 | }
190 |
191 | // Get the request path info
192 | protected function getPathInfo()
193 | {
194 | if ($this->app->bound('request')) {
195 | return $this->app['request']->getPathInfo();
196 | } else {
197 | $query = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '';
198 | return '/' . trim(str_replace("?{$query}", '', $_SERVER['REQUEST_URI']), '/');
199 | }
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | > Clockwork is a development tool for PHP available right in your browser. Clockwork gives you an insight into your application runtime - including request data, performance metrics, log entries, database queries, cache queries, redis commands, dispatched events, queued jobs, rendered views and more - for HTTP requests, commands, queue jobs and tests.
7 |
8 | > *This repository contains the server-side component of Clockwork.*
9 |
10 | > Check out on the [Clockwork website](https://underground.works/clockwork) for details.
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | ### Installation
40 |
41 | Install the Clockwork library via [Composer](https://getcomposer.org/).
42 |
43 | ```
44 | composer require itsgoingd/clockwork
45 | ```
46 |
47 | Congratulations, you are done! To enable more features like commands or queue jobs profiling, publish the configuration file via the `vendor:publish` Artisan command.
48 |
49 | **Note:** If you are using the Laravel route cache, you will need to refresh it using the route:cache Artisan command.
50 |
51 | Read [full installation instructions](https://underground.works/clockwork/#docs-installation) on the Clockwork website.
52 |
53 | ### Features
54 |
55 | #### Collecting data
56 |
57 | The Clockwork server-side component collects and stores data about your application.
58 |
59 | Clockwork is only active when your app is in debug mode by default. You can choose to explicitly enable or disable Clockwork, or even set Clockwork to always collect data without exposing them for further analysis.
60 |
61 | We collect a whole bunch of useful data by default, but you can enable more features or disable features you don't need in the config file.
62 |
63 | Some features might allow for advanced options, eg. for database queries you can set a slow query threshold or enable detecting of duplicate (N+1) queries. Check out the config file to see all what Clockwork can do.
64 |
65 | There are several options that allow you to choose for which requests Clockwork is active.
66 |
67 | On-demand mode will collect data only when Clockwork app is open. You can even specify a secret to be set in the app settings to collect request. Errors only will record only requests ending with 4xx and 5xx responses. Slow only will collect only requests with responses above the set slow threshold. You can also filter the collected and recorded requests by a custom closure. CORS pre-flight requests will not be collected by default.
68 |
69 | New in Clockwork 4.1, artisan commands, queue jobs and tests can now also be collected, you need to enable this in the config file.
70 |
71 | Clockwork also collects stack traces for data like log messages or database queries. Last 10 frames of the trace are collected by default. You can change the frames limit or disable this feature in the configuration file.
72 |
73 | #### Viewing data
74 |
75 | ##### Web interface
76 |
77 | Visit `/clockwork` route to view and interact with the collected data.
78 |
79 | The app will show all executed requests, which is useful when the request is not made by browser, but for example a mobile application you are developing an API for.
80 |
81 | ##### Browser extension
82 |
83 | A browser dev tools extension is also available for Chrome and Firefox:
84 |
85 | - [Chrome Web Store](https://chrome.google.com/webstore/detail/clockwork/dmggabnehkmmfmdffgajcflpdjlnoemp)
86 | - [Firefox Addons](https://addons.mozilla.org/en-US/firefox/addon/clockwork-dev-tools/)
87 |
88 | ##### Toolbar
89 |
90 | Clockwork now gives you an option to show basic request information in the form of a toolbar in your app.
91 |
92 | The toolbar is fully rendered client-side and requires installing a tiny javascript library.
93 |
94 | [Learn more](https://underground.works/clockwork/#docs-viewing-data) on the Clockwork website.
95 |
96 | #### Logging
97 |
98 | You can log any variable via the clock() helper, from a simple string to an array or object, even multiple values:
99 |
100 | ```php
101 | clock(User::first(), auth()->user(), $username)
102 | ```
103 |
104 | The `clock()` helper function returns it's first argument, so you can easily add inline debugging statements to your code:
105 |
106 | ```php
107 | User::create(clock($request->all()))
108 | ```
109 |
110 | If you want to specify a log level, you can use the long-form call:
111 |
112 | ```php
113 | clock()->info("User {$username} logged in!")
114 | ```
115 |
116 | #### Timeline
117 |
118 | Timeline gives you a visual representation of your application runtime.
119 |
120 | To add an event to the timeline - start it with a description, execute the tracked code and finish the event. A fluent api is available to further configure the event.
121 |
122 | ```php
123 | // using timeline api with begin/end and fluent configuration
124 | clock()->event('Importing tweets')->color('purple')->begin();
125 | ...
126 | clock()->event('Importing tweets')->end();
127 | ```
128 |
129 | Alternatively you can execute the tracked code block as a closure. You can also choose to use an array based configuration instead of the fluent api.
130 |
131 | ```php
132 | // using timeline api with run and array-based configuration
133 | clock()->event('Updating cache', [ 'color' => 'green' ])->run(function () {
134 | ...
135 | });
136 | ```
137 |
138 | Read more about available features on the [Clockwork website](https://underground.works/clockwork).
139 |
140 |
141 |
142 |
143 |
144 |
145 |
--------------------------------------------------------------------------------
/Clockwork/DataSource/LaravelDataSource.php:
--------------------------------------------------------------------------------
1 | app = $app;
36 |
37 | $this->collectLog = $collectLog;
38 | $this->collectRoutes = $collectRoutes;
39 | $this->routesOnlyNamespaces = $routesOnlyNamespaces;
40 |
41 | $this->log = new Log;
42 | }
43 |
44 | // Adds request, response information, middleware, routes, session data, user and log entries to the request
45 | public function resolve(Request $request)
46 | {
47 | $request->method = $this->getRequestMethod();
48 | $request->url = $this->getRequestUrl();
49 | $request->uri = $this->getRequestUri();
50 | $request->controller = $this->getController();
51 | $request->headers = $this->getRequestHeaders();
52 | $request->responseStatus = $this->getResponseStatus();
53 | $request->middleware = $this->getMiddleware();
54 | $request->routes = $this->getRoutes();
55 | $request->sessionData = $this->getSessionData();
56 |
57 | $this->resolveAuthenticatedUser($request);
58 |
59 | $request->log()->merge($this->log);
60 |
61 | return $request;
62 | }
63 |
64 | // Reset the data source to an empty state, clearing any collected data
65 | public function reset()
66 | {
67 | $this->log = new Log;
68 | }
69 |
70 | // Set Laravel application instance for the current request
71 | public function setApplication(Application $app)
72 | {
73 | $this->app = $app;
74 | return $this;
75 | }
76 |
77 | // Set Laravel response instance for the current request
78 | public function setResponse(Response $response)
79 | {
80 | $this->response = $response;
81 | return $this;
82 | }
83 |
84 | // Listen for the log events
85 | public function listenToEvents()
86 | {
87 | if (! $this->collectLog) return;
88 |
89 | if (class_exists(\Illuminate\Log\Events\MessageLogged::class)) {
90 | // Laravel 5.4
91 | $this->app['events']->listen(\Illuminate\Log\Events\MessageLogged::class, function ($event) {
92 | $this->log->log($event->level, $event->message, $event->context);
93 | });
94 | } else {
95 | // Laravel 5.0 to 5.3
96 | $this->app['events']->listen('illuminate.log', function ($level, $message, $context) {
97 | $this->log->log($level, $message, $context);
98 | });
99 | }
100 | }
101 |
102 | // Get a textual representation of the current route's controller
103 | protected function getController()
104 | {
105 | $router = $this->app['router'];
106 |
107 | $route = $router->current();
108 | $controller = $route ? $route->getActionName() : null;
109 |
110 | if ($controller instanceof \Closure) {
111 | $controller = 'anonymous function';
112 | } elseif (is_object($controller)) {
113 | $controller = 'instance of ' . get_class($controller);
114 | } elseif (is_array($controller) && count($controller) == 2) {
115 | if (is_object($controller[0])) {
116 | $controller = get_class($controller[0]) . '->' . $controller[1];
117 | } else {
118 | $controller = $controller[0] . '::' . $controller[1];
119 | }
120 | } elseif (! is_string($controller)) {
121 | $controller = null;
122 | }
123 |
124 | return $controller;
125 | }
126 |
127 | // Get the request headers
128 | protected function getRequestHeaders()
129 | {
130 | return $this->app['request']->headers->all();
131 | }
132 |
133 | // Get the request method
134 | protected function getRequestMethod()
135 | {
136 | return $this->app['request']->getMethod();
137 | }
138 |
139 | // Get the request URL
140 | protected function getRequestUrl()
141 | {
142 | return $this->app['request']->fullUrl();
143 | }
144 |
145 | // Get the request URI
146 | protected function getRequestUri()
147 | {
148 | return $this->app['request']->getRequestUri();
149 | }
150 |
151 | // Get the response status code
152 | protected function getResponseStatus()
153 | {
154 | return $this->response ? $this->response->getStatusCode() : null;
155 | }
156 |
157 | // Get an array of middleware for the matched route
158 | protected function getMiddleware()
159 | {
160 | $route = $this->app['router']->current();
161 |
162 | if (! $route) return;
163 |
164 | return method_exists($route, 'gatherMiddleware') ? $route->gatherMiddleware() : $route->middleware();
165 | }
166 |
167 | // Get an array of application routes
168 | protected function getRoutes()
169 | {
170 | if (! $this->collectRoutes) return [];
171 |
172 | return array_values(array_filter(array_map(function ($route) {
173 | $action = $route->getActionName() ?: 'anonymous function';
174 | $namespace = strpos($action, '\\') !== false ? explode('\\', $action)[0] : null;
175 |
176 | if (count($this->routesOnlyNamespaces) && ! in_array($namespace, $this->routesOnlyNamespaces)) return;
177 |
178 | return [
179 | 'method' => implode(', ', $route->methods()),
180 | 'uri' => $route->uri(),
181 | 'name' => $route->getName(),
182 | 'action' => $action,
183 | 'middleware' => $route->middleware(),
184 | 'before' => method_exists($route, 'beforeFilters') ? implode(', ', array_keys($route->beforeFilters())) : '',
185 | 'after' => method_exists($route, 'afterFilters') ? implode(', ', array_keys($route->afterFilters())) : ''
186 | ];
187 | }, $this->app['router']->getRoutes()->getRoutes())));
188 | }
189 |
190 | // Get the session data (normalized with removed passwords)
191 | protected function getSessionData()
192 | {
193 | if (! isset($this->app['session'])) return [];
194 |
195 | return $this->removePasswords((new Serializer)->normalizeEach($this->app['session']->all()));
196 | }
197 |
198 | // Add authenticated user data to the request
199 | protected function resolveAuthenticatedUser(Request $request)
200 | {
201 | if (! isset($this->app['auth'])) return;
202 | if (! ($user = $this->app['auth']->user())) return;
203 |
204 | if ($user instanceof \Illuminate\Database\Eloquent\Model) {
205 | // retrieve attributes in this awkward way to make sure we don't trigger exceptions with Eloquent strict mode on
206 | $keyName = method_exists($user, 'getAuthIdentifierName') ? $user->getAuthIdentifierName() : $user->getKeyName();
207 | $user = $user->getAttributes();
208 |
209 | $userId = isset($user[$keyName]) ? $user[$keyName] : null;
210 | $userEmail = isset($user['email']) ? $user['email'] : $userId;
211 | $userName = isset($user['name']) ? $user['name'] : null;
212 | } else {
213 | $userId = $user->getAuthIdentifier();
214 | $userEmail = isset($user->email) ? $user->email : $userId;
215 | $userName = isset($user->name) ? $user->name : null;
216 | }
217 |
218 | $request->setAuthenticatedUser($userEmail, $userId, [
219 | 'email' => $userEmail,
220 | 'name' => $userName
221 | ]);
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/Clockwork/Clockwork.php:
--------------------------------------------------------------------------------
1 | request = new Request;
43 | $this->authenticator = new NullAuthenticator;
44 |
45 | $this->shouldCollect = new ShouldCollect;
46 | $this->shouldRecord = new ShouldRecord;
47 | }
48 |
49 | // Add a new data source
50 | public function addDataSource(DataSourceInterface $dataSource)
51 | {
52 | $this->dataSources[] = $dataSource;
53 | return $this;
54 | }
55 |
56 | // Resolve the current request, sending it through all data sources, finalizing log and timeline
57 | public function resolveRequest()
58 | {
59 | foreach ($this->dataSources as $dataSource) {
60 | $dataSource->resolve($this->request);
61 | }
62 |
63 | $this->request->log()->sort();
64 | $this->request->timeline()->finalize($this->request->time);
65 |
66 | return $this;
67 | }
68 |
69 | // Resolve the current request as a "command" type request with command-specific data
70 | public function resolveAsCommand($name, $exitCode = null, $arguments = [], $options = [], $argumentsDefaults = [], $optionsDefaults = [], $output = null)
71 | {
72 | $this->resolveRequest();
73 |
74 | $this->request->type = RequestType::COMMAND;
75 | $this->request->commandName = $name;
76 | $this->request->commandArguments = $arguments;
77 | $this->request->commandArgumentsDefaults = $argumentsDefaults;
78 | $this->request->commandOptions = $options;
79 | $this->request->commandOptionsDefaults = $optionsDefaults;
80 | $this->request->commandExitCode = $exitCode;
81 | $this->request->commandOutput = $output;
82 |
83 | return $this;
84 | }
85 |
86 | // Resolve the current request as a "queue-job" type request with queue-job-specific data
87 | public function resolveAsQueueJob($name, $description = null, $status = 'processed', $payload = [], $queue = null, $connection = null, $options = [])
88 | {
89 | $this->resolveRequest();
90 |
91 | $this->request->type = RequestType::QUEUE_JOB;
92 | $this->request->jobName = $name;
93 | $this->request->jobDescription = $description;
94 | $this->request->jobStatus = $status;
95 | $this->request->jobPayload = (new Serializer)->normalize($payload);
96 | $this->request->jobQueue = $queue;
97 | $this->request->jobConnection = $connection;
98 | $this->request->jobOptions = (new Serializer)->normalizeEach($options);
99 |
100 | return $this;
101 | }
102 |
103 | // Resolve the current request as a "test" type request with test-specific data, accepts test name, status, status
104 | // message in case of failure and array of ran asserts
105 | public function resolveAsTest($name, $status = 'passed', $statusMessage = null, $asserts = [])
106 | {
107 | $this->resolveRequest();
108 |
109 | $this->request->type = RequestType::TEST;
110 | $this->request->testName = $name;
111 | $this->request->testStatus = $status;
112 | $this->request->testStatusMessage = $statusMessage;
113 |
114 | foreach ($asserts as $assert) {
115 | $this->request->addTestAssert($assert['name'], $assert['arguments'], $assert['passed'], $assert['trace']);
116 | }
117 |
118 | return $this;
119 | }
120 |
121 | // Extends the request with an additional data form all data sources, which is not required for normal use
122 | public function extendRequest(Request $request = null)
123 | {
124 | foreach ($this->dataSources as $dataSource) {
125 | $dataSource->extend($request ?: $this->request);
126 | }
127 |
128 | return $this;
129 | }
130 |
131 | // Store the current request via configured storage implementation
132 | public function storeRequest()
133 | {
134 | return $this->storage->store($this->request);
135 | }
136 |
137 | // Reset all data sources to an empty state, clearing any collected data
138 | public function reset()
139 | {
140 | foreach ($this->dataSources as $dataSource) {
141 | $dataSource->reset();
142 | }
143 |
144 | return $this;
145 | }
146 |
147 | // Get or set the current request instance
148 | public function request(Request $request = null)
149 | {
150 | if (! $request) return $this->request;
151 |
152 | $this->request = $request;
153 | return $this;
154 | }
155 |
156 | // Get the log instance for the current request or log a new message
157 | public function log($level = null, $message = null, array $context = [])
158 | {
159 | if ($level) {
160 | return $this->request->log()->log($level, $message, $context);
161 | }
162 |
163 | return $this->request->log();
164 | }
165 |
166 | // Get the timeline instance for the current request
167 | public function timeline()
168 | {
169 | return $this->request->timeline();
170 | }
171 |
172 | // Shortcut to create a new event on the current timeline instance
173 | public function event($description, $data = [])
174 | {
175 | return $this->request->timeline()->event($description, $data);
176 | }
177 |
178 | // Configure which requests should be collected, can be called with arrey of options, a custom closure or with no
179 | // arguments for a fluent configuration api
180 | public function shouldCollect($shouldCollect = null)
181 | {
182 | if ($shouldCollect instanceof Closure) return $this->shouldCollect->callback($shouldCollect);
183 |
184 | if (is_array($shouldCollect)) return $this->shouldCollect->merge($shouldCollect);
185 |
186 | return $this->shouldCollect;
187 | }
188 |
189 | // Configure which requests should be recorded, can be called with arrey of options, a custom closure or with no
190 | // arguments for a fluent configuration api
191 | public function shouldRecord($shouldRecord = null)
192 | {
193 | if ($shouldRecord instanceof Closure) return $this->shouldRecord->callback($shouldRecord);
194 |
195 | if (is_array($shouldRecord)) return $this->shouldRecord->merge($shouldRecord);
196 |
197 | return $this->shouldRecord;
198 | }
199 |
200 | // Get or set all data sources at once
201 | public function dataSources($dataSources = null)
202 | {
203 | if (! $dataSources) return $this->dataSources;
204 |
205 | $this->dataSources = $dataSources;
206 | return $this;
207 | }
208 |
209 | // Get or set a storage implementation
210 | public function storage(StorageInterface $storage = null)
211 | {
212 | if (! $storage) return $this->storage;
213 |
214 | $this->storage = $storage;
215 | return $this;
216 | }
217 |
218 | // Get or set an authenticator implementation
219 | public function authenticator(AuthenticatorInterface $authenticator = null)
220 | {
221 | if (! $authenticator) return $this->authenticator;
222 |
223 | $this->authenticator = $authenticator;
224 | return $this;
225 | }
226 |
227 | // Forward any other method calls to the current request and log instances
228 | public function __call($method, $args)
229 | {
230 | if (in_array($method, [ 'emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug' ])) {
231 | return $this->request->log()->$method(...$args);
232 | }
233 |
234 | return $this->request->$method(...$args);
235 | }
236 |
237 | // DEPRECATED The following apis are deprecated and will be removed in a future version
238 |
239 | // Get all added data sources
240 | public function getDataSources()
241 | {
242 | return $this->dataSources;
243 | }
244 |
245 | // Get the current request instance
246 | public function getRequest()
247 | {
248 | return $this->request;
249 | }
250 |
251 | // Set the current request instance
252 | public function setRequest(Request $request)
253 | {
254 | $this->request = $request;
255 | return $this;
256 | }
257 |
258 | // Get a storage implementation
259 | public function getStorage()
260 | {
261 | return $this->storage;
262 | }
263 |
264 | // Set a storage implementation
265 | public function setStorage(StorageInterface $storage)
266 | {
267 | $this->storage = $storage;
268 | return $this;
269 | }
270 |
271 | // Get an authenticator implementation
272 | public function getAuthenticator()
273 | {
274 | return $this->authenticator;
275 | }
276 |
277 | // Set an authenticator implementation
278 | public function setAuthenticator(AuthenticatorInterface $authenticator)
279 | {
280 | $this->authenticator = $authenticator;
281 | return $this;
282 | }
283 | }
284 |
--------------------------------------------------------------------------------