YII 初體驗 —— 搭建一個簡單的 Todo List 系統

曬兜斯發表於2022-02-03

Yii 是一個高效能,基於元件的 PHP 框架,用於快速開發現代 Web 應用程式。

今天,我本著體驗 Yii2 的想法,準備使用 Yii2 從 0 到 1 來搭建一個 Todo List,並完成以下功能:

  1. 可以基於某個 key 建立 Todo Item,然後根據 key 查詢對應的 Todo Item
  2. 可以置頂、完成、刪除單條 Todo Item,置頂的 Todo Item 將排列在最前面,完成的 Todo Item 將排列在最後面。

初始化 YII 倉庫

使用下面的命令即可初始化一個 YII 的倉庫。

composer create-project --prefer-dist yiisoft/yii2-app-basic basic

但是,我的 mac 通過這個方法老是連不上網路,安裝某些依賴失敗,所以這裡選擇第二種方式。(如下)

yiiframework 下載歸檔檔案,然後解壓到你要放置的專案目錄中。

在下載解壓完成後,需要先修改 config/web.php 檔案,給 cookieValidationKey 配置項新增一個金鑰(隨便輸入一個值就可以),以便專案能夠正常啟動。

在專案初始化完成以後,我們使用下面這個命令執行專案吧。

php yii serve --port=8888

然後我們開啟 http://localhost:8888,看到我們的頁面已經成功啟動啦!(如下圖)

image

初始化資料模型

接下來,我們來初始化我們的資料模型。

我們需要建立的欄位有下面這些:

  • id:自增主鍵;
  • key:Todo 的 key;
  • title:Todo 的標題;
  • is_completed:Todo 是否完成;
  • is_top:Todo 是否置頂;
  • is_deleted:Todo 是否刪除;

而上面這些欄位中,我們最多的場景是通過 key 來撈出相關的 Todo Item,所以應該給 key 建立一個普通索引。

綜上,我們的 sql 語句應該是這樣的:

CREATE TABLE IF NOT EXISTS `todos` (
    `id` int PRIMARY KEY AUTO_INCREMENT,
    `key` varchar(64) NOT NULL DEFAULT '',
    `title` varchar(64) NOT NULL DEFAULT '',
    `is_top` tinyint(1) NOT NULL DEFAULT 0,
    `is_completed` tinyint(1) NOT NULL DEFAULT 0,
    `is_deleted` tinyint(1) NOT NULL DEFAULT 0,
    index `key`(`key`)
) engine=InnoDB CHARSET=utf8;

在資料庫中執行該條 SQL,建立對應的資料表。

然後,我們還可以通過下面這條語句檢視我們建立的索引。

SHOW INDEX FROM `todos`;

image

處理 Todo 業務邏輯

在資料表建立成功後,我們就準備開始寫 Todo 相關的業務邏輯了。

在此之前,我們還需要做一些 Yii 的配置初始化工作。

初始化 Yii 配置

首先,我們的 php 服務需要連線資料庫,所以你需要先配置你的資料庫連線,也就是 config/db.php

  • 資料庫配置
<?php

return [
    'class' => 'yii\db\Connection',
    'dsn' => 'mysql:host=[mysql伺服器地址];port=[mysql埠];dbname=[資料庫名稱]',
    'username' => '[資料庫使用者名稱]',
    'password' => '[資料庫密碼]',
    'charset' => 'utf8',
    'attributes' => [
        // 查詢時將 int 型別按原型別返回
        PDO::ATTR_STRINGIFY_FETCHES => false,
        PDO::ATTR_EMULATE_PREPARES => false
    ]
];
  • URL 美化配置

然後,我們再來配置一下 URL 美化,這樣就可以按照標準的 restful 風格進行訪問了,調整 config/web.php 中的 urlManager 即可。(如下)

'urlManager' => [
  'enablePrettyUrl' => true,
  'enableStrictParsing' => false,
  'showScriptName' => false,
  'rules' => [
  ],
],
  • JSON 入參配置

然後,我們還需要修改一下 request 的配置,以便接受 application/json 的入參。

'components' => [
    ...
    'request' => [
        'parsers' => [
            'application/json' => 'yii\web\JsonParser',
        ]
    ],
    ...
]

修改完了配置後,可以重啟一下你的專案。

建立 TodoModel + TodoRepository + TodoService + TodoController

我們先來建立 Todo 的資料實體類 —— TodoModel,這個模型將會貫穿 Todo List 的整個生命週期。

<?php
namespace app\models;

use Yii;
use Yii\base\Model;

class TodoModel extends Model {
    public $id;
    public $key;
    public $title;
    public $is_top;
    public $is_completed;
    public $is_deleted;
}

然後,我們建立 TodoRepository,用於資料持久化。 —— SQL 寫在這裡。

<?php

namespace app\repositories;

use app\models\TodoModel;

class TodoRepository {
    public static function selectAll(TodoModel $todo) {

    }

    public static function insertOne(TodoModel $todo) {

    }

    public static function deleteOne(TodoModel $todo) {

    }

    public static function updateOne(TodoModel $todo) {

    }
}

接下來,我們來建立 TodoService,用於處理業務邏輯。 —— 所有業務邏輯放在這裡。

<?php

namespace app\services;

use app\models\TodoModel;
use app\repositories\TodoRepository;

class TodoService {
    public function getAllTodo(TodoModel $model) {
        return TodoRepository::selectAll($model);
    }

    public function addTodo(TodoModel $model) {
        
    }

    public function topTodo(TodoModel $model) {

    }

    public function completeTodo(TodoModel $model) {

    }

    public function deleteTodo(TodoModel $model) {

    }
}

最後,我們建立 TodoController,用於控制業務流程和處理介面請求。 —— 與客戶端互動的邏輯放在這裡。

<?php

namespace app\controllers;

use Yii;

use yii\rest\ActiveController;
use yii\web\Response;
use app\services\TodoService;
use app\models\TodoModel;

class TodoController extends ActiveController
{
    public TodoService $todoService;

    public function __construct($id, $module, $config = [])
    {
        parent::__construct($id, $module, $config);
        this.$todoService = new TodoService();
    }

    // 將響應資料轉成 JSON
    public function behaviors()
    {
        return [
            [
                'class' => \yii\filters\ContentNegotiator::className(),
                'only' => ['index', 'view'],
                'formats' => [
                    'application/json' => \yii\web\Response::FORMAT_JSON,
                ],
            ],
        ];
    }

    public function actionGetTodoList() {
      
    }
}

將基礎的 TodoModel + TodoRepository + TodoService + TodoController,也就是 MVC 模型準備好了以後,我們就準備開始新增真實有效的業務邏輯了。

查詢對應 keyTodo List

我們現在準備根據 key 來查詢對應的 todo 列表。

我們首先來編輯 TodoRepositoryselectAll,將對應的 SQL 查詢邏輯寫好。

class TodoRepository {
  /**
   * @throws \yii\db\Exception
   */
  public static function selectAll(TodoModel $todo) {
    $db = Yii::$app->db;
    // 組裝 SQL 語句,查詢對應 key 且未刪除的資料
    // 查詢的資料按照 `是否完成` 升序排列,按照 `是否置頂` 降序排列
    $sql = "SELECT * 
            FROM `todos`
            WHERE `key` = :code AND `is_deleted` = 0
            ORDER BY is_completed ASC, is_top DESC";
    return $db->createCommand($sql)->bindValue(':code', $todo->key)->queryAll();
  }
  //...
}

TodoRepositorySQL 語句編輯完成後,我們可以在資料庫中執行試試。(如下圖)

image

從上圖可以看出,該 SQL 按我們預想的執行 —— 使用 key 作為索引,只檢索了 4 條資料(此時資料庫有 10 條資料)。

這條 SQL 還涉及到了 Using filesort,我還沒有想到比較好的優化方案,大家可以嘗試一下優化這條 SQL。

我們來編輯 TodoControlleractionGetTodoList 方法即可(TodoService 不需要修改)。

public function actionGetTodoList() {
    $model = new TodoModel();
    $params = Yii::$app->request->get();
    // 取出 query 引數中的 key 欄位
    $model->key = $params['key'];
    return $this->todoService->getAllTodo($model);
}

在邏輯補充完後,開啟頁面 http://localhost:8888/todo/get-todo-list?key=test 驗證一下效果吧。(如下圖)

image

從上圖可以看出,資料按照我們預期的篩選和排序返回了!

補全剩餘業務邏輯 —— 增刪改

接下來,就是依次將 增刪改 的邏輯加上就好了,這應該是最簡單也是最經典的 CRUD 了。(如下)

  • TodoModel.php
<?php

namespace app\models;

use Yii;
use yii\base\Model;

class TodoModel extends Model
{
    public $id;
    public $key = '';
    public $title = '';
    public $is_top = 0;
    public $is_completed = 0;
    public $is_deleted = 0;

    public function rules()
    {
        return [
            [['id', 'key', 'title'], 'required']
        ];
    }
}
  • TodoRepository.php
<?php

namespace app\repositories;

use Yii;
use app\models\TodoModel;

class TodoRepository
{
    /**
     * @throws \yii\db\Exception
     */
    public static function selectAll(TodoModel $todo)
    {
        $db = Yii::$app->db;
        // 組裝 SQL 語句,查詢對應 key 且未刪除的資料
        // 查詢的資料按照 `是否完成` 升序排列,按照 `是否置頂` 降序排列
        $sql = "SELECT * 
                FROM `todos`
                WHERE `key` = :code AND `is_deleted` = 0
                ORDER BY is_completed ASC, is_top DESC";
        return $db->createCommand($sql)->bindValue(':code', $todo->key)->queryAll();
    }

    /**
     * @throws \yii\db\Exception
     */
    public static function insertOne(TodoModel $todo)
    {
        $db = Yii::$app->db;
        return $db->createCommand()->insert('todos', $todo)->execute();
    }

    /**
     * @throws \yii\db\Exception
     */
    public static function updateOne(array $todoData, string $id)
    {
        $db = Yii::$app->db;
        return $db
                ->createCommand()
                ->update('todos', $todoData, "id = :id")
                ->bindValue("id", $id)
                ->execute();
    }
}
  • TodoService.php
<?php

namespace app\services;

use app\models\TodoModel;
use app\repositories\TodoRepository;

class TodoService
{
    public function getAllTodo(TodoModel $model)
    {
        return TodoRepository::selectAll($model);
    }

    public function addTodo(TodoModel $model)
    {
        return TodoRepository::insertOne($model);
    }

    public function topTodo(TodoModel $model)
    {
        return TodoRepository::updateOne([
            'is_top' => 1
        ], $model->id);
    }

    public function completeTodo(TodoModel $model)
    {
        return TodoRepository::updateOne([
            'is_completed' => 1
        ], $model->id);
    }

    public function deleteTodo(TodoModel $model)
    {
        return TodoRepository::updateOne([
            'is_deleted' => 1
        ], $model->id);
    }
}
  • TodoController.php
<?php

namespace app\controllers;

use Yii;

use yii\web\Controller;
use app\services\TodoService;
use app\models\TodoModel;

class TodoController extends Controller
{
    public $todoService;
    public $enableCsrfValidation = false;

    public function __construct($id, $module, $config = [])
    {
        parent::__construct($id, $module, $config);
        $this->todoService = new TodoService();
    }

    // 將響應資料轉成 JSON
    public function behaviors()
    {
        return [
            [
                'class' => \yii\filters\ContentNegotiator::className(),
                'formats' => [
                    'application/json' => \yii\web\Response::FORMAT_JSON,
                ],
            ],
        ];
    }

    public function actionGetTodoList()
    {
        $model = new TodoModel();
        $params = Yii::$app->request->get();
        // 取出 query 引數中的 key 欄位
        $model->key = $params['key'];
        return [
            'code' => 0,
            'data' => $this->todoService->getAllTodo($model)
        ];
    }

    public function actionAdd()
    {
        $model = new TodoModel();
        $params = Yii::$app->request->post();
        $model->key = $params['key'];
        $model->title = $params['title'];
        $this->todoService->addTodo($model);
        return ['code' => 0];
    }

    public function actionTop()
    {
        $model = new TodoModel();
        $params = Yii::$app->request->post();
        $model->id = $params['id'];
        $this->todoService->topTodo($model);
        return ['code' => 0];
    }

    public function actionComplete()
    {
        $model = new TodoModel();
        $params = Yii::$app->request->post();
        $model->id = $params['id'];
        $this->todoService->completeTodo($model);
        return ['code' => 0];
    }

    public function actionDelete()
    {
        $model = new TodoModel();
        $params = Yii::$app->request->post();
        $model->id = $params['id'];
        $this->todoService->deleteTodo($model);
        return ['code' => 0];
    }
}

如此一來,我們的 Todo List 系統就基本完成了,它已經完成了下面這些功能:

  1. 可以基於某個 key 建立 Todo Item,然後根據 key 查詢對應的 Todo Item
  2. 可以置頂、完成、刪除單條 Todo Item,置頂的 Todo Item 將排列在最前面,完成的 Todo Item 將排列在最後面。

當然,我們還需要考慮引數驗證、大資料查詢的優化問題、更簡潔的引數繫結等等問題,這裡就不做展開了,可能會以一期新的文章進行講解。

部署應用

現在,我們來將我們的 Todo List 系統部署到線上吧。

啟動 Docker 容器

Yii2 的部署非常簡單,因為 Yii 內建了 docker-compose 配置檔案。

所以,我們只需要在資料夾內執行 docker-compose up -d 就可以啟動一個 docker 服務了。(如下圖)

image

現在,我們修改一下 docker-compose.yml 的埠對映改一下,將其改成一個比較特殊的埠 —— 9999

ports:
   - '9999:80'

然後,我們在我們的伺服器(我的伺服器是阿里雲 ECS)內,把對應的倉庫程式碼拉下來,執行 docker-compose up -d 啟動容器即可。

配置 Nginx

服務啟動後,我們需要配置 nginx,將我們指定域名的請求 hacker.jt-gmall.com 轉發到 9999 埠。

然後,在 nginx 上加上跨域頭,允許前端跨域請求(最後幾行)。

server {
    listen 443;
    server_name hacker.jt-gmall.com;
    ssl on;
    ssl_certificate /https/hacker.jt-gmall.com.pem;
    ssl_certificate_key /https/hacker.jt-gmall.com.key;
    ssl_session_timeout 5m;
    ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;
    ssl_protocols SSLv2 SSLv3 TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    location / {
      index index.html index.jsp;
      client_max_body_size 300m;
      client_body_buffer_size 128k;
      proxy_connect_timeout 600;
      proxy_read_timeout 600;
      proxy_send_timeout 600;
      proxy_buffer_size 64k;
      proxy_buffers 4 64k;
      proxy_busy_buffers_size 64k;
      proxy_temp_file_write_size 64k;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header Host $host;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_http_version 1.1;
      proxy_pass http://127.0.0.1:9999;

      add_header "Access-Control-Allow-Origin" "*"; # 全域性變數獲得當前請求origin,帶cookie的請求不支援*
      add_header "Access-Control-Allow-Methods" "*"; # 允許請求方法
      add_header "Access-Control-Allow-Headers" "*"; # 允許請求的 header
      # 如果是 OPTIONS 請求,則返回 204
      if ($request_method = 'OPTIONS') {
        return 204;
      }
    }
  }

安裝依賴

在服務啟動並且配置好了 nginx 後進行訪問,可能會出現下圖這個錯誤。

image

這是因為 Git 版本管理中,會忽略 Yiivendor 目錄,我們只需要使用 composer 將依賴重新安裝一遍即可,執行下面這個命令。

composer update
composer install

由於 config/db.php 中包含了資料庫連線資訊,我也沒有放到 Git 倉庫中。

如果你在使用我的 demo,也請將這個檔案補齊。

然後,我們開啟瀏覽器,輸入 https://hacker.jt-gmall.com/todo/get-todo-list?key=test 看看效果吧!(如下圖)

image

大功告成啦!

小結

在本篇文章中,我針對自己使用 Yii 搭建一個基礎 Todo List 服務的體驗,寫了一篇文章。

實際操作下來,發現使用 Yii 搭建一個服務端業務站點還是比較簡單的,經典的 MVC 模式也比較淺顯易懂。

在後續的文章裡,我可能會針對 Yii 的進階使用再進行歸納總結。

最後附上本次體驗的 Demo 地址

最後一件事

如果您已經看到這裡了,希望您還是點個贊再走吧~

您的點贊是對作者的最大鼓勵,也可以讓更多人看到本篇文章!

如果覺得本文對您有幫助,請幫忙在 github 上點亮 star 鼓勵一下吧!

相關文章