PHP/composer开发中,我们只需要require 'vendor/autoload.php'
,然后就可以直接使用各种类了。那么这些类是如何加载的呢?其中有没有什么可以优化的点呢?
概览 PHP/composer下,类的加载主要到如下部分(还没有包括各个部分的初始化逻辑):
1 2 3 4 5 6 7 8 PHP中zend_lookup_class_ex |-> EG(class_table) |-> spl_autoload_call |-> ComposerAutoloadClassLoader::loadClass |-> findFile |-> class map lookup |-> PSR-4 lookup |-> PSR-0 lookup
PHP的类加载 首先,PHP在运行的时候,需要一个类,是通过zend_lookup_class_ex
来找到这个类的相关信息的。
zend_lookup_class_ex
查找类的主要逻辑如下(假设类名字放到变量lc_name中):
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 45 46 47 48 49 50 51 52 53 54 55 ZEND_API zend_class_entry *zend_lookup_class_ex (zend_string *name, const zval *key, int use_autoload) { if (ZSTR_VAL(name)[0 ] == '') { lc_name = zend_string_alloc(ZSTR_LEN(name) - 1, 0); zend_str_tolower_copy(ZSTR_VAL(lc_name), ZSTR_VAL(name) + 1, ZSTR_LEN(name) - 1); } else { lc_name = zend_string_tolower(name); } // 2. 直接在class_table中查找 ce = zend_hash_find_ptr(EG(class_table), lc_name); if (ce) { if (!key) { zend_string_release(lc_name); } return ce; } // 3. 如果没有autoload_func,则注册默认的__autoload if (!EG(autoload_func)) { zend_function *func = zend_hash_str_find_ptr(EG(function_table), ZEND_AUTOLOAD_FUNC_NAME, sizeof(ZEND_AUTOLOAD_FUNC_NAME) - 1); if (func) { EG(autoload_func) = func; } else { if (!key) { zend_string_release(lc_name); } return NULL; } } // 4. 加载ACLASS的过程中,又加载ACLASS,递归加载,直接找不到类 if (zend_hash_add_empty_element(EG(in_autoload), lc_name) == NULL) { if (!key) { zend_string_release(lc_name); } return NULL; } // 5. 调用autoload_func ZVAL_STR_COPY(&fcall_info.function_name, EG(autoload_func)->common.function_name); fcall_info.symbol_table = NULL; zend_exception_save(); if ((zend_call_function(&fcall_info, &fcall_cache) == SUCCESS) && !EG(exception)) { ce = zend_hash_find_ptr(EG(class_table), lc_name); } zend_exception_restore(); if (!key) { zend_string_release(lc_name); } return ce; }
lc_name转化成小写(这说明PHP中类名字不区分大小写)
然后在EG(class_table)找,如果找到,直接返回(我们自己注册的类,扩展注册的类都是这样找到的)
然后查看EG(autoload_func),如果没有则将__autoload 注册上(值得注意的是,如果注册了EG(autoload_func),则不会走__autoload)
通过EG(in_autoload)判断是否递归加载了(EG(in_autoload)是一个栈,记载了那些类正在被autoload加载)
然后调用EG(autoload_func),并返回类信息
SPL扩展注册 刚刚可以看到,PHP只会调用EG(autoload_func),根本没有什么SPL的事情,那么SPL是如何让PHP调用自己的类加机制的呢?
首先,我去找SPL扩展的MINIT过程,结果发现其中并没有相关的逻辑。
出乎我的意料,这个注册过程在spl_autoload_register
中完成:
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 PHP_FUNCTION(spl_autoload_register) { if (SPL_G(autoload_functions) && zend_hash_exists(SPL_G(autoload_functions), lc_name)) { if (!Z_ISUNDEF(alfi.closure)) { Z_DELREF_P(&alfi.closure); } goto skip; } if (!SPL_G(autoload_functions)) { ALLOC_HASHTABLE(SPL_G(autoload_functions)); zend_hash_init(SPL_G(autoload_functions), 1 , NULL , autoload_func_info_dtor, 0 ); } spl_func_ptr = zend_hash_str_find_ptr(EG(function_table), "spl_autoload" , sizeof ("spl_autoload" ) - 1 ); if (EG(autoload_func) == spl_func_ptr) { autoload_func_info spl_alfi; spl_alfi.func_ptr = spl_func_ptr; ZVAL_UNDEF(&spl_alfi.obj); ZVAL_UNDEF(&spl_alfi.closure); spl_alfi.ce = NULL ; zend_hash_str_add_mem(SPL_G(autoload_functions), "spl_autoload" , sizeof ("spl_autoload" ) - 1 , &spl_alfi, sizeof (autoload_func_info)); if (prepend && SPL_G(autoload_functions)->nNumOfElements > 1 ) { HT_MOVE_TAIL_TO_HEAD(SPL_G(autoload_functions)); } } if (zend_hash_add_mem(SPL_G(autoload_functions), lc_name, &alfi, sizeof (autoload_func_info)) == NULL ) { if (obj_ptr && !(alfi.func_ptr->common.fn_flags & ZEND_ACC_STATIC)) { Z_DELREF(alfi.obj); } if (!Z_ISUNDEF(alfi.closure)) { Z_DELREF(alfi.closure); } if (UNEXPECTED(alfi.func_ptr->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE)) { zend_string_release(alfi.func_ptr->common.function_name); zend_free_trampoline(alfi.func_ptr); } } if (prepend && SPL_G(autoload_functions)->nNumOfElements > 1 ) { HT_MOVE_TAIL_TO_HEAD(SPL_G(autoload_functions)); } skip: zend_string_release(lc_name); } if (SPL_G(autoload_functions)) { EG(autoload_func) = zend_hash_str_find_ptr(EG(function_table), "spl_autoload_call" , sizeof ("spl_autoload_call" ) - 1 ); } else { EG(autoload_func) = zend_hash_str_find_ptr(EG(function_table), "spl_autoload" , sizeof ("spl_autoload" ) - 1 ); } RETURN_TRUE; }
在composer环境下,这个函数的功能就是,将用户的autoload函数放到SPL_G(autoload_functions)中,且将spl_autoload_call注册到PHP中。
这样,PHP在找一个类的时候,就会调用spl_autoload_call了。
spl_autoload_call逻辑 spl_autoload_call
的逻辑很简单:
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 PHP_FUNCTION(spl_autoload_call) { if (SPL_G(autoload_functions)) { HashPosition pos; zend_ulong num_idx; int l_autoload_running = SPL_G(autoload_running); SPL_G(autoload_running) = 1 ; lc_name = zend_string_alloc(Z_STRLEN_P(class_name), 0 ); zend_str_tolower_copy(ZSTR_VAL(lc_name), Z_STRVAL_P(class_name), Z_STRLEN_P(class_name)); zend_hash_internal_pointer_reset_ex(SPL_G(autoload_functions), &pos); while (zend_hash_get_current_key_ex(SPL_G(autoload_functions), &func_name, &num_idx, &pos) == HASH_KEY_IS_STRING) { alfi = zend_hash_get_current_data_ptr_ex(SPL_G(autoload_functions), &pos); if (UNEXPECTED(alfi->func_ptr->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE)) { zend_function *copy = emalloc(sizeof (zend_op_array)); memcpy (copy, alfi->func_ptr, sizeof (zend_op_array)); copy->op_array.function_name = zend_string_copy(alfi->func_ptr->op_array.function_name); zend_call_method(Z_ISUNDEF(alfi->obj)? NULL : &alfi->obj, alfi->ce, ©, ZSTR_VAL(func_name), ZSTR_LEN(func_name), retval, 1 , class_name, NULL ); } else { zend_call_method(Z_ISUNDEF(alfi->obj)? NULL : &alfi->obj, alfi->ce, &alfi->func_ptr, ZSTR_VAL(func_name), ZSTR_LEN(func_name), retval, 1 , class_name, NULL ); } zend_exception_save(); if (retval) { zval_ptr_dtor(retval); retval = NULL ; } if (zend_hash_exists(EG(class_table), lc_name)) { break ; } zend_hash_move_forward_ex(SPL_G(autoload_functions), &pos); } zend_exception_restore(); zend_string_free(lc_name); SPL_G(autoload_running) = l_autoload_running; } else { zend_call_method_with_1_params(NULL , NULL , NULL , "spl_autoload" , NULL , class_name); } }
判断SPL_G(autoload_functions)存在
依次调用autoload_functions
如果调用完成后,这个类存在了,那就返回
至此,SPL的部分已经讲完了。我们来看看composer做了什么。
composer注册autoload composer的autoload注册在 ‘vendor/autoload.php’ 中完成,这个文件完成了两件事:
include vendor/composer/autoload_real.php
调用ComposerAutoloaderInit<rand_id>::getLoader()
而vendor/composer/autoload_real.php
仅仅定义了ComposerAutoloaderInit<rand_id>
类和composerRequire<rand_id>
函数。
<rand_id>
是类似id一样的东西,确保要加载多个composer的autoload的时候不会冲突。composerRequire<rand_id>
则是为了避免ComposerAutoloader
require文件的时候,文件修改了ComposerAutoloader
的东西。
接下来我们关注下ComposerAutoloaderInit<rand_id>::getLoader()
做了哪些事情。
这个类的loader只会初始化一次,第二次是直接返回已经存在的loader了:
1 2 3 if (null !== self ::$loader ) { return self ::$loader ; }
如果是第一次调用,先注册['ComposerAutoloaderInit<rand_id>', 'loadClassLoader']
,然后new一个ComposerAutoloadClassLoader
作为$loader
,然后立马取消注册loadClassLoader
。
也就是说['ComposerAutoloaderInit<rand_id>', 'loadClassLoader']
的唯一作用就是加载ComposerAutoloadClassLoader
。
接下来就是在ComposerAutoloaderInit<rand_id>::getLoader()
初始刚刚拿到的$loader
了:
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 $map = require __DIR__ . '/autoload_namespaces.php' ;foreach ($map as $namespace => $path ) { $loader ->set ($namespace , $path ); } $map = require __DIR__ . '/autoload_psr4.php' ;foreach ($map as $namespace => $path ) { $loader ->setPsr4 ($namespace , $path ); } $classMap = require __DIR__ . '/autoload_classmap.php' ;if ($classMap ) { $loader ->addClassMap ($classMap ); } $loader ->register (true );$includeFiles = require __DIR__ . '/autoload_files.php' ;foreach ($includeFiles as $fileIdentifier => $file ) { composerRequire32715bcfade9cdfcb6edf37194a34c36 ($fileIdentifier , $file ); } return $loader ;
autoload_namespaces.php
返回的是各个包里面声明的PSR-0加载规则,是一个数组。key为namespace,有可能为空字符串;value为路径的数组。
$loader->set
,如果$namespace/$prefix为空,直接放到$loader->fallbackDirsPsr0数组中。如果不为空,则放到$loader->prefixesPsr0[$prefix[0]][$prefix]中(这可能是为了减少PHP内部的hash表冲突,加快查找速度)。
autoload_psr4.php
返回的是各个包里面声明的PSR-4加载规则,是一个数组。key为namespace,有可能为空字符串;value为路径的数组。
$loader->setPsr4
,如果$namespace/$prefix为空,直接放到$loader->fallbackDirsPsr4数组中。如果不为空,则将$namespace/$prefix的长度放到$loader->prefixLengthsPsr4[$prefix[0]][$prefix]中,将路径放到$loader->prefixDirsPsr4[$prefix]中。
autoload_classmap.php
返回的是各个包里面声明的classmap加载规则,是一个数组。key为class全名,value为文件路径。(这个信息是composer扫描全部文件得到的)
$loader->addClassMap
,则将这些信息array_merge到$loader->classMap中。
autoload_files.php
返回的是各个包里面声明的file加载规则,是一个数组。key为每个文件的id/hash,value是每个文件的路径。
注意,autoload_files.php里面的文件,在getLoader
的时候就已经被include了。
到这儿,我们的$loader已经初始化好了,而且也已经注册到SPL中了
composer加载类 我们之前是将[$loader, 'loadClass']
注册到了SPL中,那就看看它的逻辑吧:
1 2 3 4 5 6 7 8 public function loadClass ($class ) { if ($file = $this ->findFile ($class )) { includeFile ($file ); return true ; } }
所以看下来,重点在findFile函数里面:
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 public function findFile ($class ) { if (isset ($this ->classMap[$class ])) { return $this ->classMap[$class ]; } if ($this ->classMapAuthoritative || isset ($this ->missingClasses[$class ])) { return false ; } if (null !== $this ->apcuPrefix) { $file = apcu_fetch ($this ->apcuPrefix.$class , $hit ); if ($hit ) { return $file ; } } $file = $this ->findFileWithExtension ($class , '.php' ); return $file ; }
如果是classmap的加载规则,那就会在这儿加载成功。如果是PSR-0或者PSR-4,则需要看看findFileWithExtension的逻辑了:
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 private function findFileWithExtension ($class , $ext ) { $logicalPathPsr4 = strtr ($class , '' , DIRECTORY_SEPARATOR) . $ext ; $first = $class [0 ]; if (isset ($this ->prefixLengthsPsr4[$first ])) { $subPath = $class ; while (false !== $lastPos = strrpos ($subPath , '' )) { $subPath = substr ($subPath , 0 , $lastPos ); $search = $subPath .'' ; if (isset ($this ->prefixDirsPsr4[$search ])) { $pathEnd = DIRECTORY_SEPARATOR . substr ($logicalPathPsr4 , $lastPos + 1 ); foreach ($this ->prefixDirsPsr4[$search ] as $dir ) { if (file_exists ($file = $dir . $pathEnd )) { return $file ; } } } } } foreach ($this ->fallbackDirsPsr4 as $dir ) { if (file_exists ($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4 )) { return $file ; } } if (false !== $pos = strrpos ($class , '' )) { $logicalPathPsr0 = substr ($logicalPathPsr4 , 0 , $pos + 1 ) . strtr (substr ($logicalPathPsr4 , $pos + 1 ), '_' , DIRECTORY_SEPARATOR); } else { $logicalPathPsr0 = strtr ($class , '_' , DIRECTORY_SEPARATOR) . $ext ; } if (isset ($this ->prefixesPsr0[$first ])) { foreach ($this ->prefixesPsr0[$first ] as $prefix => $dirs ) { if (0 === strpos ($class , $prefix )) { foreach ($dirs as $dir ) { if (file_exists ($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0 )) { return $file ; } } } } } foreach ($this ->fallbackDirsPsr0 as $dir ) { if (file_exists ($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0 )) { return $file ; } } if ($this ->useIncludePath && $file = stream_resolve_include_path ($logicalPathPsr0 )) { return $file ; } return false ; }
$prefix不为空的PSR-4加载规则:
比如类ABC,先找AB对应目录下面的C.php;再找A对应目录下面的BC.php;以此类推
$prefix为空的PSR-4加载规则
如果找不到,那就在fallbackDirsPsr4下找ABC.php文件
$prefix不为空的PSR-0加载规则
PSR-0支持namespace和下划线分隔的类(PEAR-like class name);这点对一些需要向namespace迁移的旧仓库很有用
对于类ABC或者A_B_C,先找AB对应目录下面的C.php;再找A对应目录下面的BC.php;以此类推
$prefix为空的PSR-0加载规则
如果找不到,直接在prefixesPsr0中找ABC.php文件
如果还没有找到,在条件允许的状态下,可以到include path中找ABC.php文件
这样,composer就找到了这个类对应的文件,并且include了。