はじめに
こんにちは、吉田です。
PHPを用いて、剣道の交流会を開催・検索できるWebサービス 「KENYU(剣友)」 をフルスクラッチで開発しました。
私は約20年間、自動車向け組込みソフトウェアの開発に携わってきました。その後、Webエンジニアへの転向を目指してPHPやLaravelを中心に学習し、2026年3月にフリーランスとして独立。
ポートフォリオを制作するにあたり、フレームワークを使ったアプリケーションだけでなく、Webアプリケーションの内部で動く仕組みを理解していることを示せる作品を作りたいと考え、本サービスを開発しました。
LaravelなどのWebフレームワークには頼らず、次の処理を一から実装しています。
- セッションによる認証管理
- ユーザーごとの認可制御
- 入力値のバリデーション
- PDOによるデータベースアクセス
- CRUD処理
- セキュリティ対策
- ページネーション
- Ajaxによる非同期処理
この記事では、サービスを作った背景、実装した機能、システム構成、開発で工夫・苦労した点を紹介します。
目次
- 開発概要
- サービス概要
- 開発の背景
- 利用の流れ
- 主な機能
- 使用技術
- システム構成
- ER図・データベース設計
- 開発で工夫した点
- セキュリティ対策
- 苦労した点と学んだこと
- 今後の改善予定
- GitHub・デモ
- まとめ
開発概要
| 項目 | 内容 |
|---|---|
| 開発期間 | 約2か月 |
| 開発人数 | 1人(個人開発) |
| 開発形態 | PHPフルスクラッチ |
| 主要ソースコード | 約1万行(PHP・JavaScript・CSS) |
| 画面数 | 約11画面 |
| DBテーブル数 | 11テーブル |
| 開発環境 | MAMP |
| バージョン管理 | Git / GitHub |
| 公開環境 | Xserver |
初版では、イベントを「作成する・探す・参加する」というサービスの核となる機能を優先しました。ユーザー認証、イベントのCRUD、検索、お気に入り、掲示板、画像アップロード、メール送信、論理削除など、一般的なWebサービスに必要な基本機能も実装しています。
サービス概要
KENYUとは
KENYUは、剣道の交流会や自主練習会を気軽に開催・検索できるWebサービスです。
想定しているのは、次のような利用者です。
- 久しぶりに剣道を再開したい人
- 普段とは違う相手と稽古したい人
- 道場に所属せず、都合のよいときに稽古したい人
- 自分で交流会や自主練習会を開きたい人
利用者は開催日や地域などからイベントを探して参加できます。また、自分でイベントを登録し、イベント専用の掲示板で参加者と連絡を取ることもできます。
開発の背景
私自身、子どもが剣道を始めたことをきっかけに、社会人になってから剣道を再開しました。その中で感じていたのが、少しだけ、あるいは久しぶりに稽古したい人の受け皿が少ないということでした。
多くの道場やクラブでは「初心者歓迎」「週1回から参加可能」と案内されています。しかし、初めて参加する側には次のような心理的なハードルがあります。
- 道場やクラブへの所属そのものに抵抗がある
- 知り合いがいない場所へ参加するのが不安
- 継続的な参加を求められるように感じる
- 自分のレベルで参加できるか分からない
一方で、「久しぶりに友人と剣道をしたい」「普段とは違う相手と地稽古をしたい」「競技志向ではなく楽しく汗を流したい」といった需要もあります。
しかし、そのような交流会の情報はSNSや口コミに分散しており、いつ、どこで、どのような稽古が行われるのかを探すのに手間がかかります。
そこで、剣道をしたい人が単発でも気軽に集まれる場所を作りたいと考え、KENYUを開発しました。
利用の流れ
主な機能
ユーザー登録・認証
ユーザー登録、ログイン・ログアウト、プロフィール編集、パスワード変更、パスワード再設定を実装しました。ログイン状態はセッションで管理し、認証が必要な画面へのアクセスを制御しています。
イベントの登録・編集・削除
開催日時、地域、対象者、イベント内容、画像などを設定して交流会を登録できます。登録した本人だけがイベントを編集・削除できます。また、イベントの作成時には専用の掲示板も作成されます。
イベントの検索・閲覧
登録されたイベントを一覧から検索・閲覧できます。詳細画面では、開催地域、日時、対象者、説明、画像、お気に入り数、掲示板の投稿件数などを確認できます。
イベント一覧・検索画面
イベント詳細画面
イベント参加・お気に入り
イベント詳細画面から、参加・参加取消、お気に入り登録・解除を行えます。これらはAjaxで処理し、ページ全体を再読み込みせずに状態が切り替わるようにしました。
イベント専用掲示板
イベントごとに掲示板を用意し、主催者や参加者がメッセージを投稿できるようにしました。投稿者名、プロフィール画像、投稿日時も表示します。
マイページ・プロフィール編集
マイページでは、参加したイベント、自分が作成したイベント、お気に入り、新着メッセージを確認できます。プロフィール画像やユーザー情報の編集にも対応しています。
マイページ
プロフィール編集画面
関連イベント確認画面
論理削除とユーザー復帰
ユーザーやイベントには論理削除を採用しています。退会したユーザーが同じメールアドレスで再登録した場合は、既存データとの関連を維持したままアカウントを復帰させます。
使用技術
| 分類 | 使用技術 |
|---|---|
| フロントエンド | HTML5 / CSS3 / JavaScript / jQuery |
| 非同期通信 | Ajax |
| バックエンド | PHP 7.4.33 |
| データベース | MySQL |
| DBアクセス | PDO |
| 外部ライブラリ | PHPMailer 7 / phpdotenv 5 |
| 開発環境 | MAMP |
| バージョン管理 | Git / GitHub |
| 公開環境 | Xserver |
composer.jsonでは、メール送信用のPHPMailerと環境変数読込用のphpdotenvを管理しています。データベースアクセスにはPDOを利用し、プレースホルダーへ値を渡してSQLを実行しています。お気に入りとイベント参加にはAjaxを利用し、操作後の画面遷移をなくしました。
システム構成
画面単位でPHPファイルを分け、複数画面から利用するデータベース操作、バリデーション、メール送信などはfunction.phpにまとめています。ログインが必要な画面ではauth.phpを読み込み、セッションを使って認証状態を確認します。
お気に入りとイベント参加については、ajaxLike.phpとajaxParticipant.phpがリクエストを受け取り、データベースを更新します。
主要ファイル構成
KENYU
├── database/
├── img/
├── js/
├── function.php # 共通処理
├── auth.php # 認証チェック
├── index.php # イベント一覧・検索
├── eventDetail.php # イベント詳細
├── registEvent.php # イベント登録・編集
├── msg.php # イベント掲示板
├── mypage.php # マイページ
├── signup.php # ユーザー登録
├── login.php # ログイン
├── logout.php # ログアウト
├── profEdit.php # プロフィール編集
├── passEdit.php # パスワード変更
├── passRemindSend.php # 再設定メール送信
├── passRemindRecieve.php # パスワード再設定
├── ajaxLike.php # お気に入り登録・解除
├── ajaxParticipant.php # イベント参加・取消
├── head.php
├── header.php
├── footer.php
├── sidebar.php
└── style.css
共通のヘッダー、フッター、サイドバーも別ファイルへ切り出し、各画面から再利用しています。
ER図・データベース設計
イベントと対象者、イベントと参加者、ユーザーとお気に入りイベントには多対多の関係があります。そのため、event_targets、event_participants、favoritesを中間テーブルとして分離しました。
ユーザー、イベント、掲示板、メッセージにはdelete_flgを設けています。また、短時間での大量登録を抑制するため、IPアドレスと登録日時を保存するsignup_logsテーブルも用意しています。
events.prefecture_idは都道府県マスタのIDを保持していますが、現在のSQLでは外部キー制約を設定していません。今後、既存データの整合性を確認したうえで制約を追加する予定です。
開発で工夫した点
共通処理を集約
データベース接続、SQL実行、バリデーション、データ取得、メール送信、画像アップロード、ログ出力などをfunction.phpへ集約しました。処理の重複を減らし、不具合修正や仕様変更の影響範囲を限定することが目的です。
例えば、SQLの実行はqueryPost()にまとめています。各処理ではSQLと値の配列だけを渡す構成とし、PDOのプリペアドステートメントを共通して利用できるようにしました。以下はfunction.phpの実装です。
function queryPost($dbh, $sql, $data) {
//クエリ作成
$stmt = $dbh->prepare($sql);
//プレースホルダに値をセットし、SQL実行
if (!$stmt->execute($data)) {
debug('クエリに失敗しました');
debug('失敗したクエリ:' . print_r($stmt,true));
$err_msg['common'] = ERR_SYSTEM;
return 0;
} else {
debug('クエリ成功');
return $stmt;
}
}
認証・認可を自前で実装
auth.phpでは、セッションに保存したログイン日時と有効期限を確認し、期限を過ぎたユーザーや未ログインユーザーをログイン画面へ戻しています。
認証だけでなく、イベント編集画面では、取得したイベントのuser_idとログイン中のuser_idをサーバー側で比較しています。URLのイベントIDを変更されても、投稿者本人でなければ編集画面を利用できません。
// registEvent.php
$dbFormData = (!empty($e_id)) ? getEventOneInfo($e_id) : '';
if (!empty($dbFormData)
&& (int)$dbFormData['user_id'] !== (int)$_SESSION['user_id']) {
header('Location:mypage.php');
exit();
}
また、セッション開始時にはsession_regenerate_id()を実行しています。今後は再生成のタイミングをログイン成功時に限定し、Cookie属性も含めてセッション設定を整理する予定です。
Ajaxで操作時の画面遷移を削減
イベント参加とお気に入りは、JavaScriptからPHPへリクエストを送り、Ajax通信が成功したときに表示を切り替える構成にしました。ページ全体の再読込が不要になるため、操作時の待ち時間と画面遷移を減らせます。
イベント参加では、Ajax用のPHPファイルでもauth.phpを読み込み、ログイン済みユーザーだけが登録・取消を行えるようにしています。また、セッションに直前の操作時刻を保存し、1秒未満の連続操作にはHTTP 429を返します。
// ajaxParticipant.php
require('auth.php');
$now = microtime(true);
if (isset($_SESSION['last_participant_time'])
&& ($now - $_SESSION['last_participant_time']) < 1) {
http_response_code(429);
exit();
}
$_SESSION['last_participant_time'] = $now;
$participantCount = isEventParticipants(
$_POST['eventId'],
$_SESSION['user_id']
);
if ($participantCount !== 0) {
eventParticipantDelete($_POST['eventId'], $_SESSION['user_id']);
} else {
eventParticipantRegister($_POST['eventId'], $_SESSION['user_id']);
}
なお、現在のAjaxレスポンスには更新後の状態を含めていません。今後はJSONで更新結果を返し、その値に基づいて画面を変更することで、通信成功とDB更新成功を明確に区別できる構成へ改善します。
トランザクションによるデータ整合性の維持
イベントの新規登録では、イベント本体だけでなく、イベント専用掲示板と対象者情報も同時に登録します。いずれか一つだけが登録された状態を残さないよう、これらを一つのトランザクションで処理しています。
実際のコードでは、各SQLの実行結果を確認し、途中で失敗した場合は例外を発生させています。以下はregistEvent.phpから、トランザクション制御に関係する部分を抜粋したものです。
$dbh->beginTransaction();
// イベントを登録
$stmt_event = queryPost($dbh, $sql, $data);
if (!$stmt_event) {
throw new Exception('イベント情報の新規登録に失敗しました');
}
$event_id = $dbh->lastInsertId();
// ここで掲示板登録用のSQLとパラメーターを設定
// (SQL・パラメーター定義は省略)
// イベントに対応する掲示板を登録
$stmt_board = queryPost($dbh, $sql, $data);
if (!$stmt_board) {
throw new Exception('掲示板の新規作成に失敗しました');
}
// 対象者をすべて登録できた後に確定
$dbh->commit();
例外を捕捉した場合は、次の処理で一連の変更を取り消します。
if ($dbh->inTransaction()) {
$dbh->rollBack();
}
error_log(
'イベント登録・編集処理でエラーが発生しました:'
. $e->getMessage()
);
$err_msg['common'] = ERR_SYSTEM;
同様に、退会処理とイベント削除でもトランザクションを使用しています。ユーザーやイベントを論理削除すると同時に、参加情報やお気に入りなどの関連データを更新・削除しています。
論理削除で関連データを維持
ユーザーを物理削除すると、過去のイベントや掲示板投稿との関連が失われる可能性があります。そこでユーザー、主催イベント、所有する掲示板には論理削除を採用しました。一方、参加情報とお気に入りは物理削除しており、すべての関連データを一律に論理削除しているわけではありません。
退会済みユーザーが同じメールアドレスとパスワードで登録した場合は、既存ユーザーのdelete_flgを戻し、主催イベントと掲示板をトランザクション内で復帰させています。
セキュリティ対策
| 対策対象 | 実装内容 |
|---|---|
| SQLインジェクション | PDOのプリペアドステートメントへ値の配列を渡して実行 |
| XSS | 出力時のHTMLエスケープ |
| パスワード |
password_hash()によるハッシュ化とpassword_verify()による照合 |
| 認可不備 | 投稿者本人であることをサーバー側で検証 |
| 連続リクエスト | 登録はIP単位、投稿はユーザー単位、Ajaxはセッション単位で制限 |
| 画像アップロード | 画像の実体を検証し、ハッシュ値を使った名前で保存 |
| パスワード再設定 | セッションに保存した認証コードと有効期限を照合 |
sanitize()では、次のようにhtmlspecialchars()を使ってHTML出力用の文字列をエスケープしています。
function sanitize($str) {
return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
画像アップロードでは拡張子だけを信用せず、exif_imagetype()で画像の実体を判定し、GIF・PNG・JPEGだけを許可しています。
$type = @exif_imagetype($file['tmp_name']);
if (!in_array($type, [
IMAGETYPE_GIF,
IMAGETYPE_PNG,
IMAGETYPE_JPEG,
], true)) {
throw new RuntimeException('画像ファイルが不正です');
}
$path = 'uploads/'
. sha1_file($file['tmp_name'])
. image_type_to_extension($type);
if (!move_uploaded_file($file['tmp_name'], $path)) {
throw new RuntimeException('ファイル保存時にエラーが発生しました');
}
現時点のコードにはCSRFトークンによる検証が実装されていません。そのため、実装済みの対策としては記載せず、今後の改善項目としています。また、論理削除はデータ管理方針であり、脆弱性への直接的な対策ではないため、この表には含めていません。
本番環境ではAPP_DEBUG=falseに設定する必要があります。現在はデバッグ有効時に認証コード、セッション情報、再発行パスワードなどもログへ出力するため、今後は設定への依存だけでなく、機密情報そのものをログ出力処理から除外します。
苦労した点と学んだこと
別用途のアプリケーションからデータモデルを再設計
KENYUは、不用品売買を題材にした別用途のアプリケーションを土台とし、コードとデータ構造を大幅に変更して開発しました。
元の「商品・出品者・購入者」という関係を、単に「イベント・主催者・参加者」と読み替えるだけでは、複数人のイベント参加や対象者条件を表現できません。そこでデータ同士の関係を整理し、参加者、対象者、お気に入りを中間テーブルで管理する構成へ変更しました。
この経験から、画面や機能を作り始める前に、データの関係と責務を整理することが保守性に直結すると学びました。
1対1のメッセージ機能をイベント掲示板へ変更
元のアプリケーションには、購入者と出品者による1対1のメッセージ機能がありました。しかし、KENYUでは複数の利用者がイベント単位で交流する必要があります。
そこで、掲示板をイベントに紐づけ、複数ユーザーの投稿を扱えるようにデータ取得と表示処理を変更しました。投稿者名、投稿日時、プロフィール画像を表示し、自分と他のユーザーの投稿を左右に分けています。
同じ「メッセージ機能」でも、利用目的が変われば必要なデータ構造とUIも変わることを実感しました。
安全な画像アップロード処理
画像は、元のファイル名のまま保存せず、exif_imagetype()で実体が許可した画像形式かを検証したうえで、ファイル内容のSHA-1ハッシュを保存名に使用し、move_uploaded_file()で保存しています。
画像アップロードは保存処理だけで完結せず、ファイルの実体、サイズ、保存先、公開方法まで考慮する必要があると学びました。
改修による影響範囲の調査
データ構造を変更すると、イベント一覧、詳細、マイページ、掲示板など複数画面に影響が及びます。問題が発生した際には、SQLの実行結果、セッションの内容、PHPのログを確認して原因を切り分けました。
また、同じ処理が複数箇所にある場合は共通化し、修正箇所を減らしました。この経験から、機能追加だけでなく、変更の影響範囲を把握しやすい構成にすることの重要性を学びました。
今後の改善予定
初版では、イベントを作成し、探して参加できるところまでを優先して実装しました。今後は実際のイベント運営を想定し、次の順序で改善する予定です。
-
参加人数・定員管理
イベント作成時の定員設定、現在の参加人数表示、定員到達後の参加制限を追加します。 -
募集状態の管理
募集中、定員到達、募集終了、開催終了を区別し、一覧と詳細画面に表示します。 -
ユーザー情報の拡充
主催者プロフィール、参加者一覧、ユーザー詳細ページを追加します。 -
運用・品質面の強化
PHP 8系への移行、CSRF対策、セッションCookie属性の見直し、機密情報をログへ残さない処理への変更を優先します。その後、管理者機能、通知、レビュー、地図連携、検索条件の拡充、自動テストを順次追加します。AjaxについてはJSONレスポンスを導入し、サーバーの更新結果を画面側で判定できるようにします。
GitHub・デモ
- GitHub: i994114/KENYU
- デモ: KENYU(剣友)
- テストユーザー:
akane@example.com - パスワード:
aaaa1111
まとめ
KENYUの開発を通して、認証、認可、セッション管理、データベース設計、Ajax、メール送信、画像アップロードといったWebアプリケーションの基本機能を、PHPで一から実装しました。
特に、別用途のアプリケーションからサービスの目的に合わせてデータモデルを再設計したことで、画面より先にデータ同士の関係を整理する重要性を学びました。また、フレームワークが担っている処理を自分で実装したことで、その便利さだけでなく、内部の仕組みや注意点への理解も深まりました。
今後は、参加定員や募集状態など、イベント運営に欠かせない機能から改善し、実用性と品質の両面を高めていきます。












