kepawni / serge
从您的 GraphQL 模式生成 CQRS/ES 模型类。
Requires
- ext-dom: *
- ext-json: *
- ext-pdo: *
- kepawni/twilted: ^1.2
- webonyx/graphql-php: ^0.13
Requires (Dev)
README
编写您的 GraphQL 模式,让 serge 生成您的
- 命令处理器
- 聚合
- 事件类型
- 值对象
用于 CQRS 和事件源。
快速开始
运行 vendor/bin/serge-codegen
,并注意默认配置出现在您的当前工作目录中。更改设置并再次运行脚本。这将为您提供一个示例 GraphQL 模式进行测试。使用它来定义您的模型,从现在开始(同时放置两个文件),再次调用 vendor/bin/serge-codegen
将为您生成构成域模型的类文件。
重要! 使用 Git 或其他代码版本控制,因为这些类将在每次调用代码生成器时被覆盖。或者,您可以在配置文件中更改目标目录,并仔细地将类复制到您的位置。
GraphQL 模式
要使此功能正常工作,让我们从我们的 GraphQL 模式的最小需求开始。我们将关注命令端,因此我们将查询部分保持最小。实际上,建议为查询端使用另一个面向 Web 的端点。
schema {
query: CqrsQuery
mutation: CqrsAggregateMutators
}
type CqrsQuery {
status: Boolean!
}
查询部分必须在有效的 GraphQL 模式中存在,因此我们定义 CqrsQuery(顺便说一句,您可以随意命名它)为一个常规的 类型。为了满足类型至少有一个字段的条件,我们定义 状态,这样我们就有一个简单的方法来测试我们的端点是否正常工作。这可以捕获很多问题,如缺少依赖项、错误的配置路径或服务器实例没有按计划启动。
现在对于 突变 部分,它由另一个名为 CqrsAggregateMutators 的 类型 覆盖(再次,如果您喜欢,可以更改该名称)。当我们定义此 类型 时,我们为模型中的每个聚合添加一个字段
声明聚合
type CqrsAggregateMutators {
Customer(id: ID!): Customer!
Invoice(id: ID!): Invoice!
}
这些字段需要一个参数来传递聚合 ID,因此我们使用 id: ID!
作为硬编码的约定。这些字段的返回类型必须与它们的名称匹配,且不可为空,这也是一个固定的约定。这样,我们现在已经声明了模型中可用的聚合,并可以转向它们应该处理的命令。
为每个聚合定义命令
因此,我们为每个聚合定义另一个 类型
,并将命令作为字段添加。
type Invoice {
chargeCustomer(customerId: ID!, invoiceNumber: String, invoiceDate: Date): ID!
appendLineItem(item: LineItem!): Boolean!
correctMistypedInvoiceDate(invoiceDate: Date!): Boolean!
overrideDueDate(dueDate: Date!): Boolean!
removeLineItemByPosition(position: Int!): Boolean!
}
返回类型遵循另一个简单的约定
ID!
用于工厂命令,该命令将新的聚合引入到存在状态Boolean!
用于常规命令,该命令将现有聚合进行变更
字段名称应该是小写,因为这些将是此聚合提供的公共方法。
定义值对象
您可能会注意到,这些命令同时使用内置类型(例如 String
)和复杂但未定义的类型,如 LineItem
。这些是我们的值对象,这就是我们如何将它们定义为 输入 类型
input LineItem {
quantity: Float!
price: Money!
title: String
}
input Money {
amount: Float!
currency: String!
}
如您所见,它们甚至可以相互引用,形成一个复杂且灵活的类型系统。
通过这些步骤,我们已经定义了代码生成器构建模型类所需的所有内容,但既然值对象可以如此轻松地定义,为什么不用这个机制来定义域事件呢?
定义事件类型
interface InvoiceEvents {
CustomerWasCharged(customerId: ID!, invoiceNumber: String, invoiceDate: Date): Boolean!
LineItemWasAppended(item: LineItem!): Boolean!
MistypedInvoiceDateWasCorrected(invoiceDate: Date!): Boolean!
DueDateWasOverridden(dueDate: Date!): Boolean!
LineItemWasRemoved(position: Int!): Boolean!
}
为了区分值对象和事件,我们在本处使用接口类型,而另一种约定要求我们将基于发出它们的聚合事件分组,这在名称(聚合名称 + Events
)中体现出来。这次,字段名称以大写字母开头,因为这些将被转换为事件类,参数将是事件的属性。这里Boolean!
的返回类型只是另一种约定。
XML配置
上面的GraphQL模式定义了我们模型的内部结构,但为了生成实际的类文件,我们需要更多关于路径和命名空间等上下文信息。这个差距将通过文件serge.config.xml
(名称是固定的)来弥补。
如果您是第一次调用代码生成器,当没有此类配置文件时,它将为您生成一个。请参阅快速入门指南,了解如何从头开始。