スタイル・エッジ技術ブログ

士業集客支援/コンサルティングのスタイル・エッジのエンジニアによるブログです。

PHPもアトリビュートでAOP!!

はじめに

こんにちは!未だにPHP8.1で登場したEnumに心躍らせているSHISOです。

さて、今回はJavaのSpringフレームワークでできるアスペクト指向プログラミング(AOP)に憧れ、PHP8.0でリリースされたアトリビュートを使用して同じようにAOPを実現させてみました。
(ちなみにPHP8.0未満であってもLaravel 5.x~8.xをお使いであれば、Laravel-Aspectというパッケージを使ってAOPできるそうなので、興味のある方は試してみてください。)

先に今回の実装サンプル一部をチラ見せすると、クラス内に個別のgetter処理を書かずとも、アトリビュートの指定だけで値参照ができるようになったりします✨

<?php
class ExampleClass extends BaseClass
{
    public function __construct(
        #[Getter] protected string $hoge,
    ){}
}
$exampleClass = new ExampleClass('fugafuga');
// protectedなプロパティなのに 'fugafuga'と出力できる!
var_dump($exampleClass->hoge);

アトリビュートとは

アトリビュートを使うと、 コンピューターが解析できる構造化されたメタデータの情報を、 コードの宣言時に埋め込むことができます。
つまり、クラス、メソッド、関数、パラメータ、プロパティ、クラス定数にアトリビュートを指定することができます。
アトリビュートで定義されたメタデータは、 実行時に リフレクションAPI を使って調べることが出来ます。
よって、アトリビュートは、 コードに直接埋め込むことが出来る、 設定のための言語とみなすことができます。

引用元|https://www.php.net/manual/ja/language.attributes.overview.php

つまり、アトリビュートとはクラスやメソッド・プロパティなどに付与できる追加情報のことです。
このアトリビュートを付与することで、本来のロジックと直接的に関係ない情報を追加できます。

各メソッドやプロパティに付与されたアトリビュートは、リフレクションAPI を使用して取得できるため、アトリビュートの有無や引数から、共通化した処理の対象として扱うか、どういった振る舞いをさせるかなどを判断させることができます。

詳しい使い方はPHPドキュメントを参照ください。

概要

今回はアトリビュートを使用し、getterとトランザクション制御を共通化(分離)してみました。

実装の流れ

  1. アトリビュートを定義
    例: #[Attribute] class Getter {}
  2. 基底クラスやトレイトに共通処理を実装
  3. マジックメソッド内にて、アトリビュートが付与されている対象にのみ(2)の処理を実行するように実装
    例: $reflectionProperty->getAttributes(Getter::class);
  4. 共通処理を付与したい関数やプロパティに、(1)で定義したアトリビュートを指定
    例: #[Getter] protected string $hoge

最終的なコード*1

getter処理を共通化する例

<?php

#[Attribute]
class Getter
{}

/** getter処理を共通化した抽象クラス */
abstract class BaseClass
{
    public function __get(string $name): mixed
    {
        // 指定されたプロパティにGetterアトリビュートが付与されている場合、プロパティの値を返す
        $reflectionProperty = new ReflectionProperty($this, $name);
        $reflectionAttributes = $reflectionProperty->getAttributes(Getter::class);
        if (!count($reflectionAttributes)) {
            return $this->{$name};
        }

        throw new Exception('アクセス不能なプロパティです。');
    }
}

class ExampleClass extends BaseClass
{
    public function __construct(
        // 参照可能にする場合は、アトリビュート#[Getter]を付与する
        #[Getter] protected string $hoge,
    ){}
}

$exampleClass = new ExampleClass('fugafuga');
  // 'fugafuga'と出力される
var_dump($exampleClass->hoge);

トランザクション処理を共通化する例

<?php

#[Attribute]
class Transactional
{
    public function __construct(public readonly bool $needTransaction){}
}

/** 仮DB処理クラス */
class DB
{
    public static function beginTransaction(){}

    public static function commit(){}

    public static function rollback(){}
}

/** トランザクション処理を共通化した抽象クラス */
abstract class BaseClass
{
    public function __invoke(mixed ...$args): mixed
    {
        if (! (method_exists($this, 'handle') && (new \ReflectionMethod($this, 'handle'))->isProtected())) {
            throw new Exception('アクセス権がprotectedのhandleメソッドを定義してください。');
        }

        // Transactional属性を取得
        $handleMethod = new ReflectionMethod($this, 'handle');
        $transactionalAttributes = $handleMethod->getAttributes(Transactional::class);
        if (!count($transactionalAttributes)) {
            throw new Exception('Transactional属性にて、トランザクションの要否を指定してください。');
        }

        $needTransaction = $transactionalAttributes[0]->newInstance()->needTransaction;
        if ($needTransaction === false) {
            return $handleMethod->invoke($this, ...$args);
        }

        try {
            DB::beginTransaction();
            $result = $handleMethod->invoke($this, ...$args);
            DB::commit();
            return $result;
        } catch (Exception $e) {
            DB::rollback();
            throw $e;
        }
    }
}

class ExampleClass extends BaseClass
{
     // トランザクション制御したい場合は、Transactional属性をTrueに
    #[Transactional(true)]
    protected function handle() {}
}

$exampleClass = new ExampleClass();
// トランザクション制御されながらhandle処理が実行される
$exampleClass();

解説

getter処理の解説

1. まずは使用するアトリビュートのクラスを宣言

<?php
#[Attribute]
class Getter
{}

2. Getterアトリビュートの有無でgetter処理を付与する処理を実装

<?php
abstract class BaseClass
{
     // 解説1:アクセス不能(protected または private)または存在しないプロパティを呼ばれた時に実行される「マジックメソッド__get」に処理を実装
    public function __get(string $name): mixed
    {
        // 解説2:リフレクションAPIを使用し、クラスから指定された名前のプロパティを取得
        $reflectionProperty = new ReflectionProperty($this, $name);

        // 解説3:プロパティに付与されているアトリビュート情報のうち、Getterアトリビュートを取得
        $reflectionAttributes = $reflectionProperty->getAttributes(Getter::class);

        // 解説4:Getterアトリビュートが付与されていれば、プロパティの値を返す
        if (!count($reflectionAttributes)) {
            return $this->{$name};
        }

        throw new Exception('アクセス不能なプロパティです。');
    }
}

参考|__get

3. 実際にgetter処理を追加したいプロパティにアトリビュートを設定

<?php
class ExampleClass extends BaseClass
{
    public function __construct(
        // 解説1:クラス内からしかアクセスできないprotected/privateプロパティに#[Getter]アトリビュートを設定
        #[Getter] protected string $hoge,
    ){}

//    // 解説2:本来ならば下記のようなgetter処理が必要
//    public function getHoge(): string
//    {
//        return $this->hoge;
//    }   
}

4. 実行するとエラーにならず値が取得できる

<?php
$exampleClass = new ExampleClass('fugafuga');
 // 'fugafuga'と出力される
var_dump($exampleClass->hoge);

トランザクション処理の解説

1. まずは使用するアトリビュートのクラスを宣言

<?php
#[Attribute]
class Transactional
{
    // 解説1:今回はトランザクション制御の有無を引数で指定できるようにする
    public function __construct(public readonly bool $needTransaction){}
}

2. アトリビュートによるトランザクション制御処理の実装
※ getterの時と同じ解説は省略しています。

<?php
abstract class BaseClass
{
    public function __invoke(mixed ...$args): mixed
    {
        // 解説1:abstractでhandleメソッドの実装を強制すると、引数が自由に指定できないので、こちらの処理で強制
        if (! (method_exists($this, 'handle') && (new \ReflectionMethod($this, 'handle'))->isProtected())) {
            throw new Exception('アクセス権がprotectedのhandleメソッドを定義してください。');
        }

        // 解説2:Transactional属性を取得
        $handleMethod = new ReflectionMethod($this, 'handle');
        $transactionalAttributeArray = $handleMethod->getAttributes(Transactional::class);
        if (!count($transactionalAttributeArray)) {
            throw new Exception('Transactional属性にて、トランザクションの要否を指定してください。');
        }

        // 解説3:Transactional属性のフラグでトランザクション制御
        $needTransaction = $transactionalAttributes[0]->newInstance()->needTransaction;
        if ($needTransaction === false) {
            return $handleMethod->invoke($this, ...$args);
        }

        try {
            DB::beginTransaction();
            $result = $handleMethod->invoke($this, ...$args);
            DB::commit();
            return $result;
        } catch (Exception $e) {
            DB::rollback();
            throw $e;
        }
        $transactionalAttribute = $transactionalAttributeArray[0]->newInstance();
    }
}

3. 具象クラスのhandleメソッドにアトリビュート付与し、実行するとトランザクション制御される。

<?php
class ExampleClass extends BaseClass
{
    // 解説1:Transactional属性とトランザクション制御の有無フラグを引数として設定
    #[Transactional(true)]
    protected function handle() {}
}

$exampleClass = new ExampleClass();
// トランザクション制御されながらhandle処理が実行される
$exampleClass();

終わりに

いかがでしたでしょうか?
アトリビュートを利用したことで、getter処理をプロパティの数だけ書いたり、同じようなトランザクション制御を何回も書く必要がなくなりました。
アトリビュートはあくまで追加情報 ではありますが、今回のように工夫して利用することで、横断的な処理をメイン実装から剥がせたりするので、実務では大いに活躍できそうです。
ぜひ皆さんもアトリビュートを使ってAOPしてみてください!

☆スタイル・エッジ・グループでは、一緒に働く仲間を募集しています☆
もし興味を持っていただけましたら、採用サイト↓も覗いてみてください!
recruit.styleedge-labo.co.jp

参考資料

*1:実行環境:PHP 8.0