跳至正文

使用自定义对象构造器

对象可能有自定义的创建方式,在这种情况下,需要将这些构造函数注册到映射器才能使用。构造函数可以是:

  1. 命名构造函数,也称为静态工厂方法
  2. 服务的方法——例如存储库
  3. “可调用对象”——声明__invoke方法的类
  4. 任何其他可调用的——包括匿名函数

无论如何,可调用对象的返回类型将由映射器解析以了解何时使用它。可以提供任何参数,并将使用给定的源自动映射。然后可以使用这些参数以所需的方式实例化对象。

如果注册了多个构造函数,它们必须提供不同的签名以防止映射期间发生冲突 - 这意味着如果两个构造函数需要多个具有完全相同名称的参数,则映射将失败。

注意

为类注册构造函数将阻止映射器处理其原生构造函数( __construct方法)。如果需要再次启用它,则必须显式注册。

Constructor特性

注册构造函数最快、最简单的方法是使用Constructor特性,该特性会将方法标记为映射器可以使用的构造函数。

目标方法必须是公共的、静态的,并返回它所属类的实例。

final readonly class Email
{
    // When another constructor is registered for the class, the native
    // constructor is disabled. To enable it again, it is mandatory to
    // explicitly register it again.
    #[\CuyZ\Valinor\Mapper\Object\Constructor]
    public function __construct(public string $value) {}

    #[\CuyZ\Valinor\Mapper\Object\Constructor]
    public static function createFrom(string $userName, string $domainName): self
    {
        return new self($userName . '@' . $domainName);
    }
}

(new \CuyZ\Valinor\MapperBuilder())
    ->mapper()
    ->map(Email::class, [
        'userName' => 'john.doe',
        'domainName' => 'example.com',
    ]); // john.doe@example.com

手动注册构造函数

在某些情况下,无法使用Constructor特性,例如,当该类是无法修改的外部依赖项时。在这种情况下, registerConstructor方法可用于将任何可调用对象注册为构造函数。

(new \CuyZ\Valinor\MapperBuilder())
    ->registerConstructor(
        // When another constructor is registered for the class, the native
        // constructor is disabled. To enable it again, it is mandatory to
        // explicitly register it again by giving the class name to this method.
        Color::class,

        // Register a named constructor
        Color::fromHex(...),

        /**
         * An anonymous function can also be used, for instance when the desired
         * object is an external dependency that cannot be modified.
         * 
         * @param 'red'|'green'|'blue' $color
         * @param 'dark'|'light' $darkness
         */
        function (string $color, string $darkness): Color {
            $main = $darkness === 'dark' ? 128 : 255;
            $other = $darkness === 'dark' ? 0 : 128;

            return new Color(
                $color === 'red' ? $main : $other,
                $color === 'green' ? $main : $other,
                $color === 'blue' ? $main : $other,
            );
        }
    )
    ->mapper()
    ->map(Color::class, [/* … */]);

final readonly class Color
{
    /**
     * @param int<0, 255> $red
     * @param int<0, 255> $green
     * @param int<0, 255> $blue
     */
    public function __construct(
        public int $red,
        public int $green,
        public int $blue
    ) {}

    /**
     * @param non-empty-string $hex
     */
    public static function fromHex(string $hex): self
    {
        if (strlen($hex) !== 6) {
            throw new DomainException('Must be 6 characters long');
        }

        /** @var int<0, 255> $red */
        $red = hexdec(substr($hex, 0, 2));
        /** @var int<0, 255> $green */
        $green = hexdec(substr($hex, 2, 2));
        /** @var int<0, 255> $blue */
        $blue = hexdec(substr($hex, 4, 2));

        return new self($red, $green, $blue);
    }
}

接口实现构造函数

默认情况下,映射器无法实例化接口,因为它不知道要使用哪个实现。为此,可以以与类相同的方式为接口注册构造函数。

注意

由于映射器无法自动猜测接口可以使用哪个实现,因此无法使用Constructor特性,必须使用MapperBuilder::registerConstructor()方法。

在下面的示例中,映射器被教导如何从包ramsey/uuid实例化UuidInterface的实现:

(new \CuyZ\Valinor\MapperBuilder())
    ->registerConstructor(
        // The static method below has return type `UuidInterface`; therefore,
        // the mapper will build an instance of `Uuid` when it needs to 
        // instantiate an implementation of `UuidInterface`.
        Ramsey\Uuid\Uuid::fromString(...)
    )
    ->mapper()
    ->map(
        Ramsey\Uuid\UuidInterface::class,
        '663bafbf-c3b5-4336-b27f-1796be8554e0'
    );

自定义枚举构造函数

如上所述,为枚举注册构造函数的方式与为类注册构造函数的方式相同。

enum Color: string
{
    case LIGHT_RED = 'LIGHT_RED';
    case LIGHT_GREEN = 'LIGHT_GREEN';
    case LIGHT_BLUE = 'LIGHT_BLUE';
    case DARK_RED = 'DARK_RED';
    case DARK_GREEN = 'DARK_GREEN';
    case DARK_BLUE = 'DARK_BLUE';

    #[\CuyZ\Valinor\Mapper\Object\Constructor]
    public static function fromMatrix(string $type, string $color): Color
    {
        return self::from($type . '_' . $color);
    }
}

(new \CuyZ\Valinor\MapperBuilder())
    ->mapper()
    ->map(Color::class , [
        'type' => 'DARK',
        'color' => 'RED'
    ]); // Color::DARK_RED

注意

枚举构造函数可以针对特定模式:

enum Color: string
{
    case LIGHT_RED = 'LIGHT_RED';
    case LIGHT_GREEN = 'LIGHT_GREEN';
    case LIGHT_BLUE = 'LIGHT_BLUE';
    case DARK_RED = 'DARK_RED';
    case DARK_GREEN = 'DARK_GREEN';
    case DARK_BLUE = 'DARK_BLUE';

    /**
     * This constructor will be called only when pattern `SomeEnum::DARK_*`
     * is requested during mapping.
     *
     * @return Color::DARK_*
     */
    #[\CuyZ\Valinor\Mapper\Object\Constructor]
    public static function darkFrom(string $value): Color
    {
        return self::from('DARK_' . $value);
    }
}

(new \CuyZ\Valinor\MapperBuilder())
    ->mapper()
    ->map(Color::class . '::DARK_*', 'RED'); // Color::DARK_RED

动态构造函数

在某些情况下,构造函数处理的类型仅在运行时才知道,在这种情况下,构造函数需要知道必须使用哪个类来实例化对象。

例如,接口可以声明一个静态构造函数,然后由多个子类实现该构造函数。一种解决方案是为每个子类注册构造函数,这会导致大量样板代码,并且每次创建新子类时都需要重新注册。另一种方法是使用特性\CuyZ\Valinor\Mapper\Object\DynamicConstructor 。

当构造函数使用此特性时,其第一个参数必须是字符串,并且将填充调用构造函数时映射器需要构建的实际类的名称。可以添加其他参数并将其正常映射,具体取决于提供给映射器的源。

interface InterfaceWithStaticConstructor
{
    public static function from(string $value): self;
}

final readonly class ClassWithInheritedStaticConstructor implements InterfaceWithStaticConstructor
{
    private function __construct(private SomeValueObject $value) {}

    public static function from(string $value): self
    {
        return new self(new SomeValueObject($value));
    }
}

(new \CuyZ\Valinor\MapperBuilder())
    ->registerConstructor(
        #[\CuyZ\Valinor\Mapper\Object\DynamicConstructor]
        function (string $className, string $value): InterfaceWithStaticConstructor {
            return $className::from($value);
        }
    )
    ->mapper()
    ->map(ClassWithInheritedStaticConstructor::class, 'foo');