Setting up GraphQL with PHP

How to set up GraphQL using PHP with Slim Framework in less than 20 minutes

Rcls
The Startup

--

There are a lot of articles and tutorials about GraphQL. But they are mostly for JavaScript or other languages. There aren’t a lot of tutorials covering GraphQL and PHP. So here’s one.

This tutorial contains two parts.

  • Part 1 covers the installation and setting up of GraphQL using PHP.
  • Part 2 covers using a Promise based tool to defer field resolving to a later stage to use as a solution against the n+1 problem.

Considering you’ve made it so far as to implement GraphQL, I am assuming you have basic knowledge of what GraphQL is and how to use it.

If you are already too advanced to read through this and just want to look at the code, you can visit the repository I’ve set up for this article.

In this article we will be installing Slim Framework 4 Skeleton Application to work as our API. We will also be installing graphql-php as the GraphQL implementation using Composer. For demo purposes we will set up a very simple MySQL database containing two tables: book and author.

Prequisites:

  • Composer
  • PHP
  • Access to a database

Disclaimer

Slim framework is just an option I used for this tutorial. You can opt to not use any framework if you like, or use a different one. This tutorial provides you with the necessary pieces to do so.

Setting up the database

Let’s kick this off by creating the database. Let’s name it demo. I’ll leave the creating to you. For this tutorial I will be using MySQL.

Inside this database we will be installing the two tables mentioned before. You can pick up the SQL here. This creates two tables: book and author. We are also populating these tables with dummy data containing books and authors for those books.

Installing the Project

So now is the time to install Slim. Start by executing the following composer command in your www-directory:

composer create-project slim/slim-skeleton [your-app-name]

This will create a neat little project for you where the public/index.php will be your entry file.

Now cd to your app directory and let’s install some packages. First off, let’s install graphql-php for obvious reasons:

composer require webonyx/graphql-php

After this is done you can use your favorite database connector for PHP but for this tutorial we will be installing Doctrine DBAL.

composer require doctrine/dbal

That’s it! Those two extra packages installed we are now ready to launch. Run the following command to quickly host your PHP app:

php -S localhost:8080 -t public

Setting up DBAL

To have a working database connection from our app we have to set up DBAL. It’s a good thing Slim comes with a dependency container where we can put it and inject it when needed.

Settings

First, let’s go to app/settings.php and insert our database connection info in the array, inside the settings array:

'db' => [
'name' => 'demo', # Database table
'host' => 'localhost',
'username' => 'root',
'password' => '',
'driver' => 'pdo_mysql'
]

As for the driver you can use any PDO / DBAL supported database driver.

Dependency container

Now open up app/dependencies.php and set up a Connection. inside the $containerBuilder->addDefinitions() array parameter.

\Doctrine\DBAL\Connection::class => function (ContainerInterface $c) {
$settings = $c->get('settings');

$connectionParams = [
'dbname' => $settings['db']['name'],
'user' => $settings['db']['username'],
'password' => $settings['db']['password'],
'host' => $settings['db']['host'],
'driver' => $settings['db']['driver'],
];

$connection = \Doctrine\DBAL\DriverManager::getConnection($connectionParams);

# Set up SQL logging
$connection->getConfiguration()->setSQLLogger(new \Doctrine\DBAL\Logging\DebugStack());

return $connection;
}

We receive the settings from the ContainerInterface. After that we set up DBAL, but notice that we are also setting up SQLLogger which allows us to track what queries get executed. That’s pretty neat when you wanna check how many queries GraphQL executes when resolving fields.

Now we can inject the Connection to any class constructor that we set up in our Routes, app/routes.php

Setting up GraphQL

Now, Slim already sets us up with a nice project structure but we won’t be touching existing files under src/. You can explore the skeleton to figure out what it does but we’ll just be adding our own route with a Controller and GraphQL files.

Controller

First, create a file src/Application/Controllers/GraphQLController.php. This will be the class with a method our router will call when a request comes to /graphql

Let’s set it up with the following base code:

<?php

namespace App\Application\Controllers;

use Doctrine\DBAL\Connection;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Log\LoggerInterface;

class GraphQLController
{
protected $db;
protected $logger;

public function __construct(Connection $connection, LoggerInterface $logger)
{
$this->db = $connection;
$this->logger = $logger;
}

public function index(Request $request, Response $response) {}
}

Let’s break this down a little. This is a Controller that has a method called index and a constructor. The constructor can have dependencies injected to it which are set up in the dependencies.php file and are stored inside the Container.

In here we are injecting the Connection instance but a LoggerInterface as well. You might’ve notice that inside the dependencies.php file. That’s Monolog. I won’t be going into detail about it but it’s a very nifty tool for logging. We will be using it to log our queries.

Now let’s make the app router call the index method whenever a HTTP call is made to our GraphQL.

Router

Open up the app/routes.php file and add the following inside the returned function:

$app->any('/graphql', \App\Application\Controllers\GraphQLController::class . ':index');

Now what this means is whenever the application receives any type of HTTP request (POST, GET, PUT, PATCH or DELETE) for the /graphql route it calls the GraphQLController and it’s index method.

GraphQL does not care what type of HTTP request it receives. It also always responds with HTTP code 200 OK unless you are crafty enough to actually send out proper responses from your controller via proper error handling.

Schema

Let’s set up the GraphQL schema. I won’t go into detail about how a schema is constructed. For this tutorial let’s create a src/GraphQL/schema.graphqls file. This file extension allows us to use graphql schema syntax while creating our schema.

This is a personal preference, but I rather use graphqls instead of the graphql-php package type system as I think it’s more simple and reusable across multiple programming languages. You can also build your schema by splitting it into multiple files, combining them and then caching the entire thing to speed up performance when your schema grows.

Let’s set it up with the following:

schema {
query: Query
}

type Query {
getBooks: [Book]
getAuthors: [Author]
}

type Book {
id: ID
title: String!
}

type Author {
id: ID!
name: String
}

Now, I’ll break this down a little. We have a Query called getBooks that returns an array of books, containing fields id and title of a book.

We also have a Query called getAuthors that returns an array of authors, containing fields id and name.

I know that some of you started scratching your heads right there. Why do we have to get books and authors separately? Well, we’ll fix that in the Part 2 of this tutorial.

Now we need field resolvers for this.

Resolvers

So, resolvers are just what they are called: they resolve the requested thing. It’s just a function that returns something. If the user wants to fetch books, you must resolve this request by fetching the books from the database and return them.

We’re gonna set up two very simple resolvers by creating a file src/GraphQL/resolvers.php

Inside this we will just create an array that gets returned.

<?php

return [
'Query' => [
'getBooks' => function($root, $args, $context) {
return $context['db']->fetchAll("SELECT * FROM book");
},
'getAuthors' => function($root, $args, $context) {
return $context['db']->fetchAll("SELECT * FROM author");
}
]
];

So, we created a Query key at the first level of this array. Inside of this we have the two query types, getBooks and getAuthors. Both of these contain functions that return the results of a database query SELECT * FROM <table>. The $context[’db’] is actually our Connection (DBAL Connection instance) which we must set up in our GraphQLController.

Small tip

You do not have to write your resolver logic inside this file! I did this as a fast example. You can actually set up this array so that it actually calls the resolver function which is located elsewhere.

As an example you could have a file src/GraphQL/Resolvers/BookResolver.php and that file has a function called getBooks($root, $args, $context) and you call that function in your resolvers.php like so:

'getBooks' => BookResolver::getBooks($root, $args, $context)

The GraphQL method

So now we have to actually bind everything together. The resolvers, schema, context and send out a response! Let’s open our GraphQLController again and look at the empty index() method.

public function index(Request $request, Response $response)

This method has two arguments. $request and $response. These are arguments that Slim provides us when a request is made to this controller method. (There is actually a third argument, called $arguments which provides the method with GET request arguments, but we’ll ignore that since GraphQL request body cannot be sent via GET query arguments.)

We will use the $request to get the request body and the $response to send out a response to our client in JSON format.

Field resolvers

We need to create a function that actually sets default field resolvers so that we can resolve fields instead of just queries and mutations at a higher level.

This implementation is copied from the Siler project. Add the following function inside your GraphQLController:

private function setResolvers($resolvers)
{
\GraphQL\Executor\Executor::setDefaultFieldResolver(function ($source, $args, $context, \GraphQL\Type\Definition\ResolveInfo $info) use ($resolvers) {
$fieldName = $info->fieldName;

if (is_null($fieldName)) {
throw new \Exception('Could not get $fieldName from ResolveInfo');
}

if (is_null($info->parentType)) {
throw new \Exception('Could not get $parentType from ResolveInfo');
}

$parentTypeName = $info->parentType->name;

if (isset($resolvers[$parentTypeName])) {
$resolver = $resolvers[$parentTypeName];

if (is_array($resolver)) {
if (array_key_exists($fieldName, $resolver)) {
$value = $resolver[$fieldName];

return is_callable($value) ? $value($source, $args, $context, $info) : $value;
}
}

if (is_object($resolver)) {
if (isset($resolver->{$fieldName})) {
$value = $resolver->{$fieldName};

return is_callable($value) ? $value($source, $args, $context, $info) : $value;
}
}
}

return \GraphQL\Executor\Executor::defaultFieldResolver($source, $args, $context, $info);
});
}

We’ll return to this function in Part 2 of this tutorial where it will provide useful.

Getting back to index()

Now, let’s finish this method! First, let’s actually set resolvers by adding this line to our index() method:

$this->setResolvers(include dirname(__DIR__, 3) . '/src/GraphQL/resolvers.php');

So we just pass the contents of resolvers.php (array) to GraphQL that sets the default field resolvers based on it.

After this is done, we build the schema:

$schema = \GraphQL\Utils\BuildSchema::build(file_get_contents(dirname(__DIR__, 3) . '/src/GraphQL/schema.graphqls'));

Now this is single-file-implementation. You can actually split your schema to multiple files, parse them together and pass the results to this function (file contents glued together). Just remember, that you need to have line breaks between your files, otherwise your schema cannot be read properly.

Now it’s time to catch the incoming request. We have to get the request body, and set up query and variables for GraphQL, as well as set up the possible context we want to pass to our resolvers:

$input = $request->getParsedBody();
$query = $input['query'];

$variables = isset($input['variables']) ? $input['variables'] : null;

$context = [
'db' => $this->db,
'logger' => $this->logger
];

So, we get the request body using $request->getParsedBody() and inside we have GraphQL’s query and possible variables. We also pass our Connection and LoggerInterface instances inside the $context. You are free to actually extend the context to your needs.

Now, we have to execute the query.

$result = \GraphQL\GraphQL::executeQuery($schema, $query, null, $context, $variables);

This function receives the schema, query, context and variables as parameters and returns us the results from the resolver function. Most likely an array. We leave the third argument as null which is actually named $rootValue. Originally this argument should be our resolvers array, but since we already mapped those for GraphQL to use, we don’t need to use this argument.

Alternatively use Standard Server

You can opt to use the GraphQL Standard Server instead of executeQuery() which gives you more features out of the box, including query batching.

The standard server accepts PSR-7 request and response items, which Slim complies with! So this is rather simple. Replace the executeQuery() line with:

# Create server configuration
$config = \GraphQL\Server\ServerConfig::create()
->setSchema($schema)
->setContext($context)
->setQueryBatching(true);

# Allow GraphQL Server to handle the request and response
$server = new \GraphQL\Server\StandardServer($config);
$response = $server->processPsrRequest($request, $response, $response->getBody());

After this we just return the response:

# This line is not needed if you use StandardServer
$response->getBody()->write(json_encode($result));

$sqlQueryLogger = $this->db->getConfiguration()->getSQLLogger();
$this->logger->info(json_encode($sqlQueryLogger->queries));

return $response->withHeader('Content-Type', 'application/json');

So we write a JSON encoded array into the body and set the content-type header as application/json. What happens in the middle is related to your query logging.

Lines 3–4 actually get the SQLLogger from your Connection instance and log the queries made into log/app.log using Monolog. You can open up that file to see what happens when you make a request to /graphql

Your finished method should now look like this:

public function index(Request $request, Response $response)
{
$this->setResolvers(include dirname(__DIR__, 3) . '/src/GraphQL/resolvers.php');
$schema = \GraphQL\Utils\BuildSchema::build(file_get_contents(dirname(__DIR__, 3) . '/src/GraphQL/schema.graphqls'));

$input = $request->getParsedBody();
$query = $input['query'];

$variables = isset($input['variables']) ? $input['variables'] : null;

$context = [
'db' => $this->db,
'logger' => $this->logger
];

$result = \GraphQL\GraphQL::executeQuery($schema, $query, null, $context, $variables);

$response->getBody()->write(json_encode($result));

$sqlQueryLogger = $this->db->getConfiguration()->getSQLLogger();
$this->logger->info(json_encode($sqlQueryLogger->queries));

return $response->withHeader('Content-Type', 'application/json');
}

Note: You can replace these qualifiers with imports and make your code simpler:

use Doctrine\DBAL\Connection;
use GraphQL\Executor\Executor;
use GraphQL\GraphQL;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Utils\BuildSchema;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Log\LoggerInterface;

Now if you attempt to make a request to your app it won’t work. You’ll get Syntax Error: Unexpected <EOF> and i’ll explain why.

JSON parser middleware

Slim Framework v4 does not parse incoming JSON bodied requests automatically anymore. This was a feature with v3, but it was stripped from the newest version to offer developers more flexibility and to come up with their own solutions.

So, to go through this quickly, set up a src/Application/Middleware/JsonBodyParserMiddleware.php file and paste the following code in there (this is a direct copy from Slim’s documentation but they don’t have anchor links there):

<?php

namespace App\Application\Middleware;

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;

class JsonBodyParserMiddleware implements MiddlewareInterface
{
public function process(Request $request, RequestHandler $handler): Response
{
$contentType = $request->getHeaderLine('Content-Type');

if (strstr($contentType, 'application/json')) {
$contents = json_decode(file_get_contents('php://input'), true);
if (json_last_error() === JSON_ERROR_NONE) {
$request = $request->withParsedBody($contents);
}
}

return $handler->handle($request);
}
}

So this just parses the incoming input and sets the up$request with parsed body. Now open app/middleware.php and add the following inside the returned function:

$app->add(
\App\Application\Middleware\JsonBodyParserMiddleware::class
);

That’s it

You’re now ready to roll!

Make a GraphQL request to http://localhost:8080/graphql and see the results.

Example request body:

query {
getAuthors {
id
name
}
}

Response:

{
"data": {
"getAuthors": [
{
"id": "1",
"name": "J. K. Rowling"
},
{
"id": "2",
"name": "Andrzej Sapkowski"
},
{
"id": "3",
"name": "J. R. R. Tolkien"
}
]
}
}

You can use Postman, cURL or what ever is your preferred tool to make the request and test this out.

You can now build your GraphQL by extending the schema and writing your resolvers for each query and mutation. I did not cover how to use variables in this tutorial, so that’s up to you!

Remember, you can also extend your context if your resolvers need access to data. We’ll be using context in the Part 2 of this tutorial to set up loaders for fields that need deferring.

Tip: Logs

After you’ve gotten a successful response, open up your logs/app.log and see that Monolog has added your executed queries there:

[2020-01-10 15:55:41] slim-app.INFO: {"1":{"sql":"SELECT * FROM author","params":[],"types":[],"executionMS":0.03817296028137207}} [] {"uid":"b212de5"

You can use this to monitor your executed queries when creating your schema and resolvers to avoid overfetching.

--

--

Rcls
The Startup

Consultant, software architect and developer, freelance UI/UX designer, computer engineer, tech enthusiast, father.