转载https://laravel-china.org/articles/20183
导语
这篇文章是我使用 Laravel 开发 Web Api 总结的自己一套开发流程。
记录在此,方便回顾。
同时,将自己的经验分享给大家。
这套流程,可能稍微复杂,对软件设计方面支持还有待提高。
欢迎童鞋们,提出指正意见。
模型式 Web Api 接口开发。
先理一下概念,模型式的意思是指:将控制器逻辑做高度封装,放到模型父类中,通过控制器链式调用,简化代码。
关于 Api 接口的数据流程
调用我们自己开发的 Web Api。无非就两个概念:Request(请求) 和 Response(响应)
结合请求参数,请求类型(操作类–增、删、改,获取类–查)等问题进行详细分析,所有接口基本分为以下流程
第一步,对请求进行 身份验证,这步操作基本由 JWT 或其他身份验证方式负责,我们基本不用对其进行开发,尽管用就行
第二步,身份通过后,我们首先应该做的工作就是 表单验证
第三步,表单数据验证通过后,有些情况不能直接对表单数据进行使用,还需要对其 进行加工
第四步,拿到加工好的表单数据后,我们才能利用这些数据进行 数据库存储、数据库查询、第三方请求等操作
第五步,如果是获取类请求,我们还需要对数据库进行查询操作,数据库查询可能涉及模型关联、分页等复杂操作,有的还需要请求第三方
第六步,如果是操作类请求,我们要 生成数据模型,保存数据,更改数据,删除数据等
第七步,生成响应数据,在我们刚拿到响应数据时,可能数据不是前端需要的格式,我们还需要对其进行最后的 加工
第八步,发送响应数据给前端
承上启下
上面是我对一个请求周期的一些见解,不足之处,请多多指正。下面我们具体看一下如何进行 模型式开发
模型父类 Model.php
首先,我们需要一个模型父类,而这个父类继承 Laravel 的模型基类。具体模型子类则继承自此模型父类。说白一点,就是在模型继承中插入一个模型类,能够做到修改 Laravel 模型基类,而不影响其它开发者使用模型基类。
控制器大部分涉及到的代码逻辑,全部封装到这个模型父类中,通过 控制器链式调用,模型具体配置逻辑代码所需参数。从而简化了代码,规范了控制器代码逻辑流程,同时,实现逻辑方法的高度复用。
具体 模型父类 的代码内容如下(稍微有些复杂):
app/Model.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model as BaseModel;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Http\JsonResponse;
use App\Utils\Helper;
use App\Utils\Response;
class Model extends BaseModel
{
// ------------------------ 定义基础属性 --------------------------
/**
* 加载辅助 Trait 类:可自行修改定义,根据项目情况可多加载几个
*/
use Helper, Response;
/**
* 数据容器:存放请求数据、模型生成临时数据、响应数据等
*
* @var array
*/
protected $data = [];
/**
* 应该被转换成原生类型的属性。
*
* @var array
*/
protected $casts = [
'created_at' => 'string',
'updated_at' => 'string'
];
// ------------------------ 定义基础数据容器操作方法 --------------------------
/**
* 存储数据:向数据容器中合并数据,一般在方法结尾处使用
*
* @param array $data 经过系统 compact 方法处理好的数组
* @return Model
*/
protected function compact(array $data) :Model
{
$this->data = $data + $this->data;
return $this;
}
/**
* 获取数据:从数据容器中获取数据,一般在方法开始,或方法中使用
*
* @param string $keys 支持点语法,访问多维数组
* @param null $default
* @return array|mixed|null
*/
protected function extract(string $keys = '', $default = null)
{
$result = $this->data;
if ($keys == '') {
return $result;
}
$keys = explode('.', $keys);
foreach ($keys as $key) {
if (!array_key_exists($key, $result)) {
return $default;
}
$result = $result[$key];
}
return $result;
}
/**
* 数据容器对外接口
*
* @param string $keys
* @param null $default
* @return array|mixed|null
*/
public function data(string $keys = '', $default = null)
{
return $this->extract($keys, $default);
}
// ------------------------ 请求开始:验证数据,加工数据 --------------------------
/**
* 请求:使用模型的入口
*
* @param string $method 控制器方法名称
* @param array $request 请求的原始数据
* @param BaseModel|null $auth 核心入口模型,一般由授权模型获得。
* @return Model
*/
protected function request(string $method, array $request = [], BaseModel $auth = null) :Model
{
return $this->compact(compact('method', 'request', 'auth'))
->validate()
->process();
}
/**
* 验证数据:根据 rule 方法返回的规则,进行数据验证
*
* @return Model
*/
protected function validate() :Model
{
$rules = $this->rule();
if (!$rules) return $this;
$validator = Validator::make($this->extract('request'), $rules['rules'], config('message'), $rules['attrs']);
if (isset($rules['sometimes']) && count($rules['sometimes'])) {
foreach ($rules['sometimes'] as $v) {
$validator->sometimes(...$v);
}
}
$validator->validate();
return $this;
}
/**
* 数据验证规则:此方法将在 validate 方法中被调用,用以获取验证规则
*
* 注:此方法需根据情况,在子模型中重写
*
* @return array
*/
protected function rule() :array
{
return [];
}
/**
* 加工请求:请求数据验证通过后,用此方法进行数据加工与方法派遣操作
*
* 注:此方法需根据情况,在子模型中重写
*
* @return Model
*/
protected function process() :Model
{
return $this;
}
// ------------------------ 操作类请求:映射字段、生成模型、保存数据 --------------------------
/**
* 生成数据模型:此方法定义一个统一名为 model 的对外接口,建议在控制器中调用
*
* @return Model
*/
public function model() :Model
{
return $this->createDataModel();
}
/**
* 生成数据模型(内置方法)
*
* @return Model
*/
protected function createDataModel() :Model
{
$request = $this->extract('request');
$maps = $this->map();
if (!$maps) return $this;
foreach (array_keys($request) as $v) {
if (array_key_exists($v, $maps)) {
$k = $maps[$v];
$this->$k = $request[$v];
}
}
return $this;
}
/**
* 数据映射:请求字段 => 数据库字段 的映射,用以生成含有数据的数据表模型
*
* 注:此方法需根据情况,在子模型中重写
*
* @return array
*/
protected function map() :array
{
return [];
}
/**
* 保存模型:同 save 方法,可重写 save 逻辑,而不影响原 save,保证其它模块正常工作
*
* @param array $options
* @return Model
*/
public function reserve(array $options = []) :Model
{
if ($this->save($options)) {
return $this;
} else {
DB::rollBack();
abort(422, '保存失败');
}
}
// ------------------------ 获取类请求:根据规则和映射获取数据,加工数据,返回数据 --------------------------
/**
* 取出数据:从数据库获取数据,建议在控制器中调用
*
* @return Model
*/
public function fetch() :Model
{
$response = [];
$map = $this->fetchMap();
if (!$map) return $this;
$gathers = $this->isShow()->getChainRule($this, $map['chain']);
foreach ($gathers as $k => $gather) {
foreach ($map['data'] as $_k => $v) {
if (is_array($v)) {
foreach ($gather->$_k as $n => $relevancy) {
foreach ($v as $m => $_v) {
$response[$k][$_k][$n][$m] = $this->getDataRule($relevancy, explode('.', $_v));
}
}
} else {
$response[$k][$_k] = $this->getDataRule($gather, explode('.', $v));
}
}
}
return $this->compact(compact('response'))->epilogue();
}
/**
* 区分展示详情或展示列表
*
* @return Model
*/
protected function isShow() :Model
{
$isShow = $this->id ? true : false;
return $this->compact(compact('isShow'));
}
/**
* 取数据的映射规则
*
* 注:此方法需根据情况,在子模型中重写
*
* @return array
*/
protected function fetchMap() :array
{
return [];
}
/**
* 递归链式操作:封装查询构造器,根据数组参数调用查询构造顺序。
*
* @param array $chains
* @return object
*/
protected function getChainRule($model, array $chains)
{
if (!$chains) {
if ($this->extract('isShow')) {
return Collection::make([$model]);
}
return $model->get();
}
$chain = array_shift($chains);
foreach ($chain as $k => $v) {
$model = $model->$k(...$v);
}
if ($k == 'paginate') {
$page = [
'total' => $model->total(),
'lastPage' => $model->lastPage(),
];
$this->compact(compact('page'));
return $model;
} else if ($chains) {
return $this->getChainRule($model, $chains);
} else if ($this->extract('isShow')) {
return Collection::make([$model]);
} else {
return $model->get();
}
}
/**
* 递归取值:取关联模型的数据
*
* @return mixed
*/
protected function getDataRule($gather, array $rules)
{
$rule = array_shift($rules);
$gather = $gather->$rule;
if ($rules) {
return $this->getDataRule($gather, $rules);
} else {
return $gather;
}
}
// ------------------------ 响应数据 --------------------------
/**
* 发送响应:请在控制器调用,操作类请求传 message,获取类请求不要传 message
*
* @param null $message
* @return JsonResponse
*/
public function response($message = null) :JsonResponse
{
if ($message !== null) {
$this->setMessage($message);
}
return $this->send();
}
/**
* 操作类请求设置操作成功的 message
*
* @return Model
*/
protected function setMessage($message = null) :Model
{
$response = [
'code' => 200,
'message' => $message !== null ? $message : '操作成功',
];
return $this->compact(compact('response'))->epilogue();
}
/**
* 收尾:对获取的数据进行最后加工
*
* @return Model
*/
protected function epilogue() :Model
{
return $this;
}
/**
* 发送数据
*
* @return JsonResponse
*/
protected function send() :JsonResponse
{
return response()->json($this->extract('response'));
}
/**
* Handle dynamic method calls into the model.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
if (in_array($method, ['increment', 'decrement', 'request'])) {
return $this->$method(...$parameters);
}
return $this->newQuery()->$method(...$parameters);
}
}
来张图,加深一下认识
需要的前期准备
增加 message.php 配置文件
相信仔细看的童鞋,validate 方法需要一个中文配置文件 message.php
具体代码如下:
config/message.php
<?php
return [
'accepted' => ':attribute必须为yes、on、 1、或 true',
'after' => ':attribute必须为:date之后',
'alpha' => ':attribute必须完全是字母的字符',
'alpha_dash' => ':attribute应为字母、数字、破折号( - )以及下划线( _ )',
'alpha_num' => ':attribute必须完全是字母、数字',
'array' => ':attribute必须为数组',
'before' => ':attribute必须为:date之后',
'between' => ':attribute大小必须在:min与:max之间',
'confirmed' => '两次:attribute不一致',
'date' => ':attribute不是日期格式',
'date_format' => ':attribute必须是:format格式',
'different:field' => ':attribute不能与:field相等',
'email' => ':attribute不是邮箱格式',
'integer' => ':attribute必须为整数',
'max' => ':attribute最大为:max',
'min' => ':attribute最小为:min',
'numeric' => ':attribute必须为数字',
'regex' => ':attribute格式错误',
'required' => ':attribute不能为空',
'required_if' => ':attribute不能为空',
'required_with' => ':attribute不能为空',
'required_with_all' => ':attribute不能为空',
'required_without' => ':attribute不能为空',
'required_without_all' => ':attribute不能为空 ',
'size' => ':attribute必须等于:value',
'string' => ':attribute必须为字符串',
'unique' => ':attribute已存在',
'exists' => ':attribute不存在',
'json' => ':attribute必须是JSON字符串',
'image' => ':attribute必须是一个图像',
'url' => ':attribute必须是合法的URL',
];
如图:
修改 App\Exceptions\Handler 类。
主要拦截 表单验证 异常,通过 Api 返回的方式,发送给前端
app/Exceptions/Handler.php
<?php
namespace App\Exceptions;
use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\JsonResponse;
use Illuminate\Validation\ValidationException;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that are not reported.
*
* @var array
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* @var array
*/
protected $dontFlash = [
'password',
'password_confirmation',
];
/**
* Report or log an exception.
*
* @param \Exception $exception
* @return void
*/
public function report(Exception $exception)
{
parent::report($exception);
}
/**
* 公用返回格式
* @param string $message
* @param int $code
* @return JsonResponse
*/
protected function response(string $message, int $code) :JsonResponse
{
$response = [
'code' => $code,
'message' => $message,
];
return response()->json($response, 200);
}
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $exception
* @return Object
*/
public function render($request, Exception $exception)
{
// 拦截表单验证异常,修改返回方式
if ($exception instanceof ValidationException) {
$message = array_values($exception->errors())[0][0];
return $this->response($message, 422);
}
return parent::render($request, $exception);
}
}
如图
具体使用,我们来举个例子
数据表结构如下图
这是一个关于文章的管理数据结构,其中文章基本信息和文章内容是分离两个表,做一对一关系;而文章与评论是一对多关系。这样做的好处是,获取文章列表时,无需查文章内容,有助于提高查询速度,因为内容一般是很大的。
先看一下模型类和控制器的位置
Api 路由
routes/api.php
<?php
use Illuminate\Http\Request;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
// Laravel 自带路由,不用管
Route::middleware('auth:api')->get('/user', function (Request $request) {
return $request->user();
});
// 文章管理 REST API。
Route::apiResource('article', 'ArticleController');
控制器代码
app/Http/Controllers/ArticleController.php
<?php
namespace App\Http\Controllers;
use App\Models\Article;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ArticleController extends Controller
{
/**
* Display a listing of the resource.
*
* @return JsonResponse
*/
public function index(Request $request) :JsonResponse
{
return Article::request('index', $request->all())
->fetch()
->response();
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return JsonResponse
*/
public function store(Request $request) :JsonResponse
{
return Article::request('store', $request->all())
->model()
->reserve()
->response('保存成功');
}
/**
* Display the specified resource.
*
* @param Article $article
* @return JsonResponse
*/
public function show(Request $request, Article $article) :JsonResponse
{
return $article->request('show', $request->all())
->fetch()
->response();
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param Article $article
* @return JsonResponse
*/
public function update(Request $request, Article $article) :JsonResponse
{
return $article->request('update', $request->all())
->model()
->reserve()
->response('修改成功');
}
/**
* Remove the specified resource from storage.
*
* @param Article $article
* @throws
* @return JsonResponse
*/
public function destroy(Article $article) :JsonResponse
{
return $article->request('destroy', [])
->delete()
->response('删除成功');
}
}
Article 模型代码
app/Models/Article.php
<?php
namespace App\Models;
use App\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Facades\DB;
class Article extends Model
{
/**
* 与 Content 一对一
*
* @return Relation
*/
public function content() :Relation
{
return $this->hasOne(Content::class)->withDefault();
}
/**
* 与 Comment 一对多
*
* @return Relation
*/
public function comments() :Relation
{
return $this->hasMany(Comment::class);
}
/**
* 数据验证规则:此方法将在 validate 方法中被调用,用以获取验证规则
*
* @return array
*/
protected function rule() :array
{
switch ($this->extract('method')) {
case 'store':
return [
'rules' => [
'title' => 'required|string|max:140',
'content' => 'required|string',
],
'attrs' => [
'title' => '文章标题',
'content' => '文章内容',
]
];
break;
case 'update':
return [
'rules' => [
'title' => 'required_without:content|string|max:140',
'content' => 'required_without:title|string',
],
'attrs' => [
'title' => '文章标题',
'content' => '文章内容',
]
];
break;
case 'index':
case 'show':
return [
'rules' => [
'page' => 'required|integer|min:1',
'num' => 'sometimes|integer|min:1',
],
'attrs' => [
'page' => '页码',
'num' => '每页数量',
]
];
break;
}
return [];
}
/**
* 加工请求:请求数据验证通过后,用此方法进行数据加工与方法派遣操作
*
* @return Model
*/
protected function process() :Model
{
switch ($this->extract('method')) {
case 'store':
case 'update':
$request = array_map(function ($item) {
return trim($item);
}, $this->extract('request'));
return $this->compact(compact('request'));
break;
}
return $this;
}
/**
* 数据映射:请求字段 => 数据库字段 的映射,用以生成含有数据的数据表模型
*
* @return array
*/
protected function map() :array
{
return [
'title' => 'title',
];
}
/**
* 保存模型:同 save 方法,可重写 save 逻辑,而不影响原 save,保证其它模块正常工作
*
* @param array $options
* @return Model
*/
public function reserve(array $options = []) :Model
{
DB::beginTransaction();
if (
$this->save($options)
&&
$this->content->request('store', $this->extract('request'))
->model()
->save()
) {
DB::commit();
return $this;
} else {
DB::rollBack();
abort(422, '保存失败');
}
}
/**
* 删除
*
* @return $this|bool|null
* @throws \Exception
*/
public function delete()
{
DB::beginTransaction();
if (
$this->content->delete()
&&
parent::delete()
) {
DB::commit();
return $this;
} else {
DB::rollBack();
abort(422, '删除失败');
}
}
/**
* 取数据的映射规则
*
* @return array
*/
protected function fetchMap() :array
{
switch ($this->extract('method')) {
case 'index':
return [
'chain' => [
['paginate' => [$this->extract('request.num', 10)]]
],
'data' => [
'id' => 'id',
'title' => 'title',
'c_time' => 'created_at',
'u_time' => 'updated_at',
]
];
break;
case 'show':
return [
'chain' => [
['load' => ['content']],
['load' => [['comments' => function ($query) {
$paginate = $query->paginate($this->extract('request.num', 10));
$page = [
'total' => $paginate->total(),
];
$this->compact(compact('page'));
}]]],
],
'data' => [
'id' => 'id',
'title' => 'title',
'content' => 'content.content',
'comments' => [
'id' => 'id',
'comment' => 'comment',
],
'c_time' => 'created_at',
'u_time' => 'updated_at',
]
];
break;
}
return [];
}
/**
* 收尾:对获取的数据进行最后加工
*
* @return Model
*/
protected function epilogue() :Model
{
switch ($this->extract('method')) {
case 'index':
$response = [
'code' => 200,
'message' => '获取成功',
'data' => $this->extract('response'),
'total' => $this->extract('page.total'),
'lastPage' => $this->extract('page.lastPage'),
];
return $this->compact(compact('response'));
break;
case 'show':
$response = ['comments' => [
'total' => $this->extract('page.total'),
'data' => $this->extract('response.0.comments')
]] + $this->extract('response.0');
return $this->compact(compact('response'));
break;
}
return $this;
}
}
Content 模型
app/Models/Content.php
<?php
namespace App\Models;
use App\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
class Content extends Model
{
/**
* @return Relation
*/
public function article() :Relation
{
return $this->belongsTo(Article::class);
}
/**
* 数据映射:请求字段 => 数据库字段 的映射,用以生成含有数据的数据表模型
*
* @return array
*/
protected function map() :array
{
return [
'content' => 'content',
];
}
}
Comment 模型
app/Models/Comment.php
<?php
namespace App\Models;
use App\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
class Comment extends Model
{
/**
* 与 Article 多对一
*
* @return Relation
*/
public function article() :Relation
{
return $this->belongsTo(Article::class);
}
}
说一点
上面代码,我没有对 评论 写控制器,测试时,是在数据库手动添加的评论
最后,我们看一下 postman 检测结果