跳至正文

验证与错误处理

验证和错误处理是映射过程的重要方面:必须信任将非结构化输入转换为严格类型的数据结构。如果发现无效值,则无法保证所需结构的有效性,因此映射将失败。

当映射失败时,会抛出\CuyZ\Valinor\Mapper\MappingError类型的异常,其中包含有关映射过程中遇到的所有错误的信息。因此,始终建议使用 try/catch 语句包装映射函数调用并正确处理错误。

try {
    (new \CuyZ\Valinor\MapperBuilder())
        ->mapper()
        ->map(SomeClass::class, [/* … */ ]);
} catch (\CuyZ\Valinor\Mapper\MappingError $error) {
    // Get a flattened list of all messages detected during mapping
    $messages = $error->messages();

    // Formatters can be added and will be applied on all messages
    $messages = $messages->formatWith(
        new \CuyZ\Valinor\Mapper\Tree\Message\Formatter\MessageMapFormatter([
            // …
        ]),
        (new \CuyZ\Valinor\Mapper\Tree\Message\Formatter\TranslationMessageFormatter())
            ->withTranslations([
                // …
            ])
    );

    foreach ($messages as $message) {
        echo $message;
    }
}

自定义异常消息

更具体的验证应该在对象的构造函数中完成,如果给定的数据有问题,则抛出异常。

出于安全原因,映射器不会捕获构造函数中引发的异常,除非使用以下三个选项之一。

1. 自定义异常类

可以抛出实现\CuyZ\Valinor\Mapper\Tree\Message\ErrorMessage的异常。正文可以包含占位符,请参阅消息自定义章节以获取更多信息。

如果可以提供更多参数,异常还可以实现接口\CuyZ\Valinor\Mapper\Tree\Message\HasParameters该接口返回字符串值列表,使用键作为参数名称。

为了帮助识别错误,可以通过实现接口CuyZ\Valinor\Mapper\Tree\Message\HasCode来提供唯一代码。

final readonly class SomeClass
{
    public function __construct(private string $value)
    {
        if ($this->value === 'foo') {
            throw new SomeException('some custom parameter');
        }
    }
}

use CuyZ\Valinor\Mapper\Tree\Message\ErrorMessage;
use CuyZ\Valinor\Mapper\Tree\Message\HasCode;
use CuyZ\Valinor\Mapper\Tree\Message\HasParameters;

final class SomeException extends DomainException implements ErrorMessage, HasParameters, HasCode
{
    private string $someParameter;

    public function __construct(string $someParameter)
    {
        parent::__construct();

        $this->someParameter = $someParameter;
    }

    public function body() : string
    {
        return 'Some custom message / {some_parameter} / {source_value}';
    }

    public function parameters(): array
    {
        return [
            'some_parameter' => $this->someParameter,
        ];
    }

    public function code() : string
    {
        // A unique code that can help to identify the error
        return 'some_unique_code';
    }
}

try {
    (new \CuyZ\Valinor\MapperBuilder())->mapper()->map(SomeClass::class, 'foo');
} catch (\CuyZ\Valinor\Mapper\MappingError $exception) {
    // Should print:
    // Some custom message / some custom parameter / 'foo'
    echo $exception->messages()->toArray()[0];
}

2. 使用提供的消息生成器

实用程序类\CuyZ\Valinor\Mapper\Tree\Message\MessageBuilder可用于构建消息。

final readonly class SomeClass
{
    public function __construct(private string $value)
    {
        if (str_starts_with($this->value, 'foo_')) {
            throw \CuyZ\Valinor\Mapper\Tree\Message\MessageBuilder::newError(
                'Some custom error message: {value}.'
            )
            ->withCode('some_code')
            ->withParameter('value', $this->value)
            ->build();
        }
    }
}

try {
    (new \CuyZ\Valinor\MapperBuilder())->mapper()->map(
        SomeClass::class, 'foo_bar'
    );
} catch (\CuyZ\Valinor\Mapper\MappingError $exception) {
    // Should print:
    // > Some custom error message: foo_bar.
    echo $exception->messages()->toArray()[0];
}

3. 允许第三方异常

可以设置映射器可以捕获的异常列表,例如在使用 [Webmozart Assert] 等轻量级验证工具时。

建议谨慎使用此功能:用户态异常可能包含敏感信息 - 例如,绝不允许显示查询的一部分的 SQL 异常。因此,只应过滤精心挑选的例外情况的详尽列表。

final readonly class SomeClass
{
    public function __construct(private string $value)
    {
        \Webmozart\Assert\Assert::startsWith($value, 'foo_');
    }
}

try {
    (new \CuyZ\Valinor\MapperBuilder())
        ->filterExceptions(function (Throwable $exception) {
            if ($exception instanceof \Webmozart\Assert\InvalidArgumentException) {
                return \CuyZ\Valinor\Mapper\Tree\Message\MessageBuilder::from($exception);
            } 

            // If the exception should not be caught by this library, it
            // must be thrown again.
            throw $exception;
        })
        ->mapper()
        ->map(SomeClass::class, 'bar_baz');
} catch (\CuyZ\Valinor\Mapper\MappingError $exception) {
    // Should print something similar to:
    // > Expected a value to start with "foo_". Got: "bar_baz"
    echo $exception->messages()->toArray()[0];
}