🍰

CakePHP2からCakePHP4への移行のポイント

2023/04/09に公開

こんにちは。深緑です。

昨年から今年に渡り、CakePHP2からCakePHP4への移行作業をやらせていただきました。
何分CakePHPは2から3になる時に破壊的なバージョンアップがされていて、移行作業がとてつもなく大変でした。
諸般の事情により具体的なコードが出せないのですが、せめてポイントだけでも書き残しておけば誰かの役に立つかも知れないと思ったので記述してみます。

配列を使ってるところはできるだけそのまま生かすことを基本方針とする

CakePHP2とCakePHP3以降の最大の違いは、
データのやり取りが配列ではなくオブジェクトが中心になったことです。
これにより、MVC全領域に変更が発生します。
やってみてわかりましたが、
配列を使いまくっている既存のソースを全てオブジェクトを使うように変えるのは到底無理でした💦
なので、配列を使ってるところはできるだけそのまま生かすことを基本方針としました。
以降は、この基本方針の元に具体的に実行したことを書いていきます。

CakePHP2っぽいデータ取得関数を作る

CakePHP2のfindを配列を返します。
こんな感じです。

Array
(
    [0] => Array
        (
            [ModelName] => Array
                (
                    [id] => 83
                    [field1] => value1
                    [field2] => value2
                    [field3] => value3
                )

            [AssociatedModelName] => Array
                (
                    [id] => 1
                    [field1] => value1
                    [field2] => value2
                    [field3] => value3
                )

        )
)

[参考]CakePHP 2.x Cookbook - find('all')

一方、CakePHP4はオブジェクトを返します。
CakePHP 4.x Strawberry - Cookbook データのロードに Finder を使う

find() メソッドの戻り値は常に Cake\ORM\Query オブジェクトです。

CakePHP2でできた既存ソースをオブジェクトを使うように直して回るととんでもない工数かかかるので、以下のようにします。

  • CakePHP4の通常のfind()とは別に、検索結果を配列で返すメソッドを作リます。
    メソッドはfindOld()とします。
  • findOld()は検索結果のオブジェクトを配列化して返します。
  • 既存ソースのfind()を読んでる箇所をfindOld()を呼ぶように一括置換します。

これで結構な量のソースが活かせるようになりました。
ただし、findOld()はあくまで一発目の移行で既存ソースを活かすのが目的、将来データ取得処理を書く時は使用しないものとし、メソッドコメントには@deprecatedをつけておきました。

findOld()は検索結果のオブジェクトを配列化して返しますが、単純にtoArray()を使うだけではうまくいきません。
CakePHP2のfind()が返す配列と、Cake\ORM\Query オブジェクトをtoArray()した時のデータ構造に差があるからです・・・。
単一テーブルのfind()なら良いのですがjoinをしていると、データの階層構造に違いが生まれます。(CakePHP2の方は微妙にフラットな構造になる。)
ですので、findOld()では差を埋めるために階層の調整をしてあげるようにしました。

FormデータをCakePHP2のようにリクエストデータから得るようにする

ビュー側でFormを書く場合の書き方は、
CakePHP2はこう

echo $this->Form->create();

CakePHP4はこうです。

echo $this->Form->create($entity);

CakePHP2はFormデータをリクエストデータから作ります。
一方、CakePHP4の方は引数にエンティティを渡して、エンティティからFormデータが作るのが公式が一番最初に書いてる書き方になっています。
それに従ってFormをエンティティ前提にしてしまうと、既存ソースのFormの部分が全部作り直しになってしまいます。
しかし、CakePHP4でも次のように書くとCakePHP2のようにFormデータをリクエストデータから作ってくれるようになりますのでこれを利用しました。
これでビュー側の旧ソースも大分活きるようになりました。

echo $this->Form->create('data');

ちなみにこの書き方は、公式サイトにちらっとだけ書いてあります。

CakePHP 4.x Strawberry Cookbook - Form - クエリー文字列からフォームの値を取得

サポートするソースは、 context, data そして query です。 単一または複数のソースを使用できます。 FormHelper によって生成されたウィジェットは 設定した順序でソースから値を集めます。

Formデータがリクエストデータから作られるようになったとして、次はどうやってリクエスデータにデータをいれるのか?を考えました。
実は、CakePHP2でできた単純にリクエストデータを入れ替える書き方はCakePHP4ではできなくなっています。

$this->request->data = $data; // このソースはCakePHP4ではエラーになります。

下のように書くとエラーにならず、同じことができます。

$data = $this->request->getData();
$data['hoge'] = 'あああ'; // 書き換え
$this->request = $this->request->withData($data); // 入れ替え

上記一連の処理を行うメソッドを作り、$this->request->data = $data;を一括置換しました。
一括置換はVSCodeを使うといいですね。
正規表現が文字列の入れ替えまでできてとても便利です。

バリデーション定義の変換

CakePHP2のバリデーショ定義はインスタンス変数の配列です。
こんな感じです。

public $validate = array(
    'login' => array(
        'alphaNumeric' => array(
            'rule' => 'alphaNumeric',
            'required' => true,
            'message' => '文字と数字のみです'
        ),
    ),
);

[参考]CakePHP2 - データバリデーション

一方、CakePHP4はこんな感じです。

$validator
    ->requirePresence('login')
    ->notEmptyString('login', 'このフィールドに入力してください')
    ->alphaNumeric('login', '文字と数字のみです');

[参考]CakePHP4 - バリデーターを作成する

これも書き換えるのは困難だったので、CakePHP2の配列を変換するメソッド:converValidate()を作りました。
converValidate()は元の配列から各フィールドの定義を取得。
上述のloginフィールドなら、下記の動きをします。

  1. $validator->ruleに書いてあるメソッド(フィールド名, messageの値);を実行
  2. requiredがTrueなので、$validator->notEmptyString(フィールド名, messageの値);を実行
    このバリデーション定義がデフォルトのものであったとしたらこんな記述になります。
public function validationDefault(Validator $validator): Validator
{
    $validate = array(
        'login' => array(
            'alphaNumeric' => array(
                'rule' => 'alphaNumeric',
                'required' => true,
                'message' => '文字と数字のみです'
            ),
        ),
    );
    $validator = $this->converValidate($validate);

    return $validator;
}

validationDefault()を作った後、CakePHP2のインスタンス変数$validateを移動させる感じです。
この作業はかなり大変なので、どうしても漏れが発生しますので移行を機会にPHPStanを導入しました。
[公式]PHPStan
PHPStanで静的解析を行うと使ってないインスタンス変数も拾うことができるので、結果的に$validateに関する漏れを拾うことができます。

バリデーションの書き換え

バリデーションの書き方もこれまた違うのですが、
ここに関しては変換メソッドなど作らずに愚直に直して回りました。

$this->ModelName->set($this->request->data);
if ($this->ModelName->validates()) {
    // 正しい場合のロジック
} else {
    // 正しくない場合のロジック
    $errors = $this->ModelName->validationErrors;
}

[参考]CakePHP2 - コントローラーからのバリデーション

$enitity = $this->ModelName->newEntity($this->request->getData());
if (!$enitity->errors()) {
    // 正しい場合のロジック
} else {
    // 正しくない場合のロジック
    $errors = $enitity->errors();
}

[参考]CakePHP4 - エンティティーをバリデーションする

愚直に直していった理由としては、
そもそも実際のソースはもっと法則性がないので置換や変換メソッドで逃げようないというのもありますが、
newEntity()patchEntity()は最初は扱いにくいさを感じますが、
慣れてくるとCakePHP2のバリデーションより圧倒的にソースがすっきりするので下手なことはせずにフレームワークに倣ったほうが良いと判断したからです。

CakePHP4になって改善されたとこはありますが、
個人的にはCakePHP2のバリデーションがインスタンス変数を使ってるところ一番ソースが荒れる原因だと思っているのでここは絶対にフレームワーク通りが良いと思っていました。
最終的に完成したソースを見るとこの判断は正しかったと思います。

終わりに

諸般の事情で肝である変換メソッドの中身が書けないのでどうにもフワっとした記事になってしまいましたが、
以上が私が行ったCakePHP2から4の移行におけるポイントです。
ちなみにこんなに苦労するなら無理にCakePHP4に移行しなくても良くない?というのはまったくもってそうです・・・。
システムアーキテクトはバージョンアップさせるか、いっそ乗り換えるかしっかり見極めないといけませんね。

Discussion