Slide 1

Slide 1 text

【PHP】 破壊的バージョンアップと 戦った話〜決断と説得 PHP勉強会in広島 vol.3 2025.01.24 SATOSHI KANEYASU 1

Slide 2

Slide 2 text

自己紹介 氏名:兼安 聡 所属:株式会社サーバーワークス アプリケーションサービス部 在住:広島(フルリモート) 担当:DevOps、技術支援、PM、SM 2024 Japan AWS Top Engineers (Database) 2024 Japan AWS All Certifications Engineers Certified ScrumMaster PMP X:@satoshi256kbyte 2

Slide 3

Slide 3 text

このお話の設定 ➢PHP5→PHP7.3、同時にCakePHP2→CakePHP4への移行を行った話をします。 ➢この話に出てくるプロジェクトは最終的に無事終わってるので安心してください。 ➢インフラ周りの話は今回は対象外とします。 ➢システムはBtoBのWEBシステムです。 ➢サポート切れに伴うバージョンバップです。 ➢故に、期限はマストです。 ➢既存コードに自動テストはありません。 ➢チームにオリジナルの開発者はいません。 ➢CakePHPは2から3の間に破壊的バージョンアップがなされています。 3

Slide 4

Slide 4 text

CakePHPの破壊的バージョンアップとは  主にモデルの扱いが大幅に変わり、 同名メソッドでも入出力フォーマットが変わっていることを指します。 // CakePHP 2.x $this->loadModel('Post'); $post = $this->Post->find('first', [ 'conditions' => ['Post.id' => 1] ]); echo $post['Post']['title']; [ 'Post' => [ 'id' => 1, 'title' => 'CakePHP 2 example' ] ] use Cake\ORM\TableRegistry; $postsTable = TableRegistry::getTableLocator()- >get('Posts'); $post = $postsTable->find('all') ->where(['id' => 1]) ->first(); echo $post->title; App\Model\Entity\Post Object ( [id] => 1 [title] => 'CakePHP 3 example' ) CakePHP2 CakePHP3以降 出力形式 出力形式 4

Slide 5

Slide 5 text

CakePHPの破壊的バージョンアップとは  主にモデルの扱いが大幅に変わり、 同名メソッドでも入出力フォーマットが変わっていることを指します。 // CakePHP 2.x $this->loadModel('Post'); $post = $this->Post->find('first', [ 'conditions' => ['Post.id' => 1] ]); echo $post['Post']['title']; [ 'Post' => [ 'id' => 1, 'title' => 'CakePHP 2 example' ] ] use Cake\ORM\TableRegistry; $postsTable = TableRegistry::getTableLocator()- >get('Posts'); $post = $postsTable->find('all') ->where(['id' => 1]) ->first(); echo $post->title; App\Model\Entity\Post Object ( [id] => 1 [title] => 'CakePHP 3 example' ) CakePHP2 CakePHP3以降 出力形式 出力形式 5 昔ながらの配列を中心とした実装は、 中に何が入っているか不明瞭で、バグの温床となりやすい。 (いわゆる配列地獄) これの是正がフォーマットが変えられた理由(だと思う)

Slide 6

Slide 6 text

いきなり頓挫したところから話を始めます ➢PHP5→PHP7.3、同時にCakePHP2→CakePHP4への移行プロジェクト ➢厳密には、プロジェクト内でCakePHP2→3→4というステップを踏ませている ➢これを、各開発者が既存ソースを解析して新Verで作り直すという方法で開始。 ➢途中まで進めてこのままでは頓挫する!となったところから話は始まります。 ➢なお、新Verで作り直すという方針においては、一定のルール作成とトレーニング期間を設けており、 各開発者に特段の落ち度はありませんでした。 ➢純粋に難易度が高すぎ・作業量が多すぎで無理だった次第です。 6

Slide 7

Slide 7 text

4つの決断と説得 ➢プロジェクトを立て直し、最後までやり切るまでに4つの決断と説得を行っています。 変換関数を作り、既存コードを活かす 安易にフレームワークを乗り換えない 静的解析とフォーマッターをフル活用する ソースコードのメトリクスを算出する 7

Slide 8

Slide 8 text

決断と説得その1 8

Slide 9

Slide 9 text

変換関数を作り、既存コードを活かす 決断 • 入出力フォーマットが変わっている全ての 箇所について、変換関数をかまして極力 既存コードを流用できる方向にする • モダンなコーディングスタイルの優先度を 下げる 上層部への説得 • 方針転換により作業が一旦停滞するが、 コードが流用可能になるのでリカバリでき る • 既存コードが流用可能となれば、バグが あるとしたら変換関数がおかしいか、変換 関数がうまく適用できてないかになる。 • となれば、動かして通れば大体OKといえ る • テストに人海戦術が効かせやすく、プロ ジェクト後半のリスクが下がる 9

Slide 10

Slide 10 text

変換関数適用の流れ $this->loadModel('Post'); $post = $this->Post->find('first', ['conditions' => ['Post.id' => 1]]) $this->loadModel('Post'); $post = $this->Post->findOld('first', ['conditions' => ['Post.id' => 1]]) 入出力フォオーマットが変わっているメソッドを、一旦一括でリネームする リネームしたfindOldは、中で新旧メソッドの入出力フォーマットの変換を行う findOldは、@depcreatedをつけて将来的には廃止する意思を示しておく。 実際にはモデルだけでなく、バリデーションなど各所に変換関数を仕込む。 CakePHP2 10

Slide 11

Slide 11 text

変換関数適用の流れ /** * @deprecated 本メソッドは新規コードには使用しないでください */ public function findOld($type, $params = []) { $where = $params['where'] ?? []; $query = null; select ($type) { case 'first': // ここで新しいfind()を呼ぶ $query = $this->find('all’)->where($where)->first(); break; // 以下、他の$type } if ($query === null) { return []; } // 新しいfind()は遅延実行のクエリを返すので、ここで実行しつつ配列に変換 $results = $query->toArray(); // CakePHP 2の配列構造に変換 return 変換メソッド($results); } 11

Slide 12

Slide 12 text

変換関数適用の流れ /** * @deprecated 本メソッドは新規コードには使用しないでください */ public function findOld($type, $params = []) { $where = $params['where'] ?? []; $query = null; select ($type) { case 'first': // ここで新しいfind()を呼ぶ $query = $this->find('all’)->where($where)->first(); break; // 以下、他の$type } if ($query === null) { return []; } // 新しいfind()は遅延実行のクエリを返すので、ここで実行しつつ配列に変換 $results = $query->toArray(); // CakePHP 2の配列構造に変換 return 変換メソッド($results); } 12 パラメータ指定ではなく、メソッドチェーンになってるので 変換を入れる

Slide 13

Slide 13 text

変換関数適用の流れ /** * @deprecated 本メソッドは新規コードには使用しないでください */ public function findOld($type, $params = []) { $where = $params['where'] ?? []; $query = null; select ($type) { case 'first': // ここで新しいfind()を呼ぶ $query = $this->find('all’)->where($where)->first(); break; // 以下、他の$type } if ($query === null) { return []; } // 新しいfind()は遅延実行のクエリを返すので、ここで実行しつつ配列に変換 $results = $query->toArray(); // CakePHP 2の配列構造に変換 return 変換メソッド($results); } 13 最近のFW・ライブラリは、 クエリの遅延実行が多いのでフォローを入れないと 昔の感覚では使えないことに留意

Slide 14

Slide 14 text

変換関数適用の流れ /** * @deprecated 本メソッドは新規コードには使用しないでください */ public function findOld($type, $params = []) { $where = $params['where'] ?? []; $query = null; select ($type) { case 'first': // ここで新しいfind()を呼ぶ $query = $this->find('all’)->where($where)->first(); break; // 以下、他の$type } if ($query === null) { return []; } // 新しいfind()は遅延実行のクエリを返すので、ここで実行しつつ配列に変換 $results = $query->toArray(); // CakePHP 2の配列構造に変換 return 変換メソッド($results); } 14 単にtoArrayするだけではCakePHP2と同じデータ 構造にならない(ネストの深さが合わない)ので、 調整する関数を挟む

Slide 15

Slide 15 text

決断と説得その2 15

Slide 16

Slide 16 text

破壊的バージョンアップに振り回されるなら、 いっそLaravelに移行した方が早いのでは? 16

Slide 17

Slide 17 text

安易にフレームワークを乗り換えない 決断 • ビジネスが維持できることを最優先に考え る • Laravelには行かない、CakePHPのまま とする チームへの説得 • サポート切れによるバージョンアップは基 本マイナスをゼロに戻す作業 • Laravelにしても開発者の満足感以外 のメリットはない • これ以上のリスクを背負うべきではない • CakePHPは悪いフレームワークではない • Googleトレンドなどを見てもCakePHP の需要は一定以上はキープされている • 後にCakePHP5が出るのでこの判断は正しかった 17

Slide 18

Slide 18 text

決断と説得その3 18

Slide 19

Slide 19 text

静的解析とフォーマッターをフル活用する 決断 • 静的解析はPHPStanを使用し、Level Maxとする • フォーマッターは、{}を[]に変換するなど に使用 • CakePHP2からの移行では、使わなくな るインスタンス変数が大量に発生する • これらの移行の成否を目視で確認しきる のは無理なので、静的解析を活用する チームへの説得 • 機械でできるチェックはやらせる • 機械によるフォーマットはバグを生まないと すると言い切る • PHPStanはMax以外は選んだ理由に 妥当性が見つからないだろう • Levelを下げるよりも例外を指定した方 が良い • 下準備が必要なのは受け入れる、後で 苦労するよりずっと良い 19

Slide 20

Slide 20 text

CakePHP2からの移行では、インスタンス変数が大量に不要になる class Post extends AppModel { public $validate = [ 'title' => [ 'notEmpty' => [ 'rule' => 'notEmpty', 'message' => 'タイトルは必須です。' ], 'maxLength' => [ 'rule' => ['maxLength', 255], 'message' => 'タイトルは255文字以内で入力してください。' ] ], 'content' => [ 'notEmpty' => [ 'rule' => 'notEmpty', 'message' => 'コンテンツは必須です。' ] ] ]; } CakePHP2  例えばバリデーション定義 20

Slide 21

Slide 21 text

CakePHP2からの移行では、インスタンス変数が大量に不要になる namespace App\Model\Table; use Cake\ORM\Table; use Cake\Validation\Validator; class PostsTable extends Table { public $validate = [略 public function validationDefault(Validator $validator): Validator { $validator ->notEmptyString('title', 'タイトルは必須です。') ->maxLength('title', 255, 'タイトルは255文字以内で入力してください。') ->notEmptyString('content', 'コンテンツは必須です。'); return $validator; } CakePHP3以降 ➢CakePHP3以降ではバリデーション定義はメソッドで定義する ➢静的解析で未使用のインスタンス変数をチェックすることで、バリデーション定義の移行漏れ を拾う 21 これが残っていても何も意味はない。 故に静的解析で未使用変数を拾うことで移行漏れ を見つけられる。

Slide 22

Slide 22 text

下準備が必要なのは受け入れる ➢静的解析はメソッドコメントなどが揃ってないと十分に力が発揮されない ➢これについては受け入れて数日間粛々とコメントを追記していく 22

Slide 23

Slide 23 text

PHPStanはCakePHPに沿った静的解析ができる ➢PHPStanはプラグインを入れることでCakePHPに沿った静的解析ができる ➢https://github.com/CakeDC/cakephp-phpstan ➢MVCに違うものが混じっているとこのプラグインが活かしきれない ➢本PJの時は帳票出力を丸ごと移動させ、↑のプラグインを参考に帳票出力用の静的解析 ロジックを作成して解析させた ➢同じことをする必要はないと思う ➢Modelにいろんなものが混じってるのだけは脱却し、 ディレクトリで単位で対象にする・しないとかの選択ができるように持っていくのが大事 23

Slide 24

Slide 24 text

CakePHPのMVCに本来ないものを移動させる ➢MVCの概念が出始めた頃に作られたシステムは、モデルに他のものが混じってることがある ➢混じってると静的解析がうまく動かないので移動させる . ├── Controller ├── Model │ ├── モデル │ └── モデル(実質帳票出力処理) └── View . ├── Controller ├── Document │ └── 実質帳票出力処理 ├── Model │ └── モデル └── View 24

Slide 25

Slide 25 text

決断と説得その4 25

Slide 26

Slide 26 text

ソースコードのメトリクスを算出する 決断 • 移行前後のステップ数を計測 • 増減した理由を整理しておく • PHP Mess Detector (PHPMD)を用 いて、移行前後の複雑度などを計測 • 同じく変化の理由を整理しておく 関係者への説得 • バージョンアップはQCDに対する目が厳し いので、品質に対する説得材料として使 用する • ステップ数・複雑度の低下は、ディレクトリ 構造や重複コードの整理によるものと説 明 • ステップカウントはテストケース・バグ数と 照らし合わせ、IPAのゾーンモデルを元に 品質の証明材料として使用 26

Slide 27

Slide 27 text

ゾーンモデル 27 ➢引用:https://www.ipa.go.jp/archive/files/000072870.pdf

Slide 28

Slide 28 text

まとめ 28

Slide 29

Slide 29 text

まとめ 29 ➢破壊的バージョンアップと戦うには、技術だけでなく、施策の決断・順番、そして説得が必要 ➢もしみなさんが破壊的バージョンアップと戦うことがあれば、これらを参考にして欲しい。 変換関数を作り、既存コードを活かす 安易にフレームワークを乗り換えない 静的解析とフォーマッターをフル活用する ソースコードのメトリクスを算出する

Slide 30

Slide 30 text

ご清聴ありがとうございました。 30

Slide 31

Slide 31 text

参考リンク 31 ➢[Zenn]CakePHP2からCakePHP4への移行のポイント ➢[Qiita]破壊的変更のあるバージョンアップ作業の流れ