更新日志 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 不是有效字符串。
该错误是由以下行引起的:
本来应该是:
很容易修复,不是吗?
嗯……实际上没有。因为更改此设置会完全修改转换器的行为,并且该库现在缺少大量信息来正确推断转换器的返回类型。
在某些情况下,这种更改就足够了,但在某些更复杂的情况下,我们现在需要更多信息。
例如,让我们以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注释,类型匹配约定受到尊重,并且在映射到任何对象时都会调用转换器。
为了能够正确推断转换器的返回类型,我们需要:
- 能够理解函数内部的
@template注解 - 能够使用这些注释静态推断泛型
- 将推断的泛型分配给整个转换器
- 让系统正确调用转换器管道
这是一项巨大的工作量,需要在上个月进行一些小的更改,以及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 )