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

XdebugTrace::start()   B

Complexity

Conditions 8
Paths 21

Size

Total Lines 34
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 8
eloc 17
c 1
b 0
f 1
nc 21
nop 0
dl 0
loc 34
rs 8.4444
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 is_string;
16
use function restore_error_handler;
17
use function rtrim;
18
use function set_error_handler;
19
use function str_contains;
20
use function sys_get_temp_dir;
21
use function uniqid;
22
use function xdebug_get_tracefile_name;
23
use function xdebug_start_trace;
24
use function xdebug_stop_trace;
25
26
use const E_NOTICE;
27
28
final class XdebugTrace implements JsonSerializable
29
{
30
    private ?string $traceId = null;
31
32
    public function __construct(
33
        public readonly ?string $file = null,
34
    ) {
35
    }
36
37
    public static function start(): self
38
    {
39
        if (! extension_loaded('xdebug') || ! function_exists('xdebug_start_trace')) {
40
            return new self(); // @codeCoverageIgnore
41
        }
42
43
        // Check if Xdebug trace functionality is properly configured
44
        $envMode = getenv('XDEBUG_MODE');
45
        $iniMode = ini_get('xdebug.mode');
46
        $xdebugMode = $envMode !== false ? $envMode : ($iniMode !== false ? $iniMode : '');
47
        if (! str_contains($xdebugMode, 'trace')) {
48
            return new self(); // @codeCoverageIgnore
49
        }
50
51
        // Always start our own trace to ensure we have control over the file format
52
        // Stop any existing trace first to ensure we get a fresh start
53
        if (function_exists('xdebug_stop_trace')) {
54
            @xdebug_stop_trace(); // @codeCoverageIgnore - suppress errors if not running
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for xdebug_stop_trace(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

54
            /** @scrutinizer ignore-unhandled */ @xdebug_stop_trace(); // @codeCoverageIgnore - suppress errors if not running

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
55
        }
56
57
        $instance = new self();
58
        $instance->traceId = uniqid('profile_', true);
59
60
        // Use full path for trace file to ensure consistency with xhprofFile
61
        $outputDir = ini_get('xdebug.output_dir');
62
        if ($outputDir === false) {
63
            $outputDir = sys_get_temp_dir(); // @codeCoverageIgnore
64
        }
65
66
        $traceFilePrefix = rtrim($outputDir, '/') . '/' . $instance->traceId;
67
        xdebug_start_trace($traceFilePrefix); // @codeCoverageIgnore
68
69
        // Note: Return value is void, trace may fail silently if already started elsewhere
70
        return $instance;
71
    }
72
73
    public function stop(): self
74
    {
75
        if (! $this->canStopTrace()) {
76
            return new self(); // @codeCoverageIgnore
77
        }
78
79
        return $this->performStopTrace(); // @codeCoverageIgnore
80
    }
81
82
    private function canStopTrace(): bool
83
    {
84
        if (! function_exists('xdebug_stop_trace')) {
85
            return false; // @codeCoverageIgnore
86
        }
87
88
        // Check if Xdebug trace functionality is properly configured
89
        $envMode = getenv('XDEBUG_MODE');
90
        $iniMode = ini_get('xdebug.mode');
91
        $xdebugMode = $envMode !== false ? $envMode : ($iniMode !== false ? $iniMode : '');
92
93
        if (! str_contains($xdebugMode, 'trace')) {
94
            return false; // @codeCoverageIgnore
95
        }
96
97
        // Can stop if we started the trace ourselves, OR if there's an existing trace running
98
        return $this->traceId !== null || function_exists('xdebug_get_tracefile_name');
99
    }
100
101
    private function performStopTrace(): self
102
    {
103
        // If we already have a file (from existing trace), preserve it
104
        if ($this->file !== null) {
105
            return new self($this->file); // @codeCoverageIgnore
106
        }
107
108
        // Try to stop trace and get the trace file path
109
        // Suppress "Function trace was not started" error for graceful handling
110
        set_error_handler(static function (int $errno, string $errstr): bool {
111
            // Ignore specific xdebug trace errors (only handle E_NOTICE)
112
            return $errno === E_NOTICE && str_contains($errstr, 'Function trace was not started');
113
        });
114
115
        try {
116
            // Get the trace file name BEFORE stopping the trace
117
            $traceFile = function_exists('xdebug_get_tracefile_name') ? xdebug_get_tracefile_name() : false; // @codeCoverageIgnore
118
            xdebug_stop_trace(); // @codeCoverageIgnore - returns void
119
        } finally {
120
            restore_error_handler();
121
        }
122
123
        if ($traceFile === false || ! is_string($traceFile) || ! file_exists($traceFile)) {
124
            return new self(); // @codeCoverageIgnore
125
        }
126
127
        return new self($traceFile);
128
    }
129
130
    /** @return array<string, mixed> */
131
    #[Override]
132
    public function jsonSerialize(): array
133
    {
134
        if ($this->file === null) {
135
            return [];
136
        }
137
138
        return ['file' => $this->file];
139
    }
140
}
141