adventure-tech/data-transfer-object

Laravel 的严格且具有偏见的 DTO 实现

3.1.2 2023-03-22 10:21 UTC

This package is auto-updated.

Last update: 2024-09-22 13:34:05 UTC


README

这是一个严格的、具有偏见的 DTO 实现。本包的一个目标是为 DTO 创建尽可能可预测的实例。

与其他实现非常通用不同,此包提供大量关于如何创建和实例化 DTO 的规则和约定。它遵循一种“精心创建,轻松使用”的理念。

此包是为 Laravel 创建的,但可以轻松修改以在其他框架或原生 PHP 中使用。

要求

  • PHP ^8.1

安装

通过 composer 安装包

composer require adventure-tech/data-transfer-object

用法

实例化 DTO

实例化新的 DTO 非常简单。只有一种方式,即通过在 DTO 类上调用静态方法 from(),如下所示

$dto = MyDTO::from($source);

该方法接受一个参数,即 DTO 将映射其属性的源数组或对象。参数可以是三种类型之一

  • array | 与 DTO 匹配的键值对关联数组
  • stdClass | 具有与 DTO 匹配属性的通用 PHP 对象
  • Model | 具有与 DTO 匹配属性的 Laravel Eloquent 模型

创建新的 DTO 类

以下是一个名为 User 的新 DTO 的基本示例

use AdventureTech\DataTransferObject\DataTransferObject;
use Carbon\Carbon;

class User extends DataTransferObject
{
    public int $id;
    public string $first_name;
    public string $last_name;
    public string $email;
    public Carbon $created_at;
    public Carbon $deleted_at;
}

这是 DTO 的最简单也是最严格的定义。在创建新的 DTO 时,您需要了解一些规则和假设。请注意,可以通过使用提供的某些 属性 修改一些默认行为。

规则、约定及其修改方法

属性类型声明

每个属性都必须声明一个类型。总是。

可见性修饰符

您想要从源自动分配的每个属性都需要是 public

可空属性

您可以将属性设置为可空,例如 ?string,但 DTO 仍然会期望源上的对应属性存在,即使其值为 null

通过在属性上使用属性 #[Optional],您可以修改此行为。DTO 将不再关心属性是否实例化、是否在源上存在或以 null 值实例化(假定字段已声明为可空)。

命名属性

DTO 将期望源上的对应类属性名称或数组键与 DTO 属性完全相同。

通过使用属性 #[MapFrom],您可以覆盖 DTO 在源上查找的属性名称或键。请参阅下面的示例。

#[MapFrom('first_name')]
public string $firstName;

默认值

通过使用属性 #[DefaultValue],您可以定义属性的默认值。

如果 DTO 属性声明为可空,则如果源属性为 null,则将分配默认值。

如果 DTO 属性不可空,则默认值将作为后备选项,如果对应的源属性不存在或其值为 null,则将使用它。

#[DefaultValue(false)]
public bool $isAdmin;

处理日期

如果你的 DTO 属性声明为 Carbon 类型,DTO 将在赋值之前自动将源日期/日期时间值转换为 Carbon。

// The DTO will look for the created_at property on the source, and cast it to Carbon
#[MapFrom('created_at')]
public Carbon $createdAt;

布尔属性

如果你的数据库使用 int/tinyint 来表示布尔值,只要 DTO 属性声明为 bool 类型,它们将自动转换为 bool。

// The source property value can be true/false or 0/1
public bool $isPaid;

从 JSON 映射

该包使用 JsonMapper 库来处理数据库 JSON 字段。该库允许我们将整个 JSON 结构映射到一个嵌套的 DTO 结构。

其工作方式是,你需要在 DTO 中指定一个根类,并用 #[JsonMapper] 属性进行标注。这个根类不应该是一个 DataTransferObject 的实例,而应该是一个 POPO(Plain Old Php Object)。与 JSON 结构匹配的每个子类也应该是一个 POPO,并带有库 API 中的 docblock 注释。访问库的 GitHub 页面以了解更多关于使用嵌套结构的信息。

// The main DTO
class Person extends DataTransferObject
{
    #[MapFrom('first_name')]
    public string $firstName;

    #[JsonMapper(Address::class)]
    public Address $address;
}
// The Address POPO
class Address
{
    public string $street;
    public string $zip;
    public string $city;
}
let sourceJsonStructure = {
    "first_name": "John",
    "address": {
        "street": "Example street",
        "zip": "0000",
        "city": "Example city" 
    }
}

从枚举映射

如果你的数据库字段是枚举类型,你可以在应用程序中创建相应的枚举类,并将其用作 DTO 属性的类型,值将自动转换为正确的枚举值。需要注意的是,你的枚举类必须由 int 或 string 支持,这样才能工作。更多信息请参阅 这里

// The enum value from the database will be mapped to the correct enum value.
public MyEnum $myEnum;

不可变字段

遗憾的是,PHP 不支持不可变对象(目前不支持)。该包不支持 readonly 声明。原因是父 DataTransferObject 依赖于 Reflection 来为实例化的子类分配值。PHP 不允许父类将 readonly 属性分配给其子类。

PhpStorm 集成了 #[Immutable] 属性。你有机会在你的 DTO 属性上使用这个属性,但它所做的只是在你尝试为一个不可变属性赋值时在 IDE 中显示一个警告。它不能阻止你这样做。

#[Immutable]
public string $weCanPretendThisIsImmutable;

触发器

#[Trigger] 属性提供了一种在从源映射的其他属性初始化后初始化属性的方法。这个属性的典型用途是,你希望在 DTO 数据上执行一些计算,并将其作为属性存储。

该属性需要一个参数,即初始化属性的触发方法。让我们看看一个例子,我们想要连接用户的名字并将其存储为新属性。

class User extends DataTransferObject
{
    #[MapFrom('first_name')]
    public string $firstName;

    #[MapFrom('last_name')]
    public string $lastName;

    #[Trigger('setFullName')]    
    public string $fullName;
    
    protected function setFullName()
    {
        $this->fullName = $this->firstName . ' ' . $this->lastName; 
    }
}

Laravel Artisan

该包附带了一个 artisan 命令,用于创建新的 DTO 类。

php artisan make:dto --name=MyDTO

示例

Laravel 示例

定义 DTO

use AdventureTech\DataTransferObject\Attributes\DefaultValue;
use AdventureTech\DataTransferObject\Attributes\MapFrom;
use AdventureTech\DataTransferObject\Attributes\Optional;
use AdventureTech\DataTransferObject\DataTransferObject;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use stdClass;

class User extends DataTransferObject
{
    public int $id;
    #[MapFrom('first_name')]
    public string $firstName;
    #[MapFrom('last_name')]
    public string $lastName;
    public string $email;
    #[MapFrom('created_at')]
    public Carbon $createdAt;
    #[MapFrom('deleted_at')]
    public Carbon $deletedAt;
    #[Optional]
    public string $iAmNotImportant;
    // Relations
    #[DefaultValue([])]
    public array $posts;
}

使用 DTO

$eloquentModel = App\Models\User::find(1);

$dto = App\Dto\User::from($eloquentModel);