PHP

declare(strict_types=1)の効力範囲について

declare(strict_types=1); とは、PHP7から導入された、厳格な型検査モードの指定構文です。
有効範囲をあまり把握してなかったのでまとめてみました。

strict_types基本の動き

例えばこんなコード。

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

var_dump(add(1.0, 2.0));

この状態で単体実行すると、int(3)が出力されます。
渡しているのはdouble型なのですが、よしなにキャストされて処理されています。まあ、昔ながらのPHPらしい動きです。

ここで、declare(strict_types=1)を有効にしてみます。

<?php
declare(strict_types=1);

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

var_dump(add(1.0, 2.0));

実行するとTypeErrorが発生しました。この場合誰もcatchしていないので、スクリプトは停止します。

PHP Fatal error:  Uncaught TypeError: Argument 1 passed to add() must be of the type integer, float given, called in /Users/hiraku/sandbox/stricttypes/A.php on line 9 and defined in /Users/hiraku/sandbox/stricttypes/A.php:4
Stack trace:
#0 /Users/hiraku/sandbox/stricttypes/A.php(9): add(1, 2)
#1 {main}
  thrown in /Users/hiraku/sandbox/stricttypes/A.php on line 4

strict_typesの制限

なお、declare構文はスクリプトの途中に書くことが出来ません。たとえばこうして実行すると、

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

declare(strict_types=1);

var_dump(add(1.0, 2.0));
PHP Fatal error:  strict_types declaration must be the very first statement in the script in /Users/hiraku/sandbox/stricttypes/A.php on line 7

Fatal errorが発生します。これはThrowableですらない、コンパイルフェーズで発生しているエラーですね。
似たところで、ブロック構文も許されません。

<?php
declare(strict_types=1) {
  //...
}
PHP Fatal error:  strict_types declaration must not use block mode in /Users/hiraku/sandbox/stricttypes/A.php on line 2

ファイルが複数ある時

プログラムが複数ファイルに分かれているときは、それぞれのファイルの先頭にdeclare構文を書くことが出来ます。

A.php
<?php
declare(strict_types=1);
function add(int $a, int $b): int
{
    return $a + $b;
}

このA.phpを、別のファイルからrequireして使ってみましょう。

B.php
<?php
require 'A.php';
var_dump(add(1.0, 2.0));
$ php B.php
int(3)

なんと!実行できてしまいました。
B.phpはstrict_typesを宣言しておらず、ゆるいモードになっているからです。

つまり、strict_typesとは以下のような挙動なのです。

  • 関数定義時のstrictモードは、どちらであろうと動作に違いは出ない
  • 関数を実行した際のstrictモードで、違いが出る
  • declare(strict_types=1);の構文自体は、そのファイルで完結しており、別のファイルからrequireされても、require元のファイルのstrict_typesモードが切り替わることはない

宣言を逆にしてみると、strictモードの動きになります。

A.php
<?php
function add(int $a, int $b): int
{
    return $a + $b;
}
B.php
<?php
declare(strict_types=1);

require 'A.php';
var_dump(add(1.0, 2.0));
$ php B.php
PHP Fatal error:  Uncaught TypeError: Argument 1 passed to add() must be of the type integer, float given, called in /Users/hiraku/sandbox/stricttypes/B.php on line 4 and defined in /Users/hiraku/sandbox/stricttypes/A.php:2

A.phpはstrictモードがoffになってたはずなのに! B.phpの設定のみで動きが変わるわけですね。

関数定義時のdeclare(strict_types=1)の効力

だんだん混乱してきました。もう一段requireを増やして、3つのファイルの入れ子にしてみましょう。

C.php → B.php → A.php

C.php
<?php
require_once 'B.php';
var_dump(add(1.0, 2.0));
var_dump(add2(1.0, 2.0));
B.php
<?php
declare(strict_types=1);
require_once 'A.php';
function add2($a, $b)
{
    return add($a, $b);
}
A.php
<?php
function add(int $a, int $b): int
{
    return $a + $b;
}

で、実行してみるとこうなります。

$ php C.php 
int(3)
PHP Fatal error:  Uncaught TypeError: Argument 1 passed to add() must be of the type integer, float given, called in /Users/hiraku/sandbox/stricttypes/B.php on line 7 and defined in /Users/hiraku/sandbox/stricttypes/A.php:2

var_dump(add(1.0, 2.0)); はうまく実行できて、
var_dump(add2(1.0, 2.0)); はTypeErrorが発生していることがわかります。

つまり、関数定義部分に書かれたdeclare(strict_types=1);は、以下のような動きになります。

  • そのファイルで定義された関数自体には、効果を及ぼさなさい
  • そのファイルで定義された関数の中から、別の関数を呼んだ場合、そこがstrictモードになる

なかなかややこしいですね。

大本の部分でstrict_typesを指定したら?

B.phpのような中途半端な位置でstrict_typesを指定するのではなく、大本となるC.phpで指定すれば、全面的にstrictモードが有効になりそうに思いますよね?

が、実はstrictモードが有効なのは呼び出し箇所だけです。

C.php → B.php → A.php

C.php
<?php
declare(strict_types=1);
require_once 'B.php';
var_dump(add2(1.0, 2.0));
B.php
<?php
require_once 'A.php';
function add2($a, $b)
{
    return add($a, $b);
}
A.php
<?php
function add(int $a, int $b): int
{
    return $a + $b;
}
$ php C.php 
int(3)

実行できてしまいました。
C.phpの時点では、strict_types=1です。
だから、 add2(1.0, 2.0) はstrictモードで実行されます。もっとも、型宣言は書いていないので、何も効力はありません。

一方で、add2()の定義が書いてあるB.phpは非strictモードです。
だから、add($a, $b)非strictモードで実行されます。

直接呼んでる箇所でしか発動しないわけですね…!

まとめ

strictで実行される箇所は、declareの書いてあるファイルの、実行部分だけです。関数呼び出しが他のファイルに波及すると、それはそのファイルのstrict状態でモードが切り替わり、場合によっては非strictモードでの実行になってしまうこともあります。

Foo.php
<?php
// このファイルはstrict有効
declare(strict_types=1);

class Foo
{
    private $bar;

    public function __construct() // strictで呼ばれるとは限らない
    {
        $this->bar = new Bar; // 必ずstrictで実行
    }

    public function aaa() // strictで呼ばれるとは限らない
    {
        $this->bar->aaa(); // 必ずstrictで実行
    }
}
Bar.php
<?php
// このファイルはstrict無効
class Bar
{
    private $moo;

    public function __construct() // strictで呼ばれるとは限らない
    {
        $this->moo = new Moo; // 必ず非strictで実行
    }

    public function aaa() // strictで呼ばれるとは限らない
    {
        $this->moo->aaa(); // 必ず非strictで実行
    }
}

strictモードの難しさ

strict_typesというのは広範囲で全面的に有効にすることはできない仕組みになっているようです。

全面的に有効にしたくても、declare(strict_types=1);の書かれていない箇所があると無効になりますし、
また実行経路の中でdeclare(strict_types=1);があると再び有効になったりします。

だいぶややこしい機能ですね。。