Test Failed
Push — master ( d27579...352fdc )
by Jan
02:25
created

Profiler::getCollection()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 5
cts 5
cp 1
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 0
crap 2
1
<?php
2
namespace Xicrow\PhpDebug;
3
4
/**
5
 * Class Profiler
6
 *
7
 * @package Xicrow\PhpDebug
8
 */
9
abstract class Profiler {
10
	/**
11
	 * @var Collection
12
	 */
13
	public static $collections = [];
14
15
	/**
16
	 * @return Collection
17
	 */
18 1
	public function getCollection() {
19 1
		$className = static::class;
20 1
21 1
		if (!isset(static::$collections[$className])) {
22 1
			static::$collections[$className] = new Collection();
23
		}
24
25
		return static::$collections[$className];
26
	}
27
28
	/**
29
	 * @return int|float
30
	 * @codeCoverageIgnore
31
	 */
32
	public static function getMetric() {
33
		// Since we're not able to create abstract static functions, this'll do
34
		trigger_error('Implement getMetric() in child class');
35
	}
36
37
	/**
38
	 * @param int|float $metric
39
	 *
40
	 * @return mixed
41
	 * @codeCoverageIgnore
42
	 */
43
	public static function getMetricFormatted($metric) {
44
		// Since we're not able to create abstract static functions, this'll do
45
		trigger_error('Implement getMetricFormatted() in child class');
46
	}
47
48
	/**
49
	 * @param int|float $start
50
	 * @param int|float $stop
51
	 *
52
	 * @return float|int
53
	 * @codeCoverageIgnore
54
	 */
55
	public static function getMetricResult($start, $stop) {
56
		// Since we're not able to create abstract static functions, this'll do
57
		trigger_error('Implement getMetricResult() in child class');
58
	}
59
60
	/**
61
	 * @param float|int $result
62
	 *
63
	 * @return mixed
64
	 * @codeCoverageIgnore
65
	 */
66
	public static function getMetricResultFormatted($result) {
67
		// Since we're not able to create abstract static functions, this'll do
68
		trigger_error('Implement getMetricResultFormatted() in child class');
69
	}
70
71
	/**
72
	 * @param string|null $key
73 9
	 * @param array       $data
74 9
	 *
75
	 * @return bool
76
	 */
77 9
	public static function add($key = null, $data = []) {
78
		// If no key is given
79 4
		if (is_null($key)) {
80 4
			// Set key to file and line
81
			$key = Debugger::getCalledFrom(2);
82
		}
83 9
84 9
		// Make sure parent is set
85 9
		if (!isset($data['parent'])) {
86
			$data['parent'] = static::getLastItemName('started');
87
		}
88 9
89 9
		// Make sure level is set
90 9
		if (!isset($data['level'])) {
91 1
			$data['level'] = 0;
92 1
			if (isset($data['parent']) && $parent = static::getCollection()->get($data['parent'])) {
93 9
				$data['level'] = ($parent['level'] + 1);
94
			}
95
		}
96 9
97
		// Add item to collection
98
		return static::getCollection()->add($key, $data);
99
	}
100
101
	/**
102
	 * @param string|null $key
103
	 *
104 4
	 * @return bool
105
	 */
106 4
	public static function start($key = null) {
107
		// Get metric
108 4
		$metric = static::getMetric();
109
110
		// Add new item
111 4
		static::add($key, [
112 4
			'start_value'       => $metric,
113 4
			'start_time'        => microtime(true),
114 4
			'start_called_from' => Debugger::getCalledFrom(1)
115 4
		]);
116
117
		// Return true
118 4
		return true;
119
	}
120
121
	/**
122
	 * @param string|null $key
123
	 *
124
	 * @return bool
125
	 */
126 3
	public static function stop($key = null) {
127
		// Get metric
128 3
		$metric = static::getMetric();
129
130 3
		// If no key is given
131
		if (is_null($key)) {
132
			// Get key of the last started item
133 3
			$key = static::getLastItemName('started');
134
		}
135 1
136 1
		// Check for key duplicates, and find the first one not stopped
137
		$originalName = $key;
138
		$i            = 1;
139 3
		while (static::getCollection()->exists($key)) {
140 3
			if (empty(static::getCollection()->get($key)['stop_value'])) {
141 3
				break;
142 3
			}
143 3
144
			$key = $originalName . ' #' . ($i + 1);
145
146 1
			$i++;
147
		}
148 1
149 1
		// If item exists in collection
150
		if (static::getCollection()->exists($key)) {
151
			// Update the item
152 3
			static::getCollection()->update($key, [
153
				'stop_value'       => $metric,
154 3
				'stop_time'        => microtime(true),
155 3
				'stop_called_from' => Debugger::getCalledFrom(1)
156 3
			]);
157 3
158 3
			return true;
159
		}
160 3
161
		return false;
162
	}
163 1
164
	/**
165
	 * @param string|null $key
166
	 * @param int|null    $startValue
167
	 * @param int|null    $stopValue
168
	 *
169
	 * @return bool
170
	 */
171
	public static function custom($key = null, $startValue = null, $stopValue = null) {
172
		// Set data for the item
173 4
		$data = [];
174 4
		if (!is_null($startValue)) {
175
			$data['start_value']       = $startValue;
176
			$data['start_time']        = microtime(true);
177 4
			$data['start_called_from'] = Debugger::getCalledFrom(1);
178 4
		}
179 4
		if (!is_null($stopValue)) {
180 4
			$data['stop_value']       = $stopValue;
181 4
			$data['stop_time']        = microtime(true);
182 4
			$data['stop_called_from'] = Debugger::getCalledFrom(1);
183 4
		}
184 4
185 4
		// Add item to collection
186 4
		return static::add($key, $data);
187 4
	}
188
189
	/**
190 4
	 * @param string|null           $key
191
	 * @param string|array|\Closure $callback
192
	 * @param array                 ...$callbackParams
193
	 *
194
	 * @return mixed
195
	 */
196
	public static function callback($key = null, $callback, ...$callbackParams) {
197
		// Get key if no key is given
198
		if (is_null($key)) {
199
			if (is_string($callback)) {
200 1
				$key = $callback;
201 1
			} elseif (is_array($callback)) {
202
				$keyArr = [];
203
				foreach ($callback as $k => $v) {
204 1
					if (is_string($v)) {
205 1
						$keyArr[] = $v;
206 1
					} elseif (is_object($v)) {
207 1
						$keyArr[] = get_class($v);
208 1
					}
209 1
				}
210 1
211 1
				$key = implode('', $keyArr);
212 1
				if (count($keyArr) > 1) {
213 1
					$method = array_pop($keyArr);
214 1
					$key    = implode('/', $keyArr);
215 1
					$key .= '::' . $method;
216
				}
217 1
218 1
				unset($keyArr, $method);
219 1
			} elseif (is_object($callback) && $callback instanceof \Closure) {
220 1
				$key = 'closure';
221 1
			}
222 1
			$key = 'callback: ' . $key;
223
		}
224 1
225 1
		// Set default return value
226 1
		$returnValue = true;
227 1
228 1
		// Set error handler, to convert errors to exceptions
229 1
		set_error_handler(function ($errno, $errstr, $errfile, $errline, array $errcontext) {
0 ignored issues
show
Unused Code introduced by
The parameter $errcontext is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
230
			throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
231
		});
232 1
233
		try {
234
			// Start output buffer to capture any output
235 1
			ob_start();
236 1
237 1
			// Start profiler
238
			static::start($key);
239
240
			// Execute callback, and get result
241 1
			$callbackResult = call_user_func_array($callback, $callbackParams);
242
243
			// Stop profiler
244 1
			static::stop($key);
245
246
			// Get and clean output buffer
247 1
			$callbackOutput = ob_get_clean();
248
		} catch (\ErrorException $callbackException) {
249
			// Stop and clean output buffer
250 1
			ob_end_clean();
251
252
			// Show error message
253 1
			Debugger::output('Invalid callback sent to Profiler::callback: ' . str_replace('callback: ', '', $key));
254 1
255
			// Clear the item from the collection
256 1
			static::getCollection()->clear($key);
257
258
			// Clear callback result and output
259 1
			unset($callbackResult, $callbackOutput);
260
261
			// Set return value to false
262 1
			$returnValue = false;
263
		}
264
265 1
		// Restore error handler
266
		restore_error_handler();
267
268 1
		// Return result, output or true
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
269
		return (isset($callbackResult) ? $callbackResult : (!empty($callbackOutput) ? $callbackOutput : $returnValue));
270
	}
271
272 1
	/**
273
	 * @param string|null $key
274
	 * @param array       $options
275 1
	 *
276
	 * @codeCoverageIgnore
277
	 */
278
	public static function show($key = null, $options = []) {
279
		$output = static::getStats($key, $options);
280
281
		if (!empty($output)) {
282
			Debugger::output($output);
283
		}
284
	}
285
286
	/**
287
	 * @param array $options
288
	 *
289
	 * @codeCoverageIgnore
290
	 */
291
	public static function showAll($options = []) {
292
		// Merge options with default options
293
		$options = array_merge([
294
			// Sort field: index|key|start|stop|result
295
			'sort'       => 'index',
296
			// Sort order: asc|desc
297
			'sort_order' => 'asc'
298
		], $options);
299
300
		// Available sort options
301
		$sortOptions = ['index', 'key', 'start_value', 'stop_value', 'result'];
302
303
		// Get copy of collection
304
		$collection = clone static::getCollection();
305
306
		// Add result to items if needed
307
		foreach ($collection as $key => $item) {
308
			if (!isset($item['result'])) {
309
				// If item is not started
310
				if (!isset($item['start_value'])) {
311
					$collection->clear($key);
312
					continue;
313
				}
314
315
				// If item is not stopped
316
				if (!isset($item['stop_value'])) {
317
					// Stop the item
318
					static::stop($key);
319
320
					$item = static::getCollection()->get($key);
321
				}
322
323
				$collection->update($key, ['result' => static::getMetricResult($item['start_value'], $item['stop_value'])]);
324
			}
325
		}
326
327
		// If valid sort option is given
328
		if (in_array($options['sort'], $sortOptions)) {
329
			// Sort collection
330
			$collection->sort($options['sort'], $options['sort_order']);
331
		}
332
333
		// Output items
334
		$output = '';
335
		foreach ($collection as $key => $item) {
336
			$output .= (!empty($output) ? "\n" : '');
337
			$output .= static::getStats($key, $options);
338
		}
339
		Debugger::output($output);
340
	}
341
342
	/**
343
	 * @param string|null $key
344
	 * @param array       $options
345
	 *
346
	 * @return string
347
	 */
348
	public static function getStats($key = null, $options = []) {
349
		// Merge options with default options
350
		$options = array_merge([
351
			// Show nested (boolean)
352
			'nested'          => true,
353
			// Prefix for nested items (string)
354
			'nested_prefix'   => '|-- ',
355
			// Show in one line (boolean)
356
			'oneline'         => true,
357
			// If oneline, max key length (int)
358 1
			'oneline_length'  => 100,
359 1
			// Show start stop for the item (boolean)
360
			'show_start_stop' => false
361
		], $options);
362 1
363
		// Get key of the last stopped item, if no key is given
364 1
		$key = (is_null($key) ? static::getLastItemName('stopped') : $key);
365
366 1
		// If item does not exist
367
		if (!static::getCollection()->exists($key)) {
368 1
			return 'Unknow item in with key: ' . $key;
369
		}
370 1
371
		// Get item
372
		$item = static::getCollection()->get($key);
373 1
374
		// Return stats
375
		return ($options['oneline'] ? self::getStatsOneline($item, $options) : self::getStatsMultiline($item, $options));
0 ignored issues
show
Bug introduced by
It seems like $item defined by static::getCollection()->get($key) on line 372 can also be of type boolean; however, Xicrow\PhpDebug\Profiler::getStatsOneline() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Bug introduced by
It seems like $item defined by static::getCollection()->get($key) on line 372 can also be of type boolean; however, Xicrow\PhpDebug\Profiler::getStatsMultiline() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
376 1
	}
377
378
	/**
379 1
	 * @param array $item
380 1
	 * @param array $options
381
	 *
382
	 * @return string
383
	 */
384 1
	public static function getStatsOneline($item, $options = []) {
385
		// Merge options with default options
386
		$options = array_merge([
387 1
			// Show nested (boolean)
388
			'nested'          => true,
389
			// Prefix for nested items (string)
390
			'nested_prefix'   => '|-- ',
391
			// If oneline, max key length (int)
392
			'oneline_length'  => 100,
393
			// Show start stop for the item (boolean)
394
			'show_start_stop' => false
395
		], $options);
396 2
397
		// Get item result
398 2
		$itemResult = 'N/A';
399 View Code Duplication
		if (isset($item['start_value']) && isset($item['stop_value'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
400 2
			$itemResult = static::getMetricResult($item['start_value'], $item['stop_value']);
401
		}
402 2
403
		// Variable for output
404 2
		$output = '';
405
406
		// Prep key for output
407 2
		$outputName = '';
408
		$outputName .= ($options['nested'] ? str_repeat($options['nested_prefix'], $item['level']) : '');
409
		$outputName .= $item['key'];
410 2
		if (strlen($outputName) > $options['oneline_length']) {
411 2
			$outputName = '~' . substr($item['key'], -($options['oneline_length'] - 1));
412 2
		}
413 2
414
		// Add item stats
415
		$output .= str_pad($outputName, $options['oneline_length'], ' ');
416 2
		$output .= ' | ';
417
		$output .= str_pad(static::getMetricResultFormatted($itemResult), 20, ' ', ($itemResult == 'N/A' ? STR_PAD_RIGHT : STR_PAD_LEFT));
418
		if ($options['show_start_stop']) {
419 2
			$output .= ' | ';
420 2
			$output .= str_pad((isset($item['start_time']) ? static::formatDateTime($item['start_time']) : 'N/A'), 19, ' ');
421 2
			$output .= ' | ';
422 2
			$output .= str_pad((isset($item['stop_time']) ? static::formatDateTime($item['stop_time']) : 'N/A'), 19, ' ');
423 2
		}
424 2
425
		return $output;
426
	}
427 2
428 2
	/**
429 2
	 * @param array $item
430 2
	 * @param array $options
431 1
	 *
432 1
	 * @return string
433 1
	 */
434 1
	public static function getStatsMultiline($item, $options = []) {
435 1
		// Merge options with default options
436
		$options = array_merge([
437 2
			// Show nested (boolean)
438
			'nested'          => true,
439
			// Prefix for nested items (string)
440
			'nested_prefix'   => '|-- ',
441
			// Show start stop for the item (boolean)
442
			'show_start_stop' => false
443
		], $options);
444
445
		// Get item result
446 1
		$itemResult = 'N/A';
447 View Code Duplication
		if (isset($item['start_value']) && isset($item['stop_value'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
448 1
			$itemResult = static::getMetricResult($item['start_value'], $item['stop_value']);
449
		}
450 1
451
		// Variable for output
452 1
		$output = '';
453
454
		// Add item stats
455 1
		$output .= 'Item   : ' . $item['key'];
456
		if ($options['show_start_stop']) {
457
			$output .= "\n";
458 1
			$output .= 'Start   : ' . (isset($item['start_time']) ? static::formatDateTime($item['start_time']) : 'N/A');
459 1
			$output .= "\n";
460 1
			$output .= 'Stop    : ' . (isset($item['stop_time']) ? static::formatDateTime($item['stop_time']) : 'N/A');
461 1
		}
462
		$output .= "\n";
463
		$output .= 'Result : ' . static::getMetricResultFormatted($itemResult);
464 1
465
		// Show as nested
466
		if ($options['nested']) {
467 1
			$output = str_repeat($options['nested_prefix'], $item['level']) . $output;
468 1
			$output = str_replace("\n", "\n" . str_repeat($options['nested_prefix'], $item['level']), $output);
469 1
		}
470 1
471 1
		return $output;
472 1
	}
473 1
474 1
	/**
475 1
	 * @param string $type
476
	 *
477
	 * @return bool|string
478 1
	 */
479 1
	public static function getLastItemName($type = '') {
480 1
		// Set default key
481 1
		$key = false;
482
483 1
		// Get collection items
484
		$items = static::getCollection()->getAll();
485
486
		// Get reverse list of item keys
487
		$itemKeys = array_reverse(array_keys($items));
488
489
		// If unknown type is given
490
		if ($type != 'started' && $type != 'stopped') {
491 1
			// Get current/last key
492 1
			$key = current($itemKeys);
493
		} else {
494
			// Loop throug items reversed and get the last one matching the given type
495 1
			foreach ($itemKeys as $itemKey) {
496
				$itemKeyIntersect = array_values(array_intersect(array_keys($items[$itemKey]), ['start_time', 'stop_time']));
497
498 1
				if ($type == 'stopped' && $itemKeyIntersect == ['start_time', 'stop_time']) {
499
					$key = $itemKey;
500
					break;
501 1
				}
502
503
				if ($type == 'started' && $itemKeyIntersect == ['start_time']) {
504 1
					$key = $itemKey;
505
					break;
506 1
				}
507 1
			}
508
		}
509 1
510 1
		// Return the key
511
		return $key;
512 1
	}
513 1
514 1
	/**
515
	 * @param int|float $unixTimestamp
516
	 * @param bool      $showMiliseconds
517 1
	 *
518 1
	 * @return string
519 1
	 */
520
	public static function formatDateTime($unixTimestamp, $showMiliseconds = false) {
521 1
		$formatted = date('Y-m-d H:i:s', $unixTimestamp);
522
523
		if ($showMiliseconds) {
524
			$miliseconds = 0;
525 1
			if (strpos($unixTimestamp, '.') !== false) {
526
				$miliseconds = substr($unixTimestamp, (strpos($unixTimestamp, '.') + 1));
527
			}
528
529
			$formatted .= '.' . str_pad($miliseconds, 4, '0', STR_PAD_RIGHT);
530
		}
531
532
		return $formatted;
533
	}
534 1
535 1
	/**
536
	 * @param array     $units
537 1
	 * @param int|float $number
538 1
	 * @param int       $precision
539 1
	 * @param null      $forceUnit
540 1
	 *
541 1
	 * @return string
542
	 */
543 1
	public static function formatForUnits($units = [], $number = 0, $precision = 2, $forceUnit = null) {
544 1
		$value = $number;
545
		if (!empty($forceUnit) && array_key_exists($forceUnit, $units)) {
546 1
			$unit = $forceUnit;
547
			foreach ($units as $k => $v) {
548
				$value = ($value / $v);
549
				if ($k == $unit) {
550
					break;
551
				}
552
			}
553
		} else {
554
			$unit = '';
555
			foreach ($units as $k => $v) {
556
				if (empty($unit) || ($value / $v) > 1) {
557 3
					$value = ($value / $v);
558 3
					$unit  = $k;
559 3
				} else {
560 3
					break;
561 3
				}
562 3
			}
563 3
		}
564 3
565
		return sprintf('%0.' . $precision . 'f', $value) . ' ' . str_pad($unit, 2, ' ', STR_PAD_RIGHT);
566 3
	}
567 3
568 3
	/**
569 3
	 * @param      $value
570 3
	 * @param int  $precision
571 3
	 * @param null $forceUnit
572 3
	 *
573 3
	 * @return string
574 3
	 */
575
	public static function formatMiliseconds($value, $precision = 2, $forceUnit = null) {
576 3
		return self::formatForUnits([
577
			'MS' => 1,
578
			'S'  => 1000,
579 3
			'M'  => 60,
580
			'H'  => 60,
581
			'D'  => 24,
582
			'W'  => 7
583
		], $value, $precision, $forceUnit);
584
	}
585
586
	/**
587
	 * @param      $value
588
	 * @param int  $precision
589 1
	 * @param null $forceUnit
590 1
	 *
591 1
	 * @return string
592 1
	 */
593 1
	public static function formatBytes($value, $precision = 2, $forceUnit = null) {
594 1
		return self::formatForUnits([
595 1
			'B'  => 1,
596
			'KB' => 1024,
597 1
			'MB' => 1024,
598
			'GB' => 1024,
599
			'TB' => 1024,
600
			'PB' => 1024
601
		], $value, $precision, $forceUnit);
602
	}
603
}
604