YII 初體驗 —— 搭建一個簡單的 Todo List 系統(tǒng)

Yii 是一個高性能持痰,基于組件的 PHP 框架,用于快速開發(fā)現(xiàn)代 Web 應(yīng)用程序。

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

  1. 可以基于某個 key 創(chuàng)建 Todo Item,然后根據(jù) key 查詢對應(yīng)的 Todo Item堂油。
  2. 可以置頂修档、完成、刪除單條 Todo Item府框,置頂?shù)?Todo Item 將排列在最前面吱窝,完成的 Todo Item 將排列在最后面。

初始化 YII 倉庫

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

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

但是院峡,我的 mac 通過這個方法老是連不上網(wǎng)絡(luò),安裝某些依賴失敗系宜,所以這里選擇第二種方式照激。(如下)

yiiframework 下載歸檔文件,然后解壓到你要放置的項目目錄中盹牧。

在下載解壓完成后实抡,需要先修改 config/web.php 文件欠母,給 cookieValidationKey 配置項添加一個密鑰(隨便輸入一個值就可以),以便項目能夠正常啟動吆寨。

在項目初始化完成以后赏淌,我們使用下面這個命令運行項目吧。

php yii serve --port=8888

然后我們打開 http://localhost:8888啄清,看到我們的頁面已經(jīng)成功啟動啦A(如下圖)

image

初始化數(shù)據(jù)模型

接下來,我們來初始化我們的數(shù)據(jù)模型辣卒。

我們需要創(chuàng)建的字段有下面這些:

  • id:自增主鍵掷贾;
  • key:Todo 的 key;
  • title:Todo 的標(biāo)題荣茫;
  • is_completed:Todo 是否完成想帅;
  • is_top:Todo 是否置頂;
  • is_deleted:Todo 是否刪除啡莉;

而上面這些字段中港准,我們最多的場景是通過 key 來撈出相關(guān)的 Todo Item,所以應(yīng)該給 key 建立一個普通索引咧欣。

綜上浅缸,我們的 sql 語句應(yīng)該是這樣的:

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;

在數(shù)據(jù)庫中執(zhí)行該條 SQL,創(chuàng)建對應(yīng)的數(shù)據(jù)表魄咕。

然后衩椒,我們還可以通過下面這條語句查看我們創(chuàng)建的索引。

SHOW INDEX FROM `todos`;
image

處理 Todo 業(yè)務(wù)邏輯

在數(shù)據(jù)表創(chuàng)建成功后哮兰,我們就準備開始寫 Todo 相關(guān)的業(yè)務(wù)邏輯了毛萌。

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

初始化 Yii 配置

首先阁将,我們的 php 服務(wù)需要連接數(shù)據(jù)庫,所以你需要先配置你的數(shù)據(jù)庫連接囤躁,也就是 config/db.php

  • 數(shù)據(jù)庫配置
<?php

return [
    'class' => 'yii\db\Connection',
    'dsn' => 'mysql:host=[mysql服務(wù)器地址];port=[mysql端口];dbname=[數(shù)據(jù)庫名稱]',
    'username' => '[數(shù)據(jù)庫用戶名]',
    'password' => '[數(shù)據(jù)庫密碼]',
    'charset' => 'utf8',
    'attributes' => [
        // 查詢時將 int 類型按原類型返回
        PDO::ATTR_STRINGIFY_FETCHES => false,
        PDO::ATTR_EMULATE_PREPARES => false
    ]
];
  • URL 美化配置

然后冀痕,我們再來配置一下 URL 美化,這樣就可以按照標(biāo)準的 restful 風(fēng)格進行訪問了狸演,調(diào)整 config/web.php 中的 urlManager 即可言蛇。(如下)

'urlManager' => [
  'enablePrettyUrl' => true,
  'enableStrictParsing' => false,
  'showScriptName' => false,
  'rules' => [
  ],
],
  • JSON 入?yún)⑴渲?/li>

然后,我們還需要修改一下 request 的配置宵距,以便接受 application/json 的入?yún)ⅰ?/p>

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

修改完了配置后腊尚,可以重啟一下你的項目。

創(chuàng)建 TodoModel + TodoRepository + TodoService + TodoController

我們先來創(chuàng)建 Todo 的數(shù)據(jù)實體類 —— 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;
}

然后劝篷,我們創(chuàng)建 TodoRepository,用于數(shù)據(jù)持久化民宿。 —— 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) {

    }
}

接下來,我們來創(chuàng)建 TodoService活鹰,用于處理業(yè)務(wù)邏輯哈恰。 —— 所有業(yè)務(wù)邏輯放在這里。

<?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) {

    }
}

最后志群,我們創(chuàng)建 TodoController着绷,用于控制業(yè)務(wù)流程和處理接口請求。 —— 與客戶端交互的邏輯放在這里锌云。

<?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();
    }

    // 將響應(yīng)數(shù)據(jù)轉(zhuǎn)成 JSON
    public function behaviors()
    {
        return [
            [
                'class' => \yii\filters\ContentNegotiator::className(),
                'only' => ['index', 'view'],
                'formats' => [
                    'application/json' => \yii\web\Response::FORMAT_JSON,
                ],
            ],
        ];
    }

    public function actionGetTodoList() {
      
    }
}

將基礎(chǔ)的 TodoModel + TodoRepository + TodoService + TodoController荠医,也就是 MVC 模型準備好了以后,我們就準備開始添加真實有效的業(yè)務(wù)邏輯了桑涎。

查詢對應(yīng) keyTodo List

我們現(xiàn)在準備根據(jù) key 來查詢對應(yīng)的 todo 列表彬向。

我們首先來編輯 TodoRepositoryselectAll,將對應(yīng)的 SQL 查詢邏輯寫好石洗。

class TodoRepository {
  /**
   * @throws \yii\db\Exception
   */
  public static function selectAll(TodoModel $todo) {
    $db = Yii::$app->db;
    // 組裝 SQL 語句幢泼,查詢對應(yīng) key 且未刪除的數(shù)據(jù)
    // 查詢的數(shù)據(jù)按照 `是否完成` 升序排列紧显,按照 `是否置頂` 降序排列
    $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 語句編輯完成后讲衫,我們可以在數(shù)據(jù)庫中執(zhí)行試試。(如下圖)

image

從上圖可以看出孵班,該 SQL 按我們預(yù)想的運行 —— 使用 key 作為索引涉兽,只檢索了 4 條數(shù)據(jù)(此時數(shù)據(jù)庫有 10 條數(shù)據(jù))。

這條 SQL 還涉及到了 Using filesort篙程,我還沒有想到比較好的優(yōu)化方案枷畏,大家可以嘗試一下優(yōu)化這條 SQL。

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

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

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

image

從上圖可以看出氮发,數(shù)據(jù)按照我們預(yù)期的篩選和排序返回了渴肉!

補全剩余業(yè)務(wù)邏輯 —— 增刪改

接下來,就是依次將 增刪改 的邏輯加上就好了爽冕,這應(yīng)該是最簡單也是最經(jīng)典的 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 語句,查詢對應(yīng) key 且未刪除的數(shù)據(jù)
        // 查詢的數(shù)據(jù)按照 `是否完成` 升序排列颈畸,按照 `是否置頂` 降序排列
        $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();
    }

    // 將響應(yīng)數(shù)據(jù)轉(zhuǎn)成 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 參數(shù)中的 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 系統(tǒng)就基本完成了没讲,它已經(jīng)完成了下面這些功能:

  1. 可以基于某個 key 創(chuàng)建 Todo Item,然后根據(jù) key 查詢對應(yīng)的 Todo Item礁苗。
  2. 可以置頂爬凑、完成、刪除單條 Todo Item试伙,置頂?shù)?Todo Item 將排列在最前面贰谣,完成的 Todo Item 將排列在最后面。

當(dāng)然迁霎,我們還需要考慮參數(shù)驗證吱抚、大數(shù)據(jù)查詢的優(yōu)化問題、更簡潔的參數(shù)綁定等等問題考廉,這里就不做展開了秘豹,可能會以一期新的文章進行講解。

部署應(yīng)用

現(xiàn)在昌粤,我們來將我們的 Todo List 系統(tǒng)部署到線上吧既绕。

啟動 Docker 容器

Yii2 的部署非常簡單,因為 Yii 內(nèi)置了 docker-compose 配置文件涮坐。

所以凄贩,我們只需要在文件夾內(nèi)運行 docker-compose up -d 就可以啟動一個 docker 服務(wù)了。(如下圖)

image

現(xiàn)在袱讹,我們修改一下 docker-compose.yml 的端口映射改一下疲扎,將其改成一個比較特殊的端口 —— 9999

ports:
   - '9999:80'

然后捷雕,我們在我們的服務(wù)器(我的服務(wù)器是阿里云 ECS)內(nèi)椒丧,把對應(yīng)的倉庫代碼拉下來,運行 docker-compose up -d 啟動容器即可救巷。

配置 Nginx

服務(wù)啟動后壶熏,我們需要配置 nginx,將我們指定域名的請求 hacker.jt-gmall.com 轉(zhuǎn)發(fā)到 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" "*"; # 全局變量獲得當(dāng)前請求origin帽哑,帶cookie的請求不支持*
      add_header "Access-Control-Allow-Methods" "*"; # 允許請求方法
      add_header "Access-Control-Allow-Headers" "*"; # 允許請求的 header
      # 如果是 OPTIONS 請求,則返回 204
      if ($request_method = 'OPTIONS') {
        return 204;
      }
    }
  }

安裝依賴

在服務(wù)啟動并且配置好了 nginx 后進行訪問渤弛,可能會出現(xiàn)下圖這個錯誤祝拯。

image

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

composer update
composer install

由于 config/db.php 中包含了數(shù)據(jù)庫連接信息,我也沒有放到 Git 倉庫中康嘉。

如果你在使用我的 demo碉输,也請將這個文件補齊。

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

image

大功告成啦肄梨!

小結(jié)

在本篇文章中阻荒,我針對自己使用 Yii 搭建一個基礎(chǔ) Todo List 服務(wù)的體驗,寫了一篇文章众羡。

實際操作下來侨赡,發(fā)現(xiàn)使用 Yii 搭建一個服務(wù)端業(yè)務(wù)站點還是比較簡單的,經(jīng)典的 MVC 模式也比較淺顯易懂粱侣。

在后續(xù)的文章里羊壹,我可能會針對 Yii 的進階使用再進行歸納總結(jié)。

最后附上本次體驗的 Demo 地址齐婴。

最后一件事

如果您已經(jīng)看到這里了油猫,希望您還是點個贊再走吧~

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末嚣州,一起剝皮案震驚了整個濱河市鲫售,隨后出現(xiàn)的幾起案子共螺,更是在濱河造成了極大的恐慌该肴,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件藐不,死亡現(xiàn)場離奇詭異匀哄,居然都是意外死亡,警方通過查閱死者的電腦和手機雏蛮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進店門涎嚼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人挑秉,你說我怎么就攤上這事法梯。” “怎么了?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵立哑,是天一觀的道長夜惭。 經(jīng)常有香客問我,道長铛绰,這世上最難降的妖魔是什么诈茧? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮捂掰,結(jié)果婚禮上敢会,老公的妹妹穿的比我還像新娘。我一直安慰自己这嚣,他們只是感情好鸥昏,可當(dāng)我...
    茶點故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著姐帚,像睡著了一般互广。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上卧土,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天惫皱,我揣著相機與錄音,去河邊找鬼尤莺。 笑死旅敷,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的颤霎。 我是一名探鬼主播媳谁,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼友酱!你這毒婦竟也來了晴音?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤缔杉,失蹤者是張志新(化名)和其女友劉穎锤躁,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體或详,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡系羞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了霸琴。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片椒振。...
    茶點故事閱讀 40,498評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖梧乘,靈堂內(nèi)的尸體忽然破棺而出澎迎,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布夹供,位于F島的核電站辑莫,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏罩引。R本人自食惡果不足惜各吨,卻給世界環(huán)境...
    茶點故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望袁铐。 院中可真熱鬧揭蜒,春花似錦、人聲如沸剔桨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽洒缀。三九已至瑰谜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間树绩,已是汗流浹背萨脑。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留饺饭,地道東北人渤早。 一個月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像瘫俊,于是被迫代替她去往敵國和親鹊杖。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,507評論 2 359

推薦閱讀更多精彩內(nèi)容