我们在写应用里的代码时根据代码负责的不同任务讲其分为五大块Controller
, Repository
, Service
, Model
, View
。
Model
数据模型, 数据模型面向的是数据层,在这里我们只关心数据表的问题,在 Model 中应该只定义数据与对象映射相关的属性和方法如:表名、主键名、是否让 laravel 管理时间字段等属性,以及模型关联、查询作用域等方法。其他与数据表无关的业务逻辑都不应出现在这里。Repository
数据逻辑访问层,由它来对接 Model 层,理论上有一个 Model 就会有一个相应的Repository
,除了做最基础的数据访问外与数据相关的逻辑也放在这里,如果一个相对复杂的数据应用到了Repository
对应的Model
外其他Model
的数据,不要直接去访问相关Model
,应该由Repository
调用相关Model
的Repository
。Repository
是具体interface
的实现,比如做订单相关的业务,应该有OrderRepositoryInterface
定义Order
数据交互流程中必须要实现的方法然后由OrderRepository
去具体实现,之后将OrderRepositoryInterface
和OrderRepository
注册到服务容器中,解析时直接使用OrderRepositoryInterface
解析出具体实现,这样消费层既不需要关心数据来自哪里是Mysql
还是MongoDB
,也给项目提供了足够的灵活性。当数据来源从Mysql
更改为MongoDB
后,我们只需要重新写一个实现类OrderMongoRepository
然后将服务容器里OrderRepositoryInterface
的实现切换成OrderMongoRepository
就好,消费层完全不需要改动。 (Repository 是我之前一直觉得在程序设计中特别多余而现在觉得特别重要的一个 Layer,之前在 Service 中揉进去了 Repository 的职能,后续会把相关的 Example 也做一下修改)Service
项目中除了数据的 CRUD 还会有图片上传、请求外部 API 获取数据、发送邮件等等其他这些功能,这些功能应该定义在Service
层。Controller
控制器,面向的对象是一个完整的页面或者是接口,其主要职责是作为接收请求和发送响应的中控程序,通过调度项目中的 Service、 Repository 等来完成请求、组合响应数据,并通过页面响应或者接口数据的形式将响应返回给客户端。View
视图, 负责渲染 HTML 响应,使用 Laravel 自带的 blade 模版引擎,并且应该尽量减少 PHP 代码。
总结:所以如果一个请求对应的业务逻辑相当复杂,我们可以通过 Controller 方法调用多个 Service 方法(单一逻辑)来完成这个复杂的逻辑,在 Service 方法中我们通过多个 Model 操作来实现更新、获取数据。通过这种原则来对复杂逻辑进行解耦。
我们通过看两个例子来更好的理解代码如何分层:
如果一个查询在项目中只会用到一次,那么我们直接在 Controller 方法里写这个查询就很好, 举例来说你想查出所有的管理员用户:
$admins = User::where('type', 'admin')->get();
现在假设你需要在不止一个 Controller 方法中用到这个查询, 你可以在UserRepository
类里包装对User
模型的访问:
class UserRepository
{
public function getAlladmins()
{
return User::where('type', 'admin')->get();
}
}
现在你可以在用到UserRepository
的Controller
中通过依赖注入UserRepository
, 然后通过这个 UserRepository 方法获取所有管理员用户:
//Controller
public function __construct(UserRepository $UserRepository)
{
$this->UserRepository = $UserRepository;
}
//Controller action
public function index()
{
$admins = $this->UserRepository->getAllAdmins();
}
最后,假设你还需要一个查询来计算管理员的数量, 可以在把这个查询封装到 UserService 的一个方法里:
public function getAdminNum()
{
return User::where('type', 'admin')->count();
}
这样写是 OK 的,但是你可能已经注意到了User::where('type', 'admin')
这个片段在getAllAdmins
这个查询里也有用到,所以我们可以用查询作用域来改进它(查询作用域也会让你的代码可读性变得更高):
//UserModel
public function scopeAdmins($query)
{
return $query->where('type', 'admin');
}
然后在 UserRepository 里我们可以向下面这样重写它的查询:
//UserRepository
public function getAllAdmins()
{
return User:admins()->get();
}
public function getAdminNum()
{
return User::admins()->count();
}
就像上面说的那样在 Model 中我们应该撇开业务逻辑,面向数据表进行抽象,只定义表相关的属性、模型关联和查询作用域, 具体怎么应用 Model 中定义的这些内容, 那是 Controller 层和 Service 层要关心的事情。
我们在做 CMS 系统管理内容时, 通常都会涉及到文章的上下线,置顶等操作。假设在系统中我们会用两个数据表分别存储文章和置顶, 在项目中对应两个 Model: Article 和 TopArticle。
假设我们需要实现下线文章的功能,如果文章是置顶文章,还要把文章取消置顶,我们在 ArticleService 里包装这个操作:
public function setArticleOffline(Article $article)
{
if ($article->is_top == 1) {//如果是置顶文章需要将文章取消置顶
$this->cancelTopArticle($article);
}
$article->status = Article::STATUS_OFFLINE;
$article->offline_time = date('Y-m-d H:i:s');
$article->save();
return true;
}
/**
* 取消文章的置顶
* @param \App\Models\Article $article
*/
public function cancelTopArticle($article)
{
if (TopArticle::specificArticle($article->id)->count()) {
//删除置顶表里的记录(待上线的置顶文章上线后置顶表中才能有相应的记录)
TopArticle::specificArticle($article->id)->delete();
}
//将文章的is_top字段置为0
$article->is_top = 0;
$article->save();
return true;
}
在 Controller 的 Action 里我们只需要负责验证接收来的参数,查询文章对象传递给 Service 来做下线操作, 这里之所以把取消置顶也单独提取出来是因为通常 CMS 系统里还有单独的文章取消置顶功能。
//ArticleController
public function setArticleOff(Request $request, ArticleService $artService)
{
...//表单验证
$article = Article::find($request->get('article_id'));
$this->articleService->setArticleOffline($article);
...
}
除了上面讲到的主动下线文章的功能, 一般的文章系统里还会有定时上下线文章的功能, 因为我们在 ArticleService 里包装了下线文章的功能, 所以在设置定时任务时就可以再次调用 setArticleOffline 方法来完成文章下线的功能:
//App\Console\Commands\ArticleCommand
public function __construct(ArticleService $articleService)
{
$this->articleService = $articleService;
parent::__construct();
}
public function handle()
{
$operation = $this->argument('operation');
switch ($operation) {
case 'offline':
$this->setOffline();
break;
default:
break;
}
public function setOffline()
{
......//前置条件查出要下线的文章
$this>articleService>setArticleOffline($article);
......
}
上面两个例子简单说明了我们把代码分层后每个层里应该写什么类型的程序,以及代码分层后在可读性、耦合性、维护成本等方面带来的收益。