RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

型を使いこなすためのPHPDocの書き方

はじめに

こんにちは、takaramです。

今回はPHPDocについて、特にの重要性と、応用的で便利な書き方をご紹介したいと思います!

PHPの型を使いこなしたい

PHPでも近年、静的型付け言語と同じようにメソッドの引数や戻り値、クラスのプロパティなどの型宣言を書くことができるようになってきています。型宣言はPHP5.0から部分的にサポートされていましたが、本格的に使えるようになったのは2015年リリースの7.0あたりからです。

多くの静的型付け言語にあるジェネリクス機能がPHPにはないなど、まだ十分とは言えない部分もありますが、このように動的型付け言語に型宣言を追加するという試みはTypeScriptやPythonRubyなどでも行われていて、時代の流れと言えるかもしれません。

しかしそもそも、なぜPHPにわざわざ型を書く必要があるのでしょう?メリットとしては、以下のようなものがあります。

  • 実行時に型チェックされる(予期しない値が渡されることを防げる)
  • IDEで補完が効きやすくなる
  • 静的解析がより正確に実行できる

このうち実行時型チェックは、新規コードでは安心・便利な一方、型宣言のない時代のコードを引き継ぐ現場では、動いている既存コードに下手に型宣言を追加するとかえって動かなくなるというリスクも考えられます。 そのため、既存コードへの型宣言追加には二の足を踏んでいる現場も多いのではないでしょうか。

しかし、そのためにIDEのコード補完などのメリットを捨ててしまうのはもったいないです。 そこで、PHPDocによる型情報の補足が重要となってきます。PHPDocは基本的にはプログラムの動作に影響しないため、型宣言にくらべると既存コードにも追加しやすいと思います。

以下ではPHPDocとその書き方について、私が担当しているプロダクト「配配メール」の既存コードのPHPDoc整備を進めた経験も交えて紹介していこうと思います。

PHPDocとは?

そもそもPHPDocとは何か、について説明しておきましょう。そんなの知ってるよ!という方は次のセクションまで飛ばしてください。

PHPDocとは、PHPで書かれた下記のものについてのドキュメントを、ソースコードのコメント内に記述するための書式、およびその書式に従って書かれたコメントを指します。

  • 関数
  • 定数
  • クラス
  • メソッド
  • プロパティ
  • etc.

PHPDocは/** ... */Docコメントという形式のコメント内に記載します。 一部のツールやライブラリはDocコメントに特定の内容を書くことで動作を変えるものもあります*1が、基本的には単なるコメントに過ぎないためプログラムの動作には影響しないと思っていいでしょう。

例として、psr/containerソースコードを見てみてください。インターフェースとメソッドの説明がPHPDocで書かれていて、メソッドの引数や戻り値の説明は@param, @returnのような文字列(タグといいます)に続いて書かれているのが分かると思います。

ここでは書式の詳細な説明はしませんが、雰囲気だけでも掴んでください。

PHPDocの書式に則ってコメントを書くことは、通常のコメントと同じくチームの他の開発者や、未来の自分に向けた説明としてももちろん有用です。ただそれだけではなく、PHPDocを活用したツールが多く存在していて、それらを活用できるメリットがあります。具体的には、以下のようなツールがあります。

  • phpDocumentor
    • PHPDocを元にHTML形式のドキュメントを生成してくれるツール。ライブラリのリファレンスなどで使われる。
  • PhpStorm
    • PHPDocから変数等の型を検出し、コード解析、自動補完に利用する。
  • PHPStan, Phan, Psalm
    • 不具合の可能性があるコードを検知してくれる静的解析ツール。引数の型の不一致や存在しないメソッド呼び出しなどを検出する。

PHPDocとこれらのツールを使いこなすことができれば、PHPでの開発を効率的に進められます。

PHPDocの仕様

このように様々なツールで利用されているPHPDocですが、実は明確な仕様というものは決まっていないのです。

言語仕様以外のPHPの標準規格を確立するPSRというプロジェクトの一部として、PHPDocの仕様 "PSR-5" の草案が作成されてはいますが、議論が中々まとまらず、2021年現在未だに標準化されていません。

そのため各ツールで有効とされる書式が微妙に異なり、「あのツールで有効な書き方がこのツールではエラーになる」といったことが発生しうる状態です。実際にPHPDocを書く際には、自分が利用するツールの仕様書や実際の動作を確認して使うのがよいでしょう。

とはいえ、どのツールも大枠の書式は一致しています。基本的な書き方をご存じない方は、phpDocumentorのドキュメントを読んでみてください。

多くのツールで有効なPHPDocの書き方

ここからは多くのツールで有効かつ便利な、PHPDocの応用的な書き方を紹介していきます。

型の書き方

PHPDocの@paramタグや@returnタグ、@varタグ等で何気なく書いている型も、より適切な書き方があるかもしれません。

配列型

PHPDocで配列型といえば普通はarrayですが、それ以外にも書き方があるのをご存知でしょうか?

  • 数値キーの配列: int[], Clazz[]
  • 文字列キーの配列: array<string, int>, array<string, Clazz>
  • キーごとに値の型が異なる配列: array{id: int, name: string}

例として、名前・年齢・住所を持つHumanクラスのサンプルを下に示しています。ここに書かれているPHPDocでは、

  • プロパティ$propertyKeysは文字列の配列
  • プロパティ$propertyはキーが文字列、値が文字列または整数の連想配列
  • コンストラクタの引数はキーnameの値が文字列、キーageの値が整数、キーaddress(省略可能)の値が文字列の連想配列

であることを表しています。

<?php

class Human {
  /** @var string[] */
  private $propertyKeys = ['name', 'age', 'address'];

  /** @var array<string, string|int> */
  private $property = [];

  /**
   * Human constructor.
   *
   * @param array{
   *   name: string,
   *   age: int,
   *   address?: string
   * } $property
   */
  public function __construct($property) {
    foreach ($this->propertyKeys as $propertyKey) {
      $this->property[$propertyKey] = $property[$propertyKey];
    }
  }
}

こうした書き方をすることで、単にarrayと書くのに対していくつかメリットがあります。

  • new Human(['job' => 'engineer'])のような不正な引数を静的解析で検知できる
  • foreach ($array as $key => $value)$key, $valueの型がわかるので、IDEで補完が効いたり、静的解析の精度が上がったりする
  • ドキュメントとしてもわかりやすくなる

int[]形式以外の2つは残念ながらPhpStormでは未対応なのですが、静的解析ツールを利用しているならこの書き方を採用してみてください。

false型

「true か false」を表す型はboolまたはbooleanですが、単にtrue, falseと書くこともできるのはご存知でしょうか?

PHPの組み込み関数では、例えばfile_get_contentsのように、失敗したらfalseを返すような関数があります。 自作関数でもそれにならって「成功時は処理結果を、失敗時はfalseを返す」ような関数にしている場合もあると思います。そのような関数のPHPDocは、@return string|boolよりも@return string|falseと書くべきです。

もしboolと書いてしまった場合、以下のようなコードを書くと静的解析ツールに怒られます。

<?php

/**
 * @param string $fileName
 *
 * @return string|bool
 */
function getContents($fileName) {
  return file_get_contents($fileName);
}

$contents = getContents('/tmp/foo.txt');
if ($contents === false) {
  exit(1);
}
// ファイルの内容を大文字にして出力
echo mb_strtoupper($contents);

$contents === falseのときの処理もきちんとしていて、実際の動作上エラーは出ないコードですが、getContents()の戻り値をstring|boolとしてしまっているため、静的解析ツールは「trueの可能性がある値をmb_strtoupper()の引数に渡している」と判断してしまうのです。

上記のコードのPHPDocを@return string|falseに書き換えることで、静的解析のエラーは解消されます。

PHPStanの例。string|boolだとエラーが出るが、string|falseにすると解消する

@property

@propertyは主にクラスのPHPDocに書くタグで、__get(), __set()を使った動的なプロパティを記述することができます。

……という説明はググれば他にいくらでも出てくるので、今回は少し変わった使い方を紹介したいと思います。 @propertyを使うことで、親クラスから継承したプロパティの型を、より厳密にすることができます。

例)親クラスでDateTimeInterfaceのプロパティを、子クラスではDateTimeImmutableにする

<?php

class Base {
  /** @var DateTimeInterface */
  protected $datetime;

  public function __construct() {
    $this->datetime = new DateTime();
  }
}

/**
 * @property DateTimeImmutable $datetime
 */
class Child extends Base {
  public function __construct() {
    $this->datetime = new DateTimeImmutable();
  }

  /**
   * 明日の日付を返す
   *
   * @return DateTimeImmutable
   */
  public function getTomorrow() {
    return $this->datetime->modify('tomorrow');
  }
}

上記の例では、Childクラスに@property DateTimeImmutable $datetimeをつけることによって、getTomorrow()メソッドのところで静的解析に怒られないようにしています(DateTimeInterface::modifyは存在しないため)。

私のチームで実際に行った例としては、Webフレームワークの各クラスを継承して独自拡張しているクラスのプロパティでこれを利用しました。以下にサンプルコードを示します。

<?php

namespace Framework {
  class Controller {
    /** @var Session */
    protected $session;
  }

  class Session {
  }
}

namespace App {
  /**
   * @property Session $session
   */
  class BaseController extends \Framework\Controller {
  }

  class Session extends \Framework\Session {
    public function originalMethod(): void {
      echo 'I am \App\Session!';
    }
  }

  class SampleController extends BaseController {
    public function index(): void {
      $this->session->originalMethod();
    }
  }
}

このように、フレームワーク\Framework\Controller, \Framework\Sessionクラスを拡張し、\App\BaseController, \App\Sessionクラスを作成しています。一番下でSampleControllerから$this->session->originalMethod()を呼び出していますが、@propertyを書かない場合だと$this->sessionフレームワーク側のSessionクラスだと認識されてしまい、コード補完、静的解析とも上手くいきません。

注意点として、この方法には「@propertyで書いたプロパティがpublicと認識されてしまう」という欠点があります。しかしながら、上記のようなコントローラークラス等であれば、プログラマが自分でnewしてインスタンスを扱うことはほぼ無いためあまり問題にはならないかと思います。

ローカル変数の型

PHPDocを使えば、ローカル変数にも型をつけることができます。

<?php
foreach ($iterable as $item) {
  /** @var DateTime $item */
  echo $item->format('Y/m/d') . PHP_EOL;
}

上記のように、foreachのループ変数の型を示す以外に、「型定義上はmixedだが人間から見ると型が明らか」という場合に便利です。 例えば下の例のように、DBのカラムから値を取得する場合「idinteger型のカラムだから戻り値もintだ」というのはプログラマはわかっていても、IDEや静的解析ツールはそこまで汲み取ってはくれません。 そのような場合にPHPDocで変数の型を明示すると便利です。

例)DBから値を取得する場合、テーブル定義を知っていれば型もわかる

<?php

function getUserId(string $email): int {
  $pdo = new PDO('pgsql:host=localhost;dbname=postgres', 'postgres');
  $statement = $pdo->query('SELECT id FROM users WHERE email = :email');
  $statement->execute([':email' => $email]);
  /** @var int $id */
  $id = $statement->fetchColumn();

  return $id;
}

まとめ

今回は「型」に絞ってPHPDocの応用的な書き方をご紹介しました。このような方法で適切に型を記述することで、

  • IDEの自動補完 → 開発体験の向上
  • 静的解析の精度向上 → バグを減らせる

といったメリットを享受することができます。これからPHPDocを書く際に、この記事の内容が少しでもお役に立てれば幸いです。

おまけ:配配メール開発チームの取り組み

私が所属する配配メール開発チームでは現在、古くからあるコードのPHPDocの整備を進めており、今回紹介した内容も多くはその中で実際に利用したものです。

以前はPHPDocが書かれていなかったり、誤った型が書かれている箇所が多くあるなど問題となっていましたが、そこから整備を進め、現在は多くの箇所で利用している共通関数・クラスの対応を完了しました。その結果、IDEの補完が効くようになったり、誤った不要な警告が出なくなったりといった効果が出始めています*2

今後、さらに範囲を広げてPHPDoc整備を進めながら、現在はできていない静的解析の導入も目指していきたいと考えています。


◆TECH PLAY
techplay.jp

◆connpass
rakus.connpass.com

*1:PHPUnit, PHP-DI など。この用途では、今後はDocコメントではなく、PHP8で導入されたアトリビュート機能に置き換わっていくかも。

*2:なお、新規コードについてはコーディング規約とコードレビューでPHPDocが正しく書かれるようにしています。整備の対象となるコードを新たに生み出さないことも重要ですね。

Copyright © RAKUS Co., Ltd. All rights reserved.