3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【PHP8.3】標準関数のオーバーロードを削除してシグネチャをひとつにするよ

Last updated at Posted at 2023-08-21

言語によっては、同じ名前のメソッドを複数定義できるものがあります。
一般的にオーバーロードと呼ばれる機能で、Javaなど型が厳格な言語ではよく見ます。

Java
public class Foo {
    public static void bar(int val) {
        System.out.println("int型の" + val);
    }
    public static void bar(String val) {
        System.out.println("String型の" + val);
    }
}

Foo::bar(1);   // int型の1
Foo::bar("1"); // String型の1

引数の型によって別のメソッドが実行されます。

翻ってPHPは、同名のメソッド・関数を定義することができません。
昔のPHPは引数に型を書くことができず、区別をつけることができなかったからです。
今は引数の型を書くことができるようになりましたが、別に書かなくても動くので今でも区別できません。

ところがPHPの組込関数には、そんな制限をぶっ飛ばしたかのような関数が存在します。
たとえばDatePeriod::__construct()にはシグネチャが3種類もあります。

class DatePeriod
{
    public function __construct(DateTimeInterface $start, DateInterval $interval, DateTimeInterface $end, int $options = 0) {}
    public function __construct(DateTimeInterface $start, DateInterval $interval, int $recurrences, int $options = 0) {}
    public function __construct(string $isostr, int $options = 0) {}
}

まあ実際存在するメソッドはひとつだけで、関数の中でがんばって引数の型をチェックしているだけなんですけどね。

ということで組込関数において関数のオーバーロードをやめようというRFCが提出されました。
既に投票は終わっていて、今後PHP8.3以降のバージョンで順次非推奨となり、PHP9以降で削除されます。

以下は該当のRFC、Deprecate functions with overloaded signaturesの紹介です。

PHP RFC: Deprecate functions with overloaded signatures

Introduction

メソッドのオーバーロードは、Java開発者にはお馴染みですが、PHPはサポートしていません。
しかしながら、いくらかのPHP内部関数やメソッドには、複数のシグネチャが存在するように見えるものがあります。
すなわち、オーバーロードが存在します。

ここしばらく、PHPは内部コードとユーザランドコード間の不整合を減らす方向に進化しています。
この取り組みを継続するためには、PHPネイティブにオーバーロードをサポートするか、内部関数のオーバーロードを廃止していく必要があるでしょう。
前者の解決策は効率的な解決が不可能であることが知られているため、後者が望ましい選択肢です。

さらに、一貫性を維持することだけが本RFCの目的ではありません。

PHP8.0より前のバージョンでは、オーバーロードが大きな害をもたらすことはありませんでした。
PHP8.0以降では、以下のような理由によってオーバーロードは大きな問題になりつつあります。

・ReflectionParameterでデフォルト値を取ってこれるようになった。
・シグネチャによって処理が全く異なるため、名前付き引数に適切な引数名が設定できない。
・2年以上前から、オンラインマニュアルのシグネチャはソースコードに基いて半自動更新されているが、オーバーロードされた関数は自動更新できないため、シグネチャが古くなったり誤ったりする可能性がある。

名前付き引数のRFCが受理されたのち、PHP8.0では何十何百もの関数が修正され、正確な引数処理セマンティクスに従って動作するようになりました。
影響のない関数についてはずっと修正され続けています。

このRFCでは、後方互換性に影響を与えるケースを取り上げます。

Proposal

本RFCでは、以下に取り上げるオーバーロード関数のシグネチャを、PHP8.3以降のどこかの段階で廃止します。
PHP8.3では段階的移行を容易にするため、代替の関数が必要なものについては追加します。
次のマイナーバージョンでは、影響を受けるシグネチャを非推奨にします。
最後に、次のメジャーバージョンであるPHP9.0、もしくはさらに次のPHP10.0において、非推奨としていたシグネチャを実際に削除します。

DatePeriod::__construct()

受理。1つめのシグネチャ以外はPHP8.4で非推奨、PHP9.0で削除される。

DatePeriod::__construct()には3つのシグネチャが存在します。

class DatePeriod
{
    public function __construct(DateTimeInterface $start, DateInterval $interval, DateTimeInterface $end, int $options = 0) {}
    public function __construct(DateTimeInterface $start, DateInterval $interval, int $recurrences, int $options = 0) {}
    public function __construct(string $isostr, int $options = 0) {}
}

1つめを残し、3番目についてはファクトリーメソッドを用意します。

PHP8.4
class DatePeriod
{

    public function __construct(DateTimeInterface $start, DateInterval $interval, DateTimeInterface|int $end, int $options = 0) {}
 
    public static function createFromISO8601String(string $specification, int $options = 0): static {}
}

上位2000パッケージのうち、3番目の使い方をしているものは存在しませんでした。

dba_fetch()

受理。1つめのシグネチャ以外はPHP8.3で非推奨、PHP9.0で削除される。

dba_fetch()はBerkeley DBからデータをフェッチする関数であり、現在2つのシグネチャをサポートしています。

function dba_fetch(string|array $key, resource $dba, int $skip = 0): string|false {}

function dba_fetch(string|array $key, int $skip, resource $dba): string|false {}

引数$dba$skipは、どちらの順番でも受け付けてしまいます。
このRFCでは、後者の使い方をPHP8.3で非推奨とし、PHP9.0で削除します。

PHP8.3
function dba_fetch(string|array $key, resource $dba, int $skip = 0): string|false {}

上位2000パッケージのうち、後者の使い方をしているものは存在しませんでした。

FFI::cast(), FFI::new(), and FFI::type()

受理。1つめのシグネチャ以外はPHP8.3で非推奨、PHP9.0で削除される。

FFI::cast()FFI::new()FFI::type()には、現在2つのシグネチャが存在します。

class FFI
{
    public static function cast(FFI\CType|string $type, FFI\CData|int|float|bool|null &$ptr): ?FFI\CData {}
    public function cast(FFI\CType|string $type, FFI\CData|int|float|bool|null &$ptr): ?FFI\CData {}

    public static function new(FFI\CType|string $type, bool $owned = true, bool $persistent = false): ?FFI\CData {}
    public function new(FFI\CType|string $type, bool $owned = true, bool $persistent = false): ?FFI\CData {}

    public static function type(string $type): ?FFI\CType {}
    public function type(string $type): ?FFI\CType {}
}

静的メソッドは定義済の型のみですが、インスタンスメソッドはcdef型もサポートしています。

インスタンスメソッドとして呼び出す方が一般的なので、このRFCでは静的メソッドのほうを廃止します。
PHP9もしくは10で削除され、以下のようになります。

PHP9.0
class FFI
{
    public function cast(FFI\CType|string $type, FFI\CData|int|float|bool|null &$ptr): ?FFI\CData {}

    public function new(FFI\CType|string $type, bool $owned = true, bool $persistent = false): ?FFI\CData {}

    public function type(string $type): ?FFI\CType {}
}

上位2000パッケージのうち、該当のメソッドを静的に呼び出しているものは1パッケージだけでした。

get_class() and get_parent_class()

受理。1つめのシグネチャ以外はPHP8.3で非推奨、PHP9.0で削除される。

get_class()get_parent_class()には、現在2つのシグネチャが存在します。

function get_class(object $object): string {}

function get_class(): string {}

引数があればそのオブジェクトのクラス名を取得し、なければ現在のコンテキストのクラス名を取得します。

PHP7.2で受理されたget_class() disallow null parameterの影響により、get_classの引数にはデフォルト値が存在しません。

このRFCでは、引数のない呼び出しをPHP8.3で非推奨とし、PHP9で削除します。

PHP9.0
function get_class(object $object): string {}

function get_parent_class(object|string $object_or_string): string {}

現在get_class()を引数なしで呼んでいるコードは、get_class(self::class)もしくはget_class($this)と修正する必要があります。
またget_parent_class()を引数なしで呼んでいるコードは、get_class(parent::class)もしくはReflectionClass::getParentClass()を使うように修正する必要があります。

上位2000パッケージのうち、該当のメソッドを引数なしで呼び出しているものは10パッケージでした。

IntlCalendar::set()

受理。1つめのシグネチャ以外はPHP8.4で非推奨、PHP9.0で削除される。

IntlCalendar::set()には、現在4つのシグネチャが存在します。

class IntlCalendar
{

    public function set(int $field, int $value): true {}
    public function set(int $year, int $month, int $dayOfMonth): true {}
    public function set(int $year, int $month, int $dayOfMonth, int $hour, int $minute): true {}
    public function set(int $year, int $month, int $dayOfMonth, int $hour, int $minute, int $second): true {}
}

最初のひとつはフィールドを指定して直接書き換えるもので、それ以外は複数のフィールドをそれぞれ書き替えるものです。
そして、4つの引数を持つIntlCalendar::set()は呼び出すことができません。

本RFCでは、まずPHP8.3において後ろ3つのsetを代替するメソッドを新しく追加します。
そして後ろ3つのsetはPHP8.4で非推奨とし、PHP9で削除します。

PHP9.0
class IntlCalendar
{
    public function set(int $field, int $value): true {}

    public function setDate(int $year, int $month, int $dayOfMonth): void {}
    public function setDateTime(int $year, int $month, int $dayOfMonth, int $hour, int $minute, ?int $second = null): void {}
}

追加されたメソッドの返り値はvoidです。

また、手続き型として存在するintlcal_set()関数はこのエイリアスであるため、同じく非推奨化されます。
というよりそもそも手続き型自体がレガシーなので、intlcal_set()関数自体の削除を提案します。

3つ以上の引数を持つIntlCalendar::set()は、2引数のメソッドに変更するか、PHP8.3で新設されるメソッドに変更する必要があります。

上位2000パッケージのうち、IntlCalendar::set()を3つ以上の引数で呼び出しているものは存在しませんでした。

IntlGregorianCalendar::__construct()

受理。1つめのシグネチャ以外はPHP8.4で非推奨、PHP9.0で削除される。

IntlGregorianCalendar::__construct()には、現在3つのシグネチャが存在します。

class IntlGregorianCalendar
{

    public function __construct(IntlTimeZone|DateTimeZone|string|null $timezone = null, ?string $locale = null) {}
    public function __construct(int $year, int $month, int $dayOfMonth) {}
    public function __construct(int $year, int $month, int $dayOfMonth, int $hour, int $minute, ?int second = null) {}
}

IntlGregorianCalendar::__construct()も4引数で呼び出すことはできません。

本RFCでは、まずPHP8.3において後ろ2つのコンストラクタを代替するメソッドを新しく追加します。
そして後ろ2つのコンストラクタはPHP8.4で非推奨とし、PHP9で削除します。

PHP9.0
class IntlGregorianCalendar
{
    public function __construct(IntlTimeZone|DateTimeZone|string|null $timezone = null, ?string $locale = null) {}

    public static function createFromDate(int $year, int $month, int $dayOfMonth): static {}
    public static function createFromDateTime(int $year, int $month, int $dayOfMonth, int $hour, int $minute, ?int second = null): static {}
}

手続き型として存在するintlgregcal_create_instance()関数はこのエイリアスであり、同じ影響を受けます。
こちらもレガシーなので、intlgregcal_create_instance()関数自体の削除を提案します。

3つ以上の引数を持つIntlGregorianCalendar::__construct()は、2引数のメソッドに変更するか、PHP8.3で新設される静的メソッドに変更する必要があります。

上位2000パッケージのうち、IntlGregorianCalendar::__construct()を3つ以上の引数で呼び出しているものは存在しませんでした。

ldap_connect()

受理。1つめのシグネチャ以外はPHP8.4で非推奨、PHP9.0で削除される。

ldap_connect()には、現在2つのシグネチャが存在します。

function ldap_connect(?string $uri = null, int $port = 389): LDAP\Connection|false {}
 
function ldap_connect(
    ?string $uri,
    int $port,
    string $wallet,
    string $password,
    int $auth_mode
): LDAP\Connection|false {}

本RFCでは、まずPHP8.3において2つ目のシグネチャを使用する関数を追加します。
そして2つ目のシグネチャをPHP8.4で非推奨とし、PHP9で削除します。

PHP9.0
function ldap_connect(?string $uri = null, int $port = 389): LDAP\Connection|false {}

function ldap_connect_wallet(?string $uri, string $wallet, string $password, int $auth_mode): LDAP\Connection|false {}

新しい関数の引数に$portはありません。
これは別途Deprecations for PHP 8.3のRFCで廃止されるためです。

後者の引数を持つldap_connect()は、ldap_connect_wallet()に変更する必要があります。

上位2000パッケージのうち、ldap_connect()を後者の引数で呼び出しているものは存在しませんでした。

ldap_exop()

受理。1つめのシグネチャ以外はPHP8.4で非推奨、PHP9.0で削除される。

ldap_exop()には、現在2つのシグネチャが存在します。

function ldap_exop(
    LDAP\Connection $ldap,
    string $request_oid,
    ?string $request_data = null,
    ?array $controls = null
): LDAP\Result|false {}
 
/**
 * @param string $response_data
 * @param string $response_oid
 */
function ldap_exop(
    LDAP\Connection $ldap,
    string $request_oid,
    ?string $request_data = null,
    ?array $controls = null,
    &$response_data,
    &$response_oid
): bool {}

前者は非同期に実行され、後者は同期で実行されます。

本RFCでは、まずPHP8.3において2つ目のシグネチャを使用する関数を追加します。
そして2つ目のシグネチャをPHP8.4で非推奨とし、PHP9で削除します。

PHP9.0
function ldap_exop(
    LDAP\Connection $ldap,
    string $request_oid,
    ?string $request_data = null,
    ?array $controls = null
): LDAP\Result|false {}

/**
 * @param string $response_data
 * @param string $response_oid
 */
function ldap_exop_sync(
    LDAP\Connection $ldap,
    string $request_oid,
    ?string $request_data = null,
    ?array $controls = null,
    &$response_data = null,
    &$response_oid = null
): bool {}

後者の引数を持つldap_exop()は、ldap_exop_sync()に変更する必要があります。

上位2000パッケージのうち、ldap_exop()を後者の引数で呼び出しているものは存在しませんでした。

pg_fetch_result(), pg_field_prtlen(), and pg_field_is_null()

受理。1つめのシグネチャ以外はPHP8.4で非推奨、PHP9.0で削除される。

pg_fetch_result()pg_field_prtlenpg_field_is_nullには、現在2つのシグネチャが存在します。

function pg_fetch_result(PgSql\Result $result, mixed $field): string|false|null {}
function pg_fetch_result(PgSql\Result $result, int $row, mixed $field): string|false|null {}

function pg_field_prtlen(PgSql\Result $result, string|int $field): int|false {}
function pg_field_prtlen(PgSql\Result $result, int $row, string|int $field): int|false {}

function pg_field_is_null(PgSql\Result $result, string|int $field): int|false {}
function pg_field_is_null(PgSql\Result $result, int $row, string|int $field): int|false {}

本RFCでは、PHP8.3で引数$rowをnullableとし、前者をPHP8.4で非推奨とし、PHP9で削除します。

PHP9.0
function pg_fetch_result(PgSql\Result $result, ?int $row, mixed $field): string|false|null {}

function pg_field_prtlen(PgSql\Result $result, ?int $row, string|int $field): int|false {}

function pg_field_is_null(PgSql\Result $result, ?int $row, string|int $field): int|false {}

前者の引数を持つldap_exop()は、引数にnullを追加する必要があります。

上位2000パッケージのうち、前者の使い方をしているものは1パッケージでした。

Phar::setStub()

受理。1つめのシグネチャ以外はPHP8.3で非推奨、PHP9.0で削除される。

Phar::setStub()には、現在2つのシグネチャが存在します。

class Phar
{
    public function setStub(string $string) {}
    public function setStub(resource $resource, int $length) {}
}

本RFCでは、後者をPHP8.3で非推奨とし、PHP9で削除します。

PHP9.0
class Phar
{
    public function setStub(string $string) {}
}

後者を$phar->setStub(stream_get_contents($resource));と書き替えるのは簡単です。

上位2000パッケージのうち、後者の使い方をしているものは存在しませんでした。

ReflectionMethod::__construct()

受理。1つめのシグネチャ以外はPHP8.4で非推奨、PHP9.0で削除される。

ReflectionMethod::__construct()には、現在2つのシグネチャが存在します。

class ReflectionMethod
{
    public function __construct(object|string $objectOrClass, string $method) {}
    public function __construct(string $classMethod) {}
}

本RFCでは、まずPHP8.3において後者を代替する静的メソッドを追加します。
そして後者のシグネチャをPHP8.4で非推奨とし、PHP9で削除します。

PHP9.0
class ReflectionMethod
{
    public function __construct(object|string $objectOrClass, string $method) {}
    public static function createFromMethodName(string $method): static {}
}

1引数のReflectionMethod::__construct()は書き替える必要があります。

上位2000パッケージのうち、後者の使い方をしているものは10パッケージでした。

ReflectionProperty::setValue()

受理。1つめのシグネチャ以外はPHP8.3で非推奨、PHP9.0で削除される。

ReflectionProperty::setValue()には、現在3つのシグネチャが存在します。

class ReflectionProperty
{
    public function setValue(object $object, mixed $value): void {}
    public function setValue(mixed $unused, mixed $value): void {}
    public function setValue(mixed $value): void {}
}

後ろ2つのシグネチャは、静的プロパティにのみ使用可能です。
引数$unusedは完全に無視されます。

本RFCでは、後者をPHP8.3で引数$objectをnullableとし、後ろ2つのシグネチャを非推奨とします。
後ろ2つのシグネチャはPHP9で削除します。

PHP9.0
class ReflectionProperty
{
    public function setValue(?object $object, mixed $value): void {}
}

インスタンスの場合、引数$objectはオブジェクトでなければなりません。
静的プロパティの場合はオブジェクトもしくはnullとなります。

提案されている代替案は、PHP5.2以降で既に利用可能です。

session_set_save_handler()

受理。1つめのシグネチャ以外はPHP8.4で非推奨、PHP9.0で削除される。

session_set_save_handler()には、現在2つのシグネチャが存在します。

function session_set_save_handler(SessionHandlerInterface $session_handler, bool $register_shutdown = true): bool {}

function session_set_save_handler(
    callable $open,
    callable $close,
    callable $read,
    callable $write,
    callable $destroy,
    callable $gc,
    ?callable $create_sid = null,
    ?callable $validate_sid = null,
    ?callable $update_timestamp = null): bool {}

本RFCでは、後者をPHP8.4で非推奨とし、PHP9で削除します。

PHP9.0
function session_set_save_handler(SessionHandlerInterface $session_handler, bool $register_shutdown = true): bool {}

後者の書き方をしている場合は、SessionHandlerInterfaceに書き替える必要があります。

上位2000パッケージのうち、後者の使い方をしているものは1パッケージでした。

stream_context_set_option()

受理。1つめのシグネチャ以外はPHP8.4で非推奨、PHP9.0で削除される。

stream_context_set_option()には、現在2つのシグネチャが存在します。

/** @param resource $context */
function stream_context_set_option(
    $context,
    string $wrapper,
    string $option_name,
    mixed $value
): true {}
 
/** @param resource $context */
function stream_context_set_option($context, array $options): bool {}

本RFCでは、まずPHP8.3において後者を代替する関数を追加します。
そして後者のシグネチャをPHP8.4で非推奨とし、PHP9で削除します。

PHP9.0
function stream_context_set_option(
    $context,
    string $wrapper,
    string $option_name,
    mixed $value
): true {}
 
/** @param resource $context */
function stream_context_set_options($context, array $options): bool {}

後者の書き方をしている場合は、関数名を書き替える必要があります。

上位2000パッケージのうち、後者の使い方をしているものは1パッケージでした。

Policy about new functions

受理。

今後新しく追加される関数やメソッドは、複数のシグネチャを持ってはいけません。
既存の関数やメソッドに、追加のシグネチャを持たせてはいけません。

array_keys()

却下。変更されない。

array_keys()にはふたつのシグネチャが存在します。
ひとつめは全てのキーを返し、ふたつめは$filter_valueにマッチする項目のキーを返します。

function array_keys(array $array): array {}

function array_keys(array $array, mixed $filter_value, bool $strict = false): array {}

本RFCでは、まずPHP8.3において後者を代替する関数を追加します。
そして後者のシグネチャをPHP8.4で非推奨とし、PHP9で削除します。

function array_keys(array $array): array {}

function array_keys_filter(array $array, mixed $filter_value, bool $strict = false): array {}

Backwards incompatible changes

後方互換性のない変更点。

PHP8.3および8.4で、非推奨の通知が発生することがあります。
これらはPHP9.0もしくはPHP10.0において使用できなくなります。

感想

ということで、今後はシグネチャがひとつになり、どちらの書き方をすれば?となることもなくなります。
また名前付き引数とも相性がいいですね。

ただひとつだけ却下されたarray_keysですが、これはメーリングリストで異議が入ったからだと思われます。
他に比べて影響も大きそうですしね。

ほとんどの変更は、2000パッケージ中で数パッケージ、あるいは皆無といった使用率だとか、そもそも関数自体ほぼ使われてないとかであり、実質的にほぼ影響がないレベルですね。
実際私がこれまで書いてきたコードでも、影響を受ける可能性のある変更ってDatePeriod::__construct()だけだと思います。
一般的には、ほぼ気にするレベルではないと思われます。

3
0
0

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
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?