php -S はリクエストをひとつずつしか処理してなかった
久々に見かけた PHP の話題で PHP の組み込みサーバーは並列処理をしてくれないというのを見かけました
え そうだっけ?
たまにしか使わないものの あまり遅かった記憶がないです
あまり重たい処理をさせないのと ほとんどは静的ファイルのサーブ目的なので一つずつ処理しても遅いと感じてなかっただけなのかもしれません

せっかくので試してみました
PHP ファイルは sleep を入れて時間がかかるようにします

[a.php]
<?php

sleep(5);
echo 'ok';

Node.js を使って適当にリクエストを送ります
レスポンスが来たらリクエストの送信時刻(s)とレスポンスの受信時刻(e)をあわせて表示してます

> request()
{
s: 2024-03-08T14:59:55.767Z,
e: 2024-03-08T15:00:00.775Z
}
> request()
2024-03-08T15:00:03.111Z
{
s: 2024-03-08T15:00:03.111Z,
e: 2024-03-08T15:00:08.128Z
}
> request()
> request()
> request()
> request()
{
s: 2024-03-08T15:00:11.991Z,
e: 2024-03-08T15:00:16.997Z
}
{
s: 2024-03-08T15:00:12.783Z,
e: 2024-03-08T15:00:22.000Z
}
{
s: 2024-03-08T15:00:13.583Z,
e: 2024-03-08T15:00:27.000Z
}
{
s: 2024-03-08T15:00:14.222Z,
e: 2024-03-08T15:00:32.001Z
}

連続してリクエストしたときに 2 つめは 10 秒後というふうに 5 秒ずつ遅れてきてるので 1 つずつ処理されてるようですね
ちなみにサーバー側のログ

[Fri Mar  8 14:59:55 2024] 127.0.0.1:33850 Accepted
[Fri Mar 8 15:00:00 2024] 127.0.0.1:33850 [200]: GET /a.php
[Fri Mar 8 15:00:00 2024] 127.0.0.1:33850 Closing
[Fri Mar 8 15:00:03 2024] 127.0.0.1:33852 Accepted
[Fri Mar 8 15:00:08 2024] 127.0.0.1:33852 [200]: GET /a.php
[Fri Mar 8 15:00:08 2024] 127.0.0.1:33852 Closing
[Fri Mar 8 15:00:11 2024] 127.0.0.1:33854 Accepted
[Fri Mar 8 15:00:16 2024] 127.0.0.1:33854 [200]: GET /a.php
[Fri Mar 8 15:00:16 2024] 127.0.0.1:33854 Closing
[Fri Mar 8 15:00:16 2024] 127.0.0.1:33856 Accepted
[Fri Mar 8 15:00:16 2024] 127.0.0.1:33858 Accepted
[Fri Mar 8 15:00:21 2024] 127.0.0.1:33856 [200]: GET /a.php
[Fri Mar 8 15:00:21 2024] 127.0.0.1:33856 Closing
[Fri Mar 8 15:00:21 2024] 127.0.0.1:33860 Accepted
[Fri Mar 8 15:00:26 2024] 127.0.0.1:33858 [200]: GET /a.php
[Fri Mar 8 15:00:26 2024] 127.0.0.1:33858 Closing
[Fri Mar 8 15:00:31 2024] 127.0.0.1:33860 [200]: GET /a.php
[Fri Mar 8 15:00:31 2024] 127.0.0.1:33860 Closing

レスポンスを返して Closing した後に次のが Accepted になっています
本番向けじゃないと言う注意書きがありましたが こういう理由だったのですね
PHP で正規表現が全滅してた
PHP の OSS アプリケーションを動かそうとしたら謎のエラー
ドキュメントの手順どおりにやったはずだし 以前も使ったことがあるもので そのときは出た覚えのないエラー
新しいバージョンのみで発生?
エラーでググっても全然情報は無し

よくわからないし 中身まで見たくないしと動いてる環境からフォルダをコピーして来て 環境固有の設定だけ変更
これでも変わらず発生する
PHP バージョンはどっちも 8.0

仕方なくエラーが起きてるファイルと行数から中身を見てみることに
エラーの原因は null が来るはずないところで null が来てるせいみたい
null が想定されてないので null を渡すとエラーになる関数に null を渡して実行時エラー

どこで null が混入してる?と探してみると正規表現を使う関数で null が返ってきてる
引数を dump して見てみても どう考えても null になりそうにはないんだけど

コマンドラインで同じ処理を実行してみると warning が出た

preg_replace(): Compilation failed: unrecognised compile-time option bit(s) at offset 0

このせいで null になってるみたい
だけど 見たこと無いエラー
正規表現のコンパイルエラーだから不正な正規表現を入れると起こりそうだけど 特に問題ないはず
というかこれでも出る

echo preg_replace('/a/', 'b', 'abc');

PHP がバグってる?

こっちの warning でググってみると PHP ではなく libpcre の問題みたい
https://stackoverflow.com/questions/70040287/php7-4-preg-replace-compilation-failed-unrecognised-compile-time-option-bi

AlmaLinux9 だったので

dnf upgrade pcre2

で更新したら 10.40 になって エラーも出なくなった
FormData で送ると PHP の php://input で受け取れない
FormData で送るとテキストだけでも multipart/form-data 形式で送信されるようです
自分で Content-Type を指定しなくても自動で

Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryEeFLLVciEeJfWzFZ

みたいなヘッダーが追加されます

また PHP で生の POST データを受け取るための php://input は multipart 非対応らしいです
https://www.php.net/manual/ja/wrappers.php.php

送信データにファイルが無い場合でも multipart なら受け取れません
そのせいで FormData で送ると php://input でデータを受け取れないということになります

以下確認用

↓のページを用意

<?php

var_dump($_POST);
var_dump(file_get_contents('php://input'));

● 普通の form としての POST を実行してみる

fetch("", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: "a=1" })

レスポンス:
array(1) { ["a"]=> string(1) "1" } string(3) "a=1"

⇨ $_POST と php://input の両方で受け取れてる

● FormData で POST してみる

const fd = new FormData()
fd.append("a", "1")
fetch("", { method: "POST", body: fd })

ペイロード:
------WebKitFormBoundary7KvCNMsEQGTuSHn7
Content-Disposition: form-data; name="a"

1
------WebKitFormBoundary7KvCNMsEQGTuSHn7--

レスポンス:
array(1) { ["a"]=> string(1) "1" } string(0) ""

⇨ $_POST でだけ受け取れてる
PHP に組み込み関数のセーフ版を提供するライブラリがあるみたい
見かけた PHP のコードに Safe 版関数を使っているものがありました
使っているライブラリはこれのようです
https://github.com/thecodingmachine/safe

Safe 版ってなにしてるの?
文字数が長すぎるとか引数チェックをして脆弱性の対処をこのレイヤーでやってくれるの?
とか思っていたら単にエラーの場合に throw してくれるだけのようです

中身を見ても単純なものです

json_encode
https://github.com/thecodingmachine/safe/blob/v2.4.0/generated/json.php#L50

mkdir
https://github.com/thecodingmachine/safe/blob/v2.4.0/generated/filesystem.php#L1221

PHP の組み込み関数は例外を起こさず false を返して失敗を伝えるのが多いですからね

昔はその仕様に不満を感じて例外を投げてくれればいいのにと思ったこともあります
でも最近は逆で例外を投げられると catch が面倒なので return として成功と失敗を返してほしいと思ってます

JavaScript だと PHP と違いエラーが throw されるので自分でラップしてエラーでも return するようにしたりしてたりします
非同期関数だと 「.catch(error => null)」 とか 「.catch(error => ({ error }))」 で済むのに同期関数は try-catch 句が必要で const と相性が悪くてイライラしたり……

言語によっては例外というものがなく 成功/失敗という型の中に結果や原因を保持してそれを return するものもありますし PHP のこれはこれでいいものだと思います
例外が起きても ちゃんと作るなら生のエラーメッセージをそのまま使わずユーザーが見れる形のメッセージに置き換えたりといった処理が必要で catch して書き換えて 再 throw したりするなら throw はユーザーがするものとしてライブラリレイヤーでは想定されたエラーでは例外は起こさないのほうがいいのかなと思ったりです
PHP で名前付き引数が使えるようになってた
久々に PHP を使ったら名前付き引数が使えるようになってた (8.0 かららしい)

function fun($foo = 'FOO', $bar = 'BAR') {
echo "foo is $foo\n";
echo "bar is $bar\n";
}

fun(bar:"b");
foo is FOO
bar is b

PHP の関数って引数が多いのが多いし 引数だけみてこれがなんの値かわからないのが多かったからよさそう

fn1(1, 2, false, 'a', 3, true);

みたいなコードでそれぞれの引数が何を表してるのかなんて全然わからない

良さそうな機能ではあるんだけど 名前付き引数のみに制限はできないみたいなので 作る側が名前付き引数で使ってくれることを期待しても使う側が面倒だからと↑みたいコードで書くことは有り得そう
名前付き引数前提で考えてると引数の順番をそれほど考慮してないことも多くなりそうだし 名前の指定が必須ってできたほうがいいと思う

JavaScript だとオブジェクトで渡すフォーマットにしてその点を解決してるから PHP でもこれまでみたいに連想配列で受け取るほうがいいのかも

Python は構文として引数が positional のみか keyword のみかどっちでもありかを表現できるからいいんだけど
これを真似してくれないかな

def fn(foo, /, bar, *, baz):
print(foo, bar, baz)

foo は positional のみで keyword では渡せない
bar は positional でも keyword でも渡せる
baz は keyword のみで positional では渡せない

fn(1, 2, baz=3) # 1 2 3
fn(1, bar=2, baz=3) # 1 2 3

fn(foo=1, bar=2, baz=3) # error
fn(1, 2, 3) # error
AlmaLinux9 に PHP5.6 が入らなかった
PHP で古いバージョンと動作比較しようと PHP5.6 をインストールしようとしたら入りませんでした
CentOS7 や AlmaLinux8 だと remi リポジトリを追加して php56 をインストールすれば使えました
同じ remi なら AlmaLinux9 でもインストールできるだろうと思ったのですが php56 が見つかりませんでした

http://rpms.remirepo.net/enterprise/8/safe/
http://rpms.remirepo.net/enterprise/9/safe/

一覧をみても 9 用は 7.4 以降になっています

調べてみると remirepo の管理者が理由を回答していました
https://forums.centos.org/viewtopic.php?p=333674#p333699

PHP8.1 より前は RHEL9 系のデフォルトの openssl3 との互換性がないみたいです
公式リポジトリには 8.0 がありますがとても大きな互換性パッチを当ててるんだとか
remi だと 7.4 以降をインストールできるので 7.4 と 8.0 にパッチを当ててるのでしょうか
そういう苦労が必要ならかなり古い 5.6 を対応しないのは仕方ないですね

使いたいなら EL-8 を とありますし AlmaLinux8 に入れることにします
back/forward cache が効かない理由を調べる
以前 bfcache が Chrome に実装されたときにいくつかのページを試すと bfcache が使われてるページとそうでないページがあって なぜ使われたり使われなかったりするのか はっきりとした原因はわかりませんでした

その後いつのころからか devtools の Application タブに 「Back/forward cache」 という項目が増えていました
「Test back/forward cache」 というボタンがあるので確認したいページを開いて押します
ページがリロードされて bfcache が有効化どうかを表示してくれます
無効の場合は原因も教えてくれます
ときどき原因が表示されないこともありますけど

このブログだと成功になって bfcache が有効みたいですが メインの方のブログでは成功が出たかと思ったらすぐ失敗に切り替わってます

失敗になる原因のひとつは unload ハンドラが設定されてることで 結構見かけます
他にも拡張機能で JavaScript や CSS を挿入してる場合も無効になるようです
ページのカスタムや広告ブロック系の拡張機能とは相性が悪いですね

あと HTTP のレスポンスのヘッダーに 「Cache-Control: no-store」 が付いてる場合も bfcache が無効になるようです
自分で作ったページで unload などを使ってないのに bfcache が動いてなくて調べるとこれでした
Cache-Control なんてつけた覚えはなかったのですが PHP のレスポンスではデフォルトでついてるようでした
つく場所とつかない場所があって少し困りましたが session を使うと追加されるようです

戻るボタンで戻ったときでもサーバにアクセスしてログイン状態を確認したいのならいいのかもですが 戻るだけならログインチェックなどはしなくていいと思うんです
これまで見えていた画面ですし 新規に検索したり画面を移動するとそこでログインチェックが入りますし

PHP では

session_cache_limiter('');

で自動で Cache-Control ヘッダーを出力するのを防げるようです
PHP を Nginx で動かすとき index.php を公開フォルダにおかなくてもよかった
Apache の感覚で URL を rewrite して公開フォルダに配置した index.php を呼び出してそこから公開フォルダの外にある PHP ファイルを実行するものだと思ってた
だけど fastcgi_param の設定で実行するスクリプトファイルのパスを指定してる
URL のパスに応じた PHP ファイルを実行する場合は URL に応じたファイルのパスを設定するけど rewrite するような場合は URL 問わず常に同じ PHP ファイルになる
それなら直接設定ファイルにファイルのパスを書いておける
この指定は公開フォルダの内側にする必要はなし
/opt/app/static が公開フォルダだとして

fastcgi_param  SCRIPT_FILENAME  /opt/app/main.php;

と設定すれば公開フォルダ内に index.php みたいなエントリポイント PHP ファイルは要らなくなる

Apache の rewrite させる設定は何度見てもどうなってるのかよくわからないから こういうシンプルな方法でできるのはいいところ
PHP で指定の名前空間に属する関数やクラスなどの一覧を取得
指定の名前空間内で定義されているものの一覧を確認したい
だけど そういう関数はないみたいだったので自作
すべての関数取得やすべてのクラス取得といった関数はあったので 全部取得してから名前空間でフィルタしてる
サブ名前空間も含めるかを指定可能

function get_namespace_items($namespace, $include_sub_namespaces = false) {
$namespace = trim($namespace, '\\') . '\\';

$classes = get_declared_classes();
$interfaces = get_declared_interfaces();
$traits = get_declared_traits();
$functions = get_defined_functions();
$constants = get_defined_constants();

$filter = function ($items) use ($namespace, $include_sub_namespaces) {
$matched = [];
foreach ($items as $item) {
if (strpos($item, $namespace) === 0) {
if (
$include_sub_namespaces ||
strpos(substr($item, strlen($namespace)), '\\') === false
) {
$matched[] = $item;
}
}
}
return $matched;
};

return [
'class' => $filter($classes),
'interface' => $filter($interfaces),
'trait' => $filter($traits),
'function' => $filter($functions['user']),
'constant' => $filter(array_keys($constants)),
];
}



使用例

<?php

function get_namespace_items($namespace, $include_sub_namespaces = false) {
$namespace = trim($namespace, '\\') . '\\';

$classes = get_declared_classes();
$interfaces = get_declared_interfaces();
$traits = get_declared_traits();
$functions = get_defined_functions();
$constants = get_defined_constants();

$filter = function ($items) use ($namespace, $include_sub_namespaces) {
$matched = [];
foreach ($items as $item) {
if (strpos($item, $namespace) === 0) {
if (
$include_sub_namespaces ||
strpos(substr($item, strlen($namespace)), '\\') === false
) {
$matched[] = $item;
}
}
}
return $matched;
};

return [
'class' => $filter($classes),
'interface' => $filter($interfaces),
'trait' => $filter($traits),
'function' => $filter($functions['user']),
'constant' => $filter(array_keys($constants)),
];
}

// define and declare
require_once('./def.php');

var_dump(get_namespace_items('\\foo\\bar', false));
var_dump(get_namespace_items('\\foo\\bar', true));

[def.php]
<?php

namespace foo\bar;

function f() {}

class C {}

abstract class A {}

trait T {}

interface I {}

const c = 1;
define('d', 2);

// sub namespace
namespace foo\bar\baz;

function ff() {}

結果

// sub namespace なし
array(5) {
["class"]=>
array(2) {
[0]=>
string(9) "foo\bar\C"
[1]=>
string(9) "foo\bar\A"
}
["interface"]=>
array(1) {
[0]=>
string(9) "foo\bar\I"
}
["trait"]=>
array(1) {
[0]=>
string(9) "foo\bar\T"
}
["function"]=>
array(1) {
[0]=>
string(9) "foo\bar\f"
}
["constant"]=>
array(1) {
[0]=>
string(9) "foo\bar\c"
}
}

// sub namespace あり
array(5) {
["class"]=>
array(2) {
[0]=>
string(9) "foo\bar\C"
[1]=>
string(9) "foo\bar\A"
}
["interface"]=>
array(1) {
[0]=>
string(9) "foo\bar\I"
}
["trait"]=>
array(1) {
[0]=>
string(9) "foo\bar\T"
}
["function"]=>
array(2) {
[0]=>
string(9) "foo\bar\f"
[1]=>
string(14) "foo\bar\baz\ff"
}
["constant"]=>
array(1) {
[0]=>
string(9) "foo\bar\c"
}
}
HHVM の今後に期待
https://postd.cc/the-future-of-hhvm/

アロー関数やパイプライン演算子もあったりと PHP より使いやすいけどインストールが辛いし最近あまり話題聞かないから PHP7 にみんな移って放置されるのかと思ってたけど Hack が PHP とは別言語になっていくみたい

PHP ってちょっとしたことするには楽に書けてドキュメントも丁寧なのはいいけど しっかり使い込もうとすると色々辛いからね
PHP から離れて使いやすいようにしてくれるなら Hack でよくなるかも
今の Hack でも悪くはないけど HHVM のインストールが一苦労だから楽にしてくれれば助かるんだけど

でも PHP との互換性は持たせたないのに PHP の主要なツールとの互換性は持たせるってどうするんだろう?
今使われてる機能だけは残しても将来的にそのツールが Hack で廃止予定の参照などを使ったりするかもしれないのに
fedora の PHP 実行環境は FPM/FastCGI
root@localhost ~# cat /etc/fedora-release 
Fedora release 27 (Twenty Seven)
root@localhost ~# dnf install -y php httpd
root@localhost ~# systemctl restart httpd
root@localhost ~# echo "<?php phpinfo();" > /var/www/html/phpinfo.php
root@localhost ~# curl http://localhost/phpinfo.php 2>/dev/null | grep FPM
<tr><td class="e">Server API </td><td class="v">FPM/FastCGI </td></tr>
PHP の関数で参照を受け渡しがめんどう
php > $a = ["k" => []];
php > $f = function & (&$x) { return $x['k']; };
php > $b = &$f($a);
php > var_dump($a);
array(1) {
["k"]=>
&array(0) {
}
}

こうすれば 参照で渡して参照で受け取れる
$b の配列に値を追加すると $a['k'] も書き換わる状態

● 関数定義の 「function」 の後
● 引数の変数名の前
● 関数呼び出しの関数名の前

の 3 つ & が必要
どれかに & がないが場合は参照が $b に入らない

関数定義に & がないと参照を返さない
関数呼び出しに & をつけて参照を代入しようとすると Notice
php > $a = ["k" => []];
php > $f = function (&$x) { return $x['k']; };
php > $b = &$f($a);
PHP Notice: Only variables should be assigned by reference in php shell code on line 1

Notice: Only variables should be assigned by reference in php shell code on line 1
php > var_dump($a);
array(1) {
["k"]=>
array(0) {
}
}

引数に & がないと引数はコピーされたもの
参照を返して $b に代入しても $a は変わらない
php > $a = ["k" => []];
php > $f = function & ($x) { return $x['k']; };
php > $b = &$f($a);
php > var_dump($a);
array(1) {
["k"]=>
array(0) {
}
}

呼び出しに & がないと代入時に参照の代入にならない
関数は参照を返していても $b は参照じゃない
php > $a = ["k" => []];
php > $f = function & ($x) { return $x['k']; };
php > $b = $f($a);
php > var_dump($a);
array(1) {
["k"]=>
array(0) {
}
}
PHP で文字列中の {$var} を展開する
リテラルで書くときならダブルクオートにすれば展開される
$e = 'e';
$x = "abcd{$e}fg";
echo $x;
// abcdefg

でもすでに文字列になっていると出来る方法が用意されてない

eval するとか
$e = 'e';
$x = "abcd{$e}fg";
echo $x;
// abcd{$e}fg

eval("echo \"$x\";");
// abcdefg

data uri で読み込んでみるとか
$e = 'e';
$x = 'abcd{$e}fg';
echo $x;
// abcd{$e}fg

$s = include 'data:text/plain,<?php return "' . $x . '";';
echo $s;
// abcdefg

※ allow_url_include が true じゃないとだめ

変な方法ばかり

正規表現が一番マシかもだけど PHP の変に柔軟なものを全対応はさすがにやってられない
文字列にするする以上 PHP に合わせず自作の記法で置換するのがよさそう
PHP で変数名を式で作る
なんか気持ち悪い

<?php

for($i=0;$i<10;$i++){
${"fn" . $i} = function() use ($i) { echo $i; };
}

for($i=0;$i<10;$i++){
${"fn" . $i}();
}

// 0123456789