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