卡福索/类型格式化器

一个简洁、轻量级的库,用于将PHP数据类型转换为可读的字符串。非常适合安全输出类型、异常消息、调试透明度以及类似的功能。同时有助于避免打印递归对象和大型数组等固有问题的发生。

2.2.0 2024-03-08 13:06 UTC

This package is auto-updated.

Last update: 2024-09-08 14:16:35 UTC


README

一个简洁、轻量级的库,用于将PHP数据类型转换为可读的字符串。非常适合安全输出类型、异常消息、调试透明度以及类似的功能。同时有助于避免打印递归对象和大型数组等固有问题的发生。

需求

"php": ">=7.2",
"ext-mbstring": ">=7.2",
"doctrine/collections": "^1"

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

许可证 & 免责声明

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

安装

通过 Composer (https://packagist.org.cn/packages/kafoso/type-formatter)

composer install kafoso/type-formatter

通过GitHub

git clone git@github.com:kafoso/type-formatter.git

基础

类型转换为字符串

数据类型转换如下表所示。

输出示例

输出

<?php
use Kafoso\TypeFormatter\TypeFormatter;

$typeFormatter = TypeFormatter::create();

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

/**
 * Will output:
 * null true "foo" \stdClass
 */

异常

<?php
use Kafoso\TypeFormatter\TypeFormatter;

/**
 * @param string|int $value
 * @throws \InvalidArgumentException
 */
function foo($value){
    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",
            TypeFormatter::create()->typeCast($value)
        ));
    }
};

foo(["bar"]);

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

用法

\Kafoso\TypeFormatter\TypeFormatter 是不可变的。因此,它只能在构造时进行配置。

标准格式化器

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

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

<?php
use Kafoso\TypeFormatter\TypeFormatter;

$typeFormatter = TypeFormatter::create();

选项 2: 使用依赖注入容器;见下文。

依赖注入容器(默认值 & 变体)

为了方便使用,您可以将格式化器静态存储在 Kafoso\TypeFormatter\TypeFormatter 中。

您可以指定两种类型的依赖关系。

默认

<?php
use Kafoso\TypeFormatter\TypeFormatter;

$typeFormatter = TypeFormatter::getDefault();
$typeFormatter = $typeFormatter->withArrayDepthMaximum(2);
TypeFormatter::setDefault($typeFormatter);

变体

<?php
use Kafoso\TypeFormatter\TypeFormatter;

$typeFormatter = TypeFormatter::create();
$typeFormatter = $typeFormatter->withArrayDepthMaximum(3);
TypeFormatter::setVariation("variation1", $typeFormatter);
$typeFormatter = TypeFormatter::getVariation("variation1");

使用真实的依赖注入容器

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

自定义基本格式化器

您可以根据特定需求自定义格式化器,例如更改字符串样本大小、数组深度或提供自定义的数组和/或对象格式化器。之后,您可以将它存储为默认值或变体以供以后重用。

<?php
use Kafoso\TypeFormatter\Encoding;
use Kafoso\TypeFormatter\TypeFormatter;

$customTypeFormatter = TypeFormatter::create();
$customTypeFormatter = $customTypeFormatter->withArrayDepthMaximum(2);
$customTypeFormatter = $customTypeFormatter->withArraySampleSize(3);
$customTypeFormatter = $customTypeFormatter->withStringSampleSize(4);
$customTypeFormatter = $customTypeFormatter->withStringQuotingCharacter("`");

特定类型的格式化器

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

这些格式化器通过使用 with* 方法注入到 \Kafoso\TypeFormatter\TypeFormatter 的所需实例中。请注意,\Kafoso\TypeFormatter\TypeFormatter 是不可变的。

可以提供多个自定义格式化器,以便它们各自处理特定情况。顺序很重要。

最后,所有自定义格式化器都将回退到相应的标准格式化器。

包含的对象格式化器

以下对象格式化器 readily 可用。您可以使用它们“按原样”或扩展它们,提供自己的自定义逻辑。一切都非常符合开放封闭原则。

命名空间: \Kafoso\TypeFormatter\Type\Objects

自定义数组格式化器

<?php
use Kafoso\TypeFormatter\Abstraction\Type\AbstractFormatter;
use Kafoso\TypeFormatter\Collection\Type\ArrayFormatterCollection;
use Kafoso\TypeFormatter\Encoding;
use Kafoso\TypeFormatter\Type\ArrayFormatterInterface;
use Kafoso\TypeFormatter\TypeFormatter;

$customTypeFormatter = TypeFormatter::create();
$customTypeFormatter = $customTypeFormatter->withCustomArrayFormatterCollection(new ArrayFormatterCollection([
    new class extends AbstractFormatter implements ArrayFormatterInterface
    {
        /**
         * @inheritDoc
         */
        public function format(array $array): ?string
        {
            if (1 == count($array)) {
                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 $this->getTypeFormatter()->getDefaultArrayFormatter()->format($array);
            }
            return null; // Pass on to next formatter or lastly DefaultArrayFormatter
        }
    }
]));

echo $customTypeFormatter->cast(["foo"]) . PHP_EOL;

/**
 * Will output:
 * Array
 * (
 *     [0] => foo
 * )
 */

echo $customTypeFormatter->cast(["foo", "bar"]) . PHP_EOL;

/**
 * Will output:
 * I am an array!
 */

echo $customTypeFormatter->cast(["foo", "bar", "baz"]) . PHP_EOL;

/**
 * Will output:
 * [0 => "SURPRISE!", 1 => "bar", 2 => "baz"]
 */

echo $customTypeFormatter->cast(["foo", "bar", "baz", "bim"]) . PHP_EOL;

/**
 * Will output:
 * [0 => "foo", 1 => "bar", 2 => "baz", ... and 1 more element] (sample)
 */

echo $customTypeFormatter->typeCast(["foo", "bar", "baz", "bim"]) . PHP_EOL;

/**
 * Will output:
 * (array(4)) [(int) 0 => (string(3)) "foo", (int) 1 => (string(3)) "bar", (int) 2 => (string(3)) "baz", ... and 1 more element] (sample)
 */

自定义对象格式化器

在本例中,使用了 \DateTimeInterface\Throwable 以及Doctrine ORM 的 EntityManager 来提供优秀的真实世界用例。

<?php
use Doctrine\Common\Persistence\Proxy;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Kafoso\TypeFormatter\Abstraction\Type\AbstractFormatter;
use Kafoso\TypeFormatter\Collection\Type\ObjectFormatterCollection;
use Kafoso\TypeFormatter\Encoding;
use Kafoso\TypeFormatter\Type\DefaultObjectFormatter;
use Kafoso\TypeFormatter\Type\ObjectFormatterInterface;
use Kafoso\TypeFormatter\TypeFormatter;
use PHPUnit\Framework\MockObject\Generator;
use PHPUnit\Framework\TestCase;

$generator = new Generator;
$entityManager = $generator->getMock(EntityManager::class, [], [], '', false);
$metadataFactory = $generator->getMock(ClassMetadataFactory::class, [], [], '', false);
$metadataFactory
    ->expects(TestCase::any())
    ->method('isTransient')
    ->withConsecutive(TestCase::equalTo('User'), TestCase::equalTo('stdClass'))
    ->willReturnOnConsecutiveCalls(TestCase::returnValue(false), TestCase::returnValue(true));
$entityManager
    ->expects(TestCase::any())
    ->method('getMetadataFactory')
    ->will(TestCase::returnValue($metadataFactory));

$customTypeFormatter = TypeFormatter::create();
$customTypeFormatter = $customTypeFormatter->withCustomObjectFormatterCollection(new ObjectFormatterCollection([
    new class ($entityManager) extends AbstractFormatter implements ObjectFormatterInterface
    {
        /**
         * @inheritDoc
         */
        public function format($object): ?string
        {
            if (false == is_object($object)) {
                return null; // Pass on to next formatter or lastly DefaultObjectFormatter
            }
            if ($object instanceof \DateTimeInterface) {
                return sprintf(
                    "\\%s (%s)",
                    DefaultObjectFormatter::getClassName($object),
                    $object->format("c")
                );
            }
            return null; // Pass on to next formatter or lastly DefaultObjectFormatter
        }
    },
    new class extends AbstractFormatter implements ObjectFormatterInterface
    {
        /**
         * @inheritDoc
         */
        public function format($object): ?string
        {
            if (false == is_object($object)) {
                return null; // Pass on to next formatter or lastly DefaultObjectFormatter
            }
            if ($object instanceof \Throwable) {
                return sprintf(
                    "\\%s {\$code = %s, \$file = %s, \$line = %s, \$message = %s}",
                    DefaultObjectFormatter::getClassName($object),
                    $this->getTypeFormatter()->cast($object->getCode()),
                    $this->getTypeFormatter()->cast($object->getFile(), false),
                    $this->getTypeFormatter()->cast($object->getLine()),
                    $this->getTypeFormatter()->cast($object->getMessage())
                );
            }
            return null; // Pass on to next formatter or lastly DefaultObjectFormatter
        }
    },
    new class ($entityManager) extends AbstractFormatter implements ObjectFormatterInterface
    {
        /**
         * @var EntityManager
         */
        private $entityManager;

        public function __construct(EntityManager $entityManager)
        {
            $this->entityManager = $entityManager;
        }

        /**
         * @inheritDoc
         */
        public function format($object): ?string
        {
            if (false == is_object($object)) {
                return null; // Pass on to next formatter or lastly DefaultObjectFormatter
            }
            $className = ($object instanceof Proxy) ? get_parent_class($object) : DefaultObjectFormatter::getClassName($object);
            $isEntity = (false == $this->entityManager->getMetadataFactory()->isTransient($className));
            $id = null;
            if ($isEntity && method_exists($object, 'getId')) {
                // You may of course implement logic, which can extract and present any @ORM\Id columns, even composite IDs.
                $id = $object->getId();
            }
            if (is_int($id)) {
                return sprintf(
                    "\\%s {\$id = %d}",
                    $className,
                    $id
                );
            }
            return null; // Pass on to next formatter or lastly DefaultObjectFormatter
        }
    },
]));

echo $customTypeFormatter->cast(new \stdClass) . PHP_EOL;

/**
 * Will output (standard TypeFormatter object-to-string output):
 * \stdClass
 */

echo $customTypeFormatter->cast(new \DateTimeImmutable("2019-01-01T00:00:00+00:00")) . PHP_EOL;

/**
 * Will output:
 * \DateTimeImmutable ("2019-01-01T00:00:00+00:00")
 */

class User
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     */
    private $id = null;

    public function getId(): ?int
    {
        return $this->id;
    }
}
$doctrineEntity = new \User;

// Pretend we fetch it from a database
$reflectionObject = new \ReflectionObject($doctrineEntity);
$reflectionProperty = $reflectionObject->getProperty("id");
$reflectionProperty->setAccessible(true);
$reflectionProperty->setValue($doctrineEntity, 1);

echo $customTypeFormatter->cast($doctrineEntity) . PHP_EOL;

/**
 * Will output:
 * \User {$id = 1}
 */

echo $customTypeFormatter->cast(new \RuntimeException("test", 1)) . PHP_EOL;

/**
 * Will output:
 * \RuntimeException {$code = 1, $file = "<file>", $line = <line>, $message = "test"}
 * , where:
 *    - <file> is the path this this file.
 *    - <line> is the line number at which the \RuntimeException is instantiated.
 */

自定义资源格式化器

<?php
use Kafoso\TypeFormatter\Abstraction\Type\AbstractFormatter;
use Kafoso\TypeFormatter\Collection\Type\ResourceFormatterCollection;
use Kafoso\TypeFormatter\Encoding;
use Kafoso\TypeFormatter\Type\ResourceFormatterInterface;
use Kafoso\TypeFormatter\TypeFormatter;

$customTypeFormatter = TypeFormatter::create();
$customTypeFormatter = $customTypeFormatter->withCustomResourceFormatterCollection(new ResourceFormatterCollection([
    new class extends AbstractFormatter implements ResourceFormatterInterface
    {
        /**
         * @inheritDoc
         */
        public function format($resource): ?string
        {
            if (false == is_resource($resource)) {
                return null; // Pass on to next formatter or lastly DefaultResourceFormatter
            }
            if ("stream" === get_resource_type($resource)) {
                return "opendir/fopen/tmpfile/popen/fsockopen/pfsockopen {$resource}";
            }
            return null; // Pass on to next formatter or lastly DefaultResourceFormatter
        }
    },
    new class extends AbstractFormatter implements ResourceFormatterInterface
    {
        /**
         * @inheritDoc
         */
        public function format($resource): ?string
        {
            if (false == is_resource($resource)) {
                return null; // Pass on to next formatter or lastly DefaultResourceFormatter
            }
            if ("xml" === get_resource_type($resource)) {
                return "XML {$resource}";
            }
            return null; // Pass on to next formatter or lastly DefaultResourceFormatter
        }
    },
]));

echo $customTypeFormatter->cast(fopen(__FILE__, "r+")) . PHP_EOL;

/**
 * Will output:
 * opendir/fopen/tmpfile/popen/fsockopen/pfsockopen Resource id #<id>
 */

echo $customTypeFormatter->cast(\xml_parser_create("UTF-8")) . PHP_EOL;

/**
 * Will output:
 * XML Resource id #<id>
 */

自定义字符串格式化器

<?php
use Kafoso\TypeFormatter\Abstraction\Type\AbstractFormatter;
use Kafoso\TypeFormatter\Collection\Type\StringFormatterCollection;
use Kafoso\TypeFormatter\Encoding;
use Kafoso\TypeFormatter\Type\StringFormatterInterface;
use Kafoso\TypeFormatter\TypeFormatter;

$customTypeFormatter = TypeFormatter::create();
$customTypeFormatter = $customTypeFormatter->withCustomStringFormatterCollection(new StringFormatterCollection([
    new class extends AbstractFormatter implements StringFormatterInterface
    {
        /**
         * @inheritDoc
         */
        public function format(string $string): ?string
        {
            if ("What do we like?" === $string) {
                return $this->getTypeFormatter()->getDefaultStringFormatter()->format("CAKE!");
            }
            return null; // Pass on to next formatter or lastly DefaultStringFormatter
        }
    },
]));

echo $customTypeFormatter->cast("What do we like?") . PHP_EOL;

/**
 * Will output:
 * "CAKE!"
 */

测试

单元测试tests/tests/Test/Unit)将在符合基本要求的所有环境中运行。

运行测试

对于所有测试,首先遵循以下步骤

单元测试 将在大多数系统上运行。

cd tests
php ../bin/phpunit tests/Test/Unit

鸣谢

作者