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

科学万能の時代、2016年になってもPHPのリファクタリングは今でも鬼門らしく、現実は厳しい。

PHPをリファクタリングする上で、おそらく一番かんたんであろうクラス名と名前空間のリファクタリングに際しての使用箇所抽出とリネームを実現する上で考慮すべき事柄についてのポエム。PhpStormとかNetBeansとかEclipseとかのIDEがリファクタリング機能を提供してるのは知ってるけど、どの程度確実にリファクタリングしてくれるのかは知らない。

この記事を読んでも現実的な解決手段についての情報は得られない。この記事の情報をもとに実装してくれたら喜んでくれるひとは世の中にいっぱい居ると思ひますよ。

## 用語

### 名前空間

PHPの`namespace`文で定義できる名前空間のこと。`namespace`文に属さないクラス定義を「グローバルスコープ」または「グローバル名前空間」と呼ぶ。

### FQCN (Fully Qualified Class Name)

名前空間をすべて表記した名前のこと。標準の[`DateTime`](http://php.net/manual/ja/class.datetime.php)クラスであれば`\DateTime`のように、`Hoge\Fuga`名前空間の`Piyo`クラスなら`\Hoge\Fuga\Piyo`と表記する。

## いつクラス名のリネームをしたくなるのか

 * クラスの抽象度のレベルを整理したいとき
     * `App\Model\Helper\HogeHelper`を`App\Util\HogeUtil`にまとめたい、とか
 * ライブラリ／フレームワークのお作法が変ったとき
 * クラス名にtypoがあったとき (`Clawler` → `Crawler`みたいな綴りの誤り)
 * 用語が変更されたとき (`User`を`Customer`と呼ぶことにした、とか)

## 構文の種類

[PHP: 名前空間の使用法: エイリアス/インポート - Manual](http://php.net/manual/ja/language.namespaces.importing.php)とか[PHP: 名前解決のルール - Manual](http://php.net/manual/ja/language.namespaces.rules.php)あたりの内容とだいたい同じ。

### 定義文

PHPのクラス定義には概ね以下のような種類がある。

```php
/** FQCN: \X */
class X {}
```

```php
/** FQCN: \Fizz\Buzz */
namespace Fizz {
    class Buzz {}
}
```

```php
/** FQCN: \Hoge\Fuga\Piyo */
namespace Hoge\Fuga;
class Piyo {}
```

### 表記

#### `namespace`なしの場合

`namespace`の範囲外

```php
<?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
<?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
<?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
<?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
<?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
<?php
namespace zonuexe;

use X as XClass, Fizz\Buzz as FB,  Hoge\Fuga\Piyo as HfPiyo;
```

#### `namespace`あり`use`インポート(グループ化)

この構文はPHP7で追加された。

```php
<?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\Model`と`namespace 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\Piyo`を`AwesomeUtil\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のテストケースであれば、以下のような記述がありうる。

```php
$this->assertInstanceOf('\Teto\Routing\Action', $actual);
$this->assertInstanceOf(Teto\Routing\Action::class, $actual);
```

## 実装に向けたメモ

* ファイル／名前空間ごとの`use`インポートをマッピングする
* クラスのリネームに関しては実行時情報などなくても解決可能
* 動的な仕組みを全てサポートするのは諦める
    * Laravelのファサード([Illuminate\Foundation\AliasLoader | Laravel API](https://laravel.com/api/4.2/Illuminate/Foundation/AliasLoader.html))のような仕組みを完全にサポートしようと思ったらちょっと厄介
* 文字列リテラルをどこまでサポートするかは悩ましい
    * 文字列結合とかされたら諦める
    * `Example::class`みたいな記法を使ってくれた方が悩みは減る

## あとがき

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

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

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