CakePHP5入門【CakePHP5実用編⑦】さまざまな改良

B美
まずはチャットルームに入室したとき、テーマの名称(チャットの議題)をチャット画面の上のほうに表示しよう

C菜
「src/Controller」の中にある「ChatController.php」の「index」メソッドにテーマ名を取得する処理を書いて、View側に渡すだけです~
もちろん、View側(「templates/Chat」の中にある「index.php」)のほうにも、それを表示する処理を書き加えますけど~



A子
CSSの箇所に「色」を直接指定してるよね
あれを定数化しよう
(あとで好きな色に変更するために…)



B美
チャットルームに一人しかいないのに、延々としゃべり続けるという(間抜けな)事態を回避するためにも…(笑)
まずは「WebSocketServerCommand」クラスに新たなフィールド「$usernames」を追加して、初期化処理を書くよ
class WebSocketServerCommand extends Command implements MessageComponentInterface
{ protected array $clients = []; protected array $rooms = []; protected array $usernames = []; private $Comments; public function initialize(): void { parent::initialize(); $this->clients = []; $this->rooms = []; $this->usernames = []; $this->Comments = $this->fetchTable('Comments'); } |
次に「onOpen」メソッドを書き換えるんだけど、ここでは初期化処理を追加しただけね
public function onOpen(ConnectionInterface $conn): void
{ $conn->roomId = null; $conn->username = null; $this->clients[$conn->resourceId] = $conn; } |
次は中核となる「onMessage」メソッドなんだけど…
本当の中核処理はprivateのメソッドにまとめて、ここからそれ(「broadcast」と「broadcastUserList」の二つのメソッド)を呼び出しているわ
public function onMessage(ConnectionInterface $from, $msg): void
{ $data = json_decode($msg, true); if (!isset($data['type']) || !isset($data['room_id'])) { return; } $roomId = $data['room_id']; $userId = $data['user_id']; if ($data['type'] === 'join') { $from->roomId = $roomId; $from->username = $data['user']; if (!isset($this->rooms[$roomId])) { $this->rooms[$roomId] = []; } $this->rooms[$roomId][$from->resourceId] = $from; $this->usernames[$from->resourceId] = $data['user']; $this->broadcastUserList($roomId); $this->broadcast($roomId, [ 'type' => 'system', 'message' => "{$data['user']} が入室しました。" ]); } elseif ($data['type'] === 'message' && $from->roomId !== null) { $messageData = [ 'room_id' => $roomId, 'user_id' => $userId, 'user' => $data['user'], 'message' => $data['message'] ]; //データベースへの格納 $comment = $this->Comments->newEmptyEntity(); $comment->content = $data['message']; $comment->room_id = $roomId; $comment->user_id = $userId; $this->Comments->save($comment); $this->broadcast($roomId, $messageData); } } |
あと、退室時の処理を記述するから、「onClose」メソッドについても書き換えるわね
public function onClose(ConnectionInterface $conn): void
{ if ($conn->roomId !== null && isset($this->rooms[$conn->roomId][$conn->resourceId])) { $roomId = $conn->roomId; $username = $this->usernames[$conn->resourceId] ?? 'Unknown'; unset($this->rooms[$roomId][$conn->resourceId]); unset($this->usernames[$conn->resourceId]); $this->broadcastUserList($roomId); $this->broadcast($roomId, [ 'type' => 'system', 'message' => "{$username} が退室しました。" ]); } unset($this->clients[$conn->resourceId]); } |
最後に、privateメソッドとして「broadcast」と「broadcastUserList」の二つを追加するよ
private function broadcast(int $roomId, array $message): void
{ if (!isset($this->rooms[$roomId])) { return; } foreach ($this->rooms[$roomId] as $client) { $client->send(json_encode($message)); } } |
private function broadcastUserList(int $roomId): void
{ if (!isset($this->rooms[$roomId])) { return; } $userList = array_values(array_map(fn($client) => $client->username, $this->rooms[$roomId])); $this->broadcast($roomId, [ 'type' => 'user_list', 'users' => $userList ]); } |






A子
B美がいなかったら絶対無理だったよ

B美

A子
私の称賛を返せや(苦笑)

B美
(割と試行錯誤してるから…)
まぁ、そんなことはともかくとして、まだ終わりじゃないわよ
View側(JavaScriptのコード)を書き換えなきゃいけないからね
(「templates/Chat」の中にある「index.php」)
const ws = new WebSocket("ws://<?= WEBSOCKET_IP ?>:<?= WEBSOCKET_PORT ?>");
const userId = <?= json_encode($userId) ?>; const roomId = <?= json_encode($roomId) ?>; const userName = <?= json_encode($userName) ?>; ws.onopen = function() { ws.send(JSON.stringify({ type: "join", room_id: roomId, user_id: userId , user: userName})); }; ws.onmessage = function(event) { const data = JSON.parse(event.data); if (data.type === "user_list") { updateUserList(data.users); } else if (data.type === "system") { addSystemMessage(data.message); } else { addMessage(data.user_id, data.user, data.message); } }; document.getElementById("sendBtn").addEventListener("click", function () { const input = document.getElementById("message"); if (input.value.trim() !== "") { ws.send(JSON.stringify({ type: "message", room_id: roomId, user_id: userId, user: userName, message: input.value })); input.value = ""; } }); function addMessage(senderId, user, message) { const chatBox = document.getElementById("chat-box"); const msgDiv = document.createElement("div"); msgDiv.classList.add("message"); if (senderId == userId) { msgDiv.classList.add("sent"); //自分のメッセージ } else { msgDiv.classList.add("received"); //他人のメッセージ } msgDiv.innerHTML = `<p><strong>${user}</strong></p><p>${message}</p>`; chatBox.appendChild(msgDiv); chatBox.scrollTop = chatBox.scrollHeight; } function updateUserList(users) { const userList = document.getElementById("user-list"); userList.innerHTML = users.map(user => `<li>${user}</li>`).join(""); } function addSystemMessage(message) { const chatBox = document.getElementById("chat-box"); const msgDiv = document.createElement("div"); msgDiv.classList.add("system-message"); msgDiv.textContent = message; chatBox.appendChild(msgDiv); chatBox.scrollTop = chatBox.scrollHeight; } document.addEventListener("DOMContentLoaded", function () { const messageInput = document.getElementById("message"); const sendButton = document.getElementById("sendBtn"); //Enterキーで送信(Shift + Enterで改行) messageInput.addEventListener("keydown", function (event) { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); sendButton.click(); } }); }); |
あと、CSSのクラスを一つ追加して、HTML部分にも一行を書き加えるよ
.system-message {
max-width: 100%; padding: 10px; margin: 5px; border-radius: 10px; display: inline-block; word-wrap: break-word; position: relative; background-color: #ffffff; color: #ff0000!important; text-align: center; } |
<div style="width: 100%;text-align: center;color: <?= THEME_COLOR ?>;">【 <?= $chat_room->theme ?> 】</div>
<br /> <div id="chat-container"> <div id="user-list" style="background-color: <?= USER_LIST_COLOR ?>;margin: 10px;padding: 10px;"></div> <div id="chat-box"> |
ちなみに、このページの冒頭でテーマの色を(greenに)固定していたけど、これもついでに定数化しておこう
(あとで変更しやすいように…)
define("THEME_COLOR", "#006400"); //テーマの文字色
define("USER_LIST_COLOR", "#fffacd"); //ユーザ一覧の背景色 |
この二行を「config/const.php」に追記する…ってことね









A子
あー、あとさ
チャットの入力欄なんだけど、毎回「半角/全角」キーを押して「日本語入力モード」に切り替えるのが面倒くさいんだよね
それって、なんとかならないかな?
それと、「ページネーション」の件数を設定してない気がする
(「ユーザ管理」でも「チャットルーム管理」でも…)
んー、あとは、そうだなー
スマホ(レスポンシブ)対応くらいかな?

B美
(できなくはないんだけど、ブラウザに依存する処理になるの)
ただ、「ページネーション」と「レスポンシブ」については、前にもやったから難しくはないでしょう
あなたたちでやってみなさい(復習を兼ねて)

C菜
今回はここまでずっとB美部長頼りでしたからね~

A子
んじゃ、まずは「ページネーション」だね
define("USER_MAX_PAGE", 10); //ユーザ管理の一覧件数
define("CHAT_MAX_PAGE", 10); //チャットルームの一覧件数 |
を「config/const.php」に定義して…
(ついでに「APPLICATION_NAME」「HEAD_MAIN」「HEAD_SUB」「VERSION」も少し書き換えた)
そのあと、「src/Controller」の中にある「UsersController.php」と「ChatRoomsController.php」を修正するよ
$users = $this->paginate($query, ['limit' => USER_MAX_PAGE, 'order' => ['id' => 'asc']]); |
上が「UsersController.php」で、下が「ChatRoomsController.php」ね
(どちらも「index」メソッド内)
$chatRooms = $this->paginate($query, ['limit' => CHAT_MAX_PAGE, 'order' => ['created' => 'desc']]); |
リストの並び順(order)は適当に設定したけど、これで良いかな?
(【CakePHP5応用編④】を参照)





C菜
良いと思います~
それでは次は「レスポンシブ」対応ですけど~
「bbsapp(画像投稿掲示板)」の「templates/layout」の中にある「default.php」にあるCSSの記述を、「tr」を削除してから「authapp」のほうへコピーしますね~
(具体的には「templates/layout」の中にある「default.php」のheadタグ内です~)


A子
1.ログインページ(「templates/Users」の中にある「login.php」)
2.一人目のユーザを作成するためのページ(「templates/Users」の中にある「first_user.php」) 3.ユーザ管理の一覧ページ(「templates/Users」の中にある「index.php」) 4.チャットルームの一覧ページ(「templates/ChatRooms」の中にある「index.php」) |
…ってところかな?
(そのほかのページについては、多分大丈夫っぽい)

C菜
レスポンシブ対応自体はメディアクエリを使えば簡単ですけど~
(【コラム⑥】を参照)
「'required' => true」は削除しないとPOST送信ができないですね~

A子
なんで?

C菜
隠したほうは未入力のままになっちゃいますから、必須入力(required)にはできないってわけです~
(ちなみに「'id' => 'email'」や「'id' => 'password'」についても削除しました…使ってないし、値が重複するので…)



A子
ん?
ログインできないよ?
いや、正確に言えば「narrow」ではログインできるけど、「wide」ではできない…???

B美
POSTされる値は(自動的に)配列になっちゃうわよ

C菜
うーん、なんとかController側を修正せずにうまいことできませんかね~?

B美
(もちろん、一時的に…だけど)
document.getElementById('login_form').addEventListener('submit', function() {
const emailInputs = document.getElementsByName('email'); const passwordInputs = document.getElementsByName('password'); for (let input of emailInputs) { if (input.offsetParent === null) { input.removeAttribute('name'); } } for (let input of passwordInputs) { if (input.offsetParent === null) { input.removeAttribute('name'); } } }); |
…って感じ

C菜


A子
「wide」と「narrow」、どっちでもログインできたよ
(下の二つの画面は、最初が「wide」で次が「narrow」ね)



C菜
ただし、JavaScriptの部分については、以下のようになりますね~
document.getElementById('login_form').addEventListener('submit', function() {
const emailInputs = document.getElementsByName('email'); const passwordInputs = document.getElementsByName('password'); const handleNameInputs = document.getElementsByName('handle_name'); for (let input of emailInputs) { if (input.offsetParent === null) { input.removeAttribute('name'); } } for (let input of passwordInputs) { if (input.offsetParent === null) { input.removeAttribute('name'); } } for (let input of handleNameInputs) { if (input.offsetParent === null) { input.removeAttribute('name'); } } }); |




A子
ちょっと面倒だけど、以下の手順でデータベースを新しくするよ
(まじで面倒だけど…(苦笑))
mysql -u root -p chatdb[Enter]
drop database chatdb;[Enter] quit[Enter] mysql -u root -p < chatdb.sql[Enter] |


C菜
cd html/authapp[Enter]
bin/cake cache clear_all[Enter] |


A子
このテストを「wide」と「narrow」の2パターンやるために、データベースの削除と新規作成(及びキャッシュクリア)をもう一度(通算2回)やらないといけないんだよね(苦笑)
面倒くせぇ…
(下記は「narrow」画面)


C菜

A子




C菜



A子




C菜



B美
まぁ、それは置いといて、チャットのテストをやっておきましょう
(三人で、違う部屋または同じ部屋に入室したり、退室したり…ってテストね)
まずは私が「雑談3」という部屋に入るわよ


A子
おぉ、上に入室者リストが表示されてるじゃん


B美

A子


C菜
入室メッセージが同じ部屋にいる他の方々に送られて、上にある入室者リストも更新されてます~
あと、過去ログが私の画面にも出てますし、私の発言もお二人へと届いてますよ~

↓


A子

↓


B美
んじゃ、私も退室してみるわ

↓


C菜

A子

B美


C菜


B美
それじゃ、この部屋(雑談1)を出て、C菜のいる部屋(雑談3)に入り直してみましょう

↓

↓


C菜


A子

↓

↓


C菜
ほんとに完璧な動作だと思います~

B美
(まじでホッとした(苦笑))