π¨π»βπ» GraphQL als een (soort) programmeertaal
GraphQL, hoewel het de GraphQL-taal heeft, zou normaal gesproken geen programmeertaal worden genoemd, omdat er zoveel dingen zijn die we met programmeertalen kunnen doen die we met GraphQL niet kunnen.
GraphQL wordt normaal gesproken gebruikt om gegevens op te halen, bijvoorbeeld om een website aan de clientkant te renderen, en om gegevens te muteren, bijvoorbeeld om een bericht aan te maken. En dat is zo'n beetje alles.
(Andere toepassingen zijn gewoon combinaties van deze 2 vorige gevallen. Zo kan een API-gateway gegevens ophalen/muteren van een interne server die niet aan de client is blootgesteld.)
Gegevens ophalen in GraphQL:
query PrintPostTitle($postID: ID!)
{
post(by: { id: $postID }) {
title
}
}...heeft dit (soort van) equivalent in PHP:
function printPostTitle(int $postID)
{
$post = getPost($postID);
echo $post->title;
}(Alle voorbeelden hieronder gebruiken PHP als programmeertaal ter vergelijking.)
Gegevens muteren in GraphQL:
query UpdatePost($postID: ID!, $title: String!)
{
updatePost(
by: { id: $postID },
input: { title: $title }
) {
title
}
}...heeft dit (soort van) equivalent in PHP:
function updatePost(int $postID, string $title)
{
$post = getPost($postID);
$post->update(['title' => $title]);
}Dit is voldoende omdat GraphQL normaal gesproken wordt gebruikt vanuit een client (geschreven in een programmeertaal, zoals JavaScript, PHP, Java of andere) die de logica bevat van wat er met de gegevens moet worden gedaan. GraphQL wordt dus niet op zichzelf gebruikt, maar als aanvulling op iets anders.
Maar als GraphQL op zichzelf zou kunnen worden gebruikt, dan zouden veel nieuwe use cases kunnen worden opgelost met alleen GraphQL, waardoor GraphQL in nieuwe omgevingen kan worden ingezet en verantwoordelijk is voor aanvullende taken in de applicatiestack.
Daarvoor moet GraphQL echter veel kenmerken van programmeertalen ondersteunen.
De programmeertaalkenmerken die GraphQL ondersteunt zijn beperkt. Het gebruik van directive @include (of @skip) en het doorgeven van een variabele als invoer kan bijvoorbeeld worden beschouwd als (soort van) conditionele logica:
query PrintPostProperties($postID: ID!, $addContent: Boolean!)
{
post(by: { id: $postID }) {
title
content @include(if: $addContent)
}
}Deze query heeft dit PHP-equivalent:
function printPostProperties(int $postID, bool $addContent)
{
$post = getPost($postID);
echo $post->title;
if ($addContent) {
echo $post->content;
}
}Dat is zo'n beetje alles. GraphQL mist recursies, dynamische variabelen (waarbij hun waarden worden berekend en aan de variabele worden toegewezen tijdens runtime, niet als invoer in het woordenboek), variabeletoewijzingen (bijv. het toewijzen van een velduitvoer aan een variabele, die vervolgens als argument aan een ander veld kan worden meegegeven), en andere.
Bedenk hoe je een oplossing zou implementeren, met alleen GraphQL, voor het volgende probleem:
- Maak een webhook die door een dienst wordt aangeroepen wanneer een nieuwe gebruiker zich aanmeldt bij die dienst; de gebruiker heeft zich mogelijk aangemeld voor de nieuwsbrief (aangegeven door het veld
marketing_optinin de payload van de webhook); in dat geval moet de webhook het e-mailadres van de gebruiker (in het veldemailin de payload van de webhook) registreren in een Mailchimp-lijst.
Denk je dat dit haalbaar is? gemakkelijk? moeilijk? onmogelijk?
Bij Gato GraphQL, willen we dit probleem oplossen met alleen GraphQL. En nog veel meer problemen. Daarom hebben we goed nagedacht over hoe we kenmerken van programmeertalen kunnen ondersteunen.
Laten we verkennen welke programmeerfuncties we hebben ondersteund op onze GraphQL-server. Aan het einde van dit bericht zullen we zien hoe we dat probleem kunnen oplossen.
Functionaliteit
Velden in GraphQL brengen normaal gesproken gegevens, zoals de titel, inhoud of gegevens van een bericht. Maar we kunnen velden ook implementeren als "functionaliteit".
Zo kan het afdrukken van de tijd in PHP:
function printTime()
{
echo time();
}...worden gedaan met het veld _time in GraphQL:
{
_time
}Merk op dat functie time niet tot een type behoort, en het veld _time dus ook niet. Als zodanig is het een globaal veld en het is toegankelijk onder elk type uit het GraphQL-schema:
{
posts {
_time
}
}Andere voorbeelden van functionaliteitsvelden zijn:
_arrayItem_arrayJoin_date_equals_inArray_intAdd_isEmpty_isNull_makeTime_objectProperty_sprintf_strContains_strRegexReplace_strSubstr
Functies
We kunnen logica-eenheden opsplitsen in functies, en een functie een andere functie laten aanroepen:
function printPostProperties(int $postID)
{
$post = getPost($postID);
printPostTitle();
printPostContent();
}
function printPostTitle(Post $post)
{
echo $post->title;
}
function printPostContent(Post $post)
{
echo $post->content;
}In GraphQL kunnen we evenzo de query-bewerking (of mutation) in het document opsplitsen in meerdere query-bewerkingen, en een bewerking laten "afhangen" van andere bewerkingen, waardoor die eerst worden uitgevoerd:
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"
])
{
# ...
}Bij het uitvoeren van de GraphQL-query met ?operationName=PrintPostProperties naar het endpoint worden eerst de queries PrintPostTitle en PrintPostContent uitgevoerd, en pas daarna PrintPostProperties.
Dit is mogelijk via Uitvoering van Meerdere Queries.
Dynamische Variabelen
We kunnen een waarde berekenen en deze tijdens runtime aan een variabele toewijzen. Vervolgens kunnen we op basis van die waarde conditioneel bepaalde functionaliteit uitvoeren of niet:
function printPostProperties(int $postID)
{
$post = getPost($postID);
echo $post->title;
$addContent = isUserLoggedIn();
if ($addContent) {
echo $post->content;
}
}In GraphQL kunnen we een waarde "exporteren" onder een dynamische variabele in een bewerking, en deze waarde vervolgens in een andere bewerking uitlezen:
query ExportAddContent
{
addContent: isUserLoggedIn
@export(as: "addContent")
}
query PrintPostProperties($postID: ID!)
@depends(on: "ExportAddContent")
{
post(by: { id: $postID }) {
title
content @include(if: $addContent)
}
}Merk op dat de variabele $addContent, die een waarde bevat die tijdens runtime is berekend, wordt uitgelezen maar niet gedeclareerd in bewerking PrintPostProperties, omdat het een dynamische variabele is.
Conditioneel functies uitvoeren
Een alternatief voor het vorige voorbeeld is om logica te groeperen in functies, en vervolgens conditioneel een functie uit te voeren of niet, afhankelijk van de waarde van de dynamische variabele:
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;
}In GraphQL kunnen we de directive @include toevoegen aan de bewerking:
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"
])
{
# ...
}Nu wordt bewerking PrintPostContent alleen uitgevoerd als $addContent true is.
Variabelen toewijzen en ze als invoer teruggeven
Laten we het vorige voorbeeld iets aanpassen, waarbij de voorwaarde "addContent" was gekoppeld aan of de gebruiker ingelogd was of niet.
In dit andere voorbeeld is "addContent" true wanneer het vandaag weekend is, wat enige logica vereist om te berekenen:
- Haal de datum van vandaag op
- Formateer deze naar de naam van de dag, in kleine letters
- Controleer of het
"saturday"of"sunday"is
In 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;
}
}In 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)
}
}In bewerking ExportAddContent is de waarde van elk opgevraagd veld direct beschikbaar voor de velden eronder, onder dynamische variabele $__fieldName. Op deze manier kan de uitvoer van een veld direct als invoer voor een ander veld worden gebruikt, al binnen dezelfde bewerking.
Dit is mogelijk dankzij Field to Input.
Een waarde dynamisch aanpassen
In dit voorbeeld in PHP passen we de waarde van een variabele aan wanneer de ingelogde gebruiker een beheerder is, in welk geval de inhoud van het bericht wordt uitgebreid met een link om het bericht te bewerken:
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;
}In GraphQL kunnen we conditioneel de ene of de andere bewerking uitvoeren, waardoor verschillende waarden worden geproduceerd voor een bepaald veld:
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"
])
{
# ...
}Door directives @include en @skip te gebruiken met dezelfde dynamische variabele als invoer, zijn bewerkingen RetrieveContentForAdminUser en RetrieveContentForNonAdminUser wederzijds uitsluitend.
Arrays doorlopen
Stel dat we de items in een array willen doorlopen en die waarden naar hoofdletters willen omzetten:
function printUserRolesAsUppercase(int $userID)
{
$user = getUser($userID);
foreach ($user->roles as $role) {
echo strtoupper($role);
}
}In GraphQL kunnen we directive @underEachArrayItem gebruiken om over de array-items te itereren en elk van die waarden door te geven aan de volgende directive in de keten, in dit geval @strUpperCase:
query PrintUserRolesAsUppercase($userID: ID!)
{
user(by: { id: $userID }) {
roles
@underEachArrayItem
@strUpperCase
}
}Dit is mogelijk dankzij composeerbare directives.
Bulk CRUD-bewerkingen
CRUD staat voor Create (Aanmaken), Read (Lezen), Update (Bijwerken) en Delete (Verwijderen); dit zijn de bewerkingen die we toepassen op resources (berichten, gebruikers, enz.).
Bulk lezen in PHP ziet er zo uit:
function getPostTitles()
{
$posts = getPosts();
foreach ($posts as $post) {
echo $post->title;
}
}Deze use case wordt van nature ondersteund door GraphQL:
query GetPostTitles
{
posts {
title
}
}Bulk bijwerken in PHP ziet er zo uit:
function updatePostTitlesAsUppercase()
{
$posts = getPosts();
foreach ($posts as $post) {
$post->update(['title' => strtoupper($post->title)]);
}
}Het uitvoeren van bulk-updates in GraphQL wordt normaal gesproken ondersteund door een speciale mutatie updatePosts te maken die de gegevens voor alle berichten accepteert.
Ik ben geen fan van deze aanpak, omdat het het aantal mutaties in het schema effectief verdubbelt (één om de enkele resource te muteren, één om meerdere resources te muteren), en we de logica voor beide moeten onderhouden:
updatePost+updatePostscreatePost+createPosts- enz.
Een elegantere aanpak is naar mijn mening het gebruik van geneste mutaties, waarbij de mutatie Post.update wordt toegepast op elk van de opgevraagde resources:
mutation UpdatePostTitlesAsUppercase
{
posts {
title
ucTitle: _strUpperCase(text: $__title)
update(
input: { title: $__ucTitle }
) {
status
post {
title
}
}
}
}Dezelfde aanpak werkt voor het verwijderen van resources:
function deletePosts()
{
$posts = getPosts();
foreach ($posts as $post) {
$post->delete();
}
}In GraphQL:
mutation DeletePosts
{
posts {
delete {
status
}
}
}Bij het aanmaken geven we de resources niet mee omdat ze nog niet bestaan; in plaats daarvan geven we een array met de gegevensinvoer voor alle te maken resources mee:
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();
}
}Berichten in bulk aanmaken in GraphQL met een enkele createPost-mutatie is wat lastig, maar het is desalniettemin haalbaar.
Het idee is om over de array met gegevensinvoer te itereren, elk element toe te wijzen onder een dynamische variabele $input, en vervolgens de mutatie createPost uit te voeren met die invoer. Tot slot halen we de resulterende ID's van de aangemaakte berichten op onder de dynamische variabele $createdPostIDs, en halen we hun gegevens op:
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
}
}Een HTTP-verzoek verzenden (en andere functies)
Het verzenden van een HTTP-verzoek naar een webserver kan worden gedaan via een speciale functie in PHP, zoals file_get_contents of curl_exec.
Met file_get_contents:
$xml = file_get_contents("http://www.example.com/file.xml");In GraphQL kan de logica voor het uitvoeren van een HTTP-verzoek worden ingevuld via een functionaliteitsveld, zoals _sendHTTPRequest:
query {
_sendHTTPRequest(input: {
url: "http://www.example.com/file.xml",
method: GET
}) {
xml: body
}
}Hetzelfde concept geldt voor elke functionaliteit.
Zo krijgen we in PHP toegang tot de waarde van een constante:
$mailchimpUsername = constant('MAILCHIMP_API_CREDENTIALS_USERNAME');We kunnen een overeenkomstig functionaliteitsveld implementeren in GraphQL:
{
mailchimpUsername: _env(name: "MAILCHIMP_API_CREDENTIALS_USERNAME")
}De uitdaging oplossen met alleen GraphQL
Met alle programmeertaalkenmerken die we zojuist hebben behandeld, zijn we nu in staat om alleen GraphQL te gebruiken om het eerder gestelde probleem op te lossen:
- Maak een webhook die door een dienst wordt aangeroepen wanneer een nieuwe gebruiker zich aanmeldt bij die dienst; de gebruiker heeft zich mogelijk aangemeld voor de nieuwsbrief (aangegeven door het veld
marketing_optinin de payload van de webhook); in dat geval moet de webhook het e-mailadres van de gebruiker (in het veldemailin de payload van de webhook) registreren in een Mailchimp-lijst.
De oplossing is om een GraphQL persisted query als webhook te gebruiken met deze 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"
}
}
})
}In deze oplossing wordt bewerking MaybeCreateContactOnMailchimp, die het HTTP-verzoek uitvoert naar de API van Mailchimp, conditioneel uitgevoerd, afhankelijk van de waarde van het veld marketing_optin.
(Lees de blogpost π¨π»βπ« GraphQL query to automatically send the newsletter subscribers from InstaWP to Mailchimp om te zien hoe deze query werkt.)
GraphQL is krachtiger dan je dacht!
GraphQL kan voor veel meer worden gebruikt dan alleen het ophalen en muteren van gegevens... Gegevens aanpassen, de uitvoer dynamisch wijzigen, inhoud aanpassen voor verschillende contexten, een API-gateway maken met amper een paar regels code, en nog veel meer.
Door programmeertaalkenmerken te ondersteunen, kunnen we de bovenstaande uitdaging oplossen met alleen GraphQL en vermijden we dat we een client moeten inzetten als begeleider. We vereenvoudigen zo de applicatiestack: minder bewegende onderdelen, minder complexiteit, minder code om te debuggen, minder technologieΓ«n om mee te dealen.
GraphQL rocks π€