PHP代码重构历程
2019年4月 1日,公司的项目业务调整,加了两个月的班,终于上线了,过程一路忐忑,上线前还出现了不少bug,代码维护成本已经很高,绝对不仅仅是缺少注释问题。个人也和做java开发的朋友交流过,我发现他们开发体验就很好,那么同样是基于框架开发,同样是对数据库的增删改查的业务,为什么我这么痛苦,难道我要去学java,但是我坚信自己写不好代码怪语言是不对的,一定是我没有合理使用PHP。 所以后面我仔细复盘了过去在公司写的所有代码,并且在对项目存在的问题进行了深刻的反思和改进,在接下来的一年的时间,不断用改进的开发模式去完成新的业务,同时用新的业务场景不断完善了开发模式。目前来看如今的开发大规模实现了工具化、脚本化,可维护性也大大增强。下面是个人过去一年的改造之路,相信任何看完的人都会有所收获的。
1.找到过去编码存在的问题
因为项目中用的是thinkphp,所以我首先熟读了thinkphp的开发手册,通过断点调试的方式学习了tp的核心源码并做了记录,大概知道了一个http请求在框架的完整细致流程。同时看了其他基于thinkphp开源项目的代码、并且学习了yii框架,同时看了一些不用框架开发业务功能。发现问题主要可以归结为连表问题
,面向对象和函数传值
,MVC开发模式问题
,业务开发缺乏通用的模板
, 下面将逐个分析原因和改进方案。以下的只是个人所在公司代码存在的情况,不一定是普遍存在的问题。
2. 对问题的分析与解决
先假设存在以下业务场景,后面的所有的业务都围绕着该场景讨论:
数据表定义如下:
- author作者表,存在作者的基本信息
CREATE TABLE `author` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '作者id',
`name` varchar(25) NOT NULL DEFAULT '' COMMENT '作者名字',
`info` varchar(255) NOT NULL DEFAULT '' COMMENT '基本信息',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='作者表';
- book表,存放书籍的信息
CREATE TABLE `book` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '文章id',
`title` varchar(25) NOT NULL DEFAULT '' COMMENT '文章标题',
`press_id` int(11) NOT NULL DEFAULT '0' COMMENT '出版社id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='图书表';
- book_comment,图书的评论表,表示对某一本书的评论
CREATE TABLE `book_comment` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '评论的id',
`book_id` int(11) NOT NULL COMMENT '图书的id',
`content` text NOT NULL COMMENT '评论的内容',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='文章表';
- author_book_relation:图书和作者的关联表
CREATE TABLE `author_book_relation` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '文章id',
`author_id` int(11) NOT NULL COMMENT '作者id',
`book_id` int(11) NOT NULL COMMENT '文章id',
`is_main` int(11) NOT NULL COMMENT '是否是主要作者 1表示是,0表示不是',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;
press表:出版社信息表
CREATE TABLE `press` (
`id` int(11) NOT NULL COMMENT '出版社id',
`name` varchar(25) NOT NULL DEFAULT '' COMMENT '出版社名字',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
图书和出版社是一对一的关系,图书和评论是一对多,图书和作者是多对多,覆盖了业务中常见的关联关系。
2.1 join连表问题
2.1.1 问题的产生背景
下面看看如何随着业务的增加导致join复杂度越来越大的。
假设需要提供一个图书列表功能,展示的信息只有图书的名字和id。此时仅仅需要从book表查询数据就能满足需求。如果有一天需求增加了,我需要显示出版社的信息,之前的代码就会left join press表,因为有些图书可能没有出版社,用inner join可能会把这些信息过滤掉导致不符合业务逻辑,所以会用left join,此时最终生成的sql语句类似下面
select a.id,a.name,b.name as press_name from book a left join b on a.press_id = b.id;
如果有一天需要在图书列表显示作者信息,不得不join author_book_relation
,author
表,逐渐复杂度就上来了。这只是伴随着获取数据的量的增加而导致sql变的更加复杂的一个方面。其他的方面在于搜索和排序,比如我现在希望在图书列表页根据出版社的名字、或者作者的id、作者名字 搜索图书,这个功能增加以后left join又要变成inner join了,假设我需要按照图书作者的多少进行倒序排序,这个地方就更加难以维护了,因为这里会把group by book.id order by count(author_book_relation.*) desc
引入。查询的核心代码其实永远都是业务的并集,久而久之越来越难以维护了。
归纳不难发现主要原因如下:
2.1.2 问题的分析和解决方案
- join的用途错误
在开发中join的作用仅仅是确定数据筛选范围,而不是join另一个表以后从被join的表里面选数据
,这一点至关重要。比如我需要在图书列表返回图书的出版社信息,应该分两次
select * from book limit 0,15;
然后
select * from press where id in (刚刚从book表查出来的press_id)
然后再把第二个数据集拼接到第一个的数据集里面去,返回给客户端,比如
{
"id":1
"name":"C语言教程",
"press_data":{
"id":2,
"name":"人民教育出版社"
}
}
对于一对一的数据,我们返回对象,对于一对多和多对多返回的是数组。可以看到这样有效的解决了上面因为要增加附加信息而进行的连表操作,同时也不用担心表字段重名的问题。其实类似的编程思想就是ORM,大部分框架都是实现了,方便易用,只是以前没有用过而已。比如上面的press_data其实就是定义在BookModel里面的一个函数
/**
* 出版社信息
* @return \think\model\relation\HasOne
*/
public function pressData()
{
return $this->hasOne(PressModel::class, 'id', 'press_id');
}
一个类里面的函数是不可能重名的,我们可以把所有的关联都定义在model类里面。需要返回信息的时候只要 $model->with(['xxx'])就完成了,不用修改其他任何代码。
关联函数以Data结尾的优点:
- 尽量避免于数据表的字段重名,降低导致with关联的时候SQL语法报错的风险。
- 自己能对函数的作用区分,方便统一处理,比如个人就根据这个实现了文档自动生成器,字段的信息就是根据php的反射获取的方法注释。https://www.jianshu.com/p/78e3e39f58c0
- 方便和客户端的约定,客户端开发也是MVC模式,接口生成器生成的文档标明了关联数据的模型类型,在调试接口时候他们也更清晰的知道是需要新建模型,还是需要在已有的模型里面增加字段。
- 需要优化按需连表。
上面的ORM关联查询解决了返回数据问题,但是join的场景还是存在的,比如我需要根据出版社名字搜索、需要按照作者名字搜索、按照评论内容搜索。这些都需要进行连表。但是连表的目的仅仅是为了确定数据筛选范围、和返回的数据格式是分开的
。
需要连表的场景主要是where和order,并且当where条件和order条件同时出现的时候可能会产生一些更加复杂的状况,比如我需要按照出版社名字搜索图书,此时应该book inner join press
,但是如果需要仅仅按照图书的出版社成立时间,对图书进行倒序排列,此时需要的是left join,如果用inner join会出现这样的情况:本来图书列表页有10本书,假设有一个图书没有出版社,仅仅因为排了序,列表就少了一本书,显然是错误的。其实having条件也是可以作为搜索条件的,比如我要求查询有两个作者的图书,这个时候其实是 join author_book_relation表,然后group by book.id having count(author_book_relation) = 2 才能满足筛选。所以可以看到有的时候数据筛选不仅仅是where就完事,有的时候还需要group by。对于group by一般做数据的分组统计,也可以用来去重。另一个典型的场景:比如我要搜索作者id为1,2,3,4这些人写的书,彼此之间是或的关系,这个时候数据选择前就需要去重了,因为sql语句是
select book.* from book join auth_book_relation on book.id = auth_book_relation.book_id where auth_book_relation.author_id in (1,2,3,4)
如果存在作者id为1,2的用户同时写了一本书这样类似的情况,图书列表页就会出现两条相同的图书信息,为了应对这种可能出现的情况我们就需要使用group by,但是如果只搜索一个作者,此时就不需要group by去重..... 总之可以很复杂。
按照之前的代码大概这样
public function index()
{
$param = [];//查询条件map
$bookModel = new BookModel();
$where = [];
if($param['press_name']){//出版社名字
$bookModel
->alias('a')
->join(PressModel::getTable() .' b','a.press_id = b.id');
$where[] = ["b.name","like","%".$param['press_name']."%"];
}
if ($param['comment']){ //评论内容搜索
$bookModel
->alias('a')
->join(BookCommentModel::getTable() .' c','c.book_id = a.id');
$where[] = ["b.content","like","%".$param['comment']."%"];
}
if ($param['author_name']){ //根据作者名字模糊搜索
$bookModel
->alias('a')
->join(AuthorBookRelationModel::getTable() .' d','d.book_id = a.id')
->join(AuthorModel::getTable() .' e','e.id = d.author_id');
$where[] = ["e.name","like","%".$param['author_name']."%"];
}
if ($param['author_ids']){ //作者名字多个查询
$bookModel
->alias('a')
->join(AuthorBookRelationModel::getTable() .' d','d.book_id = a.id');
$where[] = ["d.author_id","in",$param['author_ids']];
if (count($param['author_ids']) > 1){
$bookModel->group('a.id');
}
}
// --
return $bookModel->where($where)->paginate();
}
按照需求写完上面代码发现几乎要崩溃了,手动连表实在太麻烦,主要在于不断的要alias表名,即使不用alias表名,每次都用原始表名,在使用过程中依然要防止同一个表被连两次,比如上面提到的order条件和where条件同时存在的时候,而且特殊业务情况下,真的有时候需要一个表被join两次。如何改进呢?数据查询条件的构造一定是一个灵活的过程,必须有一个强大的类QueryBuilder
来灵活的完成一个查询的构建,最后返回一个可以获取数据的查询对象。因为仔细思考业务不难发现,大部分连表都是因为和模型之间有关联关系,上面提到ORM关联其实是定义在模型里面的一个方法。如果能够结合ORM方法,根据ORM定义的方法名,提供一种自动连表方案,并且在这个QueryBuilder类内部自动拼接、组合、去重各种条件。最后返回一个可以满足业务查询的query对象就可以了,外接调用根本不需要关系内部是如何连表的,只要知道对某个关联关系进行联查然后去调用就可以了,但是tp没有提供一个这样的类,个人基于think/orm实现了该功能。
更改之后的
//1.创建对象
$queryBuilder = new QueryBuilder();
$queryBuilder->setModel((new BookModel()));
//2.//设置主表字段和with关联内容
$queryBuilder->setFiled("id,title,press_id");
//with的内容取决于业务逻辑,列表页少一些,详情页多一些,但是没有本质区别
$queryBuilder->setWith(['author_data' => function (Query $query) {
return $query->hidden(['pivot']);
}, 'pressData', 'commentData']);//作者信息,出版社信息,评论信息
//3.设置构造查询条件
$where = [];
//书的名字包含程序两个字的
$tableKey = $queryBuilder->getQueryKeyByField("title");//主表查询
$where[] = [$tableKey, 'like', "%程序%"];
$tableKey = $queryBuilder->getQueryKeyByField("authorData-name");
$where[] = [$tableKey, 'like', "%Dennis%"]; //作者名字为Dennis。多对多
$tableKey = $queryBuilder->getQueryKeyByField("press_id");
$where[] = [$tableKey, '=', 1]; //出版社id查询,本表的字段
$tableKey = $queryBuilder->getQueryKeyByField("pressData-name");
$where[] = [$tableKey, 'like', "%中国人民%"]; //出版社名字查询,一对一关联表查询
$tableKey = $queryBuilder->getQueryKeyByField("commentData-content");
$where[] = [$tableKey, 'like', "%好%"]; //查询评论的内容,包含好字的。
// $queryBuilder->appendManyJoins() 其他需要自己额外手动连表的操作。
$queryBuilder->setWhere($where);
//4.按照评论表的id倒序,只是演示一下排序字段,自动left join功能
$orderField = $queryBuilder->getQueryKeyByField("commentData|LEFT-id");
$queryBuilder->setOrder("$orderField desc");
//5.
//根据生成的query进行后续操作,分页,查询,limit,灵活选择。
//$list = $queryBuilder->query()->paginate();
$list = $queryBuilder->query()->select();
这样的形式,不用在关心连表的条件和表名重复的问题,只需要在定义关联关系的时候写一次就可以了,任何地方都可以调用。至次连表带来的代码难以维护问题从根本上得到了解决。
2.2 函数的参数传值
2.2.1 代码存在问题
-
默认参数滥用
代码里面几乎很少有类的方法中的形参要求传递对象,甚至可以说几乎没有,传递参数的形式只有数组(dict)或者一堆默认参数,随着业务扩展完全没有办法维护。比如:
public function pageList($where = null, $fields = '*', $order = 'id DESC', $page = 1, $size = 15, $with = []);
这是一个获取分页列表的函数,基于MVC的开发模式,这个是放在BaseModel的一个方法,最后一个with是后来加的,但是以后这个方法我再也不能维护了,假设我再需要加一个参数 $append来获取模型的追加属性。以后调用起来非常长,并且with如果不需要的时候必须手动传空才能占据位置,然后把append属性放在最后,另一个坑在于,如果子类继承了父类,同时重写了父类的方法,改动父类的方法的时候子类会提示语法错误。
- 滥用array(dict)传值。
$param = $request->param();//key value数组
$this->filter($param); //过滤以下
Model::create($param);
这种问题带来的巨大的隐患,也许是php以前对面向对象支持不够导致的,函数的参数从未被定义过类型,一般都是把dict往里面扔,然后里面做判断,类似 if(isset($param['key']))
这样的代码,业务逻辑复杂了完全懵逼,比如创建订单的过程,可能需要把用户提交来的数据传给很多不同的函数使用,拿着dict到处传值很难受。虽然上面的代码中,tp提供了Model::create(array $data)
方法用于根据数组写入数据,但是不应该在其他地方也依然是dict到处传递,可以最后调用地方实现Model::create($param->toArray())
类似这样的调用。
2.2.2 解决方案:
1.对于参数的传递,应该尽量以对象的形式传递。尤其是对参数的个数不固定,以后可能增加的时候,或者对参数需要根据业务逻辑调整以后才能被函数使用的场景。
- 尽量杜绝dict传值,dict当然有它的灵活性,比如很多配置文件都是dict,这种场景是非常实用的,但是其他的业务场景,最好用特定的类传递,比如做查询的时候我就用上面的QueryBuilder类,接收用户传递的参数的时候,我都建立对应的类然后实例化对象后传递。
以对象的形式传值的优点
-1.如果函数增加参数,不用修改函数,只要增加类的属性就可以,丝毫不用担心以前调用地方。
-2.对象里面可以做一些统一的处理,也方便增加注释,对IDE的提示也会好一些。比如我需要对图书列表的搜索条件建立一个查询类,里面的属性就是支持搜索的字段,写好注释。在任何搜索图书的地方都以这个BookSearchParam来传递搜索参数,这样系统任何地方调用图书查询都一目了然,以后再需要添加新的搜索条件或者看看当前已经支持的搜索条件的时候,点击跳转到该类然后看看属性和注释即可。
2.2.3 传递模型类的优化
因为model也是对象,所以在函数参数中传递model也是可以的,但是因为php是动态的语言,model类对应的其实是数据表,通过php的 __get()方法,tp的模型实例化以后,支持以$model->property的形式调用属性,这里包括数据表本身存在的属性、获取器、ORM关联对象。但是PHPStrom没有语法提示,因为这些都是动态的,解决这个问题,需要额外给model增加注释,为了解决这个问题,个人实现了模型属性生成器,减少了维护成本。https://www.jianshu.com/p/fcc98b7daab2
这些问题感觉和php的发展有很大的关系,个人感觉php以后会对面向对象方面的功能进一步增强,比如现在不能声明一个由BookModel组成的数组,作为一个函数的参数。唯一能做的就是在函数的参数注释的地方 增加@param BookModel[] $books
,然后在函数内遍历books的时候,编辑器会提示bookModel的属性,当然这里的BookModel的属性也需要手动增加,个人只是用生成器替代了手工劳动。
2.3 MVC开发模式
假设我们要开发一个图书模块,命名为book
,需要建立一个Book的目录,里面有model、controller、view三个目录,view存在相关html文件,model存放数据表关联的model类,controller存放对应增删改查的controller类。假设现在我们要做book增删改查。此时控制器写list、detail,create、update、delete方法,然后模型里面也写这几个方法,控制器调用模型就可以了。如果再需要开发一个作者或者评论的功能,依然可以仿照这样开发。后台功能完成后,要开发api模块用于对app用户提供服务了,在与book并列的api目录下的book文件夹中依然建立mvc三个目录,依然照搬上面的思路实现业务逻辑。这样初期看似没有问题,其实已经一步步埋下隐患了。
2.3.1 造成的问题
-
业务逻辑肯定是越来越复杂的,为了能够让代码可以被重复调用,繁重的业务逻辑代码一定是会往model里面放的,model文件会越来越大,有些文件竟然有4000多行。
-
一个模型对应其实是一个数据表,刚刚在book模块下有BookModel,在开发api接口的时候也建立了BookModel,这里不得不建立,因为我们希望实现前后端功能的分开。以此类推,当其他模块需要调用book数据表的时候,很自然的需要在该模块下建立BookModel然后写代码,久而久之BookModel可能有很多个,每个里面都有很多业务逻辑,很多可能都是重复的,比如对于一个列表页,可能仅仅是支持的查询条件和返回字段不同,久而久之维护成本极高。
-
模型提供了其他的额外功能,比如before_insert,before_update这类事件定义,可以用来在模型操作数据前做一些统一的操作,比如我希望在创建一个图书的时候自动更新广告位缓存,因为你不知道创建用的是可能是哪一个模块下的BookModel,自然你要把before_insert代码每个里面都定义一次。
2.3.2 解决方案
经过和其他同事的探讨、并且结合网上的方案,我们引入了service层,同时也增加了其他的目录结构,拆分代码。把业务逻辑普遍放在service层里面,但是如何定义service层,其实也是一个漫长不断完善的过程。经过改造后一个模块应该有以下目录:
- service:封装对于业务逻辑的代码,一个serivce默认关联一个model,默认实现对该model的增删改查,有新的业务在这里实现,api的service类继承后台的同名service类,业务之间以service形式互调。
- validate:存放一个模型创建数据需要的验证器。
- param:存放业务相关的参数类,比如下单需要用户传递一些参数,把下单的参数定义一个类,根据客户端下单的参数实例化一个对象传递给service处理。
- view:和mvc模式一样
- controller:控制器类只能调用service层代码,不允许调用model类。
- model:只允许定义修改器,获取器,设置model的一些属性、定义一些ORM关联方法。不允许出现任何业务逻代码,一个项目中一个数据表只能有一个Model,因为模型的关联关系是通用的,比如图书和出版社之间就是一对一,不论是后台还是app端。这样可以防止关联关系重复定义。
- enum:定义一些常量类,比如各种状态,禁止直接数字。
if ($status == 1) 改动前
if($status == StatusEnum::ON_LINE) //改动后
一个service的案例如下
/**
* 产业园区 service
* Class IndustryAreaService
* @package app\dbase\service
*/
class IndustryAreaService extends BaseService
{
/**
* 搜索条件的前缀
*
* @var string
*/
protected $searchPrefix = "";
//实例化引用一个模型
public function __construct($modelClass = null)
{
parent::__construct($modelClass);
$this->model = new IndustryAreaModel();
}
/**
* 根据外面传递进来的参数构造查询对象,拼接where
* @param QueryBuilder $cyQuery
* @return QueryBuilder
*/
public function buildQuerySet(QueryBuilder $cyQuery)
{
// 获取传入参数
$param = $cyQuery->getQueryParam();
// 过滤搜索条件
$param = $this->filterSearchParam($param);
$condition = [];
foreach ($param as $key => $value) {
if (in_array($key, $this->getModel()->getTableFields())) {
$tableKey = $cyQuery->getQueryKeyByField($key);
if ($key == 'name') {
$condition[] = [$tableKey, 'like', "%$value%"];
} else {
$condition[] = TpQuerySet::buildCond($tableKey, $value);
}
} elseif ($key == 'level') {
$tableKey = $cyQuery->getQueryKeyByField("parent_id");
if ($value == 1) {
$condition[] = [$tableKey, '=', 0];
} else {
$condition[] = [$tableKey, '<>', 0];
}
}
}
$cyQuery->setWhere($condition);
return $cyQuery;
}
/**
* 数据分页列表页
* @param QueryBuilder $querySet
* @return \think\Paginator
*/
public function lists(QueryBuilder $querySet)
{
$querySet->setWith(['createdData', 'updatedData', 'provinceData', 'cityData', 'areaData']);
$querySet->setAppend(['created_at_for_display', 'updated_at_for_display']);
$list = $this->search($querySet)->paginate(QueryBuilder::pageSize());
return $list;
}
/**
* 详情页
* @param QueryBuilder $querySet
* @return array|null|\PDOStatement|string|\think\Model
*/
public function detail(QueryBuilder $querySet)
{
$querySet->setWith(['provinceData', 'cityData', 'areaData','parentData' => function(Query $query){
return $query->field("id,name");
}]);
$result = $this->search($querySet)->find();
return $result;
}
/**
* @param IndustryAreaParam $param
* @param int $returnType
* @return mixed
* @throws \Exception
*/
public function create(IndustryAreaParam $param, $returnType = 0)
{
Db::startTrans();
try {
$model = IndustryAreaModel::create($param->toArr());
Db::commit();
return $returnType == 0 ? $model->getKey() : $model;
} catch (\Exception $e) {
Db::rollback();
throw $e;
}
}
/**
* 更新
* @param IndustryAreaParam $param
* @param array $where
* @return bool
* @throws \Exception
*/
public function update(IndustryAreaParam $param, $where = [])
{
Db::startTrans();
try {
if (!$where) {
$where = [$this->getPk() => $param->id];
}
$model = IndustryAreaModel::update($param->toArr(), $where);
Db::commit();
return $model->getKey();
} catch (\Exception $e) {
Db::rollback();
throw $e;
}
}
public function delete($where)
{
$this->model->where($where)->update(['status' => StatusEnum::Trash]);
}
/**
* 分组信息
* @return array
*/
public function groupInfo()
{
return $this->makeGroupInfo('status', [NormalStatusEnum::ON_LINE, NormalStatusEnum::OFF_LINE, NormalStatusEnum::TRASH]);
}
}
结合对于前面的join问题和函数参数传值问题的改正,service里面传值都以对象的形式传递,search方法是所有搜索的核心方法,用于根据querybuilder类构建一个可以查询的query对象,比较通用所以在BaseService实现,然后这个对象可以进行select、paginate、find等各种操作,可以根据不同的需求在service里面实现不同的方法,比如lists是列表页,其他地方可能也有列表页但是返回的字段可能不一样,如lists2,只要在lists2里面单独对queryBuilder类设置不同的field就可以了,所有的where条件都在buildQuerySet方法完成,对于后台相对单一的查询模式我们可以以dict的形式传参,但是如果对应复杂的搜索,可以建立对应的search类进行传递,或者可以统一都由建立对应的Search类。api下的service继承后台的service,这样就可以复用一些方法,同时在必要的时候可以进行方法覆盖,另一个好处是代码可以重复调用,比如需要写一个判断图书是否有效的函数,这个功能后台和app端都需要使用,把这个函数写在后台的service里面,api因为继承所以自然就直接可以用了,万一对于app端的判断逻辑不一致,我们就可以方便的重写这个函数内部实现。
3.额外功能的增强
虽然上面三个问题是分开讨论的,但是实际上是同时存在项目中的,可以试着想一下同时存在的痛苦。随着不断完善,结合新的业务,以前的存在的问题一去不返了,但是还是不够好。主要是代码风格渐渐一样了,每次开发都是复制已有的文件然后改动,还要改注释,麻烦。而且我也不愿意写太多注释,后来观察大部分建立的类的属性和方法都可以根据数据表创建语句的注释获取到,所以我写了一个代码自动生成器,一键生成代码和大部分注释。https://www.jianshu.com/p/78e3e39f58c0
后来随着业务越来越复杂,接口数据字段太多,前端的同事要求写注释文档。因为代码用工具生成了,导致写文档比写代码还费时间,还要手动维护接口变化。因为上面可以看到大部分已经基于orm开发,代码风格又比较统一,所以我写了一个文档自动生成器,可以根据接口快照实时生成文档。https://www.jianshu.com/p/bc54478b9609,接口不用手写文档,实时更新自动生成,偶尔会有个别字段手动维护。
后来发现定位bug是非常困难的事情,tp的日志不能满足,所以做了一个php日志统一管理功能,并做到了后台,能够快速定位异常。https://www.jianshu.com/p/4e4d3c3e10f2。
4.个人的感受
开发PHP已经3年了,也是去年此时一个人不断修复上述问题的,也痛苦过,迷茫过,甚至都不知道什么是正确的、个人的改动也没有什么标准遵守,只是以如何让自己用起来方便、代码更维护为目标来实现小姑。但是令人欣慰的是目前的改动自己和同事用起来还是非常快捷和舒服的,希望php以后能够发展的更好,不要老被唱衰。