通常情况,在使用php面向对象开发的过程中,一个类的定义都是一个文件,这样子下来,当类与类之间需要相互引用的时候就需要include(require)相应的类文件,如此一来带来的一个不是问题的问题,就是每次需要用到某个类的时候就需要去手工include(require)。
在php5之后已经有了类的自动加载机制,可以定义__autoload函数,在使用到某个未定义的类,执行php会出错,但是在此时php引擎在返回失败之前会去check下是否有定义__autoload去加载需要的类。
目前可以是用php的__autoload机制或者SPL(Standard PHP Library)的spl_autoload机制来自动加载。
0. php中自动加载机制的主要原理
通过查阅php的源代码,在zend api中的可以发现php在初始化执行器时:
1. 会先将autoload的注册函数清空;
2. 然后执行过程中如果要获取某个类时通过zend_fetch_class函数获取;
3. 而zend_fetch_class中通过zend_lookup_class_ex函数去查找对应的类是否存在,在zend_lookup_class_ex函数中实现了自动加载的功能。
如以下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
// 初始化执行器
void
init_executor(TSRMLS_D)
/* {{{ */
{
....
EG(in_autoload) = NULL;
EG(autoload_func) = NULL;
....
}
// 获取类指针
zend_class_entry *zend_fetch_class(
const
char
*class_name, uint class_name_len,
int
fetch_type TSRMLS_DC)
/* {{{ */
{
zend_class_entry **pce;
int
use_autoload = (fetch_type & ZEND_FETCH_CLASS_NO_AUTOLOAD) == 0;
...
if
(zend_lookup_class_ex(class_name, class_name_len, NULL, use_autoload, &pce TSRMLS_CC) == FAILURE) {
...
}
return
*pce;
}
// 查找类是否存在
ZEND_API
int
zend_lookup_class_ex(
const
char
*name,
int
name_length,
const
zend_literal *key,
int
use_autoload, zend_class_entry ***ce TSRMLS_DC)
/* {{{ */
{
zval **args[1];
zval autoload_function;
...
/* The compiler is not-reentrant. Make sure we __autoload() only during run-time
* (doesn't impact fuctionality of __autoload()
*/
if
(!use_autoload || zend_is_compiling(TSRMLS_C)) {
...
return
FAILURE;
}
...
fcall_info.size =
sizeof
(fcall_info);
fcall_info.function_table = EG(function_table);
fcall_info.function_name = &autoload_function;
// 这里的autoload_function指定义的__autoload函数
...
fcall_cache.initialized = EG(autoload_func) ? 1 : 0;
fcall_cache.function_handler = EG(autoload_func);
// 这里的autoload_func指注册autoload_func函数
...
retval = zend_call_function(&fcall_info, &fcall_cache TSRMLS_CC);
...
return
retval;
}
|
也就可以发现php在自动加载类时最重要的异步是:zend_call_function(&fcall_info, &fcall_cache TSRMLS_CC);
当中fcall_info指针里面的function_name是指php自带的“__autoload”函数
而fcall_cache指针里面定义的function_handler是用户注册的autoload_func
而在zend_call_function中执行过程有一个重要步骤是:
查看fcall_cache是否定义:如果有定义,则执行fcall_cache的handler;如果没有定义,则才执行fcall_info.function_name,也就是__autoload函数。
所以,综上可以结论:
php的自动加载未定义类过程:
1. 检查是否注册autoload_func
2. 如果没有注册autoload_func,则检查是否定义了__autoload;如果有定义__autoload则执行用户定义的加载方式,如果没有定义__autoload则返回错误。
3. 如果有注册autoload_func则执行注册的加载函数,不执行__autoload。
php有两种自动加载方式:
1. 通过定义__autoload函数来处理未定义类的加载;
2. 通过注册autoload_func来处理未定义类的加载,即:通过spl_autoload_register来注册autoload_func
当中注册autoload_func的方式的优先级高于使用__autoload方式,也就是说,假如同时定义__autoload和注册autoload_func,只会执行注册的autoload_func,不会继续执行__autoload。
1.使用__autoload方式
void __autoload ( string $class )
如:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
/* Foo.class.php */
class
Foo{
function
__construct()
{
echo
'constructed'
;
}
}
/* test_autoload.php*/
function
__autoload(
$className
)
{
include
(
$className
.
'.class.php'
);
}
$foo
=
new
Foo();
// output:constructed
|
在test.php中就不需要加载Foo.class.php了,__autoload会自动加载类Foo的定义。
不过要注意的是:
1. 在php 5.3.0 版之前,__autoload 函数抛出的异常不能被 catch 语句块捕获并会导致一个致命错误。从php 5.3.0+ 之后,__autoload 函数抛出的异常可以被 catch 语句块捕获,但需要遵循一个条件。如果抛出的是一个自定义异常,那么必须存在相应的自定义异常类。
2. 一个项目中只能定义一个__autoload,假如说当项目中引用了第三方文件时,刚好第三方文件中也使用了__autoload;或者项目成员多的时候,定义了不同的__autoload;此时就会出错,因为__autoload函数拥有全局域,php也支不支持函数重载,只能将几个__autoload合并成一个。
如此一来就有了更加灵活的spl_autoload_register()的自动加载机制,php官方也建议不要使用__autoload而使用spl_autoload_register()。
2. 使用spl_autoload_register方式
SPL(Standard PHP Library),是php的标准库,php5.1+ 支持。spl的自动加载机制主要原理就是通过spl_autoload_register注册函数,并且将php中autoload_func指向了注册的函数。如果通过spl_autoload_register来注册函数,spl会执行它默认的spl_autoload()函数。
bool spl_autoload_register ([ callback $autoload_function ] )
1
2
3
4
5
6
7
|
/* test_autoload.php*/
function
autoloadHandler(
$className
)
{
include
(
$className
.
'.class.php'
);
}
spl_autoload_register(
'autoloadHandler'
);
$foo
=
new
Foo();
// output:constructed
|
如果是在类中定义的handler函数,如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
/* test_autoload.php*/
class
Autoloader
{
static
function
init()
{
spl_autoload_register(
$this
,
'autoloadHandler'
);
}
public
function
autoloadHandler(
$className
)
{
include
(
$className
.
'.class.php'
);
}
}
Autoloader::init();
$foo
=
new
Foo();
// output:constructed
|
上面就是php类自动加载的主要两种方式,我觉得使用自动加载的好处有:
1. 不在需要需要用到什么类文件就require(include)了
2. 在设置autoload的过程添加一个map缓存已经加载过的类,也不再需要使用require_once(include_once)了
3. 按需加载,避免require(include)不必要的文件
补充:
在PHP开发过程中,如果希望从外部引入一个class,通常会使用include和require方法,去把定义这个class的文件包含进来,但是这样可能会使得在引用文件的新脚本中,存在大量的include或require方法调用,如果一时疏忽遗漏则会产生错误,使得代码难以维护。
自PHP5后,引入了__autoload这个拦截器方法,可以自动对class文件进行包含引用,通常我们会这么写:
function __autoload($className) {
include_once $className . '.class.php';
}
$user = new User();
当PHP引擎试图实例化一个未知类的操作时,会调用__autoload()方法,在PHP出错失败前有了最后一个机会加载所需的类。因此,上面的这段代码执行时,PHP引擎实际上替我们自动执行了一次__autoload方法,将User.class.php这个文件包含进来。
在__autoload函数中抛出的异常不能被catch语句块捕获并导致致命错误。
如果使用 PHP的CLI交互模式时,自动加载机制将不会执行。
当你希望使用PEAR风格的命名规则,例如需要引入User/Register.php文件,也可以这么实现:
//加载我
function __autoload($className) {
$file = str_replace('_', DIRECTORY_SEPARATOR, $className);
include_once $file . 'php';
}
$userRegister = new User_Register();
这种方法虽然方便,但是在一个大型应用中如果引入多个类库的时候,可能会因为不同类库的autoload机制而产生一些莫名其妙的问题。在PHP5引入SPL标准库后,我们又多了一种新的解决方案,spl_autoload_register()函数。
此函数的功能就是把函数注册至SPL的__autoload函数栈中,并移除系统默认的__autoload()函数。一旦调用spl_autoload_register()函数,当调用未定义类时,系统会按顺序调用注册到spl_autoload_register()函数的所有函数,而不是自动调用__autoload()函数,下例调用的是User/Register.php而不是User_Register.class.php:
//不加载我
function __autoload($className) {
include_once $className . '.class.php';
}
//加载我
function autoload($className) {
$file = str_replace('/', DIRECTORY_SEPARATOR, $className);
include_once $file . '.php';
}
//开始加载
spl_autoload_register('autoload');
$userRegister = new User_Register();
在使用spl_autoload_register()的时候,我们还可以考虑采用一种更安全的初始化调用方法,参考如下:
//系统默认__autoload函数
function __autoload($className) {
include_once $className . '.class.php';
}
//可供SPL加载的__autoload函数
function autoload($className) {
$file = str_replace('_', DIRECTORY_SEPARATOR, $className);
include_once $file . '.php';
}
//不小心加载错了函数名,同时又把默认__autoload机制给取消了……囧
spl_autoload_register('_autoload', false);
//容错机制
if(false === spl_autoload_functions()) {
if(function_exists('__autoload')) {
spl_autoload_register('__autoload', false);
}
}
奇技淫巧:在Unix/Linux环境下,如果你有多个规模较小的类,为了管理方便,都写在一个php文件中的时候,可以通过以ln -s命令做软链接的方式快速分发成多个不同类名的拷贝,再通过autoload机制进行加载。