Ticker

6/recent/ticker-posts

GraphQL Server với Laravel

    Để giúp chúng ta tìm hiểu cơ bản về GraphQL, trong bài viết này sẽ xây dựng một GraphQL Server sử dụng Laravel. API này cũng có khả năng CRUD( tạo, đọc, cập nhật, xoá) như các APIs bình thường vẫn hay sử dụng - để chúng ta có sự mường tượng, hình dung dễ dàng về GraphQL. 

Bài tiếp theo sẽ phân tích cái mạnh cái yếu, bản chất của nó và khi nào thì chúng ta nên áp dụng. Tuy nhiên ở bài này chúng ta cũng cần có vài cái gạch đầu dòng sau đây để hiểu được cái cơ bản GraphQL:

1. Ngắn gọn về GraphQL

2. Build GraphQL Server sử dụng Laravel

3. Sử dụng Postman để truy vấn GraphQL

Giờ chúng ta đi vào từng vấn đề.

1. Ngắn gọn về GraphQL

    Giới thiệu từ trang chủ của GraphQL đây:
GraphQL: A query language for your API
   
 "A query language" - là một ngôn ngữ truy vấn. Vậy là phải học cú pháp của nó để có thể sử dụng được. Và bản chất của nó là JSON nên cũng không quá khó để tiếp cận với nó. 

    Cái tên của nó "GraphQL"có lẽ xuất phát từ main concept của nó; như đã biết Rest API chúng ta hay sử dụng thì main concept của nó là "Resource" - nghĩa là có rất nhiều URLs tương ứng với những phương thức GET, PUT, POST ... được sinh ra với từng "Resource". Còn ở GraphQL, main concept của nó là "Entity Graph"- thường nó chỉ có 1 URL và sử dụng các truy vấn để tương tác với dữ liệu. Ở đây GraphQL sử dụng hai khái niệm chính là "Query" (lấy dữ liệu nào đó) và "Mutation" (làm gì đó mà có thay đổi dữ liệu) [và một khái niệm nữa là "Subscription and Realtime Updates" một điều khá hay nhưng trong bài viết này chưa nói đến -  nó dùng để lắng nghe sự kiên trên server, khi dữ liệu trên server được thay đổi nó cũng thay đổi theo nhằm cung cấp dữ liệu cho client một cách realtime nhất có thể]. Lát nữa chúng ta sẽ hiểu những khái niệm này ngay thôi.

2. Build GraphQL Server sử dụng Laravel

Trong bài viết này sử dụng Laravel 8.x và package rebing/graphql-laravel. Source code thì chúng sử dụng sample của Zerobug 8 cho nhanh nhé.

Cách cài đặt:

$ composer require rebing/graphql-laravel
$ php artisan vendor:publish --provider="Rebing\GraphQL\GraphQLServiceProvider"

Giờ cần chỉnh sửa lại chút ở config/graphql.php file:

return [

    // The prefix for routes
    'prefix' => 'graphql',

    // The routes to make GraphQL request. Either a string that will apply
    // to both query and mutation or an array containing the key 'query' and/or
    // 'mutation' with the according Route
    //
    // Example:
    //
    // Same route for both query and mutation
    //
    // 'routes' => 'path/to/query/{graphql_schema?}',
    //
    // or define each route
    //
    // 'routes' => [
    //     'query' => 'query/{graphql_schema?}',
    //     'mutation' => 'mutation/{graphql_schema?}',
    // ]
    //
    'routes' => '{graphql_schema?}',

    // The controller to use in GraphQL request. Either a string that will apply
    // to both query and mutation or an array containing the key 'query' and/or
    // 'mutation' with the according Controller and method
    //
    // Example:
    //
    // 'controllers' => [
    //     'query' => '\Rebing\GraphQL\GraphQLController@query',
    //     'mutation' => '\Rebing\GraphQL\GraphQLController@mutation'
    // ]
    //
    'controllers' => \Rebing\GraphQL\GraphQLController::class.'@query',

    // Any middleware for the graphql route group
    'middleware' => [],

    // Additional route group attributes
    //
    // Example:
    //
    // 'route_group_attributes' => ['guard' => 'api']
    //
    'route_group_attributes' => [],

    // The name of the default schema used when no argument is provided
    // to GraphQL::schema() or when the route is used without the graphql_schema
    // parameter.
    'default_schema' => 'news',
// The schemas for query and/or mutation. It expects an array of schemas to provide // both the 'query' fields and the 'mutation' fields. // // You can also provide a middleware that will only apply to the given schema // // Example: // // 'schema' => 'default', // // 'schemas' => [ // 'default' => [ // 'query' => [ // 'users' => 'App\GraphQL\Query\UsersQuery' // ], // 'mutation' => [ // // ] // ], // 'user' => [ // 'query' => [ // 'profile' => 'App\GraphQL\Query\ProfileQuery' // ], // 'mutation' => [ // // ], // 'middleware' => ['auth'], // ], // 'user/me' => [ // 'query' => [ // 'profile' => 'App\GraphQL\Query\MyProfileQuery' // ], // 'mutation' => [ // // ], // 'middleware' => ['auth'], // ], // ] // 'schemas' => [ 'news' => [
'query' => [ 'news' => App\GraphQL\Queries\NewsQuery::class, 'listNews' => App\GraphQL\Queries\ListNewsQuery::class, ], 'mutation' => [ // Create a news 'createNews' => App\GraphQL\Mutations\CreateNewsMutation::class, // update news 'updateNews' => App\GraphQL\Mutations\UpdateNewsMutation::class, // delete a news 'deleteNews' => App\GraphQL\Mutations\DeleteNewsMutation::class, // 'example_mutation' => ExampleMutation::class, ], 'middleware' => [], 'method' => ['get', 'post'], ], ], // The types available in the application. You can then access it from the // facade like this: GraphQL::type('user') // // Example: // // 'types' => [ // 'user' => 'App\GraphQL\Type\UserType' // ] // 'types' => [ 'News' => App\GraphQL\Types\NewsType::class, // 'example' => ExampleType::class, // 'relation_example' => ExampleRelationType::class, // \Rebing\GraphQL\Support\UploadType::class, ], // The types will be loaded on demand. Default is to load all types on each request // Can increase performance on schemes with many types // Presupposes the config type key to match the type class name property 'lazyload_types' => false, // This callable will be passed the Error object for each errors GraphQL catch. // The method should return an array representing the error. // Typically: // [ // 'message' => '', // 'locations' => [] // ] 'error_formatter' => ['\Rebing\GraphQL\GraphQL', 'formatError'], /* * Custom Error Handling * * Expected handler signature is: function (array $errors, callable $formatter): array * * The default handler will pass exceptions to laravel Error Handling mechanism */ 'errors_handler' => ['\Rebing\GraphQL\GraphQL', 'handleErrors'], // You can set the key, which will be used to retrieve the dynamic variables 'params_key' => 'variables', /* * Options to limit the query complexity and depth. See the doc * @ https://webonyx.github.io/graphql-php/security * for details. Disabled by default. */ 'security' => [ 'query_max_complexity' => null, 'query_max_depth' => null, 'disable_introspection' => false, ], /* * You can define your own pagination type. * Reference \Rebing\GraphQL\Support\PaginationType::class */ 'pagination_type' => \Rebing\GraphQL\Support\PaginationType::class, /* * Config for GraphiQL (see (https://github.com/graphql/graphiql). */ 'graphiql' => [ 'prefix' => '/graphiql', 'controller' => \Rebing\GraphQL\GraphQLController::class.'@graphiql', 'middleware' => [], 'view' => 'graphql::graphiql', 'display' => env('ENABLE_GRAPHIQL', true), ], /* * Overrides the default field resolver * See http://webonyx.github.io/graphql-php/data-fetching/#default-field-resolver * * Example: * * ```php * 'defaultFieldResolver' => function ($root, $args, $context, $info) { * }, * ``` * or * ```php * 'defaultFieldResolver' => [SomeKlass::class, 'someMethod'], * ``` */ 'defaultFieldResolver' => null, /* * Any headers that will be added to the response returned by the default controller */ 'headers' => [], /* * Any JSON encoding options when returning a response from the default controller * See http://php.net/manual/function.json-encode.php for the full list of options */ 'json_encoding_options' => 0, ];

Có vài thứ cần chú ý ở đây. Phần cấu hình route chúng ta sẽ truy cập "/graphql" hoặc tuỳ ý đặt URI gì cho nó cũng được.

// The prefix for routes
'prefix' => 'graphql',

Một thứ hay hay là cái "GraphiQL", nó cũng gần giống với cái Swagger UI chúng ta cũng có thể tương tác trực tiếp luôn trên giao diện, có thể query, có thể xem history. Tham khảo thêm về GraphiQL ở đây nhé. Tuy nhiên ở bài này sẽ sử dụng Postman để tương tác với GraphQL Server.

/*
* Config for GraphiQL (see (https://github.com/graphql/graphiql).
*/
'graphiql' => [
    'prefix' => '/graphiql',
    'controller' => \Rebing\GraphQL\GraphQLController::class.'@graphiql',
    'middleware' => [],
    'view' => 'graphql::graphiql',
    'display' => env('ENABLE_GRAPHIQL', true),
],
'prefix' => 'graphql',

Đây là cái giao diện tuyệt vời của GraphiQL:

graphiql

Tiếp theo là một điểm quan trọng, chúng ta cần định nghĩa những Schemas mà API Server cung cấp. Sau khi định nghĩa mới hay cập nhật các Schemas cũng phải quay lại phần cấu hình này để update lại.

// The name of the default schema used when no argument is provided
// to GraphQL::schema() or when the route is used without the graphql_schema
// parameter.
'default_schema' => 'news',

// The schemas for query and/or mutation. It expects an array of schemas to provide
'schemas' => [
    'news' => [
        'query' => [
            'news' => App\GraphQL\Queries\NewsQuery::class,
            'listNews' => App\GraphQL\Queries\ListNewsQuery::class,
        ],
        'mutation' => [
            // Create a news
            'createNews' => App\GraphQL\Mutations\CreateNewsMutation::class,
            // update news
            'updateNews' => App\GraphQL\Mutations\UpdateNewsMutation::class,
            // delete a news
            'deleteNews' => App\GraphQL\Mutations\DeleteNewsMutation::class,
            // 'example_mutation'  => ExampleMutation::class,
        ],
        'middleware' => [],
        'method' => ['get', 'post'],
    ],
],

Giờ thì tạo News model, seeder và migration:

$ php artisan make:model News -m

Migration:

class CreateNewsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('news', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title');
            $table->longText('desc');
            $table->longText('meta')->nullable();
            $table->longText('content');
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('news');
    }
}

Model:

class News extends Model implements HasMedia
{
    use SoftDeletes, InteractsWithMedia, HasFactory;

    public $table = 'news';

    protected $dates = [
        'created_at',
        'updated_at',
        'deleted_at',
    ];

    protected $fillable = [
        'title',
        'desc',
        'meta',
        'content',
        'created_at',
        'updated_at',
        'deleted_at',
    ];
    
    protected function serializeDate(DateTimeInterface $date)
    {
        return $date->format('Y-m-d H:i:s');
    }

    public function registerMediaConversions(Media $media = null): void
    {
        $this->addMediaConversion('thumb')->fit('crop', 50, 50);
        $this->addMediaConversion('preview')->fit('crop', 120, 120);
    }
}

Seeder:

class NewsTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $news = [
            [
                'id'             => 1,
                'title'          => 'title first news',
                'desc'           => '

desc of first news

', 'meta' => 'meta data 1', 'content' => '

content of first news

', 'created_at' => Carbon::now()->format('Y-m-d H:i:s'), 'updated_at' => Carbon::now()->format('Y-m-d H:i:s'), 'deleted_at' => null ], [ 'id' => 2, 'title' => 'title second news', 'desc' => '

desc of second news

', 'meta' => 'meta data 3', 'content' => '

content of second news

', 'created_at' => Carbon::now()->format('Y-m-d H:i:s'), 'updated_at' => Carbon::now()->format('Y-m-d H:i:s'), 'deleted_at' => null ], [ 'id' => 3, 'title' => 'title third news', 'desc' => '

desc of third news

', 'meta' => 'meta data 3', 'content' => '

content of third news

', 'created_at' => Carbon::now()->format('Y-m-d H:i:s'), 'updated_at' => Carbon::now()->format('Y-m-d H:i:s'), 'deleted_at' => null ], [ 'id' => 4, 'title' => 'title fourth news', 'desc' => '

desc of fourth news

', 'meta' => 'meta data 4', 'content' => '

content of fourth news

', 'created_at' => Carbon::now()->format('Y-m-d H:i:s'), 'updated_at' => Carbon::now()->format('Y-m-d H:i:s'), 'deleted_at' => null ], [ 'id' => 5, 'title' => 'deleted news', 'desc' => '

desc of deleted news

', 'meta' => 'meta data 5', 'content' => '

content of deleted news

', 'created_at' => Carbon::now()->format('Y-m-d H:i:s'), 'updated_at' => Carbon::now()->format('Y-m-d H:i:s'), 'deleted_at' => Carbon::now()->format('Y-m-d H:i:s') ] ]; News::insert($news); } }

Để xây dựng GraphQL Server, chúng ta bắt đầu với việc tạo một Graph Type cho News. Cần tạo  NewsType.php file trong thư mục App\GraphQL\Types

namespace App\GraphQL\Types;

use App\Models\News;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Type as GraphQLType;

class NewsType extends GraphQLType
{
    protected $attributes = [
        'name' => 'News',
        'description' => 'Get news',
        'model' => News::class
    ];


    public function fields(): array
    {
        return [
            'id' => [
                'type' => Type::nonNull(Type::int()),
                'description' => 'Id of a particular news',
            ],
            'title' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'The title of the news',
            ],
            'desc' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'Desc of the news',
            ],
            'content' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'Content of the news',
            ],
            'meta' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'Meta of the news',
            ],
            'created_at' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'created_at is the news was created',
            ],
            'updated_at' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'updated_at is the news was updated',
            ]
        ];
    }
}
Tạo các Graph Queries, NewsQuery.phpListNewsQuery.php trong thư mục App\graphql\Queries. Hai files này sẽ cho phép "query" lấy ra được tất cả các News và lấy chi tiết từng News.

NewsQuery:

namespace App\GraphQL\Queries;

use App\Models\News;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Query;

class NewsQuery extends Query
{
    protected $attributes = [
        'name' => 'news',
    ];

    public function type(): Type
    {
        return GraphQL::type('News');
    }

    public function args(): array
    {
        return [
            'id' => [
                'name' => 'id',
                'type' => Type::int(),
                'rules' => ['required']
            ],
        ];
    }

    public function resolve($root, $args)
    {
        return News::findOrFail($args['id']);
    }
}

ListNewsQuery:

namespace App\graphql\Queries;

use App\Models\News;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Query;

class ListNewsQuery extends Query
{
    protected $attributes = [
        'name' => 'listNews',
    ];

    public function type(): Type
    {
        return Type::listOf(GraphQL::type('News'));
    }

    public function resolve($root, $args)
    {
        return News::all();
    }
}

Tiếp theo tạo các Mutations. Ở đây chúng ta cần CUD[thêm, cập nhật và xoá], nên tạo tương ứng 3 files CreateNewsMutation.php, UpdateNewsMutation.phpDeleteNewsMutation.php trong thư mục App\graphql\Mutations. Nếu có nhiều có lẽ nên tạo riêng từng thư mục cho từng Schema thì sẽ thuận tiện quản lý hơn.

CreateNewsMutation:

namespace App\graphql\Mutations;

use App\Models\News;
use Rebing\GraphQL\Support\Mutation;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;

class CreateNewsMutation extends Mutation
{
    protected $attributes = [
        'name' => 'createNews'
    ];

    public function type(): Type
    {
        return GraphQL::type('News');
    }

    public function args(): array
    {
        return [
            'title' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'The title of the news',
            ],
            'desc' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'desc of the news',
            ],
            'content' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'content of the news',
            ],
            'meta' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'Meta of the news',
            ]
        ];
    }

    public function resolve($root, $args)
    {
        $news = new News();
        $news->fill($args);
        $news->save();

        return $news;
    }
}

UpdateNewsMutation:

namespace App\graphql\Mutations;

use App\Models\News;
use Rebing\GraphQL\Support\Facades\GraphQL;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Mutation;

class UpdateNewsMutation extends Mutation
{
    protected $attributes = [
        'name' => 'updateNews'
    ];

    public function type(): Type
    {
        return GraphQL::type('News');
    }

    public function args(): array
    {
        return [
            'id' => [
                'name' => 'id',
                'type' =>  Type::nonNull(Type::int()),
            ],
            'title' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'The title of the news',
            ],
            'desc' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'desc of the news',
            ],
            'content' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'content of the news',
            ],
            'meta' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'created_at is the news was created',
            ]
        ];
    }

    public function resolve($root, $args)
    {
        $news = News::findOrFail($args['id']);
        $news->fill($args);
        $news->save();

        return $news;
    }
}

DeleteNewsMutation:

namespace App\graphql\Mutations;

use App\Models\News;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Mutation;

class DeleteNewsMutation extends Mutation
{
    protected $attributes = [
        'name' => 'deleteNews',
        'description' => 'Delete a news'
    ];

    public function type(): Type
    {
        return Type::boolean();
    }


    public function args(): array
    {
        return [
            'id' => [
                'name' => 'id',
                'type' => Type::int(),
                'rules' => ['required']
            ]
        ];
    }

    public function resolve($root, $args)
    {
        $news = News::findOrFail($args['id']);

        return  $news->delete() ? true : false;
    }
}

Cũng nhớ là nếu mỗi lần thêm một Schema mới thì phải cập nhật vào config/graphql.php
trong phần cấu hình cho "Schemas"- như phần mô tả ở trên. 

Giờ thì khởi động docker-compose và chúng ta bắt đầu test được rồi. URI thì như config bên trên, nó sẽ là "/graphql".

3. Sử dụng Postman để truy vấn GraphQL

Có rất nhiều công cụ có thể thao tao với GraphQL Server. Có sẵn trong package của Laravel mà chúng ta sử dụng là GraphiQL - là một công cụ UI rất tuyệt vời rồi. Tuy nhiên, ở bài viết này sử dụng Postman (và rất nhiều bài hướng dẫn tiếp theo sẽ sử dụng Postman và Newman chuyên sâu hơn).

Đầu tiên là list tất cả các News:

graphql_list_news


response list all news


Lấy một News theo ID: [1]

Get specific news


Tạo mới một News:

Create News



Cập nhật thông tin News: [26]

Update News



Xoá luôn News vừa tạo:

Delete News

Vậy là chúng ta biết cơ bản về GraphQL nó hoạt động thế nào rồi đúng không? 

Thực tế, sẽ có nhiều thứ phức tạo hơn để áp dụng vào GraphQL. Ví dụ như trong News có thể thêm "Author",  "Comment", "Category" .. , hoặc là muốn list News theo một điều kiện filter nào đó. Lúc đó cũng chỉ cần một query là có thể lấy được hết những thứ chúng ta muốn. Khá là thú vị đúng không :)


Post a Comment

0 Comments