Subversion-Projekte lars-tiefland.laravel_shop

Revision

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 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);
        }
}