📜 ⬆️ ⬇️

We do GraphQL API on PHP and MySQL. Part 2: Mutations, Variables, Validation and Security

image

Not so long ago, I wrote an article on how to make your GraphQL server in PHP using the graphql-php library and using it to implement a simple API for getting 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 on the 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 her, but at the moment it is impossible to say which one is better.
')
Also, I’ll clarify that this article doesn’t encourage you to use PHP instead of Node.js or vice versa, but just want to show you how to use GraphQL if you’re on your own, or because of the circumstances of irresistible force working with PHP.

In order not to explain everything from the beginning, I will take the final code from the previous article as a basis. You can also view it in the article repository on Github . If you have not read the previous article, I recommend to read it before continuing.

This article will need to send INSERT and UPDATE database queries. 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
<?php namespace App; use PDO; class DB { private static $pdo; public static function init($config) { //  PDO  self::$pdo = new PDO("mysql:host={$config['host']};dbname={$config['database']}", $config['username'], $config['password']); //      self::$pdo->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 the last article - using this class to access the database on a live project is strictly prohibited. Instead, use the query designer available in the framework you are using or any other tool that can provide security.

So let's get started.

Mutations and variables


As I mentioned in the previous article, GraphQL, 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 data retrieval, and for changing POST data (PUT, DELETE) requests, in GraphQL, you should use Query for receiving data from the server, and Mutation for changing data.

The description of the field types for Mutation is the same as for Query. Mutations as well as queries can return data - this is convenient if you, for example, want to request updated information from the server immediately after the mutation is completed.

Note
According to the specification, the mutations are different from the usual requests, that the field mutations are always performed sequentially one after the other, while the field requests can be executed in parallel.

Let's create a Mutation type in a separate MutationType.php file in the Type folder:

App / Type / MutationType.php
 <?php namespace App\Type; use App\DB; use App\Types; use GraphQL\Type\Definition\ObjectType; class MutationType extends ObjectType { public function __construct() { $config = [ 'fields' => 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 schema in the graphql.php file immediately after Query:

 $schema = new Schema([ 'query' => Types::query(), 'mutation' => Types::mutation() ]); 

This completes the creation of the mutation, but so far little sense from it. Let's add to the mutation fields to change the data of existing users.

Change user information


As we already know, the data in the query can be passed as arguments. This means that we can easily add the “changeUserEmail” field to the mutation, which will take 2 arguments:


Let's change the code for the MutationType.php file:

App / Type / MutationType.php
 <?php namespace App\Type; use App\DB; use App\Types; use GraphQL\Type\Definition\ObjectType; class MutationType extends ObjectType { public function __construct() { $config = [ 'fields' => 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 will change the user's E-mail and return its data:

GraphQL user email change request

Query variables


It turned out not bad, but it is not always convenient to insert the values ​​of the arguments in the query text.
To simplify the insertion of dynamic data into a query in GraphQL, there is a special vocabulary of variable variables.

The values ​​of variables are transmitted to the server along with the request and, as a rule, in the form of a JSON object. Therefore, in order for our GraphQL server to work with them, let's change the endpoint code a little 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); 

Then the code of the graphql.php file will be as follows:

graphql.php
 <?php require_once __DIR__ . '/vendor/autoload.php'; use App\DB; use App\Types; use GraphQL\GraphQL; use GraphQL\Schema; try { //     $config = [ 'host' => '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 browser extensions for this, there is a “Query variables” tab in the lower left corner). And you can insert variables into a request by passing them into a mutation in the same way as arguments are passed to an anonymous function (with an indication of the type):
mutation($userId: Int, $userEmail: String)

Then they can be specified as argument values:
changeUserEmail (id: $userId, email: $userEmail)

And now the same query will look like this:

GraphQL user email change request using variables

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 the id, but we’d rather create a separate data type for user input.

In GraphQL data types are divided into 2 types:


All simple data types (Scalar, Enum, List, NonNull) refer to both types simultaneously.
Such types as Interface and Union relate only to Output, but in this article we will not consider them.

The composite type Object, discussed in the previous section , 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 (type) must be of the Input types type.

Let's create a new type of InputUserType to add a user. It will be similar to the UserType type, only we will now inherit not from ObjectType, but from InputObjectType:

App / Type / InputUserType.php
 <?php namespace App\Type; use App\Types; use GraphQL\Type\Definition\InputObjectType; class InputUserType extends InputObjectType { public function __construct() { $config = [ 'description' => ' ', 'fields' => function() { return [ 'name' => [ 'type' => Types::string(), 'description' => ' ' ], 'email' => [ 'type' => Types::string(), 'description' => 'E-mail ' ] ]; } ]; parent::__construct($config); } } 

And do not 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 in 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 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() ).

Is done. Now we can add a new user to the database using a mutation. User data is passed to Variables and we specify a variable as InputUser type:

GraphQL user add request

Validation and Security


I would divide the validation in GraphQL into 2 types:


And even in the comments to the previous article, it has been said many times that the security of your application is in your hands, and not in the hands of GraphQL, I will still show a couple of simple ways to protect your application when using graphql-php .

Data validation


Everything that we have done before this, in our example, is in no way protected from entering incorrect data.
Let's add a simple data validation, indicating which arguments are required, as well as organize the validation Email.

To denote the required arguments, we will use the special NonNull data type in GraphQL. Let's connect it to our type registry:

 public static function nonNull($type) { return Type::nonNull($type); } 

Now we just wrap them with the types of the 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 changeUserEmail mutation, the “id” and “email” will be required:

 'args' => [ 'id' => Types::nonNull(Types::int()), 'email' => Types::nonNull(Types::string()) ] 

Now, if we forget to specify a required parameter, we get an error. But we can still specify any string as the user's E-mail. Let's fix it.

In order for us to check the received E-mail, we need to create for it our scalar data type.

GraphQL has several built-in scalar types:


With some of them you are already familiar, 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:


The parseValue and parseLiteral methods for scalar types in many cases will be very similar, but you should pay attention to the fact that parseValue takes as its argument the value of a variable, and the parseLiteral object of the Node class that contains 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 place this file in the “Scalar” subfolder of the “Type” folder:

App / Type / Scalar / EmailType.php
 <?php namespace App\Type\Scalar; use GraphQL\Type\Definition\ScalarType; class EmailType extends ScalarType { public function serialize($value) { return $value; } public function parseValue($value) { if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { throw new \Exception('  E-mail'); } return $value; } public function parseLiteral($valueNode) { if (!filter_var($valueNode->value, FILTER_VALIDATE_EMAIL)) { throw new \Exception('  E-mail'); } return $valueNode->value; } } 

Note
You can check your E-mail for validity in any way you can. Any framework also has handy 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 email fields with the type String ( Types::string() ) with Email ( Types::email() ). For example, the full code of MutationType.php will now be like this:

App / Type / MutationType.php
 <?php namespace App\Type; use App\DB; use App\Types; use GraphQL\Type\Definition\ObjectType; class MutationType extends ObjectType { public function __construct() { $config = [ 'fields' => 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:

App / Type / InputUserType.php
 <?php namespace App\Type; use App\Types; use GraphQL\Type\Definition\InputObjectType; class InputUserType extends InputObjectType { public function __construct() { $config = [ 'description' => ' ', '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 you enter the wrong E-mail we will see the error:

Error in graphQL query: incorrect email

As you can see, now in the request for the $userEmail variable we specify the type of Email, not String. We also add exclamation marks after specifying the type of all required query arguments.

Validation request


To ensure security, GraphQL performs a number of operations related to the validation of the received request. Most of them are included in graphql-php by default and you have already encountered them when you saw errors when receiving a response from the GraphQL server, so I will not analyze them all, but I will show one - the most interesting case.

Fragment looping


When you want to request several objects that have the same fields, you would hardly want to enter the same list of fields for each object in the request.

To solve this problem in GraphQL there are fragments (Fragments). That is, you can list all the necessary fields in the fragment once, and then use this fragment in the request as many times as you need.

Syntax fragments resemble the spread operator in JavaScript. For example, let's ask for a list of users and their friends with some information about them.

Without fragments, it would look like this:

GraphQL query list of users and their friends

And by creating a userFields fragment for the User type, we can rewrite our query like this:

GraphQL query fragments

Maybe in this case we do not get much benefit from the use of fragments, but in a more complex query they will definitely be useful.

But we are now talking about security. What does it have to do with some fragments?

And despite the fact that now the potential intruder has the opportunity to loop the request, using a fragment within himself. And after that, our server would probably fall, but GraphQL will not allow it to do this and will give an error:

Error in graphQL query: fragment looping

Query complexity and depth of query


Also, in addition to the standard validation of the query, graphql-php allows you to specify the maximum complexity and maximum depth of the query.

Roughly speaking, complexity is an integer, which 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 query are zero, that is, unlimited. But we can limit them by connecting the classes for validation and the corresponding rules to 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 query:

 //      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 like this:

graphql.php
 <?php require_once __DIR__ . '/vendor/autoload.php'; use App\DB; use App\Types; use GraphQL\GraphQL; use GraphQL\Schema; use GraphQL\Validator\DocumentValidator; use GraphQL\Validator\Rules\QueryComplexity; use GraphQL\Validator\Rules\QueryDepth; try { //     $config = [ 'host' => '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:

Valid graphq query

Now we change the query so that its complexity is more than the maximum allowed:

Error in graphQL query: Exceeding maximum query complexity

And similarly we will increase the depth of the query

Error in GraphQL query: Exceeding the maximum complexity and 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 you download it, it sends a request to the endpoint to find out which fields are available, what types they are, their description, etc.

And this request is also validated. Therefore, you should either disable the validation of the request before loading the extension, or specify the maximum allowable complexity and depth greater than in this request.

In my extension for GrapiQL, the depth of the “system” query is 7, and the complexity is 109. Consider this nuance in order to avoid misunderstanding where errors occur.


That is, now you have the opportunity to limit the load on the server and deal with such a problem as Nested attak.

Conclusion


Thanks for attention.

Ask questions and I will try to answer them. And also I will be grateful if you point out my mistakes to me.

Source code with comprehensive comments is also available on Github.

Other parts of this article:
  1. Installation, layout and queries
  2. Mutations, variables, validation and security
  3. Problem solving N + 1 queries

Source: https://habr.com/ru/post/329238/


All Articles