【連載第8回】PHPで「投稿機能」を作ろう!フォームデータをデータベースに保存する完全ガイド

LAMP講座

【連載第8回】魂を吹き込め!ハリボテの画面を「動くアプリ」に変える

未経験からWebアプリ開発を目指す「AlmaLinux 9とLAMP環境で作る!Webアプリ開発完全ロードマップ(全12回)」の第8回です。

前回は、HTMLとCSSを使って、掲示板の「見た目(フロントエンド)」を作成しました。
しかし、今のままでは投稿ボタンを押しても画面がリロードされるだけで、何も起こりません。

今回は、いよいよ「バックエンド開発」の核心部分に入ります。
ユーザーが入力した名前やコメントを受け取り、画像をサーバーの奥深くに保存し、それらの情報をデータベースに記録する。
この一連の流れをPHPプログラミングで実装します。

ここを乗り越えれば、あなたはもう「Webサイト制作者」ではなく、立派な「Webアプリケーションエンジニア」です。

コウ君

先生、前回作った画面、友達に見せたら「ボタン押しても動かないじゃん」って言われちゃいました…。
早く「動くやつ」を作って見返したいです!
でも、画像を保存するのってすごく難しそう…。

リナックス先生

その悔しさが成長のバネになるわ。
確かに「ファイルアップロード」と「データベース保存」は初心者が最初に挫折する壁よ。
でも、仕組みさえ分かってしまえば恐れることはないわ。
今回は、セキュリティもしっかり意識した「プロの書き方」を教えるから、一行ずつ噛み砕いて理解していきましょう!

本講座のカリキュラム(全12回)

現在地は第8回です。ここが最大の山場です!

  1. サーバー準備編:なぜVPSが必要?AlmaLinux 9の初期設定とSSH接続
  2. Webサーバー編:Apache(httpd)のインストールとファイアウォール設定
  3. データベース編:MariaDB(MySQL)のインストールとセキュリティ設定
  4. プログラミング言語編:PHP 8.xの導入と設定ファイルのチューニング
  5. 権限・パーミッション編:LinuxでWebサイトを公開するための「所有者」の概念
  6. 接続テスト編:PHPからデータベース(DB)に接続してみよう
  7. アプリ開発①:HTML/CSSで掲示板の「見た目」を作る
  8. 【今回】アプリ開発②:投稿機能(Create)の実装とデータの保存
  9. アプリ開発③:一覧表示機能(Read)と画像表示の仕組み
  10. アプリ開発④:編集・削除機能(Update/Delete)の実装
  11. セキュリティ編:XSSやSQLインジェクション対策の基礎
  12. 公開編:独自ドメイン設定と無料SSL(Let’s Encrypt)でHTTPS化

全体像:データはどうやって流れるの?

コードを書く前に、データがどのように処理されるのか、頭の中でイメージを作っておきましょう。
これを理解していないと、エラーが出た時に「どこで詰まったのか」が分かりません。

処理のフローチャート

  1. ブラウザ: ユーザーがフォームに入力して「送信」ボタンを押す。
  2. Webサーバー(Apache): データを受け取り、PHPに渡す。
  3. PHP(今回の主役):
    ・テキストデータ(名前、コメント)を受け取る。
    ・画像ファイルを受け取り、サーバー内の特定フォルダ(/images)に保存する。
    ・「画像のファイル名」と「テキストデータ」をセットにして、データベースへ送る。
  4. データベース(MariaDB): データをテーブルに行として追加(INSERT)する。

手順1:データベースに「テーブル」を作る

まずは、データを保存するための「表(テーブル)」を用意します。
第3回で作った app_db というデータベースの中に、posts というテーブルを作成しましょう。

SSHでVPSに接続し、以下のコマンドでデータベースにログインします。
(パスワードは第3回で設定したものです。例:password123

mysql -u app_user -p

ログインできたら、使用するデータベースを選択します。

USE app_db;

続いて、以下のSQL文をコピー&ペーストして実行してください。

CREATE TABLE posts (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    comment TEXT NOT NULL,
    image_path VARCHAR(255),
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

テーブル構造の解説:

カラム名 意味
id INT 管理番号。AUTO_INCREMENT により、1, 2, 3…と自動で連番が振られます。
name VARCHAR(50) 投稿者のニックネーム(最大50文字)。
comment TEXT コメント本文。長い文章も入るように TEXT 型にします。
image_path VARCHAR(255) 【重要】 ここには画像データそのものではなく、「画像のファイル名(例:photo.jpg)」だけを保存します。画像自体はDBに入れず、ファイルとして保存するのが定石です。
created_at DATETIME 投稿日時。DEFAULT CURRENT_TIMESTAMP により、データが入った瞬間の時間が自動記録されます。

Query OK と表示されたら、exit でログアウトして元の黒い画面に戻りましょう。

手順2:PHPプログラムの実装

それでは、前回作成した index.php を改造して、プログラムを埋め込みます。

vi /var/www/html/bbs/index.php

現在書いてあるHTMLコードの「一番上の行(<!DOCTYPE html>よりも上)」に、以下のPHPコードを挿入してください。
(既存のHTMLコードは消さずに、その上に追記します)

追記するPHPコード(前半部分)

<?php
// エラーを表示する(開発時のみ。本番ではOffにすること)
ini_set('display_errors', 1);
error_reporting(E_ALL);

// DB接続情報
$dsn = 'mysql:dbname=app_db;host=localhost;charset=utf8mb4';
$user = 'app_user';
$password = 'password123';

// 変数の初期化
$success_message = null;
$error_message = null;

// ▼「送信」ボタンが押された時だけ動く処理
if ($_SERVER['REQUEST_METHOD'] === 'POST') {

    // 1. フォームデータの受け取り
    $name = $_POST['name'] ?? '';
    $comment = $_POST['comment'] ?? '';
    $image_filename = null; // 画像がない場合はnullのまま

    // バリデーション(入力チェック)
    if (empty($name) || empty($comment)) {
        $error_message = '名前とコメントは必須です。';
    } else {
        
        // 2. 画像アップロード処理
        // 画像が選択されていて、かつアップロードにエラーがない場合
        if (!empty($_FILES['image']['name']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
            
            // ファイル名が重複しないようにユニークな名前を生成(日時 + ランダム文字列)
            $original_name = $_FILES['image']['name'];
            $extension = pathinfo($original_name, PATHINFO_EXTENSION); // 拡張子を取得(jpg, png等)
            $new_filename = date('YmdHis') . '_' . uniqid() . '.' . $extension;
            
            // 保存先ディレクトリ
            $upload_dir = '/var/www/html/bbs/images/';
            
            // 一時フォルダから本番フォルダへ移動
            if (move_uploaded_file($_FILES['image']['tmp_name'], $upload_dir . $new_filename)) {
                $image_filename = $new_filename; // DB保存用にファイル名を保持
            } else {
                $error_message = '画像のアップロードに失敗しました。';
            }
        }

        // 3. データベースへの保存処理(エラーがなければ実行)
        if (empty($error_message)) {
            try {
                $pdo = new PDO($dsn, $user, $password);
                $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

                // SQLインジェクション対策(プリペアドステートメント)
                $sql = "INSERT INTO posts (name, comment, image_path) VALUES (:name, :comment, :image)";
                $stmt = $pdo->prepare($sql);

                // 値をセットして実行
                $stmt->bindValue(':name', $name, PDO::PARAM_STR);
                $stmt->bindValue(':comment', $comment, PDO::PARAM_STR);
                $stmt->bindValue(':image', $image_filename, $image_filename ? PDO::PARAM_STR : PDO::PARAM_NULL);

                $stmt->execute();

                $success_message = '投稿しました!';

            } catch (PDOException $e) {
                $error_message = 'DBエラー: ' . $e->getMessage();
            }
        }
    }
}
?>

※注意: 画像保存先のディレクトリパス /var/www/html/bbs/images/ は、第5回で作成した権限設定済みのフォルダです。

HTML部分の微修正(メッセージ表示)

PHPで処理した結果(成功メッセージやエラー)を画面に表示させるために、HTML側の <main> タグのすぐ下に、以下のコードを書き加えてください。

    <main>
        <!-- メッセージ表示エリア -->
        <?php if ($success_message): ?>
            <div style="color: green; background: #e6fffa; padding: 10px; margin-bottom: 10px; border: 1px solid green; border-radius: 4px;">
                <?php echo htmlspecialchars($success_message, ENT_QUOTES, 'UTF-8'); ?>
            </div>
        <?php endif; ?>

        <?php if ($error_message): ?>
            <div style="color: red; background: #ffe6e6; padding: 10px; margin-bottom: 10px; border: 1px solid red; border-radius: 4px;">
                <?php echo htmlspecialchars($error_message, ENT_QUOTES, 'UTF-8'); ?>
            </div>
        <?php endif; ?>

        <!-- 以下、既存のフォーム ... -->

コードの徹底解説(ここがプロの技)

ただ動けばいいというコードではありません。実務で求められる安全性を意識したポイントが3つあります。

1. 画像ファイル名のユニーク化

$new_filename = date('YmdHis') . '_' . uniqid() . '.' . $extension;

ユーザーがアップロードしたファイル名(例: photo.jpg)をそのまま使うのは非常に危険です。
もし別のユーザーが偶然同じ photo.jpg という名前で別の画像をアップロードしたら、前の画像が上書きされて消えてしまうからです。

そのため、date('YmdHis')(現在日時)と uniqid()(ランダムなID)を組み合わせて、「絶対に他人と被らないファイル名」を自動生成して保存しています。

2. 画像の一時ファイル処理(move_uploaded_file)

フォームから送信された画像は、まずサーバー内の「一時保管場所(/tmpなど)」に、暗号のような名前で仮置きされます。
この段階ではまだアプリからは使えません。

move_uploaded_file 関数を使うことで、この仮置きファイルを、私たちが用意した /images/ フォルダへ正式な名前で移動させています。
この時、移動先のフォルダに「書き込み権限(第5回参照)」がないと失敗するため、事前の chmod 755 が重要になるわけです。

3. SQLインジェクション対策(プリペアドステートメント)

リナックス先生

ここが一番重要よ!
ユーザーからの入力データをSQL文に直接埋め込むのは絶対にNG。
例えば名前に ' OR '1'='1 なんて入力されたら、データベースの中身を全部消されたり盗まれたりするの。
これを防ぐために、:name のような「プレースホルダー(仮置き場)」を使って、後から安全に値を当てはめる「プリペアドステートメント」を使うのが鉄則よ。

// NGな例(絶対にダメ!)
$sql = "INSERT INTO posts (name) VALUES ('" . $name . "')";

// OKな例(プリペアドステートメント)
$sql = "INSERT INTO posts (name) VALUES (:name)";
$stmt->bindValue(':name', $name, PDO::PARAM_STR);

動作確認:実際に投稿してみよう!

コードの保存ができたら、ブラウザで http://[IPアドレス]/bbs/ にアクセスし、フォームに入力して「投稿する」ボタンを押してみてください。

緑色の枠で「投稿しました!」と表示されれば成功です!
(※画面下の投稿一覧にはまだ反映されません。それは次回の「読み込み機能」で作ります)

本当に保存されたか確認する方法

画面には出ませんが、裏側ではデータベースに保存されているはずです。
SSHの黒い画面で確認してみましょう。

mysql -u app_user -p -D app_db -e "SELECT * FROM posts;"

以下のように、入力したデータが表示されれば完璧です!

+----+-----------+------------------+------------------------------+---------------------+
| id | name      | comment          | image_path                   | created_at          |
+----+-----------+------------------+------------------------------+---------------------+
|  1 | コウ君    | 初めての投稿!   | 20260105_65a1b...jpg         | 2026-01-05 12:34:56 |
+----+-----------+------------------+------------------------------+---------------------+
1 row in set (0.00 sec)

トラブルシューティング(うまくいかない時)

症状 考えられる原因
「画像のアップロードに失敗しました」と出る /var/www/html/bbs/images/ ディレクトリの権限を確認してください。chown apache:apachechmod 755 が必要です。
画像を選択しても送信されない <form> タグに enctype="multipart/form-data" が入っているか確認してください。これがないと画像データは送信されません。
DBエラーになる 第3回のDBユーザー作成、または今回のテーブル作成がうまくいっていない可能性があります。ユーザー名・パスワード・DB名のスペルミスもよくあります。

次回予告:保存したデータを画面に表示する

おめでとうございます!これであなたのアプリは「ユーザーの入力を記憶する」能力を手に入れました。
しかし、記憶しただけでは意味がありません。それを画面に表示して(Read)、みんなに見てもらえるようにする必要があります。

次回は、データベースからデータを取り出し、HTMLの中にループ処理で埋め込んで「タイムライン(投稿一覧)」を完成させます。
画像もしっかり表示させますよ!

リナックス先生

今回のコードは少し長かったけど、これが書ければWebエンジニアとしての第一関門突破よ。
エラーが出ても焦らず、エラーメッセージを読んで原因を突き止める練習をしてね。
次回でいよいよ、掲示板として「使える」状態になるわよ!

▼この講座で使用している推奨VPS

【2026年最新】Linuxサーバー構築におすすめのVPS比較3選!現役エンジニアが速度とコスパで厳選
Linuxの勉強、まだ「自分のPC」でやって消耗していませんか?「Linuxを覚えたいけど、環境構築でエラーが出て先に進めない…」「VirtualBoxを入れたらパソコンが重くなった…」これは、Linux学習を始める9割の人がぶつかる壁です...

コメント