LoginSignup
1
1

More than 1 year has passed since last update.

型を指定したからといって安心してはいけない(戒め

Last updated at Posted at 2020-07-03

PHPネタです。

みんな強いから知ってると思うけど、僕は雑魚なのでメモがてら。

PHPにおけるキャストの動きをオブジェクト生成(コンストラクタの挙動)を例に検証してみます。
PHP 7.3.11 で検証しています。

あまり意識してなかったキャストの挙動について

下記のようなコンストラクタをもつクラスについて、数値以外の引数を渡してしまった場合、
本来であればエラーとなって欲しい気もしますが、おそらく数値型にキャストされるだろうなと推測する方が多いかと思います。


  class MyTest {
    private $param;
    public function __construct(int $param) {
      $this->param = $param;
    }
  }

案の定、数値型にキャスト可能な文字列が引数に入っていた場合、文字列->数値へのキャストが行われ、
正常にオブジェクトが生成されます。

$obj = new MyTest("1");
var_dump($obj)

// Result
object(MyTest)#1 (1) {
  ["message":"MyTest":private]=>
  int(1)
}

数値型にキャスト可能な文字列が引数に入っていた場合

と書きましたが、"0x13", "0b110101"などの文字列リテラルで渡してもエラーを起こしません。
ただし、0に変換されてメンバ変数に格納されてしまいます。
これは仕様なのか分かりませんが、なんとなく気持ち悪いですね(小並

$obj = new MyTest("0b00101");
var_dump($obj);

// Result
object(MyTest)#1 (1) {
  ["message":"MyTest":private]=>
  int(0)
}

数値と解釈できない文字列の場合に限り、エラーとなります。

$obj = new MyTest("yahho");

// Result
Fatal error: Uncaught TypeError: Argument 1 passed to MyTest::__construct() must be of the type int, string given, called in /Users/y-nishi/Desktop/constructor.php on line 32 and defined in /Users/y-nishi/Desktop/constructor.php:9
Stack trace:
#0 /Users/y-nishi/Desktop/constructor.php(32): MyTest->__construct('yahho')
#1 {main}
  thrown in /Users/y-nishi/Desktop/constructor.php on line 9

もちろん、オブジェクトも同様にエラーになります。

$obj = new MyTest(new Hoge());

// Result
Fatal error: Uncaught TypeError: Argument 1 passed to MyTest::__construct() must be of the type string, object given, called in /Users/y-nishi/Desktop/constructor.php on line 35 and defined in /Users/y-nishi/Desktop/constructor.php:9
Stack trace:
#0 /Users/y-nishi/Desktop/constructor.php(35): MyTest->__construct(Object(Hoge))
#1 {main}
  thrown in /Users/y-nishi/Desktop/constructor.php on line 9

意図しない型の引数が入ってきたらどうしてもエラーにしたい!

そんな事言うなら静的型付け言語使えよって話なんですが、PHPをメインで使っている会社やPJはかなり多いかと思います。
この要望に立ち向かうために、前述の

もちろん、オブジェクトも同様にエラーになります。

の挙動を応用します。

プリミティブなstringやintを使うのではなく、ラップしたクラスを用意して使おうという考えです。
DDDでいうところのValueObjectの考え方と同じです。

このようなクラスを用意してみました。


class ID_A {
  private $intVal;
  public function __construct(int $intVal) {
    $this->intVal = $intVal;
  }

  public function toInt():int {
    return $this->intVal;
  }
}

class ID_B {
  private $strVal;
  public function __construct(string $strVal) {
    $this->strVal = $strVal;
  }

  public function toString():string {
    return $this->strVal;
  }
}

class Hoge{
  private $id;
  public function __construct(ID_A $id) {
    $this->id = $id;
  }
}

この状態ではID_Aクラスから生成されたオブジェクト以外の引数を受け付けません。

// 正常
$obj = new Hoge(new ID_A());
var_dump($obj);

// Result
object(Hoge)#1 (1) {
  ["id":"Hoge":private]=>
  object(ID_A)#2 (0) {
  }
}
// エラー
$obj = new Hoge(new ID_B());
var_dump($obj);

// Result
Fatal error: Uncaught TypeError: Argument 1 passed to Hoge::__construct() must be an instance of ID_A, instance of ID_B given, called in /Users/y-nishi/Desktop/constructor.php on line 42 and defined in /Users/y-nishi/Desktop/constructor.php:37
Stack trace:
#0 /Users/y-nishi/Desktop/constructor.php(42): Hoge->__construct(Object(ID_B))
#1 {main}
  thrown in /Users/y-nishi/Desktop/constructor.php on line 37

ただ1点注意事項があり、継承関係にあるクラスであり、かつアップキャスト可能なオブジェクト(スーパークラスが存在する)の場合はキャストされます。
※ダウンキャストが発生する場合はエラーとなります。

class ID_A {}

// ID_Aを継承した
class ID_B extends ID_A {}

class Hoge{
  private $id;
  public function __construct(ID_A $id) {
    $this->id = $id;
  }
}

$obj = new Hoge(new ID_B());
var_dump($obj);

// Result
object(Hoge)#1 (1) {
  ["id":"Hoge":private]=>
  object(ID_B)#2 (0) {    // ID_Bのオブジェクトが入る。
  }
}

動的片付け言語は型を意識せずに書けるのですが、挙動を意識せずに書くと意図しない動作になってしまうので注意して書いていきたいですね。

1
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1