PHPで代理クラス(プロキシクラス)を使って、PHP_CodeSnifferでのERRORをWARNINGとして扱う。

※一般化して例を出したいけどちょっと面倒なので、PHP_CodeSnifferに限った例を出します。


警告は出したいけどERRORほどではない、ってときにWARNINGにしたいのだけど、
PHP_CodeSniffer自体にそういう機能が無いので、自前で何とかしないといけない。

何も考えずに実装しようとすると、該当メソッドをオーバーライドして"addError"の部分を"addWarning"に書き換えるなんてやってしまうのだけど、

<?php
// 親クラス
class Generic_Sniffs_SomeSniff
{
    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) {
        // 長い長い記述A
        $phpcsFile->addError($error, $stackPtr);
        // 長い長い記述B
    }
}

// 自前クラス
class My_Sniffs_SomeSniff extends Generic_Sniffs_SomeSniff
{
    // override
    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) {
        // 長い長い記述Aのコピー
        $phpcsFile->addWarning($error, $stackPtr);
        // 長い長い記述Bのコピー
    }
}

ってなってしまって、これは本当にお寒い。
DRYに反してるから親クラスに変更があったときに対応するのが非常に面倒。


$phpcsFile->addErrorを呼んだ時に代わりに$phpcsFile->addWarningを呼んでくれれば良いだけなので、どうにかしてaddErrorを書き換えられないかな、と。
以前色々試してPHPでは無理っぽいというのは分かっていたので、ラッパーかませたら何とかなるんじゃないかと思ってやってみたら、とりあえず何とかなった。

<?php
// 親クラス
class Generic_Sniffs_SomeSniff
{
    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) {
        // 長い長い記述A
        $phpcsFile->addError($error, $stackPtr);
        // 長い長い記述B
    }
}

// PHP_CodeSniffer_Fileの代理クラス
class PHP_CodeSniffer_File_WarningProxy extends PHP_CodeSniffer_File
{
    private $ref;
    public function __construct(PHP_CodeSniffer_File $phpcsFile) {
        $this->ref = $phpcsFile;
    }
    // method proxy
    private function _proxy()
    {
        $backtrace = debug_backtrace();
        return call_user_func_array(array($this->ref, $backtrace[1]['function']), $backtrace[1]['args']);
    }
    // overrides
    public function setActiveListener() {return $this->_proxy();}
    public function addTokenListener() {return $this->_proxy();}
    public function removeTokenListener() {return $this->_proxy();}
    public function getTokens() {return $this->_proxy();}
    public function start() {return $this->_proxy();}
    public function cleanUp() {return $this->_proxy();}
    public function addError($error, $stackPtr, $code='')
    {
        return $this->ref->addWarning($error, $stackPtr, $code);
    }
    public function addWarning() {return $this->_proxy();}
    public function getErrorCount() {return $this->_proxy();}
    public function getWarningCount() {return $this->_proxy();}
    public function getIgnoredLines() {return $this->_proxy();}
    public function getErrors() {return $this->_proxy();}
    public function getWarnings() {return $this->_proxy();}
    public function getFilename() {return $this->_proxy();}
    public function getDeclarationName() {return $this->_proxy();}
    public function isAnonymousFunction() {return $this->_proxy();}
    public function getMethodParameters() {return $this->_proxy();}
    public function getMethodProperties() {return $this->_proxy();}
    public function getMemberProperties() {return $this->_proxy();}
    public function isReference() {return $this->_proxy();}
    public function getTokensAsString() {return $this->_proxy();}
    public function findPrevious() {return $this->_proxy();}
    public function findNext() {return $this->_proxy();}
    public function findFirstOnLine() {return $this->_proxy();}
    public function hasCondition() {return $this->_proxy();}
    public function findExtendedClassName() {return $this->_proxy();}
}

// 自前クラス
class My_Sniffs_SomeSniff extends Generic_Sniffs_SomeSniff
{
    // override
    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) {
        return parent::process(new PHP_CodeSniffer_File_WarningProxy($phpcsFile), $stackPtr);
    }
}

これで、長い長い記述Aのコピーと長い長い記述Bのコピーを書かなくて済むし、親クラスに変更があった場合にも対応不要というわけで素晴らしす。

  • 外部に公開されているインターフェイスは全て委譲しないといけないので、全メソッドオーバーライドという虚しいことしてる。
    マジックメソッド__call使ったら一発で対応できるかなと思ったけど、親クラスに定義されてたらそっちが呼ばれちゃって__callが呼ばれなかった。
  • public function getFilename() {$this->getFilename();}とか書くのがかったるいので、backtrace使って楽した(function _proxy)
  • オーバーライドしてるメソッドの引数は紙面の都合上全部省略してる。E_NOTICE大量に出るから引数書いておいても良い。
  • 実は手元のコードでは全てのメソッドを上書きできるようになってるけど、紙面の都合上割愛。また後日掲載する予定。