Skip to content

Commit 3deea5a

Browse files
committed
[HttpKernel][WebProfilerBundle] Profile stack
1 parent 3498259 commit 3deea5a

File tree

9 files changed

+239
-12
lines changed

9 files changed

+239
-12
lines changed

src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
<argument>null</argument>
2525
<argument>%profiler_listener.only_exceptions%</argument>
2626
<argument>%profiler_listener.only_master_requests%</argument>
27+
<argument type="service" id="profile_stack" />
2728
</service>
29+
30+
<service id="profile_stack" class="Symfony\Component\HttpKernel\Profiler\ProfileStack" />
2831
</services>
2932
</container>

src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616
use Symfony\Component\HttpFoundation\Request;
1717
use Symfony\Component\HttpFoundation\Response;
1818
use Symfony\Component\HttpFoundation\Session\Flash\AutoExpireFlashBag;
19+
use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector;
20+
use Symfony\Component\HttpKernel\DataCollector\ExceptionDataCollector;
1921
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
2022
use Symfony\Component\HttpKernel\KernelEvents;
23+
use Symfony\Component\HttpKernel\Profiler\ProfileStack;
2124
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
2225
use Twig\Environment;
2326

@@ -44,15 +47,17 @@ class WebDebugToolbarListener implements EventSubscriberInterface
4447
protected $mode;
4548
protected $excludedAjaxPaths;
4649
private $cspHandler;
50+
private $profileStack;
4751

48-
public function __construct(Environment $twig, bool $interceptRedirects = false, int $mode = self::ENABLED, UrlGeneratorInterface $urlGenerator = null, string $excludedAjaxPaths = '^/bundles|^/_wdt', ContentSecurityPolicyHandler $cspHandler = null)
52+
public function __construct(Environment $twig, bool $interceptRedirects = false, int $mode = self::ENABLED, UrlGeneratorInterface $urlGenerator = null, string $excludedAjaxPaths = '^/bundles|^/_wdt', ContentSecurityPolicyHandler $cspHandler = null, ProfileStack $profileStack = null)
4953
{
5054
$this->twig = $twig;
5155
$this->urlGenerator = $urlGenerator;
5256
$this->interceptRedirects = $interceptRedirects;
5357
$this->mode = $mode;
5458
$this->excludedAjaxPaths = $excludedAjaxPaths;
5559
$this->cspHandler = $cspHandler;
60+
$this->profileStack = $profileStack;
5661
}
5762

5863
public function isEnabled()
@@ -65,11 +70,33 @@ public function onKernelResponse(FilterResponseEvent $event)
6570
$response = $event->getResponse();
6671
$request = $event->getRequest();
6772

68-
if ($response->headers->has('X-Debug-Token') && null !== $this->urlGenerator) {
73+
$hasProfile = $this->profileStack instanceof ProfileStack ? $this->profileStack->has($request) : $response->headers->has('X-Debug-Token');
74+
75+
if ($hasProfile && null !== $this->urlGenerator) {
76+
if ($this->profileStack instanceof ProfileStack) {
77+
$profile = $this->profileStack->get($request);
78+
79+
$token = $profile->getToken();
80+
81+
foreach ($profile->getCollectors() as $collector) {
82+
if ($collector instanceof ExceptionDataCollector && $collector->hasException()) {
83+
$panel = $collector->getName();
84+
85+
break;
86+
} elseif ($collector instanceof DumpDataCollector && $collector->getDumpsCount() > 0) {
87+
$panel = $collector->getName();
88+
}
89+
}
90+
} else {
91+
$token = $response->headers->get('X-Debug-Token');
92+
}
6993
try {
7094
$response->headers->set(
7195
'X-Debug-Token-Link',
72-
$this->urlGenerator->generate('_profiler', ['token' => $response->headers->get('X-Debug-Token')], UrlGeneratorInterface::ABSOLUTE_URL)
96+
$this->urlGenerator->generate('_profiler', [
97+
'token' => $token,
98+
'panel' => $panel ?? null,
99+
], UrlGeneratorInterface::ABSOLUTE_URL)
73100
);
74101
} catch (\Exception $e) {
75102
$response->headers->set('X-Debug-Error', \get_class($e).': '.preg_replace('/\s+/', ' ', $e->getMessage()));
@@ -87,7 +114,7 @@ public function onKernelResponse(FilterResponseEvent $event)
87114
return;
88115
}
89116

90-
if ($response->headers->has('X-Debug-Token') && $response->isRedirect() && $this->interceptRedirects && 'html' === $request->getRequestFormat()) {
117+
if ($hasProfile && $response->isRedirect() && $this->interceptRedirects && 'html' === $request->getRequestFormat()) {
91118
if ($request->hasSession() && ($session = $request->getSession())->isStarted() && $session->getFlashBag() instanceof AutoExpireFlashBag) {
92119
// keep current flashes for one more request if using AutoExpireFlashBag
93120
$session->getFlashBag()->setAll($session->getFlashBag()->peekAll());
@@ -99,7 +126,7 @@ public function onKernelResponse(FilterResponseEvent $event)
99126
}
100127

101128
if (self::DISABLED === $this->mode
102-
|| !$response->headers->has('X-Debug-Token')
129+
|| !$hasProfile
103130
|| $response->isRedirection()
104131
|| ($response->headers->has('Content-Type') && false === strpos($response->headers->get('Content-Type'), 'html'))
105132
|| 'html' !== $request->getRequestFormat()

src/Symfony/Bundle/WebProfilerBundle/Resources/config/toolbar.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<argument type="service" id="router" on-invalid="ignore" />
1616
<argument /> <!-- paths that should be excluded from the AJAX requests shown in the toolbar -->
1717
<argument type="service" id="web_profiler.csp.handler" />
18+
<argument type="service" id="profile_stack" />
1819
</service>
1920
</services>
2021
</container>

src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@
267267
if (request.profilerUrl) {
268268
profilerCell.textContent = '';
269269
var profilerLink = document.createElement('a');
270-
profilerLink.setAttribute('href', request.statusCode < 400 ? request.profilerUrl : request.profilerUrl + '?panel=exception');
270+
profilerLink.setAttribute('href', request.profilerUrl);
271271
profilerLink.textContent = request.profile;
272272
profilerCell.appendChild(profilerLink);
273273
}

src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer;
2121
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
2222
use Symfony\Component\EventDispatcher\EventDispatcher;
23+
use Symfony\Component\HttpKernel\Profiler\ProfileStack;
2324

2425
class WebProfilerExtensionTest extends TestCase
2526
{
@@ -73,6 +74,7 @@ protected function setUp()
7374
$this->container->setParameter('data_collector.templates', []);
7475
$this->container->set('kernel', $this->kernel);
7576
$this->container->addCompilerPass(new RegisterListenersPass());
77+
$this->container->register('profile_stack', ProfileStack::class);
7678
}
7779

7880
protected function tearDown()

src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,13 @@
1616
use Symfony\Component\HttpFoundation\HeaderBag;
1717
use Symfony\Component\HttpFoundation\Request;
1818
use Symfony\Component\HttpFoundation\Response;
19+
use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface;
20+
use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector;
21+
use Symfony\Component\HttpKernel\DataCollector\ExceptionDataCollector;
1922
use Symfony\Component\HttpKernel\Event\ResponseEvent;
2023
use Symfony\Component\HttpKernel\HttpKernelInterface;
24+
use Symfony\Component\HttpKernel\Profiler\Profile;
25+
use Symfony\Component\HttpKernel\Profiler\ProfileStack;
2126
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
2227

2328
class WebDebugToolbarListenerTest extends TestCase
@@ -243,7 +248,7 @@ public function testXDebugUrlHeader()
243248
$urlGenerator
244249
->expects($this->once())
245250
->method('generate')
246-
->with('_profiler', ['token' => 'xxxxxxxx'], UrlGeneratorInterface::ABSOLUTE_URL)
251+
->with('_profiler', ['token' => 'xxxxxxxx', 'panel' => null], UrlGeneratorInterface::ABSOLUTE_URL)
247252
->willReturn('http://mydomain.com/_profiler/xxxxxxxx')
248253
;
249254

@@ -264,7 +269,7 @@ public function testThrowingUrlGenerator()
264269
$urlGenerator
265270
->expects($this->once())
266271
->method('generate')
267-
->with('_profiler', ['token' => 'xxxxxxxx'])
272+
->with('_profiler', ['token' => 'xxxxxxxx', 'panel' => null])
268273
->willThrowException(new \Exception('foo'))
269274
;
270275

@@ -285,7 +290,7 @@ public function testThrowingErrorCleanup()
285290
$urlGenerator
286291
->expects($this->once())
287292
->method('generate')
288-
->with('_profiler', ['token' => 'xxxxxxxx'])
293+
->with('_profiler', ['token' => 'xxxxxxxx', 'panel' => null])
289294
->willThrowException(new \Exception("This\nmultiline\r\ntabbed text should\tcome out\r on\n \ta single plain\r\nline"))
290295
;
291296

@@ -297,6 +302,97 @@ public function testThrowingErrorCleanup()
297302
$this->assertEquals('Exception: This multiline tabbed text should come out on a single plain line', $response->headers->get('X-Debug-Error'));
298303
}
299304

305+
public function testToolbarIsInjectedWithProfileStack()
306+
{
307+
$response = new Response('<html><head></head><body></body></html>');
308+
309+
$event = new ResponseEvent($this->getKernelMock(), $request = $this->getRequestMock(), HttpKernelInterface::MASTER_REQUEST, $response);
310+
311+
$listener = new WebDebugToolbarListener($this->getTwigMock(), false, WebDebugToolbarListener::ENABLED, null, '', null, $profileStack = new ProfileStack());
312+
313+
$profileStack->set($request, new Profile('foobar'));
314+
315+
$listener->onKernelResponse($event);
316+
317+
$this->assertEquals("<html><head></head><body>\nWDT\n</body></html>", $response->getContent());
318+
}
319+
320+
/**
321+
* @dataProvider linksToPanelsProvider
322+
*/
323+
public function testLinksToPanels(DataCollectorInterface $dataCollector, Request $request, Response $response)
324+
{
325+
$urlGenerator = $this->getUrlGeneratorMock();
326+
$urlGenerator
327+
->expects($this->once())
328+
->method('generate')
329+
->with('_profiler', ['token' => $token = 'xxxxxx', 'panel' => $dataCollector->getName()], UrlGeneratorInterface::ABSOLUTE_URL)
330+
->willReturn($expectedLink = 'http://mydomain.com/_profiler/'.$dataCollector->getName());
331+
332+
$event = new ResponseEvent($this->getKernelMock(), $request, HttpKernelInterface::MASTER_REQUEST, $response);
333+
334+
$listener = new WebDebugToolbarListener($this->getTwigMock(), false, WebDebugToolbarListener::ENABLED, $urlGenerator, '', null, $profileStack = new ProfileStack());
335+
336+
$profileStack->set($request, $profile = new Profile($token));
337+
$profile->addCollector($dataCollector);
338+
$profile->addCollector($this->createMock(DataCollectorInterface::class));
339+
340+
$listener->onKernelResponse($event);
341+
342+
$this->assertEquals($expectedLink, $response->headers->get('X-Debug-Token-Link'));
343+
}
344+
345+
public function linksToPanelsProvider()
346+
{
347+
$exceptionDataCollector = new ExceptionDataCollector();
348+
$exceptionDataCollector->collect($request = new Request(), $response = new Response(), new \DomainException());
349+
350+
yield [$exceptionDataCollector, $request, $response];
351+
352+
$dumpDataCollector = $this->createMock(DumpDataCollector::class);
353+
$dumpDataCollector
354+
->expects($this->atLeastOnce())
355+
->method('getName')
356+
->willReturn('dump');
357+
$dumpDataCollector
358+
->expects($this->atLeastOnce())
359+
->method('getDumpsCount')
360+
->willReturn(1);
361+
362+
yield [$dumpDataCollector, new Request(), new Response()];
363+
}
364+
365+
public function testLinkToExceptionPanelPriority()
366+
{
367+
$exceptionDataCollector = new ExceptionDataCollector();
368+
$exceptionDataCollector->collect($request = new Request(), $response = new Response(), new \DomainException());
369+
370+
$urlGenerator = $this->getUrlGeneratorMock();
371+
$urlGenerator
372+
->expects($this->once())
373+
->method('generate')
374+
->with('_profiler', ['token' => $token = 'xxxxxx', 'panel' => $exceptionDataCollector->getName()], UrlGeneratorInterface::ABSOLUTE_URL)
375+
->willReturn($expectedLink = 'http://mydomain.com/_profiler/'.$exceptionDataCollector->getName());
376+
377+
$event = new ResponseEvent($this->getKernelMock(), $request, HttpKernelInterface::MASTER_REQUEST, $response);
378+
379+
$listener = new WebDebugToolbarListener($this->getTwigMock(), false, WebDebugToolbarListener::ENABLED, $urlGenerator, '', null, $profileStack = new ProfileStack());
380+
381+
$profileStack->set($request, $profile = new Profile($token));
382+
$profile->addCollector($exceptionDataCollector);
383+
384+
$dumpDataCollector = $this->createMock(DumpDataCollector::class);
385+
$dumpDataCollector
386+
->expects($this->never())
387+
->method('getDumpsCount');
388+
389+
$profile->addCollector($dumpDataCollector);
390+
391+
$listener->onKernelResponse($event);
392+
393+
$this->assertEquals($expectedLink, $response->headers->get('X-Debug-Token-Link'));
394+
}
395+
300396
protected function getRequestMock($isXmlHttpRequest = false, $requestFormat = 'html', $hasSession = true)
301397
{
302398
$request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->setMethods(['getSession', 'isXmlHttpRequest', 'getRequestFormat'])->disableOriginalConstructor()->getMock();

src/Symfony/Component/HttpKernel/EventListener/ProfilerListener.php

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@
1212
namespace Symfony\Component\HttpKernel\EventListener;
1313

1414
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15+
use Symfony\Component\HttpFoundation\Request;
1516
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
1617
use Symfony\Component\HttpFoundation\RequestStack;
1718
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
1819
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
19-
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
2020
use Symfony\Component\HttpKernel\KernelEvents;
2121
use Symfony\Component\HttpKernel\Profiler\Profiler;
22+
use Symfony\Component\HttpKernel\Profiler\ProfileStack;
2223

2324
/**
2425
* ProfilerListener collects data for the current request by listening to the kernel events.
@@ -37,12 +38,13 @@ class ProfilerListener implements EventSubscriberInterface
3738
protected $profiles;
3839
protected $requestStack;
3940
protected $parents;
41+
private $profileStack;
4042

4143
/**
4244
* @param bool $onlyException True if the profiler only collects data when an exception occurs, false otherwise
4345
* @param bool $onlyMasterRequests True if the profiler only collects data when the request is a master request, false otherwise
4446
*/
45-
public function __construct(Profiler $profiler, RequestStack $requestStack, RequestMatcherInterface $matcher = null, bool $onlyException = false, bool $onlyMasterRequests = false)
47+
public function __construct(Profiler $profiler, RequestStack $requestStack, RequestMatcherInterface $matcher = null, bool $onlyException = false, bool $onlyMasterRequests = false, ProfileStack $profileStack = null)
4648
{
4749
$this->profiler = $profiler;
4850
$this->matcher = $matcher;
@@ -51,6 +53,7 @@ public function __construct(Profiler $profiler, RequestStack $requestStack, Requ
5153
$this->profiles = new \SplObjectStorage();
5254
$this->parents = new \SplObjectStorage();
5355
$this->requestStack = $requestStack;
56+
$this->profileStack = $profileStack;
5457
}
5558

5659
/**
@@ -94,9 +97,13 @@ public function onKernelResponse(FilterResponseEvent $event)
9497
$this->profiles[$request] = $profile;
9598

9699
$this->parents[$request] = $this->requestStack->getParentRequest();
100+
101+
if ($this->profileStack instanceof ProfileStack) {
102+
$this->profileStack->set($request, $profile);
103+
}
97104
}
98105

99-
public function onKernelTerminate(PostResponseEvent $event)
106+
public function onKernelTerminate()
100107
{
101108
// attach children to parents
102109
foreach ($this->profiles as $request) {
@@ -114,6 +121,10 @@ public function onKernelTerminate(PostResponseEvent $event)
114121

115122
$this->profiles = new \SplObjectStorage();
116123
$this->parents = new \SplObjectStorage();
124+
125+
if ($this->profileStack instanceof ProfileStack) {
126+
$this->profileStack->reset();
127+
}
117128
}
118129

119130
public static function getSubscribedEvents()
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\Profiler;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
16+
/**
17+
* @internal
18+
*/
19+
final class ProfileStack
20+
{
21+
/**
22+
* @var \SplObjectStorage
23+
*/
24+
private $profiles;
25+
26+
public function __construct()
27+
{
28+
$this->reset();
29+
}
30+
31+
public function has(Request $request): bool
32+
{
33+
return isset($this->profiles[$request]);
34+
}
35+
36+
public function get(Request $request): Profile
37+
{
38+
return $this->profiles[$request];
39+
}
40+
41+
public function set(Request $request, Profile $profile): void
42+
{
43+
$this->profiles[$request] = $profile;
44+
}
45+
46+
public function reset(): void
47+
{
48+
$this->profiles = new \SplObjectStorage();
49+
}
50+
}

0 commit comments

Comments
 (0)