4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ゼロから始めるPHP:チャットアプリ開発入門

Last updated at Posted at 2023-07-28

目次

記事の内容

コード例: https://github.com/nightshrine/chatapp_php
完成イメージ:
スクリーンショット 2023-07-28 13.04.46.png

この記事では、Laravelを使っている私がPHPを基礎から学び直し、チャットアプリを作成する手順を説明します。前提知識としてHTML、CSSはある程度書けることを想定しています。ご了承ください。

なぜ作ろうと思ったのか

業務でLaravelを使ってWebアプリを開発させていただいた経験や、勉強として簡単なアプリをいくつか作りました。しかし、生のPHPで何か作ったことは無く、細かいPHPの仕様について知らないことが多数ありました。フレームワークはありますが、フレームワークによって抽象化された機能の内部を深く理解することができますし、生の言語を理解することで様々な仕様に対して柔軟に対応できると思います。というわけでオライリー本の「プログラミングPHP 第4版」読んだのですが、インプットだけではつまらないので何かアウトプットをしてみようと思ったことがきっかけです。

開発環境

M2 MacbookAirで、MAMPを用いて開発しました。また開発環境の詳細は以下に示します。

PHP: 8.2.8
MySQL: 8.0.33

本編

まずはMAMPをインストールし、port番号を8080から80に変更、DirectoryRootを開発用のディレクトリを指定するなど、事前にやることがいくつかある。そういった部分についての詳しい説明に関しては別ページを参考にしてほしい。ちなみに環境によってはXAMPPなどを用いても良い。ここら辺の各種設定についてはお好みで環境構築を行なってほしい。

準備ができたら開発用のディレクトリを以下のようにする。

.
├── README.md
├── index.php
├── login.css
├── login.php
├── logout.php
├── register.php
├── send_message.php
├── sql_connection.php
├── style.css
└── update_profile.php

基本的にはファイル名通りの処理を内部で行なっている。これから各ファイルごとに説明していくが、必ずしも順番に作っていく必要はないため、各自好きな順番で作成してほしい。

DB作成

簡単なチャットアプリなため、ユーザーを管理するものと、メッセージを管理するものの2つがあれば十分だろう。ここではMySQLを使うが、どんなDBを使っても構わない。ファイルベースであるSQLiteを用いるのも良いだろう。
MySQLの詳しい使い方などは省略するが、DBを作成するためのSQL文を以下に示す。

CREATE TABLE users (
  id int NOT NULL auto_increment primary key,
  name varchar(255) NOT NULL,
  email varchar(255) NOT NULL,
  password varchar(255) NOT NULL,
);

CREATE TABLE messages (
  id INT NOT NULL auto_increment primary key,
  message TEXT NOT NULL ,
  users_id INT NOT NULL ,
  FOREIGN KEY (users_id) REFERENCES users(id)
);

画面の見た目

チャットを行う画面をindex.phpに記載するため、ログイン、新規登録したらここに飛ぶようにしたい。このページが表示されたことがわかりやすいように、index.phpに以下を記載しておこう。

<h1>index</h1>

レイアウトが崩れているととてもページが見にくくなってしまうため、cssを記述しておく。詳しい記述方法については別ページを参考にしてほしい。ついでにチャット画面のcssも設定してしまおう。
まずはlogin.cssに以下の内容を記述する。

body {
    font-family: Arial, sans-serif;
    margin: 0;
    padding: 0;
    background-color: #f2f2f2;
}

.login-container, .register-container, .update-user-container {
    max-width: 400px;
    margin: 50px auto;
    padding: 20px;
    background-color: #fff;
    border-radius: 5px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

h2 {
    text-align: center;
    margin-bottom: 20px;
}

form {
    display: flex;
    flex-direction: column;
}

label {
    margin-bottom: 5px;
}

input {
    padding: 10px;
    margin-bottom: 15px;
    border: 1px solid #ccc;
    border-radius: 3px;
}

button {
    margin-top: 10px;
    padding: 10px 15px;
    background-color: #007bff;
    color: #fff;
    border: none;
    border-radius: 3px;
    cursor: pointer;
}

button:hover {
    background-color: #0056b3;
}

p {
    text-align: center;
}

a {
    color: #007bff;
    text-decoration: none;
}

a:hover {
    text-decoration: underline;
}

style.cssに以下の内容を記述する。

body {
    font-family: Arial, sans-serif;
    margin: 0;
    padding: 0;
    background-color: #f2f2f2;
}

.container {
    display: flex;
    max-width: 1000px;
    margin: 50px auto;
    padding: 20px;
    background-color: #fff;
    border-radius: 5px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

.left {
    flex: 1;
    padding: 20px;
    text-align: center;
}

.left h2 {
    text-align: center;
    margin-bottom: 20px;
}

.right {
    flex: 2;
    padding: 20px;
}

.right h2 {
    text-align: center;
    margin-bottom: 20px;
}

.chat-box {
    max-height: 400px;
    overflow-y: auto;
    border: 1px solid #ccc;
    border-radius: 5px;
    padding: 10px;
    margin-bottom: 20px;
}

.chat-box p {
    margin: 5px 0;
}

.user-button {
    display: block;
    width: 60%;
    margin: 10px auto;
    padding: 10px 20px;
    background-color: #007bff;
    color: white;
    text-decoration: none;
    border-radius: 5px;
}
.user-button.logout {
    background-color: rgb(255, 120, 120);
}

.button-container a:hover {
    background-color: #0056b3;
}

.chat-form {
    display: flex;
    align-items: center;
}

.chat-form label {
    margin-right: 10px;
}

.chat-form input[type="text"] {
    flex: 1;
    padding: 8px;
    border: 1px solid #ccc;
    border-radius: 5px;
}

.chat-form button {
    margin-left: 10px;
    padding: 8px 20px;
    background-color: #4CAF50;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
}

.chat-form button:hover {
    background-color: #45a049;
}

登録画面

記述が単純なログイン画面から作成していきたいが、DBに接続するコードを別途記述する。DBに接続したいときに何回も同じコードを書くのは面倒であるため、別途ファイルを用意してそこに記述しておく。そのコードを利用するときはincludeすれば良い。
sql_connection.phpに以下を記述する。

<?php

$servername = "localhost";
$username = "root";
$password = "root";
$dbname = "chatapp_php";

$conn = new mysqli($servername, $username, $password, $dbname);

if ($conn->connect_error) {
die("データベースに接続できませんでした: " . $conn->connect_error);
}

mysqliを用いることで、DBに接続を行うことができる。また、エラーがあればconnect_errorに格納されるため、何かあればif文の中でエラー分を表示するように記述している。デフォルトではmysql.sockがMAMPに内蔵されているものを使うようになっている。もし普段から使用しているmysqlの内容を参照したい場合は、$socketにmysql.sockのパスを渡して引数としてmysqlini渡す必要がある。ユーザー名やパスワードなどは適宜変えてほしいが、パスワードを直書きするのはよくないため環境変数などを設定しよう。ここでは詳しい方法は省略するため、別ページを参照してほしい。

login.phpに以下を記述する。

<?php session_start();?>
<!DOCTYPE html>
<html>

<head>
    <title>ログイン</title>
    <link rel="stylesheet" type="text/css" href="login.css">
    <meta charset="utf-8">
</head>

<body>
    <?php if ($_SERVER['REQUEST_METHOD'] == 'GET') { ?>
        <div class="login-container">
            <h2>ログイン</h2>
            <form action=<?php echo $_SERVER['PHP_SELF'] ?> method="post">
                <label for="email">メールアドレス:</label>
                <input type="email" id="email" name="email" required>

                <label for="password">パスワード:</label>
                <input type="password" id="password" name="password" required>

                <button type="submit">ログイン</button>
                <?php if(isset($_SESSION["err_login_message"])) {
                    echo "<p style='color: red;'>".$_SESSION["err_login_message"]."</p>";
                } ?>
            </form>
            <p>アカウントをお持ちでない方は<a href="register.php">新規登録ページ</a></p>
        </div>
    <?php } ?>

    <?php if ($_SERVER['REQUEST_METHOD'] == 'POST') { ?>
        <?php

        include "sql_connection.php";

        // フォームからの入力を取得
        $email = $_POST["email"];
        $password = $_POST["password"];

        
        $stmt = $conn->prepare("SELECT * FROM users WHERE email = ?");
        $stmt->bind_param("s", $email);
        $stmt->execute();
        $result = $stmt->get_result();

        if ($result->num_rows > 0) {
            $user = $result->fetch_assoc();
            if (password_verify($password, $user['password'])) {
                // ログイン成功時にindex.phpにリダイレクト
                $_SESSION["id"] = $user["id"];
                $_SESSION["name"] = $user["name"];
                $_SESSION["email"] = $user["email"];
                header("Location: index.php");
                $_SESSION["err_login_message"] = null;
                exit;
            } else {
                $_SESSION["err_login_message"] = "メールアドレスまたはパスワードが間違っています。";
                header("Location: login.php");
                exit;
            }
        } else {
            // ログイン失敗
            $_SESSION["err_login_message"] = "メールアドレスまたはパスワードが間違っています。";
            header("Location: login.php");
            exit;
        }

        $conn->close();
        ?>
    <?php } ?>
</body>

</html>

このアプリは、セッションを使用してユーザーのログイン状態を管理している。
以下の部分について軽く説明を行う。

$stmt = $conn->prepare("SELECT * FROM users WHERE email = ?");
$stmt->bind_param("s", $email);
$stmt->execute();
$result = $stmt->get_result();

    if ($result->num_rows > 0) {
        $user = $result->fetch_assoc();

$stmtはstatementを表しており、文字通りSQL文を格納している。また、SQLインジェクション対策として、$emailはbind_paramを用いてSQL文に追記している。第一引数の"s"は、文字列表しており、"d"はfloat型、"i"はint型を表す。そしてexecute()で実行を行い、get_result()で結果を格納することができる。また、fetch_assoc()を使うことで、$user["id"]というような形で結果を取り出すことができる。

また、以下の記述はハッシュ化されたパスワードと等しいかを自動で確かめてくれるものである。

password_verify($password, $user['password'])

PHPコードについて主なポイントとしては以下が挙げられる。

  • 「session_start();」でセッションを開始している。
  • ログインフォームがGETリクエストの場合は、ログインフォームを表示する。POSTリクエストの場合は、ログイン処理を行う。
  • ログイン処理では、データベースに接続して、入力されたメールアドレスを使用してユーザー情報を取得する。その後、入力されたパスワードをハッシュ化されたパスワードと比較し、一致すればログイン成功、一致しなければログイン失敗となる。
  • ログインが成功した場合は、ユーザー情報をセッションに保存し、別のページにリダイレクトする。
  • ログインが失敗した場合は、エラーメッセージをセットしてログインページにリダイレクトする。

login.phpを見てみると、以下のようなページが表示されるだろう。
スクリーンショット 2023-07-28 13.15.41.png

これができれば新規登録画面、情報更新ページの作成は難しくない。次は新規登録画面を作成する。
register.phpに以下を記述する。

<?php session_start(); ?>
<!DOCTYPE html>
<html>

<head>
    <title>ログイン</title>
    <link rel="stylesheet" type="text/css" href="login.css">
    <meta charset="utf-8">
</head>

<body>
    <?php if ($_SERVER['REQUEST_METHOD'] == 'GET') { ?>
        <div class="register-container">
            <h2>新規登録</h2>
            <form action=<?php echo $_SERVER['PHP_SELF'] ?> method="post">
                <label for="name">名前:</label>
                <input type="text" id="name" name="name" required>

                <label for="email">メールアドレス:</label>
                <input type="email" id="email" name="email" required>

                <label for="password">パスワード:</label>
                <input type="password" id="password" name="password" required>

                <label for="confirm_password">パスワード確認:</label>
                <input type="password" id="confirm_password" name="confirm_password" required>

                <button type="submit">登録</button>
                <?php if (isset($_SESSION["err_register_message"])) {
                    echo "<p style='color: red;'>" . $_SESSION["err_register_message"] . "</p>";
                } ?>
            </form>
            <p>既にアカウントをお持ちの方は<a href="login.php">ログインページ</a></p>
        </div>
    <?php } ?>

    <?php if ($_SERVER['REQUEST_METHOD'] == 'POST') { ?>
        <?php

        include "sql_connection.php";

        // フォームからの入力を取得
        $name = $_POST["name"];
        $email = $_POST["email"];
        $password = $_POST["password"];
        $confirm_password = $_POST["confirm_password"];

        $stmt = $conn->prepare("SELECT * FROM users WHERE email = ?");
        $stmt->bind_param("s", $email);
        $stmt->execute();
        $result = $stmt->get_result();

        if ($result->num_rows === 0) {
            if ($password === $confirm_password) {
                $hashed_password = password_hash($password, PASSWORD_DEFAULT);
                $stmt = $conn->prepare("INSERT INTO users (name, email, password) VALUES (?, ?, ?)");
                $stmt->bind_param("sss", $name, $email, $hashed_password);
                $stmt->execute();

                $stmt = $conn->prepare("SELECT * FROM users WHERE email = ?");
                $stmt->bind_param("s", $email);
                $stmt->execute();
                $result = $stmt->get_result();
                $user = $result->fetch_assoc();
                // ログイン成功時にindex.phpにリダイレクト
                $_SESSION["id"] = $user["id"];
                $_SESSION["name"] = $user["name"];
                $_SESSION["email"] = $user["email"];
                $_SESSION["err_register_message"] = null;

                header("Location: index.php");
                exit;
            } else {
                $_SESSION["err_register_message"] = "確認と違うパスワードが入力されました。";
                header("Location: register.php");
                exit;
            }
        } else {
            // ログイン失敗
            $_SESSION["err_register_message"] = "そのemailは登録されています。";
            header("Location: register.php");
            exit;
        }

        $conn->close();
        ?>
    <?php } ?>
</body>

</html>

以下の記述でパスワードをハッシュ化している。

$hashed_password = password_hash($password, PASSWORD_DEFAULT)

暗号化の種類はいくつかあるみたいだが、ここではPASSWORD_DEFAULTを使う。

PHPコードについて主なポイントとしては以下が挙げられる。

  • 「session_start();」でセッションを開始しています。セッションを使用することで、ユーザーのログイン状態を管理できます。
  • ログインフォームがGETリクエストの場合は、新規登録フォームを表示します。
  • ログインフォームがPOSTリクエストの場合は、フォームからの入力データを取得します。
  • 入力されたメールアドレスがデータベースに存在するかを確認します。もし存在しない場合、パスワードの確認用として入力された内容も一致しているかをチェックします。
  • パスワードが確認用と一致している場合、パスワードをハッシュ化してデータベースに新しいユーザーを登録します。
  • 新しいユーザーが登録された後、再度データベースからそのユーザーの情報を取得します。ログイン成功時のために、ユーザー情報をセッションに保存します。
  • 登録に成功した場合は、「header("Location: index.php");」を使用して別のページ(ここではindex.php)にリダイレクトします。これにより、ユーザーがログイン状態になります。
  • 登録が失敗した場合は、エラーメッセージをセットして元の新規登録ページにリダイレクトします。

register.phpを見てみると、以下のようなページが表示されるだろう。
スクリーンショット 2023-07-28 13.28.21.png

この画面でフォームを入力して登録ボタンを押せば、index.phpの内容が表示されるだろう。

メイン画面

それではチャットアプリの中身を作成していく。
index.phpを以下の記述に変更する。

<?php
// ログインしていない場合はログインページにリダイレクト
session_start();
if (!isset($_SESSION["id"])) {
    header("Location: login.php");
    exit;
}
?>

<!DOCTYPE html>
<html>

<head>
    <title>チャットアプリ</title>
    <link rel="stylesheet" type="text/css" href="style.css">
    <meta charset="utf-8">
</head>

<body>
    <div class="container">
        <div class="left">
            <h2>ユーザー情報</h2>
            <p>ユーザー名:<?= $_SESSION["name"] ?></p>
            <p>メールアドレス:<?= $_SESSION["email"] ?></p>
            <a href="update_profile.php" class="user-button">ユーザー情報を変更する</a>
            <a href="logout.php" class="user-button logout">ログアウト</a>

        </div>
        <div class="right">
            <h2>チャット</h2>
            <div class="chat-box">
                <?php

                include "sql_connection.php";

                // メッセージを取得
                $sql = "SELECT messages.message, users.name FROM messages INNER JOIN users ON messages.users_id = users.id ORDER BY messages.id";
                $result = $conn->query($sql);

                if ($result->num_rows > 0) {
                    while ($row = $result->fetch_assoc()) {
                        echo "<p><strong>" . $row['name'] . ":</strong> " . $row['message'] . "</p>";
                    }
                } else {
                    echo "<p>メッセージがありません</p>";
                }

                $conn->close();
                ?>
            </div>
            <form action="send_message.php" method="post" class="chat-form">
                <label for="message">メッセージを入力してください:</label>
                <input type="text" id="message" name="message" required>
                <button type="submit">送信</button>
            </form>
        </div>
    </div>
    <script>
        // チャットボックスが常に下にスクロールされるようにする関数
        window.onload = () => {
            let chatBox = document.querySelector(".chat-box");
            chatBox.scrollTop = chatBox.scrollHeight;
        };
    </script>
</body>


</html>

PHPコードについて主なポイントとしては以下が挙げられる。

  • ログインしていない場合はログインページにリダイレクト。
  • ユーザー情報が表示される。
  • チャットメッセージが表示される。
  • 新しいメッセージの送信が可能。
  • チャットボックスが常に下にスクロールされるようにするJavaScriptコードを記載している。
  • データベース接続用のファイルがインクルードされる。
  • メッセージがない場合に「メッセージがありません」と表示。
  • 新しいメッセージをデータベースに保存して表示する。
  • セキュリティの向上のため、ログインしていないユーザーが直接アクセスした場合はログインページにリダイレクトされる。

SQL文が多少わかりにくいかもしれないが、SQLの詳しい説明は別ページを参照してほしい。
index.phpを見てみると、以下のような画面が表示されるだろう。
スクリーンショット 2023-07-28 13.04.46.png

しかし、メッセージを送信する部分は別のプログラムで書くので、まだメーセージを送信することができない。ということでメッセージを送信できるようにしよう。
send_message.phpに以下を記述する。

<?php
session_start();
include "sql_connection.php";

$message = $_POST["message"];
$users_id = $_SESSION["id"];

$stmt = $conn->prepare("INSERT INTO messages (message, users_id) VALUES (?, ?)");
$stmt->bind_param("si", $message, $users_id);
$stmt->execute();

header("Location: index.php");
exit;

ここについては、新規登録画面ができれば特に説明することはない。送信できたら自動でindex.phpに戻るようにしている。
ここまでできれば、メッセージを入力して送信ボタンを押すと入力した内容がユーザー名と一緒に表示されると思う。また、ある程度多くメッセージを入力しても勝手に一番下までスクロールしてくれるようにJSを記述したため、常に新しいメッセージが表示されるようにもなっている。
それでは残りのユーザー情報を更新するページと、ログアウトについて記述していこう。

更新ページ

update_profile.phpに以下を記述する。

<?php session_start(); ?>
<!DOCTYPE html>
<html>

<head>
    <title>ユーザー情報更新</title>
    <link rel="stylesheet" type="text/css" href="login.css">
    <meta charset="utf-8">
</head>

<body>
    <?php if ($_SERVER['REQUEST_METHOD'] == 'GET') { ?>
        <div class="update-user-container">
            <h2>ユーザー情報更新</h2>
            <form action=<?php echo $_SERVER['PHP_SELF'] ?> method="post">
                <label for="name">名前:</label>
                <input type="text" id="name" name="name" value=<?php echo $_SESSION["name"] ?> required>

                <label for="password">新しいパスワード:</label>
                <input type="password" id="password" name="password" required>

                <label for="confirm_password">パスワード確認:</label>
                <input type="password" id="confirm_password" name="confirm_password" required>

                <button type="submit">更新</button>
                <?php if (isset($_SESSION["err_update_message"])) {
                    echo "<p style='color: red;'>" . $_SESSION["err_update_message"] . "</p>";
                } ?>
            </form>
        </div>
    <?php } ?>

    <?php if ($_SERVER['REQUEST_METHOD'] == 'POST') { ?>
        <?php

        include "sql_connection.php";

        // フォームからの入力を取得
        $name = $_POST["name"];
        $password = $_POST["password"];
        $confirm_password = $_POST["confirm_password"];

        $stmt = $conn->prepare("SELECT * FROM users WHERE email = ?");
        $stmt->bind_param("s", $email);
        $stmt->execute();
        $result = $stmt->get_result();

        if ($result->num_rows === 0) {
            if ($password === $confirm_password) {
                $hashed_password = password_hash($password, PASSWORD_DEFAULT);
                $stmt = $conn->prepare("UPDATE users SET name=?, password=? WHERE id=?");
                $stmt->bind_param("ssi", $name, $hashed_password, $_SESSION["id"]);
                $stmt->execute();
                $stmt = $conn->prepare("SELECT * FROM users WHERE id = ?");
                $stmt->bind_param("s", $_SESSION["id"]);
                $stmt->execute();
                $result = $stmt->get_result();
                $user = $result->fetch_assoc();
                // ログイン成功時にindex.phpにリダイレクト
                $_SESSION["id"] = $user["id"];
                $_SESSION["name"] = $user["name"];
                $_SESSION["email"] = $user["email"];
                $_SESSION["err_update_message"] = null;

                header("Location: index.php");
                exit;
            } else {
                $_SESSION["err_update_message"] = "確認と違うパスワードが入力されました。";
                header("Location: update_profile.php");
                exit;
            }
        } else {
            // ログイン失敗
            $_SESSION["err_update_message"] = "そのemailは登録されています。";
            header("Location: update_profile.php");
            exit;
        }

        $conn->close();
        ?>
    <?php } ?>
</body>

</html>

SQL文にはUPDATEを用いているというところ以外、基本的な動作は新規登録画面に近いため説明は省略する。画面表示も、新規登録画面に近い見た目となっているだろう。

ログアウト

最後にログアウト機能を作成する。とはいえセッションの情報を削除してログイン画面に戻れば終わりである。
logout.phpに以下を記述する。

<?php
session_start();

$_SESSION["id"] = null;
$_SESSION["name"] = null;
$_SESSION["email"] = null;

header("Location: login.php");

これで全ての機能が追加することができた。ほとんどPHPだけで簡単なチャットアプリができた。

終わりに

この記事では、PHPを基礎から学び直し、魅力的なチャットアプリを作成する過程をお伝えしました。正直に申し上げると、MySQLやMAMP、php.iniの設定などには予想以上に時間がかかってしまいましたが、それでも素早くチャットアプリを完成させることができました。
しかしながら、このチャットアプリはまだセキュリティ面で完全に安全とは言えません。その点を考慮すると、まだまだアプリには改善すべき点がたくさん存在します。Laravelのようにセキュリティを内部で自動的に管理してくれるフレームワークの便利さを再認識しました。さらにPHPの理解を深めるために、このアプリを拡張していくことで学びを深めていきたいと考えています。(メッセージの編集や削除機能の実装など)
この記事をお読みいただき、心から感謝しています。今後も何か新しいアウトプットがあれば、引き続きこのような記事を書いていきたいと思います。皆さんと一緒に成長していくことを楽しみにしています。ありがとうございました。

追記

本記事で作成したアプリはXSS攻撃とCSRF攻撃などを行われてしまう危険性があります。その具体的な攻撃方法と解決策についてPHPチャットアプリのセキュリティ強化:XSSとCSRF攻撃への対策にまとめましたので、ぜひこちらもご覧ください。

4
3
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?