PHP反序列化移除对象属性

5
我有一段代码,它将PDOException序列化后发送到网络中,在稍后的时间内再进行反序列化。当我进行反序列化时,$code属性似乎已经丢失了,但对象的其余部分没有变化。
我的代码是针对一个PostgreSQL数据库运行的。使用以下DDL:
CREATE TABLE test (
    id INTEGER
);

请使用以下代码来重现我的问题(替换为您自己的PostgeSQL连接值):
<?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";
}

?>

在我的输出中,最终输出中缺少$code属性。此外,我收到以下通知:PHP Notice: Undefined property: PDOException::$code in /home/developer/test_serialize.php on line 20。我发现实际上必须执行失败的SQL语句才能看到这个问题。特别是如果我选择了错误的端口号,那么我将得到一个PDOException,但在unserialize调用后它仍然保留$code属性。请注意,序列化字符串似乎有代码属性存在,因此我认为这是unserialize函数的问题。任何见解都会被赞赏 - 我是否对某些基本概念有误解?这是PHP的错误吗?还是其他原因?我使用以下PHP版本:
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

编辑 - 添加打印输出

以下是复制脚本的输出。请注意,我稍微修改了一些内容以添加一些换行符以增强可读性,并将print_r替换为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"

在端口号不正确时,抛出PDOException的示例中,序列化的$e->getCode()为:
i:7;

如果您正在使用命名空间,请尝试在 PDOException 前添加反斜杠,即 \PDOException - Lovepreet Singh
@LovepreetSingh 我已经尝试过带和不带 - 没有区别。 - Rob Gwynn-Jones
感谢您添加赏金,@jh1711 :) - Rob Gwynn-Jones
1
print_r语句的确切输出是什么(您能将其改为var_dump并编辑问题吗)? - Blackbam
@RobGwynn-Jones,欢迎加入;我对这个问题也很感兴趣。您能否在您的复现程序和错误端口号示例中添加serialize($e->getCode());的输出结果。 - jh1711
显示剩余2条评论
2个回答

5

Blackbam的回答非常出色,但是PDO对象无法序列化只是一个误导。正如他的帖子评论中所讨论的那样,你代码中的问题确实是由$code属性类型引起的。异常被初始化为错误代码的字符串表示形式而不是整数在某些情况下。这会破坏反序列化,因此反序列化非常合理地决定丢弃具有无效类型的属性。

PDOException文档页面上的评论几乎都在谈论由于创建错误代码为字符串而不是整数而引起的问题。

您可以使用反射将受保护的值设置为整数。请参见以下内容:

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";
}

当代码包含一个字母数字值而不是转换为字符串的整数时,你将会丢失信息。错误信息可能会重复错误编号,但这可能不是可靠的。

如果微调代码,则无法序列化的堆栈跟踪可能会成为问题:

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...
}

糟糕。

所以Blackbam的回答以及他链接里创建可序列化异常类的方法可能是正确的方式。这允许你序列化异常数据但不序列化堆栈跟踪。

然而,在那时,您可能会考虑使用json_encodejson_decode来传递/存储异常信息。


非常有趣 - 感谢您提供的出色答案。我不知道 ReflectionClass 可以以那种方式使用,这绝对是我缺失的环节。您知道为什么在函数中进行 PDO 调用时会抛出致命错误,但内联调用时却没有?我将接受此答案,因为它提供了证明/解决缺失 $code 属性的问题。关于尝试序列化 PDOException 的明显 XY 问题,我同意正确的答案是不要尝试。感谢您抽出时间来研究这个问题。 - Rob Gwynn-Jones
啊,我刚刚重新阅读了@Blackbam的答案,我想我明白为什么你的修改会抛出错误了——因为将$pdo对象传递给函数意味着堆栈跟踪深度增加了一级,现在包括了一个无法序列化的PDO对象。疼痛。 - Rob Gwynn-Jones
@RobGwynn-Jones 是的,就是这样。我希望我能假装在此之前已经了解了所有这些 - Blackbam的答案确实非常出色,我在拼凑其余部分时也学到了一点。 - jstur
很棒的答案;我不知道反射的技巧。这里有一个小的复现示例,展示了异常不喜欢字符串以及它是何时引入的(你可能需要点击“eol versions”)。 - jh1711

5

首先看一下PDOException类:

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 )
}

PHP中的getCode()方法实现方式如下(参考: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;
}

这是一个 PHP 异常的构造函数:
/** 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);
}

那告诉我们什么呢?异常的$code属性只有在生成异常时才能填充,如果没有传递$code,则应该为零。那这是PHP的Bug吗?我想不是,经过一些研究,我找到了以下精彩文章:http://fabien.potencier.org/php-serialization-stack-traces-and-exceptions.html。本质上它说:
当PHP序列化异常时,它会序列化异常代码、异常消息,但也会序列化堆栈跟踪。
堆栈跟踪是一个数组,其中包含此脚本点已执行的所有函数和方法。跟踪包含文件名、文件中的行、函数名称以及传递给函数的所有参数的数组。你发现问题了吗?
堆栈跟踪包含对PDO实例的引用,因为它被传递给will_crash()函数,并且由于PDO实例不可序列化,因此在PHP序列化堆栈跟踪时会抛出异常。
每当堆栈跟踪中存在不可序列化对象时,异常将无法序列化。
我想这就是我们的serialize()/unserialize()过程失败的原因——因为异常是不可序列化的。
解决方案:
编写一个扩展了Exception的可序列化异常。
class SerializableException extends Exception implements Serializable {
// ... go ahead :-)
}

1
关于(反)序列化异常时可能遇到的问题,你写得很好。但我认为非可序列化对象并不是这个问题的原因。serializeunserialize不会崩溃;只是缺少一个属性。我也不明白如何强制PDO抛出不同的异常。所以我不确定扩展类如何有帮助。但也许我错过了什么。 - jh1711
$code在序列化异常之前被定义,但之后没有被定义,因此序列化肯定存在问题。有没有可能它会悄悄地失败,而PHP仅仅继续执行,尽管序列化并没有按预期工作? - Blackbam
概括:是的,PHP会默默地丢弃错误类型的属性。 - jh1711
感谢您提供详细的答案。当然,让我们等待原帖作者的意见,看看他对此有何想法。赏金只有一天的时间,让我们看看是否还有其他人可以添加一些价值,尽管我认为我们可能已经得到了答案。 - Blackbam
感谢您的回答。我已按要求更新了原始帖子,并附上了打印输出以及序列化错误代码。请注意,当我给PDO提供无效端口时,代码属性是一个整数,而当跟踪实际包含PDO对象时,即尝试运行SQL语句时,它是一个字符串。那么什么是主流理论?它正在尝试序列化PDO实例的事实?$code的数据类型更改已记录,因此我认为这不太可能是类型不匹配,但我可能错了。有什么好的方法来测试这个理论呢? - Rob Gwynn-Jones
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接