PHP
PSR
PHPDoc

2018年のPHPDoc事情とPSR-5

PHPDocとは、クラスや関数などのブロックに記述できるDocComment内に記述する書式の通称です。この書式の情報源として時折PSR-5が参照されることがあるので簡単に状況をまとめます。

TL;DR

  • PSR-5の標準化ステータスは昨年10月にABANDONED (放棄・議論停滞)になりました
  • PHPDocを型注釈として利用する各処理系の実装にはばらつきがあり、PSR-5とは相違点があります
    • PhpStormは現在のところ(2018.1 EAP)PSR-5と互換性がありません
    • 特にチーム開発では、対応する型記述の書式について注意が必要です
  • PhpStormに配慮して書く場合、phpDocumentorの仕様を参照する方が安全です

この記事は2018年3月11日現在の進行中の話題を扱ってるので、遠からず時代遅れになるおそれがあります。

PSRって何?

PHP-FIG (PHP Framework Interop GroupPHPフレームワーク相互運用グループ)が策定する、PHPの共通仕様です。

PSR (PHP Standards RecommendationsPHP標準勧告)は若干権威を帯びた言葉ですが、「PHP開発者全員が知っておかなければモダンなPHPを書けない」といった性質のものではありません。策定プロセスはPHP本体(The PHP Group)とは直接関係なく、飽くまでPHPを使って構築されたフレームワークやCMSなどの開発者間での合意に過ぎません。

ただし、PSR-7(HTTP Message Interface)をはじめとする有用なクラスの仕様が定義されてますので、もし自分で同じようなクラスの車輪の再発明をするならばPSRで定義されたインターフェイスに準拠することも選択肢のひとつです。

PSR-5の状態

私はWEB+DB PRESS Vol.87で、PHPDocを紹介しました。これは8ページの連載記事ですが、2018年現在においてもPHPDocについて、おそらく最も網羅的にまとめたものです。

この記事において、私はPSR-5について以下のように言及しました。

スクリーンショット 2018-03-11 午後4.09.59.png

残念ながらこの状況は改善せず、断続的な提案はあるもののコーディネイターの不在により議論はまとまらず、2015年末頃を最後に一切の作業が停滞しました。その後2017年10月頃にABANDONEDのステータスとして再分類されました。誰かがコーディネイターとして名乗り出て議論を再開させないと話が進まない状況です。

デファクトにおける型表記の仕様

この記事では、主に型表記の記述方法に絞ってPSR-5と各種実装の現状を比較します。

一般に知られた用語と、この記事で使用する用語の一覧です。

呼び名 説明
型宣言 関数定義の引数や返り値に書ける型の書式 function sum(int ...$num): int
型ヒント
(タイプヒンティング)
PHP5時代の型宣言の旧称 (型宣言より制約が多い) function hoge(array $option)
型注釈
(型アノテーション)
DocCommentに書く変数や引数・返り値に書くタグ
@param, @return, @var, @propertyなど
/** @var string $foo
型表記 型注釈の中に書ける型の書式のこと DateTimeInterface[]

型表記(type notation)の用語は、今回この記事を書くためにでっちあげたものなので、一般に広く受け入れられた用語ではないことをご承知おきください。

phpDocumentorの型表記 (基本)

型の定義 — phpDocumentorに定義があります。特に配列の書式については以下の種類があると説明されます。

  1. 内容の型の記述なし @return array
  2. 内容の単一型を記述 @return int[]
  3. 内容の複数型を記述 @return (int|string)[]

そのほか、2.と普通の複合型(int|string)を組み合せてint[]|string[]と書くこともできます。(これは3.とは別です)

Note
多くのIDEは、おそらくまだこの表記をサポートしません。

この記述の通り、現在でも3.の形式をサポートしないツールはいくつかあります。それ以外の形式はおそらく事実上の標準としてさまざまなツールでサポートされてます。

Annotating Types via PHP Doc Comments - Documentation

PSR-5の型表記

基本的にはPHPDoc形式の型表記を踏襲しつつ、コレクションのジェネリクスをサポートしました。しかし、開発が停滞した時期の都合でiterable?(nullable)が含まれません。

/**
 * @return \ArrayObject<int, \DateTimeInterface>
 */
function hoge() {
    return new ArrayObject([
        new DateTime("2003-04-07"),
        new DateTime("2008-04-01"),
        new DateTime("2112-09-03"),
    ]);
}

/**
 * @return \ArrayObject|\DateTimeInterface[]
 */
function hoge2() {
    return new ArrayObject([
        new DateTime("2003-04-07"),
        new DateTime("2008-04-01"),
        new DateTime("2112-09-03"),
    ]);
}

この形式はphpDocumentor/TypeResolverが2017年12月末にリリースされた0.5.0でようやく対応したほか、PHPStan、Phanなどがサポートします。その一方、PhpStormこの表記を2018.1 EAP(Early Access Program、次期リリースの先行評価版)においても解釈しません。

つまり、PhpStormとの互換性を保つためには、PSR-5形式のジェネリクス記法で書くことを諦める(あるいは冗長に書く…)しかありません。PhpStormを使ってるみなさんはPHPDoc PSR-5 collection type hints formatting : WI-31960に投票してください。

Phanの型表記

Phan独自のタグはAnnotating Your Source Code · phan/phan Wikiで定義されます。PhpDocumentorやPSR-5形式の型表記は基本的に受理されます。

強烈な特徴は、つい最近2018年2月にリリースされたPhan 0.11.2で導入されたarray shapes記法です。これは、配列のキーごとに型を定義できるようになった新たな配列記法です。

この記法は以下のようなパターンに対応できます。

list($id, $name, $prices) = hoge();

/**
 * @return array{0:int,1:string,2:Money[]}
 */
function hoge()
{
    return [1, 'hoge', [new Money(120), new Money(110)]];
}

また、当然連想配列に対しても型検査ができます。

$vocaloids = fuga();

foreach ($vocaloids as $v) {
    printItem($v['item']);
}

/**
 * @return array{name:string,age:int,item:?Item}
 */
function fuga()
{
    return [
        [
            'name' => "初音ミク",
            'age' => 16,
            'item' => new Item("ネギ"),
        ],
        [
            'name' => "重音ミク",
            'age' => 31,
            'item' => new Item("フランスパン"),
        ],
    ];
}

こうなるとPhpStormなどのツールとの互換性が破滅するので、@phan-var, @phan-param, @phan-return, @phan-property, @phan-methodなどの新しいタグが新設されました。array shapes記法を利用する場合は複数の記法を両立することができます。…………おう。

/**
 * @return array[]
 * @phan-return array{name:string,age:int,item:?Item}
 */
function fuga()
{

PHPStanの型表記

PHPStanについては昨日PHPerKaigi 2018での@Hirakuの発表で知りました。で、実装を読んでみて驚いたのですが、PhpDocumentorとPSR-5の型表記に対応してるだけでなく&記法でIntersectionTypeを実装してます。

型の話をしようと思ったら頭が痛くなるので、TypeScriptについて言及された、こちらの高等な型 | TypeScript 日本語ハンドブック | js STUDIOを読んでいただけると雰囲気掴める気がします。要は|(または)の逆で&(かつ)ってだけなのですが。

開発者本人が書いた記事: Union Types vs. Intersection Types – Ondřej Mirtes – Medium

たぶん以下のような型を書いて、引数の型チェックができるはずです。

/**
 * @param Countable&Iterator $xs
 */
function hoge($xs)
{

これも強力でべんりな機能ではあるのですが、いつものようにPhpStormと互換性がありません。

Someone has to drive the innovation 😊 If I had to make a choice of sacrificing either PhpStorm or PHPStan, I'd sacrifice PhpStorm 😊 So the fact that the code is understood by PHPStan is more important to me.

いいぞもっとやれ。例によってPhpStormを急かしたければSupport for intersection types : WI-39419に投票してください。

PSR-5への個人的な不満

機能として欲しいものはいくつかあるのですが、PhpDocumentorの仕様にはあるタグ@property-read@property-writeを返してほしいです……。そもそも現在のプロポーザルでは型については“Appendix A. Types”(付録)なので、勧告の中での位置付けがさっぱりわからない。

結局チームではどう書けばいいの?

PhpStormに配慮して書く場合、現段階ではPSR-5を参照せずphpDocumentorの仕様を参照にする方が安全です。

PhpStormのためのバッドノウハウ

上記にも部分的に書いてありますが、以下のように定義すると\ArrayObject<\DateTimeInterface>と同様のジェネリクスのようなものを実現することできます。しかしこれは甘い罠です。

/**
 * @return \ArrayObject|\DateTimeInterface[]
 */
function hoge2() {
    return new ArrayObject([
        new DateTime("2003-04-07"),
        new DateTime("2008-04-01"),
        new DateTime("2112-09-03"),
    ]);
}

/**
 * @return \DateTimeInterface[]
 */
function hoge4(): ArrayObject {
    return new ArrayObject([
        new DateTime("2003-04-07"),
        new DateTime("2008-04-01"),
        new DateTime("2112-09-03"),
    ]);
}
// ↑ この定義では**地味な警告**が出ます

スクリーンショット 2018-03-13 午後7.14.58.png

型注釈で|を書くか、地味な警告を無視しつつ型宣言と型注釈を書くことで、ジェネリクスっぽいものは再現できます。ループ内ではArrayObjectの補完が、ループ内では\DateTimeInterfaceの補完が機能するようになります。

スクリーンショット 2018-03-13 午後7.04.22.png

スクリーンショット 2018-03-13 午後7.09.02.png

問題は、\DateTimeInterface[]iterableなものの要素が\DateTimeInterfaceである、と思ってほしいのですが、残念なことにphpDocumentorの仕様では “The brackets inform you, and several tools, that this is an array of that Type.” ([]はその型の配列だと通知する) と定義されてるので、その返り値をarray_系の関数に投げてもエラーは通知されなくなります。

スクリーンショット 2018-03-13 午後7.47.31.png

これは補完したさに安易にUnionTypeに頼った罰です。PhpStormは一刻も早くリストのジェネリクスを実装してください。

スクリーンショット 2018-03-13 午後7.49.19.png

↑ 本当はこうやって警告してほしい。

結局どうなの

この記事の初稿の段階では「PSR-5全然実装されてないしだめだろ」みたいな論調で書こうと思ってたのですが、調べてみたらPhpStorm以外では結構実装されちゃってる感じでした。2016年11月あたりにPhan静的解析がもたらす大PHP型検査時代を書いたときにはPSR-5形式の型表記はサポートされてるツールが全然なくて絵に描いた餅だったのですが、気がついたらPHPStanやらphpDocumentor/TypeResolverが対応してくれてたので、あとはPhpStormだけが対応してくれればPSR-5の型が実用できるのでは、といった状況です。

とは言ってもまだ足りない感じがする上にPHPStanが導入してIntersectionType(交差型)を見せられてしまっては、機能としてはともかく、型表記の書式の標準化はがんばってほしいところではあります。

JetBrainsがスポンサーになってPhpStormの関係者がしゃしゃり出てコーディネートしてくれないと話が進みそうにないんですが……。PhanもPHPStanもコア開発者が一人で猛然と開発していくタイプっぽいので、議論をまとめるとかそんな感じではなさげ。うーん。

PhpStormはPHPDocの利用率を押し上げましたが、その書式についてはPhpStormのヘルプにもまともな記述もなくユーザーに混乱をもたらしていることについて、JetBrains関係者にもはっきりと認識していただきたいです。

ちなみにPSR-5が割り当てられたABANDONEDは、PHP版ジョークRFCとして名高い (?) PSR-8 Hug (日本語訳)と同じ分類です。PHPDoc=Huggable

おまけ: 資料