BEAR.Sunday Advent Calendar 2021 day14の記事です!

カルテット開発部では、大半のWebアプリケーションはSymfonyで作るのですが、小さなコマンドラインアプリケーションにはRay.DIを使っています。
今年はRay.DIと組み合わせて使えるAOPライブラリ、Ray.Aopを使ってみたので紹介します!

BEFORE

取り扱いのある各広告媒体ごとにとあるチェックを行うコマンドラインアプリケーションを作りました。

<?php

// ...

class YahooChecker implements CheckerInterface
{
    // ...
    public function check(TargetAdAccount $account, AuthenticationInterface $auth): void
    {
        if (!$auth instanceof RefreshTokenAuthentication) {
            throw new \LogicException(get_class($auth).'is not expected for Yahoo');
        }
  
        // do actual check here
    }
}

広告媒体のAPIを叩くのに必要な認証情報は媒体によって異なります。たとえば、Yahoo広告APIであればリフレッシュトークン(+ client_id, client_secret)を使う典型的なOAuth認証方式です。
CheckerInterface は全媒体に共通のinterfaceなので、checkメソッドで受け取る認証情報も全媒体に共通の AuthenticationInterface となっていますが、実際にYahoo広告のAPIコールに必要な認証情報の具象は RefreshTokenAuthentication なので最初にガード節でAuthenticationの型をチェックしています。
違う型のAuthenticationインスタンスを渡すと例外が投げられ、コマンド実行はエラーになります。

xxx@xxx myapp % php bin/console check xxx xxx

In YahooChecker.php line 16:
                                                                                        
  GoogleRefreshTokenAuthentication is not expected!  
                                                                                        

check <ad-account> <ad-account-name>

このCheckerをRay.Aopを使ってAOPに書き換えてみたいと思います!

step0. ray/aopを入れる

このCheckerアプリケーションでは ray/di を使っていたので既に ray/aop に依存しており、新たに足す必要はありませんでした。

step1. AOP用のAttributeを作る

※ 今年の前半〜開発開始したCheckerアプリケーションなので、PHP8を使っています。PHP7以下の方はAnnotationで読み替えてください。
RefreshTokenAuthentication であることを保証したいAttributeなので RefreshTokenAuthenticationRequired と名付けてみました。

<?php

#[\Attribute(\Attribute::TARGET_METHOD)]
class RefreshTokenAuthenticationRequired
{
}

step2. AOP用のMethodInterceptorを作る

メソッドの引数 $authRefreshTokenAuthentication のインスタンスでなければLogicExceptionを投げるMethodInterceptorを作りました。
ちょうどBEFOREのガード節部分ですね。

<?php

use Ray\Aop\MethodInterceptor;
use Ray\Aop\MethodInvocation;

class AuthenticationTypeBlocker implements MethodInterceptor
{
    public function invoke(MethodInvocation $invocation)
    {
        $auth = $invocation->getNamedArguments()['auth'];
        if (!$auth instanceof RefreshTokenAuthentication) {
            throw new \LogicException(get_class($auth).' is not expected!');
        }

        return $invocation->proceed();
    }
}

step3. DIモジュールでAOPを使うよう設定する

ドキュメント https://github.com/ray-di/Ray.Di#aspect-oriented-programing の通りに従来のbindの他にbindInterceptor()を追加しました。
どのクラスでも、 RefreshTokenAuthenticationRequired のAttributeがついているメソッドに対して、AuthenticationTypeBlockerで $auth 引数が RefreshTokenAuthentication のインスタンスかどうかチェックする指定です。

<?php

class YahooAppModule extends AbstractModule
{
    public function configure()
    {
        $this->bind(CheckerInterface::class)->to(YahooChecker::class);
+        $this->bindInterceptor(
+            $this->matcher->any(),
+            $this->matcher->annotatedWith(RefreshTokenAuthenticationRequired::class),
+            [AuthenticationTypeBlocker::class]
+        );
    }
}

実行してみる!

xxx@xxx myapp % php bin/console check xxx xxx

In AuthenticationTypeBlocker.php line 16:
                                                                                        
  GoogleRefreshTokenAuthentication is not expected!  
                                                                                        

check <ad-account> <ad-account-name>

例外の発生場所が In AuthenticationTypeBlocker.php line 16: と変わり、ガード節でなくInterceptorでチェックできていることがわかります。

まとめ

いままで長年(?)ray/diを使っていながら、aopは食わず嫌いをしていました。
アドベントカレンダーに登録しちゃった勢いで試してみたら、とても簡単・安全にガード節が除去できることがわかりました :relaxed: 今後はもうちょっと積極的に使ってみようと思いますー!