【PHP8.0】PHPにオブジェクト初期化子が導入される

これまで何度も[塩漬けにされたり](https://qiita.com/rana_kualu/items/98ff480a63ff1ef47153)[却下されたりしていた](https://qiita.com/rana_kualu/items/041724987393696ec926)オブジェクト初期化子ですが、ついにPHP8.0で導入されることになりました。
オブジェクト初期化子が何かというとこれです。

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

これはオブジェクト初期化子でいいのか？

日本語で何と表すのか適切な単語が思いつかなかったのでとりあえずオブジェクト初期化子としておきます。
愚直に訳すと"コンストラクタ引数昇格"ですが、そんな単語は無いうえに[型昇格](https://qiita.com/tokumoto/items/12a8dbcec6c39a703227)と紛らわしいです。
[引数プロパティ宣言](https://www.google.com/search?q=%22%E5%BC%95%E6%95%B0%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E5%AE%A3%E8%A8%80%22)も[パラメータプロパティ宣言](https://www.google.com/search?q=%22%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E5%AE%A3%E8%A8%80%22)もほぼ使われてないし何と表現すればいいのだろう。
きっと誰かが適切な語をプルリクしてくれるはず。

以下は該当のRFC、[PHP RFC: Constructor Property Promotion](https://wiki.php.net/rfc/constructor_promotion)の日本語訳です。

# PHP RFC: Constructor Property Promotion

## Introduction

PHPでは現在のところ、オブジェクトにプロパティを定義するだけでも同じことを複数回書かなければならないため、多くの無駄が必要です。
以下の単純なクラスを考えてみましょう。

```php
class Point {
    public float $x;
    public float $y;
    public float $z;
 
    public function __construct(
        float $x = 0.0,
        float $y = 0.0,
        float $z = 0.0,
    ) {
        $this->x = $x;
        $this->y = $y;
        $this->z = $z;
    }
}
```

プロパティの表記は、1:プロパティの宣言、2:コンストラクタの引数、3:プロパティの代入で3回も繰り返されます。
さらにプロパティの型も2箇所に書かなければなりません。

プロパティ宣言とコンストラクタ以外には何も含まれていないバリューオブジェクトでは特に、多くの重複によって変更が複雑になり、エラーを起こしやすいものとなります。

このRFCでは、プロパティの定義とコンストラクタを組み合わせるショートハンド構文の導入を提案します。

```php:PHP8
class Point {
    public function __construct(
        public float $x = 0.0,
        public float $y = 0.0,
        public float $z = 0.0,
    ) {}
}
```

このショートハンド構文は、前述の例と厳密に同じで、より短く書くことができます。
構文は姉妹言語[Hack](https://docs.hhvm.com/hack/classes/constructors#constructor-parameter-promotion)から採用しています。

## Proposal

コンストラクタの引数に`public/protected/private`何れかが記述されている場合、その引数は"promoteされた引数"とします。
promoteされた引数には、同じ名前のプロパティが追加され、値が割り当てられます。

### Constraints

promoteはabstractではないクラスのコンストラクタでのみ記述可能です。
従って、以下のような構文は使用不能です。

```php
// エラー：コンストラクタではない
function test(private $x) {}

abstract class Test {
    // エラー：abstractなので駄目
    abstract public function __construct(private $x);
}

interface Test {
    // エラー：interfaceも駄目
    public function __construct(private $x);
}
```

一般的でない使い方ですが、トレイトでは使用可能です。

対応する可視性キーワードは`public/protected/private`のみです。

```php
class Test {
    // エラー：varはサポートしてない
    public function __construct(var $prop) {}
}
```

promoteされた引数によるプロパティは、通常のプロパティと全く同じ扱いになります。
特に注意点として、同じプロパティを二度宣言することはできません。

```php
class Test {
    public $prop;
 
    // Error: Redeclaration of property.
    public function __construct(public $prop) {}
}
```

また、プロパティにすることのできない[callable型](https://wiki.php.net/rfc/typed_properties_v2#callable_type)は使用することができません。

```php
class Test {
    // Error: Callable type not supported for properties.
    public function __construct(public callable $callback) {}
}
```

promoteされたプロパティはプロパティ宣言と同義であるため、デフォルトがNULLの場合はNULL許容型を明示しなければなりません。

```php
class Test {
    // Error: Using null default on non-nullable property
    public function __construct(public Type $prop = null) {}
 
    // こっちはOK
    public function __construct(public ?Type $prop = null) {}
}
```

可変長引数をpromoteすることはできません。

```php
class Test {
    // エラー
    public function __construct(public string ...$strings) {}
}
```

理由としては、明示する引数の型(ここではstring)と、実際に渡される引数の型(ここではstringの配列)が異なるからです。
`$strings`プロパティをstringの配列にすることも可能ですが、それではわかりづらくなります。

promoteプロパティと明示的なプロパティ宣言を組み合わせることは可能です。
またpromoteプロパティとpromoteされない引数を同時に渡すことも可能です。

```php
// 正しい
class Test {
    public string $explicitProp;
 
    public function __construct(public int $promotedProp, int $normalArg) {
        $this->explicitProp = (string) $normalArg;
    }
}
```

### Desugaring

promoteプロパティはただのシンタックスシュガーであり、全てのpromoteプロパティに対して以下の変換が適用されます。

```php
// シンタックスシュガー
class Test {
    public function __construct(public Type $prop = DEFAULT) {}
}
 
// こう展開される
class Test {
    public Type $prop;
 
    public function __construct(Type $prop = DEFAULT) {
        $this->prop = $prop;
    }
}
```

自動的に宣言されるプロパティの可視性と型は、promoteプロパティの可視性および型と同じになります。
注目すべき点は、プロパティにデフォルト値は適用されず(つまり、未初期化で始まります)、コンストラクタ引数でのみ指定されるところです。

プロパティ宣言時にもデフォルト値を指定したほうがよいようにも思えますが、将来的にデフォルト値で指定することが望ましくなるであろう理由が存在します。

ひとつめは、プロパティのデフォルト値に任意の式を利用できるようにする拡張の可能性です。

```php
// FROM
class Test {
    public function __construct(public Dependency $prop = new Dependency()) {}
}
 
// TO
class Test {
    public Dependency $prop /* = new Dependency() */;
 
    public function __construct(Dependency $prop = new Dependency()) {
        $this->prop = $prop;
    }
}
```

こうなると、プロパティ宣言時とデフォルト値でオブジェクトを2回構築することとなるため望ましくありません。

また、[新潟アクセス修正子](https://wiki.php.net/rfc/write_once_properties)のルールではプロパティでデフォルト値を宣言すると、コンストラクタで代入することもできなくなります。

promote引数が参照であった場合、プロパティも参照になります。

```php
// FROM
class Test {
    public function __construct(public array &$array) {}
}
 
// TO
class Test {
    public array $array;
 
    public function __construct(array &$array) {
        $this->array =& $array;
    }
}
```

promoteプロパティへの引数の割り当ては、コンストラクタの冒頭で行われます。
従って、コンストラクタ内でも引数とプロパティの両方にアクセスすることが可能です。

```php
// 動作する
class PositivePoint {
    public function __construct(public float $x, public float $y) {
        assert($x >= 0.0);
        assert($y >= 0.0);
    }
}

// こっちも動作する
class PositivePoint {
    public function __construct(public float $x, public float $y) {
        assert($this->x >= 0.0);
        assert($this->y >= 0.0);
    }
}
```


### Reflection

リフレクションおよびその他の解析機構で見ると、シンタックスシュガーを解除した後の状態になります。
すなわち、promoteプロパティは明示的に宣言されたプロパティのように見え、promote引数は通常のコンストラクタ引数のように見える、ということです。

PHPは引数に関するDocコメントを公開していませんが、promoteプロパティのDocコメントも保持されます。

```php
class Test {
    public function __construct(
        /** @SomeAnnotation() */
        public $annotatedProperty
    ) {}
}
 
$rp = new ReflectionProperty(Test::class, 'annotatedProperty');
echo $rp->getDocComment(); // "/** @SomeAnnotation */"
```

この例のように、promoteプロパティではDocコメントベースのアノテーションを使用することができます。

また、2メソッドが追加されます。

・`ReflectionProperty::isPromoted()`は、promoteプロパティであればtrueを返します。
・`ReflectionParameter::isPromoted()`は、promote引数であればtrueを返します。

プロパティがpromoteされたかどうかを気にする場面はほとんど存在しないと思われますが、この情報によって元のコードをより簡単に再構築することができます。


### Inheritance

オブジェクト初期化子は継承することができますが、特に特筆すべきようなことはありません。
abstrautを含む典型的な継承のユースケースを以下に示します。

```php
abstract class Node {
    public function __construct(
        protected Location $startLoc = null,
        protected Location $endLoc = null,
    ) {}
}
 
class ParamNode extends Node {
    public function __construct(
        public string $name,
        public ExprNode $default = null,
        public TypeNode $type = null,
        public bool $byRef = false,
        public bool $variadic = false,
        Location $startLoc = null,
        Location $endLoc = null,
    ) {
        parent::__construct($startLoc, $endLoc);
    }
}
```

ParamNodeクラスでいくつかのpromoteプロパティを宣言し、さらに二つの普通の引数を親コンストラクタに転送しています。
これは以下のように展開されます。

```php
abstract class Node {
    protected Location $startLoc;
    protected Location $endLoc;
 
    public function __construct(
        Location $startLoc = null,
        Location $endLoc = null,
    ) {
        $this->startLoc = $startLoc;
        $this->endLoc = $endLoc;
    }
}
 
class ParamNode extends Node {
    public string $name;
    public ExprNode $default;
    public TypeNode $type;
    public bool $byRef;
    public bool $variadic;
 
    public function __construct(
        string $name,
        ExprNode $default = null,
        TypeNode $type = null,
        bool $byRef = false,
        bool $variadic = false,
        Location $startLoc = null,
        Location $endLoc = null,
    ) {
        $this->name = $name;
        $this->default = $default;
        $this->type = $type;
        $this->byRef = $byRef;
        $this->variadic = $variadic;
        parent::__construct($startLoc, $endLoc);
    }
}
```

プロパティへの代入は、親コンストラクタが呼ばれる前に行われることに注意してください。
これはコーディングスタイルとして一般的ではありませんが、動作に影響が出るようなことはほぼありません。


### Attributes

PHP8では[アトリビュート](https://qiita.com/rana_kualu/items/749c389a275f207a8e58)も導入されるため、相互作用を考慮する必要があります。
アトリビュートは、プロパティと引数の両方で使用することができます。

```php
class Test {
    public function __construct(
        <<ExampleAttribute>>
        public int $prop,
    ) {}
}
```

このコードがどのように解釈されるか決める必要があります。
1. アトリビュートは引数にのみ適用する。
2. アトリビュートはプロパティにのみ適用する。
3. アトリビュートは引数とプロパティの両方に適用する。
4. 曖昧さを避けるためエラーにする


```php
// Option 1: アトリビュートは引数にのみ適用する
class Test {
    public int $prop;
 
    public function __construct(
        <<ExampleAttribute>>
        int $prop,
    ) {}
}
 
// Option 2: アトリビュートはプロパティにのみ適用する
class Test {
    <<ExampleAttribute>>
    public int $prop;
 
    public function __construct(
        int $prop,
    ) {}
}
 
// Option 3: アトリビュートは引数とプロパティの両方に適用する
class Test {
    <<ExampleAttribute>>
    public int $prop;
 
    public function __construct(
        <<ExampleAttribute>>
        int $prop,
    ) {}
}
 
// Option 4: 曖昧さを避けるためエラーにする
```

このRFCでは3番目、つまり引数とプロパティの両方に適用することを提案しています。
これが最も柔軟性の高い方法だからです。

ただし、これは実装に依ると考えています。
PHP8の実装に関わる作業で、アトリビュートをプロパティにのみ配置した方がよいと判明した場合は、そのように変更される場合があります。


### Coding Style Consideration

このセクションではコーディングスタイルの推奨について解説します。
規程ではありません。

promoteプロパティを使用する場合、コンストラクタをクラス最初のメソッドとして、明示的なプロパティ宣言の直後に配置することをお勧めします。
これにより、全ての全てのプロパティが先頭にまとめられ、一目でわかるようになります。
静的メソッドを最初に配置することを要求しているコーディング規約は、コンストラクタを最初に配置するよう規約を調整する必要があります。

promoteプロパティに`@param`アノテーションを使用している場合、ドキュメントツールは`@var`アノテーションも含まれているものとして解釈されるべきです。

```php
// 元のコード
class Point {
    /**
     * Create a 3D point.
     *
     * @param float $x The X coordinate.
     * @param float $y The Y coordinate.
     * @param float $z The Z coordinate.
     */
    public function __construct(
        public float $x = 0.0,
        public float $y = 0.0,
        public float $z = 0.0,
    ) {}
}
 
// こう解釈する
class Point {
    /**
     * @var float $x The X coordinate.
     */
    public float $x;
 
    /**
     * @var float $y The Y coordinate.
     */
    public float $y;
 
    /**
     * @var float $z The Z coordinate.
     */
    public float $z;
 
    /**
     * Create a 3D point.
     *
     * @param float $x The X coordinate.
     * @param float $y The Y coordinate.
     * @param float $z The Z coordinate.
     */
    public function __construct(
        float $x = 0.0,
        float $y = 0.0,
        float $z = 0.0,
    ) {
        $this->x = $x;
        $this->y = $y;
        $this->z = $z;
    }
}
```

最後に、promoteプロパティは、あくまで一般的なケースをカバーするための便利な省略記法であるに過ぎないことに注意してください。
promoteプロパティはいつでも明示的なプロパティに書き換えることができます。
そのため、この変更は下位互換性を壊すことはありません。


## Backward Incompatible Changes

下位互換性のない変更はありません。


## Future Scope

Larryが、この機能と他の機能を組み合わせることによってオブジェクト初期化を改善する方法について、[より深いビジョンを提供しています](https://hive.blog/php/@crell/improving-php-s-object-ergonomics)。


## Prior Art

この機能、あるいは類似した機能は多くの言語でサポートされています。
・[Hack](https://docs.hhvm.com/hack/classes/constructors#constructor-parameter-promotion)
・[TypeScript](https://www.typescriptlang.org/docs/handbook/classes.html#parameter-properties)
・[Kotlin](https://kotlinlang.org/docs/reference/classes.html#constructors)

先行するRFCが存在します。
・[Automatic property initialization](https://wiki.php.net/rfc/automatic_property_initialization)　プロパティ宣言は必要とする、より弱い形です。
・[Constructor Argument Promotion](https://wiki.php.net/rfc/constructor-promotion)　このRFCとほとんど同じです。
・[Code free constructor](https://wiki.php.net/rfc/code_free_constructor)　Kotlinの文法に基づいています。


## Vote

投票期間は2020/05/29まで、2/3+1の賛成が必要です。
このRFCは賛成46反対10で受理されました。


# 感想

プロパティを書くのが格段に楽になりますね。
後から見るときにプロパティが宣言されているのかどうかちょっとわかりにくそうですが、この機能が使えるのはコンストラクタだけなのでそこだけ抑えていれば大丈夫でしょう。
コンストラクタだけではなく任意のメソッドで使えると便利では、と一瞬思ったものの、これを無制限に使えると完全に収拾が付かなくなってしまうので、やはりコンストラクタだけに留めておくのが賢明そうですね。