1. ホーム
  2. PHP

【PHP】declare(strict_types=1) と型宣言の使い方|型の自動変換と TypeError の仕組みを解説

Share

PHP では、関数の引数や戻り値、クラスのプロパティに「この値は int です」「これは string です」といった型を書いておくことができます。これを型宣言と呼び、ファイルの先頭に declare(strict_types=1); を書くと、その型のチェックがさらに厳密になります。この記事では、型宣言の基本的な書き方から、strict_types があるときとないときの挙動の違い、型が合わないときに発生する TypeError、?int や int|string、void / mixed / never といった型の意味、そして見落としやすい「strict_types は呼び出し側のファイルで効く」というルールまでを、動かせるコードつきで解説します。

型宣言で「受け取る値・返す値」を決めておく

PHP はもともと、変数にどんな種類の値でも入れられる言語です。便利な反面、数値を受け取るつもりの関数に配列や null が渡ってきても、その場ではエラーにならず、ずっと後になってから原因の分かりにくい不具合として表に出てきます。型宣言は、関数やプロパティに「受け付ける値の種類」を書いておき、違う種類の値が来たらその場でエラーにしてもらう仕組みです。

型は、引数の変数名の前、戻り値は引数リストの閉じかっこのあとに : 型 の形、プロパティは変数名の前に書きます。

type-declarations.php
<?php
// 引数 $price と $quantity は int、戻り値も int
function calcTotal(int $price, int $quantity): int
{
    return $price * $quantity;
}

class User
{
    // プロパティの型宣言(PHP 7.4 以降)
    public string $name;
    public int $age;

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

echo calcTotal(300, 2); // 600

$user = new User('Taro', 20);
echo $user->name; // Taro

型を書いておくと、コードを読む人にも「この関数には数値を渡せばよい」「この関数は数値を返す」ということが一目で伝わります。エディタの補完や静的解析ツールも型宣言を手がかりにするので、書いておいて損はありません。

ただし、型宣言を書いただけの状態では、PHP はある程度「気を利かせて」値を変換してしまいます。ここが strict_types の話につながるポイントです。

strict_types がないときは型が自動で変換される

declare(strict_types=1); を書いていないファイルは「弱い型付けモード(coercive モード)」で動きます。このモードでは、宣言された型と違う値が渡されても、変換できるものは PHP が自動的にその型へ変換してから関数に渡します。

coercive.php
<?php
// declare(strict_types=1); を書いていない

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

var_dump(add('5', '10')); // int(15)  文字列 "5" "10" が int に変換される
var_dump(add(true, 2));   // int(3)   true が 1 に変換される

// 数値として解釈できない文字列は変換できず TypeError
add('abc', 2);
// Fatal error: Uncaught TypeError: add(): Argument #1 ($a) must be of type int, string given

'5' のような数字だけの文字列や true は、エラーにならずに int に変換されて計算されています。フォームから送られてきた値(中身はすべて文字列です)をそのまま渡しても動くので手軽ですが、裏を返せば「本当は文字列を渡してしまっている」というミスに気づけないということでもあります。

小数を int の引数に渡した場合も、このモードでは整数に変換されます。ただし 1.5 のように小数部分が切り捨てられて値が変わってしまうケースは、PHP 8.1 から「Implicit conversion from float 1.5 to int loses precision」という非推奨(Deprecated)の警告が出るようになりました。戻り値も同じで、: int の関数が 1.5 を返すと警告付きで 1 に変換されます。

declare(strict_types=1) で型を厳密にチェックする

ファイルの先頭に declare(strict_types=1); を書くと、そのファイルは「厳密な型付けモード(strict モード)」になります。このモードでは自動変換が行われず、宣言された型とちがう値を渡した時点で TypeError が発生します。

strict.php
<?php
declare(strict_types=1);

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

var_dump(add(5, 10)); // int(15)

add('5', '10');
// Fatal error: Uncaught TypeError: add(): Argument #1 ($a) must be of type int, string given

同じ add('5', '10') でも、先ほどは 15 が返ってきたのに対し、今度はエラーで止まりました。「文字列の '5' を渡している」というミスを、実行したその場で教えてくれるわけです。

strict モードでチェックされるのは、自分で定義した関数だけではありません。strlen() などの PHP 組み込み関数を呼ぶときも同じように厳密になり、strlen(123) は「strlen(): Argument #1 ($string) must be of type string, int given」という TypeError になります。また、戻り値の型やプロパティへの代入もチェックの対象です。

strict-return-property.php
<?php
declare(strict_types=1);

function getCount(): int
{
    return '1'; // 文字列を返している
}

getCount();
// TypeError: getCount(): Return value must be of type int, string returned

class User
{
    public int $age = 0;
}

$user = new User();
$user->age = '20';
// TypeError: Cannot assign string to property User::$age of type int

int から float だけは strict モードでも許される

strict モードにはひとつだけ例外があり、float を宣言した引数や戻り値に int を渡すことは許されています。整数は小数として表しても値が変わらないためです。逆向き(int の引数に float を渡す)は TypeError になります。

int-to-float.php
<?php
declare(strict_types=1);

function circleArea(float $radius): float
{
    return $radius * $radius * 3.14;
}

var_dump(circleArea(2)); // float(12.56)  int の 2 を渡しても OK

2つのモードの違いを表にまとめると次のようになります。

呼び出しstrict_types なしstrict_types=1
add('5', '10')(int の引数に数字の文字列)15 に変換して実行TypeError
add(true, 2)(int の引数に bool)3 に変換して実行TypeError
add('abc', 2)(数値にできない文字列)TypeErrorTypeError
circleArea(2)(float の引数に int)実行できる実行できる
strlen(123)(組み込み関数)3 を返すTypeError

TypeError を try / catch で受け止める

TypeError は PHP 7 で追加された Error クラスの子クラスで、例外と同じように try / catch で受け止められます。$e->getMessage() で、どの引数にどんな型の値が渡されたのかを確認できます。

catch-type-error.php
<?php
declare(strict_types=1);

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

try {
    echo add('5', 10);
} catch (TypeError $e) {
    echo '型が正しくありません: ' . $e->getMessage();
}

とはいえ、TypeError は基本的に「コードの書き方が間違っている」ことを知らせるためのエラーです。catch で握りつぶすよりも、呼び出し側を直すのが本来の対処になります。フォームの入力値のように文字列で届く値は、(int) $_POST['age'] のように自分で明示的に変換してから関数に渡すようにすると、strict モードでも問題なく扱えます。

null を許す・複数の型を許す・特別な型

型宣言には int や string のような基本の型だけでなく、「null も受け付ける」「複数の型のどれか」といった書き方や、戻り値専用の特別な型も用意されています。まずは一覧で確認しておきましょう。

書き方意味使えるバージョン
?intint または null(nullable 型)PHP 7.1 以降
int|stringint または string(union 型)PHP 8.0 以降
void値を返さない(戻り値専用)PHP 7.1 以降
mixedどんな型の値でもよいPHP 8.0 以降
never呼び出し元に戻ってこない(戻り値専用)PHP 8.1 以降

?int:null も受け付ける nullable 型

型名の前に ? を付けると、その型に加えて null も許されるようになります。「見つからなかったときは null を返す」検索系の関数などでよく使います。

nullable.php
<?php
declare(strict_types=1);

// 戻り値は string または null
function findUserName(int $id): ?string
{
    $users = [1 => 'Taro', 2 => 'Hanako'];

    return $users[$id] ?? null;
}

var_dump(findUserName(1)); // string(4) "Taro"
var_dump(findUserName(9)); // NULL

// 引数にも使える。null を渡すことも省略することもできる
function greet(?string $name = null): string
{
    return 'こんにちは、' . ($name ?? 'ゲスト') . 'さん';
}

echo greet();       // こんにちは、ゲストさん
echo greet('Taro'); // こんにちは、Taroさん

?string は、次に紹介する union 型を使って string|null と書いても同じ意味になります。

int|string:複数の型を許す union 型

型を | でつなぐと、「どれかの型であればよい」という意味になります。ID を数値でも文字列でも受け付けたい、といった場面で便利です。

union.php
<?php
declare(strict_types=1);

function formatId(int|string $id): string
{
    return 'ID: ' . $id;
}

echo formatId(42);      // ID: 42
echo formatId('A-001'); // ID: A-001

formatId(1.5);
// TypeError: formatId(): Argument #1 ($id) must be of type string|int, float given

void:値を返さない関数

画面に出力するだけ、ログを書き込むだけといった、値を返さない関数の戻り値には void を指定します。void の関数の中では return; で処理を抜けることはできますが、return null; も含めて値を返すことはできません。書くとファイルを読み込んだ時点で「A void function must not return a value」という Fatal error になります。

void.php
<?php
declare(strict_types=1);

function writeLog(string $message): void
{
    if ($message === '') {
        return; // 値なしの return は OK
    }

    echo '[LOG] ' . $message . PHP_EOL;
}

writeLog('保存しました'); // [LOG] 保存しました

mixed:どんな値でも受け付ける

mixed は null を含むあらゆる型を受け付ける型です。型を書かないのと動きは似ていますが、「書き忘れ」ではなく「どんな値でも受け付ける設計である」ことをはっきり示せます。デバッグ用の出力関数のように、本当に何でも受け取る必要がある場面に限って使うとよいでしょう。

mixed.php
<?php
declare(strict_types=1);

function debug(mixed $value): void
{
    echo '<pre>';
    var_dump($value);
    echo '</pre>';
}

debug(123);
debug(['a', 'b']);
debug(null);

never:呼び出し元に戻ってこない関数

never は、必ず例外を投げる、または exit で処理を終了するなど、呼び出し元に処理が戻ってこない関数の戻り値に指定します。void が「戻ってくるけれど値はない」なのに対し、never は「そもそも戻ってこない」という意味です。never の関数に return を書くと「A never-returning function must not return」という Fatal error になります。

never.php
<?php
declare(strict_types=1);

// 必ず例外を投げるので、呼び出し元には戻らない
function fail(string $message): never
{
    throw new RuntimeException($message);
}

// リダイレクトして処理を終了する
function redirect(string $url): never
{
    header('Location: ' . $url);
    exit;
}

strict_types は「呼び出す側」のファイルで効く

strict_types でいちばん誤解されやすいのが、効く範囲です。declare(strict_types=1); は書いたファイルの中だけで有効で、引数のチェックが厳密になるかどうかは、関数を定義したファイルではなく、関数を呼び出しているファイルの設定で決まります。

たとえば、関数を定義したファイルにだけ strict_types を書いた場合を見てみます。

lib.php
<?php
declare(strict_types=1); // 定義側には書いてある

function add(int $a, int $b): int
{
    return $a + $b;
}
index.php
<?php
// 呼び出し側には strict_types を書いていない
require __DIR__ . '/lib.php';

var_dump(add('5', '10')); // int(15)  エラーにならず変換される

lib.php に strict_types を書いていても、add() を呼んでいる index.php が弱い型付けモードなので、'5' はそのまま int に変換されてしまいます。反対に、lib.php 側に書かず index.php 側にだけ declare(strict_types=1); を書くと、add('5', '10') は TypeError になります。

一方、戻り値のチェックは関数を定義したファイルの設定で決まります。: int の関数が '1' を返したとき、定義側のファイルが弱い型付けモードなら、呼び出し側が strict モードでも 1 に変換されて返ってきます。「引数は呼び出し側、戻り値は定義側」と覚えておくと整理しやすいでしょう。

このルールがあるため、プロジェクトで型を厳密にしたいなら、一部のファイルだけでなくすべての PHP ファイルの先頭に declare(strict_types=1); を書くのが一般的です。

strict_types を書いたのに期待どおりに動かないとき

「must be the very first statement」という Fatal error が出る

declare(strict_types=1); は、ファイルの最初の文として書く必要があります。<?php の直後であればコメントは前に置けますが、echo や require、namespace などの文より後ろに書くと「strict_types declaration must be the very first statement in the script」という Fatal error になります。namespace を使うファイルでも、declare を先、namespace を後に書きます。

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

namespace App;

class Order
{
    public function __construct(
        public int $id,
        public float $amount,
    ) {
    }
}

文字列を渡しているのに TypeError にならない

関数を定義しているファイルに strict_types を書いたのにエラーにならない場合は、前の章で説明したとおり、呼び出している側のファイルに declare(strict_types=1); がないことが原因です。テンプレートファイルなど、関数を呼び出しているファイルの先頭を確認してみてください。

デフォルト値 null の引数で Deprecated が出る

古いコードでは function greet(string $name = null) のように、型は string のままデフォルト値に null を書いて「null も許す」ことにしている書き方がよく見られます。これは型を見ただけでは null を受け付けることが分からないため、PHP 8.4 からは「Implicitly marking parameter $name as nullable is deprecated」という非推奨の警告が出るようになりました。?string $name = null または string|null $name = null のように、null を許すことを型で明示すれば警告は消えます。

まとめ

型宣言は、引数・戻り値・プロパティに受け付ける値の種類を書いておく仕組みです。declare(strict_types=1); がないファイルでは '5' や true のような値が自動で変換されますが、書いたファイルでは変換が行われず、型が合わなければ TypeError が発生します(int を float として受け取ることだけは例外的に許されます)。?int で null を、int|string で複数の型を許すことができ、戻り値には値を返さない void、戻ってこない never、何でも受け付ける mixed も使えます。strict_types は書いたファイルにしか効かず、引数のチェックは呼び出し側、戻り値のチェックは定義側のファイルの設定で決まるので、厳密にしたい場合はすべてのファイルの先頭に書いておきましょう。

参考ページ