CakePHP4で、フォロー機能のアソシエーションを実装してみた(Ruby on Railsの記事を参考に)

CakePHP4にて、フォロー機能を実装しようとしたのですが、なかなかうまくいきませんでした。困っていたものの、 Ruby on Railsにて実装している記事を参考にやってみたらうまくいきましたので共有します。
参考にした記事はこちら。
順番に紹介していきます!

やりたいこと

・Membersモデルを作成し、Member同士のフォロー機能のアソシエーションを作成する
・Membersのfindで、該当Memberのフォロー中Members・フォロワーMembersを取得し、それぞれのMemberデータ(名前等カラム情報)も取得する(containで取得できるようにする)
今回作成したMemberのテーブル例
  
CREATE TABLE `members` (
  `id` int(8) NOT NULL,
  `email` varchar(255) NOT NULL,
  `name` varchar(255) NOT NULL,
  `created` datetime DEFAULT NULL,
  `modified` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
ALTER TABLE `members`
  ADD PRIMARY KEY (`id`);

INSERT INTO `members` (`id`, `email`, `name`, `created`, `modified`) VALUES
(1, 'test1@example.com', 'メンバー1', '2020-12-15 05:50:28', '2020-12-15 05:50:28'),
(2, 'test2@example.com', 'メンバー2', '2020-12-15 05:50:28', '2020-12-15 05:50:28'),
(3, 'test3@example.com', 'メンバー3', '2020-12-15 05:50:28', '2020-12-15 05:50:28'),
(4, 'test4@example.com', 'メンバー4', '2021-01-13 11:22:26', '2021-01-13 11:22:26');
  

今回の例ですと、

「メンバー1」が「メンバー2」「メンバー3」をフォローして、
「メンバー3」「メンバー4」が「メンバー1」をフォローしている場合

「メンバー1」のフォロー一覧取得で「メンバー2」「メンバー3」のメンバー情報、
「メンバー1」のフォロワーー一覧取得で「メンバー3」「メンバー4」のメンバー情報 を取得したいのです。

CakePHP4にて、フォロー機能を実装する手順①中間テーブルを作成する

フォロー機能のアソシエーションは、多対多(members対members)のアソシエーションとなっています。
今回は、中間テーブル「follows」を用意して「一対多」(member対follows)のアソシエーションを作成します。

中間テーブルを作成し、bakeにてモデルを作成します。

  
CREATE TABLE `follows` (
  `id` int(8) NOT NULL,
  --フォローする側
  `following_id` int(8) NOT NULL,
  --フォローされる側   
  `follower_id` int(8) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `follows` (`id`, `following_id`, `follower_id`) VALUES
(1, 1, 2),
(2, 1, 3),
(3, 3, 1),
(3, 4, 1);
  

CakePHP4にて、フォロー機能を実装する手順②モデルを2つに分けて考える

①フォローする側の目線
②フォローされる側の目線
に分けて考えていきます。

順序として、下記のように実装していきます。
1)Followsモデルを2つに分けて考える
2)Membersモデルを2つに分けて考える

1)Followモデルを2つに分けて考える

・ActiveFollowings (フォローする側の目線)
・PassiveFollowings (フォローされる側の目線)

Memberモデルに、hasMany()メソッドを追加します。

/src/Model/Table/MembersTable.php
  
        $this->hasMany('ActiveFollowings', [
            'className' => 'Follows',
            'foreignKey' => 'following_id',
        ]);

        $this->hasMany('PassiveFollowings', [
            'className' => 'Follows',
            'foreignKey' => 'follower_id',
        ]);
  

1つのFollowモデルを仮想の2つのモデルに分けています。
なので、実際のFollowsモデルであることをclassNameプロパティにて定義します。

フォローをする側の目線では、フォローする側(following_id)を元にフォローされる側(follower_id)を引っ張ってくるので、foreignKey(外部キー)をfollowing_idに指定する必要があります。
逆に、フォローされる側の目線では、フォローされる側(follower_id)を元にフォローする側(following_id)を引っ張ってくるので、foreignKey(外部キー)をfollower_idに指定する必要があります。

2)Memberモデルを2つに分けて考える
・ Followings (フォローする側の目線)
・ Followers (フォローされる側の目線)
/src/Model/Table/FollowsTable.php
  
        $this->belongsTo('Followings', [
            'className' => 'Members',
            'foreignKey' => 'follower_id',
        ]);

        $this->belongsTo('Followers', [
            'className' => 'Members',
            'foreignKey' => 'following_id',
        ]);
  
Followモデルと同様にあくまでも、1つのMemberモデルをそれぞれ名前を付けて2つのモデルに分けたという仮想モデルであるため、本当はMembersモデルであることをclassNameで記載する必要があります。

CakePHP4にて、フォロー機能を実装する手順③フォローする側の目線で考える

1行目:Followsモデル(ここではフォローする側目線なのでActiveFollowings)について記述
上記の「CakePHP4にて、フォロー機能を実装する手順②モデルを2つに分けて考える」ところで説明済み。
2行目:ActiveFollowingsを介してフォローされた人を集める
ActiveFollowingsを通ってフォローされた人を集める。フォローされた人を集めるには、"Followers"モデルを参照することになるため、propertyName:Followersを記述する。
この一連の流れを"Followings"と命名したのでhasMany('Followings')と記述する。
/src/Model/Table/MembersTable.php
  
        $this->hasMany('ActiveFollowings', [
            'className' => 'Follows',
            'foreignKey' => 'following_id',
        ]);
        $this->hasMany('Followings', [
            'through' => 'ActiveFollowings',
            'propertyName' => 'Followers',
        ]);
  

CakePHP4にて、フォロー機能を実装する手順④フォローされる側の目線で考える

1行目:まずはFollowsモデル(ここではPassiveFollowings)について記述
上記の「CakePHP4にて、フォロー機能を実装する手順②モデルを2つに分けて考える」ところで説明済み。
2行目:PassiveFollowingsを介してフォローした人を集める
PassiveFollowingsを通ってフォローされた人を集める。フォローされた人を集めるには、"Followings"モデルを参照することになるため、propertyName: :Followingsを記述する。この一連の流れを"Followers"と命名したのでhasMany('Followers')と記述する。
/src/Model/Table/MembersTable.php
  
        $this->hasMany('PassiveFollowings', [
            'className' => 'Follows',
            'foreignKey' => 'follower_id',
        ]);

        $this->hasMany('Followers', [
            'through' => 'PassiveFollowings',
            'propertyName' => 'Followings',
        ]);
  
最終的に、Membersモデル・Followsモデルに追記する記述は以下のようになります。
/src/Model/Table/FollowsTable.php
  
        $this->belongsTo('Followings', [
            'className' => 'Members',
            'foreignKey' => 'follower_id',
        ]);

        $this->belongsTo('Followers', [
            'className' => 'Members',
            'foreignKey' => 'following_id',
        ]);
  
/src/Model/Table/MembersTable.php
  
        $this->hasMany('ActiveFollowings', [
            'className' => 'Follows',
            'foreignKey' => 'following_id',
        ]);
        $this->hasMany('Followings', [
            'through' => 'ActiveFollowings',
            'propertyName' => 'Followers',
        ]);
        $this->hasMany('PassiveFollowings', [
            'className' => 'Follows',
            'foreignKey' => 'follower_id',
        ]);
        $this->hasMany('Followers', [
            'through' => 'PassiveFollowings',
            'propertyName' => 'Followings',
        ]);
  
これにて、アソシエーションの実装は完了。

CakePHP4にて、フォロー機能を実装する手順⑤実際にフォローしているMember情報を取得

コントローラの中で、id=1のMemberがフォローしているMember(フォロー)の情報を取得します。
仮想のActiveFollowingsモデル・Followingsモデルをcontainする形です。
  
        $member = $this->Members->find('all', [
            'contain' => ['ActiveFollowings.Followings'],
            'conditions' => [
                    'Members.id' => 1
                ]
        ])->first();
  
取得結果はこんな感じ。見やすいように、上記コードのあと、コントローラの中でjsonを返すようにしています。
プロパティ「active_followings」内に、フォローしているMember一覧が、
中のプロパティ「following」内に、Memberの情報が取得されているのがわかります。

CakePHP4にて、フォロー機能を実装する手順⑥実際にフォローされているMember情報を取得

コントローラの中で、id=1のMemberがフォローされているMember(フォロワー)の情報を取得します。
仮想のPassiveFollowingsモデル・Followersモデルをcontainする形です。
  
        $member = $this->Members->find('all', [
            'contain' => ['PassiveFollowings.Followers'],
            'conditions' => [
                    'Members.id' => 1
            ],
        ])->first();
  
取得結果はこんな感じ。見やすいように、上記コードのあと、コントローラの中でjsonを返すようにしています。
プロパティ「passive_followings」内に、フォローされているMember一覧が、
中のプロパティ「follower」内に、Memberの情報が取得されているのがわかります。

まとめ

CakePHPでフォロー機能を実装するノウハウって意外とネットにないので、理解と実装に結構な時間を要しました。
こちらの記事に改めて感謝です。
記事でも書いているとおり、
「1つのモデルを、仮想の2つのモデルに分けて考える」ことに慣れれば、
大体わかってくると思います。
苦しんで理解したことを書いているため、この記事もだいぶわかりづらくなっているかもしれません(すいません・・・)。まだ私も理解を進めている最中です。
苦しんで理解するで身につくこともあるかと思いますので、みなさんも苦しんで理解してください(笑)
この記事に誤りや語弊があれば、フィードバックをお願いいたします!

Twitter