第11章 PHP中的面向对象
- 实例化一个对象并且调用它的方法
php public function hello() {
echo "hello world!\n"; }
}
function test_call() {
$obj = new baby();
$obj->hello();
}
下面我们在扩展中实现以上test_call函数。
zend_class_entry *baby_ce;
ZEND_FUNCTION(test_call)
{
zval *obj;
MAKE_STD_ZVAL(obj);
object_init_ex(obj, baby_ce);
//如果确认此类没有构造函数就不用调用了。
walu_call_user_function(NULL, obj, "__construct", "");
walu_call_user_function(NULL, obj, "hello", "");
zval_ptr_dtor(&obj);
return;
}
ZEND_METHOD(baby, __construct)
{
printf("a new baby!\n");
}
ZEND_METHOD(baby, hello)
{
printf("hello world!!!!!\n");
}
static zend_function_entry baby_method[]={
ZEND_ME(baby, __construct, NULL, ZEND_ACC_PUBLIC|ZEND_ACC_CTOR)
ZEND_ME(baby, hello, NULL, ZEND_ACC_PUBLIC)
{NULL, NULL, NULL}
};
ZEND_MINIT_FUNCTION(test)
{
zend_class_entry ce;
INIT_CLASS_ENTRY(ce, "baby", baby_method);
baby_ce = zend_register_internal_class(&ce TSRMLS_CC);
return SUCCESS;
}
- 读写对象的属性
ZEND_API zval *zend_read_property(zend_class_entry *scope, zval *object, char *name, int name_length, zend_bool silent TSRMLS_DC);
ZEND_API zval *zend_read_property(zend_class_entry *scope, zval *object, const char *name, size_t name_length, zend_bool silent, zval *rv) ;//php7
ZEND_API zval *zend_read_static_property(zend_class_entry *scope, char *name, int name_length, zend_bool silent TSRMLS_DC);
ZEND_API zval *zend_read_static_property(zend_class_entry *scope, const char *name, size_t name_length, zend_bool silent);//php7
silent参数:
0: 如果属性不存在,则抛出一个notice错误。
1: 如果属性不存在,不报错。
如果所查的属性不存在,那么此函数将返回IS_NULL类型的zval
3. 更新对象的属性
ZEND_API void zend_update_property(zend_class_entry *scope, zval *object, char *name, int name_length, zval *value TSRMLS_DC); ZEND_API void zend_update_property(zend_class_entry *scope, zval *object, const char *name, size_t name_length, zval *value);//php7
ZEND_API int zend_update_static_property(zend_class_entry *scope, char *name, int name_length, zval *value TSRMLS_DC); ZEND_API int zend_update_static_property(zend_class_entry *scope, const char *name, size_t name_length, zval *value);//php7
如果对象或者类中没有相关的属性,函数将自动的添加上。
4. 假设我们已经在扩展中定义好下面的类
class baby
{
public $age;
public static $area;
public function __construct($age, $area)
{
$this->age = $age;
self::$area = $area;
var_dump($this->age, self::$area);
}
}
内核写法
PHP_METHOD(baby, __construct)
{
zval *age, *area, rv;
zend_class_entry *ce;
ce = Z_OBJCE_P(getThis());
if( zend_parse_parameters(ZEND_NUM_ARGS(), "zz", &age, &area) == FAILURE )
{
printf("Error\n");
RETURN_NULL();
}
zend_update_property(ce, getThis(), "age", sizeof("age")-1, age);
zend_update_static_property(ce, "area", sizeof("area")-1, area );
age = NULL;
area = NULL;
age = zend_read_property(ce, getThis(), "age", sizeof("age")-1, 0, &rv );
php_var_dump(age, 1);
area = zend_read_static_property(ce, "area", sizeof("area")-1, 0, &rv );
php_var_dump(area, 1);
}
第12章 启动与终止的那点事
- 模块,请求的init方法中, 每个启动或者关闭的方法在return SUCCESS时退出。如果其中任何的函数return FAILURE,PHP为了避免出现严重问题而将请求中止。
- 配置扩展的信息
PHP_MINFO_FUNCTION(sample4) {
php_info_print_table_start();
php_info_print_table_row(2, "Sample4 Module", "enabled");
php_info_print_table_row(2, "version", PHP_SAMPLE4_EXTVER);
php_info_print_table_end();
}
- php_info_html_esc函数是php_escape_html_entities()的一个封装,htmlentites() 函数的底层实现。该函数返回的字符串通过emalloc()创建,并在使用后必须使用 efree()函数释放掉。
char *php_info_html_esc(char *str TSRMLS_DC)
- 输出开/关表格式所需的标签。HTML输出是与CLI输出一样,表现为一个简单的换行
void php_info_print_table_start(void)
void php_info_print_table_end(void)
- 输出表头行。
void php_info_print_table_header(int cols, ...)
void php_info_print_table_colspan_header(int cols, char *header)
第一个函数在可变参数列表中的char *元素外面的每一列都会输出一对th标签,第二个函数会在指定列数外面输出一对th标签。
void php_info_print_table_row(int cols, ...)
void php_info_print_table_row_ex(int cols, char *class, ...)
第一个函数在可变参数列表中的char *元素外面的每一行都会输出一对td标签,第二个函数会在指定列数外面输出一对td标签。当不在HTML中 输出的时候,两个函数将没有任何差别。
void php_info_print_hr(void)
这种函数将在HTML中输出一个br标签,或者一个表示行开始和结束的水平线
我们常用的PHPWRITE()和php_printf()函数可以在在MINFO函数中使用,你应该注意正确的信息输出取决于当前的SAPI判断是用纯文本还是HTML的方式输出 要做到这一点,只需要检查sapi_module结构中的phpinfo_as_text属性
PHP_MINFO_FUNCTION(sample4) {
php_info_print_table_start();
php_info_print_table_row(2, "Sample4 Module", "enabled");
php_info_print_table_row(2, "version", PHP_SAMPLE4_EXTVER);
if (sapi_module.phpinfo_as_text) {
/* No HTML for you */
php_info_print_table_row(2, "By",
"Example Technologies\nhttp://www.example.com");
} else {
/* HTMLified version */
php_printf("<tr>"
"<td class=\"v\">By</td>"
"<td class=\"v\">"
"<a href=\"http://www.example.com\""
" alt=\"Example Technologies\">"
"<img src=\"http://www.example.com/logo.png\" />"
"</a></td></tr>");
php_info_print_table_end();
}
}
- 你可以通过define()函数来定义一个常量。在内核中,我们将会使用REGISTER_*_CONSTANT()的 家族函数来使用常量。
一般会把常量定义在MINIT中
PHP_MINIT_FUNCTION(sample4) {
REGISTER_STRING_CONSTANT("SAMPLE4_VERSION",
PHP_SAMPLE4_EXTVER, CONST_CS | CONST_PERSISTENT);
return SUCCESS;
}
第一个参数是你要定义的这个常量的名字,常量的名称只能为文字,大家可以尝试使用一个char *的变量,这将导致sizeof计算出错误 的字符串长度。
CONST_CS标识是否大小写敏感,一般情况下CONST_CS标识是默认使用的。对于一些特殊的 情况,比如TRUE,FALSE,NULL等等,这个参数将被省略。
在|后的标识位中的标识符说明了该常量的作用域和生命周期
7. 下面列出的4个创建常量常用的函数,有一个共同需要注意的地方,常量名称一定要用文字而不是char *类型的变量
REGISTER_LONG_CONSTANT(char *name, long lval, int flags)
REGISTER_DOUBLE_CONSTANT(char *name, double dval, int flags)
REGISTER_STRING_CONSTANT(char *name, char *value, int flags)
REGISTER_STRINGL_CONSTANT(char *name,char *value, int value_len, int flags)
- 首先,我们需要在扩展的头文件中(默认是php_*.h)中定义所有的全局变量
ZEND_BEGIN_MODULE_GLOBALS(sample4)
unsigned long counter;
ZEND_END_MODULE_GLOBALS(sample4)
用ZEND_BEGIN_MODULE_GLOBALS和ZEND_END_MODULE_GLOBALS宏将定义的全局变量包起来。将上例中的宏展开后,是下面这个样子:
typedef struct _zend_sample4_globals {
unsigned long counter;
} zend_sample4_globals;
如果你还有其他的全局变量需要定义,只需加在两个宏之间就可以了,接下来我们该在simple4.c中声明我们在头文件中定义的这些全局变量了:
ZEND_DECLARE_MODULE_GLOBALS(sample4);
- 我们可以直接通过sample4_globals.counter来获取计数器的值。在线程安全的版本中,另一种方法是声明一个整数:
//非线程安全
zend_sample4_globals sample4_globals;
//线程 安全的
int sample4_globals_id;
#ifdef ZTS
ts_allocate_id(
&sample4_globals_id,
sizeof(zend_sample4_globals),
NULL, NULL);
#endif
根据其定义的信息,将为每个新线程的独立存储空间分配内存块,这种方法需要包裹在#ifdef中,以防止它在没有启动Zend Thread Safety(ZTS)时执行
10. 在非线程的环境中,会将一个zend_sample4_globals结构的副本保存在指定进程中。你可以指定他的默认值,或者在MINIT或者RINIT中分配资源来初始化它。要记得 在对应的MSHUTDOWN或者RSHUTDOWN中及时释放这些资源。
#ifdef ZTS
#include "TSRM.h"
#define SAMPLE4_G(v) TSRMG(sample4_globals_id, zend_sample4_globals*, v)
#else
#define SAMPLE4_G(v) (sample4_globals.v)
#endif
PHP_FUNCTION(sample4_counter) {
RETURN_LONG(++SAMPLE4_G(counter));
}
11, 以下是*G()系列宏
EG() 这个宏可以用来访问符号表,函数,资源信息和常量。
CG() 用来访问核心全局变量。
PG() PHP全局变量。我们知道php.ini会映射一个或者多个PHP全局结构。举几个使用这个宏的例子:PG(register_globals), PG(safe_mode), PG(memory_limit)
FG() 文件全局变量。大多数文件I/O或相关的全局变量的数据流都塞进标准扩展出口结构。
- 在PHP中有一种“特殊”的全局变量,通常我们把它们称作超级全局变量,常见的比如 G E T 、 _GET、 GET、_POST、$_FILE等等,他们会在编译之前就声明,所以在普通的脚本中,可能无法定义其它的超级全局变量,我们来看下session扩展的MINIT函数实现
PHP_MINIT_FUNCTION(session) {
zend_register_auto_global("_SESSION",
sizeof("_SESSION") - 1,
NULL TSRMLS_CC);
return SUCCESS;
}
这里的第二个参数,sizeof("_SESSION") - 1,一定要排除标识字符串结束的\0符
第13章 INI设置
- 定义一个 INI 的设置,让它的默认值为Hello World!,像下面这样:
#include "php_ini.h"
PHP_INI_BEGIN()
PHP_INI_ENTRY("sample4.greeting", "Hello World", PHP_INI_ALL, NULL)
PHP_INI_END()
PHP_INI_ENTRY 这个宏里面设置的前面的两个参数,分别代表着INI设置的名称和它的默认值。第二个参数决定设置是否允许被修改,以及它能被修改的作用域。最后一个参数是一个回调函数,当INI的值被修改时候触发此回调函数。
PHP_INI_PERDIR 指令可以在php.ini、httpd.conf或.htaccess文件中修改
PHP_INI_SYSTEM 指令可以在php.ini 和 httpd.conf 文件中修改
PHP_INI_USER 指令可以在用户脚本中修改
PHP_INI_ALL 指令可以在任何地方修改
- 已经声明了你的INI的设置,你现在准备将它用在你的问候函数之中
PHP_FUNCTION(sample4_hello_world)
{
const char *greeting = INI_STR("sample4.greeting");
php_printf("%s\n", greeting);
}
- 并不是所有的INI值都是基于字符串的;也有其他的一些用于整数、浮点数、或布尔值的宏,例如
long lval = INI_INT("sample4.intval");
double dval = INI_FLT("sample4.fltval");
zend_bool bval = INI_BOOL("sample4.boolval");
- 通常你想知道你当前的INI设置的值;恭喜你,ZEND内核刚好就存在一组这样的宏为你提供查询每种类型的INI的默认值
const char *strval = INI_ORIG_STR("sample4.stringval");
long lval = INI_ORIG_INT("sample4.intval");
double dval = INI_ORIG_FLT("sample4.fltval");
zend_bool bval = INI_ORIG_BOOL("sample4.boolval");
- 访问级别
SYSTEM 设置被放在php.ini中,或者在Apache的http.conf配置文件中的和,
它在apache启动时候生效,被认为是设置的全局变量
PERDIR 一些设置被放在Apache的http.conf的或者块中,或者.htaccess文件之中
USER 一旦脚本开始执行,唯一的改变INI设置的方法就是利用用户方法:ini_set()
- 无论INI设置在什么时候被修改,无论是通过ini_set()方法来修改还是在一个perdir指令执行期间来修改,zend引擎都会通过一个OnModify的回调来检查它
ZEND_INI_MH(php_sample4_modify_greeting)
{
if (new_value_length == 0) {
return FAILURE;
}
return SUCCESS;
}
PHP_INI_BEGIN()
PHP_INI_ENTRY("sample4.greeting", "Hello World",
PHP_INI_ALL, php_sample4_modify_greeting)
PHP_INI_END()
- ZEND引擎有一个统一的宏来输出这些ini内容,它可以被放置在PHP_MINFO_FUNCTION()块中:
PHP_MINFO_FUNCTION(sample4)
{
DISPLAY_INI_ENTRIES();
}
第14章 流的访问
- 有四种不同的路径去打开一个流. 从用户空间角度来看, 这四种不同的类别如下(函数列表只代表示例, 不是完整列表)
<?php
/* fopen包装
* 操作文件/URI方式指定远程文件类资源 */
$fp = fopen($url, $mode);
$data = file_get_contents($url);
file_put_contents($url, $data);
$lines = file($url);
/* 传输
* 基于套接字的顺序I/O */
$fp = fsockopen($host, $port);
$fp = stream_socket_client($uri);
$fp = stream_socket_server($uri, $options);
/* 目录流 */
$dir = opendir($url);
$files = scandir($url);
$obj = dir($url);
/* "特殊"的流 */
$fp = tmpfile();
$fp = popen($cmd);
proc_open($cmd, $pipes);
- 下面是我们实现的fopen()函数
PHP_FUNCTION(sample5_fopen)
{
php_stream *stream;
char *path, *mode;
int path_len, mode_len;
int options = ENFORCE_SAFE_MODE | REPORT_ERRORS;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss",
&path, &path_len, &mode, &mode_len) == FAILURE) {
return;
}
stream = php_stream_open_wrapper(path, mode, options, NULL);
if (!stream) {
RETURN_FALSE;
}
php_stream_to_zval(stream, return_value);
}
php_stream_open_wrapper()的目的应该是完全绕过底层. path指定要读写文件名或URL, 读写行为依赖于mode的值
3. 尽管传输流和fopen包装流是相同的组件组成的, 但它的注册策略和其他的流不同. 从某种程度上来说, 这是因为用户空间对它们的访问方式的不同造成的, 它们需要实现基于套接字的其他因子
4. 从扩展开发者角度来看, 打开传输流的过程是相同的. 下面是对fsockopen()的实现:
PHP_FUNCTION(sample5_fsockopen)
php_stream *stream;
char *host, *transport, *errstr = NULL;
int host_len, transport_len, implicit_tcp = 1, errcode = 0;
long port = 0;
int options = ENFORCE_SAFE_MODE;
int flags = STREAM_XPORT_CLIENT | STREAM_XPORT_CONNECT;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|l",
&host, &host_len, &port) == FAILURE) {
return;
}
if (port) {
int implicit_tcp = 1;
if (strstr(host, "://")) {
/* A protocol was specified,
* no need to fall back on tcp:// */
implicit_tcp = 0;
}
transport_len = spprintf(&transport, 0, "%s%s:%d",
implicit_tcp ? "tcp://" : "", host, port);
} else {
/* When port isn't specified
* we can safely assume that a protocol was
* (e.g. unix:// or udg://) */
transport = host;
transport_len = host_len;
}
stream = php_stream_xport_create(transport, transport_len,
options, flags,
NULL, NULL, NULL, &errstr, &errcode);
if (transport != host) {
efree(transport);
}
if (errstr) {
php_error_docref(NULL TSRMLS_CC, E_WARNING, "[%d] %s",
errcode, errstr);
efree(errstr);
}
if (!stream) {
RETURN_FALSE;
}
php_stream_to_zval(stream, return_value);
}
这个函数的基础构造和前面的fopen示例是一样的. 不同在于host和端口号使用不同的参数指定, 接着为了给出一个传输流URL就必须将它们合并到一起. 在产生了一个有意义的路径后, 将它传递给php_stream_xport_create()函数
php_stream *php_stream_xport_create(char *xport, int xport_len,
int options, int flags,
const char *persistent_id,
struct timeval *timeout,
php_stream_context *context,
char **errstr, int *errcode);
每个参数的含义如下:
xport 基于URI的传输描述符. 对于基于inet的套接字流, 它可以是tcp://127.0.0.1:80, udp://10.0.0.1:53, ssl://169.254.13.24:445等. 此外, UNIX域传输协议unix:///path/to/socket,udg:///path/to/dgramsocket等都是合法的. xport_len指定了xport的长度, 因此xport是二进制安全的.
- fopen包装器支持目录访问, 比如file://和ftp://, 下面是对opendir()的实现:
PHP_FUNCTION(sample5_opendir)
{
php_stream *stream;
char *path;
int path_len, options = ENFORCE_SAFE_MODE | REPORT_ERRORS;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s",
&path, &path_len) == FAILURE) {
return;
}
stream = php_stream_opendir(path, options, NULL);
if (!stream) {
RETURN_FALSE;
}
php_stream_to_zval(stream, return_value);
}
- 还有一些特殊类型的流不能归类到fopen/transport/directory中. 它们中每一个都有自己独有的API:
php_stream *php_stream_fopen_tmpfile(void);
php_stream *php_stream_fopen_temporary_file(const char *dir, const char *pfx, char **opened_path);
创建一个可seek的缓冲区流用于读写. 在关闭时, 这个流使用的所有临时资源, 包括所有的缓冲区(无论是在内存还是磁盘), 都将被释放. 使用这一组API中的后一个函数, 允许临时文件被以特定的格式命名放到指定路径. 这些内部API调用被用户空间的tmpfile()函数隐藏.
7. 以下3个API方法接受已经打开的FILE *资源或文件描述符ID, 使用流API的某种操作包装. fd格式的接口不会搜索匹配你前面看到过的fopen函数打开的资源, 但是它会注册持久化的资源
php_stream *php_stream_fopen_from_fd(int fd, const char *mode, const char *persistent_id);
php_stream *php_stream_fopen_from_file(FILE *file, const char *mode);
php_stream *php_stream_fopen_from_pipe(FILE *file, const char *mode);
- 在你打开一个流之后, 就可以在它上面执行I/O操作了. 使用哪种协议包装API创建了流并不重要, 它们都使用相同的访问API.
- 流的读写可以使用下面的API函数组合完成, 它们多数都是遵循POSIX I/O中对应的API规范的:
int php_stream_getc(php_stream *stream);
从数据流中接收一个字符. 如果流上再没有数据, 则返回EOF.
size_t php_stream_read(php_stream *stream, char *buf, size_t count);
从指定流中读取指定字节的数据. buf必须预分配至少count字节的内存空间. 这个函数将返回从数据流实际读到缓冲区中的数据字节数.
char *php_stream_get_line(php_stream *stream, char *buf, size_t maxlen, size_t *returned_len);
char *php_stream_gets(php_stream *stream, char *buf, size_t maxlen);
这两个函数从stream中读取最多maxlen个字符, 直到碰到换行符或流结束. buf可以是一个指向预分配的至少maxlen字节的内存空间的指针, 也可以是NULL, 当它是NULL时,则会自动的创建一个动态大小的缓冲区, 用从流中实际读出的数据填充, 成功后函数返回指向缓冲区的指针, 失败则返回NULL. 如果returned_len传递了非NULL值, 则在返回时它将被设置为实际从流中读取的字节数.
char *php_stream_get_record(php_stream *stream, size_t maxlen, size_t *returned_len, char *delim, size_t delim_len TSRMLS_DC);
//php7
PHPAPI zend_string *php_stream_get_record(php_stream *stream, size_t maxlen, const char *delim, size_t delim_len);
和php_stream_get_line()类似, 这个函数将读取最多maxlen, 或到达EOF/行结束第一次出现的位置. 但是它也有和php_stream_get_line()的不同指出, 这个函数允许指定任意的停止读取标记.
10. 从php流中读取目录项和上面从普通文件中读取普通数据相同. 这些数据放到了固定大小的dirents块中. 内部的php_stream_dirent结构体如下, 它与POSIX定义的dirent结构体一致:
typedef struct _php_stream_dirent {
char d_name[MAXPATHLEN];
} php_stream_dirent;
实际上你可以直接使用php_stream_read()函数读取数据到这个结构体中:
struct dirent entry;
if (php_stream_read(stream, (char*)&entry, sizeof(entry)) == sizeof(entry)) {
/* 成功从目录流中读取到一项 */
php_printf("File: %s\n", entry.d_name);
}
- 由于从目录流中读取是很常见的操作, php流包装层暴露了一个API, 它将记录大小的检查和类型转换处理封装到了一次调用中:
php_stream_dirent *php_stream_readdir(php_stream *dirstream, php_stream_dirent *entry);
如果成功读取到目录项, 则传入的entry指针将被返回, 否则返回NULL标识错误