👨🏻💻 GraphQL como (uma espécie de) linguagem de programação
GraphQL, embora possua a linguagem GraphQL, normalmente não seria chamado de linguagem de programação, pois há tantas coisas que podemos fazer com linguagens de programação que não podemos fazer com GraphQL.
GraphQL é normalmente usado para buscar dados, por exemplo para renderizar um site no cliente, e para mutar dados, por exemplo para criar um post. E é praticamente isso.
(Outros usos são simplesmente combinações desses 2 casos anteriores. Por exemplo, um gateway de API pode buscar/mutar dados de um servidor interno, que não é exposto ao cliente.)
Acessando dados em GraphQL:
query PrintPostTitle($postID: ID!)
{
post(by: { id: $postID }) {
title
}
}...tem este equivalente (mais ou menos) em PHP:
function printPostTitle(int $postID)
{
$post = getPost($postID);
echo $post->title;
}(Todos os exemplos abaixo usarão PHP como linguagem de programação para comparação.)
Mutando dados em GraphQL:
query UpdatePost($postID: ID!, $title: String!)
{
updatePost(
by: { id: $postID },
input: { title: $title }
) {
title
}
}...tem este equivalente (mais ou menos) em PHP:
function updatePost(int $postID, string $title)
{
$post = getPost($postID);
$post->update(['title' => $title]);
}Isso é suficiente porque GraphQL é normalmente acessado a partir de um cliente (codificado em alguma linguagem de programação, como JavaScript, PHP, Java ou outra) que conterá a lógica do que fazer com os dados. Portanto, GraphQL não é usado sozinho, mas como um companheiro de outra coisa.
Mas se GraphQL pudesse ser usado sozinho, então muitos novos casos de uso poderiam ser resolvidos apenas usando GraphQL, permitindo que GraphQL seja implantado em ambientes inovadores e seja responsável por tarefas adicionais na pilha da aplicação.
Para que isso aconteça, porém, GraphQL deve suportar muitas das funcionalidades das linguagens de programação.
As funcionalidades de linguagens de programação que GraphQL suporta são limitadas. Por exemplo, usar a diretiva @include (ou @skip) e passar uma variável como input pode ser considerado (uma espécie de) lógica condicional:
query PrintPostProperties($postID: ID!, $addContent: Boolean!)
{
post(by: { id: $postID }) {
title
content @include(if: $addContent)
}
}Esta query tem este equivalente em PHP:
function printPostProperties(int $postID, bool $addContent)
{
$post = getPost($postID);
echo $post->title;
if ($addContent) {
echo $post->content;
}
}É praticamente isso. GraphQL não possui recursões, variáveis dinâmicas (onde seus valores são computados e atribuídos à variável em tempo de execução, não como input no dicionário), atribuições de variáveis (ex: atribuir a saída de um campo a uma variável, que pode então ser fornecida como argumento para outro campo), entre outros.
Considere como você implementaria uma solução, usando apenas GraphQL, para o seguinte problema:
- Criar um webhook a ser invocado por um serviço sempre que um novo usuário se cadastrar naquele serviço; o usuário pode ter se inscrito na newsletter (indicado pelo campo
marketing_optinno payload do webhook); nesse caso, o webhook deve registrar o e-mail do usuário (no campoemaildo payload do webhook) em uma lista do Mailchimp.
Você considera isso viável? fácil? difícil? impossível?
No Gato GraphQL, queremos resolver esse problema usando apenas GraphQL. E muitos outros problemas. Por isso, pensamos muito sobre como suportar características de linguagens de programação.
Vamos explorar quais funcionalidades de programação suportamos em nosso servidor GraphQL. Ao final deste post, veremos como podemos resolver esse problema.
Funcionalidade
Os campos em GraphQL normalmente trazem dados, como o título, conteúdo ou dados de um post. Mas também podemos implementar campos como "funcionalidade".
Por exemplo, imprimir a hora em PHP:
function printTime()
{
echo time();
}...pode ser feito com o campo _time em GraphQL:
{
_time
}Observe que a função time não pertence a nenhum tipo, portanto o campo _time também não. Como tal, é um campo global, e pode ser acessado em todos os tipos do schema GraphQL:
{
posts {
_time
}
}Outros exemplos de campos de funcionalidade são:
_arrayItem_arrayJoin_date_equals_inArray_intAdd_isEmpty_isNull_makeTime_objectProperty_sprintf_strContains_strRegexReplace_strSubstr
Funções
Podemos dividir unidades de lógica em funções, e ter uma função invocando outra função:
function printPostProperties(int $postID)
{
$post = getPost($postID);
printPostTitle();
printPostContent();
}
function printPostTitle(Post $post)
{
echo $post->title;
}
function printPostContent(Post $post)
{
echo $post->content;
}Em GraphQL, podemos igualmente dividir a operação query (ou mutation) do documento em múltiplas operações query, e fazer uma operação "depender" de outras, executando-as primeiro:
query PrintPostTitle($postID: ID!)
{
postWithTitle: post(by: { id: $postID }) {
title
}
}
query PrintPostContent($postID: ID!)
{
postWithContent: post(by: { id: $postID }) {
content
}
}
query PrintPostProperties
@depends(on: [
"PrintPostTitle",
"PrintPostContent"
])
{
# ...
}Nesta query, executar a query GraphQL passando ?operationName=PrintPostProperties para o endpoint executará primeiro as queries PrintPostTitle e PrintPostContent, e somente então PrintPostProperties.
Isso é possível por meio da Execução de Múltiplas Queries.
Variáveis Dinâmicas
Podemos computar um valor e atribuí-lo a uma variável em tempo de execução. Então, com base nesse valor, podemos executar condicionalmente alguma funcionalidade ou não:
function printPostProperties(int $postID)
{
$post = getPost($postID);
echo $post->title;
$addContent = isUserLoggedIn();
if ($addContent) {
echo $post->content;
}
}Em GraphQL, podemos "exportar" um valor sob uma variável dinâmica em alguma operação, e então ler esse valor em outra operação:
query ExportAddContent
{
addContent: isUserLoggedIn
@export(as: "addContent")
}
query PrintPostProperties($postID: ID!)
@depends(on: "ExportAddContent")
{
post(by: { id: $postID }) {
title
content @include(if: $addContent)
}
}Observe que a variável $addContent, que contém um valor computado em tempo de execução, é lida mas não declarada na operação PrintPostProperties, pois é uma variável dinâmica.
Executando funções condicionalmente
Uma alternativa ao exemplo anterior é agrupar a lógica em funções, e então executar condicionalmente uma função ou não dependendo do valor da variável dinâmica:
function printPostProperties(int $postID)
{
$post = getPost($postID);
printPostTitle();
$addContent = isUserLoggedIn();
if ($addContent) {
printPostContent();
}
}
function printPostTitle(Post $post)
{
echo $post->title;
}
function printPostContent(Post $post)
{
echo $post->content;
}Em GraphQL podemos adicionar a diretiva @include na operação:
query ExportAddContent
{
addContent: isUserLoggedIn
@export(as: "addContent")
}
query PrintPostTitle($postID: ID!)
{
postWithTitle: post(by: { id: $postID }) {
title
}
}
query PrintPostContent($postID: ID!)
@depends(on: "ExportAddContent")
@include(if: $addContent)
{
postWithContent: post(by: { id: $postID }) {
content
}
}
query PrintPostProperties
@depends(on: [
"PrintPostTitle",
"PrintPostContent"
])
{
# ...
}Agora, a operação PrintPostContent só será executada se $addContent for true.
Atribuindo variáveis, fornecendo-as como input
Vamos modificar levemente o exemplo anterior, em que a condição "addContent" estava vinculada ao usuário estar logado ou não.
Neste outro exemplo, "addContent" é true sempre que hoje for fim de semana, o que envolve alguma lógica para computar:
- Obter a data de hoje
- Formatá-la para o nome do dia, em minúsculas
- Verificar se é
"saturday"ou"sunday"
Em PHP:
function addContent()
{
$today = time();
$dayName = date('l', $today);
$lcDayName = strtolower($dayName);
$isWeekend = in_array(
$lcDayName,
['saturday', 'sunday']
);
return $isWeekend;
}
function printPostProperties(int $postID)
{
$post = getPost($postID);
echo $post->title;
$addContent = addContent();
if ($addContent) {
echo $post->content;
}
}Em GraphQL:
query ExportAddContent
{
today: _time
dayName: _date(format: "l", timestamp: $__today)
lcDayName: _strLowerCase(text: $__dayName)
isWeekend: _inArray(
value: $__lcDayName
array: ["saturday", "sunday"],
)
@export(as: "addContent")
}
query PrintPostProperties($postID: ID!)
@depends(on: "ExportAddContent")
{
post(by: { id: $postID }) {
title
content @include(if: $addContent)
}
}Na operação ExportAddContent, o valor de cada campo consultado está imediatamente disponível para os campos abaixo, sob a variável dinâmica $__fieldName. Dessa forma, a saída de um campo pode ser imediatamente usada como input para outro campo, já dentro da mesma operação.
Isso é possível graças ao Field to Input.
Modificando um valor dinamicamente
Neste exemplo em PHP, modificamos o valor de uma variável sempre que o usuário logado é um administrador, caso em que o conteúdo do post recebe um link adicionado para editar o post:
function isAdminUser()
{
$user = getCurrentUser();
return in_array("administrator", $user->roles);
}
function printPostContent(int $postID)
{
$post = getPost($postID);
$postContent = $post->content;
$isAdminUser = isAdminUser();
if ($isAdminUser) {
$postContent = sprintf(
'%s<p><a href="%s">%s</a></p>',
$postContent,
$post->edit_url,
'(Admin only) Edit post'
)
}
echo $postContent;
}Em GraphQL, podemos executar condicionalmente uma operação ou outra, produzindo valores diferentes para algum campo:
query InitializeDynamicVariables
{
isAdminUser: _echo(value: false)
@export(as: "isAdminUser")
}
query ExportConditionalVariables
@depends(on: "InitializeDynamicVariables")
{
me {
roleNames
isAdminUser: _inArray(
value: "administrator",
array: $__roleNames
)
@export(as: "isAdminUser")
}
}
query RetrieveContentForAdminUser($postId: ID!)
@depends(on: "ExportConditionalVariables")
@include(if: $isAdminUser)
{
post(by: { id : $postId }) {
originalContent: content
wpAdminEditURL
content: _sprintf(
string: "%s<p><a href=\"%s\">%s</a></p>",
values: [
$__originalContent,
$__wpAdminEditURL,
"(Admin only) Edit post"
]
)
}
}
query RetrieveContentForNonAdminUser($postId: ID!)
@depends(on: "ExportConditionalVariables")
@skip(if: $isAdminUser)
{
post(by: { id : $postId }) {
content
}
}
query ExecuteAll
@depends(on: [
"RetrieveContentForAdminUser",
"RetrieveContentForNonAdminUser"
])
{
# ...
}Usando as diretivas @include e @skip com a mesma variável dinâmica como input, as operações RetrieveContentForAdminUser e RetrieveContentForNonAdminUser são mutuamente exclusivas.
Iterando arrays
Digamos que queremos iterar os itens em um array e converter esses valores para maiúsculas:
function printUserRolesAsUppercase(int $userID)
{
$user = getUser($userID);
foreach ($user->roles as $role) {
echo strtoupper($role);
}
}Em GraphQL, podemos usar a diretiva @underEachArrayItem para iterar sobre os itens do array e fornecer cada um desses valores para a diretiva seguinte na cadeia, neste caso @strUpperCase:
query PrintUserRolesAsUppercase($userID: ID!)
{
user(by: { id: $userID }) {
roles
@underEachArrayItem
@strUpperCase
}
}Isso é possível graças às diretivas composáveis.
Operações CRUD em massa
CRUD significa Create (Criar), Read (Ler), Update (Atualizar) e Delete (Excluir); são as operações que aplicamos em recursos (posts, usuários, etc).
Ler em massa em PHP tem esta aparência:
function getPostTitles()
{
$posts = getPosts();
foreach ($posts as $post) {
echo $post->title;
}
}Este caso de uso é naturalmente satisfeito pelo GraphQL:
query GetPostTitles
{
posts {
title
}
}Atualizar em massa em PHP tem esta aparência:
function updatePostTitlesAsUppercase()
{
$posts = getPosts();
foreach ($posts as $post) {
$post->update(['title' => strtoupper($post->title)]);
}
}Executar atualizações em massa em GraphQL é normalmente suportado criando uma mutation dedicada updatePosts, que recebe os dados de todos os posts.
Não gosto dessa abordagem, pois ela efetivamente duplica o número de mutations no schema (uma para mutar o recurso único, uma para mutar múltiplos recursos), e precisamos manter a lógica para ambas:
updatePost+updatePostscreatePost+createPosts- etc
Na minha opinião, uma abordagem mais elegante é usar mutations aninhadas, onde a mutation Post.update é aplicada a cada um dos recursos consultados:
mutation UpdatePostTitlesAsUppercase
{
posts {
title
ucTitle: _strUpperCase(text: $__title)
update(
input: { title: $__ucTitle }
) {
status
post {
title
}
}
}
}A mesma abordagem funciona para excluir recursos:
function deletePosts()
{
$posts = getPosts();
foreach ($posts as $post) {
$post->delete();
}
}Em GraphQL:
mutation DeletePosts
{
posts {
delete {
status
}
}
}Para criação, não passamos os recursos pois eles ainda não existem; em vez disso, fornecemos um array com os dados de input para todos os recursos a criar:
function createPosts()
{
$postDataItems = [
[
'title' => 'First title',
'content' => 'First content',
],
[
'title' => 'Second title',
'content' => 'Second content',
],
];
foreach ($postDataItems as $postDataItem) {
$post = new Post($postDataItem['title'], $postDataItem['content']);
$post->save();
}
}Criar posts em massa em GraphQL usando uma única mutation createPost é um pouco complicado, mas é possível.
A ideia é iterar sobre o array com os dados de input, atribuir cada um sob uma variável dinâmica $input, e então executar a mutation createPost passando esse input. Por fim, obtemos os IDs resultantes dos posts criados sob a variável dinâmica $createdPostIDs e recuperamos seus dados:
mutation CreatePosts
@depends(on: "GetPostsAndExportData")
{
createdPostIDs: _echo(value: [
{
title: "First title",
content: "First content"
},
{
title: "Second title",
content: "Second content"
},
])
@underEachArrayItem(
passValueOnwardsAs: "input"
)
@applyField(
name: "createPost"
arguments: {
input: $input
},
setResultInResponse: true
)
@export(as: "createdPostIDs")
}
query RetrieveCreatedPosts
@depends(on: "CreatePosts")
{
createdPosts: posts(
filter: {
ids: $createdPostIDs,
}
) {
title
content
}
}Enviando uma requisição HTTP (e outras funções)
Enviar uma requisição HTTP para algum servidor web pode ser satisfeito por uma função dedicada em PHP, como file_get_contents ou curl_exec.
Usando file_get_contents:
$xml = file_get_contents("http://www.example.com/file.xml");Em GraphQL, a lógica para executar uma requisição HTTP pode ser satisfeita por um campo de funcionalidade, como _sendHTTPRequest:
query {
_sendHTTPRequest(input: {
url: "http://www.example.com/file.xml",
method: GET
}) {
xml: body
}
}O mesmo conceito se aplica para qualquer funcionalidade.
Por exemplo, acessamos o valor de uma constante em PHP assim:
$mailchimpUsername = constant('MAILCHIMP_API_CREDENTIALS_USERNAME');Podemos implementar um campo de funcionalidade correspondente em GraphQL:
{
mailchimpUsername: _env(name: "MAILCHIMP_API_CREDENTIALS_USERNAME")
}Resolvendo o desafio usando apenas GraphQL
Com todas as funcionalidades de linguagens de programação que acabamos de cobrir, agora somos capazes de usar apenas GraphQL para resolver o problema colocado anteriormente:
- Criar um webhook a ser invocado por um serviço sempre que um novo usuário se cadastrar naquele serviço; o usuário pode ter se inscrito na newsletter (indicado pelo campo
marketing_optinno payload do webhook); nesse caso, o webhook deve registrar o e-mail do usuário (no campoemaildo payload do webhook) em uma lista do Mailchimp.
A solução é usar uma query persistida GraphQL como webhook, com esta query:
query HasSubscribedToNewsletter {
hasSubscriberOptIn: _httpRequestHasParam(name: "marketing_optin")
subscriberOptIn: _httpRequestStringParam(name: "marketing_optin")
isNotSubscriberOptInNAValue: _notEquals(value1: $__subscriberOptIn, value2: "NA")
subscribedToNewsletter: _and(values: [$__hasSubscriberOptIn, $__isNotSubscriberOptInNAValue])
@export(as: "subscribedToNewsletter")
}
query MaybeCreateContactOnMailchimp
@depends(on: "HasSubscribedToNewsletter")
@include(if: $subscribedToNewsletter)
{
subscriberEmail: _httpRequestStringParam(name: "email")
mailchimpUsername: _env(name: "MAILCHIMP_API_CREDENTIALS_USERNAME")
mailchimpPassword: _env(name: "MAILCHIMP_API_CREDENTIALS_PASSWORD")
mailchimpListMembersJSONObject: _sendJSONObjectItemHTTPRequest(input: {
url: "https://us7.api.mailchimp.com/3.0/lists/{listCode}/members",
method: POST,
options: {
auth: {
username: $__mailchimpUsername,
password: $__mailchimpPassword
},
json: {
email_address: $__subscriberEmail,
status: "subscribed"
}
}
})
}Nesta solução, a operação MaybeCreateContactOnMailchimp, que executa a requisição HTTP contra a API do Mailchimp, será executada condicionalmente, dependendo do valor do campo marketing_optin.
(Leia o post do blog 👨🏻🏫 Query GraphQL para enviar automaticamente os assinantes da newsletter do InstaWP para o Mailchimp para ver como esta query funciona.)
GraphQL é mais poderoso do que você pensava!
GraphQL pode ser usado para muito mais do que simplesmente buscar e mutar dados... Adaptar dados, modificar dinamicamente a saída, personalizar conteúdo para diferentes contextos, criar um gateway de API com apenas algumas linhas de código, e muito mais.
Ao suportar funcionalidades de linguagens de programação, podemos resolver o desafio acima usando apenas GraphQL, e evitar implantar um cliente para acompanhá-lo. Estamos assim simplificando a pilha da aplicação: menos partes móveis, menos complexidade, menos código para depurar, menos tecnologias para lidar.
GraphQL é incrível 🤘