@Otokaze
2018-03-29T08:16:17.000000Z
字数 34143
阅读 620
php
PHP(全称:PHP:Hypertext Preprocessor,即“PHP:超文本预处理器”)是一种开源的通用计算机脚本语言,尤其适用于网络开发并可嵌入HTML中使用。PHP的语法借鉴吸收C语言、Java和Perl等流行计算机语言的特点,易于一般程序员学习。PHP的主要目标是允许网络开发人员快速编写动态页面,但PHP也被用于其他很多领域。
PHP最初是由勒多夫在1995年开始开发的;现在PHP的标准由the PHP Group维护。PHP以PHP License作为许可协议,不过因为这个协议限制了PHP名称的使用,所以和开放源代码许可协议GPL不兼容。
PHP的应用范围相当广泛,尤其是在网页程序的开发上。一般来说PHP大多运行在网页服务器上,通过运行PHP代码来产生用户浏览的网页。PHP可以在多数的服务器和操作系统上运行,而且使用PHP完全是免费的。根据2013年4月的统计数据,PHP已经被安装在超过2亿4400万个网站和210万台服务器上。
PHP 手册 - Manual | PHP 官网
index.php
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>hello, world!</title>
</head>
<body>
<?php
echo '<h1>hello, world!</h1>';
?>
</body>
</html>
PHP 是一种脚本语言,常见的脚本语言有:Shell、Perl、AWK。脚本语言通常都是解释运行的,不存在编译、链接过程。比如 Shell 脚本 test.sh(可执行权限),其实质为纯文本文件,但是我们可以直接通过运行 ./test.sh
命令来执行该脚本,这是脚本语言最方便的地方。PHP 与 Shell、Perl 脚本有很多相似的地方,都可以使用类似的方法直接运行。例如上述 PHP helloworld 文件 index.php
,可以通过 php index.php
或 ./index.php
(可执行权限,首行约定标记)来获取 HTML 输出,这种方式的输出结果和浏览器接收到的结果是一模一样的,因此,你完全可以将 PHP 当作一种 Shell 来使用(当然,PHP 主要用于 WEB 开发)。
PHP 动态文件与普通的静态文件有一个显著区别,当我们用浏览器打开某个静态资源(如 HTML 文档)时,获取到的数据就是静态资源本身,没有经过任何处理。
而当我们用浏览器打开某个 PHP 文件时,获取到的却不是 PHP 文件自身(源码),而是 PHP 文件执行的输出结果!也就是说,在接收该资源前,Web 服务器将 PHP 源码进行了预处理(执行 PHP 文件,获取其输出),这也就是 Hypertext Preprocessor,超文本预处理器名称的意义所在。
PHP 与 Web Server(主要考虑 Apache、Nginx)之间是如何协作的呢?
因为我自己使用的是 nginx(轻量级嘛),因此以下内容均以 nginx 为例。假设浏览器请求的文件为 index.php,浏览器发送 HTTP 请求,nginx 接收 HTTP 请求,发现浏览器请求的不是静态资源,于是将其交给 php-fpm(FastCGI 进程管理器,只管理 php cgi 进程,所谓的 php cgi 进程就是一个普通的 php 解释器进程,用于解释运行 php 脚本),php-fpm 随机选择一个空闲的 worker 进程(也就是 php cgi 程序),将该请求交由此 worker 进程处理,处理完成后,php-fpm 将结果返回给 nginx,最后 nginx 将结果再返回给浏览器,最终,呈现给用户。
那么,shell 脚本(以此为例)可以经过类似的方法,来作为 web 脚本来使用吗?当然可以。但是我们不能使用 php-fpm 了,而是应使用通用的 fastcgi 进程管理器(比如 spawn-fcgi),此外,我们还需要一个通用的 cgi 包装器(比如 fcgiwrap),因为 shell 不支持 cgi 协议,然后就可以将 shell 脚本作为 web 脚本来使用了。
PHP 中存在三种注释语法:#
、//
、/* */
,第一种是 shell 中的注视风格。
PHP 解释器和其他的脚本语言解释器有一个显著区别:PHP 解释器只会处理 PHP 标记中的内容,而 PHP 标记外的内容会原样输出,这使得在任何文档中都很容易的插入 PHP 代码,而不需要将源文档做大变动。PHP 开始标记 <?php
、PHP 结束标记 ?>
。
注意,在以命令行脚本方式启动 php 解释器时,php 解释器还会忽略脚本首行的 unix 脚本约定标记 #!/bin/php
,但 cgi 模式下不会忽略此标记,而是被当作普通文本。
安装 PHP 的前提:已安装并配置好 nginx 服务器,可以处理 http 请求。
ArchLinux:pacman -S php-fpm
,安装 php-fpm(会自动安装 php 解释器)。
修改 /etc/php/php.ini
,将 1 改为 0,即 cgi.fix_pathinfo=0
。
修改 /etc/php-fpm.d/www.conf
,user
、group
、listen.owner
、listen.group
改为 nginx
、listen.mode
改为 0660
。
修改 nginx 站点配置文件,假如为 www.conf,注意 php-fpm 监听 socket 的路径:
server {
listen 80;
server_name www.zfl.com;
root /usr/share/nginx/html;
index index.php index.html;
location ~* \.php$ {
fastcgi_index index.php;
#fastcgi_pass 127.0.0.1:9000;
fastcgi_pass unix:/run/php-fpm/php-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
}
}
启动 php-fpm 服务,重载 nginx 配置文件:
systemctl start php-fpm
、systemctl reload nginx
测试 php 是否可以工作,新建页面 index.php:
<?php
phpinfo();
?>
打开浏览器,http://www.zfl.com/
,如果配置正常,则:
PHP 解释器只会解释运行位于 PHP 标记内的代码,PHP 开始标记为 <?php
,PHP 结束标记为 ?>
。位于 PHP 标记外的内容只会被原样输出(当然,存在条件判断语句的除外),因为这个特点,在任何文档中插入 PHP 都很容易。
PHP 中存在三种注释语法:
# 单行注释
// 单行注释
/* 多行注释 */
$argc
:命令行参数的数目,最少为 1
$argv
:命令行参数的数组,$argv[0]
为脚本名称
PHP 中用于输出内容的函数/语句:echo
、print
、print_r
、var_dump
。
echo
、print
都不是函数,而是 PHP 语言结构,因此不需要添加圆括号。
echo var1[, var2[, var3, ...]]
:输出一个或多个变量的值,不自动换行print var
:输出一个变量的值,总是返回 1,比 echo 慢,也不会自动换行print_r(var[, returnResult])
:打印变量的值(易读),不自动换行,如果参数 returnResult 为 true,则不打印变量值,而是将要打印的内容返回给调用者var_dump(var1[, var2[, var3, ...]])
:打印一个或多个变量的值(易读&调试)PHP 变量命名规则:必须以 $
开头,后接 PHP 标识符。
PHP 标识符必须以 字母、下划线 开头,后可接 字母、数字、下划线。
PHP 没有变量声明语句,在第一次给变量赋值的时候会被自动创建,如 $x = 5;
。并且我们也不需要声明变量的类型,PHP 会根据变量的值,自动推测实际的数据类型。并且,我们可以在运行过程中给同一个变量赋予不同类型的值,PHP 会自动推测该变量的实际类型,这个和 JS 是一样的。
PHP 有 3 种变量作用域,分别是:
global
关键字声明,即 global $x, $y;
后,才可以在函数内部被访问,否则报错(真特么奇葩,不知道设计者怎么想的!)static $cnt = 0;
,$cnt 变成静态局部变量,会一直存储在内存中,直到程序结束,但是,此变量仅限函数内部使用,在外部无法访问。特别注意,PHP 中没有 块作用域(语句块),这个和 JavaScript 是一样的。
PHP 中的常量类似于 C/C++ 中的宏定义,常量名就是普通的 PHP 标识符(不以 $
开头),常量一旦定义后,就不可再改变了,因此,建议使用大写字符表示常量。已定义的常量可以在脚本的任意位置被访问,而不需要使用 global 声明(函数中访问全局变量需要此关键字),其实 PHP 已经预定义了许多常量,它们大多以 PHP_
开头,我们平时用的 null
、true
、false
(不区分大小写)就是预定义的常量。
定义常量的两种方式:
bool define(string $name, mixed $value[, bool $case_insensitive = false])
const CONST_NAME = value
:只能在顶层作用域中使用,循环、if 判断、函数中不可用
超全局变量是在脚本任何位置都可用的内置变量,这是与全局变量的不同之处。如下:
$GLOBALS
:保存全局变量的数组,键就是变量名(没有 $
)$_SERVER
:保存请求头、路径、脚本位置等信息的数组,由 Web 服务器创建$_GET
:通过 URL 参数传递给当前脚本的变量的数组$_POST
:通过 POST 方法传递给当前脚本的变量的数组$_FILES
:通过 POST 方法上传给当前脚本的文件的数组$_COOKIE
:通过 Cookies 方式传递给当前脚本的变量的数组$_SESSION
:当前脚本可用 SESSION 变量的数组$_REQUEST
:包含 $_GET
、$_POST
、$_COOKIE
的数组$_ENV
:包含外部环境变量(通常是 Shell 环境变量)的数组PHP 向它运行的任何脚本提供了大量的预定义常量。不过很多常量都是由不同的扩展库定义的,只有在加载了这些扩展库时才会出现,或者动态加载后,或者在编译时已经包括进去了。
有八个魔术常量它们的值随着它们在代码中的位置改变而改变。例如 __LINE__
的值就依赖于它在脚本中所处的行来决定。这些特殊的常量不区分大小写,如下:
__LINE__
:当前所在的行号__FILE__
:脚本的绝对路径__DIR__
:脚本所在的目录__FUNCTION__
:当前所在的函数__CLASS__
:当前所在的类名__METHOD__
:当前所在的方法__NAMESPACE__
:当前所在的命名空间Boolean
布尔、Integer
整型、Float
浮点型、String
字符串
Array
数组(散列表、关联数组)、Object
对象、NULL
空指针/引用
布尔:字面量 true
、false
(不区分大小写)
可使用 (bool)
、(boolean)
进行强制类型转换
除 false
、0
、0.0
、""
、"0"
、null
、空数组外,其它均为 true
整型:二进制(0b
)、八进制(0
)、十进制、十六进制(0x
)
整型的长度是平台相关的,常量 PHP_INT_SIZE
表示整型的长度
常量 PHP_INT_MIN
、PHP_INT_MAX
分别表示整型的最小值和最大值
如果给定的数字超过了 Integer 类型的范围,会被转换为 Float 浮点型
使用 (int)
、(integer)
可以将其它类型的值强制转换为 integer 整型
true
为 1,false
为 0,浮点数转换为整数会直接丢弃小数部分,即向下取整
字符串也可以转换为整数、浮点数,并且,字符串中允许存在任意的前导空白符
对于整型 a、b,进行 a / b
运算,如果可以整除,则返回整型,否则返回浮点型
浮点型:十进制,浮点数,可使用科学记数法,如 1300 用科学记数法表示为 1.3 * 10^3
,在 PHP 中则表示为 1.3e3
或 1.3E3
。对于 aEn
,a 的取值范围 1 <= |a| < 10
,n 是一个整数(可以是正数、负数)。同样的,使用 (float)
语法可将其他类型转换为浮点类型
字符串:一个字符串可以用四种方式表达:单引号、双引号、heredoc、nowdoc。所有方式都可以跨行,即可以键入多行字符串,包括 单引号、双引号 方式。
定义一个字符串最简单的方法就是使用 单引号 将字符串内容包围起来(字符 '
)。要表达单引号本身,请使用 \'
,要表达反斜杠本身,请使用 \\
,其余任何转义序列都是无效的,都会被原样输出。不像 双引号 和 heredoc 结构,单引号字符串中的 变量引用、特殊字符转移序列 均不生效,都会被原样输出。
如果使用 双引号 存储字符串,PHP 会对以下特殊字符进行转义:
转移序列 | 具体含义 |
---|---|
\r |
回车符 |
\n |
换行符 |
\t |
水平制表 |
\v |
垂直制表 |
\f |
换页符 |
\e |
ESC 符 |
\\ |
反斜杠 |
\$ |
美元符 |
\" |
双引号 |
\[0-7]{1,3} |
(regex) 八进制数值表示的字符 |
\x[0-9A-Fa-f]{1,2} |
(regex) 十六进制数值表示的字符 |
\u{unicode-code-point} |
Unicode 码点表示的字符(PHP7) |
和单引号一样,未在上表的其他任何转移序列都会导致反斜杠被显示出来。除此之外,双引号和 heredoc 结构还有一个重要特征,其中的变量会被解析。变量解析有两种语法规则:简单规则、复杂规则(不是指语法复杂,而是它可以使用复杂的表达式)。
简单规则是最常用也是最方便的,它可以用最少的代码在一个字符串中嵌入一个 变量、数组的元素、对象的属性。当 PHP 解释器遇到字符串中的 $
时,会组合尽量多的标识以形成一个合法的变量名,如需确定边界,建议使用花括号包住变量名(和 Shell 一样)。比如 "$ABC"
,我只是想引用 $A
变量,可是 PHP 解释器却误以为我要引用 $ABC
变量,但是使用 "${A}BC"
就没有这个问题了,因为我们指明了边界。
引用 关联数组的元素,"$arr[0]"
、"$arr[key]"
(注意不需要引号);
引用 对象的属性,"$obj->prop"
(注意,箭头符号两边不允许空格符)。
复杂语法不是因为其语法复杂而得名,而是因为它可以使用复杂的表达式。只需在 PHP 表达式两边添加花括号,不过要注意的是,左花括号必须与 PHP 表达式的 $
紧密相联,中间不允许任何空白,否则 PHP 表达式仅被当作普通字符串。与简单规则相似,复杂规则中允许 3 种形式:变量、数组元素、对象属性(语法不需要改变)。
单引号 -> nowdoc、双引号 -> heredoc,它们之间的区别也于单双引号的区别类似。nowdoc、heredoc 都有着类似的语法,heredoc 以 <<<EOF
或 <<<"EOF"
开头(EOF 可为其它任意合法的标识符,下同),nowdoc 以 <<<'EOF'
开头,它们都以 EOF;
结尾(必须顶格写,前后都不允许任何空白,否则会被当作字符串内容)。
#!/bin/php
<?php
$str = <<<EOF
www.zfl9.com
www.baidu.com
www.google.com
EOF;
echo $str.PHP_EOL;
?>
$ ./main.php
www.zfl9.com
www.baidu.com
www.google.com
PHP 字符串在实现方式上,类似于 字节数组,因此,你可以直接使用下标运算符 [0]
来获取字符串的第 0 个字符,注意是可读可写的哦。如果往超出字符串长度的位置写入,则中间的字符全部被空格填充。不过用此方法修改多字节字符集很不安全,仅应对单字节编码,例如 ASCII、ISO-8859-1 的字符串进行此操作。
使用 .
运算符进行字符串拼接(注意不是 +
),使用 (string)
可将其它类型的数据转换为字符串类型,但通常这是没有必要的,因为大多数时候 PHP 会自动进行转换。
PHP 字符串使用的字符编码是没有硬性规定的,默认情况下,字符串会被按照与当前脚本文件相同的字符编码来存储。为了不必要的乱码麻烦,强烈建议使用 UTF-8 编码保存 PHP 文件,并且在操作 PHP 字符串时,也应使用对应的 mb_xxx
函数。
关联数组:PHP 中的数组其实就是 Java 中的哈希表(LinkedHashMap),也和 JavaScript 中的对象/数组类似。其中 key 为 string 类型,value 可为任意类型。key 如果为非负整数,可以省略引号,PHP 会自动转换为对应的字符串。
array(e0, e1, e2, ..., eN)
(key 为对应的下标)
array(k1 => v1, k2 => v2, k3 => v3)
(自定义 key)
array(e0, e1, k1 => v1, k2 => v2, e2, e3)
(二者混合,e2、e3 下标为 2、3)
从 PHP 5.4 起,可以使用更简单的语法来定义数组:
[]
替代array()
使用 (array)
可将其它类型的数据转换为数组类型
使用 unset(var...)
函数可以删除一个或多个变量、数组的键值对
使用 foreach 遍历数组:
foreach ($arr as $elem)
:按值传递,其中 $elem
在循环体外仍可用
foreach ($arr as &$elem)
:引用传递,其中 $elem
在循环外体仍可用
foreach ($arr as $key => $value)
:按值传递,$key
、$value
在循环体外仍可用
foreach ($arr as $key => &$value)
:引用传递,$key
、$value
在循环体外仍可用
建议在循环结束后,使用 unset()
释放 foreach 定义的引用变量,防止误改数组元素
PHP 中有 按值传递(默认)、引用传递 两种语法,引用传递使用 &
语法。注意,PHP 中的引用与 C/C++ 中的指针不同,你可以将 PHP 的引用看作 Unix 中的 硬链接。如 $a = 10
、$b = &$a
,此时 $a
、$b
是完全等价的,相当于一个文件的两个硬连接,$a
、$b
都指向了同一个地方。使用 unset($a)
或 unset($b)
仅仅相当于删除了一个文件的硬链接,只要该文件还有至少一个硬连接,它就还可以被访问。在 PHP 也是这样,只要还有至少一个变量名指向它,就可以被访问的到。
count($array)
:获取给定数组的元素数目(键值对数目,长度)
sort($array)
:升序排列
rsort($array)
:降序排列
ksort($array)
:(key)升序排列
krsort($array)
:(key)降序排列
asort($array)
:(value)升序排列
arsort($array)
:(value)降序排列
对象:要创建对象,必须先定义类,然后使用 new 关键字创建该类的实例(对象)。
NULL:NULL 本身是空指针的意思,不过,我更喜欢理解为 JS 的 undefined
、null
的合体。因为,一个未初始化的变量的值为 null,没有返回语句或不返回值的函数的执行结果为 null,这些都与 JS 的 undefined 功能对应。
PHP 中如何判断一个变量的实际类型,使用 is_xxx()
方法可判断变量的实际类型:
is_bool(var)
:是否为 boolean 布尔
is_int(var) | is_integer(var)
:是否为 integer 整型
is_float(var) | is_double(var)
:是否为 float/double 浮点型
is_numeric(var)
:是否为 integer、float/double、对应的字符串
is_nan(var)
:是否为 not-a-number 非数字
is_string(var)
:是否为 string 字符串
is_array(var)
:是否为 array 关联数组
is_object(var)
:是否为 object 实例对象
is_null(var)
:是否为 null 空指针/空引用
算术运算符:+
、-
、*
、/
、%
取余、**
乘方、+var
正、-var
负
赋值运算符:=
、+=
、-=
、*=
、/=
、%=
、**=
、.=
字符串拼接
自增/自减运算符:++var
前自增、var++
后自增、--var
前自减、var--
后自减
比较运算符:==
值等、===
全等、!=
值不等、!==
不全等、>
、>=
、<
、<=
逻辑运算符:and
/&&
、or
/||
、xor
异或(两者不同则为真)、!
非(单目)
字符串连接符:.
字符串拼接、.=
字符串拼接并赋值
数组运算符:x + y
并集(key)、==
值等、===
值相等&类型相同&顺序相同、!=
、!==
三元运算符:expr ? expr1 : expr2
,如果 expr 为真则结果为 expr1,否则为 expr2
位运算符:&
按位与、|
按位或、^
按位异或、~
按位非(单目)、>>
右移、<<
左移
类型运算符:instanceof
判断给定对象是否为给定类的实例
if
if (condition)
statement;
// or
if (condtion)
statement;
else
statement;
// or
if (condtion)
statement;
elseif // or else if
statement;
elseif // or else if
statement;
else
statement;
while
while (condition)
statement;
do...while
do
statement;
while (condition);
for
for (expr1; expr2; expr3)
statement;
foreach
// 值传递
foreach ($array as $value)
statement;
// 引用传递
foreach ($array as &$value)
statement;
// 值传递
foreach ($array as $key => $value)
statement;
// 引用传递
foreach ($array as $key => &$value)
statement;
switch
// 比较时使用 ==,可理解为 if 的变种
switch ($testValue) {
case value1:
statement;
break;
case value2:
statement;
break;
...
default:
statement;
}
break
、continue
break
:跳出当前循环,执行循环体后面的代码
continue
:结束此轮循环,直接开始下一轮循环
它们都接收一个可选的数字(正整数)参数,表示跳出多少层循环,默认值为 1,即跳出当前所在的循环(注意是从 1 开始,不是从 0 开始)。
流程控制的替代语法
PHP 提供了一些流程控制的替代语法,包括 if
,while
,for
,foreach
和 switch
。替代语法的基本形式是把左花括号({
)换成冒号(:
),把右花括号(}
)分别换成 endif;
,endwhile;
,endfor;
,endforeach;
以及 endswitch;
。
但是要注意,不能在同一控制块中混合使用两种语法。PHP 提供替代语法的主要目的就是为了方便控制 HTML 文档(以 HTML 为例)的输出。
例子,当用户使用的是 IE 浏览器时,显示 IE,当使用 Chrome 浏览器时,显示 Chrome,其他浏览器则显示其他。使用传统语法:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>hello, world!</title>
</head>
<body>
<h1>hello, world!</h1>
<?php if (strpos($_SERVER['HTTP_USER_AGENT'], 'IE') !== false) { ?>
<h1>你使用的是 IE 浏览器</h1>
<?php } elseif (strpos($_SERVER['HTTP_USER_AGENT'], 'Chrome') !== false) { ?>
<h1>你使用的是 Chrome 浏览器</h1>
<?php } else { ?>
<h1>你使用的是 其他 浏览器</h1>
<?php } ?>
</body>
</html>
很多大括号,容易混淆视线,不易检查错误,改用替代语法:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>hello, world!</title>
</head>
<body>
<h1>hello, world!</h1>
<?php if (strpos($_SERVER['HTTP_USER_AGENT'], 'IE') !== false): ?>
<h1>你使用的是 IE 浏览器</h1>
<?php elseif (strpos($_SERVER['HTTP_USER_AGENT'], 'Chrome') !== false): ?>
<h1>你使用的是 Chrome 浏览器</h1>
<?php else: ?>
<h1>你使用的是 其他 浏览器</h1>
<?php endif; ?>
</body>
</html>
PHP 中的函数语法和函数特性与 JavaScript 大致相同。如,简单的求和函数:
function sum($a, $b) {
return $a + $b;
}
函数名必须是合法的 PHP 标识符(以字母、下划线开头,后可接字母数字下划线)。
函数可以有参数,参数实际就是函数的局部变量,因此它的命名方法和普通变量一致。
函数的传参方式默认是值传递,如需引用传递,使用 &
语法,即 func(&$a, &$b)
。
函数中的 return 语句不是必须的,如果省略或 return;
,则默认为 return null;
。
任何有效的 PHP 代码都有可能出现在函数体中,包括其它函数的定义、类的定义等。
但是,在函数中嵌套的定义函数(匿名函数不算,那其实是对象)、类是不被推荐的,为什么呢?因为当一个函数中存在其他函数定义、类定义时,此时调用函数的具体表现有点像调用 PHP 解释器来运行函数体的代码,文字叙述可能不太清楚,我们还是看代码吧,外部函数 outer、内部函数 inner(以函数举例子,类定义同理):
#!/bin/php
<?php
function outer($msg) {
function inner($msg) {
echo $msg . PHP_EOL;
}
echo $msg . PHP_EOL;
inner($msg);
}
// inner('hello, world!'); Call to undefined function
outer('hello, world!');
echo '--------------'.PHP_EOL;
inner('hello, world!');
?>
因此,这样的函数你只能调用一次,因为再调用一次,就会出现重复定义的错误。
默认情况下,内部函数是不能访问外部函数的局部变量的,除非使用 use 关键字(还得是匿名函数),例子如下:
#!/bin/php
<?php
$a = 10; $b = 20;
$func = function() use($a, $b) {
echo '$a = ' . $a . ', $b = ' . $b . PHP_EOL;
};
$func();
?>
类似函数的引用传参方式,use 语句也可以使用 &
捕获外部变量的引用。
和大多数编程语言一样,函数定义可以在函数调用语句的后面,在 JS 中,这被称为函数提升,说的就是这个意思。
PHP 中的函数名是不区分大小写的,但是通常我们都不会使用与原名不同的函数名。
PHP 不支持函数重载,也不可能取消定义或者重定义已声明的函数。和 C 语言差不多。PHP 函数接收参数也比 JS 更严格,JS 函数对于传入的参数数目非常随意,管它是多是少,都会保存到函数内部的 arguments 类数组对象中。但是在 PHP 中,只允许参数更多的情况(通常情况下,这是没有意义的),如果传入的实参数目比形参数目更少,会产生语法错误。
PHP 函数支持设置默认参数,语法和大多数语言类似,默认参数通常只能使用常量表达式(基本类型、Array、NULL),并且,默认参数必须位于参数列表的后头,否则,默认参数的意义就不存在了。
PHP 函数支持可变参数列表,语法和 JS 相似,也是在变量名前使用 ...
,可变参数实际上也是一个数组,如果需要将数组解开,也是使用 ...
操作符(和 JS 一样),可变参数和默认参数一样,必须位于参数列表的尾部。
PHP 函数支持递归调用,所谓的递归调用,也就是在函数体中调用自身所在的函数,但是要注意递归条件,递归深度应尽量小,更应该避免无限递归。
PHP 函数参数允许类型声明,类型声明也称为类型提示,类型声明允许函数在调用时要求参数为特定类型。如果给出的值类型不对,那么将会产生一个错误: 在 PHP 5 中,这将是一个可恢复的致命错误,而在 PHP 7 中将会抛出一个 TypeError 异常。如果需要传入 null 值,需要为指定了类型的参数设置默认值 null,否则会报错。
可用的类型有(放在参数的前面,类似于 C/C++/Java):
bool
布尔值、int
整型、float
浮点型、string
字符串、array
数组
ClassName/InterfaceName
类/接口的实例、self
当前方法所属的类的实例
自 PHP 5 起完全重写了对象模型以得到更佳性能和更多特性。这是自 PHP 4 以来的最大变化。PHP 5 具有完整的对象模型。
PHP 5 中的新特性包括访问控制,抽象类和 final 类与方法,附加的魔术方法,接口,对象复制和类型约束。
PHP 对待对象的方式与引用和句柄相同,即每个变量都持有对象的引用(需要使用 C/C++ 中的成员运算符 ->
),而不是整个对象的拷贝。
PHP 的面向对象模型大多都是借鉴 Java 的,如抽象类,接口,继承父类,实现接口。
一个简单的 Student 类:
#!/bin/php
<?php
class Student {
private $name, $age, $score;
public function __construct($name = 'Unnamed', $age = 0, $score = 0.0) {
$this->name = $name;
$this->age = $age;
$this->score = $score;
}
public function __destruct() {
echo 'call Student::__destruct()'.PHP_EOL;
}
public function getName() {
return $this->name;
}
public function getAge() {
return $this->age;
}
public function getScore() {
return $this->score;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function setAge($age) {
$this->age = $age;
return $this;
}
public function setScore($score) {
$this->score = $score;
return $this;
}
public function print() {
echo "name: $this->name, age: $this->age, score: $this->score\n";
return $this;
}
}
$stu = new Student('ZhangSan', 15, 125);
$stu->print();
$stu->setName('Lisi')->setAge(16)->setScore(138)->print();
?>
类可以通过 extends
关键字来继承一个父类,PHP 只支持单继承,不支持多继承。
被继承的方法和属性可以通过用同样的名字重新声明被覆盖(重写 Override)。但是如果父类定义方法时使用了 final,则该方法不可被覆盖。可以通过 parent::
来访问被覆盖(名称遮蔽)的方法或属性。
当覆盖方法时,参数必须保持一致否则 PHP 将发出 E_STRICT 级别的错误信息。但构造函数例外,构造函数可在被覆盖时使用不同的参数(构造函数不存在重写一说)。
访问控制修饰符:public
、protected
、private
,用于修饰类成员的访问权限:
public
:公开访问权限,可以在任何地方被访问;
protected
:受保护访问权限,仅限本类及其子类中访问;
private
:私有访问权限,仅限本类中访问。
类成员变量允许设置默认值,默认值只能是常量(标量、Array、NULL),即可以在编译期间求得值的表达式。为了兼容 PHP4,使用 var 声明的成员变量的访问性为 public,但是强烈建议不要再使用 var 了,要使用 public/protected/private。
实例属性 $this->propName
,静态属性 self::$propName
、ClassName::$propName
在类体中可使用 const
定义一个类常量,类常量和全局常量一样,不需要 $
开头。
构造函数:public function __construct($args...)
,进行对象初始化操作。如果子类中定义了构造函数,则不会隐式的调用父类的构造函数,因此,强烈建议在子类构造函数首行执行 parent::__construct($args...)
。如果子类未定义构造函数,则会从父类中继承构造函数(就如同普通成员方法继承一样)。
析构函数:public function __destruct()
,进行对象回收操作(和 C++ 类似)。和构造函数一样,如果子类中定义了析构函数,也不会隐式的调用父类的析构函数,因此,强烈建议在子类析构函数尾行执行 parent::__destruct()
,如果子类中未定义析构函数,则自动继承父类的析构函数。析构函数即使在显式调用 exit 时也会被执行。
static
静态成员:public static $url = "www.zfl9.com"
,引用静态成员使用 ::
域解析符,如访问本类中的静态成员 url,self::$url
。
abstract class Foo { ... }
:抽象类
public abstract function func($args...);
:抽象方法
如果一个类有至少一个抽象方法,那么该类必须声明为抽象类;
但是没有抽象方法的类完全可以声明为抽象类,可以防止被实例化。
继承抽象类的子类必须实现所有抽象方法,否则,它也必须被声明为抽象类。
interface
定义接口(抽象方法+类常量),接口的所有成员必须都是 public 的。
implements
实现接口(一个类可以实现多个接口,逗号隔开),类与接口的关系。
extends
接口间的继承(一个接口可以继承多个接口,逗号隔开),接口间的关系。
匿名类(PHP7),利用匿名类,可以创建一次性的简单对象:
#!/bin/php
<?php
// 一个简单的 Util 类
class Util {
private $logger;
public function getLogger() {
return $this->logger;
}
public function setLogger($logger) {
$this->logger = $logger;
return $this;
}
}
$util = new Util();
// PHP 7 之前
class Logger {
public function log($msg) {
echo $msg;
}
}
$util -> setLogger(new Logger());
// PHP 7 之后
$util -> setLogger(new class() {
public function log($msg) {
echo $msg;
}
});
?>
我们可以传递参数给匿名类的构造器,也可以扩展另一个类,实现一个或多个接口:
<?php
interface IFA {
public function funcA();
}
interface IFB {
public function funcB();
}
class Super {
public function hello() {
echo 'hello, world!'.PHP_EOL;
}
}
$obj = new class(250) extends Super implements IFA, IFB {
private $id;
public function __construct($id = 0) {
$this->id = $id;
}
public function funcA() {
echo '匿名类实现接口 A'.PHP_EOL;
}
public function funcB() {
echo '匿名类实现接口 B'.PHP_EOL;
}
public function show() {
echo "ID = $this->id\n";
}
};
$obj->funcA();
$obj->funcB();
$obj->hello();
$obj->show();
?>
遍历对象的属性:与遍历 Array 一样,使用 foreach
语句,如:
<?php
$obj = new class() {
public $var1 = 'public-var1';
public $var2 = 'public-var2';
public $var3 = 'public-var3';
protected $var4 = 'protected-var4';
private $var5 = 'private-var5';
public function func() {
foreach ($this as $name=>$value) {
echo "$name = $value\n";
}
}
};
foreach ($obj as $name=>$value) {
echo "$name = $value\n";
}
echo '------------------'.PHP_EOL;
$obj->func();
?>
$ ./main.php
var1 = public-var1
var2 = public-var2
var3 = public-var3
------------------
var1 = public-var1
var2 = public-var2
var3 = public-var3
var4 = protected-var4
var5 = private-var5
final 最终类/方法:如果在 class 前加上 final
修饰符,则该类将不能被继承;如果父类的方法加上了 final
修饰符,则子类不能够重写父类的该 final 方法。
再谈 PHP 值传递/引用传递
前面我们说了,PHP 中有两种变量传递方式:值传递(默认)、引用传递(&
符)。
PHP 中主要的数据类型有:布尔、整型、浮点、字符串、数组、对象。除对象外,其它类型的变量都是直接保存对应的值,而对象则是保存对应对象的指针(地址)。这其实和 Java 很相似(除了 Java 的数组是对象外)。
首先,我们要明确一点:PHP 中的默认传参方式是 值传递,包括数组、对象!(这个也和 Java 差不多),但是你要注意,对象变量默认保存的是该对象的地址(指针),因此,很多时候,你会觉得将对象传递给函数就是引用传递,因为我在函数内部能够修改对象的属性,并且外部可以看得见修改的结果。其实不然,这个问题,在 Java 中也有很多人被迷惑,说到底,还是没有理解到本质。
整型、字符串、数组(值传递):
#!/bin/php
<?php
$var = 10;
echo "整型(值传递,修改前):".$var.PHP_EOL;
(function ($var) {
$var = 100;
}) ($var);
echo "整型(值传递,修改后):".$var.PHP_EOL;
$str = "baidu";
echo "字符串(值传递,修改前):".$str.PHP_EOL;
(function ($str) {
$str[0] = 'g';
}) ($str);
echo "字符串(值传递,修改后):".$str.PHP_EOL;
$arr = [1, 2, 3];
echo "数组(值传递,修改前):"; print_r($arr);
(function ($arr) {
$arr[0] = 0;
}) ($arr);
echo "数组(值传递,修改后):"; print_r($arr);
?>
$ ./main.php
整型(值传递,修改前):10
整型(值传递,修改后):10
字符串(值传递,修改前):baidu
字符串(值传递,修改后):baidu
数组(值传递,修改前):Array
(
[0] => 1
[1] => 2
[2] => 3
)
数组(值传递,修改后):Array
(
[0] => 1
[1] => 2
[2] => 3
)
整型、字符串、数组(引用传递):
#!/bin/php
<?php
$var = 10;
echo "整型(值传递,修改前):".$var.PHP_EOL;
(function (&$var) {
$var = 100;
}) ($var);
echo "整型(值传递,修改后):".$var.PHP_EOL;
$str = "baidu";
echo "字符串(值传递,修改前):".$str.PHP_EOL;
(function (&$str) {
$str[0] = 'g';
}) ($str);
echo "字符串(值传递,修改后):".$str.PHP_EOL;
$arr = [1, 2, 3];
echo "数组(值传递,修改前):"; print_r($arr);
(function (&$arr) {
$arr[0] = 0;
}) ($arr);
echo "数组(值传递,修改后):"; print_r($arr);
?>
$ ./main.php
整型(值传递,修改前):10
整型(值传递,修改后):100
字符串(值传递,修改前):baidu
字符串(值传递,修改后):gaidu
数组(值传递,修改前):Array
(
[0] => 1
[1] => 2
[2] => 3
)
数组(值传递,修改后):Array
(
[0] => 0
[1] => 2
[2] => 3
)
对象(值传递):
#!/bin/php
<?php
$obj = new stdClass();
$obj->prop = 10;
echo "对象(值传递,修改前):"; print_r($obj);
(function ($obj) {
$obj->prop = 20;
}) ($obj);
echo "对象(值传递,修改后):"; print_r($obj);
?>
$ ./main.php
对象(值传递,修改前):stdClass Object
(
[prop] => 10
)
对象(值传递,修改后):stdClass Object
(
[prop] => 20
)
这不是能修改吗,即证明对象是引用传递的。如果你这样认为,那么你就大错特错了!这实际上还是值传递,只不过此时传递的“值”有点特殊,它传递的是一个地址,因此,你可以通过这个地址修改对应对象的属性值。但是,你却不能修改此对象的指向:
#!/bin/php
<?php
$obj = new stdClass();
$obj->prop = 10;
echo "对象(值传递,修改前):"; print_r($obj);
(function ($obj) {
$obj->prop = 20;
$obj = null;
}) ($obj);
echo "对象(值传递,修改后):"; print_r($obj);
?>
$ ./main.php
对象(值传递,修改前):stdClass Object
(
[prop] => 10
)
对象(值传递,修改后):stdClass Object
(
[prop] => 20
)
你看,按道理来说,如果真的是引用传递的话,那么是可以修改变量本身的值的呀。
对象(引用传递):
#!/bin/php
<?php
$obj = new stdClass();
$obj->prop = 10;
echo "对象(引用传递,修改前):"; print_r($obj);
(function (&$obj) {
$obj->prop = 20;
}) ($obj);
echo "对象(引用传递,修改后):"; print_r($obj);
?>
$ ./main.php
对象(引用传递,修改前):stdClass Object
(
[prop] => 10
)
对象(引用传递,修改后):stdClass Object
(
[prop] => 20
)
再来验证一下,修改变量本身的值,看看能否成功,值传递情况下是不能修改的:
#!/bin/php
<?php
$obj = new stdClass();
$obj->prop = 10;
echo "对象(引用传递,修改前):"; print_r($obj);
(function (&$obj) {
$obj->prop = 20;
$obj = null;
}) ($obj);
echo "对象(引用传递,修改后):"; print_r($obj);
?>
$ ./main.php
对象(引用传递,修改前):stdClass Object
(
[prop] => 10
)
对象(引用传递,修改后):
PHP 的异常处理和 Java、JavaScript 类似,与异常处理相关的关键字:
throw
:抛出一个 Exception/Error 对象
try
:检测可能出现异常的语句块
catch
:捕获并处理对应的异常的语句(可以有多个)
finally
:无论是否发生异常都会执行的语句
try {
// 可能发生异常的语句
} catch (Exception $e) {
// 处理异常的语句
} finally {
// 资源回收语句
}
PHP 异常的继承体系:顶层接口 Throwable,Throwable 有两个子类:Exception 异常、Error 错误。如果需要创建自定义异常类,只能继承 Exception、Error 类,不能直接实现 Throwable 接口。
$_SERVER
:由 Web 服务器创建的数组,包含了诸如头信息(header)、路径(path)、以及脚本位置(script locations)等信息。不保证每个项目都可用,因服务器而异。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>hello, world!</title>
<style>
table {
border-collapse: collapse;
}
table, th, td {
border: 1px solid black;
}
th, td {
padding: 12px;
text-align: center;
}
</style>
</head>
<body>
<table>
<tr>
<th>desc</th>
<th>name</th>
<th>value</th>
</tr>
<?php
echo <<<EOF
<tr>
<td>当前脚本的名称</td>
<td>PHP_SELF</td>
<td>$_SERVER[PHP_SELF]</td>
</tr>
<tr>
<td>当前脚本的名称</td>
<td>SCRIPT_NAME</td>
<td>$_SERVER[SCRIPT_NAME]</td>
</tr>
<tr>
<td>当前脚本的绝对路径</td>
<td>SCRIPT_FILENAME</td>
<td>$_SERVER[SCRIPT_FILENAME]</td>
</tr>
<tr>
<td>CGI 规范的版本</td>
<td>GATEWAY_INTERFACE</td>
<td>$_SERVER[GATEWAY_INTERFACE]</td>
</tr>
<tr>
<td>服务器的软件信息</td>
<td>SERVER_SOFTWARE</td>
<td>$_SERVER[SERVER_SOFTWARE]</td>
</tr>
<tr>
<td>通信协议及其版本</td>
<td>SERVER_PROTOCOL</td>
<td>$_SERVER[SERVER_PROTOCOL]</td>
</tr>
<tr>
<td>文档根目录</td>
<td>DOCUMENT_ROOT</td>
<td>$_SERVER[DOCUMENT_ROOT]</td>
</tr>
<tr>
<td>服务器的虚拟主机名</td>
<td>SERVER_NAME</td>
<td>$_SERVER[SERVER_NAME]</td>
</tr>
<tr>
<td>服务器的 IP</td>
<td>SERVER_ADDR</td>
<td>$_SERVER[SERVER_ADDR]</td>
</tr>
<tr>
<td>服务器的 Port</td>
<td>SERVER_PORT</td>
<td>$_SERVER[SERVER_PORT]</td>
</tr>
<tr>
<td>客户端的 IP</td>
<td>REMOTE_ADDR</td>
<td>$_SERVER[REMOTE_ADDR]</td>
</tr>
<tr>
<td>客户端的 Port</td>
<td>REMOTE_PORT</td>
<td>$_SERVER[REMOTE_PORT]</td>
</tr>
<tr>
<td>请求的 HTTP 方法</td>
<td>REQUEST_METHOD</td>
<td>$_SERVER[REQUEST_METHOD]</td>
</tr>
<tr>
<td>请求的 URI</td>
<td>REQUEST_URI</td>
<td>$_SERVER[REQUEST_URI]</td>
</tr>
<tr>
<td>查询字符串</td>
<td>QUERY_STRING</td>
<td>$_SERVER[QUERY_STRING]</td>
</tr>
<tr>
<td>请求开始的时间戳(秒)</td>
<td>REQUEST_TIME</td>
<td>$_SERVER[REQUEST_TIME]</td>
</tr>
<tr>
<td>请求开始的时间戳(微秒)</td>
<td>REQUEST_TIME_FLOAT</td>
<td>$_SERVER[REQUEST_TIME_FLOAT]</td>
</tr>
<tr>
<td>是否使用 HTTPS 连接</td>
<td>HTTPS</td>
<td>$_SERVER[HTTPS]</td>
</tr>
<tr>
<td>请求头部字段 - ACCEPT</td>
<td>HTTP_ACCEPT</td>
<td>$_SERVER[HTTP_ACCEPT]</td>
</tr>
<tr>
<td>请求头部字段 - USER_AGENT</td>
<td>HTTP_USER_AGENT</td>
<td>$_SERVER[HTTP_USER_AGENT]</td>
</tr>
<tr>
<td>请求头部字段 - XXX</td>
<td>HTTP_XXX</td>
<td>$_SERVER[HTTP_XXX]</td>
</tr>
<tr>
<td>请求头部字段 - XXX-YYY</td>
<td>HTTP_XXX_YYY</td>
<td>$_SERVER[HTTP_XXX_YYY]</td>
</tr>
EOF;
?>
</table>
</body>
</html>
$_GET
:通过 URL 参数传递给当前脚本的变量的数组。其中 name、value 都是未经 url-encoded 编码的字符,但是,如果需要将 value 作为 HTML 内容,还需使用 htmlspecialchars()
方法转义特殊 HTML 字符实体,htmlspecialchars_decode()
则相反,将 HTML 字符实体转换为正常字符。
$_POST
:当 HTTP POST 请求的 Content-Type 是 application/x-www-form-urlencoded
(表单提交,URL查询参数)或 multipart/form-data
(文件上传,二进制数据)时,会将变量以关联数组形式传入当前脚本。
$_COOKIE
:通过 HTTP Cookies 方式传递给当前脚本的变量的数组。使用 setcookie()
(必须在任何输出之前调用)方法发送 COOKIE 给浏览器,使用 $_COOKIE
变量读取接收到的 COOKIE 信息。setcookie()
函数的原型:
bool setcookie(string $name, // 名
[string $value = ""], // 值
[int $expire = 0], // 过期时间,Unix时间戳(单位秒),
// time() 获取当前 Unix 时间戳,
// 如果为 0,则会话结束时过期
[string $path = ""], // 作用的服务器路径,默认为当前目录
// 如设置成 '/' 则对整个域有效
// 如设置成 '/foo/' 则对 /foo/ 目录
// 及其子目录有效
[string $domain = ""], // 作用的服务器域名,默认当前域名
// 若要对当前域及其所有子域生效,
// 只需设置为二级顶级域名,zfl.com
[bool $secure = false], // 设置该COOKIE是否仅通过 HTTPS
// 传递给客户端,默认为 false
[bool $httponly = false], // 设置成 TRUE,Cookie 仅可通
// 过 HTTP 协议访问。 这意思就
// 是 Cookie 无法通过类似 JS
// 这样的脚本语言访问。 要有效
// 减少 XSS 攻击时的身份窃取
// 行为,可建议用此设置(虽然
// 不是所有浏览器都支持),不过
// 这个说法经常有争议。
)
$_REQUEST
:默认情况下,包含了 $_GET
、$_POST
、$_COOKIE
的数组。
include
:包含其它 PHP 脚本,如果不存在则产生警告信息,脚本会继续执行
require
:包含其它 PHP 脚本,如果不存在则产生致命错误,脚本将终止执行
include 和 require 都不是函数,它们都是语言结构,因此不需要括号,如:
include 'part-1.php';
、require 'core.php';
、require 'framework.php';
通过 HTTP 上传文件的主要方式是 POST 请求。因此,需要配置 nginx、php.ini:
nginx.conf:
client_max_body_size 128m;
请求正文的最大大小(上传大文件时特别注意)
php.ini:
file_uploads = On
:启用文件上传功能,默认启用
upload_tmp_dir = /tmp
:上传文件的临时存储目录,默认为系统临时目录
upload_max_filesize = 120M
:允许上传的最大文件大小,适当小于 post_max_size
post_max_size = 128M
:允许发送的最大 POST 请求体的大小,应等于 nginx 配置值
max_execution_time = 300
:单个 PHP 脚本允许的最长执行时间(单位:秒)
max_input_time = 300
:单个 PHP 脚本用于接收输入的最长时间(单位:秒)
memory_limit = 128M
:单个 PHP 脚本允许占用的内存大小,适当大于 post_max_size
使用 PHP 接收上传的文件(HTML 表单上传),主要涉及一个变量、两个函数:
$_FILES
:保存已上传文件的信息的多维数组
is_uploaded_file($filePath)
:判断文件是否为 POST 方式上传的
move_uploaded_file($srcFile, $dstFile)
:移动(覆盖)通过 POST 方式上传的文件
单文件,<input type="file" name="profile">
$_FILES['profile']['name']
:文件的名称
$_FILES['profile']['type']
:文件的类型(MIME)
$_FILES['profile']['size']
:文件的大小(字节)
$_FILES['profile']['error']
:错误代码(0 表示无错误)
$_FILES['profile']['tmp_name']
:保存的临时文件名(路径)
多文件,<input type="file" name="files[]">
(允许多个此标签,HTML4)
多文件,<input type="file" name="files[]" multiple>
(HTML5,CTRL 多选)
$_FILES['files']['name'][index]
:第 n 个文件的名称(index 从 0 开始,下同)
$_FILES['files']['type'][index]
:第 n 个文件的类型
$_FILES['files']['size'][index]
:第 n 个文件的大小
$_FILES['files']['error'][index]
:第 n 个文件的错误代码
$_FILES['files']['tmp_name'][index]
:第 n 个文件的临时位置
完整的例子:
upload.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>文件上传 - 表单</title>
<style>
#single-file-upload {
float: left;
border: 1px solid green;
margin: 30px;
padding: 20px;
}
#multi-file-upload-html4 {
float: left;
border: 1px solid blue;
margin: 30px;
padding: 20px;
}
#multi-file-upload-html5 {
float: left;
border: 1px solid gray;
margin: 30px;
padding: 20px;
}
input[type="submit"] {
margin-top: 8px;
}
</style>
</head>
<body>
<form id="single-file-upload" action="upload.php?type=single" method="post" enctype="multipart/form-data">
<input type="file" name="file"><br>
<input type="submit" value="点击上传(单文件)">
</form>
<form id="multi-file-upload-html4" action="upload.php?type=multi" method="post" enctype="multipart/form-data">
<input type="file" name="file[]"><br>
<input type="file" name="file[]"><br>
<input type="file" name="file[]"><br>
<input type="submit" value="点击上传(多文件 HTML4)">
</form>
<form id="multi-file-upload-html5" action="upload.php?type=multi" method="post" enctype="multipart/form-data">
<input type="file" name="file[]" multiple><br>
<input type="submit" value="点击上传(多文件 HTML5)">
</form>
</body>
</html>
upload.php
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>文件上传 - PHP</title>
</head>
<body>
<?php
if (strcmp($_GET['type'], 'single') === 0) {
$tmpPath = $_FILES['file']['tmp_name'];
$dstPath = '/usr/share/nginx/html/upload/'.$_FILES['file']['name'];
if (move_uploaded_file($tmpPath, $dstPath))
echo "<strong style='color: green'>文件上传成功 \"{$_FILES['file']['name']}\"</strong>";
else
echo "<strong style='color: red'>文件上传失败 \"{$_FILES['file']['name']}\"</strong>";
echo "<pre>";
echo "文件上传的调试信息:\n";
print_r($_FILES);
echo "</pre>";
} elseif (strcmp($_GET['type'], 'multi') === 0) {
for ($i = 0, $len = count($_FILES['file']['name']); $i < $len; $i++) {
$tmpPath = $_FILES['file']['tmp_name'][$i];
$dstPath = '/usr/share/nginx/html/upload/'.$_FILES['file']['name'][$i];
if (move_uploaded_file($tmpPath, $dstPath))
echo "<strong style='color: green'>文件上传成功 \"{$_FILES['file']['name'][$i]}\"</strong><br>";
else
echo "<strong style='color: red'>文件上传失败 \"{$_FILES['file']['name'][$i]}\"</strong><br>";
}
echo "<pre>";
echo "文件上传的调试信息:\n";
print_r($_FILES);
echo "</pre>";
} else {
echo "<strong style='color: red'>请求参数 <code>type</code> 不正确!</strong>";
}
?>
</body>
</html>
理解会话控制的概念
理解一个概念就需要理解它的背景及产生的原因,这里引入 WEB 环境及 HTTP 协议。会话控制产生的背景:
阅读过 HTTP 协议相关资料的同学都会知道 HTTP 协议是 WEB 服务器与客户端(浏览器)相互通信的协议,它是一种无状态协议,所谓无状态,指的是不会维护 http 请求数据,http 请求是独立的,不持久的。也就是说 HTTP 协议没有一个内建的机制来维护两个事务之间的状态或者说是关系吧。当一个用户在请求一个页面后再去请求另外一个页面时,HTTP 将无法告诉我们这两个请求是否来自同一个用户。
由此我们就会觉得很奇怪了,平时我们在论坛逛帖子或电商网站购物时,只要我们在这个站点内,不论我们怎么跳转,从一个页面跑到另一个页面,网站总会记得我是谁,比如告诉你购买了哪些东西。这是怎么做到的呢,估计大家猜到了,这就是运用了 HTTP 会话控制。在网站中跟踪一个变量,通过对变量的跟踪,使多个请求事物之间建立联系,根据授权和用户身份显示不同的内容、不同页面。
PHP Session 会话控制:
PHP 的 session 会话是通过唯一的会话 ID 来驱动的,会话 ID 是一个加密的随机数字,由 PHP 生成,在会话的生命周期中都会保存在客户端。我们知道客户端(也就是浏览器)保存数据的地方只有 Cookie,所以 PHP 的会话 ID 一般保存在用户机器的 cookie 中。了解 cookie 后我们知道,浏览器是可以禁用 cookie 的,这样会话就会失效。所以 PHP 会话控制还有一种模式,就是在 URL 中传递会话 ID。如果在浏览网站时我们稍加留心的话,有些 URL 中有一串看起来像随机数字的字符串,那么其实很有可能就是 URL 形式的会话控制。
讲到这里,有些人可能会有疑问了,客户端只是保存一个会话 ID,那么会话控制中保存的会话变量比如你购物时买的物品列表等,它们是存放在哪个地方的呢?很显然,会话变量是在服务器端使用的,那么这些会话变量必定存放在服务器端。默认情况下,会话变量保存在服务器的普通文件中(也可以自己配置使用数据库来保存),会话 ID 的作用就像是一把钥匙,在服务器端保存会话的文件中找到该会话 ID 对应的会话变量,比如购买物品的列表。
那么会话控制的整个过程可能就像这个样子,用户登录或者第一次浏览某个站点的页面时,该站点会生成一个 PHP 的会话 ID 并通过 cookie 发送到客户端(浏览器)。当用户点击该站点的另一个页面时,浏览器开始连接这个 URL。在连接之前,浏览器会先搜索本地保存的 cookie,如果在 cookie 中有任何与正在连接的 URL 相关的 cookie,就将它提交到服务器。而刚好在登陆或第一次连接时,已经产生了一个与该网站 URL 相关的 cookie(保存的会话 ID),所以当用户再次连接这个站点时,站点就可以通过这个会话 ID 识别出用户,从服务器的会话文件中取出与这个会话 ID 相关的会话变量,从而保持事务之间的连续。
接下来我们了解下两个重要的概念:cookie 和 session。
关于 cookie 的维护与生命周期
Cookie 是在服务器端被创建并写回到浏览器的,浏览器读取到响应头中的 Set-Cookie
字段后,就会按照要求保存 Cookie 数据,Set-Cookie 头如下:
Set-Cookie: <name>=<value>; expires=<date>; max-age=<date>; domain=<domain>; path=<path>; httponly; secure
除<name>=<value>
字段外,其它的均为可选的。Set-Cookie 头部可以有多个,含义:
<name>
:除 控制字符、空白符、(
)
<
>
@
,
;
:
\
"
/
[
]
?
=
{
}
外的 ASCII 字符。<value>
:除 控制字符、空白符、双引号、逗号、分号 以及 反斜线 外的任意 ASCII 字符。许多应用会对 cookie 值按照 URL 编码规则进行编码,但是按照 RFC 规范,这不是必须的。不过满足规范中对于 <value>
所允许使用的字符的要求是有用的。expires
:到期时间,如果省略此字段,则表示这是一个 会话期 Cookie,客户端关闭时会话期 Cookie 被移除。如果此时间已过去,则表示,服务端希望客户端移除此 Cookie。max-age
:生存时间(秒数,优先级高),一些老浏览器不支持该字段(IE6、IE7、IE8),对于其他浏览器,如果同时存在 expires
、max-age
字段,则 max-age 优先级高。domain
:作用域名,指定该 Cookie 作用于哪些域名,如果省略,默认为当前域名;如果指定一个具体域名,则表示作用于该域名及其子域名。path
:作用路径,指定该 Cookie 作用于哪些路径,如果省略,默认为当前路径;如果指定一个具体路径,则表示作用于该路径及其子路径。httponly
:只允许通过 HTTP 来访问 Cookie,即 不允许如 JS 脚本访问该 Cookie,以防跨站脚本攻击。secure
:该 Cookie 只允许通过 HTTPS 方式发送到服务器,在 Chrome 52+、Firefox 52+ 后,不允许 http 站点设置此属性。当浏览器请求某个 URL 资源时,会检索本机存储的相关 Cookie 信息,如果有与此 URL 资源相关联的 Cookie,则使用 Cookie 请求头部发送,Cookie 头部如下:
Cookie: <name1>=<value1>[; <name2>=<value2>[; ...]]
在 PHP 中,发送 Cookie 使用函数 setcookie()
(自动进行 URL 编码)、setrawcookie()
(不进行 URL 编码);读取 Cookie 使用全局变量 $_COOKIE[name]
来读取。
关于session的维护与生命周期
Session 是由服务器维持的一个服务端的存储空间,用户在连接服务器时,会由服务器创建生成一个唯一的 sessionID,用该 sessionID 为标识符来存取服务器端的 Session 存储空间,在会话期间,分配给客户端的唯一 sessionID,用来标识当前用户,与其他用户进行区分。通过 SessionID 接受每一次访问的请求,从而识别当前用户,跟踪和保持用户的具体资料,以及 session 变量,可在 session 中存储数字或文字资料。比如 session_name,这些信息都保存在服务器端。当然,session 也可以作为会话信息保存到数据库中,进行 session 持久化。这样可以跟踪用户的登陆次数、在线与否、在线时间等从而维护 HTTP 无状态事物之间的关系。session 的内容存储是键值对的列表,键是字符串类型,值可以是任意类型,如一个对象。
在 session 会话期间,session 会分别保存在客户端和服务器端两个文件,客户端可以是 cookie 方式保存 sessionID(默认的保存方式)或通过 url 字符串形式传递。服务器端一般以文本的形式保存在指定的 session 目录中。在服务器端我们可以通过 session.use_cookies
来控制客户端使用哪一种保存方式。如果定义为 cookie 保存方式,我们可以通过 session.cookie_lifetime
(默认值 0,关闭浏览器就清除)来控制被保存在 client 上的 cookie 的有效期。而如果客户端用 cookie 方式保存的 sessionID,则使用“临时”的 cookie 保存(cookie 的名称为 PHPSESSID,可通过 php.ini session.name
进行更改),用户提交页面时,会将这一 SessionID 提交到服务器端,来存取 session 数据。这一过程,是不用开发人员干预的。
cookie、session 的区别
Cookie:存储在客户端,内容是纯文本,浏览器会自动检索与请求 URL 相关的 Cookie,然后通过 Cookie 请求头部发送到服务器。
Session:存储在服务端,默认存储方式为文本文件,也可以配置为数据库存储;用于存储特定用户的状态信息,通过 SessionID 可检索到此 Session 文件。
这样看来,Cookie 和 Session 是完全不相干的两样东西。不过,有一个细节不知你有没有注意,Session 信息需要一个 SessionID 来检索,那么浏览器和服务器之间是如何传输和保存这个 SessionID 的呢?最常见以及方便的方式就是通过 Cookie 来传递,服务器上通过 setcookie() 来设置与 SessionID 相关的 Cookie,浏览器接收后,随之存储此 Cookie 信息,当再次访问此站点时,该 Cookie 会自动的被发送给服务器,从而在无状态的 HTTP 协议中保持这个 SessionID。因此,Session 出现的地方一般都会有 Cookie 的身影。
不过,这也不是绝对的,你要知道,浏览器时可以禁用 Cookie 的,那么这种情况下,SessionID 是如何传递的呢?答案是,启用备用方案:URL 传参,细心观察会发信息,一些电商网站的地址栏夹杂着一长串无规律的字符,其实这就是 SessionID 了。
开始 Session 会话
在 PHP 中,要使用 session 机制保存会话状态,第一步就是启动 session 会话。
session_start()
来手动开始一个会话。session.auto_start=1
,则请求开始时,会话会自动开始。注意,如果是手动开始一个会话,那么 session_start()
函数必须在脚本头部被调用,因为 PHP 要根据请求头部信息判断是否要发送 Set-Cookie
字段(如用户第一次访问时)。
配置项 session.auto_start=1
其实就是自动的在每个脚本头部添加 session_start()
函数调用语句。因此,我们主要分析 session_start() 函数的处理步骤:
使用 Session 会话
会话开始后,我们可以通过 $_SESSION
全局数组来存取 session 会话信息,如下:
<?php
session_start();
if (isset($_SESSION['cnt']))
echo "你已访问本页面 ".++$_SESSION['cnt']." 次";
else {
$_SESSION['cnt'] = 1;
echo "你已访问本页面 1 次";
}
?>
结束 Session 会话
当 PHP 脚本运行结束后,PHP 会自动的读取 $_SESSION
数组,将其序列化,然后发送给会话保存管理器来进行保存(默认使用文件进行存储,可配置为数据库存储)。
当然,除了在脚本结束时自动保存会话外,还可以在脚本中显式的调用 session_write_close()
来提前结束会话,其实上面的自动保存会话也是隐式调用 session_write_close()
函数(此函数还有一个别名:session_commit()
)。
销毁 Session 会话
如果需要删除 $_SESSION
数组的某一元素,请使用 unset($_SESSION[name])
如果需要删除 $_SESSION
数组的全部元素,请使用 session_unset()
如果需要销毁当前会话的全部数据(文件也被删除),请使用 session_destroy()
使用 session_destroy()
销毁会话时,别忘了删除含有 SessionID 的 Cookie 信息:
setcookie(session_name(), '', time()-3600)
,将过期时间设为过去,就可以删除。
Session 相关函数
bool session_start([array $options = []])
:启动新会话,或重用现有会话bool session_commit()
:session_write_close()
的别名,意为提交会话bool session_write_close()
:手动结束此次会话,将会话数据写入文件bool session_abort()
:丢弃此次的会话数据,恢复上一次的会话数据void session_unset()
:删除 $_SESSION
数组的全部元素,相当于清空会话数据bool session_destroy()
:销毁当前会话的所有数据,别忘了删除 Cookiestring session_name([string $name])
:获取/设置当前会话的 SessionNamestring session_id([string $id])
:获取/设置当前会话的 SessionIDstring session_save_path([string $path])
:读取/设置当前会话的保存路径通过 URL 参数形式传递 SessionID
如果浏览器禁用了 Cookie,那么我们只能启用备用方案:通过 URL 参数形式来传递 SessionID 了。默认情况下,PHP 只允许通过 Cookie 形式传递 SessionID,如果要启用 URL 传递形式,需配置以下项目:
session.use_cookies = 1
:启用 Cookie 方式(首选)session.use_only_cookies = 0
:关闭“仅使用 Cookie 方式”session.use_trans_sid = 1
:启用 URL-param 方式(备用)该配置下,PHP 会首先尝试通过 Cookie 传递 SessionID,如果成功,则不考虑 URL-param 方式;如果失败,则使用 URL-param 方式传递 SessionID(无需开发者干涉,PHP 会自动在每个相对 url 中添加 url-param)。
PCRE 是 PHP 的核心扩展,因此,在 PHP 脚本中,PCRE 正则总是可用的。
PCRE 是 Perl Compatible Regular Expressions(Perl 兼容正则表达式)的缩写。
PHP 的正则模式与 Perl、Java 的很相似,只有少数差异(详见 PHP 的官方文档)。
因为正则模式中很多反斜杠转义,因此,建议始终使用 单引号 来表示正则表达式。
但是单引号中表示反斜杠本身,也是要转义的(单个的不需要,连续的反斜杠需要)。
例如表示一个反斜杠:'/\/'
或 '/\\/'
,表示两个反斜杠:'/\\\\/'
(必须转义)
PHP 正则模式简述
由于 PHP 正则与 Java、Perl 语法很接近,因此这里只提它们的不同之处。
分隔符:正则表达式需要使用分隔符包裹,分隔符可以是除 字母、数字、反斜杠、空白符 的任意 ASCII 可打印字符,常见的分割符有:/
、@
、#
、~
。如果模式中含有分隔符,则需要进行反斜杠转义。分隔符分为开始分隔符,结束分隔符,我们可以在结束分隔符后添加模式修饰符,如 i
表示忽略大小写。
修饰符:
i
:忽略大小写
s
:单行模式
m
:多行模式
u
:UTF-8 模式(Unicode 支持)
原始字符序列:\Q
开头,\E
结尾,它们之间的字符串将无特殊意义。
转义序列
\ddd
:八进制值为 ddd
(建议始终带上前面的 0,如果可以的话)的字符
\xhh
:十六进制值为 hh
的字符,其实 \ddd
、\xhh
只能表示单字节字符
\x{...}
:UTF-8 模式下可用,大括号中的是 UTF-8 码元(十六进制,一至四字节)
零宽断言,位置匹配符:
\b
:单词边界
\B
:非单词边界
^
:输入序列的起始位置,多行模式下还匹配行结束符之后的位置
$
:输入序列的结束位置,多行模式下还匹配行结束符之前的位置
\A
:输入序列的起始位置
\G
:前一匹配处的结束位置
\z
:输入序列的结束位置
\Z
:输入序列的结束位置或者行结束符之前的位置
贪婪量词、懒惰量词、占有量词 的语法与 Java 一样,其中,量词默认都是贪婪的。
原子组:(?>pattern)
非捕获组:(?:pattern)
匿名捕获组:(pattern)
命名捕获组:(?<name>pattern)
引用匿名捕获组:\n
引用命名捕获组:\k<name>
顺序肯定环视:(?=pattern)
顺序否定环视:(?!pattern)
逆序肯定环视:(?<=pattern)
(只支持定长模式)
逆序否定环视:(?<!pattern)
(只支持定长模式)
PCRE 函数
preg_match
,搜索与给定正则的首次匹配
int preg_match(string $regex, string $input[, array &$result[, int $flags = 0[, int $offset = 0]]])
regex
:正则模式input
:输入序列result
:匹配结果,分别为 group0、groupN,以及命名捕获组flags
:匹配标志,可能的值有: PREG_OFFSET_CAPTURE
,附加匹配结果相对于输入序列的偏移量 offset
:从指定偏移位置开始匹配模式,默认是从字符串起始处匹配,单位:字节preg_match_all
,搜索与给定正则的所有匹配
int preg_match_all(string $regex, string $input[, array &$result[, int $flags = PREG_PATTERN_ORDER[, int $offset = 0]]])
regex
:正则模式input
:输入序列result
:匹配结果,具体的存放结构与 flags 相关flags
:匹配标志,可能的值有(按位或 |
组合多个 flag, PREG_PATTERN_ORDER
和 PREG_SET_ORDER
不可同时使用): PREG_PATTERN_ORDER
:模式排序,二维数组,第一维为捕获组,第二维为第几次匹配,此为默认顺序。PREG_SET_ORDER
:匹配排序,二维数组,第一维为第几次匹配,第二维为捕获组,个人比较喜欢这种排序。PREG_OFFSET_CAPTURE
:是否附加偏移量,变成三维数组(意义同 preg_match()
)offset
:从给定偏移位置开始匹配(意义同 preg_match()
)preg_grep
,返回与给定模式匹配的数组元素
array preg_grep(string $regex, array $inputs[, int $flags = 0])
regex
:正则模式inputs
:输入数据,字符串数组flags
:过滤模式,如果 PREG_GREP_INVERT
,则表示取反匹配preg_replace
,执行正则替换
mixed preg_replace(mixed $regex, mixed $replace, mixed $input[, int $limit = -1[, int &$count]])
regex
:模式字符串、模式字符串数组replace
:替换字符串、替换字符串数组,使用 $n
(n 为 0~99) 引用捕获组input
:输入字符串、输入字符串数组limit
:替换次数限制,-1 为无限制count
:实际替换次数,注意是引用传参preg_replace_callback
,执行正则替换,执行回调
mixed preg_replace_callback(mixed $regex, string function(array $matches), mixed $input[, int $limit = -1[, int &$count]])
string function(array $matches)
:回调函数,返回要替换的字符串,返回值中的 $n
无效,其中 matches 为正则模式匹配的数组,分别为 groupN,除此之外,该函数与上一个函数的行为完全一致。preg_split
,执行正则分割
array preg_split(string $regex, string $input[, int $limit = -1[, int $flags = 0]])
regex
:正则模式input
:输入序列limit
:次数限制,-1、0、null 均为不限制,最后的元素将包含剩余的子串flags
:替换标志,可能的值有(用按位或 |
进行组合): PREG_SPLIT_NO_EMPTY
:忽略分割结果为空串的部分PREG_SPLIT_DELIM_CAPTURE
:返回分割模式中的子捕获组PREG_SPLIT_OFFSET_CAPTURE
:返回分割模式中的子捕获组的偏移量preg_quote
,取消模式的特殊意义
string preg_quote(string $input[, string $delimiter = null])
input
:输入字符串(含有特殊的元字符)delimiter
:正则分隔符,默认为 null,如果指定,则模式中的分隔符将被转义