iT邦幫忙

2022 iThome 鐵人賽

DAY 16
0
Modern Web

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

Day16 使用Laravel Excel來匯入資料

  • 分享至 

  • xImage
  •  

Excel的匯入匯出是我目前工作上常有的工作項目之一,雖然系統數位化了,但是有些對外的工作還是得轉成檔案來傳送,在這個案子中,我打算讓後台的題目新增除了手動的一題一題輸入外,也提供可以使用Excel匯入的方式,而前台則是提供使用者可以把測驗結果給輸出做為自行追踪之用。

Laravel Excel

這是一套基於PhpSpreadsheet所開發的Laravel套件,提供快速方便的Excel檔匯入匯出功能,當然如果你需要更進階的功能,可以考慮搭配 PhpSpreadsheet 來使用,因為你在安裝 Laravel Excel時,會自動安裝 PhpSpreadsheet

安裝 Laravel Excel

Laravel Excel官網去把指令複製過來執行:

composer require maatwebsite/excel

在 Laravel 9 以上的版本如果出現錯誤,可能是相關的依賴套件有問題,按照官網指示來處理:

composer require psr/simple-cache:^2.0 maatwebsite/excel

根據官網說明,安裝時會自動註冊 ServiceProvideraliases ,但也提供自己手動註冊。

最後,把設定檔建立起來,之後有些功能直接在設定上改就可以了:

php artisan vendor:publish --provider="Maatwebsite\Excel\ExcelServiceProvider" --tag=config

之後要使用時,只要在 VS CODE中下EXCEL關鍵字,就會自動把引用路徑給補上,很方便

使用說明部份詳閱官網,不逐一介紹。
Laravel Excel 官網文件

匯入前的準備

不是裝完就馬上可以用了,通常提供這種匯入功能,很難做到百分之百智慧和自動,所以我們要先定義一下匯入檔案的一些規範,比如資料的欄位要如何呈現,選項又是如何分配,要不要有標題列之類。

我這邊定義檔案的內容要有以下的欄位,並且要有這些欄位名的標題列:

  • 題號
  • 答案
  • 題目
  • 選項1
  • 選項2
  • 選項3
  • 選項4
  • 複選 : 單選填0,複選填1
  • 題庫 : 依照國家技術士職類編號或自訂
  • 級別 : A,B,C,D
  • 題組

準備上傳檔案表單

我們需要有一個表單來提供上傳,這邊會使用 Inertia 提供的功能來以ajax的方式傳送檔案到後端去。

建立相關路由 - routes\web.php

Route::get('/subjects/{bank_id}/upload',[SubjectController::class,'upload'])
        ->name('subjects.upload');
Route::post('/subjects/import',[SubjectController::class,'import'])
        ->name('subjects.import');

在題目列表增加上傳的按鈕 - resources\js\Pages\Backstage\Subjects.vue

<Link :href="route('subjects.upload',bank.id)" 
      class="inline-block py-2 px-3 border rounded-xl 
             bg-orange-600 text-orange-50 my-4 mx-2" >
    上傳題目
</Link>

準備表單頁面

<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:Object,
  form: {
    type: Object,
    default: {
      bankId: 0,
      upload:''
    },
  },
});

const form = useForm(props.form);
form.bankId=props.bank.id
const submit = () => {
    form.post(route("subjects.import"));
};
</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="file" @input="form.upload = $event.target.files[0]" />
        </div>

        <button @click="submit" class="my-2 border px-6 py-2 rounded-xl shadow">
          {{ button }}
        </button>
      </div>
    </div>
  </AuthenticatedLayout>
</template>

準備好Controller方法 - app\Http\Controllers\SubjectController.php

    function upload(Request $request)
    {
        $bank=$this->subject->getBank($bank_id);
        return Inertia::render('Backstage/UploadSubjects',
                            ['bank'=>$bank]);
    }

    function import(Request $request)
    {
       dd($request->file());     
    }

簡單測試一下檔案有沒有傳到後端去

建立匯入控制器

Laravel Excel會在Laravel中另外建立 ExportImport 兩個資料夾,Excel檔案解析的相關動作都在這裏執行,然後再返回給 Controller 或是 Model

建立Import

php artisan make:import SubjectImport

如果檔案有照規範的欄位製作,那麼按照官網的快速匯入範例,一下子就可以把資料都存入到資料表中,但是在我的經驗中,很多時候是要取得匯入結果的一些訊息來回報給前端,或是另外做一頁預覽每個要匯入的項目,讓使用者可以先做編修或調整再存入資料表。

所以大多時候,Import 的功能就只是幫助解析檔案,然後把資料轉成Collection丟回Controller中進行下一步的處理;

不過,我後來發現其實在 Import 中有很多事可以做,在翻找原始碼後,我現在除了拿來解析檔案之外,有些匯入的轉換工作也會在 Import 中就處理掉,這樣就可以讓進到Controller中的資料和使用新增功能傳來的表單資料格式是一致的,這也讓先前在Service中的新增題目功能是可以延用的,不用再為匯入另外寫一個新增題目的函式。

app\Imports\SubjectImport.php

namespace App\Imports;

use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\ToCollection;  //指定檔案使用Collection匯入
use Maatwebsite\Excel\Concerns\WithMapping;   //把欄位進行對應
use Maatwebsite\Excel\Concerns\WithHeadingRow;  //指定檔案有標題列
use App\Models\Bank;    //引入Bank Model
use Maatwebsite\Excel\Imports\HeadingRowFormatter;  //指定檔案標題列的格式

/* 預設的WithHeadinRow時會自動對標題進行命名格式化的動作,
 * 但這個動作不支援中文,會變成亂碼或消失,
 * 所以要設定不對標題列做任何處理,中文欄位名才能正確匯入*/
HeadingRowFormatter::default('none');

class SubjectImport implements ToCollection,WithHeadingRow
{
    /**
    * @param Collection $collection
    */
    public function collection(Collection $collection)
    {  }

    /* 這個map方法會比toCollection還先執行,
     * 所以我們可以在這裏處理資料匯入的邏輯
     * 調整成和表單新增一致的格式
     * */
    public function map($row):array
    {
        $bank=Bank::where('code',$row['題庫'])
                    ->where('level',$row['級別'])
                    ->first();        //取得題庫
        $row['seq']=$row['題號'];
        $row['subject']=$row['題目'];
        $row['bankId']=$bank->id;
        $row['multiple']=$row['複選'];
        
        if($row['複選']){
            //把複選題的字串拆成字元存入陣列
            $ans=str_split($row['答案']);
        }else{
            //單選題的話把答案放入陣列
            $ans=[$row['答案']];
        }
        
        //根據$ans陣列中有無選項來決定該項目是否是答案
        $row['options']=[1=>['option'=>$row['選項1'],'ans'=>in_array(1,$ans)],
                         2=>['option'=>$row['選項2'],'ans'=>in_array(2,$ans)],
                         3=>['option'=>$row['選項3'],'ans'=>in_array(3,$ans)],
                         4=>['option'=>$row['選項4'],'ans'=>in_array(4,$ans)]
                        ];
        
        //從題庫撈出題組,再根據題組編號找到題組id來指定
        $row['bankGroupId']=$bank->bankgroups
                                  ->where('seq',$row['題組'])
                                  ->first()
                                  ->id;
        
        //移除原本的欄位,只回傳存入資料表需要的資料就好
        unset($row['題號'], $row['答案'], $row['題目'], $row['選項1'], 
              $row['選項2'], $row['選項3'], $row['選項4'], $row['複選'], 
              $row['題庫'], $row['級別'], $row['題組'], $row['11'], );
        
        return $row;
    } 
}

在Controller把檔案解析成Collection後傳給Service處理,主要是檢查資料格式及內容有沒有問題,沒問題的資料就存入到資料表。

要注意的是 Laravel Excel 匯入時是包含所有的分頁都一起匯入的,所以解析出來的Collection是一個陣列,陣列中的每個元素都是一張分頁:
app\Http\Controllers\SubjectController.php

function subjectsInBank($bank_id)
{
    $subjects=$this->subject->subjectsInBank($bank_id);
    $bank=$this->subject->getBank($bank_id);
    $bankgroups=$this->subject->getGroups($bank_id);

    return Inertia::render('Backstage/Subjects',
                    [
                     'subjects'=>$subjects,
                     'bankgroups'=>$bankgroups,
                     'bank'=>$bank,
                     'count'=>$subjects->count(),
                        //增加一個屬性來判斷上傳檔案的狀態
                     'uploaded'=>(session('uploaded'))?session('uploaded'):false,
                    ]);
}

function import(Request $request)
{
    $subjects=Excel::toCollection(new SubjectImport,$request->file('upload'));
    $uploaded=$this->subject->import($subjects[0]); //把第一張分頁的資料傳入Service

    return redirect()
            ->route('bank.subjects',$request->bankId)
            ->with('uploaded',$uploaded);    //匯入處理完畢,回傳訊息給頁面
}

把匯入的資料逐筆寫入資料表中 - app\Services\SubjectService.php

function import($subjects)
{
    $subjects->each(function($subject){
        $this->store($subject->toArray());
    });

    //匯入成功的話,回傳一些訊息給Controller,失敗也是
    return ['status'=>'success','rows'=>$subjects->count()];
}

在題目列表增加對上傳結果的訊息顯示 - resources\js\Pages\Backstage\Subjects.vue

<script setup>
const props = defineProps({ subjects: Array, 
                        bankgroups:Array, 
                        count: Number ,
                        bank:Object,
                        uploaded:Object || Boolean, //增加接收一個上傳狀態的屬性
                        errors:Object});
</script>
<template>
.....略
  <div v-if="uploaded" 
      class="text-xl font-bold my-4 mx-auto w-1/2 text-center">
      <span v-if="uploaded.status==='success'"
            class="text-orange-700 bg-yellow-300 py-3 px-7 rounded-xl">
        上傳題目成功!一共新增了{{ uploaded.rows }}筆題目
      </span>
      <span v-else class="text-red-900 bg-red-500 py-3 px-7 rounded-xl">
        上傳有誤,請檢查檔案內容格式並重新上傳出
      </span>
  </div>
.....略
</template>

善用套件提供的功能,的確可以節省不少自幹的時間,不過一開始也是花了不少時間在 Laravel ExcelPhpSpreadsheet 的官方文件及原始碼中穿梭,因為有些實際上想做到的功能官方文件是沒有提到的,甚至是 stackoverflow上 也沒有。

這裏留了個問題沒處理,當初新增單一題目時,我們並沒有對複選題進行處理,全部都當成單選題在存,而且在設定單選題時,就算你勾多個答案也是可以新增成功的,在測驗時也是全部當成單選題再選題,這邊匯入時我們卻有特別對複選題的項目做出處理;所以之後要再找個時間把單複選題的問題處理一下,現在只是針對匯入題目這功能先做一下確認。


上一篇
Day15 前台測驗功能試做
下一篇
Day17 使用Laravel Excel來匯出資料
系列文
LV的全端開發體驗30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言