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 协议》,转载必须注明作者和本文链接
swagger之前我也写有一套laravel专用的。但是前端不会用这个东西。现在基本上就是用postman导出接口再用apipost之类的完善下文档的中文简介
推荐下:github.com/dedoc/scramble
基于 laravel 默认文件结构,没有心智负担。
也就是字段备注下;生成文档如下:
因为是基于 OpenAPI 格式,基本上接口调试工具都是支持导入的。
list 不是关键字了吗?
curd小王子