CakePHP3.8で登録画面→確認画面→完了画面を実装する

# 今回のお題

---

表題の通りです。

久しぶりにCakePHP3を使ってコーディングをする機会がありました。
既にCakePHP4も出ていますが、CakePHP3.8からはそんなに変化がないとのことなので
Cake4にもすんなり入れると良いなあ、などと勝手に考えており。。

需要があるかはさておき、備忘録として残しておきます。

---

## 環境
PHP: 5.6.40
CakePHP: 3.8
OS: CentOS7.2
DB: MySQL5.7.21

全体的にレガシーな環境。。
(なのでCakePHP3を採用したという経緯)

---

## テーブル構成

---

ありきたりで恐縮ですが、ユーザーテーブルへの登録です。
年齢は実年齢ではなく、10代・20代のようなレンジでの管理となります。

```sql
CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL,
  `age` tinyint(4) NOT NULL,
  `gender` enum('male','female') NOT NULL,
  `email` varchar(255) NOT NULL,
  `password` varchar(255) NOT NULL,
  `last_login_at` datetime DEFAULT NULL,
  `is_deleted` tinyint(4) NOT NULL DEFAULT '0',
  `created` datetime NOT NULL,
  `modified` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

---

## CakePHP3実装

---

### Model

---

#### Table
validationのコードも載せています。

```src/Model/Table/UsersTable.php

use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;

class UsersTable extends Table
{
    public function initialize(array $config)
    {
        parent::initialize($config);

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

        $this->addBehavior('Timestamp');
    }

    /**
     * Default validation rules.
     *
     * @param \Cake\Validation\Validator $validator Validator instance.
     * @return \Cake\Validation\Validator
     */
    public function validationDefault(Validator $validator)
    {
        $validator
            ->integer('id')
            ->allowEmptyString('id', null, 'create');

        $validator
            ->scalar('name')
            ->maxLength('name', 10, '名前は10文字以内で設定してください。')
            ->requirePresence('name', 'create')
            ->notEmptyString('name');

        $validator
            ->add('name', 'unique', [
                'rule' => 'validateUnique',
                'provider' => 'table',
                'message' => '名前がすでに登録されています。'
            ]);

        $validator
            ->scalar('age')
            ->requirePresence('age', true)
            ->notEmptyString('age');

        $validator
            ->scalar('gender')
            ->requirePresence('gender', true)
            ->notEmptyString('gender');

        $validator
            ->email('email', false, 'メールアドレスが正しくありません。')
            ->requirePresence('email', true)
            ->notEmptyString('email', 'メールアドレスを入力してください。');

        $validator
            ->add('email', 'unique', [
                'rule' => 'validateUnique',
                'provider' => 'table',
                'message' => 'メールアドレスがすでに登録されています。'
            ]);

        $validator
            ->scalar('password')
            ->minLength('password', 8, 'パスワードは半角英数字8文字以上で入力してください。')
            ->requirePresence('password', 'create', 'パスワードを入力してください。')
            ->allowEmptyString('password', 'パスワードを入力してください。', 'update');

        $validator
            ->scalar('password_confirm')
            ->minLength('password_confirm', 8, '確認用パスワードは半角英数字8文字以上で入力してください。')
            ->sameAs('password', 'password_confirm', '異なるパスワードが入力されています。')
            ->requirePresence('password_confirm', 'create', '確認用パスワードを入力してください。')
            ->allowEmptyString('password_confirm', '確認用パスワードを入力してください。', 'update');

        return $validator;
    }

    /**
     * Returns a rules checker object that will be used for validating
     * application integrity.
     *
     * @param \Cake\ORM\RulesChecker $rules The rules object to be modified.
     * @return \Cake\ORM\RulesChecker
     */
    public function buildRules(RulesChecker $rules)
    {
        $rules->add($rules->isUnique(['email']));

        return $rules;
    }
}
```

---

#### Entity
hashしたパスワードを保存するためにDefaultPasswordHasherを使用しています。

```src/Model/Entity/User.php

use Cake\Auth\DefaultPasswordHasher;
use Cake\ORM\Entity;

class User extends Entity
{
    protected $_accessible = [
        'name' => true,
        'age' => true,
        'gender' => true,
        'email' => true,
        'password' => true,
        'last_login_at' => true,
        'is_deleted' => true,
        'created' => true,
        'modified' => true,
    ];

    protected $_hidden = [
        'password',
    ];

    protected function _setPassword($password)
    {
        return (new DefaultPasswordHasher)->hash($password);
    }
}
```

---

### Controller

---

画面は以下の通りです。
add: 登録画面
confirm: 確認画面
complete: 完了画面

年齢と性別はconfigで選択肢を管理しています。
確認画面へ遷移した際にvalidationチェックを行い、通ったらセッションに保存をします。
完了画面は「完了しました」が表示されるだけです。

```src/Controller/UsersController.php

use App\Controller\AppController;
use Cake\Core\Configure;
use Cake\Event\Event;
use Cake\I18n\Time;

class UsersController extends AppController
{
    public function beforeFilter(Event $event)
    {
        parent::beforeFilter($event);
        $this->Auth->allow([
            'add',
            'confirm',
            'complete',
        ]);
    }

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

    public function add()
    {
        $user = $this->Users->newEntity();
        $ages = Configure::read('User.age');
        $genders = Configure::read('User.gender');

        if ($this->request->is('post')) {
            $user = $this->Users->patchEntity($user, $this->request->data);
            if ($user->errors()) {
                $this->Flash->error('入力内容を確認してください。');
            } else {
                $this->request->session()->write('session.user_add', $user);
                return $this->redirect(['action' => 'confirm']);
            }
        }

        if ($this->request->session()->check('session.user_add')) {
            $user = $this->request->session()->consume('session.user_add');
        }

        $this->set(compact('user', 'ages', 'genders'));
    }

    public function confirm()
    {
        if (!$this->request->session()->check('session.user_add')) {
            return $this->redirect(['action' => 'add']);
        }

        $ages = Configure::read('User.age');
        $genders = Configure::read('User.gender');

        $user = $this->request->session()->read('session.user_add');
        $this->set(compact('user', 'ages', 'genders'));
    }

    public function complete()
    {
        if ($this->request->session()->check('session.user_add')) {
            $user = $this->request->session()->consume('session.user_add');
            
            $result = $this->Users->save($user);
            if (!$result) {
                $this->Flash->error('保存できませんでした。');
                $this->request->session()->write('errors', $user);
                return $this->redirect($this->referer());
            }
        }
    }

}
```

---

### config (選択肢)

---

bootstrap.phpにファイルを定義をすると使用することができます。

```config/bootstrap.php
Configure::load('user', 'default', false);
```

定数はこのように定義します。

```config/user.php
<?php
return [
    "User"=> [
        "age" => [
            1 => '20歳未満',
            2 => '20-29歳',
            3 => '30-39歳',
            4 => '40-49歳',
            5 => '50-59歳',
            6 => '60-69歳',
            7 => '70-79歳'
        ],
        "gender" => [
            'male' => '男性',
            'female' => '女性'
        ],
    ]
];
```
---

### view

---

#### 登録画面
ここでは細かいデザインは置いておいて、大まかなフォームの設定を記述します。

```src/Template/Users/add.ctp
<?= $this->Form->create($user, ['type' => 'post', 'autocomplete' => 'off']) ?>
    
    名前
    <?= $this->Form->control('name', ['type' => 'text', 'label' => false, 'required' => false]) ?>
    
    年齢(セレクトボックス)
    <?= $this->Form->control('age', ['type' => 'select', 'label' => false, 'options' => $ages, 'required' => false, 'empty' => '選択してください']) ?>
    
    性別(ラジオボタン)
    <?= $this->Form->control('gender', ['type' => 'radio', 'label' => false, 'required' => false, 'options' => $genders]) ?>
    
    メールアドレス
    <?= $this->Form->control('email', ['type' => 'text', 'label' => false, 'required' => false]) ?>
    
    パスワード
    <?= $this->Form->control('password', ['type' => 'password', 'label' => false, 'div' => false, 'required' => false, 'value' => '']) ?>
    
    パスワード確認用
    <?= $this->Form->control('password_confirm', ['type' => 'password', 'label' => false, 'div' => false, 'required' => false, 'value' => '']) ?>
        
    <?= $this->Form->button('確認画面へ', ['type' => 'submit']) ?>

<?= $this->Form->end() ?>

```

---

#### 確認画面
ここでも設定を書き残しておきます。
セッションから値を取得して表示します。

```src/Template/Users/confirm.ctp
<?= $this->Form->create($user, ['type' => 'post', 'url' => ['controller' => 'Users', 'action' => 'complete']]) ?>
    
    名前
    <?= $user['name'] ?>
    
    年齢
    <?= $ages[$user['age']] ?>
    
    性別
    <?= $genders[$user['gender']] ?>
    
    メールアドレス
    <?= $user['email'] ?>
    
    <?= $this->Form->button('登録完了', ['type' => 'submit']) ?>

<?= $this->Form->end() ?>
```

---

以上です。
完了画面は「完了しました」が表示されるだけなので省略します。

同じCakePHP3でもフォームヘルパーの書き方が変わっていました。
CakePHP4でもこのような感じでいけるのではないかと(願望込み)。。