CakePHP5入門【CakePHP5実用編⑱】ユーザ間メッセージ送付

A子
電子メールではない「メッセージ送付」って感じで…
例えば、「×日の△時に○○のチャットルームでおしゃべりしましょう」なんてメッセージを送っておくの

C菜
「誰が」「誰に」「いつ」「何を」知らせたいかをデータベーステーブルに記録しておいて、宛先側の人は(ログイン時に)もしも「未読」のメッセージがあったら、その旨を画面上に表示するんですよ~

B美
C菜の案が一般的な模範解答でしょうね
特に難しいところも無いし、あなたたちで挑戦してみなさい

A子
まずはデータベーステーブルの設計だね
「誰が」「誰に」「いつ」「何を」知らせるかなので…
create table notifications (
id int auto_increment primary key, sent_user_id int, received_user_id int, message text, read_flag int, created datetime, modified datetime, foreign key(sent_user_id) references users(id), foreign key(received_user_id) references users(id) ) charset=utf8mb4; |
こんな感じでどうかな?
あ、「いつ」については、レコード作成日時である「created」の項目で良いと思う


C菜
(CakePHPの命名規則には反してますけど~)
あと、「message」はその名の通りメッセージで、「read_flag」は既読か未読かってことですね~

A子
(だって同じ参照元の外部キーが二つあるんだもん)

C菜
define("MESSAGE_NOT_READ", 1); //未読
define("MESSAGE_IS_READED", 2); //既読 define("MESSAGE_STATUS", ["-", "未読", "既読"]); |


A子
cd html/authapp[Enter]
bin/cake bake all notifications[Enter] |




C菜
とは言っても「$_accessible」の中の最後の二行を削除しただけですけど~

↓


A子
「belongsTo」の直後にある「SentUsers」や「ReceivedUsers」って、「Users」に変更すれば良いのかな?


B美
(名前が重複することになっちゃうからね)
「SentUsers」や「ReceivedUsers」って、「Users」のエイリアスってことにすれば良いのよ
(てか、このファイルを特に修正しなくても大丈夫だから…)

A子

B美
(「またの名は」…ってこと)
ただし、containする際には注意が必要だからね
$notifications = $this->Notifications->find()->contain(['SentUsers', 'ReceivedUsers'])->all(); |
のように、エイリアスとして定義したほうを指定しないとダメってわけ

C菜
以前「chat_rooms」テーブルの項目名の中で、「user_id」とすべきところを「admin_id」としたせいで、なぞの「Admins」という(おそらくは)エイリアスが作成されたわけですけど~
あれってcontain時に「Admins」を指定すれば良かったんじゃないですか~?
(【CakePHP5実用編④】を参照)

B美
でも、あれは(無駄な修正ではなく)必要な修正だったと思うわよ
(containするのは「Users」であるべきだから…)
ただ、今回の「notifications」テーブルには、同じ参照元(「users」テーブル)の外部キーが二つ(「sent_user_id」及び「received_user_id」)あるからね
エイリアスは必須ってわけ…

A子
まだまだ知らないことがたくさんあるなー(苦笑)
さて、気を取り直して「src/Controller」の中にある「NotificationsController.php」を修正していこう

C菜
define("MESSAGE_MAX_PAGE", 10); //メッセージの一覧件数 |
あと、メインページのメニュー項目を追加するです~
<?= $this->Html->link(__('他のユーザへのメッセージ'), ['controller' => 'Notifications', 'action' => 'index']) ?> |



A子
送付先のハンドル名を表示するためにcontain(結合)するのは「ReceivedUsers」で、送付元が自分自身であるという条件で良いのかな?
public function index()
{ $query = $this->Notifications->find() ->contain(['ReceivedUsers']) ->where(['sent_user_id' => $this->identity->id]); $notifications = $this->paginate($query, ['limit' => MESSAGE_MAX_PAGE, 'order' => ['read_flag' => 'asc', 'created' => 'desc']]); $this->set(compact('notifications')); } |
表示順(order)は「未読」のものが上にくるようにして、第2ソートキーとして「最新」のものが上にくるようにしてるよ


C菜
では、それに対応するファイルが「templates/Notifications」の中にある「index.php」ですけど、こんな感じで書き換えました~



A子
良いんじゃない?

B美
実行してみたらすぐに分かることなんだけど、それでは「メッセージ送付先」の項目をクリックしてもソートは実行されないわよ
(問題は、赤線の部分ね)

C菜
そうなんですか~?
だったらエイリアスではなく、「Users.handle_name」でしょうか~?

B美
実は「index.php」については、さっきのでOKなの
変更すべきは「NotificationsController.php」のほうなのよね
public function index()
{ $query = $this->Notifications->find() ->contain(['ReceivedUsers']) ->where(['sent_user_id' => $this->identity->id]); $notifications = $this->paginate($query, ['limit' => MESSAGE_MAX_PAGE, 'sortableFields' => [ 'id', 'ReceivedUsers.handle_name', 'read_flag', 'created', ], 'order' => ['read_flag' => 'asc', 'created' => 'desc']]); $this->set(compact('notifications')); } |
…って感じで、「sortableFields」を追加するだけよ
あ、エイリアス(ReceivedUsers)のほうを使うのがポイントだからね


A子
特に難しくはないけど、知らなかったらハマりそうだね(苦笑)

C菜
セレクトボックスに表示する内容(ユーザリスト)については、「NotificationsController.php」側で作る感じです~
(「$users_array」の箇所)


A子
public function add()
{ //ユーザの一覧を取得 $users = $this->Users->find() ->where(['id <>' => $this->identity->id]) ->order(['email' => 'ASC'])->all(); $users_array = [0 => '選択してください']; foreach ($users as $user) { $users_array += [$user->id => $user->handle_name]; } $notification = $this->Notifications->newEmptyEntity(); if ($this->request->is('post')) { $received_user_id = $this->request->getData('received_user_id'); if ($received_user_id == 0) { $this->Flash->error(__('送付先を選択してください。')); return $this->redirect(['action' => 'add']); } $notification = $this->Notifications->patchEntity($notification, $this->request->getData()); if ($this->Notifications->save($notification)) { $this->Flash->success(__('メッセージを作成しました。')); return $this->redirect(['action' => 'index']); } $this->Flash->error(__('メッセージの作成に失敗しました。')); } $this->set(compact('users_array', 'notification')); } |


C菜
ポイントは「メッセージ送付先」を「$notification->received_user->handle_name」としているところでしょうか~


A子
public function view($id = null)
{ $notification = $this->Notifications->get($id, contain: ['ReceivedUsers']); $this->set(compact('notification')); } |
あ、containするのは「ReceivedUsers」だけで…

B美
主キーである「id」をGETパラメータとして渡している場合、注意しないといけないんじゃない?
(はたしてそのままで良いのかしら?)

C菜
あっ、URLに別の数字を指定すると、自分以外が作ったメッセージを見れちゃうのでは~?

A子
public function view($id = null)
{ $notification = $this->Notifications->get($id, contain: ['ReceivedUsers']); if ($notification->sent_user_id != $this->identity->id) { $this->Flash->error(__('他人の作成したメッセージを閲覧することはできません。')); return $this->redirect(['controller' => 'Top', 'action' => 'index']); } $this->set(compact('notification')); } |
で、どうかな?


B美
前にも出た話だけど、POSTだったらチェックは要らないけど、GETではチェックが必要になる場合って結構あるからね
(【コラム⑭】を参照)

A子
public function delete($id = null)
{ $notification = $this->Notifications->get($id); if ($notification->sent_user_id != $this->identity->id) { $this->Flash->error(__('他人の作成したメッセージを削除することはできません。')); return $this->redirect(['controller' => 'Top', 'action' => 'index']); } if ($notification->read_flag == MESSAGE_IS_READED) { $this->Flash->error(__('既読メッセージを削除することはできません。')); return $this->redirect(['action' => 'index']); } $this->request->allowMethod(['post', 'delete']); $notification = $this->Notifications->get($id); if ($this->Notifications->delete($notification)) { $this->Flash->success(__('メッセージを削除しました。')); } else { $this->Flash->error(__('メッセージの削除に失敗しました。')); } return $this->redirect(['action' => 'index']); } |
チェックは2種類…
「自分が作ったやつかどうか」と、「未読かどうか」だね


B美
良いと思うわよ
まぁ、削除(delete)については「postLink」しているから、チェックは要らないんだけどね
(「念のため」という意味では決してムダじゃないけど…)
それじゃ、あとはメッセージ送付先(受け取る側)の処理ね

C菜
うーん
ということは、「src/Controller」の中にある「AppController.php」を使うべきでは~?

A子
んじゃ、修正すべきメソッドは「beforeFilter」かな?
$notification_count = $this->Notifications->find()
->where(['received_user_id' => $this->identity->id, 'read_flag' => MESSAGE_NOT_READ])->count(); $this->set(compact('notification_count')); |
「自分宛て」で、かつ「未読」のメッセージの数を取得すれば良いんじゃないかな?

B美
(だって「$this->identity」がnullだもの)

C菜
ということは~
if (!is_null($this->identity)) {
$notification_count = $this->Notifications->find() ->where(['received_user_id' => $this->identity->id, 'read_flag' => MESSAGE_NOT_READ])->count(); $this->set(compact('notification_count')); } |
とすれば、良いんじゃないでしょうか~?


A子
たしかに、そだね
protected $Notifications;
public function initialize(): void { ・・・ $this->Notifications = $this->fetchTable('Notifications'); } |
あ、当然だけどテーブルフェッチも「initialize」メソッドでやっておくよ


C菜
えっと~
あわせて「NotificationsController.php」の「list」メソッドへのリンクボタンを表示しますね~


A子
public function list()
{ $notifications = $this->Notifications->find() ->contain(['SentUsers']) ->where(['received_user_id' => $this->identity->id, 'read_flag' => MESSAGE_NOT_READ]) ->order(['created' => 'DESC'])->all(); $this->set(compact('notifications')); } |
containするのは(「ReceivedUsers」ではなく)「SentUsers」のほうね

B美
(着目すべきは「created」ね)

A子
つづりを間違ったかな?

C菜
ん~
・・・
あっ!
二つのテーブルを結合したということは、そのどちらにも「created」が存在するのでは~?

B美
正解よ
結合したテーブルに同名のフィールドが存在する場合、表名を明示しないとエラーになる(かもしれない)ってわけ

A子
public function list()
{ $notifications = $this->Notifications->find() ->contain(['SentUsers']) ->where(['received_user_id' => $this->identity->id, 'read_flag' => MESSAGE_NOT_READ]) ->order(['Notifications.created' => 'DESC'])->all(); $this->set(compact('notifications')); } |
…てことだね


C菜
ちょっと待ってください~
「index」メソッドのほうでもソートしてましたけど、(「Notifications.created」ではなく)「created」だけでうまく動いていましたよ~
(「ReceivedUsers」を結合してるのに~)

A子
もしかしてpaginateを使ったから?

B美
paginateを使う場合、主テーブルを「Notifications」であると(勝手に)判断してくれたおかげで(表名の指定を行わなくても)うまくいったのよ(多分…)

C菜
エラーを出してくれたほうがすっきりします~

B美
たとえ必要が無くても、明示的に「Notifications.created」と書くべきなのよね
(できれば「index」メソッドのほうも修正しておきなさい)

A子



C菜
(「index.php」をベースにして、それを書き換える感じで~)


A子
public function view2($id = null)
{ $notification = $this->Notifications->get($id, contain: ['SentUsers']); if ($notification->received_user_id != $this->identity->id) { $this->Flash->error(__('他人あてのメッセージの閲覧はできません。')); return $this->redirect(['controller' => 'Top', 'action' => 'index']); } $notification->read_flag = MESSAGE_IS_READED; $this->Notifications->save($notification); $this->set(compact('notification')); } |
宛先が自分かどうかのチェックと、「未読」を「既読」にする処理をやってるよ


C菜


B美
C菜あてのメッセージを作成してみるわね
(すでにA子あてのメッセージもあって、既読も付いてるけど気にしないように(苦笑)…ちょっとテストしただけだから)

↓


C菜
「このボタンをクリック」を押すと~

↓


C菜
んで、そのあと「一覧へ戻る」をクリックすると、リストから未読のメッセージが消えてますね~

↓


B美
(「未読」だったものが「既読」に変わっているわね)


A子
その画面上に「未読のメッセージがあります。」という表示があるのはおかしくない?

C菜
「list」メソッドと「view2」メソッドの実行時には、未読通知を表示しないようにしたいです~

A子

B美
そうねぇ
「templates/layout」の中にある「default.php」だけど…
$controller = $this->request->getParam('controller');
$action = $this->request->getParam('action'); |
を記述することによって「$controller」にはController名が、「$action」にはメソッド名が入るわ

A子


B美
(Controller名は「NotificationsController」ではなく「Notifications」であることに注意してね)
てか、A子もなかなかやるじゃない

C菜

B美
未読のメッセージを表示する「list」メソッドだけど、詳細表示(view2)から戻った際、レコードが空であっても表示されるのって少し違和感があるわ

A子

B美
if ($notifications->isEmpty()) {
} |
…って感じの使い方ね

A子
if ($notifications->isEmpty()) {
return $this->redirect(['controller' => 'Top', 'action' => 'index']); } |


C菜
「未読」と「既読」の文言を表示する際、色を変えたいですね~
(「未読」は赤色、「既読」は緑色に~)
なので、「config/const.php」の定数定義を変えてみました~
define("MESSAGE_STATUS", ["-", '<span style="color: red;">未読</span>', '<span style="color: green;">既読</span>']); |


B美
「if else」や「三項演算子」を使うよりも、シンプルで良いと思うわよ


A子

C菜
バッチリ成功しましたよ~

↓

↓

↓


B美
良いんじゃない?

A子
今回は…
・エイリアスについて
・sortableFieldsにソート項目を明示することで、エイリアスがからむ項目クリックによるソートを行う ・結合したテーブルに同名の項目がある場合、「表名.項目名」とするべき ・表示した画面のController名やメソッド名を取得する方法 ・検索結果のレコードが空かどうかの判別方法 |
…ってことかー

C菜