いけないとわかっていても時間の関係や取りあえずという誘惑からコードをコピーしてしまい、リファクタリングの際にはどこをどうしたのかわからなくなってしまって、そのまま放置してしまうというのはよくあったりもします。
また、コードレビューの際にも「これと似たような処理をどこかで見かけたな」と思うこともよくあったりして、そういったPHPの冗長なコードを効率的に発見してくれるツールがphpcpdです。
phpcpdをインストールする
phpcpdはPEARで提供されているのでpearコマンドでさっくりインストール、といきたいのですが自分の環境だと結構めんどくさかったです。
ちなみに、PEAR自信のバージョンが1.9.4以上を求められますので、環境が合わない場合は
# pear upgrade PEAR
とかしてPEARのバージョンをあげておきましょう。
最初に、PEARのチャネルを登録して、phpcpdをインストールします。
# pear channel-discover pear.phpunit.de # pear install phpunit/phpcpd Unknown remote channel: components.ez.no Unknown remote channel: pear.netpirates.net Unknown remote channel: pear.symfony.com phpunit/phpcpd requires package "channel://components.ez.no/ConsoleTools" (version >= 1.6) phpunit/FinderFacade requires package "channel://pear.netpirates.net/fDOMDocument" (version >= 1.3.1) phpunit/FinderFacade requires package "channel://pear.symfony.com/Finder" (version >= 2.1.0) downloading PHP_Timer-1.0.4.tgz ... Starting to download PHP_Timer-1.0.4.tgz (3,694 bytes) ....done: 3,694 bytes install ok: channel://pear.phpunit.de/PHP_Timer-1.0.4
しかし、上記のようにPHP_Timerはインストールされていますが、phpcpdはインストールされません。
幾つか必要なパッケージが要るみたいなのでそれを先にインストールします。
といっても、新たなチャネルを登録してphpcpdのインストールをリトライをしてみます。
# pear channel-discover components.ez.no # pear channel-discover pear.netpirates.net # pear channel-discover pear.symfony.com # pear install phpunit/phpcpd downloading phpcpd-1.4.0.tgz ... Starting to download phpcpd-1.4.0.tgz (10,439 bytes) .....done: 10,439 bytes downloading FinderFacade-1.0.5.tgz ... Starting to download FinderFacade-1.0.5.tgz (4,498 bytes) ...done: 4,498 bytes downloading fDOMDocument-1.3.2.tgz ... Starting to download fDOMDocument-1.3.2.tgz (13,907 bytes) ...done: 13,907 bytes install ok: channel://pear.netpirates.net/fDOMDocument-1.3.2 install ok: channel://pear.phpunit.de/FinderFacade-1.0.5 install ok: channel://pear.phpunit.de/phpcpd-1.4.0
これでようやくphpcpdがインストールされました。
インストールするとphpcpdコマンドが利用可能になっています。
# which phpcpd /usr/bin/phpcpd
おまけ的な内容ですが、phpcpdを実行した際に下記のようなエラーが出た場合は、必要なパッケージがインストールされていないか、単純にPEARディレクトリにパスが通ってないためだと思われます。
$ phpcpd PHP Warning: require(SebastianBergmann/PHPCPD/autoload.php): failed to open stream: No such file or directory in /usr/bin/phpcpd on line 52 PHP Fatal error: require(): Failed opening required 'SebastianBergmann/PHPCPD/autoload.php' (include_path='.:/var/www/lib:/usr/local/lib/php') in /usr/bin/phpcpd on line 52
後者の場合は、php.iniのinclude_pathにPEAR用のディレクトリを追加しておきましょう。
PEARディレクトリは、pear config-showで確認できます。
phpcpdの使い方
準備ができたところで、早速phpcpdの使い方を見ていきます。
phpcpdは基本的に対象ファイルを引数に指定すれば重複コードのチェックを行ってくれますが、幾つかのオプションを指定するとチェックする内容を柔軟に変更できます。
$ phpcpd --min-lines 5 --min-tokens 5 foo.php phpcpd 1.4.0 by Sebastian Bergmann. Found 1 exact clones with 11 duplicated lines in 1 files: - /home/work/workspace/foo.php:7-18 /home/work/workspace/foo.php:28-39 25.58% duplicated lines out of 43 total lines of code. Time: 0 seconds, Memory: 2.00Mb
上記は、foo.phpを解析するように指定し、その結果7行目から18行目と28行目から39行目に重複しているコードがあるという判定が出ています。
実際にチェックしてみたソースコードはこちら。
<?php function getId() { $conn = pg_connect("host=localhost port=5432 dbname=fuga"); $result = pg_query($conn, "SELECT id FROM dogss"); if (!$result) { echo "Error!.\n"; exit; } $arr = pg_fetch_all($result); $cnt = count($arr); for ($i = 0; $i < $cnt; $i++) { $key = key($arr[$i]); $data[$key][$i] = $arr[$i][$key]; } pg_close($conn); return $data; } function getName() { $conn = pg_connect("host=localhost port=5432 dbname=fuga"); $result = pg_query($conn, "SELECT name FROM dogs"); if (!$result) { echo "Error!.\n"; exit; } $arr = pg_fetch_all($result); $cnt = count($arr); for ($i = 0; $i < $cnt; $i++) { $key = key($arr[$i]); $data[$key][$i] = $arr[$i][$key]; } pg_close($conn); return $arr; }
DBから値を取得するメソッドが2つありますが、実質違うのはメソッド名とSQLだけです。
であれば、結構判定が漏れているのではないかと思うかもしれませんが、あまり細かいレベルで重複コードをチェックしていると、例えばIF文の処理一つでも冗長と判定されかねません。
phpcpdでは、その辺の調整を「--min-lines」オプション(指定の行以下の重複は無視する)と「--min-tokens」オプション(指定のPHPトークンの数以下は無視する)というオプションでできます。
先ほどの例だと、5行以下の重複とPHPトークンの数が5以下の該当コードは無視しています。
ですので、最初のpg_connectや最後のpg_closeやreturnの行は判定されていません。
ちなみに、PHPトークンはPHPコードの字句解析の単位でtoken_get_all メソッドで調べることができたりします。
先ほどのfoo.phpのコードのPHPトークンを調べてみると、
<?php $file = file_get_contents("./foo.php"); $tokens = token_get_all($file); var_dump(count($tokens));
表示される数は「282」です。
これは、コード全体でのPHPトークンですので、phpcpdの解析ではそれが「--min-lines」オプションを超える行数で、「--min-tokens」を超えるトークン数で一致していたら重複コードと判定されているようです。
これらのオプションは、デフォルトで「--min-lines」が5、「--min-tokens」が70となっているので、少し判定が厳しいかもしれません(これはソースコードの量にもよるので状況によって調整が必要でしょうが)。
あと、phpcpdは特定のPHPファイルを指定しなければならないというわけではなく、ディレクトリ単位や特定の2つのPHPファイルを解析対称にするということもできたりします。
# カレントディレクトリ以下を全て解析 $ phpcpd ./* # foo.phpとbar.phpの2つを解析する $ phpcpd foo.php bar.php
デフォルトでは、拡張子が「.php」のもののみが解析対象になるので、それを変更したい場合は「--names」オプションで拡張子を指定できます。
その他にも解析対象外の指定や重複コード部分を表示してくれるというようなオプションもあるので、ヘルプもあわせて参照してください。
$ phpcpd phpcpd 1.4.0 by Sebastian Bergmann. Usage: phpcpd [switches] <directory|file> ... --log-pmd <file> Write report in PMD-CPD XML format to file. --min-lines <N> Minimum number of identical lines (default: 5). --min-tokens <N> Minimum number of identical tokens (default: 70). --exclude <dir> Exclude <dir> from code analysis. --names <names> A comma-separated list of file names to check. (default: *.php) --help Prints this usage information. --version Prints the version and exits. --progress Show progress bar. --quiet Only print the final summary. --verbose Print duplicated code.
自分自身でコードのチェックをしてみたり、コードレビューで冗長な書き方をしていないかをチェックしたいというときに作業が効率的に行えるのではないでしょうか。
[PR]
[PR]
関連記事
[Composer] PHPのパッケージ管理にComposerを使う
PHPのセッションアダプションによって任意のセッションIDが受入れられる
PHP_CodeSnifferでコーディング規約に準拠しているかチェックをする
[PHP] 余計なHTMLタグや属性を消してくれるHTML Purifier
ノンプログラミングでも利用可能!PHPで多様なグラフを作れるpChart
PHPのプログラムをデーモンとして動かしてくれるPEAR::System_Daemon
[PEAR] オリジナルのWikiエンジンを作れるText_Wiki
BOM付きUTF-8のPHPファイルからBOMだけを一度に削除するスクリプト