---
layout: "@/layouts/MarkdownLayout.astro"
---
import Toc from "../../components/Toc.astro";
import Details from "@/components/Details.astro";
import DockerLink from "@/components/DockerLink.astro";
export const title = "Twitterライクなアプリを作ろう";
# {title}
X(旧Twitter)のような投稿アプリを作成します。
つぶやきを投稿して、タイムラインで一覧表示できるアプリです。
## TOC
## 完成イメージ
<DockerLink href="sample/php/final-work/twitter/index.php" />
このアプリでできること:
- つぶやきを投稿できる
- 投稿は新しい順に表示される
- 他の人の投稿も見られる
## データベース設計
まず、投稿データを保存するテーブルを作成します。
### テーブル構造
**テーブル名:** `tweets`
| カラム名 | 型 | 説明 |
|---------|-----|------|
| id | INT | 投稿ID(主キー、自動採番) |
| username | VARCHAR(50) | 投稿者の名前 |
| content | VARCHAR(280) | 投稿内容(280文字まで) |
| created_at | DATETIME | 投稿日時 |
### テーブル作成SQL
phpMyAdminなどで以下のSQLを実行してください。
```sql
CREATE TABLE tweets (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
content VARCHAR(280) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
```
## ファイル構成
`/workspace/php/twitter/` に以下のファイルを用意します。
```
twitter/
├── db.php # データベース接続設定
├── index.php # メインページ
└── post.php # 投稿処理
```
---
## STEP1: 基本機能を作る【必須】
まずは、投稿と表示ができる最小限のアプリを作ります。
### db.php(データベース接続)
データベース接続の設定ファイルです。**このファイルは最初から完成しています。**
```php file=public/workspace/php/twitter/db.php
<?php
function getDb() {
static $pdo = null;
if ($pdo === null) {
define('DB_HOST', "db");
define("DB_PORT", "3306");
define('DB_USER', "user");
define('DB_PASSWORD', "password");
define('DB_NAME', "trainee_db");
$dsn = "mysql:host=" . DB_HOST . ";port=" . DB_PORT . ";dbname=" . DB_NAME . ";charset=utf8mb4";
$pdo = new PDO($dsn, DB_USER, DB_PASSWORD);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
return $pdo;
}
?>
```
他のファイルから `require_once 'db.php';` で読み込んで、`$pdo = getDb();` で使います。
### index.php(メインページ)
投稿フォームとタイムラインを表示するページです。
```php
<?php
require_once 'db.php';
// TODO 1: 投稿を新しい順に取得する
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Twitterライク</title>
</head>
<body>
<h1>つぶやき投稿</h1>
<!-- TODO 2: 投稿フォームを作成する -->
<hr>
<h2>タイムライン</h2>
<!-- TODO 3: 投稿を表示する -->
</body>
</html>
```
#### TODO 1: 投稿を新しい順に取得する
<Details summary="ヒント">
```php
$pdo = getDb();
// 新しい順(created_atの降順)に取得
$stmt = $pdo->query("SELECT * FROM tweets ORDER BY created_at DESC");
$tweets = $stmt->fetchAll(PDO::FETCH_ASSOC);
```
- `getDb()` でデータベース接続を取得
- `ORDER BY created_at DESC` で新しい順に並び替え
- `fetchAll(PDO::FETCH_ASSOC)` で全てのデータを配列として取得
</Details>
#### TODO 2: 投稿フォームを作成する
<Details summary="ヒント">
```html
<form method="post" action="post.php">
<div>
<label>ユーザー名:</label>
<input type="text" name="username" maxlength="50" required>
</div>
<div>
<label>つぶやき:</label>
<textarea name="content" maxlength="280" required></textarea>
</div>
<button type="submit">投稿する</button>
</form>
```
- `maxlength` でTwitterと同じ280文字制限
- `action="post.php"` で投稿処理ページに送信
</Details>
#### TODO 3: 投稿を表示する
<Details summary="ヒント">
```php
<?php foreach ($tweets as $tweet): ?>
<div style="border: 1px solid #ddd; padding: 10px; margin: 10px 0;">
<strong><?php echo htmlspecialchars($tweet['username']); ?></strong>
<span style="color: #999; font-size: 12px;">
<?php echo $tweet['created_at']; ?>
</span>
<p><?php echo nl2br(htmlspecialchars($tweet['content'])); ?></p>
</div>
<?php endforeach; ?>
```
- `htmlspecialchars()` でHTMLエスケープ(セキュリティ対策)
- `nl2br()` で改行を`<br>`タグに変換
</Details>
### post.php(投稿処理)
フォームから送信されたデータをデータベースに保存します。
```php
<?php
require_once 'db.php';
// TODO 1: フォームから送信されたかチェックする
// TODO 2: 送信されたデータを取得する
// TODO 3: データを挿入する
// TODO 4: index.phpにリダイレクトする
?>
```
#### TODO 1: フォームから送信されたかチェックする
<Details summary="ヒント">
```php
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
header("Location: index.php");
exit;
}
```
POSTメソッド以外でアクセスされた場合は、index.phpに戻します。
</Details>
#### TODO 2: 送信されたデータを取得する
<Details summary="ヒント">
```php
$username = $_POST["username"] ?? "";
$content = $_POST["content"] ?? "";
// 空チェック
if (empty($username) || empty($content)) {
header("Location: index.php");
exit;
}
```
`??` 演算子でデフォルト値を設定し、空の場合は処理を中断します。
</Details>
#### TODO 3: データを挿入する
<Details summary="ヒント">
```php
$pdo = getDb();
$stmt = $pdo->prepare("INSERT INTO tweets (username, content) VALUES (?, ?)");
$stmt->execute([$username, $content]);
```
- `getDb()` でデータベース接続を取得
- `prepare()` と `execute()` でSQLインジェクション対策
- `created_at` は自動で設定されるので指定不要
</Details>
#### TODO 4: index.phpにリダイレクトする
<Details summary="ヒント">
```php
header("Location: index.php");
exit;
```
投稿が完了したら、タイムラインページに戻ります。
</Details>
### 確認方法
<DockerLink href="workspace/php/twitter/index.php" />
1. ユーザー名とつぶやきを入力して投稿
2. タイムラインに投稿が表示されることを確認
3. 他の人も投稿して、お互いの投稿が見えることを確認
**ここまでで基本機能は完成です!🎉**
---
## STEP2: 機能を追加する【発展】
基本機能ができたら、以下の機能を追加してみましょう。
### 発展1: いいね機能
投稿に「いいね」ボタンを追加します。
#### テーブルの追加
```sql
CREATE TABLE likes (
id INT AUTO_INCREMENT PRIMARY KEY,
tweet_id INT NOT NULL,
session_id VARCHAR(255) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_like (tweet_id, session_id)
);
```
- `UNIQUE KEY` で同じ人が同じ投稿に2回いいねできないようにする
#### 実装のヒント
<Details summary="ヒント: いいね数の表示">
```php
$pdo = getDb();
// 各投稿のいいね数を取得
$stmt = $pdo->prepare("SELECT COUNT(*) FROM likes WHERE tweet_id = ?");
$stmt->execute([$tweet['id']]);
$likeCount = $stmt->fetchColumn();
echo "{$likeCount}いいね";
```
</Details>
<Details summary="ヒント: いいねボタン">
```html
<form method="post" action="like.php" style="display: inline;">
<input type="hidden" name="tweet_id" value="<?php echo $tweet['id']; ?>">
<button type="submit">❤️ いいね</button>
</form>
```
`like.php` で `INSERT INTO likes` を実行します。
</Details>
### 発展2: 削除機能
自分の投稿を削除できるようにします。
<Details summary="ヒント: 削除ボタン">
```html
<form method="post" action="delete.php" style="display: inline;">
<input type="hidden" name="id" value="<?php echo $tweet['id']; ?>">
<button type="submit">削除</button>
</form>
```
</Details>
<Details summary="ヒント: delete.php">
```php
<?php
require_once 'db.php';
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$id = $_POST["id"] ?? 0;
if ($id > 0) {
$pdo = getDb();
$stmt = $pdo->prepare("DELETE FROM tweets WHERE id = ?");
$stmt->execute([$id]);
}
}
header("Location: index.php");
exit;
?>
```
</Details>
### 発展3: その他の改善
余裕があれば、以下の機能も追加してみましょう:
1. **文字数カウンター** - JavaScriptで残り文字数を表示
2. **ページネーション** - 投稿が多くなったら10件ずつ表示
3. **検索機能** - キーワードで投稿を検索
4. **画像投稿** - 画像をアップロードできるようにする
5. **スタイリング** - CSSで見た目を整える
## まとめ
この演習では、以下のことを学びました:
**STEP1(基本機能):**
- PHPとMySQLを組み合わせたデータの保存と取得
- フォームデータの受け取りと処理
- データベースからのデータ取得と表示
- セキュリティ対策(`htmlspecialchars()`、`prepare()`)
**STEP2(発展機能):**
- データの集計(`COUNT`)
- 複数テーブルの連携
- Sessionを使った重複防止
SNSのようなアプリケーションは、これらの技術を組み合わせて作られています。
今回作ったアプリを土台に、さらに機能を追加していくことで、本格的なウェブサービスに近づいていきます!