ざっくりcakephp2 vs cakephp4の違い

### はじめに
>
CakePHPとはRuby on Railsの概念の多くを取り入れ、Rails流の高速開発とPHPの機動性を兼ね備えたPHPフレームワークです。
現在baserCMS5に向けてucmitzプロジェクトの開発に取り組んでいるのですが、cakephp2→cakephp4への移行にかけて大きな変化があったのでその問題についてざっくりまとめていこうと思います。

**baserCMSを利用している場合[こちら](https://baserproject.github.io/5/ucmitz/index#%E9%96%8B%E7%99%BA%E4%B8%8A%E3%81%AE%E6%B3%A8%E6%84%8F%E7%82%B9)もcakephp2→cakephp4移行の参考になるのでチェックしてみてください**

***
### 目次
- **共通の変更点**
    - 名前空間について
    - 初期化について
- **コントローラーの変更点**
    - requestなどがget~/with~形式になっている
    - AuthComponentがAuthenticationとAuthorizationに変更
- **セッションの変更点**
- **ビューの変更点**
    - id名の変更点
    - 内部のオプションでidが自動で表示されない場合 id=trueを指定する
    - フォームでの単一フィールド値の取得
- **アップロードの変更点**
    - 配列形式vsオブジェクト形式
    - cakephp4で配列形式を使う
- **モデル→テーブルの変更点**
    - テーブルの呼び出し方
    - beforeValidateの廃止
    - Behaviorとアソシエーションの設定方法の違い
    - バリデーションやり方の違い
    - 保存時に'callbacks' => falseのオプションが使えない
    - cakephp2 系における\$this->dataや$this->idの移行
    - cakephp2とcakephp4のO/Rマッパーの違い
- **エンティティの追加**

***

#### 共通の変更点

##### 名前空間について

```php
// cakephp2
App::uses('BcAppController', 'Controller');
// cakephp4
namespace BaserCore\Controller\Admin;
use BaserCore\Controller\BcAppController;
```
##### 初期化について

cakephp2にはなかったinitialize
コンストラクタの後に呼ばれ、コンポネントやヘルパーの新規追加に使用する

```php
/**
// cakephp2
public function __construct($request = null, $response = null)
{
	parent::__construct($request, $response);
}
// cakephp4
public function initialize(): void
{
     parent::initialize();
}
```
***

#### コントローラーの変更点

##### requestなどかなり多くの部分でget~/with~形式になっている
主にコントローラーでrequestを扱いますが、getQueryやwithParamなどにメソッドとして用意されてるのに注意
また、\$this->passedArgsはなくなり、$this->request->getParam('pass')になってる
※ucmitzでは、パラメータをクエリ文字列でも扱えるようにgetParamではく、getQueryに移行して開発してます。

```php
// cakephp2
$hoge = $request->data; // リクエストデータの取得
$request->data = 'hoge'; // リクエストデータの書き換え
$this->passedArgs['site_id']; //　パラメータの取得
// cakephp4
$hoge = $request->getData(); // リクエストデータの取得
$request = $request->withData(name, 'hoge'); // リクエストデータの書き換え
$this->request->getParam('pass.site_id');　//　パラメータの取得
```


##### AuthComponentがAuthenticationとAuthorizationに変更

まずAuthenticationの設定の参考として[こちらの記事](https://tech.at-iroha.jp/?p=1461)がわかりやすいです。
上記の記事を踏まえてですが、Controllerのinitializeの箇所での書き方が変わりました

```php
// AuthComponent
$this->Auth->allow('login');
// Authentication ・ Authorization
$this->Authentication->allowUnauthenticated(['login']);
```

***
#### セッションの変更点

```php
// cakephp2ではコンポネントから使用でしたが、
public $components = ['Session'];
$this->Session->read('etc');
↓
// cakephp4ではリクエストからの使用に変わってます
$session = $request->getSession();
$session->read('etc');
```

***

#### ビューの変更点

##### id名の変更点

id名でドットを境にアッパーキャメルになってたのが、ハイフン区切りになってる点に注意
2系・・・アッパーキャメル（ViewSetting.mode → ViewSettingMode）
3系以降・・・ハイフン区切り（ViewSetting.mode → viewsetting-mode）

```php
// cakephp4-FormHelperを継承したBcAdminFormHelperでの例
echo $this->BcAdminForm->control('ViewSetting.mode', ['type' => 'hidden', 'value' => 'index']);
<input type="hidden" name="ViewSetting[mode]" class="bca-hidden__input" id="viewsetting-mode" value="index">
```

##### 内部のオプションでidが自動で表示されない場合 id=trueを指定する

```php
'deleteCheckbox' => ['class' => 'bca-file__delete-input']
↓
'deleteCheckbox' => ['class' => 'bca-file__delete-input', 'id' => true],
```
##### フォームでの単一フィールド値の取得

```php
// cakephp2
$this->Form->value('description');
// cakephp4
$this->Form->getSourceValue('description');
```
***

### アップロードの変更点

##### 配列形式vsオブジェクト形式
cakephp2系だと配列形式のデータを操作していたのが、cakephp4系だとオブジェクト形式(Laminas\Diactoros\UploadedFile)のデータを操作する形になっています。

後、\$this->Form->inputが$this->Form->controlになってる点にも注意

```php
// https://book.cakephp.org/2/ja/core-libraries/helpers/form.html#id10
// cakephp2系
echo $this->Form->input('ubmittedfile', array(
    'between' => '<br />',
    'type' => 'file'
));
$this->request->data['submittedfile'] = array(
    'name' => 'conference_schedule.pdf',
    'type' => 'application/pdf',
    'tmp_name' => 'C:/WINDOWS/TEMP/php1EE.tmp',
    'error' => 0,
    'size' => 41737,
);
↓
// https://book.cakephp.org/4/ja/controllers/request-response.html#request-file-uploads
// cakephp4系
echo $this->Form->control('attachment', [
    'type' => 'file'
]);
$attachment = $this->request->getData('attachment');
// 配列ではなく、メソッドとして操作
$name = $attachment->getClientFilename();
$type = $attachment->getClientMediaType();
$size = $attachment->getSize();
$tmpName = $attachment->getStream()->getMetadata('uri');
$error = $attachment->getError();
// moveTo() メソッドはファイルが実際にアップロードされたファイルであるかどうかを 
// 自動的に検証し、必要に応じて例外を投げる
$attachment->moveTo($targetPath);
```
##### cakephp4で配列形式を使う

cakephp2同様配列形式で使いたい場合config/app.php で以下のように設定することで使用可能
移行が目的なら、こちらで進めたほうが何かと変更が少なく済む

```php

// config/app.php で以下のように設定
return [
    // ...
    'App' => [
        // ...
        'uploadedFilesAsObjects' => false,
    ],
    // ...
];
```

***

#### モデル→テーブル変更点

##### テーブルの呼び出し方

```php
 // cakephp2
ClassRegistry::init('テーブル');
// cakephp4
TableRegistry::getTableLocator()->get('テーブル');
```

##### beforeValidateの廃止

beforeValidateが廃止になったので、beforeMarshalなどでエンティティに変換される前にデータの書き換えを行い移行する。
afterMarshalなどもあるので、適宜調整する

##### Behaviorとアソシエーションの設定方法の違い

今まで\$actAsに入れていたビヘイビアはテーブルのinitializeにてaddBehaviorする
今まで\$belongToに入れていたビヘイビアはテーブルのinitializeにてbelongToする

```php
// cakephp2
public $actsAs = ['Tree', ['level' => 'level']];
public $belongsTo = [
		'Site' => [
			'className' => 'Site',
			'foreignKey' => 'site_id'
		],
		'User' => [
			'className' => 'User',
			'foreignKey' => 'author_id'
		]
	];
// cakephp4
public function initialize(array $config): void
    {
        parent::initialize($config);
        $this->addBehavior('Tree', ['level' => 'level']);
        $this->belongsTo('Users', [
	         	'className' => 'BaserCore.Users',
		        'foreignKey' => 'author_id',
	    ]);
    }

```

#####  バリデーションやり方の違い

cakephp2 construct の$this->validateをcakephp4 validationDefaultに移行する

```php
// cakephp2
public function __construct($id = false, $table = null, $ds = null)
	{
		parent::__construct($id, $table, $ds);
		$this->validate = ['id' => [['rule' => ['notBlank'], 'message' => __d('baser', 'IDに不正な値が利用されています。')]];
	}
// cakephp4
public function validationDefault(Validator $validator): Validator
    {
        $validator
           ->integer('id')
	       ->notEmptyString('name', __d('baser', 'IDに不正な値が利用されています。'));
	    return $validator;
    }
```

##### 保存時に'callbacks' => falseのオプションが使えない

cakephp2系だと'callbacks' => falseが使えたが、cakephp4系だと使えない

```php
// cakephp2
$this->save($data, ['callbacks' => false]);
↓
// cakephp4
// 特定のeventが来ないようにする
$this->table->getEventManager()->off('Model.afterSave');
$this->table->save($entity, ['validate' => false]);
```

##### cakephp2 系における$this->dataや$this->idの移行

cakephp2系でかなり使われてる\$this->dataや\$this->id
これらの値は$entityから直接とるように移行する

また、 \$this->fieldも廃止になったので、直接エンティティから取得するかエンティティがない場合は$someTable->find()->where()などで取得する

```php
// cakephp2 系
	public function beforeSave($options = [])
	{
		if (!empty($this->data['Content']['id'])) {
			$this->beforeSaveParentId = $this->field('parent_id', ['Content.id' => $this->data['Content']['id']]);
		}
		return parent::beforeSave($options);
	}
↓
// cakephp4
// $this->dataは直接、Eventから入ってくる$entityを渡し、$this->idは$entity->idを渡すことで移行
public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
    {
        if (!empty($entity->id)) {
            $this->beforeSaveParentId = $entity->parent_id;
        }
        return parent::beforeSave($event, $entity, $options);
    }

```
##### cakephp2とcakephp4のO/Rマッパーの違い

cakephp4ではgetでfind('first')でidを指定したのと同じデータが取得できます。
また、containオプションでアソシエーションを$entityに含むことができます。
conditionがwhereになりました。

```php

// cakephp2
$entity = $this->table->find('first', [
    'conditions' => ['id' => $id]
]);

$entities = $this->table->find('all', [
    'conditions' => ['status' => true],
    'order' => ['created' => 'desc']
]);
↓
// cakephp4
$entity = $this->table->get($id, ['contain' => ['Other']]);
$entites = $this->table->find()
    ->where(['status' => true])
    ->order(['created' => 'desc'])
    ->all();
// $entitesはResultSetインスタンスなので、配列として扱いたい場合
$entityArray = $entities->toArray();
```
構文が複雑でsqlを直接書きたい場合
sqlベタ書きじゃなく、ConnectionManager経由で書くことができる

```php
$connection = ConnectionManager::get('default');
$content = $connection
             ->newQuery()
             ->select(['lft', 'rght'])
             ->from('contents')
             ->where(['id' => $id, 'deleted_date IS' => null])
             ->execute()
             ->fetchAll('assoc');
```

#### エンティティの追加

テーブルが全体としたら、エンティティーは個々の行を表すものです。
ファイル名はcakephp2のモデルと同じなので、注意してください(UserのEntityならUser.php)
cakephp2はないエンティティですが、メソッドを仕込めます
例)

```php
class User extends Entity
{

    /**
     * Accessible
     *
     * @var array
     */
    protected $_accessible = [
        '*' => true,
        'id' => false
    ];


    /**
     * 管理ユーザーかどうか判定する
     */
    public function isAdmin(){
      判定処理
    }
}
----- 別のコード
// UserテーブルからResultSetを呼び出す
$users = $this->Users->find()->all();
foreach($users as $user) {
 // UserEntityのメソッドを実行
	if ($user->isAdmin()) {
    何らかの処理
		}
}
```

#### 最後に

他にもcakephp4から実験的に追加された[DIコンテナ](https://book.cakephp.org/4/ja/development/dependency-injection.html)やcakephp4を使った場合のjs処理変更点など
余裕ある時に追加で書いていきます!
もっと、こうした方がいいよとかここ間違ってるよってのがありましたらコメントお待ちしてます！

**また毎月、baserCMS5開発に向けてucmitz開発合宿をしてます。cakephp4を触って開発したいよって方がいらっしゃれば、気軽にご連絡ください！**

🏠[ucmitz開発合宿](https://github.com/baserproject/ucmitz)

最後まで読んでいただきありがとうございました！！⭐⭐⭐