【PHP】型宣言とNull許容型の書き方|strict_typesと実務での落とし穴

PHP の関数やメソッドに型を付けると、想定外の値を実行時に検出しやすくなります。一方でデフォルトではスカラー型が暗黙変換され、declare(strict_types=1) の有無で同じ呼び出しでも結果が変わります。本記事では型宣言と Null 許容型、strict_types の適用範囲、実務の落とし穴を整理します。

コード例は執筆時点で PHP 8.x を主な参照先とします(手元では php -v で確認してください)。細部はバージョン差があるため、PHP 公式マニュアルの型宣言の節 を並行して参照することをおすすめします。

はじめに

「文字列の "1"int 引数に渡したら通った」「別ファイルから呼ぶと TypeError になった」「null を渡したら落ちた」——こうした型まわりの挙動で迷ったことはありませんか。PHP はデフォルトではスカラー型を可能な範囲で変換するため、型宣言を付けても「厳密」とは限りません。加えて strict_types はファイル単位で効きます。厳密になるかどうかは 呼び出し側 の設定に左右され、直感とずれやすいです。

この記事で扱う範囲は次のとおりです。

  • パラメータ・戻り値・プロパティの型宣言の基本
  • Null 許容型(?TT|null)の書き方
  • declare(strict_types=1) の意味と適用範囲
  • coercive モードとの違い、よくある失敗例
  • 実務で試せる次のアクション

主眼はスカラー型・Null 許容・strict_types です。ユニオン型や mixed は補足に留め、詳細は公式マニュアルを参照してください。

この記事で使う用語

用語意味
スカラー型bool / int / float / string(クラス型とは別)
coercive モードデフォルト。可能な範囲で期待の型へ暗黙変換する
TypeError型が合わないときに実行時に投げられる例外
ユーザー定義関数自分で書いた function / メソッド
内部関数PHP 組み込みの strlen() など

背景:型宣言があると何が起きるか

型宣言は、関数のパラメータや戻り値、クラスのプロパティ(PHP 7.4.0 以降)に付けられます。宣言と合わない値が渡されると TypeError がスローされます(公式マニュアル)。クラス定数への型宣言は PHP 8.3.0 以降で追加されていますが、本記事では深入りしません。

対象導入の目安(公式の変更履歴より)
スカラー型のパラメータ / 戻り値PHP 7.0
Null 許容(?TPHP 7.1
プロパティの型PHP 7.4
ユニオン型(`intstring` など)

型を付ける目的は「実行時のガード」と「読み手への契約の明示」です。静的解析ツール(実行前に型の不整合を検出する PHPStan など)と組み合わせると、さらに早期に不整合を見つけやすくなります。

型宣言の基本

パラメータと戻り値

function formatUserId(int $id): string
{
    return sprintf('USER-%04d', $id);
}

echo formatUserId(42); // USER-0042

スカラー型は bool / int / float / string を使います。booleaninteger はエイリアスとしては扱えず、クラス名として解釈される点に注意してください(公式マニュアルでも警告されています)。

// 避けたい例: boolean は bool のエイリアスではない
function bad(boolean $flag): void
{
}

プロパティの型(PHP 7.4 以降)

class User
{
    public int $id;
    public string $name;

    public function __construct(int $id, string $name)
    {
        $this->id = $id;
        $this->name = $name;
    }
}

未初期化の型付きプロパティを読むとエラーになります。コンストラクタやプロパティのデフォルト値で初期化する運用が一般的です。

Null 許容型の書き方

「値がない」ことを表す null を受け取りたい場合は、型側で明示します。

?T(PHP 7.1 以降)

function findName(?int $id): ?string
{
    if ($id === null) {
        return null;
    }

    return 'user-' . $id;
}

var_dump(findName(1));    // string(6) "user-1"(var_dump は実行結果の確認用)
var_dump(findName(null)); // NULL

?string は「string または null」を意味します。

T|null(PHP 8.0 以降のユニオン)

function label(string|null $value): string
{
    return $value ?? '(empty)';
}

公式マニュアルでは、?TT|null は同じ意味だと説明されています。単一型なら ?T、複数型と null を並べるなら string|int|null のようにユニオンで書く、という使い分けが読みやすいです。

デフォルト値だけに null を置く古い書き方

// Nullable を型で明示せず、デフォルト値の null だけで null を受け入れる古い書き方
function f(User $user = null): void
{
}

User $user = null のようにデフォルトだけに null を付けると、?User と書かなくても null を渡せます。ただし子クラスでデフォルトを変えると互換性が壊れやすく、公式でもこのパターンはおすすめされていません。PHP 8.4 以降は推奨されない振る舞いです。新しく書くなら ?User $user = null のように型で Null 許容を明示します。

strict_types とは何か

デフォルト(coercive モード)では、PHP は可能な範囲でスカラー型を期待の型へ変換します。たとえば int 引数に "10" を渡すと整数 10 として扱われることがあります。

// declare(strict_types=1) なし
function add(int $a, int $b): int
{
    return $a + $b;
}

var_dump(add(1, 2));     // int(3)
var_dump(add('1', '2')); // int(3)  ※coercive では変換され得る

ファイル先頭で次を宣言すると、そのファイル内からの呼び出しは厳密になります。

<?php
declare(strict_types=1);

function add(int $a, int $b): int
{
    return $a + $b;
}

var_dump(add(1, 2));     // int(3)
// var_dump(add('1', '2')); // TypeError: Argument #1 ($a) must be of type int, string given

厳密モードでも、intfloat 引数へ渡すことだけは例外として許可されます(公式マニュアル)。

適用範囲(ここが落とし穴)

公式の注意点を、平易に言い換えると次のとおりです。

  1. 厳密になるのは、strict_types を付けたファイルから呼び出したとき
  2. 定義側ファイルに宣言があっても、呼び出し側が coercive なら変換され得る
  3. 厳密な型付けの対象は、主にスカラー型の宣言
  4. 内部関数(PHP 組み込み)の中から行われる関数呼び出しは、strict_types の影響を受けない

4 点目は、コールバックを内部関数へ渡す場面などで効いてきます。実務ではまず 1〜2 の「呼び出し側ファイル」のルールを押さえ、コールバック周りで想定と違う変換が起きたら公式の警告を確認するとよいです。

つまり、定義側に declare(strict_types=1) があっても、呼び出し側ファイルに宣言がなければ coercive のまま変換されることがあります。

// lib.php
<?php
declare(strict_types=1);

function double(int $n): int
{
    return $n * 2;
}
// caller.php(strict_types なし)
<?php
require __DIR__ . '/lib.php';

// 呼び出し側が coercive のため、文字列が int に変換され得る
var_dump(double('5')); // int(10)
// strict_caller.php
<?php
declare(strict_types=1);

require __DIR__ . '/lib.php';

// var_dump(double('5')); // TypeError
var_dump(double(5)); // int(10)

「ライブラリ側で strict にしたから全体が厳密になる」わけではない、と覚えておくとトラブルを減らせます。新規 PHP ファイルではファイル先頭に declare(strict_types=1); を置くルールにしておくチームが多いです。

比較:coercive と strict

観点coercive(デフォルト)strict_types=1
"10"int 引数変換されることがあるTypeError
1float 引数通る通る(例外的に許可)
適用の単位呼び出し側ファイル呼び出し側ファイル
目的のイメージ緩い相互運用意図しない型混入を早期検出

戻り値にも同様のルールが効きます。strict なファイルから呼び出したとき、宣言と違う型を返すと TypeError になります。

<?php
declare(strict_types=1);

function ratio(int $a, int $b): int
{
    return $a / $b; // 割り算の結果は float になり得る
}

// ratio(1, 2); // TypeError: Return value must be of type int, float returned

注意点と実務での落とし穴

1. null を許可していない型に null を渡す

function greet(string $name): string
{
    return 'Hello, ' . $name;
}

// greet(null); // TypeError

DB や外部 API から来る「未設定」は null になりやすいです。受け取る側で ?string にするか、呼び出し前にデフォルト文字列へ正規化します。

2. 空文字・"0"false の変換に頼る

coercive モードでは、スカラー変換の結果が意図とずれることがあります。

function isEnabled(bool $flag): bool
{
    return $flag;
}

// declare(strict_types=1) なし(coercive)
var_dump(isEnabled(0));   // bool(false) になり得る
var_dump(isEnabled(''));  // bool(false) になり得る
var_dump(isEnabled('0')); // bool(false) になり得る

ビジネス上「空」と「0」を区別したいなら、bool 引数へ渡す前に明示的に判定します。暗黙変換に頼らないと、意図が残りやすいです。

3. エイリアス名の typo

boolean / integerbool / int ではありません。IDE や静的解析で拾えることも多いですが、レビュー観点にも入れておくと安全です。

4. ユニオンと ? の混在で読みにくくなる

?int|string のような書き方は意図が伝わりにくいです。PHP 8 では int|string|null と並べる方が明確です(構文として許可される組み合わせはバージョンと文脈に依存するため、公式の複合型の注意を確認してください)。

失敗例:呼び出し側だけ strict を忘れ、本番で型がゆるい

次のような状況が実務でありがちです。

// UserService.php
<?php
declare(strict_types=1);

final class UserService
{
    public function activate(int $userId): void
    {
        // ...
    }
}
// SomeController.php(strict_types なし)
<?php

$service = new UserService();
// リクエスト由来の文字列 ID がそのまま渡る
$service->activate($_GET['id'] ?? '0');

開発中は数値リテラルで試し、本番入力はクエリ文字列になることがあります。その結果、coercive では通ってしまう場合があります。逆に、呼び出し側へ途中から strict_types を足すと、その瞬間に TypeError が増える差も出ます。

改善の方向:

  1. 新規ファイルには declare(strict_types=1); を置く
  2. 境界(HTTP 入力、DB、外部 API)で型を正規化してからドメイン層へ渡す
  3. 可能なら PHPStan(静的解析)などで intstring の混在を検出する
<?php
declare(strict_types=1);

$id = filter_var($_GET['id'] ?? null, FILTER_VALIDATE_INT);
if ($id === false) {
    throw new InvalidArgumentException('id が不正です。');
}

$service->activate($id);

学び:型宣言は「契約」、strict は「呼び出し側の方針」

実務では次のように判断すると迷いにくいです。

  • 新規ファイルには declare(strict_types=1); を付ける
  • レガシーの呼び出し側が coercive のままなら、ドメインへ渡す直前で型を正規化する
  • チームでは「全ファイル一括で strict にするか」「新規のみか」を先に決める

型宣言はメソッドの契約、strict_types は呼び出し側の方針、という役割分担で捉えると整理しやすいです。

まとめ

  • 型宣言はパラメータ・戻り値・プロパティなどに付けられ、不一致時は TypeError になる
  • Null を受け取るなら ?T または T|null で明示する(デフォルト値だけに頼らない)
  • declare(strict_types=1) は呼び出し側ファイルに効く。定義側だけ付けても全体は厳密にならない
  • 入力境界で型を正規化し、boolean などの誤った型名を避ける

次に試せること

  1. 自分のプロジェクトで、新規作成した PHP ファイルの先頭に declare(strict_types=1); があるか確認する
  2. null が来うるメソッドを 1 つ選び、戻り値または引数を ?T / T|null に直す
  3. HTTP やフォーム由来の値を、ドメインメソッドに渡す直前で int / string へ正規化する処理を追加する
  4. PHP 公式マニュアルの型宣言 で、利用中バージョンの変更履歴(ユニオン型など)を眺める

Source

PHPPHP

Posted by 千原 耕司