要使用下面的方法,最好熟悉Laravel查询构造器怎么写,否则会一脸懵逼
工作中难免要写非常多的增删改查的逻辑,Laravel的查询构造器写起来已经很舒服了,但是仍然避免不了要写大量的重复代码,比如我们要实现一个最基本的用户模块的管理功能,要写增删改查接口,比如下面这种
- 用户列表接口(分页)
- 用户添加接口可选选项 用户可选的附加属性,例如用户组,权限等
- 用户提交添加接口 // 保存新增的用户
- 用户编辑接口 // 这里需要获取用户的数据,以及附属信息
- 用户提交编辑接口 // 提交用户修改的数据
- 删除用户接口 // 删除某个特定用户
Laravel内置Route::resource
可以直接创建restful风格的接口,直接针对资源增删改查,非常的语义化,但是实际应用的时候呢,会有下面几个问题
- 需要写大量的方法,比如一个增删改查的逻辑,就要实现下面这七个接口
- 通过HTTP动词来区分行为,前端对接起来也麻烦,PUT,POST,.GET,DELETE,PUT,PATCH等
- 不支持批量操作,我要删除一百个用户,难道我要循环发送DELETE请求
https://xxx.com/api/user/{user_id}
吗?RESTful Api没有批量操作的接口
好,先不考虑RESTfulApi风格了,为了工作简单省事儿,多偷点懒,还是用最传统的GET,POST方式来和服务端交互
在这里可以简单的分析一下,我们要写的这七个接口里面,列表接口和删除接口相对来说是单独的,但是 新增页面的数据,提交新增,编辑页面的数据,提交编辑 这四个接口很大程度上是相似的,所以可以单独抽出来封装成方法,下面直接贴代码,下面封装的方法是基于Laravel7.x的,当然6.x也可以用,手动狗头
primaryKey
: 这个主键,默认是ID,如果前端不给你传id
,一定要给你传user_id
,那你可以修改这里的值为user_id
attributes
: 这个字段是我们要处理的属性,比如我只想保存username,password
这两个属性,我可以写['username', 'password']
, 其他参数无法处理,其实跟模型上的fillable
属性一样了,只不过这样写更加灵活一点
beforeSave
: 这个方法用户保存之前的处理,校验参数啦,开启事务啦等等
afterSave
比如用户注册以后我查看他的分销关系树,给他的上级发放佣金啦,等等
loadParams
这个最简单,进入页面的时候我需要知道用户可选的用户组,可选角色是什么,这时候载入
onload
数据库里直接查出来的数据不是前端想要的,我可以放到这个回调里面处理,比如我数据库里用户等级分别存储了1,2,3分别对应,青铜,白银,黄金,那么取出来的时候我可以在这个回调函数里面做处理
public static function simpleEdit(array $options = [])
{
/** @var Model $model Model模型 */
$model = new static;
/** @var array $result 输出给客户端的结果 */
$result = [];
/** @var string $method 当前请求的方法 */
$method = request()->method();
/** @var Request $request */
$request = request();
if (!in_array($method, ['GET', 'POST'])) {
return $model->error("不支持的请求类型");
}
$options = array_merge([
'isEdit' => true,
'primaryKey' => null, //更改主键
'attributes' => null,
'beforeSave' => null, //保存之前要处理的事件
'afterSave' => null, //保存之后要处理的事件
'loadParams' => [], //要显示的参数
'validator' => null, //默认不使用验证
'onload' => null, // 数据载入的时候的回调
], $options);
// 主键
$primaryKey = $options['primaryKey'] ?? $model->getKeyName();
// 主键值
$primaryKeyValue = (int)$request->input($primaryKey, null);
// 编辑状态下如果没有主键值,直接抛出错误
if ($options['isEdit'] && empty($primaryKeyValue)) {
return $model->error('请求参数错误,hint:没有ID');
}
if ($options['isEdit']) {
$model = $model->findOrFail($primaryKeyValue);
}
/**
* POST请求的时候统一处理就可以了,在这之前,model如果是编辑状态会直接找到对应数据,
* 如果新增状态会直接实例模型,准备塞入数据
*/
if ($method == "POST") {
$model = $model->fill($options['attributes'] ?? $request->all());
if ($options['beforeSave'] instanceof Closure) {
$result = $options['beforeSave']($model);
if ($result) return $result;
}
if (!$model->save()) {
return $model->error("保存失败");
}
if ($options['afterSave'] instanceof Closure) {
$result = $options['afterSave']($model);
if ($result) return $result;
}
return [$primaryKey => $model->id];
}
// 编辑的get方法
if ($options['onload'] instanceof Closure) {
$options['onload']($model);
}
$data = $model->toArray();
if (!empty($data)) {
$result['data'] = $data;
}
$result = array_merge($result, $options['loadParams']);
return $result;
}
/**
* 简单编辑
*
* @param array $options
* @return array
* @author Vencenty <yanchengtian0536@163.com>
*/
public static function simpleCreate(array $options = [])
{
$options['isEdit'] = false;
return static::simpleEdit($options);
}
对于GET和POST请求呢,简单做了一下区分
GET只用于获取数据,POST用于提交数据
那么相应的
GET /user/create
是加载用户的相关的关联信息(用户组,权限之类的数据,上文已经提到过了)
POST /user/create
是保存用户当前数据(把用户提交的数据处理之后保存到数据库中)
GET /user/update?id=3
是获取ID为3的用户的所有信息
POST /user/update?id=3
是提交用户ID为3的的编辑请求
这里做一下展示,首先我们用一个方法完成两个接口
public function create(Request $request)
{
return $this->success(
User::simpleCreate([
'loadParams' => [
'groups' => UserGroup::all(),
'roles' => UserRole::all(),
],
// 只保存nickname,mobile属性, attributes填写的属性在 beforeSave方法的闭包参数中都能接收到
'attributes' => $request->only(['nickname', 'mobile', 'group_id']),
'beforeSave' => function (User $user) {
if($user->group_id < 0) {
return $this->error("不合法的组ID");
}
$user->password = bcrypt(md5(uniqid()));
},
'afterSave' => function (User $user) {
Log::create(['content' => "当前时间" . now() . ",用户{$user->nickname}注册成功"]);
}
])
);
}
这个例子中我们使用这几个配置实现了一个简单的用户注册,下面说明
GET /api/user/create
结果如下
{
"error": 0,
"groups": [
{
"id": 1,
"name": "会员"
},
{
"id": 2,
"name": "非会员"
},
{
"id": 3,
"name": "超级会员"
}
],
"roles": [
{
"id": 1,
"name": "管理员"
},
{
"id": 2,
"name": "开发"
},
{
"id": 3,
"name": "运营"
},
{
"id": 4,
"name": "财务"
},
{
"id": 5,
"name": "行政"
},
{
"id": 6,
"name": "产品"
}
]
}
符合预期,我们希望用户进入新增页面的时候可以选择所属的角色,以及groups
下面请求提交新增接口
POST /api/user/create
{
"error": 0,
"id": 51
}
结果符合预期,返回了我们创建最新的一条记录的ID,方便前端跳转或者其他用途
有的接口会非常简单,新增的时候不需要选择乱七八糟的东西,可能就是个表单,上传保存就行了,那么我就可以不添加任何参数,如下所示,这样一个添加接口就写完了,是不是非常简单
return $this->success(User::simpleCreate());
新增的接口说完了,下面来说下编辑接口,也就是
GET
, POST
的/api/user/edit
接口,不同于新增接口的是,每次请求编辑接口的是,每次请求都需要定位资源,需要传递id
以定位资源,代码如下
public function edit(Request $request)
{
return $this->success(
User::simpleEdit([
'loadParams' => [
'groups' => UserGroup::all(),
'roles' => UserRole::all(),
],
'attributes' => $request->all(),
'beforeSave' => function (User $user) {
if ($user->where('nickname', $user->nickname)->exists()) {
return $this->error("昵称已存在,请选择别的昵称吧");
}
},
'afterSave' => function (User $user, User $old) {
Log::create(['content' => "当前时间" . now() . ",用户名称由{$old->nickname}修改为{$user->nickname}"]);
},
// 加载之前我对某些数据进行处理
'onload' => function (User $user) {
$user['group'] = $user->group()->first();
$user->makeHidden(['created_at', 'updated_at']);
$user->mobile = str_replace(substr($user->mobile,3,4),'****', $user->mobile);
}
])
);
}
我们访问接口
GET /api/user/edit?id=51
{
"error": 0,
"data": {
"id": 51,
"nickname": "vencenty",
"mobile": "155****8899",
"password": "$2y$10$z2DRYVnPXz9R8tZIrnZdS.ESId5zMp/JZCEV6yUg0yq15FqWD8aXu",
"is_admin": 0,
"group_id": 2,
"group": {
"id": 2,
"name": "非会员"
}
},
"groups": [
{
"id": 1,
"name": "会员"
},
{
"id": 2,
"name": "非会员"
},
{
"id": 3,
"name": "超级会员"
}
],
"roles": [
{
"id": 1,
"name": "管理员"
},
{
"id": 2,
"name": "开发"
},
{
"id": 3,
"name": "运营"
},
{
"id": 4,
"name": "财务"
},
{
"id": 5,
"name": "行政"
},
{
"id": 6,
"name": "产品"
}
]
}
和 GET /api/user/create
稍微不同的是,多了一个data
字段,保存着我们需要的用户所的实体信息
接下来请求
POST /api/user/edit?id=51
{
"error": -10000,
"message": "昵称已存在,请选择别的昵称吧"
}
验证不通过,所以符合预期,修正一下请求参数
{
"error": 0,
"id": 51
}
好,去数据库看下
是OK的
下面说下删除
/**
* 批量删除接口,支持单个删除
*
* @param array $options
* @return mixed
* @author Vencenty <yanchengtian0536@163.com>
*/
public static function simpleDelete(array $options = [])
{
$request = request();
$model = new static;
if (!$request->isMethod('POST')) {
return $model->error("错误的请求方式");
}
$options = array_merge([
'beforeDelete' => null, // 删除每一项前的回调
'afterDelete' => null, // 删除每一项后的回调
'primaryKey' => null, // 主键,默认id
], $options);
if ($options['beforeDelete'] instanceof Closure) {
static::deleting($options['beforeDelete']);
}
if ($options['afterDelete'] instanceof Closure) {
static::deleted($options['afterDelete']);
}
// 主键
$primaryKey = $options['primaryKey'] ?? $model->getKeyName();
// 强转成Array
$waitDeleteIdCollection = (array)$request->post($primaryKey, null);
if (empty($waitDeleteIdCollection)) {
return $model->error("参数错误, hint:没有{$primaryKey}");
}
$affectRows = static::destroy($waitDeleteIdCollection);
return ['affectRows' => $affectRows];
}
下面例如我要删除3个用户,只需要传递给我一个数组Id即可
结果如下
{
"error": 0,
"affectRows": 3
}
好,接下来是比较复杂的列表接口,下面贴出实现
/**
* 获取列表
*
* @param array $params
* @param array $options
* @return array
* @author Vencenty <yanchengtian0536@163.com>
*/
public static function simpleList(array $params = [], $options = [])
{
/** @var Model $model */
$model = new static;
/** @var Request $request */
$request = request();
$options = array_merge([
'pager' => true, // 是否返回分页
'page' => null, // 页面Key
'pageName' => 'page', // 所在页面名称
'pageSizeName' => 'per_page', // 页面大小名称
'toArray' => false, // callback中的对象转为Array
'callback' => null, // 内容输出之前的回调函数
'sort' => null, // 排序字段
'by' => 'desc', // 排序顺序,默认降序排列
'attachParams' => [], // 添加最外层数组元素
], $options);
// 分页名称是否正确
$pageName = $options['pageName'] ?? 'page';
// 当前是第几页
$currentPage = (int)$request->get($pageName, 1);
// 分页大小,GET参数优先级最高,不设置的话读取模型里面设置
$pageSize = $request->get('per_page') ?? $model->getPerPage();
// 在这以后只要Model执行了Query,就会返回Illuminate\Database\Eloquent\Builder对象,而不是Illuminate\Database\Eloquent\Model
/**
* 请求构造器,如果要构造复杂SQL,使用这个参数,
* 闭包内$this变量指向了 Illuminate\Database\Eloquent\Model 对象,
* 但是Builder必须要有返回值,否则查询不生效
* */
if (isset($params['queryBuilder']) && $params['queryBuilder'] instanceof Closure) {
$model = $params['queryBuilder']->call($model, $request);
unset($params['queryBuilder']);
}
// 处理排序问题
$sort = $request->get('sort', $options['sort']);
$by = $request->get('by', $options['by']);
if (!empty($sort)) {
$model = $model->orderBy($sort, $by);
}
foreach ($params as $key => $param) {
if ($param instanceof Closure) {
$model = $model->{$key}($param);
continue;
}
// 多为数组的形式转为链式调用处理
// 这样处理以后原先 $model->where(type,'=','3')
// 括号里面的参数全部转变为 [type, '=', 3]
if (Arr::isMultipleArray($param)) {
foreach ($param as $condition) {
$model = $model->{$key}(...$condition);
}
continue;
}
// 普通数组形式调用解构赋值以后进行调用
$model = $model->$key(...$param);
}
// 计算总页数
$total = $model->count();
// 计算分页数据
$data = $model->offset($pageSize * ($currentPage - 1))
->limit($pageSize)
->get();
// 是否转为数组形式
$data = $options['toArray'] ? $data->toArray() : $data->all();
// 应用callback函数
if ($options['callback'] instanceof Closure) {
array_walk($data, $options['callback']);
}
// 分页数
$pageTotal = ceil($total / $pageSize);
return array_merge([
'data' => $data,
'per_page' => $pageSize, // 每页记录数
'current_page' => $currentPage, // 当前第几页
'total' => $total, // 一共有多少记录
'page_total' => $pageTotal,
], $options['attachParams']);
}
Laravel有自己的paginate
函数,但是总觉得不够灵活,比如我想往最外层的数组添加数据,就不太好实现,这里重新实现了一下,配置参数说明已经写了注释了,这里提供给前端几个开放的接口
per_page
一个页面多少条数据
page
当前显示第几页
sort
排序字段
by
可选参数asc|desc
这个SimpleList方法有几个点需要特殊说明一下,两个参数,左边的Params
参数可以用来生成各种查询条件,options
参数用于处理查询出来的数据,以及附加参数等等,我们使用Laravel框架原先写法是类似下面这种写法,但是转为数组以后怎么处理呢,这里使用了 ...拆解符
把数组拆解为一个个参数,然后通过
call_user_func_array()自己处理链式调用,所以原生Laravel类似于下面的写法的时候
User::where('id','<', 10)->limit(3)->orderByDesc('created_at')->get()
转为目前SimpleList写法要这样
public function index()
{
return $this->success(User::simpleList([
'where' => ['id', '<', 10],
'limit' => 3,
'orderByDesc' => 'created_at'
]));
}
原先的函数名要转为数组的key,然后参数变成数组,有几个参数就写几个数组元素,这样好像解决了问题,但是链式调用怎么处理,会经常写多Where调用啊,例如这种
User::leftJoin('group', 'group.id', '=', 'user.group_id')
->leftJoin('role', 'role.id', '=', 'user.role_id')
->where('id', '<', 100)
->where('name', 'like', '%张')
->limit(3)
->orderByDesc('created_at')
->get()
那我就只能再解析一层被,如果发现数组为二维数组,再遍历进行解析,所以写法就变成下面这种了,但是对于复杂的SQL,比如嵌套括号的,各种OrderBy的,
但是前段时间被同事吐槽,说这种写法特别难用,希望有能基于原生的写法来写SQL,所以后来又预留了一个接口,查了下资料,发现闭包绑定可以做到,那么又增加了一个buildQuery
配置项, buildQuery参数的闭包的$this
绑定了Illuminate\Database\Eloquent\Model
对象,在这里面可以通过$this
实现链式调用,不了解闭包绑定类的可以搜索一下php Closure::bind Closure::bindto Closure::call
函数的用法,有时间会写一个关于这个的文章,看下下面的代码示例,示例一和示例二效果是一模一样的
// 示例一
User::simpleList([
'leftJoin' => [
['role', 'role.id', '=', 'user.role_id'],
['group', 'group.id', '=', 'user.group_id']
],
'where' => [
['id', '<', 10],
['name', 'like', '%张']
],
'limit' => 3,
'orderByDesc' => 'created_at'
])
// 示例二
User::simpleList([
'buildQuery' => function ($request) {
return $this->leftJoin('group', 'group.id', '=', 'user.group_id')
->leftJoin('role', 'role.id', '=', 'user.role_id')
->where('id', '<', 100)
->where('name', 'like', '%张')
->limit(3)
->orderByDesc('created_at');
},
])
理解了以上几点以后,那么后面的$options
参数就好理解的多了,具体用法都写注释里了,后期会继续补充SimpleList方法,用于封装快速实现搜索功能
Github地址 https://github.com/Vencenty/laravel-enhance/tree/master/src/Traits
里面的
ResourceCURD.php
这个文件
当然代码示例中我使用了 $this->success()这个函数返回结果,用的也是Git这个文件夹下的
JsonResponse.php
这个Trait
要想使用这个方法,可以通过
composer require vencenty/laravel-enhance
然后控制器引入 JsonResponse 这个Trait
模型最好建立一个基类,然后导入 JsonResponse
和ResourceCURD
这个类,就可以使用了
我终于明白程序员为什么这么讨厌写文档了,写代码容易,写文档真是要疯啊,况且我这个也不是文档。给自己个差评!!