Header image

Symfony Finland
Random things on PHP, Symfony and web development

Adding a GraphQL API to your Symfony Flex application

I've been using GraphQL for some API thingamajigs, and it's been working fine. Now with Symfony 4 out, I figured a write-up on how to use GraphQL with Symfony Flex could be useful for someone.

We'll expand on a previous demo app that I built. That app already uses Doctrine ORM as storage, so let's bridge that to a GraphQL API.

If you are not familiar with GraphQL, then I suggest you do a quick read up to get an idea of what it is and if it could be useful for you, but in short it's just another API model similar to what REST is. Also, there are good GraphQL libs for PHP available.

The demo application (Sharing state in a Symfony hybrid) we'll work with lists some apartment data in a simple table. It does this on the client side with a number of JavaScript libraries using a simple JSON API. Each apartment has the following fields:

  • ID
  • Street Address
  • City
  • Zipcode
  • Country
  • Build Year
  • Size

We use the Overblog GraphQL bundle to integrate the Webonyx GraphQL library. The bundle has a Symfony Flex recipe, so we'll be able to use of the new features in it.

Installing the bundles & setting a route prefix

When using Symfony Flex to manage applications, the installed packages can auto configure themselves. Let's get started with adding the GraphQL bundle as a dependency to our Flex app:

composer require overblog/graphql-bundle

This command will download the code from the Packagist repository, as well as perform the initial configuration as defined in the recipe. In addition to the main package we'll want to install a separate debug bundle, which is useful for development. This is done with Composer as well, just like the main bundle:

composer require --dev overblog/graphiql-bundle

We can now run our Symfony Flex app with the built in web server to access our API:

./bin/console server:run

If you now go with your browser to the front page, you'll see the following message:

{"error":{"code":500,"message":"Internal Server Error",
"exception":[{"message":"Unknown type with alias \"Query\"
(verified service tag)","class":"Overblog\\
GraphQLBundle\\Resolver\\UnresolvableException","trace"...

If you plan only to expose a GraphQL API from your application, then this is fine, but since we want to keep our server side Twig rendered views, I'll add a prefix to the GraphQL route in the generated configuration in config/routes/graphql.yaml:

overblog_graphql_endpoint:
    resource: "@OverblogGraphQLBundle/Resources/config/routing/graphql.yaml"
    prefix: graphql

With this in place, our API endpoint is now at the route: http://localhost:8000/graphql

Configuring your schema

As you probably know at this point, GraphQL APIs are typed and provide this data using a GraphQL schema. The GraphQL library and integration bundle provide the infrastructure for typing, so we don't have to write everything from scratch.

To get started, let's define a type for our apartment entity using the defined YAML format of the bundle to in config/graphql/Apartment.types.yaml:

Apartment:
  type: object
  config:
    description: "An apartment"
    fields:
      id:
        type: "Int!"
        description: "The unique ID of the apartment."
      street_address:
        type: "String"
        description: "Address of the apartment"
      country:
        type: "String"
        description: "Country of the Apartment"
      city:
        type: "String"
        description: "City of the Apartment"
      zipcode:
        type: "String"
        description: "Zipcode of the Apartment"
      build_year:
        type: "Int"
        description: "Build year of the Apartment"
      size:
        type: "Int"
        description: "Size of the Apartment"

You can see that this definition is pretty self explanatory, and your API documentation will be automatically generated from this information, so put some thought into writing the descriptions and defining the data structures in real-world complex projects.

Next we will need to define the root level object that is the entrypoint to our schema. In essence it contains what you want to expose, but does not contain anything itself.

Create a new file in config/graphql/types/Query.types.yaml:

Query:
  type: object
  config:
    description: "Apartments ORM repository"
    fields:
      apartment:
        type: "apartment"

This file has the same format as our apartment type definition, but this will be where the API will start from and expose what is available. This will grow as you add more functionality to your API, so in some cases you might want multiple endpoints.

In the first step we installed the GraphiQL debugging bundle. This package adds a client interface to the GraphQL API that can be used to build and execute queries. when your app is running in development mode.

You can access the interface from /graphiql (note the extra "i"). With our schema now defined and exposed via root we can already get information using GraphiQL:

GraphiQL debugging interface

There are plenty more options to construct schemas, but you should study the documentation of the GraphQL bundle for more information.

Creating a resolver

If you try to execute valid GraphQL queries like:

{apartment{
  id
  street_address
  build_year
}}

against your endpoint at this point, you will get errors similar to:

{
  "error": {
    "code": 500,
    "message": "Internal Server Error",
    "exception": [
      {
        "message": "Unknown resolver with alias \"Apartment\" (verified service tag)",
        "class": "Overblog\\GraphQLBundle\\Resolver\\UnresolvableException",

This is simply because our query has no back end logic defined that would figure out what should be the result for a query. To do this you need to configure and write resolver in PHP code to get the results you want. Results can be fetched from any data store, but we'll focus on Doctrine ORM as our storage back end.

In our case we also want to pass a parameter (id) to our resolver so it can return a single result based on the id of the Doctrine entity. To define the arguments and the resolver, open up your Root Query definition and add the args and resolve options:

apartment:
  type: "Apartment"
  args:
    id:
      description: "Resolves using the apartment id."
      type: "Int"
  resolve: "@=resolver('Apartment', [args])"

Next up is adding the actual logic to resolve a single Doctrine entity. This is done with a PHP class based in src/GraphQL/Resolver/ApartmentResolver.php:

<?php
namespace App\GraphQL\Resolver;

use Doctrine\ORM\EntityManager;
use Overblog\GraphQLBundle\Definition\Argument;
use Overblog\GraphQLBundle\Definition\Resolver\AliasedInterface;
use Overblog\GraphQLBundle\Definition\Resolver\ResolverInterface;

class ApartmentResolver implements ResolverInterface, AliasedInterface {

    private $em;

    public function __construct(EntityManager $em)
    {
        $this->em = $em;
    }

    public function resolve(Argument $args)
    {
        $apartment = $this->em->getRepository('App:Apartment')->find($args['id']);
        return $apartment;
    }

    public static function getAliases()
    {
        return [
            'resolve' => 'Apartment'
        ];
    }
}

The class implements ResolverInterface and AliasedInterface. I The resolve method itself uses our Doctrine repository to fetch a single entity and returns that, but you could resolve from whatever backend in here by returning an array with keys matching your schema.

To make the Doctrine entity manager be available for injection in the constructor, add this to your app/config/services.yaml:

Doctrine\ORM\EntityManager: "@doctrine.orm.default_entity_manager"

Now your GraphQL API is fully functional. You can get the correct results for this query with the ID set to 42 and fetching just the fields id, street_address and zipcode:

{apartment(id:42) {
  id
  street_address
  zipcode
}}

Adding another schema and resolve

In the previous section we fetched a single object with an id. This may be useful, but more often than not you want multiple objects from a backend API. When working with GraphQL you should design your data structures so they can be reused. In this case the obvious extension is to get all apartments from the API.

To add this endpoint, the steps are identical to what we did for the apartment object:

  1. Configure type
  2. Add type to root query
  3. Create resolver
  4. Profit

Add the ApartmentList type to config/graphql/types/ApartmentList.types.yaml:

ApartmentList:
  type: object
  config:
    description: "A query to return list of Apartments"
    fields:
      apartments:
        type: "[Apartment]"
        description: "Apartment details"

Here the notable part is the apartments type definition. We define it to be an array of our custom Apartment objects. When you reuse your types, you can add properties to a single definition and be sure it's available wherever that object has been used.

To the root Query we will add ApartmentList with an argument for limit:

apartment_list:
  type: "ApartmentList"
  args:
    limit:
      description: "limit"
      type: "Int"
  resolve: "@=resolver('ApartmentList', [args])"

Create the resolver class in src/GraphQL/Resolver/ApartmentListResolver.php:

<?php

namespace App\GraphQL\Resolver;

use Doctrine\ORM\EntityManager;
use Overblog\GraphQLBundle\Definition\Argument;
use Overblog\GraphQLBundle\Definition\Resolver\AliasedInterface;
use Overblog\GraphQLBundle\Definition\Resolver\ResolverInterface;

class ApartmentListResolver implements ResolverInterface, AliasedInterface {
    private $em;
    public function __construct(EntityManager $em)
    {
        $this->em = $em;
    }

    public function resolve(Argument $args)
    {
        $apartments = $this->em->getRepository('App:Apartment')->findBy(
            [],
            ['id' => 'desc'],
            $args['limit'],
            0
        );
        return ['apartments' => $apartments];
    }

    public static function getAliases()
    {
        return [
            'resolve' => 'ApartmentList'
        ];
    }
}

Now you can run this query to retrieve 5 apartments and their build year and country:

{apartment_list(limit:5) {
  apartments {
    build_year
    country
  }
}}

Conclusion

The GraphQL integration from Overblog for the Symfony Flex seems to work very well. There are some rough edges currently, like the need for manually tagging services and the differerences in YAML prefixes (.yml vs. .yaml), which are non-issues as the documentation was simply referring to old patterns, which are still useful for projects not using Flex.

There is tons of things in the GraphQL specification that would need to be covered to make this a complete reference, so you'll have plenty to learn still. But in general all of the above is generic. For example, the Youshido GraphQL library works in pretty much the same way, just that the implementation is a bit different. Symfony Flex support for the Overblog bundle is better today, which is why I chose it for this article.

All in all, creating consistent APIs using these tools is straightforward. One you work a while with the terms and acquire the routine, adding new endpoints and types becomes fluent in my experience. The complete code for the app is available on GitHub if you want to see it for yourself: github.com/janit/symfony-hybrid-flex-port

UPDATE 2.12.2017: This article and the code in the repository was updated after first revision to further simplify configuration and resolvers. This was done based on the contribution from Jérémiah Valérie, who has been working on the Overblog GraphQL Bundle. Thank you very much for this, Jeremiah!

Learn more about GraphQL:


Written by Jani Tarvainen on Wednesday November 29, 2017
Permalink -

Leave a comment

comments powered by Disqus

« Four things I like about Symfony 4 - Benchmarks: Symfony 3 Standard Edition vs. Symfony 4 Flex »