# LaravelとNext.jsでBlobファイルの作成と取扱い

LaravelとNext.jsを使用したアプリケーションで、Blobファイルの作成と取扱いについて解説します。サーバー側(Laravel)とクライアント側(Next.js)でのBlobの生成、処理、連携方法を実装例とともに紹介します。

# Blobとは

Blob(Binary Large Object)は、JavaScriptでバイナリデータを扱うためのオブジェクトです。画像、動画、PDFなど、あらゆる種類のバイナリデータを格納できます。特に大きなファイルを扱う際に威力を発揮します。

# Blobの特徴

  • バイナリデータの扱い: 画像や動画などのバイナリデータを扱える
  • メモリ効率: 大きなファイルも効率的に処理できる
  • 型指定: MIMEタイプを指定して、ファイルの種類を明確にできる
  • URL生成: URL.createObjectURL()で一時的なURLを生成できる
  • ストリーミング対応: 大きなファイルを分割して処理できる

# LaravelでのBlob作成と取扱い

# LaravelでBlobデータを生成

Laravelでバイナリデータを生成し、Blobとして扱う方法です。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;

class FileController extends Controller
{
    /**
     * 動的にBlobデータを生成
     */
    public function generateBlob()
    {
        // バイナリデータを生成(例:画像の生成など)
        $imageData = $this->generateImageData();
        
        return response($imageData, 200)
            ->header('Content-Type', 'image/png')
            ->header('Content-Disposition', 'inline; filename="generated.png"');
    }
    
    /**
     * データベースからBlobデータを取得
     */
    public function getBlobFromDatabase($id)
    {
        $file = File::findOrFail($id);
        
        // BLOBカラムからデータを取得
        $blobData = $file->data;
        
        return response($blobData, 200)
            ->header('Content-Type', $file->mime_type)
            ->header('Content-Length', strlen($blobData));
    }
    
    /**
     * ストレージからBlobデータを取得
     */
    public function getBlobFromStorage($path)
    {
        if (!Storage::exists($path)) {
            abort(404, 'ファイルが見つかりません');
        }
        
        $fileContent = Storage::get($path);
        $mimeType = Storage::mimeType($path);
        
        return response($fileContent, 200)
            ->header('Content-Type', $mimeType)
            ->header('Content-Length', strlen($fileContent));
    }
}

# LaravelでBlobデータをストリーミング

大きなファイルをストリーミングで配信する方法です。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;

class FileController extends Controller
{
    /**
     * 大きなファイルをストリーミングで配信
     */
    public function streamBlob($id)
    {
        $file = File::findOrFail($id);
        $filePath = storage_path('app/files/' . $file->path);
        
        if (!file_exists($filePath)) {
            abort(404, 'ファイルが見つかりません');
        }
        
        return response()->streamDownload(function () use ($filePath) {
            $handle = fopen($filePath, 'rb');
            $chunkSize = 1024 * 1024; // 1MB
            
            while (!feof($handle)) {
                echo fread($handle, $chunkSize);
                flush();
            }
            
            fclose($handle);
        }, $file->filename, [
            'Content-Type' => $file->mime_type,
        ]);
    }
}

# LaravelでBlobデータを保存

アップロードされたBlobデータを保存する方法です。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use App\Models\File;

class FileController extends Controller
{
    /**
     * Blobデータをアップロードして保存
     */
    public function uploadBlob(Request $request)
    {
        $request->validate([
            'file' => 'required|file|max:102400', // 100MBまで
            'filename' => 'required|string',
        ]);
        
        $uploadedFile = $request->file('file');
        $filename = $request->input('filename');
        
        // ストレージに保存
        $path = $uploadedFile->store('files', 'public');
        
        // データベースに保存
        $file = File::create([
            'filename' => $filename,
            'path' => $path,
            'mime_type' => $uploadedFile->getMimeType(),
            'size' => $uploadedFile->getSize(),
        ]);
        
        return response()->json([
            'id' => $file->id,
            'path' => $path,
            'url' => Storage::url($path),
        ], 201);
    }
    
    /**
     * バイナリデータを直接保存
     */
    public function saveBinaryData(Request $request)
    {
        $request->validate([
            'data' => 'required|string', // base64エンコードされたデータ
            'filename' => 'required|string',
            'mime_type' => 'required|string',
        ]);
        
        // base64デコード
        $binaryData = base64_decode($request->input('data'));
        
        // ストレージに保存
        $path = 'files/' . $request->input('filename');
        Storage::put($path, $binaryData);
        
        // データベースに保存
        $file = File::create([
            'filename' => $request->input('filename'),
            'path' => $path,
            'mime_type' => $request->input('mime_type'),
            'size' => strlen($binaryData),
        ]);
        
        return response()->json([
            'id' => $file->id,
            'path' => $path,
        ], 201);
    }
}

# Next.jsでのBlob作成と取扱い

# Next.jsでBlobを作成

Next.js(クライアント側)でBlobを作成する方法です。

// app/components/BlobCreator.tsx
'use client';

import { useState } from 'react';

export default function BlobCreator() {
  const [blob, setBlob] = useState<Blob | null>(null);
  
  /**
   * テキストからBlobを作成
   */
  const createTextBlob = (text: string, mimeType: string = 'text/plain') => {
    const blob = new Blob([text], { type: mimeType });
    setBlob(blob);
    return blob;
  };
  
  /**
   * ArrayBufferからBlobを作成
   */
  const createBlobFromArrayBuffer = (arrayBuffer: ArrayBuffer, mimeType: string) => {
    const blob = new Blob([arrayBuffer], { type: mimeType });
    setBlob(blob);
    return blob;
  };
  
  /**
   * 複数のチャンクを結合してBlobを作成
   */
  const createBlobFromChunks = (chunks: Uint8Array[], mimeType: string) => {
    const blob = new Blob(chunks, { type: mimeType });
    setBlob(blob);
    return blob;
  };
  
  /**
   * 画像からBlobを作成
   */
  const createBlobFromImage = async (imageUrl: string): Promise<Blob> => {
    const response = await fetch(imageUrl);
    const blob = await response.blob();
    setBlob(blob);
    return blob;
  };
  
  return (
    <div>
      {/* Blob作成のUI */}
    </div>
  );
}

# Next.jsでBlobをダウンロード

Blobをローカルにダウンロードする方法です。

// app/utils/blobDownload.ts
export function downloadBlob(blob: Blob, filename: string) {
  const url = URL.createObjectURL(blob);
  const link = document.createElement('a');
  link.href = url;
  link.download = filename;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
  URL.revokeObjectURL(url);
}

// 使用例
export function useBlobDownload() {
  const download = async (url: string, filename: string) => {
    try {
      const response = await fetch(url);
      const blob = await response.blob();
      downloadBlob(blob, filename);
    } catch (error) {
      console.error('ダウンロードに失敗しました:', error);
    }
  };
  
  return { download };
}

# Next.jsでBlobを表示

Blobを画像や動画として表示する方法です。

// app/components/BlobViewer.tsx
'use client';

import { useState, useEffect } from 'react';

interface BlobViewerProps {
  blob: Blob;
  mimeType: string;
}

export default function BlobViewer({ blob, mimeType }: BlobViewerProps) {
  const [objectUrl, setObjectUrl] = useState<string | null>(null);
  
  useEffect(() => {
    const url = URL.createObjectURL(blob);
    setObjectUrl(url);
    
    return () => {
      URL.revokeObjectURL(url);
    };
  }, [blob]);
  
  if (!objectUrl) {
    return <div>読み込み中...</div>;
  }
  
  if (mimeType.startsWith('image/')) {
    return <img src={objectUrl} alt="Blob image" />;
  }
  
  if (mimeType.startsWith('video/')) {
    return <video src={objectUrl} controls />;
  }
  
  return <a href={objectUrl} download>ダウンロード</a>;
}

# Next.js API RouteでBlobを処理

Next.jsのAPI RouteでBlobを処理する方法です。

// app/api/files/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const fileId = searchParams.get('id');
  
  if (!fileId) {
    return NextResponse.json({ error: 'File ID is required' }, { status: 400 });
  }
  
  // Laravel APIからBlobデータを取得
  const laravelUrl = `${process.env.LARAVEL_API_URL}/api/files/${fileId}`;
  const response = await fetch(laravelUrl, {
    headers: {
      'Authorization': `Bearer ${process.env.LARAVEL_API_TOKEN}`,
    },
  });
  
  if (!response.ok) {
    return NextResponse.json({ error: 'File not found' }, { status: 404 });
  }
  
  const blob = await response.blob();
  const arrayBuffer = await blob.arrayBuffer();
  
  return new NextResponse(arrayBuffer, {
    headers: {
      'Content-Type': blob.type,
      'Content-Length': blob.size.toString(),
    },
  });
}

export async function POST(request: NextRequest) {
  const formData = await request.formData();
  const file = formData.get('file') as File;
  
  if (!file) {
    return NextResponse.json({ error: 'File is required' }, { status: 400 });
  }
  
  // Blobデータを取得
  const blob = await file.arrayBuffer();
  
  // Laravel APIに送信
  const laravelUrl = `${process.env.LARAVEL_API_URL}/api/files`;
  const response = await fetch(laravelUrl, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.LARAVEL_API_TOKEN}`,
      'Content-Type': file.type,
    },
    body: blob,
  });
  
  const data = await response.json();
  
  return NextResponse.json(data);
}

# LaravelとNext.jsの連携

# Laravel APIからBlobを取得してNext.jsで処理

// app/components/FileDownloader.tsx
'use client';

import { useState } from 'react';

export default function FileDownloader() {
  const [loading, setLoading] = useState(false);
  
  const downloadFile = async (fileId: number) => {
    setLoading(true);
    try {
      // Laravel APIからBlobデータを取得
      const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/files/${fileId}`, {
        headers: {
          'Authorization': `Bearer ${localStorage.getItem('token')}`,
        },
      });
      
      if (!response.ok) {
        throw new Error('ファイルの取得に失敗しました');
      }
      
      // Blobに変換
      const blob = await response.blob();
      
      // ファイル名を取得(Content-Dispositionヘッダーから)
      const contentDisposition = response.headers.get('Content-Disposition');
      const filename = contentDisposition
        ? contentDisposition.split('filename=')[1]?.replace(/"/g, '')
        : `file-${fileId}`;
      
      // ダウンロード
      const url = URL.createObjectURL(blob);
      const link = document.createElement('a');
      link.href = url;
      link.download = filename;
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
      URL.revokeObjectURL(url);
    } catch (error) {
      console.error('ダウンロードエラー:', error);
    } finally {
      setLoading(false);
    }
  };
  
  return (
    <button onClick={() => downloadFile(1)} disabled={loading}>
      {loading ? 'ダウンロード中...' : 'ファイルをダウンロード'}
    </button>
  );
}

# Next.jsからLaravelにBlobをアップロード

// app/components/FileUploader.tsx
'use client';

import { useState } from 'react';

export default function FileUploader() {
  const [uploading, setUploading] = useState(false);
  
  const uploadFile = async (file: File) => {
    setUploading(true);
    try {
      // FormDataを作成
      const formData = new FormData();
      formData.append('file', file);
      formData.append('filename', file.name);
      
      // Laravel APIにアップロード
      const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/files`, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${localStorage.getItem('token')}`,
        },
        body: formData,
      });
      
      if (!response.ok) {
        throw new Error('アップロードに失敗しました');
      }
      
      const data = await response.json();
      console.log('アップロード成功:', data);
    } catch (error) {
      console.error('アップロードエラー:', error);
    } finally {
      setUploading(false);
    }
  };
  
  return (
    <input
      type="file"
      onChange={(e) => {
        const file = e.target.files?.[0];
        if (file) {
          uploadFile(file);
        }
      }}
      disabled={uploading}
    />
  );
}

# ストリーミング処理の連携

大きなファイルをストリーミングで処理する例です。

// app/utils/streamBlob.ts
export async function streamBlobFromLaravel(
  fileId: number,
  onProgress?: (progress: number) => void
): Promise<Blob> {
  const response = await fetch(
    `${process.env.NEXT_PUBLIC_API_URL}/api/files/${fileId}/stream`,
    {
      headers: {
        'Authorization': `Bearer ${localStorage.getItem('token')}`,
      },
    }
  );
  
  if (!response.ok) {
    throw new Error('ストリーミングに失敗しました');
  }
  
  const reader = response.body?.getReader();
  if (!reader) {
    throw new Error('リーダーを取得できませんでした');
  }
  
  const chunks: Uint8Array[] = [];
  const contentLength = parseInt(response.headers.get('Content-Length') || '0', 10);
  let received = 0;
  
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    
    chunks.push(value);
    received += value.length;
    
    if (onProgress && contentLength > 0) {
      const progress = (received / contentLength) * 100;
      onProgress(progress);
    }
  }
  
  return new Blob(chunks, { type: response.headers.get('Content-Type') || 'application/octet-stream' });
}

# 実用的な例

# 画像編集アプリケーション

// app/components/ImageEditor.tsx
'use client';

import { useState, useRef } from 'react';

export default function ImageEditor() {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [imageBlob, setImageBlob] = useState<Blob | null>(null);
  
  const loadImage = async (file: File) => {
    const blob = await file.arrayBuffer();
    const imageBlob = new Blob([blob], { type: file.type });
    setImageBlob(imageBlob);
    
    const img = new Image();
    img.src = URL.createObjectURL(imageBlob);
    img.onload = () => {
      const canvas = canvasRef.current;
      if (canvas) {
        canvas.width = img.width;
        canvas.height = img.height;
        const ctx = canvas.getContext('2d');
        ctx?.drawImage(img, 0, 0);
      }
    };
  };
  
  const saveImage = async () => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    
    canvas.toBlob(async (blob) => {
      if (!blob) return;
      
      // Laravel APIに保存
      const formData = new FormData();
      formData.append('file', blob, 'edited-image.png');
      
      await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/files`, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${localStorage.getItem('token')}`,
        },
        body: formData,
      });
    }, 'image/png');
  };
  
  return (
    <div>
      <input type="file" accept="image/*" onChange={(e) => {
        const file = e.target.files?.[0];
        if (file) loadImage(file);
      }} />
      <canvas ref={canvasRef} />
      <button onClick={saveImage}>保存</button>
    </div>
  );
}

# 注意点とベストプラクティス

# 1. メモリ管理

Blobオブジェクトはメモリを消費するため、使用後は適切に解放する必要があります。

useEffect(() => {
  const url = URL.createObjectURL(blob);
  
  return () => {
    URL.revokeObjectURL(url); // クリーンアップ
  };
}, [blob]);

# 2. エラーハンドリング

Blobの処理中にエラーが発生する可能性があるため、適切なエラーハンドリングが必要です。

try {
  const blob = await response.blob();
  // 処理
} catch (error) {
  console.error('Blob処理エラー:', error);
  // エラー処理
}

# 3. ファイルサイズの制限

大きなファイルを扱う場合は、ストリーミング処理を検討します。

# 4. CORS設定

LaravelとNext.jsが異なるドメインで動作する場合、CORS設定が必要です。

// Laravel: config/cors.php または middleware
header('Access-Control-Allow-Origin: http://localhost:3000');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

# 実装時の注意点

LaravelとNext.jsでBlobファイルを扱う際のポイント:

  • Laravel側: バイナリデータの生成、保存、配信を効率的に処理
  • Next.js側: クライアント側でBlobを作成、表示、ダウンロード
  • 連携: APIを通じてBlobデータを送受信し、ストリーミング処理も可能
  • メモリ管理: 適切なクリーンアップでメモリリークを防止

適切に実装することで、効率的でユーザーフレンドリーなファイル処理機能を提供できる。

2026-01-02

同じタグを持つ記事をピックアップしました。