📜 ⬆️ ⬇️

We write GraphQL API server on Yii2 with a client on Polymer + Apollo. Part 1. Server

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

The article is designed for a wide range of readers and requires only basic knowledge of PHP and Javascript. If you are engaged in programming and you are familiar with the abbreviation API, then you are at.

Initially, the article assumed only a description of the distinctive features of GraphQL and the RESTful API that we encountered in practice, but in the end it turned into a volume tutorial into several parts.

And at once I want to add that I don’t consider GraphQL to be a panacea for all ills and a killer of the RESTful API.
')

Who are we?


We are a company that develops mobile applications, and, as a rule, these applications have a client on iOS (well, of course), Android and Web. Personally, I in this company are engaged in writing the server part in PHP.

Prehistory


It all started with the fact that we finished the development of one application (well, as we finished, it is impossible to finish the development of the application, just its funding was untimely suspended), and we immediately went to a new project. Fortunately, the company does not limit the developers in the choice of technologies (of course within reasonable limits), and it was decided to make certain changes in order to try to avoid problems in the past. After all, as you know, if you continue to do what you do, then you continue to receive what you get.

RESTful API issues


Not that REST had big problems, but I came across one of them quite regularly. The fact is that the developers of our projects are very highly qualified, and this, in turn, is the reason that each of them considers himself at a certain level to be an expert in his field. The API is the thinnest thread among the technologies that binds backend and frontend experts and is the cause of many disputes about exactly how it needs to be developed.

Project structure


For example, consider the typical data structure:

image

Here you need to understand that in real life such tables can have 20+ fields, which makes using GraphQL even more attractive and justified. Why, I will try to explain in the article.

What API methods does the backend developer want to create?


The server-side developer, of course, wants to write API methods in such a way that they most closely correspond to complete and comprehensive objects. For example:

GET / api / user /: id

{ id email firstName lastName createDate modifyDate lastVisitDate status } 

GET / api / address /: id

 { id userId street zip cityId createDate modifyDate status } 

GET / api / city /: id

 { id name } 

… etc. Writing such an architecture is not only easier and faster (if not to say that any scaffolding does all this for us), but also architecturally and aesthetically beautiful and more correct (at least the developer himself thinks so). At best, the backend consents to the nested objects so that instead of the addressId in the response, the nested address object or (in the case of a one-to-many bundle) an address array is returned.

What API methods does the UI developer want to call?


The client developer is a little closer to real people (application users) and their needs, as a result of which there are several (many need to be read) places in the application where different sets of the same data are needed. Thus, he wants for each functional element to have a method:

GET / api / listOfUsersForMainScreen

 [ { firstName lastName dateCreated city street } ... ] 

... and so on in the same vein. Of course, this desire is fully justified not only by the desire to reduce the work itself, but also to improve the performance of the application. First, the UI makes one call instead of three (first user, then address, and then city). Secondly, this method will save you from getting a lot of (often considerable) redundant data. At the same time, it is very desirable that dateCreated be returned in the human format, and not in the original, taken from the field in the database (otherwise you also have to convert unix time).

On this basis, conflicts are born, of which I was a witness, and sometimes a participant. Good developers are looking for and, of course, find compromises to partially satisfy each other's needs. But if you, for example, have one frontend and two backends, then the loner will have to turn on his charisma to the maximum and use all diplomatic skills in order to push through the writing of his senseless methods that will be called once during the entire life cycle of an application.

What is GraphQL and why should it solve my problems?


For those who are not familiar with GraphQL, I advise you to spend no more than 5-10 minutes and visit this page to understand what they eat.

Why does he solve the above problem? Because when using GraphQL, the server developer describes atomic entities and connections as he likes it, and the UI builds custom queries depending on the needs of a particular element. And like the sheep are fed and the wolves are intact, but, fortunately, we do not live in a perfect world and still have to adapt. But first things first.

Show me the code


On Habré, there was already a good article on how to make friends with PHP and GraphQL, from which I learned a lot of useful things, and I apologize in advance for repeating, because the main point of the article is not to teach the basics, but to show the advantages and disadvantages.

The finished demo project of the server part can be found here .

Actually, let's start writing the server. To skip the setting of the framework and the environment that does not contain any information about GraphQL itself, you can immediately proceed to the creation of the structure (step 2).

Step 1. Install and configure Yii2


This step is not related to GraphQL, but it is necessary for our further actions.

Install and configure Yii2
I will just leave it here (mandatory to perform).

After all done, raise the baza. Team

 $> yii migrate 

... will create a table in the database for the history of migrations, and ...

 $> yii migrate/create init 

... creates a new file for migration in the migrations directory (init is the name of the migration, can be any). In the created file we put the following source code:

 <?php use yii\db\Migration; class m170828_175739_init extends Migration { public function safeUp() { $this->execute("CREATE TABLE IF NOT EXISTS `user` ( `id` INT NOT NULL, `firstname` VARCHAR(45) NULL, `lastname` VARCHAR(45) NULL, `createDate` DATETIME NULL, `modityDate` DATETIME NULL, `lastVisitDate` DATETIME NULL, `status` INT NULL, PRIMARY KEY (`id`)) ENGINE = InnoDB;"); $this->execute("CREATE TABLE IF NOT EXISTS `city` ( `id` INT NOT NULL, `name` VARCHAR(45) NULL, PRIMARY KEY (`id`)) ENGINE = InnoDB;"); $this->execute("CREATE TABLE IF NOT EXISTS `address` ( `id` INT NOT NULL, `street` VARCHAR(45) NULL, `zip` VARCHAR(45) NULL, `createDate` DATETIME NULL, `modifyDate` DATETIME NULL, `status` INT NULL, `userId` INT NOT NULL, `cityId` INT NOT NULL, PRIMARY KEY (`id`), INDEX `fk_address_user_idx` (`userId` ASC), INDEX `fk_address_city1_idx` (`cityId` ASC), CONSTRAINT `fk_address_user` FOREIGN KEY (`userId`) REFERENCES `user` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT `fk_address_city1` FOREIGN KEY (`cityId`) REFERENCES `city` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION) ENGINE = InnoDB;"); } public function safeDown() { echo "m170828_175739_init cannot be reverted.\n"; return false; } } 

... and start the migration:

 $> yii migrate 


Note. I myself consider it bad form to create a database structure bypassing the funds provided by the framework and I never do this in practice. In this example, it only saved me time. The same applies to the empty function for the roll-back safeDown () that I neglected.

Note. For the test, we need some test data in our database. You can fill it yourself or take the migration from the test repository .

Based on the created data structure, we will generate models (ActiveRecord) using the Gii generator built into the framework:

 $> yii gii/model --tableName=user --modelClass=User $> yii gii/model --tableName=city --modelClass=City $> yii gii/model --tableName=address --modelClass=Address 

Finally, all the boring work behind, and now it's time to do what we started all this for.

Step 2. Installing an extension for GraphQL


For our project we will use the base webonyx / graphql-php extension (https://github.com/webonyx/graphql-php).

 $> composer require webonyx/graphql-php 

Also on github you can find an already sharpened extension under Yii2, but at first glance it did not inspire me. If you know him, share your experiences in the comments.

Step 3. Create a project structure.


The main elements of the structure involved in the implementation of GraphQL server:

schema is the directory in the root of the framework that will store entities for the GraphQL server: types and mutations. The directory name and location is not important, you can name it as you like and place it in another namespace (for example, api / or components /).

schema / QueryType.php, schema / MutationType.php - “root” types.

schema / Types.php is a kind of aggregator for initializing our custom types.

schema / mutations — mutations are preferably stored in a separate directory for convenience.

Well, actually controllers / api / GraphqlController.php is an entry point. All requests to the GraphQL server go through one entry point - / api / graphql. Thus, nothing prevents you from simultaneously containing the RESTful API (if it comes to that, then, roughly speaking, a GraphQL server is one API method that accepts GraphQL queries as input).

Step 3.1. Create types for our models.


Create a new schema directory and in it classes for our models.

/schema/CityType.php:

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

The following parameters are involved in the field description:

type - GraphQL-type field (Type :: string (), Type :: int (), etc.).

description - description (just text; will be used in the schema, gives convenience when debugging requests).

args - taken arguments (associative array, where key is the name of the argument, value is a GraphQL type).

resolve ($ root, $ args) is a function that returns a field value. Arguments: $ root - the object of the corresponding ActiveRecord (in this case, the object models \ City will come into it); $ args is an associative array of arguments (described in $ args).

All fields except type are optional.

/schema/UserType.php:

 <?php namespace app\schema; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use app\models\User; class UserType extends ObjectType { public function __construct() { $config = [ 'fields' => function() { return [ 'firstname' => [ 'type' => Type::string(), ], 'lastname' => [ 'type' => Type::string(), ], 'createDate' => [ 'type' => Type::string(), //  ,  //     //         // (       ) 'description' => 'Date when user was created', //     ,  //   format 'args' => [ 'format' => Type::string(), ], //        //  'resolve' => function(User $user, $args) { if (isset($args['format'])) { return date($args['format'], strtotime($user->createDate)); } //    format  , //    return $user->createDate; }, ], //       //    ,   //   ,  ,   'modityDate' => [ 'type' => Type::string(), ], 'lastVisitDate' => [ 'type' => Type::string(), ], 'status' => [ 'type' => Type::int(), ], //      - //  'addresses' => [ //      , //     //  Type::listOf,  //   ,     //   ,  //   'type' => Type::listOf(Types::address()), 'resolve' => function(User $user) { //  ,      //    $user    // ,    ,  .. //       ,   //        return $user->addresses; }, ], ]; } ]; parent::__construct($config); } } 

/schema/AddressType.php:

 <?php namespace app\schema; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; class AddressType extends ObjectType { public function __construct() { $config = [ 'fields' => function() { return [ 'user' => [ 'type' => Types::user(), ], 'city' => [ 'type' => Types::city(), ], //      //      //   ]; } ]; parent::__construct($config); } } 

For AddressType.php, we need the auxiliary class Types.php, which is described below.

Step 3.2. schema / Types.php


The fact is that GraphQL scheme cannot have several identical (same-named) types. This is what the aggregator Types.php is meant to follow. The name was purposely chosen by Types, so that it was similar, and, at the same time, differed from the standard class of the GraphQL library - Type. Thus, the standard type can be accessed via Type :: int (), Type :: string (), and the custom type can be Types :: query (), Types :: user (), etc.

schema / Types.php:

 <?php namespace app\schema; use GraphQL\Type\Definition\ObjectType; class Types { private static $query; private static $mutation; private static $user; private static $address; private static $city; public static function query() { return self::$query ?: (self::$query = new QueryType()); } public static function user() { return self::$user ?: (self::$user = new UserType()); } public static function address() { return self::$address ?: (self::$address = new AddressType()); } public static function city() { return self::$city ?: (self::$city = new CityType()); } } 

Step 3.3. schema / QueryType.php


 <?php namespace app\schema; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use app\models\User; use app\models\Address; class QueryType extends ObjectType { public function __construct() { $config = [ 'fields' => function() { return [ 'user' => [ 'type' => Types::user(), //   ,  //     'args' => [ //     //   Type::nonNull() 'id' => Type::nonNull(Type::int()), ], 'resolve' => function($root, $args) { //        //   $args      // `id`,    ,       //     ,     `id` //       //     return User::find()->where(['id' => $args['id']])->one(); } ], //     user  ,   //          //          //     'addresses' => [ //    //      //  'type' => Type::listOf(Types::address()), //     'args' => [ 'zip' => Type::string(), 'street' => Type::string(), ], 'resolve' => function($root, $args) { $query = Address::find(); if (!empty($args)) { $query->where($args); } return $query->all(); } ], ]; } ]; parent::__construct($config); } } 

C MutationType.php will understand a little later.

Step 3.4. Create controllers / api / GraphqlController.php


Now we’ll finish the last part so that our newly created types can be reached.

GraphqlController.php:

 <?php namespace app\controllers\api; use app\schema\Types; use GraphQL\GraphQL; use GraphQL\Schema; use yii\base\InvalidParamException; use yii\helpers\Json; class GraphqlController extends \yii\rest\ActiveController { public $modelClass = ''; /** * @inheritdoc */ protected function verbs() { return [ 'index' => ['POST'], ]; } public function actions() { return []; } public function actionIndex() { //      //   MULTIPART,    POST/GET $query = \Yii::$app->request->get('query', \Yii::$app->request->post('query')); $variables = \Yii::$app->request->get('variables', \Yii::$app->request->post('variables')); $operation = \Yii::$app->request->get('operation', \Yii::$app->request->post('operation', null)); if (empty($query)) { $rawInput = file_get_contents('php://input'); $input = json_decode($rawInput, true); $query = $input['query']; $variables = isset($input['variables']) ? $input['variables'] : []; $operation = isset($input['operation']) ? $input['operation'] : null; } //    variables  null,    //     if (!empty($variables) && !is_array($variables)) { try { $variables = Json::decode($variables); } catch (InvalidParamException $e) { $variables = null; } } //          $schema = new Schema([ 'query' => Types::query(), ]); // ! $result = GraphQL::execute( $schema, $query, null, null, empty($variables) ? null : $variables, empty($operation) ? null : $operation ); return $result; } } 

It is also worth noting that wrapping in try-catch GraphQL :: execute () for formatting error output will not do anything, because it already intercepts everything inside, and what to do with errors, I will describe a little later.

Step 4. We are testing.


Actually, it is time to realize, see and touch what we did.

Let's check our query in the extension for Chome - GraphiQL. Personally, I prefer “GraphiQL Feen”, which has more advanced functionality (saved queries, custom headers). True, the latter sometimes has problems with error output, or rather just does not output anything in case of an error on the server.

Enter the required data in the fields and enjoy the result:

image

Thus, after all we have:


Note. If you have not earned a beautiful URL, it means that you have not configured UrlManager and .htaccess, because in primeval yii it does not work. How to do this, look in the repository to the article .

Autocomplete arguments:

image

A completely custom query with a completely custom result is the developer’s frontend’s dream:

image

Could you all dream about it by developing your own RESTful, while not doing anything for it? Of course not.

It is also important to pay attention to the fact that in order to pull out the addresses and the user, we do not need to make two separate requests, but we can (and should) do everything at once:

image

The possibility of debugging queries, I personally consider as one of the significant advantages of GraphQL. The fact is that it automatically generates a scheme that the extension pulls in, and you turn on validation and autocomplete. Thus, having only the address of the entry point to the GraphQL server, you can fully explore its capabilities. Nonsekurno? As for me, security is implemented on a slightly different level. Having access to the documentation of any API, we also have its full scheme. Some RESTful APIs have similar JSON or XML schemes, unfortunately not many, but for GraphQL this is a standard, and they are also actively used by clients.

To be continued ...


In the next (their) part (s) of the article I will describe how to use all this in a beautiful way in the UI , and of course we will touch on another topic without which the API is not an API - these are mutations .

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


All Articles