I have an event listener setup to listen to the event _illuminate.log_. Everything works for normal logging using the Log interface as expected.
When an exception is thrown the page crashes rather than the normal stack trace and in nginx logs you get a "PHP message: PHP Fatal error: Trying to clone an uncloneable object of class".
After some stepping through the code I worked out that Exceptions are not clonable making the listen event illuminate.log useless.
Change App\Exceptions\Handler::report()
public function report(Exception $e)
{
// Because the event dispatcher tries to clone any object that is a
// parameter before passing it to the listener and Exceptions are not
// cloneable we have to handle the Exception logging separates from the
// event firing and only pass the message to the event dispatcher but
// the full Exception to Logger.
if ($this->shouldReport($e)) {
// Fire Event
if ($dispatcher = $this->log->getEventDispatcher()) {
$level = 'error';
$message = $e->getMessage();
$context = [];
$dispatcher->fire('illuminate.log', compact('level', 'message', 'context'));
}
// Log Exception
if ($log = $this->log->getMonolog()) {
$log->{$level}($e, []);
}
}
}
Change Illuminate\Events\Dispatcher::cloneArgumentsForQueueing() to use the reflectionclass::iscloneable() method, keeping in mind the Exceptions also cant seem to be serializabe either and createQueuedHandlerCallable() serializes the event arguments.
protected function cloneArgumentsForQueueing(array $arguments)
{
return array_map(function ($a) {
if (is_object($a)) {
$reflection = new ReflectionClass($a);
if ($reflection->isCloneable()) {
return clone $a;
}
else {
return (string)$a;
}
}
return $a;
}, $arguments);
}
By converting any argument that is not clonable there could be other unforeseen consequence.
Any listener listening to _illuminate.log_ will get a full stack trace in string format rather than just the message.
This is a bug in your code.
The framework makes no assumptions you want to serialize something.
@GrahamCampbell - I think you were a little quick to dismiss this issue and I would like to reopen this issue.
I am not the one serializing anything, the Laravel Event Dispatcher is using both the clone and serialize functions when a Listener implements the ShouldQueue interface causing the Listener and the all its arguments to be cloned and serialized and if one of those arguments is an object that is not clonable or serializable like an Exception or a Closure then everything crashes causing a fatal error.
Route::get('/', function () {
throw new Exception('Event testing...');
return view('welcome');
});
<?php
namespace App\Listeners;
use Illuminate\Contracts\Queue\ShouldQueue;
class LogTesting implements ShouldQueue
{
public function handle()
{
// Do nothing
}
}
protected $listen = [
'illuminate.log' => [
'App\Listeners\LogTesting',
],
];
The problem only takes place when two conditions are meet:
protected function createClassCallable($listener, $container)
{
list($class, $method) = $this->parseClassCallable($listener);
if ($this->handlerShouldBeQueued($class)) {
return $this->createQueuedHandlerCallable($class, $method);
} else {
return [$container->make($class), $method];
}
}
protected function createQueuedHandlerCallable($class, $method)
{
return function () use ($class, $method) {
$arguments = $this->cloneArgumentsForQueueing(func_get_args());
if (method_exists($class, 'queue')) {
$this->callQueueMethodOnHandler($class, $method, $arguments);
} else {
$this->resolveQueue()->push('Illuminate\Events\CallQueuedHandler@call', [
'class' => $class, 'method' => $method, 'data' => serialize($arguments),
]);
}
};
}
protected function cloneArgumentsForQueueing(array $arguments)
{
return array_map(function ($a) {
return is_object($a) ? clone $a : $a;
}, $arguments);
}
My original workaround still applies.
I changed my mind on my suggested fix and I think the event dispatcher should be left to error, and I think the log writer should be updated to not pass an Exception object but rather just the error message.
protected function fireLogEvent($level, $message, array $context = [])
{
// If the event dispatcher is set, we will pass along the parameters to the
// log listeners. These are useful for building profilers or other tools
// that aggregate all of the log messages for a given "request" cycle.
if (isset($this->dispatcher)) {
- $this->dispatcher->fire('illuminate.log', compact('level', 'message', 'context'));
+ $eventMessage = is_object($message) && $message instanceof \Exception ? $message->getMessage() : $message;
+ $this->dispatcher->fire('illuminate.log', compact('level', 'eventMessage', 'context'));
}
}
This is a not a framework bug. Your code is just broken. You cannot have queued handlers for such things.
@GrahamCampbell
How is my code broken, are you even reading my issue description or supplied code. I am using a documented feature in the form of Queued Event Listeners to listen to a existing framework event _illuminate.log_, doing something that is even suggested in the documentation in comments. The only problem is that the Laravel writer that fires the event is not checking the arguments it is passing through.
Quote from the Laravel Writer:
If the event dispatcher is set, we will pass along the parameters to the
log listeners. These are useful for building profilers or other tools
that aggregate all of the log messages for a given "request" cycle.
This is a real issue and I would like you to please re-open the issue and stop dismissing it so quickly.
@GrahamCampbell
I just want to know where I stand with this issue, are you ignoring my argument or evidence as to the bug or willing to discuss further. I have spent a lot of time researching this issue and providing a clear documented way of reproducing the error as well as a proposed solution.
I am happy to discuss the merits or technical correctness if the log writer should be allowed to send both text messages and Exception objects to the Event Dispatcher.