Doctrine ORM的自定义类型映射:处理数据库中的复杂或非标准数据类型

Doctrine ORM 的自定义类型映射:处理数据库中的复杂或非标准数据类型

大家好!今天我们来深入探讨 Doctrine ORM 中的自定义类型映射。在实际项目中,我们经常会遇到数据库中存储了一些 Doctrine ORM 默认不支持的数据类型,或者我们需要对现有数据类型进行特殊处理的情况。这时,自定义类型映射就显得尤为重要。它允许我们将数据库中复杂或非标准的数据类型,映射到 PHP 中的特定类型,从而方便我们进行业务逻辑的处理。

1. 为什么要使用自定义类型映射?

Doctrine ORM 已经提供了丰富的内置类型映射,例如 string, integer, datetime 等。但有些情况下,这些内置类型无法满足我们的需求:

  • 非标准数据类型: 数据库可能使用一些自定义的数据类型,例如 JSON, ENUM, PostGIS 几何类型等。Doctrine 无法直接理解这些类型。
  • 数据转换需求: 我们可能需要对数据库中的数据进行转换后再在 PHP 中使用,例如将数据库中的时间戳转换为 DateTime 对象,或者将数据库中的逗号分隔的字符串转换为 PHP 数组。
  • 枚举类型处理: 虽然 Doctrine 提供了 EnumType,但对于复杂的枚举,自定义类型映射可以提供更灵活的控制。
  • 数据加密/解密: 在存储敏感数据时,我们可能需要对数据进行加密,并在读取时进行解密。自定义类型映射可以透明地处理这些加解密操作。

2. 如何创建自定义类型映射?

创建自定义类型映射需要实现 DoctrineDBALTypesType 接口。这个接口定义了几个核心方法,用于处理类型转换和数据库抽象:

  • getName(): 返回类型的名称,在 Doctrine 配置中使用。
  • getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): 返回用于在数据库中创建列的 SQL 语句。
  • convertToPHPValue($value, AbstractPlatform $platform): 将数据库中的值转换为 PHP 值。
  • convertToDatabaseValue($value, AbstractPlatform $platform): 将 PHP 值转换为数据库值。

3. 示例:JSON 类型映射

假设我们需要将数据库中的 JSON 数据映射到 PHP 数组。 首先,创建一个名为 JsonType 的类:

<?php

namespace AppDBALTypes;

use DoctrineDBALPlatformsAbstractPlatform;
use DoctrineDBALTypesType;

class JsonType extends Type
{
    const JSON = 'json'; // 定义类型名称

    public function getName(): string
    {
        return self::JSON;
    }

    public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
    {
        return $platform->getClobTypeDeclarationSQL($fieldDeclaration); // 使用 CLOB (TEXT) 类型存储 JSON
    }

    public function convertToPHPValue($value, AbstractPlatform $platform): mixed
    {
        if ($value === null) {
            return null;
        }

        $decodedValue = json_decode($value, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new InvalidArgumentException('Invalid JSON: ' . json_last_error_msg());
        }

        return $decodedValue;
    }

    public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
    {
        if ($value === null) {
            return null;
        }

        $encodedValue = json_encode($value);

        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new InvalidArgumentException('Invalid JSON: ' . json_last_error_msg());
        }

        return $encodedValue;
    }

    public function requiresSQLCommentHint(AbstractPlatform $platform) : bool
    {
        return true;
    }
}
  • getName(): 返回类型名称 json
  • getSQLDeclaration(): 使用 getClobTypeDeclarationSQL() 函数,这通常会映射到数据库中的 TEXTCLOB 类型,适合存储较长的字符串。
  • convertToPHPValue(): 将数据库中的 JSON 字符串解码为 PHP 数组。如果解码失败,抛出异常。
  • convertToDatabaseValue(): 将 PHP 数组编码为 JSON 字符串。如果编码失败,抛出异常。
  • requiresSQLCommentHint(): 返回 true。这告诉 Doctrine 在生成的 SQL 中包含类型提示,有助于 Doctrine 理解自定义类型。

4. 注册自定义类型

在 Doctrine 的配置中注册自定义类型:

// config/packages/doctrine.yaml
doctrine:
    dbal:
        types:
            json: AppDBALTypesJsonType

5. 在实体中使用自定义类型

在实体类中使用 @Column 注解指定自定义类型:

<?php

namespace AppEntity;

use DoctrineORMMapping as ORM;

/**
 * @ORMEntity
 */
class Product
{
    /**
     * @ORMId
     * @ORMGeneratedValue
     * @ORMColumn(type="integer")
     */
    private $id;

    /**
     * @ORMColumn(type="string", length=255)
     */
    private $name;

    /**
     * @ORMColumn(type="json", nullable=true)
     */
    private $details;

    // ... getters and setters
}

现在,当 Doctrine 从数据库中读取 details 字段时,它会自动将其转换为 PHP 数组。当我们将 PHP 数组保存到数据库中时,Doctrine 会将其转换为 JSON 字符串。

6. 示例:枚举类型映射

假设我们有一个 OrderStatus 枚举,表示订单的状态:

<?php

namespace AppEnum;

enum OrderStatus: string
{
    case PENDING = 'pending';
    case PROCESSING = 'processing';
    case SHIPPED = 'shipped';
    case COMPLETED = 'completed';
    case CANCELLED = 'cancelled';
}

我们可以创建一个自定义类型映射,将这个枚举映射到数据库中的字符串类型:

<?php

namespace AppDBALTypes;

use AppEnumOrderStatus;
use DoctrineDBALPlatformsAbstractPlatform;
use DoctrineDBALTypesType;
use InvalidArgumentException;

class OrderStatusType extends Type
{
    const ORDER_STATUS = 'order_status';

    public function getName(): string
    {
        return self::ORDER_STATUS;
    }

    public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
    {
        return $platform->getStringTypeDeclarationSQL($fieldDeclaration);
    }

    public function convertToPHPValue($value, AbstractPlatform $platform): ?OrderStatus
    {
        if ($value === null) {
            return null;
        }

        try {
            return OrderStatus::from($value);
        } catch (ValueError $e) {
            throw new InvalidArgumentException(sprintf('Invalid OrderStatus value: %s', $value));
        }
    }

    public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
    {
        if ($value === null) {
            return null;
        }

        if ($value instanceof OrderStatus) {
            return $value->value;
        }

        throw new InvalidArgumentException(sprintf('Expected OrderStatus, got %s', get_debug_type($value)));
    }

    public function requiresSQLCommentHint(AbstractPlatform $platform) : bool
    {
        return true;
    }
}
  • getName(): 返回类型名称 order_status
  • getSQLDeclaration(): 使用 getStringTypeDeclarationSQL() 函数,将枚举值存储为字符串。
  • convertToPHPValue(): 将数据库中的字符串转换为 OrderStatus 枚举。如果字符串不是有效的枚举值,抛出异常。
  • convertToDatabaseValue(): 将 OrderStatus 枚举转换为字符串。如果值不是 OrderStatus 枚举,抛出异常。

注册类型:

# config/packages/doctrine.yaml
doctrine:
    dbal:
        types:
            order_status: AppDBALTypesOrderStatusType

在实体中使用:

<?php

namespace AppEntity;

use AppEnumOrderStatus;
use DoctrineORMMapping as ORM;

/**
 * @ORMEntity
 */
class Order
{
    /**
     * @ORMId
     * @ORMGeneratedValue
     * @ORMColumn(type="integer")
     */
    private $id;

    /**
     * @ORMColumn(type="order_status")
     */
    private $status;

    // ... getters and setters

    public function getStatus(): OrderStatus
    {
        return $this->status;
    }

    public function setStatus(OrderStatus $status): void
    {
        $this->status = $status;
    }
}

7. 处理 NULL 值

convertToPHPValue()convertToDatabaseValue() 方法中,需要特别注意处理 NULL 值。 通常,如果数据库中的值为 NULL,则 convertToPHPValue() 应该返回 NULL。 同样,如果 PHP 中的值为 NULL,则 convertToDatabaseValue() 应该返回 NULL。 这样可以确保 Doctrine 正确处理 NULL 值。

8. 示例:加密/解密类型映射

假设我们需要对数据库中的敏感数据进行加密存储。 我们可以创建一个自定义类型映射,在将数据保存到数据库之前对其进行加密,并在从数据库中读取数据之后对其进行解密。

<?php

namespace AppDBALTypes;

use DoctrineDBALPlatformsAbstractPlatform;
use DoctrineDBALTypesType;
use SymfonyComponentDependencyInjectionAttributeAutowire;
use SymfonyComponentSecurityCoreSecurity;

class EncryptedStringType extends Type
{
    const ENCRYPTED_STRING = 'encrypted_string';

    public function __construct(
        #[Autowire('%app.encryption_key%')]
        private string $encryptionKey
    ) {}

    public function getName(): string
    {
        return self::ENCRYPTED_STRING;
    }

    public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
    {
        return $platform->getClobTypeDeclarationSQL($fieldDeclaration);
    }

    public function convertToPHPValue($value, AbstractPlatform $platform): ?string
    {
        if ($value === null) {
            return null;
        }

        // 使用 openssl_decrypt 解密数据
        $decrypted = openssl_decrypt($value, 'aes-256-cbc', $this->encryptionKey, 0, substr(md5($this->encryptionKey), 0, 16));

        return $decrypted;
    }

    public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
    {
        if ($value === null) {
            return null;
        }

        // 使用 openssl_encrypt 加密数据
        $encrypted = openssl_encrypt($value, 'aes-256-cbc', $this->encryptionKey, 0, substr(md5($this->encryptionKey), 0, 16));

        return $encrypted;
    }

    public function requiresSQLCommentHint(AbstractPlatform $platform) : bool
    {
        return true;
    }
}
  • getName(): 返回类型名称 encrypted_string
  • getSQLDeclaration(): 使用 getClobTypeDeclarationSQL() 函数,将加密后的数据存储为字符串。
  • convertToPHPValue(): 使用 openssl_decrypt 函数解密数据库中的数据。
  • convertToDatabaseValue(): 使用 openssl_encrypt 函数加密 PHP 中的数据。

重要提示:

  • 在生产环境中,请使用更安全的方式来存储和管理加密密钥。
  • 确保数据库列足够大,以存储加密后的数据。
  • 考虑使用更高级的加密算法和技术,例如 authenticated encryption。

注册类型,这里使用构造器注入的方式传入加密的key:

# config/services.yaml
services:
    AppDBALTypesEncryptedStringType:
        arguments:
            $encryptionKey: '%app.encryption_key%'

# config/packages/doctrine.yaml
doctrine:
    dbal:
        types:
            encrypted_string: AppDBALTypesEncryptedStringType

在实体中使用:

<?php

namespace AppEntity;

use DoctrineORMMapping as ORM;

/**
 * @ORMEntity
 */
class User
{
    /**
     * @ORMId
     * @ORMGeneratedValue
     * @ORMColumn(type="integer")
     */
    private $id;

    /**
     * @ORMColumn(type="encrypted_string")
     */
    private $password;

    // ... getters and setters
}

9. 与数据库平台的兼容性

getSQLDeclaration() 方法的第二个参数是 AbstractPlatform 对象。 这个对象提供了关于当前数据库平台的信息,例如数据库类型、版本等。 我们可以使用这个对象来生成与特定数据库平台兼容的 SQL 语句。

例如,如果我们需要创建一个只在 MySQL 中可用的 ENUM 类型,我们可以这样实现:

public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
{
    if ($platform instanceof DoctrineDBALPlatformsMySQLPlatform) {
        $values = array_map(function ($value) {
            return "'" . $value . "'";
        }, $fieldDeclaration['enums']);

        return 'ENUM(' . implode(', ', $values) . ')';
    }

    return $platform->getStringTypeDeclarationSQL($fieldDeclaration);
}

10. 测试自定义类型

编写单元测试来验证自定义类型的正确性非常重要。 我们可以编写测试来确保 convertToPHPValue()convertToDatabaseValue() 方法能够正确地转换数据。

11. Doctrine 事件监听器(Listeners)和订阅者(Subscribers)

虽然自定义类型映射可以处理数据转换,但 Doctrine 的事件监听器和订阅者提供了更灵活的方式来处理实体生命周期中的事件。 例如,我们可以使用事件监听器在实体持久化之前对数据进行验证,或者在实体加载之后执行一些额外的操作。

12. 总结:灵活的数据处理方式

自定义类型映射是 Doctrine ORM 中一个强大的功能,它允许我们处理数据库中复杂或非标准的数据类型。 通过实现 DoctrineDBALTypesType 接口,我们可以将数据库中的数据转换为 PHP 中的特定类型,并反之亦然。 在使用自定义类型映射时,我们需要注意处理 NULL 值、与数据库平台的兼容性,并编写单元测试来验证其正确性。

核心方法是类型转换。

注意空值、平台兼容和编写测试。

Doctrine事件监听器提供更灵活的方式处理实体生命周期。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注