PHP 关键字与语言特性字典。每条给语义、版本约束、多场景示例、边界与坑。PHP 8.x 新语法(enum/match/readonly/属性提升)已并入条目。
1. declare(strict_types=1) – 严格类型模式
描述
文件级开关,必须写在文件最顶部(在 namespace 之前)。开启后参数/返回值类型不匹配直接抛 TypeError,不再做隐式转换。只影响当前文件。
语法和示例
<?php
declare(strict_types=1);
function add(int $a, int $b): int { return $a + $b; }
add(1, 2); // 3
add('1', 2); // TypeError(默认模式会静默转成 1)
add(1.5, 2); // TypeError(float 不会截断成 int)
// 但函数内部仍可隐式转换
function loose(int $a) { $b = $a + 1.5; } // $b 是 float,合法注意事项
- strict_types 必须写在文件第一行(注释之外),否则 fatal error
- 只对「本文件内定义/调用的函数」生效——调用别处函数时以被调用方文件的设置决定
- 返回值声明同样受严格检查:声明 int 就不能返回 ‘1’
- 严格模式不等于「类型安全」——数组元素类型、对象属性都不检查
- 推荐新项目全开;存量项目逐步开(先从新文件开始)
2. 变量作用域与超全局 $GLOBALS
描述
函数/方法内不可见外部变量,除非 global 声明或使用 $GLOBALS。超全局数组($_GET/$_POST/$_SERVER/$_SESSION/$_COOKIE/$_FILES/$_ENV/$_REQUEST)在任何作用域直接可见。
语法和示例
<?php
$count = 0;
function bump(): void {
global $count; // 显式引入全局
$count++;
$GLOBALS['count']++; // 等价写法
}
function demo(): void {
// echo $count; // 报 Warning: undefined variable
$local = 1; // 函数内局部变量
}
// 超全局:无需 global
function read(): string {
return $_SERVER['HTTP_HOST'] ?? '';
}
// 静态变量:跨调用保持
function counter(): int {
static $n = 0; // 只初始化一次
return ++$n;
}注意事项
- global 使函数依赖外部状态,可读性与可测性都差——优先用参数传值
- 静态变量在 PHP-FPM 下是「每次请求重置」的,不能当进程级缓存
- $GLOBALS 是超全局,任何作用域可用,但同样难维护
- PHP 8.1+ 未定义变量从 Notice 升为 Warning,读未定义数组键需 isset/??
- 作用域里 include 文件的变量可见性继承当前作用域,容易踩坑
3. 类型:松散比较 == 与严格比较 ===
描述
PHP 8 收紧松散比较:数字与字符串比较时,仅当字符串是「合法数字字符串」才按数字比,否则按字符串比。=== 同时比较值与类型。
语法和示例
<?php
var_dump(0 == 'abc'); // false(PHP 8;PHP 7 是 true)
var_dump(1 == '1'); // true(合法数字串)
var_dump(1 === '1'); // false(类型不同)
var_dump('1e3' == '1000'); // true(都是数字串)
var_dump(null == false); // true
var_dump('' == 0); // false(PHP 8)
var_dump([] == false); // true
// 安全比较
in_array('1', [1, 2], true); // 严格模式
array_search(0, ['a', 0], true);注意事项
- 永远用 === 与 in_array(…, true)——== 的历史坑太多
- null == false 为 true 是 PHP 的特殊规则,容易误判
- 字符串与数字比较在 PHP 8 前有巨大坑(0 == ‘abc’ 为 true),升级时注意行为变化
- 浮点数比较不要用 == ,用 abs($a – $b) < 精度阈值
- == 与 === 在数组比较上语义不同(顺序敏感、类型敏感)
4. array – 数组(有序哈希映射)
描述
PHP 数组把 list 与 dict 合一:键只能是 int 或 string,按插入顺序排序。赋值是写时复制(值语义)。
语法和示例
<?php
$list = ['a', 'b', 'c']; // 键 0,1,2
$map = ['go' => 1, 'php' => 2];
$list[] = 'd'; // 尾部追加
$map['js'] = 3;
// 键的类型转换规则
$a = [true => 1]; // 键转 1
$b = ['1' => 1]; // 数字字符串键转 int 1
$c = [1.5 => 'x']; // 浮点键截断为 int 1
// 遍历(遍历的是快照,内部改动不影响本次)
foreach ($map as $k => $v) { }
// 拷贝与引用
$copy = $list; // 值拷贝(写时复制)
$ref = &$list; // 引用
unset($list[1]); // 删键(不重排)
$reindexed = array_values($list);注意事项
- 键为 float 会被截断成 int;bool 转 0/1;null 键变空字符串
- unset 后数组键不会重排,json_encode 会变成对象而不是数组——需 array_values 重建
- 数组赋值是值语义,但对象赋值是引用语义($obj2 = $obj1 是同一对象)
- foreach 里修改数组:改值不影响遍历(快照),但改引用 &$v 会影响
- 数组合并:+ 保左优先,array_merge 数字键重排、字符串键覆盖
5. match 表达式(8.0+)
描述
switch 的表达式版:严格比较(===)、无 fall-through、可返回值、必须穷尽或给 default。用 match(true) 可做条件分发。
语法和示例
<?php
$msg = match ($code) {
200, 201 => '成功',
302 => '重定向',
404 => '未找到',
default => '其他',
};
// 条件分发(替代 switch + if 链)
$level = match (true) {
$score >= 90 => 'A',
$score >= 80 => 'B',
$score >= 60 => 'C',
default => 'F',
};
// 返回值可直接用
$len = match (strlen($s)) { 0 => 'empty', default => 'ok' };注意事项
- match 用 === 严格比较,’1′ 与 1 不匹配(switch 是松散比较)
- 没有 default 且未穷尽时抛 UnhandledMatchError(不是返回 null)
- match 的分支只能是一个表达式,不能写多语句(要复杂逻辑先抽函数)
- match(true) 模式要按范围从大到小或小到大排列,先命中的先返回
- PHP 8.0 才有,低版本用 switch 或 if 链
6. 构造器属性提升(8.0+)与 readonly(8.1+)
描述
构造器参数直接声明为类属性,省去手写声明与赋值。配 readonly 实现不可变属性(只能初始化一次)。8.2+ 支持 readonly 类。
语法和示例
<?php
class Point {
public function __construct(
public readonly float $x,
public readonly float $y,
private string $label = 'pt',
) {}
}
$p = new Point(1.0, 2.0); // 其余参数用默认值
// $p->x = 9; // Error:readonly 不可改
// 只读类(8.2+):所有属性自动 readonly
readonly class Config {
public function __construct(public string $env) {}
}
// 可见性可混合:public/private/protected + readonly注意事项
- readonly 属性只能在声明类的作用域内初始化(不能从外部写)
- 提升的参数不能有默认值之外的初始化表达式(如 new Foo() 目前允许但需注意)
- readonly 不保证对象内部不可变(引用类型属性仍可变)
- 提升的可选参数不能与可变参数一起用(顺序限制)
- 反射修改 readonly 属性会抛 Error(8.1 起)
7. enum 枚举(8.1+)
描述
纯枚举(无值)或 backed 枚举(带 int/string 值)。自带 cases() 列出全部、from/tryFrom 从值还原。可实现接口、带方法与常量。
语法和示例
<?php
enum Status: string {
case Draft = 'draft';
case Published = 'published';
public function label(): string {
return match($this) {
self::Draft => '草稿',
self::Published => '已发布',
};
}
}
Status::cases(); // 全部枚举项数组
Status::from('draft'); // Status::Draft(非法值抛 ValueError)
Status::tryFrom('x'); // null(不抛)
Status::Draft->label(); // '草稿'
Status::Draft->value; // 'draft'
// 纯枚举
enum Suit { case Heart; case Spade; }
// 可实现接口
enum Level: int implements JsonSerializable { case Low = 1; }注意事项
- enum 的 case 是单例对象,不能 new,也不能 clone
- backed enum 的值必须唯一(重复值 fatal)
- enum 不能有状态(属性),只能有常量与方法
- from 遇非法值抛 ValueError;tryFrom 返回 null——外部输入用 tryFrom
- json_encode 默认输出枚举的 value,要自定义实现 JsonSerializable
8. trait – 横向复用
描述
方法集合可注入多个类,绕过单继承限制。可含属性、抽象方法、静态方法。同名冲突用 insteadof 排除、as 改名。
语法和示例
<?php
trait Timestampable {
public ?string $createdAt = null;
public function touch(): void { $this->createdAt = date('c'); }
abstract public function id(): int; // 要求使用方实现
}
class Post {
use Timestampable;
public function id(): int { return 1; }
}
// 冲突处理
trait A { public function hi() { return 'A'; } }
trait B { public function hi() { return 'B'; } }
class C {
use A, B {
A::hi insteadof B; // 用 A 的实现,排除 B
B::hi as hiB; // 把 B 的实现改名保留
}
}注意事项
- trait 不是类型——不能 instanceof、不能被 extends
- trait 的属性与类属性同名且默认值不同会 fatal error
- trait 里的 $this 指向使用它的类实例
- 静态方法在 trait 里可用,但 static:: 后期绑定仍按使用类解析
- 过度使用 trait 会让依赖关系难追踪,优先考虑组合(依赖注入)
9. 可见性 private/protected/public
描述
对象级可见性。与 Java 不同:同类的不同实例可互相访问私有成员。PHP 8.4 支持非对称可见性(public private(set) 这种)。
语法和示例
<?php
class Wallet {
private int $balance = 0;
public function __construct(private readonly string $owner) {}
public function transferFrom(Wallet $other, int $n): void {
$other->balance -= $n; // 同类实例可访问私有!
$this->balance += $n;
}
protected function audit(): void {}
}
// 8.4 非对称可见性
class Post {
public private(set) string $title = '';
}注意事项
- protected 在子类可访问,但只能通过 $this 或父类作用域,不能访问「另一个实例的」protected
- private 方法不被子类继承——子类同名方法独立存在
- 接口方法必须 public;抽象方法可见性由实现类决定
- 属性类型声明后不可赋其他类型(PHP 有类型属性,8.0+)
10. 命名空间与 use 导入
描述
namespace 声明当前文件所属命名空间(必须在首行,declare 之后);use 导入类/函数/常量并可别名。PSR-4 目录与命名空间一一对应。
语法和示例
<?php
namespace App\Service;
use App\Model\User;
use App\Model\{Order, Item}; // 组导入
use App\Model\User as U; // 别名
use function App\Util\format; // 导入函数
use const App\Util\LIMIT; // 导入常量
// 全局类需反斜杠
new \DateTimeImmutable();
// 当前命名空间引用
self::class; static::class;
// 相对命名空间
namespace\sub\foo();注意事项
- namespace 必须是文件第一条语句(除 declare 与注释)
- 同一个文件可以有多个 namespace(不推荐,会影响可读性)
- use 只影响编译期的名字解析,不触发加载——加载靠 autoload
- 全局函数(如 strlen)未导入时 PHP 会回退到全局命名空间,但类不会
- PSR-4 要求命名空间与目录层级严格对应,否则 autoload 找不到
11. null 合并 ?? 与三目 ?:
描述
?? 只在左侧为 null/不存在时取右(不报未定义键);?: 在左侧为任意 falsy(0/”/’0’/[]/null/false)时取右。两者语义完全不同。
语法和示例
<?php
$page = $_GET['page'] ?? 1; // 键不存在也不报
$name = $input['name'] ?? '匿名';
$deep = $a['b']['c'] ?? null; // 链式安全
// 与 ?: 的差异(关键)
$zero = 0;
$r1 = $zero ?? 10; // 0(0 不是 null)
$r2 = $zero ?: 10; // 10(0 是 falsy)
$empty = '';
$r3 = $empty ?? 10; // ''
$r4 = $empty ?: 10; // 10
// 链式
$v = $a ?? $b ?? $c ?? 'default';注意事项
- ?? 不做类型转换,「存在且为 null」时才取右——精确符合 isset 语义
- ?: 会把 ‘0’ 与 [] 都视为空,容易把合法的 0 值误替换
- ?? 右侧表达式惰性求值(左侧有值时不执行)
- 嵌套 ?? 从左到右取第一个非 null,不会短路在 falsy 上
- 8.0+ 的 nullsafe 操作符 ?-> 用于方法链,与 ?? 组合使用
12. 异常体系 throw/try/catch/finally
描述
Throwable 分 Error(引擎错误)与 Exception。PHP 8 把多数引擎错误改为 Error 子类(TypeError/ValueError 等),catch Exception 不再能捕获它们。
语法和示例
<?php
try {
risky();
} catch (TypeError | ValueError $e) { // 多类型(7.1+)
// PHP 8 引擎异常也走这里
} catch (RuntimeException $e) {
throw new AppException('包装', previous: $e); // 命名参数传链
} catch (Throwable $e) {
error_log((string) $e); // 兜底
} finally {
cleanup(); // 无论成败都执行
}
// 自定义异常
class AppException extends RuntimeException {}注意事项
- catch Exception 捕不到 Error(TypeError/ArithmeticError 等)——需要 catch Throwable
- finally 在 return 之前执行;finally 里 return 会覆盖返回值
- 异常链用第三个参数 previous 传,error_log 不会自动打堆栈
- PHP 8 前很多错误是 Warning 不抛异常——用 set_error_handler 转异常
- 不要用异常控制正常流程(性能与可读性都差)
13. __construct/__destruct 与魔术方法
描述
魔术方法以双下划线开头:__construct/__destruct/__get/__set/__call/__toString/__invoke/__clone/__serialize。定义对象生命周期与动态行为。
语法和示例
<?php
class Magic {
private array $data = [];
public function __get($name) { return $this->data[$name] ?? null; }
public function __set($name, $v) { $this->data[$name] = $v; }
public function __isset($name) { return isset($this->data[$name]); }
public function __call($m, $args) { return "调用 $m"; }
public function __toString(): string { return 'Magic'; }
public function __invoke(): string { return 'callable'; }
public function __construct() {}
public function __destruct() {} // 对象销毁时
}
$m = new Magic();
$m->foo = 1; // 走 __set
$m->foo; // 走 __get
$m(); // 走 __invoke
echo $m; // 走 __toString注意事项
- 魔术方法必须 public,否则无效(会警告)
- __destruct 的执行时机不确定(GC 时机),不适合做关键清理
- __get/__set 只在属性不可访问时触发——已有同名 public 属性不触发
- __toString 不能抛异常(历史限制,8.0 后允许但别用)
- 魔术方法有性能开销,热点路径避免依赖
14. Heredoc / Nowdoc / 字符串插值
描述
双引号解析变量与转义;单引号原样(仅 \’ 与 \\);Heredoc 多行插值;Nowdoc(<<<'ID')不插值。复杂表达式用 {$expr}。
语法和示例
<?php
$name = 'World';
echo "Hello $name"; // 插值
echo "Total: {$order->total}"; // 复杂表达式必须大括号
echo 'Skip \n $name'; // 原样输出
echo "Line\nBreak"; // 转义生效
// Heredoc(插值,等价双引号)
$sql = <<<SQL
SELECT * FROM users WHERE name = '$name'
SQL;
// Nowdoc(不插值,等价单引号)
$tpl = <<<'TPL'
原始文本 $name
TPL;注意事项
- Heredoc/Nowdoc 的结束标识必须行首(8.x 起允许缩进)
- 双引号里的数组下标要 $arr[0] 或 {$arr[‘k’]}——$arr[key] 在字符串里会解析错
- 单引号只转义 \’ 和 \\,其他反斜杠原样保留
- 插值不支持函数调用:\”{$obj->method()}\” 是语法错误(先赋给变量)
- 大段 SQL 用 Heredoc 可读性远好于拼接