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

导语

这篇文章是我使用 Laravel 开发 Web Api 总结的自己一套开发流程。

记录在此,方便回顾。

同时,将自己的经验分享给大家。

这套流程,可能稍微复杂,对软件设计方面支持还有待提高。

欢迎童鞋们,提出指正意见。

模型式 Web Api 接口开发。

先理一下概念,模型式的意思是指:将控制器逻辑做高度封装,放到模型父类中,通过控制器链式调用,简化代码。

  • 关于 Api 接口的数据流程

    调用我们自己开发的 Web Api。无非就两个概念:Request(请求) 和 Response(响应)

    结合请求参数,请求类型(操作类--增、删、改,获取类--查)等问题进行详细分析,所有接口基本分为以下流程

    • 第一步,对请求进行 身份验证,这步操作基本由 JWT 或其他身份验证方式负责,我们基本不用对其进行开发,尽管用就行
    • 第二步,身份通过后,我们首先应该做的工作就是 表单验证
    • 第三步,表单数据验证通过后,有些情况不能直接对表单数据进行使用,还需要对其 进行加工
    • 第四步,拿到加工好的表单数据后,我们才能利用这些数据进行 数据库存储、数据库查询、第三方请求等操作
    • 第五步,如果是获取类请求,我们还需要对数据库进行查询操作,数据库查询可能涉及模型关联、分页等复杂操作,有的还需要请求第三方
    • 第六步,如果是操作类请求,我们要 生成数据模型,保存数据,更改数据,删除数据等
    • 第七步,生成响应数据,在我们刚拿到响应数据时,可能数据不是前端需要的格式,我们还需要对其进行最后的 加工
    • 第八步,发送响应数据给前端
  • 承上启下

    上面是我对一个请求周期的一些见解,不足之处,请多多指正。下面我们具体看一下如何进行 模型式开发

模型父类 Model.php

首先,我们需要一个模型父类,而这个父类继承 Laravel 的模型基类。具体模型子类则继承自此模型父类。说白一点,就是在模型继承中插入一个模型类,能够做到修改 Laravel 模型基类,而不影响其它开发者使用模型基类。

控制器大部分涉及到的代码逻辑,全部封装到这个模型父类中,通过 控制器链式调用,模型具体配置逻辑代码所需参数。从而简化了代码,规范了控制器代码逻辑流程,同时,实现逻辑方法的高度复用。

具体 模型父类 的代码内容如下(稍微有些复杂):

app/Model.php

<?php declare(strict_types=1);

namespace App;

use Closure;
use App\Utils\Auth;
use App\Utils\Helper;
use App\Utils\Response;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Validator;
use Illuminate\Database\Eloquent\Model as BaseModel;

class Model extends BaseModel
{
    // ------------------------ 定义基础属性 --------------------------

    /**
     * 加载辅助 Trait 类:可自行修改定义,根据项目情况可多加载几个
     */
    use Auth,
        Helper,
        Response;

    /**
     * 数据容器:存放请求数据、模型生成临时数据、响应数据等
     *
     * @var array
     */
    protected $data = [];

    /**
     * 应该被转换成原生类型的属性。
     *
     * @var array
     */
    protected $casts = [
        'created_at' => 'string',
        'updated_at' => 'string'
    ];

    /**
     * 数据表前缀
     *
     * @var string
     */
    protected $prefix = 'blogger_';
    protected $_table = '';

    /**
     * Model constructor.
     * @param array $attributes
     */
    public function __construct(array $attributes = [])
    {
        if ($this->_table) {
            $this->table = $this->prefix . $this->_table;
        }
        parent::__construct($attributes);
    }

    // ------------------------ 定义基础数据容器操作方法 --------------------------

    /**
     * 存储数据:向数据容器中合并数据,一般在方法结尾处使用
     *
     * @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
     * @param null $container
     * @return mixed
     */
    protected function extract($keys = '', $default = null, $container = null)
    {
        if (is_string($keys)) {
            $keys = explode('.', $keys);
        }
        $key = array_shift($keys);

        if ($keys) {
            if ($container === null) {
                if (array_key_exists($key, $this->data)) {
                    return $this->extract($keys, $default, $this->data[$key]);
                } else {
                    return $default;
                }
            } else {
                if (array_key_exists($key, $container)) {
                    return $this->extract($keys, $default, $container[$key]);
                } else {
                    return $default;
                }
            }
        } else {
            if ($container === null) {
                if (array_key_exists($key, $this->data)) {
                    if (is_array($this->data[$key])) {
                        return collect($this->data[$key]);
                    }
                    return $this->data[$key];
                } else {
                    return $default;
                }
            } else {
                if (array_key_exists($key, $container)) {
                    if (is_array($container[$key])) {
                        return collect($container[$key]);
                    }
                    return $container[$key];
                } else {
                    return $default;
                }
            }
        }
    }

    /**
     * 数据容器对外接口
     *
     * @param string $keys
     * @param null $default
     * @return array|mixed|null
     */
    public function data(string $keys = '', $default = null)
    {
        return $this->extract($keys, $default);
    }

    /**
     * 追加数据
     *
     * @param $keys
     * @param $data
     * @param null $container
     * @return $this|array|null
     */
    protected function add($keys, $data, $container = null)
    {
        if (is_string($keys)) {
            $keys = explode('.', $keys);
        }
        $key = array_shift($keys);

        if ($keys) {
            if ($container === null) {
                if (array_key_exists($key, $this->data)) {
                    $this->data = Collection::make($this->data)->map(function ($item, $originalKey) use ($key, $keys, $data) {
                        if ($originalKey == $key) {
                            return $this->add($keys, $data, $item);
                        }
                        return $item;
                    })->all();
                } else {
                    $this->data[$key] = $this->add($keys, $data, []);
                }
            } else {
                if (array_key_exists($key, $container)) {
                    return Collection::make($container)->map(function ($item, $originalKey) use ($key, $keys, $data) {
                        if ($originalKey == $key) {
                            return $this->add($keys, $data, $item);
                        }
                        return $item;
                    })->all();
                } else {
                    $container[$key] = $this->add($keys, $data, []);
                    return $container;
                }
            }
        } else {
            if ($container === null) {
                if (array_key_exists($key, $this->data)) {
                    if ($data instanceof Closure) {
                        $this->data[$key] = array_merge($this->data[$key], $data());
                    } else {
                        $this->data[$key] = array_merge($this->data[$key], $data);
                    }
                } else {
                    if ($data instanceof Closure) {
                        $this->data[$key] = $data();
                    } else {
                        $this->data[$key] = $data;
                    }
                }
            } else {
                if (array_key_exists($key, $container)) {
                    if ($data instanceof Closure) {
                        $container[$key] = array_merge($container[$key], $data());
                    } else {
                        $container[$key] = array_merge($container[$key], $data);
                    }
                } else {
                    if ($data instanceof Closure) {
                        $container[$key] = $data();
                    } else {
                        $container[$key] = $data;
                    }
                }
                return $container;
            }
        }
        return $this;
    }

    /**
     * 替换数据
     *
     * @param $keys
     * @param $data
     * @param null $container
     * @return $this|array|null
     */
    protected function replace($keys, $data, $container = null)
    {
        if (is_string($keys)) {
            $keys = explode('.', $keys);
        }
        $key = array_shift($keys);

        if ($keys) {
            if ($container === null) {
                if (array_key_exists($key, $this->data)) {
                    $this->data = Collection::make($this->data)->map(function ($item, $originalKey) use ($key, $keys, $data) {
                        if ($originalKey == $key) {
                            return $this->replace($keys, $data, $item);
                        }
                        return $item;
                    })->all();
                } else {
                    abort(422, '数据错误');
                }
            } else {
                if (array_key_exists($key, $container)) {
                    return Collection::make($container)->map(function ($item, $originalKey) use ($key, $keys, $data) {
                        if ($originalKey == $key) {
                            return $this->replace($keys, $data, $item);
                        }
                        return $item;
                    })->all();
                } else {
                    abort(422, '数据错误');
                }
            }
        } else {
            if ($container === null) {
                if (array_key_exists($key, $this->data)) {
                    if ($data instanceof Closure) {
                        $this->data[$key] = $data();
                    } else {
                        $this->data[$key] = $data;
                    }
                } else {
                    abort(422, '数据错误');
                }
            } else {
                if (array_key_exists($key, $container)) {
                    if ($data instanceof Closure) {
                        $container[$key] = $data();
                    } else {
                        $container[$key] = $data;
                    }
                } else {
                    abort(422, '数据错误');
                }
                return $container;
            }
        }
        return $this;
    }

    /**
     * 销毁数据
     *
     * @param $keys
     * @param null $container
     * @return $this|array|null
     */
    protected function unset($keys, $container = null)
    {
        if (is_string($keys)) {
            $keys = explode('.', $keys);
        }
        $key = array_shift($keys);

        if ($keys) {
            if ($container === null) {
                if (array_key_exists($key, $this->data)) {
                    $this->data = Collection::make($this->data)->map(function ($item, $originalKey) use ($key, $keys) {
                        if ($originalKey == $key) {
                            return $this->unset($keys);
                        }
                        return $item;
                    })->all();
                }
            } else {
                if (array_key_exists($key, $container)) {
                    return Collection::make($container)->map(function ($item, $originalKey) use ($key, $keys) {
                        if ($originalKey == $key) {
                            return $this->unset($keys);
                        }
                        return $item;
                    })->all();
                } else {
                    return $container;
                }
            }
        } else {
            if ($container === null) {
                if (array_key_exists($key, $this->data)) {
                    unset($this->data[$key]);
                }
            } else {
                if (array_key_exists($key, $container)) {
                    unset($container[$key]);
                }
                return $container;
            }
        }
        return $this;
    }

    // ------------------------ 请求开始:验证数据,加工数据 --------------------------

    /**
     * 请求:使用模型的入口
     *
     * @param string $method 控制器方法名称
     * @param array $request 请求的原始数据
     * @param BaseModel|null $origin fetch 起源模型。
     * @return Model
     */
    protected function request(string $method, array $request = [], BaseModel $origin = null): Model
    {
        return $this->compact(compact('method', 'request', 'origin'))
            ->validate()
            ->process()
            ->trim();
    }

    /**
     * 验证数据:根据 rule 方法返回的规则,进行数据验证
     *
     * @return Model
     */
    protected function validate(): Model
    {
        $rules = $this->rule();
        if (!$rules) return $this;
        $validator = Validator::make($this->extract('request')->all(), $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;
    }

    /**
     * 修剪参数
     *
     * @return Model
     */
    protected function trim(): Model
    {
        return $this->replace('request', $this->extract('request')->map(function ($value) {
            if (is_string($value)) {
                return trim($value);
            }
            return $value;
        })->all());
    }

    // ------------------------ 操作类请求:映射字段、生成模型、保存数据 --------------------------

    /**
     * 生成数据模型:此方法定义一个统一名为 model 的对外接口,建议在控制器中调用
     *
     * @return Model
     */
    public function model(): Model
    {
        return $this->createDataModel();
    }

    /**
     * 生成数据模型(内置方法)
     *
     * @return Model
     */
    protected function createDataModel(): Model
    {
        $request = $this->extract('request');
        $maps = $this->map();
        $model = $this->extract('origin') ?: $this;
        if (!$maps) return $this;
        foreach ($request->keys() as $v) {
            if (array_key_exists($v, $maps)) {
                $k = $maps[$v];
                $model->$k = $request[$v];
            }
        }
        return $model;
    }

    /**
     * 数据映射:请求字段 => 数据库字段 的映射,用以生成含有数据的数据表模型
     *
     * 注:此方法需根据情况,在子模型中重写
     *
     * @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 {
            abort(422, '保存失败');
        }
    }

    // ------------------------ 获取类请求:根据规则和映射获取数据,加工数据,返回数据 --------------------------

    /**
     * 取出数据:从数据库获取数据,建议在控制器中调用
     *
     * @param Closure|null $callback
     * @return Model
     * @throws \ReflectionException
     */
    public function fetch(Closure $callback = null): Model
    {
        $map = $this->fetchMap();
        if (!$map) return $this;
        $gathers = $this->isShow()->getCollection()->getChainRule($this->extract('origin') ?: $this, $map['chain']);
        if ($callback instanceof Closure) {
            $gathers = $callback($gathers);
        }
        $response = [];
        foreach ($gathers as $index => $gather) {
            $response[$index] = $this->relationRecursion($map['data'], $gather, []);
        }
        return $this->compact(compact('response'));
    }

    /**
     * 关系递归取值
     *
     * @param array $data
     * @param Model $model
     * @param array $response
     * @return array
     * @throws \ReflectionException
     */
    protected function relationRecursion (array $data, Model $model, array $response)
    {
        foreach ($data as $key => $value) {
            if ($key == '__method__') {
                foreach ($value as $method => $param) {
                    if ($method == '__model__') {
                        $param($model);
                    } else {
                        $model->$method(...$param);
                    }
                }
            } else if (is_array($value)) {
                foreach ($model->$key as $_index => $relevancy) {
                    $response[$key][$_index] = $this->relationRecursion($value, $relevancy, []);
                }
            } else if ($value instanceof Closure) {
                $params = (new \ReflectionObject($value))
                        ->getMethod('__invoke')
                        ->getParameters();
                if (!count($params)) {
                    $response[$key] = $value();
                    continue;
                }
                $params = collect($params)->map(function ($param) use ($model) {
                    $field = $param->name;
                    if ($field == 'model') {
                        return $model;
                    }
                    return $model->$field;
                })->all();
                $response[$key] = $value(...$params);
            } else {
                $response[$key] = $this->getDataRule($model, explode('.', $value));
            }
        }
        return $response;
    }

    /**
     * 区分展示详情或展示列表
     *
     * @return Model
     */
    protected function isShow(): Model
    {
        $__isShow__ = $this->id ? true : false;
        return $this->compact(compact('__isShow__'));
    }

    /**
     * 获取集合
     *
     * @return Model
     */
    protected function getCollection(): Model
    {
        return $this;
    }

    /**
     * 取数据的映射规则
     *
     * 注:此方法需根据情况,在子模型中重写
     *
     * @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);

        $k = '';
        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);
        } else {
            $this->epilogue();
        }

        return $this->send();
    }

    /**
     * 操作类请求设置操作成功的 message
     *
     * @param null $message
     * @return Model
     */
    protected function setMessage($message = null): Model
    {
        $response = [
            'code' => 200,
            'message' => $message !== null ? $message : 'success',
        ];
        return $this->compact(compact('response'));
    }

    /**
     * 收尾:对获取的数据进行最后加工
     *
     * @return Model
     */
    protected function epilogue(): Model
    {
        return $this->replace('response', $this->success($this->extract('response')));
    }

    /**
     * 发送数据
     *
     * @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);
    }
}

来张图,加深一下认识

file

需要的前期准备

  • 增加 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',
    ];

    如图:

    file

  • 修改 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);
      }
    }
    

    如图:

    file

具体使用,我们来举个例子

  • 数据表结构如下图

    file

    这是一个关于文章的管理数据结构,其中文章基本信息和文章内容是分离两个表,做一对一关系;而文章与评论是一对多关系。这样做的好处是,获取文章列表时,无需查文章内容,有助于提高查询速度,因为内容一般是很大的。

  • 先看一下模型类和控制器的位置

    file

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

file

file

file

file

file

本作品采用《CC 协议》,转载必须注明作者和本文链接
时空扭曲, 到底长啥样.
本帖由系统于 5年前 自动加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 5
Toiu

以 laravel本身提供的能力, 控制器已经可以非常精炼了. 个人觉得这样封装的意义不算大

5年前 评论
falling-ts

@Toiu 小型项目,意义不是很大。主要针对项目代码量高,路由几十甚至上百时,统一格式,无论开发还是修改,很爽的。简单改改数组配置就行了。

5年前 评论

apiato项目 也挺好的

5年前 评论

比较理想的情况下,这样用起来比较爽,往往业务逻辑是高度耦合的……

5年前 评论

没看明白呢~

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!