Posted at

PHPのリファクタリングを考察する(クラス名篇)

More than 3 years have passed since last update.

科学万能の時代、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\HogeHelperApp\Util\HogeUtilにまとめたい、とか



  • ライブラリ/フレームワークのお作法が変ったとき

  • クラス名にtypoがあったとき (ClawlerCrawlerみたいな綴りの誤り)

  • 用語が変更されたとき (UserCustomerと呼ぶことにした、とか)


構文の種類

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にリネームすることを想定する。


no-namespace_after.php

<?php

$xyz = new XyzClass;



namespace_before.php

<?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;


これらのファイルにクラスのリネームを適用すると、以下のようになるはずだ。


no-namespace_after.php

<?php

$xyz = new XXX;



namespace_after.php

<?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\Modelnamespace Hoge\Entityにリネームするとする。


before.php

<?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;


これは以下のようになるはずだ。


after.php

<?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\PiyoAwesomeUtil\Foobarにリネームするとする。


before.php

<?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;


このケースは若干厄介だ。上のケースと違って「かくあるべし」といった形が一意に定まらない。異論はあるだろうが、ひとつの結果を想定すれば以下のようになるだろうか。


after.php

<?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インポートをマッピングする

  • クラスのリネームに関しては実行時情報などなくても解決可能

  • 動的な仕組みを全てサポートするのは諦める



  • 文字列リテラルをどこまでサポートするかは悩ましい


    • 文字列結合とかされたら諦める


    • Example::classみたいな記法を使ってくれた方が悩みは減る




あとがき

PHPのリファクタリングに興味があったり期待を持ってたりしたら、BaguettePHP/DefInfoあたりにStar付けるかissueに適当なコメントつけてくれるかしてくれるとモチベーションになるような気がする、と乞食をしておく。

とまで言っておいて筆者は気が散りやすいので、これを読んでくれた人が自分でさっさと実装してくれた方が人類は平和になる気がする。

「PHPのリファクタリングを考察する(メソッド名篇)」に続く… のか? クラス名よりもっとつらくなることは目に見えてる。