1 PHP 基础核心知识
1.1 PHP 语法与基础机制
1.1.1 PHP 的执行机制与基本语法
PHP(Hypertext Preprocessor)是一种广泛使用的开源通用脚本语言,特别适合于 Web 开发。它的核心特性在于服务器端执行:PHP 代码在服务器上运行,将处理后的纯 HTML 结果发送回客户端浏览器。这意味着用户无法在浏览器中查看到 PHP 的源代码。
PHP 脚本可以无缝嵌入到 HTML 文档的任何位置。标准的 PHP 脚本必须包裹在特定的标签中:
- 起始标签:
<?php - 结束标签:
?>
文件规范:PHP 文件的默认文件扩展名是 .php。一个合法的 PHP 文件通常混合包含了 HTML 标签、CSS、JavaScript 以及 PHP 脚本代码。
指令分隔:在 PHP 中,每一个指令代码行都必须以分号(;)作为结束符。分号是 PHP 引擎区分不同指令集的唯一标准,遗漏分号是初学者最常犯的语法错误。
1.1.2 基础输出指令
通过 PHP 向浏览器输出文本或变量,最基础且最常用的指令有两种:echo 和 print。
echo:可以输出一个或多个字符串,速度略快,没有返回值。print:只允许输出一个字符串,返回值总为 1。
<?php
// 基础输出示例
echo "<h1>Hello World!</h1>";
echo "这是一段使用 echo 输出的普通文本。<br>";
print "<h2>这是使用 print 输出的二级标题</h2>";
print "print 通常用于单一字符串的输出。\n";
?>
1.1.3 PHP 中的注释规范
注释是代码中不可执行的部分,主要用于解释代码逻辑、标记作者信息或临时禁用某段代码。良好的注释习惯是优秀程序员的标志。
<?php
// 1. 这是最常用的 PHP 单行注释(C++ 风格)
$x = 5; // 在代码行尾部也可以添加单行注释
# 2. 这是另一种单行注释(Shell 风格),在现代 PHP 中较少使用
/* 3. 这是 PHP 的多行注释(C 风格)
你可以在这里书写大段的说明文档,
或者用于临时注释掉一整块不需要执行的代码。
甚至可以在多行注释中包含 HTML 结构说明。
*/
$y = 10;
echo $x + $y;
?>
1.2 PHP 变量与作用域
1.2.1 变量的本质与命名规则
变量是用于存储信息的内存“容器”。与代数中的变量(如 )类似,PHP 变量用于保存数据和表达式的结果。
严格的命名规则:
- 标识符前缀:所有变量必须以美元符号
$开始,紧跟变量名称。 - 首字符限制:变量名必须以英文字母或者下划线(
_)字符开始,绝对不能以数字开头。 - 字符集限制:变量名只能包含字母、数字以及下划线(即
A-z、0-9和_)。 - 禁止空格:变量名内部不能包含任何空格。如果需要包含多个单词,建议使用下划线命名法(
$my_car)或驼峰命名法($myCar)。 - 大小写敏感:PHP 的变量名是严格区分大小写的(
$age、$Age和$AGE是三个完全不同的变量)。注:PHP 的内置函数、类名等则不区分大小写。
1.2.2 弱类型语言的自动推导
PHP 是一门**弱类型(动态类型)**语言。这意味着你不需要在声明变量时指明其数据类型。变量在第一次被赋值时自动创建,并且 PHP 引擎会根据所赋的值自动推导出正确的数据类型。
<?php
// 变量声明与自动类型推导
$greeting = "Hello world!"; // 自动推导为 String (字符串),文本值需加引号
$itemCount = 5; // 自动推导为 Integer (整型)
$itemPrice = 10.5; // 自动推导为 Float (浮点型)
$isAvailable = true; // 自动推导为 Boolean (布尔型)
echo "信息:" . $greeting . "<br>";
echo "总价:" . ($itemCount * $itemPrice);
?>
1.2.3 变量作用域深度解析
作用域决定了变量在脚本中可被访问的可见范围。PHP 拥有四种主要的变量作用域:
1.2.3.1 局部作用域与全局作用域
-
全局变量:在所有函数外部定义的变量,拥有全局作用域。但在 PHP 中,函数内部无法直接访问外部的全局变量。
-
局部变量:在函数内部声明的变量,仅能在该函数内部访问。不同函数可以使用同名的局部变量而互不干扰。
如果必须在函数内部修改或读取全局变量,有两种方法:使用 global 关键字,或使用超级全局数组 $GLOBALS。
<?php
// 作用域示例代码
$global_x = 100; // 全局变量
$global_y = 200; // 全局变量
function calculateTotal() {
$local_z = 50; // 局部变量
// 错误示范:直接调用会报错,因为函数内不认识 $global_x
// echo $global_x;
// 正确方法 1:使用 global 关键字引入全局变量
global $global_x;
// 正确方法 2:使用 $GLOBALS 超级全局数组
$GLOBALS['global_y'] = $GLOBALS['global_y'] + $local_z;
echo "<p>函数内部执行:全局 x 的值为: " . $global_x . "</p>";
}
calculateTotal();
echo "<p>函数外部执行:计算后的全局 y 的值为: " . $global_y . "</p>";
// echo $local_z; // 这里会报错,因为 $local_z 是局部变量,外部不可见
?>
1.2.3.2 静态作用域
默认情况下,当函数执行完毕后,其内部的所有局部变量都会被销毁以释放内存。但有时我们需要保留某个局部变量的值供下一次函数调用时使用,这就需要用到 static 关键字。
<?php
function counterTracker() {
// 第一次调用时初始化为 0,后续调用将跳过此行,保留上次的值
static $clickCount = 0;
echo "当前点击次数: " . $clickCount . "<br>";
$clickCount++; // 变量递增,且值被保留
}
counterTracker(); // 输出 0
counterTracker(); // 输出 1
counterTracker(); // 输出 2
?>
1.2.3.3 参数作用域
参数是通过调用代码将值传递给函数的局部变量。它们在函数的参数列表中声明,其作用域等同于该函数的局部变量。
<?php
// $userName 属于参数作用域
function greetUser($userName) {
echo "Welcome back, " . $userName . "!\n";
}
greetUser("Arch");
?>
1.3 PHP 核心数据类型
PHP 支持 8 种原始数据类型,分为标量类型、复合类型和特殊类型。
1.3.1 标量数据类型
- String(字符串):一串字符序列。可以使用单引号或双引号。双引号会解析内部的变量,而单引号不会。
- Integer(整型):没有小数部分的数字。至少包含一个数字,不能包含逗号或空格。支持十进制、十六进制(
0x前缀)和八进制(0前缀)。 - Float(浮点型):带小数部分的数字,或指数形式。
- Boolean(布尔型):逻辑上的真(
TRUE)或假(FALSE),常用于条件控制。
<?php
// 标量类型详解代码
$strSingle = 'Hello $name'; // 单引号原样输出
$strDouble = "Hello World"; // 双引号可解析转义字符
var_dump($strDouble);
echo "<br>";
$intPositive = 5985;
$intNegative = -345;
$intHex = 0x8C; // 140 的十六进制
var_dump($intHex);
echo "<br>";
$floatNormal = 10.365;
$floatScientific = 2.4e3; // 2.4 * 10^3 = 2400
var_dump($floatScientific);
echo "<br>";
$boolTrue = true;
$boolFalse = false;
var_dump($boolTrue);
?>
1.3.2 复合数据类型
- Array(数组):在一个变量中存储多个独立的值。
- Object(对象):面向对象编程的核心。包含属性(变量)和方法(函数)的数据结构。必须首先使用
class关键字声明类,然后使用new实例化。
<?php
// 复合类型示例
// 数组
$frameworks = array("Laravel", "Symfony", "ThinkPHP");
var_dump($frameworks);
echo "<br>";
// 对象
class Server {
public $os;
public $ram;
// 构造函数
public function __construct($os = "Linux", $ram = "16GB") {
$this->os = $os;
$this->ram = $ram;
}
public function getServerInfo() {
return "OS: " . $this->os . " | RAM: " . $this->ram;
}
}
$myNas = new Server("Ubuntu", "32GB");
echo $myNas->getServerInfo();
?>
1.3.3 特殊数据类型
- NULL(空值):表示一个变量没有值。主要用于清空变量、区分变量是否被初始化。
- Resource(资源):保存了到外部资源的一个引用,比如数据库连接(MySQL link)、打开的文件句柄、图形画布区域等。
<?php
$emptyVar = "I have data";
$emptyVar = null; // 数据被清空,释放内存
var_dump($emptyVar);
echo "<br>";
// 资源类型演示
$filePointer = fopen(__FILE__, "r"); // 打开当前文件进行读取
echo "资源类型: " . get_resource_type($filePointer);
fclose($filePointer);
?>
1.3.4 PHP 类型比较的陷阱 (松散与严格)
PHP 的弱类型特性导致了比较时的复杂性:
- 松散比较 (
==):只比较值。如果类型不同,PHP 会尝试进行类型转换后再比较。 - 严格比较 (
===):不仅比较值,还比较数据类型。推荐在生产环境中使用严格比较以避免隐式转换带来的 Bug。
<?php
echo "=== 松散比较与严格比较深度测试 ===\n";
// 整数与字符串
var_dump(42 == "42"); // true (值相等)
var_dump(42 === "42"); // false (一个是 int, 一个是 string)
// 零与布尔/空值的比较矩阵
echo '0 == false: '; var_dump(0 == false); // true (0 转换为 false)
echo '0 === false: '; var_dump(0 === false); // false
echo '0 == null: '; var_dump(0 == null); // true
echo '0 === null: '; var_dump(0 === null); // false
echo 'false == null:';var_dump(false == null);// true
echo 'false === null:';var_dump(false === null);// false
echo '"" == false: '; var_dump("" == false); // true (空字符串转换为 false)
echo '"" === false: ';var_dump("" === false); // false
?>
1.4 PHP 数组操作全解析
数组是 PHP 中最强大的数据结构之一,它允许你在单个变量中存储、管理和操作大量数据。
1.4.1 数组的三种形态
- 数值数组(索引数组):带有数字 ID 键的数组。键名默认从 0 开始自动分配,也可手动干预。
- 关联数组(字典):带有指定字符串键的数组,每一个键映射一个具体的值。
- 多维数组:数组内部的元素也是数组,用于表示复杂的矩阵或层级数据。
<?php
// 1. 数值数组的创建与遍历 (for 循环)
$ports = array(80, 443, 3306, 6379);
$portsLength = count($ports);
echo "正在监听的端口列表:<br>";
for($i = 0; $i < $portsLength; $i++) {
echo "- 端口: " . $ports[$i] . "<br>";
}
// 2. 关联数组的创建与遍历 (foreach 循环)
$serverConfig = array(
"Host" => "192.168.1.100",
"User" => "root",
"Status" => "Running"
);
echo "<br>服务器配置信息:<br>";
foreach($serverConfig as $key => $value) {
echo "配置项 [" . $key . "] = " . $value . "<br>";
}
// 3. 多维数组简例
$ctfTeams = array(
array("TeamA", 1500, "1st"),
array("TeamB", 1200, "2nd")
);
echo "<br>第一名的分数是: " . $ctfTeams[0][1];
?>
1.4.2 数组排序机制
PHP 提供了针对不同需求的丰富排序函数:
- 数值数组排序:
sort()(升序),rsort()(降序)。 - 关联数组按“值”排序:
asort()(保持索引关系的升序),arsort()(降序)。 - 关联数组按“键”排序:
ksort()(按键名升序),krsort()(按键名降序)。
<?php
// 排序演示
$vulnScores = array("XSS" => 6.5, "SQLi" => 9.8, "CSRF" => 4.2);
echo "原始数据: "; var_dump($vulnScores);
arsort($vulnScores); // 根据 CVSS 分数(值)进行降序排列
echo "<br>按危险等级降序: "; var_dump($vulnScores);
ksort($vulnScores); // 根据漏洞名称(键)的首字母升序排列
echo "<br>按字母顺序排列: "; var_dump($vulnScores);
?>
1.4.3 核心 PHP Array 函数速查手册
这是日常开发中最常使用的内置数组处理函数:
| 函数名 | 深度描述与用途 |
|---|---|
array() | 初始化并创建一个新的数组结构。 |
array_column() | 从多维数组中提取出一列所有的值,常用于处理数据库查询返回的二维数组集。 |
array_combine() | 传入两个数组,一个作为键名(Keys),一个作为键值(Values),合并为一个全新的关联数组。 |
array_count_values() | 频率分析神器:统计数组中每一个值出现的总次数,返回以值为键、次数为值的数组。 |
array_diff() | 计算并返回第一个数组与其它数组的差集(找不同,仅比较键值)。 |
array_filter() | 传入一个回调函数,对数组中的每个元素进行逻辑判断过滤,保留返回 TRUE 的元素。 |
array_flip() | 键值对调:将数组中的键名变成键值,原来的键值变成键名。 |
array_intersect() | 计算并返回多个数组的交集(找相同点,仅比较键值)。 |
array_key_exists() | 安全检查:判断指定的键名是否存在于给定的数组中,返回布尔值。 |
array_keys() | 剥离所有的键值,仅返回一个包含原数组所有键名的新数值数组。 |
array_map() | 将用户自定义的回调函数应用到给定数组的每一个值上,返回处理后的新数组(类似数据清洗)。 |
array_merge() | 将一个或多个数组的单元合并起来,一个数组中的值附加在前一个数组的后面。 |
array_pop() | 数据结构中的“出栈”操作:弹出并返回数组的最后一个元素,数组长度减一。 |
array_push() | 数据结构中的“入栈”操作:向数组的末尾压入一个或多个元素。 |
array_shift() | 数据结构中的“出队”操作:移出并返回数组的第一个元素。 |
array_unshift() | 向数组的最开头插入一个或多个元素。 |
array_sum() | 计算数组中所有数值元素的总和。 |
array_unique() | 数据去重:移除数组中所有重复的值,仅保留第一个出现的条目。 |
count() | 获取数组内部元素的总个数(等同于 sizeof())。 |
in_array() | 搜索指定的值是否存在于数组中,常用于白名单校验。 |
1.5 常量与魔术常量
1.5.1 PHP 常量的定义与特性
常量是指一旦在脚本中定义后,其值绝对不能被改变或重新定义的标识符。
- 全局有效:常量不受变量作用域的限制,定义后可以在脚本的任何地方(包括函数内部)直接使用,无需
global关键字。 - 没有
$符号:常量名在使用和定义时都不带美元符号。 - 命名规范:通常使用全大写字母加下划线命名(如
DB_PASSWORD)。
定义常量的两种方式:
define(name, value, case_insensitive)函数:运行时定义。PHP 8.0 开始废弃了大小写不敏感(第三个参数设为 true)的设定。const关键字:编译时定义,执行效率略高。PHP 7+ 开始支持使用const定义数组常量。
<?php
// 1. 使用 define() 定义标量常量
define("SYSTEM_VERSION", "v2.0.1");
// 2. 使用 const 定义数组常量 (PHP 7+)
const DB_CREDENTIALS = [
"HOST" => "127.0.0.1",
"PORT" => 3306,
"USER" => "admin"
];
function checkSystem() {
// 函数内部直接访问常量
echo "当前系统版本: " . SYSTEM_VERSION . "<br>";
echo "数据库连接地址: " . DB_CREDENTIALS['HOST'] . ":" . DB_CREDENTIALS['PORT'];
}
checkSystem();
?>
1.5.2 PHP 魔术常量 (Magic Constants)
PHP 提供了几个特殊的预定义常量,被称为“魔术常量”。它们之所以“魔术”,是因为它们的值会随着代码位置的改变而动态变化。它们在日志记录、框架路由和调试时不可或缺。
| 魔术常量 | 动态返回值描述 |
|---|---|
__LINE__ | 返回该常量在当前文件中所处的绝对行号(常用于错误日志追踪)。 |
__FILE__ | 返回当前执行脚本的完整物理路径和文件名(如 C:\xampp\htdocs\index.php)。 |
__DIR__ | 返回当前文件所在的目录路径(等同于 dirname(__FILE__),常用于包含相对路径的文件)。 |
__FUNCTION__ | 返回当前正在执行的函数的名称。 |
__CLASS__ | 返回当前被定义的类的名称(包含命名空间)。 |
__TRAIT__ | 返回当前正在使用的 Trait 代码复用块的名字。 |
__METHOD__ | 返回当前正在调用的类方法名(格式为 ClassName::MethodName)。 |
__NAMESPACE__ | 返回当前代码所处的命名空间名称。 |
<?php
namespace SecurityTools;
class Logger {
public function recordCrash() {
echo "致命错误发生在文件: " . __FILE__ . "<br>";
echo "代码行号: 第 " . __LINE__ . " 行<br>";
echo "触发的类与方法: " . __METHOD__ . "<br>";
echo "所属命名空间: " . __NAMESPACE__ . "<br>";
}
}
$sysLog = new Logger();
$sysLog->recordCrash();
?>
1.6 字符串变量与函数速查
字符串操作是 Web 后端开发中最频繁的动作。PHP 提供了强大的原生的字符串拼接与解析能力。
1.6.1 字符串基本操作与并置
在 PHP 中,只有一个专用的字符串运算符:并置运算符 (.),它用于将两个或多个字符串拼接在一起。
除了拼接,获取长度和查找位置是两大基础需求:
strlen():获取字符串的字节长度。strpos():在母字符串中检索子串首次出现的位置(索引从 0 开始。如果未找到则返回 FALSE)。
<?php
$protocol = "https://";
$domain = "runoob.com";
$uri = "/php-tutorial";
// 使用 . 拼接 URL
$fullUrl = $protocol . $domain . $uri;
echo "完整链接: " . $fullUrl . "<br>";
echo "该 URL 的总长度为: " . strlen($fullUrl) . " 字节<br>";
// 检查是否包含特定的域名
$searchPos = strpos($fullUrl, "runoob");
if ($searchPos !== false) {
echo "'runoob' 关键词位于索引: " . $searchPos;
}
?>
1.6.2 复杂多行字符串声明 (Heredoc 语法)
除了单引号和双引号,PHP 还提供了一种专门用于定义大段多行文本(尤其是包含 HTML 结构时)的利器:Heredoc (EOF) 语法。
使用 <<< 加上一个标识符(通常习惯用 EOF、EOD 或 HTML)来标记字符串的开始。
Heredoc 的五大核心铁律:
- 开始标记:
<<<EOF后面不能有任何字符(包括空格)。 - 变量解析:位于开始和结束标记之间的 PHP 变量会被正常解析(就像双引号一样),而且不需要使用
.来拼接,直接写在文本里即可。但函数不能直接在里面解析。 - 无需转义:在内容中直接嵌套单引号或双引号时,不需要加反斜杠转义,非常适合写大段的 HTML。
- 结束标记极其严格:结束标记
EOF;必须顶头独立占一行!它的前面不能有缩进、不能有空格,后面除了分号外也不能有任何字符。 - 标识符一致:开始的标识符和结束的标识符必须完全一致。
代码示例:
<?php
$siteName = "Runoob";
// 使用 Heredoc 定义包含变量和双引号的 HTML 代码段
$htmlOutput = <<<EOF
<div class="container">
<h1>欢迎来到 "$siteName"</h1>
<p>这是一个使用 Heredoc 语法定义的多行段落。</p>
<p>这里的变量 \$siteName 被成功解析为:$siteName</p>
</div>
EOF;
// 注意:上面的 EOF; 必须死死贴着行首,不能有哪怕一个空格!
echo $htmlOutput;
?>
1.6.3 核心 PHP String 函数全景手册
PHP 核心自带了海量的字符串处理函数,以下是必须掌握的完整列表清单:
| 函数名称 | 核心业务用途描述 |
|---|---|
addslashes() | 防注入基础:在预定义的特殊字符(单引号、双引号、反斜杠、NULL)前面添加反斜杠进行转义。 |
stripslashes() | 数据还原:删除由 addslashes() 函数添加的反斜杠。 |
explode() | 字符串切片:根据指定的分隔符,把一个长字符串打散成一个数组。 |
implode() | (等同于 join())把数组的所有元素按照指定的连接符拼装成一个单一长字符串。 |
htmlspecialchars() | XSS防御核心:把预定义的字符(如 < 和 >)转换为 HTML 实体,防止浏览器将其解析为执行代码。 |
html_entity_decode() | 将 HTML 实体重新反转码回普通字符。 |
md5() | 哈希加密:计算字符串的 32 位 MD5 散列值(通常用于密码存储或文件校验)。 |
sha1() | 计算字符串的 SHA-1 散列值(比 MD5 更安全一点的算法)。 |
nl2br() | 格式化文本:自动在字符串中所有的换行符(\n)之前插入 HTML 的 <br> 换行标签。 |
str_replace() | 全局替换:在原字符串中查找所有指定的值,并替换为新的值(大小写敏感)。 |
str_ireplace() | 全局替换(大小写不敏感版本)。 |
strcmp() | 安全比较:对比两个字符串是否完全一致(返回 0 表示相等)。大小写敏感。 |
strcasecmp() | 安全比较:对比两个字符串。大小写不敏感。 |
strip_tags() | 净化文本:剥去字符串中包含的所有 HTML 和 PHP 标签,只保留纯文本。 |
strlen() | 返回字符串的字节数长度。(注意:处理中文字符应使用扩展函数 mb_strlen())。 |
strpos() | 查找字符串在另一字符串中第一次出现的位置(从左向右)。 |
strrpos() | 查找字符串在另一字符串中最后一次出现的位置(从右向左)。 |
strtolower() | 格式化:把字符串中的所有英文字符转换为纯小写。 |
strtoupper() | 格式化:把字符串中的所有英文字符转换为纯大写。 |
substr() | 截取字符串:根据指定的起始位置和长度,返回字符串的一部分片段。处理中文用 mb_substr()。 |
trim() | 清理数据:移除字符串左右两侧的空白字符(包括空格、制表符、换行符等)。 |
ltrim() / rtrim() | 仅移除左侧(ltrim)或右侧(rtrim)的空白字符。 |
1.7 PHP 超级全局变量 (Superglobals)
超级全局变量是在 PHP 4.1.0 之后引入的核心概念。它们是预定义的内部数组,在一个脚本的全部作用域中都可用,无需跨越函数边界声明 global。
1.7.1 $GLOBALS
一个包含了当前执行脚本中全部全局变量的关联数组。数组的键(Key)就是全局变量的名称。你可以在函数的任何角落随时存取。
1.7.2 $_SERVER (服务器环境变量表)
$_SERVER 是一个由 Web 服务器(如 Nginx, Apache)在每次请求时创建和填充的数组。它包含了大量的头信息、执行路径、以及客户端请求详情。这是开发后端路由和安全过滤的基石。
最核心的 $_SERVER 参数大全:
| 键名 (Key) | 详细数据描述与应用场景 |
|---|---|
$_SERVER['PHP_SELF'] | 当前正在执行的脚本相对于网站根目录的文件名。常用于表单自提交的 action 路径。 |
$_SERVER['SERVER_ADDR'] | 运行当前 PHP 脚本的后端服务器的真实物理 IP 地址。 |
$_SERVER['SERVER_NAME'] | 服务器的主机名或虚拟主机配置名称(如 www.example.com)。 |
$_SERVER['REQUEST_METHOD'] | 客户端访问该页面所使用的 HTTP 动词,如 GET, POST, PUT, DELETE 等。 |
$_SERVER['REQUEST_TIME'] | 该 HTTP 请求开始发起时的 Unix 绝对时间戳。 |
$_SERVER['QUERY_STRING'] | URL 中问号 ? 后面的所有查询字符串内容(如 id=5&type=admin)。 |
$_SERVER['HTTP_USER_AGENT'] | 客户端浏览器的 User-Agent 字符串。常用于判断设备类型(手机/PC)或拦截爬虫。 |
$_SERVER['HTTP_REFERER'] | 引导用户跳转到当前页面的前一个网页的 URL(注意:此值由客户端浏览器发送,容易被伪造,不可用于严格的安全校验)。 |
$_SERVER['REMOTE_ADDR'] | 发起请求的客户端(用户)的真实 IP 地址。 |
$_SERVER['SCRIPT_FILENAME'] | 当前执行脚本在服务器硬盘上的绝对物理路径(如 /var/www/html/index.php)。 |
1.7.3 $_REQUEST, $_POST, $_GET 表单数据接收
-
$_GET:接收通过 URL 地址栏传递的参数(HTTP GET 方法)。数据暴露在明文中,有长度限制,适合用于搜索查询或分页参数。 -
$_POST:接收通过 HTTP POST 方法在请求体中传递的数据。数据对终端用户不可见,无体积限制,绝对用于密码、大文本等敏感或大规模数据的传输。 -
$_REQUEST:一个综合性的数组,默认包含了$_GET、$_POST和$_COOKIE的所有数据。虽然方便,但在追求安全的现代框架中不推荐使用,应当明确区分请求来源。
<html>
<body>
<h3>系统登录</h3>
<form method="POST" action="<?php echo htmlspecialchars($_SERVER['PHP_SELF']);?>">
用户名: <input type="text" name="username"><br>
密 码: <input type="password" name="pwd"><br>
<input type="submit" value="登录">
</form>
<?php
// 后端处理逻辑部分
if ($_SERVER["REQUEST_METHOD"] == "POST") {
// 捕获 POST 数据
$user = $_POST['username'];
// 使用空值合并运算符提供默认值
$password = $_POST['pwd'] ?? '';
if (empty($user)) {
echo "<p style='color:red'>用户名不能为空!</p>";
} else {
echo "<p>尝试登录用户: " . htmlspecialchars($user) . "</p>";
echo "<p>您的来源 IP: " . $_SERVER['REMOTE_ADDR'] . "</p>";
}
}
?>
</body>
</html>
1.8 函数定义与变量函数
1.8.1 函数的基础架构
函数是可重用代码块的封装。
- 使用
function关键字定义。 - 可以通过参数列表(括号内的变量)接收外部传入的数据。
- 可以通过
return语句将内部计算的结果返回给调用方。
<?php
// 定义一个带有参数类型声明和默认值的复杂函数 (PHP 7+ 特性)
function generateAccessCode(string $prefix, int $length = 6) {
$randomStr = substr(md5(uniqid()), 0, $length);
$finalCode = $prefix . "-" . strtoupper($randomStr);
return $finalCode; // 抛出计算结果
}
$newCode = generateAccessCode("AUTH", 8);
echo "生成的授权码: " . $newCode . "<br>";
?>
1.8.2 变量函数 (可变函数)
PHP 拥有一个非常灵活的特性:如果一个变量名后面紧跟了一对圆括号 (),PHP 引擎将会在内部寻找与该变量的值同名的函数,并尝试执行它。这在实现回调机制、路由分发或工厂模式时极为强大。
<?php
function modeAttack() {
echo "系统已进入攻击探测模式...<br>";
}
function modeDefend() {
echo "防火墙规则已加强,防御模式启动...<br>";
}
// 动态决定执行哪个逻辑
$executionCommand = "modeDefend";
// 加上括号,引擎会自动解析并调用 modeDefend() 函数
$executionCommand();
?>
1.9 内置函数
内置函数是 PHP 核心(或通过扩展模块)预先定义好的、在运行时被调用的代码封装。与语言结构在底层编译阶段被解析不同,内置函数遵循严格的面向过程的函数调用规范。
1.9.1.1 内置函数的核心特性(与语言结构对比)
-
必须严格使用括号
(): 这是内置函数最致命的语法限制。无论该函数是否需要接收参数,调用时都必须带上括号。例如phpinfo();是合法的,但写成phpinfo;会导致语法错误。(这也是为什么在 CTF 中一旦过滤了左括号(,所有内置函数集体失效的原因)。 -
支持“变量函数”动态调用: 这是内置函数极其强大(也极其危险)的特性。你可以将函数名赋值给一个变量,然后通过该变量来动态调用函数。例如:
PHP
$func = 'system'; $func('whoami'); // 等同于执行 system('whoami');(注:这种特性常被用于编写高度隐蔽的一句话木马,或者在 WAF 绕过中进行函数名的字符串拼接。)
-
具有标准返回值: 绝大多数内置函数执行完毕后都会返回一个结果(布尔值、字符串、数组或资源等),因此它们的执行结果可以直接赋值给其他变量。例如
$len = strlen("hello");。 -
不区分大小写: 虽然不推荐,但 PHP 的内置函数名(包括自定义函数名)是不区分大小写的,
System('ls')和system('ls')效果相同。
1.9.1.2 常见的核心内置函数分类(按安全审计敏感度划分)
在代码审计和 CTF 实战中,我们通常会高度关注以下几类内置函数:
- 系统命令执行类(极度危险):
system()/passthru()(执行外部程序并显示输出)exec()/shell_exec()(执行外部程序,通常需要 return 或 echo 输出)
- 文件读取与高亮类:
file_get_contents()(将整个文件读入一个字符串,支持伪协议)highlight_file()/show_source()(语法高亮一个文件,常用于读取 flag 源码)readfile()(输出一个文件)
- 代码执行与处理类:
preg_replace()(在 PHP 5.5 以下版本中,如果使用/e修饰符,会导致代码执行)assert()(在 PHP 7 之前,如果参数是字符串,会被当做 PHP 代码执行)call_user_func()/call_user_func_array()(用于回调函数,是利用变量函数的进阶手段)
- 变量与信息查看类:
phpinfo()(输出当前 PHP 环境的超详细信息,常作为测试执行漏洞的探针)var_dump()/print_r()(注意:print是语言结构,但print_r是正儿八经的内置函数)
在漏洞挖掘中,内置函数的**“变量函数动态调用”机制是攻击者的最爱,例如利用 $_GET['a']($_GET['b']) 配合 ?a=system&b=ls 即可轻松 RCE。 然而,防守方一旦在正则表达式中加入针对括号 () 或者关键字(如 system, exec)的过滤,攻击者就不得不放弃内置函数,转而去寻找“语言结构”来作为替代方案。
1.10 语言结构
不同于内置函数,语言结构是 PHP 语法引擎(Zend Engine)本身的一部分,它们在词法分析和编译阶段就被直接解析,而不是像普通函数那样在运行时被调用。
1.10.1.1 与内置函数的核心区别
- 不需要括号: 这是最显著的特征。语言结构可以直接跟参数,例如
echo "hello";或include "flag.php";。而函数即使没有参数也必须带括号,如phpinfo();。 - 允许无空格解析: 当语言结构后面紧跟的是变量(以
$开头)时,解析器可以通过$符号天然区分边界,因此不需要空格。例如include$a;是合法的,但system$a;会报语法错误(Parse error)。 - 不支持变量函数调用: 你不能用变量来动态调用语言结构。例如
$func = 'echo'; $func('test');会报错,但如果$func = 'system';就可以正常执行。 - 行为不受限于函数规则: 有些语言结构没有返回值(如
echo),不能像函数那样赋值给变量(如$a = echo "1";是错误的)。
1.10.1.2 常见的 PHP 语言结构汇总
在日常开发和代码审计中,最常遇到的语言结构可分为以下几类:
- 输出/中断类:
echo/print(输出内容)die/exit(输出内容并终止脚本运行)
- 文件包含类(重要):
include/include_oncerequire/require_once
- 变量处理类:
isset(检测变量是否已声明且不为 null)empty(检查变量是否为空)unset(释放给定的变量)
- 代码执行类:
eval(注意: 这是一个极其特殊的语言结构,它虽然属于语言结构,但在 PHP 语法规定中必须使用括号,即eval(...))。
1.11 全面解析 PHP 运算符
运算符是指令引擎如何处理数据变量的符号集。
1.11.1 算术与赋值运算符
-
算术:加
+、减-、乘*、除/、模(求余)%、按位取反~。(PHP 7+ 新增整除函数
intdiv(10, 3)返回 3) -
赋值:基本赋值
=,它会将右侧的值载入左侧变量。结合算术可形成复合赋值:+=,-=,*=,/=,%=,.=(字符串追加赋值)。
1.11.2 递增/递减运算符的执行顺序差异
非常容易出错的考点:放在前面和放在后面的区别。
- 预递增
++$x:先将变量加 1,然后再将增加后的值用于当前表达式。 - 后递增
$x++:先将变量的当前值用于表达式计算,然后再给自身加 1。
<?php
$a = 10;
echo "预递增 (先加后输出): " . ++$a . "<br>"; // 输出 11,此时 $a=11
$b = 10;
echo "后递增 (先输出后加): " . $b++ . "<br>"; // 输出 10,输出完毕后 $b 变成 11
echo "验证后递增结果: " . $b . "<br>"; // 输出 11
?>
1.11.3 逻辑运算符优先级陷阱
and/&&:逻辑与(两者皆为真才为真)。or/||:逻辑或(任一为真即为真)。xor:异或(有且仅有一个为真时返回真,都为真或都为假则返回假)。!:逻辑非(取反)。
优先级考点: && 和 || 的优先级高于赋值运算符 =,而英文单词格式的 and 和 or 优先级低于赋值运算符。
<?php
$x = true;
$y = false;
// && 优先级高于 =, 所以先计算 true && false, 得到 false 赋值给 $result1
$result1 = $x && $y;
var_dump($result1); // bool(false)
// = 优先级高于 and, 所以先把 $x(true) 赋值给 $result2, 剩下的 and $y 被截断丢弃
$result2 = $x and $y;
var_dump($result2); // bool(true)
?>
1.11.4 高级比较符:三元、NULL合并与太空船
现代 PHP 提供了极其优雅的单行条件判断符号:
- 传统三元运算符 (
?:):条件 ? 真值 : 假值。如果条件成立返回第一项,否则第二项。自 PHP 5.3 起可简写为条件 ?: 假值(如果条件本身有值就返回条件自身)。 - NULL 合并运算符 (
??, PHP 7+):用于替代isset()判断。如果变量存在且不为 NULL,就返回它本身,否则返回右侧给定的默认值。 - 组合比较符/太空船操作符 (
<=>, PHP 7+):实现全方位比对。- 左边 > 右边,返回
1 - 左右相等,返回
0 - 左边 < 右边,返回
-1
- 左边 > 右边,返回
<?php
// 1. 三元运算符简写
$loggedInUser = "Admin";
$displayUser = $loggedInUser ?: "Guest"; // 返回 "Admin"
// 2. NULL 合并运算符 (处理表单或外部请求的绝佳实践)
$pageOffset = $_GET['offset'] ?? 0; // 如果没传参,默认 offset 为 0
// 3. 太空船运算符 (常用于自定义排序算法中的值比对)
echo "比对 5 和 10: " . (5 <=> 10) . "<br>"; // -1
echo "比对 A 和 A: " . ("A" <=> "A") . "<br>";// 0
?>
1.12 逻辑控制流与循环结构
逻辑语句指导了代码的执行路径。
1.12.1 条件控制分支 (If / Switch)
if...else:用于处理连续的范围判断或复杂的复合逻辑。switch...case:专门处理针对同一个变量匹配多个离散固定的“值”的情况。switch性能更优且结构清晰。千万别忘记写break;,否则代码会发生“贯穿(fall-through)”,错误地执行下一个 case 的代码。
<?php
// Switch 语句实战
$httpStatusCode = 404;
switch ($httpStatusCode) {
case 200:
echo "请求成功处理 (OK)";
break;
case 301:
case 302:
// 多个 case 共享一块代码 (利用贯穿特性)
echo "发生了页面重定向";
break;
case 403:
echo "拒绝访问 (Forbidden)";
break;
case 404:
echo "资源未找到 (Not Found)";
break;
default:
// 兜底策略,未匹配时执行
echo "未知的状态码";
}
?>
1.12.2 循环的艺术 (While / For / Foreach)
使你的服务器能够在一瞬间处理成千上万条记录的利器:循环。
while循环:先判断条件。只有当给定条件为 TRUE 时才进入循环体执行。适合不知道具体迭代次数,只依赖某个阈值的场景。do...while循环:先执行,后判断。特点是至少会强制执行一次循环体内的代码,然后才开始检查条件。for循环:用于预先明确知道需要执行多少次的场景。它的结构(初始值设置;条件判断界限;计数器增量)被整齐地排列在代码行头部,一目了然。foreach循环:专为遍历数组和对象属性而生。无需手动设置计数器或判断边界,引擎会自动剥离出数组当前的“键(Key)”和“值(Value)”,并在内部移动指针,直到遍历完所有元素。
<?php
echo "<h4>1. For 循环:执行固定次数</h4>";
// 初始化 $i=1; 判断 $i<=3; 每次执行后 $i 加 1
for ($i = 1; $i <= 3; $i++) {
echo "系统初始化进度: 第 " . $i . " 阶段\n<br>";
}
echo "<h4>2. Do...While 循环:至少执行一次</h4>";
$attempts = 5;
do {
echo "尝试连接服务器... (当前 attempts = " . $attempts . ")\n<br>";
$attempts++;
} while ($attempts < 3); // 虽然初始值 5 已经大于 3,但依然执行了一次
echo "<h4>3. Foreach 循环:优雅解构数组</h4>";
$systemLog = array(
"10:01" => "Auth Success",
"10:05" => "Data Fetched",
"10:45" => "Connection Closed"
);
// 自动将键赋值给 $time,将值赋值给 $event
foreach ($systemLog as $time => $event) {
echo "[时间戳 " . $time . "] 触发事件: " . $event . "\n<br>";
}
?>