在处理数据时,我们经常需要将类映射到数据库表,反之亦然。这是为了将数据库中的简单标量类型映射到更高级别的对象。
映射、获取和存储数据是与业务逻辑无关的低级操作。因此,我们通常将其隐藏在接口后面,例如 DAO 或存储库模式。
为了完全专注于业务,我们需要尽量减少处理低级代码。这需要我们将整个应用程序视为面向业务的整体,而不是仅仅关注特定的层或模块。
PHP 方式与业务接口
Ecotone
的业务接口旨在减少低级和样板代码,让开发人员能够专注于更重要的业务逻辑。
Ecotone
还提供了用于数据库访问的特殊类型的业务接口。这些接口消除了转换逻辑、参数绑定和 SQL
执行的需要。通过这种方式,开发人员可以将低级代码隐藏在抽象背后,专注于业务逻辑。
修改数据库数据
为了定义插入、更新或删除数据库记录的方法,我们可以使用 Ecotone
的 DbalWrite
属性。
<?php
interface PersonService
{
#[DbalWrite('INSERT INTO persons VALUES (:name, :surname)')]
public function register(string $name, string $surname): void;
}
该属性将告诉 Ecotone
在调用此方法时执行给定的 SQL
。该接口的实现将由 Ecotone
交付并在您的依赖容器中注册。
在上述示例中,我们创建了一个 PersonService
接口,其中包含一个 register()
方法。该方法将新记录插入到 persons
表中。
我们使用 DbalWrite
属性将 INSERT INTO persons VALUES (:name, :surname)
绑定到 register()
方法。这告诉 Ecotone
在调用 register()
方法时执行此 SQL
。
register()
方法的参数 name
和 surname
将自动绑定到 SQL
参数 :name
和 :surname
。
返回修改记录数
在更新或删除记录时,我们可能需要知道有多少记录被修改。为此,我们可以为声明的方法添加 Integer 返回类型。
<?php
interface PersonService
{
#[DbalWrite('UPDATE activities SET active = false WHERE last_activity < :activityOlderThan')]
public function markAsInactive(\DateTimeImmutable $activityOlderThan): int;
}
如您所见,我们使用了 DateTimeImmutable
作为参数。Ecotone
将使用内置转换,在执行 SQL 之前将日期时间转换为字符串。
参数转换
领域模型通常使用比数据库理解的标量类型更高级别的类。在大多数情况下,我们希望接口遵循业务类型而不是数据库类型。为此,我们可以使用转换机制。
内置类转换
Ecotone
提供了默认的日期时间转换,但它也可以为任何提供 __toString()
方法的类提供转换。
例如,以下示例类可以通过 __toString()
方法自动转换:
<?php
final readonly class PersonId
{
public function __construct(public string $id) {}
public function __toString(): string
{
return $this->id;
}
}
现在我们可以将它用作接口的一部分,而不必担心转换:
<?php
interface PersonService
{
#[DbalWrite('INSERT INTO activities VALUES (:personId, :activity, :time)')]
public function store(PersonId $personId, string $activity, \DateTimeImmutable $time): void;
}
在这种情况下,PersonId
和 DateTimeImmutable
都会自动转换。PersonId
将自动转换为字符串,因为它包含 __toString()
方法。
定制参数转换
我们可以编写自己的转换器来定制给定类的转换方式。例如,假设我们有一个 DayOfWeek
类,它在 PHP 级别上表示为枚举字符串,但在数据库中我们希望将其存储为整数。
<?php
enum DayOfWeek: string
{
case MONDAY = 'monday';
case TUESDAY = 'tuesday';
case WEDNESDAY = 'wednesday';
case THURSDAY = 'thursday';
case FRIDAY = 'friday';
case SATURDAY = 'saturday';
case SUNDAY = 'sunday';
public function toNumber(): int
{
return match ($this) {
self::MONDAY => 1,
self::TUESDAY => 2,
self::WEDNESDAY => 3,
self::THURSDAY => 4,
self::FRIDAY => 5,
self::SATURDAY => 6,
self::SUNDAY => 7,
};
}
}
在这种情况下,我们需要将 DayOfWeek
转换为整数。为此,我们可以编写一个 DayOfWeekConverter
类:
<?php
final readonly class DayOfWeekConverter
{
#[Converter]
public function dayToNumber(DayOfWeek $day): int
{
return $day->toNumber();
}
}
Converter
是一个注册在依赖容器中的类。Ecotone
将通过属性标记找到所有转换器,并在需要转换时调用它。在我们的例子中,当需要从 DayOfWeek
转换为整数时,它将被调用。
定义转换器后,我们现在可以在接口中使用 DayOfWeek
类,并确保在数据库中它将作为整数存储。
<?php
interface Scheduler
{
#[DbalWrite('INSERT INTO schedule (day, task) VALUES (:day, :task)')]
public function scheduleForDayOfWeek(DayOfWeek $day, string $task): void;
}
转换器将在您的所有接口之间重用,因此我们只需编写一次即可涵盖所有情况。
使用表达式语言
表达式语言可用于为给定场景自定义参数。这使我们可以自定义特定操作的行为。
例如,假设我们有一个 PersonName
类,我们想在保存之前将其转换为小写。
<?php
final readonly class PersonName
{
public function __construct(
public string $name
) {
}
public function toLowerCase(): string
{
return strtolower($this->name);
}
}
为了将 PersonName
存储为小写,我们可以使用表达式语言在保存之前调用此方法:
<?php
interface PersonService
{
#[DbalWrite('INSERT INTO persons VALUES (:personId, :name)')]
public function register(
int $personId,
#[DbalParameter(expression: 'payload.toLowerCase()')] PersonName $name
): void;
}
在将 PersonName
存储到数据库之前调用 toLowerCase()
通过提供 DbalParameter
属性,我们可以定义在存储给定参数之前要计算的表达式。
payload
是表达式中引用给定参数的特殊变量,在本例中为 PersonName
。
非参数方法
我们可能会遇到不需要传递参数的情况,因为它可以动态评估。为此,我们可以使用 DbalParameter
作为方法属性的一部分。
<?php
interface PersonService
{
#[DbalWrite('INSERT INTO persons VALUES (:personId, :name, :now)')]
#[DbalParameter(name: 'now', expression: "reference('clock').now()"]
public function register(
int $personId,
PersonName $name
): void;
}
在这种情况下,我们在方法级别使用 Dbal
参数预定义了“now
”参数。我们使用表达式语言来评估参数值。
reference()
是表达式中的一个特殊变量,它指向您的依赖容器。这样我们就可以获取给定的服务并直接调用它的方法。在这种情况下,我们正在获取时钟服务并调用 now()
方法。
对于方法级 Dbal
参数,我们可以通过名称访问传递给方法的所有参数。在我们的例子中,它将是“personId
”或“name
”。
基于 JSON 的数据库参数
数据库列并不总是包含简单的标量类型,它实际上可能包含 JSON
。然而,在我们的域级代码中,JSON
主要表示为更复杂的类或对象数组,因此需要转换。
假设我们要在数据库中存储人员角色数组。在域级代码中,人员角色表示为 PersonRole
类。
<?php
final readonly class PersonRole
{
public function __construct(public string $role) {}
public function getRole(): string
{
return $this->role;
}
}
然后,在接口级别,我们将定义角色数组:
<?php
interface PersonService
{
/**
* @param PersonRole[] $roles
*/
#[DbalWrite('INSERT INTO persons VALUES (:personId, :roles)')]
public function addRoles(
int $personId,
#[DbalParameter(convertToMediaType: MediaType::APPLICATION_JSON)] array $roles
): void;
}
通过使用 ConvertToMediaType
定义 DbalParameter
,我们声明我们希望将给定参数转换为特定媒体类型,在我们的例子中它将是 JSON
。
我们可以注册自己的 Media Type Converter
,但如果我们使用开箱即用的 JMS Module
,我们只需要定义一个 Converter
。
<?php
final class PersonRoleConverter
{
#[Converter]
public function from(PersonRole $personRole): string
{
return $personRole->getRole();
}
}
这足以将 PersonRoles
集合直接转换为 JSON
。
查询数据库数据
到目前为止,我们专注于存储数据和参数转换。但是,我们还可以使用 Ecotone
的抽象来查询数据。
查询多条记录
为了获取多条记录,我们可以使用 DbalQuery
属性。该属性指定 SQL
查询,Ecotone
将使用它来查询数据库。
<?php
interface PersonService
{
#[DbalQuery('SELECT person_id, name FROM persons LIMIT :limit OFFSET :offset')]
public function getPersons(int $limit, int $offset): array;
}
这将创建一个 getPersons()
方法,它将返回包含 person_id
和 name
的数组的数组。
我们还可以将 Pagination
对象作为参数传递,以使界面更具可读性。Pagination
对象包含 limit
和 offset
属性。
<?php
final readonly class Pagination
{
public function __construct(public int $limit, public int $offset)
{
}
}
然后,我们可以使用 Pagination
对象作为 getPersons()
方法的参数:
<?php
interface PersonService
{
#[DbalQuery('SELECT person_id, name FROM persons LIMIT :(pagination.limit) OFFSET :(pagination.offset)')]
public function getNameListWithIgnoredParameters(
Pagination $pagination
): array;
}
为了在 SQL 中使用表达式语言,我们可以使用 :
运算符。例如,如果我们想访问 Pagination
对象的 limit
属性,我们可以使用以下表达式:
:(pagination.limit)
转换结果集
在处理域级代码时,我们通常希望使用类而不是 Dbal
默认返回的关联数组。Ecotone
可以根据接口中定义的返回类型转换结果。
查询单个记录并将其转换为 PersonDTO
例如,假设我们有一个 PersonService
接口,它定义了一个 get()
方法,该方法返回一个 PersonDTO
对象。
<?php
interface PersonService
{
#[DbalQuery(
'SELECT person_id, name FROM persons WHERE person_id = :personId',
fetchMode: FetchMode::FIRST_ROW
)]
public function get(int $personId): PersonDTO;
}
在这种情况下,我们使用 fetchMode
属性声明我们想要从结果中获取单行。然后,Ecotone
将调用 PersonDTOConverter
类将结果转换为 PersonDTO
对象。
<?php
class PersonDTOConverter
{
#[Converter]
public function to(array $personDTO): PersonDTO
{
return new PersonNameDTO($personDTO['person_id'], $personDTO['name']);
}
}
PersonDTOConverter
类将从结果中获取 person_id
和 name
字段,并将它们用于构造 PersonDTO
对象。
返回空值
当获取单行时,我们可能根本找不到结果。对于这种情况,我们可以使用联合返回类型。
<?php
interface PersonService
{
#[DbalQuery(
'SELECT person_id, name FROM persons WHERE person_id = :personId',
fetchMode: FetchMode::FIRST_ROW
)]
public function get(int $personId): PersonDTO|null;
}
在这种情况下,如果找到记录,Ecotone
将返回 PersonDTO
对象。如果没有找到记录,Ecotone
将返回 null
。
返回单个值
对于像 SUM()
、COUNT()
、MIN()
这样的聚合函数,我们可能希望直接返回它们而不是特定的行。为此,Ecotone
提供了获取模式来返回第一行的第一列。
<?php
interface PersonService
{
#[DbalQuery(
'SELECT COUNT(*) FROM persons',
fetchMode: FetchMode::FIRST_COLUMN_OF_FIRST_ROW
)]
public function countPersons(): int;
}
在这种情况下,Ecotone
将直接返回聚合函数的值,而不是整个结果集。
转换多条记录
当获取多行时,我们也可能希望使用类而不是数组。然而,我们需要定义我们应该返回什么,并且 PHP 不支持泛型。为了解决这个缺失的功能,Ecotone
提供了读取 Docblock
的能力,以便了解我们想要转换成什么。
<?php
interface PersonService
{
/**
* @return PersonDTO[]
*/
#[DbalQuery(
'SELECT person_id, name FROM persons LIMIT :limit OFFSET :offset'
)]
public function get(int $limit, int $offset)): array;
}
在这种情况下,Ecotone
将读取 Docblock
,并将结果转换为 PersonDTO
的数组。
获取大型结果集
获取关联结果的默认模式是将所有结果都加载到内存中。然而,对于大型结果集,这可能会导致内存不足的问题。为了解决这个问题,我们可以使用 fetch()
模式来迭代结果。
<?php
interface PersonService
{
/**
* @return iterable<PersonDTO>
*/
#[DbalQuery(
'SELECT person_id, name FROM persons',
fetchMode: FetchMode::ITERATE
)]
public function getAll(): iterable;
}
在这种情况下,Ecotone
将一次只加载和转换一行,确保内存使用安全。
Doctrine ORM 支持
如果我们使用 Ecotone
的聚合支持与 Doctrine ORM
,我们可以使用特殊类型的业务接口 - 存储库。存储库接口允许我们使用 Doctrine ORM
来访问和管理数据库中的实体。
<?php
interface PersonRepository
{
#[Repository]
public function get(int $personId): ?Person;
#[Repository]
public function save(Person $person): void;
}
Repository
注解告诉 Ecotone
该接口是存储库。Ecotone
将使用 getRepository()
方法找到相关的 Doctrine ORM
实体管理器,并使用它来执行存储库方法。
Eloquent模型支持
如果我们使用 Ecotone
的聚合支持与 Eloquent
模型,我们可以使用特殊类型的业务接口——存储库。
假设 Person
是我们的 Eloquent
模型,那么我们可以像这样定义存储库接口:
<?php
interface PersonRepository
{
#[Repository]
public function get(int $personId): ?Person;
#[Repository]
public function save(Person $person): void;
}
Repository
注解告诉 Ecotone
该接口是存储库。Ecotone
将使用 getRepository()
方法找到相关的 Eloquent
模型类,并使用它来执行存储库方法。
Ecotone
的数据库抽象可以帮助我们提高数据库访问的生产力和可维护性。通过使用简单的接口来访问数据库,我们可以专注于业务逻辑,而无需担心底层的细节。
感兴趣的可以深入研究下Ecotone文档:https://docs.ecotone.tech/
发表评论