在開始要學習 GraphQL 前,我們要先稍微瞭解一下 GraphQL 基本資訊, GraphQL 是由 facebook 在 2012 開發,而後在 2015 才釋出的,主要可以用於改善前後端的溝通甚至是不同裝置所需資訊不同的問題,且 GraphQL 由原先的後端定義格式也轉變成了由前端自己去控制他所需要的資料。
以往的 Restful API 如下:
前端只能接受後端傳送過來的格式, 假如需要額外的資料需要再跟後端工程師溝通
GraphQL 變成如下
前端甚至是行動端可以控制自己想要的資訊,而不需要再一直跟後端頻繁的溝通。
了解了 GraphQL 跟 Restful API 之間的差異後,簡單的列一下 GraphQL 出現的原因以及優點。
出現原因:
1. 不同裝置所需的資料數量甚至是格式不同
2. 前後端溝通難度增加
3. 資料傳遞速度影響效能
優點:
1. 程式即文檔
2. 前後端溝通成本降低
3. 接口更為彈性且只取得所需資料
缺點:
1. Cache 難以實作
GraphQL 基本語法
GraphQL 提供5種基本資料型別
Int: 有符號的 32 位元整數
ID: 唯一識別符
String: 字串型別
Boolean: true or false
Float: 有符號的浮點數
而 GraphQL 如何定義一個 schema ?, 定義的方式如下
type User {
id: ID // id 宣告為 ID data type
account: String // 帳號 宣告為 string data type
email: String // 電子信箱 宣告為 string data type
verified: Boolean // 是否已驗證宣告為 boolean data type
amount: Float // 宣告金額為 float data type
}
GraphQL 也有 Enum, 而 enum 的定義方式如下, 以下範例為定義一個性別的 enum
enum Gender {
MAIL // 男性
FEMALE // 女性
}
假如資料有可能為陣列的話則定義方式如下
type UserResponse {
users: [User]
}
以上範例是定義一個 UserResponse 的 schema, 而他的回傳值是 users, 而 users 是一個陣列裡面的資料型別為 User schema (User Schema 可以就是上面定義的), 這邊回傳給前端的值會長得像這樣
{
'data': {
'users': [
{
'id': 1,
'username': 'Hello World',
'email': 'test@gmail.com',
'amount': 0.0,
'verified': false
}
]
}
}
假如資料一定要有值得話則定義方式如下
type User {
id: ID
account: String!
email: String!
}
以上的方式要定義該值不能為空的方式為用驚嘆號
我們來綜合以上得知識,假如我們今天要定義一個陣列且該陣列不得空的話方式如下
type UserResponse {
users: [User]!
}
這樣定義雖說 UserResponse 的 users 不得為 null, 但是以上的定義方式有可能回傳值會是 [null], 這種陣列也不是 null 但是 User 可能為 null, 因此需要在修改為以下
type UserResponse {
users: [User!]!
}
這種定義的方式的話 users 一定會有值且陣列裡面也會有值。
參考資料:
假如 GraphQL 要傳入參數的話,可以用如下的方式進行參數化以防止 GraphQL Injection。
GraphQL Fragment
有時候會取得一些重複性的欄位資料,此時就可以使用 fragment 進行優化減少程式碼的重複性,例如以下情境
query GetPeople{
person1(id: 1) {
firstName
lastName
}
person2(id: 2) {
firstName
lastName
}
person3(id: 3) {
firstName
lastName
}
}
我們 query 三個 person 的資料且都只取出 firstName 以及 lastName, 假如今天又要多撈一個 gender, 你就要一次改三遍了,此時我們可以使用 fragment 更改成如下方式
fragment personFragment on Person {
firstName
lastName
}
先定義出 fragment 名為 personFragment 他只能用於 Person,之後我們的 query 可以改為如下
query GetPeople{
person1(id: 1) {
...personFragment
}
person2(id: 2) {
...personFragment
}
person3(id: 3) {
...personFragment
}
}
這樣當我們要多撈出一個欄位資料的話只要改 personFragment 即可。
GraphQL 進階用法
union:
union 簡單來說就是可以定義多個回傳類型,我們直接來看個範例
query GetAnimal{
animal {
...on dog {
title
}
...on cat {
name
}
}
}
用例子來說明的話就是在取 animal 資料的時候,假如資料類行為 dog 的話則取 title, 為 cat 的話則取 name
interface
interface 就類似於 OOP 裡面的 interface , 在 laravel 則有些人稱為 contract (合約)
interface Post {
title: String
}
type Article implements Post {
title: String
}
type TechnicalPost implements Post {
title: String
}
type Query {
post(id: ID!): Post
}
以上的例子就是定義一個 post interface , 使 article 跟 technical post 都去實作 implement, 這樣 post 回傳值就可以直接寫 post , 因為 article 跟 technical post 都實作了 post interface
然後再取資料的時候跟 union 一樣需使用 … on
了解 GraphQL 基本結構後接著透過 Laravel 實作 GraphQL,首先需要先安裝 grpahql 套件, 如下:
composer require rebing/graphql-laravel
config 產生 graphql 設定檔案 (檔案於 config/graphql.php)
php artisan vendor:publish --provider="Rebing\GraphQL\GraphQLServiceProvider"
安裝完成後在 config/app.php 的 provider 設定
Rebing\GraphQL\GraphQLServiceProvider::class,
設定好後在 app 資料夾新增 GraphQL 資料夾且裡面分別新增 Query, Input, Type 等資料夾, 如下圖
接著我們 Type 資料夾去定義 schema, 這邊範例我們去定義 user schema
<?phpnamespace GraphQL\Type;use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Type as GraphQLType;
use \GraphQL\Type\Definition\Type;class UserType extends GraphQLType
{
protected $attributes = [
'name' => 'User',
'description' => '使用者資料',
'model' => User::class,
];
public function fields(): array
{
return [
'id' => [
'type' => Type::nonNull(Type::id()),
'description' => '用戶編號'
],
'username' => [
'type' => Type::nonNull(Type::string()),
'description' => '用戶名稱'
],
'email' => [
'type' => Type::nonNull(Type::string()),
'description' => '電子信箱'
],
];
}
}
以上範例則定義一個 User Schema , 而 User Schema 有 id, username, email 欄位, 且資料皆不能為 null。所以說假如是定義 Schema 有哪些欄位資訊的話則是定義在 fields method, 且裡面的 type 則是定義資料型別而 description 的部分則是會顯示在 graphql docs 裡面供前端方便理解此欄位的作用。
id' => [
'type' => Type::nonNull(Type::id()), // 欄位資料型別
'description' => '用戶編號' // 欄位說明
],
且看範例有定義 $attributes 這邊也要記得設定所指定的 model 是哪個以及對於此 schema 的描述
protected $attributes = [
'name' => 'User', // schema name 供 query, mutation 調用
'description' => '使用者資料', // schema 說明
'model' => User::class, // schema 去從哪個 model 取得資料
];
定義好 User Schema 之後我們接著去定義要抓取資料的 Query, 在 GraphQL 中抓資料的部分都歸列在 Query, 而對資料的修改則列在 Mutation。
接著我們要撰寫取得使用者資訊的 code, 所以我們在 Query 資料夾下新增 UserQuery, 程式碼如下
<?php
namespace GraphQL\Query;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type as GraphQLType;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Query as GraphQLQuery;
use Rebing\GraphQL\Support\SelectFields;
class UsersQuery extends GraphQLQuery
{
protected $attributes = [
'name' => 'UsersQuery',
'description' => '使用者資料查詢'
];
public function type(): GraphQLType
{
return GraphQL::paginate('User');
}
public function resolve($root, array $args, $context, ResolveInfo $resolveInfo, \Closure $getSelectFields)
{
return User::query()->paginate(10);
}
}
在撰寫 Query 以及 Mutation 的時候記得要定義 type method, type method 是定義回傳值, 以這個範例來說的話則是定義回傳的型別為分頁類型,且裡面的資料為 user shcmea 結構。而 resolve 則是用來定義我們要如何處理此資料, 以這個範例就是直接回傳 user paginator 資料且每頁為 10 筆。
code 都寫完後接著要在 config/graphql.php 設定前面我們所定義的 type 以及 query, 所以我們打開 config/graphql.php 設定檔案。
打開後我們找到 ‘schemas’ 的位置看到這邊有定義 query key, 我們把上面寫的 query 定義於此。
'query' => [
\GraphQL\Query\UsersQuery::class,
]
接著在 schemas 下面會看到 types, 此部分為全域的 data schema, 我們設定 user schema
'types' => [
\GraphQL\Types\UserType::class,
],
定義好之後我們可以透過 insomnia 做 graphql 的測試,insomnia 類似於 postman。我們在 insomnia 新增 graphql request
新增好後將在網址列輸入 http://localhost:8000/graphql (結尾的部分記得輸入 graphql)接著在 GraphQL 輸入以下查詢指令
query GetUsers {
users {
data {
id
username
}
current_page
last_page
}
}
以上指令則是取得 users 裡面的 id 以及 username 等資訊,回傳值如下:
{
"data": {
"users": {
"data": [
{
"id": 1,
"username": "username"
}
],
"current_page": 1,
"last_page": 1
}
}
}
而假如要查閱我們撰寫好的 graphql schema, 可以透過 insomnia 去做查閱, 點擊 Show Documentation 就會看到自己所撰寫的 graphql schema, 假如沒有安裝 insomnia 我們也可以透過 http://localhost:8000/graphiql/default 去查看 default schema 的文件
接著我們來撰寫的註冊的 mutation, 記住 mutation 主要用於新增、更新以及刪除等,簡單說非查詢的都用 mutation , 首先我們在 GraphQL 資料夾下新增 Mutation 資料夾, 且在 Mutation 資料夾新增 RegisterUser
<?php
namespace GraphQL\Mutations;
use GraphQL\Type\Definition\Type as GraphQLType;
use GraphQL\Type\Definition\Type;
use Illuminate\Support\Arr;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Mutation;
class RegisterUser extends Mutation
{
protected $attributes = [
'name' => 'RegisterUser',
'description' => '註冊使用者'
];
public function type(): GraphQLType
{
return GraphQL::type('User');
}
public function args(): array
{
return [
'input' => [
'type' => GraphQL::type('RegisterInput'),
],
];
}
public function resolve($root, array $args)
{
$args = $args['input'];
$args['username'] = $args['account'];
$args['password'] = \Hash::make($args['password']);
$params = Arr::only($args, ['username', 'account', 'password', 'email']);
return User::query()->create($params);
}
}
一樣我們得設定 args, 此參數用於定義此 mutation 可以接受的參數為何,以上面的例子來說他可以接受一個名叫 input 的參數, 而 input 的作用稍後會說明,以及我們也要定義回傳的格式,而回傳的格式定義方式跟 Query 一樣是定義在 type method, 而 resolve method 也跟 Query 一樣主要用於定義要如何處理資料。
在這邊我們先稍微補充一下 input 的說明,我們在使用 GraphQL 後有可能參數會很多,此時你的 args 會定義的很多且也有可能 args 需要共用,此時就出現了 input 這個做法,input 在 laravel 的定義方式如下
<?php
namespace GraphQL\Inputs;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\InputType;
class RegisterInput extends InputType
{
protected $attributes = [
'name' => 'RegisterInput',
'description' => '註冊會員 input'
];
public function fields(): array
{
return [
'account' => [
'type' => Type::nonNull(Type::string()),
'description' => '帳號',
],
'email' => [
'type' => Type::nonNull(Type::string()),
'description' => '電子信箱',
'rules' => ['required', 'email']
],
'password' => [
'name' => 'password',
'type' => Type::nonNull(Type::string()),
'description' => '密碼'
],
'password_confirmation' => [
'name' => 'password_confirmation',
'type' => Type::nonNull(Type::string()),
'description' => '確認密碼'
]
];
}
public function validationErrorMessages(array $args = []): array
{
return [
'email.email' => 'email format is incorrect',
];
}
}
Input 定義的方式也一樣,我們需要定義 fields 也就是此 input 可以接受哪些欄位資料,而我們也可以搭配使用 laravel 的 validation rule 到 input 也可以套用到 query, 要使用 validation rule 的話則定義 rules , 而要客製化錯誤訊息的話則要 override validationErrorMessages method。
都定義完之後接著執行 mutation graphql
// $input 為 變數名稱// RegisterInput 為資料型別
mutation RegisterUser($input: RegisterInput) { // 將 $input 變數的資料 assign 給 input
RegisterUser(input: $input ) {
id
username
}
}
接著定義 query variable
{
"input": {
"account": "demo",
"email": "demo@gmail.com",
"password": "123456",
"password_confirmation": "123456"
}
}
皆定義完成後按下執行按鈕即可以正常註冊成功。
Query 命名方式
我們在設計 query 的時候可能會寫成以下的命名方式例如 getUsers …. 等,
query {
getUsers { ....
} .....
}
但是 graphql 會用比較簡潔的方式去命名且這樣也比較容易看出之間的關聯性
query { users {
..... }
}
Mutation 推薦命名方式
graphql 命名並沒有明確的規範, 但是因為 mutation 通常是用來做某件事情的,因此通常會動詞在前,例如 createUser、deleteUser 等動詞在前的命名方式。 然而也可以像 Shopify 那樣的命名方式則是反過來變成 userCreate、userDelete, 這樣可以快速地得知哪些是 user 相關的功能,不過這命名方式要如何命名也可以透過團隊去自行自訂,這只是比較大宗的命名方式而已。
Mutation 回傳值設計
通常會為各自的 mutation 定義一個 payload, 也就是假如是修改訂單的 mutation, 不要緊單單回傳 order, 不然假如日後也需要多回傳其他資訊的話,又要再更改回傳結構,這邊比較建議這種情形的話可以回傳值為 OrderPayload 之類的
type Mutation {
updateOrder(id: ID!):UpdateOrderPayload
}
Relay Mutation 設計
此設計會對每個 mutation 設定自己的 input, 例如前面的 updateOrder 就會新增一個 updateOrderInput 給 updateOrder 使用。
type Mutation {
updateOrder(id: ID!, input: UpdateOrderInput):UpdateOrderPayload
}
安全性
max_depth:
假如沒有去限制深度的話惡意人士可以透過深層的 query 產生 DDOS
query {
user {
posts {
author {
posts {
// 無限深層
}
}
}
}
}
通常這種攻擊最簡單的避免方式就是限制其深度
max_complexity: 假如有設置 max_depth 的話有心人士還是可以透過
query {
users {
posts(first: 100){}
followedPosts(first: 100) {}
....
}
}
這樣使其沒有超過深度但是每次都抓取很多筆資料來造成 server 的負擔,這時就要設定 max_complexity , complexity 通常都為 1
query {
users { //complexity 1
post { // complexity 1
title // complexity 1
}
followedPosts(first: 100) { // complexity 100
....
}
}
}
以上面的例子來看的話 complexity 總共為 103 , 所以假如我們 complexity 設定為 100 的話此時 query 將會 failed
introspection:
透過 introspection 可以得知到 schema 的一些基本資訊,例如透過:
{
__schema {
types {
name
}
}
}
可以得知到一些 schema 的名稱等資訊, 因此在正式環境的情況下需要關閉 introspection
GraphQL injection
類似 SQL Injection 的攻擊方式,因此建議使用參數化的 Query 來避免 injection
mutation UpdateUser($id: Int){
updateUser(id: $id) {
id
name
age
}
}
假如要客制 error message 的話可以參考以下方式,簡單說就是將 exception 實作 ClientAware interface 並且回傳 true, 假如沒有實作 ClientAware 的話 , 當你丟出 exception 實 message 會顯示 internal error。
use GraphQL\Error\ClientAware;
class MySafeException extends \Exception implements ClientAware
{
public function isClientSafe(): bool
{
return true;
}
}
套件 github:
參考資料: