支撑工具
Pin 提供了一组通用工具,其中许多也被 Pin 内部使用。根据需要,可在业务代码中直接使用这些工具。
Facades 入口
src/Support/Facades 提供 Pin 核心服务的 Laravel Facade 入口。
| Facade | 对应类 | 容器绑定 |
|---|---|---|
Actor | Pin\Log\Actor | pin.log.actor |
Aes | Pin\Crypt\Aes | pin.crypt.aes |
HashCache | Pin\Cache\HashCache | pin.cache.hash |
Password | Pin\Password\Password | pin.password |
Rsa | Pin\Crypt\Rsa | pin.crypt.rsa |
RuntimeCache | Pin\Cache\RuntimeCache | pin.cache.runtime |
Token | Pin\Token\TokenManager | pin.token |
Tree | Pin\Tree\Tree | pin.tree |
服务提供者
Pin\Support\ServiceProvider 扩展了 Laravel ServiceProvider 的配置合并行为。
与 Laravel 默认的 mergeConfigFrom() 不同,Pin 会递归合并配置数组,因此业务项目可以只覆盖需要修改的配置项,而无需复制整个配置文件。
use Pin\Support\ServiceProvider;
class TokenServiceProvider extends ServiceProvider
{
public function boot(): void
{
$this->mergeConfigFrom(__DIR__.'/../config/pin/token.php', 'pin.token');
}
}包提供以下默认配置:
return [
'drivers' => [
'session' => [
'expires' => 7200,
'refresh_before' => 300,
],
],
];业务项目只需覆盖需要修改的配置:
return [
'drivers' => [
'session' => [
'expires' => 3600,
],
],
];最终配置为:
return [
'drivers' => [
'session' => [
'expires' => 3600,
'refresh_before' => 300,
],
],
];数据容器
DataBag 基于 Laravel 的 Illuminate\Support\Fluent,提供一致的数据访问方式,并默认启用严格模式。
Context 基于 DataBag 构建,两者具有相同的使用方式,但语义不同:
DataBag:用于保存通用数据。Context:用于在一次请求、一次操作或一段业务流程中传递上下文数据。
创建
使用 DataBag::new() 可以将常见输入统一转换为 DataBag 实例:
use Pin\Support\DataBag;
$empty = DataBag::new(null);
$fromArray = DataBag::new([
'id' => 1,
]);
$same = DataBag::new($fromArray);严格模式
默认情况下,DataBag 启用严格模式。
通过属性或数组访问不存在的键会抛出 RuntimeException。
get() 方法与 Laravel Fluent 保持一致,不受严格模式影响:
$bag = new DataBag([
'name' => 'Pin',
]);
$name = $bag->name; // Pin
$bag->get('missing'); // null
$bag->missing; // RuntimeException
$bag['missing']; // RuntimeException创建实例时传入 false 可关闭严格模式:
$bag = new DataBag([], false);
$bag->missing; // null
$bag['missing']; // nullJSON
Pin\Support\Json 提供统一的 JSON 编解码方法。
编码
默认情况下,encode() 会启用 JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR。
use Pin\Support\Json;
$json = Json::encode(['hello' => '你好']);
// '{"hello":"你好"}'自定义编码选项:
$json = Json::encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);解码
默认情况下,decode() 会返回数组。
$data = Json::decode('{"hello":"你好"}');
// ['hello' => '你好']返回 stdClass:
$data = Json::decode($json, false);异常处理
JSON 编解码失败时,Json 会抛出 Pin\Exceptions\Exception,并通过上下文保存原始数据:
try {
Json::decode($invalidJson);
} catch (\Pin\Exceptions\Exception $e) {
$context = $e->getContext();
// ['data' => $invalidJson]
}字符串
字符串分割
Str::explode() 用于按指定分隔符分割字符串,并自动去除元素两侧的空白,同时过滤空值。
默认使用 , 分割:
use Pin\Support\Str;
Str::explode(' foo, bar, ,'); // ['foo', 'bar']
Str::explode(null); // []
Str::explode(' '); // []指定分割符:
Str::explode('foo|bar', '|'); // ['foo', 'bar']转换为整数数组:
Str::explodeToIntegers('-1,2,3'); // [-1, 2, 3]WARNING
explodeToIntegers() 使用 intval() 进行转换。
Str::explodeToIntegers('1,1a,a');
// [1, 1, 0]字符串转换
Str::string() 将给定值转换为字符串。
对于枚举:
BackedEnum:返回value。UnitEnum:返回name。
enum Status: string
{
case Enabled = 'enabled';
}
enum Role
{
case Admin;
}
Str::string(Status::Enabled); // enabled
Str::string(Role::Admin); // Admin其他类型通过 (string) 转换为字符串。
Str::string('hello'); // hello
Str::string(123); // "123"
Str::string(true); // "1"
Str::string(null); // ""占位符替换
Str::format() 用于替换字符串中的占位符。
默认使用 {} 占位符:
use Pin\Support\Str;
Str::format(
'{name} has {count} items',
[
'name' => 'Pin',
'count' => 3,
]
);
// Pin has 3 items自定义占位符:
Str::format(
'Hello :name',
[
'name' => 'Pin',
],
':'
);
// Hello PinINFO
占位符规则:
- 双字符:
{key}/%key% - 单字符:
:key
UTF-8 判断
Str::isValidUtf8() 用于判断字符串是否为有效的 UTF-8 编码。
use Pin\Support\Str;
Str::isValidUtf8($value);脱敏处理
Str::maskSensitive() 用于根据字段名对敏感数据进行脱敏。
use Pin\Support\Str;
Str::maskSensitive('secret123'); // sec******可以传入字段名,用于判断是否需要脱敏:
Str::maskSensitive('secret123', 'password'); // sec******
Str::maskSensitive('secret123', 'name'); // secret123默认规则:
| 字段名 | 处理方式 |
|---|---|
未提供 (null) | 脱敏处理 |
包含 password(不区分大小写) | 脱敏处理 |
| 其他 | 保持原值 |
默认脱敏规则会保留前 3 个字符,并追加 ******。
可通过 setSensitiveValueMasker() 自定义脱敏处理逻辑,回调中的 $key 参数表示当前字段名:
Str::setSensitiveValueMasker(function (mixed $value, ?string $key): mixed {
return match ($key) {
'email' => substr((string) $value, 0, 3).'***@***',
'phone' => substr((string) $value, 0, 3).'****'.substr((string) $value, -4),
default => '******',
};
});
Str::maskSensitive('test@example.com', 'email'); // tes***@***
Str::maskSensitive('13800138000', 'phone'); // 138****8000数组
递归合并
Arr::merge() 用于递归合并数组。
与 PHP 原生 array_merge_recursive() 不同,当字符串键发生冲突时,后面的值会覆盖前面的值,而不是合并为数组。
use Pin\Support\Arr;
Arr::merge(
[
'database' => [
'host' => 'localhost',
'port' => 3306,
],
],
[
'database' => [
'host' => '127.0.0.1',
],
],
);
// [
// 'database' => [
// 'host' => '127.0.0.1',
// 'port' => 3306,
// ],
// ]默认情况下,相同数字键会追加新值:
Arr::merge(
[1 => ['one']],
[1 => ['two']],
);
// [
// 1 => ['one'],
// 2 => ['two'],
// ]
//如需在数字键冲突时覆盖原值,可将第一个参数设为 true:
Arr::merge(
true,
[1 => ['one']],
[1 => ['two']],
);
// [
// 1 => ['two'],
// ]null 转空字符串
Arr::nullToEmptyString() 会递归将数组中的 null 值转换为空字符串。
use Pin\Support\Arr;
$data = Arr::nullToEmptyString([
'name' => null,
'profile' => [
'nickname' => null,
],
]);
// [
// 'name' => '',
// 'profile' => [
// 'nickname' => '',
// ],
// ]扁平数组转树
Arr::toTree() 将扁平数组转换为树结构。
每个元素需要包含父级字段,默认使用 pid:
use Pin\Support\Arr;
$tree = Arr::toTree([
['id' => 1, 'name' => '电子产品', 'pid' => 0],
['id' => 2, 'name' => '手机', 'pid' => 1],
['id' => 3, 'name' => '手机配件', 'pid' => 2],
['id' => 4, 'name' => '手机壳', 'pid' => 3],
]);返回结果:
[
[
'id' => 1,
'name' => '电子产品',
'pid' => 0,
'children' => [
[
'id' => 2,
'name' => '手机',
'pid' => 1,
'children' => [
[
'id' => 3,
'name' => '手机配件',
'pid' => 2,
'children' => [
[
'id' => 4,
'name' => '手机壳',
'pid' => 3,
],
],
],
],
],
],
],
]默认从 pid = 0 的节点开始构建树,并将子节点保存到 children 字段。
可以自定义父级字段和子节点字段:
$tree = Arr::toTree(
$data,
'parent_id',
'items',
);敏感字段脱敏
Arr::maskSensitive() 用于递归脱敏数组中的字段值。
use Pin\Support\Arr;
$payload = Arr::maskSensitive([
'username' => 'root',
'password' => 'secret123',
'database' => [
'Password' => 'db-secret',
],
]);返回:
[
'username' => 'root',
'password' => 'sec******',
'database' => [
'Password' => 'db-******',
],
]脱敏规则参见 Str::maskSensitive()。
计时与耗时
Timer 用于记录时间点和计算耗时,Duration 用于表示耗时结果。
请求耗时
use Pin\Support\Timer;
$duration = Timer::durationSinceStartOfRequest();
$seconds = $duration->seconds();
$milliseconds = $duration->milliseconds();默认使用 REQUEST_TIME_FLOAT 作为请求开始时间,也可以手动指定开始时间:
$duration = Timer::durationSinceStartOfRequest(microtime(true) - 1);局部计时
可以使用 Timer 对指定代码片段进行计时:
$timer = new Timer();
$timer->start('query');
// do something
$duration = $timer->stop('query'); // DurationDuration
Duration 用于表示一段时间范围的耗时,并提供耗时和内存变化统计。
$duration->seconds(); // 秒,默认保留 4 位小数
$duration->milliseconds(); // 毫秒
$duration->memoryUsage(); // 内存变化(字节)seconds() 可以指定保留的小数位数:
$duration->seconds(2); // 保留 2 位小数大小单位
Pin\Support\Size 用于在人类可读大小和字节数之间转换。
use Pin\Support\Size;
Size::format(2147483648); // 2G
Size::format(524288); // 512K
Size::toBytes('10B'); // 10
Size::toBytes('1K'); // 1024
Size::toBytes('0.5M'); // 524288
Size::toBytes('2G'); // 2147483648INFO
toBytes() 根据字符串后缀识别单位,支持 b、k、kb、m、mb、g、gb(不区分大小写)。
调用定位
Caller 用于从 PHP 调用栈中解析业务代码调用位置。
use Pin\Support\Caller;
$caller = Caller::resolve();
// [
// 'file' => '/app/Services/UserService.php',
// 'line' => 42,
// ]默认情况下,Caller 会从 debug_backtrace() 获取调用栈,并跳过 vendor 目录中的文件,返回第一个业务代码位置。
如果无法找到业务代码,则返回第一个可用的调用信息。
可通过 setApplicationFileResolver() 自定义业务文件判断规则:
Caller::setApplicationFileResolver(
fn (string $file) => str_contains($file, '/app/')
);反射访问
Invoker 基于 PHP Reflection,用于访问对象或类的非公开成员。
创建
use Pin\Support\Invoker;
$invoker = new Invoker($object);
// 或者
$invoker = new Invoker(SomeClass::class);属性访问
支持访问实例属性和静态属性:
$value = $invoker->name;
$invoker->name = 'Pin';静态属性:
$value = $invoker->config;
$invoker->config = $config;也可以通过 get() 和 set() 使用点语法访问嵌套属性:
$value = $invoker->get('config.database.host');
$invoker->set('config.database.host', 'localhost');方法调用
可以调用非公开方法:
$result = $invoker->hiddenMethod($arg1, $arg2);静态方法:
$result = (new Invoker(SomeClass::class))->hiddenStaticMethod();