eve/dto

简单、灵活的数据传输对象库

v2.1.0 2021-04-14 16:42 UTC

This package is auto-updated.

Last update: 2024-09-15 15:02:59 UTC


README

Main Latest version

这是一个用PHP编写的简单、灵活的数据传输对象库。

为什么?

在层之间传递数据通常使用关联数组,这可以说是常见做法。例如,创建新用户的业务方法可能如下所示

// UserService.php
public function createUser(array $attributes): User
{
    return User::create($attributes);
}

可以通过以下方式调用此方法,例如,控制器

// UserController.php
public function store(CreateUserRequest $request)
{
    $this->userService->create($request->toArray());
}

这种方法可以工作,但存在一些缺点

  • 关联数组几乎是未结构化的——几乎没有对可以放入其中的内容或每个元素的数据类型进行限制。这使得代码难以推理(无法确切知道数组中有什么),并且可能导致严重的安全问题。
  • 始终需要参考(如果存在)文档以了解数组的“形状”。这降低了可重用性和生产力。
  • 静态代码分析和IDE自动补全支持受到很大阻碍。

现在想象一下,如果我们不是使用任意数组,而是使用具有类型属性的对象

// UserCreationData.php
class UserCreationData
{
    public string $email;
    public string $password;
    public ?int $age;
}

// UserService.php
public function createUser(UserCreationData $data): User
{
    return User::create($data->toArray());
}

// UserController.php
public function store(CreateUserRequest $request)
{
    $this->userService->create(UserCreationData::fromRequest($request));
}

采用这种方法,我们可以清楚地了解预期的字段,它们的类型和其他限制,并可以享受所有类型提示、自动补全、静态分析等。这正是eve/dto允许您做的。

需求和安装

您可以通过Composer安装eve/dto

composer require eve/dto

此软件包需要PHP ≥7.4。

从v1.x迁移

此库的v1.x版本包含严格的类型检查,例如,将字符串分配给布尔属性将引发错误。虽然这个特性很有用,但它不属于DTO的范围,已经被从v2中删除。您被鼓励使用PHPStan或Psalm这样的静态分析工具来完成任务。

用法

基本用法

按照上面的示例,首先,我们让UserCreationData扩展Eve\DTO\DataTransferObject并定义所有属性为公共属性

class UserCreationData extends \Eve\DTO\DataTransferObject
{
    public string $email;
    public string $password;
    public ?int $age;
}

要构造一个新的UserCreationData实例,请使用带有参数数组的make

$data = UserCreationData::make([
   'email' => 'alice@company.tld',
   'password' => 'SoSecureWow',
   'age' => 30,
]);

或者,您可以显式设置属性。上面的代码本质上与以下相同

$data = UserCreationData::make();
$data->email = 'alice@company.tld';
$data->password = 'SoSecureWow';
$data->age = 30;

或者您可以使用流畅的set方法,它可以接受关联数组或两个分离的$name$value参数

$data = UserCreationData::make()
    ->set('email', 'alice@company.tld')
    ->set([
       'password' => 'SoSecureWow',
       'age' => 30,
    ]);

如果传递的任何属性在类定义中不存在,将抛出异常

UserCreationData::make(['nope' => 'bar']); // throws "Public property $nope does not exist in class UserCreationData"

然后我们可以调用toArray()方法将对象转换为关联数组

$arr = $data->toArray(); // ['email' => 'alice@company.tld', 'password' => 'SoSecureWow', 'age' => 30]

请注意,未设置的属性将不会包含在输出数组中

$data = UserCreationData::make();

// Only setting email now
$data->email = 'alice@company.tld';

$arr = $data->toArray(); // ['email' => 'alice@company.tld']

这在例如您有一个修补数据库记录的方法时特别方便,因为它允许操作完全灵活——您可以选择修补所有属性或仅修补它们的子集。

嵌套DTO

嵌套DTO将转换为相应的数组

class UserCreationData extends \Eve\DTO\DataTransferObject
{
    public string $email;
    public string $password;
    public UserInformationData $information;
}

class UserInformationData extends \Eve\DTO\DataTransferObject
{
    public int $age;
}

$data = UserCreationData::make([
   'email' => 'alice@company.tld',
   'password' => 'SoSecureWow',
   'information' => UserInformationData::make(['age' => 30]),
]);

$data->toArray(); // ['email' => 'alice@company.tld', 'password' => 'SoSecureWow', ['information' => ['age' => 30]]

辅助函数

  • DataTransferObject::only(string ...$names): static返回只包含$names的输出数组的对象。

    $data = UserCreationData::make([
       'email' => 'alice@company.tld',
       'password' => 'SoSecureWow',
       'age' => 30,
    ]);
    
    $data->only('email', 'password')->toArray(); // ['email' => 'alice@company.tld', 'password' => 'SoSecureWow']
  • DataTransferObject::except(string ...$names): static返回排除$names的输出数组的对象。

    $data = UserCreationData::make([
       'email' => 'alice@company.tld',
       'password' => 'SoSecureWow',
       'age' => 30,
    ]);
    
    $data->except('email', 'password')->toArray(); // ['age' => 30]
  • DataTransferObject::compact(): static返回只包含值不为NULL的属性的输出数组的对象。

    $data = UserCreationData::make([
       'email' => 'alice@company.tld',
       'password' => 'SoSecureWow',
       'age' => null,
    ]);
    
    $data->compact()->toArray(); // ['email' => 'alice@company.tld', 'password' => 'SoSecureWow']
  • DataTransferObject::get(string $name, $default = null): mixed 返回 $name 属性的值。如果 $name 在类定义中不存在,将抛出异常。如果 $name 存在但没有初始化,将返回 $default

    重要:PHP 将未指定类型的属性,例如 public $prop,视为已用 NULL 初始化。

    $data = UserCreationData::make([
       'email' => 'alice@company.tld',
       'password' => 'SoSecureWow',
    ]);
    
    $data->get('email'); // 'alice@company.tld'
    $data->password; // 'SoSecureWow'
    
    $data->age; // throws "UserCreationData::$age must not be accessed before initialization."
    $data->get('age', 30); // 30
    
    $data->get('nope'); // throws "Public property $nope does not exist in class UserCreationData."
    $data->nope; // throws "Public property $nope does not exist in class UserCreationData."

与 spatie/data-transfer-object 的区别

尽管 eve/dto 受到 spatie/data-transfer-object 的启发并具有一些相似之处,但这两个包存在某些区别,其中最显著的区别如下

  • spatie/data-transfer-object 要求所有非空属性必须在实例化时提供。这种行为并不总是可行或理想(参考上面的数据修补示例)。eve/dto 采用一种更加宽容的方法,允许使用属性子集创建 DTO。
  • spatie/data-transfer-object 无法检测或阻止你直接分配一个不存在的属性(例如,$userData->non_existent = 'foo'),而 eve/dto 做这一点是为了确保对象的完整性。
  • spatie/data-transfer-object 实现了诸如“数据传输对象集合”和“灵活数据传输对象”等特性。为了保持简单和简洁,eve/dto 没有这些实现。

许可证

MIT