GraphQL Is Here
Magento 2.3 saw the introduction of the various GraphQL modules into the codebase. The long-term goal (according to Magento’s docs here) is to support most default functionality through GraphQL. We still have a ways to go, but there is significant support for many basic operations through this API interface.
Extension Attributes / Custom Attributes – REST API
If you’ve done any work with the Magento REST API, you’re probably aware that in order to send or receive custom attributes — e.g., attributes that are not part of the built in Data interfaces — you have to utilize Magento’s extension attribute functionality. In essence, here’s how it works:
- You define a custom module
- Your module adds extension attributes to the database
- You define them (with an optional ACL permission) in extension_attributes.xml
- You add a plugin / preference / observer to handle loading/saving your attributes
- Magento will send back extension attributes during certain REST calls as a sub-object in the json, e.g., { extension_attributes: […] }
- You are responsible for ensuring extension attributes are added to incoming and outgoing requests
At the end of the day, it’s quite verbose.
Extension Attributes / Custom Attributes – GraphQL API
However, in GraphQL, Magento has made customizing API responses significantly easier. This is not to say that you shouldn’t utilize the extension attributes functionality (there are other benefits to defining extension attributes beyond sending extra information in API calls). But, it is no longer a requirement to change how the GraphQL endpoints work.
The process for adding a custom attribute to a GraphQL query is described below:
- You define a custom module
- You create a etc/schema.graphqls file in your module
- You add new types, queries, or extend existing types and queries
- You are responsible for ensuring custom data is actually available to the GraphQL layer (more on this below)
Adding A New Attribute To GraphQL
Putting aside number 4 for now, let’s look at an example use case. Let’s say I want to add the parent_item_id as an attribute available through the GraphQL Quote Endpoint (https://devdocs.magento.com/guides/v2.3/graphql/reference/quote.html).
Now, you may already be aware, parent_item_id is an existing field (it’s not a new database attribute). But currently, it’s not supported through the native GraphQL Quote Endpoint – so it’s not accessible via GraphQL responses. We’ll walk through a simple example to add it:
First, I’ll create my module:
// File: app/code/Cadence/Example/registration.php
<?php
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Cadence_Example',
__DIR__
);
Next, I’ll add my etc/schema.graphqls file:
// File: app/code/Cadence/Example/etc/schema.graphqls
interface CartItemInterface @typeResolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemTypeResolver") {
parent_item_id: Int
}
Now, refresh your database / cache:
php bin/magento setup:upgrade
…That’s it! On the client side, I can send a GraphQL query that looks like this:
query {
cart {
items {
parent_item_id
}
}
}
(Obviously in a real-world scenario, you would request additional information).
Now, the only point that confusing at first glance is the interface definition in the schema.graphqls file. …Where did I get that? Well, if you look in the native Quote graphql module:
// File>: vendor/magento/module-quote-graph-ql/etc/schema.graphqls
...
type SimpleCartItem implements CartItemInterface @doc(description: "Simple Cart Item") {
customizable_options: [SelectedCustomizableOption] @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CustomizableOptions")
}
type VirtualCartItem implements CartItemInterface @doc(description: "Virtual Cart Item") {
customizable_options: [SelectedCustomizableOption] @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CustomizableOptions")
}
interface CartItemInterface @typeResolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemTypeResolver") {
id: String!
quantity: Float!
product: ProductInterface!
}
...
You can see above, that by default Magento 2 does several things:
- Defines an interface CartItemInterface which is used to represent cart items
- Defines concrete implementations of that interface for different product types
- Defines the fields available through GraphQL for each of them
So, by default, the only available fields for an item are id, quantity, product — the last of which is a complex ProductInterface type.
In our custom module, all we did was copy the interface definition and start adding new properties to it.
That’s an important point to remember – the GraphQL definitions function more like layout xml in Magento 2, and less like typical extension_attributes. The beauty of this is that we can freely add new types, modify queries, etc. Basically, Magento gives us carte blanche to modify the GraphQL endpoints.
Adding A New Custom Attribute To GraphQL
So, this covers point 4 from my overview. What if we’re adding a custom attribute, e.g., an attribute we’ve added to the database?
Well again, Magento gives us a lot of freedom in determining how we do this. To add support for the attribute via GraphQL is described in the previous section, but now we have to deal with making that attribute available to GraphQL. I’m going to demonstrate one way to do this, which is via a custom GraphQL resolver.
Let’s take this example use case:
- We added a new field to the quote table: order_comments
- We want to make that field accessible in the GraphQL Quote Query, so that we can use it on the frontend
We’re going to have a few files involved in this:
First, our etc/schema.graphqls
// File app/code/Cadence/Example/etc/schema.graphqls
type Cart {
order_comments: String @resolver (class: "\\Cadence\\Example\\Model\\Resolver\\Cart\\OrderComments")
}
So, you can see that this definition says a few things:
- We’re extending the built-in Cart type
- We’re adding a new field order_comments
- It is a nullable string
- It has a data resolver class — this tells Magento to use this class to get the attribute value
So, now we need to add the resolver:
// File: app/code/Cadence/Example/Model/Resolver/Cart/OrderComments.php
declare(strict_types=1);
namespace Cadence\Example\Model\Resolver\Cart;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\QuoteGraphQl\Model\Cart\GetCartForUser;
/**
* @inheritdoc
*/
class OrderComments implements ResolverInterface
{
/**
* @inheritdoc
*/
public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null)
{
if (!isset($args['cart_id']) || empty($args['cart_id'])) {
throw new GraphQlInputException(__('Required parameter "cart_id" is missing'));
}
$maskedCartId = $args['cart_id'];
$currentUserId = $context->getUserId();
$cart = $this->getCartForUser->execute($maskedCartId, $currentUserId);
return $cart->getData('order_comments');
}
}
So, in our resolver you can see:
- We are loading the quote based on the given cart id
- We are returning the value of the order_comments field (stored on the quote table)
The code from above is based on the generic cart resolver in: vendor/magento/module-quote-graph-ql/Model/Resolver/Cart.php
That’s it! GraphQL allows us to extend an existing type and supply the values for its properties through our own set of classes (pretty nifty!).
The above example assumes you have already added this field to the database — how to do that is beyond the scope of this tutorial.
Need help with Magento 2?
We’ve had a lot of experience building websites in Magento 2. If you need help, head over to the Cadence Labs contact page, or email us at [email protected].