🍐

sfp-phpstan-psr-log を用いた psr/log ロガー利用への検証

2023/05/02に公開

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 すべてに対応しています。

これより以下で具体例を説明していきますが、変数 $loggerPsr\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.

https://www.php-fig.org/psr/psr-3/#13-context

つまり、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.

https://www.php-fig.org/psr/psr-3/#12-message

つまり、空白や - なども含めてプレスホルダーに指定することはできません。

$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.

https://www.php-fig.org/psr/psr-3/#12-message

プレスホルダーがメッセージ文字列にもかかわらず、それと対となるキー名が 第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
}

補足1 - psr-3 における exceptionThrowable は含まれるのか

はい、含まれます。ご承知の通り、PHP 7であとづけとして Exception の基底インターフェイスとして Throwableが誕生しており、
PHP 5時代に psr-3 が策定されたため、Exception 以下でなければならないのか exception に Throwableも許容されるか迷った方もいるかもしれません。
このため、sfp-phpstan-psr-log作者は、PHP-FIG のメーリングリストに連絡し、exception には Throwableが含まれることで認識相違ないこと確認しました。
https://groups.google.com/g/php-fig/c/nnwDWSFmij8

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.

https://peakd.com/hive-168588/@crell/using-psr-3-placeholders-properly

補足3 - Laravel/larastan について

Laravel を利用するアプリケーションに対して phpstan を実行する場合、エクステンションである larastan を併用するのが一般的かと思います。
残念なことに最新版でのスタブファイルでは、stubs/Log/Logger.stub にて

<?php

namespace Psr\Log {
    interface LoggerInterface {}
}

namespace Illuminate\Log {

    /**

Psr\LogLoggerInterface が定義されてしまっておりスタブファイルを両立できない、という問題が判明しています。
https://github.com/nunomaduro/larastan/blob/v2.2.0/stubs/Log/Logger.stub

※ 各ルールはスタブの存在が前提となっていませんので、phpstan.neon への個別のルール指定は可能です。

Discussion