2025-09-09

Better PHP Guideline

フォーマッタレベルの話ではないここは抑えてほしいなという感じのPHPのガイドラインというのをどの会社でも毎回書いている気がするので、もうここでまとめて参照させたいな、という意図で公開してみます。


を書く

PHPは型宣言がサポートされています。適切に型付けしておくと

といったようなメリットが得られるため、積極的に型を付けていくと良いです。

型付けできるところは以下の箇所になります。

静的解析ツールを使ってより詳細に型検査を行いたい場合はPHPDocによる型記述も有効です。

PHPでは文字列の配列も数字の配列も同じ array で表現されますがPHPDocの場合は

/** @var string[] */
/** @var int[] */
/** @var array<int, string> */
/** @var array<int, int> */

のような型宣言によって区別することができます。

併せてPHPStanなどの静的解析も入れられると完璧です!

# bad
public function someMethod($arg) {}

# good
public function someMethod(string $arg): void {}

strict_types を有効化する

デフォルトでは関数・メソッドの引数の型と実際の値が間違っていても型変換を行おうとします。

function hoge(string $str) { var_dump($str); }

hoge(1); // => string(1) "1"
hoge(false); // => string(0) ""

strict_typesを有効にしておくと、実行時に引数の型が間違っている場合にエラーが発生します。これにより、意図しない引数の型やキャストを防ぐことができます。

declare(strict_types=1);

function hoge(string $str) { var_dump($str); }

hoge(1); // => TypeError  hoge(): Argument #1 ($str) must be of type string, int given, called on line 2.

strict types宣言をしたファイル内での呼び出し元のみエラーが発生することに注意してください。
参考: https://qiita.com/Hiraku/items/735f0666ab3d34c52efa

マジックメソッド を使わない

マジックメソッドは便利ですが、一般的に可読性が悪くコード補完や静的解析が効かない(効きづらい)ため推奨しません。 ライブラリなど抽象化されたコードでない限りは基本的に利用する場面も無いと思うので、if分岐させるなどベタ書きしていくことを推奨します。

# bad
public function __get($propertyName) { ... }
public function __set($propertyName) { ... }
public function __call($propertyName) { ... }

どうしても使いたい場合(使ったほうが可読性・保守性が良くなる場合)はPHPDocの @property @method を使って補完や静的解析を効かせた上で利用すると良いです。

動的呼び出しを使わない

マジックメソッド同様、動的呼び出しも可読性・コード補完・静的解析の面でデメリットが大きいため推奨しません。こちらもif文などを使って愚直にコードを書いていくのが結果として読みやすく保守性の高いコードになると思います。

# bad
$$var;
$$func();
$this->$property;
$this->$someMethod();
new $class();

# good
$res = match ($type) {
  'hoge' => $this->hoge(),
  'fuga' => $this->fuga(),
  default => throw new Exception('unknown type'),
};

$handler = null;
if ($type === 'hoge') {
  $handler = new Hoge();
} elseif ($type === 'fuga') {
  $handler = new Fuga();
}

スーパーグローバル変数を使わない

Laravelなどのフレームワークを使っていればスーパーグローバル変数を使う理由がありません。むしろ、スーパーグローバル変数を使ってしまうとテストがしづらくなるので使わないほうが良いです。

Laravelであれば以下のように書き換えられます。

// before
$_COOKIE['hoge'];
$_REQUEST['fuga'];
$_SERVER['REQUEST_URI'];

// after
/** @var $request Illuminate\Http\Request */
$request->cookie('hoge');
$request->get('fuga');
$request->getRequestUri();

echo, header(), exit を使わない

Laravelなどのフレームワークを使っている場合、echo, header(), exitを使う理由がありません。スーパーグローバル変数と同じく、これらを使うことでテストがしづらくなります。

Laravelであれば普通にResponseオブジェクトを使ってボディやヘッダを設定すれば良いです。処理を中断したい場合はexitではなく abort() や例外を投げるだけで良いです。

readonly を使う

プロパティが読み取り専用の場合は意図しない書き込みが発生しないように readonly を設定しておくと安全です。大抵のケースではコンストラクタでプロパティを設定して、そのプロパティを使って処理するだけ、という感じだと思うので readonly class を設定できるクラスもそれなりにあるのではないかと思います。

# bad
class Foo
{
  public string $foo = '';
}

# good
readonly class Foo
{
  public string $foo = '';
}

class Foo
{
  public readonly string $foo = '';
}

Constructor Property Promotion を使う

コンストラクタで受け取った引数をそのままプロパティに設定するような場合は Constructor Property Promotion を使うと記述を簡略化できます。

# bad
class Foo
{
  public string $bar;
  
  public function __construct(string $bar)
  {
    $this->bar = $bar;
  }
}

# good
class Foo
{
  public function __construct(public string $bar) {}
}

Property Hooks を使う: >=8.4

getter/setterメソッドを簡略化して書くことができて便利です。

# bad
public function getHoge(): string { return ''; }

# good
public string $hoge {
  get { return ''; }
}

Nullsafe演算子 を使う

Nullsafe演算子 ?-> を使うとnullチェックを省略でき、より可読性の高いコードを書くことができます。

# bad
$tmp = null;
if ($foo !== null) {
  if ($foo->bar !== null) {
    $tmp = $foo->bar->baz;
  }
}

# good
$tmp = $foo?->bar?->baz;

プロパティだけではなくメソッド呼び出しもOKです。存在しないメソッド呼び出しもエラーにならないです。

$foo?->foo()?->bar();

名前付き引数 を使う

デフォルト引数が複数設定されていて、呼び出し時に一部の引数だけ値をセットしたい場合は名前付き引数が有効です。

function hoge($a, $b = 'B', $c = 'C')
{
  echo implode(',', [$a, $b, $c]);
}

hoge('A'); // => A,B,C
hoge(a: 'a', b: 'b'); // => a,b,C # 変数名で名前を付けて呼び出しできる
hoge(a: 'a', c: 'c'); // => a,B,c # 途中の省略可能な引数を省略できる。8.0以前はそれまでの引数をすべて書く必要があった

また呼び出し元コードに明示的に引数名を記述することで可読性を上げることもできます。

function hoge($isStudent, $hasLicense): bool { return ... }

hoge(true, false); // 引数が何を表しているのかわからない…
hoge(isStudent: true, hasLicense: false); // 引数の意味がわかりやすい!

オブジェクトを生成するとき複数のオプションがある場合、名前付き引数が無い場合はオプションとして配列を渡すケースが多かったのですが、名前付き引数によりデフォルト引数から変える部分だけ渡せば良くなったため、より型安全なオブジェクト生成・設定ができるようになります。

# before
class Hoge
{
  public function __construct(array $options = [])
  {
    $this->option1 = $options['option1'] ?? null;
    $this->option2 = $options['option2'] ?? null;
    // ...
  }
}

new Hoge(); // デフォルト
new Hoge(['option2' => true]); // 型安全ではないし、実装によっては意味のないキーや不正な型も通ってしまう

# after
class Hoge
{
  public function __construct(
    private readonly ?string $option1 = null,
    private readonly bool $option2 = false,
  ) {}
}

new Hoge(); // デフォルト
new Hoge(option2: true); // option2だけ変更

参考: https://qiita.com/rana_kualu/items/13bc0c30192ee22c1396

match式 を使う

if文やswitch文で分岐させて変数や戻り値を決定する場合、match式を使うとすっきり書けることが多いです。

# bad
$tmp = '';
switch ($type) {
  case 'A':
    $tmp = 'a';
    ...
}

# good
$tmp = match($type) {
  'A' => 'a',
  ...
};

ifやswitchだと変数が設定されている(=副作用がある)箇所を考えながらコードを読むことになりますが、match式の場合は変数設定されるのが1箇所に集約されるので結果的に読みやすくなると思います。if/switchは命令的、matchは宣言的、とも言えるかもしれません。 match式の場合は厳密比較で、複数行の処理を書く場合は無名関数など逆に可読性が悪くなるケースがあるなど、すべてを置き換えれるわけではないですが利用できるケースでは積極的に使うと良いと思います。

参考: https://tech-blog.rakus.co.jp/entry/20200917/php

その他

final について

不要な継承をさせないという目的でfinalをつけることがありますが、 Mockeryなどの動的継承を使うライブラリ が動かなくなるので注意してください。

とはいえ、Laravelの場合はファサードを使ったりDIでモック対応できるので、チームの習熟度によって利用可否を判断していくと良いと思います。

interface について

interfaceを使って抽象化する目的としては

  1. ポリモーフィズムのような感じで呼び出し元で処理の切り替えができるようにする
  2. テストでモックできるようにする

の2点あります。

2のパターンの場合は、Mockeryなどの動的継承を使えばモックできるようになるのでテスト用途でinterfaceにしたい場合は不要なケースが多いです。finalをどうしても使いたい場合は動的継承できないので必然的にinterfaceを使うことになります。

前述の通り、Laravelの場合はHTTPリクエストやS3などのストレージアクセスはFacadeでモックできたりするため、チーム内でHTTP FacadeやStorage Facadeを使っても問題なければinterfaceを利用しなくても良いかもしれません。

またinterfaceを使うと抽象に定義ジャンプすることが多くなり可読性が下がる可能性があったり、単純にファイルが増えがちになるので、そのあたりの保守性を検討したうえで導入すると良いです。

PSR-4 によるオートロードを使う

composerはPSR4のオートロードに対応しているので、ディレクトリやクラス名・ファイル名はそれに準拠する形で設定してください。

# src/Foo/Bar/Baz.php

namespace Foo\Bar;

class Baz
{}

new Foo\Bar\Baz(); // 初回呼び出し時 Foo/Bar/Baz.php を検索してrequireされる。

一応PSR-4ではないオートロードにも対応しているのですが、PSR-0は非推奨、classmapは特定ディレクトリ以下のファイル名とクラス名をマッピングするため、開発時でクラスが作成されるたびに composer dumpautoload を呼び出す必要があるなど開発体験が悪くなりがちです。