Setting up GraphQL with PHP
How to set up GraphQL using PHP with Slim Framework in less than 20 minutes
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.