Passed
Pull Request — 1.x (#334)
by Akihito
02:27
created

XdebugTrace::stop()   B

Complexity

Conditions 9
Paths 45

Size

Total Lines 34
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 9
eloc 17
c 1
b 0
f 1
nc 45
nop 0
dl 0
loc 34
rs 8.0555
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BEAR\Resource\SemanticLog\Profile;
6
7
use JsonSerializable;
8
use Override;
9
10
use function extension_loaded;
11
use function file_exists;
12
use function function_exists;
13
use function getenv;
14
use function ini_get;
15
use function restore_error_handler;
16
use function rtrim;
17
use function set_error_handler;
18
use function str_contains;
19
use function sys_get_temp_dir;
20
use function uniqid;
21
use function xdebug_get_tracefile_name;
22
use function xdebug_start_trace;
23
use function xdebug_stop_trace;
24
25
final class XdebugTrace implements JsonSerializable
26
{
27
    private ?string $traceId = null;
28
29
    public function __construct(
30
        public readonly ?string $file = null,
31
    ) {
32
    }
33
34
    public static function start(): self
35
    {
36
        if (! extension_loaded('xdebug') || ! function_exists('xdebug_start_trace')) {
37
            return new self(); // @codeCoverageIgnore
38
        }
39
40
        // Check if Xdebug trace functionality is properly configured
41
        $envMode = getenv('XDEBUG_MODE');
42
        $iniMode = ini_get('xdebug.mode');
43
        $xdebugMode = $envMode !== false ? $envMode : ($iniMode !== false ? $iniMode : '');
44
        if (! str_contains($xdebugMode, 'trace')) {
45
            return new self(); // @codeCoverageIgnore
46
        }
47
48
        // Check if trace is already running (due to xdebug.start_with_request=yes)
49
        $existingTrace = null;
50
        if (function_exists('xdebug_get_tracefile_name')) {
51
            $existingTrace = xdebug_get_tracefile_name(); // @codeCoverageIgnore
52
        }
53
54
        if ($existingTrace !== null) {
55
            // Trace already started by xdebug.start_with_request, use existing file
56
            return new self($existingTrace); // @codeCoverageIgnore
57
        }
58
59
        $instance = new self();
60
        $instance->traceId = uniqid('profile_', true);
61
62
        // Use full path for trace file to ensure consistency with xhprofFile
63
        $outputDir = ini_get('xdebug.output_dir');
64
        if ($outputDir === false) {
65
            $outputDir = sys_get_temp_dir(); // @codeCoverageIgnore
66
        }
67
68
        $traceFilePrefix = rtrim($outputDir, '/') . '/' . $instance->traceId;
69
        xdebug_start_trace($traceFilePrefix); // @codeCoverageIgnore
70
71
        // Note: Return value is void, trace may fail silently if already started elsewhere
72
        return $instance;
73
    }
74
75
    public function stop(): self
76
    {
77
        if ($this->traceId === null || ! function_exists('xdebug_stop_trace')) {
78
            return new self(); // @codeCoverageIgnore
79
        }
80
81
        // Check if Xdebug trace functionality is properly configured
82
        $envMode = getenv('XDEBUG_MODE');
83
        $iniMode = ini_get('xdebug.mode');
84
        $xdebugMode = $envMode !== false ? $envMode : ($iniMode !== false ? $iniMode : '');
85
        if (! str_contains($xdebugMode, 'trace')) {
86
            return new self(); // @codeCoverageIgnore
87
        }
88
89
        // Try to stop trace and get the trace file path
90
        // Suppress "Function trace was not started" error for graceful handling
91
        set_error_handler(static function (int $errno, string $errstr): bool {
92
            // Ignore specific xdebug trace errors
93
            return str_contains($errstr, 'Function trace was not started');
94
        });
95
96
        try {
97
            xdebug_stop_trace(); // @codeCoverageIgnore - returns void
98
            // Try to get the trace file name if available
99
            $traceFile = function_exists('xdebug_get_tracefile_name') ? xdebug_get_tracefile_name() : false; // @codeCoverageIgnore
100
        } finally {
101
            restore_error_handler();
102
        }
103
104
        if ($traceFile === false || ! file_exists($traceFile)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $traceFile does not seem to be defined for all execution paths leading up to this point.
Loading history...
105
            return new self(); // @codeCoverageIgnore
106
        }
107
108
        return new self($traceFile);
109
    }
110
111
    /** @return array<string, mixed> */
112
    #[Override]
113
    public function jsonSerialize(): array
114
    {
115
        if ($this->file === null) {
116
            return [];
117
        }
118
119
        return ['file' => $this->file];
120
    }
121
}
122