iT邦幫忙

2024 iThome 鐵人賽

DAY 10
0
佛心分享-IT 人自學之術

後端小白自學 Laravel系列 第 10

第 10 天:文件上傳與存儲

  • 分享至 

  • xImage
  •  

文件上傳處理


文件:File Uploads
建立上傳表單: 使用 <form> 標籤和 enctype="multipart/form-data" 屬性來建立檔案上傳表單。
驗證檔案: 在控制器中使用 $request->validate() 方法來驗證上傳的檔案(如類型、大小)。
處理上傳檔案: 使用 $request->file('file')->store('path') 方法將檔案儲存到指定的路徑。

Laravel 提供了簡單且強大的方式來處理文件上傳,可以輕鬆地建立文件上傳表單,處理上傳的文件,並將其儲存到檔案系統中。

step 1:建立文件上傳表單
resources/views 目錄下建立一個 Blade 範本檔案 upload.blade.php,用於顯示檔案上傳表單:

<!-- resources/views/upload.blade.php -->
<!DOCTYPE html>
<html>
<head>
    <title>File Upload</title>
</head>
<body>
    <h1>Upload File</h1>

    <!-- 顯示上傳錯誤訊息 -->
    @if ($errors->any())
        <div>
            <ul>
                @foreach ($errors->all() as $error)
                    <li>{{ $error }}</li>
                @endforeach
            </ul>
        </div>
    @endif

    <!-- 文件上傳表單 -->
    <form action="{{ route('upload') }}" method="POST" enctype="multipart/form-data">
        @csrf
        <label for="file">Choose File:</label>
        <input type="file" id="file" name="file">
        <br>
        <button type="submit">Upload</button>
    </form>
</body>
</html>

step 2:建立文件上傳控制器
下指令 php artisan make:controller FileUploadController 生成的 app/Http/Controllers/FileUploadController.php 文件,新增處理文件上傳的邏輯:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class FileUploadController extends Controller
{
    /**
     * Show the file upload form.
     *
     * @return \Illuminate\View\View
     */
    public function showForm()
    {
        return view('upload');
    }

    /**
     * Handle file upload request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\RedirectResponse
     */
    public function upload(Request $request)
    {
        // 驗證文件
        $request->validate([
            'file' => 'required|file|mimes:jpeg,png,pdf|max:2048',
        ]);

        // 儲存檔案到本機磁碟
        $filePath = $request->file('file')->store('uploads', 'public');

        // 儲存文件到公共磁碟
        // $filePath = $request->file('file')->store('uploads');

        return redirect()->back()->with('success', 'File uploaded successfully!')->with('file', $filePath);
    }
}

也可以用前一天提到的創建表單驗證,在用依賴注入的方法引入控制器

// Http/Requests/FileUploadRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class FileUploadRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules(): array
    {
        return [
            'file'   => ['required', 'file', 'mimes:jpeg,png,pdf', 'max:2048'],
        ];
    }

    /**
     * Get custom messages for validator errors.
     * 取得已定義驗證規則的錯誤訊息: 自定義錯誤訊息
     *
     * @return array
     */
    public function messages(): array
    {
        return [
            'file.required' => '檔案是必填的',
            'file.file'     => '必須是文件',
            'file.mimes'    => '文件類型必須是 jpeg, png, pdf',
            'file.max'      => '檔案最大只能 2048'
        ];
    }
}
<?php

namespace App\Http\Controllers;

use App\Http\Requests\FileUploadRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;

class FileUploadController extends Controller
{
    /**
     * Show the file upload form.
     *
     * @return View
     */
    public function showForm(): View
    {
        return view('upload');
    }

    /**
     * Handle file upload request.
     *
     * @param FileUploadRequest $request
     * @return RedirectResponse
     */
    public function upload(FileUploadRequest $request): RedirectResponse
    {
        // 驗證文件
        $validatedData = $request->validated();

        // 儲存檔案到本機磁碟
        // $filePath = $validatedData['file']->store('uploads', 'public');

        // 或者,存儲文件到公共磁碟
        $filePath = $validatedData['file']->store('uploads');

        return redirect()->back()->with('success', '檔案上傳成功!')->with('file', $filePath);
    }
}

🐘 補充說明:
文件儲存的資料夾
從上圖可知道存放位置會隨指定不同,其中 'public'(存本機) 和 'uploads'(存公共磁碟)差異來自於公用磁碟可以讓使用隨意打就取得路徑
只要下指令 php artisan storage:link後,可以看到提示 The [public/storage] link has been connected to [storage/app/public].
提示

step 3:設定路由
routes/web.php 文件中設定路由:

use App\Http\Controllers\FileUploadController;

Route::get('/upload', [FileUploadController::class, 'showForm'])->name('upload.form');
Route::post('/upload', [FileUploadController::class, 'upload'])->name('upload');

文件存儲(本地與雲存儲)


文件:Amazon S3 Compatible Filesystems
本地儲存: 預設情況下,Laravel 使用本地存儲,檔案保存在 storage/app 目錄下。
公共儲存: 使用 public 磁碟將檔案儲存在 storage/app/public 目錄下,並且可以透過 URL 存取。
雲端儲存: 設定 config/filesystems.php 檔案以使用雲端儲存服務(如 Amazon S3)。

Laravel 的檔案儲存系統支援多種儲存方式,包括本機儲存和雲端儲存(如 Amazon S3)。
本地儲存
upload 方法中,使用 store 方法將檔案儲存到 storage/app/public/uploads 目錄。
可以使用 public 磁碟存取這些檔案。

雲端儲存
要使用雲端儲存(如 Amazon S3),首先需要安裝相關的套件並在 .env 檔案中配置相應的儲存設定。以下是一個簡單的 S3 配置範例:

FILESYSTEM_DRIVER=s3

AWS_ACCESS_KEY_ID=your-access-key-id
AWS_SECRET_ACCESS_KEY=your-secret-access-key
AWS_DEFAULT_REGION=your-region
AWS_BUCKET=your-bucket

config/filesystems.php 中設定 s3 驅動程式:

's3' => [
    'driver' => 's3',
    'key' => env('AWS_ACCESS_KEY_ID'),
    'secret' => env('AWS_SECRET_ACCESS_KEY'),
    'region' => env('AWS_DEFAULT_REGION'),
    'bucket' => env('AWS_BUCKET'),
    'url' => env('AWS_URL'),
    'endpoint' => env('AWS_ENDPOINT'),
],

然後在控制器中可以使用 store 方法將檔案儲存到 S3:

$filePath = $request->file('file')->store('uploads', 's3');

文件操作(存儲、刪除、下載)


儲存檔案: 使用 store 方法將檔案儲存到指定路徑。
刪除檔案: 使用 Storage::delete() 方法刪除檔案。
下載檔案: 使用 Storage::download() 方法提供檔案下載。

Laravel 提供了一種簡單的方法來存取和操作文字項。
儲存檔案
使用 store 方法來儲存檔案:

$filePath = $request->file('file')->store('uploads');

刪除文件
可以使用 Storage 門面來刪除檔案:

use Illuminate\Support\Facades\Storage;

Storage::delete($filePath);

下載文件
要讓使用者能夠下載文件,可以使用 Storage 門面提供的下載方法:

return Storage::download($filePath);

✍🏻 每日任務


運用文件上傳和儲存,練習把使用者頭像上傳和管理頭像。
step 1 - 規劃
上傳需要畫面也就是要先出第一支 api 是畫面渲染用的 GET;還需要有上傳檔案的媒介也就是第二支 api POST,這可能要把資料寫入資料庫到時候要拿內容渲染在畫面上以外,還要可以抓到資料刪除。
管理的部分,刪除和下載個別也需要一支 api 做資料庫處理並渲染在畫面上。

參考文章:
Laravel 上傳/刪除圖片 | Upload and Delete Image
laravel文件存储、删除、移动等操作

step 2 - 設定資料庫
下指令 php artisan make:model Avatar -m 建立模型和遷移文件,並且在遷移文件設定表格欄位名稱和型別。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('avatars', function (Blueprint $table) {
            $table->id();
            $table->string('file_path');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('avatars');
    }
};

step 3 - 建立資料庫
指令 php artisan migrate 建立資料庫

step 4 - 建立表單驗證
指令 php artisan make:request AvatarRequest,在表單驗證中建立規則和錯誤訊息回傳。

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class AvatarRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, mixed>
     */
    public function rules(): array
    {
        return [
            'avatar' => ['required', 'file', 'mimes:jpg,jpeg,png', 'max:2048'],
        ];
    }

    /**
     * Summary of messages
     * @return array
     */
    public function messages(): array
    {
        return [
            'file.required' => '檔案是必填的',
            'file.file'     => '必須是文件',
            'file.mimes'    => '文件類型必須是 jpeg, png, pdf',
            'file.max'      => '檔案最大只能 2048'
        ];
    }
}

step 5 - 建立控制器
指令 php artisan make:controller AvatarController,建立一個控制器來處理頭像的上傳、儲存、刪除和下載,其中表單驗證的部分改用創建的驗證類引入。

<?php

namespace App\Http\Controllers;

use App\Http\Requests\AvatarRequest;
use App\Models\Avatar;
use Illuminate\Support\Facades\Storage;
use \Illuminate\Http\RedirectResponse;
use \Illuminate\Contracts\View\View;

class AvatarController extends Controller
{
    /**
     * Summary of index
     * @return View
     */
    public function index(): View
    {
        $avatars = Avatar::all();
        return view('upload-avatar', compact('avatars'));
    }

    /**
     * Summary of upload
     * @param AvatarRequest $request
     * @return RedirectResponse
     */
    public function upload(AvatarRequest $request): RedirectResponse
    {
        // 取得驗證過的數據
        $validated = $request->validated();

        // 取得上傳的文件
        $file = $validated['avatar'];
        $filename = $file->hashName();
        $path = $file->storeAs('public/avatars', $filename);

        // 儲存文件資訊到資料庫
        $filePath = Avatar::query()->create(['file_path' => $filename]);

        return redirect()->back()->with('success', 'Avatar uploaded successfully!');
    }

    /**
     * Summary of delete
     * @param mixed $filename
     * @return RedirectResponse
     */
    public function delete($filename): RedirectResponse
    {
        $avatar = Avatar::where('file_path', $filename)->first();

        if ($avatar) {
            $path = "public/avatars/{$filename}";

            if (Storage::exists($path)) {
                Storage::delete($path);
                $avatar->delete();

                return redirect()->back()->with('success', 'Avatar deleted successfully!');
            }
        }

        return redirect()->back()->with('error', 'File not found.');
    }

    /**
     * Summary of download
     * 
     * @param mixed $filename
     * @return mixed|\Illuminate\Http\RedirectResponse|\Symfony\Component\HttpFoundation\BinaryFileResponse
     */
    public function download($filename): mixed
    {
        if (Storage::disk('public')->exists("avatars/{$filename}")) {
            return response()->download(storage_path("app/public/avatars/{$filename}"));
        }

        return redirect()->back()->with('error', 'File not found.');
    }
}

step 6 - 建立視圖

參考文章:laravel compact的用法

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Upload Avatar</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
</head>

<body>
    <div class="container">
        <!-- 上傳成功訊息 -->
        @if (session('success'))
        <p class="text-primary">{{ session('success') }}</p>
        @endif

        <!-- 上傳錯誤訊息 -->
        @if ($errors->any())
        <div>
            <ul>
                @foreach ($errors->all() as $error)
                <li class="text-warning">{{ $error }}</li>
                @endforeach
            </ul>
        </div>
        @endif

        <!-- 上傳表單 -->
        <form action="{{ route('upload.avatar') }}" method="POST" enctype="multipart/form-data">
            @csrf
            <label for="avatar" class="form-label">Choose Avatar:</label>
            <input type="file" id="avatar" name="avatar" class="form-control">
            <br>
            <button type="submit" class="btn btn-primary rounded-pill my-3" style="width: 15%">上傳</button>
        </form>

        <!-- 頭像列表 -->
        <ul class="row ps-0">
            @foreach ($avatars as $avatar)
            <li class="col-3 d-flex flex-column justify-content-between" style="list-style:none">
                <img src="{{ Storage::url("avatars/{$avatar->file_path}") }}" alt="{{ $avatar->file_path }}" class="rounded w-100 d-block">
                <form action="{{ route('delete.avatar', ['file_path' => $avatar->file_path]) }}" method="POST" style="display:inline;">
                    @csrf
                    @method('DELETE')
                    <div class="d-flex flex-column">
                        <button type="submit" class="btn btn-outline-danger rounded-pill my-1">刪除</button>
                        <a href="{{ route('download.avatar', ['file_path' => $avatar->file_path]) }}" class="btn btn-outline-primary rounded-pill">下載</a>
                    </div>
                </form>
            </li>
            @endforeach
        </ul>
    </div>

</body>

</html>

step 7 - 建立路由
web.php 建立路由

<?php

use App\Http\Controllers\AvatarController;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('avatars', [AvatarController::class, 'index'])->name('avatars.index');
Route::post('upload-avatar', [AvatarController::class, 'upload'])->name('upload.avatar');
Route::delete('delete-avatar/{file_path}', [AvatarController::class, 'delete'])->name('delete.avatar');
Route::get('download-avatar/{file_path}', [AvatarController::class, 'download'])->name('download.avatar');

step 8 - 建立一個符號連結讓瀏覽器收資料
指令 php artisan storage:link 用於建立符號鏈接,將 public/storage 目錄連結到 storage/app/public 目錄。

🔔 提醒:
只需要下一次即可,也就是練習的時候下過指令,這一步就可以跳過。

step 9 - 測試

  • 存取表單:在瀏覽器中造訪 http://127.0.0.1:8000/avatars 來查看上傳頭像的表單。
    起始狀態
  • 上傳頭像:選擇並上傳一個頭像檔案。
    上傳頭像
    上傳資料
  • 刪除頭像:在控制器中新增刪除頭像的功能,並在介面上測試刪除已上傳的頭像。
    刪除頭像
    刪除資料
  • 下載頭像:測試下載已上傳的頭像檔案。
    下載頭像
    下載

上一篇
第 9 天:表單處理與請求
下一篇
第 11 天:用戶認證
系列文
後端小白自學 Laravel21
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言