图源:
全局变量
在php中,直接在源码文件中而不是函数或者类中定义的变量称作“全局变量”,该变量可以在函数或类外部使用,但无法直接在函数或类内部使用:
<?php
$number = 0;
function use_global(){
echo $number.PHP_EOL;
// Warning: Undefined variable $number in
$number++;
// Warning: Undefined variable $number in
}
use_global();
echo $number.PHP_EOL;
// 0
很明显,这种情况下解释器提示use_global
函数内的$number
变量没有定义,显然是把$number
看做是一个函数范围内的局部变量,而非全局变量。
要使用函数外部的全局变量,也很容易,只要使用global
关键字声明变量:
$number = 0;
function use_global(){
global $number;
echo $number.PHP_EOL;
// 0
$number++;
}
use_global();
echo $number.PHP_EOL;
// 1
除此以外,还可以使用超全局变量$GLOBALS
访问全局变量:
$number = 0;
function use_global(){
$number = &$GLOBALS['number'];
echo $number.PHP_EOL;
// 0
$number++;
}
use_global();
echo $number.PHP_EOL;
// 1
$GLOBALS
和$_POST
等特殊变量是php的预定义变量,并且可以在代码的任意地方直接访问,这些变量被称作“超全局变量”,更多的预定义变量和超全局变量可以阅读官方手册。
局部变量
php中局部变量的概念与其它语言中的类似,都仅能在当前作用域进行访问:
<?php
function use_local(){
$func_var = "func_var";
do{
$loop_var = "loop_var";
}
while(false);
echo $loop_var.PHP_EOL;
// loop_var
}
use_local();
echo $func_var.PHP_EOL;
// Warning: Undefined variable $func_var in ...
可以看到php中“当前作用域”这个概念依然比较宽泛,基本上限定于类或者函数,循环体、判断语句等并不是一个封闭的作用域。
静态变量
很多编程语言(C++、Java等)的局部变量都会在程序栈中申请空间,这意味着随着函数调用的结束,其中包含的局部变量也会被一同释放,自然也无法在后续程序中访问这些变量。
Go语言在这一点上尤为不同,某些通过函数返回或引用传递到外部代码的局部变量将在堆中申请空间,可以在函数结束调用后继续使用,这种现象叫做“变量逃逸”。
php也遵循上边的规律,所以在某些情况下,为了能在后续执行中继续访问函数中定义的变量,我们需要使用静态变量,而非普通的局部变量:
<?php
function use_static(){
static $number;
$number++;
echo $number.PHP_EOL;
}
use_static();
// 1
use_static();
// 2
echo $number.PHP_EOL;
// Warning: Undefined variable $number in ...
这个示例说明了三点:
-
静态变量的确可以在函数调用结束后依然存在,并在下次调用时保留值。
-
static $number
仅在第一次调用函数创建静态变量时生效,后续函数调用时不会生效。 -
在函数内部声明的静态变量依然只能在该函数内访问,作用域和局部变量相同,不过生命周期不再局限于函数的生命周期。
我们可以在递归时利用静态变量来保存递归产生的中间数据,这里用一个检索二叉树的递归函数来进行说明:
<?php
class Node
{
public ?Node $left = null;
public ?Node $right = null;
public string $context = "";
public function __construct(string $context)
{
$this->context = $context;
}
}
$root = new Node("root");
$root->left = new Node("root>left");
$root->right = new Node("root>right");
$root->left->left = new Node("root>left>left");
$root->right->left = new Node("root>right>left");
function treeTraverse(?Node $root): array
{
static $contexts = [];
if ($root === null) {
return [];
}
treeTraverse($root->left);
$contexts[] = $root->context;
treeTraverse($root->right);
return $contexts;
}
$contexts = treeTraverse($root);
var_dump($contexts);
// array(5) {
// [0]=>
// string(14) "root>left>left"
// [1]=>
// string(9) "root>left"
// [2]=>
// string(4) "root"
// [3]=>
// string(15) "root>right>left"
// [4]=>
// string(10) "root>right"
// }
当然这并不是必须的,利用参数和返回值来传递和保存中间变量同样有效。
静态变量有一个局限:不能使用复杂语法来初始化,比如调用另一个函数:
function static_call(){
static $funcVar = 1;
static $funcVar = 2+3;
static $funcVar = sqrt(1.5);
//Fatal error: Constant expression contains invalid operations in ...
}
理论上来说这种限制并无必要,因为php的变量本质实际上是一种万能类型,底层包含了所有可能的类型,所以在申请内存空间时无需在意实际使用中关联何种类型,而是堆变量还是栈变量似乎并不会影响到这点。我猜测之所以会出现这种限制,可能与优化方面有关。
可变变量名
之前说过,php有个奇怪的特性,变量是可以作为其他变量的变量名的:
<?php
$varName = "number";
$$varName = 1;
echo "$number".PHP_EOL;
echo "$$varName".PHP_EOL;
echo "{$$varName}".PHP_EOL;
// 1
// $number
// 1
在上边的示例中,使用变量$varName
作为变量名构建的变量$$varName
其真实的变量名是$varName
的值number
,所以$$varName
与$number
本质上是同一个变量:
$$varName = 2;
echo "$number".PHP_EOL;
// 2
除了可以使用普通变量作为变量名,也可以使用数组元素,但此时可能会产生一些歧义:
$arr = array("number1", "number2", "number3");
$$arr[0] = 1;
// Warning: Array to string conversion in ...
这里我们是想使用$arr[0]
作为新变量的变量名,但显然发生了我们意料之外的事情。所以为了避免歧义出现,需要使用{}
明确变量名:
$arr = array("number1", "number2", "number3");
${$arr[0]} = 1;
echo $number1 . PHP_EOL;
// 1
除了可以用这种方式命名普通变量,还可以访问对象属性:
<?php
class Student{
public $name;
public $age;
public function __construct($name, $age)
{
$this->name = $name;
$this->age = $age;
}
}
$std = new Student("Li Lei", 20);
$attrName = "name";
echo $std->$attrName.PHP_EOL;
// Li Lei
甚至可以使用字符串连接符来构造变量名:
$an1 = "na";
$an2 = "me";
echo $std->{$an1.$an2}.PHP_EOL;
// Li Lei
前边说过的数组元素也是可以的:
$arr = array("name");
echo $std->{$arr[0]}.PHP_EOL;
// Li Lei
外部变量
php是一门与web开发紧密相关的语言,所以很多web service(如Apache)产生的信息都会以外部变量的形式存在于php中。下面以一些典型的外部变量进行说明。
$_post
通过$_POST
变量可以获取到通过http POST方法提交的表单内容:
<html>
<form action="/ch4/post/post.php" method="POST">
Name: <input type="text" name="username"><br />
Email: <input type="text" name="email"><br />
<input type="submit" name="submit" value="Submit me!" />
</form>
</html>
<?php
echo $_POST['username'].PHP_EOL;
echo $_POST['email'].PHP_EOL;
// ICEXMOON icexmoon@qq.com
如果表单元素名称中包含.
或者空格,生成的php变量名称会使用_
替代:
<html>
<form action="/ch4/post2/post.php" method="POST">
Name: <input type="text" name="user.name"><br />
Email: <input type="text" name="email adress"><br />
<input type="submit" name="submit" value="Submit me!" />
</form>
</html>
<?php
var_dump($_POST);
// array(3) { ["user_name"]=> string(11) "username123" ["email_adress"]=> string(10) "email12312" ["submit"]=> string(10) "Submit me!" }
可以使用数组的方式命名表单元素并提交,php也会正确解析为数组:
<html>
<form action="/ch4/post3/post.php" method="POST">
Name: <input type="text" name="person[username]"><br />
Email: <input type="text" name="person[email]"><br />
<input type="submit" name="submit" value="Submit me!" />
</form>
</html>
<?php
var_dump($_POST['person']);
// array(2) { ["username"]=> string(6) "lalala" ["email"]=> string(10) "123@qq.com" }
事实上一些多选的表单元素都是通过这种方式提交的:
<html>
<form action="/ch4/post4/post.php" method="POST">
<input type="checkbox" name="favorite[]" value="music"/>music<br/>
<input type="checkbox" name="favorite[]" value="draw"/>draw<br/>
<input type="checkbox" name="favorite[]" value="coding"/>coding<br/>
<input type="submit" name="submit" value="Submit me!" />
</form>
</html>
<?php
var_dump($_POST['favorite']);
// array(2) { [0]=> string(5) "music" [1]=> string(4) "draw" }
image submit
可以使用图像代替提交按钮来提交表单:
<html>
<form action="/ch4/post5/post.php" method="POST">
<input type="image" name="submit" src="test.jpg"/>
</form>
</html>
<?php
var_dump($_POST);
// array(2) { ["submit_x"]=> string(3) "464" ["submit_y"]=> string(3) "257" }
通过浏览器开发者工具可以发现,点击图片后实际传递的表单数据是:
submit.x=464&submit.y=257
只不过就像之前说的,生成的php变量名会使用_
替代.
。这里的submit_x
与submit_y
实际上是用户点击的图片的坐标。
HTTP Cookies
使用set_cookie
函数可以通过返回报文设定浏览器cookie:
<?php
setcookie("cookie_test", "test_value", time() + 3600);
访问相应页面后使用浏览器工具查看cookie信息就能发现,相应网站下生成了一个名称为cookie_test
,值为test_value
的cookie。
可以使用超全局变量$_COOKIE
访问请求报文传递的cookie信息:
<?php
if (!isset($_COOKIE['count'])) {
$count = 0;
} else {
$count = intval($_COOKIE['count']);
$count += 1;
}
setcookie("count", $count, time() + 3600);
echo "now count: {$count}";
这个示例使用浏览器cookie保存一个计数器,每次访问页面都会在原有计数基础上+1。
如果要保存一个复合数据(比如一个数组)到cookie,可以将数组转化为字符串后存储:
<?php
date_default_timezone_set("Asia/Shanghai");
$counter = array(
'count' => 0,
'time' => time(),
);
if (isset($_COOKIE['counter'])) {
$unResult = unserialize($_COOKIE['counter']);
if ($unResult !== false && is_array($counter)) {
$counter = $unResult;
}
}
$counter['time'] = time();
$counter['count']++;
setcookie('counter', serialize($counter), time() + 3600);
$timeStr = date("Y-m-d H:i:s",$counter['time']);
echo "count:{$counter['count']}<br/>";
echo "time:{$timeStr}<br/>";
除了使用serialize
序列化,也可以使用json_encode
将数组转换为json
字符串,两者本质上没有区别,都是一种对象持久化技术。
除了上边这种一般性做法,还可以使用数组的形式设置cookie
:
<?php
date_default_timezone_set("Asia/Shanghai");
$counter = array(
'count' => 0,
'time' => time(),
);
if (isset($_COOKIE['counter']) && is_array($_COOKIE['counter'])) {
$counter = $_COOKIE['counter'];
}
$counter['time'] = time();
$counter['count']++;
setcookie('counter[count]', $counter['count'], time() + 3600);
setcookie('counter[time]', $counter['time'], time() + 3600);
$timeStr = date("Y-m-d H:i:s", $counter['time']);
echo "count:{$counter['count']}<br/>";
echo "time:{$timeStr}<br/>";
如果使用浏览器工具查看,会发现浏览器端的cookie存储实际上是两个不同的cookie,名称分别是counter[count]
和counter[time]
,不过通过请求报文头传入Web Service后,会被php解析为一个$_COOKIE['counter']
的数组,这给处理结构简单的数组cookie提供了一种额外思路。
变量名中的点
之所以php生成外部变量时,会将其中的.
用_
进行替换,是因为.
在php中并不是合法的变量名:
$varname.ext; /* 非法变量名 */
.
在php中是字符串连接符,所以上边的写法会被解析为$varname
和ext
这个裸常量的字符串连接。
类型检查
对于外部输入的变量,需要进行合法性检查,这包括类型检查和数值检查,就像前边在HTTP Cookies
小节中的示例展示的那样,可能需要使用is_array
或intval()
这样的函数来帮助检查和类型转换,以确保得到的外部变量是我们想要的合法变量。
文章评论