PHP7.1以降なおったバグたち

2016/12/01に出たPHP7.1.0以降、2017/04/13に出た7.1.4までに修正されたバグのうち、目立ったものを取り出してみた。
メモリリークとかSegfaultとか長すぎるやつとかは確かめるのが面倒なのでスルー。

## [array_key_exists fails on arrays created by get_object_vars](https://bugs.php.net/bug.php?id=73998)

オブジェクトから配列にしたらarray_key_existsでキーが見つからないという問題。

```php
    $json = '{"2":1}';
    $obj = json_decode($json);
    $arr = get_object_vars($obj);
    $key = array_key_exists('2', $arr); // falseになる
```

これは`$arr`が`[2=>1]`ではなく`["2"=>1]`になっているせいで発生する問題です。
元々[7.2でなおす](http://qiita.com/rana_kualu/items/40ebed78742bfdbd1065#convert-numeric-keys-in-objectarray-casts)とか言ってた気がするんだけど[7.1.3でなおってました](https://3v4l.org/CK2Zn)。


## [PHP hangs when an invalid value is dynamically passed to typehinted by-ref arg](https://bugs.php.net/bug.php?id=74164)

特定の条件下で参照渡しタイプヒンティングに正しくない値を渡すと[PHPがハングアップする](https://3v4l.org/XGvob)問題。

```php
    namespace Foo;

    set_error_handler(function () {
        throw new \Exception();
    });

	call_user_func(function (array &$ref) {}, 'not_an_array_variable');
```

7.1.0から7.1.2という狭いバージョン内でだけ発生します。

やたら発生条件が細かくて、[正しい引数を渡し](https://3v4l.org/9NSkD)たり[namespaceを消し](https://3v4l.org/U8MU4)たり[set_error_handlerを消し](https://3v4l.org/vNN9X)たり[&を消し](https://3v4l.org/kG7oU)たり[call_user_funcを普通の関数にし](https://3v4l.org/e3h8C)たりするだけで発生しなくなります。
むしろどうやって発見したんだこれ。


## [ield from1251('dfd') ("from1251" is a valid function name) fails](https://bugs.php.net/bug.php?id=74302)

↓のコードがシンタックスエラーになる。

```php
    function from1($a){
    	return $a;
    }
    function foo(){
    	yield from1('foo');
    }
```

何処も間違ってないのに[Parse error: syntax error, unexpected '('](https://3v4l.org/EXjKc)が発生します。

どうやらこれ、[ジェネレータ委譲](http://php.net/manual/ja/language.generators.syntax.php#control-structures.yield.from)の検出が誤爆しているようです。

なお関数名を`froma`に変えると[発生しなく](https://3v4l.org/S2MvM)なるので、fromのあと数値が来るとおかしくなるようです。
7.1.4で修正されました。


## [ArrayObject can not notice changes](https://bugs.php.net/bug.php?id=74058)

ArrayObject内のオブジェクトが呼ばれたことを検出できない。

```php
	class MyArrayObject extends ArrayObject{
		function __construct($input=[]){
			parent::__construct($input,ArrayObject::ARRAY_AS_PROPS);
		}
		function offsetSet($x,$v){
			echo "offsetSet('{$x}')\n";
			return parent::offsetSet($x,$v);
		}
		function offsetGet($x){
			echo "offsetGet('{$x}')\n";
			return parent::offsetGet($x);
		}
		function __get($x){
			echo "__get('{$x}')\n";
		}
		function __set($x,$v){
			echo "__set('{$x}')\n";
		}
		function offsetExists($x){
			echo "offsetExists('{$x}')\n";
			return parent::offsetExists($x);
		}
	}
	$x=new MyArrayObject;
	$x->hello=5;
	$x['hi']=10;
	$x['hello']++;
	@$x->a->b=7;
	$x->a->b++;
```

PHPでは存在しない`$x->a`に対して`$x->a->b`とアクセスすると、[いきなりstdClass`$x->a`が生えます](https://3v4l.org/9T4j6)。

ArrayObject::ARRAY_AS_PROPSを使って作成したArrayObjectに対してそうアクセス場合、これまではoffsetGetも__getも[反応しません](https://3v4l.org/DJH6M)でした。
変更が検知できないため、想定していない値を入れ放題になってしまいます。
そのため7.1.4でoffsetGetが反応するようになりました。

ArrayObject::ARRAY_AS_PROPSを指定していない場合は[元々`__get`が反応する状態](https://3v4l.org/ZgSI8)だったため問題なかったようです。

ていうかHHVMではARRAY_AS_PROPSが効かないのか。


## [Swatch time value incorrect for dates before 1970](https://bugs.php.net/bug.php?id=72096)

1970年以前の[スウォッチ・インターネットタイム](https://ja.wikipedia.org/wiki/%E3%82%B9%E3%82%A6%E3%82%A9%E3%83%83%E3%83%81%E3%83%BB%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%8D%E3%83%83%E3%83%88%E3%82%BF%E3%82%A4%E3%83%A0)が[ずれることがある](https://3v4l.org/gPJqN)。

```php
    var_dump(gmdate('Y-m-d H:i:s B', -1130636800)); // 1934-03-04 22:13:20 968
    var_dump(gmdate('Y-m-d H:i:s B',  1457129600)); // 2016-03-04 22:13:20 967
    
    var_dump(gmdate('Y-m-d H:i:s B', -1130644800)); // 1934-03-04 20:00:00 875
    var_dump(gmdate('Y-m-d H:i:s B',  1457121600)); // 2016-03-04 20:00:00 875 正常な場合もある
```

PHP5以来ずっとこうなっていて、7.1.4で初めて修正されました。
そもそもスウォッチ・インターネットタイムなんて誰も使ってないからどうでもいいよね。


## [stream_get_contents maxlength>-1 returns empty string on windows](https://bugs.php.net/bug.php?id=74090)

[stream_get_contents](http://php.net/manual/ja/function.stream-get-contents.php)の第二引数$maxlengthを入れると、値が取得できない。

```php
    $fd = stream_socket_client("udp://8.8.8.8:53", $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT);
    stream_set_blocking($fd, 0);
    stream_socket_sendto($fd,base64_decode("1oIBAAABAAAAAAAAB2V4YW1wbGUDb3JnAAABAAE="));
    sleep(1);
    var_dump(stream_get_contents($fd, 100)); // string(0)
    var_dump(stream_get_contents($fd)); // string(45)
    stream_socket_shutdown($fd,STREAM_SHUT_RDWR);
```

一回目のstream_get_contentsが何故か0バイトになっています。
第二引数`100`を削除すると一回目で値が取れます。

3v4lでは外部接続できないのでいつから発生してるかはわかりませんが、PHP7.1.3で修正されました。


## [PHP on Linux should use /dev/urandom when getrandom is not available](https://bugs.php.net/bug.php?id=74105)

Linuxプラットフォームでrandom_int()したときに、[getrandom](http://man7.org/linux/man-pages/man2/getrandom.2.html)が入ってなければExceptionが出るから、その場合はdev/urandomを見るべきというもの。

7.1.3で修正されました。
ただgetrandomが入ったのは2014/10/05の[カーネル3.17](https://mag.osdn.jp/14/10/07/074100)なので、カーネルがそれ以前なのにPHP7.1.3以降が入ってる環境なんてそうそうなさそうではある。

なお[random_int](http://php.net/manual/en/function.random-int.php)のマニュアルだと、

> On Linux, the » getrandom(2) syscall will be used if available.
> On other platforms, /dev/urandom will be used.

と書かれてるから元の動作も間違っているというわけではない。


## [is_callable callable name reports misleading value for anonymous classes](https://bugs.php.net/bug.php?id=73118)

無名クラスを[is_callable](http://php.net/manual/ja/function.is-callable.php)したときに、第三引数callable_nameが"クラス名::メソッド名"ではなく["class@anonymous"になってる](https://3v4l.org/kTN4e)。

```php
    trait callback{
      public function __invoke($p){}
    }
    
    class foo { use callback; }
    $inline = new class { use callback; };
    
    var_dump(
      is_callable($inline, false, $target),
      $target, // class@anonymous
      is_callable([$inline, '__invoke'], false, $target),
      $target, // class@anonymous
      is_callable(new foo, false, $target),
      $target // foo::__invoke
    );
```

7.1.3で修正され、"class@anonymous/in/kTN4e0x7fcc76b5b092::__invoke"のようなよくわからないクラス名が返ってくるようになりました。
コンパイル時に内部で発行された名前？


## [substr_count with length=0 broken](https://bugs.php.net/bug.php?id=74041)

[substr_count](http://php.net/manual/ja/function.substr-count.php)の第4引数$lengthが0のとき動作が正しくない。

```php
	var_dump(substr_count('aaa', 'a', 0, 0)); // 3
	var_dump(substr_count('aaa', 'a', 0, 1)); // 1
```

何故か0ではなく[3になる](https://3v4l.org/f0Rr6)。
7.1.3で修正されました。
そもそも7.1より前は0に対応してなかったのでそのままでよかったんじゃないかと思いますが、どうやら[Generalize support of negative string offsets](http://qiita.com/rana_kualu/items/476385b674d27339f911#generalize-support-of-negative-string-offsets)に対応したときにエンバグしたようです。


## [ReflectionFunction for imagepng is missing last two parameters](https://bugs.php.net/bug.php?id=74031)

[imagepng](http://php.net/manual/ja/function.imagepng.php)の[引数](http://php.net/manual/ja/reflectionfunctionabstract.getparameters.php)が前の2個しか取得できないという謎のバグ。
7.13で修正されました。

```php
    $ref = new ReflectionFunction('imagepng'); 
    var_dump($ref->getParameters()); // $imと$toだけ
```

実際はReflectionのバグではなく、GDのZEND_BEGIN_ARG_INFO_EXマクロが[正しく登録されていなかった](http://git.php.net/?p=php-src.git;a=blobdiff;f=ext/gd/gd.c;h=ec2a037357de07233938967c7c3de553e817521f;hp=4f9e2f8fc613411b77b387f9014e254b79ee585a;hb=acda2563cea2acacdb126a6c14a144a9bf52e3a3;hpb=a15bffd105ac28fd0dd9b596632dbf035238fda3)せいのようです。
これは引数の[型チェックをPHPにやらせる](https://cybozu.atlassian.net/wiki/pages/viewpage.action?pageId=6160386#id-モダンなPHP拡張の書き方-ArgInfoandType-hinting)ためのもので、型チェックが不要なら[無くても問題は無い](http://stackoverflow.com/questions/14635111/how-to-make-zend-begin-arg-info-ex-control-number-of-arguments-passed-to-a-php)っぽいです。
Reflectionの正確性が努力目標だとは知らなかった。


## [gost-crypto hash incorrect if input data contains long 0xFF sequence](https://bugs.php.net/bug.php?id=73127)

暗号化する文字列中に"0xFF"が約40文字以上あった場合、[正しく暗号化されない](https://3v4l.org/FHX8l)というバグ。

```php
	$test = str_repeat("\xFF", 40);
	echo hash('gost-crypto', $test);
```

7.1.2までは`8e0be2995864c40f8111feaa9df6fc4830632fdf61e365d9feca87f1e485d1f7`(間違い)で、7.1.3以降は正しく`231d8bb980d3faa30fee6ec475df5669cf6c24bbce22f46d6737470043a99f8e`となります。

## ["new DateTime()" sometimes returns 1 second ago value on PHP 7.1.0](https://bugs.php.net/bug.php?id=73837)

時々`new DateTime()`が1秒前の値を返すというバグ

```php
	$prev_dt = new DateTime();
	while (true) {
	    $dt = new DateTime();
	    if ($prev_dt > $dt) {
	        var_dump($prev_dt->format("Y-m-d H:i:s.u"));
	        var_dump($dt->format("Y-m-d H:i:s.u"));
	        break;
	    }
	    $prev_dt = $dt;
	}
```

本来無限ループにならないといけないはずですが、だいたい数秒で中断されます。

動作を見るかぎりでは、秒が1進むよりほんの少し前にマイクロ秒が0に戻るので、そのときに`new DateTime()`されると1秒戻る、みたいな動作をしてるっぽい感じでした。
7.1.3で修正されました。

## [intval() with base 0 should detect binary](https://bugs.php.net/bug.php?id=73374)

[intval](http://php.net/manual/ja/function.intval.php)が[2進数に対応していない](https://3v4l.org/d8UEM)。

```php
	var_dump(intval('0b1101', 0)); // 0
```

そもそも[マニュアル](http://php.net/manual/en/function.intval.php)にも16進数と8進数しか対応しないと書いてあるので仕様な気もしますが、7.1.2でしれっと2進数に対応しました。
マニュアルの変更履歴はまだ書かれていません。


# 感想

まだ半分も見てないのに色々出てきますね。
大半は一生遭遇することもないマイナーな関数の変な使い方によるものですが、PHPの場合たまーにこういうところにしれっと致命的な修正が入ってきたりすることがあるから怖い。
幸い今回は大きなものは見当たらないっぽい様子でした。