見出し画像

600超のPHPファイルに名前空間をメタプログラミングで付与した方法

今回、BEAR.Sundayの作者の郡山さんと一緒に600超のPHPファイルに名前空間を付与しました。小宮山+郡山のコンビで、チームko[r|m]iyamaです。その取り組みを詳しく紹介します。

専用ライブラリを作成し、メタプログラミングで対応しました。郡山さんがcomposer, githubでライブラリを公開しているので、ぜひのぞいてみてください。

名前空間がない同じ名前のファイルがたくさん

弁護士ドットコムのサイトは、フレームワークにYii1を利用しています。10年以上運用されているので、PHPに名前空間が導入される前のコードもたくさんあります。

そういったclassはYii1の疑似名前空間での参照や直接classでrequireで呼び出されていました。そのためIDEで補完ができかず、修正範囲が探しづらくなっていました。

Yii::import('application.controllers.prefecture.indexAction');

また、class定義と一緒にrequireを使用するのは、PSR-1: Basic Coding Standard「Side Effects」にも違反しています。

// side effect: loads a file
require("file.php");


// declaration
function foo()
{
   // function body
}

現在はPHP7.3で開発しているので、新規追加のコードには名前空間が必須です。また、PSR-12: Extended Coding Styleにそって、開発が進められています。

しかし、名前空間のないコードがリファクタリングの足を引っ張っています。PHPStan静的解析を導入するにあたり、事前にautoloadが出来ず障害となり本腰を入れて対応しました。

PHPStan導入については、下記をご覧ください。

名前空間を付与するライブラリの作成

プログラムでの名前空間の追加は、nikic/PHP-Parserの構文木解析を使用して実現しています。PHP-Parserは、PHPで動くパーサーです。PHP 5.2~7.4のコードを解析し抽象構文木に変換し、元に戻せます。

グローバルクラスのimport、変更範囲の最小化など、テストを繰り返して、郡山さんに調整をしてもらいました。

処理の流れは、下記のようになります。

1. すでに名前空間があったら、パス
2. ディレクトリ名から名前空間を決める
3. PHPファイルを構文木に変換
4. 名前空間のオブジェクトを構文木に追加
5. 構文木を探索して、グローバルクラスを収集
6. 集めたグローバルクラスをuseオブジェクトとして、構文木に追加
7. 構文木をPHPファイルに戻す

下記のようなcontrollers/prefecture/indexAction.phpがあると、このように修正されます。先頭のapplicationは、Yii1の仕様に合わせて共通で付与しています。本来であれば、Bengo4などの、vendorネームが良いと思います。

<?php

class indexAction extends \CAction
<?php
namespace application\controllers\prefecture;
use CAction;

class indexAction extends \CAction
{
  ....
}

このライブラリでは名前空間の付与のみ対応します。「すでにuseされているclassをaliasで参照する」「PSR-12にそった改行」などの調整は、他のツールで行いました。なので、コードスタイルがちょっと気持ちが悪いですが、この段階では許容します。

そのため、ライブラリを作りこまなくても、他の適したツールを使い分け、迅速に対応できました。

実際、PHP-Parserで抽象構文木を書き換えて、PHPファイルの改行を調整するのは大変です。前後のノードを見て、改行&位置を調整する必要があり、複雑になります。

コーディング規約に合わせて整形

「不要な完全修飾系の修正」「改行の調整」を含むコーディング規約の調整は、PhpStormで行いました。細かく調整できて、最初に併用していたPHP-CS-Fixerは、最終的に使わなくなりました。

Code Styleで、「不必要な完全修飾系での参照の修正」等を選択し、Code Cleanupを実行します。

ここで、前段で名前空間を付与した際に、グローバルオブジェクトをuseした効果が出てきます。classの中でグローバル参照されていても、エイリアス参照に全て修正されます。

同時に、「改行の不足」も同時に修正しました。

その結果、先ほどのcontrollers/prefecture/indexAction.phpが下記のようになります。

<?php
namespace application\controllers\prefecture;
use CAction; # not use alias
class indexAction extends \CAction
{
  ...
}
<?php

namespace application\controllers\prefecture;

use CAction;

class indexAction extends CAction
{
  ...
}

私は、Visual Studio Code派です。なので、PhpStormが自動でここまで出来るのは知りませんでした。PHP-Parserの開発者nikicが勤めているだけあり、すごいですね。

名前空間を付与したファイルの呼び出し元の修正

Yii1にはimportという独自import機能があります。内部的には、ディレクトリ名にそって、require_onceしてキャッシュする仕組みです。この機能で、多くの名前空間のないファイルが参照されていました。

これらを、autoloadを使用した名前空間での参照に、IDEの正規表現で置換しました。

Yii::import('application.controllers.prefecture.indexAction');
use applications\controllers\prefecture\indexAction;

静的解析で修正箇所のチェック

静的解析には、PHPStanを使用しています。PHPStanはautoloadなど一部のファイルを実行することで、高速化を実現しています。

ですが、同名のclassがあるディレクトリを、autoloadで取り込もうとすると「Fatal: Cannot redeclare class」エラーが発生し、静的解析が上手く動きません。

今回、名前空間を付与したことで、エラーがでなくなり解析できるようになりました。

これで、名前空間を付与したclassが参照に失敗している箇所を洗い出しました。いくつか問題が発生しました。例えば、デイレクトリが予約語になっており、結果名前空間に予約語が混入し、参照に失敗していました。

下記の例だと、caseは予約語なのでアウトです。

namespase application\controllers\case;

同時に、もともと存在しないclassへの参照もパラパラ検知され、低レベルのエラーも修正していきました。

動作確認と段階的なリリース

静的解析制御のむずかしさから、対象からviewをはずしていました。ところが、viewからフレームワークの機能で暗黙的に参照されているケースがあり、数か所対応漏れが発覚しました。

比較的用途が限定されているディレクトリで実施したのですが、念入りなチェックが大切ですね。間際で気付けて良かったです。

フレームワークに依存する部分は、静的解析でも網羅するのは難しいです。最近のLarastanとかだと、うまく解釈できるのかもしれませんが。可搬性の面からも、フレームワークに依存しないプログラムが大事ですね。

リリースは、修正範囲が多いので、ディレクトリごとに分割して行いました。

まとめ

メタプログラミングにより、広範囲を修正できました。PHP-Parserはよくできており、ロジックさえ組めれば、かなりいろいろできそうです。

当初、私に抽象構文木として拡張する発想はありませんでした。郡山さんの発想は、とても刺激的でした。おかげで、私自身とても勉強になり、スムーズに対応できました。

継続的にサービスを運用していくうえで、レガシーコードとどう向き合うかが常に問題になると思います。

フルスクラッチで書き換えるのが常にベストとは限りません。新しい言語機能の取り込み、コードスタイルの調整などは、メタプログラミングによるコード修正で対応できる場合もあります。

今後もコード改善の一つのアプローチとして、着目していきたいです。誰かの参考になれば幸いです。

#弁護士ドットコム #PHP

この記事が気に入ったらサポートをしてみませんか?