Laravel 之道特别篇 3: 模型式 Web API 接口开发流程

转载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 检测结果

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值