- Date
PHP 7.1~のアクセス修飾子(public, etc..)を自動でつけるライブラリを作った
- Share on Hatena
- Share on X
- Share on Facebook
目次
特徴 インストール 変換 実行後の差分 実行前のサンプル アクセス修飾子の分類ルール ルール 1. 定数が self、parent、static 以外のユニークなクラス名で取得されている? ルール 2. 自クラスから static で取得されている? ルール 3. 継承関係のクラスから self、parent、static で取得されている? ルール 4. 同じ名前の定数が、継承関係にある親クラスと子クラスで宣言されている? 全て No で、private になるケース おおまかな処理の流れ 1. 定数情報の収集 2. 継承関係の解決 3. リファクタリング 【注意事項】 eval(), constant()等の文字列連結による参照 blade 等の PHP テンプレートエンジンの view ファイル 実験的機能(未使用定数の自動削除) 所感PHP7.1から、オブジェクト定数にアクセス修飾子(public, protected, private
)が書けます。これによりデフォルトの public ではなく、具体的な参照範囲の制限が可能になり、可読性の向上、オブジェクトのカプセル化が進みます。
PSR-12でも、全ての定数&プロパティにアクセス修飾子が必須とされています。
4.3 Properties and Constants Visibility MUST be declared on all properties.
Visibility MUST be declared on all constants if your project PHP minimum version supports constant visibilities (PHP 7.1 or later).
みなさんのコードには、全ての定数にアクセス修飾子が書かれていますか?
うちでも、アクセス修飾子が書かれていない古い定数が数千あり、自動でリファクタリングするライブラリを自作しました。 public
だけでなく、private
, protected
がつきます。
実際にプロダクトコードに適応し、アクセス修飾子がなかった定数 3033 個を付け直しました。結果 1800 程度がprivate
、65 がprotected
になり、かなり読みやすくなりました。
https://github.com/komtaki/visibility-recommender
最初、rectorphp/rectorで対応しようとして、public のみ付与するルールしかなく自作しました。そのため余裕があれば rector の拡張ルールも作りたいです。
特徴
- public のオブジェクト定数に、
private, protected, private
の三種類が自動で付与できる - 最小限の変更のみで、改行空白等は全て維持される。
- 対応できるファイル
- 名前空間がついている class と、ついていない class の混在
- アクセス修飾子がついている定数と、ついていない定数の混在
- class 定義のないプレーンなファイルなど
- 非対応
eval()
,constant()
など文字列連結での定数参照
インストール
composer require komtaki/visibility-recommender
変換
/command.phpdeclare(strict_types=1); require_once __DIR__ . '/../vendor/autoload.php'; use Komtaki\VisibilityRecommender\Commands\RecommendConstVisibility; // 変換したいファイルが使われている可能性のあるファイルやディレクトリを指定 $autoloadDirs = [__DIR__ . './src']; // 変換したいファイルやディレクトリを指定 $targetDir = __DIR__ . './src'; // 変換 (new RecommendConstVisibility($autoloadDirs, $targetDir))->run();
実行後の差分
class Mail { // not used - const STATUS_YET = 0; + private const STATUS_YET = 0; // used by command class - const STATUS_PROCESS = 1; + public const STATUS_PROCESS = 1; // not used - public const STATUS_DONE = 2; + private const STATUS_DONE = 2; // used by view - const STATUS_CANCEL = 99; + public const STATUS_CANCEL = 99; } class MailCommand { - const PROTECTED_USE_BY_SELF = true; + protected const PROTECTED_USE_BY_SELF = true; - const PROTECTED_USE_BY_CHILD = 200; + protected const PROTECTED_USE_BY_CHILD = 200; - const PROTECTED_USE_BY_GRAND_CHILD = true; + protected const PROTECTED_USE_BY_GRAND_CHILD = true; class ExtendsMailCommand extends MailCommand { - const PROTECTED_OVERRIDE = false; + protected const PROTECTED_OVERRIDE = false; class GrandChildExtendsMailCommand extends ExtendsMailCommand { - const PROTECTED_OVERRIDE =true; + protected const PROTECTED_OVERRIDE =true;
実行前のサンプル
./src/ ├── Mail.php ├── commands │ ├── ExtendsMailCommand.php │ ├── MailCommand.php │ └── GrandChildExtendsMailCommand.php └── views └── index.php
/src/Mail.phpdeclare(strict_types=1); class Mail { // not used const STATUS_YET = 0; // used by command class const STATUS_PROCESS = 1; // not used public const STATUS_DONE = 2; // used by view const STATUS_CANCEL = 99; }
/src/commands/MailCommand.phpclass MailCommand { const PROTECTED_USE_BY_SELF = true; const PROTECTED_USE_BY_CHILD = 200; const PROTECTED_USE_BY_GRAND_CHILD = true; public function run() { echo Mail::STATUS_PROCESS; } public function getStatus() { return static::PROTECTED_USE_BY_SELF; } }
/src/commands/ExtendsMailCommand.phpclass ExtendsMailCommand extends MailCommand { const PROTECTED_OVERRIDE = false; public function run() { return self::PROTECTED_USE_BY_CHILD; } }
/src/commands/GrandChildExtendsMailCommand.phpclass GrandChildExtendsMailCommand extends ExtendsMailCommand { const PROTECTED_OVERRIDE = true; public function run() { return self::PROTECTED_USE_BY_GRAND_CHILD; } }
/src/views/index.php<p><?php echo Mail::STATUS_CANCEL; ?></p>
アクセス修飾子の分類ルール
自分が目視で修正する際のルールをプログラムに起こしました。
バグを防ぐため、最大限広めに参照できるようにします。
ルール 1. 定数が self、parent、static 以外のユニークなクラス名で取得されている?
class A { public function run() { return B::STATUS; } }
自クラス名をself
ではなくて、自クラス名で呼んでいる可能性はあります。しかしマイノリティなので、今回は無視しました。
Yes
であれば、ほぼpublic
。
ルール 2. 自クラスから static で取得されている?
class A { protected const STATUS = 'ok'; public function run() { echo static::STATUS; } } class B extends A { } // 'ok' (new B())->run();
self
ではなくstatic
で呼んでいるので、継承して静的遅延束縛される可能性があります。その際、該当の定数が子クラスから参照できる必要があります。
上記のサンプルコードは、private
にすると定数にアクセス出来ずエラーになります。
PHP Fatal error: Uncaught Error: Undefined constant B::STATUS
これが静的継承のコンテキストを維持して、参照できるということです。
PHP には、遅延静的束縛と呼ばれる機能が搭載されています。これを使用すると、静的継承のコンテキストで呼び出し元のクラスを参照できるようになります。
https://www.php.net/manual/ja/language.oop5.late-static-bindings.php
継承関係にあるクラスから呼ばれる可能性があるので、Yes
であればprotected
です。
ルール 3. 継承関係のクラスから self、parent、static で取得されている?
class A { protected const STATUS = 'ok'; } class B extends A { public function run() { echo self::STATUS; } } // 'ok' (new B())->run();
自クラスにない定数を呼んでいるなら、その親クラスの定数が参照できる必要があります。
よって継承関係にあるクラスから呼ばれるので、Yes
ならprotected
と判断。
ルール 4. 同じ名前の定数が、継承関係にある親クラスと子クラスで宣言されている?
class A { protected const STATUS = 'ok'; public function run() { echo static::STATUS; } class B extends A { protected const STATUS = 'error'; } // 'error' (new B())->run();
親クラスの定数を上書きする可能性があります。
よって継承関係にあるクラスから呼ばれるので、Yes
であればprotected
と判断。
全て No で、private になるケース
ここまで一つも該当しない場合、それはprivate
です。
下記のような例が該当します。
- 自クラスからのみ参照されている定数
- どこからも参照されていない定数
おおまかな処理の流れ
AST 操作は、nikic/PHP-Parser を使っています。
1. 定数情報の収集
- 分析対象の PHP ファイルを AST に変換
- 自クラスの名前解決定
- 継承しているクラスの名前解決
- 自クラスに定義されているオブジェクト定数の収集
- 参照されているオブジェクト定数を収集して上記ルールで分類
参照されている可能性のあるファイルを全て分析することが重要です。クラス定義がない view ファイルであれば、4 だけやって終了します。
2. 継承関係の解決
- 収集した protected, public のオブジェクト定数への参照と実際の定数定義を突合して整理
継承関係にあるクラスの定数を子クラス経由で見ている場合、実際に定義されている定数クラスをはっきりさせる必要があります。継承しているクラスをたどり、実際の定義とあっているか確認します。
3. リファクタリング
- 変換対象の PHP ファイルを AST に変換
- これまでに収集した情報から、アクセス修飾子を付け直す
- 情報が何もない場合は、private にする
- もとからアクセス修飾子がついている場合は、public のみ修正
- AST を PHP ファイルに復元して、元のファイルを置換
【注意事項】
eval(), constant()等の文字列連結による参照
class A { const STATUS = 'ok'; } $name = 'STATUS'; // 'ok' eval("echo A::${name};"); // 'ok' echo constant("A::${name}"); // 'ok' $className = "A"; echo $className::STATUS;
特に eval は公式にも危険と書いてあります。使っていないですよね?基本的に文字列連結による復元は、静的解析もできずバグの原因になると思います。
eval() は非常に危険な言語構造です。というのも、任意の PHP コードを実行できてしまうからです。これを使うことはおすすめしません。
AST の探索では、どんな定数が復元されるかわからないため非対応です。該当クラス外から上記のように参照されていた場合、本来であればpublic
なのに、private
になります。
頑張って目検で乗り越えてください。
blade 等の PHP テンプレートエンジンの view ファイル
PHP のパースに失敗する可能性があります。動かない場合、Blade ならムスターシュ{{}}
をパースできる無害な文字列に変えて、変換した後に戻すのがいいかもしれません。
実験的機能(未使用定数の自動削除)
declare(strict_types=1); require_once __DIR__ . '/../vendor/autoload.php'; use Komtaki\VisibilityRecommender\Commands\RemoveUnusedConst; // 変換したいファイルが使われている可能性のあるファイルやディレクトリを指定 $autoloadDirs = [__DIR__ . './tests/Fake/FixMe']; // 変換したいファイルやディレクトリを指定 $targetDir = __DIR__ . './tests/Fake/FixMe'; // 変換 (new RemoveUnusedConst($autoloadDirs, $targetDir))->run();
未使用な定数の削除も自動で出来ます。上記の分類の時点で、処理上はわかっているのですが、private
を付けてます。
ただし、下記のようにインラインのコメントが定数についている&次行にも記述がある場合、消えるコメントがズレることがあります。本当は、// 未使用
が消えて、// 使用中
は残ってほしいところです。
class A { - const STATUS_1 = 'ok'; // 使用中 + const STATUS_1 = 'ok'; // 未使用 - const STATUS_2 = 'okok'; // 未使用 }
所感
年末に大掃除気分で作り、ディレクトリ単位で少しずつ実践投入しながら微調整しました。 かなり対象が多かったのと片手間にやっていたので、全て適応したら春になってました。
「退屈なことは Python にやらせよう」とはよく言われますが、退屈な PHP のことは PHP にやらせたいですね。
古いバージョンの PHP コードに悩んでいる誰かのお役に立てれば幸いです。
- Share on Hatena
- Share on X
- Share on Facebook
最近の #PHP の記事
www.komtaki.com
PHP Conference 2022に登壇しました
- Date
qiita.com
rectorphp/rector deep dive ~PHPStanと併用しプロダクションのPHPアプリケーションを大規模リファクタリングする方法~
- Date