📜 ⬆️ ⬇️

We write GraphQL API server on Yii2 with a client on Polymer + Apollo. Part 4. Validation. findings

Part 1. Server
Part 2. Client
Part 3. Mutations
Part 4. Validation. findings


Validation and UnionType


One of the interesting problems faced by the server was validation when changing data. What if there were errors when changing the object? In the articles you can find many solutions to this problem, but we decided to use the composite type Union . In simple words, Union is when the result of the request can be not only of the type, but different, depending on the result of the execution of resolve ().


Let's get started


For example, add the User field to our User object (see Part 1. Server ) and make it impossible to save the object with an incorrect address.


(I remind you that the finished draft of the article can be downloaded from github )


Step 1. Add a field to the database


Let's return to the server from part 1 and add an email field to our user. To add a field to the database, create a migration:


$> yii migrate/create add_email_to_user 

Open it and change the safeUp () method:


  public function safeUp() { $this->addColumn('user', 'email', $this->string()); } 

Save and run


 $> yii migrate 

Step 2. Add a rule to the User object


The only correct way to implement validation in Yii is to override the Model :: rules () method. This mechanism provides the broadest possible implementation of arbitrarily customized validations, and detailed documentation is attached to all capabilities. It should be avoided only in the rarest cases, in 99% everything is possible to do with the framework tools.


/models/User.php:


 ... /** * @inheritdoc */ public function rules() { return [ [['id', 'email'], 'required'], [['id', 'status'], 'integer'], [['createDate', 'modityDate', 'lastVisitDate'], 'safe'], [['firstname', 'lastname', 'email'], 'string', 'max' => 45], ['email', 'email'], ]; } ... 

Thus, we added 3 rules to the rules () method:


  1. field cannot be empty;
  2. field must be a string;
  3. field must contain valid email.

Step 3. Update the GraphQL type


In schema / UserType.php the changes are minimal:


 ... 'email' => [ 'type' => Type::string(), ], ... 

But in the mutation begins the most interesting.


Step 4. Adding ValidationErrorType


If you are not familiar with the Yii framework, I’ll clarify that if the object was not saved due to the fact that the fields were not validated, we can pull out all the errors using the $ object-> getErrors () method, which returns an associative array in the format :


 [ '' => [ '  ', '  ', ... ], ... ] 

The format is very convenient, but not for GraphQL. The fact is that we can not spit it out directly in JSON as it is, for the reason that the associative array is converted into an object, the attributes of which will be the fields of our object, and each object has its own. And GraphQL, as you know, works only with a predictable result. As an option, of course, we can create a separate type for each object, something like a UserValidationError, in which all fields will match the fields of the object itself and contain Type :: listOf (Type :: string ()). But the manual creation of such a number of types seemed to me too suboptimal, and I went the other way.


Add a single class with the universal schema / ValidationErrorType structure:


 <?php namespace app\schema; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; class ValidationErrorType extends ObjectType { public function __construct() { $config = [ 'fields' => function() { return [ 'field' => Type::string(), 'messages' => Type::listOf(Type::string()), ]; }, ]; parent::__construct($config); } } 

This type contains validation error information for one field. Fields of the ValidationErrorType type, as for me, are obvious and contain the name of the field in which the error occurred, and the list of messages with the content of the error. Everything is extremely simple.


Since we need to return the result of validation of all fields, and not one, create another type: schema / ValidationErrorsListType:


 <?php namespace app\schema; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; class ValidationErrorsListType extends ObjectType { public function __construct() { $config = [ 'fields' => function() { return [ 'errors' => Type::listOf(Types::validationError()), ]; }, ]; parent::__construct($config); } } 

Step 5. Generate a UnionType


In GraphQL there is a type Union , this is when the result of a resolve () field can return not only one type, but one of several (it seems I wrote this already, but it was worth mentioning this again). Thus, our goal now is that the result of a mutation of a change / creation of an object, without passing or vice versa, after successfully completing validation, returns either the modified object itself or an object of type ValidationErrorsListType.


What does "generation" mean? The fact is that we will not create for each mutation our return to UnionType, but will generate it on the go, based on the base type. How exactly, now show.


Let's change our schema / Types.php:


 ... use GraphQL\Type\Definition\UnionType; use yii\base\Model; ... private static $validationError; private static $validationErrorsList; //       private static $valitationTypes; ... // c     public static function validationError() { return self::$validationError ?: (self::$validationError = new ValidationErrorType()); } public static function validationErrorsList() { return self::$validationErrorsList ?: (self::$validationErrorsList = new ValidationErrorsListType()); } //     ,   // ,     public static function validationErrorsUnionType(ObjectType $type) { // -     ,   //    ,    // (     ,  / //    GraphQL   ) if (!isset(self::$valitationTypes[$type->name . 'ValidationErrorsType'])) { // self::$valitationTypes    ,    self::$valitationTypes[$type->name . 'ValidationErrorsType'] = new UnionType([ //    'name' => $type->name . 'ValidationErrorsType', //      // (    ,      //     ) 'types' => [ $type, Types::validationErrorsList(), ], //    resolveType //       // / , //     //     $model->getError() //      'resolveType' => function ($value) use ($type) { if ($value instanceof Model) { //   return $type; } else { //   (    , //     , //   ) return Types::validationErrorsList(); } } ]); } return self::$valitationTypes[$type->name . 'ValidationErrorsType']; } ... 

Step 6. Mutation


Make changes to the UserMutationType:


 ... use app\schema\Types; ... //     update 'type' => Types::validationErrorsUnionType(Types::user()), ... //     'email' => Type::string(), ... 'resolve' => function(User $user, $args) { //      , // ..      : //     ,     //     ,   $user->setAttributes($args); if ($user->save()) { return $user; } else { //  ,      - // ,     //    foreach ($user->getErrors() as $field => $messages) { //   ValidationErrorType $errors[] = [ 'field' => $field, 'messages' => $messages, ]; } //    //    //   (   ValidationErrorsListType) return ['errors' => $errors]; } } ... 

It's time to see how it works.


We are testing


We open again our GraphiQL and try to do something very simple.


We press in the input field Ctrl + Space, and see that we can generally return the update method:


image


Awesome.


Well, try to ask what we need, and see what happens.


image


As you can see, the first rule worked, which says that the field is mandatory.


image


Rule 2 - The email field must be in effect. Validation worked.


Well, let's finally see a successful result:


image


In general, I am sure that the described approach to validation is far from being the only optimal and imperfect, it can be refined and improved for specific needs, but in general, it seemed to me very versatile, and I hope it will help someone in practice and push the solution Problems.


findings


To understand exactly what we are drawing conclusions from, it was advisable for you to read all 4 parts.


When do you need and need you use GraphQL at all?


Well, first, you need to understand whether the game is worth the candle. If you have a task to write a couple of simple methods, of course, you don't need GraphQL. It will be something like a nailing microscope. IMHO, one of the main advantages of GraphQL is the convenience of scaling. And the possibility of scaling is necessary to lay nearly in any project (and even more so in the project in which at first glance it is not at all required).


If your API is "average", then, in principle, you will not feel the difference between the selected protocol. Replace mutations in GraphQL with actions for RESTa (an hour or two in the evening after working as a beer), and op - you already have a RESTful API server.


So...


Very low entry threshold. Understand the essence of the GraphQL query language, find the necessary libraries for the backend and frontend for any language, figure out how they work - all this will take you no more than a day (or even a few hours).


Something new. New experience is always useful (even negative). As you know, in the field of web technologies, bypassing the new, we are known to degrade.


Use for external (external) API. If your end product is an API that is used by a large number of clients (about which you do not know anything) GraphQL provides tremendous flexibility, since these clients may have completely diverse needs. But here is a double-edged sword. Technically, this is an advantage, but unfortunately, the client can be repelled by the fact that he will have to learn something new, and will have to look for developers with experience with the GraphQL API, because not everyone wants to hire developers with the promise to learn a new technology in a short time ( in spite of that, but in the case of GraphQL, these dates may indeed be very short).


Also GraphQL will help you in the case of remote work , and as a result, the lack of close communication between the backend and frontend developers (daily Skype calls are not always a guarantee of "close" communications).


Among the shortcomings, I would like to point out few examples on GraphQL + PHP online, since true soft buttons use either Node.js or Go (which prompted me to write this series of articles). The same situation with the Apollo library, all the official documentation for which is written under React, and I, as a backend developer, needed to spend time to understand how this all works with Polymer, although I would not call it a big problem during the transition. By the way, I advise you to read the very informative blog Apollo on Medium . There really are many interesting and practical articles on GraphQL.


Also one of the drawbacks is the lack of a convenient documentation generator like Swagger or ApiDoc.js. I managed to find one generator, but, unfortunately, it is very poor. If you have experience of more advanced documentation than the description in pdf, please share in the comments.


Who already uses GraphQL from well-known companies?


Github In general, nothing surprising, since the target audience of the site - the developers, and no worries about whether their API could not arise. It is worth noting that the documentation is very beautiful and thoughtful, and remarkably, contains some information about the basics of GraphQL, how to debug it, and guides to migration from REST.


Facebook They are the developers of the concept of GraphQL itself, and actively promote it. There are many reports on networks about how Facebook uses GraphQL.


PS: Friends! Never forget to implement request processing with the OPTIONS method in your API, so that for all such requests the server always returns the http-code 200, and an empty body. This will save your nerve cells.


And in general, do not forget about headers for CORS when developing any API:


 <IfModule mod_headers.c> Header add Access-Control-Allow-Origin "*" Header add Access-Control-Allow-Headers "Content-Type, Authorization" Header add Access-Control-Allow-Methods "GET, POST, OPTIONS" </IfModule> 

')

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


All Articles