iT邦幫忙

2022 iThome 鐵人賽

DAY 12
0
Modern Web

LV的全端開發體驗系列 第 12

Day12 完善題庫設定功能-善用ORM

  • 分享至 

  • xImage
  •  

先前提到題目之上還有個題庫,現在來把題庫功能完善,其實也就是CRUD再走一遍而已,不過值得一提的是,在習慣使用框架之後,真的要懂得善用框架在ORM上提供的功能,可以簡化不少作業,很多以前在Controller處理資料,我現在都會思考有沒有可能在Model端就處理掉,這樣的話提取或放入資料都會簡便不少。

先定義路由:
routes\web.php

//用來顯示某個題庫下的題目
Route::get('/bank/{id}/subjects',[SubjectController::class,'subjectsInBank'])
        ->name('bank.subjects');
//建立題庫的表單畫面
Route::get('/bank/create',[BankController::class,'create'])
        ->name('bank.create');
//儲存新增的題庫資料
Route::post('/bank',[BankController::class,'store'])
        ->name('bank.store');
//顯示編輯指定id的題庫表單
Route::get('/bank/edit/{id}',[BankController::class,'edit'])
        ->name('bank.edit');
//更新指定id的題庫資料
Route::put('/bank/{id}',[BankController::class,'update'])
        ->name('bank.update');
//刪除指定id的題庫資料
Route::delete('/bank/{id}',[BankController::class,'destroy'])
        ->name('bank.destroy');

如果你的路由定義到後面模式很固定的話,可以參考laravel的resource路由寫法並搭配Controller來簡化一部份的作業。

前面有提到大多數時候,新增和編輯的表單是很類似的,只差在欄位裏有沒有資料而已,如果可以使用一個表單檔案就能處理新增和編輯就太好了。

app\Http\Controllers\BankController.php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Services\BankService;
use Inertia\Inertia;

class BankController extends Controller
{
    function __construct(protected BankService $bank){}

    function index(){
        $all=$this->bank->all();

        return Inertia::render('Backstage/Banks',[
            'count'=>$all->count(),
            'banks'=>$all
        ]);
    }

    function create()
    {
        return Inertia::render('Backstage/BankForm');
    }

    function store(Request $request)
    {
        $this->bank->store($request->input());
        return redirect()->route('backstage.banks');
    }

    function edit($id)
    {
        return Inertia::render('Backstage/BankForm',[
            'header'=>'編輯題庫',
            'button'=>'修改',
            'bank'=>$this->bank->find($id) //取得資料
        ]);
    }

    function update(Request $request,$id)
    {
        $this->bank->update($request->input(),$id);
        return redirect()->route('backstage.banks');
    }

    function destroy($id)
    {
        $this->bank->destroy($id);
        return redirect()->back();
    }
}

LaravelModel 中有提供一個 Mutators & Casting 的功能,用來設定資料的 getter 和 setter,可以在存取資料前對資料做加工,這裏我要示範的是在取出資料時,根據level的值,為model加上一個中文級別的屬性:
app\Models\Bank.php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;

class Bank extends Model
{
    use HasFactory;

    protected $guarded=[];
    public $timestamps = false;
    
    //建立一個level值對應的中文字陣列
    protected $levelc=["A"=>"甲",
                       "B"=>"乙",
                       "C"=>"丙",
                       "D"=>"單一"];
                        
    function subjects()
    {
        return $this->hasMany(Subject::class);
    }
    
    //使用Attribute類別的功能,增加一個屬性levelC
    //levelC的結果會是回傳一個中文字
    protected function levelC():Attribute
    {
        return Attribute::make(  
            get:fn($value,$attributes)=>$this->levelc[$attributes['level']],
        );
    }

}

app\Services\BankService.php 處理商業邏輯
雖然laravel提供了工具讓我們可以方便的增加資料的屬性,但這個屬性必須是在laravel內使用時才會有效,如果你直接把撈出來的資料回傳給前端的Vue,會發現前端的資料中沒有這個欄位,所以我們要在資料丟出去給前端之前,先把這些屬性給指定進去Model中才行,這個動作要寫在Service或是Repository中其實都可以,我是選擇寫在Service中:

namespace App\Services;

use App\Repositories\BankRepository;

class BankService
{
    function __construct( protected BankRepository $bank){}

    function infos()
    {
        return ['count'=>$this->all()->count()];
    }
    
    function all()
    {
        return $this->bank->all()->map(function($bank){
            $bank->levelC=$bank->levelC;  //取用資料時增加指定級別的中文
            return $bank;
        });
    }

    function find($id)
    {
        $bank=$this->bank->find($id);
        $bank->levelC=$bank->levelC;   //取用資料時增加指定級別的中文
        return $bank;
    }

    function store($bank)
    {
        $this->bank->store($bank);
    }

    function update($bank,$id)
    {
        unset($bank['levelC']); //更新資料時移除附加的級別中文欄位
        $this->bank->update($bank,$id);
    }
    
    function destroy($id)
    {
        $this->bank->destroy($id);
    }
}

app\Repositories\BankRepository.php 處理資料存取

class BankRepository 
{
    function __construct(protected Bank $bank){}

    function all(){ return $this->bank->all(); }

    function store($bank){ $this->bank->insert($bank); }

    function find($id){ return $this->bank->find($id); }

    function update($bank,$id){ $this->find($id)->update($bank); }

    function destroy($id){ $this->bank->find($id)->delete();}
}

後端處理好了,接下來調整前端
resources\js\Pages\Backstage\Banks.vue 加上對應動作的路由

.....略
    <div class="w-[calc(100%_-_12rem)] p-4">
      <Link :href="route('bank.create')"
            class="inline-block py-2 px-3 border rounded-xl bg-blue-700 
                    text-blue-100 my-4">
        新增題庫
      </Link>
      <div class="w-full px-4 py-2">題庫總數:{{ count }}</div>
      <div v-for="bank in banks"
            :key="bank.id"
            class="w-full border rounded-xl flex p-4 bg-green-400 
                   justify-between">
        <Link :href="route('bank.subjects', bank.id)">
          {{ bank.code }}.
          {{ bank.name }}
          {{ bank.levelC }}級
        </Link>
        <div>
          <Link :href="route('bank.edit', bank.id)">編輯</Link>
          /
          <Link :href="route('bank.destroy', bank.id)" 
                method="delete" 
                as="button">刪除</Link>
        </div>
      </div>
    </div>
.....略

resources\js\Pages\Backstage\BankForm.vue 表單頁面

<script setup>
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import { Head, Link, useForm } from "@inertiajs/inertia-vue3";

const props = defineProps({
  	header: { type: String, default: "新增表單" },
  	button: { type: String, default: "新增" },
  	bank: { //建立一個預設的題庫資料屬性,預設的文字內容都是空字串,預設級別是A
            type: Object,
            default: { code: "", name: "", level: "A" },
        },
    });

//在組件建立時,把props中的bank資料引入(可能有實際的資料,也可能只是預設值)
const form = useForm(props.bank); 

//當使用者按下送出按鈕時執行submit函式
//這個函式會根據bank資料中有沒有id這個屬性來決定要執行的新增還是更新的動作
const submit = () => {
  if (typeof form.id !== "undefined") {
    form.put(route("bank.update", form.id)); //更新
  } else {
    form.post(route("bank.store"));  //新增
  }
};
</script>

<template>
  <Head title="管理中心" />

  <AuthenticatedLayout>
    <template #header>
      <h2 class="font-semibold text-xl text-gray-800 leading-tight">管理中心</h2>
    </template>

    <div class="w-[calc(100%_-_12rem)] p-4">
      <h2 class="text-xl font-bold">{{ header }}</h2>
      <div>
        <div>
          <label>題庫代碼:</label>
          <input type="text" name="code" id="code" v-model="form.code" />
        </div>
        <div>
          <label>題庫名稱:</label>
          <input type="text" name="name" id="name" v-model="form.name" />
        </div>
        <div>
          <label>題庫級別:</label>
          <select name="level" id="level" v-model="form.level">
            <option value="A">甲級</option>
            <option value="B">乙級</option>
            <option value="C">丙級</option>
            <option value="D">單一級</option>
          </select>
        </div>
        <button @click="submit" class="my-2 border px-6 py-2 rounded-xl shadow">
          {{ button }}
        </button>
      </div>
    </div>
  </AuthenticatedLayout>
</template>

新增資料畫面

新增成功回到題庫列表畫面

編輯資料畫面

編輯完成回到題庫列表畫面

在今天的示範中,我們再次複習一次CRUD的流程,和前次不同的地方在於我們透過拆解邏輯的方式讓每一步的工作變得單純,可讀性和重用性也都提高了。

在前端的部份,我們透過定義props屬性的方式,給予新增表單一個空白的資料物件,但是當表單改為編輯時,我們會在後端把指定的資料取出並傳入前端組件中,原本的空白資料物件會成為有實際資料內容的物件,並把相應資料呈現在表單中。

最後我們送出表單時會根據有無id來決定表單物件是要新增還是編輯,這樣就可以做到只用一個表單檔案,卻可以使用在兩個動作上。


上一篇
Day11 前端頁面狀態管理 - Pinia
下一篇
Day13 完善題組及題目功能
系列文
LV的全端開發體驗30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Pallas
iT邦新手 3 級 ‧ 2023-07-13 17:46:52

resources\js\Pages\Backstage\Banks.vue
上面的 少了一些東西,幫您補上

const props = defineProps({ banks: Array, count: Number });

mackliu iT邦新手 4 級 ‧ 2023-07-22 20:17:24 檢舉

感謝補充

我要留言

立即登入留言