Skip to content

Commit 11de26c

Browse files
feature #47416 [Console][FrameworkBundle][HttpKernel][WebProfilerBundle] Enable profiling commands (HeahDude)
This PR was merged into the 6.4 branch. Discussion ---------- [Console][FrameworkBundle][HttpKernel][WebProfilerBundle] Enable profiling commands | Q | A | ------------- | --- | Branch? | 6.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | Fix #45241 | License | MIT | Doc PR | ~ TLDR; I've shown a POC of this feature at the Symfony Live Paris last April to some of the core team members (ping `@nicolas`-grekas, `@stof`, `@lyrixx`, `@chalasr`, `@GromNaN`). I propose here a new work from scratch addressing the comments I already got and based on Javier's profiler redesign (#47148). Reviews should better be done by commits. Summary --------- This PR aims to leverage the profiler and its collectors by using a `VirtualRequestStack` to aggregate data on virtual requests. Such requests are obfuscated by default to avoid side effects. It can feel like a hack... or a pragmatic way to get, without much complexity, tons of useful feedback on what's going on during console execution, from basic info about the command, time/memory metrics, to every existing features already available in HTTP context: events, message dispatching, http requests, emails, serialization, validation, cache, database queries... and so on, all that just out of the box! Previous work -------------- There were some work to extract the Profiler logic in a dedicated component, that proved to require a lot of complexity and BC breaks in the API: * #10374 * #14809 (see symfony/symfony#14809 (comment)) * #20502 Screenshots ------------ For now I've focused only on the functional parts. <details><summary>Search view</summary> <img width="1221" alt="Screenshot 2022-08-28 at 11 29 25 PM" src="https://user-images.githubusercontent.com/10107633/187095381-851f6be5-cf8c-4fec-aa7b-9f9f80bf8404.png"> </details> <details><summary>Command panel</summary> <img width="1210" alt="Screenshot 2022-08-28 at 11 30 54 PM" src="https://user-images.githubusercontent.com/10107633/187095971-de8f9b85-eeb4-48cf-aff7-fdac0c6f9264.png"> <img width="974" alt="Screenshot 2022-08-28 at 11 31 08 PM" src="https://user-images.githubusercontent.com/10107633/187095980-337f4373-ebe5-4de5-bfb4-3715be868274.png"> <img width="962" alt="Screenshot 2022-08-28 at 11 31 21 PM" src="https://user-images.githubusercontent.com/10107633/187096022-ab18f70a-704a-4c75-81a6-43ca5b66eb9a.png"> <img width="964" alt="Screenshot 2022-08-28 at 11 31 34 PM" src="https://user-images.githubusercontent.com/10107633/187096037-cc45805e-ba65-447f-bca6-2d2ea38239b8.png"> If the command is signal-able the following panel will be available: <img width="961" alt="Screenshot 2022-08-28 at 11 31 46 PM" src="https://user-images.githubusercontent.com/10107633/187096084-2f6a39be-a780-411b-9000-b9ae3407e82b.png"> If sub commands are run using `$this->getApplication()->run()` sub profiles will be shown as for requests: <img width="696" alt="Screenshot 2022-08-28 at 11 31 56 PM" src="https://user-images.githubusercontent.com/10107633/187096105-bb7e4a84-42bc-47ed-9f58-527a771c48cc.png"> </details> The server tab is the same as in the request panel. <details><summary>Performance panel</summary> <img width="977" alt="Screenshot 2022-08-28 at 11 32 23 PM" src="https://user-images.githubusercontent.com/10107633/187096138-3ff3f347-61c7-4ade-8c73-b48d5b504c04.png"> <img width="969" alt="Screenshot 2022-08-28 at 11 32 32 PM" src="https://user-images.githubusercontent.com/10107633/187096168-35be4773-4941-4e5e-8dd4-f6cc009e5d48.png"> </details> <details> <summary>Failing command</summary> The exception panel is shown by default as for requests: <img width="1217" alt="Screenshot 2022-08-28 at 11 33 42 PM" src="https://user-images.githubusercontent.com/10107633/187096210-7b206c72-c2e4-4eb3-9978-916cd3dd6cd6.png"> </details> <details> <summary>Sub command</summary> <img width="1217" alt="Screenshot 2022-08-28 at 11 33 19 PM" src="https://user-images.githubusercontent.com/10107633/187096188-a090fb91-b7b8-4f98-a1d7-99b3605bf48b.png"> </details> <details> <summary>Profile token when verbose</summary> (clickable links with compatible terminals) <img width="534" alt="Screenshot 2022-08-28 at 11 26 51 PM" src="https://user-images.githubusercontent.com/10107633/187096349-8f7619b2-feb4-427c-a315-f4a844536316.png"> </details> <details> <summary>Command interrupted by signal</summary> <img width="1246" alt="Screenshot 2022-10-22 at 4 16 37 PM" src="https://user-images.githubusercontent.com/10107633/197344164-50d72a25-a6e7-4e77-ad87-2d5f54b29b93.png"> </details> Opt-in profiling --------------- Use the new global option `--profile` (in debug only) to profile a command. Future scopes -------------- * When I've discussed the limitation of profiling long running processes such as `messenger:consume` with `@GromNaN` (one of the reasons why I've added an `excludes` option), he told that it would be nice it we could find a way to profile consumers as well. So I've added ~an abstract `VirtualRequest`~ a `_virtual_type` request attribute and a `virtualType` property to profiles, that will allow to create a `MessengerWorkerRequest` and a new type of profile with ease in a follow-up PR if the current implementation is accepted. * We could add some dedicated casters for input and output in the `VarDumper` component (/cc `@nicolas`-grekas) * It could be interesting to decorate and collect traces from some helpers (i.e. when running processes) * ~Add a global option in debug to enable/disable the profiler on the fly when running commands (e.g. a negatable `--profile` flag)~ **[update] implemented in current scope in replacement of semantic config.** * Extract profiling to a new component. Limitations ----------- * ~No sub profiles are created when using `$this->getApplication()->find(...)->run()` because events (needed by the profiler to hook into) are dispatched from `Application::run()`, not from `Command::run()`.~ **[update] The docs has been updated in symfony/symfony-docs#18741 * ~No profiles are created when killing the command process (i.e. using `ctrl-C`, should we add a handler to some signals to force saving profiles?~ **[update] I've added support for this.** * ~Signals as int may not be as useful as they could in the profiler pages, does it worth trying to add a label to some (knowing that some signals can have different constants (labels) with the same int value)?~ **[update] done thanks to symfony/symfony#50663 * ~Long running processes should be excluded via configuration to avoid memory leaks~ **[update] profiling is now opt-in using the `--profile` option.** * Profiling `messenger:consume` does not work since the kernel is reset after handling a message. __________________ TODO ------ * [x] I've left some todos inside the code for reviewers to share they thought before I try going further * [x] Add a few tests * [ ] Get help for the UI (new top nav, ~svg for the command panel~) /cc `@javiereguiluz` * ~PR on `symfony/recipes` to add the new `framework.profiler.cli` node~ Commits ------- 82914bab0d [Console][FrameworkBundle][HttpKernel][WebProfilerBundle] Enable profiling commands
2 parents caff0d9 + b6887eb commit 11de26c

File tree

8 files changed

+225
-14
lines changed

8 files changed

+225
-14
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ CHANGELOG
3333
* Add parameters deprecations to the output of `debug:container` command
3434
* Change `framework.asset_mapper.importmap_polyfill` from a URL to the name of an item in the importmap
3535
* Provide `$buildDir` when running `CacheWarmer` to build read-only resources
36+
* Add the global `--profile` option to the console to enable profiling commands
3637

3738
6.3
3839
---

Console/Application.php

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
use Symfony\Component\Console\Application as BaseApplication;
1515
use Symfony\Component\Console\Command\Command;
1616
use Symfony\Component\Console\Command\ListCommand;
17+
use Symfony\Component\Console\Command\TraceableCommand;
18+
use Symfony\Component\Console\Debug\CliRequest;
1719
use Symfony\Component\Console\Input\InputInterface;
1820
use Symfony\Component\Console\Input\InputOption;
1921
use Symfony\Component\Console\Output\ConsoleOutputInterface;
@@ -42,6 +44,7 @@ public function __construct(KernelInterface $kernel)
4244
$inputDefinition = $this->getDefinition();
4345
$inputDefinition->addOption(new InputOption('--env', '-e', InputOption::VALUE_REQUIRED, 'The Environment name.', $kernel->getEnvironment()));
4446
$inputDefinition->addOption(new InputOption('--no-debug', null, InputOption::VALUE_NONE, 'Switch off debug mode.'));
47+
$inputDefinition->addOption(new InputOption('--profile', null, InputOption::VALUE_NONE, 'Enables profiling (requires debug).'));
4548
}
4649

4750
/**
@@ -79,18 +82,47 @@ public function doRun(InputInterface $input, OutputInterface $output): int
7982

8083
protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output): int
8184
{
85+
$requestStack = null;
86+
$renderRegistrationErrors = true;
87+
8288
if (!$command instanceof ListCommand) {
8389
if ($this->registrationErrors) {
8490
$this->renderRegistrationErrors($input, $output);
8591
$this->registrationErrors = [];
92+
$renderRegistrationErrors = false;
8693
}
94+
}
95+
96+
if ($input->hasParameterOption('--profile')) {
97+
$container = $this->kernel->getContainer();
8798

88-
return parent::doRunCommand($command, $input, $output);
99+
if (!$this->kernel->isDebug()) {
100+
if ($output instanceof ConsoleOutputInterface) {
101+
$output = $output->getErrorOutput();
102+
}
103+
104+
(new SymfonyStyle($input, $output))->warning('Debug mode should be enabled when the "--profile" option is used.');
105+
} elseif (!$container->has('debug.stopwatch')) {
106+
if ($output instanceof ConsoleOutputInterface) {
107+
$output = $output->getErrorOutput();
108+
}
109+
110+
(new SymfonyStyle($input, $output))->warning('The "--profile" option needs the Stopwatch component. Try running "composer require symfony/stopwatch".');
111+
} else {
112+
$command = new TraceableCommand($command, $container->get('debug.stopwatch'));
113+
114+
$requestStack = $container->get('.virtual_request_stack');
115+
$requestStack->push(new CliRequest($command));
116+
}
89117
}
90118

91-
$returnCode = parent::doRunCommand($command, $input, $output);
119+
try {
120+
$returnCode = parent::doRunCommand($command, $input, $output);
121+
} finally {
122+
$requestStack?->pop();
123+
}
92124

93-
if ($this->registrationErrors) {
125+
if ($renderRegistrationErrors && $this->registrationErrors) {
94126
$this->renderRegistrationErrors($input, $output);
95127
$this->registrationErrors = [];
96128
}

DependencyInjection/FrameworkExtension.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
use Symfony\Component\Config\ResourceCheckerInterface;
5151
use Symfony\Component\Console\Application;
5252
use Symfony\Component\Console\Command\Command;
53+
use Symfony\Component\Console\Debug\CliRequest;
5354
use Symfony\Component\Console\Messenger\RunCommandMessageHandler;
5455
use Symfony\Component\DependencyInjection\Alias;
5556
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
@@ -912,6 +913,10 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $
912913

913914
$container->getDefinition('profiler_listener')
914915
->addArgument($config['collect_parameter']);
916+
917+
if (!$container->getParameter('kernel.debug') || !class_exists(CliRequest::class) || !$container->has('debug.stopwatch')) {
918+
$container->removeDefinition('console_profiler_listener');
919+
}
915920
}
916921

917922
private function registerWorkflowConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void
@@ -1134,15 +1139,16 @@ private function registerDebugConfiguration(array $config, ContainerBuilder $con
11341139
{
11351140
$loader->load('debug_prod.php');
11361141

1142+
$debug = $container->getParameter('kernel.debug');
1143+
11371144
if (class_exists(Stopwatch::class)) {
11381145
$container->register('debug.stopwatch', Stopwatch::class)
11391146
->addArgument(true)
1147+
->setPublic($debug)
11401148
->addTag('kernel.reset', ['method' => 'reset']);
11411149
$container->setAlias(Stopwatch::class, new Alias('debug.stopwatch', false));
11421150
}
11431151

1144-
$debug = $container->getParameter('kernel.debug');
1145-
11461152
if ($debug && !$container->hasParameter('debug.container.dump')) {
11471153
$container->setParameter('debug.container.dump', '%kernel.build_dir%/%kernel.container_class%.xml');
11481154
}
@@ -1165,7 +1171,7 @@ private function registerDebugConfiguration(array $config, ContainerBuilder $con
11651171

11661172
if ($debug && class_exists(DebugProcessor::class)) {
11671173
$definition = new Definition(DebugProcessor::class);
1168-
$definition->addArgument(new Reference('request_stack'));
1174+
$definition->addArgument(new Reference('.virtual_request_stack'));
11691175
$definition->addTag('kernel.reset', ['method' => 'reset']);
11701176
$container->setDefinition('debug.log_processor', $definition);
11711177

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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\EventListener;
13+
14+
use Symfony\Component\Console\ConsoleEvents;
15+
use Symfony\Component\Console\Debug\CliRequest;
16+
use Symfony\Component\Console\Event\ConsoleCommandEvent;
17+
use Symfony\Component\Console\Event\ConsoleErrorEvent;
18+
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
19+
use Symfony\Component\Console\Output\ConsoleOutputInterface;
20+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
21+
use Symfony\Component\HttpFoundation\Request;
22+
use Symfony\Component\HttpFoundation\RequestStack;
23+
use Symfony\Component\HttpKernel\Profiler\Profile;
24+
use Symfony\Component\HttpKernel\Profiler\Profiler;
25+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
26+
use Symfony\Component\Stopwatch\Stopwatch;
27+
28+
/**
29+
* @internal
30+
*
31+
* @author Jules Pietri <jules@heahprod.com>
32+
*/
33+
final class ConsoleProfilerListener implements EventSubscriberInterface
34+
{
35+
private ?\Throwable $error = null;
36+
/** @var \SplObjectStorage<Request, Profile> */
37+
private \SplObjectStorage $profiles;
38+
/** @var \SplObjectStorage<Request, ?Request> */
39+
private \SplObjectStorage $parents;
40+
41+
public function __construct(
42+
private readonly Profiler $profiler,
43+
private readonly RequestStack $requestStack,
44+
private readonly Stopwatch $stopwatch,
45+
private readonly UrlGeneratorInterface $urlGenerator,
46+
) {
47+
$this->profiles = new \SplObjectStorage();
48+
$this->parents = new \SplObjectStorage();
49+
}
50+
51+
public static function getSubscribedEvents(): array
52+
{
53+
return [
54+
ConsoleEvents::COMMAND => ['initialize', 4096],
55+
ConsoleEvents::ERROR => ['catch', -2048],
56+
ConsoleEvents::TERMINATE => ['profile', -4096],
57+
];
58+
}
59+
60+
public function initialize(ConsoleCommandEvent $event): void
61+
{
62+
if (!$event->getInput()->getOption('profile')) {
63+
$this->profiler->disable();
64+
65+
return;
66+
}
67+
68+
$request = $this->requestStack->getCurrentRequest();
69+
70+
if (!$request instanceof CliRequest || $request->command !== $event->getCommand()) {
71+
return;
72+
}
73+
74+
$request->attributes->set('_stopwatch_token', substr(hash('sha256', uniqid(mt_rand(), true)), 0, 6));
75+
$this->stopwatch->openSection();
76+
}
77+
78+
public function catch(ConsoleErrorEvent $event): void
79+
{
80+
$this->error = $event->getError();
81+
}
82+
83+
public function profile(ConsoleTerminateEvent $event): void
84+
{
85+
if (!$this->profiler->isEnabled()) {
86+
return;
87+
}
88+
89+
$request = $this->requestStack->getCurrentRequest();
90+
91+
if (!$request instanceof CliRequest || $request->command !== $event->getCommand()) {
92+
return;
93+
}
94+
95+
if (null !== $sectionId = $request->attributes->get('_stopwatch_token')) {
96+
// we must close the section before saving the profile to allow late collect
97+
try {
98+
$this->stopwatch->stopSection($sectionId);
99+
} catch (\LogicException) {
100+
// noop
101+
}
102+
}
103+
104+
$request->command->exitCode = $event->getExitCode();
105+
$request->command->interruptedBySignal = $event->getInterruptingSignal();
106+
107+
$profile = $this->profiler->collect($request, $request->getResponse(), $this->error);
108+
$this->error = null;
109+
$this->profiles[$request] = $profile;
110+
111+
if ($this->parents[$request] = $this->requestStack->getParentRequest()) {
112+
// do not save on sub commands
113+
return;
114+
}
115+
116+
// attach children to parents
117+
foreach ($this->profiles as $request) {
118+
if (null !== $parentRequest = $this->parents[$request]) {
119+
if (isset($this->profiles[$parentRequest])) {
120+
$this->profiles[$parentRequest]->addChild($this->profiles[$request]);
121+
}
122+
}
123+
}
124+
125+
$output = $event->getOutput();
126+
$output = $output instanceof ConsoleOutputInterface && $output->isVerbose() ? $output->getErrorOutput() : null;
127+
128+
// save profiles
129+
foreach ($this->profiles as $r) {
130+
$p = $this->profiles[$r];
131+
$this->profiler->saveProfile($p);
132+
133+
$token = $p->getToken();
134+
$output?->writeln(sprintf(
135+
'See profile <href=%s>%s</>',
136+
$this->urlGenerator->generate('_profiler', ['token' => $token], UrlGeneratorInterface::ABSOLUTE_URL),
137+
$token
138+
));
139+
}
140+
141+
$this->profiles = new \SplObjectStorage();
142+
$this->parents = new \SplObjectStorage();
143+
}
144+
}

Resources/config/collectors.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
1313

1414
use Symfony\Bundle\FrameworkBundle\DataCollector\RouterDataCollector;
15+
use Symfony\Component\Console\DataCollector\CommandDataCollector;
1516
use Symfony\Component\HttpKernel\DataCollector\AjaxDataCollector;
1617
use Symfony\Component\HttpKernel\DataCollector\ConfigDataCollector;
1718
use Symfony\Component\HttpKernel\DataCollector\EventDataCollector;
@@ -30,7 +31,7 @@
3031

3132
->set('data_collector.request', RequestDataCollector::class)
3233
->args([
33-
service('request_stack')->ignoreOnInvalid(),
34+
service('.virtual_request_stack')->ignoreOnInvalid(),
3435
])
3536
->tag('kernel.event_subscriber')
3637
->tag('data_collector', ['template' => '@WebProfiler/Collector/request.html.twig', 'id' => 'request', 'priority' => 335])
@@ -48,15 +49,15 @@
4849
->set('data_collector.events', EventDataCollector::class)
4950
->args([
5051
tagged_iterator('event_dispatcher.dispatcher', 'name'),
51-
service('request_stack')->ignoreOnInvalid(),
52+
service('.virtual_request_stack')->ignoreOnInvalid(),
5253
])
5354
->tag('data_collector', ['template' => '@WebProfiler/Collector/events.html.twig', 'id' => 'events', 'priority' => 290])
5455

5556
->set('data_collector.logger', LoggerDataCollector::class)
5657
->args([
5758
service('logger')->ignoreOnInvalid(),
5859
sprintf('%s/%s', param('kernel.build_dir'), param('kernel.container_class')),
59-
service('request_stack')->ignoreOnInvalid(),
60+
service('.virtual_request_stack')->ignoreOnInvalid(),
6061
])
6162
->tag('monolog.logger', ['channel' => 'profiler'])
6263
->tag('data_collector', ['template' => '@WebProfiler/Collector/logger.html.twig', 'id' => 'logger', 'priority' => 300])
@@ -74,5 +75,8 @@
7475
->set('data_collector.router', RouterDataCollector::class)
7576
->tag('kernel.event_listener', ['event' => KernelEvents::CONTROLLER, 'method' => 'onKernelController'])
7677
->tag('data_collector', ['template' => '@WebProfiler/Collector/router.html.twig', 'id' => 'router', 'priority' => 285])
78+
79+
->set('.data_collector.command', CommandDataCollector::class)
80+
->tag('data_collector', ['template' => '@WebProfiler/Collector/command.html.twig', 'id' => 'command', 'priority' => 335])
7781
;
7882
};

Resources/config/debug.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\HttpKernel\Controller\TraceableArgumentResolver;
1616
use Symfony\Component\HttpKernel\Controller\TraceableControllerResolver;
1717
use Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher;
18+
use Symfony\Component\HttpKernel\Debug\VirtualRequestStack;
1819

1920
return static function (ContainerConfigurator $container) {
2021
$container->services()
@@ -24,7 +25,7 @@
2425
service('debug.event_dispatcher.inner'),
2526
service('debug.stopwatch'),
2627
service('logger')->nullOnInvalid(),
27-
service('request_stack')->nullOnInvalid(),
28+
service('.virtual_request_stack')->nullOnInvalid(),
2829
])
2930
->tag('monolog.logger', ['channel' => 'event'])
3031
->tag('kernel.reset', ['method' => 'reset'])
@@ -46,5 +47,9 @@
4647
->set('argument_resolver.not_tagged_controller', NotTaggedControllerValueResolver::class)
4748
->args([abstract_arg('Controller argument, set in FrameworkExtension')])
4849
->tag('controller.argument_value_resolver', ['priority' => -200])
50+
51+
->set('.virtual_request_stack', VirtualRequestStack::class)
52+
->args([service('request_stack')])
53+
->public()
4954
;
5055
};

Resources/config/profiling.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
1313

14+
use Symfony\Bundle\FrameworkBundle\EventListener\ConsoleProfilerListener;
1415
use Symfony\Component\HttpKernel\EventListener\ProfilerListener;
1516
use Symfony\Component\HttpKernel\Profiler\FileProfilerStorage;
1617
use Symfony\Component\HttpKernel\Profiler\Profiler;
@@ -35,5 +36,14 @@
3536
param('profiler_listener.only_main_requests'),
3637
])
3738
->tag('kernel.event_subscriber')
39+
40+
->set('console_profiler_listener', ConsoleProfilerListener::class)
41+
->args([
42+
service('profiler'),
43+
service('.virtual_request_stack'),
44+
service('debug.stopwatch'),
45+
service('router'),
46+
])
47+
->tag('kernel.event_subscriber')
3848
;
3949
};

Tests/Console/ApplicationTest.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use Symfony\Component\DependencyInjection\ContainerInterface;
2727
use Symfony\Component\EventDispatcher\EventDispatcher;
2828
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
29+
use Symfony\Component\HttpFoundation\RequestStack;
2930
use Symfony\Component\HttpKernel\Bundle\Bundle;
3031
use Symfony\Component\HttpKernel\Bundle\BundleInterface;
3132
use Symfony\Component\HttpKernel\KernelInterface;
@@ -242,17 +243,25 @@ private function getKernel(array $bundles, $useDispatcher = false)
242243
{
243244
$container = $this->createMock(ContainerInterface::class);
244245

246+
$requestStack = $this->createMock(RequestStack::class);
247+
$requestStack->expects($this->any())
248+
->method('push')
249+
;
250+
245251
if ($useDispatcher) {
246252
$dispatcher = $this->createMock(EventDispatcherInterface::class);
247253
$dispatcher
248254
->expects($this->atLeastOnce())
249255
->method('dispatch')
250256
;
251-
$container
252-
->expects($this->atLeastOnce())
257+
258+
$container->expects($this->atLeastOnce())
253259
->method('get')
254-
->with($this->equalTo('event_dispatcher'))
255-
->willReturn($dispatcher);
260+
->willReturnMap([
261+
['.virtual_request_stack', 2, $requestStack],
262+
['event_dispatcher', 1, $dispatcher],
263+
])
264+
;
256265
}
257266

258267
$container

0 commit comments

Comments
 (0)