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

PHP8で削除される機能

$
0
0

2017/06/22にPHP7.2.0 α2が出ました。
アップグレードガイドにDeprecated Functionalityという項目がありました。

All the deprecated functionality listed in the following will be removed in PHP 8.0.

ここに書いてあるやつはPHP8.0で削除されるぜ、とか書いてあるので見てみましょう。
基本的にPHP7.2で非推奨になった機能はPHP8.0で削除される予定です。

PHP8.0で削除される機能

Core

The trace_errors ini directive has been deprecated.

そもそもtrace_errorsってディレクティブが存在しないっぽいんだけどこれ本当に正しいのか?
track_errorsなんじゃないのという気がしてならない。
のっけからこんな間違いとか先が思いやられる。

The __autoload() mechanism has been deprecated

__autoloadはもう不要です。
今や完全上位互換のspl_autoload_registerがあるため、そちらを使いましょう。

The (unset) cast has been deprecated

(unset)キャストunset()関数とは別物で、他の(int)(string)等と同じ動作でnullにするだけのものです。
普通に(null)ってしておけばよかったのに、どうしてこんな名前になってたのでしょうか。

    $a = 1;
    (unset)$a;
    echo $a; // 1
    unset($a);
    echo $a; // Notice: Undefined variable: a

The create_function() function has been deprecated

create_functionは過去の遺物です。
クロージャを使いましょう。
というか、関数のcallable引数に渡すとき以外は普通に関数にすればいいと思うのだが。

The each() function has been deprecated

eachはforeachより10倍遅いうえ、他の機能に邪魔だったりといいことがないので削除されます。
foreachを使わずあえてeachを使わなければいけない場面って存在するだろうか。

GD

png2wbmp() and jpeg2wbmp() have been deprecated

謎のWBMP出力関数が削除されます
GDは関数名識別とリソース引き回しが面倒なので、オブジェクト指向で書き直してくれないかな。

GMP

The gmp_random() function has been deprecated

gmp_randomはプラットフォームのビット数に依存して出力範囲が変わってしまうという大問題がありました。
かわりにプラットフォームに依存しないgmp_random_bitsか、そもそも範囲を数値で指定できるgmp_random_rangeを使いましょう。

Intl

INTL_IDNA_VARIANT_2003 has been deprecated

INTL_IDNA_VARIANT_2003はは非推奨になったので、かわりにINTL_IDNA_VARIANT_UTS46を使いましょう。
とか言われても、そもそもIDNA2003とかUST46とかが何なのかがさっぱりわからん

Mbstring

The mbstring.func_overload ini directive has been deprecated

mbstringの関数オーバーロードが撤廃されます。
マルチバイト対応が必要なら、普通にmb_strposみたいに書きましょう。
経緯についてはこのあたりのMLみたいですが、よくわかりませんでした。

Standard

Calling parse_str() without the result argument has been deprecated

parse_strは第二引数を指定しないときは変数を現在のスコープに展開するというregister_globals機能があります。
今後は第二引数を指定しないと使えなくなります。
むしろ参照渡しをやめて普通に返り値にして欲しかった。

    parse_str($query_string); // 'a=1'が$aに展開される
    parse_str($query_string, $result); // 'a=1'が$result['a']に展開される

    $result = parse_str($query_string); // こうしてほしかった

Calling assert() with a string argument has been deprecated

assertに文字列を渡すとevalしていましたが、今後は式を渡すことになります。
PHP5のassertは実はなんちゃってassertだったのですが、PHP7以降ノーコストで実行できるようになり実用的になりました。

PHP7.2で削除された機能

Mcrypt

PHP7.1でDeprecatedになったMcryptが、予告通り削除されました。
今後はPECL行きで、一応インストールすれば今後も使うことはできる模様。

まとめ

今からでもこれらの機能は使わないようにしておきましょう。
といっても、まともに運用してるところではほとんど残ってないような機能しかない気もするけど。


【PHP8】PHPでJITが使えるようになる

$
0
0

JITのRFCが2019/03/21に投票開始されました。
締切は2019/03/28ですが、2019/03/27時点で賛成48反対2でほぼ導入確定です。

JITとは

JIT is 何?

PHPは現在は、アクセスが来るたびにソースコードを全部読み取って、opcodeに変換して、順番に逐次実行して、実行が終了したら全てのコードを破棄するというインタプリタ型のプログラミング言語で、処理速度は遅いです。
遅いと言っても、やってる内容からすれば異常なまでに早いんですけどね。

opcodeはCPUやOSなどの実行環境によらず同一のコードが生成されます。
逐次実行するときはさらに実行環境ごとのネイティブコードに変換して実行されます。
OPcacheは、この変換後のopcodeをメモリに保存しておいて、次のリクエストでも使い回すという仕組みです。

JITはもう一段階進んだもので、リクエストが来たらソースコードを読んでopcodeにするまでは同じですが、その後一気にネイティブコードにまで変換してしまいます。
もう一度同じ処理が呼ばれたときはネイティブコードを直接実行することで、処理速度が非常に速くなります。
また、このネイティブコードをメモリに保存して使い回すこともできるようになります。

ただし、ネイティブコードはCPUが直接実行するコードなので、CPUの種類や世代によって異なるものとなります。
実行環境によって異なるネイティブコードを作らないといけないので、大規模な改修が必要で、コードも複雑になってたいへんです。
どのくらい大規模かというと、900コミット5万行の追加という目眩のする量です。

要するにどういうこと?

OPcacheのすごいやつ。

JIT RFC

Introduction

元々PHP7でJITを実装しようと企んでいて、2011年からZendが(ほとんどはDmitryが)色々と試していたんだけど、諸々の理由で結局PHP7に入ることはありませんでした。
理由はというと、当時の方法ではたいして早くならなかったこと、そのわりに複雑だったこと、そしてJIT以外にも多くのパフォーマンス向上技術の投入があったことです。

実際PHP7は、処理時間がPHP5の半分以下になるという驚異的な高速化がなされています。
当時はそれらの高速化に注力したため、JITの導入は見送られました。

The Case for JIT Today

現在のPHPにおけるJITの導入には、以下のようなメリットが見込まれます。

まず、JIT以外の最適化戦略による高速化は、そろそろ限界に達しつつあります。
つまり、JITを使わないかぎり、これ以上の高速化は見込めません。

次に、JITを導入することによって、Web以外のCPUを多用するような処理をPHPで書く、という選択肢が有力なものになります。

最後に、C言語ではなく、もしくはC言語のかわりに、PHPで組み込み関数を開発することが可能になります。
現在のPHPでそのような戦略を採るには大きな壁となっている、パフォーマンス劣化という問題の影響をほとんど受けなくなります。
さらにPHPベースで開発すれば、C言語ベースでの開発では往々にして発生するメモリ管理、オーバーフローといった問題を、言語レベルで安全にしてくれます。

Proposal

PHP8でJITを提供します。

PHP JITはOPcacheの一部として、しかしほぼ独立したものとして実装されます。
PHPのコンパイル時に有効無効を設定します。
有効にした場合、PHPファイルのネイティブコードがOPCacheの共有メモリに保存されるようになります。

ネイティブコードの生成にはDynAsmを使用します。
これはLuaJITプロジェクトで開発された、非常に軽量で高度なツールです。
しかし同時に、ターゲットのアセンブラ言語に対する高レベルの知識を要求します。
かつてLLVMを試してみたものの、コード生成速度は100倍遅く使い物になりませんでした。
DynAsmはPOSIXとWindows上でのx86とx86_64、およびARMをサポートしています。
従って、現在のPHPがサポートしている一般的なプラットフォーム全てに対応できるはずです。
努力すれば。

あと、ここで一部の内部実装について触れてるのですがよくわかりませんでした。
additional IR formに対応してないとか、opcacheオプティマイザのSSA静的解析フレームワークがネイティブコードを生成してるよとか、解析後long型になったらメモリじゃなくてCPUレジスタに直接登録するよとか、PHP JITのレジスタ割り付けアルゴリズムはすごいよとか、なんかそんなことが書いてあったりするようなないような。 
 ※詳細は@sj-i氏のコメント参照

パフォーマンス

以下の関数のベンチマークが公開されています。

function iterate($x,$y){
    $cr = $y-0.5;
    $ci = $x;
    $zr = 0.0;
    $zi = 0.0;
    $i = 0;
    while (true) {
        $i++;
        $temp = $zr * $zi;
        $zr2 = $zr * $zr;
        $zi2 = $zi * $zi;
        $zr = $zr2 - $zi2 + $cr;
        $zi = $temp + $temp + $ci;
        if ($zi2 + $zr2 > BAILOUT)
            return $i;
        if ($i > MAX_ITERATIONS)
            return 0;
    }
}

実行結果は以下のとおり。

環境 実行時間
PHP7-JIT (JIT=on) 0.011
gcc -O2 (4.9.2) 0.013
LuaJIT-2.0.3 (JIT=on) 0.014
gcc -O0 (4.9.2) 0.022
HHVM-3.5.0 (JIT=on) 0.030
Java-1.8.0 (JIT=on) 0.059
LuaJIT-2.0.3 (JIT=off) 0.073
Java-1.8.0 (JIT=off) 0.251
PHP-7 0.281
squirrel-3.0.4 0.335
Lua-5.2.2 0.339
PHP-5.6 0.379
PHP-5.5 0.383
PHP-5.4 0.406
ruby-2.1.5 0.684
PHP-5.3 0.855
HHVM-3.5.0 (JIT=off) 0.978
PHP-5.2 1.096
python-2.7.8 1.128
PHP-5.1 1.217
perl-5.18.4 2.083
PHP-4.4 4.209
PHP-5.0 4.434

バージョンは不明ですが素のPHP7の20倍以上、HHVMの3倍、そしてgccやLuaJITより速い。
さすがに何か間違ってるんじゃないか?と目を疑いたくなる結果です。

なお、このベンチは4年前のものです。
RFCによるとJITなしのPHP7.4では0.046秒ということでした。
PHP7.4では0.046秒で、PHP7では0.281秒って、既にこの時点でおかしい気がするぞ?
単に実行環境の違いでしょうか。

あと、よく見ると、PHPだけob_start/ob_end_flushを使って出力を抑制してるのが気になりますね。
PHP同士での比較には影響ありませんが、他言語との比較はフェアでないと思われます。

ちなみにGoで並列処理したら0.002秒だったそうです。はえー。

実際に出力されたネイティブコード
JIT$Mandelbrot::iterate: ; (/home/dmitry/php/bench/b.php)
    sub $0x10, %esp
    cmp $0x1, 0x1c(%esi)
    jb .L14
    jmp .L1
.ENTRY1:
    sub $0x10, %esp
.L1:
    cmp $0x2, 0x1c(%esi)
    jb .L15
    mov $0xec3800f0, %edi
    jmp .L2
.ENTRY2:
    sub $0x10, %esp
.L2:
    cmp $0x5, 0x48(%esi)
    jnz .L16
    vmovsd 0x40(%esi), %xmm1
    vsubsd 0xec380068, %xmm1, %xmm1
.L3:
    mov 0x30(%esi), %eax
    mov 0x34(%esi), %edx
    mov %eax, 0x60(%esi)
    mov %edx, 0x64(%esi)
    mov 0x38(%esi), %edx
    mov %edx, 0x68(%esi)
    test $0x1, %dh
    jz .L4
    add $0x1, (%eax)
.L4:
    vxorps %xmm2, %xmm2, %xmm2
    vxorps %xmm3, %xmm3, %xmm3
    xor %edx, %edx
.L5:
    cmp $0x0, EG(vm_interrupt)
    jnz .L18
    add $0x1, %edx
    vmulsd %xmm3, %xmm2, %xmm4
    vmulsd %xmm2, %xmm2, %xmm5
    vmulsd %xmm3, %xmm3, %xmm6
    vsubsd %xmm6, %xmm5, %xmm7
    vaddsd %xmm7, %xmm1, %xmm2
    vaddsd %xmm4, %xmm4, %xmm4
    cmp $0x5, 0x68(%esi)
    jnz .L19
    vaddsd 0x60(%esi), %xmm4, %xmm3
.L6:
    vaddsd %xmm5, %xmm6, %xmm6
    vucomisd 0xec3800a8, %xmm6
    jp .L13
    jbe .L13
    mov 0x8(%esi), %ecx
    test %ecx, %ecx
    jz .L7
    mov %edx, (%ecx)
    mov $0x4, 0x8(%ecx)
.L7:
    test $0x1, 0x39(%esi)
    jnz .L21
.L8:
    test $0x1, 0x49(%esi)
    jnz .L23
.L9:
    test $0x1, 0x69(%esi)
    jnz .L25
.L10:
    movzx 0x1a(%esi), %ecx
    test $0x496, %ecx
    jnz JIT$$leave_function
    mov 0x20(%esi), %eax
    mov %eax, EG(current_execute_data)
    test $0x40, %ecx
    jz .L12
    mov 0x10(%esi), %eax
    sub $0x1, (%eax)
    jnz .L11
    mov %eax, %ecx
    call zend_objects_store_del
    jmp .L12
.L11:
    mov 0x4(%eax), %ecx
    and $0xfffffc10, %ecx
    cmp $0x10, %ecx
    jnz .L12
    mov %eax, %ecx
    call gc_possible_root
.L12:
    mov %esi, EG(vm_stack_top)
    mov 0x20(%esi), %esi
    cmp $0x0, EG(exception)
    mov (%esi), %edi
    jnz JIT$$leave_throw
    add $0x1c, %edi
    add $0x10, %esp
    jmp (%edi)
.L13:
    cmp $0x3e8, %edx
    jle .L5
    mov 0x8(%esi), %ecx
    test %ecx, %ecx
    jz .L7
    mov $0x0, (%ecx)
    mov $0x4, 0x8(%ecx)
    jmp .L7
.L14:
    mov %edi, (%esi)
    mov %esi, %ecx
    call zend_missing_arg_error
    jmp JIT$$exception_handler
.L15:
    mov %edi, (%esi)
    mov %esi, %ecx
    call zend_missing_arg_error
    jmp JIT$$exception_handler
.L16:
    cmp $0x4, 0x48(%esi)
    jnz .L17
    vcvtsi2sd 0x40(%esi), %xmm1, %xmm1
    vsubsd 0xec380068, %xmm1, %xmm1
    jmp .L3
.L17:
    mov %edi, (%esi)
    lea 0x50(%esi), %ecx
    lea 0x40(%esi), %edx
    sub $0xc, %esp
    push $0xec380068
    call sub_function
    add $0xc, %esp
    cmp $0x0, EG(exception)
    jnz JIT$$exception_handler
    vmovsd 0x50(%esi), %xmm1
    jmp .L3
.L18:
    mov $0xec38017c, %edi
    jmp JIT$$interrupt_handler
.L19:
    cmp $0x4, 0x68(%esi)
    jnz .L20
    vcvtsi2sd 0x60(%esi), %xmm3, %xmm3
    vaddsd %xmm4, %xmm3, %xmm3
    jmp .L6
.L20:
    mov $0xec380240, (%esi)
    lea 0x80(%esi), %ecx
    vmovsd %xmm4, 0xe0(%esi)
    mov $0x5, 0xe8(%esi)
    lea 0xe0(%esi), %edx
    sub $0xc, %esp
    lea 0x60(%esi), %eax
    push %eax
    call add_function
    add $0xc, %esp
    cmp $0x0, EG(exception)
    jnz JIT$$exception_handler
    vmovsd 0x80(%esi), %xmm3
    jmp .L6
.L21:
    mov 0x30(%esi), %ecx
    sub $0x1, (%ecx)
    jnz .L22
    mov $0x1, 0x38(%esi)
    mov $0xec3802b0, (%esi)
    call rc_dtor_func
    jmp .L8
.L22:
    mov 0x4(%ecx), %eax
    and $0xfffffc10, %eax
    cmp $0x10, %eax
    jnz .L8
    call gc_possible_root
    jmp .L8
.L23:
    mov 0x40(%esi), %ecx
    sub $0x1, (%ecx)
    jnz .L24
    mov $0x1, 0x48(%esi)
    mov $0xec3802b0, (%esi)
    call rc_dtor_func
    jmp .L9
.L24:
    mov 0x4(%ecx), %eax
    and $0xfffffc10, %eax
    cmp $0x10, %eax
    jnz .L9
    call gc_possible_root
    jmp .L9
.L25:
    mov 0x60(%esi), %ecx
    sub $0x1, (%ecx)
    jnz .L26
    mov $0x1, 0x68(%esi)
    mov $0xec3802b0, (%esi)
    call rc_dtor_func
    jmp .L10
.L26:
    mov 0x4(%ecx), %eax
    and $0xfffffc10, %eax
    cmp $0x10, %eax
    jnz .L10
    call gc_possible_root
    jmp .L10

後方互換性

互換性の壊れる変更はありません。

その他の影響

エクステンション

Xdebugのようなデバッガ、XHProf、Blackfire、Tidewaysといったプロファイラに影響が発生します。

Opcache

JITはOpcacheの一機能として実装されます。

追加される定数

追加される定数はありません。

デバッグ

JITのデバッグはとっても大変だよ!がんばれ!

php.ini

php.iniに複数の項目が追加されます。

opcache.jit_buffer_size

ネイティブコードのために予約するメモリサイズ。バイト単位で、K・Mの表記に対応。
デフォルトは0で、JIT無効という意味。

opcache.jit

JITの制御オプション。順番にCRTOを表し、デフォルトは"1205"。
おそらく"1235"に変更した方がいいかもしれない。

C

CPU最適化レベル、範囲は0-1。
0は使用しない、1はAVX命令セットを有効にする。

R

レジスタ割り当て、範囲は0-2。
0はレジスタ割り当てを使用しない、1はローカルレジスタ割り付け、2はグローバルレジスタ割り付け。

T

JITを起動するタイミング、範囲は0-5。
0は最初のスクリプト起動時に全機能を有効にする。
1は最初の処理実行時にJITを有効にする。
2は最初のリクエストでプロファイルを行い、2回目のリクエストでコンパイルする。
3はオンザフライでプロファイル、コンパイルを行う。
4は@jitってコメントが書いてある関数をコンパイルする。

O

最適化レベル、範囲は0-5。
0はJITを使わない、5が最も高度な最適化を行う。

opcache.jit_debug

JITデバッグ制御オプション。
デフォルトは0。
それぞれビット指定することで各種デバッグ情報を出力できるみたいですが、具体的に何が何なのかはよくわかりませんでした。
SSA formとかperf.mapとかJIt-ed codeとか出せるらしい。

パフォーマンス

bench.phpが0.320秒から0.140秒になりました。
CPUを多用する処理については、劇的な高速化が見込めます。
またNikitaによると、PHP-Parserが1.3倍速くなりました。

しかしながら、WordPressのようなWebアプリについては、さほど恩恵は見込めません。
315req/秒から326req/秒になった程度のようです。
このような現実的アプリについても高速化の改善を行う、追加の取り組みを実施予定です。

今後の展望

関数のプロファイリング後に最適化されたコードを生成することで、JITの改善を行う予定です。
またプリローディングやFFIとのより深い統合を行うことができるでしょう。
CではなくPHPで書かれた組み込み関数を提供する方法の標準化も見込めます。

投票

2019/03/21に投票開始、2019/03/28に投票終了。
可決には投票者の2/3+1の賛成が必要です。

PHP7.4

PHP7.4には入りませんでした。
ブランチもできていたのですが、7.4への導入は賛成18反対34で却下されました。
7.4はただでさえ新機能盛り盛りで大変ですからね。
このうえさらに5万行の追加とか、さすがに厳しいでしょう。

外部リンク

プルリクエスト / JITブランチ

コミット数900、追加5万行を超える非常に大規模なプルリクです。
こんなマージ作業、自分じゃ絶対やりたくない。

PHP7.4ブランチ

PHP7.4用のJITブランチ。
こちらが使われることはなさそうです。

DynASM / 非公式DynASMドキュメント

JITで使われるライブラリです。

[RFC] [VOTE] JIT

みんな『7.4は無理、8だけにすべき』というかんじです。
これだけ大規模な改修にもかかわらず、スレッドが意外と伸びてないのは、もはや対象バージョン以外語ることがないくらい予定調和だからでしょうか。

感想

普通にWebアプリを作っているかぎりにおいては、さしたる影響はないようです。
JITが真価を表すのは、バッチなどバックエンド処理においてでしょう。

一昔前であれば『PHPでバッチ?正気か!?』というイメージでしたが、今後はもはや下手な言語で書くよりPHPのほうが速い、までありそうですね。

なお、opcacheやそもそもJITについての理解があやふやなので、間違っている部分が多々あると思われます。
きっと誰かがプルリクしてくれるはず。

【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湧き潰しできず死ぬという事件がありましたが、それの再来にならないことを祈りましょう。

【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%の投票は廃棄されたんじゃなかったのか?

【PHP8】JIT=on の Docker を触ってみたい人。爆速だったよ。

$
0
0

PHP8 で JIT が使えるようになるそうなので、ワクワクが止まりません。手軽に触れるイメージが Docker ないものか。

TL;DR

PHP8-ish(master = 一番 PHP8 に近い状態)の PHP をソースからコンパイルして JIT を有効にした Docker イメージを作ってみました。Mac 以外にもラズパイ3、ラズパイZero の Docker で動くようにしています。

Mac に標準インストールされている PHP7.1 と比較して 2〜3 倍近く速いです。(詳しくは TS;DR)

docker pull keinos/php8-jit:latest

使い方

docker run --rm -it keinos/php8-jit:latest

基本構文

docker run --rm -it keinos/php8-jit:latest [コマンド] [引数]
コマンド実行例
docker run --rm -it keinos/php8-jit:latest php -a
  • docker run で「コンテナの起動」
  • --rm で「終了後にコンテナを破棄(remove)」(ロングオプションなので -- です)
  • -it でシェル操作をコンテナとつなげる。
    • -i -t もしくはロングオプションの --interactive --tty と同じ意味です。つまり、コンテナの標準入出力とローカルの標準入出力を tty 経由でつなげ、コンテナのシェル操作をローカルのシェルから行えるようになります。これを忘れると、実行コマンド終了後に入力待ちにならず処理が終わりコンテナがシャットダウンしてしまいます。
  • keinos/php8-jit:latest で「コンテナの元となるイメージを指定」
  • php でランタイムを指定
  • -a

対話モードで実行

PHP対話モードで実行
$ # コマンドや引数の指定がない場合は対話モードで起動します(デフォルトの動作)
$ docker run --rm -it keinos/php8-jit:latest
Interactive shell

php > echo phpversion();
8.0.0-dev
php > exit
$
$ # コマンドが php -a の場合と同じ動作です。
$ docker run --rm -it keinos/php8-jit:latest php -a
Interactive shell

php > echo phpversion();
8.0.0-dev
php > exit
$

PHP コマンドの直接実行

PHPコマンドを直接実行
$ # php --version の実行
$ docker run --rm -it keinos/php8-jit:latest php --version
PHP 8.0.0-dev (cli) (built: Jul 19 2019 07:09:36) ( NTS )
Copyright (c) The PHP Group
Zend Engine v4.0.0-dev, Copyright (c) Zend Technologies
    with Zend OPcache v8.0.0-dev, Copyright (c), by Zend Technologies
$
$ # php -r で `echo "hoge", PHP_EOL;` のスクリプトを実行
$ docker run --rm -it keinos/php8-jit:latest php -r 'echo "hoge", PHP_EOL;'
hoge
$

コンテナ内から PHP を実行

コンテナ内のシェルから実行
$ # /bin/sh でコンテナ内の Bourne Shell(sh)シェルを起動
$ docker run --rm -it keinos/php8-jit:latest /bin/sh
/ $ # コンテナ内の sh シェルから php -v を実行
/ $ php -v
PHP 8.0.0-dev (cli) (built: Jul 19 2019 07:09:36) ( NTS )
Copyright (c) The PHP Group
Zend Engine v4.0.0-dev, Copyright (c) Zend Technologies
    with Zend OPcache v8.0.0-dev, Copyright (c), by Zend Technologies
/ $ 
/ $ # コンテナ内の sh シェルから`echo "hoge", PHP_EOL;` のスクリプトを php -r で実行
/ $ php -r 'echo "hoge", PHP_EOL;'
hoge
/ $ # シェルを抜けてコンテナを終了
/ $ exit
$

コンテナにファイルをマウントして実行

ファイルをマウントして実行
$ # ローカルのファイルを用意
$ ls
test.php
$ cat test.php
<?php

echo 'Hello World!', PHP_EOL;

$ # test.php をマウントして実行
$ docker run --rm \
>   -v $(pwd)/test.php:/app/test.php \
>   keinos/php8-jit:latest \
>   php /app/test.php
Hello World!
$

コンテナにディレクトリをマウントして実行

ディレクトリをマウントして実行
$ # ローカルのディレクトリを確認
$ ls
src
$ ls src
index.php     functions.inc.php
$ # src ディレクトリをマウントして実行
$ docker run --rm \
>   -v $(pwd)/src:/app \
>   -w /app \
>   keinos/php8-jit:latest \
>   php index.php
Hello World! from a function.
$

ビルド

Docker Hub から Pull せずにイメージをビルドしたい場合は、リポジトリを clone してビルドしてください。

$ # 適当な作業ディレクトリに移動
$ cd ~/
$ # リポジトリを clone
$ git clone https://github.com/KEINOS/Dockerfile-of-PHP8-JIT.git php8
$ # リポジトリに移動
$ cd $_
$ # Docker イメージのビルド
$ docker build --no-cache -t php8-jit .
...

インストール済みの PHP モジュール

2019/10/04 現在コンパイル&インストール済みのモジュールは以下の通りです。追加して欲しいモジュールがあれば、リポジトリの Issues にあげてください。(日本語でおk)

$ docker run --rm -it keinos/php8-jit:latest php -m
[PHP Modules]
Core
ctype
curl
date
dom
FFI
fileinfo
filter
ftp
gd
hash
iconv
json
libxml
mbstring
mysqlnd
openssl
pcntl
pcre
PDO
pdo_sqlite
Phar
posix
readline
Reflection
session
SimpleXML
soap
sodium
SPL
sqlite3
standard
tokenizer
xml
xmlreader
xmlwriter
Zend OPcache
zlib

[Zend Modules]
Zend OPcache

TS;DR(リリースまで JIT 我慢できない)

上記の Qiita 記事を読んでワクワクが止まらず、触ってみたい欲にかられました。

非公式ながらも PHP 8.0.0 Dev 版の Docker イメージはあるのですが、残念ながらデフォルトで JIT が有効になっておらず、mb_string なども一緒にコンパイルされていません。

また、PHP 8.0.0 とは言っても最新の master ブランチを使っているので一番 PHP8 に近いバージョンというだけのようですが、それでも一応 master は PHP8 用として進められているようです。

JIT の正式採用は PHP8 からと決定されたので、この Docker イメージは「いちはやく JIT の挙動をみるための目的で作成された」とのこと。どうやら JIT を有効にした場合に PHP のコンパイルに難があるようで、それらの注意点をまとめた記事をベースに作られたそうです。

測定

そこで、上記記事を参考に JIT および mb_string を有効にした PHP8 の Dockerfile を作ってみたので、測定してみました。自分でコンパイルしてみたいかたは、リポジトリclone して、docker build してみてください。

測定は「フィボナッチ数」の算出と「ズンドコキヨシ」ループです。リポジトリには Zend の ベンチマークも置いてあります。

テストコード

比較用スクリプト

実行結果

  • 同じ PHP8 同士でも JIT を有効にするだけで 2〜3 倍近く速なった。
  • Docker で PHP を使うなら 7.3.6 がバランス的(速度・安定的に)にベストと思われる。
PHPバージョン 実行環境 実測値(フィボナッチ数) 
PHP 5.6.40 Docker 1.4402780532837
PHP 7.1.23 ローカル実行 0.3146538734436
PHP 7.1.23 Docker 0.57761478424072
PHP 7.3.6 Docker 0.23189496994019
PHP 8.0.0-dev Docker
(JIT=off)
0.24225091934204
PHP 8.0.0-dev Docker
(JIT=on)
0.093664884567261
PHPバージョン 実行環境 実測値(ズンドコキヨシ) 
php 5.6.40 Docker 2.3430778980255
PHP 7.1.23 ローカル実行 0.75591492652893
php 7.1.23 Docker 1.3070819377899
php 7.3.6 Docker 0.60567092895508
php 8.0.0-dev Docker
(JIT=off)
0.6335301399231
php 8.0.0-dev Docker
(JIT=on)
0.39926886558533
  • 検証結果
    • macOS のデフォルト PHP 7.1.23 のローカル実行と比較しても 2〜3 倍速い。
    • JIT を有効にしない場合は PHP8 on Docker がローカル実行の PHP7.1.23 よりやや速いくらい。
    • v7.1.23 と v7.3.6 では、v7.3.6 の方がローカルより速い。
  • 検証環境
    • マシン: MacBook Pro, 2.7 GHz IntelCore, 13inch Early 2015
    • OS: macOS Mojave (OSX 10.14.5)
    • Docker: 19.03.2

【PHP8.0】未定義変数へのアクセスが例外にな・・・らない

$
0
0

ついにこの日が来てしまったようです。

PHPのユルさの象徴のひとつとして『未定義変数に普通にアクセスできる』というものがあります。

echo $a; // Notice: Undefined variable: a

大抵の言語ではエラーや例外で落ちますが、PHPでは処理が中断することはありません。
警告は出ますが、最もエラーレベルの低いE_NOTICEです。

が、PHP8.0でこの挙動が変わることになりそうです。

Reclassifying engine warningsというRFCが提出され(提出者はいつものNikita)、2019/09/26まで投票中です。
これは様々な警告のエラーレベルを変更しようというRFCなのですが、ここではそのうちのUndefined variable、Undefined array indexに絞って紹介してみます。
RFCでもこれらの項目は特別に分けられていますしね。

Reclassifying engine warnings

変更には投票の2/3の賛成が必要です。

Proposal

警告や例外には主にE_NOTICE、E_WARNING、Error例外の3段階がありますが、一部の警告についてエラーレベルを変更します。
ただし上げる場合でも1段階しか上げません。
現在E_NOTICEの警告のエラーレベルを上げる場合はE_WARNINGに上げるだけで、いきなり例外まで上げるとはない、ということです。

ただしUndefined variableのみ例外。

Undefined variable

大抵の場合、未定義変数へのアクセスは重大なバグです。
現在のE_NOTICEというとても軽い警告は、register_globalsのような外から変数を定義してしまうことができていたPHP暗黒時代の遺物です。
理想的には、未定義変数へのアクセスはコンパイルエラーになるべきですが、PHPの性質上コンパイル時に全ての分析を行うことはできないため、このRFCでは代わりにError例外にすることを提案します。

しかしながら、例外にするとこれまでのように警告を無視することができなくなるため、大量の警告を抑制して運用しているようなレガシーコードは多大な改修が必要となります。
また一部には、未定義変数の使用は正当なコーディングスタイルであるとさえ考えている過激派も存在します。

これらの理由のため、Error例外にするか、E_WARNINGにするか、E_NOTICEのままにするかの個別投票を行います。

2019/09/14現在、Error例外が22票、E_WARNINGが8票、E_NOTICEのままが4票で、おそらくError例外になります。

Undefined array index

未定義変数や未定義プロパティと同様、未定義の配列キーへのアクセスは、モダンPHPではプログラミングのエラーであるはずです。
しかし、変数やプロパティは主に静的に定義されるのに対し、配列のキーは動的に生成されることが多いため、全く同じ扱いをすることは難しいかもしれません。

JavaScriptのような一部の言語は、未定義の配列キーへのアクセスをエラーにしませんし、そのような操作をサイレントに実行します。
PHPでも主流ではないものの、同じようなコーディングスタイルを取る者たちがいて、彼らは未定義の配列キーへのアクセスが抑制可能な警告であることを願っています。

従って個別投票により、E_WARNINGに上げるか、E_NOTICEのままかを決定します。

2019/09/14現在、E_WARNINGが23票、E_NOTICEのままが13票で拮抗しています。

感想

私としては、仕事とかで書くちゃんとしたコードは常にerror_reporting(-1)と書いているので、業務上は影響ありません。
ただ使い捨てで適当に書き散らすコードなんかは警告が出てても一回動けばいいやとかやったりしているのですが、今後はそのような手抜きが許されなくなります。
書いたらなんかとりあえず動く、という他言語に対するPHPの大きな利点(にして同時に欠点)を投げ捨ててまで得られるものがあるのかというと、個人的には少々疑問に感じます。

巷に溢れるテキトーなコードも大半が即死し、PHPを始める敷居が大きく上がってしまうことは間違いありません。
テキトーなコードなんて死滅してもいい?
むしろ絶滅させるべき?
みんな正しいコードを書くべき?
正しいコードしか許されないべき?

未定義変数アクセスの例外化は、プログラミング言語として正しい方向性でしょう。
しかし世の中、正しければ勝てるというわけでもありません
使用前の変数宣言なんて難しいコーディング、誰も彼もができるわけではないのです。
この正しい変更は、入口が狭まるというデメリットを上回るメリットを果たして得られるのでしょうか。

あと最大の問題点として、が不可能になります。
こりゃ致命的だ。

回避策はあるか?

レガシーコードではerror_reporting(E_ALL~E_NOTICE)なんてことをやって警告の通知を無視していたわけですが、例外になるとこれが無視できなくなります。
プログラムを変更せずに回避する方法はないでしょうか?

PHPには例外の挙動を変更するset_exception_handlerという関数があります。
しかし、これは実のところ単にプログラム全体を囲むtry/catchなので、発生自体を無視したり、発生箇所に戻って先に進むといったことはできません。

ということでちょっと思いつかないですね。
誰かがきっと考えてプルリクしてくれるはず。

うん、まあ、そもそも例外が出ないように修正しろよって話ですが。

【PHP8.0】オブジェクト初期化子のRFCが却下されそう

$
0
0

いつのまにやらObject InitializerというRFCが投票に入っていました。
ちょっとだけ面白そうと思ったのですが、ただ、ほぼ確実に却下されるので詳しく見てもしょうがないのでざっくり紹介してみます。

Object Initializer

文法

class Customer{
  public $id;
  public $name;
  private DateTimeImmutable $createdAt;
}

$customer = new Customer{
  id = 123,
  name = "John Doe",
};

newするときに中括弧で引数を渡すと、自動的にプロパティにセットされます。

キーが文字列ではないところが、PHPとして物凄い違和感がありますね。

上の例は、下のようなよくある文と同等です。

$customer = new Customer();
$customer->id = 123;
$customer->name = "John Doe";

従って、privateである$createdAtに値を突っ込むことはできません。

制約

オブジェクト初期化子を使う場合、全てのpublicプロパティを指定しなければなりません。

$customer = new Customer{
  id = 123, // RuntimeException class object failed due to missing required properties
};

オブジェクト初期化子自体を使わない場合は、普通にインスタンス化できます。

$customer = new Customer();

未定義プロパティ

未定義のプロパティに値を突っ込めます。

$baz = 'baz';

$obj = new stdClass {
  foo = "bar",
  $baz = true,
};

ええー、と思いますが、そもそもこれPHPの仕様だったわ。

コンストラクタ

オブジェクト初期化子を使う場合、コンストラクタに引数は渡せません。
即ち、以下のような書き方は文法エラーになります。

$customer = new Customer($dateTime){
  id = 123,
  name = "John Doe",
};

マジックメソッド

可視プロパティがなかった場合、普通にマジックメソッド__setが呼ばれます。
RFCに書かれている以下の例では、$nameはpublicなので直接値が入り、protectedである$emailはマジックメソッド__setが呼ばれることになるようです。

class EmailAddress
{
  protected string $email;
  public ?string $name = null;

  public function __set(string $name, $value): void
  {
    if ($name !== "email") {
      return;
    }
    if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
      throw new InvalidArgumentException("Invalid email address");
    }
    $this->email = $value;
  }
}

$email = new EmailAddress {
  email = "john.doe@example.org",
  name = "John Doe",
};

将来の予定

無名クラスstdClassを使うときは、もうクラス名すらも書かなくてよくない?

$obj = new {
  foo = "bar",
  $baz = true,
};

配列展開してpublicプロパティ突っ込めたらよくない?

$array = [
  'a' => 1,
  'b' => 2,
];

$obj = new { ...$array };

投票

2019/10/07投票開始、2019/10/21投票終了、可決には2/3+1の賛成が必要です。
2019/10/09現在は賛成3反対18で、ほぼ確実に却下されます。

この投票期間、RFCには何故か書かれていません。

感想

うん、まあ却下だよね。

特にpublicプロパティは全て指定しなければならないという厳格さと、未定義プロパティを指定できるという緩さが同居してるのは気持ち悪いというかなんというか。
これならまだAutomatic Property Initializationのほうがいいと思います。
Code free constructorよりはいいと思いますが。

ただ無名クラスはすごい便利そうなので、これは欲しいところですね。

$obj = new{
  'id' = 123,
  'name' = 'John Due',
};

まあ、今でもほぼ同じように書けたりはするんですけどね。

$obj = (object)[
  'id' => 123,
  'name' => 'John Due',
];

【PHP8.0】PHP8で警告のエラーレベルが軒並み厳しくなる

$
0
0

多くの警告について、PHP8.0でエラーレベルが変更されます。

これはReclassifying engine warningsというRFCで受理されたものです。
提案者はいつものNikita。
影響の大きい未定義変数アクセスについては個別に紹介しましたが、ここではそこで紹介しなかった細かい警告について見ていきます。

これまでE_NOTICEだった警告の一部がE_WARNINGに、これまでE_WARNINGだった警告の一部が例外になります。
E_WARNINGを抑制するような書き方をしている場合、PHP8では動かなくなる可能性が高いので気をつけましょう。
現在E_NOTICE以下であればいきなり動かなくなることはありませんが、そもそも抑制する書き方がよくないので、なるべく修正した方がよいでしょう。

エラーレベルの変更がない警告も並んでいるので、もしかしたら全警告が列挙されてるのか?と思ったのですが、expected to be a %s, %s givenとか色々無いものもあるので、全てを出しているわけではないようです。
どういう基準なんだろうか?

Reclassifying engine warnings

Attempt to increment/decrement property '%s' of non-object

E_WARNING → Error exception

オブジェクトではない変数のプロパティをインクリメント/デクリメントすると発生する。

    $a = 1;
    $a->b++;

Attempt to modify property '%s' of non-object

E_WARNING → Error exception

オブジェクトではない変数のプロパティを変更すると発生する。

    $a = 1;
    $a->b['c'] = 1;

Attempt to assign property '%s' of non-object

E_WARNING → Error exception

オブジェクトではない変数にプロパティを追加すると発生する。

    $a = 1;
    $a->b = 1;

このへん全部同じでいいんじゃないか?

Creating default object from empty value

E_WARNING → Error exception

未定義の変数にプロパティを追加すると発生する。

    $a->b = 1;

PHP7.3では$aが定義されるのだが、PHP8では何も定義されなくなると思われる。

ところで未定義の変数のプロパティを変更しようとすると何の警告もなくオブジェクトが生成されるのだが、こっちは今後もいいのだろうか。

    $a->b['c'] = 1; // エラー出ない
    var_dump($a); // object(stdClass)#1 (1) { ["b"]=> array(1) { ["c"]=> int(1) } }

Trying to get property '%s' of non-object

E_NOTICE → E_WARNING

オブジェクトではない変数のプロパティを参照すると発生する。

    $a = 1;
    $a->b;

Undefined property: %s::$%s

E_NOTICE → E_WARNING

オブジェクトの未定義プロパティを参照すると発生する。

$a = new stdClass();
$a->b;

PHPの場合、入力として外部引数やらAPIやらを使うことが多いため、読み取りの失敗については書き込みより寛容気味。

Cannot add element to the array as the next element is already occupied

E_WARNING → Error exception

配列の自動挿入による整数キーがPHP_INT_MAXを超えたときに発生する。

    $a = [
        PHP_INT_MAX => 1,
    ];
    $a[] = 2;

ちなみに計算値で指定すれば、PHP_INT_MAXを超えていてもいける。

    $a = [
        PHP_INT_MAX => 1,
    ];
    $a[PHP_INT_MAX+1] = 2; // -2147483648とかになる

Cannot unset offset in a non-array variable

E_WARNING → Error exception

エラーの出し方がわからない

Cannot use a scalar value as an array

E_WARNING → Error exception

文字列型ではないスカラー型の変数に配列値を追加すると発生する。

    $a = true;
    $a[] = 1;

文字列型の場合は文字単位アクセスという正しい文法。

ちなみにnullで初期化した場合は問題なく動く。

    $a = null;
    $a[] = 1; // [ 0 => 1]

なぜかfalseでも動く。

    $a = false;
    $a[] = 1; // [ 0 => 1]

Trying to access array offset on value of type %s

E_NOTICE → E_WARNING

文字列型ではないスカラー型の変数を配列形式で読み込もうとすると発生する。

    $a = true;
    $a[1];

このE_NOTICE自体PHP7.4で追加されたもので、それ以前は何も出さずにnullを返していた。

Only arrays and Traversables can be unpacked

E_WARNING → TypeError exception

関数呼び出し時の引数展開にiterableでない値を渡すと発生する。

    var_dump(...1);

unpackとは特に関係ない。

Invalid argument supplied for foreach()

E_WARNING → TypeError exception

iterableでない値をforeachすると発生する。

    $a = 1;
    foreach($a as $loop){}

Illegal offset type

E_WARNING → TypeError exception

配列のキーに配列やオブジェクトを指定すると発生する。

$a = [
    new stdClass() => 1,
    [] => 2,
];

Illegal offset type in isset or empty

E_WARNING → TypeError exception

issetおよびemptyでチェックする配列のキーに配列やオブジェクトを指定すると発生する。

    $a = [];
    isset($a[new stdClass()]);

ちなみに$aが未定義やスカラー型の場合は何のエラーも起こらない。

    isset($a[new stdClass()]); // エラー出ない
    $a = 1;
    isset($a[new stdClass()]); // エラー出ない

未定義やint型等であれば配列形式アクセスした時点でfalseだから中身を見る必要もないというのはわかるが、文字列型でもエラーが出ない理由はよくわからない。

    $a = 'a';
    isset($a[1]); // true
    isset($a[new stdClass()]); // false エラー出ない

Illegal offset type in unset

E_WARNING → TypeError exception

unsetする配列のキーに配列やオブジェクトを指定すると発生する。

    $a = [];
    unset($a[new stdClass()]);

$aが未定義の場合Illegal offset typeは発生しないが、かわりにUndefined variableのE_NOTICEが出る。
文字列以外のスカラー型には何のエラーも出さず、文字列型やオブジェクトにはFatal errorが発生する。

    unset($a[new stdClass()]); // E_NOTICE: Undefined variable
    $a = 1;
    unset($a[new stdClass()]); // エラー出ない
    $a = 'a';
    unset($a[new stdClass()]); // Fatal error: Cannot unset string offsets
    $a = new stdClass();
    unset($a[new stdClass()]); // Fatal error: Cannot use object of type stdClass as array

このあたりの法則はさっぱりわからない。

Indirect modification of overloaded element of %s has no effect

E_NOTICEのまま

SplFixedArrayに突っ込んだ配列の値を直接変更すると発生する。

    $a = new SplFixedArray(1);
    $a[0] = [1];
    $a[0][0] = 2;

値を変更しているつもりだが、実際には変更されていないという注意。

SplFixedArrayに限らず、ArrayAccessをimplementsしたクラスに一般的に発生する症状のようだ。

Indirect modification of overloaded property %s::$%s has no effect

E_NOTICEのまま

マジックメソッド__getが配列を返す場合、その返り値を直接変更すると発生する。

    class A{
        private $value = ['a' => 1, 'b' => 2];
        public function __get($k){
            return $this->value;
        }
    }

    $a = new A;
    $a->value['a'] = 3;

こちらも値を変更したつもりだが、実際には変更されていない。

なお配列ではなくオブジェクトであれば、エラーも出ないし値を直接変更できてしまう。

    class A{
        private $obj;
        public function __construct(){
            $this->obj = new stdClass();
        }
        public function __get($k){
            return $this->obj;
        }
    }

    $a = new A;
    $a->obj->b = 1;

    var_dump($a); // { 'obj' => stdClass{ 'b'=>1 } }

Object of class %s could not be converted to int/float/number

E_NOTICEのまま

オブジェクトをスカラー型にキャストすると発生する。

    (int)new stdClass();

緩い比較が内部的にこのキャストを使用しているため、オブジェクトとスカラー型を緩く比較するとE_NOTICEが発生する。

    $a = new stdClass();
    var_dump($a == 1); // E_NOTICE
    var_dump($a === 1); // エラー出ない

比較ではエラーが出るべきではないので、こちらの問題がどうにかなるまでエラーレベルを変更しない。

A non-numeric value encountered

E_WARNINGのまま

次項で一緒に解説する。

A non well formed numeric value encountered

E_NOTICEのまま

非数値文字列を数値演算すると発生する。

    1 + '1';  // エラー出ない
    1 + '1a'; // E_NOTICE: A non well formed numeric value encountered
    1 + 'a';  // E_WARNING: A non-numeric value encountered

完全に数値形式の文字列ではエラーは出ず、一部だけ数値として評価できるときはnon well formed numeric value、完全に数値でない場合はnon-numeric valueになる。
今回はエラーレベルが変わらないが、数値形式文字列の計算は安全のためキャストしておいた方がよいだろう。

    1 + (int)'a'; // エラー出ない

Accessing static property %s::$%s as non static

E_NOTICEのまま

staticプロパティにインスタンスからアクセスすると発生する。

    class A{
        public static $property = 1;
    }

    $a = new A();
    $a->property;

正しくは$a::$property、もしくはA::$property
インスタンス内部からであればself::$propertyもいける。

Array to string conversion

E_NOTICE → E_WARNING

配列を文字列型にキャストすると発生する。

    (string)[];

変換前の配列の中身がどうなっていたとしても変換後の文字列はArrayになるので、実質的に機能していない状態なのでExceptionでもいい気がする。

Resource ID#%d used as offset, casting to integer (%d)

E_NOTICE → E_WARNING

リソースIDを配列のキーとして使用すると発生する。

    $fp = fopen('hoge', 'w+');
    $array = [$fp => $fp];
    var_dump($array); // []

リソースIDは整数っぽい値であり、かつプログラム中ではユニークなので、このような使い方ができそうではあるが実際は動いていない。
明示的にキャストするとint型になるため警告は発生せず、正しく動作する。

    $fp = fopen('./hoge', 'w+');
    $array = [(int) $fp => $fp];
    var_dump($array); // [1=>resource]

そもそも動いてないので、これもいきなりExceptionでいい気がしないでもない。

String offset cast occurred

E_NOTICE → E_WARNING

文字列への角括弧オフセットアクセスのキーに整数ではない数値を使ったときに発生する。

    'string'[1.5];
    'string'[true];

下の項目と同じような内容なのでエラーレベルを揃えたという話のようだ。

Illegal string offset '%s'

E_WARNINGのまま

文字列への角括弧オフセットアクセスのキーに数値ではない値を使ったときに発生する。

    'string'['a'];

Uninitialized string offset: %d

E_NOTICE → E_WARNING

文字列への角括弧オフセットアクセスで範囲外の値を読み込もうとしたときに発生する。

    'string'[10];

Illegal string offset: %d

E_WARNINGのまま

文字列への角括弧オフセットアクセスでマイナスの範囲外の値を変更したときに発生する。

    $str = 'string';
    $str[-10] = 'a';

正の範囲外を変更したときは単に文字列が伸びるだけでエラーは発生しない。

    $str = 'string';
    $str[10] = 'a'; // 'string    a'

Cannot assign an empty string to a string offset

E_WARNING → Error exception

文字列への角括弧オフセットアクセスで値を空文字に変更しようとしたときに発生する。

    $str = 'string';
    $str[1] = '';

2文字以上与えた場合は2文字目以降が無視されるだけでエラーは発生しない。

    $str = 'string';
    $str[1] = 'abcde'; // 'saring'

Only variables should be passed by reference

E_NOTICEのまま

リファレンス関数に値を直接渡すと発生する。

    sort([2, 1]);

Only variable references should be returned by reference

E_NOTICEのまま

リファレンス返しで値を直接返すと発生する。

    function &ref(){
        return 1;
    }
    ref();

Only variable references should be yielded by reference

E_NOTICEのまま

リファレンス返しで値を直接yieldすると発生する。

    function &ref(){
        yield 1;
    }
    foreach(ref() as $v);

リファレンス返しは百害しかないので使用してはならない。

Only variables should be assigned by reference

E_NOTICEのまま

リファレンスではない関数をリファレンスで受け取ろうとすると発生する。

    function ref(){
        return 1;
    }
    $x = &ref();

Attempting to set reference to non referenceable value

E_NOTICEのまま

出し方がわからないどころか、事例すら一切出てこない謎の警告。

Cannot pass by-reference argument %d of %s%s%s() by unpacking a Traversable, passing by-value instead

E_WARNINGのまま

参照渡し関数にTraversableな値を引数アンパックして渡すと発生する。

    function ref(&$var){}
    ref(...new ArrayIterator([1]));

いみがわからない。

Division by zero

E_WARNING → DivisionByZeroError exception

数値を0で割ると発生する。

    1 / 0;

PHP7.4までは計算結果がfloat(INF)になる。

Undefined variable

E_NOTICE → E_WARNING

未定義変数を参照すると発生する。

    echo $a;

詳細は個別記事を参照のこと。

Undefined array index

E_NOTICEのまま

配列の未定義キーを参照すると発生する。

    $a = [];
    echo $a[1];

詳細は個別記事を参照のこと。

感想

そもそもどうすれば出せるのかすらわからないエラーがあった。

警告に寛容なプログラミングをしている場合、Invalid argument supplied for foreachCreating default object from empty valueあたりはよく見かけるのではないかと思います。
これらはPHP8では例外になって完全に動かなくなるので注意しましょう。

それ以外でも、ゆるふわぺちぱーに対する締め付けは年々厳しくなる一方で、彼らの肩身はどんどん狭まりつつあります。
かつてはPHP以上にアバウトで破壊と慈悲の混沌だったJavaScript界も、最近は型に嵌まっていないゆるふわJavaScripterを完全排除する流れができあがっています。
やがて彼らの居場所が完全に失われてしまったとき、難民たちはいったいどこに行くのでしょうね。

【PHP8.0】PHPでunion型が使えるようになる

$
0
0

Union Types 2.0というRFCが投票中です。
提案者はまたまたのNikita。
2019/10/25開始、2019/11/08終了で、受理には2/3+1の賛成が必要です。
2019/11/04時点で賛成55反対5であり、ほぼ導入確定です。

PHPのunion型って何かというと、TypeScriptのunion型とだいたい同じです。
int|string $aと書いたら$aint型もしくはstring型ですよ、ということです。

ちなみに別途RFCをGitHubで管理しようという実験が進行中で、このRFCの詳細はGitHub上に存在します
このRFCはまだNikitaの個人GitHub上にしかないのですが、本決まりになったらPHP公式に移動になると思います。
まあGitHubのほうが管理とか更新とか楽ですからね。
ただGitHubはURLがすぐ404になるのだけはどうにかしてほしい。

Union Types 2.0

Introduction

union型は、単一の型ではなく、複数の異なる型の値を受け入れます。
PHPは既に、2種類の特別なunion型をサポートしています。

・null許容型。?Tで使える。
・array型もしくはTraversable型。iterableで使える。

しかし任意のunion型はサポートされていません。
現在はかわりにphpdoc注釈を書くくらいしかできません。

classNumber{/**
     * @var int|float $number
     */private$number;/**
     * @param int|float $number
     */publicfunctionsetNumber($number){$this->number=$number;}/**
     * @return int|float
     */publicfunctiongetNumber(){return$this->number;}}

Statisticsセクションで、オープンソースでunion型がどれだけ普及しているかを示しています。

言語でunion型をサポートすることにより、より多くの型情報をphpdocに頼ることなく関数シグナチャに移動することができ、多数の利点が得られます。

・型は実際に強制されるため、ミスを早期に発見できる。
・エッジケースを見逃したり、仕様変更の際にドキュメントを更新し忘れたりする可能性が減る。
・継承時にもリスコフの置換原則を適用できる。
・リフレクションから利用できる。
・phpdocより分量が減らせる。

union型は、ジェネリクスと並んで型システムに残っている最大の穴です。

Proposal

union型を構文T1|T2|...で表し、型を書くことができる全ての位置でunion型を使用可能とする。

classNumber{privateint|float$number;publicfunctionsetNumber(int|float$number):void{$this->number=$number;}publicfunctiongetNumber():int|float{return$this->number;}}

Supported Types

union型は、現在PHPでサポートされている全ての型をサポートしますが、一部の型については注意が必要です。

void型

void型は、union型の一部となることは決してできません。
T|voidのような型は、戻り値を含む全ての位置で不正です。

void型は関数に戻り値がないことを表します。
あらゆる非void型と互換がありません。

かわりにnull許容型?Tがあり、これはT型もしくはnullを返すことができます。

null許容型

T1|T2|nullとしてnullを許容するunion型を定義することができます。
既存の?Tは、T|nullの省略形と見做されます。

このRFCの草稿では、null許容型の文法が2種類になることを防ぐため、?(T1|T2)という文法を提唱していました。
しかしこの構文は都合が悪く、さらにphpdocで確立されているT1|T2|null構文とも異なっています。
議論の結果は、T1|T2|nullの文法が圧倒的に優勢でした。
?TT|nullの省略形として今後も有効な構文で、推奨も非推奨もされず、当分は廃止される予定もありません。

null型は、union型の一部としてのみ有効な型で、単独で使うことはできません。

union型と?T表記を混ぜて使用することはできません。
?T1|T2T1|?T2?(T1|T2)は全て不正な文法で、この場合はT1|T2|nullを使う必要があります。

false疑似型

現在では、エラーや不正が起きた際の関数の戻り値はnullにすることが推奨されていますが、歴史的理由から多くの内部関数はfalseを返してきます。
Statisticsセクションで示すように、union型を返す内部関数は大部分がfalseを含んでいます。

一般的な例としてはint|falseを返すstrposなどです。
これを正しく表記するとint|boolですが、これは関数がtrueを返すこともあるという誤った印象を与えます。

そのため、このRFCにはfalseのみを表すfalse疑似型が含まれています。
trueのみを表すtrue疑似型は、それが必要となる歴史的理由が存在しないため、このRFCには含まれません。

false疑似型は、union型の一部としてのみ有効で、単独やnull許容型として使うことはできません。
falsefalse|null?falseは全て無効な構文です。

Duplicate and redundant types

クラスのロードを行わずに検出できる冗長な記述は、コンパイルエラーになります。
これは以下のような例を含みます。

・同じ型の重複。int|string|INTは不可。
boolにはfalseを追加できない。
objectには個別のクラス型を追加できない。
iterableにはarrayTraversableを追加できない。

これは、型が最小であることを保証はしません。
たとえばクラスAとBがクラスエイリアスや継承関係にある場合、A|Bは有効です。

functionfoo():int|INT{}// ×functionfoo():bool|false{}// ×useAasB;functionfoo():A|B{}// × 構文解析時点でわかるclass_alias('X','Y');functionfoo():X|Y{}// 許可 実行時までわからない

Type grammar

特殊なvoid型を除くと、型の構文は以下のようになります。

type: simple_type
    | "?" simple_type
    | union_type
    ;

union_type: simple_type "|" simple_type
          | union_type "|" simple_type
          ;

simple_type: "false"          # union型でのみ有効
           | "null"           # union型でのみ有効
           | "bool"
           | "int"
           | "float"
           | "string"
           | "array"
           | "object"
           | "iterable"
           | "callable"       # プロパティ型指定では無効
           | "self"
           | "parent"
           | namespaced_name
           ;

Variance

union型は、既存の型ルールに従います。
・戻り値は共変。
・パラメータ型は反変。
・プロパティ型は不変。

唯一の変更点は、union型が派生型と相互作用する点であり、そのため3つの追加ルールがあります。

・全てのU_iV_jのサブタイプであった場合、U_1|...|U_nV_1|...|V_mのサブタイプである。
iterablearray|Traversableと同じと見做す。
・false疑似型はboolのサブタイプと見做す。

以下において、許可されているものと許可されないものの例を幾つか示します。

Property types

プロパティの型は不変です。
すなわち、継承しても型は同じである必要があります。
ただし、この"同じ"は意味が同じということを表します。
これまでもクラスエイリアスで同じクラスを表す別名を付けることができました。

union型はこの"同じ"の範囲を広げ、たとえばint|stringstring|intは同じとして扱います。

classA{}classBextendsA{}classTest{publicA|B$prop;}classTest2extendsTest{publicA$prop;}

この例では、親クラスのA|B型と子クラスのA型は明らかに異なる型であるにもかかわらず、これは正当な文法です。
内部的には、以下のようにこの結果に到達します。
まず、それはAのサブタイプであるため、AA|Bのサブタイプです。1
次にAAのサブタイプであり、BAのサブタイプであるため、A|BAのサブタイプです。

Adding and removing union types

戻り値からunion型の一部を削除し、パラメータに一部の型を追加することは正しい文法です。

classTest{publicfunctionparam1(int$param){}publicfunctionparam2(int|float$param){}publicfunctionreturn1():int|float{}publicfunctionreturn2():int{}}classTest2extendsTest{publicfunctionparam1(int|float$param){}// OK: パラメータの型追加は許可publicfunctionparam2(int$param){}// NG: パラメータの型削除は禁止publicfunctionreturn1():int{}// OK: 返り値の型削除は許可publicfunctionreturn2():int|float{}// NG: 返り値の型追加は禁止}

Variance of individual union members

同様に、戻り値の型を狭めたり、パラメータの型を広げることは許可されます。

classA{}classBextendsA{}classTest{publicfunctionparam1(B|string$param){}publicfunctionparam2(A|string$param){}publicfunctionreturn1():A|string{}publicfunctionreturn2():B|string{}}classTest2extendsTest{publicfunctionparam1(A|string$param){}// OK: BをAに広げたpublicfunctionparam2(B|string$param){}// NG: AをBに狭めたpublicfunctionreturn1():B|string{}// OK: AをBに狭めたpublicfunctionreturn2():A|string{}// NG: BをAに広げた}

もちろん同じことを複数のunion型に同時に行ったり、型の追加削除と型の拡縮を組み合わせることもできます。

Coercive typing mode

strict_typesが有効でない場合、スカラー型宣言は暗黙の型変換の対象となります。
これは一部のunion型において、変更先の型を一意に決められないため問題となります。
たとえばint|stringfalseを渡すと、0""の両方が暗黙の型変換の候補になります。

従って、引数に正しくないスカラー値が渡ってきた場合、以下の優先順位で変換することにします。

1.int
2.float
3.string
4.bool

PHPの既存の型変換機構で型変換が可能である場合、その型が選ばれます。

例外として、値が文字列であり、union型がint|floatである場合、優先される型は引数の数値文字列の中身によって決まります。
すなわち、"42"はint型となり、"42.0"はfloat型となります。

上記のリストに含まれない型には自動型変換されません。
特にnullfalseへの自動型変換は起きないことに注意しましょう。

// int|string42-->42// 正しい型"42"-->"42"// 正しい型newObjectWithToString-->"__toString()の結果"// objectはint型にならない42.0-->42// floatはint型になる42.1-->42// floatはint型になる1e100-->"1.0E+100"// floatの上限を超えたらstring型になるINF-->"INF"// floatの上限を超えたらstring型になるtrue-->1// boolはint型になる[]-->TypeError// 配列はint|string型にならない// int|float|bool"45"-->45// 整数っぽいのでint型になる"45.0"-->45.0// 小数っぽいのでfloat型になる"45X"-->45+Notice:Nonwellformednumericstring// 有効部分は整数っぽいのでint型になり、E_NOTICEが出る""-->false// 数値形式文字列でないのでbool型になる"X"-->true// 数値形式文字列でないのでbool型になる[]-->TypeError// 配列はint|float|bool型にならない

Alternatives

自動型変換については、別案が2種類ありました。

ひとつめはunion型は常に厳密な型指定とすることで、複雑な強制型変換を完全に排除することです。
これは2つの欠点があります。
まず、strictでないときに型をfloatからfloat|intにすると、直感に反して有効な入力が減ります。
第二に、float型float|int型のサブタイプと言えなくなるため、union型のモデルが崩壊します。

二つ目が、変換の優先順位を型の順番にすることです。
これは即ちint|stringstring|intが異なる型になることを意味します。
この変換は直感的ではなく、継承関係に非常に不明瞭な影響を及ぼします。

Property types and references

union型プロパティへの参照は、プロパティ型指定RFCに書かれた挙動に従います。
プロパティ型指定とunion型の組み合わせによる影響は、当時から考慮されていました

classTest{publicint|string$x;publicfloat|string$y;}$test=newTest;$r="foobar";$test->x=&$r;$test->y=&$r;// $rと$test->xと$test->yは同じもので、型は{ mixed, int|string, float|string }の論理積になる$r=42;// TypeError

複数のリファレンスが紐付けられている場合、型の強制変換が行われた後の最終的な型は全ての型と互換する必要があります。
上記例の場合、$test->xはint型の42になり、$test->yはfloat型の42.0になります。
これは同じ型ではないため、TypeErrorが投げられます。

この場合は共通の型であるstring型にキャストすることでエラーは出なくなりますが、自動型変換による優先順位と異なるため、型がどうなるかわからないという欠点があります。

Reflection

union型をサポートするReflectionUnionTypeクラスが追加されます。

classReflectionUnionTypeextendsReflectionType{/** @return ReflectionType[] */publicfunctiongetTypes();/* Inherited from ReflectionType *//** @return bool */publicfunctionallowsNull();/* Inherited from ReflectionType *//** @return string */publicfunction__toString();}

getTypes()メソッドは、ReflectionTypeクラスの配列を返します。
この型は、元の型宣言と順番が異なる可能性があり、また等価である別の型になる可能性があります。

たとえばint|string型が["string", "int"]の順で要素を返す場合があります。
またiterable|array|string型は["iterable", "string"]になるかもしれないし["Traversable", "array", "string"]になるかもしれません。
Reflection APIが保証するのは、論理的に同じものであるということです。

allowsNull()メソッドは、union型の要素にnull型が含まれるか否かを返します。

__toString()メソッドは、型宣言を有効なコード表現として返します。
元の型宣言と必ずしも同じではありません。

後方互換性のため、null許容型?TT|nullのunion型は、ReflectionUnionTypeではなくReflectionNamedTypeを返します。

// getTypes()や__toString()の結果は順番が異なることもあるfunctiontest():float|int{}$rt=(newReflectionFunction('test'))->getReturnType();var_dump(get_class($rt));// "ReflectionUnionType"var_dump($rt->allowsNull());// falsevar_dump($rt->getTypes());// [ReflectionType("int"), ReflectionType("float")]var_dump((string)$rt);// "int|float"functiontest2():float|int|null{}$rt=(newReflectionFunction('test2'))->getReturnType();var_dump(get_class($rt));// "ReflectionUnionType"var_dump($rt->allowsNull());// truevar_dump($rt->getTypes());// [ReflectionType("int"), ReflectionType("float"), ReflectionType("null")]var_dump((string)$rt);// "int|float|null"functiontest3():int|null{}$rt=(newReflectionFunction('test3'))->getReturnType();var_dump(get_class($rt));// "ReflectionNamedType"var_dump($rt->allowsNull());// truevar_dump($rt->getName());// "int"var_dump((string)$rt);// "?int"

Backwards Incompatible Changes

このRFCには、後方互換性のない変更はありません。
ただしReflectionTypeを利用しているコードは、union型に関する処理を追加する必要があります。

Future Scope

この項目は今後の展望であり、このRFCには含まれていません。

Intersection Types

交差型は論理的にunion型と似ています。
union型では少なくとも一つの型が満たされる必要がありますが、交差型では全ての型が満たされる必要があります。

たとえばTraversable|CountableTraversableCountableのうち少なくともどちらかである必要がありますが、Traversable&CountableTraversableでありなおかつCountableである必要があります。

Mixed Type

mixed型は、任意の値が受け入れ可能であることを表します。
型指定が無い場合と見た目の動きは同じですが、型指定が無いと、それが本当に自由であるのか、単に型指定を書き忘れただけなのかが区別できません。

Literal Types

このRFCで導入されたfalse疑似型は、TypeScriptでサポートされているリテラル型の特殊なケースです。
リテラル型は、列挙型のように一部特定の値のみを許可できる型です。

typeArrayFilterFlags=0|ARRAY_FILTER_USE_KEY|ARRAY_FILTER_USE_BOTH;array_filter(array$array,callable$callback,ArrayFilterFlags$flag):array;

列挙型ではなくリテラル型を使用する利点は、元の文法をそのまま維持できることです。
そのため、後方互換性を壊すことなく後付けすることができます。

Type Aliases

型が複雑になると、型宣言の再利用が必要になります。
その一般的な方法は2種類が考えられます。
ひとつめは次のようなエイリアスです。

useint|floatasnumber;functionfoo(number$x){}

このnumber型はソースコード上でのみ現れる型で、コンパイル時に元のint|floatに解決されます。

もうひとつは型宣言を定義することです。

namespaceFoo;typenumber=int|float;// \Foo\numberをどこからでも使える

Statistics

上位2000パッケージの@param@returnにおいて、野生のunion型がどれだけ使われているかを分析しました。

@paramのunion型:25k。一覧
@returnのunion型:14k。一覧

PHPの内部関数を調べたところ(調査が不完全なので最低2倍はあるはず)

・336関数がunion型を返す。
・そのうち213関数がfalse型を返す。

多くの内部関数が、戻り値の型を表すためにfalse疑似型が必要であることを示しています。

感想

元々型に厳密なTypeScriptがunion型を導入する理由はまあわかるんですよ。
しかしですね、元々フリーダム型だったPHPがわざわざ型宣言やらプロパティ型指定やらで狭めてきた上でのunion型って、なんというかこうマッチポンプ感とかそんな感じを感じざるをえない。
いやまあ違う話だってのは理屈ではわかるんですけど感覚的にね。

PHPにおいてこの機能の使い道は、DateTime|falsearray|nullといった、失敗時に例外を出さず無効値を返す関数の呼び出しにほぼ限られるでしょう。
あとは$_REQUESTを受け取るときのint|stringくらいでしょうか。
PDO|GMPなんて書いてきた人がいたら、設計をどうにかしろと突っ返します。

PHPでunion型をうまく使う方法、正直あまり思いつきません。
誰かがきっといいサンプルを考えてくれるはず。


  1. itがどれにかかってるのかわからなかった。というかここの文の意味がわからん。 

CentOS8で、PHP8のインストール。

$
0
0

こんにちは。
BitByteの伊井です。

今日は、CentOS8上で、PHP8をソースからビルドして
インストールする方法をご紹介します。

まず、CentOS8環境を、Vagrantでも、普通にクラウドでも良いので、
用意します。

そして、おもむろに以下のコマンドを実行していきます。

dnf install update
dnf install git sqlite
dnf groupinstall "Development Tools"

groupinstall〜〜で、PHPビルドに必要なライブラリを、
まとめてCentOS上にインストールしてくれます。

次に、PHP8のソースを取ってきましょう。

cd ~
git clone https://github.com/php/php-src.git
cd php-src

その後、以下のコマンドを実行します。

./buildconf

そして、configureコマンドを実行します。

./configure --prefix=/opt/php/php8 --enable-opcache --with-zlib
--enable-zip --enable-json --enable-sockets --without-pear

この際に、ライブラリが足りなかったりすると、そこで止まって、
失敗してしまいます。
手順で必要なライブラリのインストールコマンドは冒頭で記載しましたが、
sqliteなどでエラーが出る可能性があります。何とか成功させてください。

configureコマンドに成功すると、以下のような表示がされ、正常終了します。
+--------------------------------------------------------------------+
| License: |
| This software is subject to the PHP License, available in this |
| distribution in the file LICENSE. By continuing this installation |
| process, you are bound by the terms of this license agreement. |
| If you do not agree with the terms of this license, you must abort |
| the installation process at this point. |
+--------------------------------------------------------------------+

Thank you for using PHP.

次に、CentOS環境で有効なCPUのコア数を表示します。
nproc

nprocで表示された数値を、-jオプションに付けて、makeコマンドを
実行します。ここでは、コア数1を想定します。
make -j1

ビルドが完了すると、以下のような表示がされて、正常終了します。
Build complete.
Don't forget to run 'make test'.

次に、
make test

を実行します。
もしかすると、errorコード1などとなり、英語で何かしらの問題が
あるかもしれませんなどと、表示されるかもしれませんが、あくまで
開発バージョンですので、無視して完了しましょう。

最後に、インストールを行います。
make install

バージョンを確認しましょう。
/opt/php/php8/bin/php -v

PHP 8.0.0-dev (cli) (built: Apr 5 2019 11:19:45) ( NTS )
Copyright (c) The PHP Group
Zend Engine v4.0.0-dev, Copyright (c) Zend Technologies
It starts to get interesting.

PHP8がインストールされています。

インストール時には、OPcacheは、無効となっています。
/opt/php/php8/libに、iniファイルを作成して、1行追加します。

cd /opt/php/php8/lib
touch php.ini

zend_extension=opcache.so

PHP8では、JITの効果が先立って、体験できます。
ここでは、割愛しますが、プログラムをネイティブコードに変換し、
それをキャッシュして再利用するというプログラム実行が高速化される機能となっています。

JITを有効にするには、以下のようなコマンドで実行する必要があります。
/opt/php/php8/bin/php -d opcache.enable_cli=1 (オプション)PHPファイルパス

このあたりは、ネットに転がっていると思いますので、時間があればJITに関しても、色々と
研究してみてください。

それでは、長文失礼しました。

【PHP8.0】マイナススタートの配列インデックスが使えるようになる

$
0
0
PHP7.4
$arr=[];$arr[-10]=1;$arr[]=1;

どうなるかというと[-10=>1, 0=>1]です。

これはマニュアルにも明記されています。

1.png

しかしこの動作はPHP8.0で変更になります。
Arrays starting with a negative indexというRFCにおいて変更が決定しました。

Arrays starting with a negative index

Introduction

array_fillのドキュメントには、『start_index が負の場合は、 返される配列の最初のインデックスが start_index となり、それ以降のインデックスはゼロから始まります』とあります。

明示的に負のキーを使用し、その後キーを省略した場合は常にこのような動作になります。

Proposal

現在の動作は、最後の数値キーがnであった場合、次の暗黙キーはn>=0であればn+1n<0であれば0ということになります。

このRFCでは、nの符号に関係なく常にn+1とすることによって、この動作を一貫させることを目指しています。

配列のドキュメントにもわざわざ注釈として書かれているように、これは例外的な動作であり、学習コストとなります。

このプロポーザルはPHP8.0を対象としています。
この変更により問題が発生する可能性がある場合、移行を容易にするために非推奨の通知を出すことも提案します。

以下のコードは全て同じ結果になります。

$a=array_fill(-2,3,true);$b=[-2=>true,true,true];$c=["string"=>true,-2=>true,true,true];unset($c["string"]);$d[-2]=true;$d[]=true;$d[]=true;

現在は全て以下のようになります。

~PHP7.4
array(3){[-2]=>bool(true)[0]=>bool(true)[1]=>bool(true)}

このRFCにより、以下の動作に変更になります。

PHP8.0~
array(3){[-2]=>bool(true)[-1]=>bool(true)[0]=>bool(true)}

非推奨を出力する投票が受理された場合、移行フェーズ中は以下のE_DEPRECATEDを発生させます。

Deprecated: In the next major version of PHP the implicit keys of this array will start from -1 instead of 0 in ...

Backward Incompatible Changes

配列を負のインデックスから開始し、その後暗黙のキーを使い、そして明示的なキーで配列要素にアクセスした際に影響が発生します。

$a[-2]=true;// 現行:キーは-2 / RFC:キーは-2$a[]=true;// 現行:キーは0 / RFC:キーは-1$a[]=true;// 現行:キーは1 / RFC:キーは-0if($a[1]===true){echo'Accessing key 1 explicitly';}

PHP8.0以降、'Accessing key 1 explicitly'が出力されなくなります。

Proposed PHP Version(s)

PHP8.0。

Unaffected PHP Functionality

明示的なキー、文字列のキーには影響しません。
また、-1以上の数値インデックスを持つ配列も影響を受けません。
最初から暗黙のキーを使う配列のインデックスは0から始まります。

キーを使用しないforeachも影響を受けません。

投票

このRFCは賛成17反対2で可決されました。

E_DEPRECATEDを出す提案は賛成8反対14で却下されました。

すなわち、PHP8.0で前触れなく突然動作が変更になります。

感想

PHP4.2から長い時を経て、元の鞘に戻ることになりました。
さすがに当時のことは知らないので、どうして変更されたのかは全くわかりません。

しかし、この仕様に依存する実装をしている人なんていないでしょう。
すなわち実質的な影響はほぼゼロです。
そもそもネガティブスタートの配列って何に使うんだろうか?

ただ、MySQLのAUTO_INCREMENTマイナスは使えないですし、あえて変更する必要があるのかは少し疑問です。

あと配列をコピーしたら何かおきそうで少し楽しみですね。

php8 JIT vs いろんな言語

$
0
0

php8 JITと他のサーバーサイド言語を使ってパフォーマンステストをしました。

php8 JITが早くなったということで、他の主要なサーバーサイドの言語とパフォーマンステストして比較する。
どの言語が一番速いかどうかの検証

パフォーマンステスト用メソッド (フィボナッチ数列の総数計算)

$n=35;functionfibonacci(int$n):int{return(($n<2)?1:fibonacci($n-2)+fibonacci($n-1));}

php8 JIT + 比較する言語

12345
php8-dev JITgo 1.13Java 11.0.4python 3.8.1Node v13.6.0(javascript)

テスト環境

各言語の実行環境は、AlpineをベースのDockerイメージをDockerHubのものを使用しています。

実行結果

言語速度
php8-dev JIT0.444秒
go 1.130.052秒
Java 11.0.40.038秒
Python 3.8.12.449秒
Node v13.6.00.2001秒

総括

当たり前ですが、コンパイラ型言語のgoとJavaが圧倒的に早かった
同じインタプリタ言語である、php, python, nodeの中でもpythonが圧倒的に遅かったですが、
Pythonが特別遅いというより、php 8 JITが早くなったという認識のほうが正しいかも。

コード

https://github.com/okeita0805/performance_test

PHP8にstr_contains関数が追加されることになったというお話

$
0
0

strposstrstrを使わずに済むようになる・・・かもしれません( 日本語対応にやや懸念あり)。

以下、 RFCをもとに紹介します。

まだ投票中ですが、3月8日現在、賛成42/反対5という圧倒的多数の支持を得ているため、追加される公算が高いです。

既存関数の問題点

ある文字列が別の文字列に含まれているかをチェックする際、これまで strposstrstrといった関数が使われてきました。

しかし、これらの関数は、以下の問題を抱えています。

  • 読み手にとってあまり直感的でない
  • 間違いやすい
  • 新しく PHP に触れる開発者にとって覚えにくい

そのため、多くの PHP フレームワークが、同様の振る舞いをするヘルパー関数を提供しています。それだけ、この機能は、重要だし、必要とされているということです。

提案

概要

str_contains ( string $haystack , string $needle ) : bool

$needle$haystackに含まれているかをチェックし、$needleが見つかったかを真偽値( true/false)で返します。

例えば、以下のようになります。

str_contains("abc","a");// truestr_contains("abc","d");// false// $needle が空文字だった場合str_contains("abc","");// truestr_contains("","");// true

$needleが空文字だった場合については「空文字は文字列のどの位置にも出現する」と考えて trueを返すことにします。

マルチバイト版

内部メーリングリストでの議論を踏まえて、この関数にはマルチバイト版(例えば mb_str_contains)を提供する必要はないとの結論に至りました。

というのも、マルチバイト版と非マルチバイト版の動作に差がないと考えられるからです。文字列が見つかったオフセットや位置が関係する場合、マルチバイト版は異なる振る舞いをします。

しかし、この関数には、その必要はありません。

大文字小文字を区別しない版

大文字小文字を区別しない版については、今回は実装しません。

提案者は、最初は小さくはじめて、徐々に大きくしていくほうが良いと考えているからです。1

後方非互換性

PHP 自体には後方非互換性はありません。
ただし、ユーザのコードで、この関数を実装している場合、後方非互換が発生するかもしれません。

感想

strposは文字列の位置を返す変数ですが、1文字目に含まれている場合は 0を返します。そして、どこにも含まれていない場合には falseを返します。

PHP では 0 == falseです。そのため、以下のような書き方をすると、バグになってしまいます。

// 1文字目に含まれている場合は 0 が返る$pos=strpos("abc","a");// 間違った書き方if(!$pos){die("含まれていない");}// 正しい書き方if($pos===false){die("含まれていない");}

そもそも strposは「文字列の位置を返すための関数」ですし、 strstrは「部分文字列を抽出するための関数」です。
どちらも文字列が含まれているかをチェックするための関数ではない

strstrってなんだよ。str s ubs tr ing のつもりか? 覚えられん!

この使いづらさから、例えば Laravel には Str::containsという メソッドが用意されていたりしますが、とうとう、PHP 本体にも入ることになったようです。万歳!

気になるのは、「日本語文字コードでも正しく動くか?」ということです。
strposには mb_strposがあり、同様に mb_str_containsも必要なのではないでしょうか。

実際、内部メーリングリストの議論を見ると、「UTF-8 ではちゃんと動くが Shift_JIS では動かないのでは?」という 意見もあり、怪しい気がします。2

This is not true for all character encodings. For UTF-8 it is correct,
but consider for example the Japanese encoding Shift_JIS, where the
second byte of a multi-byte character can be a valid first byte of a
single-byte character. str_contains() would have incorrect behaviour for
this case.

日本語対応にやや懸念は残るものの、他の言語には普通に備わっている関数が PHP にも追加されること自体は良いことなのではないか、と思います。


  1. 内部メーリングリストの議論を見ると、 str_starts_withstr_ends_withを追加する RFCが最初から大文字小文字の区別やマルチバイト版を用意した結果、否決されてしまったショックが大きいようです。  

  2. これに対して、 Nikita Popov が 返答していますが、 "That's of course true, but I consider it ultimately unimportant." と一蹴されちゃっています(苦笑) なんとなく「細かいことはあとでいいから、とりあえず導入したいんだよ!」という雰囲気を感じます。この辺の議論は内部実装の話になっていて、あまり理解できている自信がありませんが。 

【PHP8】もう`strpos($haystack, $needle)!==false`って書かなくていいんだ

$
0
0

ある文字列中に特定の文字列が存在するかを調べる方法としてstrposが存在します。

しかし、そもそもstrposは『ある文字列中で特定の文字列が何文字目に出てくるか』を調べる関数であり、第一に使用目的が異なる関数です。
そしてこちらも有名な話ですが、先頭が一致すると0が返ってくるので、緩やかな比較ではfalseと区別されません。
PHPのよくある落とし穴のひとつです。

if(strpos('放課後アトリエといろ','アトリエ')){echo'"放課後アトリエといろ"には"アトリエ"が含まれる';// 表示される}if(strpos('放課後アトリエといろ','放課後')){echo'"放課後アトリエといろ"には"放課後"が含まれる';// 表示されない!!}if(strpos('放課後アトリエといろ','放課後')!==false){echo'"放課後アトリエといろ"には"放課後"が含まれる';// 表示される}

マニュアルにも大きく警告が出されています。
01.png

この状況がついにPHP8で変わります

PHP RFC: str_contains

Introduction

str_containsは、ある文字列に特定の文字列が含まれているかをチェックし、見つかったか否かによってtrue/falseいずれかのbooleanを返します。

ある文字列に特定の文字列が含まれているかをチェックするために、一般的にはstrposstrstrが使用されます。
str_containsは凡そあらゆるプロジェクトで非常によく使われるユースケースなので、独自の関数を追加するに値するはずです。
strposstrstrには、いくつかの欠点が存在します。

・あまり直感的ではない
・間違いをしやすい(特に!==の場合)
・新規PHP開発者が覚えにくい

このため、多くのPHPフレームワークは独自のヘルパ関数を提供しています。
このことこそがstr_containsの重要性や必要性をよく示しています。

Proposal

このRFCは、新しい関数str_containsを提唱します。

str_contains(string$haystack,string$needle):bool

引数$haystack$needleを取り、$haystackの中に$needleが存在するか否かをbooleanで返します。

str_contains("abc","a");// truestr_contains("abc","d");// false// 空文字列はtrueになるstr_contains("abc","");// truestr_contains("","");// true

空文字列の扱いについて

As of PHP 8, behavior of '' in string search functions is well defined, and we consider '' to occur at every position in the string, including one past the end. As such, both of these will (or at least should) return true. The empty string is contained in every string.

PHP8において、文字列検索関数において''の動作は明確に定義されており、終端を含む文字列全ての位置にマッチします。そのため、これらは両方ともtrueを返すべきです。空文字列はあらゆる文字列に含まれています。 - Nikita Popov

Case-insensitivity and multibyte strings

internalメーリングリストなどの議論において、この関数のマルチバイト対応版(mb_str_contains)は必要ないという結論に達しました。
理由として、この関数のマルチバイト版は非マルチバイト版と全く同じ動作になります。
文字列の見付かった位置によって動作が異なる場合は、マルチバイト版は異なる動作になります。
この関数は見付かった位置によって動作は変わらないので、マルチバイト版の必要はありません。

大文字と小文字を区別しない版については、需要が大文字と小文字を区別する版よりはるかに低いため、このRFCには含まれません。
区別しない版を取り込むと、有効なバリアントはstr_contains/mb_str_icontainsだけになります。
このように中途半端なバリアントをいきなり提供するとPHP開発者が混乱する可能性があるので、最初は小さく始めた方がよいでしょう。

Backward Incompatible Changes

PHP自体に後方互換性のない変更はありません。

ユーザランドに同じ関数が実装されている場合、非互換の可能性があります。
しかし、そのようなグローバル関数はアプリケーションの起動プロセスのかなり早い段階で追加されるので、開発者はこの問題にすぐ気付くでしょう。

Proposed PHP Version(s)

PHP 8

Implementation

str_contains関数は、このプルリクで実装されています。
https://github.com/php/php-src/pull/5179

投票

投票は2020/03/16まで、受理には2/3の賛成が必要です。
2020/03/09時点では賛成43反対5で、まず間違いなく受理されます。

感想

よく考えたらずっと前からあってもおかしくない関数なのに、これまで存在してなかったのは不思議ですね。

実際のところ、文字数としてはわずか2字の差にすぎません。

if(strpos($heystack,$needle)!==false){echo'$heystackに$needleが含まれている';}// 同じif(str_contains($heystack,$needle)){echo'$heystackに$needleが含まれている';}

しかし、読みやすさ、理解のしやすさという点では圧倒的にstr_containsに分がありますね。

以下はマルチバイト対応関数が用意されない点の補足です。
マルチバイト対応strposとしてmb_strposがありますが、文字が存在するか否かをチェックするだけであれば実はstrposでも問題ありません。

echostrpos('あい','い');// 3echomb_strpos('あい','い');// 1echostrpos('あい','あ');// 0echomb_strpos('あい','あ');// 0echostrpos('あい','う');// falseechomb_strpos('あい','う');// false

値が変わるのは『存在したときの文字数の数え方』だけです。
文字列の有無を確認するだけであれば、存在しない場合は常にfalse、存在する場合は常にint型となって差が出ないわけですね。

同様にstr_containsも、存在しない場合は常にfalse、存在する場合は常にtrueなるため、あえてmb_str_containsを用意する必要はないというわけです。


【PHP8.0】gettypeとget_classの悪魔合体

$
0
0

ワレハget_debug_type、コンゴトモヨロシク…

PHPにはプリミティブ型名を取得するgettypeと、オブジェクトのクラス名を返すget_classという関数が存在します。
_があったりなかったりと命名の不統一も気になりますが、それよりgettypeはオブジェクトに使うとobjectしか返さず、get_classをプリミティブ型に使うとE_WARNINGが発生します。
いや、プリミティブ型であればintとかの型が欲しいし、オブジェクトならPDOとかの型が欲しいんだ、という問題に対する答えはありませんでした。

というわけで両者を合体させたget_debug_typeというRFCが提出されました。

PHP RFC: get_debug_type

proposal

このRFCは、指定された変数の型を返す新しい関数、get_debug_typeを追加する提案です。

これは、配列で来る値など、変数の型に基づいた既存のチェック方法では対応できないパターンを置き換えるためのものです。

$bar=$arr['key'];if(!($barinstanceofFoo)){// もっとも単純な例。しかしgettypeは"integer"を返すので、正しい型にしたいなら"int"に変換するなどが必要。thrownewTypeError('Expected '.Foo::class.' got '.(is_object($bar)?get_class($bar):gettype($bar)));}// 今後はこう書けるif(!($barinstanceofFoo)){thrownewTypeError('Expected '.Foo::class.' got '.get_debug_type($bar));}$bar->someFooMethod();

この関数は、正しい型名を返すという点でgettypeと異なります。
"integer"ではなく"int"を返し、クラスもクラス名に変換します。
次の表は、いくつかの値に対してgettypeget_debug_typeが返す値を比較したものです。

get_debug_type()gettype()
0intinteger
0.1floatdouble
trueboolboolean
falseboolboolean
"hello"string
[]array
nullnullNULL
Foo\BarFoo\Barobject
無名クラスclass@anonymousobject
リソースresource (xxx)resource
閉じたリソースresource (closed)

Backward Incompatible Changes

なし。

Proposed PHP Version(s)

PHP8.0

Implementation

https://github.com/php/php-src/pull/5143

投票

投票は2020/03/26まで、2/3の賛成で受理されます。
このRFCは賛成42、反対3で受理されました。

感想

Mark Randallは最初はgettypeがクラス名も返すようにしようとしたものの、Nikitaから「新しい関数にしてくれ」と言われてget_debug_typeを作ったようです。
まあ、これまでobjectとしか言わなかったgettypeがいきなり色々な型を喋り出したら困るところも出そうですからね。

ということで、今後は型の取得はget_debug_typeに一本化できそうです。

手間を省くための定型処理を言語機能に取り込むことは、他の言語でも多々起きていることです。
たとえばJavaScriptのasync/awaitPromiseの糖衣にすぎず、async/awaitができることは全てPromiseでもできるので、究極的にはasync/awaitは不要です。
しかし非同期処理を楽に書けるようにするために言語仕様に取り込まれました。
糖衣構文の取り込み自体は、このようにさほど珍しいことでもありません。

しかし、str_containsとかis_countableとか、他言語であれば「自分で書け」と言われそうな極端に簡単な構文まで言語仕様に取り込んでしまう言語は、PHP以外にはそうそう無いのいではないかと思います。

【PHP8.0】throw文がthrow式になる

$
0
0

throw expressionというRFCが投票中です。

最初のアイデアは2019/12/06のSebastiaan Andewegによるツイート

それに対して2020/03/19にCarusoが反応し、そしてその日のうちにiluuu1994が最初のプルリクを出しました
はえーよ。

throw expression

Introduction

PHPのthrowは文であるため、アロー関数や三項演算子、NULL合体演算子などの式しか許されない場所から例外を投げることができません。
このRFCでは、それらを可能にするためthrow文を式に変更することを提案します。

Proposal

式を記述可能なあらゆるコンテキストでthrowが利用可能になります。
以下は思いついた便利そうな例です。

// アロー関数$callable=fn()=>thrownewException();// nullチェック$value=$nullableValue??thrownewInvalidArgumentException();// trueっぽいチェック$value=$falsableValue?:thrownewInvalidArgumentException();// 空ではない配列チェック$value=!empty($array)?reset($array):thrownewInvalidArgumentException();

他にも、議論の余地のある使用方法があります。
このRFCでは、以下のような記述も許可されています。

// ifを使った方が意図が明確になる$condition&&thrownewException();$condition||thrownewException();$conditionandthrownewException();$conditionorthrownewException();

Operator precedence

throwが式になると、優先順位を決める必要があります。
以下は現時点で有効な書式の例です。

throw$this->createNotFoundException();// こうなるthrow($this->createNotFoundException());// こうではない(throw$this)->createNotFoundException();throwstatic::createNotFoundException();// こうなるthrow(static::createNotFoundException());// こうではない(throwstatic)::createNotFoundException();throw$userIsAuthorized?newForbiddenException():newUnauthorizedException();// こうなるthrow($userIsAuthorized?newForbiddenException():newUnauthorizedException());// こうではない(throw$userIsAuthorized)?newForbiddenException():newUnauthorizedException();throw$maybeNullException??newException();// こうなるthrow($maybeNullException??newException());// こうではない(throw$maybeNullException)??newException();throw$exception=newException();// こうなるthrow($exception=newException());// こうではない(throw$exception)=newException();throw$exception??=newException();// こうなるthrow($exception??=newException());// こうではない(throw$exception)??=newException();throw$condition1&&$condition2?newException1():newException2();// こうなるthrow($condition1&&$condition2?newException1():newException2());// こうではない(throw$condition1)&&$condition2?newException1():newException2();

共通して言えるのは、全てがthrowキーワードより高い優先順位を持つということです。
このため、このRFCではthrowキーワードの優先順位を可能な限り低くすることを提案します。
現在有効なコードは、たとえ直感に反する動作だったとしても、今後も同じ動作をし続けます。
なぜなら、一般的にthrowは最後に使用するべき演算子であり、それ以降に記述した式は評価されないからです。

低い優先順位の唯一の欠点は、短絡評価のために括弧が必須になることです。

$condition||thrownewException('$condition must be truthy')&&$condition2||thrownewException('$condition2 must be truthy');// こうなる$condition||(thrownewException('$condition must be truthy')&&$condition2||(thrownewException('$condition2 must be truthy')));// こうではない$condition||(thrownewException('$condition must be truthy'))&&$condition2||(thrownewException('$condition2 must be truthy'));

もっとも、こんなコードはほぼ使われていないでしょう。

Backward Incompatible Changes

後方互換性のない変更はありません。

Other languages

C#では同じ文法が2017年に実装されました。

このような言語は他にはあまりありません。
ECMAScriptにプロポーザルがありますが、これは同じ問題を抱えているからです。

Proposed PHP Version(s)

PHP8。

投票

投票は2020/04/19まで、2/3の賛成で受理されます。
2020/04/06時点では賛成14、反対1で、受理される見込みです。

過去のML

9年前とか15年前にも同じ発想があったようですが、そのときは立ち消えになりました。
当時とはPHPのおかれた環境やRFCの出し方などがだいぶ異なることと、そしてなにより実物のプルリクがあるというのは大きいでしょう。

感想

ややこしいよね文と式。
全てが式になればいいのに。

というわけで、今後はもっと気軽にthrowすることができるようになります。
それどころかアロー関数で引数によって値を返したり例外Aを出したり例外Bを出したりすることもできちゃいますよ。
まあ正直、throwを出すようなややこしい式をアロー関数に書くんじゃないよと思ったりはするわけですが。

【PHP8.0?】PHPに名前付き引数が実装されるかもしれない

$
0
0

PHPのソースを眺めていたら、先日2020/04/07にNikitaがなんか面白そうなプルリクを出していました。
Named Parametersという2013年に提出されたまま忘れ去られたRFCがあるのですが、その機能を実装したものです。

どういう機能ってこういうのです。

functionhoge($foo,$bar){echo"foo=$foo,  bar=$bar";}hoge(bar=1,foo=2);// foo=2, bar=1

PythonCSharpなんかで実装されてるやつですね。

Nikita本人は機能が幾つも不足しているよと言っているのですが、不足の内容はOpcache対応や引数アンパックといった周辺機能で、基本的な機能は既に実装されているみたいです。

PHP RFC: Named Parameters

State of this RFC

これは名前付き引数についての準備的なRFCです。
このRFCの目的は、次のPHPバージョンで名前付き引数をサポートすべきか、またサポートするときはどのように実装すべきかを確認することです。
ここで解説している構文や動作は最終的なものではなく、詳細を詰めていく必要があります。

このRFCの実装はまだ完全なものではありません。
これは非常に複雑な機能なので、この機能が必要でなかったのに時間をかけて実装したくはありません。

Update 22-05-2014

私は他のことで忙しく、このRFCには取り組めていません。
このRFCはPHP6に間に合うように復活させるつもりです。
それまでの間、未解決の問題に対する議論の結果の一部をここにまとめておきます。

・名前付き引数のコンセンサスはまだ得られていません。
・名前付き引数のアンパックと、名前のない引数のアンパックは...構文に統合されます。アンパックのRFCは既に決定しているため、ここについて選択肢はあまりありません。
・継承時のパラメータ名チェックは強制されません。これはあまりにも大きなBC breakになるとはんだんされました。

主な未解決の実装上の問題は"Patch"セクションにリストアップされています。

What are named arguments?

名前付き引数とは、パラメータの順番ではなくパラメータ名を使って関数に引数を渡す方法です。

// 通常の引数array_fill(0,100,42);// 名前付き引数array_fill(start_index=>0,num=>100,value=>42);

名前付き引数に渡す引数の順番は自由です。
上記例では関数シグネチャと同じ順番で引数を渡していましたが、異なる順番で渡すこともできます。

array_fill(value=>42,num=>100,start_index=>0);

名前付き引数と名前のない引数を組み合わせることも可能であり、オプション引数の一部のみを名前付き引数で渡すことも可能になります。

htmlspecialchars($string,double_encode=>false);// ↓と同じhtmlspecialchars($string,ENT_COMPAT|ENT_HTML401,'UTF-8',false);

What are the benefits of named arguments?

名前付き引数の明白な利点の一つは、上のコードサンプルを見ればわかります。
変更したい引数までの間にある引数に対して、それぞれデフォルト値を指定する必要から解放されます。
名前付き引数があれば、変更したい引数だけを直接指定することができます。

これはデフォルト引数のRFCでも可能ですが、名前付き引数を使えば意図がより明白になります。
構文を比較してみてください。

htmlspecialchars($string,default,default,false);// vshtmlspecialchars($string,double_encode=>false);

ひとつめのコードを見ても、たまたまhtmlspecialcharsの引数を丸暗記していないかぎりfalseが何を意味するのかは分からないでしょう。

コードの自己文書化の利点は、オプション引数をスキップしないときにおいても明らかです。

$str->contains("foo",true);// vs$str->contains("foo",caseInsensitive=>true);

名前付き引数を使うことで、新しい方法で関数を使うことができるようにbなります。
引数を順番に並んだリストとしてだけではなく、キーと値のペアのリストとしても扱えるということです。
以下のような使い方が考えられます。

// 現在の構文$db->query('SELECT * from users where firstName = ? AND lastName = ? AND age > ?',$firstName,$lastName,$minAge);// 名前付き引数$db->query('SELECT * from users where firstName = :firstName AND lastName = :lastName AND age > :minAge',firstName=>$firstName,lastName=>$lastName,minAge=>$minAge);

Implementation

Internally

名前付き引数は、他の引数と同じくVM stackを通して渡されます。
これらの引数の違いは、位置引数は常にスタックの先頭に渡されるのに対し、名前付き引数は任意の順番でスタックに挿入することができるということです。
使用されないスタックの位置にはNULLが入り、引数カウントはNULLも数えます。

Errors

位置引数と名前付き引数を混在させることが可能ですが、名前付き引数は位置引数の後に配置しなければなりません。
そうしなければコンパイルエラーが発生します。

strpos(haystack=>"foobar","bar");// Fatal error: Cannot pass positional arguments after named arguments

可変長引数ではない関数に存在しない引数名を渡した場合、致命的エラーが発生します。

strpos(hasytack=>"foobar",needle=>"bar");// Fatal error: Unknown named argument $hasytack

同じ名前の引数を複数回渡した場合、新しい方で古い方が上書きされ、警告が発生します。

functiontest($a,$b){var_dump($a,$b);}test(1,1,a=>2);// 2, 1// Warning: Overwriting already passed parameter 1 ($a)test(a=>1,b=>1,a=>2);// 2, 1// Warning: Overwriting already passed parameter 1 ($a)

Collecting unknown named arguments

可変長引数の...$args構文を使った場合、余った名前付き引数は$argsに集められます。
名前付き引数は常に位置引数より後ろとなり、渡された順番が保持されます。

functiontest(...$args){var_dump($args);}test(1,2,3,a=>'a',b=>'b');// [1, 2, 3, "a" => "a", "b" => "b"]

使用例としては前述の$db->query()があります。

これはPythonで**kwargsと呼ばれている機能です。

Unpacking named arguments

引数アンパックのRFCは名前付き引数のアンパックにも対応します。

$params=['needle'=>'bar','haystack'=>'barfoobar','offset'=>3];strpos(...$params);// int(6)

文字列キーを持つ任意の値は、名前付きパラメータとして展開されます。
それ以外のキーは通常の位置引数として扱われます。

位置引数と名前付き引数をひとつの配列にまとめることも可能ですが、その場合でも引数の順番は守らなければなりません。
名前付き引数より後に位置引数が出てきた場合は警告がスローされ、アンパックは中止されます。

func_* and call_user_func_array

名前付き引数を使って、スタックから引数としてNULLが渡ってきた場合、func_*関数の挙動は以下のようになります。

func_num_args()はNULLを含んだ引数の個数を返す。
func_get_arg($n)はデフォルト値を返す。デフォルト値がない場合はNULL。
func_get_args()はデフォルト値を返す。デフォルト値がない場合はNULL。

3関数とも、未定義の引数名は無視します。
func_get_argsは値を返さず、func_num_argsはカウントに含めません。

call_user_func_arrayは名前付き引数をサポートしません。
文字列キーを持つ配列を渡すコードが壊れるからです。

Open questions

Syntax

現在の実装および提案では、名前付き引数について以下2種類の構文をサポートしています。

test(foo=>"oof",bar=>"rab");test("foo"=>"oof","bar"=>"rab");

ふたつめの構文は、引数名が予約語である場合のためにサポートされています。

test(array=>[1,2,3]);// syntax errortest("array"=>[1,2,3]);// works

この構文の選択は恣意的なもので、特に深く考えずに採用しました。
以下に、いくつか代替構文の提案があります(ほとんどはPhil Sturgeonによる提案です)。

// currently implemented:test(foo=>"oof",bar=>"rab");test("foo"=>"oof","bar"=>"rab");// キーワードを使えるtest($foo=>"oof",$bar=>"rab");test(:foo=>"oof",:bar=>"rab");test($foo:"oof",$bar:"rab");// キーワードを使えないtest(foo="oof",bar="rab");test(foo:"oof",bar:"rab");// 既に有効なコードなので不可test($foo="oof",$bar="rab");

どの構文で決定するかは議論次第です。

Collection of unknown named args into ...$opts

現在のRFCでは、位置引数と名前付き引数の両方がまとめて可変長引数の...$optsに入ってきます。
Pythonでは前者を*argsに、後者を**kwargsに入れるというアプローチをとっています。

Pros:PHPでは、Pythonではできない配列と辞書の混在ができます。
Cons:位置引数と名前付き引数を別にすることで、意図がより明確になります。必ずしも両方の引数をサポートしたいとはかぎらず、片方だけを強制したいかもしれません。

どのように扱うのが適切か、意見や議論を歓迎します。

Unpacking named args

引数アンパックについても同じ疑問が出てきます。
...$fooは位置引数と名前付き引数を一緒にできますが、*$foo**$fooに分けるべきでしょうか。

この決定は、可変長引数と同じに揃えるべきでしょう。

Signature validation allows changing parameter names

現在のところ、引数名はシグネチャに含まれていません。
位置引数しか使わない場合、これは合理的です。
引数名は関数の呼び出しに関係ないからです。

名前付き引数はこの動作を変更します。
継承先クラスが引数名を変更した場合、名前付き引数を使った呼び出しは失敗し、LSPに違反します。

interfaceA{publicfunctiontest($foo,$bar);}classBimplementsA{publicfunctiontest($a,$b){}}$obj=newB;// Pass params according to A::test() contract$obj->test(foo=>"foo",bar=>"bar");// ERROR!

名前付き引数が導入された場合、シグネチャの検証において引数名の変更にエラーを出さなければなりません。
通常の場合、インターフェイスと実装クラスの不一致は致命的エラーを発生させますが、名前付き引数において致命的エラーを出すとBC breakが大きくなりすぎてしまいます。
かわりに、より低いエラータイプ(WARNING / NOTICE / STRICT)を出すことを検討します。

これに関する具体的な議論のポイントをひとつ挙げておきます。
PHPは、実行時の動作を変更するini設定を導入する習慣を過去に置いてきました。
従って、この挙動をiniで制御できるようにすることは、私の選択肢にはありません。

Patch

差分がこちらにありますが、このパッチは不完全で、ダーティで、既知のバグがあります。

やるべき作業はまだまだ残っています。

・"Open questions"の結果を実装する。
・内部関数の全てのarginfosをドキュメントと一致するように更新する。現在のarginfo構造体は絶望的に時代遅れで、引数割り当てなどは自動的にできるようにしたい。
・内部関数において引数がスキップされたときに適切に動作するようにする。ほとんどの場合は自動的に動作するはずだが、手動調整が必要になる関数もかなりあるだろう。

感想

2014年とかPHP6とか出てくることからわかるように、このRFCはだいぶ昔に書かれてそのまんまです。
当時作成されたパッチはもはや使い物にならないため、新たにプルリクを作ってきたようです。
パッチにしろその他の内容にしろ、当時のPHPと今のPHPはだいぶ異なったものになっているので、なんにしろRFCのリファインは必要になるでしょう。

このプルリクについても、とりあえず提出されただけで何の展開もありませんし、メーリングリストでの議論も特に進んでいるわけではありません。
従って、このプルリクも今後どうなるかはわからず、再びこのまま忘れ去られるかもしれません。
しかしNikitaのことですから、いきなり完動品のプルリクが送られてきて第一線に躍り出る、なんてことがあっても驚きはないですね。

混沌の時代に実装された関数などでは特に、同じ内容の関数でも引数の順番が異なったりしていて大変なのですが、この機能が実装されたら、そのあたりを楽に処理できるようになります。
また却下されたデフォルト引数のRFCも、この名前付き引数があれば不要になります。
絶対にないといけないというほどでもないですが、存在すれば純粋に便利になる、そんな良い機能だと思います。

あとここからは完全に妄想ですが、このプルリクはジェネリクスへの足掛かりなのではないかと感じています。
Nikitaはどうもジェネリクス大好きっ子みたいですから、このプルリクを元に引数まわりに手を付けて、ついでにジェネリクスまでできるようにちゃったぜみたいなことを考えているのではないでしょうか。

【PHP8.0】PHPでアトリビュート/アノテーション/デコレータが書けるようになる

$
0
0

Attributes v2というRFCが投票中です。
投票期間は2020/05/04まで、投票者の2/3の賛成で受理されます。
2020/04/27時点では賛成48反対1で、ほぼ間違いなく可決されます。

Attributes v2

Introduction

このRFCは、クラス/プロパティ/関数/メソッド/引数/定数の宣言に、構造化されたアトリビュートをメタデータとして記述できるようにする提案です。
アトリビュートは、コードの宣言に直接設定ディレクティブを埋め込むことで定義されます。

同じような概念としてJavaのAnnotation、C#/C++/Rust/HackにおけるAttribute、Python/JavascriptにおけるDecoratorが存在します。

これまで、PHPではこのようなメタデータとしては非構造的であるdoc-commentsしか存在しませんでした。
しかしdoc-commentsはただの文字列であり、言語によって解釈されることはありません。
構造化された情報を保持するために、PHPの様々なコミュニティにおいて@ベースの疑似メタデータが考案されてきました。

ユーザランドでの使用例に加え、拡張機能などではコンパイルや構文解析、コード生成、実行時の挙動に影響を与えるようなアトリビュートの使用例も多く存在します。
下のほうに例を示します。

ユーザランドのdoc-commentsが多く使われていることから、この機能がコミュニティから強く求められていることがわかります。

Proposal

Attribute Syntax

アトリビュートは、既存の構文トークンT_SLT_SR、すなわち<<>>を使った特別なフォーマットのテキストです。

アトリビュートは言語内の多くの対象に適用されます。
・関数 (クロージャやアロー関数含む)
・クラス (無名クラス含む)、インターフェイス、トレイト
・クラス定数
・プロパティ
・メソッド
・関数/メソッド引数

以下は例です。

<<ExampleAttribute>>classFoo{<<ExampleAttribute>>publicconstFOO='foo';<<ExampleAttribute>>public$x;<<ExampleAttribute>>publicfunctionfoo(<<ExampleAttribute>>$bar){}}$object=new<<ExampleAttribute>>class(){};<<ExampleAttribute>>functionf1(){}$f2=<<ExampleAttribute>>function(){};$f3=<<ExampleAttribute>>fn()=>1;

アトリビュートはdoc-blockコメントと同様に、それが属する宣言の直前に記載します。
doc-blockコメントとどちらが先かといえば、コメントの前にも後ろにも書くことができます。

<<ExampleAttribute>>/** docblock */<<AnotherExampleAttribute>>functionfoo(){}

関数、クラス、メソッド、プロパティ、パラメータはひとつ以上のアトリビュートを持つことができます。

<<WithoutArgument>><<SingleArgument(0)>><<FewArguments('Hello','World')>>functionfoo(){}

ひとつの対象に同じアトリビュート名を複数回指定することが可能です。

アトリビュートは1行で複数回宣言できます。

<<WithoutArgument>><<SingleArgument(0)>><<FewArguments('Hello','World')>>functionfoo(){}

<<>>は式の接頭辞として使用されており、他言語で使用されているジェネリクスの一般的な構文<T>がもし今後導入されることになったとしても、それがアトリビュート構文と衝突することはありません。

構文についてはこのRFCで最も議論されてきたポイントであり、@:として表される新たな構文トークンT_ATTRIBUTEを導入する代替案も考えられました。
構文の選択はRFCの二次投票となります。

@:WithoutArgument@:SingleArgument(0)@:FewArguments('Hello','World')functionfoo(){}

この構文は、doc-blockコメントで一般的に見られる@を使用します。
欠点としては、空白を許可するとアトリビュートの終端を判別できなくなってしまうので、空白を禁止しなければならないことです。

最も要望されるであろう@[]が使えない理由については、下にあるAlternative Syntaxの議論を参照してください。

Attribute Names Resolve to Classes

アトリビュートは、コンパイル中にその時点でインポートされている全てのシンボルについて解決されます。
これはアトリビュートに名前空間を許容するためであり、別のライブラリやアプリケーションで同じアトリビュートが誤って再利用されるのを避けるためです。

useMy\Attributes\SingleArgument;useMy\Attributes\Another;<<SingleArgument("Hello")>><<Another\SingleArgument("World")>><<\My\Attributes\FewArguments("foo","bar")>>functionfoo(){}

このことはクラスにアトリビュートを宣言する際にも利点があります。
・リフレクションが解析しやすい。後述。
・静的解析ツールによる検証が容易になる。
・IDEがオートコンプリートや引数に対応することができる。

クラスアトリビュートを指定する例は以下のようになります。

namespaceMy\Attributes;usePhpAttribute;<<PhpAttribute>>classSingleArgument{public$value;publicfunction__construct(string$value){$this->value=$value;}}

Compiler and Userland Attributes

このRFCは、2種類のアトリビュートを区別しています。
・コンパイラアトリビュート:コンパイル時に検証される
・ユーザランドアトリビュート:リフレクションで検証される

コンパイラアトリビュートは、PhpCompilerAttributeに属する内部クラスです。

ユーザランドアトリビュートは、PhpAttributeに属するユーザランドのクラスです。

コンパイル時にコンパイラアトリビュートが見つかると、実行エンジンはアトリビュートに紐付けられているバリデーションのコールバックを呼び出します。
たとえばこのパッチにはPhpCompilerAttributeのバリデーションコールバックが含まれており、ユーザがPhpCompilerAttributeを使うのを防いでいます。

#include "zend_attributes.h"
voidzend_attribute_validate_phpcompilerattribute(zval*attribute,inttarget){if(target!=ZEND_ATTRIBUTE_TARGET_CLASS){zend_error(E_COMPILE_ERROR,"The PhpCompilerAttribute can only be used on class declarations and only on internal classes");}else{zend_error(E_COMPILE_ERROR,"The PhpCompilerAttribute can only be used by internal classes, use PhpAttribute instead");}}INIT_CLASS_ENTRY(ce,"PhpCompilerAttribute",NULL);zend_ce_php_compiler_attribute=zend_register_internal_class(&ce);zend_compiler_attribute_register(zend_ce_php_compiler_attribute,zend_attribute_validate_phpcompilerattribute);

引数zvalは渡された全ての引数を含み、targetはアトリビュートが正しく宣言されているかを検証する為の定数です。
ユーザランドクラスはPhpCompilerAttributeを使用することができません。
使おうとするとエラーが発生します。

<?php<<PhpCompilerAttribute>>classMyAttribute{}// Fatal error: The PhpCompilerAttribute can only be used by internal classes, use PhpAttribute instead

アトリビュートをクラスツールにマッピングすることで、エディタやIDEは、アトリビュートの構文やコンテキスト情報を開発者に提供することができます。
この方法の欠点は、コンパイラアトリビュートがユーザランドアトリビュートであると誤って分類されてしまうことです。

Constant Expressions in Attribute Arguments

アトリビュートは定数AST式として評価されますが、これは引数を許可することを意味します。

<<SingleArgument(1+1)>><<FewArguments(PDO::class,PHP_VERSION_ID)>>

この主な使用例は、定数/クラス定数を参照することです。
定数を参照することで、既に定数として存在する情報を再定義する重複を避けることができます。
もうひとつの利点として、ツールやIDEによる静的解析でアトリビュートを検証できるということです。

定数ASTは、リフレクションを通してアクセスする際は値に解決されます。
これはかつて提出されたアトリビュートRFCとは意図的に異なる挙動です。

またパーサは、ビットシフト演算子とアトリビュート構文を区別できます。

<<BitShiftExample(4>>1,4<<1)>>functionfoo(){}

Reflection

ReflectionクラスにgetAttributes()メソッドが追加され、ReflectionAttributeインスタンスの配列を返します。

functionReflectionFunction::getAttributes(string$name=null,int$flags=0):ReflectionAttribute[];functionReflectionClass::getAttributes(string$name=null,int$flags=0):ReflectionAttribute[];functionReflectionProperty::getAttributes(string$name=null,int$flags=0):ReflectionAttribute[];functionReflectionClassConstant::getAttributes(string$name=null,int$flags=0):ReflectionAttribute[];

引数$nameがあれば指定したアトリビュート、もしくはそのサブクラスを含めたものを返します。

$attributes=$reflectionFunction->getAttributes(\My\Attributes\SingleArgument::class);

引数$flagが未指定の場合、getAttributes()メソッドは名前が完全一致したアトリビュートだけを返し、この動作がデフォルトです。
ReflectionAttribute::IS_INSTANCEOFを指定すると、instanceofを通過する全てのアトリビュートを返すようになります。

$attributes=$reflectionFunction->getAttributes(\My\Attributes\MyAbstractAttribute::class,\ReflectionAttribute::IS_INSTANCEOF);

ReflectionAttributeクラスは以下のようになります。

classReflectionAttribute{publicfunctiongetName():stringpublicfunctiongetArguments():arraypublicfunctionnewInstance():object}

アトリビュートの検証はReflectionAttribute::newInstance()でのみ行われるので、実は必ずしもアトリビュート名に対応したクラスを定義する必要はありません。
アトリビュート名と引数は直接ReflectionAttributeから取って来れます。

以下は完全な例です。

namespaceMy\Attributes{<<PhpAttribute>>classSingleArgument{public$argumentValue;publicfunction__construct($argumentValue){$this->argumentValue=$argumentValue;}}}namespace{<<SingleArgument("Hello World")>>classFoo{}$reflectionClass=new\ReflectionClass(Foo::class);$attributes=$reflectionClass->getAttributes();var_dump($attributes[0]->getName());var_dump($attributes[0]->getArguments());var_dump($attributes[0]->newInstance());}/**
string(28) "My\Attributes\SingleArgument"
array(1) {
  [0]=>
  string(11) "Hello World"
}
object(My\Attributes\SingleArgument)#1 (1) {
  ["argumentValue"]=>
  string(11) "Hello World"
}
**/

この使い方では、getAttributes()は決して例外をスローしません。
これにより、異なるライブラリが同じ名前のアトリビュートを定義していた際の問題を回避することができます。

Use Cases

Use Cases for PHP Extensions

アトリビュートの主な使用先は、PHPコアと拡張モジュールになるでしょう。

HashTablesへのアトリビュートは、全てのzend_class_entry/op_array/zend_property_info/zend_class_constantで使用可能です。

PHPコアや拡張モジュールは、ある定義にアトリビュートがあるかどうかチェックしたくなることがあるでしょう。
例としてOpcache JITに対する@jitのチェックなどです。
これは、関数やメソッドを常に最適化するようJITに指示します。

アトリビュートが実装されれば、拡張モジュールでは以下のように書けるようになります。

staticintzend_needs_manual_jit(constzend_op_array*op_array)returnop_array->attributes&&zend_hash_str_exists(op_array->attributes,"opcache\\jit",sizeof("opcache\\jit")-1));

開発者は、doc-commentのかわりにアトリビュートを使うことができます。

useOpcache\Jit;<<Jit>>functionfoo(){}

Other potential core and extensions use cases/ideas

以下はアトリビュートの使用法のアイデアです。
RFCの一部ではないことに注意してください。

関数/メソッドの非推奨。
アトリビュートを持つほぼ全ての言語にこの機能が組み込まれています。
PHPにこれがあれば、クラスやプロパティ、定数を非推奨にすることができます。

// アイデアだよ。RFCの一部ではないよusePhp\Attributes\Deprecated;<<Deprecated("Use bar() instead")>>functionfoo(){}

非推奨アトリビュートは、今のところtrigger_errorを使うことができません。

classFoo{<<Deprecated()>>constBAR='BAR';}echoFoo::BAR;// PHP Deprecated:  Constant Foo::BAR is deprecated in test.php on line 7

Reclassify Engine WarningsSupport Rewinding GeneratorsのRFCのようなレガシー動作を、オプトインで変更します。
Rustが似たような機能を持っています。

// アイデアだよ。RFCの一部ではないよusePhp\Attributes\Deny;usePhp\Attributes\Allow;<<Allow("rewind_generator")>>functionbar(){yield1;}<<Deny("undeclared_variables")>>functionfoo(){echo$foo;// PHP Fatal error:  Uncaught TypeError: Access to undeclared variable $foo}<<Deny("dynamic_properties")>>classFoo{}$foo->bar;// PHP Fatal error:  Uncaught Error: Invalid access to dynamic property Foo::$bar

Rustっぽいマクロの一部は、旧バージョンのPHPでのみPolyfillを読み込んだりするときに便利かもしれません。
ライブラリがOpcacheやpreloadingなどを条件付きで宣言するときに役立つでしょう。

// アイデアだよ。RFCの一部ではないよusePhp\Attributes\ConditionalDeclare;usePhp\Attributes\IgnoreRedeclaration;<<ConditionalDeclare(PHP_VERSION_ID<70000)>>// PHP7.0以上ならASTによって削除される<<IgnoreRedeclaration>>// 重複時はエラーを出さず単に無視するfunctionintdiv(int$numerator,int$divisor){}

最終的には、あるアトリビュートの引数を返すAPIや、全アトリビュートの一覧を返すAPIが含まれる予定です。
これによって拡張機能の作者は、最小限の労力でアトリビュートを使うことができるようになります。
以下は草案です。

/* アトリビュート名から引数一覧を返す */HashTable*zend_attribute_get(HashTable*attributes,char*name,size_tname_len);/* アトリビュートを返す */zval*zend_attribute_all(HashTable*attributes,char*name,size_tname_len);

Userland Use-Case: Declaring Event Listener Hooks on Objects

ユーザランドにおいて、アトリビュートは宣言に対する追加設定を宣言のすぐ傍に置くことができるという利点があります。
以下はSymfonyのEventSubscribersをアトリビュートを使ってリファクタリングする例です。
EventSubscriberInterfaceは、イベントをどのクラスのどのメソッドで処理するかをgetSubscribedEvents()で宣言する必要があります。

// 現在のコードclassRequestSubscriberimplementsEventSubscriberInterface{publicstaticfunctiongetSubscribedEvents():array{return[RequestEvent::class=>'onKernelRequest'];}publicfunctiononKernelRequest(RequestEvent$event){}}// リファクタした<<PhpAttribute>>classListener{public$event;publicfunction__construct(string$event){$this->event=$event;}}classRequestSubscriber{<<Listener(RequestEvent::class)>>publicfunctiononKernelRequest(RequestEvent$event){}}// アトリビュートを使ったイベントディスパッチャclassEventDispatcher{private$listeners=[];publicfunctionaddSubscriber(object$subscriber){$reflection=newReflectionObject($subscriber);foreach($reflection->getMethods()as$method){// Listenerアトリビュートを取得$attributes=$method->getAttributes(Listener::class);foreach($attributesas$listenerAttribute){/** @var $listener Listener */$listener=$listenerAttribute->newInstance();// $listener->eventはcallable$this->listeners[$listener->event][]=[$subscriber,$method->getName()];}}}publicfunctiondispatch($event,$args...){foreach($this->listeners[$event]as$listener){// 呼び出し$listener(...$args);}}}$dispatcher=newEventDispatcher();$dispatcher->addSubscriber(newRequestSubscriber());$dispatcher->dispatch(RequestEvent::class,$payload);

Userland Use-Case: Migrating Doctrine Annotations from Docblocks to Attributes

アトリビュートのRFCが考慮した主要なケースのひとつが、広く普及しているDoctrine Annotationsライブラリからの移行可能性です。

PHPコアがアトリビュートをサポートすることで、ユーザがDoctrine Annotationsからアトリビュートへ移行するための基盤を確保することができます。

このためこのRFCは、名前空間を使ったアトリビュートの操作が主な要件となっています。

Doctrineおよび任意のユーザランドライブラリは、親クラスの名前フィルタを利用して、興味のあるアトリビュートだけを抽出することができます。
提案のリフレクションAPIを使用し、独自のロジックを追加することで、より厳格なアトリビュートの使用を強制することができるようになります。

以下に、Doctrineのアノテーションと、このRFCのアトリビュートで同じことを実装した複雑なオブジェクトの例を示します。

<?phpuseDoctrine\ORM\AttributesasORM;useSymfony\Component\Validator\ConstraintsasAssert;<<ORM\Entity>>/** @ORM\Entity */classUser{/** @ORM\Id @ORM\Column(type="integer"*) @ORM\GeneratedValue */<<ORM\Id>><<ORM\Column("integer")>><<ORM\GeneratedValue>>private$id;/**
     * @ORM\Column(type="string", unique=true)
     * @Assert\Email(message="The email '{{ value }}' is not a valid email.")
     */<<ORM\Column("string",ORM\Column::UNIQUE)>><<Assert\Email(array("message"=>"The email '{{ value }}' is not a valid email."))>>private$email;/**
     * @ORM\Column(type="integer")
     * @Assert\Range(
     *      min = 120,
     *      max = 180,
     *      minMessage = "You must be at least {{ limit }}cm tall to enter",
     *      maxMessage = "You cannot be taller than {{ limit }}cm to enter"
     * )
     */<<Assert\Range(["min"=>120,"max"=>180,"minMessage"=>"You must be at least {{ limit }}cm tall to enter"])>><<ORM\Column(ORM\Column::T_INTEGER)>>protected$height;/**
     * @ORM\ManyToMany(targetEntity="Phonenumber")
     * @ORM\JoinTable(name="users_phonenumbers",
     *      joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="phonenumber_id", referencedColumnName="id", unique=true)}
     *      )
     */<<ORM\ManyToMany(Phonenumber::class)>><<ORM\JoinTable("users_phonenumbers")>><<ORM\JoinColumn("user_id","id")>><<ORM\InverseJoinColumn("phonenumber_id","id",JoinColumn::UNIQUE)>>private$phonenumbers;}

アトリビュートは名前付きパラメータをサポートしていないため、少し制限があります。
これがアトリビュートが関数呼び出しのような構文を利用している理由ですが、もし名前付きパラメータがサポートされれば、自動的にこのRFCもその恩恵を受けることになります。

ユーザランドのアトリビュートへの移行には、Rectorのようなツールが役立ちます。

Criticism and Alternative Approaches

Alternative Syntax: Why not use @ or [] like other languages?

どうして@[]ではないの?

構文として<<>>を使用する理由は、コードのこの場所でまだ使用できる数少ない構文のひとつであり、その中でも自然に見えるものだからです。
接頭辞演算子としてまだ使われていない他の記号を使うことも可能ですが、実質的に使えそうなものは'%'くらいです。
他に使える記号というのは|=/などです。

@[]は、エラー抑制演算子と配列短縮構文に競合するため使用することができません。
以下のような構文は、現在既に有効なPHPコードです。

[[@SingleArgument("Hello")]]

この構文が配列宣言かアトリビュートかを決定するためには、無制限に先のトークンを調べなければならなくなる可能性があります。

Why not extending Doc Comments?

Doc Commentsをそのまま拡張するのではだめなの?

アトリビュートはDoc Commentsより優れています。

・名前空間により、同じDoc Commentsを使用している複数のライブラリによる競合を防ぎます。
・アトリビュートの存在チェックはo(1)のハッシュキーチェックであり、strstrやDoc Commentsのパースより高性能です。
・アトリビュートをクラスにマッピングすることで、アトリビュートが正しい構文であることを保証し、Doc Commentsの書き間違いによるバグの発生を減らします。
・アノテーションは非常に多くのツールやコミュニティで一般に使われているので大きな需要があります。しかし初心者がコメントと見誤ると混乱を招くことになるでしょう。また/*/**の違いもバグの原因になります。

PHPで既存のDoc Commentsを解析して構造化することは可能かもしれませんが、Doc Commentsの方言ごとに追加のパーサを呼び出す必要があります。
Doc Commentsは正しい文法になっていない可能性があるため、文法エラーの取り扱いを決めなければなりません。
最終的に、これはPHP内部に別の言語が存在するということになりそうです。
この方法は、アトリビュートを導入するより遙かに複雑となり、望ましくありません。

Why not always map attributes to simple arrays instead for simplicity?

シンプルに、アトリビュートは常に配列にマッピングするようにすればよくない?

アトリビュートのクラス名解決が何故重要なのか、その利点を前のセクションで解説しました。
アトリビュートが正しいかどうかの検証は、文法が正しいかどうかを検証できないDoc Commentsよりも大きなメリットがあります。

Why not a stricter solution like Doctrine Annotations?

Doctrine Annotationsのようにより厳密なソリューションは導入しないの?

このRFCは、PHPの基本的なアトリビュート機能のみを提案しています。
一般的なソリューションを全て解決するためには様々なユースケースを考慮しなければなりませんが、大抵の場合はDoctrineほど細かなシステムは必要ありません。

Why are nested attributes not allowed?

アトリビュートのネストが許可されないのは何故?

アトリビュートのネストとは、あるアトリビュートを他のアトリビュートの引数として定義するということを意味します。
これは引数でアトリビュートを宣言できるということなので、意図的に禁止されています。

Naming (attributes or annotations)

この機能の"アトリビュート"という名前は、既に使われているアノテーションとの混同を避けるために付けられました。
これによってDoctrine AnnotationsはPHP7ではDoc Commentsで実装され、PHP8ではアトリビュートで実装されている、といったことがおこります。

Backward Incompatible Changes

なし。

Proposed PHP Version(s)

PHP8.0

RFC Impact

To Core

トークン、ASTノード、zend_class_entry、zend_class_constant、zend_op_array、zend_property_infoの全てにアトリビュートを追加する必要があります。

To SAPIs

なし。

To Existing Extensions

なし。

JITは@JITではなくOpcache\Jitを、@nojitのかわりにOpcache\Nojitを使うことになる予定ですが、まだ未定です。

To Opcache

パッチに含まれており、100%の互換ではない可能性があります。

New Constants

なし。

php.ini Defaults

なし。

Open Issues

なし。

Future Scope

・名前付きパラメータとの統合
・下位互換性を壊すことなく、既存関数を新しい動作で拡張させる。
・関数/メソッドが呼ばれたとき、プロパティ/定数にアクセスしたときに非推奨を通知する<<Deprecated>>
・型付きプロパティとアトリビュートで、JSON/XMLからオブジェクトへの直列化がPHPコアでできるようになる。
<<«SingleArgument("foo"), MultiArgument("bar", "baz")>>を簡単に書けるような短縮構文。

Voting

2020/04/27時点で、導入には賛成48反対1で、ほぼ導入決定です。

構文は<<>>が40人、@:が10人で、<<>>に決まると思われます。

Patches and Tests

https://github.com/beberlei/php-src/pull/2<<>>
https://github.com/kooldev/php-src/pull/2@:

References

他言語でのアトリビュート/アノテーション/デコレータ。

Rust Attributes
C# Attributes
Java Annotation
TypeScript/ECMAScript Decorators
C++ Attributes
Go Tags
Attributes in Hack

かつて却下されたり放棄されたRFC。

Attributes v1
Annotations v2
Reflection Annotations using the Doc-Comment
Simple Annotations
Annotations in DocBlock RFC
Class Metadata RFC

感想

ほぼ全員賛成というのがちょっと信じがたいんだけど。
個人的にはどちらかというと賛成ですが、正直もっと意見が割れてもよさそうな提案ですよね。

ということで、PHP8からはアトリビュートが使えるようになります。
例を見るに、AttributeInterfeceをimplementsしたりとかも不要で、普通にクラスを書いたらいきなりアトリビュート名として使えるんですかねこれ。
ちょっとユーザレベルでの使い方がいまいちよくわかりませんでした。

というか正直、全体的に意味のよくわからないところが多々ありました。
私は普段ソースまで追ってないので、いきなりAST構文木とかzend_class_entryとか言われても知らんがな!ってかんじですよ。
きっと誰かが補足してくれるはず。

あと、それでは具体的にデフォルトでどんなアトリビュートが用意されてるの、ってのもRFCには書かれていません。
複雑なアトリビュートを定義するにはどうすればいいの、というのもRFCではちょっとよくわかりません。
このあたりは今後ドキュメントの追加をおねがいしたいところですね。
まあPHPのマニュアルは親切すぎて困るくらい丁寧なので、そのうち充実してくるとは思いますが。

【PHP8.0】StartsWith/EndsWithがPHP本体に実装される

$
0
0

先日PHP8でstr_containsが導入されることが決まったばかりですが、さらにもっと直接的な『〇〇で始まる』『〇〇で終わる』関数までも導入されることになりました。

Add str_starts_with() and str_ends_with() functionsというRFCが投票中です。
2020/05/04時点では賛成50反対4で、ほぼ導入確定です。

PHP RFC: Add str_starts_with() and str_ends_with() functions

Introduction

str_starts_withは、文字列が指定の文字列で始まるか否かをチェックし、結果をbool値で返します。
str_ends_withは、文字列が指定の文字列で終わるか否かをチェックし、結果をbool値で返します。

これらの機能は既存の文字列関数、たとえばsubstrstrpos/strrpos、そしてstrncmpsubstr_compare、あまつさえstrlenなどを駆使して実装されてきました。
これらユーザランドの実装には、様々な問題点があります。

str_starts_withとstr_ends_withの需要は高く、SymfonyLaravelYiiFuelPHP、そしてPhalconと、あらゆるフレームワークによってサポートされています。

文字列の始めと終わりをチェックすることは非常に一般的なタスクであり、簡単に行えるべきです。
多くのフレームワークがこのタスクを実装しているということは、このタスクを実行することが簡単ではないことを意味しています。
JavaScript/Java/Haskell/Matlabといった多くの高水準言語が標準でこの機能を実装している理由でもあります。
文字列の開始と終了をチェックすることは、これだけのためにフレームワークを導入したり、ユーザランドで最適ではない(どころかバグが入るかもしれない)実装を行ったりする必要のあるべき作業ではありません。

Downsides of Common Userland Approaches

この機能のアドホックな実装は、専用関数に比べると直感的ではありません。
PHPのニュービーや、他言語と同時開発する開発者にとっては特にそうです。
また、特に===を含む場合、実装を簡単に間違えます。
さらに多くのユーザランド実装はパフォーマンス上の問題があります。

注意:以下の実装には、E_WARNINGを防ぐために$needle === "" ||strlen($needle) <= strlen($haystack) &&のようなガードを入れましょう。

str_starts_with

substr($haystack,0,strlen($needle))===$needle

$haystackの無駄なコピーが発生するため、メモリ効率が良くありません。

strpos($haystack,$needle)===0

$needleが見つからなかった場合に$haystackを最後まで調べてしまうため、CPU効率が悪くなります。

strncmp($haystack,$needle,strlen($needle))===0// genericstrncmp($subject,"prefix",6)===0// ad hoc

これは効率的ですが、$needleの文字数を別に渡す必要があり冗長です。

str_ends_with

substr($haystack,-strlen($needle))===$needle

str_starts_with同様、メモリ効率がよくありません。

strpos(strrev($haystack),strrev($needle))===0

str_starts_with同様CPU効率が悪いだけでなく、文字列反転処理まで入るので、さらに非効率です。

strrpos($haystack,$needle)===strlen($haystack)-strlen($needle)

冗長であり、CPURL効率が悪くなることがあります。

substr_compare($haystack,$needle,-strlen($needle))===0// genericsubstr_compare($subject,"suffix",-6)===0// ad hoc

効率的ですが、冗長です。

Proposal

2つの関数、str_starts_with()str_ends_with()を導入します。

str_starts_with(string$haystack,string$needle):boolstr_ends_with(string$haystack,string$needle):bool

str_starts_with()は、$haystack$needleで始まるかどうかを調べます。
strlen($needle) > strlen($haystack)であれば即座にfalseを返し、そうでなければ両文字列を比較し、先頭一致すればtrueを、一致しなければfalseを返します。

str_ends_with()も同じですが、後方一致です。

以下に例を示します。

$str="beginningMiddleEnd";if(str_starts_with($str,"beg"))echo"printed\n";// trueif(str_starts_with($str,"Beg"))echo"not printed\n";// falseif(str_ends_with($str,"End"))echo"printed\n";// trueif(str_ends_with($str,"end"))echo"not printed\n";// false// 空文字if(str_starts_with("a",""))echo"printed\n";// trueif(str_starts_with("",""))echo"printed\n";// trueif(str_starts_with("","a"))echo"not printed\n";// falseif(str_ends_with("a",""))echo"printed\n";// trueif(str_ends_with("",""))echo"printed\n";// trueif(str_ends_with("","a"))echo"not printed\n";// false

空文字に関しては、受理済のstr_containsのRFCの挙動に従います。
これはJavaやPythonなどと共通の動作です。

Backward Incompatible Changes

ユーザランドに同名の関数がある場合は競合します。

Proposed PHP Version(s)

PHP8

RFC Impact

・SAPI:全てのPHP環境に関数が追加されます
・エクステンション:無し
:Opcache:無し
・New Constants:無し
・php.ini Defaults:無し

Votes

投票は2020/05/04まで。
投票者の2/3+1の賛成で受理されます。

Patches and Tests

https://github.com/php/php-src/pull/5300

References

他言語の類似機能
・JavaScript: String#startsWith() / String#endsWith()
・Python: str#startswith() / str#endswith()
・Java: String#startsWith() / String#endsWith()
・Ruby: String#start_with?() / String#end_with?()
・Go: strings.HasPrefix() / strings.HasSuffix()
・Haskell: Data.String.Utils.startswith / Data.String.Utils.endswith
・MATLAB: startsWith()) / endsWith()

bugs.php.net
bug #50434 / bug #60630 / bug #67035 / bug #74449

過去のRFC
PHP RFC: rfc:add_str_begin_and_end_functions

Rejected Features

大文字小文字を区別しない版とマルチバイト版は、以前のRFCには含まれていましたが、このRFCでは廃止されました。
理由はstr_containsを参照してください。

感想

PHPの文字列関数ってやたら大量に用意されてるわりに意外と基本的なところが抜けていたのですが、PHP8でstr_continsとこの関数が追加されたことによって、テキスト処理に必要なものは出揃うことになったのではないでしょうか。

他に必要なのって何かありますかね。デフォルト関数の命名規則とか?

Viewing all 536 articles
Browse latest View live