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 协议》,转载必须注明作者和本文链接
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
讨论数量: 8

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

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

推荐下:github.com/dedoc/scramble

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

file

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

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

list 不是关键字了吗?

3个月前 评论

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