基础
变量、类型与常量
PHP 变量以 $ 开头,是动态类型的。PHP 7.4+ 支持类型化属性。使用 define() 定义运行时常量,使用 const 定义编译时常量(更快)。PHP 8.1 引入了枚举——一种优于类常量的类型化枚举系统。尽可能始终声明类型(PHP 7+),以实现早期错误检测和更好的 IDE 支持。
<?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
// 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
// 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
// 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 redirectInclude 与 Require
include/require 执行指定文件。如果文件缺失,require 会引发致命错误(用于关键依赖);include 仅发出警告(用于可选模板)。_once 变体跟踪已包含的文件以防止重复包含——对函数/类定义至关重要。Composer 的自动加载器(require_once 'vendor/autoload.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];字符串
字符串函数
PHP 有 100 多个字符串函数。strpos() 在未找到时返回 false——使用 === false 检查(0 是有效位置)。str_replace() 可以接受数组进行搜索/替换。substr() 支持负偏移量(从末尾开始)。对于多字节字符串(UTF-8),使用 mb_* 等效函数(mb_strlen、mb_substr)——strlen 计算字节数,而非字符数。始终设置 default_charset='UTF-8' 并对非 ASCII 文本使用 mb_* 函数。
<?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
$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
// 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
// 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
// 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);数组
索引数组与关联数组
PHP 数组实际上是有序的哈希映射——它们既可以作为列表也可以作为字典使用。索引数组自动分配数字键;关联数组使用字符串键。isset() 对 null 值返回 false;array_key_exists() 即使对 null 也返回 true。unset() 移除元素但不会重新索引。对于真正的列表(无间隙),删除后使用 array_values() 重新索引。PHP 8.1+ 有只读数组类型。
<?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
$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
$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
$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
$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]控制流
If / Else / Elseif
PHP 使用 elseif(一个词)——不是带空格的 'else if'(尽管那样也可以)。替代语法(if: ... endif;)在 HTML 模板中很有用,可避免大括号匹配混乱。三元运算符是右结合的——避免嵌套。空合并赋值运算符(??=)仅在当前为 null 时设置值——非常适合配置默认值的惰性初始化。
<?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 setSwitch 与 Match
switch 使用松散比较(==)并需要 break 来防止穿透——常见的错误来源。match(PHP 8+)使用严格比较(===),直接返回值,如果没有分支匹配则抛出异常(不会静默失败)。当需要返回值时,match 是 switch 的现代替代品。对复杂的多语句情况使用 switch;对简单的值选择使用 match。始终包含 default 情况。
<?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
// 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 // 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
// 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);
}函数
定义函数
PHP 7+ 支持类型化参数和返回类型(int、string、array、?Type 表示可空)。PHP 8+ 增加了命名参数(跳过默认值、重排参数)、联合类型(int|string)和 mixed 类型。可变参数(...$nums)将额外参数收集到数组中。展开运算符(...$arr)将数组解包为参数。按引用传递(&)修改原始值——谨慎使用,因为它使代码更难推理。
<?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
// 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
$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
// 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
// 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;
}OOP 与类
类、属性与构造函数
PHP 8 构造函数提升消除了样板代码——将属性声明为构造函数参数。属性可见性:public(任何地方)、protected(类 + 子类)、private(仅类)。readonly(PHP 8.1)防止初始化后修改。self 指当前类;static 指调用类(用于后期静态绑定)。在继承层次结构中使用 static:: 代替 self:: 以实现正确的多态。
<?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
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
// 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
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
// 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);Web、表单与文件 I/O
表单处理与验证
始终在服务器端验证——客户端验证是为了用户体验,而非安全。filter_input/filter_var 与 FILTER_VALIDATE_* 在无效输入时返回 false。在验证前修剪字符串。对数据库插入使用预处理语句。CSRF 令牌防止跨站请求伪造——每个会话生成并验证 POST。bin2hex(random_bytes(32)) 生成加密安全的令牌。切勿信任用户输入——验证、清理和转义。
<?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
// 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
// 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
// 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
// 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);数据库(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
// 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
// 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
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
$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
// 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日期/时间与安全
日期与时间
PHP 的日期函数默认使用服务器时区——始终设置 date_default_timezone_set('Asia/Shanghai') 或显式使用 DateTimeZone。DateTime 类是面向对象的,比过程式函数更好地处理时区、间隔和格式化。strtotime() 解析英文日期描述('next Monday'、'+1 month')——方便但在月份边界可能令人意外。对于日期数学,使用 DateTime::diff() 和 DateInterval。始终在数据库中以 UTC 存储日期。
<?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
// 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
// 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
// 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
// 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'];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 对调试有用,但在生产环境中省略它以获得更小的负载。