ジュニアエンジニア卒業認定課題へようこそ。
この課題には、Webアプリケーション開発に必要な「サーバー・DB・セキュリティ」のエッセンスがすべて詰まっています。
これが自力で作れれば、あなたはもう「指示されたコードを書く人」ではなく、「仕様からシステムを設計できる人(中級者)」の入り口に立っています。
🏆 課題:会員制マイクロブログ(Mini-Twitter)
ユーザー登録をして、つぶやきを投稿し、他人の投稿も見ることができるSNSを作ります。
📋 要件定義
- データベース設計(リレーション):
usersテーブル:ユーザー情報(ID, 名前, パスワード)。postsテーブル:投稿内容。「誰が書いたか」を記録するuser_idカラムを持つ。
- 認証機能:
- 新規会員登録(パスワードはハッシュ化)。
- ログイン / ログアウト機能。
- 投稿・表示機能:
- ログインしている人だけが投稿できる。
- タイムラインには「全員の投稿」が表示される。
- 投稿には「投稿者の名前」も併せて表示する(内部結合 JOIN を使用)。
- 認可(権限管理):
- 投稿の横に「削除」ボタンを表示する。
- ただし、削除ボタンは「自分の投稿」にしか表示されない。
- 不正に削除リクエストを送られても、他人の投稿は消せないようにガードする。
💡 ヒント
- テーブルを結合してデータを取得するには
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などのフレームワークや、オリジナルのアプリ開発に進んでください!応援しています。