
We make the GraphQL API on PHP and MySQL. Part 2: Mutations, Variables, Validation and Safety
- Tutorial

Not so long ago, I wrote an article on how to make my GraphQL server in PHP using the graphql-php library and how to use it to implement a simple API for retrieving data from MySQL.
Now I want to talk about how to make your GraphQL server work with mutations, and also try to answer the most common questions in the comments to the previous article, showing how to use data validation and touch upon the topic of security of the queries themselves.
Foreword
I want to remind you that in this example I use the graphql-php library . I know that there are other solutions besides it, but at the moment it is impossible to say with certainty which one is better.
I’ll also clarify that with this article I don’t urge you to use PHP instead of Node.js or vice versa, I just want to show how to use GraphQL if you are working with PHP due to force majeure circumstances.
In order not to explain everything all over again - I will take the final code from the previous article as the basis . You can also see it in the article repository on Github . If you have not read the previous article, I recommend that you read it before continuing.
This article will require you to send INSERT and UPDATE queries to the database. This has nothing to do with GraphQL directly, so I’ll just add a couple of new methods to the existing DB.php file so as not to focus on them in the future. As a result, the file code will be as follows:
App / DB.php
setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
}
public static function selectOne($query)
{
$records = self::select($query);
return array_shift($records);
}
public static function select($query)
{
$statement = self::$pdo->query($query);
return $statement->fetchAll();
}
public static function affectingStatement($query)
{
$statement = self::$pdo->query($query);
return $statement->rowCount();
}
public static function update($query)
{
$statement = self::$pdo->query($query);
$statement->execute();
return $statement->rowCount();
}
public static function insert($query)
{
$statement = self::$pdo->query($query);
$success = $statement->execute();
return $success ? self::$pdo->lastInsertId() : null;
}
}
Note
As I said in a previous article, it is strictly forbidden to use this class to access the database on a live project. Instead, use the query designer in the framework that you use or any other tool that can provide security.
So let's get started.
Mutations and Variables
As I mentioned in a previous article, the GraphQL schema, along with Query, can contain another root data type - Mutation.
This type is responsible for changing data on the server. Just as in REST it is recommended to use GET requests for receiving data, and for changing POST (PUT, DELETE) requests, in GraphQL you need to use Query to get data from the server, and Mutation to change the data.
The description of field types for Mutation occurs as well as for Query. Mutations, like requests, can return data - this is convenient if, for example, you want to request updated information from the server immediately after the mutation.
Note
The difference between mutations and ordinary queries, according to the specification, is that field mutations are always performed sequentially one after another, while field queries can be executed in parallel.
Let's create the Mutation type in a separate MutationType.php file in the Type folder:
App / Type / MutationType.php
And add it to our Types.php type registry:
function() {
return [
// Массив полей пока пуст
];
}
];
parent::__construct($config);
}
}
And add it to our Types.php type registry:
use App\Type\MutationType;
private static $mutation;
public static function mutation()
{
return self::$mutation ?: (self::$mutation = new MutationType());
}
It remains to add the mutation we just created to the diagram in the graphql.php file immediately after Query:
$schema = new Schema([
'query' => Types::query(),
'mutation' => Types::mutation()
]);
On this, the creation of the mutation is completed, but so far it is of little use. Let's add fields to the mutation to modify the data of existing users.
Change user information
As we already know, data can be passed to the request as arguments. So, we can easily add the “changeUserEmail” field to the mutation, which will take 2 arguments:
- id - user id
- email - new user email address
Let's change the code of the MutationType.php file:
App / Type / MutationType.php
function() {
return [
'changeUserEmail' => [
'type' => Types::user(),
'description' => 'Изменение E-mail пользователя',
'args' => [
'id' => Types::int(),
'email' => Types::string()
],
'resolve' => function ($root, $args) {
// Обновляем email пользователя
DB::update("UPDATE users SET email = '{$args['email']}' WHERE id = {$args['id']}");
// Запрашиваем и возвращаем "свежие" данные пользователя
$user = DB::selectOne("SELECT * from users WHERE id = {$args['id']}");
if (is_null($user)) {
throw new \Exception('Нет пользователя с таким id');
}
return $user;
}
]
];
}
];
parent::__construct($config);
}
}
Now we can perform a mutation that changes the user's email address and returns his data:

Query variables
It turned out pretty well, but inserting the argument values into the query text is not always convenient.
To simplify the insertion of dynamic data into a query in GraphQL, there is a special dictionary of Variables variables.
The values of the variables are transmitted to the server along with the request and, as a rule, in the form of a JSON object. Therefore, so that our GraphQL server can work with them, let's change the endpoint code a bit by adding to it the extraction and decoding of variables from the query:
$variables = isset($input['variables']) ? json_decode($input['variables'], true) : null;
And then pass them to GraphQL with the fifth parameter:
$result = GraphQL::execute($schema, $query, null, null, $variables);
After which the code of the graphql.php file will be as follows:
graphql.php
'localhost',
'database' => 'gql',
'username' => 'root',
'password' => 'root'
];
// Инициализация соединения с БД
DB::init($config);
// Получение запроса
$rawInput = file_get_contents('php://input');
$input = json_decode($rawInput, true);
$query = $input['query'];
// Получение переменных запроса
$variables = isset($input['variables']) ? json_decode($input['variables'], true) : null;
// Создание схемы
$schema = new Schema([
'query' => Types::query(),
'mutation' => Types::mutation()
]);
// Выполнение запроса
$result = GraphQL::execute($schema, $query, null, null, $variables);
} catch (\Exception $e) {
$result = [
'error' => [
'message' => $e->getMessage()
]
];
}
// Вывод результата
header('Content-Type: application/json; charset=UTF-8');
echo json_encode($result);
Now we can transfer the data in the form of JSON (in the GraphiQL extensions for the browser there is a “Query variables” tab in the lower left corner for this). And you can insert variables into the query by passing them into the mutation, just as arguments are passed to the anonymous function (indicating the type):
mutation($userId: Int, $userEmail: String)
Then you can specify them as argument values:
changeUserEmail (id: $userId, email: $userEmail)
And now the same query will look like this:

Add new user
In principle, we could make a similar mutation to add a new user, simply by adding a couple of missing arguments and removing id, but we better create a separate data type for user input.
In GraphQL, data types are divided into 2 types:
- Output types - types for outputting data (or field types)
- Input types - data input types (or argument types)
All simple data types (Scalar, Enum, List, NonNull) belong to both types at the same time.
Types such as Interface and Union refer only to Output, but in this article we will not consider them.
The composite Object type, which we examined in the previous part , also applies to Output, and for Input there is a similar type of InputObject.
The difference between InputObject and Object is that its fields cannot have arguments (args) and resolvers (resolve), and their types must be of the type Input types.
Let's create a new type of InputUserType to add a user. It will be similar to the UserType type, only now we will inherit not from ObjectType, but from InputObjectType:
App / Type / InputUserType.php
'Добавление пользователя',
'fields' => function() {
return [
'name' => [
'type' => Types::string(),
'description' => 'Имя пользователя'
],
'email' => [
'type' => Types::string(),
'description' => 'E-mail пользователя'
]
];
}
];
parent::__construct($config);
}
}
And don't forget to add it to our Types.php type registry:
use App\Type\InputUserType;
private static $inputUser;
public static function inputUser()
{
return self::$inputUser ?: (self::$inputUser = new InputUserType());
}
Fine! Now we can use it to add a new “addUser” field to MutationType.php next to the “changeUserEmail” field:
'addUser' => [
'type' => Types::user(),
'description' => 'Добавление пользователя',
'args' => [
'user' => Types::inputUser()
],
'resolve' => function ($root, $args) {
// Добавляем нового пользователя в БД
$userId = DB::insert("INSERT INTO users (name, email) VALUES ('{$args['user']['name']}', '{$args['user']['email']}')");
// Возвращаем данные только что созданного пользователя из БД
return DB::selectOne("SELECT * from users WHERE id = $userId");
}
]
I draw your attention to the fact that this field has one argument of type InputUser (
Types::inputUser()
) and returns the newly created user of type User ( Types::user()
). Done. Now we can add a new user to the database using a mutation. We pass the user data to Variables and specify the InputUser type as a variable:

Validation and Security
I would divide the validation in GraphQL into 2 types:
- Validation of data transmitted along with the request (arguments and variables)
- Validation of queries themselves
And although the comments to the previous article have repeatedly said that the security of your application is in your hands and not the hands of GraphQL, I will still show a couple of simple ways to secure my application using graphql-php .
Data validation
Everything that we did before in our example is not protected from entering incorrect data in any way.
Let's add a simple data validation, indicating which arguments are required, and also organize an email check.
To indicate the required arguments, we will use a special NonNull data type in GraphQL. Let's plug it into our type registry:
public static function nonNull($type)
{
return Type::nonNull($type);
}
Now just wrap them with the types of arguments that are required.
We assume that for the user in InputUserType.php the fields “name” and “email” are required:
'fields' => function() {
return [
'name' => [
'type' => Types::nonNull(Types::string()),
'description' => 'Имя пользователя'
],
'email' => [
'type' => Types::nonNull(Types::string()),
'description' => 'E-mail пользователя'
],
];
}
And for the mutation “changeUserEmail”, “id” and “email” will be required:
'args' => [
'id' => Types::nonNull(Types::int()),
'email' => Types::nonNull(Types::string())
]
Now if we forget to specify some required parameter, we will get an error. But we can still specify any line as the user's email address. Let's fix it.
In order for us to check the received E-mail, we need to create our own scalar data type for it.
GraphQL has several built-in scalar types:
- String
- Int
- Float
- Boolean
- Id
You are already familiar with some of them, and the purpose of the others is obvious.
To create a custom scalar data type, we need to write a class for it that will inherit from ScalarType and implement 3 methods:
- serialize - serialize the internal representation of data in a string for output
- parseValue - parsing data in Variables for internal representation
- parseLiteral - parsing data in query text for internal representation
The methods parseValue and parseLiteral for scalar types will be very similar in many cases, but you should pay attention to the fact that parseValue takes a variable value as an argument, and parseLiteral is an object of class Node containing this value in the "value" property.
Let's finally create a new scalar Email data type in a separate EmailType.php file. In order not to store all types in one big heap, I will put this file in the “Scalar” subfolder of the “Type” folder:
App / Type / Scalar / EmailType.php
value, FILTER_VALIDATE_EMAIL)) {
throw new \Exception('Не корректный E-mail');
}
return $valueNode->value;
}
}
Note
The validation of E-mail for validity you can carry out in any way available to you. In any framework there are also convenient tools for this.
It remains only to add the next data type to the Types.php registry:
use App\Type\Scalar\EmailType;
private static $emailType;
public static function email()
{
return self::$emailType ?: (self::$emailType = new EmailType());
}
And replace all the “email” fields with the String (
Types::string()
) type by Email ( Types::email()
). For example, the complete MutationType.php code will now look like this:App / Type / MutationType.php
function() {
return [
'changeUserEmail' => [
'type' => Types::user(),
'description' => 'Изменение E-mail пользователя',
'args' => [
'id' => Types::nonNull(Types::int()),
'email' => Types::nonNull(Types::email())
],
'resolve' => function ($root, $args) {
// Обновляем email пользователя
DB::update("UPDATE users SET email = '{$args['email']}' WHERE id = {$args['id']}");
// Запрашиваем и возвращаем "свежие" данные пользователя
$user = DB::selectOne("SELECT * from users WHERE id = {$args['id']}");
if (is_null($user)) {
throw new \Exception('Нет пользователя с таким id');
}
return $user;
}
],
'addUser' => [
'type' => Types::user(),
'description' => 'Добавление пользователя',
'args' => [
'user' => Types::inputUser()
],
'resolve' => function ($root, $args) {
// Добавляем нового пользователя в БД
$userId = DB::insert("INSERT INTO users (name, email) VALUES ('{$args['user']['name']}', '{$args['user']['email']}')");
// Возвращаем данные только что созданного пользователя из БД
return DB::selectOne("SELECT * from users WHERE id = $userId");
}
]
];
}
];
parent::__construct($config);
}
}
And the InputUserType.php code is like this:
App / Type / InputUserType.php
'Добавление пользователя',
'fields' => function() {
return [
'name' => [
'type' => Types::nonNull(Types::string()),
'description' => 'Имя пользователя'
],
'email' => [
'type' => Types::nonNull(Types::email()),
'description' => 'E-mail пользователя'
],
];
}
];
parent::__construct($config);
}
}
And now when entering the wrong E-mail, we will see an error: As you can see, now in the request for the variable we specify the type of Email, not String. We also add exclamation points after specifying the type of all required query arguments.

$userEmail
Request Validation
To ensure security, GraphQL performs a number of operations related to validating the received query. Most of them are enabled by default in graphql-php, and you already encountered them when you saw errors when receiving a response from the GraphQL server, so I won’t sort them all out but show one - the most interesting case.
Looping Fragments
When you want to request several objects that have the same fields, you are unlikely to want to enter the same list of fields for each object in the request.
To solve this problem, there are fragments (Fragments) in GraphQL. That is, you can list all the required fields in the fragment once, and then use this fragment in the query as many times as needed.
By syntax, the fragments resemble the spread operator in JavaScript. For example, let's request a list of users and their friends with some information about them.
Without fragments, it would look like this: And having created a fragment for the User type, we can rewrite our query like this:

userFields

Maybe in this case we don’t get much benefit from using fragments, but in a more complex query, they will definitely be useful.
But now we are talking about security. What does any fragment have to do with it?
And despite the fact that now a potential attacker has the opportunity to loop a request using a fragment inside himself. And after that, our server would probably crash, but GraphQL will not allow it to do this and will give an error:

Query complexity and query depth
In addition to the standard query validation, graphql-php allows you to specify the maximum complexity and maximum depth of the request.
Roughly speaking, complexity is an integer that in most cases corresponds to the number of fields in the query, and depth is the number of nesting levels in the query fields.
By default, the maximum complexity and maximum depth of the request are zero, that is, they are not limited. But we can limit them by connecting classes for validation and the corresponding rules in graphql.php:
use GraphQL\Validator\DocumentValidator;
use GraphQL\Validator\Rules\QueryComplexity;
use GraphQL\Validator\Rules\QueryDepth;
And adding these rules to the validator immediately before executing the request:
// Устанавливаем максимальную сложность запроса равной 6
DocumentValidator::addRule('QueryComplexity', new QueryComplexity(6));
// И максимальную глубину запроса равной 1
DocumentValidator::addRule('QueryDepth', new QueryDepth(1));
As a result, the code of the graphql.php file should look something like this:
graphql.php
'localhost',
'database' => 'gql',
'username' => 'root',
'password' => 'root'
];
// Инициализация соединения с БД
DB::init($config);
// Получение запроса
$rawInput = file_get_contents('php://input');
$input = json_decode($rawInput, true);
$query = $input['query'];
// Получение переменных запроса
$variables = isset($input['variables']) ? json_decode($input['variables'], true) : null;
// Создание схемы
$schema = new Schema([
'query' => Types::query(),
'mutation' => Types::mutation()
]);
// Устанавливаем максимальную сложность запроса равной 6
DocumentValidator::addRule('QueryComplexity', new QueryComplexity(6));
// И максимальную глубину запроса равной 1
DocumentValidator::addRule('QueryDepth', new QueryDepth(1));
// Выполнение запроса
$result = GraphQL::execute($schema, $query, null, null, $variables);
} catch (\Exception $e) {
$result = [
'error' => [
'message' => $e->getMessage()
]
];
}
// Вывод результата
header('Content-Type: application/json; charset=UTF-8');
echo json_encode($result);
Now let's check our server. First, we introduce a valid query: Now we’ll change the query so that its complexity is greater than the maximum allowable: And similarly, increase the depth of the query:



Note
If you use GraphiQL - an extension for the browser or a similar tool for testing queries, then it is worth remembering that when loading it sends a request to endpoint to find out which fields are available, what types they are, their description, etc.
And this query is also validated . Therefore, you should either disable the validation of the request before loading the extension, or indicate the maximum permissible complexity and depth more than in this request.
In my extension for GrapiQL, the depth of the "system" query is 7, and the complexity is 109. Please note this nuance in order to avoid a misunderstanding of where the errors are coming from.
And this query is also validated . Therefore, you should either disable the validation of the request before loading the extension, or indicate the maximum permissible complexity and depth more than in this request.
In my extension for GrapiQL, the depth of the "system" query is 7, and the complexity is 109. Please note this nuance in order to avoid a misunderstanding of where the errors are coming from.
That is, now you have the opportunity to limit the load on the server and deal with such a problem as Nested attack.
Conclusion
Thanks for attention.
Ask questions and I will try to answer them. And I will also be grateful if you point out to me my mistakes.
Source code with comprehensive comments is also available on Github.
Other parts of this article:
- Installation, schematic, and queries
- Mutations, Variables, Validation and Safety
- Solving the problem of N + 1 queries