74
41

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【PHP8.5】PHP8.5の新機能

Last updated at Posted at 2025-08-11

PHP8.5 / PHP8.4 / PHP8.3 / PHP8.2 / PHP8.1

2025/08/12、PHP8.5がフィーチャーフリーズしました。
言語機能に関わるような機能の追加・変更が締め切られたということです。
今後はデバッグを繰り返しながら完成度を高めていき、2025/11/20にPHP8.5.0がリリースされる予定です。

というわけでPHP8.5で実装されるRFCを見てみましょう。

RFC

Pipe operator

パイプライン演算子です。

PHP8.5
$len = random_bytes(10)
    |> fn($x) => str_repeat($x, 2)
    |> fn($x) => str_replace($x, 'c', 'z')
    |> fn($x) => substr($x, 5)
    |> strlen(...);

関数のネストを左から順に書けるようになります。
大量の括弧から解放され、フラットで見やすくなり、データの流れもわかりやすくなります。

また将来の展望として、高階関数や部分適用の導入も見据えているようで、その第一歩第二歩としての意味合いもあるようです。
今後がどうなるのか楽しみな機能ですね。

array_first() and array_last()

配列の最初と最後の値を取得する関数です。

PHP8.5
$array = [1, 2, 3, 4, 5];

echo array_first($array); // 1
echo array_last($array);  // 5

何気にこれまで存在しなかったんですよねこれ。

PHP8.4
// PHP7.3以上
echo $array[array_key_first($array)]; // 1
echo $array[array_key_last($array)];  // 5

// PHP7.3未満
echo array_shift(array_values($array)); // 1
echo array_pop(array_values($array));   // 5

わかりやすさ的にも計算量的にも、array_first/array_lastを使うのが適切です。

Clone with v2

cloneキーワードを言語構造に変更し、第二引数$withPropertiesを追加します。

結果どうなるかというと、プロパティを更新しつつcloneできるみたい。

PHP8.5
class Foo {
    public int $pub = 1;
}
 
$foo = new Foo();
var_dump($foo); // $pub=>1

$bar = clone($foo, ["pub" => 5]);
var_dump($bar); // $pub=>5

かつてほぼ同じ内容のClone withというRFCがありまして、しかしこれは独自キーワードwithを追加することから認知負荷が高くなりるということで放棄されました。
そのため普通の関数と同じように使えて理解しやすい、今回の変更が受け入れられたようです。

Final Property Promotion

PHP8.0でコンストラクタ引数昇格が導入されました。
PHP8.4でfinalプロパティが導入されました。

しかしfinalプロパティは何故かコンストラクタ引数昇格に対応していませんでした。

class HOGE{
	public function __construct(
		final public int $x
	){
		// final public $HOGE->xが生える
	}
}

ということでfinalプロパティが生えるようになりました。
正直単にPHP8.4で対応忘れていたんじゃないかという気がしないでもないですが、実際は意図的に未対応としたらしいです。
よくわからない。

Attributes on Constants

PHP8.0でアトリビュート使えるようになりました
しかし何故かコンパイル時定数には対応していなかったので、今回対応します。

ちなみにコンパイル時定数とはconstであり、define()は対象外です。

PHP8.5
#[\MyAttribute]
const Example1 = 1; // OK
 
#[\MyAttribute]
const Example2 = 2, Example3 = 3; // エラー 一括定義は禁止

#[\MyAttribute]
define('Example4' ,4); // エラー

複数値の一括定義に対してアトリビュートを使うとエラーになります。
どの定数に対するアトリビュートなのかわかりにくいからだそうです。

Grapheme cluster for levenshtein, grapheme_levenshtein function

書記素に従ってレーベンシュタイン距離を計算する関数grapheme_levenshteinを追加します。

PHP8.5
var_dump(grapheme_levenshtein("\u{0065}\u{0301}", "\u{00e9}")); // 0

\u{0065}e\u{0301}´であり、これを合わせるとéになります。
いっぽう\u{00e9}éを表す1文字です。
ということでこのふたつは同じ文字だそうです。

わかんねえよ。

元々mb_levenshteinを出したところ、いやーgrapheme_levenshteinのほうが欲しいんじゃよとか言われたみたいです。
mb_levenshteinでは"\u{0065}\u{0301}"と"\u{00e9}"は別の文字になると思いますが、よくわかりません。

Add locale for case insensitive grapheme functions

大文字と小文字を区別しない書記素関数にロケールを追加します。

PHP8.5
var_dump(grapheme_stripos("i", "\u{0130}", 0, "tr_TR")); // 0
var_dump(grapheme_stripos("i", "\u{0130}", 0, "en_US")); // false

$nabe = '邊';
$nabe_E0101 = "邊\u{E0101}";
var_dump(grapheme_levenshtein($nabe, $nabe_E0101));                               // 0
var_dump(grapheme_levenshtein($nabe, $nabe_E0101, locale: "ja_JP-u-ks-identic")); // 1
var_dump(grapheme_strpos($nabe, $nabe_E0101));                                    // 0
var_dump(grapheme_strpos($nabe, $nabe_E0101, locale: "ja_JP-u-ks-identic"));      // false

文字コードなんもわからん…
だいたいja_JP-u-ks-identicってなんだよ検索にRFC以外一切出てこないぞ。

Asymmetric Visibility for Static Properties

PHP8.4でプロパティの非対応可視性が導入されましたが、静的プロパティには非対応でした。

PHP8.4
class Example
{
    public private(set) static string $classTitle = 'Example class'; // エラー
}

当時は実装が困難だから諦めたそうですが、その後調査を進めたところ容易な実装方法が見つかったということです。

PHP8.5
class Example
{
    public private(set) static string $classTitle = 'Example class';
 
    // 暗黙的なpublic get
    protected(set) static int $counter = 0;
 
    public static function changeName(string $name): void
    {
        // private setなので許可
        self::$classTitle = $name;
    }
}

// OK
print Example::$classTitle;

// エラー
Example::$classTitle = 'Nope';

静的プロパティを、書き込みはprivate、読み取りはpublic、のように設定できるようになります。

Marking return values as important (#[\NoDiscard])

返り値を無視することを許さない、NoDiscardアトリビュートです。
由来はC23のnodiscardのようです。

PHP8.5
#[\NoDiscard]
function hoge(): int{return 1;}

hoge(); // エラー

$ret = hoge(); // OK

NoDiscardのついた関数やメソッドは、必ず変数で受け取らないといけません。
受け取った上で使わないのは問題ありません。

このアトリビュートはfopen()のような通常の関数には使われるべきではなく、flock()のように無視すると致命的な問題が起こる関数にのみ適用されます。

Add get_error_handler(), get_exception_handler() functions

関数get_error_handler()get_exception_handler()を導入します。

エラーを操作する関数としてset_error_handler()restore_error_handler()が存在します。
しかし、現在のエラーハンドラを取得する関数はなぜか存在しませんでした。
ということでこれを追加します。

PHP8.5
// 新しいハンドラ
$new_handler = $this->error_handler(...);
$old_handler = set_error_handler($new_handler);
get_error_handler() === $new_handler; // true

// 元に戻す
restore_error_handler();
get_error_handler() === $old_handler; // true

// null
set_error_handler(null);
get_error_handler(); // null

便利といえば便利ですが、しかしユーザがわざわざこれを使って何かするというのはあまり考えづらいですね。
主にフレームワークが使う機能になるでしょう。

Support Closures in constant expressions

定数式としてクロージャを書けるようになります。

PHP8.5
// 定数
const Foo = new MyObject(static function () {
    return 'foo';
});

// 引数デフォルト値
function foo(
    string $input,
    array $callbacks = [
        static function ($value) {
            return \strtoupper($value);
        },
        static function ($value) {
            return \preg_replace('/[^A-Z]/', '', $value);
        },
    ]
) {
    foreach ($callbacks as $callback) {
        $input = $callback($input);
    }
    return $input;
}
var_dump(foo('Hello, World!')); // HELLOWORLD

// アトリビュート
final class Locale
{
    #[Validator\Custom(static function (string $languageCode): bool {
        return \preg_match('/^[a-z][a-z]$/', $languageCode);
    })]
    public string $languageCode;
}

ユーザがこんなプログラムを書くかって言われたら正直書かない気がしますね。
少なくとも私には書けません。
これも主にフレームワーク向けの機能になると思います。

First Class Callables in constant expressions

↑のSupport Closures in constant expressionsの補足RFCです。
定数式で第一級オブジェクトを使うと、PHP8.4以前はコンパイルエラーとなっていたのがPHP8.5では有効になります。

ということらしいですが、正直何言ってるのかわからない。

PHP8.5
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class Attr {
    public function __construct(public Closure $value) {}
}
 
#[Attr(self::myMethod(...))]
#[Attr(strrev(...))]
class C {
    private static function myMethod(string $foo) {
        return "XXX";
    }
}
 
foreach ((new ReflectionClass(C::class))->getAttributes() as $reflectionAttribute) {
    $closure = $reflectionAttribute->newInstance()->value;
    var_dump($closure('abc'));
}

PHP8.4ではConstant expression contains invalid operationsのFatal errorになりますが、PHP8.5ではエラーにならず正常に動作します。
うむ、いったい何をやっているのかさっぱりだ。
誰か詳しい人が利点と使い道を教えてくれるはず。

Persistent curl share handle improvement

リクエストを超えてcurlハンドルを共有する関数curl_share_init_persistent()を追加します。

curl_init()は、呼び出すたびに新たな接続を開始します。
curl_share_init()を使うとひとつのリクエスト内で接続を共有しますが、それでもリクエストが終わると終了していました。
ずっと接続を開きっぱなしにしたい相手に使うことで、毎回接続切断にかかるコストや時間を減らすことができるようになります。

しかしこれ、下手にユーザが使うと相手に大迷惑になりそうですね。

PHP8.5
$cs = curl_share_init_persistent([CURL_LOCK_DATA_CONNECT, CURL_LOCK_DATA_SSL_SESSION, CURL_LOCK_DATA_DNS]);

$ch = curl_init("https://localhost");
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SHARE, $cs);

curl_exec($ch);

なお、本RFCより前にAdd persistent curl share handlesというRFCが可決されたのですが、その後の調査によって問題が発覚したので廃棄され、新たなRFCとして発行されたという経緯があります。
たとえばオプションCURL_LOCK_DATA_COOKIEは、元のRFCでは許可されていましたが、これを共有すると間違いなく事故るので禁止になりました。

Error backtraces v2

致命的エラーがバックトレースを表示するようになります。

PHPではエラーが出た場合、そのエラーについてしか情報が出力されません。
それでも下手な言語よりよっぽどわかりやすいのですが、たとえばエラーが出た関数がどこから呼ばれたのかといった情報はわかりません。
PHP8.5以降はバックトレースも出力できるようになります。

PHP8.5
set_time_limit(1);
 
function recurse() {
    usleep(100000);
    recurse();
}
 
recurse();

/*
Fatal error: Maximum execution time of 1 second exceeded in path/to/foo.php on line 6
Stack trace:
#0 path/to/foo.php(6): usleep(100000)
#1 path/to/foo.php(7): recurse()
#2 path/to/foo.php(7): recurse()
#3 path/to/foo.php(7): recurse()
#4 path/to/foo.php(7): recurse()
#5 path/to/foo.php(7): recurse()
#6 path/to/foo.php(7): recurse()
#7 path/to/foo.php(7): recurse()
#8 path/to/foo.php(7): recurse()
#9 path/to/foo.php(10): recurse()
#10 {main}
*/

サンプルの無限ループでは実質意味がないですが、実環境においてはエラーの調査が捗るようになりますね。

デフォルトはオンです。
無効にしたい場合はphp.iniからfatal_error_backtraces = Offにします。

そういえばLaravelはデフォルトでバックトレースを出してきますが、あれはどうやって実現してるんだろう。

Change Directory class to behave like a resource object

Directoryクラスは本来dir()関数で生成しますが、new Directory()と書いても動いてしまうし、それを使おうとすると確実にエラーになります。
ということで不要な操作を禁止します。

PHP8.5
new Directory();               // エラー
class HOGE extends Directory{} // これもエラー
$cloned = clone $directory;    // これもエラー

PHP8.4以下ではエラーは起きませんが、普通はこんな使い方をしないし、しても意味がありません。

PHPは外部接続やファイルハンドルなどに、かつてはリソース型という特殊なデータ構造を多用していました。
これはわかりにくいしPHPの内部的にも都合が悪いようで、片端からオブジェクトに置き換えられつつあります。

たとえばcurl_init()は以前はcurlリソースを返していたのがPHP8.0からCurlHandleクラスを返すようになりました。
しかし互換性を保つため、使い方は$curl->exec();ではなくcurl_exec($curl);のままです。
このような特殊なクラスは完全不透明クラス(fully opaque class)と呼ばれ、extendsもnewもserializeもcloneもできない、その他諸々の扱いが普通のクラスと異なる特別なクラスです。

Directoryは完全不透明クラスの概念が固まる前に作られたクラスなので、普通のクラスのように扱うことができてしまっていました。
これを完全不透明クラスに変更するのが今回のRFCになります。

Make OPcache a non-optional part of PHP

OPcacheが常にロードされるようになります

OPcacheを使うとインタプリタであるPHPをコンパイル言語のように実行することができ、圧倒的な高速化が可能となります。
是非使用しましょう。
ただ常にロードされる = 常に使用可能というわけではなく、設定で無効化することは可能です。

これはどちらかというとユーザのためというよりは開発者のための変更のようです。
OPcacheはシステムの奥深くまで入り込んでいる割に任意ロードだったので、PHPのソースコード内でOPcacheが存在するしないによる分岐がえらいことになっていました。
今後は必ず使用可能になるので、if文を減らせるとのことです。

Add RFC3986 and WHATWG URL compliant API

RFC3986に基いた、URIを正しく扱うことのできる機能を追加します。

PHPでは、curl拡張はRFC3986に基いたURLを取り扱い、FILTER_VALIDATE_URLはRFC2396で、そしてparse_urlはそのどちらでもないと扱いがバラバラです。
これは攻撃者の付け入る隙になります。

たとえばPHPではありませんが、Log4jではURLの検証と呼び出しに異なるパーサを使っていたせいでCVE-2021-45046脆弱性発生したという実例があります。

ということでドメインやパスやフラグメントなどを正確に取り扱うクラスUriを導入して相互変換できるようにします。

PHP8.5
$uri = new Uri\Rfc3986\Uri("https://example.com");    // RFC3986のURI
$uri = Uri\Rfc3986\Uri::parse("https://example.com"); // こっちもOK
echo $uri->toString();                                // https://example.com

$uri = new Uri\Rfc3986\Uri("/foo", $uri);
echo $uri->toString();                                // https://example.com/foo

echo $uri->resolve("/bar")->toString();               // https://example.com/bar

$uri = new Uri\Rfc3986\Uri("invalid uri");            // InvalidUrlException
$uri = Uri\Rfc3986\Uri::parse("invalid uri");         // null

試してみたところ、PHP8.5.0α2時点ではまだ全てがマージされておらずUri\Rfc3986\Uri::withPath()が未実装だったりであんまり動きませんでした。

#[\Deprecated] for traits

トレイトに#[\Deprecated]アトリビュートを付けられるようになります。

PHP8.5
#[\Deprecated]
trait DemoTrait {}

class DemoClass {
	use DemoTrait;
}
// Deprecated: Trait DemoTrait used by DemoClass is deprecated in %s on line %d

印がつけられたトレイトを使おうとするとE_DEPRECATEDが発生するので、移行の目印になります。

FILTER_THROW_ON_FAILURE

filter_var等は検証に失敗するとfalseが返ってきます。
オプションFILTER_NULL_ON_FAILUREを渡すと失敗したときにnullが返ってきます。
同様に失敗したらエラーになるオプションFILTER_THROW_ON_FAILUREを追加します。

PHP8.5
filter_var('hoge', FILTER_VALIDATE_BOOLEAN);                          // false
filter_var('hoge', FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);  // null
filter_var('hoge', FILTER_VALIDATE_BOOLEAN, FILTER_THROW_ON_FAILURE); // Exception

あんまり関係ないんだけど、たぶんこれをfilter_var_arrayに突っ込んだら最初にひっかかった例外1つだけが出てくると思うんだけど、これを全ての例外が入った配列(的なもの)を取得できるようにできないかなとか思ってる。

Extend #[\Override] to target properties

PHP8.3でOverrideアトリビュートが導入されましたが、プロパティには未対応でした。
当時はプロパティに設定する意味が全くなかったからですが、その後抽象クラスにプロパティを書けるようになったり、プロパティフックを書けるようになったりしたので、プロパティにもOverrideを付ける意味がでてきました。

PHP8.5
class P {
    abstract public mixed $p { get; }
}
 
class C extends P {
    #[\Override]
    public mixed $p;
}

privateプロパティには付けられない、Overrideしていないプロパティに付けられないなどの挙動はメソッドと同じです。

PHP8.5
class C
{
    #[\Override]
    public mixed $p; // エラー
}

class P {
    private mixed $p { get; }
}
 
class C extends P {
    #[\Override]
    public mixed $p; // エラー
}

#[\DelayedTargetValidation] attribute

アトリビュートのコンパイル時エラーを、実行時エラーに変更するアトリビュート#[\DelayedTargetValidation]を導入します。

どういうこと?

PHP8.5でうごかない
class P {
    public const NAME = 'P';
}
 
class C extends P {
    #[\Override]
    public const NAME = 'C';
}

このコード、PHP8.5時点ではコンパイルエラーになって一切動きません。
ところでたとえば今後もしPHP8.6で定数へのOverrideアトリビュートが有効になったとしたら、全く同じコードがPHP8.6では動作するようになります。
そのときになって後方互換性のためにこのコードをPHP8.5でも動かしたいとなったらどうすればいいでしょう。
そこでDelayedTargetValidationです。

PHP8.5でうごく
class P {
    public const NAME = 'P';
}
 
class C extends P {
    #[\DelayedTargetValidation]
    #[\Override]
    public const NAME = 'C';
}

このコードはP::NAMEを呼び出さないかぎりPHP8.5でも動作します。
P::NAMEを呼んだら実行時エラーで終了します。
めでたしめでたし。

…めでたいか?
正直使いどころがよくわからないけど、こういうのが好きな人がきっと芸術的なコードを書いてくれることでしょう。

なお、このアトリビュートが有効になるのは、OverrideなどPHP本体に含まれるアトリビュートだけに対してです。
なぜなら、ユーザが作成するアトリビュートは元より実行時エラーだからです。

Cookies Having Independent Partitioned State (CHIPS)

setcookie関数にCHIPSに対応するオプションpartitionedを追加します。

// PHP8.5
setcookie('name', 'value', ['secure' => true, 'partitioned' => true]);

// PHP8.4
setcookie('name', 'value', ['secure' => true, 'samesite' => 'strict; Partitioned;']);

PHP8.4までは冗談みたいな方法しかなかったわけですが、ようやくまともに書けるようになります。

というか任意の引数を渡せるようにしてくれ。

Warnings for PHP 8.5

いくつかの値の取り扱いが厳しくなります。
PHP8.5でE_WARNINGになり、そしてPHP9でエラーになる予定です。
気を付けましょう。

$a = (int)NAN;
// PHP8.4 エラー出ない
// PHP8.5 Warning: unexpected NAN value was coerced to TYPE

$b = (int)(PHP_INT_MAX+1);
// PHP8.4 エラー出ない
// PHP8.5 Warning: non-representable float was cast to int

ちなみに(int)(PHP_INT_MAX+1)-9223372036854775808なので、エラーが出ないPHP8.4でも盛大なバグですね。

Deprecations for PHP 8.4

PHP8.4で非推奨になった機能のRFCです。

もちろんほとんどはPHP8.4で実装されたのですが、Deprecate returning non-string values from a user output handlerDeprecate producing output in a user output handlerの2件は諸事情でPHP8.4に入りませんでした。
それらが8.5でマージされました。

ob_startは出力バッファリング終了時に発火するコールバックを設定可能で、出力を変換できます。
この返り値をstringに限定します。

PHP8.5
function callback($buffer){
  return false;
}

ob_start();
ob_start('callback');
echo 'なか';
ob_end_flush();
echo 'そと';
ob_end_flush();
// PHP8.5 Deprecated: ob_end_flush(): Returning a non-string result from user output handler callback is deprecated
// PHP8.4 "なかそと"

これまでは文字列以外にtrue/falseを返すことが可能でした。
trueは""扱いでまだいいのですが、falseを返すと出力が次のバッファに持ち越されるという謎挙動でたいへんわかりにくい動作になっていました。

そういう厄介な挙動を排除しました。

Deprecations for PHP 8.5

PHP8.5で非推奨になる機能です。
PHP8.5でE_DEPECATEDになり、そしてPHP9でエラーになる予定です。
今後は使用しないように注意しましょう。

数が非常に多いので個別紹介はまたの機会に。
まあほとんどは滅多に遭遇しないエッジケースや変な使い方です。

PHP8.5
switch ($value) {
    case 'foo':
    case 'bar'; // Deprecated: Case statements followed by a semicolon (;) are deprecated, use a colon (:) instead
    default:
        echo 'hoge';
}

$a = (integer)$i; // Deprecated: Non canonical cast (integer|boolean|double|binary) is deprecated, use the (int|bool|float|string) cast instead

`ls`; // Deprecated: The backtick (`) operator is deprecated, use shell_exec() instead

ReflectionMethod::setAccessible(true); // Deprecated: as it has no effect

switchの区切りに:のかわりに;が使えるなんて初めて知ったよ。
実際、マニュアルに堂々と書かれているにもかかわらずGitHubの上位1500パッケージでの使用数が0というほど全く使われていないようです。

一番影響が大きいのはバックティック`の非推奨ですかね。
いやーこれ受理されるとはあんまり思ってなかった。
シングルクオート'と区別がつきにくいし、この記号でまさかシェル呼び出しなんて想像つかないから、互換性を考えなければ廃止は妥当なところだと思いますが。
ということで今後は素直にshell_execを使いましょう。

ちなみにこのRFCは多少の悶着があり、特に代替手段が書かれてないからどうすればいいのかわからないという意見が受け入れられてエラーメッセージにuse xxxのような表記が追加されました。

感想

PHP8.5の目玉はやはりパイプライン演算子ですね。
これを導入することで、PHPのソースコードの見た目が劇的に変わりそうです。

それ以外にも細々とした、それでいて使い勝手は間違いなく向上する変更が多々ありますね。
今後もPHPの発展は止まりません。

あと私にはよくわからない高度な内容もいくつかあったのですが、誰か詳しい人がきっと解説してくれるはず。

74
41
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
74
41

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?