Skip to content

Navigation Menu

Sign in
Appearance settings
Sign up
Appearance settings

Commit b0b98ec

Browse filesBrowse the repository at this point in the historyBrowse files
committed
Add a command to dump information of the Symfony Profiler
1 parent 13f5330 commit b0b98ec
Copy full SHA for b0b98ec

10 files changed

+1,126-1Lines changed: 1126 additions & 1 deletion

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md‎

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ CHANGELOG
99
* Deprecate the `framework.ide` config option, use the `SYMFONY_IDE` env var instead
1010
* Allow prefixing entries with `!` in `framework.workflows.<name>.events_to_dispatch` to permanently disable an event; e.g. `events_to_dispatch: ['!workflow.announce']` fires every event except `workflow.announce`. The GuardEvent can never be disabled; `!workflow.guard` is rejected at config compile time. Mixing allow-list and block-list entries in the same list is rejected at config compile time too.
1111
* Add support for the HttpClient `max_connect_duration` option to the `http_client` configuration
12+
* Add `profiler:dump` command to get profiler information in machine-readable formats
1213

1314
8.1
1415
---
Collapse file
+204Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\FrameworkBundle\Command;
13+
14+
use Symfony\Component\Console\Attribute\AsCommand;
15+
use Symfony\Component\Console\Command\Command;
16+
use Symfony\Component\Console\Completion\CompletionInput;
17+
use Symfony\Component\Console\Completion\CompletionSuggestions;
18+
use Symfony\Component\Console\Exception\InvalidArgumentException;
19+
use Symfony\Component\Console\Input\InputArgument;
20+
use Symfony\Component\Console\Input\InputInterface;
21+
use Symfony\Component\Console\Input\InputOption;
22+
use Symfony\Component\Console\Output\OutputInterface;
23+
use Symfony\Component\Console\Style\SymfonyStyle;
24+
use Symfony\Component\HttpKernel\Profiler\ProfileDumper;
25+
use Symfony\Component\HttpKernel\Profiler\Profiler;
26+
27+
/**
28+
* Get profiler information in machine-readable formats.
29+
*
30+
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
31+
*
32+
* @final
33+
*/
34+
#[AsCommand(name: 'profiler:dump', description: 'Get profiler information for a given profile in Markdown or JSON format')]
35+
class ProfilerDumpCommand extends Command
36+
{
37+
public function __construct(
38+
private Profiler $profiler,
39+
) {
40+
parent::__construct();
41+
}
42+
43+
protected function configure(): void
44+
{
45+
$this
46+
->setDefinition([
47+
new InputArgument('token', InputArgument::OPTIONAL, 'The profile token', 'latest'),
48+
new InputOption('list', null, InputOption::VALUE_NONE, 'List recent profiles instead of dumping one'),
49+
new InputOption('panel', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Dump only the given panel(s), e.g. "db" or "logger"'),
50+
new InputOption('type', null, InputOption::VALUE_REQUIRED, 'The profile type ("request" or "command") [default: "request"]'),
51+
new InputOption('format', null, InputOption::VALUE_REQUIRED, \sprintf('The output format ("%s")', implode('", "', $this->getAvailableFormatOptions())), 'md'),
52+
new InputOption('full', null, InputOption::VALUE_NONE, 'Do not truncate long values in the output'),
53+
new InputOption('limit', null, InputOption::VALUE_REQUIRED, 'The maximum number of profiles to list', 20),
54+
new InputOption('url', null, InputOption::VALUE_REQUIRED, 'Filter profiles by URL'),
55+
new InputOption('method', null, InputOption::VALUE_REQUIRED, 'Filter profiles by HTTP method'),
56+
new InputOption('status-code', null, InputOption::VALUE_REQUIRED, 'Filter profiles by HTTP status code'),
57+
])
58+
->setHelp(<<<'EOF'
59+
The <info>%command.name%</info> command dumps the data collected by the profiler
60+
for a given profile in a format suitable for terminal consumers such as AI agents
61+
(long values are truncated in the output unless the <info>--full</info> option is used):
62+
63+
<info>php %command.full_name%</info> # dump the most recent HTTP profile
64+
<info>php %command.full_name% dd93a8</info> # dump the profile for the given token
65+
<info>php %command.full_name% --panel=db</info> # dump only the database panel
66+
<info>php %command.full_name% --status-code=500</info> # dump the most recent failed request
67+
<info>php %command.full_name% --type=command</info> # dump the most recent profiled console command
68+
<info>php %command.full_name% --format=json</info> # machine-readable output
69+
<info>php %command.full_name% --panel=request --full</info> # dump the request panel without truncating values
70+
71+
Use the <info>--list</info> option to list the most recent profiles:
72+
73+
<info>php %command.full_name% --list</info>
74+
<info>php %command.full_name% --list --limit=50 --method=POST</info>
75+
76+
Beware that the output may contain sensitive data collected from the requests
77+
(headers, cookies, query parameters, etc.).
78+
EOF
79+
)
80+
;
81+
}
82+
83+
protected function execute(InputInterface $input, OutputInterface $output): int
84+
{
85+
$io = new SymfonyStyle($input, $output);
86+
87+
$format = $input->getOption('format');
88+
if (!\in_array($format, $this->getAvailableFormatOptions(), true)) {
89+
throw new InvalidArgumentException(\sprintf('Supported formats are "%s".', implode('", "', $this->getAvailableFormatOptions())));
90+
}
91+
92+
if ($input->getOption('list')) {
93+
return $this->listProfiles($input, $output, $io, $format);
94+
}
95+
96+
$type = $input->getOption('type') ?? 'request';
97+
$token = $input->getArgument('token');
98+
if ('latest' === $token) {
99+
$latest = current($this->profiler->find(null, $input->getOption('url'), 1, $input->getOption('method'), null, null, $input->getOption('status-code'), static fn (array $profile) => $type === ($profile['virtual_type'] ?? 'request')));
100+
if (!$latest) {
101+
$io->error(\sprintf('No profiles found for type "%s". Make some requests to the application first, or profile console commands by running them with the --profile option.', $type));
102+
103+
return Command::FAILURE;
104+
}
105+
106+
$token = $latest['token'];
107+
}
108+
109+
if (!$profile = $this->profiler->loadProfile($token)) {
110+
$io->error(\sprintf('No profile found for token "%s". Run this command with the --list option to list the available profiles.', $token));
111+
112+
return Command::FAILURE;
113+
}
114+
115+
$panels = $input->getOption('panel') ?: null;
116+
$dumper = $input->getOption('full') ? new ProfileDumper(\PHP_INT_MAX, \PHP_INT_MAX, \PHP_INT_MAX, \PHP_INT_MAX) : new ProfileDumper();
117+
118+
try {
119+
$dump = 'json' === $format
120+
? json_encode($dumper->toArray($profile, $panels), \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_INVALID_UTF8_SUBSTITUTE)
121+
: $dumper->toMarkdown($profile, $panels);
122+
} catch (\InvalidArgumentException $e) {
123+
$io->error($e->getMessage());
124+
125+
return Command::FAILURE;
126+
}
127+
128+
$output->writeln($dump, OutputInterface::OUTPUT_RAW);
129+
130+
return Command::SUCCESS;
131+
}
132+
133+
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
134+
{
135+
if ($input->mustSuggestArgumentValuesFor('token')) {
136+
$suggestions->suggestValue('latest');
137+
foreach ($this->profiler->find(null, null, 20, null, null, null) as $profile) {
138+
$suggestions->suggestValue($profile['token']);
139+
}
140+
}
141+
142+
if ($input->mustSuggestOptionValuesFor('format')) {
143+
$suggestions->suggestValues($this->getAvailableFormatOptions());
144+
}
145+
146+
if ($input->mustSuggestOptionValuesFor('type')) {
147+
$suggestions->suggestValues(['request', 'command']);
148+
}
149+
}
150+
151+
private function listProfiles(InputInterface $input, OutputInterface $output, SymfonyStyle $io, string $format): int
152+
{
153+
$type = $input->getOption('type');
154+
$profiles = $this->profiler->find(
155+
null,
156+
$input->getOption('url'),
157+
$input->getOption('limit'),
158+
$input->getOption('method'),
159+
null,
160+
null,
161+
$input->getOption('status-code'),
162+
null !== $type ? static fn (array $profile) => $type === ($profile['virtual_type'] ?? 'request') : null,
163+
);
164+
165+
$profiles = array_map(static fn (array $profile) => [
166+
'token' => $profile['token'],
167+
'time' => $profile['time'] ? \DateTimeImmutable::createFromFormat('U', (string) $profile['time'])->format(\DateTimeInterface::RFC3339) : null,
168+
'method' => $profile['method'],
169+
'url' => $profile['url'],
170+
'status_code' => $profile['status_code'] ?? null,
171+
'type' => $profile['virtual_type'] ?? 'request',
172+
'has_errors' => (bool) ($profile['has_errors'] ?? false),
173+
], $profiles);
174+
175+
if ('json' === $format) {
176+
$output->writeln(json_encode($profiles, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_INVALID_UTF8_SUBSTITUTE), OutputInterface::OUTPUT_RAW);
177+
178+
return Command::SUCCESS;
179+
}
180+
181+
if (!$profiles) {
182+
$io->error('No profiles found. Make some requests to the application first, or profile console commands by running them with the --profile option.');
183+
184+
return Command::FAILURE;
185+
}
186+
187+
$rows = ['| TOKEN | TIME | METHOD | URL | STATUS | TYPE | ERRORS |', '| ----- | ---- | ------ | --- | ------ | ---- | ------ |'];
188+
foreach ($profiles as $profile) {
189+
$rows[] = \sprintf('| %s | %s | %s | %s | %s | %s | %s |', $profile['token'], $profile['time'] ?? 'n/a', $profile['method'], $profile['url'], $profile['status_code'] ?? 'n/a', $profile['type'], $profile['has_errors'] ? 'yes' : 'no');
190+
}
191+
$rows[] = '';
192+
$rows[] = 'Run "profiler:dump <token>" to inspect any of these profiles.';
193+
194+
$output->writeln($rows, OutputInterface::OUTPUT_RAW);
195+
196+
return Command::SUCCESS;
197+
}
198+
199+
/** @return string[] */
200+
private function getAvailableFormatOptions(): array
201+
{
202+
return ['md', 'json'];
203+
}
204+
}
Collapse file

‎src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php‎

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,8 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $
911911
// this is needed for the WebProfiler to work even if the profiler is disabled
912912
$container->setParameter('data_collector.templates', []);
913913

914+
$container->removeDefinition('console.command.profiler_dump');
915+
914916
return;
915917
}
916918

Collapse file

‎src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php‎

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php
+7Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use Symfony\Bundle\FrameworkBundle\Command\ContainerLintCommand;
2727
use Symfony\Bundle\FrameworkBundle\Command\DebugAutowiringCommand;
2828
use Symfony\Bundle\FrameworkBundle\Command\EventDispatcherDebugCommand;
29+
use Symfony\Bundle\FrameworkBundle\Command\ProfilerDumpCommand;
2930
use Symfony\Bundle\FrameworkBundle\Command\RouterDebugCommand;
3031
use Symfony\Bundle\FrameworkBundle\Command\RouterMatchCommand;
3132
use Symfony\Bundle\FrameworkBundle\Command\SecretsDecryptToLocalCommand;
@@ -230,6 +231,12 @@
230231
])
231232
->tag('console.command')
232233

234+
->set('console.command.profiler_dump', ProfilerDumpCommand::class)
235+
->args([
236+
service('profiler'),
237+
])
238+
->tag('console.command')
239+
233240
->set('console.command.router_debug', RouterDebugCommand::class)
234241
->args([
235242
service('router'),

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.