CakePHP3と4で1つのテーブルに2つのアソシエーションを作る方法

CakePHP3と4で1つのテーブルに2つのアソシエーションを作る方法

336 回閲覧されました

みなさんこんにちは、jonioです。

アソシエーションは通常1つのテーブル(例えばusersテーブル)のidカラムの値と他のテーブル(例えばcompanies)のuser_idを紐づけます。

しかしreviewsテーブルのレビューをする人とレビューをされる人をusersテーブルから持ってきて紐付けする場合は通常のアソシエーションではうまくいきません。

こんな時に今回のやり方ができると思います。

CakePHPのバージョン

CakePHPのバージョンは3.1.0と4.5.3で試しましたがどちらでもうまくいったので2つのバージョンの間ならうまくいくはずです。

今回すること

ユーザーを登録してレビューをするページ(http://localhost/reviews/add)に移動します。

そしてユーザーの選択をしてMessageを書くとレビュー一覧ページ(http://localhost/reviews/)に表示されます。

前提条件

親のusersテーブルと子のreviewsテーブルを作成します。

若干注意点があるのでテーブルごとに説明します。

usersテーブル

usersテーブルは下記になります、テーブルに入っているデータは私が実験で入れているので無視してください。

このテーブルはユーザー名をreviewsテーブルと紐づけますが注意点はありません。

reviewsテーブル

reviewsテーブルは下記になります、テーブルに入っているデータは私が実験で入れているので無視してください。

usersテーブルと紐づける外部キーは「customer_sender_id」と「shop_receiver_id」です、データ型は「integer」です。

「user_id」は通常のアソシエーションでは必須ですが1つのテーブルで2つの外部キーを使ったアソシエーションをする場合は必要ありません。

それではアソシエーションしていきます。

アソシエーションの設定

「CakePHPのプロジェクト > src > Model > Table」の下の階層にあるUsersTable.phpとReviewsTable.phpでアソシエーションをする為の設定をします。

UsersTable.php

下記の記述をします。

public function initialize(array $config): void
{
  parent::initialize($config);

  $this->setTable('users');
  $this->setDisplayField('name');
  $this->setPrimaryKey('id');

  $this->addBehavior('Timestamp');


  //ここから追加
  $this->hasMany('CustomerSender', [
    'className' => 'Reviews',
    'foreignKey' => 'customer_sender_id',
  ]);
  $this->hasMany('ShopReceiver', [
    'className' => 'Reviews',
    'foreignKey' => 'shop_receiver_id',
  ]);   
  //ここまで追加
  
         
}

13行目の「CustomerSender」・17行目の「ShopReceiver」は通常「Reviews」と記述します。

だけどモデルにCustomerSenderとShopReceiverはないですがreviewsテーブルのcustomer_sender_idカラム・receiver_receiver_idカラムを使う為の架空のモデルとして記述しています。

customer_sender_idカラム・receiver_receiver_idカラムはreviewsテーブルのカラムなのを示す為に14行目・18行目があります。

ReviewTable.php

下記の記述をします。

public function initialize(array $config): void
{
  parent::initialize($config);

  $this->setTable('reviews');
  $this->setDisplayField('id');
  $this->setPrimaryKey('id');

  $this->addBehavior('Timestamp');


  //ここから追加
  $this->belongsTo('Users', [
    'foreignKey' => 'customer_sender_id',
  ]);
  $this->belongsTo('CustomerSender', [
    'className' => 'Users',
    'foreignKey' => 'customer_sender_id',
  ]);
  $this->belongsTo('ShopReceiver', [
    'className' => 'Users',
    'foreignKey' => 'shop_receiver_id',
  ]);
  //ここまで追加
  
  
}

UserTable.phpで「CustomerSender」・「ShopReceiver」の設定をしているのでReviewTable.phpにも同じ設定をしないといけません、それが16行目〜23行目です。

reviewsテーブルはusersテーブルとアソシエーションをしないといけないので13行目〜15行目があります。

14行目の「customer_sender_id」は外部キーの「customer_sender_id」か「shop_receiver_id」のどちらかを書けば動作します。(ここの理由は分かりませんがとにかく動作します)

これでレビューを書くページで送る人と受け取る人に登録されているユーザーを使うことができるようになったのでレビューを書くページの作成をします。

addアクションとadd.php

ReviewsController.phpのaddアクションのコードを下記にします。

public function add()
{
  $this->loadModel('Users');
        
  $review = $this->Reviews->newEmptyEntity();

  if ($this->request->is('post')) {
    $review = $this->Reviews->patchEntity($review, $this->request->getData());

    if ($this->Reviews->save($review)) {
      $this->Flash->success(__('The review has been saved.'));

      return $this->redirect(['action' => 'index']);
    }else{
      $this->Flash->error(__('The review could not be saved. Please, try again.')); 
    }
  }

  $selectUser = $this->Users->find('list', ['limit' => 100]);

  $this->set(compact('review', 'selectUser'));
}

add.phpを下記にします。

<?php
/**
 * @var \App\View\AppView $this
 * @var \App\Model\Entity\Review $review
 */
?>
<div class="row">
    <aside class="column">
        <div class="side-nav">
            <h4 class="heading"><?= __('Actions') ?></h4>
            <?= $this->Html->link(__('List Reviews'), ['action' => 'index'], ['class' => 'side-nav-item']) ?>
        </div>
    </aside>
    <div class="column-responsive column-80">
        <div class="reviews form content">
            <?= $this->Form->create($review) ?>
            <fieldset>
                <legend><?= __('Add Review') ?></legend>
                <?php
                    echo $this->Form->control('customer_sender_id', ['options' => $selectUser]);
                    echo $this->Form->control('shop_receiver_id', ['options' => $selectUser]);
                    echo $this->Form->control('message');
                ?>
            </fieldset>
            <?= $this->Form->button(__('Submit')) ?>
            <?= $this->Form->end() ?>
        </div>
    </div>
</div>

2つのコードはほぼデフォルトなので今回の実装にあたって説明は必要ないと思います。

indexアクションとindex.php

ReviewsController.phpのindexアクションのコードを下記にします。

public function index()
{
  $reviews = $this->paginate($this->Reviews->find('all', ['contain' => ['Users', 'CustomerSender', 'ShopReceiver']]));

  $this->set(compact('reviews'));
}

ReviewTable.phpで設定したCustomerSenderとShopReceiverを3行目で登録しています。

これをしないとcustomer_sender_idとshop_receiver_idを使うことができないです。

index.phpを下記にします。

<?php
/**
 * @var \App\View\AppView $this
 * @var iterable<\App\Model\Entity\Review> $reviews
 */
?>
<div class="reviews index content">
    <?= $this->Html->link(__('New Review'), ['action' => 'add'], ['class' => 'button float-right']) ?>
    <h3><?= __('Reviews') ?></h3>
    <div class="table-responsive">
        <table>
            <thead>
                <tr>
                    <th><?= $this->Paginator->sort('id') ?></th>
                    <th><?= $this->Paginator->sort('customer_id') ?></th>
                    <th><?= $this->Paginator->sort('shop_id') ?></th>
                    <th><?= $this->Paginator->sort('message') ?></th>
                    <th><?= $this->Paginator->sort('created') ?></th>
                    <th><?= $this->Paginator->sort('modified') ?></th>
                    <th class="actions"><?= __('Actions') ?></th>
                </tr>
            </thead>
            <tbody>
                <?php foreach ($reviews as $review): ?>
                <tr>
                    <td><?= $this->Number->format($review->id) ?></td>
                    <td><?= $review->customer_sender->name ?></td>
                    <td><?= $review->shop_receiver->name ?></td>
                    <td><?= h($review->message) ?></td>
                    <td><?= h($review->created) ?></td>
                    <td><?= h($review->modified) ?></td>
                    <td class="actions">
                        <?= $this->Html->link(__('View'), ['action' => 'view', $review->id]) ?>
                        <?= $this->Html->link(__('Edit'), ['action' => 'edit', $review->id]) ?>
                        <?= $this->Form->postLink(__('Delete'), ['action' => 'delete', $review->id], ['confirm' => __('Are you sure you want to delete # {0}?', $review->id)]) ?>
                    </td>
                </tr>
                <?php endforeach; ?>
            </tbody>
        </table>
    </div>
    <div class="paginator">
        <ul class="pagination">
            <?= $this->Paginator->first('<< ' . __('first')) ?>
            <?= $this->Paginator->prev('< ' . __('previous')) ?>
            <?= $this->Paginator->numbers() ?>
            <?= $this->Paginator->next(__('next') . ' >') ?>
            <?= $this->Paginator->last(__('last') . ' >>') ?>
        </ul>
        <p><?= $this->Paginator->counter(__('Page {{page}} of {{pages}}, showing {{current}} record(s) out of {{count}} total')) ?></p>
    </div>
</div>

reviewsテーブルのcustomer_sender_idカラムとshop_receiver_idカラムをusersテーブルのnameカラムと紐づけたユーザー名を表示しているのが27行目と28行目です。

27行目・28行目はどちらも同じことをしているので27行目で説明します。

「<?= $review->customer_sender->name ?>」の「customer_sender」は通常のアソシエーションならuserですが外部キーを2つ(今回はcustomer_sender_idとshop_reviewer_id)使ってアソシエーションする場合はcustomer_sender_idの「_id」を取ればuserの身代わりって感じになりusersテーブルのカラムの値を呼び出せるようになります。

そしてusersテーブルのnameカラムを使いたいので「->name」をつけています。(他のカラムの値を呼び出すことももちろんできます、試しに「->name」ではなく「->id」とするとidカラムの値を呼び出すことができます)

これで完成です。