|
Nov
11
|
|
|
Categories: PHP
| Tags:
| Views: 403
|
行文原因
为什么要写这篇文章? 因为我对于度娘和谷爹里搜索到关于这个问题的文章耿耿于怀, 忿忿不已. 这些文章都只说了个所以, 却没说然.
在这篇文章里, 我将详细地说一下Incomplete Object的成因, 以及这其中session, serialize及unserialize引起的问题和所起的作用
网上给出的解决方案
1. 网上有个很典型的代码, 说是session_start()位置不正确:
MyClass.php:
<?php class MyClass{ private $_counter = 0; public function increment() { return $this->_counter++; } } ?>
<?php session_start(); if (! isset($_SESSION['myObject'])) { require_once __DIR__ . '/MyClass.php'; $_SESSION['myObject'] = new MyClass(); } echo get_class($_SESSION['myObject']), "\n"; echo $_SESSION['myObject']->increment(), "\n"; ?>
而给出的解决方案是:
<?php require_once __DIR__ . '/MyClass.php'; session_start(); if (! isset($_SESSION['myObject'])) { $_SESSION['myObject'] = new MyClass(); } echo get_class($_SESSION['myObject']), "\n"; echo $_SESSION['myObject']->increment(), "\n"; ?>
仔细看一下代码, 不难发现, 其实根本就不是session_start()位置不正确, 看看那个if逻辑, 在$_SESSION['myObject']
值存在时, 人为不去引用MyClass.php文件.
我们把if条件去掉, 不改变session_start()的位置, 程序正常运行!!!
2. 用serialize序列化类实例, 然后再用unserialize解序列化.
<?php session_start(); if (! isset($_SESSION['myObject'])) { require_once __DIR__ . '/MyClass.php'; $_SESSION['myObject'] = serialize(new MyClass()); } $myObject = unserialize($_SESSION['myObject']); echo get_class($myObject), "\n"; echo $myObject--->increment(), "\n"; ?>
很显然, 问题依旧.
问题的真正成因
很显然, 所谓的session_start()位置, 是否用serialize序列化, 都不是这个问题真正的成因.
从网上给出的第一个解决方案, 如果我们把if条件去掉, 程序正常运行.
实际上, 这个if语句阻止了程序再次运行时去引用类定义文件MyClass.php.
这和用不用serialize和unserialize一毛钱的关系都没有.
说到这里, 为了解决为什么不引用类定义文件, 而出现Incomplete Object的情况, 我们要先理解一下
我们把类实例(Object)传给session,那在服务器上是如何存储的呢?
实际上, 在服务器上, php会先把这个object序列化serialize, 再存储到文件里, 当调用的时候, 就会
自动反序列化unserialize,并且重新实例化这个类.
那我们现在就可以解释问题的成因了. 当把object写进一个session时$_SESSION['myObject'] = new MyClass(),
把它实例化, 而在取出的时候就会自动重新实例化这个类, 而在实例化这个类的时候, 发现这个类并没有定义, 于是就会出现
Incomplete Object错误.
简单的说, 反序列化一个未定义的类的实例, 就会将object转化为Incomplete Object, 操作时就会报错.
我们可以来做个实验.
先定义一个类, 然后获取它的实例, 再获取这个实例的序列化字符串:
$ php -r 'class A{var $x=0; function p(){echo ++$x;}} $a=new A(); echo serialize($a);' O:1:"A":1:{s:1:"x";i:0;}
我们来反序列化这个字符串, 并调用p方法:
$ php -r '$a=unserialize("O:1:\"A\":1:{s:1:\"x\";i:0;}");$a->p();'
最后我们看到报出Incomplete Object的错误:
Fatal error: Unknown: The script tried to execute a method or access a property of an incomplete object. Please ensure that the class definition "A" of the object you are trying to operate on was loaded _before_ unserialize() gets called or provide a __autoload() function to load the class definition in Command line code on line 1
$ php -r 'class A{var $x=0; function p(){echo ++$x;}} $a=unserialize("O:1:\"A\":1:{s:1:\"x\";i:0;}");$a->p();' 1
那么这个程序就可以正常运行.
$ php -r '$a=unserialize("O:1:\"A\":1:{s:1:\"x\";i:0;}");print_r($a);' __PHP_Incomplete_Class Object ( [__PHP_Incomplete_Class_Name] => A [x] => 0 )
解决办法
其实真正的解决办法, 上面已经给出来了, 就是在反序列化之前引入声明这个类, 或者引入这个类的实例.
为了避免session在取值自动反序列化将object转成Incomplete Object, 这个时候, 我们就需要显示地去serialize一个object, 然后我们取值时
取到的只是序列化的字符串, 这时php是不会去检测类是否定义的. 那么我们就可以在这个时候解决问题了.
下面给出一个解决方案:
假设命名规则是这样的A.php文件定义了类A, 而我们在session中,以小写a为key来保存:
<?php session_start(); $key = 'a'; if( !isset($_SESSION[$key]) ){ require_once strtoupper($key) . '.php'; $class = strtoupper($key); $_SESSION[$key] = serialize(new $class()); } // try to include class declaration file before use it if( !class_exists(strtoupper($key), FALSE) ){ require_once strtoupper($key). '.php'; $obj = unserialize($_SESSION[$key]); // TODO: whatever you what to do with $obj ... } ?>
当然, 你也可以用auto_loader来加载类的声明文件.
From 迷途知返, post 深入剖析php的Incomplete Object
RELATED POSTS: