Open1

AIメモ

手洗う手洗う

Laravelでファイルアップロードを最適化する4つの方法 - メモリ効率を考慮した実装パターン

はじめに

Laravelでファイルアップロード処理を実装する際、ファイルサイズによってはメモリ不足やタイムアウトが発生することがあります。本記事では、ファイルサイズに応じた最適な実装方法を4つのパターンで解説します。

環境

  • Laravel 8.x / 9.x / 10.x
  • PHP 8.0+
  • AWS S3(Laravel Flysystem)

1. シンプルな直接アップロード

最も基本的な実装方法です。小さなファイルには十分ですが、大きなファイルには不向きです。

// ファイル全体をメモリに読み込んでアップロード
Storage::disk('s3')->put($targetFile, file_get_contents($sourceFile));

メリット

  • 実装がシンプル
  • コードが読みやすい

デメリット

  • ファイル全体をメモリに読み込むため、大きなファイルでメモリ不足になる
  • memory_limitの制限を受ける

適用シーン

  • 画像のサムネイル(〜5MB)
  • CSVファイル(〜10MB)
  • 小規模なドキュメント

2. ストリームを使った基本的なアップロード

ファイルポインタを渡すことで、Laravelが内部でストリーム処理を行います。

// ファイルストリームとして開いてアップロード
Storage::disk('s3')->put($targetFile, fopen($sourceFile, 'r+'));

メリット

  • メモリ効率が良い
  • Laravelが自動的にストリーム処理を行う
  • 中規模ファイルに最適

デメリット

  • ストリーム処理の詳細をコントロールできない

適用シーン

  • PDFファイル(10〜50MB)
  • 動画ファイル(〜100MB)
  • ログファイル

3. 明示的なストリーム処理(読み込み&追記)

ストリーム処理を明示的に制御し、大規模ファイルの処理に対応します。

// S3からストリームとして読み込み
$sourceStream = Storage::disk('s3')->getDriver()->readStream($sourceFileDnS3);

// ローカルファイルに追記モードで書き込み
file_put_contents($targetFile, stream_get_contents($sourceStream), FILE_APPEND);

メリット

  • メモリ効率が最も良い
  • FILE_APPENDにより分割処理も可能
  • プログレスバーの実装が可能

デメリット

  • コードがやや複雑
  • エラーハンドリングに注意が必要

適用シーン

  • 大規模なバックアップファイル(数GB)
  • ビデオファイル(数百MB〜)
  • データベースダンプファイル

4. クラウドストレージ間の直接転送

S3からSFTPサーバーなど、異なるストレージ間での効率的な転送を実現します。

// S3からストリームとして読み込み
$sourceStream = Storage::disk('s3')->getDriver()->readStream($sourceFile);

// SFTPサーバーに直接ストリーム転送
Storage::disk('sftp')->put($targetFile, $sourceStream);

メリット

  • サーバーのメモリをほとんど使用しない
  • クラウド間の高速転送
  • 複数のストレージサービス間の連携が容易

デメリット

  • ネットワーク帯域に依存
  • エラー時のリトライ処理が必要

適用シーン

  • S3からGCSへの移行
  • バックアップの冗長化
  • CDN配信用のファイル複製

実装時の注意点

1. タイムアウト設定

大きなファイルを扱う場合は、必ずタイムアウト設定を調整してください。

// PHP実行時間の延長
set_time_limit(300); // 5分

// またはLaravelのジョブとして実装
class UploadLargeFile implements ShouldQueue
{
    public $timeout = 300;
    // ...
}

2. エラーハンドリング

ストリーム処理では適切なエラーハンドリングが重要です。

try {
    $sourceStream = Storage::disk('s3')->getDriver()->readStream($sourceFile);
    if (!$sourceStream) {
        throw new Exception('ストリームの取得に失敗しました');
    }
    
    Storage::disk('sftp')->put($targetFile, $sourceStream);
    
} catch (Exception $e) {
    Log::error('ファイルアップロードエラー: ' . $e->getMessage());
    // リトライ処理やユーザーへの通知
} finally {
    if (isset($sourceStream) && is_resource($sourceStream)) {
        fclose($sourceStream);
    }
}

3. メモリ使用量の監視

本番環境では、メモリ使用量を監視することを推奨します。

// 処理前のメモリ使用量
$startMemory = memory_get_usage();

// ファイル処理
// ...

// 処理後のメモリ使用量
$endMemory = memory_get_usage();
$usedMemory = ($endMemory - $startMemory) / 1024 / 1024; // MB単位

Log::info("メモリ使用量: {$usedMemory} MB");

パフォーマンス比較

ファイルサイズ 方法1(直接) 方法2(基本ストリーム) 方法3(明示的ストリーム) 方法4(クラウド間)
1MB ◎ 最速 ○ 高速 ○ 高速 △ やや遅い
100MB × メモリ不足 ◎ 最適 ◎ 最適 ○ 高速
1GB × 不可 △ 可能 ◎ 最適 ◎ 最適
10GB × 不可 × 困難 ○ 可能 ◎ 最適

まとめ

Laravelでファイルアップロードを実装する際は、ファイルサイズと用途に応じて適切な方法を選択することが重要です。

  • 小規模ファイル(〜10MB): シンプルなfile_get_contents()で十分
  • 中規模ファイル(10MB〜100MB): 基本的なストリーム処理を使用
  • 大規模ファイル(100MB〜): 明示的なストリーム処理やクラウド間転送を検討
  • 本番環境: 必ずエラーハンドリングとタイムアウト設定を実装

ストリーム処理を適切に使用することで、メモリ効率的で安定したファイルアップロード機能を実現できます。

参考リンク