Để 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
GraphQL: A query language for your API
2. Build GraphQL Server sử dụng Laravel
$ composer require rebing/graphql-laravel
$ php artisan vendor:publish --provider="Rebing\GraphQL\GraphQLServiceProvider"
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,
];
// The prefix for routes
'prefix' => 'graphql',
/*
* 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',
// 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'],
],
],
$ php artisan make:model News -m
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');
}
}
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);
}
}
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);
}
}
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',
]
];
}
}
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']);
}
}
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();
}
}
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;
}
}
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;
}
}
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;
}
}
3. Sử dụng Postman để truy vấn GraphQL
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 :)
0 Comments