はじめに (対象読者・この記事でわかること)

この記事は、PHPでWebアプリを開発しているエンジニア(初心者から中級者まで)を対象としています。特に、音声ファイル(.mp3・.wav など)をフォームから受け取り、MySQL などのリレーショナルデータベースに保存しようとして「アップロードできない」エラーに直面した方に向けた内容です。本記事を読むことで、以下ができるようになります。

  • ファイルアップロード時に起こり得る PHP の設定・コード上の問題点を特定できる
  • 音声ファイルを BLOB 型で直接保存する方法と、サーバー側に保存してパスだけを DB に入れる方法の両方を実装できる
  • よくあるエラーメッセージ(例:UPLOAD_ERR_INI_SIZESQLSTATE[HY000]: General error: 1366 Incorrect string value)の原因と対処法を理解できる

このテーマを選んだ背景は、実務で音声データを扱う機会が増えている一方、設定ミスやファイルサイズ制限でつまずくケースが多いため、具体的な解決手順をまとめたく思ったことにあります。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。
HTML フォームと PHP の基本的な受け取り処理
MySQL(または MariaDB)でのテーブル作成と基本的な CRUD 操作
* 基本的なサーバー環境(Apache/Nginx)での PHP 設定ファイル(php.ini)の編集方法

概要と背景

Web アプリで音声ファイルを扱う場面は、ポッドキャスト配信サイトや音声認識システム、教育用教材管理など多岐にわたります。PHP で「ファイルをデータベースに保存したい」ケースは大きく二つに分かれます。

  1. BLOB に直接保存
    バイナリデータそのものをテーブルの BLOB カラムに格納します。データベースがファイルを一元管理できる点が利点ですが、サイズ制限やバックアップの負荷が増大しやすいです。

  2. ファイルパスだけを保存
    アップロードした音声をサーバーの決められたディレクトリに保存し、その相対パスや絶対パスだけを DB に保持します。大容量ファイルでも扱いやすく、バックアップや CDN 配信が容易です。

どちらの方法でも、PHP のアップロード設定upload_max_filesizepost_max_sizemax_file_uploads)や データベース側の文字コード・カラム型 が不適切だと「保存できない」エラーが発生します。特に音声ファイルはサイズが数 MB から数百 MB と大きくなるため、設定ミスが顕著です。本節では、これらの問題点を概観し、次章で具体的な実装手順へと進みます。

音声ファイルを DB に保存する実装手順

以下では、実際に「音声ファイルをアップロードし、データベースに保存できない」問題を解決するための手順を、BLOB 直接保存ファイルパス保存 の二通りに分けて解説します。

前提:環境設定

項目 推奨設定 設定方法
upload_max_filesize 100M(必要に応じて増やす) php.iniupload_max_filesize = 100M
post_max_size 110Mupload_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:テーブル作成

Sql
CREATE 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:テーブル作成

Sql
CREATE 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:アップロード先ディレクトリの準備

Bash
mkdir -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.iniupload_max_filesize が小さい upload_max_filesizepost_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 755chown www-data:www-data で権限付与、mkdir -p でディレクトリ作成
max_allowed_packet エラー MySQL が受け取れる最大パケットサイズを超えた my.cnfmax_allowed_packet を増やす(例: 64M
PDOException: SQLSTATE[HY000]: General error: 2006 MySQL server has gone away 大容量 BLOB を一度に送信しすぎた ストリーム(PDO::PARAM_LOB + fopen)で分割送信、またはファイルパス保存へ切り替える

特に 文字コードの不一致サイズ制限 は初心者が最初にぶつかる壁です。エラーログ(error_log)を確認し、どの段階で止まっているかを追う習慣をつけると、原因特定が格段に楽になります。

まとめ

本記事では、PHP で音声ファイルのアップロードが失敗する典型的な原因 と、BLOB 直接保存ファイルパス保存 の二つの実装パターンを具体的なコード例とともに解説しました。

  • 設定の見直しphp.inimy.cnf のサイズ関連パラメータは必ず調整する
  • BLOB 保存:小容量・トランザクション管理が重要なケースで有効。PDO::PARAM_LOB と文字コード utf8mb4 に注意
  • ファイルパス保存:大容量・高アクセスが想定される場合に推奨。安全なファイル名生成とディレクトリ権限が鍵

これらを踏まえることで、音声ファイルのアップロードに関する多くの障壁をクリアでき、スムーズに機能実装へと移行できます。次回は CDN 連携やストリーミング配信 の実装例、さらに セキュリティ強化策 について掘り下げる予定です。

参考資料