GraphQL使用——Laravel5+使用GraphQL探索之路——laravel-graphql

首先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:

GraphQL使用——Laravel5+使用GraphQL探索之路——laravel-graphql_第1张图片

Ticket.php:

GraphQL使用——Laravel5+使用GraphQL探索之路——laravel-graphql_第2张图片

下面直接上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:

GraphQL使用——Laravel5+使用GraphQL探索之路——laravel-graphql_第3张图片

GraphQL使用——Laravel5+使用GraphQL探索之路——laravel-graphql_第4张图片

多说一句,配置文件第一项的prefix其实可以修改graphql查询的入口,即路由,建议初学默认,就像直接访问localhost/graphql?query=query+FetchActivities{activities{id,title}}就可以看到查询效果了,当然测试的话最好是借助工具,操作比较简单,可以直接给laravel装

"noh4ck/graphiql": "@dev"的composer包,也可以Google浏览器安装graphql插件,都是同一个工具,具体使用可自行google,上两张图:

获取所有活动及关联的门票:

GraphQL使用——Laravel5+使用GraphQL探索之路——laravel-graphql_第5张图片

根据id获取单个具体活动和关联的所有门票:(注意左边查询格式的区别)

GraphQL使用——Laravel5+使用GraphQL探索之路——laravel-graphql_第6张图片

以上是对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备案一下:

GraphQL使用——Laravel5+使用GraphQL探索之路——laravel-graphql_第7张图片

 

下面就可以在graphiQL测试一下了:

 

创建门票:

GraphQL使用——Laravel5+使用GraphQL探索之路——laravel-graphql_第8张图片

 

更新活动:

GraphQL使用——Laravel5+使用GraphQL探索之路——laravel-graphql_第9张图片

 

删除活动:

GraphQL使用——Laravel5+使用GraphQL探索之路——laravel-graphql_第10张图片

 

然后查询全部活动看一下效果(数据太多,截图不全):

GraphQL使用——Laravel5+使用GraphQL探索之路——laravel-graphql_第11张图片

先到此为止,如果后续有更进阶的使用,会继续补充。

你可能感兴趣的:(GraphQL使用——Laravel5+使用GraphQL探索之路——laravel-graphql)