diff --git a/language/constants.xml b/language/constants.xml index f1c72d632b..74d0303045 100644 --- a/language/constants.xml +++ b/language/constants.xml @@ -1,6 +1,6 @@ - + @@ -35,7 +35,6 @@ この場合の定数の値は constant 関数によってのみ取得できますが、 このような使い方は推奨できません。 - &tip.userlandnaming; @@ -69,6 +68,7 @@ define("__FOO__", "something"); (0x80-0xff)のASCII文字を指します。 + &link.superglobals;と同様に定数のスコープはグローバルです。 つまり、スコープによらずスクリプトの中ではどこでも定数に @@ -262,7 +262,7 @@ echo ANIMALS[1]; // 出力は "cat" マジック定数 - 使われる場所によって値が変化する定数(マジック定数)が 9 つあります。 + 使われる場所によって値が変化する定数(マジック定数)がいくつかあります。 例えば、__LINE__はスクリプト上において 使われる行によって値が変化します。 これらの @@ -315,7 +315,7 @@ echo ANIMALS[1]; // 出力は "cat" クラス名。 クラス名には、そのクラスが宣言されている名前空間も含みます (例 Foo\Bar)。 - トレイトのメソッド内で __CLASS__ を使うと、 + トレイトのメソッド内で __CLASS__ を使うと、 そのトレイトを use しているクラスの名前を返します。 @@ -333,6 +333,14 @@ echo ANIMALS[1]; // 出力は "cat" クラスのメソッド名。 + + __PROPERTY__ + + プロパティ名。 + プロパティフック + の内部でのみ使用可能です。 + + __NAMESPACE__ diff --git a/language/oop5.xml b/language/oop5.xml index a135ea2b3e..e7c8987919 100644 --- a/language/oop5.xml +++ b/language/oop5.xml @@ -1,5 +1,5 @@ - + @@ -29,6 +29,7 @@ &language.oop5.basic; &language.oop5.properties; + &language.oop5.property-hooks; &language.oop5.constants; &language.oop5.autoload; &language.oop5.decon; diff --git a/language/oop5/abstract.xml b/language/oop5/abstract.xml index 85a031eaba..844693c2a2 100644 --- a/language/oop5/abstract.xml +++ b/language/oop5/abstract.xml @@ -1,17 +1,19 @@ - + クラスの抽象化 - PHP には、抽象クラスと抽象メソッドの機能があります。 - abstract として定義されたクラスのインスタンスを生成することはできません。 - 1つ以上の抽象メソッドを含む全てのクラスもまた抽象クラスとなります。 - abstract として定義されたメソッドは、そのメソッドのシグネチャを宣言するのみで、 - 実装を定義することはできません。 + PHP には、抽象クラス、抽象メソッド、抽象プロパティがあります。 + abstract として定義された抽象クラスのインスタンスを生成することはできず、 + 1つ以上の抽象メソッドや抽象プロパティを含むクラスは抽象クラスでなければいけません。 + abstract として定義されたメソッドは、そのメソッドのシグネチャと public または protected のアクセス権を宣言するのみで、 + 実装を定義することはできません。抽象プロパティは、 + getset の要件を宣言することができ、 + 実装はどちらか一方に対してのみ行えます。両方同時に実装することはできません。 @@ -21,11 +23,22 @@ シグネチャの互換性に関するルール に従わなければいけません。 + + PHP 8.4 から、抽象クラスは public または protected の抽象プロパティを宣言できるようになりました。 + protected な抽象プロパティは、protected または public のスコープから読み書き可能なプロパティにより + 要件が満たされます。 + + + 抽象プロパティは、通常のプロパティによって、または + 必要な操作に対応した フック を定義したプロパティにより要件が満たされます。 + + - 抽象クラスの例 + 抽象メソッドの例 getValue() . "\n"; } } class ConcreteClass1 extends AbstractClass { - protected function getValue() { + protected function getValue() + { return "ConcreteClass1"; } - public function prefixValue($prefix) { + public function prefixValue($prefix) + { return "{$prefix}ConcreteClass1"; } } class ConcreteClass2 extends AbstractClass { - public function getValue() { + public function getValue() + { return "ConcreteClass2"; } - public function prefixValue($prefix) { + public function prefixValue($prefix) + { return "{$prefix}ConcreteClass2"; } } -$class1 = new ConcreteClass1; +$class1 = new ConcreteClass1(); $class1->printOut(); -echo $class1->prefixValue('FOO_') ."\n"; +echo $class1->prefixValue('FOO_'), "\n"; -$class2 = new ConcreteClass2; +$class2 = new ConcreteClass2(); $class2->printOut(); -echo $class2->prefixValue('FOO_') ."\n"; +echo $class2->prefixValue('FOO_'), "\n"; + ?> ]]> @@ -82,22 +101,22 @@ FOO_ConcreteClass2 - 抽象クラスの例 + 抽象メソッドの例 prefixName("Pacman"), "\n"; echo $class->prefixName("Pacwoman"), "\n"; + ?> ]]> @@ -123,9 +144,81 @@ Mrs. Pacwoman ]]> + + 抽象プロパティの例 + + +abstract class A +{ + // 継承するクラスは、public に読み取り可能なプロパティを持たなければなりません + abstract public string $readable { + get; + } + + // 継承するクラスは、protected または public に書き込み可能なプロパティを持たなければなりません + abstract protected string $writeable { + set; + } + + // 継承するクラスは、protected または public で読み書き可能なプロパティを持たなければなりません + abstract protected string $both { + get; + set; + } +} + +class C extends A +{ + // 要件を満たし、さらに書き込みも可能にしているため有効です + public string $readable; + + // public に読み取り可能でないため、要件を満たしません + protected string $readable; + // 要件を正確に満たしているため有効です + // protected のスコープからのみ書き込みが可能です + protected string $writeable { + set => $value; + } + + // protected から public にアクセス権を拡張しており、問題ありません + public string $both; +} + +?> +]]> + + + + 抽象プロパティにはフックを実装できます。 + 前の例のように、get または set のどちらかを、定義せず宣言のみ行います。 + + + 抽象プロパティの例 + +foo = $value; + } + } +} + +?> +]]> + + + - + オブジェクト インターフェイス - オブジェクト インターフェイスを使うと、 - メソッドの実装を定義せずに、 - クラスが実装する必要があるメソッドを指定するコードを作成できます。 - インターフェイス は クラス や トレイト と名前空間を共有するので、 + オブジェクト インターフェースでは、クラスが実装すべきメソッドやプロパティの + 宣言だけを行うコードを作成できます。ここでは具体的な実装は必要ありません。 + インターフェイスはクラス、トレイト、列挙型と名前空間を共有するので、 それらと同じ名前を使ってはいけません。 @@ -117,10 +116,94 @@ そのインターフェイスを継承したクラスやインターフェイスから定数をオーバーライドすることができませんでした。 + + プロパティ + + PHP 8.4.0 から、インターフェイスはプロパティを宣言できるようになりました。 + 宣言する場合、そのプロパティの読み取り可否、書き込み可否、あるいは双方を + 指定する必要があります。 + インターフェイスによる宣言は、public な読み書きに対してのみ適用できます。 + + + クラス側でインターフェイスのプロパティ要件を満たすには、 + public な通常のプロパティを定義する、 + 対応するフックを実装する public な + 仮想プロパティ + を定義するなど、複数の方法があります。 + 読み取り用プロパティは readonly プロパティによって満たすこともできます。 + ただし、インターフェイスで「書き込み可能」と宣言されているプロパティを readonly にすることはできません。 + + + インターフェイスのでのプロパティ宣言の例 + + strtoupper($this->writeable); } + + // インターフェイスはこのプロパティが「書き込み可能」であることのみ要求しています。 + // ここに get の動作も含めることは全く問題ありません。 + // この例では仮想プロパティを作成しています。 + public string $writeable { + get => $this->written; + set { + $this->written = $value; + } + } + + // このプロパティは「public に読み書き可能」でなければならないため、 + // get と set の両方を実装する必要があります。 + // デフォルトの動作のままでも問題ありません。 + public string $both { + get => $this->all; + set { + $this->all = strtoupper($value); + } + } +} +?> +]]> + + + &reftitle.examples; - Interface の例 + インターフェイスの例 + + + + プロパティフック + + + プロパティフックは、他の言語では「プロパティアクセサー」とも呼ばれる仕組みで、 + プロパティの読み書き動作へ干渉し、それを上書きする機能を提供します。 + この機能には、次の2つの目的があります: + + + + + get- メソッドや set- メソッドを使わずプロパティを参照しつつ、 + 将来的に追加の機能を追加する余地を残す。 + フックを使わない場合の定型の get/set メソッドのほとんどは + 不要になる。 + + + + + 実際に値を保持することなく、 + オブジェクトを説明するプロパティを実装する。 + + + + + 非静的なプロパティには getset の2種類のフックがあります。 + それぞれ、プロパティの読み取りと書き込みの動作を上書きします。 + フックは、型付きプロパティ、型のないプロパティ、いずれにも利用可能です。 + + + プロパティはバックドプロパティか仮想プロパティのどちらかになります。 + バックドプロパティは実体の値を持つプロパティです。 + フックを持たないプロパティは、全てバックドプロパティになります。 + 仮想プロパティは、実体の値を必要としないフックだけを持つプロパティです。 + この場合、フックは実質的にメソッドと等価であり、 + オブジェクトはこのプロパティ用のメモリを必要としません。 + + + プロパティフックは readonly プロパティと同時に使えません。 + getset の動作の変更だけでなく + アクセス自体の制限も行いたい場合、 + 非対称可視性プロパティ を使ってください。 + + + 基本的なフック構文 + + フックを宣言する一般的な構文は次のとおりです。 + + + プロパティフック (フルバージョン) + +modified) { + return $this->foo . ' (modified)'; + } + return $this->foo; + } + set(string $value) { + $this->foo = strtolower($value); + $this->modified = true; + } + } +} + +$example = new Example(); +$example->foo = 'changed'; +print $example->foo; +?> +]]> + + + + $foo プロパティの末尾はセミコロンではなく {} です。 + これがフックの存在を表します。 + ここでは getset 両方のフックを定義していますが、 + 一方だけ定義することも可能です。 + どちらのフックも {} の中に任意のコードを書くことができます。 + + + set フックは、渡される値の型と名前を + メソッドと同じ書式で指定できます。 + この型はプロパティの型と同じか、 + 反変 (より広い型) でなければなりません。 + たとえば string 型のプロパティに対しては、 + stringStringable を受け取る set フックを定義できますが、 + array のみを受け取るものは定義できません。 + + + 少なくとも一方のフックが $this->foo(プロパティ自体)を参照しているため、 + このプロパティはバックドプロパティになります。 + $example->foo = 'changed' が呼び出されると、 + 渡された文字列は小文字に変換され、それが値として保存されます。 + プロパティを読み取る際には、保存された値に対し、条件に応じて追加の文字列が + 付与されます。 + + + よくあるケースに対応するために、いくつかの短縮構文が用意されています。 + + + get フックが単独の式である場合、 + {} を省略してアロー式で置き換えることができます。 + + + プロパティの get 式 + + この例は前の例と等価です。 + + + $this->foo . ($this->modified ? ' (modified)' : ''); + + set(string $value) { + $this->foo = strtolower($value); + $this->modified = true; + } + } +} +?> +]]> + + + + set フックのパラメータの型がプロパティの型と同じ場合 (典型的にはそうなります)、 + その型は省略可能です。この場合、渡される値には自動的に $value という名前が付きます。 + + + プロパティ set のデフォルト + + この例は前の例と等価です。 + + + $this->foo . ($this->modified ? ' (modified)' : ''); + + set { + $this->foo = strtolower($value); + $this->modified = true; + } + } +} +?> +]]> + + + + set フックが、渡された値を変形して保存するだけの場合には、 + アロー式でより簡略にできます。 + 式の評価結果が値として保存されます。 + + + プロパティの set 式 + + $this->foo . ($this->modified ? ' (modified)' : ''); + set => strtolower($value); + } +} +?> +]]> + + + + この例は $this->modified の値を更新していないため + 前の例と等価ではありません。 + set フック内で複数の文が必要な場合、中括弧付きの構文を使用してください。 + + + プロパティは状況に応じて、一方または双方のフックを実装するか、どちらも実装しないことができます。 + それぞれの短縮構文は独立しており、 + たとえば「短縮構文の get と 通常の set」 + 「型を明示した短縮構文の set」など、いずれも有効です。 + + + バックドプロパティで getset のフックを省略した場合、 + それはデフォルトの読み書き動作になります。 + + + + フックは + コンストラクタのプロモーション + にも定義できます。ただし、コンストラクタで受け取る値は + set フックが受け入れる型ではなく、 + プロパティの本来の型に一致しなければならない点に注意してください。 + + + 次のような例を考えます: + + +created = $value; + } + }, + ) { + } +} +]]> + + + 内部では次のような形で処理されます: + + +created = $value; + } + } + + public function __construct( + DateTimeInterface $created, + ) { + $this->created = $created; + } +} +]]> + + + コンストラクタ以外からプロパティに値を書き込む時は string か + DateTimeInterface を受け入れますが、 + コンストラクタからは DateTimeInterface のみ受け入れます。 + プロパティの型 (DateTimeInterface) が + コンストラクタのシグネチャのパラメータ型として使われ、 + set フックが受け入れる型は参照されないためです。 + + + このような振る舞いがコンストラクタでも必要な場合、 + コンストラクタのプロモーションは使用できません。 + + + + + 仮想プロパティ + + 仮想プロパティは、値を保持しないプロパティです。 + getset いずれのフックも + プロパティ自体を正確に参照していない場合、それは仮想プロパティになります。 + 例えば、$foo という名前のプロパティのフックに $this->foo というコード含まれれば、それはバックドプロパティです。 + 次のプロパティはバックドプロパティではなく、エラーが発生します: + + + 無効な仮想プロパティ + +$temp; // $this->foo を正確に参照していません + } + } +} +?> +]]> + + + + 仮想プロパティでは、フックを省略するとその操作は存在しないとみなされ、 + 使おうとするとエラーが発生します。 + 仮想プロパティはオブジェクト内でメモリを消費しません。 + 仮想プロパティは、例えば他の2つのプロパティを組み合わせて値を作るような + 「派生」プロパティに向いています。 + + + 仮想プロパティ + + $this->h * $this->w; + } + + public function __construct(public int $h, public int $w) {} +} + +$s = new Rectangle(4, 5); +print $s->area; // 20 と表示 +$s->area = 30; // set が定義されていないためエラー +?> +]]> + + + + 仮想プロパティに対して getset 両方のフックを定義することもできます。 + + + + スコープ + + 全てのフックは、オブジェクトスコープで動作します。 + すなわち、全ての public、private、protected な + メソッドやプロパティに対してアクセスできます。 + フックを持つプロパティへアクセスする場合も同様です。 + フック内で別のプロパティにアクセスしても、そのフックがバイパスされることはありません。 + + + 重要なのは、フック内で必要に応じて、 + 任意の複雑なメソッドを呼び出すこともできるという点です。 + + + フックからメソッド呼び出し + + $this->sanitizePhone($value); + } + + private function sanitizePhone(string $value): string { + $value = ltrim($value, '+'); + $value = ltrim($value, '1'); + + if (!preg_match('/\d\d\d\-\d\d\d\-\d\d\d\d/', $value)) { + throw new \InvalidArgumentException(); + } + return $value; + } +} +?> +]]> + + + + + リファレンス + + フックはプロパティの読み書きの操作へ干渉するため、 + プロパティへのリファレンスを取得したり、 + $this->arrayProp['key'] = 'value'; のような + 間接的な変更操作を行う場合に問題が発生します。 + リファレンスによる値の変更が set フックをバイパスしてしまうからです。 + + + プロパティからフックを経由してリファレンスを取得する必要がある場合、 + get フックの先頭に & + を付与します。 + 同じプロパティで get&get を両方定義すると + 構文エラーになります。 + + + バックドプロパティでは &get フックと set フックは同時に定義できません。 + 前述のとおり、リファレンスへの書き込みは set フックをバイパスしてしまうためです。 + 仮想プロパティでは、両フックから共有される実体の値がないため、同時に定義しても問題ありません。 + + + 配列プロパティのインデックスへ書き込みを行う場合も、暗黙的にリファレンスが使われます。 + 従って、フックを伴う配列のバックドプロパティの要素の書き換えができるのは、 + &get フックだけが定義されている場合に限ります。 + 仮想プロパティの場合、 + get&get から返された配列の書き換え自体は可能ですが、 + それが実際にオブジェクトに反映されるかはフックの実装次第です。 + + + 配列全体を上書きする場合は問題ありません。他のプロパティと同様に扱われます。 + 配列の個々の要素を操作する場合のみ注意が必要です。 + + + + 継承 + + final フック + + フックは final としても宣言することもできます。 + その場合はオーバーライドできなくなります。 + + + final フックの例 + + strtolower($value); + } +} + +class Manager extends User +{ + public string $username { + // これは許可される + get => strtoupper($this->username); + + // parentのsetがfinalのため許可されない。 + set => strtoupper($value); + } +} +?> +]]> + + + + プロパティ自体を final と宣言することもできます。 + final で宣言されたプロパティは子クラスで再宣言できず、 + フックを変更したりアクセス権を広げることもできません。 + + + final と宣言したプロパティに対しフックも final とするのは + 単に冗長であり無視されます。 + これは final メソッドと同じ動作です。 + + + 子クラスでは、オーバーライドしたいフックを再定義することで、 + フックを個別に上書きできます。 + フックを持たないプロパティにフックを追加することもできます。 + フックがメソッドのように振る舞うという点で、一貫した動作です。 + + + フックの継承 + +x = $value; + } + } +} +?> +]]> + + + + それぞれのフックは親の実装を個別にオーバーライドします。 + 子クラスがフックを追加する場合、親クラスのプロパティで設定されたデフォルト値は削除され、再宣言が必要です。 + これはフックのないプロパティを継承する場合と同じ動作です。 + + + + 親フックへのアクセス + + 子クラスのフックから parent::$prop に続き目的のフックを指定することで、 + 親クラスのプロパティにアクセスできます。 + 例えば parent::$propName::get() は、 + 「親クラスに定義された prop の get 操作を実行する」 + という意味になります。同様に set 操作も実行できます。 + + + これらの方法でアクセスしない限り、親クラスのフックは無視されます。 + これはメソッドの動作と同じです。 + この方法で親クラスの記憶領域にアクセスできます。 + 親クラスのプロパティにフックが存在しない場合、 + デフォルトの get/set 動作が使われます。 + フックは、自分自身のプロパティにおける親フック以外は呼び出せません。 + + + 上記の例をより効率的に書くと、以下のようになります。 + + + 親フックへのアクセス (set) + +x = $value; + } + } +} +?> +]]> + + + + get フックだけをオーバーライドする場合、次の例のようになります: + + + 親フックへのアクセス (get) + + $this->uppercase + ? strtoupper(parent::$val::get()) + : strtolower(parent::$val::get()); + } +} +?> +]]> + + + + + + シリアライズ + + PHP には、オブジェクトを外部で利用したり + デバッグしたりするための、いくつかのシリアライズ手段があります。 + その際のフックの挙動は、用途によって異なります。 + あるケースでは、プロパティに保存された生の値が使われ、 + フックはバイパスされます。 + 別のケースでは、通常の読み書きと同じように + フックを通して処理されます。 + + + var_dump: 生の値を使用 + serialize: 生の値を使用 + unserialize: 生の値を使用 + __serialize()/__unserialize(): カスタムロジックと get/set フック + 配列キャスト: 生の値を使用 + var_export: get フックを使用 + json_encode: get フックを使用 + JsonSerializable: カスタムロジックと get フック + get_object_vars: get フックを使用 + get_mangled_object_vars: 生の値を使用 + + + + diff --git a/language/oop5/variance.xml b/language/oop5/variance.xml index 56660deeb8..a181e64de4 100644 --- a/language/oop5/variance.xml +++ b/language/oop5/variance.xml @@ -1,6 +1,6 @@ - + 共変性と反変性 @@ -12,7 +12,7 @@ 共変性とは、子クラスのメソッドが、親クラスの戻り値よりも、より特定の、狭い型を返すことを許すことです。 - 一方で、反変性とは、親クラスのものよりも、より抽象的な、広い型を引数に指定することを許すものです。 + 反変性とは、親クラスのものよりも、より抽象的な、広い型を引数に指定することを許すものです。 @@ -247,6 +247,61 @@ Fatal error: Uncaught TypeError: Argument 1 passed to Animal::eat() must be an i + + プロパティの共変性と反変性(変性) + + デフォルトでは、プロパティは共変でも反変でもなく不変です。 + つまり、子クラスでは型は変更できません。 + 「get」操作は共変でなければならず、 + 「set」操作は反変でなければならないことが理由です。 + 双方を同時に満たすには、プロパティは不変である必要があります。 + + + PHP 8.4.0 から、インターフェイスや抽象クラスでの抽象プロパティや、 + 仮想プロパティ が追加されたことにより、 + プロパティが「get」または「set」だけを持つことを宣言できるようになりました。 + つまり、「get」操作だけが必要な抽象プロパティや仮想プロパティは共変性を持ちます。 + 同様に、「set」操作だけが必要な抽象プロパティや仮想プロパティは反変性を持ちます。 + + + ただし、いったんプロパティが「get」と「set」操作の両方を持つようになると、 + それ以上の拡張において共変あるいは反変にはなりません。 + その時点で不変となります。 + + + プロパティの型の変性 + + +]]> + + +