PHP Serialization, Stack Traces, and Exceptions

Fabien Potencier

February 11, 2009

Yesterday, I fixed a bug that looks very weird at first. In this post, I will describe the problem, the solution I found, and explain some PHP behaviors in the process.

The Bug Report

First the bug report: when trying to serialize a symfony form instance, a PDO exception was thrown:

"You cannot serialize or unserialize PDO instances"
 

This exception is thrown by PDO because PDO instances are not serializable for good reasons.

But it is weird because the sfForm class does not depend on PDO. How is it possible?

The Problem

After some investigation, we discovered that the bug was only for people with sessions stored in the database using PDO. So, we looked if the form instance was somewhat tied to the session, with no luck. The sfForm class has no dependency except for the widget classes, the validators classes, and the validation error classes. So, we tried to reproduce the bug with just a widget, just a validator, and just a validator error class.

Surprisingly enough, the problem came from the validation error classes. In symfony, the validator error classes extends the PHP Exception class.

Serializing an Exception instance is enough to reproduce the bug if there is a PDO instance "flying around" as demonstrated by this code:

$dbh = new PDO('sqlite:memory:');
 
function will_crash($dbh)
{
  // serialize an exception
  echo serialize(new Exception());
}
 
// this will throw a PDOException
will_crash($dbh);
 

What happens? When PHP serializes an exception, it serializes the exception code, the exception message, but also the stack trace.

The stack trace is an array containing all functions and methods that have already been executed at this point of the script. The trace contains the file name, the line in the file, the function name, and an array of all arguments passed to the function. Do you spot the problem?

The stack trace contains a reference to the PDO instance, as it was passed to the will_crash() function, and as PDO instances are not serializable, an exception is thrown when PHP serializes the stack trace.

So, whenever a non-serializable object is present in the stack trace, the exception won't be serializable.

The Solution

In PHP, you can override the serialization process by implementing the Serializable interface. The solution was then simple enough:

class sfValidatorError extends Exception implements Serializable
{
  // class code
 
  public function serialize()
  {
    return serialize(array($this->validator, $this->arguments, $this->code, $this->message));
  }
 
  public function unserialize($serialized)
  {
    list($this->validator, $this->arguments, $this->code, $this->message) = unserialize($serialized);
  }
}
 

The serialize() method should return a string that represents the object. In our case, we just serialize all properties, except the stack trace.

The unserialize() method takes the serialized string as an argument and should initialize the object as it replaces the call to the __construct() method.

The Tests

So far so good. To ensure that the problem was fixed, I needed a way to write some tests. But I didn't want to rely on PDO for tests. Simulating the behavior of PDO is simple enough. We can just create a class that cannot be serialized:

class NotSerializable implements Serializable
{
  public function serialize()
  {
    throw new LogicException('You cannot serialize or unserialize NotSerializable instances');
  }
 
  public function unserialize($serialized)
  {
    throw new LogicException('You cannot serialize or unserialize NotSerializable instances');
  }
}
 

Discussion

gravatar Bernhard  — February 11, 2009 10:55   #1
Hi Fabien,

I experienced this behaviour already multiple times, also outside of the Form context. What I did so far was writing a custom method that scanned the arguments of all traces and, in case they were objects, replaced them with their class name.

I think that it would be very helpful if sfException implemented Serializable with the above solution. For instance for error logging it is very helpful if you can serialize exceptions with their traces, but also in other contexts it can prevent the problem you described above.


Bernhard
gravatar Fabien  — February 11, 2009 11:31   #2
@Bernhard: Sounds like a good idea. Can you create a ticket?
gravatar Eric Lemoine  — February 11, 2009 22:25   #3
Hi Fabien

Why don't you use __sleep method instead of implementing Serializable interface ?

public function __sleep()
{
return array('validator', 'arguments', 'code', 'message');
}
gravatar Fabien  — February 12, 2009 07:41   #4
@Eric: Because the Serializable interface is much more flexible. In this specific case, we don't use anything fancy, but for consistency, I always use the interface.
gravatar Anti Veeranna  — February 13, 2009 18:22   #5
That was interesting.

I'm just wondering why the stack trace is being serialized in the first place? What would that be useful for? Maybe it is just a bug in PHP?
gravatar Olmo  — April 01, 2009 20:11   #6
@Anti

For backtraces. Very useful to know the execution trace that lead to the exception being raised.