Khả năng scripting thông qua các meta-directive
Giả sử chúng ta có một directive @strTitleCase có thể được áp dụng trên một trường trong query, chuyển đổi giá trị của nó từ "hello world!" thành "Hello World!", vì vậy việc áp dụng nó chỉ có ý nghĩa trên các trường kiểu String.
Khi chạy query này:
{
post(by: { id: 1 }) {
title @strTitleCase
}
}...nó sẽ trả về:
{
"data": {
"post": {
"title": "Hello World!"
}
}
}Bây giờ, giả sử kiểu của trường là [String] (hoặc [String!]), như trong trường hợp này:
type Post {
categoryNames: [String!]
}Điều gì sẽ xảy ra khi áp dụng directive @strTitleCase trên trường categoryNames khi chạy query này?
{
post(by: { id: 1 }) {
categoryNames @strTitleCase
}
}Lý tưởng nhất, kết quả trả về sẽ là sự chuyển đổi của mỗi giá trị String bên trong mảng:
{
"data": {
"post": {
"categoryNames": [
"Software",
"Web Development",
"Mobile App"
]
}
}
}Để điều đó xảy ra, directive resolver cho @strTitleCase sẽ cần kiểm tra xem đầu vào có phải là một mảng hay không, và xử lý tương ứng (đoạn code PHP này chỉ là ví dụ, phương thức thực tế trong plugin khác):
function applyDirective(mixed $value, array $schemaDef): mixed
{
// Convert each item in an array to title case
if ($schemaDef['isArray']) {
return array_map(ucwords(...), $value);
}
// Convert the String value to title case
return ucwords($value);
}Điều đó không quá khó. Nhưng sau đó, điều gì sẽ xảy ra nếu trường là một mảng của mảng kiểu String, tức là [[String]]? Dù phức tạp hơn một chút, directive vẫn có thể xử lý được:
function applyDirective(mixed $value, array $schemaDef): mixed
{
// Convert each item in an array of arrays to title case
if ($schemaDef['isArrayOfArrays']) {
return array_map(
fn (array $array) => array_map(ucwords(...), $array),
$value
);
}
// Convert each item in an array to title case
if ($schemaDef['isArray']) {
return array_map(ucwords(...), $value);
}
// Convert the String value to title case
return ucwords($value);
}Và rồi, nếu là [[[String]]] hay [[[[String]]]] thì sao? Lúc đó việc triển khai bắt đầu trở nên khó khăn.
Tệ hơn nữa, logic boilerplate bổ sung này sẽ cần được triển khai cho bất kỳ directive nào có thể được áp dụng trên mảng. Ví dụ, để triển khai directive @strUpperCase, logic bổ sung này cũng sẽ cần thiết:
function applyDirective(mixed $value, array $schemaDef): mixed
{
// Convert each item in an array of arrays to uppercase
if ($schemaDef['isArrayOfArrays']) {
return array_map(
fn (array $array) => array_map(strtoupper(...), $array),
$value
);
}
// Convert each item in an array to uppercase
if ($schemaDef['isArray']) {
return array_map(strtoupper(...), $value);
}
// Convert the String value to uppercase
return strtoupper($value);
}Trông không được đẹp lắm, phải không?
Giải pháp: thay đổi đầu vào của một directive thông qua directive khác
Đây chính là lúc việc áp dụng một directive để thay đổi hành vi của directive khác trở nên hữu ích.
Thay vì phải xử lý từng lũy thừa mảng có thể có của trường (tức là String, [String], [[String]], [[[String]]], v.v.), @strTitleCase chỉ cần xử lý trường hợp cơ bản là String:
function applyDirective(mixed $value, array $schemaDef): mixed
{
// The input will always be `String`
// Convert the String value to title case
return ucwords($value);
}Sau đó, một directive khác là @underEachArrayItem có thể thay đổi hành vi của nó bằng cách:
- Chuyển đổi đầu vào đơn kiểu
[String]thành một mảng các đầu vào kiểuString - Lặp qua các phần tử trong mảng này và, với mỗi phần tử, gọi và áp dụng directive phía sau (
@strTitleCase), directive này sau đó sẽ nhận được đầu vào kiểuString - Chuyển đổi ngược mảng các giá trị
Stringthành một giá trị[String]duy nhất
Khi đó chúng ta có thể thực thi query này:
{
post(by: { id: 1 }) {
categoryNames @underEachArrayItem @strTitleCase
}
}Gif này minh họa @underEachArrayItem hoạt động trong thực tế:

Điểm hay của giải pháp này là nó tách rời độ sâu của mảng khỏi quá trình triển khai directive. Nếu đầu vào có kiểu [[String]], tất cả những gì chúng ta cần làm là thêm một @underEachArrayItem bổ sung, cái này sẽ thay đổi @underEachArrayItem vốn đang thay đổi directive mục tiêu:
{
customerAllNames @underEachArrayItem @underEachArrayItem @strTitleCase
}...tạo ra kết quả:
{
"data": {
"customerAllNames": [
[
"John",
"Edward",
"Stevenson"
],
[
"Samantha",
"Perkins"
],
[
"Michael",
"Edward",
"Higgs"
]
]
}
}Như vậy, ta có thể thấy rằng một directive thay đổi directive khác cũng có thể xảy ra trong một pipeline của các directive, trong đó một directive ảnh hưởng đến directive phía sau nó, và chính chúng lại được thay đổi bởi directive phía trước.
Chúng ta gọi @underEachArrayItem là một "meta-directive": một directive thay đổi hành vi của directive khác. Qua đó, nó cung cấp cho nhà phát triển khả năng "meta-scripting", để thêm một số logic lập trình bên trong GraphQL query.
Định dạng GraphQL query
Vì khoảng trắng không có giá trị ngữ nghĩa, chúng ta có thể định dạng query và SDL để thể hiện rõ hơn sự lồng nhau:
{
customerAllNames
@underEachArrayItem
@underEachArrayItem
@strTitleCase
}Xác định một pipeline gồm các directive lồng nhau
@underEachArrayItem biết rằng nó phải thay đổi hành vi của @strTitleCase như thế nào? Trong ví dụ trước, đó là vì nó được đặt ngay trước @strTitleCase. Nhưng điều gì sẽ xảy ra khi chúng ta có thêm một directive khác ngay sau chúng?
Ví dụ, trong query này:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem
@strTitleCase
@strTranslate(to: "es")
}
}...@underEachArrayItem cũng nên thay đổi hành vi của directive @strTranslate, vì directive này cũng phải được áp dụng trên String, tạo ra kết quả sau:
{
"data": {
"post": {
"categoryNames": [
"Software",
"Desarrollo web",
"Aplicación movil"
]
}
}
}Tuy nhiên, một directive được đặt phía sau cũng có thể cần được áp dụng trên mảng, chứ không phải trên từng giá trị String riêng lẻ. Ví dụ, directive @arrayPad dưới đây thêm các phần tử còn thiếu vào mảng với các giá trị mặc định, vì vậy nó không nên bị ảnh hưởng bởi @underEachArrayItem:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem
@strTitleCase
@arrayPad(length: 5, value: "undefined")
}
}...tạo ra kết quả sau:
{
"data": {
"post": {
"categoryNames": [
"Software",
"Web Development",
"Mobile App",
"undefined",
"undefined"
]
}
}
}Để phân biệt hai tình huống này, chúng ta giới thiệu đối số affectDirectivesUnderPos cho @underEachArrayItem, đối số này xác định vị trí tương đối của các directive cần bị ảnh hưởng, dưới dạng một mảng Int.
Trong query dưới đây, @underEachArrayItem biết nó cần được áp dụng trên @strTitleCase và @strTranslate, vì chúng được đặt ở vị trí tương đối 1 và 2 so với nó:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem(affectDirectivesUnderPos: [1, 2])
@strTitleCase
@strTranslate(to: "es")
}
}Trong query này, @underEachArrayItem chỉ được áp dụng trên @strTitleCase (vị trí tương đối 1) nhưng không áp dụng trên @arrayPad:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem(affectDirectivesUnderPos: [1])
@strTitleCase
@arrayPad(length: 5, value: "undefined")
}
}Giá trị mặc định của affectDirectivesUnderPos được đặt là [1], vì vậy nếu không được chỉ định, directive sẽ luôn được áp dụng trên directive ngay sau nó. Query trên do đó tương đương với query này:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem
@strTitleCase
@arrayPad(length: 5, value: "undefined")
}
}Chúng ta có thể xác định bất kỳ tổ hợp nào của các directive bị ảnh hưởng bởi meta-directive và các directive không bị ảnh hưởng:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem(affectDirectivesUnderPos: [1, 2])
@strTitleCase
@strTranslate(to: "es")
@arrayPad(length: 5, value: "undefined")
}
}