跳至正文

更新日志 2.6.0 — 2026 年 8 月 11 日

重要变更

此版本为库带来了一组新功能:

享受! 🎉


提供映射器配置器

映射器现在可以使用一组开箱即用的配置器,镜像以前版本中引入的规范化器配置器。每一个都可以通过configureWith()方法全局使用,也可以作为用于特定属性的特性在本地使用。

MapToDateTimeFromFormat配置器使用给定的日期格式解析输入字符串,该格式必须遵循DateTimeImmutable::createFromFormat()支持的语法:

use CuyZ\Valinor\Mapper\Configurator\MapToDateTimeFromFormat;
use CuyZ\Valinor\MapperBuilder;
use DateTimeInterface;

final readonly class Event
{
    public function __construct(
        public string $name,

        #[MapToDateTimeFromFormat('d/m/Y')]
        public DateTimeInterface $date,
    ) {}
}

$event = (new MapperBuilder())
    ->mapper()
    ->map(Event::class, [
        'name' => 'Release of legendary album',
        'date' => '08/11/1971', // mapped to a `DateTimeImmutable`
    ]);

MapExplodedStringToList配置器使用给定的分隔符将字符串分解为列表,当输入将列表作为单个分隔字符串(例如来自 CSV 文件或查询参数的值)时,这非常有用:

use CuyZ\Valinor\Mapper\Configurator\MapExplodedStringToList;
use CuyZ\Valinor\MapperBuilder;

final readonly class Product
{
    public function __construct(
        public string $name,

        /** @var list<string> */
        #[MapExplodedStringToList(separator: ',')]
        public array $sizes,
    ) {}
}

$product = (new MapperBuilder())
    ->mapper()
    ->map(Product::class, [
        'name' => 'T-Shirt',
        'sizes' => 'XS,S,M,L,XL', // mapped to `['XS', 'S', 'M', 'L', 'XL']`
    ]);

MapArrayToList配置器会丢弃数组的键并将其值映射到列表,对于输入是关联数组或缺少或无序索引的稀疏列表的情况,应将其作为顺序列表处理:

use CuyZ\Valinor\Mapper\Configurator\MapArrayToList;
use CuyZ\Valinor\MapperBuilder;

final readonly class Basket
{
    public function __construct(
        /** @var list<string> */
        #[MapArrayToList]
        public array $products,
    ) {}
}

$basket = (new MapperBuilder())
    ->mapper()
    ->map(Basket::class, [
        'a' => 'Coffee',
        'b' => 'Tea',
    ]); // mapped to `['Coffee', 'Tea']`

最后, MapFromJson配置器解码 JSON 字符串并将结果交给映射器,以便通常的验证和错误报告仍然适用于解码的值:

use CuyZ\Valinor\Mapper\Configurator\MapFromJson;
use CuyZ\Valinor\MapperBuilder;

final readonly class User
{
    public function __construct(
        public string $name,

        /** @var list<string> */
        #[MapFromJson]
        public array $roles,
    ) {}
}

$user = (new MapperBuilder())
    ->mapper()
    ->map(User::class, [
        'name' => 'John Doe',
        'roles' => '["admin", "editor"]', // mapped to `['admin', 'editor']`
    ]);

标量值转换

四个配置器在映射之前将标量值转换为特定类型: MapAsBool 、 MapAsInt 、 MapAsFloat和MapAsString 。当输入数据携带与目标类型不同的表示形式的值时,它们非常有用,例如在表单提交、CSV 文件或 JSON 有效负载中编码为字符串的数字或布尔值。

用作特性时,会强制转换单个属性,而对于其他每个值,严格规则均保持不变:

use CuyZ\Valinor\Mapper\Configurator\MapAsBool;
use CuyZ\Valinor\Mapper\Configurator\MapAsInt;
use CuyZ\Valinor\MapperBuilder;

final readonly class User
{
    public function __construct(
        public string $name,

        #[MapAsInt]
        public int $age,

        #[MapAsBool(true: ['on', 'yes'], false: ['off', 'no'])]
        public bool $isActive,
    ) {}
}

$user = (new MapperBuilder())
    ->mapper()
    ->map(User::class, [
        'name' => 'John Doe',
        'age' => '42', // mapped to `42`
        'isActive' => 'on', // mapped to `true`
    ]);

还可以使用映射器构建器的新的allowCastingToBoolean() 、 allowCastingToInteger() 、 allowCastingToFloat()和allowCastingToString()方法为给定类型的每个值启用转换。它们提供了比allowScalarValueCasting()更好的控制,它同时放宽了对所有标量类型的严格性:

use CuyZ\Valinor\MapperBuilder;

$age = (new MapperBuilder())
    ->allowCastingToInteger()
    ->mapper()
    ->map('int', '42'); // mapped to `42`

从特定键映射属性

新的MapFromKey属性从特定的源键提供类属性或构造函数/方法参数,而不是将其与属性名称进行匹配:

use CuyZ\Valinor\Mapper\Configurator\MapFromKey;
use CuyZ\Valinor\MapperBuilder;

final readonly class Person
{
    public function __construct(
        public string $name,

        #[MapFromKey('zipCode')]
        public string $postalCode,
    ) {}
}

$person = (new MapperBuilder())
    ->mapper()
    ->map(Person::class, [
        'name' => 'John Doe',
        'zipCode' => '75001', // mapped to `$postalCode`
    ]);

该属性建立在对用户空间开放的轻量级协议之上:任何声明mapKey(string $key): string方法并携带#[AsConverter]属性的属性类都可以重新映射其所在元素的键。这可以方便地分解出重复的转换,例如由多个属性共享的前缀:

#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_PARAMETER)]
#[\CuyZ\Valinor\Mapper\AsConverter]
final class MapWithPrefix
{
    public function __construct(private string $prefix) {}

    public function mapKey(string $key): string
    {
        return $this->prefix . $key;
    }
}

final readonly class Configuration
{
    public function __construct(
        #[MapWithPrefix('app_')] // reads from `app_host`
        public string $host,
        #[MapWithPrefix('app_')] // reads from `app_port`
        public int $port,
    ) {}
}

新的规范化器配置器

三个配置器加入了先前版本中引入的配置器。

当数据格式中使用的名称与 PHP 代码库中使用的名称不同时, NormalizeKeyTo属性会在规范化期间重命名属性的键:

use CuyZ\Valinor\Normalizer\Configurator\NormalizeKeyTo;
use CuyZ\Valinor\Normalizer\Format;
use CuyZ\Valinor\NormalizerBuilder;

final readonly class Address
{
    public function __construct(
        public string $street,

        #[NormalizeKeyTo('town')]
        public string $city,
    ) {}
}

$addressAsArray = (new NormalizerBuilder())
    ->normalizer(Format::array())
    ->normalize(new Address('221B Baker Street', 'London'));

// [
//     'street' => '221B Baker Street',
//     'town' => 'London',
// ]

NormalizeToSingleValue类展平包含单个属性的对象,以便规范化结果不是['someProperty' => 'value']而是简单的'value' 。它可以用作配置器,应用于具有单个属性的每个对象,也可以用作用于特定类或属性的特性:

use CuyZ\Valinor\Normalizer\Configurator\NormalizeToSingleValue;
use CuyZ\Valinor\Normalizer\Format;
use CuyZ\Valinor\NormalizerBuilder;

final readonly class Email
{
    public function __construct(
        public string $email,
    ) {}
}

final readonly class User
{
    public function __construct(
        public string $name,

        #[NormalizeToSingleValue]
        public Email $email,
    ) {}
}

$userAsArray = (new NormalizerBuilder())
    ->normalizer(Format::array())
    ->normalize(new User('John Doe', new Email('john.doe@example.com')));

// [
//     'name' => 'John Doe',
//     'email' => 'john.doe@example.com',
// ]

IgnoreOnNormalization属性从规范化输出中排除某个属性,例如隐藏密码等敏感数据。为了使该属性生效,还必须通过configureWith()在构建器上注册此类的实例:

use CuyZ\Valinor\Normalizer\Configurator\IgnoreOnNormalization;
use CuyZ\Valinor\Normalizer\Format;
use CuyZ\Valinor\NormalizerBuilder;

final readonly class User
{
    public function __construct(
        public string $name,

        #[IgnoreOnNormalization]
        public string $password,
    ) {}
}

$userAsArray = (new NormalizerBuilder())
    ->configureWith(new IgnoreOnNormalization())
    ->normalizer(Format::array())
    ->normalize(new User('John Doe', 's3cr3t'));

// ['name' => 'John Doe']

PHP 内部类的泛型

泛型过去仅限于用户态类,因为 PHP 内部的类或扩展提供的类不能在其自己的源代码中声明@template注释。该库现在为各种类型提供通用签名,包括ArrayObject 、 ArrayIterator 、SPL 数据结构和Ds集合类,因此它们可以像任何其他类一样进行参数化:

use CuyZ\Valinor\MapperBuilder;

$sizes = (new MapperBuilder())
    ->mapper()
    ->map('ArrayObject<string, int>', [
        'S' => 36,
        'M' => 38,
        'L' => 40,
    ]);

这些模板中的每一个都声明了一种默认类型,因此像ArrayObject这样的裸引用可以像以前一样继续解析。


模板的默认类型

@template注释现在可以使用=声明默认类型。引用类时可以省略声明默认类型的模板,在这种情况下使用默认类型:

/**
 * @template TValue
 * @template TMeta of array<string, mixed> = array<string, string>
 */
final readonly class Page
{
    public function __construct(
        /** @var list<TValue> */
        public array $items,

        /** @var TMeta */
        public array $meta,
    ) {}
}

final readonly class SomeClass
{
    public function __construct(
        // `TMeta` is not filled in, its default type is used
        /** @var Page<string> */
        public Page $pageWithDefaultMeta,

        // `TMeta` is filled in, overriding its default type
        /** @var Page<string, array{cursor: int}> */
        public Page $pageWithCursorMeta,
    ) {}
}

默认类型使得可以将模板添加到已在其他地方引用的类中:现有的引用不会填充新模板,继续解析为其默认类型,并且可以在以后变得更加精确。


覆盖不可解析的类型

当属性、参数或返回类型使用库尚无法解析的 PHPStan 或 Psalm 语法时,例如条件类型($a is 1 ? int : null) ,专用的@valinor-var 、 @valinor-param和@valinor-return注释可用于为库提供它理解的类型。它们优先于所有其他注释,因此静态分析工具在库使用覆盖时继续使用它们自己的类型:

final class SomeClass
{
    /**
     * @phpstan-param ($a is 1 ? int : null) $b
     * @valinor-param int|null $b
     */
    public function __construct(
        public readonly int $a,
        public readonly ?int $b,
    ) {}
}

新功能

  • 添加@valinor-*注释来覆盖不可解析的类型 ( 11938c )
  • 添加对@template注释的默认值支持( 0d6efe )
  • 添加映射器构建器方法以转换为标量类型( cdca3f )
  • 添加映射器配置器MapArrayToList ( 1f81fa )
  • 添加映射器配置器MapAsBool ( 65dfed )
  • 添加映射器配置器MapAsFloat ( 84eea9 )
  • 添加映射器配置器MapAsInt ( ea28a7 )
  • 添加映射器配置器MapAsString ( 6b0528 )
  • 添加映射器配置器MapExplodedStringToList ( beb4db )
  • 添加映射器配置器MapFromJson ( 469863 )
  • 添加映射器配置器MapToDateTimeFromFormat ( d6e53b )
  • 添加规范化器配置器IgnoreOnNormalization ( 9769f2 )
  • 添加规范化器配置器NormalizeKeyTo ( 947127 )
  • 添加规范化器配置器NormalizeToSingleValue ( 7c5f13 )
  • 允许将源键与特性映射( 631f66 )
  • 支持 PHP 内部类的泛型 ( 5419b4 )

错误修复

  • 将构造函数声明的模板绑定到正在映射的类型( 0629d8 )

内部的

  • 重构 HTTP 请求映射 ( 578bd5 )
  • 从标量类型中删除canCast()和cast() ( eae3f0 )
  • 统一指定结构的数组和HTTP请求节点构建( 3bb83b )