Produced by Fourier

PHP8で面倒な Getter 定義から卒業しよう

Ohashi Ohashi カレンダーアイコン 2022.04.01

PHPでクラスのプロパティを定義する際、Getter を定義するのが面倒と感じたことはありませんか?

1ヶ所や2ヶ所程度であれば特に感じないかもしれませんが、これが5ヶ所、10ヶ所件と増えるにつれ同じような記述をする必要があり面倒に感じるかと思います。

この面倒な作業をPHP8から導入されたAttributesを使用し解決するのがこの記事の目的になります。

はじめに

まずは今までのGetterの定義方法から見ていきましょう。以下がその例になります。

class Sample
{
    private int $id = 1;

    public function id(): int
    {
        return $this->id;
    }
}

いかがでしょうか?

ただプロパティの値を返すだけなのに3行も使用してしまっています。もしプロパティの数が多くほとんどのプロパティにGetterを定義するとしたら、面倒かつクラスが肥大化してしまうことがわかるかと思います。

これを以下のような形でGetterを定義できるように実装していきましょう。

class Sample
{
    #[Get] private int $id = 1;
}

捕捉

上記は簡略化のためにプロパティに直接値を代入していますが、値をコンストラクターで受け取ることの方が多いかと思います。そんな時にはこちらもPHP8で導入された Constructor Promotion を使用することで以下のように簡潔に書くことができるようになります。

class Sample
{
        public function __construct(
                #[Get] private int $id,
        ) {}
}

もし全てのプロパティにGetterを定義するのであれば以下のようにマジックメソッドの __call を使用し、プロパティが存在した場合には値を返すようにすることも可能です。ただし、この方法ですと特定のプロパティの値は返さないなどの細かな切り分けに都度条件分岐を追加しなければならずコードが肥大化してしまいます。

class Sample
{
        public function __construct(
                private int $id,
        ) {}

        public function __call($name, $arguments)
    {
        if(property_exists(self::cla, $name)) {
                    // $name が id の場合 id の値を返す
                    return $this->{$name}
                }
    }
}

前置きが長くなりましたがAttributesを使用した方法について順に実装しながら見ていきましょう。

Attributes について

まずはAttributesについて知らないと理解ができないかと思うので簡単に説明していきたいと思います。

他の言語ではデコレーターやアノテーションと呼ばれているものと同じような機能を提供します。簡単に言うとプロパティやメソッドにメタ情報を追加することができます。

そして、そのメタ情報を利用しさまざまな処理を実装することができるようになります。

説明だけだと分かりにくいかと思いますので実際に実装していきましょう。

定義方法

Attributes を定義するには以下のようにクラスに #[Attribute] を付与することで定義可能です。以下の定義方法以外にも引数を利用した方法等もありますので一度ドキュメントを読むことをお勧めします。

#[Attribute]
class Test {}

使用方法

先ほど定義したAttributesを使用するには以下のようにプロパティに定義することで、 $id に Test という Attributes を付与することが可能になります。

また、Attributes は複数付与したりプロパティだけでなくクラスやメソッドなどにも付与可能です。

class Sample
{
    #[Test] private int $id = 1;
}

Attributes の取得方法

では先ほど id プロパティに付与した Test Attributes を取得してみましょう。以下の例で $reflectionAttributes 変数が空の配列がどうかを判定することで目的のAttributes が付与されているかを確認できます。

$sample = new Sample;

// プロパティが存在しない場合は ReflectionException がスローされる
$reflectionProperty = new ReflectionProperty($sample, 'id');

// getAttributes の第一引数に取得したい Attributes クラスを指定することで
// 指定した Attributes が付与されていた場合は配列のインデックス 0 で取得でき
// 付与されていなかった場合は空の配列が返る
// また、引数を指定しない場合は全てのAttributesが取得可能
// @var ReflectionAttributes[]
$reflectionAttributes = $reflectionProperty->getAttributes(Test::class);

if (!empty($reflectionAttributes)) {
    // Test Attributes が付与されている場合の処理を記述
}

以上のことを踏まえて早速Getterを実装していきましょう。

Getter の実装

とはいえ、先ほどまでの内容でどのように実装すれば良いかなんとなくわかったのではないでしょうか?

なのでここでは継承関係の場合やトレイトの場合の挙動についても言及しながら進めていきたいと思います。ただ、結論を言ってしまうとクラスの継承と同様にアクセス修飾子により取得できるかどうかが異なります。

それでは実装にしていきたいと思います。

  1. まず Get Attributes を定義します。
    #[Attribute]
    class Get {}
  2. 続いてGetterを定義したいプロパティに Get Attributes を付与します。また確認のため static プロパティと Get Attributesを付与していないプロパティも定義します。
    1.	
    class GetterSample
    {
    		#[Get] private int $id = 1;
    
    		#[Get] private static string $staticProp = 'Static prop';
    
    		private string $noGetter = 'No getter';
    }
  3. 最後にマジックメソッドの __call を使用し id メソッドにアクセスがあった場合に id プロパティの値を返すようにします。
    // GetterSample クラスに以下を追加
    
    public function __call(string $name, array $arguments)
    {
        if (!property_exists(self::class, $name)) {
    				// プロパティが存在しない場合にReflectionProperty をインスタンス化すると 
    				// ReflectionException がスローされるが、サンプルの挙動を分かりやすくするため
    				// 存在しない場合は事前に null を返すようにしている
            return null;
        }
        
    		$reflectionProperty = new ReflectionProperty(self::class, $name);
    		$reflectionAttributes = $reflectionProperty->getAttributes(Get::class);
    		if (empty($reflectionAttributes)) { 
    		    // Get Attributes が設定されてたいなかった場合で、こちらもプロパティが存在しない場合と同様
    				// サンプルの挙動をわかりやすくするためエラーではなく null を返すようにしている
    		    return null; 
    		}
    	
    		// static プロパティとそうでないプロパティではアクセス方法が違う
    		return $reflectionProperty->isStatic() 
    		    ? $this::${$name} 
    		    : $this->{$name};
    }

確認

では確認してみましょう。

$sample = new GetterSample();
var_dump($sample->id());                // int(1)
var_dump($sample->staticProp());        // string(11) "Static prop"
var_dump($sample->noGetter());          // NULL
var_dump($sample->undefinedProperty()); // NULL

プロパティが存在しGet Attributes が付与されている場合は値が取得でき、Get Attributes が付与されていない、もしくは存在しないプロパティにアクセスした場合は NULL が返されることが確認できるかと思います。

まずは自身のプロパティを取得可能な Getter の実装を行いました。ですが先ほどの実装では継承した場合にエラーが発生してしまいますのでそこを修正していきましょう。

継承・トレイトを使用した場合の挙動

継承した場合、子クラスから親クラスのプロパティへアクセスするには通常のクラスの継承と同様 private なプロパティにはアクセスできず、protected もしくは public なプロパティにはアクセスできることが以下の例からわかります。

class ParentClass
{
		#[Get] private string $parentPrivateProp     = 'Parent private prop';
		#[Get] protected string $parentProtectedProp = 'Parent protected prop';
		#[Get] public string $parentPublicProp       = 'Parent public prop';
}

class ChildClass extends ParentClass
{
		#[Get] private string $childPrivateProp     = 'Child private prop';
		#[Get] protected string $childProtectedProp = 'Child protected prop';
		#[Get] public string $childPublicProp       = 'Child public prop';

		public function __call(string $name, array $arguments)
		{
		    // 省略
		}
}


$child = new ChildClass();

var_dump($child->childPrivateProp());   // string(18) "Child private prop"
var_dump($child->childProtectedProp()); // string(20) "Child protected prop"
var_dump($child->childPublicProp());    // string(17) "Child public prop"

var_dump($child->parentPrivateProp());   // NULL
var_dump($child->parentProtectedProp()); // string(21) "Parent protected prop"
var_dump($child->parentPublicProp());    // string(18) "Parent public prop"

また、トレイトの場合は親クラス、子クラスどちらに use するかで挙動が異なります。子クラスに use した場合は全てのプロパティにアクセスでき、親クラスに use した場合は private プロパティにはアクセスできません。

trait ParantTrait
{
		#[Get] private string $parentPrivateProp     = 'Parent private prop';
		#[Get] protected string $parentProtectedProp = 'Parent protected prop';
		#[Get] public string $parentPublicProp       = 'Parent public prop';
}

class ParentClass
{
		use ParantTrait;
}

trait ChildTrait
{
		#[Get] private string $childPrivateProp     = 'Child private prop';
		#[Get] protected string $childProtectedProp = 'Child protected prop';
		#[Get] public string $childPublicProp       = 'Child public prop';
}

class ChildClass extends ParentClass
{
		use ChildTrait;

		public function __call(string $name, array $arguments)
		{
		    // 省略
		}
}


$child = new ChildClass();

var_dump($child->childPrivateProp());   // string(18) "Child private prop"
var_dump($child->childProtectedProp()); // string(20) "Child protected prop"
var_dump($child->childPublicProp());    // string(17) "Child public prop"

var_dump($child->parentPrivateProp());   // NULL
var_dump($child->parentProtectedProp()); // string(21) "Parent protected prop"
var_dump($child->parentPublicProp());    // string(18) "Parent public prop"

親クラスの private なプロパティにもアクセス可能な Getter の定義

以上のことを踏まえ親クラスの private プロパティにアクセス可能な Getter を定義していきましょう。

まずはプロパティの存在チェックのヵ所を修正していきたいと思います。なので元々の実装を確認してみましょう。

if (!property_exists(self::class, $name)) {
        return null;
    }

単純にプロパティが存在するかをチェックし、存在しない場合は null を返すようになっています。これを自身のクラスにはプロパティが存在せず、親クラスが存在しかつ、 __call メソッドが実装されている場合のみ親クラスに処理を委譲するように修正します。

if (!property_exists(self::class, $name)) 
    if(get_parent_class() !== false && method_exists(Parent::class, '__call')) {
        // 親クラスが存在し、__callメソッドが定義されている場合
        return parent::__call($name, $arguments);
    }
  
		return null;
}

また、自身のクラスにはプロパティが存在せず、親クラスが存在しないか、親クラスに __call メソッドが定義されていない場合は null を返すようになっています。

ですが存在しないメソッドにアクセスして null が返るのは、プロパティの値が null なのか判断ができないですし、未定義のメソッドをコールした場合は元々 Error が発生するのでこれを BadMethodCallException を発生させるよう修正します。

if (!property_exists(self::class, $name)) {
    if(get_parent_class() !== false && method_exists(Parent::class, '__call')) {
        // 親クラスが存在し、__callメソッドが定義されている場合
        return parent::__call($name, $arguments);
    }

		throw new BadMethodCallException();
}

次に同様の理由から Get Attribute が付与されていないプロパティの Getter にアクセスした場合も BadMethodCallException を発生させるよう変更します。

if (empty($reflectionAttributes)) { 
		// Get Attributes が設定されてたいなかった場合
    throw new BadMethodCallException();
}

以上で修正が完了しました。あとは先ほどの修正を反映したコードを使い回しがしやすようトレイトにして完了になります。以下がそのコードになります。

ただしプロパティ名と同名のメソッドが既に定義されていた場合はそちらが優先されるのと、子クラスでオーバライドした場合 Get Attribute を付与しないと BadMethodCallException が発生するのでご注意ください。

trait HasGetter
{
    public function __call(string $name, array $arguments)
		{
		    if (!property_exists(self::class, $name)) {
		        if(get_parent_class() !== false && method_exists(Parent::class, '__call')) {
		            // 親クラスが存在し、__callメソッドが定義されている場合
		            return parent::__call($name, $arguments);
		        }
	        
						throw new BadMethodCallException();
		    }
    
				$reflectionProperty = new ReflectionProperty(self::class, $name);
				$reflectionAttributes = $reflectionProperty->getAttributes(Get::class);
				if (empty($reflectionAttributes)) { 
				    // Get Attributes が設定されてたいなかった場合
				    throw new BadMethodCallException();
				}
			
				return $reflectionProperty->isStatic() 
				    ? $this::${$name} 
				    : $this->{$name};
	}
}

使用方法は、先ほど定義した HasGetter トレイトを Get Attribute を使用したいクラスで use することで使用可能になります。もし親クラスで HasGetter を use していたとしても子クラスのプロパティは取得できないため、子クラスでも use する必要があります。

また、Get Attribute を使用しないクラスでは特に HasGetter を use する必要はありません。

class A {
    use HasGetter;
    #[Get] private string $aPrivate = 'A private';
    #[Get] protected string $aProtected = 'A protected';
    #[Get] public string $aPublic = 'A public';
    #[Get] protected static string $aStatic = 'A static';

		#[Get] private string $override = 'A override';
    private string $overrideNoGetInA = 'A override no get in A';
    #[Get] private string $overrideNoGetInC = 'A override no get in C';
}

class B extends A {
		private string $bPrivate = 'B private';
		protected string $bProtected = 'B protected';
		public string $bPublic = 'B public';
		protected static string $bStatic = 'B static';
}

class C extends B {
    use HasGetter;
    #[Get] private string $cPrivate = 'C private';
    #[Get] protected string $cProtected = 'C protected';
    #[Get] public string $cPublic = 'C public';
    #[Get] protected static string $cStatic = 'C static';

		#[Get] private string $override = 'C override';
    #[Get] private string $overrideNoGetInA = 'C override no get in A'; 
    private string $overrideNoGetInC = 'C override no get in C';
}

$c = new C();
var_dump($c->aPrivate());   // string(9) "A private"
var_dump($c->aProtected()); // string(11) "A protected"
var_dump($c->aPublic());    // string(8) "A public"
var_dump($c->aStatic());    // string(8) "A static"

try {
		var_dump($c->bPrivate());   // BadMethodCallException
		var_dump($c->bProtected()); // BadMethodCallException
		var_dump($c->bPublic());    // BadMethodCallException
		var_dump($c->bStatic());    // BadMethodCallException
} catch (BadMethodCallException) {}

var_dump($c->cPrivate());   // string(9) "C private"
var_dump($c->cProtected()); // string(11) "C protected"
var_dump($c->cPublic());    // string(8) "C public"
var_dump($c->cStatic());    // string(8) "C static"

var_dump($c->override());         // string(10) "C override"
var_dump($c->overrideNoGetInA()); // string(22) "C override no get in A"
try {
    var_dump($c->overrideNoGetInC()); // BadMethodCallException
} catch (BadMethodCallException) {}

最後に

いかがでしょうか?

Getter を定義したいクラスに毎回 HasGetter を use する必要はありますがプロパティ数が多い場合にはその効果を実感できるのではないでしょうか。

もしバグやもっといい方法がありましたらぜひ、以下のお問い合わせよりお知らせいただけましたら幸いです。

Ohashi

Ohashi slash forward icon Engineer

主にLaravelなどのバックエンドを中心にサーバー周りも担当しています。目標は腕周り40cm 越え。

関連記事