基于Laravel框架封装的Api快速增删改查的方法

3 篇文章 0 订阅

要使用下面的方法,最好熟悉Laravel查询构造器怎么写,否则会一脸懵逼

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
模型最好建立一个基类,然后导入 JsonResponseResourceCURD这个类,就可以使用了

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

我终于明白程序员为什么这么讨厌写文档了,写代码容易,写文档真是要疯啊,况且我这个也不是文档。给自己个差评!!

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值