eboreum/caster

将任何PHP值转换为有意义的、易读的字符串。非常适合类型安全的输出、异常信息、调试过程中的透明度以及类似的事物。还有助于避免固有的问题,例如打印无限循环的引用对象(无限递归)、限制大型数组和长字符串的输出,并防止(可选)输出敏感字符串,如密码。

2.0.0 2024-08-10 10:41 UTC

README

license build Code Coverage PHPStan Level

将任何PHP值转换为有意义的、易读的字符串。非常适合类型安全的输出、异常信息、调试过程中的透明度以及类似的事物。还有助于避免固有的问题,例如打印无限循环的引用对象(无限递归)、限制大型数组和长字符串的输出,并防止(可选)输出敏感字符串,如密码。

为什么使用Eboreum/Caster而不是像XDebug、symfony/var-dumper这样的库呢?

XDebug、symfony/var-dumper等库旨在用于**开发环境**。

Eboreum/Caster适用于任何**所有环境**(开发、测试、预发布、生产)。

使用Eboreum/Caster,您将能够提供有关所有PHP值的详细信息,这在**调试**和**故障场景**中都非常好。您的应用程序中的异常信息是否平淡无奇?使用Eboreum/Caster扩展您的选项和显示值吧!

可以将此包视为扩展的魔术方法__debugInfo(https://php.ac.cn/manual/en/language.oop5.magic.php#object.debuginfo)。然而,与仅使用实现类的内部结构构建有意义的调试信息不同,__debugInfo偶尔(通常是令人厌恶的)需要调用其他类的静态方法,而Caster允许有更多的多样性,包括使用适当的依赖注入的自定义格式化程序。

Eboreum/Caster通过选择性地使用自定义格式化程序,**赋予开发者——您——最大的控制权**来处理、解析和呈现输出。

最后,您可以提供一系列敏感文本字符串,如密码、认证令牌、社会保险号等,防止这些字符串在字符串内部输出。您不希望这些信息出现在错误日志、电子邮件中等等。遇到敏感字符串时,将显示静态长度字符串替换(如******)。

要求

"php": "^8.3",
"ext-mbstring": "*",
"ext-openssl": "*"

有关更多信息,请参阅composer.json文件。

安装

通过Composer(https://packagist.org.cn/packages/eboreum/caster)

composer install eboreum/caster

通过GitHub

git clone git@github.com:eboreum/caster.git

基础知识

类型转换到字符串

数据类型将按以下表格所示进行转换。

输出示例

Echo

示例

<?php

declare(strict_types=1);

use Eboreum\Caster\Caster;

$caster = Caster::create();

echo sprintf(
    "%s\n%s\n%s\n%s",
    $caster->cast(null),
    $caster->cast(true),
    $caster->cast('foo'),
    $caster->cast(new stdClass())
);

$caster = $caster->withIsPrependingType(true);

echo "\n\n";

echo sprintf(
    "%s\n%s\n%s\n%s",
    $caster->cast(null),
    $caster->cast(true),
    $caster->cast('foo'),
    $caster->cast(new stdClass())
);

输出

null
true
"foo"
\stdClass

(null) null
(bool) true
(string(3)) "foo"
(object) \stdClass

异常

示例

<?php

declare(strict_types=1);

use Eboreum\Caster\Caster;

/**
 * @throws InvalidArgumentException
 */
function foo(mixed $value): void
{
    if (false === is_string($value) && false === is_int($value)) {
        throw new InvalidArgumentException(sprintf(
            'Expects argument $value to be a string or an integer. Found: %s',
            Caster::create()->castTyped($value),
        ));
    }
}

try {
    foo(['bar']);
} catch (InvalidArgumentException $e) {
    echo $e->getMessage();
}

输出

Expects argument $value to be a string or an integer. Found: (array(1)) [(int) 0 => (string(3)) "bar"]

用法

\Eboreum\Caster\Caster是不可变的。这是防止篡改Caster类内部结构的良好保护措施。然而,提供了许多with*方法,允许克隆被修改。

建议您在自己的应用程序中实现一个 \My\Application\Caster,它扩展了 \Eboreum\Caster\Caster 并覆盖了 getInstance 方法,从而您可以完全控制自己应用程序的caster实例。

示例

<?php

declare(strict_types=1);

namespace My\Application;

use Eboreum\Caster\Abstraction\Formatter\AbstractArrayFormatter;
use Eboreum\Caster\Caster as EboreumCaster;
use Eboreum\Caster\CharacterEncoding;
use Eboreum\Caster\Collection\Formatter\ArrayFormatterCollection;
use Eboreum\Caster\Contract\CasterInterface;

use function assert;
use function dirname;
use function json_encode;
use function sprintf;

class Caster extends EboreumCaster
{
    private static ?Caster $instance = null;

    public static function getInstance(): self
    {
        if (null === self::$instance) {
            self::$instance = new self(CharacterEncoding::getInstance());

            $instance = self::$instance->withCustomArrayFormatterCollection(new ArrayFormatterCollection([
            new class extends AbstractArrayFormatter
                {
                public function format(CasterInterface $caster, array $array): ?string
                {
                    return 'I am an array!';
                }

                public function isHandling(array $array): bool
                {
                    return true;
                }
            },
            ]));

            assert($instance instanceof Caster);

            self::$instance = $instance;

            // Do more custom configuring before the instance is forever locked and returned
        }

        return self::$instance;
    }
}

echo sprintf(
    'Instances \\%s::getInstance() !== \\%s::getInstance(): %s',
    EboreumCaster::class,
    Caster::class,
    json_encode(EboreumCaster::getInstance() !== Caster::getInstance()),
) . "\n";

echo sprintf(
    'But \\%s::getInstance() === \\%s::getInstance() (same): %s',
    Caster::class,
    Caster::class,
    json_encode(Caster::getInstance() === Caster::getInstance()),
) . "\n";

输出

Instances \Eboreum\Caster\Caster::getInstance() !== \My\Application\Caster::getInstance(): true
But \My\Application\Caster::getInstance() === \My\Application\Caster::getInstance() (same): true

标准格式化器

默认情况下,Eboreum\Caster\Caster::create() 每次都返回一个新实例。如果您希望反复使用相同的实例,您有两个选择。

选项 1:将其存储在变量中并使用它。例如

<?php
use Eboreum\Caster\Caster;

$caster = Caster::create();

选项 2:使用 getInstance

为了方便使用,您可以通过调用 \Eboreum\Caster\Caster::getInstance() 来获取相同的实例。如上所述,建议您在自己的应用程序或库中创建自己的 \My\Application\Caster::getInstance()

使用实际的依赖注入容器

或者,使用实际的依赖注入容器(DIC)如 Pimple。但是,这意味着您必须将依赖项传递到需要的地方,这在 SOLID 视角下是很好的,但并不总是非常实用。

自定义基本格式化器

您可以根据自己的需求自定义格式化器,例如更改字符串样本大小、数组深度,或提供自定义的数组和/或对象格式化器。

示例

<?php

declare(strict_types=1);

use Eboreum\Caster\Caster;
use Eboreum\Caster\Common\DataType\Integer\PositiveInteger;
use Eboreum\Caster\Common\DataType\Integer\UnsignedInteger;
use Eboreum\Caster\Common\DataType\String_\Character;

$caster = Caster::create();
$caster = $caster->withDepthMaximum(new PositiveInteger(2));
$caster = $caster->withArraySampleSize(new UnsignedInteger(3));
$caster = $caster->withStringSampleSize(new UnsignedInteger(4));
$caster = $caster->withStringQuotingCharacter(new Character('`'));

echo '$caster->getDepthMaximum()->toInteger(): ' . $caster->getDepthMaximum()->toInteger() . "\n";
echo '$caster->getArraySampleSize()->toInteger(): ' . $caster->getArraySampleSize()->toInteger() . "\n";
echo '$caster->getStringSampleSize()->toInteger(): ' . $caster->getStringSampleSize()->toInteger() . "\n";
echo '$caster->getStringQuotingCharacter(): ' . $caster->getStringQuotingCharacter() . "\n";

输出

$caster->getDepthMaximum()->toInteger(): 2
$caster->getArraySampleSize()->toInteger(): 3
$caster->getStringSampleSize()->toInteger(): 4
$caster->getStringQuotingCharacter(): `

特定类型的格式化器

以下存在特定类型的格式化器,这可以帮助提供更多信息。特别是对于打印与对象相关的信息非常有用。

使用 with* 方法(返回一个副本)将格式化器(不可变地)添加到 \Eboreum\Caster\Caster

可以提供多个自定义格式化器,以便它们各自处理特定情况。顺序很重要。集合中的第一个元素首先处理。您必须在将集合元素传递给 \Eboreum\Caster\Caster 之前对集合元素进行排序。

最终,所有自定义格式化器都会回退到它们各自的标准格式化器。

包含的对象格式化器

以下对象格式化器现成可用。您可以直接使用它们或扩展它们,提供自己的自定义逻辑。一切都是非常开放-关闭原则。

命名空间: \Eboreum\Caster\Formatter\Object_

自定义数组格式化器

示例

<?php

declare(strict_types=1);

use Eboreum\Caster\Abstraction\Formatter\AbstractArrayFormatter;
use Eboreum\Caster\Caster;
use Eboreum\Caster\Collection\Formatter\ArrayFormatterCollection;
use Eboreum\Caster\Contract\CasterInterface;

$caster = Caster::create();
$caster = $caster->withCustomArrayFormatterCollection(new ArrayFormatterCollection([
    new class extends AbstractArrayFormatter
    {
        public function format(CasterInterface $caster, array $array): ?string
        {
            if (false === $this->isHandling($array)) {
                return null; // Pass on to next formatter or lastly DefaultArrayFormatter
            }

            if (1 === count($array)) {
                /*
                 * /!\ CAUTION /!\
                 * Do NOT do this in practice! You disable sensitive string masking.
                 */
                return print_r($array, true);
            }

            if (2 === count($array)) {
                return 'I am an array!';
            }

            if (3 === count($array)) {
                $array[0] = 'SURPRISE!';

                // Override and use DefaultArrayFormatter for rendering output
                return $caster->getDefaultArrayFormatter()->format($caster, $array);
            }

            return null; // Pass on to next formatter or lastly DefaultArrayFormatter
        }

        public function isHandling(array $array): bool
        {
            return true;
        }
    },
]));

echo $caster->cast(['foo']) . "\n";

echo $caster->cast(['foo', 'bar']) . "\n";

echo $caster->cast(['foo', 'bar', 'baz']) . "\n";

echo $caster->cast(['foo', 'bar', 'baz', 'bim']) . "\n";

echo $caster->castTyped(['foo', 'bar', 'baz', 'bim']) . "\n";

输出

Array
(
    [0] => foo
)

I am an array!
[0 => "SURPRISE!", 1 => "bar", 2 => "baz"]
[0 => "foo", 1 => "bar", 2 => "baz", ... and 1 more element] (sample)
(array(4)) [(int) 0 => (string(3)) "foo", (int) 1 => (string(3)) "bar", (int) 2 => (string(3)) "baz", ... and 1 more element] (sample)

自定义对象格式化器

在这个例子中,使用了 \DateTimeInterface\Throwable 来提供良好的实际用例。

示例

<?php

declare(strict_types=1);

use Eboreum\Caster\Abstraction\Formatter\AbstractObjectFormatter;
use Eboreum\Caster\Caster;
use Eboreum\Caster\Collection\Formatter\ObjectFormatterCollection;
use Eboreum\Caster\Contract\CasterInterface;

$caster = Caster::create();

$caster = $caster->withCustomObjectFormatterCollection(new ObjectFormatterCollection([
    new class extends AbstractObjectFormatter
    {
        public function format(CasterInterface $caster, object $object): ?string
        {
            if (false === $this->isHandling($object)) {
                return null; // Pass on to next formatter or lastly DefaultObjectFormatter
            }

            assert($object instanceof DateTimeInterface);

            return sprintf(
                '%s (%s)',
                Caster::makeNormalizedClassName(new ReflectionObject($object)),
                $object->format('c'),
            );
        }

        public function isHandling(object $object): bool
        {
            return ($object instanceof DateTimeInterface);
        }
    },
    new class extends AbstractObjectFormatter
    {
        public function format(CasterInterface $caster, object $object): ?string
        {
            if (false === $this->isHandling($object)) {
                return null; // Pass on to next formatter or lastly DefaultObjectFormatter
            }

            assert($object instanceof Throwable);

            return sprintf(
                '%s {$code = %s, $file = %s, $line = %s, $message = %s}',
                Caster::makeNormalizedClassName(new ReflectionObject($object)),
                $caster->cast($object->getCode()),
                $caster->cast('.../' . basename($object->getFile())),
                $caster->cast($object->getLine()),
                $caster->cast($object->getMessage()),
            );
        }

        public function isHandling(object $object): bool
        {
            return ($object instanceof Throwable);
        }
    },
]));

echo $caster->cast(new stdClass()) . "\n";

echo $caster->cast(new DateTimeImmutable('2019-01-01T00:00:00+00:00')) . "\n";

echo $caster->cast(new RuntimeException('test', 1)) . "\n";

输出

\stdClass
\DateTimeImmutable (2019-01-01T00:00:00+00:00)
\RuntimeException {$code = 1, $file = ".../example-custom-object-formatter.php", $line = 68, $message = "test"}

自定义资源格式化器

示例

<?php

declare(strict_types=1);

use Eboreum\Caster\Abstraction\Formatter\AbstractResourceFormatter;
use Eboreum\Caster\Caster;
use Eboreum\Caster\Collection\Formatter\ResourceFormatterCollection;
use Eboreum\Caster\Common\DataType\Resource_;
use Eboreum\Caster\Contract\CasterInterface;

$caster = Caster::create();

$caster = $caster->withCustomResourceFormatterCollection(new ResourceFormatterCollection([
    new class extends AbstractResourceFormatter
    {
        public function format(CasterInterface $caster, Resource_ $resource): ?string
        {
            if (false === $this->isHandling($resource)) {
                return null; // Pass on to next formatter or lastly DefaultResourceFormatter
            }

            if ('stream' === get_resource_type($resource->getResource())) {
                return sprintf(
                    'opendir/fopen/tmpfile/popen/fsockopen/pfsockopen %s',
                    preg_replace(
                        '/^(Resource id) #\d+$/',
                        '$1 #42',
                        (string)$resource->getResource(),
                    ),
                );
            }

            return null; // Pass on to next formatter or lastly DefaultResourceFormatter
        }
    },
    new class extends AbstractResourceFormatter
    {
        public function format(CasterInterface $caster, Resource_ $resource): ?string
        {
            if (false === $this->isHandling($resource)) {
                return null; // Pass on to next formatter or lastly DefaultResourceFormatter
            }

            if ('xml' === get_resource_type($resource->getResource())) {
                $identifier = preg_replace(
                    '/^(Resource id) #\d+$/',
                    '$1 #42',
                    (string)$resource->getResource(),
                );

                assert(is_string($identifier));

                return sprintf(
                    'XML %s',
                    $identifier,
                );
            }

            return null; // Pass on to next formatter or lastly DefaultResourceFormatter
        }
    },
]));

echo $caster->cast(fopen(__FILE__, 'r+')) . "\n";

输出

opendir/fopen/tmpfile/popen/fsockopen/pfsockopen Resource id #42

自定义字符串格式化器

示例

<?php

declare(strict_types=1);

use Eboreum\Caster\Abstraction\Formatter\AbstractStringFormatter;
use Eboreum\Caster\Caster;
use Eboreum\Caster\Collection\Formatter\StringFormatterCollection;
use Eboreum\Caster\Contract\CasterInterface;

$caster = Caster::create();
$caster = $caster->withCustomStringFormatterCollection(new StringFormatterCollection([
    new class extends AbstractStringFormatter
    {
        public function format(CasterInterface $caster, string $string): ?string
        {
            if (false === $this->isHandling($string)) {
                return null; // Pass on to next formatter or lastly DefaultStringFormatter
            }

            if ('What do we like?' === (string)$string) {
                return $caster->cast('CAKE!');
            }

            return null; // Pass on to next formatter or lastly DefaultStringFormatter
        }

        public function isHandling(string $string): bool
        {
            return true;
        }
    },
]));

echo $caster->cast('What do we like?') . "\n";

echo $caster->castTyped('Mmmm, cake') . "\n";

输出

"CAKE!"
(string(10)) "Mmmm, cake"

隐藏敏感子串

您可以隐藏敏感字符串,如密码、身份验证令牌、社会保险号等。

在下面的示例中,请注意 "345" 和 "456" 是重叠的,导致这两个字符串的乘积 "3456" 被屏蔽。虽然这可能潜在地揭示了其中一个敏感字符串是另一个字符串的一部分,但这比只屏蔽其中一个字符串并将第二个敏感字符串的其余部分显示在纯文本输出中要小得多。

将按以下顺序屏蔽:最长的敏感字符串到最短的。这意味着对于敏感字符串 "foo""foobar",首先处理 "foobar"。这个字符串,"foobarbaz",将变成 "******baz",而在这个例子中,"foo" 永远不会被处理。字符串 "foob foobarbaz" 将变成 "******b ******baz",依此类推。

敏感字符串被 加密(IV 和盐在运行时随机生成),这样如果 Eboreum/Caster 内部失败,它将不会在明文中泄露敏感字符串。

当使用 \Eboreum\Caster\Caster->cast(...) 时,任何字符串在传递到字符串格式化器(实现 StringFormatterInterface 的类)之前都会被屏蔽。

示例

<?php

declare(strict_types=1);

use Eboreum\Caster\Caster;
use Eboreum\Caster\Collection\EncryptedStringCollection;
use Eboreum\Caster\EncryptedString;

$caster = Caster::create();
$caster = $caster->withMaskedEncryptedStringCollection(new EncryptedStringCollection([
    new EncryptedString('bar'),
    new EncryptedString('bim'),
    new EncryptedString('345'),
    new EncryptedString('456'),
]));

echo $caster->castTyped('foo bar baz bim bum') . "\n"; // Notice: Original string length is not revealed

echo "\n\n";

echo $caster->castTyped('0123456789') . "\n"; // Notice: 3456 are masked because 345 and 456 overlap

输出

(string(25)) "foo ****** baz ****** bum" (masked)


(string(12)) "012******789" (masked)

测试

测试/开发要求

"beberlei/assert": "^3.3",
"nikic/php-parser": "^5.0",
"phpstan/phpstan": "1.11.5",
"phpunit/phpunit": "11.2.5",
"slevomat/coding-standard": "8.15.0",
"squizlabs/php_codesniffer": "3.10.1"

运行测试

对于所有单元测试,首先按照以下步骤操作

cd tests
php ../vendor/bin/phpunit

PHPStan

抑制代码

对于一些情况,我们需要抑制 PHPStan 的输出,原因有很多。我们努力避免使用 @phpstan-ignore-line(以及 @phpstan-ignore-next-line 和类似),但在极少数情况下——主要是在测试中——这是不可能的,因为我们测试的东西正是 PHPStan 不喜欢的。

许可 & 声明

请参阅LICENSE文件。基本上:使用此库风险自负。

贡献

我们更希望你在https://github.com/eboreum/caster创建一个工单或拉取请求,并在此处讨论功能或错误。

请不要https://packagist.org.cn/packages/eboreum/exceptional重新集成到本项目。我们不希望有双向依赖,因为eboreum/exceptional使用了eboreum/caster。

致谢

作者

感谢

来源于并替代:https://packagist.org.cn/packages/kafoso/type-formatter (https://github.com/kafoso/type-formatter)。