diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/messenger.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/messenger.html.twig index 908efe8771016..d8befbbf8dca6 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/messenger.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/messenger.html.twig @@ -4,17 +4,33 @@ {% block toolbar %} {% if collector.messages|length > 0 %} + {% set status_color = collector.exceptionsCount ? 'red' %} {% set icon %} {{ include('@WebProfiler/Icon/messenger.svg') }} {{ collector.messages|length }} {% endset %} - {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: 'messenger' }) }} + {% set text %} + {% for bus in collector.buses %} + {% set exceptionsCount = collector.exceptionsCount(bus) %} +
+ {{ bus }} + + {{ collector.messages(bus)|length }} + +
+ {% endfor %} + {% endset %} + + {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: 'messenger', status: status_color }) }} {% endif %} {% endblock %} {% block menu %} - + {{ include('@WebProfiler/Icon/messenger.svg') }} Messages @@ -24,7 +40,28 @@ {% endblock %} +{% block head %} + {{ parent() }} + +{% endblock %} + {% block panel %} + {% import _self as helper %} +

Messages

{% if collector.messages is empty %} @@ -32,41 +69,99 @@

No messages have been collected.

{% else %} - - - - - - - - - - {% for message in collector.messages %} - - - - - - {% endfor %} - -
BusMessageResult
{{ message.bus }} - {% if message.result.object is defined %} - {{ profiler_dump(message.message.object, maxDepth=2) }} - {% else %} - {{ message.message.type }} - {% endif %} - - {% if message.result.object is defined %} - {{ profiler_dump(message.result.object, maxDepth=2) }} - {% elseif message.result.type is defined %} - {{ message.result.type }} - {% if message.result.value is defined %} - {{ message.result.value }} - {% endif %} - {% endif %} - {% if message.exception.type is defined %} - {{ message.exception.type }} - {% endif %} -
+ +
+
+ {% set messages = collector.messages %} + {% set exceptionsCount = collector.exceptionsCount %} +

All{{ messages|length }}

+ +
+

Ordered list of dispatched messages across all your buses

+ {{ helper.render_bus_messages(messages, true) }} +
+
+ + {% for bus in collector.buses %} +
+ {% set messages = collector.messages(bus) %} + {% set exceptionsCount = collector.exceptionsCount(bus) %} +

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

+ +
+

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

+ {{ helper.render_bus_messages(messages) }} +
+
+ {% endfor %} {% endif %} + {% endblock %} + +{% macro render_bus_messages(messages, showBus = false) %} + {% set discr = random() %} + {% for i, dispatchCall in messages %} + + + + + + + + {% if showBus %} + + + + + {% endif %} + + + + + + + + + + + + + {% if dispatchCall.exception is defined %} + + + + + {% endif %} + +
+ {{ profiler_dump(dispatchCall.message.type) }} + {% if showBus %} + {{ dispatchCall.bus }} + {% endif %} + {% if dispatchCall.exception is defined %} + exception + {% endif %} + + {{ include('@Twig/images/icon-minus-square.svg') }} + {{ include('@Twig/images/icon-plus-square.svg') }} + +
Bus{{ dispatchCall.bus }}
Message{{ profiler_dump(dispatchCall.message.value, maxDepth=2) }}
Envelope items + {% for item in dispatchCall.envelopeItems %} + {{ profiler_dump(item) }} + {% else %} + No items + {% endfor %} +
Result + {% if dispatchCall.result is defined %} + {{ profiler_dump(dispatchCall.result.seek('value'), maxDepth=2) }} + {% elseif dispatchCall.exception is defined %} + No result as an exception occurred + {% endif %} +
Exception + {{ profiler_dump(dispatchCall.exception.value, maxDepth=1) }} +
+ {% endfor %} +{% endmacro %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig index 96cd8878a8091..f9bc41d6a1b54 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig @@ -215,6 +215,9 @@ table tbody ul { .text-muted { color: #999; } +.text-danger { + color: {{ colors.error|raw }}; +} .text-bold { font-weight: bold; } diff --git a/src/Symfony/Component/Messenger/DataCollector/MessengerDataCollector.php b/src/Symfony/Component/Messenger/DataCollector/MessengerDataCollector.php index 0fe44d62fea16..ed98f4cfa43f5 100644 --- a/src/Symfony/Component/Messenger/DataCollector/MessengerDataCollector.php +++ b/src/Symfony/Component/Messenger/DataCollector/MessengerDataCollector.php @@ -16,6 +16,8 @@ use Symfony\Component\HttpKernel\DataCollector\DataCollector; use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; use Symfony\Component\Messenger\TraceableMessageBus; +use Symfony\Component\VarDumper\Caster\ClassStub; +use Symfony\Component\VarDumper\Cloner\Data; /** * @author Samuel Roze @@ -44,13 +46,25 @@ public function collect(Request $request, Response $response, \Exception $except */ public function lateCollect() { - $this->data = array('messages' => array()); + $this->data = array('messages' => array(), 'buses' => array_keys($this->traceableBuses)); + $messages = array(); foreach ($this->traceableBuses as $busName => $bus) { foreach ($bus->getDispatchedMessages() as $message) { - $this->data['messages'][] = $this->collectMessage($busName, $message); + $debugRepresentation = $this->cloneVar($this->collectMessage($busName, $message)); + $messages[] = array($debugRepresentation, $message['callTime']); } } + + // Order by call time + usort($messages, function (array $a, array $b): int { + return $a[1] > $b[1] ? 1 : -1; + }); + + // Keep the messages clones only + $this->data['messages'] = array_map(function (array $item): Data { + return $item[0]; + }, $messages); } /** @@ -78,31 +92,19 @@ private function collectMessage(string $busName, array $tracedMessage) $debugRepresentation = array( 'bus' => $busName, + 'envelopeItems' => $tracedMessage['envelopeItems'] ?? null, 'message' => array( - 'type' => \get_class($message), - 'object' => $this->cloneVar($message), + 'type' => new ClassStub(\get_class($message)), + 'value' => $message, ), ); if (array_key_exists('result', $tracedMessage)) { $result = $tracedMessage['result']; - - if (\is_object($result)) { - $debugRepresentation['result'] = array( - 'type' => \get_class($result), - 'object' => $this->cloneVar($result), - ); - } elseif (\is_array($result)) { - $debugRepresentation['result'] = array( - 'type' => 'array', - 'object' => $this->cloneVar($result), - ); - } else { - $debugRepresentation['result'] = array( - 'type' => \gettype($result), - 'value' => $result, - ); - } + $debugRepresentation['result'] = array( + 'type' => \is_object($result) ? \get_class($result) : gettype($result), + 'value' => $result, + ); } if (isset($tracedMessage['exception'])) { @@ -110,15 +112,31 @@ private function collectMessage(string $busName, array $tracedMessage) $debugRepresentation['exception'] = array( 'type' => \get_class($exception), - 'message' => $exception->getMessage(), + 'value' => $exception, ); } return $debugRepresentation; } - public function getMessages(): array + public function getExceptionsCount(string $bus = null): int + { + return array_reduce($this->getMessages($bus), function (int $carry, Data $message) { + return $carry += isset($message['exception']) ? 1 : 0; + }, 0); + } + + public function getMessages(string $bus = null): array + { + $messages = $this->data['messages'] ?? array(); + + return $bus ? array_filter($messages, function (Data $message) use ($bus): bool { + return $bus === $message['bus']; + }) : $messages; + } + + public function getBuses(): array { - return $this->data['messages'] ?? array(); + return $this->data['buses']; } } diff --git a/src/Symfony/Component/Messenger/Tests/DataCollector/MessengerDataCollectorTest.php b/src/Symfony/Component/Messenger/Tests/DataCollector/MessengerDataCollectorTest.php index 91f54490ac9ec..d88593e3e747d 100644 --- a/src/Symfony/Component/Messenger/Tests/DataCollector/MessengerDataCollectorTest.php +++ b/src/Symfony/Component/Messenger/Tests/DataCollector/MessengerDataCollectorTest.php @@ -16,14 +16,22 @@ use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; use Symfony\Component\Messenger\TraceableMessageBus; -use Symfony\Component\VarDumper\Test\VarDumperTestTrait; +use Symfony\Component\VarDumper\Cloner\Data; +use Symfony\Component\VarDumper\Dumper\CliDumper; /** * @author Maxime Steinhausser */ class MessengerDataCollectorTest extends TestCase { - use VarDumperTestTrait; + /** @var CliDumper */ + private $dumper; + + protected function setUp() + { + $this->dumper = new CliDumper(); + $this->dumper->setColors(false); + } /** * @dataProvider getHandleTestData @@ -46,17 +54,18 @@ public function testHandle($returnedValue, $expected) $messages = $collector->getMessages(); $this->assertCount(1, $messages); - $this->assertDumpMatchesFormat($expected, $messages[0]); + $this->assertStringMatchesFormat($expected, $this->getDataAsString($messages[0])); } public function getHandleTestData() { $messageDump = << "default" + "envelopeItems" => null "message" => array:2 [ "type" => "Symfony\Component\Messenger\Tests\Fixtures\DummyMessage" - "object" => Symfony\Component\VarDumper\Cloner\Data {%A - %A+class: "Symfony\Component\Messenger\Tests\Fixtures\DummyMessage"%A + "value" => Symfony\Component\Messenger\Tests\Fixtures\DummyMessage %A + -message: "dummy message" } ] DUMP; @@ -64,7 +73,7 @@ public function getHandleTestData() yield 'no returned value' => array( null, << array:2 [ "type" => "NULL" @@ -77,7 +86,7 @@ public function getHandleTestData() yield 'scalar returned value' => array( 'returned value', << array:2 [ "type" => "string" @@ -90,11 +99,13 @@ public function getHandleTestData() yield 'array returned value' => array( array('returned value'), << array:2 [ "type" => "array" - "object" => Symfony\Component\VarDumper\Cloner\Data {%A + "value" => array:1 [ + 0 => "returned value" + ] ] ] DUMP @@ -123,21 +134,66 @@ public function testHandleWithException() $messages = $collector->getMessages(); $this->assertCount(1, $messages); - $this->assertDumpMatchesFormat(<<assertStringMatchesFormat(<< "default" + "envelopeItems" => null "message" => array:2 [ "type" => "Symfony\Component\Messenger\Tests\Fixtures\DummyMessage" - "object" => Symfony\Component\VarDumper\Cloner\Data {%A - %A+class: "Symfony\Component\Messenger\Tests\Fixtures\DummyMessage"%A + "value" => Symfony\Component\Messenger\Tests\Fixtures\DummyMessage %A + -message: "dummy message" } ] "exception" => array:2 [ "type" => "RuntimeException" - "message" => "foo" + "value" => RuntimeException %A ] -] +] DUMP - , $messages[0]); + , $this->getDataAsString($messages[0])); + } + + public function testKeepsOrderedDispatchCalls() + { + $firstBus = $this->getMockBuilder(MessageBusInterface::class)->getMock(); + $firstBus = new TraceableMessageBus($firstBus); + + $secondBus = $this->getMockBuilder(MessageBusInterface::class)->getMock(); + $secondBus = new TraceableMessageBus($secondBus); + + $collector = new MessengerDataCollector(); + $collector->registerBus('first bus', $firstBus); + $collector->registerBus('second bus', $secondBus); + + $firstBus->dispatch(new DummyMessage('#1')); + $secondBus->dispatch(new DummyMessage('#2')); + $secondBus->dispatch(new DummyMessage('#3')); + $firstBus->dispatch(new DummyMessage('#4')); + $secondBus->dispatch(new DummyMessage('#5')); + + $collector->lateCollect(); + + $messages = $collector->getMessages(); + $this->assertCount(5, $messages); + + $this->assertSame('#1', $messages[0]['message']['value']['message']); + $this->assertSame('first bus', $messages[0]['bus']); + + $this->assertSame('#2', $messages[1]['message']['value']['message']); + $this->assertSame('second bus', $messages[1]['bus']); + + $this->assertSame('#3', $messages[2]['message']['value']['message']); + $this->assertSame('second bus', $messages[2]['bus']); + + $this->assertSame('#4', $messages[3]['message']['value']['message']); + $this->assertSame('first bus', $messages[3]['bus']); + + $this->assertSame('#5', $messages[4]['message']['value']['message']); + $this->assertSame('second bus', $messages[4]['bus']); + } + + private function getDataAsString(Data $data): string + { + return rtrim($this->dumper->dump($data, true)); } } diff --git a/src/Symfony/Component/Messenger/Tests/Fixtures/AnEnvelopeItem.php b/src/Symfony/Component/Messenger/Tests/Fixtures/AnEnvelopeItem.php new file mode 100644 index 0000000000000..9e5bb0c92b63d --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/Fixtures/AnEnvelopeItem.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Tests\Fixtures; + +use Symfony\Component\Messenger\EnvelopeItemInterface; + +class AnEnvelopeItem implements EnvelopeItemInterface +{ + /** + * {@inheritdoc} + */ + public function serialize() + { + return ''; + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) + { + // noop + } +} diff --git a/src/Symfony/Component/Messenger/Tests/MessageBusTest.php b/src/Symfony/Component/Messenger/Tests/MessageBusTest.php index ba40eb3f9a12c..9dc93a9d15213 100644 --- a/src/Symfony/Component/Messenger/Tests/MessageBusTest.php +++ b/src/Symfony/Component/Messenger/Tests/MessageBusTest.php @@ -15,10 +15,10 @@ use Symfony\Component\Messenger\Asynchronous\Transport\ReceivedMessage; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\EnvelopeAwareInterface; -use Symfony\Component\Messenger\EnvelopeItemInterface; use Symfony\Component\Messenger\MessageBus; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\Middleware\MiddlewareInterface; +use Symfony\Component\Messenger\Tests\Fixtures\AnEnvelopeItem; use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; class MessageBusTest extends TestCase @@ -160,22 +160,3 @@ public function testThatAMiddlewareCanUpdateTheMessageWhileKeepingTheEnvelopeIte $bus->dispatch($envelope); } } - -class AnEnvelopeItem implements EnvelopeItemInterface -{ - /** - * {@inheritdoc} - */ - public function serialize() - { - return ''; - } - - /** - * {@inheritdoc} - */ - public function unserialize($serialized) - { - return new self(); - } -} diff --git a/src/Symfony/Component/Messenger/Tests/TraceableMessageBusTest.php b/src/Symfony/Component/Messenger/Tests/TraceableMessageBusTest.php index 4da1e5a630202..8a2946ee42778 100644 --- a/src/Symfony/Component/Messenger/Tests/TraceableMessageBusTest.php +++ b/src/Symfony/Component/Messenger/Tests/TraceableMessageBusTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\Tests\Fixtures\AnEnvelopeItem; use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; use Symfony\Component\Messenger\TraceableMessageBus; @@ -28,19 +29,29 @@ public function testItTracesResult() $traceableBus = new TraceableMessageBus($bus); $this->assertSame($result, $traceableBus->dispatch($message)); - $this->assertSame(array(array('message' => $message, 'result' => $result)), $traceableBus->getDispatchedMessages()); + $this->assertCount(1, $tracedMessages = $traceableBus->getDispatchedMessages()); + $this->assertArraySubset(array( + 'message' => $message, + 'result' => $result, + 'envelopeItems' => null, + ), $tracedMessages[0], true); } public function testItTracesResultWithEnvelope() { - $envelope = Envelope::wrap($message = new DummyMessage('Hello')); + $envelope = Envelope::wrap($message = new DummyMessage('Hello'))->with($envelopeItem = new AnEnvelopeItem()); $bus = $this->getMockBuilder(MessageBusInterface::class)->getMock(); $bus->expects($this->once())->method('dispatch')->with($envelope)->willReturn($result = array('foo' => 'bar')); $traceableBus = new TraceableMessageBus($bus); $this->assertSame($result, $traceableBus->dispatch($envelope)); - $this->assertSame(array(array('message' => $message, 'result' => $result)), $traceableBus->getDispatchedMessages()); + $this->assertCount(1, $tracedMessages = $traceableBus->getDispatchedMessages()); + $this->assertArraySubset(array( + 'message' => $message, + 'result' => $result, + 'envelopeItems' => array($envelopeItem), + ), $tracedMessages[0], true); } public function testItTracesExceptions() @@ -58,6 +69,11 @@ public function testItTracesExceptions() $this->assertSame($exception, $e); } - $this->assertSame(array(array('message' => $message, 'exception' => $exception)), $traceableBus->getDispatchedMessages()); + $this->assertCount(1, $tracedMessages = $traceableBus->getDispatchedMessages()); + $this->assertArraySubset(array( + 'message' => $message, + 'exception' => $exception, + 'envelopeItems' => null, + ), $tracedMessages[0], true); } } diff --git a/src/Symfony/Component/Messenger/TraceableMessageBus.php b/src/Symfony/Component/Messenger/TraceableMessageBus.php index 9620e0189daca..b60d220b15ce1 100644 --- a/src/Symfony/Component/Messenger/TraceableMessageBus.php +++ b/src/Symfony/Component/Messenger/TraceableMessageBus.php @@ -29,21 +29,27 @@ public function __construct(MessageBusInterface $decoratedBus) */ public function dispatch($message) { + $callTime = microtime(true); $messageToTrace = $message instanceof Envelope ? $message->getMessage() : $message; + $envelopeItems = $message instanceof Envelope ? array_values($message->all()) : null; try { $result = $this->decoratedBus->dispatch($message); $this->dispatchedMessages[] = array( + 'envelopeItems' => $envelopeItems, 'message' => $messageToTrace, 'result' => $result, + 'callTime' => $callTime, ); return $result; } catch (\Throwable $e) { $this->dispatchedMessages[] = array( + 'envelopeItems' => $envelopeItems, 'message' => $messageToTrace, 'exception' => $e, + 'callTime' => $callTime, ); throw $e;