clock-work
This commit is contained in:
331
vendor/itsgoingd/clockwork/Clockwork/Storage/FileStorage.php
vendored
Normal file
331
vendor/itsgoingd/clockwork/Clockwork/Storage/FileStorage.php
vendored
Normal file
@@ -0,0 +1,331 @@
|
||||
<?php namespace Clockwork\Storage;
|
||||
|
||||
use Clockwork\Request\Request;
|
||||
use Clockwork\Storage\Storage;
|
||||
|
||||
// File based storage for requests
|
||||
class FileStorage extends Storage
|
||||
{
|
||||
// Path where files are stored
|
||||
protected $path;
|
||||
|
||||
// Metadata expiration time in minutes
|
||||
protected $expiration;
|
||||
|
||||
// Compress the files using gzip
|
||||
protected $compress;
|
||||
|
||||
// Metadata cleanup chance
|
||||
protected $cleanupChance = 100;
|
||||
|
||||
// Index file handle
|
||||
protected $indexHandle;
|
||||
|
||||
// Return new storage, takes path where to store files as argument, throws exception if path is not writable
|
||||
public function __construct($path, $dirPermissions = 0700, $expiration = null, $compress = false)
|
||||
{
|
||||
if (! file_exists($path)) {
|
||||
// directory doesn't exist, try to create one
|
||||
if (! @mkdir($path, $dirPermissions, true)) {
|
||||
throw new \Exception("Directory \"{$path}\" does not exist.");
|
||||
}
|
||||
|
||||
// create default .gitignore, to ignore stored json files
|
||||
file_put_contents("{$path}/.gitignore", "*.json\n*.json.gz\nindex\n");
|
||||
} elseif (! is_writable($path)) {
|
||||
throw new \Exception("Path \"{$path}\" is not writable.");
|
||||
}
|
||||
|
||||
if (! file_exists($indexFile = "{$path}/index")) {
|
||||
file_put_contents($indexFile, '');
|
||||
} elseif (! is_writable($indexFile)) {
|
||||
throw new \Exception("Path \"{$indexFile}\" is not writable.");
|
||||
}
|
||||
|
||||
$this->path = $path;
|
||||
$this->expiration = $expiration === null ? 60 * 24 * 7 : $expiration;
|
||||
$this->compress = $compress;
|
||||
}
|
||||
|
||||
// Returns all requests
|
||||
public function all(Search $search = null)
|
||||
{
|
||||
return $this->loadRequests($this->searchIndexForward($search));
|
||||
}
|
||||
|
||||
// Return a single request by id
|
||||
public function find($id)
|
||||
{
|
||||
return $this->loadRequest($id);
|
||||
}
|
||||
|
||||
// Return the latest request
|
||||
public function latest(Search $search = null)
|
||||
{
|
||||
$requests = $this->loadRequests($this->searchIndexBackward($search, null, 1));
|
||||
return reset($requests);
|
||||
}
|
||||
|
||||
// Return requests received before specified id, optionally limited to specified count
|
||||
public function previous($id, $count = null, Search $search = null)
|
||||
{
|
||||
return $this->loadRequests($this->searchIndexBackward($search, $id, $count));
|
||||
}
|
||||
|
||||
// Return requests received after specified id, optionally limited to specified count
|
||||
public function next($id, $count = null, Search $search = null)
|
||||
{
|
||||
return $this->loadRequests($this->searchIndexForward($search, $id, $count));
|
||||
}
|
||||
|
||||
// Store request, requests are stored in JSON representation in files named <request id>.json in storage path
|
||||
public function store(Request $request, $skipIndex = false)
|
||||
{
|
||||
$path = "{$this->path}/{$request->id}.json";
|
||||
$data = @json_encode($request->toArray(), \JSON_PARTIAL_OUTPUT_ON_ERROR);
|
||||
|
||||
$this->compress
|
||||
? file_put_contents("{$path}.gz", gzcompress($data))
|
||||
: file_put_contents($path, $data);
|
||||
|
||||
if (! $skipIndex) $this->updateIndex($request);
|
||||
|
||||
$this->cleanup();
|
||||
}
|
||||
|
||||
// Update existing request
|
||||
public function update(Request $request)
|
||||
{
|
||||
return $this->store($request, true);
|
||||
}
|
||||
|
||||
// Cleanup old requests
|
||||
public function cleanup($force = false)
|
||||
{
|
||||
if ($this->expiration === false || (! $force && rand(1, $this->cleanupChance) != 1)) return;
|
||||
|
||||
$this->openIndex('start', true, true); // reopen index with lock
|
||||
|
||||
$expirationTime = time() - ($this->expiration * 60);
|
||||
|
||||
$old = $this->searchIndexForward(
|
||||
new Search([ 'received' => [ '<' . date('c', $expirationTime) ] ], [ 'stopOnFirstMismatch' => true ])
|
||||
);
|
||||
|
||||
if (! count($old)) return $this->closeIndex(true);
|
||||
|
||||
$this->readPreviousIndex();
|
||||
$this->trimIndex();
|
||||
$this->closeIndex(true); // explicitly close index to unlock asap
|
||||
|
||||
foreach ($old as $id) {
|
||||
$path = "{$this->path}/{$id}.json";
|
||||
@unlink($this->compress ? "{$path}.gz" : $path);
|
||||
}
|
||||
}
|
||||
|
||||
// Load a single request by id from filesystem
|
||||
protected function loadRequest($id)
|
||||
{
|
||||
$path = "{$this->path}/{$id}.json";
|
||||
|
||||
if (! is_readable($this->compress ? "{$path}.gz" : $path)) return;
|
||||
|
||||
$data = file_get_contents($this->compress ? "{$path}.gz" : $path);
|
||||
|
||||
return new Request(json_decode($this->compress ? gzuncompress($data) : $data, true));
|
||||
}
|
||||
|
||||
// Load multiple requests by ids from filesystem
|
||||
protected function loadRequests($ids)
|
||||
{
|
||||
return array_filter(array_map(function ($id) { return $this->loadRequest($id); }, $ids));
|
||||
}
|
||||
|
||||
// Search index backward from specified ID or last record, with optional results count limit
|
||||
protected function searchIndexBackward(Search $search = null, $id = null, $count = null)
|
||||
{
|
||||
return $this->searchIndex('previous', $search, $id, $count);
|
||||
}
|
||||
|
||||
// Search index forward from specified ID or last record, with optional results count limit
|
||||
protected function searchIndexForward(Search $search = null, $id = null, $count = null)
|
||||
{
|
||||
return $this->searchIndex('next', $search, $id, $count);
|
||||
}
|
||||
|
||||
// Search index in specified direction from specified ID or last record, with optional results count limit
|
||||
protected function searchIndex($direction, Search $search = null, $id = null, $count = null)
|
||||
{
|
||||
$this->openIndex($direction == 'previous' ? 'end' : 'start', false, true);
|
||||
|
||||
if ($id) {
|
||||
while ($request = $this->readIndex($direction)) { if ($request->id == $id) break; }
|
||||
}
|
||||
|
||||
$found = [];
|
||||
|
||||
while ($request = $this->readIndex($direction)) {
|
||||
if (! $search || $search->matches($request)) {
|
||||
$found[] = $request->id;
|
||||
} elseif ($search->stopOnFirstMismatch) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($count && count($found) == $count) break;
|
||||
}
|
||||
|
||||
return $direction == 'next' ? $found : array_reverse($found);
|
||||
}
|
||||
|
||||
// Open index file, optionally lock or move file pointer to the end, existing handle will be returned by default
|
||||
protected function openIndex($position = 'start', $lock = false, $force = false)
|
||||
{
|
||||
if ($this->indexHandle) {
|
||||
if (! $force) return;
|
||||
$this->closeIndex();
|
||||
}
|
||||
|
||||
$this->indexHandle = fopen("{$this->path}/index", 'r');
|
||||
|
||||
if ($lock) flock($this->indexHandle, LOCK_EX);
|
||||
if ($position == 'end') fseek($this->indexHandle, 0, SEEK_END);
|
||||
}
|
||||
|
||||
// Close index file, optionally unlock
|
||||
protected function closeIndex($lock = false)
|
||||
{
|
||||
if ($lock) flock($this->indexHandle, LOCK_UN);
|
||||
fclose($this->indexHandle);
|
||||
|
||||
$this->indexHandle = null;
|
||||
}
|
||||
|
||||
// Read a line from index in the specified direction (next or previous)
|
||||
protected function readIndex($direction)
|
||||
{
|
||||
return $direction == 'next' ? $this->readNextIndex() : $this->readPreviousIndex();
|
||||
}
|
||||
|
||||
// Read previous line from index
|
||||
protected function readPreviousIndex()
|
||||
{
|
||||
$position = ftell($this->indexHandle) - 1;
|
||||
|
||||
if ($position <= 0) return;
|
||||
|
||||
$line = '';
|
||||
|
||||
// reads 1024B chunks of the file backwards from the current position, until a newline is found or we reach the top
|
||||
while ($position > 0) {
|
||||
// find next position to read from, make sure we don't read beyond file boundary
|
||||
$position -= $chunkSize = min($position, 1024);
|
||||
|
||||
// read the chunk from the position
|
||||
fseek($this->indexHandle, $position);
|
||||
$chunk = fread($this->indexHandle, $chunkSize);
|
||||
|
||||
// if a newline is found, append only the part after the last newline, otherwise we can append the whole chunk
|
||||
$line = ($newline = strrpos($chunk, "\n")) === false
|
||||
? $chunk . $line : substr($chunk, $newline + 1) . $line;
|
||||
|
||||
// if a newline was found, fix the position so we read from that newline next time
|
||||
if ($newline !== false) $position += $newline + 1;
|
||||
|
||||
// move file pointer to the correct position (revert fread, apply newline fix)
|
||||
fseek($this->indexHandle, $position);
|
||||
|
||||
// if we reached a newline and put together a non-empty line we are done
|
||||
if ($newline !== false) break;
|
||||
}
|
||||
|
||||
return $this->makeRequestFromIndex(str_getcsv($line));
|
||||
}
|
||||
|
||||
// Read next line from index
|
||||
protected function readNextIndex()
|
||||
{
|
||||
if (feof($this->indexHandle)) return;
|
||||
|
||||
// File pointer is always at the start of the line, call extra fgets to skip current line
|
||||
fgets($this->indexHandle);
|
||||
$line = fgets($this->indexHandle);
|
||||
|
||||
// Check if we read an empty line or reached the end of file
|
||||
if ($line === false) return;
|
||||
|
||||
// Reset the file pointer to the start of the read line
|
||||
fseek($this->indexHandle, ftell($this->indexHandle) - strlen($line));
|
||||
|
||||
return $this->makeRequestFromIndex(str_getcsv($line));
|
||||
}
|
||||
|
||||
// Trim index file from beginning to current position (including)
|
||||
protected function trimIndex()
|
||||
{
|
||||
// File pointer is always at the start of the line, call extra fgets to skip current line
|
||||
fgets($this->indexHandle);
|
||||
|
||||
// Read the rest of the index file
|
||||
$trimmedLength = filesize("{$this->path}/index") - ftell($this->indexHandle);
|
||||
$trimmed = $trimmedLength > 0 ? fread($this->indexHandle, $trimmedLength) : '';
|
||||
|
||||
// Rewrite the index file with a trimmed version
|
||||
file_put_contents("{$this->path}/index", $trimmed);
|
||||
}
|
||||
|
||||
// Create an incomplete request from index data
|
||||
protected function makeRequestFromIndex($record)
|
||||
{
|
||||
$type = isset($record[7]) ? $record[7] : 'response';
|
||||
|
||||
if ($type == 'command') {
|
||||
$nameField = 'commandName';
|
||||
} elseif ($type == 'queue-job') {
|
||||
$nameField = 'jobName';
|
||||
} elseif ($type == 'test') {
|
||||
$nameField = 'testName';
|
||||
} else {
|
||||
$nameField = 'uri';
|
||||
}
|
||||
|
||||
return new Request(array_combine(
|
||||
[ 'id', 'time', 'method', $nameField, 'controller', 'responseStatus', 'responseDuration', 'type' ],
|
||||
array_slice($record, 0, 8) + [ null, null, null, null, null, null, null, 'response' ]
|
||||
));
|
||||
}
|
||||
|
||||
// Update index with a new request
|
||||
protected function updateIndex(Request $request)
|
||||
{
|
||||
$handle = fopen("{$this->path}/index", 'a');
|
||||
|
||||
if (! $handle) return;
|
||||
|
||||
if (! flock($handle, LOCK_EX)) return fclose($handle);
|
||||
|
||||
if ($request->type == 'command') {
|
||||
$nameField = 'commandName';
|
||||
} elseif ($request->type == 'queue-job') {
|
||||
$nameField = 'jobName';
|
||||
} elseif ($request->type == 'test') {
|
||||
$nameField = 'testName';
|
||||
} else {
|
||||
$nameField = 'uri';
|
||||
}
|
||||
|
||||
fputcsv($handle, [
|
||||
$request->id,
|
||||
$request->time,
|
||||
$request->method,
|
||||
$request->$nameField,
|
||||
$request->controller,
|
||||
$request->responseStatus,
|
||||
$request->getResponseDuration(),
|
||||
$request->type
|
||||
]);
|
||||
|
||||
flock($handle, LOCK_UN);
|
||||
fclose($handle);
|
||||
}
|
||||
}
|
||||
166
vendor/itsgoingd/clockwork/Clockwork/Storage/Search.php
vendored
Normal file
166
vendor/itsgoingd/clockwork/Clockwork/Storage/Search.php
vendored
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php namespace Clockwork\Storage;
|
||||
|
||||
use Clockwork\Request\Request;
|
||||
use Clockwork\Request\RequestType;
|
||||
|
||||
// Rules for searching requests
|
||||
class Search
|
||||
{
|
||||
// Search parameters
|
||||
public $uri = [];
|
||||
public $controller = [];
|
||||
public $method = [];
|
||||
public $status = [];
|
||||
public $time = [];
|
||||
public $received = [];
|
||||
public $name = [];
|
||||
public $type = [];
|
||||
|
||||
// Whether to stop search on the first not matching request
|
||||
public $stopOnFirstMismatch = false;
|
||||
|
||||
// Create a new instance, takes search parameters and additional options
|
||||
public function __construct($search = [], $options = [])
|
||||
{
|
||||
foreach ([ 'uri', 'controller', 'method', 'status', 'time', 'received', 'name', 'type' ] as $condition) {
|
||||
$this->$condition = isset($search[$condition]) ? $search[$condition] : [];
|
||||
}
|
||||
|
||||
foreach ([ 'stopOnFirstMismatch' ] as $option) {
|
||||
$this->$option = isset($options[$option]) ? $options[$option] : $this->$condition;
|
||||
}
|
||||
|
||||
$this->method = array_map('strtolower', $this->method);
|
||||
}
|
||||
|
||||
// Create a new instance from request input
|
||||
public static function fromRequest($data = [])
|
||||
{
|
||||
return new static($data);
|
||||
}
|
||||
|
||||
// Check whether the request matches current search parameters
|
||||
public function matches(Request $request)
|
||||
{
|
||||
if ($request->type == RequestType::COMMAND) {
|
||||
return $this->matchesCommand($request);
|
||||
} elseif ($request->type == RequestType::QUEUE_JOB) {
|
||||
return $this->matchesQueueJob($request);
|
||||
} elseif ($request->type == RequestType::TEST) {
|
||||
return $this->matchesTest($request);
|
||||
} else {
|
||||
return $this->matchesRequest($request);
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether a request type request matches
|
||||
protected function matchesRequest(Request $request)
|
||||
{
|
||||
return $this->matchesString($this->type, RequestType::REQUEST)
|
||||
&& $this->matchesString($this->uri, $request->uri)
|
||||
&& $this->matchesString($this->controller, $request->controller)
|
||||
&& $this->matchesExact($this->method, strtolower($request->method))
|
||||
&& $this->matchesNumber($this->status, $request->responseStatus)
|
||||
&& $this->matchesNumber($this->time, $request->responseDuration)
|
||||
&& $this->matchesDate($this->received, $request->time);
|
||||
}
|
||||
|
||||
// Check whether a command type request matches
|
||||
protected function matchesCommand(Request $request)
|
||||
{
|
||||
return $this->matchesString($this->type, RequestType::COMMAND)
|
||||
&& $this->matchesString($this->name, $request->commandName)
|
||||
&& $this->matchesNumber($this->status, $request->commandExitCode)
|
||||
&& $this->matchesNumber($this->time, $request->responseDuration)
|
||||
&& $this->matchesDate($this->received, $request->time);
|
||||
}
|
||||
|
||||
// Check whether a queue-job type request matches
|
||||
protected function matchesQueueJob(Request $request)
|
||||
{
|
||||
return $this->matchesString($this->type, RequestType::QUEUE_JOB)
|
||||
&& $this->matchesString($this->name, $request->jobName)
|
||||
&& $this->matchesString($this->status, $request->jobStatus)
|
||||
&& $this->matchesNumber($this->time, $request->responseDuration)
|
||||
&& $this->matchesDate($this->received, $request->time);
|
||||
}
|
||||
|
||||
// Check whether a test type request matches
|
||||
protected function matchesTest(Request $request)
|
||||
{
|
||||
return $this->matchesString($this->type, RequestType::TEST)
|
||||
&& $this->matchesString($this->name, $request->testName)
|
||||
&& $this->matchesString($this->status, $request->testStatus)
|
||||
&& $this->matchesNumber($this->time, $request->responseDuration)
|
||||
&& $this->matchesDate($this->received, $request->time);
|
||||
}
|
||||
|
||||
// Check if there are no search parameters specified
|
||||
public function isEmpty()
|
||||
{
|
||||
return ! count($this->uri) && ! count($this->controller) && ! count($this->method) && ! count($this->status)
|
||||
&& ! count($this->time) && ! count($this->received) && ! count($this->name) && ! count($this->type);
|
||||
}
|
||||
|
||||
// Check if there are some search parameters specified
|
||||
public function isNotEmpty()
|
||||
{
|
||||
return ! $this->isEmpty();
|
||||
}
|
||||
|
||||
// Check if the value matches date type search parameter
|
||||
protected function matchesDate($inputs, $value)
|
||||
{
|
||||
if (! count($inputs)) return true;
|
||||
|
||||
foreach ($inputs as $input) {
|
||||
if (preg_match('/^<(.+)$/', $input, $match)) {
|
||||
if ($value < strtotime($match[1])) return true;
|
||||
} elseif (preg_match('/^>(.+)$/', $input, $match)) {
|
||||
if ($value > strtotime($match[1])) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the value matches exact type search parameter
|
||||
protected function matchesExact($inputs, $value)
|
||||
{
|
||||
if (! count($inputs)) return true;
|
||||
|
||||
return in_array($value, $inputs);
|
||||
}
|
||||
|
||||
// Check if the value matches number type search parameter
|
||||
protected function matchesNumber($inputs, $value)
|
||||
{
|
||||
if (! count($inputs)) return true;
|
||||
|
||||
foreach ($inputs as $input) {
|
||||
if (preg_match('/^<(\d+(?:\.\d+)?)$/', $input, $match)) {
|
||||
if ($value < $match[1]) return true;
|
||||
} elseif (preg_match('/^>(\d+(?:\.\d+)?)$/', $input, $match)) {
|
||||
if ($value > $match[1]) return true;
|
||||
} elseif (preg_match('/^(\d+(?:\.\d+)?)-(\d+(?:\.\d+)?)$/', $input, $match)) {
|
||||
if ($match[1] < $value && $value < $match[2]) return true;
|
||||
} else {
|
||||
if ($value == $input) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the value matches string type search parameter
|
||||
protected function matchesString($inputs, $value)
|
||||
{
|
||||
if (! count($inputs)) return true;
|
||||
|
||||
foreach ($inputs as $input) {
|
||||
if (strpos($value, $input) !== false) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
164
vendor/itsgoingd/clockwork/Clockwork/Storage/SqlSearch.php
vendored
Normal file
164
vendor/itsgoingd/clockwork/Clockwork/Storage/SqlSearch.php
vendored
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php namespace Clockwork\Storage;
|
||||
|
||||
use Clockwork\Request\Request;
|
||||
|
||||
use PDO;
|
||||
|
||||
// Rules for searching requests using SQL storage, builds the SQL query conditions
|
||||
class SqlSearch extends Search
|
||||
{
|
||||
// Generated SQL query and bindings
|
||||
public $query;
|
||||
public $bindings;
|
||||
|
||||
// Internal representation of the SQL where conditions
|
||||
protected $conditions;
|
||||
|
||||
// PDO instance
|
||||
protected $pdo;
|
||||
|
||||
// Create a new instance, takes search parameters
|
||||
public function __construct($search = [], PDO $pdo = null)
|
||||
{
|
||||
parent::__construct($search);
|
||||
|
||||
$this->pdo = $pdo;
|
||||
|
||||
list($this->conditions, $this->bindings) = $this->resolveConditions();
|
||||
|
||||
$this->buildQuery();
|
||||
}
|
||||
|
||||
// Creates a new instance from a base Search class instance
|
||||
public static function fromBase(Search $search = null, PDO $pdo = null)
|
||||
{
|
||||
return new static((array) $search, $pdo);
|
||||
}
|
||||
|
||||
// Add an additional where condition, takes the SQL condition and array of bindings
|
||||
public function addCondition($condition, $bindings = [])
|
||||
{
|
||||
$this->conditions = array_merge([ $condition ], $this->conditions);
|
||||
$this->bindings = array_merge($bindings, $this->bindings);
|
||||
$this->buildQuery();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Resolve SQL conditions and bindings based on the search parameters
|
||||
protected function resolveConditions()
|
||||
{
|
||||
if ($this->isEmpty()) return [ [], [] ];
|
||||
|
||||
$conditions = array_filter([
|
||||
$this->resolveStringCondition([ 'type' ], $this->type),
|
||||
$this->resolveStringCondition([ 'uri', 'commandName', 'jobName', 'testName' ], array_merge($this->uri, $this->name)),
|
||||
$this->resolveStringCondition([ 'controller' ], $this->controller),
|
||||
$this->resolveExactCondition([ 'method' ], $this->method),
|
||||
$this->resolveNumberCondition([ 'responseStatus', 'commandExitCode', 'jobStatus', 'testStatus' ], $this->status),
|
||||
$this->resolveNumberCondition([ 'responseDuration' ], $this->time),
|
||||
$this->resolveDateCondition([ 'time' ], $this->received)
|
||||
]);
|
||||
|
||||
$sql = array_map(function ($condition) { return $condition[0]; }, $conditions);
|
||||
$bindings = array_reduce($conditions, function ($bindings, $condition) {
|
||||
return array_merge($bindings, $condition[1]);
|
||||
}, []);
|
||||
|
||||
return [ $sql, $bindings ];
|
||||
}
|
||||
|
||||
// Resolve a date type condition and bindings
|
||||
protected function resolveDateCondition($fields, $inputs)
|
||||
{
|
||||
if (! count($inputs)) return null;
|
||||
|
||||
$bindings = [];
|
||||
$conditions = implode(' OR ', array_map(function ($field) use ($inputs, &$bindings) {
|
||||
return implode(' OR ', array_map(function ($input, $index) use ($field, &$bindings) {
|
||||
if (preg_match('/^<(.+)$/', $input, $match)) {
|
||||
$bindings["{$field}{$index}"] = $match[1];
|
||||
return $this->quote($field) . " < :{$field}{$index}";
|
||||
} elseif (preg_match('/^>(.+)$/', $input, $match)) {
|
||||
$bindings["{$field}{$index}"] = $match[1];
|
||||
return $this->quote($field). " > :{$field}{$index}";
|
||||
}
|
||||
}, $inputs, array_keys($inputs)));
|
||||
}, $fields));
|
||||
|
||||
return [ "({$conditions})", $bindings ];
|
||||
}
|
||||
|
||||
// Resolve an exact type condition and bindings
|
||||
protected function resolveExactCondition($fields, $inputs)
|
||||
{
|
||||
if (! count($inputs)) return null;
|
||||
|
||||
$bindings = [];
|
||||
$values = implode(' OR ', array_map(function ($field) use ($inputs, &$bindings) {
|
||||
return implode(', ', array_map(function ($input, $index) use ($field, &$bindings) {
|
||||
$bindings["{$field}{$index}"] = $input;
|
||||
return ":{$field}{$index}";
|
||||
}, $inputs, array_keys($inputs)));
|
||||
}, $fields));
|
||||
|
||||
return [ $this->quote($field) . " IN ({$values})", $bindings ];
|
||||
}
|
||||
|
||||
// Resolve a number type condition and bindings
|
||||
protected function resolveNumberCondition($fields, $inputs)
|
||||
{
|
||||
if (! count($inputs)) return null;
|
||||
|
||||
$bindings = [];
|
||||
$conditions = implode(' OR ', array_map(function ($field) use ($inputs, &$bindings) {
|
||||
return implode(' OR ', array_map(function ($input, $index) use ($field, &$bindings) {
|
||||
if (preg_match('/^<(\d+(?:\.\d+)?)$/', $input, $match)) {
|
||||
$bindings["{$field}{$index}"] = $match[1];
|
||||
return $this->quote($field) . " < :{$field}{$index}";
|
||||
} elseif (preg_match('/^>(\d+(?:\.\d+)?)$/', $input, $match)) {
|
||||
$bindings["{$field}{$index}"] = $match[1];
|
||||
return $this->quote($field) . " > :{$field}{$index}";
|
||||
} elseif (preg_match('/^(\d+(?:\.\d+)?)-(\d+(?:\.\d+)?)$/', $input, $match)) {
|
||||
$bindings["{$field}{$index}from"] = $match[1];
|
||||
$bindings["{$field}{$index}to"] = $match[2];
|
||||
$quotedField = $this->quote($field);
|
||||
return "({$quotedField} > :{$field}{$index}from AND {$quotedField} < :{$field}{$index}to)";
|
||||
} else {
|
||||
$bindings["{$field}{$index}"] = $input;
|
||||
return $this->quote($field) . " = :{$field}{$index}";
|
||||
}
|
||||
}, $inputs, array_keys($inputs)));
|
||||
}, $fields));
|
||||
|
||||
return [ "({$conditions})", $bindings ];
|
||||
}
|
||||
|
||||
// Resolve a string type condition and bindings
|
||||
protected function resolveStringCondition($fields, $inputs)
|
||||
{
|
||||
if (! count($inputs)) return null;
|
||||
|
||||
$bindings = [];
|
||||
$conditions = implode(' OR ', array_map(function ($field) use ($inputs, &$bindings) {
|
||||
return implode(' OR ', array_map(function ($input, $index) use ($field, &$bindings) {
|
||||
$bindings["{$field}{$index}"] = $input;
|
||||
return $this->quote($field) . " LIKE :{$field}{$index}";
|
||||
}, $inputs, array_keys($inputs)));
|
||||
}, $fields));
|
||||
|
||||
return [ "({$conditions})", $bindings ];
|
||||
}
|
||||
|
||||
// Build the where part of the SQL query
|
||||
protected function buildQuery()
|
||||
{
|
||||
$this->query = count($this->conditions) ? 'WHERE ' . implode(' AND ', $this->conditions) : '';
|
||||
}
|
||||
|
||||
// Quotes SQL identifier name properly for the current database
|
||||
protected function quote($identifier)
|
||||
{
|
||||
return $this->pdo && $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) == 'mysql' ? "`{$identifier}`" : "\"{$identifier}\"";
|
||||
}
|
||||
}
|
||||
296
vendor/itsgoingd/clockwork/Clockwork/Storage/SqlStorage.php
vendored
Normal file
296
vendor/itsgoingd/clockwork/Clockwork/Storage/SqlStorage.php
vendored
Normal file
@@ -0,0 +1,296 @@
|
||||
<?php namespace Clockwork\Storage;
|
||||
|
||||
use Clockwork\Clockwork;
|
||||
use Clockwork\Request\Request;
|
||||
|
||||
use PDO;
|
||||
|
||||
// SQL storage for requests using PDO
|
||||
class SqlStorage extends Storage
|
||||
{
|
||||
// PDO instance
|
||||
protected $pdo;
|
||||
|
||||
// Name of the table with Clockwork requests metadata
|
||||
protected $table;
|
||||
|
||||
// Metadata expiration time in minutes
|
||||
protected $expiration;
|
||||
|
||||
// Schema for the Clockwork requests table
|
||||
protected $fields = [
|
||||
'id' => 'VARCHAR(100) PRIMARY KEY',
|
||||
'version' => 'INTEGER',
|
||||
'type' => 'VARCHAR(100) NULL',
|
||||
'time' => 'DOUBLE PRECISION NULL',
|
||||
'method' => 'VARCHAR(10) NULL',
|
||||
'url' => 'TEXT NULL',
|
||||
'uri' => 'TEXT NULL',
|
||||
'headers' => 'TEXT NULL',
|
||||
'controller' => 'VARCHAR(250) NULL',
|
||||
'getData' => 'TEXT NULL',
|
||||
'postData' => 'TEXT NULL',
|
||||
'requestData' => 'TEXT NULL',
|
||||
'sessionData' => 'TEXT NULL',
|
||||
'authenticatedUser' => 'TEXT NULL',
|
||||
'cookies' => 'TEXT NULL',
|
||||
'responseTime' => 'DOUBLE PRECISION NULL',
|
||||
'responseStatus' => 'INTEGER NULL',
|
||||
'responseDuration' => 'DOUBLE PRECISION NULL',
|
||||
'memoryUsage' => 'DOUBLE PRECISION NULL',
|
||||
'middleware' => 'TEXT NULL',
|
||||
'databaseQueries' => 'TEXT NULL',
|
||||
'databaseQueriesCount' => 'INTEGER NULL',
|
||||
'databaseSlowQueries' => 'INTEGER NULL',
|
||||
'databaseSelects' => 'INTEGER NULL',
|
||||
'databaseInserts' => 'INTEGER NULL',
|
||||
'databaseUpdates' => 'INTEGER NULL',
|
||||
'databaseDeletes' => 'INTEGER NULL',
|
||||
'databaseOthers' => 'INTEGER NULL',
|
||||
'databaseDuration' => 'DOUBLE PRECISION NULL',
|
||||
'cacheQueries' => 'TEXT NULL',
|
||||
'cacheReads' => 'INTEGER NULL',
|
||||
'cacheHits' => 'INTEGER NULL',
|
||||
'cacheWrites' => 'INTEGER NULL',
|
||||
'cacheDeletes' => 'INTEGER NULL',
|
||||
'cacheTime' => 'DOUBLE PRECISION NULL',
|
||||
'modelsActions' => 'TEXT NULL',
|
||||
'modelsRetrieved' => 'TEXT NULL',
|
||||
'modelsCreated' => 'TEXT NULL',
|
||||
'modelsUpdated' => 'TEXT NULL',
|
||||
'modelsDeleted' => 'TEXT NULL',
|
||||
'redisCommands' => 'TEXT NULL',
|
||||
'queueJobs' => 'TEXT NULL',
|
||||
'timelineData' => 'TEXT NULL',
|
||||
'log' => 'TEXT NULL',
|
||||
'events' => 'TEXT NULL',
|
||||
'routes' => 'TEXT NULL',
|
||||
'notifications' => 'TEXT NULL',
|
||||
'emailsData' => 'TEXT NULL',
|
||||
'viewsData' => 'TEXT NULL',
|
||||
'userData' => 'TEXT NULL',
|
||||
'subrequests' => 'TEXT NULL',
|
||||
'xdebug' => 'TEXT NULL',
|
||||
'commandName' => 'TEXT NULL',
|
||||
'commandArguments' => 'TEXT NULL',
|
||||
'commandArgumentsDefaults' => 'TEXT NULL',
|
||||
'commandOptions' => 'TEXT NULL',
|
||||
'commandOptionsDefaults' => 'TEXT NULL',
|
||||
'commandExitCode' => 'INTEGER NULL',
|
||||
'commandOutput' => 'TEXT NULL',
|
||||
'jobName' => 'TEXT NULL',
|
||||
'jobDescription' => 'TEXT NULL',
|
||||
'jobStatus' => 'TEXT NULL',
|
||||
'jobPayload' => 'TEXT NULL',
|
||||
'jobQueue' => 'TEXT NULL',
|
||||
'jobConnection' => 'TEXT NULL',
|
||||
'jobOptions' => 'TEXT NULL',
|
||||
'testName' => 'TEXT NULL',
|
||||
'testStatus' => 'TEXT NULL',
|
||||
'testStatusMessage' => 'TEXT NULL',
|
||||
'testAsserts' => 'TEXT NULL',
|
||||
'clientMetrics' => 'TEXT NULL',
|
||||
'webVitals' => 'TEXT NULL',
|
||||
'parent' => 'TEXT NULL',
|
||||
'updateToken' => 'VARCHAR(100) NULL'
|
||||
];
|
||||
|
||||
// List of Request keys that need to be serialized before they can be stored in database
|
||||
protected $needsSerialization = [
|
||||
'headers', 'getData', 'postData', 'requestData', 'sessionData', 'authenticatedUser', 'cookies', 'middleware',
|
||||
'databaseQueries', 'cacheQueries', 'modelsActions', 'modelsRetrieved', 'modelsCreated', 'modelsUpdated',
|
||||
'modelsDeleted', 'redisCommands', 'queueJobs', 'timelineData', 'log', 'events', 'routes', 'notifications',
|
||||
'emailsData', 'viewsData', 'userData', 'subrequests', 'xdebug', 'commandArguments', 'commandArgumentsDefaults',
|
||||
'commandOptions', 'commandOptionsDefaults', 'jobPayload', 'jobOptions', 'testAsserts', 'parent',
|
||||
'clientMetrics', 'webVitals'
|
||||
];
|
||||
|
||||
// Return a new storage, takes PDO object or DSN and optionally a table name and database credentials as arguments
|
||||
public function __construct($dsn, $table = 'clockwork', $username = null, $password = null, $expiration = null)
|
||||
{
|
||||
$this->pdo = $dsn instanceof PDO ? $dsn : new PDO($dsn, $username, $password);
|
||||
$this->table = $table;
|
||||
$this->expiration = $expiration === null ? 60 * 24 * 7 : $expiration;
|
||||
}
|
||||
|
||||
// Returns all requests
|
||||
public function all(Search $search = null)
|
||||
{
|
||||
$fields = implode(', ', array_map(function ($field) { return $this->quote($field); }, array_keys($this->fields)));
|
||||
$search = SqlSearch::fromBase($search, $this->pdo);
|
||||
$result = $this->query("SELECT {$fields} FROM {$this->table} {$search->query}", $search->bindings);
|
||||
|
||||
return $this->resultsToRequests($result);
|
||||
}
|
||||
|
||||
// Return a single request by id
|
||||
public function find($id)
|
||||
{
|
||||
$fields = implode(', ', array_map(function ($field) { return $this->quote($field); }, array_keys($this->fields)));
|
||||
$result = $this->query("SELECT {$fields} FROM {$this->table} WHERE id = :id", [ 'id' => $id ]);
|
||||
|
||||
$requests = $this->resultsToRequests($result);
|
||||
return end($requests);
|
||||
}
|
||||
|
||||
// Return the latest request
|
||||
public function latest(Search $search = null)
|
||||
{
|
||||
$fields = implode(', ', array_map(function ($field) { return $this->quote($field); }, array_keys($this->fields)));
|
||||
$search = SqlSearch::fromBase($search, $this->pdo);
|
||||
$result = $this->query(
|
||||
"SELECT {$fields} FROM {$this->table} {$search->query} ORDER BY id DESC LIMIT 1", $search->bindings
|
||||
);
|
||||
|
||||
$requests = $this->resultsToRequests($result);
|
||||
return end($requests);
|
||||
}
|
||||
|
||||
// Return requests received before specified id, optionally limited to specified count
|
||||
public function previous($id, $count = null, Search $search = null)
|
||||
{
|
||||
$count = (int) $count;
|
||||
|
||||
$fields = implode(', ', array_map(function ($field) { return $this->quote($field); }, array_keys($this->fields)));
|
||||
$search = SqlSearch::fromBase($search, $this->pdo)->addCondition('id < :id', [ 'id' => $id ]);
|
||||
$limit = $count ? "LIMIT {$count}" : '';
|
||||
$result = $this->query(
|
||||
"SELECT {$fields} FROM {$this->table} {$search->query} ORDER BY id DESC {$limit}", $search->bindings
|
||||
);
|
||||
|
||||
return array_reverse($this->resultsToRequests($result));
|
||||
}
|
||||
|
||||
// Return requests received after specified id, optionally limited to specified count
|
||||
public function next($id, $count = null, Search $search = null)
|
||||
{
|
||||
$count = (int) $count;
|
||||
|
||||
$fields = implode(', ', array_map(function ($field) { return $this->quote($field); }, array_keys($this->fields)));
|
||||
$search = SqlSearch::fromBase($search, $this->pdo)->addCondition('id > :id', [ 'id' => $id ]);
|
||||
$limit = $count ? "LIMIT {$count}" : '';
|
||||
$result = $this->query(
|
||||
"SELECT {$fields} FROM {$this->table} {$search->query} ORDER BY id ASC {$limit}", $search->bindings
|
||||
);
|
||||
|
||||
return $this->resultsToRequests($result);
|
||||
}
|
||||
|
||||
// Store the request in the database
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $request->toArray();
|
||||
|
||||
foreach ($this->needsSerialization as $key) {
|
||||
$data[$key] = @json_encode($data[$key], \JSON_PARTIAL_OUTPUT_ON_ERROR);
|
||||
}
|
||||
|
||||
$fields = implode(', ', array_map(function ($field) { return $this->quote($field); }, array_keys($this->fields)));
|
||||
$bindings = implode(', ', array_map(function ($field) { return ":{$field}"; }, array_keys($this->fields)));
|
||||
|
||||
$this->query("INSERT INTO {$this->table} ($fields) VALUES ($bindings)", $data);
|
||||
|
||||
$this->cleanup();
|
||||
}
|
||||
|
||||
// Update an existing request in the database
|
||||
public function update(Request $request)
|
||||
{
|
||||
$data = $request->toArray();
|
||||
|
||||
foreach ($this->needsSerialization as $key) {
|
||||
$data[$key] = @json_encode($data[$key], \JSON_PARTIAL_OUTPUT_ON_ERROR);
|
||||
}
|
||||
|
||||
$values = implode(', ', array_map(function ($field) {
|
||||
return $this->quote($field) . " = :{$field}";
|
||||
}, array_keys($this->fields)));
|
||||
|
||||
$this->query("UPDATE {$this->table} SET {$values} WHERE id = :id", $data);
|
||||
|
||||
$this->cleanup();
|
||||
}
|
||||
|
||||
// Cleanup old requests
|
||||
public function cleanup()
|
||||
{
|
||||
if ($this->expiration === false) return;
|
||||
|
||||
$this->query("DELETE FROM {$this->table} WHERE time < :time", [ 'time' => time() - ($this->expiration * 60) ]);
|
||||
}
|
||||
|
||||
// Create or update the Clockwork metadata table
|
||||
protected function initialize()
|
||||
{
|
||||
// first we get rid of existing table if it exists by renaming it so we won't lose any data
|
||||
try {
|
||||
$table = $this->quote($this->table);
|
||||
$backupTableName = $this->quote("{$this->table}_backup_" . date('Ymd'));
|
||||
$this->pdo->exec("ALTER TABLE {$table} RENAME TO {$backupTableName};");
|
||||
} catch (\PDOException $e) {
|
||||
// this just means the table doesn't yet exist, nothing to do here
|
||||
}
|
||||
|
||||
// create the metadata table
|
||||
$this->pdo->exec($this->buildSchema($table));
|
||||
|
||||
$indexName = $this->quote("{$this->table}_time_index");
|
||||
$this->pdo->exec("CREATE INDEX {$indexName} ON {$table} (". $this->quote('time') .')');
|
||||
}
|
||||
|
||||
// Builds the query to create Clockwork database table
|
||||
protected function buildSchema($table)
|
||||
{
|
||||
$textType = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) == 'mysql' ? 'MEDIUMTEXT' : 'TEXT';
|
||||
|
||||
$columns = implode(', ', array_map(function ($field, $type) use ($textType) {
|
||||
return $this->quote($field) . ' ' . str_replace('TEXT', $textType, $type);
|
||||
}, array_keys($this->fields), array_values($this->fields)));
|
||||
|
||||
return "CREATE TABLE {$table} ({$columns});";
|
||||
}
|
||||
|
||||
// Executes an sql query, lazily initiates the clockwork database schema if it's old or doesn't exist yet, returns
|
||||
// executed statement or false on error
|
||||
protected function query($query, array $bindings = [], $firstTry = true)
|
||||
{
|
||||
try {
|
||||
if ($stmt = $this->pdo->prepare($query)) {
|
||||
if ($stmt->execute($bindings)) return $stmt;
|
||||
throw new \PDOException;
|
||||
}
|
||||
} catch (\PDOException $e) {
|
||||
$stmt = false;
|
||||
}
|
||||
|
||||
// the query failed to execute, assume it's caused by missing or old schema, try to reinitialize database
|
||||
if (! $stmt && $firstTry) {
|
||||
$this->initialize();
|
||||
return $this->query($query, $bindings, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Quotes SQL identifier name properly for the current database
|
||||
protected function quote($identifier)
|
||||
{
|
||||
return $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) == 'mysql' ? "`{$identifier}`" : "\"{$identifier}\"";
|
||||
}
|
||||
|
||||
// Returns array of Requests instances from the executed PDO statement
|
||||
protected function resultsToRequests($stmt)
|
||||
{
|
||||
return array_map(function ($data) {
|
||||
return $this->dataToRequest($data);
|
||||
}, $stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
}
|
||||
|
||||
// Returns a Request instance from a single database record
|
||||
protected function dataToRequest($data)
|
||||
{
|
||||
foreach ($this->needsSerialization as $key) {
|
||||
$data[$key] = json_decode($data[$key], true);
|
||||
}
|
||||
|
||||
return new Request($data);
|
||||
}
|
||||
}
|
||||
12
vendor/itsgoingd/clockwork/Clockwork/Storage/Storage.php
vendored
Normal file
12
vendor/itsgoingd/clockwork/Clockwork/Storage/Storage.php
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php namespace Clockwork\Storage;
|
||||
|
||||
use Clockwork\Storage\StorageInterface;
|
||||
use Clockwork\Request\Request;
|
||||
|
||||
abstract class Storage implements StorageInterface
|
||||
{
|
||||
// Update existing request
|
||||
public function update(Request $request)
|
||||
{
|
||||
}
|
||||
}
|
||||
31
vendor/itsgoingd/clockwork/Clockwork/Storage/StorageInterface.php
vendored
Normal file
31
vendor/itsgoingd/clockwork/Clockwork/Storage/StorageInterface.php
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php namespace Clockwork\Storage;
|
||||
|
||||
use Clockwork\Request\Request;
|
||||
|
||||
// Interface for requests storage implementations
|
||||
interface StorageInterface
|
||||
{
|
||||
// Returns all requests
|
||||
public function all(Search $search = null);
|
||||
|
||||
// Return a single request by id
|
||||
public function find($id);
|
||||
|
||||
// Return the latest request
|
||||
public function latest(Search $search = null);
|
||||
|
||||
// Return requests received before specified id, optionally limited to specified count
|
||||
public function previous($id, $count = null, Search $search = null);
|
||||
|
||||
// Return requests received after specified id, optionally limited to specified count
|
||||
public function next($id, $count = null, Search $search = null);
|
||||
|
||||
// Store request
|
||||
public function store(Request $request);
|
||||
|
||||
// Update existing request
|
||||
public function update(Request $request);
|
||||
|
||||
// Cleanup old requests
|
||||
public function cleanup();
|
||||
}
|
||||
55
vendor/itsgoingd/clockwork/Clockwork/Storage/SymfonyStorage.php
vendored
Normal file
55
vendor/itsgoingd/clockwork/Clockwork/Storage/SymfonyStorage.php
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php namespace Clockwork\Storage;
|
||||
|
||||
use Clockwork\Request\Request;
|
||||
use Clockwork\Support\Symfony\ProfileTransformer;
|
||||
|
||||
use Symfony\Component\HttpKernel\Profiler\Profiler;
|
||||
|
||||
// Storage wrapping Symfony profiler
|
||||
class SymfonyStorage extends FileStorage
|
||||
{
|
||||
// Symfony profiler instance
|
||||
protected $profiler;
|
||||
|
||||
// Symfony profiler path
|
||||
protected $path;
|
||||
|
||||
// Create a new instance, takes Symfony profiler instance and path as argument
|
||||
public function __construct(Profiler $profiler, $path)
|
||||
{
|
||||
$this->profiler = $profiler;
|
||||
$this->path = $path;
|
||||
}
|
||||
|
||||
// Store request, no-op since this is read-only storage implementation
|
||||
public function store(Request $request, $skipIndex = false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Cleanup old requests, no-op since this is read-only storage implementation
|
||||
public function cleanup($force = false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
protected function loadRequest($token)
|
||||
{
|
||||
return ($profile = $this->profiler->loadProfile($token)) ? (new ProfileTransformer)->transform($profile) : null;
|
||||
}
|
||||
|
||||
// Open index file, optionally move file pointer to the end
|
||||
protected function openIndex($position = 'start', $lock = null, $force = null)
|
||||
{
|
||||
$this->indexHandle = fopen("{$this->path}/index.csv", 'r');
|
||||
|
||||
if ($position == 'end') fseek($this->indexHandle, 0, SEEK_END);
|
||||
}
|
||||
|
||||
protected function makeRequestFromIndex($record)
|
||||
{
|
||||
return new Request(array_combine(
|
||||
[ 'id', 'ip', 'method', 'uri', 'time', 'parent', 'responseStatus' ], $record
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user