TDbLogRoute::getConnectionID()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * TLogRouter, TLogRoute, TFileLogRoute, TEmailLogRoute class file
5
 *
6
 * @author Qiang Xue <[email protected]>
7
 * @link https://github.com/pradosoft/prado
8
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
9
 */
10
11
namespace Prado\Util;
12
13
use Exception;
14
use Prado\Data\TDataSourceConfig;
15
use Prado\Data\TDbConnection;
16
use Prado\Exceptions\TConfigurationException;
17
use Prado\Exceptions\TLogException;
18
use Prado\TPropertyValue;
19
20
/**
21
 * TDbLogRoute class
22
 *
23
 * TDbLogRoute stores log messages in a database table.
24
 * To specify the database table, set {@see setConnectionID ConnectionID} to be
25
 * the ID of a {@see \Prado\Data\TDataSourceConfig} module and {@see setLogTableName LogTableName}.
26
 * If they are not setting, an SQLite3 database named 'sqlite3.log' will be created and used
27
 * under the runtime directory.
28
 *
29
 * By default, the database table name is 'pradolog'. It has the following structure:
30
 * ```sql
31
 *	CREATE TABLE pradolog
32
 *  (
33
 *		log_id INTEGER NOT NULL PRIMARY KEY,
34
 *		level INTEGER,
35
 *		category VARCHAR(128),
36
 *		prefix VARCHAR(128),
37
 *		logtime VARCHAR(20),
38
 *		message VARCHAR(255)
39
 *   );
40
 * ```
41
 *
42
 * 4.3.0 Notes: Add the `prefix` to the log table:
43
 * `ALTER TABLE pradolog ADD COLUMN prefix VARCHAR(128) AFTER category;`
44
 *
45
 * @author Qiang Xue <[email protected]>
46
 * @author Brad Anderson <[email protected]>
47
 * @since 3.1.2
48
 */
49
class TDbLogRoute extends TLogRoute
50
{
51
	/**
52
	 * @var string the ID of TDataSourceConfig module
53
	 */
54
	private $_connID = '';
55
	/**
56
	 * @var TDbConnection the DB connection instance
57
	 */
58
	private $_db;
59
	/**
60
	 * @var string name of the DB log table
61
	 */
62
	private $_logTable = 'pradolog';
63
	/**
64
	 * @var bool whether the log DB table should be created automatically
65
	 */
66
	private bool $_autoCreate = true;
67
	/**
68
	 * @var ?float The number of seconds of the log to retain.  Default null for logs are
69
	 *   not deleted.
70
	 * @since 4.3.0
71
	 */
72
	private ?float $_retainPeriod = null;
73
74
	/**
75
	 * Destructor.
76
	 * Disconnect the db connection.
77
	 */
78
	public function __destruct()
79
	{
80
		if ($this->_db !== null) {
81
			$this->_db->setActive(false);
82
		}
83
		parent::__destruct();
84
	}
85
86
	/**
87
	 * Initializes this module.
88
	 * This method is required by the IModule interface.
89
	 * It initializes the database for logging purpose.
90
	 * @param \Prado\Xml\TXmlElement $config configuration for this module, can be null
91
	 * @throws TConfigurationException if the DB table does not exist.
92
	 */
93
	public function init($config)
94
	{
95
		$db = $this->getDbConnection();
96
		$db->setActive(true);
97
98
		$sql = 'SELECT * FROM ' . $this->_logTable . ' WHERE 0=1';
99
		try {
100
			$db->createCommand($sql)->query()->close();
101
		} catch (Exception $e) {
102
			// DB table not exists
103
			if ($this->_autoCreate) {
104
				$this->createDbTable();
105
			} else {
106
				throw new TConfigurationException('dblogroute_table_nonexistent', $this->_logTable);
107
			}
108
		}
109
110
		parent::init($config);
111
	}
112
113
	/**
114
	 * Stores log messages into database.
115
	 * @param array $logs list of log messages
116
	 * @param bool $final is the final flush
117
	 * @param array $meta the meta data for the logs.
118
	 * @throws TLogException when the DB insert fails.
119
	 */
120
	protected function processLogs(array $logs, bool $final, array $meta)
121
	{
122
		$sql = 'INSERT INTO ' . $this->_logTable . '(level, category, prefix, logtime, message) VALUES (:level, :category, :prefix, :logtime, :message)';
123
		$command = $this->getDbConnection()->createCommand($sql);
124
		foreach ($logs as $log) {
125
			$command->bindValue(':message', (string) $log[TLogger::LOG_MESSAGE]);
126
			$command->bindValue(':level', $log[TLogger::LOG_LEVEL]);
127
			$command->bindValue(':category', $log[TLogger::LOG_CATEGORY]);
128
			$command->bindValue(':prefix', $this->getLogPrefix($log));
129
			$command->bindValue(':logtime', sprintf('%F', $log[TLogger::LOG_TIME]));
130
			if (!$command->execute()) {
131
				throw new TLogException('dblogroute_insert_failed', $this->_logTable);
132
			}
133
		}
134
		if (!empty($seconds = $this->getRetainPeriod())) {
135
			$this->deleteDbLog(null, null, null, microtime(true) - $seconds);
136
		}
137
	}
138
139
	/**
140
	 * Computes the where SQL clause based upon level, categories, minimum time and maximum time.
141
	 * @param ?int $level  The bit mask of log levels to search for
142
	 * @param null|null|array|string $categories The categories to search for.  Strings
143
	 *   are exploded with ','.
144
	 * @param ?float $minTime All logs after this time are found
145
	 * @param ?float $maxTime All logs before this time are found
146
	 * @param mixed $values the values to fill in.
147
	 * @return string The where clause for the various SQL statements.
148
	 * @since 4.3.0
149
	 */
150
	protected function getLogWhere(?int $level, null|string|array $categories, ?float $minTime, ?float $maxTime, &$values): string
151
	{
152
		$where = '';
153
		$values = [];
154
		if ($level !== null) {
155
			$where .= '((level & :level) > 0)';
156
			$values[':level'] = $level;
157
		}
158
		if ($categories !== null) {
0 ignored issues
show
introduced by
The condition $categories !== null is always true.
Loading history...
159
			if (is_string($categories)) {
0 ignored issues
show
introduced by
The condition is_string($categories) is always false.
Loading history...
160
				$categories = array_map('trim', explode(',', $categories));
161
			}
162
			$i = 0;
163
			$or = '';
164
			foreach ($categories as $category) {
165
				$c = $category[0] ?? 0;
166
				if ($c === '!' || $c === '~') {
167
					if ($where) {
168
						$where .= ' AND ';
169
					}
170
					$category = substr($category, 1);
171
					$where .= "(category NOT LIKE :category{$i})";
172
				} else {
173
					if ($or) {
174
						$or .= ' OR ';
175
					}
176
					$or .= "(category LIKE :category{$i})";
177
				}
178
				$category = str_replace('*', '%', $category);
179
				$values[':category' . ($i++)] = $category;
180
			}
181
			if ($or) {
182
				if ($where) {
183
					$where .= ' AND ';
184
				}
185
				$where .= '(' . $or . ')';
186
			}
187
		}
188
		if ($minTime !== null) {
189
			if ($where) {
190
				$where .= ' AND ';
191
			}
192
			$where .= 'logtime >= :mintime';
193
			$values[':mintime'] = sprintf('%F', $minTime);
194
		}
195
		if ($maxTime !== null) {
196
			if ($where) {
197
				$where .= ' AND ';
198
			}
199
			$where .= 'logtime < :maxtime';
200
			$values[':maxtime'] = sprintf('%F', $maxTime);
201
		}
202
		if ($where) {
203
			$where = ' WHERE ' . $where;
204
		}
205
		return $where;
206
	}
207
208
	/**
209
	 * Gets the number of logs in the database fitting the provided criteria.
210
	 * @param ?int $level  The bit mask of log levels to search for
211
	 * @param null|null|array|string $categories The categories to search for.  Strings
212
	 *   are exploded with ','.
213
	 * @param ?float $minTime All logs after this time are found
214
	 * @param ?float $maxTime All logs before this time are found
215
	 * @return string The where clause for the various SQL statements..
216
	 * @since 4.3.0
217
	 */
218
	public function getDBLogCount(?int $level = null, null|string|array $categories = null, ?float $minTime = null, ?float $maxTime = null)
219
	{
220
		$values = [];
221
		$where = $this->getLogWhere($level, $categories, $minTime, $maxTime, $values);
222
		$sql = 'SELECT COUNT(*) FROM ' . $this->_logTable . $where;
223
		$command = $this->getDbConnection()->createCommand($sql);
224
		foreach ($values as $key => $value) {
225
			$command->bindValue($key, $value);
226
		}
227
		return $command->queryScalar();
228
	}
229
230
	/**
231
	 * Gets the number of logs in the database fitting the provided criteria.
232
	 * @param ?int $level  The bit mask of log levels to search for
233
	 * @param null|null|array|string $categories The categories to search for.  Strings
234
	 *   are exploded with ','.
235
	 * @param ?float $minTime All logs after this time are found
236
	 * @param ?float $maxTime All logs before this time are found
237
	 * @param string $order The order statement.
238
	 * @param string $limit The limit statement.
239
	 * @return \Prado\Data\TDbDataReader the logs from the database.
240
	 * @since 4.3.0
241
	 */
242
	public function getDBLogs(?int $level = null, null|string|array $categories = null, ?float $minTime = null, ?float $maxTime = null, string $order = '', string $limit = '')
243
	{
244
		$values = [];
245
		if ($order) {
246
			$order .= ' ORDER BY ' . $order;
247
		}
248
		if ($limit) {
249
			$limit .= ' LIMIT ' . $limit;
250
		}
251
		$where = $this->getLogWhere($level, $categories, $minTime, $maxTime, $values);
252
		$sql = 'SELECT * FROM ' . $this->_logTable . $where . $order . $limit;
253
		$command = $this->getDbConnection()->createCommand($sql);
254
		foreach ($values as $key => $value) {
255
			$command->bindValue($key, $value);
256
		}
257
		return $command->query();
258
	}
259
260
	/**
261
	 * Deletes log items from the database that match the criteria.
262
	 * @param ?int $level  The bit mask of log levels to search for
263
	 * @param null|null|array|string $categories The categories to search for.  Strings
264
	 *   are exploded with ','.
265
	 * @param ?float $minTime All logs after this time are found
266
	 * @param ?float $maxTime All logs before this time are found
267
	 * @return int the number of logs in the database.
268
	 * @since 4.3.0
269
	 */
270
	public function deleteDBLog(?int $level = null, null|string|array $categories = null, ?float $minTime = null, ?float $maxTime = null)
271
	{
272
		$values = [];
273
		$where = $this->getLogWhere($level, $categories, $minTime, $maxTime, $values);
274
		$sql = 'DELETE FROM ' . $this->_logTable . $where;
275
		$command = $this->getDbConnection()->createCommand($sql);
276
		foreach ($values as $key => $value) {
277
			$command->bindValue($key, $value);
278
		}
279
		return $command->execute();
280
	}
281
282
	/**
283
	 * Creates the DB table for storing log messages.
284
	 */
285
	protected function createDbTable()
286
	{
287
		$db = $this->getDbConnection();
288
		$driver = $db->getDriverName();
289
		$autoidAttributes = '';
290
		if ($driver === 'mysql') {
291
			$autoidAttributes = 'AUTO_INCREMENT';
292
		}
293
		if ($driver === 'pgsql') {
294
			$param = 'SERIAL';
295
		} else {
296
			$param = 'INTEGER NOT NULL';
297
		}
298
299
		$sql = 'CREATE TABLE ' . $this->_logTable . ' (
300
			log_id ' . $param . ' PRIMARY KEY ' . $autoidAttributes . ',
301
			level INTEGER,
302
			category VARCHAR(128),
303
			prefix VARCHAR(128),
304
			logtime VARCHAR(20),
305
			message VARCHAR(255))';
306
		$db->createCommand($sql)->execute();
307
	}
308
309
	/**
310
	 * Creates the DB connection.
311
	 * @throws TConfigurationException if module ID is invalid or empty
312
	 * @return \Prado\Data\TDbConnection the created DB connection
313
	 */
314
	protected function createDbConnection()
315
	{
316
		if ($this->_connID !== '') {
317
			$config = $this->getApplication()->getModule($this->_connID);
318
			if ($config instanceof TDataSourceConfig) {
319
				return $config->getDbConnection();
320
			} else {
321
				throw new TConfigurationException('dblogroute_connectionid_invalid', $this->_connID);
322
			}
323
		} else {
324
			$db = new TDbConnection();
325
			// default to SQLite3 database
326
			$dbFile = $this->getApplication()->getRuntimePath() . DIRECTORY_SEPARATOR . 'sqlite3.log';
327
			$db->setConnectionString('sqlite:' . $dbFile);
328
			return $db;
329
		}
330
	}
331
332
	/**
333
	 * @return \Prado\Data\TDbConnection the DB connection instance
334
	 */
335
	public function getDbConnection()
336
	{
337
		if ($this->_db === null) {
338
			$this->_db = $this->createDbConnection();
339
		}
340
		return $this->_db;
341
	}
342
343
	/**
344
	 * @return string the ID of a {@see \Prado\Data\TDataSourceConfig} module. Defaults to empty string, meaning not set.
345
	 */
346
	public function getConnectionID()
347
	{
348
		return $this->_connID;
349
	}
350
351
	/**
352
	 * Sets the ID of a TDataSourceConfig module.
353
	 * The datasource module will be used to establish the DB connection for this log route.
354
	 * @param string $value ID of the {@see \Prado\Data\TDataSourceConfig} module
355
	 * @return static The current object.
356
	 */
357
	public function setConnectionID($value): static
358
	{
359
		$this->_connID = $value;
360
361
		return $this;
362
	}
363
364
	/**
365
	 * @return string the name of the DB table to store log content. Defaults to 'pradolog'.
366
	 * @see setAutoCreateLogTable
367
	 */
368
	public function getLogTableName()
369
	{
370
		return $this->_logTable;
371
	}
372
373
	/**
374
	 * Sets the name of the DB table to store log content.
375
	 * Note, if {@see setAutoCreateLogTable AutoCreateLogTable} is false
376
	 * and you want to create the DB table manually by yourself,
377
	 * you need to make sure the DB table is of the following structure:
378
	 * (key CHAR(128) PRIMARY KEY, value BLOB, expire INT)
379
	 * @param string $value the name of the DB table to store log content
380
	 * @return static The current object.
381
	 * @see setAutoCreateLogTable
382
	 */
383
	public function setLogTableName($value): static
384
	{
385
		$this->_logTable = $value;
386
387
		return $this;
388
	}
389
390
	/**
391
	 * @return bool whether the log DB table should be automatically created if not exists. Defaults to true.
392
	 * @see setAutoCreateLogTable
393
	 */
394
	public function getAutoCreateLogTable()
395
	{
396
		return $this->_autoCreate;
397
	}
398
399
	/**
400
	 * @param bool $value whether the log DB table should be automatically created if not exists.
401
	 * @return static The current object.
402
	 * @see setLogTableName
403
	 */
404
	public function setAutoCreateLogTable($value): static
405
	{
406
		$this->_autoCreate = TPropertyValue::ensureBoolean($value);
407
408
		return $this;
409
	}
410
411
	/**
412
	 * @return ?float The seconds to retain.  Null is no end.
413
	 * @since 4.3.0
414
	 */
415
	public function getRetainPeriod(): ?float
416
	{
417
		return $this->_retainPeriod;
418
	}
419
420
	/**
421
	 * @param null|int|string $value Number of seconds or "PT" period time.
422
	 * @throws TConfigurationException when the time span is not a valid "PT" string.
423
	 * @return static The current object.
424
	 * @since 4.3.0
425
	 */
426
	public function setRetainPeriod($value): static
427
	{
428
		if (is_numeric($value)) {
429
			$value = (float) $value;
430
			if ($value === 0.0) {
0 ignored issues
show
introduced by
The condition $value === 0.0 is always false.
Loading history...
431
				$value = null;
432
			}
433
			$this->_retainPeriod = $value;
434
			return $this;
435
		}
436
		if (!($value = TPropertyValue::ensureString($value))) {
437
			$value = null;
438
		}
439
		$seconds = false;
440
		if ($value && ($seconds = static::timespanToSeconds($value)) === false) {
441
			throw new TConfigurationException('dblogroute_bad_retain_period', $value);
442
		}
443
444
		$this->_retainPeriod = ($seconds !== false) ? $seconds : $value;
0 ignored issues
show
Documentation Bug introduced by
It seems like $seconds !== false ? $seconds : $value can also be of type string. However, the property $_retainPeriod is declared as type double|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
introduced by
The condition $seconds !== false is always false.
Loading history...
445
446
		return $this;
447
	}
448
449
	/**
450
	 * @param string $timespan The time span to compute the number of seconds.
451
	 * @retutrn ?int the number of seconds of the time span.
452
	 * @since 4.3.0
453
	 */
454
	public static function timespanToSeconds(string $timespan): ?int
455
	{
456
		if (($interval = new \DateInterval($timespan)) === false) {
0 ignored issues
show
introduced by
The condition $interval = new DateInterval($timespan) === false is always false.
Loading history...
457
			return null;
458
		}
459
460
		$datetime1 = new \DateTime();
461
		$datetime2 = clone $datetime1;
462
		$datetime2->add($interval);
463
		$diff = $datetime2->getTimestamp() - $datetime1->getTimestamp();
464
		return $diff;
465
	}
466
467
}
468