PHPとデータベース掲示板に会員登録をつけて更新できるようにしよう

フォルダとファイル名

php-db12 フォルダを作成します。ファイル構成は以下になります。

php-db12/
├── common.php
├── register.php
├── register_done.php
├── login.php
├── auth.php
├── member.php
├── logout.php
├── bbs.php          # 一覧と投稿フォーム(ログインなしでもOK)
├── update.php       # 更新フォーム & 更新処理(ログイン必須)
├── delete.php       # 削除処理(ログイン必須)
└── members.sql

フローチャート

register.php(会員登録フォーム:ユーザー名・パスワード入力)
 ↓
register_done.php(データベースに保存)
 ↓
login.php(ログインフォーム)
 ↓
auth.php(ログイン認証:データベースと照合)
 →  成功 → member.php(会員専用ページ)
     ↓
     bbs.php(掲示板:ログインしていなくても閲覧・投稿可能)
      ↓
      (ログイン中のみ)
      ├─ update.php(投稿内容を編集 → bbs.phpに戻る)
      └─ delete.php(投稿を削除 → bbs.phpに戻る)
 →  失敗 → login.php に戻る
 ↓
logout.php(セッション情報の破棄)
 → bbs.php に戻る

完成イメージ

データベース:更新のためbbsテーブルにカラム追加

ALTER TABLE bbs
ADD COLUMN user_id INT NULL AFTER comment;

1.common.php(共通処理)

<?php
session_start();

// DB接続
$dsn = 'mysql:host=localhost;dbname=testdb;charset=utf8mb4';
$user = 'root';
$password = '';

try {
    $pdo = new PDO($dsn, $user, $password);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    echo "DB接続エラー:" . $e->getMessage();
    exit;
}

// エスケープ関数
function h($str)
{
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}

2.register.php(会員登録フォーム)

新規会員登録のフォーム ユーザー名とパスワードを入力

<?php require 'common.php'; ?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>新規会員登録</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
  <div class="container mt-5">
      <div class="col-md-6 mx-auto">
        <div class="card shadow-sm">
          <div class="card-body">
            <h1 class="h4 mb-4 text-center">新規会員登録</h1>

            <form action="register_done.php" method="post">
              <div class="mb-3">
                <label for="username" class="form-label">ユーザー名</label>
                <input type="text" name="username" id="username" class="form-control" required>
              </div>

              <div class="mb-3">
                <label for="password" class="form-label">パスワード</label>
                <input type="password" name="password" id="password" class="form-control" required>
              </div>

              <button type="submit" class="btn btn-primary w-100">登録</button>
            </form>

            <p class="mt-3 text-center">
              <a href="login.php">既にアカウントをお持ちの方はこちら</a>
            </p>
          </div>
        </div>
      </div>
    </div>

</body>
</html>

3.register_done.php

<?php
require 'common.php';

$username = trim($_POST['username'] ?? '');
$password = trim($_POST['password'] ?? '');

// バリデーション
if ($username === '' || $password === '') {
    exit('ユーザー名とパスワードは必須です。');
}

// パスワードをハッシュ化
$hashed = password_hash($password, PASSWORD_DEFAULT);

// 登録
try {
    $stmt = $pdo->prepare("INSERT INTO members (username, password) VALUES (?, ?)");
    $stmt->execute([$username, $hashed]);
    echo "登録完了!<a href='login.php'>ログインする</a>";
} catch (PDOException $e) {
    if ($e->getCode() === '23000') { // UNIQUE制約エラー
        echo "このユーザー名は既に使われています。<a href='register.php'>戻る</a>";
    } else {
        echo "エラー:" . $e->getMessage();
    }
}

4.login.php (ログイン)

<?php require 'common.php'; ?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ログイン</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
  <div class="container mt-5">
      <div class="col-md-6 mx-auto">
        <div class="card shadow-sm">
          <div class="card-body">
            <h1 class="h4 mb-4 text-center">ログイン</h1>

            <form action="auth.php" method="post">
              <div class="mb-3">
                <label for="username" class="form-label">ユーザー名</label>
                <input type="text" name="username" id="username" class="form-control" required autofocus>
              </div>

              <div class="mb-3">
                <label for="password" class="form-label">パスワード</label>
                <input type="password" name="password" id="password" class="form-control" required>
              </div>

              <button type="submit" class="btn btn-primary w-100">ログイン</button>
            </form>

            <p class="mt-3 text-center">
              <a href="register.php">新規会員登録はこちら</a>
            </p>
            <p class="mt-3 text-center">
              <a href="bbs.php">掲示板を見る(ゲスト)</a>
            </p>
          </div>
        </div>
      </div>
    </div>
</body>
</html>

5.auth.php(認証処理)

<?php
require 'common.php';

$username = trim($_POST['username'] ?? '');
$password = trim($_POST['password'] ?? '');

// ユーザー検索
$stmt = $pdo->prepare("SELECT * FROM members WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);

// 認証判定
if ($user && password_verify($password, $user['password'])) {
    // ログイン成功 → セッションに保存
    $_SESSION['member'] = $user['username']; // 表示用
    $_SESSION['member_id'] = $user['id'];    // ★ これを追加!

    header('Location: member.php');
    exit;
} else {
    // ログイン失敗 → シンプルなメッセージ
    echo "ユーザー名またはパスワードが違います。<a href='login.php'>戻る</a>";
}
?>
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ログイン失敗</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>

<body class="bg-light">
    <div class="container mt-5">
        <div class="row justify-content-center">
            <div class="col-md-6">
                <div class="alert alert-danger text-center" role="alert">
                    ユーザー名またはパスワードが違います。
                </div>
                <div class="text-center">
                    <a href="login.php" class="btn btn-outline-primary">ログイン画面に戻る</a>
                </div>
            </div>
        </div>
    </div>
</body>

</html>

6.member.php(会員専用ページ)

<?php
require 'common.php';
if (empty($_SESSION['member'])) {
    header('Location: login.php');
    exit;
}
?>
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>会員専用ページ</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>

<body class="bg-light">
    <div class="container mt-5">
        <div class="row justify-content-center">
            <div class="col-md-6">
                <div class="card shadow-sm">
                    <div class="card-body text-center">
                        <h1 class="h4 mb-4">ようこそ <span class="text-primary"><?= h($_SESSION['member']) ?></span> さん</h1>
                        <p class="mb-4">ログインしましたので掲示板で削除・更新が可能です。</p>

                        <p class="mb-4">
                            <a href="bbs.php" class="btn btn-outline-success w-100">掲示板に移動する</a>
                        </p>
                        <p>
                            <a href="logout.php" class="btn btn-outline-danger w-100">ログアウト</a>
                        </p>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>

</html>

7.logout.php(ログアウト処理)

<?php
require 'common.php';

// セッションの中身を空にする
$_SESSION = [];

// セッションを完全に破棄
if (session_id() !== '' || isset($_COOKIE[session_name()])) {
    setcookie(session_name(), '', time() - 42000, '/');
}
session_destroy();

// ログインページへリダイレクト
header('Location: login.php');
exit;

7.bbs.php(掲示板本体)

<?php
require 'common.php';

$errors = [];
$maxComment = 100;
$name = '';
$comment = '';

try {
    // 投稿処理
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        $name = trim($_POST['name'] ?? '');
        $comment = trim($_POST['comment'] ?? '');

        // ログイン中なら名前はセッションから強制
        if (!empty($_SESSION['member'])) {
            $name = $_SESSION['member'];
        }

        // バリデーション
        if ($name === '') {
            $errors[] = '名前を入力してください。';
        }
        if ($comment === '') {
            $errors[] = 'コメントを入力してください。';
        } elseif (mb_strlen($comment) > $maxComment) {
            $errors[] = "コメントは {$maxComment} 文字以内で入力してください。";
        }

        if (count($errors) === 0) {
            $stmt = $pdo->prepare("INSERT INTO bbs (name, comment, user_id) VALUES (?, ?, ?)");
            $stmt->execute([$name, $comment, $_SESSION['member_id'] ?? null]);

            $_SESSION['flash_msg'] = '投稿しました!';
            header('Location: bbs.php');
            exit;
        }
    }

    // 投稿一覧取得
    // 投稿一覧取得(JOIN)
    $stmt = $pdo->query("
    SELECT bbs.*, members.profile_image
    FROM bbs
    LEFT JOIN members ON bbs.user_id = members.id
    ORDER BY bbs.id DESC
");
    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
    echo "DBエラー:" . $e->getMessage();
    exit;
}
?>
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <title>Mini掲示板</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>

<body class="bg-light">
    <div class="col-md-8 mx-auto mt-4">
        <header class="text-center">
            <h1 class="mb-4">Mini掲示板</h1>
            <?php if (!empty($_SESSION['member'])): ?>
                <p class="h5 mb-4">
                    <span class="text-primary"><?= h($_SESSION['member']) ?></span> さんはログインしています。

                    <a href="member.php" class="btn btn-primary">会員専用ページに移動</a>
                </p>
            <?php else: ?>
                <p class="h6 text-muted mb-4">
                    ログインしていません(投稿はできますが、削除・更新はできません)
                    <a href="login.php">ログインする</a>
                </p>
            <?php endif; ?>
        </header>

        <!-- フラッシュメッセージ -->
        <section class="col-md-8 mx-auto mt-4">
            <?php if (!empty($_SESSION['flash_msg'])): ?>
                <div class="alert alert-success">
                    <?= h($_SESSION['flash_msg']) ?>
                </div>
                <?php unset($_SESSION['flash_msg']); ?>
            <?php endif; ?>

            <!-- エラーメッセージ -->
            <?php if (count($errors) > 0): ?>
                <div class="alert alert-danger">
                    <ul class="mb-0">
                        <?php foreach ($errors as $e): ?>
                            <li><?= h($e) ?></li>
                        <?php endforeach; ?>
                    </ul>
                </div>
            <?php endif; ?>
        </section>

        <!-- 投稿フォーム -->
        <section class="card p-3 mb-5 col-md-8 mx-auto">
            <h2 class="h5">新規投稿</h2>
            <form method="post">
                <div class="mb-3">
                    <label for="name" class="form-label">名前</label>
                    <input type="text" name="name" id="name" class="form-control"
                        value="<?= !empty($_SESSION['member']) ? h($_SESSION['member']) : h($name) ?>"
                        <?= !empty($_SESSION['member']) ? 'readonly' : '' ?>>
                </div>
                <div class="mb-3">
                    <label for="comment" class="form-label">コメント(最大<?= $maxComment ?>文字)</label>
                    <textarea name="comment" id="comment" class="form-control" maxlength="<?= $maxComment ?>"><?= h($comment) ?></textarea>
                </div>
                <button type="submit" class="btn btn-primary"><i class="bi bi-pencil"></i>&nbsp;投稿する</button>
            </form>
        </section>

        <!-- 投稿一覧 -->
        <section class="col-md-8 mx-auto">
            <h2 class="h5">投稿一覧</h2>
            <?php if (empty($rows)): ?>
                <p>まだ投稿はありません。</p>
            <?php else: ?>
                <?php foreach ($rows as $row): ?>
                    <div class="card mb-3">
                        <div class="card-body row">
                            <div class="col-lg-11">
                                <h5 class="card-title d-flex align-items-center">
                                    <?php if (!empty($row['profile_image'])): ?>
                                        <img src="uploads/<?= h($row['profile_image']) ?>" height="40">
                                    <?php else: ?>
                                        <i class="bi bi-person-circle h3"></i>
                                    <?php endif; ?>
                                    <span class="h3">
                                        &nbsp;<?= h($row['name']) ?>
                                    </span>
                                </h5>
                                <h6 class="card-subtitle mb-2 text-muted"><?= h($row['created_at']) ?></h6>
                                <p class="card-text"><?= nl2br(h($row['comment'])) ?></p>
                            </div>
                            <div class="col-lg-1 text-end">
                                <?php if (!empty($_SESSION['member_id']) && $row['user_id'] == $_SESSION['member_id']): ?>
                                    <!-- 自分の投稿だけ編集・削除ボタン表示 -->
                                    <form action="delete.php" method="post" class="mb-1">
                                        <input type="hidden" name="id" value="<?= h($row['id']) ?>">
                                        <button type="submit" class="btn btn-outline-danger btn-sm"
                                            onclick="return confirm('本当に削除しますか?');">
                                            <i class="bi bi-trash h3"></i>
                                        </button>
                                    </form>
                                    <form action=" update.php" method="post">
                                        <input type="hidden" name="update_id" value="<?= h($row['id']) ?>">
                                        <button type="submit" class="btn btn-sm btn-outline-primary">
                                            <i class="bi bi-pencil-fill h3"></i>
                                        </button>
                                    </form>
                                <?php endif; ?>
                            </div>
                        </div>
                    </div>
                <?php endforeach; ?>
            <?php endif; ?>
            <p class="mt-4"><a href="logout.php">ログアウトする</a></p>
        </section>


    </div>
</body>

</html>

7.delete.php

<?php
require 'common.php';

// ログインしていない場合はログインページへ
if (empty($_SESSION['member_id'])) {
    header('Location: login.php');
    exit;
}

// POST送信以外はbbs.phpに戻す
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    header('Location: bbs.php');
    exit;
}

$id = (int)($_POST['id'] ?? 0);
if ($id > 0) {
    // 自分の投稿かどうかを確認して削除
    $stmt = $pdo->prepare("DELETE FROM bbs WHERE id = ? AND user_id = ?");
    $stmt->execute([$id, $_SESSION['member_id']]);

    if ($stmt->rowCount() > 0) {
        $_SESSION['flash_msg'] = "削除しました。";
    } else {
        $_SESSION['flash_msg'] = "削除できません(権限がありません)。";
    }
}

header('Location: bbs.php');
exit;

7.update.php

<?php
require 'common.php';

if (empty($_SESSION['member_id'])) {
    $_SESSION['flash_msg'] = "ログインしてください。";
    header('Location: login.php');
    exit;
}

// ----------------------
// id の受け取り(POST専用)
// ----------------------
$id = isset($_POST['update_id']) ? (int)$_POST['update_id'] : 0;
if ($id <= 0) {
    header('Location: bbs.php');
    exit;
}

// ----------------------
// 投稿取得(自分の投稿のみ)
// ----------------------
$stmt = $pdo->prepare("SELECT * FROM bbs WHERE id = ? AND user_id = ?");
$stmt->execute([$id, $_SESSION['member_id']]);
$post = $stmt->fetch(PDO::FETCH_ASSOC);

if (!$post) {
    $_SESSION['flash_msg'] = "権限がありません。";
    header('Location: bbs.php');
    exit;
}

// ----------------------
// 更新処理
// ----------------------
if (isset($_POST['comment'])) { // 更新ボタンが押されたとき
    $comment = trim($_POST['comment']);
    if ($comment === '') {
        $error = 'コメントを入力してください。';
    } else {
        $stmt = $pdo->prepare("UPDATE bbs SET comment = ? WHERE id = ? AND user_id = ?");
        $stmt->execute([$comment, $id, $_SESSION['member_id']]);
        $_SESSION['flash_msg'] = '更新しました!';
        header('Location: bbs.php');
        exit;
    }
}
?>
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <title>投稿を編集</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>

<body class="bg-light">
    <div class="col-md-8 mx-auto mt-4">
        <h1 class="mb-4">投稿を編集</h1>

        <?php if (!empty($error)): ?>
            <div class="alert alert-danger"><?= h($error) ?></div>
        <?php endif; ?>

        <form method="post">
            <!-- 編集対象IDを再送信する -->
            <input type="hidden" name="update_id" value="<?= h($id) ?>">
            <div class="mb-3">
                <label for="comment" class="form-label">コメント</label>
                <textarea name="comment" id="comment" class="form-control"><?= h($post['comment']) ?></textarea>
            </div>
            <button type="submit" class="btn btn-primary">更新する</button>
            <a href="bbs.php" class="btn btn-secondary">戻る</a>
        </form>
    </div>
</body>

</html>

課題:検索フォームを追加しよう

<form method="get">
  <input type="text" name="keyword" placeholder="キーワードを入力">
  <button type="submit">検索</button>
</form>
  • 検索はget送信
  • GETメソッドを使うとURLに?keyword=◯◯が付く
  • PHPで $_GET['keyword'] からキーワードを取得できる
if (!empty($_GET['keyword'])) {
    $keyword = '%' . $_GET['keyword'] . '%';
    $stmt = $pdo->prepare("SELECT * FROM bbs WHERE name LIKE ? OR comment LIKE ? ORDER BY id DESC");
    $stmt->execute([$keyword, $keyword]);
} else {
    $stmt = $pdo->query("SELECT * FROM bbs ORDER BY id DESC");
}
  • LIKE で部分一致検索
  • namecomment 両方を対象に検索
  • キーワードが空なら全件表示

結果がなかったときの表示

<?php if (empty($rows)): ?>
    <p>該当する投稿はありません。</p>
<?php endif; ?>