僕はPHPのコードリーディングをするときにGoogle Chromeを使っています。
メリットとしては
- 同じクラス内の違うメソッドを参照するときに、別タブで同じファイルが開ける
- 間違って文字を追加or消去→取り消しの手間がない
などがあります。
ですが、別クラスのファイルを開くときには名前空間を含めたクラス名をコピペして.phpを追加しなければいけないのが面倒でした。
なので1クリックで目当てのファイルを表示できるビューワーを作ってみました。
githubのリンク
コード解説
$autoloader = 'vendor/autoload.php';
if (file_exists($autoloader)) {
try {
require($autoloader);
} catch(Throwable $e) {
$code = "invalid namespace";
goto output;
}
} else {
$code = "Invalid file path";
goto output;
}
autoload.php
ファイルが存在しない、もしくはrequireに失敗すればgoto文でoutput:
に飛びます。
$url = (empty($_SERVER['HTTPS']) ? 'http://' : 'https://').$_SERVER["HTTP_HOST"] . strtok($_SERVER["REQUEST_URI"],'?');
クエリ文字列を抜いたURLです。
// get code from namespace
if (isset($_GET['ns'])) {
$namespace = $_GET['ns'];
try {
$this_file = (new ReflectionClass($namespace))->getFileName();
$code = @file_get_contents($this_file);
preg_match('/^(class|interface|abstract class|trait) (\w+) /m', $code, $match_class);
if (isset($match_class[2])) {
$this_class = $match_class[2];
}
$pat_this_ns = '/namespace (.*?);/';
preg_match('/namespace (.*?);/', $code, $match_this_ns);
if (isset($match_this_ns[1])) {
$this_ns = $match_this_ns[1];
}
} catch (Throwable $e) {
$code = false;
}
} else {
$code = false;
}
?>
クエリ文字列ns
の名前空間を含めたクラス名を取得して、ReflectionClass
クラスを使ってクラスのファイルパスを取得し、$code
にコードを代入します。
そして正規表現を使い$this_class
に名前空間を除いたクラス名を代入します。
new ReflectionClass()'でエラーが起きた場合は
$codeに
false`を代入します。
<div class="namespace">
<form action="<?php basename(__FILE__) ?>" method="GET">
<input type="text" name="ns" value="<?php if (isset($namespace)) echo $namespace ?>" size="60" class="ns">
<button>SHOW</button>
</form>
</div>
HTMLのテキストボックスにです。
クエリ文字列ns
に文字列があればここに表示されます。
if ($code === false) {
$code = "invalid namespace";
goto output;
}
$code = htmlspecialchars($code);
$code
がfalse
ならgoto文でoutput:
に飛びます。
そうでなければhtmlspecialchars()
でHTML エンティティ化します。
//replace uses
preg_match('/namespace[\s][A-Za-z\\\]+?;[\s\S]+?(class|interface|abstract class|trait)/', $code, $match_uses_str);
$uses_str = $match_uses_str[0];
正規表現でuse
文を取得します。
preg_match_all('/use ([a-z\\\]+?);/i', $uses_str, $match_uses);
$classes_with_ns = $match_uses[1];
use
文からas
を使っていない名前空間を含めたクラス名を取得し$classes_with_ns
配列に入れます。
$replace_uses_str = $uses_str;
$class_arr = [];
foreach ($classes_with_ns as $class_with_ns) {
try {
$path = (new ReflectionClass($class_with_ns))->getFileName();
if ($path) {
$split = explode('\\', $class_with_ns);
$end = end($split);
$class_arr[$end] = $class_with_ns;
$replace_uses_str = preg_replace('/'.preg_quote($class_with_ns).';/', "<a href=\"$url?ns=$class_with_ns\">$class_with_ns</a>;", $replace_uses_str);
}
} catch (Exception $e) {}
}
$code = str_replace($uses_str, $replace_uses_str, $code);
$replace_uses_str = $uses_str;
で同じ文字列を作ります。
置換する時に$uses_str
で検索し、$replace_uses_str
に置換します。
そして$classes_with_ns
に入っているクラス名を置換していきます。
new ReflectionClass()=.getFileName()
でファイルがあるクラスだけ置換します。
`$class_arr'は[クラス名 => 名前空間を含めたいクラス名]の配列です。
これは他の部分でクラス名を置換するのに使います。
//replace uses with as
preg_match_all('/use (([A-Za-z\\\]+?) as ([A-Za-z\\\]+?));/', $replace_uses_str, $match_use_as);
$all_ns_alias_arr = [];
for ($i = 0; $i < count($match_use_as[0]); $i++) {
$all_ns_alias_arr[] = array_column($match_use_as, $i);
}
$all_ns_alias_arr = array_map(function($arr) {
return [
"all" => $arr[1],
"ns" => $arr[2],
"alias" => $arr[3]
];
} ,$all_ns_alias_arr);
$alias_ns = array_combine(
array_column($all_ns_alias_arr, 'alias'),
array_column($all_ns_alias_arr, 'ns')
);
as
でエイリアス化したクラス名を置換するための配列を作ります。
中身は[エイリアス => 名前空間を含めたクラス名]になっています。
$replace_use_with_as_str = $replace_uses_str;
foreach ($all_ns_alias_arr as $all_ns_alias) {
try {
$path = (new ReflectionClass($all_ns_alias['ns']))->getFileName();
if ($path) {
$replace_use_with_as_str = str_replace($all_ns_alias['all'], "<a href=\"$url?ns=$all_ns_alias[ns]\">$all_ns_alias[all]</a>", $replace_use_with_as_str);
}
} catch (Exception $e) {}
}
$code = str_replace($replace_uses_str, $replace_use_with_as_str, $code);
先ほどと同じように置換していきます。
//replace extends and implements
preg_match('/^(class|interface|abstract class|trait) \w+? (extends|implements) .+[\r\n]/m', $code, $match_ext_imp);
if (!empty($match_ext_imp[0])) {
$ext_imp_str = $match_ext_imp[0];
preg_match_all('/\\\?([A-Z][a-z]+\\\?)+/', $ext_imp_str, $match_exts_imps);
if (!empty($exts_imps = $match_exts_imps[0])) {
$idx = array_search($this_class, $exts_imps);
if ($idx !== false) {
array_splice($exts_imps, $idx, 1);
}
$replace_ext_imp_str = $ext_imp_str;
foreach ($exts_imps as $ext_imp) {
if (isset($class_arr[$ext_imp])) {
$replace_ext_imp_str = preg_replace('/ '.$ext_imp.'( |,|[\r\n])/', " <a href=\"$url?ns=$class_arr[$ext_imp]\">$ext_imp</a>$1", $replace_ext_imp_str);
} elseif (isset($alias_ns[$ext_imp])) {
$replace_ext_imp_str = preg_replace('/ '.$ext_imp.'( |,|[\r\n])/', " <a href=\"$url?ns=$alias_ns[$ext_imp]\">$ext_imp</a>$1", $replace_ext_imp_str);
} else {
try {
$ext_imp_with_ns = $this_ns.'\\'.trim($ext_imp, '\\');
if ((new ReflectionClass($this_ns.'\\'.trim($ext_imp, '\\')))->getFileName()) {
$replace_ext_imp_str = str_replace($ext_imp, "<a href=\"$url?ns=$ext_imp_with_ns\">$ext_imp</a>", $replace_ext_imp_str);
}
} catch (Exception $e) {}
}
}
$code = str_replace($ext_imp_str, $replace_ext_imp_str, $code);
}
}
親クラスとインターフェースの置換です。
正規表現でクラス名を取得し配列に入れて{foreachで回します。
use`文にあるクラス以外は、表示しているファイルの名前空間を使ってファイルが存在すれば置換します。
クラスメソッドのドキュメンテーション以外は同じように置換します。
//replace doc
preg_match_all('/\/\*\*[\s\S]+?\*\//', $code, $match_docs_str);
if (isset($match_docs_str[0])) {
$docs_str = $match_docs_str[0];
foreach ($docs_str as $doc_str) {
preg_match_all('/@\w+? +(\\\?([A-Z][a-z]+\\\?)+)/', $doc_str, $match_classes_with_ns);
if (isset($match_classes_with_ns[1])) {
$classes_with_ns = $match_classes_with_ns[1];
$classes_with_ns = array_unique($classes_with_ns);
$replace_doc = $doc_str;
foreach ($classes_with_ns as $class_with_ns) {
try {
if ((new ReflectionClass($class_with_ns))->getFileName()) {
$replace_doc = str_replace($class_with_ns, "<a href=\"$url?ns=$class_with_ns\">$class_with_ns</a>", $replace_doc);
$split = explode('\\', $class_with_ns);
$end = end($split);
$class_arr[$end] = $class_with_ns;
}
} catch (Exception $e) {}
}
$code = str_replace($doc_str, $replace_doc, $code);
}
}
}
PDR-4に準拠したドキュメンテーションには完全修飾クラス名が書いてあるので$class_arr
と$alias_ns
を条件分岐に使いません。
完全修飾クラス名を配列に入れて、foreach
で回して置換します。
置換可能なものを全て置換したら出力します。
<?php
//print dir path
if (isset($this_file)) {
$dir = dirname($this_file);
echo "<span class=\"dir\">$dir</span>";
}
?>
名前空間が分からないクラスを探すために、表示しているファイルのディレクトリのパスを出力します。
JavaScriptで、クリックするとクリップボードにコピーできるようになっています。
<div>
<table>
<?php
//print code with row num
$lines = explode("\n", $code);
$count = count($lines);
$i = 1;
foreach ($lines as $line) {
echo '<tr><td><pre><span class="num">'.($i++).'<span></pre></td><td><pre>'.$line.'</pre></td></tr>';
}
$div_width = (strlen($count) + 1) * 9;
?>
</table>
</div>
table
タグで行番号と一緒にコードを出力します。
行番号はCSSで選択できないようなっているので、コードだけをコピーできるようになっています。
参考
PHPで現在アクセスされているページのURLを取得する
JavaScript でテキストをクリップボードへコピーする方法