8.1 Обзор
Что такое пространство имен? Вообще говоря, пространство имен — это способ инкапсулировать вещи. Эту абстракцию можно увидеть во многих местах. Например, в операционных системах каталог используется для группировки связанных файлов и действует как пространство имен для файлов в каталоге. В качестве конкретного примера файл foo.txt может находиться в обоих каталогах /home/greg и /home/other, но в одном каталоге не может быть двух файлов foo.txt. Кроме того, при доступе к файлу foo.txt вне каталога /home/greg мы должны указать имя каталога и разделитель каталогов перед именем файла, чтобы получить /home/грег/foo.txt. Приложением этого принципа к области программирования является концепция пространств имен. (цитата с php.net)
Пространства имен в основном используются для решения двух типов проблем:
- Написанный пользователем код конфликтует с внутренними или сторонними именами классов, функций, констант и интерфейсов PHP.
- Создайте псевдоним для очень длинных имен идентификаторов, чтобы улучшить читаемость исходного кода.
Пространства имен PHP позволяют объединять связанные классы, функции, константы и интерфейсы. Классы, функции, константы и интерфейсы в разных пространствах имен изолированы друг от друга и не будут конфликтовать. Примечание. Пространства имен PHP могут изолировать только классы, функции, константы, и интерфейсы Функции, константы и интерфейсы, за исключением глобальных переменных.
В следующих двух разделах будет представлена внутренняя реализация пространства имен PHP, в основном из определения и использования пространства имен.
8.2 Определение пространства имен
8.2.1 Грамматика определения
Пространства имен объявляются с помощью ключевого слова namespace.Если файл содержит пространство имен, он должен объявлять пространство имен перед всем остальным кодом, за исключением ключевого слова declare, что означает, что перед пространством имен не может быть объявлен никакой другой код, кроме declare. Кроме того, пространства имен не имеют файловых ограничений, вы можете объявить одно и то же пространство имен в нескольких файлах или вы можете объявить несколько пространств имен в одном и том же файле.
namespace com\aa;
const MY_CONST = 1234;
function my_func(){ /* ... */ }
class my_class { /* ... */ }
Кроме того, классы, функции и константы также могут быть инкапсулированы в пространстве имен через {}:
namespace com\aa{
const MY_CONST = 1234;
function my_func(){ /* ... */ }
class my_class { /* ... */ }
}
Однако эти два определения нельзя смешивать в одном файле, и следующие определения будут недопустимыми:
namespace com\aa{
/* ... */
}
namespace com\bb;
/* ... */
Если пространство имен не определено, все классы, функции и константы определяются в глобальном пространстве, как и до того, как PHP представил концепцию пространств имен.
8.2.2 Внутренняя реализация
Реализация пространства имен на самом деле относительно проста.При объявлении пространства имен имя класса, имя функции и имя константы будут единообразно добавляться к имени пространства имен в качестве префикса при компиляции классов, функций и констант. , функции и константы в пространстве имен модифицируются, чтобы они ничем не отличались от обычного метода определения, но этот префикс автоматически добавляется ядром за нас, например:
//ns_define.php
namespace com\aa;
const MY_CONST = 1234;
function my_func(){ /* ... */ }
class my_class { /* ... */ }
Наконец, фактические имена хранилищ MY_CONST, my_func и my_class в EG(zend_constants), EG(function_table) и EG(class_table) изменены на: com\aa\MY_CONST, com\aa\my_func, com\aa\my_class .
Давайте подробно рассмотрим процесс компиляции.Синтаксис пространства имен компилируется в узел синтаксического дерева типа ZEND_AST_NAMESPACE, который имеет два дочерних узла: child[0] — имя пространства имен, а child[1] — оператор завернутый, если он определен {}.
Функция компиляции для этого узла — zend_compile_namespace():
void zend_compile_namespace(zend_ast *ast)
{
zend_ast *name_ast = ast->child[0];
zend_ast *stmt_ast = ast->child[1];
zend_string *name;
zend_bool with_bracket = stmt_ast != NULL;
//检查声明方式,不允许{}与非{}混用
...
if (FC(current_namespace)) {
zend_string_release(FC(current_namespace));
}
if (name_ast) {
name = zend_ast_get_str(name_ast);
if (ZEND_FETCH_CLASS_DEFAULT != zend_get_class_fetch_type(name)) {
zend_error_noreturn(E_COMPILE_ERROR, "Cannot use '%s' as namespace name", ZSTR_VAL(name));
}
//将命名空间名称保存到FC(current_namespace)
FC(current_namespace) = zend_string_copy(name);
} else {
FC(current_namespace) = NULL;
}
//重置use导入的命名空间符号表
zend_reset_import_tables();
...
if (stmt_ast) {
//如果是通过namespace xxx { ... }这种方式声明的则直接编译{}中的语句
zend_compile_top_stmt(stmt_ast);
zend_end_namespace();
}
}
Как видно из приведенного выше процесса компиляции, процесс компиляции определения пространства имен очень прост. Наиболее важной операцией является установка FC(current_namespace) в качестве имени текущего определенного пространства имен. Макрос FC(): CG(file_context ), предыдущий Как уже упоминалось, file_context — это структура, используемая во время компиляции:
typedef struct _zend_file_context {
zend_declarables declarables;
znode implementing_class;
//当前所属namespace
zend_string *current_namespace;
//是否在namespace中
zend_bool in_namespace;
//当前namespace是否为{}定义
zend_bool has_bracketed_namespaces;
//下面这三个值在后面介绍use时再说明,这里忽略即可
HashTable *imports;
HashTable *imports_function;
HashTable *imports_const;
} zend_file_context;
После компиляции оператора объявления пространства имен скомпилируйте следующий оператор. Классы, функции и константы, определенные после этого, принадлежат этому пространству имен, пока не встретится следующее определение пространства имен. Затем продолжайте анализировать различия в процессе компиляции этих трех типов. место.
(1) Компилировать классы и функции
В предыдущих главах подробно описан процесс компиляции функций и классов. Резюме разделено на два этапа: первый шаг — компиляция функций и классов. Этот процесс генерирует код операции ZEND_DECLARE_FUNCTION и ZEND_DECLARE_CLASS соответственно; второй шаг — скомпилировать весь скрипт Последнее выполнение zend_do_early_binding(), этот шаг эквивалентен выполнению ZEND_DECLARE_FUNCTION, ZEND_DECLARE_CLASS, функции и классы регистрируются в EG (function_table), EG (class_table) на этом шаге.
Когда сгенерированы два кода операции ZEND_DECLARE_FUNCTION и ZEND_DECLARE_CLASS, место хранения имени функции и имени класса будет записано через операнд, а затем имя функции и имя класса будут получены непосредственно на этапе zend_do_early_binding() и зарегистрированы как ключ в EG(function_table), EG(class_table) ), изменение имени функций и классов, определенных в пространстве имен, завершается, когда генерируются ZEND_DECLARE_FUNCTION и ZEND_DECLARE_CLASS. Давайте возьмем функцию в качестве примера, чтобы увидеть конкретную обработку:
//函数的编译方法
void zend_compile_func_decl(znode *result, zend_ast *ast)
{
...
//生成函数声明的opcode:ZEND_DECLARE_FUNCTION
zend_begin_func_decl(result, op_array, decl);
//编译参数、函数体
...
}
static void zend_begin_func_decl(znode *result, zend_op_array *op_array, zend_ast_decl *decl)
{
...
//获取函数名称
op_array->function_name = name = zend_prefix_with_ns(unqualified_name);
lcname = zend_string_tolower(name);
if (FC(imports_function)) {
//如果通过use导入了其他命名空间则检查函数名称是否已存在
}
....
//生成一条opcode:ZEND_DECLARE_FUNCTION
opline = get_next_op(CG(active_op_array));
opline->opcode = ZEND_DECLARE_FUNCTION;
//函数名的存储位置记录在op2中
opline->op2_type = IS_CONST;
LITERAL_STR(opline->op2, zend_string_copy(lcname));
...
}
Имя функции получается методом zend_prefix_with_ns():
zend_string *zend_prefix_with_ns(zend_string *name) {
if (FC(current_namespace)) {
//如果当前是在namespace下则拼上namespace名称作为前缀
zend_string *ns = FC(current_namespace);
return zend_concat_names(ZSTR_VAL(ns), ZSTR_LEN(ns), ZSTR_VAL(name), ZSTR_LEN(name));
} else {
return zend_string_copy(name);
}
}
В методе zend_prefix_with_ns(), если обнаруживается, что FC(current_namespace) не является пустым, к имени функции добавляется префикс FC(current_namespace), а затем измененное имя функции используется в качестве ключа при регистрации в EG(function_table). Способ обработки такой же, как у функции, и здесь повторяться не буду.
(2) Константы компиляции
Процесс компиляции констант в основном такой же, как у функций и классов.Также проверяется пусто ли FC(current_namespace) при получении имени константы в процессе компиляции.Если оно не пусто, значит константа объявлено в пространстве имен, а имя константы имеет префикс FC (current_namespace).
Обобщите определение пространства имен: если вы обнаружите, что пространство имен определено во время компиляции, сохраните имя пространства имен в FC (current_namespace).При компиляции классов, функций и констант сначала оцените, пусто ли FC (current_namespace). , нажмите "Скомпилировать с обычным именем". Если оно не пустое, добавьте к имени класса, имени функции и имени константы префикс FC (current_namespace), а затем зарегистрируйте его с измененным именем. Весь процесс эквивалентен PHP, помогающему нам заполнить имя класса, имя функции и имя константы.
8.3 Использование пространств имен
8.3.1 Основное использование
В предыдущем разделе мы знаем, что классы, функции и константы, определенные в пространстве имен, просто имеют префикс с именем пространства имен.Если это так, можно ли добавлять тот же префикс при его использовании? Ответ положительный, как в приведенном выше примере: константа MY_CONST определена в пространстве имен com\aa, тогда ее можно использовать следующим образом:
include 'ns_define.php';
echo \com\aa\MY_CONST;
Этот способ использования фактического имени класса, имени функции и имени константы прост для понимания и ничем не отличается от обычных типов.Такое имя, используемое в начале «», называется: полное имя, аналогичное концепция абсолютного каталога. Используя это имя, PHP будет напрямую искать соответствующую таблицу символов в соответствии с именем после "" (пространство имен определяется без "" перед ним, поэтому этот символ также будет удален при поиске).
Помимо этой формы имени, существуют еще две формы имени:
- Неполное имя:То есть обычное имя без какого-либо префикса пространства имен, например, my_func(), при использовании этого имени, если в настоящее время существует пространство имен, оно будет проанализировано как: currentnamespace\my_func, если в настоящее время пространства имен нет, оно будет проанализировано по оригинальному названию my_func
- Частично уточненное имя:То есть оно содержит префикс пространства имен, но не начинается с "", например: aa\my_func(), аналогично концепции относительного пути, это правило разрешения имен более сложное, если текущее пространство не использует использование для импорта любого пространства имен, то же, что и неполное имя. Правила разбора те же, то есть, если в данный момент есть пространство имен, оно будет разобрано как: currentnamespace\aa\my_func, иначе оно будет разобрано как aa\my_func , а вариант использования будет объяснен позже
8.3.2 использовать импорт
Хотя доступ к классам, функциям и константам в пространстве имен можно получить в виде полных имен, этот метод требует добавления полного имени пространства имен в каждое место, где оно используется. очень болезненно менять все используемые места.По этой причине PHP предоставляет механизм импорта/псевдонима пространства имен.Вы можете импортировать пространство имен или определить псевдоним с помощью ключевого слова use, а затем в При использовании к нему можно получить доступ через последнее поле или псевдоним имени импортированного пространства имен без использования полного имени, например:
//ns_define.php
namespace aa\bb\cc\dd;
const MY_CONST = 1234;
Его можно использовать следующими способами:
//方式1:
include 'ns_define.php';
use aa\bb\cc\dd;
echo dd\MY_CONST;
//方式2:
include 'ns_define.php';
use aa\bb\cc;
echo cc\dd\MY_CONST;
//方式3:
include 'ns_define.php';
use aa\bb\cc\dd as DD;
echo DD\MY_CONST;
//方式4:
include 'ns_define.php';
use aa\bb\cc as CC;
echo CC\dd\MY_CONST;
Принцип реализации этого механизма также относительно прост: если во время компиляции будет найден оператор использования, имя пространства имен после использования будет вставлено в хэш-таблицу: FC (импорт), а ключ хэш-таблицы — это определенный псевдоним. . Если псевдоним не определен, ключ использует последний раздел, разделенный "". Например, в случае режима 2 в качестве ключа будет использоваться cc, то есть: FC(imports)["cc"] = " aa\bb\cc\dd"; При использовании классов, функций и констант имя будет разделено на "", а затем использовать первый раздел в качестве ключа для поиска FC (импорт), если найдено, имя сохраняется в FC (импорт) и имя при использовании склеиваются в Вместе, образуют полное имя. По сути, этот механизм состоит в том, чтобы вырезать и сокращать полное имя, а затем кэшировать его, а затем сплайсировать в полное имя при его использовании, то есть ядро помогает нам собрать имя.Для ядра имя, включающее в себя наконец, используется полное пространство имен.
В дополнение к использованию, описанному выше, использование может также импортировать класс.После импорта класса вам не нужно добавлять пространство имен, например:
//ns_define.php
namespace aa\bb\cc\dd;
class my_class { /* ... */ }
include 'ns_define.php';
//导入一个类
use aa\bb\cc\dd\my_class;
//直接使用
$obj = new my_class();
var_dump($obj);
Принцип реализации этих двух вариантов использования одинаков, то есть завершение имени реализуется путем поиска FC (импорт) во время компиляции. Начиная с PHP 5.6, использование предоставило два вида импорта для функций и констант, доступ к которым можно получить черезuse function xxx
а такжеuse const xxx
Импорт функции и константы.Принцип реализации этого использования такой же, как описанный выше, но он не сохраняется в FC (импорт) во время компиляции.В данном случае используются две другие хеш-таблицы в структуре zend_file_context. :
typedef struct _zend_file_context {
...
//用于保存导入的类或命名空间
HashTable *imports;
//用于保存导入的函数
HashTable *imports_function;
//用于保存导入的常量
HashTable *imports_const;
} zend_file_context;
Краткий обзор нескольких различных вариантов использования:
- а. Импортируйте пространство имен:Импортированное имя сохраняется в FC (импорт), а таблица символов ищется на завершение при составлении используемого оператора
- Б. Импортируйте класс:Импортируемое имя сохраняется в FC (импорт), в отличие от a, оно не будет получено по последнему разделу после вырезания по "", а будет искаться непосредственно по имени класса
-
C. Функция импорта:пройти через
use function
Импортировать в FC (imports_function), сначала искать FC (imports_function) при завершении, если не найти, продолжать разбираться с ситуацией -
г. Константы импорта:пройти через
use const
Импортировать в FC (imports_const), сначала искать FC (imports_const) при завершении, если не найдено, продолжать разбираться с ситуацией
use aa\bb; //导入namespace
use aa\bb\MY_CLASS; //导入类
use function aa\bb\my_func; //导入函数
use const aa\bb\MY_CONST; //导入常量
Далее давайте посмотрим на конкретную реализацию ядра, сначала посмотрим на компиляцию использования:
void zend_compile_use(zend_ast *ast)
{
zend_string *current_ns = FC(current_namespace);
//use的类型
uint32_t type = ast->attr;
//根据类型获取存储哈希表:FC(imports)、FC(imports_function)、FC(imports_const)
HashTable *current_import = zend_get_import_ht(type);
...
//use可以同时导入多个
for (i = 0; i < list->children; ++i) {
zend_ast *use_ast = list->child[i];
zend_ast *old_name_ast = use_ast->child[0];
zend_ast *new_name_ast = use_ast->child[1];
//old_name为use后的namespace名称,new_name为as定义的别名
zend_string *old_name = zend_ast_get_str(old_name_ast);
zend_string *new_name, *lookup_name;
if (new_name_ast) {
//如果有as别名则直接使用
new_name = zend_string_copy(zend_ast_get_str(new_name_ast));
} else {
const char *unqualified_name;
size_t unqualified_name_len;
if (zend_get_unqualified_name(old_name, &unqualified_name, &unqualified_name_len)) {
//按"\"分割,取最后一节为new_name
new_name = zend_string_init(unqualified_name, unqualified_name_len, 0);
} else {
//名称中没有"\":use aa
new_name = zend_string_copy(old_name);
}
}
//如果是use const则大小写敏感,其它用法都转为小写
if (case_sensitive) {
lookup_name = zend_string_copy(new_name);
} else {
lookup_name = zend_string_tolower(new_name);
}
...
if (current_ns) {
//如果当前是在命名空间中则需要检查名称是否冲突
...
}
//插入FC(imports/imports_function/imports_const),key为lookup_name,value为old_name
if (!zend_hash_add_ptr(current_import, lookup_name, old_name)) {
...
}
}
}
Как видно из процесса компиляции использования, основная обработка во время компиляции заключается в сохранении имени, импортированного при использовании, в соответствующей хеш-таблице с псевдонимом или последним разделом в качестве ключа.Далее давайте посмотрим на использование классов , функции и константы в компиляции Как обрабатывается инструкция. Существует много типов используемых грамматик, таких как использование новых классов, доступ к статическим свойствам, вызов статических методов и т. д., но независимо от того, какой оператор пройдет этап получения имени класса, имени функции и имени константы, завершение имени класса здесь делается за один шаг.
(1) Завершите название класса
При компиляции метод zend_resolve_class_name() используется для завершения имени класса.Если пространства имен нет, возвращается исходное имя класса, например, компиляцияnew my_class()
, сначала передайте "my_class" в функцию, если после поиска FC(imports) будет обнаружено, что это импортированный класс, верните полное имя после завершения, а затем выполните последующую обработку.
zend_string *zend_resolve_class_name(zend_string *name, uint32_t type)
{
char *compound;
//"namespace\xxx\类名"这种用法表示使用当前命名空间
if (type == ZEND_NAME_RELATIVE) {
return zend_prefix_with_ns(name);
}
//完全限定的形式:new \aa\bb\my_class()
if (type == ZEND_NAME_FQ || ZSTR_VAL(name)[0] == '\\') {
if (ZSTR_VAL(name)[0] == '\\') {
name = zend_string_init(ZSTR_VAL(name) + 1, ZSTR_LEN(name) - 1, 0);
} else {
zend_string_addref(name);
}
...
return name;
}
//如果当前脚本有通过use导入namespace
if (FC(imports)) {
compound = memchr(ZSTR_VAL(name), '\\', ZSTR_LEN(name));
if (compound) {
// 1) 没有直接导入一个类的情况,用法a
//名称中包括"\",比如:new aa\bb\my_class()
size_t len = compound - ZSTR_VAL(name);
//根据按"\"分割后的最后一节为key查找FC(imports)
zend_string *import_name =
zend_hash_find_ptr_lc(FC(imports), ZSTR_VAL(name), len);
//如果找到了表示通过use导入了namespace
if (import_name) {
return zend_concat_names(
ZSTR_VAL(import_name), ZSTR_LEN(import_name), ZSTR_VAL(name) + len + 1, ZSTR_LEN(name) - len - 1);
}
} else {
// 2) 通过use导入一个类的情况,用法b
//直接根据原始类名查找
zend_string *import_name
= zend_hash_find_ptr_lc(FC(imports), ZSTR_VAL(name), ZSTR_LEN(name));
if (import_name) {
return zend_string_copy(import_name);
}
}
}
//没有使用use或没命中任何use导入的namespace,按照基本用法处理:如果当前在一个namespace下则解释为currentnamespace\my_class
return zend_prefix_with_ns(name);
}
В дополнение к имени класса, этот метод также имеет параметр типа.Этот параметр анализируется, и синтаксис определяется в соответствии с использованием.Есть три типа:
- ZEND_NAME_NOT_FQ:Неполное имя, то есть обычное имя класса без пространства имен, например: new my_class()
- ZEND_NAME_RELATIVE:Относительное имя принудительно разрешается в соответствии с текущим пространством имен.При использовании добавьте «пространство имен\xx» перед классом, например: новое пространство имен\мой_класс(), если текущее глобальное пространство эквивалентно: новый мой_класс, если текущее пространство имен - это текущее пространство имен, а затем разрешается в "currentnamespace\my_class"
- ZEND_NAME_FQ:Полное имя, т. е. начинающееся с ""
(2) Завершение имен функций и имен констант
Завершение имен функций и констант одинаково:
//补全函数名称
zend_string *zend_resolve_function_name(zend_string *name, uint32_t type, zend_bool *is_fully_qualified)
{
return zend_resolve_non_class_name(
name, type, is_fully_qualified, 0, FC(imports_function));
}
//补全常量名称
zend_string *zend_resolve_const_name(zend_string *name, uint32_t type, zend_bool *is_fully_qualified)
return zend_resolve_non_class_name(
name, type, is_fully_qualified, 1, FC(imports_const));
}
Видно, что функция и константа, наконец, вызывают один и тот же метод для обработки, разница в том, что соответствующая хеш-таблица хранилища передается:
zend_string *zend_resolve_non_class_name(
zend_string *name, uint32_t type, zend_bool *is_fully_qualified,
zend_bool case_sensitive, HashTable *current_import_sub
) {
char *compound;
*is_fully_qualified = 0;
//完整名称,直接返回,不需要补全
if (ZSTR_VAL(name)[0] == '\\') {
*is_fully_qualified = 1;
return zend_string_init(ZSTR_VAL(name) + 1, ZSTR_LEN(name) - 1, 0);
}
//与类的用法相同
if (type == ZEND_NAME_RELATIVE) {
*is_fully_qualified = 1;
return zend_prefix_with_ns(name);
}
//current_import_sub如果是函数则为FC(imports_function),否则为FC(imports_const)
if (current_import_sub) {
//查找FC(imports_function)或FC(imports_const)
...
}
//查找FC(imports)
compound = memchr(ZSTR_VAL(name), '\\', ZSTR_LEN(name));
...
return zend_prefix_with_ns(name);
}
Видно, что логика завершения функций и констант заключается только в том, чтобы сначала использовать исходное имя для поиска FC (imports_function) или FC (imports_const), а если не найдено, то перейти к FC (imports) для соответствия. Если мы импортируем такую функцию:use function aa\bb\my_func;
, скомпилироватьmy_func()
Он найдет "aa\bb\my_func" в соответствии с "my_func" в FC (imports_function), чтобы использовать полное имя.
8.3.3 Динамическое использование
Использование этих пространств имен, описанных выше, относится ко всем случаям, когда имя имеет тип CONST. Вся обработка завершается в процессе компиляции. PHP — динамический язык. Можем ли мы использовать пространства имен динамически? Например:
$class_name = "\aa\bb\my_class";
$obj = new $class_name;
Если при таком использовании может использоваться только полное имя, то есть в соответствии с фактически сохраненным именем, автоматическое завершение имени не может быть выполнено.