PHP Serialization, Stack Traces, and Exceptions
Fabien Potencier
Feb 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');
}
}