バイナリを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
$ 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を導入します
プロジェクトルートに以下のコマンドを入力します
$ composer require laravel/pint --dev
フォーマットをかけたい時は、適宜以下のコマンドを入力しましょう
使い心地が良ければ、aliasを設定してみてください
$ vendor/bin/pint -v
1. テーブルを追加する(Migration, Model)
ファイルを保存するためのテーブル(とついでにModel)を作成します
$ php artisan make:model File --migration
まずmigrationファイルを以下のように編集します
<?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');
}
}
BlueprintのbinaryメソッドによってBLOBのカラムを作成することができますが、カラムサイズが小さいため満足に機能しない可能性があります
悲しいことにスキーマビルダのBlueprintにはLONGBLOBカラムを作成するメソッドが用意されていないので、素のSQLを流し込みます
次にModelを編集します
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class File extends Model
{
protected $fillable = ['name', 'content']; // <-- 追加
}
2. 画面を用意する(View, Routing)
まず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を編集します
// ...
Route::post('/store', [App\Http\Controllers\HomeController::class, 'store'])->name('store');
Route::get('/file/{file}', [App\Http\Controllers\HomeController::class, 'streamFile'])->name('streamFile');
便利なのでルートモデルバインディングを使用しています
処理の階層構造の考え方によっては、この仕組みはControllerでDBを参照することになるので許されませんが、今回はデモということで使います
3. 保存処理を追加する
Routingで定義したstoreメソッドを書いていきます
// ...
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メソッドを書いていきます
// ...
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メソッドにも追記します
// ...
public function index()
{
$files = File::all();
return view('home', compact('files'));
}
end.Githubのリポジトリ
記事内で作成したもののソースコードは以下のリポジトリに公開しています