跳至正文

更新日志 2.2.1 — 2025 年 10 月 13 日

⚠️重要变化

此版本包含大量内部重构,需要修复有关转换器的重要错误。尽管我们尽力提供稳定的版本,但错误可能会从裂缝中溜走。如果是这种情况,请打开一个问题来描述该问题,我们将尽力尽快修复它。

⚠️ 此修复在某些情况下不向后兼容,如下所述。如果您在应用程序中使用映射器转换器,您一定应该仔细阅读以下更改。


提交d9e3cf0是一次漫长旅程的结果,其目标是修复一个非常令人不安的错误,该错误会使映射器转换器在不应该被调用的时候被调用。这可能会导致意外行为,甚至可能导致映射无效数据。

以下面为例:

我们注册一个转换器,如果字符串长度小于 5,它将返回 null。对于要调用的此转换器,目标类型应与string|null类型匹配,因为这是转换器可以返回的类型。

在此示例中,我们想要将一个值映射到string ,该值与转换器返回类型不匹配,因为它不包含null 。这意味着永远不应该调用转换器,因为它可能返回无效值( null永远不会是有效string )。

 (new \CuyZ\Valinor\MapperBuilder())
    ->registerConverter(
        // If the string length is lower than 5, we return `null`
        fn (string $val): ?string => strlen($val) < 5 ? null : $val
    )
    ->mapper()
    ->map('string', 'foo');

在此提交之前,将调用转换器并返回null ,这将引发意外错误:

路径根发生错误:值 null 不是有效字符串。

该错误是由以下行引起的:

if (! $shell->type->matches($converter->returnType)) {
    continue;
}

本来应该是:

if (! $converter->returnType->matches($shell->type)) {
    continue;
}

很容易修复,不是吗?

嗯……实际上没有。因为更改此设置会完全修改转换器的行为,并且该库现在缺少大量信息来正确推断转换器的返回类型。

在某些情况下,这种更改就足够了,但在某些更复杂的情况下,我们现在需要更多信息。

例如,让我们以CamelCaseKeys为例,因为它是在提交之前的文档中编写的:

final class CamelCaseKeys
{
    /**
     * @param array<mixed> $value
     * @param callable(array<mixed>): object $next
     */
    public function map(array $value, callable $next): object { … }
}

该转换器的类型签名有一个大问题: object返回类型意味着转换器可以返回任何内容,只要这是一个对象。这破坏了类型匹配契约,并且永远不应该调用转换器。但确实如此。

这是编写此转换器的新方法:

final class CamelCaseKeys
{
    /**
     * @template T of object
     * @param array<mixed> $value
     * @param callable(array<mixed>): T $next
     * @return T
     */
    public function map(array $value, callable $next): object { … }

现在,由于@template注释,类型匹配约定受到尊重,并且在映射到任何对象时都会调用转换器。

为了能够正确推断转换器的返回类型,我们需要:

  1. 能够理解函数内部的@template注解
  2. 能够使用这些注释静态推断泛型
  3. 将推断的泛型分配给整个转换器
  4. 让系统正确调用转换器管道

这是一项巨大的工作量,需要在上个月进行一些小的更改,以及b7f3e5f和d9e3cf0 。一行代码中的错误需要大量工作,对吗? T_T

好消息是:该库现在比以往任何时候都更强大,因为它现在能够静态推断泛型类型,这可能会在未来带来新的可能性。

现在的坏消息是:在某些情况下,此提交可能会破坏向后兼容性承诺。但由于这仍然是一个(巨大的)错误修复,我们不会发布新的主要版本,尽管它可能会破坏一些现有代码。相反,转换器应该适合使用正确的类型签名。

为了帮助解决这个问题,以下是应应用于文档中编写的转换器示例的差异列表:

CamelCaseKeys

#[\CuyZ\Valinor\Mapper\AsConverter]
#[\Attribute(\Attribute::TARGET_CLASS)]
final class CamelCaseKeys
{
    /**
+    * @template T of object
     * @param array<mixed> $value
-    * @param callable(array<mixed>): object $next
+    * @param callable(array<mixed>): T $next
+    * @return T
     */
    public function map(array $value, callable $next): object
    {
        …
    }
}

RenameKeys

#[\CuyZ\Valinor\Mapper\AsConverter]
#[\Attribute(\Attribute::TARGET_CLASS)]
final class RenameKeys
{
    public function __construct(
        /** @var non-empty-array<non-empty-string, non-empty-string> */
        private array $mapping,
    ) {}

    /**
+    * @template T of object
     * @param array<mixed> $value
-    * @param callable(array<mixed>): object $next
+    * @param callable(array<mixed>): T $next
+    * @return T
     */
    public function map(array $value, callable $next): object
    {
        …
    }
}

爆炸

#[\CuyZ\Valinor\Mapper\AsConverter]
#[\Attribute(\Attribute::TARGET_PROPERTY)]
final class Explode
{
    public function __construct(
        /** @var non-empty-string */
        private string $separator,
    ) {}

    /**
-    * @return array<mixed>
+    * @return list<string>
     */
    public function map(string $value): array
    {
        return explode($this->separator, $value);
    }
}

ArrayToList

#[\CuyZ\Valinor\Mapper\AsConverter]
#[\Attribute(\Attribute::TARGET_PROPERTY)]
final class ArrayToList
{
    /**
     * @template T
-    * @param array<mixed> $value
+    * @param non-empty-array<T> $value
-    * @return list<mixed>
+    * @return non-empty-list<T>
     */
    public function map(array $value): array
    {
        return array_values($value);
    }
}

JsonDecode

#[\CuyZ\Valinor\Mapper\AsConverter]
#[\Attribute(\Attribute::TARGET_PROPERTY)]
final class JsonDecode
{
     /**
+    * @template T
-    * @param callable(mixed): mixed $next
+    * @param callable(mixed): T $next
+    * @return T
     */
    public function map(string $value, callable $next): mixed
    {
        $decoded = json_decode($value, associative: true);

        return $next($decoded);
    }
}

错误修复

  • 使可迭代类型与数组类型不匹配( 27f2e3 )
  • 防止未定义的对象类型匹配无效类型( 4ae98a )
  • 正确处理联合和数组键类型匹配( 71787a )
  • 仅当其返回类型与当前节点( d9e3cf )匹配时才使用转换器

内部的

  • 使用原生函数检测转换器参数值 ( 81b4e5 )
  • 重构类和接口映射过程( ab1350 )
  • 重构定义类型分配以处理泛型类型 ( b7f3e5 )
  • 重构 shell 职责和节点构建器 API ( 63624c )
  • 从代码库中删除异常代码时间戳 ( 460bb2 )
  • 使用INF常量检测默认转换器值 ( 72079b )

其他

  • 增强callable类型解析( 2563a3 )