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 向浏览器输出文本或变量,最基础且最常用的指令有两种:echoprint

  • 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 变量用于保存数据和表达式的结果。

严格的命名规则:

  1. 标识符前缀:所有变量必须以美元符号 $ 开始,紧跟变量名称。
  2. 首字符限制:变量名必须以英文字母或者下划线(_)字符开始,绝对不能以数字开头
  3. 字符集限制:变量名只能包含字母、数字以及下划线(即 A-z0-9_)。
  4. 禁止空格:变量名内部不能包含任何空格。如果需要包含多个单词,建议使用下划线命名法($my_car)或驼峰命名法($myCar)。
  5. 大小写敏感: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 标量数据类型

  1. String(字符串):一串字符序列。可以使用单引号或双引号。双引号会解析内部的变量,而单引号不会。
  2. Integer(整型):没有小数部分的数字。至少包含一个数字,不能包含逗号或空格。支持十进制、十六进制(0x前缀)和八进制(0前缀)。
  3. Float(浮点型):带小数部分的数字,或指数形式。
  4. 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 复合数据类型

  1. Array(数组):在一个变量中存储多个独立的值。
  2. 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 特殊数据类型

  1. NULL(空值):表示一个变量没有值。主要用于清空变量、区分变量是否被初始化。
  2. 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 数组的三种形态

  1. 数值数组(索引数组):带有数字 ID 键的数组。键名默认从 0 开始自动分配,也可手动干预。
  2. 关联数组(字典):带有指定字符串键的数组,每一个键映射一个具体的值。
  3. 多维数组:数组内部的元素也是数组,用于表示复杂的矩阵或层级数据。
<?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 常量的定义与特性

常量是指一旦在脚本中定义后,其值绝对不能被改变或重新定义的标识符。

  1. 全局有效:常量不受变量作用域的限制,定义后可以在脚本的任何地方(包括函数内部)直接使用,无需 global 关键字。
  2. 没有 $ 符号:常量名在使用和定义时都不带美元符号。
  3. 命名规范:通常使用全大写字母加下划线命名(如 DB_PASSWORD)。

定义常量的两种方式:

  1. define(name, value, case_insensitive) 函数:运行时定义。PHP 8.0 开始废弃了大小写不敏感(第三个参数设为 true)的设定。
  2. 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) 语法

使用 <<< 加上一个标识符(通常习惯用 EOFEODHTML)来标记字符串的开始。

Heredoc 的五大核心铁律:

  1. 开始标记<<<EOF 后面不能有任何字符(包括空格)。
  2. 变量解析:位于开始和结束标记之间的 PHP 变量会被正常解析(就像双引号一样),而且不需要使用 . 来拼接,直接写在文本里即可。但函数不能直接在里面解析。
  3. 无需转义:在内容中直接嵌套单引号或双引号时,不需要加反斜杠转义,非常适合写大段的 HTML。
  4. 结束标记极其严格:结束标记 EOF; 必须顶头独立占一行!它的前面不能有缩进、不能有空格,后面除了分号外也不能有任何字符。
  5. 标识符一致:开始的标识符和结束的标识符必须完全一致。

代码示例:

<?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_once
    • require / 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:异或(有且仅有一个为真时返回真,都为真或都为假则返回假)。
  • !:逻辑非(取反)。

优先级考点: &&|| 的优先级高于赋值运算符 =,而英文单词格式的 andor 优先级低于赋值运算符。

<?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 提供了极其优雅的单行条件判断符号:

  1. 传统三元运算符 (?:)条件 ? 真值 : 假值。如果条件成立返回第一项,否则第二项。自 PHP 5.3 起可简写为 条件 ?: 假值(如果条件本身有值就返回条件自身)。
  2. NULL 合并运算符 (??, PHP 7+):用于替代 isset() 判断。如果变量存在且不为 NULL,就返回它本身,否则返回右侧给定的默认值。
  3. 组合比较符/太空船操作符 (<=>, 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)

使你的服务器能够在一瞬间处理成千上万条记录的利器:循环。

  1. while 循环先判断条件。只有当给定条件为 TRUE 时才进入循环体执行。适合不知道具体迭代次数,只依赖某个阈值的场景。
  2. do...while 循环先执行,后判断。特点是至少会强制执行一次循环体内的代码,然后才开始检查条件。
  3. for 循环:用于预先明确知道需要执行多少次的场景。它的结构(初始值设置;条件判断界限;计数器增量)被整齐地排列在代码行头部,一目了然。
  4. 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>";
}
?>