Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
199 lars 1
<?php namespace Clockwork\Storage;
2
 
3
use Clockwork\Clockwork;
4
use Clockwork\Request\Request;
5
 
6
use PDO;
7
 
8
// SQL storage for requests using PDO
9
class SqlStorage extends Storage
10
{
11
	// PDO instance
12
	protected $pdo;
13
 
14
	// Name of the table with Clockwork requests metadata
15
	protected $table;
16
 
17
	// Metadata expiration time in minutes
18
	protected $expiration;
19
 
20
	// Schema for the Clockwork requests table
21
	protected $fields = [
22
		'id'                       => 'VARCHAR(100) PRIMARY KEY',
23
		'version'                  => 'INTEGER',
24
		'type'                     => 'VARCHAR(100) NULL',
25
		'time'                     => 'DOUBLE PRECISION NULL',
26
		'method'                   => 'VARCHAR(10) NULL',
27
		'url'                      => 'TEXT NULL',
28
		'uri'                      => 'TEXT NULL',
29
		'headers'                  => 'TEXT NULL',
30
		'controller'               => 'VARCHAR(250) NULL',
31
		'getData'                  => 'TEXT NULL',
32
		'postData'                 => 'TEXT NULL',
33
		'requestData'              => 'TEXT NULL',
34
		'sessionData'              => 'TEXT NULL',
35
		'authenticatedUser'        => 'TEXT NULL',
36
		'cookies'                  => 'TEXT NULL',
37
		'responseTime'             => 'DOUBLE PRECISION NULL',
38
		'responseStatus'           => 'INTEGER NULL',
39
		'responseDuration'         => 'DOUBLE PRECISION NULL',
40
		'memoryUsage'              => 'DOUBLE PRECISION NULL',
41
		'middleware'               => 'TEXT NULL',
42
		'databaseQueries'          => 'TEXT NULL',
43
		'databaseQueriesCount'     => 'INTEGER NULL',
44
		'databaseSlowQueries'      => 'INTEGER NULL',
45
		'databaseSelects'          => 'INTEGER NULL',
46
		'databaseInserts'          => 'INTEGER NULL',
47
		'databaseUpdates'          => 'INTEGER NULL',
48
		'databaseDeletes'          => 'INTEGER NULL',
49
		'databaseOthers'           => 'INTEGER NULL',
50
		'databaseDuration'         => 'DOUBLE PRECISION NULL',
51
		'cacheQueries'             => 'TEXT NULL',
52
		'cacheReads'               => 'INTEGER NULL',
53
		'cacheHits'                => 'INTEGER NULL',
54
		'cacheWrites'              => 'INTEGER NULL',
55
		'cacheDeletes'             => 'INTEGER NULL',
56
		'cacheTime'                => 'DOUBLE PRECISION NULL',
57
		'modelsActions'            => 'TEXT NULL',
58
		'modelsRetrieved'          => 'TEXT NULL',
59
		'modelsCreated'            => 'TEXT NULL',
60
		'modelsUpdated'            => 'TEXT NULL',
61
		'modelsDeleted'            => 'TEXT NULL',
62
		'redisCommands'            => 'TEXT NULL',
63
		'queueJobs'                => 'TEXT NULL',
64
		'timelineData'             => 'TEXT NULL',
65
		'log'                      => 'TEXT NULL',
66
		'events'                   => 'TEXT NULL',
67
		'routes'                   => 'TEXT NULL',
68
		'notifications'            => 'TEXT NULL',
69
		'emailsData'               => 'TEXT NULL',
70
		'viewsData'                => 'TEXT NULL',
71
		'userData'                 => 'TEXT NULL',
72
		'subrequests'              => 'TEXT NULL',
73
		'xdebug'                   => 'TEXT NULL',
74
		'commandName'              => 'TEXT NULL',
75
		'commandArguments'         => 'TEXT NULL',
76
		'commandArgumentsDefaults' => 'TEXT NULL',
77
		'commandOptions'           => 'TEXT NULL',
78
		'commandOptionsDefaults'   => 'TEXT NULL',
79
		'commandExitCode'          => 'INTEGER NULL',
80
		'commandOutput'            => 'TEXT NULL',
81
		'jobName'                  => 'TEXT NULL',
82
		'jobDescription'           => 'TEXT NULL',
83
		'jobStatus'                => 'TEXT NULL',
84
		'jobPayload'               => 'TEXT NULL',
85
		'jobQueue'                 => 'TEXT NULL',
86
		'jobConnection'            => 'TEXT NULL',
87
		'jobOptions'               => 'TEXT NULL',
88
		'testName'                 => 'TEXT NULL',
89
		'testStatus'               => 'TEXT NULL',
90
		'testStatusMessage'        => 'TEXT NULL',
91
		'testAsserts'              => 'TEXT NULL',
92
		'clientMetrics'            => 'TEXT NULL',
93
		'webVitals'                => 'TEXT NULL',
94
		'parent'                   => 'TEXT NULL',
95
		'updateToken'              => 'VARCHAR(100) NULL'
96
	];
97
 
98
	// List of Request keys that need to be serialized before they can be stored in database
99
	protected $needsSerialization = [
100
		'headers', 'getData', 'postData', 'requestData', 'sessionData', 'authenticatedUser', 'cookies', 'middleware',
101
		'databaseQueries', 'cacheQueries', 'modelsActions', 'modelsRetrieved', 'modelsCreated', 'modelsUpdated',
102
		'modelsDeleted', 'redisCommands', 'queueJobs', 'timelineData', 'log', 'events', 'routes', 'notifications',
103
		'emailsData', 'viewsData', 'userData', 'subrequests', 'xdebug', 'commandArguments', 'commandArgumentsDefaults',
104
		'commandOptions', 'commandOptionsDefaults', 'jobPayload', 'jobOptions', 'testAsserts', 'parent',
105
		'clientMetrics', 'webVitals'
106
	];
107
 
108
	// Return a new storage, takes PDO object or DSN and optionally a table name and database credentials as arguments
109
	public function __construct($dsn, $table = 'clockwork', $username = null, $password = null, $expiration = null)
110
	{
111
		$this->pdo = $dsn instanceof PDO ? $dsn : new PDO($dsn, $username, $password);
112
		$this->table = $table;
113
		$this->expiration = $expiration === null ? 60 * 24 * 7 : $expiration;
114
	}
115
 
116
	// Returns all requests
117
	public function all(Search $search = null)
118
	{
119
		$fields = implode(', ', array_map(function ($field) { return $this->quote($field); }, array_keys($this->fields)));
120
		$search = SqlSearch::fromBase($search, $this->pdo);
121
		$result = $this->query("SELECT {$fields} FROM {$this->table} {$search->query}", $search->bindings);
122
 
123
		return $this->resultsToRequests($result);
124
	}
125
 
126
	// Return a single request by id
127
	public function find($id)
128
	{
129
		$fields = implode(', ', array_map(function ($field) { return $this->quote($field); }, array_keys($this->fields)));
130
		$result = $this->query("SELECT {$fields} FROM {$this->table} WHERE id = :id", [ 'id' => $id ]);
131
 
132
		$requests = $this->resultsToRequests($result);
133
		return end($requests);
134
	}
135
 
136
	// Return the latest request
137
	public function latest(Search $search = null)
138
	{
139
		$fields = implode(', ', array_map(function ($field) { return $this->quote($field); }, array_keys($this->fields)));
140
		$search = SqlSearch::fromBase($search, $this->pdo);
141
		$result = $this->query(
142
			"SELECT {$fields} FROM {$this->table} {$search->query} ORDER BY id DESC LIMIT 1", $search->bindings
143
		);
144
 
145
		$requests = $this->resultsToRequests($result);
146
		return end($requests);
147
	}
148
 
149
	// Return requests received before specified id, optionally limited to specified count
150
	public function previous($id, $count = null, Search $search = null)
151
	{
152
		$count = (int) $count;
153
 
154
		$fields = implode(', ', array_map(function ($field) { return $this->quote($field); }, array_keys($this->fields)));
155
		$search = SqlSearch::fromBase($search, $this->pdo)->addCondition('id < :id', [ 'id' => $id ]);
156
		$limit = $count ? "LIMIT {$count}" : '';
157
		$result = $this->query(
158
			"SELECT {$fields} FROM {$this->table} {$search->query} ORDER BY id DESC {$limit}", $search->bindings
159
		);
160
 
161
		return array_reverse($this->resultsToRequests($result));
162
	}
163
 
164
	// Return requests received after specified id, optionally limited to specified count
165
	public function next($id, $count = null, Search $search = null)
166
	{
167
		$count = (int) $count;
168
 
169
		$fields = implode(', ', array_map(function ($field) { return $this->quote($field); }, array_keys($this->fields)));
170
		$search = SqlSearch::fromBase($search, $this->pdo)->addCondition('id > :id', [ 'id' => $id ]);
171
		$limit = $count ? "LIMIT {$count}" : '';
172
		$result = $this->query(
173
			"SELECT {$fields} FROM {$this->table} {$search->query} ORDER BY id ASC {$limit}", $search->bindings
174
		);
175
 
176
		return $this->resultsToRequests($result);
177
	}
178
 
179
	// Store the request in the database
180
	public function store(Request $request)
181
	{
182
		$data = $request->toArray();
183
 
184
		foreach ($this->needsSerialization as $key) {
185
			$data[$key] = @json_encode($data[$key], \JSON_PARTIAL_OUTPUT_ON_ERROR);
186
		}
187
 
188
		$fields = implode(', ', array_map(function ($field) { return $this->quote($field); }, array_keys($this->fields)));
189
		$bindings = implode(', ', array_map(function ($field) { return ":{$field}"; }, array_keys($this->fields)));
190
 
191
		$this->query("INSERT INTO {$this->table} ($fields) VALUES ($bindings)", $data);
192
 
193
		$this->cleanup();
194
	}
195
 
196
	// Update an existing request in the database
197
	public function update(Request $request)
198
	{
199
		$data = $request->toArray();
200
 
201
		foreach ($this->needsSerialization as $key) {
202
			$data[$key] = @json_encode($data[$key], \JSON_PARTIAL_OUTPUT_ON_ERROR);
203
		}
204
 
205
		$values = implode(', ', array_map(function ($field) {
206
			return $this->quote($field) . " = :{$field}";
207
		}, array_keys($this->fields)));
208
 
209
		$this->query("UPDATE {$this->table} SET {$values} WHERE id = :id", $data);
210
 
211
		$this->cleanup();
212
	}
213
 
214
	// Cleanup old requests
215
	public function cleanup()
216
	{
217
		if ($this->expiration === false) return;
218
 
219
		$this->query("DELETE FROM {$this->table} WHERE time < :time", [ 'time' => time() - ($this->expiration * 60) ]);
220
	}
221
 
222
	// Create or update the Clockwork metadata table
223
	protected function initialize()
224
	{
225
		// first we get rid of existing table if it exists by renaming it so we won't lose any data
226
		try {
227
			$table = $this->quote($this->table);
228
			$backupTableName = $this->quote("{$this->table}_backup_" . date('Ymd'));
229
			$this->pdo->exec("ALTER TABLE {$table} RENAME TO {$backupTableName};");
230
		} catch (\PDOException $e) {
231
			// this just means the table doesn't yet exist, nothing to do here
232
		}
233
 
234
		// create the metadata table
235
		$this->pdo->exec($this->buildSchema($table));
236
 
237
		$indexName = $this->quote("{$this->table}_time_index");
238
		$this->pdo->exec("CREATE INDEX {$indexName} ON {$table} (". $this->quote('time') .')');
239
	}
240
 
241
	// Builds the query to create Clockwork database table
242
	protected function buildSchema($table)
243
	{
244
		$textType = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) == 'mysql' ? 'MEDIUMTEXT' : 'TEXT';
245
 
246
		$columns = implode(', ', array_map(function ($field, $type) use ($textType) {
247
			return $this->quote($field) . ' ' . str_replace('TEXT', $textType, $type);
248
		}, array_keys($this->fields), array_values($this->fields)));
249
 
250
		return "CREATE TABLE {$table} ({$columns});";
251
	}
252
 
253
	// Executes an sql query, lazily initiates the clockwork database schema if it's old or doesn't exist yet, returns
254
	// executed statement or false on error
255
	protected function query($query, array $bindings = [], $firstTry = true)
256
	{
257
		try {
258
			if ($stmt = $this->pdo->prepare($query)) {
259
				if ($stmt->execute($bindings)) return $stmt;
260
				throw new \PDOException;
261
			}
262
		} catch (\PDOException $e) {
263
			$stmt = false;
264
		}
265
 
266
		// the query failed to execute, assume it's caused by missing or old schema, try to reinitialize database
267
		if (! $stmt && $firstTry) {
268
			$this->initialize();
269
			return $this->query($query, $bindings, false);
270
		}
271
	}
272
 
273
	// Quotes SQL identifier name properly for the current database
274
	protected function quote($identifier)
275
	{
276
		return $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) == 'mysql' ? "`{$identifier}`" : "\"{$identifier}\"";
277
	}
278
 
279
	// Returns array of Requests instances from the executed PDO statement
280
	protected function resultsToRequests($stmt)
281
	{
282
		return array_map(function ($data) {
283
			return $this->dataToRequest($data);
284
		}, $stmt->fetchAll(PDO::FETCH_ASSOC));
285
	}
286
 
287
	// Returns a Request instance from a single database record
288
	protected function dataToRequest($data)
289
	{
290
		foreach ($this->needsSerialization as $key) {
291
			$data[$key] = json_decode($data[$key], true);
292
		}
293
 
294
		return new Request($data);
295
	}
296
}