首先composer安裝擴充包:folklore/graphql(github-most stars),然後最好是不管laravel哪個版本先去config\app.php添加個provider,即添加這樣一句:
Folklore\GraphQL\ServiceProvider::class,
然後再去publish配置
php artisan vendor:publish --provider="Folklore\GraphQL\ServiceProvider"
因為理論上laravel5.5以上版本會自動搞定,但是我之前沒添加provider就publish不完全。
Publish後就能在config\下看到多了一個graphql.php檔案,這個配置檔案不像其他的一次性就能修改好,而是根據項目需要所産生的Query和Type都需要在裡面“備案”一下,是以需要經常修改,下面會講到。
接下來開始寫接口,首先要寫Type設定以某種格式(int,string等内置或根據需要自設)傳回某個model的哪些字段,個人了解這裡的Type類似于DingoAPI的Transformer的作用,具體自行了解。
以活動和門票舉例,活動與門票為一對多關系,在Activity和Ticket的model裡當然要有相應的關聯方法,而且方法命名要合乎邏輯才行,比如這裡:
Activity.php:
Ticket.php:
下面直接上ActivityType和TicketType的代碼,直接在代碼裡注釋了
ActivityType.php
<?php
/**
* Created by Lilei.
* Date: 2018/11/14
* Time: 13:40
* Introduction:
*/
namespace App\GraphQL\Type;
use App\Models\Activity;
use Folklore\GraphQL\Support\Facades\GraphQL;
use GraphQL\Type\Definition\Type;
use Folklore\GraphQL\Support\Type as GraphQLType;
class ActivityType extends GraphQLType
{
protected $attributes = [
'name' => 'activity', // 該處設定要準确有意義,配置檔案中備案時要用到
'description' => '活動',
'model' => Activity::class
];
/**
* 定義傳回的字段接口,即可以通過你寫的接口擷取到該model哪些字段
* @return array
*/
public function fields()
{
return [
'id' => [
'type' => Type::nonNull(Type::int()), // nonNull是不能為空的意思,即該字段值為空時會查詢失敗
'description' => '活動id'
],
'title' => [
'type' => Type::string(),
'description' => '活動主題'
],
'description' => [
'type' => Type::string(),
'description' => '活動簡介'
],
'address' => [
'type' => Type::string(),
'description' => '活動舉辦位址'
],
'holding_on' => [
'type' => Type::string(),
'description' => '活動舉辦時間'
],
'started_at' => [
'type' => Type::string(),
'description' => '活動報名開始時間'
],
'ended_at' => [
'type' => Type::string(),
'description' => '活動報名結束時間'
],
'created_at' => [
'type' => Type::string(),
'description' => '建立時間'
],
'updated_at' => [
'type' => Type::string(),
'description' => '更新時間'
],
'tickets' => [
'type' => Type::listOf(GraphQL::type('ticket')), // 此處即是一對多的關聯設定,listOf方法傳回關聯的所有資料
'description' => '活動的門票類型'
]
];
}
// 因為laravel自動儲存的建立、更新、軟删除時間字段為Carbon對象,而上面隻能設定
//傳回字元串,是以要在下面對不符合指定類型的字段
// 手動做一下處理,方法的命名嚴格按照下面格式來
/**
* @description 單獨處理created_at字段為string格式
*
* @author Lilei
*/
protected function resolveCreatedAtField($root, $args)
{
return $root->created_at . '';
}
/**
* @description 單獨處理updated_at字段為string格式
*
* @author Lilei
*/
protected function resolveUpdatedAtField($root, $args)
{
return $root->updated_at . '';
}
}
TicketType.php
<?php
/**
* Created by Lilei.
* Date: 2018/11/14
* Time: 13:41
* Introduction:
*/
namespace App\GraphQL\Type;
use App\Models\Ticket;
use GraphQL\Type\Definition\Type;
use Folklore\GraphQL\Support\Type as GraphQLType;
class TicketType extends GraphQLType
{
protected $attributes = [
'name' => 'Ticket',
'description' => '',
'model' => Ticket::class
];
/**
* 定義傳回的字段接口
* @return array
*/
public function fields()
{
return [
'id' => [
'type' => Type::nonNull(Type::int()),
'description' => '門票id'
],
'name' => [
'type' => Type::string(),
'description' => '門票名字'
],
'price' => [
'type' => Type::string(),
'description' => '門票價格'
],
'total_num' => [
'type' => Type::string(),
'description' => '門票總數'
],
'remain_num' => [
'type' => Type::string(),
'description' => '門票剩餘數量'
],
'description' => [
'type' => Type::string(),
'description' => '門票說明'
],
'activity_id' => [
'type' => Type::string(),
'description' => '所屬活動id'
],
'started_at' => [
'type' => Type::string(),
'description' => '門票開售時間'
],
'ended_at' => [
'type' => Type::string(),
'description' => '門票售賣截止時間'
],
'created_at' => [
'type' => Type::string(),
'description' => '建立時間'
],
'updated_at' => [
'type' => Type::string(),
'description' => '更新時間'
]
];
}
/**
* @description 單獨處理created_at字段為string格式
*
* @author Lilei
*/
protected function resolveCreatedAtField($root, $args)
{
return $root->created_at . '';
}
/**
* @description 單獨處理updated_at字段為string格式
*
* @author Lilei
*/
protected function resolveUpdatedAtField($root, $args)
{
return $root->updated_at . '';
}
}
Type主要設定某model可以被擷取哪些字段,而Query相當于查詢的入口,可以設定要擷取哪個model的資料,而且可以自定義查詢限定條件,比如limit,id=1等等幾乎sql能用的查詢條件,下面還是直接上擷取活動表和關聯的門票表的所有資料的代碼:
ActivitiesQuery.php
<?php
/**
* Created by Lilei.
* Date: 2018/11/14
* Time: 13:41
* Introduction:
*/
namespace App\GraphQL\Query;
use App\Models\Activity;
use Folklore\GraphQL\Support\Facades\GraphQL;
use Folklore\GraphQL\Support\Query;
use GraphQL\Type\Definition\Type;
class ActivitiesQuery extends Query
{
// 給該查詢做命名标記,擷取所有為複數,擷取某個的詳情為單數(不是強制)
protected $attributes = [
'name' => 'activities'
];
// 設定擷取類型
public function type()
{
return Type::listOf(GraphQL::type('activity')); // 擷取多個活動集合
// return GraphQL::type('activity'); // 擷取單個活動
}
// 定義可選篩選條件
public function args()
{
return [
'id' => ['name' => 'id', 'type' => Type::int()],
'title' => ['name' => 'title', 'type' => Type::string()],
'description' => ['name' => 'description', 'type' => Type::string()],
'limit' => ['name' => 'limit', 'type' => Type::int()],
];
}
// 處理篩選條件的相應傳回結果
public function resolve($root, $args)
{
if (isset($args['limit'])) {
return Activity::limit($args['limit'])->get();
}
if (isset($args['id'])) {
// return Activity::where('id' , $args['id'])->first();
return Activity::where('id' , $args['id'])->get();
}
if (isset($args['title'])) {
// return Activity::where('title', $args['title'])->first();
return Activity::where('title', $args['title'])->get();
}
if (isset($args['description'])) {
// return Activity::where('description', $args['description'])->first();
return Activity::where('description', $args['description'])->get();
}
return Activity::all();
}
}
(要寫TicketQuery.php的話模仿上面即可)
上述代碼其實同時包含了擷取所有活動和擷取單個活動的寫法,當然要記得擷取單個活動時修改檔案名和查詢名為單數。
這裡遇到的坑:嘗試擷取單個活動詳情時死活擷取不到,查了一圈資料,轉回來發現當要查詢一個活動時,肯定要有限制條件的,一般就是id=?了,代碼中的Model::where()->get()的寫法是網上資料的大多數寫法,當然get()方法在laravel中也很常用,但是它傳回的是一個Eloquent對象的集合(盡管限制id時也要傳回集合),而擷取單個時,type()方法已經改為傳回一個,相應的它也隻需要一個Eloquent對象而不是集合,是以我就在坑裡爬不上來了,還好誤打誤撞發現了這裡,不然就真的出不去了,是以将get()方法改為first()或find(id)方法即可。
Type和Query寫完,不能急着運作,要記得給他們備案,直接上圖
config\graphql.php:
多說一句,配置檔案第一項的prefix其實可以修改graphql查詢的入口,即路由,建議初學預設,就像直接通路localhost/graphql?query=query+FetchActivities{activities{id,title}}就可以看到查詢效果了,當然測試的話最好是借助工具,操作比較簡單,可以直接給laravel裝
"noh4ck/graphiql": "@dev"的composer包,也可以Google浏覽器安裝graphql插件,都是同一個工具,具體使用可自行google,上兩張圖:
擷取所有活動及關聯的門票:
根據id擷取單個具體活動和關聯的所有門票:(注意左邊查詢格式的差別)
以上是對laravel使用graphql查詢資料的簡單使用探索,後面探索修改資料後會繼續補充。
———————————————分割線———————————————
下面就接着記錄laravel使用graphql建立、修改、删除資料的簡單使用探索。
前面查詢對應的接口叫Query,這裡建立和修改還有删除都涉及對資料庫的寫操作,是以對應的接口叫Mutation。
還是之前的活動和門票案例,不用多解釋,看代碼應該就能明白。
這裡先說個前提,使用Mutation去建立一條新紀錄時必須保證對應的model裡面聲明了$fillable屬性數組,否則建立時會報錯,更新和删除至少我測試時沒有強制這一點。
代碼格式和之前的Query差不多,就不多解釋了,主要是resolve不同。
建立門票App\GraphQL\Mutation\CreateTicketMutation.php
<?php
namespace App\GraphQL\Mutation;
use App\Models\Ticket;
use Folklore\GraphQL\Support\Facades\GraphQL;
use Folklore\GraphQL\Support\Mutation;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
class CreateTicketMutation extends Mutation
{
protected $attributes = [
'name' => 'createTicket',
'description' => 'The mutation to create a ticket'
];
public function type()
{
return GraphQL::type('ticket');
}
public function args()
{
return [
'name' => [
'name' => 'name',
'type' => Type::nonNull(Type::string()),
'rules' => ['required'] // 參數校驗
],
'price' => [
'name' => 'price',
'type' => Type::nonNull(Type::float()),
'rules' => ['required']
],
'total_num' => [
'name' => 'total_num',
'type' => Type::nonNull(Type::int()),
'rules' => ['required']
],
'remain_num' => [
'name' => 'remain_num',
'type' => Type::nonNull(Type::int()),
'rules' => ['required']
],
'description' => [
'name' => 'description',
'type' => Type::nonNull(Type::string()),
'rules' => ['required']
],
'activity_id' => [
'name' => 'activity_id',
'type' => Type::nonNull(Type::int()),
'rules' => ['required']
],
'started_at' => [
'name' => 'started_at',
'type' => Type::nonNull(Type::string()),
'rules' => ['required']
],
'ended_at' => [
'name' => 'ended_at',
'type' => Type::nonNull(Type::string()),
'rules' => ['required']
],
];
}
public function resolve($root, $args, $context, ResolveInfo $info)
{
$ticket = Ticket::create($args);
// 下面是文檔中的寫法,不知道是不是多對多時得這樣寫,但現在多對一好像用不到,直接按上面簡寫了,最後傳回剛建立的這條記錄;
// $ticket = new Ticket($args);
// $activity = Activity::find($args['activity_id']);
// if (!$activity) return null;
// $activity->tickets()->save($ticket);
return $ticket;
}
}
更新活動App\GraphQL\Mutation\UpdateActivityMutation.php
<?php
namespace App\GraphQL\Mutation;
use App\Models\Activity;
use Folklore\GraphQL\Support\Facades\GraphQL;
use Folklore\GraphQL\Support\Mutation;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
class UpdateActivityMutation extends Mutation
{
protected $attributes = [
'name' => 'updateActivity',
'description' => 'update activity'
];
public function type()
{
return GraphQL::type('activity');
}
public function args()
{
return [
'id' => [
'name' => 'id',
'type' => Type::nonNull(Type::int()),
'rules' => ['required'] // 參數校驗方法之二
],
'title' => [
'name' => 'title',
'type' => Type::nonNull(Type::string()),
'rules' => ['required']
]
];
}
// 參數校驗方法之一
// public function rules()
// {
// return [
// 'id' => ['required'],
// 'title' => ['required', 'email']
// ];
// }
public function resolve($root, $args, $context, ResolveInfo $info)
{
$activity = Activity::find($args['id']);
if (!$activity) {
return null;
}
$activity->title = $args['title'];
$activity->save();
// 這裡如果需要更新的字段較多時,也可以直接Activity::update($args);
// 不過要事先把$args['id']從數組中删除,因為id一般是自增的
// 我想這樣的話model應該就得強制聲明$fillable屬性數組了吧,有興趣可以測試一下
return $activity;
}
}
删除門票:App\GraphQL\Mutation\deleteTicketMutation
<?php
namespace App\GraphQL\Mutation;
use App\Models\Ticket;
use Folklore\GraphQL\Support\Mutation;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class DeleteTicketMutation extends Mutation
{
protected $attributes = [
'name' => 'deleteTicket',
'description' => 'The mutation to delete a ticket'
];
public function type()
{
return Type::string(); // 注意此處的不同,删除操作會傳回影響記錄的行數,是以不能再傳回一個Type
}
public function args()
{
return [
'id' => [
'name' => 'id',
'type' => Type::nonNull(Type::int()),
'rules' => ['required']
],
'name' => [
'name' => 'name',
'type' => Type::string(),
],
'price' => [
'name' => 'price',
'type' => Type::float(),
],
'total_num' => [
'name' => 'total_num',
'type' => Type::int(),
],
'remain_num' => [
'name' => 'remain_num',
'type' => Type::int(),
],
'description' => [
'name' => 'description',
'type' => Type::string(),
],
'started_at' => [
'name' => 'started_at',
'type' => Type::string(),
],
'ended_at' => [
'name' => 'ended_at',
'type' => Type::string(),
],
];
}
public function resolve($root, $args, $context, ResolveInfo $info)
{
if (isset($args['id'])) {
return Ticket::destroy($args['id']);
} else {
throw new BadRequestHttpException("删除失敗");
}
}
}
以上代碼中涉及了兩種參數驗證的方法,都差不多,完全看興趣選擇即可
寫完後當然不要忘記把Mutation去config\graphql.php備案一下:
下面就可以在graphiQL測試一下了:
建立門票:
更新活動:
删除活動:
然後查詢全部活動看一下效果(資料太多,截圖不全):
先到此為止,如果後續有更進階的使用,會繼續補充。