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

results matching ""

    No results matching ""