├── .editorconfig ├── .gitattributes ├── CHANGELOG.md ├── Clockwork ├── Authentication │ ├── AuthenticatorInterface.php │ ├── NullAuthenticator.php │ └── SimpleAuthenticator.php ├── Clockwork.php ├── DataSource │ ├── Concerns │ │ └── EloquentDetectDuplicateQueries.php │ ├── DBALDataSource.php │ ├── DataSource.php │ ├── DataSourceInterface.php │ ├── DoctrineDataSource.php │ ├── EloquentDataSource.php │ ├── GuzzleDataSource.php │ ├── LaravelCacheDataSource.php │ ├── LaravelDataSource.php │ ├── LaravelEventsDataSource.php │ ├── LaravelHttpClientDataSource.php │ ├── LaravelNotificationsDataSource.php │ ├── LaravelQueueDataSource.php │ ├── LaravelRedisDataSource.php │ ├── LaravelViewsDataSource.php │ ├── LumenDataSource.php │ ├── MonologDataSource.php │ ├── PhpDataSource.php │ ├── PsrMessageDataSource.php │ ├── SlimDataSource.php │ ├── SwiftDataSource.php │ ├── TwigDataSource.php │ └── XdebugDataSource.php ├── Helpers │ ├── Concerns │ │ └── ResolvesViewName.php │ ├── Serializer.php │ ├── ServerTiming.php │ ├── StackFilter.php │ ├── StackFrame.php │ └── StackTrace.php ├── Request │ ├── IncomingRequest.php │ ├── Log.php │ ├── LogLevel.php │ ├── Request.php │ ├── RequestType.php │ ├── ShouldCollect.php │ ├── ShouldRecord.php │ ├── Timeline │ │ ├── Event.php │ │ └── Timeline.php │ ├── UserData.php │ └── UserDataItem.php ├── Storage │ ├── FileStorage.php │ ├── RedisStorage.php │ ├── Search.php │ ├── SqlSearch.php │ ├── SqlStorage.php │ ├── Storage.php │ ├── StorageInterface.php │ └── SymfonyStorage.php ├── Support │ ├── Doctrine │ │ ├── Connection.php │ │ ├── Driver.php │ │ ├── Legacy │ │ │ └── Logger.php │ │ └── Middleware.php │ ├── Laravel │ │ ├── ClockworkCleanCommand.php │ │ ├── ClockworkController.php │ │ ├── ClockworkMiddleware.php │ │ ├── ClockworkServiceProvider.php │ │ ├── ClockworkSupport.php │ │ ├── Console │ │ │ ├── CapturingFormatter.php │ │ │ ├── CapturingLegacyFormatter.php │ │ │ └── CapturingOldFormatter.php │ │ ├── Eloquent │ │ │ ├── ResolveModelLegacyScope.php │ │ │ └── ResolveModelScope.php │ │ ├── Facade.php │ │ ├── Tests │ │ │ ├── ClockworkExtension.php │ │ │ └── UsesClockwork.php │ │ ├── config │ │ │ └── clockwork.php │ │ └── helpers.php │ ├── Lumen │ │ ├── ClockworkMiddleware.php │ │ ├── ClockworkServiceProvider.php │ │ ├── ClockworkSupport.php │ │ └── Controller.php │ ├── Monolog │ │ ├── Handler │ │ │ └── ClockworkHandler.php │ │ ├── Monolog │ │ │ └── ClockworkHandler.php │ │ ├── Monolog2 │ │ │ └── ClockworkHandler.php │ │ └── Monolog3 │ │ │ └── ClockworkHandler.php │ ├── Slim │ │ ├── ClockworkMiddleware.php │ │ ├── Legacy │ │ │ └── ClockworkMiddleware.php │ │ └── Old │ │ │ ├── ClockworkLogWriter.php │ │ │ └── ClockworkMiddleware.php │ ├── Swift │ │ └── SwiftPluginClockworkTimeline.php │ ├── Symfony │ │ ├── ClockworkBundle.php │ │ ├── ClockworkConfiguration.php │ │ ├── ClockworkController.php │ │ ├── ClockworkExtension.php │ │ ├── ClockworkFactory.php │ │ ├── ClockworkListener.php │ │ ├── ClockworkLoader.php │ │ ├── ClockworkSupport.php │ │ ├── ProfileTransformer.php │ │ └── Resources │ │ │ └── config │ │ │ ├── clockwork.php │ │ │ └── routing │ │ │ └── clockwork.php │ ├── Twig │ │ └── ProfilerClockworkDumper.php │ └── Vanilla │ │ ├── Clockwork.php │ │ ├── ClockworkMiddleware.php │ │ ├── config.php │ │ ├── helpers.php │ │ └── iframe.html.php └── Web │ ├── Web.php │ └── public │ ├── assets │ ├── index-159S-Gz-.js │ └── index-CqvQEu-6.css │ ├── img │ ├── appearance-auto-icon.png │ ├── appearance-dark-icon.png │ ├── appearance-light-icon.png │ ├── icon-128x128.png │ └── whats-new │ │ ├── 5.0 │ │ ├── client-metrics.png │ │ ├── clockwork-5.png │ │ ├── models-tab.png │ │ ├── notifications-tab.png │ │ ├── timeline.png │ │ └── toolbar.png │ │ ├── 5.1 │ │ └── database-queries.png │ │ └── 5.3 │ │ └── http-requests.png │ ├── index.html │ └── manifest.json ├── LICENSE ├── README.md └── composer.json /.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 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | .github/ export-ignore -------------------------------------------------------------------------------- /Clockwork/Authentication/AuthenticatorInterface.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/Clockwork.php: -------------------------------------------------------------------------------- 1 | request = new Request; 38 | $this->authenticator = new NullAuthenticator; 39 | 40 | $this->shouldCollect = new ShouldCollect; 41 | $this->shouldRecord = new ShouldRecord; 42 | } 43 | 44 | // Add a new data source 45 | public function addDataSource(DataSourceInterface $dataSource) 46 | { 47 | $this->dataSources[] = $dataSource; 48 | return $this; 49 | } 50 | 51 | // Resolve the current request, sending it through all data sources, finalizing log and timeline 52 | public function resolveRequest() 53 | { 54 | foreach ($this->dataSources as $dataSource) { 55 | $dataSource->resolve($this->request); 56 | } 57 | 58 | $this->request->log()->sort(); 59 | $this->request->timeline()->finalize($this->request->time); 60 | 61 | return $this; 62 | } 63 | 64 | // Resolve the current request as a "command" type request with command-specific data 65 | public function resolveAsCommand($name, $exitCode = null, $arguments = [], $options = [], $argumentsDefaults = [], $optionsDefaults = [], $output = null) 66 | { 67 | $this->resolveRequest(); 68 | 69 | $this->request->type = RequestType::COMMAND; 70 | $this->request->commandName = $name; 71 | $this->request->commandArguments = $arguments; 72 | $this->request->commandArgumentsDefaults = $argumentsDefaults; 73 | $this->request->commandOptions = $options; 74 | $this->request->commandOptionsDefaults = $optionsDefaults; 75 | $this->request->commandExitCode = $exitCode; 76 | $this->request->commandOutput = $output; 77 | 78 | return $this; 79 | } 80 | 81 | // Resolve the current request as a "queue-job" type request with queue-job-specific data 82 | public function resolveAsQueueJob($name, $description = null, $status = 'processed', $payload = [], $queue = null, $connection = null, $options = []) 83 | { 84 | $this->resolveRequest(); 85 | 86 | $this->request->type = RequestType::QUEUE_JOB; 87 | $this->request->jobName = $name; 88 | $this->request->jobDescription = $description; 89 | $this->request->jobStatus = $status; 90 | $this->request->jobPayload = (new Serializer)->normalize($payload); 91 | $this->request->jobQueue = $queue; 92 | $this->request->jobConnection = $connection; 93 | $this->request->jobOptions = (new Serializer)->normalizeEach($options); 94 | 95 | return $this; 96 | } 97 | 98 | // Resolve the current request as a "test" type request with test-specific data, accepts test name, status, status 99 | // message in case of failure and array of ran asserts 100 | public function resolveAsTest($name, $status = 'passed', $statusMessage = null, $asserts = []) 101 | { 102 | $this->resolveRequest(); 103 | 104 | $this->request->type = RequestType::TEST; 105 | $this->request->testName = $name; 106 | $this->request->testStatus = $status; 107 | $this->request->testStatusMessage = $statusMessage; 108 | 109 | foreach ($asserts as $assert) { 110 | $this->request->addTestAssert($assert['name'], $assert['arguments'], $assert['passed'], $assert['trace']); 111 | } 112 | 113 | return $this; 114 | } 115 | 116 | // Extends the request with an additional data form all data sources, which is not required for normal use 117 | public function extendRequest(?Request $request = null) 118 | { 119 | foreach ($this->dataSources as $dataSource) { 120 | $dataSource->extend($request ?: $this->request); 121 | } 122 | 123 | return $this; 124 | } 125 | 126 | // Store the current request via configured storage implementation 127 | public function storeRequest() 128 | { 129 | return $this->storage->store($this->request); 130 | } 131 | 132 | // Reset all data sources to an empty state, clearing any collected data 133 | public function reset() 134 | { 135 | foreach ($this->dataSources as $dataSource) { 136 | $dataSource->reset(); 137 | } 138 | 139 | return $this; 140 | } 141 | 142 | // Get or set the current request instance 143 | public function request(?Request $request = null) 144 | { 145 | if (! $request) return $this->request; 146 | 147 | $this->request = $request; 148 | return $this; 149 | } 150 | 151 | // Get the log instance for the current request or log a new message 152 | public function log($level = null, $message = null, array $context = []) 153 | { 154 | if ($level) { 155 | return $this->request->log()->log($level, $message, $context); 156 | } 157 | 158 | return $this->request->log(); 159 | } 160 | 161 | // Get the timeline instance for the current request 162 | public function timeline() 163 | { 164 | return $this->request->timeline(); 165 | } 166 | 167 | // Shortcut to create a new event on the current timeline instance 168 | public function event($description, $data = []) 169 | { 170 | return $this->request->timeline()->event($description, $data); 171 | } 172 | 173 | // Configure which requests should be collected, can be called with arrey of options, a custom closure or with no 174 | // arguments for a fluent configuration api 175 | public function shouldCollect($shouldCollect = null) 176 | { 177 | if ($shouldCollect instanceof Closure) return $this->shouldCollect->callback($shouldCollect); 178 | 179 | if (is_array($shouldCollect)) return $this->shouldCollect->merge($shouldCollect); 180 | 181 | return $this->shouldCollect; 182 | } 183 | 184 | // Configure which requests should be recorded, can be called with arrey of options, a custom closure or with no 185 | // arguments for a fluent configuration api 186 | public function shouldRecord($shouldRecord = null) 187 | { 188 | if ($shouldRecord instanceof Closure) return $this->shouldRecord->callback($shouldRecord); 189 | 190 | if (is_array($shouldRecord)) return $this->shouldRecord->merge($shouldRecord); 191 | 192 | return $this->shouldRecord; 193 | } 194 | 195 | // Get or set all data sources at once 196 | public function dataSources($dataSources = null) 197 | { 198 | if (! $dataSources) return $this->dataSources; 199 | 200 | $this->dataSources = $dataSources; 201 | return $this; 202 | } 203 | 204 | // Get or set a storage implementation 205 | public function storage(?StorageInterface $storage = null) 206 | { 207 | if (! $storage) return $this->storage; 208 | 209 | $this->storage = $storage; 210 | return $this; 211 | } 212 | 213 | // Get or set an authenticator implementation 214 | public function authenticator(?AuthenticatorInterface $authenticator = null) 215 | { 216 | if (! $authenticator) return $this->authenticator; 217 | 218 | $this->authenticator = $authenticator; 219 | return $this; 220 | } 221 | 222 | // Forward any other method calls to the current request and log instances 223 | public function __call($method, $args) 224 | { 225 | if (in_array($method, [ 'emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug' ])) { 226 | return $this->request->log()->$method(...$args); 227 | } 228 | 229 | return $this->request->$method(...$args); 230 | } 231 | 232 | // DEPRECATED The following apis are deprecated and will be removed in a future version 233 | 234 | // Get all added data sources 235 | public function getDataSources() 236 | { 237 | return $this->dataSources; 238 | } 239 | 240 | // Get the current request instance 241 | public function getRequest() 242 | { 243 | return $this->request; 244 | } 245 | 246 | // Set the current request instance 247 | public function setRequest(Request $request) 248 | { 249 | $this->request = $request; 250 | return $this; 251 | } 252 | 253 | // Get a storage implementation 254 | public function getStorage() 255 | { 256 | return $this->storage; 257 | } 258 | 259 | // Set a storage implementation 260 | public function setStorage(StorageInterface $storage) 261 | { 262 | $this->storage = $storage; 263 | return $this; 264 | } 265 | 266 | // Get an authenticator implementation 267 | public function getAuthenticator() 268 | { 269 | return $this->authenticator; 270 | } 271 | 272 | // Set an authenticator implementation 273 | public function setAuthenticator(AuthenticatorInterface $authenticator) 274 | { 275 | $this->authenticator = $authenticator; 276 | return $this; 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /Clockwork/DataSource/Concerns/EloquentDetectDuplicateQueries.php: -------------------------------------------------------------------------------- 1 | duplicateQueries as $query) { 17 | if ($query['count'] <= 1) continue; 18 | 19 | $log->warning( 20 | "N+1 queries: {$query['model']}::{$query['relation']} loaded {$query['count']} times.", 21 | [ 'performance' => true, 'trace' => $query['trace'] ] 22 | ); 23 | } 24 | 25 | $request->log()->merge($log); 26 | } 27 | 28 | protected function detectDuplicateQuery(StackTrace $trace) 29 | { 30 | $relationFrame = $trace->first(function ($frame) { 31 | return $frame->function == 'getRelationValue' 32 | || $frame->class == \Illuminate\Database\Eloquent\Relations\Relation::class; 33 | }); 34 | 35 | if (! $relationFrame || ! $relationFrame->object) return; 36 | 37 | if ($relationFrame->class == \Illuminate\Database\Eloquent\Relations\Relation::class) { 38 | $model = get_class($relationFrame->object->getParent()); 39 | $relation = get_class($relationFrame->object->getRelated()); 40 | } else { 41 | $model = get_class($relationFrame->object); 42 | $relation = $relationFrame->args[0]; 43 | } 44 | 45 | $shortTrace = $trace->skip(StackFilter::make() 46 | ->isNotVendor([ 'itsgoingd', 'laravel', 'illuminate' ]) 47 | ->isNotNamespace([ 'Clockwork', 'Illuminate' ])); 48 | 49 | $hash = implode('-', [ $model, $relation, $shortTrace->first()->file, $shortTrace->first()->line ]); 50 | 51 | if (! isset($this->duplicateQueries[$hash])) { 52 | $this->duplicateQueries[$hash] = [ 53 | 'count' => 0, 54 | 'model' => $model, 55 | 'relation' => $relation, 56 | 'trace' => $trace 57 | ]; 58 | } 59 | 60 | $this->duplicateQueries[$hash]['count']++; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Clockwork/DataSource/DBALDataSource.php: -------------------------------------------------------------------------------- 1 | connectionName = $connection ? $connection->getDatabase() : null; 24 | 25 | if (! class_exists(\Doctrine\DBAL\Logging\Middleware::class)) { 26 | $this->setupLegacyDoctrine($connection); 27 | } 28 | } 29 | 30 | // Update Doctrine configuration to include the Clockwork logging middleware (for Doctrine 3+) 31 | public function configure(?Configuration $configuration = null) 32 | { 33 | $configuration = $configuration ?? new Configuration; 34 | 35 | return $configuration->setMiddlewares(array_merge( 36 | $configuration->getMiddlewares(), [ $this->middleware() ] 37 | )); 38 | } 39 | 40 | // Returns an instance of Clockwork logging middleware associated with this data source (for Doctrine 3+) 41 | public function middleware() 42 | { 43 | return $this->logger = new Middleware(function ($query) { 44 | $this->registerQuery($query); 45 | }); 46 | } 47 | 48 | // Setup Clockwork logger for legacy Doctrine 2.x 49 | protected function setupLegacyDoctrine(Connection $connection) 50 | { 51 | $this->logger = new Logger($connection, function ($query) { 52 | $this->registerQuery($query); 53 | }); 54 | } 55 | 56 | // Adds executed database queries to the request 57 | public function resolve(Request $request) 58 | { 59 | $request->databaseQueries = array_merge($request->databaseQueries, $this->queries); 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->queries = []; 68 | $this->query = null; 69 | } 70 | 71 | // Collect an executed database query 72 | protected function registerQuery($query) 73 | { 74 | $query['duration'] = (microtime(true) - $query['time']) * 1000; 75 | $query['connection'] = $query['connection'] ?? $this->connectionName; 76 | 77 | if ($this->passesFilters([ $query ])) { 78 | $this->queries[] = $query; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Clockwork/DataSource/DataSource.php: -------------------------------------------------------------------------------- 1 | filters[$type] = array_merge($this->filters[$type] ?? [], [ $filter ]); 32 | 33 | return $this; 34 | } 35 | 36 | // Clear all registered filters 37 | public function clearFilters() 38 | { 39 | $this->filters = []; 40 | 41 | return $this; 42 | } 43 | 44 | // Returns boolean whether the filterable passes all registered filters 45 | protected function passesFilters($args, $type = 'default') 46 | { 47 | $filters = $this->filters[$type] ?? []; 48 | 49 | foreach ($filters as $filter) { 50 | if (! $filter(...$args)) return false; 51 | } 52 | 53 | return true; 54 | } 55 | 56 | // Censors passwords in an array, identified by key containing "pass" substring 57 | public function removePasswords(array $data) 58 | { 59 | $keys = array_keys($data); 60 | $values = array_map(function ($value, $key) { 61 | return strpos($key, 'pass') !== false ? '*removed*' : $value; 62 | }, $data, $keys); 63 | 64 | return array_combine($keys, $values); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Clockwork/DataSource/DataSourceInterface.php: -------------------------------------------------------------------------------- 1 | getConnection() : null); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Clockwork/DataSource/GuzzleDataSource.php: -------------------------------------------------------------------------------- 1 | collectContent = $collectContent; 25 | $this->collectRawContent = $collectRawContent; 26 | } 27 | 28 | // Returns a new Guzzle instance, pre-configured with Clockwork support 29 | public function instance(array $config = []) 30 | { 31 | return new Client($this->configure($config)); 32 | } 33 | 34 | // Updates Guzzle configuration array with Clockwork support 35 | public function configure(array $config = []) 36 | { 37 | $handler = $config['handler'] ?? HandlerStack::create(); 38 | 39 | $handler->push($this); 40 | 41 | $config['handler'] = $handler; 42 | 43 | return $config; 44 | } 45 | 46 | // Add sent notifications to the request 47 | public function resolve(Request $request) 48 | { 49 | $request->httpRequests = array_merge($request->httpRequests, $this->requests); 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->requests = []; 58 | } 59 | 60 | // Guzzle middleware implemenation, that does the requests logging itself 61 | public function __invoke(callable $handler): callable 62 | { 63 | return function(RequestInterface $request, array $options) use ($handler): PromiseInterface { 64 | $time = microtime(true); 65 | $stats = null; 66 | 67 | $originalOnStats = $options['on_stats'] ?? null; 68 | $options['on_stats'] = function (TransferStats $transferStats) use (&$stats, $originalOnStats) { 69 | $stats = $transferStats->getHandlerStats(); 70 | if ($originalOnStats) $originalOnStats($transferStats); 71 | }; 72 | 73 | return $handler($request, $options) 74 | ->then(function(ResponseInterface $response) use ($request, $time, $stats) { 75 | $this->collectRequest($request, $response, $time, $stats); 76 | 77 | return $response; 78 | }, function(GuzzleException $exception) use ($request, $time, $stats) { 79 | $response = $exception instanceof RequestException ? $exception->getResponse() : null; 80 | $this->collectRequest($request, $response, $time, $stats, $exception->getMessage()); 81 | 82 | throw $exception; 83 | }); 84 | }; 85 | } 86 | 87 | // Collect a request-response pair 88 | protected function collectRequest($request, $response = null, $startTime = null, $stats = null, $error = null) 89 | { 90 | $trace = StackTrace::get(); 91 | 92 | $request = (object) [ 93 | 'request' => (object) [ 94 | 'method' => $request->getMethod(), 95 | 'url' => $this->removeAuthFromUrl((string) $request->getUri()), 96 | 'headers' => $request->getHeaders(), 97 | 'content' => $this->collectContent ? $this->resolveRequestContent($request) : null, 98 | 'body' => $this->collectRawContent ? (string) $request->getBody() : null 99 | ], 100 | 'response' => $response ? (object) [ 101 | 'status' => (int) $response->getStatusCode(), 102 | 'headers' => $response->getHeaders(), 103 | 'content' => $this->collectContent ? json_decode((string) $response->getBody(), true) : null, 104 | 'body' => $this->collectRawContent ? (string) $response->getBody() : null 105 | ] : null, 106 | 'stats' => $stats ? (object) [ 107 | 'timing' => isset($stats['total_time_us']) ? (object) [ 108 | 'lookup' => $stats['namelookup_time_us'] / 1000, 109 | 'connect' => ($stats['pretransfer_time_us'] - $stats['namelookup_time_us']) / 1000, 110 | 'waiting' => ($stats['starttransfer_time_us'] - $stats['pretransfer_time_us']) / 1000, 111 | 'transfer' => ($stats['total_time_us'] - $stats['starttransfer_time_us']) / 1000 112 | ] : null, 113 | 'size' => (object) [ 114 | 'upload' => $stats['size_upload'] ?? null, 115 | 'download' => $stats['size_download'] ?? null 116 | ], 117 | 'speed' => (object) [ 118 | 'upload' => $stats['speed_upload'] ?? null, 119 | 'download' => $stats['speed_download'] ?? null 120 | ], 121 | 'hosts' => (object) [ 122 | 'local' => isset($stats['local_ip']) ? [ 'ip' => $stats['local_ip'], 'port' => $stats['local_port'] ] : null, 123 | 'remote' => isset($stats['primary_ip']) ? [ 'ip' => $stats['primary_ip'], 'port' => $stats['primary_port'] ] : null 124 | ], 125 | 'version' => $stats['http_version'] ?? null 126 | ] : null, 127 | 'error' => $error, 128 | 'time' => $startTime, 129 | 'duration' => (microtime(true) - $startTime) * 1000, 130 | 'trace' => (new Serializer)->trace($trace) 131 | ]; 132 | 133 | if ($response->getBody()->tell()) $response->getBody()->rewind(); 134 | 135 | if ($this->passesFilters([ $request ])) { 136 | $this->requests[] = $request; 137 | } 138 | } 139 | 140 | // Resolve request content, with support for form data and json requests 141 | protected function resolveRequestContent($request) 142 | { 143 | $body = (string) $request->getBody(); 144 | $headers = $request->getHeaders(); 145 | 146 | if (isset($headers['Content-Type']) && $headers['Content-Type'][0] == 'application/x-www-form-urlencoded') { 147 | parse_str($body, $parameters); 148 | return $parameters; 149 | } elseif (isset($headers['Content-Type']) && strpos($headers['Content-Type'][0], 'json') !== false) { 150 | return json_decode($body, true); 151 | } 152 | 153 | return []; 154 | } 155 | 156 | // Removes username and password from the URL 157 | protected function removeAuthFromUrl($url) 158 | { 159 | return preg_replace('#^(.+?://)(.+?@)(.*)$#', '$1$3', $url); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /Clockwork/DataSource/LaravelCacheDataSource.php: -------------------------------------------------------------------------------- 1 | 0, 'hit' => 0, 'write' => 0, 'delete' => 0 20 | ]; 21 | 22 | // Whether we are collecting cache queries or stats only 23 | protected $collectQueries = true; 24 | 25 | // Whether we are collecting values from cache queries 26 | protected $collectValues = true; 27 | 28 | // Create a new data source instance, takes an event dispatcher and additional options as argument 29 | public function __construct(EventDispatcher $eventDispatcher, $collectQueries = true, $collectValues = true) 30 | { 31 | $this->eventDispatcher = $eventDispatcher; 32 | 33 | $this->collectQueries = $collectQueries; 34 | $this->collectValues = $collectValues; 35 | } 36 | 37 | // Adds cache queries and stats to the request 38 | public function resolve(Request $request) 39 | { 40 | $request->cacheQueries = array_merge($request->cacheQueries, $this->queries); 41 | 42 | $request->cacheReads += $this->count['read']; 43 | $request->cacheHits += $this->count['hit']; 44 | $request->cacheWrites += $this->count['write']; 45 | $request->cacheDeletes += $this->count['delete']; 46 | 47 | return $request; 48 | } 49 | 50 | // Reset the data source to an empty state, clearing any collected data 51 | public function reset() 52 | { 53 | $this->queries = []; 54 | 55 | $this->count = [ 56 | 'read' => 0, 'hit' => 0, 'write' => 0, 'delete' => 0 57 | ]; 58 | } 59 | 60 | // Start listening to cache events 61 | public function listenToEvents() 62 | { 63 | if (class_exists(\Illuminate\Cache\Events\CacheHit::class)) { 64 | $this->eventDispatcher->listen(\Illuminate\Cache\Events\CacheHit::class, function ($event) { 65 | $this->registerQuery([ 'type' => 'hit', 'key' => $event->key, 'value' => $event->value ]); 66 | }); 67 | $this->eventDispatcher->listen(\Illuminate\Cache\Events\CacheMissed::class, function ($event) { 68 | $this->registerQuery([ 'type' => 'miss', 'key' => $event->key ]); 69 | }); 70 | $this->eventDispatcher->listen(\Illuminate\Cache\Events\KeyWritten::class, function ($event) { 71 | $this->registerQuery([ 72 | 'type' => 'write', 'key' => $event->key, 'value' => $event->value, 73 | 'expiration' => property_exists($event, 'seconds') ? $event->seconds : $event->minutes * 60 74 | ]); 75 | }); 76 | $this->eventDispatcher->listen(\Illuminate\Cache\Events\KeyForgotten::class, function ($event) { 77 | $this->registerQuery([ 'type' => 'delete', 'key' => $event->key ]); 78 | }); 79 | } else { 80 | // legacy Laravel 5.1 style events 81 | $this->eventDispatcher->listen('cache.hit', function ($key, $value) { 82 | $this->registerQuery([ 'type' => 'hit', 'key' => $key, 'value' => $value ]); 83 | }); 84 | $this->eventDispatcher->listen('cache.missed', function ($key) { 85 | $this->registerQuery([ 'type' => 'miss', 'key' => $key ]); 86 | }); 87 | $this->eventDispatcher->listen('cache.write', function ($key, $value, $minutes) { 88 | $this->registerQuery([ 89 | 'type' => 'write', 'key' => $key, 'value' => $value, 'expiration' => $minutes * 60 90 | ]); 91 | }); 92 | $this->eventDispatcher->listen('cache.delete', function ($key) { 93 | $this->registerQuery([ 'type' => 'delete', 'key' => $key ]); 94 | }); 95 | } 96 | } 97 | 98 | // Collect an executed query 99 | protected function registerQuery(array $query) 100 | { 101 | $trace = StackTrace::get()->resolveViewName(); 102 | 103 | $record = [ 104 | 'type' => $query['type'], 105 | 'key' => $query['key'], 106 | 'expiration' => $query['expiration'] ?? null, 107 | 'time' => microtime(true), 108 | 'connection' => null, 109 | 'trace' => (new Serializer)->trace($trace) 110 | ]; 111 | 112 | if ($this->collectValues && isset($query['value'])) { 113 | $record['value'] = (new Serializer)->normalize($query['value']); 114 | } 115 | 116 | $this->incrementQueryCount($record); 117 | 118 | if ($this->collectQueries && $this->passesFilters([ $record ])) { 119 | $this->queries[] = $record; 120 | } 121 | } 122 | 123 | // Increment query counts for collected query 124 | protected function incrementQueryCount($query) 125 | { 126 | if ($query['type'] == 'write') { 127 | $this->count['write']++; 128 | } elseif ($query['type'] == 'delete') { 129 | $this->count['delete']++; 130 | } else { 131 | $this->count['read']++; 132 | 133 | if ($query['type'] == 'hit') { 134 | $this->count['hit']++; 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Clockwork/DataSource/LaravelDataSource.php: -------------------------------------------------------------------------------- 1 | app = $app; 35 | 36 | $this->collectLog = $collectLog; 37 | $this->collectRoutes = $collectRoutes; 38 | $this->routesOnlyNamespaces = $routesOnlyNamespaces; 39 | 40 | $this->log = new Log; 41 | } 42 | 43 | // Adds request, response information, middleware, routes, session data, user and log entries to the request 44 | public function resolve(Request $request) 45 | { 46 | $request->method = $this->getRequestMethod(); 47 | $request->url = $this->getRequestUrl(); 48 | $request->uri = $this->getRequestUri(); 49 | $request->controller = $this->getController(); 50 | $request->headers = $this->getRequestHeaders(); 51 | $request->responseStatus = $this->getResponseStatus(); 52 | $request->middleware = $this->getMiddleware(); 53 | $request->routes = $this->getRoutes(); 54 | $request->sessionData = $this->getSessionData(); 55 | 56 | $this->resolveAuthenticatedUser($request); 57 | 58 | $request->log()->merge($this->log); 59 | 60 | return $request; 61 | } 62 | 63 | // Reset the data source to an empty state, clearing any collected data 64 | public function reset() 65 | { 66 | $this->log = new Log; 67 | } 68 | 69 | // Set Laravel application instance for the current request 70 | public function setApplication(Application $app) 71 | { 72 | $this->app = $app; 73 | return $this; 74 | } 75 | 76 | // Set Laravel response instance for the current request 77 | public function setResponse(Response $response) 78 | { 79 | $this->response = $response; 80 | return $this; 81 | } 82 | 83 | // Listen for the log events 84 | public function listenToEvents() 85 | { 86 | if (! $this->collectLog) return; 87 | 88 | if (class_exists(\Illuminate\Log\Events\MessageLogged::class)) { 89 | // Laravel 5.4 90 | $this->app['events']->listen(\Illuminate\Log\Events\MessageLogged::class, function ($event) { 91 | $this->log->log($event->level, $event->message, $event->context); 92 | }); 93 | } else { 94 | // Laravel 5.0 to 5.3 95 | $this->app['events']->listen('illuminate.log', function ($level, $message, $context) { 96 | $this->log->log($level, $message, $context); 97 | }); 98 | } 99 | } 100 | 101 | // Get a textual representation of the current route's controller 102 | protected function getController() 103 | { 104 | $router = $this->app['router']; 105 | 106 | $route = $router->current(); 107 | $controller = $route ? $route->getActionName() : null; 108 | 109 | if ($controller instanceof \Closure) { 110 | $controller = 'anonymous function'; 111 | } elseif (is_object($controller)) { 112 | $controller = 'instance of ' . get_class($controller); 113 | } elseif (is_array($controller) && count($controller) == 2) { 114 | if (is_object($controller[0])) { 115 | $controller = get_class($controller[0]) . '->' . $controller[1]; 116 | } else { 117 | $controller = $controller[0] . '::' . $controller[1]; 118 | } 119 | } elseif (! is_string($controller)) { 120 | $controller = null; 121 | } 122 | 123 | return $controller; 124 | } 125 | 126 | // Get the request headers 127 | protected function getRequestHeaders() 128 | { 129 | return $this->app['request']->headers->all(); 130 | } 131 | 132 | // Get the request method 133 | protected function getRequestMethod() 134 | { 135 | return $this->app['request']->getMethod(); 136 | } 137 | 138 | // Get the request URL 139 | protected function getRequestUrl() 140 | { 141 | return $this->app['request']->fullUrl(); 142 | } 143 | 144 | // Get the request URI 145 | protected function getRequestUri() 146 | { 147 | return $this->app['request']->getRequestUri(); 148 | } 149 | 150 | // Get the response status code 151 | protected function getResponseStatus() 152 | { 153 | return $this->response ? $this->response->getStatusCode() : null; 154 | } 155 | 156 | // Get an array of middleware for the matched route 157 | protected function getMiddleware() 158 | { 159 | $route = $this->app['router']->current(); 160 | 161 | if (! $route) return; 162 | 163 | return method_exists($route, 'gatherMiddleware') ? $route->gatherMiddleware() : $route->middleware(); 164 | } 165 | 166 | // Get an array of application routes 167 | protected function getRoutes() 168 | { 169 | if (! $this->collectRoutes) return []; 170 | 171 | return array_values(array_filter(array_map(function ($route) { 172 | $action = $route->getActionName() ?: 'anonymous function'; 173 | $namespace = strpos($action, '\\') !== false ? explode('\\', $action)[0] : null; 174 | 175 | if (count($this->routesOnlyNamespaces) && ! in_array($namespace, $this->routesOnlyNamespaces)) return; 176 | 177 | return [ 178 | 'method' => implode(', ', $route->methods()), 179 | 'uri' => $route->uri(), 180 | 'name' => $route->getName(), 181 | 'action' => $action, 182 | 'middleware' => $route->middleware(), 183 | 'before' => method_exists($route, 'beforeFilters') ? implode(', ', array_keys($route->beforeFilters())) : '', 184 | 'after' => method_exists($route, 'afterFilters') ? implode(', ', array_keys($route->afterFilters())) : '' 185 | ]; 186 | }, $this->app['router']->getRoutes()->getRoutes()))); 187 | } 188 | 189 | // Get the session data (normalized with removed passwords) 190 | protected function getSessionData() 191 | { 192 | if (! isset($this->app['session'])) return []; 193 | 194 | return $this->removePasswords((new Serializer)->normalizeEach($this->app['session']->all())); 195 | } 196 | 197 | // Add authenticated user data to the request 198 | protected function resolveAuthenticatedUser(Request $request) 199 | { 200 | if (! isset($this->app['auth'])) return; 201 | if (! ($user = $this->app['auth']->user())) return; 202 | 203 | if ($user instanceof \Illuminate\Database\Eloquent\Model) { 204 | // retrieve attributes in this awkward way to make sure we don't trigger exceptions with Eloquent strict mode on 205 | $keyName = method_exists($user, 'getAuthIdentifierName') ? $user->getAuthIdentifierName() : $user->getKeyName(); 206 | $user = $user->getAttributes(); 207 | 208 | $userId = $user[$keyName] ?? null; 209 | $userEmail = $user['email'] ?? $userId; 210 | $userName = $user['name'] ?? null; 211 | } else { 212 | $userId = $user->getAuthIdentifier(); 213 | $userEmail = $user->email ?? $userId; 214 | $userName = $user->name ?? null; 215 | } 216 | 217 | $request->setAuthenticatedUser($userEmail, $userId, [ 218 | 'email' => $userEmail, 219 | 'name' => $userName 220 | ]); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /Clockwork/DataSource/LaravelEventsDataSource.php: -------------------------------------------------------------------------------- 1 | dispatcher = $dispatcher; 24 | 25 | $this->ignoredEvents = is_array($ignoredEvents) 26 | ? array_merge($ignoredEvents, $this->defaultIgnoredEvents()) : []; 27 | } 28 | 29 | // Adds fired events to the request 30 | public function resolve(Request $request) 31 | { 32 | $request->events = array_merge($request->events, $this->events); 33 | 34 | return $request; 35 | } 36 | 37 | // Reset the data source to an empty state, clearing any collected data 38 | public function reset() 39 | { 40 | $this->events = []; 41 | } 42 | 43 | // Start listening to the events 44 | public function listenToEvents() 45 | { 46 | $this->dispatcher->listen('*', function ($event = null, $data = null) { 47 | if (method_exists($this->dispatcher, 'firing')) { // Laravel 5.0 - 5.3 48 | $data = func_get_args(); 49 | $event = $this->dispatcher->firing(); 50 | } 51 | 52 | $this->registerEvent($event, $data); 53 | }); 54 | } 55 | 56 | // Collect a fired event, prepares data for serialization and resolves registered listeners 57 | protected function registerEvent($event, array $data) 58 | { 59 | if (! $this->shouldCollect($event)) return; 60 | 61 | $trace = StackTrace::get()->resolveViewName(); 62 | 63 | $event = [ 64 | 'event' => $event, 65 | 'data' => (new Serializer)->normalize(count($data) == 1 && isset($data[0]) ? $data[0] : $data), 66 | 'time' => microtime(true), 67 | 'listeners' => $this->findListenersFor($event), 68 | 'trace' => (new Serializer)->trace($trace) 69 | ]; 70 | 71 | if ($this->passesFilters([ $event ])) { 72 | $this->events[] = $event; 73 | } 74 | } 75 | 76 | // Returns registered listeners for the specified event 77 | protected function findListenersFor($event) 78 | { 79 | $listener = $this->dispatcher->getListeners($event)[0]; 80 | 81 | return array_filter(array_map(function ($listener) { 82 | if ($listener instanceof \Closure) { 83 | // Laravel 5.4+ (and earlier versions in some cases) wrap the listener into a closure, 84 | // attempt to resolve the original listener 85 | $use = (new \ReflectionFunction($listener))->getStaticVariables(); 86 | $listener = isset($use['listener']) ? $use['listener'] : $listener; 87 | } 88 | 89 | if (is_string($listener)) { 90 | return $listener; 91 | } elseif (is_array($listener) && count($listener) == 2) { 92 | if (is_object($listener[0])) { 93 | return get_class($listener[0]) . '@' . $listener[1]; 94 | } else { 95 | return $listener[0] . '::' . $listener[1]; 96 | } 97 | } elseif ($listener instanceof \Closure) { 98 | $listener = new \ReflectionFunction($listener); 99 | 100 | if (strpos($listener->getNamespaceName(), 'Clockwork\\') === 0) { // skip our own listeners 101 | return; 102 | } 103 | 104 | $filename = str_replace(base_path(), '', $listener->getFileName()); 105 | $startLine = $listener->getStartLine(); 106 | $endLine = $listener->getEndLine(); 107 | 108 | return "Closure ({$filename}:{$startLine}-{$endLine})"; 109 | } 110 | }, $this->dispatcher->getListeners($event))); 111 | } 112 | 113 | // Returns whether the event should be collected (depending on ignored events) 114 | protected function shouldCollect($event) 115 | { 116 | return ! preg_match('/^(?:' . implode('|', $this->ignoredEvents) . ')$/', $event); 117 | } 118 | 119 | // Returns default ignored events (framework-specific events) 120 | protected function defaultIgnoredEvents() 121 | { 122 | return [ 123 | 'Illuminate\\\\.+', 124 | 'Laravel\\\\.+', 125 | 'auth\.(?:attempt|login|logout)', 126 | 'artisan\.start', 127 | 'bootstrapped:.+', 128 | 'composing:.+', 129 | 'creating:.+', 130 | 'illuminate\.query', 131 | 'connection\..+', 132 | 'eloquent\..+', 133 | 'kernel\.handled', 134 | 'illuminate\.log', 135 | 'mailer\.sending', 136 | 'router\.(?:before|after|matched)', 137 | 'router.filter:.+', 138 | 'locale\.changed', 139 | 'clockwork\..+' 140 | ]; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Clockwork/DataSource/LaravelHttpClientDataSource.php: -------------------------------------------------------------------------------- 1 | dispatcher = $dispatcher; 29 | 30 | $this->collectContent = $collectContent; 31 | $this->collectRawContent = $collectRawContent; 32 | } 33 | 34 | // Add sent notifications to the request 35 | public function resolve(Request $request) 36 | { 37 | $request->httpRequests = array_merge($request->httpRequests, $this->requests); 38 | 39 | return $request; 40 | } 41 | 42 | // Reset the data source to an empty state, clearing any collected data 43 | public function reset() 44 | { 45 | $this->requests = []; 46 | $this->executingRequests = []; 47 | } 48 | 49 | // Listen to the email and notification events 50 | public function listenToEvents() 51 | { 52 | $this->dispatcher->listen(ConnectionFailed::class, function ($event) { $this->connectionFailed($event); }); 53 | $this->dispatcher->listen(RequestSending::class, function ($event) { $this->sendingRequest($event); }); 54 | $this->dispatcher->listen(ResponseReceived::class, function ($event) { $this->responseReceived($event); }); 55 | } 56 | 57 | // Collect an executing request 58 | protected function sendingRequest(RequestSending $event) 59 | { 60 | $trace = StackTrace::get()->resolveViewName(); 61 | 62 | $request = (object) [ 63 | 'request' => (object) [ 64 | 'method' => $event->request->method(), 65 | 'url' => $this->removeAuthFromUrl($event->request->url()), 66 | 'headers' => $event->request->headers(), 67 | 'content' => $this->collectContent ? $event->request->data() : null, 68 | 'body' => $this->collectRawContent ? $event->request->body() : null 69 | ], 70 | 'response' => null, 71 | 'stats' => null, 72 | 'error' => null, 73 | 'time' => microtime(true), 74 | 'trace' => (new Serializer)->trace($trace) 75 | ]; 76 | 77 | if ($this->passesFilters([ $request ])) { 78 | $this->requests[] = $this->executingRequests[spl_object_hash($event->request)] = $request; 79 | } 80 | } 81 | 82 | // Update last request with response details and time taken 83 | protected function responseReceived($event) 84 | { 85 | if (! isset($this->executingRequests[spl_object_hash($event->request)])) return; 86 | 87 | $request = $this->executingRequests[spl_object_hash($event->request)]; 88 | $stats = $event->response->handlerStats(); 89 | 90 | $request->duration = (microtime(true) - $request->time) * 1000; 91 | $request->response = (object) [ 92 | 'status' => $event->response->status(), 93 | 'headers' => $event->response->headers(), 94 | 'content' => $this->collectContent ? $event->response->json() : null, 95 | 'body' => $this->collectRawContent ? $event->response->body() : null 96 | ]; 97 | $request->stats = (object) [ 98 | 'timing' => isset($stats['total_time_us']) ? (object) [ 99 | 'lookup' => $stats['namelookup_time_us'] / 1000, 100 | 'connect' => ($stats['pretransfer_time_us'] - $stats['namelookup_time_us']) / 1000, 101 | 'waiting' => ($stats['starttransfer_time_us'] - $stats['pretransfer_time_us']) / 1000, 102 | 'transfer' => ($stats['total_time_us'] - $stats['starttransfer_time_us']) / 1000 103 | ] : null, 104 | 'size' => (object) [ 105 | 'upload' => $stats['size_upload'] ?? null, 106 | 'download' => $stats['size_download'] ?? null 107 | ], 108 | 'speed' => (object) [ 109 | 'upload' => $stats['speed_upload'] ?? null, 110 | 'download' => $stats['speed_download'] ?? null 111 | ], 112 | 'hosts' => (object) [ 113 | 'local' => isset($stats['local_ip']) ? [ 'ip' => $stats['local_ip'], 'port' => $stats['local_port'] ] : null, 114 | 'remote' => isset($stats['primary_ip']) ? [ 'ip' => $stats['primary_ip'], 'port' => $stats['primary_port'] ] : null 115 | ], 116 | 'version' => $stats['http_version'] ?? null 117 | ]; 118 | 119 | $responseBody = $event->response->toPsrResponse()->getBody(); 120 | if ($responseBody->tell()) $responseBody->rewind(); 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/DataSource/LaravelQueueDataSource.php: -------------------------------------------------------------------------------- 1 | queue = $queue; 24 | } 25 | 26 | // Adds dispatched queue jobs to the request 27 | public function resolve(Request $request) 28 | { 29 | $request->queueJobs = array_merge($request->queueJobs, $this->getJobs()); 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->jobs = []; 38 | } 39 | 40 | // Listen to the queue events 41 | public function listenToEvents() 42 | { 43 | $this->queue->createPayloadUsing(function ($connection, $queue, $payload) { 44 | $this->registerJob([ 45 | 'id' => $id = (new Request)->id, 46 | 'connection' => $connection, 47 | 'queue' => $queue, 48 | 'name' => $payload['displayName'], 49 | 'data' => $payload['data']['command'] ?? null, 50 | 'maxTries' => $payload['maxTries'], 51 | 'timeout' => $payload['timeout'], 52 | 'time' => microtime(true) 53 | ]); 54 | 55 | return [ 'clockwork_id' => $id, 'clockwork_parent_id' => $this->currentRequestId ]; 56 | }); 57 | } 58 | 59 | // Set Clockwork ID of the current request 60 | public function setCurrentRequestId($requestId) 61 | { 62 | $this->currentRequestId = $requestId; 63 | return $this; 64 | } 65 | 66 | // Collect a dispatched queue job 67 | protected function registerJob(array $job) 68 | { 69 | $trace = StackTrace::get()->resolveViewName(); 70 | 71 | $job = array_merge($job, [ 72 | 'trace' => (new Serializer)->trace($trace) 73 | ]); 74 | 75 | if ($this->passesFilters([ $job ])) { 76 | $this->jobs[] = $job; 77 | } 78 | } 79 | 80 | // Get an array of dispatched queue jobs commands 81 | protected function getJobs() 82 | { 83 | return array_map(function ($query) { 84 | return array_merge($query, [ 85 | 'data' => isset($query['data']) ? (new Serializer)->normalize($query['data']) : null 86 | ]); 87 | }, $this->jobs); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Clockwork/DataSource/LaravelRedisDataSource.php: -------------------------------------------------------------------------------- 1 | eventDispatcher = $eventDispatcher; 24 | 25 | $this->skipCacheCommands = $skipCacheCommands; 26 | 27 | if ($this->skipCacheCommands) { 28 | $this->addFilter(function ($command, $trace) { 29 | return ! $trace->first(function ($frame) { return $frame->class == 'Illuminate\Cache\RedisStore'; }); 30 | }); 31 | } 32 | } 33 | 34 | // Adds redis commands to the request 35 | public function resolve(Request $request) 36 | { 37 | $request->redisCommands = array_merge($request->redisCommands, $this->getCommands()); 38 | 39 | return $request; 40 | } 41 | 42 | // Reset the data source to an empty state, clearing any collected data 43 | public function reset() 44 | { 45 | $this->commands = []; 46 | } 47 | 48 | // Listen to the cache events 49 | public function listenToEvents() 50 | { 51 | $this->eventDispatcher->listen(\Illuminate\Redis\Events\CommandExecuted::class, function ($event) { 52 | $this->registerCommand([ 53 | 'command' => $event->command, 54 | 'parameters' => $event->parameters, 55 | 'duration' => $event->time, 56 | 'connection' => $event->connectionName, 57 | 'time' => microtime(true) - $event->time / 1000 58 | ]); 59 | }); 60 | } 61 | 62 | // Collect an executed command 63 | protected function registerCommand(array $command) 64 | { 65 | $trace = StackTrace::get()->resolveViewName(); 66 | 67 | $command = array_merge($command, [ 68 | 'trace' => (new Serializer)->trace($trace) 69 | ]); 70 | 71 | if ($this->passesFilters([ $command, $trace ])) { 72 | $this->commands[] = $command; 73 | } 74 | } 75 | 76 | // Get an array of executed redis commands 77 | protected function getCommands() 78 | { 79 | return array_map(function ($query) { 80 | return array_merge($query, [ 81 | 'parameters' => isset($query['parameters']) ? (new Serializer)->normalize($query['parameters']) : null 82 | ]); 83 | }, $this->commands); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /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/DataSource/LumenDataSource.php: -------------------------------------------------------------------------------- 1 | app = $app; 32 | 33 | $this->collectLog = $collectLog; 34 | $this->collectRoutes = $collectRoutes; 35 | 36 | $this->log = new Log; 37 | } 38 | 39 | // Adds request, response information, middleware, routes, session data, user and log entries to the request 40 | public function resolve(Request $request) 41 | { 42 | $request->method = $this->getRequestMethod(); 43 | $request->uri = $this->getRequestUri(); 44 | $request->controller = $this->getController(); 45 | $request->headers = $this->getRequestHeaders(); 46 | $request->responseStatus = $this->getResponseStatus(); 47 | $request->routes = $this->getRoutes(); 48 | $request->sessionData = $this->getSessionData(); 49 | 50 | $this->resolveAuthenticatedUser($request); 51 | 52 | $request->log()->merge($this->log); 53 | 54 | return $request; 55 | } 56 | 57 | // Reset the data source to an empty state, clearing any collected data 58 | public function reset() 59 | { 60 | $this->log = new Log; 61 | } 62 | 63 | // Set Lumen response instance for the current request 64 | public function setResponse(Response $response) 65 | { 66 | $this->response = $response; 67 | return $this; 68 | } 69 | 70 | // Listen for the log events 71 | public function listenToEvents() 72 | { 73 | if (! $this->collectLog) return; 74 | 75 | if (class_exists(\Illuminate\Log\Events\MessageLogged::class)) { 76 | // Lumen 5.4 77 | $this->app['events']->listen(\Illuminate\Log\Events\MessageLogged::class, function ($event) { 78 | $this->log->log($event->level, $event->message, $event->context); 79 | }); 80 | } else { 81 | // Lumen 5.0 to 5.3 82 | $this->app['events']->listen('illuminate.log', function ($level, $message, $context) { 83 | $this->log->log($level, $message, $context); 84 | }); 85 | } 86 | } 87 | 88 | // Get a textual representation of current route's controller 89 | protected function getController() 90 | { 91 | $routes = method_exists($this->app, 'getRoutes') ? $this->app->getRoutes() : []; 92 | 93 | $method = $this->getRequestMethod(); 94 | $pathInfo = $this->getPathInfo(); 95 | 96 | $controller = $routes[$method.$pathInfo]['action']['uses'] ?? $routes[$method.$pathInfo]['action'][0] ?? null; 97 | 98 | if ($controller instanceof \Closure) { 99 | $controller = 'anonymous function'; 100 | } elseif (is_object($controller)) { 101 | $controller = 'instance of ' . get_class($controller); 102 | } elseif (! is_string($controller)) { 103 | $controller = null; 104 | } 105 | 106 | return $controller; 107 | } 108 | 109 | // Get the request headers 110 | protected function getRequestHeaders() 111 | { 112 | return $this->app['request']->headers->all(); 113 | } 114 | 115 | // Get the request method 116 | protected function getRequestMethod() 117 | { 118 | if ($this->app->bound('request')) { 119 | return $this->app['request']->getMethod(); 120 | } elseif (isset($_POST['_method'])) { 121 | return strtoupper($_POST['_method']); 122 | } else { 123 | return $_SERVER['REQUEST_METHOD']; 124 | } 125 | } 126 | 127 | // Get the request URI 128 | protected function getRequestUri() 129 | { 130 | return $this->app['request']->getRequestUri(); 131 | } 132 | 133 | // Get the response status code 134 | protected function getResponseStatus() 135 | { 136 | return $this->response ? $this->response->getStatusCode() : null; 137 | } 138 | 139 | // Get an array of application routes 140 | protected function getRoutes() 141 | { 142 | if (! $this->collectRoutes) return []; 143 | 144 | if (isset($this->app->router)) { 145 | $routes = array_values($this->app->router->getRoutes()); 146 | } elseif (method_exists($this->app, 'getRoutes')) { 147 | $routes = array_values($this->app->getRoutes()); 148 | } else { 149 | $routes = []; 150 | } 151 | 152 | return array_map(function ($route) { 153 | return [ 154 | 'method' => $route['method'], 155 | 'uri' => $route['uri'], 156 | 'name' => $route['action']['as'] ?? null, 157 | 'action' => is_string($route['action']['uses'] ?? null) ? $route['action']['uses'] : 'anonymous function', 158 | 'middleware' => $route['action']['middleware'] ?? null, 159 | ]; 160 | }, $routes); 161 | } 162 | 163 | // Get the session data (normalized with passwords removed) 164 | protected function getSessionData() 165 | { 166 | if (! isset($this->app['session'])) return []; 167 | 168 | return $this->removePasswords((new Serializer)->normalizeEach($this->app['session']->all())); 169 | } 170 | 171 | // Add authenticated user data to the request 172 | protected function resolveAuthenticatedUser(Request $request) 173 | { 174 | if (! isset($this->app['auth'])) return; 175 | if (! ($user = $this->app['auth']->user())) return; 176 | if (! isset($user->email) || ! isset($user->id)) return; 177 | 178 | $request->setAuthenticatedUser($user->email, $user->id, [ 179 | 'email' => $user->email, 180 | 'name' => $user->name ?? null 181 | ]); 182 | } 183 | 184 | // Get the request path info 185 | protected function getPathInfo() 186 | { 187 | if ($this->app->bound('request')) { 188 | return $this->app['request']->getPathInfo(); 189 | } else { 190 | $query = $_SERVER['QUERY_STRING'] ?? ''; 191 | return '/' . trim(str_replace("?{$query}", '', $_SERVER['REQUEST_URI']), '/'); 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /Clockwork/DataSource/MonologDataSource.php: -------------------------------------------------------------------------------- 1 | log = new Log; 19 | 20 | if (Logger::API === 1) { 21 | $handler = new Monolog\Monolog\ClockworkHandler($this->log); 22 | } elseif (Logger::API === 2) { 23 | $handler = new Monolog\Monolog2\ClockworkHandler($this->log); 24 | } else { 25 | $handler = new Monolog\Monolog3\ClockworkHandler($this->log); 26 | } 27 | 28 | $monolog->pushHandler($handler); 29 | } 30 | 31 | // Adds log entries to the request 32 | public function resolve(Request $request) 33 | { 34 | $request->log()->merge($this->log); 35 | 36 | return $request; 37 | } 38 | 39 | // Reset the data source to an empty state, clearing any collected data 40 | public function reset() 41 | { 42 | $this->log = new Log; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /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 | $headers[$header] = array_merge($headers[$header] ?? [], [ $value ]); 76 | } 77 | 78 | ksort($headers); 79 | 80 | return $headers; 81 | } 82 | 83 | // Get the request method 84 | protected function getRequestMethod() 85 | { 86 | return $_SERVER['REQUEST_METHOD'] ?? null; 87 | } 88 | 89 | // Get the response time 90 | protected function getRequestTime() 91 | { 92 | return $_SERVER['REQUEST_TIME_FLOAT'] ?? null; 93 | } 94 | 95 | // Get the request URL 96 | protected function getRequestUrl() 97 | { 98 | $https = ($_SERVER['HTTPS'] ?? null) == 'on'; 99 | $host = $_SERVER['HTTP_HOST'] ?? null; 100 | $addr = $_SERVER['SERVER_ADDR'] ?? null; 101 | $port = $_SERVER['SERVER_PORT'] ?? null; 102 | $uri = $_SERVER['REQUEST_URI'] ?? null; 103 | 104 | $scheme = $https ? 'https' : 'http'; 105 | $host = $host ?: $addr; 106 | $port = (! $https && $port != 80 || $https && $port != 443) ? ":{$port}" : ''; 107 | 108 | // remove port number from the host 109 | $host = $host ? preg_replace('/:\d+$/', '', trim($host)) : null; 110 | 111 | return "{$scheme}://{$host}{$port}{$uri}"; 112 | } 113 | 114 | // Get the request URI 115 | protected function getRequestUri() 116 | { 117 | return $_SERVER['REQUEST_URI'] ?? null; 118 | } 119 | 120 | // Get the response status code 121 | protected function getResponseStatus() 122 | { 123 | return http_response_code(); 124 | } 125 | 126 | // Get the response time (current time, assuming most of the application code has already run at this point) 127 | protected function getResponseTime() 128 | { 129 | return microtime(true); 130 | } 131 | 132 | // Get the session data (normalized with passwords removed) 133 | protected function getSessionData() 134 | { 135 | if (! isset($_SESSION)) return []; 136 | 137 | return $this->removePasswords((new Serializer)->normalizeEach($_SESSION)); 138 | } 139 | 140 | // Get the peak memory usage in bytes 141 | protected function getMemoryUsage() 142 | { 143 | return memory_get_peak_usage(true); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Clockwork/DataSource/PsrMessageDataSource.php: -------------------------------------------------------------------------------- 1 | psrRequest = $psrRequest; 20 | $this->psrResponse = $psrResponse; 21 | } 22 | 23 | // Adds request and response information to the request 24 | public function resolve(Request $request) 25 | { 26 | if ($this->psrRequest) { 27 | $request->method = $this->psrRequest->getMethod(); 28 | $request->uri = $this->getRequestUri(); 29 | $request->headers = $this->getRequestHeaders(); 30 | $request->getData = $this->sanitize($this->psrRequest->getQueryParams()); 31 | $request->postData = $this->sanitize($this->psrRequest->getParsedBody()); 32 | $request->cookies = $this->sanitize($this->psrRequest->getCookieParams()); 33 | $request->time = $this->getRequestTime(); 34 | } 35 | 36 | if ($this->psrResponse !== null) { 37 | $request->responseStatus = $this->psrResponse->getStatusCode(); 38 | $request->responseTime = $this->getResponseTime(); 39 | } 40 | 41 | return $request; 42 | } 43 | 44 | // Normalize items in the array and remove passwords 45 | protected function sanitize($data) 46 | { 47 | return is_array($data) ? $this->removePasswords((new Serializer)->normalizeEach($data)) : $data; 48 | } 49 | 50 | // Get the response time, fetching it from ServerParams 51 | protected function getRequestTime() 52 | { 53 | $env = $this->psrRequest->getServerParams(); 54 | 55 | return $env['REQUEST_TIME_FLOAT'] ?? null; 56 | } 57 | 58 | // Get the response time (current time, assuming most of the application code has already run at this point) 59 | protected function getResponseTime() 60 | { 61 | return microtime(true); 62 | } 63 | 64 | // Get the request headers 65 | protected function getRequestHeaders() 66 | { 67 | $headers = []; 68 | 69 | foreach ($this->psrRequest->getHeaders() as $header => $values) { 70 | if (strtoupper(substr($header, 0, 5)) === 'HTTP_') { 71 | $header = substr($header, 5); 72 | } 73 | 74 | $header = str_replace('_', ' ', $header); 75 | $header = ucwords(strtolower($header)); 76 | $header = str_replace(' ', '-', $header); 77 | 78 | $headers[$header] = $values; 79 | } 80 | 81 | ksort($headers); 82 | 83 | return $headers; 84 | } 85 | 86 | // Get the request URI 87 | protected function getRequestUri() 88 | { 89 | $uri = $this->psrRequest->getUri(); 90 | 91 | return $uri->getPath() . ($uri->getQuery() ? '?' . $uri->getQuery() : ''); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /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 | $headers[$header] = array_merge($headers[$header] ?? [], [ $value ]); 76 | } 77 | 78 | ksort($headers); 79 | 80 | return $headers; 81 | } 82 | 83 | // Get the request method 84 | protected function getRequestMethod() 85 | { 86 | return $this->slim->request()->getMethod(); 87 | } 88 | 89 | // Get the request URI 90 | protected function getRequestUri() 91 | { 92 | return $this->slim->request()->getPathInfo(); 93 | } 94 | 95 | // Get the response status code 96 | protected function getResponseStatus() 97 | { 98 | return $this->slim->response()->status(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /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/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/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 = $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/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/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/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/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/Helpers/StackFrame.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/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 = $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/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 $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 $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/Request/Log.php: -------------------------------------------------------------------------------- 1 | messages = $messages; 15 | } 16 | 17 | // Log a new message, with a level and context, context can be used to override serializer defaults, 18 | // $context['trace'] = true can be used to force collecting a stack trace 19 | public function log($level = LogLevel::INFO, $message = null, array $context = []) 20 | { 21 | $trace = $this->hasTrace($context) ? $context['trace'] : StackTrace::get()->resolveViewName(); 22 | 23 | $this->messages[] = [ 24 | 'message' => (new Serializer($context))->normalize($message), 25 | 'exception' => $this->formatException($context), 26 | 'context' => $this->formatContext($context), 27 | 'level' => $level, 28 | 'time' => microtime(true), 29 | 'trace' => (new Serializer(! empty($context['trace']) ? [ 'traces' => true ] : []))->trace($trace) 30 | ]; 31 | } 32 | 33 | public function emergency($message, array $context = []) 34 | { 35 | $this->log(LogLevel::EMERGENCY, $message, $context); 36 | } 37 | 38 | public function alert($message, array $context = []) 39 | { 40 | $this->log(LogLevel::ALERT, $message, $context); 41 | } 42 | 43 | public function critical($message, array $context = []) 44 | { 45 | $this->log(LogLevel::CRITICAL, $message, $context); 46 | } 47 | 48 | public function error($message, array $context = []) 49 | { 50 | $this->log(LogLevel::ERROR, $message, $context); 51 | } 52 | 53 | public function warning($message, array $context = []) 54 | { 55 | $this->log(LogLevel::WARNING, $message, $context); 56 | } 57 | 58 | public function notice($message, array $context = []) 59 | { 60 | $this->log(LogLevel::NOTICE, $message, $context); 61 | } 62 | 63 | public function info($message, array $context = []) 64 | { 65 | $this->log(LogLevel::INFO, $message, $context); 66 | } 67 | 68 | public function debug($message, array $context = []) 69 | { 70 | $this->log(LogLevel::DEBUG, $message, $context); 71 | } 72 | 73 | // Merge another log instance into the current log 74 | public function merge(Log $log) 75 | { 76 | $this->messages = array_merge($this->messages, $log->messages); 77 | 78 | return $this; 79 | } 80 | 81 | // Sort the log messages by timestamp 82 | public function sort() 83 | { 84 | usort($this->messages, function ($a, $b) { return $a['time'] * 1000 - $b['time'] * 1000; }); 85 | } 86 | 87 | // Get all messages as an array 88 | public function toArray() 89 | { 90 | return $this->messages; 91 | } 92 | 93 | // Format message context, removes exception and trace if we are serializing them 94 | protected function formatContext($context) 95 | { 96 | if ($this->hasException($context)) unset($context['exception']); 97 | if ($this->hasTrace($context)) unset($context['trace']); 98 | 99 | return (new Serializer)->normalize($context); 100 | } 101 | 102 | // Format exception if present in the context 103 | protected function formatException($context) 104 | { 105 | if ($this->hasException($context)) { 106 | return (new Serializer)->exception($context['exception']); 107 | } 108 | } 109 | 110 | // Check if context has serializable trace 111 | protected function hasTrace($context) 112 | { 113 | return ! empty($context['trace']) && $context['trace'] instanceof StackTrace && empty($context['raw']); 114 | } 115 | 116 | // Check if context has serializable exception 117 | protected function hasException($context) 118 | { 119 | return ! empty($context['exception']) 120 | && ($context['exception'] instanceof \Throwable || $context['exception'] instanceof \Exception) 121 | && empty($context['raw']); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Clockwork/Request/LogLevel.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 = $request->input['clockwork-profile'] ?? ''; 61 | $cookie = $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/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/Request/Timeline/Event.php: -------------------------------------------------------------------------------- 1 | description = $description; 24 | $this->name = $data['name'] ?? $description; 25 | 26 | $this->start = $data['start'] ?? null; 27 | $this->end = $data['end'] ?? null; 28 | 29 | $this->color = $data['color'] ?? null; 30 | $this->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/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 = $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/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/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/Storage/Search.php: -------------------------------------------------------------------------------- 1 | $condition = $search[$condition] ?? []; 26 | } 27 | 28 | foreach ([ 'stopOnFirstMismatch' ] as $option) { 29 | $this->$option = $options[$option] ?? $this->$option; 30 | } 31 | 32 | $this->method = array_map('strtolower', $this->method); 33 | } 34 | 35 | // Create a new instance from request input 36 | public static function fromRequest($data = []) 37 | { 38 | return new static($data); 39 | } 40 | 41 | // Check whether the request matches current search parameters 42 | public function matches(Request $request) 43 | { 44 | if ($request->type == RequestType::COMMAND) { 45 | return $this->matchesCommand($request); 46 | } elseif ($request->type == RequestType::QUEUE_JOB) { 47 | return $this->matchesQueueJob($request); 48 | } elseif ($request->type == RequestType::TEST) { 49 | return $this->matchesTest($request); 50 | } else { 51 | return $this->matchesRequest($request); 52 | } 53 | } 54 | 55 | // Check whether a request type request matches 56 | protected function matchesRequest(Request $request) 57 | { 58 | return $this->matchesString($this->type, RequestType::REQUEST) 59 | && $this->matchesString($this->uri, $request->uri) 60 | && $this->matchesString($this->controller, $request->controller) 61 | && $this->matchesExact($this->method, strtolower($request->method)) 62 | && $this->matchesNumber($this->status, $request->responseStatus) 63 | && $this->matchesNumber($this->time, $request->responseDuration) 64 | && $this->matchesDate($this->received, $request->time); 65 | } 66 | 67 | // Check whether a command type request matches 68 | protected function matchesCommand(Request $request) 69 | { 70 | return $this->matchesString($this->type, RequestType::COMMAND) 71 | && $this->matchesString($this->name, $request->commandName) 72 | && $this->matchesNumber($this->status, $request->commandExitCode) 73 | && $this->matchesNumber($this->time, $request->responseDuration) 74 | && $this->matchesDate($this->received, $request->time); 75 | } 76 | 77 | // Check whether a queue-job type request matches 78 | protected function matchesQueueJob(Request $request) 79 | { 80 | return $this->matchesString($this->type, RequestType::QUEUE_JOB) 81 | && $this->matchesString($this->name, $request->jobName) 82 | && $this->matchesString($this->status, $request->jobStatus) 83 | && $this->matchesNumber($this->time, $request->responseDuration) 84 | && $this->matchesDate($this->received, $request->time); 85 | } 86 | 87 | // Check whether a test type request matches 88 | protected function matchesTest(Request $request) 89 | { 90 | return $this->matchesString($this->type, RequestType::TEST) 91 | && $this->matchesString($this->name, $request->testName) 92 | && $this->matchesString($this->status, $request->testStatus) 93 | && $this->matchesNumber($this->time, $request->responseDuration) 94 | && $this->matchesDate($this->received, $request->time); 95 | } 96 | 97 | // Check if there are no search parameters specified 98 | public function isEmpty() 99 | { 100 | return ! count($this->uri) && ! count($this->controller) && ! count($this->method) && ! count($this->status) 101 | && ! count($this->time) && ! count($this->received) && ! count($this->name) && ! count($this->type); 102 | } 103 | 104 | // Check if there are some search parameters specified 105 | public function isNotEmpty() 106 | { 107 | return ! $this->isEmpty(); 108 | } 109 | 110 | // Check if the value matches date type search parameter 111 | protected function matchesDate($inputs, $value) 112 | { 113 | if (! count($inputs)) return true; 114 | 115 | foreach ($inputs as $input) { 116 | if (preg_match('/^<(.+)$/', $input, $match)) { 117 | if ($value < strtotime($match[1])) return true; 118 | } elseif (preg_match('/^>(.+)$/', $input, $match)) { 119 | if ($value > strtotime($match[1])) return true; 120 | } 121 | } 122 | 123 | return false; 124 | } 125 | 126 | // Check if the value matches exact type search parameter 127 | protected function matchesExact($inputs, $value) 128 | { 129 | if (! count($inputs)) return true; 130 | 131 | return in_array($value, $inputs); 132 | } 133 | 134 | // Check if the value matches number type search parameter 135 | protected function matchesNumber($inputs, $value) 136 | { 137 | if (! count($inputs)) return true; 138 | 139 | foreach ($inputs as $input) { 140 | if (preg_match('/^<(\d+(?:\.\d+)?)$/', $input, $match)) { 141 | if ($value < $match[1]) return true; 142 | } elseif (preg_match('/^>(\d+(?:\.\d+)?)$/', $input, $match)) { 143 | if ($value > $match[1]) return true; 144 | } elseif (preg_match('/^(\d+(?:\.\d+)?)-(\d+(?:\.\d+)?)$/', $input, $match)) { 145 | if ($match[1] < $value && $value < $match[2]) return true; 146 | } else { 147 | if ($value == $input) return true; 148 | } 149 | } 150 | 151 | return false; 152 | } 153 | 154 | // Check if the value matches string type search parameter 155 | protected function matchesString($inputs, $value) 156 | { 157 | if (! count($inputs)) return true; 158 | 159 | foreach ($inputs as $input) { 160 | if (strpos($value, $input) !== false) return true; 161 | } 162 | 163 | return false; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /Clockwork/Storage/SqlSearch.php: -------------------------------------------------------------------------------- 1 | pdo = $pdo; 26 | 27 | [ $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/Storage/Storage.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/Doctrine/Connection.php: -------------------------------------------------------------------------------- 1 | onQuery = $onQuery; 17 | } 18 | 19 | public function query(string $sql): Result 20 | { 21 | $time = microtime(true); 22 | 23 | $result = parent::query($sql); 24 | 25 | ($this->onQuery)([ 'query' => $sql, 'time' => $time ]); 26 | 27 | return $result; 28 | } 29 | 30 | public function exec(string $sql): int 31 | { 32 | $time = microtime(true); 33 | 34 | $result = parent::exec($sql); 35 | 36 | ($this->onQuery)([ 'query' => $sql, 'time' => $time ]); 37 | 38 | return $result; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Clockwork/Support/Doctrine/Driver.php: -------------------------------------------------------------------------------- 1 | onQuery = $onQuery; 16 | } 17 | 18 | public function connect(array $params): Connection 19 | { 20 | return new Connection(parent::connect($params), $this->onQuery); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Clockwork/Support/Doctrine/Legacy/Logger.php: -------------------------------------------------------------------------------- 1 | onQuery = $onQuery; 24 | 25 | $this->connection = $connection; 26 | 27 | $configuration = $this->connection->getConfiguration(); 28 | $currentLogger = $configuration->getSQLLogger(); 29 | 30 | if ($currentLogger === null) { 31 | $configuration->setSQLLogger($this); 32 | } else { 33 | $loggerChain = new LoggerChain; 34 | $loggerChain->addLogger($currentLogger); 35 | $loggerChain->addLogger($this); 36 | 37 | $configuration->setSQLLogger($loggerChain); 38 | } 39 | } 40 | 41 | // DBAL SQLLogger event 42 | public function startQuery($sql, ?array $params = null, ?array $types = null) 43 | { 44 | $this->query = [ 45 | 'query' => $sql, 46 | 'params' => $params, 47 | 'types' => $types, 48 | 'time' => microtime(true) 49 | ]; 50 | } 51 | 52 | // DBAL SQLLogger event 53 | public function stopQuery() 54 | { 55 | $this->registerQuery($this->query); 56 | $this->query = null; 57 | } 58 | 59 | // Collect an executed database query 60 | protected function registerQuery($query) 61 | { 62 | ($this->onQuery)([ 63 | 'query' => $this->createRunnableQuery($query['query'], $query['params'], $query['types']), 64 | 'bindings' => $query['params'], 65 | 'time' => $query['time'] 66 | ]); 67 | } 68 | 69 | // Takes a query, an array of params and types as arguments, returns runnable query with upper-cased keywords 70 | protected function createRunnableQuery($query, $params, $types) 71 | { 72 | // add params to query 73 | $query = $this->replaceParams($this->connection->getDatabasePlatform(), $query, $params, $types); 74 | 75 | // highlight keywords 76 | $keywords = [ 77 | 'select', 'insert', 'update', 'delete', 'into', 'values', 'set', 'where', 'from', 'limit', 'is', 'null', 78 | 'having', 'group by', 'order by', 'asc', 'desc' 79 | ]; 80 | $regexp = '/\b' . implode('\b|\b', $keywords) . '\b/i'; 81 | 82 | return preg_replace_callback($regexp, function ($match) { return strtoupper($match[0]); }, $query); 83 | } 84 | 85 | /** 86 | * Source at laravel-doctrine/orm LaravelDoctrine\ORM\Loggers\Formatters\ReplaceQueryParams::format(). 87 | * 88 | * @param AbstractPlatform $platform 89 | * @param string $sql 90 | * @param array|null $params 91 | * @param array|null $types 92 | * 93 | * 94 | * @return string 95 | */ 96 | public function replaceParams($platform, $sql, ?array $params = null, ?array $types = null) 97 | { 98 | if (is_array($params)) { 99 | foreach ($params as $key => $param) { 100 | $type = $types[$key] ?? null; 101 | $param = $this->convertParam($platform, $param, $type); 102 | $sql = preg_replace('/\?/', "$param", $sql, 1); 103 | } 104 | } 105 | return $sql; 106 | } 107 | 108 | /** 109 | * Source at laravel-doctrine/orm LaravelDoctrine\ORM\Loggers\Formatters\ReplaceQueryParams::convertParam(). 110 | * 111 | * @param mixed $param 112 | * 113 | * @throws \Exception 114 | * @return string 115 | */ 116 | protected function convertParam($platform, $param, $type = null) 117 | { 118 | if (is_object($param)) { 119 | if (!method_exists($param, '__toString')) { 120 | if ($param instanceof \DateTimeInterface) { 121 | $param = $param->format('Y-m-d H:i:s'); 122 | } elseif (Type::hasType($type)) { 123 | $type = Type::getType($type); 124 | $param = $type->convertToDatabaseValue($param, $platform); 125 | } else { 126 | throw new \Exception('Given query param is an instance of ' . get_class($param) . ' and could not be converted to a string'); 127 | } 128 | } 129 | } elseif (is_array($param)) { 130 | if ($this->isNestedArray($param)) { 131 | $param = json_encode($param, JSON_UNESCAPED_UNICODE); 132 | } else { 133 | $param = implode( 134 | ', ', 135 | array_map( 136 | function ($part) { 137 | return '"' . (string) $part . '"'; 138 | }, 139 | $param 140 | ) 141 | ); 142 | return '(' . $param . ')'; 143 | } 144 | } else { 145 | $param = htmlspecialchars((string) $param); // Originally used the e() Laravel helper 146 | } 147 | return '"' . (string) $param . '"'; 148 | } 149 | 150 | /** 151 | * Source at laravel-doctrine/orm LaravelDoctrine\ORM\Loggers\Formatters\ReplaceQueryParams::isNestedArray(). 152 | * 153 | * @param array $array 154 | * @return bool 155 | */ 156 | private function isNestedArray(array $array) 157 | { 158 | foreach ($array as $key => $value) { 159 | if (is_array($value)) { 160 | return true; 161 | } 162 | } 163 | return false; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /Clockwork/Support/Doctrine/Middleware.php: -------------------------------------------------------------------------------- 1 | onQuery = $onQuery; 16 | } 17 | 18 | public function wrap(DriverInterface $driver): DriverInterface 19 | { 20 | return new Driver($driver, $this->onQuery); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /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/Support/Laravel/ClockworkController.php: -------------------------------------------------------------------------------- 1 | ensureClockworkIsEnabled($clockworkSupport); 17 | 18 | $token = $clockwork->authenticator()->attempt( 19 | $request->only([ 'username', 'password' ]) 20 | ); 21 | 22 | return new JsonResponse([ 'token' => $token ], $token ? 200 : 403); 23 | } 24 | 25 | // Metadata retrieving endpoint 26 | public function getData(ClockworkSupport $clockworkSupport, Request $request, $id = null, $direction = null, $count = null) 27 | { 28 | $this->ensureClockworkIsEnabled($clockworkSupport); 29 | 30 | return $clockworkSupport->getData( 31 | $id, $direction, $count, $request->only([ 'only', 'except' ]) 32 | ); 33 | } 34 | 35 | // Extended metadata retrieving endpoint 36 | public function getExtendedData(ClockworkSupport $clockworkSupport, Request $request, $id = null) 37 | { 38 | $this->ensureClockworkIsEnabled($clockworkSupport); 39 | 40 | return $clockworkSupport->getExtendedData( 41 | $id, $request->only([ 'only', 'except' ]) 42 | ); 43 | } 44 | 45 | // Metadata updating endpoint 46 | public function updateData(ClockworkSupport $clockworkSupport, Request $request, $id = null) 47 | { 48 | $this->ensureClockworkIsEnabled($clockworkSupport); 49 | 50 | return $clockworkSupport->updateData($id, $request->json()->all()); 51 | } 52 | 53 | // App index 54 | public function webIndex(ClockworkSupport $clockworkSupport) 55 | { 56 | $this->ensureClockworkIsEnabled($clockworkSupport); 57 | 58 | return $clockworkSupport->getWebAsset('index.html'); 59 | } 60 | 61 | // App assets serving 62 | public function webAsset(ClockworkSupport $clockworkSupport, $path) 63 | { 64 | $this->ensureClockworkIsEnabled($clockworkSupport); 65 | 66 | return $clockworkSupport->getWebAsset($path); 67 | } 68 | 69 | // App redirect (/clockwork -> /clockwork/app) 70 | public function webRedirect(ClockworkSupport $clockworkSupport, Request $request) 71 | { 72 | $this->ensureClockworkIsEnabled($clockworkSupport); 73 | 74 | return new RedirectResponse('/' . $request->path() . '/app'); 75 | } 76 | 77 | // Ensure Clockwork is still enabled at this point and stop Telescope recording if present 78 | protected function ensureClockworkIsEnabled(ClockworkSupport $clockworkSupport) 79 | { 80 | if (class_exists(Telescope::class)) Telescope::stopRecording(); 81 | 82 | if (! $clockworkSupport->isEnabled()) abort(404); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Clockwork/Support/Laravel/Console/CapturingFormatter.php: -------------------------------------------------------------------------------- 1 | formatter = $formatter; 15 | } 16 | 17 | public function capturedOutput() 18 | { 19 | $capturedOutput = $this->capturedOutput; 20 | 21 | $this->capturedOutput = null; 22 | 23 | return $capturedOutput; 24 | } 25 | 26 | public function setDecorated(bool $decorated): void 27 | { 28 | $this->formatter->setDecorated($decorated); 29 | } 30 | 31 | public function isDecorated(): bool 32 | { 33 | return $this->formatter->isDecorated(); 34 | } 35 | 36 | public function setStyle(string $name, OutputFormatterStyleInterface $style): void 37 | { 38 | $this->formatter->setStyle($name, $style); 39 | } 40 | 41 | public function hasStyle(string $name): bool 42 | { 43 | return $this->formatter->hasStyle($name); 44 | } 45 | 46 | public function getStyle(string $name): OutputFormatterStyleInterface 47 | { 48 | return $this->formatter->getStyle($name); 49 | } 50 | 51 | public function format(?string $message): ?string 52 | { 53 | $formatted = $this->formatter->format($message); 54 | 55 | $this->capturedOutput .= $formatted; 56 | 57 | return $formatted; 58 | } 59 | 60 | public function __call($method, $args) 61 | { 62 | return $this->formatter->$method(...$args); 63 | } 64 | 65 | public function __clone() 66 | { 67 | $this->formatter = clone $this->formatter; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Clockwork/Support/Laravel/Console/CapturingLegacyFormatter.php: -------------------------------------------------------------------------------- 1 | formatter = $formatter; 15 | } 16 | 17 | public function capturedOutput() 18 | { 19 | $capturedOutput = $this->capturedOutput; 20 | 21 | $this->capturedOutput = null; 22 | 23 | return $capturedOutput; 24 | } 25 | 26 | public function setDecorated(bool $decorated) 27 | { 28 | return $this->formatter->setDecorated($decorated); 29 | } 30 | 31 | public function isDecorated(): bool 32 | { 33 | return $this->formatter->isDecorated(); 34 | } 35 | 36 | public function setStyle(string $name, OutputFormatterStyleInterface $style) 37 | { 38 | return $this->formatter->setStyle($name, $style); 39 | } 40 | 41 | public function hasStyle(string $name): bool 42 | { 43 | return $this->formatter->hasStyle($name); 44 | } 45 | 46 | public function getStyle(string $name): OutputFormatterStyleInterface 47 | { 48 | return $this->formatter->getStyle($name); 49 | } 50 | 51 | public function format(?string $message): ?string 52 | { 53 | $formatted = $this->formatter->format($message); 54 | 55 | $this->capturedOutput .= $formatted; 56 | 57 | return $formatted; 58 | } 59 | 60 | public function __call($method, $args) 61 | { 62 | return $this->formatter->$method(...$args); 63 | } 64 | 65 | public function __clone() 66 | { 67 | $this->formatter = clone $this->formatter; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Clockwork/Support/Laravel/Console/CapturingOldFormatter.php: -------------------------------------------------------------------------------- 1 | formatter = $formatter; 15 | } 16 | 17 | public function capturedOutput() 18 | { 19 | $capturedOutput = $this->capturedOutput; 20 | 21 | $this->capturedOutput = null; 22 | 23 | return $capturedOutput; 24 | } 25 | 26 | public function setDecorated($decorated) 27 | { 28 | return $this->formatter->setDecorated($decorated); 29 | } 30 | 31 | public function isDecorated() 32 | { 33 | return $this->formatter->isDecorated(); 34 | } 35 | 36 | public function setStyle($name, OutputFormatterStyleInterface $style) 37 | { 38 | return $this->formatter->setStyle($name, $style); 39 | } 40 | 41 | public function hasStyle($name) 42 | { 43 | return $this->formatter->hasStyle($name); 44 | } 45 | 46 | public function getStyle($name) 47 | { 48 | return $this->formatter->getStyle($name); 49 | } 50 | 51 | public function format($message) 52 | { 53 | $formatted = $this->formatter->format($message); 54 | 55 | $this->capturedOutput .= $formatted; 56 | 57 | return $formatted; 58 | } 59 | 60 | public function __call($method, $args) 61 | { 62 | return $this->formatter->$method(...$args); 63 | } 64 | 65 | public function __clone() 66 | { 67 | $this->formatter = clone $this->formatter; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Clockwork/Support/Laravel/Eloquent/ResolveModelLegacyScope.php: -------------------------------------------------------------------------------- 1 | dataSource = $dataSource; 14 | } 15 | 16 | public function apply(Builder $builder, Model $model) 17 | { 18 | $this->dataSource->nextQueryModel = get_class($model); 19 | } 20 | 21 | public function remove(Builder $builder, Model $model) 22 | { 23 | // nothing to do here 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Clockwork/Support/Laravel/Eloquent/ResolveModelScope.php: -------------------------------------------------------------------------------- 1 | dataSource = $dataSource; 14 | } 15 | 16 | public function apply(Builder $builder, Model $model) 17 | { 18 | $this->dataSource->nextQueryModel = get_class($model); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Clockwork/Support/Laravel/Facade.php: -------------------------------------------------------------------------------- 1 | throwable()->message()); } 23 | }, 24 | new class implements Event\Test\FailedSubscriber { 25 | public function notify($event): void { ClockworkExtension::recordTest('failed', $event->throwable()->message()); } 26 | }, 27 | new class implements Event\Test\MarkedIncompleteSubscriber { 28 | public function notify($event): void { ClockworkExtension::recordTest('incomplete', $event->throwable()->message()); } 29 | }, 30 | new class implements Event\Test\PassedSubscriber { 31 | public function notify($event): void { ClockworkExtension::recordTest('passed'); } 32 | }, 33 | new class implements Event\Test\SkippedSubscriber { 34 | public function notify($event): void { ClockworkExtension::recordTest('skipped', $event->message()); } 35 | }, 36 | interface_exists(Event\Test\AssertionSucceededSubscriber::class) ? new class implements Event\Test\AssertionSucceededSubscriber { 37 | public function notify($event): void { ClockworkExtension::recordAssertion(true); } 38 | } : null, 39 | interface_exists(Event\Test\AssertionFailedSubscriber::class) ? new class implements Event\Test\AssertionFailedSubscriber { 40 | public function notify($event): void { ClockworkExtension::recordAssertion(false); } 41 | } : null 42 | ]); 43 | 44 | $facade->registerSubscribers(...$subscribers); 45 | } 46 | 47 | public static function recordTest($status, $message = null) 48 | { 49 | $testCase = static::resolveTestCase(); 50 | 51 | if (! $testCase) return; 52 | 53 | $app = static::resolveApp($testCase); 54 | 55 | if (! $app) return; 56 | 57 | if (! $app->make('clockwork.support')->isCollectingTests()) return; 58 | if ($app->make('clockwork.support')->isTestFiltered($testCase->toString())) return; 59 | 60 | $app->make('clockwork') 61 | ->resolveAsTest( 62 | str_replace('__pest_evaluable_', '', $testCase->toString()), 63 | $status, 64 | $message, 65 | static::$asserts 66 | ) 67 | ->storeRequest(); 68 | } 69 | 70 | public static function recordAssertion($passed = true) 71 | { 72 | $trace = StackTrace::get([ 'arguments' => true, 'limit' => 10 ]); 73 | $assertFrame = $trace->filter(function ($frame) { return strpos($frame->function, 'assert') === 0; })->last(); 74 | 75 | $trace = $trace->skip(StackFilter::make()->isNotVendor([ 'itsgoingd', 'phpunit' ]))->limit(3); 76 | 77 | static::$asserts[] = [ 78 | 'name' => $assertFrame->function, 79 | 'arguments' => $assertFrame->args, 80 | 'trace' => (new Serializer)->trace($trace), 81 | 'passed' => $passed 82 | ]; 83 | } 84 | 85 | protected static function resolveTestCase() 86 | { 87 | $trace = StackTrace::get([ 'arguments' => false, 'limit' => 10 ]); 88 | 89 | $testFrame = $trace->filter(function ($frame) { return $frame->object instanceof \PHPUnit\Framework\TestCase; })->last(); 90 | 91 | return $testFrame?->object; 92 | } 93 | 94 | protected static function resolveApp($testCase) 95 | { 96 | $reflectionClass = new \ReflectionClass($testCase); 97 | 98 | if ($reflectionClass->hasProperty('app')) { 99 | $reflectionProperty = $reflectionClass->getProperty('app'); 100 | $reflectionProperty->setAccessible(true); 101 | 102 | if ($reflectionProperty->getValue($testCase)) { 103 | return $reflectionProperty->getValue($testCase); 104 | } 105 | } elseif (method_exists($testCase, 'createApplication')) { 106 | return $testCase->createApplication(); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Clockwork/Support/Laravel/Tests/UsesClockwork.php: -------------------------------------------------------------------------------- 1 | [] 14 | ]; 15 | 16 | // Set up Clockwork in this test case, should be called from the PHPUnit setUp method 17 | protected function setUpClockwork() 18 | { 19 | if (! $this->app->make('clockwork.support')->isCollectingTests()) return; 20 | 21 | $this->beforeApplicationDestroyed(function () { 22 | if ($this->app->make('clockwork.support')->isTestFiltered($this->toString())) return; 23 | 24 | $this->app->make('clockwork') 25 | ->resolveAsTest( 26 | $this->toString(), 27 | $this->resolveClockworkStatus(), 28 | $this->getStatusMessage(), 29 | $this->resolveClockworkAsserts() 30 | ) 31 | ->storeRequest(); 32 | }); 33 | } 34 | 35 | // Resolve Clockwork test status 36 | protected function resolveClockworkStatus() 37 | { 38 | $status = $this->getStatus(); 39 | 40 | $statuses = [ 41 | BaseTestRunner::STATUS_UNKNOWN => 'unknown', 42 | BaseTestRunner::STATUS_PASSED => 'passed', 43 | BaseTestRunner::STATUS_SKIPPED => 'skipped', 44 | BaseTestRunner::STATUS_INCOMPLETE => 'incomplete', 45 | BaseTestRunner::STATUS_FAILURE => 'failed', 46 | BaseTestRunner::STATUS_ERROR => 'error', 47 | BaseTestRunner::STATUS_RISKY => 'passed', 48 | BaseTestRunner::STATUS_WARNING => 'warning' 49 | ]; 50 | 51 | return $statuses[$status] ?? null; 52 | } 53 | 54 | // Resolve executed asserts 55 | protected function resolveClockworkAsserts() 56 | { 57 | $asserts = static::$clockwork['asserts']; 58 | 59 | if ($this->getStatus() == BaseTestRunner::STATUS_FAILURE && count($asserts)) { 60 | $asserts[count($asserts) - 1]['passed'] = false; 61 | } 62 | 63 | static::$clockwork['asserts'] = []; 64 | 65 | return $asserts; 66 | } 67 | 68 | // Overload the main PHPUnit assert method to collect executed asserts 69 | public static function assertThat($value, Constraint $constraint, string $message = ''): void 70 | { 71 | $trace = StackTrace::get([ 'arguments' => true, 'limit' => 10 ]); 72 | 73 | $assertFrame = $trace->filter(function ($frame) { return strpos($frame->function, 'assert') === 0; })->last(); 74 | $trace = $trace->skip(StackFilter::make()->isNotVendor([ 'itsgoingd', 'phpunit' ]))->limit(3); 75 | 76 | static::$clockwork['asserts'][] = [ 77 | 'name' => $assertFrame->function, 78 | 'arguments' => $assertFrame->args, 79 | 'trace' => (new Serializer)->trace($trace), 80 | 'passed' => true 81 | ]; 82 | 83 | parent::assertThat($value, $constraint, $message); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Clockwork/Support/Laravel/helpers.php: -------------------------------------------------------------------------------- 1 | debug($argument); 13 | } 14 | 15 | return reset($arguments); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /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/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 = $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 = $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/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/Support/Lumen/Controller.php: -------------------------------------------------------------------------------- 1 | clockwork = $clockwork; 20 | $this->clockworkSupport = $clockworkSupport; 21 | } 22 | 23 | // Authantication endpoint 24 | public function authenticate(Request $request) 25 | { 26 | $this->ensureClockworkIsEnabled(); 27 | 28 | $token = $this->clockwork->authenticator()->attempt( 29 | $request->only([ 'username', 'password' ]) 30 | ); 31 | 32 | return new JsonResponse([ 'token' => $token ], $token ? 200 : 403); 33 | } 34 | 35 | // Metadata retrieving endpoint 36 | public function getData(Request $request, $id = null, $direction = null, $count = null) 37 | { 38 | $this->ensureClockworkIsEnabled(); 39 | 40 | return $this->clockworkSupport->getData( 41 | $id, $direction, $count, $request->only([ 'only', 'except' ]) 42 | ); 43 | } 44 | 45 | // Extended metadata retrieving endpoint 46 | public function getExtendedData(Request $request, $id = null) 47 | { 48 | $this->ensureClockworkIsEnabled(); 49 | 50 | return $this->clockworkSupport->getExtendedData( 51 | $id, $request->only([ 'only', 'except' ]) 52 | ); 53 | } 54 | 55 | // Metadata updating endpoint 56 | public function updateData(Request $request, $id = null) 57 | { 58 | $this->ensureClockworkIsEnabled(); 59 | 60 | return $this->clockworkSupport->updateData($id, $request->json()->all()); 61 | } 62 | 63 | // App index 64 | public function webIndex(Request $request) 65 | { 66 | $this->ensureClockworkIsEnabled(); 67 | 68 | return $this->clockworkSupport->getWebAsset('index.html'); 69 | } 70 | 71 | // App assets serving 72 | public function webAsset($path) 73 | { 74 | $this->ensureClockworkIsEnabled(); 75 | 76 | return $this->clockworkSupport->getWebAsset($path); 77 | } 78 | 79 | // App redirect (/clockwork -> /clockwork/app) 80 | public function webRedirect(Request $request) 81 | { 82 | $this->ensureClockworkIsEnabled(); 83 | 84 | return new RedirectResponse('/' . $request->path() . '/app'); 85 | } 86 | 87 | // Ensure Clockwork is still enabled at this point and stop Telescope recording if present 88 | protected function ensureClockworkIsEnabled() 89 | { 90 | if (class_exists(Telescope::class)) Telescope::stopRecording(); 91 | 92 | if (! $this->clockworkSupport->isEnabled()) abort(404); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /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/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/Monolog/Monolog3/ClockworkHandler.php: -------------------------------------------------------------------------------- 1 | clockworkLog = $clockworkLog; 18 | } 19 | 20 | protected function write(LogRecord $record): void 21 | { 22 | $this->clockworkLog->log($record->level->getName(), $record['message']); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /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/Support/Slim/Legacy/ClockworkMiddleware.php: -------------------------------------------------------------------------------- 1 | clockwork = $storagePathOrClockwork instanceof Clockwork 20 | ? $storagePathOrClockwork : $this->createDefaultClockwork($storagePathOrClockwork); 21 | $this->startTime = $startTime ?: microtime(true); 22 | } 23 | 24 | public function __invoke(Request $request, Response $response, callable $next) 25 | { 26 | return $this->process($request, $response, $next); 27 | } 28 | 29 | public function process(Request $request, Response $response, callable $next) 30 | { 31 | $authUri = '#/__clockwork/auth#'; 32 | if (preg_match($authUri, $request->getUri()->getPath(), $matches)) { 33 | return $this->authenticate($response, $request); 34 | } 35 | 36 | $clockworkDataUri = '#/__clockwork(?:/(?([0-9-]+|latest)))?(?:/(?(?:previous|next)))?(?:/(?\d+))?#'; 37 | if (preg_match($clockworkDataUri, $request->getUri()->getPath(), $matches)) { 38 | $matches = array_merge([ 'id' => null, 'direction' => null, 'count' => null ], $matches); 39 | return $this->retrieveRequest($response, $request, $matches['id'], $matches['direction'], $matches['count']); 40 | } 41 | 42 | $response = $next($request, $response); 43 | 44 | return $this->logRequest($request, $response); 45 | } 46 | 47 | protected function authenticate(Response $response, Request $request) 48 | { 49 | $token = $this->clockwork->authenticator()->attempt($request->getParsedBody()); 50 | 51 | return $response->withJson([ 'token' => $token ])->withStatus($token ? 200 : 403); 52 | } 53 | 54 | protected function retrieveRequest(Response $response, Request $request, $id, $direction, $count) 55 | { 56 | $authenticator = $this->clockwork->authenticator(); 57 | $storage = $this->clockwork->storage(); 58 | 59 | $authenticated = $authenticator->check(current($request->getHeader('X-Clockwork-Auth'))); 60 | 61 | if ($authenticated !== true) { 62 | return $response 63 | ->withJson([ 'message' => $authenticated, 'requires' => $authenticator->requires() ]) 64 | ->withStatus(403); 65 | } 66 | 67 | if ($direction == 'previous') { 68 | $data = $storage->previous($id, $count); 69 | } elseif ($direction == 'next') { 70 | $data = $storage->next($id, $count); 71 | } elseif ($id == 'latest') { 72 | $data = $storage->latest(); 73 | } else { 74 | $data = $storage->find($id); 75 | } 76 | 77 | return $response 78 | ->withHeader('Content-Type', 'application/json') 79 | ->withJson($data); 80 | } 81 | 82 | protected function logRequest(Request $request, Response $response) 83 | { 84 | $this->clockwork->timeline()->finalize($this->startTime); 85 | $this->clockwork->addDataSource(new PsrMessageDataSource($request, $response)); 86 | 87 | $this->clockwork->resolveRequest(); 88 | $this->clockwork->storeRequest(); 89 | 90 | $clockworkRequest = $this->clockwork->request(); 91 | 92 | $response = $response 93 | ->withHeader('X-Clockwork-Id', $clockworkRequest->id) 94 | ->withHeader('X-Clockwork-Version', Clockwork::VERSION); 95 | 96 | if ($basePath = $request->getUri()->getBasePath()) { 97 | $response = $response->withHeader('X-Clockwork-Path', $basePath); 98 | } 99 | 100 | return $response->withHeader('Server-Timing', ServerTiming::fromRequest($clockworkRequest)->value()); 101 | } 102 | 103 | protected function createDefaultClockwork($storagePath) 104 | { 105 | $clockwork = new Clockwork(); 106 | 107 | $clockwork->storage(new FileStorage($storagePath)); 108 | $clockwork->authenticator(new NullAuthenticator); 109 | 110 | return $clockwork; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /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 $this->logLevels[$level] ?? $level; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Clockwork/Support/Slim/Old/ClockworkMiddleware.php: -------------------------------------------------------------------------------- 1 | storagePathOrClockwork = $storagePathOrClockwork; 18 | } 19 | 20 | public function call() 21 | { 22 | $this->app->container->singleton('clockwork', function () { 23 | if ($this->storagePathOrClockwork instanceof Clockwork) { 24 | return $this->storagePathOrClockwork; 25 | } 26 | 27 | $clockwork = new Clockwork(); 28 | 29 | $clockwork->addDataSource(new PhpDataSource()) 30 | ->addDataSource(new SlimDataSource($this->app)) 31 | ->storage(new FileStorage($this->storagePathOrClockwork)); 32 | 33 | return $clockwork; 34 | }); 35 | 36 | $originalLogWriter = $this->app->getLog()->getWriter(); 37 | $clockworkLogWriter = new ClockworkLogWriter($this->app->clockwork, $originalLogWriter); 38 | 39 | $this->app->getLog()->setWriter($clockworkLogWriter); 40 | 41 | $clockworkDataUri = '#/__clockwork(?:/(?([0-9-]+|latest)))?(?:/(?(?:previous|next)))?(?:/(?\d+))?#'; 42 | if ($this->app->config('debug') && preg_match($clockworkDataUri, $this->app->request()->getPathInfo(), $matches)) { 43 | $matches = array_merge([ 'direction' => null, 'count' => null ], $matches); 44 | return $this->retrieveRequest($matches['id'], $matches['direction'], $matches['count']); 45 | } 46 | 47 | try { 48 | $this->next->call(); 49 | $this->logRequest(); 50 | } catch (Exception $e) { 51 | $this->logRequest(); 52 | throw $e; 53 | } 54 | } 55 | 56 | public function retrieveRequest($id = null, $direction = null, $count = null) 57 | { 58 | $storage = $this->app->clockwork->storage(); 59 | 60 | if ($direction == 'previous') { 61 | $data = $storage->previous($id, $count); 62 | } elseif ($direction == 'next') { 63 | $data = $storage->next($id, $count); 64 | } elseif ($id == 'latest') { 65 | $data = $storage->latest(); 66 | } else { 67 | $data = $storage->find($id); 68 | } 69 | 70 | echo json_encode($data, \JSON_PARTIAL_OUTPUT_ON_ERROR); 71 | } 72 | 73 | protected function logRequest() 74 | { 75 | $this->app->clockwork->resolveRequest(); 76 | $this->app->clockwork->storeRequest(); 77 | 78 | if ($this->app->config('debug')) { 79 | $this->app->response()->header('X-Clockwork-Id', $this->app->clockwork->request()->id); 80 | $this->app->response()->header('X-Clockwork-Version', Clockwork::VERSION); 81 | 82 | $env = $this->app->environment(); 83 | if ($env['SCRIPT_NAME']) { 84 | $this->app->response()->header('X-Clockwork-Path', $env['SCRIPT_NAME'] . '/__clockwork/'); 85 | } 86 | 87 | $request = $this->app->clockwork->request(); 88 | $this->app->response()->header('Server-Timing', ServerTiming::fromRequest($request)->value()); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /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/Symfony/ClockworkBundle.php: -------------------------------------------------------------------------------- 1 | debug = $debug; 14 | } 15 | 16 | public function getConfigTreeBuilder(): TreeBuilder 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/Symfony/ClockworkController.php: -------------------------------------------------------------------------------- 1 | clockwork = $clockwork; 16 | $this->support = $support; 17 | } 18 | 19 | public function authenticate(Request $request) 20 | { 21 | $this->ensureClockworkIsEnabled(); 22 | 23 | $token = $this->clockwork->authenticator()->attempt($request->request->all()); 24 | 25 | return new JsonResponse([ 'token' => $token ], $token ? 200 : 403); 26 | } 27 | 28 | public function getData(Request $request, $id = null, $direction = null, $count = null) 29 | { 30 | $this->ensureClockworkIsEnabled(); 31 | 32 | return $this->support->getData($request, $id, $direction, $count); 33 | } 34 | 35 | public function webIndex(Request $request) 36 | { 37 | $this->ensureClockworkIsEnabled(); 38 | $this->ensureClockworkWebIsEnabled(); 39 | 40 | return $this->support->getWebAsset('index.html'); 41 | } 42 | 43 | public function webAsset($path) 44 | { 45 | $this->ensureClockworkIsEnabled(); 46 | $this->ensureClockworkWebIsEnabled(); 47 | 48 | return $this->support->getWebAsset($path); 49 | } 50 | 51 | public function webRedirect(Request $request) 52 | { 53 | $this->ensureClockworkIsEnabled(); 54 | $this->ensureClockworkWebIsEnabled(); 55 | 56 | $path = $this->support->webPaths()[0]; 57 | 58 | return $this->redirectToRoute("clockwork.webIndex.{$path}"); 59 | } 60 | 61 | protected function ensureClockworkIsEnabled() 62 | { 63 | if (! $this->support->isEnabled()) throw $this->createNotFoundException(); 64 | } 65 | 66 | protected function ensureClockworkWebIsEnabled() 67 | { 68 | if (! $this->support->isWebEnabled()) throw $this->createNotFoundException(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /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/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/Support/Symfony/ClockworkListener.php: -------------------------------------------------------------------------------- 1 | clockwork = $clockwork; 18 | $this->profiler = $profiler; 19 | } 20 | 21 | public function onKernelRequest(KernelEvent $event) 22 | { 23 | $disabledPaths = array_merge([ '__clockwork' ], $this->clockwork->webPaths()); 24 | 25 | foreach ($disabledPaths as $path) { 26 | if (strpos($event->getRequest()->getPathInfo(), "/{$path}") !== false) $this->profiler->disable(); 27 | } 28 | } 29 | 30 | public function onKernelResponse(KernelEvent $event) 31 | { 32 | if (! $this->clockwork->isEnabled()) return; 33 | 34 | $response = $event->getResponse(); 35 | 36 | if (! $response->headers->has('X-Debug-Token')) return; 37 | 38 | $response->headers->set('X-Clockwork-Id', $response->headers->get('X-Debug-Token')); 39 | $response->headers->set('X-Clockwork-Version', Clockwork::VERSION); 40 | } 41 | 42 | public static function getSubscribedEvents() 43 | { 44 | return [ 45 | KernelEvents::REQUEST => [ 'onKernelRequest', 512 ], 46 | KernelEvents::RESPONSE => [ 'onKernelResponse', -128 ] 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Clockwork/Support/Symfony/ClockworkLoader.php: -------------------------------------------------------------------------------- 1 | support = $support; 13 | } 14 | 15 | public function load($resource, $type = null): mixed 16 | { 17 | $routes = new RouteCollection(); 18 | 19 | $routes->add('clockwork', new Route('/__clockwork/{id}/{direction}/{count}', [ 20 | '_controller' => [ ClockworkController::class, 'getData' ], 21 | 'direction' => null, 22 | 'count' => null 23 | ], [ 'id' => '(?!(app|auth))([a-z0-9-]+|latest)', 'direction' => '(next|previous)', 'count' => '\d+' ])); 24 | 25 | $routes->add('clockwork.auth', new Route('/__clockwork/auth', [ 26 | '_controller' => [ ClockworkController::class, 'authenticate' ] 27 | ])); 28 | 29 | if (! $this->support->isWebEnabled()) return $routes; 30 | 31 | foreach ($this->support->webPaths() as $path) { 32 | $routes->add("clockwork.webRedirect.{$path}", new Route("{$path}", [ 33 | '_controller' => [ ClockworkController::class, 'webRedirect' ] 34 | ])); 35 | 36 | $routes->add("clockwork.webIndex.{$path}", new Route("{$path}/app", [ 37 | '_controller' => [ ClockworkController::class, 'webIndex' ] 38 | ])); 39 | 40 | $routes->add("clockwork.webAsset.{$path}", new Route("{$path}/{path}", [ 41 | '_controller' => [ ClockworkController::class, 'webAsset' ] 42 | ], [ 'path' => '.+' ])); 43 | } 44 | 45 | return $routes; 46 | } 47 | 48 | public function supports($resource, $type = null): bool 49 | { 50 | return $type == 'clockwork'; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Clockwork/Support/Symfony/ClockworkSupport.php: -------------------------------------------------------------------------------- 1 | container = $container; 19 | $this->config = $config; 20 | } 21 | 22 | public function getConfig($key, $default = null) 23 | { 24 | return isset($this->config[$key]) ? $this->config[$key] : $default; 25 | } 26 | 27 | public function getData(Request $request, $id = null, $direction = null, $count = null) 28 | { 29 | $authenticator = $this->container->get('clockwork')->authenticator(); 30 | $storage = $this->container->get('clockwork')->storage(); 31 | 32 | $authenticated = $authenticator->check($request->headers->get('X-Clockwork-Auth')); 33 | 34 | if ($authenticated !== true) { 35 | return new JsonResponse([ 'message' => $authenticated, 'requires' => $authenticator->requires() ], 403); 36 | } 37 | 38 | if ($direction == 'previous') { 39 | $data = $storage->previous($id, $count, Search::fromRequest($request->query->all())); 40 | } elseif ($direction == 'next') { 41 | $data = $storage->next($id, $count, Search::fromRequest($request->query->all())); 42 | } elseif ($id == 'latest') { 43 | $data = $storage->latest(Search::fromRequest($request->query->all())); 44 | } else { 45 | $data = $storage->find($id); 46 | } 47 | 48 | $data = is_array($data) 49 | ? array_map(function ($request) { return $request->toArray(); }, $data) 50 | : $data->toArray(); 51 | 52 | return new JsonResponse($data); 53 | } 54 | 55 | public function getWebAsset($path) 56 | { 57 | $web = new Web; 58 | 59 | if ($asset = $web->asset($path)) { 60 | return new BinaryFileResponse($asset['path'], 200, [ 'Content-Type' => $asset['mime'] ]); 61 | } else { 62 | throw new NotFoundHttpException; 63 | } 64 | } 65 | 66 | public function makeAuthenticator() 67 | { 68 | $authenticator = $this->getConfig('authentication'); 69 | 70 | if (is_string($authenticator)) { 71 | return $this->container->get($authenticator); 72 | } elseif ($authenticator) { 73 | return new SimpleAuthenticator($this->getConfig('authentication_password')); 74 | } else { 75 | return new NullAuthenticator; 76 | } 77 | } 78 | 79 | public function isEnabled() 80 | { 81 | return $this->getConfig('enable', false); 82 | } 83 | 84 | public function isWebEnabled() 85 | { 86 | return $this->getConfig('web', true); 87 | } 88 | 89 | public function webPaths() 90 | { 91 | $path = $this->getConfig('web', true); 92 | 93 | if (is_string($path)) return [ trim($path, '/') ]; 94 | 95 | return [ 'clockwork', '__clockwork' ]; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /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/Resources/config/routing/clockwork.php: -------------------------------------------------------------------------------- 1 | import('.', 'clockwork'); 7 | }; 8 | -------------------------------------------------------------------------------- /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' => $data[3]['wt'] ?? null, 41 | 'end' => $data[4]['wt'] ?? null, 42 | 'data' => [ 43 | 'data' => [], 44 | 'memoryUsage' => $data[4]['mu'] ?? null, 45 | 'parent' => $parent 46 | ] 47 | ]); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Clockwork/Support/Vanilla/ClockworkMiddleware.php: -------------------------------------------------------------------------------- 1 | clockwork = $clockwork; 23 | } 24 | 25 | // Returns a new middleware instance with a default singleton Clockwork instance, takes an additional configuration as argument 26 | public static function init($config = []) 27 | { 28 | return new static(Clockwork::init($config)); 29 | } 30 | 31 | // Sets a PSR-17 compatible response factory. When using the middleware with routing enabled, response factory must be manually set 32 | // or the php-http/discovery has to be intalled for zero-configuration use 33 | public function withResponseFactory(ResponseFactoryInterface $responseFactory) 34 | { 35 | $this->responseFactory = $responseFactory; 36 | return $this; 37 | } 38 | 39 | // Disables routing handling in the middleware. When disabled additional manual configuration of the application router is required. 40 | public function withoutRouting() 41 | { 42 | $this->handleRouting = false; 43 | return $this; 44 | } 45 | 46 | // Process the middleware 47 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) :ResponseInterface 48 | { 49 | $request = $request->withAttribute('clockwork', $this->clockwork); 50 | 51 | $this->clockwork->event('Controller')->begin(); 52 | 53 | if ($response = $this->handleApiRequest($request)) return $response; 54 | if ($response = $this->handleWebRequest($request)) return $response; 55 | 56 | $response = $handler->handle($request); 57 | 58 | return $this->clockwork->usePsrMessage($request, $response)->requestProcessed(); 59 | } 60 | 61 | // Handle a Clockwork REST api request if routing is enabled 62 | protected function handleApiRequest(ServerRequestInterface $request) 63 | { 64 | $path = $this->clockwork->getConfig()['api']; 65 | 66 | if (! $this->handleRouting) return; 67 | if (! preg_match("#^{$path}.*#", $request->getUri()->getPath())) return; 68 | 69 | return $this->clockwork->usePsrMessage($request, $this->prepareResponse())->handleMetadata(); 70 | } 71 | 72 | // Handle a Clockwork Web interface request if routing is enabled 73 | protected function handleWebRequest(ServerRequestInterface $request) 74 | { 75 | $path = is_string($this->clockwork->getConfig()['web']['enable']) ? $this->clockwork->getConfig()['web']['enable'] : '/clockwork'; 76 | 77 | if (! $this->handleRouting) return; 78 | if (! preg_match("#^{$path}(/.*)?#", $request->getUri()->getPath())) return; 79 | 80 | return $this->clockwork->usePsrMessage($request, $this->prepareResponse())->returnWeb(); 81 | } 82 | 83 | protected function prepareResponse() 84 | { 85 | if (! $this->responseFactory && ! class_exists(Psr17Factory::class)) { 86 | throw new \Exception('The Clockwork vanilla middleware requires a response factory or the php-http/discovery package to be installed.'); 87 | } 88 | 89 | return ($this->responseFactory ?: new Psr17Factory)->createResponse(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Clockwork/Support/Vanilla/helpers.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Clockwork 7 | 8 | 11 | 12 | 13 | 14 | 15 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /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/Web/public/img/appearance-auto-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsgoingd/clockwork/da128d871b805941b0774a1526601b12c2f116da/Clockwork/Web/public/img/appearance-auto-icon.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/appearance-dark-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsgoingd/clockwork/da128d871b805941b0774a1526601b12c2f116da/Clockwork/Web/public/img/appearance-dark-icon.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/appearance-light-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsgoingd/clockwork/da128d871b805941b0774a1526601b12c2f116da/Clockwork/Web/public/img/appearance-light-icon.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsgoingd/clockwork/da128d871b805941b0774a1526601b12c2f116da/Clockwork/Web/public/img/icon-128x128.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/whats-new/5.0/client-metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsgoingd/clockwork/da128d871b805941b0774a1526601b12c2f116da/Clockwork/Web/public/img/whats-new/5.0/client-metrics.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/whats-new/5.0/clockwork-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsgoingd/clockwork/da128d871b805941b0774a1526601b12c2f116da/Clockwork/Web/public/img/whats-new/5.0/clockwork-5.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/whats-new/5.0/models-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsgoingd/clockwork/da128d871b805941b0774a1526601b12c2f116da/Clockwork/Web/public/img/whats-new/5.0/models-tab.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/whats-new/5.0/notifications-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsgoingd/clockwork/da128d871b805941b0774a1526601b12c2f116da/Clockwork/Web/public/img/whats-new/5.0/notifications-tab.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/whats-new/5.0/timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsgoingd/clockwork/da128d871b805941b0774a1526601b12c2f116da/Clockwork/Web/public/img/whats-new/5.0/timeline.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/whats-new/5.0/toolbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsgoingd/clockwork/da128d871b805941b0774a1526601b12c2f116da/Clockwork/Web/public/img/whats-new/5.0/toolbar.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/whats-new/5.1/database-queries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsgoingd/clockwork/da128d871b805941b0774a1526601b12c2f116da/Clockwork/Web/public/img/whats-new/5.1/database-queries.png -------------------------------------------------------------------------------- /Clockwork/Web/public/img/whats-new/5.3/http-requests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsgoingd/clockwork/da128d871b805941b0774a1526601b12c2f116da/Clockwork/Web/public/img/whats-new/5.3/http-requests.png -------------------------------------------------------------------------------- /Clockwork/Web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Clockwork 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /Clockwork/Web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Clockwork", 3 | "short_name": "Clockwork", 4 | "icons": [ 5 | { 6 | "src": "img/icon-128x128.png", 7 | "sizes": "128x128", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "/", 12 | "display": "standalone", 13 | "background_color": "#fff", 14 | "theme_color": "#2786f3" 15 | } 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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": ">=7.1", 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 | --------------------------------------------------------------------------------