Newer
Older
hello-programmer-world / src / pages / final-work / 010-twitter.mdx
@h.sakamoto h.sakamoto 6 hours ago 9 KB work
---
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/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
);
```

## ファイル構成

`public/workspace/final-work/twitter/` に以下のファイルを用意します。

```
twitter/
├── db.php         # データベース接続設定
├── index.php      # メインページ
└── post.php       # 投稿処理
```

---

## STEP1: 基本機能を作る【必須】

まずは、投稿と表示ができる最小限のアプリを作ります。

### db.php(データベース接続)

データベース接続の設定ファイルです。**このファイルは最初から完成しています。**

```php file=public/workspace/final-work/twitter/db.php
public/workspace/final-work/twitter/db.php
```

他のファイルから `require_once 'db.php';` で読み込んで、`$pdo = getDb();` で使います。

### index.php(メインページ)

投稿フォームとタイムラインを表示するページです。

```php "TODO"
<?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のようなアプリケーションは、これらの技術を組み合わせて作られています。  
今回作ったアプリを土台に、さらに機能を追加していくことで、本格的なウェブサービスに近づいていきます!