科学万能の時代、2016年になってもPHPのリファクタリングは今でも鬼門らしく、現実は厳しい。
PHPをリファクタリングする上で、おそらく一番かんたんであろうクラス名と名前空間のリファクタリングに際しての使用箇所抽出とリネームを実現する上で考慮すべき事柄についてのポエム。PhpStormとかNetBeansとかEclipseとかのIDEがリファクタリング機能を提供してるのは知ってるけど、どの程度確実にリファクタリングしてくれるのかは知らない。
この記事を読んでも現実的な解決手段についての情報は得られない。この記事の情報をもとに実装してくれたら喜んでくれるひとは世の中にいっぱい居ると思ひますよ。
用語
名前空間
PHPのnamespace
文で定義できる名前空間のこと。namespace
文に属さないクラス定義を「グローバルスコープ」または「グローバル名前空間」と呼ぶ。
FQCN (Fully Qualified Class Name)
名前空間をすべて表記した名前のこと。標準のDateTime
クラスであれば\DateTime
のように、Hoge\Fuga
名前空間のPiyo
クラスなら\Hoge\Fuga\Piyo
と表記する。
いつクラス名のリネームをしたくなるのか
- クラスの抽象度のレベルを整理したいとき
-
App\Model\Helper\HogeHelper
をApp\Util\HogeUtil
にまとめたい、とか
-
- ライブラリ/フレームワークのお作法が変ったとき
- クラス名にtypoがあったとき (
Clawler
→Crawler
みたいな綴りの誤り) - 用語が変更されたとき (
User
をCustomer
と呼ぶことにした、とか)
構文の種類
PHP: 名前空間の使用法: エイリアス/インポート - ManualとかPHP: 名前解決のルール - Manualあたりの内容とだいたい同じ。
定義文
PHPのクラス定義には概ね以下のような種類がある。
/** FQCN: \X */
class X {}
/** FQCN: \Fizz\Buzz */
namespace Fizz {
class Buzz {}
}
/** FQCN: \Hoge\Fuga\Piyo */
namespace Hoge\Fuga;
class Piyo {}
表記
namespace
なしの場合
namespace
の範囲外
<?php
// namespaceなし
$x = new X();
$fb = new Fizz\Buzz();
$hfp = new Hoge\Fuga\Piyo();
X::mc();
Fizz\Buzz::mc();
Hoge\Fuga\Piyo::mc();
namespace
ありFQCN表記
<?php
namespace zonuexe;
$x = new \X();
$fb = new \Fizz\Buzz();
$hfp = new \Hoge\Fuga\Piyo();
\X::mc();
\Fizz\Buzz::mc();
\Hoge\Fuga\Piyo::mc();
namespace
ありuse
インポート
<?php
namespace zonuexe;
use X;
use Fizz\Buzz;
use Hoge\Fuga\Piyo;
$x = new X();
$fb = new Buzz();
$hfp = new Piyo();
X::mc();
Buzz::mc();
Piyo::mc();
namespace
ありuse
インポート(途中まで記述)
<?php
namespace zonuexe;
use X;
use Fizz;
use Hoge;
use Hoge\Fuga;
$x = new X();
$fb = new Fizz\Buzz();
$hfp1 = new Hoge\Fuga\Piyo();
$hfp2 = new Fuga\Piyo();
X::mc();
Fizz\Buzz::mc();
Hoge\Fuga\Piyo::mc();
Fuga\Piyo::mc();
namespace
ありuse
インポート(エイリアス)
<?php
namespace zonuexe;
use X as XClass;
use Fizz\Buzz as FB;
use Hoge\Fuga\Piyo as HfPiyo;
$x = new XClass();
$fb = new FB();
$hfp = new HfPiyo();
XClass::mc();
FB::mc();
HfPiyo::mc();
namespace
ありuse
インポート(エイリアス・一行にまとめる)
<?php
namespace zonuexe;
use X as XClass, Fizz\Buzz as FB, Hoge\Fuga\Piyo as HfPiyo;
namespace
ありuse
インポート(グループ化)
この構文はPHP7で追加された。
<?php
namespace zonuexe;
use X as XClass, Fizz\{Buzz, Nass as FN}, Hoge\{Kasu, Fuga\Piyo as fugaPiyo, Hage\Piyo as hagePiyo};
$x = new XClass();
$fb = new Buzz();
$fn = new FN();
$hfk = new Kasu\Piyo();
$hfp = new fugaPiyo();
$hhp = new hagePiyo();
XClass::mc();
Buzz::mc();
FN::mc();
Kasu\Piyo::mc();
fugaPiyo::mc();
hagePiyo::mc();
抽出/リネームを考へる
グローバルクラス
グローバル名前空間に定義されたクラス\XyzClass
を\XXX
にリネームすることを想定する。
<?php
$xyz = new XyzClass;
<?php
namespace zonuexe;
use XyzClass as X_y_z;
$XYZ = \XyzClass::class; // 文字列 "XyzClass"
$xyz1 = new $XYZ;
$xyz2 = new \XyzClass;
$xyz3 = new X_y_z;
$no_xyz = new XyzClass;
これらのファイルにクラスのリネームを適用すると、以下のようになるはずだ。
<?php
$xyz = new XXX;
<?php
namespace zonuexe;
use XXX as X_y_z;
$XYZ = \XXX::class; // 文字列 "XXX"
$xyz1 = new $XYZ;
$xyz2 = new \XXX;
$xyz3 = new X_y_z;
$no_xyz = new XyzClass; // ←この行はリネームされてはならない
このうち、$no_xyz = new XyzClass;
はリネームされてはならない。もし冒頭にuse XyzClass;
があればこの行は\XyzClass
を指すが、それを缺くのでFQDNで表記すると\zonuexe\XyzClass
を意味することになる。
一方でnamespace
定義がないファイルでは、new \XyzClass
と書かなくてもnew XyzClass
と書くことができるので、こちらは置換されないければいけない。そもそもnamespace
のないファイルでuse XyzClass;
と記述するのは無意味だ。
名前空間
namespace Hoge\Model
とnamespace Hoge\Entity
にリネームするとする。
<?php
use Hoge;
use Hoge\Model;
use Hoge\Model\{BookModel as Book, AuthorModel as Author};
$book1 = new Hoge\Model\BookModel;
$book2 = new Model\BookModel;
$book3 = new Book;
これは以下のようになるはずだ。
<?php
use Hoge;
use Hoge\Entity;
use Hoge\Entity\{BookModel as Book, AuthorModel as Author};
$book1 = new Hoge\Entity\BookModel;
$book2 = new Entity\BookModel;
$book3 = new Book;
名前空間内のクラス
クラスHoge\Fuga\Piyo
をAwesomeUtil\Foobar
にリネームするとする。
<?php
use Hoge;
use Hoge\Fuga\{A, B, Piyo};
use Hoge\{Fuga\Piyo as pi_yo, Hage};
$piyo1 = new Hoge\Fuga\Piyo;
$piyo2 = new Piyo;
$piyo3 = new pi_yo;
このケースは若干厄介だ。上のケースと違って「かくあるべし」といった形が一意に定まらない。異論はあるだろうが、ひとつの結果を想定すれば以下のようになるだろうか。
<?php
use AwesomeUtil;
use Hoge\Fuga\{A, B};
use AwesomeUtil\Foobar;
use Hoge\{Hage};
uwe AwesomeUtil\Foobar as pi_yo
$piyo1 = new AwesomeUtil\Foobar;
$piyo2 = new Foobar;
$piyo3 = new pi_yo;
このケースではステートメントの挿入があり、リファクタリング結果が単純なリネームに収まってない。
もっと意地悪なケースを想定する。クラスHoge\Fuga\Piyo\Fusa
が存在してリネーム対象ではなく、pi_yo\Fuga
のような記述があった場合は人力による補正なしにエラーなく自動リネームすることはできない。
ただ、そのようなケースを想定しすぎても仕方がないので、リファクタリングツールの実装としてはWarningを出力するに留めるのが無難か。
文字列リテラル
PHPUnitのテストケースであれば、以下のような記述がありうる。
$this->assertInstanceOf('\Teto\Routing\Action', $actual);
$this->assertInstanceOf(Teto\Routing\Action::class, $actual);
実装に向けたメモ
- ファイル/名前空間ごとの
use
インポートをマッピングする - クラスのリネームに関しては実行時情報などなくても解決可能
- 動的な仕組みを全てサポートするのは諦める
- Laravelのファサード(Illuminate\Foundation\AliasLoader | Laravel API)のような仕組みを完全にサポートしようと思ったらちょっと厄介
- 文字列リテラルをどこまでサポートするかは悩ましい
- 文字列結合とかされたら諦める
-
Example::class
みたいな記法を使ってくれた方が悩みは減る
あとがき
PHPのリファクタリングに興味があったり期待を持ってたりしたら、BaguettePHP/DefInfoあたりにStar付けるかissueに適当なコメントつけてくれるかしてくれるとモチベーションになるような気がする、と乞食をしておく。
とまで言っておいて筆者は気が散りやすいので、これを読んでくれた人が自分でさっさと実装してくれた方が人類は平和になる気がする。
「PHPのリファクタリングを考察する(メソッド名篇)」に続く… のか? クラス名よりもっとつらくなることは目に見えてる。