sfp-phpstan-psr-log を用いた psr/log ロガー利用への検証
struggle-for-php/sfp-phpstan-psr-log (以下、sfp-phpstan-psr-log ) は PHPStan 用のエクステンションです。
PSR-3: Logger Interface - PHP-FIG で定められたロガー利用での静的解析をおこないます。
インストール
composer require --dev struggle-for-php/sfp-phpstan-psr-log
phpstan.neon
に以下の設定を追加します。
includes:
- vendor/struggle-for-php/sfp-phpstan-psr-log/extension.neon
- vendor/struggle-for-php/sfp-phpstan-psr-log/rules.neon
phpstan/extension-installer を利用している場合は、上記の設定追加は必要ありません。
機能
sfp-phpstan-psr-log は現在、stubの含有とルールにより以下のチェックが行えます。
- コンテキスト配列での
exception
に設定されているのは例外オブジェクトか -
LoggerInterface::log()
メソッド利用時の第1引数はLogLevels::*
のものか- emergency, alert, critical, error, warning, notice, info, debug のどれかになっているか
- プレスホルダーの文字列が psr-3 で定められた文字列か
- プレスホルダー名とコンテキストキーが揃っているか
- コンテキスト配列のキーは文字列か
- 例外インスタンスがスコープ内にあった場合にコンテキスト
exception
に設定されているか(例外のロギング忘れ防止)
なお、stubは psr/log のバージョン 1, 2, 3 すべてに対応しています。
これより以下で具体例を説明していきますが、変数 $logger
は Psr\Log\LoggerInterface
インスタンスであることを省略させていただきます。
exception
に設定されているのは例外オブジェクトか
コンテキスト配列での psr-3 では以下の指定があります。
Implementors MUST still verify that the 'exception' key is actually an Exception before using it as such, as it MAY contain anything.
つまり、exception
キーには 例外オブジェクトを設定しないといけません。
以下、指摘される例。
<?php
use Psr\Log\LoggerInterface;
class Foo
{
private LoggerInterface $logger;
public function anyAction()
{
try {
//
} catch (\Exception $e) {
$this->logger->error('error happen.', ['exception' => 'foo']);
}
}
}
vendor/bin/phpstan analyse
Note: Using configuration file /tmp/your-project/phpstan.neon.
2/2 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
------ -------------------------------------------------------------
Line Demo.php
------ -------------------------------------------------------------
15 Parameter #2 $context of method Psr\Log\LoggerInterface::error() expects array()|array('exception' => Exception), array('exception' => 'foo') given.
------ -------------------------------------------------------------
[ERROR] Found 1 error
よくある誤用例としては、 ['exception' => $e->getMessage()]
が挙げられます。
プレスホルダーの文字列がpsr-3 で定められた文字列か
psr-3 では以下の通りです。
Placeholder names SHOULD be composed only of the characters
A-Z
,a-z
,0-9
, underscore_
, and period.
. The use of other characters is reserved for future modifications of the placeholders specification.
つまり、空白や -
なども含めてプレスホルダーに指定することはできません。
$logger->info('message has { space } .', [' space ' => 'bar']);
の場合、以下のような指摘が行われます。
Parameter $message of logger method Psr\Log\LoggerInterface::info() has braces. But it includes invalid characters for placeholder. - { space }
プレスホルダー名とコンテキストキーが揃っているか
Placeholder names MUST correspond to keys in the context array.
プレスホルダーがメッセージ文字列にもかかわらず、それと対となるキー名が 第2引数での配列 (log()
メソッドの場合は第3引数) で指定されてない場合のエラーです。
$logger->info('message has {nonContext} .');
$logger->info('message has {notMatched} .', ['foo' => 'bar']);
Parameter $context of logger method Psr\Log\LoggerInterface::info() is required, when placeholder braces exists - {nonContext}
Parameter $message of logger method Psr\Log\LoggerInterface::info() has placeholder braces, but context key is not found against them. - {notMatched}
コンテキストに対となるキーがないと置き換えができないためエラーです。
exception
に設定されているか(例外のロギング忘れ防止)
例外インスタンスがスコープ内にあった場合にコンテキスト このルールは、以下のツイートからインスパイアされ作成されました。
以下の通り、例外がカレントスコープにある場合は、コンテキスト配列にexception
がない場合はエラーとして検出されます。
try {
//
$logger->info('何かのメモ'); // OK
//
} catch (LogicException $exception) {
$logger->alert("エラーが発生しました。"); // NG
$logger->alert("エラーが発生しました。", ['exception' => $exception]); // OK
}
exception
に Throwable
は含まれるのか
補足1 - psr-3 における はい、含まれます。ご承知の通り、PHP 7であとづけとして Exception の基底インターフェイスとして Throwable
が誕生しており、
PHP 5時代に psr-3 が策定されたため、Exception
以下でなければならないのか exception
に Throwableも許容されるか迷った方もいるかもしれません。
このため、sfp-phpstan-psr-log作者は、PHP-FIG のメーリングリストに連絡し、exception
には Throwable
が含まれることで認識相違ないこと確認しました。
Monolog などの PHP 7対応しているロガーライブラリならば、$context['exception'] instanceof \Throwable
として扱われているはずです。
補足2 - Monolog でのプレスホルダー利用
psr-3 のEditor(提案者)は、Jordi Boggiano こと Monolog の作者 Seldaek です。
しかしながら、Monolog ではデフォルトでプレスホルダーが有効になっていません。
以下の通り、プロセッサーを登録しておく必要があります。
$handler->pushProcessor(new \Monolog\Processor\PsrLogMessageProcessor());
プレスホルダーがデフォルトで有効になっていない点について、今年の2月にCrellがブログに見解を記載してましたので、そちらを転載します。
In Monolog, that interpolation doesn't happen by default. That's sensible, in a way, because you only want to run that interpolation when writing to the log file sometimes; if you're writing to a database for later display in HTML, for instance, you want to do that interpolation later on display, not when writing out to the log. However, it does mean that the default behavior often does not include it. You need to enable the PsrLogMessageProcessor yourself.
補足3 - Laravel/larastan について
Laravel を利用するアプリケーションに対して phpstan を実行する場合、エクステンションである larastan を併用するのが一般的かと思います。
残念なことに最新版でのスタブファイルでは、stubs/Log/Logger.stub
にて
<?php
namespace Psr\Log {
interface LoggerInterface {}
}
namespace Illuminate\Log {
/**
と Psr\LogLoggerInterface
が定義されてしまっておりスタブファイルを両立できない、という問題が判明しています。
※ 各ルールはスタブの存在が前提となっていませんので、phpstan.neon
への個別のルール指定は可能です。
Discussion