PHP tool
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の親クラスのときだけ継承される。
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 */
Tag
よく使う@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: クラスの注釈部で指定する。メンバー変数の説明。
- @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; } }
Other
Array
PHPでよく使うArray。PHPDocでの表現方法がある。
- @return array
- @return int[]
- @return (int|string)[]
PHPDocでの配列の表現方法は以上。連想配列については説明なし。arrayしかない。
ArrayShape
- Array types - Documentation
- PHPDoc Types | PHPStan
- ArrayShape : The PhpStorm Blog | The JetBrains Blog
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} * } */
Package manager
Composer
PHPのパッケージ管理システム。PHP v5.3.2以上で動作する。
Install
Ref: インストール: Composer | モダンなPHPのパッケージマネージャー – senooken JP.
[ -e installer ] || wget https://getcomposer.org/installer php installer --install-dir="$LOCAL/stow/$PKG-$VER/bin" --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
これにより,vendor
ディレクトリーにパッケージがインストールされる。
プロジェクトに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
ファイルを生成する。以下のように,ライブラリーのクラスを使用するファイルの先頭でこのファイルを読み込めば使えるようになる。
<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
を必ず更新します。
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。
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
外部ツールも不要で手軽で悪くない。特に、-lで実行するphpのバージョンを変えるだけでチェックでき、PHPのバージョンアップ時の事前の簡易チェック、粗探しとしてとして悪くない (PHPerのための「静的解析」を語り合う【PHP TechCafe イベントレポート】 - RAKUS Developers Blog | ラクス エンジニアブログ)。
PHPStan
- 10年開発してきたPHPアプリケーションにPHPStanを導入した - BASEプロダクトチームブログ
- PHPStan導入のすすめ - Hajimari Tech Blog| 株式会社Hajimari
- PHPStanクイックガイド2023
導入が簡単なので黙って導入したらよさそう。
PHPUnit
PHPUnitを使用すれば自動単体テスト (Unit test) が可能だ。
Version
情報源: Supported Versions of PHPUnit – The PHP Testing Framework。
PHPUnitのバージョンごとに対応しているPHPのバージョンが決まっている。
PHP v7.4に対応してい最後のバージョンはPHPUnit 9なので、当分はこれを使うのが良い。
Basic
出典: 2. Writing Tests for PHPUnit — PHPUnit 9.6 Manual。
基本的な使用方法を整理する。
- 基本的にはクラス単位で試験コードを記載。<Class>クラスの試験コードは<Class>Testの命名にする。
- <Class>Test はPHPUnit\Framwork\TestCaseを継承させる。
- 試験はpublicのtest*メソッドの命名にする。あるいは、@testのアノテーションを付ければ、命名規則に従わなくてもいい。
- test*メソッド内で、assertSame() などで、期待値との比較で試験を行う。
例:
<?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)); } }
Depends
前回の試験で準備した結果を利用したい場合、@dependsのアノテーションでテスト関数を指定しておくと、指定したテスト関数のreturnを引数に受け継いだテスト関数を記述できる。
@dependsは複数指定でき、指定順に事前に試験が実行されて引数に渡される。
Data Provider
ある関数に対して、テストケースを用意して、複数の引数の組み合わせを試験したいことがよくある。こういうときのために、テスト関数に渡す引数を生成する関数のデータプロバイダーを指定できる。@dataProviderで関数を指定する。データプロバイダーは引数のリストを配列で返すようにする。
データ数が多い場合、名前付き配列にしておくと、どういうデータ項目で失敗したかがわかりやすい。
Iteratorオブジェクトを返してもいい。
Fixtures
出典: 4. Fixtures — PHPUnit 9.6 Manual。
テストメソッドの実行前に、テスト対象のインスタンスの生成や、DB接続など準備がいろいろある。これをFixturesと呼んでいる。この準備がけっこう手間になる。これを省力できるのがテストフレームワークの利点。
テストメソッド実行前後に共通で行える処理がある。
- setUp/tearDown: テストメソッド単位の前後処理。テスト対象インスタンスの生成など。tearDownは何もしなくてもいいことが多い。
- setUpBeforeClass/tearDownAfterClass: クラス単位の前後処理。 DB接続など。
XML Configuration File
出典:
基本はコマンドでテスト対象クラス・ファイルを指定してテストを実行する。他に、XMLの設定ファイル (phpunit.xml) でもテスト対象を指定できる。
testsディレクトリーの全*Test.phpを対象にする最小限の例は以下。
<phpunit bootstrap="src/autoload.php"> <testsuites> <testsuite name="money"> <directory>tests</directory> </testsuite> </testsuites> </phpunit>
以下のように--testsuiteで試験対象を指定して実行する。
phpunit --bootstrap src/autoload.php --testsuite money
Assertions
Ref:
基本はassertSameでテストすればいいのだが、それ以外にも例外とかいろいろ試験したいケースがあるので、メソッドを整理する。
Exception
特に例外の試験がイレギュラー。
<?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>: テストを指定。
Test Doubles
Ref: 8. Test Doubles — PHPUnit 9.6 Manual.
テスト時に、依存関係を模擬したもので置換したいことがある。PHPUnitにそういう仕組が用意されている。
stub=親、mock=子。
メソッド内で他のクラス・メソッドを使う場合はmockで対象クラスを模擬させる。
<?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で、該当メソッドの引数処理を指定。
デフォルトで模擬実装は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で呼び出し関数をまるごと別のものに置換できる。これが非常に便利。
Topic
Test private/protected
Ref:
- php - Best practices to test protected methods with PHPUnit - Stack Overflow
- PHPUnitでprivateメソッドをテストする
- privateとprotectedメソッドをPHPUnitでテストする方法 #PHP - Qiita
クラスのprivate/protectedメソッドのテストには工夫が必要となる。
/**
* privateメソッドを実行する.
* @param string $methodName privateメソッドの名前
* @param array $param privateメソッドに渡す引数
* @return mixed 実行結果
* @throws \ReflectionException 引数のクラスがない場合に発生.
*/
private function doMethod(string $methodName, array $param)
{
// テスト対象のクラスをnewする.
$controller = $this->instance;
// ReflectionClassをテスト対象のクラスをもとに作る.
$reflection = new \ReflectionClass($controller);
// メソッドを取得する.
$method = $reflection->getMethod($methodName);
// アクセス許可をする.
$method->setAccessible(true);
// メソッドを実行して返却値をそのまま返す.
return $method->invokeArgs($controller, $param);
}
ReflectionClassを使って取得できる。上記の関数のFormController部分を試験対象のクラスに差し替えればOK。$this->instanceを指定しておけばそのまま流用できるか。getProperty/getValueでprivateプロパティーも取得可能。
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などで置換すればよさそう?