かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

PHP 型チェックを厳密にしたい

前回 PHP の型宣言が暗黙の型変換をしてしまってそんなものかーと思っていたのですが、厳密にするオプションが有ることを教えてもらったので記録に残しておきます。

環境

declare(strict_types=1); 宣言を行うと厳密な方チェックができる

declare(strict_types=1); 宣言なし (default)

<?php
function myFunc(int $x = null): void {
  $foo = $x ?? 'default';
  var_export($foo);
}
myFunc(10); // => 10
myFunc('1'); // => 1
myFunc(null); // => 'default'
myFunc(true); // => 1
myFunc(false); // => 0
myFunc('-2.5'); // => -2
myFunc('');
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, string given
myFunc([]);
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, array given
class Bar { public $x = 1; };
$bar = new Bar();
myFunc($bar);
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, Bar given

文字列の数字や float 型、Bool値など暗黙の型変換ができる値がすり抜けてしまう

declare(strict_types=1); 宣言あり

<?php declare(strict_types=1);
function myFunc(int $x = null): void {
  $foo = $x ?? 'default';
  var_export($foo);
}
myFunc(10); // => 10
myFunc('1');
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, string given 
myFunc(null); // => 'default'
myFunc(true);
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, bool given
myFunc(false);
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, bool given
myFunc('-2.5');
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, float given
myFunc('');
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, string given
myFunc([]);
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, array given
class Bar { public $x = 1; };
$bar = new Bar();
myFunc($bar);
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, Bar given

null は通りますが、int 型以外の引数はエラーになりました!
型宣言を使う場合は基本的に declare(strict_types=1); 宣言を行うのが良さそうです

declare(strict_types=1); の影響範囲について

1. declare(strict_types=1); 宣言はファイルの先頭でなければならない

<?php
function myFunc(int $x = null): void {
  $foo = $x ?? 'default';
  var_export($foo);
  echo "\n";
}
declare(strict_types=1);
// Fatal error: strict_types declaration must be the very first statement in the script

2. declare(strict_types=1); は宣言されたファイル内でのみ有効

  1. strict モードは宣言されたファイル内でのみ有効になる
  2. strict モード宣言のあるファイルでも、呼び出し側のファイルが strict モードで無ければ厳密な型チェックは行われない
    => 呼び出し元の指定が優先される
呼び出し元が strict モードでない場合、読み込んだファイルが strict モードでも厳密な型チェックは行われない

function.php

<?php declare(strict_types=1);
function add(int $x, int $y): int {
    return $x + $y;
}

index.php

<?php
include 'functions.php';
echo add(1.5, 2.5);
// => 3
呼び出し元が strict モードなら、読み込んだファイルが strict モードでなくとも厳密な型チェックが行われる

function.php

<?php
function add(int $x, int $y): int {
    return $x + $y;
}

index.php

<?php declare(strict_types=1);
include 'functions.php';
echo add(1.5, 2.5);
// Fatal error: Uncaught TypeError: add(): Argument #1 ($x) must be of type int, float given

PHP は include / require をしているファイルにコードが挿入されて strict モードの指定は大本のファイルのでの指定が残ると捉えると理解しやすそう

Note that the include construct loads the code from another file into a file. And you’ll learn more about it in the later tutorial.
When you call a function defined in a file with strict typing (functions.php) from a file without strict typing (index.php), PHP will respect the preference of the caller (index.php). That means it’s up to the caller to decide whether to use the strict mode or not. In this case, the index.php won’t execute in the strict mode.
When you include code from another file, PHP uses the mode of the caller.
cf. PHP strict_types

Note. strict モードのファイルが中間にある場合

strict モードの指定は呼び出し元の指定が優先されるとあり、Qiitaの記事 に多重でファイルを読み込んだ場合、関数の実行ファイルに declare(strict_types=1); があれば呼び出し元に strict モード宣言がなくともエラーになったとあったので複数ファイルが作成できる paiza.io の PHP v8.1.9 で試してみました

b.php

<?php
function add(int $x, int $y): int {
    return $x + $y;
}

a.php (strict モード)

<?php declare(strict_types=1);
include_once('b.php');

function add_square(int $x, int $y): int {
  $sum = add($x, $y);
  return pow($sum, 2);
}

Main.php

<?php 
include_once('a.php');
echo phpversion();
// => 8.1.9

echo add_square(2, 3);
// => 25
echo add_square(2.0, 3.0);
// => 25
echo add_square(true, false);
// => 1

👇実行結果サンプル

Qiita の記事の例だと strict 宣言のある a.php からの呼び出しで add_square(2.0, 3.0) がエラーになると思ったのですが、そのまま実行されました。PHP のバージョンで挙動が異なるのでしょうか?

今回は詳細には調べませんが、phptutorial.net に書かれているように最終的な呼び出し元 今回は Main.php の指定が優先される。という認識で良いのかな〜と思いました。

まとめ

PHP でプロジェクトを作成するときはファイルの先頭に declare(strict_types=1); 宣言を書いて厳密な型チェックを行う方が幸せになれそう!
厳密な型チェックの存在を教えてくれた いち(@ichiart4) さんありがとう〜♡♡♡ (アイカツ!のフレンズ優秀な方が多すぎてありがたい〜)

おわり ₍ ᐢ. ̫ .ᐢ ₎


[参考]

アイカツ!を見るとプログラミングも描けるようになる!!!!