Skip to main content

composer的psr4

· 10 min read

我这次主要是要描述composer的psr4自动加载相关内容.php有很多历史的包袱,所以需要做很多妥协,而namespace 以及自动加载也是.

include 和 require的大坑

例子

include 和require的区别什么的可能还是某些面试官的问题之一,但是include和require都有一个致命的大坑,include和require一个相对路径是相对于工作目录的.

举个例子.

当前在index.php 的目录中

# tree

test
   ├── index.php
   ├── relative.php
   └── subdir
   ├── a.php
   └── relative.php

index.php 的代码很简单,就是包含一个路径

<?php
include "./subdir/a.php";

两个relative.php 文件分别输出自己的路径 subdir/relative.php文件:

<?php
echo "test/subdir/relative.php"

relative.php文件:

<?php
echo "test/relative.php";

那么如果与index.php 同目录下会include哪一个呢?

答案是:

# php index.php 
test/relative.php

include了与index.php 同一个目录下的relative.php 文件

而如果你在index.php的上一层目录执行,也就是test目录它甚至会报错

php test/index.php 
PHP Warning: include(./subdir/a.php): failed to open stream: No such file or directory in /root/test/index.php on line 2
PHP Warning: include(): Failed opening './subdir/a.php' for inclusion (include_path='.:/usr/share/php') in /root/test/index.php on line 2

这一切都是因为当是相对路径的时候,调用了getcwd()来获取工作目录,如果你使用shell的pwd话也可以看自己的工作目录.

由于这个比较坑的特性,php的代码如果手工使用include并且还使用了相对路径,那之后就非常难以维护了.所以我们需要尽量减少使用include相对路径,因为你知道的原因,你一旦写了一个相对路径,总会有后人copy and paste你的代码,然后把这个include也复制进去了,而这就是下一个屎坑的开始.

所以,自动加载可以减缓这种大坑的产生,因为他可以减少手工include相对路径的风险,因为他们往往会这样include文件include __DIR__ . 'aaa/bbb/ccc.php',由于不是相对路径,所以会好很多.

CLI模式与CGI/FASTCGI工作目录的不同

CLI SAPI 不会将当前目录改为已运行的脚本所在的目录。

以下范例显示了本模块与 CGI SAPI 模块之间的不同:

<?php
// 名为 test.php 的简单测试程序
echo getcwd(), "\n";
?>

在使用 CGI 版本时,其输出为

$ pwd
/tmp

$ php-cgi -f another_directory/test.php
/tmp/another_directory

明显可以看到 PHP 将当前目录改成了刚刚运行过的脚本所在的目录。

使用 CLI SAPI 模式,得到:

$ pwd
/tmp

$ php -q another_directory/test.php
/tmp

include 和require 的opcode和getcwd

require 和include 词法分析和语法分析后,生成opcode是73,ZEND_INCLUDE_OR_EVAL,在include或者require之后,如果是相对路径

最后会调用

VCWD_GETCWD(cwd, MAXPATHLEN)

这个最后就是调用glibc 下面的getcwd

getcwd 系统调用

每个进程task_struct会有fs_struct 结构,这个结构体会含有pwdroot,如果使用getcwd()这个函数,通过glibc会通过系统调用读取fs_struct的pwd属性并返回

static void get_fs_root_and_pwd_rcu(struct fs_struct *fs, struct path *root,
struct path *pwd)
{
...
*root = fs->root;
*pwd = fs->pwd;
...
}
SYSCALL_DEFINE2(getcwd, char __user *, buf, unsigned long, size)
{
int error;
struct path pwd, root;
char *page = __getname();

if (!page)
return -ENOMEM;

rcu_read_lock();
get_fs_root_and_pwd_rcu(current->fs, &root, &pwd); // 每个进程会关联一个fs_struct结构,fs_struct 结构有两个属性root和pwd描述了root目录和pwd目录

char *cwd = page + PATH_MAX;
int buflen = PATH_MAX;

prepend(&cwd, &buflen, "\0", 1);
error = prepend_path(&pwd, &root, &cwd, &buflen);
...
copy_to_user(buf, cwd, len) // 将处理后的pwd 返回到用户态
...

}

include和require总结

include以及require如果引入相对路径的文件,那么这个相对路径都是相对于getcwd(),也就是当前工作目录.

而cgi和cli模式又有不同

  • cli模式下的当前路径就是shell pwd的值
  • 而cgi 这个SAPI和cli这个CLI SAPI不一样的地方在于他会帮你切换一次工作目录到第一次运行的php文件的当前目录作为工作目录.

命名空间

命名空间是什么?

其实就是一堆限定符.

为什么要有命名空间?
因为我们要复用别人的代码,你想引用别人的一个库,别人库里写了个hello函数,你也写了个hello函数.这就麻烦了,所以引入命名空间,只要保证大家的命名空间不一样,那样就算大家都有相同的函数名,也不会冲突了.

自动加载

开始说到自动加载了,自动加载.什么是自动加载呢?

其实就是动态include,或者叫做运行时include.

平时我们怎么include文件的呢?

就是手工include一堆文件,就像我刚才上面的例子一样.这样至少有两个风险:

  • 新手使用了相对路径include
  • 得手工引入,但是include会重复引入文件,得使用include_once 或者require_once

就风险而言,新手使用相对路径引入的危险是非常大的.重复引入只是会校验多一点有一点性能影响而言.

spl_autoload_register

spl_autoload_*这一类的函数都是php自动加载的核心函数,实现自动加载则是依赖spl_autoload_register


/* {{{ proto bool spl_autoload_register([mixed autoload_function [, bool throw [, bool prepend]]])
Register given function as __autoload() implementation */
PHP_FUNCTION(spl_autoload_register)
{

...

if (zend_hash_add_mem(SPL_G(autoload_functions), lc_name, &alfi, sizeof(autoload_func_info)) == NULL) {
...
}
...
} /* }}} */

然后相关的调用会在zend_hash_exists(EG(class_table), lc_name) 判断是否在全局的EG(class_table) 里面
下面的spl_autoload_call是一个例子

PHP_FUNCTION(spl_autoload_call)
{

if (SPL_G(autoload_functions)) { // spl_autoload_register 放进去的 SPL_G(autoload_functions)
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);
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); // 调用注册的回调函数

if (zend_hash_exists(EG(class_table), lc_name)) { // 回调找到了类名,则跳出循环

break;
}
zend_hash_move_forward_ex(SPL_G(autoload_functions), &pos);
}
...
}
..
} /* }}} */

自动加载流程其实很简单 自动加载的例子

<?php
// test.php
spl_autoload_register(function ($class) {
include "$class" . '.php';
});
$obj = new ClassA();

以及类ClassA.php

<?php
class ClassA{}

下面是堆栈

(gdb) bt
#0 zif_spl_autoload_call (execute_data=0x7fffef61e0a0, return_value=0x7fffffffa2f0) at /home/dinosaur/Downloads/php-7.2.2/ext/spl/php_spl.c:393
#1 0x0000000000932807 in zend_call_function (fci=0x7fffffffa330, fci_cache=0x7fffffffa300) at /home/dinosaur/Downloads/php-7.2.2/Zend/zend_execute_API.c:833
#2 0x0000000000933000 in zend_lookup_class_ex (name=0x7fffe6920b58, key=0x7fffe70e63f0, use_autoload=1) at /home/dinosaur/Downloads/php-7.2.2/Zend/zend_execute_API.c:990
#3 0x0000000000933dbd in zend_fetch_class_by_name (class_name=0x7fffe6920b58, key=0x7fffe70e63f0, fetch_type=512) at /home/dinosaur/Downloads/php-7.2.2/Zend/zend_execute_API.c:1425
#4 0x00000000009b7e46 in ZEND_NEW_SPEC_CONST_HANDLER () at /home/dinosaur/Downloads/php-7.2.2/Zend/zend_vm_execute.h:3211
#5 0x0000000000a380a4 in execute_ex (ex=0x7fffef61e030) at /home/dinosaur/Downloads/php-7.2.2/Zend/zend_vm_execute.h:59929
#6 0x0000000000a3d0ab in zend_execute (op_array=0x7fffef683300, return_value=0x0) at /home/dinosaur/Downloads/php-7.2.2/Zend/zend_vm_execute.h:63760
#7 0x000000000094cd22 in zend_execute_scripts (type=8, retval=0x0, file_count=3) at /home/dinosaur/Downloads/php-7.2.2/Zend/zend.c:1496
#8 0x00000000008b0b4a in php_execute_script (primary_file=0x7fffffffcaa0) at /home/dinosaur/Downloads/php-7.2.2/main/main.c:2590
#9 0x0000000000a3fd23 in do_cli (argc=2, argv=0x1441a60) at /home/dinosaur/Downloads/php-7.2.2/sapi/cli/php_cli.c:1011
#10 0x0000000000a40ee0 in main (argc=2, argv=0x1441a60) at /home/dinosaur/Downloads/php-7.2.2/sapi/cli/php_cli.c:1404

所以整个自动加载的核心流程就是在查找类的时候会去调用spl_autoload_call,这个函数则会回调注册的自动加载函数,直到遍历所有的回调函数都没有找到或者在某个遍历的时候找到了直接返回。

psr规范与psr4

psrPHP Standards Recommendations的简称,而psr4和psr0有都是和自动加载相关的内容.

其实就是规定了一个简单的替换

\Aura\Web\Response\Status	Aura\Web	/path/to/aura-web/src/	/path/to/aura-web/src/Response/Status.php

psr4规定了我们如何去加载一个文件: 将完全限定名用前缀地址替换,后面则是后面的文件. 举个例子: 你要加载的类是:

\Aura\Web\Response\Status

那么你可以使用Aura\Web 映射/path/to/aura-web/src/,那么类\Aura\Web\Response\Status就会去/path/to/aura-web/src/Response/Status.php文件找

可以说有点像nginx的路由配置: 下面是nginx的配置

location ^~ /images/ {
    # 匹配任何已 /images/ 开头的任何查询并且停止搜索。任何正则表达式将不会被测试。
}

那么上面的\Aura\Web\Response\Status的psr4 有点像这样:

location ^~ /Aura/Web/ {
    root /path/to/aura-web/src/;
}

相关阅读