10000 [Messenger] Be able to get raw data when a message in not decodable b… · symfony/symfony@ca0627b · GitHub
[go: up one dir, main page]

Skip to content

Commit ca0627b

Browse files
committed
[Messenger] Be able to get raw data when a message in not decodable by the PHP Serializer
1 parent 14a3730 commit ca0627b

File tree

6 files changed

+164
-42
lines changed

6 files changed

+164
-42
lines changed

src/Symfony/Component/Messenger/Command/AbstractFailedMessagesCommand.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Component\ErrorHandler\Exception\FlattenException;
1818
use Symfony\Component\Messenger\Envelope;
1919
use Symfony\Component\Messenger\Stamp\ErrorDetailsStamp;
20+
use Symfony\Component\Messenger\Stamp\MessageDecodingFailedStamp;
2021
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
2122
use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp;
2223
use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp;
@@ -72,6 +73,8 @@ protected function displaySingleMessage(Envelope $envelope, SymfonyStyle $io)
7273
$lastRedeliveryStamp = $envelope->last(RedeliveryStamp::class);
7374
/** @var ErrorDetailsStamp|null $lastErrorDetailsStamp */
7475
$lastErrorDetailsStamp = $envelope->last(ErrorDetailsStamp::class);
76+
/** @var MessageDecodingFailedStamp|null $lastMessageDecodingFailedStamp */
77+
$lastMessageDecodingFailedStamp = $envelope->last(MessageDecodingFailedStamp::class);
7578
$lastRedeliveryStampWithException = $this->getLastRedeliveryStampWithException($envelope, true);
7679

7780
$rows = [
@@ -127,6 +130,9 @@ protected function displaySingleMessage(Envelope $envelope, SymfonyStyle $io)
127130

128131
if ($io->isVeryVerbose()) {
129132
$io->title('Message:');
133+
if (null !== $lastMessageDecodingFailedStamp) {
134+
$io->error('The message could not be decoded. See below an APPROXIMATIVE representation of the class.');
135+
}
130136
$dump = new Dumper($io, null, $this->createCloner());
131137
$io->writeln($dump($envelope->getMessage()));
132138
$io->title('Exception:');
@@ -138,6 +144,9 @@ protected function displaySingleMessage(Envelope $envelope, SymfonyStyle $io)
138144
}
139145
$io->writeln(null === $flattenException ? '(no data)' : $dump($flattenException));
140146
} else {
147+
if (null !== $lastMessageDecodingFailedStamp) {
148+
$io->error('The message could not be decoded.');
149+
}
141150
$io->writeln(' Re-run command with <info>-vv</info> to see more message & error details.');
142151
}
143152
}

src/Symfony/Component/Messenger/Command/FailedMessagesRemoveCommand.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Symfony\Component\Console\Style\SymfonyStyle;
2121
use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface;
2222
use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface;
23+
use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer;
2324

2425
/**
2526
* @author Ryan Weaver <ryan@symfonycasts.com>
@@ -75,7 +76,14 @@ private function removeMessages(array $ids, ReceiverInterface $receiver, Symfony
7576
}
7677

7778
foreach ($ids as $id) {
78-
$envelope = $receiver->find($id);
79+
try {
80+
$prev = PhpSerializer::$createClassNotFound;
81+
PhpSerializer::$createClassNotFound = true;
82+
$envelope = $receiver->find($id);
83+
} finally {
84+
PhpSerializer::$createClassNotFound = $prev;
85+
}
86+
7987
if (null === $envelope) {
8088
$io->error(sprintf('The message with id "%s" was not found.', $id));
8189
continue;

src/Symfony/Component/Messenger/Command/FailedMessagesRetryCommand.php

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@
2222
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
2323
use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent;
2424
use Symfony\Component\Messenger\EventListener\StopWorkerOnMessageLimitListener;
25-
use Symfony\Component\Messenger\Exception\LogicException;
2625
use Symfony\Component\Messenger\MessageBusInterface;
26+
use Symfony\Component\Messenger\Stamp\MessageDecodingFailedStamp;
2727
use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface;
2828
use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface;
2929
use Symfony\Component\Messenger\Transport\Receiver\SingleMessageReceiver;
30+
use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer;
3031
use Symfony\Component\Messenger\Worker;
3132

3233
/**
@@ -129,23 +130,27 @@ private function runInteractive(SymfonyStyle $io, bool $shouldForce)
129130
// to be temporarily "acked", even if the user aborts
130131
// handling the message
131132
while (true) {
132-
$ids = [];
133-
foreach ($receiver->all(1) as $envelope) {
134-
++$count;
133+
$envelopes = [];
135134

136-
$id = $this->getMessageId($envelope);
137-
if (null === $id) {
138-
throw new LogicException(sprintf('The "%s" receiver is able to list messages by id but the envelope is missing the TransportMessageIdStamp stamp.', $this->getReceiverName()));
135+
try {
136+
$prev = PhpSerializer::$createClassNotFound;
137+
PhpSerializer::$createClassNotFound = true;
138+
139+
foreach ($receiver->all(1) as $envelope) {
140+
++$count;
141+
142+
$envelopes[] = $envelope;
139143
}
140-
$ids[] = $id;
144+
} finally {
145+
PhpSerializer::$createClassNotFound = $prev;
141146
}
142147

143148
// break the loop if all messages are consumed
144-
if (0 === \count($ids)) {
149+
if (0 === \count($envelopes)) {
145150
break;
146151
}
147152

148-
$this->retrySpecificIds($ids, $io, $shouldForce);
153+
$this->retrySpecificEnvelop($envelopes, $io, $shouldForce);
149154
}
150155
} else {
151156
// get() and ask messages one-by-one
@@ -167,6 +172,10 @@ private function runWorker(ReceiverInterface $receiver, SymfonyStyle $io, bool $
167172

168173
$this->displaySingleMessage($envelope, $io);
169174

175+
if ($envelope->last(MessageDecodingFailedStamp::class)) {
176+
throw new \RuntimeException(sprintf('This message with id "%s" could not decoded. It can only be shown or removed.', $this->getMessageId($envelope) ?? 'NULL'));
177+
}
178+
170179
$shouldHandle = $shouldForce || $io->confirm('Do you want to retry (yes) or delete this message (no)?');
171180

172181
if ($shouldHandle) {
@@ -186,8 +195,12 @@ private function runWorker(ReceiverInterface $receiver, SymfonyStyle $io, bool $
186195
);
187196

188197
try {
198+
$prev = PhpSerializer::$createClassNotFound;
199+
PhpSerializer::$createClassNotFound = true;
200+
189201
$worker->run();
190202
} finally {
203+
PhpSerializer::$createClassNotFound = $prev;
191204
$this->eventDispatcher->removeListener(WorkerMessageReceivedEvent::class, $listener);
192205
}
193206

@@ -203,7 +216,15 @@ private function retrySpecificIds(array $ids, SymfonyStyle $io, bool $shouldForc
203216
}
204217

205218
foreach ($ids as $id) {
206-
$envelope = $receiver->find($id);
219+
try {
220+
$prev = PhpSerializer::$createClassNotFound;
221+
PhpSerializer::$createClassNotFound = true;
222+
223+
$envelope = $receiver->find($id);
224+
} finally {
225+
PhpSerializer::$createClassNotFound = $prev;
226+
}
227+
207228
if (null === $envelope) {
208229
throw new RuntimeException(sprintf('The message "%s" was not found.', $id));
209230
}
@@ -212,4 +233,14 @@ private function retrySpecificIds(array $ids, SymfonyStyle $io, bool $shouldForc
212233
$this->runWorker($singleReceiver, $io, $shouldForce);
213234
}
214235
}
236+
237+
private function retrySpecificEnvelop(array $envelopes, SymfonyStyle $io, bool $shouldForce)
238+
{
239+
$receiver = $this->getReceiver();
240+
241+
foreach ($envelopes as $envelope) {
242+
$singleReceiver = new SingleMessageReceiver($receiver, $envelope);
243+
$this->runWorker($singleReceiver, $io, $shouldForce);
244+
}
245+
}
215246
}

src/Symfony/Component/Messenger/Command/FailedMessagesShowCommand.php

Lines changed: 48 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
use Symfony\Component\Console\Output\OutputInterface;
2020
use Symfony\Component\Console\Style\SymfonyStyle;
2121
use Symfony\Component\Messenger\Stamp\ErrorDetailsStamp;
22+
use Symfony\Component\Messenger\Stamp\MessageDecodingFailedStamp;
2223
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
2324
use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface;
25+
use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer;
2426

2527
/**
2628
* @author Ryan Weaver <ryan@symfonycasts.com>
@@ -83,27 +85,34 @@ private function listMessages(SymfonyStyle $io, int $max)
8385
$envelopes = $receiver->all($max);
8486

8587
$rows = [];
86-
foreach ($envelopes as $envelope) {
87-
/** @var RedeliveryStamp|null $lastRedeliveryStamp */
88-
$lastRedeliveryStamp = $envelope->last(RedeliveryStamp::class);
89-
/** @var ErrorDetailsStamp|null $lastErrorDetailsStamp */
90-
$lastErrorDetailsStamp = $envelope->last(ErrorDetailsStamp::class);
91-
$lastRedeliveryStampWithException = $this->getLastRedeliveryStampWithException($envelope, true);
92-
93-
$errorMessage = '';
94-
if (null !== $lastErrorDetailsStamp) {
95-
$errorMessage = $lastErrorDetailsStamp->getExceptionMessage();
96-
} elseif (null !== $lastRedeliveryStampWithException) {
97-
// Try reading the errorMessage for messages that are still in the queue without the new ErrorDetailStamps.
98-
$errorMessage = $lastRedeliveryStampWithException->getExceptionMessage();
88+
try {
89+
$prev = PhpSerializer::$createClassNotFound;
90+
PhpSerializer::$createClassNotFound = true;
91+
92+
foreach ($envelopes as $envelope) {
93+
/** @var RedeliveryStamp|null $lastRedeliveryStamp */
94+
$lastRedeliveryStamp = $envelope->last(RedeliveryStamp::class);
95+
/** @var ErrorDetailsStamp|null $lastErrorDetailsStamp */
96+
$lastErrorDetailsStamp = $envelope->last(ErrorDetailsStamp::class);
97+
$lastRedeliveryStampWithException = $this->getLastRedeliveryStampWithException($envelope, true);
98+
99+
$errorMessage = '';
100+
if (null !== $lastErrorDetailsStamp) {
101+
$errorMessage = $lastErrorDetailsStamp->getExceptionMessage();
102+
} elseif (null !== $lastRedeliveryStampWithException) {
103+
// Try reading the errorMessage for messages that are still in the queue without the new ErrorDetailStamps.
104+
$errorMessage = $lastRedeliveryStampWithException->getExceptionMessage();
105+
}
106+
107+
$rows[] = [
108+
$this->getMessageId($envelope),
109+
\get_class($envelope->getMessage()),
110+
null === $lastRedeliveryStamp ? '' : $lastRedeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s'),
111+
$errorMessage,
112+
];
99113
}
100-
101-
$rows[] = [
102-
$this->getMessageId($envelope),
103-
\get_class($envelope->getMessage()),
104-
null === $lastRedeliveryStamp ? '' : $lastRedeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s'),
105-
$errorMessage,
106-
];
114+
} finally {
115+
PhpSerializer::$createClassNotFound = $prev;
107116
}
108117

109118
if (0 === \count($rows)) {
@@ -125,17 +134,30 @@ private function showMessage(string $id, SymfonyStyle $io)
125134
{
126135
/** @var ListableReceiverInterface $receiver */
127136
$receiver = $this->getReceiver();
128-
$envelope = $receiver->find($id);
137+
138+
try {
139+
$prev = PhpSerializer::$createClassNotFound;
140+
PhpSerializer::$createClassNotFound = true;
141+
142+
$envelope = $receiver->find($id);
143+
} finally {
144+
PhpSerializer::$createClassNotFound = $prev;
145+
}
146+
129147
if (null === $envelope) {
130148
throw new RuntimeException(sprintf('The message "%s" was not found.', $id));
131149
}
132150

133151
$this->displaySingleMessage($envelope, $io);
134152

135-
$io->writeln([
136-
'',
137-
sprintf(' Run <comment>messenger:failed:retry %s</comment> to retry this message.', $id),
138-
sprintf(' Run <comment>messenger:failed:remove %s</comment> to delete it.', $id),
139-
]);
153+
$io->newLine();
154+
155+
if ($envelope->last(MessageDecodingFailedStamp::class)) {
156+
$io->writeln(' This message could not be retried.');
157+
} else {
158+
$io->writeln(sprintf(' Run <comment>messenger:failed:retry %s</comment> to retry this message.', $id));
159+
}
160+
161+
$io->writeln(sprintf(' Run <comment>messenger:failed:remove %s</comment> to delete it.', $id));
140162
}
141163
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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\Component\Messenger\Stamp;
13+
14+
/**
15+
* @author Grégoire Pineau <lyrixx@lyrixx.info>
16+
*/
17+
class MessageDecodingFailedStamp implements StampInterface
18+
{
19+
}

src/Symfony/Component/Messenger/Transport/Serialization/PhpSerializer.php

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,20 @@
1313

1414
use Symfony\Component\Messenger\Envelope;
1515
use Symfony\Component\Messenger\Exception\MessageDecodingFailedException;
16+
use Symfony\Component\Messenger\Stamp\MessageDecodingFailedStamp;
1617
use Symfony\Component\Messenger\Stamp\NonSendableStampInterface;
1718

1819
/**
1920
* @author Ryan Weaver<ryan@symfonycasts.com>
2021
*/
2122
class PhpSerializer implements SerializerInterface
2223
{
24+
/**
25+
* @internal
26+
*/
27+
public static bool $createClassNotFound = false;
28+
private static bool $classNotFoundDetected = false;
29+
2330
/**
2431
* {@inheritdoc}
2532
*/
@@ -48,8 +55,9 @@ public function encode(Envelope $envelope): array
4855
];
4956
}
5057

51-
private function safelyUnserialize(string $contents)
58+
private function safelyUnserialize(string $contents): Envelope
5259
{
60+
self::$classNotFoundDetected = false;
5361
$signalingException = new MessageDecodingFailedException(sprintf('Could not decode message using PHP serialization: %s.', $contents));
5462
$prevUnserializeHandler = ini_set('unserialize_callback_func', self::class.'::handleUnserializeCallback');
5563
$prevErrorHandler = set_error_handler(function ($type, $msg, $file, $line, $context = []) use (&$prevErrorHandler, $signalingException) {
@@ -61,20 +69,45 @@ private function safelyUnserialize(string $contents)
6169
});
6270

6371
try {
64-
$meta = unserialize($contents);
72+
/** @var Envelope */
73+
$envelope = unserialize($contents);
6574
} finally {
6675
restore_error_handler();
6776
ini_set('unserialize_callback_func', $prevUnserializeHandler);
6877
}
6978

70-
return $meta;
79+
if (self::$classNotFoundDetected) {
80+
$envelope = $envelope->with(new MessageDecodingFailedStamp());
81+
}
82+
83+
return $envelope;
7184
}
7285

7386
/**
7487
* @internal
7588
*/
7689
public static function handleUnserializeCallback($class)
7790
{
78-
throw new MessageDecodingFailedException(sprintf('Message class "%s" not found during decoding.', $class));
91+
if (!self::$createClassNotFound) {
92+
throw new MessageDecodingFailedException(sprintf('Message class "%s" not found during decoding.', $class));
93+
}
94+
95+
self::$classNotFoundDetected = true;
96+
97+
$parts = explode('\\', $class);
98+
$class = array_pop($parts);
99+
$namespace = implode('\\', $parts);
100+
$code = <<<EOPHP
101+
class $class
102+
{
103+
private \$__WARNING__ = '⚠⚠ WARNING This class could not be unserialized. A mock has been created on the fly. ⚠⚠';
104+
}
105+
EOPHP;
106+
107+
if ($namespace) {
108+
eval("namespace $namespace { $code };");
109+
} else {
110+
eval($code);
111+
}
79112
}
80113
}

0 commit comments

Comments
 (0)
0