GraphQL Server With Symfony 4

Abhishek Mishra
6 min readOct 29, 2020
Get ready with your machine to finish it out
Get ready to implement

To understand this article you need to have some experience of PHP 7.4, Symfony 4, using services and Doctrine ODM with MongoDB. I tried to make code very simple and self explanatory to avoid adding more explanation about code. I also tried to cover more aspects of a real world project e.g. category and its related list products with pagination.

Lets start to setup a GraphQL server using overblog/GraphQLBundle bundle with symfony 4. We will divide it in 4 major parts:

- Installation and configuration of GraphQLBundle.

- Create documents and service to fetch the data.

- Create GraphQL types to define data that can be queried.

- Create Resolver service to resolve all the defined properties.

Installation and configuration of GraphQLBundle

I suppose you have a running application in Symfony 4 with DoctrineMongoDBBundle configured. [Here](https://symfony.com/doc/current/bundles/DoctrineMongoDBBundle/index.html) is a reference to install and configure DoctrineMongoDBBundle in case you are missing it.

Now to install GraphQL Bundle go to your project directory and run following command:

composer require overblog/graphql-bundle

After the installation of dependencies you will get options to execute contrib recipes from symfony flex:

WARNING overblog/graphql-bundle (0.9): From github.com/symfony/recipes-contrib.The recipe for this package comes from the “contrib” repository, which is open to community contributions.Do you want to execute this recipe?[y] Yes[n] No[a] Yes for all packages, only for the current installation session[p] Yes permanently, never ask again for this project(defaults to n):

You can also enable it manually by:

  1. Enabling bundle
// in config/bundles.phpreturn [    // …    Overblog\GraphQLBundle\OverblogGraphQLBundle::class => ['all' => true],];

2. Configuration about schema and types

# in config/packages/graphql.ymloverblog_graphql:    definitions:        schema:            query: Query        mappings:            types:                -                    type: yml                    dir: "%kernel.project_dir%/config/graphql/types"                    suffix: null

3. And enable GraphQL endpoint

# in config/routes/graphql.ymloverblog_graphql_endpoint:    resource: "@OverblogGraphQLBundle/Resources/config/routing/graphql.yaml"    prefix: /

Create Document

We will consider the scenario of an e-commerce application in which we will have categories and associated products.

Here is the 2 required documents:

# /src/Document/Categories.php<?phpdeclare(strict_types=1);namespace App\Document;use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;/*** Class Categories.** @MongoDB\Document(db="e-commerce", collection="categories")* @MongoDB\Indexes({*     @MongoDB\Index(keys={"name": "asc"}),*    @MongoDB\Index(keys={"id": "asc"})* })*/class Categories{    /**    * @MongoDB\Id(name="_id")    */    private string $objectId;    /**    * @MongoDB\Field(type="int")    */    private int $id;    /**    * @MongoDB\Field    */    private string $name;    /**    * @MongoDB\Field    */    private string $description;    public function getId() : int    {        return $this->id;    }    public function getName() : string    {        return $this->name;    }    public function getDescription() : string    {        return $this->description;    }}
# /src/Document/Products.php<?phpdeclare(strict_types=1);namespace App\Document;use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;use App\Document\Categories;/*** Class Products.** @MongoDB\Document(db="e-commerce", collection="products")* @MongoDB\Indexes({*     @MongoDB\Index(keys={"name": "asc"}),*     @MongoDB\Index(keys={"id": "asc"})* })*/class Products{    /**    * @MongoDB\Id(name="_id")    */    private string $objectId;    /**    * @MongoDB\Field(type="int")    */    private int $id;    /**    * @MongoDB\Field(type="int")    */    private int $category;    /**    * @MongoDB\Field    */    private string $name;    /**    * @MongoDB\Field    */    private string $description;    /**    * @MongoDB\Field(type="float")    */    private float $price;    public function getId() : int    {        return $this->id;    }    public function getName() : string    {        return $this->name;    }    public function getDescription() : string    {        return $this->description;    }    public function getCategory() : int    {        return $this->category;    }    public function getPrice() : float    {        return $this->price;    }}

These are very simple set of document classes and I can expect that you can easily understand it.

Create Service To Fetch Product Data

This service will be used to fetch products of the provided category. You will also notice the implementation of `skip`, `limit` and a function to count all the products of the category. We will use them in the implementation of pagination.

# /src/Product/Provider.php<?phpdeclare(strict_types=1);namespace App\Product;use Doctrine\ODM\MongoDB\DocumentManager;use App\Document\Products;class Provider{    private DocumentManager $documentManager;    public function __construct(        DocumentManager $documentManager    ) {        $this->documentManager = $documentManager;   }    public function getProductsByCategory(int $id, int $skip, int $limit): array    {        $productData = $this->documentManager            ->createQueryBuilder(Products::class)            ->field('category')->equals($id);        if ($limit) {            $productData = $productData->limit($limit);        }        if ($skip) {            $productData = $productData->skip($skip);        }        $productData = $productData->getQuery()->execute();        $product = [];        foreach($productData as $data) {            $product[] = [                'id' => $data->getId(),                'category' => $data->getCategory(),                'name' => $data->getName(),                'description' => $data->getDescription(),                'price' => $data->getPrice()            ];        }        return $product;    }    public function countProductsByCategory(int $id): int    {        return $this->documentManager            ->createQueryBuilder(Products::class)            ->field('category')->equals($id)            ->getQuery()->execute()->count();    }}

Create GraphQL Types

Types can be defined in 3 different ways but we will use `configuration way`. See here for more information about all the other ways.

Creating this file extension .types.yaml or .types.xml in /config/graphql/types/.

Lets define the default Query type using yaml files.

# config/graphql/types/Query.types.yamlQuery:    type: object    config:        fields:            categories:                type: Categories                args:                    id:                        type: Int                resolve: '@=resolver("App\\GraphQL\\Resolver\\CategoriesResolver::resolve", [args["id"]])'

It will allow you to make a query for categories which will be resolved using category id that will be an integer.

If you see `type: Categories`, it’s not a built-in type, So we need to define this custom Categories type.

# config/graphql/types/Categories.types.yamlCategories:    type: object    config:        resolveField: '@=resolver("SendinBlue\\GraphQL\\Resolver\\CategoriesResolver", [info, value, args])'        fields:            name:                type: String            description:                type: String            products:                type: ProductConnection                argsBuilder: Relay::ForwardConnection

`resolveField` option is used to invoke class `CategoriesResolver` to resolve all the listed fields ie. name, description, products. GraphQL will call magic method __invoke with following arguments:

- `GraphQL\Type\Definition\ResolveInfo $info` contains the name of the field that will be resolved.

- `$value` is the previously resolved Categories object.

- `Overblog\GraphQLBundle\Definition\Argument $args` ConnectionArguments contains the arguments passed in the query, we will use it in pagination.

Now if you see products property. It is a custom type `ProductConnection`. Here we go for ProductConnection type:

# config/graphql/types/ProductConnection.types.yamlProductConnection:    type: relay-connection    config:        nodeType: Products

You might notice the `Connection` suffix in this relay-connection type, If you remove the Connection suffix, it breaks.

Now finally Products type:

# config/graphql/types/Products.types.yamlProducts:    type: object    config:        fields:            id:                type: Int            name:                type: String            description:                type: String            price:                type: Float

It contains all the built-in scalar types. Now no remaining custom types and we have done with types definition.

Create Resolver Service

Now we will define a resolver that will be used to resolve defined properties.

<?phpnamespace App\GraphQL\Resolver;use App\Product\Provider;use App\Document\Categories;use GraphQL\Type\Definition\ResolveInfo;use Doctrine\ODM\MongoDB\DocumentManager;use Overblog\GraphQLBundle\Definition\Argument;use Overblog\GraphQLBundle\Relay\Connection\Paginator;use Overblog\GraphQLBundle\Relay\Connection\Output\Connection;use Overblog\GraphQLBundle\Definition\Resolver\ResolverInterface;Class CategoriesResolver implements ResolverInterface{    private DocumentManager $documentManager;    private Provider $productProvider;    public function __construct(DocumentManager $documentManager, Provider $provider)    {        $this->documentManager = $documentManager;        $this->productProvider = $provider;    }    public function __invoke(ResolveInfo $info, $value, Argument $args)    {        $method = $info->fieldName;        return $this->$method($value, $args);    }    public function resolve(int $id) :Categories    {        return $this->documentManager
->getRepository(Categories::class)
->findOneBy(['id' => $id]);
} public function name(Categories $category) :string { return $category->getName(); } public function description(Categories $category) :string { return $category->getDescription(); } public function products(Categories $category, Argument $args) :Connection { $paginator = new Paginator(function ($offset, $limit) use ($category) { return $this->productProvider
->getProductsByCategory($category->getId(),$offset , $limit);
}); return $paginator->auto($args, function() use ($category) { return $this->productProvider
->countProductsByCategory($category->getId());
}); }}

Here is some details about CategoriesResolver:

- `__invoke()` will be called by GraphQL and will then call a dedicated method to resolve the field.

- `name` and `description` will resolve respective fields.

- `products` resolve the products fields by fetching products based on `$args` which is used to pass pagination related information.

Now you will have a running GraphQL server that will fetch data based on

- Provided Category id from categories collection.

- Fetch associated products from products collection.

- Provide pagination based on provided args.

Lets make query to fetch category details of id#1 with its first:10 products records after:2 records.

/?query={categories(id:1){name,description,products(first:10,after:"YXJyYXljb25uZWN0aW9uOjI="){edges{cursor,node{name,description,price}}}}}

Here `YXJyYXljb25uZWN0aW9uOjI=` is `base64_encode(‘arrayconnection:2’)`. Follow the type-system reference for more details on it.

References

- Introduction to GraphQL

- GraphQL Cursor Connections

- Relay Pagination

- Type System

--

--

Abhishek Mishra

Software Architect | Developer | Application design | Database design | MongoDB | Golang | PHP | React | Kafka | RabbitMQ | Security Enthusiast