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

XdebugTrace   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 119
Duplicated Lines 0 %

Importance

Changes 2
Bugs 1 Features 1
Metric Value
eloc 50
c 2
b 1
f 1
dl 0
loc 119
rs 10
wmc 28

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A jsonSerialize() 0 10 2
A stop() 0 7 2
B start() 0 34 8
A canStopTrace() 0 17 6
B performStopTrace() 0 33 9
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 file_get_contents;
13
use function function_exists;
14
use function getenv;
15
use function ini_get;
16
use function is_string;
17
use function restore_error_handler;
18
use function rtrim;
19
use function set_error_handler;
20
use function str_contains;
21
use function sys_get_temp_dir;
22
use function uniqid;
23
use function unlink;
24
use function xdebug_get_tracefile_name;
25
use function xdebug_start_trace;
26
use function xdebug_stop_trace;
27
28
use const E_NOTICE;
29
30
final class XdebugTrace implements JsonSerializable
31
{
32
    private ?string $traceId = null;
33
34
    public function __construct(
35
        public readonly ?string $content = null,
36
    ) {
37
    }
38
39
    public static function start(): self
40
    {
41
        if (! extension_loaded('xdebug') || ! function_exists('xdebug_start_trace')) {
42
            return new self(); // @codeCoverageIgnore
43
        }
44
45
        // Check if Xdebug trace functionality is properly configured
46
        $envMode = getenv('XDEBUG_MODE');
47
        $iniMode = ini_get('xdebug.mode');
48
        $xdebugMode = $envMode !== false ? $envMode : ($iniMode !== false ? $iniMode : '');
49
        if (! str_contains($xdebugMode, 'trace')) {
50
            return new self(); // @codeCoverageIgnore
51
        }
52
53
        // Always start our own trace to ensure we have control over the file format
54
        // Stop any existing trace first to ensure we get a fresh start
55
        if (function_exists('xdebug_stop_trace')) {
56
            @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

56
            /** @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...
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->canStopTrace()) {
78
            return new self(); // @codeCoverageIgnore
79
        }
80
81
        return $this->performStopTrace(); // @codeCoverageIgnore
82
    }
83
84
    private function canStopTrace(): bool
85
    {
86
        if (! function_exists('xdebug_stop_trace')) {
87
            return false; // @codeCoverageIgnore
88
        }
89
90
        // Check if Xdebug trace functionality is properly configured
91
        $envMode = getenv('XDEBUG_MODE');
92
        $iniMode = ini_get('xdebug.mode');
93
        $xdebugMode = $envMode !== false ? $envMode : ($iniMode !== false ? $iniMode : '');
94
95
        if (! str_contains($xdebugMode, 'trace')) {
96
            return false; // @codeCoverageIgnore
97
        }
98
99
        // Can stop if we started the trace ourselves, OR if there's an existing trace running
100
        return $this->traceId !== null || function_exists('xdebug_get_tracefile_name');
101
    }
102
103
    private function performStopTrace(): self
104
    {
105
        // If we already have content (from existing trace), preserve it
106
        if ($this->content !== null) {
107
            return new self($this->content); // @codeCoverageIgnore
108
        }
109
110
        // Try to stop trace and get the trace file path
111
        // Suppress "Function trace was not started" error for graceful handling
112
        set_error_handler(static function (int $errno, string $errstr): bool {
113
            // Ignore specific xdebug trace errors (only handle E_NOTICE)
114
            return $errno === E_NOTICE && str_contains($errstr, 'Function trace was not started');
115
        });
116
117
        try {
118
            // Get the trace file name BEFORE stopping the trace
119
            $traceFile = function_exists('xdebug_get_tracefile_name') ? xdebug_get_tracefile_name() : false; // @codeCoverageIgnore
120
            xdebug_stop_trace(); // @codeCoverageIgnore - returns void
121
        } finally {
122
            restore_error_handler();
123
        }
124
125
        if ($traceFile === false || ! is_string($traceFile) || ! file_exists($traceFile)) {
126
            return new self(); // @codeCoverageIgnore
127
        }
128
129
        // Read trace content and delete file for self-contained implementation
130
        $content = file_get_contents($traceFile);
131
        if ($content !== false) {
132
            @unlink($traceFile); // Clean up trace file
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). 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

132
            /** @scrutinizer ignore-unhandled */ @unlink($traceFile); // Clean up trace file

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...
133
        }
134
135
        return new self($content !== false ? $content : null);
136
    }
137
138
    /** @return array<string, mixed> */
139
    #[Override]
140
    public function jsonSerialize(): array
141
    {
142
        if ($this->content === null) {
143
            return [];
144
        }
145
146
        return [
147
            'data' => $this->content,
148
            'spec_url' => 'https://xdebug.org/docs/trace#Output-Formats',
149
        ];
150
    }
151
}
152