Skip to content

Commit 6f3ac51

Browse files
authored
Merge pull request #96 from glensc/add-request-context
2 parents 244d107 + d692834 commit 6f3ac51

15 files changed

Lines changed: 482 additions & 33 deletions

README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ shutdown handler:
6969
$profiler->start(false);
7070
```
7171

72+
`start()` is the default integration path for short-lived PHP runtimes such as
73+
FPM or mod_php.
74+
7275
## Using config file
7376

7477
You can create `config/config.php` and load config from there:
@@ -103,6 +106,54 @@ $profiler_data = $profiler->disable();
103106
$profiler->save($profiler_data);
104107
```
105108

109+
For long-lived runtimes, prefer `enable()` + `stop()` around each request so
110+
request context is captured at request start instead of at process shutdown.
111+
112+
## Request context providers
113+
114+
By default, the profiler captures request context from `$_SERVER`, `$_GET`,
115+
`$_ENV`, and the CLI `argv` fallback. Long-lived runtimes can provide their own
116+
request-scoped snapshot through `profiler.request_context_provider`. That
117+
snapshot is captured when profiling starts via `enable()` / `start()`, not when
118+
profiling stops.
119+
120+
Custom providers must implement
121+
`Xhgui\Profiler\RequestContext\Provider\RequestContextProviderInterface`
122+
and return a request-context object for the current profiling run. The request
123+
time and server snapshot should describe the same captured request.
124+
Custom providers are responsible for passing the request URL or CLI command
125+
explicitly when constructing those snapshots, and for making sure the server
126+
snapshot includes `REQUEST_TIME_FLOAT` for the captured request.
127+
Include `REQUEST_TIME` too if you want it preserved in the saved `meta.SERVER`
128+
payload.
129+
130+
```php
131+
use Xhgui\Profiler\RequestContext\Provider\RequestContextProviderInterface;
132+
use Xhgui\Profiler\RequestContext\RequestContext;
133+
134+
class AppRequestContextProvider implements RequestContextProviderInterface
135+
{
136+
public function capture()
137+
{
138+
return RequestContext::fromHttp(
139+
'/example',
140+
array(),
141+
array(),
142+
array(
143+
'REQUEST_URI' => '/example',
144+
'REQUEST_METHOD' => 'GET',
145+
'HTTP_HOST' => 'example.test',
146+
'PHP_SELF' => '/index.php',
147+
'DOCUMENT_ROOT' => '/srv/app',
148+
'REQUEST_TIME_FLOAT' => 1234.56789,
149+
)
150+
);
151+
}
152+
}
153+
154+
$config['profiler.request_context_provider'] = new AppRequestContextProvider();
155+
```
156+
106157
## Autoloader
107158

108159
To be able to profile autoloader, this project provides `autoload.php` that

autoload.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99

1010
require_once __DIR__ . '/src/Exception/ProfilerException.php';
1111
require_once __DIR__ . '/src/Config.php';
12+
require_once __DIR__ . '/src/RequestContext/RequestContextInterface.php';
13+
require_once __DIR__ . '/src/RequestContext/RequestContext.php';
14+
require_once __DIR__ . '/src/RequestContext/Provider/RequestContextProviderInterface.php';
15+
require_once __DIR__ . '/src/RequestContext/Provider/DefaultProvider.php';
16+
require_once __DIR__ . '/src/RequestContextFactory.php';
1217
require_once __DIR__ . '/src/Profiler.php';
1318
require_once __DIR__ . '/src/ProfilerFactory.php';
1419
require_once __DIR__ . '/src/Profilers/AbstractProfiler.php';

config/config.default.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@
3333
'profiler.options' => array(),
3434
'profiler.exclude-env' => array(),
3535
'profiler.exclude-all-env' => false,
36+
// Set this to an implementation of
37+
// Xhgui\Profiler\RequestContext\Provider\RequestContextProviderInterface
38+
// when integrating with long-lived runtimes that must capture
39+
// request-scoped data without relying on mutable globals.
40+
'profiler.request_context_provider' => null,
3641
'profiler.simple_url' => function ($url) {
3742
return preg_replace('/=\d+/', '', $url);
3843
},

examples/autoload.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@
8282
// Environment variables to exclude from profiling data
8383
'profiler.exclude-env' => array(),
8484
'profiler.options' => array(),
85+
// Set this to an implementation of
86+
// Xhgui\Profiler\RequestContext\Provider\RequestContextProviderInterface
87+
// when integrating with long-lived runtimes that must capture
88+
// request-scoped data without relying on mutable globals.
89+
'profiler.request_context_provider' => null,
8590

8691
/**
8792
* Determine whether the profiler should run.

src/Profiler.php

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace Xhgui\Profiler;
44

55
use Xhgui\Profiler\Exception\ProfilerException;
6+
use Xhgui\Profiler\RequestContext\Provider\RequestContextProviderInterface;
7+
use Xhgui\Profiler\RequestContext\RequestContextInterface;
68
use Xhgui\Profiler\Profilers\ProfilerInterface;
79
use Xhgui\Profiler\Saver\SaverInterface;
810

@@ -36,6 +38,16 @@ final class Profiler
3638
*/
3739
private $profiler;
3840

41+
/**
42+
* @var RequestContextProviderInterface|null
43+
*/
44+
private $requestContextProvider;
45+
46+
/**
47+
* @var RequestContextInterface|null
48+
*/
49+
private $requestContext;
50+
3951
/**
4052
* Simple state variable to hold the value of 'Is the profiler running or not?'
4153
*
@@ -99,12 +111,6 @@ public function enable($flags = null, $options = null)
99111
{
100112
$this->running = false;
101113

102-
// 'REQUEST_TIME_FLOAT' isn't available before 5.4.0
103-
// https://www.php.net/manual/en/reserved.variables.server.php
104-
if (!isset($_SERVER['REQUEST_TIME_FLOAT'])) {
105-
$_SERVER['REQUEST_TIME_FLOAT'] = microtime(true);
106-
}
107-
108114
$profiler = $this->getProfiler();
109115
if (!$profiler) {
110116
throw new ProfilerException('Unable to create profiler: No suitable profiler found');
@@ -122,7 +128,9 @@ public function enable($flags = null, $options = null)
122128
$options = $this->config['profiler.options'];
123129
}
124130

131+
$context = $this->captureRequestContext();
125132
$profiler->enable($flags, $options);
133+
$this->requestContext = $context;
126134
$this->running = true;
127135
}
128136

@@ -144,10 +152,18 @@ public function disable()
144152
throw new ProfilerException('Unable to create profiler: No suitable profiler found');
145153
}
146154

147-
$profile = new ProfilingData($this->config);
155+
$context = $this->requestContext;
156+
$this->requestContext = null;
148157
$this->running = false;
158+
$data = $profiler->disable();
159+
160+
if (!$context instanceof RequestContextInterface) {
161+
throw new ProfilerException('Unable to disable profiler: Request context is missing');
162+
}
163+
164+
$profile = new ProfilingData($this->config);
149165

150-
return $profile->getProfilingData($profiler->disable());
166+
return $profile->getProfilingData($data, $context);
151167
}
152168

153169
/**
@@ -277,4 +293,39 @@ private function getSaver()
277293

278294
return $this->saveHandler ?: null;
279295
}
296+
297+
/**
298+
* @return RequestContextProviderInterface
299+
*/
300+
private function getRequestContextProvider()
301+
{
302+
if ($this->requestContextProvider === null) {
303+
$this->requestContextProvider = RequestContextFactory::create($this->config);
304+
}
305+
306+
return $this->requestContextProvider;
307+
}
308+
309+
/**
310+
* @return RequestContextInterface
311+
*/
312+
private function captureRequestContext()
313+
{
314+
$context = $this->getRequestContextProvider()->capture();
315+
316+
if (!$context instanceof RequestContextInterface) {
317+
throw new ProfilerException('Request context provider must return a RequestContextInterface');
318+
}
319+
320+
$server = $context->getServer();
321+
if (!is_array($server) || !array_key_exists('REQUEST_TIME_FLOAT', $server)) {
322+
throw new ProfilerException('Request context provider must capture REQUEST_TIME_FLOAT in server data');
323+
}
324+
325+
if (!is_numeric($server['REQUEST_TIME_FLOAT'])) {
326+
throw new ProfilerException('Request context provider must capture a numeric REQUEST_TIME_FLOAT in server data');
327+
}
328+
329+
return $context;
330+
}
280331
}

src/ProfilingData.php

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Xhgui\Profiler;
44

5+
use Xhgui\Profiler\RequestContext\RequestContextInterface;
6+
57
/**
68
* @internal
79
*/
@@ -45,19 +47,21 @@ public function __construct(Config $config)
4547
}
4648

4749
/**
50+
* @param array $profile
51+
* @param RequestContextInterface $context
4852
* @return array
4953
*/
50-
public function getProfilingData(array $profile)
54+
public function getProfilingData(array $profile, RequestContextInterface $context)
5155
{
52-
$url = $this->getUrl();
53-
54-
list($sec, $usec) = $this->getRequestTime($_SERVER['REQUEST_TIME_FLOAT']);
56+
$url = $this->getUrl($context);
57+
$server = $context->getServer();
58+
list($sec, $usec) = $this->getRequestTime($server['REQUEST_TIME_FLOAT']);
5559

5660
$meta = array(
5761
'url' => $url,
58-
'get' => $_GET,
59-
'env' => $this->getEnvironment($_ENV),
60-
'SERVER' => $this->getServer($_SERVER),
62+
'get' => $context->getQuery(),
63+
'env' => $this->getEnvironment($context->getEnv()),
64+
'SERVER' => $this->getServer($server),
6165
'simple_url' => $this->getSimpleUrl($url),
6266
'request_ts_micro' => array('sec' => $sec, 'usec' => $usec),
6367
// these are superfluous and should be dropped in the future
@@ -109,15 +113,12 @@ private function getSimpleUrl($url)
109113
}
110114

111115
/**
116+
* @param RequestContextInterface $context
112117
* @return string
113118
*/
114-
private function getUrl()
119+
private function getUrl(RequestContextInterface $context)
115120
{
116-
$url = array_key_exists('REQUEST_URI', $_SERVER) ? $_SERVER['REQUEST_URI'] : null;
117-
if (!$url && isset($_SERVER['argv'])) {
118-
$cmd = basename($_SERVER['argv'][0]);
119-
$url = $cmd . ' ' . implode(' ', array_slice($_SERVER['argv'], 1));
120-
}
121+
$url = $context->getUrl();
121122

122123
if (is_callable($this->replaceUrl)) {
123124
$url = call_user_func($this->replaceUrl, $url);
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
namespace Xhgui\Profiler\RequestContext\Provider;
4+
5+
use Xhgui\Profiler\RequestContext\RequestContext;
6+
7+
/**
8+
* @internal
9+
*/
10+
class DefaultProvider implements RequestContextProviderInterface
11+
{
12+
public function capture()
13+
{
14+
$server = $_SERVER;
15+
16+
// 'REQUEST_TIME_FLOAT' isn't available before 5.4.0
17+
// https://www.php.net/manual/en/reserved.variables.server.php
18+
if (!isset($server['REQUEST_TIME_FLOAT'])) {
19+
$server['REQUEST_TIME_FLOAT'] = microtime(true);
20+
}
21+
if (!isset($server['REQUEST_TIME'])) {
22+
$server['REQUEST_TIME'] = (int) $server['REQUEST_TIME_FLOAT'];
23+
}
24+
25+
if (array_key_exists('REQUEST_URI', $server)) {
26+
return RequestContext::fromHttp(
27+
$server['REQUEST_URI'],
28+
$_GET,
29+
$_ENV,
30+
$server
31+
);
32+
}
33+
34+
return RequestContext::fromCli(
35+
$this->getCommand(isset($server['argv']) ? $server['argv'] : array()),
36+
$_ENV,
37+
$server
38+
);
39+
}
40+
41+
/**
42+
* @param array $argv
43+
* @return string
44+
*/
45+
private function getCommand(array $argv)
46+
{
47+
if (!isset($argv[0])) {
48+
return '';
49+
}
50+
51+
$cmd = basename($argv[0]);
52+
$args = array_slice($argv, 1);
53+
54+
if (!$args) {
55+
return $cmd;
56+
}
57+
58+
return $cmd . ' ' . implode(' ', $args);
59+
}
60+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace Xhgui\Profiler\RequestContext\Provider;
4+
5+
use Xhgui\Profiler\RequestContext\RequestContextInterface;
6+
7+
interface RequestContextProviderInterface
8+
{
9+
/**
10+
* Capture request-scoped profiler metadata.
11+
*
12+
* Implementations should return a request-context object whose request time
13+
* and server snapshot describe the same request.
14+
*
15+
* @return RequestContextInterface
16+
*/
17+
public function capture();
18+
}

0 commit comments

Comments
 (0)