packagefactory/extractor

一个流畅的接口,允许在读取的同时验证原始PHP数据结构

v1.0.3 2024-09-02 14:37 UTC

This package is auto-updated.

Last update: 2024-09-16 16:01:43 UTC


README

一个流畅的接口,允许在读取的同时验证原始PHP数据结构

安装

composer require packagefactory/extractor

使用

例如,您有一个类似这样的PHP原生数组结构

$configuration = [
    'mailer' => [
        'transport' => 'smtp',
        'host' => 'smtp.example.com',
        'port' => 465
    ]
];

它包含邮件服务的配置。在许多PHP项目中,配置通常以这种格式出现,通常是通过解析YAML或JSON源来实现的。虽然这些格式易于阅读和写入,但生成的PHP数组数据结构完全缺乏类型安全。

使用此类值对象处理给定配置更为理想:

final class MailerConfiguration
{
    private function __construct(
        public readonly MailerTransport $transport,
        public readonly string $host,
        public readonly int $port
    ) {
    }
}

要将数组结构转换为该对象,可能适合编写一个静态工厂方法

final class MailerConfiguration
{
    /* ... */

    public static function fromArray(array $array): self
    {
        if (!isset($array['transport']) || !is_string($array['transport'])) {
            throw new \Exception('Transport must be a string!');
        }

        if (!isset($array['host']) || !is_string($array['host'])) {
            throw new \Exception('Host must be a string!');
        }

        if (!isset($array['port']) || !is_int($array['port'])) {
            throw new \Exception('Port must be an integer!');
        }

        return new self(
            transport: MailerTransport::from($array['transport']),
            host: $array['host'],
            port: $array['port']
        );
    }
}

遗憾的是,这需要编写大量代码,如果我们还想获得更多有用的错误信息,代码将更加冗长。

这就是Extractor发挥作用的地方。使用Extractor API,我们可以编写如下静态工厂方法:

final class MailerConfiguration
{
    /* ... */

    public static function fromExtractor(Extractor $extractor): self
    {
        return new self(
            transport: MailerTransport::from($extractor['transport']->string()),
            host: $extractor['host']->string(),
            port: $extractor['port']->int()
        );
    }
}

Extractor为我们处理运行时类型检查,并在数据结构不符合我们的假设时抛出有用的错误信息。

为了完成从开始时的示例

$configuration = [
    'mailer' => [
        'transport' => 'smtp',
        'host' => 'smtp.example.com',
        'port' => 465
    ]
];

$mailerConfiguration = MailerConfiguration::fromExtractor(
    Extractor::for($configuration)['mailer']
);

API

类型守卫

boolboolOrNull

Extractor::for(true)->bool(); // returns `true`
Extractor::for(false)->bool(); // returns `false`
Extractor::for(true)->boolOrNull(); // returns `true`
Extractor::for(false)->boolOrNull(); // returns `false`
Extractor::for(null)->boolOrNull(); // returns `null`

检查提供给Extractor的数据是否为布尔值,并在是的情况下返回它。当使用 boolOrNull 时,null 也会通过。

intintOrNull

Extractor::for(42)->int(); // returns `42`
Extractor::for(42)->intOrNull(); // returns `42`
Extractor::for(null)->intOrNull(); // returns `null`

检查提供给Extractor的数据是否为整数,并在是的情况下返回它。当使用 intOrNull 时,null 也会通过。

floatfloatOrNull

Extractor::for(47.11)->float(); // returns `47.11`
Extractor::for(47.11)->floatOrNull(); // returns `47.11`
Extractor::for(null)->floatOrNull(); // returns `null`

检查提供给Extractor的数据是否为浮点数,并在是的情况下返回它。当使用 floatOrNull 时,null 也会通过。

intOrFloatintOrFloatOrNull

Extractor::for(42)->intOrFloat(); // returns `42`
Extractor::for(47.11)->intOrFloat(); // returns `47.11`
Extractor::for(42)->intOrfloatOrNull(); // returns `42`
Extractor::for(47.11)->intOrfloatOrNull(); // returns `47.11`
Extractor::for(null)->intOrfloatOrNull(); // returns `null`

JSON 中,整数和浮点数类型之间没有区别。一切都是 number。这两个方法检查提供给Extractor的数据是否为浮点数或整数(因此是 number),并在是的情况下返回它。当使用 intOrfloatOrNull 时,null 也会通过。

stringstringOrNull

Extractor::for('string')->string(); // returns `"string"`
Extractor::for('string')->stringOrNull(); // returns `"string"`
Extractor::for(null)->stringOrNull(); // returns `null`

检查提供给Extractor的数据是否为字符串,并在是的情况下返回它。当使用 stringOrNull 时,null 也会通过。

arrayarrayOrNull

Extractor::for([])->array(); // returns `[]`
Extractor::for([])->arrayOrNull(); // returns `[]`
Extractor::for(null)->arrayOrNull(); // returns `null`

检查提供给Extractor的数据是否为数组,并在是的情况下返回它。当使用 arrayOrNull 时,null 也会通过。

数组访问

为了处理嵌套数组结构,Extractor实现了\ArrayAccess接口。

如果您有一个包含数组的Extractor,当您访问一个键时,您将收到另一个Extractor实例包裹的该键的值

$extractor = Extractor::for([ 'key' => 'value' ]);
$extractor['key']->string(); // returns `"value"`
$extractor['key']->int(); // throws

如果您访问一个未知键,它将被处理为Extractor::for(null)

$extractor['unknown key']->stringOrNull(); // returns `null`
$extractor['unknown key']->string(); // throws

如果您在不是数组的东西上访问键,Extractor将抛出异常

$extractor = Extractor::for('This is not an array...');
$extractor['key']; // throws

getPath

每个Extractor实例都提供了它被检索的访问路径

$extractor = Extractor::for([
    'some' => [
        'deep' => [
            'path' => '1234'
        ]
    ]
]);

$nested = $extractor['some']['deep']['path'];
var_dump($nested->getPath());
// Output:
// array(3) {
//   [0] =>
//   string(4) "some"
//   [1] =>
//   string(4) "deep"
//   [2] =>
//   string(4) "path"
// }

可迭代

Extractor实现了\IterableAggregate接口,这使得您可以使用foreach遍历它

foreach (Extractor::for([ 'key' => 'value' ]) as $key => $value) {
    $key->string(); // returns `"key"`
    $value->string(); // returns `"value"`

    $key->int(); // throws
}

如您所见,$key$value 本身也是 Extractor 的实例。

如果您尝试迭代一个包装的不是数组的 Extractor,则 Extractor 将会抛出

foreach (Extractor::for('This is not an array...') as $key => $value) { // throws
}

错误处理

Extractor 可能会抛出 ExtractorException 的实例。每个 ExtractorException 都携带了抛出异常的 Extractor 的访问路径,并尝试提供有帮助的错误信息

$extractor = Extractor::for([
    'some' => [
        'deep' => [
            'path' => '1234'
        ]
    ]
]);

try {
    $extractor['some']['deep']['path']->int();
} catch (ExtractorException $e) {
    var_dump($e->getPath());
    // Output:
    // array(3) {
    //   [0] =>
    //   string(4) "some"
    //   [1] =>
    //   string(4) "deep"
    //   [2] =>
    //   string(4) "path"
    // }

    var_dump($e->getMessage());
    // Output:
    // string(65) "Value was expected to be of type int, got string("1234") instead."
}

贡献

我们乐意接受贡献。请发送给我们拉取请求。

许可协议

请参阅 LICENSE