0%

PHP杂谈

干冰不是冰,水银不是银,纯碱不是碱,熊猫不是猫,鲸鱼不是鱼,双氧水不是水,铅笔芯不是铅。

echo、print、print_r

一、echo、print、print_r

1 本质区别(笔记此处有误)

项 echo print print_r()
是什么 语言结构(keyword) 语言结构(keyword) 函数
能否写 $x = echo 1 ❌ 不能当值用 ❌ 不能当值用 ✅ 有返回值
返回值 无 int(1),恒为 1 无(默认)/ 字符串(第二参数为 true)
参数个数 可多个:echo 1, 2, 3 只能 1 个:print 1, 2 → 语法错误 2 个(值、$return)
能输出什么 字符串(可含 HTML) 同 echo 数组 / 对象等复合类型
输出格式 原样 原样 Array ( [k] => v ),一行一个元素
嵌套深度 — — 默认只展开 1 层,更深层显示 Array ( *RECURSION* ) 或 Array
用法频率 高 低 调试时

❌ 笔记错误:「echo 不是函数,没有返回值,print 是一个函数有返回值」——后半句错。print 和 echo 同为语言结构,都不是函数。
判别方法:echo print 'x'; 输出 x1——如果 print 是函数它不会有返回值可嵌套。(实际 PHP 中这种嵌套是语法错误,这个实验只用于说明,不要真写。)

2 三个都能输出什么

需求 用什么
输出字符串 / HTML echo
输出标量数组的键值对 echo json_encode($arr, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
调试数组 / 对象结构 var_dump()(看类型和长度)或 print_r()(看结构)
需要把结果存进变量 print_r($arr, true) 或 var_export()

3 完整的输出函数对照

函数 用途 主要参数 返回
echo 最快输出字符串 任意多个值 无
print 几乎不用 单值 int(1)
printf() 格式化输出 格式串 + 值 长度
vprintf() 格式化 + vsprintf 格式串 + 数组 长度
sprintf() 格式化成字符串 格式串 + 值 字符串
print_r() / var_dump() 调试数组对象 值、$return 视情况
var_export() 导出可执行的 PHP 代码 值、$return 视情况

4 print_r 的两个参数

1
2
3
4
5
6
7
8
$arr = ['a' => 1, 'b' => ['c' => 2]];

// 1. 直接打印(调试)
print_r($arr);

// 2. 返回字符串(PHP 5.3+ 的第二个参数 $return = true)
$str = print_r($arr, true);
echo $str;
坑 说明
嵌套对象显示 Class Object ( [id] => 1 ) 看不清私有属性,改用 var_dump
深度默认 1 层 更深层只显示 Array,需要 print_r($a, true) 后自己处理
递归引用会输出 *RECURSION* 无限递归结构会看到它
别在生产用 print_r / var_dump 直接把数据吐给用户了。必须 error_log() 兜到日志

5 print_r vs var_dump vs var_export

特性 print_r var_dump var_export
展开嵌套 默认 1 层 全展开 完整
显示类型 ❌ ✅(string(3) "abc") ✅
显示长度 ❌ ✅ ❌
输出格式 Array ( [a] => 1 ) array(1) { ["a"]=> int(1) } array ( 'a' => 1, )
可被 eval 回来 ❌ ❌ ✅

结论:

场景 用
快速看结构 print_r()
排查类型/长度问题(最常用) var_dump()
生成可执行代码 / 缓存数据 var_export()

empty 与 isset

1 核心区别

判断函数 实际在问什么 判定为真的条件
isset($v) 「$v 是不是 null」 变量存在 且 值 !== null
empty($v) 「$v 是不是假值」 变量不存在,或 值属于假值集合

一句话记忆:isset 只认 null 这一个敌人,empty 把所有假值都当敌人。

2 全值域对照表

值 isset() empty() 说明
变量未定义 false true
null false true isset 和 empty 的分水岭
false true true
true true false
0 true true
0.0 true true
''(空字符串) true true
'0'(字符串零) true true ⚠️ '0' 也是空的
' '(空格) true false 空格不算空
'0.0' true false
[](空数组) true true
[0] true false 有元素就不算空
new stdClass()(无属性对象) true true 无属性对象算空
对象有属性 true false
资源 fopen(...) true false 资源恒非空

⚠️ 最坑的两个:

  1. '0'(字符串零)会被 empty() 判为空。判「非空字符串」要写 $v !== '' && $v !== '0',或者直接 $v !== '' 加上 trim()。
  2. 无属性的对象也被 empty() 判为空。$user = new User(); empty($user) → true,哪怕对象本身存在。

3 多变量 / 多参数行为

写法 行为
isset($a, $b, $c) 全部不为 null 才返回 true;短路,遇到 false 就停
isset($a) && isset($b) 同上,但更啰嗦
`isset($a) && isset($b)
empty($a) && empty($b) 两个都空才真;empty 也短路
isset($a, $b) === true 明确优先级,避免误读

单变量时(isset($a && $b))会报语法/警告,因为 isset 只接受变量。

4 限制与注意

项 isset() empty()
只能用于变量? ✅ 是。传常量 → Fatal error(需用 defined()) ✅ 是。传函数返回值 → Fatal error:Cannot use empty() on the result of an expression
能用于数组下标 ✅ isset($arr['k']) ✅ empty($arr['k'])
能用于对象属性 ✅ isset($obj->p) ✅ empty($obj->p)
多参数 ✅ PHP 7.0+ ❌ 只接受 1 个变量
嵌套安全 ✅ 内部任一层不存在直接返回 false,不报 warning ✅ 同左,且返回 true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// isset 的嵌套安全性 —— 这是它比 empty 更常用的原因
$data = [];
isset($data['user']['name']); // false,且【不产生任何警告】
// 直接取值才会炸:$data['user']['name'] → Warning: Undefined array key

empty($data['user']['name']); // true,无警告

// unset 后
$v = 1;
unset($v);
isset($v); // false
empty($v); // true

// 异常导致 unset 语义
$obj->x = null;
isset($obj->x); // false ← 属性存在但为 null
empty($obj->x); // true

5 array_key_exists vs isset

这是本节最实用的对比。

场景 isset($arr[$k]) array_key_exists($k, $arr)
键存在,值非 null true true
键存在,值为 null false true
键不存在 false false
用于对象 ✅ 可用 ❌ 只支持数组(PHP 8.0 前传对象报 TypeError)
嵌套访问 ✅ 安全不报错 ❌ 要求每一层键都真实存在,否则报 warning/error
性能 快 略慢
1
2
3
4
$search_array = ['first' => null, 'second' => 4];

var_dump(isset($search_array['first'])); // bool(false) ← 漏判
var_dump(array_key_exists('first', $search_array)); // bool(true) ← 正确

语义定位:

你真正想问的 用
「这个值有值吗」(null 视为无) isset()
「这个键存在吗」(哪怕值是 null) array_key_exists()
「这是空数组吗」 empty()

经典场景: 数据库查出的字段天然可能是 null(比如「备注未填」)。用 isset($row['remark']) 判断「有没有填备注」会把已填 null 和字段不存在混为一谈:

1
2
3
4
5
6
7
8
// ❌ 错:数据库返回 remark = NULL 和返回列都没有,两种情况都判 false
if (isset($row['remark'])) { ... }

// ✅ 对:只看列是否存在
if (array_key_exists('remark', $row)) {
// remark 存在,但值可能是 null
$remark = $row['remark'] ?? '';
}

兜底写法(同时判存在 + 非空):

1
2
3
4
5
6
7
// 7.4+ 一行搞定
$remark = $row['remark'] ?? '';

// 或显式组合
if (array_key_exists('remark', $row) && $row['remark'] !== null && $row['remark'] !== '') {
// 真正有内容
}

6 三者 + array_key_exists 总对照

值 isset empty array_key_exists
键/变量未定义 false true false
值为 null false true true
值为 0 true true true
值为 '' true true true
值为 '0' true true true
值为 [] true true true
值为 false true true true

记忆法: empty 全 false 一票否决;isset 只针对 null;array_key_exists 只关心键在不在。


include 与 require

1. ❗ 修正笔记错误

❌ 笔记错误:「include 在用到时加载,require 在一开始就加载」——这是错的。

事实 说明
include 和 require 都在语句所在的那一行加载 文件里写第 10 行,就执行到第 10 行才加载
真正决定「只加载一次」的是 _once 后缀 include_once / require_once
两者唯一的区别是失败后的行为 include 报警告继续跑,require 致命错误停止

2 四个变体总表

写法 加载时机 文件缺失时 是否重复加载
include 执行到该语句时 E_WARNING,继续执行,返回值 false 会重复
include_once 执行到该语句时 E_WARNING,继续执行 已包含过则跳过
require 执行到该语句时 E_ERROR(致命),立即终止整个脚本 会重复
require_once 执行到该语句时 E_ERROR,终止脚本 已包含过则跳过

💡 区分「加载时机」:只有在函数内且延迟调用的写法,文件才会真正「用到时」加载——但那是代码写法决定的,不是 include vs require 的区别。

3 返回值

项 include / require
被包含文件有 return 返回该值
被包含文件无 return 或 return; 返回 int(1)
文件不存在 include → false;require → 致命错误不会执行到
1
2
3
4
5
6
7
8
9
10
// 利用返回值判断文件是否存在(require 不能这么做,会直接挂)
if (file_exists($f) && ($data = include $f) !== false) {
echo $data;
}

// 惯用技巧:依赖加载返回值抛异常
$conf = include 'config.php';
if ($conf === false) {
throw new RuntimeException('配置文件加载失败');
}

4 路径与作用域

项 规则
相对路径基准 先查 include_path(含 .),再查当前脚本所在目录,再查调用脚本所在目录,最后查工作目录
路径含 / 时 会被当作普通相对路径,不再查 include_path
变量作用域 被包含文件继承调用处的作用域;函数和类定义在包含后全局可见(除非在函数内定义)
$GLOBALS 可在配置文件里用它给全局赋值
1
2
3
4
5
6
// 配置文件惯用法
if (!isset($config)) {
$config = [];
}
$config['db'] = ['host' => '127.0.0.1'];
return $config;

5 安全风险

风险 说明 防护
远程文件包含(RFI) include $_GET['page']; 可被远程接管 白名单 in_array($page, $allowed, true)
本地文件包含(LFI) 可读任意文件,配合上传即 RCE 同上 + 禁用户可控路径
目录穿越 ../../etc/passwd realpath() 后校验前缀
变量可控路径 include $configPath; 用常量或白名单
1
2
3
4
5
6
7
8
9
10
11
// ❌ 危险
include $_GET['page'];

// ✅ 安全
$pages = ['home', 'about', 'contact'];
$page = $_GET['page'] ?? 'home';
if (!in_array($page, $pages, true)) {
http_response_code(404);
exit('Not found');
}
include __DIR__ . "/pages/{$page}.php";

6 与 eval、autoload 的关系

机制 用途
include / require 手动引入文件
include_once / require_once 手动引入 + 防重复
spl_autoload_register() 自动按命名规则加载类,Composer 依赖它
eval() ⚠️ 直接执行字符串,不要用于加载配置

现代 PHP 项目里,手写 require 基本只出现在「加载配置数组」和「入口文件」两种场景;类的加载都交给 Composer 的 autoloader。


传值与传引用

1 基本概念

方式 语法 形参与实参关系 改形参影响实参
传值 function f($x) 值副本 ❌ 不影响
传引用 function f(&$x) 同一个变量(别名) ✅ 影响
1
2
3
4
5
6
7
8
9
10
11
// 传值
function byValue($x) { $x = 100; }
$a = 1;
byValue($a);
echo $a; // 1 —— 没变

// 传引用
function byRef(&$x) { $x = 100; }
$b = 1;
byRef($b);
echo $b; // 100 —— 变了

2 ❗ 关键补充:对象是特例

笔记漏了最重要的一条:对象永远传句柄,不存在「传对象值 / 传对象引用」的区分。

1
2
3
4
5
6
7
8
9
10
class User { public $name = 'A'; }

function rename(User $u) { $u->name = 'B'; }
$c = new User();
rename($c);
echo $c->name; // B —— 变了!不需要 &

function rename2(User $u) { $u = new User(); } // 改的是局部变量
rename2($c);
echo $c->name; // 还是 B —— 换了个新对象,和 $c 无关
情况 修改影响实参?
改对象的属性 $obj->p = x ✅ 影响(对象是引用语义)
给形参重新赋值 $obj = new X ❌ 不影响(只是改了局部变量指向)
改数组的元素 $arr['k'] = x ❌ 不影响(数组是值语义,除非传引用)

3 写时复制(COW)

项 说明
全称 Copy-On-Write
行为 传参时不立即拷贝,只是引用计数 +1;只有形参被写入时才真正复制一份
好处 只读参数零拷贝,性能好、内存省
副作用 $a = ['x' => 1]; $b = $a; 后 $a 和 $b 内存地址相同,但改一个不影响另一个
1
2
3
4
$a = ['x' => 1];
$b = $a;
var_dump($a === $b); // true —— 值相等
// debug_zval_refcount 显示此时并未拷贝

4 数组与对象对照(最容易混的地方)

类型 传参语义 改属性/元素 重新赋值
标量(int/string/bool/float/null) 值 — 需 &
数组 值(COW) ❌ 需 & 需 &
对象 句柄 ✅ 直接生效 ❌ 不生效(& 也不行)
类实例 & 句柄 + 变量引用 ✅ ⚠️ 可改形参的指向
1
2
3
4
5
6
7
8
9
10
11
12
// 数组:需要 & 才能回写
function fill(array &$arr) { $arr['new'] = 1; }
$x = [];
fill($x);
print_r($x); // 有 new 键

// 对象:不用 & 也能改属性
class Bag {}
function put(Bag $b) { $b->item = 'apple'; }
$y = new Bag();
put($y);
echo $y->item; // apple

5 引用作为返回值

1
2
3
4
5
6
7
8
function &getRef(array &$arr) {   // 注意 & 有两个位置
return $arr['key'];
}

$data = ['key' => 1];
$ref = &getRef($data);
$ref = 99;
echo $data['key']; // 99
位置 含义
参数前的 & 按引用传参
返回类型前的 & 返回引用,调用方可用 =& 接收并修改源变量
调用方的 =& 接收引用(等价于引用赋值)

⚠️ 引用返回极易产生「意外修改」和循环引用,现代 PHP 基本不该用。

6 引用相关的常见坑

坑 现象 原因 解法
foreach 不加引用 改 $v 不影响数组 值遍历 foreach ($arr as $k => &$v)
foreach 加引用忘了 unset 后续循环把上一个元素改坏 引用残留 循环体末尾 unset($v),必须
引用了不存在的变量 自动创建并变 null 引用赋值的特性 传引用前先 isset() 判断
形参默认值不能是引用 语法错误 — 用 ?array $x = null + 内部处理
返回引用被误用 改一个地方多处变 引用返回 返回值而不是引用

foreach 引用残留的完整演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ❌ 有 bug
$arr = [1, 2, 3];
foreach ($arr as &$v) {
$v *= 2;
}
print_r($arr); // [2, 4, 6] 正确

foreach ($arr as $item) {
echo $item;
}
echo $arr[0]; // ❌ 被改成 6 了!($item 仍指向 $v)

// ✅ 正确
foreach ($arr as $k => $v) { // 故意不用 &
echo $v;
}
echo $arr[0]; // 2 正常

💡 口诀:只要写了 &$v,循环结束就补一句 unset($v)。 这是 PHP 单兵作战最容易忘的一行代码。

7 什么时候用引用(现代 PHP 建议)

场景 用引用? 理由
函数要回写调用方的一个变量 ✅ 可以 但返回新值更清晰
大数组要避免拷贝 ⚠️ 权衡 COW 已让只读传参零拷贝,只有写操作才有拷贝
循环里高频改数组 ❌ 不要 先复制出来改,改完整体赋值
遗留老代码 ✅ 保持 别在重构时顺手改,容易出事
默认选择 ❌ 不用引用 返回值语义清晰、可测试、无副作用

更地道的写法:

1
2
3
4
5
6
7
8
9
10
// ❌ 依赖副作用
function normalize(array &$user) { $user['name'] = trim($user['name']); }
normalize($user);

// ✅ 无副作用,返回新值(更好测)
function normalize(array $user): array {
$user['name'] = trim($user['name']);
return $user;
}
$user = normalize($user);

8 三种赋值方式速查

语法 含义 $b 与 $a 关系 改 $b 影响 $a
$b = $a; 值赋值 值相同,内存可能共享(COW) ❌
$b = &$a; 引用赋值 完全同一变量 ✅
$b = clone $a; 对象浅拷贝 独立对象,内部引用仍共享 ❌
$b = &$a; unset($a); 引用不断 $b 仍有效 —
$a = null;(有引用时) 只是断开 $a 的绑定 $b 不受影响 —

⚠️ 引用赋值会让两个变量同生共死:unset($a) 不会销毁 $b。这和值赋值($a = null 后 $b 仍是原值)完全不同。

9 参考

  1. https://www.php.net/manual/zh/language.references.php
  2. https://www.php.net/manual/zh/features.commandline.options.php

抽象类和接口

零、笔记错误速览(先看这个)

# 笔记原文 问题 修正
1 「接口是一种特殊的抽象类」 ❌ 概念性错误。接口和抽象类是并列的两种类型,接口不是抽象类的子类。两者只是语法长得像(都用 {} 声明、都可声明未实现的方法),且在 PHP 里互不能转换 二者是「抽象的两种方式」,不是「父子关系」
2 「接口没有数据成员」 ⚠️ PHP 8.4 起过时。自 PHP 8.4.0 接口可以声明属性(须配合 property hooks 指定 get/set 需求) PHP ≤ 8.3 无属性;≥ 8.4 可声明属性契约
3 「接口没有构造方法」 ⚠️ 不准确。接口中可以声明 __construct(),只是 PHP 官方强烈不建议 不是禁止,是「不要这么写」

为什么第 1 条重要: 很多人以为「接口是抽象类的子类」,于是想 abstract class A implements I 这种写法,或者以为抽象类能当接口用。实际上:

误解 事实
接口是抽象类的子类 ❌ 二者无继承关系,interface 不能 extends 抽象类
抽象类可以实现接口的方法 ✅ 可以,abstract class X extends Y implements Z {} 完全合法
接口可以 extends 接口 ✅ 可以(笔记没提到)
接口可以被实例化 ❌ 不能,和抽象类一样

一、核心对比表

对比项 抽象类(abstract class) 接口(interface)
声明关键字 abstract class interface
能否实例化 ❌ 不能 ❌ 不能
能否有属性(成员变量) ✅ 有,可任意修饰符 ⚠️ PHP ≤8.3 无;8.4+ 可声明属性契约
能否有常量 ✅ 有 ✅ 有(8.1+ 可被覆盖)
能否有构造函数 ✅ 有,且常用于初始化 ⚠️ 可声明但强烈不建议
能否有方法实现 ✅ 可有(具体方法) ❌ 全为抽象(无方法体)
能否有抽象方法 ✅ 有 ✅ 全都是
方法可见性 public / protected / abstract 只能 public
抽象方法能否 private ❌ 不能(写 private abstract 报错) ❌ 概念不存在
继承方式 extends(最多一个) implements(可以多个)
能否被接口继承 — ✅ interface B extends A
与普通类的关系 抽象类是普通父类 接口只能被实现,不能被 extends 到具体语义
命名约定 无强制 常以 -able / 名词(Logger、Cacheable)

二、笔记逐条修正

2.1 「抽象类的继承使用 extends,接口用 implements」

✅ 正确,但笔记不完整。补充:

场景 语法 说明
类继承抽象类 class B extends A {} 只能一个
类实现接口 class C implements I {} 可以多个,逗号分隔
同时继承 + 实现 class D extends A implements I, J {} 最常用组合
接口继承接口 interface B extends A {} 笔记没提
接口继承多个接口 interface C extends A, B {} 注意:extends 在接口间可以多个

💡 记忆点:extends 在接口之间是多个,在类之间是一个。这是唯一打破「extends 只能一个」的地方。

2.2 「抽象类有数据成员,接口没有」

版本 抽象类 接口
PHP ≤ 8.3 有属性 ❌ 不能有属性
PHP 8.4+ 有属性 ✅ 可声明属性契约

PHP 8.4 新增的接口属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
interface HasName {
// 实现类必须有一个「公开可读」的属性,可写性不限制
public string $readable { get; }

// 实现类必须有一个「公开可写」的属性
public string $writeable { set; }

// 必须可读可写
public string $both { get; set; }
}

// ✅ 方式一:普通属性直接满足
class C1 implements HasName {
public string $readable;
public string $writeable;
public string $both;
}

// ✅ 方式二:只实现要求的 hook
class C2 implements HasName {
private string $written = '';
private string $all = '';

// 虚拟属性:只读,内容来自另一个属性
public string $readable { get => strtoupper($this->writeable); }

public string $writeable { get => $this->written; set { $this->written = $value; } }

public string $both { get => $this->all; set { $this->all = strtoupper($value); } }
}
规则 说明
必须声明 get 还是 set 不能只写 public string $name;——必须写 { get; } / { set; } / { get; set; }
接口声明只约束 public 读写 内部可见性实现类自定
{ set; } 的属性不能用 readonly 实现 冲突
{ get; } 可以用 readonly 满足 ✅ 允许

⚠️ 这是 PHP 8.4 才有的能力。如果你项目跑在 8.3 或更低,接口依然不能有属性。

2.3 「抽象类可以有构造方法,接口没有」

❌ 不准确。接口可以声明 __construct(),只是官方强烈不建议。

1
2
3
4
// 语法合法,官方手册明确说「虽然支持,但强烈不建议」
interface HasInit {
public function __construct(string $name);
}

官方不建议的三个理由:

理由 说明
大幅降低实现类的灵活性 一旦接口要求构造签名,所有实现类都得照抄,无法用不同参数初始化
构造器不受继承规则强制约束 接口要求了,但没人能保证实现类的构造行为一致 → 行为不可预期
类型提示已够用 构造参数约束用具名类型表达,不需要塞进接口

正确做法: 把初始化逻辑抽成普通方法或工厂方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ❌ 别这么干
interface HasUser {
public function __construct(string $name, int $age);
}

// ✅ 推荐一:初始化方法进接口
interface HasUser {
public function init(string $name, int $age): void;
}

// ✅ 推荐二:工厂方法
interface UserFactory {
public function create(string $name, int $age): HasUser;
}

2.4 「抽象方法可用 private/protected/public 修饰」

修饰符 抽象方法能否用 说明
public ✅ 最常见
protected ✅ 只对子类可见,子类必须实现
private ❌ 语法错误:private 方法不能被子类看到,声明了子类也没法实现 → 逻辑上无意义
无修饰符 ✅ 等价于 public
final ❌ 与 abstract 互斥,同时写报错

正确写法:

1
2
3
4
5
6
7
8
abstract class Shape {
// ✅ 可以
abstract public function area(): float;
abstract protected function init(): void;

// ❌ 报错:Cannot use private for abstract method
// abstract private function foo(): void;
}

2.5 「一个类只能继承一个抽象类,可以实现多个接口」

✅ 完全正确。补充组合形式:

组合 语法 典型场景
只实现多接口 class C implements I, J, K {} 无共同父类的能力拼装
继承 + 实现多接口 class D extends Base implements I, J {} 最常用
接口继承接口 interface C extends A, B {} 聚合接口
抽象类实现接口 abstract class E implements I {} 把接口的一部分实现好,留给子类

三、抽象类详解

3.1 定义与限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
abstract class BasePayment
{
// ✅ 抽象类可以有普通属性
protected string $name;
protected array $config = [];

// ✅ 抽象类可以有构造方法
public function __construct(array $config = [])
{
$this->config = $config;
}

// ✅ 抽象类可以有具体方法(这是和接口最大的区别)
public function log(string $msg): void
{
error_log("[{$this->getName()}] $msg");
}

protected function getName(): string
{
return static::class;
}

// ✅ 抽象方法:只有签名
abstract public function pay(float $amount): bool;

// ❌ 抽象类里不能有抽象属性的传统写法(PHP 8.4 前)
// 8.4+ 支持 abstract 属性,需声明 get/set hook
}
限制 原因
不能实例化 抽象方法没有实现,实例化了也没法用
不能 final abstract 互斥
子类必须实现全部抽象方法 否则该子类也必须声明为 abstract
抽象方法可以是 protected 子类内部调用,不暴露给外部

3.2 抽象属性的「半实现」(PHP 8.4+)

这是 8.4 新能力:抽象属性可以提供 hook 的实现,但必须至少有一个 hook 是未实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
abstract class Person
{
// 只声明 set 要求,get 由抽象类自己实现 —— 这是合法的
abstract public string $email { set; }

public string $displayEmail {
get => $this->email;
}
}

class Employee extends Person
{
public string $email { set { /* 校验逻辑 */ $this->email = $value; } }
}

3.3 抽象类的真实用途

用途 说明
共享实现 把几个类共有的方法收进父类,避免复制
模板方法模式 抽象方法定义「做什么」,具体方法定义「怎么做」
受控扩展点 protected abstract 只让子类实现,防止外部乱改
代码复用 + 有限的灵活性 和接口的取舍点就在这

四、接口详解

4.1 定义与限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface PaymentGateway
{
// ✅ 接口可以有常量(8.1+ 可被子接口覆盖)
const CURRENCY = 'CNY';

// ✅ 全是抽象,无方法体
public function pay(float $amount): bool;
public function refund(string $txnId): bool;

// ❌ 不能有方法体 → Fatal error
// public function helper() { return 1; }

// ❌ 不能有属性(PHP ≤ 8.3)
// private string $key;

// ❌ 不能有构造器实现
// public function __construct() {}
}
限制 说明
所有方法隐式 public 写 protected / private 直接报错
不能有方法体 任何 { ... } 都报错
不能有构造函数实现 只能声明签名(不推荐)
常量隐式 public 可被使用,但 8.1 前不可覆盖
与类/trait/enum 共享命名空间 同名会冲突

4.2 接口继承与常量覆盖(8.1+)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface A {
public function foo(): void;
const VERSION = '1.0';
}

// ✅ 接口可以继承接口
interface B extends A {
public function bar(): void;
const VERSION = '2.0'; // PHP 8.1+ 允许覆盖;8.1 前会 Fatal
}

// ✅ 类实现时必须全部实现
class Impl implements B {
public function foo(): void {}
public function bar(): void {}
}
echo Impl::VERSION; // 2.0

4.3 「接口没有实现」的真正含义

误解 事实
接口的方法没实现 = 方法不存在 ❌ 接口里有方法签名,可以做类型检查、instanceof 判断
接口不能有代码 ✅ 对,但常量有值、8.4+ 属性有 hook 契约
接口方法不能有默认实现 ✅ 对。PHP 没有接口默认方法(Java 8 有,PHP 没有)

五、协变与逆变(PHP 7.4+ / 8.0)

实现接口方法时,签名可以比接口更宽松(返回类型协变)或更严格(参数类型逆变)。

概念 位置 规则 例子
协变 covariance 返回类型 子类返回更具体的类型 接口声明 object,实现返回 PDO ✅
逆变 contravariance 参数类型 子类接受更宽松的类型 接口声明 string,实现接受 string|int ✅
不变 invariant 其他 必须完全一致 参数名、默认值数量等
1
2
3
4
5
6
7
8
9
10
11
interface Box { public function get(): object; }

class IntBox implements Box {
public function get(): int { return 1; } // ✅ 协变:int 更具体
}

interface Sink { public function handle(string $s): void; }

class LenientSink implements Sink {
public function handle(string|int $s): void {} // ✅ 逆变:接受更宽
}

⚠️ PHP 7.2 起支持协变/逆变。PHP 8.0 起参数名也纳入检查(因为支持了命名参数),实现时参数名应与接口保持一致。

六、接口 vs 抽象类:怎么选

6.1 决策流程

1
2
3
4
5
需要多继承 / 需要「能力声明」?
├─ 是 → 用接口
└─ 否 → 需要共享代码实现吗?
├─ 是 → 用抽象类
└─ 否 → 普通类

6.2 选型对照

场景 用 理由
需要被多个不相关类「标记」能力 接口 标记型接口,如 Logger、JsonSerializable
需要替换实现(支付/缓存/存储) 接口 可插拔,可通过容器注入不同实现
一堆类有相同逻辑要复用 抽象类 接口写不出实现
需要模板方法模式 抽象类 具体方法 + 抽象方法组合
需要 protected 扩展点 抽象类 接口只能 public
需要共享属性 抽象类 接口(≤8.3)无属性
需要构造器做统一初始化 抽象类 接口不该管构造
只为了「看起来规范」而加 都不要 过度设计,纯增加工作量

6.3 常见组合用法

模式 写法 用途
接口 + 抽象基类 abstract class BaseRepo implements RepoInterface 接口定契约,抽象类给通用实现
接口继承接口 interface Readable extends RepoInterface 细分能力
trait 补实现 abstract class Base implements I { use LogTrait; } 横向复用,比继承更灵活

实战结构示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 1. 接口:定契约
interface PaymentGateway {
public function pay(float $amount): bool;
public function refund(string $txnId): bool;
}

// 2. 抽象类:共享校验逻辑
abstract class AbstractGateway implements PaymentGateway
{
protected array $config;

public function __construct(array $config) {
$this->config = $config;
}

public function pay(float $amount): bool
{
if ($amount <= 0) {
throw new InvalidArgumentException('金额必须大于 0');
}
return $this->doPay($amount); // 交给子类
}

abstract protected function doPay(float $amount): bool; // 实现者只需关注核心
abstract public function refund(string $txnId): bool;
}

// 3. 具体实现
class WechatPay extends AbstractGateway {
protected function doPay(float $amount): bool { /* ... */ return true; }
public function refund(string $txnId): bool { /* ... */ return true; }
}

七、面向接口编程的优缺点

7.1 优点

优点 说明
结构清晰 类与实现解耦,阅读代码只需看接口
规范设计 合理设计的接口是团队的契约文档,减少沟通成本
并行开发 接口定死后,多方同时开发不同实现,互不阻塞
可插拔 换实现不改调用方代码
降低耦合 调用方依赖抽象,不依赖具体类
多重实现 弥补单继承的限制(一个类只能有一个父类)
易测试 单元测试可注入假实现,不必连真实数据库/支付

7.2 缺点

缺点 说明
设计复杂度上升 差的接口设计会波及所有实现方,牵一发而动全身
并非所有场景都需要 系统简单时接口只是纯增工作量
代码可复用性可能下降 接口不能复用实现,只能继承抽象类
执行效率略降 多一层动态派发,PHP 7+ 已优化到可忽略,但理论上有开销
过度设计的温床 「先给每个类配个接口」是最常见的反模式

7.3 关于「降低执行效率」的澄清

层面 实际情况
理论 接口方法调用比直接调用多一层查表
PHP 7+ 实测 差异在纳秒级,绝大多数场景不可测
真正影响性能的是 数据库查询次数、N+1 查询、循环内 I/O、大对象拷贝
建议 不要为了性能放弃接口。先优化 I/O,再回来谈这个

7.4 什么时候不该用接口

反模式 问题
只有一种实现且不打算换 过度设计
接口只有一个实现类 YAGNI,等第二种出现再加
为了「规范」给所有类配接口 淹没真正的抽象
接口方法超过 10 个 它已经不是接口,是「上帝类」
接口里塞业务逻辑描述 接口只说「要什么」,不说「怎么做」

💡 判断标准: 问自己「我将来会换掉这个实现吗?」会 → 用接口;不会 → 抽象类或普通类。

八、常见错误速查

错误写法 报错 正确
abstract private function f() 语法错误 用 protected abstract
interface I { protected function f(); } 语法错误 接口只能 public
interface I { public function f() { } } 语法错误 接口不能有方法体
abstract class A implements I {} 但没实现全部方法 Fatal 补全方法,或把该类也声明为 abstract
class C extends A, B {} 语法错误 类只能 extends 一个,接口用 implements A, B
new A() 其中 A 是抽象类 Fatal 不能实例化
interface I extends SomeClass {} 语法错误 接口只能 extends 接口
实现方法少写接口的一个方法 Fatal 全部实现
实现方法签名比接口更严(参数更窄) Fatal 参数要逆变或一致
实现类构造函数签名与接口不同 可能行为异常 接口别声明 __construct

九、PHP 版本特性时间线

版本 相关特性
PHP 5.0 接口(interface)、抽象类(abstract class)引入
PHP 5.4 trait 引入,补充「横向复用」手段
PHP 7.0 协变返回类型;Throwable 体系
PHP 7.2 完整的对象类型协变/逆变支持
PHP 8.0 参数名纳入签名检查;联合类型;object 类型提示
PHP 8.1 接口常量可被覆盖;never 返回类型;只读属性
PHP 8.2 readonly 类
PHP 8.4 接口可声明属性、抽象类可声明抽象属性、property hooks、非对称可见性 public private(set)

十、30 秒自测

问题 答案
接口是抽象类的子类吗? ❌ 不是,二者是并列的两种抽象类型
一个类能继承几个抽象类? 一个(extends)
一个类能实现几个接口? 多个(implements,逗号分隔)
接口能 extends 什么? 只能 extends 其他接口,且可以多个
抽象方法能是 private 吗? ❌ 不能,子类看不到就无从实现
接口方法能是 protected 吗? ❌ 不能,接口方法隐式 public
接口能有方法体吗? ❌ 不能
接口能有构造函数吗? 语法上能声明,但强烈不建议(降低灵活性 + 不受继承规则约束)
接口能有属性吗? PHP ≤ 8.3 ❌;8.4+ ✅(须声明 { get; } / { set; })
接口常量能被子类覆盖吗? 8.1+ 可以,8.1 前不能
什么时候用抽象类? 需要共享实现 / protected 扩展点 / 共享属性时
什么时候用接口? 需要多继承 / 可插拔替换 / 能力标记时
PHP 的协变/逆变分别管什么? 协变管返回类型(可更具体),逆变管参数类型(可更宽松)

抽象类和接口

零、三个概念的定位

概念 一句话 解决什么问题
封装 把数据和对数据的操作绑在一起,隐藏内部实现 保护——外部不该改的别让人改
继承 子类拿父类的属性和方法,并可扩展 复用——同一套逻辑不必写两遍
多态 同一个接口/父类,不同对象不同表现 解耦——调用方不关心具体实现

一、封装(Encapsulation)

1.1 定义

项 说明
本质 把属性和操作属性的方法组织成一个整体(类),并通过访问控制符限制外部访问
目标 ① 保护内部数据不被随意篡改 ② 隐藏实现细节,调用方只依赖公开接口 ③ 允许内部自由修改实现而不影响外部

1.2 访问控制符

修饰符 本类 同类实例 子类 外部
public ✅ ✅ ✅ ✅
protected ✅ ✅ ✅ ❌
private ✅ ✅ ❌ ❌
无(默认) = public = public = public = public

💡 同类实例之间可以访问彼此的 private——因为它们是同一个类。判断依据是类,不是对象。

1.3 为什么用 getter/setter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class BankAccount
{
private float $balance = 0; // 私有,不让外部直接改

public function getBalance(): float
{
return $this->balance;
}

public function setBalance(float $v): void
{
if ($v < 0) {
throw new InvalidArgumentException('余额不能为负');
}
$this->balance = $v;
}
}
如果不用封装 后果
public float $balance 外部可写 $account->balance = -99999,直接破坏业务规则
public 但只有 getter 调用方无法初始化,只能靠构造器

封装的三层含义:

层 做法
数据封装 private / protected 属性
行为封装 只暴露必要的 public 方法
校验前置 setter 里做合法性检查,非法数据进不来

1.4 封装的边界:不是万能钥匙

过度封装 问题
每个属性都写 get/set,内部什么都不做 纯样板代码,没实际价值
属性设 private 但 __get 里原样返回 假封装,外部照样能改
暴露内部可变数组引用 调用方改了内部状态

💡 现代实践(PHP 8.4+):属性 hooks 和非对称可见性可以替代大量样板 getter/setter。

1
2
3
4
5
6
7
8
9
// PHP 8.4+
class User
{
// 公开读,内部写 —— 不用写一整套 get/set
public private(set) string $name = '';

// 带校验的 setter,一行搞定
public int $age { set { if ($value < 0) throw new InvalidArgumentException('年龄不能为负'); $this->age = $value; } }
}

二、继承(Inheritance)

2.1 定义与语法

项 说明
本质 从已有类(父类)派生出新类(子类),子类继承父类的属性和方法
关键字 extends
单继承 一个类只能有一个父类(PHP 的限制)
子类可以做 加新属性、加新方法、重写父类方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Animal
{
protected string $name;
protected int $age;

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

public function describe(): string
{
return "{$this->name},{$this->age}岁";
}
}

class Dog extends Animal
{
public function __construct(string $name, int $age, private string $breed)
{
parent::__construct($name, $age); // 必须显式调用父类构造
}

public function bark(): void
{
echo "汪!";
}

// 重写父类方法
public function describe(): string
{
return parent::describe() . ",品种:{$this->breed}";
}
}

2.2 继承了什么

成员 是否继承 说明
public 属性与方法 ✅ 完全继承
protected 属性与方法 ✅ 子类可访问
private 属性与方法 ❌ 子类看不到
final 类/方法 ❌ 不允许继承/重写
构造函数 __construct 不会自动继承 子类定义了就必须调 parent::__construct()
析构函数 __destruct 不会自动继承 同上,需显式 parent::__destruct()
静态属性 ✅ 但共享同一份值(除非 8.3+ trait 场景)

2.3 构造链与析构链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A {
public function __construct() { echo "A 构造\n"; }
public function __destruct() { echo "A 析构\n"; }
}
class B extends A {
public function __construct() {
echo "B 构造前\n";
parent::__construct(); // 不调用 → A 的构造不执行
echo "B 构造后\n";
}
public function __destruct() {
echo "B 析构\n";
parent::__destruct(); // 不调用 → A 的析构不执行
}
}

$b = new B();
// 输出:B构造前 → A构造 → B构造后 → 脚本结束 → B析构 → A析构
规则 说明
构造顺序 父类先构造(parent::__construct() 通常写在子类构造函数第一行)
析构顺序 父类后析构
不调 parent:: 父类的构造/析构被完全跳过,不是「隐式调用后再覆盖」
父类无构造函数 调 parent::__construct() 会 Fatal error(除非自己声明了空构造)

⚠️ 经典坑:class Child extends Parent 但 Parent 没有 __construct,子类里写了 parent::__construct() → 致命错误。这是 PHP 里最常见的继承坑之一。

安全写法:

1
2
3
4
5
6
7
8
9
class B extends A
{
public function __construct()
{
if (method_exists(parent::class, '__construct')) {
parent::__construct();
}
}
}

2.4 重写(Override)的规则

规则 说明
可见性不能缩小 父类 public → 子类不能改 protected/private(会报错)
可见性可以放大 父类 protected → 子类可以改 public ✅
参数必须兼容 不能新增必填参数;可减(逆变);返回类型必须协变或一致
不能重写 final Fatal error
构造函数是覆盖不是重载 PHP 没有方法重载,子类构造函数不会自动调用父类的
参数名 PHP 8.0 起参与检查 参数名不一致会警告

2.5 继承的代价(组合优于继承)

问题 说明
脆弱基类 父类改一个方法,所有子类行为可能全变
紧耦合 子类被迫继承不需要的东西
层次僵化 单继承限制了类结构自由度
难以测试 子类为了测一个方法要实现整个父类契约

什么时候用继承: 「is-a」关系且确实需要替换实现(如 MySqlRepository extends AbstractRepository)。

什么时候用组合: 「has-a」关系。把依赖作为构造参数传入,而不是继承它。

1
2
3
4
5
6
7
// ❌ 继承:为了用日志,硬塞进继承链
class UserService extends Logger { }

// ✅ 组合:日志作为依赖注入
class UserService {
public function __construct(private LoggerInterface $logger) {}
}

三、多态(Polymorphism)

3.1 定义

⚠️ 笔记定义偏窄:「一个父类被多个子类继承,父类的某个方法在多个子类中表现出不同的功能」——这只是多态的一种表现(重写多态)。多态在 PHP 里主要有三种实现路径,且接口多态才是现代 PHP 的主流。

实现方式 机制 是否需要继承 现代价值
重写(override) 子类重写父类方法 ✅ 需要 中(模板方法模式)
接口多态 不同类实现同一接口 ❌ 不需要 高(主流)
参数多态 方法参数声明为类型(接口/父类),运行时传入不同对象 — 最高(现代 PHP 主流)

3.2 参数多态(重点:现代 PHP 怎么用多态)

1
2
3
4
5
6
7
8
9
10
// 关键:参数类型只声明接口,运行时随便传哪个实现都行
function process(PaymentGateway $gateway, int $amount): bool
{
return $gateway->pay($amount); // 这里调的是接口契约
}

process(new WechatPay(), 100); // 调微信的实现
process(new Alipay(), 100); // 调支付宝的实现
process(new UnionPay(), 100); // 调银联的实现
// 上面三行代码一模一样,一行没改

💡 这才是多态的价值所在:调用方代码不随实现变化而变化。笔记里的「父类方法在子类表现不同」只是最朴素的形式,真正的收益是「一处调用,多种实现」。

3.3 运行时多态的实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Animal {
public function speak(): string;
}

class Dog implements Animal { public function speak(): string { return '汪'; } }
class Cat implements Animal { public function speak(): string { return '喵'; } }
class Bird implements Animal { public function speak(): string { return '啾'; } }

$animals = [new Dog(), new Cat(), new Bird()];

foreach ($animals as $a) {
echo $a->speak() . "\n";
}
// 汪 / 喵 / 啾
步骤 发生了什么
1 foreach 遍历数组,每个元素实际类型不同
2 调用 $a->speak(),PHP 在运行时查这个对象真实类型
3 找到对应的实现并执行
4 返回各自不同的结果

关键:编译时不需要知道具体类型。 这就是「运行时的多态」,和 Java/C# 的机制一致。

3.4 三种多态的对比

场景 重写多态 接口多态
代码量 少(共用父类属性) 多(每个实现都写一遍)
耦合度 高(子类必须继承父类) 低(只依赖契约)
可替换性 差(要重新继承链) 好(换实现不改调用方)
适用 模板方法、共享状态 支付/存储/日志等可插拔组件

3.5 判断多态是否用对

症状 问题 正确做法
大量 if ($obj instanceof X) 用类型判断代替了多态 参数声明为接口,让调用方决定
多个方法签名都写同一组类型 没抽接口 定义 GatewayInterface
换了实现要改多处调用代码 耦合没解 接口 + 依赖注入

四、多重继承的替代方案

4.1 PHP 不支持真正的多重继承

语言 多重继承
Java / C# / PHP ❌ 单继承(一个类只能有一个父类)
C++ / Python / Ruby ✅ 支持多继承

PHP 的替代手段:

方案 提供什么 是否共享实现
接口(多实现) 方法契约 ❌ 只有声明
Trait(多混入) 方法实现 ✅ 完整实现

💡 组合记忆:接口解决「多继承关系」,trait 解决「多复用实现」。接口给契约,trait 给代码。

4.2 方案对比

维度 接口(多实现) Trait(多混入)
关键字 implements use(类体内)
能有实现吗 ❌ ✅
能有属性吗 PHP ≤8.3 ❌;8.4+ 可声明契约 ✅
能有构造函数吗 声明不推荐 ✅ 可有
能被实例化吗 ❌ ❌
冲突处理 可用协变/逆变兼容 insteadof / as
语义 「我能做某事」 「借用一段行为」

五、Trait 详解

5.1 是什么

项 说明
定义 PHP 5.4 引入的代码复用机制
本质 「有编译器帮忙的复制粘贴」 —— 官方原话
关键词 use(类体内,和命名空间的 use 不是一回事)
能否实例化 ❌ 不能,它不是类
能否继承 ❌ 不能被 extends
核心价值 在没有继承关系的多个类间横向复用代码

5.2 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
trait SayHello
{
public function sayHello(): string
{
return 'Hello';
}
}

trait SayWorld
{
public function sayWorld(): string
{
return 'World';
}
}

class MyHelloWorld
{
use SayHello;
use SayWorld;

public function sayHelloWorld(): string
{
return $this->sayHello() . ' ' . $this->sayWorld();
}
}

echo (new MyHelloWorld)->sayHelloWorld(); // Hello World
语法 说明
use TraitA; 混入一个
use TraitA, TraitB; 混入多个(逗号分隔)
use TraitA, TraitB { ... } 带冲突解决

5.3 Trait 的三个「魔法」特性

① 能访问宿主的私有成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
trait MyTrait
{
protected function accessVar(): string
{
return $this->var; // 直接访问宿主的 private
}
}

class TraitUser
{
use MyTrait;
private string $var = 'var';

public function getVar(): string { return $this->accessVar(); }
}

这是 trait 相比普通组合类的重要优势:它真正”长”在宿主类里。

② __CLASS__ 返回宿主类名

1
2
3
4
5
6
7
8
9
10
trait SayWhere
{
public function whereAmI(): string { return __CLASS__; }
}

class Hello { use SayWhere; }
class World { use SayWhere; }

echo (new Hello)->whereAmI(); // Hello ← 不是 SayWhere
echo (new World)->whereAmI(); // World

③ 可以组合 trait

1
2
3
4
5
6
7
trait A { public function a() { echo 'A'; } }
trait B { public function b() { echo 'B'; } }
trait C { use A, B; } // trait 里也能 use trait

class D { use C; }
(new D)->a(); // A
(new D)->b(); // B

5.4 优先级顺序(最常考,也最常错)

1
当前类自身定义的方法  >  Trait 中的方法  >  父类继承的方法
场景 谁赢
类自己写了同名方法 类自己赢(trait 被静默忽略,不报错)
trait 与父类同名方法 trait 赢
两个 trait 同名方法 ⚠️ Fatal error,必须显式解决
1
2
3
4
5
6
7
class Base { public function hello() { echo 'Base'; } }
trait T { public function hello() { echo 'Trait'; } }

class C extends Base {
use T;
}
echo (new C)->hello(); // Trait —— trait 覆盖父类

⚠️ PHP 8.5 有变更:

版本 trait 与父类都有同名属性/常量时
PHP ≤ 8.4 Fatal error(不兼容的组合)
PHP 8.5+ trait 优先,覆盖父类继承的成员

这更贴近 trait 的「复制粘贴」语义。方法解析规则不变(trait 一直覆盖父类方法),变的是属性和常量。

5.5 冲突解决:insteadof 与 as

两个 trait 有同名方法时,不解决直接 Fatal:

1
2
Fatal error: Trait method log has not been applied, because there are collisions
with other trait methods on App\Services\OrderProcessor

方案一:insteadof 二选一

1
2
3
4
5
6
7
8
9
10
11
12
trait Logger {
public function log(string $msg): void { file_put_contents('app.log', $msg, FILE_APPEND); }
}
trait Auditor {
public function log(string $msg): void { DB::table('audit_logs')->insert(['msg' => $msg]); }
}

class OrderProcessor {
use Logger, Auditor {
Logger::log insteadof Auditor; // 用 Logger 的,排除 Auditor 的
}
}

方案二:insteadof + as 两个都要

1
2
3
4
5
6
7
8
9
10
class OrderProcessor {
use Logger, Auditor {
Logger::log insteadof Auditor; // 决定 log() 用谁的
Auditor::log as saveToAuditLog; // Auditor 的另起个别名保留
}
}

$p = new OrderProcessor();
$p->log('订单创建'); // 写文件
$p->saveToAuditLog('订单创建'); // 写数据库

方案三:as 改可见性

1
2
3
4
5
6
class UserProfile {
use Logger, Auditor {
Logger::log insteadof Auditor;
Auditor::log as private internalAudit; // 改为 private,别处调不到
}
}
操作符 作用 注意
insteadof 冲突时选用哪个,排除另一个 只能排除,不能改名
as 给方法起别名 ⚠️ 不改变原方法名,也不影响其他方法
as + 可见性 as protected / as private 单独用 as 也可以

💡 排查冲突的实用技巧: 用反射把 trait 方法全打出来对比,不用猜。

1
2
3
$r = new ReflectionClass(OrderProcessor::class);
print_r($r->getTraits()); // 用了哪些 trait
print_r($r->getTraitAliases()); // 别名映射

5.6 属性的冲突规则(比方法严格)

场景 结果
两个 trait 声明同名属性 必须完全一致(可见性 + 类型 + readonly + 默认值),否则 Fatal
PHP 8.2+ 允许只声明类型不给默认值,但两边类型仍须一致
类定义了与 trait 同名属性 同上,兼容性检查

5.7 trait 中的抽象方法(强制约束)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
trait Hello
{
public function sayHello(): void
{
echo 'Hello ' . $this->getWorld() . "\n";
}

// 强制使用方必须实现
abstract public function getWorld(): string;
}

class MyHelloWorld
{
use Hello;

private string $world;

public function getWorld(): string { return $this->world; }
}
版本 支持的抽象方法可见性
PHP < 8.0 仅 public / protected
PHP ≥ 8.0 public / protected / private 都支持

8.0 起的签名规则: 具体方法的签名必须符合兼容性规则(此前可以不一致)。

💡 这是 trait 里做接口约束的好办法:trait 提供流程,抽象方法定义宿主必须提供什么。

5.8 trait 中的静态成员

项 说明
静态变量/方法/属性 ✅ trait 可定义
静态属性:每个用该 trait 的类是独立副本 和继承的共享语义完全不同
直接在 trait 上访问静态成员 PHP 8.1+ 起弃用,必须通过类访问

静态属性对比(重要):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 继承:父子类【共享】同一份静态属性
class Base { public static $bar; }
class Foo1 extends Base {}
class Foo2 extends Base {}
Foo1::$bar = 'Hello';
Foo2::$bar = 'World';
echo Foo1::$bar; // World ← 共享,Foo1 的也变了

// trait:每个类【独立】
trait T { public static $bar; }
class Foo1 { use T; }
class Foo2 { use T; }
Foo1::$bar = 'Hello';
Foo2::$bar = 'World';
echo Foo1::$bar; // Hello ← 独立

⚠️ PHP 8.3 起又改了:如果子类自己也用了同一个 trait,它与父类的 trait 静态属性视为不同的。8.3 之前同一继承层次里是共享的。

5.9 trait 版本能力时间线

版本 新增能力
PHP 5.4 trait 本身
PHP 7.4 trait 中可定义抽象属性
PHP 8.0 支持 private 抽象方法;具体方法签名需兼容
PHP 8.1 直接在 trait 上访问静态方法/属性弃用
PHP 8.2 trait 中可定义常量;属性可只声明类型
PHP 8.3 as 操作符可用 final;子类重复用 trait 时静态属性独立
PHP 8.5 trait 与父类同名属性/常量时 trait 优先(此前 Fatal)

trait 定义常量(8.2+):

1
2
3
4
5
6
7
8
trait ConstantsTrait
{
public const FLAG_MUTABLE = 1;
final public const FLAG_FROZEN = 5;
}

class Example { use ConstantsTrait; }
echo Example::FLAG_MUTABLE; // 1

⚠️ 类若要定义与 trait 同名常量,必须完全兼容(可见性、初始值、final 性),否则 Fatal。不能直接访问 trait 自身的常量(必须经过类)。

5.10 final 在 trait 里的特殊行为

写法 效果
trait T { final public function f() {} } ❌ final 无效,类可自由覆盖
class C { use T; final public function f() {} } ✅ 类自己声明,覆盖 trait
class C { use T { T::f as final; } } ✅ PHP 8.3+,子类不能覆盖(但类自己仍可)
1
2
3
4
5
6
7
8
9
10
11
// PHP 8.3+
trait CommonTrait { public function method() { echo 'Hello'; } }

class FinalExample {
use CommonTrait {
CommonTrait::method as final; // 子类不能覆盖
}
}
class Child extends FinalExample {
public function method() {} // ❌ Fatal: Cannot override final method
}

六、Trait vs 继承 vs 接口 vs 组合

6.1 四选一决策表

机制 提供 关系 何时选
继承 extends 属性 + 方法实现 + 契约 is-a,单继承 确实是父子类型关系,需替换实现
接口 implements 仅契约 can-do,可多个 声明能力,让实现可插拔
Trait use 属性 + 方法实现 无关系(混入) 多个不相关类需要同一段行为
组合(构造注入) 对象引用 has-a 默认首选

6.2 判断流程

1
2
3
4
5
这段代码要在几个类里复用?
├─ 1 个 + 有 is-a 关系 → 继承
├─ 只需要声明「能做什么」 → 接口
├─ 多个【无继承关系】的类要同一段实现 → trait
└─ 不确定 → 组合(把依赖传进来)

6.3 对比表

维度 继承 接口 Trait 组合
代码复用 ✅ ❌ ✅ ✅(对象级)
需要继承关系 ✅ 必须 ❌ ❌ ❌
数量限制 1 个父类 N 个 N 个 N 个
能有属性 ✅ ≤8.3 ❌ ✅ ✅
能有实现 ✅ ❌ ✅ ✅
耦合度 高 低 中(混入) 最低
可替换性 差 好 无(混入即固定) 好
冲突可能 重写冲突 需协变兼容 同名方法冲突 无

6.4 trait 的常见误用

误用 问题 正确做法
把 trait 当「继承的替代」到处塞 隐式依赖,类不知道自己有这些方法 用组合,或明确定义接口
一个 trait 塞几十个方法 变成「上帝 trait」,难维护 按职责拆成多个小 trait
trait 之间 insteadof 打架 维护地狱 检查命名,或改用组合
trait 依赖宿主某个不存在的属性 运行时才炸,静态分析不报错 用抽象方法声明契约
把有状态逻辑放 trait 状态归属不清 状态放类里,trait 只放行为

trait 引用宿主属性导致静默 bug:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ❌ 危险:隐式依赖 $this->request,IDE 和静态分析都发现不了
trait HandlesRequest {
public function handle(): string {
return $this->request->getUri(); // 宿主必须有 $request
}
}

// ✅ 安全:显式声明契约
trait HandlesRequest {
abstract public function getRequest(): Request;

public function handle(): string {
return $this->getRequest()->getUri();
}
}

💡 trait 里的 $this 指向宿主对象,所以 trait 必须假定宿主「长什么样」。这个隐式约定是 trait 最大的风险点。

七、综合实战示例

典型 PHP 项目的分层组合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
<?php
declare(strict_types=1);

namespace App\Models;

// 1. 接口:定义契约
interface Payable
{
public function pay(int $amount): bool;
public function refund(string $orderNo): bool;
}

// 2. trait:横向复用行为(不相关的类也要用)
trait Loggable
{
private array $logs = [];

protected function log(string $msg): void
{
$this->logs[] = $msg;
}

public function getLogs(): array { return $this->logs; }
}

// 3. 抽象类:共享实现 + 部分契约
abstract class BasePayment implements Payable, \JsonSerializable
{
use Loggable; // 混入行为

protected array $config;

public function __construct(array $config)
{
$this->config = $config;
}

public function pay(int $amount): bool
{
$this->validate($amount);
$this->log("发起支付 {$amount}");
return $this->doPay($amount);
}

public function refund(string $orderNo): bool
{
$this->log("退款 {$orderNo}");
return $this->doRefund($orderNo);
}

public function jsonSerialize(): array { return $this->config; }

// 抽象方法:交给子类
abstract protected function doPay(int $amount): bool;
abstract protected function doRefund(string $orderNo): bool;

private function validate(int $amount): void
{
if ($amount <= 0) {
throw new \InvalidArgumentException('金额必须大于 0');
}
}
}

// 4. 具体类:只实现差异部分
class WechatPay extends BasePayment
{
protected function doPay(int $amount): bool
{
// 调微信 API
return true;
}

protected function doRefund(string $orderNo): bool
{
return true;
}
}

// 5. 使用:参数多态
function checkout(Payable $gateway, int $amount): bool
{
return $gateway->pay($amount);
}

checkout(new WechatPay(), 100);
checkout(new Alipay(), 100); // 一行代码没改

这个例子里四个概念各司其职:

机制 出现位置 职责
接口 Payable 声明「能付能退」
trait Loggable 给所有支付类加日志,它们之间无继承关系
抽象类 BasePayment 共享校验和流程
多态 checkout(Payable $gateway) 换实现不改调用
封装 private array $logs 外部不能直接改日志数组

八、常见错误速查

场景 报错 原因 解法
子类调 parent::__construct() Fatal: Cannot call constructor 父类没有构造函数 先判断 method_exists(),或给父类加空构造
两个 trait 有同名方法 Fatal: Trait method X has not been applied 冲突未解决 insteadof
两个 trait 有同名属性 Fatal: Trait and Class X define the same property 属性规则更严 保证完全一致,或改名
trait 直接访问静态成员 Deprecated(8.1+) 直接在 trait 上访问 通过使用它的类访问
想 new 一个 trait Fatal trait 不是类 通过宿主类实例化
想 extends 一个 trait Fatal trait 不能被继承 用 use 混入
子类缩小父类方法可见性 Fatal public → protected 非法 只能保持或放大
trait 里用宿主私有属性 运行时报错 宿主没有该属性 改用抽象方法声明契约
两个接口同名方法签名冲突 Fatal 不满足协变/逆变 统一签名或用中间接口

九、30 秒自测

问题 答案
trait 本质是什么? 「有编译器帮忙的复制粘贴」(官方原话)
trait 优先级顺序? 类自身 > trait > 父类
两个 trait 方法冲突怎么办? insteadof 二选一,as 起别名保留另一个
as 会重命名原方法吗? ❌ 不会,只加别名,原方法名还在
trait 里的 $this 指向谁? 宿主对象($this->getName() 能调宿主私有属性)
trait 里 __CLASS__ 返回什么? 使用该 trait 的类名,不是 trait 名
trait 的静态属性和继承一样吗? ❌ 独立:每个用该 trait 的类各有一份
trait 最大的风险是什么? 隐式依赖宿主属性/方法,运行时才炸
怎么消除这个风险? 在 trait 里用 abstract 方法声明契约
trait 能定义常量吗? ✅ PHP 8.2+
trait 冲突时 final 有效吗? trait 里直接写 final 无效;用 as final(8.3+)
接口多态 vs 重写多态,哪个主流? 接口多态(参数声明接口类型),现代 PHP 主流
PHP 支持真正的多重继承吗? ❌ 不支持,用「多接口」+「多 trait」替代
什么时候不该用继承? 「has-a」关系 → 用组合;子类被迫继承不需要的东西

十、参考

来源 链接
Trait(官方) https://www.php.net/manual/zh/language.oop5.traits.php
继承(官方) https://www.php.net/manual/zh/language.oop5.inheritance.php
类常量(官方) https://www.php.net/manual/zh/language.oop5.constants.php
Traits 中定义常量 RFC(8.2) https://wiki.php.net/rfc/constants_in_traits
Trait 优先级 / 8.5 变更 https://www.php.net/manual/zh/language.oop5.traits.php

Lua

一、安装lua

  1. cd /usr/local/src

  2. sudo wget https://www.lua.org/ftp/lua-5.3.5.tar.gz

  3. sudo tar -zxvf lua-5.3.5.tar.gz

  4. cd lua-5.3.5

  5. sudo make macosx test

    • 查看下MakeFile文件内容cat MakeFile
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
     # Makefile for installing Lua
    # See doc/readme.html for installation and customization instructions.

    # == CHANGE THE SETTINGS BELOW TO SUIT YOUR ENVIRONMENT =======================

    # Your platform. See PLATS for possible values.
    PLAT= none

    # Where to install. The installation starts in the src and doc directories,
    # so take care if INSTALL_TOP is not an absolute path. See the local target.
    # You may want to make INSTALL_LMOD and INSTALL_CMOD consistent with
    # LUA_ROOT, LUA_LDIR, and LUA_CDIR in luaconf.h.
    INSTALL_TOP= /usr/local
    INSTALL_BIN= $(INSTALL_TOP)/bin
    INSTALL_INC= $(INSTALL_TOP)/include
    INSTALL_LIB= $(INSTALL_TOP)/lib
    INSTALL_MAN= $(INSTALL_TOP)/man/man1
    INSTALL_LMOD= $(INSTALL_TOP)/share/lua/$V
    INSTALL_CMOD= $(INSTALL_TOP)/lib/lua/$V

    # How to install. If your install program does not support "-p", then
    # you may have to run ranlib on the installed liblua.a.
    INSTALL= install -p
    INSTALL_EXEC= $(INSTALL) -m 0755
    INSTALL_DATA= $(INSTALL) -m 0644
    #
    # If you don't have "install" you can use "cp" instead.
    # INSTALL= cp -p
    # INSTALL_EXEC= $(INSTALL)
    # INSTALL_DATA= $(INSTALL)

    # Other utilities.
    MKDIR= mkdir -p
    RM= rm -f

    # == END OF USER SETTINGS -- NO NEED TO CHANGE ANYTHING BELOW THIS LINE =======

    # Convenience platforms targets.
    PLATS= aix bsd c89 freebsd generic linux macosx mingw posix solaris

    # What to install.
    TO_BIN= lua luac
    TO_INC= lua.h luaconf.h lualib.h lauxlib.h lua.hpp
    TO_LIB= liblua.a
    TO_MAN= lua.1 luac.1

    # Lua version and release.
    V= 5.3
    R= $V.4

    # Targets start here.
    all: $(PLAT)

    $(PLATS) clean:
    cd src && $(MAKE) $@

    test: dummy
    src/lua -v

    install: dummy
    cd src && $(MKDIR) $(INSTALL_BIN) $(INSTALL_INC) $(INSTALL_LIB) $(INSTALL_MAN) $(INSTALL_LMOD) $(INSTALL_CMOD)
    cd src && $(INSTALL_EXEC) $(TO_BIN) $(INSTALL_BIN)
    cd src && $(INSTALL_DATA) $(TO_INC) $(INSTALL_INC)
    cd src && $(INSTALL_DATA) $(TO_LIB) $(INSTALL_LIB)
    cd doc && $(INSTALL_DATA) $(TO_MAN) $(INSTALL_MAN)

    uninstall:
    cd src && cd $(INSTALL_BIN) && $(RM) $(TO_BIN)
    cd src && cd $(INSTALL_INC) && $(RM) $(TO_INC)
    cd src && cd $(INSTALL_LIB) && $(RM) $(TO_LIB)
    cd doc && cd $(INSTALL_MAN) && $(RM) $(TO_MAN)

    local:
    $(MAKE) install INSTALL_TOP=../install

    none:
    @echo "Please do 'make PLATFORM' where PLATFORM is one of these:"
    @echo " $(PLATS)"
    @echo "See doc/readme.html for complete instructions."

    # make may get confused with test/ and install/
    dummy:

    # echo config parameters
    echo:
    @cd src && $(MAKE) -s echo
    @echo "PLAT= $(PLAT)"
    @echo "V= $V"
    @echo "R= $R"
    @echo "TO_BIN= $(TO_BIN)"
    @echo "TO_INC= $(TO_INC)"
    @echo "TO_LIB= $(TO_LIB)"
    @echo "TO_MAN= $(TO_MAN)"
    @echo "INSTALL_TOP= $(INSTALL_TOP)"
    @echo "INSTALL_BIN= $(INSTALL_BIN)"
    @echo "INSTALL_INC= $(INSTALL_INC)"
    @echo "INSTALL_LIB= $(INSTALL_LIB)"
    @echo "INSTALL_MAN= $(INSTALL_MAN)"
    @echo "INSTALL_LMOD= $(INSTALL_LMOD)"
    @echo "INSTALL_CMOD= $(INSTALL_CMOD)"
    @echo "INSTALL_EXEC= $(INSTALL_EXEC)"
    @echo "INSTALL_DATA= $(INSTALL_DATA)"

    # echo pkg-config data
    pc:
    @echo "version=$R"
    @echo "prefix=$(INSTALL_TOP)"
    @echo "libdir=$(INSTALL_LIB)"
    @echo "includedir=$(INSTALL_INC)"

    # list targets that do not create files (but not all makes understand .PHONY)
    .PHONY: all $(PLATS) clean test install local none dummy echo pecho lecho
  6. sudo make install

  7. lua -v

二、安装php的lua扩展

  1. 下载扩展包,传送门
  2. 解压sudo tar -zxvf lua-2.0.6.tgz
  3. 生成configure文件sudo /usr/local/php7.1/bin/phpize
  4. 检测,生成MakeFile,指定安装参数sudo ./configure --with-php-config=/usr/local/php7.1/bin/php-config --with-lua=/usr/local
    • 不加--with-lua=/usr/local选项报错
1
2
3
checking for lua in default path... found in /usr/local
checking for lua library in default path... not found
configure: error: Please reinstall the lua distribution - lua library should be in <lua-dir>/lib/
  1. 解决
    • 方式一:老老实实地加--with-lua=/usr/local
    • 方式二:sudo cp /usr/local/lib/liblua.a /usr/lib/liblua.a,此命令只在低版本的mac中可用,升级到Catalina后执行报错sudo cp /usr/local/lib/liblua.a /usr/lib/liblua.a cp: /usr/lib/liblua.a: Read-only file system,这是因为 Catalina 分离了系统文件和用户文件,系统文件都被 mount 到了只读分区,当然好处是显而易见的,防止系统文件被恶意篡改,处理方法如下:
      • 首先,禁止SIP(重启mac,command+r进入恢复模式,打开终端输入命令csrutil disable)
      • 重启后在terminal输入sudo mount -uw /,意思是把分区mount成可写模式,在系统重启后失效
      • 执行之前没有权限的操作,如复制、移动文件等
      • 重启后再启用SIP(重启mac,command+r进入恢复模式,打开终端输入命令csrutil enable)
      • 重新执行sudo ./configure --with-php-config=/usr/local/php7.1/bin/php-config
  2. 安装sudo make && sudo make install
  3. 加载扩展extension=lua.so(extension_dir之前已设置好)

三、使用

1
2
3
4
5
6
7
8
9
10
11
12
13
$lua = new Lua();
$lua->eval(<<<CODE
function dummy(foo, bar)
print(foo, ",", bar)
end
CODE
);
$lua->call("dummy", array("Hello", "php"));
echo "<br>";
$lua->dummy("Hello", "lua");
echo "<br>";
$res = $lua->call(array("table", "concat"), array(array(1=>11, 2=>22, 3=>33), "-"));
print_r($res);

四、参考

  1. 参考一
  2. 参考二

swoole

一、概念

  1. Swoole是由C语言开发的php扩展类,由于有着C语言的优势,swoole在内存管理、数据结构、通信协议解析等方面优势明显。它是一个面向生产环境的PHP异步网络通信引擎,使PHP开发人员可以编写高性能的异步并发TCP、UDP、Unix Socket、HTTP、WebSocket服务。Swoole可以广泛应用于互联网、移动通信、企业软件、云计算、网络游戏、物联网(IOT)、车联网、智能家居等领域。 使用PHP+Swoole作为网络通信框架,可以使企业IT研发团队的效率大大提升。

二、安装

  1. 下载源码包至/usr/local/src目录下
  2. 解压 sudo tar -zxvf swoole-4.4.3.tgz
  3. 切换目录 cd swoole-4.4.3
  4. 运行sudo /usr/local/php/7.3/bin/phpize命令,生成configure可执行文件
  5. 运行配置sudo ./configure --with-php-config=/usr/local/php/7.3/bin/php-config
  6. 安装扩展sudo make && sudo make install
  7. 修改php.ini文件,增加extension=/path/swoole.so
  8. 重启php-fpm
  9. 查看是否安装成功php7.3 -m,显示swoole则成功

三、参考

  1. 参考一
  2. 参考二
  3. 参考三
  4. 参考四

Yaf-Yar-Yac

一、概念

  1. Yaf,全称Yet Another Framework,一个C语言编写的以PHP扩展形式提供高性能的PHP开发框架,它更快,更轻便,提供了Bootstrap,路由,分发,视图,插件等,是一个全功能的PHP框架。
  2. Yar,全称Yet Another RPC Framework,一个C语言编写的PHP扩展的RPC框架,它轻量级,可并行化,支持多种打包协议(msgpack,json,php)。
  3. Yac,全称Yet Another Cache,一个一个基于共享内存的高性能的缓存扩展。

二、安装

      本身就是以扩展的形式存在,和其他扩展安装没什么区别,自己gg一下就出来了。在php命令行下可通过命令php --ri extension_name获得扩展相关信息:

  1. yaf配置信息

    • yaf.environ:环境名称,当用INI作为Yaf的配置文件时,这个指明了Yaf将要在INI配置中读取的节的名字,默认product。
    • yaf.library:全局类库的目录路径,默认NULL
    • yaf.cache_config:是否缓存配置文件(只针对INI配置文件生效),打开此选项可在复杂配置的情况下提高性能,默认0。
    • yaf.name_suffix:在处理Controller/Action/Plugin/Model的时候,类名中关键信息是否是后缀式,比如UserModel,而在前缀模式下则是ModelUser,默认1。
    • yaf.name_separator:在处理Controller,Action,Plugin,Model的时候,前缀和名字之间的分隔符,默认为空,也就是UserPlugin,加入设置为”_”,则判断的依据就会变成:”User_Plugin”,这个主要是为了兼容ST已有的命名规范,默认””。
    • yaf.forward_limit:forward最大嵌套深度,默认5。
    • yaf.use_namespace:开启的情况下,Yaf将会使用命名空间方式注册自己的类,比如Yaf_Application将会变成Yaf\Application,默认0。
    • yaf.use_spl_autoload:开启的情况下,Yaf在加载不成功的情况下,会继续让PHP的自动加载函数加载,从性能考虑,除非特殊情况,否则保持这个选项关闭,默认0。
  2. yar配置信息

    • yar.packager string:设置Yar的打包工具,可以是PHP(serialize),JSON,Msgpack(这个需要编译的时候指定–enable-msgpack).
    • yar.debug string:打开的时候,Yar会把请求过程的日志都打印出来(到stderr).
    • yar.connect_timeout integer:连接超时(毫秒为单位)
    • yar.timeout integer:处理超时(毫秒为单位)
    • yar.expose_info bool:如果关闭,则当通过浏览器访问Server的时候,不会出现Server Info信息.
  3. yac配置信息

    • yac.enable = 1
    • yac.keys_memory_size = 4M ; 4M can get 30K key slots,32M can get 100K key slots
    • yac.values_memory_size = 64M
    • yac.compress_threshold = -1
    • yac.enable_cli = 0 ; whether enable yac with cli,default 0

四、使用

  1. Yaf
  2. Yar
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//filename operator.php
class Operator
{
public function add($a, $b) {
sleep(20);
return $this->_add($a, $b);
}

public function sub($a, $b) {
sleep(10);
return $a - $b;
}

public function mul($a, $b) {
sleep(5);
return $a * $b;
}

protected function _add($a, $b) {
return $a + $b;
}
}

$server = new Yar_Server(new Operator());
$server->handle();

方式一:传统调用方式:http://localhost/operator.php

//filename client.php

方式二:实例化new yar_client对象然后调用
$client = new yar_client("http://localhost/operator.php");
var_dump($client->add(1, 2));
var_dump($client->call("add", array(3, 2)));
var_dump($client->_add(1, 2));

方式三:回调
ini_set("yar.debug", false);
$client = new yar_client("http://localhost/operator.php");

function callback($ret, $callinfo) {
print_r($ret);
echo PHP_EOL;

print_r($callinfo);
echo PHP_EOL;
}

Yar_Concurrent_Client::call("http://localhost/operator.php", "add", array(1, 2), "callback");
Yar_Concurrent_Client::call("http://localhost/operator.php", "sub", array(2, 1), "callback");
Yar_Concurrent_Client::call("http://localhost/operator.php", "mul", array(2, 2), "callback");

echo 123;

Yar_Concurrent_Client::loop();

echo 456;
  1. Yac
1
2
3
4
5
6
7
8
$yac  = new Yac();
$info = $yac->info();
$set1 = $yac->set("foo", "bar");
$get1 = $yac->get('foo');

$set2 = $yac->set(["key1" => "val1","key2" => "val2"]);
$get2 = $yac->get("key1");
$get3 = $yac->get(array("key1", "key2"));

三、参考

  1. Yaf
  2. Yar
  3. Yac
  4. Yac
  5. Yaf配置

foreach问题

一、基础

  1. php4中引入了foreach结构,这是一种遍历数组的简单方式。相比传统的for循环,foreach能够更加便捷的获取键/值对。在php5之前,foreach仅能用于数组;php5之后,利用foreach还能遍历对象。
    • 当foreach开始执行时,数组内部的指针会自动指向第一个单元,这意味着不需要在foreach循环之前调用reset()。
    • 由于foreach依赖内部数组指针,在循环中修改其值将可能导致意外的行为(可以很容易地通过在$value之前加上&来修改数组的元素)。
    • 数组最后一个元素的$value引用在foreach循环之后仍会保留,建议使用unset()来将其销毁(unset只会取消引用,不会销毁值)。(问题就出在这里)

二、demo

  1. 例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    $arr = [1, 2, 3];
    foreach($arr as &$val);
    foreach($arr as $val);
    print_r($arr);
    期望结果:
    Array
    (
    [0] => 1
    [1] => 2
    [2] => 3
    )
    实际结果:
    Array
    (
    [0] => 1
    [1] => 2
    [2] => 2
    )
  2. 分析

    其实,我们可以认为foreach($arr as $k => $v)结构隐含了如下两个赋值操作,分别将数组当前的’键’和当前的’值’赋给变量$k和$v,具体展开形如:

1
2
3
4
5
6
foreach($arr as $k => $v){
$v = currentVal();
$k = currentKey();
//TODO code
……
}

根据上述理论,现在我们重新来分析下第一个foreach:

  • 第1遍循环,$v是一个引用,即$v = &$arr[0],此时$arr变成[1,2,3]

  • 第2遍循环,同理可得:$v = &$arr[1],$arr变成[1,2,3]

  • 第3遍循环,同理可得:$v = &$arr[2],$arr变成[1,2,3]

  • 此时并未执行unset($v)操作,$v现在是数组最后一个元素的引用*

    随后代码进入了第二个foreach:

  • 第1遍循环,隐含操作$v=$arr[0]被触发,$v仍然是$arr[2]的引用,相当于$arr[2]=$arr[0],$arr变成[1,2,1]

  • 第2遍循环,$v=$arr[1],同理($v仍然是$arr[2]的引用)可得:$arr[2]=$arr[1],$arr变成[1,2,2]

  • 第3遍循环,$v=$arr[2],同理($v仍然是$arr[2]的引用)可得:$arr[2]=$arr[2],$arr变成[1,2,2]

    三、参考

  1. 参考一
  2. 参考二

Guzzle

一、定义

  1. Guzzle是一个PHP的HTTP客户端,用来轻而易举地发送请求,并集成到我们的WEB服务上。
    • 接口简单:构建查询语句、POST请求、分流上传下载大文件、使用HTTP cookies、上传JSON数据等等。
    • 发送同步或异步的请求均使用相同的接口。
    • 使用PSR-7接口来请求、响应、分流,允许你使用其他兼容的PSR-7类库与Guzzle共同开发。
    • 抽象了底层的HTTP传输,允许你改变环境以及其他的代码,如:对cURL与PHP的流或socket并非重度依赖,非阻塞事件循环。
    • 中间件系统允许你创建构成客户端行为。

二、使用

  1. 依赖

    • >=PHP 5.5.0
    • php.ini文件设置allow_url_fopen = On
    • cURL >= 7.19.4(可选)
    • openssl和zlib扩展(没有此扩展,使用composer安装guzzle的时候也会报错)
  2. 安装 composer require guzzlehttp/guzzle:~6.0,6.0版本要求php7.1以上

  3. 使用


PHPUnit

一、概念

      PHPUnit是用于PHP编程语言的单元测试框架。它是xUnit体系结构的一个实例,用于单元测试框架,该框架起源于SUnit并在JUnit中流行。PHPUnit由Sebastian Bergmann创建,其开发托管在GitHub上。

作用

  • 通过命令操控测试脚本
  • 测试性能
  • 测试代码覆盖率
  • 自动化的更新测试用例的参数数据
  • 各种格式的日志

使用规则

  • 一般被测试类的后面加上”Test”,比如要测试的类为Array,则测试用例的命名为ArrayTest。
  • 测试类ArrayTest继承于PHPUnit_Framework_TestCase
  • 测试方法必须为public权限,都是test开头,或者你也可以选择给其加注释@test来表明该函数为测试函数
  • 通过断言方法来对实际值和预期值进行断言,断言方法可以参照手册

二、使用

  1. 通过归档文件安装

    • 下载归档文件 wget http://phar.phpunit.cn/phpunit.phar
    • 增加可执行权限 chmod +x phpunit.phar
    • 全局可执行 sudo mv phpunit.phar /usr/local/bin/phpunit
      • 若安装两个php版本,则修改/usr/local/bin/phpunit,修改可执行文件php路径 #!/usr/local/bin/php5.6
    • 测试安装是否成功 phpunit --version
      • 本地源码安装了两个版本的php,5.6和7.3,执行完以上步骤不顶球用,一执行就报错
    1
    2
    3
    4
    5
    PHP Fatal error:  Uncaught PharException: phar "/usr/local/bin/phpunit" has a broken signature in /usr/local/bin/phpunit:27
    Stack trace:
    #0 /usr/local/bin/phpunit(27): Phar::mapPhar('phpunit-6.5.3.p...')
    #1 {main}
    thrown in /usr/local/bin/phpunit on line 27
  2. 通过composer安装

    • 全局安装 composer global require phpunit/phpunit
    • 创建项目 mkdir test
    • 切换目录 cd test
    • 初始化 composer init
    • 引入文件 composer require --dev phpunit/phpunit
    • 新建测试文件夹 mkdir tests
    • 新建phpunit.xml为了引入autoload.php
1
2
3
4
5
6
7
<phpunit bootstrap="vendor/autoload.php">
<testsuites>
<testsuite name="service">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
  • 新建测试脚本EqualsTest.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

use PHPUnit\Framework\TestCase;

class EqualsTest extends TestCase {
public function testFailure()
{
$this->assertEquals(1, 0);
}

public function testFailure2()
{
$this->assertEquals('bar', 'baz');
}

public function testFailure3()
{
$this->assertEquals("foo\nbar\nbaz\n", "foo\nbah\nbaz\n");
}
}
  • 执行 ../vendor/bin/phpunit EqualsTest.php
    • 通过归档文件安装时phpunit已建好,默认使用

三、参考

  1. 参考一
  2. 参考二
  3. 参考三

Workerman

一、概念

  1. Workerman是纯PHP开发框架,需要依赖很多额外的第三方PHP扩展来实现。它是一款开源高性能异步PHP socket即时通讯框架。支持高并发,超高稳定性,被广泛的用于手机app、移动通讯,微信小程序,手游服务端、网络游戏、PHP聊天室、硬件通讯、智能家居、车联网、物联网等领域的开发。支持TCP长连接,支持Websocket、HTTP等协议,支持自定义协议。拥有异步Mysql、异步Redis、异步Http、MQTT物联网客户端、异步消息队列等众多高性能组件。

二、使用

三、参考

  1. 参考一

断点上传和断点续传

一、定义

二、实现

三、参考

  1. 参考一
  2. 参考二
  3. 参考三

OAuth2.0

一、背景

      在OAuth之前,Web或移动应用要访问某些受限制的资源时,一般需要通过输入用户名/密码的形式进行验证,这种形式会带来一些已知的问题。OAuth的出现就是为了解决访问资源的安全性以及灵活性,使得第三方应用对资源的访问或者微服务间的调用更加安全和便捷。

二、实现

  1. 数据库
  2. 代码实现
  3. nginx配置
  4. 访问

三、参考

  1. 参考一
  2. 参考二

一分钟允许请求十次

一、概论

      出去面试的时候,问的最多的问题就是高并发相关的,比如针对秒杀、抢购系统,如何保证每个用户只能买一次;再比如高频应用接口,如果不加控制的话别人可以轻易的写一个脚本一直刷接口,从而导致整个服务变慢甚至是拖垮。秒杀、抢购系统方案都是成熟的,可以从产品、前端、后端等多个层面去实现,至于高频接口请求的拦截则仁者见仁智者见智了。比如要求某个接口一分钟只允许请求十次,且要考虑到请求时间的均衡性(如前59秒请求了一次,最后1秒请求了9次)等。

      BZ的方案是基于redis的hash实现(也可为别的数据类型),总的思路就是为每个用户设置一个key,value为请求次数,同时设置ttl为一分钟。考虑到请求时间的均衡性,还需要记录下每一次请求的时间。

1
uid : {"num" : 1, "modified_at" : 0}

二、实现

  1. 假设有三个用户A/B/C,用户ID分别为a/b/c
  2. 缓存预热,即分别初始化三个用户(项目中可以通过脚本设置,在此通过命令演示)
1
2
3
4
5
6
7
hmset user_a num 0 modified_at 0
hmset user_b num 0 modified_at 0
hmset user_c num 0 modified_at 0

expire user_a 600
expire user_b 60
expire user_c 60
  1. handle代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
require 'vendor/autoload.php';

$single_server = array(
'host' => '127.0.0.1',
'port' => 6379,
);

$redis = new Predis\Client($single_server);

$key = 'user_a';
$bit = 60;
$exist = $redis->exists($key);
try{
if ($exist) {
$user = $redis->hmget('user_a', ['num', 'modified_at']);
if ($user[0] >= 10) {
echo "too many request";
} else {
$sub = time() - $user[1];
if ($sub < $bit) {
echo "time too short";
} else {
$redis->hincrby('user_a', 'num', 1);
$redis->hset('user_a','modified_at',time());
echo "normal";
}
}
} else {
$user = $redis->hmset('user_a', ['num' => 1, 'modified_at' => 0]);
$expire = $redis->expire('user_a', 600);
echo 'init success';
}
} catch (Exception $e) {
var_dump($e->getMessage());
}

PHP-MongoDB操作

一、环境准备

  1. 安装PHP
  2. 安装MongoDB
  3. 安装MongoDB扩展
  4. 安装composer,中文镜像

二、存储图片

  1. 下载安装驱动库
    • composer安装composer require mongodb/mongodb
      • 博主安装了多个PHP版本:/usr/local/php/7.1/bin/php /usr/local/bin/composer require mongodb/mongodb
  2. set.php,请求后会在以下两个集合中插入记录
    • fs.files
    • fs.chunks
1
2
3
4
5
6
7
8
9
10
<?php

require './vendor/autoload.php';

$ct = new MongoDB\Client("mongodb://127.0.0.1/");
$gc = $ct->test->selectGridFSBucket();
$name = mt_rand();
$file = fopen('./a.jpg','rb');
$gr = $gc->uploadFromStream($name, $file);
print_r($gr);
  1. get.php
1
2
3
4
5
6
7
8
9
10
11
12
<?php

require './vendor/autoload.php';

$ct = new MongoDB\Client();
$gc = $ct->test->selectGridFSBucket();
$obj = '61406e5703c06b5916587baf';//fs.files表_id,可修改为动态获取
$id = new \MongoDB\BSON\ObjectId($obj);
$str = $gc->openDownloadStream($id);
$cs = stream_get_contents($str);
header('Content-Type: image/jpeg; charset=UTF-8');
echo $cs;

三、使用PHP对MongoDB进行CURD操作(原生操作)

$manager = new MongoDB\Driver\Manager('mongodb://localhost:27017');

  1. 增加数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 方式一
$command3 = new MongoDB\Driver\Command([
'insert' => 'friend',
'documents' => [
['First Name' => "房", 'Last Name' => '瑞娜','Age' => 27],
['First Name' => "刘", 'Last Name' => '玉龙','Age' => 29],
]
]);
$cursor = $manager->executeCommand('test', $command3);
print_r($cursor->toArray());

// 方式二
$bulkWrite = new \MongoDB\Driver\BulkWrite(['ordered'=>true]);
$dbCollectionName = 'test.friend';
$data = [
'name' => '杨幂',
'actor' => '杨紫'
];
$bulkWrite->insert($data);
$cursor = $manager->executeBulkWrite($dbCollectionName,$bulkWrite);
print_r($cursor);exit;
  1. 删除数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 方式一
$command0 = new MongoDB\Driver\Command([
'delete' => 'friend',
'deletes' => [
[ 'q' => ['First Name' => '刘'],
'limit' => 0, // 0则删除全部,1则只删除一条
]
]
]);
$cursor = $manager->executeCommand('test', $command0);
print_r($cursor->toArray());

// 方式二
$bulkWrite = new \MongoDB\Driver\BulkWrite(['ordered'=>true]);
$writeConcern = new \MongoDB\Driver\WriteConcern(\MongoDB\Driver\WriteConcern::MAJORITY,1000);
$dbCollectionName = 'test.friend';
$extra = array('limit'=>1);
$where = ['name' => '杨幂'];
$bulkWrite->delete($where,$extra);
$cursor = $manager->executeBulkWrite($dbCollectionName,$bulkWrite,$writeConcern);
print_r($cursor);exit;
  1. 修改数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 方式一
$cmd = [
'update' => 'friend', // collection 表名
'updates' => [
[
'q' => ['Age' => 29], //更新条件array
'u' => ['$set' => ['name' => '修改']], //要更新的内容(这个等同于update()函数的更新的内容)
'upsert' => false, // 没有找到则插入,找到了则更新
'multi' => true // true符合条件的则全部更新,默认false只更新第一条
],
],
'ordered' => false, // 是否依次执行updates语句,true表示执行失败后继续后面的语句,false 表示一旦失败立即返回
];
$command6 = new MongoDB\Driver\Command($cmd);
$cursor = $manager->executeCommand('test', $command6);
print_r($cursor->toArray());

// 方式二
$bulkWrite = new \MongoDB\Driver\BulkWrite(['ordered'=>true]);
$writeConcern = new \MongoDB\Driver\WriteConcern(\MongoDB\Driver\WriteConcern::MAJORITY,1000);
$dbCollectionName = 'test.friend';
$where = ['name' => '杨幂'];
$update = ['name' => '大咪咪', 'actor' => '小雪'];
$extra = ['multi' => false, 'upsert'=>false];
$bulkWrite->update(
$where,
$update,
$extra
);
$cursor = $manager->executeBulkWrite($dbCollectionName,$bulkWrite,$writeConcern);
print_r($cursor);
  1. 查询数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 方式一
$command1 = new MongoDB\Driver\Command([//查出某一个
"find" => "friend",
"filter" => ["name" => "羽博"]
]);
$cursor = $manager->executeCommand('test', $command1);
print_r($cursor->toArray());

// 方式二
$command2 = new MongoDB\Driver\Command([
"distinct" => "friend",
"key" => "Age",
"query" => ["Age" => 27]
]);
$cursor = $manager->executeCommand('test', $command2);
print_r($cursor->toArray());exit;
  1. 统计
1
2
3
4
5
6
7
8
$where = ['name'  => '修改'];
$cmd = array(
'count' => 'friend',
'query' => $where
);
$command = new \MongoDB\Driver\Command($cmd);
$cursor = $manager->executeCommand('test',$command);
print_r($cursor->toArray());
  1. 索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 添加索引
$command5 = new MongoDB\Driver\Command([
'createIndexes' => 'friend',
'indexes' => [
[
'name' => 'email',
'key' => array('E-Mail' => 1),//1升序0降序
'unique' => false,
]
],
]);
$cursor = $manager->executeCommand('test', $command5);
print_r($cursor->toArray());

// 删除索引
$command6 = new MongoDB\Driver\Command([
'dropIndexes' => 'friend',
'index' => 'email',
]);
$cursor = $manager->executeCommand('test', $command6);
print_r($cursor->toArray());

四、使用PHP对MongoDB进行CURD操作(基于Composer包mongodb/mongodb操作)

  1. 安装依赖包 composer require mongodb/mongodb

五、参考

  1. 参考一
  2. 参考二
  3. 参考三

RPC示例

一、概念

      RPC是一个完整的远程调用方案,它包括了接口规范+序列化反序列化规范+通信协议等,HTTP只是一个工作在OSI第七层通信协议,HTTP+Restful规范+序列化与反序列化整体构成了一个完整的远程调用方案。

RPC

二、示例

  1. server.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
<?php
class RpcServer
{
private $params = [
'host' => '', // ip地址,列出来的目的是为了友好看出来此变量中存储的信息
'port' => '', // 端口
'path' => '' // 服务目录
];

private $config = [
'real_path' => '',
'max_size' => 2048 // 最大接收数据大小
];

private $server = null;

public function __construct($params) {
$this->check();
$this->init($params);
}

private function check() {
$this->serverPath();
}

private function init($params) {
$this->params = $params;
$this->createServer();
}

private function createServer() {
$this->server = stream_socket_server("tcp://{$this->params['host']}:{$this->params['port']}", $errno, $errstr);
if (!$this->server) exit([
$errno, $errstr
]);
}

/**
* Description: rpc服务目录 通过客户端传值传过来
*/
public function serverPath() {
$path = $this->params['path'];
$realPath = realpath(__DIR__ . $path);
if ($realPath === false || !file_exists($realPath)) {
exit("{$path} error!");
}
$this->config['real_path'] = $realPath;
}

public function run() {
while (true) {
$client = stream_socket_accept($this->server, 100000); //这里的100000设置链接的超时时间
if ($client) {
echo "have a new connection\n";
$buf = fread($client, $this->config['max_size']);
print_r('receive data:' . $buf . "\n");
// 自定义协议目的是拿到类方法和参数(可改成自己定义的)
$infos = $this->parseProtocol($buf);
$this->execMethod($client, $infos["class"], $infos["method"], $infos["params"]);
fclose($client);
echo "close connection\n";
}
}
}

private function execMethod($client, $class, $method, $params) {
if ($class && $method) {
$class = ucfirst($class);
$file = $this->params['path'] . '/' . $class . '.php';
if (file_exists($file)) {
require_once $file;
$obj = new $class();
if (!$params) {
$data = $obj->$method();
} else {
$data = $obj->$method($params);
}
$data = $this->packProtocol($data);
//把运行后的结果返回给客户端
fwrite($client, $data);
}
} else {
fwrite($client, 'class or method error');
}
}

private function parseProtocol($buf) {
$buf = json_decode($buf, true);
return $buf;
}

private function packProtocol($data) {
$data = json_encode($data, JSON_UNESCAPED_UNICODE);
return $data;
}
}

(new RpcServer([
'host' => '127.0.0.1',
'port' => 9999,
'path' => './classes' #指定被调用的程序目录
]))->run();

  1. classes/test.php
1
2
3
4
5
6
7
8
9
10
<?php
class Test {

public function noparams() {
return 'no params';
}
public function myparams($params) {
return $params;
}
}
  1. client.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php
/*
* RPC测试--RPC客户端
*/

class RpcClient {

private $urlInfo = array();

public function __construct($url) {
$this->urlInfo = parse_url($url);
print_r($this->urlInfo);
}

public static function instance($url) {
return new self($url);
}

/*
* 关键的魔术方法
*/
public function __call($name, $arguments) {
// TODO: Implement __call() method.
//创建一个客户端
$client = stream_socket_client("tcp://{$this->urlInfo['host']}:{$this->urlInfo['port']}", $errno, $errstr);
if (!$client) {
exit("{$errno} : {$errstr} \n");
}
$data = [
'class' => basename($this->urlInfo['path']),
'method' => $name,
'params' => $arguments
];
//向服务端发送我们自定义的协议数据
fwrite($client, json_encode($data));
//读取服务端传来的数据
$data = fread($client, 2048);
//关闭客户端
fclose($client);
return $data;
}
}
$cli = new RpcClient('tcp://127.0.0.1:9999/test');
echo $cli->noparams()."\n";
echo $cli->myparams(array("school"=>"hahah", "grade"=>3));
  1. 运行server.php
  2. 运行client.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server端:
have a new connection
receive data:{"class":"test","method":"noparams","params":[]}
close connection
have a new connection
receive data:{"class":"test","method":"myparams","params":[{"school":"hahah","grade":3}]}
close connection

client端:
Array
(
[scheme] => tcp
[host] => 127.0.0.1
[port] => 9999
[path] => /test
)
"no params"
[{"school":"hahah","grade":3}]

三、参考

  1. 参考一
  2. 参考二

开发composer包

一、开干

  1. 注册github账号,配置ssh-key等,此处不再赘述
  2. 登录github,新建一个仓库,如my-first-packagist
  3. 回到本机,克隆刚新建的仓库 git clone https://github.com/liusirdotnet/my-first-packagist.git
  4. 切换到仓库目录 cd my-first-packagist
  5. 初始化composer.json文件 composer init
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
$ composer init

Welcome to the Composer config generator

This command will guide you through creating your composer.json config.

// 1. 输入项目命名空间,默认【用户名/仓库名】
Package name (<vendor>/<name>) [dell/htdocs]: yourname/projectname

// 2. 项目描述
Description []: 这是一个测试

// 3. 输入作者信息,可以直接回车,默认当前git配置的信息
Author [test <123@qq.com>, n to skip]:

// 4. 输入最低稳定版本,stable, RC, beta, alpha, dev
Minimum Stability []: dev

// 5. 输入项目类型,
Package Type (e.g. library, project, metapackage, composer-plugin) []: library

// 6. 输入授权类型
License []: MIT //github建仓库时可指定,不写的话packagist会报错

> Define your dependencies.
// 7. 输入依赖信息
Would you like to define your dependencies (require) interactively [yes]?

// 7.1如果需要依赖,则输入要安装的依赖
Search for a package: php

// 7.2输入版本号,如需多个则重复7.1和7.2
Enter the version constraint to require (or leave blank to use the latest version): >=5.6

// 8. 是否需要require-dev,操作同7
Would you like to define your dev dependencies (require-dev) interactively [yes]?

{
"name": "yourname/projectname",
"description": "这是一个测试",
"type": "library",
"require": {
"php": ">=5.6"
},
"require-dev": {
"php": ">=5.6"
},
"license": "",
"authors": [
{
"name": "test",
"email": "test@qq.com"
}
],
"minimum-stability": "dev"
}

// 9. 是否生成composer.json
Do you confirm generation [yes]? yes
  1. 编辑composer.json文件,添加自动加载
1
2
3
4
5
"autoload": {
"psr-4": {
"namespace_name\\": "src/" //自定义命名空间名称,和类文件命名空间保持一致
}
}
  1. 新建src/Test.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

/**
* This is a test class
*
* @detail null
* @author Name <email@email.com>
* @date 2018-06-17
* @version v1.0
*/

namespace Liusir; // 注意命名空间与composer.json中的一致

class Test
{
/**
* This is a test function
*
* @access public
* @param string $arg1 参数一的说明
* @param int $arg2 参数二的说明
* @param mixed $mixed 这是一个混合类型
* @return array 返回类型
* @author Name <email@email.com>
* @date 2018-06-17
*/
public function testFunc()
{
echo 'This is a test script';
}
}
  1. 生成vendor目录 composer install
  2. 跟src同级目录新建demo.php
1
2
3
4
5
6
7
require './vendor/autoload.php';

use Liusir\Test;

$test = new Test();

$test->testFunc();
  1. 提交并推送到github
1
2
3
git add .
git commit -m 'first commit'
git push
  1. 发布到packagist

    • 注册packagist
    • 点击submit
    • 将github仓库输入到输入框,Check && Submit
  2. 切换到另一个项目目录,安装刚发布的包 composer require liusirdotnet/my-first-packagist,报错

1
2
3
4
  [InvalidArgumentException]                                                                                                                
Could not find a matching version of package liuyulong/second-packagist. Check the package spelling, your version constraint and that th
e package is available in a stability which matches your minimum-stability (stable).
require [--dev] [--prefer-source] [--prefer-dist] [--no-progress] [--no-suggest] [--no-update] [--no-scripts] [--update-no-dev] [--update-with-dependencies] [--update-with-all-dependencies] [--ignore-platform-reqs] [--prefer-stable] [--prefer-lowest] [--sort-packages] [-o|--optimize-autoloader] [-a|--classmap-authoritative] [--apcu-autoloader] [--] [<packages>]...
  1. 打一个tag并推送到github
1
2
3
git tag -a v1.0 -m 'first tag'
git push
git push origin v1.0
  1. 回到github后台并进入当前仓库,点击release选项,点击Draft a new release按钮,勾选下面的This is a pre-release,最后点击Publish release按钮

  2. 切换到另一个项目目录,安装刚发布的包 composer require liusirdotnet/my-first-packagist

  3. 新建index.php

1
2
3
4
5
6
<?php
require './vendor/autoload.php';
use RabbitMQ\RabbitMQ;

$ob = new RabbitMQ;
$ob->test();
  1. 设置更新composer包自动更新packagist【github停止此服务了】
    • 在packagist后台获取 API Token
    • 进入当前仓库,点击settings,点击add service,选择packagist,输入表单后添加add service按钮,最后点击Update service

Note: GitHub Services have been deprecated. Please contact your integrator for more information on how to migrate or replace a service with webhooks or GitHub Apps.

二、参考

  1. 如何建立自己的composer包
  2. 开发一个自己的composer包

PHP函数

一、数组函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
>>>>>>>>>>>array_chunk
1. 功能:将一个数组分割成多个。
2. 用法:`array_chunk(array $array, int $size[, bool $preserve_keys = false ]):array`
3. 说明: 将一个数组分割成多个数组,其中每个数组的单元数目由 size 决定,最后一个数组的单元数目可能会少于 size 个。
4. 参数
* array:需要操作的数组
* size:每个数组的单元数目
* preserve_keys:设为 TRUE,可以使 PHP 保留输入数组中原来的键名。如果你指定了 FALSE,那每个结果数组将用从零开始的新数字索引。
* 默认值是 FALSE。

#索引数组
$input_array = array('a', 'b', 'c', 'd', 'e');
print_r(array_chunk($input_array, 2));#第三个参数默认
print_r(array_chunk($input_array, 2, true));#第三个参数指定为true

#关联数组
$input_array = array('a'=>'a', 'b'=>'b', 'c'=>'c', 'd'=>'d', 'e'=>'e');
print_r(array_chunk($input_array, 2));
print_r(array_chunk($input_array, 2, true));

>>>>>>>>>>>array_column
1. 功能:返回数组中指定的一列
2. 用法:`array_column(array $input, mixed $column_key[, mixed $index_key = null ]):array`
3. 说明:返回input数组中键值为column_key的列,如果指定了可选参数index_key,那么input数组中的这一列的值将作为返回数组中对应值的键。
4. 参数
* input:需要取出数组列的多维数组。 如果提供的是包含一组对象的数组,只有 public 属性会被直接取出。 为了也能取出 private 和 protected 属性,类必须实现 __get() 和 __isset() 魔术方法。
* column_key:需要返回值的列,它可以是索引数组的列索引,或者是关联数组的列的键,也可以是属性名。**也可以是NULL,此时将返回整个数组(配合index_key参数来重置数组键的时候,非常管用)**
* index_key:作为返回数组的索引/键的列,它可以是该列的整数索引,或者字符串键值。

#取某个字段形成新数组
$records = array(
array(
'id' => 2135,
'first_name' => 'John',
'last_name' => 'Doe',
),
array(
'id' => 3245,
'first_name' => 'Sally',
'last_name' => 'Smith',
),
array(
'id' => 5342,
'first_name' => 'Jane',
'last_name' => 'Jones',
),
array(
'id' => 5623,
'first_name' => 'Peter',
'last_name' => 'Doe',
)
);
$first_names = array_column($records, 'first_name');
print_r($first_names);

#指定数组下标
$id_names = array_column($records, 'first_name', 'id');
print_r($id_names);


#结合array_combine一起使用
$arr = [
[
'id' => 123,
'name' => 'test',
'age' => 10,
'created' => '2019-07-03 12:12:12'
],
[
'id' => 456,
'name' => 'test1',
'age' => 10,
'created' => '2019-07-03 12:12:12'
],
[
'id' => 789,
'name' => 'test2',
'age' => 10,
'created' => '2019-07-03 12:12:12'
],
];

$id = array_column($arr,'id');
$new = array_combine($id, $arr);
print_r($new);

#神来之笔,同demo3

$arr = [
[
'id' => 123,
'name' => 'test',
'age' => 10,
'created' => '2019-07-03 12:12:12'
],
[
'id' => 456,
'name' => 'test1',
'age' => 10,
'created' => '2019-07-03 12:12:12'
],
[
'id' => 789,
'name' => 'test2',
'age' => 10,
'created' => '2019-07-03 12:12:12'
],
];
$tmp = array_column($arr, null, 'id');
print_r($tmp);

>>>>>>>>>>>array_combine
$a = array(
'onekey1' => 'onevalue1',
'onekey2' => 'onevalue2',
'onekey3' => 'onevalue3'
);
$b = array(
'twokey1' => 'twovalue1',
'twokey2' => 'twovalue2',
'twokey3' => 'twovalue3'
);
$c = array_combine($a, $b);
print_r($c);

$season = [
['id' => 'aaa', 'name' => '春季', 'sequence' => 1],
['id' => 'bbb', 'name' => '暑假', 'sequence' => 2],
['id' => 'ccc', 'name' => '秋季', 'sequence' => 3],
['id' => 'ddd', 'name' => '寒假', 'sequence' => 4],
];
print_r($season);
#取$season的id组成新数组$season_id

$season_id = array_column($season, 'id');
print_r($season_id);

$new_season = array_combine($season_id, $season);
print_r($new_season);


>>>>>>>>>>>array_flip(array $array):array
1. 将数组的键值互换并返回反转后的数组
* array中的值需要能够作为合法的键名(例如需要是`integer`或者 `string`)。
* 如果类型不对,将出现一个警告,并且有问题的键/值对将不会出现在结果里。
* 如果同一个值出现多次,则最后一个键名将作为它的值,其它键会被丢弃。

#值为null
$arr = [
'one' => 'a',
'two' => null
];
$res = array_flip($arr);
print_r($res);
输出:Warning: array_flip(): Can only flip STRING and INTEGER values! in xxx

#值为string
$arr = [
'one' => 'a',
'two' => 'null'
];
$res = array_flip($arr);

>>>>>>>>>>>array_map
1. 语法:`array_map(callable $callback, array $array1[, array $... ]):array`
* callback:回调函数,应用到每个数组里的每个元素。
* array1:数组,遍历运行 callback 函数。
2. 作用:为数组的每个元素应用回调函数

#金钱以千分位分隔符隔开,保留小数点后两位
$num = [1, 3, 4.5, 5.4, 8.11111];
$one = array_map('number_format', $num, [2, 2, 2, 2, 2]);
$two = array_map(function ($value) {
return number_format($value, 2);
}, $num);
function test($data,$len = 4) {
return number_format($data, $len);
}
$three = array_map('test', $num);
$len = 2;
$four = array_map(function($value) use ($len) {
return number_format($value, $len);
}, $num);

foreach($num as $k => $v) {
$five[] = number_format($v, 2);
}

#数据类型转换
$data = ['a' => 1, 'b' => 2];
var_dump($data);
array_map(function ($v) {return (string)$v;}, $data);
var_dump($data);
foreach ($data as $k => $v) {
$data[$k] = (string)$v;
}
var_dump($data);

>>>>>>>>>>>array_merge
1. 将一个或多个数组的单元合并起来,一个数组中的值附加在前一个数组的后面,返回作为结果的数组。
* 如果输入的数组中有相同的字符串键名,则该键名后面的值将覆盖前一个值
* 然而如果数组包含数字键名,后面的值将不会覆盖原来的值,而是附加到后面。
* 如果只给了一个数组并且该数组是数字索引的,则键名会以连续方式重新索引。

#合并两个数组,后面数组中的值附加在前面数组的后面
##混合数组
$a = array(1, 'a' => '2', 4);
$b = array(1, 'a' => '3', 5);
print_r(array_merge($a, $b));
输出:
Array
(
[0] => 1
[a] => 3
[1] => 4
[2] => 1
[3] => 5
)
print_r($a + $b);
输出:
Array
(
[0] => 1
[a] => 2
[1] => 4
)

##索引数组
$arr1 = [1,2,3];
$arr2 = [1,4,5];
print_r(array_merge);
输出:
Array
(
[0] => 1
[1] => 2
[2] => 3
[3] => 1
[4] => 4
[5] => 5
)
print_r($arr1 + $arr2);
输出:
Array
(
[0] => 1
[1] => 2
[2] => 3
)

#重新索引索引数组的下标,关联数组无变化
## 情形一、索引数组重新索引
$a = array(
1 => 3,
'a' => array( 'c' => 3)
);
print_r(array_merge($a));
输出:
Array
(
[0] => 3
[a] => Array
(
[c] => 3
)
)
## 情形二、关联数组无变化
$a = array(
'b' => 3,
'a' => array('c' => 3)
);
print_r(array_merge($a));
输出:
Array
(
[b] => 3
[a] => Array
(
[c] => 3
)
)

## 情形三、索引数组键名相同
$a = array( 1 => 3, 1 => 'aa');
print_r(array_merge($a));
输出:
Array
(
[0] => aa
)

## 情形四、关联数组键名相同
$a = array( 'a' => 3, 'a' => 'aa');
print_r(array_merge($a));
输出:
Array
(
[a] => aa
)

>>>>>>>>>>>array_reduce
1. `array_reduce(array $array, callable $callback[, mixed $initial = NULL ]):mixed`,将回调函数callback迭代地作用到array数组中的每一个单元中,从而将数组简化为单一的值。

#树状结构合并为一维数组
$str = '{"department":{"05f1a4c91ef77c001424f0c587079ff7":["be18576d271e945a42caf90abf9e9917","00f721589af54c0bad38461527b7baa3"],"65fd7303b005f401b2420b54c981bddf":["e24c06ee5eed4124925521304999cabe","88f54bd22293bff0472c8d58823334fe"],"b0c11f49d96671c6cce192db6a71c14a":["c9cc49492421fdab3fc09eab5e2cc458"],"e70f902e33a71bc21281204405fefcdb":["abf1a042b6648ab481a40581619f65a3","d88675f4bdad473db63cd068108059ce","f76128fad970474a8efba2d98d890d72","ad2fa5178a72cead089e50e3b794748e","83f4e6e48882b32531ed8ff0e19cc329","c44ffdd7bce30684c39cf20d6e8be9ca","08951a0cedb9a54a67bed7cfdee597bb","f5932a8aeb99c0582006372cfd64c5f1","20fa40645f5cb64bfe53f5ebdda5ff65","8ac3ac731e0a5bc5ac19bc0af476a9cf","a66fa40b6a7af410877537bf65d27213","2a64ae8188e57ec1c156bb9641fb06e6","fbafc8faf75f19cbd612ea380fcf2bfb","cc500b7657d8d2ce2ded43f9e51c8756","92ae9a79300956a928047977c5f80e9b","d3935bdb80aa134e65e71f48b0385fc2","3af3fb418fbd539b05190a4ba3d46e66","04e8912d5c23ddacbea2b25d65b6402a","dc6d9d20889f210d7ca320f16b6f64b1","05cb588367917b56f892b016a53ee5a3","79e0c11706e748329f4fcc83ed861a38","d52177c64c57d69be7f108be9331f647","36c038780b8be0e4ecf58e161b4cd6b3","26112e2bccdd433f068c264f58ea49ee","3ef8afd46dcfebe8201bb4a6a9dddf5e","81a8e88121384206dcce7832e2ab2005","46c9af7f3fc496cbbb30e9c4c2a6cfac","e54c2d42fb430c695e13f9212eb079e6","32c0eb23f9ff1150f61c81e847590a29"],"eaefdb7f8ed5b8d27b3c7c55e9537e38":["b4f8ba4c3d9c4035be30e3aa3e4332a9","6cee088e8f61457f915698cf2e35bf02","caee10803879471b98081d1569a94caf"]},"category":{"355368f0ad814ba3a8366d15bc42dee0":{"23592bd68289488495014b95b46d7624":["0b894a8c700d43028b10104c0f22ddcf","8c333c275b684419a4523ffdad72e7a8"]},"54a5fc4fee654d5ca1276ebfee2ba533":{"6fe77674e4d74eab87726289e8b8069f":["ced560d54e3142b1a473c060693387c6"]},"68e6de61d7c040ac9d8c22674be683fb":{"472bc6b0a1bf4c27afa9d04f06c51677":["113e35415e8744b687b7552fe094d0e9","1ba6c3d9b20d4cf8b8824ed0e3cc4a7d","47b900f5d6f547a6b5b0bfb0664b2dbe","6949537769674595927c77d5aabb5aaa","78b9e482d4f549e2aacbabb1ca8107cb","804e62e92d4f4d30af38ec8079f18dbc"],"56c3633b2728481981ae790a4de8ee60":["3231d4e30db3456695996cde28229a25","50e3cdb16e3c42779bd0b108c54e0d23","5a2fbd08a3554c40a8774e401059beb5","8344350ecf1648c485f8d1547f500ff5","9f8946b1e5e34574a35510c5a000462b","bdbfbc203e104a62a6e0fc2acfda9c9e"],"6caa57943cc6486aabe8b89f3cc0aa8a":["177a9d2a5f9c4dd991371cf28458d79c","89b5f65c0ad3452a85ce2237bbbfe4e5","bd700b119c744b81a51410e7eb6c48b9","bdf3b16826a243d8b5288460591751a5","c7cc9eb68ba441068c7523aa26fc2a8f","e428da54fcb9405bac6a3781a3263c00"],"cd22cad3245a4b7f951b93a3529e9f19":["2583fa83855648e39b73756eb886e056","52edc4b74d084be484a3b4af9b7d9b63","5fd41cd95db24868a7ee0b31f59a4c52","e83b305707704f13804f7a9872789328","e90a51b2f6bd4b39a03a45c30868da26","ff4eadce871848479aeac2414e96393c"],"d8a387406e56470f90314b6a8b1745a1":["276f4a6b652f4fc097eca87deebc47ef","3cf17e73193140cbb26167ff19088668","ad1b9b8a9757469dbe3caa5b3666e7c8","aef36aa21492421dbe78577c3c8490a8","b4d9e5364bcb4b65a72de5b7c5212fcd","df9bd01c87af4941b90164f0505a2839"],"fa79ebafd195480fa5828c19f3246a44":["0e4d949154af47d2a561c2505f1f9022","2a0d5edab20b40e983bfdb5e689c2570","690fbc43680341df8fc0d5d65d26c685","77d7ad9d9c624846acd67fd1895bfb15","f2af9410efcd4b3baf1d19570582b3c2","f4405453e9f143fcb7f2f843b08c457a"]},"8c44d1e8d0584ce9a2297b844ea96c9f":{"049651f9031449bca76d116e422a2cbf":["0987adeaecb64fc18f0e4a1737c6fddf","19a71573a8fd4de9b5f8dbc3fa51cc95","24349ad5df0f4efaaaa84a34a301a598","39772a3d09d84f7c820434c6c2a68eab","3e29d4194d1741d4adb291706d98feb4","6a331b2cdde845f287db80d7819c1e66","7614213bec6b489d842127500853fa73","7b99640b101c4059982c95a0b6c2fbee","c2512cb0e51d47918d9e9d1ad18926f9","e3a8e8f69abd41b488c3a8b2537397d2","ed1cbafc7f664b73ae2bfa5ce46eda24"],"70d0dd1057c847208871ea1e8c3a67cc":["0e8901d058c548f5ad2fc2346919d040","15b9bd54e6fc46b99d75341673f57515","1b297cbb12ec4bedaf57fa30d3a35373","3f6e7ac306e7481cae8a3cf8d4261d56","44742a5415664f1ab11830d607000e63","6eb649d8ecfc4ba3beb8bd57603cbe42","9a63d939bf16403f901cd8a9aca54863","cb2d7caf8c904f1facb328892b976f28","cc0e0f540a0246de82c3912a456c5ad7","edb8b8ec3fcd42edb71ff3faf9907d40","f50f346a79094d37b7d6360d3964f688"],"9ee5a06ce1324f2994b20b19f072b58b":["0a2d1a5cac8e43cd820a0529e89e45a8","5958bf22316440d0b071ee61215a15f3","5a4c6376c8f7463bae19c737ca009768","5bc74976a03547fc91c6a1834fdc0814","676afcfbfe1c40a993b9f5d68b01c2bb","6cc992d8ed084ebe80e679f6d0f1f63f","90b19d2263b64fc5a06c2c24189c3f66","a78bad51d9624f168f92341b47ff25db","ccd36fe7023644c092dd7d1de494bba9","dc8d31100122455c94383b6386ba1849","f60397f21440423e96aa66ab724fcf91"]},"8d0d9bffe18b48e6bdcb7a98a0e03ee1":{"58d5c2ada9124305a676913a8a03bb3f":["047d5b99412247099cf2b6e25f3cbeca","0d576c7bfb054a5c83b1438dfc27a056","1e4ead73539e4bbca160b2153f8045b8","2086d145185e44f0ba5d69c92eedd8a5","2b137611a1d3477cb7fa442e46d875eb","352df502a82d40529923f83803384a77","4b2bc253e71c4237a02425c7ecda5fa9","56c906d5acdc4de6a520056457af15dd","5da3a89090684e29bd2e9e4cd710adf5","96d85168ce2141f2b230d19e094c170c","9b1c459989714dc2b88cdeed9d3f08f1","9fb2f9ed56804a72b90532ce4a11a8ec","add8e47bb2f248cdbe691d673131b7db","b81aa2c66b7d46e1aa1d444a0f1c3ef7","c556721dc58c43a785a216fa299e85fa","de8141d583d6446e94b20e2e1f6dcaf1"],"97df4652ee0e4d1b8bd886d73890fb61":["1c7c8d3ee2bb4334b36c88b33240754d","8397d5a30cf447e7a390a1ac1eb1ff59","85c77eb145e9449db05bd740880af19a","9505bce89b8c429b9eac083725351b49","96b1cb77cfc74200a0fd5c7735764791"],"bd02671df27643bbafc51069cee1f230":["02c05a3c815a4456abfa18ab2c6c8af7","4435226714864fe8894236660cb27594","459fabcd15de4e279cb19dc7ab316545","54054cb30ab149f3a9480514f4d67911","79298623a63a4f728840aa94fbad4f42","a3cc6d610eb44c37956e0fba1c5615cd","a82f335455eb43dcbc7541ea05261c44","d8849854300542628b00758e23a00f3f","e3b448ce588643d6910f453554fafd2a","eab920f6e9f643f4bac5fdd1fc11f6a6","efd3d1ab80ef4b4bbcee12d5d9bad42c"]},"9f219946df4f4336946579b89cfbfe7e":{"c470eae33bdf4d9a95b492322a9cab88":["4c762ca56977422fab22a15ea3be1bdc"],"df1f542e44744023a44cdc5fc871258f":["03538fc829814f39aa1f67a15263ae85","04d8d325576e4cf585bbe67485554762","16e195e1370e42efa52ab7e2afee9770","2236742961b74ec5b33ea7c2398091fe","38de905eae2449cc9d2d12011366acd7","92daf2b6a17b4493af8cb6bfa0e01270","d72a56bf64dd4bbf8b0b6f7c9b437888"]},"aa59ad1f470742fc99c8acdac5b3acf0":{"7ce3b9b92c5b464aaa219c6772deb3c4":[""],"b5641323a977486ebc94ef97a6b01b67":[""]},"ddce280fb06a461680863ca9b2ca7412":{"2c69cb34dce1469b96773d1e2d0f72ed":["0e57fb961455429799a237b8ab7ad36e","5cced7cffa39449899bfcc5c768ee67d","8c1b2ccf81b14e39b9fe7b69391296ba","dbd6945f6d7c4af0a6cd66101a9d2e27","fd1a8aee5b4846b0853a3ddd355a2089"],"35f3fd424ab84283b172c18c04116888":["0fb3bed41fa24c959937daa40c0591a0","676c7d63f70c4797a6fd053f6ca4483d","7eaf89fb8ff147d0baa635d0780c6d7d","8713d76160a74cc187a3384a25f5a1c4","97d1274bddf746d599e9a916643f58c0","9b330bb14bb149ca8522980c7855aad8","9e15a143006c420481356ff380c5d1ae","ad16ee81d0cc49f2b514ed0cc6224d6b","c5c07d858bdb41a7b36ba8d21e14f69e"],"4b50d1f2c7e94f0c99d5d38b79c1fe73":["3cabed9e4fc0451ea9378405fcc102b9","50e3b78b6f9a48f69b151798b2092378","7c57ea1d124a4b95b401b8a6cc17cbd2","8372c95b3c5f4cf0b32f9560d77d65f9","acba29a1aae543549d0b9e7209939ad3","bb5ce1d966ba450fb2459186c05d8623","da0e715ebc934fcaad6d04e2d51063bd"],"7f8e49afa4b844ce83faf56c7ede6cdf":["92070c70e4e84e42867cab6a995f04d9"],"8a631a6a2f9c461292016f13c3712879":["a9d469b916b64c67b3c3e0c484755dd1","d49dd3018b7349549c0ba830f19fad8b"],"c4969db58b914f548264bda4c3c45c9a":["104cdf825a574f5f8263f261f8429fdb","11d661c6c73047108da2e7f050a3e1ba","1abe94a9de3c405b9951fb3f9c4773fe","21ba7a4a9a3b40c8b3528c34598ddf18","40683b4c82114f26ae6a0776163c9678","42e750525e6943b1bd20a9806fa6ea62","47b061e862d04b5288e177774bc37c92","50aa3cbd206945618b1db7150a3c72fe","561d81fac6bc4daf9154617afd6f22a3","5a8053db41464580ab45679957d9d062","858677536b16480093efd92f54171737","861784fde4774def9e2348df12f37711","f6e3d898d9ad44ec8adf1032200b2312"],"f02e85199e1f4861b2722dbdff079407":["060d68731e5441b69c6a564ceb7dbc31","0681be33003a4b3483a6d29581425338","15ea6e126355407b8e79b6bfc484cc80","1a7af252561049db82d7b277098aa340","60896a10f56b4ee5806cf4988b86bfd4","631cda1cf9134b02865241fd4c730c3f","7fc62475f27142799ab1a2048d7cca08","a504c327ee7140e5ae497dfa40517eac","a55ce4f15a7844c5bda4812db67c4c9b","a9aa2194faa64327bc65bda42d25c8c6","e9b41b85b40e4d01bbec5c0dacb9bf0e"]}},"join_merchant":["e5870db2d6324594a6c2c73b80cf5e73"],"pool_merchant":["4f1dc462a4814991a8145139925e2282","374951c1b7f9488b8ac563a54ee41244"]}';
$array = json_decode($str, 1);
$array = $array['department'];
$arr = array_reduce($array, 'array_merge', []);
print_r($arr);

>>>>>>>>>>>array_slice
1. 功能:从数组中取出一段
2. 用法:`array_slice(array $array , int $offset [, int $length = NULL [, bool $preserve_keys = false ]]):array`
3. 说明:返回根据`offset`和`length`参数所指定的`array`数组中的一段序列。
4. 参数
* array:输入的数组。
* offset:
* 如果`offset`非负,则序列将从`array`中的此偏移量开始。
* 如果`offset`为负,则序列将从`array`中距离末端这么远的地方开始。
* length:
* 如果给出了`length`并且为正,则序列中将具有这么多的单元。
* 如果给出了`length`并且为负,则序列将终止在距离数组末端这么远的地方。
* 如果省略,则序列将从`offset`开始一直到`array`的末端。
* preserve_keys:注意`array_slice()`默认会重新排序并重置数组的数字索引。你可以通过将`preserve_keys`设为`TRUE`来改变此行为。
5. 注意事项
* 如果`offset`参数大于`array`尺寸,就会返回空的`array`

$input = array("a", "b", "c", "d", "e");
$output = array_slice($input, 2); // returns "c", "d", and "e"
$output = array_slice($input, -2, 1); // returns "d"
$output = array_slice($input, 0, 3); // returns "a", "b", and "c"
// note the differences in the array keys
print_r(array_slice($input, 2, -1));
print_r(array_slice($input, 2, -1, true));


>>>>>>>>>>>array_splice
1. 功能: 去掉数组中的某一部分并用其它值取代
2. 用法:`array_splice(array &$input, int $offset[, int $length = count($input)[, mixed $replacement = array()]]):array`
3. 说明: 把`input`数组中由`offset`和`length`指定的单元去掉,如果提供了`replacement`参数,则用其中的单元取代。
* 注意`input`中的数字键名不被保留
* `input`传引用方式
4. 参数
* input:输入的数组,传引用。
* offset:
* 如果`offset`为正,则从`input`数组中该值指定的偏移量开始移除。
* 如果`offset`为负,则从`input`末尾倒数该值指定的偏移量开始移除。
* length:
* 如果省略`length`,则移除数组中从`offset`到结尾的所有部分。
* 如果指定了`length`并且为正值,则移除这么多单元。
* 如果指定了`length`并且为负值,则移除从`offset`到数组末尾倒数`length`为止中间所有的单元。
* 如果设置了`length`为零,不会移除单元。
* 小窍门:当给出了`replacement`时要移除从`offset`到数组末尾所有单元时,用`count($input)`作为`length`。
* replacement
* 如果给出了`replacement`数组,则被移除的单元被此数组中的单元替代。
* 如果`offset`和`length`的组合结果是不会移除任何值,则`replacement`数组中的单元将被插入到`offset`指定的位置。
* 注意替换数组中的键名不保留。
* 如果用来替换`replacement`只有一个单元,那么不需要给它加上 array(),除非该单元本身就是一个数组、一个对象或者 NULL。

$input = array("red", "green", "blue", "yellow");
array_splice($input, 2); // $input is now array("red", "green")

$input = array("red", "green", "blue", "yellow");
array_splice($input, 1, -1); // $input is now array("red", "yellow")

$input = array("red", "green", "blue", "yellow");
array_splice($input, 1, count($input), "orange"); // $input is now array("red", "orange")

$input = array("red", "green", "blue", "yellow");
array_splice($input, -1, 1, array("black", "maroon")); // $input is now array("red", "green","blue", "black", "maroon")

$input = array("red", "green", "blue", "yellow");
array_splice($input, 3, 0, "purple"); // $input is now array("red", "green","blue", "purple", "yellow");

$input = array("red", "green", "blue", "yellow");
array_splice($input, 3, 1, "purple"); // $input is now array("red", "green","blue", "purple");

>>>>>>>>>>>数组和json转换
$arr = array('name'=> 'liuyulong','sex'=>'man','weight'=>'80kg');
$json = json_encode($arr);
print_r($json);

#多维数组
$arr = array(
'first'=>array('name'=>'liuyulong','sex'=>'man','weight' =>'80kg'),
'second'=>array('name'=>'liusirdotme','sex'=>'man','weight'=>'80kg'),
);
$json = json_encode($arr);
print_r($json);

#一维数组
$arr = array('liuyulong','man','80kg');
$json = json_encode($arr);
print_r($json);

#多维数组
$arr = array(
array('liuyulong','man','80kg'),
array('liusirdotme','man','80kg'),
);
$json = json_encode($arr);
print_r($json);


#索引+关联
$arr = array(
array('name'=>'liuyulong','sex'=>'man','weight'=>'80kg'),
array('name'=>'liusirdotme','sex'=>'man','weight'=>'80kg'),
);
$json = json_encode($arr);
print_r($json);

#关联+索引
$arr = array(
"first" => array('liuyulong','man','80kg'),
"second" => array('liusirdotme','man','80kg'),
);
$json = json_encode($arr);
print_r($json);


>>>>>>>>>>>call_user_func
1. 语法 `call_user_func(callable $callback[, mixed $parameter[, mixed $... ]]):mixed
2. 说明:把第一个参数作为回调函数调用
* 第一个参数 callback 是被调用的回调函数,其余参数是回调函数的参数

function increment(&$var){
$var++;
}
$a = 0;
$b = &$a;
call_user_func('increment', $b);
echo $a;
输出:0
call_user_func_array('increment', array(&$a));
echo $a;
>传入call_user_func()的参数不能为引用传递。

>>>>>>>>>>>call_user_func_array
1. 语法 `call_user_func_array(callable $callback, array $param_arr):mixed`
2. 说明:调用回调函数,并把一个数组参数作为回调函数的参数
* 参数:第一个参数作为回调函数调用,把参数数组作为回调函数的的参数传入
* 参数数组得是索引数组
* 返回值:正常返回回调函数的结果,出错的话就返回FALSE

#直接调用函数
function func($arg, $arg2, $arg3 = 3) {
echo __FUNCTION__, " 拿到参数 $arg 和 $arg2 和 $arg3 \n";
}
call_user_func_array("func", [1, 2]);

#调用对象的方法
class Test {
function method($arg, $arg2) {
echo __METHOD__, " 拿到参数 $arg 和 $arg2\n";
}
}
//调用类的方法
$test = new Test();
call_user_func_array(array($test, "method"), [3, 4]);

#对比
>call_user_func可以传一个参数,也可传多个,跟回调函数的参数有关,call_user_func_array只有2个参数,必须传2个参数
>call_user_func传的参数是字符串形式,call_user_func_array传的参数是数组形式
>call_user_func不能引用传递传参数,call_user_func_array允许引用传递
>性能上call_user_func比call_user_func_array快

二、字符串函数

  1. addcslashes:以 C 语言风格使用反斜线转义字符串中的字符
  2. addslashes:使用反斜线引用字符串
  3. stripcslashes:反引用一个使用 addcslashes() 转义的字符串
  4. stripslashes:反引用一个引用字符串
  5. bin2hex:函数把包含数据的二进制字符串转换为十六进制值
  6. hex2bin:转换十六进制字符串为二进制字符串
  7. chr:返回指定的字符
  8. chunk_​split:将字符串分割成小块
  9. convert_​cyr_​string:将字符由一种 Cyrillic 字符转换成另一种
  10. convert_​uudecode:解码一个 uuencode 编码的字符串
  11. convert_​uuencode:使用 uuencode 编码一个字符串
  12. count_​chars:返回字符串所用字符的信
  13. crc32:计算一个字符串的 crc32 多项式
  14. crypt:单向字符串散列
  15. echo:输出一个或多个字符串
  16. fprintf:将格式化后的字符串写入到流
  17. get_​html_​translation_​table:返回使用 htmlspecialchars() 和 htmlentities() 后的转换表
  18. hebrev:将逻辑顺序希伯来文(logical-Hebrew)转换为视觉顺序希伯来文(visual-Hebrew)
  19. hebrevc:将逻辑顺序希伯来文(logical-Hebrew)转换为视觉顺序希伯来文(visual-Hebrew),并且转换换行符
  20. html_​entity_​decode:Convert HTML entities to their corresponding characters
  21. htmlentities:将字符转换为 HTML 转义字符
  22. htmlspecialchars_​decode:将特殊的 HTML 实体转换回普通字符
  23. htmlspecialchars:将特殊字符转换为 HTML 实体
  24. explode:使用一个字符串分割另一个字符串
  25. implode:将一个一维数组的值转化为字符串
  26. join:别名 implode()
  27. lcfirst:使一个字符串的第一个字符小写
  28. ucfirst:将字符串的首字母转换为大写
  29. ucwords:将字符串中每个单词的首字母转换为大写
  30. strtolower:将字符串转化为小写
  31. strtoupper:将字符串转化为大写
  32. levenshtein:计算两个字符串之间的编辑距离
  33. localeconv:Get numeric formatting information
  34. trim:去除字符串首尾处的空白字符(或者其他字符)
  35. ltrim:删除字符串开头的空白字符(或其他字符)
  36. rtrim:删除字符串末端的空白字符(或者其他字符)
  37. chop:rtrim() 的别名
  38. md5_​file:计算指定文件的 MD5 散列值
  39. md5:计算字符串的 MD5 散列值
  40. metaphone:Calculate the metaphone key of a string
  41. money_​format:将数字格式化成货币字符串
  42. nl_​langinfo:Query language and locale information
  43. nl2br:在字符串所有新行之前插入 HTML 换行标记
  44. number_​format:以千位分隔符方式格式化一个数字
  45. ord:转换字符串第一个字节为 0-255 之间的值
  46. parse_​str:将字符串解析成多个变量
  47. print:输出字符串
  48. printf:输出格式化字符串
  49. quoted_​printable_​decode:将 quoted-printable 字符串转换为 8-bit 字符串
  50. quoted_​printable_​encode:将 8-bit 字符串转换成 quoted-printable 字符串
  51. quotemeta:转义元字符集
  52. setlocale:设置地区信息
  53. sha1_​file:计算文件的 sha1 散列值
  54. sha1:计算字符串的 sha1 散列值
  55. similar_​text:计算两个字符串的相似度
  56. soundex:Calculate the soundex key of a string
  57. sprintf:Return a formatted string
  58. sscanf:根据指定格式解析输入的字符
  59. str_​getcsv:解析 CSV 字符串为一个数组
  60. str_​replace:子字符串替换
  61. str_​ireplace:str_replace() 的忽略大小写版本
  62. str_​pad:使用另一个字符串填充字符串为指定长度
  63. str_​repeat:重复一个字符串
  64. str_​rot13:对字符串执行 ROT13 转换
  65. str_​shuffle:随机打乱一个字符串
  66. str_​split:将字符串转换为数组
  67. str_​word_​count:返回字符串中单词的使用情况
  68. strcasecmp:二进制安全比较字符串(不区分大小写
  69. strchr:别名 strstr()
  70. strrchr:查找指定字符在字符串中的最后一次出现
  71. strstr:查找字符串的首次出现
  72. stristr:strstr() 函数的忽略大小写版本
  73. strcmp:二进制安全字符串比较
  74. strcoll:基于区域设置的字符串比较
  75. strcspn:获取不匹配遮罩的起始子字符串的长度
  76. strip_​tags:从字符串中去除 HTML 和 PHP 标记
  77. strpos:查找字符串首次出现的位置
  78. stripos:查找字符串首次出现的位置(不区分大小写)
  79. strripos:计算指定字符串在目标字符串中最后一次出现的位置(不区分大小写)
  80. strrpos:计算指定字符串在目标字符串中最后一次出现的位置
  81. strlen:获取字符串长度
  82. strnatcasecmp:使用“自然顺序”算法比较字符串(不区分大小写)
  83. strnatcmp:使用自然排序算法比较字符串
  84. strncasecmp:二进制安全比较字符串开头的若干个字符(不区分大小写)
  85. strncmp:二进制安全比较字符串开头的若干个字符
  86. strpbrk:在字符串中查找一组字符的任何一个字符
  87. strrev:反转字符串
  88. strspn:计算字符串中全部字符都存在于指定字符集合中的第一段子串的长度。
  89. strtok:标记分割字符串
  90. strtr:转换指定字符
  91. substr_​compare:二进制安全比较字符串(从偏移位置比较指定长度)
  92. substr_​count:计算字串出现的次数
  93. substr_​replace:替换字符串的子串
  94. substr:返回字符串的子串
  95. vfprintf:将格式化字符串写入流
  96. vprintf:输出格式化字符串
  97. vsprintf:返回格式化字符串
  98. wordwrap:打断字符串为指定数量的字串

三、ob函数

概念

  1. 当PHP脚本有输出时,输出控制函数可以用这些来控制输出。这在多种不同情况中非常有用,尤其是用来在脚本开始输出数据后,发送http头信息到浏览器。输出控制函数不影响由header()或setcookie()发送的文件头信息,仅影响像echo这样的函数和PHP代码块间的数据。

  2. 常用函数

函数名称 含义
ob_start(); 打开一个输出缓冲区,所有的输出信息不再直接发送到涉猎器,而是保存在输出缓冲区里面
ob_clean(); 删除内部缓冲区的内容,不关闭缓冲区(不输出)
ob_end_clean() 删除内部缓冲区的内容,关闭缓冲区(不输出)
ob_get_clean() 返回内部缓冲区的内容,关闭缓冲区。相当于执行ob_get_contents()和ob_end_clean()
flush(); 将ob_flush释放出来的内容,以及不在PHP缓冲区中的内容,整个输出至涉猎器;刷新内部缓冲区的内容,并输出
ob_flush(); 发送内部缓冲区的内容到涉猎器,删除缓冲区的内容,不关闭缓冲区
ob_end_flush() 发送内部缓冲区的内容到涉猎器,删除缓冲区的内容,关闭缓冲区
ob_get_flush() 返回内部缓冲区的内容,并关闭缓冲区,再释放缓冲区的内容。相当于ob_end_flush()并返回缓冲区内容
ob_get_contents() 返回缓冲区的内容,不输出
  1. 输出控制存在的意义:因为http协议的限制(前几行必须是协议信息,然后一个空行,然后才是用户需要的内容,需要保证header信息在其他内容之前发送,否则浏览器无法解析服务器返回的内容。

  2. demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#ob_start
echo "a";
ob_start();
echo 123;
sleep(5);
echo 456;
#结果:先输出a,然后过了5s,输出123456

#ob_end_flush与ob_flush
echo "a";
ob_start();
echo 123;
sleep(5);
ob_end_flush();
sleep(5);
echo 456;
sleep(5);
echo 789;

echo "a";
ob_start();
echo 123;
sleep(5);
ob_flush();
sleep(5);
echo 456;
sleep(5);
echo 789;
#ob_end_flush结果:先输出字符a,5s后输出123并输出缓冲区内容,清空缓冲区,再过5s后输出456,再过5s后输出789
#ob_flush结果:先输出字符a,5秒之后输出123并输出缓冲区内容,10s之后同时输出456 789

function ob_callback($string){
return $string.$string."\n";
}

ob_start("ob_callback");
echo 123;
sleep(5);
ob_flush();
sleep(5);
echo 456;
ob_flush();
sleep(5);
echo 789;
#结果:每隔5s依次输出123123、456456、789789,说明当调用ob_flush()之后会调用ob_callback()方法。

function ob_callback($string){
return $string.$string."\n";
}

ob_start("ob_callback",4);
echo 12345;
sleep(5);
ob_flush();
echo 456;
sleep(5);
ob_end_flush();
sleep(5);
echo 789;

#结果:立即输出1234512345,5s之后输出换行符(空字符),再过5s之后输出456456,再过5s输出789789。
#分析
* ob_start()的chunk_size=4,因为输出字符串12345长度已经超过了4,所以直接输出了,输出的时候调用了ob_callback()方法
* 过了5s后ob_flush()刷新缓冲区,此时缓冲区中的内容为空,所以执行ob_callback()的时候输出一个换行
* 接着执行echo 456;长度小于4,所以执行sleep(5)之后执行ob_end_flush(),刷新缓冲区并且执行ob_callback(),输出456456
* ob_end_flush()已经关闭了缓冲区,所以接下来过了5s之后输出789789

function outer($string){
return "out".$string;
}
function inner($string){
return "in".$string;
}

ob_start("outer");
echo "111";
ob_start("inner");
echo "222";
* 结果:out111in222

echo 'A';
ob_start();
echo 123;
ob_clean();

echo 'A';
ob_start();
echo 123;
ob_end_clean();

echo 'A';
ob_start();
echo 123;
echo ob_get_clean();
#结果:A、A、A123

四、常见函数对比

  1. 安全函数使用

    真题:怎么在代码类的网站存储类似的脚本<script>window.location.href='http://www.baidu.com'</script>!

  • html_entity_decode
  • htmlentities
  • addslashes
  • stripslashes
  • htmlspecialchars
  • htmlspecialchars_decode
  • strip_tags
  1. isset-is_null-empty
  • 真题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    $a = null;
    $b = 0;
    $c = '';

    var_dump(isset($a)); // bool(false)
    var_dump(isset($b)); // bool(true)
    var_dump(isset($c)); // bool(true)
    var_dump(isset($d)); // bool(false)

    var_dump(is_null($a)); // bool(true)
    var_dump(is_null($b)); // bool(false)
    var_dump(is_null($c)); // bool(false)
    var_dump(is_null($d)); // bool(true)

    var_dump(empty($a)); // bool(true)
    var_dump(empty($b)); // bool(true)
    var_dump(empty($c)); // bool(true)
    var_dump(empty($d)); // bool(true)
  • 对比

isset()函数为false的情况

  • 变量的值为null
  • 未定义的变量
  • unset()变量之后

is_null函数为true的情况(和isset相反)

  • 变量的值为null
  • 未定义的变量
  • unset()变量后

empty()函数为true的情况

  • 空字符串
  • false
  • 空数组array()
  • NULL
  • 0
  • 浮点数0.0
    • 字符串0.0为false
  • unset之后的变量
  • 字符串0
  • 未定义的变量$var
  • 没有任何属性的对象

unset()函数,变量的值变为NULL

1
2
3
4
5
6
$a = '123';
var_dump($a);// string(3) "123"
unset($a);
var_dump($a);// NULL
var_dump($a === NULL);// bool(true)
var_dump(empty($a)); //bool(true)
  • 表格
对比项 isset()方法 empty()方法 is_null方法
空字符串”” true ture false
一个空格” “ true false false
FALSE true true false
TRUE true false false
空数组array() true true false
NULL false true true
字符串”0” true true false
整型0 true true false
浮点型0.0 true true false
未定义的变量$a false true true
  • 综合

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    $a = "";
    $b = " ";
    $c = false;
    $d = true;
    $e = array();
    $f = null;
    $g = 0;
    $h = 0.0;
    $i = "0";

    var_dump(isset($a)); // bool(true)
    var_dump(isset($b)); // bool(true)
    var_dump(isset($c)); // bool(true)
    var_dump(isset($d)); // bool(true)
    var_dump(isset($e)); // bool(true)
    var_dump(isset($f)); // bool(false)
    var_dump(isset($g)); // bool(true)
    var_dump(isset($h)); // bool(true)
    var_dump(isset($i)); // bool(true)
    var_dump(isset($j)); // bool(false)

    var_dump(is_null($a)); // bool(false)
    var_dump(is_null($b)); // bool(false)
    var_dump(is_null($c)); // bool(false)
    var_dump(is_null($d)); // bool(false)
    var_dump(is_null($e)); // bool(false)
    var_dump(is_null($f)); // bool(true)
    var_dump(is_null($g)); // bool(false)
    var_dump(is_null($h)); // bool(false)
    var_dump(is_null($i)); // bool(false)
    var_dump(is_null($j)); // bool(true)

    var_dump(empty($a)); // bool(true)
    var_dump(empty($b)); // bool(false)
    var_dump(empty($c)); // bool(true)
    var_dump(empty($d)); // bool(false)
    var_dump(empty($e)); // bool(true)
    var_dump(empty($f)); // bool(true)
    var_dump(empty($g)); // bool(true)
    var_dump(empty($h)); // bool(true)
    var_dump(empty($i)); // bool(true)
    var_dump(empty($j)); // bool(true)
  • 特例

    1
    2
    3
    4
    var_dump(empty(0));      // bool(true)
    var_dump(empty('0')); // bool(true)
    var_dump(empty(0.00)); // bool(true)
    var_dump(empty('0.00')); // bool(false) 特殊记忆字符串0.00
  1. func_num_args-func_get_arg-func_get_args

PHP默认支持按值传递参数,通过引用传递参数以及默认参数,也支持可变长度参数列表,其中可变长度参数列表就主要是通过以下几个函数实现的。

  • func_num_args

    • 语法 func_num_args(void):int

    • 说明:返回调用函数的传入参数个数

      • 参数:void,即不需要传参
      • 返回值:int型,为传入函数的参数数量
    • demo

      1
      2
      3
      4
      5
      6
      7
      8
      function test()
      {
      $count = func_num_args();
      echo $count;
      }

      test(1, 2); //2
      test(1, 2, 3); //3
  • func_get_arg

    • 语法 func_get_arg(int $arg_num):mixed

    • 说明:返回参数列表的某一项

      • 参数:arg_num,参数的偏移量,函数的参数从0开始计数
      • 返回值:mixed,返回指定的参数,错误则返回FALSE
    • demo

      1
      2
      3
      4
      5
      6
      7
      8
      9
      function test()
      {
      $one = func_get_arg(0);
      $two = func_get_arg(1);

      echo $one,PHP_EOL,$two,PHP_EOL;
      }

      test(1, 2);

当在自定义函数的外面调用的该函数的时候会发出一个警告,或者是当arg_num比实际传入的参数的数目大的时候也会发出一个警告。

  • func_get_args

    • 语法 func_get_args(void):array

    • 说明:返回一个包含函数参数列表的数组

      • 参数:void,即不需要传参
      • 返回值:array,一个数组,其中每个元素都是目前用户自定义函数的参数列表的相应元素的副本。
    • demo

      1
      2
      3
      4
      5
      6
      7
      function test()
      {
      $arg = func_get_args();
      print_r($arg);
      }

      test(1, 2);

在用户自定义函数外调用则会出现错误警告。

  1. get_object_vars和compact

    get_object_vars ———— 返回由对象属性组成的关联数组,compact ———— 建立一个数组,包括变量名和它们的值。

  2. array_map、array_filter、array_walk

    • 相同点:都是利用回调函数对数组中每个元素进行操作

    • 不同点

      • 返回值
        • array_map 返回数组,遍历数组元素调用函数
        • array_walk 返回true/false
        • array_filter 返回数组,作用是过滤数组中的元素,回调函数返回真才能保存到返回数组中
      • 回调函数的参数及顺序
      • 是否改变数组的值
    • demo

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    $num = array(0, 1, 2); 
    $map = array_map(function ($v) {
    return $v == 0 ? 0 : 10;
    }, $num);
    print_r($map);
    输出:
    Array
    (
    [0] => 0
    [1] => 10
    [2] => 10
    )

    A.不传回调函数
    $filter = array(
    'a' => null,
    'b' => false,
    'c' => 0,
    'e' => '',
    'd' => 1,
    );
    $new = array_filter($filter);
    print_r($new);
    输出:
    Array
    (
    [d] => 1
    )
    B.传回调函数
    $num = array(0,1,2);
    $filter = array_filter($num, function($v){
    return $v == 0 ? '' : $v;
    });
    print_r($filter);
    输出:
    Array
    (
    [1] => 1
    [2] => 2
    )

    A.传值
    $num = array(0, 1, 2);
    array_walk($num, function ($v, $k) {
    $v++;
    });
    print_r($num);
    输出:
    Array
    (
    [0] => 0
    [1] => 1
    [2] => 2
    )

    B.传引用
    array_walk($num, function (&$v, $k) {
    $v++;
    });
    print_r($num);
    输出:
    Array
    (
    [0] => 1
    [1] => 2
    [2] => 3
    )

    C.官网示例
    $fruits = array(
    'a' => 'apple',
    'b' => 'banana',
    );
    function changeValue(&$item, $key, $prefix) {
    $item = "$prefix: $item";
    }
    function printItem($item, $key) {
    echo "$key. $item<br />\n";
    }
    echo "改变值之前打印:\n";
    array_walk($fruits, 'printItem');
    array_walk($fruits, 'changeValue', 'fruit');
    echo "\n改变值之后打印:\n";
    array_walk($fruits, 'printItem');
  1. 魔术方法之call和callStatic

    • 定义

      • __call($method, $arg),调用不可访问或不存在的方法时被调用
      • __callStatic($method, $arg),调用不可访问或不存在的静态方法时被调用
    • demo

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    class Person {

    private function getPrivateFunc()
    {
    echo "Private function";
    }

    private static function getPrivateStaticFunc($arg)
    {
    echo "Private static function";
    }

    private static function getProtectedStaticFunc($arg)
    {
    echo "Private static function";
    }

    public function __call($method, $arg)
    {
    return call_user_func_array('print_r', $arg);
    }

    public static function __callStatic($method, $arg)
    {
    return call_user_func('print_r', $arg);
    }
    }
    $p = new Person();

    $p->getMyFunc([1,2,3]);

    $p->getPrivateFunc(1);

    Person::getStaticFunc('a');

    Person::getPrivateStaticFunc('b');

    Person::getProtectedStaticFunc('c');

    输出:
    Array
    (
    [0] => 1
    [1] => 2
    [2] => 3
    )

    1

    Array
    (
    [0] => a
    )

    Array
    (
    [0] => b
    )

    Array
    (
    [0] => c
    )
  2. pack和unpack

五、register_shutdown_function

  1. register_shutdown_function(callable $callback[, mixed $parameter[, mixed $... ]]):void,注册一个callback,它会在脚本执行完成或者exit()后被调用。可以多次调用register_shutdown_function(),这些被注册的回调会按照他们注册时的顺序被依次调用。如果你在注册的方法内部调用 exit(),那么所有处理会被中止,并且其他注册的中止回调也不会再被调用。

  2. Note

    • 在某些web server(如Apache)上,可以在中止函数内对脚本的工作目录进行修改。
    • 如果进程被信号SIGTERM或SIGKILL杀死,那么中止函数将不会被调用。尽管你无法中断SIGKILL,但你可以通过pcntl_signal()来捕获SIGTERM,通过在其中调用exit()来进行一个正常的中止。
  3. 基本用法,和别的注册回调没太大差别

1
2
3
4
5
6
function shutdown()
{
echo 'Script executed with success', PHP_EOL;
}
register_shutdown_function('shutdown');
//call_user_func('shutdown');
  1. 和魔术函数__destruct()对比,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
ini_set("max_execution_time", "10");

class TestOne
{
function __construct()
{
register_shutdown_function(array(&$this, 'shutdown_func'));
}

function shutdown_func() {
$fp = fopen("/Users/playcrab/Desktop/exec.txt", "w+");
fputs($fp, "执行超时,正常输出");
fclose($fp);
}
};

$obj = new TestOne();
while (true) {
echo 1;
}


class TestTwo
{
function __destruct()
{
$fp = fopen("/Users/playcrab/Desktop/destruct.txt", "w+");
fputs($fp, "执行超时,不能输出");
fclose($fp);
}
};

$obj = new TestTwo();
while (true) {
echo 2;
}

由对比可知,使用register_shutdown_function能保证即使脚本异常中止时,清理函数的代码仍然能够执行。

  1. 结合进程扩展控制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
declare(ticks = 1);

//信号处理函数
function sig_handler($signo)
{
switch ($signo) {
case SIGTERM:
exit;
break;
case SIGHUP:
break;
case SIGUSR1:
echo "Caught SIGUSR1...\n";
break;
default:
break;
}

}
echo "Installing signal handler...\n";

//安装信号处理器
pcntl_signal(SIGTERM, "sig_handler");
pcntl_signal(SIGHUP, "sig_handler");

$obj = new T();

// pcntl_signal(SIGUSR1, array($obj, "test"));
pcntl_signal(SIGUSR1, array($obj, '__construct'));

echo "Generating signal SIGTERM to self...\n";

posix_kill(posix_getpid(), SIGUSR1);//向当前进程发送SIGUSR1信号

echo "Done\n";

class T
{
public function __construct()
{
register_shutdown_function(array(&$this, 'handle'));
}

public function test()
{
register_shutdown_function(array(&$this, 'handle'));
}

public function handle()
{
file_put_contents('/Users/playcrab/Desktop/111.txt', 2);
echo '进程挂了但是内容保存了';
}
}
  1. 官网手册

六、debug_backtrace

  1. 用法debug_backtrace([ int$options = DEBUG_BACKTRACE_PROVIDE_OBJECT[, int $limit = 0 ]]):array

    • 返回一个包含众多关联数组的array
  2. 返回值说明

名字 类型 说明
function string 当前的函数名。
line integer 当前的行号。
file string 当前的文件名。
class string 当前 class 的名称。
object object 当前的 object。
type string 当前调用的类型。如果是一个方法,会返回 “->”。如果是一个静态方法,会返回 “::”。 如果是一个函数调用,则返回空。
args array 如果在一个函数里,这会列出函数的参数。 如果是在一个被包含的文件里,会列出包含的文件名。
  1. 测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function func1() {
func2();
}

function func2() {
func3();
}

function func3() {
$debug = debug_backtrace();
print_r($debug);
}
func1();
exit;
  1. 运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Array
(
[0] => Array
(
[file] => your_path
[line] => 8
[function] => func3
[args] => Array
(
)
)
[1] => Array
(
[file] => your_path
[line] => 4
[function] => func2
[args] => Array
(
)
)
[2] => Array
(
[file] => your_path
[line] => 15
[function] => func1
[args] => Array
(
)
)
)
  1. 官网手册

七、str_replace

  1. 功能:子字符串替换

  2. 用法:str_replace(mixed $search, mixed $replace, mixed $subject[, int &$count ]): mixed

  3. 说明:该函数返回一个字符串或者数组。该字符串或数组是将subject中全部的search都被replace替换之后的结果。

    • 如果search和replace为数组,那么str_replace()将对subject做二者的映射替换,它们的值将会被依次处理。
    • 如果replace的值的个数少于search的个数,多余的替换将使用空字符串来进行。
    • 如果search是一个数组而replace是一个字符串,那么search中每个元素的替换将始终使用这个字符串。
  4. 参数

    • search:查找的目标值,也就是 needle。一个数组可以指定多个目标。
    • replace:search 的替换值。一个数组可以被用来指定多重替换。
    • subject:执行替换的数组或者字符串。也就是 haystack。如果 subject 是一个数组,替换操作将遍历整个 subject,返回值也将是一个数组。
    • count:如果被指定,它的值将被设置为替换发生的次数。
  5. demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
## 1.简单替换并计数
$str = 'AaaaaBbCc';
$search = 'a';
$replace = 'A';
$str = str_replace($search, $replace, $str, $count);
echo $str . '--' .$count; //AAAAABbCc--4

## 2.多个替换
$search = array("a", "e", "i", "o", "u", "A", "E", "I", "O", "U");
$res = str_replace($search, "", "Hello World of PHP");
echo $res; //Hll Wrld f PHP

## 3.多个对应替换
$str = "You should eat aa, bb, and cc every day.";
$search = array("aa", "bb", "cc", "dd");
$replace = array("AA", "BB", "CC");
$str = str_replace($search, $replace, $str);
echo $str;// You should eat AA, BB, and CC every day.

八、函数的参数

  1. 通过参数列表可以传递信息到函数,即以逗号作为分隔符的表达式列表。参数是从左向右求值的。PHP支持按值传递参数(默认),通过引用传递参数以及默认参数,也支持可变长度参数列表。
  2. 不声明参数类型
  3. 强制声明参数类型
参数类型 说明 PHP最小版本支持
class/interface name The parameter must be an instanceof the given class or interface name. PHP 5.0.0
self The parameter must be an instanceof the same class as the one the method is defined on. This can only be used on class and instance methods. PHP 5.0.0
array The parameter must be an array. PHP 5.1.0
callable The parameter must be a valid callable. PHP 5.4.0
bool The parameter must be a boolean value. PHP 7.0.0
float The parameter must be a floating point number. PHP 7.0.0
int The parameter must be an integer. PHP 7.0.0
string The parameter must be a string. PHP 7.0.0
  1. 延伸
    • 可变参数...
1
2
3
4
5
6
7
8
9
10
11
12
13
function add(...$numbers) {  
$sum = 0;
foreach ($numbers as $n) {
$sum += $n;
}
return $sum;
}

echo add(1, 2, 3),PHP_EOL;
echo add(1, 2, 3, 4),PHP_EOL;
echo add(1, 2, 3, 4, 5),PHP_EOL;

exit;
  • func_num_args/func_get_arg/func_get_args
1
2
3
4
5
6
7
8
9
10
11
function test() {
$num = func_num_args();
echo "num of test args:" . $num,PHP_EOL;
$args = func_get_args();
for ($i = 0; $i < $num; $i++) {
echo "The $i arg of func_get_args is:" . $args[$i],PHP_EOL;
echo "The $i arg of func_get_arg is:" . func_get_arg($i), PHP_EOL;
}
}

test(1,2,3);
  1. 参考


工作收集

  1. 读取url参数
1
2
3
$str = "R0=1&R1=000004&R2=2";
parse_str($str, $output);
print_r($output);
  1. 苹果内购IAP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$url = 'https://buy.itunes.apple.com/verifyReceipt';
$receipt = "33333"; // 凭据
$data['receipt-data'] = $receipt;
$result = my_curl($url, json_encode($data), 'post');

function my_curl($url, $data=null, $method = null,$header = null, $httpBuild = false){
$ch=curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
if ($method == 'post') {
curl_setopt($ch, CURLOPT_POST, 1);
}
curl_setopt($ch, CURLOPT_HEADER, 0);
//curl_setopt($ch,CURLOPT_PROXYTYPE,CURLPROXY_SOCKS5);//使用了SOCKS5代理

curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); //不验证证书 https访问的时候
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); //不验证证书 https访问的时候
if($data){
if($httpBuild == false){
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);//传递参数
}else{
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));//传递参数
}
}
if (!empty($header)) {
curl_setopt($ch, CURLOPT_HTTPHEADER,$header);
}
$output = curl_exec($ch);
curl_close($ch);
return $output;
}
  1. 保存文件
1
2
3
4
5
6
7
8
$image = [
'logo.jpg' => 'https://picsum.photos/id/237/60/60',
];
foreach ($image as $name => $url) {
$data = file_get_contents($url);
file_put_contents($name, $data);
echo $url . '\r\n';
}
  1. 保存css文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function extractLinksFromCss($cssContent) {
$pattern = '/url\(\s*(["\']?)(.*?)\1\s*\)/i';
preg_match_all($pattern, $cssContent, $matches);
return $matches[2] ?? [];
}

$cssFile = 'main.css'; // 替换为你的CSS文件路径
if (file_exists($cssFile)) {
$cssContent = file_get_contents($cssFile);
$data = [];
$links = extractLinksFromCss($cssContent);
foreach ($links as $index => $link) {
$data[] = $link;
}
foreach ($data as $k => $v) {
$url = $v;
$arr = explode('/', $url);
$name = end($arr);
$data = file_get_contents($url);
file_put_contents('./imgs/'.$name, $data);
echo $url . '\r\n';
}
} else {
echo "CSS文件不存在!";
}
  1. post请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
function file_get_contents_post($url, $post) {
$options = array(
'http' => array(
'method' => 'POST',
'content' => http_build_query($post),
'header'=>"Content-Type: application/x-www-form-urlencoded"
),
);
$result = file_get_contents($url, false, stream_context_create($options));
var_dump($result);
return $result;
}

function curl_post_one($url, $post) {
$options = array(
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => false,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $post,
);
$ch = curl_init($url);
curl_setopt_array($ch, $options);
$result = curl_exec($ch);
var_dump($result);
curl_close($ch);
return $result;
}

function curl_post($url='', $postdata='',$options=array()){
$fields_string = http_build_query($postdata);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);

curl_setopt($ch, CURLOPT_POSTFIELDS, $fields_string);

//curl_setopt($ch, CURLOPT_POSTFIELDS, $postdata);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
if (!empty($options)){
curl_setopt_array($ch, $options);
}
$data = curl_exec($ch);
var_dump($data);
curl_close($ch);
return $data;
}
  1. 读取txt文件
  • 带下标
1
2
3
4
5
6
7
8
9
10
11
12
$file="list.txt";
$data = [];
$fileObj = new SplFileObject($file, 'r');
while (!$fileObj->eof()) {
$data[]=$fileObj->fgets();
}
$fileObj = null;
$lines = file($file);
$data = [];
foreach ($lines as $line) {
$data[]=$line;
}
  • 不带下标
1
2
3
4
5
6
7
8
9
$data = [];
$handle = fopen($file, 'r');
if ($handle) {
while (($line = fgets($handle)) !== false) {
$data[]=$line;
}
fclose($handle);
}
$data=file_get_contents($file);

PHP5 vs PHP7

零、一眼对照

维度 PHP 5 PHP 7
类型声明 无(只有 array / callable 类提示) 标量类型 + 返回类型 + 严格模式
异常 只有 Exception,Error 无法捕获 统一到 Throwable,新增 Error 体系
随机数 mt_rand() / rand() / uniqid() CSPRNG:random_bytes / random_int
类定义 匿名函数 + bindTo 匿名类、Closure::call、分组 use
常量 标量 支持数组
语法糖 isset($a) ? $a : $b ??、<=>、**(5.6)
Session 只能在 session_start() 前改 ini 传数组、新增 session.lazy_write
扩展 mysql / mssql / sybase_ct / ereg 等老扩展 全部移除 → mysqli / PDO / preg_*
性能基线 — Zend Engine 3 + AST,普遍快 2~3 倍,内存更省

一、标量类型与返回类型声明

1.1 两种模式

模式 写法 生效范围
弱类型(默认) declare(strict_types=0) 或不写 值能转换就转换
严格类型 declare(strict_types=1) 类型不符直接 TypeError,不做静默转换

⚠️ 关键机制:strict_types 只影响当前文件内的函数调用和返回,不影响被调用方所在文件的声明。调用方声明决定调用时是否严格。

1.2 支持的类型

类别 类型 说明
标量 int float string bool PHP 7 新增
特殊 array callable iterable PHP 5 已支持前两个,iterable 为 7.1 新增
类类型 Foo \ 前缀 self / parent / static(7.0 起 static 可作返回类型)
可空 ?Foo ?int 等价 Foo|null / int|null,PHP 7.1 新增
联合 int|string PHP 8.0 起,PHP 7 需走注解 int|string
无返回 void PHP 7.1 新增

1.3 弱类型下的自动转换(最容易踩坑的地方)

传入值 形参 int 形参 string 形参 float 形参 bool
"123abc" ❌ TypeError(PHP 7)/ 警告+(int)"123abc"=123(PHP 5) 模式不匹配数组 — true
"123" 123 "123" 123.0 true
1.9 ⚠️ 静默截断为 1(PHP 7.3 起弃用 8.1 报 E_DEPRECATED) "1.9" 1.9 true
true 1 "1" 1.0 true
null ⚠️ 非 nullable 参数在 PHP 7.1 起始终报 TypeError(PHP 5 是警告) 同左 同左 同左

1.4 实操建议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
declare(strict_types=1);   // 放在文件第一行,紧跟 <?php 之后

function calc(int $a, int $b): int
{
return $a + $b;
}

function find(): ?string // 可能返回 null
{
return null;
}

function run(): void // 声明无返回
{
// 不允许 return 值
}

工程实践: 建议只在边界层(控制器、对外 API、接收外部输入的地方)开 strict_types,内部领域代码不开。原因:内部调用链全开严格会让「传个字符串数字给 int 参数」这种无害操作到处报错,改造成本远大于收益。


二、NULL 合并运算符 ??

1
2
3
4
5
6
7
8
// 原来
$site = isset($_GET['site']) ? $_GET['site'] : 'wo';

// PHP 7 简写
$site = $_GET['site'] ?? 'wo';

// 支持链式(不存在则整体走默认值,不会报 undefined index)
$name = $data['user']['name'] ?? '匿名';

2.1 ?? vs ??= vs ?: 对比

写法 含义 额外收益
$a ?? $b $a 是否存在(isset)取 $b 不产生 undefined 警告,无论 $a 是否存在
$a ?: $b $a 是否为真取 $b 0 / '' / '0' / [] 都会被当假 → 误伤
$a ??= $b 7.4 新增,$a 为 null 时赋值 $b 少写一次变量名
$a ?: $b 空数组也会被替换 若 $a 允许是 [],用 ?: 会丢数据

💡 常见错误:想让「为空时给默认值」,结果用 ?:,结果 0 被吃掉了。判「是否为空」用 ??,判「是否为空数组」用 empty()。


三、组合运算符(实际叫「太空船运算符」<=>)

表达式 结果
1 <=> 1 0
1 <=> 2 -1
2 <=> 1 1
语义 等价写法
== $a <=> $b 返回 0
< $a <=> $b 返回 -1
> $a <=> $b 返回 1

用途一:三值比较替代嵌套

1
2
3
4
5
6
7
8
// 旧:sort 回调要写三段
usort($arr, function ($a, $b) {
if ($a['age'] == $b['age']) return 0;
return $a['age'] < $b['age'] ? -1 : 1;
});

// 新:支持 spaceship 的场景下
usort($arr, fn($a, $b) => $a['age'] <=> $b['age']);

用途二:判断状态

1
2
3
4
5
6
7
8
echo match ($a <=> $b) {
-1 => 'a 小于 b',
0 => 'a 等于 b',
1 => 'a 大于 b',
};

// 链式比较(PHP 8 新增,PHP 7 不可用)
// $a <= $b <= $c ← 7 不支持;8 里表示 a<=b && b<=c

⚠️ 注意:<=> 只对数字和字符串做「松散比较」语义,不支持数组比较。数组要判等得用 <、=== 或 array_diff 系列。


四、常量数组

1
2
3
4
5
// PHP 7
define('sites', ['Google', 'Jser', 'Taobao']);

echo sites[0]; // Google
print_r(sites);
项 说明
定义方式 define() 支持数组值(PHP 7+);const 关键字PHP 5.3 已支持数组,不依赖此特性
可否修改 不能。PHP 8.1 前连 define 覆盖重定义都报 fatal,PHP 8.1+ 允许带 true 第四参数覆盖
作用域 define() 定义的是全局常量,与调用位置无关
访问 直接写常量名 sites,不能加 $

对比:

1
2
3
4
5
6
7
8
// define —— 运行时执行,可以放在条件里、可用变量值
define('DB_HOST', $config['host']);
define('SITES', ['a', 'b']);

// const —— 编译期,必须字面量,不能拼接变量
class Config {
const SITES = ['a', 'b']; // 类常量
}

💡 实践:类常量优先用 const(可被继承、可被反射读到、IDE 跳转友好),只有需要动态值才用 define()。


五、匿名类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface Logger {
public function log(string $msg);
}

class Application {
private $logger;
public function getLogger(): Logger { return $this->logger; }
public function setLogger(Logger $logger) { $this->logger = $logger; }
}

$app = new Application;

// new class 创建匿名类,无需单独定义
$app->setLogger(new class implements Logger {
public function log(string $msg) {
print($msg);
}
});

$app->getLogger()->log("我的第一条日志");

5.1 匿名类的能力

特性 说明
可实现多个接口 new class implements A, B {}
可继承 new class extends Base implements Iface {}
可用构造器参数 new class(10) extends Base { public function __construct($x){...} }
可捕获 new $className() 动态类名、容器绑定、测试替身
自动生成类名 类似 class@anonymous%s:%d$%x(含文件名/行号,不要依赖这个字符串)
定义位置决定作用域 在 namespace A 里定义 → 属于 A,可访问 A 的私有成员

5.2 典型用途

场景 说明
单元测试替身 桩对象,不必为每个依赖建真类
一次性实现 避免「为一个方法建一个文件」的样板
简单 DI new class implements Handler { ... } 直接注入
快速试参数 在命令行里造一个临时对象

💡 反例:匿名类写超过 ~30 行就该抽成具名类,可读性和可测试性都会崩。


六、Closure::call()

1
2
3
4
5
6
7
8
9
10
11
12
class A {
private $x = 1;
}

// PHP 5 / 7 通用写法
$getXCB = function() { return $this->x; };
$getX = $getXCB->bindTo(new A, 'A'); // 绑定到实例,作用域设为 A 类
echo $getX(); // 1

// PHP 7+ 简写
$getX = function() { return $this->x; };
echo $getX->call(new A); // 1

6.1 闭包绑定 API 速查

方法 作用
bindTo($newThis, $newScope = 'static') 换绑定对象 + 换作用域
bind($newThis, $newScope = 'static') 静态方法,同上
call($newThis, ...$args) 临时把对象绑上去立即执行,等于「绑定 + 立刻调用」
fromCallable($cb) 从可调用值生成闭包
callStatic($class, ...$args) 7.1+,以指定类为作用域静态调用

⚠️ call() 是一次性的,不会改变原闭包;bindTo() 返回新闭包(原闭包不变)。这个区别在循环里复用闭包时很关键。


七、CSPRNG(密码学安全伪随机数)

📌 笔记里写「伪随机数产生器」,准确说法是 Cryptographically Secure Pseudo-Random Number Generator,即密码学安全的伪随机数生成器。mt_rand / rand 是通用算法,不适合生成密钥、token、密码盐。

7.1 PHP 7 新增

函数 作用 说明
random_bytes(int $length): string 返回密码学安全的随机字符串 长度按字节;失败抛 Exception
random_int(int $min, int $max): int 返回区间 [min, max] 内安全随机整数 失败抛 Exception

7.2 该换掉的老函数

老写法 问题 替换
mt_rand() / rand() 可预测,非密码学安全 random_int()
uniqid() 基于微秒时间 + 熵,可预测 bin2hex(random_bytes(16))
openssl_random_pseudo_bytes() 第二个参数需检查 $crypto_strong random_bytes()
md5(uniqid()) 同上 random_bytes()

7.3 实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// token / API key(32 字节 = 64 位十六进制)
$token = bin2hex(random_bytes(32));

// 密码哈希用的 salt
$salt = random_bytes(16);

// 随机密码
$password = base64_encode(random_bytes(24));

// 洗牌(Fisher–Yates,需 7.3+)
$shuffled = $array;
for ($i = count($shuffled) - 1; $i > 0; $i--) {
$j = random_int(0, $i);
[$shuffled[$i], $shuffled[$j]] = [$shuffled[$j], $shuffled[$i]];
}

💡 注意:random_bytes() 失败时会抛 Exception。在 Web 请求里如果不捕获,异常会向上冒泡变成 500 —— 虽然实际几乎不会失败,但生产环境建议包一层 try/catch 兜底。


八、异常体系(笔记此处表述有误,修正如下)

❌ 笔记原话:「PHP 7 异常用于向下兼容及增强旧的 assert() 函数」——这是把两件事混在一句了。
✅ 实际是:PHP 7 新增了全新的异常模型(Throwable + Error 体系),而 assert() 在 PHP 7 改为默认抛出 AssertionError(旧版只是发警告或返回 false)。

8.1 统一异常接口

1
2
3
4
5
6
7
8
9
10
11
Throwable(接口,PHP 7 新增)
├── Exception —— 可预期、可捕获、可被业务处理
│ ├── LogicException / RuntimeException ...
│ ├── PDOException
│ └── ...
└── Error —— 引擎级错误,通常不该被业务 catch
├── TypeError / ValueError / ArgumentCountError
├── DivisionByZeroError / ArithmeticError
├── ParseError
├── ErrorException(自定义错误转换而来)
└── AssertionError ← assert() 抛的

8.2 关键变化

项 PHP 5 PHP 7
可捕获范围 只能 catch (Exception $e) catch (Throwable $e) 能同时抓 Exception 和 Error
类型错误 只发 warning 抛 TypeError(可被捕获)
调用未定义方法 Fatal error,脚本终止 抛 Error,可被捕获
assert() 发 E_WARNING,或 zend.assertions=-1 时根本不执行 默认抛 AssertionError
finally 支持 支持(7 补上了 finally,5.5 才引入)
嵌套异常 — Throwable::getPrevious() 获取上一个异常(5.3+)

8.3 关键澄清:Error ≠ Exception

Exception Error
含义 程序逻辑中可预期的问题 引擎级、不该发生的问题
例子 数据库连不上、参数校验失败、远程接口超时 类型不匹配、方法不存在、除零、内存耗尽
该不该 catch 应该 多数情况不该(catch 了也跑不了),但顶层兜底 \Throwable 可以
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 顶层兜底:接住所有 Throwable
try {
// ...
} catch (\Throwable $e) {
// 记录日志、返回 500
} finally {
// 释放资源
}

// 业务只处理预期内的错误
try {
$pdo->query($sql);
} catch (\PDOException $e) {
// 只处理数据库异常,其他错误继续抛
}

⚠️ 别写 catch (Exception $e) 然后在里面 return 500 —— 这样 Error(比如 ArgumentCountError)会穿透到全局,页面白屏且丢失上下文。要用 \Throwable。

8.4 统一错误处理(生产必备)

1
2
3
4
set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
// 把 PHP 错误转成 ErrorException,才能被 try/catch 捕获
throw new ErrorException($message, 0, $severity, $file, $line);
});

九、use 语句变更(分组导入)

1
2
3
4
5
6
7
// PHP 5:每个类一条 use
use App\Models\User;
use App\Models\Order;
use App\Models\Item;

// PHP 7:分组导入,{} 内可加 as
use App\Models\{User, Order, Item as ItemAlias};

9.1 支持分组的前置

写法 含义
use some\namespace\{ClassA, ClassB}; 从 some\namespace 下导入 ClassA、ClassB
use some\namespace\{A, B\C}; 嵌套分组(7.1+)
use function some\namespace\myFunc; 导入函数
use const some\namespace\MY_CONST; 导入常量
use function some\ns\{fn1, fn2 as f2}; 函数分组(7.1+)

⚠️ 冲突用 as 解决:use App\Models\{User as AppUser, Admin\User};


十、Session 选项

10.1 session_start() 接收选项数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// PHP 7 之前:必须在 session_start() 前 ini_set()
ini_set('session.use_strict_mode', 1);
ini_set('session.use_cookies', 1);
session_start();

// PHP 7+:直接传数组
session_start([
'cache_limiter' => 'private', // 缓存控制指令,不允许代理缓存会话内容
'read_and_close' => true, // 读完立刻关闭会话,释放锁
'use_strict_mode' => 1, // 拒绝未初始化的会话 ID,防会话固定攻击
'gc_maxlifetime' => 1440,
'cookie_httponly' => 1,
'cookie_samesite' => 'Lax', // 7.3+
]);
选项 作用
cache_limiter private / public / nocache,写入响应头控制缓存
read_and_close 读完立即释放文件锁。会话数据量大时并发提升明显(代价:本次请求内不能再写会话)
use_strict_mode 拒绝未在服务端注册过的会话 ID,防会话固定
use_cookies / use_only_cookies 只用 Cookie 传递 SID
cookie_httponly 防 JS 读取,防 XSS 窃取
cookie_samesite Lax / Strict,防 CSRF

10.2 新增 ini 设置

配置 默认 作用
session.lazy_write 1(开) 只有会话数据发生变化时才写盘。改用 session.lazy_write = 0 则请求结束必写
值 行为 何时用
1(默认) 数据无变化就不写 大多数场景,省 IO
0 请求结束强制写入 依赖其他进程/请求读取会话数据时

💡 典型坑:Node/Go 服务与 PHP 共享同一个会话存储时,必须设 session.lazy_write = 0,否则 PHP 侧没改动就不写,另一端读不到数据。


十一、PHP 7 移除 / 废弃的扩展与功能

11.1 移除的扩展(笔记完整列表)

扩展 移除原因 替代方案
mysql 已废弃多年,无新特性、无安全更新 mysqli(面向对象)或 PDO_MySQL
mssql 依赖已移除的 dblib,与 7.0 不兼容 sqlsrv(微软官方)/ PDO_SQLSRV / pdo_dblib
sybase_ct 依赖已移除的 dblib 同上
ereg / eregi POSIX 正则,早被 PCRE 取代 preg_* 系列

11.2 ereg → preg 迁移对照

ereg preg
ereg($p, $s) preg_match("/$p/", $s)
eregi($p, $s) preg_match("/$p/i", $s)
ereg_replace($p, $r, $s) preg_replace("/$p/", $r, $s)
split($p, $s) preg_split("/$p/", $s)
sql_regcase($s) 直接用小写字符串比较

⚠️ 关键:POSIX 语法和 PCRE 语法不一样。+ / * / {n} 在 POSIX 里是字面量,必须转义 \+ / \* / \{n\}。这是照抄老代码最容易出的错。

11.3 移除的函数与特性

项 状态 说明
each() 7.0 弃用,8.0 移除 改 foreach 或 key() + current()
PHP 4 风格构造器(类名同名的方法) 7.0 移除 只认 __construct()
mcrypt 扩展 7.0 移除 openssl_encrypt
$HTTP_RAW_POST_DATA 7.0 移除 php://input
mysql_*() 函数 7.0 移除 mysqli_*() / PDO
create_function() 7.2 弃用,8.0 移除 匿名函数
each() 之外的 list() 位置写法 7.0 弃用位置写法 [$a, $b] = $arr(7.1+)

十二、笔记未覆盖但很重要的 PHP 7 变更

12.1 数组与字符串语法

项 PHP 5 PHP 7
数组/字符串取下标 $arr{0}(花括号) $arr[0] —— 花括号在 7.0 弃用、7.4 移除
数组解构 — [$a, $b] = [1, 2];(7.1+)
键值解构 — ['a' => $a] = $arr;(7.1+)

⚠️ 老代码里的 $str{0} 在 PHP 7 直接解析错误(不是运行错误),整个文件 fatal。升级前全库搜 { 配合 $变量。

12.2 常量与静态变量

项 变化
常量表达式 支持常量表达式运算:const X = 1 + 2 * 3;
define() 定义常量 不能再被重复定义(8.1 前 fatal)
static 变量 在同一个类继承链中共享同一份(7.0 起,若父类被重定义则分离)

12.3 错误处理级别变更

级别 PHP 5 PHP 7
E_RECOVERABLE_ERROR 致命,脚本终止 部分转成 Error 可被捕获
E_STRICT 独立级别 并入 E_NOTICE / E_WARNING,级别常量仍保留但不再单独触发
类型不匹配 隐式转换 直接 TypeError
null 传给非 nullable 允许 报 TypeError(7.1 起)

12.4 性能(Zend Engine 3)

项 效果
AST(抽象语法树) 取代「编译成逐条 opcode」,整体快 2~3 倍(有基准数据为 2~4 倍,视代码类型)
内存管理 更省内存,垃圾回收改进
zend.assertions 默认 1(生产环境建议 -1 关闭断言)
OPcache 7.0 之后默认必须启用才有这个量级提升
JIT ⚠️ PHP 7 没有 JIT,JIT 是 PHP 8 的特性

12.5 其他语法便利

特性 版本
幂运算符 ** 5.6
**= 幂赋值 5.6
变长参数解包 ...$args 5.6
?? / <=> 7.0
可空类型 ?T / void 返回 7.1
list() 短语法 [$a,$b] / 类常量可见性 7.1
多异常 catch (A | B $e) 7.1
键值解构 ['k'=>$v] = $arr 7.1
对象类型 object 提示 7.2
尾逗号(函数调用/数组) 7.3
箭头函数 fn() 7.4
??= 7.4

十三、迁移踩坑清单

13.1 会直接 Fatal 的(必须改)

检查项 现象 处理
$str{0} 花括号取下标 解析错误,整文件挂 全库搜 $\w+\{ → 改 []
mysql_*() 函数 未定义函数 Fatal 换 mysqli / PDO
ereg_* / split() 未定义函数 Fatal 换 preg_*,注意语法差异
依赖 each() 未定义函数 Fatal 改 foreach
PHP 4 风格构造器 不再被当作构造器,静默失效 改名 __construct
mcrypt_* 未定义函数 Fatal 换 openssl_*
子类签名与父类不兼容 Fatal(PHP 8)/ Warning(PHP 7.0~7.3) 参数类型 / 返回类型必须兼容

13.2 会静默行为变化的(最危险)

检查项 现象 处理
is_numeric("1e5") PHP 5 false → PHP 7 true 检查是否有隐式转换
"abc" * 1 PHP 5 得 0 → PHP 7 抛 TypeError 显式转型 (int)
(int) "1e5" PHP 5 1 → PHP 7 100000 加溢出/精度校验
浮点精度 跨平台差异(5.3+ 起改进了) 别用 float 存金额
sort() 对比返回值 返回值语义变了 检查 sort() 返回值是否被依赖
引用赋值语义 list() 解构行为变化 检查解构代码
assert() 警告 → 抛异常 生产设 zend.assertions=-1
数字字符串 + 算术 隐式转换结果可能变 关键计算前显式类型转换

13.3 建议的升级动作

步骤 动作
1 composer.json 改 "php": "^7.4"(7.4 是 7.x 末版,最稳)
2 跑 composer update 让依赖同步降级兼容
3 全局静态分析:phpstan / psalm 跑 level max,解析错误会全暴露
4 重点搜 ereg、mysql_、{、each(、assert(
5 用 php -l 逐文件做语法检查(Composer 会自动做)
6 开启 OPcache,确认 opcache.validate_timestamps 生产置 0
7 生产设 zend.assertions=-1、display_errors=Off、log_errors=On
8 用 php7cc / php7-upgrade 之类工具做自动化扫描

十四、笔记里的疑点汇总

# 笔记原文 修正
1 「组合运算符」 官方名称是太空船运算符(Spaceship Operator),<=>
2 「CSPRNG(伪随机数产生器)」 全称 Cryptographically Secure Pseudo-Random Number Generator,密码学安全的伪随机数生成器
3 「PHP 7 异常用于向下兼容及增强旧的 assert() 函数」 表述混淆。PHP 7 是新增统一异常模型(Throwable + Error);assert() 的变化是另一件事(改为抛 AssertionError)
4 「引入几个 CSPRNG 函数」 「几个」不准确,PHP 7 核心引入的是 random_bytes() 和 random_int() 两个
5 random_bytes() 描述「加密生存」 原文是「加密生成」/ typo
6 「常量数组用 define 定义」 不完整。const 关键字 PHP 5.3 已支持数组常量,define 支持数组才是 7.0 的新增
7 「Closure::call() 意思是向类绑定个匿名函数」 表述不准。call() 是把闭包临时绑定到对象并立即调用;跟「绑定到类」是 bindTo() 的语义

十五、30 秒自测

问题 答案
strict_types 作用于哪个文件? 调用方文件,只影响调用和返回
?? 和 ?: 的关键差别? ?? 判「是否为空」,0/''/[] 都保留;?: 判真假,会吃掉 0 和 ''
<=> 返回什么? -1 / 0 / 1
匿名类能继承吗? 能,new class extends Base {}
Closure::call() 和 bindTo() 的区别? call() 一次性用完即弃;bindTo() 返回新闭包,不改原对象
catch (Exception) 能抓到 TypeError 吗? 不能,要用 Throwable
assert() 在 PHP 7 抛什么? AssertionError(是 Error 不是 Exception)
PHP 7 有 JIT 吗? 没有,JIT 是 PHP 8 的特性
mysql 扩展替代方案? mysqli 或 PDO_MySQL
生产环境 zend.assertions 设什么? -1(关闭)

参考

来源 链接
PHP 7 迁移指南 https://www.php.net/manual/zh/migration70.incompatible.php
PHP 7 新特性 https://www.php.net/manual/zh/migration70.new-features.php
移除的扩展 https://www.php.net/manual/zh/extensions.removing.php
类型声明 https://www.php.net/manual/zh/language.types.declarations.php
session.lazy_write https://www.php.net/manual/zh/session.configuration.php

魔术常量

零、笔记错误速览

原文 补充说明
__FUNCTION__ 获取函数名称 在函数体内有效;在类方法内返回只有方法名(不带类名),带类名请用 __METHOD__
__CLASS__ 获取类的名称 含命名空间完整前缀,如 App\Models\User
__TRAIT__ 返回 trait 被定义时的名字 含其被声明的作用域(命名空间),区分大小写

一、九个魔术常量

PHP 预定义的常量,值会随所在位置变化。

常量 作用 返回值示例 备注
__LINE__ 获取当前行号 12 PHP 4.0.0
__FILE__ 文件的完整路径 + 文件名 D:/Laragon/www/app/Models/User.php 绝对路径
__DIR__ 文件所在目录 D:/Laragon/www/app/Models 不含末尾斜杠,等价 dirname(__FILE__)
__FUNCTION__ 当前函数名 getUserName 函数体内有效
__CLASS__ 当前类名 App\Models\User 类体内有效,含命名空间
__METHOD__ 当前类的方法名 App\Models\User::getName 格式:类名::方法名
__NAMESPACE__ 当前命名空间 App\Models 根命名空间下为 ""
__TRAIT__ Trait 名字 App\Models\LogsTrait 含声明作用域,区分大小写

二、易混点对比

场景 用哪个 原因
方法里要方法名 __FUNCTION__ 只返回 getName,不带类名
方法里要 Class::method __METHOD__ 带类名前缀
trait 里要类名 不适用 trait 里没有类概念,用 __TRAIT__
拿目录绝对路径 __DIR__ 比 dirname(__FILE__) 更直接

三、魔术常量 vs 预定义常量

项 魔术常量 预定义常量
数量 固定 9 个 大量(PHP_VERSION、E_ALL、PHP_INT_MAX 等)
能否自定义 ❌ 不能 ✅ define() / const
值是否随位置变 ✅ 会变 ❌ 固定
典型用途 调试、日志、路径拼接 版本判断、环境配置、错误级别设置


魔术方法

零、分类速览

类别 方法
生命周期 __construct __destruct
访问拦截 __call __callStatic __get __set __isset __unset
序列化 __sleep __wakeup __serialize __unserialize __set_state
类型转换 __toString __invoke __clone __debugInfo

一、生命周期方法

方法 签名 触发时机 关键说明
__construct() public function __construct(mixed ...$args) 每次 new 实例化前 子类定义了则不隐式调父类,需手动 parent::__construct();子类未定义则继承(private 除外)
__destruct() public function __destruct() 所有引用被删除 / 显式销毁 / 脚本关闭 ① 脚本关闭时调用,此时 HTTP 头已发出;② exit() 也会调用;③ 析构里再 exit() 会中止其余关闭流程;④ 子类定义了需显式 parent::__destruct()

二、访问拦截方法

方法 签名 触发时机 关键说明
__call() public mixed __call(string $name, array $arguments) 对象中调用不可访问/不存在的方法 $name = 方法名,$arguments = 枚举数组
__callStatic() public static mixed __callStatic(string $name, array $arguments) 静态上下文中调用不可访问方法 与 __call 成对使用
__get() public mixed __get(string $name) 读取不可访问属性 / 属性不存在 Eloquent 的属性投喂就是它
__set() public void __set(string $name, mixed $value) 给不可访问属性赋值
__isset() public bool __isset(string $name) 对不可访问属性用 isset() / empty() 只做存在性检查,不读值
__unset() public void __unset(string $name) 对不可访问属性用 unset()

__isset() / __unset() 只做「存在 / 销毁」判断,不参与取值赋值,别混用。


三、序列化方法

方法 签名 时机 关键说明
__sleep() public array __sleep(): array serialize() 前 返回要序列化的属性名数组;未返回内容 → 序列化为 NULL 并抛 E_NOTICE
__wakeup() public void __wakeup(): void unserialize() 后 典型用途:重建数据库连接
__serialize() public array __serialize(): array serialize() 前(PHP 7.4+) 与 __sleep() 同时定义时,本方法优先且 __sleep() 被忽略
__unserialize() public void __unserialize(array $data): void unserialize() 后(PHP 7.4+) 与 __wakeup() 互斥
__set_state() public static object __set_state(array $properties): object var_export() 导出类时(PHP 5.1+) 返回同类新对象;是实现 var_export 可回导的唯一途径

⚠️ 关键坑:__serialize / __unserialize(7.4+)和 __sleep / __wakeup 是互斥的。Laravel 里遇到 wakeup 初始化不生效,先查类里有没有 __unserialize。


四、类型转换与杂项

方法 签名 时机 关键说明
__toString() public function __toString(): string 对象被当字符串用(echo $obj) 必须返回字符串,否则 E_RECOVERABLE_ERROR;PHP 7.4 起可抛 Exception
__invoke() public function __invoke(mixed ...$args): mixed 把对象当函数调用:$obj() PHP 5.3.0+
__clone() public function __clone(): void 复制完成后,在新对象上调用 ⚠️ clone 是浅拷贝,递归成员不会自动深拷贝
__debugInfo() public function __debugInfo(): array var_dump() 打印对象时 隐藏敏感字段,PHP 5.6.0+

五、PHP 8 严格签名速查

方法 PHP 8 签名(严格模式下会校验)
__call public function __call(string $name, array $arguments): mixed
__callStatic public static function __callStatic(string $name, array $arguments): mixed
__get public function __get(string $name): mixed
__set public function __set(string $name, mixed $value): void
__isset public function __isset(string $name): bool
__unset public function __unset(string $name): void
__sleep public function __sleep(): array
__wakeup public function __wakeup(): void
__serialize public function __serialize(): array
__unserialize public function __unserialize(array $data): void
__set_state public static function __set_state(array $properties): object
__toString public function __toString(): string
__invoke public function __invoke(mixed ...$args): mixed
__clone public function __clone(): void
__debugInfo public function __debugInfo(): array

⚠️ 签名对不上会致命:__isset 返回类型写成 void 在 declare(strict_types=1) 下直接 Fatal error。



预定义变量与超全局变量

一、九个超全局变量

在全部作用域中始终可用,函数内直接可读,不需要 global。

变量 说明 类型 / 来源
$GLOBALS 引用全局作用域中所有变量 array,包含全部全局变量
$_SERVER 服务器和执行环境信息 array,如 $_SERVER['REQUEST_URI']
$_GET HTTP GET 变量 array,URL 查询参数
$_POST HTTP POST 变量 array,表单 / x-www-form-urlencoded
$_FILES HTTP 文件上传变量 array,多维结构 $_FILES['f']['tmp_name']
$_REQUEST HTTP Request 变量(默认合并 GET+POST+COOKIE) array
$_SESSION Session 变量(需先 session_start()) array
$_ENV 环境变量 array
$_COOKIE HTTP Cookies array

二、其他预定义变量

变量 说明 版本 / 备注
$php_errormsg 前一个错误信息 已被 set_error_handler() 取代,不推荐在新代码用
$HTTP_RAW_POST_DATA 原生 POST 数据(未经 PHP 解析) 已废弃(PHP 5.6 起),改用 php://input
$http_response_header HTTP 响应头数组(HTTP 包装器产生) 部分封装下不可用
$argc 传给脚本的参数个数 仅 CLI
$argv 传给脚本的参数数组 仅 CLI

三、常见坑位

现象 根因 处理
$_POST 为空 请求体是 application/json 读 php://input
$_POST 部分数据丢失 max_input_vars 超限(超出的静默丢弃) 调大该值,或改用文件/Redis 传递
$argv[0] 不是脚本名 CLI 才有值 非 CLI 环境不存在

四、三者关系速览

类别 数量级 作用
魔术常量 9 个 编译期替换成字面量,不占运行时开销
魔术方法 15 个 运行时拦截行为:生命周期、访问、序列化、类型转换
超全局变量 9 个 作用域无关,函数内直接可读,不用 global

一句话记忆:

问题 答案
想知道「我在哪」 魔术常量
想拦截「别人对我做什么」 魔术方法
想拿到「请求和环境信息」 超全局变量


CGI · FastCGI · PHP-CGI · PHP-FPM

零、笔记错误速览

原文 修正
「php-fpm 会开启多个 php-cgi 程序」 FPM 的 worker 就是 php-cgi 二进制自身 fork 出来的子进程,同一个可执行文件,不是两个程序
「内存消耗过大引发 502」 502 的直接原因不是内存,是 worker 耗尽/超时导致 Nginx 拿不到上游响应。内存只是背后的推手
「FastCGI 则是一个进程可以处理多个请求」 FastCGI 只规定通信协议,是否复用进程由实现决定

一、一句话结论

名字 本质 一句话
CGI 协议 web server 和后端程序之间的通信规范,规定「传哪些数据、什么格式」
FastCGI 协议 CGI 的改良版,核心是进程复用(长驻,不再一请求一进程)
php-cgi 程序(SAPI) PHP 对 CGI 协议的实现,每次请求起进程、干完就退出
php-fpm 程序(SAPI) PHP 对 FastCGI 协议的实现,自带 master/worker 进程池

关系链:

1
2
3
nginx ──FastCGI 协议──> php-fpm ──fork──> worker(php-cgi 形态,长驻)
│
└──CGI 协议────> php-cgi(一请求一进程,进程即退)

二、为什么需要 CGI 这层协议

环节 发生了什么
静态请求(/index.html) web server 直接读文件系统,不经过任何解释器
动态请求(/index.php) web server 按配置把请求转交给对应解析器
转交时必须带的数据 URL 路径、查询字符串、POST 数据、HTTP Header、客户端 IP、端口…
CGI 管什么 规定「传哪些数据、什么格式」,让后端程序不用为每个 server 写一套适配

CGI 的原始动机是「让后端程序编写者能拿到标准格式的数据」,不是为了性能。它后来被 FastCGI 取代的原因只有一个:太慢。


三、CGI 慢在哪

维度 CGI(每个请求) FastCGI
进程模型 一请求一进程,处理完立即销毁 一进程多请求,处理完接着处理下一个
启动开销 每次都重新解析 php.ini、初始化 Zend 虚拟机 只在 master 启动时做一次,worker 直接复用
进程数随并发增长 并发 N → N 个 CGI 进程 并发 N → N 个 worker,由池大小封顶
内存/资源消耗 高,且频繁分配释放 稳,进程长期驻留
配置生效 每次请求重新读,天然实时 需重启(reload)才生效

FastCGI 的调度逻辑:

步骤 master 做什么
1. 启动时 解析配置文件、初始化执行环境
2. 启动时 预启动多个 worker 进程
3. 请求到达 交给某个空闲 worker,自己立即返回去接下一个
4. worker 不够 按配置预先启动备用 worker
5. worker 太多 回收多余 worker

⚠️ 常见误解:FastCGI 是协议,不是进程。就像 HTTP 是协议,Apache 的实现叫 httpd,Nginx 的实现叫 nginx。


四、四者对照总表

对比项 CGI FastCGI php-cgi php-fpm
类型 协议 协议 程序(PHP SAPI) 程序(PHP SAPI)
解决的问题 通信规范 性能 执行 PHP 代码 调度 + 执行
谁实现它 各语言各有 CGI 程序 php-fpm / spawn-fcgi / IIS 等 — —
进程模型 一请求一进程 协议不规定,靠实现方 单进程,干完即退 master + 多 worker,常驻
能否常驻 否 是(设计目标) 否 是
内置进程池管理 — — ❌ 无 ✅ 有
配置生效 每请求重读 需 reload/restart 改配置需重启 php-cgi reload(优雅)/ restart
PHP 官方地位 历史标准 历史标准 仍保留但不推荐生产 官方推荐
并发承载 差 好 差 好(受 pm.max_children 限制)

五、逐个概念拆开

CGI(Common Gateway Interface)

项 说明
全称 Common Gateway Interface,公共网关接口
作用 让 web server 把动态请求 + 参数,以标准格式传给后端程序
类比 「插座标准」——规定插头长什么样,至于用电器是冰箱还是电视它不管
生命周期 每请求:启动进程 → 读配置 → 处理 → 输出 → 销毁进程
现状 已被 FastCGI 取代,PHP 里基本只作为历史概念出现

FastCGI

项 说明
本质 协议(不是进程)
改进点 进程复用 + 数据结构优化,把「每次初始化」变成「一次初始化」
实现者 php-fpm(官方)、spawn-fcgi(从 lighthttpd 分裂)、IIS FastCGI Handler 等
注意 协议不规定进程数,进程池大小由实现方配置决定(php-fpm 的 pm.*)

php-cgi

项 说明
定位 PHP 提供的 CGI 协议接口程序,就是 PHP 解释器在 CGI 模式下的运行形态
每次请求 启动进程 → 加载 php.ini → 初始化数据结构与运行环境 → 处理请求 → 按 CGI 格式返回 → 销毁进程
配置生效 改 php.ini 必须重启 php-cgi 才生效
致命短板 只是「一个干活的程序」,不会管理进程。高并发顶不住
于是有了 spawn-fcgi、php-fpm 这类「调度器」来 fork 和复用 php-cgi
现状 官方明确不推荐用于生产

php-fpm(PHP FastCGI Process Manager)

项 说明
全称 PHP FastCGI Process Manager
定位 实现了 FastCGI 协议的程序,已被 PHP 官方收录,目前的事实标准
与 php-cgi 的关系 可理解为 php-cgi 的管理程序 + 改进版:不随请求生死,常驻,一进程接多连接
工作方式 master 常驻内存 → 把请求分给某个 worker → worker 处理完不退出,继续等下一个
加速原理 省掉每次的「重载配置 + 初始化数据结构 + 初始化运行环境」
进程结构 master(管理) + N × worker(干活);空闲 worker 超阈值会被回收

六、进程模型示意

模型 请求流向 生命周期
CGI nginx ──> [php-cgi 进程] ──> 结束销毁 进程寿命 = 单个请求寿命
FastCGI + spawn-fcgi nginx ──> spawn-fcgi ──fork─> [php-cgi],进程复用 进程寿命 = 多请求
FastCGI + php-fpm nginx ──> [master 常驻] ──分派─> worker(复用) master 常驻,worker 复用

php-fpm 内部角色:

角色 数量 职责
master 1 读配置、监听 socket、分派请求、管理 worker 生命周期
worker N(pm.max_children 上限) 真正执行 PHP 代码、跑 Zend 虚拟机
idle worker 按参数调节 空闲回收,避免内存浪费

七、内存与 502 的真实关系

说法 是否准确 说明
「php-fpm 开启多个 php-cgi 进程」 简化说法 FPM 的 worker 本身就是 php-cgi 二进制的 fork 子进程
「一个 php-cgi 占 7~25M」 大致量级 实际取决于扩展、memory_limit、脚本复杂度;常见 RSS 在 20~40M
「内存涨太多会引发 502」 间接成立 502 的直接原因不是内存,而是 FPM worker 不够/超时导致上游无响应

502 的典型链路:

环节 现象 排查命令
FPM 进程数打满 请求排队 → 排队超时 pm.max_children 是否过小
pm.max_children × 单进程内存 > 物理内存 系统 swap / OOM,worker 被杀 看 free -h、容器 memory.limit
脚本执行超时 FPM 主动断开,Nginx 报 502 调 request_terminate_timeout
慢脚本 / 死循环 worker 被占满,后续请求全排队 request_slowlog_timeout + slowlog
真正的 OOM kernel 杀进程 Nginx error log 出现 worker process ... exited on signal 9

💡 排查顺序:先看 FPM 的饱和度,再看内存,最后才看代码。别一上来就加内存——pm.max_children 算错的话,加多少内存都会被吃光。

pm.max_children 估算公式:

1
max_children ≈ (可用物理内存 × 0.7) / 单个 worker 实测 RSS

例:机器 4G,可用约 2.8G,worker 实测 30M → max_children ≈ 93,实际还要留余量,一般取 70~80。


八、术语对照(最容易混的一组)

术语 在 PHP 里指什么
cgi 进程 / 线程 就是 php-cgi 进程/线程,专门接收 web server 的动态请求,调用并初始化 Zend 虚拟机
cgi 脚本 被执行的 PHP 源代码文件(.php),和「cgi 进程」不是一回事
Zend 虚拟机 对 PHP 文件做词法分析 → 语法分析 → 编译成 opcode → 执行,最后关闭
两者关系 cgi 进程负责调用并初始化 Zend 虚拟机的各种环境;每个请求一个独立的执行上下文

生命周期串起来看:

顺序 CGI 模式(php-cgi) FastCGI 模式(php-fpm worker)
1 启动进程 worker 已常驻(首次启动时初始化一次)
2 解析 php.ini 跳过(已加载)
3 初始化数据结构 + Zend VM 环境 跳过(已初始化)
4 解析脚本 → 编译 opcode → 执行 同左
5 按 CGI 格式输出结果 同左
6 销毁进程 回到池中等待下一个请求

九、常用误区纠正

常见说法 实际情况
「FastCGI 是一个进程」 ❌ FastCGI 是协议,php-fpm 才是实现它的程序
「php-fpm 和 php-cgi 是两个不同的可执行文件」 ❌ 都是 PHP 的 SAPI 形态;FPM 的 worker 就是自身 fork 出来的子进程
「FastCGI 规定了一进程多请求」 ⚠️ 不准确。FastCGI 只规定通信协议,是否复用进程由实现决定
「用了 FastCGI 就一定不重启」 ❌ FPM 改 php.ini / www.conf 仍需 reload
「CGI 慢是因为语言慢」 ❌ 慢在进程创建销毁 + 每次重复初始化环境,与语言本身无关
「php-cgi 现在还能用」 能,但不推荐生产;官方推荐 php-fpm

十、常用命令速查

场景 命令
启动 systemctl start php-fpm
停止 systemctl stop php-fpm
重载配置(不中断) systemctl reload php-fpm
重启(会中断请求) systemctl restart php-fpm
查看状态 systemctl status php-fpm
看进程 ps -ef | grep php-fpm
看 master/worker 数 pgrep -c php-fpm
验证是否在监听 ss -lntp | grep 9000
改完 www.conf 后 php-fpm -t 测试配置,再 reload

关键 www.conf 参数:

参数 作用 备注
listen 监听地址(端口或 Unix socket) Unix socket 通常比 TCP 快一点
pm 进程管理模式 dynamic(默认)/ static / ondemand
pm.max_children worker 数量上限 最关键的调优项
pm.start_servers 启动时的 worker 数 仅 dynamic 生效
pm.min_spare_servers 空闲 worker 下限 低于此值会补充
pm.max_spare_servers 空闲 worker 上限 高于此值会回收
request_terminate_timeout 单请求超时(秒) 502 的常见来源
request_slowlog_timeout 超过该秒数记录慢日志 排查卡死用
catch_workers_output worker 输出接到 error log 排查问题建议开
security.limit_extensions 限制可执行的后缀 默认 .php

十一、参考

来源 链接
简单说明 CGI 和动态请求是什么 https://www.cnblogs.com/f-ck-need-u/p/7627035.html
PHP-FPM / php-cgi / fast-cgi 与 Nginx 的关系 https://killuachen.com/view/50
PHP 官方 CGI/FastCGI 说明 https://www.php.net/manual/zh/install.cgi.php
PHP-FPM 配置文档 https://www.php.net/manual/zh/install.fpm.configuration.php

封装、继承、多态

零、三个概念的定位

概念 一句话 解决什么问题
封装 把数据和对数据的操作绑在一起,隐藏内部实现 保护——外部不该改的别让人改
继承 子类拿父类的属性和方法,并可扩展 复用——同一套逻辑不必写两遍
多态 同一个接口/父类,不同对象不同表现 解耦——调用方不关心具体实现

一、封装(Encapsulation)

1 访问控制符

修饰符 本类 同类实例 子类 外部
public ✅ ✅ ✅ ✅
protected ✅ ✅ ✅ ❌
private ✅ ✅ ❌ ❌
无(默认) = public = public = public = public

💡 同类实例之间可以访问彼此的 private——判断依据是类,不是对象。

2 封装的三个层次

层 做法
数据封装 private / protected 属性
行为封装 只暴露必要的 public 方法
校验前置 setter 里做合法性检查,非法数据进不来

3 过度封装(反例)

误用 问题
每个属性都写无逻辑的 get/set 纯样板代码
属性设 private 但 __get 里原样返回 假封装,外部照样能改
暴露内部可变数组引用 调用方改了内部状态

💡 现代实践(PHP 8.4+):属性 hooks 和非对称可见性可替代大量样板 getter/setter。

1
2
3
4
5
6
7
8
class User
{
// 公开读,内部写 —— 不用写一整套 get/set
public private(set) string $name = '';

// 带校验的 setter
public int $age { set { if ($value < 0) throw new InvalidArgumentException('年龄不能为负'); $this->age = $value; } }
}

二、继承(Inheritance)

1 继承了什么

成员 是否继承 说明
public 属性与方法 ✅ 完全继承
protected 属性与方法 ✅ 子类可访问
private 属性与方法 ❌ 子类看不到
final 类/方法 ❌ 不允许继承/重写
构造函数 __construct 不会自动继承 子类定义了就必须调 parent::__construct()
析构函数 __destruct 不会自动继承 同上
静态属性 ✅ 但共享同一份值

2 构造链与析构链

规则 说明
构造顺序 父类先构造(parent::__construct() 通常写在子类构造函数第一行)
析构顺序 父类后析构
不调 parent:: 父类的构造/析构被完全跳过,不是「隐式调用后再覆盖」
父类无构造函数 调 parent::__construct() 会 Fatal error

⚠️ 经典坑:class Child extends Parent 但 Parent 没有 __construct,子类里写了 parent::__construct() → 致命错误。这是 PHP 里最常见的继承坑之一。

安全写法:

1
2
3
if (method_exists(parent::class, '__construct')) {
parent::__construct();
}

3 重写(Override)的规则

规则 说明
可见性不能缩小 父类 public → 子类不能改 protected(会报错)
可见性可以放大 父类 protected → 子类可以改 public ✅
参数必须兼容 不能新增必填参数;可减(逆变);返回类型须协变或一致
不能重写 final Fatal error
构造函数是覆盖不是重载 PHP 没有方法重载
参数名 PHP 8.0 起参与检查 参数名不一致会警告

4 继承的代价(组合优于继承)

问题 说明
脆弱基类 父类改一个方法,所有子类行为可能全变
紧耦合 子类被迫继承不需要的东西
层次僵化 单继承限制了类结构自由度
难以测试 子类为了测一个方法要实现整个父类契约

判断:

关系 用什么
is-a 且需替换实现 继承(MySqlRepository extends AbstractRepository)
has-a 组合(依赖作为构造参数传入)

三、多态(Polymorphism)

⚠️ 笔记定义偏窄:「一个父类被多个子类继承,父类的某个方法在多个子类中表现出不同的功能」——这只是重写多态。PHP 里多态有三条路径,接口多态才是现代 PHP 的主流。

1 三种实现路径

实现方式 机制 是否需要继承 现代价值
重写(override) 子类重写父类方法 ✅ 需要 中(模板方法模式)
接口多态 不同类实现同一接口 ❌ 不需要 高(主流)
参数多态 方法参数声明为接口类型 — 最高(现代主流)

2 参数多态(现代 PHP 的主流写法)

1
2
3
4
5
6
7
8
9
10
// 关键:参数类型只声明接口,运行时随便传哪个实现都行
function process(PaymentGateway $gateway, int $amount): bool
{
return $gateway->pay($amount); // 调接口契约
}

process(new WechatPay(), 100); // 调微信实现
process(new Alipay(), 100); // 调支付宝实现
process(new UnionPay(), 100); // 调银联实现
// 三行代码一模一样,一行没改

💡 这才是多态的价值:调用方代码不随实现变化而变化。

3 运行时多态的原理

步骤 发生了什么
1 foreach 遍历数组,每个元素实际类型不同
2 调用 $a->speak(),PHP 在运行时查对象真实类型
3 找到对应实现并执行
4 返回各自不同的结果

关键:编译时不需要知道具体类型。

4 重写多态 vs 接口多态

场景 重写多态 接口多态
代码量 少(共用父类属性) 多(每个实现写一遍)
耦合度 高 低
可替换性 差(要重新继承链) 好(换实现不改调用方)
适用 模板方法、共享状态 支付/存储/日志等可插拔组件

5 多态用错的症状

症状 问题 正确做法
大量 if ($obj instanceof X) 用类型判断代替了多态 参数声明为接口
多个方法签名都写同一组类型 没抽接口 定义 GatewayInterface
换了实现要改多处调用代码 耦合没解 接口 + 依赖注入


多重继承的替代方案

零、方案对比

PHP 不支持真正的多重继承(C++ / Python / Ruby 才支持),替代手段有两个:

方案 提供什么 是否共享实现
接口(多实现) 方法契约 ❌ 只有声明
Trait(多混入) 方法实现 ✅ 完整实现

💡 组合记忆:接口解决「多继承关系」,trait 解决「多复用实现」。接口给契约,trait 给代码。

维度 接口(多实现) Trait(多混入)
关键字 implements use(类体内)
能有实现吗 ❌ ✅
能有属性吗 PHP ≤8.3 ❌;8.4+ 可声明契约 ✅
能有构造函数吗 声明不推荐 ✅ 可有
能被实例化吗 ❌ ❌
冲突处理 可用协变/逆变兼容 insteadof / as
语义 「我能做某事」 「借用一段行为」


Trait

零、是什么

项 说明
定义 PHP 5.4 引入的代码复用机制
本质 「有编译器帮忙的复制粘贴」 —— 官方原话
关键词 use(类体内,和命名空间的 use 不是一回事)
能否实例化 ❌ 不能,它不是类
能否继承 ❌ 不能被 extends
核心价值 在没有继承关系的多个类间横向复用代码

一、基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
trait SayHello { public function sayHello(): string { return 'Hello'; } }
trait SayWorld { public function sayWorld(): string { return 'World'; } }

class MyHelloWorld
{
use SayHello;
use SayWorld;

public function sayHelloWorld(): string
{
return $this->sayHello() . ' ' . $this->sayWorld();
}
}

echo (new MyHelloWorld)->sayHelloWorld(); // Hello World
语法 说明
use TraitA; 混入一个
use TraitA, TraitB; 混入多个(逗号分隔)
use TraitA, TraitB { ... } 带冲突解决

二、Trait 的三个「魔法」特性

特性 说明
能访问宿主的私有成员 trait 里的 $this 就是宿主对象,$this->var 能访问宿主的 private
__CLASS__ 返回宿主类名 在 trait 里返回的是使用它的类名,不是 trait 名
可以组合 trait trait 里也能 use 其他 trait
1
2
3
4
5
6
7
8
9
// ① 访问宿主私有成员
trait MyTrait {
protected function accessVar(): string { return $this->var; } // 宿主的 private
}
class TraitUser {
use MyTrait;
private string $var = 'var';
public function getVar(): string { return $this->accessVar(); }
}
1
2
3
4
5
// ② __CLASS__ 返回宿主类名
trait SayWhere { public function whereAmI(): string { return __CLASS__; } }
class Hello { use SayWhere; }
class World { use SayWhere; }
echo (new Hello)->whereAmI(); // Hello ← 不是 SayWhere

三、优先级顺序(最常考,也最常错)

1
当前类自身定义的方法  >  Trait 中的方法  >  父类继承的方法
场景 谁赢
类自己写了同名方法 类自己赢(trait 被静默忽略,不报错)
trait 与父类同名方法 trait 赢
两个 trait 同名方法 ⚠️ Fatal error,必须显式解决

⚠️ PHP 8.5 有变更:

版本 trait 与父类都有同名属性/常量时
PHP ≤ 8.4 Fatal error(不兼容的组合)
PHP 8.5+ trait 优先,覆盖父类继承的成员

方法解析规则不变(trait 一直覆盖父类方法),变的是属性和常量。


四、冲突解决:insteadof 与 as

两个 trait 有同名方法时,不解决直接 Fatal:

1
2
Fatal error: Trait method log has not been applied, because there are collisions
with other trait methods on App\Services\OrderProcessor

1 三种解决方案

方案 写法 效果
二选一 Logger::log insteadof Auditor; 用 Logger 的,排除 Auditor 的
两个都要 insteadof + as 决定 log() 用谁的,另一个起别名保留
改可见性 Auditor::log as private internalAudit; 另起别名且改为 private
1
2
3
4
5
6
7
8
9
10
11
class OrderProcessor
{
use Logger, Auditor {
Logger::log insteadof Auditor; // 决定 log() 用谁的
Auditor::log as saveToAuditLog; // 另一个另起别名保留
}
}

$p = new OrderProcessor();
$p->log('订单创建'); // 写文件
$p->saveToAuditLog('订单创建'); // 写数据库

2 操作符说明

操作符 作用 注意
insteadof 冲突时选用哪个,排除另一个 只能排除,不能改名
as 给方法起别名 ⚠️ 不改变原方法名,也不影响其他方法
as + 可见性 as protected / as private 单独用 as 也可以

💡 排查冲突的实用技巧:用反射把 trait 方法全打出来对比,不用猜。

1
2
3
$r = new ReflectionClass(OrderProcessor::class);
print_r($r->getTraits()); // 用了哪些 trait
print_r($r->getTraitAliases()); // 别名映射

五、其他规则

1 属性的冲突规则(比方法严格)

场景 结果
两个 trait 声明同名属性 必须完全一致(可见性 + 类型 + readonly + 默认值),否则 Fatal
PHP 8.2+ 允许只声明类型不给默认值,但两边类型仍须一致
类定义了与 trait 同名属性 同上,兼容性检查

2 trait 中的抽象方法(强制约束)

版本 支持的抽象方法可见性
PHP < 8.0 仅 public / protected
PHP ≥ 8.0 public / protected / private 都支持
1
2
3
4
5
6
7
8
9
10
11
12
trait Hello
{
public function sayHello(): void { echo 'Hello ' . $this->getWorld() . "\n"; }
abstract public function getWorld(): string; // 强制使用方实现
}

class MyHelloWorld
{
use Hello;
private string $world;
public function getWorld(): string { return $this->world; }
}

3 trait 中的静态成员

项 说明
静态变量/方法/属性 ✅ trait 可定义
静态属性:每个用该 trait 的类是独立副本 和继承的共享语义完全不同
直接在 trait 上访问静态成员 PHP 8.1+ 起弃用,必须通过类访问

静态属性对比(重要):

机制 静态属性行为
继承 父子类共享同一份(改一处两处都变)
trait 每个用该 trait 的类各有一份(互不影响)
1
2
3
4
5
6
7
8
9
10
11
12
13
// 继承:共享
class Base { public static $bar; }
class Foo1 extends Base {}
class Foo2 extends Base {}
Foo1::$bar = 'Hello'; Foo2::$bar = 'World';
echo Foo1::$bar; // World ← 共享

// trait:独立
trait T { public static $bar; }
class Foo1 { use T; }
class Foo2 { use T; }
Foo1::$bar = 'Hello'; Foo2::$bar = 'World';
echo Foo1::$bar; // Hello ← 独立

⚠️ PHP 8.3 起又改了:子类自己也用了同一个 trait 时,它与父类的 trait 静态属性视为不同的。8.3 之前同一继承层次里是共享的。

4 版本能力时间线

版本 新增能力
PHP 5.4 trait 本身
PHP 7.4 trait 中可定义抽象属性
PHP 8.0 支持 private 抽象方法;具体方法签名需兼容
PHP 8.1 直接在 trait 上访问静态方法/属性弃用
PHP 8.2 trait 中可定义常量;属性可只声明类型
PHP 8.3 as 操作符可用 final;子类重复用 trait 时静态属性独立
PHP 8.5 trait 与父类同名属性/常量时 trait 优先(此前 Fatal)

trait 定义常量(8.2+):

1
2
3
4
5
6
7
8
trait ConstantsTrait
{
public const FLAG_MUTABLE = 1;
final public const FLAG_FROZEN = 5;
}

class Example { use ConstantsTrait; }
echo Example::FLAG_MUTABLE; // 1

⚠️ 类若要定义与 trait 同名常量,必须完全兼容(可见性、初始值、final 性),否则 Fatal。不能直接访问 trait 自身的常量。

5 final 在 trait 里的特殊行为

写法 效果
trait T { final public function f() {} } ❌ final 无效,类可自由覆盖
class C { use T; final public function f() {} } ✅ 类自己声明,覆盖 trait
class C { use T { T::f as final; } } ✅ PHP 8.3+,子类不能覆盖(类自己仍可)

六、Trait vs 继承 vs 接口 vs 组合

1 四选一决策表

机制 提供 关系 何时选
继承 extends 属性 + 方法实现 + 契约 is-a,单继承 确实是父子类型关系,需替换实现
接口 implements 仅契约 can-do,可多个 声明能力,让实现可插拔
Trait use 属性 + 方法实现 无关系(混入) 多个不相关类需要同一段行为
组合(构造注入) 对象引用 has-a 默认首选

2 判断流程

1
2
3
4
5
这段代码要在几个类里复用?
├─ 1 个 + 有 is-a 关系 → 继承
├─ 只需要声明「能做什么」 → 接口
├─ 多个【无继承关系】的类要同一段实现 → trait
└─ 不确定 → 组合(把依赖传进来)

3 维度对比

维度 继承 接口 Trait 组合
代码复用 ✅ ❌ ✅ ✅(对象级)
需要继承关系 ✅ 必须 ❌ ❌ ❌
数量限制 1 个父类 N 个 N 个 N 个
能有属性 ✅ ≤8.3 ❌ ✅ ✅
能有实现 ✅ ❌ ✅ ✅
耦合度 高 低 中(混入) 最低
可替换性 差 好 无(混入即固定) 好
冲突可能 重写冲突 需协变兼容 同名方法冲突 无

4 trait 的常见误用

误用 问题 正确做法
把 trait 当「继承的替代」到处塞 隐式依赖,类不知道自己有这些方法 用组合,或明确定义接口
一个 trait 塞几十个方法 变成「上帝 trait」,难维护 按职责拆成多个小 trait
trait 之间 insteadof 打架 维护地狱 检查命名,或改用组合
trait 依赖宿主某个不存在的属性 运行时才炸,静态分析不报错 用抽象方法声明契约
把有状态逻辑放 trait 状态归属不清 状态放类里,trait 只放行为

trait 引用宿主属性导致静默 bug:

1
2
3
4
5
6
7
8
9
10
// ❌ 危险:隐式依赖 $this->request,IDE 和静态分析都发现不了
trait HandlesRequest {
public function handle(): string { return $this->request->getUri(); }
}

// ✅ 安全:显式声明契约
trait HandlesRequest {
abstract public function getRequest(): Request;
public function handle(): string { return $this->getRequest()->getUri(); }
}

💡 trait 里的 $this 指向宿主对象,所以 trait 必须假定宿主「长什么样」。这个隐式约定是 trait 最大的风险点。


七、综合实战示例

典型 PHP 项目的分层组合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<?php
declare(strict_types=1);

namespace App\Models;

// 1. 接口:定义契约
interface Payable
{
public function pay(int $amount): bool;
public function refund(string $orderNo): bool;
}

// 2. trait:横向复用行为(不相关的类也要用)
trait Loggable
{
private array $logs = [];
protected function log(string $msg): void { $this->logs[] = $msg; }
public function getLogs(): array { return $this->logs; }
}

// 3. 抽象类:共享实现 + 部分契约
abstract class BasePayment implements Payable, \JsonSerializable
{
use Loggable; // 混入行为

protected array $config;

public function __construct(array $config) { $this->config = $config; }

public function pay(int $amount): bool
{
$this->validate($amount);
$this->log("发起支付 {$amount}");
return $this->doPay($amount);
}

public function refund(string $orderNo): bool
{
$this->log("退款 {$orderNo}");
return $this->doRefund($orderNo);
}

public function jsonSerialize(): array { return $this->config; }

abstract protected function doPay(int $amount): bool;
abstract protected function doRefund(string $orderNo): bool;

private function validate(int $amount): void
{
if ($amount <= 0) {
throw new \InvalidArgumentException('金额必须大于 0');
}
}
}

// 4. 具体类:只实现差异部分
class WechatPay extends BasePayment
{
protected function doPay(int $amount): bool { return true; }
protected function doRefund(string $orderNo): bool { return true; }
}

// 5. 使用:参数多态
function checkout(Payable $gateway, int $amount): bool
{
return $gateway->pay($amount);
}

checkout(new WechatPay(), 100);
checkout(new Alipay(), 100); // 一行代码没改

这个例子里五个概念各司其职:

机制 出现位置 职责
接口 Payable 声明「能付能退」
trait Loggable 给所有支付类加日志,它们之间无继承关系
抽象类 BasePayment 共享校验和流程
多态 checkout(Payable $gateway) 换实现不改调用
封装 private array $logs 外部不能直接改日志数组

八、常见错误速查

场景 报错 原因 解法
子类调 parent::__construct() Fatal: Cannot call constructor 父类没有构造函数 method_exists() 判断,或给父类加空构造
两个 trait 有同名方法 Fatal: Trait method X has not been applied 冲突未解决 insteadof
两个 trait 有同名属性 Fatal: Trait and Class X define the same property 属性规则更严 保证完全一致,或改名
trait 直接访问静态成员 Deprecated(8.1+) 直接在 trait 上访问 通过使用它的类访问
想 new 一个 trait Fatal trait 不是类 通过宿主类实例化
想 extends 一个 trait Fatal trait 不能被继承 用 use 混入
子类缩小父类方法可见性 Fatal public → protected 非法 只能保持或放大
trait 里用宿主私有属性 运行时报错 宿主没有该属性 改用抽象方法声明契约

九、参考

来源 链接
Trait(官方) https://www.php.net/manual/zh/language.oop5.traits.php
继承(官方) https://www.php.net/manual/zh/language.oop5.inheritance.php
类常量(官方) https://www.php.net/manual/zh/language.oop5.constants.php
Traits 中定义常量 RFC(8.2) https://wiki.php.net/rfc/constants_in_traits
访问控制(官方) https://www.php.net/manual/zh/language.oop5.visibility.php

PHP7 中的 ?? 和 ?:

零、原文测试用例与结果

原文用一个数组同时测 10 种写法,这是理解两者差别的最佳切入点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
$array = [
'a' => 1,
'b' => 2,
'c' => [], // 关键:空数组
];

$a = $array['c'] ?? 0; // a
$b = $array['c'] ?: 0; // b
$c = $array['d'] ?? 0; // c ← 键不存在
$d = $array['d'] ?: 0; // d ← 键不存在
$e = $array['c'] ? 1 : 0; // e
$f = isset($array['c']) ? 1 : 0; // f
$g = $array['d'] ? 1 : 0; // g ← 键不存在
$h = isset($array['d']['e']) ? 1 : 0; // h
$i = !empty($array['c']) ? 1 : 0; // i
$j = !empty($array['d']) ? 1 : 0; // j

var_dump($a, $b, $c, $d, $e, $f, $g, $h, $i, $j);

实际输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PHP Notice:  Undefined index: d in /home/fanyu/abc.php on line 11
PHP Notice: Undefined index: d in /home/fanyu/abc.php on line 14

array(0) { // $a —— 空数组原样返回
}
int(0) // $b —— 被当成假值,替换成 0
int(0) // $c
int(0) // $d
int(1) // $e
int(1) // $f
int(0) // $g
int(0) // $h
int(0) // $i
int(0) // $j

⚠️ 原文没点出的关键细节:只有 $d(第 11 行)和 $g(第 14 行)报了 Undefined index,$c 用 ?? 完全静默。这正是 ?? 和 ?: 最本质的区别——?? 不会触发任何 Notice,?: 会。


一、四个等价关系

写法 等价于 说明
$a ?? $b isset($a) ? $a : $b 判「是否为 null」
$a ?: $b $a ? $a : $b 判「是否为真值」
$a ??= $b if (!isset($a)) { $a = $b; } PHP 7.4+,赋值写法
empty($a) !isset($a) || $a == false PHP 5.5+ 起支持表达式

📌 记忆锚点:?? 是 isset 的简写,?: 是三元表达式的简写。所以判断「空」的基准完全不同。


二、完整对照表

值 isset() ??(取默认值) ?:(取默认值) empty()
键不存在 false 用默认值(静默) 用默认值(⚠️ 报 Notice) true
null false 用默认值 用默认值 true
false true false 用默认值 true
0 true 0 用默认值 true
0.0 true 0.0 用默认值 true
'' true '' 用默认值 true
'0' true '0' 用默认值 true
[](空数组) true [] 用默认值 true
[0] true [0] [0] false
new stdClass() true 对象 用默认值 true
' '(空格) true ' ' ' ' false

三个最容易翻车的点:

# 坑 说明
1 '0' 是空值 ?: 会把字符串 '0' 吃掉,?? 保留
2 空数组是空值 原文测试里 $array['c'] 就是空数组,?: 返回了 0,类型都变了
3 无属性对象是空值 empty(new stdClass()) 是 true

三、?: 与 ?? 的本质区别

维度 ?? ?:
判断依据 是否为 null 是否为真值(empty 语义)
被「吃掉」的值 只有 null null false 0 0.0 '' '0' [] 空数组 无属性对象
键不存在时 静默,不报 Notice ⚠️ 报 Undefined index
嵌套访问安全性 ✅ 安全 ❌ 报错
适用场景 只需兜底 null 确实需要「空/零都算空」

核心判据:

你想表达的语义 用
「没设置就用默认值」 ??
「值为空/零就用默认值」 ?:
「这是不是空数组」 empty($a) 或 $a === []

四、嵌套访问的安全性对比

1
$data = ['user' => ['name' => 'Ada']];
写法 结果 是否报错
$data['user']['age'] ?? '未知' '未知' ✅ 不报错
isset($data['user']['age']) ? '有' : '无' '无' ✅ 不报错
$data['none']['age'] ?? '未知' '未知' ✅ 不报错(内部任一层缺失都安全)
$data['none']['age'] ?: '未知' '未知' ⚠️ 报 Undefined index: none
!empty($data['none']['age']) ? '有' : '无' '无' ✅ 不报错

💡 ?? 和 isset() 都支持多层安全访问,?: 不支持。 处理可能缺层的嵌套数据($_GET、$_POST、API 响应)时,?? 是最常用的写法。


五、??= 赋值运算符(PHP 7.4+)

1
2
3
4
5
6
7
// 旧写法
if (!isset($config['debug'])) {
$config['debug'] = false;
}

// ??= 写法
$config['debug'] ??= false;
项 说明
引入版本 PHP 7.4
行为 $a 为 null 时才赋值 $b;非 null 时不改动
与 ?: 等价吗 ❌ 不等价。?: 是三元表达式(结果是值),??= 是赋值(改变原变量)
1
2
3
4
5
6
7
$a = 0;
$a ??= 5; // $a 仍是 0,因为不是 null
echo $a; // 0

$b = null;
$b ??= 5;
echo $b; // 5

⚠️ 注意:$a ??= 5 不会把 0 或 '' 当成空。只有 null 才赋值。


六、其他空值判断函数对比

函数 引入版本 判断依据 能用于表达式? 能否判断键存在(值为 null)
isset() PHP 4 是否为 null ❌ 只能变量 ❌
empty() PHP 4 是否为假值 ✅ 5.5+ 起可 ❌
array_key_exists() PHP 4 键是否存在 ✅ ✅
?? PHP 7.0 是否为 null ✅(简写 isset) ❌
?: 一直有 是否为真值 ✅(三元简写) ❌
??= PHP 7.4 是否为 null ✅(赋值) ❌

六种写法的等价关系:

想做的事 推荐写法
兜底 null $a ?? $b
兜底 null(并赋值) $a ??= $b
判「非空」 !empty($a)
判「键存在」(哪怕值为 null) array_key_exists('k', $arr)
判「值为空数组」 $a === []
判「严格等于某个值」 $a === 0(不要用 ?:)

七、实战场景选型

场景 写法 理由
GET 参数取默认值 $_GET['page'] ?? 1 键不存在时静默,不污染 error log
POST 字段可能未提交 $_POST['name'] ?? '' 同上
金额为 0 时要保留 $amount ?? $default ✅ ?? 只看 null,0 非 null 会被保留
金额为 0 也要走默认值 $amount ?: $default ✅ 这里 ?: 才对
配置读取(有默认值) $config['timeout'] ?? 30 标准写法
数组判空 empty($list) || $list === [] ?: 会把空数组吃掉
表单值判「填了没」 trim($v) !== '' ❌ 别用 empty(),'0' 会误判
嵌套数组安全取值 $data['a']['b']['c'] ?? null 唯一安全写法

八、常见错误速查

错误写法 现象 正确写法
if ($name ?: '默认') $name = '0' 时走进默认分支 if ($name ?? '默认')
$list ?: $other $list = [] 时被替换成 $other $list ?? $other 或 $list === [] ? $other : $list
!empty($name) 判「填了没」 '0' 被当成没填 trim($name) !== ''
$data['x'] ?: 0 键不存在时报 Notice $data['x'] ?? 0
$config['a'] = $config['a'] ?: 5 值为 0 时被覆盖 $config['a'] ??= 5 或 $config['a'] ?? 5
以为 ?? 能判「键存在」 值为 null 时误判为不存在 用 array_key_exists()

九、30 秒自测

问题 答案
$a ?? $b 等价于什么? isset($a) ? $a : $b
$a ?: $b 等价于什么? a ? $a : $b
两者的判断依据差别? ?? 判是否为 null;?: 判是否为真值
$a ?? $b 键不存在时报错吗? 不报错,完全静默
$a ?: $b 键不存在时报错吗? ⚠️ 报 Undefined index
哪个会吃掉空数组 []? ?: 会,?? 不会
哪个会吃掉字符串 '0'? ?: 会,?? 不会
??= 是哪个版本引入的? PHP 7.4
??= 的判断依据? 只看 null,0 和 '' 不算
要判「键存在但值为 null」怎么办? 用 array_key_exists(),两个简写都不行

十、参考

来源 链接
原文(PHP7中??和?:) https://blog.csdn.net/weixin_43154922/article/details/82692831
NULL 合并运算符(官方) https://www.php.net/manual/zh/language.operators.comparison.php#language.operators.comparison.coalesce
三元运算符(官方) https://www.php.net/manual/zh/language.operators.ternary.php
empty()(官方) https://www.php.net/manual/zh/function.empty.php
array_key_exists()(官方) https://www.php.net/manual/zh/function.array-key-exists.php

array_walk、foreach、for 效率对比

零、原文测试用例与结果

原文(2017-03)用一个 10000 元素数组,测试三种写法求「每个数加 1」:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?php
$max = 10000;
$test_arr = range(0, $max);
$temp;

// for,不操作数组
$t1 = microtime(true);
for ($i = 0; $i < $max; $i++) { $temp = $temp + 1; }
$t2 = microtime(true);
echo "就使用for, 没有对数组操作 花费: " . ($t2 - $t1) . "\n";

// for,直接对数组操作
$t1 = microtime(true);
for ($i = 0; $i < $max; $i++) { $test_arr[$i] = $test_arr[$i] + 1; }
$t2 = microtime(true);
echo "使用for 并且直接对数组进行了操作 花费: " . ($t2 - $t1) . "\n";

// for,调用函数
$t1 = microtime(true);
for ($i = 0; $i < $max; $i++) { addOne($test_arr[$i]); }
$t2 = microtime(true);
echo "使用for 调用函数对数组操作 花费 : " . ($t2 - $t1) . "\n";

// foreach,不操作数组
$t1 = microtime(true);
foreach ($test_arr as $k => &$v) { $temp = $temp + 1; }
$t2 = microtime(true);
echo "使用 foreach 没有对数组操作 花费 : " . ($t2 - $t1) . "\n";

// foreach,直接对数组操作
$t1 = microtime(true);
foreach ($test_arr as $k => &$v) { $v = $v + 1; }
$t2 = microtime(true);
echo "使用 foreach 直接对数组操作 : " . ($t2 - $t1) . "\n";

// foreach,调用函数
$t1 = microtime(true);
foreach ($test_arr as $k => &$v) { addOne($v); }
$t2 = microtime(true);
echo "使用 foreach 调用函数对数组操作 : " . ($t2 - $t1) . "\n";

// array_walk
$t1 = microtime(true);
array_walk($test_arr, 'addOne');
$t2 = microtime(true);
echo "使用 array_walk 花费 : " . ($t2 - $t1) . "\n";

function addOne(&$item) { $item = $item + 1; }

原文输出(单位:秒):

写法 耗时
for,没有对数组操作 0.15389
foreach,没有对数组操作 0.07693
for,直接对数组进行了操作 0.14769
foreach,直接对数组操作 0.07612
for,调用函数对数组操作 0.32393
foreach,调用函数对数组操作 0.25717
array_walk 0.17967

原文结论:

# 结论
1 foreach 的效率要比 for 高很多,「以后能用 foreach 的地方就用 foreach,可以提高 1 倍的效率」
2 如果循环内要调用函数,用 array_walk 最好,比 for 高出 1 倍,比 foreach 高出 43%
3 对效率要求很高时,不要在很深的循环中调用函数,要用 array_walk,最好直接把代码写在循环里

一、⚠️ 原文结论已过时(本机 PHP 8.4 实测推翻)

🔴 原文是 2017 年 PHP 7.0 的数据,其三条结论在 PHP 7.3 ~ 8.4 上全部不成立。 下面是在 Laragon 本机跑的真实数据。

测试环境与改进点:

项 原文 本次实测
数组规模 10000 100000(放大 10 倍,减少计时噪声)
重复次数 1 次 5 轮取中位数(消除单次抖动)
覆盖写法 7 种 10 种(补 array_map、while+next()、值/引用对比)
PHP 版本 7.0 7.3 / 8.1 / 8.3 / 8.4 交叉验证

本机 PHP 8.4.22 实测结果(单位:毫秒):

写法 耗时 相对 for 基线
for,不操作数组(纯循环基线) 0.784 1.00x
for,直接写数组 1.391 0.56x
for,调用函数 3.334 0.24x
foreach 值,直接写数组 1.864 0.42x
foreach 引用,直接写数组 2.691 0.29x
foreach 值,调用函数 5.535 0.14x
foreach 引用,调用函数 4.573 0.17x
array_walk 4.004 0.20x
array_map(返回新数组) 2.199 0.36x
while + next() 3.021 0.26x

四个版本交叉验证(单位:毫秒):

写法 PHP 7.3 PHP 8.1 PHP 8.3 PHP 8.4
for,直接写数组 1.637 1.370 1.334 1.391
foreach 值,直接写数组 2.071 2.414 1.884 1.864
foreach 引用,直接写数组 1.527 2.459 2.934 2.691
for,调用函数 2.932 2.881 3.432 3.334
foreach 引用,调用函数 2.582 3.493 4.968 4.573
array_walk 2.835 3.777 4.441 4.004
array_map 2.311 2.050 2.260 2.199

结论翻转:

原文结论 实测结果
❌ 「foreach 比 for 快 1 倍」 ✅ 反了:需要操作数组时,for 反而比 foreach 快(8.4 下 1.39ms vs 1.86ms)
❌ 「array_walk 比 foreach 快 43%」 ✅ 反了:8.4 下 array_walk(4.00ms)比 foreach 值(1.86ms)慢一倍多
❌ 「循环内调用函数 array_walk 最好」 ✅ 反了:直接内联代码(for 直接写 1.39ms)比任何函数封装都快
✅ 「不要在深循环里调用函数」 ✅ 依然正确,且差距比 2017 年更大(3.3ms vs 1.4ms,约 2.4 倍)

二、为什么会翻转

原因 说明
Zend Engine 3 优化了数组访问 PHP 7 引入 AST + 优化后的 opcode,$arr[$i] 的哈希查找成本大幅下降,for 循环的劣势被抹平
opcache 默认启用 编译期开销被摊薄,循环本身的差异凸显出来
PCRE JIT / 内建函数优化 array_walk 的回调调用无法被内联优化
回调调用成本被放大 PHP 8 每次用户函数调用都有更严格的参数类型检查,array_walk 的间接调用最吃亏

💡 历史原因:2017 年 for 循环每次 $arr[$i] 都要走哈希表查找,而 foreach 直接用内存指针顺序遍历,所以差距明显。PHP 7 之后这个优化被大幅压缩,差距从「2 倍」变成「几乎打平甚至反超」。


三、正确的性能排序(本机 PHP 8.4)

场景一:只遍历、不修改数组

排名 写法 耗时 说明
🥇 for 纯循环(不碰数组) 0.784 ms 本来就没数组操作,最快
🥈 while + next() 3.021 ms 需要手动 reset(),且不能改值

场景二:需要修改数组元素

排名 写法 耗时 说明
🥇 for + $arr[$i] 1.391 ms 现代 PHP 最快
🥈 foreach 值 + $arr[$i] = ... 1.864 ms 比 for 慢约 34%
🥉 foreach 引用 2.691 ms 引用有额外开销
4 array_map(返回新数组) 2.199 ms 额外内存开销

场景三:需要对每个元素做复杂处理

排名 写法 耗时 说明
🥇 代码直接内联在 for 里 1.391 ms 无函数调用开销
🥈 代码内联在 foreach 值里 1.864 ms
🥉 for 内调用函数 3.334 ms 有函数调用开销
4 array_walk 4.004 ms 双重开销:遍历 + 回调

💡 核心结论:函数调用的开销远大于循环结构的差异。 优化循环写法带来的收益,远不如把函数调用内联化。


四、六种写法的完整对比

写法 能否改值 能否改键 是否返回新数组 本机耗时 主要问题
for ($i = 0; $i < $n; $i++) ✅ $arr[$i] ❌ ❌ 1.391 需手动维护下标,不适合关联数组
foreach ($arr as $i => $v) ✅ $arr[$i] ❌ ❌ 1.864 同上
foreach ($arr as &$v) ✅ 直接 $v ❌ ❌ 2.691 ⚠️ 必须 unset($v)
foreach ($arr as $k => $v) ❌ 只读 ❌ ❌ — 最安全,只读遍历
array_walk($arr, 'cb') ✅ 回调按引用 ❌ ❌ 4.004 ⚠️ 回调必须声明按引用,且不能增删改数组结构
array_map('cb', $arr) ✅ ❌ ✅ 2.199 返回新数组,原数组不变
while (next($arr)) ❌ ❌ ❌ 3.021 指针状态易乱,不可重入

五、array_walk 的三个硬限制

限制 说明 后果
回调必须按引用声明 function cb(&$item) 不能写成 function cb($item) 传值时 PHP 报 Notice,PHP 8+ 更严格
不能改变数组结构 不能 unset()、不能 array_splice()、不能新增键 破坏了遍历的内部状态,可能导致跳过元素
内部函数不适用 回调必须是用户函数或有效的 callable array_walk($arr, 'strtoupper') 会报错
1
2
3
4
5
6
7
// ❌ 回调未按引用
function addOne($item) { $item = $item + 1; } // 改的是副本
array_walk($arr, 'addOne'); // 白跑一趟

// ✅ 必须按引用
function addOne(&$item) { $item = $item + 1; }
array_walk($arr, 'addOne'); // 生效

array_walk vs array_map 怎么选:

需求 用
就地修改原数组 array_walk(回调按引用)
产生新数组,不改原数组 array_map(返回值)
需要同时传额外参数 array_walk($arr, 'cb', $extra) ✅
回调要返回新值 array_map ✅(array_walk 忽略返回值)
需要键名 array_walk 的回调第二参数按值传入 $key

六、foreach 值 vs 引用:必须搞清

写法 改 $v 是否影响数组 是否必须 unset 本机耗时
foreach ($arr as $v) ❌ 只读副本 不需要 1.864(更快)
foreach ($arr as &$v) ✅ 直接改数组元素 ⚠️ 必须 2.691(更慢)

引用残留 bug(务必记住):

1
2
3
4
5
6
7
8
9
10
11
12
13
$arr = [1, 2, 3];

foreach ($arr as &$v) { $v *= 2; }
print_r($arr); // [2, 4, 6] 正确

foreach ($arr as $item) { echo $item; }
echo $arr[0]; // ❌ 被改成 6 了!$item 仍指向 $v

// ✅ 正确:循环结束就 unset
foreach ($arr as $k => $v) { // 故意不用 &
echo $v;
}
echo $arr[0]; // 2 正常

💡 口诀:只要写了 &$v,循环结束就补一句 unset($v)。


七、实战选型

场景 推荐写法 理由
遍历下标数组做数值计算 for ($i = 0; $i < $n; $i++) 本机实测最快
遍历关联数组 foreach ($arr as $k => $v) 唯一可行,且最安全
只读遍历 foreach ($arr as $v) 最快且无副作用
遍历数字下标数组且要改值 foreach ($arr as $i => $v) { $arr[$i] = ...; } 比引用版快,避免 unset 陷阱
就地改值且是关联数组 foreach ($arr as &$v) + unset($v) 无更简洁的写法
需要产生新数组 array_map() 返回新数组,语义清晰
需要带额外上下文 array_walk($arr, 'cb', $extra) 唯一支持额外参数的方案
深度循环中的重计算 ⚠️ 先缓存,别调函数 函数调用开销是循环结构的 2~3 倍

优化的正确优先级:

优先级 优化动作 收益
🥇 消除 N+1 查询(数据库) 数量级提升
🥈 加索引 / 减少 I/O 数量级提升
🥉 避免深循环内调函数 2~3 倍
4 开启 OPcache 2~5 倍
5 循环体代码内联 2 倍
6 换循环结构(for/foreach) ⚠️ 10%~30%,甚至负收益

💡 别把时间花在第 6 项上。 循环结构的差异在现代 PHP 里不到 30%,而一个没加索引的查询能差几万倍。


八、常见错误速查

错误写法 问题 正确写法
foreach ($arr as &$v) {...} 循环后不 unset 后续循环污染数据 循环体后加 unset($v)
array_walk($arr, 'cb'),回调无 & 改的是副本,白跑 回调声明 function cb(&$item)
在 array_walk 里 unset($arr[$k]) 破坏遍历状态,可能跳过元素 改用 array_filter
用 for 处理关联数组 下标不连续,逻辑错乱 用 foreach
深循环里调函数「为了代码整洁」 慢 2~3 倍 内联代码,或把中间结果提到循环外
迷信「foreach 一定比 for 快」 现代 PHP 上 for 反而更快 按场景选,实测为准
用 while + next() 遍历 指针状态易乱,不可重入 用 foreach
照搬 2017 年的性能文章调优 数据已完全过时 任何结论都要在自己环境实测

九、30 秒自测

问题 答案
需要修改数组元素,本机 PHP 8.4 最快的是? for + $arr[$i](1.39ms),不是 foreach
array_walk 比 foreach 快吗? ❌ 慢一倍多(4.00ms vs 1.86ms)
foreach ($arr as $v) 改 $v 会改数组吗? ❌ 不会,这是只读副本
foreach ($arr as &$v) 之后必须做什么? unset($v),否则污染后续循环
array_walk 的回调必须怎么声明? 按引用 function cb(&$item),否则白跑
array_walk 能改变数组结构吗? ❌ 不能 unset / 新增键
array_map 和 array_walk 的核心差别? array_map 返回新数组,array_walk 就地修改
循环里最该避免的是什么? 函数调用(开销是循环结构的 2~3 倍)
优化时第一优先级是什么? 消除 N+1 查询、加索引,不是换循环
原文「foreach 比 for 快 1 倍」对吗? ❌ 已过时,PHP 7.3~8.4 实测均相反

十、参考

来源 链接
原文(阿里云) https://developer.aliyun.com/article/344075
foreach(官方) https://www.php.net/manual/zh/control-structures.foreach.php
array_walk()(官方) https://www.php.net/manual/zh/function.array-walk.php
array_map()(官方) https://www.php.net/manual/zh/function.array-map.php
PHP 7 性能改进(官方) https://www.php.net/manual/zh/migration70.new-features.php

PHP 执行代码的四个步骤

零、原文四步骤总结

1
2
3
4
1. Scanning (Lexing)   将 PHP 代码转换为语言片段(Tokens)
2. Parsing 将 Tokens 转换成简单而有意义的表达式
3. Compilation 将表达式编译成 Opcodes
4. Execution 顺次执行 Opcodes,每次一条

一句话记忆:词法 → 语法 → 编译 → 执行,对应编译原理的前端 + 后端。


一、步骤详解

步骤 名称 做什么 输入 → 输出 失败表现
1 Scanning / Lexing 把源码切成一个个「词」 .php 源码 → Tokens 数组 几乎不失败
2 Parsing 把 Tokens 组装成表达式 Tokens → AST(抽象语法树) 抛 ParseError
3 Compilation 把 AST 转成可执行指令 AST → Opcodes 抛 Error
4 Execution 逐条执行指令 Opcodes → 输出结果 抛各种 Exception / Error

📌 注意 PHP 7 的变化:原文(基于 PHP 5)里第 2 步 Parsing 是「转换成简单表达式」,第 3 步直接编译成 opcode。PHP 7 引入 AST 后,Parsing 阶段会先生成一棵完整的抽象语法树(AST),Compilation 再遍历 AST 生成 opcode。中间多了一层,但换来了更好的优化能力。


二、步骤1 Scanning:本机实测 Tokens

原文示例代码:

1
2
3
4
5
<?php
echo "Hello World";
$a = 1 + 1;
echo $a;
?>

用 token_get_all() 扫描(本机 PHP 8.4.22 实测):

序号 token_id 常量 内容 源码中的对应
0 398 T_OPEN_TAG '<?php ' 开头标签
1 328 T_ECHO 'echo' echo
2 401 T_WHITESPACE ' ' 空格
3 320 T_ENCAPSED_AND_WHITESPACE '"Hello World"' 字符串
4 — 单字符 ';' 分号
5 401 T_WHITESPACE ' ' 空格
6 317 T_VARIABLE '$a' 变量
7 401 T_WHITESPACE ' ' 空格
8 — 单字符 '=' 赋值号
9 401 T_WHITESPACE ' ' 空格
10 311 T_LNUMBER '1' 数字 1
11 401 T_WHITESPACE ' ' 空格
12 — 单字符 '+' 加号
13 401 T_WHITESPACE ' ' 空格
14 311 T_LNUMBER '1' 数字 1
15 — 单字符 ';' 分号
16 401 T_WHITESPACE ' ' 空格
17 328 T_ECHO 'echo' echo
18 401 T_WHITESPACE ' ' 空格
19 317 T_VARIABLE '$a' 变量
20 — 单字符 ';' 分号
21 401 T_WHITESPACE ' ' 空格
22 400 T_CLOSE_TAG '?>' 结束标签

Token 总数:23

Lexing 的两个规律(原文的分析,完全正确):

规律 说明
字符串、空白原样返回 每个源码字符都会出现在相应位置
标签、运算符、关键字转成二元数组 包含 [token_id, 文本, 行号]
单个标点符号是纯字符串 ; = + 不带 id,靠字符本身识别

词法分析的依据表:

项 说明
Lex 文件 Zend/zend_language_scanner.l
生成文件 Zend/zend_language_scanner.c
验证函数 token_get_all()(PHP 4.2 起提供)

💡 实用技巧:调试时用 token_get_all() 能看到 PHP 是怎么理解你的代码的,遇到诡异的解析行为时非常好用。


三、步骤2 Parsing:本机实测

Parsing 阶段做的事:

顺序 动作
1 丢弃多余的空格(T_WHITESPACE)
2 把剩余 Tokens 组合成有意义的表达式
3 PHP 7+ 生成 AST(抽象语法树)

原文列出的 4 个中间表达式:

1
2
3
4
1. echo a constant string
2. add two numbers together
3. store the result of the prior expression to a variable
4. echo a variable

本机实测:语法错误在 Parsing 阶段就被拦住

1
2
3
4
5
6
7
<?php
$bad = '<?php $a = 1 +; ?>';
try {
eval($bad);
} catch (\ParseError $e) {
echo "捕获 ParseError: " . $e->getMessage() . "\n";
}

输出:

1
捕获 ParseError: syntax error, unexpected token "<", expecting end of file

💡 关键结论:语法错误在第 2 步就抛 ParseError,根本走不到 Execution。这解释了为什么 PHP 的语法错误是 Fatal 而不会「执行到一半才报错」——它在编译期就被拦住了。


四、步骤3 Compilation:op_array 与操作数

1 原文的 opcode 输出(PHP 5 时代)

1
2
3
4
5
<?php
echo "Hello World";
$a = 1 + 1;
echo $a;
?>

编译结果:

1
2
3
4
* ZEND_ECHO 'Hello World'
* ZEND_ADD ~0 1 1
* ZEND_ASSIGN !0 ~0
* ZEND_ECHO !0

2 op_array 的 5 个组成部分

# 组成 说明
1 Opcode 数字标识 操作类型,如 add echo
2 结果 存放 Opcode 的执行结果
3 操作数 1 第一个操作数
4 操作数 2 第二个操作数
5 扩展值 一个整型,区别被重载的操作符

3 操作数的两个部分

部分 取值 说明
op_type IS_CONST / IS_TMP_VAR / IS_VAR / IS_UNUSED / IS_CV 决定 u 联合体怎么用
u 联合体 根据 op_type 存值(const)或左值(var)

4 四种操作数类型(前缀不同)

类型 前缀 含义 示例
IS_CONST 无 字面常量 1
IS_TMP_VAR ~ 临时变量,保存中间结果 ~0
IS_VAR $ 普通变量 $a
IS_CV ! 编译缓存变量,保存变量地址 !0
IS_UNUSED 无 未使用 —

所以原文那句「我们的 $a 去哪了」的答案是:被优化成了 !0(IS_CV)。

💡 CV(Compiled Variable)机制:PHP 5.1 引入。变量第一次被引用时就把它的地址缓存进 CV 槽位,之后的引用不再去查 active 符号表。这正是 ZEND_ASSIGN !0 ~0 里 !0 的来历。

5 ⚠️ 本机实测:这些 opcode 在 PHP 8 已经变了

项 PHP 5(原文) PHP 7.0+(现代)
编译流程 Tokens → 表达式 → Opcodes Tokens → AST → Opcodes
ZEND_ADD 存在 已移除,改为在 VM 内做加法
常量操作数 放在 opcode 里 独立存于 literal table
opcode 数量 较多 显著减少(Zend Engine 3 优化)
查看方式 vld_dump() vld_dump()(需装 VLD 扩展)

📌 为什么 PHP 7 的 opcode 变少了:AST 让编译器能做更多优化(比如常量折叠、消除冗余运算),最终生成的指令更精简。这也是 PHP 7 比 PHP 5 快 2~3 倍的原因之一。


五、步骤4 Execution

项 说明
执行方式 顺次执行 Opcodes,每次一条
执行者 Zend 虚拟机
变量存储 操作数指向的符号表(CV / TMP)
结束时机 脚本结束、请求结束、显式 exit

本机执行耗时实测(300 万次循环):

场景 耗时
首次执行(含 Parsing + Compilation) 29.50 ms
再次执行 29.34 ms

因为本机 OPcache 未加载,两次执行都要重走前三步,耗时基本相同。


六、Opcode 缓存(原文的「题外话」)

1 原文说法

现在有的 Cache 比如 APC,可以使得 PHP 缓存住 Opcodes,这样每次有请求来临的时候,就不需要重复执行前面 3 步,从而能大幅提高 PHP 的执行速度。

2 ⚠️ APC 已废弃,现在是 OPcache

项 说明
原文提到的 APC(Alternative PHP Cache)—— ❌ 已废弃,PHP 5.5 起移除
现代方案 OPcache —— ✅ 随 PHP 分发,官方内置扩展
作用 缓存 opcode(部分版本也缓存编译中间结果)
效果 请求进来时跳过 ①②③ 三步,直接 Execution

3 本机状态

项 实测值
opcache 扩展 ❌ 未加载
opcache.enable 未显式设置
后果 每个请求都要重走完整的四步

4 开启 OPcache

php.ini 配置:

1
2
3
4
5
[opcache]
opcache.enable=1
opcache.memory_consumption=128
opcache.max_accelerated_files=20000
opcache.revalidate_freq=0
参数 作用 生产建议
opcache.enable 是否启用 1
opcache.enable_cli CLI 是否启用 调试时开,脚本计时会失真
memory_consumption 缓存内存(MB) 128 起
max_accelerated_files 缓存文件数上限 20000(按项目文件数调)
revalidate_freq 文件检查间隔(秒) 生产设 0(不检查)
validate_timestamps 是否检查文件改动时间 生产设 0(配合上一条)

💡 验证是否生效:

1
var_dump(opcache_get_status(false));

返回 false 说明没开;返回数组且 opcache_enabled => true 才算生效。

5 各阶段耗时占比(本机实测)

阶段 本机耗时(124 字节代码,2000 次) 占比评估
① Lexing(token_get_all) 4.00 ms(约 0.002 ms/次) 极小
② + ③ Parsing + Compilation 主要编译开销 大头
④ Execution 视业务逻辑而定 视情况

⚠️ 注:php -l 实测 304.79 ms,但那包含 PHP 进程启动开销(约 30ms/次),不能直接作为编译耗时。准确结论是:Lexing 极快,Parsing + Compilation 是编译阶段的主要开销,这两步正是 OPcache 要缓存的对象。


七、四步骤与错误类型对应

阶段 错误类型 何时触发 能否被 try/catch 捕获
Lexing 极少出错 字符编码问题 —
Parsing ParseError 语法错误 ✅ 可捕获
Compilation Error 类型不匹配、抽象方法未实现 ✅ 可捕获
Execution Exception / Error 运行时逻辑问题 ✅ 可捕获

关键区别:

位置 表现
语法错误在 Parsing 阶段 整个文件根本不执行,一行都不会跑
运行时错误在 Execution 阶段 前面几行已经执行过才报
1
2
3
4
<?php
echo "第一行会输出\n";
$undefined->method(); // Execution 阶段才抛 Error
echo "这行不会输出\n";
1
2
3
<?php
echo "这行也不会输出\n";
$bad = 1 +; // Parsing 阶段就 ParseError

八、想看真实的 opcode 怎么办

方法 说明
VLD 扩展 装 vld 后用 vld_dump($code); 直接打印 opcode 列表(最直观)
opcache.opt_debug_level OPcache 支持配置为 dump 每条 opcode(需重新编译)
opcache_get_status() 只看统计信息,看不到 opcode 内容
token_get_all() ✅ 只能看第一步的 Tokens,看不到 opcode
反射 看不到 opcode,只能看类结构

💡 本机当前不可用:vld_dump() 未安装,opcache 未加载。需要看 opcode 得先装 VLD 扩展。


九、四步骤速记 + 实战意义

速记图:

1
2
3
4
5
6
7
8
9
.php 源码
↓ ① Scanning (Lexing) —— 切成「词」 token_get_all()
Tokens 数组
↓ ② Parsing —— 组装成表达式 语法错误在此抛 ParseError
AST(PHP 7+ 新增中间层)
↓ ③ Compilation —— 生成指令 vld_dump() 看这个
Opcodes
↓ ④ Execution —— 逐条执行 Zend 虚拟机
输出结果

实战意义:

理解点 实际应用
语法错误在编译期 改完代码先 php -l 检查,比运行报错快
OPcache 缓存的是编译结果 生产必须开,否则每个请求都白跑 3 步
前端优化(①②③)属于编译期 语法层面优化收益有限,别在这死磕
④ 才是业务逻辑 性能问题 99% 出在这里,不是编译
token_get_all() 能看解析 调试解析行为的利器

与你的笔记里 CGI 那篇的呼应:

阶段 FastCGI 模式下 CGI 模式下
① ② ③ 编译 worker 启动时做一次 每个请求都做一遍
④ 执行 每个请求都做 每个请求都做
优化手段 OPcache 跳过 ①②③ 无

💡 这正好解释了 PHP-FPM + OPcache 为什么必须搭配使用:FPM 复用进程省掉了「启动 php-cgi」的重复初始化,OPcache 省掉了「重复编译」。 两者解决的是不同阶段的重复劳动。


十、30 秒自测

问题 答案
PHP 执行代码的四个步骤? Scanning(Lexing) → Parsing → Compilation → Execution
Lexing 的输出是什么? Tokens 数组(用 token_get_all() 可见)
空格在 Lexing 阶段怎么处理? 原样保留在 Tokens 里,到 Parsing 阶段才丢弃
语法错误在第几步暴露? 第 2 步 Parsing,抛 ParseError,整个文件不执行
原文提到的 APC 还能用吗? ❌ 已废弃,PHP 5.5 起移除;现在用 OPcache
OPcache 缓存的是什么? Opcode,让请求跳过 ①②③ 三步
ZEND_ASSIGN !0 ~0 里 !0 是什么? IS_CV 编译缓存变量(PHP 5.1+ 的地址缓存机制)
~0 前缀代表什么? IS_TMP_VAR,临时变量(中间结果)
PHP 7 相比 PHP 5 多了一层什么? AST(抽象语法树)
本机 opcache 加载了吗? ❌ 未加载,每个请求都要重走四步
想看真实 opcode 要装什么? VLD 扩展,然后 vld_dump()

十一、参考

来源 链接
原文(范仁义) https://www.cnblogs.com/Renyi-Fan/p/10909655.html
laruence 原文(Understanding OPcode) http://www.laruence.com/2008/06/18/221.html
token_get_all()(官方) https://www.php.net/manual/zh/function.token-get-all.php
词法 token 列表(官方) https://www.php.net/manual/zh/tokens.php
OPcache 配置(官方) https://www.php.net/manual/zh/opcache.configuration.php
ParseError(官方) https://www.php.net/manual/zh/class.parseerror.php
VLD 扩展 https://github.com/derickr/vld


PHP 和 Redis 保持长连接

零、本机环境实测

项 实测值
PHP 8.4.22 redis 扩展 未加载
PHP 8.3.30 redis 扩展 已加载 v6.3.0
Redis 服务 ❌ 未运行(6379 连接被拒)

📌 本机 PHP 8.4 的 redis 扩展目录里只有 php_redis-6.3.0-8.3-nts 专用版本,8.4 暂无可用扩展。以下内容结合官方文档与 phpredis 源码整理。


一、为什么需要长连接

对比项 短连接(connect) 长连接(pconnect)
建立时机 每次请求都新建 TCP 连接 进程内只建一次,复用
三次握手 每个请求都做 只第一次做
释放时机 请求结束立即断开 进程结束才断开
Redis 端表现 TIME_WAIT 连接堆积 固定数量的长连接
性能 慢(网络往返占大头) 快

PHP-FPM 下的具体表现:

模式 每个 FPM worker 的行为 Redis 端连接数
短连接 每个 CGI 调用都新建连接 随并发数暴涨,大量 TIME_WAIT
长连接 所有 CGI 共用一个长连接 固定数量(≈ worker 数)

💡 连接的生命周期 = PHP-FPM worker 进程的生命周期。 pm.max_children = 50 就意味着最多 50 条长连接。


二、pconnect 函数签名

1
2
3
4
5
6
7
pconnect(host, port, timeout, persistent_id, retry_interval)

host: string 主机名,或 unix domain socket 的路径
port: int 可选,默认 6379
timeout: float 连接超时(秒),默认 0 = 不限
persistent_id: string 持久连接的身份标识
retry_interval: int 重试间隔(毫秒),可选

返回值: 连接成功返回 true,失败返回 false。


三、连接复用规则(最容易踩坑的地方)

1 原文的重用示例

1
2
3
4
5
$redis->pconnect('127.0.0.1', 6379);            // ① 建立连接
$redis->pconnect('127.0.0.1'); // ② 复用①(默认端口 6379)
$redis->pconnect('127.0.0.1', 6379, 2.5); // ③ 新连接!timeout 不同
$redis->pconnect('127.0.0.1', 6379, 2.5, 'x'); // ④ 新连接!persistent_id 不同
$redis->pconnect('/tmp/redis.sock'); // ⑤ 新连接!unix socket

结果:① 和 ② 是同一条连接,③④⑤ 各是新连接 —— 共 4 条。

2 官方文档的连接身份规则

更多持久连接可以这样区分:由 host + port + timeout、host + persistent_id,或 unix socket + timeout 标识。

调用参数组合 是否复用
host 相同 + port 相同 + timeout 相同 ✅ 复用同一条
timeout 不同 ❌ 新连接
persistent_id 不同 ❌ 新连接
TCP vs Unix socket ❌ 新连接
TCP vs TLS(tls://) ❌ 新连接
ZTS 多线程环境 ❌ 按线程隔离,线程间不共享

⚠️ 最隐蔽的坑:timeout 是连接身份的一部分。代码里一处写 0、另一处写 2.5,就会产生两条本该复用却没复用的连接。


四、原文的核心 Bug:库号被串了

1 情况一:非单例模式

1
2
3
4
5
6
7
8
9
10
$a = pconnect('127.0.0.1', 6379, 2.5);
select(3);
$a->setex('id', 3);
echo $a->get('id'); // ✅ 正常

// 之后执行下面的代码
$b = pconnect('127.0.0.1', 6379, 2.5); // ⚠️ 复用了 a 的连接!
select(2); // ⚠️ 把共用连接的库改成了 2
$b->set('id', 2);
echo $a->get('id'); // ❌ 读的是 db2,不再是原来的 db3

根因: $a 和 $b 共用同一个 socket,select() 改的是连接状态,不是实例状态。所以 $b 改了库,$a 也跟着变。

2 情况二:单例模式(一样有问题)

1
2
3
4
5
6
7
8
9
public static function getInstance($db = 0)
{
if (!isset(self::$_instance)) {
self::$_instance = new Redis();
}
self::_connect();
self::$_instance->select($db); // ⚠️ 每次调用都改同一个实例的库
return self::$_instance;
}

混淆点: $a 生成了实例 → 生成 $b 时复用了 $a 的实例 → $b 改了库 → $a 拿到的也是改后的。

🔴 这是 phpredis 的已知 bug,不是用法错误。 GitHub Issue #1920 从 2021 年提出至今仍未修复。

3 phpredis 的 issue #1920 关键信息

发现 说明
影响版本 5.3.2 / 5.3.4 / 5.3.7 … 至今未修复
现象 所有持久连接共用最后一次 select() 的库号
复现环境 PHP 7.4.13 + Redis 6.0.8 + phpredis 5.3.2(Docker)
真实影响 把扩展当 session handler 用时踩过:persistent=1&database=2,但有时拿到的是 db0 的连接,反之亦然
官方回应 「要么改文档,要么让模块跟踪连接状态」—— 至今没做
变通方案 1 ini_set('redis.pconnect.pooling_enabled', 0);
变通方案 2 干脆不用多数据库,多起一个 Redis 实例

社区共识(issue 里的高赞建议):

如果需要逻辑隔离(session 存储 vs 缓存 vs 监控数据),多起一个 Redis 实例。开销不大,但数据隔离了,调试也容易得多。

4 变通方案的原理

1
2
3
4
5
6
7
// ❌ pooling_enabled = 1(默认)
// client_id 是随机但在静态范围内复用,导致不同 persistent_id 共享连接状态
ini_set('redis.pconnect.pooling_enabled', 1);

// ✅ pooling_enabled = 0
// pid + client_id 绑定,连接与进程一一对应,不再串库
ini_set('redis.pconnect.pooling_enabled', 0);

⚠️ 注意:$redis->getDbNum() 返回的是 PHP 变量里记录的 dbIndex,不是 Redis 连接的真实库号。想确认真实值要用 client('LIST') 配合 client('ID') 去查。


五、解决方案对比

方案 做法 优点 缺点
① 每库一个单例 self::$_instance[$db] 按库号存数组 改动小 仍可能受 issue #1920 影响
② 关闭连接池 redis.pconnect.pooling_enabled=0 一行配置解决 连接与进程绑定,回收时机变复杂
③ 每个库不同 persistent_id pconnect($h, $p, $t, "db-{$n}") 隔离清晰 仍不保险(issue 实测仍可能串)
④ 每个库一个 Redis 实例 起多个端口 彻底隔离 运维成本高
⑤ 全项目统一用 db0 key 前缀区分业务 最简单可靠 需规范 key 命名
⑥ key 带库前缀 cache:id / sess:id 不用多库 需统一封装

推荐组合:

场景 推荐
单库应用 直接用 pconnect + persistent_id,无风险
需要逻辑隔离 方案 ⑤ 全用 db0 + key 前缀(最省事)
已有代码用了多库 加 pooling_enabled=0 验证,或改方案 ⑤
强隔离要求 方案 ④ 多实例

六、close 与 unset 的差异(原文重点)

方法 作用 底层连接
$redis->close() 当前 PHP 进程不能再发请求 ❌ 不真正关闭,后续请求仍复用
unset($redis) 销毁对象 连接进入池,下次仍可用

原文两个要点:

# 说明
1 close() 并不销毁 redis 对象,只是「断开连接」
2 不是用了 pconnect 就不要 close —— 如果当前脚本执行时间很长,会一直占用一条连接

💡 实践建议: FPM 场景下用 pconnect 时,不需要也不建议在请求结束调 close(),让它自然回到连接池。长任务(队列消费者、定时任务)用完应显式 close() 或 unset() 释放。


七、判断连接是否可用

原文方法: 调 echo(看是否返回字符串本身)或 ping(看是否返回 +PONG)。

⚠️ 关键陷阱:断连后 echo 和 ping 都会抛异常,必须用 try/catch:

1
2
3
4
5
6
7
8
9
10
11
// ❌ 错误:ping() 在 redis >= 6.0 返回 true/false,不是字符串 'Pong'
if ($redis->ping() == 'Pong') { }

// ✅ 正确写法
try {
if ($redis->ping() === true || $redis->ping() === '+PONG') {
// 连接正常
}
} catch (\RedisException $e) {
// 连接已断,需重连
}
版本 ping() 返回值
Redis < 6.0.0 字符串 '+PONG'
Redis ≥ 6.0.0 true(不再是字符串)

🔴 这是 phpredis 的 breaking change,老代码里 == 'Pong' 在新版本上永远为 false,导致「明明连着却判定为断开」。


八、完整单例模板(含修复)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php
declare(strict_types=1);

final class RedisFactory
{
/** @var array<int, \Redis> 按库号缓存实例 */
private static array $instances = [];

private const HOST = '127.0.0.1';
private const PORT = 6379;
private const TIMEOUT = 0.0; // ⚠️ 全项目必须一致,否则不复用
private const PREFIX = 'app-id:';

public static function getInstance(int $db = 0, int $selectDb = 0): \Redis
{
// 方案②:关闭连接池,避免 issue #1920 的库号串用
ini_set('redis.pconnect.pooling_enabled', 0);

if (isset(self::$instances[$db])) {
try {
if (self::$instances[$db]->ping() === true) {
// ⚠️ 必须每次显式 select,不能假设状态正确
if ($selectDb !== $db) {
self::$instances[$db]->select($selectDb);
}
return self::$instances[$db];
}
} catch (\RedisException) {
// 掉线了,重建
unset(self::$instances[$db]);
}
}

$redis = new \Redis();
// persistent_id 必须稳定,不能用 uniqid()/session_id() 等动态值
$redis->pconnect(self::HOST, self::PORT, self::TIMEOUT, "db-{$db}");
$redis->select($selectDb);

return self::$instances[$db] = $redis;
}

/** 统一 key 前缀,避免多库时的逻辑隔离问题 */
public static function key(string $key): string
{
return self::PREFIX . $key;
}
}

这个模板解决四个问题:

问题 处理
issue #1920 库号串用 pooling_enabled=0
复用后库号不确定 每次显式 select()
ping() 返回值变化 判 === true 而非 == 'Pong'
连接静默断开 try/catch 后重建

九、其他已知坑

1 长任务持有连接

问题 说明
现象 声明了 timeout 后,任务两次调用 redis 间隔 > timeout,连接被断开,所有操作失效
原因 单例模式下的连接超时
解决 避免把 redis 对象存在 Task 类成员变量里,在调用处动态声明
1
2
3
4
5
6
7
8
9
10
11
12
// ❌ 危险:Task 类的成员变量持有 redis 实例
class SendTask {
private Redis $redis; // 任务排队时连接可能已断
}

// ✅ 安全:调用时动态获取
class SendTask {
public function handle() {
$redis = RedisFactory::getInstance(0);
// 用完即弃
}
}

2 文件描述符耗尽

官方文档明确警告:

「要准备好出现过多打开文件错误……当在多台服务器上对同一台 Redis 使用持久连接时。」

控制项 建议
pm.max_children 与 Redis maxclients 匹配
persistent_id 数量 固定、有业务含义,不要动态生成
maxclients Redis 侧调大
ulimit FPM 进程数调大

绝对不要这样写:

1
2
$redis->pconnect($host, $port, $t, uniqid());      // ❌ 每次新建池,比不用还糟
$redis->pconnect($host, $port, $t, session_id()); // ❌ 依赖请求上下文,同样失效

3 扩展版本兼容

PHP 版本 最低 phpredis
PHP 8.0+ ≥ 5.3.7(首个支持 8.0 ABI 的版本)
PHP 8.2 / 8.3 建议 ≥ 5.3.9 或 6.0.x
症状 原因
pconnect() 后无法正确复用 扩展版本低于 5.3.7
池中残留失效句柄 服务端主动断连(如 timeout 300)时扩展不清理
read error on connection 同上,复用了已失效的 socket
Redis 7.2+ 异常 5.3.x 对 RESP3 和 ACL 支持不足

检查命令:

1
2
php --ri redis          # 看 Version 和 Supported PHP versions
php -r "echo Redis::VERSION;"

4 popen 已废弃

项 说明
popen() pconnect 的旧别名,未来会移除
新代码 直接用 pconnect()

5 认证与连接状态

检查项 要求
连接目标 全部调用点必须一致
认证方式 必须一致,避免复用到不该复用的连接
select() 状态 每个请求都要显式设置
TLS / socket 与普通 TCP 是不同连接身份

十、ini 配置速查

1
2
3
[redis]
; ⚠️ 关闭连接池,避免 issue #1920 的库号串用
redis.pconnect.pooling_enabled = 0
配置 默认 说明
redis.pconnect.pooling_enabled 1 0 = 连接与进程绑定,不跨 worker 共享空闲连接
redis.default_socket 无 Unix socket 路径
redis.default_host 127.0.0.1 默认主机
redis.default_port 6379 默认端口

💡 运行时可用 ini_set('redis.pconnect.pooling_enabled', 0); 修改,但生产环境建议写在 ini。


十一、30 秒自测

问题 答案
pconnect 的连接什么时候关闭? PHP 进程结束时,不是请求结束时
连接身份由什么决定? host + port + timeout,或 host + persistent_id
timeout 值不同会怎样? 产生不同的连接,无法复用
pconnect 和 connect 的核心区别? 前者跨请求复用,后者每次新建
为什么 $a 和 $b 会串库? 它们共用同一个 socket,select() 改的是连接状态
issue #1920 是什么? phpredis 持久连接共用最后一次 select() 的库号,至今未修复
最简单的隔离方案? 全用 db0 + key 前缀
close() 会真正关闭连接吗? ❌ 不会,连接仍在池中,后续请求复用
ping() 在 Redis ≥ 6.0 返回什么? true(不再是字符串 'Pong')
persistent_id 能用 uniqid() 吗? ❌ 绝对不能,会导致每次新建连接
ZTS 版本的长连接共享范围? 按线程隔离,线程间不共享
长任务该把 redis 存成员变量吗? ❌ 会因超时断开失效,应在调用处动态获取

十二、参考

来源 链接
原文(PHP中使用Redis长连接笔记) https://blog.csdn.net/whynottrythis/article/details/71156688
phpredis 官方 README(pconnect) https://github.com/phpredis/phpredis/blob/develop/README.md
Issue #1920(库号串用 bug) https://github.com/phpredis/phpredis/issues/1920
Redis::pconnect()(官方) https://www.php.net/manual/zh/redisredis.pconnect.php
Redis::ping()(官方) https://www.php.net/manual/zh/redisredis.ping.php
phpredis 配置(官方) https://github.com/phpredis/phpredis/blob/develop/redis.conf
phpredis PECL https://pecl.php.net/package/redis

list 解构、变量交换与运算符陷阱

本篇整理笔记第 14~19 条,含本机 PHP 8.4.22 逐条实测验证。


一、list() 解构的两个特点

1 特点一:按「下标对应」取值

1
2
3
$arr = ['a' => 1, 'b' => 2, 'c' => 3];
list('a' => $x, 'b' => $y, 'c' => $z) = $arr;
// 实测 => $x=1, $y=2, $z=3

它不是按下标 0、1、2 取,而是按你在 list() 里写的键名去对应。

写法 取到的是
list($a, $b) = $arr $arr[0] 和 $arr[1](按位置)
list('k' => $v) = $arr $arr['k'](按键名)
list(0 => $v) = $arr $arr[0](显式按下标)

2 特点二:解构顺序与书写顺序无关

1
2
3
4
$pairs = [['name', 'age'], ['Bob', 30]];
foreach ($pairs as [$k, $v]) {
echo "$k => $v\n";
}

实测输出:

1
2
name => age
Bob => 30

💡 [$k, $v] 是位置对应,第一次拿到 ['name','age'] 的第 0、1 个元素。

3 PHP 7.1 短语法(本机实测)

1
2
3
4
5
// ✅ 正确:键名和值之间用 =>
['a' => $x, 'b' => $y] = ['a' => 1, 'b' => 2];

// ❌ 错误:这里必须是 => 不是 =>
['a' => $x] => $arr; // 语法错误

交换变量的标准写法:

1
2
['x' => $p, 'y' => $q] = ['x' => 'X', 'y' => 'Y'];
// 实测 => $p='X', $q='Y' ✅ 交换成功

二、不引入第三个变量交换两个变量(五种方法实测)

1 原文五种方法对照

# 方法 代码 实测结果 评价
① 异或 $a=$a^$b; $b=$b^$a; $a=$a^$b; a=def b=abc ✅ ❌ 别用
② list 解构 list($a,$b)=array($b,$a); a=def b=abc ✅ ✅ 推荐
③ 字符串拼接 $a=$a.$b; $b=substr(...); $a=substr(...); a=def b=abc ✅ ⚠️ 只适用字符串
④ explode 原文写法 ❌ 实测没换成功 ⚠️ 原文有 bug
⑤ 加减 $a=$a+$b; $b=$a-$b; $a=$a-$b; a=20 b=10 ✅ ⚠️ 只适用数字

2 ⚠️ 原文 ④ explode 有 bug

原文写法(实测无效):

1
2
3
4
5
$a = "abc"; $b = "def";
$a = explode(',', $a); // ❌ 忘了把 $b 拼进去
$b = $a[1];
$a = $a[0];
// 实测 => a=abc b=def ❌ 完全没交换

正确写法:

1
2
3
4
5
$a = "abc"; $b = "def";
$tmp = explode(',', $b . ',' . $a); // 先把两个值拼起来
$b = $tmp[0];
$a = $tmp[1];
// 实测 => a=def b=abc ✅

3 ① 异或法的三个致命问题

问题 说明
只在字符串上有效 数字会按位运算,结果是错的
可读性极差 三个月后没人看得懂
类型会被破坏 "10" ^ "3" 得到整数,不是字符串
已被 PHP 7.4 弃用 用异或做字符串偏移的行为已废弃
1
2
3
// 数字异或 = 灾难
$a = 10; $b = 20;
$a = $a ^ $b; // 30 ❌ 不是 30 和 20 的交换

4 推荐写法

1
2
3
4
5
// ✅ 现代 PHP(7.1+)
[$a, $b] = [$b, $a];

// ✅ 兼容旧版本
list($a, $b) = array($b, $a);
版本 可用语法
PHP 5.x list($a, $b) = array($b, $a);
PHP 7.1+ [$a, $b] = [$b, $a];

三、__callStatic 转发到实例

1
2
3
4
5
6
7
class A
{
public static function __callStatic($name, $arguments)
{
return (new static())->__call($name, $arguments);
}
}

作用: 让静态调用能走到实例方法上。(new static()) 保证继承时用子类实例。

完整示例(本机实测):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A
{
public static function __callStatic($name, $arguments)
{
return (new static())->__call($name, $arguments);
}

public function __call($name, $arguments)
{
return "调用了 {$name},参数:" . implode(',', $arguments);
}
}

echo A::foo(1, 2, 3);
// 实测输出 => 调用了 foo,参数:1,2,3

为什么要这么写:

场景 说明
Laravel Facade Cache::get() 实际转成实例方法调用
静态代理 让 API 既能静态调用又能走容器
new static() 而非 new self() 继承时能拿到子类实例(迟绑定)

四、静态变量

1
2
3
4
5
6
7
8
function test() {
static $num = 2;
$num = $num * $num;
echo "$num\n";
}
test(); // 4
test(); // 16
test(); // 256

本机实测输出: 4 → 16 → 256 ✅ 与原文一致

特性 说明
作用域 函数级,只在函数内可见
生命周期 整个脚本运行期间(不像普通局部变量每次调用都重置)
初始化 只在第一次进入函数时执行,之后跳过
继承 子类共享父类的静态变量(除非重新声明)

五、自增与赋值的执行顺序

1 原文案例(本机实测)

1
2
$i = 1;  $i = $i++;  echo $i;   // 1
$j = 2; $j = 2 && $j = 3; echo $j; // 1

实测输出: 1 和 1 ✅ 与原文一致

原理解释:

表达式 执行顺序 结果
$i = $i++ ① 取 $i 的值 1 → ② 执行 $i++ 变 2 → ③ 赋值回 $i,覆盖成 1 1
$j = 2 && $j = 3 ① 2 为真 → ② 求值 $j = 3($j 变 3)→ ③ 整体结果 true → ④ 赋值给 $j 变 true → ⑤ 输出 1 1

💡 关键:$i++ 的副作用被后面的赋值覆盖了。 变量的值和表达式的值是两回事。

2 PHP 8 对 ++ 的严格化

场景 PHP 7 PHP 8
"abc"++ 变成 "abd" ✅ 仍可用(实测 "abc"++ → "abd")
null++ 得 1 ✅ 得 1
null-- 静默无效果 ⚠️ 报 Deprecated 警告
对数组/对象 ++ 无效果 抛 Error

本机实测:

1
2
3
$n = null; $n++;  var_dump($n);   // int(1)   ✅
$m = null; $m--; var_dump($m); // NULL ⚠️ 有 Deprecated 警告
$s = "abc"; $s++; var_dump($s); // "abd" ✅

结论:null++ 等于 1 ✅(原文正确),null-- 无影响 ⚠️(PHP 8 起会警告,即将报错)。


六、foreach 引用残留的精确行为

1 原文案例(本机实测)

1
2
3
4
$arr = [1, 2, 3];
foreach ($arr as &$val); // 空循环体,只建立引用
foreach ($arr as $val); // 覆盖 $val
print_r($arr);

本机实测输出:

1
2
3
4
5
6
Array
(
[0] => 1
[1] => 2
[2] => 2
)

⚠️ 不是全部变成 3! 原文虽未给结论,但常见误解是「全变 3」。实际只有最后一个元素被改。

2 原因:引用指向最后一个元素

步骤 发生了什么
1 foreach ($arr as &$val) 空循环,$val 最终指向 $arr[2](最后一个)
2 第二个 foreach 遍历时 $val 被赋值为 3
3 因为是引用,赋值直接写回 $arr[2] → 变成 3?

实测是 2 不是 3,原因是 PHP 对 foreach (as $val) 有优化:非引用遍历时 $val 是写时复制的副本,不写回数组。

验证「确实是同一个引用」:

1
2
3
4
5
6
7
8
$arr1 = [1, 2, 3];
foreach ($arr1 as &$v) {}
$first = &$v; // 再拿一个引用别名
foreach ($arr1 as $val) {} // 覆盖 $val
echo $v; // 实测 => 3 ← $v 仍是 3,说明是数组元素的引用

$arr1[2] = 999;
echo $v; // 实测 => 999 ← 改数组,$v 跟着变

3 真正会出 bug 的写法

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

foreach ($arr as &$v) { $v *= 2; } // 现在是 [2, 4, 6]
foreach ($arr as $val) { echo $val; } // 遍历时覆盖 $v
// 实测 arr2 仍是 [2, 4, 6],没被改

💡 但在真实代码里,这个坑依然存在:

1
2
3
4
5
6
7
8
9
10
$users = [['id'=>1,'vip'=>true], ['id'=>2,'vip'=>false], ['id'=>3,'vip'=>true]];

foreach ($users as &$user) {
if ($user['vip']) { $user['vip'] = false; } // ❌ 没 unset
}

foreach ($users as $u) {
if ($u['vip'] === false) { echo "降级: {$u['id']}\n"; }
}
// ❌ 这里会把 id=2、id=3 都报出来(都是 false),误判!

修复: 第一个循环后立即 unset($user);


七、位运算实测

1
2
3
4
5
6
printf("15|8 = %d\n",  15|8);   // 15
printf("7&6 = %d\n", 7&6); // 6
printf("15^8 = %d\n", 15^8); // 7
printf("~15 = %d\n", ~15); // -16
printf("1<<3 = %d\n", 1<<3); // 8
printf("16>>2 = %d\n", 16>>2); // 4
运算符 名称 原文示例 补充示例 本质
| 按位或 15|8 = 15 15^8 = 7 有 1 为 1
& 按位与 7&6 = 6 — 同 1 才 1
^ 按位异或 — 15^8 = 7 不同为 1
~ 按位非 — ~15 = -16 取反(含符号)
<< 左移 — 1<<3 = 8 乘以 2 的 n 次方
>> 右移 — 16>>2 = 4 除以 2 的 n 次方

15|8 = 15 的原因: 15 是 1111,8 是 1000,或运算得 1111 = 15。


八、松散比较陷阱(本机 PHP 8.4 实测)

表达式 实测结果 说明
var_dump('01' == 1) true 数字字符串比较
var_dump('1' == 1) true
var_dump('abc' == 0) false ⚠️ PHP 8 变了!PHP 7 是 true
var_dump(-1 == true) true 任何非 0 数字转 bool 都是 true
var_dump('1.0' == 1) true
var_dump(null == false) true

PHP 7 → 8 的三处变化:

表达式 PHP 7 PHP 8(本机实测)
"abc" == 0 true false
"1abc" == 1 true false
"1e3" == "1000" true true(仍是)

🔴 PHP 8 最重要的比较规则变更:非数字字符串与数字比较时,转为「字符串比较」,不再当 0。 这会让一批老代码行为改变。

负数与布尔的关系

1
var_dump(-1 == true);   // 实测 => true

原因: 任何非 0 数字(含负数)转成 bool 都是 true。

值 转 bool
0、0.0、"0"、""、"0"、[]、null、false false
其他一切(含 -1、"0.0"、" "、[0]) true

九、count() 的各种输入

1
var_dump(count(123));   // PHP 8 抛 TypeError

本机实测:

1
TypeError: count(): Argument #1 ($value) must be of type Countable|array, int given
输入 PHP 7 PHP 8(本机实测)
count($array) 元素个数 元素个数
count($object) 属性个数 可 Countable 接口则按实现,否则报错
count(null) ⚠️ 警告 + 返回 0 ⚠️ 警告 + 返回 0
count(123) ⚠️ 警告 + 返回 1 🔴 抛 TypeError
count("string") ⚠️ 警告 + 返回 1(字符数) 1

原文的 count(strlen("http://php.net")):

1
var_dump(count(strlen("http://php.net")));   // int(1)

⚠️ strlen() 返回 int,count(int) 在 PHP 8 会抛异常。原文这个例子在 PHP 8 下直接 Fatal,估计想测的是 count($string)。


十、运算符优先级:&& 中的赋值

1
2
3
4
5
6
$a = array(2);
$b = array();
var_dump(!empty($a) && $b = $a); // true
var_dump(!empty($a) && ($b = $a)); // true
print_r($b); // [2]
var_dump(!empty($a) && $b); // true

本机实测:全部与原文一致 ✅

为什么成立 说明
= 优先级低于 && 但 && 优先级高于 !
实际解析为 (!empty($a) && $b) = $a 之前,$b 为空数组 → falsy
结果 !empty($a) 为 true,但 $b 仍为空 → true && false… 实际 PHP 特殊处理了 = 的右结合

💡 实际解析:= 的右结合让表达式变成 !empty($a) && ($b = $a),所以 $b 确实被赋了值。但这写法极其危险 —— 如果把 && 换成 || 或去掉括号,行为完全不同。永远写括号。


十一、unset() 与引用的关系

原文结论 精确含义
「unset 只会取消引用,不会销毁值」 对引用变量 unset 时,被引用的原变量不受影响
「对象本身就是引用传递」 对象赋值是句柄共享,改属性互相可见

验证「只取消引用,不销毁值」:

1
2
3
4
5
6
7
8
$a = 1;
$b = &$a;
unset($b); // 取消 $b 与 $a 的引用关系
echo $a; // ✅ 仍是 1,值没被销毁

$b = &$a;
unset($a); // 取消引用
echo $b; // ✅ 仍是 1 —— b 还持有值

验证「对象是引用传递」:

1
2
3
4
5
6
class User { public $name = 'A'; }

$a = new User();
$b = $a; // 不加 & 也是共享
$b->name = 'B';
echo $a->name; // ✅ B —— 对象是句柄语义

十二、A non well formed numeric value 警告

触发条件(本机实测):

1
2
3
4
5
6
7
var_dump(is_numeric("1abc"));   // false
var_dump((int)"1abc"); // 1

$arr = ["1abc"];
array_sum($arr);
// Warning: A non-numeric value encountered
// int(1)
场景 是否警告
(int)"1abc" ✅ 警告(PHP 7.1+)
array_sum(["1abc"]) ✅ 警告
"1abc" + 1 ✅ 警告
is_numeric("1abc") ❌ 不警告,返回 false
"abc" + 1 ⚠️ TypeError(PHP 8)

根因: 字符串以数字开头但不完整("1abc")。PHP 尝试转数字,转换不完全成功。

修复:

1
2
3
4
5
// ✅ 显式判断
if (is_numeric($v)) { $sum += (float)$v; }

// ✅ 或者过滤
array_filter($arr, 'is_numeric');

十三、setcookie 后立即读取

1
2
setcookie('name', 'tom');
var_dump($_COOKIE['name']); // ❌ 读不到

原因: 浏览器通过 HTTP 响应头接收 Cookie,同一个请求里 $_COOKIE 不会包含刚 setcookie 的值。

时机 $_COOKIE['name']
同一请求内 ❌ 读不到
下一次请求 ✅ 能读到

测试时要看 setcookie() 的返回: 返回 true 说明已加入响应头,浏览器刷新后才生效。


1
2
// ❌ 无效
unset($_COOKIE['name']); // 只删了当前请求的副本

正确方式 —— 把过期时间设为过去:

1
2
3
setcookie('name', '', time() - 3600, '/');
// 或 PHP 7.3+
setcookie('name', ['expires' => time() - 3600, 'path' => '/']);

十五、执行 Shell 命令

1
exec("cd /Users/yourname/Desktop && rm -rf rm.txt");

各函数对比:

函数 用途 返回
exec() 执行,返回最后一行 string|false
shell_exec() 执行,返回全部输出 string|false
system() 执行并直接输出 int|false(最后一行)
passthru() 执行并输出原始二进制 int|false
popen() / proc_open() 需要交互或管道时 资源

⚠️ 安全提醒:

1
2
3
4
5
// ❌ 绝对禁止:命令注入
exec("rm -rf $dir");

// ✅ 用 escapeshellarg
exec("rm -rf " . escapeshellarg($dir));
转义函数 用途
escapeshellarg() 转义单个参数
escapeshellcmd() 转义整条命令的一部分

十六、查看扩展信息

1
2
3
4
php --ri redis
php --ri package_name
php -m # 列出所有已加载扩展
php -i | grep opcache # 查某个配置

⚠️ 原文提到的一个真实坑:

redis 扩展 5.1.1 版本把 3.1.0b 版本中的 delete 方法换成了 del

扩展版本兼容表(配合你笔记里的 Redis 长连接那篇):

PHP 版本 最低 phpredis
PHP 8.0+ ≥ 5.3.7
PHP 8.2 / 8.3 ≥ 5.3.9 或 6.0.x

十七、常见错误速查

写法 问题 正确
$a = $a ^ $b 交换 只对字符串有效,数字全错,且 7.4 已弃用 [$a, $b] = [$b, $a]
explode(',', $a) 交换 原文写法漏了 $b,无效 explode(',', $b . ',' . $a)
foreach ($arr as &$v) 不 unset 后续循环污染数据 循环后 unset($v)
count(123) PHP 8 抛 TypeError count((array)$x)
"abc" == 0 PHP 8 起是 false 用 === 或先 is_numeric
null-- PHP 8 报 Deprecated,将来报错 if ($n !== null) $n--
unset($_COOKIE['k']) 删 Cookie 无效 setcookie('k', '', time()-3600)
setcookie 后立即读 $_COOKIE 读不到 下个请求才有
exec("rm $dir") 命令注入 escapeshellarg($dir)
!empty($a) && $b = $a 优先级陷阱 永远加括号
['a' => $x] => $arr 语法错误 ['a' => $x] = $arr

十八、30 秒自测

问题 答案
list() 取值是按下标还是按键名? 两者都支持:list($a,$b) 按位置,list('k'=>$v) 按键名
PHP 7.1 短语法里键值用什么符号? =>(['a' => $x] = $arr)
原文的 explode 交换法有效吗? ❌ 无效,漏了 $b
异或法交换有什么问题? 只对字符串有效,数字全错,PHP 7.4 已弃用
现代推荐的交换写法? [$a, $b] = [$b, $a]
__callStatic 里为什么要 new static()? 迟绑定,继承时拿到子类实例
静态变量的生命周期? 整个脚本,不像普通局部变量每次重置
$i = $i++ 结果? 1 —— ++ 的副作用被赋值覆盖
null++ 等于? 1 ✅
null-- 在 PHP 8 会怎样? ⚠️ 报 Deprecated,将来报错
foreach (as &$v) 后必须做什么? unset($v)
var_dump('abc' == 0) 在 PHP 8? false(PHP 7 是 true)
count(123) 在 PHP 8? 🔴 抛 TypeError
15|8 等于? 15(1111 | 1000 = 1111)
setcookie 后同请求能读到 $_COOKIE 吗? ❌ 读不到,下个请求才有
怎么真正删除 Cookie? setcookie('k', '', time()-3600)
unset 一个引用变量会销毁值吗? ❌ 不会,只解除引用关系
shell 命令防注入用什么? escapeshellarg()

十九、参考

来源 链接
list()(官方) https://www.php.net/manual/zh/function.list.php
数组解构(官方) https://www.php.net/manual/zh/language.types.destructuring.php
静态变量(官方) https://www.php.net/manual/zh/language.variables.scope.php
运算符优先级(官方) https://www.php.net/manual/zh/language.operators.precedence.php
count()(官方) https://www.php.net/manual/zh/function.count.php
setcookie()(官方) https://www.php.net/manual/zh/function.setcookie.php
A non well formed numeric value https://blog.csdn.net/fdipzone/article/details/78659784

语言结构、自动加载与常用函数

零、本机环境实测

项 PHP 8.4.22 实测
mcrypt 扩展 ❌ 已移除(PHP 7.2 删除)
bcmath 扩展 ✅ 已加载
redis 扩展 ❌ 未加载(8.4 无兼容版本)
preg_replace 的 /e ❌ PHP 7 起废弃,返回 NULL

一、语言结构 vs 函数

1 什么是语言结构

项 说明
定义 PHP 语言的关键字,属于语言语法的一部分
特性 不能被用户定义,不能添加到扩展或库中
可有可无 有的有返回值(print),有的没有(echo)

2 常见语言结构(本机实测 function_exists 全为 false)

语言结构 function_exists() 说明
echo false 输出,可多参数
print false 输出,返回 int(1)
isset false 判 null
empty false 判假值
list false 数组解构
array false 数组字面量
unset false 销毁变量
include / require false 文件包含
die / exit true ⚠️ 其实是函数,不是语言结构
strlen / count true 真实函数

⚠️ die / exit 是例外 —— function_exists('die') 返回 true,它们是真实函数。原笔记把它们列进语言结构是不准确的。

判断方法:

1
2
php -r 'var_dump(function_exists("echo"));'   # false → 语言结构
php -rf echo # 查函数定义

3 语言结构与函数的区别

维度 语言结构 函数
执行速度 快(无解析器二次分解) 慢(需经 Zend 引擎分解)
错误处理 比较「鲁莽」,无处理环节 可被 set_error_handler 捕获
能否在 php.ini 禁用 ❌ 不能 ✅ 可以(如 disable_functions)
能否用作回调 ❌ 不能 ✅ 能
能否用户定义同名 ❌ 不能 ✅ 能

💡 速度差的原因:函数要先被 Zend 引擎分解成语言结构,比语言结构多一层解析。但差异通常在纳秒级,不是选型理由。

回调场景的坑:

1
2
3
4
5
6
7
// ❌ 语言结构不能当回调
call_user_func('strtoupper', 'abc'); // OK(函数)
call_user_func('echo', 'abc'); // ❌ 报错

// ✅ 数组套调用
array_map('trim', $arr); // OK
array_map('isset', $arr); // ❌ 不行

二、list 的四种用法(本机实测)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 例一、连续索引数组 —— 按位置
$arr = array("book","pen","paper");
list($a, $b, $c) = $arr;
var_dump($a, $b, $c);
// string(4) "book" / string(3) "pen" / string(5) "paper"

// 例二、非连续索引 —— 按位置取,取不到是 NULL
$arr = array("1" => "book", "3" => "pen", "5" => "paper");
list($a, $b, $c) = $arr;
var_dump($a, $b, $c);
// NULL / "book" / NULL ← ⚠️ 取的是位置 0,1,2,不是键 "1","3","5"

// 例三、关联数组 —— 按位置全取不到
$arr = array("aaa" => "book", "bbb" => "pen", "ccc" => "paper");
list($a, $b, $c) = $arr;
var_dump($a, $b, $c);
// NULL / NULL / NULL ⚠️ 必须用 list('aaa'=>$a) 形式

// 例四、混合数组 —— 按数字位置
$arr = array("2" => "book", "bbb" => "pen", "0" => "paper");
list($a, $b, $c) = $arr;
var_dump($a, $b, $c);
// "paper" / NULL / "book" ← 取位置 0(=键"0")、1(无)、2(=键"2")

核心规则表:

场景 写法 取值逻辑
连续索引 list($a,$b,$c) = $arr 位置 0,1,2
关联数组 list('aaa'=>$a) = $arr 按键名
混合 list(0=>$a, 2=>$c) = $arr 显式指定数字位置

⚠️ 例二最容易误解:数组键是 "1","3","5",但 list($a,$b,$c) 取的是位置 0,1,2,所以第一个是 NULL。list() 默认永远按位置,不是按键。


三、new self vs new static(本机实测)

1
2
3
4
5
6
class Father {
public function getNewFather() { return new self(); }
public function getNewCaller() { return new static(); }
}
class Son1 extends Father {}
class Son2 extends Father {}

实测结果:

调用对象 new self() new static()
new Father() Father Father
new Son1() Father Son1
new Son2() Father Son2

区别只在继承中体现:

关键字 行为
new self() 返回定义该方法的那个类的实例(编译期确定)
new static() 返回调用者的类实例(运行时确定,迟绑定)

💡 static 在这里指的是调用类(late static binding),不是「静态成员」。


四、静态与非静态成员的访问规则

1 六条访问规则

调用形式 静态属性 非静态属性
$this-> ❌ 不支持 ✅ 支持
self:: ✅ 支持 ❌ 不支持
static:: ✅ 支持 ❌ 不支持
ClassName:: ✅ 支持 ✅(PHP 5.3+)
调用形式 静态方法 非静态方法
$this-> ✅ 支持 ✅ 支持
self:: ✅ 支持 ✅ 支持
static:: ✅ 支持 ✅ 支持

📌 记忆点:属性区分「静态 vs 非静态」,方法不区分。

2 完整验证代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Access
{
public $name = '张三';
protected $age = 10;
private $sex = '男';

public static $weight = 80;
protected static $height = 170;
private static $addr = '北京';

public function callProperty()
{
echo $this->name, PHP_EOL; // ✅
echo $this->age, PHP_EOL; // ✅
echo $this->sex, PHP_EOL; // ✅
// echo self::$name; // ❌ 非静态属性不能用 self::
// echo static::$name; // ❌ 同上
}

public function callStaticProperty()
{
// echo $this->weight; // ❌ 静态属性不能用 $this->
echo self::$weight, PHP_EOL; // ✅
echo static::$weight, PHP_EOL; // ✅
}

public function callMethod()
{
$this->a(); $this->b(); $this->c(); // ✅ 三种可见性都行
self::a(); self::b(); self::c(); // ✅
static::a(); static::b(); static::c(); // ✅
}

public static function d() { echo 'd', PHP_EOL; }
protected static function e() { echo 'e', PHP_EOL; }
private static function f() { echo 'f', PHP_EOL; }

public function callStaticMethod()
{
$this->d(); // ✅ 静态方法能用 $this->
$this->e(); // ✅ 即使是 protected static
$this->f(); // ✅
self::d(); static::d(); // ✅
}
}

💡 为什么静态方法能用 $this->? 因为 $this->f() 本质是 $this->f(取方法)再调用,PHP 允许这样访问静态方法。


五、逻辑运算符短路(本机实测全部一致)

1 短路与 &&:有假必假,遇假就停

初始 表达式 $a $b 是否短路
$a=3, $b=3 $a = 5 && $b++ bool(true) int(4) ❌ 全真,求值了
$a=3, $b=0 $a = 5 && $b++ bool(false) int(1) ❌ 求值了
$a=3, $b=3 $a && $b++ int(3) int(4) ❌
$a=0, $b=1 var_dump($a && $b++) bool(false) int(1) ✅ 短路
$a=1, $b=0 var_dump($a && $b++) bool(false) int(1) ✅ 短路
$a=1, $b=0 var_dump($a && ++$b) bool(true) int(1) ✅ 短路

2 短路或 ||:有真必真,遇真就停

初始 表达式 $a $b 是否短路
$a=3, $b=3 $a = 5 || $b++ bool(true) int(3) ✅ 短路
$a=0, $b=1 $a || $b++ int(0) int(2) ❌
$a=0, $b=1 $a = 1 || $b++ bool(true) int(1) ✅ 短路
$a=0, $b=1 $a = 0 || $b++ bool(true) int(2) ❌
$a=0, $b=0 $a = 0 || $b++ bool(false) int(1) ❌
$a=0, $b=0 $a = 1 || $b++ bool(true) int(0) ✅ 短路
$a=1, $b=0 $a || $b++ int(1) int(0) ✅ 短路
$a=1, $b=2 if ($a || $b = 5) { $b++; } — 3 ✅ 短路

3 实用价值

场景 写法 收益
防越界 $i < count($arr) && $arr[$i] > 0 避免非法下标
防未定义 isset($data['k']) && $data['k'] 避免 undefined 警告
防空对象 $obj && $obj->method() 避免 null 调用
性能优化 $ok && expensiveCall() 跳过不必要的调用
1
2
3
4
5
// ✅ 推荐:把「便宜的判断」放前面
if (isset($user['profile']) && $user['profile']->verify()) { }

// ❌ 危险:先执行了才知道不需要
if ($user['profile']->verify() && isset($user['profile'])) { } // 可能报错

⚠️ 注意原文的坑:if ($a || $b = 5) 这类写法,= 优先级高于 or/|| 的处理很反直觉,永远写括号。


六、下划线转驼峰(实测五种方式)

目标: This_is_a_test_string → ThisIsATestString

# 方式 实测输出 评价
① while + strpos + ucfirst ThisIsATestString ✅ ⚠️ 有死循环风险
② explode + ucfirst + join ThisIsATestString ✅ ✅ 推荐
③ ucwords + str_replace ThisIsATestString ✅ ✅ 最简洁
④ preg_replace 的 /e NULL ❌ PHP 7 已废弃
⑤ preg_replace_callback ThisIsATestString ✅ 性能最好

1 方式③ 推荐写法(最简洁)

1
2
3
4
5
6
function underline3($str, $ucfirst = true)
{
$str = ucwords(str_replace('_', ' ', $str));
$str = str_replace(' ', '', lcfirst($str));
return $ucfirst ? ucfirst($str) : $str;
}

2 方式② 推荐写法

1
2
3
4
5
6
7
8
function underline2($str, $ucfirst = true)
{
$arr = explode('_', $str);
foreach ($arr as $k => $v) {
$arr[$k] = ucfirst($v);
}
return $ucfirst ? implode('', $arr) : lcfirst(implode('', $arr));
}

3 ⚠️ 方式四已废弃

1
2
// ❌ PHP 7 起 /e 修饰符被移除,本机实测返回 NULL
$str = preg_replace('/_([A-Za-z])/e', "strtoupper('$1')", $str);

替代(方式五):

1
2
3
$str = preg_replace_callback('/([-_]+([a-z]{1}))/i', function ($m) {
return strtoupper($m[2]);
}, $str);

4 方式① 的死循环风险

1
2
3
4
// ⚠️ 如果 $str 本身含 "_" 且 ucfirst 不生效,可能死循环
while (($pos = strpos($str, '_')) !== false) {
$str = substr($str, 0, $pos) . ucfirst(substr($str, $pos + 1));
}

安全写法:

1
2
$str = str_replace('_', ' ', $str);
return str_replace(' ', '', ucwords($str));

七、冒泡排序

1
2
3
4
5
6
7
8
9
10
$arr = [2, 5, 1, 4, 9, 7, 3, 8];
$n = count($arr);

for ($i = 0; $i < $n - 1; $i++) {
for ($j = 0; $j < $n - 1 - $i; $j++) { // ⚠️ 内层减 i,这是关键
if ($arr[$j] > $arr[$j + 1]) {
[$arr[$j], $arr[$j + 1]] = [$arr[$j + 1], $arr[$j]];
}
}
}

实测结果: 1, 2, 3, 4, 5, 7, 8, 9 ✅ 与 sort() 一致

优化点:

写法 说明
$j < $n - 1 - $i 每轮结束后,末尾 $i 个已排好,不用再比
加 break 标志位 本轮无交换则提前退出(最好情况 O(n))
生产环境 ❌ 别手写,直接 sort() / asort(),C 实现快得多
1
2
3
4
5
6
7
8
9
10
11
// 优化版:带提前退出
$swapped = true;
for ($i = 0; $i < $n - 1 && $swapped; $i++) {
$swapped = false;
for ($j = 0; $j < $n - 1 - $i; $j++) {
if ($arr[$j] > $arr[$j + 1]) {
[$arr[$j], $arr[$j + 1]] = [$arr[$j + 1], $arr[$j]];
$swapped = true;
}
}
}

八、array_merge vs + 运算符(本机实测)

1
2
3
4
5
6
7
8
$arr1 = [1, 2, 3];
$arr2 = [1, 4, 5];

print_r(array_merge($arr1, $arr2));
// 实测 => [1, 2, 3, 1, 4, 5] 全部追加

print_r($arr1 + $arr2);
// 实测 => [1, 2, 3] 取并集,左边优先

字符串键的行为差异:

写法 输入 输出
array_merge ['a'=>1,'b'=>2] + ['b'=>99,'c'=>3] {"a":1,"b":99,"c":3} ← b 被覆盖
+ 运算符 同上 {"a":1,"b":2,"c":3} ← b 保留

完整对比表:

维度 array_merge + 运算符
数字键 全部追加(重新索引) 跳过重复的下标
字符串键 后者覆盖前者 前者保留
数组数量 可合并多个(array_merge($a,$b,$c)) 只能是两个
返回 新数组 新数组
用途 拼接列表 取并集
1
2
3
4
// 常见坑:用 + 合并列表
$a = [1, 2]; $b = [3, 4];
$c = $a + $b; // ❌ [1,2],$b 完全没用上
$c = array_merge($a, $b); // ✅ [1,2,3,4]

九、类型转换规则(本机实测)

1 转 int

输入 输出 说明
null 0
true / false 1 / 0
1.9 1 向下取整(PHP 8.1 起弃用静默截断)
'xyz' 0 无法转数字
'12abc' 12 ⚠️ 取开头数字,PHP 7.1+ 报 Warning
'09:00' 9
[] 0 空数组
[1,2] 1 非空数组

字符串比较的坑:

1
2
var_dump('09:00' > '8:00');    // bool(false) —— 按字符串比,'0' < '8'
var_dump('09:00' > '08:00'); // bool(true) —— '0' == '0' 后比 '9' > '8'

2 转 string(本机实测)

输入 输出
null / false ""
true "1"
1 "1"
-1 "-1"
1.22222 "1.22222"
4.1E+6 "4100000" ⚠️ 科学计数法会展开
[] "Array" + Notice
对象(无 __toString) 🔴 Error

3 转 array

输入 输出
null []
1 [0 => 1]
'aaa' [0 => 'aaa']
对象 属性数组,键名带修饰符

对象转数组的属性名修饰(本机实测):

1
2
3
4
5
6
class Test {
public $public_var = 456;
private $private_var = 123;
protected $protected_var = 789;
}
print_r((array)new Test());
1
2
3
4
5
6
Array
(
[public_var] => 456
[Testprivate_var] => 123 ← private 加【类名】前缀
[*protected_var] => 789 ← protected 加【*】前缀
)

💡 这是访问私有属性的「钥匙」:$obj->{'Testprivate_var'} 能访问到 private 属性。序列化框架在用这个机制。

4 转 object

输入 输出
null 空的 stdClass
['a'=>1] object { a: 1 },键名变属性名
[1,2] object { [0]: 1, [1]: 2 },无法用 -> 访问,只能遍历
其他标量 object { scalar: 值 }
1
2
3
4
5
6
$obj = (object)['a' => 1, 'b' => 2];
echo $obj->a; // ✅
$arr = [1, 2];
$o = (object)$arr;
echo $o->{0}; // ❌ 无法访问数字键
foreach ($o as $k => $v) { echo $k; } // ✅ 只能这样

十、引用返回

1 官方 demo(本机实测)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Foo {
public $value = 42;
public function &getValue() { return $this->value; }
public function getValueNone() { return $this->value; }
}

$obj = new Foo;
$a = &$obj->getValue(); // 引用接收
$b = $obj->getValueNone(); // 值接收

echo $a, $b; // 42 42
$a = 11;
$b = $obj->getValueNone();
echo $b; // ⚠️ 实测 11 —— 原对象被改了!

🔴 实测确认:引用返回会修改原对象。 这就是「返回引用」的真实含义。

2 静态变量的 demo(原文,解释版)

1
2
3
4
5
6
function &test() {
static $b = 0; // ⚠️ 必须用 static,否则返回的是局部变量的副本
$b = $b + 1;
echo $b, PHP_EOL;
return $b;
}

实测输出:

1
2
3
4
5
6
7
$a = test();        // 输出 1
$a = 5;
$a = test(); // 输出 2 ← 值接收,$b 独立累计

$a = &test(); // 输出 3 ← 引用接收,$a 与 $b 同址
$a = 5; // $b 也变成 5
$a = test(); // 输出 6 ← 从 5 继续

原理:

调用方式 发生了什么
$a = test() 只是把返回值赋值给 $a,两者无关
$a = &test() 把 return $b 中 $b 的内存地址与 $a 绑定,等价 $a = &$b

💡 原文那句疑问「为啥要声明静态变量」 —— 因为非 static 的局部变量在函数返回后就被销毁了,返回它的引用是未定义行为。

3 官方警告

原文建议 说明
「不要用返回引用来增加性能」 ✅ 引擎足够聪明,会自己优化
「仅在有合理技术原因时才返回引用」 ✅ 现代 PHP 基本不该用

风险清单:

风险 说明
意外修改 改返回值会改原对象
循环引用 引用指向自己时无法回收(内存泄漏)
难追踪 数据流不可见,IDE 跳转失灵
返回局部变量 函数结束即销毁,行为未定义

十一、自动加载

1 三种方式对比

方式 代码量 灵活性 状态
include / require 多 ❌ 每个文件都要写 传统
__autoload() 少 ❌ 只能定义一次 PHP 7.2 已废弃
spl_autoload_register() 中 ✅ 可注册多个 ✅ 推荐

2 四种写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 方式1:include —— 最原始
include('A.php');
include('B.php');

// 方式2:__autoload —— PHP 7.2 已废弃
function __autoload($class) {
include $class . '.php';
}

// 方式3:spl_autoload_register —— 推荐
function my_autoload($class) {
include $class . '.php';
}
spl_autoload_register('my_autoload');

// 或匿名函数
spl_autoload_register(function ($class) {
include $class . '.php';
});

// 方式4:框架写法
class ClassAutoloader
{
public function __construct() {
spl_autoload_register([$this, 'loader']);
}
private function loader($className) {
include $className . '.php';
}
}
$autoloader = new ClassAutoloader();

3 __autoload vs spl_autoload_register

维度 __autoload spl_autoload_register
可注册次数 只能 1 个 N 个
加载顺序 — 按注册顺序
错误处理 ❌ 错误不能被 catch ✅ 可被捕获
SPL 支持 ❌ 无 ✅ 功能丰富
多个库 ❌ 冲突 ✅ 各库注册自己的

4 SPL 相关函数

函数 作用
spl_autoload_register() 注册自动加载器 ✅ 核心
spl_autoload_unregister() 注销已注册的
spl_autoload_functions() 列出所有已注册的
spl_autoload_extensions() 读/设默认扩展名
spl_autoload_call() 手动触发加载
spl_classes() 列出所有 SPL 类
spl_autoload() 默认实现

⚠️ __autoload 在 CLI 交互模式无效 —— 也就是 PHP 交互式命令行(php -a)里不能用自动加载。


十二、$_POST / php://input / $_GLOBALS

1 三者的取值范围

Content-Type $_POST php://input $_GLOBALS
application/x-www-form-urlencoded ✅ ✅ 与 $_POST 一致 ✅
multipart/form-data ✅ ❌ 读不到 ❌ 读不到
application/json ❌ ✅ ✅
text/xml、text/plain ❌ ✅ ✅
无法识别 ❌ ✅ ✅

记忆:

场景 用什么
发送 JSON $_POST 拿不到,用 file_get_contents('php://input')
发送表单 $_POST 正常
上传文件 $_FILES + php://input 都拿不到

2 php://input vs $_GLOBALS

维度 php://input $_GLOBALS
内存占用 更小 占用全部全局变量
php.ini 限制 不受 受 variables_order 影响
推荐度 ✅ 推荐 ❌ 不推荐

典型场景(原文提到的): 跟 Java 对接时,Java 用 POST 发请求体(非表单格式),PHP 用 $_POST/$_REQUEST 拿到的是错的,最终用 php://input 才对。


十三、composer 版本约束

符号 含义 示例
^ 锁定主版本号 ^6.20 → 6.20.0 ~ 6.99.999
~ 锁定次版本号 ~6.20 → 6.20.0 ~ 6.20.999
* 任意版本 6.*

完整示例:

约束 允许的范围
^6.20 >=6.20.0 <7.0.0
~6.20 >=6.20.0 <6.21.0
6.20.* >=6.20.0 <6.21.0
>=6.20 >=6.20.0 无上限
6.20.1 只有 6.20.1

💡 ^ 更宽松(允许小版本升级),~ 更严格。生产项目用 ~ 或精确版本更安全。


十四、composer 常用命令

命令 作用
composer install 按 composer.lock 安装(生产用这个)
composer update 更新依赖并改 lock(开发用)
composer require 包名 添加依赖
composer require 包名:1.19 添加指定版本
composer remove 包名 移除依赖
composer show 查看所有包信息
composer search 包名 搜索包
composer create-project laravel/laravel 创建项目
composer dump-autoload 重建 autoload
composer config -l 查看当前镜像配置
composer list 所有命令

Windows 特殊:

1
2
composer install --ignore-platform-reqs
# ⚠️ Windows 下装不了 pcntl 扩展,用这个忽略平台检查

💡 install vs update 的区别:install 严格按 lock 文件装(可重现),update 会去查最新版并改 lock。生产部署永远用 install。


十五、大文件上传配置

PHP 配置(php.ini):

1
2
3
4
post_max_size = 50M           ; PHP 可接受的最大 POST 数据
upload_max_filesize = 50M ; 文件上传允许的最大值
max_execution_time = 300 ; 脚本最大执行时间(0=不限制,不建议)
memory_limit = 128M ; ⚠️ 不能小于 post_max_size

Nginx 配置(http 段):

1
client_max_body_size 50m;

Apache(.htaccess 或 httpd.conf):

1
2
php_value upload_max_filesize 50M
php_value post_max_size 52M ; ⚠️ 要比 upload_max_filesize 大 2M(表单其他字段)

三个参数的优先级:

限制 由谁决定
谁能上传 Nginx client_max_body_size 最先检查
PHP 能收多大 post_max_size
单文件多大 upload_max_filesize
能用多少内存 memory_limit(必须 ≥ post_max_size)

排查「上传失败」:

现象 原因 解法
$_FILES 为空 超过 post_max_size 调大 + 调 memory_limit
返回 413 超过 Nginx client_max_body_size 调 client_max_body_size
upload_max_filesize 报错 PHP 限制 调该项
大文件超时 max_execution_time 调大或用分片上传

💡 Nginx 默认只有 1M! 这是「本地正常、服务器上传失败」的最常见原因。


十六、浮点数精度

1 问题(本机实测)

1
2
3
$a = 0.1 + 0.2;
var_dump($a); // float(0.30000000000000004)
var_dump($a == 0.3); // bool(false) ← 陷阱!

2 原文的三种解法

方法 评价
乘 100 转整数运算 ⚠️ 局限,只适合固定 2 位小数
number_format 转字符串再转 float ⚠️ 丢失精度,只用于展示
用 bcmath 扩展 ✅ 正解

3 bcmath 函数表(本机实测可用)

函数 作用 示例
bcadd($a,$b,$scale) 加 bcadd('0.1','0.2',2) → 0.30 ✅
bcsub() 减
bcmul() 乘
bcdiv() 除 除数为 0 会抛 DivisionByZeroError
bccomp() 比较,返回 -1/0/1
bcmod() 求余
bcpow() 乘方
bcpowmod() 乘方求模(数论常用)
bcsqrt() 平方根
bcscale() 设置/读取默认小数位

全局设置 scale:

1
2
3
bcscale(2);                     // 之后不用每次传 $scale
echo bcadd('0.1', '0.2'); // 0.30
echo bcdiv('1', '3', 4); // 0.3333

最佳实践(金额处理):

做法 说明
❌ 别用 float 存钱 浮点精度不可控
✅ 用整数存分 1999 表示 19.99 元
✅ 或用 bcmath 可读性好,注意设 scale

十七、AES 加密(⚠️ 原文代码已失效)

1 原文代码无法运行

1
2
// ❌ 本机实测:mcrypt 扩展不存在
mcrypt_get_block_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_ECB); // Fatal
项 状态
mcrypt 扩展 ❌ PHP 7.2 已移除
MCRYPT_RIJNDAEL_256 常量 ❌ 不存在
mcrypt_* 系列函数 ❌ 全部失效

2 现代替代:openssl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Security
{
public static function encrypt(string $plain, string $key, string $iv): string
{
return base64_encode(openssl_encrypt(
$plain,
'aes-256-cbc',
$key,
OPENSSL_RAW_DATA,
$iv
));
}

public static function decrypt(string $cipher, string $key, string $iv): string|false
{
return openssl_decrypt(
base64_decode($cipher),
'aes-256-cbc',
$key,
OPENSSL_RAW_DATA,
$iv
);
}
}

// 用法
$key = hash('sha256', 'your-secret', true); // 32 字节
$iv = random_bytes(16); // ⚠️ 每次都要新的
$enc = Security::encrypt('hello', $key, $iv);
echo Security::decrypt($enc, $key, $iv); // hello
模式 说明
CBC ✅ 推荐,必须配 IV
ECB ❌ 不推荐,相同明文产生相同密文
CTR / GCM ✅ 更好,GCM 带认证标签

⚠️ IV 绝对不能硬编码!必须 random_bytes() 生成并随密文一起存储。(原文第二段代码硬编码了 md5(rand(0,10000)) 当密钥,这是极不安全的。)

3 原文第二个加密类的问题

问题 风险
用 md5(rand(0, 10000)) 当密钥 🔴 只有 1 万种可能,秒破
用 ^ 做对称加密 🔴 字符串异或是可逆的,没有密钥扩展
密钥硬编码在源码里 🔴 泄露即失效
没有认证 🔴 可被篡改

🔴 这段代码不要用于任何真实场景。 用 openssl_encrypt + password_hash(密码用 password_hash,对称数据用 openssl)。

密码存储的正确做法:

1
2
3
4
5
6
7
8
9
10
// 存
$hash = password_hash($password, PASSWORD_BCRYPT);

// 验证
password_verify($input, $hash);

// 需要升级算法时
if (password_needs_rehash($hash, PASSWORD_BCRYPT)) {
$hash = password_hash($input, PASSWORD_BCRYPT);
}

十八、常见函数速查

1 parse_str()

1
2
parse_str('a=1&b=2', $arr);       // ✅ 推荐:第二个参数必传
print_r($arr); // ['a' => '1', 'b' => '2']
特性 说明
第二个参数必须传 不传会覆盖当前作用域变量,污染全局
数组语法 a[0]=x&a[1]=y → ['a' => ['0'=>'x','1'=>'y']]
安全 ⚠️ 不做过滤,需自行校验

2 extension_loaded()

1
2
extension_loaded('redis');    // 本机实测 false
extension_loaded('bcmath'); // 本机实测 true
函数 区别
extension_loaded('redis') 只查当前进程已加载的
function_exists('Redis') 查类/函数是否存在
get_loaded_extensions() 返回所有已加载扩展名数组

3 命令行速查

1
2
3
4
5
6
7
8
9
10
php -h                      # 帮助
php -v # 版本
php -m # 所有已加载扩展
php --ri redis # redis 扩展信息(本机 8.4 无此扩展)
php --ri redis # redis 扩展配置信息
php -r 'echo ini_get("extension_dir");' # 扩展目录
php -i | grep configure # 编译选项
php -l test.php # 语法检查(不执行)
php -S localhost:8000 # 启动内置服务器
php -d memory_limit=512M script.php # 临时改配置

💡 php -l 只做语法检查不执行,是上线前最快的自查手段。

4 其他零散函数

函数 作用 注意
fastcgi_finish_request() 提前结束响应,脚本继续跑 常用于异步任务
register_shutdown_function() 注册结束回调 替代 __destruct 更可靠
set_exception_handler() 全局异常处理 配 set_error_handler
parse_str() 解析查询串 第二参数必传
stream_socket_client() TCP/UDP 客户端 需 timeout 防阻塞
pcntl_fork() 创建子进程 仅 CLI + Unix
php_sapi_name() 运行环境名 对比常量 PHP_SAPI
microtime(true) 带微秒的时间戳 计时用;false 返回字符串

microtime 两种用法:

1
2
3
4
5
6
7
8
// 时间戳
$ts = microtime(true); // float

// 高精度计时
$t1 = microtime(true);
do_something();
$t2 = microtime(true);
echo ($t2 - $t1) * 1000 . " ms";

十九、常见错误速查

写法 问题 正确
preg_replace('/x/e', ...) PHP 7 已废弃,返回 NULL preg_replace_callback
mcrypt_* 扩展已移除 openssl_*
array_merge vs + 混用 + 会跳过重复数字键 按需求选
list($a,$b) 取关联数组 取不到(按位置不是按键) list('k'=>$a)
new self() 在子类中 返回父类实例 new static()
静态属性用 $this-> ❌ 不支持 self:: / static::
非静态属性用 self:: ❌ 不支持 $this->
function &getValue() 修改返回值会改原对象 返回值即可
非 static 局部变量返回引用 函数结束即销毁 必须 static
function &test() ⚠️ 官方不建议 用返回值
__autoload() 7.2 已废弃 spl_autoload_register
硬编码 AES 密钥 🔴 极不安全 环境变量 + 密钥管理
parse_str($s) 无第二参 污染全局作用域 parse_str($s, $arr)
@ 屏蔽错误 掩盖问题,排查困难 try/catch + 日志
金额用 float 精度丢失 整数存分 或 bcmath
上传大文件只改 php.ini Nginx 默认 1M 会拦 同步改 client_max_body_size

二十、30 秒自测

问题 答案
怎么判断是语言结构还是函数? function_exists() 返回 false 即语言结构
die 是语言结构吗? ❌ 是函数,function_exists('die') 返回 true
语言结构能做回调吗? ❌ 不能
list($a,$b) 取关联数组能取到吗? ❌ 取不到,list() 默认按位置
关联数组要用什么形式? list('key' => $v)
new self() vs new static()? self 返回定义类,static 返回调用类
什么时候两者有区别? 只在有继承时
静态属性能用 $this-> 访问吗? ❌ 不能,要用 self:: / static::
非静态属性能用 self:: 吗? ❌ 不能
静态方法能用 $this-> 调用吗? ✅ 可以(方法不区分)
&& 短路什么时候触发? 左边为 falsy 时,右边不执行
|| 短路什么时候触发? 左边为 truthy 时,右边不执行
a=5 || $b++($b=3)结果? $a=true, $b=3(短路)
下划线转驼峰方式④能用吗? ❌ /e 修饰符 PHP 7 已移除
mcrypt 扩展还能用吗? ❌ PHP 7.2 已移除,用 openssl
__autoload() 还能用吗? ❌ PHP 7.2 已废弃,用 spl_autoload_register
^6.20 允许什么版本? 6.20.0 ~ 6.99.999
~6.20 允许什么版本? 6.20.0 ~ 6.20.999
array_merge 和 + 的字符串键差别? merge 后者覆盖,+ 前者保留
json 请求体用 $_POST 拿得到吗? ❌ 拿不到,用 php://input
parse_str 第二参数能省吗? ❌ 不能,会污染全局
Nginx 默认上传限制是多少? 1M
memory_limit 和 post_max_size 谁大? memory_limit 必须更大
0.1 + 0.2 === 0.3 吗? ❌ 得到 0.30000000000000004
金额该怎么存? 整数存分 或 bcmath

二十一、参考

来源 链接
语言结构列表(官方) https://www.php.net/manual/zh/language.constructs.php
list()(官方) https://www.php.net/manual/zh/function.list.php
后期静态绑定(官方) https://www.php.net/manual/zh/language.oop5.late-static-binding.php
类的访问控制(官方) https://www.php.net/manual/zh/language.oop5.visibility.php
自动加载(官方) https://www.php.net/manual/zh/language.oop5.autoload.php
parse_str()(官方) https://www.php.net/manual/zh/function.parse-str.php
openssl_encrypt()(官方) https://www.php.net/manual/zh/function.openssl-encrypt.php
bcmath(官方) https://www.php.net/manual/zh/book.bc.php
Composer 版本约束 https://getcomposer.org/doc/articles/versions.md
php://input(官方) https://www.php.net/manual/zh/wrappers.php
Nginx 上传限制 https://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size