If you’ve been keeping up, you’ll know that I’ve been writing a sort-of mega series around developing with GraphQL and Couchbase. In past tutorials we’ve explored how to use GraphQL with Java, how to use GraphQL with Node.js, and how to use GraphQL with Golang.
After I had produced content around all those languages, someone on my team had asked me where PHP was in the mix. From that comment spawned this tutorial regarding creating an API with GraphQL and PHP using Couchbase NoSQL as the data layer.
Before getting too invested in this GraphQL PHP and NoSQL tutorial, there are a few assumptions that must be met:
- Couchbase must have already been installed and configured.
- PHP must have been installed and configured.
- Composer and PECL must be available for downloading PHP project dependencies.
Assuming the above conditions are met, we can proceed to developing our application.
Creating a New Project with GraphQL and Couchbase Dependencies for PHP
The first step towards the success of our project is to create one and download the necessary dependencies. Somewhere on your computer, create a new directory and include a graphql.php file. Using your command prompt within this directory, execute the following:
1 |
composer require webonyx/graphql-php |
The above command will install our GraphQL dependency that does all of the heavy lifting for our API. Next, execute the following command from your command prompt:
1 |
pecl install couchbase |
The above command will install the Couchbase PHP SDK. Reiterating on my previous assumptions, you need to have Composer and PECL available to you.
More information on the Couchbase PHP SDK and the GraphQL package can be found in each of their respective documentations.
Configuring the Project for GraphQL and Couchbase Development
With the project created and the dependencies downloaded, we can work towards bootstrapping our application. Open the project’s graphql.php file and include the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?php require_once __DIR__ . '/vendor/autoload.php'; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\GraphQL; use GraphQL\Type\Schema; $cluster = new CouchbaseCluster("couchbase://localhost"); $authenticator = new \Couchbase\PasswordAuthenticator(); $authenticator->username('example')->password('123456'); $cluster->authenticate($authenticator); $bucket = $cluster->openBucket('example'); header('Content-Type: application/json'); ?> |
In the above code we are referencing our Composer dependency and including a few of the GraphQL classes that we plan to use. Without actually configuring GraphQL or using our database, we need to establish a connection to our database.
My PHP application was running locally alongside my instance of Couchbase. Make sure to fill in the gaps when it comes to your own Couchbase connection information.
Defining GraphQL Objects for Data Management
Now we can focus on defining our data model, otherwise referred to as GraphQL objects within our API. For this particular example we’re going to be referencing Pokemon data. For this reason we might have information about a particular Pokemon, what moves it has, or what video game it is found in. This is going to be the basis of our data model.
Let’s start with the following in our graphql.php file using this example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$moveType = new ObjectType([ 'name' => 'Move', 'fields' => [ 'name' => [ 'type' => Type::string() ], 'type' => [ 'type' => Type::string() ], 'power' => [ 'type' => Type::int() ], ], ]); $pokemonType = new ObjectType([ 'name' => 'Pokemon', 'fields' => [ 'name' => [ 'type' => Type::string() ], 'weight' => [ 'type' => Type::int() ], 'height' => [ 'type' => Type::int() ], 'attributes' => [ 'type' => Type::listOf(Type::string()) ], 'moves' => [ 'type' => Type::listOf($moveType) ], ], ]); |
In the above code we have two objects with the possible JSON properties, also called fields. For the $pokemonType
object, we have a field that references our other object. The listOf
type means that we can expect an array of data to be returned.
The two objects that we have are far from as complex as they could be, but they serve a realistic example. However, as of now they are nothing more than a model blueprint. We need to be able to populate them with actual data from our database.
Developing Queries to Populate GraphQL Objects
The next step is to come up with queries to be executed against our database which will fill our GraphQL objects. This is done through queries, which at the end of the day is nothing more than another GraphQL object that contains logic.
Take the following code for example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
$queryType = new ObjectType([ 'name' => 'Query', 'fields' => [ 'pokemons' => [ 'type' => Type::listOf($pokemonType), 'resolve' => function ($root, $args) { global $bucket; $query = CouchbaseN1qlQuery::fromString("SELECT example.* FROM example WHERE type = 'pokemon'"); $result = $bucket->query($query); return $result->rows; } ], 'pokemon' => [ 'type' => $pokemonType, 'args' => [ 'id' => Type::nonNull(Type::string()), ], 'resolve' => function ($root, $args) { global $bucket; $result = $bucket->get("pokemon-" . $args['id']); return $result->value; } ], ], ]); |
The above code, which would end up in your graphql.php file, has two possible queries. The first query called pokemons
will return an array of the $pokemonType
model. The resolve
function is what actually runs the logic. The logic for pokemons
will run a Couchbase N1QL query and return the result. The result of the N1QL query is mapped to each of the fields defined in our GraphQL object.
Executing the pokemons
query might look something like this:
1 |
curl http://localhost:8080 -d '{"query": "query { pokemons{ name, height, attributes } }" }' |
The pokemon
query is similar, but not the same. In the pokemon
query, we are requiring an id
be passed. This id
value is used to do a direct lookup for a particular NoSQL document rather than a NoSQL query.
The pokemon
query might look something like this:
1 |
curl http://localhost:8080 -d '{"query": "query { pokemon(id: \"25\"){ name, height, attributes } }" }' |
The result of this query will be a single object rather than an array.
Handling Model Relationships in a Simplistic Fashion
Now let’s say that we wanted some kind of data relationship. As of now we’ve assumed that move data and Pokemon data exist in the same document, but what if we had data that was split up, but related?
There are two approaches to this:
- We can do a JOIN query making potentially more complex queries to manage.
- We can modularize the queries on a per field basis keeping them small and slick.
If you had a chance to listen to my podcast titled GraphQL for API Development which featured GraphQL co-creator Lee Byron, you’ll know that neither of these are wrong and it comes down to preference. Let’s take a look at the latter where we modularize the queries and GraphQL objects.
Let’s say we now want to include game data and it is a separate object like this:
1 2 3 4 5 6 |
$gameType = new ObjectType([ 'name' => 'Game', 'fields' => [ 'name' => [ 'type' => Type::string() ], ], ]); |
Now let’s say that our Pokemon documents in our database contain a game id, but not the actual data. Similar to a primary and foreign key relationship, but not really because this is NoSQL.
What we could do now is the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
$pokemonType = new ObjectType([ 'name' => 'Pokemon', 'fields' => [ 'name' => [ 'type' => Type::string() ], 'weight' => [ 'type' => Type::int() ], 'height' => [ 'type' => Type::int() ], 'attributes' => [ 'type' => Type::listOf(Type::string()) ], 'moves' => [ 'type' => Type::listOf($moveType) ], 'game' => [ 'type' => $gameType, 'resolve' => function ($root, $args) { global $bucket; $result = $bucket->get($root->game); return $result->value; } ], ], ]); |
Notice that we’ve changed our $pokemonType
to include a game field, but this field has its own resolve
function. Inside this resolve
function, we can access the parent or $root
data which contains a game id, because that is what our pokemon
and pokemons
queries gave us. Using the id, we can get the game data that exists in a separate document and return it.
Now our original pokemons
query is spared from a JOIN statement. I’m not saying a JOIN statement is bad, but imagine if your had some wild data models that required a 50 line query. In those scenarios it might make sense to break it up rather than try to maintain a massive query. As a fun fact, the game operation on the database won’t be called unless the game field is requested in a GraphQL query from the frontend.
Bringing the Application Together
It is time to bring the application together. As of now we’ve only seen the data models and queries, but we haven’t configured it for use with GraphQL.
Take a look at the now complete code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
<?php require_once __DIR__ . '/vendor/autoload.php'; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\GraphQL; use GraphQL\Type\Schema; $cluster = new CouchbaseCluster("couchbase://localhost"); $authenticator = new \Couchbase\PasswordAuthenticator(); $authenticator->username('example')->password('123456'); $cluster->authenticate($authenticator); $bucket = $cluster->openBucket('example'); $gameType = new ObjectType([ 'name' => 'Game', 'fields' => [ 'name' => [ 'type' => Type::string() ], ], ]); $moveType = new ObjectType([ 'name' => 'Move', 'fields' => [ 'name' => [ 'type' => Type::string() ], 'type' => [ 'type' => Type::string() ], 'power' => [ 'type' => Type::int() ], ], ]); $pokemonType = new ObjectType([ 'name' => 'Pokemon', 'fields' => [ 'name' => [ 'type' => Type::string() ], 'weight' => [ 'type' => Type::int() ], 'height' => [ 'type' => Type::int() ], 'attributes' => [ 'type' => Type::listOf(Type::string()) ], 'moves' => [ 'type' => Type::listOf($moveType) ], 'game' => [ 'type' => $gameType, 'resolve' => function ($root, $args) { global $bucket; $result = $bucket->get($root->game); return $result->value; } ], ], ]); $queryType = new ObjectType([ 'name' => 'Query', 'fields' => [ 'pokemons' => [ 'type' => Type::listOf($pokemonType), 'resolve' => function ($root, $args) { global $bucket; $query = CouchbaseN1qlQuery::fromString("SELECT example.* FROM example WHERE type = 'pokemon'"); $result = $bucket->query($query); return $result->rows; } ], 'pokemon' => [ 'type' => $pokemonType, 'args' => [ 'id' => Type::nonNull(Type::string()), ], 'resolve' => function ($root, $args) { global $bucket; $result = $bucket->get("pokemon-" . $args['id']); return $result->value; } ], ], ]); $schema = new Schema([ 'query' => $queryType ]); $rawInput = file_get_contents('php://input'); $input = json_decode($rawInput, true); $query = $input['query']; $variableValues = isset($input['variables']) ? $input['variables'] : null; try { $result = GraphQL::executeQuery($schema, $query, null, null, $variableValues); $output = $result->toArray(); } catch (\Exception $e) { $output = [ 'errors' => [ [ 'message' => $e->getMessage() ] ] ]; } header('Content-Type: application/json'); echo json_encode($output); ?> |
The bottom half of the code was taken from the PHP GraphQL server official documentation. Essentially we’re defining our queries as the schema and taking in any input from the end user. This input is used in combination with the schema to produce a result that is returned to the user.
Conclusion
You just saw how to create a GraphQL API with PHP and Couchbase as the NoSQL database. GraphQL is a great alternative to writing RESTful APIs because it allows you to properly maintain a data model that can be queries via a frontend. This reduces the amount of HTTP requests as well as the payload returned in any request.
If you’d like to learn more about using Couchbase with PHP, check out a previous tutorial I wrote titled, Getting Started with NoSQL Using Couchbase Server and PHP.