Passed
Pull Request — master (#983)
by
unknown
04:56
created

TLogger::log()   B

Complexity

Conditions 9
Paths 30

Size

Total Lines 36
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

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

249
			$app->attachEventHandler('onEndRequest', [$this, 'onFlushLogs'], /** @scrutinizer ignore-type */ 20);
Loading history...
250
			//$app->attachEventHandler(TProcessHelper::FX_TERMINATE_PROCESS, [$this, 'onFlushLogs'], 20);
251
			$this->_registered = true;
252
		}
253
	}
254
255
	/**
256
	 * Adds a single log item
257
	 * @param array $log The log item to log.
258
	 * @param bool $flush Allow a flush on adding if the log size is FlushCount, default true.
259
	 * @return ?float The time delta for PROFILE_END; and null in other cases.
260
	 * @since 4.2.3
261
	 */
262
	protected function addLog(array $log, bool $flush = true): ?float
263
	{
264
		$return = null;
265
		if ($this->_flushing) {
266
			$this->_flushingLog[] = $log;
267
			return $return;
268
		}
269
		if ($log[static::LOG_LEVEL] & TLogger::PROFILE_BEGIN_SELECT) {
270
			$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

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

414
		$this->raiseEvent('onFlushLogs', $this, /** @scrutinizer ignore-type */ $final);
Loading history...
415
		$this->deleteLogs();
416
		$this->_flushing = false;
417
		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

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