I have code that is serializing a PDOException
, sending it over the wire, and then unserializing it later on. When I unserialize it, the $code
property appears to be missing. The rest of the object appears unchanged.
My code is running against a PostgreSQL database. Use the following DDL:
CREATE TABLE test (
id INTEGER
);
Use the following code to reproduce my issue (substituting your own PostgeSQL connection values):
<?php
$dsn = "pgsql: dbname=postgres;host=/var/run/postgresql;port=5432";
$user = "postgres";
$password = "";
try
{
$pdo = new PDO($dsn, $user, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$res = $pdo->exec("INSERT INTO test (id) VALUES (999999999999999)");
}
catch (PDOException $e)
{
var_dump((array) $e);
print "\n";
print $e->getCode();
print "\n";
$s = serialize($e);
print $s;
print "\n";
$d = unserialize($s);
var_dump((array) $d);
print "\n";
print $d->getCode();
print "\n";
print serialize($e->getCode());
print "\n";
}
?>
In my output, the $code
property is missing from the final output. Additionally I get the following notice:
PHP Notice: Undefined property: PDOException::$code in /home/developer/test_serialize.php on line 20
I'm finding that I actually have to execute a failing SQL statement in order to see this issue. In particular if I pick, for example, the wrong port number, then I'll get a PDOException
but it will retain the $code
property after unserialize
call.
Note that the serialized string appears to have the code property there, so I'm assuming this is an issue with the unserialize
function.
Any insight would be appreciated - am I misunderstanding something fundamental here? Is this a PHP bug? Something else? I'm on the following PHP version:
PHP 7.1.6 (cli) (built: Jun 18 2018 12:25:10) ( ZTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologies
Edit - Adding Print Output
The following is the output of the reproduction script. Note that I've modified is slightly to add some newlines for readability, and replacing print_r
with var_dump
:
array(8) {
["*message"]=>
string(75) "SQLSTATE[22003]: Numeric value out of range: 7 ERROR: integer out of range"
["Exceptionstring"]=>
string(0) ""
["*code"]=>
string(5) "22003"
["*file"]=>
string(34) "/home/developer/test_serialize.php"
["*line"]=>
int(10)
["Exceptiontrace"]=>
array(1) {
[0]=>
array(6) {
["file"]=>
string(34) "/home/developer/test_serialize.php"
["line"]=>
int(10)
["function"]=>
string(4) "exec"
["class"]=>
string(3) "PDO"
["type"]=>
string(2) "->"
["args"]=>
array(1) {
[0]=>
string(73) "INSERT INTO km_role (role_id, role_name) VALUES (999999999999999, 'test')"
}
}
}
["Exceptionprevious"]=>
NULL
["errorInfo"]=>
array(3) {
[0]=>
string(5) "22003"
[1]=>
int(7)
[2]=>
string(28) "ERROR: integer out of range"
}
}
22003
O:12:"PDOException":8:{s:10:"*message";s:75:"SQLSTATE[22003]: Numeric value out of range: 7 ERROR: integer out of range";s:17:"Exceptionstring";s:0:"";s:7:"*code";s:5:"22003";s:7:"*file";s:34:"/home/developer/test_serialize.php";s:7:"*line";i:10;s:16:"Exceptiontrace";a:1:{i:0;a:6:{s:4:"file";s:34:"/home/developer/test_serialize.php";s:4:"line";i:10;s:8:"function";s:4:"exec";s:5:"class";s:3:"PDO";s:4:"type";s:2:"->";s:4:"args";a:1:{i:0;s:73:"INSERT INTO km_role (role_id, role_name) VALUES (999999999999999, 'test')";}}}s:19:"Exceptionprevious";N;s:9:"errorInfo";a:3:{i:0;s:5:"22003";i:1;i:7;i:2;s:28:"ERROR: integer out of range";}}
array(7) {
["*message"]=>
string(75) "SQLSTATE[22003]: Numeric value out of range: 7 ERROR: integer out of range"
["Exceptionstring"]=>
string(0) ""
["*file"]=>
string(34) "/home/developer/test_serialize.php"
["*line"]=>
int(10)
["Exceptiontrace"]=>
array(1) {
[0]=>
array(6) {
["file"]=>
string(34) "/home/developer/test_serialize.php"
["line"]=>
int(10)
["function"]=>
string(4) "exec"
["class"]=>
string(3) "PDO"
["type"]=>
string(2) "->"
["args"]=>
array(1) {
[0]=>
string(73) "INSERT INTO km_role (role_id, role_name) VALUES (999999999999999, 'test')"
}
}
}
["Exceptionprevious"]=>
NULL
["errorInfo"]=>
array(3) {
[0]=>
string(5) "22003"
[1]=>
int(7)
[2]=>
string(28) "ERROR: integer out of range"
}
}
PHP Notice: Undefined property: PDOException::$code in /home/developer/test_serialize.php on line 24
s:5:"22003"
In the example where the PDOException is thrown via incorrect port number, the serialized $e->getCode()
is:
i:7;
Blackbam's answer is exceptional, but the PDO object being unserializable is a red herring. The problem with your code really is due to the type of the $code
property, as discussed in the comments to his post. The exception is initialized with a string representation of the error code instead of an integer in some circumstances. This breaks deserialization, which very reasonably decides to discard properties with invalid types.
The comments on the PDOException documentation page are almost all talking about problems caused by the error code being created as a string instead of an int.
You can set the protected value to an integer using reflection. See below:
try
{
$pdo = new PDO($dsn, $user, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$res = $pdo->exec("INSERT INTO test_schema.test (id) VALUES (999999999999999)");
}
catch (PDOException $e)
{
// the new bit is here
if (!is_int($e->getCode())) {
$reflectionClass = new ReflectionClass($e);
$reflectionProperty = $reflectionClass->getProperty('code');
$reflectionProperty->setAccessible(true);
$reflectionProperty->setValue($e, (int)$reflectionProperty->getValue($e));
}
// the rest is the same
var_dump((array) $e);
print "\n";
print $e->getCode();
print "\n";
$s = serialize($e);
print $s;
print "\n";
$d = unserialize($s);
var_dump((array) $d);
print "\n";
print $d->getCode();
print "\n";
print serialize($e->getCode());
print "\n";
}
Of course, you will lose information if the code contains an alphanumeric value rather than an integer cast as a string. The message may duplicate the error number, but that may not be great to rely on.
The unserializable stack trace Blackbam is concerned with can become a problem if you tweak your code just a little bit:
function will_crash($pdo) {
$res = $pdo->exec("INSERT INTO test_schema.test (id) VALUES (999999999999999)");
}
try
{
$pdo = new PDO($dsn, $user, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
will_crash($pdo);
}
catch (PDOException $e)
{
$s = serialize($e);
// PHP Fatal error: Uncaught PDOException: You cannot serialize or unserialize PDO instances in...
}
Whoops.
So Blackbam's answer, and the approach in his link of creating a serializable exception class, is probably the way to go. That allows you to serialize an exception's data but not the stack trace.
Then again, at that point you may as well just use json_encode
and json_decode
to pass around/store exception information.
First of all have a look at the PDOException class:
PDOException extends RuntimeException {
/* Properties */
public array $errorInfo ;
protected string $code ;
/* Inherited properties */
protected string $message ;
protected int $code ;
protected string $file ;
protected int $line ;
/* Inherited methods */
final public string Exception::getMessage ( void )
final public Throwable Exception::getPrevious ( void )
final public mixed Exception::getCode ( void )
final public string Exception::getFile ( void )
final public int Exception::getLine ( void )
final public array Exception::getTrace ( void )
final public string Exception::getTraceAsString ( void )
public string Exception::__toString ( void )
final private void Exception::__clone ( void )
}
The getCode() method in PHP is implemented just like this (ref: https://github.com/php/php-src/blob/cd953269d3d486f775f1935731b1d6d44f12a350/ext/spl/spl.php):
/** @return the code passed to the constructor
*/
final public function getCode()
{
return $this->code;
}
This is the constructor of a PHP Exception:
/** Construct an exception
*
* @param $message Some text describing the exception
* @param $code Some code describing the exception
*/
function __construct($message = NULL, $code = 0) {
if (func_num_args()) {
$this->message = $message;
}
$this->code = $code;
$this->file = __FILE__; // of throw clause
$this->line = __LINE__; // of throw clause
$this->trace = debug_backtrace();
$this->string = StringFormat($this);
}
What does that tell us? The $code
property of an Exception can only be populated when the Exception is generated and it should be zero if no $code
was passed.
So is this a PHP Bug? I guess not, after some research I have found the following great article: http://fabien.potencier.org/php-serialization-stack-traces-and-exceptions.html
In essence it says:
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.
Whenever a non-serializable object is present in the stack trace, the exception won't be serializable.
And I guess this is the reason why our serialize()/unserialize() process is failing - because the Exception is not serializable.
Solution:
Write a Serializable Exception which extends Exception
class SerializableException extends Exception implements Serializable {
// ... go ahead :-)
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With