├── CHANGELOG.md ├── Controller ├── ExceptionPanelController.php ├── ProfilerController.php └── RouterController.php ├── Csp ├── ContentSecurityPolicyHandler.php └── NonceGenerator.php ├── DependencyInjection ├── Configuration.php └── WebProfilerExtension.php ├── EventListener └── WebDebugToolbarListener.php ├── LICENSE ├── Profiler ├── CodeExtension.php └── TemplateManager.php ├── README.md ├── Resources ├── config │ ├── profiler.php │ ├── routing │ │ ├── profiler.php │ │ ├── profiler.xml │ │ ├── wdt.php │ │ └── wdt.xml │ ├── schema │ │ └── webprofiler-1.0.xsd │ └── toolbar.php ├── fonts │ ├── JetBrainsMono.woff2 │ └── LICENSE.txt └── views │ ├── Collector │ ├── ajax.html.twig │ ├── cache.html.twig │ ├── command.html.twig │ ├── config.html.twig │ ├── events.html.twig │ ├── exception.css.twig │ ├── exception.html.twig │ ├── form.html.twig │ ├── http_client.html.twig │ ├── logger.html.twig │ ├── mailer.html.twig │ ├── memory.html.twig │ ├── messenger.html.twig │ ├── notifier.html.twig │ ├── request.html.twig │ ├── router.html.twig │ ├── serializer.html.twig │ ├── time.css.twig │ ├── time.html.twig │ ├── time.js │ ├── translation.html.twig │ ├── twig.html.twig │ ├── validator.html.twig │ └── workflow.html.twig │ ├── Icon │ ├── LICENSE.txt │ ├── ajax.svg │ ├── alert-circle.svg │ ├── attachment.svg │ ├── cache.svg │ ├── chevron-down.svg │ ├── close.svg │ ├── command.svg │ ├── config.svg │ ├── download.svg │ ├── event.svg │ ├── exception.svg │ ├── file.svg │ ├── filter.svg │ ├── form.svg │ ├── forward.svg │ ├── http-client.svg │ ├── logger.svg │ ├── mailer.svg │ ├── memory.svg │ ├── menu.svg │ ├── messenger.svg │ ├── no.svg │ ├── notifier.svg │ ├── redirect.svg │ ├── referrer.svg │ ├── request.svg │ ├── router.svg │ ├── search.svg │ ├── serializer.svg │ ├── settings-theme-dark.svg │ ├── settings-theme-light.svg │ ├── settings-theme-system.svg │ ├── settings-width-fitted.svg │ ├── settings-width-fixed.svg │ ├── settings.svg │ ├── symfony.svg │ ├── time.svg │ ├── translation.svg │ ├── twig.svg │ ├── validator.svg │ ├── workflow.svg │ └── yes.svg │ ├── Profiler │ ├── _command_summary.html.twig │ ├── _request_summary.html.twig │ ├── ajax_layout.html.twig │ ├── bag.html.twig │ ├── base.html.twig │ ├── base_js.html.twig │ ├── cancel.html.twig │ ├── header.html.twig │ ├── info.html.twig │ ├── layout.html.twig │ ├── open.css.twig │ ├── open.html.twig │ ├── profiler.css.twig │ ├── results.html.twig │ ├── search.html.twig │ ├── settings.html.twig │ ├── table.html.twig │ ├── toolbar.css.twig │ ├── toolbar.html.twig │ ├── toolbar_item.html.twig │ ├── toolbar_js.html.twig │ └── toolbar_redirect.html.twig │ ├── Router │ └── panel.html.twig │ └── Script │ └── Mermaid │ └── mermaid-flowchart-v2.min.js ├── Twig └── WebProfilerExtension.php ├── WebProfilerBundle.php └── composer.json /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 7.3 5 | --- 6 | 7 | * Add `profiler.php` and `wdt.php` routing configuration files (use them instead of their XML equivalent) 8 | 9 | Before: 10 | 11 | ```yaml 12 | when@dev: 13 | web_profiler_wdt: 14 | resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' 15 | prefix: /_wdt 16 | 17 | web_profiler_profiler: 18 | resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' 19 | prefix: /_profiler 20 | ``` 21 | 22 | After: 23 | 24 | ```yaml 25 | when@dev: 26 | web_profiler_wdt: 27 | resource: '@WebProfilerBundle/Resources/config/routing/wdt.php' 28 | prefix: /_wdt 29 | 30 | web_profiler_profiler: 31 | resource: '@WebProfilerBundle/Resources/config/routing/profiler.php 32 | prefix: /_profiler 33 | ``` 34 | 35 | * Add `ajax_replace` option for replacing toolbar on AJAX requests 36 | 37 | 7.2 38 | --- 39 | 40 | * Add support for displaying profiles of multiple serializer instances 41 | 42 | 7.1 43 | --- 44 | 45 | * Set `XDEBUG_IGNORE` query parameter when sending toolbar XHR 46 | 47 | 6.4 48 | --- 49 | 50 | * Add console commands to the profiler 51 | 52 | 6.3 53 | --- 54 | 55 | * Add a "role=img" and an explicit title in the .svg file used by the web debug toolbar 56 | to improve accessibility with screen readers for blind users 57 | * Add a clickable link to the entry view twig file in the toolbar 58 | 59 | 6.1 60 | --- 61 | 62 | * Add a download link in mailer profiler for email attachments 63 | 64 | 5.4 65 | --- 66 | 67 | * Add a "preview" tab in mailer profiler for HTML email 68 | 69 | 5.2.0 70 | ----- 71 | 72 | * added session usage 73 | 74 | 5.0.0 75 | ----- 76 | 77 | * removed the `ExceptionController`, use `ExceptionPanelController` instead 78 | * removed the `TemplateManager::templateExists()` method 79 | 80 | 4.4.0 81 | ----- 82 | 83 | * added support for the Mailer component 84 | * added support for the HttpClient component 85 | * added button to clear the ajax request tab 86 | * deprecated the `ExceptionController::templateExists()` method 87 | * deprecated the `TemplateManager::templateExists()` method 88 | * deprecated the `ExceptionController` in favor of `ExceptionPanelController` 89 | * marked all classes of the WebProfilerBundle as internal 90 | * added a section with the stamps of a message after it is dispatched in the Messenger panel 91 | 92 | 4.3.0 93 | ----- 94 | 95 | * Replaced the canvas performance graph renderer with an SVG renderer 96 | 97 | 4.1.0 98 | ----- 99 | 100 | * added information about orphaned events 101 | * made the toolbar auto-update with info from ajax responses when they set the 102 | `Symfony-Debug-Toolbar-Replace header` to `1` 103 | 104 | 4.0.0 105 | ----- 106 | 107 | * removed the `WebProfilerExtension::dumpValue()` method 108 | * removed the `getTemplates()` method of the `TemplateManager` class in favor of the ``getNames()`` method 109 | * removed the `web_profiler.position` config option and the 110 | `web_profiler.debug_toolbar.position` container parameter 111 | 112 | 3.4.0 113 | ----- 114 | 115 | * Deprecated the `web_profiler.position` config option (in 4.0 version the toolbar 116 | will always be displayed at the bottom) and the `web_profiler.debug_toolbar.position` 117 | container parameter. 118 | 119 | 3.1.0 120 | ----- 121 | 122 | * added information about redirected and forwarded requests to the profiler 123 | 124 | 3.0.0 125 | ----- 126 | 127 | * removed profiler:import and profiler:export commands 128 | 129 | 2.8.0 130 | ----- 131 | 132 | * deprecated profiler:import and profiler:export commands 133 | 134 | 2.7.0 135 | ----- 136 | 137 | * [BC BREAK] if you are using a DB to store profiles, the table must be dropped 138 | * added the HTTP status code to profiles 139 | 140 | 2.3.0 141 | ----- 142 | 143 | * draw retina canvas if devicePixelRatio is bigger than 1 144 | 145 | 2.1.0 146 | ----- 147 | 148 | * deprecated the verbose setting (not relevant anymore) 149 | * [BC BREAK] You must clear old profiles after upgrading to 2.1 (don't forget 150 | to remove the table if you are using a DB) 151 | * added support for the request method 152 | * added a routing panel 153 | * added a timeline panel 154 | * The toolbar position can now be configured via the `position` option (can 155 | be `top` or `bottom`) 156 | -------------------------------------------------------------------------------- /Controller/ExceptionPanelController.php: -------------------------------------------------------------------------------- 1 | 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\Bundle\WebProfilerBundle\Controller; 13 | 14 | use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; 15 | use Symfony\Component\HttpFoundation\Response; 16 | use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; 17 | use Symfony\Component\HttpKernel\Profiler\Profiler; 18 | 19 | /** 20 | * Renders the exception panel. 21 | * 22 | * @author Yonel Ceruto 23 | * 24 | * @internal 25 | */ 26 | class ExceptionPanelController 27 | { 28 | public function __construct( 29 | private HtmlErrorRenderer $errorRenderer, 30 | private ?Profiler $profiler = null, 31 | ) { 32 | } 33 | 34 | /** 35 | * Renders the exception panel stacktrace for the given token. 36 | */ 37 | public function body(string $token): Response 38 | { 39 | if (null === $this->profiler) { 40 | throw new NotFoundHttpException('The profiler must be enabled.'); 41 | } 42 | 43 | $exception = $this->profiler->loadProfile($token) 44 | ->getCollector('exception') 45 | ->getException() 46 | ; 47 | 48 | return new Response($this->errorRenderer->getBody($exception), 200, ['Content-Type' => 'text/html']); 49 | } 50 | 51 | /** 52 | * Renders the exception panel stylesheet. 53 | */ 54 | public function stylesheet(): Response 55 | { 56 | return new Response($this->errorRenderer->getStylesheet(), 200, ['Content-Type' => 'text/css']); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Controller/RouterController.php: -------------------------------------------------------------------------------- 1 | 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\Bundle\WebProfilerBundle\Controller; 13 | 14 | use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; 15 | use Symfony\Component\HttpFoundation\Request; 16 | use Symfony\Component\HttpFoundation\Response; 17 | use Symfony\Component\HttpKernel\DataCollector\RequestDataCollector; 18 | use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; 19 | use Symfony\Component\HttpKernel\Profiler\Profiler; 20 | use Symfony\Component\Routing\Matcher\TraceableUrlMatcher; 21 | use Symfony\Component\Routing\Matcher\UrlMatcherInterface; 22 | use Symfony\Component\Routing\RouteCollection; 23 | use Symfony\Component\Routing\RouterInterface; 24 | use Twig\Environment; 25 | 26 | /** 27 | * @author Fabien Potencier 28 | * 29 | * @internal 30 | */ 31 | class RouterController 32 | { 33 | /** 34 | * @param ExpressionFunctionProviderInterface[] $expressionLanguageProviders 35 | */ 36 | public function __construct( 37 | private ?Profiler $profiler, 38 | private Environment $twig, 39 | private ?UrlMatcherInterface $matcher = null, 40 | private ?RouteCollection $routes = null, 41 | private iterable $expressionLanguageProviders = [], 42 | ) { 43 | if ($this->matcher instanceof RouterInterface) { 44 | $this->routes ??= $this->matcher->getRouteCollection(); 45 | } 46 | } 47 | 48 | /** 49 | * Renders the profiler panel for the given token. 50 | * 51 | * @throws NotFoundHttpException 52 | */ 53 | public function panelAction(string $token): Response 54 | { 55 | if (null === $this->profiler) { 56 | throw new NotFoundHttpException('The profiler must be enabled.'); 57 | } 58 | 59 | $this->profiler->disable(); 60 | 61 | if (null === $this->matcher || null === $this->routes) { 62 | return new Response('The Router is not enabled.', 200, ['Content-Type' => 'text/html']); 63 | } 64 | 65 | $profile = $this->profiler->loadProfile($token); 66 | 67 | /** @var RequestDataCollector $request */ 68 | $request = $profile->getCollector('request'); 69 | 70 | return new Response($this->twig->render('@WebProfiler/Router/panel.html.twig', [ 71 | 'request' => $request, 72 | 'router' => $profile->getCollector('router'), 73 | 'traces' => $this->getTraces($request, $profile->getMethod()), 74 | ]), 200, ['Content-Type' => 'text/html']); 75 | } 76 | 77 | /** 78 | * Returns the routing traces associated to the given request. 79 | */ 80 | private function getTraces(RequestDataCollector $request, string $method): array 81 | { 82 | $traceRequest = new Request( 83 | $request->getRequestQuery()->all(), 84 | $request->getRequestRequest()->all(), 85 | $request->getRequestAttributes()->all(), 86 | $request->getRequestCookies(true)->all(), 87 | [], 88 | $request->getRequestServer(true)->all() 89 | ); 90 | 91 | $context = $this->matcher->getContext(); 92 | $context->setMethod($method); 93 | $matcher = new TraceableUrlMatcher($this->routes, $context); 94 | foreach ($this->expressionLanguageProviders as $provider) { 95 | $matcher->addExpressionLanguageProvider($provider); 96 | } 97 | 98 | return $matcher->getTracesForRequest($traceRequest); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Csp/ContentSecurityPolicyHandler.php: -------------------------------------------------------------------------------- 1 | 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\Bundle\WebProfilerBundle\Csp; 13 | 14 | use Symfony\Component\HttpFoundation\Request; 15 | use Symfony\Component\HttpFoundation\Response; 16 | 17 | /** 18 | * Handles Content-Security-Policy HTTP header for the WebProfiler Bundle. 19 | * 20 | * @author Romain Neutron 21 | * 22 | * @internal 23 | */ 24 | class ContentSecurityPolicyHandler 25 | { 26 | private bool $cspDisabled = false; 27 | 28 | public function __construct( 29 | private NonceGenerator $nonceGenerator, 30 | ) { 31 | } 32 | 33 | /** 34 | * Returns an array of nonces to be used in Twig templates and Content-Security-Policy headers. 35 | * 36 | * Nonce can be provided by; 37 | * - The request - In case HTML content is fetched via AJAX and inserted in DOM, it must use the same nonce as origin 38 | * - The response - A call to getNonces() has already been done previously. Same nonce are returned 39 | * - They are otherwise randomly generated 40 | */ 41 | public function getNonces(Request $request, Response $response): array 42 | { 43 | if ($request->headers->has('X-SymfonyProfiler-Script-Nonce') && $request->headers->has('X-SymfonyProfiler-Style-Nonce')) { 44 | return [ 45 | 'csp_script_nonce' => $request->headers->get('X-SymfonyProfiler-Script-Nonce'), 46 | 'csp_style_nonce' => $request->headers->get('X-SymfonyProfiler-Style-Nonce'), 47 | ]; 48 | } 49 | 50 | if ($response->headers->has('X-SymfonyProfiler-Script-Nonce') && $response->headers->has('X-SymfonyProfiler-Style-Nonce')) { 51 | return [ 52 | 'csp_script_nonce' => $response->headers->get('X-SymfonyProfiler-Script-Nonce'), 53 | 'csp_style_nonce' => $response->headers->get('X-SymfonyProfiler-Style-Nonce'), 54 | ]; 55 | } 56 | 57 | $nonces = [ 58 | 'csp_script_nonce' => $this->generateNonce(), 59 | 'csp_style_nonce' => $this->generateNonce(), 60 | ]; 61 | 62 | $response->headers->set('X-SymfonyProfiler-Script-Nonce', $nonces['csp_script_nonce']); 63 | $response->headers->set('X-SymfonyProfiler-Style-Nonce', $nonces['csp_style_nonce']); 64 | 65 | return $nonces; 66 | } 67 | 68 | /** 69 | * Disables Content-Security-Policy. 70 | * 71 | * All related headers will be removed. 72 | */ 73 | public function disableCsp(): void 74 | { 75 | $this->cspDisabled = true; 76 | } 77 | 78 | /** 79 | * Cleanup temporary headers and updates Content-Security-Policy headers. 80 | * 81 | * @return array Nonces used by the bundle in Content-Security-Policy header 82 | */ 83 | public function updateResponseHeaders(Request $request, Response $response): array 84 | { 85 | if ($this->cspDisabled) { 86 | $this->removeCspHeaders($response); 87 | 88 | return []; 89 | } 90 | 91 | $nonces = $this->getNonces($request, $response); 92 | $this->cleanHeaders($response); 93 | $this->updateCspHeaders($response, $nonces); 94 | 95 | return $nonces; 96 | } 97 | 98 | private function cleanHeaders(Response $response): void 99 | { 100 | $response->headers->remove('X-SymfonyProfiler-Script-Nonce'); 101 | $response->headers->remove('X-SymfonyProfiler-Style-Nonce'); 102 | } 103 | 104 | private function removeCspHeaders(Response $response): void 105 | { 106 | $response->headers->remove('X-Content-Security-Policy'); 107 | $response->headers->remove('Content-Security-Policy'); 108 | $response->headers->remove('Content-Security-Policy-Report-Only'); 109 | } 110 | 111 | /** 112 | * Updates Content-Security-Policy headers in a response. 113 | */ 114 | private function updateCspHeaders(Response $response, array $nonces = []): array 115 | { 116 | $nonces = array_replace([ 117 | 'csp_script_nonce' => $this->generateNonce(), 118 | 'csp_style_nonce' => $this->generateNonce(), 119 | ], $nonces); 120 | 121 | $ruleIsSet = false; 122 | 123 | $headers = $this->getCspHeaders($response); 124 | 125 | $types = [ 126 | 'script-src' => 'csp_script_nonce', 127 | 'script-src-elem' => 'csp_script_nonce', 128 | 'style-src' => 'csp_style_nonce', 129 | 'style-src-elem' => 'csp_style_nonce', 130 | ]; 131 | 132 | foreach ($headers as $header => $directives) { 133 | foreach ($types as $type => $tokenName) { 134 | if ($this->authorizesInline($directives, $type)) { 135 | continue; 136 | } 137 | if (!isset($headers[$header][$type])) { 138 | if (null === $fallback = $this->getDirectiveFallback($directives, $type)) { 139 | continue; 140 | } 141 | 142 | if (['\'none\''] === $fallback) { 143 | // Fallback came from "default-src: 'none'" 144 | // 'none' is invalid if it's not the only expression in the source list, so we leave it out 145 | $fallback = []; 146 | } 147 | 148 | $headers[$header][$type] = $fallback; 149 | } 150 | $ruleIsSet = true; 151 | if (!\in_array('\'unsafe-inline\'', $headers[$header][$type], true)) { 152 | $headers[$header][$type][] = '\'unsafe-inline\''; 153 | } 154 | $headers[$header][$type][] = \sprintf('\'nonce-%s\'', $nonces[$tokenName]); 155 | } 156 | } 157 | 158 | if (!$ruleIsSet) { 159 | return $nonces; 160 | } 161 | 162 | foreach ($headers as $header => $directives) { 163 | $response->headers->set($header, $this->generateCspHeader($directives)); 164 | } 165 | 166 | return $nonces; 167 | } 168 | 169 | /** 170 | * Generates a valid Content-Security-Policy nonce. 171 | */ 172 | private function generateNonce(): string 173 | { 174 | return $this->nonceGenerator->generate(); 175 | } 176 | 177 | /** 178 | * Converts a directive set array into Content-Security-Policy header. 179 | */ 180 | private function generateCspHeader(array $directives): string 181 | { 182 | return array_reduce(array_keys($directives), fn ($res, $name) => ('' !== $res ? $res.'; ' : '').\sprintf('%s %s', $name, implode(' ', $directives[$name])), ''); 183 | } 184 | 185 | /** 186 | * Converts a Content-Security-Policy header value into a directive set array. 187 | */ 188 | private function parseDirectives(string $header): array 189 | { 190 | $directives = []; 191 | 192 | foreach (explode(';', $header) as $directive) { 193 | $parts = explode(' ', trim($directive)); 194 | if (\count($parts) < 1) { 195 | continue; 196 | } 197 | $name = array_shift($parts); 198 | $directives[$name] = $parts; 199 | } 200 | 201 | return $directives; 202 | } 203 | 204 | /** 205 | * Detects if the 'unsafe-inline' is prevented for a directive within the directive set. 206 | */ 207 | private function authorizesInline(array $directivesSet, string $type): bool 208 | { 209 | if (isset($directivesSet[$type])) { 210 | $directives = $directivesSet[$type]; 211 | } elseif (null === $directives = $this->getDirectiveFallback($directivesSet, $type)) { 212 | return false; 213 | } 214 | 215 | return \in_array('\'unsafe-inline\'', $directives, true) && !$this->hasHashOrNonce($directives); 216 | } 217 | 218 | private function hasHashOrNonce(array $directives): bool 219 | { 220 | foreach ($directives as $directive) { 221 | if (!str_ends_with($directive, '\'')) { 222 | continue; 223 | } 224 | if (str_starts_with($directive, '\'nonce-')) { 225 | return true; 226 | } 227 | if (\in_array(substr($directive, 0, 8), ['\'sha256-', '\'sha384-', '\'sha512-'], true)) { 228 | return true; 229 | } 230 | } 231 | 232 | return false; 233 | } 234 | 235 | private function getDirectiveFallback(array $directiveSet, string $type): ?array 236 | { 237 | if (\in_array($type, ['script-src-elem', 'style-src-elem'], true) || !isset($directiveSet['default-src'])) { 238 | // Let the browser fallback on it's own 239 | return null; 240 | } 241 | 242 | return $directiveSet['default-src']; 243 | } 244 | 245 | /** 246 | * Retrieves the Content-Security-Policy headers (either X-Content-Security-Policy or Content-Security-Policy) from 247 | * a response. 248 | */ 249 | private function getCspHeaders(Response $response): array 250 | { 251 | $headers = []; 252 | 253 | if ($response->headers->has('Content-Security-Policy')) { 254 | $headers['Content-Security-Policy'] = $this->parseDirectives($response->headers->get('Content-Security-Policy')); 255 | } 256 | 257 | if ($response->headers->has('Content-Security-Policy-Report-Only')) { 258 | $headers['Content-Security-Policy-Report-Only'] = $this->parseDirectives($response->headers->get('Content-Security-Policy-Report-Only')); 259 | } 260 | 261 | if ($response->headers->has('X-Content-Security-Policy')) { 262 | $headers['X-Content-Security-Policy'] = $this->parseDirectives($response->headers->get('X-Content-Security-Policy')); 263 | } 264 | 265 | return $headers; 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /Csp/NonceGenerator.php: -------------------------------------------------------------------------------- 1 | 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\Bundle\WebProfilerBundle\Csp; 13 | 14 | /** 15 | * Generates Content-Security-Policy nonce. 16 | * 17 | * @author Romain Neutron 18 | * 19 | * @internal 20 | */ 21 | class NonceGenerator 22 | { 23 | public function generate(): string 24 | { 25 | return bin2hex(random_bytes(16)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | 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\Bundle\WebProfilerBundle\DependencyInjection; 13 | 14 | use Symfony\Component\Config\Definition\Builder\TreeBuilder; 15 | use Symfony\Component\Config\Definition\ConfigurationInterface; 16 | 17 | /** 18 | * This class contains the configuration information for the bundle. 19 | * 20 | * This information is solely responsible for how the different configuration 21 | * sections are normalized, and merged. 22 | * 23 | * @author Fabien Potencier 24 | */ 25 | class Configuration implements ConfigurationInterface 26 | { 27 | /** 28 | * Generates the configuration tree builder. 29 | */ 30 | public function getConfigTreeBuilder(): TreeBuilder 31 | { 32 | $treeBuilder = new TreeBuilder('web_profiler'); 33 | 34 | $treeBuilder 35 | ->getRootNode() 36 | ->docUrl('https://symfony.com/doc/{version:major}.{version:minor}/reference/configuration/web_profiler.html', 'symfony/web-profiler-bundle') 37 | ->children() 38 | ->arrayNode('toolbar') 39 | ->info('Profiler toolbar configuration') 40 | ->canBeEnabled() 41 | ->children() 42 | ->booleanNode('ajax_replace') 43 | ->defaultFalse() 44 | ->info('Replace toolbar on AJAX requests') 45 | ->end() 46 | ->end() 47 | ->end() 48 | ->booleanNode('intercept_redirects')->defaultFalse()->end() 49 | ->scalarNode('excluded_ajax_paths')->defaultValue('^/((index|app(_[\w]+)?)\.php/)?_wdt')->end() 50 | ->end() 51 | ; 52 | 53 | return $treeBuilder; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /DependencyInjection/WebProfilerExtension.php: -------------------------------------------------------------------------------- 1 | 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\Bundle\WebProfilerBundle\DependencyInjection; 13 | 14 | use Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener; 15 | use Symfony\Component\Config\FileLocator; 16 | use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; 17 | use Symfony\Component\DependencyInjection\ContainerBuilder; 18 | use Symfony\Component\DependencyInjection\Extension\Extension; 19 | use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; 20 | use Symfony\Component\DependencyInjection\Reference; 21 | 22 | /** 23 | * WebProfilerExtension. 24 | * 25 | * Usage: 26 | * 27 | * 31 | * 32 | * @author Fabien Potencier 33 | */ 34 | class WebProfilerExtension extends Extension 35 | { 36 | /** 37 | * Loads the web profiler configuration. 38 | * 39 | * @param array $configs An array of configuration settings 40 | */ 41 | public function load(array $configs, ContainerBuilder $container): void 42 | { 43 | $configuration = $this->getConfiguration($configs, $container); 44 | $config = $this->processConfiguration($configuration, $configs); 45 | 46 | $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 47 | $loader->load('profiler.php'); 48 | 49 | if ($config['toolbar']['enabled'] || $config['intercept_redirects']) { 50 | $loader->load('toolbar.php'); 51 | $container->getDefinition('web_profiler.debug_toolbar')->replaceArgument(4, $config['excluded_ajax_paths']); 52 | $container->getDefinition('web_profiler.debug_toolbar')->replaceArgument(7, $config['toolbar']['ajax_replace']); 53 | $container->setParameter('web_profiler.debug_toolbar.intercept_redirects', $config['intercept_redirects']); 54 | $container->setParameter('web_profiler.debug_toolbar.mode', $config['toolbar']['enabled'] ? WebDebugToolbarListener::ENABLED : WebDebugToolbarListener::DISABLED); 55 | } 56 | 57 | $container->getDefinition('debug.file_link_formatter') 58 | ->replaceArgument(3, new ServiceClosureArgument(new Reference('debug.file_link_formatter.url_format'))); 59 | } 60 | 61 | public function getXsdValidationBasePath(): string|false 62 | { 63 | return __DIR__.'/../Resources/config/schema'; 64 | } 65 | 66 | public function getNamespace(): string 67 | { 68 | return 'http://symfony.com/schema/dic/webprofiler'; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /EventListener/WebDebugToolbarListener.php: -------------------------------------------------------------------------------- 1 | 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\Bundle\WebProfilerBundle\EventListener; 13 | 14 | use Symfony\Bundle\FullStack; 15 | use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler; 16 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 17 | use Symfony\Component\HttpFoundation\Request; 18 | use Symfony\Component\HttpFoundation\Response; 19 | use Symfony\Component\HttpFoundation\Session\Flash\AutoExpireFlashBag; 20 | use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector; 21 | use Symfony\Component\HttpKernel\Event\ResponseEvent; 22 | use Symfony\Component\HttpKernel\KernelEvents; 23 | use Symfony\Component\Routing\Generator\UrlGeneratorInterface; 24 | use Twig\Environment; 25 | 26 | /** 27 | * WebDebugToolbarListener injects the Web Debug Toolbar. 28 | * 29 | * The onKernelResponse method must be connected to the kernel.response event. 30 | * 31 | * The WDT is only injected on well-formed HTML (with a proper tag). 32 | * This means that the WDT is never included in sub-requests or ESI requests. 33 | * 34 | * @author Fabien Potencier 35 | * 36 | * @final 37 | */ 38 | class WebDebugToolbarListener implements EventSubscriberInterface 39 | { 40 | public const DISABLED = 1; 41 | public const ENABLED = 2; 42 | 43 | public function __construct( 44 | private Environment $twig, 45 | private bool $interceptRedirects = false, 46 | private int $mode = self::ENABLED, 47 | private ?UrlGeneratorInterface $urlGenerator = null, 48 | private string $excludedAjaxPaths = '^/bundles|^/_wdt', 49 | private ?ContentSecurityPolicyHandler $cspHandler = null, 50 | private ?DumpDataCollector $dumpDataCollector = null, 51 | private bool $ajaxReplace = false, 52 | ) { 53 | } 54 | 55 | public function isEnabled(): bool 56 | { 57 | return self::DISABLED !== $this->mode; 58 | } 59 | 60 | public function setMode(int $mode): void 61 | { 62 | if (self::DISABLED !== $mode && self::ENABLED !== $mode) { 63 | throw new \InvalidArgumentException(\sprintf('Invalid value provided for mode, use one of "%s::DISABLED" or "%s::ENABLED".', self::class, self::class)); 64 | } 65 | 66 | $this->mode = $mode; 67 | } 68 | 69 | public function onKernelResponse(ResponseEvent $event): void 70 | { 71 | $response = $event->getResponse(); 72 | $request = $event->getRequest(); 73 | 74 | if ($response->headers->has('X-Debug-Token') && null !== $this->urlGenerator) { 75 | try { 76 | $response->headers->set( 77 | 'X-Debug-Token-Link', 78 | $this->urlGenerator->generate('_profiler', ['token' => $response->headers->get('X-Debug-Token')], UrlGeneratorInterface::ABSOLUTE_URL) 79 | ); 80 | } catch (\Exception $e) { 81 | $response->headers->set('X-Debug-Error', $e::class.': '.preg_replace('/\s+/', ' ', $e->getMessage())); 82 | } 83 | } 84 | 85 | if (!$event->isMainRequest()) { 86 | return; 87 | } 88 | 89 | $nonces = []; 90 | if ($this->cspHandler) { 91 | if ($this->dumpDataCollector?->getDumpsCount() > 0) { 92 | $this->cspHandler->disableCsp(); 93 | } 94 | 95 | $nonces = $this->cspHandler->updateResponseHeaders($request, $response); 96 | } 97 | 98 | // do not capture redirects or modify XML HTTP Requests 99 | if ($request->isXmlHttpRequest()) { 100 | if (self::ENABLED === $this->mode && $this->ajaxReplace && !$response->headers->has('Symfony-Debug-Toolbar-Replace')) { 101 | $response->headers->set('Symfony-Debug-Toolbar-Replace', '1'); 102 | } 103 | 104 | return; 105 | } 106 | 107 | if ($response->headers->has('X-Debug-Token') && $response->isRedirect() && $this->interceptRedirects && 'html' === $request->getRequestFormat() && $response->headers->has('Location')) { 108 | if ($request->hasSession() && ($session = $request->getSession())->isStarted() && $session->getFlashBag() instanceof AutoExpireFlashBag) { 109 | // keep current flashes for one more request if using AutoExpireFlashBag 110 | $session->getFlashBag()->setAll($session->getFlashBag()->peekAll()); 111 | } 112 | 113 | $response->setContent($this->twig->render('@WebProfiler/Profiler/toolbar_redirect.html.twig', ['location' => $response->headers->get('Location'), 'host' => $request->getSchemeAndHttpHost()])); 114 | $response->setStatusCode(200); 115 | $response->headers->remove('Location'); 116 | } 117 | 118 | if (self::DISABLED === $this->mode 119 | || !$response->headers->has('X-Debug-Token') 120 | || $response->isRedirection() 121 | || ($response->headers->has('Content-Type') && !str_contains($response->headers->get('Content-Type') ?? '', 'html')) 122 | || 'html' !== $request->getRequestFormat() 123 | || false !== stripos($response->headers->get('Content-Disposition', ''), 'attachment;') 124 | ) { 125 | return; 126 | } 127 | 128 | $this->injectToolbar($response, $request, $nonces); 129 | } 130 | 131 | /** 132 | * Injects the web debug toolbar into the given Response. 133 | */ 134 | protected function injectToolbar(Response $response, Request $request, array $nonces): void 135 | { 136 | $content = $response->getContent(); 137 | $pos = strripos($content, ''); 138 | 139 | if (false !== $pos) { 140 | $toolbar = "\n".str_replace("\n", '', $this->twig->render( 141 | '@WebProfiler/Profiler/toolbar_js.html.twig', 142 | [ 143 | 'full_stack' => class_exists(FullStack::class), 144 | 'excluded_ajax_paths' => $this->excludedAjaxPaths, 145 | 'token' => $response->headers->get('X-Debug-Token'), 146 | 'request' => $request, 147 | 'csp_script_nonce' => $nonces['csp_script_nonce'] ?? null, 148 | 'csp_style_nonce' => $nonces['csp_style_nonce'] ?? null, 149 | ] 150 | ))."\n"; 151 | $content = substr($content, 0, $pos).$toolbar.substr($content, $pos); 152 | $response->setContent($content); 153 | } 154 | } 155 | 156 | public static function getSubscribedEvents(): array 157 | { 158 | return [ 159 | KernelEvents::RESPONSE => ['onKernelResponse', -128], 160 | ]; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2004-present Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Profiler/CodeExtension.php: -------------------------------------------------------------------------------- 1 | 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\Bundle\WebProfilerBundle\Profiler; 13 | 14 | use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter; 15 | use Twig\Extension\AbstractExtension; 16 | use Twig\TwigFilter; 17 | 18 | /** 19 | * Twig extension relate to PHP code and used by the profiler and the default exception templates. 20 | * 21 | * This extension should only be used for debugging tools code 22 | * that is never executed in a production environment. 23 | * 24 | * @author Fabien Potencier 25 | */ 26 | final class CodeExtension extends AbstractExtension 27 | { 28 | private string|FileLinkFormatter|array|false $fileLinkFormat; 29 | 30 | public function __construct( 31 | string|FileLinkFormatter $fileLinkFormat, 32 | private string $projectDir, 33 | private string $charset, 34 | ) { 35 | $this->fileLinkFormat = $fileLinkFormat ?: \ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); 36 | $this->projectDir = str_replace('\\', '/', $projectDir).'/'; 37 | } 38 | 39 | public function getFilters(): array 40 | { 41 | return [ 42 | new TwigFilter('abbr_class', $this->abbrClass(...), ['is_safe' => ['html'], 'pre_escape' => 'html']), 43 | new TwigFilter('abbr_method', $this->abbrMethod(...), ['is_safe' => ['html'], 'pre_escape' => 'html']), 44 | new TwigFilter('format_args', $this->formatArgs(...), ['is_safe' => ['html']]), 45 | new TwigFilter('format_args_as_text', $this->formatArgsAsText(...)), 46 | new TwigFilter('file_excerpt', $this->fileExcerpt(...), ['is_safe' => ['html']]), 47 | new TwigFilter('format_file', $this->formatFile(...), ['is_safe' => ['html']]), 48 | new TwigFilter('format_file_from_text', $this->formatFileFromText(...), ['is_safe' => ['html']]), 49 | new TwigFilter('format_log_message', $this->formatLogMessage(...), ['is_safe' => ['html']]), 50 | new TwigFilter('file_link', $this->getFileLink(...)), 51 | new TwigFilter('file_relative', $this->getFileRelative(...)), 52 | ]; 53 | } 54 | 55 | public function abbrClass(string $class): string 56 | { 57 | $parts = explode('\\', $class); 58 | $short = array_pop($parts); 59 | 60 | return \sprintf('%s', $class, $short); 61 | } 62 | 63 | public function abbrMethod(string $method): string 64 | { 65 | if (str_contains($method, '::')) { 66 | [$class, $method] = explode('::', $method, 2); 67 | $result = \sprintf('%s::%s()', $this->abbrClass($class), $method); 68 | } elseif ('Closure' === $method) { 69 | $result = \sprintf('%1$s', $method); 70 | } else { 71 | $result = \sprintf('%1$s()', $method); 72 | } 73 | 74 | return $result; 75 | } 76 | 77 | /** 78 | * Formats an array as a string. 79 | */ 80 | public function formatArgs(array $args): string 81 | { 82 | $result = []; 83 | foreach ($args as $key => $item) { 84 | if ('object' === $item[0]) { 85 | $item[1] = htmlspecialchars($item[1], \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset); 86 | $parts = explode('\\', $item[1]); 87 | $short = array_pop($parts); 88 | $formattedValue = \sprintf('object(%s)', $item[1], $short); 89 | } elseif ('array' === $item[0]) { 90 | $formattedValue = \sprintf('array(%s)', \is_array($item[1]) ? $this->formatArgs($item[1]) : htmlspecialchars(var_export($item[1], true), \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset)); 91 | } elseif ('null' === $item[0]) { 92 | $formattedValue = 'null'; 93 | } elseif ('boolean' === $item[0]) { 94 | $formattedValue = ''.strtolower(htmlspecialchars(var_export($item[1], true), \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset)).''; 95 | } elseif ('resource' === $item[0]) { 96 | $formattedValue = 'resource'; 97 | } elseif (preg_match('/[^\x07-\x0D\x1B\x20-\xFF]/', $item[1])) { 98 | $formattedValue = 'binary string'; 99 | } else { 100 | $formattedValue = str_replace("\n", '', htmlspecialchars(var_export($item[1], true), \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset)); 101 | } 102 | 103 | $result[] = \is_int($key) ? $formattedValue : \sprintf("'%s' => %s", htmlspecialchars($key, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset), $formattedValue); 104 | } 105 | 106 | return implode(', ', $result); 107 | } 108 | 109 | /** 110 | * Formats an array as a string. 111 | */ 112 | public function formatArgsAsText(array $args): string 113 | { 114 | return strip_tags($this->formatArgs($args)); 115 | } 116 | 117 | /** 118 | * Returns an excerpt of a code file around the given line number. 119 | */ 120 | public function fileExcerpt(string $file, int $line, int $srcContext = 3): ?string 121 | { 122 | if (is_file($file) && is_readable($file)) { 123 | // highlight_file could throw warnings 124 | // see https://bugs.php.net/25725 125 | $code = @highlight_file($file, true); 126 | if (\PHP_VERSION_ID >= 80300) { 127 | // remove main pre/code tags 128 | $code = preg_replace('#^\s*(.*)\s*#s', '\\1', $code); 129 | // split multiline span tags 130 | $code = preg_replace_callback('#]++)>((?:[^<\\n]*+\\n)++[^<]*+)#', function ($m) { 131 | return "".str_replace("\n", "\n", $m[2]).''; 132 | }, $code); 133 | $content = explode("\n", $code); 134 | } else { 135 | // remove main code/span tags 136 | $code = preg_replace('#^\s*(.*)\s*#s', '\\1', $code); 137 | // split multiline spans 138 | $code = preg_replace_callback('#]++)>((?:[^<]*+
)++[^<]*+)
#', fn ($m) => "".str_replace('
', "

", $m[2]).'', $code); 139 | $content = explode('
', $code); 140 | } 141 | 142 | $lines = []; 143 | if (0 > $srcContext) { 144 | $srcContext = \count($content); 145 | } 146 | 147 | for ($i = max($line - $srcContext, 1), $max = min($line + $srcContext, \count($content)); $i <= $max; ++$i) { 148 | $lines[] = ''.self::fixCodeMarkup($content[$i - 1]).''; 149 | } 150 | 151 | return '
    '.implode("\n", $lines).'
'; 152 | } 153 | 154 | return null; 155 | } 156 | 157 | /** 158 | * Formats a file path. 159 | */ 160 | public function formatFile(string $file, int $line, ?string $text = null): string 161 | { 162 | $file = trim($file); 163 | 164 | if (null === $text) { 165 | if (null !== $rel = $this->getFileRelative($file)) { 166 | $rel = explode('/', htmlspecialchars($rel, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset), 2); 167 | $text = \sprintf('%s%s', htmlspecialchars($this->projectDir, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset), $rel[0], '/'.($rel[1] ?? '')); 168 | } else { 169 | $text = htmlspecialchars($file, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset); 170 | } 171 | } else { 172 | $text = htmlspecialchars($text, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset); 173 | } 174 | 175 | if (0 < $line) { 176 | $text .= ' at line '.$line; 177 | } 178 | 179 | if (false !== $link = $this->getFileLink($file, $line)) { 180 | return \sprintf('%s', htmlspecialchars($link, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset), $text); 181 | } 182 | 183 | return $text; 184 | } 185 | 186 | public function getFileLink(string $file, int $line): string|false 187 | { 188 | if ($fmt = $this->fileLinkFormat) { 189 | return \is_string($fmt) ? strtr($fmt, ['%f' => $file, '%l' => $line]) : $fmt->format($file, $line); 190 | } 191 | 192 | return false; 193 | } 194 | 195 | public function getFileRelative(string $file): ?string 196 | { 197 | $file = str_replace('\\', '/', $file); 198 | 199 | if (null !== $this->projectDir && str_starts_with($file, $this->projectDir)) { 200 | return ltrim(substr($file, \strlen($this->projectDir)), '/'); 201 | } 202 | 203 | return null; 204 | } 205 | 206 | public function formatFileFromText(string $text): string 207 | { 208 | return preg_replace_callback('/in ("|")?(.+?)\1(?: +(?:on|at))? +line (\d+)/s', fn ($match) => 'in '.$this->formatFile($match[2], $match[3]), $text); 209 | } 210 | 211 | /** 212 | * @internal 213 | */ 214 | public function formatLogMessage(string $message, array $context): string 215 | { 216 | if ($context && str_contains($message, '{')) { 217 | $replacements = []; 218 | foreach ($context as $key => $val) { 219 | if (\is_scalar($val)) { 220 | $replacements['{'.$key.'}'] = $val; 221 | } 222 | } 223 | 224 | if ($replacements) { 225 | $message = strtr($message, $replacements); 226 | } 227 | } 228 | 229 | return htmlspecialchars($message, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset); 230 | } 231 | 232 | protected static function fixCodeMarkup(string $line): string 233 | { 234 | // ending tag from previous line 235 | $opening = strpos($line, ''); 237 | if (false !== $closing && (false === $opening || $closing < $opening)) { 238 | $line = substr_replace($line, '', $closing, 7); 239 | } 240 | 241 | // missing tag at the end of line 242 | $opening = strpos($line, ''); 244 | if (false !== $opening && (false === $closing || $closing > $opening)) { 245 | $line .= ''; 246 | } 247 | 248 | return trim($line); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /Profiler/TemplateManager.php: -------------------------------------------------------------------------------- 1 | 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\Bundle\WebProfilerBundle\Profiler; 13 | 14 | use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; 15 | use Symfony\Component\HttpKernel\Profiler\Profile; 16 | use Symfony\Component\HttpKernel\Profiler\Profiler; 17 | use Twig\Environment; 18 | 19 | /** 20 | * @author Fabien Potencier 21 | * @author Artur Wielogórski 22 | * 23 | * @internal 24 | */ 25 | class TemplateManager 26 | { 27 | public function __construct( 28 | protected Profiler $profiler, 29 | protected Environment $twig, 30 | protected array $templates, 31 | ) { 32 | } 33 | 34 | /** 35 | * Gets the template name for a given panel. 36 | * 37 | * @throws NotFoundHttpException 38 | */ 39 | public function getName(Profile $profile, string $panel): mixed 40 | { 41 | $templates = $this->getNames($profile); 42 | 43 | if (!isset($templates[$panel])) { 44 | throw new NotFoundHttpException(\sprintf('Panel "%s" is not registered in profiler or is not present in viewed profile.', $panel)); 45 | } 46 | 47 | return $templates[$panel]; 48 | } 49 | 50 | /** 51 | * Gets template names of templates that are present in the viewed profile. 52 | * 53 | * @throws \UnexpectedValueException 54 | */ 55 | public function getNames(Profile $profile): array 56 | { 57 | $loader = $this->twig->getLoader(); 58 | $templates = []; 59 | 60 | foreach ($this->templates as $arguments) { 61 | if (null === $arguments) { 62 | continue; 63 | } 64 | 65 | [$name, $template] = $arguments; 66 | 67 | if (!$this->profiler->has($name) || !$profile->hasCollector($name)) { 68 | continue; 69 | } 70 | 71 | if (str_ends_with($template, '.html.twig')) { 72 | $template = substr($template, 0, -10); 73 | } 74 | 75 | if (!$loader->exists($template.'.html.twig')) { 76 | throw new \UnexpectedValueException(\sprintf('The profiler template "%s.html.twig" for data collector "%s" does not exist.', $template, $name)); 77 | } 78 | 79 | $templates[$name] = $template.'.html.twig'; 80 | } 81 | 82 | return $templates; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WebProfilerBundle 2 | ================= 3 | 4 | WebProfilerBundle provides a **development tool** that gives detailed 5 | information about the execution of any request. 6 | 7 | **Never** enable it on production servers as it will lead to major security 8 | vulnerabilities in your project. 9 | 10 | Resources 11 | --------- 12 | 13 | * [Contributing](https://symfony.com/doc/current/contributing/index.html) 14 | * [Report issues](https://github.com/symfony/symfony/issues) and 15 | [send Pull Requests](https://github.com/symfony/symfony/pulls) 16 | in the [main Symfony repository](https://github.com/symfony/symfony) 17 | -------------------------------------------------------------------------------- /Resources/config/profiler.php: -------------------------------------------------------------------------------- 1 | 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\DependencyInjection\Loader\Configurator; 13 | 14 | use Symfony\Bundle\WebProfilerBundle\Controller\ExceptionPanelController; 15 | use Symfony\Bundle\WebProfilerBundle\Controller\ProfilerController; 16 | use Symfony\Bundle\WebProfilerBundle\Controller\RouterController; 17 | use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler; 18 | use Symfony\Bundle\WebProfilerBundle\Csp\NonceGenerator; 19 | use Symfony\Bundle\WebProfilerBundle\Profiler\CodeExtension; 20 | use Symfony\Bundle\WebProfilerBundle\Twig\WebProfilerExtension; 21 | use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter; 22 | use Symfony\Component\VarDumper\Dumper\HtmlDumper; 23 | 24 | return static function (ContainerConfigurator $container) { 25 | $container->services() 26 | 27 | ->set('web_profiler.controller.profiler', ProfilerController::class) 28 | ->public() 29 | ->args([ 30 | service('router')->nullOnInvalid(), 31 | service('profiler')->nullOnInvalid(), 32 | service('twig'), 33 | param('data_collector.templates'), 34 | service('web_profiler.csp.handler'), 35 | param('kernel.project_dir'), 36 | ]) 37 | 38 | ->set('web_profiler.controller.router', RouterController::class) 39 | ->public() 40 | ->args([ 41 | service('profiler')->nullOnInvalid(), 42 | service('twig'), 43 | service('router')->nullOnInvalid(), 44 | null, 45 | tagged_iterator('routing.expression_language_provider'), 46 | ]) 47 | 48 | ->set('web_profiler.controller.exception_panel', ExceptionPanelController::class) 49 | ->public() 50 | ->args([ 51 | service('error_handler.error_renderer.html'), 52 | service('profiler')->nullOnInvalid(), 53 | ]) 54 | 55 | ->set('web_profiler.csp.handler', ContentSecurityPolicyHandler::class) 56 | ->args([ 57 | inline_service(NonceGenerator::class), 58 | ]) 59 | 60 | ->set('twig.extension.webprofiler', WebProfilerExtension::class) 61 | ->args([ 62 | inline_service(HtmlDumper::class) 63 | ->args([null, param('kernel.charset'), HtmlDumper::DUMP_LIGHT_ARRAY]) 64 | ->call('setDisplayOptions', [['maxStringLength' => 4096, 'fileLinkFormat' => service('debug.file_link_formatter')]]), 65 | ]) 66 | ->tag('twig.extension') 67 | 68 | ->set('debug.file_link_formatter', FileLinkFormatter::class) 69 | ->args([ 70 | param('debug.file_link_format'), 71 | service('request_stack')->ignoreOnInvalid(), 72 | param('kernel.project_dir'), 73 | '/_profiler/open?file=%%f&line=%%l#line%%l', 74 | ]) 75 | 76 | ->set('debug.file_link_formatter.url_format', 'string') 77 | ->factory([FileLinkFormatter::class, 'generateUrlFormat']) 78 | ->args([ 79 | service('router'), 80 | '_profiler_open_file', 81 | '?file=%%f&line=%%l#line%%l', 82 | ]) 83 | 84 | ->set('twig.extension.code', CodeExtension::class) 85 | ->args([service('debug.file_link_formatter'), param('kernel.project_dir'), param('kernel.charset')]) 86 | ->tag('twig.extension') 87 | ; 88 | }; 89 | -------------------------------------------------------------------------------- /Resources/config/routing/profiler.php: -------------------------------------------------------------------------------- 1 | 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 | use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; 13 | use Symfony\Component\Routing\Loader\XmlFileLoader; 14 | 15 | return function (RoutingConfigurator $routes): void { 16 | foreach (debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT) as $trace) { 17 | if (isset($trace['object']) && $trace['object'] instanceof XmlFileLoader && 'doImport' === $trace['function']) { 18 | if (__DIR__ === dirname(realpath($trace['args'][3]))) { 19 | trigger_deprecation('symfony/routing', '7.3', 'The "profiler.xml" routing configuration file is deprecated, import "profile.php" instead.'); 20 | 21 | break; 22 | } 23 | } 24 | } 25 | 26 | $routes->add('_profiler_home', '/') 27 | ->controller('web_profiler.controller.profiler::homeAction') 28 | ; 29 | $routes->add('_profiler_search', '/search') 30 | ->controller('web_profiler.controller.profiler::searchAction') 31 | ; 32 | $routes->add('_profiler_search_bar', '/search_bar') 33 | ->controller('web_profiler.controller.profiler::searchBarAction') 34 | ; 35 | $routes->add('_profiler_phpinfo', '/phpinfo') 36 | ->controller('web_profiler.controller.profiler::phpinfoAction') 37 | ; 38 | $routes->add('_profiler_xdebug', '/xdebug') 39 | ->controller('web_profiler.controller.profiler::xdebugAction') 40 | ; 41 | $routes->add('_profiler_font', '/font/{fontName}.woff2') 42 | ->controller('web_profiler.controller.profiler::fontAction') 43 | ; 44 | $routes->add('_profiler_search_results', '/{token}/search/results') 45 | ->controller('web_profiler.controller.profiler::searchResultsAction') 46 | ; 47 | $routes->add('_profiler_open_file', '/open') 48 | ->controller('web_profiler.controller.profiler::openAction') 49 | ; 50 | $routes->add('_profiler', '/{token}') 51 | ->controller('web_profiler.controller.profiler::panelAction') 52 | ; 53 | $routes->add('_profiler_router', '/{token}/router') 54 | ->controller('web_profiler.controller.router::panelAction') 55 | ; 56 | $routes->add('_profiler_exception', '/{token}/exception') 57 | ->controller('web_profiler.controller.exception_panel::body') 58 | ; 59 | $routes->add('_profiler_exception_css', '/{token}/exception.css') 60 | ->controller('web_profiler.controller.exception_panel::stylesheet') 61 | ; 62 | }; 63 | -------------------------------------------------------------------------------- /Resources/config/routing/profiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Resources/config/routing/wdt.php: -------------------------------------------------------------------------------- 1 | 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 | use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; 13 | use Symfony\Component\Routing\Loader\XmlFileLoader; 14 | 15 | return function (RoutingConfigurator $routes): void { 16 | foreach (debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT) as $trace) { 17 | if (isset($trace['object']) && $trace['object'] instanceof XmlFileLoader && 'doImport' === $trace['function']) { 18 | if (__DIR__ === dirname(realpath($trace['args'][3]))) { 19 | trigger_deprecation('symfony/routing', '7.3', 'The "xdt.xml" routing configuration file is deprecated, import "xdt.php" instead.'); 20 | 21 | break; 22 | } 23 | } 24 | } 25 | 26 | $routes->add('_wdt_stylesheet', '/styles') 27 | ->controller('web_profiler.controller.profiler::toolbarStylesheetAction') 28 | ; 29 | $routes->add('_wdt', '/{token}') 30 | ->controller('web_profiler.controller.profiler::toolbarAction') 31 | ; 32 | }; 33 | -------------------------------------------------------------------------------- /Resources/config/routing/wdt.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Resources/config/schema/webprofiler-1.0.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Resources/config/toolbar.php: -------------------------------------------------------------------------------- 1 | 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\DependencyInjection\Loader\Configurator; 13 | 14 | use Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener; 15 | 16 | return static function (ContainerConfigurator $container) { 17 | $container->services() 18 | 19 | ->set('web_profiler.debug_toolbar', WebDebugToolbarListener::class) 20 | ->args([ 21 | service('twig'), 22 | param('web_profiler.debug_toolbar.intercept_redirects'), 23 | param('web_profiler.debug_toolbar.mode'), 24 | service('router')->ignoreOnInvalid(), 25 | abstract_arg('paths that should be excluded from the AJAX requests shown in the toolbar'), 26 | service('web_profiler.csp.handler'), 27 | service('data_collector.dump')->ignoreOnInvalid(), 28 | abstract_arg('whether to replace toolbar on AJAX requests or not'), 29 | ]) 30 | ->tag('kernel.event_subscriber') 31 | ; 32 | }; 33 | -------------------------------------------------------------------------------- /Resources/fonts/JetBrainsMono.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/symfony/web-profiler-bundle/9439737244d6a16aaad1a365587cfb368910c51d/Resources/fonts/JetBrainsMono.woff2 -------------------------------------------------------------------------------- /Resources/fonts/LICENSE.txt: -------------------------------------------------------------------------------- 1 | JetBrains Mono typeface (https://www.jetbrains.com/lp/mono/) is available 2 | under the SIL Open Font License 1.1 and can be used free of charge, for both 3 | commercial and non-commercial purposes. You do not need to give credit to 4 | JetBrains, although we will appreciate it very much if you do. 5 | 6 | Licence: https://github.com/JetBrains/JetBrainsMono/blob/master/OFL.txt 7 | -------------------------------------------------------------------------------- /Resources/views/Collector/ajax.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@WebProfiler/Profiler/layout.html.twig' %} 2 | 3 | {% block toolbar %} 4 | {% set icon %} 5 | {{ source('@WebProfiler/Icon/ajax.svg') }} 6 | 0 7 | {% endset %} 8 | 9 | {% set text %} 10 |
11 | 12 | 13 | (Clear) 14 | 15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
#ProfileMethodTypeStatusURLTime
31 |
32 | {% endset %} 33 | 34 | {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: false }) }} 35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /Resources/views/Collector/cache.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@WebProfiler/Profiler/layout.html.twig' %} 2 | 3 | {% block toolbar %} 4 | {% if collector.totals.calls > 0 %} 5 | {% set icon %} 6 | {{ source('@WebProfiler/Icon/cache.svg') }} 7 | {{ collector.totals.calls }} 8 | 9 | in 10 | {{ '%0.2f'|format(collector.totals.time * 1000) }} 11 | ms 12 | 13 | {% endset %} 14 | {% set text %} 15 |
16 | Cache Calls 17 | {{ collector.totals.calls }} 18 |
19 |
20 | Total time 21 | {{ '%0.2f'|format(collector.totals.time * 1000) }} ms 22 |
23 |
24 | Cache hits 25 | {{ collector.totals.hits }} / {{ collector.totals.reads }}{% if collector.totals.hit_read_ratio is not null %} ({{ collector.totals.hit_read_ratio }}%){% endif %} 26 |
27 |
28 | Cache writes 29 | {{ collector.totals.writes }} 30 |
31 | {% endset %} 32 | 33 | {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }} 34 | {% endif %} 35 | {% endblock %} 36 | 37 | {% block menu %} 38 | 39 | 40 | {{ source('@WebProfiler/Icon/cache.svg') }} 41 | 42 | Cache 43 | 44 | {% endblock %} 45 | 46 | {% block panel %} 47 |

Cache

48 | 49 | {% if collector.totals.calls == 0 %} 50 |
51 |

No cache calls were made.

52 |
53 | {% else %} 54 | {{ _self.render_metrics(collector.totals, true) }} 55 | 56 |

Pools

57 |
58 | {# the empty merge is needed to turn the iterator into an array #} 59 | {% set cache_pools_with_calls = collector.calls|filter(calls => calls|length > 0)|merge([]) %} 60 | {% for name, calls in cache_pools_with_calls %} 61 |
62 |

{{ name }} {{ collector.statistics[name].calls }}

63 | 64 |
65 |

Adapter

66 |
67 | {% if collector.adapters[name] is defined %} 68 | {{ collector.adapters[name] }} 69 | {% else %} 70 | Unable to get the adapter class. 71 | {% endif %} 72 |
73 | {% if calls|length == 0 %} 74 |
75 |

No calls were made for {{ name }} pool.

76 |
77 | {% else %} 78 |

Metrics

79 | {{ _self.render_metrics(collector.statistics[name]) }} 80 | 81 |

Calls

82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | {% for call in calls %} 93 | 94 | 95 | 96 | 97 | 98 | 99 | {% endfor %} 100 | 101 |
#TimeCallHit
{{ loop.index }}{{ '%0.2f'|format((call.end - call.start) * 1000) }} ms{{ call.name }}({{ call.namespace|default('') }}){{ profiler_dump(call.value.result, maxDepth=2) }}
102 | {% endif %} 103 |
104 |
105 | 106 | {% if loop.last %} 107 |
108 |

Pools without calls {{ collector.calls|filter(calls => 0 == calls|length)|length }}

109 | 110 |
111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | {% for cache_pool in collector.calls|filter(calls => 0 == calls|length)|keys|sort %} 119 | 120 | {% endfor %} 121 | 122 |
Cache pools that received no calls
{{ cache_pool }}
123 |
124 |
125 | {% endif %} 126 | {% endfor %} 127 |
128 | {% endif %} 129 | {% endblock %} 130 | 131 | {% macro render_metrics(pool, is_total = false) %} 132 |
133 |
134 | {{ pool.calls }} 135 | {{ is_total ? 'Total calls' : 'Calls' }} 136 |
137 |
138 | {{ '%0.2f'|format(pool.time * 1000) }} ms 139 | {{ is_total ? 'Total time' : 'Time' }} 140 |
141 | 142 |
143 | 144 |
145 |
146 | {{ pool.reads }} 147 | {{ is_total ? 'Total reads' : 'Reads' }} 148 |
149 |
150 | {{ pool.writes }} 151 | {{ is_total ? 'Total writes' : 'Writes' }} 152 |
153 |
154 | {{ pool.deletes }} 155 | {{ is_total ? 'Total deletes' : 'Deletes' }} 156 |
157 |
158 | 159 |
160 | 161 |
162 |
163 | {{ pool.hits }} 164 | {{ is_total ? 'Total hits' : 'Hits' }} 165 |
166 |
167 | {{ pool.misses }} 168 | {{ is_total ? 'Total misses' : 'Misses' }} 169 |
170 |
171 | 172 | {{ pool.hit_read_ratio ?? 0 }} % 173 | 174 | Hits/reads 175 |
176 |
177 |
178 | {% endmacro %} 179 | -------------------------------------------------------------------------------- /Resources/views/Collector/events.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@WebProfiler/Profiler/layout.html.twig' %} 2 | 3 | {% block menu %} 4 | 5 | {{ source('@WebProfiler/Icon/event.svg') }} 6 | Events 7 | 8 | {% endblock %} 9 | 10 | {% block panel %} 11 |

Dispatched Events

12 | 13 |
14 | {% for dispatcherName, dispatcherData in collector.data %} 15 |
16 |

{{ dispatcherName }}

17 |
18 | {% if dispatcherData['called_listeners'] is empty %} 19 |
20 |

No events have been recorded. Check that debugging is enabled in the kernel.

21 |
22 | {% else %} 23 |
24 |
25 |

Called Listeners {{ dispatcherData['called_listeners']|length }}

26 | 27 |
28 | {{ _self.render_table(dispatcherData['called_listeners']) }} 29 |
30 |
31 | 32 |
33 |

Not Called Listeners {{ dispatcherData['not_called_listeners']|length }}

34 |
35 | {% if dispatcherData['not_called_listeners'] is empty %} 36 |
37 |

38 | There are no uncalled listeners. 39 |

40 |

41 | All listeners were called or an error occurred 42 | when trying to collect uncalled listeners (in which case check the 43 | logs to get more information). 44 |

45 |
46 | {% else %} 47 | {{ _self.render_table(dispatcherData['not_called_listeners']) }} 48 | {% endif %} 49 |
50 |
51 | 52 |
53 |

Orphaned Events {{ dispatcherData['orphaned_events']|length }}

54 |
55 | {% if dispatcherData['orphaned_events'] is empty %} 56 |
57 |

58 | There are no orphaned events. 59 |

60 |

61 | All dispatched events were handled or an error occurred 62 | when trying to collect orphaned events (in which case check the 63 | logs to get more information). 64 |

65 |
66 | {% else %} 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | {% for event in dispatcherData['orphaned_events'] %} 75 | 76 | 77 | 78 | {% endfor %} 79 | 80 |
Event
{{ event }}
81 | {% endif %} 82 |
83 |
84 |
85 | {% endif %} 86 |
87 |
88 | {% endfor %} 89 |
90 | {% endblock %} 91 | 92 | {% macro render_table(listeners) %} 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | {% set previous_event = (listeners|first).event %} 102 | {% for listener in listeners %} 103 | {% if loop.first or listener.event != previous_event %} 104 | {% if not loop.first %} 105 | 106 | {% endif %} 107 | 108 | 109 | 110 | 111 | 112 | 113 | {% set previous_event = listener.event %} 114 | {% endif %} 115 | 116 | 117 | 118 | 119 | 120 | 121 | {% if loop.last %} 122 | 123 | {% endif %} 124 | {% endfor %} 125 |
PriorityListener
{{ listener.event }}
{{ listener.priority|default('-') }}{{ profiler_dump(listener.stub) }}
126 | {% endmacro %} 127 | -------------------------------------------------------------------------------- /Resources/views/Collector/exception.css.twig: -------------------------------------------------------------------------------- 1 | .container { 2 | max-width: none; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | .container .container { 7 | padding: 0; 8 | } 9 | 10 | .exception-summary { 11 | background: var(--base-0); 12 | border: var(--border); 13 | box-shadow: 0 0 1px rgba(128, 128, 128, .2); 14 | margin: 1em 0; 15 | padding: 10px; 16 | } 17 | .exception-summary.exception-without-message { 18 | display: none; 19 | } 20 | 21 | .exception-message { 22 | color: var(--color-error); 23 | } 24 | 25 | .exception-metadata, 26 | .exception-illustration { 27 | display: none; 28 | } 29 | 30 | .exception-message-wrapper .container { 31 | min-height: unset; 32 | } 33 | -------------------------------------------------------------------------------- /Resources/views/Collector/exception.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@WebProfiler/Profiler/layout.html.twig' %} 2 | 3 | {% block head %} 4 | {% if collector.hasexception %} 5 | 9 | {% endif %} 10 | {{ parent() }} 11 | {% endblock %} 12 | 13 | {% block menu %} 14 | 15 | {{ source('@WebProfiler/Icon/exception.svg') }} 16 | Exception 17 | {% if collector.hasexception %} 18 | 19 | 1 20 | 21 | {% endif %} 22 | 23 | {% endblock %} 24 | 25 | {% block panel %} 26 | {# these styles are needed to override some styles from Exception page, which wasn't 27 | updated yet to the new style of the Symfony Profiler #} 28 | 33 | 34 |

Exceptions

35 | 36 | {% if not collector.hasexception %} 37 |
38 |

No exception was thrown and caught.

39 |
40 | {% else %} 41 |
42 | {{ render(controller('web_profiler.controller.exception_panel::body', { token: token })) }} 43 |
44 | {% endif %} 45 | {% endblock %} 46 | -------------------------------------------------------------------------------- /Resources/views/Collector/http_client.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@WebProfiler/Profiler/layout.html.twig' %} 2 | 3 | {% block head %} 4 | {{ parent() }} 5 | 6 | 36 | {% endblock %} 37 | 38 | 39 | {% block toolbar %} 40 | {% if collector.requestCount %} 41 | {% set icon %} 42 | {{ source('@WebProfiler/Icon/http-client.svg') }} 43 | {% set status_color = '' %} 44 | {{ collector.requestCount }} 45 | {% endset %} 46 | 47 | {% set text %} 48 |
49 | Total requests 50 | {{ collector.requestCount }} 51 |
52 |
53 | HTTP errors 54 | {{ collector.errorCount }} 55 |
56 | {% endset %} 57 | 58 | {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }} 59 | {% endif %} 60 | {% endblock %} 61 | 62 | {% block menu %} 63 | 64 | {{ source('@WebProfiler/Icon/http-client.svg') }} 65 | HTTP Client 66 | {% if collector.requestCount %} 67 | 68 | {{ collector.requestCount }} 69 | 70 | {% endif %} 71 | 72 | {% endblock %} 73 | 74 | {% block panel %} 75 |

HTTP Client

76 | {% if collector.requestCount == 0 %} 77 |
78 |

No HTTP requests were made.

79 |
80 | {% else %} 81 |
82 |
83 | {{ collector.requestCount }} 84 | Total requests 85 |
86 |
87 | {{ collector.errorCount }} 88 | HTTP errors 89 |
90 |
91 |

Clients

92 |
93 | {% for name, client in collector.clients %} 94 |
95 |

{{ name }} {{ client.traces|length }}

96 |
97 | {% if client.traces|length == 0 %} 98 |
99 |

No requests were made with the "{{ name }}" service.

100 |
101 | {% else %} 102 |

Requests

103 | {% for trace in client.traces %} 104 | {% set profiler_token = '' %} 105 | {% set profiler_link = '' %} 106 | {% if trace.info.response_headers is defined %} 107 | {% for header in trace.info.response_headers %} 108 | {% if header matches '/^x-debug-token: .*$/i' %} 109 | {% set profiler_token = (header.getValue | slice('x-debug-token: ' | length)) %} 110 | {% endif %} 111 | {% if header matches '/^x-debug-token-link: .*$/i' %} 112 | {% set profiler_link = (header.getValue | slice('x-debug-token-link: ' | length)) %} 113 | {% endif %} 114 | {% endfor %} 115 | {% endif %} 116 | 117 | 118 | 119 | 122 | 125 | {% if profiler_token and profiler_link %} 126 | 129 | {% endif %} 130 | {% if trace.curlCommand is defined and trace.curlCommand %} 131 | 134 | {% endif %} 135 | 136 | 137 | 138 | {% if trace.options is not empty %} 139 | 140 | 141 | 142 | 143 | {% endif %} 144 | 145 | 146 | 147 | {% if trace.http_code >= 500 %} 148 | {% set responseStatus = 'error' %} 149 | {% elseif trace.http_code >= 400 %} 150 | {% set responseStatus = 'warning' %} 151 | {% else %} 152 | {% set responseStatus = 'success' %} 153 | {% endif %} 154 | 155 | {{ trace.http_code }} 156 | 157 | 158 | {{ profiler_dump(trace.info, maxDepth=1) }} 159 | 160 | {% if profiler_token and profiler_link %} 161 | 164 | {% endif %} 165 | 166 | 167 |
120 | {{ trace.method }} 121 | 123 | {{ trace.url }} 124 | 127 | Profile 128 | 132 | 133 |
Request options{{ profiler_dump(trace.options, maxDepth=1) }}
Response 162 | {{ profiler_token }} 163 |
168 | {% endfor %} 169 | {% endif %} 170 |
171 |
172 | {% endfor %} 173 | {% endif %} 174 |
175 | {% endblock %} 176 | -------------------------------------------------------------------------------- /Resources/views/Collector/memory.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@WebProfiler/Profiler/layout.html.twig' %} 2 | 3 | {% block toolbar %} 4 | {% set icon %} 5 | {% set status_color = (collector.memory / 1024 / 1024) > 50 ? 'yellow' %} 6 | {{ source('@WebProfiler/Icon/memory.svg') }} 7 | {{ '%.1f'|format(collector.memory / 1024 / 1024) }} 8 | MiB 9 | {% endset %} 10 | 11 | {% set text %} 12 |
13 | Peak memory usage 14 | {{ '%.1f'|format(collector.memory / 1024 / 1024) }} MiB 15 |
16 | 17 |
18 | PHP memory limit 19 | {{ collector.memoryLimit == -1 ? 'Unlimited' : '%.0f MiB'|format(collector.memoryLimit / 1024 / 1024) }} 20 |
21 | {% endset %} 22 | 23 | {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, name: 'time', status: status_color }) }} 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /Resources/views/Collector/messenger.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@WebProfiler/Profiler/layout.html.twig' %} 2 | 3 | {% block head %} 4 | {{ parent() }} 5 | 6 | 40 | {% endblock %} 41 | 42 | {% block toolbar %} 43 | {% if collector.messages|length > 0 %} 44 | {% set status_color = collector.exceptionsCount ? 'red' %} 45 | {% set icon %} 46 | {{ source('@WebProfiler/Icon/messenger.svg') }} 47 | {{ collector.messages|length }} 48 | {% endset %} 49 | 50 | {% set text %} 51 | {% for bus in collector.buses %} 52 | {% set exceptionsCount = collector.exceptionsCount(bus) %} 53 |
54 | {{ bus }} 55 | 59 | {{ collector.messages(bus)|length }} 60 | 61 |
62 | {% endfor %} 63 | {% endset %} 64 | 65 | {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: 'messenger', status: status_color }) }} 66 | {% endif %} 67 | {% endblock %} 68 | 69 | {% block menu %} 70 | 71 | {{ source('@WebProfiler/Icon/messenger.svg') }} 72 | Messages 73 | {% if collector.exceptionsCount > 0 %} 74 | 75 | {{ collector.exceptionsCount }} 76 | 77 | {% endif %} 78 | 79 | {% endblock %} 80 | 81 | {% block panel %} 82 |

Messages

83 | 84 | {% if collector.messages is empty %} 85 |
86 |

No messages have been collected.

87 |
88 | {% elseif 1 == collector.buses|length %} 89 |

Ordered list of dispatched messages across all your buses

90 | {{ _self.render_bus_messages(collector.messages, true) }} 91 | {% else %} 92 |
93 |
94 | {% set messages = collector.messages %} 95 | {% set exceptionsCount = collector.exceptionsCount %} 96 |

All{{ messages|length }}

97 | 98 |
99 |

Ordered list of dispatched messages across all your buses

100 | {{ _self.render_bus_messages(messages, true) }} 101 |
102 |
103 | 104 | {% for bus in collector.buses %} 105 |
106 | {% set messages = collector.messages(bus) %} 107 | {% set exceptionsCount = collector.exceptionsCount(bus) %} 108 |

{{ bus }}{{ messages|length }}

109 | 110 |
111 |

Ordered list of messages dispatched on the {{ bus }} bus

112 | {{ _self.render_bus_messages(messages) }} 113 |
114 |
115 | {% endfor %} 116 |
117 | {% endif %} 118 | 119 | {% endblock %} 120 | 121 | {% macro render_bus_messages(messages, showBus = false) %} 122 | {% set discr = random() %} 123 | {% for dispatchCall in messages %} 124 | 125 | 126 | 127 | 140 | 141 | 142 | 143 | 144 | 145 | 171 | 172 | {% if showBus %} 173 | 174 | 175 | 176 | 177 | {% endif %} 178 | 179 | 180 | 181 | 182 | 183 | 184 | 191 | 192 | {% if dispatchCall.stamps_after_dispatch is defined %} 193 | 194 | 195 | 202 | 203 | {% endif %} 204 | {% if dispatchCall.exception is defined %} 205 | 206 | 207 | 210 | 211 | {% endif %} 212 | 213 |
131 | {{ profiler_dump(dispatchCall.message.type) }} 132 | {% if dispatchCall.exception is defined %} 133 | exception 134 | {% endif %} 135 | 139 |
Caller 146 | In 147 | {% set caller = dispatchCall.caller %} 148 | {% if caller.line %} 149 | {% set link = caller.file|file_link(caller.line) %} 150 | {% if link %} 151 | {{ caller.name }} 152 | {% else %} 153 | {{ caller.name }} 154 | {% endif %} 155 | {% else %} 156 | {{ caller.name }} 157 | {% endif %} 158 | line 159 | 160 | 170 |
Bus{{ dispatchCall.bus }}
Message{{ profiler_dump(dispatchCall.message.value, maxDepth=2) }}
Envelope stamps when dispatching 185 | {% for item in dispatchCall.stamps %} 186 | {{ profiler_dump(item) }} 187 | {% else %} 188 | No items 189 | {% endfor %} 190 |
Envelope stamps after dispatch 196 | {% for item in dispatchCall.stamps_after_dispatch %} 197 | {{ profiler_dump(item) }} 198 | {% else %} 199 | No items 200 | {% endfor %} 201 |
Exception 208 | {{ profiler_dump(dispatchCall.exception.value, maxDepth=1) }} 209 |
214 | {% endfor %} 215 | {% endmacro %} 216 | -------------------------------------------------------------------------------- /Resources/views/Collector/notifier.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@WebProfiler/Profiler/layout.html.twig' %} 2 | 3 | {% block toolbar %} 4 | {% set events = collector.events %} 5 | 6 | {% if events.messages|length %} 7 | {% set icon %} 8 | {{ source('@WebProfiler/Icon/notifier.svg') }} 9 | {{ events.messages|length }} 10 | {% endset %} 11 | 12 | {% set text %} 13 |
14 | Sent notifications 15 | {{ events.messages|length }} 16 |
17 | 18 | {% for transport in events.transports %} 19 |
20 | {{ transport ?: 'Empty Transport Name' }} 21 | {{ events.messages(transport)|length }} 22 |
23 | {% endfor %} 24 | {% endset %} 25 | 26 | {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { 'link': profiler_url }) }} 27 | {% endif %} 28 | {% endblock %} 29 | 30 | {% block head %} 31 | {{ parent() }} 32 | 65 | {% endblock %} 66 | 67 | {% block menu %} 68 | {% set events = collector.events %} 69 | 70 | 71 | {{ source('@WebProfiler/Icon/notifier.svg') }} 72 | 73 | Notifications 74 | {% if events.messages|length > 0 %} 75 | 76 | {{ events.messages|length }} 77 | 78 | {% endif %} 79 | 80 | {% endblock %} 81 | 82 | {% block panel %} 83 | {% set events = collector.events %} 84 | 85 |

Notifications

86 | 87 | {% if not events.messages|length %} 88 |
89 |

No notifications were sent.

90 |
91 | {% endif %} 92 | 93 |
94 | {% for transport in events.transports %} 95 |
96 | {{ events.messages(transport)|length }} 97 | {{ transport }} 98 |
99 | {% endfor %} 100 |
101 | 102 | {% for transport in events.transports %} 103 |

{{ transport ?: 'Empty Transport Name' }}

104 | 105 |
106 |
107 | {% for event in events.events(transport) %} 108 | {% set message = event.message %} 109 |
110 |

Message #{{ loop.index }} ({{ event.isQueued() ? 'queued' : 'sent' }})

111 |
112 |
113 |
114 | Subject 115 |

{{ message.getSubject() ?? '(empty)' }}

116 |
117 | {% set notification = message.notification ?? null %} 118 | {% if notification %} 119 |
120 |
121 |
122 | Content 123 |
{{ notification.getContent() ?? '(empty)' }}
124 | Importance 125 |
{{ notification.getImportance() }}
126 |
127 |
128 |
129 | {% endif %} 130 |
131 |
132 | {% if notification %} 133 |
134 |

Notification

135 |
136 |
137 |                                                             {{- 'Subject: ' ~ notification.getSubject() }}
138 | {{- 'Content: ' ~ notification.getContent() }}
139 | {{- 'Importance: ' ~ notification.getImportance() }}
140 | {{- 'Emoji: ' ~ (notification.getEmoji() is empty ? '(empty)' : notification.getEmoji()) }}
141 | {{- 'Exception: ' ~ (notification.getException() ?? '(empty)') }}
142 | {{- 'ExceptionAsString: ' ~ (notification.getExceptionAsString() is empty ? '(empty)' : notification.getExceptionAsString()) }} 143 |
144 |
145 |
146 | {% endif %} 147 |
148 |

Message Options

149 |
150 |
151 |                                                             {%- if message.getOptions() is null %}
152 |                                                                 {{- '(empty)' }}
153 |                                                             {%- else %}
154 |                                                                 {{- message.getOptions().toArray()|json_encode(constant('JSON_PRETTY_PRINT')) }}
155 |                                                             {%- endif %}
156 |                                                         
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 | {% endfor %} 165 |
166 |
167 | {% endfor %} 168 | {% endblock %} 169 | -------------------------------------------------------------------------------- /Resources/views/Collector/router.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@WebProfiler/Profiler/layout.html.twig' %} 2 | 3 | {% block toolbar %}{% endblock %} 4 | 5 | {% block menu %} 6 | 7 | {{ source('@WebProfiler/Icon/router.svg') }} 8 | Routing 9 | 10 | {% endblock %} 11 | 12 | {% block panel %} 13 | {{ render(controller('web_profiler.controller.router::panelAction', { token: token })) }} 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /Resources/views/Collector/time.css.twig: -------------------------------------------------------------------------------- 1 | /* Legend */ 2 | 3 | .sf-profiler-timeline .legends .timeline-category { 4 | border: none; 5 | background: none; 6 | border-left: 1em solid transparent; 7 | line-height: 1em; 8 | margin: 0 1em 0 0; 9 | padding: 0 0.5em; 10 | display: none; 11 | opacity: 0.5; 12 | } 13 | 14 | .sf-profiler-timeline .legends .timeline-category.active { 15 | opacity: 1; 16 | } 17 | 18 | .sf-profiler-timeline .legends .timeline-category.present { 19 | display: inline-block; 20 | } 21 | 22 | .timeline-graph { 23 | margin: 1em 0; 24 | width: 100%; 25 | background-color: var(--table-background); 26 | border: 1px solid var(--table-border-color); 27 | } 28 | 29 | /* Typography */ 30 | 31 | .timeline-graph .timeline-label { 32 | font-family: var(--font-sans-serif); 33 | font-size: 12px; 34 | line-height: 12px; 35 | font-weight: normal; 36 | fill: var(--color-text); 37 | } 38 | 39 | .timeline-graph .timeline-label .timeline-sublabel { 40 | margin-left: 1em; 41 | fill: var(--color-muted); 42 | } 43 | 44 | .timeline-graph .timeline-subrequest, 45 | .timeline-graph .timeline-border { 46 | fill: none; 47 | stroke: var(--table-border-color); 48 | stroke-width: 1px; 49 | } 50 | 51 | .timeline-graph .timeline-subrequest { 52 | fill: url(#subrequest); 53 | fill-opacity: 0.5; 54 | } 55 | 56 | .timeline-subrequest-pattern { 57 | fill: var(--gray-200); 58 | } 59 | .theme-dark .timeline-subrequest-pattern { 60 | fill: var(--gray-600); 61 | } 62 | 63 | /* Timeline periods */ 64 | 65 | .timeline-graph .timeline-period { 66 | stroke-width: 0; 67 | } 68 | -------------------------------------------------------------------------------- /Resources/views/Collector/time.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@WebProfiler/Profiler/layout.html.twig' %} 2 | 3 | {% block head %} 4 | {{ parent() }} 5 | 6 | 39 | {% endblock %} 40 | 41 | {% block toolbar %} 42 | {% set has_time_events = collector.events|length > 0 %} 43 | {% set total_time = has_time_events ? '%.0f'|format(collector.duration) : 'n/a' %} 44 | {% set initialization_time = collector.events|length ? '%.0f'|format(collector.inittime) : 'n/a' %} 45 | {% set status_color = has_time_events and collector.duration > 1000 ? 'yellow' %} 46 | 47 | {% set icon %} 48 | {{ source('@WebProfiler/Icon/time.svg') }} 49 | {{ total_time }} 50 | ms 51 | {% endset %} 52 | 53 | {% set text %} 54 |
55 | Total time 56 | {{ total_time }} ms 57 |
58 |
59 | Initialization time 60 | {{ initialization_time }} ms 61 |
62 | {% endset %} 63 | 64 | {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }} 65 | {% endblock %} 66 | 67 | {% block menu %} 68 | 69 | {{ source('@WebProfiler/Icon/time.svg') }} 70 | Performance 71 | 72 | {% endblock %} 73 | 74 | {% block panel %} 75 | {% set has_time_events = collector.events|length > 0 %} 76 |

Performance metrics

77 | 78 |
79 |
80 |
81 | {{ '%.0f'|format(collector.duration) }} ms 82 | Total execution time 83 |
84 | 85 |
86 | {{ '%.0f'|format(collector.inittime) }} ms 87 | Symfony initialization 88 |
89 |
90 | 91 | {% if profile.collectors.memory %} 92 |
93 | 94 |
95 | {{ '%.2f'|format(profile.collectors.memory.memory / 1024 / 1024) }} MiB 96 | Peak memory usage 97 |
98 | {% endif %} 99 | 100 | {% if profile.children|length > 0 %} 101 |
102 | 103 |
104 |
105 | {{ profile.children|length }} 106 | Sub-{{ profile_type|title }}{{ profile.children|length > 1 ? 's' }} 107 |
108 | 109 | {% set subrequests_time = has_time_events 110 | ? profile.children|reduce((total, child) => total + child.getcollector('time').events.__section__.duration, 0) 111 | : 'n/a' %} 112 | 113 |
114 | {{ subrequests_time }} ms 115 | Sub-{{ profile_type|title }}{{ profile.children|length > 1 ? 's' }} time 116 |
117 |
118 | {% endif %} 119 |
120 | 121 |

Execution timeline

122 | 123 | {% if not collector.isStopwatchInstalled() %} 124 |
125 |

The Stopwatch component is not installed. If you want to see timing events, run: composer require symfony/stopwatch.

126 |
127 | {% elseif collector.events is empty %} 128 |
129 |

No timing events have been recorded. Check that symfony/stopwatch is installed and debugging enabled in the kernel.

130 |
131 | {% else %} 132 | {{ block('panelContent') }} 133 | {% endif %} 134 | {% endblock %} 135 | 136 | {% block panelContent %} 137 |
138 | 139 | 140 | ms 141 | (timeline only displays events with a duration longer than this threshold) 142 |
143 | 144 | {% if profile.parent %} 145 |

146 | Sub-{{ profile_type|title }} {{ profiler_dump(profile.getcollector('request').requestattributes.get('_controller')) }} 147 | 148 | {{ collector.events.__section__.duration }} ms 149 | Return to parent {{ profile_type }} 150 | 151 |

152 | {% elseif profile.children|length > 0 %} 153 |

154 | Main {{ profile_type|title }} {{ collector.events.__section__.duration }} ms 155 |

156 | {% endif %} 157 | 158 | {{ _self.display_timeline(token, collector.events, collector.events.__section__.origin) }} 159 | 160 | {% if profile.children|length %} 161 |

Note: sections with a striped background correspond to sub-{{ profile_type }}s.

162 | 163 |

Sub-{{ profile_type }}s ({{ profile.children|length }})

164 | 165 | {% for child in profile.children %} 166 | {% set events = child.getcollector('time').events %} 167 |

168 | {{ child.getcollector('request').identifier }} 169 | {{ events.__section__.duration }} ms 170 |

171 | 172 | {{ _self.display_timeline(child.token, events, collector.events.__section__.origin) }} 173 | {% endfor %} 174 | {% endif %} 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 186 | 189 | {% endblock %} 190 | 191 | {% macro dump_request_data(token, events, origin) %} 192 | {% autoescape 'js' %} 193 | { 194 | id: "{{ token }}", 195 | left: {{ "%F"|format(events.__section__.origin - origin) }}, 196 | end: "{{ '%F'|format(events.__section__.endtime) }}", 197 | events: [ {{ _self.dump_events(events) }} ], 198 | } 199 | {% endautoescape %} 200 | {% endmacro %} 201 | 202 | {% macro dump_events(events) %} 203 | {% autoescape 'js' %} 204 | {% for name, event in events %} 205 | {% if '__section__' != name %} 206 | { 207 | name: "{{ name }}", 208 | category: "{{ event.category }}", 209 | origin: {{ "%F"|format(event.origin) }}, 210 | starttime: {{ "%F"|format(event.starttime) }}, 211 | endtime: {{ "%F"|format(event.endtime) }}, 212 | duration: {{ "%F"|format(event.duration) }}, 213 | memory: {{ "%.1F"|format(event.memory / 1024 / 1024) }}, 214 | elements: {}, 215 | periods: [ 216 | {%- for period in event.periods -%} 217 | { 218 | start: {{ "%F"|format(period.starttime) }}, 219 | end: {{ "%F"|format(period.endtime) }}, 220 | duration: {{ "%F"|format(period.duration) }}, 221 | elements: {} 222 | }, 223 | {%- endfor -%} 224 | ], 225 | }, 226 | {% endif %} 227 | {% endfor %} 228 | {% endautoescape %} 229 | {% endmacro %} 230 | 231 | {% macro display_timeline(token, events, origin) %} 232 |
233 |
234 | 235 | 248 |
249 | {% endmacro %} 250 | -------------------------------------------------------------------------------- /Resources/views/Collector/translation.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@WebProfiler/Profiler/layout.html.twig' %} 2 | 3 | {% block toolbar %} 4 | {% if collector.messages|length %} 5 | {% set icon %} 6 | {{ source('@WebProfiler/Icon/translation.svg') }} 7 | {% set status_color = collector.countMissings ? 'red' : collector.countFallbacks ? 'yellow' %} 8 | {% set error_count = collector.countMissings + collector.countFallbacks %} 9 | {{ error_count ?: collector.countDefines }} 10 | {% endset %} 11 | 12 | {% set text %} 13 |
14 | Default locale 15 | 16 | {{ collector.locale|default('-') }} 17 | 18 |
19 |
20 | Missing messages 21 | 22 | {{ collector.countMissings }} 23 | 24 |
25 | 26 |
27 | Fallback messages 28 | 29 | {{ collector.countFallbacks }} 30 | 31 |
32 | 33 |
34 | Defined messages 35 | {{ collector.countDefines }} 36 |
37 | {% endset %} 38 | 39 | {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }} 40 | {% endif %} 41 | {% endblock %} 42 | 43 | {% block menu %} 44 | 45 | {{ source('@WebProfiler/Icon/translation.svg') }} 46 | Translation 47 | {% if collector.countMissings or collector.countFallbacks %} 48 | {% set error_count = collector.countMissings + collector.countFallbacks %} 49 | 50 | {{ error_count }} 51 | 52 | {% endif %} 53 | 54 | {% endblock %} 55 | 56 | {% block panel %} 57 |

Translation

58 | 59 |
60 |
61 | {{ collector.locale|default('-') }} 62 | Default locale 63 |
64 |
65 | {{ collector.fallbackLocales|join(', ')|default('-') }} 66 | Fallback locale{{ collector.fallbackLocales|length != 1 ? 's' }} 67 |
68 |
69 | 70 |

Messages

71 | 72 | {% if collector.messages is empty %} 73 |
74 |

No translations have been called.

75 |
76 | {% else %} 77 | {% block messages %} 78 | 79 | {# sort translation messages in groups #} 80 | {% set messages_defined, messages_missing, messages_fallback = [], [], [] %} 81 | {% for message in collector.messages %} 82 | {% if message.state == constant('Symfony\\Component\\Translation\\DataCollectorTranslator::MESSAGE_DEFINED') %} 83 | {% set messages_defined = messages_defined|merge([message]) %} 84 | {% elseif message.state == constant('Symfony\\Component\\Translation\\DataCollectorTranslator::MESSAGE_MISSING') %} 85 | {% set messages_missing = messages_missing|merge([message]) %} 86 | {% elseif message.state == constant('Symfony\\Component\\Translation\\DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK') %} 87 | {% set messages_fallback = messages_fallback|merge([message]) %} 88 | {% endif %} 89 | {% endfor %} 90 | 91 |
92 |
93 |

Defined {{ collector.countDefines }}

94 | 95 |
96 |

97 | These messages are correctly translated into the given locale. 98 |

99 | 100 | {% if messages_defined is empty %} 101 |
102 |

None of the used translation messages are defined for the given locale.

103 |
104 | {% else %} 105 | {% block defined_messages %} 106 | {{ _self.render_table(messages_defined) }} 107 | {% endblock %} 108 | {% endif %} 109 |
110 |
111 | 112 |
113 |

Fallback {{ collector.countFallbacks }}

114 | 115 |
116 |

117 | These messages are not available for the given locale 118 | but Symfony found them in the fallback locale catalog. 119 |

120 | 121 | {% if messages_fallback is empty %} 122 |
123 |

No fallback translation messages were used.

124 |
125 | {% else %} 126 | {% block fallback_messages %} 127 | {{ _self.render_table(messages_fallback, true) }} 128 | {% endblock %} 129 | {% endif %} 130 |
131 |
132 | 133 |
134 |

Missing {{ collector.countMissings }}

135 | 136 |
137 |

138 | These messages are not available for the given locale and cannot 139 | be found in the fallback locales. Add them to the translation 140 | catalogue to avoid Symfony outputting untranslated contents. 141 |

142 | 143 | {% if messages_missing is empty %} 144 |
145 |

There are no messages of this category.

146 |
147 | {% else %} 148 | {% block missing_messages %} 149 | {{ _self.render_table(messages_missing) }} 150 | {% endblock %} 151 | {% endif %} 152 |
153 |
154 |
155 | 156 | {% endblock messages %} 157 | {% endif %} 158 | 159 | {% if collector.globalParameters|default([]) %} 160 |

Global parameters

161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | {% for id, value in collector.globalParameters %} 171 | 172 | 173 | 174 | 175 | {% endfor %} 176 | 177 |
Message IDValue
{{ id }}{{ profiler_dump(value) }}
178 | {% endif %} 179 | 180 | {% endblock %} 181 | 182 | {% macro render_table(messages, is_fallback) %} 183 | 184 | 185 | 186 | 187 | {% if is_fallback %} 188 | 189 | {% endif %} 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | {% for message in messages %} 198 | 199 | 200 | {% if is_fallback %} 201 | 202 | {% endif %} 203 | 204 | 205 | 222 | 223 | 224 | {% endfor %} 225 | 226 |
LocaleFallback localeDomainTimes usedMessage IDMessage Preview
{{ message.locale }}{{ message.fallbackLocale|default('-') }}{{ message.domain }}{{ message.count }} 206 | {{ message.id }} 207 | 208 | {% if message.transChoiceNumber is not null %} 209 | (pluralization is used) 210 | {% endif %} 211 | 212 | {% if message.parameters|length > 0 %} 213 | 214 | 215 | 220 | {% endif %} 221 | {{ message.translation }}
227 | {% endmacro %} 228 | -------------------------------------------------------------------------------- /Resources/views/Collector/twig.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@WebProfiler/Profiler/layout.html.twig' %} 2 | 3 | {% block head %} 4 | {{ parent() }} 5 | 6 | 38 | {% endblock %} 39 | 40 | {% block toolbar %} 41 | {% set time = collector.templatecount ? '%0.0f'|format(collector.time) : 'n/a' %} 42 | {% set icon %} 43 | {{ source('@WebProfiler/Icon/twig.svg') }} 44 | {{ time }} 45 | ms 46 | {% endset %} 47 | 48 | {% set text %} 49 | {% set template = collector.templates|keys|first %} 50 | {% set file = collector.templatePaths[template]|default(false) %} 51 | {% set link = file ? file|file_link(1) : false %} 52 |
53 | Entry View 54 | 55 | {% if link %} 56 | 57 | {{ template }} 58 | 59 | {% else %} 60 | {{ template }} 61 | {% endif %} 62 | 63 |
64 |
65 | Render Time 66 | {{ time }} ms 67 |
68 |
69 | Template Calls 70 | {{ collector.templatecount }} 71 |
72 |
73 | Block Calls 74 | {{ collector.blockcount }} 75 |
76 |
77 | Macro Calls 78 | {{ collector.macrocount }} 79 |
80 | {% endset %} 81 | 82 | {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }} 83 | {% endblock %} 84 | 85 | {% block menu %} 86 | 87 | {{ source('@WebProfiler/Icon/twig.svg') }} 88 | Twig 89 | 90 | {% endblock %} 91 | 92 | {% block panel %} 93 | {% if collector.templatecount == 0 %} 94 |

Twig

95 | 96 |
97 |

No Twig templates were rendered.

98 |
99 | {% else %} 100 |

Twig Metrics

101 | 102 |
103 |
104 | {{ '%0.0f'|format(collector.time) }} ms 105 | Render time 106 |
107 | 108 |
109 | 110 |
111 |
112 | {{ collector.templatecount }} 113 | Template calls 114 |
115 | 116 |
117 | {{ collector.blockcount }} 118 | Block calls 119 |
120 | 121 |
122 | {{ collector.macrocount }} 123 | Macro calls 124 |
125 |
126 |
127 | 128 |

129 | Render time includes sub-requests rendering time (if any). 130 |

131 | 132 |

Rendered Templates

133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | {% for template, count in collector.templates %} 143 | 144 | {% set file = collector.templatePaths[template]|default(false) %} 145 | {% set link = file ? file|file_link(1) : false %} 146 | 156 | 157 | 158 | {% endfor %} 159 | 160 |
Template Name & PathRender Count
147 | {% if link %} 148 | 149 | {{ template }} 150 | {{ file|file_relative|default(file) }} 151 | 152 | {% else %} 153 | {{ template }} 154 | {% endif %} 155 | {{ count }}
161 | 162 |

Rendering Call Graph

163 | 164 |
165 | {{ collector.htmlcallgraph }} 166 |
167 | {% endif %} 168 | {% endblock %} 169 | -------------------------------------------------------------------------------- /Resources/views/Collector/validator.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@WebProfiler/Profiler/layout.html.twig' %} 2 | 3 | {% block head %} 4 | {{ parent() }} 5 | 6 | 30 | {% endblock %} 31 | 32 | {% block toolbar %} 33 | {% if collector.violationsCount > 0 or collector.calls|length %} 34 | {% set status_color = collector.violationsCount ? 'red' %} 35 | {% set icon %} 36 | {{ source('@WebProfiler/Icon/validator.svg') }} 37 | 38 | {{ collector.violationsCount ?: collector.calls|length }} 39 | 40 | {% endset %} 41 | 42 | {% set text %} 43 |
44 | Validator calls 45 | {{ collector.calls|length }} 46 |
47 |
48 | Number of violations 49 | {{ collector.violationsCount }} 50 |
51 | {% endset %} 52 | 53 | {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }} 54 | {% endif %} 55 | {% endblock %} 56 | 57 | {% block menu %} 58 | 59 | {{ source('@WebProfiler/Icon/validator.svg') }} 60 | Validator 61 | {% if collector.violationsCount > 0 %} 62 | 63 | {{ collector.violationsCount }} 64 | 65 | {% endif %} 66 | 67 | {% endblock %} 68 | 69 | {% block panel %} 70 |

Validator calls

71 | 72 | {% for call in collector.calls %} 73 |
74 | 88 | 89 | 99 | 100 | 103 | 104 | {% if call.violations|length %} 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | {% for violation in call.violations %} 115 | 116 | 117 | 118 | 119 | 120 | 121 | {% endfor %} 122 |
PathMessageInvalid valueViolation
{{ violation.propertyPath }}{{ violation.message }}{{ profiler_dump(violation.seek('invalidValue')) }}{{ profiler_dump(violation) }}
123 | {% else %} 124 | No violations 125 | {% endif %} 126 |
127 | {% else %} 128 |
129 |

No calls to the validator were collected.

130 |
131 | {% endfor %} 132 | {% endblock %} 133 | -------------------------------------------------------------------------------- /Resources/views/Icon/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Icons are from "Tabler Icons" (https://github.com/tabler/tabler-icons), a set of 2 | free MIT-licensed high-quality SVG icons. 3 | 4 | ----- 5 | 6 | MIT License 7 | 8 | Copyright (c) 2020-2022 Paweł Kuna 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | -------------------------------------------------------------------------------- /Resources/views/Icon/ajax.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Resources/views/Icon/alert-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Resources/views/Icon/attachment.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Resources/views/Icon/cache.svg: -------------------------------------------------------------------------------- 1 | 2 | Cache 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Resources/views/Icon/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Resources/views/Icon/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Resources/views/Icon/command.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Resources/views/Icon/config.svg: -------------------------------------------------------------------------------- 1 | 2 | Config 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Resources/views/Icon/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Resources/views/Icon/event.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Resources/views/Icon/exception.svg: -------------------------------------------------------------------------------- 1 | 2 | Exception 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Resources/views/Icon/file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Resources/views/Icon/filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Resources/views/Icon/form.svg: -------------------------------------------------------------------------------- 1 | 2 | Cache 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Resources/views/Icon/forward.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Resources/views/Icon/http-client.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Resources/views/Icon/logger.svg: -------------------------------------------------------------------------------- 1 | 2 | Logger 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Resources/views/Icon/mailer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Resources/views/Icon/memory.svg: -------------------------------------------------------------------------------- 1 | 2 | Memory 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Resources/views/Icon/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | Menu 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Resources/views/Icon/messenger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Resources/views/Icon/no.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Resources/views/Icon/notifier.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Resources/views/Icon/redirect.svg: -------------------------------------------------------------------------------- 1 | 2 | Redirect 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Resources/views/Icon/referrer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Resources/views/Icon/request.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Resources/views/Icon/router.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Resources/views/Icon/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Resources/views/Icon/serializer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Resources/views/Icon/settings-theme-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Resources/views/Icon/settings-theme-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Resources/views/Icon/settings-theme-system.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Resources/views/Icon/settings-width-fitted.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Resources/views/Icon/settings-width-fixed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Resources/views/Icon/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Resources/views/Icon/symfony.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Resources/views/Icon/time.svg: -------------------------------------------------------------------------------- 1 | 2 | Time 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Resources/views/Icon/translation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Resources/views/Icon/twig.svg: -------------------------------------------------------------------------------- 1 | 2 | Twig 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Resources/views/Icon/validator.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Resources/views/Icon/workflow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Resources/views/Icon/yes.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Resources/views/Profiler/_command_summary.html.twig: -------------------------------------------------------------------------------- 1 | {% set status_code = profile.statuscode|default(0) %} 2 | {% set interrupted = command_collector is same as false ? null : command_collector.interruptedBySignal %} 3 | {% set css_class = status_code == 113 or interrupted is not null ? 'status-warning' : status_code > 0 ? 'status-error' : 'status-success' %} 4 | 5 |
6 |
7 |

8 | 9 | {{ profile.method|upper }} 10 | 11 | 12 | 13 | {{ profile.url }} 14 | 15 |

16 | 17 | 49 |
50 |
51 | -------------------------------------------------------------------------------- /Resources/views/Profiler/_request_summary.html.twig: -------------------------------------------------------------------------------- 1 | {% set status_code = request_collector ? request_collector.statuscode|default(0) : 0 %} 2 | {% set css_class = status_code > 399 ? 'status-error' : status_code > 299 ? 'status-warning' : 'status-success' %} 3 | 4 | {% if request_collector and request_collector.redirect %} 5 | {% set redirect = request_collector.redirect %} 6 | {% set link_to_source_code = redirect.controller.class is defined ? redirect.controller.file|file_link(redirect.controller.line) %} 7 | {% set redirect_route_name = '@' ~ redirect.route %} 8 | 9 |
10 | {{ source('@WebProfiler/Icon/redirect.svg') }} 11 | 12 | {{ redirect.status_code }} redirect from 13 | 14 | {{ redirect.method }} 15 | 16 | {% if link_to_source_code %} 17 | {{ redirect_route_name }} 18 | {% else %} 19 | {{ redirect_route_name }} 20 | {% endif %} 21 | 22 | ({{ redirect.token }}) 23 |
24 | {% endif %} 25 | 26 |
27 | {% if status_code > 399 %} 28 |

29 | {{ source('@WebProfiler/Icon/alert-circle.svg') }} 30 | Error {{ status_code }} 31 | {{ request_collector.statusText }} 32 |

33 | {% endif %} 34 | 35 |

36 | 37 | {{ profile.method|upper }} 38 | 39 | 40 | {% set profile_title = profile.url|length < 160 ? profile.url : profile.url[:160] ~ '…' %} 41 | {% if profile.method|upper in ['GET', 'HEAD'] %} 42 | {{ profile_title }} 43 | {% else %} 44 | {{ profile_title }} 45 | {% endif %} 46 |

47 | 48 | 77 |
78 | 79 | {% if request_collector and request_collector.forwardtoken -%} 80 | {% set forward_profile = profile.childByToken(request_collector.forwardtoken) %} 81 | {% set controller = forward_profile ? forward_profile.collector('request').controller : 'n/a' %} 82 |
83 | {{ source('@WebProfiler/Icon/forward.svg') }} 84 | 85 | Forwarded to 86 | 87 | {% set link = controller.file is defined ? controller.file|file_link(controller.line) : null -%} 88 | {%- if link %}{% endif -%} 89 | {% if controller.class is defined %} 90 | {{- controller.class|abbr_class|striptags -}} 91 | {{- controller.method ? ' :: ' ~ controller.method -}} 92 | {% else %} 93 | {{- controller -}} 94 | {% endif %} 95 | {%- if link %}{% endif %} 96 | ({{ request_collector.forwardtoken }}) 97 | 98 |
99 | {%- endif %} 100 | -------------------------------------------------------------------------------- /Resources/views/Profiler/ajax_layout.html.twig: -------------------------------------------------------------------------------- 1 | {% block panel '' %} 2 | -------------------------------------------------------------------------------- /Resources/views/Profiler/bag.html.twig: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% for key in bag.keys|sort %} 11 | 12 | 13 | 14 | 15 | {% else %} 16 | 17 | 18 | 19 | {% endfor %} 20 | 21 |
{{ labels is defined ? labels[0] : 'Key' }}{{ labels is defined ? labels[1] : 'Value' }}
{{ key }}{{ profiler_dump(bag.get(key), maxDepth=maxDepth|default(0)) }}
(no data)
22 |
23 | -------------------------------------------------------------------------------- /Resources/views/Profiler/base.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% block title %}Symfony Profiler{% endblock %} 9 | 10 | {% set request_collector = profile is defined ? profile.collectors.request|default(null) : null %} 11 | {% set status_code = request_collector is not null ? request_collector.statuscode|default(0) : 0 %} 12 | {% set favicon_color = status_code > 399 ? 'b41939' : status_code > 299 ? 'af8503' : '000000' %} 13 | 14 | 15 | {% block head %} 16 | {% block stylesheets %} 17 | 18 | {{ include('@WebProfiler/Profiler/profiler.css.twig') }} 19 | 20 | {% endblock %} 21 | 22 | {% block javascripts %} 23 | {% endblock %} 24 | {% endblock %} 25 | 26 | 27 | 28 | if (null === localStorage.getItem('symfony/profiler/theme') || 'theme-auto' === localStorage.getItem('symfony/profiler/theme')) { 29 | document.body.classList.add((matchMedia('(prefers-color-scheme: dark)').matches ? 'theme-dark' : 'theme-light')); 30 | // needed to respond dynamically to OS changes without having to refresh the page 31 | window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { 32 | document.body.classList.remove('theme-light', 'theme-dark'); 33 | document.body.classList.add(e.matches ? 'theme-dark' : 'theme-light'); 34 | }); 35 | } else { 36 | document.body.classList.add(localStorage.getItem('symfony/profiler/theme')); 37 | } 38 | 39 | document.body.classList.add(localStorage.getItem('symfony/profiler/width') || 'width-normal'); 40 | 41 | document.body.classList.add( 42 | (navigator.appVersion.indexOf('Win') !== -1) ? 'windows' : (navigator.appVersion.indexOf('Mac') !== -1) ? 'macos' : 'linux' 43 | ); 44 | 45 | 46 | {% block body '' %} 47 | 48 | 49 | -------------------------------------------------------------------------------- /Resources/views/Profiler/cancel.html.twig: -------------------------------------------------------------------------------- 1 | {% block toolbar %} 2 | {% set icon %} 3 | {{ source('@WebProfiler/Icon/symfony.svg') }} 4 | 5 | 6 | Loading… 7 | 8 | {% endset %} 9 | 10 | {% set text %} 11 |
12 | Loading the web debug toolbar… 13 |
14 |
15 | Attempt # 16 |
17 |
18 | 19 | 20 | 21 |
22 | {% endset %} 23 | 24 | {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }} 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /Resources/views/Profiler/header.html.twig: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /Resources/views/Profiler/info.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@WebProfiler/Profiler/layout.html.twig' %} 2 | 3 | {% set messages = { 4 | 'no_token' : { 5 | status: 'error', 6 | title: (token|default('') == 'latest') ? 'There are no profiles' : 'Token not found', 7 | message: (token|default('') == 'latest') ? 'No profiles found.' : 'Token "' ~ token|default('') ~ '" not found.' 8 | } 9 | } %} 10 | 11 | {% block summary %} 12 |
13 |
14 |

{{ messages[about].status|title }}

15 |
16 |
17 | {% endblock %} 18 | 19 | {% block panel %} 20 |

{{ messages[about].title }}

21 |

{{ messages[about].message }}

22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /Resources/views/Profiler/layout.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@WebProfiler/Profiler/base.html.twig' %} 2 | 3 | {% block body %} 4 |
5 | {{ include('@WebProfiler/Profiler/header.html.twig', {profile_type: profile_type}, with_context = false) }} 6 | 7 |
8 | {% block summary %} 9 | {% if profile is defined %} 10 | {% set request_collector = profile.collectors.request|default(false) %} 11 | 12 | {{ include('@WebProfiler/Profiler/_%s_summary.html.twig'|format(profile_type), { 13 | profile: profile, 14 | command_collector: profile.collectors.command|default(false) , 15 | request_collector: request_collector, 16 | request: request, 17 | token: token 18 | }, with_context=false) }} 19 | {% endif %} 20 | {% endblock %} 21 |
22 | 23 |
24 |
25 | 66 | 67 |
68 |
69 | {{ include('@WebProfiler/Profiler/base_js.html.twig') }} 70 | {% block panel '' %} 71 |
72 |
73 |
74 |
75 |
76 | {% endblock %} 77 | -------------------------------------------------------------------------------- /Resources/views/Profiler/open.css.twig: -------------------------------------------------------------------------------- 1 | #header { 2 | margin-bottom: 30px; 3 | } 4 | 5 | #source { 6 | background: var(--page-background); 7 | border: 1px solid var(--menu-border-color); 8 | box-shadow: 0 0 0 5px var(--page-background); 9 | border-radius: 6px; 10 | margin: 0 30px 45px 0; 11 | max-width: 960px; 12 | padding: 15px 20px; 13 | } 14 | .width-full #source { 15 | max-width: unset; 16 | width: 100%; 17 | } 18 | 19 | #source code { 20 | font-size: 15px; 21 | } 22 | 23 | #source .source-file-name { 24 | border-bottom: 1px solid var(--table-border-color); 25 | font-size: 18px; 26 | font-weight: 500; 27 | margin: 0 0 15px 0; 28 | padding: 0 0 15px; 29 | } 30 | #source .source-file-name small { 31 | color: var(--color-muted); 32 | } 33 | 34 | #source .source-content { 35 | overflow-x: auto; 36 | } 37 | #source .source-content ol { 38 | margin: 0; 39 | } 40 | #source .source-content ol li { 41 | margin: 0 0 2px 0; 42 | padding-left: 5px; 43 | white-space: preserve nowrap; 44 | } 45 | #source .source-content ol li::marker { 46 | color: var(--color-muted); 47 | font-family: var(--font-family-monospace); 48 | padding-right: 5px; 49 | } 50 | #source .source-content li.selected { 51 | background: var(--yellow-100); 52 | border-radius: 4px; 53 | } 54 | .theme-dark #source .source-content li.selected { 55 | background: var(--gray-600); 56 | } 57 | #source .source-content li.selected::marker { 58 | color: var(--color-text); 59 | font-weight: bold; 60 | } 61 | 62 | #source span[style="color: #FF8000"] { color: var(--highlight-comment) !important; } 63 | #source span[style="color: #007700"] { color: var(--highlight-keyword) !important; } 64 | #source span[style="color: #0000BB"] { color: var(--color-text) !important; } 65 | #source span[style="color: #DD0000"] { color: var(--highlight-string) !important; } 66 | 67 | .file-metadata dt { 68 | color: var(--header-metadata-key); 69 | display: block; 70 | font-weight: bold; 71 | } 72 | .file-metadata dd { 73 | color: var(--header-metadata-value); 74 | margin: 5px 0 20px; 75 | 76 | /* needed to break the long file paths */ 77 | overflow-wrap: break-word; 78 | word-wrap: break-word; 79 | word-break: break-all; 80 | } 81 | -------------------------------------------------------------------------------- /Resources/views/Profiler/open.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@WebProfiler/Profiler/base.html.twig' %} 2 | 3 | {% block head %} 4 | 8 | {% endblock %} 9 | 10 | {% block body %} 11 |
12 | {{ include('@WebProfiler/Profiler/header.html.twig', with_context = false) }} 13 | 14 | {% set source = file_info.pathname|file_excerpt(line, -1) %} 15 |
16 |
17 |
18 |

{{ file }}{% if 0 < line %} line {{ line }}{% endif %}

19 | 20 |
21 | {% if source is null %} 22 |

The file is not readable.

23 | {% else %} 24 | {{ source|raw }} 25 | {% endif %} 26 |
27 |
28 | 29 | 48 |
49 |
50 |
51 | 52 | 64 | {% endblock %} 65 | -------------------------------------------------------------------------------- /Resources/views/Profiler/results.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@WebProfiler/Profiler/layout.html.twig' %} 2 | 3 | {% macro profile_search_filter(request, result, property) %} 4 | {%- if request.hasSession -%} 5 | {{ source('@WebProfiler/Icon/search.svg') }} 6 | {%- endif -%} 7 | {% endmacro %} 8 | 9 | {% block head %} 10 | {{ parent() }} 11 | 12 | 29 | {% endblock %} 30 | 31 | {% block summary %} 32 |
33 |

Profile Search

34 |
35 | {% endblock %} 36 | 37 | {% block sidebar_search_css_class %}{% endblock %} 38 | {% block sidebar_shortcuts_links %} 39 | {{ parent() }} 40 | {{ render(controller('web_profiler.controller.profiler::searchBarAction', query={type: profile_type }|merge(request.query.all))) }} 41 | {% endblock %} 42 | 43 | {% block panel %} 44 |
45 | 57 |
58 | 59 |

{{ tokens ? tokens|length : 'No' }} results found

60 | 61 | {% if tokens %} 62 | 63 | 64 | 65 | 72 | 79 | 86 | 93 | 94 | 95 | 96 | 97 | 98 | {% for result in tokens %} 99 | {% if 'command' == profile_type %} 100 | {% set css_class = result.status_code == 113 ? 'status-warning' : result.status_code > 0 ? 'status-error' : 'status-success' %} 101 | {% else %} 102 | {% set css_class = result.status_code|default(0) > 399 ? 'status-error' : result.status_code|default(0) > 299 ? 'status-warning' : 'status-success' %} 103 | {% endif %} 104 | 105 | 106 | 109 | 112 | 115 | 119 | 127 | 128 | 129 | {% endfor %} 130 | 131 |
66 | {% if 'command' == profile_type %} 67 | Exit code 68 | {% else %} 69 | Status 70 | {% endif %} 71 | 73 | {% if 'command' == profile_type %} 74 | Application 75 | {% else %} 76 | IP 77 | {% endif %} 78 | 80 | {% if 'command' == profile_type %} 81 | Mode 82 | {% else %} 83 | Method 84 | {% endif %} 85 | 87 | {% if 'command' == profile_type %} 88 | Command 89 | {% else %} 90 | URL 91 | {% endif %} 92 | TimeToken
107 | {{ result.status_code|default('n/a') }} 108 | 110 | {{ result.ip }} {{ _self.profile_search_filter(request, result, 'ip') }} 111 | 113 | {{ result.method }} {{ _self.profile_search_filter(request, result, 'method') }} 114 | 116 | {{ result.url }} 117 | {{ _self.profile_search_filter(request, result, 'url') }} 118 | 120 | 123 | 126 | {{ result.token }}
132 | {% else %} 133 |
134 |

The query returned no result.

135 |
136 | {% endif %} 137 | 138 | {% endblock %} 139 | -------------------------------------------------------------------------------- /Resources/views/Profiler/search.html.twig: -------------------------------------------------------------------------------- 1 | 94 | -------------------------------------------------------------------------------- /Resources/views/Profiler/table.html.twig: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% for key in data|keys|sort %} 11 | 12 | 13 | 14 | 15 | {% endfor %} 16 | 17 |
{{ labels is defined ? labels[0] : 'Key' }}{{ labels is defined ? labels[1] : 'Value' }}
{{ key }}{{ profiler_dump(data[key]) }}
18 |
19 | -------------------------------------------------------------------------------- /Resources/views/Profiler/toolbar.html.twig: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% for name, template in templates %} 4 | {% if block('toolbar', template) is defined %} 5 | {% with { 6 | collector: profile ? profile.getcollector(name) : null, 7 | profiler_url: profiler_url, 8 | token: token ?? (profile ? profile.token : null), 9 | name: name, 10 | profiler_markup_version: profiler_markup_version, 11 | csp_script_nonce: csp_script_nonce, 12 | csp_style_nonce: csp_style_nonce 13 | } %} 14 | {{ block('toolbar', template) }} 15 | {% endwith %} 16 | {% endif %} 17 | {% endfor %} 18 | {% if full_stack %} 19 |
20 |
21 | Using symfony/symfony is NOT supported 22 |
23 |
24 |

This project is using Symfony via the "symfony/symfony" package.

25 |

This is NOT supported anymore since Symfony 4.0.

26 |

Even if it seems to work well, it has some important limitations with no workarounds.

27 |

Using this package also makes your project slower.

28 | 29 | Please, stop using this package and replace it with individual packages instead. 30 |
31 |
32 |
33 | {% endif %} 34 | 35 | 39 |
40 | -------------------------------------------------------------------------------- /Resources/views/Profiler/toolbar_item.html.twig: -------------------------------------------------------------------------------- 1 |
2 | {% if link is not defined or link %}{% endif %} 3 |
{{ icon|default('') }}
4 | {% if link|default(false) %}
{% endif %} 5 |
{{ text|default('') }}
6 |
7 | -------------------------------------------------------------------------------- /Resources/views/Profiler/toolbar_redirect.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@WebProfiler/Profiler/base.html.twig' %} 2 | 3 | {% block title 'Redirection Intercepted' %} 4 | 5 | 6 | {% block head %} 7 | {{ parent() }} 8 | 9 | 32 | {% endblock %} 33 | 34 | {% block body %} 35 |
36 | {{ include('@WebProfiler/Profiler/header.html.twig', with_context = false) }} 37 | 38 |
39 |
40 |

Redirection Intercepted

41 | 42 | {% set absolute_url = absolute_url(location) %} 43 |

This request redirects to {{ absolute_url }}

44 | 45 |

Follow redirect

46 | 47 |

48 | The redirect was intercepted by the Symfony Web Debug toolbar to help debugging. 49 | For more information, see the "intercept-redirects" option of the Profiler. 50 |

51 |
52 |
53 |
54 | {% endblock %} 55 | -------------------------------------------------------------------------------- /Resources/views/Router/panel.html.twig: -------------------------------------------------------------------------------- 1 |

Routing

2 | 3 |
4 |
5 | {{ request.route ?: '(none)' }} 6 | Matched route 7 |
8 |
9 | 10 | {% if request.route %} 11 |

Route Parameters

12 | 13 | {% if request.routeParams is empty %} 14 |
15 |

No parameters.

16 |
17 | {% else %} 18 | {{ include('@WebProfiler/Profiler/table.html.twig', { data: request.routeParams, labels: ['Name', 'Value'] }, with_context = false) }} 19 | {% endif %} 20 | {% endif %} 21 | 22 | {% if router.redirect %} 23 |

Route Redirection

24 | 25 |

This page redirects to:

26 |
27 | {{ router.targetUrl }} 28 | {% if router.targetRoute %}(route: "{{ router.targetRoute }}"){% endif %} 29 |
30 | {% endif %} 31 | 32 |

Route Matching Logs

33 | 34 |
35 | Path to match: {{ request.pathinfo }} 36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | {% for trace in traces %} 49 | 50 | 51 | 52 | 53 | 63 | 64 | {% endfor %} 65 | 66 |
#Route namePathLog
{{ loop.index }}{{ trace.name }}{{ trace.path }} 54 | {% if trace.level == 1 %} 55 | Path almost matches, but 56 | {{ trace.log }} 57 | {% elseif trace.level == 2 %} 58 | {{ trace.log }} 59 | {% else %} 60 | Path does not match 61 | {% endif %} 62 |
67 | 68 |

69 | Note: These matching logs are based on the current router configuration, 70 | which might differ from the configuration used when profiling this request. 71 |

72 | -------------------------------------------------------------------------------- /Twig/WebProfilerExtension.php: -------------------------------------------------------------------------------- 1 | 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\Bundle\WebProfilerBundle\Twig; 13 | 14 | use Symfony\Component\VarDumper\Cloner\Data; 15 | use Symfony\Component\VarDumper\Dumper\HtmlDumper; 16 | use Twig\Environment; 17 | use Twig\Extension\ProfilerExtension; 18 | use Twig\Profiler\Profile; 19 | use Twig\Runtime\EscaperRuntime; 20 | use Twig\TwigFunction; 21 | 22 | /** 23 | * Twig extension for the profiler. 24 | * 25 | * @author Fabien Potencier 26 | * 27 | * @internal 28 | */ 29 | class WebProfilerExtension extends ProfilerExtension 30 | { 31 | private HtmlDumper $dumper; 32 | 33 | /** 34 | * @var resource 35 | */ 36 | private $output; 37 | 38 | private int $stackLevel = 0; 39 | 40 | public function __construct(?HtmlDumper $dumper = null) 41 | { 42 | $this->dumper = $dumper ?? new HtmlDumper(); 43 | $this->dumper->setOutput($this->output = fopen('php://memory', 'r+')); 44 | } 45 | 46 | public function enter(Profile $profile): void 47 | { 48 | ++$this->stackLevel; 49 | } 50 | 51 | public function leave(Profile $profile): void 52 | { 53 | if (0 === --$this->stackLevel) { 54 | $this->dumper->setOutput($this->output = fopen('php://memory', 'r+')); 55 | } 56 | } 57 | 58 | public function getFunctions(): array 59 | { 60 | return [ 61 | new TwigFunction('profiler_dump', $this->dumpData(...), ['is_safe' => ['html'], 'needs_environment' => true]), 62 | new TwigFunction('profiler_dump_log', $this->dumpLog(...), ['is_safe' => ['html'], 'needs_environment' => true]), 63 | ]; 64 | } 65 | 66 | public function dumpData(Environment $env, Data $data, int $maxDepth = 0): string 67 | { 68 | $this->dumper->setCharset($env->getCharset()); 69 | $this->dumper->dump($data, null, [ 70 | 'maxDepth' => $maxDepth, 71 | ]); 72 | 73 | $dump = stream_get_contents($this->output, -1, 0); 74 | rewind($this->output); 75 | ftruncate($this->output, 0); 76 | 77 | return str_replace("\n$1"', $message); 84 | 85 | $replacements = []; 86 | foreach ($context ?? [] as $k => $v) { 87 | $k = '{'.self::escape($env, $k).'}'; 88 | if (str_contains($message, $k)) { 89 | $replacements[$k] = $v; 90 | } 91 | } 92 | 93 | if (!$replacements) { 94 | return ''.$message.''; 95 | } 96 | 97 | foreach ($replacements as $k => $v) { 98 | $replacements['"'.$k.'"'] = $replacements['"'.$k.'"'] = $replacements[$k] = $this->dumpData($env, $v); 99 | } 100 | 101 | return ''.strtr($message, $replacements).''; 102 | } 103 | 104 | public function getName(): string 105 | { 106 | return 'profiler'; 107 | } 108 | 109 | private static function escape(Environment $env, string $s): string 110 | { 111 | return $env->getRuntime(EscaperRuntime::class)->escape($s); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /WebProfilerBundle.php: -------------------------------------------------------------------------------- 1 | 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\Bundle\WebProfilerBundle; 13 | 14 | use Symfony\Component\HttpKernel\Bundle\Bundle; 15 | 16 | /** 17 | * @author Fabien Potencier 18 | */ 19 | class WebProfilerBundle extends Bundle 20 | { 21 | public function boot(): void 22 | { 23 | if ('prod' === $this->container->getParameter('kernel.environment')) { 24 | @trigger_error('Using WebProfilerBundle in production is not supported and puts your project at risk, disable it.', \E_USER_WARNING); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/web-profiler-bundle", 3 | "type": "symfony-bundle", 4 | "description": "Provides a development tool that gives detailed information about the execution of any request", 5 | "keywords": ["dev"], 6 | "homepage": "https://symfony.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Fabien Potencier", 11 | "email": "fabien@symfony.com" 12 | }, 13 | { 14 | "name": "Symfony Community", 15 | "homepage": "https://symfony.com/contributors" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=8.2", 20 | "composer-runtime-api": ">=2.1", 21 | "symfony/config": "^7.3", 22 | "symfony/deprecation-contracts": "^2.5|^3", 23 | "symfony/framework-bundle": "^6.4|^7.0", 24 | "symfony/http-kernel": "^6.4|^7.0", 25 | "symfony/routing": "^6.4|^7.0", 26 | "symfony/twig-bundle": "^6.4|^7.0", 27 | "twig/twig": "^3.12" 28 | }, 29 | "require-dev": { 30 | "symfony/browser-kit": "^6.4|^7.0", 31 | "symfony/console": "^6.4|^7.0", 32 | "symfony/css-selector": "^6.4|^7.0", 33 | "symfony/stopwatch": "^6.4|^7.0" 34 | }, 35 | "conflict": { 36 | "symfony/form": "<6.4", 37 | "symfony/mailer": "<6.4", 38 | "symfony/messenger": "<6.4", 39 | "symfony/serializer": "<7.2", 40 | "symfony/workflow": "<7.3" 41 | }, 42 | "autoload": { 43 | "psr-4": { "Symfony\\Bundle\\WebProfilerBundle\\": "" }, 44 | "exclude-from-classmap": [ 45 | "/Tests/" 46 | ] 47 | }, 48 | "minimum-stability": "dev" 49 | } 50 | --------------------------------------------------------------------------------