Quantcast
Channel: PHP7.4タグが付けられた新着記事 - Qiita
Viewing all 113 articles
Browse latest View live

【PHP7.4】ついにPHPにプロパティ型指定がやってくる

$
0
0

Typed Properties 2.0というRFCが投票フェーズに入ったのですが、2018/09/13時点で賛成48反対0となっていて、ほぼ決まりの状態です。

Typed Properties 2.0

どういうRFCなのかというと、これです。

class User {
    public int $id;
    public string $name;

    public function __construct(int $id, string $name) {
        $this->id = $id;
        $this->name = $name;
    }
}

プロパティに型を強制することができるようになります。

導入

PHP7.4で導入される予定。

構文

class Example {
    // 対象型はvoidとcallable以外全て
    public int $scalarType;
    protected ClassName $classType;
    private ?ClassName $nullableClassType;

    // staticにも指定可能
    public static iterable $staticProp;

    // varにも使える
    var bool $flag;

    // デフォルト値を指定
    public string $str = "foo";
    public ?string $nullableStr = null;

    // 複数のプロパティを指定
    public float $x, $y;
    // ↑は↓と同じ
    public float $x;
    public float $y;
}

型指定したプロパティは、宣言した型を満たす値が入っているか、もしくはTypeErrorを発するかのいずれかになります。

使用可能な型

voidとcallable以外の全てが指定可能です。

bool, int, float, string, array, object
iterable
self, parent
任意のクラスおよびインターフェイス名
?type // null許容型

voidは意味がないうえ、PHPからvoid型の値を設定・取得する方法がありません(取得するとnullになる)。
そのため対象外となりました。

callableは実行可能かどうかがコンテキストによって決まるため、対象外とされました。

class Test {
    public callable $cb;

    public function __construct() {
        // ここからは呼べる
        $this->cb = [$this, 'method'];
    }

    private function method() {}
}

$obj = new Test;
// ここからは呼べない
($obj->cb)();

ちなみに、このあたりをどうにかしようというRFCが別途提出されています

strict_typesの影響

strict
declare(strict_types=1);

class Test {
    public int $val;
}

$test = new Test;
$test->val = "42"; // TypeError
not_strict
declare(strict_types=0);

$test = new Test;
$test->val = "42";
var_dump($test->val); // int(42)

せっかくの型宣言ですが、値は暗黙の型変換の影響を受けます。
つまり、intなフィールドに"42"を突っ込んでもTypeErrorが起こらず値は42になってしまいます。
厳密な型チェックを行いたい場合はstrict_typesを指定しましょう。

継承と分散

もちろん子クラスで型の変更はできません。

class A {
    private bool $a;
    public int $b;
    public ?int $c;
}
class B extends A {
    public string $a; // parent::$aはprivateなので別プロパティになる
    public ?int $b;   // NG
    public int $c;    // NG
}

親クラスのプロパティがprivateである場合は、単に別のプロパティになるだけなので異なる型を設定可能です。
とはいえ同じ名前で異なる型というのはわかりにくくなるだけなので、やめておいたほうが無難でしょう。

またstaticプロパティについては異なる型は禁止されます。

class A {
    public static bool $a;
}
class B extends A {
    public static int $a; // NG
}

本来これは別の値となるはずなのですが、遅延静的束縛などを使うと簡単に乗り越えられてしまうので禁止となりました。

trait T1 {
    public int $prop;
}
trait T2 {
    public string $prop;
}
class C {
    use T1, T2; // NG
}

トレイトも同名プロパティの異なる型指定はNGです。
insteadofやasを使えばいいということもなく、同時にuseしようとする時点で駄目なようです。

デフォルト値

通常のプロパティと同じくデフォルト値を指定できますが、当然ながら型に合わせないといけません。

class Test {
    // OK
    public bool     $a = true;
    public int      $b = 42;
    public float    $c = 42.42;
    public float    $d = 42; // 特別にOK
    public string   $e = "str";
    public array    $f = [1, 2, 3];
    public iterable $g = [1, 2, 3];
    public ?int     $h = null;
    public ?object  $i = null;
    public ?Test    $j = null;

    // デフォルト値を設定できない
    public object   $k;
    public Test     $l;

    // NG
    public bool     $m = 1;
    public int      $n = null;
    public Test     $o = null;
}

メソッドの型引数と同様、NULL許容型でないかぎりNULLは禁止されます。

object型はデフォルト値を設定する方法がないので、常に未定義となります。
Javaなどでは定義と同時にnewとか書けますが、PHPでは値や定数しか置くことができません。

デフォルト値をコンパイル時に評価できない場合、実行時に初めてチェックされます。
つまり以下のコードは許可されます。

class Test {
    public int $prop = FOO;
}

define('FOO', 42);
new Test;

define('FOO', 'hoge')などとした場合は実行時にTypeErrorが発生します。

初期値、unset()

デフォルト値を設定しない場合の初期値はnullではなく、未初期化という状態です。

class Test {
    public int $val;
}

$test = new Test();
var_dump($test->val); // TypeError

未初期化プロパティを参照するとTypeErrorが発生します。
安全な運用のためには、デフォルト値を書いておくか、コンストラクタで必ず設定するようにしておくべきでしょう。

またvar_dumpすると特別なuninitialized値になります。

object(Test)#1 (0) {
  ["val"]=>
  uninitialized(int)
}

print_rやvar_exportでどうなるかは書かれていませんでした。

オーバーロード

型指定プロパティをunset()すると、未定義ではなく未初期化状態に戻ります。
ちなみに通常のプロパティはunsetすると消滅します。

class Test {
    public int $typed;

    public function __construct() {
        unset($this->typed); // uninitializedになる
    }

    public function __get($name) {
        if ($name === 'typed') {
            return $this->typed = $this->computeValue(); // なんか値を入れる
        }
    }
}

$test = new Test;
var_dump($test->typed); // __getが呼ばれる
var_dump($test->typed); // 呼ばれない

未初期化プロパティはアクセス不能とみなされるので、いきなり参照しようとすると__get()が呼び出されます。
値を入れるとアクセスできるようになるので__getは呼ばれなくなります。

なお、プロパティに値を入れなかったとしても、__getは正しい型の値を返さなければTypeErrorになります。

class Test {
    public int $val;

    public function __get($name) {
        return "not an int";
    }
}

$test = new Test;
var_dump($test->val); // TypeError

これはわりと意外ですね。

間接的な型変更

PHPの暗黙型変換が悪さをすることもあります。

class Test {
    public int $x;
}

$test = new Test;
$test->x = PHP_INT_MAX;
$test->x++; // TypeError

PHP_INT_MAX+1はfloat型に自動型変換されるため、TypeErrorが発生します。

リファレンス

リファレンスは使うな!!!!

残念ながら型指定プロパティはリファレンスに対応しています。
RFCでは以下のようなコードが例示されています。

class Test {
    public array $ary = [3, 2, 1];
}
$test = new Test;
sort($test->ary);

sortの引数はリファレンス渡しなので、リファレンスが効かないとこのような書き方ができません。
ということなのだけど、そもそもこんな使い方するなって話だな。

リファレンスを使うと型合成みたいなことができます。

class Test {
    public ?int $i;
    public ?string $s;
}

$r = null;
$test->i =& $r;
$test->s =& $r;

ここで$rには?int型かつ?string型の値しか代入できないことになります。
?int型かつ?string型を満たす値はnullしかないのでこの例は役に立たないのですが、うまく使えば菱形継承みたいなこともできるかもしれません。
そんな奇天烈なことするなよ、絶対するなよ。

自動初期化

PHPでは未定義プロパティを参照すると該当プロパティが自動的に作成され、値がnullになります。

$test = new stdClass();
$b =& $test->a; // null 
var_dump($test); // $test->aが勝手に作られる

同様に、未初期化プロパティを参照すると値がnullになります。

class Test {
    public ?int $x;
    public int $y;
}

$test = new Test;
$x =& $test->x; // $text->x = nullになる
$y =& $test->y; // TypeError

$xはnullableなので未初期化参照可能ですが、$yはnull禁止なので参照した時点でTypeErrorが発生します。
ややこしいので、未定義プロパティは参照できないようにするべきでしょう。

リフレクション

ReflectionPropertyに3メソッドが追加されます。

class ReflectionProperty {
    public function getType(): ?ReflectionType;
    public function hasType(): bool;
    public function isInitialized([object $object]): bool;
}

getTypeは型指定のある場合ReflectionTypeを、なければnullを返します。
hasTypeは型指定があればtrue、なければfalseを返します。

isInitializedはプロパティが初期化されていればtrueです。
未初期化、およびunsetされたプロパティについてはfalseになります。
publicでない場合は先にsetAccessible(true)しないとReflectionExceptionが出ます。

文法について

2種類の文法が提案されました。

class Example {
    public int $num;
    public $num: int;
}

どちらの形式がよいか検討した結果前者になりました。
前者はCやJavaなどの古い言語に多く、後者はTypeScriptやRustといったモダンな言語に多い文法です。
ならば後者がいいのではないかと思いますが、PHPでは複数の変数を同時に宣言できます。

class Example {
    public int $x=1, $y=2, $z=3;
    public $x=1: int, $y=2: int, $z=3: int;
}

前者の方が楽ですね。

下位互換性

既存のPHPコードに影響が生じることはありません。

エクステンションへの影響

エクステンション開発側は以下の2点について対応が必要となります。

・write_propertyシグニチャの変更
・参照割り当ての修正

詳細も書かれていますが、使用する側にとっては特に関係ないので省略。
マクロが用意されているので使うと楽になるでしょう。

パフォーマンス

各フレームワークでの実測値が出ていますが、平均して1.7%程度遅くなるようです。
これは型指定を使用しなくても発生します。
むしろ型指定導入後に高速化しているqdigやdrupalはなんなんだ。
というか勝手に-を遅くなると考えていたのだが、これ単位がないからどっちが早いのかわからないな。

投票

2/3の賛成が必要になりますが、2018/09/13時点では賛成48反対0です。
余程重大な問題でも発覚しないかぎり、逆転されることはまずないでしょう。

感想

ようやく来たか、というかんじですね。

以前却下されたTyped Propertiesで考えが足りてなかったところを悉く潰して、周到に用意されてきたという印象です。
特にcallableや参照などのややこしいところについては、非常に細かいところまで動作とその理由が説明されています。
こんなに長いRFCは初めて見た気がします。
おかげで満場一致での採択となりました。

とはいえ使う側としては、初期化とリファレンス禁止だけ徹底していれば、奇妙な動作に惑わされることはなさそうです。

ここの解説は詳細をだいぶ端折っているので、細かい挙動が知りたい場合はRFCを直接見てください。
リファレンスの連鎖とか見るのも実装するのも嫌になるレベル。


【PHP7.4】PHPで簡単に永続プリロードできるようになる

$
0
0

PHPはHTTPリクエストが来るたびに全てのPHPコードをバイトコードに変換し、そして実行しています。
毎回そんなことやってるのにあれだけ速度が出るのは驚異的ですが、それでもやはりコンパイルにかかる時間だけどうしても遅くなってしまいます。

そこで、もっと高速化するためにOPcacheのような仕組みが存在します。
これはバイトコードをメモリ上に保持し、リクエストを超えて使い回すことでコンパイルの手間を省略し、高速化を実現するというものです。
効果はというと、単純なものでもターンアラウンドタイムが2/3、大きなフレームワークでは半分以下と、お手軽かつ強力な効果があります。

とはいえOPcacheには、元のPHPファイルに変更があるかどうかを監視したりといった僅かなコストが残っています。
特にバイトコードはファイル単位でしかキャッシュできないらしく、extendsなどで別のファイルを参照しているときは毎回再リンクしているようです。

そこで新たにPreloadingというRFCが提出されました。

提出というか既に投票フェーズに入っていて、投票期間は2018/11/14までですが、賛成46/反対0でほぼ導入確定です。

RFC: Preloading

書式

php.iniに新しいディレクティブ、opcache.preloadが追加されます。
これは単一のPHPファイルを指定し、Webサーバの起動時にこのファイルが読み込まれます。
その中でopcache_compile_file()を指定すると、そのファイルがキャッシュされます。

php.ini
opcache.preload="path/to/preload.php"
path/to/preload.php
opcache_compile_file('path/to/hoge.php');
opcache_compile_file('path/to/fuga.php');

これでサーバの再起動時にhoge.phpfuga.phpがキャッシュされます。
以後このサーバでは、その中で定義されているクラスや関数を、ネイティブ関数と同じようにいつでもどこでも自由に使うことができるようになります。

RFCにはZend Frameworkをまるごとプリロードする例が載っていました。

function _preload($preload, string $pattern = "/\.php$/", array $ignore = []) {
  if (is_array($preload)) {
    foreach ($preload as $path) {
      _preload($path, $pattern, $ignore);
    }
  } else if (is_string($preload)) {
    $path = $preload;
    if (!in_array($path, $ignore)) {
      if (is_dir($path)) {
        if ($dh = opendir($path)) {
          while (($file = readdir($dh)) !== false) {
            if ($file !== "." && $file !== "..") {
              _preload($path . "/" . $file, $pattern, $ignore);
            }
          }
          closedir($dh);
        }
      } else if (is_file($path) && preg_match($pattern, $path)) {
        if (!opcache_compile_file($path)) {
          trigger_error("Preloading Failed", E_USER_ERROR);
        }
      }
    }
  }
}

set_include_path(get_include_path() . PATH_SEPARATOR . realpath("/var/www/ZendFramework/library"));
_preload(["/var/www/ZendFramework/library"]);

注意事項

この方法で作成したキャッシュは、サーバが生きているかぎり永続します。
元のPHPファイルを変更してもキャッシュは更新されないし、たとえopcache_reset()を使ったとしても消えません。
更新する方法はサーバの再起動だけとなります。

つまり、OPcacheの通常の使用法では残っていた元ファイル監視などのコストを排除し、さらなる高速化を図ったものといえます。

制限

プリロードの恩恵を受けられるのは、親クラス・インターフェイス・トレイト・定数の定義をプリロードの範囲内で解決できるクラスだけです。
それが満たされないクラスは普通のOpcacheと同じようにopcache SHMに格納されます。
ってなってるんだけどSHMって何だ?
/dev/shmのこと?

下位互換性のない変更点

明示的に使用しないかぎり何も起らないので、何もしなければ影響はありません。

プリロードされたクラスや関数はfunction_exists()class_exists()に常にtrueを返します。

2種類以上のアプリケーションを走らせているサーバでは、うっかりPreloadingを使うと問題が起こる可能性があります。
2つのアプリでそれぞれ独自のh()関数を作っていたとして、片方をPreloadingに載せると、もう片方がCannot redeclareのFatal errorになります。

導入されるバージョン

PHP 7.4。

パフォーマンス

プリロード1000ファイル、通常ロード150個のテストで、ターンアラウンドタイムが36.4ミリ秒から29.1ミリ秒になりました。

感想

opcache.preloadで読み込んだ関数やクラスは、strlen()Exceptionといったビルトイン関数・クラスと同じように前準備なしで動くようになります。
つまり、これまではわざわざエクステンションを作らなくてはいけなかったものが、素のPHPでできるようになるということです。

そのかわり、それらは普通のPHPファイルのようにソースを変更しても変更が反映されることはなくなり、反映にはサーバの再起動が必要となります。
この点もエクステンションっぽいですね。

これが普及すれば、フレームワークの本体側はpreloadに載せておいて、ユーザによる変更部分のみ通常のPHPとして読み込む、といった運用が増えていくことでしょう。
バージョンアップごとにApache再起動させられるのは手間ですが。

【PHP7.4】PHPの新たな演算子??=ってなんぞ?

$
0
0

2019/01/22(JST)にImplement ??= operatorという謎のマージがありました。

RFC

RFCは賛成37、反対4の圧倒的多数で可決されています。

なお投票開始は2016/03/24で、終了が2016/04/02です。
つまり、それ以来3年弱ほったらかされていたということです。

??=ってなに?

RFCでは『Null Coalescing Assignment Operator』と呼ばれています。
どうも適切な日本語がないみたいなのですが、NULL合体演算子(Null Coalescing Operator)から類推すると『NULL合体代入演算子』とかになるんですかね?

名前のとおり、NULL合体演算子と代入演算子を合わせたような演算子です。

使い方

    // NULL合体代入演算子
    $id ??= getId();

    // これと同じ
    $id = $id ?? getId();
    $id = @$id ?: getId();
    $id = isset($id) ? $id : getId();

三項演算子を省略したエルビス演算子を省略したNULL合体演算子を省略したものがNULL合体代入演算子ということですね。

上記の例ではあまり恩恵を感じられませんが、左辺が長くなりがちなフレームワークでは利点が多くなることでしょう。
RFCのサンプルでは以下のような例がありました。

    // これまで
    $this->request->data['comments']['user_id'] = $this->request->data['comments']['user_id'] ?? 'value';

    // 今後
    $this->request->data['comments']['user_id'] ??= 'value';

これは確かに便利。

か?

導入

PHP7.4で導入されます。

他言語での実装

C#では2019年リリース予定のC# 8.0導入されるようです。

Rubyでは、||の自己代入演算子である、||=というイディオムが近い動きをします。
(左辺がnilまたはfalseの場合に、右辺を代入。falseも含むため、厳密には違う動きです。)

それ以外の言語では全く見当たりませんでした。

感想

データが入ってくるかどうかわからないというWeb出自のPHPらしい演算子だと思います。
他言語であればもっと入力をちゃんと定義しろって話ですが、Web APIなんかだとそういうことも言ってられませんからね。

ただ自分で使いたいかというとちょっと微妙ですね。
ただでさえ短縮構文多いのにこれ以上増えたら混乱しそうです。
まあ、?:とか??とかもいつの間にか普通に使っているので、そのうち??=も普通に使うようになっているかもしれませんが。
でも<=>は使う機会が無い。

PHP7.4の新機能

$
0
0

PHP7.4.0その2 / PHP7.4.0その1 / PHP7.3.0 / PHP7.3.0α1

PHP7.4で実装される新機能を紹介してみます。
7.3以上の大きな変更が複数導入されることになっていて、大丈夫なのかこれ。

RFC

Foreign Function Interface

賛成24、反対15で受理。

$ffi = FFI::cdef("
    typedef unsigned int time_t;
    typedef unsigned int suseconds_t;

    struct timeval {
        time_t      tv_sec;
        suseconds_t tv_usec;
    };

    struct timezone {
        int tz_minuteswest;
        int tz_dsttime;
    };

    int gettimeofday(struct timeval *tv, struct timezone *tz);    
", "libc.so.6");

$tv = $ffi->new("struct timeval");
$tz = $ffi->new("struct timezone");

var_dump($ffi->gettimeofday(FFI::addr($tv), FFI::addr($tz)));

なんだこの文法は!
phpでマークアップされないぞ!

まあ当然で、これはCのコードです。
FFIとはつまり、PHPで別言語を動かすという機能です。
これによってC言語のネイティブ機能に直接アクセスすることが可能になります。
またPHPで書くより高速になるみたいです。

元々PECLには存在していたのですが、最終更新が15年前で死亡確認済です。
今回はdstogovによるdstogov/php-ffiをPHP本体に入れるという提案です。
半数以上の賛成で可決されるという投票ルールであったため、多数の懸念にもかかわらず可決されました。

両言語に精通していないといけないため、なかなか使いこなすのが難しそうな機能です。
私はもちろん使えません。

Typed Properties 2.0

賛成70、反対1で受理。
プロパティ型指定です。

class User {
    public int $id;
    public string $name;

    public function __construct(int $id, string $name) {
        $this->id = $id;
        $this->name = $name;
    }
}

このRFC、受理されたあと一度Pending行きになって先行きが不安視されていたのですが、どうやら問題は解決されたようで、無事masterにmergeされました。

Null Coalescing Assignment Operator

賛成37、反対4で受理。
NULL合体代入演算子です。

$this->request->data['comments']['user_id'] ??= 'value';

Preloading

賛成48、反対0で受理。
プリローディングです。

php.ini
opcache.preload="path/to/preload.php"
path/to/preload.php
opcache_compile_file('path/to/hoge.php');
opcache_compile_file('path/to/fuga.php');

preload.phpから解決できる関数やクラスが、ネイティブ関数と同じようにいつでもどこでも使えるようになります。

Always available hash extension

賛成30、反対0で受理。

HASHエクステンションを常に有効にしよう、という提案。
このエクステンションにはhash_equalshash_hmacといったハッシュ関連で重要な関数が含まれています。
これまではインストール時に--disable-hash無効化できたのですが、今後は無効化することができなくなります。

この要請はどこから来たのかというと、MySQLの認証系で必要になったからみたいです。

Password Hash Registry

賛成21、反対0で受理。

上のRFCと同じHashなんたらという名前ですが、こちらはpassword_hashに関するもので、特に関係ありません。
エクステンションが独自のハッシュアルゴリズムを追加できるようにするRFCです。

  PHPAPI int php_password_algo_register(const char* ident, const php_password_algo*);
  PHPAPI void php_password_algo_unregister(const char* ident);
  PHPAPI const php_password_algo* php_password_algo_default();
  PHPAPI zend_string *php_password_algo_extract_ident(const zend_string* hash);
  PHPAPI const php_password_algo* php_password_algo_find(const zend_string* ident);
  PHPAPI const php_password_algo* php_password_algo_get_named(const zend_string* name);
  PHPAPI php_password_algo* php_password_algo_identify(const zend_string* hash);

見てのとおりC言語で、PHP側で特にどうこうするものではありません。
もしかしたらFFIでどうにかできるのかも?

PHP側にはpassword_algos関数がひとつ追加され、現在有効なハッシュアルゴリズムの一覧を取得することができるようになります。

  > print_r(password_algos());
  Array (
      [0] => "2y" // Ident for "bcrypt"
      [1] => "argon2i"
      [2] => "argon2id"
  )

またPASSWORD_DEFAULTなどの定数は現在int型ですが、文字列型に変更されます。
正しい実装をしているかぎりは、何も変更する必要はありません。

Improve openssl_random_pseudo_bytes

賛成30、反対0で受理。

openssl_random_pseudo_bytesを改善する提案。
openssl_random_pseudo_bytesはfalseを返すことがあり、さらに第二引数$crypto_strongもfalseを返すことがあります。
つまり、openssl_random_pseudo_bytesの正しい使い方は以下のようになります。

    $strong = false;
    $bytes = openssl_random_pseudo_bytes(32, $strong);
    if (false === $bytes || false === $strong) {
        throw new \Exception('CSPRNG error');
    }
    return $bytes;

まあ面倒ですね。
ということでopenssl_random_pseudo_bytesは今後、random_bytesと同じように失敗時はExceptionを出すようになります。

ただし第二引数$crypto_strongをDeprecateにする提案は賛成12反対12で却下されました。
こっちも例外でいいと思うんですけどね。

mb_str_split() Split multibyte string

賛成10、反対1で受理。
str_splitのマルチバイト版、mb_str_splitを導入する提案。
これでうっかりstr_splitに日本語を突っ込む事故を防げますね。

それにしてもmb_splitとかpreg_splitとかsplitとか似たような関数が多くてつらい。

Reflection for references

賛成30、反対1で受理。

リファレンスかどうかを判断するリフレクションがほしい、という提案。

たとえばsymfony/VarClonerは、リファレンスか否かの判定を以下のように行っています。

$array2 = $array1;
$array2[$key] = $unique_cookie;
if ($array1[$key] === $unique_cookie) {
  // リファレンスだった
}else{
  // リファレンスではなかった
}

なんというかアレですね。

ということでリファレンスを表すReflectionReferenceを追加します。

final class ReflectionReference {
    /** @return ?ReflectionReference リファレンスであればReflectionReference、そうでなければnull */
    public static function fromArrayElement(array $array, int|string $key): ?ReflectionReference;

    /** @return int|string 特定するIDを返す */
    public function getId(): int|string;

    private function __construct(); // Always throws
    private function __clone(); // Always throws
}

getIdはひとつの参照に対して一意なIDを返すメソッドで、spl_object_hashのようなものです。
複数の参照が同じオブジェクトを指しているかを判別するなどの用途に使うようです。

ユーザ側としてはそんなに必要な機能でもありませんが、テストライブラリにとっては非常に役立つものだと思われます。

その他

Abolish Narrow Margins

FFIが可決されてしまった衝撃は大きかったらしく、提案されたまま長らく放置されていた、あらゆる変更に2/3の賛成を必要とするRFCが速攻で投票され賛成30反対2の賛成多数で可決されました。

これまではPHPコアの変更や互換のない変更などは2/3の賛成、それ以外の些細な変更などは50%+1の賛成で受理されていましたが、今後はあらゆるRFCが2/3の賛成を必要とするようになります。

もしこのRFCがもっと早く受理されていたら、array_key_firstPDOStatement::activeQueryStringも実装されていなかったかもしれませんね。

感想

FFI、プロパティ型指定、??=、プリローディングと重量級の変更が幾つも入ってきていて、スケジュールとか大丈夫なのか心配になりますね。
特にFFIは反対も多いだけに(導入自体に反対ではなく、時期尚早という意見が多い)先行き不透明で、PHP7.4が来ても使用は待った方がよさそうです。

いやまあそれ以前に、能力的な問題でFFIを使いきれそうにないですが。

PHP7.4ではPEARがインストールされなくなる

$
0
0

Disable PEAR

2019/02/01にDisable PEAR by defaultというプルリクが提出され、2019/02/11にマージされました。

commitからmerge日はどうすればわかるのだろう?

内容

Installation of PEAR (including PECL) is no longer enabled by default. It can be explicitly enabled using --with-pear. This option is deprecated and may be removed in the future.

PEAR(PECLを含む)は、デフォルトではインストールされなくなりました。
--with-pearで明示的に有効にしなければなりません。
このオプションは非推奨であり、将来削除される可能性があります。

これまではPHPをインストールすると自動的にPEARも入ってきていましたが、PHP7.4以降は--with-pearを明示しないかぎりインストールされなくなります。
また--with-pearオプションも非推奨で、今後削除される可能性が有り、削除されるとPHPと一緒にPEARをインストールする方法がなくなります。

もちろんPEARが一切使用できなくなるわけではなく、PHPのインストール後にgo-pear.pharなどを使用して別途インストールすることは可能です。
Composerと同じように

メーリングリスト

プルリクと一緒にメーリングリストで話が始まりました。
提案者のNikita PopovはPHPのコア開発者で、プロパティ型指定とかジェネレータとか作ってるすごい人で、最近JetBrainsに入りました

そもそもPEARはもはやまともにメンテされておらず、2017/11/30リリースのPHP7.2で非推奨になったeach削除されたのは、2018/08/23リリースのv1.10.6create_functionに至っては2018/12/06です。
そんな長期間誰も気付かないほど誰も使ってないんだからもう要らんじゃろ、みたいなことをNikitaは言っていて、概ね同意のうえ10日後にマージされました。
プルリク者本人がマージするのはどうなんだって気がしないでもない。

PEARはともかくPECLがなくなるのは惜しいという意見はいくつかありましたが、個別に入れられますし、Pickleもあるので欲しい人には各自でやってもらいましょう。

RFC

Deprecate Bundling PEAR/PECL & Replace with composer/pickleというRFCがあるのですが、これは完全にスルーされています。
PEARの排除だけ、RFCも立たずにさっくりと行われました。
Composerのバンドルについては行うつもりはないようです。

感想

正直もうPEARは仕方ないよね。

先日のバックドアの件も、2019/01/19にシャットダウンしたサーバが復旧したのが2019/02/25で、公約してた後日の報告も2019/03/25時点で影も形もないとかいうね。

Recent Releasesを見てみると一応月に数本は更新されてはいるのですが、Composer全盛の今にあえてPEARを使う意義はほぼ感じられません。

【PHP8】short_open_tagにさよなら、しないかも?

$
0
0

Deprecate PHP Short open tagsというRFCが投票フェーズに入りました。
投票期間は2019/04/10から2019/04/24、採択には投票数の2/3+1の賛成が必要です。

Deprecate PHP Short open tags

Short open tagsとは

PHPの開始を示すタグは<?php、もしくは<?=です。
後者は<?php echoとほぼ同じであり、今回のRFCでは一切影響を受けません。

php.iniの設定でshort_open_tagを有効にすると、PHPの開始タグを<?と省略して書けるようになります。

// 通常
<?php
    echo 'hoge';

// short_open_tag=1ならこう書ける
<?
    echo 'hoge';

Short open tagsのデフォルト値

PHP7.3時点では1、つまり初期設定で有効になっている。
ただし、多くの実装系(XAMPPとか)では0と無効にされているようだ。

Short open tagsの問題点

うっかりXMLをコピペすると死ぬ。

<?xml version="1.0"?>
<?php
    echo 'XMLの中身';

このコードは、short_open_tagが有効な場合syntax errorのE_PARSEになります。
short_open_tagが無効であれば普通にXMLが出力されます。

このように、設定によって動作が根本的に変わってしまうので、あまりよろしくありません。

Remove alternative PHP tags

PHP5時代は、これ以外にも<%<script language="php">といった記述でPHPに入ることが可能でした。
それらはPHP7移行の際に、Remove alternative PHP tagsのRFCで削除されました。

が、なぜか<?だけは残ったままでした。

RFC

<?をPHP7.4でDeprecatedにし、PHP8で削除しよう、という提案です。
これによってPHPの開始タグは<?php<?=の2種類だけになり、とてもすっきりします。

プルリク

PHP7.4ではshort_open_tagのデフォルト値を1から0に変更します。
また有効にするとDirective 'short_open_tag' is deprecatedのE_DEPRECATEDが発生するようになります。

PHP8では設定項目そのものが削除され、有効にすることができなくなります。

internal

「え?short_open_tag削除して常に有効になるんじゃないの?」
「わざわざ削除するメリットが見当たらない」
「今PHP7で書いてる人はほぼ影響受けないだろうけど、未だにPHP5の人はアプデの障壁がさらに高くなるよ。」
「Facebookでアンケ取ってみたら96%が削除賛成だったよ」
<?=が削除されないんだったらかまわない」

投票

2019/04/12時点では、PHP7.4でのDeprecateは賛成17反対9で、このままだと却下されます。
PHP8でのRemoveは賛成19反対9で、このままだと受理されます。

感想

PHP7.4でのDeprecateが却下され、かつPHP8でのRemoveが受理されると、それまで使えてたのにPHP8でいきなり削除ってなるんだけどどうなんだろう。
その場合はPHP8でDeprecateになるのかな?

個人的には<?は全く使ってないので消してもらって全くかまわないんだけど、その結果vendor配下からDeprecatedが大量発生ってなったりすると困りますね。
かつてPEARでもAssigning the return value of new by reference is deprecated湧き潰しできず死ぬという事件がありましたが、それの再来にならないことを祈りましょう。

【PHP7.4】PHPでアロー関数を使えるようになる

$
0
0

かつて、提出されたもののいつのまにか取り下げられていたArrow FunctionsというRFCがありました。

    // $x*2を返す関数
    $mul2 = fn($x) => $x * 2;

    // 使い方
    $mul2(3); // 6

が、なんかV2として復活してました
しかも今回は提案者として重鎮Nikitaが参戦しています。
NikitaはPHPのコア開発者のひとりで、記憶に新しいところではプロパティ型指定を作った人です。

既にコードもできていてプルリクが出されています。

しかしRFCの提出が2019/03/12で、投票は2019/04/17開始・2019/05/01終了です。
なんでそんなスケジュールきつきつなのだ。

RFC Arrow Functions 2.0

Introduction

PHPの無名関数は、単純なことしか行わない場合でもやたら冗長になる場合があります。
使用する変数を毎回手動でインポートしなければならないなど、構文に定型句が多いためです。
このせいで、簡単なクロージャですら読みづらいものとなってしまいます。
このRFCでは、上記の問題に対してより簡潔な構文を提案します。

オーバーヘッドの例として、オンラインで見つけた関数を考えてください。

    function array_values_from_keys($arr, $keys) {
        return array_map(function ($x) use ($arr) { return $arr[$x]; }, $keys);
    }

クロージャが行っている$arr[$x]はたいして難しい内容ではありませんが、定型句のせいで少々わかりづらくなっています。
アロー関数を使うと以下のように書けるようになります。

function array_values_from_keys($arr, $keys) {
    return array_map(fn($x) => $arr[$x], $keys);
}

かつて提出された~>を使うRFCは投票の結果却下されました。
このRFCは、却下されたRFCにおいて持ち上がったいくつかの懸念を解消しています。

このRFCではさらに、様々な代替構文についての詳細な説明も含まれています。
残念ながら、クロージャの短縮構文については、構文や実装に対する制約上の問題で、完璧な解決策を見つけることはできそうにありません。
このRFCでは、その中でも一番ましだと思われる選択をしています。
クロージャ短縮構文の議論は完全に停滞しているため、今後何年も塩漬けしておくよりも、ここで妥協した方がよいでしょう。

Proposal

アロー構文の基本形は以下のようになります。

    fn(parameter_list) => expr

式中で使用されている変数が親スコープ内で定義されている場合、暗黙的に値渡しされます。
次の例では、$f1$f2は同じ動作になります。

$y = 1;

$fn1 = fn($x) => $x + $y;

$fn2 = function ($x) use ($y) {
    return $x + $y;
};

ネストもできます。

$z = 1;
$fn = fn($x) => fn($y) => $x * $y + $z;

外側の関数fn($x)は暗黙的に変数$zを取り込みます。
内側の関数fn($y)も暗黙的に変数$zを取り込みます。
全体として、$zは内側の関数からでも利用可能です。

Function signatures

アロー構文では、引数と戻り値の型、デフォルト値、可変長引数、リファレンスなど、既存の関数と同じ機能を使用できます。
以下は全て有効なアロー関数の構文です。

    fn(array $x) => $x;
    fn(): int => $x;
    fn($x = 42) => $x;
    fn(&$x) => $x;
    fn&($x) => $x;
    fn($x, ...$rest) => $rest;

$this binding and static arrow functions

通常のクロージャ同様に、クラスメソッド中では$this、遅延静的束縛が自動的にバインドされます。
通常のクロージャではstaticキーワードでこれを打ち消すことができるため、アロー関数でも同じ機能をサポートしています。

class Test {
    public function method() {
        $fn = fn() => var_dump($this);
        $fn(); // object(Test)#1 { ... }

        $fn = static fn() => var_dump($this);
        $fn(); // Error: Using $this when not in object context
    }
}

staticクロージャは滅多に使用されません。
これは主に、GCの発生を予測しづらくする$thisのバインドを防ぐために使用されます。
しかし、ほとんどのコードはこのようなことを意識する必要はありません。

この動作について、実際にクロージャ内部で$thisが使用されている場合のみ$thisをバインドするように実装することは可能です。
GCを置いておいても、この変更によって動作が変わることはありません。

残念ながら、PHPは暗黙のうちに$thisを使用することがあります。
たとえば、Fooと互換性のあるスコープからFoo::bar()を呼び出した場合、$thisが継承されます。
我々は内部仕様を知っているため$thisがどうなるか分析できますが、ユーザにとっては$thisがどうなるかは予測不可能です。
そのため、常に$thisを使用する通常のクロージャを使うことを勧めます。

By-value variable binding

既に述べましたが、アロー関数は変数束縛を値渡しで行います。
これは、関数内で使用している全ての変数にuseをすることとほぼ同じです。
従って、値渡しバインディングの値をスコープ内から変更することはできないということになります。

    $x = 1;
    $fn = fn() => $x++; // スコープ外には影響しない
    $fn();
    var_dump($x); // 1

バインディング方法の検討については、discussionセクションを参照してください。

変数の暗黙的な使用と明示的な使用には僅かな違いがあります。
暗黙的な使用では、変数未定義のE_NOTICEは、変数バインド時には発生しません。
すなわち、以下のコードで発生するエラーは、$undefをバインドするときと使用するときの2カ所ではなく、使用するときの一カ所だけです。

    $fn = fn() => $undef;
    $fn();

その理由としては、変数が読み取りのために使用されているのか、参照返り値のために使用されているのかを常に判断することができないからです。
次にやや人為的な例を示します。

$fn = fn($str) => preg_match($regex, $str, $matches) && ($matches[1] % 7 == 0)

この$matchesはpreg_matchによって代入されるので、アロー関数より前に$matchesが存在している必要はありません。
この場合はエラーを出力すべきではありません。

最後に、この自動バインドは文字列リテラルで渡された変数にのみ行われます。
すなわち、以下のコードでは関数内で$xというリテラルが使用されていないため、$xが未定義であるというE_NOTICEが発生します。

    $x = 42;
    $y = 'x';
    $fn = fn() => $$y;

この問題は、使われている変数だけをバインドするのではなく、全ての変数を束縛することによって解消することができます。
不要だと思われるためここでは対応していませんが、それが必要だという声が大きければ変更されるかもしれません。

Precedence

=>の優先順位は最も低くなります。
すなわち、=>の右側は全て=>の中になります。

    fn($x) => $x + $y;

    // ↑と同じ
    fn($x) => ($x + $y);

    // こうではない
    (fn($x) => $x) + $y;

Backward Incompatible Changes

残念ながら、fnはトップレベルのキーワードでなければならず、そしてfnは予約済ではありません。

Ilija ToviloがGitHubのトップ1000リポジトリを調査してfnの使用を調査しました。
概要として、fnの使用は全てテスト内であるか、名前空間内での使用でした。

Examples

以下の例は旧RFCからのコピペです。

silexphp/Pimple

// 現在
$extended = function ($c) use ($callable, $factory) {
    return $callable($factory($c), $c);
};

// アロー関数
$extended = fn($c) => $callable($factory($c), $c);

定型文が44文字から8文字に減りました。

Doctrine/DBAL

// 現在
$this->existingSchemaPaths = array_filter($paths, function ($v) use ($names) {
    return in_array($v, $names);
});

// アロー関数
$this->existingSchemaPaths = array_filter($paths, fn($v) => in_array($v, $names));

31文字から8文字に減りました。

多くのライブラリで見られる関数も、以下のように書けるようになります。

// 現在
function complement(callable $f) {
    return function (...$args) use ($f) {
        return !$f(...$args);
    };
}

// アロー関数
function complement(callable $f) {
    return fn(...$args) => !$f(...$args);
}

// 現在
$result = Collection::from([1, 2])
    ->map(function ($v) {
        return $v * 2;
    })
    ->reduce(function ($tmp, $v) {
        return $tmp + $v;
    }, 0);

echo $result; // 6

// アロー関数
$result = Collection::from([1, 2])
    ->map(fn($v) => $v * 2)
    ->reduce(fn($tmp, $v) => $tmp + $v, 0);

echo $result; // 6

Discussion

Syntax

おそらく最も望ましいアロー関数の構文は($x) => $x * $y、もしくは$x => $x * $yです。
非常に簡潔で、JavaScriptなどの多くの言語で使用されています。
しかしPHPにおいてこの構文は問題が多く、使用することは困難です。
このセクションではアロー関数のために考慮されたいくつかの構文について、利点と欠点を解説します。

($x) => $x * $y

最も一般的であり、そしてPHPでは不可能な構文です。
この構文最大の問題が、配列やyieldなどにおいてキーと値のペアを指定する目的で=>が既に使用されていることです。
従って、以下のコードの=>は配列宣言なのか、アロー関数なのかを特定することができません。

$array = [
    $a => $a + $b,
    $x => $x * $y,
];

しかし、この曖昧さ自体は問題ではありません。
式の構文は既に曖昧さに満ちていますが、優先順位、結合度、あるいはその他の規則によって一意に解決できます。
今回の場合は、後方互換性を保つため、上記の構文は配列宣言であり、アロー関数は以下のようにする、と定義すれば一意にすることはできます。

$array = [
    ($a => $a + $b),
    ($x => $x * $y),
];

同じ問題がyieldにも存在します。

yield $foo => $bar; // 配列
yield ($foo => $bar); // アロー関数

実はアロー関数がなかったとしても、既に解釈が曖昧になる構文は存在します。

$array = [yield $k => $v];
// こうなる
$array = [(yield $k => $v)];
// こう解釈することもできる
$array = [(yield $k) => $v];

$x => $yには、さらに次のセクションで解説する問題もあり、そして実はそちらが致命傷です。

($x) ==> $x * $y

Hackが使用している($x) ==> $x * $y、以前のRFCで提出された($x) ~> $x * $y、あるいは類似の構文についてです。
($x, $y) ==> $x + $yのような単純な構文であればサポートも可能ですが、==>の左側に任意の構造を記述できるようにすると、パーサの実装が非常に困難になります。

($x = [42] + ["foobar"]) ==> $x; // 代入式
(Type &$x) ==> $x;               // 定数とビット積

$a ? ($b): Type ==> $c : $d;

($a = ($a = ($a = ($a = ($a = 42) ))))
($a = ($a = ($a = ($a = ($a = 42) ==> $a))))

以下構文のパース処理がとってもたいへんで、解決するにはパーサを入れ替えるレベルの改修が必要だ的な記述が延々書かれているのですが、細かすぎてよくわからないので省略。

fn($x) => $x * $y

上記の構文候補に関する最大の問題は、(の対となる==>が見つかるまではアロー関数であるかそうでないかを調べ続けなければならない、ということでした。
明確な解決策は、特有の先行記号を持つように構文を修正することです。
このRFCでは、短くて読みやすい候補としてfnを提案しています。
問題点は、fnが予約語ではないことです。

先行記号のシンボルについては、もちろん他の候補もあります。
特に未使用の単項演算子というパンドラの箱を開けたら。

function($x) => $x * $y
fn($x) => $x * $y
\($x) => $x * $y
^($x) => $x * $y

*($x) => $x * $y
$($x) => $x * $y
%($x) => $x * $y
&($x) => $x * $y
=($x)=> $x * $y

// NG PHPとして正しい構文である
!($x) => $x * $y
+($x) => $x * $y
-($x) => $x * $y
~($x) => $x * $y
@($x) => $x * $y

// NG _は正しい関数名である
_($x) => $x * $y

実際に実現可能であろう文法は最初の4例でしょう。

fnは今回提案されているものです。

fucntionは既にキーワードとして使用されているので安全です。
不利な点はキーワードが長いということで、そしてアロー関数のセールスポイントのひとつが短いということです。

\($x) => $x * $yはHaskellのラムダ構文と類似の例で\はプアマンズλです。

^はC言語でサポートされています。

先頭に記号が付いた構文を使うとなれば、=>も邪魔なので消したくなります。
fn($x) ⇒ $x * $yのかわりにfn($x) $x * $yとは書けないでしょうか?
残念ながら、返り値が曖昧になるためこれは不可能です。

    fn($x): T \T \T
    // こう?
    fn($x): T\T (\T)
    // それともこう?
    fn($x): T (\T\T)

名前空間の空白サポートをやめ、名前空間は単一トークンと字句解析すれば、この曖昧さは解決可能です。
しかし、それは破壊的変更になってしまいます。

Using -> and --> as arrows

=>のかわりに->-->を使おうという提案がありました。
しかしこれらは別の既存構文と衝突します。
->はプロパティアクセスで既に使用されています。

($x) -> $x
// 正しい構文。↓と同じ
$x->{$x}

-->-->の組み合わせです。

$x --> $x
// 正しい構文。↓と同じ
$x-- > $x

必ず括弧を使うと定義した場合、-->は使用可能です。
($x)--は現在のところ正しい構文ではないからです。

いずれの記号も先行記号と組み合わせれば使用可能ですが、そうするのであれば=>でも同じことです。

Different parameter list separators

Rustなどいくつかの言語は、クロージャのパラメータに異なる種類のセパレータを使います。

|$x| => $x * $y

|は有効な単項演算子ではないため、先行記号と同じ区別に使用できます。
しかし残念ながら、|はUnion typesやビット演算子、デフォルト値などで使用されています。

|T1|T2 $x = A|B| => $x

構文上の曖昧さはないと思われますが、意味を読み取るのは困難です。
また|$x|のような構文はPHPとしては異質でしょう。

Block-based syntax

これまでに解説したものとは全く異なるアプローチとして、RubyやSwiftで使用されているブロックベース構文があります。
考えられる構文は以下のようなものです。

{ ($x) => $x + $y }

先頭に{がありますが、PHPは独立ブロックとしての{の使用をサポートしているので、先行記号としては使えません。
以下は正当なPHPコードです。

{ ($x) + $y };

すなわち、構文解析の発散問題がやはり発生することを意味します。
ただし、この場合は簡単な回避策があります。
すなわち、式文としてのクロージャ構文を禁止することです。
単独のクロージャは許可されず、何らかの式の一部である必要があるということにします。

{ ($x) => $x + $y }; // NG
$fn = { ($x) => $x + $y }; // OK

これでブロックベース構文は汎用的に実行可能になります。
個人的には、これがfn()より優れているとは思えず、特にアロー関数をネストする場合はさらにややこしくなります。

fn($x) => fn($y) => $x * $y
{ ($x) => { ($y) => $x * $y } }

C++ syntax

C++11では、以下の構文でラムダを使用可能です。

cpp
[captures](params){body}

PHPの場合は[$x]($y)が既に有効な構文であるため、この構文は使用できません。

Miscellaneous

Haskellの構文に近い、パラメータを括弧で囲まない\param_list => expr構文も考えられました。
が、PHPではこの構文は曖昧です。

[\T &$x => $y]
// こう?
[\(T &$x) => $y)]
// それともこう?
[(\T & $x) => $y]

Binding behavior

アロー関数に関するもうひとつの議論点はバインディングです。

アロー関数は親スコープに存在する変数を自動的にバインドします。
問題はそのバインドをどのように動作させるべきかです。
基本的に3つの選択肢があり、それぞれを値・参照・変数によるバインディングとします。

$x = 1;
$fn = fn() => $x++;
$fn();
var_dump($x); // 値渡しなら1
              // 参照渡しなら2

値によるバインドはuse ($x)に、参照によるバインドはuse (&$x)に相当します。
参照バインディングの利点はアロー関数内で変数を変更できることです。

残念ながら、2つの大きな問題のため、参照バインディングが値バインディングより優れているとは言えません。
ひとつめは、参照バインディングのためには参照ラッパーの作成と参照が必要となるため、パフォーマンスが低下するということです。
アロー関数と通常クロージャを選ぶときの基準が性能差であるとなれば、それはとても残念なことです。

もうひとつ重要な問題が、クロージャの内側から外側の変数を変更することができるのと同時に、クロージャの内側の変数を外側から変更することもできるということです

次の例では、なぜこれが問題になるのか、暗黙の参照バインディングがいかに直感的でない結果になるのかを説明します。

$range = range(1, 5);
$fns = [];
foreach ($range as $i) {
    $fns[] = fn() => $i;
}
foreach ($fns as $fn) {
    echo $fn();
}
// 値:   1 2 3 4 5
// 参照: 5 5 5 5 5
// 変数: 5 5 5 5 5

アロー関数が値バインドである場合、全て正常に動作します。

参照バインドの場合、以下のように動作します。
最初のループでクロージャ内の$iがforeach内の$iへの参照にバインドされます。
2回目のループで、この参照先の値が上書きされ、さらに新しいクロージャで$iにバインドされます。
ループが終了した後、全てのクロージャはひとつの参照を共有します。
そして値は最後に割り当てられた値です。

今回は議論されておらず、PHPでは使用されていない3番目のバインドが、変数バインドです。
これこそが本当のスコープバインディングで、クロージャ外の変数とクロージャ内の変数は共有されています。
これは参照バインドと同じように見えますが、次の例が示すように全く同じではありません。

$range = range(1, 5);
$fns = [];
foreach ($range as &$i) { // &を追加
    $fns[] = fn() => $i;
}
foreach ($fns as $fn) {
    echo $fn();
}
// 値:   1 2 3 4 5
// 参照: 1 2 3 4 5
// 変数: 5 5 5 5 5

参照バインドをリファレンス付きでforeachすると動作が変わります。
参照によるforeachは、値の代入ではなく、参照の代入を実行します。
これにより、前ループの参照関係は解除されます。
すなわち、各クロージャは対応する配列要素を参照する個々の参照を取得することになります。

変数バインドを使用した場合、foreachの呼び出し方は問題になりません。
外側の$iとクロージャ内の$iは文字どおり完全に同じなので、クロージャが呼び出された時点の最終的な$iの値だけが意味を持ちます。

変数バインドはPHPでは実装が難しく、値バインドと同程度の性能を出すことは不可能かもしれません。

このような問題があるため、PHPで実装可能な唯一のバインディング形式は値バインドであると私は考えています。
ただし、特に下記のブロックベース構文がサポートされたような場合は、以下のように明示的に参照バインドを許可すると有用かもしれません。

$fn = fn() use(&) {
    // ...
};

Future Scope

将来は以下のように拡張される可能性がありますが、必ずしも推奨されるわけではありません。

Multi-statement bodies

このRFCでは、アロー関数は暗黙的に返される式をひとつだけしか持つことができません。
しかし、他の言語ではブロック形式で複数の式を受け入れるのが一般的です。

fn(params) {
    stmt1;
    stmt2;
    return expr;
}

この構文は優先価値が低いため、このRFCでは省略されています。
この構文をサポートする利点は、単一の式を使用するか複数のステートメントを使用するかによって2つの異なる構文を使い分けるのではなく、常に単一のクロージャ構文を使用できるようになることです。

Switching the binding mode

デフォルトでは値バインドを使用しますが、参照バインドもできるように構文を拡張することもできます。
複数ステートメントを使用する際の本文は、外側の変数を変更することに興味がある可能性が高いので、Multi-statement bodiesと組み合わせると特に役立つでしょう。
構文は以下のようになると思われます。

$a = 1;
$fn = fn() use(&) {
    $a++;
};
$fn();
var_dump($a); // int(2)

あるいは、基本的に値バインドを維持し、参照バインドしたい変数は明示することです。

$a = 1;
$b = 2;
$fn = fn() use(&$a) {
    $a += $b;
};
$fn();
var_dump($a); // int(3)

この例では$bは値バインドであり、$aは参照バインドとなります。
ただし、この構文は既存のクロージャ構文に近いため、混乱を招く可能性があります。
通常クロージャ構文では$bはバインドされません。

Allow arrow notation for real functions

通常の関数やメソッドにもアロー関数を使用できるようにするのもよいかもしれません。
getterのような単一定型文を減らすことができるでしょう。

class Test {
    private $foo;
    private $bar;

    fn getFoo() => $this->foo;
    fn getBar() => $this->bar;
}

対象バージョン

PHP7.4です。
7.4の新機能多すぎるんだけど大丈夫なのこれ。

投票

2019/04/17に投票開始、2019/05/01に投票終了。
有権者の2/3+1の賛成で受理されます。

2019/04/22時点では賛成36、反対7で、よほどの問題でも発生しないかぎりPHP7.4で導入されます。

感想

JavaScriptのアロー関数って個人的に好きじゃないんですよね。

JavaScript
const hoge = () => a++;

let a = 1;
hoge();
a; // 2 ←

普段スコープがー副作用がーとか言ってるわりに、どうしてこれが平気なのか理解に苦しむ。

PHPのアロー関数は上記のとおり値バインドであり、このような問題は発生しません。

PHP
$hoge = fn() => $a++;

$a = 1;
$hoge();
echo $a; // 1

しかし、そういう意思を持った上で値バインドを選んだ、というわけではなく単に実装上の理由というのは少々もにょる。

また、これまでのPHPの文法とはかけ離れた異質な構文なので、慣れないと扱いづらそうです。
特にPHPの場合、=>は既に別の構文で使用されています。
パーサによるパースに対しては=>を使っていても誤解が生まれないようになっているのですが、しかし人間によるパースが誤解しやすいことは間違いありません。
どうせ既存構文とは全く互換性がないのだから、人力パースにも優しい新たな演算子を設けてほしかったところですね。

あと全く関係ないのだけど、名前空間にスペース空けられるってのをこのRFCで初めて知ったよ。

namespace A                 \B;
    echo __NAMESPACE__; // A\B

【PHP8】PHPの三項演算子が他言語の実装に一歩近付く

$
0
0

Deprecate left-associative ternary operatorというRFCが投票に入っています。

提案者のNikitaは、最近アロー関数やらAlways generate fatal error for incompatible method signaturesやらConsistent type errors for internal functionsやら立て続けに凄い勢いで活躍しててすごい草生えてる
過去一年のcontributions2000超えって何なの…?

Deprecate left-associative ternary operator

Introduction

ほとんど(全て?)の他言語と異なり、PHPの三項演算子は右結合ではなく左結合です。
左結合の振る舞いは一般的に有用ではなく、複数言語を使い分けるプログラマにとって混乱の元になっています。
このRFCでは三項演算子の左結合性を廃止・削除し、かわりに括弧の明示的な使用を強制します。

例として以下のコードを上げます。

return $a == 1 ? 'one'
     : $a == 2 ? 'two'
     : $a == 3 ? 'three'
     : $a == 4 ? 'four'
               : 'other';

大抵の言語ではこのように解釈されます。

return $a == 1 ? 'one'
     : ($a == 2 ? 'two'
     : ($a == 3 ? 'three'
     : ($a == 4 ? 'four'
               : 'other')));

これは直感的であり便利な解釈です。
PHPではそうではなく次のようになります。

return ((($a == 1 ? 'one'
     : $a == 2) ? 'two'
     : $a == 3) ? 'three'
     : $a == 4) ? 'four'
               : 'other';

これは一般的に考えられるような動作ではありません。

Proposal

PHP7.4では、括弧を使わずに三項演算子のネストを使用すると非推奨の警告を表示します。
PHP8では、コンパイルエラーになります。

1 ? 2 : 3 ? 4 : 5;   // deprecated
(1 ? 2 : 3) ? 4 : 5; // ok
1 ? 2 : (3 ? 4 : 5); // ok

これはエルビス演算子においても同様です。

1 ?: 2 ? 3 : 4;   // deprecated
(1 ?: 2) ? 3 : 4; // ok
1 ?: (2 ? 3 : 4); // ok

1 ? 2 : 3 ?: 4;   // deprecated
(1 ? 2 : 3) ?: 4; // ok
1 ? 2 : (3 ?: 4); // ok

ただし例外として、エルビス演算子を2回重ねる場合は括弧が必要ありません。

1 ?: 2 ?: 3;   // ok
(1 ?: 2) ?: 3; // ok
1 ?: (2 ?: 3); // ok

なぜかというと、($a ?: $b) ?: $c$a ?: ($b ?: $c)は左結合であろうと右結合であろうと常に同じ結果になるからです。

三項演算子の中央を三項演算子にする場合も括弧は必要ありません。
これは解釈を間違えることがなく、結合性の影響を受けないからです。

1 ? 2 ? 3 : 4 : 5 // ok
1 ? 2 ?: 3 : 4    // ok

Null合体演算子??は既に右結合なので、このRFCによる影響はありません。

Backward Incompatible Changes

三項演算子の左結合性を悪用するコードは、PHP8ではエラーになります。
左結合の三項演算子を使っているようなコードはほぼ確実にバグなので、このRFCによる影響は最小限に抑えられます。

Future Scope

しばらくエラーにしておけば、いずれ正しい三項演算子にすることができます。

投票

2019/04/23に投票開始、2019/05/07に投票終了。
有権者の2/3+1の賛成で受理されます。

2019/04/25時点では賛成22、反対8で、賛成が優勢ですが、まだ簡単にひっくり返る程度の差です。
果たしてどうなることでしょうか。

感想

一気に他言語と同じ動作にしてしまうと、同じ書き方でもPHPのバージョンによって動作が完全に異なってしまい、間違いなく大混乱になるでしょう。
そのためいったん動かないようにして、順次移行するという形にしたようです。

三項演算子について調べたとき、PHPと同じ解釈をする言語がひとつだけあったと思うんだけど何だったっけ。
その言語にとっては遂に仲間が居なくなってしまいますね。

まあそもそも根本的に、三項演算子をネストするなって話ですが。
遊ぶと楽しいのは確かですが、会社でこんなコードを書いてきたらぶん殴られて当然です。


【PHP8】演算子.と+の優先順位が変わる

$
0
0

PHP7.3現在、演算子+-.の優先順位は同じです。
01.png

すなわち左から右に評価されます。

    echo 1 . 2 + 3 . 4;

    echo ((( 1 . 2 ) + 3 ) . 4 ) ; // これと同じ

マニュアルでもわざわざ例を挙げて解説しています。
02.png

さて2019年3月にChange the precedence of the concatenation operatorというRFCが提出されました
2019/05/07現在は投票中のステータスですが賛成多数で、PHP8で上記の動作は変わることになりそうです。
演算子の追加削除はよくあることですが、優先順位の変更というのは他言語含めてもなかなか見ることのないレアなイベントではないでしょうか。

Change the precedence of the concatenation operator

Introduction

+-、そして.は長年にわたる問題です。
それは左から右に解釈されます。

echo "sum: " . $a + $b;

// こう解釈される
echo ("sum: " . $a) + $b;

// こうではない
echo "sum :" . ($a + $b);

このRFCでは、この動作をより直感的に、問題が出にくくなるようにすることを目的としています。

Proposal

現在、+-.の各演算子は優先順位が同じです。
これらは、単純に左から右に評価されます。

これは直感に反します。
一般に、数字ではない文字列を足したり引いたりすることはほとんどありません。
PHPが整数を文字列にシームレスに変換できることを考えると、文字列連結が先に来ることが望ましいでしょう。

従って、このRFCでは.に、+-より低い優先順位を与えることを提案します。
具体的には、新しい優先順位は<<>>のすぐ下になります。

Backward Incompatible Changes

.の後に、括弧を使わず+-を使用している式全てが影響を受けます。
例として、"3" . "5" + 742ではなく"312"になります。

これは、予告や警告なしに出力が変更されるという点で微妙な動作変更ですが、コードを静的解析して、この問題が発生する箇所を全て見つけることは簡単にできます。
私の知るかぎり、この問題が発生することは稀であり、そして大抵は最初から間違っています。

NikitaがMLで言及したように、既存のOSSへの影響は事実上ありません。
見つかったものは全て単なるバグです。
つまり、全体的に影響は非常に小さいということになります。

Proposed PHP Version

PHP7.4でE_DEPRECATEDを発生させ、PHP8で動作を変更します。

投票

投票開始は2019/04/30、投票終了は2019/05/14です。
2019/05/07現在、PHP8で動作変更する提案は賛成23反対3で、ほぼ確実に受理されます。
PHP7.4でDeprecatedにする提案は賛成23反対4で、こちらもおそらく受理されます。

NikitaのML

またNikitaか。

Composerパッケージの上位2000件を調査したところ、影響を受けるコードは僅か5件しかありませんでした
しかも5件とも修正後の優先順位を想定したコードで、つまり現状ではバグっています。

$this->errors->add( 'unknown_upgrade_error_' . $errors_count + 1, $string );

例を一つあげると、上記コードはadd('unknown_upgrade_error_5', $string)のような文字列を与えたいのだと思われますが、実際はadd(1, $string)になります。

今回のRFCが通ると、想定していたであろう動作にエンバグすることになります。

感想

考えてみたら、むしろどうして今まで同じだったんだ、って感じですね。
優先順位が同じであるという仕様と、文字列と数値を自由に行き来できるという仕様が相まって、PHPで.+を同時に使った演算は、ぱっと見から予想できない結果になることがありました。
今回のRFCが通ると、そのあたりの動作がすっきりすることになります。

もっとも、そういったややこしい演算には普通は括弧を使っているから、実害は全くないはずですけどね。

ところでDeprecateにする提案に"The second (secondary) voting requires a 50%+1 majority."って書いてあるんだけど、50%の投票は廃棄されたんじゃなかったのか?

PHP7.4 の FFIを試した

$
0
0

FFIを動かす準備と確認

PHP7.4に入るFFIのマニュアルが出来ていたので試してみる。
https://php.net/ffi

とは言ってもCの操作のマッピングに近いので、もしかしたらマニュアルよりCのソースを見ないと意味が分からないかもしれない。

まずはソースを取ってきてコンパイルする。

git clone https://github.com/php/php-src.git
cd php-src
git checkout PHP-7.4
./configure --with-ffi
make

--with-ffi が必要。他のオプション無しだとコンパイル時間はそれほどかからない。

FFIを有効にしつつサーバーを立ち上げるのは

php-src/sapi/cli/php -d ffi.enable=1 -S localhost:8000

というように ffi.enable=1 を設定する。
サーバーが立ち上がったら、まずは https://php.net/ffi.examples-basic この辺の例をコピペしてエラーが出ないことを確認する。

FFIのプログラムを書く

動くことを確認したら、Cで共有ライブラリを作ってみる。

#include <string.h>

#define BUF_SIZE 1024

const char * sample(const char *data, int mod)
{
  char buf[BUF_SIZE];
  const char * ret = buf;
  int i;

  for (i = 0; i <= strlen(data) && i < BUF_SIZE; i++){
    if (data[i] >= 97 && data[i] <= 122 && i % mod == 0)
      buf[i] = data[i] - 32;
    else
      buf[i] = data[i];
  }

  return ret;
}

特定の位置の小文字を大文字にするプログラム。
これを

gcc -shared -o libsample.so sample.c

で共有ライブラリにしてPHPから読み込む。

<?php

header('Content-Type: text/plain');

$ffi = FFI::cdef("

const char * sample(const char *data, int mod);

", __DIR__ . '/libsample.so');

var_dump($ffi);
var_dump($ffi->sample("sample test test", 3));
var_dump($ffi->sample("sample test test", 4));
var_dump($ffi->sample("sample test test", 1));

/* output:

object(FFI)#1 (0) {
}
string(16) "SamPle teSt TesT"
string(16) "SampLe tEst Test"
string(16) "SAMPLE TEST TEST"
*/

上手く動いた。
PHPのstringはC言語でconst char *で受け取れる。
constは必要っぽい。整数はintをそのまま使える。

コールバックを利用する

マニュアルではzend_writeを利用しているが、サンプルとしてはあまり良くないと思う。
Cの関数ポインタがPHPのクロージャにマッピングされるという動きを試してみる。

#include <string.h>

#define BUF_SIZE 1024

typedef int (*callback_t)(int);

const char * sample(const char *data, callback_t callback)
{
  char buf[BUF_SIZE];
  const char * ret = buf;
  int i;

  for (i = 0; i <= strlen(data) && i < BUF_SIZE; i++){
    if (data[i] >= 97 && data[i] <= 122 && callback(i))
      buf[i] = data[i] - 32;
    else
      buf[i] = data[i];
  }

  return ret;
}

とても単純な int を受け取って int を返す関数。
これをPHP側ではクロージャにマッピングする。

<?php
header('Content-Type: text/plain');

$ffi = FFI::cdef("

typedef int (*callback_t)(int);
const char * sample(const char *data, callback_t callback);

", __DIR__ . '/libsample.so');

var_dump($ffi);
var_dump($ffi->sample("sample test test", fn($i) => $i % 3));
var_dump($ffi->sample("sample test test", fn($i) => $i % 4));
var_dump($ffi->sample("sample test test", fn($_) => true));

/* output:

object(FFI)#1 (0) {
}
string(16) "sAMpLE TEsT tESt"
string(16) "sAMPlE TeST tEST"
string(16) "SAMPLE TEST TEST"
*/

アロー関数もPHP7.4のソースにマージされているので使える。

本当はコールバック中で文字列を受け取って文字列を返したいところだけど、文字列を返そうとするとUncaught FFI\Exception: FFI internal error. Unsupported return type のエラーになるようだ。受け取るのは出来るんだけど。

構造体を利用する

バリューオブジェクト的な引数を使いたい場合は構造体を利用する。

struct point {
  int x;
  int y;
};

これをPHPで使うには

<?php
$ffi = FFI::cdef('
struct point {
  int x;
  int y;
};
');
$point = $ffi->new('struct point');
$point->x = 1;
$point->y = 2;

とする。

折角なので先程のコールバックと組み合わせてみる。

#include <string.h>

#define BUF_SIZE 1024

typedef int (*callback_t)(int);
struct cbdata {
  callback_t f;
};

const char * sample(const char *data, struct cbdata *cbdata)
{
  char buf[BUF_SIZE];
  const char * ret = buf;
  int i;

  for (i = 0; i <= strlen(data) && i < BUF_SIZE; i++){
    if (data[i] >= 97 && data[i] <= 122 && cbdata->f(i))
      buf[i] = data[i] - 32;
    else
      buf[i] = data[i];
  }

  return ret;
}
<?php
header('Content-Type: text/plain');

$ffi = FFI::cdef("

typedef int (*callback_t)(int);
struct cbdata {
  callback_t f;
};

const char * sample(const char *data, struct cbdata *cbdata);

", __DIR__ . '/libsample.so');

$cbdata = $ffi->new('struct cbdata');
$n = 3;
$cbdata->f = function($i) use(&$n){
    return $i % $n === 0;
};

$pcbdata = FFI::addr($cbdata);

var_dump($ffi->sample("sample test test", $pcbdata));

$n = 4;
var_dump($ffi->sample("sample test test", $pcbdata));

$n = 1;
var_dump($ffi->sample("sample test test", $pcbdata));

/* output:

string(16) "SamPle teSt TesT"
string(16) "SampLe tEst Test"
string(16) "SAMPLE TEST TEST"
 */

C関数からのコールバックでもPHPの参照を使えるので、値を順次変更しながらCの関数を実行することも出来そうである。

Qiitaでよく参照渡しとかの記事が話題になっているけれども、この辺の参照やポインタが何かということが分かっていないとハマるかもしれない。

C言語以外を利用する

数値計算を高速に実行したい場合はC言語で良いかもしれないが、文字列の加工はCでやりたくない。
他の言語にもFFIがあれば、C言語を通してやり取りできる。

#include <stdio.h>
#include <HsFFI.h>

#ifdef __GLASGOW_HASKELL__
#include "Callback_stub.h"
#endif

#include "template_operations.h"

int prepare() {
  hs_init(0,0);
}
int finish() {
  hs_exit();
}

static const char * assign_string = "";
void assign_value_set(const char *value)
{
  assign_string = value;
}
const char *assign_value_get()
{
  return assign_string;
}

const char* parse(char *data, struct template_operations *tops)
{
  return hsParse(data, tops);
}
typedef struct template_operations
{
  size_t (*assign)(const char * key, const char * value);
} CTOPS;
{-# LANGUAGE ForeignFunctionInterface #-}

#include "template_operations.h"

module HaskellPhp where

import Foreign
import Foreign.C.String
import Foreign.C.Types

data Tops = Tops { assign::FunPtr (CString -> CString -> IO CInt) }

foreign export ccall hsParse :: CString -> Ptr Tops -> IO CString
foreign import ccall "dynamic" mkFun :: FunPtr (CString -> CString -> IO CInt)
                                     -> (CString -> CString -> IO CInt)
foreign import ccall "assign_value_get" value_get :: IO CString



instance Storable Tops where
    sizeOf _ = #size CTOPS
    alignment _ = #alignment CTOPS
    peek ptr = do
      assign' <- (#peek CTOPS, assign) ptr
      return Tops { assign=assign' }
    poke ptr (Tops assign') = do
                           (#poke CTOPS, assign) ptr assign'


--
-- Function to parse some string
--
hsParse :: CString -> Ptr Tops -> IO CString
hsParse cs cops = do
  ops <- peek cops
  let f = mkFun $ assign ops
  a <- newCString "foo"
  b <- newCString "bar"
  r <-  f a b
  cstr <- value_get
  s <- peekCString cstr
  newCString $ "hsParse finish: [" ++ show r ++ "]" ++ "[" ++ s ++ "]"
<?php
header('Content-Type: text/plain');
$ffi = FFI::cdef('
int prepare();
int finish();

typedef struct template_operations
{
  size_t (*assign)(const char * key, const char * value);
} CTOPS;

const char* parse(char *data, struct template_operations *tops);
void assign_value_set(const char *value);
const char * assign_value_get();

', __DIR__ . '/libcallback.so');

$ffi->prepare();

$tops = $ffi->new('struct template_operations');
$tops->assign = function($key, $value) use ($ffi){
    $value = "$key = $value";
    $ffi->assign_value_set($value);

    // Notice:
    // return string value is not supported
    return strlen($value);
};

$data = '<div>$foo</div>';
$ret = $ffi->parse($data, FFI::addr($tops));

var_dump($ffi->assign_value_get());
var_dump($ret);

$ffi->finish();

/* Output

string(9) "foo = bar"
string(30) "hsParse finish: [9][foo = bar]"

 */

Haskellを使うサンプル。
Haskellでテンプレートエンジンを作ってその中からPHPと相互にやり取りするというような想定。
Cとのやり取りはhsc2hsを使う。

返却値は文字列に出来ないのでC言語のグローバル変数にした。
PHPは元々スレッドセーフではないので、特に問題ないハズ?

ここまで来れば、もうC言語は要らないよねということで直接Haskellを呼び出す。

<?php
header('Content-Type: text/plain');
$ffi = FFI::cdef('
int hs_init(int *, char **[]);
int hs_exit();

typedef struct template_operations
{
  size_t (*assign)(const char * key, const char * value);
} CTOPS;

const char* parse(char *data, struct template_operations *tops);
void return_value_set(const char *value);
const char * return_value_get();

', __DIR__ . '/libcallback.so');

$argc = FFI::new('int');
$argv = FFI::new('char[0]');
$pargv = FFI::addr($argv);
$ffi->hs_init(FFI::addr($argc), FFI::addr($pargv));

$tops = $ffi->new('struct template_operations');
$tops->assign = function($key, $value) use ($ffi){
    $value = "$key = $value";
    $ffi->return_value_set($value);

    // Notice:
    // return string value is not supported
    return strlen($value);
};

$data = '<div>$foo</div>';
$ret = $ffi->parse($data, FFI::addr($tops));

var_dump($ffi->return_value_get());
var_dump($ret);


$ffi->hs_exit();


/* Output

string(9) "foo = bar"
string(30) "hsParse finish: [9][foo = bar]"

 */
{-# NOINLINE return_value #-}
return_value :: IORef CString
return_value = unsafePerformIO $ newCString "" >>= newIORef

return_value_set :: CString -> IO ()
return_value_set a = writeIORef return_value a

return_value_get :: IO CString
return_value_get = readIORef return_value

データのやり取りでAPIとしてC言語の宣言が出てくるけれど、C言語の実装は無くなった。
Haskellを普通に書くとグローバル変数が使えないので、そこはunsafePerformIOで。
Cだとstaticと書けばメモリ上の同じ位置だという安心感があるけれども、他の言語ではコンテキストから抜けてもグローバル変数が同じ領域を指しているのかが若干不安が残るが、問題なく動いているようだ。

同時アクセスしたときは…うーん。PHPは毎回コンテキストを生成するので影響ないよね。FFI::cdef が実行されたときにDL_OPENdlopenのエイリアス)が呼ばれるようだし。

まとめ

このサンプルプログラムは https://github.com/nishimura/php_ffi_samples ここに置いた。

これでPHPはフォーム操作とDBのやり取りに専念して、細かなロジックはHaskellでもGoでもRustでも好きな言語で書ける!

【PHP7.4】__toString()が例外を吐けるようになる

$
0
0

プロパティの設定を必須にしたかったとしましょう。

class HOGE{
    public $var;
    public function __toString(){
        if(!$this->var){
            throw new \Exception('$var must set.');
        }
        return sprintf('$var is %1$s', $this->var);
    }
}

try{
    echo new HOGE();
}catch(\Exception $e){
    var_dump($e);
}

何の変哲も無いように見えるコードですが、これ動きません。
Fatal error: Method HOGE::__toString() must not throw an exception, caught ExceptionというFatalエラーを吐いて死にます。

実は__toString()メソッド内では例外を出すことができないのです。
他のあらゆるマジックメソッドにはこんな制限はないのに、__toString()だけそういうことになっています。
よくわかりませんね。

しかしまあ、そんなよくわからない制限なら要らないよね、ということでAllow throwing exceptions from __toStringのRFCが提出されました。

PHP RFC: Allow throwing exceptions from __toString()

Introduction

__toString()からの例外スローは禁止されており、致命的エラーになります。
これは__toString()から任意のコードを呼び出すことを困難にし、一般的なAPIとして使用することが難しくなります。
このRFCは、この制限を取り除くことを目的としています。

現在のような動作になっている理由は、PHPエンジンと標準ライブラリのあらゆるところで文字列変換が行われており、そして全ての箇所が例外を正しく処理できるようになっているわけではないからです。

技術的観点からすると、この制限は無駄でしかありません。
なぜならば、文字列変換中の例外は、回復可能なエラーを例外に変換するエラーハンドラによって引き起こされる可能性があるからです。

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

try {
    (string) new stdClass;
} catch (Exception $e) {
    echo "(string) threw an exception...\n";
}

実際、Symfonyは現在の制限を回避するためにこの抜け穴を使用しています。
残念ながら、これはPHP8で削除予定の$errcontextパラメータに依存しています。

Proposal

__toString()からの例外スローを許可します。
もう致命的なエラーは発生しません。

さらにPHP7のエラーポリシーに基づいて、could not be converted to stringおよび__toString() must return a string valueというrecoverable fatal errorを、適切なError exceptionに変更します。

Extension Guidelines

エクステンション開発者は、文字列変換からの例外を適切に処理するため、以下のガイドラインを考慮に入れてください。

zval_get_string()convert_to_string()および類似の関数は、例外を生成した場合でも文字列を生成します。この文字列を含めてください。必ずしも従う必要はありませんが、そうすることができます。
・文字列変換がエラーハンドラによって例外になった場合の結果は、オブジェクトから文字列への変換の場合は空文字列、配列の場合は"Array"という文字列になります。これは以前と同じ動作です。
・通常は(EG(exception))で例外がスローされたかどうかを確認すれば十分です。

zend_string *str = zval_get_string(val);
if (EG(exception)) {
    // リソース解放など
    return;
}

・失敗する可能性のある操作を行うヘルパーAPIが多数追加されます。

// zval_get_string()に似ているが、変換に失敗したらNULLを返す
zend_string *str = zval_try_get_string(val);
if (!str) {
    // リソース解放とか
    return;
}
// Main code.
zend_string_release(str);

// zval_get_tmp_string()に似ているが、変換に失敗したらNULLを返す
zend_string *tmp, *str = zval_try_get_tmp_string(val, &tmp);
if (!str) {
    // リソース解放とか
    return;
}
// Main code.
zend_tmp_string_release(tmp);

// convert_to_string()に似ているが、変換に成功したか否かのbooleanを返す
if (!try_convert_to_string(val)) {
    // リソース解放とか
    return;
}
// Main code.

try_convert_to_string()は、変換失敗時には元の値を返しません。そのため、これを使った方が、convert_to_string()と例外チェックを使うより安全です。
・あらゆる文字列変換操作にチェックを行うことができますが、チェックを省略しても通常は余剰な警告が発生するだけです。気をつけて実装しなければならないのは、主にデータベースのような、永続的な構造を変更する操作です。

Backward Incompatible Changes

recoverable fatal errorからError exceptionへの変更は、破壊的変更です。

Vote

投票開始は2019/05/22、終了が2019/06/05、投票数の2/3+1の賛成票で受理されます。

2019/05/27現在、賛成32反対0で、ほぼ確実に導入決定です。

感想

これによって、あらゆるメソッドから例外を出すことができるようになりました。

さて、なんでこんな簡単そうな変更が今まで残っていたの?
かというと、変更が簡単ではないからです。

最初に報告があったのはなんと2011年1月であり、2012年にはあのNikitaが実装が大変なんじゃよーと言っています。
しかし結局全てのcommitがNikita自身の手によって行われました。
最終的な修正は111ファイル1000行以上に及びます。
どんだけ働いてんだこの人

PHPフォーラムは基本的に保守的な人が多く、破壊的な変更はあまり受理されない傾向にあるのですが、Nikitaについては既に、Nikitaが言うなら仕方ないというレベルに達しているようです。
三項演算子のDeprecateとか、他の人が提案してたら絶対通らなかったよね。

しかしこれ、てっきりuninitialized__toString()したときのために作ったものなのかと思ったのですが、特に関係なかったみたいですね。

そして修正規模のわりに、あまり重要な使いどころが思いつかないというか、そもそも個人的には__toString()自体まず使うことがないですね。
デバッグにはvar_dump()という超絶便利関数がありますし、文字列化したいなら適当にメソッド生やします。

【PHP7.4】数値セパレータが書けるようになる

$
0
0
echo 2305843008139952128 === 230584308139952128; // true?false?
echo 10000000000000000; // 0が何個?

わかりにくいですね。

ということでどうにかしようというRFCが提出されました
以下はNumeric Literal Separatorの日本語訳です。

Numeric Literal Separator

Introduction

人間の目は、一続きの長い数値を素早く解析するために最適化されてはいません。
従って、視覚的な区切り文字がなければ、コードの読み取りとデバッグに時間がかかり、読み取りミスを招く可能性があります。

1000000000;   // 1億?10億?100億?
107925284.88; // オーダーはいくつ?

区切り文字のない数値リテラルはさらに、財務情報がセントなのかドルなのかなど、追加情報を伝えることができません。

$discount = 13500; // 13500ドル?13500セント?

.

Proposal

数値リテラルでアンダースコアをサポートし、数値を視覚的にグループ化することによって、コードの読みやすさを向上させます。

$threshold = 1_000_000_000;  // 10億!
$testValue = 107_925_284.88; // 1億オーダー
$discount = 135_00;          // $135

アンダースコア区切り文字は、PHPがサポートしているあらゆる数値リテラルで使用可能です。

6.674_083e-11; // 指数表記
299_792_458;   // 整数
0xCAFE_F00D;   // 16進数
0b0101_1111;   // 2進数
0137_041;      // 8進数

Restrictions

唯一の制限は、アンダースコアは数値に挟まれていなければならないということです。
つまり、以下のような書式は有効ではありません。

_100; // 定数として有効な書式

100_;       // 末尾は×
1__1;       // 連続は×
1_.0; 1._0; // 小数点前後は×
0x_123;     // xの次なので×
0b_101;     // bの次なので×
1_e2; 1e_2; // eの前後なので×

Unaffected PHP Functionality

数値リテラルの間にアンダースコアを追加しても、その値は変わりません。
字句解析の時点でアンダースコアは取り除かれるため、ランタイムには影響しません。

var_dump(1_000_000); // int(1000000)

Backward Incompatible Changes

後方互換性を壊す変更はありません。

Discussion

Use cases

使用を推奨する例

数値区切り文字は、数値の視覚的認知を可能にします。
すなわち、数値が現れたときに桁数を数える必要もなく、正確に間違いなく桁数が一目でわかるようになります。
これによって、4桁を超える数字を正しく読み取るためにかかる時間が大幅に短縮されます。

大きな数値リテラルは、ビジネスロジック定数、単体テスト値、データの変換などによく使用されます。

たとえばComposerがファイルを削除する際の再実行遅延は以下です。

usleep(350000); // without separator
usleep(350_000); // with separator

Active DirectoryのタイムスタンプからUNIXタイムスタンプへの変換。

$time = (int) ($adTime / 10000000 - 11644473600); // without separator
$time = (int) ($adTime / 10_000_000 - 11_644_473_600); // with separator

物理定数。

const ASTRONOMICAL_UNIT = 149597870700; // without separator
const ASTRONOMICAL_UNIT = 149_597_870_700; // with separator

2進数リテラル、16進数リテラルのバイト区切り。

0b01010100011010000110010101101111; // without separator
0b01010100_01101000_01100101_01101111; // with separator

0x42726F776E; // without separator
0x42_72_6F_77_6E; // with separator

Use cases to avoid

使用を避けるべき例

電話番号、クレジットカード番号、社会保障番号などのデータを格納するためにセパレータの使用が魅力的に映るかもしれません。
これらの値は数値のようも見えるからです。
しかし、それはほぼ常に悪い考え方です。

経験則として、これらの値に数値演算をする意味はありません。
これらの値を格納するための最良の方法は、整数ではありません。

// 使ってはいけない
$phoneNumber = 345_6789;
$creditCard = 231_6547_9081_2543;
$socialSecurity = 111_11_1111;

Will it be harder to search for numbers?

数値の検索が難しくなるのでは?

懸念となるのは、同じ値を複数の方法で記述することができるため、数値の検索が困難になることです。

しかし、これは既に現在起こっていることです。
同じ値を2進数、8進数、10進数、16進数、指数表記などで書くことが可能です。
実際には、フォーマットが一貫しているかぎり問題は起こりません。

逆に数値を見つけやすくなることもあります。
上で出ていた例を挙げると、検索時に13_500135_00を区別することが可能になります。
また16進数リテラルをバイト区切りにすることで、_6F_のように特定のバイトを含む数値だけを見つけることが可能になります。

Should it be the role of an IDE to group digits?

数値のグループ化はIDEの役目?

IDEは大きな数値を自動的に3桁区切りにフォーマットすることは可能ですが、区切りは必ずしも必要というわけではありません。

数値を同じ方法でグループ化することが常に望ましいわけではありません。
たとえば、財務数量がセント単位であるかドル単位であるかによって、異なる方法で表すことができます。

$total = 100_500_00; // $100,500.00
$total = 10_050_000; // $10,050,000

2進数リテラル・16進数リテラルは、nibbles/bytes/wordsなどの使用用途によって、様々な桁数でグループ化することができます。

IDEは、プログラマによるこれらの意図を読み取って自動整形することはできないでしょう。

Why resurrect this proposal?

何故このRFCを復活させた?

かつてのRFCは3年以上前(2016年1月)に投票があり、過半数の賛成があったものの2/3には足りず採用されませんでした。

当時の議論を見返したところ、このRFCは良いユースケースが提示されておらず、また投票期間も一週間と短かったため、多くの賛同は得られませんでした。

そのとき以来、数値リテラルのアンダースコア区切りは、多くの言語(Python、JavaScript、TypeScript等)で実装されており、以前より有用なユースケースをこの機能に対して適用できます。

Should I vote for this feature?

この機能に投票する必要はありますか?

Andrea Fauldsが要点をまとめました

この機能はいくつかの利点があります。
それほど複雑な機能ではありません。
新しい構文やトークンはありません。既存の数値トークンの形式を変更するだけです。
既存の全ての数値リテラルに適用され、よく適合します。
他言語で確立された慣習に従います。
見た目は明らかに定数や識別子ではなく数値であることが一目でわかり、混乱の可能性は高くありません。
濫用には制限があります(先頭、末尾、連続はできません)
使用法は直感的です。

Comparison to other languages

他言語での実装例。

Ada 単独、数値間
C# 連続、数値間
C++ 単独、数値間、シングルクオート
Java 連続可、数値間
JavaScript 単独、数値間
Julia 単独、数値間
Kotlin 連続可、数値間
Perl 単独、数値間
Python 単独、数値間
Ruby 単独、数値間
Rust 連続可、どこでも可
Swift 連続可、数値間

Vote

2019/05/30投票開始、2019/06/13投票終了、可決には2/3+1の賛成が必要です。
2019/06/10現在は賛成30反対11で、おそらく受理されます。

感想

本文にもあるように、かつて却下されたRFCのリバイバルです。

当時は保守的傾向が強かったため却下されましたが、今回はユースケースをしっかり記載してきたこと、最近は(主にNikitaの働きにより)革新要素も取り込みやすくなりつつあること、そのNikitaが賛成していること、そしてJavaScriptなどでも導入されそうなことなどにより、PHPでも受け入れられそうです。

日本語だと数値は4桁区切りなので、3桁区切りの数値が入ってきても、結局桁数把握に時間がかかってしまいますね。
思考回路が英語の人であればシームレスに把握できるのでしょうが、私は完全に日本語脳なので、そこまで多大な恩恵を得ることはできないと思われます。
もっとも、一切区切りがない状態に比べたらはるかに良いでしょうけど。

PHP7.4の新機能その2

$
0
0

PHP7.4その1 / PHP7.4その2

2019/06/13、PHP7.4.0α1がリリースされました
今後はベータ、RCと完成度を高めていき、7.4.0のリリースは2019/11/28が予定されています。

なお仕様フィックスは2019/07/13で、それ以降は大きな変更は入らないようになります。
逆に言うとそれまでは追加が入る可能性はあるのですが、既にα1も出たことですし、さすがにこれからのタイミングでプロパティ型指定やらアロー関数レベルの大改修が入ることはないでしょう。

ということで前回以降入った新機能や変更点を見てみることにします。

RFC

Arrow functions 2.0

賛成51、反対8で受理。

アロー関数です。

    // 新構文
    $fn = fn($x) => $x + $y;

    // ↓と同じ
    $fn = function ($x) use ($y) {
        return $x + $y;
    };

functionのもったり感から解放されます。

Change the precedence of the concatenation operator

賛成31、反対4で受理。

演算子+-.の優先順位変更です。

    // PHP7.3まで
    echo "3" . "5" + 7; // 42

    // PHP7.4
    echo "3" . "5" + 7; // 42だがE_DEPRECATED

    // PHP8
    echo "3" . "5" + 7; // "312"

数値演算が文字列結合より優先されるようになり、イメージした通りの計算結果になりやすくなります。
まあ、こういうのはそもそも括弧を使うべきだとは思いますが。

Numeric Literal Separator

賛成33、反対11で受理。

数値セパレータです。

$threshold = 1_000_000_000;
$testValue = 107_925_284.88;
$discount = 135_00;

数値が見やすくなります。
テストに失敗して一時期Pending行きになっていたのですが、その後無事にマージすることができたようです。

Deprecate left-associative ternary operator

賛成35、反対10で受理。

三項演算子のネスト制限です。

1 ? 2 : 3 ? 4 : 5;   // deprecated
(1 ? 2 : 3) ? 4 : 5; // ok
1 ? 2 : (3 ? 4 : 5); // ok

何故か他言語と異なる評価順の三項演算子を今後合わせていくための布石で、括弧を使わずにネストするとE_DEPRECATEDが発生するようになります。

Allow throwing exceptions from __toString()

賛成42、反対0で受理。

__toString()が例外を吐けるようになります。
ユーザ側というより、エクステンション開発者向けの機能なのかもしれません。

「マージしたらめっちゃコンフリクトした」とかnikicが言ってました。

Covariant Returns and Contravariant Parameters

賛成39、反対1で受理。

共変戻り値と反変パラメータのRFC。

単語の意味はよくわかりませんが、簡単に言うと、子クラスの型宣言で親クラスの型宣言を狭められるというものです。

class A {
    function test(): DateTimeInterface{
        return new DateTimeImmutable();
    }
}

class B extends A {
    function test(): DateTimeImmutable{
        return new DateTimeImmutable();
    }
}

PHP7.3まではB:testの返り値の型がA::testと違う、と怒られていました。
Javaあたりでは普通にできるので、それを取り入れたものでしょう。
継承関係にないクラスを指定したり、逆に型宣言を広げたりすると、当然ながら今後も怒られます。

Spread Operator in Array Expression

賛成43、反対1で受理。

引数アンパックを配列中でも使えるようになります。

$array1 = [11, 12];
$array2 = [21, 22];
$array = [1, ...$array1, 2, ...$array2, 3]; // [1, 11, 12, 2, 21, 22, 3]

地味だけど、正直今回の変更点の中で一番有用なんじゃないかと思わないでもない。
配列の後ろに他の配列を追加したいという要件はよくあって、+array_mergeでは思ったようにいかないことが多いですからね。

Deprecate and remove ext/interbase

賛成49、反対0で受理。

Firebird/InterBaseいらんだろ、というRFC。
今後はPDO_FIREBIRDを使うことになります。

そもそもこれを使っている人は居るのだろうか。

weakrefs

賛成28、反対5で受理。

弱い参照です。
PHPには既にWeakRefがあるのだけど、それよりさらに弱く、必要最低限のcreategetしかないみたいです。
使いどころは正直よくわかりません。

Unbundle ext/wddx

賛成30、反対0で受理。

WDDXをPHPコアから削除するRFC。
今後はPECLに移動になります。

WDDXとは異なる言語間でデータをやりとりするための仕組みですが、全く普及しないままXML-RPCやSOAPに取って代わられ、そしてそれらも滅んでJSONにほぼ統一されました。

wddx_deserializeには安全でないデシリアライゼーションという仕様上の脆弱性があり、それをどうにかしようというRFCが提出されて話し合われたものの、そもそもWDDX自体いらなくね?って展開になったみたいです。

New custom object serialization mechanism

賛成20、反対7で受理。

マジックメソッド__serialize__unserializeを追加します。

PHPでオブジェクトのシリアライズ方法にはSerializable__sleep/__wakeupがあるのですが、実は両方とも問題を抱えています。
そこで新たな直列化方法を提供します。

ということらしいのですが問題点が細かすぎてよくわかりませんでした。
複雑な書き方をしたりすると正しくシリアライズできない、ということらしいのですが、そんな複雑な書き方をしない方がよいですよね。
そもそも個人的にはオブジェクトをシリアライズするってことを(無意識のセッション以外)全くしないので、利点も欠点もなんとも言えないのですが。

Pending

本来PHP7.4で入るはずだったのに、保留行きにされているRFC。
今後の展開次第では7.4に入るかもしれませんし、入らないかもしれません。

Deprecate PHP's Short Open Tags

賛成38、反対18で受理。

short_open_tagが削除されます。

やっぱ延長するかも?みたいな話になっていて、ちょっとどうなるかわかりません。

感想

PHP7.4やばすぎでは…?

変更があまりにドラスティックすぎて、PG能力がPHP7.4以前と以後で分断されるんじゃないかレベル。
現状PHP8で入る予定の大きな変更がJITくらいしかないので、もしかしたらPHP7.4→8よりPHP7.3→7.4のほうが大きな差になったりするかもしれませんね。さすがにないかな?

PHP 7.4 alpha 1 がリリースされたので記念に野良ビルドしてみる

$
0
0

はじめに

待望の PHP 7.4 がリリースされました!ということで記念に野良ビルドします🐘

野良ビルド集

動作環境

  • CentOS 7
$ php -v
PHP 7.4.0alpha1 (cli) (built: Jun 16 2019 02:11:23) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0-dev, Copyright (c) Zend Technologies
Removing intermediate container dddb573013ea

ビルド

ミニマム

  • configure にオプション何もつけずにやります 何もつけないとすんなりビルドが通ります。
sudo yum install epel-release
sudo yum install -y wget make automake gcc libxml2 libxml2-devel sqlite sqlite-devel
cd /tmp
wget https://downloads.php.net/~derick/php-7.4.0alpha1.tar.gz
tar zxvf php-7.4.0alpha1.tar.gz
cd php-7.4.0alpha1

./configure

make
sudo make install

Dockerfile

FROM centos:7

RUN yum install -y epel-release
RUN yum install -y wget make automake gcc libxml2 libxml2-devel sqlite sqlite-devel

WORKDIR /tmp
RUN wget https://downloads.php.net/~derick/php-7.4.0alpha1.tar.gz && \
    tar zxvf php-7.4.0alpha1.tar.gz

WORKDIR php-7.4.0alpha1
RUN ./configure

RUN make
RUN make install

オプションいっぱいつけてみる

下記のよく使いそうなオプションを付けてみます

  • apxs (Apache用モジュール) (--with-apxs2)
  • mbstring (--enable-mbstring)
  • pdo-mysql (--with-pdo-mysql)
  • gettext (--with-gettext)
  • gd (--with-gd, --enable-gd-jis-conv)
  • openssl (--with-openssl)
  • intl (--enable-intl)

ちなみに、 PHP 7.4 から ./configure で出力されるエラーメッセージがめちゃめちゃ優しくなりました。これで野良ビルド初心者の方でも野良ビルドに励むことができそうですね。
逆に今までのエラーメッセージが雑すぎた説はある :thinking:

エラーメッセージの例
configure: error: Package requirements (icu-uc >= 50.1 icu-io icu-i18n) were not met:

No package 'icu-uc' found
No package 'icu-io' found
No package 'icu-i18n' found

Consider adjusting the PKG_CONFIG_PATH environment variable if you
installed software in a non-standard prefix.

Alternatively, you may set the environment variables ICU_CFLAGS
and ICU_LIBS to avoid the need to call pkg-config.
See the pkg-config man page for more details.

下記がコマンドです

cd /tmp
sudo yum install -y epel-release
sudo yum install -y wget \
                   automake gcc \
                   libxml2 libxml2-devel \
                   sqlite sqlite-devel \
                   make \
                   httpd httpd-devel \
                   openssl openssl-devel \
                   libicu libicu-devel \
                   gcc-c++ \
                   oniguruma oniguruma-devel

wget https://downloads.php.net/~derick/php-7.4.0alpha1.tar.gz && \
    tar zxvf php-7.4.0alpha1.tar.gz


cd php-7.4.0alpha1
./configure \
        --with-apxs2 \
        --enable-mbstring \
        --with-pdo-mysql \
        --with-gd \
        --enable-gd-jis-conv \
        --with-openssl \
        --enable-intl

make
sudo make install

php -v

Dockerfile

FROM centos:7

WORKDIR /tmp
RUN wget https://downloads.php.net/~derick/php-7.4.0alpha1.tar.gz && \
    tar zxvf php-7.4.0alpha1.tar.gz

RUN yum install -y epel-release
RUN yum install -y wget \
                   automake gcc \
                   libxml2 libxml2-devel \
                   sqlite sqlite-devel \
                   make \
                   httpd httpd-devel \
                   openssl openssl-devel \
                   libicu libicu-devel \
                   gcc-c++ \
                   oniguruma oniguruma-devel

WORKDIR php-7.4.0alpha1
RUN ./configure \
        --with-apxs2 \
        --enable-mbstring \
        --with-pdo-mysql \
        --with-gd \
        --enable-gd-jis-conv \
        --with-openssl \
        --enable-intl

RUN make
RUN make install

RUN php -v

ミニマムの項目と比較して足りなかったモジュールは下記のとおりです。全部 epel に入っているので、 yum で入れられます。

  • gcc-c++
  • httpd
  • httpd-devel
  • openssl
  • openssl-devel
  • libicu
  • libicu-devel
  • oniguruma
  • oniguruma-devel

今まで登場しなかった、 oniguruma あたりが足りないモジュールだよとエラーメッセージとして登場してました。

zip 拡張について

--enable-zip を有効にしてもなぜか php -m で表示されないので、 pecl からインストールする必要がありそうです。

PHP の UPGRADING には下記のように書かれていました。

Zip:
The bundled libzip library has been removed. 
A system libzip >= 0.11 is now necessary to build the extension.

雑翻訳ですが、

バンドルされた libzip は削除されました。
拡張機能をビルドする際に現在 libzip 0.11 以上が必要となります。

と書かれているので、これからは --enable-zip をしても zip は入らないみたいですね。

libzip 自体は cmake の比較的新しいバージョンのインストールも必要のようなので、事前に準備する必要がありそうです。(yum だと 2.8 がインストール対象だったんですが 3 系が必要のようです)

ちなみに、インストールしようとしたら、下記のエラーが出てしまったので、まだオフィシャルでは PHP 7.4 に未対応なようです。

/php-7.4.0alpha1/~/zip-1.15.4/php73/php_zip.c: In function 'php_zip_pcre':
/php-7.4.0alpha1/~/zip-1.15.4/php73/php_zip.c:660:3: error: too many arguments to function 'pcre_get_compiled_regex'
   re = pcre_get_compiled_regex(regexp, &capture_count, &preg_options);
   ^
In file included from /php-7.4.0alpha1/~/zip-1.15.4/php73/php_zip.c:29:0:
/usr/local/include/php/ext/pcre/php_pcre.h:31:20: note: declared here
 PHPAPI pcre2_code* pcre_get_compiled_regex(zend_string *regex, uint32_t *capture_count);
                    ^
make: *** [php73/php_zip.lo] Error 1

で、 pcre_get_compiled_regex の引数の数が変わったので、現時点で PHP 7.4 にインストールするには、該当の箇所を変更する必要があります。

私は面倒くさいので下記で対応しました

sed -ie 's/, &preg_options//' /path/to/php_zip.c && \
sed -ie 's/, &preg_optionscapture_count//' /path/to/php_zip.c

こうすることにより正常にインストールができるようになります。

[PHP Modules]
...
zip

下記が使用した Dockerfile なので、ご参考までに。

FROM centos:7

RUN yum install -y epel-release
RUN yum install -y httpd httpd-devel \
                   openssl openssl-devel \
                   libicu libicu-devel \
                   gcc-c++ \
                   oniguruma oniguruma-devel \
                   automake gcc \
                   libxml2 libxml2-devel \
                   sqlite sqlite-devel \
                   make \
                   wget

RUN cd /tmp
RUN wget https://downloads.php.net/~derick/php-7.4.0alpha1.tar.gz && \
    tar zxvf php-7.4.0alpha1.tar.gz


WORKDIR php-7.4.0alpha1
RUN ./configure \
        --with-apxs2 \
        --enable-mbstring \
        --with-pdo-mysql \
        --with-gd \
        --enable-gd-jis-conv \
        --with-openssl \
        --enable-intl

RUN make
RUN make install

# zip を野良ビルドする
WORKDIR ~
RUN wget https://pecl.php.net/get/zip-1.15.4.tgz
RUN tar zxvf zip-1.15.4.tgz
WORKDIR zip-1.15.4
RUN phpize

# cmake を入れる
RUN wget https://github.com/Kitware/CMake/archive/v3.14.5.tar.gz && \
    tar zxvf v3.14.5.tar.gz
RUN cd CMake-3.14.5 && ./bootstrap && make && make install

# libzip を入れる
RUN mkdir /tmp/libzip && \
        cd /tmp/libzip && \
        curl -sSLO https://libzip.org/download/libzip-1.4.0.tar.gz && \
        tar zxfv libzip-1.4.0.tar.gz && \
        cd /tmp/libzip/libzip-1.4.0/
RUN cd /tmp/libzip/libzip-1.4.0/ && \
    cmake .
RUN cd /tmp/libzip/libzip-1.4.0/ && make && make install

# zip の configure を走らせる
RUN ./configure

# 7.4 より引数が変わったので削除
RUN sed -ie 's/, &preg_options//' /php-7.4.0alpha1/~/zip-1.15.4/php73/php_zip.c
RUN sed -ie 's/, &preg_optionscapture_count//' /php-7.4.0alpha1/~/zip-1.15.4/php73/php_zip.c

# PHP モジュールを入れる
RUN make && make install

# extension に追加する
RUN echo extension=zip.so >> /usr/local/lib/php.ini

追記: GitHub にすでに 7.4 対応版があるので、 sed する必要なさそう?あとで試します

最後に

ようやく Typed Property や FFI 使えるようになるのは夢が広がります💪

皆さんも最高の PHP 野良ビルドライフを送りましょう :tada:

【PHP7.4】波括弧による文字列|配列アクセスが削除される

$
0
0

大改修が入ることはないでしょうと言ったな、あれは嘘だ。
2019/07/22の仕様凍結を目前に、いくつかのRFCが駆け足で投票に入っています。
そのうちのひとつで、あらゆるPHPプログラムに影響する可能性のある変更が入りました。

Deprecate curly brace syntax for accessing array elements and string offsetsというRFCが投票中です。
2019/07/03投票開始、2019/07/17投票終了、受理には2/3+1の賛成が必要です。
2019/07/08時点では賛成23反対6で、おそらく受理されます。

Deprecate curly brace syntax for accessing array elements and string offsets

Introduction

PHPは、配列要素と文字列オフセットへのアクセスに、角括弧と波括弧の両方を使用することができます。

    $array = [1, 2];
    echo $array[1]; // 2
    echo $array{1}; // こっちも2

    $string = "foo";
    echo $string[0]; // "f"
    echo $string{0}; // こっちも"f"

しかし、これら両方の構文のサポートは、混乱をもたらす可能性があります。
波括弧構文は角括弧構文と同じ動作ですか?
パフォーマンスに違いはありますか?
波括弧はスコープを分ける標準的な方法ですが、波括弧構文にもスコープを切る機能はありますか?
そもそも波括弧構文はどうして存在しているのですか?

マニュアルにほんの少しある注釈を除いて、波括弧構文はほぼ文書化されていません。
さらに、通常の角括弧構文より機能は少なくなっています。
たとえば配列への要素の追加に波括弧を使うことはできません。

$array[] = 3;
echo $array[2]; // 3

$array{} = 3; // Parse error: syntax error, unexpected '}'

配列の作成もできません。

$array = [1, 2]; // OK

$array = {1, 2}; // Parse error: syntax error, unexpected '{'

list短縮構文にも使えません。

[$one, $two] = $array; // OK

{$one, $two} = $array; // Parse error: syntax error, unexpected ','

Proposal

波括弧構文による配列と文字列オフセットへのアクセスを非推奨にします。

PHP7.4
$arr = [1, 2, 3];
var_dump($arr{1}); // Warning: Array and string offset access syntax with curly braces is deprecated

Discussion

Wasn't the curly brace syntax deprecated once before?

波括弧構文、前いちどDeprecatedにならなかったっけ?

2008年6月の議論によると、波括弧構文はPHP5.1のRC5時点では非推奨でしたが、最終リリース前に非推奨警告は削除されました。
2006年8月のPHPマニュアルには「PHP6以降廃止予定」と書かれていましたが、PHP6はなくなったのでリリースされることはありませんでした。

Is the curly brace syntax valuable for differentiating string and array offset access?

波括弧構文は、文字列アクセスと配列オフセットアクセスを区別するのに役立たないかな?

重複した構文があれば、文字列アクセスと配列オフセットアクセスを区別することができます。
問題は言語によって使い分けが強制されず、どちらの構文も文字列と配列の両方に使用できることです。
その結果、あるコードベースでは文字列アクセスに常に$str[0]、配列アクセスに常に$arr{0}を使うようにすることはできますが、他のコードベースでは逆の規則になっているかもしれません。

How frequently is the curly brace syntax used?

波括弧構文はどれだけ使われている?

Composerパッケージのトップ2000ライブラリについてNikitaが調べたところ、2.2k箇所で使用され、これは全ての配列アクセス888.3kのうち0.25%でした。
しかし、これは調査したパッケージが重複しているため、実体より多少多く出ています。
たとえばWordPressコアリポジトリのコピーが二つあり、それぞれが182箇所で使用しています。
使用された波括弧の92%は、トップ2000のうちわずか25リポジトリに集中していました。

Will it be too much work for people to migrate code away from the curly brace syntax?

移行は大変?

パッチと一緒に移行スクリプトが公開されています。

Backward Incompatible Changes

文字列・配列に波括弧構文でアクセスすると非推奨の警告が発生するようになります。

Future Scope

PHP8もしくはその後のどこかで、波括弧構文を完全に削除します。
それ以降、波括弧構文はコンパイルエラーになります。

References

・現行スレッド:https://externals.io/message/104744
・2008年6月の議論スレッド:https://externals.io/message/38153
・2005年11月の議論スレッド:https://externals.io/message/20143

感想

消したいという話は14年も前から出ていたようですが、最近のPHP構文刷新の流れに乗ってついに可決されます。
色々なライブラリを見てきましたがこの書き方を見たことないですし、そもそもこの構文を知らない人も多いかもしれません。
なくなっても問題ないですね。

むしろ何故存在していたのかがわからない。

このRFCでは今後の予定は決まっておらず、後の人に任された状態になっています。
素直にPHP8以降で削除されればいいのですが、このまま宙ぶらりんになると困っちゃいますね。


【PHP7.4】PHP7.4では変数に型指定できる & 菱形継承っぽいものができる

$
0
0

PHPのすごい開発者Nikita Popovが書いたTyped Properties and more: What's coming in PHP 7.4?というスライドを見ていたらなにやら面白かったので、適当に紹介してみる。

変数の型指定

PHP7.4ではプロパティに型指定できるようになりましたが、実はプロパティではない只の変数にも型指定することができます。

class Dummy{
    public int $id = 42;
}
$dummy = new Dummy();

$id = & $dummy->id;

$id = 10; // OK
$id = 'not an id'; // Uncaught TypeError

ええ…

なんかもう普通にint $id = 42とか書けるようにしたほうがいい気もしないでもないですが、構文解析とかの都合で難しいのでしょう。

なんにせよ、これで変数の型指定もできるようになってしまいました。
このまま書くと非常にもったり感がありますが、きっと簡単に使えるライブラリが出てくることでしょう。

菱形継承

似たような方法でプロパティの菱形継承っぽいこと(交差型)も可能です。

class A{}

class B extends A{
    public ?Traversable $a;
}
class C extends A{
    public ?Countable $a;
}

$b = new B();
$c = new C();
$a =& $b->a;
$a =& $c->a;

$a = new RecursiveArrayIterator (); // OK
$a = new MultipleIterator(); // Uncaught TypeError: Cannot assign MultipleIterator

RecursiveArrayIteratorはTraversableとCountableをimplementsしているので代入可能ですが、MultipleIteratorはCountableをimplementsしていないので代入不可能です。
ということで菱形継承が可能になりました。

どんなときに役立つのか、よくわかりませんが。

親クラスには書けない

上記は親クラスに何も書かれていなかったので、本来の菱形継承ではありません。

親クラスでプロパティの型を指定した場合、子クラスでその制限を変更することはできません。
すなわち、以下のコードは残念ながら動きません。

動かない
class A{
    public Traversable $a;
}
class B extends A{
    public OuterIterator $a; // Fatal error: Type of B::$a must be Traversable
}

メソッドの型宣言は子クラスで狭めることができますが、プロパティはPHP7.4α1の時点では狭めることができません。
メソッドと動作を合わせるのであればこの書き方も許容されるべきですが、はたして修正されるでしょうか?
だめなままなのかな?

これが許可されるのであれば、以下のように自然な?形の菱形継承も可能になります。

動かない
class A{
    public ?Iterator $a;
}

class B extends A{
    public ?OuterIterator $a;
}
class C extends A{
    public ?RecursiveIterator $a;
}

$b = new B();
$c = new C();
$a =& $b->a;
$a =& $c->a;

$a = new RecursiveArrayIterator(); // OK
$a = new MultipleIterator(); // Uncaught TypeError: Cannot assign MultipleIterator

書けるようになったからといってどうするのかわかりませんが。

ジェネリクス

スライドを見ていたら、意外な構文が出てきました。

01.png

似非ジェネリクスではなく、本物のジェネリクスの導入を考えているみたいです。

まだ動かない
interface Event{}

class SpecificEvent implements Event{}

interface EventHandler<E: Event>{
  public function handle(E $e);
}

class SpecificEventHandler implements EventHandler<SpecificEvent>{
  public function handle(SpecificEvent $e){}
}

RFCもありませんし(Generic Types and Functionsは構文が異なる)、まだ思いつき段階といったところでしょうが、このひとNikitaですからね。
PHP8でジェネリクスが導入されていた、なんてことになったとしても驚きはないですね。

感想

変数の型指定は有用ですね。
定義方法さえ簡単になれば、あとパフォーマンスが極端に落ちるなどの問題がなければ、普通に使ってもよさそうです。
ただPHPの場合、stringを返す組み込み関数が普通にfalseを返してきたりするので、全てを置き換えるのはちょっと辛そうです。

菱形継承はそもそも役に立つ場面を知らないのでなんとも言えません。

ジェネリクスはどうなんですかね、PHPでそこまで必要なんですかね。
確かに配列要素の型を限定したいことは頻繁にありますが、しかし、あらゆるオブジェクトに型指定が欲しいとまではあまり思ったことがないですね。
今でもArrayAccessやらを使えば一応できますしね。

【PHP7.4】レガシーな仕様はどんどんしまっちゃおうねぇ

$
0
0

恒例の仕様凍結直前駆け込みRFC第二弾。
色々な古い書き方について、PHP7.4でE_DEPRECATEにし、PHP8で削除を目指すRFCが投票中です。

以下はDeprecations for PHP 7.4の日本語訳です。

Deprecations for PHP 7.4

Introduction

このRFCでは、以下に列挙されている機能についてPHP7.4で非推奨とし、PHP8で削除することを提案します。

Proposal

各提案は個別に投票を行い、投票数の2/3+1の賛成で受理されます。
投票開始は2019/07/08、投票終了は2019/07/22です。

The 'real' type

現在のPHPでは、float型にはdoubleとrealという2種類のエイリアスが存在します。
後者は滅多に使用されず、廃止されるべきです。
これは(real)キャストと、is_real関数の両方が含まれます。
settype関数はrealをサポートしていないため影響はありません。

プログラム側の対応は簡単で、全ての(real)キャストを(float)に、is_real関数をis_floatに置き換えるだけです。

Proposal:(real)キャストおよびis_real関数をE_DEPRECATEDにします

2019/07/11時点では賛成24反対6で、おそらく受理されます。

Magic quotes legacy

悪名高いmagic_quotesはPHP5.4で削除され、magic_quotesの値を調べる関数はそれ以来ずっとfalseを返しています。
PHP7にはmagic_quotesに関する機能は全く存在しないため、これらの関数はもはや存在する意義がありません。

Proposal:get_magic_quotes_gpc関数およびget_magic_quotes_runtime関数をE_DEPRECATEDにします。
この変更が影響するのは、もはやサポートされていないPHP5.4以前のレガシーなコードだけです。

2019/07/11時点では賛成34反対0で、おそらく受理されます。

array_key_exists() with objects

array_key_existsは互換性のためにオブジェクトにも対応しています。

01.png

オブジェクトに対するarray_key_existsは問題があります。
プロパティの可視性を無視して直接中身を覗きます。
また、配列のキーとオブジェクトのキーの取り扱いの違いが考慮されていないため、数値キーのプロパティに対して正しくない結果を返す可能性があります。

さらにarray_key_existsがオブジェクトを受け入れるという事実は、ArrayAccessオブジェクトをarray_key_existsで適切に操作できるのだとユーザに誤解させかねません。
これは誤りで、array_key_existsは実際にはArrayAccessをサポートしていません。

Proposal:array_key_existsにオブジェクト渡すとE_DEPRECATEDにします。

2019/07/11時点では賛成32反対0で、おそらく受理されます。

FILTER_SANITIZE_MAGIC_QUOTES

magic_quotesはPHP5.3で廃止され、PHP5.4で削除されました。

Filter関数は、magic_quotesと同じ動作をaddslashesを呼び出すことで再現するサニタイズフィルタを提供しています。
PHP7.3では、magic_quotesという単語から抜け出すため、FILTER_SANITIZE_MAGIC_QUOTESのエイリアスとしてFILTER_SANITIZE_ADD_SLASHESを追加しました。

Proposal:FILTER_SANITIZE_MAGIC_QUOTESをE_DEPRECATEDにします。かわりにFILTER_SANITIZE_ADD_SLASHESを使ってください。

2019/07/11時点では賛成33反対0で、おそらく受理されます。

Reflection export() methods

全てのリフレクションクラスはReflectorインターフェイスを実装しており、__toString()export()という2つのメソッドが存在します。
後者は静的メソッドであり、一見して引数を受け付けません。

しかし実際は、各サブクラスにおいて引数を受け入れるようにオーバーライドされています。
これは本来は互換性のない引数のエラーが発生するはずですが、内部的にエラーを抑制して実装されています。

exportメソッドは、本質的にコンストラクタ+__toStringと同じです。

    // 同じ
    ReflectionFunction::export('foo');
    echo new ReflectionFunction('foo'), "\n";

    // 同じ
    $str = ReflectionFunction::export('foo', true);
    $str = (string) new ReflectionFunction('foo');

exportメソッドはPHPの継承の規則に反していて混乱を招くため、全く不要です。

Proposal:Reflectorインターフェイスと全ての実装クラスからexportsメソッドをE_DEPRECATEDにします。PHP8で実装を削除します。

2019/07/11時点では賛成25反対3で、おそらく受理されます。

mb_strrpos() with encoding as 3rd argument

mb_strrposのドキュメントにはこのようなことが書かれています。

02.png

非推奨と書かれてはいますが、現状特に警告が出たりはしません。
両方の引数を受け入れるため、このパラメータは他のパラメータとは異なる動作をします。
たとえばstrict typesにしても型エラーが出ません。
PHP5.1とPHP7.4の両方をサポートするアプリはほぼ存在しないと思われるので、ここの動作を変更しても、後方互換性に関する重大な問題は発生しません。

Proposal:mb_strrpos関数の第三引数に文字列を渡すとE_DEPRECATEDにします。PHP8で実装を削除します。

2019/07/11時点では賛成32反対0で、おそらく受理されます。

implode() parameter order mix

歴史的な経緯により、implode関数は引数$glue$piecesを、どちらの順番でも受け入れることができます。
join関数も同じです。

Proposal:implode(array, string)の順番での引数をE_DEPRECATEDにします。implode(array)は引き続き許可されます。

2019/07/11時点では賛成27反対5で、おそらく受理されます。

Unbinding $this from non-static closures

現在、$closure->bindTo(null)を用いてクロージャから$thisを削除することができます。
PHP8では非静的メソッドの静的呼び出しが削除されたため、非静的メソッドには常に$thisが存在することが保証されました。
非静的メソッド内で宣言された非静的クロージャに対して、$thisが常に存在するという同様の保証を得たいと思います。
さもなくば、通常のアクセスもしくはクロージャからのアクセスの何れかに対して、不合理が発生するでしょう。

Proposal:クロージャから$thisの削除をE_DEPRECATEDにします。特に非静的メソッド内で宣言された非静的クロージャに対して適用されます。$thisが不要な場合は静的クロージャを使うことで余計なバインドを避けられます。

2019/07/11時点では賛成31反対0で、おそらく受理されます。

hebrevc() function

hebrevc関数は、hebrev関数の結果をnl2brするのと同じです。
これはヘブライ語のテキストを論理順から視覚順に変換する関数です。

視覚的順序付けの使用は、Unicode bidiに対応していない端末など、ごく一部のコンテキストにのみ使用するべきです。
W3Cが表明しているように、視覚的順序付けはHTMLには使用しないでください。
hebrevc関数はこの原則に明らかに反しています。

Proposal:hebrevc関数をE_DEPRECATEDにします。

2019/07/11時点では賛成20反対8で、受理されるか微妙なところです。

convert_cyr_string()

convert_cyr_string関数は、キリル文字のテキストを他のキリル文字セットに変換します。
変換する文字コードは、convert_cyr_string($str, "k", "i")のように理解しがたい1文字で指定しなければなりません。
この関数は、PHPに文字コード変換のための一般的手法が存在していなかった頃の古い関数です。
現在はmb_convert_encodingiconv、あるいはUConverterなどが、この目的のために使用されます。

Proposal:convert_cyr_string関数をE_DEPRECATEDにします。

2019/07/11時点では賛成17反対7で、受理されるか微妙なところです。

money_format()

money_format関数は、ロケールを見て金額をフォーマットします。
これはC言語のstrfmonを使用していますが、これは全てのプラットフォームがサポートしているわけではなく、特にWindowsでは利用できません。
今日ではintlが提供するNumberFormatter::formatCurrencyを使用すべきです。
これはプラットフォームに依存せず、ロケールにも依存しません。
intlではさらに金額フォーマットをパースするNumberFormatter::parseCurrencyも提供しています。

あと、MacOSでのstrfmonの実装を見ていると、どうもこの関数はバッファオーバーランしているように見受けられます。
これはすなわち、この関数が十分なテストを受けていないことを示しています。

Proposal:money_format関数をE_DEPRECATEDにします。

2019/07/11時点では賛成22反対8で、おそらく受理されます。

ezmlm_hash()

ezmlm_hash関数は、EZMLM/QMailメーリングリストが理解できるメールアドレスのハッシュを作成します。
EZMLM/QMailは最終リリースが2007年であり、全くメンテナンスされていないため、この関数を必要とするPHP開発者は限りなく少数です。
この関数は、元々php.netのメーリングリストで使用するために実装されたものです。
必要であればユーザランドで簡単に実装が可能です。

Proposal:ezmlm_hash関数をE_DEPRECATEDにします。

2019/07/11時点では賛成25反対4で、おそらく受理されます。

restore_include_path() function

restore_include_path関数は、本質的にini_restore('include_path')のエイリアスです。
restore_error_handlerrestore_exception_handlerと異なり、この関数はスタック上で動作せず、必ず初期値にリセットされます。

set_error_handler()とrestore_error_handler()をペアで使用して一時的に動作を変更する操作は有用ですが、set_include_path()とrestore_include_path()を同様にペアで使用することは安全ではありません。
従って、この関数はini_restore('include_path')と別に存在する利点はなく、誤った使い方を助長するだけです。

Proposal:restore_include_path関数をE_DEPRECATEDにします。

2019/07/11時点では賛成19反対9で、受理されるか微妙なところです。

allow_url_include

allow_url_includeディレクティブは、requirerequire_onceincludeinclude_onceの各言語構造にURLストリームラッパーの使用を許可します。
このディレクティブはデフォルト無効で、使用するにはallow_url_fopenも有効にする必要があります。

includeするパスに外部入力を使用している場合、このオプションを有効にするとセキュリティ上の問題が発生します。
外部ドメインからPHPファイルをインクルードする仕様そのものがそもそも極めて問題であり、大きなセキュリティリスクが存在するため、廃止されるべきです。

Proposal:allow_url_includeディレクティブが有効である場合E_DEPRECATEDが発生します。

2019/07/11時点では賛成30反対0で、おそらく受理されます。

Backward Incompatible Changes

PHP7.4では、非推奨の警告が表示されます。
PHP8では、動作しなくなります。

Changelog

元々は以下の項目も廃止予定に含まれていましたが、最終的に対象外になりました。

get_called_class。あまり考えられていなかった。
enable_dl。当初の提案が間違っていた。
・INPUT_SESSIONとINPUT_REQUEST。そもそも実装されてなかったので直接削除した
is_writeable。ただの誤字だけど検索しやすいから好ましいとか主張した人がいたので取り下げられた。
apache_request_headers。大きな問題の中のひとつなので、この関数だけ消して終わりではなく大規模な考察が必要。
hebrev。一部の環境ではまだ有用なため。ただしhebrevcは削除する。
enable_argc_argv、と書いてあるんだけど全く出てこないんだけど存在するのこれ?

感想

ほとんどは削除されて当然というか、そもそも全く知らない関数だった。
hebrevcとかconvert_cyr_stringとか聞いたこともないよ。
使ってる人いるんですかね?
あとmagic_quotesは、むしろどうしてPHP7.0で削除されなかったんだと聞きたいくらいですね。
概ね順当に削除されそうでなによりです。

でもReflection::export()は個人的にデバッグとかで時々使っているからちょっと困るかな。
__toString()はなんというか、これを使うのだ、と明示できないからちょっと気持ち悪いというかなんというか。

PHP7.4.0ベータ3は何処に行った?

$
0
0

2019/07/25、PHP7.4.0ベータ1がリリースされました
2019/08/08、PHP7.4.0ベータ2がリリースされました
2019/08/22、PHP7.4.0ベータ4がリリースされました

あれ?
ベータ3は何処?

internalでもベータ3飛ばされた?と聞かれていますが、「いや、PHPのバージョンスキップは誇りであり伝統なのだよ」とか返されています。
しかしPHP7.3でも7.2でも普通にベータ3は出ているので、何のことだかよくわかりません。
ただの冗談なのか?

PHP7.4.0は2019/07/22に仕様凍結され、それ以降は以降は基本的にバグ修正だけが行われています。
以下はPHP7.4.0ベータ4で修正されたバグの一部です。

Second file_put_contents in Shutdown hangs script

シャットダウン関数内でひとつのファイルに2回file_put_contentsするとハングするというバグ。

<?php
register_shutdown_function ('shutdown');
function shutdown () {
    file_put_contents ('/tmp/test.txt', "shutdown 1 \n", FILE_APPEND | LOCK_EX);
    file_put_contents ('/tmp/test.txt', "shutdown 2 \n", FILE_APPEND | LOCK_EX); // Process exited with code 137
    var_dump(file_get_contents("/tmp/test.txt"));
    exit;
}

7.4.0α3で混入したようです。
どんな原理で起こるのかさっぱりだけど、そもそも普通はこんな使い方しないから気付かないのも仕方ない気はする。

Broken file includes with user-defined stream filters

自作ストリームフィルタを通すとinclude何故かシンタックスエラーが発生するというもの。

ベータ1とベータ2でだけ発生します。
原因はこのコミットで、自作ストリームフィルタを使うとphp_stream_statの値が正しくならないことがあるとかなんとか。
そしてどうやら原因をなおすのではなく対症療法をやったみたいなので、もしかしたら今後もまたうっかり何か起こるかもしれません。

fstat mode has unexpected value on PHP 7.4

↑に関連してなのかどうなのか、fstatがPHPバージョンによって異なる値を返してくるバグが発覚しました。

$handle = popen('echo 12', 'r');
$stats = fstat($handle);
var_dump($stats['mode']); // 7.3までは4096、7.4ベータ2では33206

ただ環境に依るようで、3v4lでは全部4480でした。つまり3v4lではバグが起こりません。

Casting a DateTime to array no longer returns its properties

DateTimeインスタンスを(array)キャストすると何も出力しないバグ。

var_dump((array) new \Datetime('2000-01-01')); // 7.3までは[date, timezone_type, timezone]、7.4.0αは[]

いやー、さすがにこれはどうなんだよ。

しかしPHP内部で使うのであればクラスのままでいいし、外に出すならJSONだけどjson_encodeは正しく動くから、実用的に問題になるような状況はあまり思いつかないです。

と思いきやSymfonyが内部的に配列を使っているらしいです。
そうなると連鎖的にLaravelが死んでPHPが終焉を迎えるので、修正しないわけにはいかないですね。

Assertion failure in openssl_random_pseudo_bytes

openssl_random_pseudo_bytesに第二引数を渡すと死ぬ。

openssl_random_pseudo_bytes(4,$b);

Segmentation faultが発生します。
なんで。

diffが1文字なんだけど、うっかりゴミ'/'が残っていただけなのだろうか。
しかもこれPHP7.1あたりからあったみたいです。

Cannot "manually" unserialize class that is final and extends an internal one

より優れたシリアライズを提供するVarExporterなるSymfonyプラグインがあります。

ところが、これがPHP7.4で__serialize/__unserializeが追加されたせいで自力でのシリアライズ/デシリアライズが不可能になったとかなんとか。
具体的にはシリアライズにReflectionClass::newInstanceWithoutConstructorを使っていて、そのヘルプには"組み込みfinalクラスはReflectionClass::newInstanceWithoutConstructorできない"と書かれているのですが、実際は組み込みクラスをextendsしたfinalクラスもReflectionClass::newInstanceWithoutConstructorできないので詰んだ(dead-end)そうです。
どういう理屈なのかいまいちよくわからないのですが、Nikitaもよくわからないとか言ってて、そしてよくわからないまま解決していました

Can't access OneDrive folder

ローカルのOneDriveフォルダにglobやscandir、file_get_contentsなどでファイルアクセスが全くできないというバグ。

かつて同じような問題があり修正されたのですが、Windows 10 May 2019 Updateによる仕様変更で再発したようです。

それにしても、たかだかファイルアクセスしたいだけなのにめちゃめちゃ大変だな。

今後の予定

2019/09/05にRC1、そして以後も2週間ごとにRCがリリースされ、正式版リリースは2019/11/28の予定です。

感想

発生すればクリティカルだけど、発生条件がややこしくて誰も気付かなかった、みたいなものが多いです。
また直近で発生したバグは、多くが別の変更に伴って発生したものです。
○○を高速化した → △△が発生した → △△を修正したら××が発生した、みたいなやつですね。

原因のひとつとしてソースコードの見通しが悪いという問題点があるのは間違いないので(あらゆるところで#ifdefの嵐だ)、もっとすっきりした書き方にした方がいいのではないかと思ったりはします。
が、しかしここは言語開発の世界です。
読みやすさなどより速度のほうが遙かに重要だ、ということなのでしょう。

【Laravel5.8】PHP7.4でTrying to access array offset on valueが山盛り出るようになった

$
0
0

Laravel5.8で試したからLaravel5.8としているだけで、他のバージョンや、あるいは他のフレームワークでも発生すると思います。

PHP7.4.0がリリースされました

で、PHP7.4からスカラー型変数に配列アクセスするとE_NOTICEが出るようになりました。

$a=null;echo$a[1];

これはPHP7.3までは何も言いませんでしたが、7.4ではE_NOTICEが発生します。
さらにPHP8ではE_WARNINGになる予定です。

これの何が問題って、値が入ってるかどうかわからないテーブルにリレーション張ってる場合ですよ。

テーブルAのモデル
classTableAextendsModel{/**
     * テーブルBへのリレーション
     */publicfunctionTableB(){return$this->belongsTo(\path\to\TableB::class,'b_id','id');}}

コントローラはwithで引っ張ってきてbladeに投げるだけ。

コントローラ
$tableAs=TableA::with(['TableB'])->get();returnview('hoge.blade.php',[$tableAs]);

ビューではなんか適当に表示。

hoge.blade.php
@foreach($tableAsas$tableA)ID:{{$tableA->id}}Bの値:{{$tableA->TableB['name']}}@endforeach

テンプレートはだいたいこんな書き方をしてると思うのですが、というか私がしてるのですが、テーブルBに値が無かった場合、これまで"Bの値"は単に空白になっていました。
PHP7.4に上げた瞬間、至る所でLaravelがErrorExceptionを吐くようになってえらいことになりましたよ。

根本的に正しく解決するにはどうすればいいのかはよくわかりませんが、とりあえず

Bの値:{{$tableA->TableB['name']??''}}

ってすることで事なきを得た。

事なきを得たと思ったんですが、実はこれLaravel本体側でも発生するんですよね。
確認したところではemailバリデータを使うと出てきます。
そのうち修正されるとは思いますが、先にLaravelのアップデートを終わらせるまでPHP本体のアプデは待った方がよいでしょう。

【PHP7.4】PHP7.4がリリースされたので新機能全部やる

$
0
0

2019/11/28にPHP7.4.0がリリースされました
ということで、ここではドキュメント化されている新機能や変更点を片端から試してみます。

これら以外にもドキュメント化するほどでもない軽微な変更が多々入っているはずですし、単なるバグ修正も山ほどあるのですが、今回はそのあたりには触れません。
把握しきれていませんしね。

インストール

古いXAMPPが入っていたらディレクトリまるごと削除。
最新のXAMPPをインストール。
Windows版PHPからVC15 x64 Thread Safeをダウンロード。
解凍したディレクトリをpath\to\xampp\phpにまるごと上書きコピペ。
php.ini-developmentphp.iniにコピー。
php.iniextension_dirをエクステンションが入ってるディレクトリへのフルパスに変更し、mbstringやらgmpあたりの必要なエクステンションのコメントアウトを外す。
XAMPPコントロールパネルからApacheを起動してphpinfo()とかを表示してPHP7.4.0になっていたら成功。

Linux? Mac? Docker?
あなたなら環境構築くらい自力でできるっしょ。

新機能とか

プロパティ型指定

プロパティに型が指定できるようになりました。

classHOGE{publicint$i=0;publicstring$s='';public?object$obj;}$c=newHOGE();$c->i=1;$c->s='string';$c->obj=newstdClass();$c->i='string';// Uncaught TypeError: Typed property HOGE::$i must be int, string used

PHP7.4最大の特徴といっていいでしょう。

アロー関数

アロー関数が使えるようになりました。

$square=fn($x)=>$x**2;var_dump($square(2),$square(-5));// 4, 25

ちょっとした使い捨て関数を書くときなどに便利。

$a=1;$hoge=fn()=>++$a;echo$hoge();// 2echo$a;// 1

PHPではアロー関数の外にある変数は汚染されません。

また、アロー関数の実装に伴いfnが予約語になります。
今後function fn(){}といった文は書けなくなります。

FFI

PHP内に他言語を書けるようになりました。

$a=FFI::new("int[10]");for($i=0;$i<10;$i++){$a[$i]=$i;}$p=FFI::cast("int*",$a);var_dump($p[0]);// 0var_dump($p[2]);// 2

脳が混乱する。
あと複雑な文を入れるとすぐエラーになるんだけどCのエラーなのかPHPのエラーなのかわからなくなる。

プリローディング

php.iniを設定。

php.ini
opcache.preload="path\to\preload.php"

opcache.preloadには"プリロードする対象ファイル"ではなく、"プリロードする対象ファイルを読み込むファイル"を指定します。

preload.php
// preload_cache.phpをプリロードするopcache_compile_file('path\to\preload_cache.php');

プリロードさせたい中身はopcache_compile_fileで呼ばれているファイルに書きます。

preload_cache.php
functionh(string$str):string{returnhtmlspecialchars($str,ENT_QUOTES,'UTF-8');}

Apacheを再起動すると、h()がサーバ上のどこからでも使えるようになります。

var_dump(h('a<b>c'));// a&lt;b&gt;cfunctionh(){}// Fatal error: Cannot redeclare h()

関数h()がネイティブ関数……のようなものになりました。

NULL合体代入演算子

NULL合体代入演算子??=が導入されました。

$arr=[null,// "hoge"false,// falsetrue,// true'',// ""0,// 0[],// []];foreach($arras$i){$i??='hoge';echo$i;}$x??='hoge';echo$x;// "hoge"$x=$x??'hoge';// これと同じ

変数が未定義もしくはnullであれば、NULL合体代入演算子の後ろの値になります。
それ以外であれば元のままになります。
Undefined variableのE_NOTICEを出さない最低限の初期化が簡単に行えます。

HASHエクステンションの常時有効化

var_dump(hash_algos());// ['md2', 'md4', 'md5', …]

PHP7.3までは--disable-hashオプションで無効にできました。
PHP7.4以降は無効にすることができません。

Password Hashing Registry

エクステンションが独自のハッシュアルゴリズムを追加できるようになりました。
それに伴い、現在使用可能なハッシュアルゴリズムを確認するpassword_algos関数が追加されました。

var_dump(password_algos());// ['2y', 'argon2i', 'argon2id']

今後追加アルゴリズムは出てくるでしょうか。

openssl_random_pseudo_bytesの改善

openssl_random_pseudo_bytes関数が異常時に例外を発生するようになりました。

openssl_random_pseudo_bytes(-1);// Fatal error: Uncaught Error: Length must be greater than 0

PHP7.3までは例外が出ずにfalseが返ってきていました。

mb_str_split

mb_str_split関数が追加されました。

$x=mb_str_split('aAaA11!!あ𩸽',1);var_export($x);// ['a', 'A', 'a', 'A', '1', '1', '!', '!', 'あ', '𩸽']

半角全角入り交じりでも正常に分割してくれます。
これは素晴らしいですね。

$x=mb_str_split('がが👨‍👩‍👦‍👦',1);var_export($x);// ['が', 'か', '゙', '👨', '‍', '👩', '‍', '👦', '‍', '👦' ]

ZWJシーケンスもきれいに分割してくれる模様。

mbstring.regex_retry_limit

mbstring.regex_retry_limitディレクティブが追加されました。
デフォルト値は1000000です。

$pattern='(.*)*^';$subject='1234567890123456789012345678901234567890';mb_ereg($pattern,$subject,$regs);// false

マルチバイト正規表現において、マッチ回数が一定値を超えたら検索を打ち切ります。
pcre.backtrack_limitと同じく、ReDoS攻撃を防ぐためのものです。

ところでPCRE正規表現のエラーはpreg_last_errorで取れるんだけど、mb_ereg系のエラーはどうやって調べればいいんだろう。

ReflectionReference

ReflectionReferenceクラスが追加されました。

$ary=[0,1,2];$ref1=&$ary[1];unset($ref1);$ref2=&$ary[2];var_dump(ReflectionReference::fromArrayElement($ary,0));// nullvar_dump(ReflectionReference::fromArrayElement($ary,1));// nullvar_dump(ReflectionReference::fromArrayElement($ary,2));// ReflectionReference

リファレンスかどうかを調べることができます。
が、なんか思っていたのとちがって、何故か配列にしか使うことができないみたいです。

数値セパレータ

数値リテラルを_で区切れるようになりました。

<?phpecho149_597_870_700;// 149597870700echo0x42_72_6F_77_6E;// 0x42726F776Eecho0b01010100_01101000_01100101_01101111;// 0b01010100011010000110010101101111echo1__2;// Parse error: syntax error

人の目で見てわかりやすくするためのもので、値は_が無い状態と全く同じです。

_の使用条件は"数値に挟まれてないといけない"であり、先頭や末尾、小数点や進数記号の前後などには付けられません。
また__と連続させることもできません。

__toString()が例外を出せる

マジックメソッド__toString()が中から例外を出せるようになりました。

classHOGE{publicfunction__toString(){thrownewException('hoge');}}echonewHOGE();// Fatal error: Uncaught Exception: hoge

PHP7.3まではMethod HOGE::__toString() must not throw an exceptionのFatal errorになっていました。

共変戻り値・反変パラメータ

子クラスでパラメータの型を広げ、返り値の型を狭めることができるようになりました。

classBASE{}classEXTENDextendsBASE{}classA{publicfunctionmake(EXTEND$param):BASE{returnnewBASE();}}classBextendsA{/** @Override */publicfunctionmake(BASE$param):EXTEND{returnnewEXTEND();}}(newA())->make(newEXTEND());(newB())->make(newBASE());

PHP7.3までは親クラスと子クラスの型は全く同じでないとならず、異なっているとDeclaration must be compatibleのFatal errorが出ていました。

引数アンパック

$a=[1,2];$b=3;$c=newArrayObject([4,5]);[$a,$b,$c];// [[1, 2], 3, ArrayObject(4, 5)][...$a,$b,...$c];// [1, 2, 3, 4, 5]

可変長引数関数呼び出しという特殊な場所でだけ使えていた引数アンパックを、普通に配列中で使えるようになります。
余計なマージとかを行わず単純に配列をくっつけたいときに便利です。

数値キーは無視されて連番が振り直されます。
連想配列を渡すとCannot unpack array with string keysのFatal errorになります。

弱い参照

WeakReferenceクラスが追加されました。

$dummy=newstdClass();$wr=WeakReference::create($dummy);$wr->get();// $dummyunset($dummy);$wr->get();// null

毎回インスタンス使い捨てのPHPでどういうときに使えばいいのか、正直わかりません。
WeakRefとの違いもわかりません。

マジックメソッド__serialize/__unserialize

マジックメソッド__serialize/__unserializeが追加されました。
__sleepおよびSerializableにかわる、新たなシリアライズのメカニズムです。

unserializeに失敗するバグ
classObjectWithReferences{protected$var1;protected$var2;publicfunction__construct(){$this->var1=newStdClass();$this->var2=$this->var1;}}classWrapperObjectimplementsSerializable{private$obj;publicfunction__construct($obj){$this->obj=$obj;}publicfunctiongetObject(){return$this->obj;}publicfunctionserialize(){unserialize(serialize(new\StdClass));// ???returnserialize($this->obj);}publicfunctionunserialize($serialized){$this->obj=unserialize($serialized);}}$wrapper=newWrapperObject(newObjectWithReferences());var_dump($wrapper->getObject());// ObjectWithReferences$wrapper=unserialize(serialize($wrapper));// Notice: unserialize(): Error at offset 82 of 83 bytesvar_dump($wrapper->getObject());// false

これはバグレポに上がっていた例です。
WrapperObject::serializeメソッドに何もしないserialize/unserializeがありますが、これが入っていると何故かunserializeに失敗します。
新たなシリアライズシステムではこのような問題が起こりません。

__serialize
classWrapperObject{private$obj;publicfunction__construct($obj){$this->obj=$obj;}publicfunctiongetObject(){return$this->obj;}publicfunction__serialize():array{return(array)$this->obj;}publicfunction__unserialize(array$data){return$this->obj=(object)$data;}}$wrapper=newWrapperObject(newObjectWithReferences());var_dump($wrapper->getObject());ObjectWithReferences$wrapper=unserialize(serialize($wrapper));var_dump($wrapper->getObject());// stdClass

こっちならとても簡単。
ただし受け渡しは配列で行わないといけないので、そのあたりは手動で実装が必要になります。
上記例は手抜きしているので元に戻りません。

なおSerializable__serialize/__unserialize両方を入れた場合は__serialize/__unserializeだけが動きます。

unserialize max_depth

unserialize関数にオプションmax_depthが追加されました。

$array=[1=>[2=>[3=>[4=>[5]]]]];$ser=serialize($array);unserialize($ser,[]);// [ 1=>[ 2=>[ 3=>[ 4=>[ 5 ] ] ] ] ]unserialize($ser,['max_depth'=>1]);// false unserialize(): Maximum depth of 1 exceeded

名前からすると深い階層を無視するオプションのように見えますが、実際はmax_depthを超える階層が存在したらunserialize自体が失敗します。

PEAR

PEARがデフォルトでインストールされなくなります。

require_once("Auth/Auth.php");// failed to open stream: No such file or directory

手動でインストールすれば当然ながら今後も使用可能です。
またコンパイルオプション--with-pearを指定することでもインストールできますが、このオプションは非推奨で、今後削除される可能性があります。

Curl

PHPというより、同梱されるCurlのバージョンに依るものです。

$cfile=newCURLFile('https://www.google.com/images/srpr/logo1w.png','image/png','testpic');

libcurlのバージョンが7.56.0以降であれば、CURLFileにURLを指定できます。

また定数CURLPIPE_HTTP1がE_DEPRECATEDになりました。
libcurlでdeprecateになったためです。
libcur7.62.0以降は使えなくなります。

FILTER_VALIDATE_FLOAT

検証フィルタFILTER_VALIDATE_FLOATがオプションmin_range/max_rangeに対応し、FILTER_VALIDATE_INTと同じ挙動になりました。

filter_var(10.1,FILTER_VALIDATE_FLOAT,['options'=>['min_range'=>1,'max_range'=>10,]]);// false

むしろ何故今まで対応していなかったのだろう。

IMG_FILTER_SCATTER

GDに画像フィルタ定数IMG_FILTER_SCATTERが追加されました。

$img=imagecreatefrompng('image.png');imagefilter($img,IMG_FILTER_SCATTER,3,5);header('Content-Type: image/png');imagepng($img);imagedestroy($img);

点描のようなかんじに画像をぼかします。
第三、第四引数でぼかし度合いを調整できます。

imagefilter($img,IMG_FILTER_SCATTER,3,10);// 左imagefilter($img,IMG_FILTER_SCATTER,1,100);// 右

image01.png

正規表現フラグPREG_OFFSET_CAPTURE / PREG_UNMATCHED_AS_NULL

preg_replace_callbackおよびpreg_replace_callback_arrayが、正規表現フラグPREG_OFFSET_CAPTUREとPREG_UNMATCHED_AS_NULLを受け取るようになりました。

$subject='abcdedcba';$pattern='|.c.|';$callback=function($matches){// 第5引数が無い場合、 $matches = ['bcd'] / ['dcb']// PREG_OFFSET_CAPTUREがある場合、 $matches = ['bcd', 1] / ['dcb', 5]return'';};preg_replace_callback($pattern,$callback,$subject,-1,$count,PREG_OFFSET_CAPTURE);

PREG_OFFSET_CAPTUREがあると、マッチした位置も一緒にコールバック関数に渡ってきます。
引数の形が変わることに注意しましょう。

PREG_UNMATCHED_AS_NULLは、サブパターンがマッチしなかったときにnullで埋めて送られてきます。

preg_match('/(A)|(B)|(C)/','B',$matches,PREG_UNMATCHED_AS_NULL);// $matches = ["B", null, "B", null]preg_match('/(A)|(B)|(C)/','B',$matches);// $matches = ["B", "", "B"]

よくわからないので詳細はコメントを参照してください。

PDO DSN

PDOのDSNにユーザ名userとパスワードpasswordを書けるようになりました。

$dsn='mysql:dbname=test;host=127.0.0.1;user=testuser;password=testpass';$pdo=newPDO($dsn);

元々Postgresだけ対応していたのが、MySQLなどその他のデータベースにも書けるようになったとのことです。
これは地味に便利では。

DSNとコンストラクタが両方指定された場合はコンストラクタが優先されます。

PDO ?のエスケープ

SQLの構文中において、???でエスケープできるようになりました。

$sql="SELECT * FROM my_table WHERE my_col ?? 'my_key'";

SQL構文中で?と書くとプレースホルダと解釈されてしまいますが、それを回避することができます。
これまでPostgresの?演算子を書くことができなかったため、その対策です。

文字列値としては、これまでもこれからも普通に書けます。

strip_tags

strip_tags関数の第二引数を配列で渡せるようになりました。

$str='<a><b><i><u>テキスト</u></i></b></a>';strip_tags($str,['b','u']);// <b><u>テキスト</u></b>

むしろ今までできなかったのかよ。

なお、配列で渡す場合はタグの括弧は不要です。

array_merge

array_merge関数とarray_merge_recursive関数の第一引数を省略できるようになりました。

array_merge();// []

空の配列が返ります。
おそらくスプレッド構文に空の配列を渡してしまったとき用。

$arr=[];array_merge(...$arr);// PHP7.3まではE_WARNING

proc_open

proc_open関数の第一引数を配列で渡せるようになりました。

proc_open(['php','-r','echo "Hello World\n";'],$descriptors,$pipes);

何がうれしいのかってOSコマンドインジェクションを考えなくて済むようになります。

また、第二引数がリダイレクタとnullに対応しました。

proc_open($cmd,[2=>['redirect',1]],$pipes);// 2>&1proc_open($cmd,[2=>['null']],$pipes);// 2>null

わりとexecってやっちゃうタイプなので、個人的にはあまり使わない関数です。

pcntl_unshare

pcntl_unshare関数が追加されました。
他プロセスと共有しているコンテキストを分離するとかなんとからしいのだけど、Windowsでは動かないのでよくわかりません。

SplPriorityQueue

SplPriorityQueue::setExtractFlagsに0を渡すと即座に例外を出すようになりました。

$queue=newSplPriorityQueue();$queue->setExtractFlags(0);// Fatal error: Uncaught RuntimeException: Must specify at least one extract flag$queue->insert('A',1);$queue->top();

PHP7.3までは、setExtractFlagsした時点ではエラーは起こらず、その後topしたところでFatal errorが起きていました。

MB_ONIGURUMA_VERSION

定数MB_ONIGURUMA_VERSIONが追加されました。

echoMB_ONIGURUMA_VERSION;// 6.9.3

正規表現エンジン鬼車のバージョンがわかるようになります。

鬼車

鬼車がPHP本体にバンドルされなくなりました。
かわりにlibonigを導入しなければならないそうです。

手元のXAMPPにはそんなファイルがなかったのですが、mb正規表現は普通に動いていました。
どうして動いているのかはわかりません。

get_declared_classes

get_declared_classes関数が、まだインスタンス化されていない無名クラスを返さなくなりました。

$class1=newclass{};var_dump(get_declared_classes());// $class2は入ってない$class2=newclass{};

PHP7.3までは一覧に$class1$class2も出てきていました。
PHP7.4以降は$class1しか出てきません。

imagecreatefromtga

imagecreatefromtga関数が追加されました。

$img=imagecreatefromtga('image.tga');header('Content-Type: image/png');imagepng($img);imagedestroy($img);

誰得にも程があるのではないか。

ファイル末尾の<?php

最後に改行がない
<?php

ファイル末尾に改行を入れずに<?phpとだけ書くと、これまでは<?phpという文字列と解釈されていました。
すなわち<?phpという文字列がHTMLに出力されていました。
PHP7.4以降はPHP開始タグと解釈されるようになります。
その後が何もないので、実質的には何もしません。

STREAM_OPTION_READ_BUFFER

includeやrequireをストリームで使用している場合、streamWrapper::stream_set_optionSTREAM_OPTION_READ_BUFFERオプション付きで呼ばれるようになりました。
カスタムストリームラッパーを自作している場合、それに対する実装が必要です。

定数PASSWORD_XXXの値変更

定数PASSWORD_XXXの定数値が変更になりました。

echoPASSWORD_ARGON2ID;// argon2id

例としてPASSWORD_DEFAULTは1からnullに、PASSWORD_ARGON2IDは3から"argon2id"になります。
正しい実装をしているかぎりは影響ありません。

fread / fwrite

fread / fwriteが失敗時にfalseを返すようになりました。

$fp=fopen('/path/to/dummy','a');fread($fp,100);// false

PHP7.3までは0や""が返ってきていました。

DateIntervalの曖昧な比較

DateIntervalの曖昧な比較ができなくなりました。

newDateInterval('P1D')==newDateInterval('P1D');// Warning: Cannot compare DateInterval objects

E_WARININGが発生し、たとえ同じ値であろうとも常にfalseが返ってきます。
PHP7.3までは異なる値であろうが常にtrueとなっていました。

厳密な比較は常にfalseで、警告も発生しません。

$dti=newDateInterval('P1D');$dti2=$dti;var_dump($dti==$dti2,$dti===$dti2);// true, true

同じオブジェクトであれば当然trueであり、E_WARININGも発生しません。

リフレクションのシリアライズ

リフレクションをserializeするとFatal errorが発生するようになりました。

$ref=newReflectionClass(newstdClass());serialize($ref);// Serialization of 'ReflectionClass' is not allowed

リフレクションのシリアライズはこれまでもサポートされておらず、壊れることがありました。
PHP7.4では明確に禁止されます。

get_object_vars

get_object_varsArrayObjectを突っ込んでも、値が取れなくなりました。

$arr=newArrayObject([1,2,3]);get_object_vars($arr);// []

PHP7.3までは[1, 2, 3]が返ってきました。
これによってReflectionObject::getPropertiesIterator等が影響を受けます。

(array)$arrキャストは影響を受けず、値を取得することができます。

get_mangled_object_vars

get_mangled_object_vars関数が追加されました。

classA{public$pub=1;protected$prot=2;private$priv=3;}classBextendsA{private$priv=4;}$obj=newB;$obj->dyn=5;$obj->{"6"}=6;get_mangled_object_vars($obj);// [ ["Bpriv"]=> int(4), ["pub"]=> int(1), ["*prot"]=> int(2), ["Apriv"]=> int(3), ["dyn"]=> int(5), [6]=> int(6) ]get_object_vars($obj); // [ ["pub"]=> int(1), ["dyn"]=> int(5), [6]=> int(6) ]

get_object_varsとだいたい同じですが、privateな値までナチュラルに取ってきます。
いいのかこれ?

なお、ArrayObjectの値はこちらを使っても取得できません。

Countable SimpleXMLElement

SimpleXMLElementCountableをimplementsしました。

$xmlstr=<<<XML
<a>
  <bs>
    <b>1</b>
    <b>2</b>
    <b>3</b>
  </bs>
</a>
XML;$xml=newSimpleXMLElement($xmlstr);echocount($xml);// 1echocount($xml->bs);// 1echocount($xml->b);// 3

実はずっと昔からCountableでもないのにcountできていたので、単に現状を実態に合わせたというものです。

sapi_windows_set_ctrl_handler

sapi_windows_set_ctrl_handler関数が追加されました。

sapi_windows_set_ctrl_handler(function(int$evt){if($evt===PHP_WINDOWS_EVENT_CTRL_C){echo'Ctrl+C pressed.';exit();}elseif($evt===PHP_WINDOWS_EVENT_CTRL_BREAK){echo'Ctrl+Break pressed.';exit();}else{echo'What pressed???',$evt;}});while(1){usleep(100);}

WindowsのCLIでCtrl+CCtrl+Breakをハンドリングできます。

WindowsのCLIでPHPを動かしてる人なんてどんだけ居るんだよ(鏡を見つつ)。

OpenSSL

openssl_x509_verify関数が追加されました。

$crt=<<<CRT
-----BEGIN CERTIFICATE-----
MIIDbDCCAtWgAwIBAgIJAK7FVsxyN1CiMA0GCSqGSIb3DQEBBQUAMIGBMQswCQYD
VQQGEwJCUjEaMBgGA1UECBMRUmlvIEdyYW5kZSBkbyBTdWwxFTATBgNVBAcTDFBv
cnRvIEFsZWdyZTEeMBwGA1UEAxMVSGVucmlxdWUgZG8gTi4gQW5nZWxvMR8wHQYJ
KoZIhvcNAQkBFhBobmFuZ2Vsb0BwaHAubmV0MB4XDTA4MDYzMDEwMjg0M1oXDTA4
MDczMDEwMjg0M1owgYExCzAJBgNVBAYTAkJSMRowGAYDVQQIExFSaW8gR3JhbmRl
IGRvIFN1bDEVMBMGA1UEBxMMUG9ydG8gQWxlZ3JlMR4wHAYDVQQDExVIZW5yaXF1
ZSBkbyBOLiBBbmdlbG8xHzAdBgkqhkiG9w0BCQEWEGhuYW5nZWxvQHBocC5uZXQw
gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMteno+QK1ulX4/WDAVBYfoTPRTz
e4SZLwgael4jwWTytj+8c5nNllrFELD6WjJzfjaoIMhCF4w4I2bkWR6/PTqrvnv+
iiiItHfKvJgYqIobUhkiKmWa2wL3mgqvNRIqTrTC4jWZuCkxQ/ksqL9O/F6zk+aR
S1d+KbPaqCR5Rw+lAgMBAAGjgekwgeYwHQYDVR0OBBYEFNt+QHK9XDWF7CkpgRLo
Ymhqtz99MIG2BgNVHSMEga4wgauAFNt+QHK9XDWF7CkpgRLoYmhqtz99oYGHpIGE
MIGBMQswCQYDVQQGEwJCUjEaMBgGA1UECBMRUmlvIEdyYW5kZSBkbyBTdWwxFTAT
BgNVBAcTDFBvcnRvIEFsZWdyZTEeMBwGA1UEAxMVSGVucmlxdWUgZG8gTi4gQW5n
ZWxvMR8wHQYJKoZIhvcNAQkBFhBobmFuZ2Vsb0BwaHAubmV0ggkArsVWzHI3UKIw
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOBgQCP1GUnStC0TBqngr3Kx+zS
UW8KutKO0ORc5R8aV/x9LlaJrzPyQJgiPpu5hXogLSKRIHxQS3X2+Y0VvIpW72LW
PVKPhYlNtO3oKnfoJGKin0eEhXRZMjfEW/kznY+ZZmNifV2r8s+KhNAqI4PbClvn
4vh8xF/9+eVEj+hM+0OflA==
-----END CERTIFICATE-----
CRT;$rightKey=<<<KEY
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDLXp6PkCtbpV+P1gwFQWH6Ez0U
83uEmS8IGnpeI8Fk8rY/vHOZzZZaxRCw+loyc342qCDIQheMOCNm5Fkevz06q757
/oooiLR3yryYGKiKG1IZIiplmtsC95oKrzUSKk60wuI1mbgpMUP5LKi/Tvxes5Pm
kUtXfimz2qgkeUcPpQIDAQAB
-----END PUBLIC KEY-----
KEY;$wrongKey=<<<KEY
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArbUmVW1Y+rJzZRC3DYB0
kdIgvk7MAday78ybGPPDhVlbAb4CjWbaPs4nyUCTEt9KVG0H7pXHxDbWSsC2974z
dvqlP0L2op1/M2SteTcGCBOdwGH2jORVAZL8/WbTOf9IpKAM77oN14scsyOlQBJq
hh+xrLg8ksB2dOos54yDqo0Tq7R5tldV+alKZXWlJnqRCfFuxvqtfWI5nGTAedVZ
hvjQfLQQgujfXHoFWoGbXn2buzfwKGJEeqWPbQOZF/FeOJPlgOBhhDb3BAFNVCtM
3k71Rblj54pNd3yvq152xsgFd0o3s15fuSwZgerUjeEuw/wTK9k7vyp+MrIQHQmP
dQIDAQAB
-----END PUBLIC KEY-----
KEY;openssl_x509_verify($crt,$rightKey);// 1openssl_x509_verify($crt,$wrongKey);// 0openssl_x509_verify($crt,'hoge');// -1

X.509証明書のチェックができます。
こんなことわざわざPHPでするなよって気もしますが。

idn_to_utf8

idn_to_utf8およびidn_to_ascii関数の引数variantのデフォルト値がINTL_IDNA_VARIANT_UTS46になりました。

echoidn_to_utf8('xn--2-kq6aw43af1e4y9boczagup');// 中島第2駐輪場

そういえば日本語ドメイン最近見ませんね。

token_get_all

token_get_all関数が、不正な文字列にトークンシンボルT_BAD_CHARACTERを返すようになりました。

$tokens=token_get_all('<?php '.chr(0));echotoken_name($tokens[1][0]);// T_BAD_CHARACTER

これまでは解釈できない文字は単に無視されていたのですが、そのような文字を含めて正確にパースできるようになりました。

正しくないソースを正しく解析しても仕方ない気がしますが。

pkg-config

設定をpkg-configに寄せていくために、多くのコンパイルオプションがディレクトリ指定できなくなります。
例としてCurlは、コンパイルオプション--with-curl=path/to/dirとすることでディレクトリを指定することができました。
PHP7.4以降は--with-curlにはオプションを設定できず、ディレクトリ指定することができません。

また-with-png-dirのような、直接的にディレクトリを指定するオプションは削除されます。

まあ、最近は自力でインストールとかあまりしませんけどね。

演算子+-.の優先順位変更

演算子+-.を並べて使うとE_DEPRECATEDが起こるようになりました。
PHP8において演算子.の優先順位を下げるための布石です。

$a=1;$b=2;echo"sum: ".$a+$b;// Deprecated: The behavior of unparenthesized expressions containing both '.' and '+'/'-' will change in PHP 8

PHP7.3までは前から順に("sum: " . $a) + $bと解釈され、答えは2になっていました。
PHP7.4では、解釈は同じですが同時にE_DEPRECATEDが発生します。

PHP8以降では"sum: " . ($a + $b)と解釈されて、答えは"sum: 3"になります。

三項演算子のネスト制限

解釈が一意に定まらない三項演算子のネストはPHP7.4でE_DEPRECATEDに、PHP8でエラーになります。

1?2:3?4:5;// Deprecated: Unparenthesized `a ? b : c ? d : e` is deprecated.(1?2:3)?4:5;// ok 解釈が一意1?2:(3?4:5);// ok 解釈が一意1?2?3:4:5;// ok 解釈が一意1?:2?:3;// ok 解釈が一意

その後の予定は未定ですが、おそらくPHP9あたりで他言語と同じ解釈に変更されると思われます。

波括弧による文字列アクセス

$string='ABCDEFG';echo$string[1];// Becho$string{2};// Deprecated: Array and string offset access syntax with curly braces is deprecated

文字列に波括弧でアクセスするとE_DEPRECATEDが発生します。
PHP7.3まではエラーが出ずにアクセス可能でした。

PHP7.4リリース時点では、今後削除される予定はありません。

角括弧による文字列以外へのアクセス

$int=1234567980;echo$int[1];// Notice: Trying to access array offset on value of type int$null=null;echo$null[0];// Notice: Trying to access array offset on value of type null

int、float、bool、null、resourceに角括弧でアクセスするとE_NOTICEが発生します。
これまではエラーは出ませんでしたが、値はnullでした。

配列やstringへの[]アクセスは当然ながら今後も可能です。

htmlentities

htmlentities関数に、UTF-8以外のマルチバイト文字列を渡すとE_NOTICEが発生するようになります。

htmlentities('abc',ENT_COMPAT,'Shift_JIS');// htmlentities(): Only basic entities substitution is supported for multi-byte encodings

PHP7.1.25以降はE_STRICTが発生し、それ以前は何のエラーも起こりませんでした。
どういう意図なのかはよくわかりません。

parent without parent

親クラスのないクラスでparentを使うと、そのメソッドが呼ばれなくてもコンパイル時にE_DEPRECATEDが発生するようになります。

classDummy{publicfunctionhoge(){returnparent::hoge();}}// Deprecated: Cannot use "parent" when current class scope has no parent

PHP7.3までは、メソッドを呼ばないかぎり何も起きませんでした。

メソッドを呼び出すと、PHP7.3でも7.4でも当然Fatal errorが発生します。

BCMath

BCMath関数に数値形式でない文字列を渡すとE_WARNINGが発生するようになりました。

echobcadd("2","3a");// Warning: bcadd(): bcmath function argument is not well-formed

PHP7.3までは何も出ませんでした。

値そのものは、昔から0として扱われていました。
上記例でいうと、出力はPHP5時代からずっと"2"のままです。
解釈が通常のPHP関数と異なるので少々わかりにくいですね。

base_convert

base_convert関数に無効な文字が渡された場合、E_DEPRECATEDが発生するようになりました。

base_convert('012',2,10);// Deprecated: Invalid characters passed for attempted conversion, these have been ignored

PHP7.3までは何のエラーも出ませんでした。
いずれにせよ、途中に出てくる無効な値は単に無視されます。

base_convertのほか、hexdecoctdecbindecにも同じ変更が入ります。
単に全ての関数が同じ内部関数_php_math_basetozvalを使っているからという理由ですが。

socket_addrinfo_lookup

socket_addrinfo_lookup関数において、引数AI_IDN_ALLOW_UNASSIGNEDとAI_IDN_USE_STD3_ASCII_RULESがE_DEPRECATEDになりました。
そもそもマニュアルが日本語化されてすらいないほど、この関数自体が使われていません。

Deprecate LDAP

ldap_control_paged_result_responseldap_control_paged_resultがE_DERECATEDになりました。
かわりにldap_searchを使えということだそうです。

また、nsldapumich_ldapのサポートが削除されました。

何のことだかさっぱりわかりません。

Deprecate is_real

is_real関数および(real)キャストはPHP7.4でE_DEPRECATEDになり、PHP8で削除されます。

is_real(1);// Deprecated: Function is_real() is deprecatedis_float(1);// OK

is_realではなくis_floatを使いましょう。

Deprecate Magic quotes

get_magic_quotes_gpcおよびget_magic_quotes_runtime関数はPHP7.4でE_DEPRECATEDになり、PHP8で削除されます。

get_magic_quotes_gpc();// Deprecated: Function get_magic_quotes_gpc() is deprecatedget_magic_quotes_runtime();// Deprecated: Function get_magic_quotes_runtime() is deprecated

Magic quotesはPHP5.4で滅びました。

array_key_exists

array_key_exists関数がオブジェクトを受け付けなくなりました。

array_key_exists(1,newstdClass());// Deprecated: array_key_exists(): Using array_key_exists() on objects is deprecatedarray_key_exists(1,[]);// OK

array_key_existsの引数はarrayですが、下位互換性のためにobjectも受け入れていました。
PHP7.4でE_DEPRECATEDになり、PHP8ではエラーになります。

Deprecate FILTER_SANITIZE_MAGIC_QUOTES

定数FILTER_SANITIZE_MAGIC_QUOTESはPHP7.4でE_DEPRECATEDになり、PHP8で削除されます。

filter_var('s',\FILTER_SANITIZE_MAGIC_QUOTES);// Deprecated: filter_var(): FILTER_SANITIZE_MAGIC_QUOTES is deprecatedfilter_var('s',\FILTER_SANITIZE_ADD_SLASHES);// OK

FILTER_SANITIZE_MAGIC_QUOTESとFILTER_SANITIZE_ADD_SLASHESは全く同じですが、magic_quotesという不穏当な表現を消去するために削除されます。

Deprecate ReflectionFunction::export

Reflector::exportはPHP7.4でE_DEPRECATEDになり、PHP8で削除されます。

ReflectionFunction::export('_');// Deprecated: Function ReflectionFunction::export() is deprecatedechonewReflectionFunction('_');// OK

Reflector::exportは引数がvoidなのに、継承した各クラスは引数を受け取ります。
これはおかしいのでどうにかしなければならなかったのですが、単純にexportが削除されることになりました。

mb_strrpos

mb_strrpos関数の第三引数に文字エンコーディングを渡せなくなります。

mb_strrpos('haystack','needle','UTF-8');// Deprecated: mb_strrpos(): Passing the encoding as third parameter is deprecatedmb_strrpos('haystack','needle',0,'UTF-8');// OK

mb_strrposは、歴史的経緯により第三引数でも文字エンコーディングを受け取ることが可能でした。
この書き方はPHP7.4でE_DEPRECATEDになり、PHP8で削除されます。

implode

implode関数の引数を逆にできなくなります。

implode([1,2],',');// Deprecated: implode(): Passing glue string after array is deprecatedimplode(',',[1,2]);// OKimplode([1,2]);// OK ','区切りと同じ

implodeは、歴史的経緯により第一引数と第二引数を逆に渡すことが可能でした。
この書き方はPHP7.4でE_DEPRECATEDになり、PHP8で削除されます。
引数がひとつでの場合は今後も受け付けますが、これも使用しない方が無難でしょう。

クロージャ

クロージャから$thisを削除できなくなります。

(newclass{publicfunctiondummy(){returnfunction(){isset($this);};}})->dummy()->bindTo(null);// Deprecated: Unbinding $this of closure is deprecated

クラスメソッドでクロージャを定義すると勝手に$thisが入ってきます。
PHP7.3までは、これを無理矢理削除することができました。
PHP7.4でE_DEPRECATEDになり、PHP8でエラーになります。

Deprecate hebrevc

hebrevc関数はPHP7.4でE_DEPRECATEDになり、PHP8で削除されます。

hebrevc('a');// Deprecated: Function hebrevc() is deprecated

テキストに直接HTMLタグを書き込むという不適切な仕様が入っているためです。
かわりにnl2br(hebrev($str))を使いましょう。

Deprecate convert_cyr_string

convert_cyr_string関数はPHP7.4でE_DEPRECATEDになり、PHP8で削除されます。

convert_cyr_string('str','k','i');// Deprecated: Function convert_cyr_string() is deprecated

非常に古い関数であるため、文字コードの指定方法が他の関数と一貫していません。
今後はiconvやmb_convert_encodingを使いましょう。

Deprecate money_format

money_format関数はPHP7.4でE_DEPRECATEDになり、PHP8で削除されます。

money_format('%i',123456789);// Fatal error: Uncaught Error: Call to undefined function money_format()

money_format() は Windows では 定義されていません。

おふぅ。

money_formatは環境によって使用できなかったり、ロケールによって結果が変わったりします。
今後はNumberFormatter::formatCurrency等を使いましょう。

Deprecate ezmlm_hash

ezmlm_hash関数はPHP7.4でE_DEPRECATEDになり、PHP8で削除されます。

ezmlm_hash('test@example.com');// Deprecated: Function ezmlm_hash() is deprecated

そもそもこの関数を使っている人はいるのだろうか。

Deprecate restore_include_path

restore_include_path関数はPHP7.4でE_DEPRECATEDになり、PHP8で削除されます。

restore_include_path();// Deprecated: Function restore_include_path() is deprecated

set_xxx関数をを多重に積んだ場合、restore_error_handlerrestore_exception_handler等は変更をひとつ前の状態に戻すのに対し、restore_include_pathだけはいきなり初期値に戻します。
動作が異なっており混乱の元であるため削除されます。
今後はini_restore('include_path')を使いましょう。

Deprecate allow_url_include

allow_url_includeディレクティブはPHP7.4でE_DEPRECATEDになり、PHP8で削除されます。

allow_url_include=On

Apacheを起動すると、エラーログにPHP Deprecated: Directive 'allow_url_include' is deprecated in Unknown on line 0が入ります。
allow_url_includeはrequire_once('http://example.com/')と書けるようになるという、ヤバみしかない機能なので絶対にオンにしてはいけません。

感想

変更多過ぎぃ!

いやあPHP7.4やばいですね。
重量級だけでもプロパティ型指定・アロー関数・プリローディング・FFIなどがあって、これだけでも文法や書き方が大きく変化する勢いです。
さらに上記リストには、単なるバグ修正は含まれていません。
そこまで含めたら、とんでもない量の変更になってしまいますね。

PHPは、特にPHP7以降はモダンな書き方をごりごり取り入れていて、きちんと使えば相当に堅牢で厳格な記述を行うことが可能です。
ただ同時に古い書き方も相変わらず可能で、しかも古い書式のほうが圧倒的に文献が多いせいでなかなか新しい文法が広まらないという現状は残念なところです。
新機能を使おうにも未だにPHP5系しか使えないようなサーバも存在したりしますし、そもそもCentOS7のデフォルトPHPが未だに5.4という地雷ですからね1
もっと知識や環境を新陳代謝して使える仕組みが欲しいところです。
まあ、ほんのちょっとでも古い書き方をしようものなら全方位から銃弾が飛んでくるJavaScriptのような世界がいいかって言われたらそれも嫌ですけどね。

さすがにいきなり仕事でプロパティ型指定を書き始めたりはしませんが、一年後には大抵の場所で使えるようになっている、くらいに程々に普及するくらいには進んでほしいところです。


  1. 2019年9月リリースのCentOS8でようやくPHP7.2になった。 

Viewing all 113 articles
Browse latest View live