PHP7.3.0β3で修正されたバグたち

[7.3の新機能](https://qiita.com/rana_kualu/items/a7c6be77e165bca0f3fc) / [7.3.0β3のバグ](https://qiita.com/rana_kualu/items/12ff6d448c0d5ad73108)


2018/08/30に[PHP7.3.0β3がリリースされた](http://php.net/archive/2018.php#id2018-08-30-1)ので[NEWS](https://github.com/php/php-src/blob/php-7.3.0beta3/NEWS)を眺めていたところ、なんか意味のわからないバグが色々あったので取り上げてみます。

## [Crash in ZEND_COALESCE_SPEC_TMP_HANDLER](https://bugs.php.net/bug.php?id=76752)

[BetterReflection](https://github.com/Roave/BetterReflection)を動かすとクラッシュするんだが、という内容。
ミニマム再現条件は[こう](https://3v4l.org/iEDZc)。

```php
$obj = new stdClass;
$val = 'foo';
$obj->prop = &$val;
var_dump($obj->prop ?? []);
```
![01.png](https://qiita-image-store.s3.amazonaws.com/0/26088/9b028aca-e6d5-764a-fd82-ef82e030a94b.png)

PHP7.3.0のα2からβ2の間だけ`&string`になっています。
さらに、意味があるようには見えない`?? []`を削除すると[発生しません](https://3v4l.org/La1K2)。
なんなんだこれ。

まあ参照渡しなんて使うなってことだ。

## [parent private constant in extends class memory leak](https://bugs.php.net/bug.php?id=76754)

```php
class FOO{
    private const FOO = 'BAR';
}
class BAR extends FOO {}
```

何の変哲も無いただの定数ですが、実はこの定数がメモリリークしていたとかいうバグ。
原因は[ハッシュテーブル破棄の位置をほんのり間違っていた](http://git.php.net/?p=php-src.git;a=blobdiff;f=Zend/zend_opcode.c;h=8168f6a0476d7b32b73690d2b74b3cbf436f48a1;hp=fe1d95737a4b974a63140eb956880c486d860176;hb=04c4854fdfbc11b2b6a2cf1c8045f2efcd5dcdcc;hpb=64c88c1e168cb4489e5adae4067e35f7abd18046)というもの。

あまり詳しく読んでないのでわからないのだが、同じものが[317行目](https://github.com/php/php-src/blob/php-7.3.0beta3/Zend/zend_opcode.c#L317)にもあるのだがこちらは大丈夫なのだろうか？


## [iconv_mime_decode can return extra characters in a header](https://bugs.php.net/bug.php?id=68180)


長い文字列にiconv_mime_decodeすると正しくデコードされないことがあるというバグ。
バグチケットに添付されているサンプルは[こんなかんじ](https://3v4l.org/AvlnD)です。

```php
$original = "=?UTF-8?Q?=E3=80=8E=E3=80=90=E5=A4=96=E8=B3=87=E7=B3=BB=E6=88=A6=E7=95=A5=E3=82=B3=E3=83=B3=E3=82=B5=E3=83=AB=E3=81=8C=E9=9B=86=E7=B5=90=E3=80=91=E3=83=88=E3=83=83=E3=83=97=E3=82=B3=E3=83=B3=E3=82=B5=E3=83=AB=E3=82=BF=E3=83=B3=E3=83=88=E3=81=A8=E8=A9=B1=E3=81=9B=E3=82=8B=E3=82=B3=E3=83=B3=E3=82=B5=E3=83=AB=E6=A5=AD=E7=95=8C=E7=A0=94=E7=A9=B6=E3=82=BB=E3=83=9F=E3=83=8A=E3=83=BC=E3=80=8F=E3=81=B8=E3=81=AE=E3=82=A8=E3=83=B3=E3=83=88=E3=83=AA=E3=83=BC=E3=81=82=E3=82=8A=E3=81=8C=E3=81=A8=E3=81=86=E3=81=94=E3=81=96=E3=81=84=E3=81=BE=E3=81=97=E3=81=9F=E3=80=82?=";
$decoded = iconv_mime_decode($original, ICONV_MIME_DECODE_STRICT, 'utf-8');

$prefs = array('input-charset' => 'UTF-8', 'output-charset' => 'UTF-8', 'scheme' => 'Q', 'line-length' => 1000);
$encoded = iconv_mime_encode('Subject', $decoded, $prefs);
list($_, $encoded) = explode(' ', $encoded, 2);

echo 'Before: ' . strlen($original) . ', After: ' . strlen($encoded) . "\n";
```

![02.png](https://qiita-image-store.s3.amazonaws.com/0/26088/8981362d-5389-bffa-2687-1ff34e69755f.png)

バージョンによって文字数がバラバラですね。

日本語環境であれば、デコードした後に再エンコードせずとも[デコード文字列を直接見ればいい](https://3v4l.org/nAQGk)です。

![03.png](https://qiita-image-store.s3.amazonaws.com/0/26088/6722f7f3-a400-5bc3-e727-c1b68cc1674a.png)

なんだこのSPAMっぽいタイトル。

それはそうとデコードした文字列の後ろに変なものが入っていますね。
[_php_iconv_appendlのバグ](http://git.php.net/?p=php-src.git;a=blobdiff;f=ext/iconv/iconv.c;h=de5bccbb9773639fba80137cceaf434952c6b029;hp=d759596d653f2395f71006296f3157680f7fd540;hb=efb86aef12ff4f8f3908ceff2844f7511f7d61eb;hpb=e29c946c29afdb0bf89c5329fcf3038448d50e17)で文字列長が正しく計算されておらず、最後に変なゴミがくっついてしまっていたようです。


## [iconv_mime_decode_headers() skips some headers](https://bugs.php.net/bug.php?id=55146)

```php
iconv_set_encoding('iconv.internal_encoding', 'UTF-8');
iconv_set_encoding('iconv.input_encoding', 'UTF-8');
iconv_set_encoding('iconv.output_encoding', 'UTF-8');

$headers = <<< HEADERS
X-Header-One: H4sIAAAAAAAAA+NgFlsCAAA=
X-Header-Two: XtLePq6GTMn8G68F0
HEADERS;

echo "Wrong decoding:\n";
var_dump(iconv_mime_decode_headers($headers, ICONV_MIME_DECODE_CONTINUE_ON_ERROR));

echo "Correct decoding:\n";
var_dump(iconv_mime_decode_headers($headers, ICONV_MIME_DECODE_STRICT));
```

前者はエラーが出ても処理を継続する[ICONV_MIME_DECODE_CONTINUE_ON_ERRORオプション](http://php.net/manual/ja/function.iconv-mime-decode-headers.php)、後者は正しいヘッダしか受け付けないICONV_MIME_DECODE_STRICTを指定しているのだが、

![04.png](https://qiita-image-store.s3.amazonaws.com/0/26088/d99e9cbe-8615-64ea-cd42-8bc8b7654bfb.png)

結果は[どう見ても逆](https://3v4l.org/mq2J5)になっている。

ところで[iconv_set_encodingのマニュアル](http://php.net/manual/en/function.iconv-set-encoding.php)には`internal_encoding`の設定が[Deprecated](https://3v4l.org/QcCSR)であることが書かれていない。
かわりに`iconv.internal_encoding`を入れるとエラーが出ずに設定できるのだが、この書き方は[PHP5.6.0で非推奨になった](http://php.net/manual/en/iconv.configuration.php)と書かれている。
いったいどうすればいいんだ。


## [iconv_mime_decode_headers function is skipping headers](https://bugs.php.net/bug.php?id=63839)

[iconv_mime_decode_headers](http://php.net/manual/ja/function.iconv-mime-decode-headers.php)関数において、Subjectに`=?`という文字列があるとそれ以降のフィールドが[全て消滅することがある](https://3v4l.org/9Yb90)というバグ。

```php
<?php
$headers = 'From: "xyz" <xyz@xyz.com>
To: <xyz@xyz.com>
Subject: Reply Is? white side-LED =? in Help
Date: Sat, 22 Dec 2012
Message-ID: <006f01cde00e$d9f79da0$8de6d8e0>
MIME-Version: 1.0
Content-Type: multipart/alternative;
	boundary="----=_NextPart_000_0070_01CDE03C.F3AFD9A0"
X-Mailer: Microsoft Office Outlook 12.0
Thread-Index: Ac3gDtcH2huHjzYcQVmFJPPoWjJogA==
Content-Language: en-us

';
$headers = iconv_mime_decode_headers($headers, ICONV_MIME_DECODE_CONTINUE_ON_ERROR);
print_r($headers);
```

![05.png](https://qiita-image-store.s3.amazonaws.com/0/26088/bc0f27a3-8a2c-9dfa-e587-1aefae0cea23.png)

`Subject`の`side-LED`より後が全部消えています。
修正のプルリクが[ものすごい場当たり的なものにしか見えない](http://git.php.net/?p=php-src.git;a=blobdiff;f=ext/iconv/iconv.c;h=dd56ebadca938585327514c086b0c1c79ea63503;hp=a76b6fd802ea12181e880168795db735450255b5;hb=8754d44167d95a381c9c60beeb26492f4176fe1c;hpb=6e1980e1520306cc5ec7109ccb3d7c8a0a672689)ので大丈夫なのだろうかと思ったのですが、ソースを見てみたらswitchが[芸術品だった](https://github.com/php/php-src/blob/php-7.3.0beta3/ext/iconv/iconv.c#L1515)ので読むのを諦めました。

iconvは全部で5個のバグフィックスが行われていますが、この勢いを見るにまだまだバグが出てきそうな気がしますね。

## ["public id" parameter of libxml_set_external_entity_loader callback undefined](https://bugs.php.net/bug.php?id=76777)


[libxml_set_external_entity_loader](http://php.net/manual/ja/function.libxml-set-external-entity-loader.php)関数は、引数に三つの引数をとるcallableを受け取ります。
が、何故か[第一引数が未定義になる](https://3v4l.org/9endu)というバグ。

```php
$xml=<<<EOF
<?xml version="1.0"?>
<test/>
EOF;

$xsd=<<<EOF
<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:include schemaLocation="nonexistent.xsd"/>
  <xs:element name="test"/>
</xs:schema>
EOF;

libxml_set_external_entity_loader(function($p,$s,$c) {
    var_dump($p,$s,$c);
    die();
});

$dom=new DOMDocument($xml);
var_dump($dom->schemaValidateSource($xsd));

```

![07.png](https://qiita-image-store.s3.amazonaws.com/0/26088/e269f982-283b-fbf9-9ca8-17b8bfb21f25.png)

マニュアルには「はじめの二つは文字列でそれぞれ パブリックID とシステムID 」とありますが、パブリックIDが存在しないときに何故かnullではなく[Undefined variable](https://3v4l.org/5N24o#v7028)になっています。
そもそもlibxml関数自体使ったことないのだが、いったいどういう需要があるのだろうか、と思っていたら各種XMLモジュールが[libxmlに依存してる](http://php.net/manual/ja/intro.libxml.php)みたい。

あと、そんなことより`ini_set('error_reporting', PHP_INT_MAX-1)`とかいう設定方法初めて見た。


## [Opcache treats path containing "test.pharma.tld" as a phar file](https://bugs.php.net/bug.php?id=76747)

OpCacheを有効にすると、名前に`phar`が入ったディレクトリで[stream_wrapper_unregister('phar')](http://php.net/manual/ja/function.stream-wrapper-unregister.php)した後で`require`できなくなるバグ。

```php:/var/www/test.pharma.tld/test.php
stream_wrapper_unregister('phar');
require 'info.php'; // Warning: require(): Unable to find the wrapper "phar"
```

ディレクトリ名を`test.blabla.tld`に変更したら起きなくなったということです。
再現環境は[Dockerで用意](https://github.com/stucki/php-bug-76747)されていますが、手元のXAMPP環境(Windows10・PHP7.2)では何故か再現しませんでした。

## [Assignment of empty string creates extraneous text node](https://bugs.php.net/bug.php?id=76712)

[SimpleXML](http://php.net/manual/ja/book.simplexml.php)において、[addChild](http://php.net/manual/ja/simplexmlelement.addchild.php)のやりかたによってできあがるXMLが何故か異なるというバグ。

```php
$sxe = new SimpleXMLElement('<foo></foo>');
$sxe->addChild('bar');
echo $sxe->asXML(); // <foo><bar/></foo>

$sxe = new SimpleXMLElement('<foo></foo>');
$sxe->addChild('bar', '');
echo $sxe->asXML(); // <foo><bar/></foo>

$sxe = new SimpleXMLElement('<foo></foo>');
$sxe->addChild('bar');
$sxe->bar = '';
echo $sxe->asXML(); // <foo><bar></bar></foo>
```

barの要素は最初だけがnull、2番目3番目は空文字列になるはずですが、出力XMLは何故か3番目だけが異なっています。

実はSimpleXMLだけではなく[DOM](http://php.net/manual/ja/book.dom.php)でも[発生します](https://3v4l.org/Of5p1)。

```php
$dom = new DOMDocument();
$dom->appendChild($dom->createElement("foo"));
echo $dom->saveXML(); // <foo/>

$dom = new DOMDocument();
$dom->appendChild($dom->createElement("foo", ""));
echo $dom->saveXML(); // <foo/>

$dom = new DOMDocument();
$dom->appendChild($foo = $dom->createElement("foo"));
$foo->textContent = "";
echo $dom->saveXML(); // <foo/>

$dom = new DOMDocument();
$dom->appendChild($foo = $dom->createElement("foo"));
$foo->nodeValue = "";
echo $dom->saveXML(); // <foo></foo>
```

実際のところこれが何か問題を引き起こすとは到底思えないし、そもそも[仕様上はほぼ同じ](https://www.w3.org/TR/REC-xml/#sec-starttags)なのでどちらでもいいのですが、やり方によって結果が異なるのは気持ち悪いですね。

ということで空要素タグに統一されました。
でも修正は[SimpleXMLにしか行われてない](http://git.php.net/?p=php-src.git;a=commit;h=692e5d5c88a939a7c3ce3de61c5fd39effe7c7ae)ように見えるんだけど大丈夫なのだろうか。

## [RegexIterator pregFlags are NULL instead of 0](https://bugs.php.net/bug.php?id=68175)

[RegexIterator::getPregFlags](http://php.net/manual/ja/regexiterator.getpregflags.php)が0を返すべき時に[nullを返す](https://3v4l.org/cRmui)バグ。

```php
$arr = new ArrayIterator([]); 
$regex = new RegexIterator($arr, '/^test/'); 

var_dump($regex->getMode(), $regex->getFlags(), $regex->getPregFlags()); // 0, 0, NULL
```

そもそも正規表現フラグってなんだよってかんじですが、[コンストラクタ](http://php.net/manual/ja/regexiterator.construct.php)の第5引数で渡すフラグです。
原因は[return;って書いてたから](http://git.php.net/?p=php-src.git;a=commitdiff;h=3257d22fea101453682941a9a74643e33c6f8ed5;hp=837ed71c65eaafc339f718cb02e7c0fe065e98d9)という単純なものだったので修正も1行で終わっています。

## [array_reduce leaks memory if callback throws exception](https://bugs.php.net/bug.php?id=76778)

[array_reduce](http://php.net/manual/ja/function.array-reduce.php)がメモリリークしているバグ。

```php
for ($i = 0; $i < 1000; $i++) {
    try {
        array_reduce(
            [1],
            function ($carry, $item) {
                throw new Exception;
            },
            range(1, 200000)
        );
    } catch (Exception $e) {
    }
}
```

原因は[参照解放してなかった](http://git.php.net/?p=php-src.git;a=blobdiff;f=ext/standard/array.c;h=4ba286e70ea99aed1bb6d4ff7d8435e45f2e92ce;hp=a1e3faa3ddc44b9def7bbee243409312c3c03c93;hb=70b2fca220fc27a16cc9f81f6c6da0f2a6d6d610;hpb=cf2fc66b0289dc7a34a0d9c0e67bccb8e97472bd)というよくあるやつです。

PHPはメモリ関連の話題をほぼ全く気にせずに使うことができますが、それはPHP本体が裏で確保や管理や解放を色々やってくれているからです。
全くありがたいことですね。


## [setcookie does not accept "double" type for expire time](https://bugs.php.net/bug.php?id=76755)

[setcookie](http://php.net/manual/ja/function.setcookie.php)の第三引数にlongを渡すと[E_WARNINGが出る](https://3v4l.org/CYpTo)バグ。

```php
setcookie('name', 'value', (real)(time() + 1296000));
```

![08.png](https://qiita-image-store.s3.amazonaws.com/0/26088/03237bfd-0e90-b0bc-bac4-4ab58ca2bed4.png)

7.3.0β1とβ2でだけ発生するというレアなバグです。

α4までは起こらなかったのにβ1で発生した理由はというと[Same Site Cookie](https://qiita.com/tadsan/items/96db7894e285743d996b)という[RFC](https://wiki.php.net/rfc/same-site-cookie)です。
しかしSame Site Cookie自体は原因ではなく、引数の与え方が変わったのが原因です。

元々setcookieは`setcookie($name, $value, $expires, $path, $domain, $secure, $httponly)`と引数を並べて与えていました。
しかし、ここからさらにSame Site Cookieフラグ引数を追加するととても残念なかんじになります。
そこでRFCでは、引数を配列でも与えることができるようにしようと提案されました。

RFCでは`setcookie($name, $value, $expires, $options)`と第4引数に配列を渡すようになっていましたが、実際はβ1で[第三引数に配列を渡す](https://3v4l.org/bVCdt)ことができるように[実装されました](https://github.com/php/php-src/commit/2b58ab23c6ad3301b31a2015f5faa31801147dfd)。

```php
setcookie('hoge', 'fuga', [
    'expires' => 3600,
    'path' => '/',
    'secure' => true,
    'httponly' => true,
    "samesite" => 'Strict',
]);
```

こっちのほうがわかりやすいのでよかったと思いますが、このときに引数の型の扱い方が変わったためエラーが出るようになりました。
そもそもexpiresの値はintなのでエラーが出る方が正しいと思うのですが、これまでの動作に合わせる方を選んだようです。


## [Error message should be more detailed](https://bugs.php.net/bug.php?id=76674)

配列関数のエラーメッセージがおかしいという指摘。

PHPのエラーメッセージはめったらやたら親切で、引数の型が合わない場合は`XX expects parameter 1 to be XX, XX given`のように[正しい型と与えられた型まで出して](https://3v4l.org/PpCj9)くれます。

![09.png](https://qiita-image-store.s3.amazonaws.com/0/26088/24f3cb41-c592-2d2d-c3f0-dfdfcaf492c3.png)

なのですが、何故か配列関数に限っては`Argument #1 is not an array`のように[メッセージが省エネ](https://3v4l.org/EGaIl)です。

![10.png](https://qiita-image-store.s3.amazonaws.com/0/26088/cb011b83-e296-4f30-278f-aee9bef86740.png)

ということで他のエラーメッセージに合わせるように[修正されました](http://git.php.net/?p=php-src.git;a=blobdiff;f=ext/standard/array.c;h=1049ec1fce617fe4b21a0c69ea60a6aa2f2df4f8;hp=187b7182a4ccdda7e41d9725f722aa44d00fa388;hb=efbf846f;hpb=52ea129efb7b49aaa5c49c43d886602ee0a8d878)。


# 感想

はっきり言って、普段書くようなコードには全く影響の無いようなバグがほとんどです。

しかし、そんな重箱の隅のようなものでも地道になおしてくれる人たちのおかげで、かつてはバグの宝庫と言われていたPHPも、非常に安定して運用できるまでに成長してきました。
誠にありがたいことです。

まあ、かつてのカオスだったPHPも嫌いじゃないですが、さすがに今となっては[10リクエスト毎にApache再起動](https://en.wikiquote.org/wiki/Rasmus_Lerdorf)というわけにもいかないでしょう。