value ] public $clientMetrics = []; // Web vitals in the form of [ vital => value ] public $webVitals = []; // Parent request public $parent; // Token to update this request data public $updateToken; // Log instance for the current request protected $currentLog; // Timeline instance for the current request protected $currentTimeline; // Array of property values to override collected values from data sources protected $overrides = []; // Create a new request, if optional data array argument is provided, it will be used to populate the request object, // otherwise an empty request with current time, autogenerated ID and update token will be created public function __construct(array $data = []) { $this->id = isset($data['id']) ? $data['id'] : $this->generateRequestId(); $this->time = microtime(true); $this->updateToken = isset($data['updateToken']) ? $data['updateToken'] : $this->generateUpdateToken(); foreach ($data as $key => $val) $this->$key = $val; $this->currentLog = new Log($this->log); $this->currentTimeline = new Timeline\Timeline($this->timelineData); } // Compute the sum of durations of all database queries public function getDatabaseDuration() { return array_reduce($this->databaseQueries, function ($total, $query) { return isset($query['duration']) ? $total + $query['duration'] : $total; }, 0); } // Compute response duration in milliseconds public function getResponseDuration() { return ($this->responseTime - $this->time) * 1000; } // Get all request data as an array public function toArray() { return [ 'id' => $this->id, 'version' => $this->version, 'type' => $this->type, 'time' => $this->time, 'method' => $this->method, 'url' => $this->url, 'uri' => $this->uri, 'headers' => $this->headers, 'controller' => $this->controller, 'getData' => $this->getData, 'postData' => $this->postData, 'requestData' => $this->requestData, 'sessionData' => $this->sessionData, 'authenticatedUser' => $this->authenticatedUser, 'cookies' => $this->cookies, 'responseTime' => $this->responseTime, 'responseStatus' => $this->responseStatus, 'responseDuration' => $this->responseDuration ?: $this->getResponseDuration(), 'memoryUsage' => $this->memoryUsage, 'middleware' => $this->middleware, 'databaseQueries' => $this->databaseQueries, 'databaseQueriesCount' => $this->databaseQueriesCount, 'databaseSlowQueries' => $this->databaseSlowQueries, 'databaseSelects' => $this->databaseSelects, 'databaseInserts' => $this->databaseInserts, 'databaseUpdates' => $this->databaseUpdates, 'databaseDeletes' => $this->databaseDeletes, 'databaseOthers' => $this->databaseOthers, 'databaseDuration' => $this->getDatabaseDuration(), 'cacheQueries' => $this->cacheQueries, 'cacheReads' => $this->cacheReads, 'cacheHits' => $this->cacheHits, 'cacheWrites' => $this->cacheWrites, 'cacheDeletes' => $this->cacheDeletes, 'cacheTime' => $this->cacheTime, 'modelsActions' => $this->modelsActions, 'modelsRetrieved' => $this->modelsRetrieved, 'modelsCreated' => $this->modelsCreated, 'modelsUpdated' => $this->modelsUpdated, 'modelsDeleted' => $this->modelsDeleted, 'redisCommands' => $this->redisCommands, 'queueJobs' => $this->queueJobs, 'timelineData' => $this->timeline()->toArray(), 'log' => $this->log()->toArray(), 'events' => $this->events, 'routes' => $this->routes, 'notifications' => $this->notifications, 'emailsData' => $this->emailsData, 'viewsData' => $this->viewsData, 'userData' => array_map(function ($data) { return $data instanceof UserData ? $data->toArray() : $data; }, $this->userData), 'subrequests' => $this->subrequests, 'xdebug' => $this->xdebug, 'commandName' => $this->commandName, 'commandArguments' => $this->commandArguments, 'commandArgumentsDefaults' => $this->commandArgumentsDefaults, 'commandOptions' => $this->commandOptions, 'commandOptionsDefaults' => $this->commandOptionsDefaults, 'commandExitCode' => $this->commandExitCode, 'commandOutput' => $this->commandOutput, 'jobName' => $this->jobName, 'jobDescription' => $this->jobDescription, 'jobStatus' => $this->jobStatus, 'jobPayload' => $this->jobPayload, 'jobQueue' => $this->jobQueue, 'jobConnection' => $this->jobConnection, 'jobOptions' => $this->jobOptions, 'testName' => $this->testName, 'testStatus' => $this->testStatus, 'testStatusMessage' => $this->testStatusMessage, 'testAsserts' => $this->testAsserts, 'clientMetrics' => $this->clientMetrics, 'webVitals' => $this->webVitals, 'parent' => $this->parent, 'updateToken' => $this->updateToken ]; } // Get all request data as a JSON string public function toJson() { return json_encode($this->toArray(), \JSON_PARTIAL_OUTPUT_ON_ERROR); } // Return request data except specified keys as an array public function except($keys) { return array_filter($this->toArray(), function ($value, $key) use ($keys) { return ! in_array($key, $keys); }, ARRAY_FILTER_USE_BOTH); } // Return only request data with specified keys as an array public function only($keys) { return array_filter($this->toArray(), function ($value, $key) use ($keys) { return in_array($key, $keys); }, ARRAY_FILTER_USE_BOTH); } // Return log instance for the current request public function log() { return $this->currentLog; } // Return timeline instance for the current request public function timeline() { return $this->currentTimeline; } // Add a new overriden property public function override($property, $value) { $this->overrides[$property] = $value; return $this; } // Get or set all overrides at once public function overrides($overrides = null) { if (! $overrides) return $this->overrides; $this->overrides = $overrides; return $this; } // Add database query, takes query, bindings, duration (in ms) and additional data - connection (connection name), // time (when was the query executed), file (caller file name), line (caller line number), trace (serialized trace), // model (associated ORM model) public function addDatabaseQuery($query, $bindings = [], $duration = null, $data = []) { $this->databaseQueries[] = [ 'query' => $query, 'bindings' => (new Serializer)->normalize($bindings), 'duration' => $duration, 'connection' => isset($data['connection']) ? $data['connection'] : null, 'time' => isset($data['time']) ? $data['time'] : microtime(true) - ($duration ?: 0) / 1000, 'file' => isset($data['file']) ? $data['file'] : null, 'line' => isset($data['line']) ? $data['line'] : null, 'trace' => isset($data['trace']) ? $data['trace'] : null, 'model' => isset($data['model']) ? $data['model'] : null, 'tags' => array_merge( isset($data['tags']) ? $data['tags'] : [], isset($data['slow']) ? [ 'slow' ] : [] ) ]; } // Add model action, takes model, action and additional data - key, attributes, changes, time (when was the action // executed), query, duration (in ms), connection (connection name), trace (serialized trace), file (caller file // name), line (caller line number), tags public function addModelAction($model, $action, $data = []) { $this->modelActions[] = [ 'model' => $model, 'key' => isset($data['key']) ? $data['key'] : null, 'action' => $action, 'attributes' => isset($data['attributes']) ? $data['attributes'] : [], 'changes' => isset($data['changes']) ? $data['changes'] : [], 'duration' => $duration = isset($data['duration']) ? $data['duration'] : null, 'time' => isset($data['time']) ? $data['time'] : microtime(true) - ($duration ?: 0) / 1000, 'query' => isset($data['query']) ? $data['query'] : null, 'connection' => isset($data['connection']) ? $data['connection'] : null, 'trace' => isset($data['trace']) ? $data['trace'] : null, 'file' => isset($data['file']) ? $data['file'] : null, 'line' => isset($data['line']) ? $data['line'] : null, 'tags' => isset($data['tags']) ? $data['tags'] : [] ]; } // Add cache query, takes type, key, value, duration (in ms) and additional data - connection (connection name), // time (when was the query executed), file (caller file name), line (caller line number), trace (serialized trace), // expiration public function addCacheQuery($type, $key, $value = null, $duration = null, $data = []) { $this->cacheQueries[] = [ 'type' => $type, 'key' => $key, 'value' => (new Serializer)->normalize($value), 'duration' => $duration, 'connection' => isset($data['connection']) ? $data['connection'] : null, 'time' => isset($data['time']) ? $data['time'] : microtime(true) - ($duration ?: 0) / 1000, 'file' => isset($data['file']) ? $data['file'] : null, 'line' => isset($data['line']) ? $data['line'] : null, 'trace' => isset($data['trace']) ? $data['trace'] : null, 'expiration' => isset($data['expiration']) ? $data['expiration'] : null ]; } // Add event, takes event name, data, time and additional data - listeners, duration (in ms), file (caller file // name), line (caller line number), trace (serialized trace) public function addEvent($event, $eventData = null, $time = null, $data = []) { $this->events[] = [ 'event' => $event, 'data' => (new Serializer)->normalize($eventData), 'duration' => $duration = isset($data['duration']) ? $data['duration'] : null, 'time' => $time ? $time : microtime(true) - ($duration ?: 0) / 1000, 'listeners' => isset($data['listeners']) ? $data['listeners'] : null, 'file' => isset($data['file']) ? $data['file'] : null, 'line' => isset($data['line']) ? $data['line'] : null, 'trace' => isset($data['trace']) ? $data['trace'] : null ]; } // Add route, takes method, uri, action and additional data - name, middleware, before (before filters), after // (after filters) public function addRoute($method, $uri, $action, $data = []) { $this->routes[] = [ 'method' => $method, 'uri' => $uri, 'action' => $action, 'name' => isset($data['name']) ? $data['name'] : null, 'middleware' => isset($data['middleware']) ? $data['middleware'] : null, 'before' => isset($data['before']) ? $data['before'] : null, 'after' => isset($data['after']) ? $data['after'] : null ]; } // Add sent notifucation, takes subject, recipient, sender, and additional data - time, duration, type, content, data public function addNotification($subject, $to, $from = null, $data = []) { $this->notifications[] = [ 'subject' => $subject, 'from' => $from, 'to' => $to, 'content' => isset($data['content']) ? $data['content'] : null, 'type' => isset($data['type']) ? $data['type'] : null, 'data' => isset($data['data']) ? $data['data'] : [], 'duration' => $duration = isset($data['duration']) ? $data['duration'] : null, 'time' => isset($data['time']) ? $data['time'] : microtime(true) - ($duration ?: 0) / 1000, 'trace' => isset($data['trace']) ? $data['trace'] : null, 'file' => isset($data['file']) ? $data['file'] : null, 'line' => isset($data['line']) ? $data['line'] : null ]; } // Add sent email, takes subject, recipient address, sender address, array of headers, and additional data - time // (when was the email sent), duration (sending time in ms) public function addEmail($subject, $to, $from = null, $headers = [], $data = []) { $this->emailsData[] = [ 'start' => isset($data['time']) ? $data['time'] : null, 'end' => isset($data['time'], $data['duration']) ? $data['time'] + $data['duration'] / 1000 : null, 'duration' => isset($data['duration']) ? $data['duration'] : null, 'description' => 'Sending an email message', 'data' => [ 'subject' => $subject, 'to' => $to, 'from' => $from, 'headers' => (new Serializer)->normalize($headers) ] ]; } // Add view, takes view name, view data and additional data - time (when was the view rendered), duration (sending // time in ms) public function addView($name, $viewData = [], $data = []) { $this->viewsData[] = [ 'start' => isset($data['time']) ? $data['time'] : null, 'end' => isset($data['time'], $data['duration']) ? $data['time'] + $data['duration'] / 1000 : null, 'duration' => isset($data['duration']) ? $data['duration'] : null, 'description' => 'Rendering a view', 'data' => [ 'name' => $name, 'data' => (new Serializer)->normalize($viewData) ] ]; } // Add executed subrequest, takes the requested url, subrequest Clockwork ID and additional data - path if non-default public function addSubrequest($url, $id, $data = []) { $this->subrequests[] = [ 'url' => $url, 'id' => $id, 'path' => isset($data['path']) ? $data['path'] : null ]; } // Set the authenticated user, takes a username, an id and additional data - email and name public function setAuthenticatedUser($username, $id = null, $data = []) { $this->authenticatedUser = [ 'id' => $id, 'username' => $username, 'email' => isset($data['email']) ? $data['email'] : null, 'name' => isset($data['name']) ? $data['name'] : null ]; } // Set parent request, takes the request id and additional options - url and path if non-default public function setParent($id, $data = []) { $this->parent = [ 'id' => $id, 'url' => isset($data['url']) ? $data['url'] : null, 'path' => isset($data['path']) ? $data['path'] : null ]; } // Add custom user data public function userData($key = null) { if ($key && isset($this->userData[$key])) { return $this->userData[$key]; } $userData = (new UserData)->title($key); return $key ? $this->userData[$key] = $userData : $this->userData[] = $userData; } // Add a ran test assert, takes the assert name, arguments, whether it passed and trace as arguments public function addTestAssert($name, $arguments = null, $passed = true, $trace = null) { $this->testAsserts[] = [ 'name' => $name, 'arguments' => (new Serializer)->normalize($arguments), 'trace' => $trace, 'passed' => $passed ]; } // Generate unique request ID in the form of - protected function generateRequestId() { return str_replace('.', '-', sprintf('%.4F', microtime(true))) . '-' . mt_rand(); } // Generate a random update token protected function generateUpdateToken() { $length = 8; $bytes = function_exists('random_bytes') ? random_bytes($length) : openssl_random_pseudo_bytes($length); return substr(bin2hex($bytes), 0, $length); } }