当前位置: 首页 > 后端技术 > PHP

WebAPI开发实践

时间:2023-03-29 22:58:21 PHP

前言之前在公司负责一个项目,前后端分离。笔者负责整个项目的基础架构搭建,在这里总结了一些经验。本文主要介绍后端webapi的设计与实现。demo代码链接:github代码基本架构代码分层应用的基本架构主要包括以下五个部分:ControllerLayer(控制器层)TransformerLayer(转换层)ServiceLayer(服务层)RepositoryLayer(仓库层)ModelLayer(Model层各层主要职责如下图所示,基本程序流程见上图,从1到8详细介绍,如果业务逻辑比较简单,Service层可以跳过直接,Controller层可以直接调用Repository层,各个层级可以通过依赖注入连接起来,业务逻辑主要分布在Service层和Model层,Service层负责工作流逻辑,即具体的执行任务的流程,比如事务处理等;Model层负责领域逻辑,包括业务规则和业务计算。Service层包含了主要的工作流逻辑,复用性比较差。但是,当Service层的业务逻辑积累到一定程度,就会沉淀出一些通用的业务逻辑(工作流逻辑)。最好将一般的业务逻辑抽取出来,在Service层中形成一个子层,称为“通用流程层”。这部分代码可以放在当前Services目录下的General目录下。Service层的返回值:1.业务对象(模型等业务数据)2.布尔值,表示处理结果。当Service层的业务逻辑不能正常执行时,需要抛出业务处理异常BusinessException(注意不是程序执行异常。业务处理异常举例:账户余额不足,转账无法制作)。通过业务处理异常,将异常的业务处理结果返回给调用方(如:Controller或其他Service)。在业务逻辑正常执行的情况下,返回Service层的正常返回值,也就是上面的第五点。在每一层中,在开启一个新的子类时,最好为该子类建立一个基类。以Controller层为例,当需要在app/Api/Controllers/V1目录下创建Blog子目录时,最好在创建的目录下添加一个BaseController作为该目录下的基类。Model层又可以细分为AR(ActiveRecord)层和Domain层。Domain层通常基于AR层。AR层中的每个类对应一个数据库表,Domain类中包含的数据可以来自多个AR类。通常,与数据库相关的代码都写在AR层,比如表之间的关系,表属性的可能取值等等。对应的领域逻辑通常写在Domain层。eg:领域模型某些取值的取值规则Domain类代表一个完整的领域模型,而AR类不一定构成一个完整的领域模型。eg:产品数据存储在多个表中:product_a和product_b等,所以会有多个AR类对应这些表;同时可以引入一个名为“Product”的Domain类,代表一个完整的产品(DomainModel)。域类可以基于底层AR类之一(一般基于主表)。目录结构目录结构如下:详细说明如上图所示,每一层Controller、Service、Transformer、Model、Repository都有自己对应的目录Controllers目录说明(Controller层)Controller层,所有的api控制器都放在这个在目录下是按照版本(V1,V2...)分类的,在版本目录下是按照业务分类的。Controller层的职责:验证输入,处理请求&构建响应,调用Transformer层,Service层,Repository层,但不应该在Controller中包含每个版本目录下的任何业务逻辑(V1,V2...),并且将Controller按照业务(eg:Blog,Marketing...)划分到不同的子目录,而不是按照数据库划分,虽然按照业务划分和数据库划分的结果可能是一样的。每个版本目录下都有一个版本控制器(eg:V1Controller),该版本下的所有控制器都需要继承这个控制器。版本控制器必须继承自App\Http\Controllers\ApiController在按业务划分的controller子目录下应该有一个controller基类(eg:BaseController),该目录下的所有controller都继承自该基类ControllerCommon目录描述TheCommon目录用于放置一些通用的代码,可以在整个项目中使用。通常,这些代码不应该包含特定的业务逻辑。子目录Components用于放置组件代码(注意:这些组件代码不要继承自框架代码/第三方代码,否则应放在Extensions目录下)。通常这些代码可以提供特定的功能,但不依赖于框架本身,可以作为其他项目的第三方包。自框架代码/第三方代码),注意与Components子目录区分Enum用于放置“常量定义”子目录的代码Helpers用于放置一些工具类,工具类通常会提供一些静态方法方便调用subdirectoryScopes用于放置EloquentORM相关的Scopes定义子目录。lib是用来存放一些底层库文件的Models目录description(Modellayer)模型层,所有的模型类都放在这个目录下。通常按数据库分类(如:DbBlog)模型层职责(继承自Eloquent类时):对应一张数据库表,一个模型实例表示表中的一个记录处理属性,如$db、$table、$fillable等.;处理scopeAccessors&Mutators:从模型实例获取或存储属性时,格式化关联关系配置:使用模型自身行为的代码如hasMany()、belongsTo()(即领域逻辑代码,即部分业务逻辑),包括模型在运行时的状态变化,比如状态从有效变为无效模型层职责(当没有继承自Eloquent类时):作为一个领域类,它包含领域逻辑当一个完整的领域类分在多个数据库中当表存储在数据库中时,可以在每个数据库目录下创建一个Domain目录(如:DbBlog)来存放完整的领域类。所有数据库表对应的Model都应该间接继承自AppModel。每个数据库目录(如:DbBlog)都应该包含一个BaseModel(代表数据库),其他Model继承自BaseModel注意:请不要在数据库表中放置“增删改查”操作代码模型,以及“添加”应该添加“删除,修改和检查”代码放在Repository层Repositories目录描述(Repositorylayer)Repository层,所有仓库类都放在这个目录下。通常Repository层的职责是根据业务/数据库来划分的:只包括直接对数据库进行增删改查的代码,辅助Model层(请不要放置其他代码;通常是逻辑增删改查比较简单,而检查时会出现多种情况,这里实现了各种查询逻辑)Repository层只包含直接操作数据库的代码,其他涉及功能的代码如外部调用应该考虑放在Service层。所有存储库类都应继承自AppRepository类。Services目录说明(Servicelayer)服务层,所有的服务类都放在这个目录下。通常按业务分类Service层的职责:处理涉及的外部行为:如发送邮件、使用外部API(如使用队列、调用thrift、调用其他团队的服务等)包括业务逻辑(主要是工作流逻辑(workflow)logic),即完成某项任务的具体过程):service层是业务逻辑主要存在的地方,辅助Controller层;当需要对数据库进行增删改查时,调用相应的Repository层。所有的服务类都应该继承自AppService类Transformers目录description(Transformerlayer)Transformer层,所有的转换类都放在这个目录下。通常按业务分类。Transformer层职责:处理展示逻辑管理API接口的输出(将接口的输出与底层的Service、Repository、Model等解耦,这样即使修改了底层的数据库表也不影响接口的使用)所有的转换所有的类都应该继承AppTransformer类Response注意:这里所说的响应格式是指应用业务相关的响应,第三方提供的API接口的响应不包含在处理范围(如:laravelpassport提供的响应,swagger提供的响应)响应分类成功响应:http响应码在200到300之间。返回这种类型的响应表明服务器完整地处理了请求,没有未处理的异常或错误。(除正常情况外,当业务逻辑处理失败时,也会返回此类响应,同时带上相应的业务处理失败信息)失败响应:http响应码不在200~300之间.返回这种类型的响应表明服务器抛出了未处理的异常或错误。响应示例成功响应1.业务逻辑处理成功2.业务逻辑处理失败。结构如上图所示:结构同业务逻辑处理成功。不同的是code为0代表成功,对应的错误码代表失败。code的值为app\Common\Enum\ErrorCode.php中的业务级错误码(错误码见下)。失败响应失败响应的格式在文件config/api.php中配置(关键字:errorFormat)。主要包括message、errors、code、status_code、debug。有些信息在生产环境中是不会显示的。响应格式化处理的思路响应格式化处理的总体思路:在返回给用户(使用事件机制)时截取特定请求的处理结果(标记此类请求),格式化原始响应处理和。Responsecode:App\Http\Middleware\BusinessFormatOutput:路由中间件,如果中间件放在某些路由中,请求会被标记,表示需要格式化响应App\Listeners\AddBusinessStatusToResponse:Eventhandler,由dingo处理的触发的ResponseWasMorphed事件格式化响应。App\Http\Controllers\ApiController.php文件中的常量BusinessStatusHeader通过响应中的header作为中介将业务逻辑处理结果传递给2中的事件处理器,最终构成格式化的响应。错误码错误码相关的代码文件为:app\Common\Enum\ErrorCode.php错误码格式:A-BB-CCCA:表示错误级别,0表示成功,1表示系统级错误,2表示服务(业务)级错误;B:表示项目/模块/类别;C:具体错误号;不同错误级别错误码的使用:业务级别的错误码用于表示业务处理结果。Service层业务处理失败,抛出BusinessException时,使用业务层状态码。Controller层构造response时,定义response的业务处理结果,eg:return$this->response->array($validator->errors()->toArray())->withHeader(self::BusinessStatusHeader,[ErrorCode::BUSINESS_INVALID_PARAM,'业务处理结果信息']);用于日志记录(与业务相关的日志)。系统级错误代码用于表示代码运行异常。用于记录系统异常日志。它可以用于Controller、Service、Transformer、Repository、Model的每一层。注意:错误代码文件不能被重写。如果有新的错误码,请按照已有的分类添加。旧的不能删除或修改。错误代码。异常和异常处理异常相关的代码:app/Exceptions目录。在应用程序代码中,只能抛出BusinessException或SystemException。请不要抛出其他异常。不同的异常通过代码来区分(代码定义在app/Common/Enum/ErrorCode.php)。当业务逻辑执行失败时,抛出BusinessException。常见的可能情况如下:Controller层验证输入失败,抛出BusinessException。失败(但没有抛出异常,但返回值表示执行失败),接收到返回值抛出的调用方BusinessExceptionController必须捕获BusinessException(所以即使抛出BusinessException,仍然需要返回成功类响应(见above)),并根据来自BusinessException的相应信息构造响应。建议所有的Controlleraction都写成下面的格式。publicfunctionadd(Request$request,ReserveService$reserveService){try{//将所有控制器逻辑放在try块中$postData=$request->post();//检查数据有效性/**@var\Illuminate\Validation\Validator$validator*/$validator=Validator::make($postData,['orderName'=>'required','reservePhone'=>'required',]);if($validator->fails()){//验证失败newBusinessException(ErrorCode::BUSINESS_INVALID_PARAM,"",$validator->errors()->toArray());}$result=$reserveService->addReservation($postData);if(true===$result){//业务逻辑执行成功return$this->response->array([]);}else{//返回值表示业务逻辑执行失败thrownewBusinessException(ErrorCode::BUSINESS_BUSY);}}catch(BusinessException$e){//捕获BusinessException并根据异常信息构造响应。以下代码可以通用return$this->response->array($e->getExtra())->withHeader(self::BUSINESS_STATUS_HEADER,[$e->getCode(),$e->getMessage()]);}}当发生底层系统异常时,抛出未被捕获和处理的SystemException将导致失败类响应(见上文)。日志和预警日志组件和预警组件的存在是为了更好的维护项目,及时处理bug。您应该根据需要添加相应的日志组件和预警组件。文档可以选择集成一个成熟的文档工具,比如swagger、blueprint等。