一、架构体系
Model一般放的是粒度比较细的业务
而Service是对Model层粒度的组装
但是Model和Service统称为业务层,就是业务复杂的时候要用到Service 不复杂的时候直接走Model层 因为Model和Service是平行关系.
业务层是通过Think DB 框架去调用Mysql 从而获得相关的业务数据
这个图适合中小型项目
二、Banner
Banner数据表设计分析
banner表:记录banner位,即将程序中用到轮播图的地方编码并记录在此表中,此项目中虽然只有一个地方有轮播图,即只有一个banner,但是这种逻辑想法还是要有的,以备以后添加新的banner。
banner_item表:记录某一个banner中的具体内容,用banner_id来标识此banner_item属于哪一个banner。
banner和banner_item是属于一对多的关系,即一个banner可以有多个banner_item,某个banner_item只能属于一个banner。
Banner接口定义以及自定义控制器多级目录
目录
Banner.php
<?php
namespace app\api\controller\v1;
class Banner
{
/**
* 获取指定id的banner信息
* @url /banner/:id 访问接口的路径
* @http GET
* @id banner的id号
*/
public function getBanner($id){
}
}
route.php
Route::get('banner/:id','api/v1.Banner/getBanner');//三段式(模块/控制器/方法)
三、构建验证层
使用Validate类构建参数校验层的两种方法
校验客户端传来的参数
验证:内置规则
1、独立验证
<?php
namespace app\api\controller\v1;
use think\Validate;
class Banner
{
/**
* 获取指定id的banner信息
* @url /banner/:id 访问接口的路径
* @http GET
* @id banner的id号
*/
//1数据2规则3校验
public function getBanner($id){
$data = [
'name'=>'vendor111111111',
'email'=>'vendorqq.com'
];
$validate = new Validate([
'name'=>'require|max:10',
'email'=>'email'
]);
$result = $validate->batch()->check($data);//batch批量验证
//echo $validate->getError();
var_dump($validate->getError());
}
}
2、验证器
自定义验证规则
若手册中没有自己需要的内置规则,则需要自定义规则
class IDMustBePositiveint extends Validate
{
protected $rule = [
'id'=>'require|isPositiveInteger'
];
//自定义规则
protected function isPositiveInteger($value,$rule='',$data='',$field=''){
if(is_numeric($value) && is_int($value+0) && ($value+0)>0){//value是string类型
return true;
}
else{
return $field.'必须是正整数';
}
}
}
构建接口参数校验层
项目会随后定义一些列的验证器,所以将所有验证器写在同一目录下,在处理业务逻辑时实现复用,减少代码量。所有的验证器否会有一个goCheck()方法,具体有两个功能:一是获取http传入的参数,二是对参数校验。将这两个功能的代码封装在一个公共基类BaseValidate中,BaseValidate继承Validate类,其他验证器继承BaseValidate类,如此一来,在Banner.php中只需要写入下面代码即可实现校验。
public function getBanner($id){
(new IDMustBePositiveint())->goCheck();
}
四、REST与RESTFul
五、AOP与全局异常处理
构建model层处理业务逻辑
在controller目录下的Banner.php中将获取的id校验后,下一步则是通过获取的id号来获取Banner信息,这个业务逻辑需要写在model层,所以新建一个model文件夹用来处理业务逻辑。
在model目录下的Banner.php写需要的业务逻辑
public static function getBannerByID($id){
//TODO:根据Banner ID号 获取Banner信息
}
然后在controller目录下的Banner.php中调用处理此业务逻辑的函数,因为两个目录下的Banner.php重名,所以可以在控制器调用前取个别名,然后调用。
use app\api\model\Banner as BannerModel;
$banner = BannerModel::getBannerByID($id);
异常处理流程
全局异常处理用来处理两种情况:一是不想逐级向上抛出异常,二是一些无法提前预测的异常。全局异常处理:1记录日志,2返回统一的错误信息格式到客户端
固有的处理异常的思维模式与流程
常规的异常处理 不够灵活且复用性比较低
model下的Banner.php
public static function getBannerByID($id){
//TODO: 根据id号, 获取Banner信息
try{
1/0;
}catch (Exception $ex) {
//TODO:可以记录日志
throw $ex;
}
return 'this is Banner info';
}
controller下的 Banner.php
//这里捕获的异常是调用getBannerByID方法时getBannerByID方法内部抛出的异常
try{
$banner = BannerModel::getBannerByID($id);
}catch (Exception $ex) {
$err = [
'error_code' => 10001,
'msg' => $ex->getMessage()
];
//如果再抛出异常则是抛给TP5自带的异常处理类
//利用TP5的json函数将数组转化为json
//400表示的是返回的状态码
return json($err,400);
}
异常分类
自定义全局异常处理
让异常类和模块相互独立,以类库的形式存在,利于复用
model下的Banner.php抛出异常给controller下的Banner.php,controller下的Banner.php无法处理,再抛给自定义异常处理类,其中render方法会接受所有的异常,并且按照自定义的格式返回到客户端。注意,要想让异常抛给自定义异常处理类,需要在配置文件config.php中修改exception_handle参数,将参数设置成自定义异常处理类的命名空间。
config.php
// 异常处理handle类 留空使用 \think\exception\Handle
'exception_handle' => 'app\lib\exception\ExceptionHandler',
model下的Banner.php
<?php
namespace app\api\model;
use think\Exception;
class Banner
{
public static function getBannerByID($id){
//TODO:根据Banner ID号 获取Banner信息
try {
1 / 0;
}
catch (Exception $e){
//TODO:可以记录日志
throw $e;
}
return 'this is banner info';
}
}
controller下的Banner.php
<?php
namespace app\api\controller\v1;
use app\api\validate\IDMustBePositiveint;
use app\api\model\Banner as BannerModel;
class Banner
{
/**
* 获取指定id的banner信息
* @url /banner/:id 访问接口的路径
* @http GET
* @id banner的id号
*/
public function getBanner($id){
(new IDMustBePositiveint())->goCheck();//$this换成了IDMustBePositiveint()实例
$banner = BannerModel::getBannerByID($id);
return $banner;
}
}
自定义异常处理类ExceptionHandler.php
<?php
namespace app\lib\exception;
use Exception;
use think\exception\Handle;
class ExceptionHandler extends Handle//重写某个类的最好方法就是继承并且覆盖默认方法
{
public function render(Exception $e)
{
return json('111111111111111111');//试验能否顺利调用重写的render方法
}
}
Postman
验证成功后进一步写自定义异常处理类ExceptionHandler.php的业务逻辑
<?php
namespace app\lib\exception;
use Exception;
use think\exception\Handle;
use think\Request;
class ExceptionHandler extends Handle//重写某个类的最好方法就是继承并且覆盖默认方法
{
private $code;
private $msg;
private $errorCode;
//需要返回客户端当前请求的url路径
public function render(Exception $e)
{
if($e instanceof BaseException){//第一类异常
//如果是自定义的异常
$this->code = $e->code;
$this->msg = $e->msg;
$this->errorCode = $e->errorCode;
}
else{//第二类异常
$this->code = 500;
$this->msg = '服务器内部错误';
$this->errorCode = 999;
}
$request = Request::instance();
$result = [
'msg' => $this->msg,
'error_code'=>$this->errorCode,
'request_url'=>$request->url()
];
return json($result,$this->code);
}
}
在controller下的Banner.php编写抛出异常,即抛出BannerMissException(banner不存在)这个异常类(这里的BannerMissException需要继承BaseException公共异常类,同时BaseException要继承框架自带的Exception类,继承的时候进行重写属性和方法,简明的说,任何自定义异常类都要继承框架自带的异常类才能让自定义的异常处理类来接收自定义异常类,因为自定义异常处理类也 有继承框架自带的异常处理类)
ThinkPHP5中的日志系统
TP5具有默认记录日志功能,目录:
修改日志目录:
在index.php入口文件里修改
define('LOG_PATH', __DIR__ . '/../log/');
为什么需要修改日志目录(+自定义日志格式)?
TP5默认记录日志功能会将所有的记录存放起来,但是有些日志记录是不需要的,这些无意义的记录会耗费资源去存储、排查,比如一些因为用户操作不当而产生的异常记录。我们只需要记录服务器内部产生的异常,所以需要修改日志目录并自定义日志格式
在全局异常处理中加入日志记录
首先修改config.php文件
'log' => [
// 日志记录方式,内置 file socket 支持扩展
'type' => 'test',//改为test
// 日志保存目录
'path' => LOG_PATH,
// 日志记录级别
'level' => [],
]
再修改自定义异常处理类ExceptionHandler.php
private function recordErrorLog(Exception $e){
//初始化日志
Log::init([
'type'=>'File',
'path'=>LOG_PATH,
'level'=>['error']
]);
Log::record($e->getMessage(),'error');
}
再在服务器出错的业务逻辑里调用此方法
$this->recordErrorLog($e);//调用
全局异常处理的应用
出于当发生错误时客户端希望看到json结构体,但是服务器管理人员希望看到发生错误的具体原因,只看到json结构体的内容,不足以去排查错误。所以需要设置一个开关switch,用来控制两者的转换,将switch换成配置文件config.php‘中的app_debug,开时是调试模式,关则是生产模式。所以修改ExceptionHandler.php
public function render(Exception $e)
{
if($e instanceof BaseException){//第一类异常
//如果是自定义的异常
$this->code = $e->code;
$this->msg = $e->msg;
$this->errorCode = $e->errorCode;
}
else{//第二类异常
//$switch = true;
//if($switch){
//Config::get('app_debug');
if(config('app_debug')){
//return default error page
return parent::render($e);
//调用父类的render 还原TP5的默认rennder
}
else{
$this->code = 500;
$this->msg = '服务器内部错误';
$this->errorCode = 999;
$this->recordErrorLog($e);//调用
}
}
进一步来看
BaseValidate:
class BaseValidate extends Validate
{
public function goCheck(){
//获取http传入的参数
//对参数校验
$request = Request::instance();
$param = $request->param();//获取所有参数
$result= $this->check($param);//校验
if(!$result){
$error = $this->error;
throw new Exception($error);//Exception不属于BaseException,所以属于第二类异常
}
else{
return true;
}
}
}
如果校验不通过,则会返回错误,通过app_debug的开关,来观察发现无论是开还是关返回给客户端的信息都不满足我们想要的
具体例子:将输入的id改为0.1
app_debug关:
app_debug开:
所以新创建一个参数异常类ParameterException,修改BaseValidate,如下:
class BaseValidate extends Validate
{
public function goCheck(){
//获取http传入的参数
//对参数校验
$request = Request::instance();
$param = $request->param();//获取所有参数
$result= $this->check($param);//校验
if(!$result){
$e = new ParameterException();//自定义 属于第一类异常
$e->msg = $this->error;//覆盖msg属性的内容 数据来自validate的验证结果
throw $e;
//$error = $this->error;
// throw new Exception($error);
}
else{
return true;
}
}
}
优化1:
在BaseValidate中如果从外部访问成员变量的方式给成员变量赋值也未尝不可,但是我们建议有更好的一种面向对象写法,我们在new一个对象的时候就完成了赋值,而不是在new一个对象之后再赋值。可以通过构造函数来初始化赋值操作,更加符合面向对象的基本特性。则实例化的时候即可传入参数,相应的构造函数(父类中创建即可)会进行处理。
BaseValidate
if(!$result){
$e = new ParameterException([
'msg'=>$this->error
]);
在BaseException(ParameterException的父类)中添加构造方法:
public function __construct($params = [])//构造函数
{
if(!is_array($params)){
return ;
//throw Exception('参数必须是数组');
}
if(array_key_exists('code',$params)){
$this->code = $params['code'];
}
if(array_key_exists('msg',$params)){
$this->msg = $params['msg'];
}
if(array_key_exists('errorCode',$params)){
$this->errorCode = $params['errorCode'];
}
}
优化2:
在goCheck()中加入批量验证处理
$result = $this->batch()->check($param);
补充错误-了解Exception的继承关系
当输入路由错误时,页面如下:
由于抛出的异常HttpException和render方法中需要传入的参数为think的Exception不存在父子类关系(可以查找并分析这两个类),尝试将HttpException传入到本应该传think的Exception时就会出现报错。所以需要将传入的参数修改为两者的基类Exception。Exception
修改后:
或者:
本章小结与AOP思想
1学会重构代码,考虑代码的复用性和层次结构
2AOP思想
例子1: 校验层和异常处理层
我们处理异常的时候,我们并不会把异常分散到具体的每一个业务代码中,而我们提供了一个类似于横切面的东西,这个横切面就是我们的ExceptionHandler.php下的render方法,它会统一的处理所有的异常.
例子2:我们去看电影,电影院有一个检票口,也许你的票在猫眼,美团…上买的.不管你在哪买的,最后我们都要在检票口看你的票能不能入我的电影院.我们不能给每一个观影人都配一个检票员.