通过 Apiato 框架引入构建 API 应用的两种软件架构模式 —— Porto / MVC
接下来的系列中,我们将基于 Apiato 这个 API 框架来演示如何快速构建 API 应用来实现常见功能。
前面我们在 Apiato 快速入门 这篇文档中大概已经了解了 Apiato 的功能特性以及如何创建一个新的应用并对应用接口的访问有了初步的了解,在继续深入介绍该框架所有功能特性之前我们先来了解下 Apiato 框架的架构模式,以便从根本上了解框架的运行原理,从而更好地掌握它。
锲子
我们可以通过以下两种常见的软件架构模式基于 Apiato 来构建应用:
- Porto(Porto 是什么,请参考下面的介绍)
- MVC(Apiato 中的 MVC 和标准的 MVC 模式有些区别,后面我们会深入探讨)
不过,在 Apiato 中我们推荐使用 Porto 模式来构建可扩展的 API(虽然也支持 MVC 模式),Apiato 默认基于 Porto 模式构建,下面我们就来演示如何在 Apiato 分别使用这两种架构模式。
Porto
简介
Porto 是一个现代的软件架构模式(SAP),被设计用于帮助开发者以高可维护性、可复用的方式来管理组织代码。对于中大型项目而言,随着代码越来越复杂,相对 MVC 而言,Porto 是个绝佳的替代方案。
Porto 脱胎自 MVC(Model-View-Controller)、DDD(Domain-driven design)、ADR(Action–domain–responder)、模块化以及分层等架构模式,同时遵循一系列的软件设计原则,如 SOLID(单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)、OOP(面向对象程序设计)、LIFT(这是啥)、DRY(不要重复造轮子)、CoC(约定优于配置)、GRASP(通用责任分配软件模式)、Generalization(泛化)、高内聚、低耦合等。
功能特性
- 解耦:将业务逻辑与底层基础设施以及框架核心分离:底层代码包含框架核心(一般位于
vendor
目录下);中间层代码包含应用通用底层代码(一般位于App/Ship
目录下);上层代码包含业务逻辑代码(一般位于App/Container
目录下) - 模块化:Porto 业务逻辑代码一般会以功能维度划分到多个容器中,这些容器位于
Containers
目录下。容器和模块化中的模块类似,不同之处在于容器件组件可以相互调用。 - 易测试:每个容器都包含一个
Tests
目录用于单元测试。 - 易定位:Porto 架构的一大好处是可以快速在大量代码中找到指定代码片段。
- 可扩展
Apiato 中的 Porto 架构
Apiato 默认就是采用这种架构模式来实现:
包含 Container 层(处理业务逻辑)和 Ship 层(基础代码实现)。
其中,Container 层负责处理业务逻辑,类似模块、DDD(Domain-Driven Design)和插件;Apiato 允许将业务逻辑分割到多个不同的称之为 Container 的文件夹。
而 Ship 层负责应用的基础设施代码,承担着连接更底层框架核心和业务代码的桥梁功能,同时也为上层业务逻辑提供公共的服务方法。
下图可能更加形象:
Apiato 请求生命周期
典型的 API 调用场景如下:
- 用于请求匹配到指定入口路由
- 入口路由调用中间件处理认证
- 调用与路由匹配的控制器动作
- 注入到控制器的请求实例自动应用验证和授权规则
- 控制器调用
Action
并传入请求数据 Action
处理业务逻辑,或者调用相应的任务处理可复用的业务逻辑子集- 任务类处理业务逻辑子集(一个任务只做一件事)
Action
聚合处理结果并返回给控制器- 控制器构建响应并将其通过视图或转化器(
Transformer
)发送给用户
注:关于 Porto 模式的更多细节可以参考其 Github 项目。
容器(Container)开发
移除容器
Apiato 默认自带了一些容器,所有这些容器都是可选的,如果我们不想要其中某个容器,比如 Document
容器,可以直接将容器对应文件夹删除,然后运行 composer update
移除依赖。
创建新容器
我们可以通过 Artisan 命令 php artisan apiato:generate:container
快速创建新容器。
容器约定
- 容器名称遵循驼峰格式
- 命名空间需要和容器名称保持一致
- 理论上可以任意命名容器名称,不过推荐使用主模型的名字作为容器名
MVC
由于 MVC 的流行,以至很多开发者没有意愿花费精力去学习新的架构模式,所以 Apiato 也支持 MVC 架构,但是和标准的 MVC 略微有点差别。
下面我们来看看如何基于 MVC 在 Apiato 之上构建 API 应用。
设置 Apiato MVC 应用
安装新的 Apiato 应用
具体细节可参考 Apiato 快速上手文档。
创建应用
忽略 Apiato 自带的 app/Containers
中提供的所有容器,在 app/Containers
目录下单独新建一个 Application
目录用于存放模型、视图、控制器等。
创建路由文件
在 Laravel 中,路由文件默认存放在 routes
目录下。但是在 Apiato MVC 中,路由文件需要存放在以下路径下:
app/Containers/Application/UI/API/Routes/
(API路由)app/Containers/Application/UI/WEB/Routes/
(Web路由)
在 app/Containers/Application/UI/API/Routes/
下创建 api.php
文件,在 app/Containers/Application/UI/WEB/Routes/
目录下创建 web.php
文件。
另外需要注意的是在路由文件中必须使用 $router->
来替代 Route::
门面:
<?php
// 使用 `$router` 变量
$router->get('/', function () {
return view('welcome');
});
// 不要使用 `Route` 门面
Route::get('/', function () {
return view('welcome');
});
创建控制器
在 Laravel 中,控制器位于 app/Http/Controllers
目录下,和路由类似,在 Apiato MVC 中,控制器需要存放在以下目录中:
app/Containers/Application/UI/API/Controllers/
(处理API请求,必须继承自App\Ship\Parents\Controllers\ApiController
)app/Containers/Application/UI/WEB/Controllers/
(处理Web请求,必须继承自App\Ship\Parents\Controllers\ WebController
)
在 Laravel 中,模型类位于 app
目录下,在 Apiato MVC 中,模型位于 app/Containers/Application/Models/
目录下,且必须继承自 App\Ship\Parents\Models\Model
。
创建视图
在 Laravel 中,视图位于 resources/views/
目录下,在 Apiato MVC 中,视图类需要存放在 app/Containers/Application/UI/WEB/Views/
目录。
创建转化器
在 Laravel 中,转化器位于 app/Transformers/
目录,在 Apiato MVC 中,存放在 app/Containers/Application/UI/API/Transformers/
,用于处理数据格式转化,转化器必须继承自 App\Ship\Parents\Transformers\Transformer
。
创建服务提供者
在 Laravel 中,服务提供者位于 app/Providers/
目录,在 Apiato MVC 中,一般存放在 app/Containers/Application/Providers/
目录下,如果你想要服务提供者可以被自动加载,而无需在 config/app.php
中手动注册,需要将 MainServiceProvider.php
(app/Containers/Application/Providers/MainServiceProvider.php
)重命名,否则需要手动注册。
创建数据库迁移文件
在 Laravel 中,迁移文件位于 database/migrations/
,在 Apiato MVC 中,对应目录是 app/Containers/Application/Data/Migrations
。
创建数据填充器
在 Laravel 中,填充器类位于 database/migrations/
,在 Apiato MVC 中,需要存放在 app/Containers/Application/Data/Seeders/
目录。
如何使用 Apiato 功能
Apiato 的所有功能都以 Action & Task 类的方式提供:
- 每个 Action 类都有且只有一个
run
方法实现某个独立功能; - 每个 Task 类都有且只有一个
run
方法实现某个独立任务
$user = \Apiato::call('Car@GetDriversAction', [$request->id]);
$user = \Apiato::call(GetDriversAction::class, [$request->id]);
$user = $this->call(GetDriversAction::class, [$request->id]);
$user = $this->call('Car@GetDriversAction', [$request->id]);
$user = $action = new GetDriversAction::class; $action->run($request->id);
$user = \App::make(GetDriversAction::class)->run($request->id);
No Comments