【Laravel】 MySQLにファイルをバイナリで保存・表示する

:::note warn
バイナリをMySQLに保存することの是非については今回扱いません
また既存のHomeController,home.blade.phpを編集する形で進めていきます
:::

## 環境

- Laravel: 8.6.2
- Vue.js: 2.6.12
- PHP: 8.0.8
- Composer: 2.5.1
- Node.js: 19.3.0

```zsh
$ composer create-project --prefer-dist laravel/laravel:^8.0 project-name
$ cd project-name
$ php artisan migrate:fresh
$ composer require laravel/ui
$ php artisan ui bootstrap
$ npm install && npm run dev
$ php artisan serve
```

### Tips.Laravel Pintの導入

せっかくなのでLaravel専用のFormatterであるLaravel Pintを導入します
プロジェクトルートに以下のコマンドを入力します

```zsh
$ composer require laravel/pint --dev
```

フォーマットをかけたい時は、適宜以下のコマンドを入力しましょう
使い心地が良ければ、aliasを設定してみてください

```zsh
$ vendor/bin/pint -v
```

## 1. テーブルを追加する(Migration, Model)

ファイルを保存するためのテーブル(とついでにModel)を作成します

```zsh
$ php artisan make:model File --migration
```

まずmigrationファイルを以下のように編集します

```php:create_files_table
<?php

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

class CreateFilesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('files', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->binary('content');
            $table->timestamps();
        });
        DB::statement('ALTER TABLE files MODIFY content LONGBLOB not null');
    }

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

:::note warn
BlueprintのbinaryメソッドによってBLOBのカラムを作成することができますが、カラムサイズが小さいため満足に機能しない可能性があります
悲しいことにスキーマビルダのBlueprintにはLONGBLOBカラムを作成するメソッドが用意されていないので、素のSQLを流し込みます
:::

次にModelを編集します

```php:File.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class File extends Model
{
    protected $fillable = ['name', 'content']; // <-- 追加
}
```


## 2. 画面を用意する(View, Routing)

まずhome.blade.phpを編集します

```php:home.blade.php
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">ファイル保存</div>

                    <div class="card-body">
                        <form method="POST" action="{{ route('store') }}" enctype="multipart/form-data">
                            @csrf
                            <input type="file" name="file">
                            <button type="submit">登録</button>
                        </form>
                        @if(is_null($files) === false)
                            @foreach ($files as $row)
                                <a href="{{ route('streamFile', ['file' => $row]) }}">{{ $row->name }}</a>
                            @endforeach
                        @endif
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

```

保存部分だけでなく表示部分も一緒に書いておきます
次にRoutingを編集します

```php:web.php
// ...
Route::post('/store', [App\Http\Controllers\HomeController::class, 'store'])->name('store');
Route::get('/file/{file}', [App\Http\Controllers\HomeController::class, 'streamFile'])->name('streamFile');
```

:::note info
便利なのでルートモデルバインディングを使用しています
処理の階層構造の考え方によっては、この仕組みはControllerでDBを参照することになるので許されませんが、今回はデモということで使います
:::

## 3. 保存処理を追加する

Routingで定義したstoreメソッドを書いていきます

```php:HomeController.php
// ...
    public function store(Request $request, File $file)
    {
        if (empty($request->file) === false) {
            $file->name = $request->file('file')->getClientOriginalName();
            $file->content = $request->file('file')->get();
            $file->save();
        }

        return redirect()->route('home');
    }
```

DIもじゃんじゃん使っていきます

## 4. 表示処理を追加する

Routingで定義したstreamFileメソッドを書いていきます

```php:HomeController.php
// ...
    public function streamFile(File $file)
    {
        $callback = function () use ($file) {
            // 出力バッファをopen
            $stream = fopen('php://output', 'w');
            fwrite($stream, $file->content);
            fclose($stream);
        };

        $header = [
            'Content-Type' => 'application/octet-stream',
        ];

        return response()->streamDownload($callback, $file->name, $header);
    }
```

表示画面作成時に$filesを追記したので、indexメソッドにも追記します

```php:HomeController.php
// ...
   public function index()
    {
        $files = File::all();
        return view('home', compact('files'));
    }
```

## end.Githubのリポジトリ

記事内で作成したもののソースコードは以下のリポジトリに公開しています

https://github.com/bayuTiger/laravel-binary-file