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

XdebugTrace::performStopTrace()   A

Complexity

Conditions 5
Paths 10

Size

Total Lines 22
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 10
c 0
b 0
f 0
nc 10
nop 0
dl 0
loc 22
rs 9.6111
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
use const E_NOTICE;
26
27
final class XdebugTrace implements JsonSerializable
28
{
29
    private ?string $traceId = null;
30
31
    public function __construct(
32
        public readonly ?string $file = null,
33
    ) {
34
    }
35
36
    public static function start(): self
37
    {
38
        if (! extension_loaded('xdebug') || ! function_exists('xdebug_start_trace')) {
39
            return new self(); // @codeCoverageIgnore
40
        }
41
42
        // Check if Xdebug trace functionality is properly configured
43
        $envMode = getenv('XDEBUG_MODE');
44
        $iniMode = ini_get('xdebug.mode');
45
        $xdebugMode = $envMode !== false ? $envMode : ($iniMode !== false ? $iniMode : '');
46
        if (! str_contains($xdebugMode, 'trace')) {
47
            return new self(); // @codeCoverageIgnore
48
        }
49
50
        // Check if trace is already running (due to xdebug.start_with_request=yes)
51
        $existingTrace = null;
52
        if (function_exists('xdebug_get_tracefile_name')) {
53
            $existingTrace = xdebug_get_tracefile_name(); // @codeCoverageIgnore
54
        }
55
56
        if ($existingTrace !== null) {
57
            // Trace already started by xdebug.start_with_request, use existing file
58
            return new self($existingTrace); // @codeCoverageIgnore
59
        }
60
61
        $instance = new self();
62
        $instance->traceId = uniqid('profile_', true);
63
64
        // Use full path for trace file to ensure consistency with xhprofFile
65
        $outputDir = ini_get('xdebug.output_dir');
66
        if ($outputDir === false) {
67
            $outputDir = sys_get_temp_dir(); // @codeCoverageIgnore
68
        }
69
70
        $traceFilePrefix = rtrim($outputDir, '/') . '/' . $instance->traceId;
71
        xdebug_start_trace($traceFilePrefix); // @codeCoverageIgnore
72
73
        // Note: Return value is void, trace may fail silently if already started elsewhere
74
        return $instance;
75
    }
76
77
    public function stop(): self
78
    {
79
        if (! $this->canStopTrace()) {
80
            return new self(); // @codeCoverageIgnore
81
        }
82
83
        return $this->performStopTrace(); // @codeCoverageIgnore
84
    }
85
86
    private function canStopTrace(): bool
87
    {
88
        if ($this->traceId === null || ! function_exists('xdebug_stop_trace')) {
89
            return false; // @codeCoverageIgnore
90
        }
91
92
        // Check if Xdebug trace functionality is properly configured
93
        $envMode = getenv('XDEBUG_MODE');
94
        $iniMode = ini_get('xdebug.mode');
95
        $xdebugMode = $envMode !== false ? $envMode : ($iniMode !== false ? $iniMode : '');
96
97
        return str_contains($xdebugMode, 'trace'); // @codeCoverageIgnore
98
    }
99
100
    private function performStopTrace(): self
101
    {
102
        // Try to stop trace and get the trace file path
103
        // Suppress "Function trace was not started" error for graceful handling
104
        set_error_handler(static function (int $errno, string $errstr): bool {
105
            // Ignore specific xdebug trace errors (only handle E_NOTICE)
106
            return $errno === E_NOTICE && str_contains($errstr, 'Function trace was not started');
107
        });
108
109
        try {
110
            xdebug_stop_trace(); // @codeCoverageIgnore - returns void
111
            // Try to get the trace file name if available
112
            $traceFile = function_exists('xdebug_get_tracefile_name') ? xdebug_get_tracefile_name() : false; // @codeCoverageIgnore
113
        } finally {
114
            restore_error_handler();
115
        }
116
117
        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...
118
            return new self(); // @codeCoverageIgnore
119
        }
120
121
        return new self($traceFile);
122
    }
123
124
    /** @return array<string, mixed> */
125
    #[Override]
126
    public function jsonSerialize(): array
127
    {
128
        if ($this->file === null) {
129
            return [];
130
        }
131
132
        return ['file' => $this->file];
133
    }
134
}
135