CakePHP3・4のネットでは見つからないちょっとした実装集

CakePHPでネットでは見つからないちょっとした実装集

144 回閲覧されました

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

CakePHPに関して検索しても見つけることができなくて苦労して実装した実装集を掲載します。

情報が見つかり次第追記します。

アソシエーション時に条件を満たしたら削除ボタンを表示

アイドル会社1とアイドル会社2があってそれぞれの会社に部署1と部署2があり部署1と部署2にはそれぞれ従業員が3人いたとします。

会社のカラムは下記になります。

部署のカラムは下記になります。

従業員のカラムは下記になります。

会社はcompaniesテーブル、部署はdepartmentsテーブル、従業員はusersテーブルに情報が保存されているとします。

アソシエーションの関係は以下になります。

  • 従業員(usersテーブル) :companiesテーブルとdepartmentsテーブルに対して子テーブル
  • 部署(departmentsテーブル) : companiesテーブルに対して子テーブル
  • 会社(companiesテーブル) : departmentsテーブルとusersテーブルに対して親テーブル

アソシエーションの設定はしたとします。

viewアクション

「http://localhost/companies/view/1」にアクセスした時に下記の表示にするのを目標にします。

会社の中で部署に紐づいているユーザーがいますが部署に紐づくユーザーがいなくなったら削除ボタンが表示されるようにします。

Companiesコントローラーのviewアクションのコードは下記になります。

public function view($id = null)
{
  $company = $this->Companies->get($id, [
    'contain' => ['Users'],
  ]);

  $companyId = $this->Auth->user('company_id');

  $departments = $this->Companies->Departments->find()
                                              ->where(['company_id' => $companyId])
                                              ->contain(['Users'])
                                              ->all();

  $this->set(compact('company', 'departments'));
}

今回の実装に関係あるのは7行目〜12行目です。

会社に紐づく部署を考えるので9行目の「->Companies->Departments」があります。

実装に使う会社はユーザーがログインしている会社にしないといけないので会社idを7行目で取得して10行目で使っています。

これだけだとユーザーが考慮されていないのでユーザーも含めます。

ユーザーがログインしている会社の中のユーザーを使うので11行目があります。

companiesテーブルに紐づくdepartmentsテーブルに紐づくユーザーが取得できているかを確認します。

viewアクションの中に下記の記述をします。

public function view($id = null)
{
  $company = $this->Companies->get($id, [
    'contain' => ['Users'],
  ]);

  $companyId = $this->Auth->user('company_id');

  $departments = $this->Companies->Departments->find()
                                              ->where(['company_id' => $companyId])
                                              ->contain(['Users'])
                                              ->all();

  dd($departments);        //←この行を追加
  
  $this->set(compact('company', 'departments'));
}

すると会社の部署に紐づいているユーザーがいる場合は下記の表示になります。

会社の部署に紐づいているユーザーがいない場合は下記の表示になります。

viewアクションで表示するビューのコードを下記にします。

<div class="related">
  <h4><?= __('部署') ?></h4>
  <div class="table-responsive">
    <table>
       <tr>
         <th><?= __('Name') ?></th>
         <th class="actions"><?= __('Actions') ?></th>
       </tr>
       <?php foreach ($departments as $department) : ?>
         <tr>
           <td><?= h($department->name) ?></td>
           <td class="actions">
             <?= $this->Html->link(__('View'), ['controller' => 'Departments', 'action' => 'view', $department->id]) ?>
             <?= $this->Html->link(__('Edit'), ['controller' => 'Departments', 'action' => 'edit', $department->id]) ?>
             <?php if(count($department->users) == 0): ?>
               <?= $this->Form->postLink(__('Delete'), ['controller' => 'Departments', 'action' => 'delete', $department->id], ['confirm' => __('Are you sure you want to delete # {0}?', $department->id)]) ?>
             <?php endif; ?>
           </td>
         </tr>
       <?php endforeach; ?>
     </table>
   </div>
</div>

部署に紐づく従業員が0人の時のみ削除ボタンを表示させているのが15行目です。

viewアクションで「dd($departments);」をして部署に紐づくユーザーを取得するのは「$department->users」です。(usersは下記参照)

これで実装が完成です。

1つのカラムを登録すると他のカラムも自動的に登録される

例えばusersテーブルにnameカラムとemailカラムとpasswordカラムがあったとします。

postsテーブルに紐づくビューでusersテーブルのnameカラムのデータをリストで表示させて名前を選択して登録すると自動的に名前に紐づいているemailカラム・passwordカラムもpostsテーブルに登録させたいみたいな時に使えます。

簡潔にすると「レコードの1つのカラムを登録すると他のカラムが自動的に登録される」と言えます。

今回はnameカラムを登録した時にemailカラムも登録できるようにします。

allコマンドでモデル・ビュー・コントローラーは作成したのを前提に話を進めます。

ユーザーの登録(usersテーブル)はコントローラー・ビュー共にデフォルトのまま使っているのでコードの掲載はしません。

テーブル

usersテーブルは下記です、nameカラム・emailカラム・passwordカラムがありますがnameカラムとemailカラムを使います。

postsテーブルは下記です、titleカラム・contentカラムがあります。

ビュー

コードを下記にします。

<div class="row">
    <aside class="column">
        <div class="side-nav">
            <h4 class="heading"><?= __('Actions') ?></h4>
            <?= $this->Html->link(__('List Posts'), ['action' => 'index'], ['class' => 'side-nav-item']) ?>
        </div>
    </aside>
    <div class="column-responsive column-80">
        <div class="posts form content">
            <?= $this->Form->create() ?>
            <fieldset>
                <legend><?= __('Add Post') ?></legend>
                <?= $this->Form->select('title', ['options' => $users]) ?>
            </fieldset>
            <?= $this->Form->button(__('Submit')) ?>
            <?= $this->Form->end() ?>
        </div>
    </div>
</div>

13行目が登録フォームです、「’options’ => $users」の$usersはコントローラーから情報を渡します。

コントローラー

コードを下記にします。

public function add()
{
  $this->loadModel('Users');

  $users = $this->Users
                ->find('list', ['keyField' => 'name', 'valueField' => 'name'])
                ->toArray();
    
  $post = $this->Posts->newEmptyEntity();
  
  if ($this->request->is('post')) {
    $postData = $this->request->getData();
    $user = $this->Users->find()->where(['name' => $postData['title']])->first();
            
    if ($user) {
      $postData['content'] = $user->email;
      $post = $this->Posts->patchEntity($post, $postData);
      if ($this->Posts->save($post)) {
        $this->Flash->success(__('The post has been saved.'));
        return $this->redirect(['action' => 'index']);
      } else {
        $this->Flash->error(__('The post could not be saved. Please, try again.'));
      }  
    }

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

5行目の「[‘keyField’ => ‘name‘, ‘valueField’ => ‘name‘]」は「KeyField」が連想配列のkeyに当たりValueFieldが連想配列のvalueに当たります。

keyとvalueに「name」を使っていますがusersテーブルのnameカラムになります、存在しないカラム名を使うとエラーになります。

valueFieldのnameが登録フォームに表示される名前でkeyFieldのnameが登録フォームで名前を選択して登録した時にテーブルに保存される値です。

本当に値が入っているかを確認します。

dd関数を使います。

public function add()
{
  $this->loadModel('Users');

  $users = $this->Users
                ->find('list', ['keyField' => 'name', 'valueField' => 'name'])
                ->toArray();
  
  dd($users);     //←この行を追加


登録画面をリロードすると下記の表示になります。

//左の項目がkeyFieldで右の項目がvalueField
[
'jonio' => 'jonio',
'jonio2' => 'jonio2',
]

右の項目が登録フォームに表示される名前で左の項目が実際に登録される値です。

カラム名を2つともnameにして見にくいですが表示される名前と登録される名前が同じの方が対応がわかりやすいと思いわざと同じにしています。

コントローラーのコードに話を戻して登録フォームで名前を選択して登録した時にpostsテーブルのtitleカラムにusersテーブルのnameカラムの値が登録されているかを確認します。

dd関数を使います。

public function add()
{
  $this->loadModel('Users');

  $users = $this->Users
                ->find('list', ['keyField' => 'name', 'valueField' => 'name'])
                ->toArray();
    
  $post = $this->Posts->newEmptyEntity();
  
  if ($this->request->is('post')) {
    $postData = $this->request->getData();
    dd($postData);               //←を追加

登録フォームで名前を選択して登録すると下記の表示になります。

//名前の選択でjonio2を選択しています

[
'title' => 'jonio2',
]

「dd($postData)」ではなく「dd($postData[‘title’])」にするとtitleカラムの値(jonio2)を取得できているのを確認できます。

話をまたコントローラーのコードに戻して今のままではcontentカラムに値を登録することができませんがそれをできるようにする媒体みたいな物が13行目です。

これがあることで登録フォームで選択した名前(今回の例だとjonio2)があるレコードを取得しています。

ちなみに「where([‘name’ => $postData[‘title’]])」で登録レコードを選択して「$postData[‘title’]」でjonio2が取得できます。

今からcontentカラムにemailを登録しますが13行目の$userに何が入っているかをdd関数で見てみます。

public function add()
{
  $this->loadModel('Users');

  $users = $this->Users
                ->find('list', ['keyField' => 'name', 'valueField' => 'name'])
                ->toArray();
    
  $post = $this->Posts->newEmptyEntity();
  
  if ($this->request->is('post')) {
    $postData = $this->request->getData();
    $user = $this->Users->find()->where(['name' => $postData['title']])->first();
    dd($user);             //←この行

するとemailカラムの値を含んだオブジェクトが取得できます。

object(App\Model\Entity\User) id:0 {
'id' => (int) 2
'name' => 'jonio2'
'email' => 'b@demo.com'
'password' => 'xxxxxxx'
'[new]' => false
'[accessible]' => [ ]
'[dirty]' => [ ]
'[original]' => [ ]
'[virtual]' => [ ]
'[hasErrors]' => false
'[errors]' => [ ]
'[invalid]' => [ ]
'[repository]' => 'Users'
}

よって16行目の「$user->email」でjonio2に紐づいたemailカラムの値を取得してpostsテーブルのcontentカラム($postData[‘content’])に登録します。

あとはコードを読めば分かると思います。

jQuery(JavaScript)にコントローラー側からの変数を含める

CakePHP3.1で実装しています。

コントローラーのアクションに下記の記述をしたとします、ajaxを使おうとしています。

$this->set('_serialize', ['names', 'departments']);

namesには名前が入っていてdepartmentsには部署が入っていたとします。

そしてjQueryに下記の記述をしたとします。(必要な部分のに記載しています)

$(document).ready(function() {

  function loadCards() {

    $.ajax({
                
      url: '/users/index/',

      data: { page: page },

      dataType: 'json'
    })
    .done(function(response) {
           
    response.departments.forEach(department => {
              
      let department_content = `
        <div class="department-wrap">${department.content}</div>
      `;

      if (response.names == 'jonio') {
        department_content += `
          <p>2024年</p>
        `;
      }



アクションのnamesをjQueryの21行目で使っています。

存在しないURLにアクセスすると特定のページにリダイレクト

CakePHP4.5.4で実装しました、3系では動かないかもしれません。

ErrorController.php

「CakePHPのプロジェクト > src > Controller > ErrorController.php」の「beforeRender」メソッドに記述します。

public function beforeRender(EventInterface $event)
{
  parent::beforeRender($event);

  if ($this->response->getStatusCode() == 404) {

    return $this->redirect('リダイレクトする時のコントローラーとアクションを指定');
  }
}

beforeRenderメソッドはビューを表示する前に処理をする為の記述をする時に使うみたいです。

だからリダイレクトの処理はこのメソッドに記述します。

リダイレクト時のフラッシュの表示

コードを下記にします。

<?php
declare(strict_types=1);

/**
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 * @link          https://cakephp.org CakePHP(tm) Project
 * @since         3.3.4
 * @license       https://opensource.org/licenses/mit-license.php MIT License
 */
namespace App\Controller;

use Cake\Event\EventInterface;

/**
 * Error Handling Controller
 *
 * Controller used by ExceptionRenderer to render error responses.
 */
class ErrorController extends AppController
{
    /**
     * Initialization hook method.
     *
     * @return void
     */
    public function initialize(): void
    {
        $this->loadComponent('RequestHandler');

        $this->loadComponent('Flash');    //この行を追加
    }
    



                  
    /**
     * beforeRender callback.
     *
     * @param \Cake\Event\EventInterface $event Event.
     * @return \Cake\Http\Response|null|void
     */
    public function beforeRender(EventInterface $event)
    {
        parent::beforeRender($event);

        if ($this->response->getStatusCode() == 404) {

            $this->Flash->error(__('アクセスしたURLは存在しません'));    //この行を追加

            return $this->redirect('/');
        }
    }
}

通常のコントローラーだと「$this->loadComponent(‘Flash’);」の記述があるAppControllerを継承しているのでいきなり56行目の記述ができますがErrorControllerはAppControllerを継承しても「$this->loadComponent(‘Flash’);」が適用されないみたいです。

だからinitializeメソッドに記述(37行目)しています。

これで404の時に特定のページにリダイレクトしてフラッシュメッセージも表示されます。

ログインなしでアクションを許可している時に動作しない

何らかのコントローラーで下記の記述をすればログインしなくてもアクションの動作ができるようになります。

use Cake\Event\EventInterface;




public function beforeFilter(EventInterface $event)
{
  parent::beforeFilter($event);
        
  $this->Authentication->addUnauthenticatedActions(['add']);
}

この場合はaddアクションが対象になります。

アクションを修正した時に起こる可能性がありますがアクションに下記などのログイン情報を記述していたとします。

$this->request->getAttribute('identity');

cookbookに記載がなくネットで情報を見つけることができなかったのですがアクションに対してログインを必要なくしてもアクションにログイン関連の記述をするとログインが必須になる仕様みたいです。