【連載第10回】投稿を修正・削除したい!PHPで実装する「Update/Delete」とIDの重要性

LAMP講座

【連載第10回】CRUD完結!「消す」機能を作る責任と恐怖

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

これまでの連載で、私たちは以下の機能を実装してきました。

  • Create(作成): フォームからデータをデータベースに保存する。
  • Read(読み取り): 保存されたデータを一覧表示する。

これだけでも「掲示板」としては機能しますが、使っていると必ずこんな不満が出てきます。
「誤字脱字をしちゃったから直したい!」「変な写真をアップしちゃったから消したい!」

今回は、Webアプリの基本機能であるCRUD(クラッド)の残り2つ、「Update(更新)」「Delete(削除)」を実装し、アプリを完成形へと導きます。

ただし、先に言っておきます。「データを消す処理」は、Web開発の中で最も慎重に行わなければならない作業です。
プログラムを1行書き間違えるだけで、「ユーザー全員のデータが消えてしまう」という大事故に繋がるからです。
今日は、そんなエンジニアの責任感についても学んでいきましょう。

コウ君

先生!実は僕、さっきテスト投稿で「こんにちは」を「こんちには」って打ち間違えちゃって…。
恥ずかしいから早く直したいです!
でも、全員のデータが消えるってどういうことですか?怖すぎます!

リナックス先生

ふふ、それがSQLの怖いところよ。
「削除しろ」とだけ命令したら、データベースは素直に「全部」削除しちゃうの。
「どの投稿を」削除するのかを指定する、ID(主キー)の概念をしっかり理解することが、今日の最大のテーマよ。

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

現在地は第10回です。ついにアプリ開発編の完結です!

  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化

なぜ「ID」が必要なのか?

編集や削除を行う前に、理論を整理しましょう。
第8回でデータベースのテーブルを作った時、id というカラムを作成し、PRIMARY KEY(主キー) を設定したのを覚えていますか?

このIDは、データが作られるたびに自動的に「1, 2, 3…」と連番が振られます。
これが「世界に一つだけの背番号」として機能します。

名前で削除してはいけない理由

もし、IDを使わずに「名前が『コウ君』の投稿を削除して」と命令したらどうなるでしょう?
もしコウ君が過去に100件投稿していたら、その100件がすべて消えてしまいます。
また、別の「コウ君(同名ユーザー)」がいた場合、その人の投稿まで巻き添えにしてしまいます。

だからこそ、「IDが5番の投稿を削除して」というように、絶対に被らない番号で指定する必要があるのです。

実装ステップ1:トップページに削除ボタンを設置する

まずは、index.php を修正して、各投稿に「削除」ボタンを追加しましょう。
削除ボタンは、押した瞬間に「どのIDを削除するか」という情報を送信する必要があります。

SSHで vi /var/www/html/bbs/index.php を開き、<article class="post"> の中(例えば <p>コメント</p> の下あたり)に以下のコードを追加します。

<!-- 編集・削除エリア -->
<div class="post-actions" style="margin-top: 10px; text-align: right;">
    <!-- 編集ボタン(GETリクエスト) -->
    <a href="edit.php?id=<?php echo $post['id']; ?>" style="color: blue; margin-right: 10px; text-decoration: none;">編集</a>
    
    <!-- 削除ボタン(POSTリクエスト) -->
    <form action="delete.php" method="post" style="display: inline;" onsubmit="return confirm('本当に削除しますか?');">
        <input type="hidden" name="id" value="<?php echo $post['id']; ?>">
        <button type="submit" style="color: red; border: none; background: none; cursor: pointer; text-decoration: underline;">削除</button>
    </form>
</div>

ここがポイント!

  • 編集はリンク(GET)でOK: 編集画面に移動するだけなので、edit.php?id=5 のようにURLでIDを渡します。
  • 削除はフォーム(POST)にする: これが重要です。もし削除をリンク(GET)にしてしまうと、Googleの検索ロボットが巡回してきただけで投稿が削除されてしまったり、攻撃者が作った「削除リンク」を踏んだだけでデータが消えたりします。データを変更・削除する操作は必ずPOSTメソッドを使うのが鉄則です。
  • onsubmit=”return confirm(…)”: 削除ボタンを押した時に、「本当に削除しますか?」というポップアップを出すJavaScriptです。これがあるだけで誤操作を激減できます。

実装ステップ2:削除機能(delete.php)を作る

次に、削除処理を行う専用のファイル delete.php を作成します。
このファイルは画面を持たず、裏でデータベースを操作して、すぐにトップページへリダイレクト(転送)します。

vi /var/www/html/bbs/delete.php
<?php
// DB接続情報
$dsn = 'mysql:dbname=app_db;host=localhost;charset=utf8mb4';
$user = 'app_user';
$password = 'password123';

// POSTリクエスト以外は拒否(直接URLを叩いても動かないようにする)
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    header('Location: index.php');
    exit;
}

// IDの取得
$id = $_POST['id'] ?? null;

if ($id) {
    try {
        $pdo = new PDO($dsn, $user, $password);
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

        // 1. 画像ファイルを削除するために、まずは情報を取得
        $stmt = $pdo->prepare("SELECT image_path FROM posts WHERE id = :id");
        $stmt->bindValue(':id', $id, PDO::PARAM_INT);
        $stmt->execute();
        $post = $stmt->fetch(PDO::FETCH_ASSOC);

        // 画像があればサーバーからファイルを削除(ゴミ掃除)
        if (!empty($post['image_path'])) {
            $image_file = '/var/www/html/bbs/images/' . $post['image_path'];
            if (file_exists($image_file)) {
                unlink($image_file); // ファイル削除実行
            }
        }

        // 2. データベースから行を削除
        $stmt = $pdo->prepare("DELETE FROM posts WHERE id = :id");
        $stmt->bindValue(':id', $id, PDO::PARAM_INT);
        $stmt->execute();

    } catch (PDOException $e) {
        // エラー時は何もしないかログに残す
        // echo $e->getMessage();
    }
}

// 処理が終わったらトップページへ戻る
header('Location: index.php');
exit;
?>

プロのこだわりポイント:unlink(アンリンク)

データベースから文字情報を消すだけでは不十分です。
アップロードされた「画像ファイル」はサーバーのフォルダに残ったままになり、これが積もり積もるとサーバーの容量を圧迫してしまいます。

そこで、DELETE する前に一度 SELECT して画像ファイル名を特定し、PHPの unlink() 関数を使って画像ファイルそのものも物理的に削除しています。
「来た時よりも美しく」。これがサーバーエンジニアの作法です。

実装ステップ3:編集機能(edit.php)を作る

最後に、編集機能です。
これは「現在のデータを表示する(Read)」と「新しいデータで上書きする(Update)」を同時に行う必要があるため、少し複雑になります。

vi /var/www/html/bbs/edit.php
<?php
// DB接続情報
$dsn = 'mysql:dbname=app_db;host=localhost;charset=utf8mb4';
$user = 'app_user';
$password = 'password123';

$message = null;
$error_message = null;

// ID取得(GETパラメータから)
$id = $_GET['id'] ?? null;

if (!$id) {
    header('Location: index.php');
    exit;
}

try {
    $pdo = new PDO($dsn, $user, $password);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // ▼ 更新処理(POST時)
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        $name = $_POST['name'] ?? '';
        $comment = $_POST['comment'] ?? '';

        if (empty($name) || empty($comment)) {
            $error_message = '名前とコメントは必須です。';
        } else {
            // 画像の更新があるかチェック
            $sql = "UPDATE posts SET name = :name, comment = :comment WHERE id = :id";
            
            // もし新しい画像がアップロードされていたら、画像パスも更新する
            // (実装を簡単にするため、今回は画像更新は省略し、テキストのみ更新とします)
            // ※本格的なアプリでは、ここで古い画像をunlinkし、新しい画像をuploadする処理が入ります。

            $stmt = $pdo->prepare($sql);
            $stmt->bindValue(':name', $name, PDO::PARAM_STR);
            $stmt->bindValue(':comment', $comment, PDO::PARAM_STR);
            $stmt->bindValue(':id', $id, PDO::PARAM_INT);
            $stmt->execute();

            $message = '更新しました!';
        }
    }

    // ▼ 現在のデータを取得(フォームに表示するため)
    $stmt = $pdo->prepare("SELECT * FROM posts WHERE id = :id");
    $stmt->bindValue(':id', $id, PDO::PARAM_INT);
    $stmt->execute();
    $post = $stmt->fetch(PDO::FETCH_ASSOC);

    if (!$post) {
        header('Location: index.php'); // データが存在しない場合
        exit;
    }

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

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>投稿の編集</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <header>
        <h1>投稿の編集</h1>
    </header>

    <main>
        <?php if ($message): ?>
            <div class="alert success"><?php echo htmlspecialchars($message); ?></div>
        <?php endif; ?>
        <?php if ($error_message): ?>
            <div class="alert error"><?php echo htmlspecialchars($error_message); ?></div>
        <?php endif; ?>

        <div class="form-container">
            <!-- 編集フォーム(初期値 value に現在のデータを入れる) -->
            <form action="edit.php?id=<?php echo $post['id']; ?>" method="post">
                <div class="input-group">
                    <label>ニックネーム</label>
                    <input type="text" name="name" required 
                           value="<?php echo htmlspecialchars($post['name'], ENT_QUOTES, 'UTF-8'); ?>">
                </div>
                <div class="input-group">
                    <label>コメント</label>
                    <textarea name="comment" required><?php echo htmlspecialchars($post['comment'], ENT_QUOTES, 'UTF-8'); ?></textarea>
                </div>
                
                <button type="submit" class="submit-btn">更新する</button>
            </form>
        </div>
        
        <div style="text-align: center; margin-top: 20px;">
            <a href="index.php">一覧に戻る</a>
        </div>
    </main>
</body>
</html>

value属性の活用

編集画面のポイントは、<input value="..."> を使って、テキストボックスにあらかじめ現在の値を入れておくことです。
これがないと、ユーザーはまた最初から文章を書き直さなければならず、非常に使い勝手が悪くなります。

また、textarea タグには value 属性がないため、タグの間(<textarea>ここ</textarea>)に値を入れます。
ここでも必ず htmlspecialchars を忘れないでください!XSSの危険性は編集画面でも同じです。

重要な概念:WHERE句を忘れる恐怖

コウ君

先生、編集も削除もIDを使うんですね。
もしSQLで WHERE id = :id を書き忘れたらどうなるんですか?

リナックス先生

いい質問ね。背筋が凍る話をしてあげる。
もし DELETE FROM posts とだけ書いて実行すると、データベース内の全投稿が一瞬ですべて消えるわ。
UPDATE posts SET name = 'バカ' と書けば、全ユーザーの名前が「バカ」に変わるわ。
取り返しがつかないから、SQLを書くときは「まずWHEREを書く」くらいの癖をつけなさい。

おまけ:物理削除と論理削除

今回実装したのは、データベースから本当に行を消してしまう「物理削除」です。
しかし、実際のSNSや業務システムでは、データを本当には消さない「論理削除」という手法がよく使われます。

種類 動作 メリット デメリット
物理削除
(今回)
DELETE 文でデータを抹消する。 データ容量が空く。
個人情報などを完全に消せる。
誤って消した場合、復旧が不可能。
過去の履歴が残らない。
論理削除
(実務)
UPDATE 文で「削除フラグ」をONにするだけ。
表示する時に「フラグOFFのものだけ」を表示する。
「間違えて消した!」と言われてもすぐに元に戻せる。
分析などに使える。
データが溜まり続ける。
プログラムが少し複雑になる(常にフラグを確認する必要がある)。

初心者のうちは物理削除で十分ですが、「プロは簡単にはデータを消さない」ということも頭の片隅に置いておいてください。

次回予告:セキュリティの総仕上げ

これで、Webアプリに必要な「CRUD機能」はすべて揃いました!
掲示板としてはもう立派に機能しています。

しかし、今の状態には一つ大きな欠点があります。
それは「誰でも他人の投稿を編集・削除できてしまう」ということです。
本来は「ログイン機能」を作って本人確認をする必要がありますが、今回はLAMP構築講座なのでそこまでは踏み込みません。

その代わり、次回はWebアプリを守るための「セキュリティ対策の総まとめ」を行います。
SQLインジェクション、XSS、そしてまだ触れていないCSRFなどの攻撃手法とその防御策を学び、安全なサイト運営の知識を身につけましょう。

リナックス先生

アプリが完成して浮かれている時が、一番セキュリティホール(穴)を作りやすいの。
次回でこのアプリを「要塞」にするわよ。
まだサーバーを持っていない人は、今のうちにVPSを契約して、これまでのコードを写経しておいてね!

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

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

コメント