首先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
'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
'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
'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
'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
'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
'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测试一下了:
创建门票:
更新活动:
删除活动:
然后查询全部活动看一下效果(数据太多,截图不全):
先到此为止,如果后续有更进阶的使用,会继续补充。