PHPでクラスのプロパティを定義する際、Getter を定義するのが面倒と感じたことはありませんか?
1ヶ所や2ヶ所程度であれば特に感じないかもしれませんが、これが5ヶ所、10ヶ所件と増えるにつれ同じような記述をする必要があり面倒に感じるかと思います。
この面倒な作業をPHP8から導入されたAttributesを使用し解決するのがこの記事の目的になります。
はじめに
まずは今までのGetterの定義方法から見ていきましょう。以下がその例になります。
いかがでしょうか?
ただプロパティの値を返すだけなのに3行も使用してしまっています。もしプロパティの数が多くほとんどのプロパティにGetterを定義するとしたら、面倒かつクラスが肥大化してしまうことがわかるかと思います。
これを以下のような形でGetterを定義できるように実装していきましょう。
捕捉
上記は簡略化のためにプロパティに直接値を代入していますが、値をコンストラクターで受け取ることの方が多いかと思います。そんな時にはこちらもPHP8で導入された Constructor Promotion を使用することで以下のように簡潔に書くことができるようになります。
もし全てのプロパティにGetterを定義するのであれば以下のようにマジックメソッドの __call を使用し、プロパティが存在した場合には値を返すようにすることも可能です。ただし、この方法ですと特定のプロパティの値は返さないなどの細かな切り分けに都度条件分岐を追加しなければならずコードが肥大化してしまいます。
前置きが長くなりましたがAttributesを使用した方法について順に実装しながら見ていきましょう。
Attributes について
まずはAttributesについて知らないと理解ができないかと思うので簡単に説明していきたいと思います。
他の言語ではデコレーターやアノテーションと呼ばれているものと同じような機能を提供します。簡単に言うとプロパティやメソッドにメタ情報を追加することができます。
そして、そのメタ情報を利用しさまざまな処理を実装することができるようになります。
説明だけだと分かりにくいかと思いますので実際に実装していきましょう。
定義方法
Attributes を定義するには以下のようにクラスに #[Attribute] を付与することで定義可能です。以下の定義方法以外にも引数を利用した方法等もありますので一度ドキュメントを読むことをお勧めします。
使用方法
先ほど定義したAttributesを使用するには以下のようにプロパティに定義することで、 $id に Test という Attributes を付与することが可能になります。
また、Attributes は複数付与したりプロパティだけでなくクラスやメソッドなどにも付与可能です。
Attributes の取得方法
では先ほど id プロパティに付与した Test Attributes を取得してみましょう。以下の例で $reflectionAttributes 変数が空の配列がどうかを判定することで目的のAttributes が付与されているかを確認できます。
以上のことを踏まえて早速Getterを実装していきましょう。
Getter の実装
とはいえ、先ほどまでの内容でどのように実装すれば良いかなんとなくわかったのではないでしょうか?
なのでここでは継承関係の場合やトレイトの場合の挙動についても言及しながら進めていきたいと思います。ただ、結論を言ってしまうとクラスの継承と同様にアクセス修飾子により取得できるかどうかが異なります。
それでは実装にしていきたいと思います。
-
まず Get Attributes を定義します。
-
続いてGetterを定義したいプロパティに Get Attributes を付与します。また確認のため static プロパティと Get Attributesを付与していないプロパティも定義します。
-
最後にマジックメソッドの __call を使用し id メソッドにアクセスがあった場合に id プロパティの値を返すようにします。
確認
では確認してみましょう。
プロパティが存在しGet Attributes が付与されている場合は値が取得でき、Get Attributes が付与されていない、もしくは存在しないプロパティにアクセスした場合は NULL が返されることが確認できるかと思います。
まずは自身のプロパティを取得可能な Getter の実装を行いました。ですが先ほどの実装では継承した場合にエラーが発生してしまいますのでそこを修正していきましょう。
継承・トレイトを使用した場合の挙動
継承した場合、子クラスから親クラスのプロパティへアクセスするには通常のクラスの継承と同様 private なプロパティにはアクセスできず、protected もしくは public なプロパティにはアクセスできることが以下の例からわかります。
また、トレイトの場合は親クラス、子クラスどちらに use するかで挙動が異なります。子クラスに use した場合は全てのプロパティにアクセスでき、親クラスに use した場合は private プロパティにはアクセスできません。
親クラスの private なプロパティにもアクセス可能な Getter の定義
以上のことを踏まえ親クラスの private プロパティにアクセス可能な Getter を定義していきましょう。
まずはプロパティの存在チェックのヵ所を修正していきたいと思います。なので元々の実装を確認してみましょう。
単純にプロパティが存在するかをチェックし、存在しない場合は null を返すようになっています。これを自身のクラスにはプロパティが存在せず、親クラスが存在しかつ、 __call メソッドが実装されている場合のみ親クラスに処理を委譲するように修正します。
また、自身のクラスにはプロパティが存在せず、親クラスが存在しないか、親クラスに __call メソッドが定義されていない場合は null を返すようになっています。
ですが存在しないメソッドにアクセスして null が返るのは、プロパティの値が null なのか判断ができないですし、未定義のメソッドをコールした場合は元々 Error が発生するのでこれを BadMethodCallException を発生させるよう修正します。
次に同様の理由から Get Attribute が付与されていないプロパティの Getter にアクセスした場合も BadMethodCallException を発生させるよう変更します。
以上で修正が完了しました。あとは先ほどの修正を反映したコードを使い回しがしやすようトレイトにして完了になります。以下がそのコードになります。
ただしプロパティ名と同名のメソッドが既に定義されていた場合はそちらが優先されるのと、子クラスでオーバライドした場合 Get Attribute を付与しないと BadMethodCallException が発生するのでご注意ください。
使用方法は、先ほど定義した HasGetter トレイトを Get Attribute を使用したいクラスで use することで使用可能になります。もし親クラスで HasGetter を use していたとしても子クラスのプロパティは取得できないため、子クラスでも use する必要があります。
また、Get Attribute を使用しないクラスでは特に HasGetter を use する必要はありません。
最後に
いかがでしょうか?
Getter を定義したいクラスに毎回 HasGetter を use する必要はありますがプロパティ数が多い場合にはその効果を実感できるのではないでしょうか。
もしバグやもっといい方法がありましたらぜひ、以下のお問い合わせよりお知らせいただけましたら幸いです。