Passed
Push — master ( d979ac...b22592 )
by Fabio
05:10
created

TDbLogRoute::setRetainPeriod()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

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