Laravel9 API开发,分三层,处理不同的业务

我的项目开发中把程序分为3层,重点是这3层都是用代码生成器直接生成的
1、控制层 controller 处理接口命名、入参检验,不处理逻辑,检验都放在一起多方便啊
2、服务层 service 处理业务逻辑、判断、对比,简单的数据库操作
3、模型层 Model 处理数据库相关、出参控制、复杂的数据库操作

  • controller层 称为接口服务层,负责对客户端的请求进行响应,处理接收客户端传递的参数,进行高层决策并对领域业务层进行调度,最后将处理结果返回给客户端。

  • service层 称为领域业务层,负责对领域业务的规则处理,重点关注对数据的逻辑处理、转换和加工,封装并体现特定领域业务的规则。

  • Model层 称为数据模型层,负责技术层面上对数据信息的提取、存储、更新和删除等操作,数据可来自内存,也可以来自持久化存储媒介,甚至可以是来自外部第三方系统。

与其他写的不同的是分了三层,控制层有getRules入参,模型层有getRules列表出参
借鉴JAVA的入参和出参分开的规则,入参控制校验和SWAGGER生成,出参控制列表输出,顺便生成SWAGGER 文件,说实在的,SWAGGER的注释太不是东西了,如果入参和出参多都要写的话,那是很遭罪的,所以我把入参的字段名称、描述、规则,放在控制层的getRules,我把出参的字段名称、描述,放在模型层的getRules,自己生成SWAGGER api-docs.json 文件

一、控制层:

<?php
namespace App\Http\Controllers;

use App\Services\DepartmentsService;
use App\Traits\ApiResponse;
use Illuminate\Http\Request;

/**
 * Departments 接口控制器
 * @desc 部门模块
 */
class DepartmentsController extends Controller
{
    use ApiResponse;

    protected $service;

    public function __construct()
    {
        $this->service = new DepartmentsService();
    }

    // 接口参数规则
    public function getRules()
    {
        return [
            'store' => [
                'parentid' => ['name' => 'parentid', 'type' => 'integer', 'required' => true, 'desc' => '父ID'],
                'title' => ['name' => 'title', 'type' => 'string', 'required' => true, 'desc' => '名称'],
                'subtitle' => ['name' => 'subtitle', 'type' => 'string', 'required' => false, 'desc' => '短标|英标|副标'],
                'userid' => ['name' => 'userid', 'type' => 'string', 'required' => true, 'default' => 0, 'desc' => '管理者ID'],
            ],
            'destroy' => [
                'id' => ['name' => 'id', 'type' => 'integer', 'required' => true, 'desc' => '主键ID'],
            ],
            'show' => [
                'id' => ['name' => 'id', 'type' => 'integer', 'required' => true, 'desc' => '主键ID'],
            ],
            'update' => [
                'id' => ['name' => 'id', 'type' => 'integer', 'required' => true, 'desc' => '主键ID'],
                'parentid' => ['name' => 'parentid', 'type' => 'integer', 'required' => false, 'desc' => '父ID'],
                'title' => ['name' => 'title', 'type' => 'string', 'required' => false, 'desc' => '名称'],
                'subtitle' => ['name' => 'subtitle', 'type' => 'string', 'required' => false, 'desc' => '短标|英标|副标'],
                'userid' => ['name' => 'userid', 'type' => 'string', 'required' => true, 'default' => 0, 'desc' => '管理者ID'],
            ],
            'list' => [
                'parentid' => ['name' => 'parentid', 'type' => 'integer', 'required' => true, 'default' => 0, 'description' => '检索父ID:数字'],
            ],
        ];
    }

    /**
     * Departments:新增单条数据
     * @desc 权限:通用
     * @return int 新增ID
     * @method POST
     */
    public function store(Request $request)
    {
        $fields = verifyInputParams($request, $this->getRules()['store']);
        $result = $this->service->store($fields);
        return $this->formatUniteResult($result);
    }

    /**
     * Departments:删除单条数据
     * @desc 权限:分配
     * @return int 影响行数
     * @method POST
     */
    public function destroy(Request $request)
    {
        $fields = verifyInputParams($request, $this->getRules()['destroy']);
        $result = $this->service->destroy($fields);
        return $this->formatUniteResult($result);
    }

    /**
     * Departments:读取单条数据
     * @desc 权限:通用
     * @return array DepartmentsModel@show
     * @method GET
     */
    public function show(Request $request)
    {
        $fields = verifyInputParams($request, $this->getRules()['show']);
        $result = $this->service->show($fields);
        return $this->formatUniteResult($result);
    }

    /**
     * Departments:更新单条数据
     * @desc 权限:通用,修改一行多字段值
     * @return int 影响行数
     * @method POST
     */
    public function update(Request $request)
    {
        $fields = verifyInputParams($request, $this->getRules()['update']);
        $result = $this->service->update($fields);
        return $this->formatUniteResult($result);
    }

    /**
     * Departments:列表记录数据
     * @desc 权限:通用,多用于下拉数据, 集合:不分页
     * @return array DepartmentsModel@list
     * @method GET
     */
    public function list(Request $request)
    {
        $fields = verifyInputParams($request, $this->getRules()['list']);
        $result = $this->service->list($fields);
        return $this->formatUniteResult($result);
    }
}

说明下:
getRules方法是每个接口的入参,由代码生成器生成

verifyInputParams 检验函数, 我放在app\Helpers\function.php里,关联自定义的函数可以查看Laravel9 常用自定义函数部署应用

$this->formatUniteResult 参考Laravel9 API开发统一格式化数据

use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;

/**
 * 过滤传入参数
 * @param $request 表单提交字段
 * @param $rules 接口允许字段
 */
function filterInputParams($request, $rules)
{
    $params = [];
    $key = array_keys($rules);
    foreach ($key as $k => $v) {
        if ($request->has($v)) {
            $params = array_merge($params, [$v => $request->input($v)]);
        }
    }
    return $params;
}

/**
 * 验证传入参数
 * @param $request 表单提交字段
 * @param $rules  接口允许字段
 */
function verifyInputParams(object $request, $rules)
{
    $filter = [];
    foreach ($rules as $k => $v) {
        $result = [];
        // 有默认值和没有传参
        !isset($v['require']) ?: $v['required'] = $v['require'];
        if ($v['required'] == true) {
            isset($v['default']) && !isset($request[$v['name']]) ? $request[$v['name']] = $v['default'] : '';
            $result[] = 'required';
            ($v['type'] == 'int') ? $v['type'] = 'integer' : '';
            isset($v['type']) ? $result[] = $v['type'] : $result[] = 'string';
            isset($v['max']) ? $result[] = 'max:' . $v['max'] : '';
            isset($v['min']) ? $result[] = 'min:' . $v['min'] : '';
        } else {
            if (!isset($request[$v['name']])) { //没有传入
                if (isset($v['default'])) {
                    $request[$v['name']] = $v['default'];
                } else {
                    unset($request[$v['name']]);
                }
            }
        }
        $filter[$v['name']] = $result;
    }
    // 过滤不在规则内参数,必填有默认值除外
    $params = filterInputParams($request, $filter);
    // 验证参数规则
    $validator = Validator::make($params, $filter);
    if ($validator->fails()) {
        throw new \App\Exceptions\BusinessException(422, $validator->errors());
    }
    return $params;
}

throw new \App\Exceptions\BusinessException(422, $validator->errors());
参考Laravel9 自定义异常处理

当调用接口的时候,
校验入参:verifyInputParams
调用逻辑处理:$result = $this->service->list($fields);
统一输出:return $this->formatUniteResult($result);

二、业务层:

<?php
namespace App\Services;
use App\Models\Departments;
/**
 * Departments 业务逻辑
 */
class DepartmentsService
{
    protected $model;

    public function __construct()
    {
        $this->model = new Departments();
    }

    // 新增数据 Model层 $fillable要加上
    public function store($forms = [])
    {
        return $this->model::insertGetId($forms); // ID
    }

    // 删除数据 Model层 $fillable要加上delete_time | deleted_at
    public function destroy($forms = [])
    {
        $where = ['id' => $forms['id']];
        $param = ['delete_time' => time()];
        return $this->model->where($where)->update($param); // 软删除
        // return $this->model::destroy($where); // 直删除
    }

    // 读取数据
    public function show($forms = [])
    {
        $data = $this->model::findOrFail($forms['id']); // 用于ID条件
        return listArrayAllowKeys($data, $this->model->getRules()['show']);
    }

    // 更新多列单条 Model层 $fillable要存在对应的字段
    public function update($forms = [])
    {
        $where = ['id' => $forms['id']];
        return $this->model::where($where)->update($forms); // 用于多条件 // id不存在,return 0;
    }

    // 列表数据
    public function list($forms = [])
    {
        $fields = $forms;
        unset($fields['parentid']);
        $result = $this->model->list($fields);
        return listArrayAllowKeys($result, $this->model->getRules()['list']);

    }
}

listArrayAllowKeys 函数 格式话输出参数

/**
 * 获取数组中需要的键
 * @param array $array 待处理的数组,可以是一维数组或二维数组
 * @param string|array $allowKeys 待需要的键,字符串时使用英文逗号分割
 * @return array 排除key后的新数组
 */
function listArrayAllowKeys($array, $allowKeys)
{
    if (!is_array($array) && !is_object($array)) {
        throw new \App\Exceptions\BusinessException(400, '检查数据格式:JSON|ARRAY');
    }
    $is_items = false;
    $allArray = json_decode(json_encode($array), true);
    $is_data = array_key_exists('data', $allArray);
    $array = ($is_data) ? $allArray['data'] : $allArray;
    $allowKeys = array_keys($allowKeys);
    //$excludeKeys = is_array($excludeKeys) ? $excludeKeys : explode(',', $excludeKeys);
    foreach ($array as $key => $value) {
        if (is_array($value)) {
            foreach ($array[$key] as $subKey => $subValue) {
                if (!in_array($subKey, $allowKeys, true)) {
                    unset($array[$key][$subKey]);
                }
            }
        } else if (!in_array($key, $allowKeys, true)) {
            unset($array[$key]);
        }
    }
    if ($is_data) {
        $allArray['data'] = $array;
        return $allArray;
    }
    return $array;
}

三、模型层

<?php
namespace App\Models;

use Eloquence\Behaviours\CamelCasing; //把 a_b 变成 aB
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;

/**
 * Departments 数据模型
 */
class Departments extends Model
{
    use CamelCasing;
    protected $table = 'departments';
    protected $fillable = ['parentid', 'title', 'subtitle', 'userid', 'organizationid', 'sort', 'status', 'delete_time']; // 增改时允许赋值字段(白名单)
    protected $guarded = ['id']; // 增改时禁止赋值字段(黑名单)
    protected $hidden = []; // 列表时隐藏的字段
    protected $appends = []; // 需要追加的字段
    public $timestamps = false;

    // 列表数据
    public function list($forms = [])
    {
        // ** 传入参数处理 **
        $parentid = isset($forms['parentid']) ? $forms['parentid'] : '';
        // ** 执行SQL语句 **
        $query = self::query();
        $query->select($this->table . '.*');
        $query->where($this->table . '.delete_time', 0);
        $query->when($parentid, function ($query) use ($parentid) {
            $query->where(function ($query) use ($parentid) {
                $query->Where($this->table . '.parentid', $parentid);
                $query->orWhere($this->table . '.id', $parentid);
            });
        });
        $query->Orderby($this->table . '.sort');
        return $query->get();
    }

    // 接口参数规则
    public function getRules()
    {
        return [
            'show' => [
                'id' => ['type' => 'integer', 'description' => '主健ID'],
                'parentid' => ['type' => 'integer', 'description' => '父ID'],
                'title' => ['type' => 'string', 'description' => '名称'],
                'subtitle' => ['type' => 'string', 'description' => '短标|英标|副标'],
                'userid' => ['type' => 'string', 'description' => '管理者ID'],
                'organizationid' => ['type' => 'integer', 'description' => '组织ID'],
                'sort' => ['type' => 'integer', 'description' => '排序'],
                'status' => ['type' => 'integer', 'description' => '状态'],
                'deleteTime' => ['type' => 'integer', 'description' => '删除时间'],

            ],
            'list' => [
                'id' => ['type' => 'integer', 'description' => '主健ID'],
                'parentid' => ['type' => 'integer', 'description' => '父ID'],
                'title' => ['type' => 'string', 'description' => '名称'],
                'subtitle' => ['type' => 'string', 'description' => '短标|英标|副标'],
                'userid' => ['type' => 'string', 'description' => '管理者ID'],
                'organizationid' => ['type' => 'integer', 'description' => '组织ID'],
                'sort' => ['type' => 'integer', 'description' => '排序'],
                'status' => ['type' => 'integer', 'description' => '状态'],
                'deleteTime' => ['type' => 'integer', 'description' => '删除时间'],
                /**新增**/
                'level' => ['type' => 'integer', 'description' => '层级'],
                'master' => ['type' => 'integer', 'description' => '管理者'],
            ],
        ];
    }
}

getRules 在SERVICE层调用
$this->model->getRules()[‘show’] 单条的字段控制输出字段
$this->model->getRules()[‘list’] 列表的字段控制输出字段

读取文件类、方法、注释、参数的代码

<?php

namespace App\Traits;

use Str;

trait Swagger
{
    //const API_CATE_TYPE_API_CLASS_NAME = 0; // 按API类名分类
    //const API_CATE_TYPE_API_CLASS_TITLE = 1; // 按接口模块名称分类
    //const API_LIST_SORT_BY_API_NAME = 0; // 接口列表,根据接口名称排序
    //const API_LIST_SORT_BY_API_TITLE = 1; // 接口列表,根据接口标题排序

    /**
     * @var int $apiCateType 接口分类的方式
     */
    protected $apiCateType;

    /**
     * @var int $apiListSortBy 接口列表的排序方式
     */
    //protected $apiListSortBy;

    public function swaggerJson()
    {
        $rootPath = base_path();
        defined('D_S') || define('D_S', DIRECTORY_SEPARATOR);

        //App\Http\Controllers 及往下目录
        $psr = ['App\Http\Controllers'];
        foreach (glob(app_path('Http\Controllers') . '/*', GLOB_ONLYDIR) as $dirName) {
            $name = pathinfo($dirName, PATHINFO_FILENAME);
            $psr[] = 'App\Http\Controllers' . D_S . $name;
        }

        // 按照Api.php的类排除的隐藏和默认的方法
        $allPhalApiApiMethods = get_class_methods('\\App\\Http\\Controllers\\Api');
        $allApiS = [];
        $allModel = [];
        $allResource = [];

        // 扫描接口文件
        // $srcPath = "App\\Http\\Controllers"
        foreach ($psr as $namespace => $srcPath) {
            foreach (glob(app_path(str_replace('App\\', '', $srcPath)) . '/*.php') as $dirName) {
                $apiFileName = str_replace('.php', '', str_replace(app_path(str_replace('App\\', '', $srcPath)) . '/', '', $dirName));
                $apiClassName = $srcPath . '\\' . $apiFileName;
                if (!class_exists($apiClassName) || $apiFileName == 'Controller' || $apiFileName == 'Api' || $apiFileName == 'Swagger') {
                    continue;
                }
                $ref = new \ReflectionClass($apiClassName);
                $title = "//请检测接口服务注释($apiClassName)";
                $desc = '-';
                $isClassIgnore = false; // 是否屏蔽此接口类
                $docComment = $ref->getDocComment();
                if ($docComment !== false) {
                    $docCommentArr = explode("\n", $docComment);
                    $comment = trim($docCommentArr[1]);
                    $title = trim(substr($comment, strpos($comment, '*') + 1));
                    foreach ($docCommentArr as $comment) {
                        $pos = stripos($comment, '@desc');
                        if ($pos !== false) {
                            $desc = trim(substr($comment, $pos + 5));
                        }
                        if (stripos($comment, '@ignore') !== false) {
                            $isClassIgnore = true;
                        }
                    }
                }
                if ($isClassIgnore) {
                    continue;
                }
                $apiCateVal = $this->apiCateType == 1 ? $title : $apiFileName;
                if (!isset($allApiS[$srcPath][$apiCateVal])) {
                    $allApiS[$srcPath][$apiCateVal] = array('methods' => array());
                }
                $allApiS[$srcPath][$apiCateVal]['class'] = $apiClassName;
                $allApiS[$srcPath][$apiCateVal]['summary'] = $title;
                $allApiS[$srcPath][$apiCateVal]['description'] = $desc;
                $method = array_diff(get_class_methods($apiClassName), $allPhalApiApiMethods);

                //Set Swagger
                $rulesObject = new $apiClassName;
                $rulesArray = $rulesObject->getRules();
                //Set Swagger

                foreach ($method as $mValue) {
                    $rMethod = new \Reflectionmethod($apiClassName, $mValue);
                    if (!$rMethod->isPublic() || strpos($mValue, '__') === 0 || $mValue == 'getRules') {
                        continue;
                    }

                    $title = '//请检测函数注释';
                    $desc = '//请使用@desc注释';
                    $methods = 'GET/POST';
                    $isMethodIgnore = false;
                    $docComment = $rMethod->getDocComment();
                    if ($docComment !== false) {
                        $docCommentArr = explode("\n", $docComment);
                        $comment = trim($docCommentArr[1]);
                        $title = trim(substr($comment, strpos($comment, '*') + 1));
                        foreach ($docCommentArr as $comment) {
                            $pos = stripos($comment, '@desc');
                            if ($pos !== false) {
                                $desc = trim(substr($comment, $pos + 5));
                            }
                            if (stripos($comment, '@ignore') !== false) {
                                $isMethodIgnore = true;
                            }
                            $pos = stripos($comment, '@return');
                            if ($pos !== false) {
                                $back = trim(substr($comment, $pos + 8));
                            }
                            $pos = stripos($comment, '@method');
                            if ($pos !== false) {
                                $methods = trim(substr($comment, $pos + 8));
                                continue;
                            }

                        }
                    }
                    if ($isMethodIgnore) {
                        continue;
                    }

                    $resourcesName = str_replace('Controller', '', $apiFileName);
                    $apiResourceName = '\App\\Models\\' . $resourcesName;
                    if (class_exists($apiResourceName)) {
                        $resourceObject = new $apiResourceName;
                        if (method_exists($apiResourceName, 'getRules')) {
                            $getRulesKeys = array_keys($resourceObject->getRules());
                            foreach ($getRulesKeys as $key => $value) {
                                $allResource[$resourcesName . 'Model@' . Str::snake($value)]['type'] = 'object';
                                $allResource[$resourcesName . 'Model@' . Str::snake($value)]['properties'] = $resourceObject->getRules()[$value];
                                $allResource[$resourcesName . 'Model@' . Str::snake($value)]['title'] = $resourcesName . "Model@" . $value;
                            }
                        }
                    }


                    $service = trim($srcPath, '\\') . '\\' . $apiFileName . '@' . $mValue;
                    $parameters = isset($rulesArray[$mValue]) ? $this->handleRules($rulesArray[$mValue]) : []; //set Swagger
                    $allApiS[$srcPath][$apiCateVal]['methods'][$mValue] = array(
                        'actions' => $service,
                        'summary' => $title,
                        'description' => trim($desc),
                        'methods' => $methods,
                        'parameters' => $parameters,
                        'name' => $mValue,
                        'back' => $back,
                    );
                }
            }
        }
        return ['api' => $allApiS, 'model' => $allModel, 'resource' => $allResource];
    }

    public function handleRules($array = [])
    {
        $result = [];
        foreach ($array as $key => $value) {
            !isset($value['desc']) ?: $value['description'] = $value['desc']; // 兼容 PHALAPI
            !isset($value['require']) ?: $value['required'] = $value['require']; // 兼容 PHALAPI
            ($value['type'] == 'int') ? $value['type'] = 'integer' : ''; // 兼容 PHALAPI

            $value['schema']['type'] = $value['type'];
            !isset($value['default']) ?: $value['schema']['default'] = $value['default'];
            !isset($value['min']) ?: $value['schema']['minimum'] = $value['min'];
            !isset($value['max']) ?: $value['schema']['maximum'] = $value['max'];
            unset($value['min']);
            unset($value['max']);
            unset($value['default']);
            unset($value['type']);

            unset($value['desc']); // 兼容 PHALAPI
            unset($value['require']); // 兼容 PHALAPI

            $value['in'] = 'query';
            $result[] = $value;
        }
        return $result;
    }
}

过滤文件App\Http\Controller\Api.php

<?php
namespace App\Http\Controllers;

use App\Traits\ApiResponse;

/**
 * Api 接口控制器
 * @desc 作为过滤SWAGGER方法模版使用
 */
class Api extends Controller
{
    use ApiResponse;

    // 接口参数规则
    public function getRules()
    {
        return [];
    }
}

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 13

swagger之前我也写有一套laravel专用的。但是前端不会用这个东西。现在基本上就是用postman导出接口再用apipost之类的完善下文档的中文简介

1个月前 评论
guangguijun (楼主) 1个月前
Cooper

推荐下:github.com/dedoc/scramble

基于 laravel 默认文件结构,没有心智负担。
也就是字段备注下;生成文档如下:

file

因为是基于 OpenAPI 格式,基本上接口调试工具都是支持导入的。

1个月前 评论
guangguijun (楼主) 1个月前
Cooper (作者) 1个月前
guangguijun (楼主) 1个月前

没用过swagger,很多地方看不懂你为什么那么做,但是想刀你

1个月前 评论
guangguijun (楼主) 1个月前
唐章明 (作者) 1个月前
唐章明 (作者) 1个月前
guangguijun (楼主) 1个月前

list 不是关键字了吗?

1个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!