Skip to content

PHP 速查表

用于 Web 开发的流行通用脚本语言。

01

基础

变量、类型与常量

PHP 变量以 $ 开头,是动态类型的。PHP 7.4+ 支持类型化属性。使用 define() 定义运行时常量,使用 const 定义编译时常量(更快)。PHP 8.1 引入了枚举——一种优于类常量的类型化枚举系统。尽可能始终声明类型(PHP 7+),以实现早期错误检测和更好的 IDE 支持。

php
<?php
$name = "Alice";          // string
$age = 30;                // integer
$price = 19.99;           // float (double)
$active = true;           // boolean
$items = [1, 2, 3];       // array
$null = null;             // null

// constants
define("MAX_USERS", 100); // runtime constant
const PI = 3.14159;       // compile-time constant
echo MAX_USERS;            // no $ prefix

// PHP 8.1+ enums
enum Status: string {
    case Active = 'active';
    case Inactive = 'inactive';
}
$s = Status::Active;

Echo、Print 与调试

echo 是一个语言结构(不是函数)——输出速度最快。print 返回 1,因此可用于表达式中。printf/sprintf 使用 C 风格的格式说明符(%s 字符串、%d 整数、%f 浮点数、%x 十六进制)。var_dump() 是主要的调试工具——显示类型和值。error_log() 写入 PHP 错误日志或 syslog。在生产环境中,切勿向用户暴露调试输出。

php
<?php
// echo: no return, multiple args (fastest)
echo "Hello", " ", "World";

// print: returns 1, single arg
print "Hello World";

// printf: formatted output
printf("Name: %s, Age: %d, Price: %.2f", "Alice", 30, 19.99);

// debugging output
print_r($array);           // human-readable
var_dump($variable);       // type + value (detailed)
var_export($array, true);  // valid PHP code (for caching)

// sprintf: return formatted string (don't print)
$log = sprintf("[%s] %s", date('H:i:s'), "Started");
error_log($log);           // log to error log

运算符与比较

始终使用 ===(严格比较)以避免类型转换错误。== 在比较前转换类型,会导致令人意外的结果(在 PHP 7 中 0 == 'abc' 为 true)。太空船运算符(<=>)返回 -1/0/1——对 usort 很有用。空合并运算符(??)是提供默认值的惯用方式。空安全运算符(?->)(PHP 8+)在 null 上短路方法链,替代了冗长的 isset() 检查。

php
<?php
// arithmetic
$sum = 10 + 3;       // 13
$mod = 10 % 3;       // 1
$pow = 2 ** 3;       // 8 (PHP 5.6+)

// comparison: == vs ===
echo (0 == "abc");   // true (loose, PHP 7-); false (PHP 8+)
echo (0 === "abc");  // false (strict — type + value)
echo ("1" == 1);     // true (loose)
echo ("1" === 1);    // false (strict)

// spaceship operator (PHP 7+)
echo 1 <=> 2;        // -1 (less than)
echo 2 <=> 2;        // 0 (equal)
echo 3 <=> 2;        // 1 (greater than)

// null coalescing
$name = $input ?? "default";    // if $input is null
$deep = $data['user']['name'] ?? "Anonymous";

// null safe operator (PHP 8+)
$country = $user?->getAddress()?->country; // null if any step is null

超全局变量与 Web

超全局变量是在所有作用域中都可用的内置关联数组。$_GET 和 $_POST 包含用户输入——使用前务必进行清理/验证。filter_input() 比直接访问更安全。切勿信任可被客户端伪造的 $_SERVER 值(如 HTTP_USER_AGENT)。在 header('Location:') 之后始终调用 exit——否则 PHP 会继续执行。在任何输出之前使用 session_start() 启动会话。

php
<?php
// superglobals available everywhere
$_GET['name'];           // query string params
$_POST['email'];         // form POST data
$_REQUEST['x'];          // GET + POST + COOKIE
$_SERVER['HTTP_HOST'];   // server/env info
$_SERVER['REQUEST_METHOD']; // GET, POST, etc.
$_COOKIE['session'];     // cookies
$_FILES['upload'];       // file uploads
$_SESSION['user_id'];    // session data (after session_start())

// get client IP
$ip = $_SERVER['REMOTE_ADDR'];

// check request method
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $name = filter_input(INPUT_POST, 'name', FILTER_SANITIZE_STRING);
}

// redirect
header("Location: /dashboard");
exit; // always exit after redirect

Include 与 Require

include/require 执行指定文件。如果文件缺失,require 会引发致命错误(用于关键依赖);include 仅发出警告(用于可选模板)。_once 变体跟踪已包含的文件以防止重复包含——对函数/类定义至关重要。Composer 的自动加载器(require_once 'vendor/autoload.php')消除了手动包含管理。文件可以返回值,使其适用于配置。

php
<?php
// include: warning on failure, continues
include 'header.php';
include_once 'config.php'; // only once

// require: fatal error on failure, stops
require 'database.php';
require_once 'vendor/autoload.php'; // Composer autoload

// _once variants prevent re-declaration errors
// use require for critical files (DB config, autoloaders)
// use include for optional files (templates)

// return values from included files
$config = include 'config.php';
// config.php: return ['db' => 'mysql://...', 'debug' => true];
02

字符串

字符串函数

PHP 有 100 多个字符串函数。strpos() 在未找到时返回 false——使用 === false 检查(0 是有效位置)。str_replace() 可以接受数组进行搜索/替换。substr() 支持负偏移量(从末尾开始)。对于多字节字符串(UTF-8),使用 mb_* 等效函数(mb_strlen、mb_substr)——strlen 计算字节数,而非字符数。始终设置 default_charset='UTF-8' 并对非 ASCII 文本使用 mb_* 函数。

php
<?php
$s = "Hello, World";

// length and case
echo strlen($s);              // 12
echo str_word_count($s);      // 2
echo strtoupper($s);          // HELLO, WORLD
echo strtolower($s);          // hello, world
echo ucfirst("hello");        // Hello
echo ucwords("hello world");  // Hello World

// search and replace
echo strpos($s, "World");     // 7 (false if not found)
echo str_replace("o", "0", $s); // Hell0, W0rld
echo substr($s, 0, 5);        // Hello
echo substr($s, -5);          // World
echo strrev($s);              // dlroW ,olleH

// trim
echo trim("  hi  ");          // "hi"
echo ltrim("  hi");           // "hi"
echo rtrim("hi  ");           // "hi"

字符串插值与 Heredoc

双引号字符串插值变量;单引号字符串不插值(字面文本使用单引号——稍快)。使用 {$var} 表示复杂表达式(对象属性、数组访问、方法调用)。Heredoc(<<<ID)非常适合多行字符串,如 SQL 或 HTML——它会插值变量。Nowdoc(<<<'ID')是不解释的版本,适用于带反斜杠的正则表达式模式。

php
<?php
$name = "Alice";
$age = 30;

// double quotes: variable interpolation
echo "Hello, $name!";          // Hello, Alice!
echo "Hello, {$name}!";        // Hello, Alice! (braces for clarity)
echo "Age: {$age}";            // Age: 30

// single quotes: no interpolation (faster)
echo 'Hello, $name';           // Hello, $name (literal)

// complex expressions need braces
echo "Result: {$obj->method()}";
echo "Item: {$array['key']}";

// heredoc: multi-line, interpolates
$sql = <<<SQL
    SELECT * FROM users
    WHERE name = '$name'
    AND age > $age
SQL;

// nowdoc: multi-line, NO interpolation (like single quotes)
$regex = <<<'REGEX'
    \d{3}-\d{4}
REGEX;

sprintf 与格式化

sprintf() 对于安全构建格式化字符串至关重要——与字符串插值不同,它处理类型转换和填充。对整数使用 %d(而非 %s)以确保数字格式化。number_format() 用千位分隔符格式化数字——对货币显示至关重要。在预处理语句中对 SQL 片段始终使用 sprintf(尽管用户输入仍需要预处理语句)。

php
<?php
// sprintf: format string (returns, doesn't print)
$formatted = sprintf(
    "%-10s | %5d | %8.2f",
    "Alice", 42, 19.99
);
echo $formatted;  // "Alice      |    42 |    19.99"

// format specifiers:
// %s string, %d int, %f float, %x hex, %b binary, %c char
// %5d  right-padded to width 5
// %-5d left-padded
// %05d zero-padded
// %.2f 2 decimal places
// %8.2f width 8, 2 decimals

// numbered placeholders
echo sprintf("Hi %1$s, bye %1$s", "Alice"); // Hi Alice, bye Alice

// number_format
echo number_format(1234567.891, 2);  // 1,234,567.89
echo number_format(1234567.89, 2, ',', '.'); // European format

正则表达式(PCRE)

PHP 使用 PCRE(Perl 兼容正则表达式),以 /pattern/ 作为分隔符。preg_match 匹配返回 1,不匹配返回 0(使用 ===,而非 ==,因为 0 是假值)。始终用正则表达式验证用户输入,但不要仅依赖它——对电子邮件、URL 使用 filter_var()。preg_replace 功能强大,但在大字符串上可能很慢。使用 [^...] 白名单字符而非黑名单。i 标志使匹配不区分大小写。

php
<?php
// preg_match: test pattern (returns 0 or 1)
if (preg_match('/^[a-z]+$/i', "Hello")) {
    echo "alphabetic";
}

// capture groups
preg_match('/(d{4})-(d{2})-(d{2})/', "2024-01-15", $matches);
// $matches[0] = "2024-01-15" (full match)
// $matches[1] = "2024" (group 1)
// $matches[2] = "01"
// $matches[3] = "15"

// preg_match_all: all matches
preg_match_all('/d+/', "a1 b22 c333", $matches);
// $matches[0] = ["1", "22", "333"]

// preg_replace: substitute
$clean = preg_replace('/[^a-z0-9]/i', '', "Hello, World!"); // HelloWorld

// preg_split: split by pattern
$parts = preg_split('/[s,]+/', "one, two,three  four");
// ["one", "two", "three", "four"]

多字节与编码

PHP 的默认字符串函数是面向字节的,而非面向字符的——它们在多字节字符(UTF-8、中文、表情符号)上会出错。对非 ASCII 文本始终使用 mb_* 函数(mb_strlen、mb_substr、mb_strpos、mb_strtoupper)。在应用程序开始时设置 mb_internal_encoding('UTF-8')。使用 JSON_UNESCAPED_UNICODE 保持 JSON 输出中的中文/表情符号可读。这是国际化应用程序中常见的错误来源。

php
<?php
// UTF-8 strings: use mb_* functions
$text = "Café";  // 4 chars, 5 bytes (é = 2 bytes)

echo strlen($text);      // 5 (bytes!)
echo mb_strlen($text);   // 4 (characters!)

echo substr($text, 0, 3);    // "Caf" (might break UTF-8!)
echo mb_substr($text, 0, 3); // "Caf" (safe)

// case conversion
echo strtoupper("straße");    // "STRAßE" (wrong!)
echo mb_strtoupper("straße"); // "STRAßE" (correct, locale-aware)

// encoding detection
$encoding = mb_detect_encoding($text);
$utf8 = mb_convert_encoding($text, 'UTF-8', 'auto');

// set internal encoding
mb_internal_encoding('UTF-8');
mb_regex_encoding('UTF-8');

// JSON with UTF-8
$json = json_encode($data, JSON_UNESCAPED_UNICODE);
03

数组

索引数组与关联数组

PHP 数组实际上是有序的哈希映射——它们既可以作为列表也可以作为字典使用。索引数组自动分配数字键;关联数组使用字符串键。isset() 对 null 值返回 false;array_key_exists() 即使对 null 也返回 true。unset() 移除元素但不会重新索引。对于真正的列表(无间隙),删除后使用 array_values() 重新索引。PHP 8.1+ 有只读数组类型。

php
<?php
// indexed array (numeric keys)
$nums = [10, 20, 30];
$nums[] = 40;              // append
echo $nums[0];             // 10
echo count($nums);         // 4

// associative array (string keys)
$user = [
    "name" => "Alice",
    "age" => 30,
    "email" => "[email protected]",
];
echo $user["name"];        // Alice
$user["phone"] = "555-1234"; // add key

// mixed keys
$mixed = [0 => "a", "name" => "b", 5 => "c"];

// check key existence
if (isset($user["email"])) { /* ... */ }
if (array_key_exists("name", $user)) { /* ... */ }

// remove element
unset($user["phone"]);

多维数组与迭代

多维数组是数组的数组。foreach 是迭代的惯用方式——比 for 循环更快、更易读。使用 &$value 原地修改元素(循环后始终 unset 引用以避免错误)。array_column() 从二维数组中提取单列——对转换数据库结果集极其有用。PHP 数组保持插入顺序。

php
<?php
$users = [
    ["name" => "Alice", "age" => 30],
    ["name" => "Bob", "age" => 25],
    ["name" => "Carol", "age" => 35],
];

// iterate with key + value
foreach ($users as $index => $user) {
    echo "$index: {$user['name']} ({$user['age']})\n";
}

// modify by reference
foreach ($users as &$user) {
    $user['age'] += 1; // increment each age
}
unset($user); // break reference!

// nested iteration
$matrix = [[1, 2], [3, 4], [5, 6]];
foreach ($matrix as $row) {
    foreach ($row as $cell) {
        echo $cell . " ";
    }
    echo "\n";
}

// extract column
$names = array_column($users, 'name'); // ["Alice", "Bob", "Carol"]

数组函数:map、filter、reduce

array_map、array_filter 和 array_reduce 是数组的函数式编程三件套。箭头函数(fn() =>)使这些更简洁。array_filter 保留键——如需要重新索引请使用 array_values()。array_merge 重新索引数字键但保留字符串键(后面的值覆盖前面的)。array_column、array_chunk 和 array_slice 是数据操作的基础。这些函数是 PHP 数据处理的骨干。

php
<?php
$nums = [1, 2, 3, 4, 5];

// map: transform each element
$doubled = array_map(fn($n) => $n * 2, $nums);  // [2, 4, 6, 8, 10]

// filter: keep elements matching condition
$evens = array_filter($nums, fn($n) => $n % 2 === 0);  // [2, 4]

// reduce: accumulate to single value
$sum = array_reduce($nums, fn($carry, $n) => $carry + $n, 0);  // 15

// walk: like map but modifies in place (by reference)
array_walk($nums, fn(&$n) => $n *= 2);

// combining arrays
$merged = array_merge([1, 2], [3, 4]);        // [1, 2, 3, 4]
$combined = array_combine(['a', 'b'], [1, 2]); // ['a' => 1, 'b' => 2]
$sliced = array_slice($nums, 1, 2);            // [2, 3]
$chunked = array_chunk($nums, 2);              // [[1,2], [3,4], [5]]

数组排序

PHP 排序函数原地修改数组(按引用传递)。sort/rsort 重新索引;asort/arsort 保留键。usort 使用比较函数(使用 <=>)按自定义逻辑排序。natsort() 执行自然排序(img2 在 img10 之前)——对文件名至关重要。对于多维数组,使用 usort 和比较所需字段的闭包。太空船运算符(<=>)简化了比较函数。

php
<?php
$nums = [3, 1, 4, 1, 5, 9, 2, 6];

// sort by value (reindexes)
sort($nums);                          // [1, 1, 2, 3, 4, 5, 6, 9]
rsort($nums);                         // descending

// sort preserving keys
asort($nums);   // ascending, preserve keys
arsort($nums);  // descending, preserve keys

// sort by key
ksort($nums);   // by key ascending
krsort($nums);  // by key descending

// custom sort with callback
$users = [["name" => "Bob", "age" => 25], ["name" => "Alice", "age" => 30]];
usort($users, fn($a, $b) => $a['age'] <=> $b['age']);
// sorted by age ascending

// natural sort (for strings with numbers)
$files = ["img10.jpg", "img2.jpg", "img1.jpg"];
natsort($files); // ["img1.jpg", "img2.jpg", "img10.jpg"]
sort($files);    // ["img1.jpg", "img10.jpg", "img2.jpg"] (wrong!)

数组检查与操作

in_array 设置 strict=true(第三个参数)可防止类型转换错误。array_search 返回键(使用 === false 检查)。array_push/pop 实现 LIFO(栈);array_shift/unshift 实现 FIFO(队列)——但 shift 是 O(n)。对于大队列,使用 SplQueue 或 SplDoublyLinkedList。array_unique 保留键。array_diff/intersect 比较值;使用 array_diff_key/intersect_key 进行基于键的比较。

php
<?php
$arr = [1, 2, 3, 4, 5];

// inspection
echo count($arr);                  // 5
echo in_array(3, $arr);            // true
echo in_array("3", $arr, true);    // false (strict)
echo array_search(3, $arr);        // 2 (key, false if not found)
print_r(array_keys($arr));         // [0, 1, 2, 3, 4]
print_r(array_values($arr));       // [1, 2, 3, 4, 5]

// stack/queue operations
array_push($arr, 6);    // push to end
$last = array_pop($arr); // pop from end
$first = array_shift($arr); // remove from front
array_unshift($arr, 0); // add to front

// set operations
$unique = array_unique([1, 2, 2, 3]); // [1, 2, 3]
$diff = array_diff([1, 2, 3], [2, 3, 4]); // [1] (in first, not second)
$intersect = array_intersect([1, 2, 3], [2, 3, 4]); // [2, 3]

// flip and reverse
$flipped = array_flip(['a' => 1, 'b' => 2]); // [1 => 'a', 2 => 'b']
$reversed = array_reverse([1, 2, 3]); // [3, 2, 1]
04

控制流

If / Else / Elseif

PHP 使用 elseif(一个词)——不是带空格的 'else if'(尽管那样也可以)。替代语法(if: ... endif;)在 HTML 模板中很有用,可避免大括号匹配混乱。三元运算符是右结合的——避免嵌套。空合并赋值运算符(??=)仅在当前为 null 时设置值——非常适合配置默认值的惰性初始化。

php
<?php
$score = 85;

if ($score >= 90) {
    $grade = "A";
} elseif ($score >= 80) {
    $grade = "B";
} elseif ($score >= 70) {
    $grade = "C";
} else {
    $grade = "F";
}

// alternative syntax (for templates)
if ($score >= 90):
    echo "Excellent";
elseif ($score >= 80):
    echo "Good";
else:
    echo "Try harder";
endif;

// ternary
$status = $age >= 18 ? "adult" : "minor";

// null coalescing assignment (PHP 7.4+)
$config['timeout'] ??= 30; // set if not set

Switch 与 Match

switch 使用松散比较(==)并需要 break 来防止穿透——常见的错误来源。match(PHP 8+)使用严格比较(===),直接返回值,如果没有分支匹配则抛出异常(不会静默失败)。当需要返回值时,match 是 switch 的现代替代品。对复杂的多语句情况使用 switch;对简单的值选择使用 match。始终包含 default 情况。

php
<?php
// switch: loose comparison (==)
$day = "Mon";
switch ($day) {
    case "Mon":
    case "Tue":
    case "Wed":
        echo "Weekday";
        break;
    case "Sat":
    case "Sun":
        echo "Weekend";
        break;
    default:
        echo "Unknown";
}

// match (PHP 8+): strict comparison (===), returns value
$status = 404;
$message = match($status) {
    200, 201 => "Success",
    301, 302 => "Redirect",
    404 => "Not Found",
    500 => "Server Error",
    default => "Unknown",
};
// match throws UnhandledMatchError if no match and no default

循环:for、while、foreach、do-while

foreach 是数组的惯用循环——比带 count() 的 for 更快更安全。使用 continue 跳过迭代,使用 break 退出。PHP 没有带标签的 break/continue(与 Java/Rust 不同)。对于关联数组,foreach ($arr as $key => $value) 是标准模式。do-while 至少运行一次——适用于输入验证。避免在 foreach 期间修改数组(使用单独的数组存储结果)。

php
<?php
// for loop
for ($i = 0; $i < 5; $i++) {
    echo $i;  // 01234
}

// foreach (most common in PHP)
$fruits = ["apple", "banana", "cherry"];
foreach ($fruits as $fruit) {
    echo $fruit;
}

// foreach with key
foreach ($fruits as $index => $fruit) {
    echo "$index: $fruit";
}

// while
$count = 0;
while ($count < 3) {
    echo $count++;
}

// do-while (runs at least once)
do {
    $line = readline("> ");
} while ($line !== "quit");

// break and continue
for ($i = 0; $i < 10; $i++) {
    if ($i === 3) continue; // skip 3
    if ($i === 7) break;    // stop at 7
    echo $i;
}

模板中的控制流

PHP 的替代控制语法(if:/elseif:/else:/endif;,foreach:/endforeach;)专为 HTML 模板设计。<?= $var ?> 是 <?php echo $var; ?> 的简写——在模板中始终使用它以提高可读性。始终用 htmlspecialchars() 转义输出以防止 XSS。PHP 逻辑与 HTML 表现的分离是 Twig 和 Blade 等模板系统的基础,它们提供更清晰的语法和自动转义。

php
<?php // template file ?>
<?php if ($user->isAdmin()): ?>
    <div class="admin-panel">Admin Tools</div>
<?php elseif ($user->isEditor()): ?>
    <div class="editor-tools">Edit Tools</div>
<?php else: ?>
    <div class="user-view">Read Only</div>
<?php endif; ?>

<?php foreach ($products as $p): ?>
    <div class="product">
        <?= htmlspecialchars($p['name']) ?>
        - $<?= number_format($p['price'], 2) ?>
    </div>
<?php endforeach; ?>

<?php // shorthand echo ?>
<h1><?= $title ?></h1>

<?php // ternary in templates ?>
<span class="<?= $active ? 'on' : 'off' ?>"><?= $active ? 'Active' : 'Inactive' ?></span>

异常与错误处理

PHP 7+ 对大多数错误使用异常。始终捕获特定异常类型(而非仅 Exception)以适当处理不同的失败。finally 始终执行——用于清理(关闭文件、连接)。自定义异常扩展 Exception 并添加领域上下文。PHP 8+ 允许使用 | 捕获多种异常类型。将 PDO 设置为异常模式以实现一致的错误处理。切勿捕获异常而不记录——静默失败会隐藏错误。

php
<?php
// try / catch / finally
try {
    $pdo = new PDO("mysql:host=localhost;dbname=test", "user", "pass");
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    error_log("DB connection failed: " . $e->getMessage());
    die("Service unavailable");
} finally {
    // always runs, even after return/throw
    echo "Cleanup";
}

// custom exception
class ValidationException extends Exception {
    public function __construct(string $field, string $message = "") {
        parent::__construct("$field: $message");
    }
}

// throw
if (empty($email)) {
    throw new ValidationException("email", "is required");
}

// catch multiple types (PHP 8+)
try {
    riskyOperation();
} catch (PDOException | RuntimeException $e) {
    // catches either type
    log_error($e);
}
05

函数

定义函数

PHP 7+ 支持类型化参数和返回类型(int、string、array、?Type 表示可空)。PHP 8+ 增加了命名参数(跳过默认值、重排参数)、联合类型(int|string)和 mixed 类型。可变参数(...$nums)将额外参数收集到数组中。展开运算符(...$arr)将数组解包为参数。按引用传递(&)修改原始值——谨慎使用,因为它使代码更难推理。

php
<?php
// basic function with return type
function add(int $a, int $b): int {
    return $a + $b;
}

// default parameters
function greet(string $name, string $greeting = "Hello"): string {
    return "$greeting, $name!";
}

// named arguments (PHP 8+)
echo greet(name: "Alice", greeting: "Hi");

// variadic functions
function sum(int ...$nums): int {
    return array_sum($nums);
}
echo sum(1, 2, 3, 4);  // 10

// spread operator
$nums = [1, 2, 3];
echo sum(...$nums);  // 6

// pass by reference
function increment(int &$n): void {
    $n++;
}
$x = 5;
increment($x);
echo $x;  // 6

箭头函数与闭包

箭头函数(fn() =>)是简洁的、单表达式闭包,自动按值捕获外部变量。传统闭包(function() use ($var))用于多行体或按引用捕获(&$var)。闭包对于 array_map、array_filter、usort 和事件处理程序至关重要。箭头函数不能有语句(没有 if、for)——对复杂逻辑使用传统闭包。闭包是一等对象(Closure 类)。

php
<?php
// arrow function (PHP 7.4+): single expression, auto-capture
$square = fn($x) => $x * $x;
echo $square(5);  // 25

// auto-captures outer variables by value
$multiplier = 3;
$multiply = fn($x) => $x * $multiplier;
echo $multiply(5);  // 15

// traditional closure (multi-line, explicit capture)
$factor = 10;
$scale = function ($x) use ($factor) {
    return $x * $factor + 1;
};

// capture by reference
$count = 0;
$increment = function () use (&$count) {
    $count++;
};
$increment();
echo $count;  // 1

// closures as callbacks
$nums = [1, 2, 3, 4];
$evens = array_filter($nums, fn($n) => $n % 2 === 0);
$doubled = array_map(fn($n) => $n * 2, $nums);

变量作用域与全局变量

PHP 具有函数级作用域——在函数外部定义的变量如果不使用 'global' 或 $GLOBALS 就无法在函数内部访问。避免使用 'global'——它创建隐藏的依赖关系并使测试困难。改用依赖注入。静态变量在函数调用之间持久存在,但作用域限于函数——适用于缓存/记忆化,但在长时间运行的进程中可能引起问题。闭包必须使用 'use' 显式捕获变量。

php
<?php
$global = "I'm global";

function testScope(): void {
    // echo $global; // ERROR: not in scope
    global $global;  // import global
    echo $global;    // OK

    // $GLOBALS superglobal (alternative)
    echo $GLOBALS['global'];
}

// static variables: persist across calls
function counter(): int {
    static $count = 0;
    return ++$count;
}
echo counter();  // 1
echo counter();  // 2
echo counter();  // 3

// closures don't see outer scope by default
$outer = "hello";
$closure = function () {
    // echo $outer; // ERROR
};
$closure2 = function () use ($outer) {
    echo $outer;  // OK, captured
};

类型声明与严格类型

declare(strict_types=1) 必须是第一条语句——它对整个文件强制执行严格类型检查(无转换)。没有它,PHP 会转换类型(传递给字符串参数的 int 5 变为 '5')。在新代码中始终使用严格类型。PHP 8+ 增加了联合类型、mixed、never(函数永不返回)和 static(返回类)。一等可调用语法(func(...))从任何可调用对象创建闭包——比函数引用更清晰。

php
<?php
// declare strict types (FIRST line of file)
declare(strict_types=1);

function divide(float $a, float $b): float {
    if ($b === 0.0) {
        throw new DivisionByZeroError();
    }
    return $a / $b;
}

// nullable types (?Type or Type|null)
function findUser(?int $id): ?string {
    if ($id === null) return null;
    return "User $id";
}

// union types (PHP 8+)
function process(int|string $input): int|string {
    return is_int($input) ? $input * 2 : strtoupper($input);
}

// return types: void, never, mixed, static
function log(string $msg): void { /* no return */ }
function redirect(): never { header("Location: /"); exit; }

// first-class callable syntax (PHP 8.1+)
$func = strlen(...);  // creates Closure from function
echo $func("hello");  // 5

生成器与 Yield

生成器(带 yield 的函数)惰性产生值——它们不会预先计算所有值,从而节省内存。这对处理大文件或数据集至关重要。yield 暂停函数,返回一个值;当请求下一个值时函数恢复。生成器实现 Iterator,因此它们与 foreach 一起使用。将生成器用于:文件处理、数据库行迭代、无限序列和管道。它们还可以产生 key=>value 对并通过 send() 接受值。

php
<?php
// generator: memory-efficient iteration
function readLines(string $file): Generator {
    $handle = fopen($file, 'r');
    while (($line = fgets($handle)) !== false) {
        yield trim($line);
    }
    fclose($handle);
}

foreach (readLines("large.txt") as $line) {
    echo $line;  // one line at a time, low memory
}

// infinite generator
function fibonacci(): Generator {
    [$a, $b] = [0, 1];
    while (true) {
        yield $a;
        [$a, $b] = [$b, $a + $b];
    }
}

// take first 10
$fib = fibonacci();
for ($i = 0; $i < 10; $i++) {
    echo $fib->current() . " ";
    $fib->next();
}

// yield with key
function pairs(): Generator {
    yield 'a' => 1;
    yield 'b' => 2;
    yield 'c' => 3;
}
06

OOP 与类

类、属性与构造函数

PHP 8 构造函数提升消除了样板代码——将属性声明为构造函数参数。属性可见性:public(任何地方)、protected(类 + 子类)、private(仅类)。readonly(PHP 8.1)防止初始化后修改。self 指当前类;static 指调用类(用于后期静态绑定)。在继承层次结构中使用 static:: 代替 self:: 以实现正确的多态。

php
<?php
class Person {
    // typed properties (PHP 7.4+)
    public string $name;
    protected int $age;
    private string $email;
    public readonly string $id;  // PHP 8.1+

    // constructor promotion (PHP 8+)
    public function __construct(
        string $name,
        int $age,
        string $email = "",
        string $id = ""
    ) {
        $this->name = $name;
        $this->age = $age;
        $this->email = $email;
        $this->id = $id;
    }

    // methods
    public function greet(): string {
        return "Hi, I'm {$this->name}";
    }

    // static method
    public static function create(string $name): self {
        return new self($name, 0);
    }
}

$p = new Person("Alice", 30);
echo $p->greet();
echo Person::create("Bob")->name;

继承与抽象类

抽象类不能被实例化——它们为子类定义模板。抽象方法必须由具体子类实现。PHP 仅支持单继承(一个 extends)。当实现不应改变时,使用 final 防止继承/覆盖。protected 成员可在子类中访问——用于内部 API。如果父类有构造函数,始终调用 parent::__construct()。instanceof 检查类型:if ($dog instanceof Animal)。

php
<?php
abstract class Animal {
    protected string $name;

    public function __construct(string $name) {
        $this->name = $name;
    }

    // abstract method: must be implemented by subclasses
    abstract public function speak(): string;

    // concrete method: inherited as-is
    public function describe(): string {
        return "{$this->name} says {$this->speak()}";
    }
}

class Dog extends Animal {
    public function speak(): string {
        return "Woof";
    }
}

class Cat extends Animal {
    public function speak(): string {
        return "Meow";
    }
}

$dog = new Dog("Rex");
echo $dog->describe();  // Rex says Woof

// final class/method: cannot be extended/overridden
final class Singleton { /* ... */ }

接口与 Trait

接口定义契约——类可以实现多个接口(与单继承不同)。所有接口方法必须是 public。Trait 在不继承的情况下提供代码重用——它们在语言级别是 '复制粘贴'。Trait 可以有属性、方法甚至抽象方法。将 Trait 用于横切关注点(时间戳、日志记录、软删除)。冲突解决:当 trait 有相同方法名时,使用 TraitA::method insteadof TraitB。

php
<?php
// interface: contract (no implementation)
interface Comparable {
    public function compareTo(object $other): int;
}

interface JsonSerializable {
    public function jsonSerialize(): mixed;
}

// a class can implement multiple interfaces
class Product implements Comparable, JsonSerializable {
    public function __construct(public float $price) {}

    public function compareTo(object $other): int {
        return $this->price <=> $other->price;
    }

    public function jsonSerialize(): mixed {
        return ['price' => $this->price];
    }
}

// trait: reusable code (horizontal reuse)
trait Timestampable {
    public DateTime $createdAt;

    public function setCreatedAt(): void {
        $this->createdAt = new DateTime();
    }

    public function age(): DateInterval {
        return $this->createdAt->diff(new DateTime());
    }
}

class Article {
    use Timestampable; // use trait
}

魔术方法

魔术方法是拦截对象操作的特殊方法。__get/__set 实现属性重载(动态属性)。__toString 启用字符串转换。__invoke 使对象可调用。__clone 在克隆时运行(clone $obj)。谨慎使用——它们增加了难以追踪的 '魔术'。__get/__set 对数据传输对象或惰性加载很有用。始终清楚地记录魔术行为。PHP 8.2 弃用动态属性——使用 __get/__set 或 #[AllowDynamicProperties]。

php
<?php
class Magic {
    private array $data = [];

    // called when accessing undefined property
    public function __get(string $name): mixed {
        return $this->data[$name] ?? null;
    }

    // called when setting undefined property
    public function __set(string $name, mixed $value): void {
        $this->data[$name] = $value;
    }

    // called when isset() or empty() on undefined property
    public function __isset(string $name): bool {
        return isset($this->data[$name]);
    }

    // called when object is used as string
    public function __toString(): string {
        return json_encode($this->data);
    }

    // called when object is called as function
    public function __invoke(string $arg): string {
        return "Called with: $arg";
    }

    // called on clone
    public function __clone(): void {
        $this->data = []; // reset on clone
    }
}

$m = new Magic();
$m->foo = "bar";       // __set
echo $m->foo;          // __get -> "bar"
echo $m;               // __toString -> {"foo":"bar"}
echo $m("test");       // __invoke

命名空间与自动加载

命名空间防止类名冲突——类似于 Java 中的包。命名空间必须是第一条语句。use 导入类(带可选别名:use Foo\Bar as B)。PSR-4 自动加载将命名空间映射到文件路径:App\Models\User → src/Models/User.php。Composer 的自动加载器(require 'vendor/autoload.php')自动处理此操作。在现代 PHP 中始终使用命名空间。字符串中的 \\ 是转义的反斜杠(命名空间分隔符)。

php
<?php
// file: src/Models/User.php
namespace App\Models;

use App\Database\Connection;
use App\Exceptions\UserNotFoundException;

class User {
    private Connection $db;

    public function __construct(Connection $db) {
        $this->db = $db;
    }

    public function find(int $id): ?self {
        // ...
        throw new UserNotFoundException("User $id not found");
    }
}

// composer.json (PSR-4 autoloading)
// {
//   "autoload": {
//     "psr-4": { "App\\": "src/" }
//   }
// }

// usage
use App\Models\User;
$user = new User($db);
07

Web、表单与文件 I/O

表单处理与验证

始终在服务器端验证——客户端验证是为了用户体验,而非安全。filter_input/filter_var 与 FILTER_VALIDATE_* 在无效输入时返回 false。在验证前修剪字符串。对数据库插入使用预处理语句。CSRF 令牌防止跨站请求伪造——每个会话生成并验证 POST。bin2hex(random_bytes(32)) 生成加密安全的令牌。切勿信任用户输入——验证、清理和转义。

php
<?php
// process POST form
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $name = trim($_POST['name'] ?? '');
    $email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
    $age = filter_var($_POST['age'] ?? 0, FILTER_VALIDATE_INT);

    $errors = [];

    if (empty($name)) {
        $errors[] = "Name is required";
    } elseif (strlen($name) > 100) {
        $errors[] = "Name too long";
    }

    if ($email === false) {
        $errors[] = "Valid email required";
    }

    if ($age === false || $age < 18) {
        $errors[] = "Must be 18+";
    }

    if (empty($errors)) {
        // process valid data
        // save to database, redirect, etc.
        header("Location: /success");
        exit;
    }
}

// CSRF token
session_start();
$token = bin2hex(random_bytes(32));
$_SESSION['csrf'] = $token;
// in form: <input type="hidden" name="csrf" value="<?= $token ?>">

会话与 Cookie

会话在服务器端存储数据(由会话 ID cookie 标识)。session_start() 必须在任何输出之前调用(或使用 ob_start())。在会话中存储最少的数据——它们消耗服务器内存。对于 cookie,始终设置 secure(仅 HTTPS)、httponly(防止 XSS 访问)和 samesite(CSRF 保护)。正确销毁会话:unset 变量、销毁会话、清除 cookie。对于可扩展的应用程序,使用由 Redis/数据库支持的会话处理程序代替文件。

php
<?php
// start session (must be before any output)
session_start();

// set session data
$_SESSION['user_id'] = 42;
$_SESSION['username'] = "Alice";
$_SESSION['login_time'] = time();

// read session data
$userId = $_SESSION['user_id'] ?? null;

// check if logged in
function isLoggedIn(): bool {
    return isset($_SESSION['user_id']);
}

// destroy session
session_unset();     // clear variables
session_destroy();   // destroy session
setcookie(session_name(), '', time() - 3600, '/'); // clear cookie

// cookies
setcookie("theme", "dark", [
    'expires' => time() + 86400 * 30, // 30 days
    'path' => '/',
    'secure' => true,     // HTTPS only
    'httponly' => true,   // not accessible via JS
    'samesite' => 'Strict', // CSRF protection
]);

// read cookie
$theme = $_COOKIE['theme'] ?? 'light';

文件 I/O

file_get_contents/file_put_contents 适用于小文件。对于大文件,使用 fopen/fread/fwrite 和流。fgetcsv/fputcsv 处理 CSV 格式(包括引号/转义)。json_decode 设置 true 返回关联数组(默认为对象)。始终检查 file_exists 并处理错误(权限、磁盘已满)。对于文件上传,使用 move_uploaded_file() 以确保安全。并发写入时使用 flock() 锁定文件。

php
<?php
// read entire file
$content = file_get_contents("data.txt");
$lines = file("data.txt", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

// write file
file_put_contents("output.txt", "Hello World");
file_put_contents("log.txt", "entry\n", FILE_APPEND); // append

// CSV
$csv = fopen("data.csv", "r");
while (($row = fgetcsv($csv)) !== false) {
    print_r($row); // array of column values
}
fclose($csv);

// write CSV
$out = fopen("export.csv", "w");
fputcsv($out, ["Name", "Email", "Age"]);
fputcsv($out, ["Alice", "[email protected]", 30]);
fclose($out);

// JSON
$data = json_decode(file_get_contents("config.json"), true); // assoc array
file_put_contents("config.json", json_encode($data, JSON_PRETTY_PRINT));

// file info
file_exists("data.txt");  // bool
filesize("data.txt");     // bytes
filemtime("data.txt");    // modification timestamp
is_dir("folder");         // bool

文件上传

文件上传通过 $_FILES 而非 $_POST 传递。始终验证:检查错误代码、用 finfo 验证 MIME 类型(而非 $_FILES['type'],它是客户端提供的且可伪造)、强制执行大小限制并生成安全文件名(切勿信任原始名称)。move_uploaded_file() 是一个安全函数——它验证文件是通过 HTTP POST 上传的。将上传文件存储在 Web 根目录之外或通过 PHP 提供服务以防止直接访问。考虑扫描上传文件以查找恶意软件。

php
<?php
// HTML: <form method="POST" enctype="multipart/form-data">
//   <input type="file" name="document">
//   <input type="submit">
// </form>

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $file = $_FILES['document'];

    // $file contains:
    // ['name'] => original filename
    // ['type'] => MIME type (unreliable!)
    // ['tmp_name'] => temporary path
    // ['error'] => UPLOAD_ERR_OK (0) on success
    // ['size'] => file size in bytes

    if ($file['error'] !== UPLOAD_ERR_OK) {
        die("Upload error: " . $file['error']);
    }

    // validate
    $allowedTypes = ['application/pdf', 'image/jpeg', 'image/png'];
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $mimeType = $finfo->file($file['tmp_name']);

    if (!in_array($mimeType, $allowedTypes)) {
        die("Invalid file type");
    }

    if ($file['size'] > 5 * 1024 * 1024) { // 5MB
        die("File too large");
    }

    // move to permanent location
    $dest = "uploads/" . uniqid() . "_" . $file['name'];
    move_uploaded_file($file['tmp_name'], $dest);
    echo "Uploaded to: $dest";
}

cURL 与 HTTP 请求

cURL 是 PHP 中的标准 HTTP 客户端——它处理 HTTPS、重定向、cookie 和身份验证。始终设置 CURLOPT_RETURNTRANSFER 以字符串形式获取响应(否则会打印出来)。设置超时以避免挂起。对于简单请求,file_get_contents 与 stream_context 一起使用但缺少功能。对于生产环境,使用 Guzzle(composer require guzzlehttp/guzzle)或 Symfony HTTP Client——它们提供更好的 API、重试逻辑和 PSR-18 合规性。

php
<?php
// GET request
$ch = curl_init("https://api.example.com/users");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($httpCode === 200) {
    $data = json_decode($response, true);
    print_r($data);
}

// POST request with JSON
$ch = curl_init("https://api.example.com/users");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => json_encode(['name' => 'Alice']),
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $token,
    ],
]);
$response = curl_exec($ch);
curl_close($ch);

// simpler: file_get_contents with context
$context = stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' => "Content-Type: application/json\r\n",
        'content' => json_encode(['name' => 'Alice']),
    ],
]);
$result = file_get_contents("https://api.example.com/users", false, $context);
08

数据库(PDO)

PDO 连接与基础

PDO(PHP Data Objects)是标准的数据库抽象层——支持 MySQL、PostgreSQL、SQLite 等。始终设置 ERRMODE_EXCEPTION 以进行正确的错误处理,设置 ATTR_EMULATE_PREPARES=false 以使用真正的预处理语句(更好的安全性)。FETCH_ASSOC 返回关联数组(使用 FETCH_OBJ 获取对象,FETCH_CLASS 映射到类)。始终使用 utf8mb4 字符集以获得完整的 Unicode 支持(包括表情符号)。将连接存储在单例或 DI 容器中。

php
<?php
// connect (always use exception mode)
$dsn = "mysql:host=localhost;dbname=test;charset=utf8mb4";
$pdo = new PDO($dsn, "username", "password", [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES => false, // use real prepared statements
]);

// simple query
$stmt = $pdo->query("SELECT * FROM users LIMIT 5");
$users = $stmt->fetchAll(); // array of associative arrays

// fetch one row
$user = $pdo->query("SELECT * FROM users WHERE id = 1")->fetch();

// fetch column
$count = $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();

// execute (no params)
$pdo->exec("DELETE FROM logs WHERE created_at < '2023-01-01'");
$deleted = $pdo->rowCount(); // affected rows

预处理语句(SQL 注入预防)

对于任何带用户输入的查询,预处理语句是强制性的——它们将 SQL 结构与数据分离,使注入不可能。使用 ? 表示位置参数或 :name 表示命名参数。对于 IN 子句,必须动态构建占位符字符串(但值仍然是参数化的)。lastInsertId() 返回最后的自增值。切勿将用户输入连接到 SQL 中——即使使用转义函数。这是 PHP 中的第一安全规则。

php
<?php
// prepared statements: THE way to prevent SQL injection
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ? AND status = ?");
$stmt->execute([$email, 'active']);
$user = $stmt->fetch();

// named parameters (more readable)
$stmt = $pdo->prepare(
    "INSERT INTO users (name, email, age) VALUES (:name, :email, :age)"
);
$stmt->execute([
    ':name' => 'Alice',
    ':email' => '[email protected]',
    ':age' => 30,
]);
$id = $pdo->lastInsertId(); // get auto-increment ID

// IN clause with prepared statements
$ids = [1, 2, 3, 4];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);
$users = $stmt->fetchAll();

// NEVER do this (SQL injection!):
// $pdo->query("SELECT * FROM users WHERE name = '$_GET[name]'");

事务与错误处理

事务确保原子性——所有操作要么全部成功,要么全部失败。beginTransaction/commit/rollBack 包装工作单元。始终在 try/catch 中包装事务并在任何异常时回滚。PDO 在错误时抛出 PDOException(设置 ERRMODE_EXCEPTION)。保持事务简短以减少锁争用。对于嵌套事务,使用保存点或事务管理器。切勿让事务保持打开状态——始终提交或回滚。

php
<?php
try {
    $pdo->beginTransaction();

    $pdo->prepare("UPDATE accounts SET balance = balance - ? WHERE id = ?")
        ->execute([$amount, $fromId]);

    $pdo->prepare("UPDATE accounts SET balance = balance + ? WHERE id = ?")
        ->execute([$amount, $toId]);

    // log transaction
    $pdo->prepare("INSERT INTO transfers (from_id, to_id, amount) VALUES (?, ?, ?)")
        ->execute([$fromId, $toId, $amount]);

    $pdo->commit();
    echo "Transfer complete";
} catch (PDOException $e) {
    $pdo->rollBack(); // undo all changes
    error_log("Transfer failed: " . $e->getMessage());
    throw new RuntimeException("Transfer failed", 0, $e);
}

// check if in transaction
if ($pdo->inTransaction()) {
    $pdo->commit();
}

获取数据模式

根据用例选择正确的获取模式。FETCH_ASSOC 最常见(带列名的数组)。FETCH_CLASS 将行映射到对象——非常适合领域模型。FETCH_KEY_PAIR 创建 id=>value 映射(用于下拉列表)。FETCH_GROUP 按第一列分组行——适用于一对多关系。对于大型结果集,在循环中使用 fetch() 代替 fetchAll() 以节省内存。完成后始终用 $stmt->closeCursor() 关闭游标。

php
<?php
$stmt = $pdo->prepare("SELECT id, name, email FROM users WHERE active = 1");
$stmt->execute();

// fetch modes
$row = $stmt->fetch(PDO::FETCH_ASSOC);  // ['id' => 1, 'name' => 'Alice']
$row = $stmt->fetch(PDO::FETCH_NUM);    // [1, 'Alice', '[email protected]']
$row = $stmt->fetch(PDO::FETCH_BOTH);   // both (default)
$obj = $stmt->fetch(PDO::FETCH_OBJ);    // stdClass with properties

// fetch all
$all = $stmt->fetchAll(PDO::FETCH_ASSOC);

// fetch into class
class User {
    public int $id;
    public string $name;
}
$stmt->setFetchMode(PDO::FETCH_CLASS, User::class);
$users = $stmt->fetchAll(); // array of User objects

// fetch key-value pairs
$pairs = $pdo->query("SELECT id, name FROM users")
    ->fetchAll(PDO::FETCH_KEY_PAIR); // [1 => 'Alice', 2 => 'Bob']

// fetch grouped
$grouped = $pdo->query("SELECT dept, name FROM employees")
    ->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_ASSOC);
// ['IT' => [['name' => 'Alice']], 'HR' => [['name' => 'Bob']]]

数据库最佳实践

仓储模式将数据访问与业务逻辑分离——使代码可测试(模拟 PDO)且可维护。使用依赖注入传递 PDO 连接。切勿为每个查询创建新的 PDO 连接——重用单个连接(或池)。对于高流量应用程序,考虑使用连接池(MySQL 用 ProxySQL,PostgreSQL 用 PgBouncer)。始终使用 EXPLAIN 分析慢查询并添加适当的索引。对于复杂领域,考虑使用 ORM(Doctrine、Eloquent)。

php
<?php
// 1. Connection singleton (or use DI container)
class Database {
    private static ?PDO $instance = null;

    public static function conn(): PDO {
        if (self::$instance === null) {
            self::$instance = new PDO(
                "mysql:host=localhost;dbname=app;charset=utf8mb4",
                "user", "pass",
                [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                 PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
            );
        }
        return self::$instance;
    }
}

// 2. Repository pattern
class UserRepository {
    public function __construct(private PDO $db) {}

    public function findById(int $id): ?array {
        $stmt = $this->db->prepare("SELECT * FROM users WHERE id = ?");
        $stmt->execute([$id]);
        $user = $stmt->fetch();
        return $user ?: null;
    }
}

// 3. Always close statements (or let them go out of scope)
// 4. Use LIMIT for queries that might return huge results
// 5. Index columns used in WHERE, JOIN, ORDER BY
// 6. Use EXPLAIN to analyze slow queries
09

日期/时间与安全

日期与时间

PHP 的日期函数默认使用服务器时区——始终设置 date_default_timezone_set('Asia/Shanghai') 或显式使用 DateTimeZone。DateTime 类是面向对象的,比过程式函数更好地处理时区、间隔和格式化。strtotime() 解析英文日期描述('next Monday'、'+1 month')——方便但在月份边界可能令人意外。对于日期数学,使用 DateTime::diff() 和 DateInterval。始终在数据库中以 UTC 存储日期。

php
<?php
// current time
$now = date('Y-m-d H:i:s');           // 2024-06-15 14:30:00
$timestamp = time();                   // Unix timestamp
$dt = new DateTime();                  // DateTime object

// formatting
echo date('Y-m-d');                    // 2024-06-15
echo date('d/m/Y H:i:s');              // 15/06/2024 14:30:00
echo date('l, F j, Y');                // Saturday, June 15, 2024

// create from string
$dt = new DateTime('2024-01-15');
$dt = DateTime::createFromFormat('d/m/Y', '15/01/2024');

// modify dates
$dt->modify('+1 month');
$dt->modify('-2 days');
$tomorrow = date('Y-m-d', strtotime('tomorrow'));
$nextWeek = date('Y-m-d', strtotime('+1 week'));

// difference
$start = new DateTime('2024-01-01');
$end = new DateTime('2024-12-31');
$diff = $start->diff($end);
echo $diff->days;  // 365

// timezone
$dt = new DateTime('now', new DateTimeZone('Asia/Shanghai'));
$dt->setTimezone(new DateTimeZone('UTC'));

密码哈希与安全

password_hash() 使用 bcrypt(或 Argon2,如果可用)并自动生成盐——切勿自己实现哈希。password_verify() 安全地检查密码与哈希的匹配(常数时间比较以防止时序攻击)。password_needs_rehash() 允许在增加成本因子时升级哈希。对于随机令牌(CSRF、API 密钥、密码重置),始终使用 random_bytes()——而非 rand() 或 mt_rand(),它们是可预测的。使用 hash_hmac 进行消息认证。

php
<?php
// hash password (bcrypt by default)
$hash = password_hash("mypassword", PASSWORD_DEFAULT);
// $2y$10$... (includes algorithm, cost, salt)

// verify password
if (password_verify($input, $hash)) {
    echo "Valid password";
}

// check if hash needs rehash (algorithm upgrade)
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
    $newHash = password_hash($input, PASSWORD_DEFAULT);
    // update stored hash
}

// NEVER use md5() or sha1() for passwords!
// NEVER store plaintext passwords!

// generate secure random
$token = bin2hex(random_bytes(32));    // 64-char hex string
$bytes = random_bytes(16);             // raw bytes
$int = random_int(1, 1000);            // cryptographically secure

// hash for integrity (not passwords)
$checksum = hash('sha256', $data);
$hmac = hash_hmac('sha256', $data, $secretKey);

输出转义与 XSS 预防

XSS 是第一 Web 漏洞——始终根据上下文转义输出。htmlspecialchars() 用于 HTML(ENT_QUOTES 转义单引号和双引号)。urlencode() 用于 URL。json_encode() 带 hex 标志用于 JavaScript 上下文。切勿信任用户输入——在输出时转义,而非输入时(您可能需要在其他地方使用原始数据)。设置 Content-Security-Policy 头作为深度防御。考虑使用默认自动转义的模板引擎(Twig、Blade)。

php
<?php
// XSS: Cross-Site Scripting — always escape output!
$name = $_GET['name']; // could be: <script>alert('xss')</script>

// HTML context
echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
// converts < > " ' & to HTML entities

// in HTML template
?>
<p>Hello, <?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?></p>
<input value="<?= htmlspecialchars($value, ENT_QUOTES, 'UTF-8') ?>">

<?php
// URL context
$url = "https://example.com/search?q=" . urlencode($query);

// JavaScript context
$json = json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT);
?>
<script>
    var data = <?= $json ?>;
</script>

<?php
// Content Security Policy header
header("Content-Security-Policy: default-src 'self'; script-src 'self'");

JSON 与 API 响应

json_encode/decode 是标准的 JSON 函数。始终为 API 响应设置 Content-Type: application/json。使用 JSON_UNESCAPED_UNICODE 保持中文/表情符号可读(否则它们变为 \uXXXX)。json_decode 设置 true 返回关联数组(在 PHP 中更常见)。解码不受信任的 JSON 后始终检查 json_last_error()。对于 REST API,设置适当的 HTTP 状态码(200、201、400、404、500)并使用一致的响应结构。

php
<?php
// encode PHP array/object to JSON
$data = ['name' => 'Alice', 'age' => 30, 'hobbies' => ['reading', 'coding']];
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
// {"name": "Alice", "age": 30, "hobbies": ["reading", "coding"]}

// decode JSON to PHP
$obj = json_decode($json);       // stdClass object
$arr = json_decode($json, true); // associative array

// handle errors
$data = json_decode($badJson, true);
if (json_last_error() !== JSON_ERROR_NONE) {
    throw new RuntimeException("JSON error: " . json_last_error_msg());
}

// API response
header('Content-Type: application/json; charset=utf-8');
http_response_code(200);
echo json_encode([
    'status' => 'success',
    'data' => $users,
    'meta' => ['page' => 1, 'total' => 100],
], JSON_UNESCAPED_UNICODE);

// API request handling
$input = json_decode(file_get_contents('php://input'), true);

Composer 与依赖管理

Composer 是 PHP 的包管理器——对现代 PHP 至关重要。require 指定生产依赖;require-dev 用于开发(测试等)。PSR-4 自动加载将命名空间映射到目录。始终提交 composer.json 和 composer.lock(锁定确切版本)。在生产环境中使用 composer install(从锁文件),使用 composer update 获取最新版本。流行的包:Monolog(日志)、Guzzle(HTTP)、PHPUnit(测试)、Symfony 组件、Laravel 框架。切勿提交 vendor/ 目录。

php
<?php
// composer.json
// {
//   "require": {
//     "monolog/monolog": "^3.0",
//     "guzzlehttp/guzzle": "^7.0"
//   },
//   "autoload": {
//     "psr-4": {"App\\": "src/"}
//   }
// }

// install: composer install
// update:  composer update
// add:    composer require monolog/monolog

// autoload (at the top of your app)
require 'vendor/autoload.php';

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$log = new Logger('app');
$log->pushHandler(new StreamHandler('app.log', Logger::WARNING));
$log->warning('User not found', ['user_id' => 42]);

// environment variables (vlucas/phpdotenv)
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
$dbHost = $_ENV['DB_HOST'];
10

会话与 Cookie

设置与读取 Cookie

Cookie 在用户浏览器中存储少量数据。setcookie() 必须在任何 HTML 输出之前调用(它设置 HTTP 头)。httponly 标志防止 JavaScript 读取 cookie(缓解 XSS),secure 确保它仅通过 HTTPS 发送。Cookie 随每个请求发送到匹配的域/路径,因此不要存储大数据。对于敏感数据,改用会话(数据保留在服务器上)。始终验证和清理 cookie 值——它们来自客户端且可能被篡改。

php
// Set a cookie (must be before any output!)
setcookie("user", "Alice", time() + 3600, "/", "", true, true);
// params: name, value, expiry, path, domain, secure, httponly

// Read cookies (from $_COOKIE superglobal)
if (isset($_COOKIE['user'])) {
    echo "Welcome back, " . htmlspecialchars($_COOKIE['user']);
}

// Delete a cookie: set expiry in the past
setcookie("user", "", time() - 3600, "/");

// Cookie limitations:
// - Sent with every HTTP request (adds bandwidth)
// - Max 4KB per cookie, ~50 per domain
// - Stored client-side (don't store sensitive data!)
// - httponly=true prevents JavaScript access (XSS protection)
// - secure=true sends only over HTTPS

会话管理

会话在服务器上存储数据,由存储在 cookie 中的会话 ID 标识。与 cookie 不同,会话数据对用户不可见(对敏感数据更安全)。session_start() 必须在使用会话的每个页面上调用,在任何输出之前。session_regenerate_id(true) 通过创建新 ID 并删除旧 ID 来防止会话固定攻击——在登录后调用它。始终在注销时销毁会话。对于大规模应用程序,将会话存储在 Redis/数据库中而非文件(默认),以便在多个服务器之间共享。

php
// Start or resume a session (must be before output)
session_start();

// Store data in the session (lives on server, identified by session ID cookie)
$_SESSION['user_id'] = 42;
$_SESSION['username'] = 'alice';
$_SESSION['cart'] = ['item1', 'item2'];

// Read session data
if (isset($_SESSION['user_id'])) {
    echo "User: " . $_SESSION['username'];
}

// Remove a single session variable
unset($_SESSION['cart']);

// Destroy the entire session
session_unset();   // clear all variables
session_destroy(); // destroy session data on server
setcookie(session_name(), '', time() - 3600, '/'); // clear session cookie

// Regenerate ID to prevent session fixation attacks
session_regenerate_id(true);

会话配置(php.ini)

会话安全配置至关重要。cookie_httponly 防止 XSS 窃取会话 ID。cookie_secure 确保会话仅在 HTTPS 上工作。SameSite=Strict 防止 CSRF(cookie 不会在跨站请求中发送)。use_strict_mode 拒绝未初始化的会话 ID。gc_maxlifetime 设置不活动超时。对于多服务器部署,实现自定义 SessionHandlerInterface 将会话存储在数据库或 Redis 中——默认的基于文件的存储无法跨服务器工作。始终在生产环境中配置这些设置。

php
// php.ini session settings (or ini_set at runtime)
ini_set('session.cookie_lifetime', 0);     // 0 = until browser closes
ini_set('session.cookie_httponly', 1);     // prevent JS access
ini_set('session.cookie_secure', 1);       // HTTPS only
ini_set('session.cookie_samesite', 'Strict'); // CSRF protection
ini_set('session.use_strict_mode', 1);     // reject uninitialized IDs
ini_set('session.gc_maxlifetime', 1800);   // 30 min inactivity timeout

// Custom session handler (store in database/Redis)
class MySessionHandler implements SessionHandlerInterface {
    public function open($savePath, $sessionName) { /* connect DB */ }
    public function close() { /* close DB */ }
    public function read($id) { /* fetch from DB */ }
    public function write($id, $data) { /* save to DB */ }
    public function destroy($id) { /* delete from DB */ }
    public function gc($maxlifetime) { /* cleanup old sessions */ }
}
session_set_save_handler(new MySessionHandler(), true);
session_start();

闪存消息(一次性通知)

闪存消息是基于会话的通知,显示一次(例如,'保存成功!')然后自动清除。模式:在 POST 请求中将消息存储在 $_SESSION 中,然后在下一个 GET 请求中读取并 unset 它。这实现了 Post/Redirect/Get(PRG)模式——表单提交后,重定向以防止刷新时重新提交,并在重定向页面上显示闪存消息。Laravel($request->session()->flash())和 Symfony 等框架提供内置的闪存消息支持。

php
// Flash messages: shown once, then deleted
session_start();

// Set a flash message (e.g., after form submission)
$_SESSION['flash'] = [
    'type' => 'success',
    'message' => 'Item added to cart!'
];

// On the next page, display and clear:
if (isset($_SESSION['flash'])) {
    $flash = $_SESSION['flash'];
    unset($_SESSION['flash']);  // delete so it shows only once
    echo "<div class='alert alert-{$flash['type']}'>{$flash['message']}</div>";
}

// Helper function pattern
function flash(string $key, string $value = null): ?string {
    if ($value !== null) {
        $_SESSION['flash_' . $key] = $value;
        return null;
    }
    $val = $_SESSION['flash_' . $key] ?? null;
    unset($_SESSION['flash_' . $key]);
    return $val;
}

会话安全最佳实践

会话安全需要多层防护。登录后重新生成会话 ID 以防止固定攻击(攻击者设置已知会话 ID)。跟踪 last_activity 以实现空闲超时。可选地,将会话绑定到 IP/用户代理以检测劫持(注意:这可能在更改 IP 的移动网络上导致误报)。切勿在会话中存储密码或信用卡号——仅存储用户 ID,并在需要时从数据库获取敏感数据。始终在生产环境中使用 HTTPS 以防止会话 ID 拦截。

php
// 1. Always start session before output
session_start();

// 2. Regenerate ID after login (prevent fixation)
if ($loginSuccessful) {
    session_regenerate_id(true);
    $_SESSION['user_id'] = $user->id;
}

// 3. Validate session on each request
if (isset($_SESSION['user_id'])) {
    // Check session hasn't expired
    if (isset($_SESSION['last_activity']) &&
        time() - $_SESSION['last_activity'] > 1800) {
        session_unset();
        session_destroy();
        header('Location: /login');
        exit;
    }
    $_SESSION['last_activity'] = time();

    // Optional: verify IP/user-agent hasn't changed (anti-hijacking)
    if ($_SESSION['ip'] !== $_SERVER['REMOTE_ADDR']) {
        session_destroy();
        exit('Session hijacking detected');
    }
}

// 4. Use prepared statements for session storage in DB
// 5. Set secure cookie flags (see previous example)
// 6. Never store sensitive data (passwords, CC numbers) in sessions
11

REST API 开发

处理 JSON 请求与响应

REST API 交换 JSON。与表单提交(填充 $_POST)不同,JSON 请求必须从 php://input 读取并用 json_decode 解码。始终为响应设置 Content-Type: application/json 并使用 http_response_code() 设置正确的 HTTP 状态码。验证所有输入——json_decode 不保证预期结构。使用空合并运算符(??)进行安全访问。JSON_PRETTY_PRINT 对调试有用,但在生产环境中省略它以获得更小的负载。

php
// Get JSON from request body (not $_POST for JSON!)
$json = file_get_contents('php://input');
$data = json_decode($json, true);  // true = associative array

if (json_last_error() !== JSON_ERROR_NONE) {
    http_response_code(400);
    echo json_encode(['error' => 'Invalid JSON']);
    exit;
}

// Process the data
$name = $data['name'] ?? '';
$email = filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL);

// Return JSON response
header('Content-Type: application/json');
http_response_code(200);  // 200 OK, 201 Created, 400 Bad Request, etc.
echo json_encode([
    'success' => true,
    'data' => ['id' => 1, 'name' => $name],
    'message' => 'User created'
], JSON_PRETTY_PRINT);

路由与 HTTP 方法

REST API 将 HTTP 方法映射到 CRUD 操作:GET(读取)、POST(创建)、PUT/PATCH(更新)、DELETE(删除)。路由将方法 + URL 路径匹配到处理程序。使用 preg_match 处理参数化路由(例如,/api/users/42)。在生产环境中,使用路由库(FastRoute、Symfony Routing)或框架(Laravel、Slim)以获得更清晰的路由、中间件和依赖注入。始终返回适当的 HTTP 状态码:200(OK)、201(已创建)、204(无内容)、400(错误请求)、404(未找到)、500(服务器错误)。

php
// Simple REST router based on method + path
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$path = trim($path, '/');

// Route table: method => [pattern => handler]
switch (true) {
    case ($method === 'GET' && $path === 'api/users'):
        echo json_encode(getAllUsers());
        break;
    case ($method === 'GET' && preg_match('#^api/users/(\d+)$#', $path, $m)):
        echo json_encode(getUser((int)$m[1]));
        break;
    case ($method === 'POST' && $path === 'api/users'):
        $data = json_decode(file_get_contents('php://input'), true);
        echo json_encode(createUser($data));
        http_response_code(201);
        break;
    case ($method === 'PUT' && preg_match('#^api/users/(\d+)$#', $path, $m)):
        $data = json_decode(file_get_contents('php://input'), true);
        echo json_encode(updateUser((int)$m[1], $data));
        break;
    case ($method === 'DELETE' && preg_match('#^api/users/(\d+)$#', $path, $m)):
        deleteUser((int)$m[1]);
        http_response_code(204);  // No Content
        break;
    default:
        http_response_code(404);
        echo json_encode(['error' => 'Not found']);
}

API 身份验证(JWT)

JWT 启用无状态身份验证——服务器不需要存储会话。令牌包含有效负载(用户 ID、过期时间),用密钥签名。客户端在 Authorization 头中发送令牌(Bearer 令牌)。服务器验证签名以确保令牌未被篡改。JWT 非常适合 API 和微服务(不需要共享会话存储)。但是,JWT 在过期前无法撤销——使用短过期时间和刷新令牌策略。在生产环境中,使用 firebase/php-jwt 库而非自己实现加密。切勿在 JWT 有效负载中存储敏感数据——它只是 base64 编码,而非加密。

php
// JWT (JSON Web Token) authentication flow
// 1. Login: verify credentials, issue JWT
function login($email, $password) {
    $user = findUserByEmail($email);
    if ($user && password_verify($password, $user['password_hash'])) {
        // Create JWT: header.payload.signature
        $header = base64url_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
        $payload = base64url_encode(json_encode([
            'user_id' => $user['id'],
            'exp' => time() + 3600  // expires in 1 hour
        ]));
        $signature = hash_hmac('sha256', "$header.$payload", SECRET_KEY, true);
        $jwt = "$header.$payload." . base64url_encode($signature);
        return $jwt;
    }
    return null;
}

// 2. Verify JWT on protected routes
function verifyJWT($token) {
    $parts = explode('.', $token);
    if (count($parts) !== 3) return false;
    [$header, $payload, $signature] = $parts;
    $expected = base64url_encode(
        hash_hmac('sha256', "$header.$payload", SECRET_KEY, true)
    );
    if (!hash_equals($expected, $signature)) return false;
    $data = json_decode(base64url_decode($payload), true);
    return ($data['exp'] ?? 0) > time() ? $data : false;
}

// Use firebase/php-jwt library in production!

输入验证与清理

输入验证对 API 安全至关重要。PHP 的 filter_var 提供内置验证器(FILTER_VALIDATE_EMAIL、FILTER_VALIDATE_INT、FILTER_VALIDATE_URL),带最小/最大范围等选项。始终在服务器端验证——客户端验证是为了用户体验,而非安全。输出 HTML 时用 htmlspecialchars 清理字符串以防止 XSS。对于 JSON API,验证错误返回 422(不可处理实体)并附带描述性消息。考虑使用验证库(Respect/Validation、Symfony Validator)处理复杂规则。切勿信任用户输入——验证类型、长度、格式和业务规则。

php
// Validate and sanitize API input
function validateUserInput(array $data): array {
    $errors = [];

    // Required fields
    if (empty($data['name'])) {
        $errors[] = 'Name is required';
    }

    // Email validation
    $email = filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL);
    if (!$email) {
        $errors[] = 'Valid email is required';
    }

    // Age: integer between 18 and 120
    $age = filter_var($data['age'] ?? null, FILTER_VALIDATE_INT, [
        'options' => ['min_range' => 18, 'max_range' => 120]
    ]);
    if ($age === false) {
        $errors[] = 'Age must be between 18 and 120';
    }

    // String sanitization (remove tags, trim)
    $name = htmlspecialchars(trim($data['name'] ?? ''), ENT_QUOTES, 'UTF-8');

    // URL validation
    $website = filter_var($data['website'] ?? '', FILTER_VALIDATE_URL);

    return ['errors' => $errors, 'data' => compact('email', 'age', 'name')];
}

$result = validateUserInput($input);
if (!empty($result['errors'])) {
    http_response_code(422);  // Unprocessable Entity
    echo json_encode(['errors' => $result['errors']]);
    exit;
}

CORS(跨源资源共享)

CORS 控制哪些域可以从浏览器访问您的 API。浏览器对非简单请求(PUT/DELETE、自定义头)发送预检 OPTIONS 请求。您的服务器必须用适当的 Access-Control-Allow-* 头响应。为了安全,指定确切的源而非 '*'(特别是带凭据时)。如果 API 使用 cookie 或 Authorization 头,需要 Access-Control-Allow-Credentials: true。Vary: Origin 告诉缓存响应因源而异。配置错误的 CORS 可能使您的 API 暴露给任何网站——始终白名单信任的源。

php
// Enable CORS for API requests from browsers
header('Access-Control-Allow-Origin: https://example.com');  // specific origin
// OR: header('Access-Control-Allow-Origin: *');  // any origin (less secure)
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Allow-Credentials: true');  // for cookies/auth
header('Access-Control-Max-Age: 86400');  // cache preflight for 24h

// Handle preflight OPTIONS request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit;
}

// Dynamic origin (check against whitelist)
$allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowedOrigins)) {
    header('Access-Control-Allow-Origin: ' . $origin);
    header('Vary: Origin');  // important for caching
}
12

安全(XSS、CSRF、SQL 注入)

防止 XSS(跨站脚本攻击)

XSS 在未转义的情况下将不受信任的数据插入 HTML 时发生,允许攻击者在受害者浏览器中执行 JavaScript。修复:始终用 htmlspecialchars 转义输出(将 <、>、&、"、' 转换为 HTML 实体)。不同上下文需要不同转义:HTML 主体(htmlspecialchars)、HTML 属性(htmlspecialchars 带 ENT_QUOTES)、JavaScript(json_encode)、URL(urlencode)。内容安全策略(CSP)头通过限制脚本可以从哪里加载来增加深度防御。切勿对用户输入使用 eval()、innerHTML 或 document.write()。Twig 和 Blade 等框架默认自动转义。

php
// XSS: attacker injects malicious JavaScript into your page
// BAD: outputting user input without escaping
echo "<p>" . $_GET['name'] . "</p>";
// If name = <script>alert('xss')</script>, it executes!

// GOOD: escape output with htmlspecialchars
echo "<p>" . htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8') . "</p>";
// & < > " ' are converted to HTML entities

// For HTML attributes:
echo '<input value="' . htmlspecialchars($value, ENT_QUOTES) . '">';

// For JavaScript context (JSON encode):
echo '<script>var name = ' . json_encode($name) . ';</script>';

// For URL parameters:
echo '<a href="?q=' . urlencode($query) . '">Search</a>';

// Content Security Policy (CSP) header — defense in depth
header("Content-Security-Policy: default-src 'self'; script-src 'self'");

// Use prepared statements (prevents SQL injection too)

防止 CSRF(跨站请求伪造)

CSRF 诱骗已认证用户的浏览器在不知情的情况下向您的站点发送请求(例如,资金转账)。防御:在表单中包含攻击者无法猜测的不可预测令牌。令牌存储在会话中并在提交时验证。使用 hash_equals() 进行时序安全比较(防止时序攻击)。对于 AJAX/API 调用,SameSite=Strict cookie 和要求自定义头(如 X-Requested-With)提供保护。GET 请求永远不应修改数据(它们可以通过图像标签或链接触发)。Laravel 和 Symfony 等框架有内置的 CSRF 中间件。

php
// CSRF: attacker tricks a logged-in user into submitting a form
// to your site (using their session cookie)

// Defense: anti-CSRF tokens
session_start();

// Generate a token (once per session)
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

// Include token in forms as a hidden field
echo '<form method="POST" action="/transfer">';
echo '<input type="hidden" name="csrf_token" value="' . $_SESSION['csrf_token'] . '">';
echo '<input type="text" name="amount">';
echo '<button type="submit">Transfer</button>';
echo '</form>';

// Verify token on POST/PUT/DELETE requests
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $token = $_POST['csrf_token'] ?? '';
    if (!hash_equals($_SESSION['csrf_token'], $token)) {
        http_response_code(403);
        die('CSRF token validation failed');
    }
    // Process the form...
}

// For APIs: use SameSite cookies + custom headers
// (browsers block cross-origin requests without explicit CORS permission)

防止 SQL 注入

SQL 注入是第一 Web 漏洞——它让攻击者读取/修改/删除您的整个数据库。通用修复:预处理语句(参数化查询)。查询结构和数据分开发送,因此用户输入永远不会被解释为 SQL。切勿将用户输入连接到查询中。PDO 的 prepare/execute 自动处理转义。用类型绑定参数(PDO::PARAM_INT、PDO::PARAM_STR)。对于带可变项的 IN 子句,动态生成占位符。将 PDO::ATTR_EMULATE_PREPARES 设置为 false 以使用真正的服务器端预处理语句(更好的安全性)。

php
// SQL injection: attacker manipulates queries via unescaped input
// BAD: string concatenation
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";
// If id = "1 OR 1=1", returns ALL users!
// If id = "1; DROP TABLE users; --", deletes the table!

// GOOD: prepared statements with parameter binding
$pdo = new PDO('mysql:host=localhost;dbname=myapp', $user, $pass);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);  // real prepared statements

$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$user = $stmt->fetch(PDO::FETCH_ASSOC);

// Multiple parameters:
$stmt = $pdo->prepare('INSERT INTO users (name, email, age) VALUES (:name, :email, :age)');
$stmt->execute([
    ':name' => $name,
    ':email' => $email,
    ':age' => $age
]);

// IN clause with variable arguments:
$ids = [1, 2, 3];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);

密码哈希与身份验证

password_hash() 使用 bcrypt(或 Argon2)带随机盐——密码存储的行业标准。盐嵌入在哈希中,因此您无需单独管理。password_verify() 安全地将输入与存储的哈希比较(时序安全)。password_needs_rehash() 允许在增加成本因子或切换算法时升级哈希——它检查哈希是否匹配当前设置并在下次登录时重新哈希。切勿使用 MD5、SHA1 或明文存储密码——它们容易被破解。强制执行强密码策略,但优先考虑长度而非复杂性(NIST 建议最少 8 个字符)。

php
// NEVER store plain-text or MD5/SHA1 passwords!
// Use password_hash() (bcrypt/argon2 by default)

// Hash a password (on registration)
$password = $_POST['password'];
$hash = password_hash($password, PASSWORD_DEFAULT);
// PASSWORD_DEFAULT = bcrypt (or argon2id in PHP 7.3+)
// Store $hash in the database

// Verify a password (on login)
if (password_verify($inputPassword, $storedHash)) {
    // Password is correct
    session_regenerate_id(true);
    $_SESSION['user_id'] = $user['id'];
} else {
    echo "Invalid credentials";
}

// Rehash if algorithm was upgraded (migration)
if (password_verify($input, $hash) &&
    password_needs_rehash($hash, PASSWORD_DEFAULT)) {
    $newHash = password_hash($input, PASSWORD_DEFAULT);
    // Update database with $newHash
}

// Password requirements validation
if (strlen($password) < 8 ||
    !preg_match('/[A-Z]/', $password) ||
    !preg_match('/[a-z]/', $password) ||
    !preg_match('/[0-9]/', $password)) {
    echo "Password must be 8+ chars with upper, lower, and number";
}

文件上传安全

文件上传是主要攻击向量。切勿信任 $_FILES['type'](由浏览器设置,容易伪造)——使用 finfo 检测真实 MIME 类型。切勿使用用户提供的文件名(它可能包含路径遍历,如 ../../script.php)——生成随机名称。将上传文件存储在 Web 根目录之外或禁用 PHP 执行的目录中。对于图像,重新编码它们(imagecreatefromjpeg + imagejpeg)以去除隐藏在 EXIF 数据中的嵌入式 PHP 代码。限制文件大小以防止拒绝服务。验证扩展名、MIME 类型和魔术字节。考虑使用防病毒软件(ClamAV)扫描上传文件以获得额外安全。

php
// Secure file upload handling
$upload = $_FILES['avatar'];

// 1. Check for upload errors
if ($upload['error'] !== UPLOAD_ERR_OK) {
    die('Upload failed: error ' . $upload['error']);
}

// 2. Validate file size
$maxSize = 2 * 1024 * 1024;  // 2MB
if ($upload['size'] > $maxSize) {
    die('File too large (max 2MB)');
}

// 3. Validate MIME type (don't trust $_FILES['type']!)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($upload['tmp_name']);
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($mimeType, $allowedTypes)) {
    die('Invalid file type');
}

// 4. Generate a safe filename (never use user-supplied name)
$safeName = bin2hex(random_bytes(16)) . '.jpg';

// 5. Store OUTSIDE the web root (or in a non-executable dir)
$dest = __DIR__ . '/uploads/' . $safeName;
if (!move_uploaded_file($upload['tmp_name'], $dest)) {
    die('Failed to save file');
}

// 6. For images: re-encode to strip malicious metadata
$img = imagecreatefromjpeg($dest);
imagejpeg($img, $dest, 90);  // re-encode strips embedded PHP/scripts
13

cURL 与 HTTP 请求

基本 cURL GET 与 POST

cURL 是 PHP 最强大的 HTTP 客户端,支持 GET、POST、自定义方法、头、cookie 和 SSL。始终设置 CURLOPT_RETURNTRANSFER 以字符串形式获取响应(否则直接回显)。CURLOPT_TIMEOUT 防止在慢速服务器上挂起。对于带 JSON 的 POST,显式设置 Content-Type 和 Content-Length 头。检查 curl_errno() 获取连接错误,curl_getinfo(CURLINFO_HTTP_CODE) 获取 HTTP 状态。始终用 curl_close() 关闭 cURL 句柄以释放资源。对于更简单的代码,考虑 Guzzle(一个带更清晰 API 的 cURL 包装器)。

php
// GET request
$ch = curl_init('https://api.example.com/users');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
    echo 'Error: ' . curl_error($ch);
}
curl_close($ch);
$data = json_decode($response, true);

// POST request with JSON body
$ch = curl_init('https://api.example.com/users');
$payload = json_encode(['name' => 'Alice', 'email' => '[email protected]']);
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $token,
        'Content-Length: ' . strlen($payload)
    ],
    CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);

带身份验证与 Cookie 的 cURL

cURL 支持多种身份验证方法。CURLOPT_USERPWD 设置 HTTP 基本认证。Bearer 令牌放在 Authorization 头中。对于基于 cookie 的会话(如登录网站),使用 CURLOPT_COOKIEJAR 保存 cookie,CURLOPT_COOKIEFILE 在后续请求中发送它们——这跨多个 cURL 调用维护会话。使用临时文件存储 cookie 并用 unlink() 清理。对于 API 调用,优先使用基于令牌的认证(Bearer)而非 cookie。始终使用 HTTPS(cURL 默认验证 SSL——在生产环境中不要禁用 CURLOPT_SSL_VERIFYPEER)。

php
// Basic Auth
$ch = curl_init('https://api.example.com/data');
curl_setopt($ch, CURLOPT_USERPWD, 'username:password');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);

// Bearer token (API key)
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer ' . $apiKey,
    'Accept: application/json'
]);

// Cookie-based session (login then reuse cookies)
$cookieFile = tempnam(sys_get_temp_dir(), 'cookie');

// Step 1: Login (saves cookies)
$ch = curl_init('https://example.com/login');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query(['user' => 'alice', 'pass' => 'secret']),
    CURLOPT_COOKIEJAR => $cookieFile,   // save cookies here
    CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
curl_close($ch);

// Step 2: Access protected page (sends saved cookies)
$ch = curl_init('https://example.com/dashboard');
curl_setopt_array($ch, [
    CURLOPT_COOKIEFILE => $cookieFile,  // send cookies from here
    CURLOPT_RETURNTRANSFER => true,
]);
$dashboard = curl_exec($ch);
curl_close($ch);
unlink($cookieFile);  // cleanup

文件下载与流式传输

对于大文件下载,使用 CURLOPT_FILE 直接写入文件句柄——这避免将整个响应加载到内存中。CURLOPT_FOLLOWLOCATION 跟随 HTTP 重定向(301、302)。对于流式传输(例如,实时数据),使用 CURLOPT_WRITEFUNCTION 在数据块到达时处理它们——适用于流式传输数据的 API。CURLOPT_PROGRESSFUNCTION 监控下载/上传进度。为大文件设置宽裕的 CURLOPT_TIMEOUT。对于非常大的上传,使用 CURLOPT_INFILE 从文件流式传输而非加载到内存中。始终关闭文件句柄和 cURL 句柄以防止资源泄漏。

php
// Download a file to disk
$ch = curl_init('https://example.com/large-file.zip');
$fp = fopen('downloaded.zip', 'w');
curl_setopt_array($ch, [
    CURLOPT_FILE => $fp,           // write directly to file
    CURLOPT_FOLLOWLOCATION => true, // follow redirects
    CURLOPT_TIMEOUT => 300,        // 5 min for large files
]);
curl_exec($ch);
fclose($fp);
curl_close($ch);

// Stream response with callback (progress monitoring)
$ch = curl_init('https://example.com/stream');
curl_setopt_array($ch, [
    CURLOPT_WRITEFUNCTION => function($ch, $chunk) {
        echo $chunk;  // stream to output
        ob_flush();
        flush();
        return strlen($chunk);
    },
    CURLOPT_PROGRESSFUNCTION => function($ch, $dlTotal, $dlNow, $ulTotal, $ulNow) {
        if ($dlTotal > 0) {
            printf("
Progress: %.1f%%", ($dlNow / $dlTotal) * 100);
        }
        return 0;
    },
]);
curl_exec($ch);
curl_close($ch);

并发请求(Multi cURL)

curl_multi_exec 并行运行多个 HTTP 请求——当需要来自多个端点的数据时,比顺序请求快得多。模式:创建多句柄,添加单个 cURL 句柄,在循环中执行多句柄(curl_multi_exec + curl_multi_select 以提高效率),然后收集结果。这对于聚合来自多个 API 的数据、预取资源或批量操作很有用。对于更高级的并发,考虑 ReactPHP 或 Amp(异步 PHP 框架)。注意 multi-cURL 仍然会阻塞 PHP 进程——对于真正的异步,使用事件循环或消息队列。

php
// Fetch multiple URLs in parallel (much faster than sequential)
$urls = [
    'https://api.example.com/users',
    'https://api.example.com/posts',
    'https://api.example.com/comments'
];

$multi = curl_multi_init();
$handles = [];

// Create individual handles
foreach ($urls as $i => $url) {
    $handles[$i] = curl_init($url);
    curl_setopt($handles[$i], CURLOPT_RETURNTRANSFER, true);
    curl_multi_add_handle($multi, $handles[$i]);
}

// Execute all requests concurrently
$active = null;
do {
    $status = curl_multi_exec($multi, $active);
    if ($active) {
        curl_multi_select($multi);  // wait for activity
    }
} while ($active && $status === CURLM_OK);

// Collect results
$responses = [];
foreach ($handles as $i => $ch) {
    $responses[$i] = json_decode(curl_multi_getcontent($ch), true);
    curl_multi_remove_handle($multi, $ch);
    curl_close($ch);
}
curl_multi_close($multi);

// $responses[0], [1], [2] now contain all three API responses

使用 Guzzle(现代 HTTP 客户端)

Guzzle 是现代 PHP 的标准 HTTP 客户端——比原始 cURL 清晰得多。它提供流畅的 API、PSR-7 兼容的请求/响应对象、中间件(日志、重试)和通过 Promise 的异步请求。'json' 选项自动编码正文并设置 Content-Type。getAsync/postAsync 返回 Promise 用于并发请求,无需 multi-cURL 复杂性。异常处理内置:RequestException 捕获 HTTP 错误(4xx、5xx)。Guzzle 被大多数框架使用(Laravel 的 HTTP 客户端包装 Guzzle)。通过 Composer 安装:composer require guzzlehttp/guzzle。

php
// Guzzle is the most popular PHP HTTP client (composer require guzzlehttp/guzzle)
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

$client = new Client([
    'base_uri' => 'https://api.example.com',
    'timeout'  => 30,
    'headers'  => ['Accept' => 'application/json'],
]);

// GET request
$response = $client->get('/users/42');
$body = json_decode($response->getBody(), true);
echo $response->getStatusCode();  // 200

// POST with JSON
$response = $client->post('/users', [
    'json' => ['name' => 'Alice', 'email' => '[email protected]'],
    'headers' => ['Authorization' => 'Bearer ' . $token],
]);

// Concurrent requests (Promise-based)
$promises = [
    'users' => $client->getAsync('/users'),
    'posts' => $client->getAsync('/posts'),
];
$results = GuzzleHttp\Promise\Utils::settle($promises)->wait();

// Error handling with try/catch
try {
    $response = $client->get('/nonexistent');
} catch (RequestException $e) {
    echo $e->getResponse()->getStatusCode();  // 404
}
14

DateTime 深入

创建与操作 DateTime

DateTime 是 PHP 强大的日期/时间类。DateTimeImmutable 优于 DateTime——它在修改时返回新对象,防止意外变异错误(当同一日期在多处使用时至关重要)。createFromFormat 解析自定义格式。modify() 接受相对表达式如 '+1 week' 或 'last day of next month'。始终显式指定时区以避免依赖服务器配置的行为。对于日期数学(添加间隔),使用 DateInterval('P1D' = 1 天,'P2W' = 2 周,'PT2H' = 2 小时)与 add()/sub()。

php
$now = new DateTime();  // current time
$now->setTimezone(new DateTimeZone('Asia/Shanghai'));

// From string (many formats supported)
$date = new DateTime('2024-06-15 14:30:00');
$date = new DateTime('first day of next month');
$date = new DateTime('next Friday');

// From format (precise parsing)
$date = DateTime::createFromFormat('d/m/Y', '15/06/2024');

// Modify dates with relative expressions
$date->modify('+1 week');
$date->modify('-2 days');
$date->modify('last day of this month');
$date->modify('+1 year 3 months');

// Immutable version (recommended — doesn't modify original)
$dt = new DateTimeImmutable('2024-06-15');
$next = $dt->modify('+1 day');  // $dt unchanged, $next is new object
echo $dt->format('Y-m-d');   // 2024-06-15 (unchanged!)
echo $next->format('Y-m-d'); // 2024-06-16

格式化与时区

format() 使用模式字母自定义输出——Y(4 位年)、m(2 位月)、d(2 位日)、H(24 小时)、i(分钟)、s(秒)。对于 ISO 8601(用于 API),使用 'Y-m-d\TH:i:sP' 或 'c' 快捷方式。时区转换:用源时区创建,然后 setTimezone 转换。始终在数据库中以 UTC 存储日期,仅在显示时转换为用户时区。PHP 的时区数据库很全面(包括夏令时规则)。使用 DateTimeZone::listIdentifiers() 获取所有支持的时区。format() 中的 'T' 转义字符输出字面 'T'(用于 ISO 8601)。

php
// Format codes (most common):
// Y=2024  y=24  m=06  n=6  d=15  j=15
// H=14 (24h)  h=02 (12h)  i=30  s=00  A=PM  a=pm
// D=Mon  l=Monday  M=Jun  F=June  N=1 (Mon=1..Sun=7)

$date = new DateTime('2024-06-15 14:30:00');
echo $date->format('Y-m-d H:i:s');  // 2024-06-15 14:30:00
echo $date->format('l, F j, Y');    // Saturday, June 15, 2024
echo $date->format('Y-m-d\TH:i:sP'); // 2024-06-15T14:30:00+08:00 (ISO 8601)

// Timezone conversion
$utc = new DateTime('2024-06-15 14:00:00', new DateTimeZone('UTC'));
$utc->setTimezone(new DateTimeZone('Asia/Shanghai'));
echo $utc->format('H:i');  // 22:00 (UTC+8)

// List supported timezones
$tzs = DateTimeZone::listIdentifiers();
// ['UTC', 'America/New_York', 'Asia/Shanghai', ...]

// Get timezone offset
$tz = new DateTimeZone('America/Los_Angeles');
$offset = $tz->getOffset(new DateTime());  // seconds from UTC

日期间隔与差异

DateInterval 使用 ISO 8601 持续时间格式(P1Y2M3DT4H5M6S)表示时间持续时间。add() 和 sub() 将间隔应用于日期。diff() 返回表示两个日期之间差异的 DateInterval——'days' 属性给出总天数,而 'y'、'm'、'd' 给出组件分解。'invert' 属性指示方向(如果第二个日期更早则为 1)。注意月份算术:将 'P1M' 加到 1 月 31 日得到 3 月 2 日(2 月有 28-29 天),而非 2 月 31 日。对于工作日计算,手动迭代并跳过周末/假日或使用 nesbot/carbon 等库。

php
// DateInterval: represents a duration
$interval = new DateInterval('P1Y2M3DT4H5M6S');
// P = period, 1Y = 1 year, 2M = 2 months, 3D = 3 days
// T = time separator, 4H = 4 hours, 5M = 5 minutes, 6S = 6 seconds

$date = new DateTime('2024-06-15');
$date->add(new DateInterval('P1M'));  // +1 month → 2024-07-15
$date->sub(new DateInterval('P10D')); // -10 days → 2024-07-05

// Difference between two dates
$d1 = new DateTime('2024-01-01');
$d2 = new DateTime('2024-12-31');
$diff = $d1->diff($d2);
echo $diff->days;     // 365 (total days)
echo $diff->m;        // 0 (months component)
echo $diff->y;        // 0 (years component)
echo $diff->format('%a days');  // 365 days
echo $diff->format('%y years, %m months, %d days');  // 0 years, 11 months, 30 days

// Check if inverted (d2 < d1)
echo $diff->invert;   // 0 if d2 >= d1, 1 if d2 < d1

DatePeriod(迭代日期范围)

DatePeriod 以指定间隔迭代日期范围——非常适合生成日历、报告或重复事件。构造函数接受 (start, interval, end) 或 (start, interval, recurrences)。结束日期是排他的。常见用例:为日历视图生成一个月中的所有天、列出发薪周期或创建重复事件计划。使用 iterator_to_array() 将期间物化为数组。对于复杂的重复规则(例如,'每第 2 个星期二'),考虑使用专用库如 rrule(RFC 5545 重复规则)。

php
// DatePeriod iterates over a range of dates
$start = new DateTime('2024-06-01');
$end = new DateTime('2024-06-10');
$interval = new DateInterval('P1D');  // 1 day

$period = new DatePeriod($start, $interval, $end);
foreach ($period as $day) {
    echo $day->format('Y-m-d (D)') . "\n";
}
// 2024-06-01 (Sat)
// 2024-06-02 (Sun)
// ... through 2024-06-09 (end is exclusive)

// Generate next 12 months
$start = new DateTime('first day of this month');
$interval = new DateInterval('P1M');
$period = new DatePeriod($start, $interval, 12);  // 12 recurrences
foreach ($period as $month) {
    echo $month->format('F Y') . "\n";
}

// Every Monday for a year
$start = new DateTime('next Monday');
$end = new DateTime('+1 year');
$period = new DatePeriod($start, new DateInterval('P1W'), $end);
$mondays = iterator_to_array($period);

Carbon 库(增强 DateTime)

Carbon 用流畅、富有表现力的 API 扩展 DateTime——它是 PHP 生态系统中的事实标准(被 Laravel 使用)。diffForHumans() 产生 '5 天前'、'3 小时后'——非常适合 UI 时间戳。流畅 API 链式方法(addYear()->subMonth()->endOfMonth())。比较方法(isWeekend、isPast、isToday)简化常见检查。本地化支持 50 多种语言的人类可读输出。Carbon 3(2024+)默认不可变。通过 Composer 安装:composer require nesbot/carbon。如果您使用 Laravel,Carbon 已包含在内。

php
// Carbon: the most popular DateTime library (composer require nesbot/carbon)
use Carbon\Carbon;

$now = Carbon::now('Asia/Shanghai');
$tomorrow = Carbon::tomorrow();
$lastWeek = Carbon::now()->subWeek();

// Human-readable differences
echo Carbon::now()->diffForHumans(Carbon::now()->subDays(5));
// "5 days ago"
echo Carbon::now()->addHours(3)->diffForHumans();
// "3 hours from now"

// Fluent API
$date = Carbon::create(2024, 6, 15, 14, 30, 0)
    ->addYear()
    ->subMonth()
    ->endOfMonth()
    ->setTimezone('UTC');

// Comparison methods
if ($date->isWeekend()) { echo "Weekend!"; }
if ($date->isPast()) { echo "Past"; }
if ($date->isFuture()) { echo "Future"; }
if ($date->isToday()) { echo "Today"; }
if ($date->isLeapYear()) { echo "Leap year"; }

// Localization
Carbon::setLocale('zh');
echo Carbon::now()->subDay()->diffForHumans();  // "1天前"
15

命名空间与自动加载

命名空间基础

命名空间将代码组织到层次化包中,防止库之间的类名冲突。命名空间声明必须是第一条语句(在 declare() 之后)。'use' 语句从其他命名空间导入类——将 use 语句放在文件顶部。别名(as)在两个类同名时解决冲突。前导反斜杠(\DateTime)引用全局命名空间。PHP 命名空间使用反斜杠(\)作为分隔符,映射到 PSR-4 自动加载中的目录结构。组 use 语句(use App\Models\{User, Post})减少样板代码。

php
<?php
// Namespaces prevent name collisions (like packages in Java)
namespace App\Services;

class UserService {
    public function find($id) { /* ... */ }
}

// Using namespaced classes
use App\Services\UserService;
use App\Models\User;

$service = new UserService();
$user = new User();

// Aliasing (resolve conflicts or shorten names)
use App\Services\UserService as USvc;
use App\Models\User as UserModel;

// Global namespace (backslash prefix)
$now = new \DateTime();  // \ means root/global namespace
$array = new \ArrayObject();

// Multiple use statements grouped
use App\Models\{User, Post, Comment};
use App\Services\{UserService, PostService};

PSR-4 自动加载标准

PSR-4 是标准的自动加载规范——它将命名空间映射到目录路径,因此您永远不需要手动 require/include 语句。规则:App\Services\UserService 映射到 src/Services/UserService.php(App\ → src/)。在 composer.json 的 autoload 部分配置映射。添加新类后,运行 'composer dump-autoload' 重新生成类映射。vendor/autoload.php 文件(由 Composer 生成)处理加载——在入口点(index.php)中包含它一次。PSR-4 强制类名匹配文件名(UserService → UserService.php)。

php
// PSR-4: namespace structure maps to file paths
// App\Services\UserService → src/Services/UserService.php

// composer.json PSR-4 configuration:
// {
//   "autoload": {
//     "psr-4": {
//       "App\\": "src/"
//     }
//   }
// }

// File: src/Services/UserService.php
namespace App\Services;

class UserService {
    public function getUser($id) {
        return "User $id";
    }
}

// File: src/Models/User.php
namespace App\Models;

class User {
    public $name;
    public function __construct($name) {
        $this->name = $name;
    }
}

// After adding classes, regenerate autoloader:
// $ composer dump-autoload

// The autoloader is included once in your entry point:
require __DIR__ . '/vendor/autoload.php';

$service = new App\Services\UserService();
$user = new App\Models\User('Alice');

不使用 Composer 的自动加载(spl_autoload)

spl_autoload_register 注册一个在类尚未加载时调用的函数——它接收完全限定类名并应 require 相应文件。您可以注册多个自动加载器(它们按顺序调用)。这是 Composer 内部使用的。对于生产环境,始终使用 Composer 的 PSR-4 自动加载器——它经过优化,处理边缘情况,并生成类映射以加快查找。仅在小项目或 Composer 不可用时直接使用 spl_autoload_register。class_exists() 中的 'true' 参数在类未加载时触发自动加载。

php
<?php
// Simple autoloader for small projects (no Composer needed)
spl_autoload_register(function ($className) {
    // Convert namespace separators to directory separators
    $file = __DIR__ . '/src/' . str_replace('\\', '/', $className) . '.php';

    // App\Services\UserService → src/App/Services/UserService.php
    // Adjust prefix mapping as needed:
    $file = str_replace('App/', '', $file);  // remove 'App' prefix

    if (file_exists($file)) {
        require $file;
    }
});

// Now classes are loaded automatically on first use
$service = new App\Services\UserService();

// Multiple autoloaders (called in order until one loads the class)
spl_autoload_register(function ($class) {
    $path = __DIR__ . '/lib/' . $class . '.php';
    if (file_exists($path)) require $path;
});

// Check if a class is autoloadable
if (class_exists('App\Helper', true)) {  // true = attempt autoload
    $helper = new App\Helper();
}

命名空间常量与函数

命名空间可以包含常量和函数,不仅仅是类。使用 'use const' 和 'use function' 导入它们(PHP 5.6+)。这对于配置常量和实用函数很有用。非限定函数/常量调用有回退行为:PHP 首先在当前命名空间中查找,然后回退到全局命名空间。这就是为什么您可以在不带反斜杠的情况下调用 strlen()——但为了性能和清晰度,在命名空间代码中为全局函数加 \ 前缀。组导入(use App\Config\{const DB_HOST, function connect})减少冗长。

php
<?php
namespace App\Config;

// Constants in namespaces
const DB_HOST = 'localhost';
const DB_PORT = 3306;

// Functions in namespaces
function connect() {
    return 'Connected to ' . DB_HOST;
}

// Using namespaced constants and functions
use const App\Config\DB_HOST;
use function App\Config\connect;

echo DB_HOST;      // localhost
echo connect();    // Connected to localhost

// Or with fully-qualified names:
echo \App\Config\DB_HOST;
echo \App\Config\connect();

// Namespace-level use for multiple imports
use App\Config\{const DB_HOST, const DB_PORT, function connect};

// Fallback: unqualified function calls fall back to global
// if not found in current namespace
namespace App;
$len = strlen('hello');  // calls global strlen() (fallback)

匿名类与自动加载

匿名类(PHP 7+)让您无需定义命名类即可创建简单的一次性对象——适用于接口、模拟对象和回调。它们可以实现接口、扩展类、有构造函数和使用 trait。类在运行时生成,带有自动生成的名称(class@anonymous)。匿名类立即加载(无需自动加载)。将它们用于:简单策略模式、测试替身/模拟、事件监听器和 DTO。对于可重用的类,始终使用正确的 PSR-4 自动加载定义命名类。匿名类在测试中创建轻量级存根特别方便。

php
<?php
namespace App\Factory;

// Anonymous class (PHP 7+): create a one-off class inline
interface Logger {
    public function log(string $msg): void;
}

class App {
    private $logger;
    public function setLogger(Logger $logger) {
        $this->logger = $logger;
    }
}

$app = new App();
$app->setLogger(new class implements Logger {
    public function log(string $msg): void {
        echo "[LOG] $msg\n";
    }
});

// Anonymous class with constructor
$comparator = new class($ascending = true) {
    private $asc;
    public function __construct(bool $asc) { $this->asc = $asc; }
    public function compare($a, $b): int {
        return $this->asc ? $a <=> $b : $b <=> $a;
    }
};

// Get the auto-generated class name
echo get_class($comparator);  // class@anonymous...
16

OOP 深入(Trait、接口、抽象)

抽象类与方法

抽象类提供带有共享实现的基础,子类扩展它。它们不能直接实例化。抽象方法定义契约(仅签名),具体子类必须实现——这是 '模板方法模式'。与接口不同,抽象类可以有属性、构造函数和具体方法。当子类共享大量实现('is-a' 关系)时使用抽象类。当您只需要任何类都可以实现的契约('can-do' 关系)时使用接口。一个类只能扩展一个抽象类,但可以实现多个接口。

php
<?php
// Abstract class: can't be instantiated, may have abstract methods
abstract class Animal {
    protected $name;

    public function __construct(string $name) {
        $this->name = $name;
    }

    // Abstract method: must be implemented by subclasses
    abstract public function makeSound(): string;

    // Concrete method: shared implementation
    public function describe(): string {
        return $this->name . " says " . $this->makeSound();
    }
}

class Dog extends Animal {
    public function makeSound(): string {
        return "Woof!";
    }
}

class Cat extends Animal {
    public function makeSound(): string {
        return "Meow!";
    }
}

$dog = new Dog("Rex");
echo $dog->describe();  // Rex says Woof!
// new Animal("test");  // Error: cannot instantiate abstract class

接口与多重实现

接口定义契约——方法签名无实现。一个类可以实现多个接口(与类的单继承不同)。接口启用多态:任何实现 Comparable 的类都可以被排序,无论其具体类型如何。使用接口定义跨越类层次结构的能力(Comparable、Serializable、Iterable)。使用接口进行类型提示(function sort(Comparable $a))比具体类更灵活。接口继承(interface A extends B, C)组合契约。现代 PHP 还支持接口常量和接口中的静态方法。

php
<?php
// Interface: pure contract (no implementation)
interface Comparable {
    public function compareTo($other): int;
}

interface Serializable {
    public function serialize(): string;
    public function unserialize(string $data): void;
}

// A class can implement MULTIPLE interfaces
class Product implements Comparable, Serializable {
    private $price;

    public function __construct(float $price) {
        $this->price = $price;
    }

    public function compareTo($other): int {
        return $this->price <=> $other->price;  // spaceship operator
    }

    public function serialize(): string {
        return serialize($this->price);
    }

    public function unserialize(string $data): void {
        $this->price = unserialize($data);
    }
}

// Type hinting with interfaces
function sortItems(array $items): array {
    usort($items, fn($a, $b) => $a->compareTo($b));
    return $items;
}

// Interface inheritance
interface Repository extends Comparable, Serializable {
    public function find(int $id): ?object;
}

Trait(无继承的代码重用)

Trait 提供水平代码重用——可以 '粘贴' 到任何类中而无需继承的方法。这解决了菱形问题(PHP 是单继承)。常见 trait 用途:日志记录、单例模式、软删除、时间戳。一个类可以使用多个 trait。当 trait 有冲突方法时,使用 'insteadof' 选择一个,使用 'as' 为另一个起别名。Trait 可以有抽象方法(强制使用类实现它们)和静态方法/属性。注意不要过度使用 trait——它们会使代码更难追踪。对于复杂行为,优先使用组合(注入依赖)而非 trait。

php
<?php
// Trait: reusable method groups (PHP's answer to multiple inheritance)
trait Logger {
    protected function log(string $msg, string $level = 'INFO'): void {
        echo "[$level] " . date('Y-m-d H:i:s') . " $msg\n";
        // In real code: write to file/database
    }
}

trait Singleton {
    private static $instance;
    public static function getInstance(): self {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
}

class UserService {
    use Logger, Singleton;

    public function findUser($id) {
        $this->log("Finding user $id");
        return "User $id";
    }
}

$svc = UserService::getInstance();
$svc->findUser(42);  // [INFO] 2024-06-15 14:30:00 Finding user 42

// Conflict resolution when traits have same method
trait A { public function hello() { return 'A'; } }
trait B { public function hello() { return 'B'; } }
class C {
    use A, B {
        B::hello insteadof A;  // use B's hello
        A::hello as helloFromA; // alias A's as helloFromA
    }
}

后期静态绑定(static:: vs self::)

后期静态绑定(LSB)是 self::(编译时,始终引用定义类)和 static::(运行时,引用调用类)之间的区别。这在继承中很重要:如果 Base 有使用 self::$table 的方法,即使在 Child 上调用,它也始终看到 Base 的 $table。使用 static::$table 使它看到 Child 的 $table。LSB 对工厂模式(new static() 创建调用类的实例)、ActiveRecord(每个模型有自己的表)和单例模式至关重要。'static' 返回类型(PHP 8+)声明方法返回调用类的实例。

php
<?php
class Base {
    protected static $table = 'base';

    public static function getTable(): string {
        // self:: refers to the class where the method is DEFINED
        return self::$table;  // always 'base'
    }

    public static function getTableStatic(): string {
        // static:: refers to the class that was CALLED (runtime)
        return static::$table;  // late static binding
    }

    public static function create(): static {
        // 'static' return type + new static() = factory pattern
        return new static();
    }
}

class Child extends Base {
    protected static $table = 'child';
}

echo Child::getTable();        // 'base' (self:: = Base)
echo Child::getTableStatic();  // 'child' (static:: = Child)
echo get_class(Child::create()); // 'Child' (new static = Child)

// self:: is resolved at compile time (the defining class)
// static:: is resolved at runtime (the calling class)
// This is "Late Static Binding" — essential for factory patterns

魔术方法

魔术方法是拦截对象操作的特殊方法。__get/__set 创建动态属性(适用于数据传输对象、ORM)。__toString 启用 echo $object。__invoke 使对象像函数一样可调用。__isset/__unset 支持对动态属性使用 isset()/unset()。__debugInfo 自定义 var_dump 输出。其他魔术方法:__construct、__destruct、__clone(用于深克隆)、__call/__callStatic(用于未定义方法,启用流畅 API 和 mixin)、__serialize/__unserialize(在 PHP 7.4+ 中替代 __sleep/__wakeup)。谨慎使用魔术方法——它们增加了难以调试的 '魔术' 行为。清楚地记录它们。

php
<?php
class MagicBox {
    private $data = [];

    // Called when accessing undefined properties
    public function __get($name) {
        return $this->data[$name] ?? null;
    }

    // Called when setting undefined properties
    public function __set($name, $value) {
        $this->data[$name] = $value;
    }

    // Called when isset() or empty() on undefined property
    public function __isset($name): bool {
        return isset($this->data[$name]);
    }

    // Called when unset() on undefined property
    public function __unset($name): void {
        unset($this->data[$name]);
    }

    // Called when object is used as string
    public function __toString(): string {
        return json_encode($this->data);
    }

    // Called when object is called as function
    public function __invoke($arg) {
        return "Called with $arg";
    }

    // Called for var_dump/debugging
    public function __debugInfo(): array {
        return ['keys' => array_keys($this->data)];
    }
}

$box = new MagicBox();
$box->name = "Alice";       // __set
echo $box->name;            // __get → Alice
echo $box;                  // __toString → {"name":"Alice"}
echo $box("test");          // __invoke → Called with test
17

Composer 包管理

composer.json 基础

composer.json 是 PHP 项目的清单。require 列出生产依赖及版本约束(^ 允许次要更新,~ 允许补丁)。autoload 定义 PSR-4 命名空间到目录的映射。require-dev 包含仅开发依赖。运行 composer install 设置项目。

php
{
    "name": "myorg/myapp",
    "type": "project",
    "require": {
        "php": ">=8.1",
        "monolog/monolog": "^3.0",
        "symfony/console": "^7.0"
    },
    "require-dev": {
        "phpunit/phpunit": "^11.0"
    },
    "autoload": {
        "psr-4": { "MyApp\\": "src/" }
    }
}

安装与更新

composer install 读取 composer.lock 获取确切版本(可重现构建)。composer require 添加包并解析依赖。composer update 在约束内获取更新版本。生产环境使用 --no-dev。--optimize-autoloader 将 PSR-4 转换为 classmap,以在生产环境中更快自动加载。

php
# Install all dependencies from composer.lock
composer install

# Add a package (modifies composer.json)
composer require monolog/monolog
composer require --dev phpunit/phpunit

# Update all packages to latest allowed versions
composer update

# Update a single package
composer update monolog/monolog

# Production install (no dev, optimized autoloader)
composer install --no-dev --optimize-autoloader

# Show installed packages
composer show

版本约束

插入符号(^)是最常见的约束:允许不修改最左侧非零数字的更改。波浪号(~)锁定到补丁级别。对于 0.x 版本,^0.3 允许 0.3.x 但不允许 0.4。始终使用约束以获取安全补丁,同时避免破坏性更改。在 composer.lock 中固定确切版本以实现可重现性。

php
"require": {
    // Caret: >=1.2.0, <2.0.0 (allows minor+patch)
    "vendor/pkg": "^1.2",

    // Tilde: >=1.2.0, <1.3.0 (patch only)
    "vendor/pkg2": "~1.2",

    // Exact version
    "vendor/pkg3": "1.2.3",

    // Range
    "vendor/pkg4": ">=1.0 <2.0",

    // Wildcard
    "vendor/pkg5": "1.2.*",

    // Stability flags
    "vendor/pkg6": "dev-main",
    "vendor/pkg7": "2.0@beta"
}

PSR-4 自动加载

PSR-4 自动加载将命名空间前缀映射到目录:MyApp\Services\UserService 解析为 src/Services/User.php。添加新类后运行 composer dump-autoload。对于生产环境,使用 --optimize 生成 classmap(一次数组查找代替文件系统检查)。Classmap 自动加载扫描目录,对于固定代码库最快。

php
// PSR-4: "MyApp\\": "src/" means
// MyApp\User -> src/User.php
// MyApp\Services\Auth -> src/Services/Auth.php

namespace MyApp\Services;

class UserService
{
    public function find(int $id): ?User
    {
        // ...
    }
}

// After running composer dump-autoload:
// require 'vendor/autoload.php';
// $service = new \MyApp\Services\UserService();

// Classmap (faster for production)
// "autoload": { "classmap": ["src/", "lib/"] }

脚本与钩子

Composer 脚本定义项目特定命令。使用 composer <name> 运行。内置事件(post-install-cmd、post-update-cmd、pre-autoload-dump)自动触发。脚本可以用 @name 引用其他脚本。使用脚本在团队成员之间标准化开发工作流。

php
{
    "scripts": {
        "test": "phpunit",
        "lint": "phpcs --standard=PSR12 src/",
        "fix": "phpcbf --standard=PSR12 src/",
        "post-install-cmd": [
            "MyApp\\Setup::postInstall",
            "php artisan migrate"
        ],
        "post-update-cmd": "@post-install-cmd"
    }
}

// Run: composer test, composer lint, etc.
18

高级 cURL

多请求(并行)

curl_multi_exec 并行运行多个请求,显著减少批量 API 调用的总时间。curl_multi_select 阻塞直到有活动,避免忙等待。始终关闭句柄和多句柄以释放资源。这是高性能 HTTP 抓取和 API 聚合的基础。

php
<?php
$urls = [
    'https://api.example.com/users',
    'https://api.example.com/posts',
    'https://api.example.com/comments',
];

$multi = curl_multi_init();
$handles = [];

foreach ($urls as $i => $url) {
    $handles[$i] = curl_init($url);
    curl_setopt($handles[$i], CURLOPT_RETURNTRANSFER, true);
    curl_setopt($handles[$i], CURLOPT_TIMEOUT, 10);
    curl_multi_add_handle($multi, $handles[$i]);
}

do {
    $status = curl_multi_exec($multi, $active);
    if ($active) curl_multi_select($multi);
} while ($active && $status === CURLM_OK);

foreach ($handles as $i => $ch) {
    $responses[$i] = curl_multi_getcontent($ch);
    curl_multi_remove_handle($multi, $ch);
}
curl_multi_close($multi);

流式响应

CURLOPT_WRITEFUNCTION 为响应的每个块提供回调,启用大文件的流式处理而无需完全加载到内存中。返回块长度以表示已消费。这对于下载大文件、处理流式 API 或增量解析 CSV/JSON 至关重要。

php
<?php
$ch = curl_init('https://example.com/large-file.csv');
$file = fopen('download.csv', 'w');

curl_setopt($ch, CURLOPT_FILE, $file);  // Write to file
// OR use callback for streaming processing:
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $chunk) use ($file) {
    fwrite($file, $chunk);
    // Or parse incrementally:
    // $lines = explode("\n", $chunk);
    return strlen($chunk);  // Must return bytes consumed
});

curl_exec($ch);
fclose($file);
curl_close($ch);

身份验证与 Cookie

使用 CURLOPT_HTTPHEADER 设置自定义头进行身份验证(Bearer 令牌、API 密钥)。COOKIEJAR/COOKIEFILE 在请求之间持久化 cookie 以进行基于会话的认证。CURLOPT_USERPWD 设置 HTTP 基本认证。对于 POST,设置 CURLOPT_POSTFIELDS 为 JSON 和 Content-Type 头。始终设置 Accept 以控制响应格式。

php
<?php
$ch = curl_init('https://api.example.com/data');

// Bearer token
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer ' . $token,
    'Content-Type: application/json',
]);

// Cookie jar (persist across requests)
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');   // Save
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');  // Load

// Basic auth
curl_setopt($ch, CURLOPT_USERPWD, 'username:password');

// POST with JSON body
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['key' => 'value']));

$response = curl_exec($ch);

错误处理与重试

始终检查 curl_exec 返回值(失败时为 false)和 curl_error 获取消息。curl_getinfo 提供 HTTP 状态码、计时和重定向信息。实现指数退避重试以处理速率限制和瞬时故障。区分网络错误(curl 错误)和 HTTP 错误(状态码)。

php
<?php
function fetchWithRetry(string $url, int $max = 3): ?string
{
    for ($attempt = 1; $attempt <= $max; $attempt++) {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);

        $response = curl_exec($ch);
        $error = curl_error($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($response !== false && $code >= 200 && $code < 300) {
            return $response;
        }

        if ($attempt < $max) {
            sleep(pow(2, $attempt));  // Exponential backoff
        }
    }
    throw new RuntimeException("Failed: {$error}");
}

cURL 选项参考

CURLOPT_FOLLOWLOCATION 跟随 HTTP 重定向(3xx)。在生产环境中始终保持 SSL_VERIFYPEER 为 true 以防止 MITM 攻击;从 curl.haxx.se 下载 cacert.pem。CURLOPT_ENCODING 启用压缩。使用 CURLOPT_VERBOSE 和 STDERR 调试连接问题。设置合理的超时以避免挂起。

php
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://example.com');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);  // Return not echo
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);   // Follow redirects
curl_setopt($ch, CURLOPT_MAXREDIRS, 5);

// SSL (keep VERIFYPEER true in production!)
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_CAINFO, '/path/to/cacert.pem');

// Performance
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_ENCODING, 'gzip');

// Debug
curl_setopt($ch, CURLOPT_VERBOSE, true);
curl_setopt($ch, CURLOPT_STDERR, fopen('curl.log', 'w'));
19

图像处理(GD)

创建与加载图像

imagecreatetruecolor 创建真彩色图像(数百万种颜色)。imagecolorallocate 注册颜色并返回标识符。imagecreatefromjpeg/png/webp 加载现有文件。始终检查返回值(失败时为 false)。使用 imagesx/imagesy 获取尺寸。完成后用 imagedestroy 释放内存。

php
<?php
// Create a blank image
$img = imagecreatetruecolor(400, 300);

// Allocate colors
$white = imagecolorallocate($img, 255, 255, 255);
$red = imagecolorallocate($img, 255, 0, 0);

// Fill background
imagefill($img, 0, 0, $white);

// Load existing images
$photo = imagecreatefromjpeg('photo.jpg');
$png = imagecreatefrompng('logo.png');
$webp = imagecreatefromwebp('image.webp');

// Get dimensions
$width = imagesx($photo);
$height = imagesy($photo);

绘制形状与文本

GD 提供绘图原语:矩形、椭圆、线、多边形和弧。填充变体(imagefilled*)绘制实心形状。imagettftext 渲染 TrueType 字体,带角度和大小控制。输出图像数据前始终发送 Content-Type 头。调用 imagedestroy 释放内存。

php
<?php
$img = imagecreatetruecolor(400, 300);
$white = imagecolorallocate($img, 255, 255, 255);
$red = imagecolorallocate($img, 255, 0, 0);
$blue = imagecolorallocate($img, 0, 0, 255);
imagefill($img, 0, 0, $white);

// Shapes
imagerectangle($img, 50, 50, 150, 120, $red);
imagefilledrectangle($img, 200, 50, 300, 120, $blue);
imageellipse($img, 100, 200, 80, 80, $red);
imageline($img, 0, 0, 400, 300, $red);

// Text with TrueType font
imagettftext($img, 20, 0, 50, 270, $red, 'arial.ttf', 'Hello GD!');

header('Content-Type: image/png');
imagepng($img);
imagedestroy($img);

调整大小与裁剪

imagecopyresampled 产生比 imagecopyresized 质量更高的结果(使用插值)。通过从原始尺寸计算维度来保持纵横比。对于缩略图,居中裁剪为正方形以获得一致布局。复制后始终销毁源图像以防止批处理中的内存泄漏。

php
<?php
function resizeImage($src, $maxW, $maxH) {
    list($w, $h) = getimagesize($src);
    $ratio = min($maxW / $w, $maxH / $h);
    $newW = (int)($w * $ratio);
    $newH = (int)($h * $ratio);

    $srcImg = imagecreatefromjpeg($src);
    $dstImg = imagecreatetruecolor($newW, $newH);

    // High-quality resampling
    imagecopyresampled($dstImg, $srcImg, 0, 0, 0, 0,
        $newW, $newH, $w, $h);

    imagedestroy($srcImg);
    return $dstImg;
}

// Center crop to square
function cropSquare($src, $size) {
    list($w, $h) = getimagesize($src);
    $min = min($w, $h);
    $x = (int)(($w - $min) / 2);
    $y = (int)(($h - $min) / 2);
    $dst = imagecreatetruecolor($size, $size);
    $img = imagecreatefromjpeg($src);
    imagecopyresampled($dst, $img, 0, 0, $x, $y, $size, $size, $min, $min);
    return $dst;
}

滤镜与效果

imagefilter 应用内置效果:灰度、亮度(范围 -255 到 255)、对比度(负值增加)、模糊、边缘检测、反相和着色(RGB + alpha)。Pixelate 创建马赛克效果。这些快速但基础;对于高级效果,使用 ImageMagick(Imagick 扩展),它支持卷积矩阵和自定义滤镜。

php
<?php
$img = imagecreatefromjpeg('photo.jpg');

// Built-in filters
imagefilter($img, IMG_FILTER_GRAYSCALE);      // B&W
imagefilter($img, IMG_FILTER_BRIGHTNESS, 30);  // Brighten
imagefilter($img, IMG_FILTER_CONTRAST, -20);   // More contrast
imagefilter($img, IMG_FILTER_GAUSSIAN_BLUR);   // Blur
imagefilter($img, IMG_FILTER_EDGEDETECT);      // Edge detection
imagefilter($img, IMG_FILTER_NEGATE);          // Invert colors

// Colorize (tint)
imagefilter($img, IMG_FILTER_COLORIZE, 0, 0, 100, 0);  // Blue tint

// Pixelate
imagefilter($img, IMG_FILTER_PIXELATE, 10, true);

imagepng($img, 'filtered.png');
imagedestroy($img);

水印与合成

imagecopymerge 将一张图像叠加到另一张上,带可调不透明度(0-100)。带 alpha 通道的 PNG 水印自然混合。对于文本水印,使用 imagecolorallocatealpha 创建半透明文本。imagejpeg 质量范围从 0(最差)到 100(最好);75-90 是 Web 的良好平衡。始终销毁两张图像。

php
<?php
$photo = imagecreatefromjpeg('photo.jpg');
$watermark = imagecreatefrompng('logo.png');

$pw = imagesx($photo); $ph = imagesy($photo);
$ww = imagesx($watermark); $wh = imagesy($watermark);

// Position: bottom-right with 20px padding
$destX = $pw - $ww - 20;
$destY = $ph - $wh - 20;

// Merge with 50% opacity
imagecopymerge($photo, $watermark, $destX, $destY, 0, 0, $ww, $wh, 50);

// Text watermark
$color = imagecolorallocatealpha($photo, 255, 255, 255, 60);
imagettftext($photo, 30, 0, 20, $ph - 20, $color, 'arial.ttf', '© 2025');

imagejpeg($photo, 'watermarked.jpg', 90);  // 90% quality
imagedestroy($photo);
imagedestroy($watermark);
20

会话与 Cookie 深入

会话安全

安全会话需要:HttpOnly cookie(无 JavaScript 访问)、Secure 标志(仅 HTTPS)、SameSite=Strict(CSRF 保护)和严格模式(拒绝未初始化的会话 ID)。在权限更改(登录、管理员访问)后始终重新生成会话 ID 以防止会话固定。使用自定义会话名称以避免暴露 PHP。

php
<?php
// php.ini or runtime configuration
ini_set('session.cookie_httponly', 1);    // No JS access
ini_set('session.cookie_secure', 1);       // HTTPS only
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.use_strict_mode', 1);     // Reject uninitialized IDs
ini_set('session.gc_maxlifetime', 3600);   // 1 hour

session_name('APP_SID');  // Custom name
session_start();

// Regenerate ID after login (prevent fixation)
session_regenerate_id(true);

$_SESSION['user_id'] = 123;
$_SESSION['login_time'] = time();

自定义会话处理程序

自定义会话处理程序将会话数据存储在数据库、Redis 或 Memcached 中而非文件。实现 SessionHandlerInterface,包含 open、close、read、write、destroy 和 gc 方法。数据库存储启用跨多服务器共享会话(负载均衡)。始终使用参数化查询以防止会话 ID 中的 SQL 注入。

php
<?php
class DbSessionHandler implements SessionHandlerInterface
{
    private PDO $pdo;
    public function __construct(PDO $pdo) { $this->pdo = $pdo; }

    public function open($path, $name): bool { return true; }
    public function close(): bool { return true; }

    public function read($id): string|false {
        $stmt = $this->pdo->prepare(
            'SELECT data FROM sessions WHERE id = ? AND expires > ?'
        );
        $stmt->execute([$id, time()]);
        return $stmt->fetchColumn() ?: '';
    }

    public function write($id, $data): bool {
        $exp = time() + (int)ini_get('session.gc_maxlifetime');
        return $this->pdo->prepare(
            'REPLACE INTO sessions (id, data, expires) VALUES (?, ?, ?)'
        )->execute([$id, $data, $exp]);
    }

    public function destroy($id): bool {
        return $this->pdo->prepare('DELETE FROM sessions WHERE id = ?')
            ->execute([$id]);
    }

    public function gc($max): int|false {
        return $this->pdo->prepare('DELETE FROM sessions WHERE expires < ?')
            ->execute([time()]);
    }
}

session_set_save_handler(new DbSessionHandler($pdo), true);
session_start();

Cookie 管理

使用 setcookie 的选项数组形式(PHP 7.3+)以获得清晰度并设置 SameSite。安全 cookie 需要 HTTPS。HttpOnly 防止基于 XSS 的 cookie 窃取。SameSite=Lax 阻止跨站 POST(对大多数 CSRF 保护足够);Strict 阻止所有跨站请求。通过将过期时间设置为过去并使用相同路径/域来删除 cookie。

php
<?php
// Set a cookie with all security options
setcookie('preferences', json_encode(['theme' => 'dark']), [
    'expires' => time() + 86400 * 30,  // 30 days
    'path' => '/',
    'domain' => '.example.com',
    'secure' => true,                   // HTTPS only
    'httponly' => true,                 // No JavaScript access
    'samesite' => 'Lax'                 // CSRF protection
]);

// Read cookies
$theme = $_COOKIE['preferences'] ?? 'default';

// Delete a cookie (set expiration in the past)
setcookie('preferences', '', [
    'expires' => time() - 3600,
    'path' => '/',
]);

闪存消息

闪存消息在会话中存储一次性通知,在重定向后显示(Post/Redirect/Get 模式)。消息在重定向前设置,显示后清除。这防止重新提交警告并保持 UI 清洁。存储为数组以支持多条消息。读取后立即清除以防止陈旧显示。

php
<?php
// Set a flash message (one-time notification)
function flash(string $key, string $message): void {
    $_SESSION['_flash'][$key] = $message;
}

// Get and clear flash message
function getFlash(string $key): ?string {
    $msg = $_SESSION['_flash'][$key] ?? null;
    unset($_SESSION['_flash'][$key]);
    return $msg;
}

// Usage in controller
flash('success', 'Item saved!');
header('Location: /items');
exit;

// In view after redirect
if ($msg = getFlash('success')) {
    echo "<div class='alert'>$msg</div>";
}

JWT 身份验证

JWT 启用无状态身份验证:服务器不存储会话数据,使其非常适合 API 和微服务。令牌包含声明(用户 ID、角色、过期时间),用密钥签名。权衡:令牌不易撤销(使用短过期 + 刷新令牌),且它们增加请求大小。使用 HttpOnly cookie 防止 XSS 令牌窃取。

php
<?php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

// Generate token on login
$payload = [
    'user_id' => 123,
    'role' => 'admin',
    'iat' => time(),           // Issued at
    'exp' => time() + 3600,    // Expires in 1 hour
];
$token = JWT::encode($payload, $secretKey, 'HS256');

// Send to client (cookie or Authorization header)
setcookie('auth_token', $token, [
    'expires' => time() + 3600,
    'httponly' => true,
    'secure' => true,
    'samesite' => 'Lax',
]);

// Verify on each request
try {
    $decoded = JWT::decode(
        $_COOKIE['auth_token'],
        new Key($secretKey, 'HS256')
    );
    $userId = $decoded->user_id;
} catch (Exception $e) {
    http_response_code(401);
    exit('Unauthorized');
}
21

REST API 深入

路由与请求处理

REST API 将 HTTP 方法映射到 CRUD 操作:GET(读取)、POST(创建)、PUT/PATCH(更新)、DELETE(删除)。解析 URL 路径进行资源识别。从 php://input 读取 POST/PUT 的请求正文。始终返回适当的 HTTP 状态码(200、201、400、404、500)和带 Content-Type 头的 JSON 响应。

php
<?php
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$segments = explode('/', trim($path, '/'));

switch (true) {
    case $method === 'GET' && $segments[0] === 'users':
        if (isset($segments[1])) getUser((int)$segments[1]);
        else listUsers();
        break;

    case $method === 'POST' && $segments[0] === 'users':
        $data = json_decode(file_get_contents('php://input'), true);
        createUser($data);
        break;

    case $method === 'PUT' && $segments[0] === 'users' && isset($segments[1]):
        $data = json_decode(file_get_contents('php://input'), true);
        updateUser((int)$segments[1], $data);
        break;

    default:
        http_response_code(404);
        echo json_encode(['error' => 'Not Found']);
}

响应与状态码

始终为 API 响应设置 Content-Type: application/json。使用正确的状态码:200(OK)、201(已创建)、204(无内容)、400(错误请求)、401(未授权)、403(禁止)、404(未找到)、422(不可处理实体)、429(请求过多)、500(服务器错误)。包含错误详情用于调试,但切勿在生产环境中暴露堆栈跟踪。

php
<?php
function jsonResponse($data, int $status = 200): void
{
    http_response_code($status);
    header('Content-Type: application/json');
    echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
    exit;
}

// Success responses
jsonResponse(['data' => $users], 200);          // OK
jsonResponse(['data' => $user], 201);           // Created

// Error responses
jsonResponse(['error' => 'Validation failed'], 400);
jsonResponse(['error' => 'Unauthorized'], 401);
jsonResponse(['error' => 'Forbidden'], 403);
jsonResponse(['error' => 'Not Found'], 404);
jsonResponse(['error' => 'Server error'], 500);

分页与过滤

使用 LIMIT/OFFSET 实现分页并返回元数据(总数、当前页、总页数)。针对白名单验证和清理排序列以防止 SQL 注入。限制 per_page 以防止过多查询。使用 LIKE 进行带通配符的搜索。在与数据分开的 meta 对象中返回分页元数据。

php
<?php
$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = min(100, max(1, (int)($_GET['per_page'] ?? 20)));
$offset = ($page - 1) * $perPage;

// Whitelist sort columns (prevent SQL injection)
$sort = in_array($_GET['sort'] ?? '', ['name', 'email'])
    ? $_GET['sort'] : 'id';
$order = strtolower($_GET['order'] ?? 'asc') === 'desc' ? 'DESC' : 'ASC';

$sql = "SELECT * FROM users ORDER BY $sort $order LIMIT ? OFFSET ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$perPage, $offset]);
$users = $stmt->fetchAll();

// Total count for pagination metadata
$total = (int)$pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();

jsonResponse([
    'data' => $users,
    'meta' => [
        'page' => $page,
        'per_page' => $perPage,
        'total' => $total,
        'total_pages' => ceil($total / $perPage),
    ],
]);

速率限制

速率限制防止 API 滥用。使用固定窗口(简单)或滑动窗口(更准确)算法。对于分布式系统,将计数器存储在 Redis 中。返回 X-RateLimit 头(Limit、Remaining、Reset)以便客户端自我调节。HTTP 429 带 Retry-After 告诉客户端何时重试。对于生产环境,使用 Redis 或专用速率限制器。

php
<?php
function checkRateLimit(int $userId, int $max = 100, int $window = 3600): bool
{
    $file = sys_get_temp_dir() . "/rate_{$userId}_" . floor(time() / $window);
    $count = file_exists($file) ? (int)file_get_contents($file) : 0;

    if ($count >= $max) {
        $reset = (floor(time() / $window) + 1) * $window;
        header("X-RateLimit-Limit: $max");
        header("X-RateLimit-Remaining: 0");
        header("Retry-After: " . ($reset - time()));
        http_response_code(429);
        echo json_encode(['error' => 'Rate limit exceeded']);
        return false;
    }

    file_put_contents($file, $count + 1);
    header("X-RateLimit-Remaining: " . ($max - $count - 1));
    return true;
}

if (!checkRateLimit($userId)) exit;

API 版本控制

API 版本控制策略:URL 前缀(/v1/)最明确且缓存友好;Accept 头是 RESTful 但更难测试。使用 OpenAPI(Swagger)注解记录 API。使用 swagger-php 等工具生成交互式文档。从一开始就进行版本控制;破坏性更改需要新版本。使用 Sunset 头弃用旧版本。

php
<?php
// Versioning via URL prefix: /v1/users, /v2/users
$version = $segments[0] ?? 'v1';

// Versioning via Accept header
// Accept: application/vnd.myapp.v2+json
preg_match('/vnd\.myapp\.(v\d+)\+json/',
    $_SERVER['HTTP_ACCEPT'] ?? '', $m);
$version = $m[1] ?? 'v1';

// OpenAPI/Swagger documentation
/**
 * @OA\Get(path="/api/users",
 *   @OA\Response(response=200, description="List users")
 * )
 */
22

安全深入(XSS/CSRF)

XSS 预防

XSS(跨站脚本攻击)将恶意脚本注入网页。通过根据上下文编码输出预防:HTML 用 htmlspecialchars,JavaScript 用 json_encode,URL 用 urlencode。ENT_QUOTES 转义单引号和双引号。内容安全策略(CSP)通过限制脚本源增加深度防御。切勿信任用户输入。

php
<?php
// Output encoding (prevents XSS)
echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');

// JavaScript string (use json_encode)
echo '<script>var name = ' . json_encode($name) . ';</script>';

// URL parameter
echo 'redirect=' . urlencode($url);

// Content-Security-Policy header
header("Content-Security-Policy: default-src 'self'; script-src 'self'");

// Disable inline scripts
header("X-XSS-Protection: 1; mode=block");

CSRF 保护

CSRF(跨站请求伪造)诱骗用户提交不需要的操作。使用反 CSRF 令牌预防:每个会话生成随机令牌,在表单中作为隐藏字段包含,并在 POST/PUT/DELETE 时验证。使用 hash_equals 进行时序安全比较。对于 AJAX,在自定义头中发送令牌。SameSite=Strict cookie 提供额外保护。

php
<?php
// Generate CSRF token
function csrfToken(): string {
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

// Verify CSRF token (timing-safe)
function verifyCsrf(string $token): bool {
    return !empty($_SESSION['csrf_token'])
        && hash_equals($_SESSION['csrf_token'], $token);
}

// In the form
echo '<input type="hidden" name="csrf_token" value="' . csrfToken() . '">';

// On POST request
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!verifyCsrf($_POST['csrf_token'] ?? '')) {
        http_response_code(403);
        die('CSRF token validation failed');
    }
}

SQL 注入预防

SQL 注入允许攻击者执行任意 SQL。始终使用带参数化查询的预处理语句:数据库将 SQL 逻辑与数据分离,使注入不可能。切勿将用户输入连接到 SQL 字符串中。对于动态查询(IN 子句、ORDER BY),用占位符构建 SQL 结构并将值作为参数传递。

php
<?php
// BAD: string concatenation (SQL injection vulnerable)
$sql = "SELECT * FROM users WHERE name = '" . $_GET['name'] . "'";
// Attack: ?name=' OR '1'='1

// GOOD: prepared statements
$stmt = $pdo->prepare('SELECT * FROM users WHERE name = ? AND status = ?');
$stmt->execute([$_GET['name'], 'active']);
$users = $stmt->fetchAll();

// Named parameters
$stmt = $pdo->prepare(
    'INSERT INTO users (name, email) VALUES (:name, :email)'
);
$stmt->execute([':name' => $name, ':email' => $email]);

// Dynamic IN clause (still safe)
$ids = [1, 2, 3];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);

密码哈希

切勿存储明文密码。password_hash 使用 bcrypt(或 Argon2,如果可用)带自动盐生成。哈希包含算法、成本和盐,因此 password_verify 可以针对任何格式检查。使用 password_needs_rehash 在增加成本因子或切换算法时升级哈希。新应用程序推荐使用 Argon2。

php
<?php
// Hash a password (uses bcrypt by default, auto-salts)
$hash = password_hash('myPassword123', PASSWORD_DEFAULT);
// $2y$10$... (bcrypt with cost 10)

// Verify a password
if (password_verify($inputPassword, $storedHash)) {
    // Check if rehash is needed (algorithm upgrade)
    if (password_needs_rehash($storedHash, PASSWORD_DEFAULT)) {
        $newHash = password_hash($inputPassword, PASSWORD_DEFAULT);
        // Update stored hash in database
    }
    // Log user in
}

// Argon2 (PHP 7.2+, requires libsodium)
$hash = password_hash('password', PASSWORD_ARGON2ID, [
    'memory_cost' => 65536,  // 64 MB
    'time_cost'   => 4,      // iterations
    'threads'     => 2,
]);

输入验证

始终在服务器端验证输入(客户端验证仅用于用户体验)。使用 filter_input 与 FILTER_VALIDATE_* 进行类型检查,FILTER_SANITIZE_* 进行清理。对于自定义规则,使用正则表达式或专用验证库(Respect/Validation、Symfony Validator)。使用白名单方法:仅接受已知字段,拒绝其他一切。

php
<?php
// Validate email
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if ($email === false) {
    $errors[] = 'Invalid email';
}

// Validate integer with range
$age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, [
    'options' => ['min_range' => 1, 'max_range' => 150]
]);

// Validate URL
$url = filter_input(INPUT_POST, 'website', FILTER_VALIDATE_URL);

// Custom validation with regex
function validateUsername(string $u): ?string {
    if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $u)) {
        return 'Username must be 3-20 alphanumeric chars';
    }
    return null;
}

// Whitelist approach for arrays
$allowed = ['name', 'email', 'age'];
$input = array_intersect_key($_POST, array_flip($allowed));
23

命名空间与自动加载深入

命名空间声明

命名空间防止类名冲突并层次化组织代码。命名空间声明必须是第一条语句。use 导入类、函数和常量。别名(as)解决冲突。PHP 命名使用反斜杠。PSR-4 标准将命名空间分隔符映射到目录分隔符:MyApp\Services\UserService -> src/Services/UserService.php。

php
<?php
// File: src/Services/UserService.php
namespace MyApp\Services;

use MyApp\Models\User;
use MyApp\Repositories\UserRepository;
use MyApp\Exceptions\NotFoundException;

class UserService
{
    public function __construct(
        private UserRepository $repo
    ) {}

    public function find(int $id): User
    {
        $user = $this->repo->findById($id);
        if (!$user) {
            throw new NotFoundException("User {$id} not found");
        }
        return $user;
    }
}

PSR-4 自动加载

PSR-4 是标准自动加载规范:命名空间前缀映射到基础目录,每个命名空间分隔符成为目录分隔符。Composer 生成自动加载器,自动将类名解析为文件路径。添加新类后运行 composer dump-autoload。自动加载器仅在首次引用类时加载它们(惰性加载)。

php
// composer.json
{
    "autoload": {
        "psr-4": {
            "MyApp\\": "src/",
            "Tests\\": "tests/"
        }
    }
}

// After composer install:
require __DIR__ . '/vendor/autoload.php';

// Now all classes auto-load:
use MyApp\Services\UserService;
use MyApp\Controllers\UserController;

// PSR-4 rules:
// MyApp\User          -> src/User.php
// MyApp\Services\Auth -> src/Services/Auth.php
// MyApp\Models\User\Profile -> src/Models/User/Profile.php

自定义自动加载器

spl_autoload_register 将函数添加到自动加载器栈。当引用类但未加载时,PHP 按顺序调用每个注册的自动加载器,直到一个加载该类。多个自动加载器可以共存(例如,一个用于 PSR-4,一个用于遗留类)。require 前始终检查文件是否存在以避免错误。Composer 内部使用此机制。

php
<?php
spl_autoload_register(function (string $class): void {
    $prefix = 'MyApp\\';
    $baseDir = __DIR__ . '/src/';

    // Check if class uses our namespace prefix
    $len = strlen($prefix);
    if (strncmp($prefix, $class, $len) !== 0) {
        return;  // Not our class
    }

    // Get relative class name
    $relativeClass = substr($class, $len);

    // Replace namespace separators with directory separators
    $file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';

    if (file_exists($file)) {
        require $file;
    }
});

// Multiple autoloaders can be registered (stack-based)
spl_autoload_register(function ($class) {
    $path = __DIR__ . '/legacy/' . $class . '.php';
    if (file_exists($path)) require $path;
});

Classmap 与文件自动加载

classmap 自动加载在 dump-autoload 时扫描目录并构建类名到文件路径的映射数组。这比 PSR-4 快(一次数组查找 vs 文件系统检查),推荐用于生产环境。files 在每次请求时加载特定文件,适用于无法作为类自动加载的辅助函数和常量。生产环境使用 --optimize 生成 classmap。

php
{
    "autoload": {
        "psr-4": { "MyApp\\": "src/" },
        "classmap": ["src/Legacy/", "lib/OldClasses.php"],
        "files": ["src/helpers.php", "src/constants.php"]
    }
}

// classmap: scans directories and builds class-to-file map
// Faster than PSR-4 for production (no filesystem checks)

// files: always-loaded files (for functions and constants)
// src/helpers.php:
<?php
function dd($var) { var_dump($var); die; }
function env(string $key, $default = null) { /* ... */ }

命名空间解析

在命名空间代码中,非限定类名首先通过导入解析,然后是当前命名空间。内置类(DateTime、PDO、Exception)位于全局命名空间;用前导反斜杠引用它们或导入它们。函数和常量在本地未找到时回退到全局命名空间。使用 FQCN(前导反斜杠)进行绝对引用。

php
<?php
namespace MyApp\Services;

use MyApp\Models\User;

class UserService
{
    public function create(): User
    {
        // User resolves to MyApp\Models\User (imported)
        return new User();
    }

    public function find(): \MyApp\Models\User
    {
        // Fully Qualified Class Name (leading backslash)
        return new \MyApp\Models\User();
    }

    public function date(): \DateTime
    {
        // Global classes need backslash or import
        return new \DateTime();
    }
}
24

生成器与 Yield

基本生成器

生成器用 yield 惰性产生值,一次一个,无需在内存中构建整个集合。这对大型或无限序列节省内存。函数返回实现 Iterator 的 Generator 对象。每个 yield 暂停执行,在下一次迭代时恢复。将生成器用于读取大文件、数据库游标和计算序列。

php
<?php
function rangeGen(int $start, int $end, int $step = 1): Generator
{
    for ($i = $start; $i <= $end; $i += $step) {
        yield $i;
    }
}

// Iterate without loading all values into memory
foreach (rangeGen(1, 5) as $value) {
    echo $value . ' ';  // 1 2 3 4 5
}

// Memory efficient for large sequences
foreach (rangeGen(1, 1000000) as $value) {
    if ($value % 100000 === 0) echo "Reached {$value}\n";
}

Yield 键值对

生成器可以使用 yield key => value 语法产生键值对,就像关联数组一样。这在转换中保留键。要过滤值,只需不 yield 它们。Generator 在迭代中维护其位置,因此您可以构建管道式处理,每个生成器转换或过滤流。

php
<?php
function mapGen(array $data): Generator
{
    foreach ($data as $key => $value) {
        yield $key => strtoupper($value);
    }
}

foreach (mapGen(['a' => 'hello', 'b' => 'world']) as $key => $value) {
    echo "{$key} => {$value}\n";
}
// a => HELLO
// b => WORLD

// Filter by not yielding unwanted values
function filterGen(array $data): Generator
{
    foreach ($data as $value) {
        if ($value > 0) yield $value;
    }
}

向生成器发送值

send() 方法将值传递给生成器,成为 yield 表达式的结果。这启用了双向通信,适用于协程和状态机。current() 启动生成器。getReturn() 在生成器完成后检索返回值。finally 块在生成器销毁时运行,启用资源清理。

php
<?php
function accumulator(): Generator
{
    $total = 0;
    while (true) {
        $value = yield $total;
        if ($value === null) break;
        $total += $value;
    }
    return $total;
}

$gen = accumulator();
$gen->current();  // Start: 0
$gen->send(10);   // Returns 10
$gen->send(20);   // Returns 30
$gen->send(5);    // Returns 35
$gen->send(null); // Triggers return
echo $gen->getReturn();  // 35

Yield from(委托)

yield from 委托给另一个生成器、数组或 Traversable,将其值展平到外部生成器。内部生成器的返回值对外部生成器可用。这启用组合:从简单生成器构建复杂管道。yield from 也比手动迭代和重新 yield 更高效。

php
<?php
function innerGen(): Generator
{
    yield 1;
    yield 2;
    return 'inner done';
}

function outerGen(): Generator
{
    yield 0;
    $result = yield from innerGen();
    echo "Inner returned: {$result}\n";
    yield 3;
}

foreach (outerGen() as $value) {
    echo $value . ' ';  // 0 1 2 3
}
// Inner returned: inner done

// Yield from arrays
function mixedGen(): Generator {
    yield from [10, 20, 30];
    yield from new ArrayObject([40, 50]);
}

实际用例

生成器擅长处理大型或无限数据流:逐行读取文件、数据库游标迭代、分页 API 获取和数学序列。take() 模式限制无限生成器。生成器组合良好:通过多个生成器管道传输数据进行过滤、映射和归约。无论数据大小如何,内存保持恒定。

php
<?php
// 1. Read large files line by line (memory efficient)
function readLines(string $file): Generator {
    $handle = fopen($file, 'r');
    while (!feof($handle)) {
        $line = fgets($handle);
        if ($line !== false) yield rtrim($line);
    }
    fclose($handle);
}

foreach (readLines('large.log') as $line) {
    if (str_contains($line, 'ERROR')) {
        echo "{$line}\n";
    }
}

// 2. Infinite sequence with take()
function naturals(): Generator {
    $n = 1;
    while (true) yield $n++;
}

function take(Generator $gen, int $n): Generator {
    for ($i = 0; $i < $n; $i++) {
        yield $gen->current();
        $gen->next();
    }
}

foreach (take(naturals(), 5) as $num) {
    echo $num . ' ';  // 1 2 3 4 5
}
25

安全

SQL 注入预防

当用户输入被连接到 SQL 时发生 SQL 注入。始终使用带参数化查询的预处理语句。PDO 和 MySQLi 都支持它们。切勿信任用户输入。验证和清理所有外部数据。

php
// BAD: vulnerable
$sql = "SELECT * FROM users WHERE name = '" . $_POST['name'] . "'";
// GOOD: prepared statements
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = :name");
$stmt->execute([':name' => $_POST['name']]);
$users = $stmt->fetchAll();

XSS 预防

XSS(跨站脚本攻击)注入恶意脚本。htmlspecialchars 将特殊字符转换为 HTML 实体。ENT_QUOTES 转义单引号和双引号。输出用户数据到 HTML 时始终转义。使用内容安全策略头进行深度防御。

php
// BAD: output without escaping
echo $_GET['name'];
// GOOD: escape output
echo htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8');
// For HTML attributes
echo 'value="' . htmlspecialchars($value, ENT_QUOTES) . '"';

密码哈希

password_hash 使用 bcrypt(或 argon2)带自动盐生成。切勿使用 md5 或 sha1 存储密码。password_verify 检查密码与哈希的匹配。password_needs_rehash 允许升级哈希算法。盐嵌入在哈希字符串中。

php
// Hash a password
$hash = password_hash('mypassword', PASSWORD_DEFAULT);
// Verify
if (password_verify('mypassword', $hash)) {
    echo 'Valid password';
}
// Check if needs rehash
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
    $newHash = password_hash('mypassword', PASSWORD_DEFAULT);
}

CSRF 保护

CSRF(跨站请求伪造)诱骗用户执行不需要的操作。每个会话生成随机令牌。在表单中作为隐藏字段包含。POST 时使用 hash_equals 验证(时序安全比较)。SameSite cookie 提供额外保护。

php
session_start();
if (empty($_SESSION['token'])) {
    $_SESSION['token'] = bin2hex(random_bytes(32));
}
// In form
echo '<input type="hidden" name="token" value="' . $_SESSION['token'] . '">';
// Verify
if (!hash_equals($_SESSION['token'], $_POST['token'] ?? '')) {
    die('CSRF token mismatch');
}

会话安全

cookie_httponly 防止 JavaScript 访问。cookie_secure 确保仅 HTTPS。samesite=Strict 防止 CSRF。use_strict_mode 拒绝未初始化的会话 ID。session_regenerate_id 防止会话固定。权限更改后始终重新生成。

php
// Secure session settings
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);  // HTTPS only
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.use_strict_mode', 1);
session_start();
// Regenerate ID after login
session_regenerate_id(true);

这篇内容对您有帮助吗?