課題(脱初心者)

ジュニアエンジニア卒業認定課題へようこそ。
この課題には、Webアプリケーション開発に必要な「サーバー・DB・セキュリティ」のエッセンスがすべて詰まっています。

これが自力で作れれば、あなたはもう「指示されたコードを書く人」ではなく、「仕様からシステムを設計できる人(中級者)」の入り口に立っています。


🏆 課題:会員制マイクロブログ(Mini-Twitter)

ユーザー登録をして、つぶやきを投稿し、他人の投稿も見ることができるSNSを作ります。

📋 要件定義

  1. データベース設計(リレーション):
    • users テーブル:ユーザー情報(ID, 名前, パスワード)。
    • posts テーブル:投稿内容。「誰が書いたか」を記録する user_id カラムを持つ。
  2. 認証機能:
    • 新規会員登録(パスワードはハッシュ化)。
    • ログイン / ログアウト機能。
  3. 投稿・表示機能:
    • ログインしている人だけが投稿できる。
    • タイムラインには「全員の投稿」が表示される。
    • 投稿には「投稿者の名前」も併せて表示する(内部結合 JOIN を使用)。
  4. 認可(権限管理):
    • 投稿の横に「削除」ボタンを表示する。
    • ただし、削除ボタンは「自分の投稿」にしか表示されない。
    • 不正に削除リクエストを送られても、他人の投稿は消せないようにガードする。

💡 ヒント

  • テーブルを結合してデータを取得するには SELECT ... FROM posts JOIN users ON posts.user_id = users.id を使います。
  • 削除のSQLは DELETE FROM posts WHERE id = :post_id AND user_id = :my_id のように、IDだけでなく「自分のIDか?」も条件に加えるのが鉄則です。
▶︎ 解答コードを見る(クリックで展開)

※このコードは1つのファイル(index.php)ですべて完結するように書かれていますが、実務ではファイルを分けるのが一般的です。

/* --- ステップ1:データベース作成SQL(phpMyAdminで実行) --- */
/*
CREATE DATABASE micro_blog DEFAULT CHARACTER SET utf8mb4;
USE micro_blog;

-- ユーザーテーブル
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL
);

-- 投稿テーブル
CREATE TABLE posts (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    content VARCHAR(140) NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id)
);
*/


/* --- ステップ2:index.php --- */

<?php
session_start();

// DB接続
try {
    $pdo = new PDO('mysql:dbname=micro_blog;host=localhost;charset=utf8mb4', 'root', '');
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    exit('DB Error');
}

// XSS対策関数
function h($str) {
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}

// === 処理の分岐 ===

// 1. ログアウト処理
if (isset($_GET['action']) && $_GET['action'] === 'logout') {
    session_destroy();
    header("Location: index.php");
    exit;
}

// 2. 新規登録処理
if (isset($_POST['register'])) {
    $name = $_POST['name'];
    $email = $_POST['email'];
    $pass = password_hash($_POST['password'], PASSWORD_DEFAULT); // ハッシュ化

    try {
        $stmt = $pdo->prepare("INSERT INTO users (name, email, password) VALUES (?, ?, ?)");
        $stmt->execute([$name, $email, $pass]);
        $_SESSION['user_id'] = $pdo->lastInsertId(); // 自動ログイン
        header("Location: index.php");
        exit;
    } catch (Exception $e) {
        $error = "登録に失敗しました(メールアドレスが重複しています)";
    }
}

// 3. ログイン処理
if (isset($_POST['login'])) {
    $email = $_POST['email'];
    $stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?");
    $stmt->execute([$email]);
    $user = $stmt->fetch();

    if ($user && password_verify($_POST['password'], $user['password'])) {
        $_SESSION['user_id'] = $user['id']; // ログイン成功
        header("Location: index.php");
        exit;
    } else {
        $error = "メールまたはパスワードが違います";
    }
}

// 4. 投稿処理(ログイン時のみ)
if (isset($_POST['tweet']) && isset($_SESSION['user_id'])) {
    if (!empty($_POST['content'])) {
        $stmt = $pdo->prepare("INSERT INTO posts (user_id, content) VALUES (?, ?)");
        $stmt->execute([$_SESSION['user_id'], $_POST['content']]);
        header("Location: index.php"); // 二重送信防止リダイレクト
        exit;
    }
}

// 5. 削除処理(超重要:自分の投稿しか消せないようにする)
if (isset($_POST['delete_id']) && isset($_SESSION['user_id'])) {
    // WHERE id = 投稿ID AND user_id = 自分のID
    $stmt = $pdo->prepare("DELETE FROM posts WHERE id = ? AND user_id = ?");
    $stmt->execute([$_POST['delete_id'], $_SESSION['user_id']]);
    header("Location: index.php");
    exit;
}

// === データ取得(タイムライン表示用) ===
// postsテーブルとusersテーブルを結合(JOIN)して、投稿者の名前も一緒に取る
$sql = "SELECT posts.*, users.name 
        FROM posts 
        JOIN users ON posts.user_id = users.id 
        ORDER BY posts.created_at DESC";
$posts = $pdo->query($sql)->fetchAll();

?>

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Mini Twitter</title>
    <style>
        body { font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; }
        .error { color: red; }
        .post { border-bottom: 1px solid #ddd; padding: 15px 0; }
        .meta { color: #666; font-size: 0.9em; }
        .form-box { background: #f9f9f9; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
    </style>
</head>
<body>
    <h1>🐤 Mini Twitter</h1>

    <!-- エラーがあれば表示 -->
    <?php if (isset($error)): ?>
        <p class="error"><?= h($error) ?></p>
    <?php endif; ?>

    <!-- ▼▼▼ 未ログイン時:登録・ログインフォーム ▼▼▼ -->
    <?php if (!isset($_SESSION['user_id'])): ?>
        <div class="form-box">
            <h3>ログイン</h3>
            <form method="post">
                <input type="email" name="email" placeholder="メール" required>
                <input type="password" name="password" placeholder="パスワード" required>
                <button type="submit" name="login">ログイン</button>
            </form>
            <hr>
            <h3>新規登録</h3>
            <form method="post">
                <input type="text" name="name" placeholder="名前" required>
                <input type="email" name="email" placeholder="メール" required>
                <input type="password" name="password" placeholder="パスワード" required>
                <button type="submit" name="register">登録してはじめる</button>
            </form>
        </div>

    <!-- ▼▼▼ ログイン済み:投稿フォーム ▼▼▼ -->
    <?php else: ?>
        <div style="text-align:right">
            <a href="?action=logout">ログアウト</a>
        </div>
        <div class="form-box">
            <form method="post">
                <textarea name="content" rows="3" style="width:100%" placeholder="いまどうしてる?" required></textarea>
                <button type="submit" name="tweet">ツイートする</button>
            </form>
        </div>
    <?php endif; ?>

    <!-- ▼▼▼ タイムライン(全員見れる) ▼▼▼ -->
    <h2>タイムライン</h2>
    <?php foreach ($posts as $post): ?>
        <div class="post">
            <div class="meta">
                <strong><?= h($post['name']) ?></strong> 
                <small>(<?= $post['created_at'] ?>)</small>
            </div>
            
            <p><?= h($post['content']) ?></p>

            <!-- ★自分の投稿の時だけ削除ボタンを出す -->
            <?php if (isset($_SESSION['user_id']) && $_SESSION['user_id'] == $post['user_id']): ?>
                <form method="post" onsubmit="return confirm('本当に削除しますか?');">
                    <input type="hidden" name="delete_id" value="<?= $post['id'] ?>">
                    <button type="submit" style="color:red; font-size:0.8em">削除</button>
                </form>
            <?php endif; ?>
        </div>
    <?php endforeach; ?>

</body>
</html>

👨‍🏫 挑戦するあなたへ

このコードは非常に短く圧縮されていますが、中身は「本物のWebサービスの縮図」です。

  • JOIN:ユーザーテーブルと投稿テーブルを繋げて、投稿者の名前を表示しています。
  • 認可制御:if ($post['user_id'] == $_SESSION['user_id']) というチェックで、「削除ボタン」を出し分けています。

これが理解できれば、あなたはもう初心者ではありません。
自信を持って、Laravelなどのフレームワークや、オリジナルのアプリ開発に進んでください!応援しています。

 

タイトルとURLをコピーしました