php编译期use别名冲突
Fatal error: Cannot use ... as ... because the name is already in use in ...
之前分析过该问题,把问题复杂化了,这里重新做下简化;
触发条件
:
1. class A {}
2. use B (默认被解析成 use B as B), 或者 use B as C;
3. 同命名空间 类名 与 use别名冲突;
触发时机
:use ... as alias, 会在编译期class_table检查alias是否冲突,冲突在则fatal error
Notice
: 因为触发点是use as,所以就算出现冲突,但是use别名先被加载,也不会触发fatal error;
所以
,具体业务场景复杂,不能依赖require或者autoload顺序,需要人工保证不会出现名称冲突;
并且
,use const 与 use function 同样存在该问题;
0x00 注意: 官方文档可能是错误的
http://php.net/manual/zh/language.namespaces.faq.php#language.namespaces.faq.conflict
There is no name conflict, even though the class MyClass exists within the my\stuff namespace, because the MyClass definition is in a separate file.
<?php
//file1.php
namespace my\stuff;
class MyClass {}
//another.php
namespace another;
class thing {}
//file2.php 入口文件
namespace my\stuff;
include 'file1.php';
include 'another.php';
use another\thing as MyClass;
$a = new MyClass; // instantiates class "thing" from namespace another
修改官方case入口文件:
Fatal error: Cannot use another\thing as MyClass because the name is already in use in...
<?php
//file1.php 入口文件
namespace my\stuff;
// MyClass声明尽管写在后面,但是执行到var_dump()时候,已经可以找到说明类声明先解析
var_dump(array_search(__NAMESPACE__ . "\\MyClass", get_declared_classes(), true) !== false);
include 'file2.php';
include 'another.php';
class MyClass {}
//another.php
namespace another;
class thing {}
//file2.php
namespace my\stuff
use another\thing as MyClass;
$a = new MyClass; // instantiates class "thing" from namespace another
原因
:
官方的case
//file2.php 入口文件
namespace my\stuff
include 'file1.php';
include 'another.php';
use another\thing as MyClass;
根据zend_language_parser.y确认命名空间声明与use语句解析优先级高于include,
所以会先解析 use another\thing as MyClass;
, 对别名MyClass加入命名空间前缀,变为my\stuff\MyClass,
去CG(class_table)查重,然后再include file1.php, 声明my\stuff\MyClass并加入CG(class_table), 无问题;
修改后的case
//file1.php 入口文件
namespace my\stuff;
include 'file2.php';
include 'another.php';
class MyClass {}
根据zend_language_parser.y文件可以查看到 类声明的优先级要高于 include;
会先解析类声明,将my\stuff\MyClass加入CG(class_table)全局类表,然后再执行include, 遇到
use another\thing as MyClass;
对别名MyClass加入命名空间前缀,变为my\stuff\MyClass,
再去CG(class_table)查重,报错;
所以,官方文档的解释
because the MyClass definition is in a separate file.
可以认为是错误的,与是否单独文件无关;只要别名检查时,同命名空间class已声明就必然报错;
这两个case看起来是矛盾的,原因是因为声明类时候会加入CG(class_table),
use...as...结果加入CG(current_import)却检查CG(class_table), 导致顺序不一致,结果不一样
个人认为算是php use语法的一个bug
0x01 以具体的case分析zend编译期use源码实现
example:
<?php
namespace A
{
class Error {}
}
namespace A
{
use \ErrorException as Error;
}
version 5.6.16
zend_compile.c
void zend_do_use(znode *ns_name, znode *new_name);
// (gdb) source .gdbinit
// (gdb) b zend_do_use
// (gdb) r
// (gdb) focus cmd
// use A as B;
// ns_name: use后面的node 即导入名称ErrorException节点
// new_name: as后面的node 即别名Error节点,如无as则new_name为0
void zend_do_use(znode *ns_name, znode *new_name TSRMLS_DC) /* {{{ */
{
/*
(gdb) printzv &(ns_name->u.constant)
[0x7fffffff8750] (refcount=1) string(14): "ErrorException"
(gdb) printzv &(new_name->u.constant)
[0x7fffffff87a0] (refcount=1) string(5): "Error"
*/
// 小写导入名称别名, php类名大小写不敏感,内部全部转为小写存储
char *lcname;
// name: 导入名称别名,此处即 Error
// ns: 导入名称 此处即ErrorException
zval *name, *ns, tmp;
zend_bool warn = 0;
zend_class_entry **pce;
// 初始化compiler_globals->current_import,即存储当前命名空间导入类的hashtable
// key 别名 value 导入名称, use x as y -> current_import[strtolower(y)] = zval(x)
if (!CG(current_import)) {
CG(current_import) = emalloc(sizeof(HashTable));
zend_hash_init(CG(current_import), 0, NULL, ZVAL_PTR_DTOR, 0);
}
/*
(gdb) print_ht compiler_globals->current_import
[0x7ffff7fd32d0] {
}
*/
// 初始化ns & 将导入名称字面量复制到ns
MAKE_STD_ZVAL(ns);
ZVAL_ZVAL(ns, &ns_name->u.constant, 0, 0);
/*
(gdb) printzv ns
[0x7ffff7fd3328] (refcount=1) string(14): "ErrorException"
*/
// 以下分支语句获取导入别名,区分于是否显示使用as关键词
// 有则直接使用, 没有如注释所言,The form "use A\B" is eqivalent to "use A\B as B".
if (new_name) {
name = &new_name->u.constant;
/*
(gdb) printzv name
[0x7fffffff87a0] (refcount=1) string(5): "Error"
*/
} else {
const char *p;
/* The form "use A\B" is eqivalent to "use A\B as B".
So we extract the last part of compound name to use as a new_name */
name = &tmp;
// 查找最后一次出现的'\'的位置
p = zend_memrchr(Z_STRVAL_P(ns), '\\', Z_STRLEN_P(ns));
if (p) {
// 导入复合名称
// use A\B; 别名为B
ZVAL_STRING(name, p+1, 1);
} else {
// 导入非复合名称
// use B; 别名为B
ZVAL_ZVAL(name, ns, 1, 0);
// 在非命名空间下导入非复合名称,给予警告
warn = !CG(current_namespace);
/*
命名空间内导入无别名非复合名称没问题
<?php
namespace ns;
use something;
!!! 在命名空间外导入无别名非复合名称会有warning
<?php
use something;
有别名则无问题
<?php
use something as another;
*/
}
}
// 此处为 "error"
lcname = zend_str_tolower_dup(Z_STRVAL_P(name), Z_STRLEN_P(name));
if (((Z_STRLEN_P(name) == sizeof("self")-1) &&
!memcmp(lcname, "self", sizeof("self")-1)) ||
((Z_STRLEN_P(name) == sizeof("parent")-1) &&
!memcmp(lcname, "parent", sizeof("parent")-1))) {
zend_error_noreturn(E_COMPILE_ERROR, "Cannot use %s as %s because '%s' is a special class name", Z_STRVAL_P(ns), Z_STRVAL_P(name), Z_STRVAL_P(name));
}
/*
<?php
namespace ns;
保留字不允许定义别名
use something as self;
use something as parent;
*/
/*
(gdb) printzv compiler_globals->current_namespace
[0x7ffff7fd10f8] (refcount=1) string(1): "A"
*/
// 当前use语句是否在命名空间内
if (CG(current_namespace)) {
// !!! 将别名拼接use所在命名空间的名称作为前缀当做key存入编译期类表(HashTable) 避免类名冲突
// c_ns_name: 当前命名空间的导入名称: 命名空间\别名
/* Prefix import name with current namespace name to avoid conflicts with classes */
char *c_ns_name = emalloc(Z_STRLEN_P(CG(current_namespace)) + 1 + Z_STRLEN_P(name) + 1);
// 将当前命名空间转换成小写copy到c_ns_name
zend_str_tolower_copy(c_ns_name, Z_STRVAL_P(CG(current_namespace)), Z_STRLEN_P(CG(current_namespace)));
// c_ns_name = "a"
c_ns_name[Z_STRLEN_P(CG(current_namespace))] = '\\';
// c_ns_name = "a\\"
// 把小写导入名称copy到c_ns_name
memcpy(c_ns_name+Z_STRLEN_P(CG(current_namespace))+1, lcname, Z_STRLEN_P(name)+1);
// c_ns_name = "a\\error"
// print_ht compiler_globals->class_table
// 查看编译期全局类表(hashtable)是否已经含有 key "a\\error"
// 因为之前声明过 namespace A { class Error {} }, 所以"a\\error" 已经存在,所以发生名称冲突
// 确认冲突的双方不是同一个类,然后抛出编译器错误
if (zend_hash_exists(CG(class_table), c_ns_name, Z_STRLEN_P(CG(current_namespace)) + 1 + Z_STRLEN_P(name)+1)) {
// 将use名称从zval提取出来转小写复制到临时变量
// tmp2 "errorexception"
char *tmp2 = zend_str_tolower_dup(Z_STRVAL_P(ns), Z_STRLEN_P(ns));
// 判断use名称是否等于 命名空间\别名,如果相等说明冲突的双方是同一个类定义,忽略
// 即 namespace A; use A\Exception as Exception; class Exception {} 这种情况
if (Z_STRLEN_P(ns) != Z_STRLEN_P(CG(current_namespace)) + 1 + Z_STRLEN_P(name) ||
// errorexception != a\\error
memcmp(tmp2, c_ns_name, Z_STRLEN_P(ns))) {
zend_error_noreturn(E_COMPILE_ERROR, "Cannot use %s as %s because the name is already in use", Z_STRVAL_P(ns), Z_STRVAL_P(name));
}
efree(tmp2);
}
efree(c_ns_name);
} else if
// 命名空间外使用use情况
// 与导入别名同名的类已经定义,且已定义类为用户类而非内部类
(zend_hash_find(CG(class_table), lcname, Z_STRLEN_P(name)+1, (void**)&pce) == SUCCESS &&
(*pce)->type == ZEND_USER_CLASS &&
(*pce)->info.user.filename == CG(compiled_filename)) {
char *c_tmp = zend_str_tolower_dup(Z_STRVAL_P(ns), Z_STRLEN_P(ns));
// 字面导入名称与导入名称不相等
if (Z_STRLEN_P(ns) != Z_STRLEN_P(name) ||
memcmp(c_tmp, lcname, Z_STRLEN_P(ns))) {
zend_error_noreturn(E_COMPILE_ERROR, "Cannot use %s as %s because the name is already in use", Z_STRVAL_P(ns), Z_STRVAL_P(name));
}
efree(c_tmp);
/*
<?php
namespace { class UserClass {} }
namespace A { class SplDoublyLinkedList {} }
namespace {
// 命名空间外 别名可以与系统内部同名
use A\SplDoublyLinkedList as SplDoublyLinkedList;
// 因为可以这样使用
assert(new SplDoublyLinkedList() instanceof A\SplDoublyLinkedList);
assert(new \SplDoublyLinkedList() instanceof \SplDoublyLinkedList);
// !!! 但是不可以与用户类同名 !!! waring
use A\SplDoublyLinkedList as UserClass;
}
*/
}
// 存储当前导入类
// current_import["error"] = "ErrorException"
if (zend_hash_add(CG(current_import), lcname, Z_STRLEN_P(name)+1, &ns, sizeof(zval*), NULL) != SUCCESS) {
zend_error_noreturn(E_COMPILE_ERROR, "Cannot use %s as %s because the name is already in use", Z_STRVAL_P(ns), Z_STRVAL_P(name));
}
if (warn) {
// 当不使用命名空间却 声明 use "strict";
// 额,js...
if (!strcmp(Z_STRVAL_P(name), "strict")) {
zend_error_noreturn(E_COMPILE_ERROR, "You seem to be trying to use a different language...");
}
zend_error(E_WARNING, "The use statement with non-compound name '%s' has no effect", Z_STRVAL_P(name));
}
efree(lcname);
zval_dtor(name);
}
/* }}} */
use function 与 use const 逻辑大致相同,不再分析,参考下方case;
0x02 单文件case
无问题代码
<?php
namespace
{
use \ErrorException as Error;
}
namespace
{
class Error {}
}
Fatal error: Cannot use ErrorException as Error because the name is already in use
<?php
namespace
{
class Error {}
}
namespace
{
use \ErrorException as Error;
}
无问题代码
<?php
namespace A
{
use \Exception;
}
namespace A
{
class Exception {}
}
Fatal error: Cannot use Exception as Exception because the name is already in use in
<?php
namespace A
{
class Exception {}
}
namespace A
{
use \Exception;
}
Fatal error: Cannot use ErrorException as Error because the name is already in use in
<?php
namespace A
{
class Error {}
}
namespace A
{
use \ErrorException as Error;
}
无问题代码:
<?php
namespace Earth
{
use Moon\Boy;
class Girl {}
}
namespace Earth
{
class Boy {}
}
namespace Moon
{
class Boy {}
}
Fatal error 代码:
<?php
namespace Earth
{
class Boy {}
}
namespace Earth
{
use Moon\Boy;
// Fatal error: Cannot use Moon\Boy as Boy because the name is already in use
// 这里实际上 是 use ... as ValidationException
// 会对as的别名做检查该命名空间已经存在ValidationException, 所以报错
class Girl {}
}
namespace Moon
{
class Boy {}
}
Fatal error 代码:
<?php
namespace Earth
{
class Boy {}
}
namespace Earth
{
use Moon\SuperGirl as Boy;
// Fatal error: Cannot use Moon\Boy as Boy because the name is already in use
// 同理
class Girl {}
}
namespace Moon
{
class SuperBoy {}
}
use function Fatal error
Fatal error: Cannot use function Moon\Get as Get because the name is already in use
原因同上,且拆成多个文件仍旧成立(依赖require顺序)
<?php
namespace Earth
{
function Get() {}
}
namespace Earth
{
use function Moon\Get;
function Post() {}
}
namespace Moon
{
function Get() {}
}
use const Fatal error
Fatal error: Cannot use const Moon\BOY as BOY because the name is already in use
原因同上,且拆成多个文件仍旧成立(依赖require顺序)
<?php
namespace Earth
{
const BOY = 1;
}
namespace Earth
{
use const Moon\BOY;
const GIRL = 2;
}
namespace Moon
{
const BOY = 3;
}
0x03 多文件case
无问题代码(入口:EarthGirl.php):
EarthBoy.php
<?php
namespace Earth;
class Boy {}
EarthGirl.php
<?php
namespace Earth;
use Moon\Boy;
require __DIR__ . "/EarthBoy.php";
require __DIR__ . "/MoonBoy.php";
class Girl {}
MoonBoy.php
<?php
namespace Moon;
class Boy {}
Fatal error 代码(入口:EarthBoy.php
):
EarthBoy.php
<?php
namespace Earth;
require __DIR__ . "/MoonBoy.php";
require __DIR__ . "/EarthGirl.php"; // Boy 已经存在,use ... as Boy 冲突
class Boy {}
EarthGirl.php
<?php
namespace Earth;
use Moon\Boy;
class Girl {}
MoonBoy.php
<?php
namespace Moon;
class Boy {}
无问题代码(入口:R.php
):
R.php
<?php
require __DIR__ . "/EarthGirl.php";
require __DIR__ . "/EarthBoy.php";
require __DIR__ . "/MoonBoy.php";
EarthBoy.php
<?php
namespace Earth;
class Boy {}
EarthGirl.php
<?php
namespace Earth;
use Moon\Boy;
class Girl {}
MoonBoy.php
<?php
namespace Moon;
class Boy {}
Fatal error 代码(入口:R.php
):
R.php
<?php
require __DIR__ . "/EarthBoy.php";
require __DIR__ . "/EarthGirl.php"; // 同理
require __DIR__ . "/MoonBoy.php";
EarthBoy.php
<?php
namespace Earth;
class Boy {}
EarthGirl.php
<?php
namespace Earth;
use Moon\Boy;
class Girl {}
MoonBoy.php
<?php
namespace Moon;
class Boy {}
附录:之前的case
Fatal error: Cannot use PfApi\Dituiapp\Service\GroundStoreService as GroundStoreService because the name is already in use in ...
<?php
// file vendor/nova-service/pf/gen-php/Dituiapp/Interfaces/GroundStoreService.php
namespace Com\Youzan\Pf\Dituiapp\Interfaces {
interface GroundStoreService {}
}
// file vendor/nova-service/pf/gen-php/Open/Dituiapp/Interfaces/GroundStoreService.php
namespace Com\Youzan\Pf\Open\Dituiapp\Interfaces {
interface GroundStoreService {}
}
// file src/Open/Dituiapp/Service/GroundStoreService.php
namespace PfApi\Dituiapp\Service {
use Com\Youzan\Pf\Dituiapp\Interfaces\GroundStoreService as GroundStoreServiceInterface;
class GroundStoreService implements GroundStoreServiceInterface {}
}
// file src/Open/Dituiapp/Service/GroundStoreService.php
namespace PfApi\Open\Dituiapp\Service {
// 这里依赖的接口与需要的服务同名,所以用了别名
use Com\Youzan\Pf\Open\Dituiapp\Interfaces\GroundStoreService as GroundStoreServiceInterface;
use PfApi\Dituiapp\Service\GroundStoreService as ApiGroundStoreService;
class GroundStoreService implements GroundStoreServiceInterface {}
}
// file src/Open/Dituiapp/Service/GroundTaskService.php
namespace PfApi\Open\Dituiapp\Service {
use PfApi\Dituiapp\Service\GroundStoreService;
// 这里没有使用别名, 其实可以看成 use PfApi\Dituiapp\Service\GroundStoreService as GroundStoreService;
// 但是 在上一个文件src/Open/Dituiapp/Service/GroundStoreService.php中
// PfApi\Dituiapp\Service\GroundStoreService 已经被as成 ApiGroundStoreService了
// 两次as冲突,所以一旦 src/Open/Dituiapp/Service/GroundStoreService.php 与 src/Open/Dituiapp/Service/GroundStoreService.php
// 被同时require进来,就会引发Fatal Error:
// Fatal error: Cannot use PfApi\Dituiapp\Service\GroundStoreService as GroundStoreService because the name is already in use in ...
class GroundTaskService implements GroundTaskServiceInterface {}
}
another case
```php <?php namespace Zan\Framework\Utilities\Validation { class ValidationException {} }
namespace Zan\Framework\Utilities\Validation { use Zan\Framework\Contract\Utilities\Validation\ValidationException; trait ValidatesWhenResolvedTrait {} }
namespace Zan\Framework\Contract\Utilities\Validation {
class ValidationException {}
}
```XX