adventure-tech / data-transfer-object
Laravel 的严格且具有偏见的 DTO 实现
Requires
- php: ^8.2
- laravel/framework: ^10.0
- nesbot/carbon: ^2.0
- netresearch/jsonmapper: ^4.1
Requires (Dev)
- jetbrains/phpstorm-attributes: ^1.0
- pestphp/pest: ^2.0
- phpunit/phpunit: ^10.0
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);