TLogger   F
last analyzed

Complexity

Total Complexity 125

Size/Duplication

Total Lines 712
Duplicated Lines 0 %

Test Coverage

Coverage 7.94%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 252
dl 0
loc 712
ccs 5
cts 63
cp 0.0794
rs 2
c 1
b 0
f 0
wmc 125

24 Methods

Rating   Name   Duplication   Size   Complexity  
B addLog() 0 36 8
A checkLogsSize() 0 4 4
B log() 0 36 9
A checkProfileLogsSize() 0 12 5
A onCollectLogs() 0 3 1
A getLogCount() 0 10 4
B onFlushLogs() 0 30 7
C deleteLogs() 0 38 15
A deleteProfileLogs() 0 8 1
A filterByTimeStamp() 0 6 2
C filterByCategories() 0 31 17
A orderByTimeStamp() 0 3 3
C getLogs() 0 39 15
A __construct() 0 10 2
A setFlushCount() 0 8 1
A getTraceLevel() 0 3 1
A getFlushCount() 0 3 1
A getLoggedProfileLogCount() 0 3 1
A filterByPID() 0 6 2
A ensureFlushing() 0 9 3
A mergeLogs() 0 6 2
A filterByLevels() 0 7 2
D filterByControl() 0 34 18
A setTraceLevel() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like TLogger often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TLogger, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * TLogger 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 Prado\Prado;
14
use Prado\TEventParameter;
15
use Prado\Web\UI\TControl;
16
17
/**
18
 * TLogger class.
19
 *
20
 * TLogger records log messages in memory and implements the methods to
21
 * retrieve the messages with filter conditions, including log levels,
22
 * log categories, and by control.
23
 *
24
 * Filtering categories and controls can use a '!' or '~' prefix and the element
25
 * will be excluded rather than included.  Using a trailing '*' will include
26
 * elements starting with the specified text.
27
 *
28
 * The log message, log level, log category (class), microtime Time stamp
29
 * memory used, control ID, traces, and process ID.
30
 *
31
 * @author Qiang Xue <[email protected]>
32
 * @author Brad Anderson <[email protected]>
33
 * @since 3.0
34
 */
35
class TLogger extends \Prado\TComponent
36
{
37
	/**
38
	 * Log levels.
39
	 */
40
	public const DEBUG = 0x01;
41
	public const INFO = 0x02;
42
	public const NOTICE = 0x04;
43
	public const WARNING = 0x08;
44
	public const ERROR = 0x10;
45
	public const ALERT = 0x20;
46
	public const FATAL = 0x40;
47
48
	public const PROFILE = 0x100;
49
	public const PROFILE_BEGIN = 0x300;
50
	public const PROFILE_END = 0x500;
51
52
	/**
53
	 * These are for selecting just the PROFILE_BEGIN and PROFILE_END logs without
54
	 * the PROFILE bit.
55
	 */
56
	public const PROFILE_BEGIN_SELECT = 0x200;
57
	public const PROFILE_END_SELECT = 0x400;
58
59
	public const LOGGED = 0x8000;
60
61
	/**
62
	 * Log Information Index.
63
	 */
64
	public const LOG_MESSAGE = 0;
65
	public const LOG_LEVEL = 1;
66
	public const LOG_CATEGORY = 2;
67
	public const LOG_TIME = 3;
68
	public const LOG_MEMORY = 4;
69 32
	public const LOG_CONTROL = 5;
70
	public const LOG_TRACES = 6;
71 32
	public const LOG_PID = 7;
72
73
	/**
74
	 * @var array log messages
75
	 */
76
	private array $_logs = [];
77
	/**
78 32
	 * @var array unmatched PROFILE_BEGIN log messages
79
	 * @since 4.3.0
80 32
	 */
81 32
	private array $_profileLogs = [];
82
	/**
83
	 * @var int the profileLogs count that are not static::LOGGED.
84
	 * @since 4.3.0
85
	 */
86
	private int $_profileLogsCount = 0;
87
	/**
88
	 * @var array The maintained Profile Begin times
89
	 * @since 4.3.0
90
	 */
91
	private array $_profileBeginTimes = [];
92
	/**
93
	 * @var ?int log levels (bits) to be filtered
94
	 */
95
	private ?int $_levels = null;
96
	/**
97
	 * @var ?array list of categories to be filtered
98
	 */
99
	private ?array $_categories = null;
100
	/**
101
	 * @var array list of control client ids to be filtered
102
	 */
103
	private ?array $_controls = null;
104
	/**
105
	 * @var ?float timestamp used to filter
106
	 */
107
	private ?float $_timestamp;
108
	/**
109
	 * @var ?int process id used to filter
110
	 * @since 4.3.0
111
	 */
112
	private ?int $_pid;
113
114
	/**
115
	 * @var int the number of logs before flushing.
116
	 * @since 4.3.0
117
	 */
118
	private int $_flushCount = 1000;
119
	/**
120
	 * @var bool is the logger flushing.
121
	 * @since 4.3.0
122
	 */
123
	private bool $_flushing = false;
124
	/**
125
	 * @var ?array any logged messages during flushing so they aren't flushed.
126
	 * @since 4.3.0
127
	 */
128
	private ?array $_flushingLog = null;
129
	/**
130
	 * @var int The depth of a trace, default 0 for no trace.
131
	 * @since 4.3.0
132
	 */
133
	private int $_traceLevel = 0;
134
	/**
135
	 * @var bool Is the logger Registered with onEndRequest
136
	 * @since 4.3.0
137
	 */
138
	private bool $_registered = false;
139
140
	/**
141
	 * @param bool $flushShutdown Should onFlushLogs be a register_shutdown_function.
142
	 * @since 4.3.0
143
	 */
144
	public function __construct(bool $flushShutdown = true)
145
	{
146
		if ($flushShutdown) {
147
			register_shutdown_function(function () {
148
				$this->onFlushLogs();
149
				// flush any logs in the shutdown function.
150
				register_shutdown_function([$this, 'onFlushLogs'], $this, true);
151
			});
152
		}
153
		parent::__construct();
154
	}
155
156
	/**
157
	 * @return int The number of logs before triggering {@see self::onFlushLogs()}
158
	 * @since 4.3.0
159
	 */
160
	public function getFlushCount(): int
161
	{
162
		return $this->_flushCount;
163
	}
164
165
	/**
166
	 * @param int $value the number of logs before triggering {@see self::onFlushLogs()}
167
	 * @return static $this
168
	 * @since 4.3.0
169
	 */
170
	public function setFlushCount(int $value): static
171
	{
172
		$this->_flushCount = $value;
173
174
		$this->checkProfileLogsSize();
175
		$this->checkLogsSize();
176
177
		return $this;
178
	}
179
180
	/**
181
	 * @return int How much debug trace stack information to include.
182
	 * @since 4.3.0
183
	 */
184
	public function getTraceLevel(): int
185
	{
186
		return $this->_traceLevel;
187
	}
188
189
	/**
190
	 * @param int $value How much debug trace stack information to include.
191
	 * @return static $this
192
	 * @since 4.3.0
193
	 */
194
	public function setTraceLevel(int $value): static
195
	{
196
		$this->_traceLevel = $value;
197
198
		return $this;
199
	}
200
201
	/**
202
	 * This returns the number of logs being held.  When the $selector the following
203
	 * is returned:
204
	 *   - null is the ~ number of logs a route will receive
205
	 *   - 0 is the ~ number of logs to be collected before removing static::LOGGGED logs
206
	 *   - true is the number of logs without profile begin
207
	 *   - false is the number of profile begin logs
208
	 *
209
	 * When a PROFILE_BEGIN is flushed without a paired PROFILE_END, then it is logged
210
	 * and flagged as static::LOGGED.  These log items will be resent until and with
211
	 * a paired PROFILE_END.  The PROFILE_BEGIN are relogged to computing timing but are
212
	 * removed before being processed by the log route.
213
	 *
214
	 * @param ?bool $selector Which list of logs to count. Default null for both.
215
	 * @return int The number of logs.
216
	 * @since 4.3.0
217
	 */
218
	public function getLogCount(null|bool|int $selector = null): int
219
	{
220
		if ($selector === null) {// logs @TLogRoute::processLogs after removing logged elements.
221
			return count($this->_logs) + $this->_profileLogsCount;
222
		} elseif ($selector === 0) {// logs @TLogRoute::collectLogs, from TLogger::getLogs
223
			return count($this->_logs) + count($this->_profileLogs);
224
		} elseif ($selector) {
225
			return count($this->_logs);
226
		} //elseif false
227
		return count($this->_profileLogs);
228
	}
229
230
	/**
231
	 * This is the number of Profile Begin Logs that have been logged.
232
	 * @return int The Profile Logs already logged.
233
	 * @since 4.3.0
234
	 */
235
	public function getLoggedProfileLogCount(): int
236
	{
237
		return count($this->_profileLogs) - $this->_profileLogsCount;
238
	}
239
240
	/**
241
	 * Ensures that the logger is registered with the Application onEndRequest.
242
	 * @since 4.3.0
243
	 */
244
	protected function ensureFlushing()
245
	{
246
		if ($this->_registered) {
247
			return;
248
		}
249
250
		if ($app = Prado::getApplication()) {
251
			$app->attachEventHandler('onEndRequest', [$this, 'onFlushLogs'], 20);
0 ignored issues
show
Bug introduced by
20 of type integer is incompatible with the type Prado\numeric|null expected by parameter $priority of Prado\TComponent::attachEventHandler(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

251
			$app->attachEventHandler('onEndRequest', [$this, 'onFlushLogs'], /** @scrutinizer ignore-type */ 20);
Loading history...
252
			$this->_registered = true;
253
		}
254
	}
255
256
	/**
257
	 * Adds a single log item
258
	 * @param array $log The log item to log.
259
	 * @param bool $flush Allow a flush on adding if the log size is FlushCount, default true.
260
	 * @return ?float The time delta for PROFILE_END; and null in other cases.
261
	 * @since 4.3.0
262
	 */
263
	protected function addLog(array $log, bool $flush = true): ?float
264
	{
265
		$return = null;
266
		if ($this->_flushing) {
267
			$this->_flushingLog[] = $log;
268
			return $return;
269
		}
270
		if ($log[static::LOG_LEVEL] & TLogger::PROFILE_BEGIN_SELECT) {
271
			$profileName = $this->dyProfilerRename('[' . $log[static::LOG_MESSAGE] . '][' . $log[static::LOG_PID] . ']');
0 ignored issues
show
Bug introduced by
The method dyProfilerRename() does not exist on Prado\Util\TLogger. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

271
			/** @scrutinizer ignore-call */ 
272
   $profileName = $this->dyProfilerRename('[' . $log[static::LOG_MESSAGE] . '][' . $log[static::LOG_PID] . ']');
Loading history...
272
			$add = !isset($this->_profileLogs[$profileName]);
273
			$this->_profileLogs[$profileName] = $log;
274
			$this->_profileBeginTimes[$profileName] = $log[static::LOG_TIME];
275
			if ($add) {
276
				$this->_profileLogsCount++;
277
				$this->checkProfileLogsSize($flush);
278
			}
279
		} else {
280
			if ($log[static::LOG_LEVEL] & TLogger::PROFILE_END_SELECT) { // Move ProfileBegin to the primary log.
281
				$profileName = $this->dyProfilerRename('[' . $log[static::LOG_MESSAGE] . '][' . $log[static::LOG_PID] . ']');
282
				if (isset($this->_profileLogs[$profileName])) {
283
					$this->_logs[] = $this->_profileLogs[$profileName];
284
285
					if (!($this->_profileLogs[$profileName][static::LOG_LEVEL] & static::LOGGED)) {
286
						$this->_profileLogsCount--;
287
					}
288
289
					unset($this->_profileLogs[$profileName]);
290
				}
291
				if (isset($this->_profileBeginTimes[$profileName])) {
292
					$return = $log[static::LOG_TIME] - $this->_profileBeginTimes[$profileName];
293
				}
294
			}
295
			$this->_logs[] = $log;
296
			$this->checkLogsSize($flush);
297
		}
298
		return $return;
299
	}
300
301
	/**
302
	 * When the ProfileLog Count is equal or more than the FlushCount, then the Retained
303
	 * profile logs are deleted and a WARNING is logged.
304
	 * @param bool $flush Is the log being flushing, default true.
305
	 * @since 4.3.0
306
	 */
307
	protected function checkProfileLogsSize(bool $flush = true)
308
	{
309
		if ($this->_flushCount && $this->getLogCount(false) >= $this->_flushCount) {
310
			$this->deleteProfileLogs();
311
			$saved = null;
312
			if (!$flush) {
313
				$saved = $this->_flushCount;
314
				$this->_flushCount = 0;
315
			}
316
			$this->log("Too many retained profiler logs", static::WARNING, self::class . '\Profiler');
317
			if (!$flush) {
318
				$this->_flushCount = $saved;
319
			}
320
		}
321
	}
322
323
	/**
324
	 * If we allow a flush (not a bulk logging), and the log count is equal to or more
325
	 * than the FlushCount then we flush the logs.
326
	 * @param bool $flush Is the log being flushing, default true.
327
	 * @since 4.3.0
328
	 */
329
	protected function checkLogsSize(bool $flush = true)
330
	{
331
		if ($flush && $this->_flushCount && $this->getLogCount() >= $this->_flushCount) {
332
			$this->onFlushLogs();
333
		}
334
	}
335
336
	/**
337
	 * Logs a message.
338
	 * Messages logged by this method may be retrieved via {@see getLogs}.
339
	 * @param mixed $token message to be logged
340
	 * @param int $level level of the message. Valid values include
341
	 * TLogger::DEBUG, TLogger::INFO, TLogger::NOTICE, TLogger::WARNING,
342
	 * TLogger::ERROR, TLogger::ALERT, TLogger::FATAL, TLogger::PROFILE,
343
	 * TLogger::PROFILE_BEGIN, TLogger::PROFILE_END
344
	 * @param ?string $category category of the message
345
	 * @param null|string|TControl $ctl control of the message
346
	 * @return ?float The time delta for PROFILE_END; and null in other cases.
347
	 */
348
	public function log(mixed $token, int $level, ?string $category = null, mixed $ctl = null): ?float
349
	{
350
		$this->ensureFlushing();
351
352
		if ($ctl instanceof TControl) {
353
			$ctl = $ctl->getClientID();
354
		} elseif (!is_string($ctl)) {
355
			$ctl = null;
356
		}
357
358
		if ($category === null) {
359
			$category = Prado::callingObject()::class;
360
		}
361
362
		$traces = null;
363
		if ($this->_traceLevel > 0) {
364
365
			$traces = [];
366
			$count = 0;
367
			$allTraces = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
368
			array_pop($allTraces); // remove the last trace since it would be the entry script, not very useful
369
370
			foreach ($allTraces as $trace) {
371
				if (isset($trace['file'], $trace['line']) && strpos($trace['file'], PRADO_DIR) !== 0) {
372
373
					unset($trace['object'], $trace['args']);
374
					$traces[] = $trace;
375
376
					if (++$count >= $this->_traceLevel) {
377
						break;
378
					}
379
				}
380
			}
381
		}
382
383
		return $this->addLog([$token, $level, $category, microtime(true), memory_get_usage(), $ctl, $traces, getmypid()]);
384
	}
385
386
387
	/**
388
	 * This event collects any logs from other aspects of the application that may
389
	 * not be able to directly log to the TLogger.
390
	 * @param bool $final Is the final collection, default false.
391
	 * @since 4.3.0
392
	 */
393
	public function onCollectLogs(bool $final = false)
394
	{
395
		return $this->raiseEvent('onCollectLogs', $this, $final);
0 ignored issues
show
Bug introduced by
$final of type boolean is incompatible with the type Prado\TEventParameter expected by parameter $param of Prado\TComponent::raiseEvent(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

395
		return $this->raiseEvent('onCollectLogs', $this, /** @scrutinizer ignore-type */ $final);
Loading history...
396
	}
397
398
	/**
399
	 * This is an Event and an event handler.  When {@see Application::onEndRequest()}
400
	 * is raised and as a `register_shutdown_function` function, this is called.
401
	 * On calling, this raises the event `onFlushLogs`.
402
	 *
403
	 * @param mixed $sender the sender of the raised event.
404
	 * @param mixed $final true on `register_shutdown_function`.
405
	 * @since 4.3.0
406
	 */
407
	public function onFlushLogs(mixed $sender = null, mixed $final = null)
408
	{
409
		if ($this->_flushing) {
410
			return;
411
		}
412
413
		if (is_bool($sender)) {
414
			$final = $sender;
415
			$sender = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $sender is dead and can be removed.
Loading history...
416
		}
417
		if (!is_bool($final)) {
418
			$final = false;
419
		}
420
		$this->onCollectLogs($final);
421
422
		$this->_flushing = true;
423
		$this->_flushingLog = [];
424
		$this->raiseEvent('onFlushLogs', $this, $final);
0 ignored issues
show
Bug introduced by
$final of type boolean is incompatible with the type Prado\TEventParameter expected by parameter $param of Prado\TComponent::raiseEvent(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

424
		$this->raiseEvent('onFlushLogs', $this, /** @scrutinizer ignore-type */ $final);
Loading history...
425
		$this->deleteLogs();
426
		$this->_flushing = false;
427
		if (count($this->_flushingLog)) {
0 ignored issues
show
Bug introduced by
It seems like $this->_flushingLog can also be of type null; however, parameter $value of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

427
		if (count(/** @scrutinizer ignore-type */ $this->_flushingLog)) {
Loading history...
428
			foreach ($this->_flushingLog as $log) {
429
				$this->addLog($log, false); // $final = false to stop any possible recursion w/ low flushCount.
430
			}
431
		}
432
		$this->_flushingLog = null;
433
434
		$this->_profileLogsCount = 0;
435
		foreach (array_keys($this->_profileLogs) as $key) {
436
			$this->_profileLogs[$key][static::LOG_LEVEL] |= static::LOGGED;
437
		}
438
	}
439
440
	/**
441
	 * Retrieves log messages.
442
	 * Messages may be filtered by log levels and/or categories and/or control client ids and/or timestamp.
443
	 * A level filter is specified by an integer, whose bits indicate the levels interested.
444
	 * For example, (TLogger::INFO | TLogger::WARNING) specifies INFO and WARNING levels.
445
	 * A category filter is specified by an array of categories to filter.
446
	 * A message whose category name starts with any filtering category
447
	 * will be returned. For example, a category filter array('Prado\Web','Prado\IO')
448
	 * will return messages under categories such as 'Prado\Web', 'Prado\IO',
449
	 * 'Prado\Web\UI', 'Prado\Web\UI\WebControls', etc.
450
	 * A control client id filter is specified by an array of control client id
451
	 * A message whose control client id starts with any filtering naming panels
452
	 * will be returned. For example, a category filter array('ctl0_body_header',
453
	 * 'ctl0_body_content_sidebar')
454
	 * will return messages under categories such as 'ctl0_body_header', 'ctl0_body_content_sidebar',
455
	 * 'ctl0_body_header_title', 'ctl0_body_content_sidebar_savebutton', etc.
456
	 * A timestamp filter is specified as an interger or float number.
457
	 * A message whose registered timestamp is less or equal the filter value will be returned.
458
	 * Level filter, category filter, control filter and timestamp filter are combinational, i.e., only messages
459
	 * satisfying all filter conditions will they be returned.
460
	 * @param ?int $levels level filter
461
	 * @param null|array|string $categories category filter
462
	 * @param null|array|string $controls control filter
463
	 * @param null|float|int $timestamp filter
464
	 * @param ?int $pid
465
	 * @return array list of messages. Each array elements represents one message
466
	 * with the following structure:
467
	 * array(
468
	 *   [0] => message
469
	 *   [1] => level
470
	 *   [2] => category
471
	 *   [3] => timestamp (by microtime(true), float number)
472
	 *   [4] => memory in bytes
473
	 *   [5] => control client id; null when absent
474
	 *      @ since 4.3.0:
475
	 *   [6] => traces, when configured; null when absent
476
	 *   [7] => process id)
477
	 */
478
	public function getLogs(?int $levels = null, null|array|string $categories = null, null|array|string $controls = null, null|int|float $timestamp = null, ?int $pid = null)
479
	{
480
		$this->_levels = $levels;
481
		if (!empty($categories) && !is_array($categories)) {
0 ignored issues
show
introduced by
The condition is_array($categories) is always true.
Loading history...
482
			$categories = [$categories];
483
		}
484
		$this->_categories = $categories;
485
		if (!empty($controls) && !is_array($controls)) {
0 ignored issues
show
introduced by
The condition is_array($controls) is always true.
Loading history...
486
			$controls = [$controls];
487
		}
488
		$this->_controls = $controls;
489
		$this->_timestamp = $timestamp;
490
		$this->_pid = $pid;
491
492
		$logs = array_merge($this->_profileLogs, $this->_logs);
493
		if (empty($levels) && empty($categories) && empty($controls) && $timestamp === null && $pid === null) {
494
			usort($logs, [$this, 'orderByTimeStamp']);
495
			return $logs;
496
		}
497
498
		if (!empty($levels)) {
499
			$logs = array_filter($logs, [$this, 'filterByLevels']);
500
		}
501
		if (!empty($categories)) {
502
			$logs = array_filter($logs, [$this, 'filterByCategories']);
503
		}
504
		if (!empty($controls)) {
505
			$logs = array_filter($logs, [$this, 'filterByControl']);
506
		}
507
		if ($timestamp !== null) {
508
			$logs = array_filter($logs, [$this, 'filterByTimeStamp']);
509
		}
510
		if ($pid !== null) {
511
			$logs = array_filter($logs, [$this, 'filterByPID']);
512
		}
513
514
		usort($logs, [$this, 'orderByTimeStamp']);
515
516
		return array_values($logs);
517
	}
518
519
	/**
520
	 * This merges a set of logs with the current running logs.
521
	 * @param array $logs the logs elements to insert.
522
	 * @since 4.3.0
523
	 */
524
	public function mergeLogs(array $logs)
525
	{
526
		$count = count($logs);
527
		foreach ($logs as $log) {
528
			$log[static::LOG_LEVEL] &= ~TLogger::LOGGED;
529
			$this->addLog($log, !(--$count));
530
		}
531
	}
532
533
	/**
534
	 * Deletes log messages from the queue.
535
	 * Messages may be filtered by log levels and/or categories and/or control client ids and/or timestamp.
536
	 * A level filter is specified by an integer, whose bits indicate the levels interested.
537
	 * For example, (TLogger::INFO | TLogger::WARNING) specifies INFO and WARNING levels.
538
	 * A category filter is specified by an array of categories to filter.
539
	 * A message whose category name starts with any filtering category
540
	 * will be deleted. For example, a category filter array('Prado\Web','Prado\IO')
541
	 * will delete messages under categories such as 'Prado\Web', 'Prado\IO',
542
	 * 'Prado\Web\UI', 'Prado\Web\UI\WebControls', etc.
543
	 * A control client id filter is specified by an array of control client id
544
	 * A message whose control client id starts with any filtering naming panels
545
	 * will be deleted. For example, a category filter array('ctl0_body_header',
546
	 * 'ctl0_body_content_sidebar')
547
	 * will delete messages under categories such as 'ctl0_body_header', 'ctl0_body_content_sidebar',
548
	 * 'ctl0_body_header_title', 'ctl0_body_content_sidebar_savebutton', etc.
549
	 * A timestamp filter is specified as an interger or float number.
550
	 * A message whose registered timestamp is less or equal the filter value will be returned.
551
	 * Level filter, category filter, control filter and timestamp filter are combinational, i.e., only messages
552
	 * satisfying all filter conditions will they be returned.
553
	 * @param null|int $levels level filter
554
	 * @param null|array|string $categories category filter
555
	 * @param null|array|string $controls control filter
556
	 * @param null|float|int $timestamp timestamp filter
557
	 * @param ?int $pid
558
	 * @return array The logs being delete are returned.
559
	 */
560
	public function deleteLogs(?int $levels = null, null|string|array $categories = null, null|string|array $controls = null, null|int|float $timestamp = null, ?int $pid = null): array
561
	{
562
		$this->_levels = $levels;
563
		if (!empty($categories) && !is_array($categories)) {
0 ignored issues
show
introduced by
The condition is_array($categories) is always true.
Loading history...
564
			$categories = [$categories];
565
		}
566
		$this->_categories = $categories;
567
		if (!empty($controls) && !is_array($controls)) {
0 ignored issues
show
introduced by
The condition is_array($controls) is always true.
Loading history...
568
			$controls = [$controls];
569
		}
570
		$this->_controls = $controls;
571
		$this->_timestamp = $timestamp;
572
		$this->_pid = $pid;
573
574
		if (empty($levels) && empty($categories) && empty($controls) && $timestamp === null && $pid === null) {
575
			$logs = $this->_logs;
576
			$this->_logs = [];
577
			return $logs;
578
		}
579
		$logs = $this->_logs;
580
		if (!empty($levels)) {
581
			$logs = (array_filter($logs, [$this, 'filterByLevels']));
582
		}
583
		if (!empty($categories)) {
584
			$logs = (array_filter($logs, [$this, 'filterByCategories']));
585
		}
586
		if (!empty($controls)) {
587
			$logs = (array_filter($logs, [$this, 'filterByControl']));
588
		}
589
		if ($timestamp !== null) {
590
			$logs = (array_filter($logs, [$this, 'filterByTimeStamp']));
591
		}
592
		if ($pid !== null) {
593
			$logs = array_filter($logs, [$this, 'filterByPID']);
594
		}
595
		$this->_logs = array_values(array_diff_key($this->_logs, $logs));
596
597
		return array_values($logs);
598
	}
599
600
	/**
601
	 * Deletes the retained Profile Begin logs.
602
	 * @return array The deleted Profile Begin Logs.
603
	 * @since 4.3.0
604
	 */
605
	public function deleteProfileLogs(): array
606
	{
607
		$profileLogs = array_values($this->_profileLogs);
608
		$this->_profileLogs = [];
609
		$this->_profileLogsCount = 0;
610
		$this->_profileBeginTimes = [];
611
612
		return $profileLogs;
613
	}
614
615
	/**
616
	 * Order function used by {@see static::getLogs()}.
617
	 * @param mixed $a First element to compare.
618
	 * @param mixed $b Second element to compare.
619
	 * @return int The order between the two elements.
620
	 * @since 4.3.0
621
	 */
622
	private function orderByTimeStamp($a, $b): int
623
	{
624
		return ($a[static::LOG_TIME] != $b[static::LOG_TIME]) ? ($a[static::LOG_TIME] <= $b[static::LOG_TIME]) ? -1 : 1 : 0;
625
	}
626
627
	/**
628
	 * Filter function used by {@see static::getLogs()}.
629
	 * @param array $log element to be filtered
630
	 * @return bool retain the element.
631
	 */
632
	private function filterByCategories($log): bool
633
	{
634
		$open = true;
635
		$match = false;
636
		$exclude = false;
637
638
		foreach ($this->_categories as $category) {
639
			if (empty($category)) {
640
				$category = '';
641
			}
642
			$c = $category[0] ?? 0;
643
			if ($c === '!' || $c === '~') {
644
				if (!$exclude) {
645
					$category = substr($category, 1);
646
					if ($log[static::LOG_CATEGORY] === $category || str_ends_with($category, '*') && strpos($log[static::LOG_CATEGORY], rtrim($category, '*')) === 0) {
647
						$exclude = true;
648
					}
649
				}
650
			} elseif (!$match) {
651
				$open = false;
652
				if (($log[static::LOG_CATEGORY] === $category || str_ends_with($category, '*') && strpos($log[static::LOG_CATEGORY], rtrim($category, '*')) === 0)) {
653
					$match = true;
654
				}
655
			}
656
657
			if ($match && $exclude) {
658
				break;
659
			}
660
		}
661
662
		return ($open || $match) && !$exclude;
663
	}
664
665
	/**
666
	 * Filter function used by {@see static::getLogs()}
667
	 * @param array $log element to be filtered
668
	 * @return bool retain the element.
669
	 */
670
	private function filterByLevels($log): bool
671
	{
672
		// element 1 are the levels
673
		if ($log[static::LOG_LEVEL] & $this->_levels) {
674
			return true;
675
		} else {
676
			return false;
677
		}
678
	}
679
680
	/**
681
	 * Filter function used by {@see static::getLogs()}
682
	 * @param array $log element to be filtered
683
	 * @return bool retain the element.
684
	 */
685
	private function filterByControl($log): bool
686
	{
687
		$open = true;
688
		$match = false;
689
		$exclude = false;
690
691
		if (empty($log[static::LOG_CONTROL])) {
692
			$log[static::LOG_CONTROL] = '';
693
		}
694
695
		foreach ($this->_controls as $control) {
696
			if (empty($control)) {
697
				$control = '';
698
			}
699
			$c = $control[0] ?? 0;
700
			if ($c === '!' || $c === '~') {
701
				if (!$exclude) {
702
					$control = substr($control, 1);
703
					if ($log[static::LOG_CONTROL] === $control || str_ends_with($control, '*') && strpos($log[static::LOG_CONTROL], rtrim($control, '*')) === 0) {
704
						$exclude = true;
705
					}
706
				}
707
			} elseif (!$match) {
708
				$open = false;
709
				if (($log[static::LOG_CONTROL] === $control || str_ends_with($control, '*') && strpos($log[static::LOG_CONTROL], rtrim($control, '*')) === 0)) {
710
					$match = true;
711
				}
712
			}
713
			if ($match && $exclude) {
714
				break;
715
			}
716
		}
717
718
		return ($open || $match) && !$exclude;
719
	}
720
721
	/**
722
	 * Filter function used by {@see static::getLogs()}
723
	 * @param array $log element to be filtered
724
	 * @return bool retain the element.
725
	 */
726
	private function filterByTimeStamp($log): bool
727
	{
728
		if ($log[static::LOG_TIME] <= $this->_timestamp) {
729
			return true;
730
		} else {
731
			return false;
732
		}
733
	}
734
735
	/**
736
	 * Filter function used by {@see static::getLogs()}
737
	 * @param array $log element to be filtered
738
	 * @return bool retain the element.
739
	 * @since 4.3.0
740
	 */
741
	private function filterByPID($log): bool
742
	{
743
		if ($log[static::LOG_PID] === $this->_pid) {
744
			return true;
745
		} else {
746
			return false;
747
		}
748
	}
749
}
750