Blame | Letzte Änderung | Log anzeigen | RSS feed
<?php namespace Clockwork\Storage;use Clockwork\Clockwork;use Clockwork\Request\Request;use PDO;// SQL storage for requests using PDOclass SqlStorage extends Storage{// PDO instanceprotected $pdo;// Name of the table with Clockwork requests metadataprotected $table;// Metadata expiration time in minutesprotected $expiration;// Schema for the Clockwork requests tableprotected $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 databaseprotected $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 argumentspublic 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 requestspublic 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 idpublic 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 requestpublic 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 countpublic 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 countpublic 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 databasepublic 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 databasepublic 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 requestspublic 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 tableprotected function initialize(){// first we get rid of existing table if it exists by renaming it so we won't lose any datatry {$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 tableprotected 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 errorprotected 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 databaseif (! $stmt && $firstTry) {$this->initialize();return $this->query($query, $bindings, false);}}// Quotes SQL identifier name properly for the current databaseprotected function quote($identifier){return $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) == 'mysql' ? "`{$identifier}`" : "\"{$identifier}\"";}// Returns array of Requests instances from the executed PDO statementprotected 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 recordprotected function dataToRequest($data){foreach ($this->needsSerialization as $key) {$data[$key] = json_decode($data[$key], true);}return new Request($data);}}