Skip to content

支撑工具

Pin 提供了一组通用工具,其中许多也被 Pin 内部使用。根据需要,可在业务代码中直接使用这些工具。

Facades 入口

src/Support/Facades 提供 Pin 核心服务的 Laravel Facade 入口。

Facade对应类容器绑定
ActorPin\Log\Actorpin.log.actor
AesPin\Crypt\Aespin.crypt.aes
HashCachePin\Cache\HashCachepin.cache.hash
PasswordPin\Password\Passwordpin.password
RsaPin\Crypt\Rsapin.crypt.rsa
RuntimeCachePin\Cache\RuntimeCachepin.cache.runtime
TokenPin\Token\TokenManagerpin.token
TreePin\Tree\Treepin.tree

服务提供者

Pin\Support\ServiceProvider 扩展了 Laravel ServiceProvider 的配置合并行为。

与 Laravel 默认的 mergeConfigFrom() 不同,Pin 会递归合并配置数组,因此业务项目可以只覆盖需要修改的配置项,而无需复制整个配置文件。

php
use Pin\Support\ServiceProvider;

class TokenServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->mergeConfigFrom(__DIR__.'/../config/pin/token.php', 'pin.token');
    }
}

包提供以下默认配置:

php
return [
    'drivers' => [
        'session' => [
            'expires' => 7200,
            'refresh_before' => 300,
        ],
    ],
];

业务项目只需覆盖需要修改的配置:

php
return [
    'drivers' => [
        'session' => [
            'expires' => 3600,
        ],
    ],
];

最终配置为:

php
return [
    'drivers' => [
        'session' => [
            'expires' => 3600, 
            'refresh_before' => 300,
        ],
    ],
];

数据容器

DataBag 基于 Laravel 的 Illuminate\Support\Fluent,提供一致的数据访问方式,并默认启用严格模式。

Context 基于 DataBag 构建,两者具有相同的使用方式,但语义不同:

  • DataBag:用于保存通用数据。
  • Context:用于在一次请求、一次操作或一段业务流程中传递上下文数据。

创建

使用 DataBag::new() 可以将常见输入统一转换为 DataBag 实例:

php
use Pin\Support\DataBag;

$empty = DataBag::new(null);

$fromArray = DataBag::new([
    'id' => 1,
]);

$same = DataBag::new($fromArray);

严格模式

默认情况下,DataBag 启用严格模式。

通过属性或数组访问不存在的键会抛出 RuntimeException

get() 方法与 Laravel Fluent 保持一致,不受严格模式影响:

php
$bag = new DataBag([
    'name' => 'Pin',
]);

$name = $bag->name;   // Pin
$bag->get('missing'); // null

$bag->missing;   // RuntimeException
$bag['missing']; // RuntimeException

创建实例时传入 false 可关闭严格模式:

php
$bag = new DataBag([], false);

$bag->missing;   // null
$bag['missing']; // null

JSON

Pin\Support\Json 提供统一的 JSON 编解码方法。

编码

默认情况下,encode() 会启用 JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR

php
use Pin\Support\Json;

$json = Json::encode(['hello' => '你好']);
// '{"hello":"你好"}'

自定义编码选项:

php
$json = Json::encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

解码

默认情况下,decode() 会返回数组。

php
$data = Json::decode('{"hello":"你好"}');
// ['hello' => '你好']

返回 stdClass

php
$data = Json::decode($json, false);

异常处理

JSON 编解码失败时,Json 会抛出 Pin\Exceptions\Exception,并通过上下文保存原始数据:

php
try {
    Json::decode($invalidJson);
} catch (\Pin\Exceptions\Exception $e) {
    $context = $e->getContext();
    // ['data' => $invalidJson]
}

字符串

字符串分割

Str::explode() 用于按指定分隔符分割字符串,并自动去除元素两侧的空白,同时过滤空值。

默认使用 , 分割:

php
use Pin\Support\Str;

Str::explode(' foo, bar, ,');    // ['foo', 'bar']
Str::explode(null);             // []
Str::explode(' ');              // []

指定分割符:

php
Str::explode('foo|bar', '|'); // ['foo', 'bar']

转换为整数数组:

php
Str::explodeToIntegers('-1,2,3'); // [-1, 2, 3]

WARNING

explodeToIntegers() 使用 intval() 进行转换。

php
Str::explodeToIntegers('1,1a,a');
// [1, 1, 0]

字符串转换

Str::string() 将给定值转换为字符串。

对于枚举:

  • BackedEnum:返回 value
  • UnitEnum:返回 name
php
enum Status: string
{
    case Enabled = 'enabled';
}

enum Role
{
    case Admin;
}

Str::string(Status::Enabled); // enabled
Str::string(Role::Admin);     // Admin

其他类型通过 (string) 转换为字符串。

php
Str::string('hello'); // hello
Str::string(123);     // "123"
Str::string(true);    // "1"
Str::string(null);    // ""

占位符替换

Str::format() 用于替换字符串中的占位符。

默认使用 {} 占位符:

php
use Pin\Support\Str;

Str::format(
    '{name} has {count} items',
    [
        'name' => 'Pin',
        'count' => 3,
    ]
);

// Pin has 3 items

自定义占位符:

php
Str::format(
    'Hello :name',
    [
        'name' => 'Pin',
    ],
    ':'
);

// Hello Pin

INFO

占位符规则:

  • 双字符:{key} / %key%
  • 单字符::key

UTF-8 判断

Str::isValidUtf8() 用于判断字符串是否为有效的 UTF-8 编码。

php
use Pin\Support\Str;

Str::isValidUtf8($value);

脱敏处理

Str::maskSensitive() 用于根据字段名对敏感数据进行脱敏。

php
use Pin\Support\Str;

Str::maskSensitive('secret123'); // sec******

可以传入字段名,用于判断是否需要脱敏:

php
Str::maskSensitive('secret123', 'password'); // sec******
Str::maskSensitive('secret123', 'name');     // secret123

默认规则:

字段名处理方式
未提供 (null)脱敏处理
包含 password(不区分大小写)脱敏处理
其他保持原值

默认脱敏规则会保留前 3 个字符,并追加 ******

可通过 setSensitiveValueMasker() 自定义脱敏处理逻辑,回调中的 $key 参数表示当前字段名:

php
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() 不同,当字符串键发生冲突时,后面的值会覆盖前面的值,而不是合并为数组。

php
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,
//     ],
// ]

默认情况下,相同数字键会追加新值:

php
Arr::merge(
    [1 => ['one']],
    [1 => ['two']],
);

// [
//     1 => ['one'],
//     2 => ['two'],
// ]
//

如需在数字键冲突时覆盖原值,可将第一个参数设为 true

php
Arr::merge(
    true,
    [1 => ['one']],
    [1 => ['two']],
);

// [
//     1 => ['two'],
// ]

null 转空字符串

Arr::nullToEmptyString() 会递归将数组中的 null 值转换为空字符串。

php
use Pin\Support\Arr;

$data = Arr::nullToEmptyString([
    'name' => null,
    'profile' => [
        'nickname' => null,
    ],
]);

// [
//     'name' => '',
//     'profile' => [
//         'nickname' => '',
//     ],
// ]

扁平数组转树

Arr::toTree() 将扁平数组转换为树结构。

每个元素需要包含父级字段,默认使用 pid

php
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],
]);

返回结果:

php
[
    [
        '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 字段。

可以自定义父级字段和子节点字段:

php
$tree = Arr::toTree(
    $data,
    'parent_id',
    'items',
);

敏感字段脱敏

Arr::maskSensitive() 用于递归脱敏数组中的字段值。

php
use Pin\Support\Arr;

$payload = Arr::maskSensitive([
    'username' => 'root',
    'password' => 'secret123',
    'database' => [
        'Password' => 'db-secret',
    ],
]);

返回:

php
[
    'username' => 'root',
    'password' => 'sec******',
    'database' => [
        'Password' => 'db-******',
    ],
]

脱敏规则参见 Str::maskSensitive()

计时与耗时

Timer 用于记录时间点和计算耗时,Duration 用于表示耗时结果。

请求耗时

php
use Pin\Support\Timer;

$duration = Timer::durationSinceStartOfRequest();

$seconds = $duration->seconds();
$milliseconds = $duration->milliseconds();

默认使用 REQUEST_TIME_FLOAT 作为请求开始时间,也可以手动指定开始时间:

php
$duration = Timer::durationSinceStartOfRequest(microtime(true) - 1);

局部计时

可以使用 Timer 对指定代码片段进行计时:

php
$timer = new Timer();

$timer->start('query');

// do something

$duration = $timer->stop('query'); // Duration

Duration

Duration 用于表示一段时间范围的耗时,并提供耗时和内存变化统计。

php
$duration->seconds();        // 秒,默认保留 4 位小数
$duration->milliseconds();   // 毫秒
$duration->memoryUsage();    // 内存变化(字节)

seconds() 可以指定保留的小数位数:

php
$duration->seconds(2); // 保留 2 位小数

大小单位

Pin\Support\Size 用于在人类可读大小和字节数之间转换。

php
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');   // 2147483648

INFO

toBytes() 根据字符串后缀识别单位,支持 bkkbmmbggb(不区分大小写)。

调用定位

Caller 用于从 PHP 调用栈中解析业务代码调用位置。

php
use Pin\Support\Caller;

$caller = Caller::resolve();

// [
//     'file' => '/app/Services/UserService.php',
//     'line' => 42,
// ]

默认情况下,Caller 会从 debug_backtrace() 获取调用栈,并跳过 vendor 目录中的文件,返回第一个业务代码位置。

如果无法找到业务代码,则返回第一个可用的调用信息。

可通过 setApplicationFileResolver() 自定义业务文件判断规则:

php
Caller::setApplicationFileResolver(
    fn (string $file) => str_contains($file, '/app/')
);

反射访问

Invoker 基于 PHP Reflection,用于访问对象或类的非公开成员。

创建

php
use Pin\Support\Invoker;

$invoker = new Invoker($object);

// 或者
$invoker = new Invoker(SomeClass::class);

属性访问

支持访问实例属性和静态属性:

php
$value = $invoker->name;

$invoker->name = 'Pin';

静态属性:

php
$value = $invoker->config;

$invoker->config = $config;

也可以通过 get()set() 使用点语法访问嵌套属性:

php
$value = $invoker->get('config.database.host');

$invoker->set('config.database.host', 'localhost');

方法调用

可以调用非公开方法:

php
$result = $invoker->hiddenMethod($arg1, $arg2);

静态方法:

php
$result = (new Invoker(SomeClass::class))->hiddenStaticMethod();

基于 MIT 许可发布