Laravel 之道特別篇 3: 模型式 Web API 介面開發流程

yuanshang發表於2018-11-24

導語

這篇文章是我使用 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 協議》,轉載必須註明作者和本文連結
我們是一群被時空壓迫的孩子。 ---- 愛因斯坦

相關文章