Friction River Software

  • お問い合わせ

CakePHP5入門【CakePHP5実用編⑫】入室制限

A子

現時点では、誰でもチャットルームに入室できて、かつ発言も可能なわけだけどさ

限られた人だけが入室できるように、改良したいんだよね

C菜

それは~

1.原則として誰でも入室できるけど、指定した人については例外的に入室できないようにする
2.原則としてチャットルーム作成者以外を入室できないようにしておいて、指定した人だけは例外的に入室できるようにする

のどちらでしょうか~?

A子

あー、私のイメージ的には2番だったんだけど、1番もありかもね

B美

1番を「原則許可(Allオール Allowアラウ」、2番を「原則拒否(Allオール Denyディナイ」と呼ぶことにして、どちらも実装すれば良いんじゃない?

チャットルームの作成時や編集の際に、どちらにするかを選択できるようにして…

C菜

それ良いと思います~

「chat_rooms」テーブルに「原則許可」と「原則拒否」の種別を格納する項目を追加しましょう~

A子

んじゃ、「chat_rooms」テーブルに追加するのは「access_policy」という項目にしよう

create table chat_rooms (
    id int auto_increment primary key,
    theme varchar(255) not null,
    admin_id int not null,
    delete_flag int not null,
    access_policy int,
    created datetime,
    modified datetime,
    foreign key(admin_id) references users(id)
) charset=utf8mb4;

C菜

そこに格納する値については、以下のように定数化しましょう~

define("ALL_ALLOW", 1);    //原則許可
define("ALL_DENY", 2);    //原則拒否

で、どうでしょうか~?

B美

ふむ、良いんじゃない?

それじゃ、次は「例外」を格納するテーブルね

A子

うーんと「exceptions」テーブルって名前にしようか

create table exceptions (
    id int auto_increment primary key,
    chat_room_id int not null,
    user_id int not null,
    access_policy int,
    created datetime,
    modified datetime,
    foreign key(chat_room_id) references chat_rooms(id),
    foreign key(user_id) references users(id)
) charset=utf8mb4;

今回は、きちんとCakePHPの命名規則に沿った名付けにしてるよ

具体的には、外部キーである「chat_room_id」と「user_id」って、「参照先のテーブル名の単数形+'_id'」にしてるからね
(【CakePHP5実用編④】を参照)

C菜

あっ、しかもこれって「クロステーブル」になっているような~?

チャットルームとユーザの関係(カーディナリティ)って、「多対多」になりますもんね~
(【コラム⑩】を参照)

B美

二人ともこれまでの経験(と知識)が生かされているようで、ほんと頼もしいわね

それじゃ、さっそくデータベースの修正からやってみなさい

A子

うん
まずは「chat_rooms」テーブルの修正からだね

mysql -u root -p chatdb[Enter]

ALTER TABLE chat_rooms
    ADD access_policy int AFTER delete_flag;[Enter]

C菜

「exceptions」テーブルについても、さきほどの「CREATE TABLE」文をコピペして実行しますね~

A子

んじゃ、「exceptions」テーブルを「bake model」したあと、キャッシュクリアについても実行しておこう

cd html/authapp[Enter]
bin/cake bake model exceptions[Enter]
bin/cake cache clear_all[Enter]


B美

あ、一点だけ口を出させてもらうわよ

「src/Model/Entity」の中にある「ChatRoom.php」だけど、$_accessibleの中に以下の一文を追加しておきなさいね

'access_policy' => true,

C菜

なぜでしょうか~?

B美

「src/Controller」の中にある「ChatRoomsController.php」の「add」メソッドや「edit」メソッドを特に変更しなくてすむように…かな

この記述が無いと、HTMLフォームの中に「アクセスポリシーのラジオボタン」を追加しても、それが無視されちゃうからね
(「ChatRoomsController.php」を修正しても良いんだけど、面倒くさい(苦笑))

A子

よし!
そんじゃ、まずは「config」の中にある「const.php」に定数を追加することからだね

define("ALL_ALLOW", 1);    //原則許可
define("ALL_DENY", 2);    //原則拒否
define("POLICY_NAME", ["-", "原則許可", "原則拒否"]);

C菜

それをふまえて「templates/ChatRooms」の中にある「add.php」「edit.php」「view.php」を修正してみました~

あ、「新規作成」時のデフォルト値については、「原則許可」にしてますよ~



A子

うん、良い感じだね

テストしてみたけど、全く問題なかったよ

B美

問題は、クロステーブルである「exceptions」テーブルの処理なのよね

できるだけ自動化したい(レコード操作をCakePHPに任せたい)から、Modelモデル側のファイルを修正するのが王道のやり方なんだけど、私としてはあまりそれに頼りたくない
(というか、お勧めしない)

C菜

なぜですか~?

B美

うまく動かなかったり、エラーが出たときにその原因がよく分からないからよ

belongsToMany」を使いこなせる人も割といるみたいだけどね

A子

びろんぐすとぅめにぃ?

何それ?

B美

Modelモデルである「UsersTable.php」や「ChatRoomsTable.php」に記述することで、「多対多」の処理を自動化することができる魔法のキーワードよ(笑)

私には使いこなせないけど…(苦笑)

C菜

B美部長でダメなら、私たちには無理っぽいですね~

A子

ふむ

それじゃ、どうすんの?

B美

いや、普通に(今まで通り)実装するだけよ

まずは「templates/ChatRooms」の中にある「add.php」を修正してみましょう


C菜

ChatRoomsController.php」のほうでユーザの一覧を取得して、それを「$users」という形で使えるようにするんですよね~?

B美

そういうこと

自分自身を一覧の中に表示しないってことと、チェックボックスが配列になってるってことがポイントね
(お試しユーザにマークを付けるってこともやってるけど…)

A子

ChatRoomsController.php」については、私に任せてよ

まずはテーブルのフェッチからだね

private $Users;
private $Exceptions;

public function initialize(): void
{
    parent::initialize();

    $this->Users = $this->fetchTable('Users');
    $this->Exceptions = $this->fetchTable('Exceptions');
}

んで、「add」メソッドはこんな感じでどうかな?

public function add()
{
    $this->checkGuest();

    //ユーザの一覧を取得
    $users = $this->Users->find()->all();

    $chatRoom = $this->ChatRooms->newEmptyEntity();
    if ($this->request->is('post')) {
        $chatRoom = $this->ChatRooms->patchEntity($chatRoom, $this->request->getData());
        if ($this->ChatRooms->save($chatRoom)) {

            //例外ユーザの追加
            $exception_users = $this->request->getData('exception_users');
            if (!empty($exception_users)) {
                foreach ($exception_users as $user_id) {
                    $exception = $this->Exceptions->newEmptyEntity();
                    $exception->chat_room_id = $chatRoom->id;
                    $exception->user_id = $user_id;
                    $exception->access_policy = $chatRoom->access_policy;
                    $this->Exceptions->save($exception);
                }
            }


            $this->Flash->success(__('チャットルームを作成しました。'));

            return $this->redirect(['action' => 'index']);
        }
        $this->Flash->error(__('チャットルームの作成に失敗しました。'));
    }
    $this->set(compact('chatRoom', 'users'));
}

配列として渡されてきたチェックボックスの値(ユーザID)を「exceptions」テーブルに格納していくだけなんだけどね


C菜

「templates/ChatRooms」の中にある「edit.php」ですけど、さきほどの「add.php」を参考にして作ってみました~
(ほとんど流用ですけど~)

ポイントはチェックボックスのチェック状態をデータベースから反映させることですね~


A子

ん?
その中にある「$exceptions」って?

C菜

「ChatRoomsController.php」の「edit」メソッドの中で、こう取得するです~

$exceptions = $this->Exceptions->find()->where(['chat_room_id' => $id])->all()->extract('user_id')->toList();

ポイントは「user_id」の値だけをリスト(配列)化していることですね~

A子

ふむ、なるほどね
だったら「edit」メソッドはこんな感じになるかな?

public function edit($id = null)
{
    $this->checkGuest();

    $chatRoom = $this->ChatRooms->get($id, contain: []);

    //管理人でなければリダイレクト
    if ($chatRoom->admin_id != $this->identity->id) {
        return $this->redirect(['action' => 'index']);
    }

    //ユーザの一覧を取得
    $users = $this->Users->find()->all();

    //このチャットルームに紐づく例外ユーザを検索し、そのユーザIDのみを配列として取得する
    $exceptions = $this->Exceptions->find()->where(['chat_room_id' => $id])->all()->extract('user_id')->toList();

    if ($this->request->is(['patch', 'post', 'put'])) {
        $chatRoom = $this->ChatRooms->patchEntity($chatRoom, $this->request->getData());
        if ($this->ChatRooms->save($chatRoom)) {

            //例外ユーザの削除及び追加
            $this->Exceptions->deleteAll(['chat_room_id' => $id]);
            $exception_users = $this->request->getData('exception_users');
            if (!empty($exception_users)) {
                foreach ($exception_users as $user_id) {
                    $exception = $this->Exceptions->newEmptyEntity();
                    $exception->chat_room_id = $id;
                    $exception->user_id = $user_id;
                    $exception->access_policy = $chatRoom->access_policy;
                    $this->Exceptions->save($exception);
                }
            }


            $this->Flash->success(__('チャットルーム情報を更新しました。'));

            return $this->redirect(['action' => 'index']);
        }
        $this->Flash->error(__('チャットルーム情報の更新に失敗しました。'));
    }
    $this->set(compact('chatRoom', 'users', 'exceptions'));
}

ポイントとしては、このチャットルームの例外リストを全て削除してから、新たに挿入(新規作成)していることだね
(更新処理を行うより分かりやすいと思うんだけど…)


B美

良いわね
(てか、なかなかやるわね…口には出さないけど)

それじゃ、最後は「ChatController.php」ね
この中の「index」メソッドで、チャットルームに入室できるかどうかを検証してみなさい

A子

OK
えーっと、「exceptions」テーブルの中に、アクセスしてきたユーザの「ユーザID」があるかどうかを調べれば良いんだよね

で、「原則許可」のときにレコードが存在すればアクセス禁止にして、「原則拒否」のときに存在するならアクセスを許可すれば良いのか…

「initialize」メソッド内で「exceptions」テーブルをフェッチするのは当然として、「index」メソッド内にこういう記述を行えば良いと思う

$chat_room = $this->ChatRooms->find()->where(['id' => $room_id])->first();
if ($chat_room->admin_id != $this->identity->id) {
    //例外リストに記載があるか
    $ex_count = $this->Exceptions->find()->where(['chat_room_id' => $room_id, 'user_id' => $this->identity->id])->count();

    if ($chat_room->access_policy == ALL_ALLOW) {
        //原則許可
        if ($ex_count > 0) {
            //ログインユーザが例外リストに入っている場合、アクセス拒否
            $this->Flash->error(__('このチャットルームに入室する権利がありません。'));

            return $this->redirect(['controller' => 'ChatRooms', 'action' => 'index']);
        }
    } else if ($chat_room->access_policy == ALL_DENY) {
        //原則拒否
        if ($ex_count == 0) {
            //ログインユーザが例外リストに入っていない場合、アクセス拒否
            $this->Flash->error(__('このチャットルームに入室する権利がありません。'));

            return $this->redirect(['controller' => 'ChatRooms', 'action' => 'index']);
        }
    }
}

えっと、2行目のif文で「作成者」は無条件で入室できるようにしてるよ
あと、4行目で「exceptions」テーブル内に、入室しようとするユーザのレコードがあるかどうかを調べてるから…


B美

うーん、特に問題は見当たらないわね

C菜

あっ、そうだ~
ついでと言っては何ですが、「templates/ChatRooms」の中にある「index.php」と「view.php」内の表記を少しだけ変更しましょう~

具体的には「管理人」という文言を「作成者」に変更するです~
(ユーザ権限としての「管理者」と、チャットルーム作成者としての「管理人」がごっちゃになりそうなので~)


A子

うん、良いと思うよ
さて、それじゃテストしてみますかー

まずは「チャットルーム作成」だね
・・・
うん、自分以外が例外ユーザとしてきちんと列挙されてるじゃん
(お試しユーザの右端にも「*」が付いてる)



A子

「誰でも入ることができる部屋」って名前で、このまま作ってみよう
「原則許可」で例外なしってことね)

C菜

「作成者以外入ることができない部屋」も作りましょう~
「原則拒否」で例外なしってことです~)



A子

それじゃ、お試しユーザを拒否する部屋を2パターン作ってみよう
(「原則許可」と「原則拒否」の二つね)





B美

ふむ

私のアカウントで入室を試みてみましょう
・・・
うん、7番の部屋以外は入れたわね

C菜

私のアカウントでも同様でした~

B美

それじゃ、お試しユーザでログインしてっと…

OK、OK
6番の部屋以外は入室拒否されたわね
(所定の性能を発揮した…ってこと)

A子

次は「編集」だね

7番の部屋について、B美だけは許可してあげるよ

B美

何を偉そうに…(苦笑)

うん、大丈夫みたい
入室できたわよ

C菜

私はダメでした~

A子

このあと他の部屋についても数回「編集」を試してみたけど、全然問題ないみたいだね

完璧じゃん!

B美

あ、一点だけ注意しておくけど…

さっき「お試しユーザを拒否」って部屋を作ったわよね
あれって、「例外ユーザ」として個別にチェックを入れただけで、「お試しユーザ」全員を一斉に拒否する機能なんて無いからね
(勘違いしないように!)

A子

あー
その機能って追加したほうが良いかな?

C菜

要らないと思いますよ~
(個別に指定できるんですから~)

B美

ふむ
私も要らないと思うけどね

まぁ、この記事を読んでいる皆さんが(必要ならば)独自に拡張すれば良いんじゃない?

A子

出たよ
メタ発言(笑)