「PHP tool」の版間の差分

提供:senooken JP Wiki
(古いスタイルのPHPの改善)
(Methods @throws)
 
(同じ利用者による、間の19版が非表示)
79行目: 79行目:
  {@tag value}
  {@tag value}


==== @return ====
==== inheritance ====
[https://docs.phpdoc.org/guide/references/phpdoc/basic-syntax.html phpDocumentor]
 
以下の継承関係、グループがある。
{| class="wikitable"
!Elements
!Inherited tags
|-
|''Any''
|[[/docs.phpdoc.org/guide/references/phpdoc/tags/author.html|@author]], [[/docs.phpdoc.org/guide/references/phpdoc/tags/version.html|@version]], [[/docs.phpdoc.org/guide/references/phpdoc/tags/copyright.html|@copyright]]
|-
|''Classes and Interfaces''
|[[/docs.phpdoc.org/guide/references/phpdoc/tags/category.html|@category]], [[/docs.phpdoc.org/guide/references/phpdoc/tags/package.html|@package]], [[/docs.phpdoc.org/guide/references/phpdoc/tags/subpackage.html|@subpackage]]
|-
|''Methods''
|[[/docs.phpdoc.org/guide/references/phpdoc/tags/param.html|@param]], [[/docs.phpdoc.org/guide/references/phpdoc/tags/return.html|@return]], [[/docs.phpdoc.org/guide/references/phpdoc/tags/throws.html|@throws]]
|-
|''Properties''
|[[/docs.phpdoc.org/guide/references/phpdoc/tags/var.html|@var]]
|}
 
==== Methods ====
 
===== @return =====
[https://docs.phpdoc.org/guide/references/phpdoc/tags/return.html#return phpDocumentor]
[https://docs.phpdoc.org/guide/references/phpdoc/tags/return.html#return phpDocumentor]


89行目: 112行目:


ただ、関数の@returnでreturnが何を返すのかを書いた方がいい。
ただ、関数の@returnでreturnが何を返すのかを書いた方がいい。
===== @throws =====
例外が発生する場合に記載する。複数の種類の例外がありえるなら、その数だけ@throwsを記載する。


==== 注意喚起 ====
==== 注意喚起 ====
242行目: 268行目:
この方法を使わない場合、同じプロパティーを初期値指定で再定義しないといけない。@propertyで指定するとそれを回避できる。
この方法を使わない場合、同じプロパティーを初期値指定で再定義しないといけない。@propertyで指定するとそれを回避できる。


==phpDocumentor==
=== phpDocumentor ===
Ref: [https://phpdoc.org/ Home | phpDocumentor].
Ref: [https://phpdoc.org/ Home | phpDocumentor].


425行目: 451行目:
  <code>composer update monolog/monolog</code>
  <code>composer update monolog/monolog</code>
Composerでインストール可能な主なパッケージは,Packagistリポジトリーに配置されている。
Composerでインストール可能な主なパッケージは,Packagistリポジトリーに配置されている。
====自動読み込み (Autoloading)====
====Autoloading (自動読み込み )====
 
===== About =====
[https://getcomposer.org/doc/01-basic-usage.md#autoloading Basic usage - Composer]
ライブラリーの自動読み込みのために,Composerは<code>vendor/autoload.php</code>ファイルを生成する。以下のように,ライブラリーのクラスを使用するファイルの先頭でこのファイルを読み込めば使えるようになる。
ライブラリーの自動読み込みのために,Composerは<code>vendor/autoload.php</code>ファイルを生成する。以下のように,ライブラリーのクラスを使用するファイルの先頭でこのファイルを読み込めば使えるようになる。
  <php
  <php
433行目: 462行目:
  $log->pushHandler(new Monolog\Handler\StreamHandler('app.log', Monolog\Logger::WARNING));
  $log->pushHandler(new Monolog\Handler\StreamHandler('app.log', Monolog\Logger::WARNING));
  $log->addWarning('Foo');
  $log->addWarning('Foo');
composer.jsonの<code>autoload</code>欄に指定することで,ライブラリー以外の自分のコードも自動読み込みの対象にできる。
このほかに、composer.jsonの<code>autoload</code>欄に指定することで,ライブラリー以外の自分のコードも自動読み込みの対象にできる。
{
    "autoload": {
        "psr-4": {"Acme\\": "src/"}
    }
}
 
===== PSR-4 =====
[https://www.php-fig.org/psr/psr-4/ PSR-4: Autoloader - PHP-FIG]
psr-4はPHPの標準仕様。
名前空間、クラスメイト、ファイルパスとの対応関係を記している。


# 名前空間の単位ごとに、ディレクトリーに対応。
# 大文字小文字は区別。
# 大文字小文字を区別して、ファイル名は.phpの拡張子で終わる。
===== dump-autoload =====
composer.jsonを編集した場合、<code>composer dump-autoload</code>を実行して<code>vendor/autolaod.php</code>を必ず更新します。
composer.jsonを編集した場合、<code>composer dump-autoload</code>を実行して<code>vendor/autolaod.php</code>を必ず更新します。


670行目: 714行目:
改善手順がある。
改善手順がある。


=== php -l ===
# 静的解析ツール (php -l/phpstan)
# 仕様化テスト (phpunit)
# リファクタリング (関数抽出)
# リファクタリング (クラス化)
# 単体テスト


* [https://ja.wikipedia.org/wiki/%E9%9D%99%E7%9A%84%E3%82%B3%E3%83%BC%E3%83%89%E8%A7%A3%E6%9E%90 静的コード解析 - Wikipedia]
===== 仕様化テスト/Characterization test =====
* [https://www.php.net/manual/ja/features.commandline.options.php PHP: オプション - Manual]
[https://maku.blog/p/p6awy3z/ 読書メモ『レガシーコード改善ガイド』マイケル・C・フェザーズ|まくろぐ]
 
特徴付けテストと直訳することもあるらしい。マイケル・C・フェザーズの2009年翻訳のレガシーコード改善ガイドで提唱された内容。著者の独自の概念。


-l/--syntac-checkオプションで構文チェックのみを行う。-lはlintのlだと思われる。
ドキュメントや仕様が不明瞭なコードを扱う際に有効。既存コードの動作を把握し、そのふるまいを固定化するための手法。


成功したらNo syntax errors detected in <filename> が標準出力に書き込まれ、リターンコードは 0
現状コードが今何をするのかをテストで記録する。リファクタリングとかをする前に現在の動作をある程度保証するテストを記載することで、意図しない影響を防げる。


失敗した場合、テキスト Errors parsing <filename> に加え、内部パーサエラーメッセージ が標準出力に書き込まれ、シェルリターンコードは、 -1 となります。
例えば、昔ながらの、phpファイルがそのまま応答を返すタイプのアプリの場合。


このオプションは、(未定義の関数のような)致命的なエラー(fatal error) はみつけません。致命的なエラーについても調べたい場合は、 -f を使用してください。
環境変数を設定して、バッファリングで、文字列の有無で、ある程度現在のふるまいをテストできる。


-lは-r (コードの実行)とは併用不能。以下のコードで全phpファイルをチェックできる。
PHPUnitでやる場合。
  for i in `find . -name \*.php`; do php -l $i | grep -v "No syntax errors"; done
<?php declare(strict_types=1);
上記コードより、以下の方が少し早い。
  find . -name \*.php -exec php -l {} \; | grep -v '^No syntax errors'
  namespace Tests\Unit\Service\Resign;
外部ツールも不要で手軽で悪くない。特に、-lで実行するphpのバージョンを変えるだけでチェックでき、PHPのバージョンアップ時の事前の簡易チェック、粗探しとしてとして悪くない ([https://tech-blog.rakus.co.jp/entry/20220922/php PHPerのための「静的解析」を語り合う【PHP TechCafe イベントレポート】 - RAKUS Developers Blog | ラクス エンジニアブログ])
  final class SsResignConfirmTest extends \Tests\Unit\BaseTestCase
{
    public function testBaseline(): void
    {
        $this->assertBaseline('service/resign/ss_resign_confirm.php');
    }
}


ただ、php -lは構文エラーのみで、型チェックはできない。PHPStanを使うしかない模様。
  <?php declare(strict_types=1);
 
gitのpre-commitに登録する場合、以下のような内容にするとよい。
  #!/bin/sh
## Lint added/modified PHP file.
set -eu
has_error=false
PHP_FILES=$(git diff --staged --name-only --diff-filter=ACM | grep '\.php$' || :)
[ -z "$PHP_FILES" ] && exit || :
   
   
  while read -r file; do
  namespace Tests\Unit;
php -l "$file" | grep -v '^No syntax errors' && has_error=true
done <<-EOT
$PHP_FILES
EOT
$has_error && exit 1 || :
   
   
  ./vendor/bin/phpstan analyze --memory-limit 5G $PHP_FILES || has_error=true
  /**
$has_error && exit 1 || :
  * 単体テストのベースクラス。
# parameters.pathsのコメントアウトを解除して全体解析する場合、上記のファイル単位のphpstanをやめて、以下の全体解析を実行する。
  *
  # ./vendor/bin/phpstan analyze --memory-limit 5G || exit 1
  * tests/Unit/以下はこのクラスを継承して実装する。基本は、継承先の子クラスのtestBaselineでassertBaselineを呼び出す。
  *
  * 他に、テストで共通で行うようなものがあれば、こちらに記述して共通化していく。
  */
abstract class BaseTestCase extends \PHPUnit\Framework\TestCase
{
    /**
      * 仕様化テスト (characterization test) として、該当ファイルの応答にエラーがないことを確認する共通テストメソッド。
      *
      * 継承先の子クラスのtestBaseline内で `$this->assertBaseline('path');` で実行想定。
      * @param string $relative_include_path テスト対象のファイルの相対パス。
      */
    protected function assertBaseline(string $relative_include_path): void
    {
        ob_start();
        include PROJECT_ROOT . '/' . $relative_include_path;
        $html = ob_get_clean();
   
        $this->assertStringNotContainsString('URLが不正です。', $html);
        /** err.tpl */
        $this->assertStringNotContainsString('<title>エラー', $html);
    }
こんな感じで、output bufferingでechoのHTTP本体相当を変数に格納して、その変数にエラー文字がないかでチェックする。
 
https://grok.com/share/c2hhcmQtMw%3D%3D_d3073260-4256-46a8-8762-ef7c63967681
 
仕様化テストだから、Baseじゃなくて、Baselineとするのがいい。assertBaseline/testBaselineでやると、一貫性ある命名規則にできる。


ただ、根本的なPHPの構文エラーがあると、PHPStanはそこで失敗して詳細情報がない。php -lもあっていい気がする。
https://grok.com/share/c2hhcmQtMw%3D%3D_57517641-d440-468e-ae2e-956958ce27f9


=== PHPStan ===
API系の場合、結合テストに近い。サーバーを立てて、リクエストを送って、レスポンスがどうなるかをテストする感じ。


* [https://tech.hajimari.inc/entry/2022/06/16/120000 PHPStan導入のすすめ - Hajimari Tech Blog| 株式会社Hajimari]
一部クラスになっている場合も、基本は現在の挙動確認。現在の挙動を確認するような動作のテストをする。最終出力回りを重点的に書くとよい。
* [https://zenn.dev/pixiv/articles/7467448592862e PHPStanクイックガイド2023]


導入が簡単なので黙って導入したらよさそう。
===== 関数抽出 =====
<?php
require 'smarty_setup.php'; // Smarty初期化
header('Content-Type: text/html; charset=UTF-8');
$id = $_GET['id'];
$db = new PDO('...'); // DB接続
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
$user = $stmt->fetch();
$smarty->assign('user', $user);
echo $smarty->fetch('user.tpl');
?>


==== User Guide ====
<?php
function fetchUser($db, $id) { // ロジック関数
    $stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
    $stmt->execute([$id]);
    return $stmt->fetch();
}
function renderUser(Smarty $smarty, $user) { // ビュー関数
    $smarty->assign('user', $user);
    return $smarty->fetch('user.tpl'); // echoせず返す
}
// メインスクリプト
require 'smarty_setup.php';
header('Content-Type: text/html; charset=UTF-8');
$db = new PDO('...');
$id = $_GET['id'];
$user = fetchUser($db, $id);
$output = renderUser($smarty, $user);
echo $output;
?>
こんな感じで、内容ごとに処理を関数にまとめる。


===== Getting Started =====
# header出力
[https://phpstan.org/user-guide/getting-started Getting Started | PHPStan]
# DB操作
composer require --dev phpstan/phpstan
# テンプレート操作
以下のコマンドでバージョンを確認できればOK。
vendor/bin/phpstan analyze --version


PHPStan - PHP Static Analysis Tool 2.1.10
関数化した部分のテストは、仕様化テストと同様で、phpunitでoutput bufferingで出力部分を破棄して、関数定義だけ取り込んでテストする。


====== git pre-commit ======
長期保守前提であれば、関数化の段階をすっ飛ばして、オブジェクト・クラス化したほうが手っ取り早い。
#!/bin/sh
 
## Lint added/modified PHP file.
===== クラス化 =====
  set -eu
https://grok.com/share/c2hhcmQtMw%3D%3D_21361425-16d6-419a-84ae-3c793d627eef
  has_error=false
 
  PHP_FILES=$(git diff --staged --name-only --diff-filter=ACM | grep '\.php$' || :)
シンプルで無難なMVCの形でクラス化する。
[ -z "$PHP_FILES" ] && exit || :
 
* DB操作: Modelクラス
* 画面表示: Viewクラス
* ロジック呼び出し: Controllerkクラス
'''コード例''':
Model (models/UserModel.php):
  class UserModel {
    private $db;
   
    public function __construct(PDO $db) {
        $this->db = $db;
    }
   
    public function fetchUser($id) {
        $stmt = $this->db->prepare("SELECT * FROM users WHERE id = ?");
        $stmt->execute([$id]);
        return $stmt->fetch();
    }
}
View (views/UserView.php):
class UserView {
    private $smarty;
    public function __construct(Smarty $smarty) {
        $this->smarty = $smarty;
    }
   
   
while read -r file; do
    public function renderUser($user) {
php -l "$file" | grep -v '^No syntax errors' && has_error=true
        $this->smarty->assign('user', $user);
  done <<-EOT
        return $this->smarty->fetch('user.tpl');
  $PHP_FILES
    }
EOT
  }
$has_error && exit 1 || :
Controller (controllers/UserController.php):
  class UserController {
    private $model;
    private $view;
   
   
./vendor/bin/phpstan analyze --memory-limit 5G $PHP_FILES || has_error=true
    public function __construct(UserModel $model, UserView $view) {
  $has_error && exit 1 || :
        $this->model = $model;
# parameters.pathsのコメントアウトを解除して全体解析する場合、上記のファイル単位のphpstanをやめて、以下の全体解析を実行する。
        $this->view = $view;
# ./vendor/bin/phpstan analyze --memory-limit 5G || exit 1
    }
 
   
  includes:
    public function handleRequest() {
    - phpstan-baseline.neon
        $id = $_GET['id'];
  parameters:
        $user = $this->model->fetchUser($id);
    level: 0
        return $this->view->renderUser($user);
    tmpDir: var/tmp/phpstan
    }
    # parallel:
  }
    #    processTimeout: 15000.0
メインスクリプト (user.php):
    # scanDirectories:
  <?php
    #    - ../../
require 'autoload.php'; // Composerのautoload
    # todo ある程度指摘対応ができたら常に全体解析にする。
$db = new PDO('...');
    # paths:
$smarty = new Smarty(); // Smartyセットアップ
    excludePaths:
$model = new UserModel($db);
        - var
$view = new UserView($smarty);
最初は修正ファイルだけphpstanで解析して、安定してきたらプロジェクト全体を解析する形にするとよいだろう。
$controller = new UserController($model, $view);
header('Content-Type: text/html; charset=UTF-8');
$output = $controller->handleRequest();
echo $output;
?>
これでMVCパターンに近づく。SmartyをViewクラスでラップしてテストしやすく。


phpstanのファイル単独実行は時間がかかるので、修正対象をまとめて実行した方がいい。
あるいは、メインスクリプトをControllerとみなして、constructorで画面表示処理を書くというのもありかもしれない。
if (!debug_backtrace()) {}
このコードでincludeとの違いも検知できる。


===== Command Line Usage =====
PDOなどのDB接続インスタンスは、メインスクリプトで従来通り毎回書くか、シングルトンかstaticメソッドで管理して渡すとか。
[https://phpstan.org/user-guide/command-line-usage Command Line Usage | PHPStan]


====== Analyzing code ======
===== .inc/.include/.class =====
<code>vendor/bin/phpstan analyse [options] [<paths>...]</code>
https://grok.com/share/c2hhcmQtMw%3D%3D_6193ad5e-6df3-42c7-9dbc-dc67deb44bcd
いくつか重要なオプションがある。


* paths: 検査対象ファイルパス。設定ファイルで指定可能。
phpファイルが直接応答を返す古いパターンだと、拡張子が.inc/.include/.classなど.phpじゃないことがある。
* --level|-l: 実行レベル。設定ファイルで指定可能。
* --configuration|-c: 設定ファイルを指定する。
* --generate-baseline|-b: ベースラインを作成する。オプション引数で出力ファイルのパスを指定できる。デフォルトはphpstan-baseline.neon。
* --memory-limit: php.iniと同じ形式で最大メモリーを指定。


====== Running without arguments ======
公開ディレクトリーに、同居する都合、サーバーアクセスで拡張子で、アクセス制御しているからだろう。
PHPStanは基本的に、コマンド引数で指定した、ディレクトリー類を対象に解析する。


毎回コマンド引数を指定するのは手間なので、設定ファイルに記述しておくこともできる。
本来であれば、現代的なpublic/index.phpでコントローラーを明示的に振り分けて、ユーザーがアクセス可能なディレクトリーを制限すべきだろう。


以下の条件を満たせば、引数なしで、設定ファイルの内容で解析できる。
=== PHP ===


# phpstan.neonかphpstan.neon.distの存在。
==== declare(strict_types=1); ====
# pathsに解析対象パスリストが存在。
新規ファイルには、基本的に指定したほうがいい。既存ファイルは慎重に。
# levelパラメーターの存在。


最小限の例。
==== php -l ====
parameters:
* [https://ja.wikipedia.org/wiki/%E9%9D%99%E7%9A%84%E3%82%B3%E3%83%BC%E3%83%89%E8%A7%A3%E6%9E%90 静的コード解析 - Wikipedia]
level: 0
* [https://www.php.net/manual/ja/features.commandline.options.php PHP: オプション - Manual]
paths:
- src
- tests
現実的な例。
includes:
    - phpstan-baseline.neon
parameters:
    level: 0
    parallel:
        processTimeout: 15000.0
    tmpDir: var/tmp/phpstan
    scanDirectories:
    paths:
    excludePaths:


====== Minimum file ======
-l/--syntac-checkオプションで構文チェックのみを行う。-lはlintのlだと思われる。
https://chatgpt.com/c/67fe029c-9ce0-800b-9397-8e1d7f18b303


phpstanは基本的に引数で<paths>でディレクトリーやファイルを指定する。ただし、ここで指定していないファイルは、見に行けないので未定義の警告などが出る。
成功したらNo syntax errors detected in <filename> が標準出力に書き込まれ、リターンコードは 0


基本はパス全体を指定する。コマンドライン引数か、phpstan.dist.neonで指定しておく。
失敗した場合、テキスト Errors parsing <filename> に加え、内部パーサエラーメッセージ が標準出力に書き込まれ、シェルリターンコードは、 -1 となります。


他に、pre-commitように、autoload_filesかbootstarpFilesでベースの依存ファイルを指定する。
このオプションは、(未定義の関数のような)致命的なエラー(fatal error) はみつけません。致命的なエラーについても調べたい場合は、 -f を使用してください。


他に、phpstanはcomposerのautoloadを理解するので、composer.jsonにautoloadを追加する。
-lは-r (コードの実行)とは併用不能。以下のコードで全phpファイルをチェックできる。
for i in `find . -name \*.php`; do php -l $i | grep -v "No syntax errors"; done
上記コードより、以下の方が少し早い。
find . -name \*.php -exec php -l {} \; | grep -v '^No syntax errors'
外部ツールも不要で手軽で悪くない。特に、-lで実行するphpのバージョンを変えるだけでチェックでき、PHPのバージョンアップ時の事前の簡易チェック、粗探しとしてとして悪くない ([https://tech-blog.rakus.co.jp/entry/20220922/php PHPerのための「静的解析」を語り合う【PHP TechCafe イベントレポート】 - RAKUS Developers Blog | ラクス エンジニアブログ])。


====== Reflection error: Circular reference to class ======
ただ、php -lは構文エラーのみで、型チェックはできない。PHPStanを使うしかない模様。
名前通り循環参照が起きている。具体的に、自分と同名のクラスを最終的にextendsしている。


元ファイルを直す他、excludePathsで検査対象などから除外すれば回避できる。
gitのpre-commitに登録する場合、以下のような内容にするとよい。
#!/bin/sh
## Lint added/modified PHP file.
set -eu
has_error=false
PHP_FILES=$(git diff --staged --name-only --diff-filter=ACM | grep '\.php$' || :)
[ -z "$PHP_FILES" ] && exit || :
while read -r file; do
php -l "$file" | grep -v '^No syntax errors' && has_error=true
done <<-EOT
$PHP_FILES
EOT
$has_error && exit 1 || :
./vendor/bin/phpstan analyze --memory-limit 5G $PHP_FILES || has_error=true
$has_error && exit 1 || :
# parameters.pathsのコメントアウトを解除して全体解析する場合、上記のファイル単位のphpstanをやめて、以下の全体解析を実行する。
# ./vendor/bin/phpstan analyze --memory-limit 5G || exit 1


====== Internal error: Child process timed out after 3000.0 seconds. Try making it longer with parallel.processTimeout setting. ======
ただ、根本的なPHPの構文エラーがあると、PHPStanはそこで失敗して詳細情報がない。php -lもあっていい気がする。
https://chatgpt.com/share/68020423-6350-800b-9200-3d8c2d2483e2


--generate-baselineを指定すると、初回だけ時間がかかる。
=== PHPStan ===


parallel.processTimeoutでタイムアウトの時間を設定する。成功させるために、進捗率と現在の設定の比率でカバーできるだけの時間にする。3000で40 %くらいの進捗だったら9000とか。
* [https://tech.hajimari.inc/entry/2022/06/16/120000 PHPStan導入のすすめ - Hajimari Tech Blog| 株式会社Hajimari]
* [https://zenn.dev/pixiv/articles/7467448592862e PHPStanクイックガイド2023]


初回だけ時間がかかる。
導入が簡単なので黙って導入したらよさそう。


====== Syntax error, unexpected T_EXTENDS on line 18 ======
==== User Guide ====
https://chatgpt.com/c/6801fda7-2434-800b-84da-601c44645488


phpstanを実行すると上記のような、エラーが出ることがある。これは、PHPStanの前に根本的なPHPの構文エラーの可能性が高い。
===== Getting Started =====
[https://phpstan.org/user-guide/getting-started Getting Started | PHPStan]
composer require --dev phpstan/phpstan
以下のコマンドでバージョンを確認できればOK。
vendor/bin/phpstan analyze --version


PHPStan実行の前に、先にphp -lで解決しておく必要がある。
  PHPStan - PHP Static Analysis Tool 2.1.10
  time -p (find . -name \*.php -exec php -l {} \; | grep -v 'No syntax errors' >php-l.log) 2>&1


====== Error while loading phpstan-baseline.neon: Invalid UTF-8 sequence. ======
====== git pre-commit ======
作成したBaselineをincludeで読み込んで実行すると上記のエラーが出た。
hooks/pre-commit
 
#!/bin/sh
https://chatgpt.com/share/68020423-6350-800b-9200-3d8c2d2483e2
## Lint added/modified PHP file.
 
set -eu
ベースラインファイルにへんな文字が入ったのが原因。
has_error=false
PHP_FILES=$(git diff --staged --name-only --diff-filter=ACM | grep '\.php$' || :)
[ -z "$PHP_FILES" ] && exit || :
while read -r file; do
php -l "$file" | grep -v '^No syntax errors' && has_error=true
done <<-EOT
$PHP_FILES
EOT
$has_error && exit 1 || :
./vendor/bin/phpstan analyze --memory-limit 5G $PHP_FILES || has_error=true
$has_error && exit 1 || :
# parameters.pathsのコメントアウトを解除して全体解析する場合、上記のファイル単位のphpstanをやめて、以下の全体解析を実行する。
# ./vendor/bin/phpstan analyze --memory-limit 5G || exit 1


以下のコマンドで変な文字の行を特定できる。
# phpstan.dist.neon
  grep -axv '.*' phpstan-baseline.neon
  includes:
--error-format=rawを指定してbaselineを作り直すと良い。
    - phpstan-baseline.neon
parameters:
    level: 0
    tmpDir: var/tmp/phpstan
    # parallel:
    #    processTimeout: 15000.0
    # scanDirectories:
    #    - ../../
    # todo ある程度指摘対応ができたら常に全体解析にする。
    # paths:
    excludePaths:
        - var
最初は修正ファイルだけphpstanで解析して、安定してきたらプロジェクト全体を解析する形にするとよいだろう。


===== The Baseline =====
phpstanのファイル単独実行は時間がかかるので、修正対象をまとめて実行した方がいい。
[https://phpstan.org/user-guide/baseline The Baseline | PHPStan]


PHPStanはレベルベースでチェックしてくれる。が、既存コードで警告が多い場合、新規追加分だけ考慮したいだろう。
https://grok.com/share/c2hhcmQtMw%3D%3D_85f34631-62c7-426a-89cb-ecb407c0246b


そういう場合に、ベースラインが役立つ。
なお、WSLやDockerを使っている場合、docker exec <container-name> php -lなどで、docker内のphp/phpstanをホスト側で使うのがいい。


以下のように、--generate-baselineオプションを指定して、PHPStanを実行すると、現在のエラーリストをエクスポートしてくれる。
docker runで起動している場合、--nameでコンテナー名を固定する。
vendor/bin/phpstan analyse --generate-baseline
ベースラインファイルを使う場合、オプションで指定するか、設定ファイルで明記する。


デフォルトでphpstan-baseline.neon。オプション引数で、出力ベースラインファイルを指定できる。
===== Command Line Usage =====
[https://phpstan.org/user-guide/command-line-usage Command Line Usage | PHPStan]


作成したら、設定ファイルphpstan.dist.neonに読み込む。
====== Analyzing code ======
  includes:
  <code>vendor/bin/phpstan analyse [options] [<paths>...]</code>
- phpstan-baseline.neon
いくつか重要なオプションがある。
parameters:
# your usual configuration options
これで次回からはベースラインのエラーを無視してくれる。必要に応じて、ベースラインを編集したらい、ベースラインを再生成してもいい。


なお、ベースラインで無視したエラーがなくなった場合、ベースラインファイルから削除するまで、PHPStanは通知する。この通知を無効にしたかったら、以下を設定する。
* paths: 検査対象ファイルパス。設定ファイルで指定可能。
parameters:
* --level|-l: 実行レベル。設定ファイルで指定可能。
reportUnmatchedIgnoredErrors: false
* --configuration|-c: 設定ファイルを指定する。
なお、ベースライン作成時に、以下の警告が出ることがあり、全ての指摘をベースラインに出力できるわけではない。
* --generate-baseline|-b: ベースラインを作成する。オプション引数で出力ファイルのパスを指定できる。デフォルトはphpstan-baseline.neon。
[WARNING] Baseline generated with 79012 errors.                              
* --memory-limit: php.iniと同じ形式で最大メモリーを指定。
Some errors could not be put into baseline. Re-run PHPStan with "-vv"
and fix them.            
致命的なエラーはベースラインに出力できないので、直すしかない。


===== Discovering Symbols =====
====== Running without arguments ======
[https://phpstan.org/user-guide/discovering-symbols Discovering Symbols | PHPStan]
PHPStanは基本的に、コマンド引数で指定した、ディレクトリー類を対象に解析する。


https://chatgpt.com/c/67e0b340-da84-800b-89c8-4c72691fdc44
毎回コマンド引数を指定するのは手間なので、設定ファイルに記述しておくこともできる。


pathsで指定したファイルで使用されているシンボルをPHPStanは必要とする。デフォルトで以下の2箇所を探す。
以下の条件を満たせば、引数なしで、設定ファイルの内容で解析できる。


* pathsの対象 (コマンドライン引数と設定ファイル)。
# phpstan.neonかphpstan.neon.distの存在。
* composerの依存関係
# pathsに解析対象パスリストが存在。
# levelパラメーターの存在。


基本はこれで見つかるはず。
最小限の例。
 
parameters:
====== Third party code outside of Composer dependencies ======
level: 0
PEARとか追加で設定が必要だったりする。そういう場合、scanFilesとscanDirectoriesの設定が使える。
paths:
 
- src
これらで指定したファイル、ディレクトリーをシンボルとして探す。探すだけで解析はしない。
- tests
 
現実的な例。
https://chatgpt.com/share/680b240a-bf28-800b-bfc2-a84f639fe0c0
includes:
    - phpstan-baseline.neon
parameters:
    level: 0
    parallel:
        processTimeout: 15000.0
    tmpDir: var/tmp/phpstan
    scanDirectories:
    paths:
    excludePaths:


プロジェクト外部はみれないらしい。
====== Minimum file ======
https://chatgpt.com/c/67fe029c-9ce0-800b-9397-8e1d7f18b303


autoload_filesでautoload.phpを指定するか。bootstrapFilesで読み込む。
phpstanは基本的に引数で<paths>でディレクトリーやファイルを指定する。ただし、ここで指定していないファイルは、見に行けないので未定義の警告などが出る。


===== Output Format =====
基本はパス全体を指定する。コマンドライン引数か、phpstan.dist.neonで指定しておく。
[https://phpstan.org/user-guide/output-format Output Format | PHPStan]


--error-formatのオプションでPHPStanの報告形式を指定できる。
他に、pre-commitように、autoload_filesかbootstarpFilesでベースの依存ファイルを指定する。


.neonファイルに以下の形式で指定可能。
他に、phpstanはcomposerのautoloadを理解するので、composer.jsonにautoloadを追加する。
parameters:
errorFormat: json
特に重要なものがある。


* table: デフォルト。
====== Reflection error: Circular reference to class ======
* raw: 1行1データで機械処理用。文字化けなどが起こりにくい。
名前通り循環参照が起きている。具体的に、自分と同名のクラスを最終的にextendsしている。
* json: IDEなどとの連携用。
* checkstyle: XML形式。CIツール連携用。


困ったらrawを指定しておく。
元ファイルを直す他、excludePathsで検査対象などから除外すれば回避できる。


===== Ignoring Errors =====
====== Internal error: Child process timed out after 3000.0 seconds. Try making it longer with parallel.processTimeout setting. ======
[https://phpstan.org/user-guide/ignoring-errors Ignoring Errors | PHPStan]
https://chatgpt.com/share/68020423-6350-800b-9200-3d8c2d2483e2


====== Excluding whole files ======
--generate-baselineを指定すると、初回だけ時間がかかる。


====== Broken symbolic link ======
parallel.processTimeoutでタイムアウトの時間を設定する。成功させるために、進捗率と現在の設定の比率でカバーできるだけの時間にする。3000で40 %くらいの進捗だったら9000とか。
シンボリックリンクのリンク切れなどで以下のエラーが出ることがある。
  Could not read file:
https://chatgpt.com/c/68088168-ac54-800b-b78e-e28743b81011


excludePathsはファイルの中身に有効で、scanDirectiriesで指定したディレクトリーに、破損したシンボリックリンクがあると、スキャン対象に入った時点で失敗する。
初回だけ時間がかかる。


事前に破損したシンボリックリンクを削除しておく必要がある。
====== Syntax error, unexpected T_EXTENDS on line 18 ======
https://chatgpt.com/c/6801fda7-2434-800b-84da-601c44645488


以下のコマンドで破損リンクを削除できる。
phpstanを実行すると上記のような、エラーが出ることがある。これは、PHPStanの前に根本的なPHPの構文エラーの可能性が高い。
find path/to/scan -type l ! -exec test -e {} \; -exec rm {} \;
# find path/to/scan -type l ! -exec test -e {} \; -print # 確認用


===== Result Cache =====
PHPStan実行の前に、先にphp -lで解決しておく必要がある。
PHPStanは分析実行結果をキャッシュする。条件は、解析対象ファイルリスト。同一じゃないとキャッシュは使えない。
time -p (find . -name \*.php -exec php -l {} \; | grep -v 'No syntax errors' >php-l.log) 2>&1


キャッシュが使えると、数秒で解析が終わることもある。
====== Error while loading phpstan-baseline.neon: Invalid UTF-8 sequence. ======
作成したBaselineをincludeで読み込んで実行すると上記のエラーが出た。


====== tmpDir ======
https://chatgpt.com/share/68020423-6350-800b-9200-3d8c2d2483e2
実行結果は%tmpDir%/resultCache.phpになる。


sys_get_temp_dir() . '/phpstan' (usually /tmp/phpstan)
ベースラインファイルにへんな文字が入ったのが原因。


parameters.tmpDirで上書き指定可能。
以下のコマンドで変な文字の行を特定できる。
grep -axv '.*' phpstan-baseline.neon
--error-format=rawを指定してbaselineを作り直すと良い。


Macだと以下のような場所。
===== The Baseline =====
php -r 'echo sys_get_temp_dir() ."\n";'
[https://phpstan.org/user-guide/baseline The Baseline | PHPStan]


/var/folders/hr/9q7pmn9x5_788w3fsp9dd71r316wyt/T
PHPStanはレベルベースでチェックしてくれる。が、既存コードで警告が多い場合、新規追加分だけ考慮したいだろう。
このtmpDirのデフォルトは、基本的にOSを再起動すると消去される。なので、基本的にneonファイルに以下のような指定がほぼ必須。
parameters:tmpDir: var/tmp/phpstan
.gitignoreにもついでに以下を追加する。
/var/


====== scannedFiles ======
そういう場合に、ベースラインが役立つ。
phpstanを実行すると以下のようにキャッシュの不一致が示されることがある。
time -p vendor/bin/phpstan analyze --memory-limit=5G --generate-baseline --error-format=raw -vv 2>&1 | tee phpstan.log
Note: Using configuration file phpstan.dist.neon.
Result cache not used because the metadata do not match: projectConfig, scannedFiles
設定ファイルと、スキャンファイル一覧のどちらか。


原因把握のために、scannedFilesを確認したい。
以下のように、--generate-baselineオプションを指定して、PHPStanを実行すると、現在のエラーリストをエクスポートしてくれる。
vendor/bin/phpstan analyse --generate-baseline
ベースラインファイルを使う場合、オプションで指定するか、設定ファイルで明記する。


resultCache.phpのscannedFilesの連想配列のキーに、ファイル一覧が入っている。
デフォルトでphpstan-baseline.neon。オプション引数で、出力ベースラインファイルを指定できる。


確認すると、.phpの拡張子のファイルが入っている。.gitとかは含んでいない。が、tmpDirに指定したキャッシュ内の.phpは見ている。除外必要。
作成したら、設定ファイルphpstan.dist.neonに読み込む。
includes:
- phpstan-baseline.neon
parameters:
# your usual configuration options
これで次回からはベースラインのエラーを無視してくれる。必要に応じて、ベースラインを編集したらい、ベースラインを再生成してもいい。


parameters.fileExtensionsで追加可能 ([https://phpstan.org/config-reference#discovering-symbols Config Reference | PHPStan])。
なお、ベースラインで無視したエラーがなくなった場合、ベースラインファイルから削除するまで、PHPStanは通知する。この通知を無効にしたかったら、以下を設定する。
parameters:
reportUnmatchedIgnoredErrors: false
なお、ベースライン作成時に、以下の警告が出ることがあり、全ての指摘をベースラインに出力できるわけではない。
[WARNING] Baseline generated with 79012 errors.                              
Some errors could not be put into baseline. Re-run PHPStan with "-vv"
and fix them.             
致命的なエラーはベースラインに出力できないので、直すしかない。


====== paths/excludePath/scanDirectories ======
===== Discovering Symbols =====
結果キャッシュに影響のある中で、scannedFilesに影響のあるこれらの設定の関係がわかりにくいので整理する。
[https://phpstan.org/user-guide/discovering-symbols Discovering Symbols | PHPStan]


* pathsの指定があれば含む。
https://chatgpt.com/c/67e0b340-da84-800b-89c8-4c72691fdc44
* scanDirectoriesがあれば含む
* excludePath (analyzeAndScan) の指定があれば、除外される。paths内で一部を除外する場合、指定が必要。シンボルとしては見たい場合はanalyze。analyzeと併用したい場合はanalayzeAndScan。scanDirectories以下も同様。


==== Config Reference ====
pathsで指定したファイルで使用されているシンボルをPHPStanは必要とする。デフォルトで以下の2箇所を探す。
[https://phpstan.org/config-reference Config Reference | PHPStan]


基本はphpstan.neon.distまたはphpstan.dist.neonをバージョン管理する。ユーザーはphpstan.neonで上書きできるようにする。
* pathsの対象 (コマンドライン引数と設定ファイル)。
* composerの依存関係


phpstan.dist.neonのほうが、拡張子が明示されてよく感じる。
基本はこれで見つかるはず。
 
====== Third party code outside of Composer dependencies ======
PEARとか追加で設定が必要だったりする。そういう場合、scanFilesとscanDirectoriesの設定が使える。
 
これらで指定したファイル、ディレクトリーをシンボルとして探す。探すだけで解析はしない。


===PHPUnit===
https://chatgpt.com/share/680b240a-bf28-800b-bfc2-a84f639fe0c0
PHPUnitを使用すれば自動単体テスト (Unit test) が可能だ。


==== About ====
プロジェクト外部はみれないらしい。


===== Version =====
autoload_filesでautoload.phpを指定するか。bootstrapFilesで読み込む。
情報源: [https://phpunit.de/supported-versions.html Supported Versions of PHPUnit – The PHP Testing Framework]。


PHPUnitのバージョンごとに対応しているPHPのバージョンが決まっている。
https://grok.com/share/c2hhcmQtMw%3D%3D_fc03d339-2ee1-4f76-85bd-ee567a2575ef


PHP v7.4に対応してい最後のバージョンはPHPUnit 9なので、当分はこれを使うのが良い。
例えば、PEARの依存関係を追加したい場合、以下のコマンドでpearのパスを確認する。
pear config-get php_dir
これをscanDirectoriesに指定すればOK。
parameters:
    scanFiles:
        - /usr/local/php


===== Install =====
====== Bootstrap ======
*[https://web.gnusocial.jp/post/2023/07/07/7428/ 設置: PHPUnit | PHPの定番テストフレームワーク  |  GNU social JP Web]
https://grok.com/share/c2hhcmQtMw%3D%3D_fc03d339-2ee1-4f76-85bd-ee567a2575ef
以下のコマンドでPHP 7.4に対応している最後のPHPUnitのv9をインストール。
composer require --dev phpunit/phpunit ^9
composer.jsonに以下が追加される。
{
    "require-dev": {
        "phpunit/phpunit": "^9"
    }
}
composer.jsonとcomposer.lockをVCSで管理する。


composerのautoloadのclassmapでソースファイルのルートディレクトリーを指定する。
PHPStanの実行前に、PHPのランタイム設定したい場合、bootstrapFilesで自前の起動時ファイルを指定できる。
  {
  parameters:
    "autoload": {
bootstrapFiles:
        "classmap": [
- phpstan-bootstrap.php
            "src/"
例えば、環境変数の設定とかをこれでうまくできる。独自のautoload的なことやっている場合もこれでいける。
        ]
    },
    "require-dev": {
        "phpunit/phpunit": "^9"
    }
}
最後に以下のコマンドでvendor/autoload.phpを更新。
composer dump-autoload


==== Getting Started with PHPUnit 9 ====
===== Output Format =====
[https://phpunit.de/getting-started/phpunit-9.html Getting Started with Version 9 of PHPUnit]
[https://phpstan.org/user-guide/output-format Output Format | PHPStan]


# phpunitをインストール
--error-formatのオプションでPHPStanの報告形式を指定できる。
# テストコード記述
# phpunit実行


上記の3ステップで使う。
.neonファイルに以下の形式で指定可能。
parameters:
errorFormat: json
特に重要なものがある。


以下のように引数に、ディレクトリーかテスト対象ファイルの相対パスを探す。
* table: デフォルト。
phpunit tests
* raw: 1行1データで機械処理用。文字化けなどが起こりにくい。
* json: IDEなどとの連携用。
* checkstyle: XML形式。CIツール連携用。


====2. Writing Tests for PHPUnit====
困ったらrawを指定しておく。
出典: [https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html 2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual]。
 
===== Ignoring Errors =====
[https://phpstan.org/user-guide/ignoring-errors Ignoring Errors | PHPStan]
 
====== Excluding whole files ======
 
====== Broken symbolic link ======
シンボリックリンクのリンク切れなどで以下のエラーが出ることがある。
  Could not read file:
https://chatgpt.com/c/68088168-ac54-800b-b78e-e28743b81011
 
excludePathsはファイルの中身に有効で、scanDirectiriesで指定したディレクトリーに、破損したシンボリックリンクがあると、スキャン対象に入った時点で失敗する。
 
事前に破損したシンボリックリンクを削除しておく必要がある。


基本的な使用方法を整理する。
以下のコマンドで破損リンクを削除できる。
#基本的にはクラス単位で試験コードを記載。<Class>クラスの試験コードは<Class>Testの命名にする。
find path/to/scan -type l ! -exec test -e {} \; -exec rm {} \;
#<Class>Test はPHPUnit\Framwork\TestCaseを継承させる。
  # find path/to/scan -type l ! -exec test -e {} \; -print # 確認用
#試験はpublicのtest*メソッドの命名にする。あるいは、@testのアノテーションを付ければ、命名規則に従わなくてもいい。
 
#test*メソッド内で、assertSame() などで、期待値との比較で試験を行う。
===== Result Cache =====
#test*メソッドで1メソッドに対して、試験する。例えば、正常形、異常系、境界値など。1メソッド1テストメソッドにするとわかりやすい。
PHPStanは分析実行結果をキャッシュする。条件は、解析対象ファイルリスト。同一じゃないとキャッシュは使えない。
例:
 
<?php declare(strict_types=1);
キャッシュが使えると、数秒で解析が終わることもある。
use PHPUnit\Framework\TestCase;
 
====== tmpDir ======
final class StackTest extends TestCase
実行結果は%tmpDir%/resultCache.phpになる。
{
 
<nowiki> </nowiki>  private static $dbh;
sys_get_temp_dir() . '/phpstan' (usually /tmp/phpstan)
  <nowiki> </nowiki>  private $instance;
 
<nowiki> </nowiki> 
parameters.tmpDirで上書き指定可能。
<nowiki> </nowiki>  public static function setUpBeforeClass(): void
 
<nowiki> </nowiki>  {
Macだと以下のような場所。
<nowiki> </nowiki>      // DB接続などクラス全体の初期化処理
  php -r 'echo sys_get_temp_dir() ."\n";'
<nowiki> </nowiki>      self::$dbh = new PDO(<nowiki>''</nowiki>);
<nowiki> </nowiki>  }
<nowiki> </nowiki>  public static function tearDownAfterClass(): void
<nowiki> </nowiki>  {
<nowiki> </nowiki>      self::$dbh = null;
<nowiki> </nowiki>  }
<nowiki> </nowiki>  protected function setUp(): void
<nowiki> </nowiki>  {
<nowiki> </nowiki>    // 該当インスタンスの生成などメソッド単位の初期化処理。
<nowiki> </nowiki>    $instance = new Stack();
<nowiki> </nowiki>  }
<nowiki> </nowiki>  public function testPushAndPop(): void
<nowiki> </nowiki>  {
<nowiki> </nowiki>      $stack = [];
<nowiki> </nowiki>      $this->assertSame(0, count($stack));
  <nowiki> </nowiki>      array_push($stack, 'foo');
<nowiki> </nowiki>      $this->assertSame('foo', $stack[count($stack)-1]);
<nowiki> </nowiki>      $this->assertSame(1, count($stack));
<nowiki> </nowiki>      $this->assertSame('foo', array_pop($stack));
<nowiki> </nowiki>      $this->assertSame(0, count($stack));
<nowiki> </nowiki>  }
}
命名規則があっていないと、以下のメッセージが出てphpunitの実行に失敗する。
Class <Class>Test could not be found in /path/to/<Class>Test.php
後述するが、メソッドテスト時は、data providerでテストデータを与えて、1テストメソッドで1試験対象メソッドをテストするといい。


=====Depends=====
/var/folders/hr/9q7pmn9x5_788w3fsp9dd71r316wyt/T
前回の試験で準備した結果を利用したい場合、@dependsのアノテーションでテスト関数を指定しておくと、指定したテスト関数のreturnを引数に受け継いだテスト関数を記述できる。
このtmpDirのデフォルトは、基本的にOSを再起動すると消去される。なので、基本的にneonファイルに以下のような指定がほぼ必須。
parameters:tmpDir: var/tmp/phpstan
.gitignoreにもついでに以下を追加する。
/var/


@dependsは複数指定でき、指定順に事前に試験が実行されて引数に渡される。
====== scannedFiles ======
=====Data Provider=====
phpstanを実行すると以下のようにキャッシュの不一致が示されることがある。
time -p vendor/bin/phpstan analyze --memory-limit=5G --generate-baseline --error-format=raw -vv 2>&1 | tee phpstan.log
Note: Using configuration file phpstan.dist.neon.
Result cache not used because the metadata do not match: projectConfig, scannedFiles
設定ファイルと、スキャンファイル一覧のどちらか。
 
原因把握のために、scannedFilesを確認したい。
 
resultCache.phpのscannedFilesの連想配列のキーに、ファイル一覧が入っている。
 
確認すると、.phpの拡張子のファイルが入っている。.gitとかは含んでいない。が、tmpDirに指定したキャッシュ内の.phpは見ている。除外必要。


====== About ======
parameters.fileExtensionsで追加可能 ([https://phpstan.org/config-reference#discovering-symbols Config Reference | PHPStan])。
* [https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html#data-providers 2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual]
* https://chatgpt.com/share/68390cd8-5cc4-800b-afce-36c2abdc4b83


ある関数に対して、テストケースを用意して、複数の引数の組み合わせを試験したいことがよくある。こういうときのために、テスト関数に渡す引数を生成する関数のデータプロバイダーを指定できる。@dataProviderで関数を指定する。データプロバイダーは引数のリストを配列で返すようにする。
====== paths/excludePath/scanDirectories ======
結果キャッシュに影響のある中で、scannedFilesに影響のあるこれらの設定の関係がわかりにくいので整理する。


データプロバイダーを使うことで、テスト用のメソッドは1個で、データだけ変えてテストできる。1メソッド1テストメソッドで対応できてスマート。また、データプロバイダーを使うと、どのデータで失敗したかがわかる。
* pathsの指定があれば含む。
<?php declare(strict_types=1);
* scanDirectoriesがあれば含む
use PHPUnit\Framework\TestCase;
* excludePath (.analyseAndScan) の指定があれば、除外される。paths内で一部を除外する場合に使う。シンボルとしては見たい場合はexcludePath.analyseの指定。analyseと併用・別々に記載したい場合はanalayseAndScan。scanDirectories以下も同様。気持ち悪いが、analyzeではなくてanalyseじゃないと認識されない。
parameters:
final class DataTest extends TestCase
     paths:
{
         - src
    /**
     excludePaths:
      * @dataProvider additionProvider
        analyse:
      */
            - src/thirdparty
    public function testAdd(int $a, int $b, int $expected): void
         analyseAndScan:
     {
             - src/broken
         $this->assertSame($expected, $a + $b);
srcの中のsrc/thirdpartyを、解析対象に含めたくないが、シンボルとしては認識したい場合。src/brokenは見に行くとエラーになるのでシンブルとしても無視。
     }
 
==== Config Reference ====
    public function additionProvider(): array
[https://phpstan.org/config-reference Config Reference | PHPStan]
    {
 
         return [
基本はphpstan.neon.distまたはphpstan.dist.neonをバージョン管理する。ユーザーはphpstan.neonで上書きできるようにする。
             [0, 0, 0],
            [0, 1, 1],
            [1, 0, 1],
            [1, 1, 3]
        ];
    }
}
data providerの使用方法。


# テストデータを配列かIteratorで返却するpublic メソッド (data provider) を用意
phpstan.dist.neonのほうが、拡張子が明示されてよく感じる。
# テストメソッドの引数で、data providerの配列を受け付ける。
# data providerを使用したいテストメソッドで、@dataProvider <nowiki><data provider method> のアノテーションを指定する。</nowiki>


データセットはリストでもいいが、連想配列でキーにテスト名を書くとわかりやすい。
===PHPUnit===
<?php declare(strict_types=1);
PHPUnitを使用すれば自動単体テスト (Unit test) が可能だ。
use PHPUnit\Framework\TestCase;
 
==== About ====
final class DataTest extends TestCase
 
{
===== Version =====
    /**
情報源: [https://phpunit.de/supported-versions.html Supported Versions of PHPUnit – The PHP Testing Framework]
      * @dataProvider additionProvider
      */
    public function testAdd(int $a, int $b, int $expected): void
    {
        $this->assertSame($expected, $a + $b);
    }
    public function additionProvider(): array
    {
        return [
            'adding zeros'  => [0, 0, 0],
            'zero plus one' => [0, 1, 1],
            'one plus zero' => [1, 0, 1],
            'one plus one'  => [1, 1, 3]
        ];
    }
}
テストデータに意味がある場合は、記載した方がよさそう。このテストデータ部分にMock用のデータを渡したりもできる。


Mockでデータに応じて処理を変える場合も、データプロバイダーのデータでif文などの条件を入れたり、mock_dataのような専用のキーを渡す形にすればいい。
PHPUnitのバージョンごとに対応しているPHPのバージョンが決まっている。


データ数が多い場合、名前付き配列にしておくと、どういうデータ項目で失敗したかがわかりやすい。Iteratorオブジェクトを返してもいい。
PHP v7.4に対応してい最後のバージョンはPHPUnit 9なので、当分はこれを使うのが良い。


====== Exception ======
===== Install =====
テストデータによって、例外が発生することを期待する場合、いくつか方法がある。テストデータにthrowsException:boolの列を用意して、それで判定する。または、例外用のテストメソッドを用意してそちらにするというのもある。複雑なら後者、テストケースがそんなに多くなくてシンプルなら前者でいいと思う。
*[https://web.gnusocial.jp/post/2023/07/07/7428/ 設置: PHPUnit | PHPの定番テストフレームワーク  | GNU social JP Web]
  /**
以下のコマンドでPHP 7.4に対応している最後のPHPUnitのv9をインストール。
  * @dataProvider provideCases
composer require --dev phpunit/phpunit ^9
  */
composer.jsonに以下が追加される。
public function testSomething($input, $expected, $throwsException)
  {
  {
     if ($throwsException) {
     "require-dev": {
        $this->expectException(\InvalidArgumentException::class);
         "phpunit/phpunit": "^9"
    }
    $result = $this->target->doSomething($input);
   
    if (!$throwsException) {
         $this->assertSame($expected, $result);
     }
     }
  }
  }
composer.jsonとcomposer.lockをVCSで管理する。
public static function provideCases(): array
 
composerのautoloadのclassmapでソースファイルのルートディレクトリーを指定する。
  {
  {
     return [
     "autoload": {
         '正常系' => ['input' => 10, 'expected' => 100, 'throwsException' => false],
        "classmap": [
         '異常系' => ['input' => -1, 'expected' => null, 'throwsException' => true],
            "src/"
     ];
         ]
    },
    "require-dev": {
         "phpunit/phpunit": "^9"
     }
  }
  }
最後に以下のコマンドでvendor/autoload.phpを更新。
composer dump-autoload


====== Naming ======
==== Getting Started with PHPUnit 9 ====
data providerメソッドの命名。「複数メソッド共通: provideXxx」にする。
[https://phpunit.de/getting-started/phpunit-9.html Getting Started with Version 9 of PHPUnit]


「特定メソッド専用: testXxxDataProvider」にすると、testメソッドと対応が同じでわかりやすいのだが、testから始まると、testメソッド扱いされるので、assertがないと警告が出る。命名規則的にはxxxxTest/xxxProviderがきれいなんだけど、test/provideで使う場所と隣同士にすれば、そんなに困ることはないだろう。
# phpunitをインストール
# テストコード記述
# phpunit実行


他に、返却する配列の順番。公式だと、引数→期待の順番。だが、引数は複数あり得るのだから、期待→引数の順番がわかりやすい。assertの順番ともあうし。
上記の3ステップで使う。


メソッドは、test→provideの順番に書くといいみたい。testメソッドに対して、テストケースのデータを流すから。メソッドが先で、データが後。どういう試験をするかが、メソッドで先にわかるイメージの模様。
以下のように引数に、ディレクトリーかテスト対象ファイルの相対パスを探す。
phpunit tests


テストメソッドの名前は、test<nethod name><テスト観点> みたいな感じにすると良い。次のResolutionのように、1メソッドに対して、違うテスト観点で複数テストすることがあるから。
====2. Writing Tests for PHPUnit====
出典: [https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html 2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual]。


====== Resolution ======
基本的な使用方法を整理する。
https://chatgpt.com/share/68390cd8-5cc4-800b-afce-36c2abdc4b83
#基本的にはクラス単位で試験コードを記載。<Class>クラスの試験コードは<Class>Testの命名にする。
 
#<Class>Test はPHPUnit\Framwork\TestCaseを継承させる。
data providerを使って渡すテストデータは、同じ観点にする。
#試験はpublicのtest*メソッドの命名にする。あるいは、@testのアノテーションを付ければ、命名規則に従わなくてもいい。
 
#test*メソッド内で、assertSame() などで、期待値との比較で試験を行う。
例えば、表示の試験は、表示のバリエーション。表示の他に、ボタンの動作試験をしたい場合、違うテストメソッド、data providerにしたらしい。
#test*メソッドで1メソッドに対して、試験する。例えば、正常形、異常系、境界値など。1メソッド1テストメソッドにするとわかりやすい。
 
例:
1個のdata providerで複数の観点の試験もできなくはないが、引数とテストメソッドが複雑になって、わかりにくくなる。
<?php declare(strict_types=1);
 
use PHPUnit\Framework\TestCase;
===== 3. The Command-Line Test Runner =====
[https://docs.phpunit.de/en/9.6/textui.html 3. The Command-Line Test Runner — PHPUnit 9.6 Manual]
final class StackTest extends TestCase
 
{
phpunitのコマンド自体の使用方法。
<nowiki> </nowiki>  private static $dbh;
  phpunit <file>
<nowiki> </nowiki>  private $instance;
  phpunit <directory>
<nowiki> </nowiki> 
引数にテスト対象ファイルの相対パスか、ディレクトリーを指定する。ディレクトリーを指定した場合、配下の全*Test.phpを実行する。
<nowiki> </nowiki>  public static function setUpBeforeClass(): void
 
<nowiki> </nowiki>  {
基本はphpunit testsを実行すればいいと思う。
<nowiki> </nowiki>      // DB接続などクラス全体の初期化処理
=====Fixtures=====
<nowiki> </nowiki>      self::$dbh = new PDO(<nowiki>''</nowiki>);
出典: [https://docs.phpunit.de/en/9.6/fixtures.html 4. Fixtures — PHPUnit 9.6 Manual]。
<nowiki> </nowiki>  }
<nowiki> </nowiki>  public static function tearDownAfterClass(): void
<nowiki> </nowiki>  {
<nowiki> </nowiki>      self::$dbh = null;
<nowiki> </nowiki>  }
<nowiki> </nowiki>  protected function setUp(): void
<nowiki> </nowiki>  {
<nowiki> </nowiki>    // 該当インスタンスの生成などメソッド単位の初期化処理。
<nowiki> </nowiki>    $instance = new Stack();
<nowiki> </nowiki>  }
<nowiki> </nowiki>  public function testPushAndPop(): void
<nowiki> </nowiki>  {
<nowiki> </nowiki>      $stack = [];
<nowiki> </nowiki>      $this->assertSame(0, count($stack));
<nowiki> </nowiki>      array_push($stack, 'foo');
<nowiki> </nowiki>      $this->assertSame('foo', $stack[count($stack)-1]);
<nowiki> </nowiki>      $this->assertSame(1, count($stack));
  <nowiki> </nowiki>      $this->assertSame('foo', array_pop($stack));
  <nowiki> </nowiki>       $this->assertSame(0, count($stack));
<nowiki> </nowiki>  }
}
命名規則があっていないと、以下のメッセージが出てphpunitの実行に失敗する。
Class <Class>Test could not be found in /path/to/<Class>Test.php
後述するが、メソッドテスト時は、data providerでテストデータを与えて、1テストメソッドで1試験対象メソッドをテストするといい。


テストメソッドの実行前に、テスト対象のインスタンスの生成や、DB接続など準備がいろいろある。これをFixturesと呼んでいる。この準備がけっこう手間になる。これを省力できるのがテストフレームワークの利点。
=====Depends=====
前回の試験で準備した結果を利用したい場合、@dependsのアノテーションでテスト関数を指定しておくと、指定したテスト関数のreturnを引数に受け継いだテスト関数を記述できる。


テストメソッド実行前後に共通で行える処理がある。
@dependsは複数指定でき、指定順に事前に試験が実行されて引数に渡される。
*setUp/tearDown: テストメソッド単位の前後処理。テスト対象インスタンスの生成など。tearDownは何もしなくてもいいことが多い。
=====Data Provider=====
*setUpBeforeClass/tearDownAfterClass: クラス単位の前後処理。 DB接続など。


====== 共通クラスの初期化処理 ======
====== About ======
https://chatgpt.com/share/6836d32d-e9b8-800b-9433-444ff1a1e2bf
* [https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html#data-providers 2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual]
* https://chatgpt.com/share/68390cd8-5cc4-800b-afce-36c2abdc4b83


PHPUnitのTestCaseを継承した独自の共通処理クラスを作る場合。初期化処理は__constructでやってはいけないらしい。
ある関数に対して、テストケースを用意して、複数の引数の組み合わせを試験したいことがよくある。こういうときのために、テスト関数に渡す引数を生成する関数のデータプロバイダーを指定できる。@dataProviderで関数を指定する。データプロバイダーは引数のリストを配列で返すようにする。


テスト用にいろいろ特別な初期化をしているから。
データプロバイダーを使うことで、テスト用のメソッドは1個で、データだけ変えてテストできる。1メソッド1テストメソッドで対応できてスマート。また、データプロバイダーを使うと、どのデータで失敗したかがわかる。
 
<?php declare(strict_types=1);
代わりに、setUpで共通で使うインスタンス生成などをする。子クラスではparent::setUp()が毎回必要になるがしかたない。
use PHPUnit\Framework\TestCase;
 
==== XML Configuration File ====
final class DataTest extends TestCase
出典:
{
*[https://docs.phpunit.de/en/9.6/organizing-tests.html 5. Organizing Tests — PHPUnit 9.6 Manual]
    /**
*[https://docs.phpunit.de/en/9.6/configuration.html 3. The XML Configuration File — PHPUnit 9.6 Manual]
      * @dataProvider additionProvider
基本はコマンドでテスト対象クラス・ファイルを指定してテストを実行する。他に、XMLの設定ファイル (phpunit.xml) でもテスト対象を指定できる。
      */
 
    public function testAdd(int $a, int $b, int $expected): void
ただし、phpunit.xmlかphpunit.xml.distがあって、かつ--configurationの指定がない場合、これらのファイルを自動的に読み込む。
    {
        $this->assertSame($expected, $a + $b);
    }
    public function additionProvider(): array
    {
        return [
            [0, 0, 0],
            [0, 1, 1],
            [1, 0, 1],
            [1, 1, 3]
        ];
    }
}
data providerの使用方法。


testsディレクトリーの全*Test.phpを対象にする最小限の例は以下。
# テストデータを配列かIteratorで返却するpublic メソッド (data provider) を用意
<phpunit bootstrap="src/autoload.php">
# テストメソッドの引数で、data providerの配列を受け付ける。
  <testsuites>
# data providerを使用したいテストメソッドで、@dataProvider <nowiki><data provider method> のアノテーションを指定する。</nowiki>
    <testsuite name="money">
 
      <directory>tests</directory>
データセットはリストでもいいが、連想配列でキーにテスト名を書くとわかりやすい。
    </testsuite>
  <?php declare(strict_types=1);
  </testsuites>
use PHPUnit\Framework\TestCase;
  </phpunit>
   
phpunit.xmlがあれば、単にphpunitコマンドを実行するだけでいい。
final class DataTest extends TestCase
  phpunit --bootstrap src/autoload.php --testsuite money
{
上記のコマンド相当になる。<syntaxhighlight lang="xml">
    /**
<?xml version="1.0" encoding="UTF-8"?>
      * @dataProvider additionProvider
<phpunit bootstrap="vendor/autoload.php" colors="true">
      */
    <testsuites>
    public function testAdd(int $a, int $b, int $expected): void
        <testsuite name="tests">
    {
            <directory>tests</directory>
        $this->assertSame($expected, $a + $b);
        </testsuite>
    }
    </testsuites>
</phpunit>
    public function additionProvider(): array
</syntaxhighlight>
    {
 
        return [
https://chatgpt.com/share/6821bc52-3324-800b-a98c-cbefe6c42324
            'adding zeros'  => [0, 0, 0],
{| class="wikitable"
            'zero plus one' => [0, 1, 1],
|+
            'one plus zero' => [1, 0, 1],
!element
            'one plus one'  => [1, 1, 3]
!attribute
        ];
!default
    }
!
}
|-
テストデータに意味がある場合は、記載した方がよさそう。このテストデータ部分にMock用のデータを渡したりもできる。
|phpunit
 
|bootstrap
Mockでデータに応じて処理を変える場合も、データプロバイダーのデータでif文などの条件を入れたり、mock_dataのような専用のキーを渡す形にすればいい。
| -
 
| --bootstrap相当。テスト実行前の読込スクリプト。
データ数が多い場合、名前付き配列にしておくと、どういうデータ項目で失敗したかがわかりやすい。Iteratorオブジェクトを返してもいい。
|-
 
|
====== Exception ======
|colors
テストデータによって、例外が発生することを期待する場合、いくつか方法がある。テストデータにthrowsException:boolの列を用意して、それで判定する。または、例外用のテストメソッドを用意してそちらにするというのもある。複雑なら後者、テストケースがそんなに多くなくてシンプルなら前者でいいと思う。
|false
/**
|true=--colors=auto相当、false=--colors=never相当。
  * @dataProvider provideCases
|-
  */
|
public function testSomething($input, $expected, $throwsException)
|verbose
{
|
    if ($throwsException) {
|
        $this->expectException(\InvalidArgumentException::class);
|-
    }
|testsuites
| -
    $result = $this->target->doSomething($input);
| -
   
|testsuiteの親要素。
    if (!$throwsException) {
|-
        $this->assertSame($expected, $result);
|testsuite
    }
| -
}
| -
|name属性が必須で、テスト検索用の1以上のdirectoryかfile要素が必要。
public static function provideCases(): array
|-
{
|testsuite
    return [
|name
        '正常系' => ['input' => 10, 'expected' => 100, 'throwsException' => false],
|
        '異常系' => ['input' => -1, 'expected' => null, 'throwsException' => true],
|必須。ディレクトリー名でいい気がする。
    ];
|-
}
|phpunit/php
 
|
====== Naming ======
|
data providerメソッドの命名。「複数メソッド共通: provideXxx」にする。
|PHPの設定。
 
|-
「特定メソッド専用: testXxxDataProvider」にすると、testメソッドと対応が同じでわかりやすいのだが、testから始まると、testメソッド扱いされるので、assertがないと警告が出る。命名規則的にはxxxxTest/xxxProviderがきれいなんだけど、test/provideで使う場所と隣同士にすれば、そんなに困ることはないだろう。
|phpunit/php/includePath
|
|
|include_pathの先頭に追加するパス。
|}


===== bootstrap =====
他に、返却する配列の順番。公式だと、引数→期待の順番。だが、引数は複数あり得るのだから、期待→引数の順番がわかりやすい。assertの順番ともあうし。
https://chatgpt.com/share/6821bc52-3324-800b-a98c-cbefe6c42324


autoloadを使っていないプロジェクトだと、手動でプロジェクトルートパスなどの指定が必要になる。
メソッドは、test→provideの順番に書くといいみたい。testメソッドに対して、テストケースのデータを流すから。メソッドが先で、データが後。どういう試験をするかが、メソッドで先にわかるイメージの模様。


tests以下にbootstrap.phpを用意して、その中で必要なものを読み込む形にするといい。
テストメソッドの名前は、test<nethod name><テスト観点> みたいな感じにすると良い。次のResolutionのように、1メソッドに対して、違うテスト観点で複数テストすることがあるから。
<?php
// tests/bootstrap.php
// $additionalPath = PROJECT_ROOT . '/src'; set_include_path(get_include_path() . PATH_SEPARATOR . $additionalPath); set_include_path(implode(PATH_SEPARATOR, [ get_include_path(), PROJECT_ROOT . '/src', PROJECT_ROOT . '/lib' ]));
// Composerのオートローダー
require_once __DIR__ . '/../vendor/autoload.php';
// オートローダーでは読み込めない独自ファイル
require_once __DIR__ . '/manual-loads/special-loader.php';
// 必要なら定数定義
define('PROJECT_ROOT', dirname(__DIR__));


<!-- phpunit.xml -->
====== Resolution ======
<phpunit bootstrap="tests/bootstrap.php">
https://chatgpt.com/share/68390cd8-5cc4-800b-afce-36c2abdc4b83


====Assertions====
data providerを使って渡すテストデータは、同じ観点にする。


===== About =====
例えば、表示の試験は、表示のバリエーション。表示の他に、ボタンの動作試験をしたい場合、違うテストメソッド、data providerにしたらしい。
Ref:
*[https://docs.phpunit.de/en/9.6/assertions.html 1. Assertions — PHPUnit 9.6 Manual].
*[https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html?highlight=assertSame#testing-exceptions 2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual]
基本はassertSameでテストすればいいのだが、それ以外にも例外とかいろいろ試験したいケースがあるので、メソッドを整理する。


===== Method =====
1個のdata providerで複数の観点の試験もできなくはないが、引数とテストメソッドが複雑になって、わかりにくくなる。
インスタンスメソッドとstaticメソッドの2種類がある。他に、グローバル関数もある。どれを使ってもいい。


* アサーションメソッド: PHPUnit\Framework\Assert
===== 3. The Command-Line Test Runner =====
* PHPUnit\Framework\TestCaseはAssertを継承している。
[https://docs.phpunit.de/en/9.6/textui.html 3. The Command-Line Test Runner — PHPUnit 9.6 Manual]
* phpunit/phpunit/src/Framework/Assert/Functions.php でグローバル関数。


入力文字数や好みの問題。グローバル関数は内部的にstaticメソッドを呼んでいる。
phpunitのコマンド自体の使用方法。
phpunit <file>
phpunit <directory>
引数にテスト対象ファイルの相対パスか、ディレクトリーを指定する。ディレクトリーを指定した場合、配下の全*Test.phpを実行する。


TestCaseのインスタンスメソッドの方が違和感がないという説がある。
基本はphpunit testsを実行すればいいと思う。
=====Fixtures=====
出典: [https://docs.phpunit.de/en/9.6/fixtures.html 4. Fixtures — PHPUnit 9.6 Manual]。


$this->よりもself::のほうが短い。グローバル関数は名前空間がないのがまずい。self::のstaticメソッドでいい気がする。
テストメソッドの実行前に、テスト対象のインスタンスの生成や、DB接続など準備がいろいろある。これをFixturesと呼んでいる。この準備がけっこう手間になる。これを省力できるのがテストフレームワークの利点。


===== Negate =====
テストメソッド実行前後に共通で行える処理がある。
assert系メソッドは、assertNotでNotを前置したら否定形になる。
*setUp/tearDown: テストメソッド単位の前後処理。テスト対象インスタンスの生成など。tearDownは何もしなくてもいいことが多い。
*setUpBeforeClass/tearDownAfterClass: クラス単位の前後処理。 DB接続など。


===== 同値判定 =====
====== 共通クラスの初期化処理 ======
https://chatgpt.com/share/6836d32d-e9b8-800b-9433-444ff1a1e2bf


* assertSame/assertNotSame: 型と値の両方を検査 (===相当)。
PHPUnitのTestCaseを継承した独自の共通処理クラスを作る場合。初期化処理は__constructでやってはいけないらしい。
* assertEquals: 値を検査 (==相当)。


オブジェクトの属性値の値が同じかどうかをみたいなら、assertEqualsになる。assertSameは参照 (ポインター) のアドレス値の比較みたいなことをするから。
テスト用にいろいろ特別な初期化をしているから。


基本はassertSameでよいと思われる。
代わりに、setUpで共通で使うインスタンス生成などをする。子クラスではparent::setUp()が毎回必要になるがしかたない。


* assertEqualsCanonicalizing()
==== XML Configuration File ====
* assertEqualsIgnoringCase()
出典:
* assertEqualsWithDelta()
*[https://docs.phpunit.de/en/9.6/organizing-tests.html 5. Organizing Tests — PHPUnit 9.6 Manual]
*[https://docs.phpunit.de/en/9.6/configuration.html 3. The XML Configuration File — PHPUnit 9.6 Manual]
基本はコマンドでテスト対象クラス・ファイルを指定してテストを実行する。他に、XMLの設定ファイル (phpunit.xml) でもテスト対象を指定できる。


他にこういうバリエーションがある。
ただし、phpunit.xmlかphpunit.xml.distがあって、かつ--configurationの指定がない場合、これらのファイルを自動的に読み込む。


===== 論理判定 =====
testsディレクトリーの全*Test.phpを対象にする最小限の例は以下。
<phpunit bootstrap="src/autoload.php">
  <testsuites>
    <testsuite name="money">
      <directory>tests</directory>
    </testsuite>
  </testsuites>
</phpunit>
phpunit.xmlがあれば、単にphpunitコマンドを実行するだけでいい。
phpunit --bootstrap src/autoload.php --testsuite money
上記のコマンド相当になる。<syntaxhighlight lang="xml">
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php" colors="true">
    <testsuites>
        <testsuite name="tests">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
</phpunit>
</syntaxhighlight>


* assertIsBool
https://chatgpt.com/share/6821bc52-3324-800b-a98c-cbefe6c42324
* assertFalse
{| class="wikitable"
* assertTrue()
|+
 
!element
===== 数値判定 =====
!attribute
 
!default
* assertGreaterThan
!
* assertGreaterThanOrEqual
|-
* assertLessThan
|phpunit
* assertLessThanOrEqual
|bootstrap
* assertInfinite
| -
* assertNan
| --bootstrap相当。テスト実行前の読込スクリプト。
* assertIsInt
|-
* assertIsNumeric
|
* assertIsFloat
|colors
 
|false
===== 文字列判定 =====
|true=--colors=auto相当、false=--colors=never相当。
 
|-
* assertIsString
|
* assertMatchesRegularExpression
|verbose
* assertStringContainsString
|
* assertStringContainsStringIgnoringCase()
|
* assertStringMatchesFormat
|-
* assertStringMatchesFormatFile
|testsuites
* assertStringEndsWith
| -
* assertStringEqualsFile
| -
* assertStringStartsWith
|testsuiteの親要素。
 
|-
===== 配列判定 =====
|testsuite
配列の検査用のメソッドがある。
| -
 
| -
* 要素数
|name属性が必須で、テスト検索用の1以上のdirectoryかfile要素が必要。
** assertEmpty
|-
** assertSameSize(): 2個の配列の要素数が同じか。
|testsuite
** assertCount(): 指定した配列の要素数が指定数か。データの取得数などでこちらをよく使いそう。
|name
* 包含
|
** assertContains
|必須。ディレクトリー名でいい気がする。
** assertContainsOnly()
|-
** assertContainsOnlyInstancesOf()
|phpunit/php
** assertArrayHasKey
|
* assertIsArray
|
* assertIsIterable
|PHPの設定。
 
|-
===== クラス =====
|phpunit/php/includePath
 
|
* assertClassHasAttribute
|
* assertClassHasStaticAttribute
|include_pathの先頭に追加するパス。
* assertObjectEquals
|-
* assertInstanceOf
|phpunit/php/ini
* assertIsCallable
|name/value
* assertIsObject
|
* assertIsResource
|
* assertIsScalar
|-
* assertNull
|phpunit/php/const, var
* assertObjectHasProperty
|
|
|グローバルな定数、変数を設定。
|-
|phpunit/php/env
|name/value
|
|環境変数。
|-
|phpunit/php/get, post, cookie, server, files, request
|name/value
|
|該当するスーパーグローバル変数の設定。
|}
GuzzleのHTTPクライアントでプロキシー設定無視で以下の設定はよく使うかもしれない。
<php><env name="NO_PROXY" value="*"/></php>


===== ファイル =====
===== bootstrap =====
https://chatgpt.com/share/6821bc52-3324-800b-a98c-cbefe6c42324


* assertDirectoryExists
autoloadを使っていないプロジェクトだと、手動でプロジェクトルートパスなどの指定が必要になる。
* assertDirectoryIsReadable
* assertDirectoryIsWritable
* assertFileEquals
* assertFileExists
* assertFileIsReadable
* assertFileIsWritable
* assertIsReadable
* assertIsWritable


===== その他 =====
tests以下にbootstrap.phpを用意して、その中で必要なものを読み込む形にするといい。
<?php
// tests/bootstrap.php
// $additionalPath = PROJECT_ROOT . '/src'; set_include_path(get_include_path() . PATH_SEPARATOR . $additionalPath); set_include_path(implode(PATH_SEPARATOR, [ get_include_path(), PROJECT_ROOT . '/src', PROJECT_ROOT . '/lib' ]));
// Composerのオートローダー
require_once __DIR__ . '/../vendor/autoload.php';
// オートローダーでは読み込めない独自ファイル
require_once __DIR__ . '/manual-loads/special-loader.php';
// 必要なら定数定義
define('PROJECT_ROOT', dirname(__DIR__));


* assertThat
<!-- phpunit.xml -->
* assertJsonFileEqualsJsonFile
<phpunit bootstrap="tests/bootstrap.php">
* assertJsonStringEqualsJsonFile
* assertJsonStringEqualsJsonString
* assertXmlFileEqualsXmlFile
* assertXmlStringEqualsXmlFile
* assertXmlStringEqualsXmlString


=====Exception=====
====Assertions====
[https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html#testing-exceptions 2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual]


特に例外の試験がイレギュラー。
===== About =====
<?php declare(strict_types=1);
Ref:
use PHPUnit\Framework\TestCase;
*[https://docs.phpunit.de/en/9.6/assertions.html 1. Assertions — PHPUnit 9.6 Manual].
*[https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html?highlight=assertSame#testing-exceptions 2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual]
final class ExceptionTest extends TestCase
基本はassertSameでテストすればいいのだが、それ以外にも例外とかいろいろ試験したいケースがあるので、メソッドを整理する。
{
    public function testException(): void
    {
        $this->expectException(InvalidArgumentException::class);
        // Run test target code following.
    }
}
上記のようにexpectExceptionを使う。
*expectException:
*expectExceptionCode:
*expectExceptionMessage:
*expectExceptionMessageMatches:
例外が発生する処理の前に記述しておく。
=====Testing Output=====
[https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html#testing-output 2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual]


echoなど標準出力を試験する際も専用のメソッドがある。
===== Method =====
*<code>void expectOutputRegex(string $regularExpression)</code>
インスタンスメソッドとstaticメソッドの2種類がある。他に、グローバル関数もある。どれを使ってもいい。
*<code>void expectOutputString(string $expectedString)</code>
*<code>bool setOutputCallback(callable $callback)</code>
*<code>string getActualOutput()</code>
expectExceptionと同様に事前にセットしておく。


===== Constraint =====
* アサーションメソッド: PHPUnit\Framework\Assert
[https://docs.phpunit.de/en/9.6/assertions.html#appendixes-assertions-assertthat-tables-constraints 1. Assertions — PHPUnit 9.6 Manual]
* PHPUnit\Framework\TestCaseはAssertを継承している。
assertThatとwithで使えるConstraint。通常のassertと似たような名前のものも多いので注意する。equalToとかはConstraintにしかない。PHPUnit\Framework\Constraintの名前空間の各種クラスのメソッドになる。
* phpunit/phpunit/src/Framework/Assert/Functions.php でグローバル関数。
 
入力文字数や好みの問題。グローバル関数は内部的にstaticメソッドを呼んでいる。
 
TestCaseのインスタンスメソッドの方が違和感がないという説がある。
 
$this->よりもself::のほうが短い。グローバル関数は名前空間がないのがまずい。self::のstaticメソッドでいい気がする。
 
===== Negate =====
assert系メソッドは、assertNotでNotを前置したら否定形になる。assertStringとかグループがある場合は、assertStringNotのように、グループの後にNotになる。
 
===== 同値判定 =====
 
* assertSame/assertNotSame: 型と値の両方を検査 (===相当)。
* assertEquals: 値を検査 (==相当)。
 
オブジェクトの属性値の値が同じかどうかをみたいなら、assertEqualsになる。assertSameは参照 (ポインター) のアドレス値の比較みたいなことをするから。


* 論理
基本はassertSameでよいと思われる。
** isFalse
 
** isTrue
* assertEqualsCanonicalizing()
** logicalAnd
* assertEqualsIgnoringCase()
** logicalNot
* assertEqualsWithDelta()
** logicalOr
** logicalXor
* 数値
** greaterThan
** greaterThanOrEqual
** lessThan
** lessThanOrEqual
* 文字列
** matchesRegularExpression
** stringContains
** stringEndsWith
** stringStartsWith
* 配列
** arrayHasKey
** contains
** containsOnly
** containsOnlyInstanceOf
* クラス
** classHasAttribute
** classHasStaticAttribute
** objectHasAttribute
** isInstanceOf
** isNull
* ファイル
** directorExists
** fileExists
** isReadable
** isWritable
* その他
** isType
** anything
** equalTo
** identicalTo


上記にない制約は$this->callbackで自分で定義する。引数に引数がきて、true/falseを返す。
他にこういうバリエーションがある。
{| class="wikitable"
 
|+Table 1.1 Constraints
===== 論理判定 =====
!Constraint
 
!Meaning
* assertIsBool
|-
* assertFalse
|<code>PHPUnit\Framework\Constraint\IsAnything anything()</code>
* assertTrue()
|Constraint that accepts any input value.
 
|-
===== 数値判定 =====
|<code>PHPUnit\Framework\Constraint\ArrayHasKey arrayHasKey(mixed $key)</code>
 
|Constraint that asserts that the array has a given key.
* assertGreaterThan
|-
* assertGreaterThanOrEqual
|<code>PHPUnit\Framework\Constraint\TraversableContains contains(mixed $value)</code>
* assertLessThan
|Constraint that asserts that the <code>array</code> or object that implements the <code>Iterator</code> interface contains a given value.
* assertLessThanOrEqual
|-
* assertInfinite
|<code>PHPUnit\Framework\Constraint\TraversableContainsOnly containsOnly(string $type)</code>
* assertNan
|Constraint that asserts that the <code>array</code> or object that implements the <code>Iterator</code> interface contains only values of a given type.
* assertIsInt
|-
* assertIsNumeric
|<code>PHPUnit\Framework\Constraint\TraversableContainsOnly containsOnlyInstancesOf(string $classname)</code>
* assertIsFloat
|Constraint that asserts that the <code>array</code> or object that implements the <code>Iterator</code> interface contains only instances of a given classname.
 
|-
===== 文字列判定 =====
|<code>PHPUnit\Framework\Constraint\IsEqual equalTo($value, $delta = 0, $maxDepth = 10)</code>
 
|Constraint that checks if one value is equal to another.
* assertIsString
|-
* assertMatchesRegularExpression
|<code>PHPUnit\Framework\Constraint\DirectoryExists directoryExists()</code>
* assertStringContainsString
|Constraint that checks if the directory exists.
* assertStringContainsStringIgnoringCase()
|-
* assertStringMatchesFormat
|<code>PHPUnit\Framework\Constraint\FileExists fileExists()</code>
* assertStringMatchesFormatFile
|Constraint that checks if the file(name) exists.
* assertStringEndsWith
|-
* assertStringEqualsFile
|<code>PHPUnit\Framework\Constraint\IsReadable isReadable()</code>
* assertStringStartsWith
|Constraint that checks if the file(name) is readable.
 
|-
===== 配列判定 =====
|<code>PHPUnit\Framework\Constraint\IsWritable isWritable()</code>
配列の検査用のメソッドがある。
|Constraint that checks if the file(name) is writable.
 
|-
* 要素数
|<code>PHPUnit\Framework\Constraint\GreaterThan greaterThan(mixed $value)</code>
** assertEmpty
|Constraint that asserts that the value is greater than a given value.
** assertSameSize(): 2個の配列の要素数が同じか。
|-
** assertCount(): 指定した配列の要素数が指定数か。データの取得数などでこちらをよく使いそう。
|<code>PHPUnit\Framework\Constraint\LogicalOr greaterThanOrEqual(mixed $value)</code>
* 包含
|Constraint that asserts that the value is greater than or equal to a given value.
** assertContains
|-
** assertContainsOnly()
|<code>PHPUnit\Framework\Constraint\ClassHasAttribute classHasAttribute(string $attributeName)</code>
** assertContainsOnlyInstancesOf()
|Constraint that asserts that the class has a given attribute.
** assertArrayHasKey
|-
* assertIsArray
|<code>PHPUnit\Framework\Constraint\ClassHasStaticAttribute classHasStaticAttribute(string $attributeName)</code>
* assertIsIterable
|Constraint that asserts that the class has a given static attribute.
 
|-
===== クラス =====
|<code>PHPUnit\Framework\Constraint\ObjectHasAttribute objectHasAttribute(string $attributeName)</code>
 
|Constraint that asserts that the object has a given attribute.
* assertClassHasAttribute
|-
* assertClassHasStaticAttribute
|<code>PHPUnit\Framework\Constraint\IsIdentical identicalTo(mixed $value)</code>
* assertObjectEquals
|Constraint that asserts that one value is identical to another.
* assertInstanceOf
|-
* assertIsCallable
|<code>PHPUnit\Framework\Constraint\IsFalse isFalse()</code>
* assertIsObject
|Constraint that asserts that the value is <code>false</code>.
* assertIsResource
|-
* assertIsScalar
|<code>PHPUnit\Framework\Constraint\IsInstanceOf isInstanceOf(string $className)</code>
* assertNull
|Constraint that asserts that the object is an instance of a given class.
* assertObjectHasProperty
|-
 
|<code>PHPUnit\Framework\Constraint\IsNull isNull()</code>
===== ファイル =====
|Constraint that asserts that the value is <code>null</code>.
 
|-
* assertDirectoryExists
|<code>PHPUnit\Framework\Constraint\IsTrue isTrue()</code>
* assertDirectoryIsReadable
|Constraint that asserts that the value is <code>true</code>.
* assertDirectoryIsWritable
|-
* assertFileEquals
|<code>PHPUnit\Framework\Constraint\IsType isType(string $type)</code>
* assertFileExists
|Constraint that asserts that the value is of a specified type.
* assertFileIsReadable
|-
* assertFileIsWritable
|<code>PHPUnit\Framework\Constraint\LessThan lessThan(mixed $value)</code>
* assertIsReadable
|Constraint that asserts that the value is smaller than a given value.
* assertIsWritable
|-
 
|<code>PHPUnit\Framework\Constraint\LogicalOr lessThanOrEqual(mixed $value)</code>
===== その他 =====
|Constraint that asserts that the value is smaller than or equal to a given value.
 
|-
* assertThat
|<code>logicalAnd()</code>
* assertJsonFileEqualsJsonFile
|Logical AND.
* assertJsonStringEqualsJsonFile
|-
* assertJsonStringEqualsJsonString
|<code>logicalNot(PHPUnit\Framework\Constraint $constraint)</code>
* assertXmlFileEqualsXmlFile
|Logical NOT.
* assertXmlStringEqualsXmlFile
|-
* assertXmlStringEqualsXmlString
|<code>logicalOr()</code>
|Logical OR.
|-
|<code>logicalXor()</code>
|Logical XOR.
|-
|<code>PHPUnit\Framework\Constraint\PCREMatch matchesRegularExpression(string $pattern)</code>
|Constraint that asserts that the string matches a regular expression.
|-
|<code>PHPUnit\Framework\Constraint\StringContains stringContains(string $string, bool $case)</code>
|Constraint that asserts that the string contains a given string.
|-
|<code>PHPUnit\Framework\Constraint\StringEndsWith stringEndsWith(string $suffix)</code>
|Constraint that asserts that the string ends with a given suffix.
|-
|<code>PHPUnit\Framework\Constraint\StringStartsWith stringStartsWith(string $prefix)</code>
|Constraint that asserts that the string starts with a given prefix.
|}


====Command-Line====
=====Exception=====
Ref: [https://docs.phpunit.de/en/9.6/textui.html 3. The Command-Line Test Runner — PHPUnit 9.6 Manual].
[https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html#testing-exceptions 2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual]


phpunitコマンドでいろいろできる。いくつか重要なオプション、使用方法がある。
特に例外の試験がイレギュラー。
*phpunit file.php: 指定したファイルのテストを実行。
<?php declare(strict_types=1);
*--testsuite <name>: テストを指定。
use PHPUnit\Framework\TestCase;
====8. Test Doubles====
Ref: [https://docs.phpunit.de/en/9.6/test-doubles.html 8. Test Doubles — PHPUnit 9.6 Manual].
final class ExceptionTest extends TestCase
{
    public function testException(): void
    {
        $this->expectException(InvalidArgumentException::class);
        // Run test target code following.
    }
}
上記のようにexpectExceptionを使う。
*expectException:
*expectExceptionCode:
*expectExceptionMessage:
*expectExceptionMessageMatches:
例外が発生する処理の前に記述しておく。
=====Testing Output=====
[https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html#testing-output 2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual]


===== About =====
echoなど標準出力を試験する際も専用のメソッドがある。
テスト時に、依存関係を模擬したもので置換したいことがある。PHPUnitにそういう仕組が用意されている。
*<code>void expectOutputRegex(string $regularExpression)</code>
*<code>void expectOutputString(string $expectedString)</code>
*<code>bool setOutputCallback(callable $callback)</code>
*<code>string getActualOutput()</code>
expectExceptionと同様に事前にセットしておく。


[https://ja.wikipedia.org/wiki/%E3%83%86%E3%82%B9%E3%83%88%E3%83%80%E3%83%96%E3%83%AB テストダブル - Wikipedia]」Doubleは代役、影武者を意味する。ロックマンX4のダブルはたぶんこの英単語が由来だろう。
===== Constraint =====
[https://docs.phpunit.de/en/9.6/assertions.html#appendixes-assertions-assertthat-tables-constraints 1. Assertions — PHPUnit 9.6 Manual]
assertThatとwithで使えるConstraint。通常のassertと似たような名前のものも多いので注意する。equalToとかはConstraintにしかない。PHPUnit\Framework\Constraintの名前空間の各種クラスのメソッドになる。


テスト対象クラスのプロパティーのクラスやメソッドの代用品をテストダブルと呼ぶ。テストダブルで置換するというような言葉の使い方をすると思われる。
* 論理
 
** isFalse
テストダブルには、stubとmockがある。
** isTrue
 
** logicalAnd
* Stub: SUT内メソッド戻り値の検証
** logicalNot
* Mock: SUT内メソッド引数の検証。
** logicalOr
 
** logicalXor
メソッド内で他のクラス・メソッドを使う場合はmockで対象クラスを模擬させる。
* 数値
 
** greaterThan
PHPUnitではテストダブル用に3の基本APIがある。
** greaterThanOrEqual
 
** lessThan
* createStub
** lessThanOrEqual
* createMock
* 文字列
* getMockBuilder
** matchesRegularExpression
 
** stringContains
引数に置換対象のクラスを指定してインスタンスを作成する。createStubとcreateMockは対象を全部置換する。getMockBuilderはこれら2種のメソッドを含んでいて、置換するメソッド・プロパティーを自分で選択できる。特定メソッドだけを置換して、他はそのまま使いたい場合、これを使うしかない。
** stringEndsWith
** stringStartsWith
* 配列
** arrayHasKey
** contains
** containsOnly
** containsOnlyInstanceOf
* クラス
** classHasAttribute
** classHasStaticAttribute
** objectHasAttribute
** isInstanceOf
** isNull
* ファイル
** directorExists
** fileExists
** isReadable
** isWritable
* その他
** isType
** anything
** equalTo
** identicalTo


===== Stub =====
上記にない制約は$this->callbackで自分で定義する。引数に引数がきて、true/falseを返す。
オブジェクトのメソッドの戻り値を、テストダブルに置換する手法をスタブと呼ぶ。メソッド内の特定メソッドの戻り値を模擬することで、テストと関係ない処理の影響を無視できる。
{| class="wikitable"
 
|+Table 1.1 Constraints
実際に使う際は、テスト対象クラスのインスタンス作成後に、インスタンスがのプロパティーに設定して、インスタンス内の別クラスメソッド呼び出しを模擬する。
!Constraint
<?php declare(strict_types=1);
!Meaning
class SomeClass
|-
{
|<code>PHPUnit\Framework\Constraint\IsAnything anything()</code>
    public function doSomething()
|Constraint that accepts any input value.
    {
|-
        // Do something.
|<code>PHPUnit\Framework\Constraint\ArrayHasKey arrayHasKey(mixed $key)</code>
    }
|Constraint that asserts that the array has a given key.
}
|-
 
|<code>PHPUnit\Framework\Constraint\TraversableContains contains(mixed $value)</code>
<?php declare(strict_types=1);
|Constraint that asserts that the <code>array</code> or object that implements the <code>Iterator</code> interface contains a given value.
use PHPUnit\Framework\TestCase;
|-
|<code>PHPUnit\Framework\Constraint\TraversableContainsOnly containsOnly(string $type)</code>
final class StubTest extends TestCase
|Constraint that asserts that the <code>array</code> or object that implements the <code>Iterator</code> interface contains only values of a given type.
{
|-
    public function testStub(): void
|<code>PHPUnit\Framework\Constraint\TraversableContainsOnly containsOnlyInstancesOf(string $classname)</code>
    {
|Constraint that asserts that the <code>array</code> or object that implements the <code>Iterator</code> interface contains only instances of a given classname.
        // Create a stub for the SomeClass class.
        $stub = $this->createStub(SomeClass::class);
        // Configure the stub.
        $stub->method('doSomething')
              ->willReturn('foo');
        // Calling $stub->doSomething() will now return
        // 'foo'.
        $this->assertSame('foo', $stub->doSomething());
    }
}
単に、メソッドの戻り値を模擬したいだけなら、expectsとwithは不要。method()->willReturnだけで十分。
{| class="wikitable"
|+Table 8.1 Stubbing short hands
!short hand
!longer syntax
|-
|-
|<code>willReturn($value)</code>
|<code>PHPUnit\Framework\Constraint\IsEqual equalTo($value, $delta = 0, $maxDepth = 10)</code>
|<code>will($this->returnValue($value))</code>
|Constraint that checks if one value is equal to another.
|-
|-
|<code>willReturnArgument($argumentIndex)</code>
|<code>PHPUnit\Framework\Constraint\DirectoryExists directoryExists()</code>
|<code>will($this->returnArgument($argumentIndex))</code>
|Constraint that checks if the directory exists.
|-
|-
|<code>willReturnCallback($callback)</code>
|<code>PHPUnit\Framework\Constraint\FileExists fileExists()</code>
|<code>will($this->returnCallback($callback))</code>
|Constraint that checks if the file(name) exists.
|-
|-
|<code>willReturnMap($valueMap)</code>
|<code>PHPUnit\Framework\Constraint\IsReadable isReadable()</code>
|<code>will($this->returnValueMap($valueMap))</code>
|Constraint that checks if the file(name) is readable.
|-
|<code>PHPUnit\Framework\Constraint\IsWritable isWritable()</code>
|Constraint that checks if the file(name) is writable.
|-
|<code>PHPUnit\Framework\Constraint\GreaterThan greaterThan(mixed $value)</code>
|Constraint that asserts that the value is greater than a given value.
|-
|<code>PHPUnit\Framework\Constraint\LogicalOr greaterThanOrEqual(mixed $value)</code>
|Constraint that asserts that the value is greater than or equal to a given value.
|-
|-
|<code>willReturnOnConsecutiveCalls($value1, $value2)</code>
|<code>PHPUnit\Framework\Constraint\ClassHasAttribute classHasAttribute(string $attributeName)</code>
|<code>will($this->onConsecutiveCalls($value1, $value2))</code>
|Constraint that asserts that the class has a given attribute.
|-
|-
|<code>willReturnSelf()</code>
|<code>PHPUnit\Framework\Constraint\ClassHasStaticAttribute classHasStaticAttribute(string $attributeName)</code>
|<code>will($this->returnSelf())</code>
|Constraint that asserts that the class has a given static attribute.
|-
|-
|<code>willThrowException($exception)</code>
|<code>PHPUnit\Framework\Constraint\ObjectHasAttribute objectHasAttribute(string $attributeName)</code>
|<code>will($this->throwException($exception))</code>
|Constraint that asserts that the object has a given attribute.
|}willReturnCallbackで呼び出し関数をまるごと別のものに置換できる。これが非常に便利。
|-
 
|<code>PHPUnit\Framework\Constraint\IsIdentical identicalTo(mixed $value)</code>
引数を指定したい場合、with(引数)->willReturn()がシンプル。ただし、この方法は最後に指定した引数1個しか対応できない。引数違いで戻り値を模擬したいなら、willReturnMapかwillReturnCallbackで、引数に応じた戻り値を設定する。
|Constraint that asserts that one value is identical to another.
 
|-
willReturnCallbackのcallbackに渡される引数の記載はないが、モック対象メソッドに渡される引数が、その順番で渡される動きになっている。
|<code>PHPUnit\Framework\Constraint\IsFalse isFalse()</code>
 
|Constraint that asserts that the value is <code>false</code>.
===== Mock Objects =====
|-
https://chatgpt.com/share/683e72cc-461c-800b-a7fc-5e8b417a44aa
|<code>PHPUnit\Framework\Constraint\IsInstanceOf isInstanceOf(string $className)</code>
 
|Constraint that asserts that the object is an instance of a given class.
メソッドが呼び出されたかどうかを検証するための、テストダブルへの置換をモッキングと呼ぶ。
|-
 
|<code>PHPUnit\Framework\Constraint\IsNull isNull()</code>
テスト対象メソッド内で、特定メソッドの呼び出しを検査できる。MVCでViewへの値渡しなんかの検査にうってつけ。
|Constraint that asserts that the value is <code>null</code>.
 
|-
モックオブジェクトにはテストスタブの機能も含んでいる。が、モッキングのための専用処理があるので、インスタンス生成時などのコストがやや大きい。理由がなければ、Stubを使った方がいい。
|<code>PHPUnit\Framework\Constraint\IsTrue isTrue()</code>
 
|Constraint that asserts that the value is <code>true</code>.
判断基準として、expects/withを使うならMock、そうじゃなければStubのようなイメージ。<syntaxhighlight lang="php">
|-
<?php declare(strict_types=1);
|<code>PHPUnit\Framework\Constraint\IsType isType(string $type)</code>
use PHPUnit\Framework\TestCase;
|Constraint that asserts that the value is of a specified type.
 
|-
class Subject
|<code>PHPUnit\Framework\Constraint\LessThan lessThan(mixed $value)</code>
{
|Constraint that asserts that the value is smaller than a given value.
    protected $observers = [];
|-
    protected $name;
|<code>PHPUnit\Framework\Constraint\LogicalOr lessThanOrEqual(mixed $value)</code>
 
|Constraint that asserts that the value is smaller than or equal to a given value.
    public function __construct($name)
|-
    {
|<code>logicalAnd()</code>
        $this->name = $name;
|Logical AND.
    }
|-
 
|<code>logicalNot(PHPUnit\Framework\Constraint $constraint)</code>
    public function getName()
|Logical NOT.
    {
|-
        return $this->name;
|<code>logicalOr()</code>
    }
|Logical OR.
 
|-
    public function attach(Observer $observer)
|<code>logicalXor()</code>
    {
|Logical XOR.
        $this->observers[] = $observer;
|-
    }
|<code>PHPUnit\Framework\Constraint\PCREMatch matchesRegularExpression(string $pattern)</code>
|Constraint that asserts that the string matches a regular expression.
|-
|<code>PHPUnit\Framework\Constraint\StringContains stringContains(string $string, bool $case)</code>
|Constraint that asserts that the string contains a given string.
|-
|<code>PHPUnit\Framework\Constraint\StringEndsWith stringEndsWith(string $suffix)</code>
|Constraint that asserts that the string ends with a given suffix.
|-
|<code>PHPUnit\Framework\Constraint\StringStartsWith stringStartsWith(string $prefix)</code>
|Constraint that asserts that the string starts with a given prefix.
|}


    public function doSomething()
====Command-Line====
    {
Ref: [https://docs.phpunit.de/en/9.6/textui.html 3. The Command-Line Test Runner — PHPUnit 9.6 Manual].
        // Do something.
        // ...


        // Notify observers that we did something.
phpunitコマンドでいろいろできる。いくつか重要なオプション、使用方法がある。
        $this->notify('something');
*phpunit file.php: 指定したファイルのテストを実行。
    }
*--testsuite <name>: テストを指定。
====8. Test Doubles====
Ref: [https://docs.phpunit.de/en/9.6/test-doubles.html 8. Test Doubles — PHPUnit 9.6 Manual].


    public function doSomethingBad()
===== About =====
    {
テスト時に、依存関係を模擬したもので置換したいことがある。PHPUnitにそういう仕組が用意されている。
        foreach ($this->observers as $observer) {
            $observer->reportError(42, 'Something bad happened', $this);
        }
    }


    protected function notify($argument)
「[https://ja.wikipedia.org/wiki/%E3%83%86%E3%82%B9%E3%83%88%E3%83%80%E3%83%96%E3%83%AB テストダブル - Wikipedia]」Doubleは代役、影武者を意味する。ロックマンX4のダブルはたぶんこの英単語が由来だろう。
    {
        foreach ($this->observers as $observer) {
            $observer->update($argument);
        }
    }


    // Other methods.
テスト対象クラスのプロパティーのクラスやメソッドの代用品をテストダブルと呼ぶ。テストダブルで置換するというような言葉の使い方をすると思われる。
}


class Observer
テストダブルには、stubとmockがある。
{
    public function update($argument)
    {
        // Do something.
    }


    public function reportError($errorCode, $errorMessage, Subject $subject)
* Stub: SUT内メソッド戻り値の検証
    {
* Mock: SUT内メソッド引数の検証。
        // Do something
    }


    // Other methods.
メソッド内で他のクラス・メソッドを使う場合はmockで対象クラスを模擬させる。
}


</syntaxhighlight>
PHPUnitではテストダブル用に3の基本APIがある。


<syntaxhighlight lang="php">
* createStub
<?php declare(strict_types=1);
* createMock
use PHPUnit\Framework\TestCase;
* getMockBuilder


final class SubjectTest extends TestCase
引数に置換対象のクラスを指定してインスタンスを作成する。createStubとcreateMockは対象を全部置換する。getMockBuilderはこれら2種のメソッドを含んでいて、置換するメソッド・プロパティーを自分で選択できる。特定メソッドだけを置換して、他はそのまま使いたい場合、これを使うしかない。
{
    public function testObserversAreUpdated(): void
    {
        // Create a mock for the Observer class,
        // only mock the update() method.
        $observer = $this->createMock(Observer::class);


        // Set up the expectation for the update() method
===== Stub =====
        // to be called only once and with the string 'something'
オブジェクトのメソッドの戻り値を、テストダブルに置換する手法をスタブと呼ぶ。メソッド内の特定メソッドの戻り値を模擬することで、テストと関係ない処理の影響を無視できる。
        // as its parameter.
        $observer->expects($this->once())
                ->method('update')
                ->with($this->equalTo('something'));


        // Create a Subject object and attach the mocked
実際に使う際は、テスト対象クラスのインスタンス作成後に、インスタンスがのプロパティーに設定して、インスタンス内の別クラスメソッド呼び出しを模擬する。
        // Observer object to it.
<?php declare(strict_types=1);
        $subject = new Subject('My subject');
class SomeClass
        $subject->attach($observer);
{
    public function doSomething()
    {
        // Do something.
    }
}


        // Call the doSomething() method on the $subject object
<?php declare(strict_types=1);
        // which we expect to call the mocked Observer object's
use PHPUnit\Framework\TestCase;
        // update() method with the string 'something'.
        $subject->doSomething();
final class StubTest extends TestCase
    }
{
}
    public function testStub(): void
 
    {
</syntaxhighlight>基本的な作り。
        // Create a stub for the SomeClass class.
#createMock(<class>::class)で該当クラスのモックを作成。
        $stub = $this->createStub(SomeClass::class);
#expectsに呼出回数条件のオブジェクトをセット。これはなくてもいい。
#methodで対象メソッドを指定。
        // Configure the stub.
#withで該当メソッドの引数の検証条件を指定。
        $stub->method('doSomething')
#willReturnなどで戻り値を指定。
              ->willReturn('foo');
デフォルトで模擬実装はnullを返す。戻り値を変更したければ、will($this->returnValue())などを指定する。よく使うので短縮記法もある。
 
        // Calling $stub->doSomething() will now return
===== Matcher =====
        // 'foo'.
使えるメソッド。[https://github.com/sebastianbergmann/phpunit/blob/main/src/Framework/TestCase.php#L959<nowiki>] あたりからの一連のメソッドと思われる。</nowiki>
        $this->assertSame('foo', $stub->doSomething());
 
    }
* any
}
* never
単に、メソッドの戻り値を模擬したいだけなら、expectsとwithは不要。method()->willReturnだけで十分。
* atLeast
* once
* exactly (旧at)
* atMost
{| class="wikitable"
{| class="wikitable"
|+Table 8.2 Matchers
|+Table 8.1 Stubbing short hands
!Matcher
!short hand
!Meaning
!longer syntax
|-
|-
|<code>PHPUnit\Framework\MockObject\Matcher\AnyInvokedCount any()</code>
|<code>willReturn($value)</code>
|Returns a matcher that matches when the method it is evaluated for is executed zero or more times.
|<code>will($this->returnValue($value))</code>
|-
|<code>willReturnArgument($argumentIndex)</code>
|<code>will($this->returnArgument($argumentIndex))</code>
|-
|-
|<code>PHPUnit\Framework\MockObject\Matcher\InvokedCount never()</code>
|<code>willReturnCallback($callback)</code>
|Returns a matcher that matches when the method it is evaluated for is never executed.
|<code>will($this->returnCallback($callback))</code>
|-
|-
|<code>PHPUnit\Framework\MockObject\Matcher\InvokedAtLeastOnce atLeastOnce()</code>
|<code>willReturnMap($valueMap)</code>
|Returns a matcher that matches when the method it is evaluated for is executed at least once.
|<code>will($this->returnValueMap($valueMap))</code>
|-
|-
|<code>PHPUnit\Framework\MockObject\Matcher\InvokedCount once()</code>
|<code>willReturnOnConsecutiveCalls($value1, $value2)</code>
|Returns a matcher that matches when the method it is evaluated for is executed exactly once.
|<code>will($this->onConsecutiveCalls($value1, $value2))</code>
|-
|-
|<code>PHPUnit\Framework\MockObject\Matcher\InvokedCount exactly(int $count)</code>
|<code>willReturnSelf()</code>
|Returns a matcher that matches when the method it is evaluated for is executed exactly <code>$count</code> times.
|<code>will($this->returnSelf())</code>
|-
|-
|<code>PHPUnit\Framework\MockObject\Matcher\InvokedAtIndex at(int $index)</code>
|<code>willThrowException($exception)</code>
|Returns a matcher that matches when the method it is evaluated for is invoked at the given <code>$index</code>.
|<code>will($this->throwException($exception))</code>
|}
|}willReturnCallbackで呼び出し関数をまるごと別のものに置換できる。これが非常に便利。


===== Mock builder =====
引数を指定したい場合、with(引数)->willReturn()がシンプル。ただし、この方法は最後に指定した引数1個しか対応できない。引数違いで戻り値を模擬したいなら、willReturnMapかwillReturnCallbackで、引数に応じた戻り値を設定する。
https://chatgpt.com/share/68424beb-e544-800b-9292-b79f89cf412d


createStubやcreateMockは対象クラスを全部置換する。一部だけ置換したり、もともと存在しないメソッドを追加したり、複雑なことをしたい場合、getMockBuilderで取得する、Mock Builderを使う必要がある。メソッド一覧は以下となる。
willReturnCallbackのcallbackに渡される引数の記載はないが、モック対象メソッドに渡される引数が、その順番で渡される動きになっている。
 
===== Mock Objects =====
https://chatgpt.com/share/683e72cc-461c-800b-a7fc-5e8b417a44aa
 
メソッドが呼び出されたかどうかを検証するための、テストダブルへの置換をモッキングと呼ぶ。
 
テスト対象メソッド内で、特定メソッドの呼び出しを検査できる。MVCでViewへの値渡しなんかの検査にうってつけ。
 
モックオブジェクトにはテストスタブの機能も含んでいる。が、モッキングのための専用処理があるので、インスタンス生成時などのコストがやや大きい。理由がなければ、Stubを使った方がいい。


* <code>onlyMethods(array $methods)</code>Mock Builderオブジェクトで呼び出すことで、設定可能なテストダブルに置き換えるメソッドを指定できます。他のメソッドの動作は変更されません。各メソッドは、指定されたモッククラス内に存在している必要があります。
判断基準として、expects/withを使うならMock、そうじゃなければStubのようなイメージ。<syntaxhighlight lang="php">
* <code>addMethods(array $methods)</code>Mock Builderオブジェクトで呼び出すことで、指定されたモッククラスに(まだ)存在しないメソッドを指定できます。他のメソッドの動作は同じです。
<?php declare(strict_types=1);
* <code>setMethodsExcept(array $methods)</code>Mock Builderオブジェクトで を呼び出すことで、他のすべてのパブリックメソッドを置き換えながら、設定可能なテストダブルに置き換えないメソッドを指定できます。これは の逆の動作をします<code>onlyMethods()</code>。
use PHPUnit\Framework\TestCase;
* <code>setConstructorArgs(array $args)</code>元のクラスのコンストラクターに渡されるパラメーター配列を提供するために呼び出すことができます (デフォルトではダミー実装に置き換えられません)。
* <code>setMockClassName($name)</code>生成されたテストダブルクラスのクラス名を指定するために使用できます。
* <code>disableOriginalConstructor()</code>元のクラスのコンストラクターへの呼び出しを無効にするために使用できます。
* <code>disableOriginalClone()</code>元のクラスのクローンコンストラクターの呼び出しを無効にするために使用できます。
* <code>disableAutoload()__autoload()</code>テストダブルクラスの生成中に無効にするために使用できます。


use PHPUnit\Framework\TestCase;
class Subject
{
class MyClassTest extends TestCase
    protected $observers = [];
{
    protected $name;
    public function testAddColumnAddsNewColumn()
    {
        // fetchDataだけをスタブ化する
        $stub = $this->getMockBuilder(MyClass::class)
                      ->onlyMethods(['fetchData']) // ← ここでfetchDataだけモック化
                      ->getMock();
        // fetchDataが返す想定の配列をセット
        $stub->method('fetchData')->willReturn([
            ['id' => 1, 'name' => 'Alice'],
            ['id' => 2, 'name' => 'Bob'],
        ]);
        // addColumnを呼び出してテスト
        $result = $stub->addColumn();
        // 検証
        $this->assertEquals('value', $result[0]['new_column']);
        $this->assertEquals('value', $result[1]['new_column']);
    }
}
上記のように、最終的にgetMock()でインスタンスを取得して、methodなどで戻り値を設定する。


===== MVC =====
    public function __construct($name)
https://chatgpt.com/share/68395ad2-678c-800b-b27a-19c58e3a3a0a
    {
        $this->name = $name;
    }


MVC系のアプリで、Viewにセットする値・変数をチェックしたいことがある。
    public function getName()
 
    {
この場合、ControllerでViewにセットしているメソッドを試験する。
        return $this->name;
$controller-><view>->assign()
    }
$controller->setApp()
これらのメソッドを、expects/withで引数を検査する。willReturnはいらない。assert系のメソッドも呼ばなくていい。expects/withで引数の検査をするイメージ。


===== expects =====
    public function attach(Observer $observer)
https://chatgpt.com/share/682e8fd6-376c-800b-a01e-faa832b0fd07
    {
        $this->observers[] = $observer;
    }


「[https://docs.phpunit.de/en/9.6/test-doubles.html 8. Test Doubles — PHPUnit 9.6 Manual]」でモックオブジェクトを作成時に、模擬したメソッドの設定時に、expectsを使っている。
    public function doSomething()
    {
        // Do something.
        // ...


このexpectsは該当メソッドが呼ばれたか、呼ばれなかったかを試験するためのもの。例えば、if文で条件分岐していて、その中で該当メソッドが呼ばれたかどうかをチェックできる。
        // Notify observers that we did something.
        $this->notify('something');
    }


一本道で、呼び出し有無を気にしなくていいなら、expectsは不要。if文の条件を気にしなくて、一緒に試験できるのが便利。
    public function doSomethingBad()
    {
        foreach ($this->observers as $observer) {
            $observer->reportError(42, 'Something bad happened', $this);
        }
    }


なお、expectsを使わない場合、同名で競合するので、methodという名前のメソッドが、モックオブジェクトにあってはいけない。ある場合、expects($this->any())でexpectsを挟む必要がある。
    protected function notify($argument)
    {
        foreach ($this->observers as $observer) {
            $observer->update($argument);
        }
    }


ただ、methodという名前のメソッドがあることは普通ないと思う。
    // Other methods.
}


このexpectsは「[https://github.com/sebastianbergmann/phpunit/blob/5226e323514a73151c1c7909af224c01a1bbe9aa/src/Framework/MockObject/Runtime/Interface/MockObject.php#L19 phpunit/src/Framework/MockObject/Runtime/Interface/MockObject.php at 5226e323514a73151c1c7909af224c01a1bbe9aa · sebastianbergmann/phpunit]」
class Observer
interface MockObject extends Stub
{
{
    public function update($argument)
    public function expects(InvocationOrder $invocationRule): InvocationStubber;
    {
}
        // Do something.
これが定義な模様。引数にはInvocationOrderをとる。InvocationOrderは「[https://github.com/sebastianbergmann/phpunit/blob/main/src/Framework/MockObject/Runtime/Rule/InvokedCount.php phpunit/src/Framework/MockObject/Runtime/Rule/InvokedCount.php at main · sebastianbergmann/phpunit]」などで継承されている。
    }


===== with =====
    public function reportError($errorCode, $errorMessage, Subject $subject)
モックオブジェクトのメソッドの引数の検証に使うメソッド。ソースコードは以下。
    {
        // Do something
    }


* [https://github.com/sebastianbergmann/phpunit/blob/e075df0a9d89a824ba66e12a7c23618d1a42faf3/src/Framework/MockObject/Runtime/Interface/InvocationStubber.php#L46 phpunit/src/Framework/MockObject/Runtime/Interface/InvocationStubber.php at e075df0a9d89a824ba66e12a7c23618d1a42faf3 · sebastianbergmann/phpunit]
    // Other methods.
* [https://github.com/sebastianbergmann/phpunit/blob/e075df0a9d89a824ba66e12a7c23618d1a42faf3/src/Framework/MockObject/Runtime/InvocationStubberImplementation.php#L133 phpunit/src/Framework/MockObject/Runtime/InvocationStubberImplementation.php at e075df0a9d89a824ba66e12a7c23618d1a42faf3 · sebastianbergmann/phpunit]
}


https://chatgpt.com/share/683d657e-8578-800b-8e71-b09dbce969dd
</syntaxhighlight>


「[https://docs.phpunit.de/en/9.6/test-doubles.html#mock-objects 8. Test Doubles — PHPUnit 9.6 Manual]」に記載がある。<blockquote>The <code>with()</code> method can take any number of arguments, corresponding to the number of arguments to the method being mocked. You can specify more advanced constraints on the method’s arguments than a simple match.</blockquote>モック対象メソッドの個数と同じ引数を受け取れる。
<syntaxhighlight lang="php">
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;


それぞれの引数の位置で、該当引数の検証条件を指定する。指定可能な内容は以下の3種類。
final class SubjectTest extends TestCase
{
    public function testObserversAreUpdated(): void
    {
        // Create a mock for the Observer class,
        // only mock the update() method.
        $observer = $this->createMock(Observer::class);


# リテラル値
        // Set up the expectation for the update() method
# Constraint: 指定可能なConstraints (制約) は「[https://docs.phpunit.de/en/9.6/assertions.html#appendixes-assertions-assertthat-tables-constraints 1. Assertions — PHPUnit 9.6 Manual]」にある。
        // to be called only once and with the string 'something'
# callback: callbackの引数は、検証対象の引数で、OKならtrueを返す。
        // as its parameter.
        $observer->expects($this->once())
                ->method('update')
                ->with($this->equalTo('something'));


配列要素数や、複数のConstraintを組み合わせたい場合などは、callbackを使うしかない。
        // Create a Subject object and attach the mocked
        // Observer object to it.
        $subject = new Subject('My subject');
        $subject->attach($observer);


setAppやassignなどで、同じメソッドで1番目の引数に応じて、2番目の内容が変わる場合、工夫が必要。
        // Call the doSomething() method on the $subject object
 
        // which we expect to call the mocked Observer object's
expectやwithでは検証しないで、willReturnCallbackを使うしかない。連想配列に、実際に渡ってきたキー・バリューのセットを格納して、実行後にまとめてassertするか、コールバック内でassertしてチェックする。
        // update() method with the string 'something'.
$mock->method('assign')
        $subject->doSomething();
      ->willReturnCallback(function ($key, $val) {
    }
          switch ($key) {
}
              case 'title':
                  PHPUnit\Framework\Assert::assertSame('マイページ', $val);
                  break;
              case 'user':
                  PHPUnit\Framework\Assert::assertInstanceOf(User::class, $val);
                  break;
              default:
                  PHPUnit\Framework\Assert::fail("Unexpected key: $key");
          }
      });


$assigned = [];
</syntaxhighlight>基本的な作り。
#createMock(<class>::class)で該当クラスのモックを作成。
$mock->method('assign')
#expectsに呼出回数条件のオブジェクトをセット。これはなくてもいい。
      ->willReturnCallback(function ($key, $val) use (&$assigned) {
#methodで対象メソッドを指定。
          $assigned[$key] = $val;
#withで該当メソッドの引数の検証条件を指定。
      });
#willReturnなどで戻り値を指定。
デフォルトで模擬実装はnullを返す。戻り値を変更したければ、will($this->returnValue())などを指定する。よく使うので短縮記法もある。
// テスト対象実行後にチェック
$this->assertSame('マイページ', $assigned['title'] ?? null);
$this->assertInstanceOf(User::class, $assigned['user'] ?? null);
willReturnCallback内のifでいい気がする。複雑なら後者のスパイ風で。


willReturnCallbackでやる場合、Viewに渡すメソッドのもともとの戻り値がnullだから問題ないが、そうでない場合は、ちゃんとreturnでダミーの値を返さないと、後続の処理で不都合出るので注意する。
===== Matcher =====
使えるメソッド。[https://github.com/sebastianbergmann/phpunit/blob/main/src/Framework/TestCase.php#L959<nowiki>] あたりからの一連のメソッドと思われる。</nowiki>


withで検査する場合、self::assertSame()でメソッドの検証はせずに、単にテスト対象メソッドを呼び出す。呼び出すと、withで仕込んだものが呼ばれて、中で検証するイメージになる。
* any
 
* never
===== DI =====
* atLeast
https://chatgpt.com/share/683faf08-f374-800b-ac40-b96b427d7365
* once
 
* exactly (旧at)
テスト対象クラスのプロパティーのインスタンスをテストダブルに置換する場合、注意が必要。だいたい、privateになっているから、DIで渡す前に、置換対象のメソッドの模擬を設定してから、コンストラクターに渡す必要がある。
* atMost
 
{| class="wikitable"
面倒くさかったら、テストダブルインスタンスの作成+メソッド設定を共通メソッドにしてもよいかもしれない。が、テストダブルの戻り値は、DB取得結果とかだと大事なので、手間だが1個ずつやった方がいいかもしれない。
|+Table 8.2 Matchers
protected function prepareLoggerWithLogReturn($value): LoggerInterface
!Matcher
{
!Meaning
    $logger = $this->createLoggerMock();
|-
    $logger->method('log')->willReturn($value);
|<code>PHPUnit\Framework\MockObject\Matcher\AnyInvokedCount any()</code>
    return $logger;
|Returns a matcher that matches when the method it is evaluated for is executed zero or more times.
}
|-
|<code>PHPUnit\Framework\MockObject\Matcher\InvokedCount never()</code>
$logger = $this->prepareLoggerWithLogReturn(true);
|Returns a matcher that matches when the method it is evaluated for is never executed.
|-
|<code>PHPUnit\Framework\MockObject\Matcher\InvokedAtLeastOnce atLeastOnce()</code>
|Returns a matcher that matches when the method it is evaluated for is executed at least once.
|-
|<code>PHPUnit\Framework\MockObject\Matcher\InvokedCount once()</code>
|Returns a matcher that matches when the method it is evaluated for is executed exactly once.
|-
|<code>PHPUnit\Framework\MockObject\Matcher\InvokedCount exactly(int $count)</code>
|Returns a matcher that matches when the method it is evaluated for is executed exactly <code>$count</code> times.
|-
|<code>PHPUnit\Framework\MockObject\Matcher\InvokedAtIndex at(int $index)</code>
|Returns a matcher that matches when the method it is evaluated for is invoked at the given <code>$index</code>.
|}


===== 外部static/グローバル関数の模擬 =====
===== Mock builder =====
https://chatgpt.com/share/68424beb-e544-800b-9292-b79f89cf412d
https://chatgpt.com/share/68424beb-e544-800b-9292-b79f89cf412d


例えば、ログインユーザーIDなど、引数に渡すまでもない共通値の取得が、SUTのメソッド内にあったりする。ただ、こういうstaticやグローバルなメソッド・関数は、PHPUnitで置換が難しい。
createStubやcreateMockは対象クラスを全部置換する。一部だけ置換したり、もともと存在しないメソッドを追加したり、複雑なことをしたい場合、getMockBuilderで取得する、Mock Builderを使う必要がある。メソッド一覧は以下となる。


回避方法がいくつかある。
* <code>onlyMethods(array $methods)</code>Mock Builderオブジェクトで呼び出すことで、設定可能なテストダブルに置き換えるメソッドを指定できます。他のメソッドの動作は変更されません。各メソッドは、指定されたモッククラス内に存在している必要があります。
 
* <code>addMethods(array $methods)</code>Mock Builderオブジェクトで呼び出すことで、指定されたモッククラスに(まだ)存在しないメソッドを指定できます。他のメソッドの動作は同じです。
# DIで対象staticメソッドクラスをプロパティーに持たせる。
* <code>setMethodsExcept(array $methods)</code>Mock Builderオブジェクトで を呼び出すことで、他のすべてのパブリックメソッドを置き換えながら、設定可能なテストダブルに置き換えないメソッドを指定できます。これは の逆の動作をします<code>onlyMethods()</code>。
# 1と似た考えで、ラッパークラスを作る。
* <code>setConstructorArgs(array $args)</code>元のクラスのコンストラクターに渡されるパラメーター配列を提供するために呼び出すことができます (デフォルトではダミー実装に置き換えられません)。
# 対象クラス内で、ラッパーメソッド (protected) を用意する。
* <code>setMockClassName($name)</code>生成されたテストダブルクラスのクラス名を指定するために使用できます。
* <code>disableOriginalConstructor()</code>元のクラスのコンストラクターへの呼び出しを無効にするために使用できます。
* <code>disableOriginalClone()</code>元のクラスのクローンコンストラクターの呼び出しを無効にするために使用できます。
* <code>disableAutoload()__autoload()</code>テストダブルクラスの生成中に無効にするために使用できます。


1や2が望ましいようだが、3のラッパーメソッド用意は簡単。ひとまず3でいい。
use PHPUnit\Framework\TestCase;
 
テストのためだけに意味ない関数を追加するように見える。が、「テストできないコードはそれだけで設計に問題がある」とも言える。そういうものと思っておくと良いらしい。
class MyClassTest extends TestCase
 
{
====Other====
     public function testAddColumnAddsNewColumn()
=====Test private/protected=====
    {
*[https://stackoverflow.com/questions/249664/best-practices-to-test-protected-methods-with-phpunit php - Best practices to test protected methods with PHPUnit - Stack Overflow]
        // fetchDataだけをスタブ化する
*[https://zenn.dev/ttskch/articles/c7dcd5c1188cdd PHPUnitでprivateメソッドをテストする]
        $stub = $this->getMockBuilder(MyClass::class)
*[https://qiita.com/ponsuke0531/items/6dc6fc34fff1e9b37901 privateとprotectedメソッドをPHPUnitでテストする方法 #PHP - Qiita]
                      ->onlyMethods(['fetchData']) // ← ここでfetchDataだけモック化
クラスのprivate/protectedメソッドのテストには工夫が必要となる。<syntaxhighlight lang="php">
                      ->getMock();
    /**
    * privateメソッドを実行する.
        // fetchDataが返す想定の配列をセット
    * @param object $sut テスト対象のインスタンス。
        $stub->method('fetchData')->willReturn([
    * @param string $method_name privateメソッドの名前。
            ['id' => 1, 'name' => 'Alice'],
     * @param array $param privateメソッドに渡す引数。
            ['id' => 2, 'name' => 'Bob'],
    * @return mixed 実行結果。
         ]);
    * @throws \ReflectionException 引数のクラスがない場合に発生.
    */
    private function doMethod(object $sut, string $method_name, array $param): mixed
    {
        // ReflectionClassをテスト対象のクラスをもとに作る.
        $reflection = new \ReflectionClass($sut);
        // メソッドを取得する.
        $method = $reflection->getMethod($method_name);
        // アクセス許可をする.
        $method->setAccessible(true);
        // メソッドを実行して返却値をそのまま返す.
        return $method->invokeArgs($sut, $param);
    }
</syntaxhighlight>ReflectionClassを使って取得できる。上記の関数のFormController部分を試験対象のクラスに差し替えればOK。getProperty/getValueでprivateプロパティーも取得可能。
 
https://chatgpt.com/c/673ebadb-1638-800b-89f0-b3ed131317da
 
基本はpublicメソッドのみテストすべきという考え方。
class MyClass {
    private function myPrivateMethod($a, $b) {
         return $a + $b;
    }
}
   
   
class MyClassTest extends PHPUnit\Framework\TestCase {
        // addColumnを呼び出してテスト
    public function testMyPrivateMethod() {
         $result = $stub->addColumn();
         $object = new MyClass();
   
   
         // Reflectionを使ってprivateメソッドにアクセス
         // 検証
         $reflection = new ReflectionClass($object);
         $this->assertEquals('value', $result[0]['new_column']);
        $method = $reflection->getMethod('myPrivateMethod');
         $this->assertEquals('value', $result[1]['new_column']);
        $method->setAccessible(true);
        // メソッドを呼び出してテスト
        $result = $method->invoke($object, 2, 3);
         $this->assertEquals(5, $result);
     }
     }
  }
  }
上記のように、最終的にgetMock()でインスタンスを取得して、methodなどで戻り値を設定する。


=====Test header=====
===== MVC =====
Ref: [https://stackoverflow.com/questions/9745080/test-php-headers-with-phpunit unit testing - Test PHP headers with PHPUnit - Stack Overflow].
https://chatgpt.com/share/68395ad2-678c-800b-b27a-19c58e3a3a0a


header関数を使用する場合、phpunitの標準出力と干渉して以下のエラーが出て試験できない。
MVC系のアプリで、Viewにセットする値・変数をチェックしたいことがある。
Cannot modify header information - headers already sent by (output started at .../vendor/phpunit/phpunit/src/Util/Printer.php:138)
回避方法が2種類ある。
#<code>@runInSeparateProcess</code>
#phpunit --stderr
1個目のアノテーションをテストメソッドに指定すると別プロセスでの実行になる。ただ、プロセス生成は時間がかかるため、試験が多いと効率が悪い。


2個目のphpunitの出力を標準エラーに出力させる方法がシンプルで効率もいい。phpunit.xmlに stderr="true"を指定するとキー入力を省略できる。こちらで対応しよう。
この場合、ControllerでViewにセットしているメソッドを試験する。
=====Test exit=====
$controller-><view>->assign()
Ref:
$controller->setApp()
*[https://qiita.com/kumagaias/items/5b1d95a897bae11f2a5a PHP でテストコードを意識したコーディング #PHPUnit - Qiita]
これらのメソッドを、expects/withで引数を検査する。willReturnはいらない。assert系のメソッドも呼ばなくていい。expects/withで引数の検査をするイメージ。
*[https://qiita.com/tenkoma/items/1ac9625b4233c5893812 echo + exit しているPHPコードをユニットテストで保護しながら改善する #PHP - Qiita]
*[https://uzulla.hateblo.jp/entry/2019/06/27/193210 header後にdieするテストのアンチパターン - uzullaがブログ]
*[https://stackoverflow.com/questions/23915434/ignore-exit-and-die-with-phpunit php - Ignore exit() and die() with PHPUnit - Stack Overflow]
*[https://stackoverflow.com/questions/1347794/how-do-you-use-phpunit-to-test-a-function-if-that-function-is-supposed-to-kill-p unit testing - How do you use PHPUnit to test a function if that function is supposed to kill PHP? - Stack Overflow]
header()後のexit()など、exit/dieを使用するコードがある。phpunit内でこれらがあると、テストも強制終了になる。


上記の別プロセスで実行していた場合、以下のエラーになる。
===== expects =====
Test was run in child process and ended unexpectedly
https://chatgpt.com/share/682e8fd6-376c-800b-a01e-faa832b0fd07
対処方法がいくつかある。
#exitを使わないコードに変更。
#isTestのようなフラグを元コードに入れてテスト可否で分岐してexitを回避。
#execで外部プロセスで実行してexitCodeを試験。
#exit/die部分だけ別関数に抽出してmockで置換?
<https://notabug.org/gnusocialjp/gnusocial/src/main/actions/apiaccountregister.php> のclientErrorが内部でexitする。


このclientErrorをwillなどで置換すればよさそう?
「[https://docs.phpunit.de/en/9.6/test-doubles.html 8. Test Doubles — PHPUnit 9.6 Manual]」でモックオブジェクトを作成時に、模擬したメソッドの設定時に、expectsを使っている。


===== sut =====
このexpectsは該当メソッドが呼ばれたか、呼ばれなかったかを試験するためのもの。例えば、if文で条件分岐していて、その中で該当メソッドが呼ばれたかどうかをチェックできる。
https://chatgpt.com/share/682ada62-d3e0-800b-9444-db245dc44183


試験対象のクラスをメンバー変数・プロパティーに格納する。その際の名前を何にするか?
一本道で、呼び出し有無を気にしなくていいなら、expectsは不要。if文の条件を気にしなくて、一緒に試験できるのが便利。


instance/classとかが思いつく。対話AIによると、sutというのがいいらしい。system under testの略。テスト対象の意味。試験の専門用語っぽい。短いのでこれでいいと思う。
なお、expectsを使わない場合、同名で競合するので、methodという名前のメソッドが、モックオブジェクトにあってはいけない。ある場合、expects($this->any())でexpectsを挟む必要がある。


===== 表示内容の試験 =====
ただ、methodという名前のメソッドがあることは普通ないと思う。
画面UIに該当文字列があるかどうかなどを試験したいことがある。
 
このexpectsは「[https://github.com/sebastianbergmann/phpunit/blob/5226e323514a73151c1c7909af224c01a1bbe9aa/src/Framework/MockObject/Runtime/Interface/MockObject.php#L19 phpunit/src/Framework/MockObject/Runtime/Interface/MockObject.php at 5226e323514a73151c1c7909af224c01a1bbe9aa · sebastianbergmann/phpunit]」
interface MockObject extends Stub
{
    public function expects(InvocationOrder $invocationRule): InvocationStubber;
}
これが定義な模様。引数にはInvocationOrderをとる。InvocationOrderは「[https://github.com/sebastianbergmann/phpunit/blob/main/src/Framework/MockObject/Runtime/Rule/InvokedCount.php phpunit/src/Framework/MockObject/Runtime/Rule/InvokedCount.php at main · sebastianbergmann/phpunit]」などで継承されている。


LaravelにはassertSeeというのがあるのでこれを使える。
===== with =====
モックオブジェクトのメソッドの引数の検証に使うメソッド。ソースコードは以下。


PHPUnit自体にはない。assertStringContainsStringなど、文字列試験メソッドを使って、自分でresponseを何かで取得して評価する。こういうのは基本は機能試験で行う内容。
* [https://github.com/sebastianbergmann/phpunit/blob/e075df0a9d89a824ba66e12a7c23618d1a42faf3/src/Framework/MockObject/Runtime/Interface/InvocationStubber.php#L46 phpunit/src/Framework/MockObject/Runtime/Interface/InvocationStubber.php at e075df0a9d89a824ba66e12a7c23618d1a42faf3 · sebastianbergmann/phpunit]
* [https://github.com/sebastianbergmann/phpunit/blob/e075df0a9d89a824ba66e12a7c23618d1a42faf3/src/Framework/MockObject/Runtime/InvocationStubberImplementation.php#L133 phpunit/src/Framework/MockObject/Runtime/InvocationStubberImplementation.php at e075df0a9d89a824ba66e12a7c23618d1a42faf3 · sebastianbergmann/phpunit]


===== 単体試験と機能試験 =====
https://chatgpt.com/share/683d657e-8578-800b-8e71-b09dbce969dd
https://chatgpt.com/share/682ada62-d3e0-800b-9444-db245dc44183


単体試験と機能試験がある。
「[https://docs.phpunit.de/en/9.6/test-doubles.html#mock-objects 8. Test Doubles — PHPUnit 9.6 Manual]」に記載がある。<blockquote>The <code>with()</code> method can take any number of arguments, corresponding to the number of arguments to the method being mocked. You can specify more advanced constraints on the method’s arguments than a simple match.</blockquote>モック対象メソッドの個数と同じ引数を受け取れる。


単体試験は、基本的にクラス単位。クラスのメソッドを試験するイメージ。
それぞれの引数の位置で、該当引数の検証条件を指定する。指定可能な内容は以下の3種類。


機能試験は、特定機能に関する、クラス・メソッドを試験する。1個の試験で、複数クラスを試験する違いがある。
# リテラル値
# Constraint: 指定可能なConstraints (制約) は「[https://docs.phpunit.de/en/9.6/assertions.html#appendixes-assertions-assertthat-tables-constraints 1. Assertions — PHPUnit 9.6 Manual]」にある。
# callback: callbackの引数は、検証対象の引数で、OKならtrueを返す。


実際のアプリ開発では、ユーザー動作や仕様の動作が重要だから、機能試験中心で問題ない気がする。
配列要素数や、複数のConstraintを組み合わせたい場合などは、callbackを使うしかない。


重要なところ、複雑なところ、バグの多いところをUnitTestで試験するのがいいと思う。メンテ不能なテストができてしまうのは避けたい。
setAppやassignなどで、同じメソッドで1番目の引数に応じて、2番目の内容が変わる場合、工夫が必要。


* tests
expectやwithでは検証しないで、willReturnCallbackを使うしかない。連想配列に、実際に渡ってきたキー・バリューのセットを格納して、実行後にまとめてassertするか、コールバック内でassertしてチェックする。
** Unit
$mock->method('assign')
      ->willReturnCallback(function ($key, $val) {
          switch ($key) {
              case 'title':
                  PHPUnit\Framework\Assert::assertSame('マイページ', $val);
                  break;
              case 'user':
                  PHPUnit\Framework\Assert::assertInstanceOf(User::class, $val);
                  break;
              default:
                  PHPUnit\Framework\Assert::fail("Unexpected key: $key");
          }
      });
 
$assigned = [];
$mock->method('assign')
      ->willReturnCallback(function ($key, $val) use (&$assigned) {
          $assigned[$key] = $val;
      });
// テスト対象実行後にチェック
$this->assertSame('マイページ', $assigned['title'] ?? null);
$this->assertInstanceOf(User::class, $assigned['user'] ?? null);
willReturnCallback内のifでいい気がする。複雑なら後者のスパイ風で。
 
willReturnCallbackでやる場合、Viewに渡すメソッドのもともとの戻り値がnullだから問題ないが、そうでない場合は、ちゃんとreturnでダミーの値を返さないと、後続の処理で不都合出るので注意する。
 
withで検査する場合、self::assertSame()でメソッドの検証はせずに、単にテスト対象メソッドを呼び出す。呼び出すと、withで仕込んだものが呼ばれて、中で検証するイメージになる。
 
===== DI =====
https://chatgpt.com/share/683faf08-f374-800b-ac40-b96b427d7365
 
テスト対象クラスのプロパティーのインスタンスをテストダブルに置換する場合、注意が必要。だいたい、privateになっているから、DIで渡す前に、置換対象のメソッドの模擬を設定してから、コンストラクターに渡す必要がある。
 
面倒くさかったら、テストダブルインスタンスの作成+メソッド設定を共通メソッドにしてもよいかもしれない。が、テストダブルの戻り値は、DB取得結果とかだと大事なので、手間だが1個ずつやった方がいいかもしれない。
protected function prepareLoggerWithLogReturn($value): LoggerInterface
{
    $logger = $this->createLoggerMock();
    $logger->method('log')->willReturn($value);
    return $logger;
}
$logger = $this->prepareLoggerWithLogReturn(true);
 
===== 外部static/グローバル関数の模擬 =====
https://chatgpt.com/share/68424beb-e544-800b-9292-b79f89cf412d
 
例えば、ログインユーザーIDなど、引数に渡すまでもない共通値の取得が、SUTのメソッド内にあったりする。ただ、こういうstaticやグローバルなメソッド・関数は、PHPUnitで置換が難しい。
 
回避方法がいくつかある。
 
# DIで対象staticメソッドクラスをプロパティーに持たせる。
# 1と似た考えで、ラッパークラスを作る。
# 対象クラス内で、ラッパーメソッド (protected) を用意する。
 
1や2が望ましいようだが、3のラッパーメソッド用意は簡単。ひとまず3でいい。
 
テストのためだけに意味ない関数を追加するように見える。が、「テストできないコードはそれだけで設計に問題がある」とも言える。そういうものと思っておくと良いらしい。
 
====Other====
=====Test private/protected=====
*[https://stackoverflow.com/questions/249664/best-practices-to-test-protected-methods-with-phpunit php - Best practices to test protected methods with PHPUnit - Stack Overflow]
*[https://zenn.dev/ttskch/articles/c7dcd5c1188cdd PHPUnitでprivateメソッドをテストする]
*[https://qiita.com/ponsuke0531/items/6dc6fc34fff1e9b37901 privateとprotectedメソッドをPHPUnitでテストする方法 #PHP - Qiita]
クラスのprivate/protectedメソッドのテストには工夫が必要となる。<syntaxhighlight lang="php">
    /**
    * privateメソッドを実行する.
    * @param object $sut テスト対象のインスタンス。
    * @param string $method_name privateメソッドの名前。
    * @param array $param privateメソッドに渡す引数。
    * @return mixed 実行結果。
    * @throws \ReflectionException 引数のクラスがない場合に発生.
    */
    private function doMethod(object $sut, string $method_name, array $param): mixed
    {
        // ReflectionClassをテスト対象のクラスをもとに作る.
        $reflection = new \ReflectionClass($sut);
        // メソッドを取得する.
        $method = $reflection->getMethod($method_name);
        // アクセス許可をする.
        $method->setAccessible(true);
        // メソッドを実行して返却値をそのまま返す.
        return $method->invokeArgs($sut, $param);
    }
</syntaxhighlight>ReflectionClassを使って取得できる。上記の関数のFormController部分を試験対象のクラスに差し替えればOK。getProperty/getValueでprivateプロパティーも取得可能。
 
https://chatgpt.com/c/673ebadb-1638-800b-89f0-b3ed131317da
 
基本はpublicメソッドのみテストすべきという考え方。
class MyClass {
    private function myPrivateMethod($a, $b) {
        return $a + $b;
    }
}
class MyClassTest extends PHPUnit\Framework\TestCase {
    public function testMyPrivateMethod() {
        $object = new MyClass();
        // Reflectionを使ってprivateメソッドにアクセス
        $reflection = new ReflectionClass($object);
        $method = $reflection->getMethod('myPrivateMethod');
        $method->setAccessible(true);
        // メソッドを呼び出してテスト
        $result = $method->invoke($object, 2, 3);
        $this->assertEquals(5, $result);
    }
}
 
=====Test header=====
Ref: [https://stackoverflow.com/questions/9745080/test-php-headers-with-phpunit unit testing - Test PHP headers with PHPUnit - Stack Overflow].
 
header関数を使用する場合、phpunitの標準出力と干渉して以下のエラーが出て試験できない。
Cannot modify header information - headers already sent by (output started at .../vendor/phpunit/phpunit/src/Util/Printer.php:138)
回避方法が2種類ある。
#<code>@runInSeparateProcess</code>
#phpunit --stderr
1個目のアノテーションをテストメソッドに指定すると別プロセスでの実行になる。ただ、プロセス生成は時間がかかるため、試験が多いと効率が悪い。
 
2個目のphpunitの出力を標準エラーに出力させる方法がシンプルで効率もいい。phpunit.xmlに stderr="true"を指定するとキー入力を省略できる。こちらで対応しよう。
=====Test exit=====
Ref:
*[https://qiita.com/kumagaias/items/5b1d95a897bae11f2a5a PHP でテストコードを意識したコーディング #PHPUnit - Qiita]
*[https://qiita.com/tenkoma/items/1ac9625b4233c5893812 echo + exit しているPHPコードをユニットテストで保護しながら改善する #PHP - Qiita]
*[https://uzulla.hateblo.jp/entry/2019/06/27/193210 header後にdieするテストのアンチパターン - uzullaがブログ]
*[https://stackoverflow.com/questions/23915434/ignore-exit-and-die-with-phpunit php - Ignore exit() and die() with PHPUnit - Stack Overflow]
*[https://stackoverflow.com/questions/1347794/how-do-you-use-phpunit-to-test-a-function-if-that-function-is-supposed-to-kill-p unit testing - How do you use PHPUnit to test a function if that function is supposed to kill PHP? - Stack Overflow]
header()後のexit()など、exit/dieを使用するコードがある。phpunit内でこれらがあると、テストも強制終了になる。
 
上記の別プロセスで実行していた場合、以下のエラーになる。
Test was run in child process and ended unexpectedly
対処方法がいくつかある。
#exitを使わないコードに変更。
#isTestのようなフラグを元コードに入れてテスト可否で分岐してexitを回避。
#execで外部プロセスで実行してexitCodeを試験。
#exit/die部分だけ別関数に抽出してmockで置換?
<https://notabug.org/gnusocialjp/gnusocial/src/main/actions/apiaccountregister.php> のclientErrorが内部でexitする。
 
このclientErrorをwillなどで置換すればよさそう?
 
===== sut =====
https://chatgpt.com/share/682ada62-d3e0-800b-9444-db245dc44183
 
試験対象のクラスをメンバー変数・プロパティーに格納する。その際の名前を何にするか?
 
instance/classとかが思いつく。対話AIによると、sutというのがいいらしい。system under testの略。テスト対象の意味。試験の専門用語っぽい。短いのでこれでいいと思う。
 
===== 表示内容の試験 =====
画面UIに該当文字列があるかどうかなどを試験したいことがある。
 
LaravelにはassertSeeというのがあるのでこれを使える。
 
PHPUnit自体にはない。assertStringContainsStringなど、文字列試験メソッドを使って、自分でresponseを何かで取得して評価する。こういうのは基本は機能試験で行う内容。
 
===== 単体試験と機能試験 =====
https://chatgpt.com/share/682ada62-d3e0-800b-9444-db245dc44183
 
単体試験と機能試験がある。
 
単体試験は、基本的にクラス単位。クラスのメソッドを試験するイメージ。
 
機能試験は、特定機能に関する、クラス・メソッドを試験する。1個の試験で、複数クラスを試験する違いがある。
 
実際のアプリ開発では、ユーザー動作や仕様の動作が重要だから、機能試験中心で問題ない気がする。
 
重要なところ、複雑なところ、バグの多いところをUnitTestで試験するのがいいと思う。メンテ不能なテストができてしまうのは避けたい。
 
* tests
** Unit
** Feature
** Feature


Unitは元ファイルのディレクトリー構成にして、Featureは機能単位。Featureで機能単位の試験を入れる。で、基本はFeatureを拡充させる。
Unitは元ファイルのディレクトリー構成にして、Featureは機能単位。Featureで機能単位の試験を入れる。で、基本はFeatureを拡充させる。
 
 
ファイル名・クラス名。Featureの方はXXFreatureTest.phpとかにすることが多いらしい。が、せっかくディレクトリー分けている意味がないので、Test.phpでいいでしょう。
ファイル名・クラス名。Featureの方はXXFreatureTest.phpとかにすることが多いらしい。が、せっかくディレクトリー分けている意味がないので、Test.phpでいいでしょう。
 
 
===== result cache =====
===== result cache =====
 
 
* [https://stackoverflow.com/questions/55091768/what-is-phpunit-result-cache php - What is .phpunit.result.cache - Stack Overflow]
* [https://stackoverflow.com/questions/55091768/what-is-phpunit-result-cache php - What is .phpunit.result.cache - Stack Overflow]
* [https://docs.phpunit.de/en/9.6/textui.html 3. The Command-Line Test Runner — PHPUnit 9.6 Manual]
* [https://docs.phpunit.de/en/9.6/textui.html 3. The Command-Line Test Runner — PHPUnit 9.6 Manual]
* [https://docs.phpunit.de/en/9.6/configuration.html 3. The XML Configuration File — PHPUnit 9.6 Manual]
* [https://docs.phpunit.de/en/9.6/configuration.html 3. The XML Configuration File — PHPUnit 9.6 Manual]
* [https://github.com/sebastianbergmann/phpunit/blob/42966d97ce3b66e65beb46411b4f72bf467746f2/src/Runner/ResultCache/DefaultResultCache.php#L37 phpunit/src/Runner/ResultCache/DefaultResultCache.php at 42966d97ce3b66e65beb46411b4f72bf467746f2 · sebastianbergmann/phpunit]
* [https://github.com/sebastianbergmann/phpunit/blob/42966d97ce3b66e65beb46411b4f72bf467746f2/src/Runner/ResultCache/DefaultResultCache.php#L37 phpunit/src/Runner/ResultCache/DefaultResultCache.php at 42966d97ce3b66e65beb46411b4f72bf467746f2 · sebastianbergmann/phpunit]
* [https://github.com/sebastianbergmann/phpunit/blob/42966d97ce3b66e65beb46411b4f72bf467746f2/src/TextUI/Configuration/Merger.php#L86 phpunit/src/TextUI/Configuration/Merger.php at 42966d97ce3b66e65beb46411b4f72bf467746f2 · sebastianbergmann/phpunit]
* [https://github.com/sebastianbergmann/phpunit/blob/42966d97ce3b66e65beb46411b4f72bf467746f2/src/TextUI/Configuration/Merger.php#L86 phpunit/src/TextUI/Configuration/Merger.php at 42966d97ce3b66e65beb46411b4f72bf467746f2 · sebastianbergmann/phpunit]
 
 
phpunitを実行すると、.phpunit.result.cacheファイルが作成される。
phpunitを実行すると、.phpunit.result.cacheファイルが作成される。
 
 
phpunit.xmlのcacheResultがデフォルトtrueになっており作成されている。
phpunit.xmlのcacheResultがデフォルトtrueになっており作成されている。
 
 
* phpunit.xml
* phpunit.xml
** phpunit.cacheResult: 初期値true。結果キャッシュ作成有無。
** phpunit.cacheResult: 初期値true。結果キャッシュ作成有無。
** phpunit.cacheResultFile: 初期値.phpunit.result.cache。ファイルパス。
** phpunit.cacheResultFile: 初期値.phpunit.result.cache。ファイルパス。
* option
* option
** --cache-result: キャッシュ結果を出力する (既定)。
** --cache-result: キャッシュ結果を出力する (既定)。
** --do-not-cache-result: キャッシュ結果を出力しない。
** --do-not-cache-result: キャッシュ結果を出力しない。
** --cache-result-file <file>: キャッシュ結果のパスを指定する (既定: ./.phpunit.result.cache)。
** --cache-result-file <file>: キャッシュ結果のパスを指定する (既定: ./.phpunit.result.cache)。
cacheResultFileは他にPHPUNIT_RESULT_CACHE環境変数でも設定できる模様。
cacheResultFileは他にPHPUNIT_RESULT_CACHE環境変数でも設定できる模様。
 
 
phpstanの結果キャッシュをvar/tmp/phpstanに配置しているので、これにならってvar/tmp/phpunit/.phpunit.result.cacheにするといいかも。
phpstanの結果キャッシュをvar/tmp/phpstanに配置しているので、これにならってvar/tmp/phpunit/.phpunit.result.cacheにするといいかも。
  <?xml version="1.0" encoding="UTF-8"?>
  <?xml version="1.0" encoding="UTF-8"?>
  <phpunit bootstrap="tests/bootstrap.php" cacheResultFile="var/tmp/phpunit/.phpunit.result.cache" colors="true">
  <phpunit bootstrap="tests/bootstrap.php" cacheResultFile="var/tmp/phpunit/.phpunit.result.cache" colors="true">
     <testsuites>
     <testsuites>
         <testsuite name="tests">
         <testsuite name="tests">
             <directory>tests</directory>
             <directory>tests</directory>
         </testsuite>
         </testsuite>
     </testsuites>
     </testsuites>
  </phpunit>
  </phpunit>
 
===== getallheaders() =====
 
* [https://stackoverflow.com/questions/41427359/phpunit-getallheaders-not-work http headers - PHPUnit - getallheaders not work - Stack Overflow]
* [https://www.php.net/manual/ja/function.getallheaders.php PHP: getallheaders - Manual]
 
Error: Call to undefined function getallheaders()
apache_request_headers()のエイリアス。サーバー固有のAPIはPHPUnit実行時は使えないので、bootstrap.phpに、代替関数を用意する。
if (!function_exists('getallheaders')) {
    function getallheaders() {
    $headers = [];
    foreach ($_SERVER as $name => $value) {
        if (substr($name, 0, 5) == 'HTTP_') {
            $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
        }
    }
    return $headers;
    }
}
 
===== PHP Fatal error:  Cannot declare interface PHPUnit_Framework_SelfDescribing, because the name is already in use in =====
      PHP Fatal error:  Cannot declare interface PHPUnit_Framework_SelfDescribing, because the name is already in use in /common/php/src/PHPUnit/Framework/SelfDescribing.php on line 58                                 
      Fatal error: Cannot declare interface PHPUnit_Framework_SelfDescribing, because the name is already in use in /common/php/src/PHPUnit/Framework/SelfDescribing.php on line 58                                     
      while running parallel worker
古いPHPUnitがある環境に、新しめのPHPUnitがある状態で、PHPStanを実行すると上記のエラーが出てしまった。
 
https://chatgpt.com/share/6837b3e4-01ac-800b-a547-164fcca8dbf8
 
古いPHPUnitと新しいPHPUnitとで、同じクラスでも指定方法が変わっている。その都合で、両方がinclude_pathにあると競合する。include_pathから除外する必要がある。
 
===== テストのグループ化 =====
https://chatgpt.com/share/68390cd8-5cc4-800b-afce-36c2abdc4b83
 
Jestにはdescribe()内にit()を入れるような、テストメソッドのネスト構造ができた。PHPUnitにはそれがない。
 
@groupアノテーションがあるが、これは--groupや--exclude-groupで指定したグループに属するテストを対象にしたり、除外したりするためのもの。ネストとは概念が異なる。
 
ネスト構造はクラスとメソッドの親子関係での表現になる。分けたかったら、テストメソッドの命名規則などになる。
 
===== 親クラスBaseTestCase =====
テストクラスで、共通の処理とかしたいことがある。親クラスにまとめたい。


===== getallheaders() =====
PHPUntはTestCaseがテスト対象じゃないことを意味するので、親クラスも最後はTestではなくてTestCaseにする。


* [https://stackoverflow.com/questions/41427359/phpunit-getallheaders-not-work http headers - PHPUnit - getallheaders not work - Stack Overflow]
https://grok.com/share/c2hhcmQtMw%3D%3D_0cfa311d-b290-4d5b-9183-ca7f7efb2d53
* [https://www.php.net/manual/ja/function.getallheaders.php PHP: getallheaders - Manual]


Error: Call to undefined function getallheaders()
BaseTestCaseとか。
apache_request_headers()のエイリアス。サーバー固有のAPIはPHPUnit実行時は使えないので、bootstrap.phpに、代替関数を用意する。
if (!function_exists('getallheaders')) {
    function getallheaders() {
    $headers = [];
    foreach ($_SERVER as $name => $value) {
        if (substr($name, 0, 5) == 'HTTP_') {
            $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
        }
    }
    return $headers;
    }
}


===== PHP Fatal error:  Cannot declare interface PHPUnit_Framework_SelfDescribing, because the name is already in use in =====
共通テストメソッドはassertXxxにする。testXxxはPHPUnitのtest対象になるので。
      PHP Fatal error:  Cannot declare interface PHPUnit_Framework_SelfDescribing, because the name is already in use in /common/php/src/PHPUnit/Framework/SelfDescribing.php on line 58                                 
      Fatal error: Cannot declare interface PHPUnit_Framework_SelfDescribing, because the name is already in use in /common/php/src/PHPUnit/Framework/SelfDescribing.php on line 58                                     
      while running parallel worker
古いPHPUnitがある環境に、新しめのPHPUnitがある状態で、PHPStanを実行すると上記のエラーが出てしまった。


https://chatgpt.com/share/6837b3e4-01ac-800b-a547-164fcca8dbf8
===== プロセス =====
https://grok.com/share/c2hhcmQtMw%3D%3D_e1586c8a-b519-44be-b04d-185e61969ae2


古いPHPUnitと新しいPHPUnitとで、同じクラスでも指定方法が変わっている。その都合で、両方がinclude_pathにあると競合する。include_pathから除外する必要がある。
phpunitは基本的に同一プロセスで全テストを実行する。その都合で、あるファイルでincludeしたシンボルはグローバルに存在するので、他のファイルにも影響ある。


===== テストのグループ化 =====
あるファイルAでincludeしてシンボルAAが登場して、別のファイルBでincludeするファイル内にシンボルBBがある場合、エラーになる。
https://chatgpt.com/share/68390cd8-5cc4-800b-afce-36c2abdc4b83


Jestにはdescribe()内にit()を入れるような、テストメソッドのネスト構造ができた。PHPUnitにはそれがない。
対策がいくつかある。


@groupアノテーションがあるが、これは--groupや--exclude-groupで指定したグループに属するテストを対象にしたり、除外したりするためのもの。ネストとは概念が異なる。
* 名前空間を活用してシンボル衝突を防ぐ。
* phpunit --process-isolationのオプションを指定。ただし、遅くなる。
* テストスイートの分離。


ネスト構造はクラスとメソッドの親子関係での表現になる。分けたかったら、テストメソッドの命名規則などになる。
根本的には、同じシンボルが複数ファイルでグローバルで登場するのがまずい。名前空間、require_once、function_existsとかでガードすべき。元ファイルが想定していないなら、--process-isolationオプションを使う。
[[Category:PHP]]
[[Category:PHP]]

2025年10月2日 (木) 15:43時点における最新版

PHPDoc

PHPDoc reference

About

PHPDocはPHP流のコメントのスタイル。phpDocumentorはPHPDocからドキュメントを生成するツール。PHPDocを解析して文書を作成したり利用するソフトはいろいろある。例えば、VS CodeやPHPStormなどのIDEもPHPDocを使う。

Definition of a Type

phpDocumentor

ABNF

type-expression          = 1*(array-of-type-expression|array-of-type|type ["|"])
array-of-type-expression = "(" type-expression ")[]"
array-of-type            = type "[]"
type                     = class-name|keyword
class-name               = 1*CHAR
keyword                  = "string"|"integer"|"int"|"boolean"|"bool"|"float"
                           |"double"|"object"|"mixed"|"array"|"resource"|"scalar"
                           |"void"|"null"|"callable"|"false"|"true"|"self"

nullable

型でnullを許容する場合、PHPDocではint|nullのように|で型をunion型のように書く。

PHP 7.1以上だと?intの書き方が許容されているが、PHPDocは|だけ。null|のように先頭に持ってくるのがわかりやすい。型は長いことがあるから。

Tag

General

よく使う@param/@returnの構文。

<type-expression          = 1*(array-of-type-expression|array-of-type|type ["|"])
array-of-type-expression = "(" type-expression ")[]"
array-of-type            = type "[]"
type                     = class-name|keyword
class-name               = 1*CHAR
keyword                  = "string"|"integer"|"int"|"boolean"|"bool"|"float"
                           |"double"|"object"|"mixed"|"array"|"resource"|"scalar"
                           |"void"|"null"|"callable"|"false"|"true"|"self"

クラス名以外は全小文字。

基本は @<directive> <Type> <name> <description> の書式。スペース区切り。

  • @property: __get/__setのマジックプロパティーを使う場合にクラスの注釈部で指定する。基本は使わない。が、型定義のない親クラスの型の明示にも使える。
  • @var: 変数、プロパティー、定数で使用する。一番よく使う。
/** @var int $int This is a counter. */
$int = 0;

// There should be no docblock here.
$int++;

class Foo
{
    /**
     * Full docblock with a summary.
     *
     * @var int
     */
    const INDENT = 4;

    /** @var string|null Short docblock, should contain a description. */
    protected $description = null;

    public function setDescription($description)
    {
        // There should be no docblock here.
        $this->description = $description;
    }
}

inline tag reference

inline tag reference - phpDocumentor

以下のタグはインラインでも使用可能。ツールによって若干解釈方法が異なる。

  • @example
  • @internal
  • @inheritdoc
  • @link
  • @see

以下の書式で全体を波括弧で囲んで使う。

{@tag value}

inheritance

phpDocumentor

以下の継承関係、グループがある。

Elements Inherited tags
Any @author, @version, @copyright
Classes and Interfaces @category, @package, @subpackage
Methods @param, @return, @throws
Properties @var

Methods

@return

phpDocumentor

https://chatgpt.com/c/67998298-f248-800b-a921-a47210ab8d72

関数やメソッドの説明のために関数定義の前に書くのが正しい使い方。

return文の直前に書くものではない。returnのところを説明したかったら、通常コメント。

ただ、関数の@returnでreturnが何を返すのかを書いた方がいい。

@throws

例外が発生する場合に記載する。複数の種類の例外がありえるなら、その数だけ@throwsを記載する。

注意喚起

https://chatgpt.com/c/67998298-f248-800b-a921-a47210ab8d72

注意が必要な挙動を文書化したい場合。@warnや@noticeはない。代わりの気泡を使う。

タグ 使い道
@todo 修正や機能追加が必要な場合
@deprecated 非推奨の機能を警告
@throws 例外が発生する可能性を示す
// /* */ 特定の処理に対する注意を記述

迷ったら/** */でいいと思われる。

マジックメソッド

https://chatgpt.com/c/67a5c298-bdf8-800b-9f42-8618f36274ab

独自クラスで__getや__setで独自のマジックメソッドで動的プロパティーを実装している場合。PHPDocがないと型や補完が一切されなくて辛い。

/**
 * クラスの説明
 *
 * @property-read string $name ユーザーの名前
 * @property int $age ユーザーの年齢
 */
class User {
    private array $data = [];

    public function __get(string $name) {
        return $this->data[$name] ?? null;
    }

    public function __set(string $name, $value): void {
        $this->data[$name] = $value;
    }
}

$name/$ageのプロパティー名のプロパティーが動的に追加されるなら、@propertyで変数名と型を書いておくと補完してくれる。

https://chatgpt.com/c/67a9546f-904c-800b-9459-af827e0b9fee

__getに@returnしてもいいが、これはプロパティーの情報がない。__getに@returnするくらいなら、@property(-read) でプロパティーとセットで書いた方がいい。

Other

Array

PHPでよく使うArray。PHPDocでの表現方法がある。

  1. @return array
  2. @return int[]
  3. @return int[][]
  4. @return (int|string)[]

PHPDocでの配列の表現方法は以上。単純配列の配列の配列の場合、[]を増やせばいい。

連想配列については説明なし。arrayしかない。単純配列はPHPDocのこれで問題ない。問題は連想配列。

ArrayShape

PHPDocでは未対応の連想配列だが、他の静的解析ツールの独自拡張で対応している。ArrayShapeという注釈。generics arrayとも呼んでいる。

いくつかの記法がある。

function getUserData(): array {
    return [
        'name' => 'John Doe',
        'age' => 30,
        'email' => 'john.doe@example.com',
    ];
}
 * @return array{name: string, age: int, email: string}
/**
 * @return array{
 *     user: array{name: string, age: int, email: string},
 *     address: array{city: string, zip: int}
 * }
 */
@var array{foo: int, bar?: string} <var-name> 説明。
@var array<array{foo: int, bar?: string}> <var-name> 説明。

【PHP】タイプヒンティングをより強力にするArrayShape - デザインワン・ジャパン Tech Blog

こういう書式。ただし、この書式だと要素は説明できないので変数全体のところに説明を入れる。

連想配列のキーが可変の場合。以下のように<>で型だけ指定する。{}はキー名が決まっている場合。

array<string, int>

リスト型で、要素ごとに型が決まっているなら、キー部分を数字にすると型を明示できる。

 * @return array{0: string, 1: int, 2: string}
/**
 * @param array{
 *     0: string, // ユーザー名
 *     1: int,    // 年齢
 *     2: bool    // アクティブフラグ
 * } $userData
 */
function processUser(array $userData): void {
    // ...
}
/**
 * @param list{
 *     string, // ユーザー名
 *     int,    // 年齢
 *     bool    // アクティブフラグ
 * } $userData
 */
function processUser(array $userData): void {
    // ...
}
/** @var array{0: string, 1: int, 2: bool} $userData ユーザー情報:名前, 年齢, アクティブ */

3番目の形式がいい気がする。

stdClass

https://chatgpt.com/c/67b833b8-1d20-800b-bc27-16dfa133e3b0

関数で複数の値を一度に返したい時がある。その際の選択肢はオブジェクトか配列。

オブジェクトはstdClass。stdClassは動的にプロパティーを設定する前提。だが、phpdocでうまく認識してくれない。

@var stdClassでこの後ろに説明を掛けるくらい。

配列の方がまだいいか。

プロパティーの継承

https://chatgpt.com/c/67cab3d0-eac4-800b-96c3-85ea480d1d91

子クラス側で再定義してしまうと、そちらが優先される。再定義しなければ、親クラスのプロパティーのphpdocが使われる。

型を使いこなすためのPHPDocの書き方 - RAKUS Developers Blog | ラクス エンジニアブログ

例えば、外部のフレームワークを使っていて、そのベースクラス側で型定義がない場合、継承後に@propertyで使いたいプロパティーの型を明示できる。

この方法を使わない場合、同じプロパティーを初期値指定で再定義しないといけない。@propertyで指定するとそれを回避できる。

phpDocumentor

Ref: Home | phpDocumentor.

PHPのソースコードにコメントを残す際に、構文に従って記載すると、ツールで表示したり、文書に出力できたりする。ソースコードリーディングにも役立つので、積極的に記載したほうがよさそう。構文を整理しておく。

特に記法が大事。

ファイル冒頭の<?php の直後あたりに書くと、ファイルレベルのDocBlockになる。逆にclassの直前などに書くと、ファイル冒頭でもclassレベルになる。

以下の要素に前置できる。

  • require(_once)
  • include(_once)
  • class
  • interface
  • trait
  • function (including methods)
  • property
  • constant
  • variables, both local and global scope.

Inheritance

DocBlockはSummary/Descriptionを上書きしたり、拡張できる。@inheritdocを使う。

要素ごとに以下のタグを継承する。

Elements Inherited tags
Any @author, @version, @copyright
Classes and Interfaces @category, @package, @subpackage
Methods @param, @return, @throws
Properties @var

@subpackageタグは同じ@packageの親クラスのときだけ継承される。

一番よく使う変数の説明は@var (変数、プロパティー) と@param (関数引数)。

DocBlock

DocComments

DocBlockはDocCommentと呼ばれるコメントで囲まれる。DocCommentは/**で始まり、*/で終わる。そして、DocComment内の行の先頭は* で始まるべき。

<?php
/**
 * This is a DocBlock.
 */
function associatedFunction()
{
}

/** This is a single line DocComment. */

複数行形式と1行形式がある。

変数などの説明には1行形式でいいと思う。

PHPDoc

DocBlockは3部構成。

  1. Summary=短い説明。改行直前の.か空行で終わり。
  2. Description=長い説明。アルゴリズムの機能や、使用方法、例など。最初のタグか、改行、DocBlockの終端で終わる。
  3. Tags/Anntations=要素のメタ情報。新しい行の@から始まる。

具体例。

<?php
/**
 * A summary informing the user what the associated element does.
 *
 * A *description*, that can span multiple lines, to go _in-depth_ into
 * the details of this element and to provide some background information
 * or textual references.
 *
 * @param string $myArgument With a *description* of this argument,
 *                           these may also span multiple lines.
 *
 * @return void
 */
 function myFunction($myArgument)
 {
 }

Summary

/**
 * This is a summary
 *
 * This is a description
 */
/**
 * This is a summary.
 * This is a description
 */

https://chatgpt.com/c/67400d8d-0300-800b-8db3-f3453fee3355

なお、summary/descriptionとtagの間は空行はあってもなくてもいい。縦に間延びするのでなくていいと思う。

空行があると、*の直後に終端スペースが残ったりしてごみが入ることがあるし。

Usage/code example

https://chatgpt.com/c/674687f5-6fc0-800b-95a6-34758bfda434

サンプルコードの埋め込み表示はmarkdownのコードブロック記法を使う。

/**
 * この関数は数値を二倍にします。
 *
 * 使用例:
 * ```php
 * $result = double(5);
 * echo $result; // 出力: 10
 * ```
 *
 * @param int $number 入力値
 * @return int 倍になった値
 */
function double(int $number): int {
    return $number * 2;
}

外部ファイルにサンプルファイルがある場合、@exampleタグで外部ファイルのパスを相対URIか絶対URIで指定できる (@example - phpDocumentor)。phpdocでフォーマット時におそらく、リンクなどで中身をみれるようにしてくれるのだと思う。

https://chatgpt.com/c/67998298-f248-800b-a921-a47210ab8d72

他に@see/@tutorialも使える。が、専用のものはない。/** */ 内でUsageなどで普通の文章で説明するしかない。

Package manager

Composer

Composer

PHPのパッケージ管理システム。PHP v5.3.2以上で動作する。

Install

Ref: インストール: Composer | モダンなPHPのパッケージマネージャー – senooken JP.

LOCAL=~/.local PKG=composer VER= DIR=$LOCAL/stow/$PKG-$VER/bin
[ -e installer ] || wget https://getcomposer.org/installer
[ -e installer ] || curl -LO https://getcomposer.org/installer
mkdir -p $DIR
php installer --install-dir="$DIR" --filename=$PKG

公式サイトからinstallerをダウンロードしてそれを使って任意の場所に設置する。

Composer

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php composer-setup.php --install-dir=$HOME/.local/bin --filename=composer

これでもいい。

Usage

Basic usage - Composer

Composerを使う場合,composer.jsonファイルを用意する。このファイルはプロジェクトの依存関係を記載する。VCSで管理すべきファイルだ。

このファイルに使用するライブラリーを以下のように記入する。

<{
    "require": {
        "monolog/monolog": "1.0.*"
    }
}

composer.jsonに指定する最初の項目はrequireキーだ。このキーで依存パッケージをComposerに知らせる。パッケージ名とバージョンを指定する。

新規にパッケージを追加する場合は、以下のコマンドでインストールとcomposer.jsonへの追記を行えます。同時に、composer.lockファイルも作成されます。composer.lockも管理すべきファイル。

composer require "monolog/monolog:1.0.*"

パッケージ名はベンダー名とプロジェクト名から構成される。

1.0.*は1.0の任意のバージョンを示す。

composer.jsonを用意したら,以下のようにcomposerのinstallコマンドを実行する。

php composer.phar install
composer install

これにより,vendorディレクトリーにパッケージがインストールされる。デフォルトでrequire-devの開発用パッケージもインストールする。除外したければ、--no-devのオプションを指定する。

プロジェクトにgitを使っている場合,.gitignoreにvendorディレクトリーを追加したほうがいい。

Composerによるインストールが完了すると,composer.lockファイルにダウンロードしたパッケージとバージョンを出力する。composer.lockをプロジェクトリポジトリーに追加して,プロジェクトメンバー全員が同じバージョンのパッケージを使用する。

composer.lockが存在するプロジェクトで上記コマンドを実行する場合,composer.jsonの内容に加えて,composer.lockの内容も参照されて,composer.lockと同じバージョンがインストールされる。

パッケージを最新バージョンに更新したい場合,composer updateコマンドを使う。このコマンドを実行すると,最新バージョンをインストールして,composer.lockも更新する。動作としては,composer.lockを削除後にcomposer installを実行することと等しい。composer updateは基本的には使わない。

updateやinstallの後にパッケージ名を指定すると,指定したパッケージだけ更新やインストールできる。

composer update monolog/monolog

Composerでインストール可能な主なパッケージは,Packagistリポジトリーに配置されている。

Autoloading (自動読み込み )

About

Basic usage - Composer ライブラリーの自動読み込みのために,Composerはvendor/autoload.phpファイルを生成する。以下のように,ライブラリーのクラスを使用するファイルの先頭でこのファイルを読み込めば使えるようになる。

<php
require __DIR__ . '/vendor/autoload.php';

$log = new Monolog\Logger('name');
$log->pushHandler(new Monolog\Handler\StreamHandler('app.log', Monolog\Logger::WARNING));
$log->addWarning('Foo');

このほかに、composer.jsonのautoload欄に指定することで,ライブラリー以外の自分のコードも自動読み込みの対象にできる。

{
    "autoload": {
        "psr-4": {"Acme\\": "src/"}
    }
}
PSR-4

PSR-4: Autoloader - PHP-FIG psr-4はPHPの標準仕様。 名前空間、クラスメイト、ファイルパスとの対応関係を記している。

  1. 名前空間の単位ごとに、ディレクトリーに対応。
  2. 大文字小文字は区別。
  3. 大文字小文字を区別して、ファイル名は.phpの拡張子で終わる。
dump-autoload

composer.jsonを編集した場合、composer dump-autoloadを実行してvendor/autolaod.phpを必ず更新します。

https://chatgpt.com/share/6837bdb1-1798-800b-a982-650ce38bc30a

なお、require_onceではなくrequireになっているのには理由がある。

  • そもそも何回も読み込む用途ではない。普通アプリのルートで1回しか読み込まない。
  • 万が一複数回読み込んでも、内部的にspl_autoload_registerを呼んでいて、重複登録されない。
  • requireのほうが読み込み確認がないぶんわずかに早い。

Libraries

Libraries - Composer

自前のライブラリーをComposerでインストール可能な形式にする方法がある。

Every project is a package

ディレクトリーにcomposer.jsonがあると、そのディレクトリーはパッケージになる。プロジェクトとパッケージの違いは、名前の有無。プロジェクトは名前のないパッケージという扱いになる。

パッケージをインストール可能にするにあたって、composer.jsonに最低限名前が必要。

{
    "name": "acme/hello-world",
    "require": {
        "monolog/monolog": "1.0.*"
    }
}

acme/hello-worldというプロジェクトになる。acmeはベンダー名で、ベンダー名は必須。

ベンダー名に迷う場合、GitHubのユーザー名が適している。パッケージ名は小文字必須。単語区切りは-にするのが慣例。

Library Versioning

VCSでパッケージを管理している場合、composerはVCSからバージョンを自動で判別する。VCSを使っていない場合だけ、versionプロパティーを追加する。

{
    "version": "1.0.0"
}
Publishing to a VCS

composer.jsonを用意したらVCSのリモートリポジトリーに公開する。ベンダー名とユーザー名は不一致でも問題ない。

公開したパッケージを取り込む場合、requireで指定する。

{
    "name": "acme/blog",
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/username/hello-world"
        }
    ],
    "require": {
        "acme/hello-world": "dev-master"
    }
}

パッケージ名hello-worldに必要なリポジトリーの情報をrepositoriesで指定している。たぶん、末尾のパッケージ名とリポジトリー名は一致が必要。

Publishing to packagist

VCSでの公開のケースは以上。ただ、repositoriesの情報は省略する方法がある。これは、Packagistに登録している場合。composerはpackagitstから同盟パッケージを探す。公開して問題ないなら、Packagistへの登録を検討する。

Light-weight distribution packages

.githubディレクトリーのように、パッケージに不要なファイルがある。

.gitattributesでパッケージやzipに含めないファイルを指定できる。

// .gitattributes
/demo export-ignore
phpunit.xml.dist export-ignore
/.github/ export-ignore

以下のコマンドで確認できる。

git archive branchName --format zip -o file.zip

パッケージに含まれないだけで、Gitリポジトリーには入っている。

Distribution

php - How to include Composer within a GitHub repository - Stack Overflow

Composerを使ったプロジェクトを一般配布する場合。ユーザーにcomposer installしてもらえるならリポジトリーのまま配布すればいい。

composer installしてもらえないならば、以下の2の方法をとるしかない。

  1. vendorディレクトリーをコミットに含める。
  2. リリース用zipにのみvendorを含める (例: Yoast/wordpress-seo: Yoast SEO for WordPress)。

Yoast SEOはGruntでリリース用に用意している模様。

composer.jsonにscriptsを含めることできるし、package.jsonでもOK。

Repositories

Repositories - Composer

パッケージとリポジトリーの概念。

Concepts
Package

パッケージは何かを含むディレクトリー。名前とバージョンを含んでいて、パッケージを識別する。

Repository

パッケージのソース。パッケージとバージョンのリスト。

Types

リポジトリーの種類を指定する。

  • composer: デフォルト
  • vcs: gitリポジトリー。ローカルのgitリポジトリーもurlにパス指定で対応できるが、基本はURL。ローカルはtype=path指定で対応する。
  • package: zipファイルなど。
Hosting your own

外部のホスティングサービス、サイトなどに配置していないものをリポジトリーとして使いたいことがあったりする。

例えば、社内のライブラリーのビルド結果とか。プライベートなリポジトリーとか。

  • type=artifact: ZIPやtarファイル。ルートにcomposer.jsonを含む。
  • type: path: 絶対パス、相対パスでローカルディレクトリーを指定。VCSの場合、type=vcsでurlを相対パスでもできるが、ローカルの場合はpathにするのがよさそう。。バージョンは現在のブランチ・タグから推測する。あるいは、パッケージのcomposer.jsonで明示。どれでも解決できない場合、dev-masterとみなす。

Articles

Scripts

composerの実行中に生じるイベントに対して行うコールバックがScriptsの基本的な概念。それとは別に、独自コマンドも追加できる。

testなどちょっとした定型コマンドの実行に便利。

Event names

いくつかイベントがある。

  • Command Events
    • post-install-cmd: install実行後。
Writing custom commands

composer.jsonの以下のプロパティー定義でcomposer testでphpunitを実行できる。

{
    "scripts": {
        "test": "phpunit",
        "do-something": "MyVendor\\MyClass::doSomething"
        "my-cmd": "MyVendor\\MyCommand"
    }
}

コマンドにオプションを渡す場合、--で区切る。

composer test -- --filter <pattern>

これでtestに--filter <pattern>を渡す。

コマンドの他に、PHPコード自体も実行できる。

composer do-something arg

これでstatic function doSomething(\Composer\Script\Event $event)が呼ばれる。

Executing PHP scripts

@phpや@composerと@を前置すると、composer.jsonを呼び出している@php/@composerを流用してコマンドを実行できる。

{
    "scripts": {
        "test": [
            "@php script.php",
            "phpunit"
        ]
    }
}

また、通常のシェルスクリプトを記述する場合、PHP_BINARYの環境変数で、実行中PHPのフルパスを参照できる。

Versions and constraints

Versions and constraints - Composer

composer requireなどで指定するパッケージのバージョンにはいくつか記法がある。このバージョン部分は、composerではversion constraint (バージョン制約) と呼んでいる。このバージョン制約で、チェックアウト対象を判断する。

~/my-library$ git branch
v1
v2
my-feature
another-feature
~/my-library$ git tag
v1.0
v1.0.1
v1.0.2
v1.1-BETA
v1.1-RC1
v1.1-RC2
v1.1
v1.1.1
v2.0-BETA
v2.0-RC1
v2.0
v2.0.1
v2.0.2
tag

基本的に、composerはタグを扱う。

上記のようなタグの場合、composerは先頭のvなどのプレフィクスを除外して考える。基本的にはこの中で、一番新しいものを優先的に探す。

branch

タグではなく、ブランチのチェックアウトが必要なら、特別なdev-*プレフィクス/サフィックスを指定してブランチを指定する。

上記の例で、my-featureブランチの指定が必要ならば、dev-my-featureを指定する。

ブランチ名がバージョン名と似ている場合、記法が変わる。v1.x-devのように指定する。v1タグではなく、v1ブランチを明示するために、.xは必須。あるいは、タグ名とブランチ名を完全に別の名前 (v1ブランチの代わりにv1.xブランチ) にしておけば、.xは不要。

バージョン名によく似たブランチ名を指定する場合だけ、dev-プレイフィクスではなく、-devサフィックスを指定する。



Test

General

Static

PHPの静的解析ツール。

  • php -l
  • PHPStan
    • Larastan: LaravelでPHPStanを使うと、static呼び出しなどで大量のエラーが出ますので、それらをカバーしてくれます。
  • PHP Code Sniffer
  • PHPMD
  • Psalm
  • PhpStorm
  • Rector

上記が有名。

PHPStanとPsalmはベースラインを設定できるので既存のプロジェクトにも導入しやすい。

PHPのバージョンアップの互換性の確認などもできる。

古いスタイルのPHPの改善

https://grok.com/share/c2hhcmQtMw%3D%3D_938039fe-7a79-4b4b-aad5-6b0fb3e71c8f

https://grok.com/share/c2hhcmQtMw%3D%3D_87563ab0-56ca-4faa-aa15-f5fb31dcf52d

昔ながらの、要請が来たらheader/echoで手動で応答を返すスタイルのPHPコード。このままだと、テストとかしにくいし、同一コードが散見して冗長になる。保守に問題がある。

改善手順がある。

  1. 静的解析ツール (php -l/phpstan)
  2. 仕様化テスト (phpunit)
  3. リファクタリング (関数抽出)
  4. リファクタリング (クラス化)
  5. 単体テスト
仕様化テスト/Characterization test

読書メモ『レガシーコード改善ガイド』マイケル・C・フェザーズ|まくろぐ

特徴付けテストと直訳することもあるらしい。マイケル・C・フェザーズの2009年翻訳のレガシーコード改善ガイドで提唱された内容。著者の独自の概念。

ドキュメントや仕様が不明瞭なコードを扱う際に有効。既存コードの動作を把握し、そのふるまいを固定化するための手法。

現状コードが今何をするのかをテストで記録する。リファクタリングとかをする前に現在の動作をある程度保証するテストを記載することで、意図しない影響を防げる。

例えば、昔ながらの、phpファイルがそのまま応答を返すタイプのアプリの場合。

環境変数を設定して、バッファリングで、文字列の有無で、ある程度現在のふるまいをテストできる。

PHPUnitでやる場合。

<?php declare(strict_types=1);

namespace Tests\Unit\Service\Resign;

final class SsResignConfirmTest extends \Tests\Unit\BaseTestCase
{
    public function testBaseline(): void
    {
        $this->assertBaseline('service/resign/ss_resign_confirm.php');
    }
}
<?php declare(strict_types=1);

namespace Tests\Unit;

/**
 * 単体テストのベースクラス。
 * 
 * tests/Unit/以下はこのクラスを継承して実装する。基本は、継承先の子クラスのtestBaselineでassertBaselineを呼び出す。
 * 
 * 他に、テストで共通で行うようなものがあれば、こちらに記述して共通化していく。
 */
abstract class BaseTestCase extends \PHPUnit\Framework\TestCase
{
    /**
     * 仕様化テスト (characterization test) として、該当ファイルの応答にエラーがないことを確認する共通テストメソッド。
     * 
     * 継承先の子クラスのtestBaseline内で `$this->assertBaseline('path');` で実行想定。
     * @param string $relative_include_path テスト対象のファイルの相対パス。
     */
    protected function assertBaseline(string $relative_include_path): void
    {
        ob_start();
        include PROJECT_ROOT . '/' . $relative_include_path;
        $html = ob_get_clean();

        $this->assertStringNotContainsString('URLが不正です。', $html);
        /** err.tpl */
        $this->assertStringNotContainsString('<title>エラー', $html);
    }

こんな感じで、output bufferingでechoのHTTP本体相当を変数に格納して、その変数にエラー文字がないかでチェックする。

https://grok.com/share/c2hhcmQtMw%3D%3D_d3073260-4256-46a8-8762-ef7c63967681

仕様化テストだから、Baseじゃなくて、Baselineとするのがいい。assertBaseline/testBaselineでやると、一貫性ある命名規則にできる。

https://grok.com/share/c2hhcmQtMw%3D%3D_57517641-d440-468e-ae2e-956958ce27f9

API系の場合、結合テストに近い。サーバーを立てて、リクエストを送って、レスポンスがどうなるかをテストする感じ。

一部クラスになっている場合も、基本は現在の挙動確認。現在の挙動を確認するような動作のテストをする。最終出力回りを重点的に書くとよい。

関数抽出
<?php
require 'smarty_setup.php'; // Smarty初期化
header('Content-Type: text/html; charset=UTF-8');
$id = $_GET['id'];
$db = new PDO('...'); // DB接続
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
$user = $stmt->fetch();
$smarty->assign('user', $user);
echo $smarty->fetch('user.tpl');
?>
<?php
function fetchUser($db, $id) { // ロジック関数
    $stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
    $stmt->execute([$id]);
    return $stmt->fetch();
}

function renderUser(Smarty $smarty, $user) { // ビュー関数
    $smarty->assign('user', $user);
    return $smarty->fetch('user.tpl'); // echoせず返す
}

// メインスクリプト
require 'smarty_setup.php';
header('Content-Type: text/html; charset=UTF-8');
$db = new PDO('...');
$id = $_GET['id'];
$user = fetchUser($db, $id);
$output = renderUser($smarty, $user);
echo $output;
?>

こんな感じで、内容ごとに処理を関数にまとめる。

  1. header出力
  2. DB操作
  3. テンプレート操作

関数化した部分のテストは、仕様化テストと同様で、phpunitでoutput bufferingで出力部分を破棄して、関数定義だけ取り込んでテストする。

長期保守前提であれば、関数化の段階をすっ飛ばして、オブジェクト・クラス化したほうが手っ取り早い。

クラス化

https://grok.com/share/c2hhcmQtMw%3D%3D_21361425-16d6-419a-84ae-3c793d627eef

シンプルで無難なMVCの形でクラス化する。

  • DB操作: Modelクラス
  • 画面表示: Viewクラス
  • ロジック呼び出し: Controllerkクラス

コード例: Model (models/UserModel.php):

class UserModel {
    private $db;

    public function __construct(PDO $db) {
        $this->db = $db;
    }

    public function fetchUser($id) {
        $stmt = $this->db->prepare("SELECT * FROM users WHERE id = ?");
        $stmt->execute([$id]);
        return $stmt->fetch();
    }
}

View (views/UserView.php):

class UserView {
    private $smarty;

    public function __construct(Smarty $smarty) {
        $this->smarty = $smarty;
    }

    public function renderUser($user) {
        $this->smarty->assign('user', $user);
        return $this->smarty->fetch('user.tpl');
    }
}

Controller (controllers/UserController.php):

class UserController {
    private $model;
    private $view;

    public function __construct(UserModel $model, UserView $view) {
        $this->model = $model;
        $this->view = $view;
    }

    public function handleRequest() {
        $id = $_GET['id'];
        $user = $this->model->fetchUser($id);
        return $this->view->renderUser($user);
    }
}

メインスクリプト (user.php):

<?php
require 'autoload.php'; // Composerのautoload
$db = new PDO('...');
$smarty = new Smarty(); // Smartyセットアップ
$model = new UserModel($db);
$view = new UserView($smarty);
$controller = new UserController($model, $view);
header('Content-Type: text/html; charset=UTF-8');
$output = $controller->handleRequest();
echo $output;
?>

これでMVCパターンに近づく。SmartyをViewクラスでラップしてテストしやすく。

あるいは、メインスクリプトをControllerとみなして、constructorで画面表示処理を書くというのもありかもしれない。

if (!debug_backtrace()) {}

このコードでincludeとの違いも検知できる。

PDOなどのDB接続インスタンスは、メインスクリプトで従来通り毎回書くか、シングルトンかstaticメソッドで管理して渡すとか。

.inc/.include/.class

https://grok.com/share/c2hhcmQtMw%3D%3D_6193ad5e-6df3-42c7-9dbc-dc67deb44bcd

phpファイルが直接応答を返す古いパターンだと、拡張子が.inc/.include/.classなど.phpじゃないことがある。

公開ディレクトリーに、同居する都合、サーバーアクセスで拡張子で、アクセス制御しているからだろう。

本来であれば、現代的なpublic/index.phpでコントローラーを明示的に振り分けて、ユーザーがアクセス可能なディレクトリーを制限すべきだろう。

PHP

declare(strict_types=1);

新規ファイルには、基本的に指定したほうがいい。既存ファイルは慎重に。

php -l

-l/--syntac-checkオプションで構文チェックのみを行う。-lはlintのlだと思われる。

成功したらNo syntax errors detected in <filename> が標準出力に書き込まれ、リターンコードは 0

失敗した場合、テキスト Errors parsing <filename> に加え、内部パーサエラーメッセージ が標準出力に書き込まれ、シェルリターンコードは、 -1 となります。

このオプションは、(未定義の関数のような)致命的なエラー(fatal error) はみつけません。致命的なエラーについても調べたい場合は、 -f を使用してください。

-lは-r (コードの実行)とは併用不能。以下のコードで全phpファイルをチェックできる。

for i in `find . -name \*.php`; do php -l $i | grep -v "No syntax errors"; done

上記コードより、以下の方が少し早い。

find . -name \*.php -exec php -l {} \; | grep -v '^No syntax errors'

外部ツールも不要で手軽で悪くない。特に、-lで実行するphpのバージョンを変えるだけでチェックでき、PHPのバージョンアップ時の事前の簡易チェック、粗探しとしてとして悪くない (PHPerのための「静的解析」を語り合う【PHP TechCafe イベントレポート】 - RAKUS Developers Blog | ラクス エンジニアブログ)。

ただ、php -lは構文エラーのみで、型チェックはできない。PHPStanを使うしかない模様。

gitのpre-commitに登録する場合、以下のような内容にするとよい。

#!/bin/sh
## Lint added/modified PHP file.
set -eu
has_error=false
PHP_FILES=$(git diff --staged --name-only --diff-filter=ACM | grep '\.php$' || :)
[ -z "$PHP_FILES" ] && exit || :

while read -r file; do
	php -l "$file" | grep -v '^No syntax errors' && has_error=true
done <<-EOT
$PHP_FILES
EOT
$has_error && exit 1 || :

./vendor/bin/phpstan analyze --memory-limit 5G $PHP_FILES || has_error=true
$has_error && exit 1 || :
# parameters.pathsのコメントアウトを解除して全体解析する場合、上記のファイル単位のphpstanをやめて、以下の全体解析を実行する。
# ./vendor/bin/phpstan analyze --memory-limit 5G || exit 1

ただ、根本的なPHPの構文エラーがあると、PHPStanはそこで失敗して詳細情報がない。php -lもあっていい気がする。

PHPStan

導入が簡単なので黙って導入したらよさそう。

User Guide

Getting Started

Getting Started | PHPStan

composer require --dev phpstan/phpstan

以下のコマンドでバージョンを確認できればOK。

vendor/bin/phpstan analyze --version
PHPStan - PHP Static Analysis Tool 2.1.10
git pre-commit

hooks/pre-commit

#!/bin/sh
## Lint added/modified PHP file.
set -eu
has_error=false
PHP_FILES=$(git diff --staged --name-only --diff-filter=ACM | grep '\.php$' || :)
[ -z "$PHP_FILES" ] && exit || :

while read -r file; do
	php -l "$file" | grep -v '^No syntax errors' && has_error=true
done <<-EOT
$PHP_FILES
EOT
$has_error && exit 1 || :

./vendor/bin/phpstan analyze --memory-limit 5G $PHP_FILES || has_error=true
$has_error && exit 1 || :
# parameters.pathsのコメントアウトを解除して全体解析する場合、上記のファイル単位のphpstanをやめて、以下の全体解析を実行する。
# ./vendor/bin/phpstan analyze --memory-limit 5G || exit 1
# phpstan.dist.neon
includes:
    - phpstan-baseline.neon
parameters:
    level: 0
    tmpDir: var/tmp/phpstan
    # parallel:
    #    processTimeout: 15000.0
    # scanDirectories:
    #     - ../../
    # todo ある程度指摘対応ができたら常に全体解析にする。
    # paths:
    excludePaths:
        - var

最初は修正ファイルだけphpstanで解析して、安定してきたらプロジェクト全体を解析する形にするとよいだろう。

phpstanのファイル単独実行は時間がかかるので、修正対象をまとめて実行した方がいい。

https://grok.com/share/c2hhcmQtMw%3D%3D_85f34631-62c7-426a-89cb-ecb407c0246b

なお、WSLやDockerを使っている場合、docker exec <container-name> php -lなどで、docker内のphp/phpstanをホスト側で使うのがいい。

docker runで起動している場合、--nameでコンテナー名を固定する。

Command Line Usage

Command Line Usage | PHPStan

Analyzing code
vendor/bin/phpstan analyse [options] [<paths>...]

いくつか重要なオプションがある。

  • paths: 検査対象ファイルパス。設定ファイルで指定可能。
  • --level|-l: 実行レベル。設定ファイルで指定可能。
  • --configuration|-c: 設定ファイルを指定する。
  • --generate-baseline|-b: ベースラインを作成する。オプション引数で出力ファイルのパスを指定できる。デフォルトはphpstan-baseline.neon。
  • --memory-limit: php.iniと同じ形式で最大メモリーを指定。
Running without arguments

PHPStanは基本的に、コマンド引数で指定した、ディレクトリー類を対象に解析する。

毎回コマンド引数を指定するのは手間なので、設定ファイルに記述しておくこともできる。

以下の条件を満たせば、引数なしで、設定ファイルの内容で解析できる。

  1. phpstan.neonかphpstan.neon.distの存在。
  2. pathsに解析対象パスリストが存在。
  3. levelパラメーターの存在。

最小限の例。

parameters:
	level: 0
	paths:
		- src
		- tests

現実的な例。

includes:
    - phpstan-baseline.neon
parameters:
    level: 0
    parallel:
       processTimeout: 15000.0
    tmpDir: var/tmp/phpstan
    scanDirectories:
    paths:
    excludePaths:
Minimum file

https://chatgpt.com/c/67fe029c-9ce0-800b-9397-8e1d7f18b303

phpstanは基本的に引数で<paths>でディレクトリーやファイルを指定する。ただし、ここで指定していないファイルは、見に行けないので未定義の警告などが出る。

基本はパス全体を指定する。コマンドライン引数か、phpstan.dist.neonで指定しておく。

他に、pre-commitように、autoload_filesかbootstarpFilesでベースの依存ファイルを指定する。

他に、phpstanはcomposerのautoloadを理解するので、composer.jsonにautoloadを追加する。

Reflection error: Circular reference to class

名前通り循環参照が起きている。具体的に、自分と同名のクラスを最終的にextendsしている。

元ファイルを直す他、excludePathsで検査対象などから除外すれば回避できる。

Internal error: Child process timed out after 3000.0 seconds. Try making it longer with parallel.processTimeout setting.

https://chatgpt.com/share/68020423-6350-800b-9200-3d8c2d2483e2

--generate-baselineを指定すると、初回だけ時間がかかる。

parallel.processTimeoutでタイムアウトの時間を設定する。成功させるために、進捗率と現在の設定の比率でカバーできるだけの時間にする。3000で40 %くらいの進捗だったら9000とか。

初回だけ時間がかかる。

Syntax error, unexpected T_EXTENDS on line 18

https://chatgpt.com/c/6801fda7-2434-800b-84da-601c44645488

phpstanを実行すると上記のような、エラーが出ることがある。これは、PHPStanの前に根本的なPHPの構文エラーの可能性が高い。

PHPStan実行の前に、先にphp -lで解決しておく必要がある。

time -p (find . -name \*.php -exec php -l {} \; | grep -v 'No syntax errors' >php-l.log) 2>&1
Error while loading phpstan-baseline.neon: Invalid UTF-8 sequence.

作成したBaselineをincludeで読み込んで実行すると上記のエラーが出た。

https://chatgpt.com/share/68020423-6350-800b-9200-3d8c2d2483e2

ベースラインファイルにへんな文字が入ったのが原因。

以下のコマンドで変な文字の行を特定できる。

grep -axv '.*' phpstan-baseline.neon

--error-format=rawを指定してbaselineを作り直すと良い。

The Baseline

The Baseline | PHPStan

PHPStanはレベルベースでチェックしてくれる。が、既存コードで警告が多い場合、新規追加分だけ考慮したいだろう。

そういう場合に、ベースラインが役立つ。

以下のように、--generate-baselineオプションを指定して、PHPStanを実行すると、現在のエラーリストをエクスポートしてくれる。

vendor/bin/phpstan analyse --generate-baseline

ベースラインファイルを使う場合、オプションで指定するか、設定ファイルで明記する。

デフォルトでphpstan-baseline.neon。オプション引数で、出力ベースラインファイルを指定できる。

作成したら、設定ファイルphpstan.dist.neonに読み込む。

includes:
	- phpstan-baseline.neon

parameters:
	# your usual configuration options

これで次回からはベースラインのエラーを無視してくれる。必要に応じて、ベースラインを編集したらい、ベースラインを再生成してもいい。

なお、ベースラインで無視したエラーがなくなった場合、ベースラインファイルから削除するまで、PHPStanは通知する。この通知を無効にしたかったら、以下を設定する。

parameters:
	reportUnmatchedIgnoredErrors: false

なお、ベースライン作成時に、以下の警告が出ることがあり、全ての指摘をベースラインに出力できるわけではない。

[WARNING] Baseline generated with 79012 errors.                                
Some errors could not be put into baseline. Re-run PHPStan with "-vv"
and fix them.              

致命的なエラーはベースラインに出力できないので、直すしかない。

Discovering Symbols

Discovering Symbols | PHPStan

https://chatgpt.com/c/67e0b340-da84-800b-89c8-4c72691fdc44

pathsで指定したファイルで使用されているシンボルをPHPStanは必要とする。デフォルトで以下の2箇所を探す。

  • pathsの対象 (コマンドライン引数と設定ファイル)。
  • composerの依存関係

基本はこれで見つかるはず。

Third party code outside of Composer dependencies

PEARとか追加で設定が必要だったりする。そういう場合、scanFilesとscanDirectoriesの設定が使える。

これらで指定したファイル、ディレクトリーをシンボルとして探す。探すだけで解析はしない。

https://chatgpt.com/share/680b240a-bf28-800b-bfc2-a84f639fe0c0

プロジェクト外部はみれないらしい。

autoload_filesでautoload.phpを指定するか。bootstrapFilesで読み込む。

https://grok.com/share/c2hhcmQtMw%3D%3D_fc03d339-2ee1-4f76-85bd-ee567a2575ef

例えば、PEARの依存関係を追加したい場合、以下のコマンドでpearのパスを確認する。

pear config-get php_dir

これをscanDirectoriesに指定すればOK。

parameters:
    scanFiles:
        - /usr/local/php
Bootstrap

https://grok.com/share/c2hhcmQtMw%3D%3D_fc03d339-2ee1-4f76-85bd-ee567a2575ef

PHPStanの実行前に、PHPのランタイム設定したい場合、bootstrapFilesで自前の起動時ファイルを指定できる。

parameters:
	bootstrapFiles:
		- phpstan-bootstrap.php

例えば、環境変数の設定とかをこれでうまくできる。独自のautoload的なことやっている場合もこれでいける。

Output Format

Output Format | PHPStan

--error-formatのオプションでPHPStanの報告形式を指定できる。

.neonファイルに以下の形式で指定可能。

parameters:
	errorFormat: json

特に重要なものがある。

  • table: デフォルト。
  • raw: 1行1データで機械処理用。文字化けなどが起こりにくい。
  • json: IDEなどとの連携用。
  • checkstyle: XML形式。CIツール連携用。

困ったらrawを指定しておく。

Ignoring Errors

Ignoring Errors | PHPStan

Excluding whole files
Broken symbolic link

シンボリックリンクのリンク切れなどで以下のエラーが出ることがある。

  Could not read file:

https://chatgpt.com/c/68088168-ac54-800b-b78e-e28743b81011

excludePathsはファイルの中身に有効で、scanDirectiriesで指定したディレクトリーに、破損したシンボリックリンクがあると、スキャン対象に入った時点で失敗する。

事前に破損したシンボリックリンクを削除しておく必要がある。

以下のコマンドで破損リンクを削除できる。

find path/to/scan -type l ! -exec test -e {} \; -exec rm {} \;
# find path/to/scan -type l ! -exec test -e {} \; -print # 確認用
Result Cache

PHPStanは分析実行結果をキャッシュする。条件は、解析対象ファイルリスト。同一じゃないとキャッシュは使えない。

キャッシュが使えると、数秒で解析が終わることもある。

tmpDir

実行結果は%tmpDir%/resultCache.phpになる。

sys_get_temp_dir() . '/phpstan' (usually /tmp/phpstan)

parameters.tmpDirで上書き指定可能。

Macだと以下のような場所。

php -r 'echo sys_get_temp_dir() ."\n";'
/var/folders/hr/9q7pmn9x5_788w3fsp9dd71r316wyt/T

このtmpDirのデフォルトは、基本的にOSを再起動すると消去される。なので、基本的にneonファイルに以下のような指定がほぼ必須。

parameters:tmpDir: var/tmp/phpstan

.gitignoreにもついでに以下を追加する。

/var/
scannedFiles

phpstanを実行すると以下のようにキャッシュの不一致が示されることがある。

time -p vendor/bin/phpstan analyze --memory-limit=5G --generate-baseline --error-format=raw -vv 2>&1 | tee phpstan.log

Note: Using configuration file phpstan.dist.neon.
Result cache not used because the metadata do not match: projectConfig, scannedFiles

設定ファイルと、スキャンファイル一覧のどちらか。

原因把握のために、scannedFilesを確認したい。

resultCache.phpのscannedFilesの連想配列のキーに、ファイル一覧が入っている。

確認すると、.phpの拡張子のファイルが入っている。.gitとかは含んでいない。が、tmpDirに指定したキャッシュ内の.phpは見ている。除外必要。

parameters.fileExtensionsで追加可能 (Config Reference | PHPStan)。

paths/excludePath/scanDirectories

結果キャッシュに影響のある中で、scannedFilesに影響のあるこれらの設定の関係がわかりにくいので整理する。

  • pathsの指定があれば含む。
  • scanDirectoriesがあれば含む
  • excludePath (.analyseAndScan) の指定があれば、除外される。paths内で一部を除外する場合に使う。シンボルとしては見たい場合はexcludePath.analyseの指定。analyseと併用・別々に記載したい場合はanalayseAndScan。scanDirectories以下も同様。気持ち悪いが、analyzeではなくてanalyseじゃないと認識されない。
parameters:
    paths:
        - src
    excludePaths:
        analyse:
            - src/thirdparty
        analyseAndScan:
            - src/broken

srcの中のsrc/thirdpartyを、解析対象に含めたくないが、シンボルとしては認識したい場合。src/brokenは見に行くとエラーになるのでシンブルとしても無視。

Config Reference

Config Reference | PHPStan

基本はphpstan.neon.distまたはphpstan.dist.neonをバージョン管理する。ユーザーはphpstan.neonで上書きできるようにする。

phpstan.dist.neonのほうが、拡張子が明示されてよく感じる。

PHPUnit

PHPUnitを使用すれば自動単体テスト (Unit test) が可能だ。

About

Version

情報源: Supported Versions of PHPUnit – The PHP Testing Framework

PHPUnitのバージョンごとに対応しているPHPのバージョンが決まっている。

PHP v7.4に対応してい最後のバージョンはPHPUnit 9なので、当分はこれを使うのが良い。

Install

以下のコマンドでPHP 7.4に対応している最後のPHPUnitのv9をインストール。

composer require --dev phpunit/phpunit ^9

composer.jsonに以下が追加される。

{
    "require-dev": {
        "phpunit/phpunit": "^9"
    }
}

composer.jsonとcomposer.lockをVCSで管理する。

composerのautoloadのclassmapでソースファイルのルートディレクトリーを指定する。

{
    "autoload": {
        "classmap": [
            "src/"
        ]
    },
    "require-dev": {
        "phpunit/phpunit": "^9"
    }
}

最後に以下のコマンドでvendor/autoload.phpを更新。

composer dump-autoload

Getting Started with PHPUnit 9

Getting Started with Version 9 of PHPUnit

  1. phpunitをインストール
  2. テストコード記述
  3. phpunit実行

上記の3ステップで使う。

以下のように引数に、ディレクトリーかテスト対象ファイルの相対パスを探す。

phpunit tests

2. Writing Tests for PHPUnit

出典: 2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual

基本的な使用方法を整理する。

  1. 基本的にはクラス単位で試験コードを記載。<Class>クラスの試験コードは<Class>Testの命名にする。
  2. <Class>Test はPHPUnit\Framwork\TestCaseを継承させる。
  3. 試験はpublicのtest*メソッドの命名にする。あるいは、@testのアノテーションを付ければ、命名規則に従わなくてもいい。
  4. test*メソッド内で、assertSame() などで、期待値との比較で試験を行う。
  5. test*メソッドで1メソッドに対して、試験する。例えば、正常形、異常系、境界値など。1メソッド1テストメソッドにするとわかりやすい。

例:

<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class StackTest extends TestCase
{
    private static $dbh;
    private $instance;
    
    public static function setUpBeforeClass(): void
    {
       // DB接続などクラス全体の初期化処理
       self::$dbh = new PDO('');
    }
    public static function tearDownAfterClass(): void
    {
        self::$dbh = null;
    }
    protected function setUp(): void
    {
      // 該当インスタンスの生成などメソッド単位の初期化処理。
      $instance = new Stack();
    }
    public function testPushAndPop(): void
    {
        $stack = [];
        $this->assertSame(0, count($stack));

        array_push($stack, 'foo');
        $this->assertSame('foo', $stack[count($stack)-1]);
        $this->assertSame(1, count($stack));

        $this->assertSame('foo', array_pop($stack));
        $this->assertSame(0, count($stack));
    }
}

命名規則があっていないと、以下のメッセージが出てphpunitの実行に失敗する。

Class <Class>Test could not be found in /path/to/<Class>Test.php

後述するが、メソッドテスト時は、data providerでテストデータを与えて、1テストメソッドで1試験対象メソッドをテストするといい。

Depends

前回の試験で準備した結果を利用したい場合、@dependsのアノテーションでテスト関数を指定しておくと、指定したテスト関数のreturnを引数に受け継いだテスト関数を記述できる。

@dependsは複数指定でき、指定順に事前に試験が実行されて引数に渡される。

Data Provider
About

ある関数に対して、テストケースを用意して、複数の引数の組み合わせを試験したいことがよくある。こういうときのために、テスト関数に渡す引数を生成する関数のデータプロバイダーを指定できる。@dataProviderで関数を指定する。データプロバイダーは引数のリストを配列で返すようにする。

データプロバイダーを使うことで、テスト用のメソッドは1個で、データだけ変えてテストできる。1メソッド1テストメソッドで対応できてスマート。また、データプロバイダーを使うと、どのデータで失敗したかがわかる。

<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class DataTest extends TestCase
{
    /**
     * @dataProvider additionProvider
     */
    public function testAdd(int $a, int $b, int $expected): void
    {
        $this->assertSame($expected, $a + $b);
    }

    public function additionProvider(): array
    {
        return [
            [0, 0, 0],
            [0, 1, 1],
            [1, 0, 1],
            [1, 1, 3]
        ];
    }
}

data providerの使用方法。

  1. テストデータを配列かIteratorで返却するpublic メソッド (data provider) を用意
  2. テストメソッドの引数で、data providerの配列を受け付ける。
  3. data providerを使用したいテストメソッドで、@dataProvider <data provider method> のアノテーションを指定する。

データセットはリストでもいいが、連想配列でキーにテスト名を書くとわかりやすい。

<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class DataTest extends TestCase
{
    /**
     * @dataProvider additionProvider
     */
    public function testAdd(int $a, int $b, int $expected): void
    {
        $this->assertSame($expected, $a + $b);
    }

    public function additionProvider(): array
    {
        return [
            'adding zeros'  => [0, 0, 0],
            'zero plus one' => [0, 1, 1],
            'one plus zero' => [1, 0, 1],
            'one plus one'  => [1, 1, 3]
        ];
    }
}

テストデータに意味がある場合は、記載した方がよさそう。このテストデータ部分にMock用のデータを渡したりもできる。

Mockでデータに応じて処理を変える場合も、データプロバイダーのデータでif文などの条件を入れたり、mock_dataのような専用のキーを渡す形にすればいい。

データ数が多い場合、名前付き配列にしておくと、どういうデータ項目で失敗したかがわかりやすい。Iteratorオブジェクトを返してもいい。

Exception

テストデータによって、例外が発生することを期待する場合、いくつか方法がある。テストデータにthrowsException:boolの列を用意して、それで判定する。または、例外用のテストメソッドを用意してそちらにするというのもある。複雑なら後者、テストケースがそんなに多くなくてシンプルなら前者でいいと思う。

/**
 * @dataProvider provideCases
 */
public function testSomething($input, $expected, $throwsException)
{
    if ($throwsException) {
        $this->expectException(\InvalidArgumentException::class);
    }

    $result = $this->target->doSomething($input);
    
    if (!$throwsException) {
        $this->assertSame($expected, $result);
    }
}

public static function provideCases(): array
{
    return [
        '正常系' => ['input' => 10, 'expected' => 100, 'throwsException' => false],
        '異常系' => ['input' => -1, 'expected' => null, 'throwsException' => true],
    ];
}
Naming

data providerメソッドの命名。「複数メソッド共通: provideXxx」にする。

「特定メソッド専用: testXxxDataProvider」にすると、testメソッドと対応が同じでわかりやすいのだが、testから始まると、testメソッド扱いされるので、assertがないと警告が出る。命名規則的にはxxxxTest/xxxProviderがきれいなんだけど、test/provideで使う場所と隣同士にすれば、そんなに困ることはないだろう。

他に、返却する配列の順番。公式だと、引数→期待の順番。だが、引数は複数あり得るのだから、期待→引数の順番がわかりやすい。assertの順番ともあうし。

メソッドは、test→provideの順番に書くといいみたい。testメソッドに対して、テストケースのデータを流すから。メソッドが先で、データが後。どういう試験をするかが、メソッドで先にわかるイメージの模様。

テストメソッドの名前は、test<nethod name><テスト観点> みたいな感じにすると良い。次のResolutionのように、1メソッドに対して、違うテスト観点で複数テストすることがあるから。

Resolution

https://chatgpt.com/share/68390cd8-5cc4-800b-afce-36c2abdc4b83

data providerを使って渡すテストデータは、同じ観点にする。

例えば、表示の試験は、表示のバリエーション。表示の他に、ボタンの動作試験をしたい場合、違うテストメソッド、data providerにしたらしい。

1個のdata providerで複数の観点の試験もできなくはないが、引数とテストメソッドが複雑になって、わかりにくくなる。

3. The Command-Line Test Runner

3. The Command-Line Test Runner — PHPUnit 9.6 Manual

phpunitのコマンド自体の使用方法。

phpunit <file>
phpunit <directory>

引数にテスト対象ファイルの相対パスか、ディレクトリーを指定する。ディレクトリーを指定した場合、配下の全*Test.phpを実行する。

基本はphpunit testsを実行すればいいと思う。

Fixtures

出典: 4. Fixtures — PHPUnit 9.6 Manual

テストメソッドの実行前に、テスト対象のインスタンスの生成や、DB接続など準備がいろいろある。これをFixturesと呼んでいる。この準備がけっこう手間になる。これを省力できるのがテストフレームワークの利点。

テストメソッド実行前後に共通で行える処理がある。

  • setUp/tearDown: テストメソッド単位の前後処理。テスト対象インスタンスの生成など。tearDownは何もしなくてもいいことが多い。
  • setUpBeforeClass/tearDownAfterClass: クラス単位の前後処理。 DB接続など。
共通クラスの初期化処理

https://chatgpt.com/share/6836d32d-e9b8-800b-9433-444ff1a1e2bf

PHPUnitのTestCaseを継承した独自の共通処理クラスを作る場合。初期化処理は__constructでやってはいけないらしい。

テスト用にいろいろ特別な初期化をしているから。

代わりに、setUpで共通で使うインスタンス生成などをする。子クラスではparent::setUp()が毎回必要になるがしかたない。

XML Configuration File

出典:

基本はコマンドでテスト対象クラス・ファイルを指定してテストを実行する。他に、XMLの設定ファイル (phpunit.xml) でもテスト対象を指定できる。

ただし、phpunit.xmlかphpunit.xml.distがあって、かつ--configurationの指定がない場合、これらのファイルを自動的に読み込む。

testsディレクトリーの全*Test.phpを対象にする最小限の例は以下。

<phpunit bootstrap="src/autoload.php">
  <testsuites>
    <testsuite name="money">
      <directory>tests</directory>
    </testsuite>
  </testsuites>
</phpunit>

phpunit.xmlがあれば、単にphpunitコマンドを実行するだけでいい。

phpunit --bootstrap src/autoload.php --testsuite money

上記のコマンド相当になる。

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php" colors="true">
    <testsuites>
        <testsuite name="tests">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

https://chatgpt.com/share/6821bc52-3324-800b-a98c-cbefe6c42324

element attribute default
phpunit bootstrap - --bootstrap相当。テスト実行前の読込スクリプト。
colors false true=--colors=auto相当、false=--colors=never相当。
verbose
testsuites - - testsuiteの親要素。
testsuite - - name属性が必須で、テスト検索用の1以上のdirectoryかfile要素が必要。
testsuite name 必須。ディレクトリー名でいい気がする。
phpunit/php PHPの設定。
phpunit/php/includePath include_pathの先頭に追加するパス。
phpunit/php/ini name/value
phpunit/php/const, var グローバルな定数、変数を設定。
phpunit/php/env name/value 環境変数。
phpunit/php/get, post, cookie, server, files, request name/value 該当するスーパーグローバル変数の設定。

GuzzleのHTTPクライアントでプロキシー設定無視で以下の設定はよく使うかもしれない。

<php><env name="NO_PROXY" value="*"/></php>
bootstrap

https://chatgpt.com/share/6821bc52-3324-800b-a98c-cbefe6c42324

autoloadを使っていないプロジェクトだと、手動でプロジェクトルートパスなどの指定が必要になる。

tests以下にbootstrap.phpを用意して、その中で必要なものを読み込む形にするといい。

<?php
// tests/bootstrap.php

// $additionalPath = PROJECT_ROOT . '/src'; set_include_path(get_include_path() . PATH_SEPARATOR . $additionalPath); set_include_path(implode(PATH_SEPARATOR, [ get_include_path(), PROJECT_ROOT . '/src', PROJECT_ROOT . '/lib' ]));

// Composerのオートローダー
require_once __DIR__ . '/../vendor/autoload.php';

// オートローダーでは読み込めない独自ファイル
require_once __DIR__ . '/manual-loads/special-loader.php';

// 必要なら定数定義
define('PROJECT_ROOT', dirname(__DIR__));
<phpunit bootstrap="tests/bootstrap.php">

Assertions

About

Ref:

基本はassertSameでテストすればいいのだが、それ以外にも例外とかいろいろ試験したいケースがあるので、メソッドを整理する。

Method

インスタンスメソッドとstaticメソッドの2種類がある。他に、グローバル関数もある。どれを使ってもいい。

  • アサーションメソッド: PHPUnit\Framework\Assert
  • PHPUnit\Framework\TestCaseはAssertを継承している。
  • phpunit/phpunit/src/Framework/Assert/Functions.php でグローバル関数。

入力文字数や好みの問題。グローバル関数は内部的にstaticメソッドを呼んでいる。

TestCaseのインスタンスメソッドの方が違和感がないという説がある。

$this->よりもself::のほうが短い。グローバル関数は名前空間がないのがまずい。self::のstaticメソッドでいい気がする。

Negate

assert系メソッドは、assertNotでNotを前置したら否定形になる。assertStringとかグループがある場合は、assertStringNotのように、グループの後にNotになる。

同値判定
  • assertSame/assertNotSame: 型と値の両方を検査 (===相当)。
  • assertEquals: 値を検査 (==相当)。

オブジェクトの属性値の値が同じかどうかをみたいなら、assertEqualsになる。assertSameは参照 (ポインター) のアドレス値の比較みたいなことをするから。

基本はassertSameでよいと思われる。

  • assertEqualsCanonicalizing()
  • assertEqualsIgnoringCase()
  • assertEqualsWithDelta()

他にこういうバリエーションがある。

論理判定
  • assertIsBool
  • assertFalse
  • assertTrue()
数値判定
  • assertGreaterThan
  • assertGreaterThanOrEqual
  • assertLessThan
  • assertLessThanOrEqual
  • assertInfinite
  • assertNan
  • assertIsInt
  • assertIsNumeric
  • assertIsFloat
文字列判定
  • assertIsString
  • assertMatchesRegularExpression
  • assertStringContainsString
  • assertStringContainsStringIgnoringCase()
  • assertStringMatchesFormat
  • assertStringMatchesFormatFile
  • assertStringEndsWith
  • assertStringEqualsFile
  • assertStringStartsWith
配列判定

配列の検査用のメソッドがある。

  • 要素数
    • assertEmpty
    • assertSameSize(): 2個の配列の要素数が同じか。
    • assertCount(): 指定した配列の要素数が指定数か。データの取得数などでこちらをよく使いそう。
  • 包含
    • assertContains
    • assertContainsOnly()
    • assertContainsOnlyInstancesOf()
    • assertArrayHasKey
  • assertIsArray
  • assertIsIterable
クラス
  • assertClassHasAttribute
  • assertClassHasStaticAttribute
  • assertObjectEquals
  • assertInstanceOf
  • assertIsCallable
  • assertIsObject
  • assertIsResource
  • assertIsScalar
  • assertNull
  • assertObjectHasProperty
ファイル
  • assertDirectoryExists
  • assertDirectoryIsReadable
  • assertDirectoryIsWritable
  • assertFileEquals
  • assertFileExists
  • assertFileIsReadable
  • assertFileIsWritable
  • assertIsReadable
  • assertIsWritable
その他
  • assertThat
  • assertJsonFileEqualsJsonFile
  • assertJsonStringEqualsJsonFile
  • assertJsonStringEqualsJsonString
  • assertXmlFileEqualsXmlFile
  • assertXmlStringEqualsXmlFile
  • assertXmlStringEqualsXmlString
Exception

2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual

特に例外の試験がイレギュラー。

<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class ExceptionTest extends TestCase
{
    public function testException(): void
    {
        $this->expectException(InvalidArgumentException::class);
        // Run test target code following.
    }
}

上記のようにexpectExceptionを使う。

  • expectException:
  • expectExceptionCode:
  • expectExceptionMessage:
  • expectExceptionMessageMatches:

例外が発生する処理の前に記述しておく。

Testing Output

2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual

echoなど標準出力を試験する際も専用のメソッドがある。

  • void expectOutputRegex(string $regularExpression)
  • void expectOutputString(string $expectedString)
  • bool setOutputCallback(callable $callback)
  • string getActualOutput()

expectExceptionと同様に事前にセットしておく。

Constraint

1. Assertions — PHPUnit 9.6 Manual assertThatとwithで使えるConstraint。通常のassertと似たような名前のものも多いので注意する。equalToとかはConstraintにしかない。PHPUnit\Framework\Constraintの名前空間の各種クラスのメソッドになる。

  • 論理
    • isFalse
    • isTrue
    • logicalAnd
    • logicalNot
    • logicalOr
    • logicalXor
  • 数値
    • greaterThan
    • greaterThanOrEqual
    • lessThan
    • lessThanOrEqual
  • 文字列
    • matchesRegularExpression
    • stringContains
    • stringEndsWith
    • stringStartsWith
  • 配列
    • arrayHasKey
    • contains
    • containsOnly
    • containsOnlyInstanceOf
  • クラス
    • classHasAttribute
    • classHasStaticAttribute
    • objectHasAttribute
    • isInstanceOf
    • isNull
  • ファイル
    • directorExists
    • fileExists
    • isReadable
    • isWritable
  • その他
    • isType
    • anything
    • equalTo
    • identicalTo

上記にない制約は$this->callbackで自分で定義する。引数に引数がきて、true/falseを返す。

Table 1.1 Constraints
Constraint Meaning
PHPUnit\Framework\Constraint\IsAnything anything() Constraint that accepts any input value.
PHPUnit\Framework\Constraint\ArrayHasKey arrayHasKey(mixed $key) Constraint that asserts that the array has a given key.
PHPUnit\Framework\Constraint\TraversableContains contains(mixed $value) Constraint that asserts that the array or object that implements the Iterator interface contains a given value.
PHPUnit\Framework\Constraint\TraversableContainsOnly containsOnly(string $type) Constraint that asserts that the array or object that implements the Iterator interface contains only values of a given type.
PHPUnit\Framework\Constraint\TraversableContainsOnly containsOnlyInstancesOf(string $classname) Constraint that asserts that the array or object that implements the Iterator interface contains only instances of a given classname.
PHPUnit\Framework\Constraint\IsEqual equalTo($value, $delta = 0, $maxDepth = 10) Constraint that checks if one value is equal to another.
PHPUnit\Framework\Constraint\DirectoryExists directoryExists() Constraint that checks if the directory exists.
PHPUnit\Framework\Constraint\FileExists fileExists() Constraint that checks if the file(name) exists.
PHPUnit\Framework\Constraint\IsReadable isReadable() Constraint that checks if the file(name) is readable.
PHPUnit\Framework\Constraint\IsWritable isWritable() Constraint that checks if the file(name) is writable.
PHPUnit\Framework\Constraint\GreaterThan greaterThan(mixed $value) Constraint that asserts that the value is greater than a given value.
PHPUnit\Framework\Constraint\LogicalOr greaterThanOrEqual(mixed $value) Constraint that asserts that the value is greater than or equal to a given value.
PHPUnit\Framework\Constraint\ClassHasAttribute classHasAttribute(string $attributeName) Constraint that asserts that the class has a given attribute.
PHPUnit\Framework\Constraint\ClassHasStaticAttribute classHasStaticAttribute(string $attributeName) Constraint that asserts that the class has a given static attribute.
PHPUnit\Framework\Constraint\ObjectHasAttribute objectHasAttribute(string $attributeName) Constraint that asserts that the object has a given attribute.
PHPUnit\Framework\Constraint\IsIdentical identicalTo(mixed $value) Constraint that asserts that one value is identical to another.
PHPUnit\Framework\Constraint\IsFalse isFalse() Constraint that asserts that the value is false.
PHPUnit\Framework\Constraint\IsInstanceOf isInstanceOf(string $className) Constraint that asserts that the object is an instance of a given class.
PHPUnit\Framework\Constraint\IsNull isNull() Constraint that asserts that the value is null.
PHPUnit\Framework\Constraint\IsTrue isTrue() Constraint that asserts that the value is true.
PHPUnit\Framework\Constraint\IsType isType(string $type) Constraint that asserts that the value is of a specified type.
PHPUnit\Framework\Constraint\LessThan lessThan(mixed $value) Constraint that asserts that the value is smaller than a given value.
PHPUnit\Framework\Constraint\LogicalOr lessThanOrEqual(mixed $value) Constraint that asserts that the value is smaller than or equal to a given value.
logicalAnd() Logical AND.
logicalNot(PHPUnit\Framework\Constraint $constraint) Logical NOT.
logicalOr() Logical OR.
logicalXor() Logical XOR.
PHPUnit\Framework\Constraint\PCREMatch matchesRegularExpression(string $pattern) Constraint that asserts that the string matches a regular expression.
PHPUnit\Framework\Constraint\StringContains stringContains(string $string, bool $case) Constraint that asserts that the string contains a given string.
PHPUnit\Framework\Constraint\StringEndsWith stringEndsWith(string $suffix) Constraint that asserts that the string ends with a given suffix.
PHPUnit\Framework\Constraint\StringStartsWith stringStartsWith(string $prefix) Constraint that asserts that the string starts with a given prefix.

Command-Line

Ref: 3. The Command-Line Test Runner — PHPUnit 9.6 Manual.

phpunitコマンドでいろいろできる。いくつか重要なオプション、使用方法がある。

  • phpunit file.php: 指定したファイルのテストを実行。
  • --testsuite <name>: テストを指定。

8. Test Doubles

Ref: 8. Test Doubles — PHPUnit 9.6 Manual.

About

テスト時に、依存関係を模擬したもので置換したいことがある。PHPUnitにそういう仕組が用意されている。

テストダブル - Wikipedia」Doubleは代役、影武者を意味する。ロックマンX4のダブルはたぶんこの英単語が由来だろう。

テスト対象クラスのプロパティーのクラスやメソッドの代用品をテストダブルと呼ぶ。テストダブルで置換するというような言葉の使い方をすると思われる。

テストダブルには、stubとmockがある。

  • Stub: SUT内メソッド戻り値の検証
  • Mock: SUT内メソッド引数の検証。

メソッド内で他のクラス・メソッドを使う場合はmockで対象クラスを模擬させる。

PHPUnitではテストダブル用に3の基本APIがある。

  • createStub
  • createMock
  • getMockBuilder

引数に置換対象のクラスを指定してインスタンスを作成する。createStubとcreateMockは対象を全部置換する。getMockBuilderはこれら2種のメソッドを含んでいて、置換するメソッド・プロパティーを自分で選択できる。特定メソッドだけを置換して、他はそのまま使いたい場合、これを使うしかない。

Stub

オブジェクトのメソッドの戻り値を、テストダブルに置換する手法をスタブと呼ぶ。メソッド内の特定メソッドの戻り値を模擬することで、テストと関係ない処理の影響を無視できる。

実際に使う際は、テスト対象クラスのインスタンス作成後に、インスタンスがのプロパティーに設定して、インスタンス内の別クラスメソッド呼び出しを模擬する。

<?php declare(strict_types=1);
class SomeClass
{
    public function doSomething()
    {
        // Do something.
    }
}
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class StubTest extends TestCase
{
    public function testStub(): void
    {
        // Create a stub for the SomeClass class.
        $stub = $this->createStub(SomeClass::class);

        // Configure the stub.
        $stub->method('doSomething')
             ->willReturn('foo');

        // Calling $stub->doSomething() will now return
        // 'foo'.
        $this->assertSame('foo', $stub->doSomething());
    }
}

単に、メソッドの戻り値を模擬したいだけなら、expectsとwithは不要。method()->willReturnだけで十分。

Table 8.1 Stubbing short hands
short hand longer syntax
willReturn($value) will($this->returnValue($value))
willReturnArgument($argumentIndex) will($this->returnArgument($argumentIndex))
willReturnCallback($callback) will($this->returnCallback($callback))
willReturnMap($valueMap) will($this->returnValueMap($valueMap))
willReturnOnConsecutiveCalls($value1, $value2) will($this->onConsecutiveCalls($value1, $value2))
willReturnSelf() will($this->returnSelf())
willThrowException($exception) will($this->throwException($exception))

willReturnCallbackで呼び出し関数をまるごと別のものに置換できる。これが非常に便利。

引数を指定したい場合、with(引数)->willReturn()がシンプル。ただし、この方法は最後に指定した引数1個しか対応できない。引数違いで戻り値を模擬したいなら、willReturnMapかwillReturnCallbackで、引数に応じた戻り値を設定する。

willReturnCallbackのcallbackに渡される引数の記載はないが、モック対象メソッドに渡される引数が、その順番で渡される動きになっている。

Mock Objects

https://chatgpt.com/share/683e72cc-461c-800b-a7fc-5e8b417a44aa

メソッドが呼び出されたかどうかを検証するための、テストダブルへの置換をモッキングと呼ぶ。

テスト対象メソッド内で、特定メソッドの呼び出しを検査できる。MVCでViewへの値渡しなんかの検査にうってつけ。

モックオブジェクトにはテストスタブの機能も含んでいる。が、モッキングのための専用処理があるので、インスタンス生成時などのコストがやや大きい。理由がなければ、Stubを使った方がいい。

判断基準として、expects/withを使うならMock、そうじゃなければStubのようなイメージ。

<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

class Subject
{
    protected $observers = [];
    protected $name;

    public function __construct($name)
    {
        $this->name = $name;
    }

    public function getName()
    {
        return $this->name;
    }

    public function attach(Observer $observer)
    {
        $this->observers[] = $observer;
    }

    public function doSomething()
    {
        // Do something.
        // ...

        // Notify observers that we did something.
        $this->notify('something');
    }

    public function doSomethingBad()
    {
        foreach ($this->observers as $observer) {
            $observer->reportError(42, 'Something bad happened', $this);
        }
    }

    protected function notify($argument)
    {
        foreach ($this->observers as $observer) {
            $observer->update($argument);
        }
    }

    // Other methods.
}

class Observer
{
    public function update($argument)
    {
        // Do something.
    }

    public function reportError($errorCode, $errorMessage, Subject $subject)
    {
        // Do something
    }

    // Other methods.
}
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class SubjectTest extends TestCase
{
    public function testObserversAreUpdated(): void
    {
        // Create a mock for the Observer class,
        // only mock the update() method.
        $observer = $this->createMock(Observer::class);

        // Set up the expectation for the update() method
        // to be called only once and with the string 'something'
        // as its parameter.
        $observer->expects($this->once())
                 ->method('update')
                 ->with($this->equalTo('something'));

        // Create a Subject object and attach the mocked
        // Observer object to it.
        $subject = new Subject('My subject');
        $subject->attach($observer);

        // Call the doSomething() method on the $subject object
        // which we expect to call the mocked Observer object's
        // update() method with the string 'something'.
        $subject->doSomething();
    }
}

基本的な作り。

  1. createMock(<class>::class)で該当クラスのモックを作成。
  2. expectsに呼出回数条件のオブジェクトをセット。これはなくてもいい。
  3. methodで対象メソッドを指定。
  4. withで該当メソッドの引数の検証条件を指定。
  5. willReturnなどで戻り値を指定。

デフォルトで模擬実装はnullを返す。戻り値を変更したければ、will($this->returnValue())などを指定する。よく使うので短縮記法もある。

Matcher

使えるメソッド。[https://github.com/sebastianbergmann/phpunit/blob/main/src/Framework/TestCase.php#L959] あたりからの一連のメソッドと思われる。

  • any
  • never
  • atLeast
  • once
  • exactly (旧at)
  • atMost
Table 8.2 Matchers
Matcher Meaning
PHPUnit\Framework\MockObject\Matcher\AnyInvokedCount any() Returns a matcher that matches when the method it is evaluated for is executed zero or more times.
PHPUnit\Framework\MockObject\Matcher\InvokedCount never() Returns a matcher that matches when the method it is evaluated for is never executed.
PHPUnit\Framework\MockObject\Matcher\InvokedAtLeastOnce atLeastOnce() Returns a matcher that matches when the method it is evaluated for is executed at least once.
PHPUnit\Framework\MockObject\Matcher\InvokedCount once() Returns a matcher that matches when the method it is evaluated for is executed exactly once.
PHPUnit\Framework\MockObject\Matcher\InvokedCount exactly(int $count) Returns a matcher that matches when the method it is evaluated for is executed exactly $count times.
PHPUnit\Framework\MockObject\Matcher\InvokedAtIndex at(int $index) Returns a matcher that matches when the method it is evaluated for is invoked at the given $index.
Mock builder

https://chatgpt.com/share/68424beb-e544-800b-9292-b79f89cf412d

createStubやcreateMockは対象クラスを全部置換する。一部だけ置換したり、もともと存在しないメソッドを追加したり、複雑なことをしたい場合、getMockBuilderで取得する、Mock Builderを使う必要がある。メソッド一覧は以下となる。

  • onlyMethods(array $methods)Mock Builderオブジェクトで呼び出すことで、設定可能なテストダブルに置き換えるメソッドを指定できます。他のメソッドの動作は変更されません。各メソッドは、指定されたモッククラス内に存在している必要があります。
  • addMethods(array $methods)Mock Builderオブジェクトで呼び出すことで、指定されたモッククラスに(まだ)存在しないメソッドを指定できます。他のメソッドの動作は同じです。
  • setMethodsExcept(array $methods)Mock Builderオブジェクトで を呼び出すことで、他のすべてのパブリックメソッドを置き換えながら、設定可能なテストダブルに置き換えないメソッドを指定できます。これは の逆の動作をしますonlyMethods()
  • setConstructorArgs(array $args)元のクラスのコンストラクターに渡されるパラメーター配列を提供するために呼び出すことができます (デフォルトではダミー実装に置き換えられません)。
  • setMockClassName($name)生成されたテストダブルクラスのクラス名を指定するために使用できます。
  • disableOriginalConstructor()元のクラスのコンストラクターへの呼び出しを無効にするために使用できます。
  • disableOriginalClone()元のクラスのクローンコンストラクターの呼び出しを無効にするために使用できます。
  • disableAutoload()__autoload()テストダブルクラスの生成中に無効にするために使用できます。
use PHPUnit\Framework\TestCase;

class MyClassTest extends TestCase
{
    public function testAddColumnAddsNewColumn()
    {
        // fetchDataだけをスタブ化する
        $stub = $this->getMockBuilder(MyClass::class)
                     ->onlyMethods(['fetchData']) // ← ここでfetchDataだけモック化
                     ->getMock();

        // fetchDataが返す想定の配列をセット
        $stub->method('fetchData')->willReturn([
            ['id' => 1, 'name' => 'Alice'],
            ['id' => 2, 'name' => 'Bob'],
        ]);

        // addColumnを呼び出してテスト
        $result = $stub->addColumn();

        // 検証
        $this->assertEquals('value', $result[0]['new_column']);
        $this->assertEquals('value', $result[1]['new_column']);
    }
}

上記のように、最終的にgetMock()でインスタンスを取得して、methodなどで戻り値を設定する。

MVC

https://chatgpt.com/share/68395ad2-678c-800b-b27a-19c58e3a3a0a

MVC系のアプリで、Viewにセットする値・変数をチェックしたいことがある。

この場合、ControllerでViewにセットしているメソッドを試験する。

$controller-><view>->assign()
$controller->setApp()

これらのメソッドを、expects/withで引数を検査する。willReturnはいらない。assert系のメソッドも呼ばなくていい。expects/withで引数の検査をするイメージ。

expects

https://chatgpt.com/share/682e8fd6-376c-800b-a01e-faa832b0fd07

8. Test Doubles — PHPUnit 9.6 Manual」でモックオブジェクトを作成時に、模擬したメソッドの設定時に、expectsを使っている。

このexpectsは該当メソッドが呼ばれたか、呼ばれなかったかを試験するためのもの。例えば、if文で条件分岐していて、その中で該当メソッドが呼ばれたかどうかをチェックできる。

一本道で、呼び出し有無を気にしなくていいなら、expectsは不要。if文の条件を気にしなくて、一緒に試験できるのが便利。

なお、expectsを使わない場合、同名で競合するので、methodという名前のメソッドが、モックオブジェクトにあってはいけない。ある場合、expects($this->any())でexpectsを挟む必要がある。

ただ、methodという名前のメソッドがあることは普通ないと思う。

このexpectsは「phpunit/src/Framework/MockObject/Runtime/Interface/MockObject.php at 5226e323514a73151c1c7909af224c01a1bbe9aa · sebastianbergmann/phpunit

interface MockObject extends Stub
{
    public function expects(InvocationOrder $invocationRule): InvocationStubber;
}

これが定義な模様。引数にはInvocationOrderをとる。InvocationOrderは「phpunit/src/Framework/MockObject/Runtime/Rule/InvokedCount.php at main · sebastianbergmann/phpunit」などで継承されている。

with

モックオブジェクトのメソッドの引数の検証に使うメソッド。ソースコードは以下。

https://chatgpt.com/share/683d657e-8578-800b-8e71-b09dbce969dd

8. Test Doubles — PHPUnit 9.6 Manual」に記載がある。

The with() method can take any number of arguments, corresponding to the number of arguments to the method being mocked. You can specify more advanced constraints on the method’s arguments than a simple match.

モック対象メソッドの個数と同じ引数を受け取れる。

それぞれの引数の位置で、該当引数の検証条件を指定する。指定可能な内容は以下の3種類。

  1. リテラル値
  2. Constraint: 指定可能なConstraints (制約) は「1. Assertions — PHPUnit 9.6 Manual」にある。
  3. callback: callbackの引数は、検証対象の引数で、OKならtrueを返す。

配列要素数や、複数のConstraintを組み合わせたい場合などは、callbackを使うしかない。

setAppやassignなどで、同じメソッドで1番目の引数に応じて、2番目の内容が変わる場合、工夫が必要。

expectやwithでは検証しないで、willReturnCallbackを使うしかない。連想配列に、実際に渡ってきたキー・バリューのセットを格納して、実行後にまとめてassertするか、コールバック内でassertしてチェックする。

$mock->method('assign')
     ->willReturnCallback(function ($key, $val) {
         switch ($key) {
             case 'title':
                 PHPUnit\Framework\Assert::assertSame('マイページ', $val);
                 break;
             case 'user':
                 PHPUnit\Framework\Assert::assertInstanceOf(User::class, $val);
                 break;
             default:
                 PHPUnit\Framework\Assert::fail("Unexpected key: $key");
         }
     });
$assigned = [];

$mock->method('assign')
     ->willReturnCallback(function ($key, $val) use (&$assigned) {
         $assigned[$key] = $val;
     });

// テスト対象実行後にチェック
$this->assertSame('マイページ', $assigned['title'] ?? null);
$this->assertInstanceOf(User::class, $assigned['user'] ?? null);

willReturnCallback内のifでいい気がする。複雑なら後者のスパイ風で。

willReturnCallbackでやる場合、Viewに渡すメソッドのもともとの戻り値がnullだから問題ないが、そうでない場合は、ちゃんとreturnでダミーの値を返さないと、後続の処理で不都合出るので注意する。

withで検査する場合、self::assertSame()でメソッドの検証はせずに、単にテスト対象メソッドを呼び出す。呼び出すと、withで仕込んだものが呼ばれて、中で検証するイメージになる。

DI

https://chatgpt.com/share/683faf08-f374-800b-ac40-b96b427d7365

テスト対象クラスのプロパティーのインスタンスをテストダブルに置換する場合、注意が必要。だいたい、privateになっているから、DIで渡す前に、置換対象のメソッドの模擬を設定してから、コンストラクターに渡す必要がある。

面倒くさかったら、テストダブルインスタンスの作成+メソッド設定を共通メソッドにしてもよいかもしれない。が、テストダブルの戻り値は、DB取得結果とかだと大事なので、手間だが1個ずつやった方がいいかもしれない。

protected function prepareLoggerWithLogReturn($value): LoggerInterface
{
    $logger = $this->createLoggerMock();
    $logger->method('log')->willReturn($value);
    return $logger;
}

$logger = $this->prepareLoggerWithLogReturn(true);
外部static/グローバル関数の模擬

https://chatgpt.com/share/68424beb-e544-800b-9292-b79f89cf412d

例えば、ログインユーザーIDなど、引数に渡すまでもない共通値の取得が、SUTのメソッド内にあったりする。ただ、こういうstaticやグローバルなメソッド・関数は、PHPUnitで置換が難しい。

回避方法がいくつかある。

  1. DIで対象staticメソッドクラスをプロパティーに持たせる。
  2. 1と似た考えで、ラッパークラスを作る。
  3. 対象クラス内で、ラッパーメソッド (protected) を用意する。

1や2が望ましいようだが、3のラッパーメソッド用意は簡単。ひとまず3でいい。

テストのためだけに意味ない関数を追加するように見える。が、「テストできないコードはそれだけで設計に問題がある」とも言える。そういうものと思っておくと良いらしい。

Other

Test private/protected

クラスのprivate/protectedメソッドのテストには工夫が必要となる。

    /**
     * privateメソッドを実行する.
     * @param object $sut テスト対象のインスタンス。
     * @param string $method_name privateメソッドの名前。
     * @param array $param privateメソッドに渡す引数。
     * @return mixed 実行結果。
     * @throws \ReflectionException 引数のクラスがない場合に発生.
     */
    private function doMethod(object $sut, string $method_name, array $param): mixed
    {
        // ReflectionClassをテスト対象のクラスをもとに作る.
        $reflection = new \ReflectionClass($sut);
        // メソッドを取得する.
        $method = $reflection->getMethod($method_name);
        // アクセス許可をする.
        $method->setAccessible(true);
        // メソッドを実行して返却値をそのまま返す.
        return $method->invokeArgs($sut, $param);
    }

ReflectionClassを使って取得できる。上記の関数のFormController部分を試験対象のクラスに差し替えればOK。getProperty/getValueでprivateプロパティーも取得可能。

https://chatgpt.com/c/673ebadb-1638-800b-89f0-b3ed131317da

基本はpublicメソッドのみテストすべきという考え方。

class MyClass {
    private function myPrivateMethod($a, $b) {
        return $a + $b;
    }
}

class MyClassTest extends PHPUnit\Framework\TestCase {
    public function testMyPrivateMethod() {
        $object = new MyClass();

        // Reflectionを使ってprivateメソッドにアクセス
        $reflection = new ReflectionClass($object);
        $method = $reflection->getMethod('myPrivateMethod');
        $method->setAccessible(true);

        // メソッドを呼び出してテスト
        $result = $method->invoke($object, 2, 3);
        $this->assertEquals(5, $result);
    }
}
Test header

Ref: unit testing - Test PHP headers with PHPUnit - Stack Overflow.

header関数を使用する場合、phpunitの標準出力と干渉して以下のエラーが出て試験できない。

Cannot modify header information - headers already sent by (output started at .../vendor/phpunit/phpunit/src/Util/Printer.php:138)

回避方法が2種類ある。

  1. @runInSeparateProcess
  2. phpunit --stderr

1個目のアノテーションをテストメソッドに指定すると別プロセスでの実行になる。ただ、プロセス生成は時間がかかるため、試験が多いと効率が悪い。

2個目のphpunitの出力を標準エラーに出力させる方法がシンプルで効率もいい。phpunit.xmlに stderr="true"を指定するとキー入力を省略できる。こちらで対応しよう。

Test exit

Ref:

header()後のexit()など、exit/dieを使用するコードがある。phpunit内でこれらがあると、テストも強制終了になる。

上記の別プロセスで実行していた場合、以下のエラーになる。

Test was run in child process and ended unexpectedly

対処方法がいくつかある。

  1. exitを使わないコードに変更。
  2. isTestのようなフラグを元コードに入れてテスト可否で分岐してexitを回避。
  3. execで外部プロセスで実行してexitCodeを試験。
  4. exit/die部分だけ別関数に抽出してmockで置換?

<https://notabug.org/gnusocialjp/gnusocial/src/main/actions/apiaccountregister.php> のclientErrorが内部でexitする。

このclientErrorをwillなどで置換すればよさそう?

sut

https://chatgpt.com/share/682ada62-d3e0-800b-9444-db245dc44183

試験対象のクラスをメンバー変数・プロパティーに格納する。その際の名前を何にするか?

instance/classとかが思いつく。対話AIによると、sutというのがいいらしい。system under testの略。テスト対象の意味。試験の専門用語っぽい。短いのでこれでいいと思う。

表示内容の試験

画面UIに該当文字列があるかどうかなどを試験したいことがある。

LaravelにはassertSeeというのがあるのでこれを使える。

PHPUnit自体にはない。assertStringContainsStringなど、文字列試験メソッドを使って、自分でresponseを何かで取得して評価する。こういうのは基本は機能試験で行う内容。

単体試験と機能試験

https://chatgpt.com/share/682ada62-d3e0-800b-9444-db245dc44183

単体試験と機能試験がある。

単体試験は、基本的にクラス単位。クラスのメソッドを試験するイメージ。

機能試験は、特定機能に関する、クラス・メソッドを試験する。1個の試験で、複数クラスを試験する違いがある。

実際のアプリ開発では、ユーザー動作や仕様の動作が重要だから、機能試験中心で問題ない気がする。

重要なところ、複雑なところ、バグの多いところをUnitTestで試験するのがいいと思う。メンテ不能なテストができてしまうのは避けたい。

  • tests
    • Unit
    • Feature

Unitは元ファイルのディレクトリー構成にして、Featureは機能単位。Featureで機能単位の試験を入れる。で、基本はFeatureを拡充させる。

ファイル名・クラス名。Featureの方はXXFreatureTest.phpとかにすることが多いらしい。が、せっかくディレクトリー分けている意味がないので、Test.phpでいいでしょう。

result cache

phpunitを実行すると、.phpunit.result.cacheファイルが作成される。

phpunit.xmlのcacheResultがデフォルトtrueになっており作成されている。

  • phpunit.xml
    • phpunit.cacheResult: 初期値true。結果キャッシュ作成有無。
    • phpunit.cacheResultFile: 初期値.phpunit.result.cache。ファイルパス。
  • option
    • --cache-result: キャッシュ結果を出力する (既定)。
    • --do-not-cache-result: キャッシュ結果を出力しない。
    • --cache-result-file <file>: キャッシュ結果のパスを指定する (既定: ./.phpunit.result.cache)。

cacheResultFileは他にPHPUNIT_RESULT_CACHE環境変数でも設定できる模様。

phpstanの結果キャッシュをvar/tmp/phpstanに配置しているので、これにならってvar/tmp/phpunit/.phpunit.result.cacheにするといいかも。

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/bootstrap.php" cacheResultFile="var/tmp/phpunit/.phpunit.result.cache" colors="true">
    <testsuites>
        <testsuite name="tests">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
</phpunit>
getallheaders()
Error: Call to undefined function getallheaders()

apache_request_headers()のエイリアス。サーバー固有のAPIはPHPUnit実行時は使えないので、bootstrap.phpに、代替関数を用意する。

if (!function_exists('getallheaders')) {
    function getallheaders() {
    $headers = [];
    foreach ($_SERVER as $name => $value) {
        if (substr($name, 0, 5) == 'HTTP_') {
            $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
        }
    }
    return $headers;
    }
}
PHP Fatal error:  Cannot declare interface PHPUnit_Framework_SelfDescribing, because the name is already in use in
     PHP Fatal error:  Cannot declare interface PHPUnit_Framework_SelfDescribing, because the name is already in use in /common/php/src/PHPUnit/Framework/SelfDescribing.php on line 58                                  
     Fatal error: Cannot declare interface PHPUnit_Framework_SelfDescribing, because the name is already in use in /common/php/src/PHPUnit/Framework/SelfDescribing.php on line 58                                       
      while running parallel worker

古いPHPUnitがある環境に、新しめのPHPUnitがある状態で、PHPStanを実行すると上記のエラーが出てしまった。

https://chatgpt.com/share/6837b3e4-01ac-800b-a547-164fcca8dbf8

古いPHPUnitと新しいPHPUnitとで、同じクラスでも指定方法が変わっている。その都合で、両方がinclude_pathにあると競合する。include_pathから除外する必要がある。

テストのグループ化

https://chatgpt.com/share/68390cd8-5cc4-800b-afce-36c2abdc4b83

Jestにはdescribe()内にit()を入れるような、テストメソッドのネスト構造ができた。PHPUnitにはそれがない。

@groupアノテーションがあるが、これは--groupや--exclude-groupで指定したグループに属するテストを対象にしたり、除外したりするためのもの。ネストとは概念が異なる。

ネスト構造はクラスとメソッドの親子関係での表現になる。分けたかったら、テストメソッドの命名規則などになる。

親クラスBaseTestCase

テストクラスで、共通の処理とかしたいことがある。親クラスにまとめたい。

PHPUntはTestCaseがテスト対象じゃないことを意味するので、親クラスも最後はTestではなくてTestCaseにする。

https://grok.com/share/c2hhcmQtMw%3D%3D_0cfa311d-b290-4d5b-9183-ca7f7efb2d53

BaseTestCaseとか。

共通テストメソッドはassertXxxにする。testXxxはPHPUnitのtest対象になるので。

プロセス

https://grok.com/share/c2hhcmQtMw%3D%3D_e1586c8a-b519-44be-b04d-185e61969ae2

phpunitは基本的に同一プロセスで全テストを実行する。その都合で、あるファイルでincludeしたシンボルはグローバルに存在するので、他のファイルにも影響ある。

あるファイルAでincludeしてシンボルAAが登場して、別のファイルBでincludeするファイル内にシンボルBBがある場合、エラーになる。

対策がいくつかある。

  • 名前空間を活用してシンボル衝突を防ぐ。
  • phpunit --process-isolationのオプションを指定。ただし、遅くなる。
  • テストスイートの分離。

根本的には、同じシンボルが複数ファイルでグローバルで登場するのがまずい。名前空間、require_once、function_existsとかでガードすべき。元ファイルが想定していないなら、--process-isolationオプションを使う。