GraphQL is a specification, not an implementation for a specific language. As such, there are GraphQL servers for many languages, including JavaScript, PHP, Rust, Python, Go, and others.
graphql-js
, the first GraphQL server, is coded in JavaScript. It is also the reference implementation of the GraphQL specification, so other GraphQL servers may follow the architectural decisions set by graphql-js
for their own solutions.
Different languages have different characteristics, however, and porting the solution for a JavaScript-based server might not produce optimal results for servers based on other languages, or with a different philosophy on how the application is expected to work. Instead, having the room to deviate from these guidelines might provide better results.
For this write-up, I will explore how building a GraphQL solution for WordPress may diverge from the standard JavaScript guidelines on three circumstances:
I will use my own plugin GraphQL API for WordPress to demonstrate the WordPress approach.
For the comparison, I will draw on the different characteristics between JavaScript and PHP, on the underlying philosophies driving their communities, and on the accepted concepts and standard ways employed when working with these technologies.
I will also explore whether a PHP-based GraphQL server could be able to offer custom features, which cannot be part of the GraphQL specification because graphql-js
cannot support them.
Let’s start.
A WordPress application is highly extensible through plugins. Within the context of GraphQL, we may want plugins be able to modify:
In this section, we’ll explore the second element: How could the GraphQL service support modifying the results of the query?
WordPress uses hooks to modify behavior. Hooks are simple pieces of code that can override a value or execute an action whenever triggered.
In this example, function getBlockCategories
reacts to filter block_categories
to modify the block categories enabled in the WordPress editor:
class AbstractBlockCategory { function initialize() { add_filter( 'block_categories', [$this, 'getBlockCategories'] ); } public function getBlockCategories(array $categories): array { return [ ...$categories, [ 'slug' => 'graphql-api-access-control', 'title' => __('Access Control for GraphQL', 'graphql-api'), ], ]; } }
Hooks are simple, versatile, and powerful; they can be abused, but when well implemented, they make the application highly extensible in ways that the developer did not plan in advance.
Influenced by this WordPress solution, I’ve searched for a similar solution for GraphQL and came to the conclusion that directives can be considered their equivalent.
A directive is like a WordPress hook: it is a function that modifies the value of a field, thus augmenting some other functionality. As its counterpart, it is simple, versatile, and powerful.
For instance, let’s say we retrieve a list of post titles with this query:
query { posts { title } }
These results are in English. How can we translate them to French? A directive @translate
applied on field title
, which gets the value of the field as an input, calls the Google Translate API to translate the title
and returns this output, as in this query:
query { posts { title @translate(from:"en", to:"fr") } }
I find the use case for extensibility quite clear: given a value for field title
, modify it in any desired way through a directive. In this case, its modification is the translation to French through @translate
, but it could also be converting it to upper-/lowercase through @upperCase
and @lowerCase
, or anything else.
But does the JS community share my convictions?
I believe my previous solution is not widely accepted within the (mostly working with JS) GraphQL community.
When describing this solution, I would be rebuked on how terrible query directives are, as in this comment on Reddit:
The presence of extensive directives on the query side feels like “code smell” to me
That comment thread ended up in a fight, which I’m not proud of 😔. It’s incredible how even directives can become a motive of allegiance, hostility and contention! 🤣
The alternative solution I was offered is to use arguments in the field itself, like this:
query { posts { title(translateFrom: "en", translateTo: "fr") } }
I’ve come across this solution being exhorted in other places as well — for instance, here.
For me, using field arguments is a poor solution; it is mixing the logic from translation with the logic of resolving a field, all within that field resolver. It might work, but it is not particularly elegant.
Also, what happens if we need to translate the excerpt, too? It’s very easy with directives since they can be readily applied to any field without any change, as in this query:
query { posts { title @translate(from:"en", to:"fr") excerpt @translate(from:"en", to:"fr") } }
As a field argument, though, the translation logic would also need to “pollute” the resolution for field excerpt
:
query { posts { title(translateFrom: "en", translateTo: "fr") excerpt(translateFrom: "en", translateTo: "fr") } }
Different developers working with different technologies will have different ways of architecting solutions, and as long as they work, there’s no issue — there is no right or wrong.
However, concerning the specific topic at hand (extensibility of resolving GraphQL queries), will using field arguments to replace directives always work? Or only when all information is known in advance, as when coding a GraphQL API for one’s own company, or for a specific client?
Would it work given the WordPress expectation to make the service always extensible, even if we don’t know which other plugin will be extending it, or what custom requirements may the user have?
And given WordPress’ extreme user-friendliness, which means that functionality can be enabled through the user interface, could the GraphQL schema be extended without touching a line of code?
No, I don’t think it will: using field arguments, a developer will always need to know what integration must be done on the GraphQL schema, and it must be carried out through code. Hence, this methodology is not suitable for the WordPress way of doing things.
Using directives, though, all logic is cleanly and elegantly decoupled, extensibility can be achieved on the query itself, and it can even be integrated to the schema through configuration, and via a user interface (in which case we could even avoid exposing the query directives to the client side).
I don’t think this is an issue of JavaScript vs. PHP/WordPress as technologies, but about their underlying philosophies and methodologies of work. While JS is more developer-friendly, WordPress would rather prioritize the needs of the end user over convenience for the developer.
(As a side note, a similar discussion has been raging on lately regarding a supposed rivalry between WordPress and the Jamstack, about which I have my own opinion.)
As for myself, when I promote solutions based on directives, I’m coming from the “make it extensible through hooks” philosophy that has so well served WordPress, and could serve GraphQL equally well too.
This philosophy has been deeply embedded within plugin GraphQL API for WordPress: it uses a directive pipeline to resolve queries, and directives are treated as first-class citizens. Among others, they can authenticate users, remove the output for a field, and stop the execution of the upcoming stages on the pipeline.
Developers publishing plugins on the WordPress directory do not know in advance who will use their plugins or what configuration/environment the site will have, including what other plugins may be installed. As a consequence, the plugin must be prepared for conflicts and attempt to prevent them beforehand.
How do WordPress plugins avoid conflicts?
Let’s talk again about hooks. If my plugin needs to add a filter to modify the user’s name, the filter cannot be simply called "userName"
since this name is too generic and I risk another plugin using the same name. Instead, I must prefix it with some unique name, such as "GraphQLAPIForWP:userName"
.
Namespacing serves the same role — to make some code unique.
Namespaces are widely used within the PHP community, following the PHP Standard Recommendation PSR-4
to enable Composer autoloading. PHP packages must include the vendor’s name, as "vendor-name/package-name"
, and the corresponding namespace is present on the PHP code:
<?php namespace VendorNamePackageName;
Would namespacing make sense for a GraphQL service, too?
Namespacing could make sense within the context of GraphQL as well to avoid the following potential conflicts happening on the schema:
Namespacing has actually been requested for the GraphQL spec:
At the moment, all GraphQL types share one global namespace. This is also true for all of the fields in mutation/subscription type. It can be a concern for bigger projects, which may contain several loosely coupled parts in a GraphQL schema.
This issue, which was opened more than four years ago, still remains open. Lee Byron (one of the creators of GraphQL while working at Facebook) has expressed his opposition to this feature. In this comment, he explains how Facebook manages the thousands of types in its GraphQL schema without conflict:
We avoid naming collisions in two ways:
- Integration tests. We don’t allow any commit to merge into our repository that would result in a broken GraphQL schema. […]
- Common naming patterns. We have common patterns for naming things, which naturally avoid collision problems. […]
But I have to wonder, will what works for Facebook necessarily work for WordPress, too?
Please notice that in this section, WordPress is not being compared against JavaScript but against Facebook. However, since Facebook is one of the biggest stakeholders pushing GraphQL forward, deeply influencing the design decisions for graphql-js
, I think the comparison is valid.
Lee Byron claims that if Facebook does not need namespacing to manage its thousands of types, then we can all do without it.
There is an important factor at play here, however: Facebook controls all inputs to its GraphQL schema! In this case, it can afford to follow a procedure to name the entities, making sure that no conflict arises.
But this isn’t how WordPress works; the WordPress site relies heavily on third-party plugins, and it does not control how these plugins are produced.
For instance, if a site uses the WooCommerce and Easy Digital Downloads plugins, and they both have a type named Product
for the GraphQL schema, there will be a conflict. The only resource for the site owner is to reach out to one of the companies and ask them to modify their code. This is not prevention but correction, and it’s unreliable.
I’ve been speaking theory so far, but let’s consider this topic in practice: Will conflicts really happen?
WordPress plugins must pay serious mind to preventing naming conflicts because of the sheer number of available plugins (over 58,000 to date in the directory alone). The chances of some overlap happening are non-negligible.
But how popular is GraphQL within WordPress?
Unfortunately, its use is not yet very widespread. Unlike REST (which is built into WordPress core through the WP REST API), GraphQL must be installed from a third-party plugin. As a consequence, other than agencies creating themes and plugins for their clients, or when coding a personal site, developers can’t make the assumption that there will be a GraphQL service running on the WordPress site.
Currently, there are two GraphQL solutions for WordPress:
We are concerned about plugins extending the GraphQL schema. How many are there?
Searching on the WordPress plugin directory produces only a handful of results, none with more than 200+ activations.
Searching on GitHub produces the following WPGraphQL extensions with 10 stars or more:
(There are no extensions for the GraphQL API for WordPress yet.)
So then, if GraphQL is not so popular within WordPress yet, do we need namespacing?
I think we do need namespacing, even if it won’t solve any problem right now. This is because:
The last item has the following logic: if WooCommerce wanted to be sure no conflict will ever arise, then it can’t use generic names for their types, such as Product
, Download
, or Payment
. Instead, it would need to “namespace” them on the type name itself: WCProduct
, WCDownload
, and WCPayment
. Or, to be absolutely confident it will always work, it might call them WooCommerceProduct
, WooCommerceDownload
, and WooCommercePayment
.
Well, the schema then became more verbose, less elegant. That is something that should be avoided, if possible.
Given that we may not really need namespacing, the best strategy for the GraphQL server is to provide it as an opt-in feature that, when enabled, will automatically namespace all types in the schema.
This strategy is feasible in the GraphQL API for WordPress plugin because:
For instance, in this code, field Post.comments
declares being of type Comment
by referencing its corresponding PHP class CommentTypeResolver
:
class CustomPostFieldResolver extends AbstractQueryableFieldResolver { // ... public function resolveFieldTypeResolverClass( TypeResolverInterface $typeResolver, string $fieldName, array $fieldArgs = [] ): ?string { switch ($fieldName) { case 'comments': return CommentTypeResolver::class; } return null; } }
As a further enhancement, the plugin needs no input from the user to namespace types. Instead, the existing PSR-4
naming for all PHP packages is used, transforming "VendorNamePackageName"
to "VendorName_PackageName"
for their types in the schema (neither characters ""
or "/"
are currently allowed in the syntax for type names, so "_"
is used instead).
Mutations are operations that can alter data in the GraphQL server, such as when creating a post, updating the user’s name, adding a comment to a post, and so on.
In GraphQL, mutations are exposed under the RootMutation
type only, like this:
type RootMutation { createPost(id: ID!, title: String!, content: String): Post! updateUserName(userID: ID!, newName: String!): User! addCommentToPost(postID: ID!, comment: String!, userID: ID): Comment! }
With this schema, modifying the user’s name is achieved like this:
mutation { updateUserName(userID: 37, newName: "Peter") { name } }
Mutations are exposed in the mutation root object type only to enforce that they are executed serially, as explained in the GraphQL spec:
It is expected that the top-level fields in a mutation operation perform side effects on the underlying data system. Serial execution of the provided mutations ensures against race conditions during these side effects.
The term “serial execution” is opposed to “parallel execution,” which is otherwise the recommended behavior to resolve fields.
For instance, in the query below, it doesn’t matter which field (whether name
or email
) the GraphQL server resolves first, and these can be resolved in parallel:
query { user(id: 37) { name email } }
Mutations alter data, though, so the order in which fields are resolved does matter; thus, they must be executed serially, or otherwise, they could produce race conditions.
For instance, the two queries below will produce different results:
# Query 1: after execution, user name will be "John" mutation { updateUserName(userID: 37, newName: "Peter") { name } updateUserName(userID: 37, newName: "John") { name } } # Query 2: after execution, user name will be "Peter" mutation { updateUserName(userID: 37, newName: "John") { name } updateUserName(userID: 37, newName: "Peter") { name } }
The consequence of exposing mutations only through RootMutation
is that this type becomes heavily bloated, containing fields with nothing in common among themselves other than having to be executed serially (which is a technical matter, not an interface design decision).
From the mutations above, only createPost
truly lives under the RootMutation
type because it is creating a new element out of nowhere. Mutations updateUserName
and addCommentToPost
, though, can have equivalent operations applied on an existing entity from another type:
type User { updateName(newName: String!): User! } type Post { addComment(comment: String!, userID: ID): Comment! }
With this schema, modifying the user’s name could be achieved like this:
mutation { user(ID: 37) { updateName(newName: "Peter") { name } } }
This feature is called nested mutations — applying a mutation to the result of another operation, whether a query or mutation.
Please notice how using nested mutations makes the GraphQL schema more elegant:
RootMutation.updateUserName
must receive the ID
of the user, its equivalent operation User.updateName
must not since it is already executed on a user entityupdateUserName
to updateName
In addition, the GraphQL service becomes simpler and more understandable since we can navigate among entities in the graph to modify their data in the same way as to query their data.
Nested mutations can go down multiple levels. For instance, we can add a comment on a newly created post, all within a single query:
mutation { createPost(ID: 37, title: "Hello world!", content: "Just another post") { id addComment(comment: "Lovely post") { id } } }
From this, nested mutations can also improve performance by reducing round-trip latency, from executing multiple queries to mutate several elements to executing a single query.
Nested mutations have been requested for the GraphQL spec almost four years ago, but the issue remains open (for reasons we’ll explore below).
The GraphQL spec is meant to work for all implementations of GraphQL servers for any language. However, its driving force is JavaScript through graphql-js
, the reference implementation.
In other words, any feature that cannot be supported by graphql-js
will not become part of the specification.
Since JavaScript supports promises, parallel resolution of fields was feasible, and parallelism became one of the fundamental principles when first designing graphql-js
, as manifest in DataLoader (the data fetching layer), whose batching functions return JavaScript promises.
The advantages of parallel execution for performance are too many, and nested mutations cannot work with parallelism. It has been decided that it would not be worth trading parallel execution for nested mutations. Hence, nested mutations will not make it to the spec.
But what happens for other languages that don’t support promises?
PHP does not naturally support promises (unless using an external library, such as ReactPHP). Therefore, supporting nested mutations would not produce the negative consequence of removing parallel execution since this feature doesn’t exist in the first place.
This means that a PHP-based GraphQL server should be perfectly capable of supporting nested mutations, providing all of its benefits, and suffering none of its consequences.
It is an opportunity. Why shouldn’t it be taken?
For the GraphQL API for WordPress plugin, fields are always resolved serially, and the order in which they are resolved is deterministic (it is this trait that makes it possible to support the @export
directive).
This characteristic does not affect the query resolution performance because the server uses queues (instead of a graph) when resolving queries, delivering a complexity time O(n)
based on the number of types on the schema. Resolving a GraphQL query with linear time is as good as it can get.
As I’m writing this article, the implementation of nested mutations for this plugin is a work in progress. Since nested mutations are not supported by the GraphQL spec, this feature will be added as opt-in, with a disclaimer that it is non-standard functionality.
In this article we’ve explored how different factors may affect the implementation of certain elements of a GraphQL server for WordPress as compared to its equivalent server for JavaScript:
There are no good or bad solutions, just solutions that work better for different contexts. I have proposed and demonstrated solutions for WordPress, which, taking into account its philosophy, its opinionatedness, the capabilities from PHP, and others, I believe work better than the JavaScript-based alternative.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic GraphQL requests to quickly understand the root cause. In addition, you can track Apollo client state and inspect GraphQL queries' key-value pairs.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]