Date

PHP 7.1~のアクセス修飾子(public, etc..)を自動でつけるライブラリを作った

  • Share on Hatena
  • Share on Twitter
  • Share on Facebook
Go back to top
目次 特徴 インストール 変換 実行後の差分 実行前のサンプル アクセス修飾子の分類ルール ルール 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.php
declare(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.php
declare(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.php
class 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.php
class ExtendsMailCommand extends MailCommand { const PROTECTED_OVERRIDE = false; public function run() { return self::PROTECTED_USE_BY_CHILD; } }
/src/commands/GrandChildExtendsMailCommand.php
class 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>

アクセス修飾子の分類ルール

ロジック分岐図.png

自分が目視で修正する際のルールをプログラムに起こしました。

バグを防ぐため、最大限広めに参照できるようにします。

ルール 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. 定数情報の収集

  1. 分析対象の PHP ファイルを AST に変換
  2. 自クラスの名前解決定
  3. 継承しているクラスの名前解決
  4. 自クラスに定義されているオブジェクト定数の収集
  5. 参照されているオブジェクト定数を収集して上記ルールで分類

参照されている可能性のあるファイルを全て分析することが重要です。クラス定義がない view ファイルであれば、4 だけやって終了します。

2. 継承関係の解決

  1. 収集した protected, public のオブジェクト定数への参照と実際の定数定義を突合して整理

継承関係にあるクラスの定数を子クラス経由で見ている場合、実際に定義されている定数クラスをはっきりさせる必要があります。継承しているクラスをたどり、実際の定義とあっているか確認します。

3. リファクタリング

  1. 変換対象の PHP ファイルを AST に変換
  2. これまでに収集した情報から、アクセス修飾子を付け直す
  3. 情報が何もない場合は、private にする
  4. もとからアクセス修飾子がついている場合は、public のみ修正
  5. 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 コードを実行できてしまうからです。これを使うことはおすすめしません。

https://www.php.net/manual/ja/function.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 コードに悩んでいる誰かのお役に立てれば幸いです。