initial commit

This commit is contained in:
boris
2025-09-30 09:24:25 +01:00
committed by boris
parent a783a12c97
commit c7770ea03b
4695 changed files with 525784 additions and 0 deletions

View File

@@ -0,0 +1,155 @@
CHANGELOG
=========
7.3
---
* Add `profiler.php` and `wdt.php` routing configuration files (use them instead of their XML equivalent)
Before:
```yaml
when@dev:
web_profiler_wdt:
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
prefix: /_wdt
web_profiler_profiler:
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
prefix: /_profiler
```
After:
```yaml
when@dev:
web_profiler_wdt:
resource: '@WebProfilerBundle/Resources/config/routing/wdt.php'
prefix: /_wdt
web_profiler_profiler:
resource: '@WebProfilerBundle/Resources/config/routing/profiler.php
prefix: /_profiler
```
* Add `ajax_replace` option for replacing toolbar on AJAX requests
7.2
---
* Add support for displaying profiles of multiple serializer instances
7.1
---
* Set `XDEBUG_IGNORE` query parameter when sending toolbar XHR
6.4
---
* Add console commands to the profiler
6.3
---
* Add a "role=img" and an explicit title in the .svg file used by the web debug toolbar
to improve accessibility with screen readers for blind users
* Add a clickable link to the entry view twig file in the toolbar
6.1
---
* Add a download link in mailer profiler for email attachments
5.4
---
* Add a "preview" tab in mailer profiler for HTML email
5.2.0
-----
* added session usage
5.0.0
-----
* removed the `ExceptionController`, use `ExceptionPanelController` instead
* removed the `TemplateManager::templateExists()` method
4.4.0
-----
* added support for the Mailer component
* added support for the HttpClient component
* added button to clear the ajax request tab
* deprecated the `ExceptionController::templateExists()` method
* deprecated the `TemplateManager::templateExists()` method
* deprecated the `ExceptionController` in favor of `ExceptionPanelController`
* marked all classes of the WebProfilerBundle as internal
* added a section with the stamps of a message after it is dispatched in the Messenger panel
4.3.0
-----
* Replaced the canvas performance graph renderer with an SVG renderer
4.1.0
-----
* added information about orphaned events
* made the toolbar auto-update with info from ajax responses when they set the
`Symfony-Debug-Toolbar-Replace header` to `1`
4.0.0
-----
* removed the `WebProfilerExtension::dumpValue()` method
* removed the `getTemplates()` method of the `TemplateManager` class in favor of the ``getNames()`` method
* removed the `web_profiler.position` config option and the
`web_profiler.debug_toolbar.position` container parameter
3.4.0
-----
* Deprecated the `web_profiler.position` config option (in 4.0 version the toolbar
will always be displayed at the bottom) and the `web_profiler.debug_toolbar.position`
container parameter.
3.1.0
-----
* added information about redirected and forwarded requests to the profiler
3.0.0
-----
* removed profiler:import and profiler:export commands
2.8.0
-----
* deprecated profiler:import and profiler:export commands
2.7.0
-----
* [BC BREAK] if you are using a DB to store profiles, the table must be dropped
* added the HTTP status code to profiles
2.3.0
-----
* draw retina canvas if devicePixelRatio is bigger than 1
2.1.0
-----
* deprecated the verbose setting (not relevant anymore)
* [BC BREAK] You must clear old profiles after upgrading to 2.1 (don't forget
to remove the table if you are using a DB)
* added support for the request method
* added a routing panel
* added a timeline panel
* The toolbar position can now be configured via the `position` option (can
be `top` or `bottom`)

View File

@@ -0,0 +1,58 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\WebProfilerBundle\Controller;
use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Profiler\Profiler;
/**
* Renders the exception panel.
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*
* @internal
*/
class ExceptionPanelController
{
public function __construct(
private HtmlErrorRenderer $errorRenderer,
private ?Profiler $profiler = null,
) {
}
/**
* Renders the exception panel stacktrace for the given token.
*/
public function body(string $token): Response
{
if (null === $this->profiler) {
throw new NotFoundHttpException('The profiler must be enabled.');
}
$exception = $this->profiler->loadProfile($token)
->getCollector('exception')
->getException()
;
return new Response($this->errorRenderer->getBody($exception), 200, ['Content-Type' => 'text/html']);
}
/**
* Renders the exception panel stylesheet.
*/
public function stylesheet(): Response
{
return new Response($this->errorRenderer->getStylesheet(), 200, ['Content-Type' => 'text/css']);
}
}

View File

@@ -0,0 +1,432 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\WebProfilerBundle\Controller;
use Symfony\Bundle\FullStack;
use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler;
use Symfony\Bundle\WebProfilerBundle\Profiler\TemplateManager;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Flash\AutoExpireFlashBag;
use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector;
use Symfony\Component\HttpKernel\DataCollector\ExceptionDataCollector;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Profiler\Profiler;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Environment;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
class ProfilerController
{
private TemplateManager $templateManager;
public function __construct(
private UrlGeneratorInterface $generator,
private ?Profiler $profiler,
private Environment $twig,
private array $templates,
private ?ContentSecurityPolicyHandler $cspHandler = null,
private ?string $baseDir = null,
) {
}
/**
* Redirects to the last profiles.
*
* @throws NotFoundHttpException
*/
public function homeAction(): RedirectResponse
{
$this->denyAccessIfProfilerDisabled();
return new RedirectResponse($this->generator->generate('_profiler_search_results', ['token' => 'empty', 'limit' => 10]), 302, ['Content-Type' => 'text/html']);
}
/**
* Renders a profiler panel for the given token.
*
* @throws NotFoundHttpException
*/
public function panelAction(Request $request, string $token): Response
{
$this->denyAccessIfProfilerDisabled();
$this->cspHandler?->disableCsp();
$panel = $request->query->get('panel');
$page = $request->query->get('page', 'home');
$profileType = $request->query->get('type', 'request');
if ('latest' === $token && $latest = current($this->profiler->find(null, null, 1, null, null, null, null, fn ($profile) => $profileType === $profile['virtual_type']))) {
$token = $latest['token'];
}
if (!$profile = $this->profiler->loadProfile($token)) {
return $this->renderWithCspNonces($request, '@WebProfiler/Profiler/info.html.twig', ['about' => 'no_token', 'token' => $token, 'request' => $request, 'profile_type' => $profileType]);
}
$profileType = $profile->getVirtualType() ?? 'request';
if (null === $panel) {
$panel = $profileType;
foreach ($profile->getCollectors() as $collector) {
if ($collector instanceof ExceptionDataCollector && $collector->hasException()) {
$panel = $collector->getName();
break;
}
if ($collector instanceof DumpDataCollector && $collector->getDumpsCount() > 0) {
$panel = $collector->getName();
}
}
}
if (!$profile->hasCollector($panel)) {
throw new NotFoundHttpException(\sprintf('Panel "%s" is not available for token "%s".', $panel, $token));
}
return $this->renderWithCspNonces($request, $this->getTemplateManager()->getName($profile, $panel), [
'token' => $token,
'profile' => $profile,
'collector' => $profile->getCollector($panel),
'panel' => $panel,
'page' => $page,
'request' => $request,
'templates' => $this->getTemplateManager()->getNames($profile),
'is_ajax' => $request->isXmlHttpRequest(),
'profiler_markup_version' => 3, // 1 = original profiler, 2 = Symfony 2.8+ profiler, 3 = Symfony 6.2+ profiler
'profile_type' => $profileType,
]);
}
/**
* Renders the Web Debug Toolbar.
*
* @throws NotFoundHttpException
*/
public function toolbarAction(Request $request, ?string $token = null): Response
{
if (null === $this->profiler) {
throw new NotFoundHttpException('The profiler must be enabled.');
}
if (!$request->attributes->getBoolean('_stateless') && $request->hasSession()
&& ($session = $request->getSession())->isStarted() && $session->getFlashBag() instanceof AutoExpireFlashBag
) {
// keep current flashes for one more request if using AutoExpireFlashBag
$session->getFlashBag()->setAll($session->getFlashBag()->peekAll());
}
if ('empty' === $token || null === $token) {
return new Response('', 200, ['Content-Type' => 'text/html']);
}
$this->profiler->disable();
if (!$profile = $this->profiler->loadProfile($token)) {
return new Response('', 404, ['Content-Type' => 'text/html']);
}
$url = null;
try {
$url = $this->generator->generate('_profiler', ['token' => $token], UrlGeneratorInterface::ABSOLUTE_URL);
} catch (\Exception) {
// the profiler is not enabled
}
return $this->renderWithCspNonces($request, '@WebProfiler/Profiler/toolbar.html.twig', [
'full_stack' => class_exists(FullStack::class),
'request' => $request,
'profile' => $profile,
'templates' => $this->getTemplateManager()->getNames($profile),
'profiler_url' => $url,
'token' => $token,
'profiler_markup_version' => 3, // 1 = original toolbar, 2 = Symfony 2.8+ profiler, 3 = Symfony 6.2+ profiler
]);
}
/**
* Renders the Web Debug Toolbar stylesheet.
*
* @throws NotFoundHttpException
*/
public function toolbarStylesheetAction(): Response
{
$this->denyAccessIfProfilerDisabled();
$this->cspHandler?->disableCsp();
return new Response(
$this->twig->render('@WebProfiler/Profiler/toolbar.css.twig'),
200,
[
'Content-Type' => 'text/css',
'Cache-Control' => 'max-age=600, private',
],
);
}
/**
* Renders the profiler search bar.
*
* @throws NotFoundHttpException
*/
public function searchBarAction(Request $request): Response
{
$this->denyAccessIfProfilerDisabled();
$this->cspHandler?->disableCsp();
$session = null;
if (!$request->attributes->getBoolean('_stateless') && $request->hasSession()) {
$session = $request->getSession();
}
return new Response(
$this->twig->render('@WebProfiler/Profiler/search.html.twig', [
'token' => $request->query->get('token', $session?->get('_profiler_search_token')),
'ip' => $request->query->get('ip', $session?->get('_profiler_search_ip')),
'method' => $request->query->get('method', $session?->get('_profiler_search_method')),
'status_code' => $request->query->get('status_code', $session?->get('_profiler_search_status_code')),
'url' => $request->query->get('url', $session?->get('_profiler_search_url')),
'start' => $request->query->get('start', $session?->get('_profiler_search_start')),
'end' => $request->query->get('end', $session?->get('_profiler_search_end')),
'limit' => $request->query->get('limit', $session?->get('_profiler_search_limit')),
'request' => $request,
'profile_type' => $request->query->get('type', $session?->get('_profiler_search_type', 'request')),
]),
200,
['Content-Type' => 'text/html']
);
}
/**
* Renders the search results.
*
* @throws NotFoundHttpException
*/
public function searchResultsAction(Request $request, string $token): Response
{
$this->denyAccessIfProfilerDisabled();
$this->cspHandler?->disableCsp();
$profile = $this->profiler->loadProfile($token);
$ip = $request->query->get('ip');
$method = $request->query->get('method');
$statusCode = $request->query->get('status_code');
$url = $request->query->get('url');
$start = $request->query->get('start', null);
$end = $request->query->get('end', null);
$limit = $request->query->get('limit');
$profileType = $request->query->get('type', 'request');
return $this->renderWithCspNonces($request, '@WebProfiler/Profiler/results.html.twig', [
'request' => $request,
'token' => $token,
'profile' => $profile,
'tokens' => $this->profiler->find($ip, $url, $limit, $method, $start, $end, $statusCode, fn ($profile) => $profileType === $profile['virtual_type']),
'ip' => $ip,
'method' => $method,
'status_code' => $statusCode,
'url' => $url,
'start' => $start,
'end' => $end,
'limit' => $limit,
'panel' => null,
'profile_type' => $profileType,
]);
}
/**
* Narrows the search bar.
*
* @throws NotFoundHttpException
*/
public function searchAction(Request $request): Response
{
$this->denyAccessIfProfilerDisabled();
$ip = $request->query->get('ip');
$method = $request->query->get('method');
$statusCode = $request->query->get('status_code');
$url = $request->query->get('url');
$start = $request->query->get('start', null);
$end = $request->query->get('end', null);
$limit = $request->query->get('limit');
$token = $request->query->get('token');
$profileType = $request->query->get('type', 'request');
if (!$request->attributes->getBoolean('_stateless') && $request->hasSession()) {
$session = $request->getSession();
$session->set('_profiler_search_ip', $ip);
$session->set('_profiler_search_method', $method);
$session->set('_profiler_search_status_code', $statusCode);
$session->set('_profiler_search_url', $url);
$session->set('_profiler_search_start', $start);
$session->set('_profiler_search_end', $end);
$session->set('_profiler_search_limit', $limit);
$session->set('_profiler_search_token', $token);
$session->set('_profiler_search_type', $profileType);
}
if ($token) {
return new RedirectResponse($this->generator->generate('_profiler', ['token' => $token]), 302, ['Content-Type' => 'text/html']);
}
$tokens = $this->profiler->find($ip, $url, $limit, $method, $start, $end, $statusCode, fn ($profile) => $profileType === $profile['virtual_type']);
return new RedirectResponse($this->generator->generate('_profiler_search_results', [
'token' => $tokens ? $tokens[0]['token'] : 'empty',
'ip' => $ip,
'method' => $method,
'status_code' => $statusCode,
'url' => $url,
'start' => $start,
'end' => $end,
'limit' => $limit,
'type' => $profileType,
]), 302, ['Content-Type' => 'text/html']);
}
/**
* Displays the PHP info.
*
* @throws NotFoundHttpException
*/
public function phpinfoAction(): Response
{
$this->denyAccessIfProfilerDisabled();
$this->cspHandler?->disableCsp();
ob_start();
phpinfo();
$phpinfo = ob_get_clean();
return new Response($phpinfo, 200, ['Content-Type' => 'text/html']);
}
/**
* Displays the Xdebug info.
*
* @throws NotFoundHttpException
*/
public function xdebugAction(): Response
{
$this->denyAccessIfProfilerDisabled();
if (!\function_exists('xdebug_info')) {
throw new NotFoundHttpException('Xdebug must be installed in version 3.');
}
$this->cspHandler?->disableCsp();
ob_start();
xdebug_info();
$xdebugInfo = ob_get_clean();
return new Response($xdebugInfo, 200, ['Content-Type' => 'text/html']);
}
/**
* Returns the custom web fonts used in the profiler.
*
* @throws NotFoundHttpException
*/
public function fontAction(string $fontName): Response
{
$this->denyAccessIfProfilerDisabled();
if ('JetBrainsMono' !== $fontName) {
throw new NotFoundHttpException(\sprintf('Font file "%s.woff2" not found.', $fontName));
}
$fontFile = \dirname(__DIR__).'/Resources/fonts/'.$fontName.'.woff2';
if (!is_file($fontFile) || !is_readable($fontFile)) {
throw new NotFoundHttpException(\sprintf('Cannot read font file "%s".', $fontFile));
}
$this->profiler?->disable();
return new BinaryFileResponse($fontFile, 200, ['Content-Type' => 'font/woff2']);
}
/**
* Displays the source of a file.
*
* @throws NotFoundHttpException
*/
public function openAction(Request $request): Response
{
if (null === $this->baseDir) {
throw new NotFoundHttpException('The base dir should be set.');
}
$this->profiler?->disable();
$file = $request->query->get('file');
$line = $request->query->get('line');
$filename = $this->baseDir.\DIRECTORY_SEPARATOR.$file;
if (preg_match("'(^|[/\\\\])\.'", $file) || !is_readable($filename)) {
throw new NotFoundHttpException(\sprintf('The file "%s" cannot be opened.', $file));
}
return $this->renderWithCspNonces($request, '@WebProfiler/Profiler/open.html.twig', [
'file_info' => new \SplFileInfo($filename),
'file' => $file,
'line' => $line,
]);
}
protected function getTemplateManager(): TemplateManager
{
return $this->templateManager ??= new TemplateManager($this->profiler, $this->twig, $this->templates);
}
/**
* @throws NotFoundHttpException
*/
private function denyAccessIfProfilerDisabled(): void
{
if (null === $this->profiler) {
throw new NotFoundHttpException('The profiler must be enabled.');
}
$this->profiler->disable();
}
private function renderWithCspNonces(Request $request, string $template, array $variables, int $code = 200, array $headers = ['Content-Type' => 'text/html']): Response
{
$response = new Response('', $code, $headers);
$nonces = $this->cspHandler ? $this->cspHandler->getNonces($request, $response) : [];
$variables['csp_script_nonce'] = $nonces['csp_script_nonce'] ?? null;
$variables['csp_style_nonce'] = $nonces['csp_style_nonce'] ?? null;
$response->setContent($this->twig->render($template, $variables));
return $response;
}
}

View File

@@ -0,0 +1,100 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\WebProfilerBundle\Controller;
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\RequestDataCollector;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Profiler\Profiler;
use Symfony\Component\Routing\Matcher\TraceableUrlMatcher;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\RouterInterface;
use Twig\Environment;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
class RouterController
{
/**
* @param ExpressionFunctionProviderInterface[] $expressionLanguageProviders
*/
public function __construct(
private ?Profiler $profiler,
private Environment $twig,
private ?UrlMatcherInterface $matcher = null,
private ?RouteCollection $routes = null,
private iterable $expressionLanguageProviders = [],
) {
if ($this->matcher instanceof RouterInterface) {
$this->routes ??= $this->matcher->getRouteCollection();
}
}
/**
* Renders the profiler panel for the given token.
*
* @throws NotFoundHttpException
*/
public function panelAction(string $token): Response
{
if (null === $this->profiler) {
throw new NotFoundHttpException('The profiler must be enabled.');
}
$this->profiler->disable();
if (null === $this->matcher || null === $this->routes) {
return new Response('The Router is not enabled.', 200, ['Content-Type' => 'text/html']);
}
$profile = $this->profiler->loadProfile($token);
/** @var RequestDataCollector $request */
$request = $profile->getCollector('request');
return new Response($this->twig->render('@WebProfiler/Router/panel.html.twig', [
'request' => $request,
'router' => $profile->getCollector('router'),
'traces' => $this->getTraces($request, $profile->getMethod()),
]), 200, ['Content-Type' => 'text/html']);
}
/**
* Returns the routing traces associated to the given request.
*/
private function getTraces(RequestDataCollector $request, string $method): array
{
$traceRequest = new Request(
$request->getRequestQuery()->all(),
$request->getRequestRequest()->all(),
$request->getRequestAttributes()->all(),
$request->getRequestCookies(true)->all(),
[],
$request->getRequestServer(true)->all()
);
$context = $this->matcher->getContext();
$context->setMethod($method);
$matcher = new TraceableUrlMatcher($this->routes, $context);
foreach ($this->expressionLanguageProviders as $provider) {
$matcher->addExpressionLanguageProvider($provider);
}
return $matcher->getTracesForRequest($traceRequest);
}
}

View File

@@ -0,0 +1,267 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\WebProfilerBundle\Csp;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Handles Content-Security-Policy HTTP header for the WebProfiler Bundle.
*
* @author Romain Neutron <imprec@gmail.com>
*
* @internal
*/
class ContentSecurityPolicyHandler
{
private bool $cspDisabled = false;
public function __construct(
private NonceGenerator $nonceGenerator,
) {
}
/**
* Returns an array of nonces to be used in Twig templates and Content-Security-Policy headers.
*
* Nonce can be provided by;
* - The request - In case HTML content is fetched via AJAX and inserted in DOM, it must use the same nonce as origin
* - The response - A call to getNonces() has already been done previously. Same nonce are returned
* - They are otherwise randomly generated
*/
public function getNonces(Request $request, Response $response): array
{
if ($request->headers->has('X-SymfonyProfiler-Script-Nonce') && $request->headers->has('X-SymfonyProfiler-Style-Nonce')) {
return [
'csp_script_nonce' => $request->headers->get('X-SymfonyProfiler-Script-Nonce'),
'csp_style_nonce' => $request->headers->get('X-SymfonyProfiler-Style-Nonce'),
];
}
if ($response->headers->has('X-SymfonyProfiler-Script-Nonce') && $response->headers->has('X-SymfonyProfiler-Style-Nonce')) {
return [
'csp_script_nonce' => $response->headers->get('X-SymfonyProfiler-Script-Nonce'),
'csp_style_nonce' => $response->headers->get('X-SymfonyProfiler-Style-Nonce'),
];
}
$nonces = [
'csp_script_nonce' => $this->generateNonce(),
'csp_style_nonce' => $this->generateNonce(),
];
$response->headers->set('X-SymfonyProfiler-Script-Nonce', $nonces['csp_script_nonce']);
$response->headers->set('X-SymfonyProfiler-Style-Nonce', $nonces['csp_style_nonce']);
return $nonces;
}
/**
* Disables Content-Security-Policy.
*
* All related headers will be removed.
*/
public function disableCsp(): void
{
$this->cspDisabled = true;
}
/**
* Cleanup temporary headers and updates Content-Security-Policy headers.
*
* @return array Nonces used by the bundle in Content-Security-Policy header
*/
public function updateResponseHeaders(Request $request, Response $response): array
{
if ($this->cspDisabled) {
$this->removeCspHeaders($response);
return [];
}
$nonces = $this->getNonces($request, $response);
$this->cleanHeaders($response);
$this->updateCspHeaders($response, $nonces);
return $nonces;
}
private function cleanHeaders(Response $response): void
{
$response->headers->remove('X-SymfonyProfiler-Script-Nonce');
$response->headers->remove('X-SymfonyProfiler-Style-Nonce');
}
private function removeCspHeaders(Response $response): void
{
$response->headers->remove('X-Content-Security-Policy');
$response->headers->remove('Content-Security-Policy');
$response->headers->remove('Content-Security-Policy-Report-Only');
}
/**
* Updates Content-Security-Policy headers in a response.
*/
private function updateCspHeaders(Response $response, array $nonces = []): array
{
$nonces = array_replace([
'csp_script_nonce' => $this->generateNonce(),
'csp_style_nonce' => $this->generateNonce(),
], $nonces);
$ruleIsSet = false;
$headers = $this->getCspHeaders($response);
$types = [
'script-src' => 'csp_script_nonce',
'script-src-elem' => 'csp_script_nonce',
'style-src' => 'csp_style_nonce',
'style-src-elem' => 'csp_style_nonce',
];
foreach ($headers as $header => $directives) {
foreach ($types as $type => $tokenName) {
if ($this->authorizesInline($directives, $type)) {
continue;
}
if (!isset($headers[$header][$type])) {
if (null === $fallback = $this->getDirectiveFallback($directives, $type)) {
continue;
}
if (['\'none\''] === $fallback) {
// Fallback came from "default-src: 'none'"
// 'none' is invalid if it's not the only expression in the source list, so we leave it out
$fallback = [];
}
$headers[$header][$type] = $fallback;
}
$ruleIsSet = true;
if (!\in_array('\'unsafe-inline\'', $headers[$header][$type], true)) {
$headers[$header][$type][] = '\'unsafe-inline\'';
}
$headers[$header][$type][] = \sprintf('\'nonce-%s\'', $nonces[$tokenName]);
}
}
if (!$ruleIsSet) {
return $nonces;
}
foreach ($headers as $header => $directives) {
$response->headers->set($header, $this->generateCspHeader($directives));
}
return $nonces;
}
/**
* Generates a valid Content-Security-Policy nonce.
*/
private function generateNonce(): string
{
return $this->nonceGenerator->generate();
}
/**
* Converts a directive set array into Content-Security-Policy header.
*/
private function generateCspHeader(array $directives): string
{
return array_reduce(array_keys($directives), fn ($res, $name) => ('' !== $res ? $res.'; ' : '').\sprintf('%s %s', $name, implode(' ', $directives[$name])), '');
}
/**
* Converts a Content-Security-Policy header value into a directive set array.
*/
private function parseDirectives(string $header): array
{
$directives = [];
foreach (explode(';', $header) as $directive) {
$parts = explode(' ', trim($directive));
if (\count($parts) < 1) {
continue;
}
$name = array_shift($parts);
$directives[$name] = $parts;
}
return $directives;
}
/**
* Detects if the 'unsafe-inline' is prevented for a directive within the directive set.
*/
private function authorizesInline(array $directivesSet, string $type): bool
{
if (isset($directivesSet[$type])) {
$directives = $directivesSet[$type];
} elseif (null === $directives = $this->getDirectiveFallback($directivesSet, $type)) {
return false;
}
return \in_array('\'unsafe-inline\'', $directives, true) && !$this->hasHashOrNonce($directives);
}
private function hasHashOrNonce(array $directives): bool
{
foreach ($directives as $directive) {
if (!str_ends_with($directive, '\'')) {
continue;
}
if (str_starts_with($directive, '\'nonce-')) {
return true;
}
if (\in_array(substr($directive, 0, 8), ['\'sha256-', '\'sha384-', '\'sha512-'], true)) {
return true;
}
}
return false;
}
private function getDirectiveFallback(array $directiveSet, string $type): ?array
{
if (\in_array($type, ['script-src-elem', 'style-src-elem'], true) || !isset($directiveSet['default-src'])) {
// Let the browser fallback on it's own
return null;
}
return $directiveSet['default-src'];
}
/**
* Retrieves the Content-Security-Policy headers (either X-Content-Security-Policy or Content-Security-Policy) from
* a response.
*/
private function getCspHeaders(Response $response): array
{
$headers = [];
if ($response->headers->has('Content-Security-Policy')) {
$headers['Content-Security-Policy'] = $this->parseDirectives($response->headers->get('Content-Security-Policy'));
}
if ($response->headers->has('Content-Security-Policy-Report-Only')) {
$headers['Content-Security-Policy-Report-Only'] = $this->parseDirectives($response->headers->get('Content-Security-Policy-Report-Only'));
}
if ($response->headers->has('X-Content-Security-Policy')) {
$headers['X-Content-Security-Policy'] = $this->parseDirectives($response->headers->get('X-Content-Security-Policy'));
}
return $headers;
}
}

View File

@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\WebProfilerBundle\Csp;
/**
* Generates Content-Security-Policy nonce.
*
* @author Romain Neutron <imprec@gmail.com>
*
* @internal
*/
class NonceGenerator
{
public function generate(): string
{
return bin2hex(random_bytes(16));
}
}

View File

@@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\WebProfilerBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
/**
* This class contains the configuration information for the bundle.
*
* This information is solely responsible for how the different configuration
* sections are normalized, and merged.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Configuration implements ConfigurationInterface
{
/**
* Generates the configuration tree builder.
*/
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('web_profiler');
$treeBuilder
->getRootNode()
->docUrl('https://symfony.com/doc/{version:major}.{version:minor}/reference/configuration/web_profiler.html', 'symfony/web-profiler-bundle')
->children()
->arrayNode('toolbar')
->info('Profiler toolbar configuration')
->canBeEnabled()
->children()
->booleanNode('ajax_replace')
->defaultFalse()
->info('Replace toolbar on AJAX requests')
->end()
->end()
->end()
->booleanNode('intercept_redirects')->defaultFalse()->end()
->scalarNode('excluded_ajax_paths')->defaultValue('^/((index|app(_[\w]+)?)\.php/)?_wdt')->end()
->end()
;
return $treeBuilder;
}
}

View File

@@ -0,0 +1,70 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\WebProfilerBundle\DependencyInjection;
use Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\DependencyInjection\Reference;
/**
* WebProfilerExtension.
*
* Usage:
*
* <webprofiler:config
* toolbar="true"
* intercept-redirects="true"
* />
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class WebProfilerExtension extends Extension
{
/**
* Loads the web profiler configuration.
*
* @param array $configs An array of configuration settings
*/
public function load(array $configs, ContainerBuilder $container): void
{
$configuration = $this->getConfiguration($configs, $container);
$config = $this->processConfiguration($configuration, $configs);
$loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('profiler.php');
if ($config['toolbar']['enabled'] || $config['intercept_redirects']) {
$loader->load('toolbar.php');
$container->getDefinition('web_profiler.debug_toolbar')->replaceArgument(4, $config['excluded_ajax_paths']);
$container->getDefinition('web_profiler.debug_toolbar')->replaceArgument(7, $config['toolbar']['ajax_replace']);
$container->setParameter('web_profiler.debug_toolbar.intercept_redirects', $config['intercept_redirects']);
$container->setParameter('web_profiler.debug_toolbar.mode', $config['toolbar']['enabled'] ? WebDebugToolbarListener::ENABLED : WebDebugToolbarListener::DISABLED);
}
$container->getDefinition('debug.file_link_formatter')
->replaceArgument(3, new ServiceClosureArgument(new Reference('debug.file_link_formatter.url_format')));
}
public function getXsdValidationBasePath(): string|false
{
return __DIR__.'/../Resources/config/schema';
}
public function getNamespace(): string
{
return 'http://symfony.com/schema/dic/webprofiler';
}
}

View File

@@ -0,0 +1,162 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\WebProfilerBundle\EventListener;
use Symfony\Bundle\FullStack;
use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Flash\AutoExpireFlashBag;
use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Environment;
/**
* WebDebugToolbarListener injects the Web Debug Toolbar.
*
* The onKernelResponse method must be connected to the kernel.response event.
*
* The WDT is only injected on well-formed HTML (with a proper </body> tag).
* This means that the WDT is never included in sub-requests or ESI requests.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/
class WebDebugToolbarListener implements EventSubscriberInterface
{
public const DISABLED = 1;
public const ENABLED = 2;
public function __construct(
private Environment $twig,
private bool $interceptRedirects = false,
private int $mode = self::ENABLED,
private ?UrlGeneratorInterface $urlGenerator = null,
private string $excludedAjaxPaths = '^/bundles|^/_wdt',
private ?ContentSecurityPolicyHandler $cspHandler = null,
private ?DumpDataCollector $dumpDataCollector = null,
private bool $ajaxReplace = false,
) {
}
public function isEnabled(): bool
{
return self::DISABLED !== $this->mode;
}
public function setMode(int $mode): void
{
if (self::DISABLED !== $mode && self::ENABLED !== $mode) {
throw new \InvalidArgumentException(\sprintf('Invalid value provided for mode, use one of "%s::DISABLED" or "%s::ENABLED".', self::class, self::class));
}
$this->mode = $mode;
}
public function onKernelResponse(ResponseEvent $event): void
{
$response = $event->getResponse();
$request = $event->getRequest();
if ($response->headers->has('X-Debug-Token') && null !== $this->urlGenerator) {
try {
$response->headers->set(
'X-Debug-Token-Link',
$this->urlGenerator->generate('_profiler', ['token' => $response->headers->get('X-Debug-Token')], UrlGeneratorInterface::ABSOLUTE_URL)
);
} catch (\Exception $e) {
$response->headers->set('X-Debug-Error', $e::class.': '.preg_replace('/\s+/', ' ', $e->getMessage()));
}
}
if (!$event->isMainRequest()) {
return;
}
$nonces = [];
if ($this->cspHandler) {
if ($this->dumpDataCollector?->getDumpsCount() > 0) {
$this->cspHandler->disableCsp();
}
$nonces = $this->cspHandler->updateResponseHeaders($request, $response);
}
// do not capture redirects or modify XML HTTP Requests
if ($request->isXmlHttpRequest()) {
if (self::ENABLED === $this->mode && $this->ajaxReplace && !$response->headers->has('Symfony-Debug-Toolbar-Replace')) {
$response->headers->set('Symfony-Debug-Toolbar-Replace', '1');
}
return;
}
if ($response->headers->has('X-Debug-Token') && $response->isRedirect() && $this->interceptRedirects && 'html' === $request->getRequestFormat() && $response->headers->has('Location')) {
if ($request->hasSession() && ($session = $request->getSession())->isStarted() && $session->getFlashBag() instanceof AutoExpireFlashBag) {
// keep current flashes for one more request if using AutoExpireFlashBag
$session->getFlashBag()->setAll($session->getFlashBag()->peekAll());
}
$response->setContent($this->twig->render('@WebProfiler/Profiler/toolbar_redirect.html.twig', ['location' => $response->headers->get('Location'), 'host' => $request->getSchemeAndHttpHost()]));
$response->setStatusCode(200);
$response->headers->remove('Location');
}
if (self::DISABLED === $this->mode
|| !$response->headers->has('X-Debug-Token')
|| $response->isRedirection()
|| ($response->headers->has('Content-Type') && !str_contains($response->headers->get('Content-Type') ?? '', 'html'))
|| 'html' !== $request->getRequestFormat()
|| false !== stripos($response->headers->get('Content-Disposition', ''), 'attachment;')
) {
return;
}
$this->injectToolbar($response, $request, $nonces);
}
/**
* Injects the web debug toolbar into the given Response.
*/
protected function injectToolbar(Response $response, Request $request, array $nonces): void
{
$content = $response->getContent();
$pos = strripos($content, '</body>');
if (false !== $pos) {
$toolbar = "\n".str_replace("\n", '', $this->twig->render(
'@WebProfiler/Profiler/toolbar_js.html.twig',
[
'full_stack' => class_exists(FullStack::class),
'excluded_ajax_paths' => $this->excludedAjaxPaths,
'token' => $response->headers->get('X-Debug-Token'),
'request' => $request,
'csp_script_nonce' => $nonces['csp_script_nonce'] ?? null,
'csp_style_nonce' => $nonces['csp_style_nonce'] ?? null,
]
))."\n";
$content = substr($content, 0, $pos).$toolbar.substr($content, $pos);
$response->setContent($content);
}
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::RESPONSE => ['onKernelResponse', -128],
];
}
}

View File

@@ -0,0 +1,19 @@
Copyright (c) 2004-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,296 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\WebProfilerBundle\Profiler;
use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
/**
* Twig extension related to PHP code and used by the profiler and the default exception templates.
*
* This extension should only be used for debugging tools code
* that is never executed in a production environment.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class CodeExtension extends AbstractExtension
{
private string|FileLinkFormatter|array|false $fileLinkFormat;
public function __construct(
string|FileLinkFormatter $fileLinkFormat,
private string $projectDir,
private string $charset,
) {
$this->fileLinkFormat = $fileLinkFormat ?: \ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format');
$this->projectDir = str_replace('\\', '/', $projectDir).'/';
}
public function getFilters(): array
{
return [
new TwigFilter('abbr_class', $this->abbrClass(...), ['is_safe' => ['html'], 'pre_escape' => 'html']),
new TwigFilter('abbr_method', $this->abbrMethod(...), ['is_safe' => ['html'], 'pre_escape' => 'html']),
new TwigFilter('format_args', $this->formatArgs(...), ['is_safe' => ['html']]),
new TwigFilter('format_args_as_text', $this->formatArgsAsText(...)),
new TwigFilter('file_excerpt', $this->fileExcerpt(...), ['is_safe' => ['html']]),
new TwigFilter('format_file', $this->formatFile(...), ['is_safe' => ['html']]),
new TwigFilter('format_file_from_text', $this->formatFileFromText(...), ['is_safe' => ['html']]),
new TwigFilter('format_log_message', $this->formatLogMessage(...), ['is_safe' => ['html']]),
new TwigFilter('file_link', $this->getFileLink(...)),
new TwigFilter('file_relative', $this->getFileRelative(...)),
];
}
public function abbrClass(string $class): string
{
$parts = explode('\\', $class);
$short = array_pop($parts);
return \sprintf('<abbr title="%s">%s</abbr>', $class, $short);
}
public function abbrMethod(string $method): string
{
if (str_contains($method, '::')) {
[$class, $method] = explode('::', $method, 2);
$result = \sprintf('%s::%s()', $this->abbrClass($class), $method);
} elseif ('Closure' === $method) {
$result = \sprintf('<abbr title="%s">%1$s</abbr>', $method);
} else {
$result = \sprintf('<abbr title="%s">%1$s</abbr>()', $method);
}
return $result;
}
/**
* Formats an array as a string.
*/
public function formatArgs(array $args): string
{
$result = [];
foreach ($args as $key => $item) {
if ('object' === $item[0]) {
$item[1] = htmlspecialchars($item[1], \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset);
$parts = explode('\\', $item[1]);
$short = array_pop($parts);
$formattedValue = \sprintf('<em>object</em>(<abbr title="%s">%s</abbr>)', $item[1], $short);
} elseif ('array' === $item[0]) {
$formattedValue = \sprintf('<em>array</em>(%s)', \is_array($item[1]) ? $this->formatArgs($item[1]) : htmlspecialchars(var_export($item[1], true), \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset));
} elseif ('null' === $item[0]) {
$formattedValue = '<em>null</em>';
} elseif ('boolean' === $item[0]) {
$formattedValue = '<em>'.strtolower(htmlspecialchars(var_export($item[1], true), \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset)).'</em>';
} elseif ('resource' === $item[0]) {
$formattedValue = '<em>resource</em>';
} elseif (preg_match('/[^\x07-\x0D\x1B\x20-\xFF]/', $item[1])) {
$formattedValue = '<em>binary string</em>';
} else {
$formattedValue = str_replace("\n", '', htmlspecialchars(var_export($item[1], true), \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset));
}
$result[] = \is_int($key) ? $formattedValue : \sprintf("'%s' => %s", htmlspecialchars($key, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset), $formattedValue);
}
return implode(', ', $result);
}
/**
* Formats an array as a string.
*/
public function formatArgsAsText(array $args): string
{
return strip_tags($this->formatArgs($args));
}
/**
* Returns an excerpt of a code file around the given line number.
*/
public function fileExcerpt(string $file, int $line, int $srcContext = 3): ?string
{
if (!is_file($file) || !is_readable($file)) {
return null;
}
$contents = file_get_contents($file);
if (!str_contains($contents, '<?php') && !str_contains($contents, '<?=')) {
$lines = explode("\n", $contents);
if (0 > $srcContext) {
$srcContext = \count($lines);
}
return $this->formatFileExcerpt(
$this->extractExcerptLines($lines, $line, $srcContext),
$line,
$srcContext
);
}
// highlight_string could throw warnings
// see https://bugs.php.net/25725
$code = @highlight_string($contents, true);
if (\PHP_VERSION_ID >= 80300) {
// remove main pre/code tags
$code = preg_replace('#^<pre.*?>\s*<code.*?>(.*)</code>\s*</pre>#s', '\\1', $code);
// split multiline span tags
$code = preg_replace_callback(
'#<span ([^>]++)>((?:[^<\\n]*+\\n)++[^<]*+)</span>#',
static fn (array $m): string => "<span $m[1]>".str_replace("\n", "</span>\n<span $m[1]>", $m[2]).'</span>',
$code
);
$lines = explode("\n", $code);
} else {
// remove main code/span tags
$code = preg_replace('#^<code.*?>\s*<span.*?>(.*)</span>\s*</code>#s', '\\1', $code);
// split multiline spans
$code = preg_replace_callback(
'#<span ([^>]++)>((?:[^<]*+<br \/>)++[^<]*+)</span>#',
static fn (array $m): string => "<span $m[1]>".str_replace('<br />', "</span><br /><span $m[1]>", $m[2]).'</span>',
$code
);
$lines = explode('<br />', $code);
}
if (0 > $srcContext) {
$srcContext = \count($lines);
}
return $this->formatFileExcerpt(
array_map(
self::fixCodeMarkup(...),
$this->extractExcerptLines($lines, $line, $srcContext),
),
$line,
$srcContext
);
}
private function extractExcerptLines(array $lines, int $selectedLine, int $srcContext): array
{
return \array_slice(
$lines,
max($selectedLine - $srcContext, 0),
min($srcContext * 2 + 1, \count($lines) - $selectedLine + $srcContext),
true
);
}
private function formatFileExcerpt(array $lines, int $selectedLine, int $srcContext): string
{
$start = max($selectedLine - $srcContext, 1);
return "<ol start=\"{$start}\">".implode("\n", array_map(
static fn (string $line, int $num): string => '<li'.(++$num === $selectedLine ? ' class="selected"' : '')."><a class=\"anchor\" id=\"line{$num}\"></a><code>{$line}</code></li>",
$lines,
array_keys($lines),
)).'</ol>';
}
/**
* Formats a file path.
*/
public function formatFile(string $file, int $line, ?string $text = null): string
{
$file = trim($file);
if (null === $text) {
if (null !== $rel = $this->getFileRelative($file)) {
$rel = explode('/', htmlspecialchars($rel, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset), 2);
$text = \sprintf('<abbr title="%s%2$s">%s</abbr>%s', htmlspecialchars($this->projectDir, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset), $rel[0], '/'.($rel[1] ?? ''));
} else {
$text = htmlspecialchars($file, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset);
}
} else {
$text = htmlspecialchars($text, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset);
}
if (0 < $line) {
$text .= ' at line '.$line;
}
if (false !== $link = $this->getFileLink($file, $line)) {
return \sprintf('<a href="%s" title="Click to open this file" class="file_link">%s</a>', htmlspecialchars($link, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset), $text);
}
return $text;
}
public function getFileLink(string $file, int $line): string|false
{
if ($fmt = $this->fileLinkFormat) {
return \is_string($fmt) ? strtr($fmt, ['%f' => $file, '%l' => $line]) : $fmt->format($file, $line);
}
return false;
}
public function getFileRelative(string $file): ?string
{
$file = str_replace('\\', '/', $file);
if (null !== $this->projectDir && str_starts_with($file, $this->projectDir)) {
return ltrim(substr($file, \strlen($this->projectDir)), '/');
}
return null;
}
public function formatFileFromText(string $text): string
{
return preg_replace_callback('/in ("|&quot;)?(.+?)\1(?: +(?:on|at))? +line (\d+)/s', fn ($match) => 'in '.$this->formatFile($match[2], $match[3]), $text);
}
/**
* @internal
*/
public function formatLogMessage(string $message, array $context): string
{
if ($context && str_contains($message, '{')) {
$replacements = [];
foreach ($context as $key => $val) {
if (\is_scalar($val)) {
$replacements['{'.$key.'}'] = $val;
}
}
if ($replacements) {
$message = strtr($message, $replacements);
}
}
return htmlspecialchars($message, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset);
}
protected static function fixCodeMarkup(string $line): string
{
// </span> ending tag from previous line
$opening = strpos($line, '<span');
$closing = strpos($line, '</span>');
if (false !== $closing && (false === $opening || $closing < $opening)) {
$line = substr_replace($line, '', $closing, 7);
}
// missing </span> tag at the end of line
$opening = strpos($line, '<span');
$closing = strpos($line, '</span>');
if (false !== $opening && (false === $closing || $closing < $opening)) {
$line .= '</span>';
}
return trim($line);
}
}

View File

@@ -0,0 +1,84 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\WebProfilerBundle\Profiler;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Profiler\Profile;
use Symfony\Component\HttpKernel\Profiler\Profiler;
use Twig\Environment;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Artur Wielogórski <wodor@wodor.net>
*
* @internal
*/
class TemplateManager
{
public function __construct(
protected Profiler $profiler,
protected Environment $twig,
protected array $templates,
) {
}
/**
* Gets the template name for a given panel.
*
* @throws NotFoundHttpException
*/
public function getName(Profile $profile, string $panel): mixed
{
$templates = $this->getNames($profile);
if (!isset($templates[$panel])) {
throw new NotFoundHttpException(\sprintf('Panel "%s" is not registered in profiler or is not present in viewed profile.', $panel));
}
return $templates[$panel];
}
/**
* Gets template names of templates that are present in the viewed profile.
*
* @throws \UnexpectedValueException
*/
public function getNames(Profile $profile): array
{
$loader = $this->twig->getLoader();
$templates = [];
foreach ($this->templates as $arguments) {
if (null === $arguments) {
continue;
}
[$name, $template] = $arguments;
if (!$this->profiler->has($name) || !$profile->hasCollector($name)) {
continue;
}
if (str_ends_with($template, '.html.twig')) {
$template = substr($template, 0, -10);
}
if (!$loader->exists($template.'.html.twig')) {
throw new \UnexpectedValueException(\sprintf('The profiler template "%s.html.twig" for data collector "%s" does not exist.', $template, $name));
}
$templates[$name] = $template.'.html.twig';
}
return $templates;
}
}

View File

@@ -0,0 +1,16 @@
WebProfilerBundle
=================
WebProfilerBundle provides a **development tool** that gives detailed
information about the execution of any request.
**Never** enable it on production servers as it will lead to major security
vulnerabilities in your project.
Resources
---------
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)

View File

@@ -0,0 +1,88 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Symfony\Bundle\WebProfilerBundle\Controller\ExceptionPanelController;
use Symfony\Bundle\WebProfilerBundle\Controller\ProfilerController;
use Symfony\Bundle\WebProfilerBundle\Controller\RouterController;
use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler;
use Symfony\Bundle\WebProfilerBundle\Csp\NonceGenerator;
use Symfony\Bundle\WebProfilerBundle\Profiler\CodeExtension;
use Symfony\Bundle\WebProfilerBundle\Twig\WebProfilerExtension;
use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter;
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
return static function (ContainerConfigurator $container) {
$container->services()
->set('web_profiler.controller.profiler', ProfilerController::class)
->public()
->args([
service('router')->nullOnInvalid(),
service('profiler')->nullOnInvalid(),
service('twig'),
param('data_collector.templates'),
service('web_profiler.csp.handler'),
param('kernel.project_dir'),
])
->set('web_profiler.controller.router', RouterController::class)
->public()
->args([
service('profiler')->nullOnInvalid(),
service('twig'),
service('router')->nullOnInvalid(),
null,
tagged_iterator('routing.expression_language_provider'),
])
->set('web_profiler.controller.exception_panel', ExceptionPanelController::class)
->public()
->args([
service('error_handler.error_renderer.html'),
service('profiler')->nullOnInvalid(),
])
->set('web_profiler.csp.handler', ContentSecurityPolicyHandler::class)
->args([
inline_service(NonceGenerator::class),
])
->set('twig.extension.webprofiler', WebProfilerExtension::class)
->args([
inline_service(HtmlDumper::class)
->args([null, param('kernel.charset'), HtmlDumper::DUMP_LIGHT_ARRAY])
->call('setDisplayOptions', [['maxStringLength' => 4096, 'fileLinkFormat' => service('debug.file_link_formatter')]]),
])
->tag('twig.extension')
->set('debug.file_link_formatter', FileLinkFormatter::class)
->args([
param('debug.file_link_format'),
service('request_stack')->ignoreOnInvalid(),
param('kernel.project_dir'),
'/_profiler/open?file=%%f&line=%%l#line%%l',
])
->set('debug.file_link_formatter.url_format', 'string')
->factory([FileLinkFormatter::class, 'generateUrlFormat'])
->args([
service('router'),
'_profiler_open_file',
'?file=%%f&line=%%l#line%%l',
])
->set('twig.extension.code', CodeExtension::class)
->args([service('debug.file_link_formatter'), param('kernel.project_dir'), param('kernel.charset')])
->tag('twig.extension')
;
};

View File

@@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
use Symfony\Component\Routing\Loader\XmlFileLoader;
return function (RoutingConfigurator $routes): void {
foreach (debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT) as $trace) {
if (isset($trace['object']) && $trace['object'] instanceof XmlFileLoader && 'doImport' === $trace['function']) {
if (__DIR__ === dirname(realpath($trace['args'][3]))) {
trigger_deprecation('symfony/routing', '7.3', 'The "profiler.xml" routing configuration file is deprecated, import "profiler.php" instead.');
break;
}
}
}
$routes->add('_profiler_home', '/')
->controller('web_profiler.controller.profiler::homeAction')
;
$routes->add('_profiler_search', '/search')
->controller('web_profiler.controller.profiler::searchAction')
;
$routes->add('_profiler_search_bar', '/search_bar')
->controller('web_profiler.controller.profiler::searchBarAction')
;
$routes->add('_profiler_phpinfo', '/phpinfo')
->controller('web_profiler.controller.profiler::phpinfoAction')
;
$routes->add('_profiler_xdebug', '/xdebug')
->controller('web_profiler.controller.profiler::xdebugAction')
;
$routes->add('_profiler_font', '/font/{fontName}.woff2')
->controller('web_profiler.controller.profiler::fontAction')
;
$routes->add('_profiler_search_results', '/{token}/search/results')
->controller('web_profiler.controller.profiler::searchResultsAction')
;
$routes->add('_profiler_open_file', '/open')
->controller('web_profiler.controller.profiler::openAction')
;
$routes->add('_profiler', '/{token}')
->controller('web_profiler.controller.profiler::panelAction')
;
$routes->add('_profiler_router', '/{token}/router')
->controller('web_profiler.controller.router::panelAction')
;
$routes->add('_profiler_exception', '/{token}/exception')
->controller('web_profiler.controller.exception_panel::body')
;
$routes->add('_profiler_exception_css', '/{token}/exception.css')
->controller('web_profiler.controller.exception_panel::stylesheet')
;
};

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing https://symfony.com/schema/routing/routing-1.0.xsd">
<import resource="profiler.php" type="php" />
</routes>

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
use Symfony\Component\Routing\Loader\XmlFileLoader;
return function (RoutingConfigurator $routes): void {
foreach (debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT) as $trace) {
if (isset($trace['object']) && $trace['object'] instanceof XmlFileLoader && 'doImport' === $trace['function']) {
if (__DIR__ === dirname(realpath($trace['args'][3]))) {
trigger_deprecation('symfony/routing', '7.3', 'The "wdt.xml" routing configuration file is deprecated, import "wdt.php" instead.');
break;
}
}
}
$routes->add('_wdt_stylesheet', '/styles')
->controller('web_profiler.controller.profiler::toolbarStylesheetAction')
;
$routes->add('_wdt', '/{token}')
->controller('web_profiler.controller.profiler::toolbarAction')
;
};

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing https://symfony.com/schema/routing/routing-1.0.xsd">
<import resource="wdt.php" type="php" />
</routes>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<xsd:schema xmlns="http://symfony.com/schema/dic/webprofiler"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://symfony.com/schema/dic/webprofiler"
elementFormDefault="qualified">
<xsd:element name="config" type="config" />
<xsd:complexType name="config">
<xsd:attribute name="toolbar" type="xsd:boolean" />
<xsd:sequence>
<xsd:element name="toolbar" type="toolbar" minOccurs="0" maxOccurs="1" />
</xsd:sequence>
<xsd:attribute name="intercept-redirects" type="xsd:boolean" />
</xsd:complexType>
<xsd:complexType name="toolbar">
<xsd:attribute name="enabled" type="xsd:boolean" />
<xsd:attribute name="ajax-replace" type="xsd:boolean" />
</xsd:complexType>
</xsd:schema>

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener;
return static function (ContainerConfigurator $container) {
$container->services()
->set('web_profiler.debug_toolbar', WebDebugToolbarListener::class)
->args([
service('twig'),
param('web_profiler.debug_toolbar.intercept_redirects'),
param('web_profiler.debug_toolbar.mode'),
service('router')->ignoreOnInvalid(),
abstract_arg('paths that should be excluded from the AJAX requests shown in the toolbar'),
service('web_profiler.csp.handler'),
service('data_collector.dump')->ignoreOnInvalid(),
abstract_arg('whether to replace toolbar on AJAX requests or not'),
])
->tag('kernel.event_subscriber')
;
};

View File

@@ -0,0 +1,6 @@
JetBrains Mono typeface (https://www.jetbrains.com/lp/mono/) is available
under the SIL Open Font License 1.1 and can be used free of charge, for both
commercial and non-commercial purposes. You do not need to give credit to
JetBrains, although we will appreciate it very much if you do.
Licence: https://github.com/JetBrains/JetBrainsMono/blob/master/OFL.txt

View File

@@ -0,0 +1,35 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% set icon %}
{{ source('@WebProfiler/Icon/ajax.svg') }}
<span class="sf-toolbar-value sf-toolbar-ajax-request-counter">0</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<span class="sf-toolbar-header">
<b class="sf-toolbar-ajax-info"></b>
<b class="sf-toolbar-action">(<a class="sf-toolbar-ajax-clear" href="javascript:void(0);">Clear</a>)</b>
</span>
</div>
<div class="sf-toolbar-info-piece">
<table class="sf-toolbar-ajax-requests">
<thead>
<tr>
<th>#</th>
<th>Profile</th>
<th>Method</th>
<th>Type</th>
<th>Status</th>
<th>URL</th>
<th>Time</th>
</tr>
</thead>
<tbody class="sf-toolbar-ajax-request-list"></tbody>
</table>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: false }) }}
{% endblock %}

View File

@@ -0,0 +1,178 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% if collector.totals.calls > 0 %}
{% set icon %}
{{ source('@WebProfiler/Icon/cache.svg') }}
<span class="sf-toolbar-value">{{ collector.totals.calls }}</span>
<span class="sf-toolbar-info-piece-additional-detail">
<span class="sf-toolbar-label">in</span>
<span class="sf-toolbar-value">{{ '%0.2f'|format(collector.totals.time * 1000) }}</span>
<span class="sf-toolbar-label">ms</span>
</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Cache Calls</b>
<span>{{ collector.totals.calls }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Total time</b>
<span>{{ '%0.2f'|format(collector.totals.time * 1000) }} ms</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Cache hits</b>
<span>{{ collector.totals.hits }} / {{ collector.totals.reads }}{% if collector.totals.hit_read_ratio is not null %} ({{ collector.totals.hit_read_ratio }}%){% endif %}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Cache writes</b>
<span>{{ collector.totals.writes }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label {{ collector.totals.calls == 0 ? 'disabled' }}">
<span class="icon">
{{ source('@WebProfiler/Icon/cache.svg') }}
</span>
<strong>Cache</strong>
</span>
{% endblock %}
{% block panel %}
<h2>Cache</h2>
{% if collector.totals.calls == 0 %}
<div class="empty empty-panel">
<p>No cache calls were made.</p>
</div>
{% else %}
{{ _self.render_metrics(collector.totals, true) }}
<h2>Pools</h2>
<div class="sf-tabs">
{# the empty merge is needed to turn the iterator into an array #}
{% set cache_pools_with_calls = collector.calls|filter(calls => calls|length > 0)|merge([]) %}
{% for name, calls in cache_pools_with_calls %}
<div class="tab {{ calls|length == 0 ? 'disabled' }}">
<h3 class="tab-title">{{ name }} <span class="badge">{{ collector.statistics[name].calls }}</span></h3>
<div class="tab-content">
<h4>Adapter</h4>
<div class="card">
{% if collector.adapters[name] is defined %}
<code>{{ collector.adapters[name] }}</code>
{% else %}
<span class="text-muted">Unable to get the adapter class.</span>
{% endif %}
</div>
{% if calls|length == 0 %}
<div class="empty">
<p>No calls were made for {{ name }} pool.</p>
</div>
{% else %}
<h4>Metrics</h4>
{{ _self.render_metrics(collector.statistics[name]) }}
<h4>Calls</h4>
<table>
<thead>
<tr>
<th>#</th>
<th>Time</th>
<th>Call</th>
<th>Hit</th>
</tr>
</thead>
<tbody>
{% for call in calls %}
<tr>
<td class="font-normal text-small text-muted nowrap">{{ loop.index }}</td>
<td class="nowrap">{{ '%0.2f'|format((call.end - call.start) * 1000) }} ms</td>
<td class="nowrap">{{ call.name }}({{ call.namespace|default('') }})</td>
<td>{{ profiler_dump(call.value.result, maxDepth=2) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
{% if loop.last %}
<div class="tab">
<h3 class="tab-title">Pools without calls <span class="badge">{{ collector.calls|filter(calls => 0 == calls|length)|length }}</span></h3>
<div class="tab-content">
<table>
<thead>
<tr>
<th>Cache pools that received no calls</th>
</tr>
</thead>
<tbody>
{% for cache_pool in collector.calls|filter(calls => 0 == calls|length)|keys|sort %}
<tr><td>{{ cache_pool }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% endblock %}
{% macro render_metrics(pool, is_total = false) %}
<div class="metrics">
<div class="metric">
<span class="value">{{ pool.calls }}</span>
<span class="label">{{ is_total ? 'Total calls' : 'Calls' }}</span>
</div>
<div class="metric">
<span class="value">{{ '%0.2f'|format(pool.time * 1000) }} <span class="unit">ms</span></span>
<span class="label">{{ is_total ? 'Total time' : 'Time' }}</span>
</div>
<div class="metric-divider"></div>
<div class="metric-group">
<div class="metric">
<span class="value">{{ pool.reads }}</span>
<span class="label">{{ is_total ? 'Total reads' : 'Reads' }}</span>
</div>
<div class="metric">
<span class="value">{{ pool.writes }}</span>
<span class="label">{{ is_total ? 'Total writes' : 'Writes' }}</span>
</div>
<div class="metric">
<span class="value">{{ pool.deletes }}</span>
<span class="label">{{ is_total ? 'Total deletes' : 'Deletes' }}</span>
</div>
</div>
<div class="metric-divider"></div>
<div class="metric-group">
<div class="metric">
<span class="value">{{ pool.hits }}</span>
<span class="label">{{ is_total ? 'Total hits' : 'Hits' }}</span>
</div>
<div class="metric">
<span class="value">{{ pool.misses }}</span>
<span class="label">{{ is_total ? 'Total misses' : 'Misses' }}</span>
</div>
<div class="metric">
<span class="value">
{{ pool.hit_read_ratio ?? 0 }} <span class="unit">%</span>
</span>
<span class="label">Hits/reads</span>
</div>
</div>
</div>
{% endmacro %}

View File

@@ -0,0 +1,249 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block menu %}
<span class="label">
<span class="icon">{{ source('@WebProfiler/Icon/command.svg') }}</span>
<strong>Console Command</strong>
</span>
{% endblock %}
{% block panel %}
<h2>
{% set command = collector.command %}
<a href="{{ command.file|file_link(command.line) }}">
{% if command.executor is defined %}
{{ command.executor|abbr_method }}
{% else %}
{{ command.class|abbr_class }}
{% endif %}
</a>
</h2>
<div class="sf-tabs">
<div class="tab">
<h3 class="tab-title">Command</h3>
<div class="tab-content">
<div class="metrics">
<div class="metric">
<span class="value">{{ collector.duration }}</span>
<span class="label">Duration</span>
</div>
<div class="metric">
<span class="value">{{ collector.maxMemoryUsage }}</span>
<span class="label">Peak Memory Usage</span>
</div>
<div class="metric">
<span class="value">{{ collector.verbosityLevel }}</span>
<span class="label">Verbosity Level</span>
</div>
</div>
<div class="metrics">
<div class="metric">
<span class="value">{{ source('@WebProfiler/Icon/' ~ (collector.signalable is not empty ? 'yes' : 'no') ~ '.svg') }}</span>
<span class="label">Signalable</span>
</div>
<div class="metric">
<span class="value">{{ source('@WebProfiler/Icon/' ~ (collector.interactive ? 'yes' : 'no') ~ '.svg') }}</span>
<span class="label">Interactive</span>
</div>
<div class="metric">
<span class="value">{{ source('@WebProfiler/Icon/' ~ (collector.validateInput ? 'yes' : 'no') ~ '.svg') }}</span>
<span class="label">Validate Input</span>
</div>
<div class="metric">
<span class="value">{{ source('@WebProfiler/Icon/' ~ (collector.enabled ? 'yes' : 'no') ~ '.svg') }}</span>
<span class="label">Enabled</span>
</div>
<div class="metric">
<span class="value">{{ source('@WebProfiler/Icon/' ~ (collector.visible ? 'yes' : 'no') ~ '.svg') }}</span>
<span class="label">Visible</span>
</div>
</div>
<h3>Arguments</h3>
{% if collector.arguments is empty %}
<div class="empty">
<p>No arguments were set</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/table.html.twig', { data: collector.arguments, labels: ['Argument', 'Value'], maxDepth: 2 }, with_context=false) }}
{% endif %}
<h3>Options</h3>
{% if collector.options is empty %}
<div class="empty">
<p>No options were set</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/table.html.twig', { data: collector.options, labels: ['Option', 'Value'], maxDepth: 2 }, with_context=false) }}
{% endif %}
{% if collector.interactive %}
<h3>Interactive Inputs</h3>
<p class="help">
The values which have been set interactively.
</p>
{% if collector.interactiveInputs is empty %}
<div class="empty">
<p>No inputs were set</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/table.html.twig', { data: collector.interactiveInputs, labels: ['Input', 'Value'], maxDepth: 2 }, with_context=false) }}
{% endif %}
{% endif %}
<h3>Application inputs</h3>
{% if collector.applicationInputs is empty %}
<div class="empty">
<p>No application inputs are set</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/table.html.twig', { data: collector.applicationInputs, labels: ['Input', 'Value'], maxDepth: 2 }, with_context=false) }}
{% endif %}
</div>
</div>
<div class="tab">
<h3 class="tab-title">Input / Output</h3>
<div class="tab-content">
<table>
<tr>
<td class="font-normal">Input</td>
<td class="font-normal">{{ profiler_dump(collector.input) }}</td>
</tr>
<tr>
<td class="font-normal">Output</td>
<td class="font-normal">{{ profiler_dump(collector.output) }}</td>
</tr>
</table>
</div>
</div>
<div class="tab">
<h3 class="tab-title">Helper Set</h3>
<div class="tab-content">
{% if collector.helperSet is empty %}
<div class="empty">
<p>No helpers</p>
</div>
{% else %}
<table class="{{ class|default('') }}">
<thead>
<tr>
<th scope="col">Helpers</th>
</tr>
</thead>
<tbody>
{% for helper in collector.helperSet|sort %}
<tr>
<td>{{ profiler_dump(helper) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
<div class="tab">
{% set request_collector = profile.collectors.request %}
<h3 class="tab-title">Server Parameters</h3>
<div class="tab-content">
<h3>Server Parameters</h3>
<h4>Defined in .env</h4>
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: request_collector.dotenvvars }, with_context = false) }}
<h4>Defined as regular env variables</h4>
{% set requestserver = [] %}
{% for key, value in request_collector.requestserver|filter((_, key) => key not in request_collector.dotenvvars.keys) %}
{% set requestserver = requestserver|merge({(key): value}) %}
{% endfor %}
{{ include('@WebProfiler/Profiler/table.html.twig', { data: requestserver }, with_context = false) }}
</div>
</div>
{% if collector.signalable is not empty %}
<div class="tab">
<h3 class="tab-title">Signals</h3>
<div class="tab-content">
<h3>Subscribed signals</h3>
{{ collector.signalable|join(', ') }}
<h3>Handled signals</h3>
{% if collector.handledSignals is empty %}
<div class="empty">
<p>No signals handled</p>
</div>
{% else %}
<table>
<thead>
<tr>
<th>Signal</th>
<th>Times handled</th>
<th>Total execution time</th>
<th>Memory peak</th>
</tr>
</thead>
<tbody>
{% for signal, data in collector.handledSignals %}
<tr>
<td>{{ signal }}</td>
<td>{{ data.handled }}</td>
<td>{{ data.duration }} ms</td>
<td>{{ data.memory }} MiB</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
{% endif %}
{% if profile.parent %}
<div class="tab">
<h3 class="tab-title">Parent Command</h3>
<div class="tab-content">
<h3>
<a href="{{ path('_profiler', { token: profile.parent.token }) }}">Return to parent command</a>
<small>(token = {{ profile.parent.token }})</small>
</h3>
{{ profile.parent.url }}
</div>
</div>
{% endif %}
{% if profile.children|length %}
<div class="tab">
<h3 class="tab-title">Sub Commands <span class="badge">{{ profile.children|length }}</span></h3>
<div class="tab-content">
{% for child in profile.children %}
<h3>
{{ child.url }}
<small>(token = <a href="{{ path('_profiler', { token: child.token }) }}">{{ child.token }}</a>)</small>
</h3>
{% endfor %}
</div>
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,300 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block head %}
{{ parent() }}
<style>
.config-symfony-version-lts {
border: 0;
color: var(--color-muted);
font-size: 21px;
line-height: 33px;
}
.config-symfony-version-lts[title] {
text-decoration: none;
}
.config-symfony-version-status-badge {
background-color: var(--badge-background);
border-radius: 4px;
color: var(--badge-color);
display: inline-block;
font-size: 15px;
font-weight: bold;
margin: 10px 0 5px;
padding: 3px 7px;
white-space: nowrap;
}
.config-symfony-version-status-badge.status-success {
background-color: var(--badge-success-background);
color: var(--badge-success-color);
}
.config-symfony-version-status-badge.status-warning {
background-color: var(--badge-warning-background);
color: var(--badge-warning-color);
}
.config-symfony-version-status-badge.status-error {
background-color: var(--badge-danger-background);
color: var(--badge-danger-color);
}
.config-symfony-version-roadmap-link {
display: inline-block;
margin: 10px 5px 5px;
}
.config-symfony-eol {
margin-top: 5px;
}
</style>
{% endblock %}
{% block toolbar %}
{% if 'unknown' == collector.symfonyState %}
{% set block_status = '' %}
{% set symfony_version_status = 'Unable to retrieve information about the Symfony version.' %}
{% elseif 'eol' == collector.symfonyState %}
{% set block_status = 'red' %}
{% set symfony_version_status = 'This Symfony version will no longer receive security fixes.' %}
{% elseif 'eom' == collector.symfonyState %}
{% set block_status = 'yellow' %}
{% set symfony_version_status = 'This Symfony version will only receive security fixes.' %}
{% elseif 'dev' == collector.symfonyState %}
{% set block_status = 'yellow' %}
{% set symfony_version_status = 'This Symfony version is still in the development phase.' %}
{% else %}
{% set block_status = '' %}
{% set symfony_version_status = '' %}
{% endif %}
{% set icon %}
<span class="sf-toolbar-label">
{{ source('@WebProfiler/Icon/symfony.svg') }}
</span>
<span class="sf-toolbar-value">{{ collector.symfonyState is defined ? collector.symfonyversion : 'n/a' }}</span>
{% endset %}
{% set text %}
{% if symfony_version_status %}
<div class="sf-toolbar-info-group">
<div class="sf-toolbar-info-piece">
<span>{{ symfony_version_status }}</span>
</div>
</div>
{% endif %}
<div class="sf-toolbar-info-group">
<div class="sf-toolbar-info-piece">
<b>Profiler token</b>
<span>
{% if profiler_url %}
<a href="{{ profiler_url }}">{{ collector.token }}</a>
{% else %}
{{ collector.token }}
{% endif %}
</span>
</div>
{% if 'n/a' is not same as(collector.env) %}
<div class="sf-toolbar-info-piece">
<b>Environment</b>
<span>{{ collector.env }}</span>
</div>
{% endif %}
{% if 'n/a' is not same as(collector.debug) %}
<div class="sf-toolbar-info-piece">
<b>Debug</b>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.debug ? 'green' : 'red' }}">{{ collector.debug ? 'enabled' : 'disabled' }}</span>
</div>
{% endif %}
</div>
<div class="sf-toolbar-info-group">
<div class="sf-toolbar-info-piece sf-toolbar-info-php">
<b>PHP version</b>
<span{% if collector.phpversionextra %} title="{{ collector.phpversion ~ collector.phpversionextra }}"{% endif %}>
{{ collector.phpversion }}
&nbsp; <a href="{{ path('_profiler_phpinfo') }}">View phpinfo()</a>
</span>
</div>
<div class="sf-toolbar-info-piece sf-toolbar-info-php-ext">
<b>PHP Extensions</b>
{% if collector.hasXdebugInfo %}
<a href="{{ path('_profiler_xdebug') }}" title="View xdebug_info()">
{% endif %}
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.hasXdebug ? 'green' : 'gray' }}">Xdebug {{ collector.hasXdebug ? '✓' : '✗' }}</span>
{% if collector.hasXdebugInfo %}
</a>
{% endif %}
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.hasapcu ? 'green' : 'gray' }}">APCu {{ collector.hasapcu ? '✓' : '✗' }}</span>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.haszendopcache ? 'green' : 'red' }}">OPcache {{ collector.haszendopcache ? '✓' : '✗' }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>PHP SAPI</b>
<span>{{ collector.sapiName }}</span>
</div>
</div>
<div class="sf-toolbar-info-group">
{% if collector.symfonyversion is defined %}
<div class="sf-toolbar-info-piece">
<b>Resources</b>
<span>
<a href="https://symfony.com/doc/{{ collector.symfonyversion }}/index.html" rel="help">
Read Symfony {{ collector.symfonyversion }} Docs
</a>
</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Help</b>
<span>
<a href="https://symfony.com/support">
Symfony Support Channels
</a>
</span>
</div>
{% endif %}
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: true, name: 'config', status: block_status, additional_classes: 'sf-toolbar-block-right' }) }}
{% endblock %}
{% block menu %}
<span class="label label-status-{{ collector.symfonyState == 'eol' ? 'red' : collector.symfonyState in ['eom', 'dev'] ? 'yellow' }}">
<span class="icon">{{ source('@WebProfiler/Icon/config.svg') }}</span>
<strong>Configuration</strong>
</span>
{% endblock %}
{% block panel %}
<h2>Symfony Configuration</h2>
<div class="metrics">
<div class="metric">
<span class="value">
{{ collector.symfonyversion }}
{% if collector.symfonylts %}
<abbr class="config-symfony-version-lts" title="This is a Long-Term Support version">(LTS)</abbr>
{% endif %}
</span>
<span class="label">Symfony version</span>
</div>
{% if 'n/a' is not same as(collector.env) %}
<div class="metric">
<span class="value">{{ collector.env }}</span>
<span class="label">Environment</span>
</div>
{% endif %}
{% if 'n/a' is not same as(collector.debug) %}
<div class="metric">
<span class="value">{{ collector.debug ? 'enabled' : 'disabled' }}</span>
<span class="label">Debug</span>
</div>
{% endif %}
</div>
{% set symfony_status = { dev: 'In Development', stable: 'Maintained', eom: 'Security Fixes Only', eol: 'Unmaintained' } %}
{% set symfony_status_class = { dev: 'warning', stable: 'success', eom: 'warning', eol: 'error' } %}
<div class="metrics">
<div class="metric-group">
<div class="metric">
<span class="value">
<span class="config-symfony-version-status-badge status-{{ symfony_status_class[collector.symfonystate] }}">{{ symfony_status[collector.symfonystate]|upper }}</span>
</span>
<span class="label">Your Symfony version status</span>
</div>
{% if collector.symfonylts %}
<div class="metric">
<span class="value config-symfony-eol">
{{ collector.symfonyeom }}
</span>
<span class="label">Bug fixes {{ collector.symfonystate in ['eom', 'eol'] ? 'ended on' : 'until' }}</span>
</div>
{% endif %}
<div class="metric">
<span class="value config-symfony-eol">
{{ collector.symfonyeol }}
</span>
<span class="label">
{{ collector.symfonylts ? 'Security fixes' : 'Bug fixes and security fixes' }}
{{ 'eol' == collector.symfonystate ? 'ended on' : 'until' }}</span>
</div>
</div>
</div>
<a class="config-symfony-version-roadmap-link" href="https://symfony.com/releases/{{ collector.symfonyminorversion }}">View Symfony {{ collector.symfonyversion }} release details</a>
<h2>PHP Configuration</h2>
<div class="metrics">
<div class="metric">
<span class="value">{{ collector.phpversion }}{% if collector.phpversionextra %} <span class="unit">{{ collector.phpversionextra }}</span>{% endif %}</span>
<span class="label">PHP version</span>
</div>
<div class="metric">
<span class="value">{{ collector.phparchitecture }} <span class="unit">bits</span></span>
<span class="label">Architecture</span>
</div>
<div class="metric">
<span class="value">{{ collector.phpintllocale }}</span>
<span class="label">Intl locale</span>
</div>
<div class="metric">
<span class="value">{{ collector.phptimezone }}</span>
<span class="label">Timezone</span>
</div>
</div>
<div class="metrics">
<div class="metric-group">
<div class="metric">
<span class="value value-is-icon {{ not collector.haszendopcache ? 'value-shows-no-color' }}" title="{{ collector.zendopcachestatus|default('') }}">{{ source('@WebProfiler/Icon/' ~ (collector.haszendopcache ? 'yes' : 'no') ~ '.svg') }}</span>
<span class="label">OPcache</span>
</div>
<div class="metric">
<span class="value value-is-icon {{ not collector.hasapcu ? 'value-shows-no-color' }}" title="{{ collector.apcustatus|default('') }}">{{ source('@WebProfiler/Icon/' ~ (collector.hasapcu ? 'yes' : 'no') ~ '.svg') }}</span>
<span class="label">APCu</span>
</div>
<div class="metric">
<span class="value value-is-icon {{ not collector.hasxdebug ? 'value-shows-no-color' }}" title="{{ collector.xdebugstatus|default('') }}">{{ source('@WebProfiler/Icon/' ~ (collector.hasxdebug ? 'yes' : 'no') ~ '.svg') }}</span>
<span class="label">Xdebug</span>
</div>
</div>
</div>
<p>
<a href="{{ path('_profiler_phpinfo') }}">View full PHP configuration</a>
</p>
{% if collector.bundles %}
<h2>Enabled Bundles <small>({{ collector.bundles|length }})</small></h2>
<table>
<thead>
<tr>
<th class="key">Name</th>
<th>Class</th>
</tr>
</thead>
<tbody>
{% for name in collector.bundles|keys|sort %}
<tr>
<th scope="row" class="font-normal">{{ name }}</th>
<td class="font-normal">{{ profiler_dump(collector.bundles[name]) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,126 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block menu %}
<span class="label">
<span class="icon">{{ source('@WebProfiler/Icon/event.svg') }}</span>
<strong>Events</strong>
</span>
{% endblock %}
{% block panel %}
<h2>Dispatched Events</h2>
<div class="sf-tabs">
{% for dispatcherName, dispatcherData in collector.data %}
<div class="tab">
<h3 class="tab-title">{{ dispatcherName }}</h3>
<div class="tab-content">
{% if dispatcherData['called_listeners'] is empty %}
<div class="empty empty-panel">
<p>No events have been recorded. Check that debugging is enabled in the kernel.</p>
</div>
{% else %}
<div class="sf-tabs">
<div class="tab">
<h3 class="tab-title">Called Listeners <span class="badge">{{ dispatcherData['called_listeners']|length }}</span></h3>
<div class="tab-content">
{{ _self.render_table(dispatcherData['called_listeners']) }}
</div>
</div>
<div class="tab">
<h3 class="tab-title">Not Called Listeners <span class="badge">{{ dispatcherData['not_called_listeners']|length }}</span></h3>
<div class="tab-content">
{% if dispatcherData['not_called_listeners'] is empty %}
<div class="empty">
<p>
<strong>There are no uncalled listeners</strong>.
</p>
<p>
All listeners were called or an error occurred
when trying to collect uncalled listeners (in which case check the
logs to get more information).
</p>
</div>
{% else %}
{{ _self.render_table(dispatcherData['not_called_listeners']) }}
{% endif %}
</div>
</div>
<div class="tab">
<h3 class="tab-title">Orphaned Events <span class="badge">{{ dispatcherData['orphaned_events']|length }}</span></h3>
<div class="tab-content">
{% if dispatcherData['orphaned_events'] is empty %}
<div class="empty">
<p>
<strong>There are no orphaned events</strong>.
</p>
<p>
All dispatched events were handled or an error occurred
when trying to collect orphaned events (in which case check the
logs to get more information).
</p>
</div>
{% else %}
<table>
<thead>
<tr>
<th>Event</th>
</tr>
</thead>
<tbody>
{% for event in dispatcherData['orphaned_events'] %}
<tr>
<td class="font-normal">{{ event }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% endblock %}
{% macro render_table(listeners) %}
<table>
<thead>
<tr>
<th class="text-right">Priority</th>
<th>Listener</th>
</tr>
</thead>
{% set previous_event = (listeners|first).event %}
{% for listener in listeners %}
{% if loop.first or listener.event != previous_event %}
{% if not loop.first %}
</tbody>
{% endif %}
<tbody>
<tr>
<th colspan="2" class="colored font-normal">{{ listener.event }}</th>
</tr>
{% set previous_event = listener.event %}
{% endif %}
<tr>
<td class="text-right nowrap">{{ listener.priority|default('-') }}</td>
<td class="font-normal">{{ profiler_dump(listener.stub) }}</td>
</tr>
{% if loop.last %}
</tbody>
{% endif %}
{% endfor %}
</table>
{% endmacro %}

View File

@@ -0,0 +1,32 @@
.container {
max-width: none;
margin: 0;
padding: 0;
}
.container .container {
padding: 0;
}
.exception-summary {
background: var(--base-0);
border: var(--border);
box-shadow: 0 0 1px rgba(128, 128, 128, .2);
margin: 1em 0;
padding: 10px;
}
.exception-summary.exception-without-message {
display: none;
}
.exception-message {
color: var(--color-error);
}
.exception-metadata,
.exception-illustration {
display: none;
}
.exception-message-wrapper .container {
min-height: unset;
}

View File

@@ -0,0 +1,45 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block head %}
{% if collector.hasexception %}
<style>
{{ render(controller('web_profiler.controller.exception_panel::stylesheet', { token: token })) }}
{{ include('@WebProfiler/Collector/exception.css.twig') }}
</style>
{% endif %}
{{ parent() }}
{% endblock %}
{% block menu %}
<span class="label {{ collector.hasexception ? 'label-status-error' : 'disabled' }}">
<span class="icon">{{ source('@WebProfiler/Icon/exception.svg') }}</span>
<strong>Exception</strong>
{% if collector.hasexception %}
<span class="count">
<span>1</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
{# these styles are needed to override some styles from Exception page, which wasn't
updated yet to the new style of the Symfony Profiler #}
<style>
.tab-navigation li { background: none; border: 0; font-size: 14px; }
.tab-navigation li.active { border-radius: 6px; }
.tab-navigation li.active .badge { background-color: var(--selected-badge-background); color: var(--selected-badge-color); }
</style>
<h2>Exceptions</h2>
{% if not collector.hasexception %}
<div class="empty empty-panel">
<p>No exception was thrown and caught.</p>
</div>
{% else %}
<div class="sf-reset">
{{ render(controller('web_profiler.controller.exception_panel::body', { token: token })) }}
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,706 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% if collector.data.nb_errors > 0 or collector.data.forms|length %}
{% set status_color = collector.data.nb_errors ? 'red' %}
{% set icon %}
{{ source('@WebProfiler/Icon/form.svg') }}
<span class="sf-toolbar-value">
{{ collector.data.nb_errors ?: collector.data.forms|length }}
</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Number of forms</b>
<span class="sf-toolbar-status">{{ collector.data.forms|length }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Number of errors</b>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.data.nb_errors > 0 ? 'red' }}">{{ collector.data.nb_errors }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label label-status-{{ collector.data.nb_errors ? 'error' }} {{ collector.data.forms is empty ? 'disabled' }}">
<span class="icon">{{ source('@WebProfiler/Icon/form.svg') }}</span>
<strong>Forms</strong>
{% if collector.data.nb_errors > 0 %}
<span class="count">
<span>{{ collector.data.nb_errors }}</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block stylesheets %}
{{ parent() }}
<style>
.form-type-class {
font-size: var(--font-size-body);
display: flex;
margin: 0 0 15px;
}
.form-type-class-label {
margin-right: 4px;
}
.form-type-class pre.sf-dump {
font-family: var(--font-family-system) !important;
font-size: var(--font-size-body) !important;
margin-left: 5px;
}
.form-type-class .sf-dump .sf-dump-str {
color: var(--color-link) !important;
text-decoration: underline;
}
.form-type-class .sf-dump .sf-dump-str:hover {
text-decoration: none;
}
#tree-menu {
float: left;
padding-right: 10px;
width: 220px;
}
#tree-menu ul {
list-style: none;
margin: 0;
padding-left: 0;
}
#tree-menu li {
margin: 0;
padding: 0;
width: 100%;
}
#tree-menu .empty {
border: 0;
box-shadow: none !important;
padding: 0;
}
#tree-details-container {
border-left: 1px solid var(--table-border-color);
margin-left: 230px;
padding-left: 20px;
}
.tree-details {
padding-bottom: 40px;
}
.tree-details h3 {
font-size: 18px;
position: relative;
}
.toggle-icon {
display: inline-block;
}
.closed .toggle-icon, .closed.toggle-icon {
}
.tree .tree-inner {
cursor: pointer;
padding: 5px 7px 5px 22px;
position: relative;
overflow: hidden;
text-overflow: ellipsis;
}
.tree .toggle-button {
width: 16px;
height: 16px;
margin-left: -18px;
display: inline-grid;
place-content: center;
background: none;
border: none;
transition: transform 200ms;
}
.tree .toggle-button.closed svg {
transform: rotate(-90deg);
}
.tree .toggle-button svg {
transform: rotate(0deg);
width: 16px;
height: 16px;
}
.tree .toggle-icon.empty {
width: 5px;
height: 5px;
position: absolute;
top: 50%;
margin-top: -2px;
margin-left: -13px;
}
.tree .tree-inner {
border-radius: 4px;
}
.tree ul ul .tree-inner {
padding-left: 32px;
}
.tree ul ul ul .tree-inner {
padding-left: 48px;
}
.tree ul ul ul ul .tree-inner {
padding-left: 64px;
}
.tree ul ul ul ul ul .tree-inner {
padding-left: 72px;
}
.tree .tree-inner:hover {
background: var(--gray-200);
}
.tree .tree-inner.active, .tree .tree-inner.active:hover {
background: var(--tree-active-background);
font-weight: bold;
}
.tree-details .toggle-icon {
width: 16px;
height: 16px;
/* vertically center the button */
position: absolute;
top: 50%;
margin-top: -9px;
margin-left: 6px;
}
.badge-error {
float: right;
background: var(--background-error);
color: #FFF;
padding: 1px 4px;
font-size: 10px;
font-weight: bold;
vertical-align: middle;
}
.has-error {
color: var(--color-error);
}
.errors h3 {
color: var(--color-error);
}
.errors th {
background: var(--background-error);
color: #FFF;
}
.errors .toggle-icon {
background-color: var(--background-error);
}
h3 a, h3 a:hover, h3 a:focus {
color: inherit;
text-decoration: inherit;
}
</style>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script>
window.addEventListener('DOMContentLoaded', () => {
new SymfonyProfilerFormPanel();
});
class SymfonyProfilerFormPanel {
#activeTreeItem;
#activeTreePanel;
#storage;
#storageKey = 'sf_toggle_data';
#togglerStates = {};
constructor() {
this.#storage = sessionStorage;
this.#initTrees();
this.#initTogglerButtons();
}
#initTrees() {
const treeItems = document.querySelectorAll('.tree .tree-inner');
treeItems.forEach((treeItem) => {
var targetId = treeItem.getAttribute('data-tab-target-id');
const target = document.getElementById(targetId);
if (!target) {
throw `Tab target ${targetId} does not exist`;
}
treeItem.addEventListener('click', (e) => {
this.#selectTreeItem(treeItem);
e.stopPropagation();
return false;
});
target.classList.add('hidden');
});
if (treeItems.length > 0) {
this.#selectTreeItem(treeItems[0]);
}
};
#selectTreeItem(treeItem) {
const treePanelId = treeItem.getAttribute('data-tab-target-id');
const treePanel = document.getElementById(treePanelId);
if (!treePanel) {
throw `The tree panel ${treePanelId} does not exist`;
}
if (this.#activeTreeItem) {
this.#activeTreeItem.classList.remove('active');
}
if (this.#activeTreePanel) {
this.#activeTreePanel.classList.add('hidden');
}
treeItem.classList.add('active');
treePanel.classList.remove('hidden');
this.#activeTreeItem = treeItem;
this.#activeTreePanel = treePanel;
}
#initTogglerButtons() {
this.#togglerStates = this.#getTogglerStates();
if (!this.#isObject(this.#togglerStates)) {
this.#togglerStates = {};
}
const buttons = document.querySelectorAll('.toggle-button');
buttons.forEach((button) => {
const targetId = button.getAttribute('data-toggle-target-id');
const target = document.getElementById(targetId);
if (!target) {
throw `Toggle target ${targetId} does not exist`;
}
// correct the initial state of the button
if (target.classList.contains('hidden')) {
button.classList.add('closed');
}
button.addEventListener('click', (e) => {
this.#toggleToggler(button);
e.stopPropagation();
return false;
});
if (this.#togglerStates.hasOwnProperty(targetId)) {
// open or collapse based on stored data
if (0 === this.#togglerStates[targetId]) {
this.#collapseToggler(button);
} else {
this.#expandToggler(button);
}
}
});
};
#isTogglerCollapsed(button) {
return button.classList.contains('closed');
}
#isTogglerExpanded(button) {
return !this.#isTogglerCollapsed(button);
}
#expandToggler(button) {
const targetId = button.getAttribute('data-toggle-target-id');
const target = document.getElementById(targetId);
if (!target) {
throw "Toggle target " + targetId + " does not exist";
}
if (this.#isTogglerCollapsed(button)) {
button.classList.remove('closed');
target.classList.remove('hidden');
this.#togglerStates[targetId] = 1;
this.#saveTogglerStates();
}
}
#collapseToggler(button) {
const targetId = button.getAttribute('data-toggle-target-id');
const target = document.getElementById(targetId);
if (!target) {
throw `Toggle target ${targetId} does not exist`;
}
if (this.#isTogglerExpanded(button)) {
button.classList.add('closed');
target.classList.add('hidden');
this.#togglerStates[targetId] = 0;
this.#saveTogglerStates();
}
}
#toggleToggler(button) {
if (button.classList.contains('closed')) {
this.#expandToggler(button);
} else {
this.#collapseToggler(button);
}
}
#saveTogglerStates() {
this.#storage.setItem(this.#storageKey, JSON.stringify(this.#togglerStates));
}
#getTogglerStates() {
const data = this.#storage.getItem(this.#storageKey);
if (null !== data) {
try {
return JSON.parse(data);
} catch(e) {
}
}
return {};
}
#isObject(variable) {
return null !== variable && 'object' === typeof variable && !Array.isArray(variable);
}
}
</script>
{% endblock %}
{% block panel %}
<h2>Forms</h2>
{% if collector.data.forms|length %}
<div id="tree-menu" class="tree">
<ul>
{% for formName, formData in collector.data.forms %}
{{ _self.form_tree_entry(formName, formData, true) }}
{% endfor %}
</ul>
</div>
<div id="tree-details-container">
{% for formName, formData in collector.data.forms %}
{{ _self.form_tree_details(formName, formData, collector.data.forms_by_hash, loop.first) }}
{% endfor %}
</div>
{% else %}
<div class="empty empty-panel">
<p>No forms were submitted.</p>
</div>
{% endif %}
{% endblock %}
{% macro form_tree_entry(name, data, is_root) %}
{% set has_error = data.errors is defined and data.errors|length > 0 %}
<li>
<div class="tree-inner" data-tab-target-id="{{ data.id }}-details" title="{{ name|default('(no name)') }}">
{% if has_error %}
<div class="badge-error">{{ data.errors|length }}</div>
{% endif %}
{% if data.children is not empty %}
<button class="toggle-button" data-toggle-target-id="{{ data.id }}-children">
{{ source('@WebProfiler/Icon/chevron-down.svg') }}
</button>
{% else %}
<div class="toggle-icon empty"></div>
{% endif %}
<span {% if has_error or data.has_children_error|default(false) %}class="has-error"{% endif %}>
{{ name|default('(no name)') }}
</span>
</div>
{% if data.children is not empty %}
<ul id="{{ data.id }}-children" {% if not is_root and not data.has_children_error|default(false) %}class="hidden"{% endif %}>
{% for childName, childData in data.children %}
{{ _self.form_tree_entry(childName, childData, false) }}
{% endfor %}
</ul>
{% endif %}
</li>
{% endmacro %}
{% macro form_tree_details(name, data, forms_by_hash, show) %}
<div class="tree-details{% if not show|default(false) %} hidden{% endif %}" {% if data.id is defined %}id="{{ data.id }}-details"{% endif %}>
<h2>{{ name|default('(no name)') }}</h2>
{% if data.type_class is defined %}
<div class="form-type-class">
<span class="form-type-class-label">Form type:</span>
{{ profiler_dump(data.type_class) }}
</div>
{% endif %}
{% set form_has_errors = data.errors ?? [] is not empty %}
<div class="sf-tabs">
<div class="tab {{ form_has_errors ? 'active' : 'disabled' }}">
<h3 class="tab-title">Errors</h3>
<div class="tab-content">
{{ _self.render_form_errors(data) }}
</div>
</div>
<div class="tab {{ not form_has_errors ? 'active' }}">
<h3 class="tab-title">Default Data</h3>
<div class="tab-content">
{{ _self.render_form_default_data(data) }}
</div>
</div>
<div class="tab {{ (data.submitted_data ?? []) is empty ? 'disabled' }}">
<h3 class="tab-title">Submitted Data</h3>
<div class="tab-content">
{{ _self.render_form_submitted_data(data) }}
</div>
</div>
<div class="tab {{ (data.passed_options ?? []) is empty ? 'disabled' }}">
<h3 class="tab-title">Passed Options</h3>
<div class="tab-content">
{{ _self.render_form_passed_options(data) }}
</div>
</div>
<div class="tab {{ (data.resolved_options ?? []) is empty ? 'disabled' }}">
<h3 class="tab-title">Resolved Options</h3>
<div class="tab-content">
{{ _self.render_form_resolved_options(data) }}
</div>
</div>
<div class="tab {{ (data.view_vars ?? []) is empty ? 'disabled' }}">
<h3 class="tab-title">View Vars</h3>
<div class="tab-content">
{{ _self.render_form_view_variables(data) }}
</div>
</div>
</div>
</div>
{% for childName, childData in data.children %}
{{ _self.form_tree_details(childName, childData, forms_by_hash) }}
{% endfor %}
{% endmacro %}
{% macro render_form_errors(data) %}
{% if data.errors is defined and data.errors|length > 0 %}
<div class="errors">
<table id="{{ data.id }}-errors">
<thead>
<tr>
<th>Message</th>
<th>Origin</th>
<th>Cause</th>
</tr>
</thead>
<tbody>
{% for error in data.errors %}
<tr>
<td>{{ error.message }}</td>
<td>
{% if error.origin is empty %}
<em>This form.</em>
{% elseif forms_by_hash[error.origin] is not defined %}
<em>Unknown.</em>
{% else %}
{{ forms_by_hash[error.origin].name }}
{% endif %}
</td>
<td>
{% if error.trace %}
<span class="newline">Caused by:</span>
{% for stacked in error.trace %}
{{ profiler_dump(stacked) }}
{% endfor %}
{% else %}
<em>Unknown.</em>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty">
<p>This form has no errors.</p>
</div>
{% endif %}
{% endmacro %}
{% macro render_form_default_data(data) %}
{% if data.default_data is defined %}
<table>
<thead>
<tr>
<th style="width: 180px">Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<th class="font-normal" scope="row">Model Format</th>
<td>
{% if data.default_data.model is defined %}
{{ profiler_dump(data.default_data.seek('model')) }}
{% else %}
<em class="font-normal text-muted">same as normalized format</em>
{% endif %}
</td>
</tr>
<tr>
<th class="font-normal" scope="row">Normalized Format</th>
<td>{{ profiler_dump(data.default_data.seek('norm')) }}</td>
</tr>
<tr>
<th class="font-normal" scope="row">View Format</th>
<td>
{% if data.default_data.view is defined %}
{{ profiler_dump(data.default_data.seek('view')) }}
{% else %}
<em class="font-normal text-muted">same as normalized format</em>
{% endif %}
</td>
</tr>
</tbody>
</table>
{% else %}
<div class="empty">
<p>This form has default data defined.</p>
</div>
{% endif %}
{% endmacro %}
{% macro render_form_submitted_data(data) %}
{% if data.submitted_data.norm is defined %}
<table>
<thead>
<tr>
<th style="width: 180px">Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<th class="font-normal" scope="row">View Format</th>
<td>
{% if data.submitted_data.view is defined %}
{{ profiler_dump(data.submitted_data.seek('view')) }}
{% else %}
<em class="font-normal text-muted">same as normalized format</em>
{% endif %}
</td>
</tr>
<tr>
<th class="font-normal" scope="row">Normalized Format</th>
<td>{{ profiler_dump(data.submitted_data.seek('norm')) }}</td>
</tr>
<tr>
<th class="font-normal" scope="row">Model Format</th>
<td>
{% if data.submitted_data.model is defined %}
{{ profiler_dump(data.submitted_data.seek('model')) }}
{% else %}
<em class="font-normal text-muted">same as normalized format</em>
{% endif %}
</td>
</tr>
</tbody>
</table>
{% else %}
<div class="empty">
<p>This form was not submitted.</p>
</div>
{% endif %}
{% endmacro %}
{% macro render_form_passed_options(data) %}
{% if data.passed_options ?? [] is not empty %}
<table>
<thead>
<tr>
<th style="width: 180px">Option</th>
<th>Passed Value</th>
<th>Resolved Value</th>
</tr>
</thead>
<tbody>
{% for option, value in data.passed_options %}
<tr>
<th>{{ option }}</th>
<td>{{ profiler_dump(value) }}</td>
<td>
{# values can be stubs #}
{% set option_value = (value.value is defined) ? value.value : value %}
{% set resolved_option_value = (data.resolved_options[option].value is defined)
? data.resolved_options[option].value
: data.resolved_options[option] %}
{% if resolved_option_value == option_value %}
<em class="font-normal text-muted">same as passed value</em>
{% else %}
{{ profiler_dump(data.resolved_options.seek(option)) }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty">
<p>No options were passed when constructing this form.</p>
</div>
{% endif %}
{% endmacro %}
{% macro render_form_resolved_options(data) %}
<table>
<thead>
<tr>
<th style="width: 180px">Option</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for option, value in data.resolved_options ?? [] %}
<tr>
<th scope="row">{{ option }}</th>
<td>{{ profiler_dump(value) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endmacro %}
{% macro render_form_view_variables(data) %}
<table>
<thead>
<tr>
<th style="width: 180px">Variable</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for variable, value in data.view_vars ?? [] %}
<tr>
<th scope="row">{{ variable }}</th>
<td>{{ profiler_dump(value) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endmacro %}

View File

@@ -0,0 +1,175 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block head %}
{{ parent() }}
<style>
.sf-profiler-httpclient-requests thead th {
vertical-align: top;
}
.sf-profiler-httpclient-requests .http-method {
border: 1px solid var(--header-status-request-method-color);
border-radius: 5px;
color: var(--header-status-request-method-color);
display: inline-block;
font-weight: 500;
line-height: 1;
margin-right: 6px;
padding: 2px 4px;
text-align: center;
white-space: nowrap;
}
.sf-profiler-httpclient-requests .status-response-status-code {
background: var(--gray-600);
border-radius: 4px;
color: var(--white);
display: inline-block;
font-size: 12px;
font-weight: bold;
margin-bottom: 2px;
padding: 1px 3px;
}
.sf-profiler-httpclient-requests .status-response-status-code.status-success { background: var(--header-success-status-code-background); color: var(--header-success-status-code-color); }
.sf-profiler-httpclient-requests .status-response-status-code.status-warning { background: var(--header-warning-status-code-background); color: var(--header-warning-status-code-color); }
.sf-profiler-httpclient-requests .status-response-status-code.status-error { background: var(--header-error-status-code-background); color: var(--header-error-status-code-color); }
</style>
{% endblock %}
{% block toolbar %}
{% if collector.requestCount %}
{% set icon %}
{{ source('@WebProfiler/Icon/http-client.svg') }}
{% set status_color = '' %}
<span class="sf-toolbar-value">{{ collector.requestCount }}</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Total requests</b>
<span>{{ collector.requestCount }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>HTTP errors</b>
<span class="sf-toolbar-status {{ collector.errorCount > 0 ? 'sf-toolbar-status-red' }}">{{ collector.errorCount }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label {{ collector.requestCount == 0 ? 'disabled' }}">
<span class="icon">{{ source('@WebProfiler/Icon/http-client.svg') }}</span>
<strong>HTTP Client</strong>
{% if collector.requestCount %}
<span class="count">
{{ collector.requestCount }}
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
<h2>HTTP Client</h2>
{% if collector.requestCount == 0 %}
<div class="empty empty-panel">
<p>No HTTP requests were made.</p>
</div>
{% else %}
<div class="metrics">
<div class="metric">
<span class="value">{{ collector.requestCount }}</span>
<span class="label">Total requests</span>
</div>
<div class="metric">
<span class="value">{{ collector.errorCount }}</span>
<span class="label">HTTP errors</span>
</div>
</div>
<h2>Clients</h2>
<div class="sf-tabs">
{% for name, client in collector.clients %}
<div class="tab {{ client.traces|length == 0 ? 'disabled' }}">
<h3 class="tab-title">{{ name }} <span class="badge">{{ client.traces|length }}</span></h3>
<div class="tab-content">
{% if client.traces|length == 0 %}
<div class="empty">
<p>No requests were made with the "{{ name }}" service.</p>
</div>
{% else %}
<h4>Requests</h4>
{% for trace in client.traces %}
{% set profiler_token = '' %}
{% set profiler_link = '' %}
{% if trace.info.response_headers is defined %}
{% for header in trace.info.response_headers %}
{% if header matches '/^x-debug-token: .*$/i' %}
{% set profiler_token = (header.getValue | slice('x-debug-token: ' | length)) %}
{% endif %}
{% if header matches '/^x-debug-token-link: .*$/i' %}
{% set profiler_link = (header.getValue | slice('x-debug-token-link: ' | length)) %}
{% endif %}
{% endfor %}
{% endif %}
<table class="sf-profiler-httpclient-requests">
<thead>
<tr>
<th>
<span class="http-method">{{ trace.method }}</span>
</th>
<th class="full-width">
{{ trace.url }}
</th>
{% if profiler_token and profiler_link %}
<th>
Profile
</th>
{% endif %}
{% if trace.curlCommand is defined and trace.curlCommand %}
<th>
<button class="btn btn-sm hidden" title="Copy as cURL" data-clipboard-text="{{ trace.curlCommand }}">Copy as cURL</button>
</th>
{% endif %}
</tr>
</thead>
<tbody>
{% if trace.options is not empty %}
<tr>
<th class="font-normal">Request options</th>
<td>{{ profiler_dump(trace.options, maxDepth=1) }}</td>
</tr>
{% endif %}
<tr>
<th class="font-normal">Response</th>
<td{% if trace.curlCommand is defined and trace.curlCommand %} colspan="2"{% endif %}>
{% if trace.http_code >= 500 %}
{% set responseStatus = 'error' %}
{% elseif trace.http_code >= 400 %}
{% set responseStatus = 'warning' %}
{% else %}
{% set responseStatus = 'success' %}
{% endif %}
<span class="font-normal status-response-status-code status-{{ responseStatus }}">
{{ trace.http_code }}
</span>
{{ profiler_dump(trace.info, maxDepth=1) }}
</td>
{% if profiler_token and profiler_link %}
<td>
<span><a href="{{ profiler_link }}" target="_blank">{{ profiler_token }}</a></span>
</td>
{% endif %}
</tr>
</tbody>
</table>
{% endfor %}
{% endif %}
</div>
</div>
{% endfor %}
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,581 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block stylesheets %}
{{ parent() }}
<style>
:root {
--log-filter-active-num-color: #2563EB;
--log-timestamp-color: #555;
}
.theme-dark {
--log-filter-active-num-color: #2563EB;
--log-timestamp-color: #ccc;
}
.log-filters {
display: flex;
flex-wrap: wrap;
}
.log-filters .log-filter {
flex-shrink: 0;
margin-right: 15px;
position: relative;
}
.log-filters .log-filter summary {
align-items: center;
background: var(--button-background);
border-radius: 6px;
border: 1px solid var(--button-border-color);
box-shadow: var(--button-box-shadow);
color: var(--button-color);
cursor: pointer;
display: flex;
font-size: 13px;
font-weight: 500;
padding: 4px 8px;
white-space: nowrap;
}
.log-filters .log-filter summary:active {
box-shadow: none;
transform: translateY(1px);
}
.log-filters .log-filter summary .icon {
height: 18px;
width: 18px;
margin: 0 7px 0 0;
}
.log-filters .log-filter summary svg {
height: 18px;
width: 18px;
opacity: 0.7;
}
.log-filters .log-filter summary svg {
stroke-width: 2;
}
.log-filters .log-filter summary .filter-active-num {
color: var(--log-filter-active-num-color);
font-weight: bold;
padding: 0 1px;
}
.log-filter .tab-navigation {
position: relative;
}
.log-filter .tab-navigation input[type="radio"] {
position: absolute;
pointer-events: none;
opacity: 0;
}
.tab-navigation input[type="radio"]:checked + .tab-control {
background-color: var(--tab-active-background);
border-radius: 6px;
box-shadow: inset 0 0 0 1.5px var(--tab-active-border-color);
color: var(--tab-active-color);
position: relative;
z-index: 1;
}
.theme-dark .tab-navigation input[type="radio"]:checked + .tab-control {
box-shadow: inset 0 0 0 1px var(--tab-border-color);
}
.tab-navigation input[type="radio"]:focus-visible + .tab-control {
outline: 1px solid var(--color-link);
}
.tab-navigation input[type="radio"]:checked + .tab-control + input[type="radio"] + .tab-control:before{
width: 0;
}
.log-filters .log-filter .log-filter-content {
background: var(--base-0);
border: 1px solid var(--table-border-color);
border-radius: 6px;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
padding: 15px;
position: absolute;
left: 0;
top: 32px;
max-width: 400px;
min-width: 200px;
z-index: 9999;
}
.log-filters .log-filter .log-filter-content .log-filter-option {
align-items: center;
display: flex;
}
.log-filter .filter-select-all-or-none {
margin-bottom: 10px;
}
.log-filter .filter-select-all-or-none button + button {
margin-left: 15px;
}
.log-filters .log-filter .log-filter-content .log-filter-option + .log-filter-option {
margin: 7px 0 0;
}
.log-filters .log-filter .log-filter-content .log-filter-option label {
cursor: pointer;
flex: 1;
padding-left: 10px;
}
table.logs {
border-bottom-width: 0;
border-collapse: collapse;
}
table.logs tr + tr td {
border-width: 1px 0 0;
}
table.logs .metadata {
display: block;
font-size: 12px;
}
.theme-dark tr.status-error td,
.theme-dark tr.status-warning td { border-bottom: unset; border-top: unset; }
table.logs .log-timestamp {
color: var(--log-timestamp-color);
}
table.logs .log-metadata {
margin: 8px 0 0;
}
table.logs .log-metadata > span {
display: inline-block;
}
table.logs .log-metadata > span + span {
margin-left: 10px;
}
table.logs .log-metadata .log-channel {
color: var(--base-1);
font-size: 13px;
font-weight: bold;
}
table.logs .log-metadata .badge {
background: var(--badge-light-background);
color: var(--badge-light-color);
}
table.logs .log-metadata .log-num-occurrences {
color: var(--color-muted);
font-size: 13px;
}
table.logs .log-metadata .context {
background: var(--code-block-background);
border-radius: 4px;
padding: 5px;
}
table.logs .log-metadata .context + .context {
margin-top: 10px;
}
.log-type-badge {
background: var(--badge-light-background);
box-shadow: none;
color: var(--badge-light-color);
display: inline-block;
font-family: var(--font-family-system);
margin-top: 5px;
}
.log-type-badge.badge-deprecation,
.log-type-badge.badge-warning {
background: var(--badge-warning-background);
color: var(--badge-warning-color);
}
.log-type-badge.badge-error {
background: var(--badge-danger-background);
color: var(--badge-danger-color);
}
.log-type-badge.badge-silenced {
background: #EDE9FE;
color: #6D28D9;
}
.theme-dark .log-type-badge.badge-silenced {
background: #5B21B6;
color: #EDE9FE;
}
tr.log-status-warning > td:first-child,
tr.log-status-error > td:first-child,
tr.log-status-silenced > td:first-child {
position: relative;
}
tr.log-status-warning > td:first-child:before,
tr.log-status-error > td:first-child:before,
tr.log-status-silenced > td:first-child:before {
background: transparent;
border-radius: 0;
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
}
tr.log-status-warning > td:first-child:before {
background: var(--yellow-400);
}
tr.log-status-error > td:first-child:before {
background: var(--red-400);
}
tr.log-status-silenced > td:first-child:before {
background: #a78bfa;
}
.container-compilation-logs {
background: var(--table-background);
border: 1px solid var(--base-2);
border-radius: 6px;
margin-top: 30px;
padding: 15px;
}
.container-compilation-logs summary {
cursor: pointer;
}
.container-compilation-logs summary h4 {
margin: 0 0 5px;
}
.container-compilation-logs summary p {
margin: 0;
}
</style>
{% endblock %}
{% block javascripts %}
<script>
window.addEventListener('DOMContentLoaded', () => {
new SymfonyProfilerLoggerPanel();
});
class SymfonyProfilerLoggerPanel {
constructor() {
this.#initializeLogsTable();
}
#initializeLogsTable() {
this.#updateLogsTable();
document.querySelectorAll('.log-filter input').forEach((input) => {
input.addEventListener('change', () => { this.#updateLogsTable(); });
});
document.querySelectorAll('.filter-select-all-or-none button').forEach((link) => {
link.addEventListener('click', () => {
const selectAll = link.classList.contains('select-all');
link.closest('.log-filter-content').querySelectorAll('input').forEach((input) => {
input.checked = selectAll;
});
this.#updateLogsTable();
});
});
document.body.addEventListener('click', (event) => {
document.querySelectorAll('details.log-filter').forEach((filterElement) => {
if (!filterElement.contains(event.target) && filterElement.open) {
filterElement.open = false;
}
});
});
}
#updateLogsTable() {
const logs = document.querySelector('table.logs');
if (null === logs) {
return;
}
const selectedType = document.querySelector('#log-filter-type input:checked').value;
const priorities = document.querySelectorAll('#log-filter-priority input');
const allPriorities = Array.from(priorities).map((input) => input.value);
const selectedPriorities = Array.from(priorities).filter((input) => input.checked).map((input) => input.value);
const channels = document.querySelectorAll('#log-filter-channel input');
const selectedChannels = Array.from(channels).filter((input) => input.checked).map((input) => input.value);
/* hide rows that don't match the current filters */
let numVisibleRows = 0;
logs.querySelectorAll('tbody tr').forEach((row) => {
if ('all' !== selectedType && selectedType !== row.getAttribute('data-type')) {
row.style.display = 'none';
return;
}
const priority = row.getAttribute('data-priority');
if (false === selectedPriorities.includes(priority) && true === allPriorities.includes(priority)) {
row.style.display = 'none';
return;
}
if ('' !== row.getAttribute('data-channel') && false === selectedChannels.includes(row.getAttribute('data-channel'))) {
row.style.display = 'none';
return;
}
row.style.display = 'table-row';
numVisibleRows++;
});
document.querySelector('table.logs').style.display = 0 === numVisibleRows ? 'none' : 'table';
document.querySelector('.no-logs-message').style.display = 0 === numVisibleRows ? 'block' : 'none';
/* update the selected totals of all filters */
document.querySelector('#log-filter-priority .filter-active-num').innerText = (priorities.length === selectedPriorities.length) ? 'All' : selectedPriorities.length;
document.querySelector('#log-filter-channel .filter-active-num').innerText = (channels.length === selectedChannels.length) ? 'All' : selectedChannels.length;
}
}
</script>
{% endblock %}
{% block toolbar %}
{% if collector.counterrors or collector.countdeprecations or collector.countwarnings %}
{% set icon %}
{% set status_color = collector.counterrors ? 'red' : collector.countwarnings ? 'yellow' : 'none' %}
{{ source('@WebProfiler/Icon/logger.svg') }}
<span class="sf-toolbar-value">{{ collector.counterrors ?: (collector.countdeprecations + collector.countwarnings) }}</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Errors</b>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.counterrors ? 'red' }}">{{ collector.counterrors|default(0) }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Warnings</b>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.countwarnings ? 'yellow' }}">{{ collector.countwarnings|default(0) }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Deprecations</b>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.countdeprecations ? 'none' }}">{{ collector.countdeprecations|default(0) }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label label-status-{{ collector.counterrors ? 'error' : collector.countwarnings ? 'warning' : 'none' }} {{ collector.logs is empty ? 'disabled' }}">
<span class="icon">{{ source('@WebProfiler/Icon/logger.svg') }}</span>
<strong>Logs</strong>
{% if collector.counterrors or collector.countdeprecations or collector.countwarnings %}
<span class="count">
<span>{{ collector.counterrors ?: (collector.countdeprecations + collector.countwarnings) }}</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
<h2>Log Messages</h2>
{% if collector.processedLogs is empty %}
<div class="empty empty-panel">
<p>No log messages available.</p>
</div>
{% else %}
{% set has_error_logs = collector.processedLogs|column('type')|filter(type => 'error' == type)|length > 0 %}
{% set has_deprecation_logs = collector.processedLogs|column('type')|filter(type => 'deprecation' == type)|length > 0 %}
{% set filters = collector.filters %}
<div class="log-filters">
<div id="log-filter-type" class="log-filter">
<div class="tab-navigation">
{% set initially_active_tab = has_error_logs ? 'error' : has_deprecation_logs ? 'deprecation' : 'all' %}
<input type="radio" id="filter-log-type-all" name="filter-log-type" value="all" {{ 'all' == initially_active_tab ? 'checked' }}>
<label for="filter-log-type-all" class="tab-control">
All messages
</label>
<input type="radio" id="filter-log-type-error" name="filter-log-type" value="error" {{ 'error' == initially_active_tab ? 'checked' }}>
<label for="filter-log-type-error" class="tab-control">
Errors
<span class="badge status-{{ collector.counterrors ? 'error' }}">{{ collector.counterrors|default(0) }}</span>
</label>
<input type="radio" id="filter-log-type-deprecation" name="filter-log-type" value="deprecation" {{ 'deprecation' == initially_active_tab ? 'checked' }}>
<label for="filter-log-type-deprecation" class="tab-control">
Deprecations
<span class="badge status-{{ collector.countdeprecations ? 'warning' }}">{{ collector.countdeprecations|default(0) }}</span>
</label>
</div>
</div>
<details id="log-filter-priority" class="log-filter">
<summary>
<span class="icon">{{ source('@WebProfiler/Icon/filter.svg') }}</span>
Level (<span class="filter-active-num">{{ filters.priority|length - 1 }}</span>)
</summary>
<div class="log-filter-content">
<div class="filter-select-all-or-none">
<button type="button" class="btn btn-link select-all">Select All</button>
<button type="button" class="btn btn-link select-none">Select None</button>
</div>
{% for label, value in filters.priority %}
<div class="log-filter-option">
<input {{ 'debug' != value ? 'checked' }} type="checkbox" id="filter-log-level-{{ value }}" name="filter-log-level-{{ value }}" value="{{ value }}">
<label for="filter-log-level-{{ value }}">{{ label }}</label>
</div>
{% endfor %}
</div>
</details>
<details id="log-filter-channel" class="log-filter">
<summary>
<span class="icon">{{ source('@WebProfiler/Icon/filter.svg') }}</span>
Channel (<span class="filter-active-num">{{ filters.channel|length - 1 }}</span>)
</summary>
<div class="log-filter-content">
<div class="filter-select-all-or-none">
<button type="button" class="btn btn-link select-all">Select All</button>
<button type="button" class="btn btn-link select-none">Select None</button>
</div>
{% for value in filters.channel %}
<div class="log-filter-option">
<input {{ 'event' != value ? 'checked' }} type="checkbox" id="filter-log-channel-{{ value }}" name="filter-log-channel-{{ value }}" value="{{ value }}">
<label for="filter-log-channel-{{ value }}">{{ value|title }}</label>
</div>
{% endfor %}
</div>
</details>
</div>
<table class="logs">
<colgroup>
<col style="width: 140px">
<col>
</colgroup>
<thead>
<tr>
<th>Time</th>
<th>Message</th>
</tr>
</thead>
<tbody>
{% for log in collector.processedLogs %}
{% set css_class = 'error' == log.type ? 'error'
: (log.priorityName == 'WARNING' or 'deprecation' == log.type) ? 'warning'
: 'silenced' == log.type ? 'silenced'
%}
<tr class="log-status-{{ css_class }}" data-type="{{ log.type }}" data-priority="{{ log.priority }}" data-channel="{{ log.channel }}" style="{{ 'event' == log.channel or 'DEBUG' == log.priorityName ? 'display: none' }}">
<td class="log-timestamp">
<time class="newline" title="{{ log.timestamp|date('r') }}" datetime="{{ log.timestamp|date(constant('DateTimeInterface::RFC3339_EXTENDED')) }}" data-convert-to-user-timezone data-render-as-time data-render-with-millisecond-precision>
{{ log.timestamp|date('H:i:s.v') }}
</time>
{% if log.type in ['error', 'deprecation', 'silenced'] or 'WARNING' == log.priorityName %}
<span class="log-type-badge badge badge-{{ css_class }}">
{% if 'error' == log.type or 'WARNING' == log.priorityName %}
{{ log.priorityName|lower }}
{% else %}
{{ log.type|lower }}
{% endif %}
</span>
{% else %}
<span class="log-type-badge badge badge-{{ css_class }}">
{{ log.priorityName|lower }}
</span>
{% endif %}
</td>
<td class="font-normal">
{{ _self.render_log_message('debug', loop.index, log) }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="no-logs-message empty">
<p>There are no log messages.</p>
</div>
{% endif %}
{% set compilerLogTotal = collector.compilerLogs|reduce((total, logs) => total + logs|length, 0) %}
<details class="container-compilation-logs">
<summary>
<h4>Container Compilation Logs <span class="text-muted">({{ compilerLogTotal }})</span></h4>
<span class="text-muted">Log messages generated during the compilation of the service container.</span>
</summary>
{% if collector.compilerLogs is empty %}
<div class="empty">
<p>There are no compiler log messages.</p>
</div>
{% else %}
<table class="container-logs">
<thead>
<tr>
<th>Messages</th>
<th class="full-width">Class</th>
</tr>
</thead>
<tbody>
{% for class, logs in collector.compilerLogs %}
<tr>
<td class="font-normal text-right">{{ logs|length }}</td>
<td class="font-normal">
{% set context_id = 'context-compiler-' ~ loop.index %}
<button type="button" class="btn btn-link sf-toggle" data-toggle-selector="#{{ context_id }}" data-toggle-alt-content="{{ class }}">{{ class }}</button>
<div id="{{ context_id }}" class="context sf-toggle-content sf-toggle-hidden">
<ul class="break-long-words">
{% for log in logs %}
<li>{{ profiler_dump_log(log.message) }}</li>
{% endfor %}
</ul>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</details>
{% endblock %}
{% macro render_log_message(category, log_index, log) %}
{% set has_context = log.context is defined and log.context is not empty %}
{% set has_trace = log.context.exception.trace is defined %}
{% if not has_context %}
{{ profiler_dump_log(log.message) }}
{% else %}
{{ profiler_dump_log(log.message, log.context) }}
{% endif %}
<div class="log-metadata">
{% if log.channel %}
<span class="badge">{{ log.channel }}</span>
{% endif %}
{% if log.errorCount is defined and log.errorCount > 1 %}
<span class="log-num-occurrences">{{ log.errorCount }} times</span>
{% endif %}
{% if has_context %}
{% set context_id = 'context-' ~ category ~ '-' ~ log_index %}
<span><button type="button" class="btn btn-link text-small sf-toggle" data-toggle-selector="#{{ context_id }}" data-toggle-alt-content="Hide context">Show context</button></span>
{% endif %}
{% if has_trace %}
{% set trace_id = 'trace-' ~ category ~ '-' ~ log_index %}
<span><button type="button" class="btn btn-link text-small sf-toggle" data-toggle-selector="#{{ trace_id }}" data-toggle-alt-content="Hide trace">Show trace</button></span>
{% endif %}
{% if has_context %}
<div id="{{ context_id }}" class="context sf-toggle-content sf-toggle-hidden">
{{ profiler_dump(log.context, maxDepth=1) }}
</div>
{% endif %}
{% if has_trace %}
<div id="{{ trace_id }}" class="context sf-toggle-content sf-toggle-hidden">
{{ profiler_dump(log.context.exception.trace, maxDepth=1) }}
</div>
{% endif %}
</div>
{% endmacro %}

View File

@@ -0,0 +1,511 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block stylesheets %}
{{ parent() }}
<style>
:root {
--mailer-email-table-wrapper-background: var(--gray-100);
--mailer-email-table-active-row-background: #dbeafe;
--mailer-email-table-active-row-color: var(--color-text);
}
.theme-dark {
--mailer-email-table-wrapper-background: var(--gray-900);
--mailer-email-table-active-row-background: var(--gray-300);
--mailer-email-table-active-row-color: var(--gray-800);
}
.mailer-email-summary-table-wrapper {
background: var(--mailer-email-table-wrapper-background);
border-bottom: 4px double var(--table-border-color);
border-radius: inherit;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
margin: 0 -9px 10px -9px;
padding-bottom: 10px;
transform: translateY(-9px);
max-height: 265px;
overflow-y: auto;
}
.mailer-email-summary-table,
.mailer-email-summary-table tr,
.mailer-email-summary-table td {
border: 0;
border-radius: inherit;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
box-shadow: none;
}
.mailer-email-summary-table th {
color: var(--color-muted);
font-size: 13px;
padding: 4px 10px;
}
.mailer-email-summary-table tr td,
.mailer-email-summary-table tr:last-of-type td {
border: solid var(--table-border-color);
border-width: 1px 0;
}
.mailer-email-summary-table-row {
margin: 5px 0;
}
.mailer-email-summary-table-row:hover {
cursor: pointer;
}
.mailer-email-summary-table-row.active {
background: var(--mailer-email-table-active-row-background);
color: var(--mailer-email-table-active-row-color);
}
.mailer-email-summary-table-row td {
font-family: var(--font-family-system);
font-size: inherit;
}
.mailer-email-details {
display: none;
}
.mailer-email-details.active {
display: block;
}
.mailer-transport-information {
border-bottom: 1px solid var(--form-input-border-color);
padding-bottom: 5px;
font-size: var(--font-size-body);
margin: 5px 0 10px 5px;
}
.mailer-transport-information .badge {
font-size: inherit;
font-weight: inherit;
}
.mailer-message-subject {
font-size: 21px;
font-weight: bold;
margin: 5px;
}
.mailer-message-headers {
margin-bottom: 10px;
}
.mailer-message-headers p {
font-size: var(--font-size-body);
margin: 2px 5px;
}
.mailer-message-header-secondary {
color: var(--color-muted);
}
.mailer-message-attachments-title {
align-items: center;
display: flex;
font-size: var(--font-size-body);
font-weight: 600;
margin-bottom: 10px;
}
.mailer-message-attachments-title svg {
color: var(--color-muted);
margin-right: 5px;
height: 18px;
width: 18px;
}
.mailer-message-attachments-title span {
font-weight: normal;
margin-left: 4px;
}
.mailer-message-attachments-list {
list-style: none;
margin: 0 0 5px 20px;
padding: 0;
}
.mailer-message-attachments-list li {
align-items: center;
display: flex;
}
.mailer-message-attachments-list li svg {
margin-right: 5px;
height: 18px;
width: 18px;
}
.mailer-message-attachments-list li a {
margin-left: 5px;
}
.mailer-email-body {
margin: 0;
padding: 6px 8px;
}
.mailer-empty-email-body {
background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' stroke='%23e5e5e5' stroke-width='4' stroke-dasharray='6%2c 14' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e");
border-radius: 6px;
color: var(--color-muted);
margin: 1em 0 0;
padding: .5em 1em;
}
.theme-dark .mailer-empty-email-body {
background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' stroke='%23737373' stroke-width='4' stroke-dasharray='6%2c 14' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e");
}
.mailer-empty-email-body p {
font-size: var(--font-size-body);
margin: 0;
padding: 0.5em 0;
}
.mailer-message-download-raw {
align-items: center;
display: flex;
padding: 5px 0 0 5px;
}
.mailer-message-download-raw svg {
height: 18px;
width: 18px;
margin-right: 3px;
}
</style>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script>
window.addEventListener('DOMContentLoaded', () => {
new SymfonyProfilerMailerPanel();
});
class SymfonyProfilerMailerPanel {
constructor() {
this.#initializeEmailsTable();
}
#initializeEmailsTable() {
const emailRows = document.querySelectorAll('.mailer-email-summary-table-row');
emailRows.forEach((emailRow) => {
emailRow.addEventListener('click', () => {
emailRows.forEach((row) => row.classList.remove('active'));
emailRow.classList.add('active');
document.querySelectorAll('.mailer-email-details').forEach((emailDetails) => emailDetails.style.display = 'none');
document.querySelector(emailRow.getAttribute('data-target')).style.display = 'block';
});
});
}
}
</script>
{% endblock %}
{% block toolbar %}
{% set events = collector.events %}
{% if events.messages|length %}
{% set icon %}
{{ source('@WebProfiler/Icon/mailer.svg') }}
<span class="sf-toolbar-value">{{ events.messages|length }}</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Queued messages</b>
<span class="sf-toolbar-status">{{ events.events|filter(e => e.isQueued())|length }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Sent messages</b>
<span class="sf-toolbar-status">{{ events.events|filter(e => not e.isQueued())|length }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { 'link': profiler_url }) }}
{% endif %}
{% endblock %}
{% block menu %}
{% set events = collector.events %}
<span class="label {{ events.messages is empty ? 'disabled' }}">
<span class="icon">{{ source('@WebProfiler/Icon/mailer.svg') }}</span>
<strong>Emails</strong>
{% if events.messages|length > 0 %}
<span class="count">
<span>{{ events.messages|length }}</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
{% set events = collector.events %}
<h2>Emails</h2>
{% if not events.messages|length %}
<div class="empty empty-panel">
<p>No emails were sent.</p>
</div>
{% else %}
<div class="metrics">
<div class="metric-group">
<div class="metric">
<span class="value">{{ events.events|filter(e => e.isQueued())|length }}</span>
<span class="label">Queued</span>
</div>
<div class="metric">
<span class="value">{{ events.events|filter(e => not e.isQueued())|length }}</span>
<span class="label">Sent</span>
</div>
</div>
</div>
{% endif %}
{% if events.transports|length > 1 %}
{% for transport in events.transports %}
<h2><code>{{ transport }}</code> transport</h2>
{{ _self.render_transport_details(collector, transport) }}
{% endfor %}
{% elseif events.transports is not empty %}
{{ _self.render_transport_details(collector, events.transports|first, true) }}
{% endif %}
{% macro render_transport_details(collector, transport, show_transport_name = false) %}
<div class="card">
{% set num_emails = collector.events.events(transport)|length %}
{% if num_emails > 1 %}
<div class="mailer-email-summary-table-wrapper">
<table class="mailer-email-summary-table">
<thead>
<tr>
<th>#</th>
<th>Subject</th>
<th>To</th>
<th class="visually-hidden">Actions</th>
</tr>
</thead>
<tbody>
{% for event in collector.events.events(transport) %}
<tr class="mailer-email-summary-table-row {{ loop.first ? 'active' }}" data-target="#email-{{ loop.index }}">
<td>{{ loop.index }}</td>
<td>
{% if event.message.subject is defined %}
{{ event.message.getSubject() ?? '(No subject)' }}
{% elseif event.message.headers.has('subject') %}
{{ event.message.headers.get('subject').bodyAsString()|default('(No subject)') }}
{% else %}
(No subject)
{% endif %}
</td>
<td>
{% if event.message.to is defined %}
{{ event.message.getTo()|map(addr => addr.toString())|join(', ')|default('(empty)') }}
{% elseif event.message.headers.has('to') %}
{{ event.message.headers.get('to').bodyAsString()|default('(empty)') }}
{% else %}
(empty)
{% endif %}
</td>
<td class="visually-hidden"><button class="mailer-email-summary-table-row-button" data-target="#email-{{ loop.index }}">View email details</button></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% for event in collector.events.events(transport) %}
<div class="mailer-email-details {{ loop.first ? 'active' }}" id="email-{{ loop.index }}">
{{ _self.render_email_details(collector, transport, event.message, event.isQueued, show_transport_name) }}
</div>
{% endfor %}
{% else %}
{% set event = (collector.events.events(transport)|first) %}
{{ _self.render_email_details(collector, transport, event.message, event.isQueued, show_transport_name) }}
{% endif %}
</div>
{% endmacro %}
{% macro render_email_details(collector, transport, message, message_is_queued, show_transport_name = false) %}
{% if show_transport_name %}
<p class="mailer-transport-information">
<strong>Status:</strong> <span class="badge badge-{{ message_is_queued ? 'warning' : 'success' }}">{{ message_is_queued ? 'Queued' : 'Sent' }}</span>
&bull;
<strong>Transport:</strong> <code>{{ transport }}</code>
</p>
{% endif %}
{% if message.headers is not defined %}
{# render the raw message contents #}
<a class="mailer-message-download-raw" href="data:application/octet-stream;base64,{{ collector.base64Encode(message.toString()) }}" download="email.eml">
{{ source('@WebProfiler/Icon/download.svg') }}
Download as EML file
</a>
<pre class="prewrap" style="max-height: 600px; margin-left: 5px">{{ message.toString() }}</pre>
{% else %}
<div class="sf-tabs">
<div class="tab">
<h3 class="tab-title">Email contents</h3>
<div class="tab-content">
<div class="card-block">
<p class="mailer-message-subject">
{% if message.subject is defined %}
{{ message.getSubject() ?? '(No subject)' }}
{% elseif message.headers.has('subject') %}
{{ message.headers.get('subject').bodyAsString()|default('(No subject)') }}
{% else %}
(No subject)
{% endif %}
</p>
<div class="mailer-message-headers">
<p>
<strong>From:</strong>
{% if message.from is defined %}
{{ message.getFrom()|map(addr => addr.toString())|join(', ')|default('(empty)') }}
{% elseif message.headers.has('from') %}
{{ message.headers.get('from').bodyAsString()|default('(empty)') }}
{% else %}
(empty)
{% endif %}
</p>
<p>
<strong>To:</strong>
{% if message.to is defined %}
{{ message.getTo()|map(addr => addr.toString())|join(', ')|default('(empty)') }}
{% elseif message.headers.has('to') %}
{{ message.headers.get('to').bodyAsString()|default('(empty)') }}
{% else %}
(empty)
{% endif %}
</p>
{% for header in message.headers.all|filter(header => (header.name ?? '')|lower not in ['subject', 'from', 'to']) %}
<p class="mailer-message-header-secondary">{{ header.toString }}</p>
{% endfor %}
</div>
</div>
{% if message.attachments is defined and message.attachments %}
<div class="card-block">
{% set num_of_attachments = message.attachments|length %}
{% set total_attachments_size_in_bytes = message.attachments|reduce((total_size, attachment) => total_size + attachment.body|length, 0) %}
<p class="mailer-message-attachments-title">
{{ source('@WebProfiler/Icon/attachment.svg') }}
Attachments <span>({{ num_of_attachments }} file{{ num_of_attachments > 1 ? 's' }} / {{ _self.render_file_size_humanized(total_attachments_size_in_bytes) }})</span>
</p>
<ul class="mailer-message-attachments-list">
{% for attachment in message.attachments %}
<li>
{{ source('@WebProfiler/Icon/file.svg') }}
{% if attachment.filename|default %}
{{ attachment.filename }}
{% else %}
<em>(no filename)</em>
{% endif %}
({{ _self.render_file_size_humanized(attachment.body|length) }})
<a href="data:{{ attachment.contentType|default('application/octet-stream') }};base64,{{ collector.base64Encode(attachment.body) }}" download="{{ attachment.filename|default('attachment') }}">Download</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<div class="card-block">
<div class="sf-tabs sf-tabs-sm">
{% if message.htmlBody is defined %}
{% set textBody = message.textBody %}
{% set htmlBody = message.htmlBody %}
<div class="tab {{ not textBody ? 'disabled' }} {{ textBody ? 'active' }}">
<h3 class="tab-title">Text content</h3>
<div class="tab-content">
{% if textBody %}
<pre class="mailer-email-body prewrap" style="max-height: 600px">
{%- if message.textCharset() %}
{{- textBody|convert_encoding('UTF-8', message.textCharset()) }}
{%- else %}
{{- textBody }}
{%- endif -%}
</pre>
{% else %}
<div class="mailer-empty-email-body">
<p>The text body is empty.</p>
</div>
{% endif %}
</div>
</div>
{% if htmlBody %}
<div class="tab">
<h3 class="tab-title">HTML preview</h3>
<div class="tab-content">
<pre class="prewrap" style="max-height: 600px"><iframe src="data:text/html;charset=utf-8;base64,{{ collector.base64Encode(htmlBody) }}" style="height: 80vh;width: 100%;"></iframe>
</pre>
</div>
</div>
{% endif %}
<div class="tab {{ not htmlBody ? 'disabled' }} {{ not textBody and htmlBody ? 'active' }}">
<h3 class="tab-title">HTML content</h3>
<div class="tab-content">
{% if htmlBody %}
<pre class="mailer-email-body prewrap" style="max-height: 600px">
{%- if message.htmlCharset() %}
{{- htmlBody|convert_encoding('UTF-8', message.htmlCharset()) }}
{%- else %}
{{- htmlBody }}
{%- endif -%}
</pre>
{% else %}
<div class="mailer-empty-email-body">
<p>The HTML body is empty.</p>
</div>
{% endif %}
</div>
</div>
{% else %}
{% set body = message.body ? message.body.toString() : null %}
<div class="tab {{ not body ? 'disabled' }} {{ body ? 'active' }}">
<h3 class="tab-title">Content</h3>
<div class="tab-content">
{% if body %}
<pre class="mailer-email-body prewrap" style="max-height: 600px">
{{- body }}
</pre>
{% else %}
<div class="mailer-empty-email-body">
<p>The body is empty.</p>
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<div class="tab">
<h3 class="tab-title">MIME parts</h3>
<div class="tab-content">
<pre class="prewrap" style="max-height: 600px; margin-left: 5px">{{ message.body().asDebugString() }}</pre>
</div>
</div>
<div class="tab">
<h3 class="tab-title">Raw Message</h3>
<div class="tab-content">
<a class="mailer-message-download-raw" href="data:application/octet-stream;base64,{{ collector.base64Encode(message.toString()) }}" download="email.eml">
{{ source('@WebProfiler/Icon/download.svg') }}
Download as EML file
</a>
<pre class="prewrap" style="max-height: 600px; margin-left: 5px">{{ message.toString() }}</pre>
</div>
</div>
</div>
{% endif %}
{% endmacro %}
{% macro render_file_size_humanized(bytes) %}
{%- if bytes < 1000 -%}
{{- bytes ~ ' bytes' -}}
{%- elseif bytes < 1000 ** 2 -%}
{{- (bytes / 1000)|number_format(2) ~ ' kB' -}}
{%- else -%}
{{- (bytes / 1000 ** 2)|number_format(2) ~ ' MB' -}}
{%- endif -%}
{% endmacro %}
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% set icon %}
{% set status_color = (collector.memory / 1024 / 1024) > 50 ? 'yellow' %}
{{ source('@WebProfiler/Icon/memory.svg') }}
<span class="sf-toolbar-value">{{ '%.1f'|format(collector.memory / 1024 / 1024) }}</span>
<span class="sf-toolbar-label">MiB</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Peak memory usage</b>
<span>{{ '%.1f'|format(collector.memory / 1024 / 1024) }} MiB</span>
</div>
<div class="sf-toolbar-info-piece">
<b>PHP memory limit</b>
<span>{{ collector.memoryLimit == -1 ? 'Unlimited' : '%.0f MiB'|format(collector.memoryLimit / 1024 / 1024) }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, name: 'time', status: status_color }) }}
{% endblock %}

View File

@@ -0,0 +1,215 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block head %}
{{ parent() }}
<style>
.message-item thead th { position: relative; cursor: pointer; user-select: none; padding-right: 35px; }
.message-item tbody tr td:first-child { width: 170px; }
.message-item .label { float: right; padding: 1px 5px; opacity: .75; margin-left: 5px; }
.message-item .toggle-button { position: absolute; right: 6px; top: 6px; opacity: .5; pointer-events: none; color: inherit; }
.message-item .icon svg { height: 24px; width: 24px; }
.message-item .icon-close svg { transform: rotate(180deg); }
.message-item .sf-toggle-off .icon-close, .sf-toggle-on .icon-open { display: none; }
.message-item .sf-toggle-off .icon-open, .sf-toggle-on .icon-close { display: block; }
.message-bus .badge.status-some-errors { line-height: 16px; border-bottom: 2px solid #B0413E; }
.message-item tbody.sf-toggle-content.sf-toggle-visible { display: table-row-group; }
#collector-content .message-bus .trace {
border: var(--border);
background: var(--base-0);
padding: 10px;
margin: 0.5em 0;
overflow: auto;
}
#collector-content .message-bus .trace {
font-size: 12px;
}
#collector-content .message-bus .trace li {
margin-bottom: 0;
padding: 0;
}
#collector-content .message-bus .trace li.selected {
background: var(--highlight-selected-line);
}
</style>
{% endblock %}
{% block toolbar %}
{% if collector.messages|length > 0 %}
{% set status_color = collector.exceptionsCount ? 'red' %}
{% set icon %}
{{ source('@WebProfiler/Icon/messenger.svg') }}
<span class="sf-toolbar-value">{{ collector.messages|length }}</span>
{% endset %}
{% set text %}
{% for bus in collector.buses %}
{% set exceptionsCount = collector.exceptionsCount(bus) %}
<div class="sf-toolbar-info-piece">
<b>{{ bus }}</b>
<span
title="{{ exceptionsCount }} message(s) with exceptions"
class="sf-toolbar-status sf-toolbar-status-{{ exceptionsCount ? 'red' }}"
>
{{ collector.messages(bus)|length }}
</span>
</div>
{% endfor %}
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: 'messenger', status: status_color }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label{{ collector.exceptionsCount ? ' label-status-error' }}{{ collector.messages is empty ? ' disabled' }}">
<span class="icon">{{ source('@WebProfiler/Icon/messenger.svg') }}</span>
<strong>Messages</strong>
{% if collector.exceptionsCount > 0 %}
<span class="count">
<span>{{ collector.exceptionsCount }}</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
<h2>Messages</h2>
{% if collector.messages is empty %}
<div class="empty empty-panel">
<p>No messages have been collected.</p>
</div>
{% elseif 1 == collector.buses|length %}
<p class="text-muted">Ordered list of dispatched messages across all your buses</p>
{{ _self.render_bus_messages(collector.messages, true) }}
{% else %}
<div class="sf-tabs message-bus">
<div class="tab">
{% set messages = collector.messages %}
{% set exceptionsCount = collector.exceptionsCount %}
<h3 class="tab-title">All<span class="badge {{ exceptionsCount ? exceptionsCount == messages|length ? 'status-error' : 'status-some-errors' }}">{{ messages|length }}</span></h3>
<div class="tab-content">
<p class="text-muted">Ordered list of dispatched messages across all your buses</p>
{{ _self.render_bus_messages(messages, true) }}
</div>
</div>
{% for bus in collector.buses %}
<div class="tab message-bus">
{% set messages = collector.messages(bus) %}
{% set exceptionsCount = collector.exceptionsCount(bus) %}
<h3 class="tab-title">{{ bus }}<span class="badge {{ exceptionsCount ? exceptionsCount == messages|length ? 'status-error' : 'status-some-errors' }}">{{ messages|length }}</span></h3>
<div class="tab-content">
<p class="text-muted">Ordered list of messages dispatched on the <code>{{ bus }}</code> bus</p>
{{ _self.render_bus_messages(messages) }}
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% endblock %}
{% macro render_bus_messages(messages, showBus = false) %}
{% set discr = random() %}
{% for dispatchCall in messages %}
<table class="message-item">
<thead>
<tr>
<th colspan="2" class="sf-toggle"
data-toggle-selector="#message-item-{{ discr }}-{{ loop.index0 }}-details"
data-toggle-initial="{{ loop.first ? 'display' }}"
>
<span class="dump-inline">{{ profiler_dump(dispatchCall.message.type) }}</span>
{% if dispatchCall.exception is defined %}
<span class="label status-error">exception</span>
{% endif %}
<button class="btn btn-link toggle-button" type="button">
<span class="icon icon-close">{{ source('@WebProfiler/Icon/chevron-down.svg') }}</span>
<span class="icon icon-open">{{ source('@WebProfiler/Icon/chevron-down.svg') }}</span>
</button>
</th>
</tr>
</thead>
<tbody id="message-item-{{ discr }}-{{ loop.index0 }}-details" class="sf-toggle-content">
<tr>
<th scope="row" class="font-normal">Caller</th>
<td class="message-bus-dispatch-caller">
In
{% set caller = dispatchCall.caller %}
{% if caller.line %}
{% set link = caller.file|file_link(caller.line) %}
{% if link %}
<a href="{{ link }}" title="{{ caller.file }}">{{ caller.name }}</a>
{% else %}
<abbr title="{{ caller.file }}">{{ caller.name }}</abbr>
{% endif %}
{% else %}
{{ caller.name }}
{% endif %}
line <button type="button" class="btn-link text-small sf-toggle" data-toggle-selector="#sf-trace-{{ discr }}-{{ loop.index0 }}">{{ caller.line }}</button>
<div class="hidden" id="sf-trace-{{ discr }}-{{ loop.index0 }}">
<div class="trace">
{{ caller.file|file_excerpt(caller.line)|replace({
'#DD0000': 'var(--highlight-string)',
'#007700': 'var(--highlight-keyword)',
'#0000BB': 'var(--highlight-default)',
'#FF8000': 'var(--highlight-comment)'
})|raw }}
</div>
</div>
</td>
</tr>
{% if showBus %}
<tr>
<th scope="row" class="font-normal">Bus</th>
<td>{{ dispatchCall.bus }}</td>
</tr>
{% endif %}
<tr>
<th scope="row" class="font-normal">Message</th>
<td>{{ profiler_dump(dispatchCall.message.value, maxDepth=2) }}</td>
</tr>
<tr>
<th scope="row" class="font-normal">Envelope stamps <span class="block text-muted">when dispatching</span></th>
<td>
{% for item in dispatchCall.stamps %}
{{ profiler_dump(item) }}
{% else %}
<span class="text-muted font-normal">No items</span>
{% endfor %}
</td>
</tr>
{% if dispatchCall.stamps_after_dispatch is defined %}
<tr>
<th scope="row" class="font-normal">Envelope stamps <span class="block text-muted">after dispatch</span></th>
<td>
{% for item in dispatchCall.stamps_after_dispatch %}
{{ profiler_dump(item) }}
{% else %}
<span class="text-muted">No items</span>
{% endfor %}
</td>
</tr>
{% endif %}
{% if dispatchCall.exception is defined %}
<tr>
<td class="text-bold">Exception</td>
<td>
{{ profiler_dump(dispatchCall.exception.value, maxDepth=1) }}
</td>
</tr>
{% endif %}
</tbody>
</table>
{% endfor %}
{% endmacro %}

View File

@@ -0,0 +1,168 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% set events = collector.events %}
{% if events.messages|length %}
{% set icon %}
{{ source('@WebProfiler/Icon/notifier.svg') }}
<span class="sf-toolbar-value">{{ events.messages|length }}</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Sent notifications</b>
<span class="sf-toolbar-status">{{ events.messages|length }}</span>
</div>
{% for transport in events.transports %}
<div class="sf-toolbar-info-piece">
<b>{{ transport ?: '<em>Empty Transport Name</em>' }}</b>
<span class="sf-toolbar-status">{{ events.messages(transport)|length }}</span>
</div>
{% endfor %}
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { 'link': profiler_url }) }}
{% endif %}
{% endblock %}
{% block head %}
{{ parent() }}
<style>
/* utility classes */
.m-t-0 { margin-top: 0 !important; }
.m-t-10 { margin-top: 10px !important; }
/* basic grid */
.row {
display: flex;
flex-wrap: wrap;
margin-right: -15px;
margin-left: -15px;
}
.col {
flex-basis: 0;
flex-grow: 1;
max-width: 100%;
position: relative;
width: 100%;
min-height: 1px;
padding-right: 15px;
padding-left: 15px;
}
.col-4 {
flex: 0 0 33.333333%;
max-width: 33.333333%;
}
/* small tabs */
.sf-tabs-sm .tab-navigation li {
font-size: 14px;
padding: .3em .5em;
}
</style>
{% endblock %}
{% block menu %}
{% set events = collector.events %}
<span class="label {{ events.messages|length ? '' : 'disabled' }}">
<span class="icon">{{ source('@WebProfiler/Icon/notifier.svg') }}</span>
<strong>Notifications</strong>
{% if events.messages|length > 0 %}
<span class="count">
<span>{{ events.messages|length }}</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
{% set events = collector.events %}
<h2>Notifications</h2>
{% if not events.messages|length %}
<div class="empty empty-panel">
<p>No notifications were sent.</p>
</div>
{% endif %}
<div class="metrics">
{% for transport in events.transports %}
<div class="metric">
<span class="value">{{ events.messages(transport)|length }}</span>
<span class="label">{{ transport }}</span>
</div>
{% endfor %}
</div>
{% for transport in events.transports %}
<h3>{{ transport ?: '<em>Empty Transport Name</em>' }}</h3>
<div class="card-block">
<div class="sf-tabs sf-tabs-sm">
{% for event in events.events(transport) %}
{% set message = event.message %}
<div class="tab">
<h3 class="tab-title">Message #{{ loop.index }} <small>({{ event.isQueued() ? 'queued' : 'sent' }})</small></h3>
<div class="tab-content">
<div class="card">
<div class="card-block">
<span class="label">Subject</span>
<h2 class="m-t-10">{{ message.getSubject() ?? '(empty)' }}</h2>
</div>
{% set notification = message.notification ?? null %}
{% if notification %}
<div class="card-block">
<div class="row">
<div class="col">
<span class="label">Content</span>
<pre class="prewrap">{{ notification.getContent() ?? '(empty)' }}</pre>
<span class="label">Importance</span>
<pre class="prewrap">{{ notification.getImportance() }}</pre>
</div>
</div>
</div>
{% endif %}
<div class="card-block">
<div class="sf-tabs sf-tabs-sm">
{% if notification %}
<div class="tab">
<h3 class="tab-title">Notification</h3>
<div class="tab-content">
<pre class="prewrap" style="max-height: 600px">
{{- 'Subject: ' ~ notification.getSubject() }}<br/>
{{- 'Content: ' ~ notification.getContent() }}<br/>
{{- 'Importance: ' ~ notification.getImportance() }}<br/>
{{- 'Emoji: ' ~ (notification.getEmoji() is empty ? '(empty)' : notification.getEmoji()) }}<br/>
{{- 'Exception: ' ~ (notification.getException() ?? '(empty)') }}<br/>
{{- 'ExceptionAsString: ' ~ (notification.getExceptionAsString() is empty ? '(empty)' : notification.getExceptionAsString()) }}
</pre>
</div>
</div>
{% endif %}
<div class="tab">
<h3 class="tab-title">Message Options</h3>
<div class="tab-content">
<pre class="prewrap" style="max-height: 600px">
{%- if message.getOptions() is null %}
{{- '(empty)' }}
{%- else %}
{{- message.getOptions().toArray()|json_encode(constant('JSON_PRETTY_PRINT')) }}
{%- endif %}
</pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
{% endblock %}

View File

@@ -0,0 +1,431 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block head %}
{{ parent() }}
<style>
.empty-query-post-files {
display: flex;
justify-content: space-between;
}
.empty-query-post-files > div {
flex: 1;
}
.empty-query-post-files > div + div {
margin-left: 30px;
}
.empty-query-post-files h3 {
margin-top: 0;
}
.empty-query-post-files .empty {
margin-bottom: 0;
}
</style>
{% endblock %}
{% block toolbar %}
{% set request_handler %}
{{ _self.set_handler(collector.controller) }}
{% endset %}
{% if collector.redirect %}
{% set redirect_handler %}
{{ _self.set_handler(collector.redirect.controller, collector.redirect.route, 'GET' != collector.redirect.method ? collector.redirect.method) }}
{% endset %}
{% endif %}
{% if collector.forwardtoken %}
{% set forward_profile = profile.childByToken(collector.forwardtoken) %}
{% set forward_handler %}
{{ _self.set_handler(forward_profile ? forward_profile.collector('request').controller : 'n/a') }}
{% endset %}
{% endif %}
{% set request_status_code_color = (collector.statuscode >= 400) ? 'red' : (collector.statuscode >= 300) ? 'yellow' : 'green' %}
{% set icon %}
<span class="sf-toolbar-status sf-toolbar-status-{{ request_status_code_color }}">{{ collector.statuscode }}</span>
{% if collector.route %}
{% if collector.redirect %}<span class="sf-toolbar-request-icon">{{ source('@WebProfiler/Icon/redirect.svg') }}</span>{% endif %}
{% if collector.forwardtoken %}<span class="sf-toolbar-request-icon">{{ source('@WebProfiler/Icon/forward.svg') }}</span>{% endif %}
<span class="sf-toolbar-label">{{ 'GET' != collector.method ? collector.method }} @</span>
<span class="sf-toolbar-value sf-toolbar-info-piece-additional">{{ collector.route }}</span>
{% endif %}
{% endset %}
{% set text %}
<div class="sf-toolbar-info-group">
<div class="sf-toolbar-info-piece">
<b>HTTP status</b>
<span>{{ collector.statuscode }} {{ collector.statustext }}</span>
</div>
{% if 'GET' != collector.method -%}
<div class="sf-toolbar-info-piece">
<b>Method</b>
<span>{{ collector.method }}</span>
</div>
{%- endif %}
<div class="sf-toolbar-info-piece">
<b>Controller</b>
<span>{{ request_handler }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Route name</b>
<span>{{ collector.route|default('n/a') }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Has session</b>
<span>{% if collector.sessionmetadata|length %}yes{% else %}no{% endif %}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Stateless Check</b>
<span>{% if collector.statelesscheck %}yes{% else %}no{% endif %}</span>
</div>
</div>
{% if redirect_handler is defined -%}
<div class="sf-toolbar-info-group">
<div class="sf-toolbar-info-piece">
<b>
<span class="sf-toolbar-redirection-status sf-toolbar-status-yellow">{{ collector.redirect.status_code }}</span>
Redirect from
</b>
<span>
{{ redirect_handler }}
(<a href="{{ path('_profiler', { token: collector.redirect.token }) }}">{{ collector.redirect.token }}</a>)
</span>
</div>
</div>
{% endif %}
{% if forward_handler is defined %}
<div class="sf-toolbar-info-group">
<div class="sf-toolbar-info-piece">
<b>Forwarded to</b>
<span>
{{ forward_handler }}
(<a href="{{ path('_profiler', { token: collector.forwardtoken }) }}">{{ collector.forwardtoken }}</a>)
</span>
</div>
</div>
{% endif %}
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }}
{% endblock %}
{% block menu %}
<span class="label">
<span class="icon">{{ source('@WebProfiler/Icon/request.svg') }}</span>
<strong>Request / Response</strong>
</span>
{% endblock %}
{% block panel %}
{% set controller_name = _self.set_handler(collector.controller) %}
<h2>
{{ 'n/a' in controller_name ? 'Request / Response' : controller_name }}
</h2>
<div class="sf-tabs">
<div class="tab">
<h3 class="tab-title">Request</h3>
<div class="tab-content">
{% set has_no_query_post_or_files = collector.requestquery.all is empty and collector.requestrequest.all is empty and collector.requestfiles is empty %}
{% if has_no_query_post_or_files %}
<div class="empty-query-post-files" style="display: flex; align-items: stretch">
<div>
<h3>GET Parameters</h3>
<div class="empty"><p>None</p></div>
</div>
<div>
<h3>POST Parameters</h3>
<div class="empty"><p>None</p></div>
</div>
<div>
<h3>Uploaded Files</h3>
<div class="empty"><p>None</p></div>
</div>
</div>
{% else %}
<h3>GET Parameters</h3>
{% if collector.requestquery.all is empty %}
<div class="empty">
<p>No GET parameters</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestquery, maxDepth: 1 }, with_context = false) }}
{% endif %}
<h3>POST Parameters</h3>
{% if collector.requestrequest.all is empty %}
<div class="empty">
<p>No POST parameters</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestrequest, maxDepth: 1 }, with_context = false) }}
{% endif %}
<h4>Uploaded Files</h4>
{% if collector.requestfiles is empty %}
<div class="empty">
<p>No files were uploaded</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestfiles, maxDepth: 1 }, with_context = false) }}
{% endif %}
{% endif %}
<h3>Request Attributes</h3>
{% if collector.requestattributes.all is empty %}
<div class="empty">
<p>No attributes</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestattributes }, with_context = false) }}
{% endif %}
<h3>Request Headers</h3>
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestheaders, labels: ['Header', 'Value'], maxDepth: 1 }, with_context = false) }}
<h3>Request Content</h3>
{% if collector.content == false %}
<div class="empty">
<p>Request content not available (it was retrieved as a resource).</p>
</div>
{% elseif collector.content %}
<div class="sf-tabs">
{% set prettyJson = collector.isJsonRequest ? collector.prettyJson : null %}
{% if prettyJson is not null %}
<div class="tab">
<h3 class="tab-title">Pretty</h3>
<div class="tab-content">
<div class="card" style="max-height: 500px; overflow-y: auto;">
<pre class="break-long-words">{{ prettyJson }}</pre>
</div>
</div>
</div>
{% endif %}
<div class="tab">
<h3 class="tab-title">Raw</h3>
<div class="tab-content">
<div class="card">
<pre class="break-long-words">{{ collector.content }}</pre>
</div>
</div>
</div>
</div>
{% else %}
<div class="empty">
<p>No content</p>
</div>
{% endif %}
</div>
</div>
<div class="tab">
<h3 class="tab-title">Response</h3>
<div class="tab-content">
<h3>Response Headers</h3>
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.responseheaders, labels: ['Header', 'Value'], maxDepth: 1 }, with_context = false) }}
</div>
</div>
<div class="tab {{ collector.requestcookies.all is empty and collector.responsecookies.all is empty ? 'disabled' }}">
<h3 class="tab-title">Cookies</h3>
<div class="tab-content">
<h3>Request Cookies</h3>
{% if collector.requestcookies.all is empty %}
<div class="empty">
<p>No request cookies</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestcookies }, with_context = false) }}
{% endif %}
<h3>Response Cookies</h3>
{% if collector.responsecookies.all is empty %}
<div class="empty">
<p>No response cookies</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.responsecookies }, with_context = true) }}
{% endif %}
</div>
</div>
<div class="tab {{ collector.sessionmetadata is empty ? 'disabled' }}">
<h3 class="tab-title">Session{% if collector.sessionusages is not empty %} <span class="badge">{{ collector.sessionusages|length }}</span>{% endif %}</h3>
<div class="tab-content">
<h3>Session Metadata</h3>
{% if collector.sessionmetadata is empty %}
<div class="empty">
<p>No session metadata</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/table.html.twig', { data: collector.sessionmetadata }, with_context = false) }}
{% endif %}
<h3>Session Attributes</h3>
{% if collector.sessionattributes is empty %}
<div class="empty">
<p>No session attributes</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/table.html.twig', { data: collector.sessionattributes, labels: ['Attribute', 'Value'] }, with_context = false) }}
{% endif %}
<h3>Session Usage</h3>
<div class="metrics">
<div class="metric">
<span class="value">{{ collector.sessionusages|length }}</span>
<span class="label">Usages</span>
</div>
<div class="metric">
<span class="value">{{ source('@WebProfiler/Icon/' ~ (collector.statelesscheck ? 'yes' : 'no') ~ '.svg') }}</span>
<span class="label">Stateless check enabled</span>
</div>
</div>
{% if collector.sessionusages is empty %}
<div class="empty">
<p>Session not used.</p>
</div>
{% else %}
<table class="session_usages">
<thead>
<tr>
<th class="full-width">Usage</th>
</tr>
</thead>
<tbody>
{% for key, usage in collector.sessionusages %}
<tr>
<td class="font-normal">
{%- set link = usage.file|file_link(usage.line) %}
{%- if link %}<a href="{{ link }}" title="{{ usage.name }}">{% else %}<span title="{{ usage.name }}">{% endif %}
{{ usage.name }}
{%- if link %}</a>{% else %}</span>{% endif %}
<div class="text-small font-normal">
{% set usage_id = 'session-usage-trace-' ~ key %}
<a class="btn btn-link text-small sf-toggle" data-toggle-selector="#{{ usage_id }}" data-toggle-alt-content="Hide trace">Show trace</a>
</div>
<div id="{{ usage_id }}" class="context sf-toggle-content sf-toggle-hidden">
{{ profiler_dump(usage.trace, maxDepth=2) }}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
<div class="tab {{ collector.flashes is empty ? 'disabled' }}">
<h3 class="tab-title">Flashes</h3>
<div class="tab-content">
<h3>Flashes</h3>
{% if collector.flashes is empty %}
<div class="empty">
<p>No flash messages were created.</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/table.html.twig', { data: collector.flashes }, with_context = false) }}
{% endif %}
</div>
</div>
<div class="tab">
<h3 class="tab-title">Server Parameters</h3>
<div class="tab-content">
<h3>Server Parameters</h3>
<h4>Defined in .env</h4>
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.dotenvvars }, with_context = false) }}
<h4>Defined as regular env variables</h4>
{% set requestserver = [] %}
{% for key, value in collector.requestserver|filter((_, key) => key not in collector.dotenvvars.keys) %}
{% set requestserver = requestserver|merge({(key): value}) %}
{% endfor %}
{{ include('@WebProfiler/Profiler/table.html.twig', { data: requestserver }, with_context = false) }}
</div>
</div>
{% if profile.parent %}
<div class="tab">
<h3 class="tab-title">Parent Request</h3>
<div class="tab-content">
<h3>
<a href="{{ path('_profiler', { token: profile.parent.token }) }}">Return to parent request</a>
<small>(token = {{ profile.parent.token }})</small>
</h3>
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: profile.parent.getcollector('request').requestattributes }, with_context = false) }}
</div>
</div>
{% endif %}
{% if profile.children|length %}
<div class="tab">
<h3 class="tab-title">Sub Requests <span class="badge">{{ profile.children|length }}</span></h3>
<div class="tab-content">
{% for child in profile.children %}
<h3>
{{ _self.set_handler(child.getcollector('request').controller) }}
<small>(token = <a href="{{ path('_profiler', { token: child.token }) }}">{{ child.token }}</a>)</small>
</h3>
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: child.getcollector('request').requestattributes }, with_context = false) }}
{% endfor %}
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% macro set_handler(controller, route, method) %}
{% if controller.class is defined -%}
{%- if method|default(false) %}<span class="sf-toolbar-status sf-toolbar-redirection-method">{{ method }}</span>{% endif -%}
{%- set link = controller.file|file_link(controller.line) %}
{%- if link %}<a href="{{ link }}" title="{{ controller.class }}">{% else %}<span title="{{ controller.class }}">{% endif %}
{%- if route|default(false) -%}
@{{ route }}
{%- else -%}
{{- controller.class|abbr_class|striptags -}}
{{- controller.method ? ' :: ' ~ controller.method -}}
{%- endif -%}
{%- if link %}</a>{% else %}</span>{% endif %}
{%- else -%}
<span>{{ route|default(controller) }}</span>
{%- endif %}
{% endmacro %}

View File

@@ -0,0 +1,14 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}{% endblock %}
{% block menu %}
<span class="label">
<span class="icon">{{ source('@WebProfiler/Icon/router.svg') }}</span>
<strong>Routing</strong>
</span>
{% endblock %}
{% block panel %}
{{ render(controller('web_profiler.controller.router::panelAction', { token: token })) }}
{% endblock %}

View File

@@ -0,0 +1,370 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block head %}
{{ parent() }}
<style>
#collector-content .sf-serializer {
margin-bottom: 2em;
}
#collector-content .sf-serializer .trace {
border: var(--border);
background: var(--page-background);
padding: 10px;
margin: 0.5em 0;
overflow: auto;
}
#collector-content .sf-serializer .trace {
font-size: 12px;
}
#collector-content .sf-serializer .trace li {
margin-bottom: 0;
padding: 0;
}
#collector-content .sf-serializer .trace li.selected {
background: var(--highlight-selected-line);
}
</style>
{% endblock %}
{% block toolbar %}
{% if collector.handledCount > 0 %}
{% set icon %}
{{ source('@WebProfiler/Icon/serializer.svg') }}
<span class="sf-toolbar-value">
{{ collector.handledCount }}
</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Total calls</b>
<span class="sf-toolbar-status">{{ collector.handledCount }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Total time</b>
<span>
{{ '%.2f'|format(collector.totalTime * 1000) }} <span class="unit">ms</span>
</span>
</div>
<div class="detailed-metrics">
<div>
<div class="sf-toolbar-info-piece">
<b>Serialize</b>
<span class="sf-toolbar-status">{{ collector.data.serialize|length }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Deserialize</b>
<span class="sf-toolbar-status">{{ collector.data.deserialize|length }}</span>
</div>
</div>
<div>
<div class="sf-toolbar-info-piece">
<b>Encode</b>
<span class="sf-toolbar-status">{{ collector.data.encode|length }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Decode</b>
<span class="sf-toolbar-status">{{ collector.data.decode|length }}</span>
</div>
</div>
<div>
<div class="sf-toolbar-info-piece">
<b>Normalize</b>
<span class="sf-toolbar-status">{{ collector.data.normalize|length }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Denormalize</b>
<span class="sf-toolbar-status">{{ collector.data.denormalize|length }}</span>
</div>
</div>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label {{ not collector.handledCount ? 'disabled' }}">
<span class="icon">{{ source('@WebProfiler/Icon/serializer.svg') }}</span>
<strong>Serializer</strong>
</span>
{% endblock %}
{% block panel %}
<h2>Serializer</h2>
<div class="sf-serializer sf-reset">
{% if not collector.handledCount %}
<div class="empty empty-panel">
<p>Nothing was handled by the serializer.</p>
</div>
{% else %}
<div class="metrics">
<div class="metric">
<span class="value">{{ collector.handledCount }}</span>
<span class="label">Handled</span>
</div>
<div class="metric">
<span class="value">{{ '%.2f'|format(collector.totalTime * 1000) }} <span class="unit">ms</span></span>
<span class="label">Total time</span>
</div>
</div>
<div class="sf-tabs">
{% for serializer in collector.serializerNames %}
{{ _self.render_serializer_tab(collector, serializer) }}
{% endfor %}
</div>
{% endif %}
</div>
{% endblock %}
{% macro render_serializer_tab(collector, serializer) %}
<div class="tab">
<h3 class="tab-title">{{ serializer }} <span class="badge">{{ collector.handledCount(serializer) }}</h3>
<div class="tab-content">
<div class="sf-tabs">
{{ _self.render_serialize_tab(collector.data(serializer), true, serializer) }}
{{ _self.render_serialize_tab(collector.data(serializer), false, serializer) }}
{{ _self.render_normalize_tab(collector.data(serializer), true, serializer) }}
{{ _self.render_normalize_tab(collector.data(serializer), false, serializer) }}
{{ _self.render_encode_tab(collector.data(serializer), true, serializer) }}
{{ _self.render_encode_tab(collector.data(serializer), false, serializer) }}
</div>
</div>
</div>
{% endmacro %}
{% macro render_serialize_tab(collectorData, serialize, serializer) %}
{% set data = serialize ? collectorData.serialize : collectorData.deserialize %}
{% set cellPrefix = serialize ? 'serialize' : 'deserialize' %}
<div class="tab {{ not data ? 'disabled' }}">
<h3 class="tab-title">{{ serialize ? 'serialize' : 'deserialize' }} <span class="badge">{{ data|length }}</h3>
<div class="tab-content">
{% if not data|length %}
<div class="empty">
<p>Nothing was {{ serialize ? 'serialized' : 'deserialized' }}.</p>
</div>
{% else %}
<table>
<thead>
<tr>
<th>Data</th>
<th>Context</th>
<th>Normalizer</th>
<th>Encoder</th>
<th>Time</th>
<th>Caller</th>
</tr>
</thead>
<tbody>
{% for item in data %}
<tr>
<td>{{ _self.render_data_cell(item, loop.index, cellPrefix, serializer) }}</td>
<td>{{ _self.render_context_cell(item, loop.index, cellPrefix, serializer) }}</td>
<td>{{ _self.render_normalizer_cell(item, loop.index, cellPrefix, serializer) }}</td>
<td>{{ _self.render_encoder_cell(item, loop.index, cellPrefix, serializer) }}</td>
<td>{{ _self.render_time_cell(item) }}</td>
<td>{{ _self.render_caller_cell(item, loop.index, cellPrefix, serializer) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
{% endmacro %}
{% macro render_caller_cell(item, index, method, serializer) %}
{% if item.caller is defined %}
{% set trace_id = 'sf-trace-' ~ serializer ~ '-' ~ method ~ '-' ~ index %}
<span class="metadata">
{% set caller = item.caller %}
{% if caller.line %}
{% set link = caller.file|file_link(caller.line) %}
{% if link %}
<a href="{{ link }}" title="{{ caller.file }}">{{ caller.name }}</a>
{% else %}
<abbr title="{{ caller.file }}">{{ caller.name }}</abbr>
{% endif %}
{% else %}
{{ caller.name }}
{% endif %}
line <a class="text-small sf-toggle" data-toggle-selector="#{{ trace_id }}">{{ caller.line }}</a>
</span>
<div class="sf-serializer-compact hidden" id="{{ trace_id }}">
<div class="trace">
{{ caller.file|file_excerpt(caller.line)|replace({
'#DD0000': 'var(--highlight-string)',
'#007700': 'var(--highlight-keyword)',
'#0000BB': 'var(--highlight-default)',
'#FF8000': 'var(--highlight-comment)'
})|raw }}
</div>
</div>
{% endif %}
{% endmacro %}
{% macro render_normalize_tab(collectorData, normalize, serializer) %}
{% set data = normalize ? collectorData.normalize : collectorData.denormalize %}
{% set cellPrefix = normalize ? 'normalize' : 'denormalize' %}
<div class="tab {{ not data ? 'disabled' }}">
<h3 class="tab-title">{{ normalize ? 'normalize' : 'denormalize' }} <span class="badge">{{ data|length }}</h3>
<div class="tab-content">
{% if not data|length %}
<div class="empty">
<p>Nothing was {{ normalize ? 'normalized' : 'denormalized' }}.</p>
</div>
{% else %}
<table>
<thead>
<tr>
<th>Data</th>
<th>Context</th>
<th>Normalizer</th>
<th>Time</th>
<th>Caller</th>
</tr>
</thead>
<tbody>
{% for item in data %}
<tr>
<td>{{ _self.render_data_cell(item, loop.index, cellPrefix, serializer) }}</td>
<td>{{ _self.render_context_cell(item, loop.index, cellPrefix, serializer) }}</td>
<td>{{ _self.render_normalizer_cell(item, loop.index, cellPrefix, serializer) }}</td>
<td>{{ _self.render_time_cell(item) }}</td>
<td>{{ _self.render_caller_cell(item, loop.index, cellPrefix, serializer) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
{% endmacro %}
{% macro render_encode_tab(collectorData, encode, serializer) %}
{% set data = encode ? collectorData.encode : collectorData.decode %}
{% set cellPrefix = encode ? 'encode' : 'decode' %}
<div class="tab {{ not data ? 'disabled' }}">
<h3 class="tab-title">{{ encode ? 'encode' : 'decode' }} <span class="badge">{{ data|length }}</h3>
<div class="tab-content">
{% if not data|length %}
<div class="empty">
<p>Nothing was {{ encode ? 'encoded' : 'decoded' }}.</p>
</div>
{% else %}
<table>
<thead>
<tr>
<th>Data</th>
<th>Context</th>
<th>Encoder</th>
<th>Time</th>
<th>Caller</th>
</tr>
</thead>
<tbody>
{% for item in data %}
<tr>
<td>{{ _self.render_data_cell(item, loop.index, cellPrefix, serializer) }}</td>
<td>{{ _self.render_context_cell(item, loop.index, cellPrefix, serializer) }}</td>
<td>{{ _self.render_encoder_cell(item, loop.index, cellPrefix, serializer) }}</td>
<td>{{ _self.render_time_cell(item) }}</td>
<td>{{ _self.render_caller_cell(item, loop.index, cellPrefix, serializer) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
{% endmacro %}
{% macro render_data_cell(item, index, method, serializer) %}
{% set data_id = 'data-' ~ serializer ~ '-' ~ method ~ '-' ~ index %}
<span class="nowrap">{{ item.dataType }}</span>
<div>
<a class="btn btn-link text-small sf-toggle" data-toggle-selector="#{{ data_id }}" data-toggle-alt-content="Hide contents">Show contents</a>
<div id="{{ data_id }}" class="context sf-toggle-content sf-toggle-hidden">
{{ profiler_dump(item.data) }}
</div>
</div>
{% endmacro %}
{% macro render_context_cell(item, index, method, serializer) %}
{% set context_id = 'context-' ~ serializer ~ '-' ~ method ~ '-' ~ index %}
{% if item.type %}
<span class="nowrap">Type: {{ item.type }}</span>
<div class="nowrap">Format: {{ item.format ? item.format : 'none' }}</div>
{% else %}
<span class="nowrap">Format: {{ item.format ? item.format : 'none' }}</span>
{% endif %}
<div>
<a class="btn btn-link text-small sf-toggle" data-toggle-selector="#{{ context_id }}" data-toggle-alt-content="Hide context">Show context</a>
<div id="{{ context_id }}" class="context sf-toggle-content sf-toggle-hidden">
{{ profiler_dump(item.context) }}
</div>
</div>
{% endmacro %}
{% macro render_normalizer_cell(item, index, method, serializer) %}
{% set nested_normalizers_id = 'nested-normalizers-' ~ serializer ~ '-' ~ method ~ '-' ~ index %}
{% if item.normalizer is defined %}
<span class="nowrap"><a href="{{ item.normalizer.file|file_link(item.normalizer.line) }}" title="{{ item.normalizer.file }}">{{ item.normalizer.class }}</a> ({{ '%.2f'|format(item.normalizer.time * 1000) }} ms)</span>
{% endif %}
{% if item.normalization|length > 1 %}
<div>
<a class="btn btn-link text-small sf-toggle" data-toggle-selector="#{{ nested_normalizers_id }}" data-toggle-alt-content="Hide nested normalizers">Show nested normalizers</a>
<div id="{{ nested_normalizers_id }}" class="context sf-toggle-content sf-toggle-hidden">
<ul class="text-small" style="line-height:80%;margin-top:10px">
{% for normalizer in item.normalization %}
<li><span class="nowrap">x{{ normalizer.calls }} <a href="{{ normalizer.file|file_link(normalizer.line) }}" title="{{ normalizer.file }}">{{ normalizer.class }}</a> ({{ '%.2f'|format(normalizer.time * 1000) }} ms)</span></li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% endmacro %}
{% macro render_encoder_cell(item, index, method, serializer) %}
{% set nested_encoders_id = 'nested-encoders-' ~ serializer ~ '-' ~ method ~ '-' ~ index %}
{% if item.encoder is defined %}
<span class="nowrap"><a href="{{ item.encoder.file|file_link(item.encoder.line) }}" title="{{ item.encoder.file }}">{{ item.encoder.class }}</a> ({{ '%.2f'|format(item.encoder.time * 1000) }} ms)</span>
{% endif %}
{% if item.encoding|length > 1 %}
<div>
<a class="btn btn-link text-small sf-toggle" data-toggle-selector="#{{ nested_encoders_id }}" data-toggle-alt-content="Hide nested encoders">Show nested encoders</a>
<div id="{{ nested_encoders_id }}" class="context sf-toggle-content sf-toggle-hidden">
<ul class="text-small" style="line-height:80%;margin-top:10px">
{% for encoder in item.encoding %}
<li><span class="nowrap">x{{ encoder.calls }} <a href="{{ encoder.file|file_link(encoder.line) }}" title="{{ encoder.file }}">{{ encoder.class }}</a> ({{ '%.2f'|format(encoder.time * 1000) }} ms)</span></li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% endmacro %}
{% macro render_time_cell(item) %}
<span class="nowrap">{{ '%.2f'|format(item.time * 1000) }} ms</span>
{% endmacro %}

View File

@@ -0,0 +1,67 @@
/* Legend */
.sf-profiler-timeline .legends .timeline-category {
border: none;
background: none;
border-left: 1em solid transparent;
line-height: 1em;
margin: 0 1em 0 0;
padding: 0 0.5em;
display: none;
opacity: 0.5;
}
.sf-profiler-timeline .legends .timeline-category.active {
opacity: 1;
}
.sf-profiler-timeline .legends .timeline-category.present {
display: inline-block;
}
.timeline-graph {
margin: 1em 0;
width: 100%;
background-color: var(--table-background);
border: 1px solid var(--table-border-color);
}
/* Typography */
.timeline-graph .timeline-label {
font-family: var(--font-sans-serif);
font-size: 12px;
line-height: 12px;
font-weight: normal;
fill: var(--color-text);
}
.timeline-graph .timeline-label .timeline-sublabel {
margin-left: 1em;
fill: var(--color-muted);
}
.timeline-graph .timeline-subrequest,
.timeline-graph .timeline-border {
fill: none;
stroke: var(--table-border-color);
stroke-width: 1px;
}
.timeline-graph .timeline-subrequest {
fill: url(#subrequest);
fill-opacity: 0.5;
}
.timeline-subrequest-pattern {
fill: var(--gray-200);
}
.theme-dark .timeline-subrequest-pattern {
fill: var(--gray-600);
}
/* Timeline periods */
.timeline-graph .timeline-period {
stroke-width: 0;
}

View File

@@ -0,0 +1,249 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block head %}
{{ parent() }}
<style>
#timeline-control {
background: var(--table-background);
box-shadow: var(--shadow);
margin: 1em 0;
padding: 10px;
}
#timeline-control label {
font-weight: bold;
margin-right: 1em;
}
#timeline-control input {
background: var(--metric-value-background);
font-size: 16px;
padding: 4px;
text-align: right;
width: 5em;
}
#timeline-control .help {
margin-left: 1em;
}
.sf-profiler-timeline .legends {
font-size: 12px;
line-height: 1.5em;
}
.sf-profiler-timeline .legends button {
color: var(--color-text);
}
.sf-profiler-timeline + p.help {
margin-top: 0;
}
</style>
{% endblock %}
{% block toolbar %}
{% set has_time_events = collector.events|length > 0 %}
{% set total_time = has_time_events ? '%.0f'|format(collector.duration) : 'n/a' %}
{% set initialization_time = collector.events|length ? '%.0f'|format(collector.inittime) : 'n/a' %}
{% set status_color = has_time_events and collector.duration > 1000 ? 'yellow' %}
{% set icon %}
{{ source('@WebProfiler/Icon/time.svg') }}
<span class="sf-toolbar-value">{{ total_time }}</span>
<span class="sf-toolbar-label">ms</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Total time</b>
<span>{{ total_time }} ms</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Initialization time</b>
<span>{{ initialization_time }} ms</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }}
{% endblock %}
{% block menu %}
<span class="label">
<span class="icon">{{ source('@WebProfiler/Icon/time.svg') }}</span>
<strong>Performance</strong>
</span>
{% endblock %}
{% block panel %}
{% set has_time_events = collector.events|length > 0 %}
<h2>Performance metrics</h2>
<div class="metrics">
<div class="metric-group">
<div class="metric">
<span class="value">{{ '%.0f'|format(collector.duration) }} <span class="unit">ms</span></span>
<span class="label">Total execution time</span>
</div>
<div class="metric">
<span class="value">{{ '%.0f'|format(collector.inittime) }} <span class="unit">ms</span></span>
<span class="label">Symfony initialization</span>
</div>
</div>
{% if profile.collectors.memory %}
<div class="metric-divider"></div>
<div class="metric">
<span class="value">{{ '%.2f'|format(profile.collectors.memory.memory / 1024 / 1024) }} <span class="unit">MiB</span></span>
<span class="label">Peak memory usage</span>
</div>
{% endif %}
{% if profile.children|length > 0 %}
<div class="metric-divider"></div>
<div class="metric-group">
<div class="metric">
<span class="value">{{ profile.children|length }}</span>
<span class="label">Sub-{{ profile_type|title }}{{ profile.children|length > 1 ? 's' }}</span>
</div>
{% set subrequests_time = has_time_events
? profile.children|reduce((total, child) => total + child.getcollector('time').events.__section__.duration, 0)
: 'n/a' %}
<div class="metric">
<span class="value">{{ subrequests_time }} <span class="unit">ms</span></span>
<span class="label">Sub-{{ profile_type|title }}{{ profile.children|length > 1 ? 's' }} time</span>
</div>
</div>
{% endif %}
</div>
<h2>Execution timeline</h2>
{% if not collector.isStopwatchInstalled() %}
<div class="empty empty-panel">
<p>The Stopwatch component is not installed. If you want to see timing events, run: <code>composer require symfony/stopwatch</code>.</p>
</div>
{% elseif collector.events is empty %}
<div class="empty">
<p>No timing events have been recorded. Check that symfony/stopwatch is installed and debugging enabled in the kernel.</p>
</div>
{% else %}
{{ block('panelContent') }}
{% endif %}
{% endblock %}
{% block panelContent %}
<form id="timeline-control" action="" method="get">
<input type="hidden" name="panel" value="time">
<label for="threshold">Threshold</label>
<input type="number" name="threshold" id="threshold" value="1" min="0" placeholder="1.1"> ms
<span class="help">(timeline only displays events with a duration longer than this threshold)</span>
</form>
{% if profile.parent %}
<h3 class="dump-inline">
Sub-{{ profile_type|title }} {{ profiler_dump(profile.getcollector('request').requestattributes.get('_controller')) }}
<small>
{{ collector.events.__section__.duration }} ms
<a class="newline" href="{{ path('_profiler', { token: profile.parent.token, panel: 'time' }) }}">Return to parent {{ profile_type }}</a>
</small>
</h3>
{% elseif profile.children|length > 0 %}
<h3>
Main {{ profile_type|title }} <small>{{ collector.events.__section__.duration }} ms</small>
</h3>
{% endif %}
{{ _self.display_timeline(token, collector.events, collector.events.__section__.origin) }}
{% if profile.children|length %}
<p class="help">Note: sections with a striped background correspond to sub-{{ profile_type }}s.</p>
<h3>Sub-{{ profile_type }}s <small>({{ profile.children|length }})</small></h3>
{% for child in profile.children %}
{% set events = child.getcollector('time').events %}
<h4>
<a href="{{ path('_profiler', { token: child.token, panel: 'time' }) }}">{{ child.getcollector('request').identifier }}</a>
<small>{{ events.__section__.duration }} ms</small>
</h4>
{{ _self.display_timeline(child.token, events, collector.events.__section__.origin) }}
{% endfor %}
{% endif %}
<svg id="timeline-template" width="0" height="0">
<defs>
<pattern id="subrequest" class="timeline-subrequest-pattern" patternUnits="userSpaceOnUse" width="20" height="20" viewBox="0 0 40 40">
<path d="M0 40L40 0H20L0 20M40 40V20L20 40"/>
</pattern>
</defs>
</svg>
<style>
{{ include('@WebProfiler/Collector/time.css.twig') }}
</style>
<script>
{{ source('@WebProfiler/Collector/time.js') }}
</script>
{% endblock %}
{% macro dump_request_data(token, events, origin) %}
{% autoescape 'js' %}
{
id: "{{ token }}",
left: {{ "%F"|format(events.__section__.origin - origin) }},
end: "{{ '%F'|format(events.__section__.endtime) }}",
events: [ {{ _self.dump_events(events) }} ],
}
{% endautoescape %}
{% endmacro %}
{% macro dump_events(events) %}
{% autoescape 'js' %}
{% for name, event in events %}
{% if '__section__' != name %}
{
name: "{{ name }}",
category: "{{ event.category }}",
origin: {{ "%F"|format(event.origin) }},
starttime: {{ "%F"|format(event.starttime) }},
endtime: {{ "%F"|format(event.endtime) }},
duration: {{ "%F"|format(event.duration) }},
memory: {{ "%.1F"|format(event.memory / 1024 / 1024) }},
elements: {},
periods: [
{%- for period in event.periods -%}
{
start: {{ "%F"|format(period.starttime) }},
end: {{ "%F"|format(period.endtime) }},
duration: {{ "%F"|format(period.duration) }},
elements: {}
},
{%- endfor -%}
],
},
{% endif %}
{% endfor %}
{% endautoescape %}
{% endmacro %}
{% macro display_timeline(token, events, origin) %}
<div class="sf-profiler-timeline">
<div id="legend-{{ token }}" class="legends"></div>
<svg id="timeline-{{ token }}" class="timeline-graph"></svg>
<script>{% autoescape 'js' %}
window.addEventListener('load', function onLoad() {
const theme = new Theme();
new TimelineEngine(
theme,
new SvgRenderer(document.getElementById('timeline-{{ token }}')),
new Legend(document.getElementById('legend-{{ token }}'), theme),
document.getElementById('threshold'),
{{ _self.dump_request_data(token, events, origin) }}
);
});
{% endautoescape %}</script>
</div>
{% endmacro %}

View File

@@ -0,0 +1,457 @@
'use strict';
class TimelineEngine {
/**
* @param {Theme} theme
* @param {Renderer} renderer
* @param {Legend} legend
* @param {Element} threshold
* @param {Object} request
* @param {Number} eventHeight
* @param {Number} horizontalMargin
*/
constructor(theme, renderer, legend, threshold, request, eventHeight = 36, horizontalMargin = 10) {
this.theme = theme;
this.renderer = renderer;
this.legend = legend;
this.threshold = threshold;
this.request = request;
this.scale = renderer.width / request.end;
this.eventHeight = eventHeight;
this.horizontalMargin = horizontalMargin;
this.labelY = Math.round(this.eventHeight * 0.48);
this.periodY = Math.round(this.eventHeight * 0.66);
this.FqcnMatcher = /\\([^\\]+)$/i;
this.origin = null;
this.createEventElements = this.createEventElements.bind(this);
this.createBackground = this.createBackground.bind(this);
this.createPeriod = this.createPeriod.bind(this);
this.render = this.render.bind(this);
this.renderEvent = this.renderEvent.bind(this);
this.renderPeriod = this.renderPeriod.bind(this);
this.onResize = this.onResize.bind(this);
this.isActive = this.isActive.bind(this);
this.threshold.addEventListener('change', this.render);
this.legend.addEventListener('change', this.render);
window.addEventListener('resize', this.onResize);
this.createElements();
this.render();
}
onResize() {
this.renderer.measure();
this.setScale(this.renderer.width / this.request.end);
}
setScale(scale) {
if (scale !== this.scale) {
this.scale = scale;
this.render();
}
}
createElements() {
this.origin = this.renderer.setFullVerticalLine(this.createBorder(), 0);
this.renderer.add(this.origin);
this.request.events
.filter(event => event.category === 'section')
.map(this.createBackground)
.forEach(this.renderer.add);
this.request.events
.map(this.createEventElements)
.forEach(this.renderer.add);
}
createBackground(event) {
const subrequest = event.name === '__section__.child';
const background = this.renderer.create('rect', subrequest ? 'timeline-subrequest' : 'timeline-border');
event.elements = Object.assign(event.elements || {}, { background });
return background;
}
createEventElements(event) {
const { name, category, duration, memory, periods } = event;
const border = this.renderer.setFullHorizontalLine(this.createBorder(), 0);
const lines = periods.map(period => this.createPeriod(period, category));
const label = this.createLabel(this.getShortName(name), duration, memory, periods[0]);
const title = this.renderer.createTitle(name);
const group = this.renderer.group([title, border, label].concat(lines), this.theme.getCategoryColor(event.category));
event.elements = Object.assign(event.elements || {}, { group, label, border });
this.legend.add(event.category)
return group;
}
createLabel(name, duration, memory, period) {
const label = this.renderer.createText(name, period.start * this.scale, this.labelY, 'timeline-label');
const sublabel = this.renderer.createTspan(` ${duration} ms / ${memory} MiB`, 'timeline-sublabel');
label.appendChild(sublabel);
return label;
}
createPeriod(period, category) {
const timeline = this.renderer.createPath(null, 'timeline-period', this.theme.getCategoryColor(category));
period.draw = category === 'section' ? this.renderer.setSectionLine : this.renderer.setPeriodLine;
period.elements = Object.assign(period.elements || {}, { timeline });
return timeline;
}
createBorder() {
return this.renderer.createPath(null, 'timeline-border');
}
isActive(event) {
const { duration, category } = event;
return duration >= this.threshold.value && this.legend.isActive(category);
}
render() {
const events = this.request.events.filter(this.isActive);
const width = this.renderer.width + this.horizontalMargin * 2;
const height = this.eventHeight * events.length;
// Set view box
this.renderer.setViewBox(-this.horizontalMargin, 0, width, height);
// Show 0ms origin
this.renderer.setFullVerticalLine(this.origin, 0);
// Render all events
this.request.events.forEach(event => this.renderEvent(event, events.indexOf(event)));
}
renderEvent(event, index) {
const { name, category, duration, memory, periods, elements } = event;
const { group, label, border, background } = elements;
const visible = index >= 0;
group.setAttribute('visibility', visible ? 'visible' : 'hidden');
if (background) {
background.setAttribute('visibility', visible ? 'visible' : 'hidden');
if (visible) {
const [min, max] = this.getEventLimits(event);
this.renderer.setFullRectangle(background, min * this.scale, max * this.scale);
}
}
if (visible) {
// Position the group
group.setAttribute('transform', `translate(0, ${index * this.eventHeight})`);
// Update top border
this.renderer.setFullHorizontalLine(border, 0);
// render label and ensure it doesn't escape the viewport
this.renderLabel(label, event);
// Update periods
periods.forEach(this.renderPeriod);
}
}
renderLabel(label, event) {
const width = this.getLabelWidth(label);
const [min, max] = this.getEventLimits(event);
const alignLeft = (min * this.scale) + width <= this.renderer.width;
label.setAttribute('x', (alignLeft ? min : max) * this.scale);
label.setAttribute('text-anchor', alignLeft ? 'start' : 'end');
}
renderPeriod(period) {
const { elements, start, duration } = period;
period.draw(elements.timeline, start * this.scale, this.periodY, Math.max(duration * this.scale, 1));
}
getLabelWidth(label) {
if (typeof label.width === 'undefined') {
label.width = label.getBBox().width;
}
return label.width;
}
getEventLimits(event) {
if (typeof event.limits === 'undefined') {
const { periods } = event;
event.limits = [
periods[0].start,
periods[periods.length - 1].end
];
}
return event.limits;
}
getShortName(name) {
const matches = this.FqcnMatcher.exec(name);
if (matches) {
return matches[1];
}
return name;
}
}
class Legend {
constructor(element, theme) {
this.element = element;
this.theme = theme;
this.toggle = this.toggle.bind(this);
this.createCategory = this.createCategory.bind(this);
this.categories = [];
this.theme.getDefaultCategories().forEach(this.createCategory);
}
add(category) {
this.get(category).classList.add('present');
}
createCategory(category) {
const element = document.createElement('button');
element.className = `timeline-category active`;
element.style.borderColor = this.theme.getCategoryColor(category);
element.innerText = category;
element.value = category;
element.type = 'button';
element.addEventListener('click', this.toggle);
this.element.appendChild(element);
this.categories.push(element);
return element;
}
toggle(event) {
event.target.classList.toggle('active');
this.emit('change');
}
isActive(category) {
return this.get(category).classList.contains('active');
}
get(category) {
return this.categories.find(element => element.value === category) || this.createCategory(category);
}
emit(name) {
this.element.dispatchEvent(new Event(name));
}
addEventListener(name, callback) {
this.element.addEventListener(name, callback);
}
removeEventListener(name, callback) {
this.element.removeEventListener(name, callback);
}
}
class SvgRenderer {
/**
* @param {SVGElement} element
*/
constructor(element) {
this.ns = 'http://www.w3.org/2000/svg';
this.width = null;
this.viewBox = {};
this.element = element;
this.add = this.add.bind(this);
this.setViewBox(0, 0, 0, 0);
this.measure();
}
setViewBox(x, y, width, height) {
this.viewBox = { x, y, width, height };
this.element.setAttribute('viewBox', `${x} ${y} ${width} ${height}`);
}
measure() {
this.width = this.element.getBoundingClientRect().width;
}
add(element) {
this.element.appendChild(element);
}
group(elements, className) {
const group = this.create('g', className);
elements.forEach(element => group.appendChild(element));
return group;
}
setHorizontalLine(element, x, y, width) {
element.setAttribute('d', `M${x},${y} h${width}`);
return element;
}
setVerticalLine(element, x, y, height) {
element.setAttribute('d', `M${x},${y} v${height}`);
return element;
}
setFullHorizontalLine(element, y) {
return this.setHorizontalLine(element, this.viewBox.x, y, this.viewBox.width);
}
setFullVerticalLine(element, x) {
return this.setVerticalLine(element, x, this.viewBox.y, this.viewBox.height);
}
setFullRectangle(element, min, max) {
element.setAttribute('x', min);
element.setAttribute('y', this.viewBox.y);
element.setAttribute('width', max - min);
element.setAttribute('height', this.viewBox.height);
}
setSectionLine(element, x, y, width, height = 4, markerSize = 6) {
const totalHeight = height + markerSize;
const maxMarkerWidth = Math.min(markerSize, width / 2);
const widthWithoutMarker = Math.max(0, width - (maxMarkerWidth * 2));
element.setAttribute('d', `M${x},${y + totalHeight} v${-totalHeight} h${width} v${totalHeight} l${-maxMarkerWidth} ${-markerSize} h${-widthWithoutMarker} Z`);
}
setPeriodLine(element, x, y, width, height = 4, markerWidth = 2, markerHeight = 4) {
const totalHeight = height + markerHeight;
const maxMarkerWidth = Math.min(markerWidth, width);
element.setAttribute('d', `M${x + maxMarkerWidth},${y + totalHeight} h${-maxMarkerWidth} v${-totalHeight} h${width} v${height} h${maxMarkerWidth-width}Z`);
}
createText(content, x, y, className) {
const element = this.create('text', className);
element.setAttribute('x', x);
element.setAttribute('y', y);
element.textContent = content;
return element;
}
createTspan(content, className) {
const element = this.create('tspan', className);
element.textContent = content;
return element;
}
createTitle(content) {
const element = this.create('title');
element.textContent = content;
return element;
}
createPath(path = null, className = null, color = null) {
const element = this.create('path', className);
if (path) {
element.setAttribute('d', path);
}
if (color) {
element.setAttribute('fill', color);
}
return element;
}
create(name, className = null) {
const element = document.createElementNS(this.ns, name);
if (className) {
element.setAttribute('class', className);
}
return element;
}
}
class Theme {
constructor(element) {
this.reservedCategoryColors = {
'default': '#737373',
'section': '#a3a3a3',
'event_listener': '#54aeff',
'template': '#4ac26b',
'doctrine': '#fd8c73',
'messenger_middleware': '#ff8182',
'controller.argument_value_resolver': '#c297ff',
'http_client': '#d4a72c',
};
this.customCategoryColors = [
'#d4a72c', // dark yellow
'#ffaba8', // light red
'#e6af05', // yellow
'#6fdd8b', // light green
'#76e3ea', // cyan
'#a475f9', // light purple
];
this.getCategoryColor = this.getCategoryColor.bind(this);
this.getDefaultCategories = this.getDefaultCategories.bind(this);
}
getDefaultCategories() {
return Object.keys(this.reservedCategoryColors);
}
getCategoryColor(category) {
return this.reservedCategoryColors[category] || this.getRandomColor(category);
}
getRandomColor(category) {
// instead of pure randomness, colors are assigned deterministically based on the
// category name, to ensure that each custom category always displays the same color
return this.customCategoryColors[this.hash(category) % this.customCategoryColors.length];
}
// copied from https://github.com/darkskyapp/string-hash
hash(string) {
var hash = 5381;
var i = string.length;
while(i) {
hash = (hash * 33) ^ string.charCodeAt(--i);
}
return hash >>> 0;
}
}

View File

@@ -0,0 +1,227 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% if collector.messages|length %}
{% set icon %}
{{ source('@WebProfiler/Icon/translation.svg') }}
{% set status_color = collector.countMissings ? 'red' : collector.countFallbacks ? 'yellow' %}
{% set error_count = collector.countMissings + collector.countFallbacks %}
<span class="sf-toolbar-value">{{ error_count ?: collector.countDefines }}</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Default locale</b>
<span class="sf-toolbar-status">
{{ collector.locale|default('-') }}
</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Missing messages</b>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.countMissings ? 'red' }}">
{{ collector.countMissings }}
</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Fallback messages</b>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.countFallbacks ? 'yellow' }}">
{{ collector.countFallbacks }}
</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Defined messages</b>
<span class="sf-toolbar-status">{{ collector.countDefines }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label label-status-{{ collector.countMissings ? 'error' : collector.countFallbacks ? 'warning' }} {{ collector.messages is empty ? 'disabled' }}">
<span class="icon">{{ source('@WebProfiler/Icon/translation.svg') }}</span>
<strong>Translation</strong>
{% if collector.countMissings or collector.countFallbacks %}
{% set error_count = collector.countMissings + collector.countFallbacks %}
<span class="count">
<span>{{ error_count }}</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
<h2>Translation</h2>
<div class="metrics">
<div class="metric">
<span class="value">{{ collector.locale|default('-') }}</span>
<span class="label">Default locale</span>
</div>
<div class="metric">
<span class="value">{{ collector.fallbackLocales|join(', ')|default('-') }}</span>
<span class="label">Fallback locale{{ collector.fallbackLocales|length != 1 ? 's' }}</span>
</div>
</div>
<h2>Messages</h2>
{% if collector.messages is empty %}
<div class="empty empty-panel">
<p>No translations have been called.</p>
</div>
{% else %}
{% block messages %}
{# sort translation messages in groups #}
{% set messages_defined, messages_missing, messages_fallback = [], [], [] %}
{% for message in collector.messages %}
{% if message.state == constant('Symfony\\Component\\Translation\\DataCollectorTranslator::MESSAGE_DEFINED') %}
{% set messages_defined = messages_defined|merge([message]) %}
{% elseif message.state == constant('Symfony\\Component\\Translation\\DataCollectorTranslator::MESSAGE_MISSING') %}
{% set messages_missing = messages_missing|merge([message]) %}
{% elseif message.state == constant('Symfony\\Component\\Translation\\DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK') %}
{% set messages_fallback = messages_fallback|merge([message]) %}
{% endif %}
{% endfor %}
<div class="sf-tabs">
<div class="tab {{ collector.countMissings == 0 ? 'active' }}">
<h3 class="tab-title">Defined <span class="badge">{{ collector.countDefines }}</span></h3>
<div class="tab-content">
<p class="help">
These messages are correctly translated into the given locale.
</p>
{% if messages_defined is empty %}
<div class="empty">
<p>None of the used translation messages are defined for the given locale.</p>
</div>
{% else %}
{% block defined_messages %}
{{ _self.render_table(messages_defined) }}
{% endblock %}
{% endif %}
</div>
</div>
<div class="tab">
<h3 class="tab-title">Fallback <span class="badge {{ collector.countFallbacks ? 'status-warning' }}">{{ collector.countFallbacks }}</span></h3>
<div class="tab-content">
<p class="help">
These messages are not available for the given locale
but Symfony found them in the fallback locale catalog.
</p>
{% if messages_fallback is empty %}
<div class="empty">
<p>No fallback translation messages were used.</p>
</div>
{% else %}
{% block fallback_messages %}
{{ _self.render_table(messages_fallback, true) }}
{% endblock %}
{% endif %}
</div>
</div>
<div class="tab {{ collector.countMissings > 0 ? 'active' }}">
<h3 class="tab-title">Missing <span class="badge {{ collector.countMissings ? 'status-error' }}">{{ collector.countMissings }}</span></h3>
<div class="tab-content">
<p class="help">
These messages are not available for the given locale and cannot
be found in the fallback locales. Add them to the translation
catalogue to avoid Symfony outputting untranslated contents.
</p>
{% if messages_missing is empty %}
<div class="empty">
<p>There are no messages of this category.</p>
</div>
{% else %}
{% block missing_messages %}
{{ _self.render_table(messages_missing) }}
{% endblock %}
{% endif %}
</div>
</div>
</div>
{% endblock messages %}
{% endif %}
{% if collector.globalParameters|default([]) %}
<h2>Global parameters</h2>
<table>
<thead>
<tr>
<th>Message ID</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for id, value in collector.globalParameters %}
<tr>
<td class="font-normal text-small nowrap">{{ id }}</td>
<td class="font-normal text-small nowrap">{{ profiler_dump(value) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endblock %}
{% macro render_table(messages, is_fallback) %}
<table>
<thead>
<tr>
<th>Locale</th>
{% if is_fallback %}
<th>Fallback locale</th>
{% endif %}
<th>Domain</th>
<th>Times used</th>
<th>Message ID</th>
<th>Message Preview</th>
</tr>
</thead>
<tbody>
{% for message in messages %}
<tr>
<td class="font-normal text-small nowrap">{{ message.locale }}</td>
{% if is_fallback %}
<td class="font-normal text-small nowrap">{{ message.fallbackLocale|default('-') }}</td>
{% endif %}
<td class="font-normal text-small text-bold nowrap">{{ message.domain }}</td>
<td class="font-normal text-small nowrap">{{ message.count }}</td>
<td>
<span class="{{ message.id|length < 64 ? 'nowrap' }}">{{ message.id }}</span>
{% if message.transChoiceNumber is not null %}
<small class="newline">(pluralization is used)</small>
{% endif %}
{% if message.parameters|length > 0 %}
<button class="btn-link newline text-small sf-toggle" data-toggle-selector="#parameters-{{ loop.index }}" data-toggle-alt-content="Hide parameters">Show parameters</button>
<div id="parameters-{{ loop.index }}" class="hidden">
{% for parameters in message.parameters %}
{{ profiler_dump(parameters, maxDepth=1) }}
{% endfor %}
</div>
{% endif %}
</td>
<td class="prewrap">{{ message.translation }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endmacro %}

View File

@@ -0,0 +1,168 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block head %}
{{ parent() }}
<style>
#twig-dump pre {
font-size: var(--font-size-monospace);
line-height: 1.7;
background-color: var(--page-background);
border: var(--border);
border-radius: 6px;
padding: 15px;
box-shadow: 0 0 1px rgba(128, 128, 128, .2);
}
#twig-dump span {
border-radius: 2px;
padding: 1px 2px;
}
#twig-dump .status-error { background: transparent; color: var(--color-error); }
#twig-dump .status-warning { background: rgba(240, 181, 24, 0.3); }
#twig-dump .status-success { background: rgba(100, 189, 99, 0.2); }
#twig-dump .status-info { background: var(--info-background); }
.theme-dark #twig-dump .status-warning { color: var(--yellow-200); }
.theme-dark #twig-dump .status-success { color: var(--green-200); }
#twig-table tbody td {
position: relative;
}
#twig-table tbody td div {
margin: 0;
}
#twig-table .template-file-path {
color: var(--color-muted);
display: block;
}
</style>
{% endblock %}
{% block toolbar %}
{% set time = collector.templatecount ? '%0.0f'|format(collector.time) : 'n/a' %}
{% set icon %}
{{ source('@WebProfiler/Icon/twig.svg') }}
<span class="sf-toolbar-value">{{ time }}</span>
<span class="sf-toolbar-label">ms</span>
{% endset %}
{% set text %}
{% set template = collector.templates|keys|first %}
{% set file = collector.templatePaths[template]|default(false) %}
{% set link = file ? file|file_link(1) : false %}
<div class="sf-toolbar-info-piece">
<b>Entry View</b>
<span>
{% if link %}
<a href="{{ link }}" title="{{ file }}">
{{ template }}
</a>
{% else %}
{{ template }}
{% endif %}
</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Render Time</b>
<span>{{ time }} ms</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Template Calls</b>
<span class="sf-toolbar-status">{{ collector.templatecount }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Block Calls</b>
<span class="sf-toolbar-status">{{ collector.blockcount }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Macro Calls</b>
<span class="sf-toolbar-status">{{ collector.macrocount }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }}
{% endblock %}
{% block menu %}
<span class="label {{ 0 == collector.templateCount ? 'disabled' }}">
<span class="icon">{{ source('@WebProfiler/Icon/twig.svg') }}</span>
<strong>Twig</strong>
</span>
{% endblock %}
{% block panel %}
{% if collector.templatecount == 0 %}
<h2>Twig</h2>
<div class="empty empty-panel">
<p>No Twig templates were rendered.</p>
</div>
{% else %}
<h2>Twig Metrics</h2>
<div class="metrics">
<div class="metric">
<span class="value">{{ '%0.0f'|format(collector.time) }} <span class="unit">ms</span></span>
<span class="label">Render time</span>
</div>
<div class="metric-divider"></div>
<div class="metric-group">
<div class="metric">
<span class="value">{{ collector.templatecount }}</span>
<span class="label">Template calls</span>
</div>
<div class="metric">
<span class="value">{{ collector.blockcount }}</span>
<span class="label">Block calls</span>
</div>
<div class="metric">
<span class="value">{{ collector.macrocount }}</span>
<span class="label">Macro calls</span>
</div>
</div>
</div>
<p class="help">
Render time includes sub-requests rendering time (if any).
</p>
<h2>Rendered Templates</h2>
<table id="twig-table">
<thead>
<tr>
<th scope="col">Template Name &amp; Path</th>
<th class="num-col" scope="col">Render Count</th>
</tr>
</thead>
<tbody>
{% for template, count in collector.templates %}
<tr>
{% set file = collector.templatePaths[template]|default(false) %}
{% set link = file ? file|file_link(1) : false %}
<td class="font-normal">
{% if link %}
<a href="{{ link }}" title="{{ file }}" class="stretched-link">
{{ template }}
<span class="template-file-path">{{ file|file_relative|default(file) }}</span>
</a>
{% else %}
{{ template }}
{% endif %}
</td>
<td class="font-normal num-col">{{ count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<h2>Rendering Call Graph</h2>
<div id="twig-dump">
{{ collector.htmlcallgraph }}
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,132 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block head %}
{{ parent() }}
<style>
#collector-content .sf-validator {
margin-bottom: 2em;
}
#collector-content .sf-validator .sf-validator-context,
#collector-content .sf-validator .trace {
border: var(--border);
background: var(--base-0);
padding: 10px;
margin: 0.5em 0;
overflow: auto;
}
#collector-content .sf-validator .trace {
font-size: 12px;
}
#collector-content .sf-validator .trace li {
margin-bottom: 0;
padding: 0;
}
#collector-content .sf-validator .trace li.selected {
background: var(--highlight-selected-line);
}
</style>
{% endblock %}
{% block toolbar %}
{% if collector.violationsCount > 0 or collector.calls|length %}
{% set status_color = collector.violationsCount ? 'red' %}
{% set icon %}
{{ source('@WebProfiler/Icon/validator.svg') }}
<span class="sf-toolbar-value">
{{ collector.violationsCount ?: collector.calls|length }}
</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Validator calls</b>
<span class="sf-toolbar-status">{{ collector.calls|length }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Number of violations</b>
<span class="sf-toolbar-status {{- collector.violationsCount > 0 ? ' sf-toolbar-status-red' }}">{{ collector.violationsCount }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label {{- collector.violationsCount ? ' label-status-error' }} {{ collector.calls is empty ? 'disabled' }}">
<span class="icon">{{ source('@WebProfiler/Icon/validator.svg') }}</span>
<strong>Validator</strong>
{% if collector.violationsCount > 0 %}
<span class="count">
<span>{{ collector.violationsCount }}</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
<h2>Validator calls</h2>
{% for call in collector.calls %}
<div class="sf-validator sf-reset">
<span class="metadata">In
{% set caller = call.caller %}
{% if caller.line %}
{% set link = caller.file|file_link(caller.line) %}
{% if link %}
<a href="{{ link }}" title="{{ caller.file }}">{{ caller.name }}</a>
{% else %}
<abbr title="{{ caller.file }}">{{ caller.name }}</abbr>
{% endif %}
{% else %}
{{ caller.name }}
{% endif %}
line <button type="button" class="btn-link text-small sf-toggle" data-toggle-selector="#sf-trace-{{ loop.index0 }}">{{ caller.line }}</button> (<button type="button" class="btn-link text-small sf-toggle" data-toggle-selector="#sf-context-{{ loop.index0 }}">context</button>):
</span>
<div class="sf-validator-compact hidden" id="sf-trace-{{ loop.index0 }}">
<div class="trace">
{{ caller.file|file_excerpt(caller.line)|replace({
'#DD0000': 'var(--highlight-string)',
'#007700': 'var(--highlight-keyword)',
'#0000BB': 'var(--highlight-default)',
'#FF8000': 'var(--highlight-comment)'
})|raw }}
</div>
</div>
<div class="sf-validator-compact hidden sf-validator-context" id="sf-context-{{ loop.index0 }}">
{{ profiler_dump(call.context, maxDepth=1) }}
</div>
{% if call.violations|length %}
<table>
<thead>
<tr>
<th>Path</th>
<th>Message</th>
<th>Invalid value</th>
<th>Violation</th>
</tr>
</thead>
{% for violation in call.violations %}
<tr>
<td>{{ violation.propertyPath }}</td>
<td>{{ violation.message }}</td>
<td>{{ profiler_dump(violation.seek('invalidValue')) }}</td>
<td>{{ profiler_dump(violation) }}</td>
</tr>
{% endfor %}
</table>
{% else %}
No violations
{% endif %}
</div>
{% else %}
<div class="empty empty-panel">
<p>No calls to the validator were collected.</p>
</div>
{% endfor %}
{% endblock %}

View File

@@ -0,0 +1,359 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block stylesheets %}
{{ parent() }}
<style>
:root {
--workflow-dialog-background: var(--gray-200);
}
.theme-dark {
--workflow-dialog-background: var(--gray-900);
}
dialog {
background: var(--workflow-dialog-background);
border: none;
border-radius: 6px;
box-shadow: var(--settings-modal-shadow);
max-width: 94%;
width: 1200px;
}
dialog::backdrop {
background: linear-gradient(
45deg,
rgb(18, 18, 20, 0.4),
rgb(17, 17, 20, 0.8)
);
}
dialog[open] {
animation: scale 0.3s ease normal;
}
dialog[open]::backdrop {
animation: backdrop 0.3s ease normal;
}
dialog.hide {
animation-direction: reverse;
}
dialog h2 {
box-shadow: none !important;
color: var(--page-color);
font-size: 18px;
margin: 0 0 .5em;
}
dialog i.cancel {
cursor: pointer;
padding: 0 5px;
float: right;
}
dialog table {
background: var(--page-background);
border-collapse: collapse;
border-radius: 4px;
box-shadow: inset 0 0 0 1px var(--table-border-color);
color: var(--page-color);
margin: 0 0 1em 0;
margin-bottom: 1em;
padding: 0;
}
dialog table tr {
border: 1px solid var(--table-border-color);
padding: .35em;
}
dialog table th,
dialog table td {
border: 1px solid var(--table-border-color);
padding: .625em;
text-align: left;
word-wrap: break-word;
}
dialog table thead th {
background: var(--table-header);
border-top-left-radius: 4px;
border-top-right-radius: 4px;
font-size: .85em;
}
dialog menu {
display: none;
}
@keyframes scale {
from { transform: scale(0); }
to { transform: scale(1); }
}
@keyframes backdrop {
from { opacity: 0; }
to { opacity: 1; }
}
</style>
{% endblock %}
{% block toolbar %}
{% if collector.callsCount > 0 %}
{% set icon %}
{{ source('@WebProfiler/Icon/workflow.svg') }}
<span class="sf-toolbar-value">{{ collector.callsCount }}</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Workflow Calls</b>
<span>{{ collector.callsCount }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label {{ collector.workflows|length == 0 ? 'disabled' }}">
<span class="icon">
{{ source('@WebProfiler/Icon/workflow.svg') }}
</span>
<strong>Workflow</strong>
</span>
{% endblock %}
{% block panel %}
<h2>Workflow</h2>
{% if collector.workflows|length == 0 %}
<div class="empty empty-panel">
<p>There are no workflows configured.</p>
</div>
{% else %}
<script>
{{ source('@WebProfiler/Script/Mermaid/mermaid-flowchart-v2.min.js') }}
const isDarkMode = document.querySelector('body').classList.contains('theme-dark');
mermaid.initialize({
flowchart: {
useMaxWidth: true,
},
securityLevel: 'loose',
theme: 'base',
themeVariables: {
darkMode: isDarkMode,
fontFamily: 'var(--font-family-system)',
fontSize: 'var(--font-size-body)',
// the properties below don't support CSS variables
primaryColor: isDarkMode ? 'lightsteelblue' : 'aliceblue',
primaryTextColor: isDarkMode ? '#000' : '#000',
primaryBorderColor: isDarkMode ? 'steelblue' : 'lightsteelblue',
lineColor: isDarkMode ? '#939393' : '#d4d4d4',
secondaryColor: isDarkMode ? 'lightyellow' : 'lightyellow',
tertiaryColor: isDarkMode ? 'lightSalmon' : 'lightSalmon',
}
});
{% for name, data in collector.workflows %}
window.showNodeDetails{{ collector.hash(name) }} = function (node) {
const map = {{ data.listeners|json_encode|raw }};
showNodeDetails(node, map);
};
{% endfor %}
const showNodeDetails = function (node, map) {
const dialog = document.getElementById('detailsDialog');
dialog.querySelector('tbody').innerHTML = '';
for (const [eventName, listeners] of Object.entries(map[node])) {
listeners.forEach(listener => {
const row = document.createElement('tr');
const eventNameCode = document.createElement('code');
eventNameCode.textContent = eventName;
const eventNameCell = document.createElement('td');
eventNameCell.appendChild(eventNameCode);
row.appendChild(eventNameCell);
const listenerDetailsCell = document.createElement('td');
row.appendChild(listenerDetailsCell);
let listenerDetails;
const listenerDetailsCode = document.createElement('code');
listenerDetailsCode.textContent = listener.title;
if (listener.file) {
const link = document.createElement('a');
link.href = listener.file;
link.appendChild(listenerDetailsCode);
listenerDetails = link;
} else {
listenerDetails = listenerDetailsCode;
}
listenerDetailsCell.appendChild(listenerDetails);
if (typeof listener.guardExpressions === 'object') {
listenerDetailsCell.appendChild(document.createElement('br'));
const guardExpressionsWrapper = document.createElement('span');
guardExpressionsWrapper.appendChild(document.createTextNode('guard expressions: '));
listener.guardExpressions.forEach((expression, index) => {
if (index > 0) {
guardExpressionsWrapper.appendChild(document.createTextNode(', '));
}
const expressionCode = document.createElement('code');
expressionCode.textContent = expression;
guardExpressionsWrapper.appendChild(expressionCode);
});
listenerDetailsCell.appendChild(guardExpressionsWrapper);
}
dialog.querySelector('tbody').appendChild(row);
});
};
if (dialog.dataset.processed) {
dialog.showModal();
return;
}
dialog.addEventListener('click', (e) => {
const rect = dialog.getBoundingClientRect();
const inDialog =
rect.top <= e.clientY &&
e.clientY <= rect.top + rect.height &&
rect.left <= e.clientX &&
e.clientX <= rect.left + rect.width;
!inDialog && dialog.close();
});
dialog.querySelectorAll('.cancel').forEach(elt => {
elt.addEventListener('click', () => dialog.close());
});
dialog.showModal();
dialog.dataset.processed = true;
};
// We do not load all mermaid diagrams at once, but only when the tab is opened
// This is because mermaid diagrams are in a tab, and cannot be renderer with a
// "good size" if they are not visible
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.tab').forEach((el) => {
const observer = new MutationObserver(() => {
if (!el.classList.contains('block')) {
return;
}
const mEl = el.querySelector('.sf-mermaid');
if (mEl.dataset.processed) {
return;
}
mermaid.run({
nodes: [mEl],
});
});
observer.observe(el, { attributeFilter: ['class'] });
});
});
</script>
<div class="sf-tabs js-tabs">
{% for name, data in collector.workflows %}
<div class="tab">
<h2 class="tab-title">{{ name }}{% if data.calls|length %} ({{ data.calls|length }}){% endif %}</h2>
<div class="tab-content">
<h3>Definition</h3>
<pre class="sf-mermaid">
{{ data.dump|raw }}
{% for nodeId, events in data.listeners %}
click {{ nodeId }} showNodeDetails{{ collector.hash(name) }}
{% endfor %}
</pre>
<a href="{{ collector.buildMermaidLiveLink(name) }}">View on mermaid.live</a>
<h3>Calls</h3>
<table>
<thead>
<tr>
<th>#</th>
<th>Call</th>
<th>Args</th>
<th>Return</th>
<th>Exception</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
{% for call in data.calls %}
<tr>
<td class="font-normal text-small text-muted nowrap">{{ loop.index }}</td>
<td>
<code>{{ call.method }}()</code>
{% if call.previousMarking ?? null %}
<hr />
Previous marking:
{{ profiler_dump(call.previousMarking) }}
{% endif %}
</td>
<td>
{{ profiler_dump(call.args) }}
</td>
<td>
{% if call.return is defined %}
{% if call.return is same as true %}
<code>true</code>
{% elseif call.return is same as false %}
<code>false</code>
{% else %}
{{ profiler_dump(call.return) }}
{% endif %}
{% endif %}
</td>
<td>
{% if call.exception is defined %}
{{ profiler_dump(call.exception) }}
{% endif %}
</td>
<td>
{{ '%0.2f ms'|format(call.duration) }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<dialog id="detailsDialog">
<h2>
Event listeners
<i class="cancel">×</i>
</h2>
<table>
<thead>
<tr>
<th>event</th>
<th>listener</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<menu>
<small><i>⌨</i> <kbd>esc</kbd></small>
<button class="btn btn-sm cancel">Close</button>
</menu>
</dialog>
{% endblock %}

View File

@@ -0,0 +1,26 @@
Icons are from "Tabler Icons" (https://github.com/tabler/tabler-icons), a set of
free MIT-licensed high-quality SVG icons.
-----
MIT License
Copyright (c) 2020-2022 Paweł Kuna
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-arrows-down-up" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<line x1="17" y1="3" x2="17" y2="21"></line>
<path d="M10 18l-3 3l-3 -3"></path>
<line x1="7" y1="21" x2="7" y2="3"></line>
<path d="M20 6l-3 -3l-3 3"></path>
</svg>

After

Width:  |  Height:  |  Size: 472 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-alert-circle" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<circle cx="12" cy="12" r="9"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-paperclip" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M15 7l-6.5 6.5a1.5 1.5 0 0 0 3 3l6.5 -6.5a3 3 0 0 0 -6 -6l-6.5 6.5a4.5 4.5 0 0 0 9 9l6.5 -6.5"></path>
</svg>

After

Width:  |  Height:  |  Size: 408 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-stack-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" role="img">
<title>Cache</title>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<polyline points="12 4 4 8 12 12 20 8 12 4"></polyline>
<polyline points="4 12 12 16 20 12"></polyline>
<polyline points="4 16 12 20 20 16"></polyline>
</svg>

After

Width:  |  Height:  |  Size: 490 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-chevron-down" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 9l6 6l6 -6"></path>
</svg>

After

Width:  |  Height:  |  Size: 331 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-x" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>

After

Width:  |  Height:  |  Size: 380 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-terminal-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M8 9l3 3l-3 3"></path>
<line x1="13" y1="15" x2="16" y2="15"></line>
<rect x="3" y="4" width="18" height="16" rx="2"></rect>
</svg>

After

Width:  |  Height:  |  Size: 437 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-adjustments" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" role="img">
<title>Config</title>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<circle cx="6" cy="10" r="2"></circle>
<line x1="6" y1="4" x2="6" y2="8"></line>
<line x1="6" y1="12" x2="6" y2="20"></line>
<circle cx="12" cy="16" r="2"></circle>
<line x1="12" y1="4" x2="12" y2="14"></line>
<line x1="12" y1="18" x2="12" y2="20"></line>
<circle cx="18" cy="7" r="2"></circle>
<line x1="18" y1="4" x2="18" y2="5"></line>
<line x1="18" y1="9" x2="18" y2="20"></line>
</svg>

After

Width:  |  Height:  |  Size: 751 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-download" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2"></path>
<polyline points="7 11 12 16 17 11"></polyline>
<line x1="12" y1="4" x2="12" y2="16"></line>
</svg>

After

Width:  |  Height:  |  Size: 457 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-access-point" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<line x1="12" y1="12" x2="12" y2="12.01"></line>
<path d="M14.828 9.172a4 4 0 0 1 0 5.656"></path>
<path d="M17.657 6.343a8 8 0 0 1 0 11.314"></path>
<path d="M9.168 14.828a4 4 0 0 1 0 -5.656"></path>
<path d="M6.337 17.657a8 8 0 0 1 0 -11.314"></path>
</svg>

After

Width:  |  Height:  |  Size: 568 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-ghost" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" role="img">
<title>Exception</title>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M5 11a7 7 0 0 1 14 0v7a1.78 1.78 0 0 1 -3.1 1.4a1.65 1.65 0 0 0 -2.6 0a1.65 1.65 0 0 1 -2.6 0a1.65 1.65 0 0 0 -2.6 0a1.78 1.78 0 0 1 -3.1 -1.4v-7"></path>
<line x1="10" y1="10" x2="10.01" y2="10"></line>
<line x1="14" y1="10" x2="14.01" y2="10"></line>
<path d="M10 14a3.5 3.5 0 0 0 4 0"></path>
</svg>

After

Width:  |  Height:  |  Size: 649 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-file" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"></path>
</svg>

After

Width:  |  Height:  |  Size: 426 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-filter" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M5.5 5h13a1 1 0 0 1 .5 1.5l-5 5.5l0 7l-4 -3l0 -4l-5 -5.5a1 1 0 0 1 .5 -1.5"></path>
</svg>

After

Width:  |  Height:  |  Size: 386 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-forms" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" role="img">
<title>Cache</title>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 3a3 3 0 0 0 -3 3v12a3 3 0 0 0 3 3"></path>
<path d="M6 3a3 3 0 0 1 3 3v12a3 3 0 0 1 -3 3"></path>
<path d="M13 7h7a1 1 0 0 1 1 1v8a1 1 0 0 1 -1 1h-7"></path>
<path d="M5 7h-1a1 1 0 0 0 -1 1v8a1 1 0 0 0 1 1h1"></path>
<path d="M17 12h.01"></path>
<path d="M13 12h.01"></path>
</svg>

After

Width:  |  Height:  |  Size: 636 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-arrow-narrow-right" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<line x1="5" y1="12" x2="19" y2="12"></line>
<line x1="15" y1="16" x2="19" y2="12"></line>
<line x1="15" y1="8" x2="19" y2="12"></line>
</svg>

After

Width:  |  Height:  |  Size: 449 B

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-affiliate" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M5.931 6.936l1.275 4.249m5.607 5.609l4.251 1.275"></path>
<path d="M11.683 12.317l5.759 -5.759"></path>
<circle cx="5.5" cy="5.5" r="1.5"></circle>
<circle cx="18.5" cy="5.5" r="1.5"></circle>
<circle cx="18.5" cy="18.5" r="1.5"></circle>
<circle cx="8.5" cy="15.5" r="4.5"></circle>
</svg>

After

Width:  |  Height:  |  Size: 609 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-file-alert" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" role="img">
<title>Logger</title>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"></path>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
<line x1="12" y1="11" x2="12" y2="14"></line>
</svg>

After

Width:  |  Height:  |  Size: 572 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-mail" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<rect x="3" y="5" width="18" height="14" rx="2"></rect>
<polyline points="3 7 12 13 21 7"></polyline>
</svg>

After

Width:  |  Height:  |  Size: 398 B

View File

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-cpu" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" role="img">
<title>Memory</title>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<rect x="5" y="5" width="14" height="14" rx="1"></rect>
<path d="M9 9h6v6h-6z"></path>
<path d="M3 10h2"></path>
<path d="M3 14h2"></path>
<path d="M10 3v2"></path>
<path d="M14 3v2"></path>
<path d="M21 10h-2"></path>
<path d="M21 14h-2"></path>
<path d="M14 21v-2"></path>
<path d="M10 21v-2"></path>
</svg>

After

Width:  |  Height:  |  Size: 666 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-menu-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" role="img">
<title>Menu</title>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<line x1="4" y1="6" x2="20" y2="6"></line>
<line x1="4" y1="12" x2="20" y2="12"></line>
<line x1="4" y1="18" x2="20" y2="18"></line>
</svg>

After

Width:  |  Height:  |  Size: 469 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-subtask" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<line x1="6" y1="9" x2="12" y2="9"></line>
<line x1="4" y1="5" x2="8" y2="5"></line>
<path d="M6 5v11a1 1 0 0 0 1 1h5"></path>
<rect x="12" y="7" width="8" height="4" rx="1"></rect>
<rect x="12" y="15" width="8" height="4" rx="1"></rect>
</svg>

After

Width:  |  Height:  |  Size: 548 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-x" width="24" height="24" viewBox="0 0 24 24" stroke-width="3" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>

After

Width:  |  Height:  |  Size: 378 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-bell-ringing" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M10 5a2 2 0 0 1 4 0a7 7 0 0 1 4 6v3a4 4 0 0 0 2 3h-16a4 4 0 0 0 2 -3v-3a7 7 0 0 1 4 -6"></path>
<path d="M9 17v1a3 3 0 0 0 6 0v-1"></path>
<path d="M21 6.727a11.05 11.05 0 0 0 -2.794 -3.727"></path>
<path d="M3 6.727a11.05 11.05 0 0 1 2.792 -3.727"></path>
</svg>

After

Width:  |  Height:  |  Size: 577 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-corner-down-right" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" role="img">
<title>Redirect</title>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 6v6a3 3 0 0 0 3 3h10l-4 -4m0 8l4 -4"></path>
</svg>

After

Width:  |  Height:  |  Size: 400 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-arrow-back-up" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M9 13l-4 -4l4 -4m-4 4h11a4 4 0 0 1 0 8h-1"></path>
</svg>

After

Width:  |  Height:  |  Size: 360 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-browser" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<rect x="4" y="4" width="16" height="16" rx="1"></rect>
<line x1="4" y1="8" x2="20" y2="8"></line>
<line x1="8" y1="4" x2="8" y2="8"></line>
</svg>

After

Width:  |  Height:  |  Size: 443 B

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-directions" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 21v-4"></path>
<path d="M12 13v-4"></path>
<path d="M12 5v-2"></path>
<path d="M10 21h4"></path>
<path d="M8 5v4h11l2 -2l-2 -2z"></path>
<path d="M14 13v4h-8l-2 -2l2 -2z"></path>
</svg>

After

Width:  |  Height:  |  Size: 509 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-search" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<circle cx="10" cy="10" r="7"></circle>
<line x1="21" y1="21" x2="15" y2="15"></line>
</svg>

After

Width:  |  Height:  |  Size: 383 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-arrows-right" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<line x1="21" y1="17" x2="3" y2="17"></line>
<path d="M18 4l3 3l-3 3"></path>
<path d="M18 20l3 -3l-3 -3"></path>
<line x1="21" y1="7" x2="3" y2="7"></line>
</svg>

After

Width:  |  Height:  |  Size: 458 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-moon" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" />
</svg>

After

Width:  |  Height:  |  Size: 377 B

View File

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-sun-high" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M14.828 14.828a4 4 0 1 0 -5.656 -5.656a4 4 0 0 0 5.656 5.656z" />
<path d="M6.343 17.657l-1.414 1.414" />
<path d="M6.343 6.343l-1.414 -1.414" />
<path d="M17.657 6.343l1.414 -1.414" />
<path d="M17.657 17.657l1.414 1.414" />
<path d="M4 12h-2" />
<path d="M12 4v-2" />
<path d="M20 12h2" />
<path d="M12 20v2" />
</svg>

After

Width:  |  Height:  |  Size: 644 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-device-laptop" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<line x1="3" y1="19" x2="21" y2="19" />
<rect x="5" y="6" width="14" height="10" rx="1" />
</svg>

After

Width:  |  Height:  |  Size: 389 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-autofit-width" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M4 12v-6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v6" />
<path d="M10 18h-7" />
<path d="M21 18h-7" />
<path d="M6 15l-3 3l3 3" />
<path d="M18 15l3 3l-3 3" />
</svg>

After

Width:  |  Height:  |  Size: 483 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-rectangle" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<rect x="3" y="5" width="18" height="14" rx="2" />
</svg>

After

Width:  |  Height:  |  Size: 349 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-settings" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>

After

Width:  |  Height:  |  Size: 881 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Symfony" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M12 .9C5.8.9.9 5.8.9 12a11 11 0 1 0 22.2 0A11 11 0 0 0 12 .9zm6.5 6c-.6 0-.9-.3-.9-.8 0-.2 0-.4.2-.6l.2-.4c0-.3-.5-.4-.6-.4-1.8.1-2.3 2.5-2.7 4.4l-.2 1c1 .2 1.8 0 2.2-.3.6-.4-.2-.7-.1-1.2.1-.3.5-.5.7-.6.5 0 .7.5.7.9 0 .7-1 1.8-3 1.8l-.6-.1-.6 2.4c-.4 1.6-.8 3.8-2.4 5.7-1.4 1.7-2.9 1.9-3.5 1.9-1.2 0-1.9-.6-2-1.5 0-.8.7-1.3 1.2-1.3.6 0 1.1.5 1.1 1s-.2.6-.4.6c-.1.1-.3.2-.3.4 0 .1.1.3.4.3.5 0 .8-.3 1.1-.5 1.2-.9 1.6-2.7 2.2-5.7l.1-.7.7-3.2c-.8-.6-1.3-1.4-2.4-1.7-.6-.1-1.1.1-1.5.5-.4.5-.2 1.1.2 1.5l.7.6c.7.8 1.2 1.6 1 2.5-.3 1.5-2 2.6-4 1.9-1.8-.6-2-1.8-1.8-2.5.2-.6.6-.7 1.1-.6.5.2.6.7.6 1.2l-.1.3c-.2.1-.3.3-.3.4-.1.4.4.6.7.7.7.3 1.6-.2 1.8-.8a1 1 0 0 0-.4-1.1l-.7-.8c-.4-.4-1.1-1.4-.7-2.6.1-.5.4-.9.7-1.3a4 4 0 0 1 2.8-.6c1.2.4 1.8 1.1 2.6 1.8.5-1.2 1-2.4 1.8-3.5.9-.9 1.9-1.6 3.1-1.7 1.3.2 2.2.7 2.2 1.6 0 .4-.2 1.1-.9 1.1z"/></svg>

After

Width:  |  Height:  |  Size: 982 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-chart-infographic" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" role="img">
<title>Time</title>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<circle cx="7" cy="7" r="4"></circle>
<path d="M7 3v4h4"></path>
<line x1="9" y1="17" x2="9" y2="21"></line>
<line x1="17" y1="14" x2="17" y2="21"></line>
<line x1="13" y1="13" x2="13" y2="21"></line>
<line x1="21" y1="12" x2="21" y2="21"></line>
</svg>

After

Width:  |  Height:  |  Size: 606 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-language-hiragana" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 5h7"></path>
<path d="M7 4c0 4.846 0 7 .5 8"></path>
<path d="M10 8.5c0 2.286 -2 4.5 -3.5 4.5s-2.5 -1.135 -2.5 -2c0 -2 1 -3 3 -3s5 .57 5 2.857c0 1.524 -.667 2.571 -2 3.143"></path>
<path d="M12 20l4 -9l4 9"></path>
<path d="M19.1 18h-6.2"></path>
</svg>

After

Width:  |  Height:  |  Size: 579 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-seeding" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" role="img">
<title>Twig</title>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 10a6 6 0 0 0 -6 -6h-3v2a6 6 0 0 0 6 6h3"></path>
<path d="M12 14a6 6 0 0 1 6 -6h3v1a6 6 0 0 1 -6 6h-3"></path>
<line x1="12" y1="20" x2="12" y2="10"></line>
</svg>

After

Width:  |  Height:  |  Size: 507 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-checklist" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M9.615 20h-2.615a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8"></path>
<path d="M14 19l2 2l4 -4"></path>
<path d="M9 8h4"></path>
<path d="M9 12h2"></path>
</svg>

After

Width:  |  Height:  |  Size: 481 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 18m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"></path>
<path d="M7 6m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"></path>
<path d="M17 6m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"></path>
<path d="M7 8v2a2 2 0 0 0 2 2h6a2 2 0 0 0 2 -2v-2"></path>
<path d="M12 12l0 4"></path>
</svg>

After

Width:  |  Height:  |  Size: 527 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-check" width="24" height="24" viewBox="0 0 24 24" stroke-width="3" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M5 12l5 5l10 -10"></path>
</svg>

After

Width:  |  Height:  |  Size: 325 B

View File

@@ -0,0 +1,50 @@
{% set status_code = profile.statuscode|default(0) %}
{% set interrupted = command_collector is same as false ? null : command_collector.interruptedBySignal %}
{% set css_class = status_code == 113 or interrupted is not null ? 'status-warning' : status_code > 0 ? 'status-error' : 'status-success' %}
<div class="terminal status {{ css_class }}">
<div class="container">
<h2>
<span class="status-request-method">
{{ profile.method|upper }}
</span>
<span class="status-command">
{{ profile.url }}
</span>
</h2>
<dl class="metadata">
{% if interrupted %}
<span class="status-response-status-code">Interrupted</span>
<dt>Signal</dt>
<dd class="status-response-status-text">{{ interrupted }}</dd>
<dt>Exit code</dt>
<dd class="status-response-status-text">{{ status_code }}</dd>
{% elseif status_code == 0 %}
<span class="status-response-status-code">Success</span>
{% elseif status_code > 0 %}
<span class="status-response-status-code">Error</span>
<dt>Exit code</dt>
<dd class="status-response-status-text"><span class="status-response-status-code">{{ status_code }}</span></dd>
{% endif %}
{% if request_collector.requestserver.has('SYMFONY_CLI_BINARY_NAME') %}
<dt>Symfony CLI</dt>
<dd>v{{ request_collector.requestserver.get('SYMFONY_CLI_VERSION') }}</dd>
{% endif %}
<dt>Application</dt>
<dd>
<a href="{{ path('_profiler_search_results', { token: token, limit: 10, ip: profile.ip, type: 'command' }) }}">{{ profile.ip }}</a>
</dd>
<dt>Profiled on</dt>
<dd><time data-convert-to-user-timezone data-render-as-datetime datetime="{{ profile.time|date('c') }}">{{ profile.time|date('r') }}</time></dd>
<dt>Token</dt>
<dd>{{ profile.token }}</dd>
</dl>
</div>
</div>

View File

@@ -0,0 +1,99 @@
{% set status_code = request_collector ? request_collector.statuscode|default(0) : 0 %}
{% set css_class = status_code > 399 ? 'status-error' : status_code > 299 ? 'status-warning' : 'status-success' %}
{% if request_collector and request_collector.redirect %}
{% set redirect = request_collector.redirect %}
{% set link_to_source_code = redirect.controller.class is defined ? redirect.controller.file|file_link(redirect.controller.line) %}
{% set redirect_route_name = '@' ~ redirect.route %}
<div class="status status-compact status-warning">
<span class="icon icon-redirect">{{ source('@WebProfiler/Icon/redirect.svg') }}</span>
<span class="status-response-status-code">{{ redirect.status_code }}</span> redirect from
<span class="status-request-method">{{ redirect.method }}</span>
{% if link_to_source_code %}
<a href="{{ link_to_source_code }}" title="{{ redirect.controller.file }}">{{ redirect_route_name }}</a>
{% else %}
{{ redirect_route_name }}
{% endif %}
(<a href="{{ path('_profiler', { token: redirect.token, panel: request.query.get('panel', 'request') }) }}">{{ redirect.token }}</a>)
</div>
{% endif %}
<div class="status {{ css_class }}">
{% if status_code > 399 %}
<p class="status-error-details">
<span class="icon">{{ source('@WebProfiler/Icon/alert-circle.svg') }}</span>
<span class="status-response-status-code">Error {{ status_code }}</span>
<span class="status-response-status-text">{{ request_collector.statusText }}</span>
</p>
{% endif %}
<h2>
<span class="status-request-method">
{{ profile.method|upper }}
</span>
{% set profile_title = profile.url|length < 160 ? profile.url : profile.url[:160] ~ '…' %}
{% if profile.method|upper in ['GET', 'HEAD'] %}
<a href="{{ profile.url }}">{{ profile_title }}</a>
{% else %}
{{ profile_title }}
{% endif %}
</h2>
<dl class="metadata">
{% if status_code < 400 %}
<dt>Response</dt>
<dd>
<span class="status-response-status-code">{{ status_code }}</span>
<span class="status-response-status-text">{{ request_collector.statusText }}</span>
</dd>
{% endif %}
{% set referer = request_collector ? request_collector.requestheaders.get('referer') : null %}
{% if referer %}
<dt></dt>
<dd>
<span class="icon icon-referer">{{ source('@WebProfiler/Icon/referrer.svg') }}</span>
<a href="{{ referer }}" class="referer">Browse referrer URL</a>
</dd>
{% endif %}
<dt>IP</dt>
<dd>
<a href="{{ path('_profiler_search_results', { token: token, limit: 10, ip: profile.ip }) }}">{{ profile.ip }}</a>
</dd>
<dt>Profiled on</dt>
<dd><time data-convert-to-user-timezone data-render-as-datetime datetime="{{ profile.time|date('c') }}">{{ profile.time|date('r') }}</time></dd>
<dt>Token</dt>
<dd>{{ profile.token }}</dd>
</dl>
</div>
{% if request_collector and request_collector.forwardtoken -%}
{% set forward_profile = profile.childByToken(request_collector.forwardtoken) %}
{% set controller = forward_profile ? forward_profile.collector('request').controller : 'n/a' %}
<div class="status status-compact status-compact-forward">
<span class="icon icon-forward">{{ source('@WebProfiler/Icon/forward.svg') }}</span>
Forwarded to
{% set link = controller.file is defined ? controller.file|file_link(controller.line) : null -%}
{%- if link %}<a href="{{ link }}" title="{{ controller.file }}">{% endif -%}
{% if controller.class is defined %}
{{- controller.class|abbr_class|striptags -}}
{{- controller.method ? ' :: ' ~ controller.method -}}
{% else %}
{{- controller -}}
{% endif %}
{%- if link %}</a>{% endif %}
(<a href="{{ path('_profiler', { token: request_collector.forwardtoken }) }}">{{ request_collector.forwardtoken }}</a>)
</div>
{%- endif %}

View File

@@ -0,0 +1 @@
{% block panel '' %}

View File

@@ -0,0 +1,22 @@
<div class="table-with-search-field">
<table class="{{ class|default('') }}">
<thead>
<tr>
<th scope="col" class="key">{{ labels is defined ? labels[0] : 'Key' }}</th>
<th scope="col">{{ labels is defined ? labels[1] : 'Value' }}</th>
</tr>
</thead>
<tbody>
{% for key in bag.keys|sort %}
<tr>
<th>{{ key }}</th>
<td>{{ profiler_dump(bag.get(key), maxDepth=maxDepth|default(0)) }}</td>
</tr>
{% else %}
<tr>
<td colspan="2">(no data)</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

View File

@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="{{ _charset }}" />
<meta name="robots" content="noindex,nofollow" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="view-transition" content="same-origin">
<title>{% block title %}Symfony Profiler{% endblock %}</title>
{% set request_collector = profile is defined ? profile.collectors.request|default(null) : null %}
{% set status_code = request_collector is not null ? request_collector.statuscode|default(0) : 0 %}
{% set favicon_color = status_code > 399 ? 'b41939' : status_code > 299 ? 'af8503' : '000000' %}
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg viewBox='0 0 600 600' xmlns='http://www.w3.org/2000/svg' fill-rule='evenodd' clip-rule='evenodd' stroke-linejoin='round' stroke-miterlimit='2'%3E%3Cstyle%3E%23circle %7B fill: %23{{ favicon_color }}; %7D %23sf %7B fill: %23ffffff; %7D%3C/style%3E%3Cpath fill='none' d='M0 0h600v600H0z'/%3E%3CclipPath id='a'%3E%3Cpath d='M0 0h600v600H0z'/%3E%3C/clipPath%3E%3Cg clip-path='url(%23a)'%3E%3Cpath id='circle' d='M599.985 299.974c0 165.696-134.307 300.024-300.003 300.024C134.302 599.998 0 465.67 0 299.974 0 134.304 134.302-.002 299.982-.002c165.696 0 300.003 134.307 300.003 299.976z' fill-rule='nonzero'/%3E%3Cpath id='sf' d='M431.154 110.993c-30.474 1.043-57.08 17.866-76.884 41.076-21.926 25.49-36.508 55.696-47.03 86.55-18.791-15.416-33.282-35.364-63.457-44.04-23.311-6.702-47.794-3.948-70.314 12.833-10.667 7.965-18.016 19.995-21.51 31.34-9.05 29.416 9.506 55.61 17.942 65.004l18.444 19.743c3.792 3.879 12.95 13.983 8.467 28.458-4.82 15.764-23.809 25.938-43.285 19.958-8.703-2.67-21.199-9.147-18.396-18.257 1.145-3.739 3.82-6.553 5.264-9.74 1.305-2.788 1.941-4.858 2.337-6.099 3.557-11.602-1.31-26.714-13.747-30.56-11.613-3.562-23.488-.738-28.094 14.202-5.22 16.979 2.905 47.795 46.436 61.206 51 15.694 94.13-12.084 100.249-48.287 3.857-22.675-6.392-39.536-25.147-61.2l-15.293-16.92c-9.254-9.248-12.437-25.018-2.856-37.134 8.093-10.233 19.6-14.581 38.476-9.457 27.543 7.468 39.809 26.58 60.285 41.996-8.44 27.741-13.977 55.584-18.973 80.548l-3.07 18.626c-14.636 76.766-25.816 118.939-54.856 143.144-5.858 4.167-14.218 10.399-26.821 10.843-6.622.203-8.757-4.355-8.847-6.344-.15-4.628 3.755-6.756 6.349-8.837 3.889-2.124 9.757-5.633 9.356-16.882-.423-13.293-11.431-24.815-27.35-24.286-11.919.402-30.09 11.608-29.4 32.149.701 21.22 20.472 37.118 50.288 36.107 15.935-.535 51.528-7.018 86.592-48.699 40.82-47.8 52.235-102.576 60.82-142.673l9.591-52.946a177.574 177.574 0 0017.209 1.22c50.844 1.075 76.257-25.249 76.653-44.41.257-11.591-7.6-23.011-18.61-22.739-7.863.22-17.759 5.473-20.123 16.353-2.332 10.671 16.17 20.316 1.712 29.704-10.27 6.643-28.683 11.319-54.615 7.526l4.712-26.061c9.623-49.416 21.493-110.193 66.528-111.68 3.284-.155 15.282.139 15.56 8.088.08 2.637-.582 3.332-3.68 9.393-3.166 4.729-4.36 8.773-4.204 13.394.433 12.608 10.024 20.91 23.916 20.429 18.572-.626 23.906-18.7 23.6-27.998-.759-21.846-23.776-35.647-54.224-34.641z' fill-rule='nonzero'/%3E%3C/g%3E%3C/svg%3E"/>
{% block head %}
{% block stylesheets %}
<style{% if csp_style_nonce is defined and csp_style_nonce %} nonce="{{ csp_style_nonce }}"{% endif %}>
{{ include('@WebProfiler/Profiler/profiler.css.twig') }}
</style>
{% endblock %}
{% block javascripts %}
{% endblock %}
{% endblock %}
</head>
<body>
<script{% if csp_script_nonce is defined and csp_script_nonce %} nonce="{{ csp_script_nonce }}"{% endif %}>
if (null === localStorage.getItem('symfony/profiler/theme') || 'theme-auto' === localStorage.getItem('symfony/profiler/theme')) {
document.body.classList.add((matchMedia('(prefers-color-scheme: dark)').matches ? 'theme-dark' : 'theme-light'));
// needed to respond dynamically to OS changes without having to refresh the page
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
document.body.classList.remove('theme-light', 'theme-dark');
document.body.classList.add(e.matches ? 'theme-dark' : 'theme-light');
});
} else {
document.body.classList.add(localStorage.getItem('symfony/profiler/theme'));
}
document.body.classList.add(localStorage.getItem('symfony/profiler/width') || 'width-normal');
document.body.classList.add(
(navigator.appVersion.indexOf('Win') !== -1) ? 'windows' : (navigator.appVersion.indexOf('Mac') !== -1) ? 'macos' : 'linux'
);
</script>
{% block body '' %}
</body>
</html>

View File

@@ -0,0 +1,280 @@
{# This file is partially duplicated in src/Symfony/Component/ErrorHandler/Resources/assets/js/exception.js.
If you make any change in this file, verify the same change is needed in the other file. #}
{# CAUTION: the contents of this file are processed by Twig before loading
them as JavaScript source code. Always use '/*' comments instead
of '//' comments to avoid impossible-to-debug side-effects #}
<script{% if csp_script_nonce is defined and csp_script_nonce %} nonce="{{ csp_script_nonce }}"{% endif %}>
window.addEventListener('DOMContentLoaded', () => {
new SymfonyProfiler();
});
class SymfonyProfiler {
constructor() {
this.#createTabs();
this.#createTableSearchFields();
this.#createToggles();
this.#createCopyToClipboard();
this.#convertDateTimesToUserTimezone();
}
#createTabs() {
/* the accessibility options of this component have been defined according to: */
/* www.w3.org/WAI/ARIA/apg/example-index/tabs/tabs-manual.html */
const tabGroups = document.querySelectorAll('.sf-tabs:not([data-processed=true])');
/* create the tab navigation for each group of tabs */
tabGroups.forEach((tabGroup, i) => {
const tabs = tabGroup.querySelectorAll(':scope > .tab');
const tabNavigation = document.createElement('div');
tabNavigation.classList.add('tab-navigation');
tabNavigation.setAttribute('role', 'tablist');
let selectedTabId = `tab-${i}-0`; /* select the first tab by default */
tabs.forEach((tab, j) => {
const tabId = `tab-${i}-${j}`;
const tabTitle = tab.querySelector('.tab-title').innerHTML;
const tabNavigationItem = document.createElement('button');
tabNavigationItem.classList.add('tab-control');
tabNavigationItem.setAttribute('data-tab-id', tabId);
tabNavigationItem.setAttribute('role', 'tab');
tabNavigationItem.setAttribute('aria-controls', tabId);
if (tab.classList.contains('active')) { selectedTabId = tabId; }
if (tab.classList.contains('disabled')) {
tabNavigationItem.classList.add('disabled');
}
tabNavigationItem.innerHTML = tabTitle;
tabNavigation.appendChild(tabNavigationItem);
const tabContent = tab.querySelector('.tab-content');
tabContent.parentElement.setAttribute('id', tabId);
});
tabGroup.insertBefore(tabNavigation, tabGroup.firstChild);
document.querySelector('[data-tab-id="' + selectedTabId + '"]').classList.add('active');
});
/* display the active tab and add the 'click' event listeners */
tabGroups.forEach((tabGroup) => {
const tabs = tabGroup.querySelectorAll(':scope > .tab-navigation .tab-control');
tabs.forEach((tab) => {
const tabId = tab.getAttribute('data-tab-id');
const tabPanel = document.getElementById(tabId);
tabPanel.setAttribute('role', 'tabpanel');
tabPanel.setAttribute('aria-labelledby', tabId);
tabPanel.querySelector('.tab-title').className = 'hidden';
if (tab.classList.contains('active')) {
tabPanel.className = 'block';
tab.setAttribute('aria-selected', 'true');
tab.removeAttribute('tabindex');
} else {
tabPanel.className = 'hidden';
tab.removeAttribute('aria-selected');
tab.setAttribute('tabindex', '-1');
}
tab.addEventListener('click', function(e) {
let activeTab = e.target || e.srcElement;
/* needed because when the tab contains HTML contents, user can click */
/* on any of those elements instead of their parent '<button>' element */
while ('button' !== activeTab.tagName.toLowerCase()) {
activeTab = activeTab.parentNode;
}
/* get the full list of tabs through the parent of the active tab element */
const tabs = Array.from(activeTab.parentNode.children);
tabs.forEach((tab) => {
const tabId = tab.getAttribute('data-tab-id');
document.getElementById(tabId).className = 'hidden';
tab.classList.remove('active');
tab.removeAttribute('aria-selected');
tab.setAttribute('tabindex', '-1');
});
activeTab.classList.add('active');
activeTab.setAttribute('aria-selected', 'true');
activeTab.removeAttribute('tabindex');
const activeTabId = activeTab.getAttribute('data-tab-id');
document.getElementById(activeTabId).className = 'block';
});
});
tabGroup.setAttribute('data-processed', 'true');
});
}
#createTableSearchFields() {
document.querySelectorAll('div.table-with-search-field').forEach((tableWrapper, i) => {
const searchField = document.createElement('input');
searchField.type = 'search';
searchField.placeholder = 'search...';
searchField.id = `table-search-field-${i}`;
searchField.classList.add(`table-search-field-input`);
searchField.autocapitalize = 'off';
searchField.autocomplete = 'off';
searchField.autocorrect = 'off';
tableWrapper.insertBefore(searchField, tableWrapper.firstChild);
const labelField = document.createElement('label');
labelField.htmlFor = `table-search-field-${i}`;
labelField.classList.add(`table-search-field-label`);
labelField.textContent = 'Search inside the contents of the table';
tableWrapper.insertBefore(labelField, tableWrapper.firstChild);
searchField.addEventListener('input', () => {
const query = searchField.value.toLowerCase();
let allRowsAreHidden = true;
tableWrapper.querySelectorAll('tbody tr').forEach((row) => {
const rowMatchesQuery = row.textContent.toLowerCase().includes(query);
row.style.display = rowMatchesQuery ? '' : 'none';
if (rowMatchesQuery) {
allRowsAreHidden = false;
}
});
/* if there are no results and all rows are hidden, show a message to avoid confusion */
const noResultsElement = tableWrapper.querySelector('.no-results-message');
if (allRowsAreHidden) {
if (null === noResultsElement) {
const noResultsElement = document.createElement('p');
noResultsElement.textContent = 'No results found.';
noResultsElement.classList.add('no-results-message');
tableWrapper.appendChild(noResultsElement);
} else {
noResultsElement.style.display = '';
}
} else {
if (null !== noResultsElement) {
noResultsElement.style.display = 'none';
}
}
});
});
}
#createToggles() {
const toggles = document.querySelectorAll('.sf-toggle:not([data-processed=true])');
toggles.forEach((toggle) => {
const elementSelector = toggle.getAttribute('data-toggle-selector');
const element = document.querySelector(elementSelector);
element.classList.add('sf-toggle-content');
if (toggle.hasAttribute('data-toggle-initial') && 'display' === toggle.getAttribute('data-toggle-initial')) {
toggle.classList.add('sf-toggle-on');
element.classList.add('sf-toggle-visible');
} else {
toggle.classList.add('sf-toggle-off');
element.classList.add('sf-toggle-hidden');
}
toggle.addEventListener('click', (e) => {
const toggle = e.currentTarget;
if (e.target.closest('a, .sf-toggle') !== toggle) {
return;
}
e.preventDefault();
if ('' !== window.getSelection().toString()) {
/* Don't do anything on text selection */
return;
}
const element = document.querySelector(toggle.getAttribute('data-toggle-selector'));
toggle.classList.toggle('sf-toggle-on');
toggle.classList.toggle('sf-toggle-off');
element.classList.toggle('sf-toggle-hidden');
element.classList.toggle('sf-toggle-visible');
/* the toggle doesn't change its contents when clicking on it */
if (!toggle.hasAttribute('data-toggle-alt-content')) {
return;
}
if (!toggle.hasAttribute('data-toggle-original-content')) {
toggle.setAttribute('data-toggle-original-content', toggle.innerHTML);
}
const currentContent = toggle.innerHTML;
const originalContent = toggle.getAttribute('data-toggle-original-content');
const altContent = toggle.getAttribute('data-toggle-alt-content');
toggle.innerHTML = currentContent !== altContent ? altContent : originalContent;
});
toggle.setAttribute('data-processed', 'true');
});
}
#createCopyToClipboard() {
if (!navigator.clipboard) {
return;
}
const copyToClipboardElements = document.querySelectorAll('[data-clipboard-text]');
copyToClipboardElements.forEach((copyToClipboardElement) => {
copyToClipboardElement.classList.remove('hidden');
copyToClipboardElement.addEventListener('click', (e) => {
/* Prevents from disallowing clicks on "copy to clipboard" elements inside toggles */
e.stopPropagation();
navigator.clipboard.writeText(copyToClipboardElement.getAttribute('data-clipboard-text'));
let oldContent = copyToClipboardElement.textContent;
copyToClipboardElement.textContent = `✅ Copied!`;
copyToClipboardElement.disabled = true;
setTimeout(() => {
copyToClipboardElement.textContent = oldContent;
copyToClipboardElement.disabled = false;
}, 7000);
});
});
}
#convertDateTimesToUserTimezone() {
const userTimezoneName = Intl.DateTimeFormat().resolvedOptions().timeZone;
document.querySelectorAll('time[data-convert-to-user-timezone]').forEach((timeElement) => {
const iso8601Datetime = timeElement.getAttribute('datetime');
const dateInUserTimezone = new Date(iso8601Datetime);
let options = {};
if (timeElement.hasAttribute('data-render-as-datetime')) {
options = {
year: 'numeric', month: 'long', day: 'numeric',
hour: 'numeric', minute: 'numeric', second: 'numeric'
};
} else if (timeElement.hasAttribute('data-render-as-date')) {
options = { year: 'numeric', month: 'long', day: 'numeric' };
} else if (timeElement.hasAttribute('data-render-as-time')) {
options = { hour: 'numeric', minute: 'numeric', second: 'numeric' };
}
if (timeElement.hasAttribute('data-render-with-millisecond-precision')) {
options.fractionalSecondDigits = 3;
}
/* dates/times are always rendered in English to match the rest of the Profiler interface */
timeElement.textContent = dateInUserTimezone.toLocaleString('en', options);
if (undefined !== userTimezoneName) {
const existingTitle = timeElement.getAttribute('title');
const newTitle = null === existingTitle
? `Date/times shown in your timezone: ${userTimezoneName}`
: existingTitle + ` (date/times shown in your timezone: ${userTimezoneName})`;
timeElement.setAttribute('title', newTitle);
}
});
}
}
</script>

View File

@@ -0,0 +1,25 @@
{% block toolbar %}
{% set icon %}
{{ source('@WebProfiler/Icon/symfony.svg') }}
<span class="sf-toolbar-value sf-toolbar-ajax-request-counter">
Loading&hellip;
</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Loading the web debug toolbar&hellip;</b>
</div>
<div class="sf-toolbar-info-piece">
Attempt #<span id="sfLoadCounter-{{ token }}"></span>
</div>
<div class="sf-toolbar-info-piece">
<b>
<button class="sf-cancel-button" type="button" id="sfLoadCancel-{{ token }}" title="Cancel loading">Cancel</button>
</b>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }}
{% endblock %}

View File

@@ -0,0 +1,12 @@
<div id="header">
<h1><a href="{{ path('_profiler_home') }}">{{ source('@WebProfiler/Icon/symfony.svg') }} Symfony Profiler</a></h1>
<div class="search">
<form method="get" action="https://symfony.com/search" target="_blank">
<div class="form-row">
<input name="q" id="search-id" type="search" placeholder="search on symfony.com" aria-label="Search on symfony.com">
<button type="submit" class="visually-hidden">Search</button>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,22 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% set messages = {
'no_token' : {
status: 'error',
title: (token|default('') == 'latest') ? 'There are no profiles' : 'Token not found',
message: (token|default('') == 'latest') ? 'No profiles found.' : 'Token "' ~ token|default('') ~ '" not found.'
}
} %}
{% block summary %}
<div class="status status-{{ messages[about].status }}">
<div class="container">
<h2>{{ messages[about].status|title }}</h2>
</div>
</div>
{% endblock %}
{% block panel %}
<h2>{{ messages[about].title }}</h2>
<p>{{ messages[about].message }}</p>
{% endblock %}

View File

@@ -0,0 +1,76 @@
{% extends '@WebProfiler/Profiler/base.html.twig' %}
{% block body %}
<div class="container">
{{ include('@WebProfiler/Profiler/header.html.twig', {profile_type: profile_type}, with_context = false) }}
<div id="summary">
{% block summary %}
{% if profile is defined %}
{% set request_collector = profile.collectors.request|default(false) %}
{{ include('@WebProfiler/Profiler/_%s_summary.html.twig'|format(profile_type), {
profile: profile,
command_collector: profile.collectors.command|default(false) ,
request_collector: request_collector,
request: request,
token: token
}, with_context=false) }}
{% endif %}
{% endblock %}
</div>
<div id="content">
<div id="main">
<div id="sidebar">
{% block sidebar %}
<div id="sidebar-contents">
<div id="sidebar-shortcuts">
{% block sidebar_shortcuts_links %}
<div class="shortcuts">
<a class="btn btn-link" href="{{ path('_profiler_search', { limit: 10, type: profile_type }) }}">{{ source('@WebProfiler/Icon/search.svg') }} Search profiles</a>
<a class="btn btn-link" href="{{ path('_profiler', { token: 'latest', type: profile_type }|merge(request.query.all)) }}">Latest</a>
</div>
{% endblock sidebar_shortcuts_links %}
</div>
{% if templates is defined %}
<ul id="menu-profiler">
{% if 'request' is same as(profile_type) %}
{% set excludes = ['command'] %}
{% elseif 'command' is same as(profile_type) %}
{% set excludes = ['request', 'router'] %}
{% endif %}
{% for name, template in templates|filter((t, n) => n not in excludes) %}
{% set menu -%}
{%- if block('menu', template) is defined -%}
{% with { collector: profile.getcollector(name), profiler_markup_version: profiler_markup_version } %}
{{- block('menu', template) -}}
{% endwith %}
{%- endif -%}
{%- endset %}
{% if menu is not empty %}
<li class="{{ name }} {{ name == panel ? 'selected' }}">
<a href="{{ path('_profiler', { token: token, panel: name, type: profile_type }) }}">{{ menu|raw }}</a>
</li>
{% endif %}
{% endfor %}
</ul>
{% endif %}
</div>
{{ include('@WebProfiler/Profiler/settings.html.twig') }}
{% endblock sidebar %}
</div>
<div id="collector-wrapper">
<div id="collector-content">
{{ include('@WebProfiler/Profiler/base_js.html.twig') }}
{% block panel '' %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,80 @@
#header {
margin-bottom: 30px;
}
#source {
background: var(--page-background);
border: 1px solid var(--menu-border-color);
box-shadow: 0 0 0 5px var(--page-background);
border-radius: 6px;
margin: 0 30px 45px 0;
max-width: 960px;
padding: 15px 20px;
}
.width-full #source {
max-width: unset;
width: 100%;
}
#source code {
font-size: 15px;
}
#source .source-file-name {
border-bottom: 1px solid var(--table-border-color);
font-size: 18px;
font-weight: 500;
margin: 0 0 15px 0;
padding: 0 0 15px;
}
#source .source-file-name small {
color: var(--color-muted);
}
#source .source-content {
overflow-x: auto;
}
#source .source-content ol {
margin: 0;
}
#source .source-content ol li {
margin: 0 0 2px 0;
padding-left: 5px;
white-space: preserve nowrap;
}
#source .source-content ol li::marker {
color: var(--color-muted);
font-family: var(--font-family-monospace);
padding-right: 5px;
}
#source .source-content li.selected {
background: var(--yellow-100);
border-radius: 4px;
}
.theme-dark #source .source-content li.selected {
background: var(--gray-600);
}
#source .source-content li.selected::marker {
color: var(--color-text);
font-weight: bold;
}
#source span[style="color: #FF8000"] { color: var(--highlight-comment) !important; }
#source span[style="color: #007700"] { color: var(--highlight-keyword) !important; }
#source span[style="color: #0000BB"] { color: var(--color-text) !important; }
#source span[style="color: #DD0000"] { color: var(--highlight-string) !important; }
.file-metadata dt {
color: var(--header-metadata-key);
display: block;
font-weight: bold;
}
.file-metadata dd {
color: var(--header-metadata-value);
margin: 5px 0 20px;
/* needed to break the long file paths */
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-all;
}

Some files were not shown because too many files have changed in this diff Show More