はじめに (対象読者・この記事でわかること)
この記事は、PHPでWebアプリを開発しているエンジニア(初心者から中級者まで)を対象としています。特に、音声ファイル(.mp3・.wav など)をフォームから受け取り、MySQL などのリレーショナルデータベースに保存しようとして「アップロードできない」エラーに直面した方に向けた内容です。本記事を読むことで、以下ができるようになります。
- ファイルアップロード時に起こり得る PHP の設定・コード上の問題点を特定できる
- 音声ファイルを BLOB 型で直接保存する方法と、サーバー側に保存してパスだけを DB に入れる方法の両方を実装できる
- よくあるエラーメッセージ(例:
UPLOAD_ERR_INI_SIZE、SQLSTATE[HY000]: General error: 1366 Incorrect string value)の原因と対処法を理解できる
このテーマを選んだ背景は、実務で音声データを扱う機会が増えている一方、設定ミスやファイルサイズ制限でつまずくケースが多いため、具体的な解決手順をまとめたく思ったことにあります。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
HTML フォームと PHP の基本的な受け取り処理
MySQL(または MariaDB)でのテーブル作成と基本的な CRUD 操作
* 基本的なサーバー環境(Apache/Nginx)での PHP 設定ファイル(php.ini)の編集方法
概要と背景
Web アプリで音声ファイルを扱う場面は、ポッドキャスト配信サイトや音声認識システム、教育用教材管理など多岐にわたります。PHP で「ファイルをデータベースに保存したい」ケースは大きく二つに分かれます。
-
BLOB に直接保存
バイナリデータそのものをテーブルのBLOBカラムに格納します。データベースがファイルを一元管理できる点が利点ですが、サイズ制限やバックアップの負荷が増大しやすいです。 -
ファイルパスだけを保存
アップロードした音声をサーバーの決められたディレクトリに保存し、その相対パスや絶対パスだけを DB に保持します。大容量ファイルでも扱いやすく、バックアップや CDN 配信が容易です。
どちらの方法でも、PHP のアップロード設定(upload_max_filesize、post_max_size、max_file_uploads)や データベース側の文字コード・カラム型 が不適切だと「保存できない」エラーが発生します。特に音声ファイルはサイズが数 MB から数百 MB と大きくなるため、設定ミスが顕著です。本節では、これらの問題点を概観し、次章で具体的な実装手順へと進みます。
音声ファイルを DB に保存する実装手順
以下では、実際に「音声ファイルをアップロードし、データベースに保存できない」問題を解決するための手順を、BLOB 直接保存 と ファイルパス保存 の二通りに分けて解説します。
前提:環境設定
| 項目 | 推奨設定 | 設定方法 |
|---|---|---|
upload_max_filesize |
100M(必要に応じて増やす) |
php.ini で upload_max_filesize = 100M |
post_max_size |
110M(upload_max_filesize より大きく) |
同上 post_max_size = 110M |
max_execution_time |
300(長時間のアップロードに備える) |
同上 max_execution_time = 300 |
MySQL max_allowed_packet |
64M 以上 |
my.cnf の [mysqld] に max_allowed_packet=64M を追加し再起動 |
| テーブルの文字コード | utf8mb4 |
CREATE DATABASE … CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; |
設定変更後は、Web サーバーとデータベースの再起動を忘れずに行ってください。
BLOB 直接保存パターン
ステップ1:テーブル作成
SqlCREATE TABLE audio_files ( id INT AUTO_INCREMENT PRIMARY KEY, filename VARCHAR(255) NOT NULL, mime_type VARCHAR(100) NOT NULL, data LONGBLOB NOT NULL, uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
LONGBLOBは最大 4GB まで格納可能です。音声ファイルは通常 100MB 未満であれば問題なく収まります。
ステップ2:HTML フォーム
Html<form action="upload_blob.php" method="post" enctype="multipart/form-data"> <label for="audio">音声ファイルを選択:</label> <input type="file" name="audio" id="audio" accept="audio/*" required> <button type="submit">アップロード</button> </form>
accept="audio/*"でブラウザ側に音声ファイルだけ選択させます。
ステップ3:PHP アップロード処理(upload_blob.php)
Php<?php // 1. エラーチェック if ($_FILES['audio']['error'] !== UPLOAD_ERR_OK) { die('ファイルアップロードエラー: ' . $_FILES['audio']['error']); } // 2. 許可する MIME タイプを限定 $allowed = ['audio/mpeg', 'audio/wav', 'audio/ogg']; $finfo = new finfo(FILEINFO_MIME_TYPE); $mime = $finfo->file($_FILES['audio']['tmp_name']); if (!in_array($mime, $allowed, true)) { die('対応していない音声形式です。'); } // 3. ファイル内容をバイナリとして取得 $binary = file_get_contents($_FILES['audio']['tmp_name']); $filename = basename($_FILES['audio']['name']); // 4. DB 接続(PDO 推奨) $dsn = 'mysql:host=localhost;dbname=yourdb;charset=utf8mb4'; $pdo = new PDO($dsn, 'dbuser', 'dbpass', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ]); // 5. プレースホルダーで安全に INSERT $sql = "INSERT INTO audio_files (filename, mime_type, data) VALUES (:name, :mime, :data)"; $stmt = $pdo->prepare($sql); $stmt->bindParam(':name', $filename); $stmt->bindParam(':mime', $mime); $stmt->bindParam(':data', $binary, PDO::PARAM_LOB); $stmt->execute(); echo 'アップロードが完了しました。'; ?>
PDO::PARAM_LOBを指定しないと文字列として扱われ、文字コードエラー(Incorrect string value)が起きることがあります。file_get_contentsで取得したバイナリはメモリに読み込むので、極端に大きいファイルはstreamを使うことを推奨します(後述)。
ステップ4:BLOB 取得例
Php<?php $id = $_GET['id'] ?? 0; $stmt = $pdo->prepare('SELECT filename, mime_type, data FROM audio_files WHERE id = :id'); $stmt->execute([':id' => $id]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if ($row) { header('Content-Type: ' . $row['mime_type']); header('Content-Disposition: inline; filename="' . $row['filename'] . '"'); echo $row['data']; exit; } ?>
- 取得時に
Content-Typeを正しく設定すると、ブラウザで直接再生できます。
ファイルパス保存パターン
ステップ1:テーブル作成
SqlCREATE TABLE audio_files_path ( id INT AUTO_INCREMENT PRIMARY KEY, filename VARCHAR(255) NOT NULL, mime_type VARCHAR(100) NOT NULL, path VARCHAR(500) NOT NULL, uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
pathカラムはファイルが保存されたサーバー上の相対パスです。
ステップ2:アップロード先ディレクトリの準備
Bashmkdir -p /var/www/html/uploads/audio chmod 755 /var/www/html/uploads/audio
- 外部から直接アクセスできないように
.htaccessで禁止したり、PHP だけが書き込める権限にしておくと安全です。
ステップ3:HTML フォームは BLOB パターンと同様
ステップ4:PHP アップロード処理(upload_path.php)
Php<?php if ($_FILES['audio']['error'] !== UPLOAD_ERR_OK) { die('アップロードエラー: ' . $_FILES['audio']['error']); } $allowed = ['audio/mpeg', 'audio/wav', 'audio/ogg']; $finfo = new finfo(FILEINFO_MIME_TYPE); $mime = $finfo->file($_FILES['audio']['tmp_name']); if (!in_array($mime, $allowed, true)) { die('対応していない形式です。'); } // 保存先ディレクトリとファイル名の決定 $uploadDir = __DIR__ . '/uploads/audio/'; $ext = pathinfo($_FILES['audio']['name'], PATHINFO_EXTENSION); $basename = bin2hex(random_bytes(8)); // 衝突防止 $targetPath = $uploadDir . $basename . '.' . $ext; // 移動 if (!move_uploaded_file($_FILES['audio']['tmp_name'], $targetPath)) { die('ファイルの保存に失敗しました。'); } // DB にパスだけ保存 $relativePath = 'uploads/audio/' . $basename . '.' . $ext; $filename = $_FILES['audio']['name']; $dsn = 'mysql:host=localhost;dbname=yourdb;charset=utf8mb4'; $pdo = new PDO($dsn, 'dbuser', 'dbpass', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ]); $sql = "INSERT INTO audio_files_path (filename, mime_type, path) VALUES (:name, :mime, :path)"; $stmt = $pdo->prepare($sql); $stmt->execute([ ':name' => $filename, ':mime' => $mime, ':path' => $relativePath ]); echo 'アップロード成功!'; ?>
move_uploaded_fileは一時領域から目的ディレクトリへ安全に移動します。- ランダム文字列でファイル名を再生成することで、同名ファイルの上書きを防止します。
ステップ5:保存した音声の配信
Php<?php $id = $_GET['id'] ?? 0; $stmt = $pdo->prepare('SELECT filename, mime_type, path FROM audio_files_path WHERE id = :id'); $stmt->execute([':id' => $id]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if ($row && file_exists(__DIR__ . '/' . $row['path'])) { header('Content-Type: ' . $row['mime_type']); header('Content-Disposition: inline; filename="' . $row['filename'] . '"'); readfile(__DIR__ . '/' . $row['path']); exit; } die('ファイルが見つかりません。'); ?>
readfileでストリームしながら出力すれば、メモリ使用量を抑制できます。
ハマった点やエラー解決
| エラーメッセージ | 原因 | 解決策 |
|---|---|---|
UPLOAD_ERR_INI_SIZE (1) |
php.ini の upload_max_filesize が小さい |
upload_max_filesize と post_max_size を拡張 |
SQLSTATE[HY000]: General error: 1366 Incorrect string value |
BLOB ではなく VARCHAR にバイナリを入れようとした、または文字コードが utf8mb4 でない |
カラムを LONGBLOB に変更、接続時に charset=utf8mb4 を指定 |
File could not be uploaded (move_uploaded_file が false) |
書き込み権限がない、またはディレクトリが存在しない | chmod 755/chown www-data:www-data で権限付与、mkdir -p でディレクトリ作成 |
max_allowed_packet エラー |
MySQL が受け取れる最大パケットサイズを超えた | my.cnf の max_allowed_packet を増やす(例: 64M) |
PDOException: SQLSTATE[HY000]: General error: 2006 MySQL server has gone away |
大容量 BLOB を一度に送信しすぎた | ストリーム(PDO::PARAM_LOB + fopen)で分割送信、またはファイルパス保存へ切り替える |
特に 文字コードの不一致 と サイズ制限 は初心者が最初にぶつかる壁です。エラーログ(error_log)を確認し、どの段階で止まっているかを追う習慣をつけると、原因特定が格段に楽になります。
まとめ
本記事では、PHP で音声ファイルのアップロードが失敗する典型的な原因 と、BLOB 直接保存 と ファイルパス保存 の二つの実装パターンを具体的なコード例とともに解説しました。
- 設定の見直し:
php.ini・my.cnfのサイズ関連パラメータは必ず調整する - BLOB 保存:小容量・トランザクション管理が重要なケースで有効。
PDO::PARAM_LOBと文字コードutf8mb4に注意 - ファイルパス保存:大容量・高アクセスが想定される場合に推奨。安全なファイル名生成とディレクトリ権限が鍵
これらを踏まえることで、音声ファイルのアップロードに関する多くの障壁をクリアでき、スムーズに機能実装へと移行できます。次回は CDN 連携やストリーミング配信 の実装例、さらに セキュリティ強化策 について掘り下げる予定です。
参考資料
- PHP: ファイルアップロード - マニュアル
- MySQL 8.0 Reference Manual – Server System Variables
- Laravel でのファイル保存ベストプラクティス (英語)
- 書籍:『PHPエンジニア養成読本 第2版』技術評論社、2022 年
