schema.graphqls

Each module declares its part of the GraphQL schema in etc/schema.graphqls, and Magento merges every file into one schema.

SyntaxfieldName(arg: Type = default): ReturnType @resolver(class: "Vendor\\Module\\Model\\Resolver\\Name")

A module declares its slice of the GraphQL schema in one file: app/code/Vendor/Module/etc/schema.graphqls. It is written in SDL, the GraphQL schema definition language. Magento reads that file from every enabled module and stitches the declarations into a single schema, so there is no central schema file to edit.

The directives

A directive is an annotation written with @. Magento_GraphQl declares the ones Magento understands:

DirectiveWhat it does
@resolver(class: "...")Names the PHP class that supplies the field's value
@doc(description: "...")Description returned by introspection
@cache(cacheIdentity: "..." cacheable: Boolean)Declared for a query field; controls HTTP caching
@typeResolver(class: "...")Picks the concrete type behind an interface or union
@deprecated(reason: "...")Keeps a field working while clients move off it

A field with no @resolver takes its value from the array key of the same name in whatever its parent resolver returned. That is why most fields on a type need no resolver of their own.

Adding to a type another module owns

Magento's schema files have no extend keyword. You declare the type again with only your own fields, and Magento merges the field lists rather than letting the last file win. Magento_CatalogUrlRewriteGraphQl adds url_suffix by declaring interface ProductInterface containing that one field.

After you edit it

Run bin/magento cache:clean config. In production mode a new resolver class also needs bin/magento setup:di:compile.

Example

Example · graphql
# app/code/Learning/Faq/etc/schema.graphqls

type Query {
    faqEntries(
        pageSize: Int = 20 @doc(description: "Maximum number of entries to return.")
        currentPage: Int = 1 @doc(description: "Page of results to return.")
    ): FaqEntries
    @resolver(class: "Learning\\Faq\\Model\\Resolver\\FaqEntries")
    @doc(description: "Return published FAQ entries for the current store.")
    @cache(cacheIdentity: "Learning\\Faq\\Model\\Resolver\\FaqEntries\\Identity")
}

type Mutation {
    voteFaqEntry(
        uid: ID! @doc(description: "The unique ID of the FAQ entry.")
        helpful: Boolean! @doc(description: "Whether the entry answered the question.")
    ): VoteFaqEntryOutput
    @resolver(class: "Learning\\Faq\\Model\\Resolver\\VoteFaqEntry")
    @doc(description: "Record a vote on an FAQ entry.")
}

type FaqEntries @doc(description: "Contains a page of FAQ entries.") {
    items: [FaqEntry]! @doc(description: "The entries on the requested page.")
    total_count: Int @doc(description: "Number of entries matching the query.")
}

type FaqEntry @doc(description: "A single question and answer.") {
    uid: ID! @doc(description: "The unique ID of the entry.")
    question: String! @doc(description: "The question text.")
    answer: String @doc(description: "The answer, in raw HTML.")
    legacy_id: Int @deprecated(reason: "Use `uid` instead.")
}

type VoteFaqEntryOutput @doc(description: "Result of a voteFaqEntry call.") {
    entry: FaqEntry! @doc(description: "The entry after the vote was recorded.")
}

When to use it

  • Expose a custom entity to a headless storefront as a new query and mutation.
  • Add one field to ProductInterface without touching Magento_CatalogGraphQl.
  • Deprecate an integer id field in favour of a uid without breaking existing clients.
  • Work out why a newly added field is missing from introspection.

More examples

Merging fields into types other modules own

This is how every core GraphQL module contributes. Reach for it whenever a field belongs on an existing type; declaring a whole parallel query instead forces clients to make two round trips. After saving, run bin/magento cache:clean config and query the field to confirm the merge happened.

Example · graphql
# app/code/Learning/Faq/etc/schema.graphqls (second half)

# Magento_CatalogGraphQl already declares ProductInterface. Declaring it
# again with one field adds that field; nothing of the catalog's is lost.
interface ProductInterface {
    faq_count: Int
    @doc(description: "Number of FAQ entries attached to this product.")
    @resolver(class: "Learning\\Faq\\Model\\Resolver\\Product\\FaqCount")
}

# The same pattern adds a field to a core output type.
type StoreConfig {
    faq_page_size: Int @doc(description: "Entries shown per FAQ page.")
}

The resolver you can leave out

Written out, the implicit behaviour is obvious: a field with no @resolver reads the key of its own name from the array its parent resolver returned. Write a class like this only when the field needs to do something extra, such as formatting or a permission check. Otherwise leave the directive off and let the parent's array carry the value.

Example · php
<?php
// app/code/Learning/Faq/Model/Resolver/FaqEntry/Question.php
declare(strict_types=1);

namespace Learning\Faq\Model\Resolver\FaqEntry;

use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;

// Longhand:  question: String! @resolver(class: "...\\FaqEntry\\Question")
// Shorthand: question: String!
// The two are the same thing. The default behaviour of a field with no
// resolver is exactly what this class does.
class Question implements ResolverInterface
{
    /**
     * @inheritDoc
     */
    public function resolve(
        Field $field,
        $context,
        ResolveInfo $info,
        ?array $value = null,
        ?array $args = null
    ): ?string {
        return $value['question'] ?? null;
    }
}

Marking a field uncacheable and an interface resolvable

Core uses @cache(cacheable: false) on customer, cart and customerOrders for the same reason. Use it on any query whose answer depends on who is asking. @typeResolver is required as soon as you declare an interface or a union: without it, Magento cannot decide which concrete type a resolved row is and the query fails at runtime rather than at compile time.

Example · graphql
# app/code/Learning/Faq/etc/schema.graphqls (excerpt)

type Query {
    # Per-customer data. Never store this in a shared HTTP cache.
    myFaqVotes: [FaqVote]
    @resolver(class: "Learning\\Faq\\Model\\Resolver\\MyFaqVotes")
    @cache(cacheable: false)
}

# An interface needs a type resolver: a class that decides which concrete
# type each resolved row is, so GraphQL knows which fields apply.
interface FaqAnswerInterface
@typeResolver(class: "Learning\\Faq\\Model\\Resolver\\FaqAnswerTypeResolver")
{
    answer: String
}

type FaqTextAnswer implements FaqAnswerInterface {
    answer: String
}

type FaqVideoAnswer implements FaqAnswerInterface {
    answer: String
    video_url: String
}

Discussion

  • Be the first to comment on this lesson.