HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

fopenに存在しないファイル名を指定したときの挙動

PHP 5.4.16からPHP 8.1.7にバージョンアップをしようとしたときのこと。

<input type="file" name="file" ~に値が指定されていないときに

fopen($_FILES['file']['tmp_name'], 'r');

したとき、例外が発生するようになってしまったので調査。同じPHP 5.x系ということで、php56を使い、php56とphp81での比較とする。

そもそものコードは以下のような感じ。

Main1.php

<?php

$filename = $argv[1];

$fp = fopen($filename, "r");
if (!$fp) {
    print "{$filename}: Cannot open file.\n";
    exit(1);
}

print "{$filename}: OK\n";

// ・・・

fclose($fp);

php56での実行結果。

$ php56 Main1.php Main1.php
Main1.php: OK
$ php56 Main1.php a.txt
PHP Warning:  fopen(a.txt): failed to open stream: No such file or directory in /home/hhelibex/blog/2022-0705-01/Main1.php on line 5
a.txt: Cannot open file.
$ php56 Main1.php ""
PHP Warning:  fopen(): Filename cannot be empty in /home/hhelibex/blog/2022-0705-01/Main1.php on line 5
: Cannot open file.
$ 

php81での実行結果。

$ php81 Main1.php Main1.php
Main1.php: OK
$ php81 Main1.php a.txt
PHP Warning:  fopen(a.txt): Failed to open stream: No such file or directory in /home/hhelibex/blog/2022-0705-01/Main1.php on line 5
a.txt: Cannot open file.
$ php81 Main1.php ""
PHP Fatal error:  Uncaught ValueError: Path cannot be empty in /home/hhelibex/blog/2022-0705-01/Main1.php:5
Stack trace:
#0 /home/hhelibex/blog/2022-0705-01/Main1.php(5): fopen()
#1 {main}
  thrown in /home/hhelibex/blog/2022-0705-01/Main1.php on line 5
$ 

ファイル名が空でなく存在しないファイルの場合の挙動はWarningのまま変わらないが、ファイル名が空文字列の場合に、PHP 8.1ではValueErrorが発生するようになってしまった。

対症療法をするなら、以下のような感じだろうか。

Main2.php

<?php

$filename = $argv[1];

if (empty($filename)) {
    print "Filename cannot be empty.\n";
    exit(1);
}
$fp = fopen($filename, "r");
if (!$fp) {
    print "{$filename}: Cannot open file.\n";
    exit(1);
}

print "{$filename}: OK\n";

// ・・・

fclose($fp);

しかし、ファイルが存在しない場合のWarningが気持ち悪い。

これは、C言語でプログラムを書いていた頃の癖でfopen()の実行結果しかチェックしないのが問題なのだろう。

ということで、PHPでの最適解は以下になるのではないか。

Main3.php

<?php

$filename = $argv[1];

if (!file_exists($filename)
|| !($fp = fopen($filename, "r"))) {
    print "{$filename}: Cannot open file.\n";
    exit(1);
}

print "{$filename}: OK\n";

// ・・・

fclose($fp);

php81での実行結果。

$ php81 Main3.php Main3.php
Main3.php: OK
$ php81 Main3.php a.txt
a.txt: Cannot open file.
$ php81 Main3.php ""
: Cannot open file.
$ 

厳密には、file_exists()とfopen()の間にファイルが存在しなくなった場合のエラーを考えなければならないが、それはイレギュラーケースとしてログに記録されてもいいのではないだろうか。

型宣言~float~

型宣言のfloat編。どこまで許容されるのか検証。

Main.php

<?php

function to_float($val):float {
    print "=== val=" . var_export($val, true) . " ===" . PHP_EOL;
    try {
        return $val;
    } catch (TypeError $e) {
        print $e->getMessage() . PHP_EOL;
        print $e->getTraceAsString() . PHP_EOL;
        return -1;
    }
}

class Hoge {
    private int $val;
    public function __construct($val) {
        $this->val = $val;
    }
}
class MyInteger {
    private int $val;
    public function __construct($val) {
        $this->val = $val;
    }
    public function __toString():string {
        return $this->val;
    }
}

var_dump(to_float(0));
var_dump(to_float(00));
var_dump(to_float(123));
var_dump(to_float(0123));
var_dump(to_float(0x123));
var_dump(to_float(0.1));
var_dump(to_float(1.23));
var_dump(to_float(false));
var_dump(to_float(true));
var_dump(to_float("0"));
var_dump(to_float("00"));
var_dump(to_float("123"));
var_dump(to_float("0123"));
var_dump(to_float("0x123"));
var_dump(to_float("0.1"));
var_dump(to_float("1.23"));
var_dump(to_float("hello"));
var_dump(to_float(null));
var_dump(to_float(new Hoge("123456789")));
var_dump(to_float(new MyInteger("123456789")));

実行結果。

$ php81 Main.php
=== val=0 ===
float(0)
=== val=0 ===
float(0)
=== val=123 ===
float(123)
=== val=83 ===
float(83)
=== val=291 ===
float(291)
=== val=0.1 ===
float(0.1)
=== val=1.23 ===
float(1.23)
=== val=false ===
float(0)
=== val=true ===
float(1)
=== val='0' ===
float(0)
=== val='00' ===
float(0)
=== val='123' ===
float(123)
=== val='0123' ===
float(123)
=== val='0x123' ===
to_float(): Return value must be of type float, string returned
#0 /home/hhelibex/blog/2022-0704-01/Main.php(43): to_float()
#1 {main}
float(-1)
=== val='0.1' ===
float(0.1)
=== val='1.23' ===
float(1.23)
=== val='hello' ===
to_float(): Return value must be of type float, string returned
#0 /home/hhelibex/blog/2022-0704-01/Main.php(46): to_float()
#1 {main}
float(-1)
=== val=NULL ===
to_float(): Return value must be of type float, null returned
#0 /home/hhelibex/blog/2022-0704-01/Main.php(47): to_float()
#1 {main}
float(-1)
=== val=Hoge::__set_state(array(
   'val' => 123456789,
)) ===
to_float(): Return value must be of type float, Hoge returned
#0 /home/hhelibex/blog/2022-0704-01/Main.php(48): to_float()
#1 {main}
float(-1)
=== val=MyInteger::__set_state(array(
   'val' => 123456789,
)) ===
to_float(): Return value must be of type float, MyInteger returned
#0 /home/hhelibex/blog/2022-0704-01/Main.php(49): to_float()
#1 {main}
float(-1)
$ 

まぁ、floatへのキャストと挙動は同じですな。取り立てて面白いところがない。

型宣言~bool~

型宣言のbool編。どこまで許容されるのか検証。

Main.php

<?php

function to_bool($val):bool {
    print "=== val=" . var_export($val, true) . " ===" . PHP_EOL;
    try {
        return $val;
    } catch (TypeError $e) {
        print $e->getMessage() . PHP_EOL;
        print $e->getTraceAsString() . PHP_EOL;
        return false;
    }
}

class Hoge {
    private int $val;
    public function __construct($val) {
        $this->val = $val;
    }
}
class MyInteger {
    private int $val;
    public function __construct($val) {
        $this->val = $val;
    }
    public function __toString():string {
        return $this->val;
    }
}

var_dump(to_bool(0));
var_dump(to_bool(00));
var_dump(to_bool(123));
var_dump(to_bool(0123));
var_dump(to_bool(0x123));
var_dump(to_bool(0.1));
var_dump(to_bool(1.23));
var_dump(to_bool(false));
var_dump(to_bool(true));
var_dump(to_bool("0"));
var_dump(to_bool("00"));
var_dump(to_bool("123"));
var_dump(to_bool("0123"));
var_dump(to_bool("0x123"));
var_dump(to_bool("hello"));
var_dump(to_bool("false"));
var_dump(to_bool("true"));
var_dump(to_bool(null));
var_dump(to_bool(new Hoge("123456789")));
var_dump(to_bool(new MyInteger("123456789")));

実行結果。

$ php81 Main.php
=== val=0 ===
bool(false)
=== val=0 ===
bool(false)
=== val=123 ===
bool(true)
=== val=83 ===
bool(true)
=== val=291 ===
bool(true)
=== val=0.1 ===
bool(true)
=== val=1.23 ===
bool(true)
=== val=false ===
bool(false)
=== val=true ===
bool(true)
=== val='0' ===
bool(false)
=== val='00' ===
bool(true)
=== val='123' ===
bool(true)
=== val='0123' ===
bool(true)
=== val='0x123' ===
bool(true)
=== val='hello' ===
bool(true)
=== val='false' ===
bool(true)
=== val='true' ===
bool(true)
=== val=NULL ===
to_bool(): Return value must be of type bool, null returned
#0 /home/hhelibex/blog/2022-0703-01/Main.php(47): to_bool()
#1 {main}
bool(false)
=== val=Hoge::__set_state(array(
   'val' => 123456789,
)) ===
to_bool(): Return value must be of type bool, Hoge returned
#0 /home/hhelibex/blog/2022-0703-01/Main.php(48): to_bool()
#1 {main}
bool(false)
=== val=MyInteger::__set_state(array(
   'val' => 123456789,
)) ===
to_bool(): Return value must be of type bool, MyInteger returned
#0 /home/hhelibex/blog/2022-0703-01/Main.php(49): to_bool()
#1 {main}
bool(false)
$ 

当然ながら、文字列 "false"、"true" はいずれも true となる。

ここで注意すべきなのは、あくまでも「:bool」であり「:boolean」とは書けないということ。 マニュアルに以下の記述がある。

警告 上記のスカラー型のエイリアスはサポートされていません。 つまり、これらはクラスやインターフェイスの名前として扱われているということです。 たとえば、型の宣言に boolean を使った場合、 値が boolean クラスまたはインターフェイスインスタンスであることが要求されます。 bool 型ではありません。

もし「:boolean」と書いた場合、実行結果は以下のようになる。

PHP Warning:  "boolean" will be interpreted as a class name. Did you mean "bool"? Write "\boolean" to suppress this warning in /home/hhelibex/blog/2022-0703-01/Main.php on line 3
=== val=0 ===
to_bool(): Return value must be of type boolean, int returned
#0 /home/hhelibex/blog/2022-0703-01/Main.php(30): to_bool()
#1 {main}
PHP Fatal error:  Uncaught TypeError: to_bool(): Return value must be of type boolean, bool returned in /home/hhelibex/blog/2022-0703-01/Main.php:10
Stack trace:
#0 /home/hhelibex/blog/2022-0703-01/Main.php(30): to_bool()
#1 {main}
  thrown in /home/hhelibex/blog/2022-0703-01/Main.php on line 10

型宣言~string~

型宣言のstring編。どこまで許容されるのか検証。

Main.php

<?php

function to_string($val):string {
    print "=== val=" . var_export($val, true) . " ===" . PHP_EOL;
    try {
        return $val;
    } catch (TypeError $e) {
        print $e->getMessage() . PHP_EOL;
        print $e->getTraceAsString() . PHP_EOL;
        return '';
    }
}

class Hoge {
    private int $val;
    public function __construct($val) {
        $this->val = $val;
    }
}
class MyInteger {
    private int $val;
    public function __construct($val) {
        $this->val = $val;
    }
    public function __toString():string {
        return $this->val;
    }
}

var_dump(to_string(0));
var_dump(to_string(00));
var_dump(to_string(123));
var_dump(to_string(0123));
var_dump(to_string(0x123));
var_dump(to_string(0.1));
var_dump(to_string(1.23));
var_dump(to_string(false));
var_dump(to_string(true));
var_dump(to_string("0"));
var_dump(to_string("00"));
var_dump(to_string("123"));
var_dump(to_string("0123"));
var_dump(to_string("0x123"));
var_dump(to_string("hello"));
var_dump(to_string(null));
var_dump(to_string(new Hoge("123456789")));
var_dump(to_string(new MyInteger("123456789")));

実行結果。

$ php81 Main.php
=== val=0 ===
string(1) "0"
=== val=0 ===
string(1) "0"
=== val=123 ===
string(3) "123"
=== val=83 ===
string(2) "83"
=== val=291 ===
string(3) "291"
=== val=0.1 ===
string(3) "0.1"
=== val=1.23 ===
string(4) "1.23"
=== val=false ===
string(0) ""
=== val=true ===
string(1) "1"
=== val='0' ===
string(1) "0"
=== val='00' ===
string(2) "00"
=== val='123' ===
string(3) "123"
=== val='0123' ===
string(4) "0123"
=== val='0x123' ===
string(5) "0x123"
=== val='hello' ===
string(5) "hello"
=== val=NULL ===
to_string(): Return value must be of type string, null returned
#0 /home/hhelibex/blog/2022-0617-01/Main.php(45): to_string()
#1 {main}
string(0) ""
=== val=Hoge::__set_state(array(
   'val' => 123456789,
)) ===
to_string(): Return value must be of type string, Hoge returned
#0 /home/hhelibex/blog/2022-0617-01/Main.php(46): to_string()
#1 {main}
string(0) ""
=== val=MyInteger::__set_state(array(
   'val' => 123456789,
)) ===
string(9) "123456789"
$ 

予想通り、__toString() を実装しておくと、objectをstringに変換してくれる。

しかし、「false」「true」が「""」「"1"」に変換されるのには未だに慣れない。

型宣言~int~

型宣言のint編。どこまで許容されるのか検証。

Main.php

<?php

function to_int($val):int {
    print "=== val=" . var_export($val, true) . " ===" . PHP_EOL;
    try {
        return $val;
    } catch (TypeError $e) {
        print $e->getMessage() . PHP_EOL;
        print $e->getTraceAsString() . PHP_EOL;
        return -1;
    }
}

class Hoge {
    private int $val;
    public function __construct($val) {
        $this->val = $val;
    }
}
class MyInteger {
    private int $val;
    public function __construct($val) {
        $this->val = $val;
    }
    public function __toString():string {
        return $this->val;
    }
}

var_dump(to_int(0));
var_dump(to_int(00));
var_dump(to_int(123));
var_dump(to_int(0123));
var_dump(to_int(0x123));
var_dump(to_int(0.1));
var_dump(to_int(1.23));
var_dump(to_int(false));
var_dump(to_int(true));
var_dump(to_int("0"));
var_dump(to_int("00"));
var_dump(to_int("123"));
var_dump(to_int("0123"));
var_dump(to_int("0x123"));
var_dump(to_int("hello"));
var_dump(to_int(null));
var_dump(to_int(new Hoge("123456789")));
var_dump(to_int(new MyInteger("123456789")));

実行結果。

$ php81 Main.php
=== val=0 ===
int(0)
=== val=0 ===
int(0)
=== val=123 ===
int(123)
=== val=83 ===
int(83)
=== val=291 ===
int(291)
=== val=0.1 ===
int(0)
=== val=1.23 ===
int(1)
=== val=false ===
int(0)
=== val=true ===
int(1)
=== val='0' ===
int(0)
=== val='00' ===
int(0)
=== val='123' ===
int(123)
=== val='0123' ===
int(123)
=== val='0x123' ===
to_int(): Return value must be of type int, string returned
#0 /home/hhelibex/blog/2022-0616-01/Main.php(43): to_int()
#1 {main}
int(-1)
=== val='hello' ===
to_int(): Return value must be of type int, string returned
#0 /home/hhelibex/blog/2022-0616-01/Main.php(44): to_int()
#1 {main}
int(-1)
=== val=NULL ===
to_int(): Return value must be of type int, null returned
#0 /home/hhelibex/blog/2022-0616-01/Main.php(45): to_int()
#1 {main}
int(-1)
=== val=Hoge::__set_state(array(
   'val' => 123456789,
)) ===
to_int(): Return value must be of type int, Hoge returned
#0 /home/hhelibex/blog/2022-0616-01/Main.php(46): to_int()
#1 {main}
int(-1)
=== val=MyInteger::__set_state(array(
   'val' => 123456789,
)) ===
to_int(): Return value must be of type int, MyInteger returned
#0 /home/hhelibex/blog/2022-0616-01/Main.php(47): to_int()
#1 {main}
int(-1)
$ 

最後のMyIntegerクラスは、ワンチャン __toString() が呼び出されて変換されたりしないかなと思ったりしたけどそんなわけはなかった。

ここで注意すべきなのは、あくまでも「:int」であり「:integer」とは書けないということ。 マニュアルに以下の記述がある。

警告 上記のスカラー型のエイリアスはサポートされていません。 つまり、これらはクラスやインターフェイスの名前として扱われているということです。 たとえば、型の宣言に boolean を使った場合、 値が boolean クラスまたはインターフェイスインスタンスであることが要求されます。 bool 型ではありません。

もし「:integer」と書いた場合、実行結果は以下のようになる。

PHP Warning:  "integer" will be interpreted as a class name. Did you mean "int"? Write "\integer" to suppress this warning in /home/hhelibex/blog/2022-0616-01/Main.php on line 3
=== val=0 ===
to_int(): Return value must be of type integer, int returned
#0 /home/hhelibex/blog/2022-0616-01/Main.php(30): to_int()
#1 {main}
PHP Fatal error:  Uncaught TypeError: to_int(): Return value must be of type integer, int returned in /home/hhelibex/blog/2022-0616-01/Main.php:10
Stack trace:
#0 /home/hhelibex/blog/2022-0616-01/Main.php(30): to_int()
#1 {main}
  thrown in /home/hhelibex/blog/2022-0616-01/Main.php on line 10

type hintingの改善(Type declarations/型宣言)

だいぶ昔に以下の記事を書いた。

hhelibex.hatenablog.jp

それからだいぶ時が経ったものの、PHP 5.xの世界でずっと生きてきたので気付かなかった。

これによると、

英語

Type declarations can be added to function arguments, return values, and, as of PHP 7.4.0, class properties. They ensure that the value is of the specified type at call time, otherwise a TypeError is thrown.

日本語

関数のパラメータや戻り値、 クラスのプロパティ (PHP 7.4.0 以降) に対して型を宣言することができます。 これによって、その値が特定の型であることを保証できます。 その型でない場合は、TypeError がスローされます。

というわけでさわりだけ試してみる。

パラメータ

Main1.php

<?php

function hoge(int $val) {
    var_dump($val);
}

class Hoge {
    public function foo(int $val) {
        var_dump(__FUNCTION__, $val);
    }
    public function bar(string $val) {
        var_dump(__FUNCTION__, $val);
    }
    public function piyo(Hoge $val) {
        var_dump(__FUNCTION__, $val);
    }
    public function __toString() {
        return var_export($this, true);
    }
}

print "===function hoge(int)===" . PHP_EOL;
foreach (array(1, "2", true, "00", "hello", new Hoge()) as $val) {
    print "===    " . var_export($val, true) . "    ===" . PHP_EOL;
    try {
        hoge($val);
    } catch (TypeError $e) {
        print $e->getMessage() . PHP_EOL;
        print $e->getTraceAsString() . PHP_EOL;
    }
}

print "===method foo(int)===" . PHP_EOL;
$hoge = new Hoge();
foreach (array(1, "2", true, "00", "hello", new Hoge()) as $val) {
    print "===    " . var_export($val, true) . "    ===" . PHP_EOL;
    try {
        $hoge->foo($val);
    } catch (TypeError $e) {
        print $e->getMessage() . PHP_EOL;
        print $e->getTraceAsString() . PHP_EOL;
    }
}
print "===method bar(string)===" . PHP_EOL;
$hoge = new Hoge();
foreach (array(1, "2", true, "00", "hello", new Hoge()) as $val) {
    print "===    " . var_export($val, true) . "    ===" . PHP_EOL;
    try {
        $hoge->bar($val);
    } catch (TypeError $e) {
        print $e->getMessage() . PHP_EOL;
        print $e->getTraceAsString() . PHP_EOL;
    }
}
print "===method piyo(Hoge)===" . PHP_EOL;
foreach (array(1, "2", true, "00", "hello", new Hoge()) as $val) {
    print "===    " . var_export($val, true) . "    ===" . PHP_EOL;
    try {
        $hoge->piyo($val);
    } catch (TypeError $e) {
        print $e->getMessage() . PHP_EOL;
        print $e->getTraceAsString() . PHP_EOL;
    }
}

実行結果。長くなるので、代表してPHP 8.1に出てもらう。

$ php81 Main1.php
===function hoge(int)===
===    1    ===
int(1)
===    '2'    ===
int(2)
===    true    ===
int(1)
===    '00'    ===
int(0)
===    'hello'    ===
hoge(): Argument #1 ($val) must be of type int, string given, called in /home/hhelibex/blog/2022-0615-01/Main1.php on line 26
#0 /home/hhelibex/blog/2022-0615-01/Main1.php(26): hoge()
#1 {main}
===    Hoge::__set_state(array(
))    ===
hoge(): Argument #1 ($val) must be of type int, Hoge given, called in /home/hhelibex/blog/2022-0615-01/Main1.php on line 26
#0 /home/hhelibex/blog/2022-0615-01/Main1.php(26): hoge()
#1 {main}
===method foo(int)===
===    1    ===
string(3) "foo"
int(1)
===    '2'    ===
string(3) "foo"
int(2)
===    true    ===
string(3) "foo"
int(1)
===    '00'    ===
string(3) "foo"
int(0)
===    'hello'    ===
Hoge::foo(): Argument #1 ($val) must be of type int, string given, called in /home/hhelibex/blog/2022-0615-01/Main1.php on line 38
#0 /home/hhelibex/blog/2022-0615-01/Main1.php(38): Hoge->foo()
#1 {main}
===    Hoge::__set_state(array(
))    ===
Hoge::foo(): Argument #1 ($val) must be of type int, Hoge given, called in /home/hhelibex/blog/2022-0615-01/Main1.php on line 38
#0 /home/hhelibex/blog/2022-0615-01/Main1.php(38): Hoge->foo()
#1 {main}
===method bar(string)===
===    1    ===
string(3) "bar"
string(1) "1"
===    '2'    ===
string(3) "bar"
string(1) "2"
===    true    ===
string(3) "bar"
string(1) "1"
===    '00'    ===
string(3) "bar"
string(2) "00"
===    'hello'    ===
string(3) "bar"
string(5) "hello"
===    Hoge::__set_state(array(
))    ===
string(3) "bar"
string(27) "Hoge::__set_state(array(
))"
===method piyo(Hoge)===
===    1    ===
Hoge::piyo(): Argument #1 ($val) must be of type Hoge, int given, called in /home/hhelibex/blog/2022-0615-01/Main1.php on line 59
#0 /home/hhelibex/blog/2022-0615-01/Main1.php(59): Hoge->piyo()
#1 {main}
===    '2'    ===
Hoge::piyo(): Argument #1 ($val) must be of type Hoge, string given, called in /home/hhelibex/blog/2022-0615-01/Main1.php on line 59
#0 /home/hhelibex/blog/2022-0615-01/Main1.php(59): Hoge->piyo()
#1 {main}
===    true    ===
Hoge::piyo(): Argument #1 ($val) must be of type Hoge, bool given, called in /home/hhelibex/blog/2022-0615-01/Main1.php on line 59
#0 /home/hhelibex/blog/2022-0615-01/Main1.php(59): Hoge->piyo()
#1 {main}
===    '00'    ===
Hoge::piyo(): Argument #1 ($val) must be of type Hoge, string given, called in /home/hhelibex/blog/2022-0615-01/Main1.php on line 59
#0 /home/hhelibex/blog/2022-0615-01/Main1.php(59): Hoge->piyo()
#1 {main}
===    'hello'    ===
Hoge::piyo(): Argument #1 ($val) must be of type Hoge, string given, called in /home/hhelibex/blog/2022-0615-01/Main1.php on line 59
#0 /home/hhelibex/blog/2022-0615-01/Main1.php(59): Hoge->piyo()
#1 {main}
===    Hoge::__set_state(array(
))    ===
string(4) "piyo"
object(Hoge)#4 (0) {
}
$

ある程度なら(string(1) "2"⇒int(2)/bool(true)⇒int(1)/string(2) "00"⇒int(0)/ ⇒string() "*")キャストして扱ってくれるらしい。 オブジェクトも、__toString()メソッドを実装するとstringにキャストして渡される。

戻り値

Main2.php

<?php

function hoge($val):int {
    return $val;
}

class Hoge {
    public function foo($val):int {
        return $val;
    }
    public function bar($val):string {
        return $val;
    }
    public function piyo($val):Hoge {
        return $val;
    }
    public function __toString() {
        return var_export($this, true);
    }
}

print "===function hoge():int===" . PHP_EOL;
foreach (array(1, "2", true, "00", "hello", new Hoge()) as $val) {
    print "===    " . var_export($val, true) . "    ===" . PHP_EOL;
    try {
        var_dump($val, hoge($val));
    } catch (TypeError $e) {
        print $e->getMessage() . PHP_EOL;
        print $e->getTraceAsString() . PHP_EOL;
    }
}

print "===method foo():int===" . PHP_EOL;
$hoge = new Hoge();
foreach (array(1, "2", true, "00", "hello", new Hoge()) as $val) {
    print "===    " . var_export($val, true) . "    ===" . PHP_EOL;
    try {
        var_dump($val, $hoge->foo($val));
    } catch (TypeError $e) {
        print $e->getMessage() . PHP_EOL;
        print $e->getTraceAsString() . PHP_EOL;
    }
}
print "===method bar():string===" . PHP_EOL;
$hoge = new Hoge();
foreach (array(1, "2", true, "00", "hello", new Hoge()) as $val) {
    print "===    " . var_export($val, true) . "    ===" . PHP_EOL;
    try {
        var_dump($val, $hoge->bar($val));
    } catch (TypeError $e) {
        print $e->getMessage() . PHP_EOL;
        print $e->getTraceAsString() . PHP_EOL;
    }
}
print "===method piyo():Hoge===" . PHP_EOL;
foreach (array(1, "2", true, "00", "hello", new Hoge()) as $val) {
    print "===    " . var_export($val, true) . "    ===" . PHP_EOL;
    try {
        var_dump($val, $hoge->piyo($val));
    } catch (TypeError $e) {
        print $e->getMessage() . PHP_EOL;
        print $e->getTraceAsString() . PHP_EOL;
    }
}

実行結果。

$ php81 Main2.php
===function hoge():int===
===    1    ===
int(1)
int(1)
===    '2'    ===
string(1) "2"
int(2)
===    true    ===
bool(true)
int(1)
===    '00'    ===
string(2) "00"
int(0)
===    'hello'    ===
hoge(): Return value must be of type int, string returned
#0 /home/hhelibex/blog/2022-0615-01/Main2.php(26): hoge()
#1 {main}
===    Hoge::__set_state(array(
))    ===
hoge(): Return value must be of type int, Hoge returned
#0 /home/hhelibex/blog/2022-0615-01/Main2.php(26): hoge()
#1 {main}
===method foo():int===
===    1    ===
int(1)
int(1)
===    '2'    ===
string(1) "2"
int(2)
===    true    ===
bool(true)
int(1)
===    '00'    ===
string(2) "00"
int(0)
===    'hello'    ===
Hoge::foo(): Return value must be of type int, string returned
#0 /home/hhelibex/blog/2022-0615-01/Main2.php(38): Hoge->foo()
#1 {main}
===    Hoge::__set_state(array(
))    ===
Hoge::foo(): Return value must be of type int, Hoge returned
#0 /home/hhelibex/blog/2022-0615-01/Main2.php(38): Hoge->foo()
#1 {main}
===method bar():string===
===    1    ===
int(1)
string(1) "1"
===    '2'    ===
string(1) "2"
string(1) "2"
===    true    ===
bool(true)
string(1) "1"
===    '00'    ===
string(2) "00"
string(2) "00"
===    'hello'    ===
string(5) "hello"
string(5) "hello"
===    Hoge::__set_state(array(
))    ===
object(Hoge)#2 (0) {
}
string(27) "Hoge::__set_state(array(
))"
===method piyo():Hoge===
===    1    ===
Hoge::piyo(): Return value must be of type Hoge, int returned
#0 /home/hhelibex/blog/2022-0615-01/Main2.php(59): Hoge->piyo()
#1 {main}
===    '2'    ===
Hoge::piyo(): Return value must be of type Hoge, string returned
#0 /home/hhelibex/blog/2022-0615-01/Main2.php(59): Hoge->piyo()
#1 {main}
===    true    ===
Hoge::piyo(): Return value must be of type Hoge, bool returned
#0 /home/hhelibex/blog/2022-0615-01/Main2.php(59): Hoge->piyo()
#1 {main}
===    '00'    ===
Hoge::piyo(): Return value must be of type Hoge, string returned
#0 /home/hhelibex/blog/2022-0615-01/Main2.php(59): Hoge->piyo()
#1 {main}
===    'hello'    ===
Hoge::piyo(): Return value must be of type Hoge, string returned
#0 /home/hhelibex/blog/2022-0615-01/Main2.php(59): Hoge->piyo()
#1 {main}
===    Hoge::__set_state(array(
))    ===
object(Hoge)#4 (0) {
}
object(Hoge)#4 (0) {
}
$ 

クラスのプロパティ(PHP 7.4.0以降)

Main3.php

<?php

class Hoge {
    public int $int_val;
    public string $str_val;
    public Hoge $hoge_val;
    public function __toString() {
        return var_export($this, true);
    }
}

$hoge = new Hoge();
foreach (array(1, "2", true, "00", "hello", new Hoge()) as $val) {
    print "===    " . var_export($val, true) . "    ===" . PHP_EOL;
    try {
        $hoge->int_val = $val;
        var_dump($hoge->int_val);
    } catch (TypeError $e) {
        print $e->getMessage() . PHP_EOL;
        print $e->getTraceAsString() . PHP_EOL;
    }
    try {
        $hoge->str_val = $val;
        var_dump($hoge->str_val);
    } catch (TypeError $e) {
        print $e->getMessage() . PHP_EOL;
        print $e->getTraceAsString() . PHP_EOL;
    }
    try {
        $hoge->hoge_val = $val;
        var_dump($hoge->hoge_val);
    } catch (TypeError $e) {
        print $e->getMessage() . PHP_EOL;
        print $e->getTraceAsString() . PHP_EOL;
    }
}

実行結果。

$ php81 Main3.php
===    1    ===
int(1)
string(1) "1"
Cannot assign int to property Hoge::$hoge_val of type Hoge
#0 {main}
===    '2'    ===
int(2)
string(1) "2"
Cannot assign string to property Hoge::$hoge_val of type Hoge
#0 {main}
===    true    ===
int(1)
string(1) "1"
Cannot assign bool to property Hoge::$hoge_val of type Hoge
#0 {main}
===    '00'    ===
int(0)
string(2) "00"
Cannot assign string to property Hoge::$hoge_val of type Hoge
#0 {main}
===    'hello'    ===
Cannot assign string to property Hoge::$int_val of type int
#0 {main}
string(5) "hello"
Cannot assign string to property Hoge::$hoge_val of type Hoge
#0 {main}
===    Hoge::__set_state(array(
))    ===
Cannot assign Hoge to property Hoge::$int_val of type int
#0 {main}
string(27) "Hoge::__set_state(array(
))"
object(Hoge)#2 (0) {
  ["int_val"]=>
  uninitialized(int)
  ["str_val"]=>
  uninitialized(string)
  ["hoge_val"]=>
  uninitialized(Hoge)
}
$ 

所感

パラメータの型宣言(type hinting)がもう少し早くプリミティブ型に対応していてくれれば、あんな苦労(何)をせずに済んだのになぁ、とは思う。

いずれにしても、これで型安全なコーディングがしやすくなることは確か。

もう少し検証が必要な気がするので、それはまた後日。

オブジェクトの存在しないプロパティを参照したときの挙動

もしかしてオブジェクトの場合もか?ということで試したメモ。

Main.php

<?php

ini_set('error_reporting', E_ALL);
ini_set('display_errors', 1);

class A {
    public $a = null;
}
$a = new A();
var_dump($a->a);
var_dump($a->b);

実行結果。

$ for php in php56 php70 php71 php72 php73 php74 php80 php81 php82 ; do echo "===${php}===" ; ${php} Main.php ; done
===php56===
NULL
PHP Notice:  Undefined property: A::$b in /home/hhelibex/blog/2022-0614-01/Main.php on line 11

Notice: Undefined property: A::$b in /home/hhelibex/blog/2022-0614-01/Main.php on line 11
NULL
===php70===
NULL
PHP Notice:  Undefined property: A::$b in /home/hhelibex/blog/2022-0614-01/Main.php on line 11

Notice: Undefined property: A::$b in /home/hhelibex/blog/2022-0614-01/Main.php on line 11
NULL
===php71===
NULL
PHP Notice:  Undefined property: A::$b in /home/hhelibex/blog/2022-0614-01/Main.php on line 11

Notice: Undefined property: A::$b in /home/hhelibex/blog/2022-0614-01/Main.php on line 11
NULL
===php72===
NULL
PHP Notice:  Undefined property: A::$b in /home/hhelibex/blog/2022-0614-01/Main.php on line 11

Notice: Undefined property: A::$b in /home/hhelibex/blog/2022-0614-01/Main.php on line 11
NULL
===php73===
NULL
PHP Notice:  Undefined property: A::$b in /home/hhelibex/blog/2022-0614-01/Main.php on line 11

Notice: Undefined property: A::$b in /home/hhelibex/blog/2022-0614-01/Main.php on line 11
NULL
===php74===
NULL
PHP Notice:  Undefined property: A::$b in /home/hhelibex/blog/2022-0614-01/Main.php on line 11

Notice: Undefined property: A::$b in /home/hhelibex/blog/2022-0614-01/Main.php on line 11
NULL
===php80===
NULL
PHP Warning:  Undefined property: A::$b in /home/hhelibex/blog/2022-0614-01/Main.php on line 11

Warning: Undefined property: A::$b in /home/hhelibex/blog/2022-0614-01/Main.php on line 11
NULL
===php81===
NULL
PHP Warning:  Undefined property: A::$b in /home/hhelibex/blog/2022-0614-01/Main.php on line 11

Warning: Undefined property: A::$b in /home/hhelibex/blog/2022-0614-01/Main.php on line 11
NULL
===php82===
NULL
PHP Warning:  Undefined property: A::$b in /home/hhelibex/blog/2022-0614-01/Main.php on line 11

Warning: Undefined property: A::$b in /home/hhelibex/blog/2022-0614-01/Main.php on line 11
NULL
$ 

配列の場合と同様に、PHP 8.0から「Warning」に変わっている。

参考

hhelibex.hatenablog.jp hhelibex.hatenablog.jp