PHP tool
PHPDoc
About
PHPDocはPHP流のコメントのスタイル。phpDocumentorはPHPDocからドキュメントを生成するツール。PHPDocを解析して文書を作成したり利用するソフトはいろいろある。例えば、VS CodeやPHPStormなどのIDEもPHPDocを使う。
Definition of a 'Type
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}
@return
https://chatgpt.com/c/67998298-f248-800b-a921-a47210ab8d72
関数やメソッドの説明のために関数定義の前に書くのが正しい使い方。
return文の直前に書くものではない。returnのところを説明したかったら、通常コメント。
ただ、関数の@returnでreturnが何を返すのかを書いた方がいい。
注意喚起
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での表現方法がある。
- @return array
- @return int[]
- @return int[][]
- @return (int|string)[]
PHPDocでの配列の表現方法は以上。単純配列の配列の配列の場合、[]を増やせばいい。
連想配列については説明なし。arrayしかない。単純配列はPHPDocのこれで問題ない。問題は連想配列。
ArrayShape
- Array types - Documentation
- PHPDoc Types | PHPStan
- ArrayShape : The PhpStorm Blog | The JetBrains Blog
- PhpStorm の新機能 - 究極の進化を遂げた最強の PHP 開発環境
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>
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部構成。
- Summary=短い説明。改行直前の.か空行で終わり。
- Description=長い説明。アルゴリズムの機能や、使用方法、例など。最初のタグか、改行、DocBlockの終端で終わる。
- 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
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をダウンロードしてそれを使って任意の場所に設置する。
Usage
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)
ライブラリーの自動読み込みのために,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欄に指定することで,ライブラリー以外の自分のコードも自動読み込みの対象にできる。
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
自前のライブラリーを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の方法をとるしかない。
- vendorディレクトリーをコミットに含める。
- リリース用zipにのみvendorを含める (例: Yoast/wordpress-seo: Yoast SEO for WordPress)。
Yoast SEOはGruntでリリース用に用意している模様。
composer.jsonにscriptsを含めることできるし、package.jsonでもOK。
Repositories
パッケージとリポジトリーの概念。
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
- 静的コード解析 - Wikipedia
- PHPerのための「静的解析」を語り合う【PHP TechCafe イベントレポート】 - RAKUS Developers Blog | ラクス エンジニアブログ
- PHPコードの静的解析ツールたち - Innovator Japan Engineers’ Blog
- 行事: 「12月にPHP8.3が出るので、PHP8で増えた文法をおさらいしましょうセミナー」参加報告 | PHP8対応の肝は型とエラーレベル | GNU social JP Web
PHPの静的解析ツール。
- php -l
- PHPStan
- Larastan: LaravelでPHPStanを使うと、static呼び出しなどで大量のエラーが出ますので、それらをカバーしてくれます。
- PHP Code Sniffer
- PHPMD
- Psalm
- PhpStorm
- Rector
上記が有名。
PHPStanとPsalmはベースラインを設定できるので既存のプロジェクトにも導入しやすい。
PHPのバージョンアップの互換性の確認などもできる。
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
composer require --dev phpstan/phpstan
以下のコマンドでバージョンを確認できればOK。
vendor/bin/phpstan analyze --version
PHPStan - PHP Static Analysis Tool 2.1.10
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
includes:
- phpstan-baseline.neon
parameters:
level: 0
tmpDir: var/tmp/phpstan
# parallel:
# processTimeout: 15000.0
# scanDirectories:
# - ../../
# todo ある程度指摘対応ができたら常に全体解析にする。
# paths:
excludePaths:
- var
最初は修正ファイルだけphpstanで解析して、安定してきたらプロジェクト全体を解析する形にするとよいだろう。
phpstanのファイル単独実行は時間がかかるので、修正対象をまとめて実行した方がいい。
Command Line Usage
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は基本的に、コマンド引数で指定した、ディレクトリー類を対象に解析する。
毎回コマンド引数を指定するのは手間なので、設定ファイルに記述しておくこともできる。
以下の条件を満たせば、引数なしで、設定ファイルの内容で解析できる。
- phpstan.neonかphpstan.neon.distの存在。
- pathsに解析対象パスリストが存在。
- 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
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
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で読み込む。
Output Format
--error-formatのオプションでPHPStanの報告形式を指定できる。
.neonファイルに以下の形式で指定可能。
parameters: errorFormat: json
特に重要なものがある。
- table: デフォルト。
- raw: 1行1データで機械処理用。文字化けなどが起こりにくい。
- json: IDEなどとの連携用。
- checkstyle: XML形式。CIツール連携用。
困ったらrawを指定しておく。
Ignoring Errors
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 (analyzeAndScan) の指定があれば、除外される。paths内で一部を除外する場合、指定が必要。シンボルとしては見たい場合はanalyze。analyzeと併用したい場合はanalayzeAndScan。scanDirectories以下も同様。
Config Reference
基本は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
- phpunitをインストール
- テストコード記述
- phpunit実行
上記の3ステップで使う。
以下のように引数に、ディレクトリーかテスト対象ファイルの相対パスを探す。
phpunit tests
2. Writing Tests for PHPUnit
出典: 2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual。
基本的な使用方法を整理する。
- 基本的にはクラス単位で試験コードを記載。<Class>クラスの試験コードは<Class>Testの命名にする。
- <Class>Test はPHPUnit\Framwork\TestCaseを継承させる。
- 試験はpublicのtest*メソッドの命名にする。あるいは、@testのアノテーションを付ければ、命名規則に従わなくてもいい。
- test*メソッド内で、assertSame() などで、期待値との比較で試験を行う。
- 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
- 2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual
- https://chatgpt.com/share/68390cd8-5cc4-800b-afce-36c2abdc4b83
ある関数に対して、テストケースを用意して、複数の引数の組み合わせを試験したいことがよくある。こういうときのために、テスト関数に渡す引数を生成する関数のデータプロバイダーを指定できる。@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の使用方法。
- テストデータを配列かIteratorで返却するpublic メソッド (data provider) を用意
- テストメソッドの引数で、data providerの配列を受け付ける。
- 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の先頭に追加するパス。 |
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種類がある。他に、グローバル関数もある。どれを使ってもいい。
入力文字数や好みの問題。グローバル関数でいい気がする。
Negate
assert系メソッドは、assertNotでNotを前置したら否定形になる。
同値判定
- assertSame/assertNotSame: 型と値の両方を検査 (===相当)。
- assertEquals: 値を検査 (==相当)。
オブジェクトの属性値の値が同じかどうかをみたいなら、assertEqualsになる。assertSameは参照 (ポインター) のアドレス値の比較みたいなことをするから。
基本はassertSameでよいと思われる。
- assertEqualsCanonicalizing()
- assertEqualsIgnoringCase()
- assertEqualsWithDelta()
- assertNan
他にこういうバリエーションがある。
論理判定
- assertIsBool
- assertFalse
- assertTrue()
数値判定
- assertGreaterThan
- assertGreaterThanOrEqual
- assertLessThan
- assertLessThanOrEqual
- assertInfinite
文字列判定
- assertIsString
- assertIsInt
- assertIsNumeric
- assertIsFloat
- assertMatchesRegularExpression
- assertStringContainsString
- assertStringContainsStringIgnoringCase()
- assertStringMatchesFormat
- assertStringMatchesFormatFile
- assertStringEndsWith
- assertStringEqualsFile
- assertStringStartsWith
配列判定
配列の検査用のメソッドがある。
- 要素数
- assertEmpty
- assertSameSize(): 2個の配列の要素数が同じか。
- assertCount(): 指定した配列の要素数が指定数か。データの取得数などでこちらをよく使いそう。
- 包含
- assertContains
- assertContainsOnly()
- assertContainsOnlyInstancesOf()
- assertArrayHasKey
クラス
オブジェクト
その他
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:
例外が発生する処理の前に記述しておく。
Output
echoなど標準出力を試験する際も専用のメソッドがある。
void expectOutputRegex(string $regularExpression)void expectOutputString(string $expectedString)bool setOutputCallback(callable $callback)string getActualOutput()
expectExceptionと同様に事前にセットしておく。
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.
テスト時に、依存関係を模擬したもので置換したいことがある。PHPUnitにそういう仕組が用意されている。
「テストダブル - Wikipedia」Doubleは代役、影武者を意味する。ロックマンX4のダブルはたぶんこの英単語が由来だろう。
stub=親、mock=子。
メソッド内で他のクラス・メソッドを使う場合はmockで対象クラスを模擬させる。
Mock Objects
<?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();
}
}
基本的な作り。
- createMock(<class>::class)で該当クラスのモックを作成。
- expectsに呼出回数条件のオブジェクトをセット。これはなくてもいい。
- methodで対象メソッドを指定。
- withで、該当メソッドの引数処理を指定。
- willReturnなどで戻り値を指定。
デフォルトで模擬実装はnullを返す。戻り値を変更したければ、will($this->returnValue())などを指定する。よく使うので短縮記法もある。
| 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で、引数に応じた戻り値を設定する。
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という名前のメソッドがあることは普通ないと思う。
interface MockObject extends Stub
{
public function expects(InvocationOrder $invocationRule): InvocationStubber;
}
これが定義な模様。引数にはInvocationOrderをとる。InvocationOrderは「phpunit/src/Framework/MockObject/Runtime/Rule/InvokedCount.php at main · sebastianbergmann/phpunit」などで継承されている。
使えるメソッド。[https://github.com/sebastianbergmann/phpunit/blob/main/src/Framework/TestCase.php#L959] あたりからの一連のメソッドと思われる。
- any
- never
- atLeast
- once
- exactly
- atMost
Other
Test private/protected
- php - Best practices to test protected methods with PHPUnit - Stack Overflow
- PHPUnitでprivateメソッドをテストする
- privateとprotectedメソッドをPHPUnitでテストする方法 #PHP - Qiita
クラスの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種類ある。
@runInSeparateProcess- phpunit --stderr
1個目のアノテーションをテストメソッドに指定すると別プロセスでの実行になる。ただ、プロセス生成は時間がかかるため、試験が多いと効率が悪い。
2個目のphpunitの出力を標準エラーに出力させる方法がシンプルで効率もいい。phpunit.xmlに stderr="true"を指定するとキー入力を省略できる。こちらで対応しよう。
Test exit
Ref:
- PHP でテストコードを意識したコーディング #PHPUnit - Qiita
- echo + exit しているPHPコードをユニットテストで保護しながら改善する #PHP - Qiita
- header後にdieするテストのアンチパターン - uzullaがブログ
- php - Ignore exit() and die() with PHPUnit - Stack Overflow
- 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
Unitは元ファイルのディレクトリー構成にして、Featureは機能単位。Featureで機能単位の試験を入れる。で、基本はFeatureを拡充させる。
ファイル名・クラス名。Featureの方はXXFreatureTest.phpとかにすることが多いらしい。が、せっかくディレクトリー分けている意味がないので、Test.phpでいいでしょう。
result cache
- php - What is .phpunit.result.cache - Stack Overflow
- 3. The Command-Line Test Runner — PHPUnit 9.6 Manual
- 3. The XML Configuration File — PHPUnit 9.6 Manual
- phpunit/src/Runner/ResultCache/DefaultResultCache.php at 42966d97ce3b66e65beb46411b4f72bf467746f2 · sebastianbergmann/phpunit
- phpunit/src/TextUI/Configuration/Merger.php at 42966d97ce3b66e65beb46411b4f72bf467746f2 · sebastianbergmann/phpunit
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で指定したグループに属するテストを対象にしたり、除外したりするためのもの。ネストとは概念が異なる。
ネスト構造はクラスとメソッドの親子関係での表現になる。分けたかったら、テストメソッドの命名規則などになる。
